@heyramzi/ui 7.0.0 → 8.1.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/COMPONENTS.md +28 -17
- package/README.md +6 -2
- package/package.json +25 -15
- package/scripts/sync-shadcn.mjs +17 -13
- package/scripts/sync-transitions.mjs +11 -11
- package/src/agents/agent-avatar.svelte +67 -114
- package/src/agents/agent-cast.svelte +31 -7
- package/src/agents/demo.svelte +55 -8
- package/src/agents/helpers.ts +58 -0
- package/src/agents/page-state.ts +70 -0
- package/src/agents/types.ts +3 -1
- package/src/ai/ai-button/AiButton.svelte +86 -0
- package/src/ai/ai-button/index.ts +4 -0
- package/src/ai/confirmation/Confirmation.svelte +8 -8
- package/src/ai/confirmation/confirmation-context.svelte.ts +2 -2
- package/src/ai/generation-progress/GenerationProgress.svelte +354 -0
- package/src/ai/generation-progress/index.ts +7 -0
- package/src/ai/index.ts +16 -6
- package/src/ai/prompt-input/PromptInput.svelte +19 -1
- package/src/ai/prompt-input/PromptInputSubmit.svelte +9 -3
- package/src/ai/reasoning/Reasoning.svelte +11 -8
- package/src/ambient.d.ts +4 -0
- package/src/animations/README.md +14 -14
- package/src/animations/index.css +3 -14
- package/src/animations/upstream-coverage.test.ts +1 -1
- package/src/components/copy/clipboard.test.ts +206 -0
- package/src/components/copy/clipboard.ts +49 -0
- package/src/components/copy/copied.svelte.ts +39 -0
- package/src/components/copy/copy-button.svelte +175 -0
- package/src/components/copy/copy-button.test.ts +110 -0
- package/src/components/copy/index.ts +3 -0
- package/src/components/data-table/data-table.test.ts +134 -0
- package/src/components/data-table/data-table.ts +77 -0
- package/src/components/data-table/index.ts +20 -0
- package/src/components/data-table/infinite-scroll.svelte +69 -0
- package/src/components/data-table/pagination.svelte +62 -0
- package/src/components/data-table/row-arrow.svelte +28 -0
- package/src/components/data-table/row-context-menu.svelte +106 -0
- package/src/components/data-table/row-context-menu.test.ts +18 -0
- package/src/components/data-table/sortable-head.svelte +56 -0
- package/src/components/data-table/table-skeleton.svelte +83 -0
- package/src/components/hold-to-confirm/hold-to-confirm.svelte +232 -0
- package/src/components/hold-to-confirm/index.ts +1 -0
- package/src/components/icon/icon.svelte +23 -0
- package/src/components/icon/index.ts +1 -0
- package/src/components/language-flag/index.ts +1 -0
- package/src/components/language-flag/language-flag.svelte +60 -0
- package/src/components/list-page/detail-empty.svelte +50 -0
- package/src/components/list-page/detail-header.svelte +107 -0
- package/src/components/list-page/detail-panel.svelte +90 -0
- package/src/components/list-page/detail-row.svelte +36 -0
- package/src/components/list-page/detail-stack.svelte +33 -0
- package/src/components/list-page/empty-state.svelte +91 -0
- package/src/components/list-page/filter-item.svelte +49 -0
- package/src/components/list-page/filter-popover.svelte +88 -0
- package/src/components/list-page/filter-section.ts +12 -0
- package/src/components/list-page/grouped-list.svelte +116 -0
- package/src/components/list-page/index.ts +28 -0
- package/src/components/list-page/list-page-shell.svelte +195 -0
- package/src/components/list-page/list-table.ts +19 -0
- package/src/components/list-page/master-detail.svelte +206 -0
- package/src/components/list-page/master-detail.test.ts +37 -0
- package/src/components/list-page/notice-bar.svelte +68 -0
- package/src/components/list-page/prev-next.svelte +104 -0
- package/src/components/list-page/queue-rail.ts +25 -0
- package/src/components/list-page/rail-body.svelte +29 -0
- package/src/components/list-page/rail-empty.svelte +51 -0
- package/src/components/list-page/rail-head.svelte +49 -0
- package/src/components/list-page/rail-row.svelte +163 -0
- package/src/components/list-page/section-host.ts +24 -0
- package/src/components/list-page/section-label.svelte +39 -0
- package/src/components/list-page/step.test.ts +21 -0
- package/src/components/list-page/step.ts +10 -0
- package/src/components/list-page/toolbar-search.svelte +63 -0
- package/src/components/list-page/toolbar-trigger.ts +23 -0
- package/src/components/list-page/types.ts +1 -0
- package/src/components/locale-switch/index.ts +1 -0
- package/src/components/locale-switch/locale-switch.svelte +59 -0
- package/src/components/mark/index.ts +1 -0
- package/src/components/mark/mark.svelte +70 -0
- package/src/components/open-in/index.ts +6 -0
- package/src/components/open-in/open-in.svelte +106 -0
- package/src/components/picker/index.ts +4 -0
- package/src/components/picker/mark-picker.svelte +84 -0
- package/src/components/picker/picker.svelte +143 -0
- package/src/components/picker/trigger.ts +42 -0
- package/src/components/picker/types.ts +26 -0
- package/src/components/segmented/animated-tabs.svelte +175 -0
- package/src/components/segmented/collapse.test.ts +49 -0
- package/src/components/segmented/collapse.ts +39 -0
- package/src/components/segmented/index.ts +41 -0
- package/src/components/segmented/section-runs.svelte +79 -0
- package/src/components/segmented/segment.svelte +106 -0
- package/src/components/segmented/segmented-filter.svelte +125 -0
- package/src/components/segmented/segmented-nav.svelte +82 -0
- package/src/components/segmented/segmented-overflow.svelte +85 -0
- package/src/components/segmented/segmented-track.svelte +181 -0
- package/src/components/segmented/segmented.test.ts +20 -0
- package/src/components/segmented/segmented.ts +80 -0
- package/src/components/star-rating-input/index.ts +9 -0
- package/src/components/star-rating-input/star-rating-input.svelte +256 -0
- package/src/components/svg/index.ts +1 -0
- package/src/components/svg/svg.svelte +9 -0
- package/src/effects/border-beam/BorderBeam.svelte +1 -1
- package/src/effects/image-comparison/ImageComparison.css +42 -8
- package/src/effects/image-comparison/ImageComparison.svelte +138 -21
- package/src/effects/image-comparison/compare.test.ts +17 -0
- package/src/effects/image-comparison/compare.ts +5 -0
- package/src/effects/interactive-hover-button/InteractiveHoverButton.svelte +338 -0
- package/src/effects/magic-card/MagicCard.css +47 -0
- package/src/effects/magic-card/MagicCard.svelte +57 -0
- package/src/effects/marquee/Marquee.css +27 -1
- package/src/effects/marquee/Marquee.svelte +67 -3
- package/src/effects/reveal/index.ts +2 -1
- package/src/effects/reveal/observe.test.ts +60 -0
- package/src/effects/reveal/observe.ts +33 -1
- package/src/effects/reveal/types.ts +7 -0
- package/src/effects/theme-toggle/ThemeToggle.css +199 -6
- package/src/effects/theme-toggle/ThemeToggle.svelte +51 -26
- package/src/gallery.test.ts +52 -0
- package/src/index.ts +5 -1
- package/src/lib/panel.ts +7 -1
- package/src/lib/theme-transition.ts +37 -11
- package/src/lib/tone.ts +48 -0
- package/src/shadcn/accordion/accordion-trigger.svelte +15 -6
- package/src/shadcn/alert/alert.svelte +3 -1
- package/src/shadcn/badge/badge.svelte +8 -7
- package/src/shadcn/breadcrumb/breadcrumb-ellipsis.svelte +2 -2
- package/src/shadcn/breadcrumb/breadcrumb-separator.svelte +2 -2
- package/src/shadcn/calendar/calendar-month-select.svelte +2 -2
- package/src/shadcn/calendar/calendar-next-button.svelte +2 -2
- package/src/shadcn/calendar/calendar-prev-button.svelte +2 -2
- package/src/shadcn/calendar/calendar-year-select.svelte +2 -2
- package/src/shadcn/calendar/calendar.svelte +6 -0
- package/src/shadcn/code-block/code-block-copy-button.svelte +6 -55
- package/src/shadcn/command/index.ts +3 -0
- package/src/shadcn/field/index.ts +3 -0
- package/src/shadcn/field/simple-field.svelte +71 -0
- package/src/shadcn/field/simple-field.test.ts +47 -0
- package/src/shadcn/message-scroller/message-scroller-button.svelte +2 -2
- package/src/shadcn/radio-group/radio-group-item.svelte +3 -2
- package/src/shadcn/sheet/sheet-content.svelte +1 -0
- package/src/shadcn/sheet/sheet.css +48 -0
- package/src/shadcn/sonner/sonner.svelte +7 -10
- package/src/shadcn/toggle-group/toggle-group.svelte +10 -3
- package/src/shadcn/trace/trace-collapsible-trigger.svelte +2 -2
- package/src/ai/composer/Composer.css +0 -108
- package/src/ai/composer/Composer.svelte +0 -111
- package/src/ai/composer/index.ts +0 -10
- package/src/animations/accordion.css +0 -58
- package/src/animations/icon-swap.css +0 -43
- package/src/animations/notification-badge.css +0 -81
- package/src/animations/panel.css +0 -86
- package/src/animations/reasoning-stream.css +0 -49
- package/src/animations/skeleton-reveal.css +0 -97
- package/src/animations/streaming-text.css +0 -31
- package/src/animations/tabs-sliding.css +0 -83
- package/src/animations/texts-reveal.css +0 -70
- package/src/animations/tooltip.css +0 -72
- package/src/animations/view-transition-reveal.ts +0 -74
- package/src/lib/utils.ts +0 -45
- package/src/showcase/AiSection.svelte +0 -150
- package/src/showcase/AnimationsSection.svelte +0 -1658
- package/src/showcase/EffectsSection.svelte +0 -152
- package/src/showcase/ShadcnSection.svelte +0 -27
- package/src/showcase/ShowcaseImport.svelte +0 -126
- package/src/showcase/ShowcaseList.svelte +0 -132
- package/src/showcase/ShowcaseNav.svelte +0 -247
- package/src/showcase/ShowcaseSection.svelte +0 -112
- package/src/showcase/ShowcaseShell.svelte +0 -55
- package/src/showcase/ShowcaseStage.svelte +0 -342
- package/src/showcase/ShowcaseVariant.svelte +0 -109
- package/src/showcase/assets/image-reveal/art-1.jpg +0 -0
- package/src/showcase/assets/image-reveal/art-2.jpg +0 -0
- package/src/showcase/assets/image-reveal/art-3.jpg +0 -0
- package/src/showcase/check.svg +0 -1
- package/src/showcase/chevron-arms.svg +0 -2
- package/src/showcase/chevron-down.svg +0 -1
- package/src/showcase/copy.svg +0 -2
- package/src/showcase/demos/ai/confirmation.svelte +0 -59
- package/src/showcase/demos/ai/loading-state.svelte +0 -19
- package/src/showcase/demos/ai/prompt-input.svelte +0 -57
- package/src/showcase/demos/ai/reasoning.svelte +0 -19
- package/src/showcase/demos/ai/thinking-dots.svelte +0 -15
- package/src/showcase/demos/ai/voice-waveform.svelte +0 -24
- package/src/showcase/demos/effects/animated-beam.svelte +0 -50
- package/src/showcase/demos/effects/animated-group.svelte +0 -47
- package/src/showcase/demos/effects/border-beam-default.svelte +0 -20
- package/src/showcase/demos/effects/border-beam-line.svelte +0 -20
- package/src/showcase/demos/effects/border-beam-mono.svelte +0 -20
- package/src/showcase/demos/effects/border-beam-sunset.svelte +0 -20
- package/src/showcase/demos/effects/image-comparison.svelte +0 -25
- package/src/showcase/demos/effects/image-reveal.svelte +0 -49
- package/src/showcase/demos/effects/in-view.svelte +0 -41
- package/src/showcase/demos/effects/integrations-hub.svelte +0 -82
- package/src/showcase/demos/effects/magnetic.svelte +0 -31
- package/src/showcase/demos/effects/marquee.svelte +0 -33
- package/src/showcase/demos/effects/progressive-blur.svelte +0 -47
- package/src/showcase/demos/effects/spotlight.svelte +0 -40
- package/src/showcase/demos/effects/text-effect.svelte +0 -55
- package/src/showcase/demos/shadcn/accordion.svelte +0 -45
- package/src/showcase/demos/shadcn/alert-dialog.svelte +0 -23
- package/src/showcase/demos/shadcn/alert.svelte +0 -30
- package/src/showcase/demos/shadcn/attachment.svelte +0 -71
- package/src/showcase/demos/shadcn/avatar.svelte +0 -30
- package/src/showcase/demos/shadcn/badge.svelte +0 -26
- package/src/showcase/demos/shadcn/breadcrumb.svelte +0 -34
- package/src/showcase/demos/shadcn/bubble.svelte +0 -36
- package/src/showcase/demos/shadcn/button.svelte +0 -11
- package/src/showcase/demos/shadcn/calendar.svelte +0 -8
- package/src/showcase/demos/shadcn/card.svelte +0 -39
- package/src/showcase/demos/shadcn/checkbox.svelte +0 -39
- package/src/showcase/demos/shadcn/code-block.svelte +0 -80
- package/src/showcase/demos/shadcn/collapsible.svelte +0 -20
- package/src/showcase/demos/shadcn/command.svelte +0 -48
- package/src/showcase/demos/shadcn/context-menu.svelte +0 -54
- package/src/showcase/demos/shadcn/demo-harness.svelte +0 -12
- package/src/showcase/demos/shadcn/demos.test.ts +0 -20
- package/src/showcase/demos/shadcn/dialog.svelte +0 -38
- package/src/showcase/demos/shadcn/dropdown-menu.svelte +0 -59
- package/src/showcase/demos/shadcn/empty.svelte +0 -29
- package/src/showcase/demos/shadcn/field.svelte +0 -115
- package/src/showcase/demos/shadcn/hover-card.svelte +0 -32
- package/src/showcase/demos/shadcn/index.ts +0 -382
- package/src/showcase/demos/shadcn/input-otp.svelte +0 -19
- package/src/showcase/demos/shadcn/input.svelte +0 -5
- package/src/showcase/demos/shadcn/label.svelte +0 -11
- package/src/showcase/demos/shadcn/marker.svelte +0 -30
- package/src/showcase/demos/shadcn/message-scroller.svelte +0 -44
- package/src/showcase/demos/shadcn/message.svelte +0 -75
- package/src/showcase/demos/shadcn/popover.svelte +0 -36
- package/src/showcase/demos/shadcn/progress.svelte +0 -13
- package/src/showcase/demos/shadcn/radio-group.svelte +0 -19
- package/src/showcase/demos/shadcn/scroll-area.svelte +0 -18
- package/src/showcase/demos/shadcn/select.svelte +0 -29
- package/src/showcase/demos/shadcn/separator.svelte +0 -12
- package/src/showcase/demos/shadcn/sheet.svelte +0 -48
- package/src/showcase/demos/shadcn/skeleton.svelte +0 -11
- package/src/showcase/demos/shadcn/slider.svelte +0 -7
- package/src/showcase/demos/shadcn/sonner.svelte +0 -18
- package/src/showcase/demos/shadcn/spinner.svelte +0 -11
- package/src/showcase/demos/shadcn/switch.svelte +0 -9
- package/src/showcase/demos/shadcn/table.svelte +0 -76
- package/src/showcase/demos/shadcn/tabs.svelte +0 -62
- package/src/showcase/demos/shadcn/textarea.svelte +0 -5
- package/src/showcase/demos/shadcn/toggle-group.svelte +0 -18
- package/src/showcase/demos/shadcn/toggle.svelte +0 -14
- package/src/showcase/demos/shadcn/tooltip.svelte +0 -13
- package/src/showcase/demos/shadcn/trace.svelte +0 -51
- package/src/showcase/heart.svg +0 -1
- package/src/showcase/index.ts +0 -38
- package/src/showcase/kit.test.ts +0 -72
- package/src/showcase/kit.ts +0 -16
- package/src/showcase/plus.svg +0 -1
- package/src/showcase/showcase-shell.css +0 -91
- package/src/showcase/stacked-section.css +0 -15
- package/src/showcase/success-check.svg +0 -1
- package/src/showcase/types.ts +0 -29
- package/src/svg-raw.d.ts +0 -7
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// =========================
|
|
3
|
+
// IMPORTS
|
|
4
|
+
// =========================
|
|
5
|
+
import type { Snippet } from "svelte";
|
|
6
|
+
import { getContext } from "svelte";
|
|
7
|
+
import { cn } from "@heyramzi/utils";
|
|
8
|
+
import { MASTER_DETAIL_PANE, type MasterDetailPane } from "./queue-rail.js";
|
|
9
|
+
|
|
10
|
+
// =========================
|
|
11
|
+
// TYPES
|
|
12
|
+
// =========================
|
|
13
|
+
type Props = {
|
|
14
|
+
// One row shape for every queue rail, in a fixed order: identity, preview,
|
|
15
|
+
// facts. Three pages used to draw three different rows.
|
|
16
|
+
title: string;
|
|
17
|
+
selected?: boolean;
|
|
18
|
+
onclick: () => void;
|
|
19
|
+
// A thumbnail or avatar ahead of the title.
|
|
20
|
+
leading?: Snippet;
|
|
21
|
+
// The record's state, at the end of the preview line. Never beside the title,
|
|
22
|
+
// where it competes with the name.
|
|
23
|
+
trailing?: Snippet;
|
|
24
|
+
// One line of the record's own words, under the title.
|
|
25
|
+
preview?: string | null;
|
|
26
|
+
// Dot-separated facts under the preview. Falsy entries drop out.
|
|
27
|
+
meta?: Array<string | null | undefined | false>;
|
|
28
|
+
// Which fact gives up its width first. The others hold their full text, so
|
|
29
|
+
// a date or a count never truncates to nonsense.
|
|
30
|
+
metaTruncate?: "first" | "last";
|
|
31
|
+
// Icons that qualify the facts (a waiting reply, a removed comment).
|
|
32
|
+
metaTrail?: Snippet;
|
|
33
|
+
// Controls that appear on hover, over the row's top-right corner.
|
|
34
|
+
actions?: Snippet;
|
|
35
|
+
// Long records (thread titles) need two lines to be recognisable.
|
|
36
|
+
titleLines?: 1 | 2;
|
|
37
|
+
class?: string;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// =========================
|
|
41
|
+
// PROPS
|
|
42
|
+
// =========================
|
|
43
|
+
let {
|
|
44
|
+
title,
|
|
45
|
+
selected = false,
|
|
46
|
+
onclick,
|
|
47
|
+
leading,
|
|
48
|
+
trailing,
|
|
49
|
+
preview,
|
|
50
|
+
meta,
|
|
51
|
+
metaTruncate = "first",
|
|
52
|
+
metaTrail,
|
|
53
|
+
actions,
|
|
54
|
+
titleLines = 1,
|
|
55
|
+
class: className,
|
|
56
|
+
}: Props = $props();
|
|
57
|
+
|
|
58
|
+
// =========================
|
|
59
|
+
// DERIVED
|
|
60
|
+
// =========================
|
|
61
|
+
let facts = $derived((meta ?? []).filter((item): item is string => Boolean(item)));
|
|
62
|
+
|
|
63
|
+
// =========================
|
|
64
|
+
// FUNCTIONS
|
|
65
|
+
// =========================
|
|
66
|
+
// Picking a row moves a phone from list to record. `undefined` outside a
|
|
67
|
+
// `MasterDetail`, where a rail is just a list.
|
|
68
|
+
let row = $state<HTMLDivElement>();
|
|
69
|
+
|
|
70
|
+
// A row picked from a link, a shortcut or the next arrow can sit below the fold.
|
|
71
|
+
$effect(() => {
|
|
72
|
+
if (selected) row?.scrollIntoView({ block: "nearest" });
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
let pane = getContext<MasterDetailPane | undefined>(MASTER_DETAIL_PANE);
|
|
76
|
+
|
|
77
|
+
function select() {
|
|
78
|
+
onclick();
|
|
79
|
+
pane?.open();
|
|
80
|
+
}
|
|
81
|
+
</script>
|
|
82
|
+
|
|
83
|
+
<!-- =========================== -->
|
|
84
|
+
<!-- MARKUP -->
|
|
85
|
+
<!-- =========================== -->
|
|
86
|
+
<!-- The hairline belongs to the row, so grouped and flat rails draw it the same.
|
|
87
|
+
`first:border-t-0` keeps each group's top row clean under its header. -->
|
|
88
|
+
<div
|
|
89
|
+
bind:this={row}
|
|
90
|
+
class={cn(
|
|
91
|
+
// `group/row` too: `CopyHandoff` waits for `group-hover/row`, and a bare
|
|
92
|
+
// `group` left it invisible on every rail.
|
|
93
|
+
"group group/row relative border-t border-border first:border-t-0",
|
|
94
|
+
selected ? "bg-accent" : "hover:bg-muted/50",
|
|
95
|
+
className,
|
|
96
|
+
)}
|
|
97
|
+
>
|
|
98
|
+
<button
|
|
99
|
+
type="button"
|
|
100
|
+
class={cn(
|
|
101
|
+
"t-press flex w-full items-start gap-3 px-4 py-3 text-left any-pointer-coarse:py-3.5",
|
|
102
|
+
"focus-visible:outline-none focus-visible:ring-3 focus-visible:ring-ring/50 focus-visible:ring-inset",
|
|
103
|
+
// WHY: the hover actions sit over this corner, so the text stops short
|
|
104
|
+
// of them rather than sliding underneath when a title is long.
|
|
105
|
+
actions && "pr-24",
|
|
106
|
+
)}
|
|
107
|
+
onclick={select}
|
|
108
|
+
aria-current={selected ? "page" : undefined}
|
|
109
|
+
>
|
|
110
|
+
{#if leading}
|
|
111
|
+
<span class="shrink-0">{@render leading()}</span>
|
|
112
|
+
{/if}
|
|
113
|
+
<!-- The title owns its column. A chip beside it took a third of the width
|
|
114
|
+
and clamped two-line titles. -->
|
|
115
|
+
<span class="flex min-w-0 flex-1 flex-col gap-1">
|
|
116
|
+
<span
|
|
117
|
+
class={cn(
|
|
118
|
+
"text-sm font-medium leading-snug",
|
|
119
|
+
titleLines === 2 ? "line-clamp-2" : "truncate",
|
|
120
|
+
selected && "text-accent-foreground",
|
|
121
|
+
)}
|
|
122
|
+
>
|
|
123
|
+
{title}
|
|
124
|
+
</span>
|
|
125
|
+
<!-- The state rides the preview, not the facts. Facts are `shrink-0`, so
|
|
126
|
+
at 280px the chip got pushed off the row. Prose can give up width. -->
|
|
127
|
+
{#if preview || trailing}
|
|
128
|
+
<span class="flex min-w-0 items-center text-xs text-muted-foreground">
|
|
129
|
+
{#if preview}
|
|
130
|
+
<span class="truncate">{preview}</span>
|
|
131
|
+
{/if}
|
|
132
|
+
{#if trailing}
|
|
133
|
+
<span class="ml-auto flex shrink-0 items-center gap-1.5 pl-2">{@render trailing()}</span>
|
|
134
|
+
{/if}
|
|
135
|
+
</span>
|
|
136
|
+
{/if}
|
|
137
|
+
{#if facts.length > 0 || metaTrail}
|
|
138
|
+
<span class="flex min-w-0 items-center text-xs text-muted-foreground">
|
|
139
|
+
{#each facts as fact, i (i)}
|
|
140
|
+
{#if i > 0}
|
|
141
|
+
<span aria-hidden="true" class="mx-1.5 shrink-0 opacity-40">·</span>
|
|
142
|
+
{/if}
|
|
143
|
+
{@const flexible =
|
|
144
|
+
facts.length > 1 && i === (metaTruncate === "last" ? facts.length - 1 : 0)}
|
|
145
|
+
<span class={flexible ? "truncate" : "shrink-0"}>{fact}</span>
|
|
146
|
+
{/each}
|
|
147
|
+
{#if metaTrail}
|
|
148
|
+
<span class="ml-1.5 flex shrink-0 items-center gap-1">{@render metaTrail()}</span>
|
|
149
|
+
{/if}
|
|
150
|
+
</span>
|
|
151
|
+
{/if}
|
|
152
|
+
</span>
|
|
153
|
+
</button>
|
|
154
|
+
{#if actions}
|
|
155
|
+
<!-- WHY visible on a coarse pointer: there is no hover on a touchscreen, so
|
|
156
|
+
the row's actions would be unreachable there. -->
|
|
157
|
+
<div
|
|
158
|
+
class="absolute right-2 top-2 hidden items-center gap-1 group-hover:flex group-focus-within:flex any-pointer-coarse:flex"
|
|
159
|
+
>
|
|
160
|
+
{@render actions()}
|
|
161
|
+
</div>
|
|
162
|
+
{/if}
|
|
163
|
+
</div>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { getContext, setContext } from "svelte";
|
|
2
|
+
import type { SectionStrip } from "../segmented/section-runs.svelte";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* What an app shell gives a list page so the page's toolbar can draw the
|
|
6
|
+
* section strip. `strip` must be reactive (a `$state` field), and `claim` tells
|
|
7
|
+
* the shell a toolbar is drawing it and returns the release.
|
|
8
|
+
*/
|
|
9
|
+
export interface SectionHost {
|
|
10
|
+
readonly strip: SectionStrip | null;
|
|
11
|
+
claim(): () => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// WHY context: the shell decides the strip once and ninety pages draw it, so a prop on every page would be ninety edits.
|
|
15
|
+
const KEY = Symbol("list-page.section-host");
|
|
16
|
+
|
|
17
|
+
/** Call once in the layout that owns the section strip. */
|
|
18
|
+
export function setSectionHost(host: SectionHost): void {
|
|
19
|
+
setContext(KEY, host);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function getSectionHost(): SectionHost | undefined {
|
|
23
|
+
return getContext<SectionHost | undefined>(KEY);
|
|
24
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// =========================
|
|
3
|
+
// IMPORTS
|
|
4
|
+
// =========================
|
|
5
|
+
import type { Snippet } from "svelte";
|
|
6
|
+
import { cn } from "@heyramzi/utils";
|
|
7
|
+
import type { PhosphorIcon } from "../../lib/icon-types.js";
|
|
8
|
+
|
|
9
|
+
// =========================
|
|
10
|
+
// TYPES
|
|
11
|
+
// =========================
|
|
12
|
+
type Props = {
|
|
13
|
+
// The one label style for panel, rail group and sub-section headers. Retyped
|
|
14
|
+
// copies drifted to a different tracking value.
|
|
15
|
+
icon?: PhosphorIcon;
|
|
16
|
+
children: Snippet;
|
|
17
|
+
class?: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// =========================
|
|
21
|
+
// PROPS
|
|
22
|
+
// =========================
|
|
23
|
+
let { icon: Icon, children, class: className }: Props = $props();
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
<!-- =========================== -->
|
|
27
|
+
<!-- MARKUP -->
|
|
28
|
+
<!-- =========================== -->
|
|
29
|
+
<span
|
|
30
|
+
class={cn(
|
|
31
|
+
"flex items-center gap-2 text-xs font-semibold uppercase tracking-[0.14em] text-muted-foreground",
|
|
32
|
+
className,
|
|
33
|
+
)}
|
|
34
|
+
>
|
|
35
|
+
{#if Icon}
|
|
36
|
+
<Icon class="size-4 text-brand-indigo" aria-hidden="true" />
|
|
37
|
+
{/if}
|
|
38
|
+
{@render children()}
|
|
39
|
+
</span>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { describe, expect, it } from "vite-plus/test";
|
|
2
|
+
import { stepTo } from "./step.js";
|
|
3
|
+
|
|
4
|
+
describe("stepTo", () => {
|
|
5
|
+
const ids = ["a", "b", "c"];
|
|
6
|
+
|
|
7
|
+
it("steps both ways from the middle", () => {
|
|
8
|
+
expect(stepTo(ids, "b", -1)).toBe("a");
|
|
9
|
+
expect(stepTo(ids, "b", 1)).toBe("c");
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("stops at either end", () => {
|
|
13
|
+
expect(stepTo(ids, "a", -1)).toBe(null);
|
|
14
|
+
expect(stepTo(ids, "c", 1)).toBe(null);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("goes nowhere without a listed selection", () => {
|
|
18
|
+
expect(stepTo(ids, null, 1)).toBe(null);
|
|
19
|
+
expect(stepTo(ids, "z", 1)).toBe(null);
|
|
20
|
+
});
|
|
21
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** The id `by` rows away from `current`, or null at either end or when `current` isn't listed. */
|
|
2
|
+
export function stepTo(
|
|
3
|
+
ids: readonly string[],
|
|
4
|
+
current: string | null | undefined,
|
|
5
|
+
by: 1 | -1,
|
|
6
|
+
): string | null {
|
|
7
|
+
const index = current ? ids.indexOf(current) : -1;
|
|
8
|
+
if (index < 0) return null;
|
|
9
|
+
return ids[index + by] ?? null;
|
|
10
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// =========================
|
|
3
|
+
// IMPORTS
|
|
4
|
+
// =========================
|
|
5
|
+
import { Input } from "../../shadcn/input/index.js";
|
|
6
|
+
import { MagnifyingGlass } from "phosphor-svelte";
|
|
7
|
+
import { cn } from "@heyramzi/utils";
|
|
8
|
+
|
|
9
|
+
// =========================
|
|
10
|
+
// TYPES
|
|
11
|
+
// =========================
|
|
12
|
+
type Props = {
|
|
13
|
+
value: string;
|
|
14
|
+
placeholder?: string;
|
|
15
|
+
size?: "sm" | "default";
|
|
16
|
+
class?: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// =========================
|
|
20
|
+
// PROPS
|
|
21
|
+
// =========================
|
|
22
|
+
let {
|
|
23
|
+
value = $bindable(""),
|
|
24
|
+
placeholder = "Search...",
|
|
25
|
+
size = "default",
|
|
26
|
+
class: extraClass,
|
|
27
|
+
}: Props = $props();
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
<!-- =========================== -->
|
|
31
|
+
<!-- MARKUP -->
|
|
32
|
+
<!-- =========================== -->
|
|
33
|
+
<!-- WHY: leading magnifying glass + a fixed height so every list page's toolbar
|
|
34
|
+
keeps the same rhythm; size="sm" matches the compact filter chips and
|
|
35
|
+
tabs used on dense list pages. -->
|
|
36
|
+
<!-- Drops the caller's desk width once the toolbar row breaks, read from the
|
|
37
|
+
shell's container. Outside the shell the caller's width stands. -->
|
|
38
|
+
<div
|
|
39
|
+
class={cn(
|
|
40
|
+
"relative",
|
|
41
|
+
extraClass,
|
|
42
|
+
"@max-3xl/toolbar:w-auto @max-3xl/toolbar:min-w-0 @max-3xl/toolbar:flex-1"
|
|
43
|
+
)}
|
|
44
|
+
>
|
|
45
|
+
<MagnifyingGlass
|
|
46
|
+
class={cn(
|
|
47
|
+
"pointer-events-none absolute top-1/2 -translate-y-1/2 text-muted-foreground",
|
|
48
|
+
size === "sm" ? "left-2 size-3" : "left-2 size-3",
|
|
49
|
+
)}
|
|
50
|
+
aria-hidden="true"
|
|
51
|
+
/>
|
|
52
|
+
<!-- Keyed to the pointer, not the viewport. On an iPad, 12px text makes
|
|
53
|
+
Safari zoom on focus and never zoom back. -->
|
|
54
|
+
<Input
|
|
55
|
+
bind:value
|
|
56
|
+
{placeholder}
|
|
57
|
+
class={cn(
|
|
58
|
+
"w-full",
|
|
59
|
+
size === "sm" ? "h-7 pl-7 pr-2.5 text-xs" : "h-7 pl-7 pr-2.5 text-xs",
|
|
60
|
+
"any-pointer-coarse:h-10 any-pointer-coarse:text-base",
|
|
61
|
+
)}
|
|
62
|
+
/>
|
|
63
|
+
</div>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { cn } from "@heyramzi/utils";
|
|
2
|
+
|
|
3
|
+
type ToolbarTriggerOpts = {
|
|
4
|
+
active?: boolean;
|
|
5
|
+
className?: string;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
// The trigger class for every toolbar control. Coarse pointers get a taller
|
|
9
|
+
// box at the same type size, since the chip is too small for a thumb.
|
|
10
|
+
|
|
11
|
+
// One fixed height, matching the `sm` button and the segmented track. A second
|
|
12
|
+
// size put a 24px filter beside 28px controls on the calendar (14 Oct 2026).
|
|
13
|
+
export function toolbarTriggerClass({
|
|
14
|
+
active = false,
|
|
15
|
+
className,
|
|
16
|
+
}: ToolbarTriggerOpts = {}): string {
|
|
17
|
+
return cn(
|
|
18
|
+
"inline-flex cursor-pointer items-center gap-1.5 rounded-lg border border-border font-medium transition-colors active:translate-y-px",
|
|
19
|
+
"h-7 px-2.5 text-xs any-pointer-coarse:h-10 any-pointer-coarse:px-3",
|
|
20
|
+
active ? "bg-muted text-foreground" : "text-muted-foreground hover:text-foreground",
|
|
21
|
+
className,
|
|
22
|
+
);
|
|
23
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type ViewMode = "list" | "board" | "grid";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as LocaleSwitch } from "./locale-switch.svelte";
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// =========================
|
|
3
|
+
// IMPORTS
|
|
4
|
+
// =========================
|
|
5
|
+
import { SegmentedFilter, type TabDef } from "../segmented/index.js";
|
|
6
|
+
|
|
7
|
+
// =========================
|
|
8
|
+
// TYPES
|
|
9
|
+
// =========================
|
|
10
|
+
type LocaleOption = {
|
|
11
|
+
value: string;
|
|
12
|
+
label: string;
|
|
13
|
+
flag: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
type Props = {
|
|
17
|
+
// Wraps `SegmentedFilter` so it shares one track with every other strip. It
|
|
18
|
+
// only adds each language's flag.
|
|
19
|
+
value: string;
|
|
20
|
+
onchange: (value: string) => void;
|
|
21
|
+
options?: LocaleOption[];
|
|
22
|
+
/** Names the control for a screen reader. Without it the group is two bare letters. */
|
|
23
|
+
ariaLabel?: string;
|
|
24
|
+
class?: string;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// =========================
|
|
28
|
+
// PROPS
|
|
29
|
+
// =========================
|
|
30
|
+
let {
|
|
31
|
+
value,
|
|
32
|
+
onchange,
|
|
33
|
+
options = [
|
|
34
|
+
{ value: "en", label: "EN", flag: "/flags/US.svg" },
|
|
35
|
+
{ value: "fr", label: "FR", flag: "/flags/FR.svg" },
|
|
36
|
+
],
|
|
37
|
+
ariaLabel = "Language",
|
|
38
|
+
class: className,
|
|
39
|
+
}: Props = $props();
|
|
40
|
+
|
|
41
|
+
// =========================
|
|
42
|
+
// DERIVED
|
|
43
|
+
// =========================
|
|
44
|
+
let segments = $derived<TabDef<string>[]>(
|
|
45
|
+
options.map((option) => ({ value: option.value, label: option.label, image: option.flag })),
|
|
46
|
+
);
|
|
47
|
+
</script>
|
|
48
|
+
|
|
49
|
+
<!-- =========================== -->
|
|
50
|
+
<!-- MARKUP -->
|
|
51
|
+
<!-- =========================== -->
|
|
52
|
+
<SegmentedFilter
|
|
53
|
+
{value}
|
|
54
|
+
onValueChange={onchange}
|
|
55
|
+
options={segments}
|
|
56
|
+
block={false}
|
|
57
|
+
{ariaLabel}
|
|
58
|
+
class={className}
|
|
59
|
+
/>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as Mark } from "./mark.svelte";
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// =========================
|
|
3
|
+
// IMPORTS
|
|
4
|
+
// =========================
|
|
5
|
+
import { cn } from "@heyramzi/utils";
|
|
6
|
+
|
|
7
|
+
// =========================
|
|
8
|
+
// TYPES
|
|
9
|
+
// =========================
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* A brand's mark at glyph size: a kit icon, a channel avatar, a client logo.
|
|
13
|
+
* One renderer, because three places each handled a missing file their own way.
|
|
14
|
+
*/
|
|
15
|
+
type Props = {
|
|
16
|
+
/** The file. A missing or broken one falls back to the label's initial. */
|
|
17
|
+
src?: string;
|
|
18
|
+
/** Read for the fallback letter. The mark itself is decorative. */
|
|
19
|
+
label: string;
|
|
20
|
+
/** `circle` crops a photograph to a disc, `square` fits a logo whole. */
|
|
21
|
+
shape?: "square" | "circle";
|
|
22
|
+
class?: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// =========================
|
|
26
|
+
// PROPS
|
|
27
|
+
// =========================
|
|
28
|
+
let { src, label, shape = "square", class: className }: Props = $props();
|
|
29
|
+
|
|
30
|
+
// =========================
|
|
31
|
+
// STATE
|
|
32
|
+
// =========================
|
|
33
|
+
/** The src that failed, so a later one gets its own chance to load. */
|
|
34
|
+
let broken = $state("");
|
|
35
|
+
|
|
36
|
+
// =========================
|
|
37
|
+
// DERIVED
|
|
38
|
+
// =========================
|
|
39
|
+
let round = $derived(shape === "circle");
|
|
40
|
+
</script>
|
|
41
|
+
|
|
42
|
+
<!-- =========================== -->
|
|
43
|
+
<!-- MARKUP -->
|
|
44
|
+
<!-- =========================== -->
|
|
45
|
+
<!-- WHY the letter is a fallback and not a backdrop: a tile drawn UNDER the mark
|
|
46
|
+
shows its initial through every logo that is not a full-bleed square. -->
|
|
47
|
+
{#if src && broken !== src}
|
|
48
|
+
<img
|
|
49
|
+
{src}
|
|
50
|
+
alt=""
|
|
51
|
+
loading="lazy"
|
|
52
|
+
referrerpolicy="no-referrer"
|
|
53
|
+
class={cn(
|
|
54
|
+
"size-4 shrink-0",
|
|
55
|
+
round ? "rounded-full object-cover" : "object-contain",
|
|
56
|
+
className,
|
|
57
|
+
)}
|
|
58
|
+
onerror={() => (broken = src)}
|
|
59
|
+
/>
|
|
60
|
+
{:else}
|
|
61
|
+
<span
|
|
62
|
+
class={cn(
|
|
63
|
+
"bg-muted text-muted-foreground flex size-4 shrink-0 items-center justify-center text-[10px] leading-none font-bold",
|
|
64
|
+
round ? "rounded-full" : "rounded-xs",
|
|
65
|
+
className,
|
|
66
|
+
)}
|
|
67
|
+
>
|
|
68
|
+
{label.charAt(0)}
|
|
69
|
+
</span>
|
|
70
|
+
{/if}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
import type { Component } from "svelte";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The one way out to a tool: mark, name, go-to arrow, never the verb, which
|
|
6
|
+
* survives as the accessible name.
|
|
7
|
+
*/
|
|
8
|
+
export interface OpenInTool {
|
|
9
|
+
/** The visible text, and the word the tool calls itself. */
|
|
10
|
+
name: string;
|
|
11
|
+
/** The default accessible sentence, when the caller passes no translated one. */
|
|
12
|
+
action: string;
|
|
13
|
+
icon: Component<{ size?: number; class?: string }>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** `chip` is the bordered inline tag, `button` sits in a header beside other
|
|
17
|
+
* buttons, `icon` is the bare mark for a dense row. */
|
|
18
|
+
export type OpenInVariant = "chip" | "button" | "icon";
|
|
19
|
+
export type OpenInSize = "sm" | "default";
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<script lang="ts">
|
|
23
|
+
// =========================
|
|
24
|
+
// IMPORTS
|
|
25
|
+
// =========================
|
|
26
|
+
import { ArrowSquareOut } from "phosphor-svelte";
|
|
27
|
+
import { cn } from "@heyramzi/utils";
|
|
28
|
+
import { buttonVariants } from "../../shadcn/button/index.js";
|
|
29
|
+
|
|
30
|
+
// =========================
|
|
31
|
+
// TYPES
|
|
32
|
+
// =========================
|
|
33
|
+
interface Props {
|
|
34
|
+
/** The tool to link to: its name, accessible sentence and mark. */
|
|
35
|
+
tool: OpenInTool;
|
|
36
|
+
href: string;
|
|
37
|
+
variant?: OpenInVariant;
|
|
38
|
+
size?: OpenInSize;
|
|
39
|
+
/** Overrides the visible text. The accessible name is `title`, never this. */
|
|
40
|
+
label?: string;
|
|
41
|
+
/** The accessible sentence, translated by the caller. */
|
|
42
|
+
title?: string;
|
|
43
|
+
class?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// =========================
|
|
47
|
+
// PROPS
|
|
48
|
+
// =========================
|
|
49
|
+
let {
|
|
50
|
+
tool,
|
|
51
|
+
href,
|
|
52
|
+
variant = "chip",
|
|
53
|
+
size = "default",
|
|
54
|
+
label,
|
|
55
|
+
title,
|
|
56
|
+
class: className,
|
|
57
|
+
}: Props = $props();
|
|
58
|
+
|
|
59
|
+
// =========================
|
|
60
|
+
// DERIVED
|
|
61
|
+
// =========================
|
|
62
|
+
let Icon = $derived(tool.icon);
|
|
63
|
+
let iconSize = $derived(size === "sm" ? 12 : 14);
|
|
64
|
+
let action = $derived(title ?? tool.action);
|
|
65
|
+
let text = $derived(label ?? tool.name);
|
|
66
|
+
</script>
|
|
67
|
+
|
|
68
|
+
<!-- =========================== -->
|
|
69
|
+
<!-- MARKUP -->
|
|
70
|
+
<!-- =========================== -->
|
|
71
|
+
<!-- The click stops here because these usually sit in a clickable row, and a
|
|
72
|
+
call site that forgot would open the row as well. -->
|
|
73
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
74
|
+
<div
|
|
75
|
+
class="inline-flex shrink-0"
|
|
76
|
+
onclick={(e) => e.stopPropagation()}
|
|
77
|
+
onkeydown={(e) => e.stopPropagation()}
|
|
78
|
+
>
|
|
79
|
+
<a
|
|
80
|
+
{href}
|
|
81
|
+
target="_blank"
|
|
82
|
+
rel="noopener noreferrer"
|
|
83
|
+
aria-label={action}
|
|
84
|
+
title={action}
|
|
85
|
+
class={cn(
|
|
86
|
+
variant === "button"
|
|
87
|
+
? buttonVariants({ variant: "outline", size: size === "sm" ? "sm" : "default" })
|
|
88
|
+
: variant === "icon"
|
|
89
|
+
? "inline-flex shrink-0 items-center justify-center rounded text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
|
|
90
|
+
: cn(
|
|
91
|
+
"inline-flex shrink-0 items-center gap-1.5 whitespace-nowrap rounded-md border border-border text-muted-foreground transition-colors hover:bg-muted hover:text-foreground",
|
|
92
|
+
size === "sm" ? "px-1.5 py-0.5 text-[11px]" : "px-2 py-1 text-xs",
|
|
93
|
+
),
|
|
94
|
+
variant === "icon" && (size === "sm" ? "size-5" : "size-6"),
|
|
95
|
+
className,
|
|
96
|
+
)}
|
|
97
|
+
>
|
|
98
|
+
<Icon size={iconSize} />
|
|
99
|
+
{#if variant !== "icon"}
|
|
100
|
+
{text}
|
|
101
|
+
<!-- The one thing the mark can't say: this leaves the app. It stays muted
|
|
102
|
+
so the eye lands on the tool name first. -->
|
|
103
|
+
<ArrowSquareOut size={iconSize} class="text-muted-foreground" />
|
|
104
|
+
{/if}
|
|
105
|
+
</a>
|
|
106
|
+
</div>
|