@simplysm/solid 13.0.72 → 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 (210) 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.js +10 -10
  6. package/dist/components/data/sheet/DataSheet.js.map +2 -2
  7. package/dist/components/data/sheet/DataSheetConfigDialog.d.ts.map +1 -1
  8. package/dist/components/data/sheet/DataSheetConfigDialog.js +27 -9
  9. package/dist/components/data/sheet/DataSheetConfigDialog.js.map +2 -2
  10. package/dist/components/disclosure/Dialog.d.ts +1 -1
  11. package/dist/components/disclosure/Dialog.d.ts.map +1 -1
  12. package/dist/components/disclosure/Dialog.js +5 -5
  13. package/dist/components/disclosure/Dialog.js.map +2 -2
  14. package/dist/components/disclosure/dialogZIndex.d.ts +1 -1
  15. package/dist/components/features/crud-detail/CrudDetail.js +23 -23
  16. package/dist/components/features/crud-detail/CrudDetail.js.map +2 -2
  17. package/dist/components/features/crud-sheet/CrudSheet.js +49 -49
  18. package/dist/components/features/crud-sheet/CrudSheet.js.map +2 -2
  19. package/dist/components/features/crud-sheet/types.d.ts +4 -4
  20. package/dist/components/features/crud-sheet/types.d.ts.map +1 -1
  21. package/dist/components/features/data-select-button/DataSelectButton.d.ts +25 -7
  22. package/dist/components/features/data-select-button/DataSelectButton.d.ts.map +1 -1
  23. package/dist/components/features/data-select-button/DataSelectButton.js +27 -12
  24. package/dist/components/features/data-select-button/DataSelectButton.js.map +2 -2
  25. package/dist/components/features/permission-table/PermissionTable.js +4 -4
  26. package/dist/components/features/permission-table/PermissionTable.js.map +2 -2
  27. package/dist/components/features/shared-data/SharedDataSelect.d.ts +22 -10
  28. package/dist/components/features/shared-data/SharedDataSelect.d.ts.map +1 -1
  29. package/dist/components/features/shared-data/SharedDataSelect.js +113 -29
  30. package/dist/components/features/shared-data/SharedDataSelect.js.map +2 -2
  31. package/dist/components/features/shared-data/SharedDataSelectButton.d.ts +3 -3
  32. package/dist/components/features/shared-data/SharedDataSelectButton.d.ts.map +1 -1
  33. package/dist/components/features/shared-data/SharedDataSelectButton.js.map +1 -1
  34. package/dist/components/features/shared-data/SharedDataSelectList.js +5 -4
  35. package/dist/components/features/shared-data/SharedDataSelectList.js.map +2 -2
  36. package/dist/components/feedback/notification/NotificationBanner.js +3 -3
  37. package/dist/components/feedback/notification/NotificationBanner.js.map +2 -2
  38. package/dist/components/feedback/notification/NotificationBell.d.ts.map +1 -1
  39. package/dist/components/feedback/notification/NotificationBell.js +12 -5
  40. package/dist/components/feedback/notification/NotificationBell.js.map +2 -2
  41. package/dist/components/feedback/notification/NotificationProvider.d.ts.map +1 -1
  42. package/dist/components/feedback/notification/NotificationProvider.js +3 -1
  43. package/dist/components/feedback/notification/NotificationProvider.js.map +2 -2
  44. package/dist/components/form-control/ThemeToggle.d.ts.map +1 -1
  45. package/dist/components/form-control/ThemeToggle.js +9 -6
  46. package/dist/components/form-control/ThemeToggle.js.map +2 -2
  47. package/dist/components/form-control/checkbox/Checkbox.d.ts.map +1 -1
  48. package/dist/components/form-control/checkbox/Checkbox.js +3 -1
  49. package/dist/components/form-control/checkbox/Checkbox.js.map +2 -2
  50. package/dist/components/form-control/checkbox/CheckboxGroup.js +1 -1
  51. package/dist/components/form-control/checkbox/CheckboxGroup.js.map +2 -2
  52. package/dist/components/form-control/checkbox/Radio.d.ts.map +1 -1
  53. package/dist/components/form-control/checkbox/Radio.js +3 -1
  54. package/dist/components/form-control/checkbox/Radio.js.map +2 -2
  55. package/dist/components/form-control/checkbox/RadioGroup.js +1 -1
  56. package/dist/components/form-control/checkbox/RadioGroup.js.map +2 -2
  57. package/dist/components/form-control/color-picker/ColorPicker.d.ts.map +1 -1
  58. package/dist/components/form-control/color-picker/ColorPicker.js +3 -1
  59. package/dist/components/form-control/color-picker/ColorPicker.js.map +2 -2
  60. package/dist/components/form-control/combobox/Combobox.d.ts.map +1 -1
  61. package/dist/components/form-control/combobox/Combobox.js +9 -5
  62. package/dist/components/form-control/combobox/Combobox.js.map +2 -2
  63. package/dist/components/form-control/date-range-picker/DateRangePicker.js +9 -9
  64. package/dist/components/form-control/date-range-picker/DateRangePicker.js.map +2 -2
  65. package/dist/components/form-control/editor/EditorToolbar.js +3 -3
  66. package/dist/components/form-control/editor/EditorToolbar.js.map +2 -2
  67. package/dist/components/form-control/field/DatePicker.d.ts.map +1 -1
  68. package/dist/components/form-control/field/DatePicker.js +9 -3
  69. package/dist/components/form-control/field/DatePicker.js.map +2 -2
  70. package/dist/components/form-control/field/DateTimePicker.d.ts.map +1 -1
  71. package/dist/components/form-control/field/DateTimePicker.js +9 -3
  72. package/dist/components/form-control/field/DateTimePicker.js.map +2 -2
  73. package/dist/components/form-control/field/NumberInput.d.ts.map +1 -1
  74. package/dist/components/form-control/field/NumberInput.js +9 -3
  75. package/dist/components/form-control/field/NumberInput.js.map +2 -2
  76. package/dist/components/form-control/field/TextInput.d.ts.map +1 -1
  77. package/dist/components/form-control/field/TextInput.js +10 -4
  78. package/dist/components/form-control/field/TextInput.js.map +2 -2
  79. package/dist/components/form-control/field/Textarea.d.ts.map +1 -1
  80. package/dist/components/form-control/field/Textarea.js +9 -3
  81. package/dist/components/form-control/field/Textarea.js.map +2 -2
  82. package/dist/components/form-control/field/TimePicker.d.ts.map +1 -1
  83. package/dist/components/form-control/field/TimePicker.js +9 -3
  84. package/dist/components/form-control/field/TimePicker.js.map +2 -2
  85. package/dist/components/form-control/numpad/Numpad.d.ts.map +1 -1
  86. package/dist/components/form-control/numpad/Numpad.js +5 -1
  87. package/dist/components/form-control/numpad/Numpad.js.map +2 -2
  88. package/dist/components/form-control/select/Select.js +7 -7
  89. package/dist/components/form-control/select/Select.js.map +2 -2
  90. package/dist/components/form-control/state-preset/StatePreset.d.ts.map +1 -1
  91. package/dist/components/form-control/state-preset/StatePreset.js +42 -20
  92. package/dist/components/form-control/state-preset/StatePreset.js.map +2 -2
  93. package/dist/components/layout/sidebar/SidebarContainer.js +3 -3
  94. package/dist/components/layout/sidebar/SidebarContainer.js.map +2 -2
  95. package/dist/components/layout/sidebar/SidebarMenu.d.ts.map +1 -1
  96. package/dist/components/layout/sidebar/SidebarMenu.js +5 -2
  97. package/dist/components/layout/sidebar/SidebarMenu.js.map +2 -2
  98. package/dist/components/layout/topbar/Topbar.js +3 -4
  99. package/dist/components/layout/topbar/Topbar.js.map +2 -2
  100. package/dist/components/layout/topbar/TopbarMenu.js +3 -3
  101. package/dist/components/layout/topbar/TopbarMenu.js.map +2 -2
  102. package/dist/hooks/createSelectionGroup.d.ts +2 -2
  103. package/dist/hooks/createSelectionGroup.d.ts.map +1 -1
  104. package/dist/hooks/createSelectionGroup.js +5 -2
  105. package/dist/hooks/createSelectionGroup.js.map +2 -2
  106. package/dist/providers/i18n/I18nContext.d.ts +0 -4
  107. package/dist/providers/i18n/I18nContext.d.ts.map +1 -1
  108. package/dist/providers/i18n/I18nContext.js +1 -5
  109. package/dist/providers/i18n/I18nContext.js.map +2 -2
  110. package/dist/providers/i18n/locales/en.d.ts +38 -0
  111. package/dist/providers/i18n/locales/en.d.ts.map +1 -1
  112. package/dist/providers/i18n/locales/en.js +39 -1
  113. package/dist/providers/i18n/locales/en.js.map +1 -1
  114. package/dist/providers/i18n/locales/ko.d.ts +38 -0
  115. package/dist/providers/i18n/locales/ko.d.ts.map +1 -1
  116. package/dist/providers/i18n/locales/ko.js +39 -1
  117. package/dist/providers/i18n/locales/ko.js.map +1 -1
  118. package/package.json +6 -6
  119. package/src/components/data/calendar/Calendar.tsx +3 -4
  120. package/src/components/data/sheet/DataSheet.tsx +11 -11
  121. package/src/components/data/sheet/DataSheetConfigDialog.tsx +12 -10
  122. package/src/components/data/sheet/types.ts +1 -1
  123. package/src/components/disclosure/Dialog.tsx +10 -10
  124. package/src/components/disclosure/dialogZIndex.ts +1 -1
  125. package/src/components/features/crud-detail/CrudDetail.tsx +25 -25
  126. package/src/components/features/crud-sheet/CrudSheet.tsx +53 -53
  127. package/src/components/features/crud-sheet/types.ts +4 -4
  128. package/src/components/features/data-select-button/DataSelectButton.tsx +51 -21
  129. package/src/components/features/permission-table/PermissionTable.tsx +3 -3
  130. package/src/components/features/shared-data/SharedDataSelect.tsx +172 -33
  131. package/src/components/features/shared-data/SharedDataSelectButton.tsx +3 -2
  132. package/src/components/features/shared-data/SharedDataSelectList.tsx +4 -4
  133. package/src/components/feedback/notification/NotificationBanner.tsx +3 -3
  134. package/src/components/feedback/notification/NotificationBell.tsx +6 -4
  135. package/src/components/feedback/notification/NotificationProvider.tsx +3 -1
  136. package/src/components/form-control/ThemeToggle.tsx +10 -6
  137. package/src/components/form-control/checkbox/Checkbox.tsx +4 -1
  138. package/src/components/form-control/checkbox/CheckboxGroup.tsx +1 -1
  139. package/src/components/form-control/checkbox/Radio.tsx +4 -1
  140. package/src/components/form-control/checkbox/RadioGroup.tsx +1 -1
  141. package/src/components/form-control/color-picker/ColorPicker.tsx +4 -1
  142. package/src/components/form-control/combobox/Combobox.tsx +6 -3
  143. package/src/components/form-control/date-range-picker/DateRangePicker.tsx +8 -8
  144. package/src/components/form-control/editor/EditorToolbar.tsx +23 -23
  145. package/src/components/form-control/field/DatePicker.tsx +6 -3
  146. package/src/components/form-control/field/DateTimePicker.tsx +6 -3
  147. package/src/components/form-control/field/NumberInput.tsx +6 -3
  148. package/src/components/form-control/field/TextInput.tsx +7 -4
  149. package/src/components/form-control/field/Textarea.tsx +6 -3
  150. package/src/components/form-control/field/TimePicker.tsx +6 -3
  151. package/src/components/form-control/numpad/Numpad.tsx +3 -1
  152. package/src/components/form-control/select/Select.tsx +7 -7
  153. package/src/components/form-control/state-preset/StatePreset.tsx +14 -12
  154. package/src/components/layout/sidebar/SidebarContainer.tsx +3 -3
  155. package/src/components/layout/sidebar/SidebarMenu.tsx +3 -1
  156. package/src/components/layout/topbar/Topbar.tsx +3 -3
  157. package/src/components/layout/topbar/TopbarMenu.tsx +3 -3
  158. package/src/hooks/createSelectionGroup.tsx +8 -4
  159. package/src/providers/i18n/I18nContext.tsx +0 -7
  160. package/src/providers/i18n/locales/en.ts +38 -0
  161. package/src/providers/i18n/locales/ko.ts +38 -0
  162. package/tailwind.config.ts +2 -2
  163. package/tests/components/data/kanban/Kanban.selection.spec.tsx +34 -24
  164. package/tests/components/disclosure/Dialog.spec.tsx +28 -28
  165. package/tests/components/disclosure/DialogProvider.spec.tsx +51 -25
  166. package/tests/components/features/address/AddressSearch.spec.tsx +12 -4
  167. package/tests/components/features/crud-detail/CrudDetail.spec.tsx +1 -0
  168. package/tests/components/features/crud-sheet/CrudSheet.spec.tsx +30 -6
  169. package/tests/components/features/data-select-button/DataSelectButton.spec.tsx +77 -56
  170. package/tests/components/features/permission-table/PermissionTable.spec.tsx +12 -8
  171. package/tests/components/features/shared-data/SharedDataSelect.spec.tsx +172 -0
  172. package/tests/components/features/shared-data/SharedDataSelectList.spec.tsx +14 -2
  173. package/tests/components/feedback/notification/LiveRegion.spec.tsx +20 -9
  174. package/tests/components/feedback/notification/NotificationBanner.spec.tsx +64 -46
  175. package/tests/components/feedback/notification/NotificationBell.spec.tsx +70 -51
  176. package/tests/components/feedback/notification/NotificationContext.spec.tsx +105 -78
  177. package/tests/components/form-control/checkbox/Checkbox.spec.tsx +25 -20
  178. package/tests/components/form-control/checkbox/CheckboxGroup.spec.tsx +53 -30
  179. package/tests/components/form-control/checkbox/Radio.spec.tsx +25 -20
  180. package/tests/components/form-control/checkbox/RadioGroup.spec.tsx +53 -30
  181. package/tests/components/form-control/color-picker/ColorPicker.spec.tsx +24 -15
  182. package/tests/components/form-control/combobox/Combobox.spec.tsx +92 -59
  183. package/tests/components/form-control/date-range-picker/DateRangePicker.spec.tsx +2 -2
  184. package/tests/components/form-control/field/DatePicker.spec.tsx +50 -44
  185. package/tests/components/form-control/field/DateTimePicker.spec.tsx +51 -45
  186. package/tests/components/form-control/field/NumberInput.spec.tsx +53 -47
  187. package/tests/components/form-control/field/TextInput.spec.tsx +50 -44
  188. package/tests/components/form-control/field/Textarea.spec.tsx +35 -29
  189. package/tests/components/form-control/field/TimePicker.spec.tsx +43 -37
  190. package/tests/components/form-control/numpad/Numpad.spec.tsx +175 -25
  191. package/tests/components/form-control/select/Select.spec.tsx +5 -0
  192. package/tests/components/form-control/select/SelectItem.spec.tsx +1 -0
  193. package/tests/components/layout/sidebar/Sidebar.spec.tsx +79 -35
  194. package/tests/components/layout/sidebar/SidebarContainer.spec.tsx +1 -0
  195. package/tests/components/layout/sidebar/SidebarMenu.spec.tsx +28 -17
  196. package/tests/components/layout/topbar/TopbarActions.spec.tsx +41 -23
  197. package/tests/components/layout/topbar/createTopbarActions.spec.tsx +1 -0
  198. package/tests/hooks/usePrint.spec.tsx +1 -1
  199. package/tests/hooks/useRouterLink.spec.tsx +2 -0
  200. package/tests/hooks/useSyncConfig.spec.tsx +1 -0
  201. package/tests/providers/ErrorLoggerProvider.spec.tsx +1 -0
  202. package/tests/providers/PwaUpdateProvider.spec.tsx +16 -6
  203. package/tests/providers/ServiceClientContext.spec.tsx +40 -25
  204. package/tests/providers/i18n/I18nContext.spec.tsx +3 -4
  205. package/tests/providers/shared-data/SharedDataProvider.spec.tsx +2 -0
  206. package/dist/hooks/usePrint.d.ts +0 -3
  207. package/dist/hooks/usePrint.d.ts.map +0 -1
  208. package/dist/hooks/usePrint.js +0 -5
  209. package/dist/hooks/usePrint.js.map +0 -6
  210. package/src/hooks/usePrint.ts +0 -2
@@ -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
 
@@ -31,7 +31,7 @@ const { Group } = createSelectionGroup({
31
31
  mode: "multiple",
32
32
  contextName: "CheckboxGroup",
33
33
  ItemComponent: Checkbox,
34
- emptyErrorMsg: "Please select an item",
34
+ emptyErrorMsgKey: "validation.selectItem",
35
35
  });
36
36
 
37
37
  export const CheckboxGroup = Group as unknown as CheckboxGroupComponent;
@@ -3,6 +3,7 @@ import { twMerge } from "tailwind-merge";
3
3
  import { createControllableSignal } from "../../../hooks/createControllableSignal";
4
4
  import { ripple } from "../../../directives/ripple";
5
5
  import clsx from "clsx";
6
+ import { useI18n } from "../../../providers/i18n/I18nContext";
6
7
  import {
7
8
  type CheckboxSize,
8
9
  checkboxBaseClass,
@@ -52,6 +53,8 @@ export const Radio: ParentComponent<RadioProps> = (props) => {
52
53
  "children",
53
54
  ]);
54
55
 
56
+ const i18n = useI18n();
57
+
55
58
  const [value, setValue] = createControllableSignal({
56
59
  value: () => local.value ?? false,
57
60
  onChange: () => local.onValueChange,
@@ -85,7 +88,7 @@ export const Radio: ParentComponent<RadioProps> = (props) => {
85
88
 
86
89
  const errorMsg = createMemo(() => {
87
90
  const v = local.value ?? false;
88
- if (local.required && !v) return "This is a required selection";
91
+ if (local.required && !v) return i18n.t("validation.requiredSelection");
89
92
  return local.validate?.(v);
90
93
  });
91
94
 
@@ -31,7 +31,7 @@ const { Group } = createSelectionGroup({
31
31
  mode: "single",
32
32
  contextName: "RadioGroup",
33
33
  ItemComponent: Radio,
34
- emptyErrorMsg: "Please select an item",
34
+ emptyErrorMsgKey: "validation.selectItem",
35
35
  });
36
36
 
37
37
  export const RadioGroup = Group as unknown as RadioGroupComponent;
@@ -4,6 +4,7 @@ import { twMerge } from "tailwind-merge";
4
4
  import { createControllableSignal } from "../../../hooks/createControllableSignal";
5
5
  import { Invalid } from "../Invalid";
6
6
  import { type ComponentSize } from "../../../styles/tokens.styles";
7
+ import { useI18n } from "../../../providers/i18n/I18nContext";
7
8
 
8
9
  // Base style
9
10
  const baseClass = clsx(
@@ -94,6 +95,8 @@ export const ColorPicker: Component<ColorPickerProps> = (props) => {
94
95
  "style",
95
96
  ]);
96
97
 
98
+ const i18n = useI18n();
99
+
97
100
  const [value, setValue] = createControllableSignal({
98
101
  value: () => local.value,
99
102
  onChange: () => local.onValueChange,
@@ -113,7 +116,7 @@ export const ColorPicker: Component<ColorPickerProps> = (props) => {
113
116
 
114
117
  const errorMsg = createMemo(() => {
115
118
  const v = value();
116
- if (local.required && (v === undefined || v === "")) return "This field is required";
119
+ if (local.required && (v === undefined || v === "")) return i18n.t("validation.required");
117
120
  return local.validate?.(v);
118
121
  });
119
122
 
@@ -12,6 +12,7 @@ import { createControllableSignal } from "../../../hooks/createControllableSigna
12
12
  import { type ComponentSize, textMuted } from "../../../styles/tokens.styles";
13
13
  import { chevronWrapperClass, getTriggerClass } from "../DropdownTrigger.styles";
14
14
  import { Invalid } from "../Invalid";
15
+ import { useI18n } from "../../../providers/i18n/I18nContext";
15
16
 
16
17
  void ripple;
17
18
 
@@ -149,6 +150,8 @@ export const Combobox: ComboboxComponent = <T,>(props: ComboboxProps<T>) => {
149
150
  "touchMode",
150
151
  ]);
151
152
 
153
+ const i18n = useI18n();
154
+
152
155
  // State
153
156
  const [open, setOpen] = createSignal(false);
154
157
  const [query, setQuery] = createSignal("");
@@ -259,7 +262,7 @@ export const Combobox: ComboboxComponent = <T,>(props: ComboboxProps<T>) => {
259
262
  const errorMsg = createMemo(() => {
260
263
  const v = getValue();
261
264
  if (local.required && (v === undefined || v === null || v === ""))
262
- return "This field is required";
265
+ return i18n.t("validation.required");
263
266
  return local.validate?.(v);
264
267
  });
265
268
 
@@ -310,12 +313,12 @@ export const Combobox: ComboboxComponent = <T,>(props: ComboboxProps<T>) => {
310
313
 
311
314
  // Loading
312
315
  if (busyCount() > 0) {
313
- return <div class={noResultsClass}>Searching...</div>;
316
+ return <div class={noResultsClass}>{i18n.t("combobox.searching")}</div>;
314
317
  }
315
318
 
316
319
  // Items empty
317
320
  if (items().length === 0) {
318
- return <div class={noResultsClass}>No results found</div>;
321
+ return <div class={noResultsClass}>{i18n.t("combobox.noResults")}</div>;
319
322
  }
320
323
 
321
324
  // ItemTemplate approach
@@ -6,7 +6,7 @@ import { createControllableSignal } from "../../../hooks/createControllableSigna
6
6
  import { type FieldSize } from "../field/Field.styles";
7
7
  import { DatePicker } from "../field/DatePicker";
8
8
  import { Select } from "../select/Select";
9
- import { useI18nOptional } from "../../../providers/i18n/I18nContext";
9
+ import { useI18n } from "../../../providers/i18n/I18nContext";
10
10
 
11
11
  export type DateRangePeriodType = "day" | "month" | "range";
12
12
 
@@ -78,7 +78,7 @@ function getLastDayOfMonth(date: DateOnly): DateOnly {
78
78
  * ```
79
79
  */
80
80
  export const DateRangePicker: Component<DateRangePickerProps> = (props) => {
81
- const i18n = useI18nOptional();
81
+ const i18n = useI18n();
82
82
 
83
83
  const [local, rest] = splitProps(props, [
84
84
  "periodType",
@@ -163,9 +163,9 @@ export const DateRangePicker: Component<DateRangePickerProps> = (props) => {
163
163
  onValueChange={handlePeriodTypeChange}
164
164
  renderValue={(v: DateRangePeriodType) => {
165
165
  const labels = {
166
- day: i18n?.t("dateRangePicker.day") ?? "Day",
167
- month: i18n?.t("dateRangePicker.month") ?? "Month",
168
- range: i18n?.t("dateRangePicker.range") ?? "Range"
166
+ day: i18n.t("dateRangePicker.day"),
167
+ month: i18n.t("dateRangePicker.month"),
168
+ range: i18n.t("dateRangePicker.range"),
169
169
  };
170
170
  return <>{labels[v]}</>;
171
171
  }}
@@ -175,13 +175,13 @@ export const DateRangePicker: Component<DateRangePickerProps> = (props) => {
175
175
  inset
176
176
  >
177
177
  <Select.Item value={"day" as DateRangePeriodType}>
178
- {i18n?.t("dateRangePicker.day") ?? "Day"}
178
+ {i18n.t("dateRangePicker.day")}
179
179
  </Select.Item>
180
180
  <Select.Item value={"month" as DateRangePeriodType}>
181
- {i18n?.t("dateRangePicker.month") ?? "Month"}
181
+ {i18n.t("dateRangePicker.month")}
182
182
  </Select.Item>
183
183
  <Select.Item value={"range" as DateRangePeriodType}>
184
- {i18n?.t("dateRangePicker.range") ?? "Range"}
184
+ {i18n.t("dateRangePicker.range")}
185
185
  </Select.Item>
186
186
  </Select>
187
187