@lovett/ui 0.0.3 → 0.0.5

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,562 @@
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
+ * prefers-reduced-motion block — accessibility
29
+ *
30
+ * Out of scope for Phase 1: .empty, .skel, .field*, .pg-header, .search-bar,
31
+ * .breadcrumbs, .topbar, .checkbox, .radio, .switch, .reasoning-pulse,
32
+ * .chat-md-list, .selection-reply-btn, ::selection. Those land when their
33
+ * consumers (Field, EmptyState, PageHeader, etc.) arrive in later phases.
34
+ */
35
+
36
+ /* ---- CARD SURFACE ---- */
37
+ .ds-card-surface {
38
+ background: rgb(var(--bg-card));
39
+ border: 1px solid rgb(var(--border-card));
40
+ border-radius: var(--radius-lg);
41
+ box-shadow: var(--card-shadow-rest);
42
+ }
43
+ .ds-card-surface[data-framed="true"] {
44
+ background: rgb(var(--card-frame-bg));
45
+ }
46
+ .ds-card-surface[data-interactive="true"] {
47
+ cursor: pointer;
48
+ transition:
49
+ transform var(--dur-fast) var(--ease-out),
50
+ box-shadow var(--dur-fast) var(--ease-out),
51
+ border-color var(--dur-fast) var(--ease-out);
52
+ }
53
+ .ds-card-surface[data-interactive="true"]:hover {
54
+ transform: translateY(-1px);
55
+ box-shadow: var(--card-shadow-hover);
56
+ }
57
+
58
+ /* ---- CARD TRAY ---- */
59
+ .ds-card-tray {
60
+ margin: var(--tray-inset);
61
+ background: rgb(var(--tray-bg));
62
+ /* Owner ask: visible 1px border on the inner tray so the tray surface
63
+ * reads as distinct from the surrounding card-frame. Uses the same
64
+ * --border-card the outer card uses for consistency. */
65
+ border: 1px solid rgb(var(--border-card));
66
+ border-radius: var(--radius-tray);
67
+ /* ADR-076 D7: recessed-tray shadow — formalizes the "bright inset
68
+ * panel" reading the tray already carries via the frame/tray
69
+ * brightness step. Subtle in both themes so every existing
70
+ * ProfileSection / Card.Tray gains depth without a visible regression. */
71
+ box-shadow: var(--tray-shadow);
72
+ overflow: hidden;
73
+ }
74
+ .ds-card-tray:not(:last-child) {
75
+ margin-bottom: 0;
76
+ }
77
+
78
+ /* ---- BUTTONS ---- */
79
+ .btn {
80
+ display: inline-flex;
81
+ align-items: center;
82
+ justify-content: center;
83
+ gap: 6px;
84
+ height: var(--ctrl-md);
85
+ padding: 0 16px;
86
+ /* ADR-026 D1 — bumped from --radius-sm (8px) to --radius-md (12px).
87
+ * Inspired by gray-ui-csm rounded-xl button treatment (recon §13 DQ-1). */
88
+ border-radius: var(--radius-md);
89
+ font-size: 13px;
90
+ font-weight: 500;
91
+ cursor: pointer;
92
+ transition:
93
+ background var(--dur-fast) var(--ease-out),
94
+ border-color var(--dur-fast) var(--ease-out),
95
+ color var(--dur-fast) var(--ease-out),
96
+ box-shadow var(--dur-fast) var(--ease-out),
97
+ transform var(--dur-fast) var(--ease-out),
98
+ filter var(--dur-fast) var(--ease-out);
99
+ border: 1px solid transparent;
100
+ font-family: var(--font-sans);
101
+ white-space: nowrap;
102
+ user-select: none;
103
+ }
104
+ .btn:focus-visible {
105
+ outline: none;
106
+ box-shadow: var(--ring-focus);
107
+ }
108
+
109
+ .btn-primary {
110
+ background: rgb(var(--accent));
111
+ color: white;
112
+ border-color: rgb(var(--accent));
113
+ }
114
+ .btn-primary:hover {
115
+ background: rgb(var(--accent-hover));
116
+ border-color: rgb(var(--accent-hover));
117
+ box-shadow: var(--shadow-glow);
118
+ transform: translateY(-1px);
119
+ }
120
+ .btn-primary:active {
121
+ transform: translateY(0);
122
+ box-shadow: 0 1px 3px rgb(var(--accent-glow));
123
+ }
124
+
125
+ .btn-secondary {
126
+ background: rgb(var(--bg-card));
127
+ color: rgb(var(--foreground));
128
+ border-color: rgb(var(--border));
129
+ }
130
+ .btn-secondary:hover {
131
+ background: rgb(var(--bg-card-hover));
132
+ border-color: rgb(var(--border-strong));
133
+ }
134
+
135
+ .btn-outline {
136
+ background: transparent;
137
+ color: rgb(var(--foreground));
138
+ border-color: rgb(var(--border-strong));
139
+ }
140
+ .btn-outline:hover {
141
+ background: rgb(var(--surface-overlay-soft));
142
+ border-color: rgb(var(--foreground));
143
+ }
144
+
145
+ /* FilterBar (ADR-121) — match the filter pills' rest border to the search
146
+ field's input border (--border-input is a touch stronger than the generic
147
+ --border the secondary button uses), so the search box + filter chips read
148
+ as one bordered set. Hover already aligns on --border-strong; the active
149
+ (engaged) pill keeps its inline accent border. */
150
+ .filter-bar .btn-secondary {
151
+ border-color: rgb(var(--border-input));
152
+ }
153
+ .filter-bar .btn-secondary:hover {
154
+ border-color: rgb(var(--border-strong));
155
+ }
156
+
157
+ .btn-ghost {
158
+ background: transparent;
159
+ color: rgb(var(--muted-foreground));
160
+ }
161
+ .btn-ghost:hover {
162
+ background: rgb(var(--surface-overlay-soft));
163
+ color: rgb(var(--foreground));
164
+ }
165
+
166
+ .btn-destructive {
167
+ background: rgb(var(--destructive));
168
+ color: white;
169
+ border-color: rgb(var(--destructive));
170
+ }
171
+ .btn-destructive:hover {
172
+ filter: brightness(0.94);
173
+ }
174
+
175
+ .btn-destructive-soft {
176
+ background: rgb(var(--destructive) / 0.10);
177
+ color: rgb(var(--destructive));
178
+ border-color: rgb(var(--destructive) / 0.25);
179
+ }
180
+ .btn-destructive-soft:hover {
181
+ background: rgb(var(--destructive) / 0.15);
182
+ }
183
+
184
+ .btn-icon {
185
+ width: var(--ctrl-md);
186
+ height: var(--ctrl-md);
187
+ padding: 0;
188
+ background: transparent;
189
+ color: rgb(var(--muted-foreground));
190
+ border: 1px solid rgb(var(--border));
191
+ }
192
+ .btn-icon:hover {
193
+ color: rgb(var(--foreground));
194
+ background: rgb(var(--surface-overlay-soft));
195
+ border-color: rgb(var(--border-strong));
196
+ }
197
+ /* ADR-026 D3 — circular IconButton variant (Button shape="circle"
198
+ * combined with variant="icon"). Used by AppHeader + future header
199
+ * surfaces per gray-ui-csm recon §13 DQ-3. Pill-radius wins over the
200
+ * .btn base radius when both classes are present. */
201
+ .btn-icon.is-circle {
202
+ border-radius: 999px;
203
+ }
204
+
205
+ .btn-toolbar {
206
+ background: rgb(var(--bg-elevated));
207
+ color: rgb(var(--text-secondary));
208
+ border-color: rgb(var(--border));
209
+ border-radius: 999px;
210
+ }
211
+ .btn-toolbar:hover {
212
+ background: rgb(var(--bg-card-hover));
213
+ color: rgb(var(--foreground));
214
+ border-color: rgb(var(--border-strong));
215
+ }
216
+
217
+ /* Size modifiers */
218
+ .btn.size-sm {
219
+ height: var(--ctrl-sm);
220
+ padding: 0 12px;
221
+ font-size: 12.5px;
222
+ gap: 5px;
223
+ }
224
+ .btn.size-sm.btn-icon {
225
+ width: var(--ctrl-sm);
226
+ padding: 0;
227
+ }
228
+ .btn.size-lg {
229
+ height: var(--ctrl-lg);
230
+ padding: 0 22px;
231
+ font-size: 14px;
232
+ gap: 8px;
233
+ }
234
+ .btn.size-lg.btn-icon {
235
+ width: var(--ctrl-lg);
236
+ padding: 0;
237
+ }
238
+
239
+ /* Loading + disabled */
240
+ .btn.is-loading {
241
+ color: transparent;
242
+ position: relative;
243
+ pointer-events: none;
244
+ }
245
+ .btn.is-loading::after {
246
+ content: '';
247
+ position: absolute;
248
+ inset: 0;
249
+ margin: auto;
250
+ width: 14px;
251
+ height: 14px;
252
+ border: 2px solid currentColor;
253
+ border-top-color: transparent;
254
+ border-radius: 999px;
255
+ color: white;
256
+ animation: btn-spin 720ms linear infinite;
257
+ }
258
+ .btn-secondary.is-loading::after,
259
+ .btn-ghost.is-loading::after,
260
+ .btn-outline.is-loading::after,
261
+ .btn-destructive-soft.is-loading::after,
262
+ .btn-toolbar.is-loading::after {
263
+ color: rgb(var(--foreground));
264
+ }
265
+ .btn[disabled] {
266
+ opacity: 0.5;
267
+ cursor: not-allowed;
268
+ transform: none;
269
+ box-shadow: none;
270
+ }
271
+ @keyframes btn-spin {
272
+ to {
273
+ transform: rotate(360deg);
274
+ }
275
+ }
276
+
277
+ /* Joined button group — typed React wrapper at packages/ui/src/button-group.tsx.
278
+ * ADR-026 D5 — corner rules bumped from --radius-sm to --radius-md to match
279
+ * the .btn base radius bump (D1). Inspired by gray-ui-csm view-switcher
280
+ * (recon §13 DQ-5). */
281
+ .btn-group {
282
+ display: inline-flex;
283
+ }
284
+ .btn-group .btn {
285
+ border-radius: 0;
286
+ border-right-width: 0;
287
+ }
288
+ .btn-group .btn:first-child {
289
+ border-top-left-radius: var(--radius-md);
290
+ border-bottom-left-radius: var(--radius-md);
291
+ }
292
+ .btn-group .btn:last-child {
293
+ border-top-right-radius: var(--radius-md);
294
+ border-bottom-right-radius: var(--radius-md);
295
+ border-right-width: 1px;
296
+ }
297
+
298
+ /* ---- INPUT SHELL ---- */
299
+ .input-shell {
300
+ position: relative;
301
+ display: flex;
302
+ align-items: stretch;
303
+ background: rgb(var(--bg-input));
304
+ border: 1px solid rgb(var(--border-input));
305
+ border-radius: var(--radius-md);
306
+ transition: border-color 160ms, box-shadow 160ms, background 160ms;
307
+ height: var(--ctrl-md);
308
+ }
309
+ .input-shell:hover {
310
+ border-color: rgb(var(--border-strong));
311
+ background: rgb(var(--bg-input-hover));
312
+ }
313
+ .input-shell:focus-within {
314
+ border-color: rgb(var(--accent));
315
+ box-shadow: var(--ring-focus);
316
+ background: rgb(var(--bg-input));
317
+ }
318
+ .input-shell.is-error {
319
+ border-color: rgb(var(--destructive));
320
+ }
321
+ .input-shell.is-error:focus-within {
322
+ box-shadow: var(--ring-error);
323
+ }
324
+ .input-shell.is-disabled {
325
+ opacity: 0.55;
326
+ pointer-events: none;
327
+ background: rgb(var(--surface-overlay-soft));
328
+ }
329
+ .input-shell input,
330
+ .input-shell select,
331
+ .input-shell textarea {
332
+ flex: 1;
333
+ min-width: 0;
334
+ appearance: none;
335
+ border: 0;
336
+ outline: 0;
337
+ background: transparent;
338
+ color: rgb(var(--foreground));
339
+ font: 500 13.5px/1.4 var(--font-sans);
340
+ padding: 0 12px;
341
+ }
342
+ .input-shell.has-leading input,
343
+ .input-shell.has-leading select {
344
+ padding-left: 0;
345
+ }
346
+ .input-shell.has-trailing input,
347
+ .input-shell.has-trailing select {
348
+ padding-right: 0;
349
+ }
350
+ .input-shell input::placeholder,
351
+ .input-shell textarea::placeholder {
352
+ color: rgb(var(--text-tertiary));
353
+ }
354
+
355
+ .input-shell .affix {
356
+ display: inline-flex;
357
+ align-items: center;
358
+ justify-content: center;
359
+ padding: 0 10px;
360
+ color: rgb(var(--text-tertiary));
361
+ font: 500 12.5px/1 var(--font-sans);
362
+ pointer-events: none;
363
+ flex-shrink: 0;
364
+ }
365
+ .input-shell .affix.action {
366
+ pointer-events: auto;
367
+ cursor: pointer;
368
+ background: transparent;
369
+ border: 0;
370
+ transition: color 120ms;
371
+ }
372
+ .input-shell .affix.action:hover {
373
+ color: rgb(var(--foreground));
374
+ }
375
+ .input-shell .affix.divider {
376
+ border-left: 1px solid rgb(var(--border));
377
+ color: rgb(var(--muted-foreground));
378
+ font-size: 11.5px;
379
+ }
380
+
381
+ /* Numeric variant (ADR-076 D8) — monospace + tabular figures so digits
382
+ * align as values change. Orthogonal to the size modifiers. */
383
+ .input-shell.is-numeric input {
384
+ font-family: var(--font-mono);
385
+ font-variant-numeric: tabular-nums;
386
+ font-weight: 600;
387
+ }
388
+
389
+ .input-shell.size-sm {
390
+ height: var(--ctrl-sm);
391
+ }
392
+ .input-shell.size-sm input,
393
+ .input-shell.size-sm select {
394
+ font-size: 12.5px;
395
+ padding: 0 10px;
396
+ }
397
+ .input-shell.size-lg {
398
+ height: var(--ctrl-lg);
399
+ }
400
+ .input-shell.size-lg input,
401
+ .input-shell.size-lg select {
402
+ font-size: 14.5px;
403
+ padding: 0 14px;
404
+ }
405
+
406
+ /* Textarea variant — auto height */
407
+ .input-shell.textarea-shell {
408
+ height: auto;
409
+ align-items: flex-start;
410
+ padding: 8px 0;
411
+ }
412
+ .input-shell.textarea-shell textarea {
413
+ min-height: 80px;
414
+ resize: vertical;
415
+ padding: 0 12px;
416
+ font-family: var(--font-sans);
417
+ }
418
+
419
+ /* ---- SCROLLBAR-HIDE UTILITY (ChipNav) ---- */
420
+ .scrollbar-hide {
421
+ -ms-overflow-style: none;
422
+ scrollbar-width: none;
423
+ }
424
+ .scrollbar-hide::-webkit-scrollbar {
425
+ display: none;
426
+ }
427
+
428
+ /* ---- PROGRESS BAR INDETERMINATE ---- */
429
+ /* Used by `<ProgressBar>` primitive (no `percent` prop) — animated
430
+ sweep across the track. Added 2026-05-18 per ADR-020 D10 promotion. */
431
+ .progress-indeterminate {
432
+ width: 40%;
433
+ animation: progress-indeterminate-sweep 1.4s ease-in-out infinite;
434
+ }
435
+ @keyframes progress-indeterminate-sweep {
436
+ 0% { transform: translateX(-100%); }
437
+ 100% { transform: translateX(250%); }
438
+ }
439
+
440
+ /* ---- RANGE SLIDER ----
441
+ * Dual-thumb range slider. Two native range inputs are stacked over a
442
+ * shared track + fill. The inputs themselves are transparent and
443
+ * pointer-events:none so they don't block each other — only the THUMB
444
+ * pseudo-elements re-enable pointer events. Thumb styling can only be
445
+ * done via ::-webkit-slider-thumb / ::-moz-range-thumb pseudo-elements,
446
+ * which is why this lives in CSS rather than inline styles. */
447
+ .ds-range-input {
448
+ position: absolute;
449
+ left: 0;
450
+ top: 50%;
451
+ transform: translateY(-50%);
452
+ width: 100%;
453
+ height: 16px;
454
+ margin: 0;
455
+ background: transparent;
456
+ pointer-events: none;
457
+ -webkit-appearance: none;
458
+ appearance: none;
459
+ }
460
+ .ds-range-input::-webkit-slider-runnable-track {
461
+ background: transparent;
462
+ height: 16px;
463
+ }
464
+ .ds-range-input::-moz-range-track {
465
+ background: transparent;
466
+ height: 16px;
467
+ }
468
+ .ds-range-input::-webkit-slider-thumb {
469
+ -webkit-appearance: none;
470
+ appearance: none;
471
+ pointer-events: auto;
472
+ height: 16px;
473
+ width: 16px;
474
+ border-radius: var(--radius-full);
475
+ background: rgb(var(--bg-elevated));
476
+ border: 2px solid rgb(var(--accent));
477
+ box-shadow: var(--shadow-sm, 0 1px 2px rgb(0 0 0 / 0.12));
478
+ cursor: pointer;
479
+ transition: transform var(--dur-fast) var(--ease-out);
480
+ }
481
+ .ds-range-input::-webkit-slider-thumb:hover {
482
+ transform: scale(1.12);
483
+ }
484
+ .ds-range-input::-moz-range-thumb {
485
+ pointer-events: auto;
486
+ height: 16px;
487
+ width: 16px;
488
+ border-radius: var(--radius-full);
489
+ background: rgb(var(--bg-elevated));
490
+ border: 2px solid rgb(var(--accent));
491
+ box-shadow: var(--shadow-sm, 0 1px 2px rgb(0 0 0 / 0.12));
492
+ cursor: pointer;
493
+ }
494
+ .ds-range-input:focus-visible::-webkit-slider-thumb {
495
+ outline: 2px solid rgb(var(--accent));
496
+ outline-offset: 2px;
497
+ }
498
+ .ds-range-input:focus-visible::-moz-range-thumb {
499
+ outline: 2px solid rgb(var(--accent));
500
+ outline-offset: 2px;
501
+ }
502
+ .ds-range-input:disabled::-webkit-slider-thumb {
503
+ border-color: rgb(var(--border-strong));
504
+ cursor: not-allowed;
505
+ }
506
+ .ds-range-input:disabled::-moz-range-thumb {
507
+ border-color: rgb(var(--border-strong));
508
+ cursor: not-allowed;
509
+ }
510
+
511
+ /* ---- RESIZABLE HANDLE ----
512
+ * Hover/drag tint comes from inline styles + Tailwind utility (handled in
513
+ * resizable.tsx). The transition lives here so prefers-reduced-motion can
514
+ * suppress it without touching the JS — drag responsiveness is never
515
+ * gated on animation, only the colour fade.
516
+ *
517
+ * MAJ-1 (Sidebar-A hot-fix, ADR-049 D9 §212): on active drag the line
518
+ * grows from 4 px → 8 px so the user has a positive confirmation of the
519
+ * grab. Implemented with `transform: scale...` on the cross-axis so the
520
+ * line stays centred on its midpoint — no margin recalc, no layout
521
+ * thrash. `transform-origin: center` is the CSS default but stated for
522
+ * clarity. The orientation comes from the `data-orientation` attribute
523
+ * on the element (set by resizable.tsx alongside `data-dragging`).
524
+ */
525
+ .resizable-handle {
526
+ transition: background-color var(--dur-fast) var(--ease-out),
527
+ transform var(--dur-fast) var(--ease-out);
528
+ transform-origin: center center;
529
+ }
530
+ .resizable-handle[data-orientation='vertical'][data-dragging] {
531
+ transform: scaleX(2);
532
+ }
533
+ .resizable-handle[data-orientation='horizontal'][data-dragging] {
534
+ transform: scaleY(2);
535
+ }
536
+
537
+ /* ---- REDUCED MOTION ---- */
538
+ @media (prefers-reduced-motion: reduce) {
539
+ .btn,
540
+ .btn-primary,
541
+ .input-shell {
542
+ transition: none !important;
543
+ animation: none !important;
544
+ }
545
+ .btn-primary:hover,
546
+ .btn:hover {
547
+ transform: none !important;
548
+ }
549
+ .progress-indeterminate {
550
+ animation: none !important;
551
+ width: 100% !important;
552
+ }
553
+ .ds-range-input::-webkit-slider-thumb {
554
+ transition: none !important;
555
+ }
556
+ .ds-range-input::-webkit-slider-thumb:hover {
557
+ transform: none !important;
558
+ }
559
+ .resizable-handle {
560
+ transition: none !important;
561
+ }
562
+ }