@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,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
|