@lovett/ui 0.0.4 → 0.0.6

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.
Files changed (59) hide show
  1. package/dist/index.d.ts +2659 -0
  2. package/dist/index.js +14451 -0
  3. package/dist/index.js.map +1 -0
  4. package/dist/styles.css +636 -0
  5. package/dist/tokens.css +700 -0
  6. package/package.json +44 -17
  7. package/src/__tests__/button.test.tsx +137 -0
  8. package/src/__tests__/card.test.tsx +103 -0
  9. package/src/__tests__/dead-render.test.tsx +117 -0
  10. package/src/__tests__/input.test.tsx +134 -0
  11. package/src/__tests__/modal.test.tsx +154 -0
  12. package/src/__tests__/page-shell.test.tsx +128 -0
  13. package/src/__tests__/setup.ts +43 -0
  14. package/src/__tests__/token-shape.test.ts +193 -0
  15. package/src/allocation-sparkbar.tsx +90 -0
  16. package/src/card.tsx +1 -1
  17. package/src/collapsible-card.tsx +85 -0
  18. package/src/data-grid/table-body.tsx +8 -1
  19. package/src/dropdown-menu.tsx +1 -1
  20. package/src/floating-status-bar.tsx +112 -0
  21. package/src/folder-tree-picker.tsx +5 -6
  22. package/src/frame-stack.tsx +27 -10
  23. package/src/hero-form-card.tsx +2 -2
  24. package/src/icons/brand.tsx +187 -0
  25. package/src/index.ts +43 -0
  26. package/src/lib/clipboard.ts +14 -0
  27. package/src/lib/color.ts +111 -0
  28. package/src/meta-cell.tsx +52 -0
  29. package/src/meta-previews/MetaFeedCarousel.tsx +1 -1
  30. package/src/meta-previews/MetaFeedPreview.tsx +1 -1
  31. package/src/microsoft-logo.tsx +33 -0
  32. package/src/modal.tsx +77 -6
  33. package/src/pill-button.tsx +23 -5
  34. package/src/profile-section.tsx +40 -9
  35. package/src/sortable-table.tsx +5 -1
  36. package/src/styles.css +74 -0
  37. package/src/tabs.tsx +4 -0
  38. package/src/tag-chip-input.tsx +1 -1
  39. package/src/theme-v2.css +466 -0
  40. package/src/token-badge.tsx +92 -0
  41. package/src/tokens.css +181 -60
  42. package/src/v2/README.md +208 -0
  43. package/src/v2/__demo__/showcase.tsx +1045 -0
  44. package/src/v2/action.tsx +91 -0
  45. package/src/v2/callout.tsx +76 -0
  46. package/src/v2/document-section.tsx +82 -0
  47. package/src/v2/document-shell.tsx +0 -0
  48. package/src/v2/field-row.tsx +113 -0
  49. package/src/v2/icons.tsx +165 -0
  50. package/src/v2/index.ts +147 -0
  51. package/src/v2/layout.tsx +293 -0
  52. package/src/v2/progress-track.tsx +89 -0
  53. package/src/v2/stat-tile.tsx +129 -0
  54. package/src/v2/states.tsx +271 -0
  55. package/src/v2/status-pill.tsx +74 -0
  56. package/src/v2/theme.css +1861 -0
  57. package/src/v2/timeline.tsx +81 -0
  58. package/src/v2/tokens.ts +228 -0
  59. package/src/value-chip.tsx +76 -0
@@ -0,0 +1,636 @@
1
+ /*
2
+ * @lovett/ui — component CSS utilities
3
+ *
4
+ * Ported from brand-ai-extension/webapp/src/index.css
5
+ * tag : v-pre-workspace-intake-phase1
6
+ * sha : 172dd010fee808bcd03795119ba888206da5bafd
7
+ *
8
+ * Phase 1 deviation (logged in completion report): ADR-008 Decision 2 talks
9
+ * about porting `:root { ... }` token blocks only. In practice the BRING_AS_IS
10
+ * primitives (Card, Button, Input, Textarea, Modal, ProfileSection) render
11
+ * through the CSS utility classes defined in source `index.css` below the
12
+ * token block. Those classes ARE the primitive's implementation — leaving them
13
+ * behind would ship non-functional primitives. They live here, separate from
14
+ * the token vocabulary, and are re-exported from the package as
15
+ * `@lovett/ui/styles.css` so apps import alongside `@lovett/ui/tokens.css`.
16
+ *
17
+ * Scope: only the utilities consumed by the in-scope primitives below:
18
+ * .ds-card-surface (+ data-framed, data-interactive) — Card, Modal,
19
+ * ProfileSection
20
+ * .ds-card-tray — Card, Modal,
21
+ * ProfileSection
22
+ * .btn, .btn-*, size-* modifiers, .is-loading,
23
+ * .btn-group — Button
24
+ * .input-shell, .textarea-shell, .affix variants,
25
+ * .has-leading/trailing, .is-error/disabled,
26
+ * .size-sm/lg — Input, Textarea
27
+ * .scrollbar-hide — ChipNav
28
+ * .ds-enter-pop, .ds-enter-rise — DropdownMenu,
29
+ * FloatingStatusBar
30
+ * prefers-reduced-motion block — accessibility
31
+ *
32
+ * Out of scope for Phase 1: .empty, .skel, .field*, .pg-header, .search-bar,
33
+ * .breadcrumbs, .topbar, .checkbox, .radio, .switch, .reasoning-pulse,
34
+ * .chat-md-list, .selection-reply-btn, ::selection. Those land when their
35
+ * consumers (Field, EmptyState, PageHeader, etc.) arrive in later phases.
36
+ */
37
+
38
+ /* ---- PANE MASK ---- */
39
+ /**
40
+ * Opaque fill matching the MainLayout content pane, for a sticky element that
41
+ * must hide content scrolling beneath it.
42
+ *
43
+ * Exists because the mask has to equal the pane EXACTLY or content shows
44
+ * through / bands, and the only token that does is `--main-content-bg` —
45
+ * internal, so lens code cannot name it (CLAUDE.md §5). Same shape as
46
+ * `.ds-card-tray` owning `--tray-bg`: the class lives here, consumers use the
47
+ * class. This is NOT a raised surface — do not reach for `--surface-raised`
48
+ * (ADR-063); a sticky mask must match the pane, not sit above it.
49
+ *
50
+ * Consumers: sem-spec `output/index.tsx`, `share-view/ad-preview-section.tsx`.
51
+ */
52
+ .ds-pane-mask {
53
+ background: rgb(var(--main-content-bg));
54
+ }
55
+
56
+ /* ---- CARD SURFACE ---- */
57
+ .ds-card-surface {
58
+ background: rgb(var(--bg-card));
59
+ border: 1px solid rgb(var(--border-card));
60
+ border-radius: var(--radius-lg);
61
+ box-shadow: var(--card-shadow-rest);
62
+ }
63
+ .ds-card-surface[data-framed="true"] {
64
+ background: rgb(var(--card-frame-bg));
65
+ }
66
+ .ds-card-surface[data-interactive="true"] {
67
+ cursor: pointer;
68
+ transition:
69
+ transform var(--dur-fast) var(--ease-out),
70
+ box-shadow var(--dur-fast) var(--ease-out),
71
+ border-color var(--dur-fast) var(--ease-out);
72
+ }
73
+ .ds-card-surface[data-interactive="true"]:hover {
74
+ transform: translateY(-1px);
75
+ box-shadow: var(--card-shadow-hover);
76
+ }
77
+
78
+ /* ---- CARD TRAY ---- */
79
+ .ds-card-tray {
80
+ /* 4px, not 10. Concentric radius is outer = inner + padding, so a
81
+ 16px card around a 12px tray wants a 4px inset — at 10 the maths
82
+ was 22 and the corners fought each other. It is also what every
83
+ kit in the corpus measures (AI Agent: pad 4, r16 outer / r12
84
+ inner), and it is what reads as thinner and sleeker. */
85
+ margin: var(--tray-inset);
86
+ background: rgb(var(--tray-bg));
87
+ /* Owner ask: visible 1px border on the inner tray so the tray surface
88
+ * reads as distinct from the surrounding card-frame. Uses the same
89
+ * --border-card the outer card uses for consistency. */
90
+ border: 1px solid rgb(var(--border-card));
91
+ border-radius: var(--radius-tray);
92
+ /* ADR-076 D7: recessed-tray shadow — formalizes the "bright inset
93
+ * panel" reading the tray already carries via the frame/tray
94
+ * brightness step. Subtle in both themes so every existing
95
+ * ProfileSection / Card.Tray gains depth without a visible regression. */
96
+ box-shadow: var(--tray-shadow);
97
+ overflow: hidden;
98
+ }
99
+ .ds-card-tray:not(:last-child) {
100
+ margin-bottom: 0;
101
+ }
102
+
103
+ /* ---- BUTTONS ---- */
104
+ .btn {
105
+ display: inline-flex;
106
+ align-items: center;
107
+ justify-content: center;
108
+ gap: 6px;
109
+ height: var(--ctrl-md);
110
+ padding: 0 16px;
111
+ /* ADR-026 D1 — bumped from --radius-sm (8px) to --radius-md (12px).
112
+ * Inspired by gray-ui-csm rounded-xl button treatment (recon §13 DQ-1). */
113
+ border-radius: var(--radius-md);
114
+ font-size: 13px;
115
+ font-weight: 500;
116
+ cursor: pointer;
117
+ transition:
118
+ background var(--dur-fast) var(--ease-out),
119
+ border-color var(--dur-fast) var(--ease-out),
120
+ color var(--dur-fast) var(--ease-out),
121
+ box-shadow var(--dur-fast) var(--ease-out),
122
+ transform var(--dur-fast) var(--ease-out),
123
+ filter var(--dur-fast) var(--ease-out);
124
+ border: 1px solid transparent;
125
+ font-family: var(--font-sans);
126
+ white-space: nowrap;
127
+ user-select: none;
128
+ }
129
+ .btn:focus-visible {
130
+ outline: none;
131
+ box-shadow: var(--ring-focus);
132
+ }
133
+
134
+ .btn-primary {
135
+ background: rgb(var(--accent));
136
+ color: white;
137
+ border-color: rgb(var(--accent));
138
+ }
139
+ .btn-primary:hover {
140
+ background: rgb(var(--accent-hover));
141
+ border-color: rgb(var(--accent-hover));
142
+ box-shadow: var(--shadow-glow);
143
+ transform: translateY(-1px);
144
+ }
145
+ .btn-primary:active {
146
+ transform: translateY(0);
147
+ box-shadow: 0 1px 3px rgb(var(--accent-glow));
148
+ }
149
+
150
+ .btn-secondary {
151
+ background: rgb(var(--bg-card));
152
+ color: rgb(var(--foreground));
153
+ border-color: rgb(var(--border));
154
+ }
155
+ .btn-secondary:hover {
156
+ background: rgb(var(--bg-card-hover));
157
+ border-color: rgb(var(--border-strong));
158
+ }
159
+
160
+ .btn-outline {
161
+ background: transparent;
162
+ color: rgb(var(--foreground));
163
+ border-color: rgb(var(--border-strong));
164
+ }
165
+ .btn-outline:hover {
166
+ background: rgb(var(--surface-overlay-soft));
167
+ border-color: rgb(var(--foreground));
168
+ }
169
+
170
+ /* FilterBar (ADR-121) — match the filter pills' rest border to the search
171
+ field's input border (--border-input is a touch stronger than the generic
172
+ --border the secondary button uses), so the search box + filter chips read
173
+ as one bordered set. Hover already aligns on --border-strong; the active
174
+ (engaged) pill keeps its inline accent border. */
175
+ .filter-bar .btn-secondary {
176
+ border-color: rgb(var(--border-input));
177
+ }
178
+ .filter-bar .btn-secondary:hover {
179
+ border-color: rgb(var(--border-strong));
180
+ }
181
+
182
+ .btn-ghost {
183
+ background: transparent;
184
+ color: rgb(var(--muted-foreground));
185
+ }
186
+ .btn-ghost:hover {
187
+ background: rgb(var(--surface-overlay-soft));
188
+ color: rgb(var(--foreground));
189
+ }
190
+
191
+ .btn-destructive {
192
+ background: rgb(var(--destructive));
193
+ color: white;
194
+ border-color: rgb(var(--destructive));
195
+ }
196
+ .btn-destructive:hover {
197
+ filter: brightness(0.94);
198
+ }
199
+
200
+ .btn-destructive-soft {
201
+ background: rgb(var(--destructive) / 0.10);
202
+ color: rgb(var(--destructive));
203
+ border-color: rgb(var(--destructive) / 0.25);
204
+ }
205
+ .btn-destructive-soft:hover {
206
+ background: rgb(var(--destructive) / 0.15);
207
+ }
208
+
209
+ .btn-icon {
210
+ width: var(--ctrl-md);
211
+ height: var(--ctrl-md);
212
+ padding: 0;
213
+ background: transparent;
214
+ color: rgb(var(--muted-foreground));
215
+ border: 1px solid rgb(var(--border));
216
+ }
217
+ .btn-icon:hover {
218
+ color: rgb(var(--foreground));
219
+ background: rgb(var(--surface-overlay-soft));
220
+ border-color: rgb(var(--border-strong));
221
+ }
222
+ /* ADR-026 D3 — circular IconButton variant (Button shape="circle"
223
+ * combined with variant="icon"). Used by AppHeader + future header
224
+ * surfaces per gray-ui-csm recon §13 DQ-3. Pill-radius wins over the
225
+ * .btn base radius when both classes are present. */
226
+ .btn-icon.is-circle {
227
+ border-radius: 999px;
228
+ }
229
+
230
+ .btn-toolbar {
231
+ background: rgb(var(--bg-elevated));
232
+ color: rgb(var(--text-secondary));
233
+ border-color: rgb(var(--border));
234
+ border-radius: 999px;
235
+ }
236
+ .btn-toolbar:hover {
237
+ background: rgb(var(--bg-card-hover));
238
+ color: rgb(var(--foreground));
239
+ border-color: rgb(var(--border-strong));
240
+ }
241
+
242
+ /* Size modifiers */
243
+ .btn.size-sm {
244
+ height: var(--ctrl-sm);
245
+ padding: 0 12px;
246
+ font-size: 12.5px;
247
+ gap: 5px;
248
+ }
249
+ .btn.size-sm.btn-icon {
250
+ width: var(--ctrl-sm);
251
+ padding: 0;
252
+ }
253
+ .btn.size-lg {
254
+ height: var(--ctrl-lg);
255
+ padding: 0 22px;
256
+ font-size: 14px;
257
+ gap: 8px;
258
+ }
259
+ .btn.size-lg.btn-icon {
260
+ width: var(--ctrl-lg);
261
+ padding: 0;
262
+ }
263
+
264
+ /* Loading + disabled */
265
+ .btn.is-loading {
266
+ color: transparent;
267
+ position: relative;
268
+ pointer-events: none;
269
+ }
270
+ .btn.is-loading::after {
271
+ content: '';
272
+ position: absolute;
273
+ inset: 0;
274
+ margin: auto;
275
+ width: 14px;
276
+ height: 14px;
277
+ border: 2px solid currentColor;
278
+ border-top-color: transparent;
279
+ border-radius: 999px;
280
+ color: white;
281
+ animation: btn-spin 720ms linear infinite;
282
+ }
283
+ .btn-secondary.is-loading::after,
284
+ .btn-ghost.is-loading::after,
285
+ .btn-outline.is-loading::after,
286
+ .btn-destructive-soft.is-loading::after,
287
+ .btn-toolbar.is-loading::after {
288
+ color: rgb(var(--foreground));
289
+ }
290
+ .btn[disabled] {
291
+ opacity: 0.5;
292
+ cursor: not-allowed;
293
+ transform: none;
294
+ box-shadow: none;
295
+ }
296
+ @keyframes btn-spin {
297
+ to {
298
+ transform: rotate(360deg);
299
+ }
300
+ }
301
+
302
+ /* Joined button group — typed React wrapper at packages/ui/src/button-group.tsx.
303
+ * ADR-026 D5 — corner rules bumped from --radius-sm to --radius-md to match
304
+ * the .btn base radius bump (D1). Inspired by gray-ui-csm view-switcher
305
+ * (recon §13 DQ-5). */
306
+ .btn-group {
307
+ display: inline-flex;
308
+ }
309
+ .btn-group .btn {
310
+ border-radius: 0;
311
+ border-right-width: 0;
312
+ }
313
+ .btn-group .btn:first-child {
314
+ border-top-left-radius: var(--radius-md);
315
+ border-bottom-left-radius: var(--radius-md);
316
+ }
317
+ .btn-group .btn:last-child {
318
+ border-top-right-radius: var(--radius-md);
319
+ border-bottom-right-radius: var(--radius-md);
320
+ border-right-width: 1px;
321
+ }
322
+
323
+ /* ---- INPUT SHELL ---- */
324
+ .input-shell {
325
+ position: relative;
326
+ display: flex;
327
+ align-items: stretch;
328
+ background: rgb(var(--bg-input));
329
+ border: 1px solid rgb(var(--border-input));
330
+ border-radius: var(--radius-md);
331
+ transition: border-color 160ms, box-shadow 160ms, background 160ms;
332
+ height: var(--ctrl-md);
333
+ }
334
+ .input-shell:hover {
335
+ border-color: rgb(var(--border-strong));
336
+ background: rgb(var(--bg-input-hover));
337
+ }
338
+ .input-shell:focus-within {
339
+ border-color: rgb(var(--accent));
340
+ box-shadow: var(--ring-focus);
341
+ background: rgb(var(--bg-input));
342
+ }
343
+ .input-shell.is-error {
344
+ border-color: rgb(var(--destructive));
345
+ }
346
+ .input-shell.is-error:focus-within {
347
+ box-shadow: var(--ring-error);
348
+ }
349
+ .input-shell.is-disabled {
350
+ opacity: 0.55;
351
+ pointer-events: none;
352
+ background: rgb(var(--surface-overlay-soft));
353
+ }
354
+ .input-shell input,
355
+ .input-shell select,
356
+ .input-shell textarea {
357
+ flex: 1;
358
+ min-width: 0;
359
+ appearance: none;
360
+ border: 0;
361
+ outline: 0;
362
+ background: transparent;
363
+ color: rgb(var(--foreground));
364
+ font: 500 13.5px/1.4 var(--font-sans);
365
+ padding: 0 12px;
366
+ }
367
+ .input-shell.has-leading input,
368
+ .input-shell.has-leading select {
369
+ padding-left: 0;
370
+ }
371
+ .input-shell.has-trailing input,
372
+ .input-shell.has-trailing select {
373
+ padding-right: 0;
374
+ }
375
+ .input-shell input::placeholder,
376
+ .input-shell textarea::placeholder {
377
+ color: rgb(var(--text-tertiary));
378
+ }
379
+
380
+ .input-shell .affix {
381
+ display: inline-flex;
382
+ align-items: center;
383
+ justify-content: center;
384
+ padding: 0 10px;
385
+ color: rgb(var(--text-tertiary));
386
+ font: 500 12.5px/1 var(--font-sans);
387
+ pointer-events: none;
388
+ flex-shrink: 0;
389
+ }
390
+ .input-shell .affix.action {
391
+ pointer-events: auto;
392
+ cursor: pointer;
393
+ background: transparent;
394
+ border: 0;
395
+ transition: color 120ms;
396
+ }
397
+ .input-shell .affix.action:hover {
398
+ color: rgb(var(--foreground));
399
+ }
400
+ .input-shell .affix.divider {
401
+ border-left: 1px solid rgb(var(--border));
402
+ color: rgb(var(--muted-foreground));
403
+ font-size: 11.5px;
404
+ }
405
+
406
+ /* Numeric variant (ADR-076 D8) — monospace + tabular figures so digits
407
+ * align as values change. Orthogonal to the size modifiers. */
408
+ .input-shell.is-numeric input {
409
+ font-family: var(--font-mono);
410
+ font-variant-numeric: tabular-nums;
411
+ font-weight: 600;
412
+ }
413
+
414
+ .input-shell.size-sm {
415
+ height: var(--ctrl-sm);
416
+ }
417
+ .input-shell.size-sm input,
418
+ .input-shell.size-sm select {
419
+ font-size: 12.5px;
420
+ padding: 0 10px;
421
+ }
422
+ .input-shell.size-lg {
423
+ height: var(--ctrl-lg);
424
+ }
425
+ .input-shell.size-lg input,
426
+ .input-shell.size-lg select {
427
+ font-size: 14.5px;
428
+ padding: 0 14px;
429
+ }
430
+
431
+ /* Textarea variant — auto height */
432
+ .input-shell.textarea-shell {
433
+ height: auto;
434
+ align-items: flex-start;
435
+ padding: 8px 0;
436
+ }
437
+ .input-shell.textarea-shell textarea {
438
+ min-height: 80px;
439
+ resize: vertical;
440
+ padding: 0 12px;
441
+ font-family: var(--font-sans);
442
+ }
443
+
444
+ /* ---- SCROLLBAR-HIDE UTILITY (ChipNav) ---- */
445
+ .scrollbar-hide {
446
+ -ms-overflow-style: none;
447
+ scrollbar-width: none;
448
+ }
449
+ .scrollbar-hide::-webkit-scrollbar {
450
+ display: none;
451
+ }
452
+
453
+ /* ---- PROGRESS BAR INDETERMINATE ---- */
454
+ /* Used by `<ProgressBar>` primitive (no `percent` prop) — animated
455
+ sweep across the track. Added 2026-05-18 per ADR-020 D10 promotion. */
456
+ .progress-indeterminate {
457
+ width: 40%;
458
+ animation: progress-indeterminate-sweep 1.4s ease-in-out infinite;
459
+ }
460
+ @keyframes progress-indeterminate-sweep {
461
+ 0% { transform: translateX(-100%); }
462
+ 100% { transform: translateX(250%); }
463
+ }
464
+
465
+ /* ---- RANGE SLIDER ----
466
+ * Dual-thumb range slider. Two native range inputs are stacked over a
467
+ * shared track + fill. The inputs themselves are transparent and
468
+ * pointer-events:none so they don't block each other — only the THUMB
469
+ * pseudo-elements re-enable pointer events. Thumb styling can only be
470
+ * done via ::-webkit-slider-thumb / ::-moz-range-thumb pseudo-elements,
471
+ * which is why this lives in CSS rather than inline styles. */
472
+ .ds-range-input {
473
+ position: absolute;
474
+ left: 0;
475
+ top: 50%;
476
+ transform: translateY(-50%);
477
+ width: 100%;
478
+ height: 16px;
479
+ margin: 0;
480
+ background: transparent;
481
+ pointer-events: none;
482
+ -webkit-appearance: none;
483
+ appearance: none;
484
+ }
485
+ .ds-range-input::-webkit-slider-runnable-track {
486
+ background: transparent;
487
+ height: 16px;
488
+ }
489
+ .ds-range-input::-moz-range-track {
490
+ background: transparent;
491
+ height: 16px;
492
+ }
493
+ .ds-range-input::-webkit-slider-thumb {
494
+ -webkit-appearance: none;
495
+ appearance: none;
496
+ pointer-events: auto;
497
+ height: 16px;
498
+ width: 16px;
499
+ border-radius: var(--radius-full);
500
+ background: rgb(var(--bg-elevated));
501
+ border: 2px solid rgb(var(--accent));
502
+ box-shadow: var(--shadow-sm, 0 1px 2px rgb(0 0 0 / 0.12));
503
+ cursor: pointer;
504
+ transition: transform var(--dur-fast) var(--ease-out);
505
+ }
506
+ .ds-range-input::-webkit-slider-thumb:hover {
507
+ transform: scale(1.12);
508
+ }
509
+ .ds-range-input::-moz-range-thumb {
510
+ pointer-events: auto;
511
+ height: 16px;
512
+ width: 16px;
513
+ border-radius: var(--radius-full);
514
+ background: rgb(var(--bg-elevated));
515
+ border: 2px solid rgb(var(--accent));
516
+ box-shadow: var(--shadow-sm, 0 1px 2px rgb(0 0 0 / 0.12));
517
+ cursor: pointer;
518
+ }
519
+ .ds-range-input:focus-visible::-webkit-slider-thumb {
520
+ outline: 2px solid rgb(var(--accent));
521
+ outline-offset: 2px;
522
+ }
523
+ .ds-range-input:focus-visible::-moz-range-thumb {
524
+ outline: 2px solid rgb(var(--accent));
525
+ outline-offset: 2px;
526
+ }
527
+ .ds-range-input:disabled::-webkit-slider-thumb {
528
+ border-color: rgb(var(--border-strong));
529
+ cursor: not-allowed;
530
+ }
531
+ .ds-range-input:disabled::-moz-range-thumb {
532
+ border-color: rgb(var(--border-strong));
533
+ cursor: not-allowed;
534
+ }
535
+
536
+ /* ---- RESIZABLE HANDLE ----
537
+ * Hover/drag tint comes from inline styles + Tailwind utility (handled in
538
+ * resizable.tsx). The transition lives here so prefers-reduced-motion can
539
+ * suppress it without touching the JS — drag responsiveness is never
540
+ * gated on animation, only the colour fade.
541
+ *
542
+ * MAJ-1 (Sidebar-A hot-fix, ADR-049 D9 §212): on active drag the line
543
+ * grows from 4 px → 8 px so the user has a positive confirmation of the
544
+ * grab. Implemented with `transform: scale...` on the cross-axis so the
545
+ * line stays centred on its midpoint — no margin recalc, no layout
546
+ * thrash. `transform-origin: center` is the CSS default but stated for
547
+ * clarity. The orientation comes from the `data-orientation` attribute
548
+ * on the element (set by resizable.tsx alongside `data-dragging`).
549
+ */
550
+ .resizable-handle {
551
+ transition: background-color var(--dur-fast) var(--ease-out),
552
+ transform var(--dur-fast) var(--ease-out);
553
+ transform-origin: center center;
554
+ }
555
+ .resizable-handle[data-orientation='vertical'][data-dragging] {
556
+ transform: scaleX(2);
557
+ }
558
+ .resizable-handle[data-orientation='horizontal'][data-dragging] {
559
+ transform: scaleY(2);
560
+ }
561
+
562
+ /* ---- ENTRANCE ANIMATIONS ----
563
+ *
564
+ * These exist because `animate-in` / `fade-in-0` / `zoom-in-95` /
565
+ * `slide-in-from-bottom-2` are NOT Tailwind core utilities — they ship in
566
+ * `tailwindcss-animate` / `tw-animate-css`, neither of which is a dependency
567
+ * of this repo. Both of the kit's entrance animations were therefore
568
+ * referencing classes that were never generated and had never run, while
569
+ * typecheck, lint and build stayed green.
570
+ *
571
+ * Adding the dependency would need an ADR (CLAUDE.md §1); two keyframes do
572
+ * not. Durations come from the motion tokens rather than being re-specified
573
+ * per call site.
574
+ */
575
+ @keyframes ds-enter-pop {
576
+ from {
577
+ opacity: 0;
578
+ transform: scale(0.95);
579
+ }
580
+ to {
581
+ opacity: 1;
582
+ transform: scale(1);
583
+ }
584
+ }
585
+ @keyframes ds-enter-rise {
586
+ from {
587
+ opacity: 0;
588
+ transform: translateY(0.5rem);
589
+ }
590
+ to {
591
+ opacity: 1;
592
+ transform: translateY(0);
593
+ }
594
+ }
595
+ /* Popovers, menus, dropdowns — anchored to a trigger, so they scale open. */
596
+ .ds-enter-pop {
597
+ animation: ds-enter-pop var(--dur-fast) var(--ease-out);
598
+ }
599
+ /* Bars and toasts entering from a screen edge. Animates `transform`, which
600
+ * composes with (rather than overwrites) a `translate`-based centering
601
+ * utility such as `-translate-x-1/2`. */
602
+ .ds-enter-rise {
603
+ animation: ds-enter-rise var(--dur-base) var(--ease-out);
604
+ }
605
+
606
+ /* ---- REDUCED MOTION ---- */
607
+ @media (prefers-reduced-motion: reduce) {
608
+ /* 0.01ms rather than `none` so animationend still fires for any listener. */
609
+ .ds-enter-pop,
610
+ .ds-enter-rise {
611
+ animation-duration: 0.01ms !important;
612
+ }
613
+ .btn,
614
+ .btn-primary,
615
+ .input-shell {
616
+ transition: none !important;
617
+ animation: none !important;
618
+ }
619
+ .btn-primary:hover,
620
+ .btn:hover {
621
+ transform: none !important;
622
+ }
623
+ .progress-indeterminate {
624
+ animation: none !important;
625
+ width: 100% !important;
626
+ }
627
+ .ds-range-input::-webkit-slider-thumb {
628
+ transition: none !important;
629
+ }
630
+ .ds-range-input::-webkit-slider-thumb:hover {
631
+ transform: none !important;
632
+ }
633
+ .resizable-handle {
634
+ transition: none !important;
635
+ }
636
+ }