@fgv/ts-app-shell 5.1.0-1 → 5.1.0-11
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/dist/packlets/editing/EditFieldHelpers.js +4 -4
- package/dist/packlets/sidebar/CollectionSection.js +173 -43
- package/dist/packlets/top-bar/TabBar.js +18 -1
- package/lib/packlets/editing/EditFieldHelpers.d.ts +6 -2
- package/lib/packlets/editing/EditFieldHelpers.js +4 -4
- package/lib/packlets/sidebar/CollectionSection.d.ts +29 -2
- package/lib/packlets/sidebar/CollectionSection.js +172 -42
- package/lib/packlets/sidebar/index.d.ts +1 -1
- package/lib/packlets/top-bar/TabBar.d.ts +12 -0
- package/lib/packlets/top-bar/TabBar.js +18 -1
- package/lib/packlets/top-bar/index.d.ts +1 -1
- package/package.json +16 -16
|
@@ -33,18 +33,18 @@ import React from 'react';
|
|
|
33
33
|
* Horizontal label + field layout for a single edit field.
|
|
34
34
|
* @public
|
|
35
35
|
*/
|
|
36
|
-
export function EditField({ label, children }) {
|
|
36
|
+
export function EditField({ label, tooltip, children }) {
|
|
37
37
|
return (React.createElement("div", { className: "flex items-baseline gap-2 py-1" },
|
|
38
|
-
React.createElement("label", { className: "text-xs text-muted w-32 shrink-0" }, label),
|
|
38
|
+
React.createElement("label", { className: "text-xs text-muted w-32 shrink-0", title: tooltip }, label),
|
|
39
39
|
React.createElement("div", { className: "flex-1" }, children)));
|
|
40
40
|
}
|
|
41
41
|
/**
|
|
42
42
|
* Titled section wrapper for grouping related edit fields.
|
|
43
43
|
* @public
|
|
44
44
|
*/
|
|
45
|
-
export function EditSection({ title, children }) {
|
|
45
|
+
export function EditSection({ title, tooltip, children }) {
|
|
46
46
|
return (React.createElement("div", { className: "mb-4" },
|
|
47
|
-
React.createElement("h4", { className: "text-xs font-medium text-muted uppercase tracking-wider mb-1.5" }, title),
|
|
47
|
+
React.createElement("h4", { className: "text-xs font-medium text-muted uppercase tracking-wider mb-1.5", title: tooltip }, title),
|
|
48
48
|
children));
|
|
49
49
|
}
|
|
50
50
|
/**
|
|
@@ -27,31 +27,139 @@
|
|
|
27
27
|
*
|
|
28
28
|
* @packageDocumentation
|
|
29
29
|
*/
|
|
30
|
-
import React, { useCallback, useState } from 'react';
|
|
30
|
+
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
|
31
|
+
import { StarIcon as StarIconOutline } from '@heroicons/react/24/outline';
|
|
32
|
+
import { StarIcon as StarIconSolid, ExclamationTriangleIcon, BuildingLibraryIcon, ShieldCheckIcon, ShieldExclamationIcon, ArrowDownTrayIcon, PencilSquareIcon, ArrowsPointingInIcon, TrashIcon, FolderPlusIcon, ArchiveBoxArrowDownIcon, FolderOpenIcon, ArrowUpTrayIcon, PlusIcon, ChevronRightIcon } from '@heroicons/react/20/solid';
|
|
33
|
+
const BADGE_BASE_CLASSES = 'inline-flex shrink-0 items-center justify-center rounded-full font-medium leading-none';
|
|
34
|
+
const BADGE_TONE_CLASSES = {
|
|
35
|
+
info: 'bg-status-info-bg text-status-info-text',
|
|
36
|
+
warning: 'bg-status-warning-bg text-status-warning-text',
|
|
37
|
+
danger: 'bg-status-error-bg text-status-error-text'
|
|
38
|
+
};
|
|
39
|
+
function renderBadge(badge) {
|
|
40
|
+
var _a, _b;
|
|
41
|
+
const tone = (_a = badge.tone) !== null && _a !== void 0 ? _a : 'info';
|
|
42
|
+
const ariaLabel = badge.ariaLabel;
|
|
43
|
+
if (badge.kind === 'dot') {
|
|
44
|
+
return (React.createElement("span", { className: `ml-1.5 h-2 w-2 ${BADGE_BASE_CLASSES} ${BADGE_TONE_CLASSES[tone]}`, "aria-label": ariaLabel }));
|
|
45
|
+
}
|
|
46
|
+
return (React.createElement("span", { className: `ml-1.5 min-w-4 px-1.5 py-0.5 text-[0.6875rem] ${BADGE_BASE_CLASSES} ${BADGE_TONE_CLASSES[tone]}`, "aria-label": ariaLabel }, (_b = badge.count) !== null && _b !== void 0 ? _b : 0));
|
|
47
|
+
}
|
|
48
|
+
// ============================================================================
|
|
49
|
+
// Context Menu (internal)
|
|
50
|
+
// ============================================================================
|
|
51
|
+
const LONG_PRESS_MS = 500;
|
|
52
|
+
function CollectionContextMenu(props) {
|
|
53
|
+
const { menu, isHidden, onHide, onShow, onClose } = props;
|
|
54
|
+
const ref = useRef(null);
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
function handleClickOutside(e) {
|
|
57
|
+
if (ref.current && !ref.current.contains(e.target)) {
|
|
58
|
+
onClose();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
62
|
+
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
63
|
+
}, [onClose]);
|
|
64
|
+
const action = isHidden ? onShow : onHide;
|
|
65
|
+
if (!action) {
|
|
66
|
+
return React.createElement(React.Fragment, null);
|
|
67
|
+
}
|
|
68
|
+
return (React.createElement("div", { ref: ref, className: "fixed z-50 bg-surface-raised border border-border rounded shadow-lg py-1 min-w-[140px]", style: { left: menu.x, top: menu.y } },
|
|
69
|
+
React.createElement("button", { className: "w-full text-left px-3 py-1.5 text-sm text-secondary hover:bg-hover transition-colors", onClick: () => {
|
|
70
|
+
action(menu.collectionId);
|
|
71
|
+
onClose();
|
|
72
|
+
} }, isHidden ? 'Show collection' : 'Hide collection')));
|
|
73
|
+
}
|
|
74
|
+
// ============================================================================
|
|
75
|
+
// Long-press hook (internal)
|
|
76
|
+
// ============================================================================
|
|
77
|
+
function useLongPress(onLongPress) {
|
|
78
|
+
const timerRef = useRef(undefined);
|
|
79
|
+
const firedRef = useRef(false);
|
|
80
|
+
const onTouchStart = useCallback((e) => {
|
|
81
|
+
firedRef.current = false;
|
|
82
|
+
timerRef.current = setTimeout(() => {
|
|
83
|
+
firedRef.current = true;
|
|
84
|
+
onLongPress(e);
|
|
85
|
+
}, LONG_PRESS_MS);
|
|
86
|
+
}, [onLongPress]);
|
|
87
|
+
const cancel = useCallback(() => {
|
|
88
|
+
if (timerRef.current !== undefined) {
|
|
89
|
+
clearTimeout(timerRef.current);
|
|
90
|
+
timerRef.current = undefined;
|
|
91
|
+
}
|
|
92
|
+
}, []);
|
|
93
|
+
return { onTouchStart, onTouchEnd: cancel, onTouchMove: cancel };
|
|
94
|
+
}
|
|
31
95
|
// ============================================================================
|
|
32
96
|
// CollectionRow (internal)
|
|
33
97
|
// ============================================================================
|
|
34
98
|
function CollectionRow(props) {
|
|
35
|
-
var _a
|
|
36
|
-
const { collection, onToggleVisibility, onSetDefault, onDelete, onExport, onUnlock, onRename, onMerge } = props;
|
|
99
|
+
var _a;
|
|
100
|
+
const { collection, onToggleVisibility, onSetDefault, onDelete, onExport, onUnlock, onRename, onMerge, borderColorClass, onContextMenu, isHiddenRow } = props;
|
|
37
101
|
const displayName = (_a = collection.name) !== null && _a !== void 0 ? _a : collection.id;
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
(collection.
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
102
|
+
const handleContextMenu = useCallback((e) => {
|
|
103
|
+
if (onContextMenu) {
|
|
104
|
+
e.preventDefault();
|
|
105
|
+
onContextMenu(collection.id, e.clientX, e.clientY);
|
|
106
|
+
}
|
|
107
|
+
}, [onContextMenu, collection.id]);
|
|
108
|
+
const handleLongPress = useCallback((e) => {
|
|
109
|
+
if (onContextMenu && e.touches.length > 0) {
|
|
110
|
+
e.preventDefault();
|
|
111
|
+
const touch = e.touches[0];
|
|
112
|
+
onContextMenu(collection.id, touch.clientX, touch.clientY);
|
|
113
|
+
}
|
|
114
|
+
}, [onContextMenu, collection.id]);
|
|
115
|
+
const longPress = useLongPress(handleLongPress);
|
|
116
|
+
return (React.createElement("div", Object.assign({ onClick: () => onToggleVisibility(collection.id), onContextMenu: handleContextMenu }, longPress, { className: `flex flex-col px-3 py-2.5 text-sm transition-colors hover:bg-hover cursor-pointer ${isHiddenRow
|
|
117
|
+
? 'text-muted opacity-30 line-through'
|
|
118
|
+
: collection.isVisible
|
|
119
|
+
? 'text-secondary'
|
|
120
|
+
: 'text-muted opacity-50'} ${borderColorClass ? `border-l-4 ${borderColorClass}` : ''}`, role: "button", "aria-pressed": collection.isVisible, title: collection.sourceName ? `Source: ${collection.sourceName}` : displayName }),
|
|
121
|
+
React.createElement("div", { className: "flex items-center gap-1.5" },
|
|
122
|
+
onSetDefault && collection.isMutable && (React.createElement("button", { onClick: (e) => {
|
|
123
|
+
e.stopPropagation();
|
|
124
|
+
onSetDefault(collection.id);
|
|
125
|
+
}, 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
|
|
126
|
+
? 'Default collection for new items'
|
|
127
|
+
: 'Set as default collection for new items', "aria-label": collection.isDefault ? `${displayName} is default` : `Set ${displayName} as default`, "aria-pressed": collection.isDefault }, collection.isDefault ? (React.createElement(StarIconSolid, { className: "w-4 h-4" })) : (React.createElement(StarIconOutline, { className: "w-4 h-4" })))),
|
|
128
|
+
collection.hasConflict && (React.createElement("span", { className: "shrink-0 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}` },
|
|
129
|
+
React.createElement(ExclamationTriangleIcon, { className: "w-4 h-4" }))),
|
|
130
|
+
!collection.isMutable && (React.createElement("span", { className: "shrink-0 text-muted", title: "Built-in collection (read-only)" },
|
|
131
|
+
React.createElement(BuildingLibraryIcon, { className: "w-4 h-4" }))),
|
|
132
|
+
collection.isProtected &&
|
|
133
|
+
(collection.isUnlocked || !onUnlock ? (React.createElement("span", { className: `shrink-0 ${collection.isUnlocked ? 'text-status-success-icon' : 'text-muted'}`, title: collection.isUnlocked ? 'Protected (unlocked)' : 'Protected (locked)' }, collection.isUnlocked ? (React.createElement(ShieldCheckIcon, { className: "w-4 h-4" })) : (React.createElement(ShieldExclamationIcon, { className: "w-4 h-4" })))) : (React.createElement("button", { onClick: (e) => {
|
|
134
|
+
e.stopPropagation();
|
|
135
|
+
onUnlock(collection.id);
|
|
136
|
+
}, className: "shrink-0 text-muted hover:text-star transition-colors", title: "Click to unlock", "aria-label": `Unlock ${displayName}` },
|
|
137
|
+
React.createElement(ShieldExclamationIcon, { className: "w-4 h-4" })))),
|
|
138
|
+
React.createElement("span", { className: "flex-1 truncate", title: displayName }, displayName),
|
|
139
|
+
collection.badge && renderBadge(collection.badge),
|
|
140
|
+
React.createElement("span", { className: "shrink-0 text-xs text-muted" }, collection.itemCount)),
|
|
141
|
+
React.createElement("div", { className: "flex items-center gap-1 mt-1 ml-[24px]" },
|
|
142
|
+
collection.isMutable && onExport && (React.createElement("button", { onClick: (e) => {
|
|
143
|
+
e.stopPropagation();
|
|
144
|
+
onExport(collection.id);
|
|
145
|
+
}, className: "shrink-0 w-5 h-5 flex items-center justify-center text-faint hover:text-brand-accent transition-colors", title: `Export ${displayName}`, "aria-label": `Export ${displayName}` },
|
|
146
|
+
React.createElement(ArrowDownTrayIcon, { className: "w-4 h-4" }))),
|
|
147
|
+
collection.isMutable && onRename && (React.createElement("button", { onClick: (e) => {
|
|
148
|
+
e.stopPropagation();
|
|
149
|
+
onRename(collection.id);
|
|
150
|
+
}, className: "shrink-0 w-5 h-5 flex items-center justify-center text-faint hover:text-brand-accent transition-colors", title: `Rename ${displayName}`, "aria-label": `Rename ${displayName}` },
|
|
151
|
+
React.createElement(PencilSquareIcon, { className: "w-4 h-4" }))),
|
|
152
|
+
collection.isMutable && onMerge && (React.createElement("button", { onClick: (e) => {
|
|
153
|
+
e.stopPropagation();
|
|
154
|
+
onMerge(collection.id);
|
|
155
|
+
}, className: "shrink-0 w-5 h-5 flex items-center justify-center text-faint hover:text-brand-accent transition-colors", title: `Merge ${displayName} into another collection`, "aria-label": `Merge ${displayName}` },
|
|
156
|
+
React.createElement(ArrowsPointingInIcon, { className: "w-4 h-4" }))),
|
|
157
|
+
collection.isMutable && onDelete && (React.createElement("button", { onClick: (e) => {
|
|
158
|
+
e.stopPropagation();
|
|
159
|
+
onDelete(collection.id);
|
|
160
|
+
}, className: "shrink-0 w-5 h-5 flex items-center justify-center text-faint hover:text-status-error-icon transition-colors", title: `Remove ${displayName}`, "aria-label": `Remove ${displayName}` },
|
|
161
|
+
React.createElement(TrashIcon, { className: "w-4 h-4" }))),
|
|
162
|
+
!collection.isMutable && React.createElement("span", { className: "text-xs text-muted" }, "(built-in)"))));
|
|
55
163
|
}
|
|
56
164
|
// ============================================================================
|
|
57
165
|
// CollectionSection
|
|
@@ -65,43 +173,65 @@ function CollectionRow(props) {
|
|
|
65
173
|
* @public
|
|
66
174
|
*/
|
|
67
175
|
export function CollectionSection(props) {
|
|
68
|
-
|
|
176
|
+
var _a;
|
|
177
|
+
const { collections, onToggleVisibility, onAddDirectory, onCreateCollection, onDeleteCollection, onSetDefaultCollection, onExportCollection, onExportAllAsZip, onImportCollection, onOpenCollectionFromFile, onUnlockCollection, onRenameCollection, onMergeCollection, onHideCollection, onShowCollection, defaultCollapsed = false, sourceColorMap, sourceColorFallback } = props;
|
|
69
178
|
const [collapsed, setCollapsed] = useState(defaultCollapsed);
|
|
70
|
-
const
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
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]);
|
|
179
|
+
const [hiddenExpanded, setHiddenExpanded] = useState(false);
|
|
180
|
+
const [contextMenu, setContextMenu] = useState(undefined);
|
|
181
|
+
const visibleCollections = collections.filter((c) => !c.isHidden);
|
|
182
|
+
const hiddenCollections = collections.filter((c) => c.isHidden);
|
|
82
183
|
const handleToggleCollapse = useCallback(() => {
|
|
83
184
|
setCollapsed((prev) => !prev);
|
|
84
185
|
}, []);
|
|
186
|
+
const handleToggleHiddenExpanded = useCallback(() => {
|
|
187
|
+
setHiddenExpanded((prev) => !prev);
|
|
188
|
+
}, []);
|
|
189
|
+
const handleOpenContextMenu = useCallback((collectionId, x, y) => {
|
|
190
|
+
setContextMenu({ collectionId, x, y });
|
|
191
|
+
}, []);
|
|
192
|
+
const handleCloseContextMenu = useCallback(() => {
|
|
193
|
+
setContextMenu(undefined);
|
|
194
|
+
}, []);
|
|
195
|
+
const getBorderColor = useCallback((sourceName) => {
|
|
196
|
+
if (!sourceColorMap)
|
|
197
|
+
return undefined;
|
|
198
|
+
if (sourceName && sourceName in sourceColorMap) {
|
|
199
|
+
return sourceColorMap[sourceName];
|
|
200
|
+
}
|
|
201
|
+
return sourceColorFallback;
|
|
202
|
+
}, [sourceColorMap, sourceColorFallback]);
|
|
203
|
+
const contextMenuCollection = contextMenu
|
|
204
|
+
? collections.find((c) => c.id === contextMenu.collectionId)
|
|
205
|
+
: undefined;
|
|
85
206
|
return (React.createElement("div", { className: "flex flex-col border-t border-border mt-1" },
|
|
86
207
|
React.createElement("div", { className: "flex items-center justify-between px-3 py-1.5" },
|
|
87
208
|
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(
|
|
209
|
+
React.createElement(ChevronRightIcon, { className: `w-3 h-3 transition-transform ${collapsed ? '' : 'rotate-90'}` }),
|
|
89
210
|
"Collections",
|
|
90
211
|
React.createElement("span", { className: "text-muted normal-case font-normal" },
|
|
91
212
|
"(",
|
|
92
213
|
collections.length,
|
|
93
214
|
")")),
|
|
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
215
|
React.createElement("div", { className: "flex items-center gap-1" },
|
|
96
216
|
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')),
|
|
217
|
+
React.createElement(FolderPlusIcon, { className: "w-4 h-4" }))),
|
|
99
218
|
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
|
-
"
|
|
101
|
-
|
|
102
|
-
|
|
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" },
|
|
104
|
-
|
|
105
|
-
|
|
219
|
+
React.createElement(ArchiveBoxArrowDownIcon, { className: "w-4 h-4" }))),
|
|
220
|
+
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" },
|
|
221
|
+
React.createElement(FolderOpenIcon, { className: "w-4 h-4" }))),
|
|
222
|
+
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" },
|
|
223
|
+
React.createElement(ArrowUpTrayIcon, { className: "w-4 h-4" }))),
|
|
224
|
+
onCreateCollection && (React.createElement("button", { onClick: onCreateCollection, "data-testid": "sidebar-new-collection-button", className: "text-muted hover:text-brand-accent transition-colors px-1", title: "New collection", "aria-label": "New collection" },
|
|
225
|
+
React.createElement(PlusIcon, { className: "w-4 h-4" }))))),
|
|
226
|
+
!collapsed && (React.createElement("div", { className: "flex flex-col" },
|
|
227
|
+
visibleCollections.length === 0 && hiddenCollections.length === 0 ? (React.createElement("div", { className: "px-3 py-2 text-xs text-muted" }, "No collections")) : (visibleCollections.map((collection) => (React.createElement(CollectionRow, { key: collection.id, collection: collection, onToggleVisibility: onToggleVisibility, onSetDefault: onSetDefaultCollection, onDelete: onDeleteCollection, onExport: onExportCollection, onUnlock: onUnlockCollection, onRename: onRenameCollection, onMerge: onMergeCollection, borderColorClass: getBorderColor(collection.sourceName), onContextMenu: onHideCollection ? handleOpenContextMenu : undefined })))),
|
|
228
|
+
hiddenCollections.length > 0 && (React.createElement(React.Fragment, null,
|
|
229
|
+
React.createElement("button", { onClick: handleToggleHiddenExpanded, className: "flex items-center gap-1 px-3 py-1 text-xs text-muted hover:text-secondary transition-colors" },
|
|
230
|
+
React.createElement(ChevronRightIcon, { className: `w-3 h-3 transition-transform ${hiddenExpanded ? 'rotate-90' : ''}` }),
|
|
231
|
+
hiddenCollections.length,
|
|
232
|
+
" hidden"),
|
|
233
|
+
hiddenExpanded &&
|
|
234
|
+
hiddenCollections.map((collection) => (React.createElement(CollectionRow, { key: collection.id, collection: collection, onToggleVisibility: onToggleVisibility, onSetDefault: onSetDefaultCollection, onDelete: onDeleteCollection, onExport: onExportCollection, onUnlock: onUnlockCollection, onRename: onRenameCollection, onMerge: onMergeCollection, borderColorClass: getBorderColor(collection.sourceName), onContextMenu: onShowCollection ? handleOpenContextMenu : undefined, isHiddenRow: true }))))))),
|
|
235
|
+
contextMenu && contextMenuCollection && (React.createElement(CollectionContextMenu, { menu: contextMenu, isHidden: (_a = contextMenuCollection.isHidden) !== null && _a !== void 0 ? _a : false, onHide: onHideCollection, onShow: onShowCollection, onClose: handleCloseContextMenu }))));
|
|
106
236
|
}
|
|
107
237
|
//# sourceMappingURL=CollectionSection.js.map
|
|
@@ -20,6 +20,21 @@
|
|
|
20
20
|
* SOFTWARE.
|
|
21
21
|
*/
|
|
22
22
|
import React from 'react';
|
|
23
|
+
const BADGE_BASE_CLASSES = 'inline-flex shrink-0 items-center justify-center rounded-full font-medium leading-none';
|
|
24
|
+
const BADGE_TONE_CLASSES = {
|
|
25
|
+
info: 'bg-status-info-bg text-status-info-text',
|
|
26
|
+
warning: 'bg-status-warning-bg text-status-warning-text',
|
|
27
|
+
danger: 'bg-status-error-bg text-status-error-text'
|
|
28
|
+
};
|
|
29
|
+
function renderBadge(badge) {
|
|
30
|
+
var _a, _b;
|
|
31
|
+
const tone = (_a = badge.tone) !== null && _a !== void 0 ? _a : 'info';
|
|
32
|
+
const ariaLabel = badge.ariaLabel;
|
|
33
|
+
if (badge.kind === 'dot') {
|
|
34
|
+
return (React.createElement("span", { className: `ml-1.5 h-2 w-2 ${BADGE_BASE_CLASSES} ${BADGE_TONE_CLASSES[tone]}`, "aria-label": ariaLabel }));
|
|
35
|
+
}
|
|
36
|
+
return (React.createElement("span", { className: `ml-1.5 min-w-4 px-1.5 py-0.5 text-[0.6875rem] ${BADGE_BASE_CLASSES} ${BADGE_TONE_CLASSES[tone]}`, "aria-label": ariaLabel }, (_b = badge.count) !== null && _b !== void 0 ? _b : 0));
|
|
37
|
+
}
|
|
23
38
|
/**
|
|
24
39
|
* Second-level tab bar for switching views within a mode.
|
|
25
40
|
* @public
|
|
@@ -29,7 +44,9 @@ export function TabBar(props) {
|
|
|
29
44
|
return (React.createElement("div", { className: "flex items-center gap-1 px-4 py-1 bg-brand-secondary text-white border-t border-white/10 overflow-x-auto" },
|
|
30
45
|
tabs.map((tab) => (React.createElement("button", { key: tab.id, onClick: () => onTabChange(tab.id), className: `px-3 py-1.5 rounded-md text-sm font-medium transition-colors shrink-0 ${activeTab === tab.id
|
|
31
46
|
? 'bg-white/20 text-white'
|
|
32
|
-
: 'text-white/60 hover:text-white hover:bg-white/10'}`, "aria-current": activeTab === tab.id ? 'page' : undefined },
|
|
47
|
+
: 'text-white/60 hover:text-white hover:bg-white/10'}`, "aria-current": activeTab === tab.id ? 'page' : undefined },
|
|
48
|
+
React.createElement("span", null, tab.label),
|
|
49
|
+
tab.badge && renderBadge(tab.badge)))),
|
|
33
50
|
rightContent !== undefined && (React.createElement(React.Fragment, null,
|
|
34
51
|
React.createElement("div", { className: "flex-1" }),
|
|
35
52
|
rightContent))));
|
|
@@ -15,6 +15,8 @@ import React from 'react';
|
|
|
15
15
|
export interface IEditFieldProps {
|
|
16
16
|
/** Label text displayed to the left of the field */
|
|
17
17
|
readonly label: string;
|
|
18
|
+
/** Optional tooltip shown on hover over the label */
|
|
19
|
+
readonly tooltip?: string;
|
|
18
20
|
/** The input control(s) to render */
|
|
19
21
|
readonly children: React.ReactNode;
|
|
20
22
|
}
|
|
@@ -22,7 +24,7 @@ export interface IEditFieldProps {
|
|
|
22
24
|
* Horizontal label + field layout for a single edit field.
|
|
23
25
|
* @public
|
|
24
26
|
*/
|
|
25
|
-
export declare function EditField({ label, children }: IEditFieldProps): React.ReactElement;
|
|
27
|
+
export declare function EditField({ label, tooltip, children }: IEditFieldProps): React.ReactElement;
|
|
26
28
|
/**
|
|
27
29
|
* Props for the EditSection component.
|
|
28
30
|
* @public
|
|
@@ -30,6 +32,8 @@ export declare function EditField({ label, children }: IEditFieldProps): React.R
|
|
|
30
32
|
export interface IEditSectionProps {
|
|
31
33
|
/** Section heading text */
|
|
32
34
|
readonly title: string;
|
|
35
|
+
/** Optional tooltip shown on hover over the section heading */
|
|
36
|
+
readonly tooltip?: string;
|
|
33
37
|
/** Section content (typically EditField components) */
|
|
34
38
|
readonly children: React.ReactNode;
|
|
35
39
|
}
|
|
@@ -37,7 +41,7 @@ export interface IEditSectionProps {
|
|
|
37
41
|
* Titled section wrapper for grouping related edit fields.
|
|
38
42
|
* @public
|
|
39
43
|
*/
|
|
40
|
-
export declare function EditSection({ title, children }: IEditSectionProps): React.ReactElement;
|
|
44
|
+
export declare function EditSection({ title, tooltip, children }: IEditSectionProps): React.ReactElement;
|
|
41
45
|
/**
|
|
42
46
|
* Props for the TextInput component.
|
|
43
47
|
* @public
|
|
@@ -47,18 +47,18 @@ const react_1 = __importDefault(require("react"));
|
|
|
47
47
|
* Horizontal label + field layout for a single edit field.
|
|
48
48
|
* @public
|
|
49
49
|
*/
|
|
50
|
-
function EditField({ label, children }) {
|
|
50
|
+
function EditField({ label, tooltip, children }) {
|
|
51
51
|
return (react_1.default.createElement("div", { className: "flex items-baseline gap-2 py-1" },
|
|
52
|
-
react_1.default.createElement("label", { className: "text-xs text-muted w-32 shrink-0" }, label),
|
|
52
|
+
react_1.default.createElement("label", { className: "text-xs text-muted w-32 shrink-0", title: tooltip }, label),
|
|
53
53
|
react_1.default.createElement("div", { className: "flex-1" }, children)));
|
|
54
54
|
}
|
|
55
55
|
/**
|
|
56
56
|
* Titled section wrapper for grouping related edit fields.
|
|
57
57
|
* @public
|
|
58
58
|
*/
|
|
59
|
-
function EditSection({ title, children }) {
|
|
59
|
+
function EditSection({ title, tooltip, children }) {
|
|
60
60
|
return (react_1.default.createElement("div", { className: "mb-4" },
|
|
61
|
-
react_1.default.createElement("h4", { className: "text-xs font-medium text-muted uppercase tracking-wider mb-1.5" }, title),
|
|
61
|
+
react_1.default.createElement("h4", { className: "text-xs font-medium text-muted uppercase tracking-wider mb-1.5", title: tooltip }, title),
|
|
62
62
|
children));
|
|
63
63
|
}
|
|
64
64
|
/**
|
|
@@ -7,6 +7,16 @@
|
|
|
7
7
|
* @packageDocumentation
|
|
8
8
|
*/
|
|
9
9
|
import React from 'react';
|
|
10
|
+
/**
|
|
11
|
+
* Describes a single collection for rendering in the sidebar.
|
|
12
|
+
* @public
|
|
13
|
+
*/
|
|
14
|
+
export interface ICollectionBadge {
|
|
15
|
+
readonly kind: 'dot' | 'count';
|
|
16
|
+
readonly tone?: 'info' | 'warning' | 'danger';
|
|
17
|
+
readonly count?: number;
|
|
18
|
+
readonly ariaLabel?: string;
|
|
19
|
+
}
|
|
10
20
|
/**
|
|
11
21
|
* Describes a single collection for rendering in the sidebar.
|
|
12
22
|
* @public
|
|
@@ -33,18 +43,27 @@ export interface ICollectionRowItem {
|
|
|
33
43
|
* in another storage root. When true, a repair action should be offered.
|
|
34
44
|
*/
|
|
35
45
|
readonly hasConflict?: boolean;
|
|
46
|
+
/** Whether this collection is explicitly hidden by the user */
|
|
47
|
+
readonly isHidden?: boolean;
|
|
48
|
+
/** The name of the storage source this collection was loaded from */
|
|
49
|
+
readonly sourceName?: string;
|
|
50
|
+
/** Optional badge rendered alongside the collection name */
|
|
51
|
+
readonly badge?: ICollectionBadge;
|
|
36
52
|
}
|
|
37
53
|
/**
|
|
38
54
|
* Props for the CollectionSection component.
|
|
39
55
|
* @public
|
|
40
56
|
*/
|
|
57
|
+
/**
|
|
58
|
+
* Maps source names to Tailwind border color classes for the left-border indicator.
|
|
59
|
+
* @public
|
|
60
|
+
*/
|
|
61
|
+
export type SourceColorMap = Readonly<Record<string, string>>;
|
|
41
62
|
export interface ICollectionSectionProps {
|
|
42
63
|
/** Collection items to display */
|
|
43
64
|
readonly collections: ReadonlyArray<ICollectionRowItem>;
|
|
44
65
|
/** Callback when visibility is toggled for a collection */
|
|
45
66
|
readonly onToggleVisibility: (collectionId: string) => void;
|
|
46
|
-
/** Callback when all-visible toggle is clicked; receives true to show all, false to hide all */
|
|
47
|
-
readonly onToggleAllVisibility?: (showAll: boolean) => void;
|
|
48
67
|
/** Callback when "Add Directory" is clicked */
|
|
49
68
|
readonly onAddDirectory?: () => void;
|
|
50
69
|
/** Callback when "New Collection" is clicked */
|
|
@@ -67,8 +86,16 @@ export interface ICollectionSectionProps {
|
|
|
67
86
|
readonly onRenameCollection?: (collectionId: string) => void;
|
|
68
87
|
/** Callback when merge is clicked for a mutable collection */
|
|
69
88
|
readonly onMergeCollection?: (collectionId: string) => void;
|
|
89
|
+
/** Callback when hide is selected from the context menu */
|
|
90
|
+
readonly onHideCollection?: (collectionId: string) => void;
|
|
91
|
+
/** Callback when show (unhide) is selected from the context menu */
|
|
92
|
+
readonly onShowCollection?: (collectionId: string) => void;
|
|
70
93
|
/** Whether the section starts collapsed */
|
|
71
94
|
readonly defaultCollapsed?: boolean;
|
|
95
|
+
/** Maps sourceName values to Tailwind border-l color classes */
|
|
96
|
+
readonly sourceColorMap?: SourceColorMap;
|
|
97
|
+
/** Fallback border-l color class when sourceName is not in the map */
|
|
98
|
+
readonly sourceColorFallback?: string;
|
|
72
99
|
}
|
|
73
100
|
/**
|
|
74
101
|
* Collapsible sidebar section for managing collections.
|
|
@@ -64,30 +64,138 @@ exports.CollectionSection = CollectionSection;
|
|
|
64
64
|
* @packageDocumentation
|
|
65
65
|
*/
|
|
66
66
|
const react_1 = __importStar(require("react"));
|
|
67
|
+
const outline_1 = require("@heroicons/react/24/outline");
|
|
68
|
+
const solid_1 = require("@heroicons/react/20/solid");
|
|
69
|
+
const BADGE_BASE_CLASSES = 'inline-flex shrink-0 items-center justify-center rounded-full font-medium leading-none';
|
|
70
|
+
const BADGE_TONE_CLASSES = {
|
|
71
|
+
info: 'bg-status-info-bg text-status-info-text',
|
|
72
|
+
warning: 'bg-status-warning-bg text-status-warning-text',
|
|
73
|
+
danger: 'bg-status-error-bg text-status-error-text'
|
|
74
|
+
};
|
|
75
|
+
function renderBadge(badge) {
|
|
76
|
+
var _a, _b;
|
|
77
|
+
const tone = (_a = badge.tone) !== null && _a !== void 0 ? _a : 'info';
|
|
78
|
+
const ariaLabel = badge.ariaLabel;
|
|
79
|
+
if (badge.kind === 'dot') {
|
|
80
|
+
return (react_1.default.createElement("span", { className: `ml-1.5 h-2 w-2 ${BADGE_BASE_CLASSES} ${BADGE_TONE_CLASSES[tone]}`, "aria-label": ariaLabel }));
|
|
81
|
+
}
|
|
82
|
+
return (react_1.default.createElement("span", { className: `ml-1.5 min-w-4 px-1.5 py-0.5 text-[0.6875rem] ${BADGE_BASE_CLASSES} ${BADGE_TONE_CLASSES[tone]}`, "aria-label": ariaLabel }, (_b = badge.count) !== null && _b !== void 0 ? _b : 0));
|
|
83
|
+
}
|
|
84
|
+
// ============================================================================
|
|
85
|
+
// Context Menu (internal)
|
|
86
|
+
// ============================================================================
|
|
87
|
+
const LONG_PRESS_MS = 500;
|
|
88
|
+
function CollectionContextMenu(props) {
|
|
89
|
+
const { menu, isHidden, onHide, onShow, onClose } = props;
|
|
90
|
+
const ref = (0, react_1.useRef)(null);
|
|
91
|
+
(0, react_1.useEffect)(() => {
|
|
92
|
+
function handleClickOutside(e) {
|
|
93
|
+
if (ref.current && !ref.current.contains(e.target)) {
|
|
94
|
+
onClose();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
98
|
+
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
99
|
+
}, [onClose]);
|
|
100
|
+
const action = isHidden ? onShow : onHide;
|
|
101
|
+
if (!action) {
|
|
102
|
+
return react_1.default.createElement(react_1.default.Fragment, null);
|
|
103
|
+
}
|
|
104
|
+
return (react_1.default.createElement("div", { ref: ref, className: "fixed z-50 bg-surface-raised border border-border rounded shadow-lg py-1 min-w-[140px]", style: { left: menu.x, top: menu.y } },
|
|
105
|
+
react_1.default.createElement("button", { className: "w-full text-left px-3 py-1.5 text-sm text-secondary hover:bg-hover transition-colors", onClick: () => {
|
|
106
|
+
action(menu.collectionId);
|
|
107
|
+
onClose();
|
|
108
|
+
} }, isHidden ? 'Show collection' : 'Hide collection')));
|
|
109
|
+
}
|
|
110
|
+
// ============================================================================
|
|
111
|
+
// Long-press hook (internal)
|
|
112
|
+
// ============================================================================
|
|
113
|
+
function useLongPress(onLongPress) {
|
|
114
|
+
const timerRef = (0, react_1.useRef)(undefined);
|
|
115
|
+
const firedRef = (0, react_1.useRef)(false);
|
|
116
|
+
const onTouchStart = (0, react_1.useCallback)((e) => {
|
|
117
|
+
firedRef.current = false;
|
|
118
|
+
timerRef.current = setTimeout(() => {
|
|
119
|
+
firedRef.current = true;
|
|
120
|
+
onLongPress(e);
|
|
121
|
+
}, LONG_PRESS_MS);
|
|
122
|
+
}, [onLongPress]);
|
|
123
|
+
const cancel = (0, react_1.useCallback)(() => {
|
|
124
|
+
if (timerRef.current !== undefined) {
|
|
125
|
+
clearTimeout(timerRef.current);
|
|
126
|
+
timerRef.current = undefined;
|
|
127
|
+
}
|
|
128
|
+
}, []);
|
|
129
|
+
return { onTouchStart, onTouchEnd: cancel, onTouchMove: cancel };
|
|
130
|
+
}
|
|
67
131
|
// ============================================================================
|
|
68
132
|
// CollectionRow (internal)
|
|
69
133
|
// ============================================================================
|
|
70
134
|
function CollectionRow(props) {
|
|
71
|
-
var _a
|
|
72
|
-
const { collection, onToggleVisibility, onSetDefault, onDelete, onExport, onUnlock, onRename, onMerge } = props;
|
|
135
|
+
var _a;
|
|
136
|
+
const { collection, onToggleVisibility, onSetDefault, onDelete, onExport, onUnlock, onRename, onMerge, borderColorClass, onContextMenu, isHiddenRow } = props;
|
|
73
137
|
const displayName = (_a = collection.name) !== null && _a !== void 0 ? _a : collection.id;
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
(collection.
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
138
|
+
const handleContextMenu = (0, react_1.useCallback)((e) => {
|
|
139
|
+
if (onContextMenu) {
|
|
140
|
+
e.preventDefault();
|
|
141
|
+
onContextMenu(collection.id, e.clientX, e.clientY);
|
|
142
|
+
}
|
|
143
|
+
}, [onContextMenu, collection.id]);
|
|
144
|
+
const handleLongPress = (0, react_1.useCallback)((e) => {
|
|
145
|
+
if (onContextMenu && e.touches.length > 0) {
|
|
146
|
+
e.preventDefault();
|
|
147
|
+
const touch = e.touches[0];
|
|
148
|
+
onContextMenu(collection.id, touch.clientX, touch.clientY);
|
|
149
|
+
}
|
|
150
|
+
}, [onContextMenu, collection.id]);
|
|
151
|
+
const longPress = useLongPress(handleLongPress);
|
|
152
|
+
return (react_1.default.createElement("div", Object.assign({ onClick: () => onToggleVisibility(collection.id), onContextMenu: handleContextMenu }, longPress, { className: `flex flex-col px-3 py-2.5 text-sm transition-colors hover:bg-hover cursor-pointer ${isHiddenRow
|
|
153
|
+
? 'text-muted opacity-30 line-through'
|
|
154
|
+
: collection.isVisible
|
|
155
|
+
? 'text-secondary'
|
|
156
|
+
: 'text-muted opacity-50'} ${borderColorClass ? `border-l-4 ${borderColorClass}` : ''}`, role: "button", "aria-pressed": collection.isVisible, title: collection.sourceName ? `Source: ${collection.sourceName}` : displayName }),
|
|
157
|
+
react_1.default.createElement("div", { className: "flex items-center gap-1.5" },
|
|
158
|
+
onSetDefault && collection.isMutable && (react_1.default.createElement("button", { onClick: (e) => {
|
|
159
|
+
e.stopPropagation();
|
|
160
|
+
onSetDefault(collection.id);
|
|
161
|
+
}, 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
|
|
162
|
+
? 'Default collection for new items'
|
|
163
|
+
: 'Set as default collection for new items', "aria-label": collection.isDefault ? `${displayName} is default` : `Set ${displayName} as default`, "aria-pressed": collection.isDefault }, collection.isDefault ? (react_1.default.createElement(solid_1.StarIcon, { className: "w-4 h-4" })) : (react_1.default.createElement(outline_1.StarIcon, { className: "w-4 h-4" })))),
|
|
164
|
+
collection.hasConflict && (react_1.default.createElement("span", { className: "shrink-0 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}` },
|
|
165
|
+
react_1.default.createElement(solid_1.ExclamationTriangleIcon, { className: "w-4 h-4" }))),
|
|
166
|
+
!collection.isMutable && (react_1.default.createElement("span", { className: "shrink-0 text-muted", title: "Built-in collection (read-only)" },
|
|
167
|
+
react_1.default.createElement(solid_1.BuildingLibraryIcon, { className: "w-4 h-4" }))),
|
|
168
|
+
collection.isProtected &&
|
|
169
|
+
(collection.isUnlocked || !onUnlock ? (react_1.default.createElement("span", { className: `shrink-0 ${collection.isUnlocked ? 'text-status-success-icon' : 'text-muted'}`, title: collection.isUnlocked ? 'Protected (unlocked)' : 'Protected (locked)' }, collection.isUnlocked ? (react_1.default.createElement(solid_1.ShieldCheckIcon, { className: "w-4 h-4" })) : (react_1.default.createElement(solid_1.ShieldExclamationIcon, { className: "w-4 h-4" })))) : (react_1.default.createElement("button", { onClick: (e) => {
|
|
170
|
+
e.stopPropagation();
|
|
171
|
+
onUnlock(collection.id);
|
|
172
|
+
}, className: "shrink-0 text-muted hover:text-star transition-colors", title: "Click to unlock", "aria-label": `Unlock ${displayName}` },
|
|
173
|
+
react_1.default.createElement(solid_1.ShieldExclamationIcon, { className: "w-4 h-4" })))),
|
|
174
|
+
react_1.default.createElement("span", { className: "flex-1 truncate", title: displayName }, displayName),
|
|
175
|
+
collection.badge && renderBadge(collection.badge),
|
|
176
|
+
react_1.default.createElement("span", { className: "shrink-0 text-xs text-muted" }, collection.itemCount)),
|
|
177
|
+
react_1.default.createElement("div", { className: "flex items-center gap-1 mt-1 ml-[24px]" },
|
|
178
|
+
collection.isMutable && onExport && (react_1.default.createElement("button", { onClick: (e) => {
|
|
179
|
+
e.stopPropagation();
|
|
180
|
+
onExport(collection.id);
|
|
181
|
+
}, className: "shrink-0 w-5 h-5 flex items-center justify-center text-faint hover:text-brand-accent transition-colors", title: `Export ${displayName}`, "aria-label": `Export ${displayName}` },
|
|
182
|
+
react_1.default.createElement(solid_1.ArrowDownTrayIcon, { className: "w-4 h-4" }))),
|
|
183
|
+
collection.isMutable && onRename && (react_1.default.createElement("button", { onClick: (e) => {
|
|
184
|
+
e.stopPropagation();
|
|
185
|
+
onRename(collection.id);
|
|
186
|
+
}, className: "shrink-0 w-5 h-5 flex items-center justify-center text-faint hover:text-brand-accent transition-colors", title: `Rename ${displayName}`, "aria-label": `Rename ${displayName}` },
|
|
187
|
+
react_1.default.createElement(solid_1.PencilSquareIcon, { className: "w-4 h-4" }))),
|
|
188
|
+
collection.isMutable && onMerge && (react_1.default.createElement("button", { onClick: (e) => {
|
|
189
|
+
e.stopPropagation();
|
|
190
|
+
onMerge(collection.id);
|
|
191
|
+
}, className: "shrink-0 w-5 h-5 flex items-center justify-center text-faint hover:text-brand-accent transition-colors", title: `Merge ${displayName} into another collection`, "aria-label": `Merge ${displayName}` },
|
|
192
|
+
react_1.default.createElement(solid_1.ArrowsPointingInIcon, { className: "w-4 h-4" }))),
|
|
193
|
+
collection.isMutable && onDelete && (react_1.default.createElement("button", { onClick: (e) => {
|
|
194
|
+
e.stopPropagation();
|
|
195
|
+
onDelete(collection.id);
|
|
196
|
+
}, className: "shrink-0 w-5 h-5 flex items-center justify-center text-faint hover:text-status-error-icon transition-colors", title: `Remove ${displayName}`, "aria-label": `Remove ${displayName}` },
|
|
197
|
+
react_1.default.createElement(solid_1.TrashIcon, { className: "w-4 h-4" }))),
|
|
198
|
+
!collection.isMutable && react_1.default.createElement("span", { className: "text-xs text-muted" }, "(built-in)"))));
|
|
91
199
|
}
|
|
92
200
|
// ============================================================================
|
|
93
201
|
// CollectionSection
|
|
@@ -101,43 +209,65 @@ function CollectionRow(props) {
|
|
|
101
209
|
* @public
|
|
102
210
|
*/
|
|
103
211
|
function CollectionSection(props) {
|
|
104
|
-
|
|
212
|
+
var _a;
|
|
213
|
+
const { collections, onToggleVisibility, onAddDirectory, onCreateCollection, onDeleteCollection, onSetDefaultCollection, onExportCollection, onExportAllAsZip, onImportCollection, onOpenCollectionFromFile, onUnlockCollection, onRenameCollection, onMergeCollection, onHideCollection, onShowCollection, defaultCollapsed = false, sourceColorMap, sourceColorFallback } = props;
|
|
105
214
|
const [collapsed, setCollapsed] = (0, react_1.useState)(defaultCollapsed);
|
|
106
|
-
const
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
}
|
|
111
|
-
else {
|
|
112
|
-
const ids = allVisible
|
|
113
|
-
? collections.map((c) => c.id)
|
|
114
|
-
: collections.filter((c) => !c.isVisible).map((c) => c.id);
|
|
115
|
-
ids.forEach((id) => onToggleVisibility(id));
|
|
116
|
-
}
|
|
117
|
-
}, [onToggleAllVisibility, onToggleVisibility, allVisible, collections]);
|
|
215
|
+
const [hiddenExpanded, setHiddenExpanded] = (0, react_1.useState)(false);
|
|
216
|
+
const [contextMenu, setContextMenu] = (0, react_1.useState)(undefined);
|
|
217
|
+
const visibleCollections = collections.filter((c) => !c.isHidden);
|
|
218
|
+
const hiddenCollections = collections.filter((c) => c.isHidden);
|
|
118
219
|
const handleToggleCollapse = (0, react_1.useCallback)(() => {
|
|
119
220
|
setCollapsed((prev) => !prev);
|
|
120
221
|
}, []);
|
|
222
|
+
const handleToggleHiddenExpanded = (0, react_1.useCallback)(() => {
|
|
223
|
+
setHiddenExpanded((prev) => !prev);
|
|
224
|
+
}, []);
|
|
225
|
+
const handleOpenContextMenu = (0, react_1.useCallback)((collectionId, x, y) => {
|
|
226
|
+
setContextMenu({ collectionId, x, y });
|
|
227
|
+
}, []);
|
|
228
|
+
const handleCloseContextMenu = (0, react_1.useCallback)(() => {
|
|
229
|
+
setContextMenu(undefined);
|
|
230
|
+
}, []);
|
|
231
|
+
const getBorderColor = (0, react_1.useCallback)((sourceName) => {
|
|
232
|
+
if (!sourceColorMap)
|
|
233
|
+
return undefined;
|
|
234
|
+
if (sourceName && sourceName in sourceColorMap) {
|
|
235
|
+
return sourceColorMap[sourceName];
|
|
236
|
+
}
|
|
237
|
+
return sourceColorFallback;
|
|
238
|
+
}, [sourceColorMap, sourceColorFallback]);
|
|
239
|
+
const contextMenuCollection = contextMenu
|
|
240
|
+
? collections.find((c) => c.id === contextMenu.collectionId)
|
|
241
|
+
: undefined;
|
|
121
242
|
return (react_1.default.createElement("div", { className: "flex flex-col border-t border-border mt-1" },
|
|
122
243
|
react_1.default.createElement("div", { className: "flex items-center justify-between px-3 py-1.5" },
|
|
123
244
|
react_1.default.createElement("button", { onClick: handleToggleCollapse, className: "flex items-center gap-1 text-xs font-medium text-muted uppercase tracking-wider hover:text-secondary transition-colors" },
|
|
124
|
-
react_1.default.createElement(
|
|
245
|
+
react_1.default.createElement(solid_1.ChevronRightIcon, { className: `w-3 h-3 transition-transform ${collapsed ? '' : 'rotate-90'}` }),
|
|
125
246
|
"Collections",
|
|
126
247
|
react_1.default.createElement("span", { className: "text-muted normal-case font-normal" },
|
|
127
248
|
"(",
|
|
128
249
|
collections.length,
|
|
129
250
|
")")),
|
|
130
|
-
collections.length > 1 && (react_1.default.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}')),
|
|
131
251
|
react_1.default.createElement("div", { className: "flex items-center gap-1" },
|
|
132
252
|
onAddDirectory && (react_1.default.createElement("button", { onClick: onAddDirectory, className: "text-xs text-muted hover:text-brand-accent transition-colors px-1", title: "Add directory", "aria-label": "Add directory" },
|
|
133
|
-
"
|
|
134
|
-
'\uD83D\uDCC1')),
|
|
253
|
+
react_1.default.createElement(solid_1.FolderPlusIcon, { className: "w-4 h-4" }))),
|
|
135
254
|
onExportAllAsZip && (react_1.default.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" },
|
|
136
|
-
"
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
onImportCollection && (react_1.default.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" },
|
|
140
|
-
|
|
141
|
-
|
|
255
|
+
react_1.default.createElement(solid_1.ArchiveBoxArrowDownIcon, { className: "w-4 h-4" }))),
|
|
256
|
+
onOpenCollectionFromFile && (react_1.default.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" },
|
|
257
|
+
react_1.default.createElement(solid_1.FolderOpenIcon, { className: "w-4 h-4" }))),
|
|
258
|
+
onImportCollection && (react_1.default.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" },
|
|
259
|
+
react_1.default.createElement(solid_1.ArrowUpTrayIcon, { className: "w-4 h-4" }))),
|
|
260
|
+
onCreateCollection && (react_1.default.createElement("button", { onClick: onCreateCollection, "data-testid": "sidebar-new-collection-button", className: "text-muted hover:text-brand-accent transition-colors px-1", title: "New collection", "aria-label": "New collection" },
|
|
261
|
+
react_1.default.createElement(solid_1.PlusIcon, { className: "w-4 h-4" }))))),
|
|
262
|
+
!collapsed && (react_1.default.createElement("div", { className: "flex flex-col" },
|
|
263
|
+
visibleCollections.length === 0 && hiddenCollections.length === 0 ? (react_1.default.createElement("div", { className: "px-3 py-2 text-xs text-muted" }, "No collections")) : (visibleCollections.map((collection) => (react_1.default.createElement(CollectionRow, { key: collection.id, collection: collection, onToggleVisibility: onToggleVisibility, onSetDefault: onSetDefaultCollection, onDelete: onDeleteCollection, onExport: onExportCollection, onUnlock: onUnlockCollection, onRename: onRenameCollection, onMerge: onMergeCollection, borderColorClass: getBorderColor(collection.sourceName), onContextMenu: onHideCollection ? handleOpenContextMenu : undefined })))),
|
|
264
|
+
hiddenCollections.length > 0 && (react_1.default.createElement(react_1.default.Fragment, null,
|
|
265
|
+
react_1.default.createElement("button", { onClick: handleToggleHiddenExpanded, className: "flex items-center gap-1 px-3 py-1 text-xs text-muted hover:text-secondary transition-colors" },
|
|
266
|
+
react_1.default.createElement(solid_1.ChevronRightIcon, { className: `w-3 h-3 transition-transform ${hiddenExpanded ? 'rotate-90' : ''}` }),
|
|
267
|
+
hiddenCollections.length,
|
|
268
|
+
" hidden"),
|
|
269
|
+
hiddenExpanded &&
|
|
270
|
+
hiddenCollections.map((collection) => (react_1.default.createElement(CollectionRow, { key: collection.id, collection: collection, onToggleVisibility: onToggleVisibility, onSetDefault: onSetDefaultCollection, onDelete: onDeleteCollection, onExport: onExportCollection, onUnlock: onUnlockCollection, onRename: onRenameCollection, onMerge: onMergeCollection, borderColorClass: getBorderColor(collection.sourceName), onContextMenu: onShowCollection ? handleOpenContextMenu : undefined, isHiddenRow: true }))))))),
|
|
271
|
+
contextMenu && contextMenuCollection && (react_1.default.createElement(CollectionContextMenu, { menu: contextMenu, isHidden: (_a = contextMenuCollection.isHidden) !== null && _a !== void 0 ? _a : false, onHide: onHideCollection, onShow: onShowCollection, onClose: handleCloseContextMenu }))));
|
|
142
272
|
}
|
|
143
273
|
//# sourceMappingURL=CollectionSection.js.map
|
|
@@ -8,5 +8,5 @@ export { FilterRow, type IFilterRowProps, type IFilterOption } from './FilterRow
|
|
|
8
8
|
export { FilterBar, type IFilterBarProps } from './FilterBar';
|
|
9
9
|
export { EntityList, type IEntityListProps, type IEntityDescriptor, type IEntityStatus, type IEmptyStateConfig, type IEmptyStateAction } from './EntityList';
|
|
10
10
|
export { GroupedEntityList, type IGroupedEntityListProps, type IEntityGroupDescriptor } from './GroupedEntityList';
|
|
11
|
-
export { CollectionSection, type ICollectionSectionProps, type ICollectionRowItem } from './CollectionSection';
|
|
11
|
+
export { CollectionSection, type ICollectionBadge, type ICollectionSectionProps, type ICollectionRowItem, type SourceColorMap } from './CollectionSection';
|
|
12
12
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Optional badge shown alongside a tab label.
|
|
4
|
+
* @public
|
|
5
|
+
*/
|
|
6
|
+
export interface ITabBadge {
|
|
7
|
+
readonly kind: 'dot' | 'count';
|
|
8
|
+
readonly tone?: 'info' | 'warning' | 'danger';
|
|
9
|
+
readonly count?: number;
|
|
10
|
+
readonly ariaLabel?: string;
|
|
11
|
+
}
|
|
2
12
|
/**
|
|
3
13
|
* Configuration for a single tab.
|
|
4
14
|
* @public
|
|
@@ -8,6 +18,8 @@ export interface ITabConfig<TTab extends string> {
|
|
|
8
18
|
readonly id: TTab;
|
|
9
19
|
/** Display label */
|
|
10
20
|
readonly label: string;
|
|
21
|
+
/** Optional badge rendered next to the tab label */
|
|
22
|
+
readonly badge?: ITabBadge;
|
|
11
23
|
}
|
|
12
24
|
/**
|
|
13
25
|
* Props for the TabBar component.
|
|
@@ -26,6 +26,21 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
26
26
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
27
27
|
exports.TabBar = TabBar;
|
|
28
28
|
const react_1 = __importDefault(require("react"));
|
|
29
|
+
const BADGE_BASE_CLASSES = 'inline-flex shrink-0 items-center justify-center rounded-full font-medium leading-none';
|
|
30
|
+
const BADGE_TONE_CLASSES = {
|
|
31
|
+
info: 'bg-status-info-bg text-status-info-text',
|
|
32
|
+
warning: 'bg-status-warning-bg text-status-warning-text',
|
|
33
|
+
danger: 'bg-status-error-bg text-status-error-text'
|
|
34
|
+
};
|
|
35
|
+
function renderBadge(badge) {
|
|
36
|
+
var _a, _b;
|
|
37
|
+
const tone = (_a = badge.tone) !== null && _a !== void 0 ? _a : 'info';
|
|
38
|
+
const ariaLabel = badge.ariaLabel;
|
|
39
|
+
if (badge.kind === 'dot') {
|
|
40
|
+
return (react_1.default.createElement("span", { className: `ml-1.5 h-2 w-2 ${BADGE_BASE_CLASSES} ${BADGE_TONE_CLASSES[tone]}`, "aria-label": ariaLabel }));
|
|
41
|
+
}
|
|
42
|
+
return (react_1.default.createElement("span", { className: `ml-1.5 min-w-4 px-1.5 py-0.5 text-[0.6875rem] ${BADGE_BASE_CLASSES} ${BADGE_TONE_CLASSES[tone]}`, "aria-label": ariaLabel }, (_b = badge.count) !== null && _b !== void 0 ? _b : 0));
|
|
43
|
+
}
|
|
29
44
|
/**
|
|
30
45
|
* Second-level tab bar for switching views within a mode.
|
|
31
46
|
* @public
|
|
@@ -35,7 +50,9 @@ function TabBar(props) {
|
|
|
35
50
|
return (react_1.default.createElement("div", { className: "flex items-center gap-1 px-4 py-1 bg-brand-secondary text-white border-t border-white/10 overflow-x-auto" },
|
|
36
51
|
tabs.map((tab) => (react_1.default.createElement("button", { key: tab.id, onClick: () => onTabChange(tab.id), className: `px-3 py-1.5 rounded-md text-sm font-medium transition-colors shrink-0 ${activeTab === tab.id
|
|
37
52
|
? 'bg-white/20 text-white'
|
|
38
|
-
: 'text-white/60 hover:text-white hover:bg-white/10'}`, "aria-current": activeTab === tab.id ? 'page' : undefined },
|
|
53
|
+
: 'text-white/60 hover:text-white hover:bg-white/10'}`, "aria-current": activeTab === tab.id ? 'page' : undefined },
|
|
54
|
+
react_1.default.createElement("span", null, tab.label),
|
|
55
|
+
tab.badge && renderBadge(tab.badge)))),
|
|
39
56
|
rightContent !== undefined && (react_1.default.createElement(react_1.default.Fragment, null,
|
|
40
57
|
react_1.default.createElement("div", { className: "flex-1" }),
|
|
41
58
|
rightContent))));
|
|
@@ -3,5 +3,5 @@
|
|
|
3
3
|
* @packageDocumentation
|
|
4
4
|
*/
|
|
5
5
|
export { ModeSelector, type IModeConfig, type IModeSelectorProps } from './ModeSelector';
|
|
6
|
-
export { TabBar, type ITabConfig, type ITabBarProps } from './TabBar';
|
|
6
|
+
export { TabBar, type ITabBadge, type ITabConfig, type ITabBarProps } from './TabBar';
|
|
7
7
|
//# sourceMappingURL=index.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,17 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fgv/ts-app-shell",
|
|
3
|
-
"version": "5.1.0-
|
|
3
|
+
"version": "5.1.0-11",
|
|
4
4
|
"description": "Shared React UI primitives for application shells: column cascade, sidebar, toast/log messages, command palette, keybinding registry",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"types": "lib/index.d.ts",
|
|
7
|
-
"scripts": {
|
|
8
|
-
"build": "heft build --clean",
|
|
9
|
-
"clean": "heft clean",
|
|
10
|
-
"test": "heft test --clean",
|
|
11
|
-
"lint": "eslint src --ext .ts,.tsx",
|
|
12
|
-
"fixlint": "eslint src --ext .ts,.tsx --fix",
|
|
13
|
-
"coverage": "jest --coverage"
|
|
14
|
-
},
|
|
15
7
|
"sideEffects": false,
|
|
16
8
|
"keywords": [
|
|
17
9
|
"typescript",
|
|
@@ -23,18 +15,16 @@
|
|
|
23
15
|
"author": "Erik Fortune",
|
|
24
16
|
"license": "MIT",
|
|
25
17
|
"dependencies": {
|
|
26
|
-
"@fgv/ts-utils": "workspace:*",
|
|
27
18
|
"@heroicons/react": "~2.2.0",
|
|
28
19
|
"tslib": "^2.8.1",
|
|
29
|
-
"@fgv/ts-
|
|
20
|
+
"@fgv/ts-utils": "5.1.0-11",
|
|
21
|
+
"@fgv/ts-extras": "5.1.0-11"
|
|
30
22
|
},
|
|
31
23
|
"peerDependencies": {
|
|
32
24
|
"react": ">=18.0.0",
|
|
33
25
|
"react-dom": ">=18.0.0"
|
|
34
26
|
},
|
|
35
27
|
"devDependencies": {
|
|
36
|
-
"@fgv/heft-dual-rig": "workspace:*",
|
|
37
|
-
"@fgv/ts-utils-jest": "workspace:*",
|
|
38
28
|
"@types/react": "~19.2.8",
|
|
39
29
|
"@types/react-dom": "~19.2.3",
|
|
40
30
|
"react": "~19.2.3",
|
|
@@ -56,11 +46,13 @@
|
|
|
56
46
|
"rimraf": "^6.1.2",
|
|
57
47
|
"ts-jest": "^29.4.6",
|
|
58
48
|
"typescript": "5.9.3",
|
|
59
|
-
"@rushstack/heft": "1.2.
|
|
49
|
+
"@rushstack/heft": "1.2.7",
|
|
60
50
|
"@rushstack/eslint-config": "4.6.4",
|
|
61
51
|
"@rushstack/heft-jest-plugin": "1.2.6",
|
|
62
52
|
"@testing-library/dom": "^10.4.0",
|
|
63
|
-
"@rushstack/heft-node-rig": "2.11.
|
|
53
|
+
"@rushstack/heft-node-rig": "2.11.27",
|
|
54
|
+
"@fgv/heft-dual-rig": "5.1.0-11",
|
|
55
|
+
"@fgv/ts-utils-jest": "5.1.0-11"
|
|
64
56
|
},
|
|
65
57
|
"exports": {
|
|
66
58
|
".": {
|
|
@@ -78,5 +70,13 @@
|
|
|
78
70
|
"repository": {
|
|
79
71
|
"type": "git",
|
|
80
72
|
"url": "https://github.com/ErikFortune/fgv.git"
|
|
73
|
+
},
|
|
74
|
+
"scripts": {
|
|
75
|
+
"build": "heft build --clean",
|
|
76
|
+
"clean": "heft clean",
|
|
77
|
+
"test": "heft test --clean",
|
|
78
|
+
"lint": "eslint src --ext .ts,.tsx",
|
|
79
|
+
"fixlint": "eslint src --ext .ts,.tsx --fix",
|
|
80
|
+
"coverage": "jest --coverage"
|
|
81
81
|
}
|
|
82
|
-
}
|
|
82
|
+
}
|