@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,107 @@
|
|
|
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 collection management section for the sidebar.
|
|
24
|
+
*
|
|
25
|
+
* Renders a collapsible list of collections with visibility toggles,
|
|
26
|
+
* status indicators, and action buttons.
|
|
27
|
+
*
|
|
28
|
+
* @packageDocumentation
|
|
29
|
+
*/
|
|
30
|
+
import React, { useCallback, useState } from 'react';
|
|
31
|
+
// ============================================================================
|
|
32
|
+
// CollectionRow (internal)
|
|
33
|
+
// ============================================================================
|
|
34
|
+
function CollectionRow(props) {
|
|
35
|
+
var _a, _b, _c, _d;
|
|
36
|
+
const { collection, onToggleVisibility, onSetDefault, onDelete, onExport, onUnlock, onRename, onMerge } = props;
|
|
37
|
+
const displayName = (_a = collection.name) !== null && _a !== void 0 ? _a : collection.id;
|
|
38
|
+
return (React.createElement("div", { className: `flex items-center gap-1.5 px-3 py-1.5 text-sm transition-colors hover:bg-hover ${collection.isVisible ? 'text-secondary' : 'text-muted'}` },
|
|
39
|
+
React.createElement("button", { onClick: () => onToggleVisibility(collection.id), className: "shrink-0 w-5 h-5 flex items-center justify-center text-xs hover:text-brand-accent transition-colors", title: collection.isVisible ? 'Hide collection' : 'Show collection', "aria-label": `${collection.isVisible ? 'Hide' : 'Show'} ${displayName}` }, collection.isVisible ? '\u{1F441}' : '\u{1F441}\u{FE0F}\u{200D}\u{1F5E8}\u{FE0F}'),
|
|
40
|
+
onSetDefault && collection.isMutable && (React.createElement("button", { onClick: () => onSetDefault(collection.id), className: `shrink-0 w-5 h-5 flex items-center justify-center transition-colors ${collection.isDefault ? 'text-star hover:text-star' : 'text-faint hover:text-star'}`, title: collection.isDefault
|
|
41
|
+
? 'Default collection for new items'
|
|
42
|
+
: 'Set as default collection for new items', "aria-label": collection.isDefault
|
|
43
|
+
? `${(_b = collection.name) !== null && _b !== void 0 ? _b : collection.id} is default`
|
|
44
|
+
: `Set ${(_c = collection.name) !== null && _c !== void 0 ? _c : collection.id} as default`, "aria-pressed": collection.isDefault }, collection.isDefault ? '★' : '☆')),
|
|
45
|
+
collection.hasConflict && (React.createElement("span", { className: "shrink-0 text-xs text-status-warning-strong cursor-default", title: "An encrypted copy of this collection from another storage root has the same ID. Go to Settings \u2192 Storage to resolve the conflict.", "aria-label": `Conflict: encrypted shadow for ${displayName}` }, '⚠')),
|
|
46
|
+
!collection.isMutable && (React.createElement("span", { className: "shrink-0 text-xs text-muted", title: "Built-in collection (read-only)" }, '\uD83D\uDD12')),
|
|
47
|
+
collection.isProtected &&
|
|
48
|
+
(collection.isUnlocked || !onUnlock ? (React.createElement("span", { className: `shrink-0 text-xs ${collection.isUnlocked ? 'text-status-success-icon' : 'text-muted'}`, title: collection.isUnlocked ? 'Protected (unlocked)' : 'Protected (locked)' }, '\uD83D\uDEE1')) : (React.createElement("button", { onClick: () => onUnlock(collection.id), className: "shrink-0 text-xs text-muted hover:text-star transition-colors", title: "Click to unlock", "aria-label": `Unlock ${(_d = collection.name) !== null && _d !== void 0 ? _d : collection.id}` }, '\uD83D\uDEE1'))),
|
|
49
|
+
React.createElement("span", { className: "flex-1 truncate", title: displayName }, displayName),
|
|
50
|
+
React.createElement("span", { className: "shrink-0 text-xs text-muted" }, collection.itemCount),
|
|
51
|
+
collection.isMutable && onExport && (React.createElement("button", { onClick: () => onExport(collection.id), className: "shrink-0 w-5 h-5 flex items-center justify-center text-xs text-faint hover:text-brand-accent transition-colors", title: `Export ${displayName}`, "aria-label": `Export ${displayName}` }, "\u2193")),
|
|
52
|
+
collection.isMutable && onRename && (React.createElement("button", { onClick: () => onRename(collection.id), className: "shrink-0 w-5 h-5 flex items-center justify-center text-xs text-faint hover:text-brand-accent transition-colors", title: `Rename ${displayName}`, "aria-label": `Rename ${displayName}` }, '✎')),
|
|
53
|
+
collection.isMutable && onMerge && (React.createElement("button", { onClick: () => onMerge(collection.id), className: "shrink-0 w-5 h-5 flex items-center justify-center text-xs text-faint hover:text-brand-accent transition-colors", title: `Merge ${displayName} into another collection`, "aria-label": `Merge ${displayName}` }, '⤵')),
|
|
54
|
+
collection.isMutable && onDelete && (React.createElement("button", { onClick: () => onDelete(collection.id), className: "shrink-0 w-5 h-5 flex items-center justify-center text-xs text-faint hover:text-status-error-icon transition-colors", title: `Remove ${displayName}`, "aria-label": `Remove ${displayName}` }, '×'))));
|
|
55
|
+
}
|
|
56
|
+
// ============================================================================
|
|
57
|
+
// CollectionSection
|
|
58
|
+
// ============================================================================
|
|
59
|
+
/**
|
|
60
|
+
* Collapsible sidebar section for managing collections.
|
|
61
|
+
*
|
|
62
|
+
* Renders a header with action buttons and a list of collection rows
|
|
63
|
+
* with visibility toggles, status indicators, and delete actions.
|
|
64
|
+
*
|
|
65
|
+
* @public
|
|
66
|
+
*/
|
|
67
|
+
export function CollectionSection(props) {
|
|
68
|
+
const { collections, onToggleVisibility, onToggleAllVisibility, onAddDirectory, onCreateCollection, onDeleteCollection, onSetDefaultCollection, onExportCollection, onExportAllAsZip, onImportCollection, onOpenCollectionFromFile, onUnlockCollection, onRenameCollection, onMergeCollection, defaultCollapsed = false } = props;
|
|
69
|
+
const [collapsed, setCollapsed] = useState(defaultCollapsed);
|
|
70
|
+
const allVisible = collections.length > 0 && collections.every((c) => c.isVisible);
|
|
71
|
+
const handleToggleAllVisibility = useCallback(() => {
|
|
72
|
+
if (onToggleAllVisibility) {
|
|
73
|
+
onToggleAllVisibility(!allVisible);
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
const ids = allVisible
|
|
77
|
+
? collections.map((c) => c.id)
|
|
78
|
+
: collections.filter((c) => !c.isVisible).map((c) => c.id);
|
|
79
|
+
ids.forEach((id) => onToggleVisibility(id));
|
|
80
|
+
}
|
|
81
|
+
}, [onToggleAllVisibility, onToggleVisibility, allVisible, collections]);
|
|
82
|
+
const handleToggleCollapse = useCallback(() => {
|
|
83
|
+
setCollapsed((prev) => !prev);
|
|
84
|
+
}, []);
|
|
85
|
+
return (React.createElement("div", { className: "flex flex-col border-t border-border mt-1" },
|
|
86
|
+
React.createElement("div", { className: "flex items-center justify-between px-3 py-1.5" },
|
|
87
|
+
React.createElement("button", { onClick: handleToggleCollapse, className: "flex items-center gap-1 text-xs font-medium text-muted uppercase tracking-wider hover:text-secondary transition-colors" },
|
|
88
|
+
React.createElement("span", { className: `text-[10px] transition-transform ${collapsed ? '' : 'rotate-90'}` }, '\u203A'),
|
|
89
|
+
"Collections",
|
|
90
|
+
React.createElement("span", { className: "text-muted normal-case font-normal" },
|
|
91
|
+
"(",
|
|
92
|
+
collections.length,
|
|
93
|
+
")")),
|
|
94
|
+
collections.length > 1 && (React.createElement("button", { onClick: handleToggleAllVisibility, className: "text-xs text-muted hover:text-brand-accent transition-colors px-1", title: allVisible ? 'Hide all collections' : 'Show all collections', "aria-label": allVisible ? 'Hide all collections' : 'Show all collections' }, allVisible ? '\u{1F441}\u{FE0F}\u{200D}\u{1F5E8}\u{FE0F}' : '\u{1F441}')),
|
|
95
|
+
React.createElement("div", { className: "flex items-center gap-1" },
|
|
96
|
+
onAddDirectory && (React.createElement("button", { onClick: onAddDirectory, className: "text-xs text-muted hover:text-brand-accent transition-colors px-1", title: "Add directory", "aria-label": "Add directory" },
|
|
97
|
+
"+",
|
|
98
|
+
'\uD83D\uDCC1')),
|
|
99
|
+
onExportAllAsZip && (React.createElement("button", { onClick: onExportAllAsZip, className: "text-xs text-muted hover:text-brand-accent transition-colors px-1", title: "Export all mutable collections as zip", "aria-label": "Export all as zip" },
|
|
100
|
+
"\u2193",
|
|
101
|
+
'🗂')),
|
|
102
|
+
onOpenCollectionFromFile && (React.createElement("button", { onClick: onOpenCollectionFromFile, className: "text-xs text-muted hover:text-brand-accent transition-colors px-1", title: "Open collection file for in-place editing", "aria-label": "Open collection from file" }, '📂')),
|
|
103
|
+
onImportCollection && (React.createElement("button", { onClick: onImportCollection, className: "text-xs text-muted hover:text-brand-accent transition-colors px-1", title: "Import collection from file (in-memory)", "aria-label": "Import collection from file" }, "\u2191")),
|
|
104
|
+
onCreateCollection && (React.createElement("button", { onClick: onCreateCollection, "data-testid": "sidebar-new-collection-button", className: "text-xs text-muted hover:text-brand-accent transition-colors px-1", title: "New collection", "aria-label": "New collection" }, "+")))),
|
|
105
|
+
!collapsed && (React.createElement("div", { className: "flex flex-col" }, collections.length === 0 ? (React.createElement("div", { className: "px-3 py-2 text-xs text-muted" }, "No collections")) : (collections.map((collection) => (React.createElement(CollectionRow, { key: collection.id, collection: collection, onToggleVisibility: onToggleVisibility, onSetDefault: onSetDefaultCollection, onDelete: onDeleteCollection, onExport: onExportCollection, onUnlock: onUnlockCollection, onRename: onRenameCollection, onMerge: onMergeCollection }))))))));
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=CollectionSection.js.map
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2026 Erik Fortune
|
|
3
|
+
*
|
|
4
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
|
+
* of this software and associated documentation files (the "Software"), to deal
|
|
6
|
+
* in the Software without restriction, including without limitation the rights
|
|
7
|
+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
* copies of the Software, and to permit persons to whom the Software is
|
|
9
|
+
* furnished to do so, subject to the following conditions:
|
|
10
|
+
*
|
|
11
|
+
* The above copyright notice and this permission notice shall be included in all
|
|
12
|
+
* copies or substantial portions of the Software.
|
|
13
|
+
*
|
|
14
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15
|
+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
16
|
+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
17
|
+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
18
|
+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
19
|
+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
20
|
+
* SOFTWARE.
|
|
21
|
+
*/
|
|
22
|
+
import React, { useCallback, useEffect, useRef } from 'react';
|
|
23
|
+
import { useResponsive } from '../responsive';
|
|
24
|
+
// ============================================================================
|
|
25
|
+
// EntityList Component
|
|
26
|
+
// ============================================================================
|
|
27
|
+
/**
|
|
28
|
+
* Generic entity list component for the sidebar.
|
|
29
|
+
*
|
|
30
|
+
* Renders a scrollable list of entities with:
|
|
31
|
+
* - Primary label + optional sublabel
|
|
32
|
+
* - Optional status indicator
|
|
33
|
+
* - Selection highlighting
|
|
34
|
+
* - Empty state with optional CTA
|
|
35
|
+
*
|
|
36
|
+
* @public
|
|
37
|
+
*/
|
|
38
|
+
export function EntityList(props) {
|
|
39
|
+
const { entities, descriptor, selectedId, onSelect, onDrill, compareMode, checkedIds, onCheckedChange, emptyState, header, onDelete, canDelete } = props;
|
|
40
|
+
const listRef = useRef(null);
|
|
41
|
+
const { layoutMode } = useResponsive();
|
|
42
|
+
const isMobile = layoutMode === 'mobile';
|
|
43
|
+
// Scroll the selected item into view when selection changes
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
if (selectedId && listRef.current) {
|
|
46
|
+
const selectedButton = listRef.current.querySelector(`[data-entity-id="${selectedId}"]`);
|
|
47
|
+
if (selectedButton) {
|
|
48
|
+
selectedButton.scrollIntoView({ block: 'nearest' });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}, [selectedId]);
|
|
52
|
+
// Find the index of the currently selected entity
|
|
53
|
+
const selectedIndex = entities.findIndex((e) => descriptor.getId(e) === selectedId);
|
|
54
|
+
const handleKeyDown = useCallback((e) => {
|
|
55
|
+
switch (e.key) {
|
|
56
|
+
case 'ArrowDown': {
|
|
57
|
+
e.preventDefault();
|
|
58
|
+
const nextIndex = selectedIndex < entities.length - 1 ? selectedIndex + 1 : 0;
|
|
59
|
+
onSelect(descriptor.getId(entities[nextIndex]));
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
case 'ArrowUp': {
|
|
63
|
+
e.preventDefault();
|
|
64
|
+
const prevIndex = selectedIndex > 0 ? selectedIndex - 1 : entities.length - 1;
|
|
65
|
+
onSelect(descriptor.getId(entities[prevIndex]));
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
case 'Enter':
|
|
69
|
+
case 'ArrowRight': {
|
|
70
|
+
if (selectedId !== undefined && onDrill) {
|
|
71
|
+
e.preventDefault();
|
|
72
|
+
onDrill();
|
|
73
|
+
}
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
default:
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
}, [entities, descriptor, selectedId, selectedIndex, onSelect, onDrill]);
|
|
80
|
+
if (entities.length === 0 && emptyState) {
|
|
81
|
+
return React.createElement(EmptyState, { config: emptyState });
|
|
82
|
+
}
|
|
83
|
+
return (React.createElement("div", { className: "flex flex-col flex-1 overflow-hidden", onKeyDown: handleKeyDown },
|
|
84
|
+
header !== undefined ? (React.createElement("div", { className: "px-3 py-1.5 text-xs text-muted border-b border-border-subtle" }, header)) : entities.length > 0 ? (React.createElement("div", { className: "flex items-center justify-between px-3 py-1.5 border-b border-border-subtle" },
|
|
85
|
+
React.createElement("span", { className: "text-xs text-muted" },
|
|
86
|
+
entities.length,
|
|
87
|
+
" item",
|
|
88
|
+
entities.length !== 1 ? 's' : '',
|
|
89
|
+
compareMode && props.compareCount !== undefined && props.compareCount > 0 && (React.createElement("span", { className: "ml-1.5 text-brand-accent" },
|
|
90
|
+
"\u00B7 ",
|
|
91
|
+
props.compareCount,
|
|
92
|
+
" selected"))),
|
|
93
|
+
React.createElement("div", { className: "flex items-center gap-1" },
|
|
94
|
+
compareMode &&
|
|
95
|
+
props.onStartComparison &&
|
|
96
|
+
props.compareCount !== undefined &&
|
|
97
|
+
props.compareCount >= 2 && (React.createElement("button", { onClick: (e) => {
|
|
98
|
+
var _a;
|
|
99
|
+
e.stopPropagation();
|
|
100
|
+
(_a = props.onStartComparison) === null || _a === void 0 ? void 0 : _a.call(props);
|
|
101
|
+
}, className: "px-2 py-0.5 text-[11px] rounded border transition-colors bg-brand-primary text-white border-brand-primary hover:bg-brand-primary/90" }, "Compare Now")),
|
|
102
|
+
props.onToggleCompare && (React.createElement("button", { onClick: (e) => {
|
|
103
|
+
var _a;
|
|
104
|
+
e.stopPropagation();
|
|
105
|
+
(_a = props.onToggleCompare) === null || _a === void 0 ? void 0 : _a.call(props);
|
|
106
|
+
}, className: `px-2 py-0.5 text-[11px] rounded border transition-colors ${compareMode
|
|
107
|
+
? 'bg-brand-accent text-white border-brand-accent'
|
|
108
|
+
: 'bg-surface text-muted border-border hover:border-brand-accent hover:text-brand-accent'}` }, compareMode ? 'Cancel' : 'Compare'))))) : null,
|
|
109
|
+
React.createElement("div", { ref: listRef, className: "flex-1 overflow-y-auto" }, entities.map((entity) => {
|
|
110
|
+
var _a, _b;
|
|
111
|
+
const id = descriptor.getId(entity);
|
|
112
|
+
const label = descriptor.getLabel(entity);
|
|
113
|
+
const sublabel = (_a = descriptor.getSublabel) === null || _a === void 0 ? void 0 : _a.call(descriptor, entity);
|
|
114
|
+
const status = (_b = descriptor.getStatus) === null || _b === void 0 ? void 0 : _b.call(descriptor, entity);
|
|
115
|
+
const isSelected = id === selectedId;
|
|
116
|
+
const isChecked = compareMode === true && checkedIds !== undefined && checkedIds.has(id);
|
|
117
|
+
return (React.createElement("div", { key: id, className: `group flex items-center gap-2 w-full border-b border-border-subtle transition-colors ${isChecked
|
|
118
|
+
? 'bg-selected border-l-2 border-l-selected-border'
|
|
119
|
+
: isSelected && !compareMode
|
|
120
|
+
? 'bg-selected border-l-2 border-l-selected-border'
|
|
121
|
+
: 'hover:bg-hover border-l-2 border-l-transparent'}` },
|
|
122
|
+
React.createElement("button", { "data-entity-id": id, onClick: () => {
|
|
123
|
+
if (compareMode && onCheckedChange) {
|
|
124
|
+
onCheckedChange(id);
|
|
125
|
+
}
|
|
126
|
+
else if (isSelected && onDrill) {
|
|
127
|
+
onDrill();
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
onSelect(id);
|
|
131
|
+
}
|
|
132
|
+
}, className: `flex items-center gap-2 flex-1 min-w-0 px-3 text-left ${isMobile ? 'py-3' : 'py-2'}` },
|
|
133
|
+
compareMode && (React.createElement("span", { className: `flex items-center justify-center w-4 h-4 rounded border shrink-0 transition-colors ${isChecked
|
|
134
|
+
? 'bg-brand-accent border-brand-accent text-white'
|
|
135
|
+
: 'border-border bg-surface'}` }, isChecked && (React.createElement("svg", { className: "w-3 h-3", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 3 },
|
|
136
|
+
React.createElement("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M5 13l4 4L19 7" }))))),
|
|
137
|
+
React.createElement("div", { className: "flex-1 min-w-0" },
|
|
138
|
+
React.createElement("div", { className: `text-sm truncate ${(isSelected && !compareMode) || isChecked
|
|
139
|
+
? 'font-medium text-brand-primary'
|
|
140
|
+
: 'text-primary'}` }, label),
|
|
141
|
+
sublabel && React.createElement("div", { className: "text-xs text-muted truncate mt-0.5" }, sublabel)),
|
|
142
|
+
status && (React.createElement("span", { className: "flex items-center gap-1 shrink-0 mt-0.5" },
|
|
143
|
+
React.createElement("span", { className: `w-2 h-2 rounded-full ${status.colorClass}` }),
|
|
144
|
+
React.createElement("span", { className: "text-xs text-muted" }, status.label)))),
|
|
145
|
+
onDelete && !compareMode && (!canDelete || canDelete(id)) && (React.createElement("button", { onClick: (e) => {
|
|
146
|
+
e.stopPropagation();
|
|
147
|
+
onDelete(id);
|
|
148
|
+
}, className: `shrink-0 mr-1 flex items-center justify-center text-muted hover:text-status-error-text transition-opacity rounded ${isMobile
|
|
149
|
+
? 'w-11 h-11 opacity-100'
|
|
150
|
+
: 'w-6 h-6 opacity-0 group-hover:opacity-100 focus:opacity-100'}`, title: `Delete ${label}`, "aria-label": `Delete ${label}` },
|
|
151
|
+
React.createElement("svg", { className: "w-3.5 h-3.5", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 2 },
|
|
152
|
+
React.createElement("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" }))))));
|
|
153
|
+
}))));
|
|
154
|
+
}
|
|
155
|
+
// ============================================================================
|
|
156
|
+
// Empty State
|
|
157
|
+
// ============================================================================
|
|
158
|
+
function EmptyState({ config }) {
|
|
159
|
+
return (React.createElement("div", { className: "flex flex-col items-center justify-center flex-1 p-6 text-center" },
|
|
160
|
+
React.createElement("h3", { className: "text-sm font-medium text-secondary mb-1" }, config.title),
|
|
161
|
+
React.createElement("p", { className: "text-xs text-muted mb-4" }, config.description),
|
|
162
|
+
config.action && (React.createElement("button", { onClick: config.action.onClick, className: "px-3 py-1.5 text-xs font-medium text-white bg-brand-accent rounded-md hover:bg-brand-primary transition-colors" }, config.action.label))));
|
|
163
|
+
}
|
|
164
|
+
//# sourceMappingURL=EntityList.js.map
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2026 Erik Fortune
|
|
3
|
+
*
|
|
4
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
|
+
* of this software and associated documentation files (the "Software"), to deal
|
|
6
|
+
* in the Software without restriction, including without limitation the rights
|
|
7
|
+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
* copies of the Software, and to permit persons to whom the Software is
|
|
9
|
+
* furnished to do so, subject to the following conditions:
|
|
10
|
+
*
|
|
11
|
+
* The above copyright notice and this permission notice shall be included in all
|
|
12
|
+
* copies or substantial portions of the Software.
|
|
13
|
+
*
|
|
14
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15
|
+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
16
|
+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
17
|
+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
18
|
+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
19
|
+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
20
|
+
* SOFTWARE.
|
|
21
|
+
*/
|
|
22
|
+
import React from 'react';
|
|
23
|
+
import { SearchBar } from './SearchBar';
|
|
24
|
+
/**
|
|
25
|
+
* Composite sidebar filter bar: search input + filter rows + clear-all.
|
|
26
|
+
*
|
|
27
|
+
* Renders the search bar at the top, filter rows in the middle,
|
|
28
|
+
* and a "Clear all" button when any filters are active.
|
|
29
|
+
*
|
|
30
|
+
* @public
|
|
31
|
+
*/
|
|
32
|
+
export function FilterBar(props) {
|
|
33
|
+
const { search, activeFilterCount, onClearAll, children } = props;
|
|
34
|
+
const hasActiveFilters = activeFilterCount > 0 || search.value.length > 0;
|
|
35
|
+
return (React.createElement("div", { className: "flex flex-col" },
|
|
36
|
+
React.createElement(SearchBar, Object.assign({}, search)),
|
|
37
|
+
React.createElement("div", { className: "flex items-center justify-between px-3 py-1" },
|
|
38
|
+
React.createElement("span", { className: "text-xs font-medium text-muted uppercase tracking-wider" }, "Filters"),
|
|
39
|
+
hasActiveFilters && (React.createElement("button", { onClick: onClearAll, className: "text-xs text-brand-accent hover:text-brand-primary" }, "Clear all"))),
|
|
40
|
+
React.createElement("div", { className: "flex flex-col" }, children)));
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=FilterBar.js.map
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2026 Erik Fortune
|
|
3
|
+
*
|
|
4
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
|
+
* of this software and associated documentation files (the "Software"), to deal
|
|
6
|
+
* in the Software without restriction, including without limitation the rights
|
|
7
|
+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
* copies of the Software, and to permit persons to whom the Software is
|
|
9
|
+
* furnished to do so, subject to the following conditions:
|
|
10
|
+
*
|
|
11
|
+
* The above copyright notice and this permission notice shall be included in all
|
|
12
|
+
* copies or substantial portions of the Software.
|
|
13
|
+
*
|
|
14
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15
|
+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
16
|
+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
17
|
+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
18
|
+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
19
|
+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
20
|
+
* SOFTWARE.
|
|
21
|
+
*/
|
|
22
|
+
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
23
|
+
import { useResponsive } from '../responsive';
|
|
24
|
+
// ============================================================================
|
|
25
|
+
// FilterRow Component
|
|
26
|
+
// ============================================================================
|
|
27
|
+
/** Minimum number of options before showing the search-within-filter input */
|
|
28
|
+
const SEARCH_THRESHOLD = 8;
|
|
29
|
+
/**
|
|
30
|
+
* A compact filter row with a right-side flyout overlay for selecting filter values.
|
|
31
|
+
*
|
|
32
|
+
* Collapsed: shows the label, status summary text, and a `\u203A` chevron.
|
|
33
|
+
* Expanded: a flyout panel slides out to the right of the sidebar, overlaying the main pane.
|
|
34
|
+
*
|
|
35
|
+
* @public
|
|
36
|
+
*/
|
|
37
|
+
export function FilterRow(props) {
|
|
38
|
+
const { label, options, selected, onSelectionChange, multiple = true, isEqual = defaultIsEqual } = props;
|
|
39
|
+
const { layoutMode } = useResponsive();
|
|
40
|
+
const isMobile = layoutMode === 'mobile';
|
|
41
|
+
const [open, setOpen] = useState(false);
|
|
42
|
+
const [flyoutSearch, setFlyoutSearch] = useState('');
|
|
43
|
+
const rowRef = useRef(null);
|
|
44
|
+
const flyoutRef = useRef(null);
|
|
45
|
+
// Close flyout on outside click (check both row and flyout)
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
if (!open) {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
const handleClick = (e) => {
|
|
51
|
+
const target = e.target;
|
|
52
|
+
if (rowRef.current &&
|
|
53
|
+
!rowRef.current.contains(target) &&
|
|
54
|
+
flyoutRef.current &&
|
|
55
|
+
!flyoutRef.current.contains(target)) {
|
|
56
|
+
setOpen(false);
|
|
57
|
+
setFlyoutSearch('');
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
document.addEventListener('mousedown', handleClick);
|
|
61
|
+
return () => document.removeEventListener('mousedown', handleClick);
|
|
62
|
+
}, [open]);
|
|
63
|
+
// Close flyout on Escape
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
if (!open) {
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
const handleKey = (e) => {
|
|
69
|
+
if (e.key === 'Escape') {
|
|
70
|
+
setOpen(false);
|
|
71
|
+
setFlyoutSearch('');
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
document.addEventListener('keydown', handleKey);
|
|
75
|
+
return () => document.removeEventListener('keydown', handleKey);
|
|
76
|
+
}, [open]);
|
|
77
|
+
const isSelected = useCallback((value) => selected.some((s) => isEqual(s, value)), [selected, isEqual]);
|
|
78
|
+
const toggleValue = useCallback((value) => {
|
|
79
|
+
if (multiple) {
|
|
80
|
+
if (isSelected(value)) {
|
|
81
|
+
onSelectionChange(selected.filter((s) => !isEqual(s, value)));
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
onSelectionChange([...selected, value]);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
// Single-select: toggle off if already selected, otherwise select
|
|
89
|
+
if (isSelected(value)) {
|
|
90
|
+
onSelectionChange([]);
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
onSelectionChange([value]);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}, [multiple, selected, onSelectionChange, isSelected, isEqual]);
|
|
97
|
+
const clearSelection = useCallback((e) => {
|
|
98
|
+
e.stopPropagation();
|
|
99
|
+
onSelectionChange([]);
|
|
100
|
+
}, [onSelectionChange]);
|
|
101
|
+
const activeCount = selected.length;
|
|
102
|
+
// Status summary text for the collapsed row
|
|
103
|
+
const statusText = useMemo(() => {
|
|
104
|
+
if (activeCount === 0) {
|
|
105
|
+
return 'All';
|
|
106
|
+
}
|
|
107
|
+
if (activeCount === 1) {
|
|
108
|
+
// Show the label of the single selected option
|
|
109
|
+
const sel = selected[0];
|
|
110
|
+
const match = options.find((o) => isEqual(o.value, sel));
|
|
111
|
+
return match ? match.label : '1 selected';
|
|
112
|
+
}
|
|
113
|
+
return `${activeCount} selected`;
|
|
114
|
+
}, [activeCount, selected, options, isEqual]);
|
|
115
|
+
// Filter options by flyout search
|
|
116
|
+
const filteredOptions = useMemo(() => {
|
|
117
|
+
const q = flyoutSearch.trim().toLowerCase();
|
|
118
|
+
if (q.length === 0) {
|
|
119
|
+
return options;
|
|
120
|
+
}
|
|
121
|
+
return options.filter((o) => o.label.toLowerCase().includes(q));
|
|
122
|
+
}, [options, flyoutSearch]);
|
|
123
|
+
// Compute flyout position: anchored to the right edge of the sidebar row, aligned vertically
|
|
124
|
+
const [flyoutStyle, setFlyoutStyle] = useState({});
|
|
125
|
+
useEffect(() => {
|
|
126
|
+
if (!open || !rowRef.current) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const rect = rowRef.current.getBoundingClientRect();
|
|
130
|
+
// Find the sidebar container (the <aside> ancestor)
|
|
131
|
+
let sidebar = rowRef.current;
|
|
132
|
+
while (sidebar && sidebar.tagName !== 'ASIDE') {
|
|
133
|
+
sidebar = sidebar.parentElement;
|
|
134
|
+
}
|
|
135
|
+
const rightEdge = sidebar ? sidebar.getBoundingClientRect().right : rect.right;
|
|
136
|
+
// Flyout appears at the right edge of the sidebar, vertically aligned with the row
|
|
137
|
+
const maxHeight = Math.min(400, window.innerHeight - rect.top - 8);
|
|
138
|
+
setFlyoutStyle({
|
|
139
|
+
position: 'fixed',
|
|
140
|
+
top: rect.top,
|
|
141
|
+
left: rightEdge,
|
|
142
|
+
width: 280,
|
|
143
|
+
maxHeight,
|
|
144
|
+
zIndex: 50
|
|
145
|
+
});
|
|
146
|
+
}, [open]);
|
|
147
|
+
const handleToggle = useCallback(() => {
|
|
148
|
+
setOpen((prev) => {
|
|
149
|
+
if (prev) {
|
|
150
|
+
setFlyoutSearch('');
|
|
151
|
+
}
|
|
152
|
+
return !prev;
|
|
153
|
+
});
|
|
154
|
+
}, []);
|
|
155
|
+
return (React.createElement("div", { ref: rowRef },
|
|
156
|
+
React.createElement("button", { onClick: handleToggle, className: `flex items-center justify-between w-full px-3 text-sm hover:bg-hover transition-colors ${isMobile ? 'py-3' : 'py-1.5'} ${activeCount > 0 ? 'text-brand-primary font-medium' : 'text-secondary'}` },
|
|
157
|
+
React.createElement("span", { className: "truncate" }, label),
|
|
158
|
+
React.createElement("span", { className: "flex items-center gap-1.5 shrink-0 ml-2" },
|
|
159
|
+
activeCount > 0 && (React.createElement("span", { onClick: clearSelection, className: "text-muted hover:text-secondary cursor-pointer text-xs", role: "button", "aria-label": `Clear ${label} filter` }, '\u00D7')),
|
|
160
|
+
React.createElement("span", { className: `text-xs ${activeCount > 0 ? 'text-brand-accent' : 'text-muted'}` }, statusText),
|
|
161
|
+
React.createElement("span", { className: "text-muted text-xs" }, '\u203A'))),
|
|
162
|
+
open && (React.createElement("div", { ref: flyoutRef, className: "bg-surface border border-border rounded-r-lg shadow-xl overflow-hidden flex flex-col", style: flyoutStyle },
|
|
163
|
+
React.createElement("div", { className: "flex items-center justify-between px-3 py-2 border-b border-border-subtle bg-surface-alt" },
|
|
164
|
+
React.createElement("span", { className: "text-xs font-medium text-secondary" }, label),
|
|
165
|
+
activeCount > 0 && (React.createElement("button", { onClick: () => onSelectionChange([]), className: "text-xs text-brand-accent hover:text-brand-primary" }, "Clear"))),
|
|
166
|
+
options.length >= SEARCH_THRESHOLD && (React.createElement("div", { className: "px-3 py-1.5 border-b border-border-subtle" },
|
|
167
|
+
React.createElement("input", { type: "text", value: flyoutSearch, onChange: (e) => setFlyoutSearch(e.target.value), placeholder: `Search ${label.toLowerCase()}...`, className: "w-full px-2 py-1 text-xs border border-border rounded bg-surface text-primary focus:outline-none focus:border-focus-ring", autoFocus: true }))),
|
|
168
|
+
React.createElement("div", { className: "flex-1 overflow-y-auto" }, filteredOptions.length === 0 ? (React.createElement("div", { className: "px-3 py-2 text-xs text-muted" }, options.length === 0 ? 'No options available' : 'No matches')) : (filteredOptions.map((option, idx) => {
|
|
169
|
+
const checked = isSelected(option.value);
|
|
170
|
+
return (React.createElement("button", { key: idx, onClick: () => toggleValue(option.value), className: `flex items-center gap-2 w-full px-3 text-sm text-left hover:bg-hover transition-colors ${isMobile ? 'py-2.5' : 'py-1.5'} ${checked ? 'text-brand-primary font-medium' : 'text-secondary'}` },
|
|
171
|
+
React.createElement("span", { className: `inline-flex items-center justify-center w-4 h-4 border rounded ${multiple ? 'rounded' : 'rounded-full'} ${checked ? 'bg-brand-accent border-brand-accent text-white' : 'border-border'}` }, checked && React.createElement("span", { className: "text-[10px]" }, '\u2713')),
|
|
172
|
+
React.createElement("span", { className: "flex-1 truncate" }, option.label),
|
|
173
|
+
option.count !== undefined && React.createElement("span", { className: "text-xs text-muted" }, option.count)));
|
|
174
|
+
})))))));
|
|
175
|
+
}
|
|
176
|
+
// ============================================================================
|
|
177
|
+
// Helpers
|
|
178
|
+
// ============================================================================
|
|
179
|
+
function defaultIsEqual(a, b) {
|
|
180
|
+
return a === b;
|
|
181
|
+
}
|
|
182
|
+
//# sourceMappingURL=FilterRow.js.map
|