@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.
- package/README.md +1 -1
- package/dist/components/data/crud-detail/CrudDetail.d.ts.map +1 -1
- package/dist/components/data/crud-detail/CrudDetail.js +55 -42
- package/dist/components/data/crud-detail/CrudDetail.js.map +2 -2
- package/dist/components/data/crud-sheet/CrudSheet.d.ts.map +1 -1
- package/dist/components/data/crud-sheet/CrudSheet.js +120 -94
- package/dist/components/data/crud-sheet/CrudSheet.js.map +2 -2
- package/dist/components/data/crud-sheet/CrudSheetColumn.js +1 -1
- package/dist/components/data/crud-sheet/CrudSheetColumn.js.map +2 -2
- package/dist/components/data/crud-sheet/types.d.ts +4 -3
- package/dist/components/data/crud-sheet/types.d.ts.map +1 -1
- package/dist/components/data/kanban/Kanban.d.ts.map +1 -1
- package/dist/components/data/kanban/Kanban.js +3 -4
- package/dist/components/data/kanban/Kanban.js.map +2 -2
- package/dist/components/data/kanban/KanbanContext.d.ts +2 -3
- package/dist/components/data/kanban/KanbanContext.d.ts.map +1 -1
- package/dist/components/data/kanban/KanbanContext.js.map +1 -1
- package/dist/components/data/list/ListItem.d.ts.map +1 -1
- package/dist/components/data/list/ListItem.js +3 -3
- package/dist/components/data/list/ListItem.js.map +2 -2
- package/dist/components/data/sheet/DataSheet.styles.d.ts.map +1 -1
- package/dist/components/data/sheet/DataSheet.styles.js +3 -8
- package/dist/components/data/sheet/DataSheet.styles.js.map +1 -1
- package/dist/components/disclosure/Dialog.d.ts.map +1 -1
- package/dist/components/disclosure/Dialog.js +36 -27
- package/dist/components/disclosure/Dialog.js.map +2 -2
- package/dist/components/disclosure/Dropdown.d.ts.map +1 -1
- package/dist/components/disclosure/Dropdown.js +7 -15
- package/dist/components/disclosure/Dropdown.js.map +2 -2
- package/dist/components/display/Icon.js +1 -1
- package/dist/components/display/Icon.js.map +1 -1
- package/dist/components/feedback/notification/NotificationBanner.js +1 -1
- package/dist/components/feedback/notification/NotificationBanner.js.map +1 -1
- package/dist/components/feedback/notification/NotificationContext.d.ts +1 -1
- package/dist/components/feedback/notification/NotificationContext.d.ts.map +1 -1
- package/dist/components/feedback/notification/NotificationContext.js.map +1 -1
- package/dist/components/feedback/notification/NotificationProvider.d.ts.map +1 -1
- package/dist/components/feedback/notification/NotificationProvider.js +8 -12
- package/dist/components/feedback/notification/NotificationProvider.js.map +2 -2
- package/dist/components/form-control/color-picker/ColorPicker.d.ts +5 -3
- package/dist/components/form-control/color-picker/ColorPicker.d.ts.map +1 -1
- package/dist/components/form-control/color-picker/ColorPicker.js +11 -6
- package/dist/components/form-control/color-picker/ColorPicker.js.map +2 -2
- package/dist/components/form-control/field/NumberInput.d.ts.map +1 -1
- package/dist/components/form-control/field/NumberInput.js +2 -2
- package/dist/components/form-control/field/NumberInput.js.map +2 -2
- package/dist/components/form-control/field/TextInput.d.ts.map +1 -1
- package/dist/components/form-control/field/TextInput.js +3 -3
- package/dist/components/form-control/field/TextInput.js.map +2 -2
- package/dist/components/form-control/select/Select.d.ts.map +1 -1
- package/dist/components/form-control/select/Select.js +3 -4
- package/dist/components/form-control/select/Select.js.map +2 -2
- package/dist/components/form-control/select/SelectContext.d.ts +1 -2
- package/dist/components/form-control/select/SelectContext.d.ts.map +1 -1
- package/dist/components/form-control/select/SelectContext.js.map +1 -1
- package/dist/components/form-control/select/SelectItem.d.ts.map +1 -1
- package/dist/components/form-control/select/SelectItem.js +3 -3
- package/dist/components/form-control/select/SelectItem.js.map +2 -2
- package/dist/helpers/createAppStructure.d.ts +7 -4
- package/dist/helpers/createAppStructure.d.ts.map +1 -1
- package/dist/helpers/createAppStructure.js +20 -2
- package/dist/helpers/createAppStructure.js.map +1 -1
- package/dist/hooks/createPointerDrag.d.ts +1 -1
- package/dist/hooks/createPointerDrag.d.ts.map +1 -1
- package/dist/hooks/createPointerDrag.js +6 -4
- package/dist/hooks/createPointerDrag.js.map +1 -1
- package/dist/hooks/createSlotSignal.d.ts +9 -0
- package/dist/hooks/createSlotSignal.d.ts.map +1 -0
- package/dist/hooks/createSlotSignal.js +10 -0
- package/dist/hooks/createSlotSignal.js.map +6 -0
- package/dist/index.d.ts +15 -17
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -39
- package/dist/index.js.map +1 -1
- package/docs/data-components.md +18 -8
- package/docs/feedback.md +17 -10
- package/docs/helpers.md +24 -0
- package/docs/hooks.md +166 -83
- package/docs/styling.md +1 -0
- package/package.json +3 -3
- package/src/components/data/crud-detail/CrudDetail.tsx +45 -40
- package/src/components/data/crud-sheet/CrudSheet.tsx +99 -103
- package/src/components/data/crud-sheet/CrudSheetColumn.tsx +1 -1
- package/src/components/data/crud-sheet/types.ts +4 -3
- package/src/components/data/kanban/Kanban.tsx +3 -5
- package/src/components/data/kanban/KanbanContext.ts +2 -3
- package/src/components/data/list/ListItem.tsx +2 -5
- package/src/components/data/sheet/DataSheet.styles.ts +3 -8
- package/src/components/disclosure/Dialog.tsx +26 -26
- package/src/components/disclosure/Dropdown.tsx +7 -20
- package/src/components/display/Icon.tsx +1 -1
- package/src/components/feedback/notification/NotificationBanner.tsx +1 -1
- package/src/components/feedback/notification/NotificationContext.ts +2 -7
- package/src/components/feedback/notification/NotificationProvider.tsx +8 -15
- package/src/components/form-control/color-picker/ColorPicker.tsx +19 -9
- package/src/components/form-control/field/NumberInput.tsx +2 -4
- package/src/components/form-control/field/TextInput.tsx +2 -5
- package/src/components/form-control/select/Select.tsx +3 -6
- package/src/components/form-control/select/SelectContext.ts +1 -2
- package/src/components/form-control/select/SelectItem.tsx +2 -5
- package/src/helpers/createAppStructure.ts +36 -6
- package/src/hooks/createPointerDrag.ts +8 -5
- package/src/hooks/createSlotSignal.ts +14 -0
- package/src/index.ts +15 -41
- package/tailwind.config.ts +1 -0
package/docs/feedback.md
CHANGED
|
@@ -24,14 +24,15 @@ function MyComponent() {
|
|
|
24
24
|
});
|
|
25
25
|
};
|
|
26
26
|
|
|
27
|
-
//
|
|
27
|
+
// Error handling with error()
|
|
28
28
|
const handleLoad = async () => {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
| `
|
|
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
|
|
62
|
-
- `NotificationBell` -- Notification bell icon (shows unread count
|
|
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
|
|
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]` | `
|
|
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 `
|
|
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
|
|
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
|
-
|
|
312
|
+
// appStructure.ts
|
|
313
|
+
import { createAppStructure } from "@simplysm/solid";
|
|
256
314
|
import { IconHome, IconUsers } from "@tabler/icons-solidjs";
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
-
|
|
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
|
-
|
|
295
|
-
import {
|
|
296
|
-
import { appStructure } from "./appStructure";
|
|
349
|
+
// main.tsx or App.tsx
|
|
350
|
+
import { AppStructureProvider } from "./appStructure";
|
|
297
351
|
|
|
298
352
|
render(
|
|
299
353
|
() => (
|
|
300
|
-
<
|
|
301
|
-
<
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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>[];
|
|
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
|
|
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 {
|
|
422
|
-
|
|
423
|
-
const appStructure = createAppStructure({ items });
|
|
498
|
+
import { useLocation } from "@solidjs/router";
|
|
499
|
+
import { useAppStructure } from "./appStructure";
|
|
424
500
|
|
|
425
|
-
|
|
426
|
-
const
|
|
501
|
+
function Breadcrumb() {
|
|
502
|
+
const appStructure = useAppStructure();
|
|
503
|
+
const location = useLocation();
|
|
427
504
|
|
|
428
|
-
//
|
|
429
|
-
|
|
505
|
+
// Returns ["Sales", "Invoice"] for /home/sales/invoice
|
|
506
|
+
const breadcrumb = () => appStructure.getTitleChainByHref(location.pathname);
|
|
430
507
|
|
|
431
|
-
|
|
432
|
-
|
|
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
|
-
|
|
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@simplysm/solid",
|
|
3
|
-
"version": "13.0.
|
|
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.
|
|
54
|
-
"@simplysm/core-common": "13.0.
|
|
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 {
|
|
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 {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
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
|
-
|
|
122
|
-
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
if (
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
<
|
|
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
|
-
</
|
|
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-
|
|
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
|
|
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
|
|
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
|
|
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>
|