@simplysm/solid 13.0.58 → 13.0.60

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 (79) hide show
  1. package/README.md +1 -1
  2. package/dist/components/data/crud-sheet/CrudSheet.d.ts.map +1 -1
  3. package/dist/components/data/crud-sheet/CrudSheet.js +8 -11
  4. package/dist/components/data/crud-sheet/CrudSheet.js.map +2 -2
  5. package/dist/components/data/crud-sheet/types.d.ts +1 -1
  6. package/dist/components/data/crud-sheet/types.d.ts.map +1 -1
  7. package/dist/components/data/kanban/Kanban.d.ts.map +1 -1
  8. package/dist/components/data/kanban/Kanban.js +3 -4
  9. package/dist/components/data/kanban/Kanban.js.map +2 -2
  10. package/dist/components/data/kanban/KanbanContext.d.ts +2 -3
  11. package/dist/components/data/kanban/KanbanContext.d.ts.map +1 -1
  12. package/dist/components/data/kanban/KanbanContext.js.map +1 -1
  13. package/dist/components/data/list/ListItem.d.ts.map +1 -1
  14. package/dist/components/data/list/ListItem.js +3 -3
  15. package/dist/components/data/list/ListItem.js.map +2 -2
  16. package/dist/components/disclosure/Dialog.d.ts.map +1 -1
  17. package/dist/components/disclosure/Dialog.js +3 -4
  18. package/dist/components/disclosure/Dialog.js.map +2 -2
  19. package/dist/components/disclosure/Dropdown.d.ts.map +1 -1
  20. package/dist/components/disclosure/Dropdown.js +7 -15
  21. package/dist/components/disclosure/Dropdown.js.map +2 -2
  22. package/dist/components/form-control/color-picker/ColorPicker.d.ts +5 -3
  23. package/dist/components/form-control/color-picker/ColorPicker.d.ts.map +1 -1
  24. package/dist/components/form-control/color-picker/ColorPicker.js +11 -6
  25. package/dist/components/form-control/color-picker/ColorPicker.js.map +2 -2
  26. package/dist/components/form-control/field/NumberInput.d.ts.map +1 -1
  27. package/dist/components/form-control/field/NumberInput.js +2 -2
  28. package/dist/components/form-control/field/NumberInput.js.map +2 -2
  29. package/dist/components/form-control/field/TextInput.d.ts.map +1 -1
  30. package/dist/components/form-control/field/TextInput.js +3 -3
  31. package/dist/components/form-control/field/TextInput.js.map +2 -2
  32. package/dist/components/form-control/select/Select.d.ts.map +1 -1
  33. package/dist/components/form-control/select/Select.js +3 -4
  34. package/dist/components/form-control/select/Select.js.map +2 -2
  35. package/dist/components/form-control/select/SelectContext.d.ts +1 -2
  36. package/dist/components/form-control/select/SelectContext.d.ts.map +1 -1
  37. package/dist/components/form-control/select/SelectContext.js.map +1 -1
  38. package/dist/components/form-control/select/SelectItem.d.ts.map +1 -1
  39. package/dist/components/form-control/select/SelectItem.js +3 -3
  40. package/dist/components/form-control/select/SelectItem.js.map +2 -2
  41. package/dist/helpers/createAppStructure.d.ts +7 -4
  42. package/dist/helpers/createAppStructure.d.ts.map +1 -1
  43. package/dist/helpers/createAppStructure.js +20 -2
  44. package/dist/helpers/createAppStructure.js.map +1 -1
  45. package/dist/hooks/createPointerDrag.d.ts +1 -1
  46. package/dist/hooks/createPointerDrag.d.ts.map +1 -1
  47. package/dist/hooks/createPointerDrag.js +6 -4
  48. package/dist/hooks/createPointerDrag.js.map +1 -1
  49. package/dist/hooks/createSlotSignal.d.ts +9 -0
  50. package/dist/hooks/createSlotSignal.d.ts.map +1 -0
  51. package/dist/hooks/createSlotSignal.js +10 -0
  52. package/dist/hooks/createSlotSignal.js.map +6 -0
  53. package/dist/index.d.ts +15 -17
  54. package/dist/index.d.ts.map +1 -1
  55. package/dist/index.js +15 -39
  56. package/dist/index.js.map +1 -1
  57. package/docs/data-components.md +18 -8
  58. package/docs/feedback.md +8 -2
  59. package/docs/helpers.md +24 -0
  60. package/docs/hooks.md +166 -83
  61. package/docs/styling.md +1 -0
  62. package/package.json +3 -3
  63. package/src/components/data/crud-sheet/CrudSheet.tsx +8 -11
  64. package/src/components/data/crud-sheet/types.ts +1 -1
  65. package/src/components/data/kanban/Kanban.tsx +3 -5
  66. package/src/components/data/kanban/KanbanContext.ts +2 -3
  67. package/src/components/data/list/ListItem.tsx +2 -5
  68. package/src/components/disclosure/Dialog.tsx +3 -6
  69. package/src/components/disclosure/Dropdown.tsx +7 -20
  70. package/src/components/form-control/color-picker/ColorPicker.tsx +19 -9
  71. package/src/components/form-control/field/NumberInput.tsx +2 -4
  72. package/src/components/form-control/field/TextInput.tsx +2 -5
  73. package/src/components/form-control/select/Select.tsx +3 -6
  74. package/src/components/form-control/select/SelectContext.ts +1 -2
  75. package/src/components/form-control/select/SelectItem.tsx +2 -5
  76. package/src/helpers/createAppStructure.ts +36 -6
  77. package/src/hooks/createPointerDrag.ts +8 -5
  78. package/src/hooks/createSlotSignal.ts +14 -0
  79. package/src/index.ts +15 -41
package/docs/hooks.md CHANGED
@@ -30,13 +30,22 @@ Local-only persistent storage hook. Always uses `localStorage` regardless of `sy
30
30
  ```tsx
31
31
  import { useLocalStorage } from "@simplysm/solid";
32
32
 
33
- const [token, setToken] = useLocalStorage<string | undefined>("auth-token", undefined);
33
+ const [token, setToken] = useLocalStorage<string>("auth-token");
34
+
35
+ // Set value
36
+ setToken("abc123");
37
+
38
+ // Remove value
39
+ setToken(undefined);
40
+
41
+ // Functional update
42
+ setToken((prev) => prev + "-refreshed");
34
43
  ```
35
44
 
36
45
  | Return value | Type | Description |
37
46
  |--------------|------|-------------|
38
- | `[0]` | `Accessor<T>` | Value getter |
39
- | `[1]` | `Setter<T>` | Value setter |
47
+ | `[0]` | `Accessor<T \| undefined>` | Value getter |
48
+ | `[1]` | `StorageSetter<T>` | Value setter (accepts value, `undefined` to remove, or updater function) |
40
49
 
41
50
  ---
42
51
 
@@ -66,21 +75,24 @@ const [theme, setTheme, ready] = useSyncConfig("theme", "light");
66
75
  Logging hook. If `LoggerProvider` is present, logs are sent to the adapter only. Otherwise, logs fall back to `consola`.
67
76
 
68
77
  ```tsx
69
- import { useLogger } from "@simplysm/solid";
78
+ import { useLogger, type Logger } from "@simplysm/solid";
70
79
 
71
- const logger = useLogger();
80
+ const logger: Logger = useLogger();
72
81
  logger.log("user action", { userId: 123 });
73
82
  logger.info("app started");
74
83
  logger.error("something failed", errorObj);
75
84
  logger.warn("deprecation notice");
76
85
  ```
77
86
 
87
+ **Logger interface:**
88
+
78
89
  | Method | Signature | Description |
79
90
  |--------|-----------|-------------|
80
91
  | `log` | `(...args: unknown[]) => void` | Log message (general) |
81
92
  | `info` | `(...args: unknown[]) => void` | Log message (informational) |
82
93
  | `warn` | `(...args: unknown[]) => void` | Log message (warning) |
83
94
  | `error` | `(...args: unknown[]) => void` | Log message (error) |
95
+ | `configure` | `(fn: (origin: LogAdapter) => LogAdapter) => void` | Set or wrap the log adapter (inside `LoggerProvider` only) |
84
96
 
85
97
  **Configuring a custom adapter (decorator pattern):**
86
98
 
@@ -222,13 +234,44 @@ const { mounted, animating, unmount } = createMountTransition(() => open());
222
234
 
223
235
  ## createIMEHandler
224
236
 
225
- Hook that delays `onValueChange` calls during IME (Korean, etc.) composition to prevent interrupted input.
237
+ Hook that delays `setValue` calls during IME (Korean, CJK, etc.) composition to prevent interrupted input. Use inside contenteditable or custom input components.
238
+
239
+ ```tsx
240
+ import { createIMEHandler } from "@simplysm/solid";
241
+
242
+ const {
243
+ composingValue,
244
+ handleCompositionStart,
245
+ handleInput,
246
+ handleCompositionEnd,
247
+ flushComposition,
248
+ } = createIMEHandler((value) => {
249
+ // called only when composition is complete
250
+ setMyValue(value);
251
+ });
252
+
253
+ // Wire up to DOM element events:
254
+ <div
255
+ contentEditable
256
+ onCompositionStart={handleCompositionStart}
257
+ onCompositionEnd={(e) => handleCompositionEnd(e.currentTarget.textContent ?? "")}
258
+ onInput={(e) => handleInput(e.currentTarget.textContent ?? "", e.isComposing)}
259
+ />
260
+ ```
261
+
262
+ | Return value | Type | Description |
263
+ |--------------|------|-------------|
264
+ | `composingValue` | `Accessor<string \| null>` | Current composing value (for display); `null` when not composing |
265
+ | `handleCompositionStart` | `() => void` | Call on `compositionstart` event |
266
+ | `handleInput` | `(value: string, isComposing: boolean) => void` | Call on `input` event |
267
+ | `handleCompositionEnd` | `(value: string) => void` | Call on `compositionend` event |
268
+ | `flushComposition` | `() => void` | Immediately commit any pending composition |
226
269
 
227
270
  ---
228
271
 
229
272
  ## useRouterLink
230
273
 
231
- `@solidjs/router`-based navigation hook. Automatically handles Ctrl/Alt + click (new tab), Shift + click (new window).
274
+ `@solidjs/router`-based navigation hook. Automatically handles Ctrl/Alt + click (new tab), Shift + click (new window), and regular click (SPA routing).
232
275
 
233
276
  ```tsx
234
277
  import { useRouterLink } from "@simplysm/solid";
@@ -239,112 +282,146 @@ const navigate = useRouterLink();
239
282
  Dashboard
240
283
  </List.Item>
241
284
 
242
- // Pass state
285
+ // Pass state (not visible in URL)
243
286
  <List.Item onClick={navigate({ href: "/users/123", state: { from: "list" } })}>
244
287
  User
245
288
  </List.Item>
289
+
290
+ // Custom new window size on Shift+click
291
+ <List.Item onClick={navigate({ href: "/reports/pdf", window: { width: 1200, height: 900 } })}>
292
+ Report
293
+ </List.Item>
246
294
  ```
247
295
 
296
+ **Options:**
297
+
298
+ | Option | Type | Default | Description |
299
+ |--------|------|---------|-------------|
300
+ | `href` | `string` | (required) | Navigation path (fully-formed URL, e.g., `"/home/dashboard?tab=1"`) |
301
+ | `state` | `Record<string, unknown>` | - | State to pass to the route (not exposed in URL) |
302
+ | `window.width` | `number` | `800` | New window width (Shift+click) |
303
+ | `window.height` | `number` | `800` | New window height (Shift+click) |
304
+
248
305
  ---
249
306
 
250
307
  ## createAppStructure
251
308
 
252
- Utility for declaratively defining app structure (routing, menus, permissions). Takes a single options object.
309
+ Utility for declaratively defining app structure (routing, menus, permissions). Takes a factory function and returns `{ AppStructureProvider, useAppStructure }` for Context-based access with full `InferPerms` type preservation.
253
310
 
254
311
  ```tsx
255
- import { createAppStructure, type AppStructureItem } from "@simplysm/solid";
312
+ // appStructure.ts
313
+ import { createAppStructure } from "@simplysm/solid";
256
314
  import { IconHome, IconUsers } from "@tabler/icons-solidjs";
257
-
258
- const items: AppStructureItem<string>[] = [
259
- {
260
- code: "home",
261
- title: "Home",
262
- icon: IconHome,
263
- component: HomePage,
264
- perms: ["use"],
265
- },
266
- {
267
- code: "admin",
268
- title: "Admin",
269
- icon: IconUsers,
270
- children: [
271
- { code: "users", title: "User Management", component: UsersPage, perms: ["use", "edit"] },
272
- { code: "roles", title: "Role Management", component: RolesPage, perms: ["use"], isNotMenu: true },
315
+ import { useAuth } from "./AuthProvider";
316
+
317
+ export const { AppStructureProvider, useAppStructure } = createAppStructure(() => {
318
+ const auth = useAuth();
319
+ return {
320
+ items: [
321
+ {
322
+ code: "home",
323
+ title: "Home",
324
+ icon: IconHome,
325
+ component: HomePage,
326
+ perms: ["use"],
327
+ },
328
+ {
329
+ code: "admin",
330
+ title: "Admin",
331
+ icon: IconUsers,
332
+ children: [
333
+ { code: "users", title: "User Management", component: UsersPage, perms: ["use", "edit"] },
334
+ { code: "roles", title: "Role Management", component: RolesPage, perms: ["use"], isNotMenu: true },
335
+ ],
336
+ },
273
337
  ],
274
- },
275
- ];
276
-
277
- const structure = createAppStructure({
278
- items,
279
- usableModules: () => activeModules(), // optional: filter by active modules
280
- permRecord: () => userPermissions(), // optional: Record<string, boolean> permission state
338
+ usableModules: () => activeModules(), // optional: filter by active modules
339
+ permRecord: () => auth.authInfo()?.permissions, // optional: Record<string, boolean>
340
+ };
281
341
  });
282
-
283
- // structure.allRoutes -- AppRoute[] - all routes with permCode + module info (static)
284
- // structure.usableMenus() -- Accessor<AppMenu[]> - filtered menu array for Sidebar.Menu
285
- // structure.usableFlatMenus() -- Accessor<AppFlatMenu[]> - flat filtered menu list
286
- // structure.usablePerms() -- Accessor<AppPerm[]> - filtered permission tree
287
- // structure.allFlatPerms -- AppFlatPerm[] - all flat perm entries (static)
288
- // structure.checkRouteAccess(r) -- boolean - check if route is accessible
289
342
  ```
290
343
 
291
- **Routing integration with `@solidjs/router`:**
344
+ The factory function (`getOpts`) is called inside `AppStructureProvider` at render time, so context hooks like `useAuth()` can be used. The `const TItems` generic preserves item literal types for full `InferPerms` type inference.
345
+
346
+ **Provider setup:**
292
347
 
293
348
  ```tsx
294
- import { render } from "solid-js/web";
295
- import { HashRouter, Navigate, Route } from "@solidjs/router";
296
- import { appStructure } from "./appStructure";
349
+ // main.tsx or App.tsx
350
+ import { AppStructureProvider } from "./appStructure";
297
351
 
298
352
  render(
299
353
  () => (
300
- <HashRouter>
301
- <Route path="/" component={App}>
302
- <Route path="/home" component={Home}>
303
- <Route path="/" component={() => <Navigate href="/home/main" />} />
304
- {appStructure.allRoutes.map((r) => (
305
- <Route
306
- path={r.path}
307
- component={() => {
308
- if (!appStructure.checkRouteAccess(r)) {
309
- return <Navigate href="/login" />;
310
- }
311
- return <r.component />;
312
- }}
313
- />
314
- ))}
315
- <Route path="/*" component={NotFoundPage} />
316
- </Route>
317
- <Route path="/" component={() => <Navigate href="/home" />} />
318
- </Route>
319
- </HashRouter>
354
+ <AppStructureProvider>
355
+ <HashRouter>
356
+ {/* ... */}
357
+ </HashRouter>
358
+ </AppStructureProvider>
320
359
  ),
321
360
  document.getElementById("root")!,
322
361
  );
323
362
  ```
324
363
 
325
- `allRoutes` is a static (non-reactive) array containing all routes with `permCode` and module chain information. Use `checkRouteAccess(route)` to reactively verify access based on `permRecord` and `usableModules` signals.
364
+ **Using the hook in components:**
365
+
366
+ ```tsx
367
+ import { useAppStructure } from "./appStructure";
368
+
369
+ function Home(props: RouteSectionProps) {
370
+ const appStructure = useAppStructure();
371
+
372
+ // appStructure.usableRoutes() -- Accessor<AppRoute[]>
373
+ // appStructure.usableMenus() -- Accessor<AppMenu[]>
374
+ // appStructure.usableFlatMenus() -- Accessor<AppFlatMenu[]>
375
+ // appStructure.usablePerms() -- Accessor<AppPerm[]>
376
+ // appStructure.allFlatPerms -- AppFlatPerm[]
377
+ // appStructure.getTitleChainByHref(href) -- string[]
378
+ // appStructure.perms -- typed permission accessor
379
+
380
+ return <Sidebar.Menu menus={appStructure.usableMenus()} />;
381
+ }
382
+ ```
383
+
384
+ **Routing integration with `@solidjs/router`:**
385
+
386
+ ```tsx
387
+ import { useAppStructure } from "./appStructure";
388
+
389
+ function HomeRoutes() {
390
+ const appStructure = useAppStructure();
391
+ return (
392
+ <For each={appStructure.usableRoutes()}>
393
+ {(r) => <Route path={r.path} component={r.component} />}
394
+ </For>
395
+ );
396
+ }
397
+
398
+ // In your router setup (inside AppStructureProvider):
399
+ <Route path="/home" component={Home}>
400
+ <Route path="/" component={() => <Navigate href="/home/main" />} />
401
+ <HomeRoutes />
402
+ <Route path="/*" component={NotFoundPage} />
403
+ </Route>
404
+ ```
405
+
406
+ `usableRoutes()` is a reactive accessor returning routes filtered by `usableModules` and `permRecord`. Items with `perms: ["use"]` are excluded from routes when the user lacks the `use` permission.
326
407
 
327
408
  **AppStructure return type:**
328
409
 
329
410
  ```typescript
330
411
  interface AppStructure<TModule> {
331
412
  items: AppStructureItem<TModule>[];
332
- allRoutes: AppRoute<TModule>[]; // static, all routes with perm/module info
413
+ usableRoutes: Accessor<AppRoute[]>; // reactive, filtered by modules + permRecord
333
414
  usableMenus: Accessor<AppMenu[]>; // reactive, filtered by modules + permRecord
334
415
  usableFlatMenus: Accessor<AppFlatMenu[]>; // reactive, flat version of usableMenus
335
416
  usablePerms: Accessor<AppPerm<TModule>[]>; // reactive, filtered permission tree
336
- allFlatPerms: AppFlatPerm<TModule>[]; // static, all perm entries (not reactive)
337
- checkRouteAccess(route: AppRoute<TModule>): boolean; // reactive access check
417
+ allFlatPerms: AppFlatPerm<TModule>[]; // static, all perm entries (not reactive)
338
418
  getTitleChainByHref(href: string): string[];
339
419
  perms: InferPerms<TItems>; // typed permission accessor (getter-based reactive booleans)
340
420
  }
341
421
 
342
- interface AppRoute<TModule = string> {
422
+ interface AppRoute {
343
423
  path: string;
344
424
  component: Component;
345
- permCode?: string;
346
- modulesChain: TModule[][];
347
- requiredModulesChain: TModule[][];
348
425
  }
349
426
 
350
427
  interface AppMenu {
@@ -418,18 +495,18 @@ type AppStructureItem<TModule> = AppStructureGroupItem<TModule> | AppStructureLe
418
495
  Retrieves the breadcrumb title chain for a given href path. Works on raw items (including `isNotMenu` items).
419
496
 
420
497
  ```tsx
421
- import { createAppStructure } from "@simplysm/solid";
422
-
423
- const appStructure = createAppStructure({ items });
498
+ import { useLocation } from "@solidjs/router";
499
+ import { useAppStructure } from "./appStructure";
424
500
 
425
- // Returns ["Sales", "Invoice"] for /home/sales/invoice
426
- const titles = appStructure.getTitleChainByHref("/home/sales/invoice");
501
+ function Breadcrumb() {
502
+ const appStructure = useAppStructure();
503
+ const location = useLocation();
427
504
 
428
- // Use with router for dynamic breadcrumb
429
- import { useLocation } from "@solidjs/router";
505
+ // Returns ["Sales", "Invoice"] for /home/sales/invoice
506
+ const breadcrumb = () => appStructure.getTitleChainByHref(location.pathname);
430
507
 
431
- const location = useLocation();
432
- const breadcrumb = () => appStructure.getTitleChainByHref(location.pathname);
508
+ return <span>{breadcrumb().join(" > ")}</span>;
509
+ }
433
510
  ```
434
511
 
435
512
  #### perms
@@ -439,7 +516,10 @@ Typed permission accessor providing dot-notation access with TypeScript autocomp
439
516
  **Important:** For type inference to work, pass items inline to `createAppStructure`. Declaring items as a separate variable with explicit `AppStructureItem[]` type annotation widens literal types, losing autocompletion.
440
517
 
441
518
  ```typescript
442
- const appStructure = createAppStructure({
519
+ // appStructure.ts
520
+ import { createAppStructure } from "@simplysm/solid";
521
+
522
+ export const { AppStructureProvider, useAppStructure } = createAppStructure(() => ({
443
523
  items: [
444
524
  {
445
525
  code: "home",
@@ -457,7 +537,10 @@ const appStructure = createAppStructure({
457
537
  },
458
538
  ],
459
539
  permRecord: () => userPermissions(),
460
- });
540
+ }));
541
+
542
+ // In a component (inside AppStructureProvider):
543
+ const appStructure = useAppStructure();
461
544
 
462
545
  // Typed access with autocompletion:
463
546
  appStructure.perms.home.user.use // boolean (reactive)
package/docs/styling.md CHANGED
@@ -40,6 +40,7 @@
40
40
  | `z-dropdown` | 1000 | Dropdown popup |
41
41
  | `z-modal-backdrop` | 1999 | Modal backdrop |
42
42
  | `z-modal` | 2000 | Modal dialog |
43
+ | `z-notification` | 3000 | Notification banner |
43
44
 
44
45
  ## Dark Mode
45
46
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simplysm/solid",
3
- "version": "13.0.58",
3
+ "version": "13.0.60",
4
4
  "description": "심플리즘 패키지 - SolidJS 라이브러리",
5
5
  "author": "김석래",
6
6
  "license": "Apache-2.0",
@@ -50,8 +50,8 @@
50
50
  "solid-tiptap": "^0.8.0",
51
51
  "tailwind-merge": "^3.5.0",
52
52
  "tailwindcss": "^3.4.19",
53
- "@simplysm/core-browser": "13.0.58",
54
- "@simplysm/core-common": "13.0.58"
53
+ "@simplysm/core-browser": "13.0.60",
54
+ "@simplysm/core-common": "13.0.60"
55
55
  },
56
56
  "devDependencies": {
57
57
  "@solidjs/testing-library": "^0.8.10"
@@ -168,8 +168,8 @@ const CrudSheetBase = <TItem, TFilter extends Record<string, any>>(
168
168
  setLastFilter(() => objClone(filter));
169
169
  }
170
170
 
171
- function handleRefresh() {
172
- setLastFilter(() => ({ ...lastFilter() }));
171
+ async function handleRefresh() {
172
+ await doRefresh();
173
173
  }
174
174
 
175
175
  // -- Inline Edit --
@@ -309,14 +309,14 @@ const CrudSheetBase = <TItem, TFilter extends Record<string, any>>(
309
309
  }
310
310
 
311
311
  // -- Keyboard Shortcuts --
312
- createEventListener(document, "keydown", (e: KeyboardEvent) => {
312
+ createEventListener(document, "keydown", async (e: KeyboardEvent) => {
313
313
  if (e.ctrlKey && e.key === "s" && !isSelectMode()) {
314
314
  e.preventDefault();
315
315
  formRef?.requestSubmit();
316
316
  }
317
317
  if (e.ctrlKey && e.altKey && e.key === "l") {
318
318
  e.preventDefault();
319
- handleRefresh();
319
+ await doRefresh();
320
320
  }
321
321
  });
322
322
 
@@ -356,8 +356,7 @@ const CrudSheetBase = <TItem, TFilter extends Record<string, any>>(
356
356
  },
357
357
  save: handleSave,
358
358
  refresh: async () => {
359
- handleRefresh();
360
- await Promise.resolve();
359
+ await doRefresh();
361
360
  },
362
361
  addItem: handleAddRow,
363
362
  setPage,
@@ -501,9 +500,7 @@ const CrudSheetBase = <TItem, TFilter extends Record<string, any>>(
501
500
  onSortsChange={setSorts}
502
501
  selectMode={
503
502
  isSelectMode()
504
- ? local.selectMode === "multi"
505
- ? "multiple"
506
- : "single"
503
+ ? local.selectMode
507
504
  : local.modalEdit?.deleteItems != null
508
505
  ? "multiple"
509
506
  : undefined
@@ -605,10 +602,10 @@ const CrudSheetBase = <TItem, TFilter extends Record<string, any>>(
605
602
  <div class="flex-1" />
606
603
  <Show when={selectedItems().length > 0}>
607
604
  <Button size="sm" theme="danger" onClick={handleSelectCancel}>
608
- {local.selectMode === "multi" ? "모두" : "선택"} 해제
605
+ {local.selectMode === "multiple" ? "모두" : "선택"} 해제
609
606
  </Button>
610
607
  </Show>
611
- <Show when={local.selectMode === "multi"}>
608
+ <Show when={local.selectMode === "multiple"}>
612
609
  <Button size="sm" theme="primary" onClick={handleSelectConfirm}>
613
610
  확인({selectedItems().length})
614
611
  </Button>
@@ -85,7 +85,7 @@ interface CrudSheetBaseProps<TItem, TFilter extends Record<string, any>> {
85
85
  items?: TItem[];
86
86
  onItemsChange?: (items: TItem[]) => void;
87
87
  excel?: ExcelConfig<TItem>;
88
- selectMode?: "single" | "multi";
88
+ selectMode?: "single" | "multiple";
89
89
  onSelect?: (result: SelectResult<TItem>) => void;
90
90
  hideAutoTools?: boolean;
91
91
  class?: string;
@@ -18,6 +18,7 @@ import { Checkbox } from "../../form-control/checkbox/Checkbox";
18
18
  import { Icon } from "../../display/Icon";
19
19
  import { BusyContainer } from "../../feedback/busy/BusyContainer";
20
20
  import { createControllableSignal } from "../../../hooks/createControllableSignal";
21
+ import { createSlotSignal } from "../../../hooks/createSlotSignal";
21
22
  import "./Kanban.css";
22
23
  import { iconButtonBase } from "../../../styles/patterns.styles";
23
24
  import {
@@ -386,11 +387,8 @@ const KanbanLane: ParentComponent<KanbanLaneProps> = (props) => {
386
387
  };
387
388
 
388
389
  // Slot signals
389
- type SlotAccessor = (() => JSX.Element) | undefined;
390
- const [title, _setTitle] = createSignal<SlotAccessor>();
391
- const [tools, _setTools] = createSignal<SlotAccessor>();
392
- const setTitle = (content: SlotAccessor) => _setTitle(() => content);
393
- const setTools = (content: SlotAccessor) => _setTools(() => content);
390
+ const [title, setTitle] = createSlotSignal();
391
+ const [tools, setTools] = createSlotSignal();
394
392
 
395
393
  const laneContextValue: KanbanLaneContextValue = {
396
394
  value: () => local.value,
@@ -1,4 +1,5 @@
1
- import { createContext, useContext, type Accessor, type JSX, type Setter } from "solid-js";
1
+ import { createContext, useContext, type Accessor, type Setter } from "solid-js";
2
+ import type { SlotAccessor } from "../../../hooks/createSlotSignal";
2
3
 
3
4
  // ── 타입 ──────────────────────────────────────────────────────
4
5
 
@@ -50,8 +51,6 @@ export function useKanbanContext(): KanbanContextValue {
50
51
 
51
52
  // ── Lane Context ───────────────────────────────────────────────
52
53
 
53
- type SlotAccessor = (() => JSX.Element) | undefined;
54
-
55
54
  export interface KanbanLaneContextValue<L = unknown, T = unknown> {
56
55
  value: Accessor<L | undefined>;
57
56
  dropTarget: Accessor<KanbanDropTarget<T> | undefined>;
@@ -1,7 +1,6 @@
1
1
  import {
2
2
  type Component,
3
3
  createContext,
4
- createSignal,
5
4
  type JSX,
6
5
  onCleanup,
7
6
  type ParentComponent,
@@ -16,6 +15,7 @@ import { twMerge } from "tailwind-merge";
16
15
  import { ripple } from "../../../directives/ripple";
17
16
  import { Collapse } from "../../disclosure/Collapse";
18
17
  import { createControllableSignal } from "../../../hooks/createControllableSignal";
18
+ import { createSlotSignal, type SlotAccessor } from "../../../hooks/createSlotSignal";
19
19
  import { useListContext } from "./ListContext";
20
20
  import { List } from "./List";
21
21
  import {
@@ -32,8 +32,6 @@ import type { ComponentSize } from "../../../styles/tokens.styles";
32
32
 
33
33
  void ripple;
34
34
 
35
- type SlotAccessor = (() => JSX.Element) | undefined;
36
-
37
35
  interface ListItemSlotsContextValue {
38
36
  setChildren: (content: SlotAccessor) => void;
39
37
  }
@@ -166,8 +164,7 @@ export const ListItem: ListItemComponent = (props) => {
166
164
  onChange: () => local.onOpenChange,
167
165
  });
168
166
 
169
- const [childrenSlot, _setChildrenSlot] = createSignal<SlotAccessor>();
170
- const setChildrenSlot = (content: SlotAccessor) => _setChildrenSlot(() => content);
167
+ const [childrenSlot, setChildrenSlot] = createSlotSignal();
171
168
  const hasChildren = () => childrenSlot() !== undefined;
172
169
 
173
170
  const useRipple = () => !(local.readonly || local.disabled);
@@ -16,6 +16,7 @@ import clsx from "clsx";
16
16
  import { twMerge } from "tailwind-merge";
17
17
  import { IconX } from "@tabler/icons-solidjs";
18
18
  import { createControllableSignal } from "../../hooks/createControllableSignal";
19
+ import { createSlotSignal, type SlotAccessor } from "../../hooks/createSlotSignal";
19
20
  import { createMountTransition } from "../../hooks/createMountTransition";
20
21
  import { createPointerDrag } from "../../hooks/createPointerDrag";
21
22
  import { mergeStyles } from "../../helpers/mergeStyles";
@@ -25,8 +26,6 @@ import { DialogDefaultsContext } from "./DialogContext";
25
26
  import { bringToFront, registerDialog, unregisterDialog } from "./dialogZIndex";
26
27
  import { Button } from "../form-control/Button";
27
28
 
28
- type SlotAccessor = (() => JSX.Element) | undefined;
29
-
30
29
  interface DialogSlotsContextValue {
31
30
  setHeader: (content: SlotAccessor) => void;
32
31
  setAction: (content: SlotAccessor) => void;
@@ -189,10 +188,8 @@ export const Dialog: DialogComponent = (props) => {
189
188
 
190
189
  const headerId = "dialog-header-" + createUniqueId();
191
190
 
192
- const [header, _setHeader] = createSignal<SlotAccessor>();
193
- const setHeader = (content: SlotAccessor) => _setHeader(() => content);
194
- const [action, _setAction] = createSignal<SlotAccessor>();
195
- const setAction = (content: SlotAccessor) => _setAction(() => content);
191
+ const [header, setHeader] = createSlotSignal();
192
+ const [action, setAction] = createSlotSignal();
196
193
  const hasHeader = () => header() !== undefined;
197
194
 
198
195
  const [open, setOpen] = createControllableSignal({
@@ -10,6 +10,8 @@ import {
10
10
  splitProps,
11
11
  } from "solid-js";
12
12
  import { createResizeObserver } from "@solid-primitives/resize-observer";
13
+ import { createControllableSignal } from "../../hooks/createControllableSignal";
14
+ import { createSlotSignal, type SlotAccessor } from "../../hooks/createSlotSignal";
13
15
  import { createMountTransition } from "../../hooks/createMountTransition";
14
16
  import { Portal } from "solid-js/web";
15
17
  import clsx from "clsx";
@@ -19,8 +21,6 @@ import { borderSubtle } from "../../styles/tokens.styles";
19
21
 
20
22
  // --- DropdownContext (internal) ---
21
23
 
22
- type SlotAccessor = (() => JSX.Element) | undefined;
23
-
24
24
  interface DropdownContextValue {
25
25
  toggle: () => void;
26
26
  setTrigger: (content: SlotAccessor) => void;
@@ -154,22 +154,11 @@ export const Dropdown: DropdownComponent = ((props: DropdownProps) => {
154
154
  "children",
155
155
  ]);
156
156
 
157
- const [open, setOpenInternal] = createSignal(false);
158
-
159
- // props.open 변경 시 내부 상태 동기화
160
- createEffect(() => {
161
- const propOpen = local.open;
162
- if (propOpen !== undefined) {
163
- setOpenInternal(propOpen);
164
- }
157
+ const [open, setOpen] = createControllableSignal({
158
+ value: () => local.open ?? false,
159
+ onChange: () => local.onOpenChange,
165
160
  });
166
161
 
167
- // 콜백 포함 setter
168
- const setOpen = (value: boolean) => {
169
- setOpenInternal(value);
170
- local.onOpenChange?.(value);
171
- };
172
-
173
162
  // toggle 함수 (disabled 체크 포함)
174
163
  const toggle = () => {
175
164
  if (local.disabled) return;
@@ -177,10 +166,8 @@ export const Dropdown: DropdownComponent = ((props: DropdownProps) => {
177
166
  };
178
167
 
179
168
  // 슬롯 등록 시그널
180
- const [triggerSlot, _setTriggerSlot] = createSignal<SlotAccessor>();
181
- const setTrigger = (content: SlotAccessor) => _setTriggerSlot(() => content);
182
- const [contentSlot, _setContentSlot] = createSignal<SlotAccessor>();
183
- const setContent = (content: SlotAccessor) => _setContentSlot(() => content);
169
+ const [triggerSlot, setTrigger] = createSlotSignal();
170
+ const [contentSlot, setContent] = createSlotSignal();
184
171
 
185
172
  // Trigger wrapper ref (위치 계산에 필요)
186
173
  let triggerRef: HTMLDivElement | undefined;