@simplysm/solid 13.0.84 → 13.0.85

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 (91) hide show
  1. package/dist/components/data/sheet/DataSheet.d.ts.map +1 -1
  2. package/dist/components/data/sheet/DataSheet.js +6 -9
  3. package/dist/components/data/sheet/DataSheet.js.map +2 -2
  4. package/dist/components/data/sheet/hooks/createDataSheetExpansion.d.ts.map +1 -1
  5. package/dist/components/data/sheet/hooks/createDataSheetExpansion.js +15 -17
  6. package/dist/components/data/sheet/hooks/createDataSheetExpansion.js.map +1 -1
  7. package/dist/components/data/sheet/hooks/createDataSheetReorder.d.ts.map +1 -1
  8. package/dist/components/data/sheet/hooks/createDataSheetReorder.js +12 -12
  9. package/dist/components/data/sheet/hooks/createDataSheetReorder.js.map +1 -1
  10. package/dist/components/data/sheet/hooks/createDataSheetSelection.d.ts.map +1 -1
  11. package/dist/components/data/sheet/hooks/createDataSheetSelection.js +9 -3
  12. package/dist/components/data/sheet/hooks/createDataSheetSelection.js.map +1 -1
  13. package/dist/components/disclosure/Dialog.d.ts.map +1 -1
  14. package/dist/components/disclosure/Dialog.js +3 -21
  15. package/dist/components/disclosure/Dialog.js.map +2 -2
  16. package/dist/components/disclosure/Dropdown.d.ts.map +1 -1
  17. package/dist/components/disclosure/Dropdown.js +1 -11
  18. package/dist/components/disclosure/Dropdown.js.map +2 -2
  19. package/dist/components/disclosure/Tabs.d.ts.map +1 -1
  20. package/dist/components/disclosure/Tabs.js +1 -3
  21. package/dist/components/disclosure/Tabs.js.map +2 -2
  22. package/dist/components/features/crud-detail/CrudDetail.js +103 -102
  23. package/dist/components/features/crud-detail/CrudDetail.js.map +2 -2
  24. package/dist/components/features/crud-sheet/CrudSheet.d.ts.map +1 -1
  25. package/dist/components/features/crud-sheet/CrudSheet.js +3 -5
  26. package/dist/components/features/crud-sheet/CrudSheet.js.map +1 -1
  27. package/dist/components/feedback/busy/BusyContainer.d.ts.map +1 -1
  28. package/dist/components/feedback/busy/BusyContainer.js +1 -6
  29. package/dist/components/feedback/busy/BusyContainer.js.map +2 -2
  30. package/dist/components/form-control/checkbox/SelectableBase.d.ts.map +1 -1
  31. package/dist/components/form-control/checkbox/SelectableBase.js +2 -4
  32. package/dist/components/form-control/checkbox/SelectableBase.js.map +2 -2
  33. package/dist/components/form-control/date-range-picker/DateRangePicker.d.ts.map +1 -1
  34. package/dist/components/form-control/date-range-picker/DateRangePicker.js +1 -2
  35. package/dist/components/form-control/date-range-picker/DateRangePicker.js.map +2 -2
  36. package/dist/components/form-control/editor/RichTextEditor.d.ts.map +1 -1
  37. package/dist/components/form-control/editor/RichTextEditor.js +2 -4
  38. package/dist/components/form-control/editor/RichTextEditor.js.map +2 -2
  39. package/dist/components/form-control/field/NumberInput.d.ts.map +1 -1
  40. package/dist/components/form-control/field/NumberInput.js +7 -7
  41. package/dist/components/form-control/field/NumberInput.js.map +2 -2
  42. package/dist/components/form-control/field/Textarea.d.ts.map +1 -1
  43. package/dist/components/form-control/field/Textarea.js +1 -3
  44. package/dist/components/form-control/field/Textarea.js.map +2 -2
  45. package/dist/components/form-control/select/Select.d.ts +2 -0
  46. package/dist/components/form-control/select/Select.d.ts.map +1 -1
  47. package/dist/components/form-control/select/Select.js +11 -10
  48. package/dist/components/form-control/select/Select.js.map +2 -2
  49. package/dist/components/form-control/state-preset/StatePreset.d.ts.map +1 -1
  50. package/dist/components/form-control/state-preset/StatePreset.js +3 -7
  51. package/dist/components/form-control/state-preset/StatePreset.js.map +2 -2
  52. package/dist/components/layout/topbar/Topbar.js +1 -3
  53. package/dist/components/layout/topbar/Topbar.js.map +2 -2
  54. package/dist/hooks/createControllableStore.d.ts.map +1 -1
  55. package/dist/hooks/createControllableStore.js +8 -5
  56. package/dist/hooks/createControllableStore.js.map +1 -1
  57. package/dist/hooks/useLocalStorage.d.ts.map +1 -1
  58. package/dist/hooks/useLocalStorage.js +3 -2
  59. package/dist/hooks/useLocalStorage.js.map +1 -1
  60. package/dist/hooks/useSyncConfig.d.ts.map +1 -1
  61. package/dist/hooks/useSyncConfig.js +5 -4
  62. package/dist/hooks/useSyncConfig.js.map +1 -1
  63. package/dist/providers/shared-data/SharedDataProvider.d.ts.map +1 -1
  64. package/dist/providers/shared-data/SharedDataProvider.js +0 -1
  65. package/dist/providers/shared-data/SharedDataProvider.js.map +1 -1
  66. package/package.json +5 -5
  67. package/src/components/data/sheet/DataSheet.tsx +6 -10
  68. package/src/components/data/sheet/hooks/createDataSheetExpansion.ts +17 -18
  69. package/src/components/data/sheet/hooks/createDataSheetReorder.ts +12 -13
  70. package/src/components/data/sheet/hooks/createDataSheetSelection.ts +9 -3
  71. package/src/components/disclosure/Dialog.tsx +45 -59
  72. package/src/components/disclosure/Dropdown.tsx +4 -14
  73. package/src/components/disclosure/Tabs.tsx +12 -17
  74. package/src/components/features/crud-detail/CrudDetail.tsx +4 -4
  75. package/src/components/features/crud-sheet/CrudSheet.tsx +4 -5
  76. package/src/components/feedback/busy/BusyContainer.tsx +12 -18
  77. package/src/components/form-control/checkbox/SelectableBase.tsx +10 -16
  78. package/src/components/form-control/date-range-picker/DateRangePicker.tsx +1 -4
  79. package/src/components/form-control/editor/RichTextEditor.tsx +11 -12
  80. package/src/components/form-control/field/NumberInput.tsx +6 -8
  81. package/src/components/form-control/field/Textarea.tsx +11 -10
  82. package/src/components/form-control/select/Select.tsx +23 -9
  83. package/src/components/form-control/state-preset/StatePreset.tsx +15 -22
  84. package/src/components/layout/topbar/Topbar.tsx +2 -2
  85. package/src/hooks/createControllableStore.ts +8 -4
  86. package/src/hooks/useLocalStorage.ts +3 -2
  87. package/src/hooks/useSyncConfig.ts +5 -4
  88. package/src/providers/shared-data/SharedDataProvider.tsx +0 -1
  89. package/tests/components/features/crud-detail/CrudDetail.spec.tsx +49 -0
  90. package/tests/components/form-control/select/SelectItem.spec.tsx +5 -0
  91. package/tests/providers/shared-data/SharedDataProvider.spec.tsx +0 -104
@@ -59,13 +59,6 @@ export interface TextareaProps {
59
59
  style?: JSX.CSSProperties;
60
60
  }
61
61
 
62
- const textareaBaseClass = clsx(
63
- "absolute left-0 top-0",
64
- "size-full",
65
- "resize-none overflow-hidden",
66
- "bg-transparent",
67
- text.placeholder,
68
- );
69
62
 
70
63
  /**
71
64
  * Textarea component
@@ -164,8 +157,6 @@ export const Textarea: Component<TextareaProps> = (props) => {
164
157
  "whitespace-pre-wrap break-all",
165
158
  );
166
159
 
167
- const getTextareaClass = () =>
168
- twMerge(textareaBaseClass, textAreaSizeClasses[local.size ?? "md"], local.inset && "p-0");
169
160
 
170
161
  // Whether editable
171
162
  const isEditable = () => !local.disabled && !local.readOnly;
@@ -228,7 +219,17 @@ export const Textarea: Component<TextareaProps> = (props) => {
228
219
  {contentForHeight()}
229
220
  </div>
230
221
  <textarea
231
- class={getTextareaClass()}
222
+ class={twMerge(
223
+ clsx(
224
+ "absolute left-0 top-0",
225
+ "size-full",
226
+ "resize-none overflow-hidden",
227
+ "bg-transparent",
228
+ text.placeholder,
229
+ ),
230
+ textAreaSizeClasses[local.size ?? "md"],
231
+ local.inset && "p-0",
232
+ )}
232
233
  value={value()}
233
234
  placeholder={local.placeholder}
234
235
  title={local.title}
@@ -32,6 +32,7 @@ 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,
@@ -65,13 +66,15 @@ export interface SelectContextValue<TValue = unknown> {
65
66
 
66
67
  /** Register item template */
67
68
  setItemTemplate: (fn: ((...args: unknown[]) => JSX.Element) | undefined) => void;
69
+
70
+ /** Trigger size */
71
+ size: Accessor<ComponentSize>;
68
72
  }
69
73
 
70
74
  export const SelectContext = createContext<SelectContextValue>();
71
- const SelectCtx = SelectContext;
72
75
 
73
76
  function useSelectContext<TValue = unknown>(): SelectContextValue<TValue> {
74
- const context = useContext(SelectCtx);
77
+ const context = useContext(SelectContext);
75
78
  if (!context) {
76
79
  throw new Error("useSelectContext can only be used inside Select component");
77
80
  }
@@ -90,6 +93,7 @@ const [SelectActionSlot, createActionSlotAccessor] = createSlot<{ children: JSX.
90
93
 
91
94
  const SelectAction = (props: SelectActionProps) => {
92
95
  const [local, rest] = splitProps(props, ["children", "class"]);
96
+ const ctx = useSelectContext();
93
97
 
94
98
  const handleClick: JSX.EventHandlerUnion<HTMLButtonElement, MouseEvent> = (e) => {
95
99
  if (typeof rest.onClick === "function") {
@@ -105,7 +109,15 @@ const SelectAction = (props: SelectActionProps) => {
105
109
  {...rest}
106
110
  type="button"
107
111
  onClick={handleClick}
108
- class={twMerge("p-2", themeTokens.base.hoverBg, local.class)}
112
+ class={twMerge(
113
+ "inline-flex items-center",
114
+ pad[ctx.size()],
115
+ "border",
116
+ border.default,
117
+ groupFocusBorderClass,
118
+ themeTokens.base.hoverBg,
119
+ local.class,
120
+ )}
109
121
  data-select-action
110
122
  >
111
123
  {local.children}
@@ -116,6 +128,8 @@ const SelectAction = (props: SelectActionProps) => {
116
128
 
117
129
  //#endregion
118
130
 
131
+ const groupFocusBorderClass = "group-focus-within:border-primary-400 dark:group-focus-within:border-primary-400";
132
+
119
133
  const selectAllBtnClass = clsx(
120
134
  "text-primary-500",
121
135
  "hover:text-primary-600 dark:hover:text-primary-400",
@@ -186,13 +200,12 @@ const SelectItemInner = <TValue,>(
186
200
  const getClassName = () =>
187
201
  twMerge(
188
202
  listItemBaseClass,
203
+ listItemSizeClasses[context.size()],
189
204
  isSelected() && listItemSelectedClass,
190
205
  local.disabled && listItemDisabledClass,
191
206
  local.class,
192
207
  );
193
208
 
194
- const getCheckIconClass = () => getListItemSelectedIconClass(isSelected());
195
-
196
209
  return (
197
210
  <ItemChildrenProvider>
198
211
  <button
@@ -209,7 +222,7 @@ const SelectItemInner = <TValue,>(
209
222
  onClick={handleClick}
210
223
  >
211
224
  <Show when={context.multiple() && !hasChildren()}>
212
- <Icon icon={IconCheck} class={getCheckIconClass()} />
225
+ <Icon icon={IconCheck} class={getListItemSelectedIconClass(isSelected())} />
213
226
  </Show>
214
227
  <span class={listItemContentClass}>{local.children}</span>
215
228
  </button>
@@ -440,6 +453,7 @@ const SelectInnerComponent = <TValue,>(props: SelectProps<TValue>) => {
440
453
  toggleValue,
441
454
  closeDropdown,
442
455
  setItemTemplate,
456
+ size: () => local.size ?? "md",
443
457
  };
444
458
 
445
459
  // Trigger keyboard handling (only Enter/Space, ArrowUp/Down handled by Dropdown)
@@ -624,7 +638,7 @@ const SelectInnerComponent = <TValue,>(props: SelectProps<TValue>) => {
624
638
  action() !== undefined &&
625
639
  clsx(
626
640
  "rounded-r-none border-r-0",
627
- "group-focus-within:border-primary-400 dark:group-focus-within:border-primary-400",
641
+ groupFocusBorderClass,
628
642
  ),
629
643
  )}
630
644
  style={local.style}
@@ -698,13 +712,13 @@ const SelectInnerComponent = <TValue,>(props: SelectProps<TValue>) => {
698
712
 
699
713
  return (
700
714
  <Invalid message={errorMsg()} variant="border" lazyValidation={local.lazyValidation}>
701
- <SelectCtx.Provider value={contextValue as SelectContextValue}>
715
+ <SelectContext.Provider value={contextValue as SelectContextValue}>
702
716
  <HeaderProvider>
703
717
  <ActionProvider>
704
718
  <SelectInnerRender>{local.children}</SelectInnerRender>
705
719
  </ActionProvider>
706
720
  </HeaderProvider>
707
- </SelectCtx.Provider>
721
+ </SelectContext.Provider>
708
722
  </Invalid>
709
723
  );
710
724
  };
@@ -191,33 +191,18 @@ function StatePresetInner<TValue>(props: StatePresetProps<TValue>): JSX.Element
191
191
 
192
192
  // ── Render ──
193
193
 
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
194
  const resolvedIconBtnClass = () =>
202
195
  twMerge("rounded-full", iconBtnSizeClasses[local.size ?? "md"]);
203
196
 
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
- );
214
-
215
197
  return (
216
- <div class={containerClass()} style={local.style}>
198
+ <div class={twMerge(clsx("inline-flex items-center", gap.lg, "flex-wrap"), local.class)} style={local.style}>
217
199
  {/* Star button - add preset */}
218
200
  <button
219
201
  type="button"
220
- class={resolvedStarBtnClass()}
202
+ class={twMerge(
203
+ clsx("inline-flex cursor-pointer items-center justify-center rounded-full text-warning-500 transition-colors focus:outline-none", themeTokens.warning.hoverBg),
204
+ starBtnSizeClasses[local.size ?? "md"],
205
+ )}
221
206
  onClick={handleStartAdd}
222
207
  title={i18n.t("statePreset.addPreset")}
223
208
  >
@@ -227,7 +212,12 @@ function StatePresetInner<TValue>(props: StatePresetProps<TValue>): JSX.Element
227
212
  {/* Preset chips */}
228
213
  <For each={presets()}>
229
214
  {(preset, index) => (
230
- <span class={resolvedChipClass()}>
215
+ <span
216
+ class={twMerge(
217
+ clsx("inline-flex items-center", gap.md, "rounded-full", bg.subtle, text.default, "cursor-default"),
218
+ chipSizeClasses[local.size ?? "md"],
219
+ )}
220
+ >
231
221
  <button
232
222
  type="button"
233
223
  class="cursor-pointer hover:underline focus:outline-none"
@@ -266,7 +256,10 @@ function StatePresetInner<TValue>(props: StatePresetProps<TValue>): JSX.Element
266
256
  requestAnimationFrame(() => el.focus());
267
257
  }}
268
258
  type="text"
269
- class={resolvedInputClass()}
259
+ class={twMerge(
260
+ clsx("rounded-full", bg.subtle, text.default, "border border-transparent focus:outline-none focus:ring-1 focus:ring-primary-400", text.placeholder),
261
+ inputSizeClasses[local.size ?? "md"],
262
+ )}
270
263
  placeholder={i18n.t("statePreset.namePlaceholder")}
271
264
  autocomplete="one-time-code"
272
265
  value={inputValue()}
@@ -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());
@@ -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
+ });
@@ -23,6 +23,7 @@ describe("SelectItem component", () => {
23
23
  toggleValue,
24
24
  closeDropdown: vi.fn(),
25
25
  setItemTemplate: vi.fn(),
26
+ size: () => "md" as const,
26
27
  };
27
28
 
28
29
  const { getByText } = render(() => (
@@ -43,6 +44,7 @@ describe("SelectItem component", () => {
43
44
  toggleValue: vi.fn(),
44
45
  closeDropdown,
45
46
  setItemTemplate: vi.fn(),
47
+ size: () => "md" as const,
46
48
  };
47
49
 
48
50
  const { getByText } = render(() => (
@@ -63,6 +65,7 @@ describe("SelectItem component", () => {
63
65
  toggleValue: vi.fn(),
64
66
  closeDropdown,
65
67
  setItemTemplate: vi.fn(),
68
+ size: () => "md" as const,
66
69
  };
67
70
 
68
71
  const { getByText } = render(() => (
@@ -84,6 +87,7 @@ describe("SelectItem component", () => {
84
87
  toggleValue: vi.fn(),
85
88
  closeDropdown: vi.fn(),
86
89
  setItemTemplate: vi.fn(),
90
+ size: () => "md" as const,
87
91
  };
88
92
 
89
93
  render(() => (
@@ -106,6 +110,7 @@ describe("SelectItem component", () => {
106
110
  toggleValue,
107
111
  closeDropdown: vi.fn(),
108
112
  setItemTemplate: vi.fn(),
113
+ size: () => "md" as const,
109
114
  };
110
115
 
111
116
  const { getByText } = render(() => (
@@ -418,108 +418,4 @@ describe("SharedDataProvider", () => {
418
418
  result.unmount();
419
419
  });
420
420
 
421
- it("fetches eagerly after configure()", async () => {
422
- const { serviceClientValue, mockClient } = createMockServiceClient();
423
- const mockUsers: TestUser[] = [{ id: 1, name: "Alice" }];
424
-
425
- const fetchFn = vi.fn(() => Promise.resolve(mockUsers));
426
-
427
- const definitions: { user: SharedDataDefinition<TestUser> } = {
428
- user: {
429
- fetch: fetchFn,
430
- getKey: (item) => item.id,
431
- orderBy: [[(item) => item.name, "asc"]],
432
- },
433
- };
434
-
435
- function ConfigureOnly() {
436
- const shared = useTestSharedData();
437
- shared.configure(() => definitions);
438
- return <div data-testid="configured">configured</div>;
439
- }
440
-
441
- const result = render(() => (
442
- <NotificationContext.Provider value={createMockNotification()}>
443
- <ServiceClientContext.Provider value={serviceClientValue}>
444
- <SharedDataProvider>
445
- <ConfigureOnly />
446
- </SharedDataProvider>
447
- </ServiceClientContext.Provider>
448
- </NotificationContext.Provider>
449
- ));
450
-
451
- await vi.waitFor(() => {
452
- expect(result.getByTestId("configured").textContent).toBe("configured");
453
- });
454
-
455
- // Now configure() triggers eager init
456
- await vi.waitFor(() => {
457
- expect(fetchFn).toHaveBeenCalledTimes(1);
458
- expect(mockClient.addListener).toHaveBeenCalledTimes(1);
459
- });
460
-
461
- result.unmount();
462
- });
463
-
464
- it("wait() resolves after data is loaded even without items() access", async () => {
465
- const { serviceClientValue } = createMockServiceClient();
466
-
467
- let resolveUsers!: (value: TestUser[]) => void;
468
- const fetchPromise = new Promise<TestUser[]>((resolve) => {
469
- resolveUsers = resolve;
470
- });
471
-
472
- const fetchFn = vi.fn(() => fetchPromise);
473
-
474
- const definitions: { user: SharedDataDefinition<TestUser> } = {
475
- user: {
476
- fetch: fetchFn,
477
- getKey: (item) => item.id,
478
- orderBy: [[(item) => item.name, "asc"]],
479
- },
480
- };
481
-
482
- let waitResolved = false;
483
-
484
- // Component that calls configure() + wait() but never accesses items()
485
- function ConfigureAndWait() {
486
- const shared = useTestSharedData();
487
- shared.configure(() => definitions);
488
-
489
- // Call wait() immediately — should NOT resolve until fetch completes
490
- void shared.wait().then(() => {
491
- waitResolved = true;
492
- });
493
-
494
- return <div data-testid="ready">{String(waitResolved)}</div>;
495
- }
496
-
497
- const result = render(() => (
498
- <NotificationContext.Provider value={createMockNotification()}>
499
- <ServiceClientContext.Provider value={serviceClientValue}>
500
- <SharedDataProvider>
501
- <ConfigureAndWait />
502
- </SharedDataProvider>
503
- </ServiceClientContext.Provider>
504
- </NotificationContext.Provider>
505
- ));
506
-
507
- // fetch should have been called (eager init)
508
- await vi.waitFor(() => {
509
- expect(fetchFn).toHaveBeenCalledTimes(1);
510
- });
511
-
512
- // wait() should NOT have resolved yet (fetch still pending)
513
- expect(waitResolved).toBe(false);
514
-
515
- // Resolve the fetch
516
- resolveUsers([{ id: 1, name: "Alice" }]);
517
-
518
- // wait() should now resolve
519
- await vi.waitFor(() => {
520
- expect(waitResolved).toBe(true);
521
- });
522
-
523
- result.unmount();
524
- });
525
421
  });