@fgv/ts-app-shell 5.1.0-1
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 +26 -0
- package/dist/index.browser.js +3 -0
- package/dist/index.js +43 -0
- package/dist/packlets/ai-assist/index.js +6 -0
- package/dist/packlets/ai-assist/useAiAssist.js +219 -0
- package/dist/packlets/cascade/CascadeContainer.js +83 -0
- package/dist/packlets/cascade/ComparisonView.js +48 -0
- package/dist/packlets/cascade/EntityTabLayout.js +104 -0
- package/dist/packlets/cascade/MobileCascadeStack.js +63 -0
- package/dist/packlets/cascade/index.js +37 -0
- package/dist/packlets/cascade/model.js +30 -0
- package/dist/packlets/cascade/useCascadeOps.js +206 -0
- package/dist/packlets/cascade/useCascadeTransitions.js +58 -0
- package/dist/packlets/detail/DetailHelpers.js +103 -0
- package/dist/packlets/detail/index.js +6 -0
- package/dist/packlets/drop-zone/JsonDropZone.js +112 -0
- package/dist/packlets/drop-zone/index.js +6 -0
- package/dist/packlets/editing/EditFieldHelpers.js +130 -0
- package/dist/packlets/editing/MultiActionButton.js +73 -0
- package/dist/packlets/editing/NumericInput.js +119 -0
- package/dist/packlets/editing/TypeaheadInput.js +207 -0
- package/dist/packlets/editing/index.js +10 -0
- package/dist/packlets/editing/useTypeaheadMatch.js +102 -0
- package/dist/packlets/keyboard/index.js +7 -0
- package/dist/packlets/keyboard/registry.js +133 -0
- package/dist/packlets/keyboard/useKeyboardShortcuts.js +117 -0
- package/dist/packlets/messages/MessagesContext.js +76 -0
- package/dist/packlets/messages/MessagesLogger.js +103 -0
- package/dist/packlets/messages/StatusBar.js +154 -0
- package/dist/packlets/messages/Toast.js +68 -0
- package/dist/packlets/messages/index.js +11 -0
- package/dist/packlets/messages/model.js +56 -0
- package/dist/packlets/messages/useLogReporter.js +66 -0
- package/dist/packlets/modal/ConfirmDialog.js +78 -0
- package/dist/packlets/modal/Modal.js +55 -0
- package/dist/packlets/modal/index.js +7 -0
- package/dist/packlets/print/PrintEnclosure.js +60 -0
- package/dist/packlets/print/index.js +7 -0
- package/dist/packlets/print/openPrintWindow.js +112 -0
- package/dist/packlets/responsive/ResponsiveProvider.js +56 -0
- package/dist/packlets/responsive/index.js +7 -0
- package/dist/packlets/responsive/useResponsiveLayout.js +118 -0
- package/dist/packlets/selectors/EntityRow.js +276 -0
- package/dist/packlets/selectors/PreferredSelector.js +251 -0
- package/dist/packlets/selectors/index.js +24 -0
- package/dist/packlets/sidebar/CollectionSection.js +107 -0
- package/dist/packlets/sidebar/EntityList.js +164 -0
- package/dist/packlets/sidebar/FilterBar.js +42 -0
- package/dist/packlets/sidebar/FilterRow.js +182 -0
- package/dist/packlets/sidebar/GroupedEntityList.js +183 -0
- package/dist/packlets/sidebar/SearchBar.js +34 -0
- package/dist/packlets/sidebar/SidebarLayout.js +62 -0
- package/dist/packlets/sidebar/index.js +12 -0
- package/dist/packlets/theme/ThemeProvider.js +141 -0
- package/dist/packlets/theme/index.js +6 -0
- package/dist/packlets/top-bar/ModeSelector.js +46 -0
- package/dist/packlets/top-bar/TabBar.js +37 -0
- package/dist/packlets/top-bar/index.js +7 -0
- package/dist/packlets/url-sync/index.js +6 -0
- package/dist/packlets/url-sync/useUrlSync.js +157 -0
- package/eslint.config.js +22 -0
- package/lib/index.browser.d.ts +2 -0
- package/lib/index.browser.js +19 -0
- package/lib/index.d.ts +28 -0
- package/lib/index.js +59 -0
- package/lib/packlets/ai-assist/index.d.ts +6 -0
- package/lib/packlets/ai-assist/index.js +11 -0
- package/lib/packlets/ai-assist/useAiAssist.d.ts +77 -0
- package/lib/packlets/ai-assist/useAiAssist.js +223 -0
- package/lib/packlets/cascade/CascadeContainer.d.ts +44 -0
- package/lib/packlets/cascade/CascadeContainer.js +119 -0
- package/lib/packlets/cascade/ComparisonView.d.ts +35 -0
- package/lib/packlets/cascade/ComparisonView.js +54 -0
- package/lib/packlets/cascade/EntityTabLayout.d.ts +47 -0
- package/lib/packlets/cascade/EntityTabLayout.js +110 -0
- package/lib/packlets/cascade/MobileCascadeStack.d.ts +20 -0
- package/lib/packlets/cascade/MobileCascadeStack.js +99 -0
- package/lib/packlets/cascade/index.d.ts +12 -0
- package/lib/packlets/cascade/index.js +48 -0
- package/lib/packlets/cascade/model.d.ts +57 -0
- package/lib/packlets/cascade/model.js +33 -0
- package/lib/packlets/cascade/useCascadeOps.d.ts +111 -0
- package/lib/packlets/cascade/useCascadeOps.js +209 -0
- package/lib/packlets/cascade/useCascadeTransitions.d.ts +19 -0
- package/lib/packlets/cascade/useCascadeTransitions.js +62 -0
- package/lib/packlets/detail/DetailHelpers.d.ts +83 -0
- package/lib/packlets/detail/DetailHelpers.js +113 -0
- package/lib/packlets/detail/index.d.ts +6 -0
- package/lib/packlets/detail/index.js +14 -0
- package/lib/packlets/drop-zone/JsonDropZone.d.ts +40 -0
- package/lib/packlets/drop-zone/JsonDropZone.js +149 -0
- package/lib/packlets/drop-zone/index.d.ts +6 -0
- package/lib/packlets/drop-zone/index.js +10 -0
- package/lib/packlets/editing/EditFieldHelpers.d.ts +171 -0
- package/lib/packlets/editing/EditFieldHelpers.js +144 -0
- package/lib/packlets/editing/MultiActionButton.d.ts +45 -0
- package/lib/packlets/editing/MultiActionButton.js +109 -0
- package/lib/packlets/editing/NumericInput.d.ts +47 -0
- package/lib/packlets/editing/NumericInput.js +155 -0
- package/lib/packlets/editing/TypeaheadInput.d.ts +46 -0
- package/lib/packlets/editing/TypeaheadInput.js +243 -0
- package/lib/packlets/editing/index.d.ts +10 -0
- package/lib/packlets/editing/index.js +26 -0
- package/lib/packlets/editing/useTypeaheadMatch.d.ts +42 -0
- package/lib/packlets/editing/useTypeaheadMatch.js +105 -0
- package/lib/packlets/keyboard/index.d.ts +7 -0
- package/lib/packlets/keyboard/index.js +15 -0
- package/lib/packlets/keyboard/registry.d.ts +92 -0
- package/lib/packlets/keyboard/registry.js +138 -0
- package/lib/packlets/keyboard/useKeyboardShortcuts.d.ts +50 -0
- package/lib/packlets/keyboard/useKeyboardShortcuts.js +155 -0
- package/lib/packlets/messages/MessagesContext.d.ts +40 -0
- package/lib/packlets/messages/MessagesContext.js +113 -0
- package/lib/packlets/messages/MessagesLogger.d.ts +50 -0
- package/lib/packlets/messages/MessagesLogger.js +107 -0
- package/lib/packlets/messages/StatusBar.d.ts +22 -0
- package/lib/packlets/messages/StatusBar.js +190 -0
- package/lib/packlets/messages/Toast.d.ts +31 -0
- package/lib/packlets/messages/Toast.js +105 -0
- package/lib/packlets/messages/index.d.ts +11 -0
- package/lib/packlets/messages/index.js +24 -0
- package/lib/packlets/messages/model.d.ts +59 -0
- package/lib/packlets/messages/model.js +61 -0
- package/lib/packlets/messages/useLogReporter.d.ts +22 -0
- package/lib/packlets/messages/useLogReporter.js +69 -0
- package/lib/packlets/modal/ConfirmDialog.d.ts +39 -0
- package/lib/packlets/modal/ConfirmDialog.js +114 -0
- package/lib/packlets/modal/Modal.d.ts +22 -0
- package/lib/packlets/modal/Modal.js +91 -0
- package/lib/packlets/modal/index.d.ts +7 -0
- package/lib/packlets/modal/index.js +12 -0
- package/lib/packlets/print/PrintEnclosure.d.ts +33 -0
- package/lib/packlets/print/PrintEnclosure.js +96 -0
- package/lib/packlets/print/index.d.ts +7 -0
- package/lib/packlets/print/index.js +12 -0
- package/lib/packlets/print/openPrintWindow.d.ts +35 -0
- package/lib/packlets/print/openPrintWindow.js +118 -0
- package/lib/packlets/responsive/ResponsiveProvider.d.ts +35 -0
- package/lib/packlets/responsive/ResponsiveProvider.js +93 -0
- package/lib/packlets/responsive/index.d.ts +7 -0
- package/lib/packlets/responsive/index.js +13 -0
- package/lib/packlets/responsive/useResponsiveLayout.d.ts +48 -0
- package/lib/packlets/responsive/useResponsiveLayout.js +121 -0
- package/lib/packlets/selectors/EntityRow.d.ts +45 -0
- package/lib/packlets/selectors/EntityRow.js +315 -0
- package/lib/packlets/selectors/PreferredSelector.d.ts +50 -0
- package/lib/packlets/selectors/PreferredSelector.js +287 -0
- package/lib/packlets/selectors/index.d.ts +5 -0
- package/lib/packlets/selectors/index.js +29 -0
- package/lib/packlets/sidebar/CollectionSection.d.ts +82 -0
- package/lib/packlets/sidebar/CollectionSection.js +143 -0
- package/lib/packlets/sidebar/EntityList.d.ts +105 -0
- package/lib/packlets/sidebar/EntityList.js +200 -0
- package/lib/packlets/sidebar/FilterBar.d.ts +26 -0
- package/lib/packlets/sidebar/FilterBar.js +48 -0
- package/lib/packlets/sidebar/FilterRow.d.ts +42 -0
- package/lib/packlets/sidebar/FilterRow.js +218 -0
- package/lib/packlets/sidebar/GroupedEntityList.d.ts +59 -0
- package/lib/packlets/sidebar/GroupedEntityList.js +219 -0
- package/lib/packlets/sidebar/SearchBar.d.ts +19 -0
- package/lib/packlets/sidebar/SearchBar.js +40 -0
- package/lib/packlets/sidebar/SidebarLayout.d.ts +28 -0
- package/lib/packlets/sidebar/SidebarLayout.js +98 -0
- package/lib/packlets/sidebar/index.d.ts +12 -0
- package/lib/packlets/sidebar/index.js +22 -0
- package/lib/packlets/theme/ThemeProvider.d.ts +68 -0
- package/lib/packlets/theme/ThemeProvider.js +178 -0
- package/lib/packlets/theme/index.d.ts +6 -0
- package/lib/packlets/theme/index.js +11 -0
- package/lib/packlets/top-bar/ModeSelector.d.ts +38 -0
- package/lib/packlets/top-bar/ModeSelector.js +52 -0
- package/lib/packlets/top-bar/TabBar.d.ts +31 -0
- package/lib/packlets/top-bar/TabBar.js +43 -0
- package/lib/packlets/top-bar/index.d.ts +7 -0
- package/lib/packlets/top-bar/index.js +12 -0
- package/lib/packlets/url-sync/index.d.ts +6 -0
- package/lib/packlets/url-sync/index.js +12 -0
- package/lib/packlets/url-sync/useUrlSync.d.ts +75 -0
- package/lib/packlets/url-sync/useUrlSync.js +162 -0
- package/package.json +82 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2026 Erik Fortune
|
|
3
|
+
*
|
|
4
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
|
+
* of this software and associated documentation files (the "Software"), to deal
|
|
6
|
+
* in the Software without restriction, including without limitation the rights
|
|
7
|
+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
* copies of the Software, and to permit persons to whom the Software is
|
|
9
|
+
* furnished to do so, subject to the following conditions:
|
|
10
|
+
*
|
|
11
|
+
* The above copyright notice and this permission notice shall be included in all
|
|
12
|
+
* copies or substantial portions of the Software.
|
|
13
|
+
*
|
|
14
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15
|
+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
16
|
+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
17
|
+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
18
|
+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
19
|
+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
20
|
+
* SOFTWARE.
|
|
21
|
+
*/
|
|
22
|
+
// ============================================================================
|
|
23
|
+
// Cascade Entry Base
|
|
24
|
+
// ============================================================================
|
|
25
|
+
/**
|
|
26
|
+
* Sentinel value used as entityId when creating a new entity.
|
|
27
|
+
* @public
|
|
28
|
+
*/
|
|
29
|
+
export const CASCADE_NEW_ENTITY_ID = '__new__';
|
|
30
|
+
//# sourceMappingURL=model.js.map
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2026 Erik Fortune
|
|
3
|
+
*
|
|
4
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
|
+
* of this software and associated documentation files (the "Software"), to deal
|
|
6
|
+
* in the Software without restriction, including without limitation the rights
|
|
7
|
+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
* copies of the Software, and to permit persons to whom the Software is
|
|
9
|
+
* furnished to do so, subject to the following conditions:
|
|
10
|
+
*
|
|
11
|
+
* The above copyright notice and this permission notice shall be included in all
|
|
12
|
+
* copies or substantial portions of the Software.
|
|
13
|
+
*
|
|
14
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15
|
+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
16
|
+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
17
|
+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
18
|
+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
19
|
+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
20
|
+
* SOFTWARE.
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* Semantic cascade operations hook.
|
|
24
|
+
*
|
|
25
|
+
* Encodes cascade navigation rules so tabs declare intent (select, drillDown,
|
|
26
|
+
* openEditor, pop) rather than manually computing cascade stacks.
|
|
27
|
+
*
|
|
28
|
+
* The hook is generic over the entry type `TEntry extends ICascadeEntryBase`.
|
|
29
|
+
* Domain-specific applications pass their extended entry type to preserve
|
|
30
|
+
* full type information through the cascade operations.
|
|
31
|
+
*
|
|
32
|
+
* @packageDocumentation
|
|
33
|
+
*/
|
|
34
|
+
import { useCallback, useMemo } from 'react';
|
|
35
|
+
import { fail, succeed } from '@fgv/ts-utils';
|
|
36
|
+
// ============================================================================
|
|
37
|
+
// Helpers
|
|
38
|
+
// ============================================================================
|
|
39
|
+
function isEditOrCreate(mode) {
|
|
40
|
+
return mode === 'edit' || mode === 'create';
|
|
41
|
+
}
|
|
42
|
+
function findEditorsAbove(stack, depth) {
|
|
43
|
+
const result = [];
|
|
44
|
+
for (let i = depth + 1; i < stack.length; i++) {
|
|
45
|
+
if (isEditOrCreate(stack[i].mode)) {
|
|
46
|
+
result.push({ depth: i, entry: stack[i] });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
// ============================================================================
|
|
52
|
+
// Hook
|
|
53
|
+
// ============================================================================
|
|
54
|
+
/**
|
|
55
|
+
* Hook providing semantic cascade operations.
|
|
56
|
+
*
|
|
57
|
+
* Takes the cascade stack and a squash function as parameters, making it
|
|
58
|
+
* independent of any specific state management solution. Domain-specific
|
|
59
|
+
* applications typically provide a convenience wrapper that reads these
|
|
60
|
+
* values from their own store.
|
|
61
|
+
*
|
|
62
|
+
* @typeParam TEntry - The cascade entry type. Inferred from `cascadeStack`.
|
|
63
|
+
* @public
|
|
64
|
+
*/
|
|
65
|
+
export function useCascadeOps(cascadeStack, squashCascade) {
|
|
66
|
+
const asTEntry = (entry) => entry;
|
|
67
|
+
const select = useCallback((entry) => {
|
|
68
|
+
var _a;
|
|
69
|
+
const created = asTEntry(Object.assign(Object.assign({}, entry), { mode: (_a = entry.mode) !== null && _a !== void 0 ? _a : 'view', origin: 'primary' }));
|
|
70
|
+
squashCascade([created]);
|
|
71
|
+
return succeed({ depth: 0, entry: created });
|
|
72
|
+
},
|
|
73
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
74
|
+
[squashCascade]);
|
|
75
|
+
const drillDown = useCallback((fromDepth, entry) => {
|
|
76
|
+
var _a;
|
|
77
|
+
const nextEntry = cascadeStack[fromDepth + 1];
|
|
78
|
+
if ((nextEntry === null || nextEntry === void 0 ? void 0 : nextEntry.entityType) === entry.entityType && nextEntry.entityId === entry.entityId) {
|
|
79
|
+
// Toggle: collapse if same entity is already at fromDepth + 1
|
|
80
|
+
squashCascade(cascadeStack.slice(0, fromDepth + 1));
|
|
81
|
+
return succeed({ depth: fromDepth + 1, entry: nextEntry });
|
|
82
|
+
}
|
|
83
|
+
const created = asTEntry(Object.assign(Object.assign({}, entry), { mode: (_a = entry.mode) !== null && _a !== void 0 ? _a : 'view', origin: 'nested' }));
|
|
84
|
+
squashCascade([...cascadeStack.slice(0, fromDepth + 1), created]);
|
|
85
|
+
return succeed({ depth: fromDepth + 1, entry: created });
|
|
86
|
+
},
|
|
87
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
88
|
+
[cascadeStack, squashCascade]);
|
|
89
|
+
const editorsAbove = useCallback((depth) => {
|
|
90
|
+
return findEditorsAbove(cascadeStack, depth);
|
|
91
|
+
}, [cascadeStack]);
|
|
92
|
+
const openEditor = useCallback((depth) => {
|
|
93
|
+
const entry = cascadeStack[depth];
|
|
94
|
+
if (!entry) {
|
|
95
|
+
return fail(`depth ${depth} out of bounds for cascade stack of length ${cascadeStack.length}`);
|
|
96
|
+
}
|
|
97
|
+
// Check for editors above the target depth
|
|
98
|
+
const conflicts = findEditorsAbove(cascadeStack, depth);
|
|
99
|
+
if (conflicts.length > 0) {
|
|
100
|
+
const names = conflicts.map((c) => `${c.entry.entityType}(${c.entry.mode}) at depth ${c.depth}`);
|
|
101
|
+
return fail(`blocked by editors above: ${names.join(', ')}`);
|
|
102
|
+
}
|
|
103
|
+
// Trim everything above depth (downstream views become incoherent),
|
|
104
|
+
// switch target to edit mode, preserving origin
|
|
105
|
+
const edited = asTEntry(Object.assign(Object.assign({}, entry), { mode: 'edit' }));
|
|
106
|
+
squashCascade([...cascadeStack.slice(0, depth), edited]);
|
|
107
|
+
return succeed({ depth, entry: edited });
|
|
108
|
+
},
|
|
109
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
110
|
+
[cascadeStack, squashCascade]);
|
|
111
|
+
const openNested = useCallback((fromDepth, entry) => {
|
|
112
|
+
const created = asTEntry(Object.assign(Object.assign({}, entry), { origin: 'nested' }));
|
|
113
|
+
const newDepth = fromDepth + 1;
|
|
114
|
+
squashCascade([...cascadeStack.slice(0, newDepth), created]);
|
|
115
|
+
return succeed({ depth: newDepth, entry: created });
|
|
116
|
+
},
|
|
117
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
118
|
+
[cascadeStack, squashCascade]);
|
|
119
|
+
const pop = useCallback(() => {
|
|
120
|
+
if (cascadeStack.length === 0) {
|
|
121
|
+
return fail('cannot pop from empty cascade stack');
|
|
122
|
+
}
|
|
123
|
+
const removed = cascadeStack[cascadeStack.length - 1];
|
|
124
|
+
squashCascade(cascadeStack.slice(0, -1));
|
|
125
|
+
return succeed({ depth: cascadeStack.length - 1, entry: removed });
|
|
126
|
+
}, [cascadeStack, squashCascade]);
|
|
127
|
+
const popToView = useCallback((depth, refreshedEntity) => {
|
|
128
|
+
const entry = cascadeStack[depth];
|
|
129
|
+
if (!entry) {
|
|
130
|
+
return fail(`depth ${depth} out of bounds for cascade stack of length ${cascadeStack.length}`);
|
|
131
|
+
}
|
|
132
|
+
const viewed = asTEntry(Object.assign(Object.assign(Object.assign({}, entry), { mode: 'view' }), (refreshedEntity !== undefined ? { entity: refreshedEntity } : {})));
|
|
133
|
+
squashCascade([...cascadeStack.slice(0, depth), viewed]);
|
|
134
|
+
return succeed({ depth, entry: viewed });
|
|
135
|
+
},
|
|
136
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
137
|
+
[cascadeStack, squashCascade]);
|
|
138
|
+
const trimTo = useCallback((depth) => {
|
|
139
|
+
const trimmed = Math.max(0, depth);
|
|
140
|
+
const newStack = cascadeStack.slice(0, trimmed);
|
|
141
|
+
squashCascade(newStack);
|
|
142
|
+
if (newStack.length === 0) {
|
|
143
|
+
return fail('cascade stack is empty after trim');
|
|
144
|
+
}
|
|
145
|
+
const lastIdx = newStack.length - 1;
|
|
146
|
+
return succeed({ depth: lastIdx, entry: newStack[lastIdx] });
|
|
147
|
+
}, [cascadeStack, squashCascade]);
|
|
148
|
+
const hasUnsavedEditors = useCallback(() => {
|
|
149
|
+
return cascadeStack.some((e) => isEditOrCreate(e.mode));
|
|
150
|
+
}, [cascadeStack]);
|
|
151
|
+
const canSaveOrCancel = useCallback((depth) => {
|
|
152
|
+
return findEditorsAbove(cascadeStack, depth).length === 0;
|
|
153
|
+
}, [cascadeStack]);
|
|
154
|
+
const clear = useCallback(() => {
|
|
155
|
+
squashCascade([]);
|
|
156
|
+
}, [squashCascade]);
|
|
157
|
+
const clearIf = useCallback((predicate) => {
|
|
158
|
+
if (cascadeStack.some(predicate)) {
|
|
159
|
+
squashCascade([]);
|
|
160
|
+
}
|
|
161
|
+
}, [cascadeStack, squashCascade]);
|
|
162
|
+
const clearById = useCallback((entityId) => {
|
|
163
|
+
clearIf((e) => e.entityId === entityId);
|
|
164
|
+
}, [clearIf]);
|
|
165
|
+
const find = useCallback((predicate) => {
|
|
166
|
+
const depth = cascadeStack.findIndex(predicate);
|
|
167
|
+
if (depth < 0) {
|
|
168
|
+
return fail('no matching entry in cascade stack');
|
|
169
|
+
}
|
|
170
|
+
return succeed({ depth, entry: cascadeStack[depth] });
|
|
171
|
+
}, [cascadeStack]);
|
|
172
|
+
return useMemo(() => ({
|
|
173
|
+
select,
|
|
174
|
+
drillDown,
|
|
175
|
+
openEditor,
|
|
176
|
+
openNested,
|
|
177
|
+
pop,
|
|
178
|
+
popToView,
|
|
179
|
+
trimTo,
|
|
180
|
+
editorsAbove,
|
|
181
|
+
hasUnsavedEditors,
|
|
182
|
+
canSaveOrCancel,
|
|
183
|
+
clear,
|
|
184
|
+
clearById,
|
|
185
|
+
clearIf,
|
|
186
|
+
find,
|
|
187
|
+
stack: cascadeStack
|
|
188
|
+
}), [
|
|
189
|
+
select,
|
|
190
|
+
drillDown,
|
|
191
|
+
openEditor,
|
|
192
|
+
openNested,
|
|
193
|
+
pop,
|
|
194
|
+
popToView,
|
|
195
|
+
trimTo,
|
|
196
|
+
editorsAbove,
|
|
197
|
+
hasUnsavedEditors,
|
|
198
|
+
canSaveOrCancel,
|
|
199
|
+
clear,
|
|
200
|
+
clearById,
|
|
201
|
+
clearIf,
|
|
202
|
+
find,
|
|
203
|
+
cascadeStack
|
|
204
|
+
]);
|
|
205
|
+
}
|
|
206
|
+
//# sourceMappingURL=useCascadeOps.js.map
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2026 Erik Fortune
|
|
3
|
+
*
|
|
4
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
|
+
* of this software and associated documentation files (the "Software"), to deal
|
|
6
|
+
* in the Software without restriction, including without limitation the rights
|
|
7
|
+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
* copies of the Software, and to permit persons to whom the Software is
|
|
9
|
+
* furnished to do so, subject to the following conditions:
|
|
10
|
+
*
|
|
11
|
+
* The above copyright notice and this permission notice shall be included in all
|
|
12
|
+
* copies or substantial portions of the Software.
|
|
13
|
+
*
|
|
14
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15
|
+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
16
|
+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
17
|
+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
18
|
+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
19
|
+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
20
|
+
* SOFTWARE.
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* Shared cascade transition hooks for entity tabs.
|
|
24
|
+
*
|
|
25
|
+
* @packageDocumentation
|
|
26
|
+
*/
|
|
27
|
+
import { useCallback } from 'react';
|
|
28
|
+
/**
|
|
29
|
+
* Returns a depth-aware squash helper used by cascade views.
|
|
30
|
+
*
|
|
31
|
+
* Keeps stack entries up to and including `depth`, then appends `entry`.
|
|
32
|
+
*
|
|
33
|
+
* @public
|
|
34
|
+
*/
|
|
35
|
+
export function useSquashAt(cascadeStack, squashCascade) {
|
|
36
|
+
return useCallback((depth, entry) => {
|
|
37
|
+
squashCascade([...cascadeStack.slice(0, depth + 1), entry]);
|
|
38
|
+
}, [cascadeStack, squashCascade]);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Returns a shared drill-down toggle helper for cascade columns.
|
|
42
|
+
*
|
|
43
|
+
* If the target entry is already immediately to the right of `depth`, it collapses
|
|
44
|
+
* back to `depth`. Otherwise, it appends a new view entry at `depth + 1`.
|
|
45
|
+
*
|
|
46
|
+
* @public
|
|
47
|
+
*/
|
|
48
|
+
export function useCascadeDrillDown(cascadeStack, squashCascade, squashAt) {
|
|
49
|
+
return useCallback((depth, entityType, entityId, extra) => {
|
|
50
|
+
const nextEntry = cascadeStack[depth + 1];
|
|
51
|
+
if ((nextEntry === null || nextEntry === void 0 ? void 0 : nextEntry.entityType) === entityType && nextEntry.entityId === entityId) {
|
|
52
|
+
squashCascade(cascadeStack.slice(0, depth + 1));
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
squashAt(depth, Object.assign({ entityType, entityId, mode: 'view' }, extra));
|
|
56
|
+
}, [cascadeStack, squashAt, squashCascade]);
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=useCascadeTransitions.js.map
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2026 Erik Fortune
|
|
3
|
+
*
|
|
4
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
|
+
* of this software and associated documentation files (the "Software"), to deal
|
|
6
|
+
* in the Software without restriction, including without limitation the rights
|
|
7
|
+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
* copies of the Software, and to permit persons to whom the Software is
|
|
9
|
+
* furnished to do so, subject to the following conditions:
|
|
10
|
+
*
|
|
11
|
+
* The above copyright notice and this permission notice shall be included in all
|
|
12
|
+
* copies or substantial portions of the Software.
|
|
13
|
+
*
|
|
14
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15
|
+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
16
|
+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
17
|
+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
18
|
+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
19
|
+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
20
|
+
* SOFTWARE.
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* Generic detail-view primitive components.
|
|
24
|
+
*
|
|
25
|
+
* Domain-agnostic building blocks for entity detail panels:
|
|
26
|
+
* - `DetailSection` — labeled section wrapper
|
|
27
|
+
* - `DetailRow` — key/value row with spread or inline layout
|
|
28
|
+
* - `TagList` — pill tag row
|
|
29
|
+
* - `StatusBadge` — colored pill badge (caller supplies color classes)
|
|
30
|
+
* - `DetailHeader` — two-line header with indicators and action slots
|
|
31
|
+
*
|
|
32
|
+
* @packageDocumentation
|
|
33
|
+
*/
|
|
34
|
+
import React from 'react';
|
|
35
|
+
import { XMarkIcon } from '@heroicons/react/20/solid';
|
|
36
|
+
/**
|
|
37
|
+
* Labeled section wrapper with uppercase tracking header.
|
|
38
|
+
* @public
|
|
39
|
+
*/
|
|
40
|
+
export function DetailSection({ title, children }) {
|
|
41
|
+
return (React.createElement("div", { className: "mb-4" },
|
|
42
|
+
React.createElement("h3", { className: "text-xs font-semibold text-muted uppercase tracking-wider mb-1.5" }, title),
|
|
43
|
+
children));
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Key/value row for detail panels.
|
|
47
|
+
* Returns `null` if `value` is `null` or `undefined`.
|
|
48
|
+
* @public
|
|
49
|
+
*/
|
|
50
|
+
export function DetailRow({ label, value, layout = 'spread' }) {
|
|
51
|
+
if (value === null || value === undefined) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
if (layout === 'inline') {
|
|
55
|
+
return (React.createElement("div", { className: "flex items-baseline gap-2 py-0.5" },
|
|
56
|
+
React.createElement("span", { className: "text-xs text-muted w-32 shrink-0" }, label),
|
|
57
|
+
React.createElement("span", { className: "text-sm text-primary" }, value)));
|
|
58
|
+
}
|
|
59
|
+
return (React.createElement("div", { className: "flex items-baseline justify-between py-0.5 text-sm" },
|
|
60
|
+
React.createElement("span", { className: "text-muted shrink-0 mr-2" }, label),
|
|
61
|
+
React.createElement("span", { className: "text-primary text-right" }, value)));
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Horizontal pill row for string tags. Returns `null` if tags is empty.
|
|
65
|
+
* @public
|
|
66
|
+
*/
|
|
67
|
+
export function TagList({ tags }) {
|
|
68
|
+
if (tags.length === 0) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
return (React.createElement("div", { className: "flex flex-wrap gap-1" }, tags.map((tag) => (React.createElement("span", { key: tag, className: "px-2 py-0.5 text-xs rounded-full bg-surface-raised text-secondary" }, tag)))));
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Generic pill badge. Caller supplies the Tailwind color classes.
|
|
75
|
+
* @public
|
|
76
|
+
*/
|
|
77
|
+
export function StatusBadge({ label, colorClass }) {
|
|
78
|
+
return (React.createElement("span", { className: `inline-block px-2 py-0.5 text-[11px] font-medium rounded-full ${colorClass}` }, label));
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Three-line entity detail header.
|
|
82
|
+
*
|
|
83
|
+
* Line 1: full-width `title` (bold headline)
|
|
84
|
+
* Line 2: optional `subtitle` (de-emphasized, monospace — e.g. entity ID)
|
|
85
|
+
* Line 3: `indicators` left-justified, `actions` right-justified (status bar)
|
|
86
|
+
* Below: optional `description`
|
|
87
|
+
*
|
|
88
|
+
* Both `indicators` and `actions` are `React.ReactNode` — callers own the content.
|
|
89
|
+
* @public
|
|
90
|
+
*/
|
|
91
|
+
export function DetailHeader({ title, subtitle, description, indicators, actions, onClose }) {
|
|
92
|
+
return (React.createElement("div", { className: "mb-4 relative" },
|
|
93
|
+
React.createElement("div", { className: "flex items-start" },
|
|
94
|
+
React.createElement("h2", { className: "text-lg font-semibold text-primary flex-1 min-w-0" }, title),
|
|
95
|
+
onClose && (React.createElement("button", { type: "button", onClick: onClose, title: "Close", className: "shrink-0 ml-2 mt-0.5 p-0.5 text-muted hover:text-secondary hover:bg-surface-raised rounded transition-colors" },
|
|
96
|
+
React.createElement(XMarkIcon, { className: "w-4 h-4" })))),
|
|
97
|
+
subtitle && React.createElement("p", { className: "text-xs text-muted font-mono mt-0.5 mb-1" }, subtitle),
|
|
98
|
+
(indicators !== undefined || actions !== undefined) && (React.createElement("div", { className: "flex items-center justify-between gap-2 mt-1" },
|
|
99
|
+
React.createElement("div", { className: "flex items-center gap-2" }, indicators),
|
|
100
|
+
actions !== undefined && React.createElement("div", { className: "flex items-center gap-1 shrink-0" }, actions))),
|
|
101
|
+
description && React.createElement("p", { className: "text-sm text-secondary mt-1" }, description)));
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=DetailHelpers.js.map
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2026 Erik Fortune
|
|
3
|
+
*
|
|
4
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
|
+
* of this software and associated documentation files (the "Software"), to deal
|
|
6
|
+
* in the Software without restriction, including without limitation the rights
|
|
7
|
+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
* copies of the Software, and to permit persons to whom the Software is
|
|
9
|
+
* furnished to do so, subject to the following conditions:
|
|
10
|
+
*
|
|
11
|
+
* The above copyright notice and this permission notice shall be included in all
|
|
12
|
+
* copies or substantial portions of the Software.
|
|
13
|
+
*
|
|
14
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15
|
+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
16
|
+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
17
|
+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
18
|
+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
19
|
+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
20
|
+
* SOFTWARE.
|
|
21
|
+
*/
|
|
22
|
+
import React, { useCallback, useState } from 'react';
|
|
23
|
+
/**
|
|
24
|
+
* Strips markdown code fences from text.
|
|
25
|
+
* AI agents often wrap JSON in code fences.
|
|
26
|
+
* @param text - Raw text that may contain code fences
|
|
27
|
+
* @returns Text with outer code fences removed
|
|
28
|
+
* @internal
|
|
29
|
+
*/
|
|
30
|
+
export function stripCodeFences(text) {
|
|
31
|
+
const trimmed = text.trim();
|
|
32
|
+
const fencePattern = /^```(?:\w+)?\s*\n?([\s\S]*?)\n?\s*```$/;
|
|
33
|
+
const match = fencePattern.exec(trimmed);
|
|
34
|
+
return match ? match[1].trim() : trimmed;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Generic JSON drop/paste target with converter-based validation.
|
|
38
|
+
* Accepts text via drag-and-drop or paste, strips markdown code fences,
|
|
39
|
+
* parses as JSON, validates through the provided converter, and calls
|
|
40
|
+
* the appropriate callback.
|
|
41
|
+
*
|
|
42
|
+
* @typeParam T - The expected validated type
|
|
43
|
+
* @public
|
|
44
|
+
*/
|
|
45
|
+
export function JsonDropZone(props) {
|
|
46
|
+
const { converter, onValueReceived, onError, error, hint, className } = props;
|
|
47
|
+
const [isDragOver, setIsDragOver] = useState(false);
|
|
48
|
+
const [internalError, setInternalError] = useState(undefined);
|
|
49
|
+
const displayError = error !== null && error !== void 0 ? error : internalError;
|
|
50
|
+
const processText = useCallback((rawText) => {
|
|
51
|
+
setInternalError(undefined);
|
|
52
|
+
const stripped = stripCodeFences(rawText);
|
|
53
|
+
let parsed;
|
|
54
|
+
try {
|
|
55
|
+
parsed = JSON.parse(stripped);
|
|
56
|
+
}
|
|
57
|
+
catch (_a) {
|
|
58
|
+
const message = 'Invalid JSON: could not parse the dropped text.';
|
|
59
|
+
setInternalError(message);
|
|
60
|
+
onError === null || onError === void 0 ? void 0 : onError(message);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const result = converter.convert(parsed);
|
|
64
|
+
if (result.isSuccess()) {
|
|
65
|
+
setInternalError(undefined);
|
|
66
|
+
onValueReceived(result.value);
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
const message = `Validation failed: ${result.message}`;
|
|
70
|
+
setInternalError(message);
|
|
71
|
+
onError === null || onError === void 0 ? void 0 : onError(message);
|
|
72
|
+
}
|
|
73
|
+
}, [converter, onValueReceived, onError]);
|
|
74
|
+
const handleDragOver = useCallback((e) => {
|
|
75
|
+
e.preventDefault();
|
|
76
|
+
e.stopPropagation();
|
|
77
|
+
setIsDragOver(true);
|
|
78
|
+
}, []);
|
|
79
|
+
const handleDragLeave = useCallback((e) => {
|
|
80
|
+
e.preventDefault();
|
|
81
|
+
e.stopPropagation();
|
|
82
|
+
setIsDragOver(false);
|
|
83
|
+
}, []);
|
|
84
|
+
const handleDrop = useCallback((e) => {
|
|
85
|
+
e.preventDefault();
|
|
86
|
+
e.stopPropagation();
|
|
87
|
+
setIsDragOver(false);
|
|
88
|
+
const text = e.dataTransfer.getData('text/plain') || e.dataTransfer.getData('text');
|
|
89
|
+
if (text) {
|
|
90
|
+
processText(text);
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
const message = 'No text data found in the dropped content.';
|
|
94
|
+
setInternalError(message);
|
|
95
|
+
onError === null || onError === void 0 ? void 0 : onError(message);
|
|
96
|
+
}
|
|
97
|
+
}, [processText, onError]);
|
|
98
|
+
const handlePaste = useCallback((e) => {
|
|
99
|
+
e.preventDefault();
|
|
100
|
+
const text = e.clipboardData.getData('text/plain') || e.clipboardData.getData('text');
|
|
101
|
+
if (text) {
|
|
102
|
+
processText(text);
|
|
103
|
+
}
|
|
104
|
+
}, [processText]);
|
|
105
|
+
const borderClass = displayError
|
|
106
|
+
? 'border-status-error-border bg-status-error-bg'
|
|
107
|
+
: isDragOver
|
|
108
|
+
? 'border-status-info-border bg-status-info-bg'
|
|
109
|
+
: 'border-border bg-surface-alt';
|
|
110
|
+
return (React.createElement("div", { className: `border-2 border-dashed rounded-md p-3 text-center text-sm transition-colors cursor-default ${borderClass} ${className !== null && className !== void 0 ? className : ''}`, onDragOver: handleDragOver, onDragLeave: handleDragLeave, onDrop: handleDrop, onPaste: handlePaste, tabIndex: 0, role: "region", "aria-label": hint !== null && hint !== void 0 ? hint : 'Drop or paste JSON here' }, displayError ? (React.createElement("span", { className: "text-status-error-text text-xs" }, displayError)) : (React.createElement("span", { className: "text-muted" }, hint !== null && hint !== void 0 ? hint : 'Drop or paste JSON here'))));
|
|
111
|
+
}
|
|
112
|
+
//# sourceMappingURL=JsonDropZone.js.map
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2026 Erik Fortune
|
|
3
|
+
*
|
|
4
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
|
+
* of this software and associated documentation files (the "Software"), to deal
|
|
6
|
+
* in the Software without restriction, including without limitation the rights
|
|
7
|
+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
* copies of the Software, and to permit persons to whom the Software is
|
|
9
|
+
* furnished to do so, subject to the following conditions:
|
|
10
|
+
*
|
|
11
|
+
* The above copyright notice and this permission notice shall be included in all
|
|
12
|
+
* copies or substantial portions of the Software.
|
|
13
|
+
*
|
|
14
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15
|
+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
16
|
+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
17
|
+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
18
|
+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
19
|
+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
20
|
+
* SOFTWARE.
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* Generic edit field helper components for entity editors.
|
|
24
|
+
*
|
|
25
|
+
* Provides reusable form field primitives: text inputs, number inputs,
|
|
26
|
+
* select dropdowns, tag editors, checkboxes, and layout wrappers.
|
|
27
|
+
* These components are domain-agnostic and can be used by any entity editor.
|
|
28
|
+
*
|
|
29
|
+
* @packageDocumentation
|
|
30
|
+
*/
|
|
31
|
+
import React from 'react';
|
|
32
|
+
/**
|
|
33
|
+
* Horizontal label + field layout for a single edit field.
|
|
34
|
+
* @public
|
|
35
|
+
*/
|
|
36
|
+
export function EditField({ label, children }) {
|
|
37
|
+
return (React.createElement("div", { className: "flex items-baseline gap-2 py-1" },
|
|
38
|
+
React.createElement("label", { className: "text-xs text-muted w-32 shrink-0" }, label),
|
|
39
|
+
React.createElement("div", { className: "flex-1" }, children)));
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Titled section wrapper for grouping related edit fields.
|
|
43
|
+
* @public
|
|
44
|
+
*/
|
|
45
|
+
export function EditSection({ title, children }) {
|
|
46
|
+
return (React.createElement("div", { className: "mb-4" },
|
|
47
|
+
React.createElement("h4", { className: "text-xs font-medium text-muted uppercase tracking-wider mb-1.5" }, title),
|
|
48
|
+
children));
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Single-line text input for required string fields.
|
|
52
|
+
* @public
|
|
53
|
+
*/
|
|
54
|
+
export function TextInput({ value, onChange, placeholder }) {
|
|
55
|
+
return (React.createElement("input", { type: "text", value: value, onChange: (e) => onChange(e.target.value), placeholder: placeholder, className: "w-full px-2 py-1 text-sm border border-border rounded focus:outline-none focus:ring-1 focus:ring-focus-ring focus:border-focus-ring" }));
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Single-line text input for optional string fields.
|
|
59
|
+
* Empty input produces `undefined`.
|
|
60
|
+
* @public
|
|
61
|
+
*/
|
|
62
|
+
export function OptionalTextInput({ value, onChange, placeholder }) {
|
|
63
|
+
return (React.createElement("input", { type: "text", value: value !== null && value !== void 0 ? value : '', onChange: (e) => onChange(e.target.value || undefined), placeholder: placeholder, className: "w-full px-2 py-1 text-sm border border-border rounded focus:outline-none focus:ring-1 focus:ring-focus-ring focus:border-focus-ring" }));
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Multi-line text input for optional string fields.
|
|
67
|
+
* Empty input produces `undefined`.
|
|
68
|
+
* @public
|
|
69
|
+
*/
|
|
70
|
+
export function TextAreaInput({ value, onChange, placeholder }) {
|
|
71
|
+
return (React.createElement("textarea", { value: value !== null && value !== void 0 ? value : '', onChange: (e) => onChange(e.target.value || undefined), placeholder: placeholder, rows: 3, className: "w-full px-2 py-1 text-sm border border-border rounded focus:outline-none focus:ring-1 focus:ring-focus-ring focus:border-focus-ring resize-y" }));
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Numeric input for optional number fields.
|
|
75
|
+
* Empty input produces `undefined`.
|
|
76
|
+
* @public
|
|
77
|
+
*/
|
|
78
|
+
export function NumberInput({ value, onChange, label, min, max, step }) {
|
|
79
|
+
return (React.createElement("input", { type: "number", value: value !== null && value !== void 0 ? value : '', onChange: (e) => {
|
|
80
|
+
const raw = e.target.value;
|
|
81
|
+
if (!raw.trim()) {
|
|
82
|
+
onChange(undefined);
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
const num = parseFloat(raw);
|
|
86
|
+
if (!isNaN(num)) {
|
|
87
|
+
onChange(num);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}, min: min, max: max, step: step !== null && step !== void 0 ? step : 0.1, className: "w-24 px-2 py-1 text-sm border border-border rounded focus:outline-none focus:ring-1 focus:ring-focus-ring focus:border-focus-ring text-right", "aria-label": label }));
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Dropdown select for enumerated string values.
|
|
94
|
+
* @public
|
|
95
|
+
*/
|
|
96
|
+
export function SelectInput({ value, options, onChange }) {
|
|
97
|
+
return (React.createElement("select", { value: value, onChange: (e) => onChange(e.target.value), className: "px-2 py-1 text-sm border border-border rounded focus:outline-none focus:ring-1 focus:ring-focus-ring focus:border-focus-ring" }, options.map((opt) => (React.createElement("option", { key: opt, value: opt }, opt)))));
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Comma-separated tag editor for string arrays.
|
|
101
|
+
* Empty input produces `undefined`.
|
|
102
|
+
* @public
|
|
103
|
+
*/
|
|
104
|
+
export function TagsInput({ value, onChange, placeholder }) {
|
|
105
|
+
var _a;
|
|
106
|
+
const text = (_a = value === null || value === void 0 ? void 0 : value.join(', ')) !== null && _a !== void 0 ? _a : '';
|
|
107
|
+
return (React.createElement("input", { type: "text", value: text, onChange: (e) => {
|
|
108
|
+
const raw = e.target.value;
|
|
109
|
+
if (!raw.trim()) {
|
|
110
|
+
onChange(undefined);
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
onChange(raw
|
|
114
|
+
.split(',')
|
|
115
|
+
.map((s) => s.trim())
|
|
116
|
+
.filter((s) => s.length > 0));
|
|
117
|
+
}
|
|
118
|
+
}, placeholder: placeholder !== null && placeholder !== void 0 ? placeholder : 'comma-separated values', className: "w-full px-2 py-1 text-sm border border-border rounded focus:outline-none focus:ring-1 focus:ring-focus-ring focus:border-focus-ring" }));
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Checkbox input for optional boolean fields.
|
|
122
|
+
* Unchecked produces `undefined`.
|
|
123
|
+
* @public
|
|
124
|
+
*/
|
|
125
|
+
export function CheckboxInput({ value, onChange, label }) {
|
|
126
|
+
return (React.createElement("label", { className: "flex items-center gap-2 text-sm text-secondary cursor-pointer" },
|
|
127
|
+
React.createElement("input", { type: "checkbox", checked: value !== null && value !== void 0 ? value : false, onChange: (e) => onChange(e.target.checked || undefined), className: "rounded border-border text-brand-primary focus:ring-focus-ring" }),
|
|
128
|
+
label));
|
|
129
|
+
}
|
|
130
|
+
//# sourceMappingURL=EditFieldHelpers.js.map
|