@warkypublic/svelix 0.1.44 → 0.1.46
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/CardGrid/CardGrid.svelte +1312 -0
- package/dist/components/CardGrid/CardGrid.svelte.d.ts +61 -0
- package/dist/components/CardGrid/CardGridFilterPanel.svelte +299 -0
- package/dist/components/CardGrid/CardGridFilterPanel.svelte.d.ts +11 -0
- package/dist/components/CardGrid/DefaultCard.svelte +303 -0
- package/dist/components/CardGrid/DefaultCard.svelte.d.ts +10 -0
- package/dist/components/CardGrid/ImageCardStory.svelte +257 -0
- package/dist/components/CardGrid/ImageCardStory.svelte.d.ts +18 -0
- package/dist/components/CardGrid/cardGrid.d.ts +33 -0
- package/dist/components/CardGrid/cardGrid.js +21 -0
- package/dist/components/CardGrid/index.d.ts +4 -0
- package/dist/components/CardGrid/index.js +4 -0
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.js +1 -0
- package/llm/COMPONENT_GUIDE.md +240 -0
- package/llm/plans/card_grid.plan.md +305 -0
- package/package.json +19 -18
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import CardGrid from './CardGrid.svelte';
|
|
3
|
+
import type { CardGridColumn, CardSortOption } from './cardGrid.js';
|
|
4
|
+
|
|
5
|
+
const columns: CardGridColumn[] = [
|
|
6
|
+
{ id: 'id', title: 'ID', disableSearch: true, disableFilter: true },
|
|
7
|
+
{ id: 'title', title: 'Title' },
|
|
8
|
+
{ id: 'photographer', title: 'Photographer' },
|
|
9
|
+
{ id: 'category', title: 'Category' },
|
|
10
|
+
{ id: 'likes', title: 'Likes', format: 'number' },
|
|
11
|
+
{ id: 'views', title: 'Views', format: 'number' },
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
const sortOptions: CardSortOption[] = [
|
|
15
|
+
{ columnId: 'title', label: 'Title' },
|
|
16
|
+
{ columnId: 'photographer', label: 'Photographer' },
|
|
17
|
+
{ columnId: 'likes', label: 'Likes' },
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
const CATEGORIES = ['Architecture', 'Nature', 'People', 'Travel', 'Abstract', 'Animals'];
|
|
21
|
+
const PHOTOGRAPHERS = ['Alice Monroe', 'Ben Carter', 'Cara Lee', 'Daniel Park', 'Emma Rios', 'Felix Braun'];
|
|
22
|
+
const TITLES = [
|
|
23
|
+
'Golden Hour', 'Morning Mist', 'Urban Jungle', 'Still Waters', 'First Light',
|
|
24
|
+
'Hidden Path', 'City Pulse', 'Wild Plains', 'Quiet Corner', 'Storm Front',
|
|
25
|
+
'Desert Bloom', 'Arctic Echo', 'Neon Drift', 'Forest Floor', 'Tidal Wave',
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
const data = Array.from({ length: 60 }, (_, i) => ({
|
|
29
|
+
id: i + 1,
|
|
30
|
+
title: `${TITLES[i % TITLES.length]} ${Math.floor(i / TITLES.length) || ''}`.trim(),
|
|
31
|
+
photographer: PHOTOGRAPHERS[i % PHOTOGRAPHERS.length],
|
|
32
|
+
category: CATEGORIES[i % CATEGORIES.length],
|
|
33
|
+
likes: Math.floor(Math.random() * 4800 + 200),
|
|
34
|
+
views: Math.floor(Math.random() * 48000 + 2000),
|
|
35
|
+
imageUrl: `https://picsum.photos/seed/${i + 1}/400/260`,
|
|
36
|
+
}));
|
|
37
|
+
</script>
|
|
38
|
+
|
|
39
|
+
<!-- cardMaxFrontColumns=3 splits: front=[id,title,photographer], back=[category,likes,views] -->
|
|
40
|
+
<CardGrid
|
|
41
|
+
{columns}
|
|
42
|
+
{sortOptions}
|
|
43
|
+
{data}
|
|
44
|
+
cardMinWidth={260}
|
|
45
|
+
cardMaxFrontColumns={3}
|
|
46
|
+
pageSize={20}
|
|
47
|
+
searchPlaceholder="Search photos…"
|
|
48
|
+
>
|
|
49
|
+
{#snippet card(record, _i, selected, _focused)}
|
|
50
|
+
<article class="ic-card" class:ic-selected={selected}>
|
|
51
|
+
<div class="ic-image-wrap">
|
|
52
|
+
<img
|
|
53
|
+
src={record.imageUrl as string}
|
|
54
|
+
alt={record.title as string}
|
|
55
|
+
class="ic-image"
|
|
56
|
+
loading="lazy"
|
|
57
|
+
/>
|
|
58
|
+
<span class="ic-category">{record.category as string}</span>
|
|
59
|
+
</div>
|
|
60
|
+
<div class="ic-body">
|
|
61
|
+
<p class="ic-title">{record.title as string}</p>
|
|
62
|
+
<p class="ic-photographer">by {record.photographer as string}</p>
|
|
63
|
+
</div>
|
|
64
|
+
</article>
|
|
65
|
+
{/snippet}
|
|
66
|
+
|
|
67
|
+
{#snippet cardBack(record, _i, selected, _focused)}
|
|
68
|
+
<article class="ic-card ic-back" class:ic-selected={selected}>
|
|
69
|
+
<div class="ic-back-image-wrap">
|
|
70
|
+
<img
|
|
71
|
+
src={record.imageUrl as string}
|
|
72
|
+
alt={record.title as string}
|
|
73
|
+
class="ic-back-image"
|
|
74
|
+
loading="lazy"
|
|
75
|
+
/>
|
|
76
|
+
<div class="ic-back-overlay">
|
|
77
|
+
<p class="ic-back-title">{record.title as string}</p>
|
|
78
|
+
<p class="ic-back-photographer">by {record.photographer as string}</p>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
<div class="ic-body ic-back-body">
|
|
82
|
+
<div class="ic-back-stat">
|
|
83
|
+
<svg width="14" height="14" viewBox="0 0 20 20" fill="none" aria-hidden="true">
|
|
84
|
+
<path d="M3 7h14M3 10h10M3 13h6" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/>
|
|
85
|
+
</svg>
|
|
86
|
+
<span class="ic-back-label">Category</span>
|
|
87
|
+
<span class="ic-back-value">{record.category as string}</span>
|
|
88
|
+
</div>
|
|
89
|
+
<div class="ic-back-stat">
|
|
90
|
+
<svg width="14" height="14" viewBox="0 0 20 20" fill="none" aria-hidden="true">
|
|
91
|
+
<path d="M10 4C5 4 1 10 1 10s4 6 9 6 9-6 9-6-4-6-9-6Z" stroke="currentColor" stroke-width="1.6"/>
|
|
92
|
+
<circle cx="10" cy="10" r="3" stroke="currentColor" stroke-width="1.6"/>
|
|
93
|
+
</svg>
|
|
94
|
+
<span class="ic-back-label">Views</span>
|
|
95
|
+
<span class="ic-back-value">{(record.views as number).toLocaleString()}</span>
|
|
96
|
+
</div>
|
|
97
|
+
<div class="ic-back-stat">
|
|
98
|
+
<svg width="14" height="14" viewBox="0 0 20 20" fill="none" aria-hidden="true">
|
|
99
|
+
<path d="M10 17s-8-5-8-10a5 5 0 0 1 8-4 5 5 0 0 1 8 4c0 5-8 10-8 10Z" stroke="currentColor" stroke-width="1.6"/>
|
|
100
|
+
</svg>
|
|
101
|
+
<span class="ic-back-label">Likes</span>
|
|
102
|
+
<span class="ic-back-value">{(record.likes as number).toLocaleString()}</span>
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
</article>
|
|
106
|
+
{/snippet}
|
|
107
|
+
</CardGrid>
|
|
108
|
+
|
|
109
|
+
<style>
|
|
110
|
+
/* ── Shared ───────────────────────────────────────────────────────────── */
|
|
111
|
+
|
|
112
|
+
.ic-card {
|
|
113
|
+
display: flex;
|
|
114
|
+
flex-direction: column;
|
|
115
|
+
height: 100%;
|
|
116
|
+
overflow: hidden;
|
|
117
|
+
border-radius: 10px;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.ic-body {
|
|
121
|
+
padding: 12px 14px 14px;
|
|
122
|
+
display: flex;
|
|
123
|
+
flex-direction: column;
|
|
124
|
+
gap: 3px;
|
|
125
|
+
flex: 1;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/* ── Front face ───────────────────────────────────────────────────────── */
|
|
129
|
+
|
|
130
|
+
.ic-image-wrap {
|
|
131
|
+
position: relative;
|
|
132
|
+
overflow: hidden;
|
|
133
|
+
aspect-ratio: 16 / 10;
|
|
134
|
+
background: var(--cg-border-subtle, #f4f6f9);
|
|
135
|
+
flex-shrink: 0;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.ic-image {
|
|
139
|
+
width: 100%;
|
|
140
|
+
height: 100%;
|
|
141
|
+
object-fit: cover;
|
|
142
|
+
display: block;
|
|
143
|
+
transition: transform 300ms ease;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.ic-card:hover .ic-image {
|
|
147
|
+
transform: scale(1.04);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.ic-category {
|
|
151
|
+
position: absolute;
|
|
152
|
+
top: 10px;
|
|
153
|
+
left: 10px;
|
|
154
|
+
background: rgba(0, 0, 0, 0.48);
|
|
155
|
+
color: #fff;
|
|
156
|
+
font-size: 0.7rem;
|
|
157
|
+
font-weight: 600;
|
|
158
|
+
letter-spacing: 0.04em;
|
|
159
|
+
text-transform: uppercase;
|
|
160
|
+
padding: 3px 8px;
|
|
161
|
+
border-radius: 999px;
|
|
162
|
+
backdrop-filter: blur(4px);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.ic-title {
|
|
166
|
+
font-size: 0.9rem;
|
|
167
|
+
font-weight: 600;
|
|
168
|
+
color: var(--cg-text, #0f172a);
|
|
169
|
+
margin: 0;
|
|
170
|
+
white-space: nowrap;
|
|
171
|
+
overflow: hidden;
|
|
172
|
+
text-overflow: ellipsis;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.ic-photographer {
|
|
176
|
+
font-size: 0.78rem;
|
|
177
|
+
color: var(--cg-text-muted, #64748b);
|
|
178
|
+
margin: 0;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.ic-selected .ic-image-wrap::after {
|
|
182
|
+
content: '';
|
|
183
|
+
position: absolute;
|
|
184
|
+
inset: 0;
|
|
185
|
+
box-shadow: inset 0 0 0 3px var(--cg-accent, #6366f1);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/* ── Back face ────────────────────────────────────────────────────────── */
|
|
189
|
+
|
|
190
|
+
.ic-back-image-wrap {
|
|
191
|
+
position: relative;
|
|
192
|
+
overflow: hidden;
|
|
193
|
+
aspect-ratio: 16 / 10;
|
|
194
|
+
flex-shrink: 0;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.ic-back-image {
|
|
198
|
+
width: 100%;
|
|
199
|
+
height: 100%;
|
|
200
|
+
object-fit: cover;
|
|
201
|
+
display: block;
|
|
202
|
+
filter: brightness(0.45) saturate(0.7);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
.ic-back-overlay {
|
|
206
|
+
position: absolute;
|
|
207
|
+
inset: 0;
|
|
208
|
+
display: flex;
|
|
209
|
+
flex-direction: column;
|
|
210
|
+
justify-content: flex-end;
|
|
211
|
+
padding: 12px 14px;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.ic-back-title {
|
|
215
|
+
font-size: 0.95rem;
|
|
216
|
+
font-weight: 700;
|
|
217
|
+
color: #fff;
|
|
218
|
+
margin: 0;
|
|
219
|
+
line-height: 1.3;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.ic-back-photographer {
|
|
223
|
+
font-size: 0.75rem;
|
|
224
|
+
color: rgba(255,255,255,.75);
|
|
225
|
+
margin: 0;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.ic-back-body {
|
|
229
|
+
gap: 10px;
|
|
230
|
+
justify-content: center;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.ic-back-stat {
|
|
234
|
+
display: flex;
|
|
235
|
+
align-items: center;
|
|
236
|
+
gap: 8px;
|
|
237
|
+
color: var(--cg-text-muted, #64748b);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
.ic-back-label {
|
|
241
|
+
font-size: 0.78rem;
|
|
242
|
+
flex: 1;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
.ic-back-value {
|
|
246
|
+
font-size: 0.82rem;
|
|
247
|
+
font-weight: 600;
|
|
248
|
+
color: var(--cg-text, #0f172a);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
.ic-selected .ic-back-image-wrap::after {
|
|
252
|
+
content: '';
|
|
253
|
+
position: absolute;
|
|
254
|
+
inset: 0;
|
|
255
|
+
box-shadow: inset 0 0 0 3px var(--cg-accent, #6366f1);
|
|
256
|
+
}
|
|
257
|
+
</style>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
|
|
2
|
+
new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
|
|
3
|
+
$$bindings?: Bindings;
|
|
4
|
+
} & Exports;
|
|
5
|
+
(internal: unknown, props: {
|
|
6
|
+
$$events?: Events;
|
|
7
|
+
$$slots?: Slots;
|
|
8
|
+
}): Exports & {
|
|
9
|
+
$set?: any;
|
|
10
|
+
$on?: any;
|
|
11
|
+
};
|
|
12
|
+
z_$$bindings?: Bindings;
|
|
13
|
+
}
|
|
14
|
+
declare const ImageCardStory: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
|
|
15
|
+
[evt: string]: CustomEvent<any>;
|
|
16
|
+
}, {}, {}, string>;
|
|
17
|
+
type ImageCardStory = InstanceType<typeof ImageCardStory>;
|
|
18
|
+
export default ImageCardStory;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { GridColumnFormat } from '../Types/generic_grid.js';
|
|
2
|
+
export type { GridColumnSortOrder, GridColumnFilters } from '../Types/generic_grid.js';
|
|
3
|
+
export type { GridlerAdapterConfig, GridlerPageResult } from '../Gridler/types.js';
|
|
4
|
+
export interface CardGridColumn {
|
|
5
|
+
id: string;
|
|
6
|
+
title: string;
|
|
7
|
+
/** Field path on the record; defaults to id. Supports dot notation. */
|
|
8
|
+
dataKey?: string;
|
|
9
|
+
format?: GridColumnFormat;
|
|
10
|
+
disableSort?: boolean;
|
|
11
|
+
disableFilter?: boolean;
|
|
12
|
+
disableSearch?: boolean;
|
|
13
|
+
}
|
|
14
|
+
export interface CardSortOption {
|
|
15
|
+
columnId: string;
|
|
16
|
+
label: string;
|
|
17
|
+
}
|
|
18
|
+
export interface CardGridContextMenuItem {
|
|
19
|
+
id: string;
|
|
20
|
+
label: string;
|
|
21
|
+
disabled?: boolean;
|
|
22
|
+
icon?: string;
|
|
23
|
+
kind?: 'item' | 'separator';
|
|
24
|
+
onselect?: (rowData?: Record<string, unknown>) => void;
|
|
25
|
+
}
|
|
26
|
+
/** Add or remove an item from the selection by uniqueID match. */
|
|
27
|
+
export declare function toggleItemInSelection(items: Record<string, unknown>[], item: Record<string, unknown>, uniqueID: string): Record<string, unknown>[];
|
|
28
|
+
/** Return all items between anchor and target indices, inclusive. */
|
|
29
|
+
export declare function rangeSelect(allItems: Record<string, unknown>[], anchorIndex: number, targetIndex: number): Record<string, unknown>[];
|
|
30
|
+
/** Returns true if item is in selectedItems by uniqueID. */
|
|
31
|
+
export declare function isItemSelected(selectedItems: Record<string, unknown>[], item: Record<string, unknown>, uniqueID: string): boolean;
|
|
32
|
+
/** Compute how many cards fit in a row given container width, min card width, and gap. */
|
|
33
|
+
export declare function computeColumnsInRow(containerWidth: number, cardMinWidth: number, gap: number): number;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Add or remove an item from the selection by uniqueID match. */
|
|
2
|
+
export function toggleItemInSelection(items, item, uniqueID) {
|
|
3
|
+
const idx = items.findIndex((i) => i[uniqueID] === item[uniqueID]);
|
|
4
|
+
if (idx === -1)
|
|
5
|
+
return [...items, item];
|
|
6
|
+
return items.filter((_, i) => i !== idx);
|
|
7
|
+
}
|
|
8
|
+
/** Return all items between anchor and target indices, inclusive. */
|
|
9
|
+
export function rangeSelect(allItems, anchorIndex, targetIndex) {
|
|
10
|
+
const start = Math.min(anchorIndex, targetIndex);
|
|
11
|
+
const end = Math.max(anchorIndex, targetIndex);
|
|
12
|
+
return allItems.slice(start, end + 1);
|
|
13
|
+
}
|
|
14
|
+
/** Returns true if item is in selectedItems by uniqueID. */
|
|
15
|
+
export function isItemSelected(selectedItems, item, uniqueID) {
|
|
16
|
+
return selectedItems.some((s) => s[uniqueID] === item[uniqueID]);
|
|
17
|
+
}
|
|
18
|
+
/** Compute how many cards fit in a row given container width, min card width, and gap. */
|
|
19
|
+
export function computeColumnsInRow(containerWidth, cardMinWidth, gap) {
|
|
20
|
+
return Math.max(1, Math.floor((containerWidth + gap) / (cardMinWidth + gap)));
|
|
21
|
+
}
|
package/dist/components/index.js
CHANGED
package/llm/COMPONENT_GUIDE.md
CHANGED
|
@@ -767,6 +767,246 @@ Story behavior worth preserving:
|
|
|
767
767
|
- markdown, Monaco/text, media, and PDF flows are all real supported paths
|
|
768
768
|
- office-document integrations expose `insertText` and `insertHtml` hooks for external automation
|
|
769
769
|
|
|
770
|
+
### CardGrid: virtualized card-based data grid
|
|
771
|
+
|
|
772
|
+
Based on:
|
|
773
|
+
- [src/lib/components/CardGrid/CardGrid.svelte](/home/hein/dev/svelix/src/lib/components/CardGrid/CardGrid.svelte)
|
|
774
|
+
- [src/lib/components/CardGrid/CardGrid.stories.ts](/home/hein/dev/svelix/src/lib/components/CardGrid/CardGrid.stories.ts)
|
|
775
|
+
|
|
776
|
+
A responsive, virtualized card grid with search, sort, filter, multi-select, context menus, and optional flip mode. Renders a toolbar above a scrollable grid of cards. Supports local data, async local data, and server-side paging via ResolveSpec or HeaderSpec adapters — the same adapter layer used by `GridlerFull`.
|
|
777
|
+
|
|
778
|
+
The grid requires a bounded height on its scroll container. Always place it inside an element that has an explicit height, or use the `height: 100%` + flex approach shown below.
|
|
779
|
+
|
|
780
|
+
#### Minimal local-data example
|
|
781
|
+
|
|
782
|
+
```svelte
|
|
783
|
+
<script lang="ts">
|
|
784
|
+
import { CardGrid } from '@warkypublic/svelix';
|
|
785
|
+
import type { CardGridColumn } from '@warkypublic/svelix';
|
|
786
|
+
|
|
787
|
+
const columns: CardGridColumn[] = [
|
|
788
|
+
{ id: 'id', title: 'ID', disableSearch: true },
|
|
789
|
+
{ id: 'name', title: 'Name' },
|
|
790
|
+
{ id: 'role', title: 'Role' },
|
|
791
|
+
];
|
|
792
|
+
|
|
793
|
+
const data = [
|
|
794
|
+
{ id: 1, name: 'Alice Smith', role: 'Admin' },
|
|
795
|
+
{ id: 2, name: 'Bob Jones', role: 'Viewer' },
|
|
796
|
+
];
|
|
797
|
+
</script>
|
|
798
|
+
|
|
799
|
+
<div style="height: 600px; display: flex; flex-direction: column;">
|
|
800
|
+
<CardGrid {columns} {data} pageSize={20} searchPlaceholder="Search…" />
|
|
801
|
+
</div>
|
|
802
|
+
```
|
|
803
|
+
|
|
804
|
+
#### Props reference
|
|
805
|
+
|
|
806
|
+
| Prop | Type | Default | Description |
|
|
807
|
+
|---|---|---|---|
|
|
808
|
+
| `columns` | `CardGridColumn[]` | — | Column definitions. **Required.** |
|
|
809
|
+
| `displayColumns` | `string[]` | — | Subset of column IDs to show; omit to show all. |
|
|
810
|
+
| `cardMinWidth` | `number` | `280` | Minimum card width in px. Grid fills available columns using `auto-fill`. |
|
|
811
|
+
| `cardGap` | `number` | `16` | Gap between cards in px. |
|
|
812
|
+
| `cardMaxFrontColumns` | `number` | — | Enables flip mode: first N columns on the front face, the rest on the back. |
|
|
813
|
+
| `card` | `Snippet` | — | Custom front-face card snippet. Receives `(record, index, selected, focused)`. |
|
|
814
|
+
| `cardBack` | `Snippet` | — | Custom back-face snippet for flip mode. Same signature as `card`. |
|
|
815
|
+
| `empty` | `Snippet` | — | Rendered when the data set is empty. |
|
|
816
|
+
| `skeleton` | `Snippet` | — | Rendered while the initial load is in flight. |
|
|
817
|
+
| `data` | `Record<string,unknown>[] \| (() => Promise<Record<string,unknown>[]>)` | — | Local array or async factory. Used when `dataSource` is `'local'`. |
|
|
818
|
+
| `dataSource` | `'local' \| 'resolvespec' \| 'headerspec'` | `'local'` | Data source adapter. |
|
|
819
|
+
| `dataSourceOptions` | `object` | — | Adapter config — see table below. |
|
|
820
|
+
| `pageSize` | `number` | `20` | Rows per page / server fetch page size. |
|
|
821
|
+
| `uniqueID` | `string` | `'id'` | Field used for identity in selection and cursor paging. Overridden by `dataSourceOptions.uniqueID`. |
|
|
822
|
+
| `searchValue` | `string` | — | Controlled search string. |
|
|
823
|
+
| `onSearchValueChange` | `(v: string) => void` | — | Fires on every search input change. |
|
|
824
|
+
| `serverSideSearch` | `boolean` | `true` | When true, search sends `ilike` filters to the server instead of filtering locally. |
|
|
825
|
+
| `searchColumns` | `string[]` | — | Column IDs to search. Defaults to all searchable columns. |
|
|
826
|
+
| `searchPlaceholder` | `string` | `'Search…'` | Placeholder text in the search input. |
|
|
827
|
+
| `sortOptions` | `CardSortOption[]` | — | Sort options shown in the toolbar sort menu. |
|
|
828
|
+
| `sortOrder` | `GridColumnSortOrder` | — | Controlled sort state. |
|
|
829
|
+
| `onSortOrderChange` | `(order: GridColumnSortOrder) => void` | — | Fires when sort changes. |
|
|
830
|
+
| `filters` | `GridColumnFilters` | — | Controlled filter state. |
|
|
831
|
+
| `onFilterChange` | `(filters: GridColumnFilters) => void` | — | Fires when filters are applied or cleared. |
|
|
832
|
+
| `selectedItems` | `Record<string,unknown>[]` | — | Controlled selection. |
|
|
833
|
+
| `onSelectedItemsChange` | `(items: Record<string,unknown>[]) => void` | — | Fires when selection changes. |
|
|
834
|
+
| `multiSelect` | `boolean` | `true` | `false` = single-select mode. Arrow keys move the selection. |
|
|
835
|
+
| `menuItems` | `CardGridContextMenuItem[]` | — | Custom context menu items appended to the built-in ones. |
|
|
836
|
+
| `onCardClick` | `(record, index) => void` | — | Fires on single click. |
|
|
837
|
+
| `onCardDblClick` | `(record, index) => void` | — | Fires on double click. |
|
|
838
|
+
| `onCardContextMenu` | `(record, index, x, y) => void` | — | Fires on right-click. `x`/`y` are viewport coordinates. |
|
|
839
|
+
| `total` | `number` | bindable | Server-reported total count. |
|
|
840
|
+
| `loading` | `boolean` | bindable | `true` while any fetch is in flight. |
|
|
841
|
+
| `onLoadError` | `(error: string) => void` | — | Fires on adapter fetch failure. |
|
|
842
|
+
| `toolbarEnd` | `Snippet` | — | Extra content rendered at the right end of the toolbar. |
|
|
843
|
+
| `class` | `string` | — | Extra CSS class on the root element. |
|
|
844
|
+
|
|
845
|
+
#### `CardGridColumn` fields
|
|
846
|
+
|
|
847
|
+
| Field | Type | Description |
|
|
848
|
+
|---|---|---|
|
|
849
|
+
| `id` | `string` | Unique column identifier. Also used as the field key unless `dataKey` is set. |
|
|
850
|
+
| `title` | `string` | Display label. |
|
|
851
|
+
| `dataKey` | `string` | Dot-notation path into the record (e.g. `'login.contact.email'`). Defaults to `id`. |
|
|
852
|
+
| `format` | `'currency' \| 'number' \| 'percentage' \| 'date' \| 'datetime'` | Value formatter applied in the default card renderer. |
|
|
853
|
+
| `disableSort` | `boolean` | Excludes this column from sort options. |
|
|
854
|
+
| `disableFilter` | `boolean` | Excludes this column from the filter panel. |
|
|
855
|
+
| `disableSearch` | `boolean` | Excludes this column from local/server search. |
|
|
856
|
+
|
|
857
|
+
#### `dataSourceOptions` fields
|
|
858
|
+
|
|
859
|
+
| Field | Description |
|
|
860
|
+
|---|---|
|
|
861
|
+
| `url` | API base URL. |
|
|
862
|
+
| `authToken` | Bearer token sent in `Authorization` header. |
|
|
863
|
+
| `schema` | Database schema (ResolveSpec). |
|
|
864
|
+
| `entity` | Table / entity name (ResolveSpec). |
|
|
865
|
+
| `uniqueID` | Unique field for cursor paging. |
|
|
866
|
+
| `hotfields` | Extra fields fetched but not rendered. |
|
|
867
|
+
| `extraOptions` | Passed through to the adapter as `Partial<Options>` — same shape as `GridlerFull`. |
|
|
868
|
+
|
|
869
|
+
#### Server-side paging
|
|
870
|
+
|
|
871
|
+
```svelte
|
|
872
|
+
<CardGrid
|
|
873
|
+
{columns}
|
|
874
|
+
{sortOptions}
|
|
875
|
+
dataSource="resolvespec"
|
|
876
|
+
dataSourceOptions={{
|
|
877
|
+
url: 'https://api.example.com',
|
|
878
|
+
schema: 'public',
|
|
879
|
+
entity: 'employees',
|
|
880
|
+
uniqueID: 'id',
|
|
881
|
+
}}
|
|
882
|
+
pageSize={20}
|
|
883
|
+
serverSideSearch={true}
|
|
884
|
+
searchColumns={['name', 'email']}
|
|
885
|
+
/>
|
|
886
|
+
```
|
|
887
|
+
|
|
888
|
+
Cards are loaded a page at a time. Scrolling to the bottom appends the next page automatically. `total` and `loading` are bindable and update after each fetch.
|
|
889
|
+
|
|
890
|
+
#### Flip mode (default card renderer)
|
|
891
|
+
|
|
892
|
+
Set `cardMaxFrontColumns` to split columns across front and back faces. A `+N` badge appears on cards that have overflow columns; clicking it flips the card to show the remaining fields.
|
|
893
|
+
|
|
894
|
+
```svelte
|
|
895
|
+
<CardGrid
|
|
896
|
+
{columns}
|
|
897
|
+
{data}
|
|
898
|
+
cardMaxFrontColumns={3}
|
|
899
|
+
/>
|
|
900
|
+
```
|
|
901
|
+
|
|
902
|
+
The flip button and the back button are always in the same bottom-right position on their respective faces.
|
|
903
|
+
|
|
904
|
+
#### Custom card snippets
|
|
905
|
+
|
|
906
|
+
Use `card` and `cardBack` snippets to replace the default renderer with fully custom markup. Both receive `(record, index, selected, focused)`.
|
|
907
|
+
|
|
908
|
+
```svelte
|
|
909
|
+
<CardGrid {columns} {data} cardMaxFrontColumns={3} pageSize={20}>
|
|
910
|
+
{#snippet card(record, _i, selected)}
|
|
911
|
+
<article class:selected>
|
|
912
|
+
<img src={record.imageUrl as string} alt={record.title as string} />
|
|
913
|
+
<p>{record.title as string}</p>
|
|
914
|
+
</article>
|
|
915
|
+
{/snippet}
|
|
916
|
+
|
|
917
|
+
{#snippet cardBack(record, _i, selected)}
|
|
918
|
+
<article class:selected>
|
|
919
|
+
<p>Views: {(record.views as number).toLocaleString()}</p>
|
|
920
|
+
<p>Likes: {(record.likes as number).toLocaleString()}</p>
|
|
921
|
+
</article>
|
|
922
|
+
{/snippet}
|
|
923
|
+
</CardGrid>
|
|
924
|
+
```
|
|
925
|
+
|
|
926
|
+
When `cardBack` is provided without `card`, the default renderer is used for the front face and the custom snippet for the back.
|
|
927
|
+
|
|
928
|
+
#### Custom card snippets from a `.ts` story file
|
|
929
|
+
|
|
930
|
+
Svelte snippets cannot be written directly in a `.ts` file. Wrap them in a `.svelte` wrapper component and render that component from the story:
|
|
931
|
+
|
|
932
|
+
```typescript
|
|
933
|
+
// MyStory.stories.ts
|
|
934
|
+
import WrapperComponent from './WrapperComponent.svelte';
|
|
935
|
+
|
|
936
|
+
export const Custom: Story = {
|
|
937
|
+
render: () => ({ Component: WrapperComponent }),
|
|
938
|
+
};
|
|
939
|
+
```
|
|
940
|
+
|
|
941
|
+
```svelte
|
|
942
|
+
<!-- WrapperComponent.svelte -->
|
|
943
|
+
<script lang="ts">
|
|
944
|
+
import CardGrid from './CardGrid.svelte';
|
|
945
|
+
const data = [...];
|
|
946
|
+
const columns = [...];
|
|
947
|
+
</script>
|
|
948
|
+
|
|
949
|
+
<CardGrid {columns} {data} cardMaxFrontColumns={3}>
|
|
950
|
+
{#snippet card(record, _i, selected, focused)}
|
|
951
|
+
...
|
|
952
|
+
{/snippet}
|
|
953
|
+
</CardGrid>
|
|
954
|
+
```
|
|
955
|
+
|
|
956
|
+
#### Multi-select keyboard shortcuts
|
|
957
|
+
|
|
958
|
+
| Key | Behaviour |
|
|
959
|
+
|---|---|
|
|
960
|
+
| `Space` / `Enter` | Toggle selection on the focused card. |
|
|
961
|
+
| `Arrow keys` | Move focus. In `multiSelect: false` mode, also moves selection. |
|
|
962
|
+
| `Ctrl+A` / `Cmd+A` | Select all loaded cards (multi-select only). |
|
|
963
|
+
| `Shift+Click` | Range-select from the last anchor to the clicked card. |
|
|
964
|
+
| `Ctrl+Click` | Toggle a single card without clearing others. |
|
|
965
|
+
|
|
966
|
+
#### Storybook height decorator
|
|
967
|
+
|
|
968
|
+
The virtualizer requires a bounded scroll container. In Storybook, apply this decorator to give the root element a height:
|
|
969
|
+
|
|
970
|
+
```typescript
|
|
971
|
+
const heightDecorator = (Story) => {
|
|
972
|
+
const root = document.getElementById('storybook-root');
|
|
973
|
+
if (root) root.style.cssText = 'height:calc(100vh - 40px);display:flex;flex-direction:column;';
|
|
974
|
+
document.body.style.margin = '0';
|
|
975
|
+
return Story();
|
|
976
|
+
};
|
|
977
|
+
|
|
978
|
+
const meta = {
|
|
979
|
+
decorators: [heightDecorator],
|
|
980
|
+
...
|
|
981
|
+
};
|
|
982
|
+
```
|
|
983
|
+
|
|
984
|
+
#### Large local datasets in stories
|
|
985
|
+
|
|
986
|
+
Generate large datasets lazily to avoid crashing Storybook at module-load time:
|
|
987
|
+
|
|
988
|
+
```typescript
|
|
989
|
+
let rows: ReturnType<typeof makeRow>[] | null = null;
|
|
990
|
+
function getRows() {
|
|
991
|
+
if (!rows) rows = Array.from({ length: 100_000 }, (_, i) => makeRow(i));
|
|
992
|
+
return rows;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
export const HundredThousandRows: Story = {
|
|
996
|
+
render: (args) => ({ Component: CardGrid, props: { ...args, data: getRows() } }),
|
|
997
|
+
};
|
|
998
|
+
```
|
|
999
|
+
|
|
1000
|
+
Story behaviors worth preserving:
|
|
1001
|
+
- `StaticData`: local array, verifies rendering, context menu, and search
|
|
1002
|
+
- `ServerSideCursorPaging`: cursor-forward paging, verifies infinite scroll
|
|
1003
|
+
- `ServerSideSearch`: `ilike` filters sent on every keystroke
|
|
1004
|
+
- `ServerError`: `onLoadError` fires on non-2xx response
|
|
1005
|
+
- `MultiSelect` / `SingleSelect`: selection modes and keyboard shortcuts
|
|
1006
|
+
- `FlipMode`: `cardMaxFrontColumns` splits columns, flip/back buttons match position
|
|
1007
|
+
- `ImageCards`: fully custom `card` + `cardBack` snippets via a wrapper `.svelte` component
|
|
1008
|
+
- `HundredThousandRows`: virtualizer handles 100 000 rows; only visible rows are in the DOM
|
|
1009
|
+
|
|
770
1010
|
### BetterMenu: interaction-first menu behavior
|
|
771
1011
|
|
|
772
1012
|
Based on:
|