@simplysm/solid 13.0.57 → 13.0.59

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 (105) hide show
  1. package/README.md +1 -1
  2. package/dist/components/data/crud-detail/CrudDetail.d.ts.map +1 -1
  3. package/dist/components/data/crud-detail/CrudDetail.js +55 -42
  4. package/dist/components/data/crud-detail/CrudDetail.js.map +2 -2
  5. package/dist/components/data/crud-sheet/CrudSheet.d.ts.map +1 -1
  6. package/dist/components/data/crud-sheet/CrudSheet.js +120 -94
  7. package/dist/components/data/crud-sheet/CrudSheet.js.map +2 -2
  8. package/dist/components/data/crud-sheet/CrudSheetColumn.js +1 -1
  9. package/dist/components/data/crud-sheet/CrudSheetColumn.js.map +2 -2
  10. package/dist/components/data/crud-sheet/types.d.ts +4 -3
  11. package/dist/components/data/crud-sheet/types.d.ts.map +1 -1
  12. package/dist/components/data/kanban/Kanban.d.ts.map +1 -1
  13. package/dist/components/data/kanban/Kanban.js +3 -4
  14. package/dist/components/data/kanban/Kanban.js.map +2 -2
  15. package/dist/components/data/kanban/KanbanContext.d.ts +2 -3
  16. package/dist/components/data/kanban/KanbanContext.d.ts.map +1 -1
  17. package/dist/components/data/kanban/KanbanContext.js.map +1 -1
  18. package/dist/components/data/list/ListItem.d.ts.map +1 -1
  19. package/dist/components/data/list/ListItem.js +3 -3
  20. package/dist/components/data/list/ListItem.js.map +2 -2
  21. package/dist/components/data/sheet/DataSheet.styles.d.ts.map +1 -1
  22. package/dist/components/data/sheet/DataSheet.styles.js +3 -8
  23. package/dist/components/data/sheet/DataSheet.styles.js.map +1 -1
  24. package/dist/components/disclosure/Dialog.d.ts.map +1 -1
  25. package/dist/components/disclosure/Dialog.js +36 -27
  26. package/dist/components/disclosure/Dialog.js.map +2 -2
  27. package/dist/components/disclosure/Dropdown.d.ts.map +1 -1
  28. package/dist/components/disclosure/Dropdown.js +7 -15
  29. package/dist/components/disclosure/Dropdown.js.map +2 -2
  30. package/dist/components/display/Icon.js +1 -1
  31. package/dist/components/display/Icon.js.map +1 -1
  32. package/dist/components/feedback/notification/NotificationBanner.js +1 -1
  33. package/dist/components/feedback/notification/NotificationBanner.js.map +1 -1
  34. package/dist/components/feedback/notification/NotificationContext.d.ts +1 -1
  35. package/dist/components/feedback/notification/NotificationContext.d.ts.map +1 -1
  36. package/dist/components/feedback/notification/NotificationContext.js.map +1 -1
  37. package/dist/components/feedback/notification/NotificationProvider.d.ts.map +1 -1
  38. package/dist/components/feedback/notification/NotificationProvider.js +8 -12
  39. package/dist/components/feedback/notification/NotificationProvider.js.map +2 -2
  40. package/dist/components/form-control/color-picker/ColorPicker.d.ts +5 -3
  41. package/dist/components/form-control/color-picker/ColorPicker.d.ts.map +1 -1
  42. package/dist/components/form-control/color-picker/ColorPicker.js +11 -6
  43. package/dist/components/form-control/color-picker/ColorPicker.js.map +2 -2
  44. package/dist/components/form-control/field/NumberInput.d.ts.map +1 -1
  45. package/dist/components/form-control/field/NumberInput.js +2 -2
  46. package/dist/components/form-control/field/NumberInput.js.map +2 -2
  47. package/dist/components/form-control/field/TextInput.d.ts.map +1 -1
  48. package/dist/components/form-control/field/TextInput.js +3 -3
  49. package/dist/components/form-control/field/TextInput.js.map +2 -2
  50. package/dist/components/form-control/select/Select.d.ts.map +1 -1
  51. package/dist/components/form-control/select/Select.js +3 -4
  52. package/dist/components/form-control/select/Select.js.map +2 -2
  53. package/dist/components/form-control/select/SelectContext.d.ts +1 -2
  54. package/dist/components/form-control/select/SelectContext.d.ts.map +1 -1
  55. package/dist/components/form-control/select/SelectContext.js.map +1 -1
  56. package/dist/components/form-control/select/SelectItem.d.ts.map +1 -1
  57. package/dist/components/form-control/select/SelectItem.js +3 -3
  58. package/dist/components/form-control/select/SelectItem.js.map +2 -2
  59. package/dist/helpers/createAppStructure.d.ts +7 -4
  60. package/dist/helpers/createAppStructure.d.ts.map +1 -1
  61. package/dist/helpers/createAppStructure.js +20 -2
  62. package/dist/helpers/createAppStructure.js.map +1 -1
  63. package/dist/hooks/createPointerDrag.d.ts +1 -1
  64. package/dist/hooks/createPointerDrag.d.ts.map +1 -1
  65. package/dist/hooks/createPointerDrag.js +6 -4
  66. package/dist/hooks/createPointerDrag.js.map +1 -1
  67. package/dist/hooks/createSlotSignal.d.ts +9 -0
  68. package/dist/hooks/createSlotSignal.d.ts.map +1 -0
  69. package/dist/hooks/createSlotSignal.js +10 -0
  70. package/dist/hooks/createSlotSignal.js.map +6 -0
  71. package/dist/index.d.ts +15 -17
  72. package/dist/index.d.ts.map +1 -1
  73. package/dist/index.js +15 -39
  74. package/dist/index.js.map +1 -1
  75. package/docs/data-components.md +18 -8
  76. package/docs/feedback.md +17 -10
  77. package/docs/helpers.md +24 -0
  78. package/docs/hooks.md +166 -83
  79. package/docs/styling.md +1 -0
  80. package/package.json +3 -3
  81. package/src/components/data/crud-detail/CrudDetail.tsx +45 -40
  82. package/src/components/data/crud-sheet/CrudSheet.tsx +99 -103
  83. package/src/components/data/crud-sheet/CrudSheetColumn.tsx +1 -1
  84. package/src/components/data/crud-sheet/types.ts +4 -3
  85. package/src/components/data/kanban/Kanban.tsx +3 -5
  86. package/src/components/data/kanban/KanbanContext.ts +2 -3
  87. package/src/components/data/list/ListItem.tsx +2 -5
  88. package/src/components/data/sheet/DataSheet.styles.ts +3 -8
  89. package/src/components/disclosure/Dialog.tsx +26 -26
  90. package/src/components/disclosure/Dropdown.tsx +7 -20
  91. package/src/components/display/Icon.tsx +1 -1
  92. package/src/components/feedback/notification/NotificationBanner.tsx +1 -1
  93. package/src/components/feedback/notification/NotificationContext.ts +2 -7
  94. package/src/components/feedback/notification/NotificationProvider.tsx +8 -15
  95. package/src/components/form-control/color-picker/ColorPicker.tsx +19 -9
  96. package/src/components/form-control/field/NumberInput.tsx +2 -4
  97. package/src/components/form-control/field/TextInput.tsx +2 -5
  98. package/src/components/form-control/select/Select.tsx +3 -6
  99. package/src/components/form-control/select/SelectContext.ts +1 -2
  100. package/src/components/form-control/select/SelectItem.tsx +2 -5
  101. package/src/helpers/createAppStructure.ts +36 -6
  102. package/src/hooks/createPointerDrag.ts +8 -5
  103. package/src/hooks/createSlotSignal.ts +14 -0
  104. package/src/index.ts +15 -41
  105. package/tailwind.config.ts +1 -0
package/docs/feedback.md CHANGED
@@ -24,14 +24,15 @@ function MyComponent() {
24
24
  });
25
25
  };
26
26
 
27
- // Automatic error handling with try
27
+ // Error handling with error()
28
28
  const handleLoad = async () => {
29
- const data = await notification.try(
30
- async () => await fetchData(),
31
- "Failed to load data", // optional header
32
- );
33
- // On error: shows danger notification with header + err.message,
34
- // logs err.stack, returns undefined
29
+ try {
30
+ const data = await fetchData();
31
+ } catch (err) {
32
+ notification.error(err, "Failed to load data"); // optional header
33
+ // Shows danger notification with header + err.message,
34
+ // logs err.stack via useLogger
35
+ }
35
36
  };
36
37
 
37
38
  return <Button onClick={handleSave}>Save</Button>;
@@ -49,7 +50,7 @@ function MyComponent() {
49
50
  | `success` | `(title: string, message?: string, options?: NotificationOptions) => string` | Success notification |
50
51
  | `warning` | `(title: string, message?: string, options?: NotificationOptions) => string` | Warning notification |
51
52
  | `danger` | `(title: string, message?: string, options?: NotificationOptions) => string` | Error notification |
52
- | `try` | `<TResult>(fn: () => Promise<TResult> \| TResult, header?: string) => Promise<TResult \| undefined>` | Execute function with automatic error handling (shows danger notification + logs to `useLogger`) |
53
+ | `error` | `(err?: any, header?: string) => void` | Show error notification from caught error (shows danger notification + logs to `useLogger`). Re-throws if `err` is not an `Error` instance. |
53
54
  | `update` | `(id: string, updates: Partial<NotificationItem>, options?: { renotify?: boolean }) => void` | Update notification |
54
55
  | `remove` | `(id: string) => void` | Remove notification |
55
56
  | `markAsRead` | `(id: string) => void` | Mark as read |
@@ -58,8 +59,14 @@ function MyComponent() {
58
59
  | `clear` | `() => void` | Clear all |
59
60
 
60
61
  **Components:**
61
- - `NotificationBanner` -- Top-of-screen notification banner (add inside `NotificationProvider`)
62
- - `NotificationBell` -- Notification bell icon (shows unread count, add to your layout as needed)
62
+ - `NotificationBanner` -- Top-of-screen notification banner. Automatically included by `SystemProvider`.
63
+ - `NotificationBell` -- Notification bell icon (shows unread count badge; click to view history). Add to your layout as needed. By default includes its own `NotificationBanner` instance via `showBanner` prop — set `showBanner={false}` when using with `SystemProvider` to avoid a duplicate banner.
64
+
65
+ **NotificationBell Props:**
66
+
67
+ | Prop | Type | Default | Description |
68
+ |------|------|---------|-------------|
69
+ | `showBanner` | `boolean` | `true` | Render a `NotificationBanner` alongside the bell. Set to `false` when `SystemProvider` already provides one. |
63
70
 
64
71
  ---
65
72
 
package/docs/helpers.md CHANGED
@@ -28,3 +28,27 @@ void ripple;
28
28
  - Creates internal ripple container, operates without affecting parent element
29
29
  - Automatically disabled when `prefers-reduced-motion: reduce` is set
30
30
  - Single ripple mode: removes previous ripple on new click
31
+
32
+ ---
33
+
34
+ ## createAppStructure
35
+
36
+ See [createAppStructure](hooks.md#createappstructure) in the Hooks documentation for full API details.
37
+
38
+ Exports the following types:
39
+
40
+ ```typescript
41
+ import {
42
+ createAppStructure,
43
+ type AppStructure,
44
+ type AppStructureItem,
45
+ type AppStructureGroupItem,
46
+ type AppStructureLeafItem,
47
+ type AppStructureSubPerm,
48
+ type AppRoute,
49
+ type AppMenu,
50
+ type AppFlatMenu,
51
+ type AppPerm,
52
+ type AppFlatPerm,
53
+ } from "@simplysm/solid";
54
+ ```
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.57",
3
+ "version": "13.0.59",
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.57",
54
- "@simplysm/core-common": "13.0.57"
53
+ "@simplysm/core-browser": "13.0.59",
54
+ "@simplysm/core-common": "13.0.59"
55
55
  },
56
56
  "devDependencies": {
57
57
  "@solidjs/testing-library": "^0.8.10"
@@ -15,18 +15,24 @@ import { BusyContainer } from "../../feedback/busy/BusyContainer";
15
15
  import { useNotification } from "../../feedback/notification/NotificationContext";
16
16
  import { Button } from "../../form-control/Button";
17
17
  import { Icon } from "../../display/Icon";
18
- import { TopbarContext, createTopbarActions } from "../../layout/topbar/TopbarContext";
18
+ import { createTopbarActions, TopbarContext } from "../../layout/topbar/TopbarContext";
19
19
  import { useDialogInstance } from "../../disclosure/DialogInstanceContext";
20
20
  import { Dialog } from "../../disclosure/Dialog";
21
21
  import { createEventListener } from "@solid-primitives/event-listener";
22
22
  import clsx from "clsx";
23
- import { IconDeviceFloppy, IconRefresh, IconTrash, IconTrashOff } from "@tabler/icons-solidjs";
24
- import { isCrudDetailToolsDef, CrudDetailTools } from "./CrudDetailTools";
25
- import { isCrudDetailBeforeDef, CrudDetailBefore } from "./CrudDetailBefore";
26
- import { isCrudDetailAfterDef, CrudDetailAfter } from "./CrudDetailAfter";
23
+ import {
24
+ IconCheck,
25
+ IconDeviceFloppy,
26
+ IconRefresh,
27
+ IconTrash,
28
+ IconTrashOff,
29
+ } from "@tabler/icons-solidjs";
30
+ import { CrudDetailTools, isCrudDetailToolsDef } from "./CrudDetailTools";
31
+ import { CrudDetailBefore, isCrudDetailBeforeDef } from "./CrudDetailBefore";
32
+ import { CrudDetailAfter, isCrudDetailAfterDef } from "./CrudDetailAfter";
27
33
  import type {
28
- CrudDetailBeforeDef,
29
34
  CrudDetailAfterDef,
35
+ CrudDetailBeforeDef,
30
36
  CrudDetailContext,
31
37
  CrudDetailInfo,
32
38
  CrudDetailProps,
@@ -77,13 +83,14 @@ const CrudDetailBase = <TData extends object>(props: CrudDetailProps<TData>) =>
77
83
  // -- Load --
78
84
  async function doLoad() {
79
85
  setBusyCount((c) => c + 1);
80
- // eslint-disable-next-line solid/reactivity -- noti.try 내부에서 비동기 호출
81
- await noti.try(async () => {
86
+ try {
82
87
  const result = await local.load();
83
88
  setData(reconcile(result.data) as any);
84
89
  originalData = objClone(result.data);
85
90
  setInfo(result.info);
86
- }, "조회 실패");
91
+ } catch (err) {
92
+ noti.error(err, "조회실패");
93
+ }
87
94
  setBusyCount((c) => c - 1);
88
95
  setReady(true);
89
96
  }
@@ -118,9 +125,8 @@ const CrudDetailBase = <TData extends object>(props: CrudDetailProps<TData>) =>
118
125
  }
119
126
 
120
127
  setBusyCount((c) => c + 1);
121
- // eslint-disable-next-line solid/reactivity -- noti.try 내부에서 비동기 호출
122
- await noti.try(async () => {
123
- const result = await local.submit!(objClone(unwrap(data)));
128
+ try {
129
+ const result = await local.submit(objClone(unwrap(data)));
124
130
  if (result) {
125
131
  noti.success("저장 완료", "저장되었습니다.");
126
132
  if (dialogInstance) {
@@ -129,7 +135,9 @@ const CrudDetailBase = <TData extends object>(props: CrudDetailProps<TData>) =>
129
135
  await doLoad();
130
136
  }
131
137
  }
132
- }, "저장 실패");
138
+ } catch (err) {
139
+ noti.error(err, "저장 실패");
140
+ }
133
141
  setBusyCount((c) => c - 1);
134
142
  }
135
143
 
@@ -149,25 +157,19 @@ const CrudDetailBase = <TData extends object>(props: CrudDetailProps<TData>) =>
149
157
  const del = !currentInfo.isDeleted;
150
158
 
151
159
  setBusyCount((c) => c + 1);
152
- /* eslint-disable solid/reactivity -- noti.try 내부에서 비동기 호출 */
153
- await noti.try(
154
- async () => {
155
- const result = await local.toggleDelete!(del);
156
- if (result) {
157
- noti.success(
158
- del ? "삭제 완료" : "복구 완료",
159
- del ? "삭제되었습니다." : "복구되었습니다.",
160
- );
161
- if (dialogInstance) {
162
- dialogInstance.close(true);
163
- } else {
164
- await doLoad();
165
- }
160
+ try {
161
+ const result = await local.toggleDelete(del);
162
+ if (result) {
163
+ noti.success(del ? "삭제 완료" : "복구 완료", del ? "삭제되었습니다." : "복구되었습니다.");
164
+ if (dialogInstance) {
165
+ dialogInstance.close(true);
166
+ } else {
167
+ await doLoad();
166
168
  }
167
- },
168
- del ? "삭제 실패" : "복구 실패",
169
- );
170
- /* eslint-enable solid/reactivity */
169
+ }
170
+ } catch (err) {
171
+ noti.error(err, del ? "삭제 실패" : "복구 실패");
172
+ }
171
173
  setBusyCount((c) => c - 1);
172
174
  }
173
175
 
@@ -242,12 +244,9 @@ const CrudDetailBase = <TData extends object>(props: CrudDetailProps<TData>) =>
242
244
  {/* Modal mode: Dialog.Action (refresh button in header) */}
243
245
  <Show when={isModal}>
244
246
  <Dialog.Action>
245
- <button
246
- class="flex items-center px-2 text-base-400 hover:text-base-600"
247
- onClick={() => void handleRefresh()}
248
- >
247
+ <Button size={"sm"} variant={"ghost"} onClick={() => void handleRefresh()}>
249
248
  <Icon icon={IconRefresh} />
250
- </button>
249
+ </Button>
251
250
  </Dialog.Action>
252
251
  </Show>
253
252
 
@@ -297,7 +296,7 @@ const CrudDetailBase = <TData extends object>(props: CrudDetailProps<TData>) =>
297
296
  <Show when={defs().before}>{(beforeDef) => beforeDef().children}</Show>
298
297
 
299
298
  {/* Form */}
300
- <form ref={formRef} class="flex-1 overflow-auto p-2" onSubmit={handleFormSubmit}>
299
+ <form ref={formRef} class="flex-1 overflow-auto p-4" onSubmit={handleFormSubmit}>
301
300
  {formContent()}
302
301
  </form>
303
302
 
@@ -316,20 +315,26 @@ const CrudDetailBase = <TData extends object>(props: CrudDetailProps<TData>) =>
316
315
 
317
316
  {/* Modal mode: bottom bar */}
318
317
  <Show when={isModal && canEdit()}>
319
- <div class="flex gap-2 border-t border-base-200 p-2">
318
+ <div class="flex gap-2 border-t border-base-200 px-3 py-1.5">
320
319
  <div class="flex-1" />
321
320
  <Show
322
321
  when={local.toggleDelete && info() && !info()!.isNew && (local.deletable ?? true)}
323
322
  >
324
323
  {(_) => (
325
- <Button size="sm" theme="danger" onClick={() => void handleToggleDelete()}>
324
+ <Button variant={"solid"} theme="danger" onClick={() => void handleToggleDelete()}>
326
325
  <Icon icon={info()!.isDeleted ? IconTrashOff : IconTrash} class="mr-1" />
327
326
  {info()!.isDeleted ? "복구" : "삭제"}
328
327
  </Button>
329
328
  )}
330
329
  </Show>
331
330
  <Show when={local.submit}>
332
- <Button size="sm" theme="primary" onClick={() => formRef?.requestSubmit()}>
331
+ <Button
332
+ variant={"solid"}
333
+ theme="primary"
334
+ onClick={() => formRef?.requestSubmit()}
335
+ class={"gap-1"}
336
+ >
337
+ <Icon icon={IconCheck} />
333
338
  확인
334
339
  </Button>
335
340
  </Show>