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,548 @@
1
+ import type React from 'react';
2
+ import { useEffect, useId, useRef, useState } from 'react';
3
+
4
+ /**
5
+ * Dialog — a destructive-confirmation dialog.
6
+ *
7
+ * Substrate (never compromised, agent-legible):
8
+ * - a real <button> trigger that opens an in-DOM modal
9
+ * - a role="dialog" aria-modal="true" surface with a real heading,
10
+ * explanatory copy naming the workspace ("Aurora"), a real typed-confirm
11
+ * <input>, and two real <button>s (Cancel / Confirm delete)
12
+ * - all task-relevant text/state lives in the DOM when open
13
+ * - focus moves into the dialog on open and is restored on close
14
+ * - Escape and overlay click both dismiss
15
+ *
16
+ * Style is expressed ENTIRELY through CSS custom properties (var(--x)) supplied
17
+ * by an ancestor [data-flagship] theme wrapper, so the single authored component
18
+ * renders convincingly under all five archetypes.
19
+ *
20
+ * onCommit("on"|"off") fires when notification settings are saved.
21
+ */
22
+
23
+ const WORKSPACE = 'Aurora';
24
+
25
+ export const Dialog: React.FC<{ onCommit?: (v: string) => void }> = ({ onCommit }) => {
26
+ const [open, setOpen] = useState(false);
27
+ const [notif, setNotif] = useState(false);
28
+ const [status, setStatus] = useState<string>('');
29
+
30
+ const surfaceRef = useRef<HTMLDivElement>(null);
31
+ const triggerRef = useRef<HTMLButtonElement>(null);
32
+ const inputRef = useRef<HTMLInputElement>(null);
33
+
34
+ const titleId = useId();
35
+ const descId = useId();
36
+ const inputId = useId();
37
+
38
+
39
+ // Focus management: move focus into the dialog on open, restore on close,
40
+ // and trap Tab within the surface while open.
41
+ useEffect(() => {
42
+ if (!open) return;
43
+ const previouslyFocused = triggerRef.current;
44
+ // Focus the confirm input first — it is the primary interaction.
45
+ const raf = requestAnimationFrame(() => inputRef.current?.focus());
46
+
47
+ const onKeyDown = (e: KeyboardEvent) => {
48
+ if (e.key === 'Escape') {
49
+ e.preventDefault();
50
+ close();
51
+ return;
52
+ }
53
+ if (e.key !== 'Tab') return;
54
+ const surface = surfaceRef.current;
55
+ if (!surface) return;
56
+ const focusables = surface.querySelectorAll<HTMLElement>(
57
+ 'button:not([disabled]), input, [href], [tabindex]:not([tabindex="-1"])',
58
+ );
59
+ if (focusables.length === 0) return;
60
+ const first = focusables[0];
61
+ const last = focusables[focusables.length - 1];
62
+ const active = document.activeElement as HTMLElement | null;
63
+ if (e.shiftKey && active === first) {
64
+ e.preventDefault();
65
+ last.focus();
66
+ } else if (!e.shiftKey && active === last) {
67
+ e.preventDefault();
68
+ first.focus();
69
+ }
70
+ };
71
+
72
+ document.addEventListener('keydown', onKeyDown);
73
+ const prevOverflow = document.body.style.overflow;
74
+ document.body.style.overflow = 'hidden';
75
+
76
+ return () => {
77
+ cancelAnimationFrame(raf);
78
+ document.removeEventListener('keydown', onKeyDown);
79
+ document.body.style.overflow = prevOverflow;
80
+ previouslyFocused?.focus();
81
+ };
82
+ // eslint-disable-next-line react-hooks/exhaustive-deps
83
+ }, [open]);
84
+
85
+ function openDialog() {
86
+ setStatus('');
87
+ setOpen(true);
88
+ }
89
+
90
+ function close() {
91
+ setOpen(false);
92
+ }
93
+
94
+ function save() {
95
+ const value = notif ? 'on' : 'off';
96
+ setStatus(value);
97
+ onCommit?.(value);
98
+ setOpen(false);
99
+ }
100
+
101
+ return (
102
+ <div className="dlgf-root">
103
+ <style>{css}</style>
104
+
105
+ {/* Substrate trigger — the operable target where it visibly is. */}
106
+ <div className="dlgf-launch">
107
+ <div className="dlgf-launch-copy">
108
+ <span className="dlgf-launch-title">Preferences</span>
109
+ <span className="dlgf-launch-sub">
110
+ Notification and delivery settings for the <strong>{WORKSPACE}</strong> workspace.
111
+ </span>
112
+ </div>
113
+ <button
114
+ ref={triggerRef}
115
+ type="button"
116
+ className="dlgf-btn dlgf-btn--danger-outline"
117
+ aria-haspopup="dialog"
118
+ aria-expanded={open}
119
+ onClick={openDialog}
120
+ >
121
+ <GearIcon />
122
+ Settings
123
+ </button>
124
+ </div>
125
+
126
+ {status && !open && (
127
+ <p className="dlgf-receipt" role="status">
128
+ <CheckIcon />
129
+ Notifications saved: <strong>{status}</strong>.
130
+ </p>
131
+ )}
132
+
133
+ {open && (
134
+ <div className="dlgf-overlay" onMouseDown={close}>
135
+ <div
136
+ ref={surfaceRef}
137
+ className="dlgf-surface"
138
+ role="dialog"
139
+ aria-modal="true"
140
+ aria-labelledby={titleId}
141
+ aria-describedby={descId}
142
+ // Stop overlay-dismiss when interacting inside the surface.
143
+ onMouseDown={(e) => e.stopPropagation()}
144
+ >
145
+ <div className="dlgf-head">
146
+ <span className="dlgf-badge" aria-hidden="true">
147
+ <AlertIcon />
148
+ </span>
149
+ <div className="dlgf-head-copy">
150
+ <h2 id={titleId} className="dlgf-title">
151
+ Settings
152
+ </h2>
153
+ <p id={descId} className="dlgf-desc">
154
+ Choose how <strong>{WORKSPACE}</strong> notifies you. Changes apply when you
155
+ save.
156
+ </p>
157
+ </div>
158
+ <button
159
+ type="button"
160
+ className="dlgf-icon-btn"
161
+ aria-label="Close dialog"
162
+ onClick={close}
163
+ >
164
+ <CloseIcon />
165
+ </button>
166
+ </div>
167
+
168
+ <div className="dlgf-field">
169
+ <div className="dlgf-switchrow">
170
+ <span className="dlgf-label">
171
+ Notifications
172
+ <span className="dlgf-hint">Email me when a run finishes.</span>
173
+ </span>
174
+ <label className="dlgf-switch" data-on={notif || undefined}>
175
+ <input
176
+ ref={inputRef as unknown as React.RefObject<HTMLInputElement>}
177
+ id={inputId}
178
+ type="checkbox"
179
+ className="dlgf-switch-input"
180
+ checked={notif}
181
+ onChange={(e) => setNotif(e.target.checked)}
182
+ />
183
+ <span className="dlgf-switch-thumb" aria-hidden="true" />
184
+ <span className="dlgf-switch-text">{notif ? 'on' : 'off'}</span>
185
+ </label>
186
+ </div>
187
+ </div>
188
+
189
+ <div className="dlgf-actions">
190
+ <button type="button" className="dlgf-btn dlgf-btn--ghost" onClick={close}>
191
+ Cancel
192
+ </button>
193
+ <button type="button" className="dlgf-btn dlgf-btn--danger" onClick={save}>
194
+ <CheckIcon />
195
+ Save
196
+ </button>
197
+ </div>
198
+ </div>
199
+ </div>
200
+ )}
201
+ </div>
202
+ );
203
+ };
204
+
205
+ /* ---------- inline icons (currentColor, no external assets) ---------- */
206
+
207
+ const GearIcon: React.FC = () => (
208
+ <svg className="dlgf-ico" viewBox="0 0 16 16" aria-hidden="true" focusable="false">
209
+ <path fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinejoin="round"
210
+ d="M8 10.2a2.2 2.2 0 1 0 0-4.4 2.2 2.2 0 0 0 0 4.4Z" />
211
+ <path fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinejoin="round"
212
+ d="m13.2 9.6.9.7-1.2 2-1.1-.4-1.2.7-.2 1.2H8l-.2-1.2-1.2-.7-1.1.4-1.2-2 .9-.7V7.9l-.9-.7 1.2-2 1.1.4 1.2-.7L8 3.7h2.4l.2 1.2 1.2.7 1.1-.4 1.2 2-.9.7v1.7Z" />
213
+ </svg>
214
+ );
215
+
216
+ const AlertIcon: React.FC = () => (
217
+ <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" width="20" height="20">
218
+ <path
219
+ fill="none"
220
+ stroke="currentColor"
221
+ strokeWidth="1.8"
222
+ strokeLinecap="round"
223
+ strokeLinejoin="round"
224
+ d="M12 3.6 21.2 19.4a1 1 0 0 1-.87 1.5H3.67a1 1 0 0 1-.87-1.5L12 3.6ZM12 9.5v4.2M12 17.2h.01"
225
+ />
226
+ </svg>
227
+ );
228
+
229
+ const CloseIcon: React.FC = () => (
230
+ <svg className="dlgf-ico" viewBox="0 0 16 16" aria-hidden="true" focusable="false">
231
+ <path
232
+ fill="none"
233
+ stroke="currentColor"
234
+ strokeWidth="1.5"
235
+ strokeLinecap="round"
236
+ d="m4 4 8 8M12 4l-8 8"
237
+ />
238
+ </svg>
239
+ );
240
+
241
+ const CheckIcon: React.FC = () => (
242
+ <svg className="dlgf-ico" viewBox="0 0 16 16" aria-hidden="true" focusable="false">
243
+ <path
244
+ fill="none"
245
+ stroke="currentColor"
246
+ strokeWidth="1.6"
247
+ strokeLinecap="round"
248
+ strokeLinejoin="round"
249
+ d="m3 8.5 3.2 3.2L13 5"
250
+ />
251
+ </svg>
252
+ );
253
+
254
+ /* ---------- scoped, fully token-driven styling ---------- */
255
+
256
+ export const css = `
257
+ .dlgf-root {
258
+ font-family: var(--font);
259
+ font-size: var(--text-base);
260
+ line-height: var(--leading);
261
+ color: var(--fg);
262
+ letter-spacing: var(--tracking-tight);
263
+ -webkit-font-smoothing: antialiased;
264
+ }
265
+ .dlgf-root *, .dlgf-root *::before, .dlgf-root *::after { box-sizing: border-box; }
266
+
267
+ /* trigger card */
268
+ .dlgf-launch {
269
+ display: flex;
270
+ align-items: center;
271
+ justify-content: space-between;
272
+ gap: var(--pad-loose);
273
+ flex-wrap: wrap;
274
+ max-width: 560px;
275
+ padding: var(--pad-loose);
276
+ background: var(--glass-bg, var(--surface));
277
+ backdrop-filter: var(--glass-blur, none);
278
+ -webkit-backdrop-filter: var(--glass-blur, none);
279
+ border: 1px solid var(--glass-border, var(--border));
280
+ border-radius: var(--radius-lg);
281
+ box-shadow: var(--shadow-sm);
282
+ }
283
+ .dlgf-launch-copy { display: flex; flex-direction: column; gap: calc(var(--gap) * 0.35); min-width: 12rem; }
284
+ .dlgf-launch-title {
285
+ font-family: var(--font-display);
286
+ font-weight: var(--weight-display);
287
+ font-size: calc(var(--text-base) * var(--scale-ratio));
288
+ letter-spacing: var(--tracking-tight);
289
+ color: var(--fg);
290
+ }
291
+ .dlgf-launch-sub { color: var(--fg-muted); font-size: var(--text-base); }
292
+ .dlgf-launch-sub strong { color: var(--fg); font-weight: var(--weight-medium); }
293
+
294
+ .dlgf-receipt {
295
+ display: inline-flex;
296
+ align-items: center;
297
+ gap: calc(var(--gap) * 0.6);
298
+ margin: var(--pad) 0 0;
299
+ padding: var(--pad-tight) var(--pad);
300
+ color: var(--success);
301
+ background: var(--surface-2);
302
+ border: 1px solid var(--border);
303
+ border-radius: var(--radius-full);
304
+ font-size: calc(var(--text-base) / var(--scale-ratio) * var(--scale-ratio) * 0.92);
305
+ }
306
+ .dlgf-receipt strong { font-weight: var(--weight-bold); }
307
+
308
+ /* buttons */
309
+ .dlgf-btn {
310
+ display: inline-flex;
311
+ align-items: center;
312
+ justify-content: center;
313
+ gap: calc(var(--gap) * 0.5);
314
+ padding: var(--pad-tight) var(--pad);
315
+ min-height: calc(var(--text-base) * 2.6);
316
+ font-family: var(--font);
317
+ font-size: var(--text-base);
318
+ font-weight: var(--weight-medium);
319
+ letter-spacing: var(--tracking-tight);
320
+ line-height: 1;
321
+ border-radius: var(--radius);
322
+ border: 1px solid transparent;
323
+ cursor: pointer;
324
+ transition:
325
+ background var(--dur-fast) var(--ease),
326
+ color var(--dur-fast) var(--ease),
327
+ border-color var(--dur-fast) var(--ease),
328
+ box-shadow var(--dur-fast) var(--ease),
329
+ transform var(--dur-fast) var(--ease);
330
+ }
331
+ .dlgf-btn:active { transform: translateY(0.5px); }
332
+ .dlgf-btn:focus-visible {
333
+ outline: none;
334
+ box-shadow: 0 0 0 3px var(--ring);
335
+ }
336
+
337
+ .dlgf-btn--danger-outline {
338
+ color: var(--danger);
339
+ background: var(--surface);
340
+ border-color: var(--border-strong);
341
+ }
342
+ .dlgf-btn--danger-outline:hover {
343
+ border-color: var(--danger);
344
+ background: var(--surface-2);
345
+ }
346
+
347
+ .dlgf-btn--ghost {
348
+ color: var(--fg-muted);
349
+ background: transparent;
350
+ border-color: var(--border-strong);
351
+ }
352
+ .dlgf-btn--ghost:hover { color: var(--fg); background: var(--surface-2); }
353
+
354
+ .dlgf-btn--danger {
355
+ color: var(--accent-fg);
356
+ background: var(--danger);
357
+ border-color: var(--danger);
358
+ font-weight: var(--weight-bold);
359
+ box-shadow: var(--shadow-sm);
360
+ }
361
+ .dlgf-btn--danger:hover:not(:disabled) { filter: brightness(0.94); }
362
+ .dlgf-btn--danger:disabled,
363
+ .dlgf-btn--danger[aria-disabled="true"] {
364
+ cursor: not-allowed;
365
+ opacity: 0.5;
366
+ box-shadow: none;
367
+ }
368
+
369
+ .dlgf-icon-btn {
370
+ display: inline-flex;
371
+ align-items: center;
372
+ justify-content: center;
373
+ width: calc(var(--text-base) * 2.2);
374
+ height: calc(var(--text-base) * 2.2);
375
+ margin: calc(var(--pad-tight) * -0.5);
376
+ color: var(--fg-subtle);
377
+ background: transparent;
378
+ border: 1px solid transparent;
379
+ border-radius: var(--radius-sm);
380
+ cursor: pointer;
381
+ transition: color var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease);
382
+ }
383
+ .dlgf-icon-btn:hover { color: var(--fg); background: var(--surface-2); }
384
+ .dlgf-icon-btn:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--ring); }
385
+
386
+ .dlgf-ico { width: 1em; height: 1em; flex: none; }
387
+
388
+ /* overlay + surface */
389
+ .dlgf-overlay {
390
+ position: fixed;
391
+ inset: 0;
392
+ z-index: 50;
393
+ display: flex;
394
+ align-items: center;
395
+ justify-content: center;
396
+ padding: var(--pad-loose);
397
+ background: color-mix(in srgb, var(--fg) 55%, transparent);
398
+ backdrop-filter: blur(2px);
399
+ animation: dlgf-fade var(--dur) var(--ease);
400
+ }
401
+ @supports not (background: color-mix(in srgb, red, blue)) {
402
+ .dlgf-overlay { background: rgba(0, 0, 0, 0.55); }
403
+ }
404
+
405
+ .dlgf-surface {
406
+ width: 100%;
407
+ max-width: 30rem;
408
+ background: var(--glass-bg, var(--surface));
409
+ backdrop-filter: var(--glass-blur, none);
410
+ -webkit-backdrop-filter: var(--glass-blur, none);
411
+ color: var(--fg);
412
+ border: 1px solid var(--glass-border, var(--border));
413
+ border-radius: var(--radius-lg);
414
+ box-shadow: var(--shadow-lg);
415
+ padding: var(--pad-loose);
416
+ display: flex;
417
+ flex-direction: column;
418
+ gap: var(--gap);
419
+ transform-origin: center;
420
+ animation: dlgf-pop var(--dur) var(--ease-emphasis);
421
+ }
422
+
423
+ .dlgf-head { display: flex; align-items: flex-start; gap: var(--gap); }
424
+ .dlgf-badge {
425
+ display: inline-flex;
426
+ align-items: center;
427
+ justify-content: center;
428
+ flex: none;
429
+ width: calc(var(--text-base) * 2.8);
430
+ height: calc(var(--text-base) * 2.8);
431
+ color: var(--danger);
432
+ background: color-mix(in srgb, var(--danger) 12%, var(--surface));
433
+ border: 1px solid color-mix(in srgb, var(--danger) 28%, transparent);
434
+ border-radius: var(--radius);
435
+ }
436
+ @supports not (background: color-mix(in srgb, red, blue)) {
437
+ .dlgf-badge { background: var(--surface-2); }
438
+ }
439
+ .dlgf-head-copy { flex: 1; display: flex; flex-direction: column; gap: calc(var(--gap) * 0.4); min-width: 0; }
440
+ .dlgf-title {
441
+ margin: 0;
442
+ font-family: var(--font-display);
443
+ font-weight: var(--weight-display);
444
+ font-size: calc(var(--text-base) * var(--scale-ratio) * var(--scale-ratio));
445
+ line-height: 1.2;
446
+ letter-spacing: var(--tracking-tight);
447
+ color: var(--fg);
448
+ }
449
+ .dlgf-desc { margin: 0; color: var(--fg-muted); font-size: var(--text-base); }
450
+ .dlgf-desc strong { color: var(--fg); font-weight: var(--weight-bold); }
451
+ .dlgf-num {
452
+ font-family: var(--font-mono);
453
+ font-weight: var(--weight-medium);
454
+ font-variant-numeric: tabular-nums;
455
+ color: var(--fg);
456
+ }
457
+
458
+ /* confirm field */
459
+ .dlgf-field { display: flex; flex-direction: column; gap: calc(var(--gap) * 0.5); margin-top: calc(var(--gap) * 0.25); }
460
+ .dlgf-label { color: var(--fg-muted); font-size: calc(var(--text-base) * 0.94); font-weight: var(--weight-medium); }
461
+ .dlgf-code {
462
+ font-family: var(--font-mono);
463
+ font-size: 0.92em;
464
+ color: var(--danger);
465
+ background: var(--surface-2);
466
+ border: 1px solid var(--border);
467
+ border-radius: var(--radius-sm);
468
+ padding: 0.05em 0.4em;
469
+ }
470
+ .dlgf-input {
471
+ width: 100%;
472
+ padding: var(--pad-tight) var(--pad);
473
+ min-height: calc(var(--text-base) * 2.8);
474
+ font-family: var(--font-mono);
475
+ font-size: var(--text-base);
476
+ color: var(--fg);
477
+ background: var(--bg);
478
+ border: 1px solid var(--border-strong);
479
+ border-radius: var(--radius);
480
+ transition: border-color var(--dur-fast) var(--ease), box-shadow var(--dur-fast) var(--ease);
481
+ }
482
+ .dlgf-input::placeholder { color: var(--fg-subtle); }
483
+ .dlgf-input:focus-visible {
484
+ outline: none;
485
+ border-color: var(--accent);
486
+ box-shadow: 0 0 0 3px var(--ring);
487
+ }
488
+ .dlgf-hint {
489
+ display: inline-flex;
490
+ align-items: center;
491
+ gap: calc(var(--gap) * 0.4);
492
+ min-height: 1.4em;
493
+ color: var(--fg-subtle);
494
+ font-size: calc(var(--text-base) * 0.88);
495
+ }
496
+ .dlgf-hint .dlgf-ico { color: var(--success); }
497
+
498
+ .dlgf-switchrow {
499
+ display: flex; align-items: center; justify-content: space-between; gap: var(--gap);
500
+ }
501
+ .dlgf-switch {
502
+ display: inline-flex; align-items: center; gap: var(--pad-tight);
503
+ padding: var(--pad-tight) var(--pad);
504
+ border: 1px solid var(--border-strong);
505
+ border-radius: var(--radius-full);
506
+ background: var(--surface-2); color: var(--fg);
507
+ font: inherit; cursor: pointer;
508
+ transition: background var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease);
509
+ }
510
+ .dlgf-switch[data-on] { background: var(--accent); border-color: var(--accent); color: var(--accent-fg); }
511
+ .dlgf-switch:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--ring); }
512
+ .dlgf-switch { position: relative; }
513
+ .dlgf-switch-input {
514
+ position: absolute; inset: 0; width: 100%; height: 100%;
515
+ margin: 0; opacity: 0; cursor: pointer;
516
+ }
517
+ .dlgf-switch-thumb {
518
+ width: .75em; height: .75em; border-radius: var(--radius-full);
519
+ background: currentColor; opacity: .55;
520
+ transition: opacity var(--dur-fast) var(--ease);
521
+ }
522
+ .dlgf-switch[data-on] .dlgf-switch { position: relative; }
523
+ .dlgf-switch-input {
524
+ position: absolute; inset: 0; width: 100%; height: 100%;
525
+ margin: 0; opacity: 0; cursor: pointer;
526
+ }
527
+ .dlgf-switch-thumb { opacity: 1; }
528
+ .dlgf-switch-text { font-variant-numeric: tabular-nums; }
529
+ .dlgf-actions {
530
+ display: flex;
531
+ justify-content: flex-end;
532
+ gap: calc(var(--gap) * 0.75);
533
+ margin-top: var(--gap);
534
+ padding-top: var(--pad);
535
+ border-top: 1px solid var(--border);
536
+ }
537
+
538
+ @keyframes dlgf-fade { from { opacity: 0; } to { opacity: 1; } }
539
+ @keyframes dlgf-pop {
540
+ from { opacity: 0; transform: translateY(8px) scale(0.98); }
541
+ to { opacity: 1; transform: translateY(0) scale(1); }
542
+ }
543
+ @media (prefers-reduced-motion: reduce) {
544
+ .dlgf-overlay, .dlgf-surface, .dlgf-btn { animation: none; transition: none; }
545
+ }
546
+ `;
547
+
548
+ export default Dialog;