@adea-ai/ui 0.101.0 → 0.102.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/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/components/ui/navigation-menu/navigation-menu.d.ts.map +1 -1
- package/dist/components/ui/navigation-menu/navigation-menu.js +8 -7
- package/dist/components/ui/navigation-menu/navigation-menu.js.map +1 -1
- package/dist/components/ui/tabs/tabs.d.ts +13 -0
- package/dist/components/ui/tabs/tabs.d.ts.map +1 -1
- package/dist/components/ui/tabs/tabs.js +15 -1
- package/dist/components/ui/tabs/tabs.js.map +1 -1
- package/dist/components/ui/toast/toast.d.ts.map +1 -1
- package/dist/components/ui/toast/toast.js +6 -2
- package/dist/components/ui/toast/toast.js.map +1 -1
- package/dist/index.js +2 -2
- package/dist/r/board.json +2 -0
- package/dist/r/registry.json +2 -0
- 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/components/ui/navigation-menu/navigation-menu.tsx +27 -20
- package/dist/r/src/components/ui/tabs/tabs.tsx +18 -1
- package/dist/r/src/components/ui/toast/toast.tsx +7 -6
- package/dist/r/src/styles/base.css +11 -0
- package/package.json +1 -1
- package/registry.json +2 -0
- package/src/components/ui/board/board.tsx +231 -77
- package/src/components/ui/board/index.ts +2 -0
- package/src/components/ui/navigation-menu/navigation-menu.tsx +27 -20
- package/src/components/ui/tabs/tabs.tsx +18 -1
- package/src/components/ui/toast/toast.tsx +7 -6
- 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
|
+
}
|
|
@@ -2,7 +2,14 @@ import { splitProps, type ComponentProps, type JSX } from 'solid-js'
|
|
|
2
2
|
import * as KobalteNavigationMenu from '@kobalte/core/navigation-menu'
|
|
3
3
|
import { ChevronDown } from 'lucide-solid'
|
|
4
4
|
import { cn } from '../../../lib/utils'
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
menuContentPadding,
|
|
7
|
+
menuItem,
|
|
8
|
+
menuLabel,
|
|
9
|
+
menuSeparator,
|
|
10
|
+
overlayMotion,
|
|
11
|
+
overlaySurface,
|
|
12
|
+
} from '../../../lib/overlay'
|
|
6
13
|
|
|
7
14
|
/**
|
|
8
15
|
* NavigationMenu.
|
|
@@ -123,7 +130,11 @@ export function NavigationMenuContent(props: ComponentProps<typeof KobalteNaviga
|
|
|
123
130
|
class={cn(
|
|
124
131
|
// Absolute so the viewport sizes to the active panel's measured box and
|
|
125
132
|
// animates between panels, rather than stacking them.
|
|
126
|
-
'absolute top-0 left-0 grid w-max gap-1
|
|
133
|
+
'absolute top-0 left-0 grid w-max gap-1 outline-none',
|
|
134
|
+
// The viewport is the surface and carries no padding of its own (its
|
|
135
|
+
// measured size is the panel's), so the panel supplies the inset every
|
|
136
|
+
// menu surface shares — the one the separator's bleed is cut for.
|
|
137
|
+
menuContentPadding,
|
|
127
138
|
// The motion is the panel's, not the viewport's. Kobalte keeps an
|
|
128
139
|
// outgoing panel mounted until the viewport's own animation ends; a
|
|
129
140
|
// viewport that carries an entrance animation never fires that again
|
|
@@ -193,7 +204,11 @@ export function NavigationMenuItem(props: NavigationMenuItemProps) {
|
|
|
193
204
|
<KobalteNavigationMenu.Item
|
|
194
205
|
class={cn(
|
|
195
206
|
menuItem,
|
|
196
|
-
|
|
207
|
+
// The description sits under the label, in the label's column. Without
|
|
208
|
+
// an icon there is no leading column, and a two-column grid would set
|
|
209
|
+
// the description beside the label instead of beneath it.
|
|
210
|
+
local.icon ? 'grid grid-cols-[auto_1fr]' : 'grid grid-cols-1',
|
|
211
|
+
'items-center gap-x-2 gap-y-0.5',
|
|
197
212
|
local.description && 'grid-rows-[auto_auto] py-2',
|
|
198
213
|
local.class
|
|
199
214
|
)}
|
|
@@ -202,7 +217,13 @@ export function NavigationMenuItem(props: NavigationMenuItemProps) {
|
|
|
202
217
|
{local.icon}
|
|
203
218
|
<span class="font-medium">{local.children}</span>
|
|
204
219
|
{local.description && (
|
|
205
|
-
<span
|
|
220
|
+
<span
|
|
221
|
+
class={cn('text-xs text-muted-foreground', {
|
|
222
|
+
'col-start-2': !!local.icon,
|
|
223
|
+
})}
|
|
224
|
+
>
|
|
225
|
+
{local.description}
|
|
226
|
+
</span>
|
|
206
227
|
)}
|
|
207
228
|
</KobalteNavigationMenu.Item>
|
|
208
229
|
)
|
|
@@ -212,12 +233,7 @@ export function NavigationMenuSeparator(
|
|
|
212
233
|
props: ComponentProps<typeof KobalteNavigationMenu.Separator>
|
|
213
234
|
) {
|
|
214
235
|
const [local, rest] = splitProps(props, ['class'])
|
|
215
|
-
return (
|
|
216
|
-
<KobalteNavigationMenu.Separator
|
|
217
|
-
class={cn('-mx-1 my-1 h-px bg-border', local.class)}
|
|
218
|
-
{...rest}
|
|
219
|
-
/>
|
|
220
|
-
)
|
|
236
|
+
return <KobalteNavigationMenu.Separator class={cn(menuSeparator, local.class)} {...rest} />
|
|
221
237
|
}
|
|
222
238
|
|
|
223
239
|
/** A non-interactive caption above a group of items. */
|
|
@@ -225,16 +241,7 @@ export function NavigationMenuGroupLabel(
|
|
|
225
241
|
props: ComponentProps<typeof KobalteNavigationMenu.GroupLabel>
|
|
226
242
|
) {
|
|
227
243
|
const [local, rest] = splitProps(props, ['class'])
|
|
228
|
-
return (
|
|
229
|
-
<KobalteNavigationMenu.GroupLabel
|
|
230
|
-
class={cn(
|
|
231
|
-
menuContentPadding,
|
|
232
|
-
'text-2xs font-semibold text-muted-foreground uppercase',
|
|
233
|
-
local.class
|
|
234
|
-
)}
|
|
235
|
-
{...rest}
|
|
236
|
-
/>
|
|
237
|
-
)
|
|
244
|
+
return <KobalteNavigationMenu.GroupLabel class={cn(menuLabel, local.class)} {...rest} />
|
|
238
245
|
}
|
|
239
246
|
|
|
240
247
|
export function NavigationMenuGroup(props: ComponentProps<typeof KobalteNavigationMenu.Group>) {
|
|
@@ -13,6 +13,7 @@ import { cn } from '../../../lib/utils'
|
|
|
13
13
|
|
|
14
14
|
type TabsIdContextValue = {
|
|
15
15
|
id: Accessor<string>
|
|
16
|
+
orientation: Accessor<'horizontal' | 'vertical'>
|
|
16
17
|
triggerIds: Accessor<ReadonlyMap<string, string>>
|
|
17
18
|
registerTriggerId: (value: string, id: string) => void
|
|
18
19
|
unregisterTriggerId: (value: string, id: string) => void
|
|
@@ -43,6 +44,7 @@ export function Tabs(props: ComponentProps<typeof KobalteTabs>) {
|
|
|
43
44
|
const [triggerIds, setTriggerIds] = createSignal<ReadonlyMap<string, string>>(new Map())
|
|
44
45
|
const tabs: TabsIdContextValue = {
|
|
45
46
|
id,
|
|
47
|
+
orientation: () => rest.orientation ?? 'horizontal',
|
|
46
48
|
triggerIds,
|
|
47
49
|
registerTriggerId: (value, triggerId) => {
|
|
48
50
|
setTriggerIds((current) => new Map(current).set(value, triggerId))
|
|
@@ -103,6 +105,19 @@ export type TabsTriggerProps = ComponentProps<typeof KobalteTabs.Trigger> & {
|
|
|
103
105
|
appearance?: 'underline' | 'segmented'
|
|
104
106
|
}
|
|
105
107
|
|
|
108
|
+
/**
|
|
109
|
+
* The underline appearance marks the selected trigger on the edge it shares
|
|
110
|
+
* with the list's axis. A horizontal row reads left to right along a baseline,
|
|
111
|
+
* so the mark is a bottom bar sitting on the list's rule. A vertical list stacks
|
|
112
|
+
* its triggers, and a bottom bar there underlines one row of a column — it reads
|
|
113
|
+
* as a divider, not a selection — so the mark moves to the inline-start edge,
|
|
114
|
+
* the leading bar a side navigation uses, and the label aligns to that edge.
|
|
115
|
+
*
|
|
116
|
+
* The orientation is read from the root rather than from a `data-orientation`
|
|
117
|
+
* selector so the vertical classes are unprefixed: a composite that replaces the
|
|
118
|
+
* mark (SettingsNavigation's filled row passes `border-0`) then overrides it
|
|
119
|
+
* through the ordinary class merge instead of fighting a variant-scoped border.
|
|
120
|
+
*/
|
|
106
121
|
export function TabsTrigger(props: TabsTriggerProps) {
|
|
107
122
|
const [local, rest] = splitProps(props, ['class', 'appearance', 'id'])
|
|
108
123
|
const tabs = useTabsId()
|
|
@@ -128,7 +143,9 @@ export function TabsTrigger(props: TabsTriggerProps) {
|
|
|
128
143
|
'[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg]:size-4',
|
|
129
144
|
local.appearance === 'segmented'
|
|
130
145
|
? 'h-control-sm rounded-md px-control-sm data-[selected]:bg-card data-[selected]:shadow-xs'
|
|
131
|
-
:
|
|
146
|
+
: tabs.orientation() === 'vertical'
|
|
147
|
+
? 'h-control-md justify-start rounded-none border-s-2 border-transparent ps-3 pe-2 data-[selected]:border-primary'
|
|
148
|
+
: 'h-control-md -mb-px rounded-none border-b-2 border-transparent px-2 pb-2 data-[selected]:border-primary',
|
|
132
149
|
local.class
|
|
133
150
|
)}
|
|
134
151
|
{...rest}
|
|
@@ -180,6 +180,10 @@ export type ToasterProps = ComponentProps<typeof KobalteToast.Region> & {
|
|
|
180
180
|
}
|
|
181
181
|
|
|
182
182
|
/**
|
|
183
|
+
* `class` lands on the region too: the props describe the region, and the
|
|
184
|
+
* region is the element a caller lays out (an inset clear of a status bar, a
|
|
185
|
+
* narrower stack). The list inside it is an implementation detail.
|
|
186
|
+
*
|
|
183
187
|
* The corner is set on the region, not the list. The region is the `fixed`
|
|
184
188
|
* element: an inset on the list inside it positions nothing, and the region falls
|
|
185
189
|
* back to wherever it happens to sit in the document flow.
|
|
@@ -217,16 +221,13 @@ export function Toaster(props: ToasterProps) {
|
|
|
217
221
|
*/
|
|
218
222
|
class={cn(
|
|
219
223
|
'pointer-events-none fixed z-(--z-toast) flex max-h-screen w-full flex-col gap-2 p-4 sm:max-w-sm',
|
|
220
|
-
toasterPosition[local.position ?? 'bottom-right']
|
|
224
|
+
toasterPosition[local.position ?? 'bottom-right'],
|
|
225
|
+
local.class
|
|
221
226
|
)}
|
|
222
227
|
{...rest}
|
|
223
228
|
>
|
|
224
229
|
<KobalteToast.List
|
|
225
|
-
class={cn(
|
|
226
|
-
'flex flex-col gap-2',
|
|
227
|
-
toasterAlignment[local.position ?? 'bottom-right'],
|
|
228
|
-
local.class
|
|
229
|
-
)}
|
|
230
|
+
class={cn('flex flex-col gap-2', toasterAlignment[local.position ?? 'bottom-right'])}
|
|
230
231
|
/>
|
|
231
232
|
</KobalteToast.Region>
|
|
232
233
|
)
|
|
@@ -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/package.json
CHANGED