@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.
Files changed (180) hide show
  1. package/README.md +26 -0
  2. package/dist/index.browser.js +3 -0
  3. package/dist/index.js +43 -0
  4. package/dist/packlets/ai-assist/index.js +6 -0
  5. package/dist/packlets/ai-assist/useAiAssist.js +219 -0
  6. package/dist/packlets/cascade/CascadeContainer.js +83 -0
  7. package/dist/packlets/cascade/ComparisonView.js +48 -0
  8. package/dist/packlets/cascade/EntityTabLayout.js +104 -0
  9. package/dist/packlets/cascade/MobileCascadeStack.js +63 -0
  10. package/dist/packlets/cascade/index.js +37 -0
  11. package/dist/packlets/cascade/model.js +30 -0
  12. package/dist/packlets/cascade/useCascadeOps.js +206 -0
  13. package/dist/packlets/cascade/useCascadeTransitions.js +58 -0
  14. package/dist/packlets/detail/DetailHelpers.js +103 -0
  15. package/dist/packlets/detail/index.js +6 -0
  16. package/dist/packlets/drop-zone/JsonDropZone.js +112 -0
  17. package/dist/packlets/drop-zone/index.js +6 -0
  18. package/dist/packlets/editing/EditFieldHelpers.js +130 -0
  19. package/dist/packlets/editing/MultiActionButton.js +73 -0
  20. package/dist/packlets/editing/NumericInput.js +119 -0
  21. package/dist/packlets/editing/TypeaheadInput.js +207 -0
  22. package/dist/packlets/editing/index.js +10 -0
  23. package/dist/packlets/editing/useTypeaheadMatch.js +102 -0
  24. package/dist/packlets/keyboard/index.js +7 -0
  25. package/dist/packlets/keyboard/registry.js +133 -0
  26. package/dist/packlets/keyboard/useKeyboardShortcuts.js +117 -0
  27. package/dist/packlets/messages/MessagesContext.js +76 -0
  28. package/dist/packlets/messages/MessagesLogger.js +103 -0
  29. package/dist/packlets/messages/StatusBar.js +154 -0
  30. package/dist/packlets/messages/Toast.js +68 -0
  31. package/dist/packlets/messages/index.js +11 -0
  32. package/dist/packlets/messages/model.js +56 -0
  33. package/dist/packlets/messages/useLogReporter.js +66 -0
  34. package/dist/packlets/modal/ConfirmDialog.js +78 -0
  35. package/dist/packlets/modal/Modal.js +55 -0
  36. package/dist/packlets/modal/index.js +7 -0
  37. package/dist/packlets/print/PrintEnclosure.js +60 -0
  38. package/dist/packlets/print/index.js +7 -0
  39. package/dist/packlets/print/openPrintWindow.js +112 -0
  40. package/dist/packlets/responsive/ResponsiveProvider.js +56 -0
  41. package/dist/packlets/responsive/index.js +7 -0
  42. package/dist/packlets/responsive/useResponsiveLayout.js +118 -0
  43. package/dist/packlets/selectors/EntityRow.js +276 -0
  44. package/dist/packlets/selectors/PreferredSelector.js +251 -0
  45. package/dist/packlets/selectors/index.js +24 -0
  46. package/dist/packlets/sidebar/CollectionSection.js +107 -0
  47. package/dist/packlets/sidebar/EntityList.js +164 -0
  48. package/dist/packlets/sidebar/FilterBar.js +42 -0
  49. package/dist/packlets/sidebar/FilterRow.js +182 -0
  50. package/dist/packlets/sidebar/GroupedEntityList.js +183 -0
  51. package/dist/packlets/sidebar/SearchBar.js +34 -0
  52. package/dist/packlets/sidebar/SidebarLayout.js +62 -0
  53. package/dist/packlets/sidebar/index.js +12 -0
  54. package/dist/packlets/theme/ThemeProvider.js +141 -0
  55. package/dist/packlets/theme/index.js +6 -0
  56. package/dist/packlets/top-bar/ModeSelector.js +46 -0
  57. package/dist/packlets/top-bar/TabBar.js +37 -0
  58. package/dist/packlets/top-bar/index.js +7 -0
  59. package/dist/packlets/url-sync/index.js +6 -0
  60. package/dist/packlets/url-sync/useUrlSync.js +157 -0
  61. package/eslint.config.js +22 -0
  62. package/lib/index.browser.d.ts +2 -0
  63. package/lib/index.browser.js +19 -0
  64. package/lib/index.d.ts +28 -0
  65. package/lib/index.js +59 -0
  66. package/lib/packlets/ai-assist/index.d.ts +6 -0
  67. package/lib/packlets/ai-assist/index.js +11 -0
  68. package/lib/packlets/ai-assist/useAiAssist.d.ts +77 -0
  69. package/lib/packlets/ai-assist/useAiAssist.js +223 -0
  70. package/lib/packlets/cascade/CascadeContainer.d.ts +44 -0
  71. package/lib/packlets/cascade/CascadeContainer.js +119 -0
  72. package/lib/packlets/cascade/ComparisonView.d.ts +35 -0
  73. package/lib/packlets/cascade/ComparisonView.js +54 -0
  74. package/lib/packlets/cascade/EntityTabLayout.d.ts +47 -0
  75. package/lib/packlets/cascade/EntityTabLayout.js +110 -0
  76. package/lib/packlets/cascade/MobileCascadeStack.d.ts +20 -0
  77. package/lib/packlets/cascade/MobileCascadeStack.js +99 -0
  78. package/lib/packlets/cascade/index.d.ts +12 -0
  79. package/lib/packlets/cascade/index.js +48 -0
  80. package/lib/packlets/cascade/model.d.ts +57 -0
  81. package/lib/packlets/cascade/model.js +33 -0
  82. package/lib/packlets/cascade/useCascadeOps.d.ts +111 -0
  83. package/lib/packlets/cascade/useCascadeOps.js +209 -0
  84. package/lib/packlets/cascade/useCascadeTransitions.d.ts +19 -0
  85. package/lib/packlets/cascade/useCascadeTransitions.js +62 -0
  86. package/lib/packlets/detail/DetailHelpers.d.ts +83 -0
  87. package/lib/packlets/detail/DetailHelpers.js +113 -0
  88. package/lib/packlets/detail/index.d.ts +6 -0
  89. package/lib/packlets/detail/index.js +14 -0
  90. package/lib/packlets/drop-zone/JsonDropZone.d.ts +40 -0
  91. package/lib/packlets/drop-zone/JsonDropZone.js +149 -0
  92. package/lib/packlets/drop-zone/index.d.ts +6 -0
  93. package/lib/packlets/drop-zone/index.js +10 -0
  94. package/lib/packlets/editing/EditFieldHelpers.d.ts +171 -0
  95. package/lib/packlets/editing/EditFieldHelpers.js +144 -0
  96. package/lib/packlets/editing/MultiActionButton.d.ts +45 -0
  97. package/lib/packlets/editing/MultiActionButton.js +109 -0
  98. package/lib/packlets/editing/NumericInput.d.ts +47 -0
  99. package/lib/packlets/editing/NumericInput.js +155 -0
  100. package/lib/packlets/editing/TypeaheadInput.d.ts +46 -0
  101. package/lib/packlets/editing/TypeaheadInput.js +243 -0
  102. package/lib/packlets/editing/index.d.ts +10 -0
  103. package/lib/packlets/editing/index.js +26 -0
  104. package/lib/packlets/editing/useTypeaheadMatch.d.ts +42 -0
  105. package/lib/packlets/editing/useTypeaheadMatch.js +105 -0
  106. package/lib/packlets/keyboard/index.d.ts +7 -0
  107. package/lib/packlets/keyboard/index.js +15 -0
  108. package/lib/packlets/keyboard/registry.d.ts +92 -0
  109. package/lib/packlets/keyboard/registry.js +138 -0
  110. package/lib/packlets/keyboard/useKeyboardShortcuts.d.ts +50 -0
  111. package/lib/packlets/keyboard/useKeyboardShortcuts.js +155 -0
  112. package/lib/packlets/messages/MessagesContext.d.ts +40 -0
  113. package/lib/packlets/messages/MessagesContext.js +113 -0
  114. package/lib/packlets/messages/MessagesLogger.d.ts +50 -0
  115. package/lib/packlets/messages/MessagesLogger.js +107 -0
  116. package/lib/packlets/messages/StatusBar.d.ts +22 -0
  117. package/lib/packlets/messages/StatusBar.js +190 -0
  118. package/lib/packlets/messages/Toast.d.ts +31 -0
  119. package/lib/packlets/messages/Toast.js +105 -0
  120. package/lib/packlets/messages/index.d.ts +11 -0
  121. package/lib/packlets/messages/index.js +24 -0
  122. package/lib/packlets/messages/model.d.ts +59 -0
  123. package/lib/packlets/messages/model.js +61 -0
  124. package/lib/packlets/messages/useLogReporter.d.ts +22 -0
  125. package/lib/packlets/messages/useLogReporter.js +69 -0
  126. package/lib/packlets/modal/ConfirmDialog.d.ts +39 -0
  127. package/lib/packlets/modal/ConfirmDialog.js +114 -0
  128. package/lib/packlets/modal/Modal.d.ts +22 -0
  129. package/lib/packlets/modal/Modal.js +91 -0
  130. package/lib/packlets/modal/index.d.ts +7 -0
  131. package/lib/packlets/modal/index.js +12 -0
  132. package/lib/packlets/print/PrintEnclosure.d.ts +33 -0
  133. package/lib/packlets/print/PrintEnclosure.js +96 -0
  134. package/lib/packlets/print/index.d.ts +7 -0
  135. package/lib/packlets/print/index.js +12 -0
  136. package/lib/packlets/print/openPrintWindow.d.ts +35 -0
  137. package/lib/packlets/print/openPrintWindow.js +118 -0
  138. package/lib/packlets/responsive/ResponsiveProvider.d.ts +35 -0
  139. package/lib/packlets/responsive/ResponsiveProvider.js +93 -0
  140. package/lib/packlets/responsive/index.d.ts +7 -0
  141. package/lib/packlets/responsive/index.js +13 -0
  142. package/lib/packlets/responsive/useResponsiveLayout.d.ts +48 -0
  143. package/lib/packlets/responsive/useResponsiveLayout.js +121 -0
  144. package/lib/packlets/selectors/EntityRow.d.ts +45 -0
  145. package/lib/packlets/selectors/EntityRow.js +315 -0
  146. package/lib/packlets/selectors/PreferredSelector.d.ts +50 -0
  147. package/lib/packlets/selectors/PreferredSelector.js +287 -0
  148. package/lib/packlets/selectors/index.d.ts +5 -0
  149. package/lib/packlets/selectors/index.js +29 -0
  150. package/lib/packlets/sidebar/CollectionSection.d.ts +82 -0
  151. package/lib/packlets/sidebar/CollectionSection.js +143 -0
  152. package/lib/packlets/sidebar/EntityList.d.ts +105 -0
  153. package/lib/packlets/sidebar/EntityList.js +200 -0
  154. package/lib/packlets/sidebar/FilterBar.d.ts +26 -0
  155. package/lib/packlets/sidebar/FilterBar.js +48 -0
  156. package/lib/packlets/sidebar/FilterRow.d.ts +42 -0
  157. package/lib/packlets/sidebar/FilterRow.js +218 -0
  158. package/lib/packlets/sidebar/GroupedEntityList.d.ts +59 -0
  159. package/lib/packlets/sidebar/GroupedEntityList.js +219 -0
  160. package/lib/packlets/sidebar/SearchBar.d.ts +19 -0
  161. package/lib/packlets/sidebar/SearchBar.js +40 -0
  162. package/lib/packlets/sidebar/SidebarLayout.d.ts +28 -0
  163. package/lib/packlets/sidebar/SidebarLayout.js +98 -0
  164. package/lib/packlets/sidebar/index.d.ts +12 -0
  165. package/lib/packlets/sidebar/index.js +22 -0
  166. package/lib/packlets/theme/ThemeProvider.d.ts +68 -0
  167. package/lib/packlets/theme/ThemeProvider.js +178 -0
  168. package/lib/packlets/theme/index.d.ts +6 -0
  169. package/lib/packlets/theme/index.js +11 -0
  170. package/lib/packlets/top-bar/ModeSelector.d.ts +38 -0
  171. package/lib/packlets/top-bar/ModeSelector.js +52 -0
  172. package/lib/packlets/top-bar/TabBar.d.ts +31 -0
  173. package/lib/packlets/top-bar/TabBar.js +43 -0
  174. package/lib/packlets/top-bar/index.d.ts +7 -0
  175. package/lib/packlets/top-bar/index.js +12 -0
  176. package/lib/packlets/url-sync/index.d.ts +6 -0
  177. package/lib/packlets/url-sync/index.js +12 -0
  178. package/lib/packlets/url-sync/useUrlSync.d.ts +75 -0
  179. package/lib/packlets/url-sync/useUrlSync.js +162 -0
  180. package/package.json +82 -0
@@ -0,0 +1,111 @@
1
+ import { type Result } from '@fgv/ts-utils';
2
+ import type { CascadeColumnMode, ICascadeEntryBase } from './model';
3
+ /**
4
+ * Describes a located entry in the cascade stack.
5
+ * @public
6
+ */
7
+ export interface ICascadeFind<TEntry extends ICascadeEntryBase = ICascadeEntryBase> {
8
+ readonly depth: number;
9
+ readonly entry: TEntry;
10
+ }
11
+ /**
12
+ * Describes editors that would be affected by a cascade operation.
13
+ * Returned by {@link ICascadeOps.openEditor} when the operation is blocked.
14
+ * @public
15
+ */
16
+ export interface ICascadeConflict<TEntry extends ICascadeEntryBase = ICascadeEntryBase> {
17
+ readonly conflictingEditors: ReadonlyArray<ICascadeFind<TEntry>>;
18
+ }
19
+ /**
20
+ * Partial cascade entry for operations that set mode and origin automatically.
21
+ * @public
22
+ */
23
+ export type CascadeEntrySpec<TEntry extends ICascadeEntryBase = ICascadeEntryBase> = Omit<TEntry, 'mode' | 'origin'> & {
24
+ readonly mode?: CascadeColumnMode;
25
+ };
26
+ /**
27
+ * Semantic cascade operations.
28
+ *
29
+ * These operations encode the cascade navigation rules:
30
+ * 1. Views stack on views
31
+ * 2. Editor replaces its view and trims views above (but not below)
32
+ * 3. Editors never squash editors — blocked if editors exist above target
33
+ * 4. Editors can stack on editors (child above parent is fine)
34
+ * 5. Save/cancel on nested panels pops; on primary panels returns to view
35
+ * 6. Preview behaves like view for all stacking/trimming rules
36
+ * 7. Save/cancel blocked when child editors exist above
37
+ *
38
+ * @typeParam TEntry - The cascade entry type, defaults to {@link ICascadeEntryBase}.
39
+ * @public
40
+ */
41
+ export interface ICascadeOps<TEntry extends ICascadeEntryBase = ICascadeEntryBase> {
42
+ /**
43
+ * Replace entire stack with a single view entry (from list selection).
44
+ * @returns The created entry at depth 0.
45
+ */
46
+ readonly select: (entry: CascadeEntrySpec<TEntry>) => Result<ICascadeFind<TEntry>>;
47
+ /**
48
+ * Push a view entry after `fromDepth`, trimming anything beyond.
49
+ * Toggle: if the same entity is already at `fromDepth + 1`, collapse instead.
50
+ * @returns The pushed entry, or the collapsed entry on toggle.
51
+ */
52
+ readonly drillDown: (fromDepth: number, entry: CascadeEntrySpec<TEntry>) => Result<ICascadeFind<TEntry>>;
53
+ /**
54
+ * Switch panel at `depth` to edit mode.
55
+ * Trims view/preview panels above `depth`. Blocked if editors/creates exist above.
56
+ * @returns The entry switched to edit mode, or failure if blocked by conflicting editors.
57
+ */
58
+ readonly openEditor: (depth: number) => Result<ICascadeFind<TEntry>>;
59
+ /**
60
+ * Push a nested panel (create or edit) on top of the current stack.
61
+ * Used for typeahead-on-blur creation and sub-entity editing.
62
+ * @returns The pushed nested entry.
63
+ */
64
+ readonly openNested: (fromDepth: number, entry: Omit<TEntry, 'origin'>) => Result<ICascadeFind<TEntry>>;
65
+ /**
66
+ * Pop the topmost entry (for nested save/cancel). Always safe.
67
+ * @returns The removed entry, or failure if the stack was empty.
68
+ */
69
+ readonly pop: () => Result<ICascadeFind<TEntry>>;
70
+ /**
71
+ * Transition entry at `depth` from edit/create to view mode.
72
+ * Used for primary entity save/cancel.
73
+ * @returns The entry transitioned to view mode.
74
+ */
75
+ readonly popToView: (depth: number, refreshedEntity?: unknown) => Result<ICascadeFind<TEntry>>;
76
+ /**
77
+ * Trim the stack to keep only entries below `depth` (exclusive).
78
+ * Removes the entry at `depth` and everything above it.
79
+ * @returns The last remaining entry after trimming, or failure if stack becomes empty.
80
+ */
81
+ readonly trimTo: (depth: number) => Result<ICascadeFind<TEntry>>;
82
+ /** Return editors/creates above a given depth. */
83
+ readonly editorsAbove: (depth: number) => ReadonlyArray<ICascadeFind<TEntry>>;
84
+ /** Whether any editors/creates exist anywhere in the stack. */
85
+ readonly hasUnsavedEditors: () => boolean;
86
+ /** Whether save/cancel is allowed at `depth` (false if child editors exist above). */
87
+ readonly canSaveOrCancel: (depth: number) => boolean;
88
+ /** Clear the entire cascade stack. */
89
+ readonly clear: () => void;
90
+ /** Clear the cascade if any entry has the given entity ID. */
91
+ readonly clearById: (entityId: string) => void;
92
+ /** Clear the cascade if any entry matches the predicate. */
93
+ readonly clearIf: (predicate: (entry: TEntry) => boolean) => void;
94
+ /** Find the first entry matching a predicate. */
95
+ readonly find: (predicate: (entry: TEntry) => boolean) => Result<ICascadeFind<TEntry>>;
96
+ /** The current cascade stack (for rendering). */
97
+ readonly stack: ReadonlyArray<TEntry>;
98
+ }
99
+ /**
100
+ * Hook providing semantic cascade operations.
101
+ *
102
+ * Takes the cascade stack and a squash function as parameters, making it
103
+ * independent of any specific state management solution. Domain-specific
104
+ * applications typically provide a convenience wrapper that reads these
105
+ * values from their own store.
106
+ *
107
+ * @typeParam TEntry - The cascade entry type. Inferred from `cascadeStack`.
108
+ * @public
109
+ */
110
+ export declare function useCascadeOps<TEntry extends ICascadeEntryBase>(cascadeStack: ReadonlyArray<TEntry>, squashCascade: (entries: ReadonlyArray<TEntry>) => void): ICascadeOps<TEntry>;
111
+ //# sourceMappingURL=useCascadeOps.d.ts.map
@@ -0,0 +1,209 @@
1
+ "use strict";
2
+ /*
3
+ * Copyright (c) 2026 Erik Fortune
4
+ *
5
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ * of this software and associated documentation files (the "Software"), to deal
7
+ * in the Software without restriction, including without limitation the rights
8
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ * copies of the Software, and to permit persons to whom the Software is
10
+ * furnished to do so, subject to the following conditions:
11
+ *
12
+ * The above copyright notice and this permission notice shall be included in all
13
+ * copies or substantial portions of the Software.
14
+ *
15
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ * SOFTWARE.
22
+ */
23
+ Object.defineProperty(exports, "__esModule", { value: true });
24
+ exports.useCascadeOps = useCascadeOps;
25
+ /**
26
+ * Semantic cascade operations hook.
27
+ *
28
+ * Encodes cascade navigation rules so tabs declare intent (select, drillDown,
29
+ * openEditor, pop) rather than manually computing cascade stacks.
30
+ *
31
+ * The hook is generic over the entry type `TEntry extends ICascadeEntryBase`.
32
+ * Domain-specific applications pass their extended entry type to preserve
33
+ * full type information through the cascade operations.
34
+ *
35
+ * @packageDocumentation
36
+ */
37
+ const react_1 = require("react");
38
+ const ts_utils_1 = require("@fgv/ts-utils");
39
+ // ============================================================================
40
+ // Helpers
41
+ // ============================================================================
42
+ function isEditOrCreate(mode) {
43
+ return mode === 'edit' || mode === 'create';
44
+ }
45
+ function findEditorsAbove(stack, depth) {
46
+ const result = [];
47
+ for (let i = depth + 1; i < stack.length; i++) {
48
+ if (isEditOrCreate(stack[i].mode)) {
49
+ result.push({ depth: i, entry: stack[i] });
50
+ }
51
+ }
52
+ return result;
53
+ }
54
+ // ============================================================================
55
+ // Hook
56
+ // ============================================================================
57
+ /**
58
+ * Hook providing semantic cascade operations.
59
+ *
60
+ * Takes the cascade stack and a squash function as parameters, making it
61
+ * independent of any specific state management solution. Domain-specific
62
+ * applications typically provide a convenience wrapper that reads these
63
+ * values from their own store.
64
+ *
65
+ * @typeParam TEntry - The cascade entry type. Inferred from `cascadeStack`.
66
+ * @public
67
+ */
68
+ function useCascadeOps(cascadeStack, squashCascade) {
69
+ const asTEntry = (entry) => entry;
70
+ const select = (0, react_1.useCallback)((entry) => {
71
+ var _a;
72
+ const created = asTEntry(Object.assign(Object.assign({}, entry), { mode: (_a = entry.mode) !== null && _a !== void 0 ? _a : 'view', origin: 'primary' }));
73
+ squashCascade([created]);
74
+ return (0, ts_utils_1.succeed)({ depth: 0, entry: created });
75
+ },
76
+ // eslint-disable-next-line react-hooks/exhaustive-deps
77
+ [squashCascade]);
78
+ const drillDown = (0, react_1.useCallback)((fromDepth, entry) => {
79
+ var _a;
80
+ const nextEntry = cascadeStack[fromDepth + 1];
81
+ if ((nextEntry === null || nextEntry === void 0 ? void 0 : nextEntry.entityType) === entry.entityType && nextEntry.entityId === entry.entityId) {
82
+ // Toggle: collapse if same entity is already at fromDepth + 1
83
+ squashCascade(cascadeStack.slice(0, fromDepth + 1));
84
+ return (0, ts_utils_1.succeed)({ depth: fromDepth + 1, entry: nextEntry });
85
+ }
86
+ const created = asTEntry(Object.assign(Object.assign({}, entry), { mode: (_a = entry.mode) !== null && _a !== void 0 ? _a : 'view', origin: 'nested' }));
87
+ squashCascade([...cascadeStack.slice(0, fromDepth + 1), created]);
88
+ return (0, ts_utils_1.succeed)({ depth: fromDepth + 1, entry: created });
89
+ },
90
+ // eslint-disable-next-line react-hooks/exhaustive-deps
91
+ [cascadeStack, squashCascade]);
92
+ const editorsAbove = (0, react_1.useCallback)((depth) => {
93
+ return findEditorsAbove(cascadeStack, depth);
94
+ }, [cascadeStack]);
95
+ const openEditor = (0, react_1.useCallback)((depth) => {
96
+ const entry = cascadeStack[depth];
97
+ if (!entry) {
98
+ return (0, ts_utils_1.fail)(`depth ${depth} out of bounds for cascade stack of length ${cascadeStack.length}`);
99
+ }
100
+ // Check for editors above the target depth
101
+ const conflicts = findEditorsAbove(cascadeStack, depth);
102
+ if (conflicts.length > 0) {
103
+ const names = conflicts.map((c) => `${c.entry.entityType}(${c.entry.mode}) at depth ${c.depth}`);
104
+ return (0, ts_utils_1.fail)(`blocked by editors above: ${names.join(', ')}`);
105
+ }
106
+ // Trim everything above depth (downstream views become incoherent),
107
+ // switch target to edit mode, preserving origin
108
+ const edited = asTEntry(Object.assign(Object.assign({}, entry), { mode: 'edit' }));
109
+ squashCascade([...cascadeStack.slice(0, depth), edited]);
110
+ return (0, ts_utils_1.succeed)({ depth, entry: edited });
111
+ },
112
+ // eslint-disable-next-line react-hooks/exhaustive-deps
113
+ [cascadeStack, squashCascade]);
114
+ const openNested = (0, react_1.useCallback)((fromDepth, entry) => {
115
+ const created = asTEntry(Object.assign(Object.assign({}, entry), { origin: 'nested' }));
116
+ const newDepth = fromDepth + 1;
117
+ squashCascade([...cascadeStack.slice(0, newDepth), created]);
118
+ return (0, ts_utils_1.succeed)({ depth: newDepth, entry: created });
119
+ },
120
+ // eslint-disable-next-line react-hooks/exhaustive-deps
121
+ [cascadeStack, squashCascade]);
122
+ const pop = (0, react_1.useCallback)(() => {
123
+ if (cascadeStack.length === 0) {
124
+ return (0, ts_utils_1.fail)('cannot pop from empty cascade stack');
125
+ }
126
+ const removed = cascadeStack[cascadeStack.length - 1];
127
+ squashCascade(cascadeStack.slice(0, -1));
128
+ return (0, ts_utils_1.succeed)({ depth: cascadeStack.length - 1, entry: removed });
129
+ }, [cascadeStack, squashCascade]);
130
+ const popToView = (0, react_1.useCallback)((depth, refreshedEntity) => {
131
+ const entry = cascadeStack[depth];
132
+ if (!entry) {
133
+ return (0, ts_utils_1.fail)(`depth ${depth} out of bounds for cascade stack of length ${cascadeStack.length}`);
134
+ }
135
+ const viewed = asTEntry(Object.assign(Object.assign(Object.assign({}, entry), { mode: 'view' }), (refreshedEntity !== undefined ? { entity: refreshedEntity } : {})));
136
+ squashCascade([...cascadeStack.slice(0, depth), viewed]);
137
+ return (0, ts_utils_1.succeed)({ depth, entry: viewed });
138
+ },
139
+ // eslint-disable-next-line react-hooks/exhaustive-deps
140
+ [cascadeStack, squashCascade]);
141
+ const trimTo = (0, react_1.useCallback)((depth) => {
142
+ const trimmed = Math.max(0, depth);
143
+ const newStack = cascadeStack.slice(0, trimmed);
144
+ squashCascade(newStack);
145
+ if (newStack.length === 0) {
146
+ return (0, ts_utils_1.fail)('cascade stack is empty after trim');
147
+ }
148
+ const lastIdx = newStack.length - 1;
149
+ return (0, ts_utils_1.succeed)({ depth: lastIdx, entry: newStack[lastIdx] });
150
+ }, [cascadeStack, squashCascade]);
151
+ const hasUnsavedEditors = (0, react_1.useCallback)(() => {
152
+ return cascadeStack.some((e) => isEditOrCreate(e.mode));
153
+ }, [cascadeStack]);
154
+ const canSaveOrCancel = (0, react_1.useCallback)((depth) => {
155
+ return findEditorsAbove(cascadeStack, depth).length === 0;
156
+ }, [cascadeStack]);
157
+ const clear = (0, react_1.useCallback)(() => {
158
+ squashCascade([]);
159
+ }, [squashCascade]);
160
+ const clearIf = (0, react_1.useCallback)((predicate) => {
161
+ if (cascadeStack.some(predicate)) {
162
+ squashCascade([]);
163
+ }
164
+ }, [cascadeStack, squashCascade]);
165
+ const clearById = (0, react_1.useCallback)((entityId) => {
166
+ clearIf((e) => e.entityId === entityId);
167
+ }, [clearIf]);
168
+ const find = (0, react_1.useCallback)((predicate) => {
169
+ const depth = cascadeStack.findIndex(predicate);
170
+ if (depth < 0) {
171
+ return (0, ts_utils_1.fail)('no matching entry in cascade stack');
172
+ }
173
+ return (0, ts_utils_1.succeed)({ depth, entry: cascadeStack[depth] });
174
+ }, [cascadeStack]);
175
+ return (0, react_1.useMemo)(() => ({
176
+ select,
177
+ drillDown,
178
+ openEditor,
179
+ openNested,
180
+ pop,
181
+ popToView,
182
+ trimTo,
183
+ editorsAbove,
184
+ hasUnsavedEditors,
185
+ canSaveOrCancel,
186
+ clear,
187
+ clearById,
188
+ clearIf,
189
+ find,
190
+ stack: cascadeStack
191
+ }), [
192
+ select,
193
+ drillDown,
194
+ openEditor,
195
+ openNested,
196
+ pop,
197
+ popToView,
198
+ trimTo,
199
+ editorsAbove,
200
+ hasUnsavedEditors,
201
+ canSaveOrCancel,
202
+ clear,
203
+ clearById,
204
+ clearIf,
205
+ find,
206
+ cascadeStack
207
+ ]);
208
+ }
209
+ //# sourceMappingURL=useCascadeOps.js.map
@@ -0,0 +1,19 @@
1
+ import type { ICascadeEntryBase } from './model';
2
+ /**
3
+ * Returns a depth-aware squash helper used by cascade views.
4
+ *
5
+ * Keeps stack entries up to and including `depth`, then appends `entry`.
6
+ *
7
+ * @public
8
+ */
9
+ export declare function useSquashAt(cascadeStack: ReadonlyArray<ICascadeEntryBase>, squashCascade: (entries: ReadonlyArray<ICascadeEntryBase>) => void): (depth: number, entry: ICascadeEntryBase) => void;
10
+ /**
11
+ * Returns a shared drill-down toggle helper for cascade columns.
12
+ *
13
+ * If the target entry is already immediately to the right of `depth`, it collapses
14
+ * back to `depth`. Otherwise, it appends a new view entry at `depth + 1`.
15
+ *
16
+ * @public
17
+ */
18
+ export declare function useCascadeDrillDown(cascadeStack: ReadonlyArray<ICascadeEntryBase>, squashCascade: (entries: ReadonlyArray<ICascadeEntryBase>) => void, squashAt: (depth: number, entry: ICascadeEntryBase) => void): (depth: number, entityType: string, entityId: string, extra?: Partial<ICascadeEntryBase>) => void;
19
+ //# sourceMappingURL=useCascadeTransitions.d.ts.map
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+ /*
3
+ * Copyright (c) 2026 Erik Fortune
4
+ *
5
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ * of this software and associated documentation files (the "Software"), to deal
7
+ * in the Software without restriction, including without limitation the rights
8
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ * copies of the Software, and to permit persons to whom the Software is
10
+ * furnished to do so, subject to the following conditions:
11
+ *
12
+ * The above copyright notice and this permission notice shall be included in all
13
+ * copies or substantial portions of the Software.
14
+ *
15
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ * SOFTWARE.
22
+ */
23
+ Object.defineProperty(exports, "__esModule", { value: true });
24
+ exports.useSquashAt = useSquashAt;
25
+ exports.useCascadeDrillDown = useCascadeDrillDown;
26
+ /**
27
+ * Shared cascade transition hooks for entity tabs.
28
+ *
29
+ * @packageDocumentation
30
+ */
31
+ const react_1 = require("react");
32
+ /**
33
+ * Returns a depth-aware squash helper used by cascade views.
34
+ *
35
+ * Keeps stack entries up to and including `depth`, then appends `entry`.
36
+ *
37
+ * @public
38
+ */
39
+ function useSquashAt(cascadeStack, squashCascade) {
40
+ return (0, react_1.useCallback)((depth, entry) => {
41
+ squashCascade([...cascadeStack.slice(0, depth + 1), entry]);
42
+ }, [cascadeStack, squashCascade]);
43
+ }
44
+ /**
45
+ * Returns a shared drill-down toggle helper for cascade columns.
46
+ *
47
+ * If the target entry is already immediately to the right of `depth`, it collapses
48
+ * back to `depth`. Otherwise, it appends a new view entry at `depth + 1`.
49
+ *
50
+ * @public
51
+ */
52
+ function useCascadeDrillDown(cascadeStack, squashCascade, squashAt) {
53
+ return (0, react_1.useCallback)((depth, entityType, entityId, extra) => {
54
+ const nextEntry = cascadeStack[depth + 1];
55
+ if ((nextEntry === null || nextEntry === void 0 ? void 0 : nextEntry.entityType) === entityType && nextEntry.entityId === entityId) {
56
+ squashCascade(cascadeStack.slice(0, depth + 1));
57
+ return;
58
+ }
59
+ squashAt(depth, Object.assign({ entityType, entityId, mode: 'view' }, extra));
60
+ }, [cascadeStack, squashAt, squashCascade]);
61
+ }
62
+ //# sourceMappingURL=useCascadeTransitions.js.map
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Generic detail-view primitive components.
3
+ *
4
+ * Domain-agnostic building blocks for entity detail panels:
5
+ * - `DetailSection` — labeled section wrapper
6
+ * - `DetailRow` — key/value row with spread or inline layout
7
+ * - `TagList` — pill tag row
8
+ * - `StatusBadge` — colored pill badge (caller supplies color classes)
9
+ * - `DetailHeader` — two-line header with indicators and action slots
10
+ *
11
+ * @packageDocumentation
12
+ */
13
+ import React from 'react';
14
+ export interface IDetailSectionProps {
15
+ readonly title: string;
16
+ readonly children: React.ReactNode;
17
+ }
18
+ /**
19
+ * Labeled section wrapper with uppercase tracking header.
20
+ * @public
21
+ */
22
+ export declare function DetailSection({ title, children }: IDetailSectionProps): React.ReactElement;
23
+ export interface IDetailRowProps {
24
+ readonly label: string;
25
+ readonly value: React.ReactNode;
26
+ /**
27
+ * Layout variant:
28
+ * - `'spread'` (default) — label left, value right, justify-between
29
+ * - `'inline'` — label fixed-width, value follows immediately
30
+ */
31
+ readonly layout?: 'spread' | 'inline';
32
+ }
33
+ /**
34
+ * Key/value row for detail panels.
35
+ * Returns `null` if `value` is `null` or `undefined`.
36
+ * @public
37
+ */
38
+ export declare function DetailRow({ label, value, layout }: IDetailRowProps): React.ReactElement | null;
39
+ export interface ITagListProps {
40
+ readonly tags: ReadonlyArray<string>;
41
+ }
42
+ /**
43
+ * Horizontal pill row for string tags. Returns `null` if tags is empty.
44
+ * @public
45
+ */
46
+ export declare function TagList({ tags }: ITagListProps): React.ReactElement | null;
47
+ export interface IStatusBadgeProps {
48
+ readonly label: string;
49
+ /** Tailwind color classes, e.g. `'bg-amber-100 text-amber-800'` */
50
+ readonly colorClass: string;
51
+ }
52
+ /**
53
+ * Generic pill badge. Caller supplies the Tailwind color classes.
54
+ * @public
55
+ */
56
+ export declare function StatusBadge({ label, colorClass }: IStatusBadgeProps): React.ReactElement;
57
+ export interface IDetailHeaderProps {
58
+ /** Primary entity name — rendered full-width on its own line */
59
+ readonly title: string;
60
+ /** Optional de-emphasized subtitle (e.g. entity ID) rendered below the title */
61
+ readonly subtitle?: string;
62
+ /** Optional description rendered below the status bar */
63
+ readonly description?: string;
64
+ /** Left slot of the status bar — badges, icons, etc. */
65
+ readonly indicators?: React.ReactNode;
66
+ /** Right slot of the status bar — action buttons */
67
+ readonly actions?: React.ReactNode;
68
+ /** If provided, renders a close button in the upper-right corner, inline with the title */
69
+ readonly onClose?: () => void;
70
+ }
71
+ /**
72
+ * Three-line entity detail header.
73
+ *
74
+ * Line 1: full-width `title` (bold headline)
75
+ * Line 2: optional `subtitle` (de-emphasized, monospace — e.g. entity ID)
76
+ * Line 3: `indicators` left-justified, `actions` right-justified (status bar)
77
+ * Below: optional `description`
78
+ *
79
+ * Both `indicators` and `actions` are `React.ReactNode` — callers own the content.
80
+ * @public
81
+ */
82
+ export declare function DetailHeader({ title, subtitle, description, indicators, actions, onClose }: IDetailHeaderProps): React.ReactElement;
83
+ //# sourceMappingURL=DetailHelpers.d.ts.map
@@ -0,0 +1,113 @@
1
+ "use strict";
2
+ /*
3
+ * Copyright (c) 2026 Erik Fortune
4
+ *
5
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ * of this software and associated documentation files (the "Software"), to deal
7
+ * in the Software without restriction, including without limitation the rights
8
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ * copies of the Software, and to permit persons to whom the Software is
10
+ * furnished to do so, subject to the following conditions:
11
+ *
12
+ * The above copyright notice and this permission notice shall be included in all
13
+ * copies or substantial portions of the Software.
14
+ *
15
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ * SOFTWARE.
22
+ */
23
+ var __importDefault = (this && this.__importDefault) || function (mod) {
24
+ return (mod && mod.__esModule) ? mod : { "default": mod };
25
+ };
26
+ Object.defineProperty(exports, "__esModule", { value: true });
27
+ exports.DetailSection = DetailSection;
28
+ exports.DetailRow = DetailRow;
29
+ exports.TagList = TagList;
30
+ exports.StatusBadge = StatusBadge;
31
+ exports.DetailHeader = DetailHeader;
32
+ /**
33
+ * Generic detail-view primitive components.
34
+ *
35
+ * Domain-agnostic building blocks for entity detail panels:
36
+ * - `DetailSection` — labeled section wrapper
37
+ * - `DetailRow` — key/value row with spread or inline layout
38
+ * - `TagList` — pill tag row
39
+ * - `StatusBadge` — colored pill badge (caller supplies color classes)
40
+ * - `DetailHeader` — two-line header with indicators and action slots
41
+ *
42
+ * @packageDocumentation
43
+ */
44
+ const react_1 = __importDefault(require("react"));
45
+ const solid_1 = require("@heroicons/react/20/solid");
46
+ /**
47
+ * Labeled section wrapper with uppercase tracking header.
48
+ * @public
49
+ */
50
+ function DetailSection({ title, children }) {
51
+ return (react_1.default.createElement("div", { className: "mb-4" },
52
+ react_1.default.createElement("h3", { className: "text-xs font-semibold text-muted uppercase tracking-wider mb-1.5" }, title),
53
+ children));
54
+ }
55
+ /**
56
+ * Key/value row for detail panels.
57
+ * Returns `null` if `value` is `null` or `undefined`.
58
+ * @public
59
+ */
60
+ function DetailRow({ label, value, layout = 'spread' }) {
61
+ if (value === null || value === undefined) {
62
+ return null;
63
+ }
64
+ if (layout === 'inline') {
65
+ return (react_1.default.createElement("div", { className: "flex items-baseline gap-2 py-0.5" },
66
+ react_1.default.createElement("span", { className: "text-xs text-muted w-32 shrink-0" }, label),
67
+ react_1.default.createElement("span", { className: "text-sm text-primary" }, value)));
68
+ }
69
+ return (react_1.default.createElement("div", { className: "flex items-baseline justify-between py-0.5 text-sm" },
70
+ react_1.default.createElement("span", { className: "text-muted shrink-0 mr-2" }, label),
71
+ react_1.default.createElement("span", { className: "text-primary text-right" }, value)));
72
+ }
73
+ /**
74
+ * Horizontal pill row for string tags. Returns `null` if tags is empty.
75
+ * @public
76
+ */
77
+ function TagList({ tags }) {
78
+ if (tags.length === 0) {
79
+ return null;
80
+ }
81
+ return (react_1.default.createElement("div", { className: "flex flex-wrap gap-1" }, tags.map((tag) => (react_1.default.createElement("span", { key: tag, className: "px-2 py-0.5 text-xs rounded-full bg-surface-raised text-secondary" }, tag)))));
82
+ }
83
+ /**
84
+ * Generic pill badge. Caller supplies the Tailwind color classes.
85
+ * @public
86
+ */
87
+ function StatusBadge({ label, colorClass }) {
88
+ return (react_1.default.createElement("span", { className: `inline-block px-2 py-0.5 text-[11px] font-medium rounded-full ${colorClass}` }, label));
89
+ }
90
+ /**
91
+ * Three-line entity detail header.
92
+ *
93
+ * Line 1: full-width `title` (bold headline)
94
+ * Line 2: optional `subtitle` (de-emphasized, monospace — e.g. entity ID)
95
+ * Line 3: `indicators` left-justified, `actions` right-justified (status bar)
96
+ * Below: optional `description`
97
+ *
98
+ * Both `indicators` and `actions` are `React.ReactNode` — callers own the content.
99
+ * @public
100
+ */
101
+ function DetailHeader({ title, subtitle, description, indicators, actions, onClose }) {
102
+ return (react_1.default.createElement("div", { className: "mb-4 relative" },
103
+ react_1.default.createElement("div", { className: "flex items-start" },
104
+ react_1.default.createElement("h2", { className: "text-lg font-semibold text-primary flex-1 min-w-0" }, title),
105
+ onClose && (react_1.default.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" },
106
+ react_1.default.createElement(solid_1.XMarkIcon, { className: "w-4 h-4" })))),
107
+ subtitle && react_1.default.createElement("p", { className: "text-xs text-muted font-mono mt-0.5 mb-1" }, subtitle),
108
+ (indicators !== undefined || actions !== undefined) && (react_1.default.createElement("div", { className: "flex items-center justify-between gap-2 mt-1" },
109
+ react_1.default.createElement("div", { className: "flex items-center gap-2" }, indicators),
110
+ actions !== undefined && react_1.default.createElement("div", { className: "flex items-center gap-1 shrink-0" }, actions))),
111
+ description && react_1.default.createElement("p", { className: "text-sm text-secondary mt-1" }, description)));
112
+ }
113
+ //# sourceMappingURL=DetailHelpers.js.map
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Detail packlet - generic detail-view primitive components.
3
+ * @packageDocumentation
4
+ */
5
+ export { DetailSection, type IDetailSectionProps, DetailRow, type IDetailRowProps, TagList, type ITagListProps, StatusBadge, type IStatusBadgeProps, DetailHeader, type IDetailHeaderProps } from './DetailHelpers';
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ /**
3
+ * Detail packlet - generic detail-view primitive components.
4
+ * @packageDocumentation
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.DetailHeader = exports.StatusBadge = exports.TagList = exports.DetailRow = exports.DetailSection = void 0;
8
+ var DetailHelpers_1 = require("./DetailHelpers");
9
+ Object.defineProperty(exports, "DetailSection", { enumerable: true, get: function () { return DetailHelpers_1.DetailSection; } });
10
+ Object.defineProperty(exports, "DetailRow", { enumerable: true, get: function () { return DetailHelpers_1.DetailRow; } });
11
+ Object.defineProperty(exports, "TagList", { enumerable: true, get: function () { return DetailHelpers_1.TagList; } });
12
+ Object.defineProperty(exports, "StatusBadge", { enumerable: true, get: function () { return DetailHelpers_1.StatusBadge; } });
13
+ Object.defineProperty(exports, "DetailHeader", { enumerable: true, get: function () { return DetailHelpers_1.DetailHeader; } });
14
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,40 @@
1
+ import React from 'react';
2
+ import { Converter } from '@fgv/ts-utils';
3
+ /**
4
+ * Strips markdown code fences from text.
5
+ * AI agents often wrap JSON in code fences.
6
+ * @param text - Raw text that may contain code fences
7
+ * @returns Text with outer code fences removed
8
+ * @internal
9
+ */
10
+ export declare function stripCodeFences(text: string): string;
11
+ /**
12
+ * Props for the JsonDropZone component.
13
+ * @typeParam T - The expected validated type
14
+ * @public
15
+ */
16
+ export interface IJsonDropZoneProps<T> {
17
+ /** Converter to validate parsed JSON against */
18
+ readonly converter: Converter<T>;
19
+ /** Called with the validated value on successful drop/paste */
20
+ readonly onValueReceived: (value: T) => void;
21
+ /** Called with an error message on parse/validation failure */
22
+ readonly onError?: (message: string) => void;
23
+ /** Externally-controlled error message to display */
24
+ readonly error?: string;
25
+ /** Placeholder hint text */
26
+ readonly hint?: string;
27
+ /** Additional CSS class names */
28
+ readonly className?: string;
29
+ }
30
+ /**
31
+ * Generic JSON drop/paste target with converter-based validation.
32
+ * Accepts text via drag-and-drop or paste, strips markdown code fences,
33
+ * parses as JSON, validates through the provided converter, and calls
34
+ * the appropriate callback.
35
+ *
36
+ * @typeParam T - The expected validated type
37
+ * @public
38
+ */
39
+ export declare function JsonDropZone<T>(props: IJsonDropZoneProps<T>): React.ReactElement;
40
+ //# sourceMappingURL=JsonDropZone.d.ts.map