@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,134 @@
|
|
|
1
|
+
import { describe, expect, it } from "vite-plus/test";
|
|
2
|
+
import {
|
|
3
|
+
toggleSort,
|
|
4
|
+
sortRows,
|
|
5
|
+
paginateRows,
|
|
6
|
+
totalPages,
|
|
7
|
+
normalizeQuery,
|
|
8
|
+
containsQuery,
|
|
9
|
+
anyContainsQuery,
|
|
10
|
+
formatCount,
|
|
11
|
+
} from "./data-table.js";
|
|
12
|
+
|
|
13
|
+
type Item = { name: string; value: number; date: string | null };
|
|
14
|
+
|
|
15
|
+
const items: Item[] = [
|
|
16
|
+
{ name: "Charlie", value: 3, date: "2025-03-01" },
|
|
17
|
+
{ name: "Alice", value: 1, date: "2025-01-01" },
|
|
18
|
+
{ name: "Bob", value: 2, date: null },
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
describe("toggleSort", () => {
|
|
22
|
+
it("returns asc when no current sort", () => {
|
|
23
|
+
expect(toggleSort<Item>(null, "name")).toEqual({ key: "name", direction: "asc" });
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("returns desc when current is asc on same key", () => {
|
|
27
|
+
expect(toggleSort<Item>({ key: "name", direction: "asc" }, "name")).toEqual({
|
|
28
|
+
key: "name",
|
|
29
|
+
direction: "desc",
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("returns null when current is desc on same key", () => {
|
|
34
|
+
expect(toggleSort<Item>({ key: "name", direction: "desc" }, "name")).toBeNull();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("returns asc on new key", () => {
|
|
38
|
+
expect(toggleSort<Item>({ key: "name", direction: "desc" }, "value")).toEqual({
|
|
39
|
+
key: "value",
|
|
40
|
+
direction: "asc",
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe("sortRows", () => {
|
|
46
|
+
it("returns same order when sort is null", () => {
|
|
47
|
+
expect(sortRows(items, null)).toEqual(items);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("sorts strings ascending", () => {
|
|
51
|
+
const sorted = sortRows(items, { key: "name", direction: "asc" });
|
|
52
|
+
expect(sorted.map((i) => i.name)).toEqual(["Alice", "Bob", "Charlie"]);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("sorts strings descending", () => {
|
|
56
|
+
const sorted = sortRows(items, { key: "name", direction: "desc" });
|
|
57
|
+
expect(sorted.map((i) => i.name)).toEqual(["Charlie", "Bob", "Alice"]);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("sorts numbers ascending", () => {
|
|
61
|
+
const sorted = sortRows(items, { key: "value", direction: "asc" });
|
|
62
|
+
expect(sorted.map((i) => i.value)).toEqual([1, 2, 3]);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("pushes nulls to end", () => {
|
|
66
|
+
const sorted = sortRows(items, { key: "date", direction: "asc" });
|
|
67
|
+
expect(sorted[2].date).toBeNull();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("does not mutate original array", () => {
|
|
71
|
+
const original = [...items];
|
|
72
|
+
sortRows(items, { key: "name", direction: "asc" });
|
|
73
|
+
expect(items).toEqual(original);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe("paginateRows", () => {
|
|
78
|
+
const rows = Array.from({ length: 25 }, (_, i) => ({ id: i }));
|
|
79
|
+
|
|
80
|
+
it("returns first page", () => {
|
|
81
|
+
const result = paginateRows(rows, 1, 10);
|
|
82
|
+
expect(result).toHaveLength(10);
|
|
83
|
+
expect(result[0].id).toBe(0);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("returns last partial page", () => {
|
|
87
|
+
const result = paginateRows(rows, 3, 10);
|
|
88
|
+
expect(result).toHaveLength(5);
|
|
89
|
+
expect(result[0].id).toBe(20);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("returns empty for out-of-range page", () => {
|
|
93
|
+
expect(paginateRows(rows, 5, 10)).toHaveLength(0);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe("totalPages", () => {
|
|
98
|
+
it("calculates correct total", () => {
|
|
99
|
+
expect(totalPages(25, 10)).toBe(3);
|
|
100
|
+
expect(totalPages(20, 10)).toBe(2);
|
|
101
|
+
expect(totalPages(0, 10)).toBe(1);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe("formatCount", () => {
|
|
106
|
+
it("shows total when no filter active", () => {
|
|
107
|
+
expect(formatCount(10, 10, "items")).toBe("10 items");
|
|
108
|
+
});
|
|
109
|
+
it("shows filtered of total when filtered", () => {
|
|
110
|
+
expect(formatCount(3, 10, "items")).toBe("3 of 10 items");
|
|
111
|
+
});
|
|
112
|
+
it("singularizes when count is 1", () => {
|
|
113
|
+
expect(formatCount(1, 1, "items")).toBe("1 item");
|
|
114
|
+
});
|
|
115
|
+
it("singularizes filtered count", () => {
|
|
116
|
+
expect(formatCount(1, 10, "items")).toBe("1 of 10 items");
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe("existing utilities", () => {
|
|
121
|
+
it("normalizeQuery trims and lowercases", () => {
|
|
122
|
+
expect(normalizeQuery(" Hello ")).toBe("hello");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("containsQuery handles null", () => {
|
|
126
|
+
expect(containsQuery(null, "test")).toBe(false);
|
|
127
|
+
expect(containsQuery("testing", "test")).toBe(true);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("anyContainsQuery matches any value", () => {
|
|
131
|
+
expect(anyContainsQuery(["foo", null, "bar"], "bar")).toBe(true);
|
|
132
|
+
expect(anyContainsQuery(["foo", null], "bar")).toBe(false);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { Snippet } from "svelte";
|
|
2
|
+
|
|
3
|
+
export type DataTableFilter<T> = (row: T, query: string) => boolean;
|
|
4
|
+
|
|
5
|
+
export type DataTableEmptyState = {
|
|
6
|
+
title: string;
|
|
7
|
+
description: string;
|
|
8
|
+
searchDescription?: string;
|
|
9
|
+
action?: Snippet;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function normalizeQuery(query: string): string {
|
|
13
|
+
return query.trim().toLowerCase();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function containsQuery(value: string | null | undefined, query: string): boolean {
|
|
17
|
+
if (!query) return true;
|
|
18
|
+
|
|
19
|
+
return value?.toLowerCase().includes(query) ?? false;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function anyContainsQuery(values: Array<string | null | undefined>, query: string): boolean {
|
|
23
|
+
return values.some((value) => containsQuery(value, query));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ========================= TYPES
|
|
27
|
+
export type SortDirection = "asc" | "desc";
|
|
28
|
+
|
|
29
|
+
export type SortState<T> = {
|
|
30
|
+
key: keyof T & string;
|
|
31
|
+
direction: SortDirection;
|
|
32
|
+
} | null;
|
|
33
|
+
|
|
34
|
+
// ========================= FUNCTIONS
|
|
35
|
+
export function toggleSort<T>(current: SortState<T>, key: keyof T & string): SortState<T> {
|
|
36
|
+
if (current?.key !== key) return { key, direction: "asc" };
|
|
37
|
+
if (current.direction === "asc") return { key, direction: "desc" };
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function sortRows<T>(rows: T[], sort: SortState<T>): T[] {
|
|
42
|
+
if (!sort) return rows;
|
|
43
|
+
const { key, direction } = sort;
|
|
44
|
+
const multiplier = direction === "asc" ? 1 : -1;
|
|
45
|
+
return [...rows].sort((a, b) => {
|
|
46
|
+
const aVal = a[key];
|
|
47
|
+
const bVal = b[key];
|
|
48
|
+
if (aVal == null && bVal == null) return 0;
|
|
49
|
+
if (aVal == null) return 1;
|
|
50
|
+
if (bVal == null) return -1;
|
|
51
|
+
if (typeof aVal === "string" && typeof bVal === "string") {
|
|
52
|
+
return multiplier * aVal.localeCompare(bVal);
|
|
53
|
+
}
|
|
54
|
+
if (typeof aVal === "number" && typeof bVal === "number") {
|
|
55
|
+
return multiplier * (aVal - bVal);
|
|
56
|
+
}
|
|
57
|
+
return multiplier * String(aVal).localeCompare(String(bVal));
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function paginateRows<T>(rows: T[], page: number, pageSize: number): T[] {
|
|
62
|
+
const start = (page - 1) * pageSize;
|
|
63
|
+
return rows.slice(start, start + pageSize);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function totalPages(total: number, pageSize: number): number {
|
|
67
|
+
return Math.max(1, Math.ceil(total / pageSize));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function formatCount(visible: number, total: number, label: string): string {
|
|
71
|
+
const singular = label.endsWith("s") ? label.slice(0, -1) : label;
|
|
72
|
+
if (visible === total) {
|
|
73
|
+
const displayLabel = visible === 1 ? singular : label;
|
|
74
|
+
return `${total} ${displayLabel}`;
|
|
75
|
+
}
|
|
76
|
+
return `${visible} of ${total} ${label}`;
|
|
77
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export { default as InfiniteScroll } from "./infinite-scroll.svelte";
|
|
2
|
+
export { default as Pagination } from "./pagination.svelte";
|
|
3
|
+
export { default as RowArrow } from "./row-arrow.svelte";
|
|
4
|
+
export { default as RowContextMenu } from "./row-context-menu.svelte";
|
|
5
|
+
export { default as SortableHead } from "./sortable-head.svelte";
|
|
6
|
+
export { default as TableSkeleton } from "./table-skeleton.svelte";
|
|
7
|
+
export {
|
|
8
|
+
anyContainsQuery,
|
|
9
|
+
containsQuery,
|
|
10
|
+
formatCount,
|
|
11
|
+
normalizeQuery,
|
|
12
|
+
paginateRows,
|
|
13
|
+
sortRows,
|
|
14
|
+
toggleSort,
|
|
15
|
+
totalPages,
|
|
16
|
+
type DataTableEmptyState,
|
|
17
|
+
type DataTableFilter,
|
|
18
|
+
type SortDirection,
|
|
19
|
+
type SortState,
|
|
20
|
+
} from "./data-table.js";
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
<script lang="ts" generics="T">
|
|
2
|
+
// ========================= IMPORTS
|
|
3
|
+
import type { Snippet } from "svelte";
|
|
4
|
+
import { Spinner } from "../../shadcn/spinner/index.js";
|
|
5
|
+
|
|
6
|
+
// ========================= TYPES
|
|
7
|
+
type Props = {
|
|
8
|
+
items: T[];
|
|
9
|
+
isDone: boolean;
|
|
10
|
+
isLoading: boolean;
|
|
11
|
+
children: Snippet<[{ items: T[] }]>;
|
|
12
|
+
/** Skeleton snippet rendered while loading more */
|
|
13
|
+
skeleton?: Snippet;
|
|
14
|
+
onLoadMore: () => void;
|
|
15
|
+
threshold?: number;
|
|
16
|
+
loadingClass?: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// ========================= PROPS
|
|
20
|
+
let {
|
|
21
|
+
items,
|
|
22
|
+
isDone,
|
|
23
|
+
isLoading,
|
|
24
|
+
children,
|
|
25
|
+
skeleton,
|
|
26
|
+
onLoadMore,
|
|
27
|
+
threshold = 200,
|
|
28
|
+
loadingClass,
|
|
29
|
+
}: Props = $props();
|
|
30
|
+
|
|
31
|
+
// ========================= STATE
|
|
32
|
+
let sentinelEl: HTMLDivElement | undefined = $state();
|
|
33
|
+
|
|
34
|
+
// ========================= EFFECTS
|
|
35
|
+
$effect(() => {
|
|
36
|
+
if (!sentinelEl || isDone) return;
|
|
37
|
+
|
|
38
|
+
const observer = new IntersectionObserver(
|
|
39
|
+
(entries) => {
|
|
40
|
+
if (entries[0]?.isIntersecting && !isLoading && !isDone) {
|
|
41
|
+
onLoadMore();
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
{ rootMargin: `${threshold}px` },
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
observer.observe(sentinelEl);
|
|
48
|
+
return () => observer.disconnect();
|
|
49
|
+
});
|
|
50
|
+
</script>
|
|
51
|
+
|
|
52
|
+
<!-- =========================== -->
|
|
53
|
+
<!-- MARKUP -->
|
|
54
|
+
<!-- =========================== -->
|
|
55
|
+
{@render children({ items })}
|
|
56
|
+
|
|
57
|
+
{#if !isDone}
|
|
58
|
+
<div bind:this={sentinelEl} class={loadingClass ?? ""}>
|
|
59
|
+
{#if isLoading}
|
|
60
|
+
{#if skeleton}
|
|
61
|
+
{@render skeleton()}
|
|
62
|
+
{:else}
|
|
63
|
+
<div class="flex items-center justify-center py-4">
|
|
64
|
+
<Spinner class="size-5 text-muted-foreground" />
|
|
65
|
+
</div>
|
|
66
|
+
{/if}
|
|
67
|
+
{/if}
|
|
68
|
+
</div>
|
|
69
|
+
{/if}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { CaretLeft, CaretRight } from "phosphor-svelte";
|
|
3
|
+
import { Button } from "../../shadcn/button/index.js";
|
|
4
|
+
import { totalPages as calcTotalPages } from "./data-table.js";
|
|
5
|
+
|
|
6
|
+
// ========================= TYPES
|
|
7
|
+
type Props = {
|
|
8
|
+
page: number;
|
|
9
|
+
pageSize: number;
|
|
10
|
+
total: number;
|
|
11
|
+
onPageChange?: (page: number) => void;
|
|
12
|
+
/** If provided, renders <a> tags instead of buttons (for server-side pagination) */
|
|
13
|
+
buildHref?: (page: number) => string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// ========================= PROPS
|
|
17
|
+
let { page, pageSize, total, onPageChange, buildHref }: Props = $props();
|
|
18
|
+
|
|
19
|
+
// ========================= DERIVED
|
|
20
|
+
let pages = $derived(calcTotalPages(total, pageSize));
|
|
21
|
+
let hasPrev = $derived(page > 1);
|
|
22
|
+
let hasNext = $derived(page < pages);
|
|
23
|
+
let start = $derived((page - 1) * pageSize + 1);
|
|
24
|
+
let end = $derived(Math.min(page * pageSize, total));
|
|
25
|
+
</script>
|
|
26
|
+
|
|
27
|
+
{#if pages > 1}
|
|
28
|
+
<div class="flex items-center justify-between border-t px-4 py-3 text-sm text-muted-foreground">
|
|
29
|
+
<span>{start}-{end} of {total}</span>
|
|
30
|
+
<div class="flex items-center gap-1">
|
|
31
|
+
{#if buildHref}
|
|
32
|
+
{#if hasPrev}
|
|
33
|
+
<Button variant="outline" size="sm" icon href={buildHref(page - 1)}>
|
|
34
|
+
<CaretLeft class="size-4" />
|
|
35
|
+
</Button>
|
|
36
|
+
{:else}
|
|
37
|
+
<Button variant="outline" size="sm" icon disabled>
|
|
38
|
+
<CaretLeft class="size-4" />
|
|
39
|
+
</Button>
|
|
40
|
+
{/if}
|
|
41
|
+
<span class="mx-2 tabular-nums">{page} / {pages}</span>
|
|
42
|
+
{#if hasNext}
|
|
43
|
+
<Button variant="outline" size="sm" icon href={buildHref(page + 1)}>
|
|
44
|
+
<CaretRight class="size-4" />
|
|
45
|
+
</Button>
|
|
46
|
+
{:else}
|
|
47
|
+
<Button variant="outline" size="sm" icon disabled>
|
|
48
|
+
<CaretRight class="size-4" />
|
|
49
|
+
</Button>
|
|
50
|
+
{/if}
|
|
51
|
+
{:else}
|
|
52
|
+
<Button variant="outline" size="sm" icon disabled={!hasPrev} onclick={() => onPageChange?.(page - 1)}>
|
|
53
|
+
<CaretLeft class="size-4" />
|
|
54
|
+
</Button>
|
|
55
|
+
<span class="mx-2 tabular-nums">{page} / {pages}</span>
|
|
56
|
+
<Button variant="outline" size="sm" icon disabled={!hasNext} onclick={() => onPageChange?.(page + 1)}>
|
|
57
|
+
<CaretRight class="size-4" />
|
|
58
|
+
</Button>
|
|
59
|
+
{/if}
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
{/if}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// =========================
|
|
3
|
+
// IMPORTS
|
|
4
|
+
// =========================
|
|
5
|
+
import { CaretRight } from "phosphor-svelte";
|
|
6
|
+
import { cn } from "@heyramzi/utils";
|
|
7
|
+
|
|
8
|
+
// =========================
|
|
9
|
+
// TYPES
|
|
10
|
+
// =========================
|
|
11
|
+
type Props = {
|
|
12
|
+
class?: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
// =========================
|
|
16
|
+
// PROPS
|
|
17
|
+
// =========================
|
|
18
|
+
let { class: className }: Props = $props();
|
|
19
|
+
</script>
|
|
20
|
+
|
|
21
|
+
<!-- =========================== -->
|
|
22
|
+
<!-- MARKUP -->
|
|
23
|
+
<!-- =========================== -->
|
|
24
|
+
<CaretRight
|
|
25
|
+
class={cn("size-3.5 text-muted-foreground opacity-0 transition-opacity group-hover/row:opacity-100", className)}
|
|
26
|
+
weight="bold"
|
|
27
|
+
aria-hidden="true"
|
|
28
|
+
/>
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// =========================
|
|
3
|
+
// IMPORTS
|
|
4
|
+
// =========================
|
|
5
|
+
import * as ContextMenu from "../../shadcn/context-menu/index.js";
|
|
6
|
+
import { HoldToConfirm } from "../hold-to-confirm/index.js";
|
|
7
|
+
import { ArrowUpRight, Copy, LinkSimple, Trash } from "phosphor-svelte";
|
|
8
|
+
import type { Snippet } from "svelte";
|
|
9
|
+
|
|
10
|
+
// =========================
|
|
11
|
+
// TYPES
|
|
12
|
+
// =========================
|
|
13
|
+
type Props = {
|
|
14
|
+
onDelete: () => void;
|
|
15
|
+
href?: string;
|
|
16
|
+
id?: string;
|
|
17
|
+
deleteLabel?: string;
|
|
18
|
+
copyLinkLabel?: string;
|
|
19
|
+
copyIdLabel?: string;
|
|
20
|
+
/** Called after a successful clipboard copy so consumers can show their own toast */
|
|
21
|
+
onCopied?: () => void;
|
|
22
|
+
children: Snippet<[Record<string, unknown>]>;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// =========================
|
|
26
|
+
// PROPS
|
|
27
|
+
// =========================
|
|
28
|
+
let {
|
|
29
|
+
onDelete,
|
|
30
|
+
href,
|
|
31
|
+
id,
|
|
32
|
+
deleteLabel = "Delete",
|
|
33
|
+
copyLinkLabel = "Copy link",
|
|
34
|
+
copyIdLabel = "Copy ID",
|
|
35
|
+
onCopied,
|
|
36
|
+
children,
|
|
37
|
+
}: Props = $props();
|
|
38
|
+
|
|
39
|
+
// =========================
|
|
40
|
+
// FUNCTIONS
|
|
41
|
+
// =========================
|
|
42
|
+
function handleOpen() {
|
|
43
|
+
if (href) window.location.href = href;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// WHY the whole URL and not the path: what leaves this menu is pasted into a
|
|
47
|
+
// message, a doc or an agent prompt, none of which can resolve "/clients/42".
|
|
48
|
+
async function handleCopyLink() {
|
|
49
|
+
if (!href) return;
|
|
50
|
+
await navigator.clipboard.writeText(new URL(href, window.location.origin).href);
|
|
51
|
+
onCopied?.();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function handleCopyId() {
|
|
55
|
+
if (!id) return;
|
|
56
|
+
await navigator.clipboard.writeText(id);
|
|
57
|
+
onCopied?.();
|
|
58
|
+
}
|
|
59
|
+
</script>
|
|
60
|
+
|
|
61
|
+
<!-- =========================== -->
|
|
62
|
+
<!-- MARKUP -->
|
|
63
|
+
<!-- =========================== -->
|
|
64
|
+
<ContextMenu.Root>
|
|
65
|
+
<ContextMenu.Trigger>
|
|
66
|
+
{#snippet child({ props })}
|
|
67
|
+
{@render children(props)}
|
|
68
|
+
{/snippet}
|
|
69
|
+
</ContextMenu.Trigger>
|
|
70
|
+
<ContextMenu.Content class="w-48">
|
|
71
|
+
{#if href}
|
|
72
|
+
<ContextMenu.Item onclick={handleOpen}>
|
|
73
|
+
<ArrowUpRight size={14} />
|
|
74
|
+
Open
|
|
75
|
+
</ContextMenu.Item>
|
|
76
|
+
{/if}
|
|
77
|
+
{#if href}
|
|
78
|
+
<ContextMenu.Item onclick={handleCopyLink}>
|
|
79
|
+
<LinkSimple size={14} />
|
|
80
|
+
{copyLinkLabel}
|
|
81
|
+
</ContextMenu.Item>
|
|
82
|
+
{/if}
|
|
83
|
+
{#if id}
|
|
84
|
+
<ContextMenu.Item onclick={handleCopyId}>
|
|
85
|
+
<Copy size={14} />
|
|
86
|
+
{copyIdLabel}
|
|
87
|
+
</ContextMenu.Item>
|
|
88
|
+
{/if}
|
|
89
|
+
{#if href || id}
|
|
90
|
+
<ContextMenu.Separator />
|
|
91
|
+
{/if}
|
|
92
|
+
<HoldToConfirm
|
|
93
|
+
duration={800}
|
|
94
|
+
onConfirm={onDelete}
|
|
95
|
+
class="relative flex w-full cursor-default items-center justify-start gap-2 rounded-md px-1.5 py-1 text-sm text-destructive outline-hidden"
|
|
96
|
+
>
|
|
97
|
+
{#snippet idle()}
|
|
98
|
+
<Trash size={14} />
|
|
99
|
+
{deleteLabel}
|
|
100
|
+
{/snippet}
|
|
101
|
+
{#snippet holding()}
|
|
102
|
+
Hold to delete...
|
|
103
|
+
{/snippet}
|
|
104
|
+
</HoldToConfirm>
|
|
105
|
+
</ContextMenu.Content>
|
|
106
|
+
</ContextMenu.Root>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { existsSync, 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 sourcePath = resolve(here, "row-context-menu.svelte");
|
|
8
|
+
if (!existsSync(sourcePath)) {
|
|
9
|
+
throw new Error(`Unable to find source file: ${sourcePath}`);
|
|
10
|
+
}
|
|
11
|
+
const source = readFileSync(sourcePath, "utf8");
|
|
12
|
+
|
|
13
|
+
describe("row context menu delete affordance", () => {
|
|
14
|
+
it("renders the hold-to-delete action at full menu-item width", () => {
|
|
15
|
+
expect(source).toContain("w-full");
|
|
16
|
+
expect(source).toContain("justify-start");
|
|
17
|
+
});
|
|
18
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
<script lang="ts" generics="T">
|
|
2
|
+
import type { Snippet } from "svelte";
|
|
3
|
+
import { CaretDown, CaretUp, CaretUpDown } from "phosphor-svelte";
|
|
4
|
+
import { cn } from "@heyramzi/utils";
|
|
5
|
+
import type { SortDirection, SortState } from "./data-table.js";
|
|
6
|
+
|
|
7
|
+
// ========================= TYPES
|
|
8
|
+
type Props = {
|
|
9
|
+
key: keyof T & string;
|
|
10
|
+
sort: SortState<T>;
|
|
11
|
+
onSort: (key: keyof T & string) => void;
|
|
12
|
+
class?: string;
|
|
13
|
+
children: Snippet;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// ========================= PROPS
|
|
17
|
+
let { key, sort, onSort, class: className, children }: Props = $props();
|
|
18
|
+
|
|
19
|
+
// ========================= DERIVED
|
|
20
|
+
let active = $derived(sort?.key === key);
|
|
21
|
+
let direction = $derived<SortDirection | null>(sort && active ? sort.direction : null);
|
|
22
|
+
let ariaSort = $derived<"ascending" | "descending" | "none">(
|
|
23
|
+
direction === "asc" ? "ascending" : direction === "desc" ? "descending" : "none",
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
function handleKeydown(e: KeyboardEvent) {
|
|
27
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
28
|
+
e.preventDefault();
|
|
29
|
+
onSort(key);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
</script>
|
|
33
|
+
|
|
34
|
+
<th
|
|
35
|
+
data-slot="table-head"
|
|
36
|
+
class={cn(
|
|
37
|
+
"text-muted-foreground h-8 px-2 text-left align-middle text-xs font-medium whitespace-nowrap select-none",
|
|
38
|
+
"cursor-pointer hover:bg-muted/30 transition-colors",
|
|
39
|
+
className,
|
|
40
|
+
)}
|
|
41
|
+
tabindex="0"
|
|
42
|
+
aria-sort={ariaSort}
|
|
43
|
+
onclick={() => onSort(key)}
|
|
44
|
+
onkeydown={handleKeydown}
|
|
45
|
+
>
|
|
46
|
+
<span class="inline-flex items-center gap-1">
|
|
47
|
+
{@render children()}
|
|
48
|
+
{#if direction === "asc"}
|
|
49
|
+
<CaretUp class="size-3.5 text-foreground" weight="bold" />
|
|
50
|
+
{:else if direction === "desc"}
|
|
51
|
+
<CaretDown class="size-3.5 text-foreground" weight="bold" />
|
|
52
|
+
{:else}
|
|
53
|
+
<CaretUpDown class="size-3.5 text-muted-foreground/50" />
|
|
54
|
+
{/if}
|
|
55
|
+
</span>
|
|
56
|
+
</th>
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// ========================= IMPORTS
|
|
3
|
+
import { Skeleton } from "../../shadcn/skeleton/index.js";
|
|
4
|
+
|
|
5
|
+
// ========================= TYPES
|
|
6
|
+
type ColumnDef = {
|
|
7
|
+
width?: string;
|
|
8
|
+
align?: "left" | "center" | "right";
|
|
9
|
+
/** Show a small circle before the skeleton bar (e.g. logo/avatar column) */
|
|
10
|
+
circle?: boolean;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
type Props = {
|
|
14
|
+
/** Number of skeleton rows to render */
|
|
15
|
+
rows?: number;
|
|
16
|
+
/** Column definitions control skeleton widths and alignment */
|
|
17
|
+
columns?: ColumnDef[];
|
|
18
|
+
/** Simple mode: just specify column count */
|
|
19
|
+
columnCount?: number;
|
|
20
|
+
class?: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// ========================= PROPS
|
|
24
|
+
let {
|
|
25
|
+
rows = 5,
|
|
26
|
+
columns,
|
|
27
|
+
columnCount = 6,
|
|
28
|
+
class: className,
|
|
29
|
+
}: Props = $props();
|
|
30
|
+
|
|
31
|
+
// ========================= DERIVED
|
|
32
|
+
let cols = $derived(
|
|
33
|
+
columns ??
|
|
34
|
+
Array.from<unknown, ColumnDef>({ length: columnCount }, () => ({
|
|
35
|
+
width: undefined,
|
|
36
|
+
align: "left" as const,
|
|
37
|
+
})),
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
const WIDTHS = ["w-24", "w-20", "w-28", "w-16", "w-32", "w-20"];
|
|
41
|
+
</script>
|
|
42
|
+
|
|
43
|
+
<!-- =========================== -->
|
|
44
|
+
<!-- MARKUP -->
|
|
45
|
+
<!-- =========================== -->
|
|
46
|
+
<tbody class={className}>
|
|
47
|
+
{#each { length: rows } as _, rowIdx (rowIdx)}
|
|
48
|
+
<tr
|
|
49
|
+
data-slot="table-row"
|
|
50
|
+
class="border-b border-border/50 transition-opacity"
|
|
51
|
+
style="animation: skeleton-fade-in var(--duration-slow) var(--ease-out) calc(var(--duration-stagger) * {rowIdx}) both"
|
|
52
|
+
>
|
|
53
|
+
{#each cols as col, colIdx (colIdx)}
|
|
54
|
+
<td
|
|
55
|
+
data-slot="table-cell"
|
|
56
|
+
class="h-11 px-2 {col.align === 'center' ? 'text-center' : col.align === 'right' ? 'text-right' : ''}"
|
|
57
|
+
>
|
|
58
|
+
<div class="flex items-center gap-2 {col.align === 'center' ? 'justify-center' : col.align === 'right' ? 'justify-end' : ''}">
|
|
59
|
+
{#if col.circle}
|
|
60
|
+
<Skeleton class="size-6 shrink-0 rounded-md" />
|
|
61
|
+
{/if}
|
|
62
|
+
<Skeleton
|
|
63
|
+
class="h-3.5 rounded {col.width ?? WIDTHS[(rowIdx + colIdx) % WIDTHS.length]}"
|
|
64
|
+
/>
|
|
65
|
+
</div>
|
|
66
|
+
</td>
|
|
67
|
+
{/each}
|
|
68
|
+
</tr>
|
|
69
|
+
{/each}
|
|
70
|
+
</tbody>
|
|
71
|
+
|
|
72
|
+
<style>
|
|
73
|
+
@keyframes skeleton-fade-in {
|
|
74
|
+
from {
|
|
75
|
+
opacity: 0;
|
|
76
|
+
transform: translateY(4px);
|
|
77
|
+
}
|
|
78
|
+
to {
|
|
79
|
+
opacity: 1;
|
|
80
|
+
transform: translateY(0);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
</style>
|