@adea-ai/ui 0.101.1 → 0.103.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.
- package/README.md +27 -11
- package/dist/components/composites/appearance-editor/appearance-choices.d.ts +7 -0
- package/dist/components/composites/appearance-editor/appearance-choices.d.ts.map +1 -1
- package/dist/components/composites/appearance-editor/appearance-choices.js +44 -3
- package/dist/components/composites/appearance-editor/appearance-choices.js.map +1 -1
- package/dist/components/composites/appearance-editor/appearance-editor.d.ts.map +1 -1
- package/dist/components/composites/appearance-editor/appearance-editor.js +7 -3
- package/dist/components/composites/appearance-editor/appearance-editor.js.map +1 -1
- package/dist/components/composites/appearance-editor/appearance-types.d.ts +8 -0
- package/dist/components/composites/appearance-editor/appearance-types.d.ts.map +1 -1
- package/dist/components/composites/update-dialog/update-dialog.d.ts +15 -2
- package/dist/components/composites/update-dialog/update-dialog.d.ts.map +1 -1
- package/dist/components/composites/update-dialog/update-dialog.js +80 -16
- package/dist/components/composites/update-dialog/update-dialog.js.map +1 -1
- package/dist/components/theme/appearance-panel.d.ts +2 -1
- package/dist/components/theme/appearance-panel.d.ts.map +1 -1
- package/dist/components/theme/appearance-panel.js +62 -22
- package/dist/components/theme/appearance-panel.js.map +1 -1
- package/dist/components/theme/theme-provider.d.ts +13 -2
- package/dist/components/theme/theme-provider.d.ts.map +1 -1
- package/dist/components/theme/theme-provider.js +7 -4
- package/dist/components/theme/theme-provider.js.map +1 -1
- package/dist/components/ui/board/board.d.ts +39 -2
- package/dist/components/ui/board/board.d.ts.map +1 -1
- package/dist/components/ui/board/board.js +230 -92
- package/dist/components/ui/board/board.js.map +1 -1
- package/dist/components/ui/board/index.d.ts +1 -1
- package/dist/components/ui/board/index.d.ts.map +1 -1
- package/dist/components/ui/board/index.js +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/lib/theme-accents.d.ts +9 -0
- package/dist/lib/theme-accents.d.ts.map +1 -0
- package/dist/lib/theme-accents.js +41 -0
- package/dist/lib/theme-accents.js.map +1 -0
- package/dist/lib/themes.d.ts +69 -5
- package/dist/lib/themes.d.ts.map +1 -1
- package/dist/lib/themes.js +100 -7
- package/dist/lib/themes.js.map +1 -1
- package/dist/r/appearance-editor.json +1 -1
- package/dist/r/board.json +2 -0
- package/dist/r/lib.json +6 -1
- package/dist/r/registry.json +10 -2
- package/dist/r/src/components/composites/appearance-editor/appearance-choices.tsx +41 -4
- package/dist/r/src/components/composites/appearance-editor/appearance-editor.tsx +12 -6
- package/dist/r/src/components/composites/appearance-editor/appearance-types.ts +8 -0
- package/dist/r/src/components/composites/update-dialog/update-dialog.tsx +90 -10
- package/dist/r/src/components/theme/appearance-panel.tsx +60 -29
- package/dist/r/src/components/theme/theme-provider.tsx +31 -7
- package/dist/r/src/components/ui/board/board.tsx +231 -77
- package/dist/r/src/components/ui/board/index.ts +2 -0
- package/dist/r/src/lib/theme-accents.ts +44 -0
- package/dist/r/src/lib/themes.ts +116 -5
- package/dist/r/src/styles/base.css +11 -0
- package/dist/r/theming.json +1 -0
- package/package.json +2 -2
- package/registry.json +10 -2
- package/src/components/composites/appearance-editor/appearance-choices.tsx +41 -4
- package/src/components/composites/appearance-editor/appearance-editor.tsx +12 -6
- package/src/components/composites/appearance-editor/appearance-types.ts +8 -0
- package/src/components/composites/update-dialog/update-dialog.tsx +90 -10
- package/src/components/theme/appearance-panel.tsx +60 -29
- package/src/components/theme/theme-provider.tsx +31 -7
- package/src/components/ui/board/board.tsx +231 -77
- package/src/components/ui/board/index.ts +2 -0
- package/src/index.ts +7 -0
- package/src/lib/theme-accents.ts +44 -0
- package/src/lib/themes.ts +116 -5
- package/src/styles/base.css +11 -0
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
|
+
type ComponentProps,
|
|
2
3
|
createEffect,
|
|
3
4
|
createMemo,
|
|
4
5
|
createSignal,
|
|
5
6
|
For,
|
|
7
|
+
Index,
|
|
6
8
|
onCleanup,
|
|
7
9
|
Show,
|
|
8
10
|
splitProps,
|
|
@@ -10,6 +12,8 @@ import {
|
|
|
10
12
|
} from 'solid-js'
|
|
11
13
|
import { cva, type VariantProps } from '../../../lib/variants'
|
|
12
14
|
import { cn } from '../../../lib/utils'
|
|
15
|
+
import { badgeVariants } from '../badge/badge'
|
|
16
|
+
import { headingVariants, textVariants } from '../typography'
|
|
13
17
|
|
|
14
18
|
/**
|
|
15
19
|
* Board.
|
|
@@ -37,13 +41,48 @@ import { cn } from '../../../lib/utils'
|
|
|
37
41
|
* of drag props. That is deliberate: a card's contents are domain — a title, a
|
|
38
42
|
* priority tag, an assignee — and a board that rendered them itself could only
|
|
39
43
|
* ever serve one product.
|
|
44
|
+
*
|
|
45
|
+
* `collapseEmpty` folds a column with no cards to a strip one line of text wide,
|
|
46
|
+
* its label turned on its side. A board of seven lanes where three are empty
|
|
47
|
+
* otherwise spends half its width on nothing, and the lanes with work in them
|
|
48
|
+
* are the ones that need the room. A folded lane is still a full-height drop
|
|
49
|
+
* target and still accepts a keyboard move, so nothing about operating the
|
|
50
|
+
* board changes; it does not unfold under a drag, because a lane that grows
|
|
51
|
+
* under the pointer moves every lane after it and the drop lands somewhere else.
|
|
52
|
+
*
|
|
53
|
+
* Give the board a height (`class="h-full"`) and each lane scrolls on its own;
|
|
54
|
+
* leave it unconstrained and lanes grow with their cards and the page scrolls.
|
|
40
55
|
*/
|
|
41
56
|
|
|
57
|
+
/**
|
|
58
|
+
* A lane's identifying colour, drawn as a dot before its label. The chart hues,
|
|
59
|
+
* in the order the token set gives them, because a lane colour tells lanes apart
|
|
60
|
+
* like a series does; it carries no status. `neutral` is for a lane that should
|
|
61
|
+
* not draw the eye — a backlog, an archive.
|
|
62
|
+
*/
|
|
63
|
+
export type BoardTone =
|
|
64
|
+
| 'neutral'
|
|
65
|
+
| 'chart-1'
|
|
66
|
+
| 'chart-2'
|
|
67
|
+
| 'chart-3'
|
|
68
|
+
| 'chart-4'
|
|
69
|
+
| 'chart-5'
|
|
70
|
+
| 'chart-6'
|
|
71
|
+
|
|
42
72
|
export type BoardColumn = Readonly<{
|
|
43
73
|
id: string
|
|
44
74
|
label: string
|
|
45
|
-
/**
|
|
75
|
+
/** The number of cards, shown as an accent count beside the label. */
|
|
76
|
+
count?: number
|
|
77
|
+
/** A limit or any other note, shown after the count. */
|
|
46
78
|
meta?: JSX.Element
|
|
79
|
+
/** The lane's dot colour. Omitted, the header has no dot. */
|
|
80
|
+
tone?: BoardTone
|
|
81
|
+
/**
|
|
82
|
+
* `false` keeps this lane open when it is empty under `collapseEmpty` — the
|
|
83
|
+
* inbox lane new cards arrive in, which should always show where they land.
|
|
84
|
+
*/
|
|
85
|
+
collapsible?: boolean
|
|
47
86
|
/** Dim the column and refuse drops — a lane that is closed or read-only. */
|
|
48
87
|
disabled?: boolean
|
|
49
88
|
}>
|
|
@@ -70,6 +109,8 @@ export type BoardProps<T> = {
|
|
|
70
109
|
emptyColumn?: (column: BoardColumn) => JSX.Element
|
|
71
110
|
/** The board's accessible name. Defaults to "Board". */
|
|
72
111
|
label?: string
|
|
112
|
+
/** Fold columns with no cards to a narrow strip with a sideways label. */
|
|
113
|
+
collapseEmpty?: boolean
|
|
73
114
|
class?: string
|
|
74
115
|
}
|
|
75
116
|
|
|
@@ -78,6 +119,40 @@ const cardDragState = {
|
|
|
78
119
|
dragging: 'opacity-40',
|
|
79
120
|
} as const
|
|
80
121
|
|
|
122
|
+
const toneDot: Record<BoardTone, string> = {
|
|
123
|
+
neutral: 'bg-muted-foreground',
|
|
124
|
+
'chart-1': 'bg-chart-1',
|
|
125
|
+
'chart-2': 'bg-chart-2',
|
|
126
|
+
'chart-3': 'bg-chart-3',
|
|
127
|
+
'chart-4': 'bg-chart-4',
|
|
128
|
+
'chart-5': 'bg-chart-5',
|
|
129
|
+
'chart-6': 'bg-chart-6',
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function BoardColumnDot(props: { tone?: BoardTone }) {
|
|
133
|
+
return (
|
|
134
|
+
<Show when={props.tone}>
|
|
135
|
+
{(tone) => (
|
|
136
|
+
<span aria-hidden="true" class={cn('size-2 shrink-0 rounded-full', toneDot[tone()])} />
|
|
137
|
+
)}
|
|
138
|
+
</Show>
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function BoardColumnCount(props: { count?: number }) {
|
|
143
|
+
return (
|
|
144
|
+
<Show when={props.count !== undefined}>
|
|
145
|
+
{/* The Badge's classes, not the component: Board stays free of Kobalte. */}
|
|
146
|
+
<span
|
|
147
|
+
class={cn(badgeVariants({ variant: 'subtle', size: 'sm' }), 'tabular-nums')}
|
|
148
|
+
data-slot="board-column-count"
|
|
149
|
+
>
|
|
150
|
+
{props.count}
|
|
151
|
+
</span>
|
|
152
|
+
</Show>
|
|
153
|
+
)
|
|
154
|
+
}
|
|
155
|
+
|
|
81
156
|
export function Board<T>(props: BoardProps<T>) {
|
|
82
157
|
const [local] = splitProps(props, [
|
|
83
158
|
'columns',
|
|
@@ -89,6 +164,7 @@ export function Board<T>(props: BoardProps<T>) {
|
|
|
89
164
|
'children',
|
|
90
165
|
'emptyColumn',
|
|
91
166
|
'label',
|
|
167
|
+
'collapseEmpty',
|
|
92
168
|
'class',
|
|
93
169
|
])
|
|
94
170
|
|
|
@@ -135,9 +211,15 @@ export function Board<T>(props: BoardProps<T>) {
|
|
|
135
211
|
})
|
|
136
212
|
})
|
|
137
213
|
|
|
138
|
-
|
|
214
|
+
/**
|
|
215
|
+
* `always` is for a pointer drop: dragging a card is a direct act on it, so it
|
|
216
|
+
* ends focused wherever focus was. Chromium also blurs a focused control
|
|
217
|
+
* inside a draggable once the native drag starts, so "was focus in the card"
|
|
218
|
+
* cannot be read at drop time.
|
|
219
|
+
*/
|
|
220
|
+
const prepareFocus = (item: T, columnId: string, source?: HTMLElement, always = false) => {
|
|
139
221
|
const card = source ?? cardElements.get(local.itemId(item))
|
|
140
|
-
if (card
|
|
222
|
+
if (card && (always || card.contains(card.ownerDocument.activeElement))) {
|
|
141
223
|
setPendingFocus({ itemId: local.itemId(item), columnId, source: card })
|
|
142
224
|
}
|
|
143
225
|
}
|
|
@@ -163,7 +245,7 @@ export function Board<T>(props: BoardProps<T>) {
|
|
|
163
245
|
setDragging(null)
|
|
164
246
|
setOverColumn(null)
|
|
165
247
|
if (!item || !from || from === column.id || !accepted) return
|
|
166
|
-
prepareFocus(item, column.id)
|
|
248
|
+
prepareFocus(item, column.id, undefined, true)
|
|
167
249
|
local.onMove({ itemId: local.itemId(item), from, to: column.id })
|
|
168
250
|
}
|
|
169
251
|
|
|
@@ -198,97 +280,144 @@ export function Board<T>(props: BoardProps<T>) {
|
|
|
198
280
|
with more columns than fit, has nothing focusable to scroll from.
|
|
199
281
|
*/
|
|
200
282
|
<div
|
|
201
|
-
class={cn('flex min-h-0 gap-
|
|
283
|
+
class={cn('flex min-h-0 gap-3 overflow-x-auto pb-2', local.class)}
|
|
202
284
|
role="region"
|
|
203
285
|
aria-label={local.label ?? 'Board'}
|
|
204
286
|
tabindex="0"
|
|
205
287
|
>
|
|
206
|
-
|
|
288
|
+
{/*
|
|
289
|
+
Index, not For: callers derive columns (a count, a tone) and hand over
|
|
290
|
+
fresh objects whenever a card moves. Keyed by identity, every lane would
|
|
291
|
+
remount on each move and take the moved card's restored focus with it.
|
|
292
|
+
*/}
|
|
293
|
+
<Index each={local.columns}>
|
|
207
294
|
{(column) => {
|
|
208
|
-
const items = () => byColumn().get(column.id) ?? []
|
|
295
|
+
const items = () => byColumn().get(column().id) ?? []
|
|
296
|
+
const collapsed = () =>
|
|
297
|
+
Boolean(local.collapseEmpty) && column().collapsible !== false && items().length === 0
|
|
298
|
+
const over = () => overColumn() === column().id
|
|
209
299
|
return (
|
|
210
300
|
<section
|
|
211
301
|
class={cn(
|
|
212
|
-
'flex
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
302
|
+
'flex shrink-0 flex-col rounded-lg border border-border bg-muted/40 transition-colors',
|
|
303
|
+
{
|
|
304
|
+
'w-72': !collapsed(),
|
|
305
|
+
'w-10 items-center': collapsed(),
|
|
306
|
+
'border-dashed border-ring': accepts(column()) && !over(),
|
|
307
|
+
'border-ring bg-primary-subtle': accepts(column()) && over(),
|
|
308
|
+
'border-dashed opacity-60': over() && !accepts(column()),
|
|
309
|
+
'opacity-50': column().disabled,
|
|
310
|
+
}
|
|
216
311
|
)}
|
|
217
|
-
aria-label={column.label}
|
|
312
|
+
aria-label={column().label}
|
|
313
|
+
data-collapsed={collapsed() ? '' : undefined}
|
|
218
314
|
onDragOver={(event) => {
|
|
219
|
-
setOverColumn(column.id)
|
|
220
|
-
if (accepts(column)) event.preventDefault()
|
|
315
|
+
setOverColumn(column().id)
|
|
316
|
+
if (accepts(column())) event.preventDefault()
|
|
317
|
+
}}
|
|
318
|
+
onDragLeave={(event) => {
|
|
319
|
+
// Leaving for a child is not leaving the lane.
|
|
320
|
+
if (event.currentTarget.contains(event.relatedTarget as Node | null)) return
|
|
321
|
+
setOverColumn((current) => (current === column().id ? null : current))
|
|
221
322
|
}}
|
|
222
|
-
onDragLeave={() =>
|
|
223
|
-
setOverColumn((current) => (current === column.id ? null : current))
|
|
224
|
-
}
|
|
225
323
|
onDrop={(event) => {
|
|
226
324
|
event.preventDefault()
|
|
227
|
-
drop(column)
|
|
325
|
+
drop(column())
|
|
228
326
|
}}
|
|
229
327
|
>
|
|
230
|
-
<
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
<
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
328
|
+
<Show
|
|
329
|
+
when={!collapsed()}
|
|
330
|
+
fallback={
|
|
331
|
+
<header class="flex flex-col items-center gap-2 py-3">
|
|
332
|
+
<BoardColumnDot tone={column().tone} />
|
|
333
|
+
<BoardColumnCount count={column().count} />
|
|
334
|
+
<h3
|
|
335
|
+
class={cn(
|
|
336
|
+
headingVariants({ size: 'subsection', leading: 'none', tone: 'muted' }),
|
|
337
|
+
'text-vertical'
|
|
338
|
+
)}
|
|
339
|
+
>
|
|
340
|
+
{column().label}
|
|
341
|
+
</h3>
|
|
342
|
+
</header>
|
|
343
|
+
}
|
|
344
|
+
>
|
|
345
|
+
<header class="flex h-10 shrink-0 items-center gap-2 px-3">
|
|
346
|
+
<BoardColumnDot tone={column().tone} />
|
|
347
|
+
<h3
|
|
348
|
+
class={cn(
|
|
349
|
+
headingVariants({ size: 'subsection', leading: 'none', tone: 'foreground' }),
|
|
350
|
+
'truncate'
|
|
351
|
+
)}
|
|
352
|
+
>
|
|
353
|
+
{column().label}
|
|
354
|
+
</h3>
|
|
355
|
+
<BoardColumnCount count={column().count} />
|
|
356
|
+
<Show when={column().meta}>
|
|
357
|
+
<span class="ms-auto text-2xs text-muted-foreground">{column().meta}</span>
|
|
358
|
+
</Show>
|
|
359
|
+
</header>
|
|
360
|
+
<div class="flex min-h-16 flex-1 flex-col gap-2 overflow-y-auto px-2 pb-2">
|
|
361
|
+
<For each={items()}>
|
|
362
|
+
{(item) => {
|
|
363
|
+
const isDragging = () => {
|
|
364
|
+
const current = dragging()
|
|
365
|
+
return Boolean(current && local.itemId(current) === local.itemId(item))
|
|
366
|
+
}
|
|
367
|
+
return (
|
|
368
|
+
<article
|
|
369
|
+
ref={(element) => {
|
|
370
|
+
const id = local.itemId(item)
|
|
371
|
+
cardElements.set(id, element)
|
|
372
|
+
onCleanup(() => {
|
|
373
|
+
if (cardElements.get(id) === element) cardElements.delete(id)
|
|
374
|
+
})
|
|
375
|
+
}}
|
|
376
|
+
class={cn(
|
|
377
|
+
'relative shrink-0 cursor-grab rounded-md border border-border bg-card text-sm shadow-xs outline-none transition-colors hover:bg-surface-hover focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-primary-subtle active:cursor-grabbing',
|
|
378
|
+
cardDragState[isDragging() ? 'dragging' : 'idle']
|
|
379
|
+
)}
|
|
380
|
+
// A board card is a control that can be moved, so it is
|
|
381
|
+
// focusable and carries the chord it responds to.
|
|
382
|
+
tabindex="0"
|
|
383
|
+
aria-roledescription="Draggable card"
|
|
384
|
+
draggable={true}
|
|
385
|
+
onDragStart={(event) => {
|
|
386
|
+
event.dataTransfer?.setData('text/plain', local.itemId(item))
|
|
387
|
+
if (event.dataTransfer) event.dataTransfer.effectAllowed = 'move'
|
|
388
|
+
setDragging(() => item)
|
|
389
|
+
}}
|
|
390
|
+
onDragEnd={() => {
|
|
391
|
+
setDragging(null)
|
|
392
|
+
setOverColumn(null)
|
|
393
|
+
}}
|
|
394
|
+
onKeyDown={(event) => {
|
|
395
|
+
if (!(event.ctrlKey || event.metaKey)) return
|
|
396
|
+
if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight') return
|
|
397
|
+
event.preventDefault()
|
|
398
|
+
moveByKeyboard(
|
|
399
|
+
item,
|
|
400
|
+
event.key === 'ArrowRight' ? 1 : -1,
|
|
401
|
+
event.currentTarget
|
|
402
|
+
)
|
|
403
|
+
}}
|
|
404
|
+
>
|
|
405
|
+
{local.children(item, { dragging: isDragging() })}
|
|
406
|
+
</article>
|
|
407
|
+
)
|
|
408
|
+
}}
|
|
409
|
+
</For>
|
|
410
|
+
<Show when={items().length === 0}>
|
|
411
|
+
<p class="rounded-md border border-dashed border-border px-2 py-4 text-center text-2xs text-muted-foreground">
|
|
412
|
+
{local.emptyColumn?.(column()) ?? 'Nothing here'}
|
|
413
|
+
</p>
|
|
414
|
+
</Show>
|
|
415
|
+
</div>
|
|
416
|
+
</Show>
|
|
288
417
|
</section>
|
|
289
418
|
)
|
|
290
419
|
}}
|
|
291
|
-
</
|
|
420
|
+
</Index>
|
|
292
421
|
{/*
|
|
293
422
|
The keyboard move's result, announced. A drag speaks for itself — the card
|
|
294
423
|
visibly moves — while a keyboard move happens with the focus elsewhere.
|
|
@@ -320,3 +449,28 @@ export function BoardCardTitle(props: BoardCardTitleProps) {
|
|
|
320
449
|
export function BoardCardBody(props: { class?: string; children: JSX.Element }) {
|
|
321
450
|
return <div class={cn('grid gap-1.5 p-2.5', props.class)}>{props.children}</div>
|
|
322
451
|
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* The card's open action: its title, as a real button whose hit area stretches
|
|
455
|
+
* over the whole card. A card is one target to a pointer — clicking anywhere on
|
|
456
|
+
* it opens it — while assistive technology still meets a named button rather
|
|
457
|
+
* than a clickable `article`. Other controls on the card sit above the stretched
|
|
458
|
+
* area by being positioned (`class="relative"`), so they keep their own click.
|
|
459
|
+
*/
|
|
460
|
+
export function BoardCardTrigger(props: ComponentProps<'button'>) {
|
|
461
|
+
const [local, rest] = splitProps(props, ['class', 'type'])
|
|
462
|
+
return (
|
|
463
|
+
<button
|
|
464
|
+
type={local.type ?? 'button'}
|
|
465
|
+
data-slot="board-card-trigger"
|
|
466
|
+
class={cn(
|
|
467
|
+
textVariants({ variant: 'label', tone: 'foreground' }),
|
|
468
|
+
'min-w-0 flex-1 truncate rounded-sm text-start outline-none',
|
|
469
|
+
'after:absolute after:inset-0 after:rounded-md',
|
|
470
|
+
'focus-visible:ring-3 focus-visible:ring-primary-subtle',
|
|
471
|
+
local.class
|
|
472
|
+
)}
|
|
473
|
+
{...rest}
|
|
474
|
+
/>
|
|
475
|
+
)
|
|
476
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The accents a theme carries, beside the six presets.
|
|
3
|
+
*
|
|
4
|
+
* `@adea-ai/themes` offers two kinds of accent. The presets (`ACCENTS`) are brand
|
|
5
|
+
* colours, the same six on every theme. Since 0.9 the catalogue also offers the
|
|
6
|
+
* accent-shaped colours a theme carries itself — its ANSI blue, magenta, cyan and
|
|
7
|
+
* green, paired across a light and a dark theme and kept only when they clear the
|
|
8
|
+
* catalogue's accent floors — through `themeAccentPresets`. Those read as part of
|
|
9
|
+
* the palette rather than as visitors on it, which is why they are offered here.
|
|
10
|
+
*
|
|
11
|
+
* Their ids are role-shaped (`ansi-blue`), not value-shaped: a stored theme accent
|
|
12
|
+
* survives switching themes and resolves to the *new* theme's blue. That has one
|
|
13
|
+
* consequence a consumer must handle, and this module is where it is handled once:
|
|
14
|
+
* a stored theme accent may name a slot the current pair does not offer, and then
|
|
15
|
+
* it resolves to nothing and the theme's own primary applies — the same fallback a
|
|
16
|
+
* stale preset id gets.
|
|
17
|
+
*
|
|
18
|
+
* This module holds only the id test, and reads only the schema entry: the
|
|
19
|
+
* appearance editor uses it, and the editor's packed renderer gate forbids the
|
|
20
|
+
* catalogue and normalization modules that *computing* a theme accent needs
|
|
21
|
+
* (`themeAccentPresets` measures through `normalize.js`). Resolution lives in
|
|
22
|
+
* `lib/themes.ts`, beside the catalogue it reads.
|
|
23
|
+
*/
|
|
24
|
+
import { ANSI_KEYS } from '@adea-ai/themes/schema'
|
|
25
|
+
|
|
26
|
+
/** The prefix the catalogue gives every theme-derived accent id. */
|
|
27
|
+
const THEME_ACCENT_PREFIX = 'ansi-'
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Whether an id names a theme-derived accent slot.
|
|
31
|
+
*
|
|
32
|
+
* Deliberately wider than what any one pair offers: the id is valid to *store*
|
|
33
|
+
* whenever it names a slot, because the next theme may offer it even if this one
|
|
34
|
+
* does not. Whether it resolves is `resolveAccentPreset`'s question.
|
|
35
|
+
*/
|
|
36
|
+
export function isThemeAccentId(id: string): boolean {
|
|
37
|
+
// Computed per call rather than held in a module-level set: a top-level
|
|
38
|
+
// `new Set(...)` is a statement a bundler cannot prove pure, and it would keep
|
|
39
|
+
// this module in every bundle that touches the root entry.
|
|
40
|
+
return (
|
|
41
|
+
id.startsWith(THEME_ACCENT_PREFIX) &&
|
|
42
|
+
(ANSI_KEYS as readonly string[]).includes(id.slice(THEME_ACCENT_PREFIX.length))
|
|
43
|
+
)
|
|
44
|
+
}
|
package/dist/r/src/lib/themes.ts
CHANGED
|
@@ -35,9 +35,11 @@
|
|
|
35
35
|
|
|
36
36
|
import {
|
|
37
37
|
ACCENTS,
|
|
38
|
+
accentForeground,
|
|
38
39
|
accentRoles,
|
|
39
40
|
chartSeries,
|
|
40
41
|
getAccent,
|
|
42
|
+
getTheme,
|
|
41
43
|
hasTheme,
|
|
42
44
|
shadcnDestructiveProjection,
|
|
43
45
|
statusForeground,
|
|
@@ -47,10 +49,16 @@ import {
|
|
|
47
49
|
syntaxRoles,
|
|
48
50
|
themes as catalogue,
|
|
49
51
|
themesByAppearance,
|
|
52
|
+
themeAccentPresets,
|
|
50
53
|
validateCatalogue,
|
|
54
|
+
type AccentPreset,
|
|
55
|
+
type AdeaTheme,
|
|
51
56
|
type AdeaThemeRecord,
|
|
52
57
|
type ContrastFinding,
|
|
53
58
|
} from '@adea-ai/themes'
|
|
59
|
+
import { isThemeAccentId } from './theme-accents'
|
|
60
|
+
|
|
61
|
+
export { isThemeAccentId } from './theme-accents'
|
|
54
62
|
|
|
55
63
|
export type ThemeAppearance = 'light' | 'dark'
|
|
56
64
|
|
|
@@ -320,6 +328,100 @@ export function themeFamilies(): { family: string; label: string; themes: ThemeV
|
|
|
320
328
|
/** The accent presets, in the order a picker should show them. */
|
|
321
329
|
export const accents = ACCENTS
|
|
322
330
|
|
|
331
|
+
/**
|
|
332
|
+
* The canonical catalogue records, in catalogue order.
|
|
333
|
+
*
|
|
334
|
+
* {@link builtinThemes} is this system's projection of the catalogue onto the
|
|
335
|
+
* shadcn vocabulary, which is what a document is painted with. A *preview* of a
|
|
336
|
+
* theme — the miniatures `AppearanceEditor` and `ThemeMiniature` draw — reads the
|
|
337
|
+
* catalogue's own roles instead (`surfaceActive`, `borderMuted`, `textSubtle`…),
|
|
338
|
+
* several of which the projection folds together. These are those records, so a
|
|
339
|
+
* consumer hands the editor exactly what it takes rather than reconstructing it
|
|
340
|
+
* from the projection.
|
|
341
|
+
*/
|
|
342
|
+
export const themeRecords: readonly AdeaThemeRecord[] = catalogue
|
|
343
|
+
|
|
344
|
+
export function themeRecordById(id: string): AdeaThemeRecord | undefined {
|
|
345
|
+
return getTheme(id)
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** A light/dark theme pair by id, with the defaults standing in for an unknown id. */
|
|
349
|
+
function recordPair(lightThemeId: string, darkThemeId: string) {
|
|
350
|
+
const light = getTheme(lightThemeId) ?? getTheme(defaultLightThemeId)!
|
|
351
|
+
const dark = getTheme(darkThemeId) ?? getTheme(defaultDarkThemeId)!
|
|
352
|
+
return { light, dark }
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* The accents the selected light/dark pair carries — offered beside the presets.
|
|
357
|
+
*
|
|
358
|
+
* Computed per pair rather than per theme because a theme accent is a *pair* of
|
|
359
|
+
* values, like a preset: a slot is offered only when both themes can offer it.
|
|
360
|
+
*/
|
|
361
|
+
export function themeAccentsFor(
|
|
362
|
+
lightThemeId: string,
|
|
363
|
+
darkThemeId: string
|
|
364
|
+
): readonly AccentPreset[] {
|
|
365
|
+
const { light, dark } = recordPair(lightThemeId, darkThemeId)
|
|
366
|
+
return themeAccentPresets(light, dark)
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* An accent id resolved against a theme pair: a preset, a theme accent the pair
|
|
371
|
+
* offers, or `undefined` for `theme`, an unknown id, or a slot this pair cannot
|
|
372
|
+
* offer — each of which means "use the theme's own primary".
|
|
373
|
+
*
|
|
374
|
+
* Theme accent ids are role-shaped (`ansi-blue`), so a stored one survives a theme
|
|
375
|
+
* switch and resolves to the new theme's blue; when the new pair cannot offer the
|
|
376
|
+
* slot it resolves to nothing, the same fallback a stale preset id gets.
|
|
377
|
+
*/
|
|
378
|
+
export function resolveAccentPreset(
|
|
379
|
+
accentId: string,
|
|
380
|
+
light: AdeaTheme,
|
|
381
|
+
dark: AdeaTheme
|
|
382
|
+
): AccentPreset | undefined {
|
|
383
|
+
if (accentId === 'theme') return undefined
|
|
384
|
+
const preset = getAccent(accentId)
|
|
385
|
+
if (preset) return preset
|
|
386
|
+
if (!isThemeAccentId(accentId)) return undefined
|
|
387
|
+
return themeAccentPresets(light, dark).find((option) => option.id === accentId)
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* A theme with an accent laid over it, in the catalogue's own shape.
|
|
392
|
+
*
|
|
393
|
+
* The accent replaces `accent` and its measured label, which is all an accent
|
|
394
|
+
* changes about a theme; `undefined` returns the theme untouched. This is the
|
|
395
|
+
* overlay a host needs for `AppearanceEditor`'s `lightTheme`/`darkTheme`.
|
|
396
|
+
*/
|
|
397
|
+
export function withAccent<T extends AdeaTheme>(theme: T, accent: AccentPreset | undefined): T {
|
|
398
|
+
if (!accent) return theme
|
|
399
|
+
const value = theme.appearance === 'dark' ? accent.dark : accent.light
|
|
400
|
+
return {
|
|
401
|
+
...theme,
|
|
402
|
+
colors: { ...theme.colors, accent: value, accentForeground: accentForeground(value) },
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* The two preview themes an appearance selection resolves to, accent applied.
|
|
408
|
+
*
|
|
409
|
+
* The adapter between a stored selection and `AppearanceEditor`: it takes ids —
|
|
410
|
+
* what a preference holds — and returns the canonical records the editor's
|
|
411
|
+
* `lightTheme` and `darkTheme` props take, so a host does not map roles by hand.
|
|
412
|
+
* Unknown ids fall back to the defaults and an accent the pair cannot offer falls
|
|
413
|
+
* back to the theme's own, exactly as `ThemeProvider` resolves them.
|
|
414
|
+
*/
|
|
415
|
+
export function appearancePreviewThemes(selection: {
|
|
416
|
+
lightThemeId: string
|
|
417
|
+
darkThemeId: string
|
|
418
|
+
accent: string
|
|
419
|
+
}): { lightTheme: AdeaThemeRecord; darkTheme: AdeaThemeRecord } {
|
|
420
|
+
const { light, dark } = recordPair(selection.lightThemeId, selection.darkThemeId)
|
|
421
|
+
const accent = resolveAccentPreset(selection.accent, light, dark)
|
|
422
|
+
return { lightTheme: withAccent(light, accent), darkTheme: withAccent(dark, accent) }
|
|
423
|
+
}
|
|
424
|
+
|
|
323
425
|
/**
|
|
324
426
|
* The properties an accent selection overrides, as CSS custom properties.
|
|
325
427
|
*
|
|
@@ -339,16 +441,25 @@ export const accents = ACCENTS
|
|
|
339
441
|
* provider and the CSS generator call `accentRoles` out of `@adea-ai/themes`, and
|
|
340
442
|
* `tests/appearance-axes.test.ts` asserts the two agree value for value.
|
|
341
443
|
*
|
|
342
|
-
*
|
|
343
|
-
*
|
|
344
|
-
*
|
|
444
|
+
* A theme accent (`ansi-blue`) has no block: its value depends on the theme pair,
|
|
445
|
+
* so it is resolved against `pair` — the selection's light and dark theme ids —
|
|
446
|
+
* and only ever applied here. Without a pair it resolves against the defaults.
|
|
447
|
+
*
|
|
448
|
+
* Returns `undefined` for `theme` — the variant's own primary — for an id the
|
|
449
|
+
* catalogue does not have, and for a theme accent the pair does not offer, so a
|
|
450
|
+
* stale stored preference falls back to the theme rather than blanking the primary.
|
|
345
451
|
*/
|
|
346
452
|
export function accentVariables(
|
|
347
453
|
accentId: string,
|
|
348
|
-
appearance: ThemeAppearance
|
|
454
|
+
appearance: ThemeAppearance,
|
|
455
|
+
pair: { lightThemeId: string; darkThemeId: string } = {
|
|
456
|
+
lightThemeId: defaultLightThemeId,
|
|
457
|
+
darkThemeId: defaultDarkThemeId,
|
|
458
|
+
}
|
|
349
459
|
): Record<string, string> | undefined {
|
|
350
460
|
if (accentId === 'theme') return undefined
|
|
351
|
-
const
|
|
461
|
+
const { light, dark } = recordPair(pair.lightThemeId, pair.darkThemeId)
|
|
462
|
+
const preset = resolveAccentPreset(accentId, light, dark)
|
|
352
463
|
if (!preset) return undefined
|
|
353
464
|
|
|
354
465
|
const roles = accentRoles(preset, appearance)
|
|
@@ -244,6 +244,17 @@
|
|
|
244
244
|
}
|
|
245
245
|
}
|
|
246
246
|
|
|
247
|
+
/**
|
|
248
|
+
* A label set on its side, reading top to bottom (turned 90° clockwise). For a
|
|
249
|
+
* collapsed lane or rail section that must stay named while it is only as wide
|
|
250
|
+
* as a line of text. Letters keep their orientation for the reader's head tilt
|
|
251
|
+
* rather than stacking, which is unreadable past a few characters.
|
|
252
|
+
*/
|
|
253
|
+
@utility text-vertical {
|
|
254
|
+
writing-mode: vertical-rl;
|
|
255
|
+
text-orientation: mixed;
|
|
256
|
+
}
|
|
257
|
+
|
|
247
258
|
@utility no-scrollbar {
|
|
248
259
|
-ms-overflow-style: none;
|
|
249
260
|
scrollbar-width: none;
|
package/dist/r/theming.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adea-ai/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.103.0",
|
|
4
4
|
"description": "The Adea design system: Kobalte-backed Solid components, semantic OKLCH tokens, and the app-shell layout primitives shared by Adea and Cortana.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"adea",
|
|
@@ -119,7 +119,7 @@
|
|
|
119
119
|
"check:packed-paste-model": "node scripts/check-packed-paste-model.mjs"
|
|
120
120
|
},
|
|
121
121
|
"dependencies": {
|
|
122
|
-
"@adea-ai/themes": "^0.
|
|
122
|
+
"@adea-ai/themes": "^0.9.5",
|
|
123
123
|
"@corvu/calendar": "^0.1.2",
|
|
124
124
|
"@corvu/drawer": "^0.2.4",
|
|
125
125
|
"@corvu/otp-field": "^0.1.4",
|