@r2digisolutions/components 0.1.6 → 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 +1 -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,68 @@
1
+ export type { FieldType, ResourceField, ResourceCapabilities, ResourceDefinition, ResourceStudioMode, ResourceStudioLayout, ResourceStudioLabels, ResourceStudioView } from './types.js';
2
+ export { DEFAULT_CAPABILITIES, resolveCapabilities } from './types.js';
3
+ export { fieldsToColumns } from './fieldsToColumns.js';
4
+ export { fieldsToSchema } from './fieldsToSchema.js';
5
+ import type { Snippet } from 'svelte';
6
+ import type { CellNote, ConditionalFormatRule, DataGridColumn, GridSelection } from '../DataGrid/types.js';
7
+ import type { BulkAction } from '../../molecules/BulkActionBar/BulkActionBar.svelte';
8
+ import { type ResourceDefinition, type ResourceStudioMode, type ResourceStudioLayout, type ResourceStudioLabels, type ResourceStudioView } from './types.js';
9
+ interface ResourceStudioProps {
10
+ resources?: ResourceDefinition[];
11
+ activeResourceId?: string;
12
+ records?: Record<string, unknown>[];
13
+ totalRecords?: number;
14
+ loading?: boolean;
15
+ page?: number;
16
+ pageSize?: number;
17
+ query?: string;
18
+ selection?: GridSelection;
19
+ notes?: CellNote[];
20
+ formatRules?: ConditionalFormatRule[];
21
+ modes?: ResourceStudioMode[];
22
+ activeMode?: ResourceStudioMode;
23
+ layout?: ResourceStudioLayout;
24
+ labels?: ResourceStudioLabels;
25
+ views?: ResourceStudioView[];
26
+ activeViewId?: string;
27
+ /** Filter `records` locally by the search box. Set false for server-side search. */
28
+ clientSearch?: boolean;
29
+ /** Open / bind the conditional format rules panel. */
30
+ formatPanelOpen?: boolean;
31
+ /** Override bulk dock actions (defaults from capabilities). */
32
+ actions?: BulkAction[];
33
+ class?: string;
34
+ toolbar?: Snippet;
35
+ sidebarItem?: Snippet<[ResourceDefinition, boolean]>;
36
+ sidebarFooter?: Snippet;
37
+ detail?: Snippet<[record: Record<string, unknown> | null]>;
38
+ schemaPanel?: Snippet<[ResourceDefinition]>;
39
+ queryPanel?: Snippet;
40
+ activityPanel?: Snippet;
41
+ recordForm?: Snippet<[mode: 'create' | 'edit', record: Record<string, unknown> | null]>;
42
+ empty?: Snippet;
43
+ dockExtra?: Snippet;
44
+ cell?: Snippet<[Record<string, unknown>, DataGridColumn]>;
45
+ onresourcechange?: (id: string) => void;
46
+ onsearch?: (q: string) => void;
47
+ onpagechange?: (page: number, pageSize: number) => void;
48
+ onsort?: (columnId: string, dir: 'asc' | 'desc') => void;
49
+ oncreate?: () => void;
50
+ oninsert?: (record: Record<string, unknown>) => void;
51
+ onupdate?: (id: string, patch: Record<string, unknown>) => void;
52
+ ondelete?: (ids: string[]) => void;
53
+ onduplicate?: (ids: string[]) => void;
54
+ onexport?: (ids: string[] | 'all') => void;
55
+ onimport?: (file: File) => void;
56
+ oncelledit?: (id: string, fieldId: string, value: unknown) => void;
57
+ /** Fired for dock actions not handled built-in (e.g. custom bulk workflow). */
58
+ onaction?: (id: string, rowIds: string[]) => void;
59
+ onselectionchange?: (s: GridSelection) => void;
60
+ onnoteschange?: (notes: CellNote[]) => void;
61
+ onformatruleschange?: (rules: ConditionalFormatRule[]) => void;
62
+ onmodechange?: (mode: ResourceStudioMode) => void;
63
+ onviewchange?: (viewId: string) => void;
64
+ onrefresh?: () => void;
65
+ }
66
+ declare const ResourceStudio: import("svelte").Component<ResourceStudioProps, {}, "query" | "page" | "notes" | "selection" | "formatRules" | "activeResourceId" | "activeMode" | "activeViewId" | "formatPanelOpen">;
67
+ type ResourceStudio = ReturnType<typeof ResourceStudio>;
68
+ export default ResourceStudio;
@@ -0,0 +1,600 @@
1
+ <script lang="ts">
2
+ import ResourceStudio from './ResourceStudio.svelte';
3
+ import type { ResourceDefinition } from './types.js';
4
+ import type {
5
+ CellNote,
6
+ ConditionalFormatRule,
7
+ GridSelection
8
+ } from '../DataGrid/types.js';
9
+ import { EMPTY_SELECTION } from '../DataGrid/types.js';
10
+ import Badge from '../../atoms/Badge/Badge.svelte';
11
+ import Button from '../../atoms/Button/Button.svelte';
12
+ import CodeEditor from '../../molecules/CodeEditor/CodeEditor.svelte';
13
+ import type { BulkAction } from '../../molecules/BulkActionBar/BulkActionBar.svelte';
14
+
15
+ type Example =
16
+ | 'crudAdmin'
17
+ | 'cms'
18
+ | 'readOnly'
19
+ | 'sqlBrowser'
20
+ | 'customQuery'
21
+ | 'notesAndRules'
22
+ | 'bulkWorkflow'
23
+ | 'empty'
24
+ | 'loading'
25
+ | 'mainOnly'
26
+ | 'permissions';
27
+
28
+ let { example = 'crudAdmin' }: { example?: Example } = $props();
29
+
30
+ const users: ResourceDefinition = {
31
+ id: 'users',
32
+ name: 'Users',
33
+ group: 'Auth',
34
+ description: 'Account holders',
35
+ recordCount: 4,
36
+ fields: [
37
+ { id: 'id', label: 'ID', type: 'string', readonly: true, unique: true },
38
+ { id: 'name', label: 'Name', type: 'string', required: true },
39
+ { id: 'email', label: 'Email', type: 'string', required: true, unique: true },
40
+ {
41
+ id: 'role',
42
+ label: 'Role',
43
+ type: 'enum',
44
+ options: [
45
+ { value: 'admin', label: 'Admin' },
46
+ { value: 'editor', label: 'Editor' },
47
+ { value: 'viewer', label: 'Viewer' }
48
+ ]
49
+ },
50
+ { id: 'active', label: 'Active', type: 'boolean' }
51
+ ]
52
+ };
53
+
54
+ const products: ResourceDefinition = {
55
+ id: 'products',
56
+ name: 'Products',
57
+ group: 'Catalog',
58
+ recordCount: 3,
59
+ fields: [
60
+ { id: 'id', label: 'ID', type: 'string', readonly: true },
61
+ { id: 'name', label: 'Name', type: 'string', required: true },
62
+ { id: 'price', label: 'Price', type: 'number', required: true },
63
+ {
64
+ id: 'status',
65
+ label: 'Status',
66
+ type: 'enum',
67
+ options: [
68
+ { value: 'draft', label: 'Draft' },
69
+ { value: 'live', label: 'Live' },
70
+ { value: 'archived', label: 'Archived' }
71
+ ]
72
+ }
73
+ ]
74
+ };
75
+
76
+ const orders: ResourceDefinition = {
77
+ id: 'orders',
78
+ name: 'Orders',
79
+ group: 'Catalog',
80
+ recordCount: 5,
81
+ capabilities: {
82
+ notes: true,
83
+ conditionalFormat: true,
84
+ duplicate: true,
85
+ bulkEdit: true
86
+ },
87
+ defaultFormatRules: [
88
+ {
89
+ id: 'failed',
90
+ name: 'Failed',
91
+ enabled: true,
92
+ target: 'row',
93
+ when: { op: 'eq', columnId: 'status', value: 'failed' },
94
+ style: { background: '#fee2e2', color: '#991b1b' },
95
+ priority: 1
96
+ },
97
+ {
98
+ id: 'high',
99
+ name: 'High amount',
100
+ enabled: true,
101
+ target: 'cell',
102
+ columnId: 'total',
103
+ when: { op: 'gt', columnId: 'total', value: 500 },
104
+ style: { background: '#dcfce7', color: '#166534', fontWeight: 'semibold' },
105
+ priority: 2
106
+ }
107
+ ],
108
+ fields: [
109
+ { id: 'id', label: 'ID', type: 'string', readonly: true },
110
+ {
111
+ id: 'userId',
112
+ label: 'User',
113
+ type: 'relation',
114
+ relation: { resourceId: 'users', fieldId: 'id', displayField: 'name' }
115
+ },
116
+ {
117
+ id: 'status',
118
+ label: 'Status',
119
+ type: 'enum',
120
+ options: [
121
+ { value: 'paid', label: 'Paid' },
122
+ { value: 'pending', label: 'Pending' },
123
+ { value: 'failed', label: 'Failed' }
124
+ ]
125
+ },
126
+ { id: 'total', label: 'Total', type: 'number' }
127
+ ]
128
+ };
129
+
130
+ const posts: ResourceDefinition = {
131
+ id: 'posts',
132
+ name: 'Posts',
133
+ group: 'Content',
134
+ fields: [
135
+ { id: 'id', label: 'ID', type: 'string', readonly: true },
136
+ { id: 'title', label: 'Title', type: 'string', required: true },
137
+ { id: 'slug', label: 'Slug', type: 'string' },
138
+ { id: 'body', label: 'Body', type: 'text' },
139
+ {
140
+ id: 'status',
141
+ label: 'Status',
142
+ type: 'enum',
143
+ options: [
144
+ { value: 'draft', label: 'Draft' },
145
+ { value: 'published', label: 'Published' }
146
+ ]
147
+ }
148
+ ]
149
+ };
150
+
151
+ const pages: ResourceDefinition = {
152
+ id: 'pages',
153
+ name: 'Pages',
154
+ group: 'Content',
155
+ fields: [
156
+ { id: 'id', label: 'ID', type: 'string', readonly: true },
157
+ { id: 'title', label: 'Title', type: 'string', required: true },
158
+ { id: 'path', label: 'Path', type: 'string' }
159
+ ]
160
+ };
161
+
162
+ const media: ResourceDefinition = {
163
+ id: 'media',
164
+ name: 'Media',
165
+ group: 'Content',
166
+ fields: [
167
+ { id: 'id', label: 'ID', type: 'string', readonly: true },
168
+ { id: 'name', label: 'Name', type: 'string' },
169
+ { id: 'mime', label: 'MIME', type: 'string' },
170
+ { id: 'size', label: 'Size', type: 'number' }
171
+ ]
172
+ };
173
+
174
+ const sqlUsers: ResourceDefinition = {
175
+ ...users,
176
+ group: 'public',
177
+ name: 'users',
178
+ fields: users.fields.map((f) => ({
179
+ ...f,
180
+ meta: { sqlType: f.type === 'string' ? 'varchar' : f.type }
181
+ }))
182
+ };
183
+
184
+ const sqlOrders: ResourceDefinition = {
185
+ ...orders,
186
+ group: 'public',
187
+ name: 'orders'
188
+ };
189
+
190
+ const dataByResource: Record<string, Record<string, unknown>[]> = {
191
+ users: [
192
+ { id: 'u1', name: 'Ana López', email: 'ana@r2.dev', role: 'admin', active: true },
193
+ { id: 'u2', name: 'Carlos Ruiz', email: 'carlos@r2.dev', role: 'editor', active: true },
194
+ { id: 'u3', name: 'Maya Chen', email: 'maya@r2.dev', role: 'viewer', active: false },
195
+ { id: 'u4', name: 'Jon Park', email: 'jon@r2.dev', role: 'editor', active: true }
196
+ ],
197
+ products: [
198
+ { id: 'p1', name: 'Pro Plan', price: 49, status: 'live' },
199
+ { id: 'p2', name: 'Starter', price: 19, status: 'live' },
200
+ { id: 'p3', name: 'Legacy', price: 9, status: 'archived' }
201
+ ],
202
+ orders: [
203
+ { id: 'o1', userId: 'u1', status: 'paid', total: 1280 },
204
+ { id: 'o2', userId: 'u2', status: 'pending', total: 120 },
205
+ { id: 'o3', userId: 'u3', status: 'failed', total: 89 },
206
+ { id: 'o4', userId: 'u1', status: 'paid', total: 640 },
207
+ { id: 'o5', userId: 'u4', status: 'failed', total: 990 }
208
+ ],
209
+ posts: [
210
+ {
211
+ id: 'post1',
212
+ title: 'Launch notes',
213
+ slug: 'launch-notes',
214
+ body: 'We shipped ResourceStudio.',
215
+ status: 'published'
216
+ },
217
+ {
218
+ id: 'post2',
219
+ title: 'Draft ideas',
220
+ slug: 'draft-ideas',
221
+ body: 'WIP',
222
+ status: 'draft'
223
+ }
224
+ ],
225
+ pages: [
226
+ { id: 'pg1', title: 'Home', path: '/' },
227
+ { id: 'pg2', title: 'Pricing', path: '/pricing' }
228
+ ],
229
+ media: [
230
+ { id: 'm1', name: 'hero.png', mime: 'image/png', size: 240000 },
231
+ { id: 'm2', name: 'guide.pdf', mime: 'application/pdf', size: 1200000 }
232
+ ]
233
+ };
234
+
235
+ const readOnlyResources: ResourceDefinition[] = [
236
+ {
237
+ ...products,
238
+ capabilities: {
239
+ create: false,
240
+ update: false,
241
+ delete: false,
242
+ editable: false,
243
+ export: true,
244
+ notes: false,
245
+ conditionalFormat: false
246
+ }
247
+ }
248
+ ];
249
+
250
+ const permissionResources: ResourceDefinition[] = [
251
+ {
252
+ ...users,
253
+ capabilities: {
254
+ create: true,
255
+ update: true,
256
+ delete: false,
257
+ export: true,
258
+ editable: true
259
+ }
260
+ }
261
+ ];
262
+
263
+ const bulkOrders: ResourceDefinition = {
264
+ ...orders,
265
+ description: 'Select rows → bulk approve / reject / assign',
266
+ capabilities: {
267
+ notes: false,
268
+ conditionalFormat: false,
269
+ duplicate: true,
270
+ bulkEdit: true,
271
+ delete: true,
272
+ export: true,
273
+ create: false,
274
+ editable: false
275
+ },
276
+ defaultFormatRules: []
277
+ };
278
+
279
+ const resources = $derived.by((): ResourceDefinition[] => {
280
+ if (example === 'cms') return [posts, pages, media];
281
+ if (example === 'readOnly') return readOnlyResources;
282
+ if (example === 'sqlBrowser' || example === 'customQuery') return [sqlUsers, sqlOrders];
283
+ if (example === 'notesAndRules') return [orders, users, products];
284
+ if (example === 'bulkWorkflow') return [bulkOrders];
285
+ if (example === 'mainOnly') return [orders];
286
+ if (example === 'permissions') return permissionResources;
287
+ if (example === 'empty' || example === 'loading') return [users, products, orders];
288
+ return [users, products, orders];
289
+ });
290
+
291
+ let activeResourceId = $state('users');
292
+ let selection = $state<GridSelection>(EMPTY_SELECTION);
293
+ let notes = $state<CellNote[]>([
294
+ { rowId: 'o3', columnId: 'status', text: 'Chargeback risk — follow up' },
295
+ { rowId: 'o5', columnId: 'total', text: 'High value — verify manually' }
296
+ ]);
297
+ let formatRules = $state<ConditionalFormatRule[]>([]);
298
+ let formatPanelOpen = $state(false);
299
+ let query = $state('');
300
+ let page = $state(1);
301
+ let sql = $state('select * from orders where total > 500;');
302
+ let gql = $state('query {\n orders(where: { total: { _gt: 500 } }) {\n id\n total\n }\n}');
303
+ let log = $state('');
304
+ let activeMode = $state<'browse' | 'detail' | 'schema' | 'query' | 'activity'>('browse');
305
+
306
+ const bulkActions: BulkAction[] = [
307
+ { id: 'approve', label: 'Approve', variant: 'primary' },
308
+ { id: 'reject', label: 'Reject', variant: 'destructive', confirm: true, confirmLabel: 'Reject' },
309
+ { id: 'assign', label: 'Assign', variant: 'secondary' },
310
+ { id: 'export', label: 'Export', variant: 'secondary' },
311
+ { id: 'duplicate', label: 'Duplicate', variant: 'ghost', overflow: true }
312
+ ];
313
+
314
+ $effect(() => {
315
+ // Reset when the Storybook example changes — do not fight user clicks mid-story.
316
+ if (example === 'mainOnly' || example === 'notesAndRules') {
317
+ activeResourceId = 'orders';
318
+ } else if (example === 'bulkWorkflow') {
319
+ activeResourceId = 'orders';
320
+ } else if (example === 'cms') {
321
+ activeResourceId = 'posts';
322
+ } else if (example === 'sqlBrowser' || example === 'customQuery') {
323
+ activeResourceId = 'users';
324
+ } else if (example === 'readOnly') {
325
+ activeResourceId = 'products';
326
+ } else if (example === 'permissions') {
327
+ activeResourceId = 'users';
328
+ } else {
329
+ activeResourceId = 'users';
330
+ }
331
+ page = 1;
332
+ query = '';
333
+ formatPanelOpen = example === 'notesAndRules';
334
+ activeMode =
335
+ example === 'sqlBrowser' || example === 'customQuery'
336
+ ? 'query'
337
+ : example === 'bulkWorkflow'
338
+ ? 'browse'
339
+ : 'browse';
340
+ selection =
341
+ example === 'bulkWorkflow'
342
+ ? { type: 'rows', ids: ['o2', 'o3', 'o5'] }
343
+ : EMPTY_SELECTION;
344
+ });
345
+
346
+ $effect(() => {
347
+ const res = resources.find((r) => r.id === activeResourceId);
348
+ formatRules = (res?.defaultFormatRules ?? []).map((r) => ({ ...r }));
349
+ });
350
+
351
+ const records = $derived.by(() => {
352
+ if (example === 'empty') return [];
353
+ const key =
354
+ activeResourceId === 'users' || activeResourceId === 'sql-users'
355
+ ? 'users'
356
+ : activeResourceId;
357
+ return dataByResource[key] ?? dataByResource[activeResourceId] ?? [];
358
+ });
359
+
360
+ const modes = $derived.by(() => {
361
+ if (example === 'sqlBrowser' || example === 'customQuery') {
362
+ return ['browse', 'schema', 'query', 'detail'] as const;
363
+ }
364
+ if (example === 'cms') return ['browse', 'detail', 'schema'] as const;
365
+ if (example === 'mainOnly') return ['browse', 'detail'] as const;
366
+ if (example === 'bulkWorkflow') return ['browse', 'activity', 'detail'] as const;
367
+ if (example === 'notesAndRules') return ['browse', 'detail', 'schema'] as const;
368
+ return ['browse', 'detail', 'schema'] as const;
369
+ });
370
+
371
+ const layout = $derived(
372
+ example === 'mainOnly' ? ('main-only' as const) : ('sidebar-main-detail' as const)
373
+ );
374
+
375
+ const labels = $derived(
376
+ example === 'sqlBrowser'
377
+ ? {
378
+ resources: 'Tables',
379
+ records: 'rows',
380
+ addRecord: 'Insert row',
381
+ schema: 'Schema',
382
+ query: 'SQL',
383
+ runQuery: 'Run SQL'
384
+ }
385
+ : example === 'customQuery'
386
+ ? {
387
+ resources: 'Collections',
388
+ query: 'GraphQL',
389
+ runQuery: 'Execute'
390
+ }
391
+ : example === 'bulkWorkflow'
392
+ ? {
393
+ resources: 'Queues',
394
+ records: 'orders',
395
+ activity: 'Workflow',
396
+ detail: 'Order'
397
+ }
398
+ : example === 'notesAndRules'
399
+ ? {
400
+ resources: 'Resources',
401
+ records: 'orders',
402
+ detail: 'Order'
403
+ }
404
+ : undefined
405
+ );
406
+
407
+ function mutateInsert(record: Record<string, unknown>) {
408
+ const list = dataByResource[activeResourceId];
409
+ if (list) list.unshift(record);
410
+ log = `insert ${JSON.stringify(record)}`;
411
+ }
412
+
413
+ function mutateUpdate(id: string, patch: Record<string, unknown>) {
414
+ const list = dataByResource[activeResourceId];
415
+ if (!list) return;
416
+ const idx = list.findIndex((r) => String(r.id) === id);
417
+ if (idx >= 0) list[idx] = { ...list[idx], ...patch };
418
+ log = `update ${id}`;
419
+ }
420
+
421
+ function mutateDelete(ids: string[]) {
422
+ const list = dataByResource[activeResourceId];
423
+ if (!list) return;
424
+ dataByResource[activeResourceId] = list.filter((r) => !ids.includes(String(r.id)));
425
+ log = `delete ${ids.join(',')}`;
426
+ }
427
+ </script>
428
+
429
+ <div class="flex h-svh flex-col">
430
+ {#if log}
431
+ <p class="shrink-0 rounded-none border-b border-border bg-surface-overlay px-3 py-2 font-mono text-[11px] text-secondary">
432
+ {log}
433
+ </p>
434
+ {/if}
435
+
436
+ <div class="min-h-0 flex-1">
437
+ <ResourceStudio
438
+ class="h-full min-h-0 rounded-none border-0"
439
+ {resources}
440
+ bind:activeResourceId
441
+ {records}
442
+ totalRecords={records.length}
443
+ loading={example === 'loading'}
444
+ bind:page
445
+ bind:query
446
+ bind:selection
447
+ bind:notes
448
+ bind:formatRules
449
+ bind:formatPanelOpen
450
+ bind:activeMode
451
+ modes={[...modes]}
452
+ {layout}
453
+ {labels}
454
+ actions={example === 'bulkWorkflow' ? bulkActions : undefined}
455
+ views={example === 'cms'
456
+ ? [
457
+ { id: 'all', label: 'All' },
458
+ { id: 'published', label: 'Published' },
459
+ { id: 'drafts', label: 'Drafts' }
460
+ ]
461
+ : []}
462
+ oninsert={mutateInsert}
463
+ onupdate={mutateUpdate}
464
+ ondelete={mutateDelete}
465
+ oncelledit={(id, field, value) => mutateUpdate(id, { [field]: value })}
466
+ onexport={(ids) => {
467
+ log = `export ${ids === 'all' ? 'all' : ids.join(',')}`;
468
+ }}
469
+ onduplicate={(ids) => {
470
+ log = `duplicate ${ids.join(',')}`;
471
+ }}
472
+ onaction={(id, ids) => {
473
+ log = `${id} → ${ids.join(', ')}`;
474
+ }}
475
+ onsearch={(q) => {
476
+ log = `search ${q}`;
477
+ }}
478
+ >
479
+ {#snippet activityPanel()}
480
+ <div class="space-y-3 rounded-xl border border-border bg-surface-elevated p-4">
481
+ <p class="text-sm font-semibold text-primary">Bulk workflow</p>
482
+ <p class="text-xs text-secondary">
483
+ Orders are pre-selected. Use the dock to Approve, Reject or Assign in one step —
484
+ typical ops queue.
485
+ </p>
486
+ <ol class="list-decimal space-y-1.5 pl-4 text-xs text-secondary">
487
+ <li>Select rows (checkbox or Shift)</li>
488
+ <li>Choose a bulk action in the bottom dock</li>
489
+ <li>Confirm destructive steps when prompted</li>
490
+ </ol>
491
+ {#if selection.type === 'rows' && selection.ids.length}
492
+ <p class="text-xs font-medium text-brand-600 dark:text-brand-400">
493
+ {selection.ids.length} selected: {selection.ids.join(', ')}
494
+ </p>
495
+ {:else}
496
+ <p class="text-xs text-muted">No rows selected.</p>
497
+ {/if}
498
+ {#if log}
499
+ <p class="rounded-lg bg-surface-overlay px-2 py-1.5 font-mono text-[11px] text-secondary">
500
+ Last action: {log}
501
+ </p>
502
+ {/if}
503
+ </div>
504
+ {/snippet}
505
+
506
+ {#snippet queryPanel()}
507
+ {#if example === 'sqlBrowser'}
508
+ <div class="flex h-full min-h-0 flex-1 flex-col gap-2">
509
+ <p class="shrink-0 text-xs font-semibold text-secondary">SQL runner (slot)</p>
510
+ <CodeEditor
511
+ bind:value={sql}
512
+ language="sql"
513
+ filename="query.sql"
514
+ class="min-h-0 flex-1"
515
+ minHeight={240}
516
+ />
517
+ <div class="flex shrink-0 justify-end">
518
+ <Button
519
+ size="sm"
520
+ variant="primary"
521
+ onclick={() => {
522
+ log = `run sql: ${sql}`;
523
+ }}>Run SQL</Button
524
+ >
525
+ </div>
526
+ </div>
527
+ {:else}
528
+ <div class="flex h-full min-h-0 flex-1 flex-col gap-2">
529
+ <p class="shrink-0 text-xs font-semibold text-secondary">GraphQL (custom slot)</p>
530
+ <CodeEditor
531
+ bind:value={gql}
532
+ language="graphql"
533
+ filename="query.graphql"
534
+ class="min-h-0 flex-1"
535
+ minHeight={240}
536
+ />
537
+ <div class="flex shrink-0 justify-end">
538
+ <Button
539
+ size="sm"
540
+ variant="primary"
541
+ onclick={() => {
542
+ log = `run graphql`;
543
+ }}>Execute</Button
544
+ >
545
+ </div>
546
+ </div>
547
+ {/if}
548
+ {/snippet}
549
+
550
+ {#snippet detail(record)}
551
+ {#if example === 'cms' && record}
552
+ <div class="space-y-2">
553
+ <p class="text-xs font-semibold">CMS preview</p>
554
+ <h3 class="text-base font-semibold">{String(record.title ?? '')}</h3>
555
+ {#if record.body}
556
+ <p class="text-sm text-secondary whitespace-pre-wrap">{String(record.body)}</p>
557
+ {/if}
558
+ <Badge variant="secondary">{String(record.status ?? record.path ?? '')}</Badge>
559
+ </div>
560
+ {:else if record}
561
+ <div class="space-y-3">
562
+ <p class="text-xs font-semibold text-secondary">Record</p>
563
+ <dl class="space-y-2">
564
+ {#each Object.entries(record) as [key, value] (`${String(record.id ?? '')}:${key}`)}
565
+ <div>
566
+ <dt class="text-[10px] font-semibold tracking-wide text-muted uppercase">{key}</dt>
567
+ <dd class="mt-0.5 break-all text-sm text-primary">
568
+ {value == null ? '—' : String(value)}
569
+ </dd>
570
+ </div>
571
+ {/each}
572
+ </dl>
573
+ </div>
574
+ {:else}
575
+ <p class="py-8 text-center text-xs text-muted">Select a row to inspect.</p>
576
+ {/if}
577
+ {/snippet}
578
+
579
+ {#snippet cell(row, column)}
580
+ {#if column.id === 'status'}
581
+ <Badge
582
+ variant={row.status === 'failed' || row.status === 'archived'
583
+ ? 'error'
584
+ : row.status === 'pending' || row.status === 'draft'
585
+ ? 'warning'
586
+ : 'success'}
587
+ >
588
+ {String(row.status ?? '')}
589
+ </Badge>
590
+ {:else if column.id === 'total' || column.id === 'price' || column.id === 'size'}
591
+ <span class="tabular-nums">{Number(row[column.id] ?? 0).toLocaleString()}</span>
592
+ {:else if column.id === 'active'}
593
+ {row.active ? 'Yes' : 'No'}
594
+ {:else}
595
+ {row[column.id] == null ? '' : String(row[column.id])}
596
+ {/if}
597
+ {/snippet}
598
+ </ResourceStudio>
599
+ </div>
600
+ </div>
@@ -0,0 +1,7 @@
1
+ type Example = 'crudAdmin' | 'cms' | 'readOnly' | 'sqlBrowser' | 'customQuery' | 'notesAndRules' | 'bulkWorkflow' | 'empty' | 'loading' | 'mainOnly' | 'permissions';
2
+ type $$ComponentProps = {
3
+ example?: Example;
4
+ };
5
+ declare const ResourceStudioStory: import("svelte").Component<$$ComponentProps, {}, "">;
6
+ type ResourceStudioStory = ReturnType<typeof ResourceStudioStory>;
7
+ export default ResourceStudioStory;
@@ -0,0 +1,4 @@
1
+ import type { DataGridColumn } from '../DataGrid/types.js';
2
+ import type { ResourceField } from './types.js';
3
+ /** Map ResourceField[] → DataGrid columns (skips hidden fields). */
4
+ export declare function fieldsToColumns<Row extends Record<string, unknown> = Record<string, unknown>>(fields: ResourceField[]): DataGridColumn<Row>[];