@heyramzi/ui 7.0.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 +28 -17
- package/README.md +5 -1
- package/package.json +25 -15
- package/scripts/sync-shadcn.mjs +17 -13
- 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 -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,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>
|