@simplysm/solid 13.0.70 → 13.0.72

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 (106) hide show
  1. package/README.md +1 -1
  2. package/dist/components/data/sheet/DataSheet.d.ts.map +1 -1
  3. package/dist/components/data/sheet/DataSheet.js +3 -6
  4. package/dist/components/data/sheet/DataSheet.js.map +1 -1
  5. package/dist/components/data/sheet/DataSheet.styles.d.ts.map +1 -1
  6. package/dist/components/data/sheet/DataSheet.styles.js +1 -1
  7. package/dist/components/data/sheet/DataSheet.styles.js.map +1 -1
  8. package/dist/components/disclosure/Dropdown.d.ts +6 -4
  9. package/dist/components/disclosure/Dropdown.d.ts.map +1 -1
  10. package/dist/components/disclosure/Dropdown.js +24 -8
  11. package/dist/components/disclosure/Dropdown.js.map +2 -2
  12. package/dist/components/disclosure/dialogZIndex.d.ts +2 -0
  13. package/dist/components/disclosure/dialogZIndex.d.ts.map +1 -1
  14. package/dist/components/disclosure/dialogZIndex.js +4 -0
  15. package/dist/components/disclosure/dialogZIndex.js.map +1 -1
  16. package/dist/components/features/crud-detail/CrudDetail.d.ts.map +1 -1
  17. package/dist/components/features/crud-detail/CrudDetail.js +16 -7
  18. package/dist/components/features/crud-detail/CrudDetail.js.map +2 -2
  19. package/dist/components/features/crud-sheet/CrudSheet.d.ts.map +1 -1
  20. package/dist/components/features/crud-sheet/CrudSheet.js +14 -5
  21. package/dist/components/features/crud-sheet/CrudSheet.js.map +2 -2
  22. package/dist/components/features/crudRegistry.d.ts +16 -0
  23. package/dist/components/features/crudRegistry.d.ts.map +1 -0
  24. package/dist/components/features/crudRegistry.js +37 -0
  25. package/dist/components/features/crudRegistry.js.map +6 -0
  26. package/dist/components/features/permission-table/PermissionTable.d.ts.map +1 -1
  27. package/dist/components/features/permission-table/PermissionTable.js +71 -86
  28. package/dist/components/features/permission-table/PermissionTable.js.map +2 -2
  29. package/dist/components/features/shared-data/SharedDataSelect.js +2 -4
  30. package/dist/components/features/shared-data/SharedDataSelect.js.map +2 -2
  31. package/dist/components/features/shared-data/SharedDataSelectList.d.ts +2 -4
  32. package/dist/components/features/shared-data/SharedDataSelectList.d.ts.map +1 -1
  33. package/dist/components/features/shared-data/SharedDataSelectList.js +11 -46
  34. package/dist/components/features/shared-data/SharedDataSelectList.js.map +2 -2
  35. package/dist/components/form-control/select/Select.d.ts.map +1 -1
  36. package/dist/components/form-control/select/Select.js +1 -1
  37. package/dist/components/form-control/select/Select.js.map +1 -1
  38. package/dist/helpers/createAppStructure.d.ts.map +1 -1
  39. package/dist/helpers/createAppStructure.js +3 -2
  40. package/dist/helpers/createAppStructure.js.map +1 -1
  41. package/dist/helpers/createHmrSafeContext.d.ts +3 -0
  42. package/dist/helpers/createHmrSafeContext.d.ts.map +1 -0
  43. package/dist/helpers/createHmrSafeContext.js +10 -0
  44. package/dist/helpers/createHmrSafeContext.js.map +6 -0
  45. package/dist/hooks/createSelectionGroup.d.ts.map +1 -1
  46. package/dist/hooks/createSelectionGroup.js +3 -2
  47. package/dist/hooks/createSelectionGroup.js.map +2 -2
  48. package/package.json +6 -5
  49. package/src/components/data/sheet/DataSheet.styles.ts +1 -1
  50. package/src/components/data/sheet/DataSheet.tsx +3 -4
  51. package/src/components/disclosure/Dropdown.tsx +31 -17
  52. package/src/components/disclosure/dialogZIndex.ts +5 -0
  53. package/src/components/features/crud-detail/CrudDetail.tsx +16 -5
  54. package/src/components/features/crud-sheet/CrudSheet.tsx +13 -3
  55. package/src/components/features/crudRegistry.ts +60 -0
  56. package/src/components/features/permission-table/PermissionTable.tsx +49 -46
  57. package/src/components/features/shared-data/SharedDataSelect.tsx +2 -2
  58. package/src/components/features/shared-data/SharedDataSelectList.tsx +11 -36
  59. package/src/components/form-control/select/Select.tsx +1 -5
  60. package/src/helpers/createAppStructure.ts +3 -2
  61. package/src/helpers/createHmrSafeContext.ts +8 -0
  62. package/src/hooks/createSelectionGroup.tsx +4 -2
  63. package/tests/components/data/List.spec.tsx +52 -52
  64. package/tests/components/data/Pagination.spec.tsx +43 -43
  65. package/tests/components/data/Table.spec.tsx +4 -4
  66. package/tests/components/data/kanban/Kanban.selection.spec.tsx +21 -21
  67. package/tests/components/data/sheet/DataSheet.spec.tsx +50 -50
  68. package/tests/components/disclosure/Collapse.spec.tsx +24 -24
  69. package/tests/components/disclosure/Dialog.spec.tsx +33 -33
  70. package/tests/components/disclosure/DialogProvider.spec.tsx +9 -9
  71. package/tests/components/disclosure/Dropdown.spec.tsx +134 -14
  72. package/tests/components/disclosure/Tabs.spec.tsx +21 -21
  73. package/tests/components/disclosure/dialogZIndex.spec.ts +45 -0
  74. package/tests/components/display/Alert.spec.tsx +4 -4
  75. package/tests/components/display/Barcode.spec.tsx +7 -7
  76. package/tests/components/display/Card.spec.tsx +3 -3
  77. package/tests/components/display/Link.spec.tsx +5 -5
  78. package/tests/components/display/Tag.spec.tsx +4 -4
  79. package/tests/components/features/address/AddressSearch.spec.tsx +3 -3
  80. package/tests/components/features/crudRegistry.spec.ts +119 -0
  81. package/tests/components/features/data-select-button/DataSelectButton.spec.tsx +8 -8
  82. package/tests/components/features/permission-table/PermissionTable.spec.tsx +43 -43
  83. package/tests/components/features/shared-data/SharedDataSelectList.spec.tsx +2 -17
  84. package/tests/components/feedback/busy/BusyContainer.spec.tsx +7 -7
  85. package/tests/components/feedback/notification/NotificationBell.spec.tsx +9 -9
  86. package/tests/components/feedback/print/Print.spec.tsx +4 -4
  87. package/tests/components/form-control/Button.spec.tsx +18 -18
  88. package/tests/components/form-control/checkbox/Checkbox.spec.tsx +20 -20
  89. package/tests/components/form-control/checkbox/CheckboxGroup.spec.tsx +12 -12
  90. package/tests/components/form-control/checkbox/Radio.spec.tsx +21 -21
  91. package/tests/components/form-control/checkbox/RadioGroup.spec.tsx +12 -12
  92. package/tests/components/form-control/color-picker/ColorPicker.spec.tsx +10 -10
  93. package/tests/components/form-control/combobox/Combobox.spec.tsx +16 -16
  94. package/tests/components/form-control/combobox/ComboboxItem.spec.tsx +7 -7
  95. package/tests/components/form-control/date-range-picker/DateRangePicker.spec.tsx +24 -24
  96. package/tests/components/form-control/field/DatePicker.spec.tsx +50 -50
  97. package/tests/components/form-control/field/DateTimePicker.spec.tsx +47 -47
  98. package/tests/components/form-control/field/NumberInput.spec.tsx +54 -54
  99. package/tests/components/form-control/field/TextInput.spec.tsx +49 -49
  100. package/tests/components/form-control/field/Textarea.spec.tsx +33 -33
  101. package/tests/components/form-control/field/TimePicker.spec.tsx +42 -42
  102. package/tests/components/form-control/numpad/Numpad.spec.tsx +40 -40
  103. package/tests/components/form-control/select/Select.spec.tsx +9 -9
  104. package/tests/components/form-control/select/SelectItem.spec.tsx +10 -10
  105. package/tests/helpers/createAppStructure.spec.tsx +57 -57
  106. package/tests/helpers/mergeStyles.spec.ts +31 -31
@@ -18,6 +18,7 @@ import { twMerge } from "tailwind-merge";
18
18
  import { mergeStyles } from "../../helpers/mergeStyles";
19
19
  import { createSlotComponent } from "../../helpers/createSlotComponent";
20
20
  import { borderSubtle } from "../../styles/tokens.styles";
21
+ import { tabbable } from "tabbable";
21
22
 
22
23
  // --- DropdownContext (internal) ---
23
24
 
@@ -70,13 +71,15 @@ export interface DropdownProps {
70
71
  * Enable keyboard navigation (used in Select, etc)
71
72
  *
72
73
  * When direction=down:
73
- * - ArrowDown from trigger -> focus first focusable item
74
- * - ArrowUp from first item -> focus trigger
74
+ * - ArrowDown from trigger -> focus first tabbable item in popup
75
+ * - ArrowUp/ArrowDown within popup -> navigate between tabbable items
76
+ * - ArrowUp from first tabbable -> focus trigger
75
77
  * - ArrowUp from trigger -> close
76
78
  *
77
79
  * When direction=up:
78
- * - ArrowUp from trigger -> focus last focusable item
79
- * - ArrowDown from last item -> focus trigger
80
+ * - ArrowUp from trigger -> focus last tabbable item in popup
81
+ * - ArrowUp/ArrowDown within popup -> navigate between tabbable items
82
+ * - ArrowDown from last tabbable -> focus trigger
80
83
  * - ArrowDown from trigger -> close
81
84
  */
82
85
  keyboardNav?: boolean;
@@ -319,11 +322,7 @@ export const Dropdown: DropdownComponent = ((props: DropdownProps) => {
319
322
  if (!popup) return;
320
323
 
321
324
  const dir = direction();
322
- const focusables = [
323
- ...popup.querySelectorAll<HTMLElement>(
324
- '[tabindex]:not([tabindex="-1"]), button, [data-list-item]',
325
- ),
326
- ];
325
+ const focusables = tabbable(popup);
327
326
 
328
327
  if (dir === "down") {
329
328
  if (e.key === "ArrowDown" && focusables.length > 0) {
@@ -354,16 +353,30 @@ export const Dropdown: DropdownComponent = ((props: DropdownProps) => {
354
353
 
355
354
  if (!triggerRef) return;
356
355
 
356
+ const popup = popupRef();
357
+ if (!popup) return;
358
+
357
359
  const dir = direction();
360
+ const allTabbable = tabbable(popup);
361
+ const current = (document.activeElement ?? e.target) as HTMLElement;
362
+ const currentIdx = allTabbable.indexOf(current);
358
363
 
359
- // If ArrowUp/ArrowDown not handled in popup (first/last item)
360
- // Move focus to trigger
361
- if (dir === "down" && e.key === "ArrowUp") {
362
- e.preventDefault();
363
- triggerRef.focus();
364
- } else if (dir === "up" && e.key === "ArrowDown") {
365
- e.preventDefault();
366
- triggerRef.focus();
364
+ if (e.key === "ArrowUp") {
365
+ if (currentIdx > 0) {
366
+ e.preventDefault();
367
+ allTabbable[currentIdx - 1].focus();
368
+ } else if (dir === "down") {
369
+ e.preventDefault();
370
+ triggerRef.focus();
371
+ }
372
+ } else if (e.key === "ArrowDown") {
373
+ if (currentIdx >= 0 && currentIdx < allTabbable.length - 1) {
374
+ e.preventDefault();
375
+ allTabbable[currentIdx + 1].focus();
376
+ } else if (dir === "up") {
377
+ e.preventDefault();
378
+ triggerRef.focus();
379
+ }
367
380
  }
368
381
  };
369
382
 
@@ -432,6 +445,7 @@ export const Dropdown: DropdownComponent = ((props: DropdownProps) => {
432
445
  ref={(el) => {
433
446
  triggerRef = el;
434
447
  }}
448
+ tabIndex={-1}
435
449
  data-dropdown-trigger
436
450
  onClick={toggle}
437
451
  onKeyDown={handleTriggerKeyDown}
@@ -46,3 +46,8 @@ function reindex(): void {
46
46
  export function isTopmost(el: HTMLElement): boolean {
47
47
  return stack.length > 0 && stack[stack.length - 1] === el;
48
48
  }
49
+
50
+ /** Get the topmost (front-most) Dialog element, or null if none are open */
51
+ export function getTopmostDialog(): HTMLElement | null {
52
+ return stack.length > 0 ? stack[stack.length - 1] : null;
53
+ }
@@ -2,12 +2,15 @@ import {
2
2
  children,
3
3
  createMemo,
4
4
  createSignal,
5
+ createUniqueId,
5
6
  type JSX,
7
+ onCleanup,
6
8
  onMount,
7
9
  Show,
8
10
  splitProps,
9
11
  useContext,
10
12
  } from "solid-js";
13
+ import { registerCrud, unregisterCrud, activateCrud, isActiveCrud } from "../crudRegistry";
11
14
  import { reconcile, unwrap } from "solid-js/store";
12
15
  import { createControllableStore } from "../../../hooks/createControllableStore";
13
16
  import { objClone, objEqual } from "@simplysm/core-common";
@@ -82,6 +85,8 @@ const CrudDetailBase = <TData extends object>(props: CrudDetailProps<TData>) =>
82
85
 
83
86
  let formRef: HTMLFormElement | undefined;
84
87
 
88
+ const crudId = createUniqueId();
89
+
85
90
  // -- Load --
86
91
  async function doLoad() {
87
92
  setBusyCount((c) => c + 1);
@@ -100,6 +105,10 @@ const CrudDetailBase = <TData extends object>(props: CrudDetailProps<TData>) =>
100
105
  onMount(() => {
101
106
  void doLoad();
102
107
  });
108
+ onCleanup(() => unregisterCrud(crudId));
109
+
110
+ createEventListener(() => formRef, "pointerdown", () => activateCrud(crudId));
111
+ createEventListener(() => formRef, "focusin", () => activateCrud(crudId));
103
112
 
104
113
  // -- Change Detection --
105
114
  function hasChanges(): boolean {
@@ -177,13 +186,15 @@ const CrudDetailBase = <TData extends object>(props: CrudDetailProps<TData>) =>
177
186
 
178
187
  // -- Keyboard Shortcuts --
179
188
  createEventListener(document, "keydown", (e: KeyboardEvent) => {
180
- if (!formRef?.contains(document.activeElement)) return;
189
+ if (!isActiveCrud(crudId)) return;
181
190
  if (e.ctrlKey && e.key === "s") {
182
191
  e.preventDefault();
183
- formRef.requestSubmit();
192
+ e.stopImmediatePropagation();
193
+ formRef?.requestSubmit();
184
194
  }
185
195
  if (e.ctrlKey && e.altKey && e.key === "l") {
186
196
  e.preventDefault();
197
+ e.stopImmediatePropagation();
187
198
  void handleRefresh();
188
199
  }
189
200
  });
@@ -273,11 +284,11 @@ const CrudDetailBase = <TData extends object>(props: CrudDetailProps<TData>) =>
273
284
  <BusyContainer
274
285
  ready={ready()}
275
286
  busy={busyCount() > 0}
276
- class={clsx("flex h-full flex-col", local.class)}
287
+ class={clsx("flex h-full flex-col gap-2", local.class)}
277
288
  >
278
289
  {/* Toolbar */}
279
290
  <Show when={(!isModal && !topbarCtx) || defs().tools}>
280
- <div class="flex gap-2 p-2 pb-0">
291
+ <div class="flex gap-2 pb-0">
281
292
  <Show when={!topbarCtx && !isModal}>
282
293
  <Show when={canEdit() && local.submit}>
283
294
  <Button
@@ -324,7 +335,7 @@ const CrudDetailBase = <TData extends object>(props: CrudDetailProps<TData>) =>
324
335
  <Show when={defs().before}>{(beforeDef) => beforeDef().children}</Show>
325
336
 
326
337
  {/* Form */}
327
- <form ref={formRef} class="flex-1 overflow-auto p-4" onSubmit={handleFormSubmit}>
338
+ <form ref={(el) => { formRef = el; registerCrud(crudId, el); }} class="flex-1 overflow-auto" onSubmit={handleFormSubmit}>
328
339
  {formContent()}
329
340
  </form>
330
341
 
@@ -3,8 +3,10 @@ import {
3
3
  createEffect,
4
4
  createMemo,
5
5
  createSignal,
6
+ createUniqueId,
6
7
  For,
7
8
  type JSX,
9
+ onCleanup,
8
10
  Show,
9
11
  splitProps,
10
12
  useContext,
@@ -41,6 +43,7 @@ import {
41
43
  IconTrashOff,
42
44
  IconUpload,
43
45
  } from "@tabler/icons-solidjs";
46
+ import { registerCrud, unregisterCrud, activateCrud, isActiveCrud } from "../crudRegistry";
44
47
  import { CrudSheetColumn, isCrudSheetColumnDef } from "./CrudSheetColumn";
45
48
  import { CrudSheetFilter, isCrudSheetFilterDef } from "./CrudSheetFilter";
46
49
  import { CrudSheetTools, isCrudSheetToolsDef } from "./CrudSheetTools";
@@ -134,6 +137,11 @@ const CrudSheetBase = <TItem, TFilter extends Record<string, any>>(
134
137
 
135
138
  let formRef: HTMLFormElement | undefined;
136
139
 
140
+ const crudId = createUniqueId();
141
+ onCleanup(() => unregisterCrud(crudId));
142
+ createEventListener(() => formRef, "pointerdown", () => activateCrud(crudId));
143
+ createEventListener(() => formRef, "focusin", () => activateCrud(crudId));
144
+
137
145
  createEffect(() => {
138
146
  void doRefresh();
139
147
  });
@@ -389,13 +397,15 @@ const CrudSheetBase = <TItem, TFilter extends Record<string, any>>(
389
397
 
390
398
  // -- Keyboard Shortcuts --
391
399
  createEventListener(document, "keydown", async (e: KeyboardEvent) => {
392
- if (!formRef?.contains(document.activeElement)) return;
400
+ if (!isActiveCrud(crudId)) return;
393
401
  if (e.ctrlKey && e.key === "s" && !isSelectMode()) {
394
402
  e.preventDefault();
395
- formRef.requestSubmit();
403
+ e.stopImmediatePropagation();
404
+ formRef?.requestSubmit();
396
405
  }
397
406
  if (e.ctrlKey && e.altKey && e.key === "l") {
398
407
  e.preventDefault();
408
+ e.stopImmediatePropagation();
399
409
  if (!checkIgnoreChanges()) return;
400
410
  await doRefresh();
401
411
  }
@@ -602,7 +612,7 @@ const CrudSheetBase = <TItem, TFilter extends Record<string, any>>(
602
612
  </Show>
603
613
 
604
614
  {/* DataSheet */}
605
- <form ref={formRef} class="flex-1 overflow-hidden p-2 pt-1" onSubmit={handleFormSubmit}>
615
+ <form ref={(el) => { formRef = el; registerCrud(crudId, el); }} class="flex-1 overflow-hidden p-2 pt-1" onSubmit={handleFormSubmit}>
606
616
  <DataSheet
607
617
  class="h-full"
608
618
  items={items}
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Crud activation registry
3
+ *
4
+ * Tracks mounted CrudDetail/CrudSheet instances and determines which one
5
+ * should respond to keyboard shortcuts (Ctrl+S, Ctrl+Alt+L).
6
+ *
7
+ * Priority rules:
8
+ * 1. If a Dialog is open, only cruds inside the topmost Dialog are candidates.
9
+ * 2. Among candidates, the most recently activated (interacted) crud wins.
10
+ * 3. On mount, cruds are auto-activated (last mounted = active).
11
+ */
12
+
13
+ import { getTopmostDialog } from "../disclosure/dialogZIndex";
14
+
15
+ interface CrudEntry {
16
+ id: string;
17
+ formEl: HTMLFormElement;
18
+ lastActivatedAt: number;
19
+ }
20
+
21
+ const entries: CrudEntry[] = [];
22
+ let _counter = 0;
23
+
24
+ export function registerCrud(id: string, formEl: HTMLFormElement): void {
25
+ const existing = entries.find((e) => e.id === id);
26
+ if (existing) return;
27
+ entries.push({ id, formEl, lastActivatedAt: ++_counter });
28
+ }
29
+
30
+ export function unregisterCrud(id: string): void {
31
+ const idx = entries.findIndex((e) => e.id === id);
32
+ if (idx >= 0) entries.splice(idx, 1);
33
+ }
34
+
35
+ export function activateCrud(id: string): void {
36
+ const entry = entries.find((e) => e.id === id);
37
+ if (entry) entry.lastActivatedAt = ++_counter;
38
+ }
39
+
40
+ export function isActiveCrud(id: string): boolean {
41
+ const entry = entries.find((e) => e.id === id);
42
+ if (!entry) return false;
43
+
44
+ const topDialog = getTopmostDialog();
45
+
46
+ const candidates = topDialog
47
+ ? entries.filter((e) => topDialog.contains(e.formEl))
48
+ : entries;
49
+
50
+ if (candidates.length === 0) return false;
51
+
52
+ let best = candidates[0];
53
+ for (let i = 1; i < candidates.length; i++) {
54
+ if (candidates[i].lastActivatedAt > best.lastActivatedAt) {
55
+ best = candidates[i];
56
+ }
57
+ }
58
+
59
+ return best.id === id;
60
+ }
@@ -10,7 +10,6 @@ import {
10
10
  splitProps,
11
11
  } from "solid-js";
12
12
  import clsx from "clsx";
13
- import { twMerge } from "tailwind-merge";
14
13
  import { DataSheet } from "../../data/sheet/DataSheet";
15
14
  import { Checkbox } from "../../form-control/checkbox/Checkbox";
16
15
  import { borderDefault } from "../../../styles/tokens.styles";
@@ -261,51 +260,55 @@ export const PermissionTable: Component<PermissionTableProps> = (props) => {
261
260
  };
262
261
 
263
262
  return (
264
- <div data-permission-table class={twMerge(local.class)} style={local.style}>
265
- <DataSheet
266
- items={visibleItems()}
267
- getChildren={getChildren}
268
- expandedItems={expandedItems()}
269
- onExpandedItemsChange={setExpandedItems}
270
- hideConfigBar
263
+ <DataSheet
264
+ data-permission-table
265
+ items={visibleItems()}
266
+ getChildren={getChildren}
267
+ expandedItems={expandedItems()}
268
+ onExpandedItemsChange={setExpandedItems}
269
+ hideConfigBar
270
+ >
271
+ <DataSheet.Column
272
+ key="title"
273
+ header={i18n?.t("permissionTable.permissionItem") ?? "Permission Item"}
274
+ sortable={false}
275
+ resizable={false}
271
276
  >
272
- <DataSheet.Column key="title" header={i18n?.t("permissionTable.permissionItem") ?? "Permission Item"} sortable={false} resizable={false}>
273
- {(ctx) => {
274
- const item = ctx.item as AppPerm;
275
- return (
276
- <div class={titleCellClass}>
277
- <For each={Array.from({ length: ctx.depth })}>
278
- {() => (
279
- <div class={indentGuideWrapperClass}>
280
- <div class={indentGuideLineClass} />
281
- </div>
282
- )}
283
- </For>
284
- <span class="py-1">{item.title}</span>
285
- </div>
286
- );
287
- }}
288
- </DataSheet.Column>
289
- <For each={allPerms()}>
290
- {(perm) => (
291
- <DataSheet.Column key={`perm-${perm}`} header={perm} sortable={false} resizable={false}>
292
- {(ctx) => {
293
- const item = ctx.item as AppPerm;
294
- return (
295
- <Show when={hasPermInTree(item, perm)}>
296
- <Checkbox
297
- value={isGroupPermChecked(item, perm, currentValue())}
298
- onValueChange={(checked) => handlePermChange(item, perm, checked)}
299
- disabled={local.disabled || isPermDisabled(item, perm, currentValue())}
300
- inset
301
- />
302
- </Show>
303
- );
304
- }}
305
- </DataSheet.Column>
306
- )}
307
- </For>
308
- </DataSheet>
309
- </div>
277
+ {(ctx) => {
278
+ const item = ctx.item as AppPerm;
279
+ return (
280
+ <div class={titleCellClass}>
281
+ <For each={Array.from({ length: ctx.depth })}>
282
+ {() => (
283
+ <div class={indentGuideWrapperClass}>
284
+ <div class={indentGuideLineClass} />
285
+ </div>
286
+ )}
287
+ </For>
288
+ <span class="py-1">{item.title}</span>
289
+ </div>
290
+ );
291
+ }}
292
+ </DataSheet.Column>
293
+ <For each={allPerms()}>
294
+ {(perm) => (
295
+ <DataSheet.Column key={`perm-${perm}`} header={perm} sortable={false} resizable={false}>
296
+ {(ctx) => {
297
+ const item = ctx.item as AppPerm;
298
+ return (
299
+ <Show when={hasPermInTree(item, perm)}>
300
+ <Checkbox
301
+ value={isGroupPermChecked(item, perm, currentValue())}
302
+ onValueChange={(checked) => handlePermChange(item, perm, checked)}
303
+ disabled={local.disabled || isPermDisabled(item, perm, currentValue())}
304
+ inset
305
+ />
306
+ </Show>
307
+ );
308
+ }}
309
+ </DataSheet.Column>
310
+ )}
311
+ </For>
312
+ </DataSheet>
310
313
  );
311
314
  };
@@ -90,12 +90,12 @@ export function SharedDataSelect<TItem>(props: SharedDataSelectProps<TItem>): JS
90
90
  <Select.ItemTemplate>{local.children}</Select.ItemTemplate>
91
91
  {local.modal && (
92
92
  <Select.Action onClick={() => void handleOpenModal()} aria-label={i18n?.t("sharedDataSelect.search") ?? "Search"}>
93
- <Icon icon={IconSearch} size="1em" />
93
+ <Icon icon={IconSearch} />
94
94
  </Select.Action>
95
95
  )}
96
96
  {local.editModal && (
97
97
  <Select.Action onClick={() => void handleOpenEditModal()} aria-label={i18n?.t("sharedDataSelect.edit") ?? "Edit"}>
98
- <Icon icon={IconEdit} size="1em" />
98
+ <Icon icon={IconEdit} />
99
99
  </Select.Action>
100
100
  )}
101
101
  </Select>
@@ -1,14 +1,10 @@
1
1
  import { createEffect, createMemo, createSignal, For, type JSX, Show, splitProps } from "solid-js";
2
- import { IconExternalLink } from "@tabler/icons-solidjs";
3
2
  import clsx from "clsx";
4
3
  import { twMerge } from "tailwind-merge";
5
4
  import { type SharedDataAccessor } from "../../../providers/shared-data/SharedDataContext";
6
5
  import { List } from "../../data/list/List";
7
6
  import { Pagination } from "../../data/Pagination";
8
- import { Button } from "../../form-control/Button";
9
- import { Icon } from "../../display/Icon";
10
7
  import { TextInput } from "../../form-control/field/TextInput";
11
- import { useDialog } from "../../disclosure/DialogContext";
12
8
  import { useI18nOptional } from "../../../providers/i18n/I18nContext";
13
9
  import { textMuted } from "../../../styles/tokens.styles";
14
10
  import { createSlotSignal } from "../../../hooks/createSlotSignal";
@@ -39,10 +35,8 @@ export interface SharedDataSelectListProps<TItem> {
39
35
  canChange?: (item: TItem | undefined) => boolean | Promise<boolean>;
40
36
  /** Page size (shows Pagination if provided) */
41
37
  pageSize?: number;
42
- /** Header text */
43
- header?: string;
44
- /** Management modal component factory */
45
- modal?: () => JSX.Element;
38
+ /** Header content */
39
+ header?: JSX.Element;
46
40
 
47
41
  /** Compound sub-components (ItemTemplate, Filter) */
48
42
  children?: JSX.Element;
@@ -57,8 +51,6 @@ export interface SharedDataSelectListProps<TItem> {
57
51
 
58
52
  const containerClass = clsx("flex-col gap-1");
59
53
 
60
- const headerClass = clsx("flex items-center gap-1 px-2 py-1 text-sm font-semibold");
61
-
62
54
  // ─── Component ───────────────────────────────────────────
63
55
 
64
56
  export interface SharedDataSelectListComponent {
@@ -83,10 +75,8 @@ export const SharedDataSelectList: SharedDataSelectListComponent = (<TItem,>(
83
75
  "canChange",
84
76
  "pageSize",
85
77
  "header",
86
- "modal",
87
78
  ]);
88
79
 
89
- const dialog = useDialog();
90
80
  const i18n = useI18nOptional();
91
81
 
92
82
  // ─── Slot signals ──────────────────────────────────────
@@ -192,13 +182,6 @@ export const SharedDataSelectList: SharedDataSelectListComponent = (<TItem,>(
192
182
  }
193
183
  };
194
184
 
195
- // ─── Open modal ────────────────────────────────────────
196
-
197
- const handleOpenModal = async () => {
198
- if (!local.modal) return;
199
- await dialog.show(local.modal, {});
200
- };
201
-
202
185
  // ─── Render ────────────────────────────────────────────
203
186
 
204
187
  return (
@@ -212,26 +195,18 @@ export const SharedDataSelectList: SharedDataSelectListComponent = (<TItem,>(
212
195
  style={local.style}
213
196
  >
214
197
  {/* Header */}
215
- <Show when={local.header != null || local.modal != null}>
216
- <div class={headerClass}>
217
- <Show when={local.header != null}>{local.header}</Show>
218
- <Show when={local.modal != null}>
219
- <Button size="sm" onClick={() => void handleOpenModal()}>
220
- <Icon icon={IconExternalLink} />
221
- </Button>
222
- </Show>
223
- </div>
224
- </Show>
198
+ <Show when={local.header != null}>{local.header}</Show>
225
199
 
226
200
  {/* Search input: when Filter compound is absent and getSearchText exists */}
227
201
  <Show when={!filter() && local.data.getSearchText}>
228
- <TextInput
229
- value={searchText()}
230
- onValueChange={setSearchText}
231
- placeholder={i18n?.t("sharedDataSelectList.searchPlaceholder") ?? "Search..."}
232
- size="sm"
233
- inset
234
- />
202
+ <div class={"p-1"}>
203
+ <TextInput
204
+ value={searchText()}
205
+ onValueChange={setSearchText}
206
+ placeholder={i18n?.t("sharedDataSelectList.searchPlaceholder") ?? "Search..."}
207
+ class={"w-full"}
208
+ />
209
+ </div>
235
210
  </Show>
236
211
 
237
212
  {/* Custom Filter */}
@@ -44,11 +44,7 @@ const searchInputClass = clsx(
44
44
  "w-full",
45
45
  "rounded-none",
46
46
  "border-0 border-b",
47
- borderSubtle,
48
- "bg-transparent dark:bg-transparent",
49
- "h-auto",
50
- "py-1.5",
51
- "text-sm",
47
+ borderSubtle
52
48
  );
53
49
 
54
50
  // Select all/deselect all button area styles
@@ -1,5 +1,6 @@
1
1
  import type { Component, ParentComponent } from "solid-js";
2
- import { type Accessor, createContext, createMemo, createRoot, useContext } from "solid-js";
2
+ import { type Accessor, createMemo, createRoot, useContext } from "solid-js";
3
+ import { createHmrSafeContext } from "./createHmrSafeContext";
3
4
  import type { IconProps } from "@tabler/icons-solidjs";
4
5
 
5
6
  // ── Input Types ──
@@ -497,7 +498,7 @@ export function createAppStructure<TModule, const TItems extends AppStructureIte
497
498
  } {
498
499
  type TRet = AppStructure<TModule> & { perms: InferPerms<TItems> };
499
500
 
500
- const Ctx = createContext<TRet>();
501
+ const Ctx = createHmrSafeContext<TRet>("AppStructure");
501
502
 
502
503
  const AppStructureProvider: ParentComponent = (props) => {
503
504
  const structure = buildAppStructure(getOpts());
@@ -0,0 +1,8 @@
1
+ import { type Context, createContext } from "solid-js";
2
+
3
+ const CACHE_KEY = "__simplysm_ctx__";
4
+ const cache = ((globalThis as unknown as Record<string, Record<string, unknown>>)[CACHE_KEY] ??= {});
5
+
6
+ export function createHmrSafeContext<TValue>(key: string): Context<TValue | undefined> {
7
+ return (cache[key] ??= createContext<TValue>()) as Context<TValue | undefined>;
8
+ }
@@ -1,11 +1,11 @@
1
1
  import {
2
2
  type JSX,
3
3
  type ParentComponent,
4
- createContext,
5
4
  createMemo,
6
5
  splitProps,
7
6
  useContext,
8
7
  } from "solid-js";
8
+ import { createHmrSafeContext } from "../helpers/createHmrSafeContext";
9
9
  import { twMerge } from "tailwind-merge";
10
10
  import { createControllableSignal } from "./createControllableSignal";
11
11
  import { Invalid } from "../components/form-control/Invalid";
@@ -106,7 +106,9 @@ export function createSelectionGroup(config: MultiGroupConfig | SingleGroupConfi
106
106
  Item: <TValue = unknown>(props: SelectionGroupItemProps<TValue>) => JSX.Element;
107
107
  };
108
108
  } {
109
- const Context = createContext<MultiSelectContext | SingleSelectContext>();
109
+ const Context = createHmrSafeContext<MultiSelectContext | SingleSelectContext>(
110
+ `SelectionGroup_${config.contextName}`,
111
+ );
110
112
  const ItemComponent = config.ItemComponent;
111
113
 
112
114
  function ItemInner<TValue>(props: SelectionGroupItemProps<TValue>) {