@simplysm/solid 13.0.53 → 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 +6 -2
- 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.css +28 -10
- 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/Invalid.js +1 -1
- 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/DatePicker.d.ts.map +1 -1
- package/dist/components/form-control/field/DatePicker.js +3 -2
- package/dist/components/form-control/field/DatePicker.js.map +2 -2
- package/dist/components/form-control/field/DateTimePicker.d.ts.map +1 -1
- package/dist/components/form-control/field/DateTimePicker.js +3 -2
- package/dist/components/form-control/field/DateTimePicker.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/field/TimePicker.d.ts.map +1 -1
- package/dist/components/form-control/field/TimePicker.js +3 -2
- package/dist/components/form-control/field/TimePicker.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/form-control/state-preset/StatePreset.d.ts.map +1 -1
- package/dist/components/form-control/state-preset/StatePreset.js +1 -1
- package/dist/components/form-control/state-preset/StatePreset.js.map +1 -1
- package/dist/components/layout/topbar/Topbar.d.ts +2 -0
- package/dist/components/layout/topbar/Topbar.d.ts.map +1 -1
- package/dist/components/layout/topbar/Topbar.js +2 -0
- package/dist/components/layout/topbar/Topbar.js.map +2 -2
- package/dist/components/layout/topbar/TopbarActions.d.ts +3 -0
- package/dist/components/layout/topbar/TopbarActions.d.ts.map +1 -0
- package/dist/components/layout/topbar/TopbarActions.js +17 -0
- package/dist/components/layout/topbar/TopbarActions.js.map +6 -0
- package/dist/components/layout/topbar/TopbarContainer.d.ts +1 -1
- package/dist/components/layout/topbar/TopbarContainer.d.ts.map +1 -1
- package/dist/components/layout/topbar/TopbarContainer.js +21 -12
- package/dist/components/layout/topbar/TopbarContainer.js.map +2 -2
- package/dist/components/layout/topbar/TopbarContext.d.ts +9 -0
- package/dist/components/layout/topbar/TopbarContext.d.ts.map +1 -0
- package/dist/components/layout/topbar/TopbarContext.js +29 -0
- package/dist/components/layout/topbar/TopbarContext.js.map +6 -0
- 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 +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -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/docs/layout.md +70 -1
- 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.css +28 -10
- 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/Invalid.tsx +1 -1
- package/src/components/form-control/combobox/Combobox.tsx +109 -133
- 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/DatePicker.tsx +4 -1
- package/src/components/form-control/field/DateTimePicker.tsx +3 -0
- package/src/components/form-control/field/Field.styles.ts +1 -0
- package/src/components/form-control/field/NumberInput.tsx +131 -86
- package/src/components/form-control/field/TextInput.tsx +139 -88
- package/src/components/form-control/field/TimePicker.tsx +3 -0
- 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/form-control/state-preset/StatePreset.tsx +1 -0
- package/src/components/layout/topbar/Topbar.tsx +3 -0
- package/src/components/layout/topbar/TopbarActions.tsx +8 -0
- package/src/components/layout/topbar/TopbarContainer.tsx +9 -5
- package/src/components/layout/topbar/TopbarContext.ts +36 -0
- 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 +6 -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/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/docs/layout.md
CHANGED
|
@@ -127,10 +127,79 @@ const userMenus: TopbarUserMenu[] = [
|
|
|
127
127
|
```
|
|
128
128
|
|
|
129
129
|
**Sub-components:**
|
|
130
|
-
- `Topbar.Container` -- Container wrapping main content
|
|
130
|
+
- `Topbar.Container` -- Container wrapping topbar and main content, provides `TopbarContext`
|
|
131
|
+
- `Topbar.Actions` -- Slot outlet that renders actions registered via `createTopbarActions`
|
|
131
132
|
- `Topbar.Menu` -- Menu items list
|
|
132
133
|
- `Topbar.User` -- User menu (dropdown)
|
|
133
134
|
|
|
135
|
+
### Topbar Actions Slot
|
|
136
|
+
|
|
137
|
+
A slot pattern that lets child pages inject action buttons (save, delete, etc.) into the topbar. The parent layout defines **where** actions appear, child pages define **what** to show.
|
|
138
|
+
|
|
139
|
+
**Layout (define the slot):**
|
|
140
|
+
|
|
141
|
+
```tsx
|
|
142
|
+
import { Topbar } from "@simplysm/solid";
|
|
143
|
+
|
|
144
|
+
<Topbar.Container>
|
|
145
|
+
<Topbar>
|
|
146
|
+
<span>Title</span>
|
|
147
|
+
<Topbar.Actions />
|
|
148
|
+
<div class="flex-1" />
|
|
149
|
+
</Topbar>
|
|
150
|
+
<main class="flex-1 overflow-auto p-4">
|
|
151
|
+
{props.children}
|
|
152
|
+
</main>
|
|
153
|
+
</Topbar.Container>
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
**Child page (fill the slot):**
|
|
157
|
+
|
|
158
|
+
```tsx
|
|
159
|
+
import { createTopbarActions, Button } from "@simplysm/solid";
|
|
160
|
+
|
|
161
|
+
function UserPage() {
|
|
162
|
+
createTopbarActions(() => (
|
|
163
|
+
<>
|
|
164
|
+
<Button theme="primary">Save</Button>
|
|
165
|
+
<Button>Cancel</Button>
|
|
166
|
+
</>
|
|
167
|
+
));
|
|
168
|
+
|
|
169
|
+
return <div>...</div>;
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
When `UserPage` mounts, the buttons appear in the topbar. When it unmounts, the actions are automatically cleaned up via `onCleanup`.
|
|
174
|
+
|
|
175
|
+
**createTopbarActions(accessor: () => JSX.Element): void**
|
|
176
|
+
|
|
177
|
+
Registers actions in the nearest `Topbar.Container` scope. Automatically removes actions on component unmount. Must be called inside `Topbar.Container`.
|
|
178
|
+
|
|
179
|
+
**useTopbarActionsAccessor(): Accessor<JSX.Element | undefined>**
|
|
180
|
+
|
|
181
|
+
Returns the actions accessor directly. For advanced use cases such as building custom topbar components. Must be called inside `Topbar.Container`.
|
|
182
|
+
|
|
183
|
+
```tsx
|
|
184
|
+
import { useTopbarActionsAccessor } from "@simplysm/solid";
|
|
185
|
+
|
|
186
|
+
const actions = useTopbarActionsAccessor();
|
|
187
|
+
// actions() returns the currently registered JSX.Element or undefined
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
**TopbarContext:**
|
|
191
|
+
|
|
192
|
+
`TopbarContext` is exported for building custom topbar layouts. `Topbar.Container` wraps children with `TopbarContext.Provider`, sharing `actions` accessor and `setActions` setter.
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
import { TopbarContext } from "@simplysm/solid";
|
|
196
|
+
|
|
197
|
+
interface TopbarContextValue {
|
|
198
|
+
actions: Accessor<JSX.Element | undefined>;
|
|
199
|
+
setActions: Setter<JSX.Element | undefined>;
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
134
203
|
**TopbarMenuItem type:**
|
|
135
204
|
|
|
136
205
|
```typescript
|
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
|
+
}
|