@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
package/README.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# @fgv/ts-app-shell
|
|
2
|
+
|
|
3
|
+
Shared React UI primitives for application shells in the `@fgv` monorepo.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Column Cascade** — Master-detail drill-down with horizontal scrolling, breadcrumb navigation, and min-width columns
|
|
8
|
+
- **Compact Sidebar** — Filter rows with summary text and flyout selectors that overlay the main pane
|
|
9
|
+
- **Toast Notifications** — Ephemeral notifications with auto-dismiss and actionable links
|
|
10
|
+
- **Log Message Panel** — Collapsible bottom panel with severity-filtered message history
|
|
11
|
+
- **Command Palette** — Cmd+K quick navigation overlay
|
|
12
|
+
- **Keybinding Registry** — Infrastructure for registering and managing keyboard shortcuts
|
|
13
|
+
|
|
14
|
+
## Usage
|
|
15
|
+
|
|
16
|
+
```typescript
|
|
17
|
+
import { /* components */ } from '@fgv/ts-app-shell';
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Development
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
rushx build # Build the library
|
|
24
|
+
rushx test # Run tests
|
|
25
|
+
rushx coverage # Run tests with coverage
|
|
26
|
+
```
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* \@fgv/ts-app-shell - Shared React UI primitives for application shells.
|
|
3
|
+
*
|
|
4
|
+
* Provides reusable components for:
|
|
5
|
+
* - Column cascade (master-detail drill-down)
|
|
6
|
+
* - Compact sidebar with flyout filter selectors
|
|
7
|
+
* - Toast notifications and log message panel
|
|
8
|
+
* - Command palette
|
|
9
|
+
* - Keybinding registry
|
|
10
|
+
*
|
|
11
|
+
* @packageDocumentation
|
|
12
|
+
*/
|
|
13
|
+
// Top bar components
|
|
14
|
+
export * from './packlets/top-bar';
|
|
15
|
+
// Messages infrastructure (context, toasts, status bar)
|
|
16
|
+
export * from './packlets/messages';
|
|
17
|
+
// URL hash synchronization for mode/tab navigation
|
|
18
|
+
export * from './packlets/url-sync';
|
|
19
|
+
// Modal dialog
|
|
20
|
+
export * from './packlets/modal';
|
|
21
|
+
// Keyboard shortcut registry
|
|
22
|
+
export * from './packlets/keyboard';
|
|
23
|
+
// Sidebar layout, search, filters, and entity list
|
|
24
|
+
export * from './packlets/sidebar';
|
|
25
|
+
// Column cascade container
|
|
26
|
+
export * from './packlets/cascade';
|
|
27
|
+
// Selectors (PreferredSelector popover)
|
|
28
|
+
export * from './packlets/selectors';
|
|
29
|
+
// JSON drop zone (generic drop/paste target with converter validation)
|
|
30
|
+
export * from './packlets/drop-zone';
|
|
31
|
+
// Generic edit field primitives for entity editors
|
|
32
|
+
export * from './packlets/editing';
|
|
33
|
+
// Generic detail-view primitive components
|
|
34
|
+
export * from './packlets/detail';
|
|
35
|
+
// AI assist hook (generic, context-free)
|
|
36
|
+
export * from './packlets/ai-assist';
|
|
37
|
+
// Print popup window enclosure
|
|
38
|
+
export * from './packlets/print';
|
|
39
|
+
// Theme provider and hook
|
|
40
|
+
export * from './packlets/theme';
|
|
41
|
+
// Responsive layout detection and context
|
|
42
|
+
export * from './packlets/responsive';
|
|
43
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,219 @@
|
|
|
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 AI assist hook — parameterized by settings and keystore, no app-specific context.
|
|
24
|
+
*
|
|
25
|
+
* Provides a list of available AI assist actions and functions for copy-paste
|
|
26
|
+
* and direct API generation flows.
|
|
27
|
+
*
|
|
28
|
+
* @packageDocumentation
|
|
29
|
+
*/
|
|
30
|
+
import { useCallback, useMemo, useState } from 'react';
|
|
31
|
+
import { AiAssist } from '@fgv/ts-extras';
|
|
32
|
+
import { fail, succeed } from '@fgv/ts-utils';
|
|
33
|
+
// ============================================================================
|
|
34
|
+
// Helpers
|
|
35
|
+
// ============================================================================
|
|
36
|
+
/**
|
|
37
|
+
* Checks whether a parsed AI response is an error object (with an "error" field)
|
|
38
|
+
* rather than a valid entity. AI prompts instruct the model to return
|
|
39
|
+
* `{ "error": "...", "term": "..." }` when it cannot confidently generate the entity.
|
|
40
|
+
*
|
|
41
|
+
* @param parsed - The parsed JSON from the AI response
|
|
42
|
+
* @returns A failure Result with the error message if it's an error object, or undefined if not
|
|
43
|
+
*/
|
|
44
|
+
export function checkForAiErrorObject(parsed) {
|
|
45
|
+
if (typeof parsed === 'object' &&
|
|
46
|
+
parsed !== null &&
|
|
47
|
+
'error' in parsed &&
|
|
48
|
+
typeof parsed.error === 'string') {
|
|
49
|
+
const errorObj = parsed;
|
|
50
|
+
const term = typeof errorObj.term === 'string' ? ` (term: "${errorObj.term}")` : '';
|
|
51
|
+
return fail(`AI declined to generate${term}: ${errorObj.error}`);
|
|
52
|
+
}
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
// ============================================================================
|
|
56
|
+
// Hook
|
|
57
|
+
// ============================================================================
|
|
58
|
+
/**
|
|
59
|
+
* Generic hook providing AI assist actions based on supplied settings and keystore.
|
|
60
|
+
* @param params - Settings and keystore to use (typically from a workspace or app context)
|
|
61
|
+
* @returns Available actions and execution functions
|
|
62
|
+
* @public
|
|
63
|
+
*/
|
|
64
|
+
export function useAiAssist(params) {
|
|
65
|
+
const { settings, keyStore, logger } = params;
|
|
66
|
+
const [isWorking, setIsWorking] = useState(false);
|
|
67
|
+
const actions = useMemo(() => {
|
|
68
|
+
var _a, _b, _c;
|
|
69
|
+
const providers = (_a = settings === null || settings === void 0 ? void 0 : settings.providers) !== null && _a !== void 0 ? _a : [{ provider: 'copy-paste' }];
|
|
70
|
+
const enabledSet = new Set(providers.map((p) => p.provider));
|
|
71
|
+
const defaultProvider = (settings === null || settings === void 0 ? void 0 : settings.defaultProvider) && enabledSet.has(settings.defaultProvider)
|
|
72
|
+
? settings.defaultProvider
|
|
73
|
+
: (_c = (_b = providers[0]) === null || _b === void 0 ? void 0 : _b.provider) !== null && _c !== void 0 ? _c : 'copy-paste';
|
|
74
|
+
return providers.map((config) => {
|
|
75
|
+
var _a, _b;
|
|
76
|
+
let isAvailable = true;
|
|
77
|
+
let unavailableReason;
|
|
78
|
+
if (config.provider !== 'copy-paste') {
|
|
79
|
+
// API-based providers need a secret name and unlocked keystore with that secret
|
|
80
|
+
if (!config.secretName) {
|
|
81
|
+
isAvailable = false;
|
|
82
|
+
unavailableReason = 'No API key secret configured';
|
|
83
|
+
}
|
|
84
|
+
else if (!keyStore) {
|
|
85
|
+
isAvailable = false;
|
|
86
|
+
unavailableReason = 'No keystore available';
|
|
87
|
+
}
|
|
88
|
+
else if (!keyStore.isUnlocked) {
|
|
89
|
+
isAvailable = false;
|
|
90
|
+
unavailableReason = 'Keystore is locked';
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
const hasSecret = keyStore.hasSecret(config.secretName);
|
|
94
|
+
if (hasSecret.isFailure() || !hasSecret.value) {
|
|
95
|
+
isAvailable = false;
|
|
96
|
+
unavailableReason = `API key "${config.secretName}" not found in keystore`;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const label = (_b = (_a = AiAssist.getProviderDescriptor(config.provider).orDefault()) === null || _a === void 0 ? void 0 : _a.buttonLabel) !== null && _b !== void 0 ? _b : config.provider;
|
|
101
|
+
return {
|
|
102
|
+
provider: config.provider,
|
|
103
|
+
label,
|
|
104
|
+
isDefault: config.provider === defaultProvider,
|
|
105
|
+
isAvailable,
|
|
106
|
+
unavailableReason
|
|
107
|
+
};
|
|
108
|
+
});
|
|
109
|
+
}, [settings, keyStore]);
|
|
110
|
+
const copyPrompt = useCallback(async (prompt) => {
|
|
111
|
+
try {
|
|
112
|
+
await navigator.clipboard.writeText(prompt.combined);
|
|
113
|
+
return succeed('copied');
|
|
114
|
+
}
|
|
115
|
+
catch (_a) {
|
|
116
|
+
return fail('Failed to copy prompt to clipboard');
|
|
117
|
+
}
|
|
118
|
+
}, []);
|
|
119
|
+
const generateDirect = useCallback(async (provider, prompt, convert, tools) => {
|
|
120
|
+
// Find the provider config and descriptor
|
|
121
|
+
const providerConfig = settings === null || settings === void 0 ? void 0 : settings.providers.find((p) => p.provider === provider);
|
|
122
|
+
if (!providerConfig) {
|
|
123
|
+
return fail(`Provider "${provider}" not configured`);
|
|
124
|
+
}
|
|
125
|
+
const descriptorResult = AiAssist.getProviderDescriptor(provider);
|
|
126
|
+
if (descriptorResult.isFailure()) {
|
|
127
|
+
return fail(descriptorResult.message);
|
|
128
|
+
}
|
|
129
|
+
const descriptor = descriptorResult.value;
|
|
130
|
+
if (!providerConfig.secretName) {
|
|
131
|
+
return fail(`Provider "${provider}" has no secret name configured`);
|
|
132
|
+
}
|
|
133
|
+
if (!keyStore) {
|
|
134
|
+
return fail('No keystore available');
|
|
135
|
+
}
|
|
136
|
+
// Get API key from keystore
|
|
137
|
+
const apiKeyResult = keyStore.getApiKey(providerConfig.secretName);
|
|
138
|
+
if (apiKeyResult.isFailure()) {
|
|
139
|
+
return fail(`Failed to get API key: ${apiKeyResult.message}`);
|
|
140
|
+
}
|
|
141
|
+
setIsWorking(true);
|
|
142
|
+
try {
|
|
143
|
+
const maxAttempts = 3;
|
|
144
|
+
const correctionMessages = [];
|
|
145
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
146
|
+
// Resolve effective tools: per-call override > settings defaults > none
|
|
147
|
+
const effectiveTools = AiAssist.resolveEffectiveTools(descriptor, providerConfig.tools, tools);
|
|
148
|
+
// Call the API — through proxy if configured, otherwise direct
|
|
149
|
+
const completionParams = {
|
|
150
|
+
descriptor,
|
|
151
|
+
apiKey: apiKeyResult.value,
|
|
152
|
+
prompt,
|
|
153
|
+
additionalMessages: correctionMessages.length > 0 ? correctionMessages : undefined,
|
|
154
|
+
modelOverride: providerConfig.model,
|
|
155
|
+
logger,
|
|
156
|
+
tools: effectiveTools.length > 0 ? effectiveTools : undefined
|
|
157
|
+
};
|
|
158
|
+
const useProxy = !!(settings === null || settings === void 0 ? void 0 : settings.proxyUrl) && (settings.proxyAllProviders === true || descriptor.corsRestricted);
|
|
159
|
+
const responseResult = useProxy
|
|
160
|
+
? await AiAssist.callProxiedCompletion(settings.proxyUrl, completionParams)
|
|
161
|
+
: await AiAssist.callProviderCompletion(completionParams);
|
|
162
|
+
if (responseResult.isFailure()) {
|
|
163
|
+
logger === null || logger === void 0 ? void 0 : logger.error(`AI completion failed: ${responseResult.message}`);
|
|
164
|
+
return fail(responseResult.message);
|
|
165
|
+
}
|
|
166
|
+
const { content: rawResponse, truncated } = responseResult.value;
|
|
167
|
+
logger === null || logger === void 0 ? void 0 : logger.detail(`AI response received (${rawResponse.length} chars, truncated=${truncated})`);
|
|
168
|
+
// Truncated responses are almost certainly malformed JSON — fail early
|
|
169
|
+
if (truncated) {
|
|
170
|
+
logger === null || logger === void 0 ? void 0 : logger.warn('AI response truncated due to token limits');
|
|
171
|
+
return fail('AI response was truncated due to token limits — try a shorter prompt');
|
|
172
|
+
}
|
|
173
|
+
// Strip markdown code fences and parse JSON
|
|
174
|
+
const stripped = rawResponse
|
|
175
|
+
.trim()
|
|
176
|
+
.replace(/^```(?:\w+)?\s*\n?([\s\S]*?)\n?\s*```$/, '$1')
|
|
177
|
+
.trim();
|
|
178
|
+
let parsed;
|
|
179
|
+
try {
|
|
180
|
+
parsed = JSON.parse(stripped);
|
|
181
|
+
}
|
|
182
|
+
catch (err) {
|
|
183
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
184
|
+
// JSON parse failures are not retryable — the model isn't producing valid JSON at all
|
|
185
|
+
return fail(`AI returned invalid JSON: ${detail}`);
|
|
186
|
+
}
|
|
187
|
+
// Check for AI error object (model declined to generate)
|
|
188
|
+
const aiError = checkForAiErrorObject(parsed);
|
|
189
|
+
if (aiError !== undefined) {
|
|
190
|
+
return aiError;
|
|
191
|
+
}
|
|
192
|
+
// Validate with the provided converter
|
|
193
|
+
const entityResult = convert(parsed);
|
|
194
|
+
if (entityResult.isSuccess()) {
|
|
195
|
+
return succeed({ entity: entityResult.value, source: 'ai' });
|
|
196
|
+
}
|
|
197
|
+
// Validation failed — if we have retries left, send a correction
|
|
198
|
+
logger === null || logger === void 0 ? void 0 : logger.warn(`AI response validation failed (attempt ${attempt + 1}/${maxAttempts}): ${entityResult.message}`);
|
|
199
|
+
if (attempt < maxAttempts - 1) {
|
|
200
|
+
correctionMessages.push({ role: 'assistant', content: rawResponse }, {
|
|
201
|
+
role: 'user',
|
|
202
|
+
content: `The JSON you returned failed validation with the following error:\n\n${entityResult.message}\n\nPlease fix the JSON and return ONLY the corrected JSON object, nothing else.`
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
// Out of retries
|
|
207
|
+
return fail(`AI response validation failed after ${maxAttempts} attempts: ${entityResult.message}`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
// Unreachable, but TypeScript needs it
|
|
211
|
+
return fail('AI generation failed');
|
|
212
|
+
}
|
|
213
|
+
finally {
|
|
214
|
+
setIsWorking(false);
|
|
215
|
+
}
|
|
216
|
+
}, [settings, keyStore, logger]);
|
|
217
|
+
return { actions, isWorking, copyPrompt, generateDirect };
|
|
218
|
+
}
|
|
219
|
+
//# sourceMappingURL=useAiAssist.js.map
|
|
@@ -0,0 +1,83 @@
|
|
|
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
|
+
* Column cascade container — horizontal scroll of detail columns.
|
|
24
|
+
* @packageDocumentation
|
|
25
|
+
*/
|
|
26
|
+
import React, { useCallback, useEffect, useRef } from 'react';
|
|
27
|
+
import { useResponsive } from '../responsive';
|
|
28
|
+
import { MobileCascadeStack } from './MobileCascadeStack';
|
|
29
|
+
// ============================================================================
|
|
30
|
+
// CascadeContainer Component
|
|
31
|
+
// ============================================================================
|
|
32
|
+
/**
|
|
33
|
+
* Horizontal scroll container for the column cascade.
|
|
34
|
+
*
|
|
35
|
+
* Renders a breadcrumb trail at the top and horizontally-scrollable
|
|
36
|
+
* detail columns below. Auto-scrolls to the rightmost column when
|
|
37
|
+
* a new column is pushed.
|
|
38
|
+
*
|
|
39
|
+
* @public
|
|
40
|
+
*/
|
|
41
|
+
export function CascadeContainer(props) {
|
|
42
|
+
const { columns, onPopTo, minColumnWidth = '400px', onFocus, rootLabel = 'List' } = props;
|
|
43
|
+
const { layoutMode } = useResponsive();
|
|
44
|
+
const scrollRef = useRef(null);
|
|
45
|
+
const handleMouseDown = useCallback(() => {
|
|
46
|
+
onFocus === null || onFocus === void 0 ? void 0 : onFocus();
|
|
47
|
+
}, [onFocus]);
|
|
48
|
+
const handleKeyDown = useCallback((e) => {
|
|
49
|
+
if (e.key === 'Escape' || e.key === 'ArrowLeft') {
|
|
50
|
+
e.preventDefault();
|
|
51
|
+
e.stopPropagation();
|
|
52
|
+
if (columns.length > 1) {
|
|
53
|
+
onPopTo(columns.length - 1);
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
onPopTo(0);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}, [columns.length, onPopTo]);
|
|
60
|
+
// Auto-scroll to rightmost column when columns change
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
if (scrollRef.current) {
|
|
63
|
+
scrollRef.current.scrollTo({
|
|
64
|
+
left: scrollRef.current.scrollWidth,
|
|
65
|
+
behavior: 'smooth'
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}, [columns.length]);
|
|
69
|
+
if (layoutMode === 'mobile') {
|
|
70
|
+
return React.createElement(MobileCascadeStack, Object.assign({}, props));
|
|
71
|
+
}
|
|
72
|
+
if (columns.length === 0) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
return (React.createElement("div", { className: "flex flex-col flex-1 overflow-hidden outline-none", tabIndex: -1, onKeyDown: handleKeyDown },
|
|
76
|
+
React.createElement("div", { className: "flex items-center gap-1 px-3 py-1.5 bg-surface-alt border-b border-border text-xs shrink-0 overflow-x-auto" },
|
|
77
|
+
React.createElement("button", { onClick: () => onPopTo(0), className: "text-brand-accent hover:text-brand-primary hover:underline shrink-0" }, rootLabel),
|
|
78
|
+
columns.map((col, idx) => (React.createElement(React.Fragment, { key: col.key },
|
|
79
|
+
React.createElement("span", { className: "text-muted shrink-0" }, "/"),
|
|
80
|
+
idx < columns.length - 1 ? (React.createElement("button", { onClick: () => onPopTo(idx + 1), className: "text-brand-accent hover:text-brand-primary hover:underline truncate max-w-[200px] shrink-0" }, col.label)) : (React.createElement("span", { className: "text-secondary font-medium truncate max-w-[200px] shrink-0" }, col.label)))))),
|
|
81
|
+
React.createElement("div", { ref: scrollRef, className: "flex flex-1 overflow-x-auto overflow-y-hidden", onMouseDown: handleMouseDown }, columns.map((col) => (React.createElement("div", { key: col.key, className: "flex flex-col shrink-0 border-r border-border overflow-y-auto", style: { minWidth: minColumnWidth, width: minColumnWidth } }, col.content))))));
|
|
82
|
+
}
|
|
83
|
+
//# sourceMappingURL=CascadeContainer.js.map
|
|
@@ -0,0 +1,48 @@
|
|
|
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
|
+
* ComparisonView — side-by-side read-only comparison of 2–4 entities.
|
|
24
|
+
* @packageDocumentation
|
|
25
|
+
*/
|
|
26
|
+
import React from 'react';
|
|
27
|
+
// ============================================================================
|
|
28
|
+
// ComparisonView Component
|
|
29
|
+
// ============================================================================
|
|
30
|
+
/**
|
|
31
|
+
* Side-by-side comparison view for entities.
|
|
32
|
+
*
|
|
33
|
+
* Renders 2–4 entity detail views in equal-width columns with
|
|
34
|
+
* synchronized scrolling (future) and column headers.
|
|
35
|
+
*
|
|
36
|
+
* @public
|
|
37
|
+
*/
|
|
38
|
+
export function ComparisonView(props) {
|
|
39
|
+
const { columns } = props;
|
|
40
|
+
if (columns.length < 2) {
|
|
41
|
+
return (React.createElement("div", { className: "flex flex-1 items-center justify-center text-muted text-sm" }, "Select at least 2 items to compare."));
|
|
42
|
+
}
|
|
43
|
+
return (React.createElement("div", { className: "flex flex-1 overflow-hidden" }, columns.map((col) => (React.createElement("div", { key: col.key, className: "flex flex-col flex-1 min-w-0 border-r border-border last:border-r-0 overflow-hidden" },
|
|
44
|
+
React.createElement("div", { className: "px-3 py-1.5 bg-surface-alt border-b border-border shrink-0" },
|
|
45
|
+
React.createElement("span", { className: "text-xs font-medium text-secondary truncate block" }, col.label)),
|
|
46
|
+
React.createElement("div", { className: "flex-1 overflow-y-auto" }, col.content))))));
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=ComparisonView.js.map
|
|
@@ -0,0 +1,104 @@
|
|
|
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
|
+
* EntityTabLayout — shared layout for entity tab content with list + cascade + collapse-on-focus.
|
|
24
|
+
* @packageDocumentation
|
|
25
|
+
*/
|
|
26
|
+
import React from 'react';
|
|
27
|
+
import { CascadeContainer } from './CascadeContainer';
|
|
28
|
+
import { useResponsive } from '../responsive';
|
|
29
|
+
import { ComparisonView } from './ComparisonView';
|
|
30
|
+
// ============================================================================
|
|
31
|
+
// EntityTabLayout Component
|
|
32
|
+
// ============================================================================
|
|
33
|
+
/**
|
|
34
|
+
* Shared layout for entity tab content.
|
|
35
|
+
*
|
|
36
|
+
* Renders an entity list on the left and a cascade container on the right.
|
|
37
|
+
* The list stays expanded while browsing (selecting items in the list).
|
|
38
|
+
* It collapses when the user clicks inside the cascade detail pane,
|
|
39
|
+
* signaling they are focused on the detail rather than browsing.
|
|
40
|
+
*
|
|
41
|
+
* @public
|
|
42
|
+
*/
|
|
43
|
+
export function EntityTabLayout(props) {
|
|
44
|
+
var _a;
|
|
45
|
+
const { list, cascadeColumns, onPopTo, listCollapsed, onListCollapse, compareMode, comparisonColumns } = props;
|
|
46
|
+
const variationCompareColumns = props.variationCompareColumns;
|
|
47
|
+
const onExitVariationCompare = props.onExitVariationCompare;
|
|
48
|
+
const showingComparison = (_a = props.showingComparison) !== null && _a !== void 0 ? _a : false;
|
|
49
|
+
const onExitComparison = props.onExitComparison;
|
|
50
|
+
const { layoutMode } = useResponsive();
|
|
51
|
+
const isVariationCompare = variationCompareColumns !== undefined && variationCompareColumns.length >= 2;
|
|
52
|
+
const showComparison = !isVariationCompare &&
|
|
53
|
+
showingComparison &&
|
|
54
|
+
comparisonColumns !== undefined &&
|
|
55
|
+
comparisonColumns.length >= 2;
|
|
56
|
+
const showCascade = !compareMode && !isVariationCompare && !showComparison && cascadeColumns.length > 0;
|
|
57
|
+
const hasCascadeOrCompare = cascadeColumns.length > 0 || showComparison;
|
|
58
|
+
// Variation compare banner — shared between mobile and desktop
|
|
59
|
+
const variationCompareBanner = isVariationCompare && onExitVariationCompare && (React.createElement("div", { className: "flex items-center gap-2 px-3 py-1.5 bg-status-warning-bg border-b border-status-warning-border shrink-0" },
|
|
60
|
+
React.createElement("span", { className: "text-xs text-status-warning-text" },
|
|
61
|
+
"Comparing ",
|
|
62
|
+
variationCompareColumns.length,
|
|
63
|
+
" variations"),
|
|
64
|
+
React.createElement("button", { onClick: onExitVariationCompare, className: "px-2 py-0.5 text-xs rounded border border-status-warning-border text-status-warning-text hover:bg-status-warning-surface transition-colors" }, "Exit")));
|
|
65
|
+
// Mobile: show one pane at a time — list or cascade/comparison full-screen
|
|
66
|
+
if (layoutMode === 'mobile') {
|
|
67
|
+
const hasCascadeContent = showCascade || showComparison || isVariationCompare;
|
|
68
|
+
return (React.createElement("div", { className: "flex flex-col flex-1 overflow-hidden" },
|
|
69
|
+
variationCompareBanner,
|
|
70
|
+
hasCascadeContent ? (React.createElement(React.Fragment, null,
|
|
71
|
+
showCascade && React.createElement(CascadeContainer, { columns: cascadeColumns, onPopTo: onPopTo }),
|
|
72
|
+
showComparison && (React.createElement("div", { className: "flex flex-col flex-1 overflow-hidden" },
|
|
73
|
+
React.createElement("div", { className: "flex items-center gap-2 px-3 py-1.5 bg-status-info-bg border-b border-status-info-border shrink-0" },
|
|
74
|
+
React.createElement("span", { className: "text-xs text-status-info-text" },
|
|
75
|
+
"Comparing ",
|
|
76
|
+
comparisonColumns.length,
|
|
77
|
+
" items"),
|
|
78
|
+
onExitComparison && (React.createElement("button", { onClick: onExitComparison, className: "px-2 py-0.5 text-xs rounded border border-status-info-border text-status-info-text hover:bg-status-info-surface transition-colors" }, "\u2190 Back to list"))),
|
|
79
|
+
React.createElement(ComparisonView, { columns: comparisonColumns }))),
|
|
80
|
+
isVariationCompare && React.createElement(ComparisonView, { columns: variationCompareColumns }))) : (React.createElement("div", { className: "flex flex-col flex-1 overflow-hidden" }, list))));
|
|
81
|
+
}
|
|
82
|
+
// Desktop/compact: side-by-side list and cascade
|
|
83
|
+
return (React.createElement("div", { className: "flex flex-col flex-1 overflow-hidden" },
|
|
84
|
+
variationCompareBanner,
|
|
85
|
+
React.createElement("div", { className: "flex flex-1 overflow-hidden" },
|
|
86
|
+
React.createElement("div", { className: `flex flex-col overflow-hidden transition-all ${isVariationCompare || showComparison
|
|
87
|
+
? 'w-0 min-w-0'
|
|
88
|
+
: listCollapsed
|
|
89
|
+
? 'w-0 min-w-0'
|
|
90
|
+
: hasCascadeOrCompare
|
|
91
|
+
? 'w-1/4 max-w-xs shrink-0 border-r border-border'
|
|
92
|
+
: 'w-full max-w-sm shrink-0 border-r border-border'}` }, list),
|
|
93
|
+
showCascade && (React.createElement(CascadeContainer, { columns: cascadeColumns, onPopTo: onPopTo, onFocus: onListCollapse })),
|
|
94
|
+
showComparison && (React.createElement("div", { className: "flex flex-col flex-1 overflow-hidden" },
|
|
95
|
+
React.createElement("div", { className: "flex items-center gap-2 px-3 py-1.5 bg-status-info-bg border-b border-status-info-border shrink-0" },
|
|
96
|
+
React.createElement("span", { className: "text-xs text-status-info-text" },
|
|
97
|
+
"Comparing ",
|
|
98
|
+
comparisonColumns.length,
|
|
99
|
+
" items"),
|
|
100
|
+
onExitComparison && (React.createElement("button", { onClick: onExitComparison, className: "px-2 py-0.5 text-xs rounded border border-status-info-border text-status-info-text hover:bg-status-info-surface transition-colors" }, "\u2190 Back to list"))),
|
|
101
|
+
React.createElement(ComparisonView, { columns: comparisonColumns }))),
|
|
102
|
+
isVariationCompare && React.createElement(ComparisonView, { columns: variationCompareColumns }))));
|
|
103
|
+
}
|
|
104
|
+
//# sourceMappingURL=EntityTabLayout.js.map
|
|
@@ -0,0 +1,63 @@
|
|
|
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
|
+
* Mobile view-stack cascade — one full-screen column at a time with back navigation.
|
|
24
|
+
* @packageDocumentation
|
|
25
|
+
*/
|
|
26
|
+
import React, { useCallback } from 'react';
|
|
27
|
+
/**
|
|
28
|
+
* Mobile replacement for {@link CascadeContainer}.
|
|
29
|
+
*
|
|
30
|
+
* Shows the rightmost (deepest) cascade column full-screen with a back button
|
|
31
|
+
* that pops one level at a time. At the first column, back returns to the list
|
|
32
|
+
* by calling `onPopTo(0)`.
|
|
33
|
+
*
|
|
34
|
+
* Accepts the same props as {@link CascadeContainer} so `CascadeContainer` can
|
|
35
|
+
* delegate to it transparently on mobile.
|
|
36
|
+
*
|
|
37
|
+
* @public
|
|
38
|
+
*/
|
|
39
|
+
export function MobileCascadeStack(props) {
|
|
40
|
+
const { columns, onPopTo, rootLabel = 'List' } = props;
|
|
41
|
+
const handleBack = useCallback(() => {
|
|
42
|
+
if (columns.length > 1) {
|
|
43
|
+
onPopTo(columns.length - 1);
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
onPopTo(0);
|
|
47
|
+
}
|
|
48
|
+
}, [columns.length, onPopTo]);
|
|
49
|
+
if (columns.length === 0) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
const currentColumn = columns[columns.length - 1];
|
|
53
|
+
const backLabel = columns.length > 1 ? columns[columns.length - 2].label : rootLabel;
|
|
54
|
+
return (React.createElement("div", { className: "flex flex-col flex-1 overflow-hidden" },
|
|
55
|
+
React.createElement("div", { className: "flex items-center gap-2 px-3 py-2 bg-surface-alt border-b border-border shrink-0" },
|
|
56
|
+
React.createElement("button", { onClick: handleBack, className: "flex items-center gap-1 text-sm text-brand-accent hover:text-brand-primary", "aria-label": `Back to ${backLabel}` },
|
|
57
|
+
React.createElement("svg", { className: "w-4 h-4 shrink-0", fill: "none", viewBox: "0 0 24 24", strokeWidth: 2, stroke: "currentColor" },
|
|
58
|
+
React.createElement("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M15.75 19.5L8.25 12l7.5-7.5" })),
|
|
59
|
+
backLabel),
|
|
60
|
+
columns.length > 1 && (React.createElement("span", { className: "ml-auto text-xs text-muted truncate" }, currentColumn.label))),
|
|
61
|
+
React.createElement("div", { className: "flex flex-col flex-1 overflow-y-auto" }, currentColumn.content)));
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=MobileCascadeStack.js.map
|
|
@@ -0,0 +1,37 @@
|
|
|
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 packlet - column cascade container, layout, and semantic operations.
|
|
24
|
+
* @packageDocumentation
|
|
25
|
+
*/
|
|
26
|
+
// UI components
|
|
27
|
+
export { CascadeContainer } from './CascadeContainer';
|
|
28
|
+
export { MobileCascadeStack } from './MobileCascadeStack';
|
|
29
|
+
export { EntityTabLayout } from './EntityTabLayout';
|
|
30
|
+
export { ComparisonView } from './ComparisonView';
|
|
31
|
+
// Cascade model types
|
|
32
|
+
export { CASCADE_NEW_ENTITY_ID } from './model';
|
|
33
|
+
// Cascade transition hooks
|
|
34
|
+
export { useSquashAt, useCascadeDrillDown } from './useCascadeTransitions';
|
|
35
|
+
// Semantic cascade operations
|
|
36
|
+
export { useCascadeOps } from './useCascadeOps';
|
|
37
|
+
//# sourceMappingURL=index.js.map
|