@heyramzi/ui 3.0.0 → 3.2.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/charts/dual-axis.js +38 -0
- package/dist/lib/sliding-pill.js +2 -2
- package/dist/record/measure-clamp.js +23 -0
- package/dist/shadcn/button-group/index.js +6 -0
- package/dist/shadcn/checkbox/checkbox-box.js +3 -0
- package/dist/shadcn/input-group/index.js +9 -0
- package/dist/shadcn/item/index.js +13 -0
- package/dist/shadcn/native-select/index.js +6 -0
- package/dist/shadcn/pagination/index.js +12 -0
- package/dist/slides/deck-controller.svelte.js +62 -15
- package/dist/slides/layout.js +3 -2
- package/package.json +9 -2
- package/scripts/svelte-dts.mjs +3 -0
- package/scripts/sync-shadcn.mjs +0 -5
- package/src/ai/generation-progress/GenerationProgress.svelte +14 -12
- package/src/canvas/pen/PenOverlay.svelte +1 -30
- package/src/canvas/screen/PartView.svelte +13 -8
- package/src/canvas/screen/ScreenBody.svelte +18 -11
- package/src/canvas/ui/BarTooltip.svelte +46 -0
- package/src/canvas/ui/BarTooltip.svelte.d.ts +17 -0
- package/src/canvas/ui/CanvasToolbar.svelte +170 -156
- package/src/canvas/ui/StylePanel.svelte +179 -102
- package/src/canvas/ui/canvas-ui.css +12 -76
- package/src/charts/area-chart.css +6 -0
- package/src/charts/area-chart.svelte +115 -17
- package/src/charts/area-chart.svelte.d.ts +11 -2
- package/src/charts/chart-tokens.css +4 -3
- package/src/charts/dashboard/data-table.svelte +80 -80
- package/src/charts/dashboard/hint.svelte +34 -0
- package/src/charts/dashboard/hint.svelte.d.ts +10 -0
- package/src/charts/dashboard/rating-chip.svelte +4 -3
- package/src/charts/dual-axis.test.ts +40 -0
- package/src/charts/dual-axis.ts +43 -0
- package/src/charts/legend.css +0 -8
- package/src/charts/legend.svelte +26 -30
- package/src/components/copy/copy-button.svelte +10 -2
- package/src/components/copy/copy-button.test.ts +7 -0
- package/src/components/data-table/table-skeleton.svelte +15 -7
- package/src/components/data-table/table-skeleton.svelte.d.ts +4 -0
- package/src/components/docs/on-this-page.svelte +2 -2
- package/src/components/hold-to-confirm/hold-to-confirm.svelte +2 -1
- package/src/components/list-page/filter-item.svelte +20 -7
- package/src/components/list-page/filter-popover.svelte +41 -16
- package/src/components/list-page/filter-popover.svelte.d.ts +1 -1
- package/src/components/list-page/grouped-list.svelte +41 -23
- package/src/components/picker/picker.svelte +8 -3
- package/src/components/picker/picker.svelte.d.ts +2 -1
- package/src/components/segmented/segmented-filter.svelte +19 -10
- package/src/components/segmented/segmented-track.svelte +11 -3
- package/src/components/table-view/table-view.svelte +22 -6
- package/src/components/tags-input/tags-input.svelte +94 -77
- package/src/components/toolbar/toolbar.svelte +2 -1
- package/src/components/toolbar/toolbar.test.ts +5 -4
- package/src/components/work-parts/group-head.svelte +28 -9
- package/src/components/work-parts/group-head.svelte.d.ts +5 -2
- package/src/editor/editor-toolbar.svelte +231 -193
- package/src/editor/editor.svelte +57 -39
- package/src/editor/image-toolbar.svelte +99 -37
- package/src/editor/image-toolbar.svelte.d.ts +4 -5
- package/src/editor/url-dialog.svelte +96 -0
- package/src/editor/url-dialog.svelte.d.ts +15 -0
- package/src/effects/theme-toggle/ThemeToggle.css +8 -8
- package/src/effects/theme-toggle/ThemeToggle.svelte +20 -6
- package/src/inbox/thread-inbox.svelte +9 -1
- package/src/lib/live-region.svelte +12 -4
- package/src/lib/live-region.svelte.d.ts +5 -1
- package/src/lib/sliding-pill.ts +3 -2
- package/src/record/info-list.svelte +33 -59
- package/src/record/measure-clamp.ts +31 -0
- package/src/record/record-shell.svelte +38 -39
- package/src/record/record-timeline.svelte +10 -5
- package/src/record/steps-rail.svelte +50 -36
- package/src/record/where-we-are.svelte +5 -19
- package/src/shadcn/button-group/button-group-separator.svelte +23 -0
- package/src/shadcn/button-group/button-group-separator.svelte.d.ts +13 -0
- package/src/shadcn/button-group/button-group-text.svelte +28 -0
- package/src/shadcn/button-group/button-group-text.svelte.d.ts +11 -0
- package/src/shadcn/button-group/button-group.svelte +46 -0
- package/src/shadcn/button-group/button-group.svelte.d.ts +16 -0
- package/src/shadcn/button-group/index.ts +15 -0
- package/src/shadcn/checkbox/checkbox-box.ts +4 -0
- package/src/shadcn/checkbox/checkbox.svelte +2 -4
- package/src/shadcn/input-group/index.ts +22 -0
- package/src/shadcn/input-group/input-group-addon.svelte +51 -0
- package/src/shadcn/input-group/input-group-addon.svelte.d.ts +18 -0
- package/src/shadcn/input-group/input-group-button.svelte +49 -0
- package/src/shadcn/input-group/input-group-button.svelte.d.ts +18 -0
- package/src/shadcn/input-group/input-group-input.svelte +20 -0
- package/src/shadcn/input-group/input-group-input.svelte.d.ts +11 -0
- package/src/shadcn/input-group/input-group-text.svelte +19 -0
- package/src/shadcn/input-group/input-group-text.svelte.d.ts +5 -0
- package/src/shadcn/input-group/input-group-textarea.svelte +20 -0
- package/src/shadcn/input-group/input-group-textarea.svelte.d.ts +3 -0
- package/src/shadcn/input-group/input-group.svelte +24 -0
- package/src/shadcn/input-group/input-group.svelte.d.ts +5 -0
- package/src/shadcn/item/index.ts +34 -0
- package/src/shadcn/item/item-actions.svelte +20 -0
- package/src/shadcn/item/item-actions.svelte.d.ts +5 -0
- package/src/shadcn/item/item-content.svelte +23 -0
- package/src/shadcn/item/item-content.svelte.d.ts +5 -0
- package/src/shadcn/item/item-description.svelte +23 -0
- package/src/shadcn/item/item-description.svelte.d.ts +5 -0
- package/src/shadcn/item/item-footer.svelte +20 -0
- package/src/shadcn/item/item-footer.svelte.d.ts +5 -0
- package/src/shadcn/item/item-group.svelte +21 -0
- package/src/shadcn/item/item-group.svelte.d.ts +5 -0
- package/src/shadcn/item/item-header.svelte +20 -0
- package/src/shadcn/item/item-header.svelte.d.ts +5 -0
- package/src/shadcn/item/item-media.svelte +42 -0
- package/src/shadcn/item/item-media.svelte.d.ts +17 -0
- package/src/shadcn/item/item-separator.svelte +19 -0
- package/src/shadcn/item/item-separator.svelte.d.ts +13 -0
- package/src/shadcn/item/item-title.svelte +20 -0
- package/src/shadcn/item/item-title.svelte.d.ts +5 -0
- package/src/shadcn/item/item.svelte +61 -0
- package/src/shadcn/item/item.svelte.d.ts +28 -0
- package/src/shadcn/native-select/index.ts +13 -0
- package/src/shadcn/native-select/native-select-opt-group.svelte +20 -0
- package/src/shadcn/native-select/native-select-opt-group.svelte.d.ts +5 -0
- package/src/shadcn/native-select/native-select-option.svelte +20 -0
- package/src/shadcn/native-select/native-select-option.svelte.d.ts +5 -0
- package/src/shadcn/native-select/native-select.svelte +45 -0
- package/src/shadcn/native-select/native-select.svelte.d.ts +8 -0
- package/src/shadcn/pagination/index.ts +31 -0
- package/src/shadcn/pagination/pagination-content.svelte +20 -0
- package/src/shadcn/pagination/pagination-content.svelte.d.ts +5 -0
- package/src/shadcn/pagination/pagination-ellipsis.svelte +22 -0
- package/src/shadcn/pagination/pagination-ellipsis.svelte.d.ts +5 -0
- package/src/shadcn/pagination/pagination-item.svelte +14 -0
- package/src/shadcn/pagination/pagination-item.svelte.d.ts +5 -0
- package/src/shadcn/pagination/pagination-link.svelte +41 -0
- package/src/shadcn/pagination/pagination-link.svelte.d.ts +9 -0
- package/src/shadcn/pagination/pagination-next-button.svelte +34 -0
- package/src/shadcn/pagination/pagination-next-button.svelte.d.ts +4 -0
- package/src/shadcn/pagination/pagination-next.svelte +26 -0
- package/src/shadcn/pagination/pagination-next.svelte.d.ts +4 -0
- package/src/shadcn/pagination/pagination-prev-button.svelte +34 -0
- package/src/shadcn/pagination/pagination-prev-button.svelte.d.ts +4 -0
- package/src/shadcn/pagination/pagination-previous.svelte +30 -0
- package/src/shadcn/pagination/pagination-previous.svelte.d.ts +4 -0
- package/src/shadcn/pagination/pagination.svelte +27 -0
- package/src/shadcn/pagination/pagination.svelte.d.ts +4 -0
- package/src/slides/Controls.svelte +39 -36
- package/src/slides/Deck.svelte +23 -10
- package/src/slides/Overview.svelte +10 -4
- package/src/slides/README.md +1 -1
- package/src/slides/Slide.svelte +17 -11
- package/src/slides/deck-controller.svelte.ts +62 -16
- package/src/slides/layout.ts +3 -2
- package/src/slides/theme.css +34 -5
- package/src/thread/thread.svelte +15 -8
- package/src/work/detail/task-detail.svelte +1 -1
- package/src/work/gantt/gantt-task-row.svelte +3 -6
- package/src/work/inbox/inbox-card.svelte +7 -5
- package/src/work/inbox/inbox-trace.svelte +14 -10
- package/src/work/kanban-board.svelte +40 -25
- package/src/work/kanban-hidden-rail.svelte +67 -65
- package/src/work/task-list.svelte +145 -124
- package/src/work/task-list.svelte.test.ts +5 -2
- package/src/work/time/time-tracker.svelte +35 -10
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
import * as ContextMenu from "../../shadcn/context-menu/index.js";
|
|
18
18
|
import { Checkbox } from "../../shadcn/checkbox/index.js";
|
|
19
19
|
import { SortableHead, TableSkeleton } from "../data-table/index.js";
|
|
20
|
+
import GroupHead from "../work-parts/group-head.svelte";
|
|
20
21
|
import { toggleSort, type SortState } from "../data-table/data-table.js";
|
|
21
22
|
import { MENU_KEYS, openMenuFromKey } from "../data-table/menu-key.js";
|
|
22
23
|
|
|
@@ -119,6 +120,9 @@
|
|
|
119
120
|
skeletonRows = 5,
|
|
120
121
|
}: Props = $props();
|
|
121
122
|
|
|
123
|
+
// Names each group's body, so a fold button can say which rows it folds.
|
|
124
|
+
const uid = $props.id();
|
|
125
|
+
|
|
122
126
|
// =========================
|
|
123
127
|
// DERIVED
|
|
124
128
|
// =========================
|
|
@@ -229,6 +233,15 @@
|
|
|
229
233
|
<!-- =========================== -->
|
|
230
234
|
<!-- MARKUP -->
|
|
231
235
|
<!-- =========================== -->
|
|
236
|
+
{#snippet groupTitle(tableGroup: TableGroup<Row>, className?: string)}
|
|
237
|
+
<!-- The th sets the type: GroupHead takes the name as markup, so no size of its own lands on it. -->
|
|
238
|
+
<GroupHead class={className}>
|
|
239
|
+
{#snippet title()}
|
|
240
|
+
{#if groupHeader}{@render groupHeader(tableGroup)}{:else}{tableGroup.key}{/if}
|
|
241
|
+
{/snippet}
|
|
242
|
+
</GroupHead>
|
|
243
|
+
{/snippet}
|
|
244
|
+
|
|
232
245
|
{#snippet body(row: Row)}
|
|
233
246
|
{#each columns as column (column.key)}
|
|
234
247
|
<Table.Cell
|
|
@@ -403,10 +416,11 @@
|
|
|
403
416
|
<TableSkeleton rows={skeletonRows} columns={skeletonColumns} />
|
|
404
417
|
{:else}
|
|
405
418
|
{#if group}
|
|
406
|
-
{#each entries as entry (entry.id)}
|
|
419
|
+
{#each entries as entry, index (entry.id)}
|
|
407
420
|
{@const folded = entry.key !== null && collapsible && collapsed.has(entry.key)}
|
|
408
421
|
{@const tableGroup = { key: entry.key ?? "", rows: entry.rows }}
|
|
409
|
-
|
|
422
|
+
{@const bodyId = `${uid}-group-${index}`}
|
|
423
|
+
<Table.Body id={bodyId}>
|
|
410
424
|
{#if entry.key !== null}
|
|
411
425
|
<Table.Row
|
|
412
426
|
class="bg-foreground/4 hover:translate-y-0 hover:bg-foreground/4 hover:shadow-none"
|
|
@@ -416,6 +430,7 @@
|
|
|
416
430
|
<button
|
|
417
431
|
type="button"
|
|
418
432
|
aria-expanded={!folded}
|
|
433
|
+
aria-controls={bodyId}
|
|
419
434
|
class="t-press flex h-8 w-full items-center gap-2 px-2 text-start outline-none focus-visible:shadow-(--ring-focus-inset)"
|
|
420
435
|
onclick={() => toggleGroup(tableGroup.key)}
|
|
421
436
|
>
|
|
@@ -426,12 +441,13 @@
|
|
|
426
441
|
)}
|
|
427
442
|
aria-hidden="true"
|
|
428
443
|
/>
|
|
429
|
-
{
|
|
444
|
+
{@render groupTitle(tableGroup)}
|
|
430
445
|
</button>
|
|
431
446
|
{:else}
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
447
|
+
<!-- No gap: unlike the toggle, a fixed head never spaced its parts. -->
|
|
448
|
+
<div class="flex h-8 items-center px-2">
|
|
449
|
+
{@render groupTitle(tableGroup, "gap-0")}
|
|
450
|
+
</div>
|
|
435
451
|
{/if}
|
|
436
452
|
</Table.Head>
|
|
437
453
|
</Table.Row>
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// IMPORTS
|
|
4
4
|
// =========================
|
|
5
5
|
import { tick } from "svelte";
|
|
6
|
+
import { Combobox } from "bits-ui";
|
|
6
7
|
import { Plus } from "phosphor-svelte";
|
|
7
8
|
import { cn } from "../../lib/utils.js";
|
|
8
9
|
import TagChip from "../chip/tag-chip.svelte";
|
|
@@ -79,11 +80,11 @@
|
|
|
79
80
|
// =========================
|
|
80
81
|
let editing = $state(false);
|
|
81
82
|
let query = $state("");
|
|
82
|
-
let active = $state(0);
|
|
83
83
|
// What the list shows while a save is in flight, so a tag doesn't blink out and back.
|
|
84
84
|
let pending = $state<string[] | null>(null);
|
|
85
85
|
let input = $state<HTMLInputElement | null>(null);
|
|
86
86
|
let root = $state<HTMLElement | null>(null);
|
|
87
|
+
let menu = $state<HTMLElement | null>(null);
|
|
87
88
|
/** What the live region last said: the tags a save added or removed. */
|
|
88
89
|
let announcement = $state("");
|
|
89
90
|
const listId = $props.id();
|
|
@@ -177,30 +178,35 @@
|
|
|
177
178
|
|
|
178
179
|
function add(name: string) {
|
|
179
180
|
query = "";
|
|
180
|
-
active = 0;
|
|
181
181
|
void commit(addTag(tags, name, options, max));
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
+
// Bits adds the picked row's value to the list (Enter or a click). The name goes through add for its spelling and the max.
|
|
185
|
+
function pick(next: string[]) {
|
|
186
|
+
const name = next.find((entry) => !tags.includes(entry));
|
|
187
|
+
if (name !== undefined) add(name);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// The row Bits highlights: a close option when there is one, else the visible Create row.
|
|
191
|
+
function highlighted() {
|
|
192
|
+
const lit = menu?.querySelector("[data-highlighted]")?.getAttribute("data-value");
|
|
193
|
+
return rows.find((row) => row.name === lit) ?? rows[0];
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Bits owns the arrows and Enter on a typed query. These keys sit on top of it, and run first.
|
|
184
197
|
function onKey(event: KeyboardEvent) {
|
|
185
|
-
const
|
|
186
|
-
const separator = event.key === "," || (
|
|
187
|
-
if (free && (separator || event.key === "
|
|
188
|
-
event.preventDefault();
|
|
189
|
-
if (query.trim()) add(query);
|
|
190
|
-
else if (event.key === "Enter") finish();
|
|
191
|
-
} else if (event.key === "ArrowDown" || event.key === "ArrowUp") {
|
|
192
|
-
if (rows.length === 0) return;
|
|
198
|
+
const typed = query.trim() !== "";
|
|
199
|
+
const separator = event.key === "," || (event.key === "Tab" && typed);
|
|
200
|
+
if (free && (separator || event.key === " " || (event.key === "Enter" && typed))) {
|
|
193
201
|
event.preventDefault();
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
} else if (event.key === "Enter") {
|
|
202
|
+
if (typed) add(query);
|
|
203
|
+
} else if (!free && separator) {
|
|
197
204
|
event.preventDefault();
|
|
198
|
-
|
|
199
|
-
if (
|
|
200
|
-
|
|
201
|
-
} else if (event.key === "," || (event.key === "Tab" && query.trim())) {
|
|
205
|
+
const row = highlighted();
|
|
206
|
+
if (typed && row) add(row.name);
|
|
207
|
+
} else if (event.key === "Enter" && !typed) {
|
|
202
208
|
event.preventDefault();
|
|
203
|
-
|
|
209
|
+
finish();
|
|
204
210
|
} else if (
|
|
205
211
|
event.key === "ArrowLeft" &&
|
|
206
212
|
tags.length > 0 &&
|
|
@@ -257,12 +263,6 @@
|
|
|
257
263
|
const next = event.relatedTarget as Node | null;
|
|
258
264
|
if (editing && !(next && root?.contains(next))) stop();
|
|
259
265
|
}
|
|
260
|
-
|
|
261
|
-
// A new query starts from its best match.
|
|
262
|
-
$effect(() => {
|
|
263
|
-
void query;
|
|
264
|
-
active = 0;
|
|
265
|
-
});
|
|
266
266
|
</script>
|
|
267
267
|
|
|
268
268
|
<!-- =========================== -->
|
|
@@ -283,6 +283,23 @@
|
|
|
283
283
|
</TagChip>
|
|
284
284
|
{/snippet}
|
|
285
285
|
|
|
286
|
+
<!-- The free box is a plain text field. With suggestions, Bits adds the combobox role, state and keys. -->
|
|
287
|
+
{#snippet box(attrs: Record<string, unknown>)}
|
|
288
|
+
<input
|
|
289
|
+
{...attrs}
|
|
290
|
+
bind:this={input}
|
|
291
|
+
bind:value={query}
|
|
292
|
+
type="text"
|
|
293
|
+
aria-label={label}
|
|
294
|
+
autocomplete="off"
|
|
295
|
+
autocapitalize="off"
|
|
296
|
+
spellcheck="false"
|
|
297
|
+
enterkeyhint="done"
|
|
298
|
+
placeholder={tags.length === 0 ? text.add : ""}
|
|
299
|
+
class="h-5 min-w-16 flex-1 bg-transparent px-1 text-base outline-none placeholder:text-muted-foreground sm:text-xs"
|
|
300
|
+
/>
|
|
301
|
+
{/snippet}
|
|
302
|
+
|
|
286
303
|
<div
|
|
287
304
|
bind:this={root}
|
|
288
305
|
class={cn("relative min-w-0", className)}
|
|
@@ -298,62 +315,62 @@
|
|
|
298
315
|
{/each}
|
|
299
316
|
{#if full}
|
|
300
317
|
<span class="px-1 text-xs text-muted-foreground">{text.max}</span>
|
|
318
|
+
{:else if free}
|
|
319
|
+
{@render box({ onkeydown: onKey, onpaste: onPaste })}
|
|
301
320
|
{:else}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
+
<!-- The list's open state follows the query, so Bits' own open and close requests are dropped. -->
|
|
322
|
+
<Combobox.Root
|
|
323
|
+
type="multiple"
|
|
324
|
+
loop
|
|
325
|
+
bind:value={() => tags, pick}
|
|
326
|
+
bind:open={() => open, () => {}}
|
|
327
|
+
>
|
|
328
|
+
<Combobox.Input onkeydown={onKey} onpaste={onPaste}>
|
|
329
|
+
{#snippet child({ props })}
|
|
330
|
+
<!-- The box shows the query, not the text Bits writes after a pick. -->
|
|
331
|
+
{@const { value: _shown, ...bits } = props}
|
|
332
|
+
{@render box({ ...bits, "aria-controls": open ? listId : undefined })}
|
|
333
|
+
{/snippet}
|
|
334
|
+
</Combobox.Input>
|
|
335
|
+
<Combobox.Portal>
|
|
336
|
+
<!-- WHY customAnchor: the list sits under the whole field, not under the box at the end of the tags. -->
|
|
337
|
+
<Combobox.Content
|
|
338
|
+
bind:ref={menu}
|
|
339
|
+
id={listId}
|
|
340
|
+
aria-label={label}
|
|
341
|
+
customAnchor={root}
|
|
342
|
+
side="bottom"
|
|
343
|
+
align="start"
|
|
344
|
+
sideOffset={4}
|
|
345
|
+
collisionPadding={8}
|
|
346
|
+
class="z-50 max-h-60 w-(--bits-combobox-anchor-width) min-w-48 rounded-xl bg-popover text-popover-foreground shadow-edge-raised"
|
|
347
|
+
>
|
|
348
|
+
<Combobox.Viewport class="p-1">
|
|
349
|
+
{#each rows as row (row.kind + row.name)}
|
|
350
|
+
<Combobox.Item
|
|
351
|
+
value={row.name}
|
|
352
|
+
label={row.name}
|
|
353
|
+
class="flex min-h-8 cursor-default items-center gap-2 rounded-lg px-2 text-sm any-pointer-coarse:min-h-11 data-highlighted:bg-foreground/6 data-highlighted:text-foreground"
|
|
354
|
+
>
|
|
355
|
+
{#if row.kind === "create"}
|
|
356
|
+
<Plus class="size-3.5 shrink-0 text-muted-foreground" aria-hidden="true" />
|
|
357
|
+
<span class="truncate">{text.create(row.name)}</span>
|
|
358
|
+
{:else}
|
|
359
|
+
<span class="truncate">{row.name}</span>
|
|
360
|
+
{/if}
|
|
361
|
+
</Combobox.Item>
|
|
362
|
+
{/each}
|
|
363
|
+
{#if nothing}
|
|
364
|
+
<div class="flex min-h-8 items-center px-2 text-sm text-muted-foreground">
|
|
365
|
+
{text.none}
|
|
366
|
+
</div>
|
|
367
|
+
{/if}
|
|
368
|
+
</Combobox.Viewport>
|
|
369
|
+
</Combobox.Content>
|
|
370
|
+
</Combobox.Portal>
|
|
371
|
+
</Combobox.Root>
|
|
321
372
|
{/if}
|
|
322
373
|
</div>
|
|
323
|
-
{#if open}
|
|
324
|
-
<ul
|
|
325
|
-
id={listId}
|
|
326
|
-
role="listbox"
|
|
327
|
-
aria-label={label}
|
|
328
|
-
class="absolute top-full left-0 z-50 mt-1 max-h-60 w-full min-w-48 overflow-y-auto rounded-xl bg-popover p-1 text-popover-foreground shadow-edge-raised"
|
|
329
|
-
>
|
|
330
|
-
{#each rows as row, index (row.kind + row.name)}
|
|
331
|
-
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
332
|
-
<li
|
|
333
|
-
id="{listId}-{index}"
|
|
334
|
-
role="option"
|
|
335
|
-
aria-selected={index === active}
|
|
336
|
-
class={cn(
|
|
337
|
-
"flex min-h-8 cursor-default items-center gap-2 rounded-lg px-2 text-sm any-pointer-coarse:min-h-11",
|
|
338
|
-
index === active && "bg-foreground/6 text-foreground",
|
|
339
|
-
)}
|
|
340
|
-
onmousedown={(e) => e.preventDefault()}
|
|
341
|
-
onmousemove={() => (active = index)}
|
|
342
|
-
onclick={() => add(row.name)}
|
|
343
|
-
>
|
|
344
|
-
{#if row.kind === "create"}
|
|
345
|
-
<Plus class="size-3.5 shrink-0 text-muted-foreground" aria-hidden="true" />
|
|
346
|
-
<span class="truncate">{text.create(row.name)}</span>
|
|
347
|
-
{:else}
|
|
348
|
-
<span class="truncate">{row.name}</span>
|
|
349
|
-
{/if}
|
|
350
|
-
</li>
|
|
351
|
-
{/each}
|
|
352
|
-
{#if nothing}
|
|
353
|
-
<li class="flex min-h-8 items-center px-2 text-sm text-muted-foreground">{text.none}</li>
|
|
354
|
-
{/if}
|
|
355
|
-
</ul>
|
|
356
|
-
{/if}
|
|
357
374
|
{:else}
|
|
358
375
|
<button
|
|
359
376
|
type="button"
|
|
@@ -101,8 +101,9 @@
|
|
|
101
101
|
class="@max-2xl/toolbar:hidden"
|
|
102
102
|
/>
|
|
103
103
|
<DropdownMenu.Root>
|
|
104
|
+
<!-- The name carries the tab it shows (WCAG 2.5.3), so "click Open" reaches it. -->
|
|
104
105
|
<DropdownMenu.Trigger
|
|
105
|
-
aria-label={tabsLabel}
|
|
106
|
+
aria-label={currentTab ? `${tabsLabel}: ${currentTab.label}` : tabsLabel}
|
|
106
107
|
class={cn(toolbarTriggerClass(), "min-w-0 @2xl/toolbar:hidden")}
|
|
107
108
|
>
|
|
108
109
|
<span class="truncate">{currentTab?.label}</span>
|
|
@@ -55,22 +55,23 @@ describe("Toolbar", () => {
|
|
|
55
55
|
tabsLabel: "Status",
|
|
56
56
|
viewLabel: "Layout",
|
|
57
57
|
});
|
|
58
|
-
for (const label of ["Find", "Status", "Layout"]) {
|
|
58
|
+
for (const label of ["Find", "Status: All", "Layout"]) {
|
|
59
59
|
expect(root.querySelector(`button[aria-label="${label}"]`), label).not.toBeNull();
|
|
60
60
|
}
|
|
61
61
|
});
|
|
62
62
|
|
|
63
|
-
|
|
63
|
+
// The name holds the visible tab too (WCAG 2.5.3), so a speech user can say it.
|
|
64
|
+
it("shows the current tab on the narrow menu trigger and names it", async () => {
|
|
64
65
|
const root = render({ search: "", tabs, tab: "open", views, view: "list" });
|
|
65
66
|
await tick();
|
|
66
|
-
expect(root.querySelector('button[aria-label="Show"]')?.textContent?.trim()).toBe("Open");
|
|
67
|
+
expect(root.querySelector('button[aria-label="Show: Open"]')?.textContent?.trim()).toBe("Open");
|
|
67
68
|
});
|
|
68
69
|
|
|
69
70
|
it("leaves out what it isn't given", () => {
|
|
70
71
|
const root = render({ tabs, tab: "all" });
|
|
71
72
|
expect(root.querySelector('button[aria-label="Search"]')).toBeNull();
|
|
72
73
|
expect(root.querySelector('button[aria-label="View"]')).toBeNull();
|
|
73
|
-
expect(root.querySelector('button[aria-label="Show"]')).not.toBeNull();
|
|
74
|
+
expect(root.querySelector('button[aria-label="Show: All"]')).not.toBeNull();
|
|
74
75
|
});
|
|
75
76
|
|
|
76
77
|
// The 10 Nov audit: the wide search box had only its placeholder for a name.
|
|
@@ -6,10 +6,13 @@
|
|
|
6
6
|
import CountChip from "../chip/count-chip.svelte";
|
|
7
7
|
|
|
8
8
|
interface Props {
|
|
9
|
-
label
|
|
9
|
+
label?: string;
|
|
10
|
+
/** Drawn in place of `label`, in the caller's own type, for a head whose name is markup. */
|
|
11
|
+
title?: Snippet;
|
|
10
12
|
/** The status colour of the dot. Leave out for no dot. */
|
|
11
13
|
hue?: string;
|
|
12
|
-
count
|
|
14
|
+
/** Leave out for no count chip. */
|
|
15
|
+
count?: number;
|
|
13
16
|
/** Shows a plus at the end that adds an item to this group. */
|
|
14
17
|
onAdd?: () => void;
|
|
15
18
|
addLabel?: string;
|
|
@@ -18,16 +21,32 @@
|
|
|
18
21
|
class?: string;
|
|
19
22
|
}
|
|
20
23
|
|
|
21
|
-
let {
|
|
24
|
+
let {
|
|
25
|
+
label,
|
|
26
|
+
title,
|
|
27
|
+
hue,
|
|
28
|
+
count,
|
|
29
|
+
onAdd,
|
|
30
|
+
addLabel = "Add",
|
|
31
|
+
children,
|
|
32
|
+
class: className,
|
|
33
|
+
}: Props = $props();
|
|
22
34
|
</script>
|
|
23
35
|
|
|
24
|
-
<!-- A group's head, the same in a kanban column
|
|
25
|
-
|
|
36
|
+
<!-- A group's head, the same in a kanban column, a list group, a grouped list and a table group:
|
|
37
|
+
dot, name, count, plus. A span, since two callers put it inside their toggle button. -->
|
|
38
|
+
<span class={cn("flex h-8 min-w-0 flex-1 items-center gap-2", className)}>
|
|
26
39
|
{#if hue}
|
|
27
40
|
<span class="size-2 shrink-0 rounded-full" style:background={hue} aria-hidden="true"></span>
|
|
28
41
|
{/if}
|
|
29
|
-
|
|
30
|
-
|
|
42
|
+
{#if title}
|
|
43
|
+
{@render title()}
|
|
44
|
+
{:else}
|
|
45
|
+
<span class="truncate text-sm font-medium text-foreground">{label}</span>
|
|
46
|
+
{/if}
|
|
47
|
+
{#if count !== undefined}
|
|
48
|
+
<CountChip {count} />
|
|
49
|
+
{/if}
|
|
31
50
|
{#if children || onAdd}
|
|
32
51
|
<span class="ms-auto flex shrink-0 items-center gap-0.5">
|
|
33
52
|
{@render children?.()}
|
|
@@ -36,7 +55,7 @@
|
|
|
36
55
|
variant="ghost"
|
|
37
56
|
size="sm"
|
|
38
57
|
icon
|
|
39
|
-
aria-label=
|
|
58
|
+
aria-label={label ? `${addLabel}: ${label}` : addLabel}
|
|
40
59
|
class="text-muted-foreground"
|
|
41
60
|
onclick={onAdd}
|
|
42
61
|
>
|
|
@@ -45,4 +64,4 @@
|
|
|
45
64
|
{/if}
|
|
46
65
|
</span>
|
|
47
66
|
{/if}
|
|
48
|
-
</
|
|
67
|
+
</span>
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import type { Snippet } from "svelte";
|
|
2
2
|
interface Props {
|
|
3
|
-
label
|
|
3
|
+
label?: string;
|
|
4
|
+
/** Drawn in place of `label`, in the caller's own type, for a head whose name is markup. */
|
|
5
|
+
title?: Snippet;
|
|
4
6
|
/** The status colour of the dot. Leave out for no dot. */
|
|
5
7
|
hue?: string;
|
|
6
|
-
count
|
|
8
|
+
/** Leave out for no count chip. */
|
|
9
|
+
count?: number;
|
|
7
10
|
/** Shows a plus at the end that adds an item to this group. */
|
|
8
11
|
onAdd?: () => void;
|
|
9
12
|
addLabel?: string;
|