@heyramzi/ui 3.1.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/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/package.json +1 -1
- 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/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/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 +223 -193
- package/src/editor/editor.svelte +56 -38
- 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 +11 -8
- 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
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// A span whose `text` shows as a tooltip on hover and on focus. A native title never
|
|
3
|
+
// showed on focus, so the keyboard never reached it.
|
|
4
|
+
import type { Snippet } from "svelte";
|
|
5
|
+
import * as Tooltip from "../../shadcn/tooltip/index.js";
|
|
6
|
+
import { cn } from "../../lib/utils.js";
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
/** Left out, the span is plain and takes no focus. */
|
|
10
|
+
text?: string;
|
|
11
|
+
class?: string;
|
|
12
|
+
children: Snippet;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let { text, class: className, children }: Props = $props();
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
{#if text}
|
|
19
|
+
<Tooltip.Root>
|
|
20
|
+
<Tooltip.Trigger>
|
|
21
|
+
{#snippet child({ props })}
|
|
22
|
+
<span
|
|
23
|
+
{...props}
|
|
24
|
+
class={cn("rounded-xs outline-none focus-visible:shadow-(--ring-focus)", className)}
|
|
25
|
+
>
|
|
26
|
+
{@render children()}
|
|
27
|
+
</span>
|
|
28
|
+
{/snippet}
|
|
29
|
+
</Tooltip.Trigger>
|
|
30
|
+
<Tooltip.Content>{text}</Tooltip.Content>
|
|
31
|
+
</Tooltip.Root>
|
|
32
|
+
{:else}
|
|
33
|
+
<span class={className}>{@render children()}</span>
|
|
34
|
+
{/if}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Snippet } from "svelte";
|
|
2
|
+
interface Props {
|
|
3
|
+
/** Left out, the span is plain and takes no focus. */
|
|
4
|
+
text?: string;
|
|
5
|
+
class?: string;
|
|
6
|
+
children: Snippet;
|
|
7
|
+
}
|
|
8
|
+
declare const $$Component: import("svelte").Component<Props, {}, "">;
|
|
9
|
+
type $$Component = ReturnType<typeof $$Component>;
|
|
10
|
+
export default $$Component;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// One A to E grade on the shared tone scale, so a C means one thing everywhere.
|
|
3
3
|
import { TONE_TINT, type Tone } from "../../lib/tone.js";
|
|
4
4
|
import { cn } from "../../lib/utils.js";
|
|
5
|
+
import Hint from "./hint.svelte";
|
|
5
6
|
|
|
6
7
|
interface Props {
|
|
7
8
|
grade: string;
|
|
@@ -21,13 +22,13 @@
|
|
|
21
22
|
};
|
|
22
23
|
</script>
|
|
23
24
|
|
|
24
|
-
<
|
|
25
|
+
<Hint
|
|
26
|
+
text={hint}
|
|
25
27
|
class={cn(
|
|
26
28
|
"inline-flex h-6 min-w-6 items-center justify-center rounded-md px-1.5 text-xs font-semibold tabular-nums",
|
|
27
29
|
TONE_TINT[GRADE_TONE[grade] ?? "neutral"],
|
|
28
30
|
className,
|
|
29
31
|
)}
|
|
30
|
-
title={hint}
|
|
31
32
|
>
|
|
32
33
|
{grade}
|
|
33
|
-
</
|
|
34
|
+
</Hint>
|
package/src/charts/legend.css
CHANGED
|
@@ -39,13 +39,10 @@
|
|
|
39
39
|
align-items: center;
|
|
40
40
|
gap: 0.5rem 0.625rem;
|
|
41
41
|
padding: 0.375rem 0.5rem;
|
|
42
|
-
border: 0;
|
|
43
42
|
border-radius: 0.5rem;
|
|
44
43
|
background: transparent;
|
|
45
|
-
font: inherit;
|
|
46
44
|
color: var(--foreground);
|
|
47
45
|
text-align: left;
|
|
48
|
-
cursor: pointer;
|
|
49
46
|
transition:
|
|
50
47
|
background 160ms ease,
|
|
51
48
|
opacity 160ms ease;
|
|
@@ -57,11 +54,6 @@
|
|
|
57
54
|
padding: 0.25rem 0.375rem;
|
|
58
55
|
}
|
|
59
56
|
|
|
60
|
-
.hr-legend-item:focus-visible {
|
|
61
|
-
background: var(--muted);
|
|
62
|
-
outline: none;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
57
|
@media (hover: hover) and (pointer: fine) {
|
|
66
58
|
.hr-legend-item:hover {
|
|
67
59
|
background: var(--muted);
|
package/src/charts/legend.svelte
CHANGED
|
@@ -77,36 +77,32 @@
|
|
|
77
77
|
<ul class="hr-legend-items">
|
|
78
78
|
{#each items as item, i (item.label + i)}
|
|
79
79
|
{@const dimmed = hoveredIndex !== null && hoveredIndex !== i}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
<span class="hr-legend-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
></span>
|
|
107
|
-
</span>
|
|
108
|
-
{/if}
|
|
109
|
-
</button>
|
|
80
|
+
<!-- A row, not a button: hover only highlights its series, so a Tab stop here did nothing. -->
|
|
81
|
+
<li
|
|
82
|
+
class="hr-legend-item"
|
|
83
|
+
class:dimmed
|
|
84
|
+
onmouseenter={() => setHover(i)}
|
|
85
|
+
onmouseleave={() => setHover(null)}
|
|
86
|
+
>
|
|
87
|
+
<span class="hr-legend-marker" style:background={item.color}></span>
|
|
88
|
+
<span class="hr-legend-label">{item.label}</span>
|
|
89
|
+
{#if showValue}
|
|
90
|
+
<span class="hr-legend-value">
|
|
91
|
+
{formatValue(item.value)}{#if showPercentage && item.maxValue}<span
|
|
92
|
+
class="hr-legend-pct">{formatPercentage(pct(item))}</span
|
|
93
|
+
>{/if}
|
|
94
|
+
</span>
|
|
95
|
+
{/if}
|
|
96
|
+
{#if showProgress && item.maxValue}
|
|
97
|
+
<span class="hr-legend-track">
|
|
98
|
+
<span
|
|
99
|
+
class="hr-legend-bar"
|
|
100
|
+
style:--hr-bar-scale={pct(item) / 100}
|
|
101
|
+
style:background={item.color}
|
|
102
|
+
style:animation-delay="{160 + i * 120}ms"
|
|
103
|
+
></span>
|
|
104
|
+
</span>
|
|
105
|
+
{/if}
|
|
110
106
|
</li>
|
|
111
107
|
{/each}
|
|
112
108
|
</ul>
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
// =========================
|
|
52
52
|
import { Check, Copy, StarFour } from "phosphor-svelte";
|
|
53
53
|
import { cn } from "../../lib/utils.js";
|
|
54
|
+
import LiveRegion from "../../lib/live-region.svelte";
|
|
54
55
|
import { buttonVariants } from "../../shadcn/button/index.js";
|
|
55
56
|
import { copyText } from "./clipboard.js";
|
|
56
57
|
|
|
@@ -81,6 +82,13 @@
|
|
|
81
82
|
// DERIVED
|
|
82
83
|
// =========================
|
|
83
84
|
const inline = $derived(text !== undefined && variant === "inline");
|
|
85
|
+
// A chip's name holds the text it shows (WCAG 2.5.3, label in name), so a
|
|
86
|
+
// speech user who says "click acme-4821" reaches it.
|
|
87
|
+
const restingName = $derived(
|
|
88
|
+
text === undefined || label.toLowerCase().includes(text.toLowerCase())
|
|
89
|
+
? label
|
|
90
|
+
: `${label} ${text}`,
|
|
91
|
+
);
|
|
84
92
|
|
|
85
93
|
// =========================
|
|
86
94
|
// FUNCTIONS
|
|
@@ -107,7 +115,7 @@
|
|
|
107
115
|
type="button"
|
|
108
116
|
onclick={copy}
|
|
109
117
|
title={label}
|
|
110
|
-
aria-label={announce === "name" && copied ? copiedLabel :
|
|
118
|
+
aria-label={announce === "name" && copied ? copiedLabel : restingName}
|
|
111
119
|
class={cn(
|
|
112
120
|
"group/copy t-press shrink-0 outline-none focus-visible:shadow-(--ring-focus)",
|
|
113
121
|
text === undefined
|
|
@@ -170,6 +178,6 @@
|
|
|
170
178
|
|
|
171
179
|
<!-- The swap is invisible to a reader who cannot see it. -->
|
|
172
180
|
{#if announce === "region"}
|
|
173
|
-
<
|
|
181
|
+
<LiveRegion message={copied ? copiedLabel : ""} />
|
|
174
182
|
{/if}
|
|
175
183
|
</button>
|
|
@@ -60,6 +60,13 @@ describe("CopyButton", () => {
|
|
|
60
60
|
done();
|
|
61
61
|
});
|
|
62
62
|
|
|
63
|
+
it("adds the chip's visible text to a label that leaves it out", () => {
|
|
64
|
+
stubClipboard();
|
|
65
|
+
const { button, done } = render({ value: "acme-4821", text: "acme-4821", label: "Copy id" });
|
|
66
|
+
expect(button.getAttribute("aria-label")).toBe("Copy id acme-4821");
|
|
67
|
+
done();
|
|
68
|
+
});
|
|
69
|
+
|
|
63
70
|
it("announces in a live region by default and keeps its name", async () => {
|
|
64
71
|
stubClipboard();
|
|
65
72
|
const { button, target, done } = render({
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
// ========================= IMPORTS
|
|
3
|
+
import { cn } from "../../lib/utils.js";
|
|
3
4
|
import { Skeleton } from "../../shadcn/skeleton/index.js";
|
|
4
5
|
|
|
5
6
|
// ========================= TYPES
|
|
@@ -8,6 +9,8 @@
|
|
|
8
9
|
align?: "left" | "center" | "right";
|
|
9
10
|
/** Show a small circle before the skeleton bar (e.g. logo/avatar column) */
|
|
10
11
|
circle?: boolean;
|
|
12
|
+
/** Merged onto the cell, for a table whose own cells pad, hide or rule differently. */
|
|
13
|
+
class?: string;
|
|
11
14
|
};
|
|
12
15
|
|
|
13
16
|
interface Props {
|
|
@@ -17,11 +20,13 @@
|
|
|
17
20
|
columns?: ColumnDef[];
|
|
18
21
|
/** Simple mode: just specify column count */
|
|
19
22
|
columnCount?: number;
|
|
23
|
+
/** Merged onto each row, for a table whose own rows draw a different line. */
|
|
24
|
+
rowClass?: string;
|
|
20
25
|
class?: string;
|
|
21
26
|
}
|
|
22
27
|
|
|
23
28
|
// ========================= PROPS
|
|
24
|
-
let { rows = 5, columns, columnCount = 6, class: className }: Props = $props();
|
|
29
|
+
let { rows = 5, columns, columnCount = 6, rowClass, class: className }: Props = $props();
|
|
25
30
|
|
|
26
31
|
// ========================= DERIVED
|
|
27
32
|
let cols = $derived(
|
|
@@ -40,19 +45,22 @@
|
|
|
40
45
|
<!-- =========================== -->
|
|
41
46
|
<tbody class={className}>
|
|
42
47
|
{#each { length: rows } as _, rowIdx (rowIdx)}
|
|
48
|
+
<!-- Placeholders, so a screen reader skips them rather than reading empty rows. -->
|
|
43
49
|
<tr
|
|
44
50
|
data-slot="table-row"
|
|
45
|
-
class="border-b border-border/70 transition-opacity"
|
|
51
|
+
class={cn("border-b border-border/70 transition-opacity", rowClass)}
|
|
52
|
+
aria-hidden="true"
|
|
46
53
|
style="animation: skeleton-fade-in var(--duration-slow) var(--ease-smooth-out) calc(var(--duration-stagger) * {rowIdx}) both"
|
|
47
54
|
>
|
|
48
55
|
{#each cols as col, colIdx (colIdx)}
|
|
49
56
|
<td
|
|
50
57
|
data-slot="table-cell"
|
|
51
|
-
class=
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
58
|
+
class={cn(
|
|
59
|
+
"h-8 px-2 any-pointer-coarse:h-11",
|
|
60
|
+
col.align === "center" && "text-center",
|
|
61
|
+
col.align === "right" && "text-right",
|
|
62
|
+
col.class,
|
|
63
|
+
)}
|
|
56
64
|
>
|
|
57
65
|
<div
|
|
58
66
|
class="flex items-center gap-2 {col.align === 'center'
|
|
@@ -3,6 +3,8 @@ type ColumnDef = {
|
|
|
3
3
|
align?: "left" | "center" | "right";
|
|
4
4
|
/** Show a small circle before the skeleton bar (e.g. logo/avatar column) */
|
|
5
5
|
circle?: boolean;
|
|
6
|
+
/** Merged onto the cell, for a table whose own cells pad, hide or rule differently. */
|
|
7
|
+
class?: string;
|
|
6
8
|
};
|
|
7
9
|
interface Props {
|
|
8
10
|
/** Number of skeleton rows to render */
|
|
@@ -11,6 +13,8 @@ interface Props {
|
|
|
11
13
|
columns?: ColumnDef[];
|
|
12
14
|
/** Simple mode: just specify column count */
|
|
13
15
|
columnCount?: number;
|
|
16
|
+
/** Merged onto each row, for a table whose own rows draw a different line. */
|
|
17
|
+
rowClass?: string;
|
|
14
18
|
class?: string;
|
|
15
19
|
}
|
|
16
20
|
declare const $$Component: import("svelte").Component<Props, {}, "">;
|
|
@@ -123,7 +123,7 @@
|
|
|
123
123
|
aria-label={item.label}
|
|
124
124
|
aria-current={current ? "location" : undefined}
|
|
125
125
|
onclick={() => pick(item.id)}
|
|
126
|
-
class="otp-tick group/tick flex h-
|
|
126
|
+
class="otp-tick group/tick flex h-6 w-7 items-center pl-2"
|
|
127
127
|
>
|
|
128
128
|
<span
|
|
129
129
|
class="block h-0.5 rounded-full transition-[width,background-color] duration-[var(--duration-fast)] ease-[var(--ease-smooth-out)] {current
|
|
@@ -203,8 +203,8 @@
|
|
|
203
203
|
{:else}
|
|
204
204
|
<Popover.Root bind:open>
|
|
205
205
|
<div class="relative">
|
|
206
|
+
<!-- Named by its own text, the label then the current heading (WCAG 2.5.3, label in name). -->
|
|
206
207
|
<Popover.Trigger
|
|
207
|
-
aria-label={label}
|
|
208
208
|
class="flex h-11 w-full items-center gap-2.5 px-4 text-left transition-colors duration-[var(--duration-fast)] ease-[var(--ease-smooth-out)] hover:bg-muted/50"
|
|
209
209
|
>
|
|
210
210
|
<List size={16} class="shrink-0 text-muted-foreground" aria-hidden="true" />
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
// ========================= IMPORTS
|
|
3
3
|
import { cn } from "../../lib/utils.js";
|
|
4
|
+
import LiveRegion from "../../lib/live-region.svelte";
|
|
4
5
|
import { buttonVariants, type ButtonSize, type ButtonVariant } from "../../shadcn/button/index.js";
|
|
5
6
|
import type { Snippet } from "svelte";
|
|
6
7
|
import type { HTMLButtonAttributes } from "svelte/elements";
|
|
@@ -191,7 +192,7 @@
|
|
|
191
192
|
</span>
|
|
192
193
|
</button>
|
|
193
194
|
<!-- Outside the button so it isn't part of its name, and mounted empty so the prompt is read when it lands. -->
|
|
194
|
-
<
|
|
195
|
+
<LiveRegion message={armed ? confirmPrompt : ""} />
|
|
195
196
|
|
|
196
197
|
<!-- ========================= STYLE -->
|
|
197
198
|
<style>
|
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
// IMPORTS
|
|
4
4
|
// =========================
|
|
5
5
|
import type { Snippet } from "svelte";
|
|
6
|
-
import {
|
|
6
|
+
import { DropdownMenu } from "bits-ui";
|
|
7
7
|
import { Check } from "phosphor-svelte";
|
|
8
8
|
import { cn } from "../../lib/utils.js";
|
|
9
|
+
import { checkboxBoxClass } from "../../shadcn/checkbox/checkbox-box.js";
|
|
9
10
|
|
|
10
11
|
// =========================
|
|
11
12
|
// TYPES
|
|
@@ -26,10 +27,14 @@
|
|
|
26
27
|
<!-- =========================== -->
|
|
27
28
|
<!-- MARKUP -->
|
|
28
29
|
<!-- =========================== -->
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
<!-- WHY both variants are checkbox items: a radio item needs a group value, and `checked` with
|
|
31
|
+
`onclick` is the whole interface callers have. The setter hands the toggle to `onclick`, so
|
|
32
|
+
the caller's state stays the only truth. -->
|
|
33
|
+
<DropdownMenu.CheckboxItem
|
|
34
|
+
bind:checked={() => checked, () => onclick()}
|
|
35
|
+
closeOnSelect={false}
|
|
36
|
+
data-slot="dropdown-menu-checkbox-item"
|
|
37
|
+
class="flex min-h-8 items-center gap-2 rounded-lg px-2 text-sm outline-hidden select-none transition-colors duration-quick ease-smooth-out hover:bg-foreground/6 data-highlighted:bg-foreground/6 motion-reduce:transition-none any-pointer-coarse:min-h-11"
|
|
33
38
|
>
|
|
34
39
|
{#if variant === "radio"}
|
|
35
40
|
<span
|
|
@@ -43,7 +48,15 @@
|
|
|
43
48
|
{/if}
|
|
44
49
|
</span>
|
|
45
50
|
{:else}
|
|
46
|
-
<
|
|
51
|
+
<span
|
|
52
|
+
aria-hidden="true"
|
|
53
|
+
data-state={checked ? "checked" : "unchecked"}
|
|
54
|
+
class={checkboxBoxClass}
|
|
55
|
+
>
|
|
56
|
+
{#if checked}
|
|
57
|
+
<Check aria-hidden="true" size={14} />
|
|
58
|
+
{/if}
|
|
59
|
+
</span>
|
|
47
60
|
{/if}
|
|
48
61
|
{@render children()}
|
|
49
|
-
</
|
|
62
|
+
</DropdownMenu.CheckboxItem>
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
// =========================
|
|
3
3
|
// IMPORTS
|
|
4
4
|
// =========================
|
|
5
|
-
import type
|
|
6
|
-
import
|
|
5
|
+
import { tick, type Snippet } from "svelte";
|
|
6
|
+
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
|
7
|
+
import * as DropdownMenu from "../../shadcn/dropdown-menu/index.js";
|
|
7
8
|
import { Funnel as FunnelIcon } from "phosphor-svelte";
|
|
8
9
|
import { cn } from "../../lib/utils.js";
|
|
9
10
|
import { Button } from "../../shadcn/button/index.js";
|
|
@@ -58,13 +59,28 @@
|
|
|
58
59
|
// =========================
|
|
59
60
|
let hasCount = $derived(count > 0);
|
|
60
61
|
let isActive = $derived(active ?? hasCount);
|
|
62
|
+
|
|
63
|
+
// =========================
|
|
64
|
+
// STATE
|
|
65
|
+
// =========================
|
|
66
|
+
let content = $state<HTMLElement | null>(null);
|
|
67
|
+
|
|
68
|
+
// =========================
|
|
69
|
+
// HANDLERS
|
|
70
|
+
// =========================
|
|
71
|
+
// Clear unmounts itself, so focus goes back to the menu and the arrows keep working.
|
|
72
|
+
async function clear() {
|
|
73
|
+
onClear?.();
|
|
74
|
+
await tick();
|
|
75
|
+
content?.focus();
|
|
76
|
+
}
|
|
61
77
|
</script>
|
|
62
78
|
|
|
63
79
|
<!-- =========================== -->
|
|
64
80
|
<!-- MARKUP -->
|
|
65
81
|
<!-- =========================== -->
|
|
66
|
-
<
|
|
67
|
-
<
|
|
82
|
+
<DropdownMenu.Root bind:open>
|
|
83
|
+
<DropdownMenu.Trigger class={toolbarTriggerClass({ active: isActive, className: triggerClass })}>
|
|
68
84
|
<IconComponent aria-hidden="true" class="size-3.5" />
|
|
69
85
|
<span>{label}</span>
|
|
70
86
|
{#if trailing}
|
|
@@ -73,18 +89,27 @@
|
|
|
73
89
|
{#if hasCount}
|
|
74
90
|
<CountChip {count} size="sm" tone="brand" class="ml-0.5" />
|
|
75
91
|
{/if}
|
|
76
|
-
</
|
|
77
|
-
|
|
92
|
+
</DropdownMenu.Trigger>
|
|
93
|
+
<!-- The popover's surface: rounder, text-sm, sized by `width` and not by the trigger. -->
|
|
94
|
+
<DropdownMenu.Content
|
|
95
|
+
bind:ref={content}
|
|
96
|
+
class={cn("min-w-0 rounded-2xl p-2 text-sm", width, contentClass)}
|
|
97
|
+
{align}
|
|
98
|
+
>
|
|
78
99
|
{@render children()}
|
|
79
100
|
{#if onClear && hasCount}
|
|
80
|
-
<
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
101
|
+
<DropdownMenuPrimitive.Item closeOnSelect={false} onSelect={clear}>
|
|
102
|
+
{#snippet child({ props })}
|
|
103
|
+
<Button
|
|
104
|
+
{...props}
|
|
105
|
+
variant="ghost"
|
|
106
|
+
size="sm"
|
|
107
|
+
class="mt-2 w-full text-xs text-muted-foreground"
|
|
108
|
+
>
|
|
109
|
+
{clearLabel}
|
|
110
|
+
</Button>
|
|
111
|
+
{/snippet}
|
|
112
|
+
</DropdownMenuPrimitive.Item>
|
|
88
113
|
{/if}
|
|
89
|
-
</
|
|
90
|
-
</
|
|
114
|
+
</DropdownMenu.Content>
|
|
115
|
+
</DropdownMenu.Root>
|
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
// =========================
|
|
5
5
|
import { untrack, type Snippet } from "svelte";
|
|
6
6
|
import { SvelteSet } from "svelte/reactivity";
|
|
7
|
+
import { Accordion } from "bits-ui";
|
|
7
8
|
import { CaretDown, Plus } from "phosphor-svelte";
|
|
8
9
|
import { cn } from "../../lib/utils.js";
|
|
9
|
-
import
|
|
10
|
+
import GroupHead from "../work-parts/group-head.svelte";
|
|
10
11
|
|
|
11
12
|
// =========================
|
|
12
13
|
// TYPES
|
|
@@ -58,16 +59,24 @@
|
|
|
58
59
|
// =========================
|
|
59
60
|
// STATE
|
|
60
61
|
// =========================
|
|
62
|
+
// WHY the collapsed set stays the truth and not the accordion's open list: a group that
|
|
63
|
+
// leaves `groups` and comes back keeps its fold.
|
|
61
64
|
const collapsed = new SvelteSet<string>(untrack(() => defaultCollapsed));
|
|
65
|
+
const uid = $props.id();
|
|
66
|
+
|
|
67
|
+
// =========================
|
|
68
|
+
// DERIVED
|
|
69
|
+
// =========================
|
|
70
|
+
let openKeys = $derived(groups.map(getKey).filter((key) => !collapsed.has(key)));
|
|
62
71
|
|
|
63
72
|
// =========================
|
|
64
73
|
// FUNCTIONS
|
|
65
74
|
// =========================
|
|
66
|
-
function
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
collapsed.add(key);
|
|
75
|
+
function setOpen(next: string[]) {
|
|
76
|
+
for (const group of groups) {
|
|
77
|
+
const key = getKey(group);
|
|
78
|
+
if (next.includes(key)) collapsed.delete(key);
|
|
79
|
+
else collapsed.add(key);
|
|
71
80
|
}
|
|
72
81
|
onCollapsedChange?.([...collapsed]);
|
|
73
82
|
}
|
|
@@ -76,13 +85,19 @@
|
|
|
76
85
|
<!-- =========================== -->
|
|
77
86
|
<!-- MARKUP -->
|
|
78
87
|
<!-- =========================== -->
|
|
79
|
-
<
|
|
88
|
+
<Accordion.Root
|
|
89
|
+
type="multiple"
|
|
90
|
+
bind:value={() => openKeys, setOpen}
|
|
91
|
+
class={cn("flex w-full flex-col", className)}
|
|
92
|
+
>
|
|
80
93
|
{#each groups as group, index (getKey(group))}
|
|
81
94
|
{@const key = getKey(group)}
|
|
82
95
|
{@const isCollapsed = collapsed.has(key)}
|
|
96
|
+
{@const rowsId = `${uid}-rows-${index}`}
|
|
83
97
|
<!-- Groups part by space, not a rule: the header band already marks where one starts
|
|
84
98
|
(DESIGN.md rule 19). -->
|
|
85
|
-
<
|
|
99
|
+
<Accordion.Item
|
|
100
|
+
value={key}
|
|
86
101
|
class={cn(index > 0 && "mt-4")}
|
|
87
102
|
role={onGroupDragOver || onGroupDrop ? "group" : undefined}
|
|
88
103
|
ondragover={onGroupDragOver ? (e) => onGroupDragOver(e, group) : undefined}
|
|
@@ -94,11 +109,10 @@
|
|
|
94
109
|
<div
|
|
95
110
|
class="group/header hover-row flex min-h-8 items-center gap-1 rounded-lg bg-foreground/4 transition-colors duration-quick ease-smooth-out hover:bg-foreground/6 motion-reduce:transition-none any-pointer-coarse:min-h-11"
|
|
96
111
|
>
|
|
97
|
-
|
|
98
|
-
|
|
112
|
+
<!-- No Accordion.Header: it would make every group lid a heading. -->
|
|
113
|
+
<Accordion.Trigger
|
|
99
114
|
class="flex min-w-0 flex-1 cursor-pointer items-center gap-2 self-stretch rounded-lg px-2.5 text-left outline-none focus-visible:shadow-[var(--ring-focus)]"
|
|
100
|
-
|
|
101
|
-
aria-expanded={!isCollapsed}
|
|
115
|
+
aria-controls={rowsId}
|
|
102
116
|
>
|
|
103
117
|
<!-- The accordion chevron: it turns on the fast step and holds still under
|
|
104
118
|
reduced motion, where the open state is aria-expanded and the rows. -->
|
|
@@ -110,11 +124,13 @@
|
|
|
110
124
|
)}
|
|
111
125
|
weight="bold"
|
|
112
126
|
/>
|
|
113
|
-
|
|
114
|
-
{
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
127
|
+
<!-- h-auto: the band sets the height, so a tall header still grows it. -->
|
|
128
|
+
<GroupHead count={getCount?.(group)} class="h-auto">
|
|
129
|
+
{#snippet title()}
|
|
130
|
+
{@render header(group, { collapsed: isCollapsed })}
|
|
131
|
+
{/snippet}
|
|
132
|
+
</GroupHead>
|
|
133
|
+
</Accordion.Trigger>
|
|
118
134
|
{#if actions}
|
|
119
135
|
<div class="row-action flex shrink-0 items-center gap-1 pe-1">
|
|
120
136
|
{@render actions(group)}
|
|
@@ -132,14 +148,16 @@
|
|
|
132
148
|
{/if}
|
|
133
149
|
</div>
|
|
134
150
|
|
|
135
|
-
|
|
136
|
-
|
|
151
|
+
<!-- bits-ui keeps a closed panel in the DOM, hidden. The rows stay unmounted, so a
|
|
152
|
+
folded group of 200 rows costs nothing. -->
|
|
153
|
+
<Accordion.Content id={rowsId}>
|
|
154
|
+
{#if !isCollapsed}
|
|
137
155
|
{@render rows(group)}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
</
|
|
156
|
+
{/if}
|
|
157
|
+
</Accordion.Content>
|
|
158
|
+
</Accordion.Item>
|
|
141
159
|
{/each}
|
|
142
|
-
</
|
|
160
|
+
</Accordion.Root>
|
|
143
161
|
|
|
144
162
|
<!-- No entrance motion. Four stacked animations made the last group of a
|
|
145
163
|
40-row board arrive two seconds late. Working surfaces don't animate in. -->
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
options?: PickerOption<V>[];
|
|
19
19
|
/** Titled runs, when the menu has a shape worth naming. */
|
|
20
20
|
groups?: PickerGroup<V>[];
|
|
21
|
-
|
|
21
|
+
/** An array checks each row in it, and a pick leaves the menu open for the next one. */
|
|
22
|
+
value: V | V[] | null;
|
|
22
23
|
onSelect: (value: V) => void;
|
|
23
24
|
/** The control that opens the menu. Rendered inside `Popover.Trigger`. */
|
|
24
25
|
trigger: Snippet;
|
|
@@ -74,10 +75,14 @@
|
|
|
74
75
|
const WIDTH = { sm: "w-44", default: "w-52", lg: "w-60" } as const;
|
|
75
76
|
|
|
76
77
|
function pick(next: V) {
|
|
77
|
-
open = false;
|
|
78
|
+
if (!Array.isArray(value)) open = false;
|
|
78
79
|
onSelect(next);
|
|
79
80
|
}
|
|
80
81
|
|
|
82
|
+
function isChecked(option: PickerOption<V>): boolean {
|
|
83
|
+
return Array.isArray(value) ? value.includes(option.value) : option.value === value;
|
|
84
|
+
}
|
|
85
|
+
|
|
81
86
|
/**
|
|
82
87
|
* WHY a custom filter: the default one scores `value`, which is often an id.
|
|
83
88
|
* Typing "an" fuzzy-matched every uuid, so only the visible words are searched.
|
|
@@ -152,7 +157,7 @@
|
|
|
152
157
|
{#each resolved as group, i (group.heading ?? i)}
|
|
153
158
|
<Command.Group heading={group.heading}>
|
|
154
159
|
{#each group.options as option (option.value)}
|
|
155
|
-
{@const active = option
|
|
160
|
+
{@const active = isChecked(option)}
|
|
156
161
|
<Command.Item
|
|
157
162
|
value={option.value}
|
|
158
163
|
keywords={keywordsFor(option)}
|
|
@@ -6,7 +6,8 @@ declare function $$render<V extends string>(): {
|
|
|
6
6
|
options?: PickerOption<V>[];
|
|
7
7
|
/** Titled runs, when the menu has a shape worth naming. */
|
|
8
8
|
groups?: PickerGroup<V>[];
|
|
9
|
-
|
|
9
|
+
/** An array checks each row in it, and a pick leaves the menu open for the next one. */
|
|
10
|
+
value: V | V[] | null;
|
|
10
11
|
onSelect: (value: V) => void;
|
|
11
12
|
/** The control that opens the menu. Rendered inside `Popover.Trigger`. */
|
|
12
13
|
trigger: Snippet;
|