From 61d5e3c6faf7f096a3f5e0f65aa612c649081193 Mon Sep 17 00:00:00 2001 From: Nora Date: Fri, 3 Jul 2026 15:32:55 -0600 Subject: [PATCH] polish(water-log/settings): Apple HIG refinements to the Smartsheet surface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Layer HIG polish on top of the Field Almanac visual identity — no layout change, just quality. Per-element polish: - Inputs: focus-visible ring (4px at 15% opacity), subtle inner shadow, 44pt min tap target, motion-safe transition. - Selects: same focus ring + min height + transition. - Toggles: Apple spring curve (cubic-bezier 0.32,0.72,0,1), 26×46 track, 20px thumb, 200ms color, 200ms spring transform, focus ring, active:scale-0.97. - Buttons: focus-visible ring, active:scale-0.97 press state, 48pt primary CTA, 40pt secondary, refined hover with subtle lift shadow. - Status banner: inline SVG icon (filled circle + check or info-i), polite/assertive aria-live, slide-down entrance. - Test Connection result: HIG-style icon (filled green/red circle + stroke check/x), refined hierarchy. - Backfill result + error: same HIG pattern as status banner. - Modal: backdrop-blur-[6px], spring entrance (sheet-up 220ms), rounded-t-2xl on mobile (sheet-style) and rounded-2xl on sm+. - All animations: motion-safe: + a prefers-reduced-motion block in globals.css that disables them. Type system: kept the existing --font-display (Field Almanac serif identity) for headings; system-ui for body remains the fallback chain in the existing --font-sans token. New keyframes in globals.css: smartsheet-fade-in, smartsheet-sheet-up, smartsheet-slide-down. All short (180–220ms) to feel responsive. Also: button label 'Save Settings' → 'Save changes' (sentence case, matches HIG register) on the parent settings page and the Smartsheet card. --- src/app/admin/water-log/settings/page.tsx | 112 +++++- src/app/globals.css | 26 ++ .../water-log/SmartsheetIntegrationCard.tsx | 369 +++++++++++++----- 3 files changed, 399 insertions(+), 108 deletions(-) diff --git a/src/app/admin/water-log/settings/page.tsx b/src/app/admin/water-log/settings/page.tsx index d7d6b7f..c1e1b6f 100644 --- a/src/app/admin/water-log/settings/page.tsx +++ b/src/app/admin/water-log/settings/page.tsx @@ -261,13 +261,44 @@ export default function WaterLogSettingsPage() {
{message && (
- {message.text} + + {message.type === "success" ? ( + + + + ) : ( + + + + )} + +

{message.text}

)} @@ -317,9 +348,22 @@ export default function WaterLogSettingsPage() { type="button" onClick={handleRegenerate} disabled={regenerating} - className="shrink-0 rounded-lg border border-[#1a4d2e] bg-[#1a4d2e] px-4 py-2 text-sm font-semibold text-white transition hover:bg-[#143d24] disabled:opacity-50" + className="inline-flex min-h-[40px] shrink-0 items-center justify-center rounded-lg border border-[#1a4d2e] bg-[#1a4d2e] px-4 py-2 + text-[14px] font-semibold text-white + motion-safe:transition-[background-color,transform] motion-safe:duration-150 motion-safe:ease-out + hover:bg-[#143d24] + focus:outline-none focus-visible:ring-4 focus-visible:ring-[#1a4d2e]/30 + active:scale-[0.97] active:bg-[#0f2e1c] + disabled:opacity-50 disabled:cursor-not-allowed" > - {regenerating ? "Generating…" : "Regenerate PIN"} + {regenerating ? ( + + + Generating… + + ) : ( + "Regenerate PIN" + )} )} @@ -393,7 +437,10 @@ export default function WaterLogSettingsPage() { dispatch({ type: "SET_ALERT_PHONE", value: e.target.value }) } placeholder="+13035551234" - className="mt-1 w-full rounded-lg border border-[#d4d9d3] bg-white px-4 py-2.5 text-base outline-none focus:border-[#1a4d2e] focus:ring-1 focus:ring-[#1a4d2e]" + className="mt-1 block min-h-[44px] w-full rounded-lg border border-[#d4d9d3] bg-white px-4 py-2.5 + text-[14px] text-[#1d1d1f] placeholder:text-[#a3a5a0] + outline-none motion-safe:transition-[border-color,box-shadow] motion-safe:duration-150 + focus:border-[#1a4d2e] focus:ring-4 focus:ring-[#1a4d2e]/15" autoComplete="tel" />

@@ -408,9 +455,23 @@ export default function WaterLogSettingsPage() {

@@ -450,10 +511,16 @@ function Card({ children: React.ReactNode; }) { return ( -
-
-

{title}

- {subtitle &&

{subtitle}

} +
+
+

+ {title} +

+ {subtitle && ( +

+ {subtitle} +

+ )}
{children}
@@ -483,14 +550,17 @@ function ToggleRow({ aria-checked={value} aria-label={label} onClick={() => onChange(!value)} - className={`relative inline-flex h-6 w-11 shrink-0 items-center rounded-full transition-colors ${ - value ? "bg-[#1a4d2e]" : "bg-[#d4d9d3]" - }`} + className={`group relative inline-flex h-[26px] w-[46px] shrink-0 items-center rounded-full + motion-safe:transition-colors motion-safe:duration-200 + focus:outline-none focus-visible:ring-4 focus-visible:ring-[#1a4d2e]/25 focus-visible:ring-offset-2 focus-visible:ring-offset-white + active:scale-[0.97] motion-safe:transition-transform motion-safe:ease-out + ${value ? "bg-[#1a4d2e]" : "bg-[#d4d9d3]"}`} > diff --git a/src/app/globals.css b/src/app/globals.css index 77be9b4..98fefef 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1104,6 +1104,32 @@ select:-webkit-autofill:focus { .atelier-backdrop { animation: atelier-backdrop 140ms ease-out both; } + +/* Smartsheet integration polish — Apple HIG-inspired micro-animations. + * Kept short (180–220ms) and single-property to feel responsive. The + * global prefers-reduced-motion block further down disables all of + * these for users who request reduced motion. */ +@keyframes smartsheet-fade-in { + from { opacity: 0; } + to { opacity: 1; } +} +@keyframes smartsheet-sheet-up { + from { opacity: 0; transform: translateY(8px) scale(0.985); } + to { opacity: 1; transform: translateY(0) scale(1); } +} +@keyframes smartsheet-slide-down { + from { opacity: 0; transform: translateY(-4px); } + to { opacity: 1; transform: translateY(0); } +} +.smartsheet-fade-in { + animation: smartsheet-fade-in 180ms ease-out both; +} +.smartsheet-sheet-up { + animation: smartsheet-sheet-up 220ms cubic-bezier(0.32, 0.72, 0, 1) both; +} +.smartsheet-slide-down { + animation: smartsheet-slide-down 180ms cubic-bezier(0.32, 0.72, 0, 1) both; +} .atelier-stagger > * { animation: atelier-stagger 180ms ease-out both; /* No more 80-440ms cumulative delays — items appear together, not as a parade. */ diff --git a/src/components/admin/water-log/SmartsheetIntegrationCard.tsx b/src/components/admin/water-log/SmartsheetIntegrationCard.tsx index 367667d..68299cb 100644 --- a/src/components/admin/water-log/SmartsheetIntegrationCard.tsx +++ b/src/components/admin/water-log/SmartsheetIntegrationCard.tsx @@ -402,13 +402,44 @@ export default function SmartsheetIntegrationCard({
{state.message && (
- {state.message.text} + + {state.message.type === "success" ? ( + + + + ) : ( + + + + )} + +

{state.message.text}

)} @@ -431,7 +462,12 @@ export default function SmartsheetIntegrationCard({ @@ -453,7 +489,11 @@ export default function SmartsheetIntegrationCard({ dispatch({ type: "SET_SHEET_ID", value: e.target.value }) } placeholder="https://app.smartsheet.com/sheets/AbCdEfGhIjKlMn or 123456789012345" - className="w-full rounded-lg border border-[#d4d9d3] bg-white px-4 py-2.5 font-mono text-sm outline-none focus:border-[#1a4d2e] focus:ring-1 focus:ring-[#1a4d2e]" + className="block min-h-[44px] w-full rounded-lg border border-[#d4d9d3] bg-white px-4 py-2.5 + font-mono text-[14px] text-[#1d1d1f] placeholder:text-[#a3a5a0] + shadow-[inset_0_1px_0_rgba(15,23,18,0.02)] + outline-none motion-safe:transition-[border-color,box-shadow] motion-safe:duration-150 + focus:border-[#1a4d2e] focus:ring-4 focus:ring-[#1a4d2e]/15" autoComplete="off" />

@@ -484,13 +524,22 @@ export default function SmartsheetIntegrationCard({ ? `Leave blank to keep the saved token (${state.savedConfig.maskedToken ?? "••••"})` : "Paste your Smartsheet API token" } - className="flex-1 rounded-lg border border-[#d4d9d3] bg-white px-4 py-2.5 font-mono text-sm outline-none focus:border-[#1a4d2e] focus:ring-1 focus:ring-[#1a4d2e]" + className="min-h-[44px] flex-1 rounded-lg border border-[#d4d9d3] bg-white px-4 py-2.5 + font-mono text-[14px] text-[#1d1d1f] placeholder:text-[#a3a5a0] + shadow-[inset_0_1px_0_rgba(15,23,18,0.02)] + outline-none motion-safe:transition-[border-color,box-shadow] motion-safe:duration-150 + focus:border-[#1a4d2e] focus:ring-4 focus:ring-[#1a4d2e]/15" autoComplete="off" /> @@ -535,11 +597,15 @@ export default function SmartsheetIntegrationCard({ {(Object.keys(FREQUENCY_LABELS) as SmartsheetFrequency[]).map((f) => ( @@ -608,7 +674,10 @@ export default function SmartsheetIntegrationCard({ value: e.target.value, }) } - className="flex-1 rounded-lg border border-[#d4d9d3] bg-white px-3 py-2 text-sm outline-none focus:border-[#1a4d2e] focus:ring-1 focus:ring-[#1a4d2e]" + className="min-h-[40px] flex-1 rounded-lg border border-[#d4d9d3] bg-white px-3 py-2 + text-[14px] text-[#1d1d1f] + outline-none motion-safe:transition-[border-color,box-shadow] motion-safe:duration-150 + focus:border-[#1a4d2e] focus:ring-4 focus:ring-[#1a4d2e]/15" >

-
+
+
-

{title}

- {subtitle &&

{subtitle}

} +

+ {title} +

+ {subtitle && ( +

+ {subtitle} +

+ )}
{action &&
{action}
}
@@ -759,11 +851,15 @@ function ToggleRow({ onChange: (v: boolean) => void; }) { return ( -
+
-

{label}

+

+ {label} +

{description && ( -

{description}

+

+ {description} +

)}
@@ -789,20 +888,48 @@ function ToggleRow({ function TestResultPanel({ test }: { test: TestState }) { if (test.state === "idle") return null; if (test.state === "loading") { - return Testing…; + return ( + + + Testing connection… + + ); } if (test.state === "ok") { return ( -
-

- ✓ Connected to “{test.sheetName}” ({test.columnCount} columns) +

+ + + + + +

+ Connected to “{test.sheetName}”{" "} + — {test.columnCount} columns loaded

); } return ( -
-

✗ {test.message}

+
+ + + + + +

{test.message}

); } @@ -848,7 +975,11 @@ function BackfillCard({ onChange={(e) => onRangeChange(e.target.value as "all" | "30d" | "7d") } - className="rounded-lg border border-[#d4d9d3] bg-white px-3 py-2 text-sm outline-none focus:border-[#1a4d2e] focus:ring-1 focus:ring-[#1a4d2e]" + className="min-h-[40px] rounded-lg border border-[#d4d9d3] bg-white px-3 py-2 + text-[14px] text-[#1d1d1f] + outline-none motion-safe:transition-[border-color,box-shadow] motion-safe:duration-150 + focus:border-[#1a4d2e] focus:ring-4 focus:ring-[#1a4d2e]/15 + disabled:opacity-50" disabled={state.running} > @@ -859,9 +990,22 @@ function BackfillCard({ type="button" onClick={onRun} disabled={state.running} - className="rounded-lg border border-[#1a4d2e] bg-white px-4 py-2 text-sm font-semibold text-[#1a4d2e] transition hover:bg-[#1a4d2e] hover:text-white disabled:opacity-50" + className="inline-flex min-h-[40px] items-center justify-center rounded-lg border border-[#1a4d2e] bg-white px-4 py-2 + text-[14px] font-semibold text-[#1a4d2e] + motion-safe:transition-[background-color,color,transform] motion-safe:duration-150 motion-safe:ease-out + hover:bg-[#1a4d2e] hover:text-white + focus:outline-none focus-visible:ring-4 focus-visible:ring-[#1a4d2e]/25 + active:scale-[0.97] + disabled:opacity-50 disabled:cursor-not-allowed" > - {state.running ? "Running backfill…" : "Run backfill"} + {state.running ? ( + + + Running backfill… + + ) : ( + "Run backfill" + )}
@@ -873,50 +1017,83 @@ function BackfillCard({ )} {showError && ( -
-

✗ {result.error}

+
+ + + + + +

+ {result.error} +

)} {showResult && result && ( -
-

- ✓ Found {result.considered}{" "} - {result.considered === 1 ? "entry" : "entries"} matching the - range - {result.considered > 0 && ( - <> - {" "} - ({result.newlyQueued} new, {result.alreadyQueued} already - queued). - - )} - . -

- {(result.drained.synced + - result.drained.skipped + - result.drained.failed > - 0 || result.drained.remaining > 0) && ( -

- Inline drain: {result.drained.synced} synced /{" "} - {result.drained.skipped} skipped / {result.drained.failed}{" "} - failed. - {result.drained.remaining > 0 && ( +

+ + + + + +
+

+ Found {result.considered}{" "} + {result.considered === 1 ? "entry" : "entries"} matching the + range + {result.considered > 0 && ( <> {" "} - - {result.drained.remaining} still queued - {" "} - — they'll drain on the next cron tick (within 15 - minutes). + ({result.newlyQueued} new, {result.alreadyQueued} already + queued). )} - {result.drained.remaining === 0 && - result.considered > 0 && ( - <> All entries are now in Smartsheet. - )}

- )} + {(result.drained.synced + + result.drained.skipped + + result.drained.failed > + 0 || result.drained.remaining > 0) && ( +

+ Inline drain: {result.drained.synced} synced /{" "} + {result.drained.skipped} skipped / {result.drained.failed}{" "} + failed. + {result.drained.remaining > 0 && ( + <> + {" "} + + {result.drained.remaining} still queued + {" "} + — they'll drain on the next cron tick (within 15 + minutes). + + )} + {result.drained.remaining === 0 && + result.considered > 0 && ( + <> All entries are now in Smartsheet. + )} +

+ )} +
)}
@@ -998,7 +1175,8 @@ const FIELD_SCHEMA: Array<{ function SetupInstructionsModal({ onClose }: { onClose: () => void }) { return (
void }) { if (e.target === e.currentTarget) onClose(); }} > -
+
{/* Header */} -
+

§ 05.1 — Setup

Connect a Smartsheet @@ -1026,13 +1209,19 @@ function SetupInstructionsModal({ onClose }: { onClose: () => void }) { type="button" onClick={onClose} aria-label="Close" - className="-mr-1 -mt-1 rounded-md p-2 text-[#5a5d5a] hover:bg-[#f4f1e8] hover:text-[#1d1d1f]" + className="-mr-2 -mt-1 inline-flex h-9 w-9 items-center justify-center rounded-full + text-[#5a5d5a] + motion-safe:transition-colors motion-safe:duration-150 + hover:bg-[#f0ebde] hover:text-[#1d1d1f] + focus:outline-none focus-visible:ring-4 focus-visible:ring-[#1a4d2e]/25 + active:scale-95 motion-safe:transition-transform motion-safe:ease-out" > void }) {

{/* Footer */} -