@heyramzi/ui 6.1.0 → 8.0.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 +32 -40
- package/README.md +10 -4
- package/package.json +31 -29
- package/scripts/sync-shadcn.mjs +29 -16
- package/scripts/sync-transitions.mjs +11 -11
- 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 -24
- 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 +17 -17
- 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/sliding-pill.ts +68 -0
- package/src/lib/theme-transition.ts +41 -13
- 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-actions.svelte +23 -0
- package/src/shadcn/code-block/code-block-content.svelte +55 -0
- package/src/shadcn/code-block/code-block-copy-button.svelte +10 -0
- package/src/shadcn/code-block/code-block-footer.svelte +20 -0
- package/src/shadcn/code-block/code-block-header.svelte +23 -0
- package/src/shadcn/code-block/code-block-line.svelte +71 -0
- package/src/shadcn/code-block/code-block-title.svelte +23 -0
- package/src/shadcn/code-block/code-block.svelte +47 -0
- package/src/shadcn/code-block/index.ts +42 -0
- 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/hooks/highlighter.ts +132 -0
- package/src/shadcn/hooks/use-highlighted-code.svelte.ts +98 -0
- package/src/shadcn/kbd/index.ts +10 -0
- package/src/shadcn/kbd/kbd-group.svelte +20 -0
- package/src/shadcn/kbd/kbd.svelte +23 -0
- package/src/shadcn/message-scroller/index.ts +28 -0
- package/src/shadcn/message-scroller/message-scroller-button.svelte +52 -0
- package/src/shadcn/message-scroller/message-scroller-content.svelte +17 -0
- package/src/shadcn/message-scroller/message-scroller-item.svelte +22 -0
- package/src/shadcn/message-scroller/message-scroller-provider.svelte +7 -0
- package/src/shadcn/message-scroller/message-scroller-viewport.svelte +20 -0
- package/src/shadcn/message-scroller/message-scroller.svelte +20 -0
- 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/tabs/tabs-list.svelte +13 -2
- package/src/shadcn/tabs/tabs-trigger.svelte +1 -1
- package/src/shadcn/toggle-group/toggle-group.svelte +10 -3
- package/src/shadcn/trace/index.ts +29 -0
- package/src/shadcn/trace/trace-collapsible-content.svelte +29 -0
- package/src/shadcn/trace/trace-collapsible-trigger.svelte +29 -0
- package/src/shadcn/trace/trace-collapsible.svelte +20 -0
- package/src/shadcn/trace/trace-description.svelte +20 -0
- package/src/shadcn/trace/trace-icon.svelte +24 -0
- package/src/shadcn/trace/trace-line.svelte +31 -0
- package/src/shadcn/trace/trace-title.svelte +20 -0
- package/src/shadcn/trace/trace.svelte +23 -0
- 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/ai/conversation/Conversation.svelte +0 -28
- package/src/ai/conversation/ConversationContent.svelte +0 -35
- package/src/ai/conversation/ConversationEmptyState.svelte +0 -46
- package/src/ai/conversation/ConversationScrollButton.svelte +0 -51
- package/src/ai/conversation/index.ts +0 -33
- package/src/ai/conversation/stick-to-bottom-context.svelte.ts +0 -131
- 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/charts/animated-value.css +0 -7
- package/src/charts/animated-value.svelte +0 -110
- package/src/charts/area-chart.css +0 -29
- package/src/charts/area-chart.svelte +0 -195
- package/src/charts/bar-chart.css +0 -119
- package/src/charts/bar-chart.svelte +0 -269
- package/src/charts/charts.css +0 -201
- package/src/charts/crosshair.svelte +0 -35
- package/src/charts/donut-chart.css +0 -56
- package/src/charts/donut-chart.svelte +0 -158
- package/src/charts/edge-fade.svelte +0 -43
- package/src/charts/edge-fade.ts +0 -16
- package/src/charts/figure.css +0 -100
- package/src/charts/figure.svelte +0 -122
- package/src/charts/gauge-chart.css +0 -94
- package/src/charts/gauge-chart.svelte +0 -303
- package/src/charts/hover-band.test.ts +0 -30
- package/src/charts/hover-band.ts +0 -21
- package/src/charts/hover-focus.svelte +0 -85
- package/src/charts/index.ts +0 -30
- package/src/charts/legend.css +0 -131
- package/src/charts/legend.svelte +0 -112
- package/src/charts/line-chart.css +0 -26
- package/src/charts/line-chart.svelte +0 -256
- package/src/charts/motion.ts +0 -19
- package/src/charts/number-chart.css +0 -168
- package/src/charts/number-chart.svelte +0 -177
- package/src/charts/primitives/chart-container.svelte +0 -57
- package/src/charts/primitives/chart-style.svelte +0 -36
- package/src/charts/primitives/chart-tooltip.svelte +0 -161
- package/src/charts/primitives/chart-utils.ts +0 -54
- package/src/charts/primitives/index.ts +0 -11
- package/src/charts/reveal.svelte +0 -61
- package/src/charts/ring-chart.css +0 -96
- package/src/charts/ring-chart.svelte +0 -158
- package/src/charts/sparkline.css +0 -45
- package/src/charts/sparkline.svelte +0 -140
- package/src/charts/stat-card.css +0 -103
- package/src/charts/stat-card.svelte +0 -267
- package/src/charts/time-series-chart.css +0 -28
- package/src/charts/time-series-chart.svelte +0 -179
- package/src/charts/trend-pill.css +0 -38
- package/src/charts/trend-pill.svelte +0 -48
- package/src/lib/hooks/is-mobile.svelte.ts +0 -11
- package/src/lib/utils.ts +0 -45
- package/src/showcase/AiSection.svelte +0 -157
- package/src/showcase/AnimationsSection.svelte +0 -1658
- package/src/showcase/ChartsSection.svelte +0 -458
- 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/conversation.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/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 -358
- 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.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/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 -17
- 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,206 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// =========================
|
|
3
|
+
// IMPORTS
|
|
4
|
+
// =========================
|
|
5
|
+
import type { Snippet } from "svelte";
|
|
6
|
+
import { setContext } from "svelte";
|
|
7
|
+
import { CaretLeft } from "phosphor-svelte";
|
|
8
|
+
import { cn } from "@heyramzi/utils";
|
|
9
|
+
import { Button } from "../../shadcn/button/index.js";
|
|
10
|
+
import { MASTER_DETAIL_PANE, type MasterDetailPane } from "./queue-rail.js";
|
|
11
|
+
|
|
12
|
+
// =========================
|
|
13
|
+
// TYPES
|
|
14
|
+
// =========================
|
|
15
|
+
type Props = {
|
|
16
|
+
// The list rail (an <aside>) and the content pane. Pages style both; this
|
|
17
|
+
// shell owns the two columns and the min-h-0/min-w-0 scroll containment.
|
|
18
|
+
rail: Snippet;
|
|
19
|
+
detail: Snippet;
|
|
20
|
+
// A full-width strip above both columns for facts to read before acting,
|
|
21
|
+
// like a posting cap. Pages wrapping the shell to add one broke the padding.
|
|
22
|
+
banner?: Snippet;
|
|
23
|
+
// WHY: a CSS grid track for the rail column ("16rem", "minmax(280px, 340px)").
|
|
24
|
+
// Queue pages pass `queueRailWidth` rather than a literal.
|
|
25
|
+
railWidth?: string;
|
|
26
|
+
// Stack to one column below this. "lg" because rail and detail can't both be
|
|
27
|
+
// read under 1024px. null only for a rail narrow enough for a phone.
|
|
28
|
+
stackBelow?: "lg" | "xl" | null;
|
|
29
|
+
// WHY: the phone back button says where it goes back to. "Back" alone is the
|
|
30
|
+
// browser's word and the reader has to remember what they left.
|
|
31
|
+
backLabel?: string;
|
|
32
|
+
// Bindable so a url naming a record opens on it after a refresh or a pasted
|
|
33
|
+
// link. The page sets it. Ignored above the stack breakpoint.
|
|
34
|
+
pane?: "rail" | "detail";
|
|
35
|
+
gap?: boolean;
|
|
36
|
+
pad?: boolean;
|
|
37
|
+
railClass?: string;
|
|
38
|
+
detailClass?: string;
|
|
39
|
+
class?: string;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// =========================
|
|
43
|
+
// PROPS
|
|
44
|
+
// =========================
|
|
45
|
+
let {
|
|
46
|
+
rail,
|
|
47
|
+
detail,
|
|
48
|
+
banner,
|
|
49
|
+
railWidth = "minmax(260px, 320px)",
|
|
50
|
+
stackBelow = "lg",
|
|
51
|
+
backLabel = "Back to the list",
|
|
52
|
+
// The rail wins by default: an unpicked detail is an empty state or a
|
|
53
|
+
// guessed row, both worse than the list.
|
|
54
|
+
pane = $bindable("rail"),
|
|
55
|
+
gap = true,
|
|
56
|
+
pad = true,
|
|
57
|
+
railClass,
|
|
58
|
+
detailClass,
|
|
59
|
+
class: className,
|
|
60
|
+
}: Props = $props();
|
|
61
|
+
|
|
62
|
+
// =========================
|
|
63
|
+
// DERIVED
|
|
64
|
+
// =========================
|
|
65
|
+
// One pane at a time when stacked. Two rows put a 34svh list scroller on top of
|
|
66
|
+
// a record scroller, and neither was readable.
|
|
67
|
+
let gridClass = $derived(
|
|
68
|
+
stackBelow === "xl"
|
|
69
|
+
? "grid-cols-1 grid-rows-[minmax(0,1fr)] xl:grid-rows-1 xl:[grid-template-columns:var(--md-cols)]"
|
|
70
|
+
: stackBelow === "lg"
|
|
71
|
+
? "grid-cols-1 grid-rows-[minmax(0,1fr)] lg:grid-rows-1 lg:[grid-template-columns:var(--md-cols)]"
|
|
72
|
+
: "[grid-template-columns:var(--md-cols)]",
|
|
73
|
+
);
|
|
74
|
+
// Hidden while stacked; back to normal above the breakpoint, where both panes show.
|
|
75
|
+
let hideWhileStacked = $derived(stackBelow === "xl" ? "max-xl:hidden" : "max-lg:hidden");
|
|
76
|
+
// Drawn only while stacked. The back bar has no job beside a visible rail.
|
|
77
|
+
let stackedOnly = $derived(stackBelow === "xl" ? "xl:hidden" : "lg:hidden");
|
|
78
|
+
|
|
79
|
+
// =========================
|
|
80
|
+
// FUNCTIONS
|
|
81
|
+
// =========================
|
|
82
|
+
// `RailRow` calls this when a record is picked. Above the breakpoint the classes
|
|
83
|
+
// that read `pane` are all prefixed away, so the flag changes nothing there.
|
|
84
|
+
setContext<MasterDetailPane>(MASTER_DETAIL_PANE, {
|
|
85
|
+
open: () => {
|
|
86
|
+
if (stackBelow) pane = "detail";
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
</script>
|
|
90
|
+
|
|
91
|
+
<!-- =========================== -->
|
|
92
|
+
<!-- MARKUP -->
|
|
93
|
+
<!-- =========================== -->
|
|
94
|
+
{#snippet columns(inBanner: boolean)}
|
|
95
|
+
<div
|
|
96
|
+
class={cn(
|
|
97
|
+
"md-cols grid min-h-0",
|
|
98
|
+
inBanner ? "flex-1" : "h-full",
|
|
99
|
+
gap && "gap-4",
|
|
100
|
+
pad && !inBanner && "p-4",
|
|
101
|
+
gridClass,
|
|
102
|
+
className,
|
|
103
|
+
)}
|
|
104
|
+
style="--md-cols: {railWidth} minmax(0, 1fr)"
|
|
105
|
+
data-pane={stackBelow ? pane : undefined}
|
|
106
|
+
data-stack={stackBelow ?? undefined}
|
|
107
|
+
>
|
|
108
|
+
<aside
|
|
109
|
+
class={cn(
|
|
110
|
+
"md-rail min-h-0",
|
|
111
|
+
railClass,
|
|
112
|
+
stackBelow && pane === "detail" && hideWhileStacked,
|
|
113
|
+
)}
|
|
114
|
+
>
|
|
115
|
+
{@render rail()}
|
|
116
|
+
</aside>
|
|
117
|
+
<div
|
|
118
|
+
class={cn(
|
|
119
|
+
"md-detail flex min-h-0 min-w-0 flex-col",
|
|
120
|
+
stackBelow && pane === "rail" && hideWhileStacked,
|
|
121
|
+
)}
|
|
122
|
+
>
|
|
123
|
+
{#if stackBelow}
|
|
124
|
+
<!-- WHY a button and not a link: nothing navigated, so there is no url to
|
|
125
|
+
go back to and the browser's own back would leave the page. -->
|
|
126
|
+
<Button
|
|
127
|
+
variant="ghost"
|
|
128
|
+
size="sm"
|
|
129
|
+
class={cn("-mx-1 mb-2 self-start px-1 text-muted-foreground", stackedOnly)}
|
|
130
|
+
onclick={() => (pane = "rail")}
|
|
131
|
+
>
|
|
132
|
+
<CaretLeft size={16} weight="bold" />
|
|
133
|
+
{backLabel}
|
|
134
|
+
</Button>
|
|
135
|
+
{/if}
|
|
136
|
+
<div class={cn("min-h-0 min-w-0 flex-1", detailClass)}>
|
|
137
|
+
{@render detail()}
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
{/snippet}
|
|
142
|
+
|
|
143
|
+
{#if banner}
|
|
144
|
+
<div class={cn("flex h-full min-h-0 flex-col", gap && "gap-4", pad && "p-4")}>
|
|
145
|
+
{@render banner()}
|
|
146
|
+
{@render columns(true)}
|
|
147
|
+
</div>
|
|
148
|
+
{:else}
|
|
149
|
+
{@render columns(false)}
|
|
150
|
+
{/if}
|
|
151
|
+
|
|
152
|
+
<!-- =========================== -->
|
|
153
|
+
<!-- STYLE -->
|
|
154
|
+
<!-- =========================== -->
|
|
155
|
+
<style>
|
|
156
|
+
/* The stacked list-to-record swap. The incoming pane moves: forward from the
|
|
157
|
+
right, back from the left. It runs when `data-pane` flips. */
|
|
158
|
+
@media (max-width: 1023px) {
|
|
159
|
+
.md-cols[data-stack="lg"][data-pane="detail"] > .md-detail {
|
|
160
|
+
animation: md-in-forward var(--duration-fast) var(--ease-smooth-out) both;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.md-cols[data-stack="lg"][data-pane="rail"] > .md-rail {
|
|
164
|
+
animation: md-in-back var(--duration-fast) var(--ease-smooth-out) both;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
@media (max-width: 1279px) {
|
|
169
|
+
.md-cols[data-stack="xl"][data-pane="detail"] > .md-detail {
|
|
170
|
+
animation: md-in-forward var(--duration-fast) var(--ease-smooth-out) both;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.md-cols[data-stack="xl"][data-pane="rail"] > .md-rail {
|
|
174
|
+
animation: md-in-back var(--duration-fast) var(--ease-smooth-out) both;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
@keyframes md-in-forward {
|
|
179
|
+
from {
|
|
180
|
+
opacity: 0;
|
|
181
|
+
transform: translateX(var(--distance-large));
|
|
182
|
+
}
|
|
183
|
+
to {
|
|
184
|
+
opacity: 1;
|
|
185
|
+
transform: none;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
@keyframes md-in-back {
|
|
190
|
+
from {
|
|
191
|
+
opacity: 0;
|
|
192
|
+
transform: translateX(calc(var(--distance-large) * -1));
|
|
193
|
+
}
|
|
194
|
+
to {
|
|
195
|
+
opacity: 1;
|
|
196
|
+
transform: none;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
@media (prefers-reduced-motion: reduce) {
|
|
201
|
+
.md-cols > .md-rail,
|
|
202
|
+
.md-cols > .md-detail {
|
|
203
|
+
animation-duration: 1ms;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
</style>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { resolve, dirname } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { describe, expect, it } from "vite-plus/test";
|
|
5
|
+
|
|
6
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const shell = readFileSync(resolve(here, "master-detail.svelte"), "utf8");
|
|
8
|
+
const row = readFileSync(resolve(here, "rail-row.svelte"), "utf8");
|
|
9
|
+
|
|
10
|
+
describe("master detail on a phone", () => {
|
|
11
|
+
it("shows one pane at a time below the stack breakpoint", () => {
|
|
12
|
+
// Two rows put a 34svh rail scroller above a second one. One row, one
|
|
13
|
+
// pane, hidden only while stacked so desktop stays untouched.
|
|
14
|
+
expect(shell).toContain("grid-rows-[minmax(0,1fr)] lg:grid-rows-1");
|
|
15
|
+
expect(shell).toContain("grid-rows-[minmax(0,1fr)] xl:grid-rows-1");
|
|
16
|
+
expect(shell).not.toContain("max-h-[34svh]");
|
|
17
|
+
expect(shell).toContain('stackBelow === "xl" ? "max-xl:hidden" : "max-lg:hidden"');
|
|
18
|
+
expect(shell).toContain('stackBelow && pane === "detail" && hideWhileStacked');
|
|
19
|
+
expect(shell).toContain('stackBelow && pane === "rail" && hideWhileStacked');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("draws a way back to the list, and only while stacked", () => {
|
|
23
|
+
expect(shell).toContain('stackBelow === "xl" ? "xl:hidden" : "lg:hidden"');
|
|
24
|
+
expect(shell).toContain('onclick={() => (pane = "rail")}');
|
|
25
|
+
expect(shell).toContain("{backLabel}");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("lets a page open on the record its url names", () => {
|
|
29
|
+
expect(shell).toContain('pane = $bindable("rail")');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("moves to the record when a rail row is picked", () => {
|
|
33
|
+
expect(shell).toContain("setContext<MasterDetailPane>(MASTER_DETAIL_PANE");
|
|
34
|
+
expect(row).toContain("getContext<MasterDetailPane | undefined>(MASTER_DETAIL_PANE)");
|
|
35
|
+
expect(row).toContain("pane?.open()");
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// =========================
|
|
3
|
+
// IMPORTS
|
|
4
|
+
// =========================
|
|
5
|
+
import type { Snippet } from "svelte";
|
|
6
|
+
import { Warning, Info, CheckCircle, WarningCircle } from "phosphor-svelte";
|
|
7
|
+
import { cn } from "@heyramzi/utils";
|
|
8
|
+
import { Alert } from "../../shadcn/alert/index.js";
|
|
9
|
+
import { TONE_CLASS, type Tone as SeverityTone } from "../../lib/tone.js";
|
|
10
|
+
import type { PhosphorIcon } from "../../lib/icon-types.js";
|
|
11
|
+
|
|
12
|
+
// =========================
|
|
13
|
+
// TYPES
|
|
14
|
+
// =========================
|
|
15
|
+
// A strip that stops the reader is a severity. Neutral and accent are chip tones.
|
|
16
|
+
type Tone = Extract<SeverityTone, "destructive" | "warning" | "info" | "success">;
|
|
17
|
+
|
|
18
|
+
interface Props {
|
|
19
|
+
// The "look at this before you act" strip. Hand-rolled ones used raw amber
|
|
20
|
+
// and sky, so one severity showed two colours one route apart.
|
|
21
|
+
tone?: Tone;
|
|
22
|
+
icon?: PhosphorIcon;
|
|
23
|
+
// The trailing control: a retry, a jump to the thing that needs fixing.
|
|
24
|
+
actions?: Snippet;
|
|
25
|
+
children: Snippet;
|
|
26
|
+
class?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const TONE_ICON: Record<Tone, PhosphorIcon> = {
|
|
30
|
+
destructive: Warning,
|
|
31
|
+
warning: WarningCircle,
|
|
32
|
+
info: Info,
|
|
33
|
+
success: CheckCircle,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// =========================
|
|
37
|
+
// PROPS
|
|
38
|
+
// =========================
|
|
39
|
+
let { tone = "destructive", icon, actions, children, class: className }: Props = $props();
|
|
40
|
+
|
|
41
|
+
// =========================
|
|
42
|
+
// DERIVED
|
|
43
|
+
// =========================
|
|
44
|
+
let Icon = $derived(icon ?? TONE_ICON[tone]);
|
|
45
|
+
</script>
|
|
46
|
+
|
|
47
|
+
<!-- =========================== -->
|
|
48
|
+
<!-- MARKUP -->
|
|
49
|
+
<!-- =========================== -->
|
|
50
|
+
<!-- WHY `status` for the calm tones: info and success shouldn't interrupt a
|
|
51
|
+
screen reader the way a failure does. -->
|
|
52
|
+
<Alert
|
|
53
|
+
variant={tone === "destructive" ? "destructive" : "default"}
|
|
54
|
+
role={tone === "destructive" || tone === "warning" ? "alert" : "status"}
|
|
55
|
+
class={cn(
|
|
56
|
+
"flex flex-wrap items-center gap-x-3 gap-y-2 rounded-xl [&>svg]:translate-y-0",
|
|
57
|
+
TONE_CLASS[tone],
|
|
58
|
+
className,
|
|
59
|
+
)}
|
|
60
|
+
>
|
|
61
|
+
<Icon class="size-4 shrink-0" aria-hidden="true" />
|
|
62
|
+
<span class="min-w-0 flex-1 font-medium">{@render children()}</span>
|
|
63
|
+
{#if actions}
|
|
64
|
+
<div class="flex shrink-0 items-center gap-2">
|
|
65
|
+
{@render actions()}
|
|
66
|
+
</div>
|
|
67
|
+
{/if}
|
|
68
|
+
</Alert>
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// =========================
|
|
3
|
+
// IMPORTS
|
|
4
|
+
// =========================
|
|
5
|
+
import { CaretLeft, CaretRight } from "phosphor-svelte";
|
|
6
|
+
import { cn } from "@heyramzi/utils";
|
|
7
|
+
import { Button } from "../../shadcn/button/index.js";
|
|
8
|
+
import { stepTo } from "./step.js";
|
|
9
|
+
|
|
10
|
+
// =========================
|
|
11
|
+
// TYPES
|
|
12
|
+
// =========================
|
|
13
|
+
type Props = {
|
|
14
|
+
/** Ids in the order the list shows them. */
|
|
15
|
+
ids: readonly string[];
|
|
16
|
+
current: string | null | undefined;
|
|
17
|
+
onselect: (id: string) => void;
|
|
18
|
+
previousLabel?: string;
|
|
19
|
+
nextLabel?: string;
|
|
20
|
+
/** Off when two steppers share a page, so one key press moves one list. */
|
|
21
|
+
keys?: boolean;
|
|
22
|
+
class?: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// =========================
|
|
26
|
+
// PROPS
|
|
27
|
+
// =========================
|
|
28
|
+
let {
|
|
29
|
+
ids,
|
|
30
|
+
current,
|
|
31
|
+
onselect,
|
|
32
|
+
previousLabel = "Previous",
|
|
33
|
+
nextLabel = "Next",
|
|
34
|
+
keys = true,
|
|
35
|
+
class: className,
|
|
36
|
+
}: Props = $props();
|
|
37
|
+
|
|
38
|
+
// =========================
|
|
39
|
+
// DERIVED
|
|
40
|
+
// =========================
|
|
41
|
+
let index = $derived(current ? ids.indexOf(current) : -1);
|
|
42
|
+
let prev = $derived(stepTo(ids, current, -1));
|
|
43
|
+
let next = $derived(stepTo(ids, current, 1));
|
|
44
|
+
|
|
45
|
+
// =========================
|
|
46
|
+
// HANDLERS
|
|
47
|
+
// =========================
|
|
48
|
+
function isTyping(event: KeyboardEvent): boolean {
|
|
49
|
+
const target = event.target as HTMLElement | null;
|
|
50
|
+
return Boolean(target?.closest("input, textarea, select, [contenteditable='true']"));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function ignored(event: KeyboardEvent): boolean {
|
|
54
|
+
const modified = event.metaKey || event.ctrlKey || event.altKey;
|
|
55
|
+
return !keys || modified || event.defaultPrevented || isTyping(event);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// K and J, as in Gmail: the arrows already belong to slide viewers and text fields.
|
|
59
|
+
function targetFor(key: string): string | null {
|
|
60
|
+
if (key === "k") return prev;
|
|
61
|
+
if (key === "j") return next;
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function onkeydown(event: KeyboardEvent) {
|
|
66
|
+
if (ignored(event)) return;
|
|
67
|
+
const id = targetFor(event.key);
|
|
68
|
+
if (!id) return;
|
|
69
|
+
event.preventDefault();
|
|
70
|
+
onselect(id);
|
|
71
|
+
}
|
|
72
|
+
</script>
|
|
73
|
+
|
|
74
|
+
<svelte:window {onkeydown} />
|
|
75
|
+
|
|
76
|
+
{#if index >= 0 && ids.length > 1}
|
|
77
|
+
<div class={cn("flex items-center gap-0.5 text-muted-foreground", className)}>
|
|
78
|
+
<Button
|
|
79
|
+
variant="ghost"
|
|
80
|
+
size="sm"
|
|
81
|
+
icon
|
|
82
|
+
class="text-muted-foreground"
|
|
83
|
+
disabled={!prev}
|
|
84
|
+
onclick={() => prev && onselect(prev)}
|
|
85
|
+
aria-label={previousLabel}
|
|
86
|
+
title={`${previousLabel} (K)`}
|
|
87
|
+
>
|
|
88
|
+
<CaretLeft size={15} />
|
|
89
|
+
</Button>
|
|
90
|
+
<span class="min-w-9 text-center text-xs tabular-nums">{index + 1}/{ids.length}</span>
|
|
91
|
+
<Button
|
|
92
|
+
variant="ghost"
|
|
93
|
+
size="sm"
|
|
94
|
+
icon
|
|
95
|
+
class="text-muted-foreground"
|
|
96
|
+
disabled={!next}
|
|
97
|
+
onclick={() => next && onselect(next)}
|
|
98
|
+
aria-label={nextLabel}
|
|
99
|
+
title={`${nextLabel} (J)`}
|
|
100
|
+
>
|
|
101
|
+
<CaretRight size={15} />
|
|
102
|
+
</Button>
|
|
103
|
+
</div>
|
|
104
|
+
{/if}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The rail surface of a queue page, passed to `MasterDetail`'s `railClass`.
|
|
3
|
+
* A class, not a wrapper: the shell's `<aside>` already owns the scroll height.
|
|
4
|
+
*/
|
|
5
|
+
// Copies of this string drifted to `rounded-lg`, the control radius, not the container one.
|
|
6
|
+
export const queueRailClass =
|
|
7
|
+
"flex flex-col overflow-hidden rounded-xl border border-border bg-card";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The rail column track, passed to `MasterDetail`'s `railWidth`. One page ran
|
|
11
|
+
* 20px wider, so the detail pane jumped between pages.
|
|
12
|
+
*/
|
|
13
|
+
// 280px fits a two-line thread title. Past 340px the list competes with the record.
|
|
14
|
+
export const queueRailWidth = "minmax(280px, 340px)";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Context key for the pane `MasterDetail` shows on a phone. Picking a row moves
|
|
18
|
+
* to the record, so `RailRow` reads this instead of each page passing a callback.
|
|
19
|
+
*/
|
|
20
|
+
export const MASTER_DETAIL_PANE = Symbol("master-detail-pane");
|
|
21
|
+
|
|
22
|
+
export type MasterDetailPane = {
|
|
23
|
+
/** Show the detail pane. No-op above the stack breakpoint, where both are visible. */
|
|
24
|
+
open: () => void;
|
|
25
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// =========================
|
|
3
|
+
// IMPORTS
|
|
4
|
+
// =========================
|
|
5
|
+
import type { Snippet } from "svelte";
|
|
6
|
+
import { cn } from "@heyramzi/utils";
|
|
7
|
+
|
|
8
|
+
// =========================
|
|
9
|
+
// TYPES
|
|
10
|
+
// =========================
|
|
11
|
+
type Props = {
|
|
12
|
+
// The scroller under `RailHead`. `min-h-0` makes a flex child scroll instead
|
|
13
|
+
// of growing its parent, and it's the line hand-rolled rails forgot.
|
|
14
|
+
children: Snippet;
|
|
15
|
+
class?: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// =========================
|
|
19
|
+
// PROPS
|
|
20
|
+
// =========================
|
|
21
|
+
let { children, class: className }: Props = $props();
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<!-- =========================== -->
|
|
25
|
+
<!-- MARKUP -->
|
|
26
|
+
<!-- =========================== -->
|
|
27
|
+
<div class={cn("min-h-0 flex-1 overflow-y-auto", className)}>
|
|
28
|
+
{@render children()}
|
|
29
|
+
</div>
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// =========================
|
|
3
|
+
// IMPORTS
|
|
4
|
+
// =========================
|
|
5
|
+
import { cn } from "@heyramzi/utils";
|
|
6
|
+
import * as Empty from "../../shadcn/empty/index.js";
|
|
7
|
+
import type { PhosphorIcon, IconWeight } from "../../lib/icon-types.js";
|
|
8
|
+
|
|
9
|
+
// =========================
|
|
10
|
+
// TYPES
|
|
11
|
+
// =========================
|
|
12
|
+
interface Props {
|
|
13
|
+
// Not `EmptyState`, which has a 48px icon and an action. This says a filter
|
|
14
|
+
// came back empty, in one sentence that fits a 280px rail.
|
|
15
|
+
icon?: PhosphorIcon;
|
|
16
|
+
iconWeight?: IconWeight;
|
|
17
|
+
iconClass?: string;
|
|
18
|
+
title: string;
|
|
19
|
+
hint?: string;
|
|
20
|
+
class?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// =========================
|
|
24
|
+
// PROPS
|
|
25
|
+
// =========================
|
|
26
|
+
let {
|
|
27
|
+
icon: Icon,
|
|
28
|
+
iconWeight,
|
|
29
|
+
iconClass,
|
|
30
|
+
title,
|
|
31
|
+
hint,
|
|
32
|
+
class: className,
|
|
33
|
+
}: Props = $props();
|
|
34
|
+
</script>
|
|
35
|
+
|
|
36
|
+
<!-- =========================== -->
|
|
37
|
+
<!-- MARKUP -->
|
|
38
|
+
<!-- =========================== -->
|
|
39
|
+
<Empty.Root class={cn("h-full gap-2 px-6 py-12", className)}>
|
|
40
|
+
{#if Icon}
|
|
41
|
+
<Icon
|
|
42
|
+
weight={iconWeight}
|
|
43
|
+
class={cn("size-6", iconClass ?? "text-muted-foreground/40")}
|
|
44
|
+
aria-hidden="true"
|
|
45
|
+
/>
|
|
46
|
+
{/if}
|
|
47
|
+
<Empty.Title>{title}</Empty.Title>
|
|
48
|
+
{#if hint}
|
|
49
|
+
<Empty.Description class="max-w-56 text-xs leading-relaxed">{hint}</Empty.Description>
|
|
50
|
+
{/if}
|
|
51
|
+
</Empty.Root>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// =========================
|
|
3
|
+
// IMPORTS
|
|
4
|
+
// =========================
|
|
5
|
+
import type { Snippet } from "svelte";
|
|
6
|
+
import SectionLabel from "./section-label.svelte";
|
|
7
|
+
import { cn } from "@heyramzi/utils";
|
|
8
|
+
|
|
9
|
+
// =========================
|
|
10
|
+
// TYPES
|
|
11
|
+
// =========================
|
|
12
|
+
type Props = {
|
|
13
|
+
// The fixed top of a queue rail: label, filter, search. A fixed height, so
|
|
14
|
+
// rows start on the same line on every queue page.
|
|
15
|
+
label?: string;
|
|
16
|
+
// A low-emphasis control beside the label (compose, refresh).
|
|
17
|
+
action?: Snippet;
|
|
18
|
+
// The filter, the search, whatever narrows the list. Optional: a rail of four
|
|
19
|
+
// records has nothing to narrow, and an empty strip under the label is chrome.
|
|
20
|
+
children?: Snippet;
|
|
21
|
+
class?: string;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// =========================
|
|
25
|
+
// PROPS
|
|
26
|
+
// =========================
|
|
27
|
+
let { label, action, children, class: className }: Props = $props();
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
<!-- =========================== -->
|
|
31
|
+
<!-- MARKUP -->
|
|
32
|
+
<!-- =========================== -->
|
|
33
|
+
<div class={cn("shrink-0 space-y-2 border-b border-border px-3 py-2.5", className)}>
|
|
34
|
+
{#if label || action}
|
|
35
|
+
<div class="flex min-h-7 items-center justify-between gap-2 pl-1">
|
|
36
|
+
{#if label}
|
|
37
|
+
<SectionLabel>{label}</SectionLabel>
|
|
38
|
+
{:else}
|
|
39
|
+
<span></span>
|
|
40
|
+
{/if}
|
|
41
|
+
{#if action}
|
|
42
|
+
{@render action()}
|
|
43
|
+
{/if}
|
|
44
|
+
</div>
|
|
45
|
+
{/if}
|
|
46
|
+
{#if children}
|
|
47
|
+
{@render children()}
|
|
48
|
+
{/if}
|
|
49
|
+
</div>
|