@simplysm/solid 13.0.55 → 13.0.56
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 +3 -1
- package/dist/components/data/crud-detail/CrudDetail.d.ts +14 -0
- package/dist/components/data/crud-detail/CrudDetail.d.ts.map +1 -0
- package/dist/components/data/crud-detail/CrudDetail.js +348 -0
- package/dist/components/data/crud-detail/CrudDetail.js.map +6 -0
- package/dist/components/data/crud-detail/CrudDetailAfter.d.ts +7 -0
- package/dist/components/data/crud-detail/CrudDetailAfter.d.ts.map +1 -0
- package/dist/components/data/crud-detail/CrudDetailAfter.js +14 -0
- package/dist/components/data/crud-detail/CrudDetailAfter.js.map +6 -0
- package/dist/components/data/crud-detail/CrudDetailBefore.d.ts +7 -0
- package/dist/components/data/crud-detail/CrudDetailBefore.d.ts.map +1 -0
- package/dist/components/data/crud-detail/CrudDetailBefore.js +14 -0
- package/dist/components/data/crud-detail/CrudDetailBefore.js.map +6 -0
- package/dist/components/data/crud-detail/CrudDetailTools.d.ts +7 -0
- package/dist/components/data/crud-detail/CrudDetailTools.d.ts.map +1 -0
- package/dist/components/data/crud-detail/CrudDetailTools.js +14 -0
- package/dist/components/data/crud-detail/CrudDetailTools.js.map +6 -0
- package/dist/components/data/crud-detail/types.d.ts +45 -0
- package/dist/components/data/crud-detail/types.d.ts.map +1 -0
- package/dist/components/data/crud-detail/types.js +1 -0
- package/dist/components/data/crud-detail/types.js.map +6 -0
- package/dist/components/data/crud-sheet/CrudSheet.d.ts +17 -0
- package/dist/components/data/crud-sheet/CrudSheet.d.ts.map +1 -0
- package/dist/components/data/crud-sheet/CrudSheet.js +679 -0
- package/dist/components/data/crud-sheet/CrudSheet.js.map +6 -0
- package/dist/components/data/crud-sheet/CrudSheetColumn.d.ts +5 -0
- package/dist/components/data/crud-sheet/CrudSheetColumn.d.ts.map +1 -0
- package/dist/components/data/crud-sheet/CrudSheetColumn.js +29 -0
- package/dist/components/data/crud-sheet/CrudSheetColumn.js.map +6 -0
- package/dist/components/data/crud-sheet/CrudSheetFilter.d.ts +7 -0
- package/dist/components/data/crud-sheet/CrudSheetFilter.d.ts.map +1 -0
- package/dist/components/data/crud-sheet/CrudSheetFilter.js +14 -0
- package/dist/components/data/crud-sheet/CrudSheetFilter.js.map +6 -0
- package/dist/components/data/crud-sheet/CrudSheetHeader.d.ts +7 -0
- package/dist/components/data/crud-sheet/CrudSheetHeader.d.ts.map +1 -0
- package/dist/components/data/crud-sheet/CrudSheetHeader.js +14 -0
- package/dist/components/data/crud-sheet/CrudSheetHeader.js.map +6 -0
- package/dist/components/data/crud-sheet/CrudSheetTools.d.ts +7 -0
- package/dist/components/data/crud-sheet/CrudSheetTools.d.ts.map +1 -0
- package/dist/components/data/crud-sheet/CrudSheetTools.js +14 -0
- package/dist/components/data/crud-sheet/CrudSheetTools.js.map +6 -0
- package/dist/components/data/crud-sheet/types.d.ts +109 -0
- package/dist/components/data/crud-sheet/types.d.ts.map +1 -0
- package/dist/components/data/crud-sheet/types.js +1 -0
- package/dist/components/data/crud-sheet/types.js.map +6 -0
- package/dist/components/data/kanban/Kanban.d.ts.map +1 -1
- package/dist/components/data/kanban/Kanban.js +137 -138
- package/dist/components/data/kanban/Kanban.js.map +2 -2
- package/dist/components/data/kanban/KanbanContext.d.ts +5 -1
- 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 +109 -99
- package/dist/components/data/list/ListItem.js.map +2 -2
- package/dist/components/data/sheet/DataSheet.js +1 -1
- package/dist/components/data/sheet/DataSheet.js.map +2 -2
- package/dist/components/data/sheet/DataSheet.styles.d.ts.map +1 -1
- package/dist/components/data/sheet/DataSheet.styles.js +1 -1
- package/dist/components/data/sheet/DataSheet.styles.js.map +1 -1
- package/dist/components/disclosure/Dialog.d.ts +16 -10
- package/dist/components/disclosure/Dialog.d.ts.map +1 -1
- package/dist/components/disclosure/Dialog.js +126 -91
- package/dist/components/disclosure/Dialog.js.map +2 -2
- package/dist/components/disclosure/DialogContext.d.ts +2 -4
- package/dist/components/disclosure/DialogContext.d.ts.map +1 -1
- package/dist/components/disclosure/DialogContext.js.map +1 -1
- package/dist/components/disclosure/DialogProvider.d.ts.map +1 -1
- package/dist/components/disclosure/DialogProvider.js +14 -9
- package/dist/components/disclosure/DialogProvider.js.map +2 -2
- package/dist/components/disclosure/Dropdown.d.ts +46 -22
- package/dist/components/disclosure/Dropdown.d.ts.map +1 -1
- package/dist/components/disclosure/Dropdown.js +100 -65
- package/dist/components/disclosure/Dropdown.js.map +2 -2
- package/dist/components/feedback/notification/NotificationBanner.d.ts.map +1 -1
- package/dist/components/feedback/notification/NotificationBanner.js +3 -3
- package/dist/components/feedback/notification/NotificationBanner.js.map +1 -1
- package/dist/components/feedback/notification/NotificationBell.d.ts.map +1 -1
- package/dist/components/feedback/notification/NotificationBell.js +84 -84
- package/dist/components/feedback/notification/NotificationBell.js.map +2 -2
- package/dist/components/form-control/combobox/Combobox.d.ts +6 -3
- package/dist/components/form-control/combobox/Combobox.d.ts.map +1 -1
- package/dist/components/form-control/combobox/Combobox.js +150 -168
- package/dist/components/form-control/combobox/Combobox.js.map +2 -2
- package/dist/components/form-control/combobox/ComboboxContext.d.ts +3 -0
- package/dist/components/form-control/combobox/ComboboxContext.d.ts.map +1 -1
- package/dist/components/form-control/combobox/ComboboxContext.js.map +1 -1
- package/dist/components/form-control/date-range-picker/DateRangePicker.d.ts +0 -2
- package/dist/components/form-control/date-range-picker/DateRangePicker.d.ts.map +1 -1
- package/dist/components/form-control/date-range-picker/DateRangePicker.js +9 -17
- package/dist/components/form-control/date-range-picker/DateRangePicker.js.map +2 -2
- package/dist/components/form-control/field/Field.styles.d.ts.map +1 -1
- package/dist/components/form-control/field/Field.styles.js +2 -1
- package/dist/components/form-control/field/Field.styles.js.map +1 -1
- package/dist/components/form-control/field/NumberInput.d.ts +15 -5
- package/dist/components/form-control/field/NumberInput.d.ts.map +1 -1
- package/dist/components/form-control/field/NumberInput.js +181 -141
- package/dist/components/form-control/field/NumberInput.js.map +2 -2
- package/dist/components/form-control/field/TextInput.d.ts +9 -5
- package/dist/components/form-control/field/TextInput.d.ts.map +1 -1
- package/dist/components/form-control/field/TextInput.js +199 -154
- package/dist/components/form-control/field/TextInput.js.map +2 -2
- package/dist/components/form-control/select/Select.d.ts +3 -3
- package/dist/components/form-control/select/Select.d.ts.map +1 -1
- package/dist/components/form-control/select/Select.js +116 -100
- package/dist/components/form-control/select/Select.js.map +2 -2
- package/dist/components/form-control/select/SelectContext.d.ts +9 -1
- 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 +77 -67
- package/dist/components/form-control/select/SelectItem.js.map +2 -2
- package/dist/components/layout/topbar/TopbarMenu.d.ts.map +1 -1
- package/dist/components/layout/topbar/TopbarMenu.js +63 -57
- package/dist/components/layout/topbar/TopbarMenu.js.map +2 -2
- package/dist/components/layout/topbar/TopbarUser.d.ts.map +1 -1
- package/dist/components/layout/topbar/TopbarUser.js +53 -54
- package/dist/components/layout/topbar/TopbarUser.js.map +2 -2
- package/dist/hooks/createControllableStore.d.ts +29 -0
- package/dist/hooks/createControllableStore.d.ts.map +1 -0
- package/dist/hooks/createControllableStore.js +19 -0
- package/dist/hooks/createControllableStore.js.map +6 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -2
- package/dist/index.js.map +1 -1
- package/dist/styles/patterns.styles.d.ts.map +1 -1
- package/dist/styles/patterns.styles.js +7 -1
- package/dist/styles/patterns.styles.js.map +1 -1
- package/docs/data-components.md +428 -0
- package/docs/disclosure.md +65 -35
- package/docs/form-controls.md +18 -3
- package/docs/helpers.md +0 -39
- package/docs/hooks.md +39 -0
- package/package.json +4 -3
- package/src/components/data/crud-detail/CrudDetail.tsx +346 -0
- package/src/components/data/crud-detail/CrudDetailAfter.tsx +19 -0
- package/src/components/data/crud-detail/CrudDetailBefore.tsx +19 -0
- package/src/components/data/crud-detail/CrudDetailTools.tsx +19 -0
- package/src/components/data/crud-detail/types.ts +58 -0
- package/src/components/data/crud-sheet/CrudSheet.tsx +628 -0
- package/src/components/data/crud-sheet/CrudSheetColumn.tsx +34 -0
- package/src/components/data/crud-sheet/CrudSheetFilter.tsx +21 -0
- package/src/components/data/crud-sheet/CrudSheetHeader.tsx +19 -0
- package/src/components/data/crud-sheet/CrudSheetTools.tsx +21 -0
- package/src/components/data/crud-sheet/types.ts +133 -0
- package/src/components/data/kanban/Kanban.tsx +72 -65
- package/src/components/data/kanban/KanbanContext.ts +7 -1
- package/src/components/data/list/ListItem.tsx +31 -18
- package/src/components/data/sheet/DataSheet.styles.ts +1 -1
- package/src/components/data/sheet/DataSheet.tsx +1 -1
- package/src/components/disclosure/Dialog.tsx +143 -105
- package/src/components/disclosure/DialogContext.ts +2 -4
- package/src/components/disclosure/DialogProvider.tsx +4 -2
- package/src/components/disclosure/Dropdown.tsx +174 -86
- package/src/components/feedback/notification/NotificationBanner.tsx +3 -9
- package/src/components/feedback/notification/NotificationBell.tsx +51 -57
- package/src/components/form-control/combobox/Combobox.tsx +109 -134
- package/src/components/form-control/combobox/ComboboxContext.ts +4 -1
- package/src/components/form-control/date-range-picker/DateRangePicker.tsx +6 -16
- package/src/components/form-control/field/Field.styles.ts +1 -0
- package/src/components/form-control/field/NumberInput.tsx +131 -88
- package/src/components/form-control/field/TextInput.tsx +139 -88
- package/src/components/form-control/select/Select.tsx +85 -67
- package/src/components/form-control/select/SelectContext.ts +12 -1
- package/src/components/form-control/select/SelectItem.tsx +39 -18
- package/src/components/layout/topbar/TopbarMenu.tsx +52 -55
- package/src/components/layout/topbar/TopbarUser.tsx +28 -31
- package/src/hooks/createControllableStore.ts +47 -0
- package/src/index.ts +5 -1
- package/src/styles/patterns.styles.ts +7 -1
- package/tailwind.css +4 -0
- package/dist/helpers/splitSlots.d.ts +0 -25
- package/dist/helpers/splitSlots.d.ts.map +0 -1
- package/dist/helpers/splitSlots.js +0 -25
- package/dist/helpers/splitSlots.js.map +0 -6
- package/dist/hooks/createItemTemplate.d.ts +0 -17
- package/dist/hooks/createItemTemplate.d.ts.map +0 -1
- package/dist/hooks/createItemTemplate.js +0 -40
- package/dist/hooks/createItemTemplate.js.map +0 -6
- package/src/helpers/splitSlots.ts +0 -51
- package/src/hooks/createItemTemplate.tsx +0 -42
package/docs/form-controls.md
CHANGED
|
@@ -31,6 +31,8 @@ Text input field with format mask and IME (Korean, etc.) composition support.
|
|
|
31
31
|
|
|
32
32
|
```tsx
|
|
33
33
|
import { TextInput } from "@simplysm/solid";
|
|
34
|
+
import { IconSearch } from "@tabler/icons-solidjs";
|
|
35
|
+
import { Icon } from "@simplysm/solid";
|
|
34
36
|
|
|
35
37
|
// Basic usage
|
|
36
38
|
<TextInput value={name()} onValueChange={setName} placeholder="Enter name" />
|
|
@@ -41,6 +43,11 @@ import { TextInput } from "@simplysm/solid";
|
|
|
41
43
|
// Format mask (e.g., phone number)
|
|
42
44
|
<TextInput format="XXX-XXXX-XXXX" value={phone()} onValueChange={setPhone} />
|
|
43
45
|
|
|
46
|
+
// With prefix
|
|
47
|
+
<TextInput value={query()} onValueChange={setQuery}>
|
|
48
|
+
<TextInput.Prefix><Icon icon={IconSearch} /></TextInput.Prefix>
|
|
49
|
+
</TextInput>
|
|
50
|
+
|
|
44
51
|
// With validation
|
|
45
52
|
<TextInput required minLength={3} value={name()} onValueChange={setName} />
|
|
46
53
|
<TextInput
|
|
@@ -56,7 +63,6 @@ import { TextInput } from "@simplysm/solid";
|
|
|
56
63
|
| `onValueChange` | `(value: string) => void` | - | Value change callback |
|
|
57
64
|
| `type` | `"text" \| "password" \| "email"` | `"text"` | Input type |
|
|
58
65
|
| `format` | `string` | - | Input format (`X` represents character position, rest are separators) |
|
|
59
|
-
| `prefixIcon` | `Component<TablerIconProps>` | - | Prefix icon (Tabler Icons component) |
|
|
60
66
|
| `placeholder` | `string` | - | Placeholder |
|
|
61
67
|
| `title` | `string` | - | Tooltip title |
|
|
62
68
|
| `autocomplete` | `JSX.HTMLAutocomplete` | - | HTML autocomplete attribute |
|
|
@@ -73,6 +79,9 @@ import { TextInput } from "@simplysm/solid";
|
|
|
73
79
|
| `validate` | `(value: string) => string \| undefined` | - | Custom validation function |
|
|
74
80
|
| `touchMode` | `boolean` | - | Show error only after field loses focus |
|
|
75
81
|
|
|
82
|
+
**Sub-components:**
|
|
83
|
+
- `TextInput.Prefix` -- Prefix element (icon, text, etc.) displayed before the input
|
|
84
|
+
|
|
76
85
|
---
|
|
77
86
|
|
|
78
87
|
## NumberInput
|
|
@@ -91,6 +100,11 @@ import { NumberInput } from "@simplysm/solid";
|
|
|
91
100
|
// Minimum 2 decimal places
|
|
92
101
|
<NumberInput value={price()} minDigits={2} />
|
|
93
102
|
|
|
103
|
+
// With prefix
|
|
104
|
+
<NumberInput value={price()} onValueChange={setPrice}>
|
|
105
|
+
<NumberInput.Prefix>₩</NumberInput.Prefix>
|
|
106
|
+
</NumberInput>
|
|
107
|
+
|
|
94
108
|
// With validation
|
|
95
109
|
<NumberInput required min={0} max={100} value={score()} onValueChange={setScore} />
|
|
96
110
|
```
|
|
@@ -101,7 +115,6 @@ import { NumberInput } from "@simplysm/solid";
|
|
|
101
115
|
| `onValueChange` | `(value: number \| undefined) => void` | - | Value change callback |
|
|
102
116
|
| `comma` | `boolean` | `true` | Show thousand separators |
|
|
103
117
|
| `minDigits` | `number` | - | Minimum decimal places |
|
|
104
|
-
| `prefixIcon` | `Component<TablerIconProps>` | - | Prefix icon (Tabler Icons component) |
|
|
105
118
|
| `placeholder` | `string` | - | Placeholder |
|
|
106
119
|
| `title` | `string` | - | Tooltip title |
|
|
107
120
|
| `disabled` | `boolean` | - | Disabled state |
|
|
@@ -116,6 +129,9 @@ import { NumberInput } from "@simplysm/solid";
|
|
|
116
129
|
| `validate` | `(value: number \| undefined) => string \| undefined` | - | Custom validation function |
|
|
117
130
|
| `touchMode` | `boolean` | - | Show error only after field loses focus |
|
|
118
131
|
|
|
132
|
+
**Sub-components:**
|
|
133
|
+
- `NumberInput.Prefix` -- Prefix element (currency symbol, icon, etc.) displayed before the input
|
|
134
|
+
|
|
119
135
|
---
|
|
120
136
|
|
|
121
137
|
## DatePicker
|
|
@@ -296,7 +312,6 @@ const [to, setTo] = createSignal<DateOnly>();
|
|
|
296
312
|
| `required` | `boolean` | - | Required field |
|
|
297
313
|
| `disabled` | `boolean` | - | Disabled state |
|
|
298
314
|
| `size` | `"xs" \| "sm" \| "lg" \| "xl"` | - | Size |
|
|
299
|
-
| `periodLabels` | `Partial<Record<DateRangePeriodType, string>>` | `{ day: "일", month: "월", range: "범위" }` | Period type labels (Korean by default) |
|
|
300
315
|
| `class` | `string` | - | Additional CSS class |
|
|
301
316
|
| `style` | `JSX.CSSProperties` | - | Inline style |
|
|
302
317
|
|
package/docs/helpers.md
CHANGED
|
@@ -12,45 +12,6 @@ const style = mergeStyles("color: red", { fontSize: "14px" }, props.style);
|
|
|
12
12
|
|
|
13
13
|
---
|
|
14
14
|
|
|
15
|
-
## splitSlots
|
|
16
|
-
|
|
17
|
-
Utility for splitting resolved children into named slots based on `data-*` attributes. Returns a tuple of `[slots accessor, content accessor]`.
|
|
18
|
-
|
|
19
|
-
Slot keys are matched against the element's `dataset` (camelCase). Sub-components that set a matching data attribute (e.g., `data-select-header`) are separated into the named slot; all other children remain in `content`.
|
|
20
|
-
|
|
21
|
-
```typescript
|
|
22
|
-
import { splitSlots } from "@simplysm/solid";
|
|
23
|
-
import { children } from "solid-js";
|
|
24
|
-
|
|
25
|
-
// Inside a component:
|
|
26
|
-
const resolved = children(() => props.children);
|
|
27
|
-
const [slots, content] = splitSlots(resolved, ["selectHeader", "selectAction"] as const);
|
|
28
|
-
|
|
29
|
-
// Access inside JSX (within a reactive context)
|
|
30
|
-
<div>{slots().selectHeader}</div> // HTMLElement[] matching data-select-header
|
|
31
|
-
<div>{content()}</div> // JSX.Element[] – remaining children
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
**Signature:**
|
|
35
|
-
|
|
36
|
-
```typescript
|
|
37
|
-
function splitSlots<K extends string>(
|
|
38
|
-
resolved: { toArray: () => unknown[] },
|
|
39
|
-
keys: readonly K[],
|
|
40
|
-
): [Accessor<Record<K, HTMLElement[]>>, Accessor<JSX.Element[]>]
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
| Parameter | Type | Description |
|
|
44
|
-
|-----------|------|-------------|
|
|
45
|
-
| `resolved` | `{ toArray: () => unknown[] }` | Resolved children from SolidJS `children()` |
|
|
46
|
-
| `keys` | `readonly K[]` | Slot key names (camelCase, matched against `element.dataset`) |
|
|
47
|
-
|
|
48
|
-
Returns a tuple:
|
|
49
|
-
- `slots` — `Accessor<Record<K, HTMLElement[]>>` — named slot elements per key
|
|
50
|
-
- `content` — `Accessor<JSX.Element[]>` — remaining children not matched by any key
|
|
51
|
-
|
|
52
|
-
---
|
|
53
|
-
|
|
54
15
|
## ripple directive
|
|
55
16
|
|
|
56
17
|
Material Design ripple effect directive. Displays ripple effect on click.
|
package/docs/hooks.md
CHANGED
|
@@ -163,6 +163,45 @@ setValue((prev) => prev + "!");
|
|
|
163
163
|
|
|
164
164
|
---
|
|
165
165
|
|
|
166
|
+
## createControllableStore
|
|
167
|
+
|
|
168
|
+
Store hook that automatically handles Controlled/Uncontrolled patterns for objects and arrays. Similar to `createControllableSignal` but uses SolidJS `createStore` internally, supporting path-based updates via `SetStoreFunction`.
|
|
169
|
+
|
|
170
|
+
Operates in controlled mode when `onChange` is provided (setter calls `onChange` with cloned value), uncontrolled mode otherwise (internal store only).
|
|
171
|
+
|
|
172
|
+
```tsx
|
|
173
|
+
import { createControllableStore } from "@simplysm/solid";
|
|
174
|
+
|
|
175
|
+
// Controlled mode (parent manages state)
|
|
176
|
+
const [items, setItems] = createControllableStore<Item[]>({
|
|
177
|
+
value: () => props.items ?? [],
|
|
178
|
+
onChange: () => props.onItemsChange,
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// Uncontrolled mode (internal state only)
|
|
182
|
+
const [items, setItems] = createControllableStore<Item[]>({
|
|
183
|
+
value: () => [],
|
|
184
|
+
onChange: () => undefined,
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// Supports all SetStoreFunction overloads
|
|
188
|
+
setItems(0, "name", "updated"); // path-based update
|
|
189
|
+
setItems(produce((draft) => { ... })); // produce
|
|
190
|
+
setItems(reconcile(newItems)); // reconcile
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
| Option | Type | Description |
|
|
194
|
+
|--------|------|-------------|
|
|
195
|
+
| `value` | `() => TValue` | Reactive value accessor (syncs external changes to internal store) |
|
|
196
|
+
| `onChange` | `() => ((value: TValue) => void) \| undefined` | Change callback accessor. When defined, enables controlled mode |
|
|
197
|
+
|
|
198
|
+
| Return | Type | Description |
|
|
199
|
+
|--------|------|-------------|
|
|
200
|
+
| `[0]` | `TValue` | Store (reactive proxy) |
|
|
201
|
+
| `[1]` | `SetStoreFunction<TValue>` | Store setter (triggers `onChange` in controlled mode) |
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
166
205
|
## createMountTransition
|
|
167
206
|
|
|
168
207
|
Mount transition hook for open/close CSS animations. Control DOM rendering with `mounted()` and toggle CSS classes with `animating()`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@simplysm/solid",
|
|
3
|
-
"version": "13.0.
|
|
3
|
+
"version": "13.0.56",
|
|
4
4
|
"description": "심플리즘 패키지 - SolidJS 라이브러리",
|
|
5
5
|
"author": "김석래",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
"*.css"
|
|
24
24
|
],
|
|
25
25
|
"dependencies": {
|
|
26
|
+
"@solid-primitives/event-listener": "^2.4.4",
|
|
26
27
|
"@solid-primitives/media": "^2.3.3",
|
|
27
28
|
"@solid-primitives/resize-observer": "^2.1.3",
|
|
28
29
|
"@solid-primitives/storage": "^4.3.3",
|
|
@@ -49,8 +50,8 @@
|
|
|
49
50
|
"solid-tiptap": "^0.8.0",
|
|
50
51
|
"tailwind-merge": "^3.5.0",
|
|
51
52
|
"tailwindcss": "^3.4.19",
|
|
52
|
-
"@simplysm/core-browser": "13.0.
|
|
53
|
-
"@simplysm/core-common": "13.0.
|
|
53
|
+
"@simplysm/core-browser": "13.0.56",
|
|
54
|
+
"@simplysm/core-common": "13.0.56"
|
|
54
55
|
},
|
|
55
56
|
"devDependencies": {
|
|
56
57
|
"@solidjs/testing-library": "^0.8.10"
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
import {
|
|
2
|
+
children,
|
|
3
|
+
createMemo,
|
|
4
|
+
createSignal,
|
|
5
|
+
type JSX,
|
|
6
|
+
onMount,
|
|
7
|
+
Show,
|
|
8
|
+
splitProps,
|
|
9
|
+
useContext,
|
|
10
|
+
} from "solid-js";
|
|
11
|
+
import { reconcile, unwrap } from "solid-js/store";
|
|
12
|
+
import { createControllableStore } from "../../../hooks/createControllableStore";
|
|
13
|
+
import { objClone, objEqual } from "@simplysm/core-common";
|
|
14
|
+
import { BusyContainer } from "../../feedback/busy/BusyContainer";
|
|
15
|
+
import { useNotification } from "../../feedback/notification/NotificationContext";
|
|
16
|
+
import { Button } from "../../form-control/Button";
|
|
17
|
+
import { Icon } from "../../display/Icon";
|
|
18
|
+
import { TopbarContext, createTopbarActions } from "../../layout/topbar/TopbarContext";
|
|
19
|
+
import { useDialogInstance } from "../../disclosure/DialogInstanceContext";
|
|
20
|
+
import { Dialog } from "../../disclosure/Dialog";
|
|
21
|
+
import { createEventListener } from "@solid-primitives/event-listener";
|
|
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";
|
|
27
|
+
import type {
|
|
28
|
+
CrudDetailBeforeDef,
|
|
29
|
+
CrudDetailAfterDef,
|
|
30
|
+
CrudDetailContext,
|
|
31
|
+
CrudDetailInfo,
|
|
32
|
+
CrudDetailProps,
|
|
33
|
+
CrudDetailToolsDef,
|
|
34
|
+
} from "./types";
|
|
35
|
+
|
|
36
|
+
interface CrudDetailComponent {
|
|
37
|
+
<TData extends object>(props: CrudDetailProps<TData>): JSX.Element;
|
|
38
|
+
Tools: typeof CrudDetailTools;
|
|
39
|
+
Before: typeof CrudDetailBefore;
|
|
40
|
+
After: typeof CrudDetailAfter;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const CrudDetailBase = <TData extends object>(props: CrudDetailProps<TData>) => {
|
|
44
|
+
const [local, _rest] = splitProps(props, [
|
|
45
|
+
"load",
|
|
46
|
+
"children",
|
|
47
|
+
"submit",
|
|
48
|
+
"toggleDelete",
|
|
49
|
+
"editable",
|
|
50
|
+
"deletable",
|
|
51
|
+
"data",
|
|
52
|
+
"onDataChange",
|
|
53
|
+
"class",
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
const noti = useNotification();
|
|
57
|
+
const topbarCtx = useContext(TopbarContext);
|
|
58
|
+
const dialogInstance = useDialogInstance<boolean>();
|
|
59
|
+
|
|
60
|
+
const isModal = dialogInstance !== undefined;
|
|
61
|
+
|
|
62
|
+
const canEdit = () => local.editable ?? true;
|
|
63
|
+
|
|
64
|
+
// -- State --
|
|
65
|
+
const [data, setData] = createControllableStore<TData>({
|
|
66
|
+
value: () => local.data ?? ({} as TData),
|
|
67
|
+
onChange: () => local.onDataChange,
|
|
68
|
+
});
|
|
69
|
+
let originalData: TData | undefined;
|
|
70
|
+
|
|
71
|
+
const [info, setInfo] = createSignal<CrudDetailInfo>();
|
|
72
|
+
const [busyCount, setBusyCount] = createSignal(0);
|
|
73
|
+
const [ready, setReady] = createSignal(false);
|
|
74
|
+
|
|
75
|
+
let formRef: HTMLFormElement | undefined;
|
|
76
|
+
|
|
77
|
+
// -- Load --
|
|
78
|
+
async function doLoad() {
|
|
79
|
+
setBusyCount((c) => c + 1);
|
|
80
|
+
// eslint-disable-next-line solid/reactivity -- noti.try 내부에서 비동기 호출
|
|
81
|
+
await noti.try(async () => {
|
|
82
|
+
const result = await local.load();
|
|
83
|
+
setData(reconcile(result.data) as any);
|
|
84
|
+
originalData = objClone(result.data);
|
|
85
|
+
setInfo(result.info);
|
|
86
|
+
}, "조회 실패");
|
|
87
|
+
setBusyCount((c) => c - 1);
|
|
88
|
+
setReady(true);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
onMount(() => {
|
|
92
|
+
void doLoad();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// -- Change Detection --
|
|
96
|
+
function hasChanges(): boolean {
|
|
97
|
+
if (originalData == null) return false;
|
|
98
|
+
return !objEqual(unwrap(data) as unknown, originalData as unknown);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// -- Refresh --
|
|
102
|
+
async function handleRefresh() {
|
|
103
|
+
if (hasChanges()) {
|
|
104
|
+
if (!confirm("변경사항을 무시하시겠습니까?")) return;
|
|
105
|
+
}
|
|
106
|
+
await doLoad();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// -- Save --
|
|
110
|
+
async function handleSave() {
|
|
111
|
+
if (busyCount() > 0) return;
|
|
112
|
+
if (!local.submit) return;
|
|
113
|
+
|
|
114
|
+
const currentInfo = info();
|
|
115
|
+
if (currentInfo && !currentInfo.isNew && !hasChanges()) {
|
|
116
|
+
noti.info("안내", "변경사항이 없습니다.");
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
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)));
|
|
124
|
+
if (result) {
|
|
125
|
+
noti.success("저장 완료", "저장되었습니다.");
|
|
126
|
+
if (dialogInstance) {
|
|
127
|
+
dialogInstance.close(true);
|
|
128
|
+
} else {
|
|
129
|
+
await doLoad();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}, "저장 실패");
|
|
133
|
+
setBusyCount((c) => c - 1);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function handleFormSubmit(e: Event) {
|
|
137
|
+
e.preventDefault();
|
|
138
|
+
await handleSave();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// -- Toggle Delete --
|
|
142
|
+
async function handleToggleDelete() {
|
|
143
|
+
if (busyCount() > 0) return;
|
|
144
|
+
if (!local.toggleDelete) return;
|
|
145
|
+
|
|
146
|
+
const currentInfo = info();
|
|
147
|
+
if (!currentInfo) return;
|
|
148
|
+
|
|
149
|
+
const del = !currentInfo.isDeleted;
|
|
150
|
+
|
|
151
|
+
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
|
+
}
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
del ? "삭제 실패" : "복구 실패",
|
|
169
|
+
);
|
|
170
|
+
/* eslint-enable solid/reactivity */
|
|
171
|
+
setBusyCount((c) => c - 1);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// -- Keyboard Shortcuts --
|
|
175
|
+
createEventListener(document, "keydown", (e: KeyboardEvent) => {
|
|
176
|
+
if (e.ctrlKey && e.key === "s") {
|
|
177
|
+
e.preventDefault();
|
|
178
|
+
formRef?.requestSubmit();
|
|
179
|
+
}
|
|
180
|
+
if (e.ctrlKey && e.altKey && e.key === "l") {
|
|
181
|
+
e.preventDefault();
|
|
182
|
+
void handleRefresh();
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// -- Topbar Actions (Page mode) --
|
|
187
|
+
if (topbarCtx) {
|
|
188
|
+
createTopbarActions(() => (
|
|
189
|
+
<>
|
|
190
|
+
<Show when={canEdit() && local.submit}>
|
|
191
|
+
<Button
|
|
192
|
+
size="lg"
|
|
193
|
+
variant="ghost"
|
|
194
|
+
theme="primary"
|
|
195
|
+
onClick={() => formRef?.requestSubmit()}
|
|
196
|
+
>
|
|
197
|
+
<Icon icon={IconDeviceFloppy} class="mr-1" />
|
|
198
|
+
저장
|
|
199
|
+
</Button>
|
|
200
|
+
</Show>
|
|
201
|
+
<Button size="lg" variant="ghost" theme="info" onClick={() => void handleRefresh()}>
|
|
202
|
+
<Icon icon={IconRefresh} class="mr-1" />
|
|
203
|
+
새로고침
|
|
204
|
+
</Button>
|
|
205
|
+
</>
|
|
206
|
+
));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// -- Context --
|
|
210
|
+
const ctx: CrudDetailContext<TData> = {
|
|
211
|
+
data,
|
|
212
|
+
setData,
|
|
213
|
+
info: () => info()!,
|
|
214
|
+
busy: () => busyCount() > 0,
|
|
215
|
+
hasChanges,
|
|
216
|
+
save: handleSave,
|
|
217
|
+
refresh: handleRefresh,
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
// -- Children Resolution --
|
|
221
|
+
const rendered = children(() => local.children(ctx));
|
|
222
|
+
const defs = createMemo(() => {
|
|
223
|
+
const arr = rendered.toArray();
|
|
224
|
+
return {
|
|
225
|
+
tools: arr.find(isCrudDetailToolsDef) as CrudDetailToolsDef | undefined,
|
|
226
|
+
before: arr.find(isCrudDetailBeforeDef) as CrudDetailBeforeDef | undefined,
|
|
227
|
+
after: arr.find(isCrudDetailAfterDef) as CrudDetailAfterDef | undefined,
|
|
228
|
+
};
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
const formContent = () =>
|
|
232
|
+
rendered
|
|
233
|
+
.toArray()
|
|
234
|
+
.filter(
|
|
235
|
+
(el) =>
|
|
236
|
+
!isCrudDetailToolsDef(el) && !isCrudDetailBeforeDef(el) && !isCrudDetailAfterDef(el),
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
// -- Render --
|
|
240
|
+
return (
|
|
241
|
+
<>
|
|
242
|
+
{/* Modal mode: Dialog.Action (refresh button in header) */}
|
|
243
|
+
<Show when={isModal}>
|
|
244
|
+
<Dialog.Action>
|
|
245
|
+
<button
|
|
246
|
+
class="flex items-center px-2 text-base-400 hover:text-base-600"
|
|
247
|
+
onClick={() => void handleRefresh()}
|
|
248
|
+
>
|
|
249
|
+
<Icon icon={IconRefresh} />
|
|
250
|
+
</button>
|
|
251
|
+
</Dialog.Action>
|
|
252
|
+
</Show>
|
|
253
|
+
|
|
254
|
+
<BusyContainer
|
|
255
|
+
ready={ready()}
|
|
256
|
+
busy={busyCount() > 0}
|
|
257
|
+
class={clsx("flex h-full flex-col", local.class)}
|
|
258
|
+
>
|
|
259
|
+
{/* Toolbar (page/control mode) */}
|
|
260
|
+
<Show when={!isModal && canEdit()}>
|
|
261
|
+
<div class="flex gap-2 p-2 pb-0">
|
|
262
|
+
<Show when={local.submit}>
|
|
263
|
+
<Button
|
|
264
|
+
size="sm"
|
|
265
|
+
theme="primary"
|
|
266
|
+
variant="ghost"
|
|
267
|
+
onClick={() => formRef?.requestSubmit()}
|
|
268
|
+
>
|
|
269
|
+
<Icon icon={IconDeviceFloppy} class="mr-1" />
|
|
270
|
+
저장
|
|
271
|
+
</Button>
|
|
272
|
+
</Show>
|
|
273
|
+
<Button size="sm" theme="info" variant="ghost" onClick={() => void handleRefresh()}>
|
|
274
|
+
<Icon icon={IconRefresh} class="mr-1" />
|
|
275
|
+
새로고침
|
|
276
|
+
</Button>
|
|
277
|
+
<Show
|
|
278
|
+
when={local.toggleDelete && info() && !info()!.isNew && (local.deletable ?? true)}
|
|
279
|
+
>
|
|
280
|
+
{(_) => (
|
|
281
|
+
<Button
|
|
282
|
+
size="sm"
|
|
283
|
+
theme="danger"
|
|
284
|
+
variant="ghost"
|
|
285
|
+
onClick={() => void handleToggleDelete()}
|
|
286
|
+
>
|
|
287
|
+
<Icon icon={info()!.isDeleted ? IconTrashOff : IconTrash} class="mr-1" />
|
|
288
|
+
{info()!.isDeleted ? "복구" : "삭제"}
|
|
289
|
+
</Button>
|
|
290
|
+
)}
|
|
291
|
+
</Show>
|
|
292
|
+
<Show when={defs().tools}>{(toolsDef) => toolsDef().children}</Show>
|
|
293
|
+
</div>
|
|
294
|
+
</Show>
|
|
295
|
+
|
|
296
|
+
{/* Before (outside form) */}
|
|
297
|
+
<Show when={defs().before}>{(beforeDef) => beforeDef().children}</Show>
|
|
298
|
+
|
|
299
|
+
{/* Form */}
|
|
300
|
+
<form ref={formRef} class="flex-1 overflow-auto p-2" onSubmit={handleFormSubmit}>
|
|
301
|
+
{formContent()}
|
|
302
|
+
</form>
|
|
303
|
+
|
|
304
|
+
{/* Last modified info */}
|
|
305
|
+
<Show when={info()?.lastModifiedAt}>
|
|
306
|
+
{(_) => (
|
|
307
|
+
<div class="px-2 pb-1 text-xs text-base-400">
|
|
308
|
+
최종 수정: {info()!.lastModifiedAt!.toFormatString("yyyy-MM-dd HH:mm")}
|
|
309
|
+
<Show when={info()?.lastModifiedBy}> ({info()!.lastModifiedBy})</Show>
|
|
310
|
+
</div>
|
|
311
|
+
)}
|
|
312
|
+
</Show>
|
|
313
|
+
|
|
314
|
+
{/* After (outside form) */}
|
|
315
|
+
<Show when={defs().after}>{(afterDef) => afterDef().children}</Show>
|
|
316
|
+
|
|
317
|
+
{/* Modal mode: bottom bar */}
|
|
318
|
+
<Show when={isModal && canEdit()}>
|
|
319
|
+
<div class="flex gap-2 border-t border-base-200 p-2">
|
|
320
|
+
<div class="flex-1" />
|
|
321
|
+
<Show
|
|
322
|
+
when={local.toggleDelete && info() && !info()!.isNew && (local.deletable ?? true)}
|
|
323
|
+
>
|
|
324
|
+
{(_) => (
|
|
325
|
+
<Button size="sm" theme="danger" onClick={() => void handleToggleDelete()}>
|
|
326
|
+
<Icon icon={info()!.isDeleted ? IconTrashOff : IconTrash} class="mr-1" />
|
|
327
|
+
{info()!.isDeleted ? "복구" : "삭제"}
|
|
328
|
+
</Button>
|
|
329
|
+
)}
|
|
330
|
+
</Show>
|
|
331
|
+
<Show when={local.submit}>
|
|
332
|
+
<Button size="sm" theme="primary" onClick={() => formRef?.requestSubmit()}>
|
|
333
|
+
확인
|
|
334
|
+
</Button>
|
|
335
|
+
</Show>
|
|
336
|
+
</div>
|
|
337
|
+
</Show>
|
|
338
|
+
</BusyContainer>
|
|
339
|
+
</>
|
|
340
|
+
);
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
export const CrudDetail = CrudDetailBase as unknown as CrudDetailComponent;
|
|
344
|
+
CrudDetail.Tools = CrudDetailTools;
|
|
345
|
+
CrudDetail.Before = CrudDetailBefore;
|
|
346
|
+
CrudDetail.After = CrudDetailAfter;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { JSX } from "solid-js";
|
|
2
|
+
import type { CrudDetailAfterDef } from "./types";
|
|
3
|
+
|
|
4
|
+
export function isCrudDetailAfterDef(value: unknown): value is CrudDetailAfterDef {
|
|
5
|
+
return (
|
|
6
|
+
value != null &&
|
|
7
|
+
typeof value === "object" &&
|
|
8
|
+
(value as Record<string, unknown>)["__type"] === "crud-detail-after"
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/* eslint-disable solid/reactivity -- plain object 반환 패턴으로 reactive context 불필요 */
|
|
13
|
+
export function CrudDetailAfter(props: { children: JSX.Element }): JSX.Element {
|
|
14
|
+
return {
|
|
15
|
+
__type: "crud-detail-after",
|
|
16
|
+
children: props.children,
|
|
17
|
+
} as unknown as JSX.Element;
|
|
18
|
+
}
|
|
19
|
+
/* eslint-enable solid/reactivity */
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { JSX } from "solid-js";
|
|
2
|
+
import type { CrudDetailBeforeDef } from "./types";
|
|
3
|
+
|
|
4
|
+
export function isCrudDetailBeforeDef(value: unknown): value is CrudDetailBeforeDef {
|
|
5
|
+
return (
|
|
6
|
+
value != null &&
|
|
7
|
+
typeof value === "object" &&
|
|
8
|
+
(value as Record<string, unknown>)["__type"] === "crud-detail-before"
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/* eslint-disable solid/reactivity -- plain object 반환 패턴으로 reactive context 불필요 */
|
|
13
|
+
export function CrudDetailBefore(props: { children: JSX.Element }): JSX.Element {
|
|
14
|
+
return {
|
|
15
|
+
__type: "crud-detail-before",
|
|
16
|
+
children: props.children,
|
|
17
|
+
} as unknown as JSX.Element;
|
|
18
|
+
}
|
|
19
|
+
/* eslint-enable solid/reactivity */
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { JSX } from "solid-js";
|
|
2
|
+
import type { CrudDetailToolsDef } from "./types";
|
|
3
|
+
|
|
4
|
+
export function isCrudDetailToolsDef(value: unknown): value is CrudDetailToolsDef {
|
|
5
|
+
return (
|
|
6
|
+
value != null &&
|
|
7
|
+
typeof value === "object" &&
|
|
8
|
+
(value as Record<string, unknown>)["__type"] === "crud-detail-tools"
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/* eslint-disable solid/reactivity -- plain object 반환 패턴으로 reactive context 불필요 */
|
|
13
|
+
export function CrudDetailTools(props: { children: JSX.Element }): JSX.Element {
|
|
14
|
+
return {
|
|
15
|
+
__type: "crud-detail-tools",
|
|
16
|
+
children: props.children,
|
|
17
|
+
} as unknown as JSX.Element;
|
|
18
|
+
}
|
|
19
|
+
/* eslint-enable solid/reactivity */
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { JSX } from "solid-js";
|
|
2
|
+
import type { SetStoreFunction } from "solid-js/store";
|
|
3
|
+
import type { DateTime } from "@simplysm/core-common";
|
|
4
|
+
|
|
5
|
+
// ── Detail Info ──
|
|
6
|
+
|
|
7
|
+
export interface CrudDetailInfo {
|
|
8
|
+
isNew: boolean;
|
|
9
|
+
isDeleted: boolean;
|
|
10
|
+
lastModifiedAt?: DateTime;
|
|
11
|
+
lastModifiedBy?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// ── Context ──
|
|
15
|
+
|
|
16
|
+
export interface CrudDetailContext<TData> {
|
|
17
|
+
data: TData;
|
|
18
|
+
setData: SetStoreFunction<TData>;
|
|
19
|
+
info: () => CrudDetailInfo;
|
|
20
|
+
busy: () => boolean;
|
|
21
|
+
hasChanges: () => boolean;
|
|
22
|
+
save: () => Promise<void>;
|
|
23
|
+
refresh: () => Promise<void>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ── Props ──
|
|
27
|
+
|
|
28
|
+
export interface CrudDetailProps<TData extends object> {
|
|
29
|
+
load: () => Promise<{ data: TData; info: CrudDetailInfo }>;
|
|
30
|
+
children: (ctx: CrudDetailContext<TData>) => JSX.Element;
|
|
31
|
+
|
|
32
|
+
submit?: (data: TData) => Promise<boolean | undefined>;
|
|
33
|
+
toggleDelete?: (del: boolean) => Promise<boolean | undefined>;
|
|
34
|
+
editable?: boolean;
|
|
35
|
+
deletable?: boolean;
|
|
36
|
+
|
|
37
|
+
data?: TData;
|
|
38
|
+
onDataChange?: (data: TData) => void;
|
|
39
|
+
|
|
40
|
+
class?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ── Sub-component Defs ──
|
|
44
|
+
|
|
45
|
+
export interface CrudDetailToolsDef {
|
|
46
|
+
__type: "crud-detail-tools";
|
|
47
|
+
children: JSX.Element;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface CrudDetailBeforeDef {
|
|
51
|
+
__type: "crud-detail-before";
|
|
52
|
+
children: JSX.Element;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface CrudDetailAfterDef {
|
|
56
|
+
__type: "crud-detail-after";
|
|
57
|
+
children: JSX.Element;
|
|
58
|
+
}
|