@r2digisolutions/components 0.1.5 → 0.1.7

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.
Files changed (72) hide show
  1. package/README.md +6 -1
  2. package/dist/components/atoms/Checkbox/Checkbox.svelte +10 -3
  3. package/dist/components/atoms/Checkbox/Checkbox.svelte.d.ts +3 -0
  4. package/dist/components/atoms/Input/Input.svelte +17 -2
  5. package/dist/components/atoms/Input/Input.svelte.d.ts +6 -1
  6. package/dist/components/atoms/MarqueeRect/MarqueeRect.svelte +21 -0
  7. package/dist/components/atoms/MarqueeRect/MarqueeRect.svelte.d.ts +8 -0
  8. package/dist/components/atoms/Textarea/Textarea.svelte +4 -1
  9. package/dist/components/atoms/Textarea/Textarea.svelte.d.ts +1 -0
  10. package/dist/components/atoms/Toggle/Toggle.svelte +1 -1
  11. package/dist/components/molecules/BulkActionBar/BulkActionBar.svelte +19 -5
  12. package/dist/components/molecules/CodeEditor/CodeEditor.svelte +13 -10
  13. package/dist/components/molecules/ColorPicker/ColorPicker.svelte +166 -96
  14. package/dist/components/molecules/ConditionalFormatPanel/ConditionalFormatPanel.stories.d.ts +9 -0
  15. package/dist/components/molecules/ConditionalFormatPanel/ConditionalFormatPanel.stories.js +8 -0
  16. package/dist/components/molecules/ConditionalFormatPanel/ConditionalFormatPanel.svelte +412 -0
  17. package/dist/components/molecules/ConditionalFormatPanel/ConditionalFormatPanel.svelte.d.ts +14 -0
  18. package/dist/components/molecules/ConditionalFormatPanel/ConditionalFormatPanelStory.svelte +27 -0
  19. package/dist/components/molecules/ConditionalFormatPanel/ConditionalFormatPanelStory.svelte.d.ts +3 -0
  20. package/dist/components/molecules/ContextMenu/ContextMenu.svelte +73 -21
  21. package/dist/components/molecules/ContextMenu/ContextMenu.svelte.d.ts +8 -1
  22. package/dist/components/molecules/DateTimePicker/DateTimePicker.svelte.d.ts +1 -1
  23. package/dist/components/molecules/FormDateTimePicker/FormDateTimePicker.svelte.d.ts +1 -1
  24. package/dist/components/molecules/Gauge/Gauge.stories.d.ts +1 -1
  25. package/dist/components/molecules/InfiniteScroll/InfiniteScroll.stories.d.ts +1 -1
  26. package/dist/components/organisms/AssetPickerPage/AssetPickerPage.svelte.d.ts +1 -1
  27. package/dist/components/organisms/BacklogPage/BacklogPage.svelte.d.ts +1 -1
  28. package/dist/components/organisms/CalendarApp/CalendarApp.svelte.d.ts +1 -1
  29. package/dist/components/organisms/DataGrid/DataGrid.stories.d.ts +65 -0
  30. package/dist/components/organisms/DataGrid/DataGrid.stories.js +95 -0
  31. package/dist/components/organisms/DataGrid/DataGrid.svelte +2264 -0
  32. package/dist/components/organisms/DataGrid/DataGrid.svelte.d.ts +99 -0
  33. package/dist/components/organisms/DataGrid/DataGridStory.svelte +659 -0
  34. package/dist/components/organisms/DataGrid/DataGridStory.svelte.d.ts +11 -0
  35. package/dist/components/organisms/DataGrid/breakpoints.d.ts +10 -0
  36. package/dist/components/organisms/DataGrid/breakpoints.js +20 -0
  37. package/dist/components/organisms/DataGrid/cellEditors.d.ts +8 -0
  38. package/dist/components/organisms/DataGrid/cellEditors.js +59 -0
  39. package/dist/components/organisms/DataGrid/conditionalFormat.d.ts +11 -0
  40. package/dist/components/organisms/DataGrid/conditionalFormat.js +98 -0
  41. package/dist/components/organisms/DataGrid/filterRows.d.ts +5 -0
  42. package/dist/components/organisms/DataGrid/filterRows.js +60 -0
  43. package/dist/components/organisms/DataGrid/marks.d.ts +9 -0
  44. package/dist/components/organisms/DataGrid/marks.js +91 -0
  45. package/dist/components/organisms/DataGrid/types.d.ts +126 -0
  46. package/dist/components/organisms/DataGrid/types.js +21 -0
  47. package/dist/components/organisms/ResourceStudio/ResourceStudio.stories.d.ts +33 -0
  48. package/dist/components/organisms/ResourceStudio/ResourceStudio.stories.js +56 -0
  49. package/dist/components/organisms/ResourceStudio/ResourceStudio.svelte +830 -0
  50. package/dist/components/organisms/ResourceStudio/ResourceStudio.svelte.d.ts +68 -0
  51. package/dist/components/organisms/ResourceStudio/ResourceStudioStory.svelte +600 -0
  52. package/dist/components/organisms/ResourceStudio/ResourceStudioStory.svelte.d.ts +7 -0
  53. package/dist/components/organisms/ResourceStudio/fieldsToColumns.d.ts +4 -0
  54. package/dist/components/organisms/ResourceStudio/fieldsToColumns.js +20 -0
  55. package/dist/components/organisms/ResourceStudio/fieldsToSchema.d.ts +6 -0
  56. package/dist/components/organisms/ResourceStudio/fieldsToSchema.js +53 -0
  57. package/dist/components/organisms/ResourceStudio/types.d.ts +69 -0
  58. package/dist/components/organisms/ResourceStudio/types.js +15 -0
  59. package/dist/components/organisms/Scheduler/Scheduler.svelte.d.ts +1 -1
  60. package/dist/components/organisms/SchemaForm/SchemaForm.svelte +10 -3
  61. package/dist/components/organisms/SchemaForm/SchemaForm.svelte.d.ts +1 -0
  62. package/dist/components/templates/ResourceStudioTemplate/ResourceStudioTemplate.stories.d.ts +25 -0
  63. package/dist/components/templates/ResourceStudioTemplate/ResourceStudioTemplate.stories.js +18 -0
  64. package/dist/components/templates/ResourceStudioTemplate/ResourceStudioTemplate.svelte +192 -0
  65. package/dist/components/templates/ResourceStudioTemplate/ResourceStudioTemplate.svelte.d.ts +62 -0
  66. package/dist/components/templates/ResourceStudioTemplate/ResourceStudioTemplateStory.svelte +129 -0
  67. package/dist/components/templates/ResourceStudioTemplate/ResourceStudioTemplateStory.svelte.d.ts +7 -0
  68. package/dist/index.d.ts +11 -0
  69. package/dist/index.js +8 -0
  70. package/dist/utils/marqueeSelect.d.ts +48 -0
  71. package/dist/utils/marqueeSelect.js +189 -0
  72. package/package.json +1 -1
@@ -0,0 +1,659 @@
1
+ <script lang="ts">
2
+ import DataGrid from './DataGrid.svelte';
3
+ import type {
4
+ DataGridColumn,
5
+ CellNote,
6
+ GridMark,
7
+ DataGridFilter,
8
+ DataGridViewMode,
9
+ ConditionalFormatRule,
10
+ GridSelection
11
+ } from './types.js';
12
+ import { EMPTY_SELECTION, noteKey } from './types.js';
13
+ import ConditionalFormatPanel from '../../molecules/ConditionalFormatPanel/ConditionalFormatPanel.svelte';
14
+ import Badge from '../../atoms/Badge/Badge.svelte';
15
+ import Button from '../../atoms/Button/Button.svelte';
16
+ import type { BulkAction } from '../../molecules/BulkActionBar/BulkActionBar.svelte';
17
+
18
+ type Example =
19
+ | 'default'
20
+ | 'rowSelection'
21
+ | 'columnAndCell'
22
+ | 'withNotes'
23
+ | 'conditional'
24
+ | 'formatPanel'
25
+ | 'editable'
26
+ | 'pinned'
27
+ | 'large'
28
+ | 'empty'
29
+ | 'loading'
30
+ | 'customDock'
31
+ | 'readOnly'
32
+ | 'marquee'
33
+ | 'contextMenu'
34
+ | 'responsive';
35
+
36
+ let {
37
+ example = 'default',
38
+ sortable = true,
39
+ editable = false,
40
+ compact = false,
41
+ striped = false
42
+ }: {
43
+ example?: Example;
44
+ sortable?: boolean;
45
+ editable?: boolean;
46
+ compact?: boolean;
47
+ striped?: boolean;
48
+ } = $props();
49
+
50
+ type Order = {
51
+ id: string;
52
+ customer: string;
53
+ status: 'paid' | 'pending' | 'failed' | 'refunded';
54
+ amount: number;
55
+ channel: string;
56
+ vip: boolean;
57
+ due: string;
58
+ };
59
+
60
+ const statusOptions = [
61
+ { value: 'paid', label: 'Paid' },
62
+ { value: 'pending', label: 'Pending' },
63
+ { value: 'failed', label: 'Failed' },
64
+ { value: 'refunded', label: 'Refunded' }
65
+ ];
66
+
67
+ const baseColumns: DataGridColumn<Order>[] = [
68
+ { id: 'customer', header: 'Customer', sortable: true, minWidth: '10rem' },
69
+ { id: 'status', header: 'Status', sortable: true, width: '7rem', filterable: true },
70
+ { id: 'amount', header: 'Amount', align: 'right', sortable: true, width: '6rem' },
71
+ { id: 'channel', header: 'Channel', sortable: true, filterable: true }
72
+ ];
73
+
74
+ const responsiveColumns: DataGridColumn<Order>[] = [
75
+ { id: 'customer', header: 'Customer', sortable: true, minWidth: '10rem', pinned: true },
76
+ { id: 'status', header: 'Status', sortable: true, width: '7rem', filterable: true },
77
+ {
78
+ id: 'amount',
79
+ header: 'Amount',
80
+ align: 'right',
81
+ sortable: true,
82
+ width: '6rem',
83
+ hideBelow: 'sm'
84
+ },
85
+ { id: 'channel', header: 'Channel', sortable: true, filterable: true, hideBelow: 'md' },
86
+ { id: 'due', header: 'Due', sortable: true, width: '9rem', hideBelow: 'lg' },
87
+ { id: 'vip', header: 'VIP', width: '5rem', hideBelow: 'lg' }
88
+ ];
89
+
90
+ const editableColumns: DataGridColumn<Order>[] = [
91
+ {
92
+ id: 'customer',
93
+ header: 'Customer',
94
+ sortable: true,
95
+ minWidth: '10rem',
96
+ edit: { type: 'text', placeholder: 'Name' }
97
+ },
98
+ {
99
+ id: 'status',
100
+ header: 'Status',
101
+ sortable: true,
102
+ width: '9rem',
103
+ edit: { type: 'select', options: statusOptions }
104
+ },
105
+ {
106
+ id: 'amount',
107
+ header: 'Amount',
108
+ align: 'right',
109
+ sortable: true,
110
+ width: '7rem',
111
+ edit: { type: 'number', min: 0, step: 1 }
112
+ },
113
+ {
114
+ id: 'vip',
115
+ header: 'VIP',
116
+ width: '5rem',
117
+ edit: { type: 'boolean' }
118
+ },
119
+ {
120
+ id: 'due',
121
+ header: 'Due',
122
+ sortable: true,
123
+ width: '9rem',
124
+ edit: { type: 'date' }
125
+ },
126
+ {
127
+ id: 'channel',
128
+ header: 'Channel',
129
+ sortable: true,
130
+ edit: { type: 'text' }
131
+ }
132
+ ];
133
+
134
+ const pinnedColumns: DataGridColumn<Order>[] = [
135
+ { id: 'customer', header: 'Customer', sortable: true, pinned: true, width: '10rem' },
136
+ { id: 'status', header: 'Status', sortable: true, width: '7rem' },
137
+ { id: 'amount', header: 'Amount', align: 'right', sortable: true, width: '6rem' },
138
+ { id: 'channel', header: 'Channel', sortable: true, width: '8rem' },
139
+ { id: 'region', header: 'Region', sortable: true, width: '8rem', accessor: 'channel' },
140
+ { id: 'notes', header: 'Notes', width: '12rem', accessor: 'customer' }
141
+ ];
142
+
143
+ const seed: Order[] = [
144
+ {
145
+ id: '1',
146
+ customer: 'Ana López',
147
+ status: 'paid',
148
+ amount: 1280,
149
+ channel: 'Web',
150
+ vip: true,
151
+ due: '2026-08-01'
152
+ },
153
+ {
154
+ id: '2',
155
+ customer: 'Carlos Ruiz',
156
+ status: 'pending',
157
+ amount: 420,
158
+ channel: 'App',
159
+ vip: false,
160
+ due: '2026-08-12'
161
+ },
162
+ {
163
+ id: '3',
164
+ customer: 'Maya Chen',
165
+ status: 'failed',
166
+ amount: 89,
167
+ channel: 'Web',
168
+ vip: false,
169
+ due: '2026-07-28'
170
+ },
171
+ {
172
+ id: '4',
173
+ customer: 'Jon Park',
174
+ status: 'paid',
175
+ amount: 2400,
176
+ channel: 'Partner',
177
+ vip: true,
178
+ due: '2026-09-01'
179
+ },
180
+ {
181
+ id: '5',
182
+ customer: 'Sara Vidal',
183
+ status: 'refunded',
184
+ amount: 310,
185
+ channel: 'Web',
186
+ vip: false,
187
+ due: '2026-08-05'
188
+ },
189
+ {
190
+ id: '6',
191
+ customer: 'Luis Ortega',
192
+ status: 'paid',
193
+ amount: 1560,
194
+ channel: 'App',
195
+ vip: true,
196
+ due: '2026-08-20'
197
+ },
198
+ {
199
+ id: '7',
200
+ customer: 'Nora Kim',
201
+ status: 'pending',
202
+ amount: 75,
203
+ channel: 'Web',
204
+ vip: false,
205
+ due: '2026-07-30'
206
+ },
207
+ {
208
+ id: '8',
209
+ customer: 'Evan Brooks',
210
+ status: 'failed',
211
+ amount: 990,
212
+ channel: 'Partner',
213
+ vip: false,
214
+ due: '2026-08-15'
215
+ }
216
+ ];
217
+
218
+ let rows = $state<Order[]>([...seed]);
219
+ let selection = $state<GridSelection>(EMPTY_SELECTION);
220
+ let notes = $state<CellNote[]>([]);
221
+ let marks = $state<GridMark[]>([]);
222
+ let formatRules = $state<ConditionalFormatRule[]>([]);
223
+ let filterQuery = $state('');
224
+ let columnFilters = $state<DataGridFilter[]>([]);
225
+ let expandedRows = $state<string[]>([]);
226
+ let viewMode = $state<DataGridViewMode>('table');
227
+ let lastAction = $state('');
228
+
229
+ const demoNotes: CellNote[] = [
230
+ { rowId: '3', columnId: 'status', text: 'Retry payment tomorrow', author: 'ops' }
231
+ ];
232
+
233
+ const demoMarks: GridMark[] = [
234
+ { rowId: '2', color: 'amber' },
235
+ { rowId: '4', columnId: 'amount', color: 'rose' }
236
+ ];
237
+
238
+ const demoRules: ConditionalFormatRule[] = [
239
+ {
240
+ id: 'r1',
241
+ name: 'Failed orders',
242
+ enabled: true,
243
+ target: 'row',
244
+ when: { op: 'eq', columnId: 'status', value: 'failed' },
245
+ style: { background: '#fee2e2', color: '#991b1b' },
246
+ priority: 1
247
+ },
248
+ {
249
+ id: 'r2',
250
+ name: 'High value',
251
+ enabled: true,
252
+ target: 'cell',
253
+ columnId: 'amount',
254
+ when: { op: 'gt', columnId: 'amount', value: 1000 },
255
+ style: { background: '#dcfce7', color: '#166534', fontWeight: 'semibold' },
256
+ priority: 2
257
+ }
258
+ ];
259
+
260
+ // Reset demo state only when the story variant changes (not on every bind write-back).
261
+ let seededFor = $state<Example | null>(null);
262
+ $effect(() => {
263
+ if (seededFor === example) return;
264
+ seededFor = example;
265
+ const wantsNotes = example === 'withNotes' || example === 'conditional' || example === 'formatPanel';
266
+ const wantsRules =
267
+ example === 'withNotes' || example === 'conditional' || example === 'formatPanel';
268
+ const wantsMarks = example === 'contextMenu' || example === 'columnAndCell' || example === 'withNotes';
269
+ notes = wantsNotes ? demoNotes.map((n) => ({ ...n })) : [];
270
+ marks = wantsMarks ? demoMarks.map((m) => ({ ...m })) : [];
271
+ formatRules = wantsRules ? demoRules.map((r) => ({ ...r })) : [];
272
+ selection = EMPTY_SELECTION;
273
+ filterQuery = '';
274
+ columnFilters = [];
275
+ expandedRows = example === 'responsive' ? ['1'] : [];
276
+ viewMode = 'table';
277
+ });
278
+
279
+ const largeRows = $derived.by(() => {
280
+ if (example !== 'large') return [] as Order[];
281
+ return Array.from({ length: 80 }, (_, i) => ({
282
+ id: String(i + 1),
283
+ customer: `Customer ${i + 1}`,
284
+ status: (['paid', 'pending', 'failed', 'refunded'] as const)[i % 4],
285
+ amount: Math.round(50 + ((i * 37) % 3000)),
286
+ channel: ['Web', 'App', 'Partner'][i % 3],
287
+ vip: i % 5 === 0,
288
+ due: `2026-08-${String((i % 28) + 1).padStart(2, '0')}`
289
+ }));
290
+ });
291
+
292
+ const statusVariant = {
293
+ paid: 'success',
294
+ pending: 'warning',
295
+ failed: 'error',
296
+ refunded: 'default'
297
+ } as const;
298
+
299
+ const expandMeta: Record<
300
+ string,
301
+ {
302
+ owner: string;
303
+ sla: string;
304
+ items: Array<{ sku: string; qty: number; price: number }>;
305
+ activity: Array<{ at: string; text: string }>;
306
+ }
307
+ > = {
308
+ '1': {
309
+ owner: 'Support · Elena',
310
+ sla: 'Next reply in 2h',
311
+ items: [
312
+ { sku: 'PLAN-PRO', qty: 1, price: 980 },
313
+ { sku: 'ADDON-SEATS', qty: 3, price: 100 }
314
+ ],
315
+ activity: [
316
+ { at: '09:12', text: 'Invoice sent to billing@acme.io' },
317
+ { at: 'Yesterday', text: 'VIP flag reviewed — keep priority queue' }
318
+ ]
319
+ },
320
+ '2': {
321
+ owner: 'Billing · Marc',
322
+ sla: 'Awaiting customer',
323
+ items: [{ sku: 'PLAN-STARTER', qty: 1, price: 420 }],
324
+ activity: [
325
+ { at: '11:40', text: 'Payment authorization pending' },
326
+ { at: '10:02', text: 'Checkout completed on App' }
327
+ ]
328
+ },
329
+ '3': {
330
+ owner: 'Ops · Priya',
331
+ sla: 'Retry scheduled',
332
+ items: [{ sku: 'CREDITS-50', qty: 1, price: 89 }],
333
+ activity: [
334
+ { at: '08:15', text: 'Card declined — insufficient funds' },
335
+ { at: '08:16', text: 'Auto-retry queued for tomorrow 09:00' }
336
+ ]
337
+ },
338
+ '4': {
339
+ owner: 'Partners · Alex',
340
+ sla: 'On track',
341
+ items: [
342
+ { sku: 'PLAN-ENT', qty: 1, price: 1900 },
343
+ { sku: 'IMPL-PKG', qty: 1, price: 500 }
344
+ ],
345
+ activity: [
346
+ { at: 'Mon', text: 'Partner portal order confirmed' },
347
+ { at: 'Sun', text: 'Contract countersigned' }
348
+ ]
349
+ },
350
+ '5': {
351
+ owner: 'Support · Elena',
352
+ sla: 'Refund posted',
353
+ items: [{ sku: 'PLAN-STARTER', qty: 1, price: 310 }],
354
+ activity: [
355
+ { at: '14:20', text: 'Refund issued to original card' },
356
+ { at: '13:55', text: 'Customer requested cancel within window' }
357
+ ]
358
+ },
359
+ '6': {
360
+ owner: 'Success · Jo',
361
+ sla: 'Healthy',
362
+ items: [
363
+ { sku: 'PLAN-PRO', qty: 1, price: 980 },
364
+ { sku: 'ADDON-API', qty: 2, price: 290 }
365
+ ],
366
+ activity: [{ at: 'Tue', text: 'Upsell to Pro completed' }]
367
+ },
368
+ '7': {
369
+ owner: 'Billing · Marc',
370
+ sla: 'Follow up today',
371
+ items: [{ sku: 'CREDITS-50', qty: 1, price: 75 }],
372
+ activity: [{ at: '07:50', text: 'Reminder email opened' }]
373
+ },
374
+ '8': {
375
+ owner: 'Ops · Priya',
376
+ sla: 'Investigate gateway',
377
+ items: [{ sku: 'PLAN-PRO', qty: 1, price: 990 }],
378
+ activity: [
379
+ { at: '16:01', text: '3DS challenge timed out' },
380
+ { at: '16:02', text: 'Escalated to payments channel' }
381
+ ]
382
+ }
383
+ };
384
+
385
+ function rowExpand(row: Order) {
386
+ return (
387
+ expandMeta[row.id] ?? {
388
+ owner: 'Unassigned',
389
+ sla: '—',
390
+ items: [{ sku: 'ITEM', qty: 1, price: row.amount }],
391
+ activity: [{ at: '—', text: 'No recent activity' }]
392
+ }
393
+ );
394
+ }
395
+
396
+ function rowNotes(rowId: string) {
397
+ return notes.filter((n) => n.rowId === rowId);
398
+ }
399
+
400
+ const customActions: BulkAction[] = [
401
+ { id: 'assign', label: 'Assign', variant: 'secondary' },
402
+ { id: 'tag', label: 'Tag', variant: 'secondary' },
403
+ { id: 'archive', label: 'Archive', variant: 'secondary' },
404
+ { id: 'export', label: 'Export CSV', variant: 'primary' },
405
+ { id: 'delete', label: 'Delete', variant: 'destructive', confirm: true }
406
+ ];
407
+
408
+ const isEmpty = $derived(example === 'empty');
409
+ const isLoading = $derived(example === 'loading');
410
+ const isLarge = $derived(example === 'large');
411
+ const isReadOnly = $derived(example === 'readOnly');
412
+ const usePinned = $derived(example === 'pinned');
413
+ const useResponsive = $derived(example === 'responsive');
414
+ const useEditableCols = $derived(example === 'editable' || example === 'contextMenu');
415
+ const useCustomDock = $derived(example === 'customDock' || example === 'contextMenu');
416
+ const canEdit = $derived(editable || example === 'editable' || example === 'contextMenu');
417
+ const activeRows = $derived(isEmpty ? [] : isLarge ? largeRows : rows);
418
+ const activeColumns = $derived(
419
+ usePinned
420
+ ? pinnedColumns
421
+ : useResponsive
422
+ ? responsiveColumns
423
+ : useEditableCols
424
+ ? editableColumns
425
+ : baseColumns
426
+ );
427
+ const showFormatPanel = $derived(example === 'formatPanel' || example === 'conditional');
428
+ const useCellSelect = $derived(
429
+ example === 'columnAndCell' ||
430
+ example === 'withNotes' ||
431
+ example === 'customDock' ||
432
+ example === 'marquee' ||
433
+ example === 'contextMenu'
434
+ );
435
+
436
+ function onCellEdit(rowId: string, columnId: string, value: unknown) {
437
+ rows = rows.map((r) => {
438
+ if (r.id !== rowId) return r;
439
+ if (columnId === 'amount') return { ...r, amount: Number(value) || 0 };
440
+ if (columnId === 'status') return { ...r, status: String(value) as Order['status'] };
441
+ if (columnId === 'customer') return { ...r, customer: String(value) };
442
+ if (columnId === 'channel') return { ...r, channel: String(value) };
443
+ if (columnId === 'vip') return { ...r, vip: Boolean(value) };
444
+ if (columnId === 'due') return { ...r, due: String(value) };
445
+ return r;
446
+ });
447
+ lastAction = `Edited ${rowId}.${columnId} → ${String(value)}`;
448
+ }
449
+ </script>
450
+
451
+ <div class="space-y-4">
452
+ {#if example === 'columnAndCell' || example === 'marquee'}
453
+ <p class="text-xs text-muted">
454
+ Tip: click a cell to select · drag to rubber-band select (Windows-style) · Shift+drag to add ·
455
+ Cmd/Ctrl+drag to toggle · Shift+click for range · Cmd/Ctrl+click to multi-select · Alt+click or
456
+ right-click header to select a column · Mark from the dock to tint cells.
457
+ </p>
458
+ {:else if example === 'editable'}
459
+ <p class="text-xs text-muted">
460
+ Tip: double-click or press F2 to edit · editors adapt by column type (text, select, number,
461
+ toggle, date).
462
+ </p>
463
+ {:else if example === 'contextMenu'}
464
+ <p class="text-xs text-muted">
465
+ Tip: right-click for Mark / Clear mark · tinted backgrounds are persistent · dock Mark opens the
466
+ color palette · selection uses a stronger brand highlight.
467
+ </p>
468
+ {:else if example === 'responsive'}
469
+ <p class="text-xs text-muted">
470
+ Tip: resize the viewport — columns with hideBelow move into the row expand panel · switch
471
+ table/list/card/grid · search and column filters · drag header edges to resize columns.
472
+ </p>
473
+ {:else if example === 'default' || example === 'rowSelection'}
474
+ <p class="text-xs text-muted">
475
+ Tip: drag across rows to rubber-band select · right-click for options · ⋯ for row actions ·
476
+ Shift+drag to add · Cmd/Ctrl+drag to toggle.
477
+ </p>
478
+ {/if}
479
+
480
+ {#if lastAction}
481
+ <p class="rounded-lg bg-surface-overlay px-3 py-2 text-xs text-secondary">{lastAction}</p>
482
+ {/if}
483
+
484
+ <DataGrid
485
+ columns={activeColumns}
486
+ rows={activeRows}
487
+ bind:selection
488
+ bind:notes
489
+ bind:marks
490
+ bind:formatRules
491
+ bind:filterQuery
492
+ bind:columnFilters
493
+ bind:expandedRows
494
+ bind:viewMode
495
+ {sortable}
496
+ {compact}
497
+ {striped}
498
+ editable={canEdit && !isReadOnly}
499
+ selectable={!isReadOnly}
500
+ collapsible={useResponsive || example === 'withNotes'}
501
+ filterable={useResponsive}
502
+ showViewModes={useResponsive}
503
+ titleColumnId="customer"
504
+ selectColumns={example === 'columnAndCell' || example === 'customDock' || example === 'marquee' || example === 'contextMenu'}
505
+ selectCells={useCellSelect}
506
+ marqueeSelect={!isReadOnly}
507
+ contextMenu={!isReadOnly}
508
+ showRowActions={!isReadOnly}
509
+ actions={useCustomDock ? customActions : undefined}
510
+ loading={isLoading}
511
+ empty={isEmpty ? 'No orders match your filters.' : 'No results'}
512
+ showDock={!isReadOnly}
513
+ oncelledit={onCellEdit}
514
+ onaction={(id) => {
515
+ lastAction = `Action: ${id}`;
516
+ }}
517
+ >
518
+ {#snippet cell(row, column)}
519
+ {#if column.id === 'status'}
520
+ <Badge variant={statusVariant[row.status as Order['status']]}>
521
+ {row.status}
522
+ </Badge>
523
+ {:else if column.id === 'amount'}
524
+ <span class="tabular-nums">${Number(row.amount).toLocaleString()}</span>
525
+ {:else if column.id === 'customer'}
526
+ <span class="font-medium">{row.customer as string}</span>
527
+ {:else if column.id === 'vip'}
528
+ <span class={row.vip ? 'font-medium text-brand-600' : 'text-muted'}>
529
+ {row.vip ? 'Yes' : 'No'}
530
+ </span>
531
+ {:else if column.id === 'due'}
532
+ <span class="tabular-nums text-secondary">{row.due as string}</span>
533
+ {:else}
534
+ {row[column.id as keyof Order]}
535
+ {/if}
536
+ {/snippet}
537
+ {#snippet expandedRow(row, hiddenCols)}
538
+ {@const meta = rowExpand(row)}
539
+ {@const cellNotes = rowNotes(row.id)}
540
+ <div class="space-y-4">
541
+ <div class="flex flex-wrap items-start justify-between gap-3">
542
+ <div class="min-w-0 space-y-1">
543
+ <p class="text-sm font-semibold text-primary">
544
+ Order #{row.id}
545
+ <span class="font-normal text-muted">· {row.customer}</span>
546
+ </p>
547
+ <div class="flex flex-wrap items-center gap-2">
548
+ <Badge size="sm" variant={statusVariant[row.status]}>{row.status}</Badge>
549
+ {#if row.vip}
550
+ <Badge size="sm" variant="primary">VIP</Badge>
551
+ {/if}
552
+ <span class="text-xs text-muted">{row.channel} · due {row.due}</span>
553
+ </div>
554
+ </div>
555
+ <div class="text-right text-xs text-muted">
556
+ <p class="font-medium text-secondary">{meta.owner}</p>
557
+ <p>{meta.sla}</p>
558
+ </div>
559
+ </div>
560
+
561
+ {#if hiddenCols.length}
562
+ <div>
563
+ <p class="mb-2 text-[11px] font-semibold tracking-wide text-muted uppercase">
564
+ Hidden on this viewport
565
+ </p>
566
+ <dl class="grid gap-2 sm:grid-cols-2 lg:grid-cols-3">
567
+ {#each hiddenCols as col (col.id)}
568
+ <div class="min-w-0 rounded-lg border border-border bg-surface px-3 py-2">
569
+ <dt class="text-[11px] font-semibold text-muted uppercase">{col.header}</dt>
570
+ <dd class="mt-0.5 text-sm text-primary">
571
+ {row[col.id as keyof Order]}
572
+ </dd>
573
+ </div>
574
+ {/each}
575
+ </dl>
576
+ </div>
577
+ {/if}
578
+
579
+ <div class="grid gap-3 lg:grid-cols-2">
580
+ <div class="rounded-xl border border-border bg-surface p-3">
581
+ <p class="text-[11px] font-semibold tracking-wide text-muted uppercase">Line items</p>
582
+ <ul class="mt-2 divide-y divide-border">
583
+ {#each meta.items as item (item.sku)}
584
+ <li class="flex items-center justify-between gap-3 py-1.5 text-sm">
585
+ <span class="font-medium text-primary">
586
+ {item.sku}
587
+ <span class="font-normal text-muted">×{item.qty}</span>
588
+ </span>
589
+ <span class="tabular-nums text-secondary"
590
+ >${item.price.toLocaleString()}</span
591
+ >
592
+ </li>
593
+ {/each}
594
+ </ul>
595
+ <p class="mt-2 flex justify-between border-t border-border pt-2 text-xs font-semibold text-primary">
596
+ <span>Total</span>
597
+ <span class="tabular-nums">${row.amount.toLocaleString()}</span>
598
+ </p>
599
+ </div>
600
+
601
+ <div class="rounded-xl border border-border bg-surface p-3">
602
+ <p class="text-[11px] font-semibold tracking-wide text-muted uppercase">Activity</p>
603
+ <ul class="mt-2 space-y-2">
604
+ {#each meta.activity as event, i (`${row.id}-${i}`)}
605
+ <li class="flex gap-2 text-sm">
606
+ <span class="w-16 shrink-0 tabular-nums text-xs text-muted">{event.at}</span>
607
+ <span class="text-primary">{event.text}</span>
608
+ </li>
609
+ {/each}
610
+ </ul>
611
+ {#if cellNotes.length}
612
+ <div class="mt-3 space-y-1.5 border-t border-border pt-2">
613
+ <p class="text-[11px] font-semibold tracking-wide text-muted uppercase">
614
+ Cell notes
615
+ </p>
616
+ {#each cellNotes as note (noteKey(note.rowId, note.columnId))}
617
+ <p class="rounded-md bg-amber-500/10 px-2 py-1.5 text-xs text-primary">
618
+ <span class="font-semibold text-secondary">{note.columnId}:</span>
619
+ {note.text}
620
+ </p>
621
+ {/each}
622
+ </div>
623
+ {/if}
624
+ </div>
625
+ </div>
626
+ </div>
627
+ {/snippet}
628
+ </DataGrid>
629
+
630
+ {#if showFormatPanel}
631
+ <ConditionalFormatPanel
632
+ bind:rules={formatRules}
633
+ columns={baseColumns.map((c) => ({ id: c.id, label: c.header }))}
634
+ />
635
+ {/if}
636
+
637
+ {#if example === 'withNotes' || example === 'contextMenu' || example === 'columnAndCell'}
638
+ <div class="flex items-center gap-2 text-xs text-muted">
639
+ <span>Notes: {notes.length}</span>
640
+ <span>Marks: {marks.length}</span>
641
+ {#if example === 'withNotes'}
642
+ <Button
643
+ size="xs"
644
+ variant="secondary"
645
+ onclick={() => {
646
+ notes = [
647
+ ...notes,
648
+ {
649
+ rowId: '1',
650
+ columnId: 'customer',
651
+ text: 'VIP account — prioritize support'
652
+ }
653
+ ];
654
+ }}>Seed note on Ana</Button
655
+ >
656
+ {/if}
657
+ </div>
658
+ {/if}
659
+ </div>
@@ -0,0 +1,11 @@
1
+ type Example = 'default' | 'rowSelection' | 'columnAndCell' | 'withNotes' | 'conditional' | 'formatPanel' | 'editable' | 'pinned' | 'large' | 'empty' | 'loading' | 'customDock' | 'readOnly' | 'marquee' | 'contextMenu' | 'responsive';
2
+ type $$ComponentProps = {
3
+ example?: Example;
4
+ sortable?: boolean;
5
+ editable?: boolean;
6
+ compact?: boolean;
7
+ striped?: boolean;
8
+ };
9
+ declare const DataGridStory: import("svelte").Component<$$ComponentProps, {}, "">;
10
+ type DataGridStory = ReturnType<typeof DataGridStory>;
11
+ export default DataGridStory;
@@ -0,0 +1,10 @@
1
+ export type Breakpoint = 'sm' | 'md' | 'lg' | 'xl';
2
+ /** Min viewport width (px) for each Tailwind-aligned breakpoint. */
3
+ export declare const BREAKPOINT_MIN: Record<Breakpoint, number>;
4
+ /** True when the viewport is at least the given breakpoint. */
5
+ export declare function matchesMinBreakpoint(bp: Breakpoint, width: number): boolean;
6
+ /**
7
+ * Column with `hideBelow: 'md'` is auto-hidden when width < 768.
8
+ * Returns true if the column should be visible at `width`.
9
+ */
10
+ export declare function isColumnVisibleAtWidth(hideBelow: Breakpoint | undefined, width: number): boolean;
@@ -0,0 +1,20 @@
1
+ /** Min viewport width (px) for each Tailwind-aligned breakpoint. */
2
+ export const BREAKPOINT_MIN = {
3
+ sm: 640,
4
+ md: 768,
5
+ lg: 1024,
6
+ xl: 1280
7
+ };
8
+ /** True when the viewport is at least the given breakpoint. */
9
+ export function matchesMinBreakpoint(bp, width) {
10
+ return width >= BREAKPOINT_MIN[bp];
11
+ }
12
+ /**
13
+ * Column with `hideBelow: 'md'` is auto-hidden when width < 768.
14
+ * Returns true if the column should be visible at `width`.
15
+ */
16
+ export function isColumnVisibleAtWidth(hideBelow, width) {
17
+ if (!hideBelow)
18
+ return true;
19
+ return matchesMinBreakpoint(hideBelow, width);
20
+ }