@simplysm/solid 13.0.84 → 13.0.86

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 (180) hide show
  1. package/README.md +143 -28
  2. package/dist/components/data/list/ListItem.d.ts.map +1 -1
  3. package/dist/components/data/list/ListItem.js +11 -4
  4. package/dist/components/data/list/ListItem.js.map +2 -2
  5. package/dist/components/data/list/ListItem.styles.d.ts +2 -0
  6. package/dist/components/data/list/ListItem.styles.d.ts.map +1 -1
  7. package/dist/components/data/list/ListItem.styles.js +11 -1
  8. package/dist/components/data/list/ListItem.styles.js.map +1 -1
  9. package/dist/components/data/sheet/DataSheet.d.ts.map +1 -1
  10. package/dist/components/data/sheet/DataSheet.js +6 -9
  11. package/dist/components/data/sheet/DataSheet.js.map +2 -2
  12. package/dist/components/data/sheet/hooks/createDataSheetExpansion.d.ts.map +1 -1
  13. package/dist/components/data/sheet/hooks/createDataSheetExpansion.js +15 -17
  14. package/dist/components/data/sheet/hooks/createDataSheetExpansion.js.map +1 -1
  15. package/dist/components/data/sheet/hooks/createDataSheetReorder.d.ts.map +1 -1
  16. package/dist/components/data/sheet/hooks/createDataSheetReorder.js +12 -12
  17. package/dist/components/data/sheet/hooks/createDataSheetReorder.js.map +1 -1
  18. package/dist/components/data/sheet/hooks/createDataSheetSelection.d.ts.map +1 -1
  19. package/dist/components/data/sheet/hooks/createDataSheetSelection.js +9 -3
  20. package/dist/components/data/sheet/hooks/createDataSheetSelection.js.map +1 -1
  21. package/dist/components/disclosure/Dialog.d.ts.map +1 -1
  22. package/dist/components/disclosure/Dialog.js +3 -21
  23. package/dist/components/disclosure/Dialog.js.map +2 -2
  24. package/dist/components/disclosure/Dropdown.d.ts.map +1 -1
  25. package/dist/components/disclosure/Dropdown.js +1 -11
  26. package/dist/components/disclosure/Dropdown.js.map +2 -2
  27. package/dist/components/disclosure/Tabs.d.ts.map +1 -1
  28. package/dist/components/disclosure/Tabs.js +1 -3
  29. package/dist/components/disclosure/Tabs.js.map +2 -2
  30. package/dist/components/features/crud-detail/CrudDetail.js +103 -102
  31. package/dist/components/features/crud-detail/CrudDetail.js.map +2 -2
  32. package/dist/components/features/crud-sheet/CrudSheet.d.ts.map +1 -1
  33. package/dist/components/features/crud-sheet/CrudSheet.js +10 -5
  34. package/dist/components/features/crud-sheet/CrudSheet.js.map +2 -2
  35. package/dist/components/features/data-select-button/DataSelectButton.d.ts.map +1 -1
  36. package/dist/components/features/data-select-button/DataSelectButton.js +30 -26
  37. package/dist/components/features/data-select-button/DataSelectButton.js.map +2 -2
  38. package/dist/components/features/permission-table/PermissionTable.js +5 -1
  39. package/dist/components/features/permission-table/PermissionTable.js.map +2 -2
  40. package/dist/components/feedback/busy/BusyContainer.d.ts.map +1 -1
  41. package/dist/components/feedback/busy/BusyContainer.js +1 -6
  42. package/dist/components/feedback/busy/BusyContainer.js.map +2 -2
  43. package/dist/components/form-control/DropdownTrigger.styles.js +1 -1
  44. package/dist/components/form-control/checkbox/SelectableBase.d.ts.map +1 -1
  45. package/dist/components/form-control/checkbox/SelectableBase.js +2 -4
  46. package/dist/components/form-control/checkbox/SelectableBase.js.map +2 -2
  47. package/dist/components/form-control/combobox/Combobox.d.ts +19 -5
  48. package/dist/components/form-control/combobox/Combobox.d.ts.map +1 -1
  49. package/dist/components/form-control/combobox/Combobox.js +2 -4
  50. package/dist/components/form-control/combobox/Combobox.js.map +1 -1
  51. package/dist/components/form-control/date-range-picker/DateRangePicker.d.ts +2 -2
  52. package/dist/components/form-control/date-range-picker/DateRangePicker.d.ts.map +1 -1
  53. package/dist/components/form-control/date-range-picker/DateRangePicker.js +11 -3
  54. package/dist/components/form-control/date-range-picker/DateRangePicker.js.map +2 -2
  55. package/dist/components/form-control/editor/RichTextEditor.d.ts +2 -2
  56. package/dist/components/form-control/editor/RichTextEditor.d.ts.map +1 -1
  57. package/dist/components/form-control/editor/RichTextEditor.js +2 -4
  58. package/dist/components/form-control/editor/RichTextEditor.js.map +2 -2
  59. package/dist/components/form-control/field/DatePicker.d.ts +2 -2
  60. package/dist/components/form-control/field/DatePicker.d.ts.map +1 -1
  61. package/dist/components/form-control/field/DatePicker.js.map +1 -1
  62. package/dist/components/form-control/field/DateTimePicker.d.ts +2 -2
  63. package/dist/components/form-control/field/DateTimePicker.d.ts.map +1 -1
  64. package/dist/components/form-control/field/DateTimePicker.js.map +1 -1
  65. package/dist/components/form-control/field/Field.styles.d.ts +6 -7
  66. package/dist/components/form-control/field/Field.styles.d.ts.map +1 -1
  67. package/dist/components/form-control/field/Field.styles.js.map +1 -1
  68. package/dist/components/form-control/field/NumberInput.d.ts +2 -2
  69. package/dist/components/form-control/field/NumberInput.d.ts.map +1 -1
  70. package/dist/components/form-control/field/NumberInput.js +7 -7
  71. package/dist/components/form-control/field/NumberInput.js.map +2 -2
  72. package/dist/components/form-control/field/TextInput.d.ts +2 -2
  73. package/dist/components/form-control/field/TextInput.d.ts.map +1 -1
  74. package/dist/components/form-control/field/TextInput.js.map +1 -1
  75. package/dist/components/form-control/field/Textarea.d.ts +2 -2
  76. package/dist/components/form-control/field/Textarea.d.ts.map +1 -1
  77. package/dist/components/form-control/field/Textarea.js +1 -3
  78. package/dist/components/form-control/field/Textarea.js.map +2 -2
  79. package/dist/components/form-control/field/TimePicker.d.ts +2 -2
  80. package/dist/components/form-control/field/TimePicker.d.ts.map +1 -1
  81. package/dist/components/form-control/field/TimePicker.js.map +1 -1
  82. package/dist/components/form-control/numpad/Numpad.d.ts.map +1 -1
  83. package/dist/components/form-control/numpad/Numpad.js +4 -17
  84. package/dist/components/form-control/numpad/Numpad.js.map +2 -2
  85. package/dist/components/form-control/select/Select.d.ts +2 -0
  86. package/dist/components/form-control/select/Select.d.ts.map +1 -1
  87. package/dist/components/form-control/select/Select.js +29 -15
  88. package/dist/components/form-control/select/Select.js.map +2 -2
  89. package/dist/components/form-control/state-preset/StatePreset.d.ts +1 -3
  90. package/dist/components/form-control/state-preset/StatePreset.d.ts.map +1 -1
  91. package/dist/components/form-control/state-preset/StatePreset.js +69 -95
  92. package/dist/components/form-control/state-preset/StatePreset.js.map +2 -2
  93. package/dist/components/layout/FormGroup.js +1 -1
  94. package/dist/components/layout/FormGroup.js.map +1 -1
  95. package/dist/components/layout/FormTable.js +3 -3
  96. package/dist/components/layout/FormTable.js.map +1 -1
  97. package/dist/components/layout/sidebar/Sidebar.d.ts.map +1 -1
  98. package/dist/components/layout/sidebar/Sidebar.js +3 -6
  99. package/dist/components/layout/sidebar/Sidebar.js.map +2 -2
  100. package/dist/components/layout/topbar/Topbar.js +1 -3
  101. package/dist/components/layout/topbar/Topbar.js.map +2 -2
  102. package/dist/hooks/createControllableStore.d.ts.map +1 -1
  103. package/dist/hooks/createControllableStore.js +8 -5
  104. package/dist/hooks/createControllableStore.js.map +1 -1
  105. package/dist/hooks/useLocalStorage.d.ts.map +1 -1
  106. package/dist/hooks/useLocalStorage.js +3 -2
  107. package/dist/hooks/useLocalStorage.js.map +1 -1
  108. package/dist/hooks/useSyncConfig.d.ts.map +1 -1
  109. package/dist/hooks/useSyncConfig.js +5 -4
  110. package/dist/hooks/useSyncConfig.js.map +1 -1
  111. package/dist/providers/i18n/locales/en.d.ts +2 -3
  112. package/dist/providers/i18n/locales/en.d.ts.map +1 -1
  113. package/dist/providers/i18n/locales/en.js +3 -4
  114. package/dist/providers/i18n/locales/en.js.map +1 -1
  115. package/dist/providers/i18n/locales/ko.d.ts +2 -3
  116. package/dist/providers/i18n/locales/ko.d.ts.map +1 -1
  117. package/dist/providers/i18n/locales/ko.js +3 -4
  118. package/dist/providers/i18n/locales/ko.js.map +1 -1
  119. package/dist/providers/shared-data/SharedDataProvider.d.ts.map +1 -1
  120. package/dist/providers/shared-data/SharedDataProvider.js +0 -1
  121. package/dist/providers/shared-data/SharedDataProvider.js.map +1 -1
  122. package/docs/display-feedback.md +279 -0
  123. package/docs/features.md +357 -213
  124. package/docs/form-controls.md +261 -403
  125. package/docs/layout-data.md +386 -0
  126. package/docs/providers-hooks.md +411 -0
  127. package/package.json +5 -5
  128. package/src/components/data/list/ListItem.styles.ts +14 -2
  129. package/src/components/data/list/ListItem.tsx +13 -4
  130. package/src/components/data/sheet/DataSheet.tsx +6 -10
  131. package/src/components/data/sheet/hooks/createDataSheetExpansion.ts +17 -18
  132. package/src/components/data/sheet/hooks/createDataSheetReorder.ts +12 -13
  133. package/src/components/data/sheet/hooks/createDataSheetSelection.ts +9 -3
  134. package/src/components/disclosure/Dialog.tsx +45 -59
  135. package/src/components/disclosure/Dropdown.tsx +4 -14
  136. package/src/components/disclosure/Tabs.tsx +12 -17
  137. package/src/components/features/crud-detail/CrudDetail.tsx +4 -4
  138. package/src/components/features/crud-sheet/CrudSheet.tsx +12 -5
  139. package/src/components/features/data-select-button/DataSelectButton.tsx +39 -32
  140. package/src/components/features/permission-table/PermissionTable.tsx +1 -1
  141. package/src/components/feedback/busy/BusyContainer.tsx +12 -18
  142. package/src/components/form-control/DropdownTrigger.styles.ts +1 -1
  143. package/src/components/form-control/checkbox/SelectableBase.tsx +10 -16
  144. package/src/components/form-control/combobox/Combobox.tsx +42 -16
  145. package/src/components/form-control/date-range-picker/DateRangePicker.tsx +7 -8
  146. package/src/components/form-control/editor/RichTextEditor.tsx +14 -16
  147. package/src/components/form-control/field/DatePicker.tsx +3 -2
  148. package/src/components/form-control/field/DateTimePicker.tsx +3 -2
  149. package/src/components/form-control/field/Field.styles.ts +6 -8
  150. package/src/components/form-control/field/NumberInput.tsx +9 -10
  151. package/src/components/form-control/field/TextInput.tsx +3 -2
  152. package/src/components/form-control/field/Textarea.tsx +14 -12
  153. package/src/components/form-control/field/TimePicker.tsx +3 -2
  154. package/src/components/form-control/numpad/Numpad.tsx +16 -18
  155. package/src/components/form-control/select/Select.tsx +41 -13
  156. package/src/components/form-control/state-preset/StatePreset.tsx +39 -71
  157. package/src/components/layout/FormGroup.tsx +1 -1
  158. package/src/components/layout/FormTable.tsx +3 -3
  159. package/src/components/layout/sidebar/Sidebar.tsx +2 -3
  160. package/src/components/layout/topbar/Topbar.tsx +2 -2
  161. package/src/hooks/createControllableStore.ts +8 -4
  162. package/src/hooks/useLocalStorage.ts +3 -2
  163. package/src/hooks/useSyncConfig.ts +5 -4
  164. package/src/providers/i18n/locales/en.ts +2 -3
  165. package/src/providers/i18n/locales/ko.ts +2 -3
  166. package/src/providers/shared-data/SharedDataProvider.tsx +0 -1
  167. package/tests/components/features/crud-detail/CrudDetail.spec.tsx +49 -0
  168. package/tests/components/features/data-select-button/DataSelectButton.spec.tsx +62 -7
  169. package/tests/components/form-control/combobox/Combobox.spec.tsx +3 -3
  170. package/tests/components/form-control/date-range-picker/DateRangePicker.spec.tsx +56 -0
  171. package/tests/components/form-control/select/SelectItem.spec.tsx +5 -0
  172. package/tests/providers/shared-data/SharedDataProvider.spec.tsx +0 -104
  173. package/docs/data.md +0 -204
  174. package/docs/disclosure.md +0 -146
  175. package/docs/display.md +0 -125
  176. package/docs/feedback.md +0 -156
  177. package/docs/helpers.md +0 -173
  178. package/docs/hooks.md +0 -146
  179. package/docs/layout.md +0 -94
  180. package/docs/providers.md +0 -180
@@ -32,12 +32,16 @@ import { TextInput } from "../field/TextInput";
32
32
  import { useI18n } from "../../../providers/i18n/I18nProvider";
33
33
  import {
34
34
  listItemBaseClass,
35
+ listItemSizeClasses,
35
36
  listItemSelectedClass,
36
37
  listItemDisabledClass,
37
38
  listItemIndentGuideClass,
38
39
  listItemContentClass,
39
40
  getListItemSelectedIconClass,
41
+ listItemBasePadLeft,
42
+ LIST_ITEM_INDENT_SIZE,
40
43
  } from "../../data/list/ListItem.styles";
44
+ import { useListContext } from "../../data/list/ListContext";
41
45
 
42
46
  void ripple;
43
47
 
@@ -65,13 +69,15 @@ export interface SelectContextValue<TValue = unknown> {
65
69
 
66
70
  /** Register item template */
67
71
  setItemTemplate: (fn: ((...args: unknown[]) => JSX.Element) | undefined) => void;
72
+
73
+ /** Trigger size */
74
+ size: Accessor<ComponentSize>;
68
75
  }
69
76
 
70
77
  export const SelectContext = createContext<SelectContextValue>();
71
- const SelectCtx = SelectContext;
72
78
 
73
79
  function useSelectContext<TValue = unknown>(): SelectContextValue<TValue> {
74
- const context = useContext(SelectCtx);
80
+ const context = useContext(SelectContext);
75
81
  if (!context) {
76
82
  throw new Error("useSelectContext can only be used inside Select component");
77
83
  }
@@ -90,6 +96,7 @@ const [SelectActionSlot, createActionSlotAccessor] = createSlot<{ children: JSX.
90
96
 
91
97
  const SelectAction = (props: SelectActionProps) => {
92
98
  const [local, rest] = splitProps(props, ["children", "class"]);
99
+ const ctx = useSelectContext();
93
100
 
94
101
  const handleClick: JSX.EventHandlerUnion<HTMLButtonElement, MouseEvent> = (e) => {
95
102
  if (typeof rest.onClick === "function") {
@@ -104,8 +111,17 @@ const SelectAction = (props: SelectActionProps) => {
104
111
  <button
105
112
  {...rest}
106
113
  type="button"
114
+ use:ripple={!rest.disabled}
107
115
  onClick={handleClick}
108
- class={twMerge("p-2", themeTokens.base.hoverBg, local.class)}
116
+ class={twMerge(
117
+ "inline-flex items-center",
118
+ pad[ctx.size()],
119
+ "border",
120
+ border.default,
121
+ groupFocusBorderClass,
122
+ themeTokens.base.hoverBg,
123
+ local.class,
124
+ )}
109
125
  data-select-action
110
126
  >
111
127
  {local.children}
@@ -116,6 +132,8 @@ const SelectAction = (props: SelectActionProps) => {
116
132
 
117
133
  //#endregion
118
134
 
135
+ const groupFocusBorderClass = "group-focus-within:border-primary-400 dark:group-focus-within:border-primary-400";
136
+
119
137
  const selectAllBtnClass = clsx(
120
138
  "text-primary-500",
121
139
  "hover:text-primary-600 dark:hover:text-primary-400",
@@ -163,9 +181,11 @@ export interface SelectItemProps<TValue = unknown> extends Omit<
163
181
  const SelectItemInner = <TValue,>(
164
182
  props: SelectItemProps<TValue> & { children?: JSX.Element },
165
183
  ) => {
166
- const [local, rest] = splitProps(props, ["children", "class", "value", "disabled"]);
184
+ const [local, rest] = splitProps(props, ["children", "class", "style", "value", "disabled"]);
167
185
 
168
186
  const context = useSelectContext<TValue>();
187
+ const listContext = useListContext();
188
+ const level = listContext.level;
169
189
 
170
190
  const [childrenSlot, ItemChildrenProvider] = createItemChildrenSlotAccessor();
171
191
  const hasChildren = () => childrenSlot() !== undefined;
@@ -186,12 +206,18 @@ const SelectItemInner = <TValue,>(
186
206
  const getClassName = () =>
187
207
  twMerge(
188
208
  listItemBaseClass,
209
+ listItemSizeClasses[context.size()],
189
210
  isSelected() && listItemSelectedClass,
190
211
  local.disabled && listItemDisabledClass,
191
212
  local.class,
192
213
  );
193
214
 
194
- const getCheckIconClass = () => getListItemSelectedIconClass(isSelected());
215
+ const indentPaddingLeft = level >= 1
216
+ ? `${listItemBasePadLeft[context.size()] + level * LIST_ITEM_INDENT_SIZE}rem`
217
+ : undefined;
218
+
219
+ const getGuideLeft = () =>
220
+ `${listItemBasePadLeft[context.size()] + level * LIST_ITEM_INDENT_SIZE + LIST_ITEM_INDENT_SIZE * 0.5}rem`;
195
221
 
196
222
  return (
197
223
  <ItemChildrenProvider>
@@ -200,6 +226,7 @@ const SelectItemInner = <TValue,>(
200
226
  type="button"
201
227
  use:ripple={useRipple()}
202
228
  class={getClassName()}
229
+ style={{ ...(local.style as JSX.CSSProperties), "padding-left": indentPaddingLeft }}
203
230
  data-select-item
204
231
  data-list-item
205
232
  role="option"
@@ -209,15 +236,15 @@ const SelectItemInner = <TValue,>(
209
236
  onClick={handleClick}
210
237
  >
211
238
  <Show when={context.multiple() && !hasChildren()}>
212
- <Icon icon={IconCheck} class={getCheckIconClass()} />
239
+ <Icon icon={IconCheck} class={getListItemSelectedIconClass(isSelected())} />
213
240
  </Show>
214
241
  <span class={listItemContentClass}>{local.children}</span>
215
242
  </button>
216
243
  <Show when={hasChildren()}>
217
244
  <Collapse open={true}>
218
- <div class="flex">
219
- <div class={listItemIndentGuideClass} />
220
- <List inset class="flex-1">
245
+ <div class="relative">
246
+ <div class={listItemIndentGuideClass} style={{ left: getGuideLeft() }} />
247
+ <List inset>
221
248
  {childrenSlot()!.children}
222
249
  </List>
223
250
  </div>
@@ -440,6 +467,7 @@ const SelectInnerComponent = <TValue,>(props: SelectProps<TValue>) => {
440
467
  toggleValue,
441
468
  closeDropdown,
442
469
  setItemTemplate,
470
+ size: () => local.size ?? "md",
443
471
  };
444
472
 
445
473
  // Trigger keyboard handling (only Enter/Space, ArrowUp/Down handled by Dropdown)
@@ -608,7 +636,7 @@ const SelectInnerComponent = <TValue,>(props: SelectProps<TValue>) => {
608
636
  local.multiple === true && !local.hideSelectAll && local.items !== undefined;
609
637
 
610
638
  return (
611
- <div {...rest} data-select class={clsx("group", local.inset ? "flex" : "inline-flex")}>
639
+ <div {...rest} data-select class={clsx("group", local.inset ? "flex" : "inline-flex", "[&>[data-dropdown-trigger]]:flex")}>
612
640
  <Dropdown disabled={local.disabled} open={open()} onOpenChange={setOpen} keyboardNav>
613
641
  <Dropdown.Trigger>
614
642
  <div
@@ -624,7 +652,7 @@ const SelectInnerComponent = <TValue,>(props: SelectProps<TValue>) => {
624
652
  action() !== undefined &&
625
653
  clsx(
626
654
  "rounded-r-none border-r-0",
627
- "group-focus-within:border-primary-400 dark:group-focus-within:border-primary-400",
655
+ groupFocusBorderClass,
628
656
  ),
629
657
  )}
630
658
  style={local.style}
@@ -698,13 +726,13 @@ const SelectInnerComponent = <TValue,>(props: SelectProps<TValue>) => {
698
726
 
699
727
  return (
700
728
  <Invalid message={errorMsg()} variant="border" lazyValidation={local.lazyValidation}>
701
- <SelectCtx.Provider value={contextValue as SelectContextValue}>
729
+ <SelectContext.Provider value={contextValue as SelectContextValue}>
702
730
  <HeaderProvider>
703
731
  <ActionProvider>
704
732
  <SelectInnerRender>{local.children}</SelectInnerRender>
705
733
  </ActionProvider>
706
734
  </HeaderProvider>
707
- </SelectCtx.Provider>
735
+ </SelectContext.Provider>
708
736
  </Invalid>
709
737
  );
710
738
  };
@@ -9,7 +9,6 @@ import { useNotification } from "../../feedback/notification/NotificationProvide
9
9
  import { Icon } from "../../display/Icon";
10
10
  import { bg, text } from "../../../styles/base.styles";
11
11
  import { type ComponentSize, gap, pad } from "../../../styles/control.styles";
12
- import { themeTokens } from "../../../styles/theme.styles";
13
12
  import { Button } from "../Button";
14
13
  import { useI18n } from "../../../providers/i18n/I18nProvider";
15
14
 
@@ -20,53 +19,33 @@ interface StatePresetItem<TValue> {
20
19
  state: TValue;
21
20
  }
22
21
 
23
- type StatePresetSize = ComponentSize;
24
-
25
22
  export interface StatePresetProps<TValue> {
26
23
  storageKey: string;
27
24
  value: TValue;
28
25
  onValueChange: (value: TValue) => void;
29
- size?: StatePresetSize;
26
+ size?: ComponentSize;
30
27
  class?: string;
31
28
  style?: JSX.CSSProperties;
32
29
  }
33
30
 
34
31
  // ── Style constants ──
35
32
 
36
- const chipSizeClasses: Record<StatePresetSize, string> = {
33
+ const sizeClasses: Record<ComponentSize, string> = {
37
34
  md: pad.md,
38
- xs: clsx(pad.xs, "text-sm"),
35
+ xs: pad.xs,
39
36
  sm: pad.sm,
40
37
  lg: pad.lg,
41
- xl: clsx(pad.xl, "text-lg"),
42
- };
43
-
44
- const iconBtnSizeClasses: Record<StatePresetSize, string> = {
45
- md: "p-0.5",
46
- xs: "p-0",
47
- sm: "p-0.5",
48
- lg: "p-1",
49
- xl: "p-1.5",
50
- };
51
-
52
- const starBtnSizeClasses: Record<StatePresetSize, string> = {
53
- md: "p-1",
54
- xs: "p-0",
55
- sm: "p-0.5",
56
- lg: "p-1.5",
57
- xl: "p-2",
38
+ xl: pad.xl,
58
39
  };
59
40
 
60
- const inputSizeClasses: Record<StatePresetSize, string> = {
61
- md: clsx(pad.md, "w-24"),
62
- xs: clsx("w-16", pad.xs, "text-sm"),
63
- sm: clsx(pad.sm, "w-20"),
64
- lg: clsx(pad.lg, "w-32"),
65
- xl: clsx(pad.xl, "w-36 text-lg"),
41
+ const iconSizeMap: Record<ComponentSize, string> = {
42
+ xs: "0.875em",
43
+ sm: "1em",
44
+ md: "1.25em",
45
+ lg: "1.5em",
46
+ xl: "1.75em",
66
47
  };
67
48
 
68
- const iconSize = "0.85em";
69
-
70
49
  // ── Component ──
71
50
 
72
51
  function StatePresetInner<TValue>(props: StatePresetProps<TValue>): JSX.Element {
@@ -191,43 +170,31 @@ function StatePresetInner<TValue>(props: StatePresetProps<TValue>): JSX.Element
191
170
 
192
171
  // ── Render ──
193
172
 
194
- const containerClass = () => twMerge(clsx("inline-flex items-center", gap.lg, "flex-wrap"), local.class);
195
-
196
- const resolvedChipClass = () => twMerge(
197
- clsx("inline-flex items-center", gap.md, "rounded-full", bg.subtle, text.default, "cursor-default"),
198
- chipSizeClasses[local.size ?? "md"],
199
- );
200
-
201
- const resolvedIconBtnClass = () =>
202
- twMerge("rounded-full", iconBtnSizeClasses[local.size ?? "md"]);
203
-
204
- const resolvedStarBtnClass = () =>
205
- twMerge(
206
- clsx("inline-flex cursor-pointer items-center justify-center rounded-full text-warning-500 transition-colors focus:outline-none", themeTokens.warning.hoverBg),
207
- starBtnSizeClasses[local.size ?? "md"],
208
- );
209
-
210
- const resolvedInputClass = () => twMerge(
211
- clsx("rounded-full", bg.subtle, text.default, "border border-transparent focus:outline-none focus:ring-1 focus:ring-primary-400", text.placeholder),
212
- inputSizeClasses[local.size ?? "md"],
213
- );
173
+ const resolvedSize = () => local.size ?? "md";
214
174
 
215
175
  return (
216
- <div class={containerClass()} style={local.style}>
176
+ <div class={twMerge(clsx("inline-flex items-center", gap.lg, "flex-wrap"), local.class)} style={local.style}>
217
177
  {/* Star button - add preset */}
218
- <button
219
- type="button"
220
- class={resolvedStarBtnClass()}
178
+ <Button
179
+ size={local.size}
180
+ variant="ghost"
181
+ class="rounded-full text-warning-500"
221
182
  onClick={handleStartAdd}
222
183
  title={i18n.t("statePreset.addPreset")}
223
184
  >
224
- <Icon icon={IconStar} size={iconSize} />
225
- </button>
185
+ {"\u200B"}
186
+ <Icon icon={IconStar} size={iconSizeMap[resolvedSize()]} />
187
+ </Button>
226
188
 
227
189
  {/* Preset chips */}
228
190
  <For each={presets()}>
229
191
  {(preset, index) => (
230
- <span class={resolvedChipClass()}>
192
+ <span
193
+ class={twMerge(
194
+ clsx("inline-flex items-center", gap.md, "rounded-full border border-transparent", bg.subtle, text.default, "cursor-default"),
195
+ sizeClasses[resolvedSize()],
196
+ )}
197
+ >
231
198
  <button
232
199
  type="button"
233
200
  class="cursor-pointer hover:underline focus:outline-none"
@@ -236,24 +203,22 @@ function StatePresetInner<TValue>(props: StatePresetProps<TValue>): JSX.Element
236
203
  >
237
204
  {preset.name}
238
205
  </button>
239
- <Button
240
- variant="ghost"
241
- size="xs"
242
- class={resolvedIconBtnClass()}
206
+ <button
207
+ type="button"
208
+ class="cursor-pointer rounded-full opacity-50 hover:opacity-100 focus:outline-none"
243
209
  onClick={() => handleOverwrite(index())}
244
210
  title={i18n.t("statePreset.overwrite")}
245
211
  >
246
- <Icon icon={IconDeviceFloppy} size={iconSize} />
247
- </Button>
248
- <Button
249
- variant="ghost"
250
- size="xs"
251
- class={resolvedIconBtnClass()}
212
+ <Icon icon={IconDeviceFloppy} size={iconSizeMap[resolvedSize()]} />
213
+ </button>
214
+ <button
215
+ type="button"
216
+ class="cursor-pointer rounded-full opacity-50 hover:opacity-100 focus:outline-none"
252
217
  onClick={() => handleDelete(index())}
253
218
  title={i18n.t("statePreset.deletePreset")}
254
219
  >
255
- <Icon icon={IconX} size={iconSize} />
256
- </Button>
220
+ <Icon icon={IconX} size={iconSizeMap[resolvedSize()]} />
221
+ </button>
257
222
  </span>
258
223
  )}
259
224
  </For>
@@ -266,7 +231,10 @@ function StatePresetInner<TValue>(props: StatePresetProps<TValue>): JSX.Element
266
231
  requestAnimationFrame(() => el.focus());
267
232
  }}
268
233
  type="text"
269
- class={resolvedInputClass()}
234
+ class={twMerge(
235
+ clsx("inline-flex items-center rounded-full leading-normal", bg.subtle, text.default, "border border-transparent focus:outline-none focus:ring-1 focus:ring-primary-400", text.placeholder),
236
+ sizeClasses[resolvedSize()],
237
+ )}
270
238
  placeholder={i18n.t("statePreset.namePlaceholder")}
271
239
  autocomplete="one-time-code"
272
240
  value={inputValue()}
@@ -43,7 +43,7 @@ const FormGroupBase: ParentComponent<FormGroupProps> = (props) => {
43
43
  const [local, rest] = splitProps(props, ["children", "class", "inline"]);
44
44
 
45
45
  const getClassName = () => twMerge(
46
- local.inline ? "inline-flex flex-row flex-wrap items-center gap-2" : "inline-flex flex-col gap-2",
46
+ local.inline ? "inline-flex flex-row flex-wrap items-center gap-x-2 gap-y-1" : "inline-flex flex-col gap-2",
47
47
  local.class,
48
48
  );
49
49
 
@@ -11,7 +11,7 @@ export interface FormTableItemProps extends JSX.TdHTMLAttributes<HTMLTableCellEl
11
11
 
12
12
  const FormTableRow: ParentComponent<JSX.HTMLAttributes<HTMLTableRowElement>> = (props) => {
13
13
  const [local, rest] = splitProps(props, ["children", "class"]);
14
- return <tr class={twMerge("[&>*:last-child]:pr-0 last:[&>*]:pb-0", local.class)} {...rest}>{local.children}</tr>;
14
+ return <tr class={twMerge("[&>td:last-child]:pr-0 [&:last-child>*]:pb-0", local.class)} {...rest}>{local.children}</tr>;
15
15
  };
16
16
 
17
17
  const FormTableItem: ParentComponent<FormTableItemProps> = (props) => {
@@ -26,9 +26,9 @@ const FormTableItem: ParentComponent<FormTableItemProps> = (props) => {
26
26
  return (
27
27
  <>
28
28
  <Show when={local.label}>
29
- <th class="w-0 whitespace-nowrap pb-1 pl-1 pr-1.5 text-right align-middle">{local.label}</th>
29
+ <th class="w-0 whitespace-nowrap pb-1 pr-2 text-right align-middle">{local.label}</th>
30
30
  </Show>
31
- <td class={twMerge("pb-1 pr-1.5 align-middle", local.class)} colspan={effectiveColspan()} {...rest}>
31
+ <td class={twMerge("pb-1 pr-2 align-middle", local.class)} colspan={effectiveColspan()} {...rest}>
32
32
  {local.children}
33
33
  </td>
34
34
  </>
@@ -216,7 +216,6 @@ const MenuContext = createContext<MenuContextValue>();
216
216
  */
217
217
  const SidebarMenu: Component<SidebarMenuProps> = (props) => {
218
218
  const [local, rest] = splitProps(props, ["menus", "class"]);
219
- const i18n = useI18n();
220
219
 
221
220
  const location = useLocation();
222
221
 
@@ -252,9 +251,9 @@ const SidebarMenu: Component<SidebarMenuProps> = (props) => {
252
251
  return (
253
252
  <MenuContext.Provider value={{ initialOpenItems }}>
254
253
  <div {...rest} data-sidebar-menu class={getClassName()}>
255
- <div class={clsx("px-4 py-2 text-xs font-bold", text.muted, "uppercase tracking-wider")}>{i18n.t("sidebarMenu.menu")}</div>
254
+ <div class={clsx("px-3.5 py-1 text-sm font-bold", text.muted, "uppercase tracking-wider")}>MENU</div>
256
255
  <List inset>
257
- <For each={local.menus}>{(menu) => <MenuItem menu={menu} size="lg" />}</For>
256
+ <For each={local.menus}>{(menu) => <MenuItem menu={menu} />}</For>
258
257
  </List>
259
258
  </div>
260
259
  </MenuContext.Provider>
@@ -133,8 +133,8 @@ const TopbarInner: ParentComponent<TopbarProps> = (props) => {
133
133
  return (
134
134
  <header {...rest} data-topbar class={getClassName()}>
135
135
  <Show when={sidebarContext}>
136
- <Button variant="ghost" onClick={handleToggle} class="p-2" aria-label={i18n.t("topbar.toggleSidebar")}>
137
- <Icon icon={IconMenu2} size="1.5em" />
136
+ <Button variant="ghost" onClick={handleToggle} aria-label={i18n.t("topbar.toggleSidebar")}>
137
+ <Icon icon={IconMenu2} />
138
138
  </Button>
139
139
  </Show>
140
140
  {local.children}
@@ -39,10 +39,14 @@ export function createControllableStore<TValue extends object>(options: {
39
39
 
40
40
  // Wrap setter with a function wrapper to add onChange notification
41
41
  const wrappedSet = ((...args: any[]) => {
42
- const before = obj.clone(unwrap(store));
43
- (rawSet as any)(...args);
44
- if (!obj.equal(before, unwrap(store))) {
45
- options.onChange()?.(obj.clone(unwrap(store)));
42
+ if (options.onChange() != null) {
43
+ const before = obj.clone(unwrap(store));
44
+ (rawSet as any)(...args);
45
+ if (!obj.equal(before, unwrap(store))) {
46
+ options.onChange()!(obj.clone(unwrap(store)));
47
+ }
48
+ } else {
49
+ (rawSet as any)(...args);
46
50
  }
47
51
  }) as SetStoreFunction<TValue>;
48
52
 
@@ -1,4 +1,5 @@
1
1
  import { type Accessor, createSignal } from "solid-js";
2
+ import { json } from "@simplysm/core-common";
2
3
  import { useConfig } from "../providers/ConfigContext";
3
4
 
4
5
  type StorageSetter<TValue> = (
@@ -43,7 +44,7 @@ export function useLocalStorage<TValue>(
43
44
  try {
44
45
  const item = localStorage.getItem(prefixedKey);
45
46
  if (item !== null) {
46
- storedValue = JSON.parse(item) as TValue;
47
+ storedValue = json.parse<TValue>(item);
47
48
  }
48
49
  } catch {
49
50
  // Use initial value on JSON parse failure
@@ -67,7 +68,7 @@ export function useLocalStorage<TValue>(
67
68
  if (resolved === undefined) {
68
69
  localStorage.removeItem(prefixedKey);
69
70
  } else {
70
- localStorage.setItem(prefixedKey, JSON.stringify(resolved));
71
+ localStorage.setItem(prefixedKey, json.stringify(resolved));
71
72
  }
72
73
 
73
74
  return resolved;
@@ -1,4 +1,5 @@
1
1
  import { type Accessor, type Setter, createEffect, createSignal, untrack } from "solid-js";
2
+ import { json } from "@simplysm/core-common";
2
3
  import { useConfig } from "../providers/ConfigContext";
3
4
  import { useSyncStorage } from "../providers/SyncStorageProvider";
4
5
 
@@ -49,7 +50,7 @@ export function useSyncConfig<TValue>(
49
50
  try {
50
51
  const stored = localStorage.getItem(prefixedKey);
51
52
  if (stored !== null && writeVersion === versionBefore) {
52
- setValue(() => JSON.parse(stored) as TValue);
53
+ setValue(() => json.parse<TValue>(stored));
53
54
  }
54
55
  } catch {
55
56
  // Ignore parse errors, keep default value
@@ -62,14 +63,14 @@ export function useSyncConfig<TValue>(
62
63
  try {
63
64
  const stored = await currentAdapter.getItem(prefixedKey);
64
65
  if (stored !== null && writeVersion === versionBefore) {
65
- setValue(() => JSON.parse(stored) as TValue);
66
+ setValue(() => json.parse<TValue>(stored));
66
67
  }
67
68
  } catch {
68
69
  // Fall back to localStorage on error
69
70
  try {
70
71
  const stored = localStorage.getItem(prefixedKey);
71
72
  if (stored !== null && writeVersion === versionBefore) {
72
- setValue(() => JSON.parse(stored) as TValue);
73
+ setValue(() => json.parse<TValue>(stored));
73
74
  }
74
75
  } catch {
75
76
  // Ignore parse errors
@@ -84,7 +85,7 @@ export function useSyncConfig<TValue>(
84
85
  createEffect(() => {
85
86
  if (!ready()) return; // Don't save until storage has been read
86
87
  const currentValue = value();
87
- const serialized = JSON.stringify(currentValue);
88
+ const serialized = json.stringify(currentValue);
88
89
 
89
90
  // Read adapter untracked to avoid re-running save effect when adapter changes
90
91
  const currentAdapter = untrack(() => syncStorageCtx?.adapter());
@@ -83,6 +83,8 @@ export default {
83
83
  },
84
84
  permissionTable: {
85
85
  permissionItem: "Permission Item",
86
+ use: "Use",
87
+ edit: "Edit",
86
88
  },
87
89
  crudSheet: {
88
90
  save: "Save",
@@ -180,9 +182,6 @@ export default {
180
182
  numpad: {
181
183
  enter: "ENT",
182
184
  },
183
- sidebarMenu: {
184
- menu: "MENU",
185
- },
186
185
  validation: {
187
186
  required: "This is a required field",
188
187
  requiredField: "Required field",
@@ -83,6 +83,8 @@ export default {
83
83
  },
84
84
  permissionTable: {
85
85
  permissionItem: "권한 항목",
86
+ use: "사용",
87
+ edit: "편집",
86
88
  },
87
89
  crudSheet: {
88
90
  save: "저장",
@@ -180,9 +182,6 @@ export default {
180
182
  numpad: {
181
183
  enter: "입력",
182
184
  },
183
- sidebarMenu: {
184
- menu: "메뉴",
185
- },
186
185
  validation: {
187
186
  required: "필수 입력 항목입니다",
188
187
  requiredField: "필수 항목",
@@ -302,7 +302,6 @@ export function SharedDataProvider(props: { children: JSX.Element }): JSX.Elemen
302
302
  const entry = createSharedDataEntry(name, def, client);
303
303
  entries.set(name, entry);
304
304
  accessors[name] = entry;
305
- void entry.initialize();
306
305
  }
307
306
  }
308
307
 
@@ -5,6 +5,7 @@ import { CrudDetailTools } from "../../../../src/components/features/crud-detail
5
5
  import { CrudDetailBefore } from "../../../../src/components/features/crud-detail/CrudDetailBefore";
6
6
  import { CrudDetailAfter } from "../../../../src/components/features/crud-detail/CrudDetailAfter";
7
7
  import { CrudDetail } from "../../../../src/components/features/crud-detail/CrudDetail";
8
+ import { Dialog } from "../../../../src/components/disclosure/Dialog";
8
9
  import { ConfigContext, ConfigProvider } from "../../../../src/providers/ConfigContext";
9
10
  import { NotificationProvider } from "../../../../src/components/feedback/notification/NotificationProvider";
10
11
  import { Topbar } from "../../../../src/components/layout/topbar/Topbar";
@@ -474,3 +475,51 @@ describe("CrudDetail button layout by mode", () => {
474
475
  expect(container.textContent).toContain("커스텀도구");
475
476
  });
476
477
  });
478
+
479
+ describe("CrudDetail dialog mode layout", () => {
480
+ beforeEach(() => {
481
+ localStorage.setItem("test.i18n-locale", JSON.stringify("en"));
482
+ });
483
+
484
+ afterEach(() => {
485
+ localStorage.removeItem("test.i18n-locale");
486
+ });
487
+
488
+ it("bottom bar's direct parent should not have gap-2 class", async () => {
489
+ render(() => (
490
+ <ConfigProvider clientName="test"><I18nProvider>
491
+ <TestWrapper>
492
+ <Dialog open={true}>
493
+ <Dialog.Header>Test</Dialog.Header>
494
+ <CrudDetail<TestData>
495
+ load={() =>
496
+ Promise.resolve({
497
+ data: { id: 1, name: "홍길동" },
498
+ info: { isNew: false, isDeleted: false },
499
+ })
500
+ }
501
+ submit={() => Promise.resolve(true)}
502
+ close={() => {}}
503
+ >
504
+ {(ctx) => <div>{ctx.data.name}</div>}
505
+ </CrudDetail>
506
+ </Dialog>
507
+ </TestWrapper>
508
+ </I18nProvider></ConfigProvider>
509
+ ));
510
+
511
+ await new Promise((r) => setTimeout(r, 100));
512
+
513
+ // Find the bottom bar via Confirm button
514
+ const confirmBtn = Array.from(document.querySelectorAll("button")).find(
515
+ (btn) => btn.textContent.includes("Confirm"),
516
+ );
517
+ expect(confirmBtn).toBeTruthy();
518
+
519
+ const bottomBar = confirmBtn!.closest(".border-t");
520
+ expect(bottomBar).toBeTruthy();
521
+
522
+ // Bottom bar's direct parent should NOT have gap-2 class
523
+ expect(bottomBar!.parentElement!.classList.contains("gap-2")).toBe(false);
524
+ });
525
+ });