@simplysm/solid 13.0.55 → 13.0.56

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 (181) hide show
  1. package/README.md +3 -1
  2. package/dist/components/data/crud-detail/CrudDetail.d.ts +14 -0
  3. package/dist/components/data/crud-detail/CrudDetail.d.ts.map +1 -0
  4. package/dist/components/data/crud-detail/CrudDetail.js +348 -0
  5. package/dist/components/data/crud-detail/CrudDetail.js.map +6 -0
  6. package/dist/components/data/crud-detail/CrudDetailAfter.d.ts +7 -0
  7. package/dist/components/data/crud-detail/CrudDetailAfter.d.ts.map +1 -0
  8. package/dist/components/data/crud-detail/CrudDetailAfter.js +14 -0
  9. package/dist/components/data/crud-detail/CrudDetailAfter.js.map +6 -0
  10. package/dist/components/data/crud-detail/CrudDetailBefore.d.ts +7 -0
  11. package/dist/components/data/crud-detail/CrudDetailBefore.d.ts.map +1 -0
  12. package/dist/components/data/crud-detail/CrudDetailBefore.js +14 -0
  13. package/dist/components/data/crud-detail/CrudDetailBefore.js.map +6 -0
  14. package/dist/components/data/crud-detail/CrudDetailTools.d.ts +7 -0
  15. package/dist/components/data/crud-detail/CrudDetailTools.d.ts.map +1 -0
  16. package/dist/components/data/crud-detail/CrudDetailTools.js +14 -0
  17. package/dist/components/data/crud-detail/CrudDetailTools.js.map +6 -0
  18. package/dist/components/data/crud-detail/types.d.ts +45 -0
  19. package/dist/components/data/crud-detail/types.d.ts.map +1 -0
  20. package/dist/components/data/crud-detail/types.js +1 -0
  21. package/dist/components/data/crud-detail/types.js.map +6 -0
  22. package/dist/components/data/crud-sheet/CrudSheet.d.ts +17 -0
  23. package/dist/components/data/crud-sheet/CrudSheet.d.ts.map +1 -0
  24. package/dist/components/data/crud-sheet/CrudSheet.js +679 -0
  25. package/dist/components/data/crud-sheet/CrudSheet.js.map +6 -0
  26. package/dist/components/data/crud-sheet/CrudSheetColumn.d.ts +5 -0
  27. package/dist/components/data/crud-sheet/CrudSheetColumn.d.ts.map +1 -0
  28. package/dist/components/data/crud-sheet/CrudSheetColumn.js +29 -0
  29. package/dist/components/data/crud-sheet/CrudSheetColumn.js.map +6 -0
  30. package/dist/components/data/crud-sheet/CrudSheetFilter.d.ts +7 -0
  31. package/dist/components/data/crud-sheet/CrudSheetFilter.d.ts.map +1 -0
  32. package/dist/components/data/crud-sheet/CrudSheetFilter.js +14 -0
  33. package/dist/components/data/crud-sheet/CrudSheetFilter.js.map +6 -0
  34. package/dist/components/data/crud-sheet/CrudSheetHeader.d.ts +7 -0
  35. package/dist/components/data/crud-sheet/CrudSheetHeader.d.ts.map +1 -0
  36. package/dist/components/data/crud-sheet/CrudSheetHeader.js +14 -0
  37. package/dist/components/data/crud-sheet/CrudSheetHeader.js.map +6 -0
  38. package/dist/components/data/crud-sheet/CrudSheetTools.d.ts +7 -0
  39. package/dist/components/data/crud-sheet/CrudSheetTools.d.ts.map +1 -0
  40. package/dist/components/data/crud-sheet/CrudSheetTools.js +14 -0
  41. package/dist/components/data/crud-sheet/CrudSheetTools.js.map +6 -0
  42. package/dist/components/data/crud-sheet/types.d.ts +109 -0
  43. package/dist/components/data/crud-sheet/types.d.ts.map +1 -0
  44. package/dist/components/data/crud-sheet/types.js +1 -0
  45. package/dist/components/data/crud-sheet/types.js.map +6 -0
  46. package/dist/components/data/kanban/Kanban.d.ts.map +1 -1
  47. package/dist/components/data/kanban/Kanban.js +137 -138
  48. package/dist/components/data/kanban/Kanban.js.map +2 -2
  49. package/dist/components/data/kanban/KanbanContext.d.ts +5 -1
  50. package/dist/components/data/kanban/KanbanContext.d.ts.map +1 -1
  51. package/dist/components/data/kanban/KanbanContext.js.map +1 -1
  52. package/dist/components/data/list/ListItem.d.ts.map +1 -1
  53. package/dist/components/data/list/ListItem.js +109 -99
  54. package/dist/components/data/list/ListItem.js.map +2 -2
  55. package/dist/components/data/sheet/DataSheet.js +1 -1
  56. package/dist/components/data/sheet/DataSheet.js.map +2 -2
  57. package/dist/components/data/sheet/DataSheet.styles.d.ts.map +1 -1
  58. package/dist/components/data/sheet/DataSheet.styles.js +1 -1
  59. package/dist/components/data/sheet/DataSheet.styles.js.map +1 -1
  60. package/dist/components/disclosure/Dialog.d.ts +16 -10
  61. package/dist/components/disclosure/Dialog.d.ts.map +1 -1
  62. package/dist/components/disclosure/Dialog.js +126 -91
  63. package/dist/components/disclosure/Dialog.js.map +2 -2
  64. package/dist/components/disclosure/DialogContext.d.ts +2 -4
  65. package/dist/components/disclosure/DialogContext.d.ts.map +1 -1
  66. package/dist/components/disclosure/DialogContext.js.map +1 -1
  67. package/dist/components/disclosure/DialogProvider.d.ts.map +1 -1
  68. package/dist/components/disclosure/DialogProvider.js +14 -9
  69. package/dist/components/disclosure/DialogProvider.js.map +2 -2
  70. package/dist/components/disclosure/Dropdown.d.ts +46 -22
  71. package/dist/components/disclosure/Dropdown.d.ts.map +1 -1
  72. package/dist/components/disclosure/Dropdown.js +100 -65
  73. package/dist/components/disclosure/Dropdown.js.map +2 -2
  74. package/dist/components/feedback/notification/NotificationBanner.d.ts.map +1 -1
  75. package/dist/components/feedback/notification/NotificationBanner.js +3 -3
  76. package/dist/components/feedback/notification/NotificationBanner.js.map +1 -1
  77. package/dist/components/feedback/notification/NotificationBell.d.ts.map +1 -1
  78. package/dist/components/feedback/notification/NotificationBell.js +84 -84
  79. package/dist/components/feedback/notification/NotificationBell.js.map +2 -2
  80. package/dist/components/form-control/combobox/Combobox.d.ts +6 -3
  81. package/dist/components/form-control/combobox/Combobox.d.ts.map +1 -1
  82. package/dist/components/form-control/combobox/Combobox.js +150 -168
  83. package/dist/components/form-control/combobox/Combobox.js.map +2 -2
  84. package/dist/components/form-control/combobox/ComboboxContext.d.ts +3 -0
  85. package/dist/components/form-control/combobox/ComboboxContext.d.ts.map +1 -1
  86. package/dist/components/form-control/combobox/ComboboxContext.js.map +1 -1
  87. package/dist/components/form-control/date-range-picker/DateRangePicker.d.ts +0 -2
  88. package/dist/components/form-control/date-range-picker/DateRangePicker.d.ts.map +1 -1
  89. package/dist/components/form-control/date-range-picker/DateRangePicker.js +9 -17
  90. package/dist/components/form-control/date-range-picker/DateRangePicker.js.map +2 -2
  91. package/dist/components/form-control/field/Field.styles.d.ts.map +1 -1
  92. package/dist/components/form-control/field/Field.styles.js +2 -1
  93. package/dist/components/form-control/field/Field.styles.js.map +1 -1
  94. package/dist/components/form-control/field/NumberInput.d.ts +15 -5
  95. package/dist/components/form-control/field/NumberInput.d.ts.map +1 -1
  96. package/dist/components/form-control/field/NumberInput.js +181 -141
  97. package/dist/components/form-control/field/NumberInput.js.map +2 -2
  98. package/dist/components/form-control/field/TextInput.d.ts +9 -5
  99. package/dist/components/form-control/field/TextInput.d.ts.map +1 -1
  100. package/dist/components/form-control/field/TextInput.js +199 -154
  101. package/dist/components/form-control/field/TextInput.js.map +2 -2
  102. package/dist/components/form-control/select/Select.d.ts +3 -3
  103. package/dist/components/form-control/select/Select.d.ts.map +1 -1
  104. package/dist/components/form-control/select/Select.js +116 -100
  105. package/dist/components/form-control/select/Select.js.map +2 -2
  106. package/dist/components/form-control/select/SelectContext.d.ts +9 -1
  107. package/dist/components/form-control/select/SelectContext.d.ts.map +1 -1
  108. package/dist/components/form-control/select/SelectContext.js.map +1 -1
  109. package/dist/components/form-control/select/SelectItem.d.ts.map +1 -1
  110. package/dist/components/form-control/select/SelectItem.js +77 -67
  111. package/dist/components/form-control/select/SelectItem.js.map +2 -2
  112. package/dist/components/layout/topbar/TopbarMenu.d.ts.map +1 -1
  113. package/dist/components/layout/topbar/TopbarMenu.js +63 -57
  114. package/dist/components/layout/topbar/TopbarMenu.js.map +2 -2
  115. package/dist/components/layout/topbar/TopbarUser.d.ts.map +1 -1
  116. package/dist/components/layout/topbar/TopbarUser.js +53 -54
  117. package/dist/components/layout/topbar/TopbarUser.js.map +2 -2
  118. package/dist/hooks/createControllableStore.d.ts +29 -0
  119. package/dist/hooks/createControllableStore.d.ts.map +1 -0
  120. package/dist/hooks/createControllableStore.js +19 -0
  121. package/dist/hooks/createControllableStore.js.map +6 -0
  122. package/dist/index.d.ts +5 -1
  123. package/dist/index.d.ts.map +1 -1
  124. package/dist/index.js +6 -2
  125. package/dist/index.js.map +1 -1
  126. package/dist/styles/patterns.styles.d.ts.map +1 -1
  127. package/dist/styles/patterns.styles.js +7 -1
  128. package/dist/styles/patterns.styles.js.map +1 -1
  129. package/docs/data-components.md +428 -0
  130. package/docs/disclosure.md +65 -35
  131. package/docs/form-controls.md +18 -3
  132. package/docs/helpers.md +0 -39
  133. package/docs/hooks.md +39 -0
  134. package/package.json +4 -3
  135. package/src/components/data/crud-detail/CrudDetail.tsx +346 -0
  136. package/src/components/data/crud-detail/CrudDetailAfter.tsx +19 -0
  137. package/src/components/data/crud-detail/CrudDetailBefore.tsx +19 -0
  138. package/src/components/data/crud-detail/CrudDetailTools.tsx +19 -0
  139. package/src/components/data/crud-detail/types.ts +58 -0
  140. package/src/components/data/crud-sheet/CrudSheet.tsx +628 -0
  141. package/src/components/data/crud-sheet/CrudSheetColumn.tsx +34 -0
  142. package/src/components/data/crud-sheet/CrudSheetFilter.tsx +21 -0
  143. package/src/components/data/crud-sheet/CrudSheetHeader.tsx +19 -0
  144. package/src/components/data/crud-sheet/CrudSheetTools.tsx +21 -0
  145. package/src/components/data/crud-sheet/types.ts +133 -0
  146. package/src/components/data/kanban/Kanban.tsx +72 -65
  147. package/src/components/data/kanban/KanbanContext.ts +7 -1
  148. package/src/components/data/list/ListItem.tsx +31 -18
  149. package/src/components/data/sheet/DataSheet.styles.ts +1 -1
  150. package/src/components/data/sheet/DataSheet.tsx +1 -1
  151. package/src/components/disclosure/Dialog.tsx +143 -105
  152. package/src/components/disclosure/DialogContext.ts +2 -4
  153. package/src/components/disclosure/DialogProvider.tsx +4 -2
  154. package/src/components/disclosure/Dropdown.tsx +174 -86
  155. package/src/components/feedback/notification/NotificationBanner.tsx +3 -9
  156. package/src/components/feedback/notification/NotificationBell.tsx +51 -57
  157. package/src/components/form-control/combobox/Combobox.tsx +109 -134
  158. package/src/components/form-control/combobox/ComboboxContext.ts +4 -1
  159. package/src/components/form-control/date-range-picker/DateRangePicker.tsx +6 -16
  160. package/src/components/form-control/field/Field.styles.ts +1 -0
  161. package/src/components/form-control/field/NumberInput.tsx +131 -88
  162. package/src/components/form-control/field/TextInput.tsx +139 -88
  163. package/src/components/form-control/select/Select.tsx +85 -67
  164. package/src/components/form-control/select/SelectContext.ts +12 -1
  165. package/src/components/form-control/select/SelectItem.tsx +39 -18
  166. package/src/components/layout/topbar/TopbarMenu.tsx +52 -55
  167. package/src/components/layout/topbar/TopbarUser.tsx +28 -31
  168. package/src/hooks/createControllableStore.ts +47 -0
  169. package/src/index.ts +5 -1
  170. package/src/styles/patterns.styles.ts +7 -1
  171. package/tailwind.css +4 -0
  172. package/dist/helpers/splitSlots.d.ts +0 -25
  173. package/dist/helpers/splitSlots.d.ts.map +0 -1
  174. package/dist/helpers/splitSlots.js +0 -25
  175. package/dist/helpers/splitSlots.js.map +0 -6
  176. package/dist/hooks/createItemTemplate.d.ts +0 -17
  177. package/dist/hooks/createItemTemplate.d.ts.map +0 -1
  178. package/dist/hooks/createItemTemplate.js +0 -40
  179. package/dist/hooks/createItemTemplate.js.map +0 -6
  180. package/src/helpers/splitSlots.ts +0 -51
  181. package/src/hooks/createItemTemplate.tsx +0 -42
@@ -29,7 +29,13 @@ const fieldSurface = clsx(
29
29
  "rounded",
30
30
  "focus-within:border-primary-500 dark:focus-within:border-primary-400"
31
31
  );
32
- const inputBase = clsx("min-w-0 flex-1", "bg-transparent", "outline-none", textPlaceholder);
32
+ const inputBase = clsx(
33
+ "min-w-0 flex-1",
34
+ "bg-transparent",
35
+ "outline-none",
36
+ "[text-decoration:inherit]",
37
+ textPlaceholder
38
+ );
33
39
  export {
34
40
  fieldSurface,
35
41
  iconButtonBase,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/styles/patterns.styles.ts"],
4
- "mappings": "AAAA,OAAO,UAAU;AACjB,SAAS,eAAe,aAAa,uBAAuB;AAGrD,MAAM,iBAAiB;AAAA,EAC5B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAGO,MAAM,oBAAoB;AAAA,EAC/B;AAAA,EACA;AAAA,EACA;AACF;AAGO,MAAM,wBAAwB;AAAA,EACnC;AAAA,EACA;AAAA,EACA;AACF;AAGO,MAAM,YAAY;AAGlB,MAAM,eAAe;AAAA,EAC1B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAGO,MAAM,YAAY,KAAK,kBAAkB,kBAAkB,gBAAgB,eAAe;",
4
+ "mappings": "AAAA,OAAO,UAAU;AACjB,SAAS,eAAe,aAAa,uBAAuB;AAGrD,MAAM,iBAAiB;AAAA,EAC5B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAGO,MAAM,oBAAoB;AAAA,EAC/B;AAAA,EACA;AAAA,EACA;AACF;AAGO,MAAM,wBAAwB;AAAA,EACnC;AAAA,EACA;AAAA,EACA;AACF;AAGO,MAAM,YAAY;AAGlB,MAAM,eAAe;AAAA,EAC1B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAGO,MAAM,YAAY;AAAA,EACvB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;",
5
5
  "names": []
6
6
  }
@@ -331,3 +331,431 @@ interface PermissionItem<TModule = string> {
331
331
  ```
332
332
 
333
333
  **Cascading behavior:** Checking a parent checks all children. Unchecking `perms[0]` (base permission) automatically unchecks all other permissions for that item.
334
+
335
+ ---
336
+
337
+ ## CrudSheet
338
+
339
+ Full-featured CRUD data sheet component. Wraps `DataSheet` with built-in search, filter form, inline editing, modal editing, Excel import/export, select mode, pagination, sorting, and topbar action integration. Uses compound component pattern with `CrudSheet.Column`, `CrudSheet.Filter`, `CrudSheet.Tools`, and `CrudSheet.Header`.
340
+
341
+ ```tsx
342
+ import { CrudSheet, type SearchResult, type SortingDef } from "@simplysm/solid";
343
+
344
+ interface User {
345
+ id?: number;
346
+ name: string;
347
+ email: string;
348
+ isDeleted?: boolean;
349
+ }
350
+
351
+ interface UserFilter {
352
+ keyword: string;
353
+ }
354
+
355
+ // Inline editing mode
356
+ <CrudSheet<User, UserFilter>
357
+ search={async (filter, page, sorts) => {
358
+ const result = await api.getUsers(filter, page, sorts);
359
+ return { items: result.items, pageCount: result.pageCount };
360
+ }}
361
+ getItemKey={(item) => item.id}
362
+ persistKey="user-crud"
363
+ itemsPerPage={20}
364
+ filterInitial={{ keyword: "" }}
365
+ inlineEdit={{
366
+ newItem: () => ({ name: "", email: "" }),
367
+ submit: async (diffs) => { await api.saveUsers(diffs); },
368
+ deleteProp: "isDeleted",
369
+ }}
370
+ >
371
+ <CrudSheet.Filter>
372
+ {(filter, setFilter) => (
373
+ <FormGroup.Item label="Keyword">
374
+ <TextInput value={filter.keyword} onValueChange={(v) => setFilter("keyword", v)} />
375
+ </FormGroup.Item>
376
+ )}
377
+ </CrudSheet.Filter>
378
+ <CrudSheet.Column key="name" header="Name" class="px-2 py-1">
379
+ {(ctx) => (
380
+ <TextInput value={ctx.item.name} onValueChange={(v) => ctx.setItem("name", v)} />
381
+ )}
382
+ </CrudSheet.Column>
383
+ <CrudSheet.Column key="email" header="Email" class="px-2 py-1">
384
+ {(ctx) => (
385
+ <TextInput value={ctx.item.email} onValueChange={(v) => ctx.setItem("email", v)} />
386
+ )}
387
+ </CrudSheet.Column>
388
+ </CrudSheet>
389
+
390
+ // Modal editing mode
391
+ <CrudSheet<User, UserFilter>
392
+ search={async (filter, page, sorts) => {
393
+ return await api.getUsers(filter, page, sorts);
394
+ }}
395
+ getItemKey={(item) => item.id}
396
+ persistKey="user-modal-crud"
397
+ itemsPerPage={20}
398
+ modalEdit={{
399
+ editItem: async (item) => {
400
+ const result = await dialog.show((close) => <UserEditDialog item={item} onClose={close} />);
401
+ return result === true;
402
+ },
403
+ deleteItems: async (items) => {
404
+ if (!confirm("Delete selected items?")) return false;
405
+ await api.deleteUsers(items.map((i) => i.id!));
406
+ return true;
407
+ },
408
+ }}
409
+ >
410
+ <CrudSheet.Column key="name" header="Name" editable class="px-2 py-1">
411
+ {(ctx) => <>{ctx.item.name}</>}
412
+ </CrudSheet.Column>
413
+ <CrudSheet.Column key="email" header="Email" class="px-2 py-1">
414
+ {(ctx) => <>{ctx.item.email}</>}
415
+ </CrudSheet.Column>
416
+ </CrudSheet>
417
+
418
+ // Select mode (for picker dialogs)
419
+ <CrudSheet<User, UserFilter>
420
+ search={async (filter, page, sorts) => {
421
+ return await api.getUsers(filter, page, sorts);
422
+ }}
423
+ getItemKey={(item) => item.id}
424
+ persistKey="user-select"
425
+ itemsPerPage={20}
426
+ selectMode="multi"
427
+ onSelect={(result) => {
428
+ // result.items: selected User[]
429
+ // result.keys: selected (string | number)[]
430
+ onConfirm(result.items);
431
+ }}
432
+ >
433
+ <CrudSheet.Column key="name" header="Name" class="px-2 py-1">
434
+ {(ctx) => <>{ctx.item.name}</>}
435
+ </CrudSheet.Column>
436
+ </CrudSheet>
437
+ ```
438
+
439
+ **CrudSheet Props:**
440
+
441
+ | Prop | Type | Default | Description |
442
+ |------|------|---------|-------------|
443
+ | `search` | `(filter: TFilter, page: number, sorts: SortingDef[]) => Promise<SearchResult<TItem>>` | **(required)** | Search function. `page=0` means no pagination |
444
+ | `getItemKey` | `(item: TItem) => string \| number \| undefined` | **(required)** | Unique key extractor for diff tracking |
445
+ | `persistKey` | `string` | - | LocalStorage key for column configuration |
446
+ | `itemsPerPage` | `number` | - | Items per page (enables pagination when set) |
447
+ | `editable` | `() => boolean` | `() => true` | Whether editing is allowed |
448
+ | `itemEditable` | `(item: TItem) => boolean` | `() => true` | Per-item edit control (modal edit mode) |
449
+ | `itemDeletable` | `(item: TItem) => boolean` | `() => true` | Per-item delete control |
450
+ | `filterInitial` | `TFilter` | - | Initial filter state |
451
+ | `items` | `TItem[]` | - | Controlled items (external state) |
452
+ | `onItemsChange` | `(items: TItem[]) => void` | - | Items change callback (controlled mode) |
453
+ | `inlineEdit` | `InlineEditConfig<TItem>` | - | Inline editing configuration (mutually exclusive with `modalEdit`) |
454
+ | `modalEdit` | `ModalEditConfig<TItem>` | - | Modal editing configuration (mutually exclusive with `inlineEdit`) |
455
+ | `excel` | `ExcelConfig<TItem>` | - | Excel download/upload configuration |
456
+ | `selectMode` | `"single" \| "multi"` | - | Select mode (disables editing, shows selection UI) |
457
+ | `onSelect` | `(result: SelectResult<TItem>) => void` | - | Select confirmation callback |
458
+ | `hideAutoTools` | `boolean` | - | Hide auto-generated toolbar buttons |
459
+ | `class` | `string` | - | CSS class |
460
+
461
+ **InlineEditConfig:**
462
+
463
+ ```typescript
464
+ interface InlineEditConfig<TItem> {
465
+ submit: (diffs: ArrayDiffs2Result<TItem>[]) => Promise<void>;
466
+ newItem: () => TItem;
467
+ deleteProp?: keyof TItem & string;
468
+ }
469
+ ```
470
+
471
+ | Field | Description |
472
+ |-------|-------------|
473
+ | `submit` | Save function receiving changed items (inserted/updated/deleted diffs) |
474
+ | `newItem` | Factory function for creating new empty rows |
475
+ | `deleteProp` | Property name for soft-delete flag (e.g., `"isDeleted"`) |
476
+
477
+ **ModalEditConfig:**
478
+
479
+ ```typescript
480
+ interface ModalEditConfig<TItem> {
481
+ editItem: (item?: TItem) => Promise<boolean>;
482
+ deleteItems?: (items: TItem[]) => Promise<boolean>;
483
+ }
484
+ ```
485
+
486
+ | Field | Description |
487
+ |-------|-------------|
488
+ | `editItem` | Open edit dialog. `undefined` = new item. Return `true` to refresh |
489
+ | `deleteItems` | Delete selected items. Return `true` to refresh |
490
+
491
+ **ExcelConfig:**
492
+
493
+ ```typescript
494
+ interface ExcelConfig<TItem> {
495
+ download: (items: TItem[]) => Promise<void>;
496
+ upload?: (file: File) => Promise<void>;
497
+ }
498
+ ```
499
+
500
+ **SelectResult:**
501
+
502
+ ```typescript
503
+ interface SelectResult<TItem> {
504
+ items: TItem[];
505
+ keys: (string | number)[];
506
+ }
507
+ ```
508
+
509
+ **Sub-components:**
510
+
511
+ - `CrudSheet.Column` -- Column definition (extends `DataSheet.Column` props + `editable`)
512
+ - `CrudSheet.Filter` -- Filter form render prop
513
+ - `CrudSheet.Tools` -- Custom toolbar render prop
514
+ - `CrudSheet.Header` -- Header area above filter
515
+
516
+ **CrudSheet.Column Props:**
517
+
518
+ Inherits all `DataSheet.Column` props (`key`, `header`, `width`, `fixed`, `hidden`, `sortable`, `resizable`, `class`, `headerContent`, `headerStyle`, `summary`, `tooltip`, `collapse`) plus:
519
+
520
+ | Prop | Type | Default | Description |
521
+ |------|------|---------|-------------|
522
+ | `editable` | `boolean` | `false` | Wrap cell with edit link (modal edit mode only) |
523
+ | `children` | `(ctx: CrudSheetCellContext<TItem>) => JSX.Element` | **(required)** | Cell render function |
524
+
525
+ **CrudSheetCellContext:**
526
+
527
+ ```typescript
528
+ interface CrudSheetCellContext<TItem> {
529
+ item: TItem;
530
+ index: number;
531
+ row: number;
532
+ depth: number;
533
+ setItem: <TKey extends keyof TItem>(key: TKey, value: TItem[TKey]) => void;
534
+ }
535
+ ```
536
+
537
+ `setItem` updates a specific field of the current row item (inline edit mode).
538
+
539
+ **CrudSheet.Filter:**
540
+
541
+ ```tsx
542
+ <CrudSheet.Filter>
543
+ {(filter, setFilter) => (
544
+ <FormGroup.Item label="Search">
545
+ <TextInput value={filter.keyword} onValueChange={(v) => setFilter("keyword", v)} />
546
+ </FormGroup.Item>
547
+ )}
548
+ </CrudSheet.Filter>
549
+ ```
550
+
551
+ Receives `filter` (store) and `setFilter` (`SetStoreFunction`) as render prop arguments.
552
+
553
+ **CrudSheet.Tools:**
554
+
555
+ ```tsx
556
+ <CrudSheet.Tools>
557
+ {(ctx) => (
558
+ <Button size="sm" onClick={() => ctx.refresh()}>Custom Refresh</Button>
559
+ )}
560
+ </CrudSheet.Tools>
561
+ ```
562
+
563
+ **CrudSheetContext (Tools render prop):**
564
+
565
+ ```typescript
566
+ interface CrudSheetContext<TItem> {
567
+ items(): TItem[];
568
+ selectedItems(): TItem[];
569
+ page(): number;
570
+ sorts(): SortingDef[];
571
+ busy(): boolean;
572
+ hasChanges(): boolean;
573
+ save(): Promise<void>;
574
+ refresh(): Promise<void>;
575
+ addItem(): void;
576
+ setPage(page: number): void;
577
+ setSorts(sorts: SortingDef[]): void;
578
+ }
579
+ ```
580
+
581
+ **CrudSheet.Header:**
582
+
583
+ ```tsx
584
+ <CrudSheet.Header>
585
+ <h2 class="p-2 text-lg font-bold">User Management</h2>
586
+ </CrudSheet.Header>
587
+ ```
588
+
589
+ Renders static content above the filter area.
590
+
591
+ **Keyboard shortcuts:**
592
+ - `Ctrl+S` -- Save (inline edit mode)
593
+ - `Ctrl+Alt+L` -- Refresh
594
+
595
+ **Topbar integration:** When used inside `Topbar.Container`, CrudSheet automatically registers Save and Refresh buttons in the topbar via `createTopbarActions`.
596
+
597
+ ---
598
+
599
+ ## CrudDetail
600
+
601
+ CRUD detail form component for single-record editing. Provides load, save, soft-delete/restore, change detection, and topbar action integration. Works both as a standalone page component and inside a `Dialog` (modal mode).
602
+
603
+ ```tsx
604
+ import { CrudDetail, type CrudDetailInfo } from "@simplysm/solid";
605
+ import { FormTable, TextInput, NumberInput } from "@simplysm/solid";
606
+
607
+ interface User {
608
+ id?: number;
609
+ name: string;
610
+ age: number;
611
+ }
612
+
613
+ // Page mode (standalone)
614
+ <CrudDetail<User>
615
+ load={async () => {
616
+ const user = await api.getUser(userId);
617
+ return {
618
+ data: user,
619
+ info: { isNew: user.id == null, isDeleted: false, lastModifiedAt: user.updatedAt },
620
+ };
621
+ }}
622
+ submit={async (data) => {
623
+ await api.saveUser(data);
624
+ return true; // return true to trigger success notification + refresh/close
625
+ }}
626
+ toggleDelete={async (del) => {
627
+ await api.toggleDeleteUser(userId, del);
628
+ return true;
629
+ }}
630
+ >
631
+ {(ctx) => (
632
+ <FormTable>
633
+ <tbody>
634
+ <tr>
635
+ <th>Name</th>
636
+ <td>
637
+ <TextInput value={ctx.data.name} onValueChange={(v) => ctx.setData("name", v)} />
638
+ </td>
639
+ </tr>
640
+ <tr>
641
+ <th>Age</th>
642
+ <td>
643
+ <NumberInput value={ctx.data.age} onValueChange={(v) => ctx.setData("age", v!)} />
644
+ </td>
645
+ </tr>
646
+ </tbody>
647
+ </FormTable>
648
+ )}
649
+ </CrudDetail>
650
+
651
+ // Modal mode (inside Dialog via useDialog)
652
+ const dialog = useDialog();
653
+
654
+ const handleEdit = async () => {
655
+ const result = await dialog.show<boolean>(
656
+ () => (
657
+ <CrudDetail<User>
658
+ load={async () => ({ data: user, info: { isNew: false, isDeleted: false } })}
659
+ submit={async (data) => { await api.saveUser(data); return true; }}
660
+ >
661
+ {(ctx) => (
662
+ <FormTable>
663
+ <tbody>
664
+ <tr>
665
+ <th>Name</th>
666
+ <td><TextInput value={ctx.data.name} onValueChange={(v) => ctx.setData("name", v)} /></td>
667
+ </tr>
668
+ </tbody>
669
+ </FormTable>
670
+ )}
671
+ </CrudDetail>
672
+ ),
673
+ { header: "Edit User", width: 500 },
674
+ );
675
+ if (result) { /* saved successfully */ }
676
+ };
677
+
678
+ // With custom tools and before/after slots
679
+ <CrudDetail<User>
680
+ load={loadUser}
681
+ submit={saveUser}
682
+ editable={hasEditPermission()}
683
+ deletable={hasDeletePermission()}
684
+ >
685
+ {(ctx) => (
686
+ <>
687
+ <CrudDetail.Tools>
688
+ <Button size="sm" variant="ghost" onClick={() => ctx.refresh()}>Custom Refresh</Button>
689
+ </CrudDetail.Tools>
690
+ <CrudDetail.Before>
691
+ <div class="p-2 bg-info-50">Info banner (outside form)</div>
692
+ </CrudDetail.Before>
693
+ <FormTable>
694
+ <tbody>
695
+ <tr>
696
+ <th>Name</th>
697
+ <td><TextInput value={ctx.data.name} onValueChange={(v) => ctx.setData("name", v)} /></td>
698
+ </tr>
699
+ </tbody>
700
+ </FormTable>
701
+ <CrudDetail.After>
702
+ <div class="p-2">Footer content (outside form)</div>
703
+ </CrudDetail.After>
704
+ </>
705
+ )}
706
+ </CrudDetail>
707
+ ```
708
+
709
+ **CrudDetail Props:**
710
+
711
+ | Prop | Type | Default | Description |
712
+ |------|------|---------|-------------|
713
+ | `load` | `() => Promise<{ data: TData; info: CrudDetailInfo }>` | **(required)** | Load function returning data and metadata |
714
+ | `children` | `(ctx: CrudDetailContext<TData>) => JSX.Element` | **(required)** | Render prop receiving context |
715
+ | `submit` | `(data: TData) => Promise<boolean \| undefined>` | - | Save function. Return `true` to trigger success notification |
716
+ | `toggleDelete` | `(del: boolean) => Promise<boolean \| undefined>` | - | Soft-delete/restore function. `del=true` for delete, `false` for restore |
717
+ | `editable` | `() => boolean` | `() => true` | Whether editing is allowed |
718
+ | `deletable` | `() => boolean` | `() => true` | Whether delete/restore is allowed |
719
+ | `data` | `TData` | - | Controlled data state |
720
+ | `onDataChange` | `(data: TData) => void` | - | Data change callback (controlled mode) |
721
+ | `class` | `string` | - | CSS class |
722
+
723
+ **CrudDetailInfo:**
724
+
725
+ ```typescript
726
+ interface CrudDetailInfo {
727
+ isNew: boolean;
728
+ isDeleted: boolean;
729
+ lastModifiedAt?: DateTime;
730
+ lastModifiedBy?: string;
731
+ }
732
+ ```
733
+
734
+ **CrudDetailContext (render prop argument):**
735
+
736
+ ```typescript
737
+ interface CrudDetailContext<TData> {
738
+ data: TData;
739
+ setData: SetStoreFunction<TData>;
740
+ info: () => CrudDetailInfo;
741
+ busy: () => boolean;
742
+ hasChanges: () => boolean;
743
+ save: () => Promise<void>;
744
+ refresh: () => Promise<void>;
745
+ }
746
+ ```
747
+
748
+ **Sub-components:**
749
+ - `CrudDetail.Tools` -- Custom toolbar buttons (rendered in the toolbar area)
750
+ - `CrudDetail.Before` -- Content rendered before the form (outside `<form>`)
751
+ - `CrudDetail.After` -- Content rendered after the form (outside `<form>`)
752
+
753
+ **Page vs Modal mode:**
754
+ - **Page mode**: Toolbar with Save/Refresh/Delete buttons appears at the top. When inside `Topbar.Container`, Save and Refresh are also registered in the topbar via `createTopbarActions`.
755
+ - **Modal mode** (inside `useDialog`): Refresh button appears in the dialog header. Save/Delete buttons appear in the bottom bar. On save/delete success, the dialog closes with `true` as the return value.
756
+
757
+ **Keyboard shortcuts:**
758
+ - `Ctrl+S` -- Save
759
+ - `Ctrl+Alt+L` -- Refresh
760
+
761
+ **Change detection:** On refresh, if unsaved changes exist, a confirmation dialog is shown. On save, if no changes exist (and not a new record), an info notification is shown instead.
@@ -59,37 +59,56 @@ Animation is automatically disabled when `prefers-reduced-motion` is set.
59
59
 
60
60
  ## Dropdown
61
61
 
62
- Positioned dropdown popup. Position is determined relative to trigger element or absolute coordinates.
62
+ Positioned dropdown popup using compound components. Trigger click auto-toggles open state.
63
63
 
64
64
  ```tsx
65
- import { Dropdown, Button } from "@simplysm/solid";
66
- import { createSignal } from "solid-js";
67
-
68
- const [open, setOpen] = createSignal(false);
69
- let triggerRef!: HTMLButtonElement;
65
+ import { Dropdown, Button, List } from "@simplysm/solid";
66
+
67
+ // Trigger/Content compound components
68
+ <Dropdown>
69
+ <Dropdown.Trigger>
70
+ <Button>Open</Button>
71
+ </Dropdown.Trigger>
72
+ <Dropdown.Content>
73
+ <p class="p-3">Dropdown content</p>
74
+ </Dropdown.Content>
75
+ </Dropdown>
70
76
 
71
- <Button ref={triggerRef} onClick={() => setOpen(!open())}>Open</Button>
72
- <Dropdown triggerRef={() => triggerRef} open={open()} onOpenChange={setOpen}>
73
- <p class="p-3">Dropdown content</p>
77
+ // Controlled open state
78
+ <Dropdown open={open()} onOpenChange={setOpen}>
79
+ <Dropdown.Trigger>
80
+ <Button>Open</Button>
81
+ </Dropdown.Trigger>
82
+ <Dropdown.Content>
83
+ <p class="p-3">Dropdown content</p>
84
+ </Dropdown.Content>
74
85
  </Dropdown>
75
86
 
76
- // Context menu (absolute position)
87
+ // Context menu (absolute position, no Trigger)
77
88
  <Dropdown position={{ x: 100, y: 200 }} open={menuOpen()} onOpenChange={setMenuOpen}>
78
- <List inset>
79
- <List.Item>Menu item 1</List.Item>
80
- <List.Item>Menu item 2</List.Item>
81
- </List>
89
+ <Dropdown.Content>
90
+ <List inset>
91
+ <List.Item>Menu item 1</List.Item>
92
+ <List.Item>Menu item 2</List.Item>
93
+ </List>
94
+ </Dropdown.Content>
82
95
  </Dropdown>
83
96
  ```
84
97
 
85
98
  | Prop | Type | Default | Description |
86
99
  |------|------|---------|-------------|
87
- | `triggerRef` | `() => HTMLElement \| undefined` | - | Trigger element reference (mutually exclusive with position) |
88
- | `position` | `{ x: number; y: number }` | - | Absolute position (mutually exclusive with triggerRef) |
100
+ | `position` | `{ x: number; y: number }` | - | Absolute position (context menu mode, no Trigger needed) |
89
101
  | `open` | `boolean` | - | Open state |
90
102
  | `onOpenChange` | `(open: boolean) => void` | - | State change callback |
91
103
  | `maxHeight` | `number` | `300` | Maximum height (px) |
104
+ | `disabled` | `boolean` | - | Disabled state (Trigger click ignored) |
92
105
  | `keyboardNav` | `boolean` | - | Enable keyboard navigation (used by Select, etc.) |
106
+ | `class` | `string` | - | Additional CSS class for popup |
107
+ | `style` | `JSX.CSSProperties` | - | Inline style for popup |
108
+
109
+ **Sub-components:**
110
+ - `Dropdown.Trigger` -- Trigger element wrapper (click to toggle)
111
+ - `Dropdown.Content` -- Dropdown popup content
93
112
 
94
113
  ---
95
114
 
@@ -106,28 +125,32 @@ import { createSignal } from "solid-js";
106
125
  const [open, setOpen] = createSignal(false);
107
126
 
108
127
  <Button onClick={() => setOpen(true)}>Open</Button>
109
- <Dialog
110
- title="Dialog Title"
111
- open={open()}
112
- onOpenChange={setOpen}
113
- closeOnBackdrop
114
- width={600}
115
- >
128
+ <Dialog open={open()} onOpenChange={setOpen} closeOnBackdrop width={600}>
129
+ <Dialog.Header>Dialog Title</Dialog.Header>
116
130
  <div class="p-4">
117
131
  Dialog content
118
132
  </div>
119
133
  </Dialog>
120
134
 
135
+ // With header action buttons
136
+ <Dialog open={open()} onOpenChange={setOpen}>
137
+ <Dialog.Header>My Dialog</Dialog.Header>
138
+ <Dialog.Action>
139
+ <Button size="sm" variant="ghost">Help</Button>
140
+ </Dialog.Action>
141
+ <div class="p-4">Dialog content</div>
142
+ </Dialog>
143
+
121
144
  // Floating mode (no backdrop)
122
- <Dialog
123
- title="Notification"
124
- open={open()}
125
- onOpenChange={setOpen}
126
- float
127
- position="bottom-right"
128
- >
145
+ <Dialog open={open()} onOpenChange={setOpen} float position="bottom-right">
146
+ <Dialog.Header>Notification</Dialog.Header>
129
147
  <div class="p-4">Floating dialog</div>
130
148
  </Dialog>
149
+
150
+ // No header (content only)
151
+ <Dialog open={open()} onOpenChange={setOpen}>
152
+ <div class="p-4">Dialog without header</div>
153
+ </Dialog>
131
154
  ```
132
155
 
133
156
  **Programmatic usage with `useDialog`:**
@@ -158,7 +181,7 @@ function MyPage() {
158
181
  const handleOpen = async () => {
159
182
  const result = await dialog.show<string>(
160
183
  () => <EditDialog />,
161
- { title: "Edit Name", width: 400, closeOnBackdrop: true },
184
+ { header: "Edit Name", width: 400, closeOnBackdrop: true },
162
185
  );
163
186
  if (result != null) {
164
187
  // result is the value passed to dialogInstance.close()
@@ -176,8 +199,6 @@ function MyPage() {
176
199
  |------|------|---------|-------------|
177
200
  | `open` | `boolean` | - | Open state |
178
201
  | `onOpenChange` | `(open: boolean) => void` | - | State change callback |
179
- | `title` | `string` | **(required)** | Modal title |
180
- | `hideHeader` | `boolean` | - | Hide header |
181
202
  | `closable` | `boolean` | `true` | Show close button |
182
203
  | `closeOnBackdrop` | `boolean` | - | Close on backdrop click |
183
204
  | `closeOnEscape` | `boolean` | `true` | Close on Escape key |
@@ -190,19 +211,28 @@ function MyPage() {
190
211
  | `minWidth` | `number` | - | Minimum width (px) |
191
212
  | `minHeight` | `number` | - | Minimum height (px) |
192
213
  | `position` | `"bottom-right" \| "top-right"` | - | Fixed position |
193
- | `headerAction` | `JSX.Element` | - | Header action area |
194
214
  | `headerStyle` | `JSX.CSSProperties \| string` | - | Header style |
195
215
  | `canDeactivate` | `() => boolean` | - | Pre-close confirmation function |
196
216
  | `onCloseComplete` | `() => void` | - | Post-close animation callback |
197
217
  | `class` | `string` | - | Additional CSS class applied to the dialog element |
198
218
 
219
+ **Sub-components:**
220
+ - `Dialog.Header` -- Dialog title (renders as `<h5>`, sets `aria-labelledby` on the dialog)
221
+ - `Dialog.Action` -- Header action area (rendered between header text and close button)
222
+
223
+ > The header bar (including close button) is only rendered when `Dialog.Header` is present. If no `Dialog.Header` is provided, the dialog renders content only with no header bar.
224
+
199
225
  **useDialog API:**
200
226
 
201
227
  | Method | Signature | Description |
202
228
  |--------|-----------|-------------|
203
229
  | `show` | `<T>(factory: () => JSX.Element, options: DialogShowOptions) => Promise<T \| undefined>` | Open dialog, returns result on close |
204
230
 
205
- `DialogShowOptions` accepts all Dialog props except `open`, `onOpenChange`, and `children`.
231
+ `DialogShowOptions` accepts all Dialog props except `open`, `onOpenChange`, `onCloseComplete`, and `children`, plus:
232
+
233
+ | Option | Type | Description |
234
+ |--------|------|-------------|
235
+ | `header` | `JSX.Element` | Dialog header content (renders inside `Dialog.Header`) |
206
236
 
207
237
  **useDialogInstance API:**
208
238