@lovett/ui 0.0.1
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.
- package/README.md +28 -0
- package/package.json +53 -0
- package/src/accordion.tsx +153 -0
- package/src/atoms.tsx +197 -0
- package/src/badge.tsx +91 -0
- package/src/brand-logo-tile.tsx +200 -0
- package/src/bulk-action-bar.tsx +104 -0
- package/src/button-group.tsx +46 -0
- package/src/button.tsx +93 -0
- package/src/calculator-shell-v2.tsx +380 -0
- package/src/calculator-shell.tsx +310 -0
- package/src/card.tsx +369 -0
- package/src/checkbox.tsx +114 -0
- package/src/chip-nav.tsx +347 -0
- package/src/choropleth.tsx +62 -0
- package/src/code-block.tsx +235 -0
- package/src/completion-ring.tsx +119 -0
- package/src/copy-field.tsx +169 -0
- package/src/data-grid/column-options-menu.tsx +127 -0
- package/src/data-grid/data-grid.tsx +391 -0
- package/src/data-grid/drawer-panel.tsx +146 -0
- package/src/data-grid/index.ts +29 -0
- package/src/data-grid/sortable-parts.tsx +215 -0
- package/src/data-grid/table-body.tsx +238 -0
- package/src/data-grid/table-elements.tsx +135 -0
- package/src/data-grid/table-header.tsx +95 -0
- package/src/data-grid/table-overlays.tsx +78 -0
- package/src/data-grid/table-parts.tsx +15 -0
- package/src/data-grid/table-summary-footer.tsx +101 -0
- package/src/data-grid/table-view.tsx +176 -0
- package/src/data-grid/types.ts +141 -0
- package/src/data-grid/use-grid-columns.ts +311 -0
- package/src/data-grid/use-grid-editing.ts +154 -0
- package/src/device-frame.tsx +274 -0
- package/src/dropdown-button.tsx +221 -0
- package/src/dropdown-menu.tsx +450 -0
- package/src/empty-state.tsx +85 -0
- package/src/file-upload/file-drop-zone.tsx +156 -0
- package/src/file-upload/file-preview-grid.tsx +39 -0
- package/src/file-upload/file-preview-item.tsx +175 -0
- package/src/file-upload/file-thumbnail.tsx +98 -0
- package/src/file-upload/file-upload-button.tsx +95 -0
- package/src/file-upload/index.ts +13 -0
- package/src/file-upload/use-file-upload.ts +371 -0
- package/src/filter-bar.tsx +125 -0
- package/src/filter-dropdown.tsx +289 -0
- package/src/floating-drawer.tsx +420 -0
- package/src/folder-card.tsx +188 -0
- package/src/folder-tree-picker.tsx +511 -0
- package/src/format.ts +117 -0
- package/src/frame-stack.tsx +73 -0
- package/src/hero-form-card.tsx +471 -0
- package/src/index.ts +372 -0
- package/src/input.tsx +130 -0
- package/src/kbd.tsx +41 -0
- package/src/lib/layout.ts +22 -0
- package/src/lib/utils.ts +6 -0
- package/src/markdown.tsx +214 -0
- package/src/menu-button.tsx +142 -0
- package/src/meta-previews/CreativeMedia.tsx +119 -0
- package/src/meta-previews/InstagramExplorePreview.tsx +340 -0
- package/src/meta-previews/InstagramFeedCarousel.tsx +245 -0
- package/src/meta-previews/InstagramFeedPreview.tsx +259 -0
- package/src/meta-previews/MetaFeedCarousel.tsx +228 -0
- package/src/meta-previews/MetaFeedPreview.tsx +294 -0
- package/src/meta-previews/MetaInstreamPreview.tsx +135 -0
- package/src/meta-previews/MetaMessengerPreview.tsx +795 -0
- package/src/meta-previews/MetaReelPreview.tsx +750 -0
- package/src/meta-previews/MetaRightColumnPreview.tsx +245 -0
- package/src/meta-previews/MetaSearchPreview.tsx +113 -0
- package/src/meta-previews/MetaStoryPreview.tsx +1132 -0
- package/src/meta-previews/ProfileAvatar.tsx +79 -0
- package/src/meta-previews/defaults.ts +27 -0
- package/src/meta-previews/index.ts +37 -0
- package/src/meta-previews/types.ts +219 -0
- package/src/metric-card.tsx +117 -0
- package/src/modal.tsx +131 -0
- package/src/option-tile.tsx +252 -0
- package/src/page-hero.tsx +78 -0
- package/src/page-shell.tsx +440 -0
- package/src/pagination.tsx +86 -0
- package/src/pill-button.tsx +103 -0
- package/src/profile-section.tsx +389 -0
- package/src/progress-bar.tsx +84 -0
- package/src/radio.tsx +172 -0
- package/src/range-slider.tsx +161 -0
- package/src/resizable.tsx +282 -0
- package/src/search-bar.tsx +81 -0
- package/src/segmented-pill.tsx +143 -0
- package/src/shell.tsx +464 -0
- package/src/slider.tsx +107 -0
- package/src/sortable-table.tsx +198 -0
- package/src/sortable.tsx +137 -0
- package/src/stat-row.tsx +132 -0
- package/src/stats-grid.tsx +38 -0
- package/src/step-loader.tsx +141 -0
- package/src/styles.css +562 -0
- package/src/tabs.tsx +143 -0
- package/src/tag-chip-input.tsx +394 -0
- package/src/textarea.tsx +39 -0
- package/src/toast.tsx +59 -0
- package/src/tokens.css +579 -0
package/src/styles.css
ADDED
|
@@ -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
|
+
}
|
package/src/tabs.tsx
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tabs — horizontal tab nav with optional leading icons.
|
|
3
|
+
*
|
|
4
|
+
* Promoted in ADR-023 (Phase 8 Keywords, first committed consumer).
|
|
5
|
+
* Cross-batch consumers: Phase 12 SEMSpec (5-tab review dashboard),
|
|
6
|
+
* Phase 3b Discovery (sub-tabs).
|
|
7
|
+
*
|
|
8
|
+
* Distinct from <ChipNav> (Phase 1 / Phase 7a) — ChipNav serves
|
|
9
|
+
* section scroll-spy + section-nav-as-tabs on profile pages. <Tabs>
|
|
10
|
+
* is the simpler horizontal-button-row for top-of-content tab navigation.
|
|
11
|
+
*
|
|
12
|
+
* Token discipline: all colors via public tokens. Active tab uses the
|
|
13
|
+
* accent token; inactive uses text-tertiary.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { ReactNode } from 'react'
|
|
17
|
+
|
|
18
|
+
export interface Tab {
|
|
19
|
+
id: string
|
|
20
|
+
label: string
|
|
21
|
+
/** Optional Lucide icon (h-3.5 w-3.5 to h-4 w-4 expected). */
|
|
22
|
+
icon?: ReactNode
|
|
23
|
+
/** Optional count badge rendered after the label. */
|
|
24
|
+
count?: number
|
|
25
|
+
disabled?: boolean
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface TabsProps {
|
|
29
|
+
tabs: Tab[]
|
|
30
|
+
activeTabId: string
|
|
31
|
+
onTabChange: (id: string) => void
|
|
32
|
+
/** Compact (12px label) or default (13px). */
|
|
33
|
+
size?: 'sm' | 'md'
|
|
34
|
+
/** Underline (default) or pill active-state. */
|
|
35
|
+
variant?: 'underline' | 'pill'
|
|
36
|
+
/** Stretch the tablist to full width with equal-width, centred tabs. */
|
|
37
|
+
stretch?: boolean
|
|
38
|
+
className?: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function Tabs({
|
|
42
|
+
tabs,
|
|
43
|
+
activeTabId,
|
|
44
|
+
onTabChange,
|
|
45
|
+
size = 'md',
|
|
46
|
+
variant = 'underline',
|
|
47
|
+
stretch = false,
|
|
48
|
+
className,
|
|
49
|
+
}: TabsProps) {
|
|
50
|
+
const textSize = size === 'sm' ? 'text-[12px]' : 'text-[13px]'
|
|
51
|
+
return (
|
|
52
|
+
<div
|
|
53
|
+
role="tablist"
|
|
54
|
+
className={
|
|
55
|
+
className ??
|
|
56
|
+
(variant === 'underline'
|
|
57
|
+
? `${stretch ? 'flex w-full' : 'inline-flex'} items-center gap-1 border-b`
|
|
58
|
+
: `${stretch ? 'flex w-full' : 'inline-flex'} items-center gap-1 p-1`)
|
|
59
|
+
}
|
|
60
|
+
style={
|
|
61
|
+
variant === 'underline'
|
|
62
|
+
? { borderColor: 'rgb(var(--border))' }
|
|
63
|
+
: {
|
|
64
|
+
background: 'rgb(var(--surface-overlay-soft))',
|
|
65
|
+
border: '1px solid rgb(var(--border))',
|
|
66
|
+
borderRadius: 'var(--radius-full)',
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
>
|
|
70
|
+
{tabs.map((tab) => {
|
|
71
|
+
const isActive = tab.id === activeTabId
|
|
72
|
+
const base = `inline-flex items-center gap-1.5 px-3 h-9 ${textSize} font-medium transition-colors${stretch ? ' flex-1 justify-center' : ''}`
|
|
73
|
+
const underlineClass = `${base} border-b-2 -mb-px`
|
|
74
|
+
const pillClass = `${base}`
|
|
75
|
+
const className = variant === 'underline' ? underlineClass : pillClass
|
|
76
|
+
|
|
77
|
+
const activeStyle =
|
|
78
|
+
variant === 'underline'
|
|
79
|
+
? {
|
|
80
|
+
color: 'rgb(var(--accent))',
|
|
81
|
+
borderColor: 'rgb(var(--accent))',
|
|
82
|
+
}
|
|
83
|
+
: {
|
|
84
|
+
// Pill variant — filled-accent active state.
|
|
85
|
+
color: 'rgb(var(--primary-foreground))',
|
|
86
|
+
background: 'rgb(var(--accent))',
|
|
87
|
+
border: '1px solid rgb(var(--accent))',
|
|
88
|
+
borderRadius: 'var(--radius-full)',
|
|
89
|
+
}
|
|
90
|
+
const inactiveStyle =
|
|
91
|
+
variant === 'underline'
|
|
92
|
+
? {
|
|
93
|
+
color: 'rgb(var(--text-tertiary))',
|
|
94
|
+
borderColor: 'transparent',
|
|
95
|
+
}
|
|
96
|
+
: {
|
|
97
|
+
color: 'rgb(var(--text-secondary))',
|
|
98
|
+
background: 'transparent',
|
|
99
|
+
border: '1px solid transparent',
|
|
100
|
+
borderRadius: 'var(--radius-full)',
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<button
|
|
105
|
+
key={tab.id}
|
|
106
|
+
role="tab"
|
|
107
|
+
type="button"
|
|
108
|
+
aria-selected={isActive}
|
|
109
|
+
disabled={tab.disabled}
|
|
110
|
+
onClick={() => onTabChange(tab.id)}
|
|
111
|
+
className={`${className} ${tab.disabled ? 'cursor-not-allowed opacity-40' : 'cursor-pointer'}`}
|
|
112
|
+
style={isActive ? activeStyle : inactiveStyle}
|
|
113
|
+
>
|
|
114
|
+
{tab.icon && (
|
|
115
|
+
<span className="flex-shrink-0" aria-hidden>
|
|
116
|
+
{tab.icon}
|
|
117
|
+
</span>
|
|
118
|
+
)}
|
|
119
|
+
{tab.label}
|
|
120
|
+
{tab.count !== undefined && (
|
|
121
|
+
<span
|
|
122
|
+
className="ml-0.5 inline-flex min-w-[1.1rem] items-center justify-center rounded-full px-1 text-[10.5px] font-semibold tabular-nums"
|
|
123
|
+
style={
|
|
124
|
+
isActive && variant === 'pill'
|
|
125
|
+
? {
|
|
126
|
+
background: 'rgb(var(--primary-foreground) / 0.22)',
|
|
127
|
+
color: 'rgb(var(--primary-foreground))',
|
|
128
|
+
}
|
|
129
|
+
: {
|
|
130
|
+
background: 'rgb(var(--surface-overlay))',
|
|
131
|
+
color: 'rgb(var(--text-tertiary))',
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
>
|
|
135
|
+
{tab.count}
|
|
136
|
+
</span>
|
|
137
|
+
)}
|
|
138
|
+
</button>
|
|
139
|
+
)
|
|
140
|
+
})}
|
|
141
|
+
</div>
|
|
142
|
+
)
|
|
143
|
+
}
|