affora 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,583 @@
1
+ import type React from 'react';
2
+ import { useId, useState } from 'react';
3
+
4
+ /**
5
+ * SettingsForm — a notifications settings form.
6
+ *
7
+ * Authored once and rendered under every Affora theme. Every stylistic decision
8
+ * resolves to a design token (var(--x)); the semantic substrate never changes:
9
+ * a real <form> / <fieldset> / <legend>, real <label for>, a real <input>,
10
+ * a real <select>, a real radio group, and a switch as <button role="switch">.
11
+ *
12
+ * onCommit("saved") fires on submit (preventDefault).
13
+ */
14
+ const API_KEY = 'CFG-7';
15
+
16
+ export const SettingsForm: React.FC<{ onCommit?: (v: string) => void }> = ({ onCommit }) => {
17
+ const [keyEntry, setKeyEntry] = useState('')
18
+ const uid = useId();
19
+ const id = (s: string) => `${uid}-${s}`;
20
+
21
+ const [displayName, setDisplayName] = useState('Ada Lovelace');
22
+ const [timezone, setTimezone] = useState('europe-london');
23
+ const [digests, setDigests] = useState(true);
24
+ const [theme, setTheme] = useState<'system' | 'light' | 'dark'>('system');
25
+ const [saved, setSaved] = useState(false);
26
+
27
+ const themes: { value: 'system' | 'light' | 'dark'; label: string; hint: string; icon: React.ReactNode }[] = [
28
+ { value: 'system', label: 'System', hint: 'Match device', icon: <IconMonitor /> },
29
+ { value: 'light', label: 'Light', hint: 'Always bright', icon: <IconSun /> },
30
+ { value: 'dark', label: 'Dark', hint: 'Always dim', icon: <IconMoon /> },
31
+ ];
32
+
33
+ const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
34
+ e.preventDefault();
35
+ setSaved(true);
36
+ onCommit?.(keyEntry.trim());
37
+ };
38
+
39
+ // Computed type scale off the theme's base + ratio.
40
+ const t = {
41
+ xs: 'calc(var(--text-base) / var(--scale-ratio))',
42
+ sm: 'calc(var(--text-base) * 0.92)',
43
+ base: 'var(--text-base)',
44
+ lg: 'calc(var(--text-base) * var(--scale-ratio))',
45
+ xl: 'calc(var(--text-base) * var(--scale-ratio) * var(--scale-ratio))',
46
+ };
47
+
48
+ return (
49
+ <div className="sff">
50
+ <style>{css}</style>
51
+
52
+ <form className="sff-card" onSubmit={handleSubmit} onChange={() => setSaved(false)} noValidate>
53
+ <header className="sff-head">
54
+ <div className="sff-eyebrow">Account · Preferences</div>
55
+ <h2 className="sff-title" style={{ fontSize: t.xl }}>
56
+ Notification settings
57
+ </h2>
58
+ <p className="sff-lede" style={{ fontSize: t.sm }}>
59
+ Choose how Aperture reaches you and how the workspace looks. Changes apply the moment you save.
60
+ </p>
61
+ </header>
62
+
63
+ <fieldset className="sff-fieldset">
64
+ <legend className="sff-legend" style={{ fontSize: t.xs }}>
65
+ Notifications
66
+ </legend>
67
+
68
+ {/* API key — the task's fact, present as DOM text (never revealed
69
+ behind a portal, which is the anti-pattern this arm replaces) --- */}
70
+ <div className="sff-field">
71
+ <span className="sff-label" style={{ fontSize: t.sm }}>Current API key</span>
72
+ <p className="sff-apikey" style={{ fontSize: t.base }}>{API_KEY}</p>
73
+ <label className="sff-label" htmlFor={id('apikey')} style={{ fontSize: t.sm }}>
74
+ API key
75
+ </label>
76
+ <input
77
+ id={id('apikey')}
78
+ name="apiKey"
79
+ type="text"
80
+ className="sff-input"
81
+ value={keyEntry}
82
+ onChange={(e) => setKeyEntry(e.target.value)}
83
+ autoComplete="off"
84
+ placeholder="Re-enter the key to confirm"
85
+ style={{ fontSize: t.base }}
86
+ />
87
+ <p className="sff-help" style={{ fontSize: t.xs }}>
88
+ Used by the CLI and CI. Re-enter it to confirm the active key.
89
+ </p>
90
+ </div>
91
+
92
+ {/* Display name -------------------------------------------------- */}
93
+ <div className="sff-field">
94
+ <label className="sff-label" htmlFor={id('name')} style={{ fontSize: t.sm }}>
95
+ Display name
96
+ </label>
97
+ <input
98
+ id={id('name')}
99
+ name="displayName"
100
+ type="text"
101
+ className="sff-input"
102
+ value={displayName}
103
+ onChange={(e) => setDisplayName(e.target.value)}
104
+ autoComplete="name"
105
+ placeholder="e.g. Ada Lovelace"
106
+ style={{ fontSize: t.base }}
107
+ />
108
+ <p className="sff-help" style={{ fontSize: t.xs }}>
109
+ Shown on digests and in mentions across your team.
110
+ </p>
111
+ </div>
112
+
113
+ {/* Timezone ------------------------------------------------------ */}
114
+ <div className="sff-field">
115
+ <label className="sff-label" htmlFor={id('tz')} style={{ fontSize: t.sm }}>
116
+ Timezone
117
+ </label>
118
+ <div className="sff-selectwrap">
119
+ <select
120
+ id={id('tz')}
121
+ name="timezone"
122
+ className="sff-select"
123
+ value={timezone}
124
+ onChange={(e) => setTimezone(e.target.value)}
125
+ style={{ fontSize: t.base }}
126
+ >
127
+ <option value="america-los_angeles">Pacific — Los Angeles (GMT−8)</option>
128
+ <option value="america-new_york">Eastern — New York (GMT−5)</option>
129
+ <option value="europe-london">GMT — London (GMT+0)</option>
130
+ <option value="europe-berlin">Central European — Berlin (GMT+1)</option>
131
+ <option value="asia-singapore">Singapore (GMT+8)</option>
132
+ <option value="asia-tokyo">Japan — Tokyo (GMT+9)</option>
133
+ </select>
134
+ <span className="sff-caret" aria-hidden="true">
135
+ <IconChevron />
136
+ </span>
137
+ </div>
138
+ <p className="sff-help" style={{ fontSize: t.xs }}>
139
+ Daily digests are sent at 8:00 AM in this timezone.
140
+ </p>
141
+ </div>
142
+
143
+ {/* Email digests switch ----------------------------------------- */}
144
+ <div className="sff-field sff-switchrow">
145
+ <label className="sff-label" htmlFor={id('digests')} style={{ fontSize: t.sm }}>
146
+ Email digests
147
+ <span className="sff-sublabel" style={{ fontSize: t.xs }}>
148
+ A once-a-day summary instead of live emails
149
+ </span>
150
+ </label>
151
+ <button
152
+ id={id('digests')}
153
+ type="button"
154
+ role="switch"
155
+ aria-checked={digests}
156
+ className="sff-switch"
157
+ data-on={digests}
158
+ onClick={() => {
159
+ setDigests((v) => !v);
160
+ setSaved(false);
161
+ }}
162
+ >
163
+ <span className="sff-switch-track" aria-hidden="true">
164
+ <span className="sff-switch-thumb" />
165
+ </span>
166
+ <span className="sff-switch-state" style={{ fontSize: t.xs }}>
167
+ {digests ? 'On' : 'Off'}
168
+ </span>
169
+ </button>
170
+ </div>
171
+
172
+ {/* Theme radio group -------------------------------------------- */}
173
+ <div className="sff-field">
174
+ <span className="sff-label" style={{ fontSize: t.sm }} id={id('theme-label')}>
175
+ Theme
176
+ </span>
177
+ <div className="sff-radiogroup" role="radiogroup" aria-labelledby={id('theme-label')}>
178
+ {themes.map((opt) => {
179
+ const checked = theme === opt.value;
180
+ return (
181
+ <label key={opt.value} className="sff-radio" data-checked={checked} htmlFor={id(`theme-${opt.value}`)}>
182
+ <input
183
+ id={id(`theme-${opt.value}`)}
184
+ type="radio"
185
+ name="theme"
186
+ value={opt.value}
187
+ aria-label={`${opt.label} — ${opt.hint}`}
188
+ checked={checked}
189
+ onChange={() => {
190
+ setTheme(opt.value);
191
+ setSaved(false);
192
+ }}
193
+ className="sff-radio-input"
194
+ />
195
+ <span className="sff-radio-icon" aria-hidden="true">
196
+ {opt.icon}
197
+ </span>
198
+ <span className="sff-radio-body">
199
+ <span className="sff-radio-label" style={{ fontSize: t.sm }}>
200
+ {opt.label}
201
+ </span>
202
+ <span className="sff-radio-hint" style={{ fontSize: t.xs }}>
203
+ {opt.hint}
204
+ </span>
205
+ </span>
206
+ <span className="sff-radio-dot" aria-hidden="true" />
207
+ </label>
208
+ );
209
+ })}
210
+ </div>
211
+ </div>
212
+ </fieldset>
213
+
214
+ <footer className="sff-foot">
215
+ <p className="sff-savedhint" data-shown={saved} aria-live="polite" style={{ fontSize: t.sm }}>
216
+ <IconCheck />
217
+ Preferences saved
218
+ </p>
219
+ <button type="submit" className="sff-submit" style={{ fontSize: t.sm }}>
220
+ Submit
221
+ </button>
222
+ </footer>
223
+ </form>
224
+ </div>
225
+ );
226
+ };
227
+
228
+ /* ---- inline icons (currentColor) --------------------------------------- */
229
+ const IconChevron = () => (
230
+ <svg width="1em" height="1em" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round">
231
+ <path d="M4 6l4 4 4-4" />
232
+ </svg>
233
+ );
234
+ const IconCheck = () => (
235
+ <svg width="1em" height="1em" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
236
+ <path d="M3 8.5l3.2 3.2L13 5" />
237
+ </svg>
238
+ );
239
+ const IconMonitor = () => (
240
+ <svg width="1em" height="1em" viewBox="0 0 20 20" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round">
241
+ <rect x="2.5" y="3.5" width="15" height="10" rx="1.5" />
242
+ <path d="M7 16.5h6M10 13.5v3" />
243
+ </svg>
244
+ );
245
+ const IconSun = () => (
246
+ <svg width="1em" height="1em" viewBox="0 0 20 20" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round">
247
+ <circle cx="10" cy="10" r="3.4" />
248
+ <path d="M10 2.5v2M10 15.5v2M2.5 10h2M15.5 10h2M4.7 4.7l1.4 1.4M13.9 13.9l1.4 1.4M15.3 4.7l-1.4 1.4M6.1 13.9l-1.4 1.4" />
249
+ </svg>
250
+ );
251
+ const IconMoon = () => (
252
+ <svg width="1em" height="1em" viewBox="0 0 20 20" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round">
253
+ <path d="M16 11.2A6.5 6.5 0 018.8 4a6.5 6.5 0 100 12 6.5 6.5 0 007.2-4.8z" />
254
+ </svg>
255
+ );
256
+
257
+ /* ---- scoped styles ----------------------------------------------------- */
258
+ export const css = `
259
+ .sff {
260
+ font-family: var(--font);
261
+ color: var(--fg);
262
+ line-height: var(--leading);
263
+ letter-spacing: var(--tracking-tight);
264
+ -webkit-font-smoothing: antialiased;
265
+ display: flex;
266
+ justify-content: center;
267
+ }
268
+ .sff *, .sff *::before, .sff *::after { box-sizing: border-box; }
269
+
270
+ .sff-card {
271
+ width: 100%;
272
+ max-width: var(--lay-width, 30rem);
273
+ background: var(--glass-bg, var(--surface));
274
+ backdrop-filter: var(--glass-blur, none);
275
+ -webkit-backdrop-filter: var(--glass-blur, none);
276
+ border: 1px solid var(--glass-border, var(--border));
277
+ border-radius: var(--radius-lg);
278
+ box-shadow: var(--shadow-lg);
279
+ padding: var(--pad-loose);
280
+ display: flex;
281
+ flex-direction: column;
282
+ gap: var(--pad-loose);
283
+ }
284
+
285
+ /* header ------------------------------------------------------ */
286
+ .sff-head { display: flex; flex-direction: column; gap: calc(var(--gap) * 0.4); }
287
+ .sff-eyebrow {
288
+ font-family: var(--font-mono);
289
+ font-size: calc(var(--text-base) / var(--scale-ratio) / var(--scale-ratio));
290
+ text-transform: uppercase;
291
+ letter-spacing: 0.14em;
292
+ font-weight: var(--weight-medium);
293
+ color: var(--accent);
294
+ }
295
+ .sff-title {
296
+ font-family: var(--font-display);
297
+ font-weight: var(--weight-display);
298
+ line-height: 1.12;
299
+ letter-spacing: var(--tracking-tight);
300
+ margin: 0;
301
+ color: var(--fg);
302
+ }
303
+ .sff-lede { margin: 0; color: var(--fg-muted); max-width: 42ch; }
304
+
305
+ /* fieldset ---------------------------------------------------- */
306
+ .sff-fieldset {
307
+ border: 0; margin: 0; padding: 0; min-inline-size: 0;
308
+ display: flex; flex-direction: column; gap: var(--pad-loose);
309
+ }
310
+ .sff-legend {
311
+ padding: 0 0 var(--pad-tight) 0;
312
+ width: 100%;
313
+ font-family: var(--font-mono);
314
+ text-transform: uppercase;
315
+ letter-spacing: 0.12em;
316
+ font-weight: var(--weight-bold);
317
+ color: var(--fg-subtle);
318
+ border-bottom: 1px solid var(--border);
319
+ }
320
+
321
+ .sff-field { display: flex; flex-direction: column; gap: calc(var(--gap) * 0.5); }
322
+ /* saas/dashboard layouts put the label beside its control instead of above */
323
+ [data-layout="saas"] .sff-field,
324
+ [data-layout="dashboard"] .sff-field {
325
+ display: grid;
326
+ grid-template-columns: minmax(6rem, 34%) 1fr;
327
+ align-items: baseline;
328
+ column-gap: var(--pad);
329
+ }
330
+ [data-layout="saas"] .sff-field > .sff-help,
331
+ [data-layout="dashboard"] .sff-field > .sff-help { grid-column: 2; }
332
+ .sff-label {
333
+ font-weight: var(--weight-medium);
334
+ color: var(--fg);
335
+ display: flex;
336
+ flex-direction: column;
337
+ gap: 2px;
338
+ }
339
+ .sff-sublabel { font-weight: var(--weight-normal); color: var(--fg-muted); letter-spacing: 0; }
340
+ .sff-apikey {
341
+ margin: 0;
342
+ font-family: var(--font-mono);
343
+ font-variant-numeric: tabular-nums;
344
+ letter-spacing: .02em;
345
+ color: var(--fg);
346
+ }
347
+ .sff-help { margin: 0; color: var(--fg-subtle); letter-spacing: 0; }
348
+
349
+ /* text input -------------------------------------------------- */
350
+ .sff-input {
351
+ font-family: inherit;
352
+ font-weight: var(--weight-normal);
353
+ color: var(--fg);
354
+ background: var(--bg);
355
+ border: 1px solid var(--border-strong);
356
+ border-radius: var(--radius);
357
+ padding: var(--pad-tight) var(--pad);
358
+ width: 100%;
359
+ transition: border-color var(--dur) var(--ease), box-shadow var(--dur) var(--ease), background var(--dur) var(--ease);
360
+ }
361
+ .sff-input::placeholder { color: var(--fg-subtle); }
362
+ .sff-input:hover { border-color: var(--fg-subtle); }
363
+ .sff-input:focus-visible {
364
+ outline: none;
365
+ border-color: var(--accent);
366
+ box-shadow: 0 0 0 3px var(--ring);
367
+ }
368
+
369
+ /* select ------------------------------------------------------ */
370
+ .sff-selectwrap { position: relative; display: block; }
371
+ .sff-select {
372
+ appearance: none; -webkit-appearance: none;
373
+ font-family: inherit;
374
+ font-weight: var(--weight-normal);
375
+ color: var(--fg);
376
+ background: var(--bg);
377
+ border: 1px solid var(--border-strong);
378
+ border-radius: var(--radius);
379
+ padding: var(--pad-tight) calc(var(--pad) * 2.4) var(--pad-tight) var(--pad);
380
+ width: 100%;
381
+ cursor: pointer;
382
+ transition: border-color var(--dur) var(--ease), box-shadow var(--dur) var(--ease);
383
+ }
384
+ .sff-select:hover { border-color: var(--fg-subtle); }
385
+ .sff-select:focus-visible {
386
+ outline: none;
387
+ border-color: var(--accent);
388
+ box-shadow: 0 0 0 3px var(--ring);
389
+ }
390
+ .sff-caret {
391
+ position: absolute;
392
+ right: var(--pad);
393
+ top: 50%;
394
+ transform: translateY(-50%);
395
+ display: inline-flex;
396
+ pointer-events: none;
397
+ color: var(--fg-muted);
398
+ font-size: var(--text-base);
399
+ }
400
+
401
+ /* switch ------------------------------------------------------ */
402
+ .sff-switchrow {
403
+ flex-direction: row;
404
+ align-items: center;
405
+ justify-content: space-between;
406
+ gap: var(--gap);
407
+ }
408
+ .sff-switch {
409
+ appearance: none;
410
+ font-family: inherit;
411
+ cursor: pointer;
412
+ display: inline-flex;
413
+ align-items: center;
414
+ gap: calc(var(--gap) * 0.6);
415
+ background: none;
416
+ border: 0;
417
+ padding: 4px;
418
+ border-radius: var(--radius-full);
419
+ flex-shrink: 0;
420
+ }
421
+ .sff-switch-track {
422
+ position: relative;
423
+ display: inline-block;
424
+ width: calc(var(--text-base) * 2.6);
425
+ height: calc(var(--text-base) * 1.5);
426
+ background: var(--surface-2);
427
+ border: 1px solid var(--border-strong);
428
+ border-radius: var(--radius-full);
429
+ transition: background var(--dur) var(--ease), border-color var(--dur) var(--ease);
430
+ }
431
+ .sff-switch[data-on="true"] .sff-switch-track {
432
+ background: var(--accent);
433
+ border-color: var(--accent);
434
+ }
435
+ .sff-switch-thumb {
436
+ position: absolute;
437
+ top: 50%;
438
+ left: 2px;
439
+ width: calc(var(--text-base) * 1.5 - 8px);
440
+ height: calc(var(--text-base) * 1.5 - 8px);
441
+ border-radius: var(--radius-full);
442
+ background: var(--surface);
443
+ box-shadow: var(--shadow-sm);
444
+ transform: translate(0, -50%);
445
+ transition: transform var(--dur) var(--ease), background var(--dur) var(--ease);
446
+ }
447
+ .sff-switch[data-on="true"] .sff-switch-thumb {
448
+ transform: translate(calc(var(--text-base) * 1.1), -50%);
449
+ background: var(--accent-fg);
450
+ }
451
+ .sff-switch-state {
452
+ font-family: var(--font-mono);
453
+ font-weight: var(--weight-medium);
454
+ color: var(--fg-muted);
455
+ min-width: 2.2em;
456
+ text-align: left;
457
+ letter-spacing: 0.02em;
458
+ }
459
+ .sff-switch[data-on="true"] .sff-switch-state { color: var(--accent); }
460
+ .sff-switch:focus-visible { outline: none; }
461
+ .sff-switch:focus-visible .sff-switch-track { box-shadow: 0 0 0 3px var(--ring); }
462
+
463
+ /* radio group ------------------------------------------------- */
464
+ .sff-radiogroup { display: flex; flex-direction: column; gap: calc(var(--gap) * 0.5); }
465
+ .sff-radio {
466
+ position: relative;
467
+ display: flex;
468
+ align-items: center;
469
+ gap: var(--pad);
470
+ padding: var(--pad);
471
+ background: var(--bg);
472
+ border: 1px solid var(--border);
473
+ border-radius: var(--radius);
474
+ cursor: pointer;
475
+ transition: border-color var(--dur) var(--ease), background var(--dur) var(--ease), box-shadow var(--dur) var(--ease);
476
+ }
477
+ .sff-radio:hover { border-color: var(--border-strong); background: var(--surface-2); }
478
+ .sff-radio[data-checked="true"] {
479
+ border-color: var(--accent);
480
+ background: var(--accent-weak);
481
+ box-shadow: var(--shadow-sm);
482
+ }
483
+ .sff-radio-input {
484
+ /* Full-cover transparent target: the mark/badge lands on the card center and a
485
+ pixel click hits the radio directly — not the 1px hidden-control anti-pattern. */
486
+ position: absolute;
487
+ inset: 0;
488
+ opacity: 0;
489
+ width: 100%; height: 100%;
490
+ margin: 0;
491
+ cursor: pointer;
492
+ }
493
+ .sff-radio:has(.sff-radio-input:focus-visible) {
494
+ box-shadow: 0 0 0 3px var(--ring);
495
+ border-color: var(--accent);
496
+ }
497
+ .sff-radio-icon {
498
+ display: inline-flex;
499
+ align-items: center;
500
+ justify-content: center;
501
+ width: calc(var(--text-base) * 2.1);
502
+ height: calc(var(--text-base) * 2.1);
503
+ border-radius: var(--radius-sm);
504
+ background: var(--surface-2);
505
+ color: var(--fg-muted);
506
+ font-size: calc(var(--text-base) * 1.1);
507
+ flex-shrink: 0;
508
+ transition: background var(--dur) var(--ease), color var(--dur) var(--ease);
509
+ }
510
+ .sff-radio[data-checked="true"] .sff-radio-icon { background: var(--accent); color: var(--accent-fg); }
511
+ .sff-radio-body { display: flex; flex-direction: column; gap: 1px; flex: 1; min-width: 0; }
512
+ .sff-radio-label { font-weight: var(--weight-medium); color: var(--fg); }
513
+ .sff-radio-hint { color: var(--fg-subtle); letter-spacing: 0; }
514
+ .sff-radio-dot {
515
+ width: calc(var(--text-base) * 1.15);
516
+ height: calc(var(--text-base) * 1.15);
517
+ border-radius: var(--radius-full);
518
+ border: 1.5px solid var(--border-strong);
519
+ background: var(--surface);
520
+ flex-shrink: 0;
521
+ position: relative;
522
+ transition: border-color var(--dur) var(--ease);
523
+ }
524
+ .sff-radio[data-checked="true"] .sff-radio-dot { border-color: var(--accent); }
525
+ .sff-radio-dot::after {
526
+ content: "";
527
+ position: absolute;
528
+ inset: 50%;
529
+ width: calc(var(--text-base) * 0.55);
530
+ height: calc(var(--text-base) * 0.55);
531
+ border-radius: var(--radius-full);
532
+ background: var(--accent);
533
+ transform: translate(-50%, -50%) scale(0);
534
+ transition: transform var(--dur) var(--ease-emphasis);
535
+ }
536
+ .sff-radio[data-checked="true"] .sff-radio-dot::after { transform: translate(-50%, -50%) scale(1); }
537
+
538
+ /* footer ------------------------------------------------------ */
539
+ .sff-foot {
540
+ display: flex;
541
+ align-items: center;
542
+ justify-content: space-between;
543
+ gap: var(--gap);
544
+ padding-top: var(--pad);
545
+ border-top: 1px solid var(--border);
546
+ }
547
+ .sff-savedhint {
548
+ margin: 0;
549
+ display: inline-flex;
550
+ align-items: center;
551
+ gap: calc(var(--gap) * 0.4);
552
+ color: var(--success);
553
+ font-weight: var(--weight-medium);
554
+ opacity: 0;
555
+ transform: translateY(2px);
556
+ transition: opacity var(--dur) var(--ease), transform var(--dur) var(--ease);
557
+ pointer-events: none;
558
+ }
559
+ .sff-savedhint[data-shown="true"] { opacity: 1; transform: translateY(0); }
560
+ .sff-submit {
561
+ font-family: inherit;
562
+ font-weight: var(--weight-bold);
563
+ letter-spacing: var(--tracking-tight);
564
+ color: var(--accent-fg);
565
+ background: var(--accent);
566
+ border: 1px solid var(--accent);
567
+ border-radius: var(--radius);
568
+ padding: var(--pad-tight) var(--pad-loose);
569
+ box-shadow: var(--shadow-sm);
570
+ cursor: pointer;
571
+ white-space: nowrap;
572
+ transition: transform var(--dur-fast) var(--ease), box-shadow var(--dur) var(--ease), filter var(--dur) var(--ease);
573
+ }
574
+ .sff-submit:hover { box-shadow: var(--shadow); filter: brightness(1.05); }
575
+ .sff-submit:active { transform: translateY(1px); box-shadow: var(--shadow-sm); }
576
+ .sff-submit:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--ring); }
577
+
578
+ @media (prefers-reduced-motion: reduce) {
579
+ .sff * { transition-duration: 1ms !important; }
580
+ }
581
+ `;
582
+
583
+ export default SettingsForm;