@simplysm/solid 13.0.71 → 13.0.74

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 (215) hide show
  1. package/README.md +209 -202
  2. package/dist/components/data/calendar/Calendar.d.ts.map +1 -1
  3. package/dist/components/data/calendar/Calendar.js +3 -11
  4. package/dist/components/data/calendar/Calendar.js.map +2 -2
  5. package/dist/components/data/sheet/DataSheet.d.ts.map +1 -1
  6. package/dist/components/data/sheet/DataSheet.js +13 -16
  7. package/dist/components/data/sheet/DataSheet.js.map +2 -2
  8. package/dist/components/data/sheet/DataSheet.styles.d.ts.map +1 -1
  9. package/dist/components/data/sheet/DataSheet.styles.js +1 -1
  10. package/dist/components/data/sheet/DataSheet.styles.js.map +1 -1
  11. package/dist/components/data/sheet/DataSheetConfigDialog.d.ts.map +1 -1
  12. package/dist/components/data/sheet/DataSheetConfigDialog.js +27 -9
  13. package/dist/components/data/sheet/DataSheetConfigDialog.js.map +2 -2
  14. package/dist/components/disclosure/Dialog.d.ts +1 -1
  15. package/dist/components/disclosure/Dialog.d.ts.map +1 -1
  16. package/dist/components/disclosure/Dialog.js +5 -5
  17. package/dist/components/disclosure/Dialog.js.map +2 -2
  18. package/dist/components/disclosure/dialogZIndex.d.ts +1 -1
  19. package/dist/components/features/crud-detail/CrudDetail.js +23 -23
  20. package/dist/components/features/crud-detail/CrudDetail.js.map +2 -2
  21. package/dist/components/features/crud-sheet/CrudSheet.js +49 -49
  22. package/dist/components/features/crud-sheet/CrudSheet.js.map +2 -2
  23. package/dist/components/features/crud-sheet/types.d.ts +4 -4
  24. package/dist/components/features/crud-sheet/types.d.ts.map +1 -1
  25. package/dist/components/features/data-select-button/DataSelectButton.d.ts +25 -7
  26. package/dist/components/features/data-select-button/DataSelectButton.d.ts.map +1 -1
  27. package/dist/components/features/data-select-button/DataSelectButton.js +27 -12
  28. package/dist/components/features/data-select-button/DataSelectButton.js.map +2 -2
  29. package/dist/components/features/permission-table/PermissionTable.js +4 -4
  30. package/dist/components/features/permission-table/PermissionTable.js.map +2 -2
  31. package/dist/components/features/shared-data/SharedDataSelect.d.ts +22 -10
  32. package/dist/components/features/shared-data/SharedDataSelect.d.ts.map +1 -1
  33. package/dist/components/features/shared-data/SharedDataSelect.js +113 -29
  34. package/dist/components/features/shared-data/SharedDataSelect.js.map +2 -2
  35. package/dist/components/features/shared-data/SharedDataSelectButton.d.ts +3 -3
  36. package/dist/components/features/shared-data/SharedDataSelectButton.d.ts.map +1 -1
  37. package/dist/components/features/shared-data/SharedDataSelectButton.js.map +1 -1
  38. package/dist/components/features/shared-data/SharedDataSelectList.js +5 -4
  39. package/dist/components/features/shared-data/SharedDataSelectList.js.map +2 -2
  40. package/dist/components/feedback/notification/NotificationBanner.js +3 -3
  41. package/dist/components/feedback/notification/NotificationBanner.js.map +2 -2
  42. package/dist/components/feedback/notification/NotificationBell.d.ts.map +1 -1
  43. package/dist/components/feedback/notification/NotificationBell.js +12 -5
  44. package/dist/components/feedback/notification/NotificationBell.js.map +2 -2
  45. package/dist/components/feedback/notification/NotificationProvider.d.ts.map +1 -1
  46. package/dist/components/feedback/notification/NotificationProvider.js +3 -1
  47. package/dist/components/feedback/notification/NotificationProvider.js.map +2 -2
  48. package/dist/components/form-control/ThemeToggle.d.ts.map +1 -1
  49. package/dist/components/form-control/ThemeToggle.js +9 -6
  50. package/dist/components/form-control/ThemeToggle.js.map +2 -2
  51. package/dist/components/form-control/checkbox/Checkbox.d.ts.map +1 -1
  52. package/dist/components/form-control/checkbox/Checkbox.js +3 -1
  53. package/dist/components/form-control/checkbox/Checkbox.js.map +2 -2
  54. package/dist/components/form-control/checkbox/CheckboxGroup.js +1 -1
  55. package/dist/components/form-control/checkbox/CheckboxGroup.js.map +2 -2
  56. package/dist/components/form-control/checkbox/Radio.d.ts.map +1 -1
  57. package/dist/components/form-control/checkbox/Radio.js +3 -1
  58. package/dist/components/form-control/checkbox/Radio.js.map +2 -2
  59. package/dist/components/form-control/checkbox/RadioGroup.js +1 -1
  60. package/dist/components/form-control/checkbox/RadioGroup.js.map +2 -2
  61. package/dist/components/form-control/color-picker/ColorPicker.d.ts.map +1 -1
  62. package/dist/components/form-control/color-picker/ColorPicker.js +3 -1
  63. package/dist/components/form-control/color-picker/ColorPicker.js.map +2 -2
  64. package/dist/components/form-control/combobox/Combobox.d.ts.map +1 -1
  65. package/dist/components/form-control/combobox/Combobox.js +9 -5
  66. package/dist/components/form-control/combobox/Combobox.js.map +2 -2
  67. package/dist/components/form-control/date-range-picker/DateRangePicker.js +9 -9
  68. package/dist/components/form-control/date-range-picker/DateRangePicker.js.map +2 -2
  69. package/dist/components/form-control/editor/EditorToolbar.js +3 -3
  70. package/dist/components/form-control/editor/EditorToolbar.js.map +2 -2
  71. package/dist/components/form-control/field/DatePicker.d.ts.map +1 -1
  72. package/dist/components/form-control/field/DatePicker.js +9 -3
  73. package/dist/components/form-control/field/DatePicker.js.map +2 -2
  74. package/dist/components/form-control/field/DateTimePicker.d.ts.map +1 -1
  75. package/dist/components/form-control/field/DateTimePicker.js +9 -3
  76. package/dist/components/form-control/field/DateTimePicker.js.map +2 -2
  77. package/dist/components/form-control/field/NumberInput.d.ts.map +1 -1
  78. package/dist/components/form-control/field/NumberInput.js +9 -3
  79. package/dist/components/form-control/field/NumberInput.js.map +2 -2
  80. package/dist/components/form-control/field/TextInput.d.ts.map +1 -1
  81. package/dist/components/form-control/field/TextInput.js +10 -4
  82. package/dist/components/form-control/field/TextInput.js.map +2 -2
  83. package/dist/components/form-control/field/Textarea.d.ts.map +1 -1
  84. package/dist/components/form-control/field/Textarea.js +9 -3
  85. package/dist/components/form-control/field/Textarea.js.map +2 -2
  86. package/dist/components/form-control/field/TimePicker.d.ts.map +1 -1
  87. package/dist/components/form-control/field/TimePicker.js +9 -3
  88. package/dist/components/form-control/field/TimePicker.js.map +2 -2
  89. package/dist/components/form-control/numpad/Numpad.d.ts.map +1 -1
  90. package/dist/components/form-control/numpad/Numpad.js +5 -1
  91. package/dist/components/form-control/numpad/Numpad.js.map +2 -2
  92. package/dist/components/form-control/select/Select.js +7 -7
  93. package/dist/components/form-control/select/Select.js.map +2 -2
  94. package/dist/components/form-control/state-preset/StatePreset.d.ts.map +1 -1
  95. package/dist/components/form-control/state-preset/StatePreset.js +42 -20
  96. package/dist/components/form-control/state-preset/StatePreset.js.map +2 -2
  97. package/dist/components/layout/sidebar/SidebarContainer.js +3 -3
  98. package/dist/components/layout/sidebar/SidebarContainer.js.map +2 -2
  99. package/dist/components/layout/sidebar/SidebarMenu.d.ts.map +1 -1
  100. package/dist/components/layout/sidebar/SidebarMenu.js +5 -2
  101. package/dist/components/layout/sidebar/SidebarMenu.js.map +2 -2
  102. package/dist/components/layout/topbar/Topbar.js +3 -4
  103. package/dist/components/layout/topbar/Topbar.js.map +2 -2
  104. package/dist/components/layout/topbar/TopbarMenu.js +3 -3
  105. package/dist/components/layout/topbar/TopbarMenu.js.map +2 -2
  106. package/dist/hooks/createSelectionGroup.d.ts +2 -2
  107. package/dist/hooks/createSelectionGroup.d.ts.map +1 -1
  108. package/dist/hooks/createSelectionGroup.js +5 -2
  109. package/dist/hooks/createSelectionGroup.js.map +2 -2
  110. package/dist/providers/i18n/I18nContext.d.ts +0 -4
  111. package/dist/providers/i18n/I18nContext.d.ts.map +1 -1
  112. package/dist/providers/i18n/I18nContext.js +1 -5
  113. package/dist/providers/i18n/I18nContext.js.map +2 -2
  114. package/dist/providers/i18n/locales/en.d.ts +38 -0
  115. package/dist/providers/i18n/locales/en.d.ts.map +1 -1
  116. package/dist/providers/i18n/locales/en.js +39 -1
  117. package/dist/providers/i18n/locales/en.js.map +1 -1
  118. package/dist/providers/i18n/locales/ko.d.ts +38 -0
  119. package/dist/providers/i18n/locales/ko.d.ts.map +1 -1
  120. package/dist/providers/i18n/locales/ko.js +39 -1
  121. package/dist/providers/i18n/locales/ko.js.map +1 -1
  122. package/package.json +6 -6
  123. package/src/components/data/calendar/Calendar.tsx +3 -4
  124. package/src/components/data/sheet/DataSheet.styles.ts +1 -1
  125. package/src/components/data/sheet/DataSheet.tsx +14 -15
  126. package/src/components/data/sheet/DataSheetConfigDialog.tsx +12 -10
  127. package/src/components/data/sheet/types.ts +1 -1
  128. package/src/components/disclosure/Dialog.tsx +10 -10
  129. package/src/components/disclosure/dialogZIndex.ts +1 -1
  130. package/src/components/features/crud-detail/CrudDetail.tsx +25 -25
  131. package/src/components/features/crud-sheet/CrudSheet.tsx +53 -53
  132. package/src/components/features/crud-sheet/types.ts +4 -4
  133. package/src/components/features/data-select-button/DataSelectButton.tsx +51 -21
  134. package/src/components/features/permission-table/PermissionTable.tsx +3 -3
  135. package/src/components/features/shared-data/SharedDataSelect.tsx +172 -33
  136. package/src/components/features/shared-data/SharedDataSelectButton.tsx +3 -2
  137. package/src/components/features/shared-data/SharedDataSelectList.tsx +4 -4
  138. package/src/components/feedback/notification/NotificationBanner.tsx +3 -3
  139. package/src/components/feedback/notification/NotificationBell.tsx +6 -4
  140. package/src/components/feedback/notification/NotificationProvider.tsx +3 -1
  141. package/src/components/form-control/ThemeToggle.tsx +10 -6
  142. package/src/components/form-control/checkbox/Checkbox.tsx +4 -1
  143. package/src/components/form-control/checkbox/CheckboxGroup.tsx +1 -1
  144. package/src/components/form-control/checkbox/Radio.tsx +4 -1
  145. package/src/components/form-control/checkbox/RadioGroup.tsx +1 -1
  146. package/src/components/form-control/color-picker/ColorPicker.tsx +4 -1
  147. package/src/components/form-control/combobox/Combobox.tsx +6 -3
  148. package/src/components/form-control/date-range-picker/DateRangePicker.tsx +8 -8
  149. package/src/components/form-control/editor/EditorToolbar.tsx +23 -23
  150. package/src/components/form-control/field/DatePicker.tsx +6 -3
  151. package/src/components/form-control/field/DateTimePicker.tsx +6 -3
  152. package/src/components/form-control/field/NumberInput.tsx +6 -3
  153. package/src/components/form-control/field/TextInput.tsx +7 -4
  154. package/src/components/form-control/field/Textarea.tsx +6 -3
  155. package/src/components/form-control/field/TimePicker.tsx +6 -3
  156. package/src/components/form-control/numpad/Numpad.tsx +3 -1
  157. package/src/components/form-control/select/Select.tsx +7 -7
  158. package/src/components/form-control/state-preset/StatePreset.tsx +14 -12
  159. package/src/components/layout/sidebar/SidebarContainer.tsx +3 -3
  160. package/src/components/layout/sidebar/SidebarMenu.tsx +3 -1
  161. package/src/components/layout/topbar/Topbar.tsx +3 -3
  162. package/src/components/layout/topbar/TopbarMenu.tsx +3 -3
  163. package/src/hooks/createSelectionGroup.tsx +8 -4
  164. package/src/providers/i18n/I18nContext.tsx +0 -7
  165. package/src/providers/i18n/locales/en.ts +38 -0
  166. package/src/providers/i18n/locales/ko.ts +38 -0
  167. package/tailwind.config.ts +2 -2
  168. package/tests/components/data/kanban/Kanban.selection.spec.tsx +34 -24
  169. package/tests/components/disclosure/Dialog.spec.tsx +28 -28
  170. package/tests/components/disclosure/DialogProvider.spec.tsx +51 -25
  171. package/tests/components/features/address/AddressSearch.spec.tsx +12 -4
  172. package/tests/components/features/crud-detail/CrudDetail.spec.tsx +1 -0
  173. package/tests/components/features/crud-sheet/CrudSheet.spec.tsx +30 -6
  174. package/tests/components/features/data-select-button/DataSelectButton.spec.tsx +77 -56
  175. package/tests/components/features/permission-table/PermissionTable.spec.tsx +12 -8
  176. package/tests/components/features/shared-data/SharedDataSelect.spec.tsx +172 -0
  177. package/tests/components/features/shared-data/SharedDataSelectList.spec.tsx +14 -2
  178. package/tests/components/feedback/notification/LiveRegion.spec.tsx +20 -9
  179. package/tests/components/feedback/notification/NotificationBanner.spec.tsx +64 -46
  180. package/tests/components/feedback/notification/NotificationBell.spec.tsx +70 -51
  181. package/tests/components/feedback/notification/NotificationContext.spec.tsx +105 -78
  182. package/tests/components/form-control/checkbox/Checkbox.spec.tsx +25 -20
  183. package/tests/components/form-control/checkbox/CheckboxGroup.spec.tsx +53 -30
  184. package/tests/components/form-control/checkbox/Radio.spec.tsx +25 -20
  185. package/tests/components/form-control/checkbox/RadioGroup.spec.tsx +53 -30
  186. package/tests/components/form-control/color-picker/ColorPicker.spec.tsx +24 -15
  187. package/tests/components/form-control/combobox/Combobox.spec.tsx +92 -59
  188. package/tests/components/form-control/date-range-picker/DateRangePicker.spec.tsx +2 -2
  189. package/tests/components/form-control/field/DatePicker.spec.tsx +50 -44
  190. package/tests/components/form-control/field/DateTimePicker.spec.tsx +51 -45
  191. package/tests/components/form-control/field/NumberInput.spec.tsx +53 -47
  192. package/tests/components/form-control/field/TextInput.spec.tsx +50 -44
  193. package/tests/components/form-control/field/Textarea.spec.tsx +35 -29
  194. package/tests/components/form-control/field/TimePicker.spec.tsx +43 -37
  195. package/tests/components/form-control/numpad/Numpad.spec.tsx +175 -25
  196. package/tests/components/form-control/select/Select.spec.tsx +5 -0
  197. package/tests/components/form-control/select/SelectItem.spec.tsx +1 -0
  198. package/tests/components/layout/sidebar/Sidebar.spec.tsx +79 -35
  199. package/tests/components/layout/sidebar/SidebarContainer.spec.tsx +1 -0
  200. package/tests/components/layout/sidebar/SidebarMenu.spec.tsx +28 -17
  201. package/tests/components/layout/topbar/TopbarActions.spec.tsx +41 -23
  202. package/tests/components/layout/topbar/createTopbarActions.spec.tsx +1 -0
  203. package/tests/hooks/usePrint.spec.tsx +1 -1
  204. package/tests/hooks/useRouterLink.spec.tsx +2 -0
  205. package/tests/hooks/useSyncConfig.spec.tsx +1 -0
  206. package/tests/providers/ErrorLoggerProvider.spec.tsx +1 -0
  207. package/tests/providers/PwaUpdateProvider.spec.tsx +16 -6
  208. package/tests/providers/ServiceClientContext.spec.tsx +40 -25
  209. package/tests/providers/i18n/I18nContext.spec.tsx +3 -4
  210. package/tests/providers/shared-data/SharedDataProvider.spec.tsx +2 -0
  211. package/dist/hooks/usePrint.d.ts +0 -3
  212. package/dist/hooks/usePrint.d.ts.map +0 -1
  213. package/dist/hooks/usePrint.js +0 -5
  214. package/dist/hooks/usePrint.js.map +0 -6
  215. package/src/hooks/usePrint.ts +0 -2
@@ -1,4 +1,5 @@
1
1
  import {
2
+ type Component,
2
3
  createEffect,
3
4
  createMemo,
4
5
  createResource,
@@ -15,7 +16,8 @@ import { IconSearch, IconX } from "@tabler/icons-solidjs";
15
16
  import { Icon } from "../../display/Icon";
16
17
  import { Invalid } from "../../form-control/Invalid";
17
18
  import { useDialog, type DialogShowOptions } from "../../disclosure/DialogContext";
18
- import { useI18nOptional } from "../../../providers/i18n/I18nContext";
19
+ import { useDialogInstance } from "../../disclosure/DialogInstanceContext";
20
+ import { useI18n } from "../../../providers/i18n/I18nContext";
19
21
  import { createControllableSignal } from "../../../hooks/createControllableSignal";
20
22
  import { type ComponentSize, textMuted } from "../../../styles/tokens.styles";
21
23
  import {
@@ -25,11 +27,31 @@ import {
25
27
  triggerSizeClasses,
26
28
  } from "../../form-control/DropdownTrigger.styles";
27
29
 
28
- /** Result interface returned from modal */
29
- export interface DataSelectModalResult<TKey> {
30
+ /** Result interface returned from dialog */
31
+ export interface DataSelectDialogResult<TKey> {
30
32
  selectedKeys: TKey[];
31
33
  }
32
34
 
35
+ /** Props automatically injected into dialog component by DataSelectButton/SharedDataSelect */
36
+ export interface InjectedSelectProps {
37
+ /** Selection mode */
38
+ selectMode: "single" | "multiple";
39
+ /** Currently selected keys */
40
+ selectedKeys: (string | number)[];
41
+ /** Selection callback — automatically closes dialog with result */
42
+ onSelect: (result: { keys: (string | number)[] }) => void;
43
+ }
44
+
45
+ /** Declarative dialog configuration */
46
+ export interface DialogConfig<TUserProps = any> {
47
+ /** Dialog component (must accept InjectedSelectProps) */
48
+ component: Component<TUserProps & InjectedSelectProps>;
49
+ /** User-defined props for the component */
50
+ props?: TUserProps;
51
+ /** Dialog options (header, size, etc.) */
52
+ option?: DialogShowOptions;
53
+ }
54
+
33
55
  /** DataSelectButton Props */
34
56
  export interface DataSelectButtonProps<TItem, TKey = string | number> {
35
57
  /** Currently selected key(s) (single or multiple) */
@@ -39,8 +61,8 @@ export interface DataSelectButtonProps<TItem, TKey = string | number> {
39
61
 
40
62
  /** Function to load items by key */
41
63
  load: (keys: TKey[]) => TItem[] | Promise<TItem[]>;
42
- /** Selection modal component factory */
43
- modal: () => JSX.Element;
64
+ /** Selection dialog configuration */
65
+ dialog: DialogConfig;
44
66
  /** Item rendering function */
45
67
  renderItem: (item: TItem) => JSX.Element;
46
68
 
@@ -59,9 +81,6 @@ export interface DataSelectButtonProps<TItem, TKey = string | number> {
59
81
  validate?: (value: unknown) => string | undefined;
60
82
  /** touchMode: show error only after focus is lost */
61
83
  touchMode?: boolean;
62
-
63
- /** Dialog options */
64
- dialogOptions?: DialogShowOptions;
65
84
  }
66
85
 
67
86
  // Styles
@@ -100,7 +119,7 @@ export function DataSelectButton<TItem, TKey = string | number>(
100
119
  "value",
101
120
  "onValueChange",
102
121
  "load",
103
- "modal",
122
+ "dialog",
104
123
  "renderItem",
105
124
  "multiple",
106
125
  "required",
@@ -109,10 +128,9 @@ export function DataSelectButton<TItem, TKey = string | number>(
109
128
  "inset",
110
129
  "validate",
111
130
  "touchMode",
112
- "dialogOptions",
113
131
  ]);
114
132
 
115
- const i18n = useI18nOptional();
133
+ const i18n = useI18n();
116
134
  const dialog = useDialog();
117
135
 
118
136
  // Always normalize value to array
@@ -164,18 +182,30 @@ export function DataSelectButton<TItem, TKey = string | number>(
164
182
  const v = getValue();
165
183
  if (local.required) {
166
184
  const keys = normalizeKeys(v);
167
- if (keys.length === 0) return "Required field";
185
+ if (keys.length === 0) return i18n.t("validation.requiredField");
168
186
  }
169
187
  return local.validate?.(v);
170
188
  });
171
189
 
172
- // Open modal
173
- const handleOpenModal = async () => {
190
+ // Open dialog
191
+ const handleOpenDialog = async () => {
174
192
  if (local.disabled) return;
175
193
 
176
- const result = await dialog.show<DataSelectModalResult<TKey>>(
177
- local.modal,
178
- local.dialogOptions ?? {},
194
+ const result = await dialog.show<DataSelectDialogResult<TKey>>(
195
+ () => {
196
+ const instance = useDialogInstance<DataSelectDialogResult<TKey>>();
197
+ return (
198
+ <local.dialog.component
199
+ {...(local.dialog.props ?? {})}
200
+ selectMode={local.multiple ? "multiple" : "single"}
201
+ selectedKeys={normalizeKeys(getValue()) as (string | number)[]}
202
+ onSelect={(r: { keys: (string | number)[] }) =>
203
+ instance?.close({ selectedKeys: r.keys as TKey[] })
204
+ }
205
+ />
206
+ );
207
+ },
208
+ local.dialog.option ?? {},
179
209
  );
180
210
 
181
211
  if (result) {
@@ -243,7 +273,7 @@ export function DataSelectButton<TItem, TKey = string | number>(
243
273
  if (local.disabled) return;
244
274
  if (e.key === "Enter" || e.key === " ") {
245
275
  e.preventDefault();
246
- void handleOpenModal();
276
+ void handleOpenDialog();
247
277
  }
248
278
  }}
249
279
  >
@@ -256,7 +286,7 @@ export function DataSelectButton<TItem, TKey = string | number>(
256
286
  class={twMerge(actionButtonClass, "text-base-400 hover:text-danger-500")}
257
287
  onClick={handleClear}
258
288
  tabIndex={-1}
259
- aria-label={i18n?.t("dataSelectButton.deselect") ?? "Deselect"}
289
+ aria-label={i18n.t("dataSelectButton.deselect")}
260
290
  >
261
291
  <Icon icon={IconX} size="0.875em" />
262
292
  </button>
@@ -266,9 +296,9 @@ export function DataSelectButton<TItem, TKey = string | number>(
266
296
  type="button"
267
297
  data-search-button
268
298
  class={twMerge(actionButtonClass, "text-base-400 hover:text-primary-500")}
269
- onClick={() => void handleOpenModal()}
299
+ onClick={() => void handleOpenDialog()}
270
300
  tabIndex={-1}
271
- aria-label={i18n?.t("dataSelectButton.search") ?? "Search"}
301
+ aria-label={i18n.t("dataSelectButton.search")}
272
302
  >
273
303
  <Icon icon={IconSearch} size="0.875em" />
274
304
  </button>
@@ -14,7 +14,7 @@ import { DataSheet } from "../../data/sheet/DataSheet";
14
14
  import { Checkbox } from "../../form-control/checkbox/Checkbox";
15
15
  import { borderDefault } from "../../../styles/tokens.styles";
16
16
  import type { AppPerm } from "../../../helpers/createAppStructure";
17
- import { useI18nOptional } from "../../../providers/i18n/I18nContext";
17
+ import { useI18n } from "../../../providers/i18n/I18nContext";
18
18
 
19
19
  const titleCellClass = clsx("flex items-stretch", "px-2");
20
20
  const indentGuideWrapperClass = clsx("mr-1 flex w-3", "justify-center");
@@ -215,7 +215,7 @@ export const PermissionTable: Component<PermissionTableProps> = (props) => {
215
215
  "style",
216
216
  ]);
217
217
 
218
- const i18n = useI18nOptional();
218
+ const i18n = useI18n();
219
219
 
220
220
  // Visible top-level items (preserve object reference)
221
221
  const visibleItems = createMemo(() => {
@@ -270,7 +270,7 @@ export const PermissionTable: Component<PermissionTableProps> = (props) => {
270
270
  >
271
271
  <DataSheet.Column
272
272
  key="title"
273
- header={i18n?.t("permissionTable.permissionItem") ?? "Permission Item"}
273
+ header={i18n.t("permissionTable.permissionItem")}
274
274
  sortable={false}
275
275
  resizable={false}
276
276
  >
@@ -1,20 +1,79 @@
1
- import { createMemo, type JSX, mergeProps, splitProps } from "solid-js";
2
- import { IconEdit, IconSearch } from "@tabler/icons-solidjs";
1
+ import {
2
+ children as resolveChildren,
3
+ type Component,
4
+ createMemo,
5
+ For,
6
+ type JSX,
7
+ mergeProps,
8
+ splitProps,
9
+ } from "solid-js";
10
+ import { IconSearch } from "@tabler/icons-solidjs";
3
11
  import { type SharedDataAccessor } from "../../../providers/shared-data/SharedDataContext";
4
12
  import { Select, type SelectProps } from "../../form-control/select/Select";
5
13
  import { Icon } from "../../display/Icon";
6
14
  import { useDialog } from "../../disclosure/DialogContext";
7
- import { useI18nOptional } from "../../../providers/i18n/I18nContext";
15
+ import { useDialogInstance } from "../../disclosure/DialogInstanceContext";
16
+ import { useI18n } from "../../../providers/i18n/I18nContext";
8
17
  import { type ComponentSize } from "../../../styles/tokens.styles";
18
+ import {
19
+ type DataSelectDialogResult,
20
+ type DialogConfig,
21
+ } from "../data-select-button/DataSelectButton";
22
+
23
+ // -- Slot detection --
24
+ const ITEM_TEMPLATE_BRAND = Symbol("SharedDataSelect.ItemTemplate");
25
+ const ACTION_BRAND = Symbol("SharedDataSelect.Action");
26
+
27
+ interface ItemTemplateDef {
28
+ __brand: typeof ITEM_TEMPLATE_BRAND;
29
+ children: (item: any, index: number, depth: number) => JSX.Element;
30
+ }
31
+
32
+ interface ActionDef {
33
+ __brand: typeof ACTION_BRAND;
34
+ children: JSX.Element;
35
+ onClick?: (e: MouseEvent) => void;
36
+ }
37
+
38
+ function isItemTemplateDef(v: unknown): v is ItemTemplateDef {
39
+ return v != null && typeof v === "object" && "__brand" in v && (v as any).__brand === ITEM_TEMPLATE_BRAND;
40
+ }
41
+
42
+ function isActionDef(v: unknown): v is ActionDef {
43
+ return v != null && typeof v === "object" && "__brand" in v && (v as any).__brand === ACTION_BRAND;
44
+ }
45
+
46
+ // -- Compound components --
47
+ const ItemTemplate: Component<{
48
+ children: (item: any, index: number, depth: number) => JSX.Element;
49
+ }> = (props) => {
50
+ // eslint-disable-next-line solid/reactivity -- factory function, not reactive JSX
51
+ return (() => ({
52
+ __brand: ITEM_TEMPLATE_BRAND,
53
+ children: props.children,
54
+ })) as unknown as JSX.Element;
55
+ };
56
+
57
+ const Action: Component<{
58
+ children?: JSX.Element;
59
+ onClick?: (e: MouseEvent) => void;
60
+ }> = (props) => {
61
+ // eslint-disable-next-line solid/reactivity -- factory function, not reactive JSX
62
+ return (() => ({
63
+ __brand: ACTION_BRAND,
64
+ children: props.children,
65
+ onClick: props.onClick,
66
+ })) as unknown as JSX.Element;
67
+ };
9
68
 
10
69
  /** SharedDataSelect Props */
11
70
  export interface SharedDataSelectProps<TItem> {
12
71
  /** Shared data accessor */
13
72
  data: SharedDataAccessor<TItem>;
14
73
 
15
- /** Currently selected value */
74
+ /** Currently selected key value (translated to item internally) */
16
75
  value?: unknown;
17
- /** Value change callback */
76
+ /** Value change callback (receives key, not item) */
18
77
  onValueChange?: (value: unknown) => void;
19
78
  /** Multiple selection mode */
20
79
  multiple?: boolean;
@@ -29,21 +88,37 @@ export interface SharedDataSelectProps<TItem> {
29
88
 
30
89
  /** Item filter function */
31
90
  filterFn?: (item: TItem, index: number) => boolean;
32
- /** Selection modal component factory */
33
- modal?: () => JSX.Element;
34
- /** Edit modal component factory */
35
- editModal?: () => JSX.Element;
91
+ /** Selection dialog configuration */
92
+ dialog?: DialogConfig;
93
+
94
+ /** Compound children: ItemTemplate, Action */
95
+ children: JSX.Element;
96
+ }
36
97
 
37
- /** Item rendering function */
38
- children: (item: TItem, index: number, depth: number) => JSX.Element;
98
+ interface SharedDataSelectComponent {
99
+ <TItem>(props: SharedDataSelectProps<TItem>): JSX.Element;
100
+ ItemTemplate: typeof ItemTemplate;
101
+ Action: typeof Action;
39
102
  }
40
103
 
41
- export function SharedDataSelect<TItem>(props: SharedDataSelectProps<TItem>): JSX.Element {
42
- const [local, rest] = splitProps(props, ["data", "filterFn", "modal", "editModal", "children"]);
104
+ const SharedDataSelectBase = <TItem,>(props: SharedDataSelectProps<TItem>): JSX.Element => {
105
+ const [local, rest] = splitProps(props, [
106
+ "data", "filterFn", "dialog", "children",
107
+ ]);
43
108
 
44
- const i18n = useI18nOptional();
109
+ const i18n = useI18n();
45
110
  const dialog = useDialog();
46
111
 
112
+ // Resolve compound children
113
+ const resolved = resolveChildren(() => local.children);
114
+ const defs = createMemo(() => {
115
+ const arr = resolved.toArray();
116
+ return {
117
+ itemTemplate: arr.find(isItemTemplateDef) as unknown as ItemTemplateDef | undefined,
118
+ actions: arr.filter(isActionDef) as unknown as ActionDef[],
119
+ };
120
+ });
121
+
47
122
  // Items with filterFn applied
48
123
  const items = createMemo(() => {
49
124
  const allItems = local.data.items();
@@ -51,21 +126,77 @@ export function SharedDataSelect<TItem>(props: SharedDataSelectProps<TItem>): JS
51
126
  return allItems.filter(local.filterFn);
52
127
  });
53
128
 
54
- // Open modal
55
- const handleOpenModal = async () => {
56
- if (!local.modal) return;
57
- await dialog.show(local.modal, {});
129
+ // Normalize value to keys array
130
+ const normalizeKeys = (value: unknown): (string | number)[] => {
131
+ if (value === undefined || value === null) return [];
132
+ if (Array.isArray(value)) return value;
133
+ return [value as string | number];
58
134
  };
59
135
 
60
- // Open edit modal
61
- const handleOpenEditModal = async () => {
62
- if (!local.editModal) return;
63
- await dialog.show(local.editModal, {});
136
+ // Translate key(s) to item(s) for Select's value prop
137
+ const keyToItem = (key: string | number): TItem | undefined => {
138
+ return local.data.get(key);
139
+ };
140
+
141
+ const valueAsItem = createMemo((): TItem | TItem[] | undefined => {
142
+ const key = rest.value;
143
+ if (key === undefined || key === null) return undefined;
144
+ if (Array.isArray(key)) {
145
+ return key.map((k) => keyToItem(k as string | number)).filter((v): v is TItem => v !== undefined);
146
+ }
147
+ return keyToItem(key as string | number);
148
+ });
149
+
150
+ // Translate item back to key for onValueChange callback
151
+ const itemToKey = (item: TItem | TItem[] | undefined): unknown => {
152
+ if (item === undefined || item === null) return undefined;
153
+ if (Array.isArray(item)) return item.map((i) => local.data.getKey(i));
154
+ return local.data.getKey(item);
155
+ };
156
+
157
+ // Open dialog and handle selection result
158
+ const handleOpenDialog = async () => {
159
+ if (!local.dialog) return;
160
+
161
+ const dialogConfig = local.dialog;
162
+ const result = await dialog.show<DataSelectDialogResult<string | number>>(
163
+ () => {
164
+ const instance = useDialogInstance<DataSelectDialogResult<string | number>>();
165
+ return (
166
+ <dialogConfig.component
167
+ {...(dialogConfig.props ?? {})}
168
+ selectMode={rest.multiple ? "multiple" : "single"}
169
+ selectedKeys={normalizeKeys(rest.value)}
170
+ onSelect={(r: { keys: (string | number)[] }) =>
171
+ instance?.close({ selectedKeys: r.keys })
172
+ }
173
+ />
174
+ );
175
+ },
176
+ dialogConfig.option ?? {},
177
+ );
178
+
179
+ if (result) {
180
+ const newKeys = result.selectedKeys;
181
+ if (rest.multiple) {
182
+ rest.onValueChange?.(newKeys);
183
+ } else {
184
+ rest.onValueChange?.(newKeys.length > 0 ? newKeys[0] : undefined);
185
+ }
186
+ }
64
187
  };
65
188
 
66
- // Use mergeProps + as for Select's discriminated union (multiple: true | false?) and TItem → unknown conversion
67
- // Wrap with getter to satisfy SolidJS reactivity lint rules
68
189
  const selectProps = mergeProps(rest, {
190
+ get value() {
191
+ return valueAsItem();
192
+ },
193
+ get onValueChange() {
194
+ if (!rest.onValueChange) return undefined;
195
+ // eslint-disable-next-line solid/reactivity -- inside getter, tracked scope
196
+ return (item: TItem | TItem[] | undefined) => {
197
+ rest.onValueChange!(itemToKey(item));
198
+ };
199
+ },
69
200
  get items() {
70
201
  return items();
71
202
  },
@@ -87,17 +218,25 @@ export function SharedDataSelect<TItem>(props: SharedDataSelectProps<TItem>): JS
87
218
 
88
219
  return (
89
220
  <Select {...selectProps}>
90
- <Select.ItemTemplate>{local.children}</Select.ItemTemplate>
91
- {local.modal && (
92
- <Select.Action onClick={() => void handleOpenModal()} aria-label={i18n?.t("sharedDataSelect.search") ?? "Search"}>
93
- <Icon icon={IconSearch} />
94
- </Select.Action>
221
+ {defs().itemTemplate && (
222
+ <Select.ItemTemplate>{defs().itemTemplate!.children}</Select.ItemTemplate>
95
223
  )}
96
- {local.editModal && (
97
- <Select.Action onClick={() => void handleOpenEditModal()} aria-label={i18n?.t("sharedDataSelect.edit") ?? "Edit"}>
98
- <Icon icon={IconEdit} />
224
+ {local.dialog && (
225
+ <Select.Action onClick={() => void handleOpenDialog()} aria-label={i18n.t("sharedDataSelect.search")}>
226
+ <Icon icon={IconSearch} />
99
227
  </Select.Action>
100
228
  )}
229
+ <For each={defs().actions}>
230
+ {(action) => (
231
+ <Select.Action onClick={action.onClick}>
232
+ {action.children}
233
+ </Select.Action>
234
+ )}
235
+ </For>
101
236
  </Select>
102
237
  );
103
- }
238
+ };
239
+
240
+ export const SharedDataSelect: SharedDataSelectComponent = SharedDataSelectBase as any;
241
+ SharedDataSelect.ItemTemplate = ItemTemplate;
242
+ SharedDataSelect.Action = Action;
@@ -3,6 +3,7 @@ import { type SharedDataAccessor } from "../../../providers/shared-data/SharedDa
3
3
  import {
4
4
  DataSelectButton,
5
5
  type DataSelectButtonProps,
6
+ type DialogConfig,
6
7
  } from "../data-select-button/DataSelectButton";
7
8
  import { type ComponentSize } from "../../../styles/tokens.styles";
8
9
 
@@ -26,8 +27,8 @@ export interface SharedDataSelectButtonProps<TItem> {
26
27
  /** Borderless style */
27
28
  inset?: boolean;
28
29
 
29
- /** Selection modal component factory */
30
- modal: () => JSX.Element;
30
+ /** Selection dialog configuration */
31
+ dialog: DialogConfig;
31
32
  /** Item rendering function */
32
33
  children: (item: TItem) => JSX.Element;
33
34
  }
@@ -5,7 +5,7 @@ import { type SharedDataAccessor } from "../../../providers/shared-data/SharedDa
5
5
  import { List } from "../../data/list/List";
6
6
  import { Pagination } from "../../data/Pagination";
7
7
  import { TextInput } from "../../form-control/field/TextInput";
8
- import { useI18nOptional } from "../../../providers/i18n/I18nContext";
8
+ import { useI18n } from "../../../providers/i18n/I18nContext";
9
9
  import { textMuted } from "../../../styles/tokens.styles";
10
10
  import { createSlotSignal } from "../../../hooks/createSlotSignal";
11
11
  import {
@@ -77,7 +77,7 @@ export const SharedDataSelectList: SharedDataSelectListComponent = (<TItem,>(
77
77
  "header",
78
78
  ]);
79
79
 
80
- const i18n = useI18nOptional();
80
+ const i18n = useI18n();
81
81
 
82
82
  // ─── Slot signals ──────────────────────────────────────
83
83
 
@@ -203,7 +203,7 @@ export const SharedDataSelectList: SharedDataSelectListComponent = (<TItem,>(
203
203
  <TextInput
204
204
  value={searchText()}
205
205
  onValueChange={setSearchText}
206
- placeholder={i18n?.t("sharedDataSelectList.searchPlaceholder") ?? "Search..."}
206
+ placeholder={i18n.t("sharedDataSelectList.searchPlaceholder")}
207
207
  class={"w-full"}
208
208
  />
209
209
  </div>
@@ -231,7 +231,7 @@ export const SharedDataSelectList: SharedDataSelectListComponent = (<TItem,>(
231
231
  disabled={local.disabled}
232
232
  onClick={() => handleSelect(undefined)}
233
233
  >
234
- <span class={textMuted}>Unspecified</span>
234
+ <span class={textMuted}>{i18n.t("sharedDataSelectList.unspecified")}</span>
235
235
  </List.Item>
236
236
  </Show>
237
237
 
@@ -3,7 +3,7 @@ import { Portal } from "solid-js/web";
3
3
  import clsx from "clsx";
4
4
  import { IconX } from "@tabler/icons-solidjs";
5
5
  import { useNotification } from "./NotificationContext";
6
- import { useI18nOptional } from "../../../providers/i18n/I18nContext";
6
+ import { useI18n } from "../../../providers/i18n/I18nContext";
7
7
  import { Icon } from "../../display/Icon";
8
8
  import { themeTokens } from "../../../styles/tokens.styles";
9
9
 
@@ -39,7 +39,7 @@ const dismissButtonClass = clsx("rounded", "p-1", "hover:bg-white/20");
39
39
 
40
40
  export const NotificationBanner: Component = () => {
41
41
  const notification = useNotification();
42
- const i18n = useI18nOptional();
42
+ const i18n = useI18n();
43
43
 
44
44
  const handleDismiss = () => {
45
45
  notification.dismissBanner();
@@ -74,7 +74,7 @@ export const NotificationBanner: Component = () => {
74
74
  </Show>
75
75
  <button
76
76
  type="button"
77
- aria-label={i18n?.t("notification.close") ?? "Close notification"}
77
+ aria-label={i18n.t("notification.close")}
78
78
  class={dismissButtonClass}
79
79
  onClick={handleDismiss}
80
80
  >
@@ -7,6 +7,7 @@ import { Dropdown } from "../../disclosure/Dropdown";
7
7
  import { Icon } from "../../display/Icon";
8
8
  import { NotificationBanner } from "./NotificationBanner";
9
9
  import { iconButtonBase } from "../../../styles/patterns.styles";
10
+ import { useI18n } from "../../../providers/i18n/I18nContext";
10
11
 
11
12
  export interface NotificationBellProps {
12
13
  showBanner?: boolean;
@@ -52,6 +53,7 @@ const itemTimeClass = clsx("mt-1 text-xs", "text-base-400");
52
53
 
53
54
  export const NotificationBell: Component<NotificationBellProps> = (props) => {
54
55
  const notification = useNotification();
56
+ const i18n = useI18n();
55
57
  const [open, setOpen] = createSignal(false);
56
58
 
57
59
  const handleClear = () => {
@@ -78,7 +80,7 @@ export const NotificationBell: Component<NotificationBellProps> = (props) => {
78
80
  type="button"
79
81
  data-notification-bell
80
82
  class={buttonClass}
81
- aria-label={`${notification.unreadCount()} notifications`}
83
+ aria-label={i18n.t("notificationBell.unreadCount", { count: String(notification.unreadCount()) })}
82
84
  aria-haspopup="true"
83
85
  aria-expanded={open()}
84
86
  >
@@ -93,7 +95,7 @@ export const NotificationBell: Component<NotificationBellProps> = (props) => {
93
95
  <Dropdown.Content>
94
96
  <div class="w-80 p-2">
95
97
  <div class={dropdownHeaderClass}>
96
- <span class="font-bold">Notifications</span>
98
+ <span class="font-bold">{i18n.t("notificationBell.notifications")}</span>
97
99
  <Show when={notification.items().length > 0}>
98
100
  <button
99
101
  type="button"
@@ -101,14 +103,14 @@ export const NotificationBell: Component<NotificationBellProps> = (props) => {
101
103
  class={clearButtonClass}
102
104
  onClick={handleClear}
103
105
  >
104
- Clear All
106
+ {i18n.t("notificationBell.clearAll")}
105
107
  </button>
106
108
  </Show>
107
109
  </div>
108
110
 
109
111
  <Show
110
112
  when={notification.items().length > 0}
111
- fallback={<div class={emptyClass}>No notifications</div>}
113
+ fallback={<div class={emptyClass}>{i18n.t("notificationBell.noNotifications")}</div>}
112
114
  >
113
115
  <div class={listClass}>
114
116
  <For each={[...notification.items()].reverse()}>
@@ -8,6 +8,7 @@ import {
8
8
  type NotificationUpdateOptions,
9
9
  } from "./NotificationContext";
10
10
  import { useLogger } from "../../../hooks/useLogger";
11
+ import { useI18n } from "../../../providers/i18n/I18nContext";
11
12
 
12
13
  const MAX_ITEMS = 50;
13
14
 
@@ -22,6 +23,7 @@ const MAX_ITEMS = 50;
22
23
  */
23
24
  export const NotificationProvider: ParentComponent = (props) => {
24
25
  const logger = useLogger();
26
+ const i18n = useI18n();
25
27
  const [items, setItems] = createSignal<NotificationItem[]>([]);
26
28
  const [dismissedBannerId, setDismissedBannerId] = createSignal<string | null>(null);
27
29
 
@@ -158,7 +160,7 @@ export const NotificationProvider: ParentComponent = (props) => {
158
160
  {/* Screen reader Live Region */}
159
161
  <div role="status" aria-live="polite" aria-atomic="true" class="sr-only">
160
162
  <Show when={latestUnread()}>
161
- {(item) => `Notification: ${item().title} ${item().message ?? ""}`}
163
+ {(item) => `${i18n.t("notificationProvider.prefix")} ${item().title} ${item().message ?? ""}`}
162
164
  </Show>
163
165
  </div>
164
166
  {props.children}
@@ -5,6 +5,7 @@ import { useTheme, type ThemeMode } from "../../providers/ThemeContext";
5
5
  import { Icon } from "../display/Icon";
6
6
  import { ripple } from "../../directives/ripple";
7
7
  import { iconButtonBase } from "../../styles/patterns.styles";
8
+ import { useI18n } from "../../providers/i18n/I18nContext";
8
9
 
9
10
  void ripple;
10
11
 
@@ -18,10 +19,10 @@ const iconSizes: Record<"sm" | "lg", string> = {
18
19
  lg: "1.5em",
19
20
  };
20
21
 
21
- const modeLabels: Record<ThemeMode, string> = {
22
- light: "Light mode",
23
- system: "System settings",
24
- dark: "Dark mode",
22
+ const modeLabelKeys: Record<ThemeMode, string> = {
23
+ light: "themeToggle.light",
24
+ system: "themeToggle.system",
25
+ dark: "themeToggle.dark",
25
26
  };
26
27
 
27
28
  export interface ThemeToggleProps extends Omit<
@@ -54,6 +55,9 @@ export const ThemeToggle: Component<ThemeToggleProps> = (props) => {
54
55
  const [local, rest] = splitProps(props, ["class", "size"]);
55
56
 
56
57
  const { mode, cycleMode } = useTheme();
58
+ const i18n = useI18n();
59
+
60
+ const modeLabel = () => i18n.t(modeLabelKeys[mode()]);
57
61
 
58
62
  const getClassName = () =>
59
63
  twMerge(iconButtonBase, "p-1.5", local.size && sizeClasses[local.size], local.class);
@@ -68,8 +72,8 @@ export const ThemeToggle: Component<ThemeToggleProps> = (props) => {
68
72
  type="button"
69
73
  class={getClassName()}
70
74
  onClick={cycleMode}
71
- title={modeLabels[mode()]}
72
- aria-label={modeLabels[mode()]}
75
+ title={modeLabel()}
76
+ aria-label={modeLabel()}
73
77
  >
74
78
  <Switch>
75
79
  <Match when={mode() === "light"}>
@@ -4,6 +4,7 @@ import { IconCheck } from "@tabler/icons-solidjs";
4
4
  import { createControllableSignal } from "../../../hooks/createControllableSignal";
5
5
  import { ripple } from "../../../directives/ripple";
6
6
  import { Icon } from "../../display/Icon";
7
+ import { useI18n } from "../../../providers/i18n/I18nContext";
7
8
  import {
8
9
  type CheckboxSize,
9
10
  checkboxBaseClass,
@@ -51,6 +52,8 @@ export const Checkbox: ParentComponent<CheckboxProps> = (props) => {
51
52
  "children",
52
53
  ]);
53
54
 
55
+ const i18n = useI18n();
56
+
54
57
  const [value, setValue] = createControllableSignal({
55
58
  value: () => local.value ?? false,
56
59
  onChange: () => local.onValueChange,
@@ -84,7 +87,7 @@ export const Checkbox: ParentComponent<CheckboxProps> = (props) => {
84
87
 
85
88
  const errorMsg = createMemo(() => {
86
89
  const v = local.value ?? false;
87
- if (local.required && !v) return "This is a required selection";
90
+ if (local.required && !v) return i18n.t("validation.requiredSelection");
88
91
  return local.validate?.(v);
89
92
  });
90
93