@fgv/ts-app-shell 5.1.0-3 → 5.1.0-31
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 +159 -0
- package/dist/index.browser.js.map +1 -0
- package/dist/index.js.map +1 -0
- package/dist/packlets/ai-assist/index.js.map +1 -0
- package/dist/packlets/ai-assist/useAiAssist.js +216 -15
- package/dist/packlets/ai-assist/useAiAssist.js.map +1 -0
- package/dist/packlets/badge/index.js +23 -0
- package/dist/packlets/badge/index.js.map +1 -0
- package/dist/packlets/badge/renderStatusBadge.js +44 -0
- package/dist/packlets/badge/renderStatusBadge.js.map +1 -0
- package/dist/packlets/cascade/CascadeContainer.js.map +1 -0
- package/dist/packlets/cascade/ComparisonView.js.map +1 -0
- package/dist/packlets/cascade/EntityTabLayout.js.map +1 -0
- package/dist/packlets/cascade/MobileCascadeStack.js.map +1 -0
- package/dist/packlets/cascade/index.js.map +1 -0
- package/dist/packlets/cascade/model.js.map +1 -0
- package/dist/packlets/cascade/useCascadeOps.js.map +1 -0
- package/dist/packlets/cascade/useCascadeTransitions.js.map +1 -0
- package/dist/packlets/detail/DetailHelpers.js.map +1 -0
- package/dist/packlets/detail/index.js.map +1 -0
- package/dist/packlets/drop-zone/JsonDropZone.js.map +1 -0
- package/dist/packlets/drop-zone/index.js.map +1 -0
- package/dist/packlets/editing/EditFieldHelpers.js +4 -4
- package/dist/packlets/editing/EditFieldHelpers.js.map +1 -0
- package/dist/packlets/editing/MultiActionButton.js.map +1 -0
- package/dist/packlets/editing/NumericInput.js.map +1 -0
- package/dist/packlets/editing/TypeaheadInput.js.map +1 -0
- package/dist/packlets/editing/index.js.map +1 -0
- package/dist/packlets/editing/useTypeaheadMatch.js.map +1 -0
- package/dist/packlets/keyboard/index.js.map +1 -0
- package/dist/packlets/keyboard/registry.js.map +1 -0
- package/dist/packlets/keyboard/useKeyboardShortcuts.js.map +1 -0
- package/dist/packlets/messages/MessagesContext.js.map +1 -0
- package/dist/packlets/messages/MessagesLogger.js.map +1 -0
- package/dist/packlets/messages/StatusBar.js.map +1 -0
- package/dist/packlets/messages/Toast.js.map +1 -0
- package/dist/packlets/messages/index.js.map +1 -0
- package/dist/packlets/messages/model.js.map +1 -0
- package/dist/packlets/messages/useLogReporter.js.map +1 -0
- package/dist/packlets/modal/ConfirmDialog.js.map +1 -0
- package/dist/packlets/modal/Modal.js.map +1 -0
- package/dist/packlets/modal/index.js.map +1 -0
- package/dist/packlets/print/PrintEnclosure.js.map +1 -0
- package/dist/packlets/print/index.js.map +1 -0
- package/dist/packlets/print/openPrintWindow.js.map +1 -0
- package/dist/packlets/responsive/ResponsiveProvider.js.map +1 -0
- package/dist/packlets/responsive/index.js.map +1 -0
- package/dist/packlets/responsive/useResponsiveLayout.js.map +1 -0
- package/dist/packlets/selectors/EntityRow.js.map +1 -0
- package/dist/packlets/selectors/PreferredSelector.js.map +1 -0
- package/dist/packlets/selectors/index.js.map +1 -0
- package/dist/packlets/sidebar/CollectionSection.js +158 -43
- package/dist/packlets/sidebar/CollectionSection.js.map +1 -0
- package/dist/packlets/sidebar/EntityList.js.map +1 -0
- package/dist/packlets/sidebar/FilterBar.js.map +1 -0
- package/dist/packlets/sidebar/FilterRow.js.map +1 -0
- package/dist/packlets/sidebar/GroupedEntityList.js.map +1 -0
- package/dist/packlets/sidebar/SearchBar.js.map +1 -0
- package/dist/packlets/sidebar/SidebarLayout.js.map +1 -0
- package/dist/packlets/sidebar/index.js.map +1 -0
- package/dist/packlets/theme/ThemeProvider.js.map +1 -0
- package/dist/packlets/theme/index.js.map +1 -0
- package/dist/packlets/top-bar/ModeSelector.js.map +1 -0
- package/dist/packlets/top-bar/TabBar.js +4 -1
- package/dist/packlets/top-bar/TabBar.js.map +1 -0
- package/dist/packlets/top-bar/index.js.map +1 -0
- package/dist/packlets/url-sync/index.js.map +1 -0
- package/dist/packlets/url-sync/useUrlSync.js.map +1 -0
- package/dist/styles/theme.css +194 -0
- package/lib/index.browser.d.ts.map +1 -0
- package/lib/index.browser.js.map +1 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js.map +1 -0
- package/lib/packlets/ai-assist/index.d.ts.map +1 -0
- package/lib/packlets/ai-assist/index.js.map +1 -0
- package/lib/packlets/ai-assist/useAiAssist.d.ts +26 -0
- package/lib/packlets/ai-assist/useAiAssist.d.ts.map +1 -0
- package/lib/packlets/ai-assist/useAiAssist.js +216 -15
- package/lib/packlets/ai-assist/useAiAssist.js.map +1 -0
- package/lib/packlets/badge/index.d.ts +2 -0
- package/lib/packlets/badge/index.d.ts.map +1 -0
- package/lib/packlets/badge/index.js +27 -0
- package/lib/packlets/badge/index.js.map +1 -0
- package/lib/packlets/badge/renderStatusBadge.d.ts +11 -0
- package/lib/packlets/badge/renderStatusBadge.d.ts.map +1 -0
- package/lib/packlets/badge/renderStatusBadge.js +50 -0
- package/lib/packlets/badge/renderStatusBadge.js.map +1 -0
- package/lib/packlets/cascade/CascadeContainer.d.ts.map +1 -0
- package/lib/packlets/cascade/CascadeContainer.js.map +1 -0
- package/lib/packlets/cascade/ComparisonView.d.ts.map +1 -0
- package/lib/packlets/cascade/ComparisonView.js.map +1 -0
- package/lib/packlets/cascade/EntityTabLayout.d.ts.map +1 -0
- package/lib/packlets/cascade/EntityTabLayout.js.map +1 -0
- package/lib/packlets/cascade/MobileCascadeStack.d.ts.map +1 -0
- package/lib/packlets/cascade/MobileCascadeStack.js.map +1 -0
- package/lib/packlets/cascade/index.d.ts.map +1 -0
- package/lib/packlets/cascade/index.js.map +1 -0
- package/lib/packlets/cascade/model.d.ts.map +1 -0
- package/lib/packlets/cascade/model.js.map +1 -0
- package/lib/packlets/cascade/useCascadeOps.d.ts.map +1 -0
- package/lib/packlets/cascade/useCascadeOps.js.map +1 -0
- package/lib/packlets/cascade/useCascadeTransitions.d.ts.map +1 -0
- package/lib/packlets/cascade/useCascadeTransitions.js.map +1 -0
- package/lib/packlets/detail/DetailHelpers.d.ts.map +1 -0
- package/lib/packlets/detail/DetailHelpers.js.map +1 -0
- package/lib/packlets/detail/index.d.ts.map +1 -0
- package/lib/packlets/detail/index.js.map +1 -0
- package/lib/packlets/drop-zone/JsonDropZone.d.ts.map +1 -0
- package/lib/packlets/drop-zone/JsonDropZone.js.map +1 -0
- package/lib/packlets/drop-zone/index.d.ts.map +1 -0
- package/lib/packlets/drop-zone/index.js.map +1 -0
- package/lib/packlets/editing/EditFieldHelpers.d.ts +6 -2
- package/lib/packlets/editing/EditFieldHelpers.d.ts.map +1 -0
- package/lib/packlets/editing/EditFieldHelpers.js +4 -4
- package/lib/packlets/editing/EditFieldHelpers.js.map +1 -0
- package/lib/packlets/editing/MultiActionButton.d.ts.map +1 -0
- package/lib/packlets/editing/MultiActionButton.js.map +1 -0
- package/lib/packlets/editing/NumericInput.d.ts.map +1 -0
- package/lib/packlets/editing/NumericInput.js.map +1 -0
- package/lib/packlets/editing/TypeaheadInput.d.ts.map +1 -0
- package/lib/packlets/editing/TypeaheadInput.js.map +1 -0
- package/lib/packlets/editing/index.d.ts.map +1 -0
- package/lib/packlets/editing/index.js.map +1 -0
- package/lib/packlets/editing/useTypeaheadMatch.d.ts.map +1 -0
- package/lib/packlets/editing/useTypeaheadMatch.js.map +1 -0
- package/lib/packlets/keyboard/index.d.ts.map +1 -0
- package/lib/packlets/keyboard/index.js.map +1 -0
- package/lib/packlets/keyboard/registry.d.ts.map +1 -0
- package/lib/packlets/keyboard/registry.js.map +1 -0
- package/lib/packlets/keyboard/useKeyboardShortcuts.d.ts.map +1 -0
- package/lib/packlets/keyboard/useKeyboardShortcuts.js.map +1 -0
- package/lib/packlets/messages/MessagesContext.d.ts.map +1 -0
- package/lib/packlets/messages/MessagesContext.js.map +1 -0
- package/lib/packlets/messages/MessagesLogger.d.ts.map +1 -0
- package/lib/packlets/messages/MessagesLogger.js.map +1 -0
- package/lib/packlets/messages/StatusBar.d.ts.map +1 -0
- package/lib/packlets/messages/StatusBar.js.map +1 -0
- package/lib/packlets/messages/Toast.d.ts.map +1 -0
- package/lib/packlets/messages/Toast.js.map +1 -0
- package/lib/packlets/messages/index.d.ts.map +1 -0
- package/lib/packlets/messages/index.js.map +1 -0
- package/lib/packlets/messages/model.d.ts.map +1 -0
- package/lib/packlets/messages/model.js.map +1 -0
- package/lib/packlets/messages/useLogReporter.d.ts.map +1 -0
- package/lib/packlets/messages/useLogReporter.js.map +1 -0
- package/lib/packlets/modal/ConfirmDialog.d.ts.map +1 -0
- package/lib/packlets/modal/ConfirmDialog.js.map +1 -0
- package/lib/packlets/modal/Modal.d.ts.map +1 -0
- package/lib/packlets/modal/Modal.js.map +1 -0
- package/lib/packlets/modal/index.d.ts.map +1 -0
- package/lib/packlets/modal/index.js.map +1 -0
- package/lib/packlets/print/PrintEnclosure.d.ts.map +1 -0
- package/lib/packlets/print/PrintEnclosure.js.map +1 -0
- package/lib/packlets/print/index.d.ts.map +1 -0
- package/lib/packlets/print/index.js.map +1 -0
- package/lib/packlets/print/openPrintWindow.d.ts.map +1 -0
- package/lib/packlets/print/openPrintWindow.js.map +1 -0
- package/lib/packlets/responsive/ResponsiveProvider.d.ts.map +1 -0
- package/lib/packlets/responsive/ResponsiveProvider.js.map +1 -0
- package/lib/packlets/responsive/index.d.ts.map +1 -0
- package/lib/packlets/responsive/index.js.map +1 -0
- package/lib/packlets/responsive/useResponsiveLayout.d.ts.map +1 -0
- package/lib/packlets/responsive/useResponsiveLayout.js.map +1 -0
- package/lib/packlets/selectors/EntityRow.d.ts.map +1 -0
- package/lib/packlets/selectors/EntityRow.js.map +1 -0
- package/lib/packlets/selectors/PreferredSelector.d.ts.map +1 -0
- package/lib/packlets/selectors/PreferredSelector.js.map +1 -0
- package/lib/packlets/selectors/index.d.ts.map +1 -0
- package/lib/packlets/selectors/index.js.map +1 -0
- package/lib/packlets/sidebar/CollectionSection.d.ts +40 -2
- package/lib/packlets/sidebar/CollectionSection.d.ts.map +1 -0
- package/lib/packlets/sidebar/CollectionSection.js +157 -42
- package/lib/packlets/sidebar/CollectionSection.js.map +1 -0
- package/lib/packlets/sidebar/EntityList.d.ts.map +1 -0
- package/lib/packlets/sidebar/EntityList.js.map +1 -0
- package/lib/packlets/sidebar/FilterBar.d.ts.map +1 -0
- package/lib/packlets/sidebar/FilterBar.js.map +1 -0
- package/lib/packlets/sidebar/FilterRow.d.ts.map +1 -0
- package/lib/packlets/sidebar/FilterRow.js.map +1 -0
- package/lib/packlets/sidebar/GroupedEntityList.d.ts.map +1 -0
- package/lib/packlets/sidebar/GroupedEntityList.js.map +1 -0
- package/lib/packlets/sidebar/SearchBar.d.ts.map +1 -0
- package/lib/packlets/sidebar/SearchBar.js.map +1 -0
- package/lib/packlets/sidebar/SidebarLayout.d.ts.map +1 -0
- package/lib/packlets/sidebar/SidebarLayout.js.map +1 -0
- package/lib/packlets/sidebar/index.d.ts +1 -1
- package/lib/packlets/sidebar/index.d.ts.map +1 -0
- package/lib/packlets/sidebar/index.js.map +1 -0
- package/lib/packlets/theme/ThemeProvider.d.ts.map +1 -0
- package/lib/packlets/theme/ThemeProvider.js.map +1 -0
- package/lib/packlets/theme/index.d.ts.map +1 -0
- package/lib/packlets/theme/index.js.map +1 -0
- package/lib/packlets/top-bar/ModeSelector.d.ts.map +1 -0
- package/lib/packlets/top-bar/ModeSelector.js.map +1 -0
- package/lib/packlets/top-bar/TabBar.d.ts +12 -0
- package/lib/packlets/top-bar/TabBar.d.ts.map +1 -0
- package/lib/packlets/top-bar/TabBar.js +4 -1
- package/lib/packlets/top-bar/TabBar.js.map +1 -0
- package/lib/packlets/top-bar/index.d.ts +1 -1
- package/lib/packlets/top-bar/index.d.ts.map +1 -0
- package/lib/packlets/top-bar/index.js.map +1 -0
- package/lib/packlets/url-sync/index.d.ts.map +1 -0
- package/lib/packlets/url-sync/index.js.map +1 -0
- package/lib/packlets/url-sync/useUrlSync.d.ts.map +1 -0
- package/lib/packlets/url-sync/useUrlSync.js.map +1 -0
- package/lib/styles/theme.css +194 -0
- package/package.json +13 -9
- package/tailwind-preset.cjs +102 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CollectionSection.js","sourceRoot":"","sources":["../../../src/packlets/sidebar/CollectionSection.tsx"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n\n/**\n * Generic collection management section for the sidebar.\n *\n * Renders a collapsible list of collections with visibility toggles,\n * status indicators, and action buttons.\n *\n * @packageDocumentation\n */\n\nimport React, { useCallback, useEffect, useRef, useState } from 'react';\n\nimport { StarIcon as StarIconOutline } from '@heroicons/react/24/outline';\nimport {\n StarIcon as StarIconSolid,\n ExclamationTriangleIcon,\n BuildingLibraryIcon,\n BuildingStorefrontIcon,\n ShieldCheckIcon,\n ShieldExclamationIcon,\n ArrowDownTrayIcon,\n PencilSquareIcon,\n ArrowsPointingInIcon,\n TrashIcon,\n FolderPlusIcon,\n ArchiveBoxArrowDownIcon,\n FolderOpenIcon,\n ArrowUpTrayIcon,\n PlusIcon,\n ChevronRightIcon\n} from '@heroicons/react/20/solid';\n\nimport { renderStatusBadge } from '../badge';\n\n// ============================================================================\n// Collection Row Item\n// ============================================================================\n\n/**\n * Describes a single collection for rendering in the sidebar.\n * @public\n */\nexport interface ICollectionBadge {\n readonly kind: 'dot' | 'count';\n readonly tone?: 'info' | 'warning' | 'danger';\n readonly count?: number;\n readonly ariaLabel?: string;\n}\n\n/**\n * Describes a single collection for rendering in the sidebar.\n * @public\n */\nexport interface ICollectionRowItem {\n /** Collection identifier */\n readonly id: string;\n /** Display name (falls back to id if undefined) */\n readonly name: string | undefined;\n /** Number of items in this collection */\n readonly itemCount: number;\n /** Whether this collection can be modified */\n readonly isMutable: boolean;\n /** Whether this collection is encrypted/protected */\n readonly isProtected: boolean;\n /** Whether the protected collection has been unlocked */\n readonly isUnlocked: boolean;\n /** Whether this collection is currently visible */\n readonly isVisible: boolean;\n /** Whether this collection is the default target for new entities */\n readonly isDefault: boolean;\n /**\n * Whether this loaded collection has an orphaned encrypted shadow with the same ID\n * in another storage root. When true, a repair action should be offered.\n */\n readonly hasConflict?: boolean;\n /** Whether this collection is explicitly hidden by the user */\n readonly isHidden?: boolean;\n /** The name of the storage source this collection was loaded from */\n readonly sourceName?: string;\n /** Optional badge rendered alongside the collection name */\n readonly badge?: ICollectionBadge;\n /**\n * Label shown for immutable collections. Defaults to `\"(built-in)\"` when\n * omitted. Set to e.g. `\"(trading post)\"` for imported read-only content.\n */\n readonly readOnlyLabel?: string;\n /**\n * When true, the collection can be deleted even though it is not mutable.\n * Use for user-imported read-only content (e.g., Trading Post collections)\n * that the user should be able to remove but not edit.\n */\n readonly isDeletable?: boolean;\n}\n\n// ============================================================================\n// Collection Section Props\n// ============================================================================\n\n/**\n * Props for the CollectionSection component.\n * @public\n */\n/**\n * Maps source names to Tailwind border color classes for the left-border indicator.\n * @public\n */\nexport type SourceColorMap = Readonly<Record<string, string>>;\n\nexport interface ICollectionSectionProps {\n /** Collection items to display */\n readonly collections: ReadonlyArray<ICollectionRowItem>;\n /** Callback when visibility is toggled for a collection */\n readonly onToggleVisibility: (collectionId: string) => void;\n /** Callback when \"Add Directory\" is clicked */\n readonly onAddDirectory?: () => void;\n /** Callback when \"New Collection\" is clicked */\n readonly onCreateCollection?: () => void;\n /** Callback when delete is clicked for a mutable collection */\n readonly onDeleteCollection?: (collectionId: string) => void;\n /** Callback when the star/default is clicked for a collection */\n readonly onSetDefaultCollection?: (collectionId: string) => void;\n /** Callback when export is clicked for a mutable collection */\n readonly onExportCollection?: (collectionId: string) => void;\n /** Callback when \"Export All as Zip\" is clicked (header-level) */\n readonly onExportAllAsZip?: () => void;\n /** Callback when \"Import Collection\" is clicked (header-level) */\n readonly onImportCollection?: () => void;\n /** Callback when \"Open from File\" is clicked (header-level, File System Access API) */\n readonly onOpenCollectionFromFile?: () => void;\n /** Callback when the unlock button is clicked for a locked protected collection */\n readonly onUnlockCollection?: (collectionId: string) => void;\n /** Callback when rename is clicked for a mutable collection */\n readonly onRenameCollection?: (collectionId: string) => void;\n /** Callback when merge is clicked for a mutable collection */\n readonly onMergeCollection?: (collectionId: string) => void;\n /** Callback when hide is selected from the context menu */\n readonly onHideCollection?: (collectionId: string) => void;\n /** Callback when show (unhide) is selected from the context menu */\n readonly onShowCollection?: (collectionId: string) => void;\n /** Whether the section starts collapsed */\n readonly defaultCollapsed?: boolean;\n /** Maps sourceName values to Tailwind border-l color classes */\n readonly sourceColorMap?: SourceColorMap;\n /** Fallback border-l color class when sourceName is not in the map */\n readonly sourceColorFallback?: string;\n}\n\n// ============================================================================\n// Context Menu (internal)\n// ============================================================================\n\nconst LONG_PRESS_MS: number = 500;\n\ninterface IContextMenuState {\n readonly collectionId: string;\n readonly x: number;\n readonly y: number;\n}\n\nfunction CollectionContextMenu(props: {\n readonly menu: IContextMenuState;\n readonly isHidden: boolean;\n readonly onHide?: (id: string) => void;\n readonly onShow?: (id: string) => void;\n readonly onClose: () => void;\n}): React.ReactElement {\n const { menu, isHidden, onHide, onShow, onClose } = props;\n const ref = useRef<HTMLDivElement>(null);\n\n useEffect(() => {\n function handleClickOutside(e: MouseEvent): void {\n if (ref.current && !ref.current.contains(e.target as Node)) {\n onClose();\n }\n }\n document.addEventListener('mousedown', handleClickOutside);\n return (): void => document.removeEventListener('mousedown', handleClickOutside);\n }, [onClose]);\n\n const action = isHidden ? onShow : onHide;\n if (!action) {\n return <></>;\n }\n\n return (\n <div\n ref={ref}\n className=\"fixed z-50 bg-surface-raised border border-border rounded shadow-lg py-1 min-w-[140px]\"\n style={{ left: menu.x, top: menu.y }}\n >\n <button\n className=\"w-full text-left px-3 py-1.5 text-sm text-secondary hover:bg-hover transition-colors\"\n onClick={(): void => {\n action(menu.collectionId);\n onClose();\n }}\n >\n {isHidden ? 'Show collection' : 'Hide collection'}\n </button>\n </div>\n );\n}\n\n// ============================================================================\n// Long-press hook (internal)\n// ============================================================================\n\nfunction useLongPress(onLongPress: (e: React.TouchEvent) => void): {\n onTouchStart: (e: React.TouchEvent) => void;\n onTouchEnd: () => void;\n onTouchMove: () => void;\n} {\n const timerRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);\n const firedRef = useRef(false);\n\n const onTouchStart = useCallback(\n (e: React.TouchEvent): void => {\n firedRef.current = false;\n timerRef.current = setTimeout(() => {\n firedRef.current = true;\n onLongPress(e);\n }, LONG_PRESS_MS);\n },\n [onLongPress]\n );\n\n const cancel = useCallback((): void => {\n if (timerRef.current !== undefined) {\n clearTimeout(timerRef.current);\n timerRef.current = undefined;\n }\n }, []);\n\n return { onTouchStart, onTouchEnd: cancel, onTouchMove: cancel };\n}\n\n// ============================================================================\n// CollectionRow (internal)\n// ============================================================================\n\nfunction CollectionRow(props: {\n readonly collection: ICollectionRowItem;\n readonly onToggleVisibility: (id: string) => void;\n readonly onSetDefault?: (id: string) => void;\n readonly onDelete?: (id: string) => void;\n readonly onExport?: (id: string) => void;\n readonly onUnlock?: (id: string) => void;\n readonly onRename?: (id: string) => void;\n readonly onMerge?: (id: string) => void;\n readonly borderColorClass?: string;\n readonly onContextMenu?: (collectionId: string, x: number, y: number) => void;\n readonly isHiddenRow?: boolean;\n}): React.ReactElement {\n const {\n collection,\n onToggleVisibility,\n onSetDefault,\n onDelete,\n onExport,\n onUnlock,\n onRename,\n onMerge,\n borderColorClass,\n onContextMenu,\n isHiddenRow\n } = props;\n const displayName = collection.name ?? collection.id;\n\n const handleContextMenu = useCallback(\n (e: React.MouseEvent): void => {\n if (onContextMenu) {\n e.preventDefault();\n onContextMenu(collection.id, e.clientX, e.clientY);\n }\n },\n [onContextMenu, collection.id]\n );\n\n const handleLongPress = useCallback(\n (e: React.TouchEvent): void => {\n if (onContextMenu && e.touches.length > 0) {\n e.preventDefault();\n const touch = e.touches[0];\n onContextMenu(collection.id, touch.clientX, touch.clientY);\n }\n },\n [onContextMenu, collection.id]\n );\n\n const longPress = useLongPress(handleLongPress);\n\n return (\n <div\n onClick={(): void => onToggleVisibility(collection.id)}\n onContextMenu={handleContextMenu}\n {...longPress}\n className={`flex flex-col px-3 py-2.5 text-sm transition-colors hover:bg-hover cursor-pointer ${\n isHiddenRow\n ? 'text-muted opacity-30 line-through'\n : collection.isVisible\n ? 'text-secondary'\n : 'text-muted opacity-50'\n } ${borderColorClass ? `border-l-4 ${borderColorClass}` : ''}`}\n role=\"button\"\n aria-pressed={collection.isVisible}\n title={collection.sourceName ? `Source: ${collection.sourceName}` : displayName}\n >\n {/* Top line: status icons + name */}\n <div className=\"flex items-center gap-1.5\">\n {/* Default collection star */}\n {onSetDefault && collection.isMutable && (\n <button\n onClick={(e): void => {\n e.stopPropagation();\n onSetDefault(collection.id);\n }}\n className={`shrink-0 w-5 h-5 flex items-center justify-center transition-colors ${\n collection.isDefault ? 'text-star hover:text-star' : 'text-faint hover:text-star'\n }`}\n title={\n collection.isDefault\n ? 'Default collection for new items'\n : 'Set as default collection for new items'\n }\n aria-label={collection.isDefault ? `${displayName} is default` : `Set ${displayName} as default`}\n aria-pressed={collection.isDefault}\n >\n {collection.isDefault ? (\n <StarIconSolid className=\"w-4 h-4\" />\n ) : (\n <StarIconOutline className=\"w-4 h-4\" />\n )}\n </button>\n )}\n\n {/* Conflict indicator */}\n {collection.hasConflict && (\n <span\n className=\"shrink-0 text-status-warning-strong cursor-default\"\n title=\"An encrypted copy of this collection from another storage root has the same ID. Go to Settings → Storage to resolve the conflict.\"\n aria-label={`Conflict: encrypted shadow for ${displayName}`}\n >\n <ExclamationTriangleIcon className=\"w-4 h-4\" />\n </span>\n )}\n\n {/* Read-only indicator */}\n {!collection.isMutable && (\n <span\n className=\"shrink-0 text-muted\"\n title={collection.readOnlyLabel ?? 'Built-in collection (read-only)'}\n >\n {collection.isDeletable ? (\n <BuildingStorefrontIcon className=\"w-4 h-4\" />\n ) : (\n <BuildingLibraryIcon className=\"w-4 h-4\" />\n )}\n </span>\n )}\n\n {/* Protected/shield indicator */}\n {collection.isProtected &&\n (collection.isUnlocked || !onUnlock ? (\n <span\n className={`shrink-0 ${collection.isUnlocked ? 'text-status-success-icon' : 'text-muted'}`}\n title={collection.isUnlocked ? 'Protected (unlocked)' : 'Protected (locked)'}\n >\n {collection.isUnlocked ? (\n <ShieldCheckIcon className=\"w-4 h-4\" />\n ) : (\n <ShieldExclamationIcon className=\"w-4 h-4\" />\n )}\n </span>\n ) : (\n <button\n onClick={(e): void => {\n e.stopPropagation();\n onUnlock(collection.id);\n }}\n className=\"shrink-0 text-muted hover:text-star transition-colors\"\n title=\"Click to unlock\"\n aria-label={`Unlock ${displayName}`}\n >\n <ShieldExclamationIcon className=\"w-4 h-4\" />\n </button>\n ))}\n\n {/* Name */}\n <span className=\"flex-1 truncate\" title={displayName}>\n {displayName}\n </span>\n {collection.badge && renderStatusBadge(collection.badge)}\n <span className=\"shrink-0 text-xs text-muted\">{collection.itemCount}</span>\n </div>\n\n {/* Bottom line: action buttons or built-in label, indented to align under name */}\n <div className=\"flex items-center gap-1 mt-1 ml-[24px]\">\n {/* Action buttons (mutable only) */}\n {collection.isMutable && onExport && (\n <button\n onClick={(e): void => {\n e.stopPropagation();\n onExport(collection.id);\n }}\n className=\"shrink-0 w-5 h-5 flex items-center justify-center text-faint hover:text-brand-accent transition-colors\"\n title={`Export ${displayName}`}\n aria-label={`Export ${displayName}`}\n >\n <ArrowDownTrayIcon className=\"w-4 h-4\" />\n </button>\n )}\n {collection.isMutable && onRename && (\n <button\n onClick={(e): void => {\n e.stopPropagation();\n onRename(collection.id);\n }}\n className=\"shrink-0 w-5 h-5 flex items-center justify-center text-faint hover:text-brand-accent transition-colors\"\n title={`Rename ${displayName}`}\n aria-label={`Rename ${displayName}`}\n >\n <PencilSquareIcon className=\"w-4 h-4\" />\n </button>\n )}\n {collection.isMutable && onMerge && (\n <button\n onClick={(e): void => {\n e.stopPropagation();\n onMerge(collection.id);\n }}\n className=\"shrink-0 w-5 h-5 flex items-center justify-center text-faint hover:text-brand-accent transition-colors\"\n title={`Merge ${displayName} into another collection`}\n aria-label={`Merge ${displayName}`}\n >\n <ArrowsPointingInIcon className=\"w-4 h-4\" />\n </button>\n )}\n {(collection.isMutable || collection.isDeletable) && onDelete && (\n <button\n onClick={(e): void => {\n e.stopPropagation();\n onDelete(collection.id);\n }}\n className=\"shrink-0 w-5 h-5 flex items-center justify-center text-faint hover:text-status-error-icon transition-colors\"\n title={`Remove ${displayName}`}\n aria-label={`Remove ${displayName}`}\n >\n <TrashIcon className=\"w-4 h-4\" />\n </button>\n )}\n\n {/* Read-only label for immutable collections */}\n {!collection.isMutable && (\n <span className=\"text-xs text-muted\">{collection.readOnlyLabel ?? '(built-in)'}</span>\n )}\n </div>\n </div>\n );\n}\n\n// ============================================================================\n// CollectionSection\n// ============================================================================\n\n/**\n * Collapsible sidebar section for managing collections.\n *\n * Renders a header with action buttons and a list of collection rows\n * with visibility toggles, status indicators, and delete actions.\n *\n * @public\n */\nexport function CollectionSection(props: ICollectionSectionProps): React.ReactElement {\n const {\n collections,\n onToggleVisibility,\n onAddDirectory,\n onCreateCollection,\n onDeleteCollection,\n onSetDefaultCollection,\n onExportCollection,\n onExportAllAsZip,\n onImportCollection,\n onOpenCollectionFromFile,\n onUnlockCollection,\n onRenameCollection,\n onMergeCollection,\n onHideCollection,\n onShowCollection,\n defaultCollapsed = false,\n sourceColorMap,\n sourceColorFallback\n } = props;\n\n const [collapsed, setCollapsed] = useState(defaultCollapsed);\n const [hiddenExpanded, setHiddenExpanded] = useState(false);\n const [contextMenu, setContextMenu] = useState<IContextMenuState | undefined>(undefined);\n\n const visibleCollections = collections.filter((c) => !c.isHidden);\n const hiddenCollections = collections.filter((c) => c.isHidden);\n\n const handleToggleCollapse = useCallback((): void => {\n setCollapsed((prev) => !prev);\n }, []);\n\n const handleToggleHiddenExpanded = useCallback((): void => {\n setHiddenExpanded((prev) => !prev);\n }, []);\n\n const handleOpenContextMenu = useCallback((collectionId: string, x: number, y: number): void => {\n setContextMenu({ collectionId, x, y });\n }, []);\n\n const handleCloseContextMenu = useCallback((): void => {\n setContextMenu(undefined);\n }, []);\n\n const getBorderColor = useCallback(\n (sourceName: string | undefined): string | undefined => {\n if (!sourceColorMap) return undefined;\n if (sourceName && sourceName in sourceColorMap) {\n return sourceColorMap[sourceName];\n }\n return sourceColorFallback;\n },\n [sourceColorMap, sourceColorFallback]\n );\n\n const contextMenuCollection = contextMenu\n ? collections.find((c) => c.id === contextMenu.collectionId)\n : undefined;\n\n return (\n <div className=\"flex flex-col border-t border-border mt-1\">\n {/* Header */}\n <div className=\"flex items-center justify-between px-3 py-1.5\">\n <button\n onClick={handleToggleCollapse}\n className=\"flex items-center gap-1 text-xs font-medium text-muted uppercase tracking-wider hover:text-secondary transition-colors\"\n >\n <ChevronRightIcon className={`w-3 h-3 transition-transform ${collapsed ? '' : 'rotate-90'}`} />\n Collections\n <span className=\"text-muted normal-case font-normal\">({collections.length})</span>\n </button>\n\n {/* Action buttons */}\n <div className=\"flex items-center gap-1\">\n {onAddDirectory && (\n <button\n onClick={onAddDirectory}\n className=\"text-xs text-muted hover:text-brand-accent transition-colors px-1\"\n title=\"Add directory\"\n aria-label=\"Add directory\"\n >\n <FolderPlusIcon className=\"w-4 h-4\" />\n </button>\n )}\n {onExportAllAsZip && (\n <button\n onClick={onExportAllAsZip}\n className=\"text-xs text-muted hover:text-brand-accent transition-colors px-1\"\n title=\"Export all mutable collections as zip\"\n aria-label=\"Export all as zip\"\n >\n <ArchiveBoxArrowDownIcon className=\"w-4 h-4\" />\n </button>\n )}\n {onOpenCollectionFromFile && (\n <button\n onClick={onOpenCollectionFromFile}\n className=\"text-xs text-muted hover:text-brand-accent transition-colors px-1\"\n title=\"Open collection file for in-place editing\"\n aria-label=\"Open collection from file\"\n >\n <FolderOpenIcon className=\"w-4 h-4\" />\n </button>\n )}\n {onImportCollection && (\n <button\n onClick={onImportCollection}\n className=\"text-xs text-muted hover:text-brand-accent transition-colors px-1\"\n title=\"Import collection from file (in-memory)\"\n aria-label=\"Import collection from file\"\n >\n <ArrowUpTrayIcon className=\"w-4 h-4\" />\n </button>\n )}\n {onCreateCollection && (\n <button\n onClick={onCreateCollection}\n data-testid=\"sidebar-new-collection-button\"\n className=\"text-muted hover:text-brand-accent transition-colors px-1\"\n title=\"New collection\"\n aria-label=\"New collection\"\n >\n <PlusIcon className=\"w-4 h-4\" />\n </button>\n )}\n </div>\n </div>\n\n {/* Collection list */}\n {!collapsed && (\n <div className=\"flex flex-col\">\n {visibleCollections.length === 0 && hiddenCollections.length === 0 ? (\n <div className=\"px-3 py-2 text-xs text-muted\">No collections</div>\n ) : (\n visibleCollections.map((collection) => (\n <CollectionRow\n key={collection.id}\n collection={collection}\n onToggleVisibility={onToggleVisibility}\n onSetDefault={onSetDefaultCollection}\n onDelete={onDeleteCollection}\n onExport={onExportCollection}\n onUnlock={onUnlockCollection}\n onRename={onRenameCollection}\n onMerge={onMergeCollection}\n borderColorClass={getBorderColor(collection.sourceName)}\n onContextMenu={onHideCollection ? handleOpenContextMenu : undefined}\n />\n ))\n )}\n\n {/* Hidden collections expander */}\n {hiddenCollections.length > 0 && (\n <>\n <button\n onClick={handleToggleHiddenExpanded}\n className=\"flex items-center gap-1 px-3 py-1 text-xs text-muted hover:text-secondary transition-colors\"\n >\n <ChevronRightIcon\n className={`w-3 h-3 transition-transform ${hiddenExpanded ? 'rotate-90' : ''}`}\n />\n {hiddenCollections.length} hidden\n </button>\n {hiddenExpanded &&\n hiddenCollections.map((collection) => (\n <CollectionRow\n key={collection.id}\n collection={collection}\n onToggleVisibility={onToggleVisibility}\n onSetDefault={onSetDefaultCollection}\n onDelete={onDeleteCollection}\n onExport={onExportCollection}\n onUnlock={onUnlockCollection}\n onRename={onRenameCollection}\n onMerge={onMergeCollection}\n borderColorClass={getBorderColor(collection.sourceName)}\n onContextMenu={onShowCollection ? handleOpenContextMenu : undefined}\n isHiddenRow={true}\n />\n ))}\n </>\n )}\n </div>\n )}\n\n {/* Context menu */}\n {contextMenu && contextMenuCollection && (\n <CollectionContextMenu\n menu={contextMenu}\n isHidden={contextMenuCollection.isHidden ?? false}\n onHide={onHideCollection}\n onShow={onShowCollection}\n onClose={handleCloseContextMenu}\n />\n )}\n </div>\n );\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"EntityList.js","sourceRoot":"","sources":["../../../src/packlets/sidebar/EntityList.tsx"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n\nimport React, { useCallback, useEffect, useRef } from 'react';\nimport { useResponsive } from '../responsive';\n\n// ============================================================================\n// Entity Item Descriptor\n// ============================================================================\n\n/**\n * Describes how to extract display properties from an entity.\n * Consuming apps provide this to configure the list for their entity types.\n * @public\n */\nexport interface IEntityDescriptor<TEntity, TId extends string = string> {\n /** Extract the unique ID from an entity */\n readonly getId: (entity: TEntity) => TId;\n /** Extract the primary display label */\n readonly getLabel: (entity: TEntity) => string;\n /** Extract an optional secondary line (subtitle, description) */\n readonly getSublabel?: (entity: TEntity) => string | undefined;\n /** Extract an optional status badge or indicator */\n readonly getStatus?: (entity: TEntity) => IEntityStatus | undefined;\n}\n\n/**\n * Status indicator for an entity list item.\n * @public\n */\nexport interface IEntityStatus {\n /** Status label */\n readonly label: string;\n /** CSS color class for the status dot */\n readonly colorClass: string;\n}\n\n// ============================================================================\n// EntityList Props\n// ============================================================================\n\n/**\n * Props for the EntityList component.\n * @public\n */\nexport interface IEntityListProps<TEntity, TId extends string = string> {\n /** The entities to display */\n readonly entities: ReadonlyArray<TEntity>;\n /** Descriptor for extracting display properties */\n readonly descriptor: IEntityDescriptor<TEntity, TId>;\n /** Currently selected entity ID (if any) */\n readonly selectedId?: TId;\n /** Callback when an entity is selected (browse — list stays open) */\n readonly onSelect: (id: TId) => void;\n /** Callback when the user drills into the selected entity (Enter/→ — collapses list) */\n readonly onDrill?: () => void;\n /** Whether compare mode is active (shows checkboxes for multi-select) */\n readonly compareMode?: boolean;\n /** Entity IDs currently checked for comparison */\n readonly checkedIds?: ReadonlySet<string>;\n /** Callback to toggle an entity ID in/out of the compare selection */\n readonly onCheckedChange?: (id: TId) => void;\n /** Empty state configuration */\n readonly emptyState?: IEmptyStateConfig;\n /** Optional header content (e.g., result count) */\n readonly header?: React.ReactNode;\n /** Callback to toggle compare mode on/off (shows compare button in header) */\n readonly onToggleCompare?: () => void;\n /** Number of items currently selected for comparison */\n readonly compareCount?: number;\n /** Callback to start the comparison view (user clicks 'Compare Now') */\n readonly onStartComparison?: () => void;\n /**\n * Optional callback to delete an entity.\n * When provided, a delete button appears on each row (visible on hover/selection).\n * The consumer is responsible for showing a confirmation dialog before calling this.\n */\n readonly onDelete?: (id: TId) => void;\n /**\n * Optional predicate to control per-entity delete button visibility.\n * When `onDelete` is provided but `canDelete` is not, all rows show the delete button.\n * When both are provided, only rows where `canDelete(id)` returns true show the button.\n */\n readonly canDelete?: (id: TId) => boolean;\n}\n\n/**\n * Configuration for the empty state display.\n * @public\n */\nexport interface IEmptyStateConfig {\n /** Title for the empty state */\n readonly title: string;\n /** Description text */\n readonly description: string;\n /** Optional CTA button */\n readonly action?: IEmptyStateAction;\n}\n\n/**\n * Call-to-action for the empty state.\n * @public\n */\nexport interface IEmptyStateAction {\n /** Button label */\n readonly label: string;\n /** Callback when clicked */\n readonly onClick: () => void;\n}\n\n// ============================================================================\n// EntityList Component\n// ============================================================================\n\n/**\n * Generic entity list component for the sidebar.\n *\n * Renders a scrollable list of entities with:\n * - Primary label + optional sublabel\n * - Optional status indicator\n * - Selection highlighting\n * - Empty state with optional CTA\n *\n * @public\n */\nexport function EntityList<TEntity, TId extends string = string>(\n props: IEntityListProps<TEntity, TId>\n): React.ReactElement {\n const {\n entities,\n descriptor,\n selectedId,\n onSelect,\n onDrill,\n compareMode,\n checkedIds,\n onCheckedChange,\n emptyState,\n header,\n onDelete,\n canDelete\n } = props;\n const listRef = useRef<HTMLDivElement>(null);\n const { layoutMode } = useResponsive();\n const isMobile = layoutMode === 'mobile';\n\n // Scroll the selected item into view when selection changes\n useEffect(() => {\n if (selectedId && listRef.current) {\n const selectedButton = listRef.current.querySelector(`[data-entity-id=\"${selectedId}\"]`);\n if (selectedButton) {\n selectedButton.scrollIntoView({ block: 'nearest' });\n }\n }\n }, [selectedId]);\n\n // Find the index of the currently selected entity\n const selectedIndex = entities.findIndex((e) => descriptor.getId(e) === selectedId);\n\n const handleKeyDown = useCallback(\n (e: React.KeyboardEvent): void => {\n switch (e.key) {\n case 'ArrowDown': {\n e.preventDefault();\n const nextIndex = selectedIndex < entities.length - 1 ? selectedIndex + 1 : 0;\n onSelect(descriptor.getId(entities[nextIndex]));\n break;\n }\n case 'ArrowUp': {\n e.preventDefault();\n const prevIndex = selectedIndex > 0 ? selectedIndex - 1 : entities.length - 1;\n onSelect(descriptor.getId(entities[prevIndex]));\n break;\n }\n case 'Enter':\n case 'ArrowRight': {\n if (selectedId !== undefined && onDrill) {\n e.preventDefault();\n onDrill();\n }\n break;\n }\n default:\n break;\n }\n },\n [entities, descriptor, selectedId, selectedIndex, onSelect, onDrill]\n );\n\n if (entities.length === 0 && emptyState) {\n return <EmptyState config={emptyState} />;\n }\n\n return (\n <div className=\"flex flex-col flex-1 overflow-hidden\" onKeyDown={handleKeyDown}>\n {/* Header with item count and optional compare toggle */}\n {header !== undefined ? (\n <div className=\"px-3 py-1.5 text-xs text-muted border-b border-border-subtle\">{header}</div>\n ) : entities.length > 0 ? (\n <div className=\"flex items-center justify-between px-3 py-1.5 border-b border-border-subtle\">\n <span className=\"text-xs text-muted\">\n {entities.length} item{entities.length !== 1 ? 's' : ''}\n {compareMode && props.compareCount !== undefined && props.compareCount > 0 && (\n <span className=\"ml-1.5 text-brand-accent\">· {props.compareCount} selected</span>\n )}\n </span>\n <div className=\"flex items-center gap-1\">\n {compareMode &&\n props.onStartComparison &&\n props.compareCount !== undefined &&\n props.compareCount >= 2 && (\n <button\n onClick={(e): void => {\n e.stopPropagation();\n props.onStartComparison?.();\n }}\n 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\"\n >\n Compare Now\n </button>\n )}\n {props.onToggleCompare && (\n <button\n onClick={(e): void => {\n e.stopPropagation();\n props.onToggleCompare?.();\n }}\n className={`px-2 py-0.5 text-[11px] rounded border transition-colors ${\n compareMode\n ? 'bg-brand-accent text-white border-brand-accent'\n : 'bg-surface text-muted border-border hover:border-brand-accent hover:text-brand-accent'\n }`}\n >\n {compareMode ? 'Cancel' : 'Compare'}\n </button>\n )}\n </div>\n </div>\n ) : null}\n\n {/* List */}\n <div ref={listRef} className=\"flex-1 overflow-y-auto\">\n {entities.map((entity) => {\n const id = descriptor.getId(entity);\n const label = descriptor.getLabel(entity);\n const sublabel = descriptor.getSublabel?.(entity);\n const status = descriptor.getStatus?.(entity);\n const isSelected = id === selectedId;\n const isChecked = compareMode === true && checkedIds !== undefined && checkedIds.has(id);\n\n return (\n <div\n key={id}\n className={`group flex items-center gap-2 w-full border-b border-border-subtle transition-colors ${\n isChecked\n ? 'bg-selected border-l-2 border-l-selected-border'\n : isSelected && !compareMode\n ? 'bg-selected border-l-2 border-l-selected-border'\n : 'hover:bg-hover border-l-2 border-l-transparent'\n }`}\n >\n <button\n data-entity-id={id}\n onClick={(): void => {\n if (compareMode && onCheckedChange) {\n onCheckedChange(id);\n } else if (isSelected && onDrill) {\n onDrill();\n } else {\n onSelect(id);\n }\n }}\n className={`flex items-center gap-2 flex-1 min-w-0 px-3 text-left ${\n isMobile ? 'py-3' : 'py-2'\n }`}\n >\n {compareMode && (\n <span\n className={`flex items-center justify-center w-4 h-4 rounded border shrink-0 transition-colors ${\n isChecked\n ? 'bg-brand-accent border-brand-accent text-white'\n : 'border-border bg-surface'\n }`}\n >\n {isChecked && (\n <svg\n className=\"w-3 h-3\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n strokeWidth={3}\n >\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" d=\"M5 13l4 4L19 7\" />\n </svg>\n )}\n </span>\n )}\n <div className=\"flex-1 min-w-0\">\n <div\n className={`text-sm truncate ${\n (isSelected && !compareMode) || isChecked\n ? 'font-medium text-brand-primary'\n : 'text-primary'\n }`}\n >\n {label}\n </div>\n {sublabel && <div className=\"text-xs text-muted truncate mt-0.5\">{sublabel}</div>}\n </div>\n {status && (\n <span className=\"flex items-center gap-1 shrink-0 mt-0.5\">\n <span className={`w-2 h-2 rounded-full ${status.colorClass}`} />\n <span className=\"text-xs text-muted\">{status.label}</span>\n </span>\n )}\n </button>\n\n {onDelete && !compareMode && (!canDelete || canDelete(id)) && (\n <button\n onClick={(e): void => {\n e.stopPropagation();\n onDelete(id);\n }}\n className={`shrink-0 mr-1 flex items-center justify-center text-muted hover:text-status-error-text transition-opacity rounded ${\n isMobile\n ? 'w-11 h-11 opacity-100'\n : 'w-6 h-6 opacity-0 group-hover:opacity-100 focus:opacity-100'\n }`}\n title={`Delete ${label}`}\n aria-label={`Delete ${label}`}\n >\n <svg\n className=\"w-3.5 h-3.5\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n strokeWidth={2}\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n 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\"\n />\n </svg>\n </button>\n )}\n </div>\n );\n })}\n </div>\n </div>\n );\n}\n\n// ============================================================================\n// Empty State\n// ============================================================================\n\nfunction EmptyState({ config }: { readonly config: IEmptyStateConfig }): React.ReactElement {\n return (\n <div className=\"flex flex-col items-center justify-center flex-1 p-6 text-center\">\n <h3 className=\"text-sm font-medium text-secondary mb-1\">{config.title}</h3>\n <p className=\"text-xs text-muted mb-4\">{config.description}</p>\n {config.action && (\n <button\n onClick={config.action.onClick}\n className=\"px-3 py-1.5 text-xs font-medium text-white bg-brand-accent rounded-md hover:bg-brand-primary transition-colors\"\n >\n {config.action.label}\n </button>\n )}\n </div>\n );\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FilterBar.js","sourceRoot":"","sources":["../../../src/packlets/sidebar/FilterBar.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B,OAAO,EAAE,SAAS,EAAwB,MAAM,aAAa,CAAC;AAiB9D;;;;;;;GAOG;AACH,MAAM,UAAU,SAAS,CAAC,KAAsB;IAC9C,MAAM,EAAE,MAAM,EAAE,iBAAiB,EAAE,UAAU,EAAE,QAAQ,EAAE,GAAG,KAAK,CAAC;IAElE,MAAM,gBAAgB,GAAG,iBAAiB,GAAG,CAAC,IAAI,MAAM,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC;IAE1E,OAAO,CACL,6BAAK,SAAS,EAAC,eAAe;QAE5B,oBAAC,SAAS,oBAAK,MAAM,EAAI;QAGzB,6BAAK,SAAS,EAAC,6CAA6C;YAC1D,8BAAM,SAAS,EAAC,yDAAyD,cAAe;YACvF,gBAAgB,IAAI,CACnB,gCAAQ,OAAO,EAAE,UAAU,EAAE,SAAS,EAAC,oDAAoD,gBAElF,CACV,CACG;QAGN,6BAAK,SAAS,EAAC,eAAe,IAAE,QAAQ,CAAO,CAC3C,CACP,CAAC;AACJ,CAAC","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n\nimport React from 'react';\n\nimport { SearchBar, type ISearchBarProps } from './SearchBar';\n\n/**\n * Props for the FilterBar component.\n * @public\n */\nexport interface IFilterBarProps {\n /** Search bar props */\n readonly search: ISearchBarProps;\n /** Total number of active filters across all filter rows */\n readonly activeFilterCount: number;\n /** Callback to clear all filters (search + all filter rows) */\n readonly onClearAll: () => void;\n /** Filter row children (FilterRow components) */\n readonly children: React.ReactNode;\n}\n\n/**\n * Composite sidebar filter bar: search input + filter rows + clear-all.\n *\n * Renders the search bar at the top, filter rows in the middle,\n * and a \"Clear all\" button when any filters are active.\n *\n * @public\n */\nexport function FilterBar(props: IFilterBarProps): React.ReactElement {\n const { search, activeFilterCount, onClearAll, children } = props;\n\n const hasActiveFilters = activeFilterCount > 0 || search.value.length > 0;\n\n return (\n <div className=\"flex flex-col\">\n {/* Search */}\n <SearchBar {...search} />\n\n {/* Filter header with clear-all */}\n <div className=\"flex items-center justify-between px-3 py-1\">\n <span className=\"text-xs font-medium text-muted uppercase tracking-wider\">Filters</span>\n {hasActiveFilters && (\n <button onClick={onClearAll} className=\"text-xs text-brand-accent hover:text-brand-primary\">\n Clear all\n </button>\n )}\n </div>\n\n {/* Filter rows */}\n <div className=\"flex flex-col\">{children}</div>\n </div>\n );\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FilterRow.js","sourceRoot":"","sources":["../../../src/packlets/sidebar/FilterRow.tsx"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n\nimport React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport { useResponsive } from '../responsive';\n\n// ============================================================================\n// Filter Option\n// ============================================================================\n\n/**\n * A single selectable filter option.\n * Generic over the value type so apps can use string IDs, enums, etc.\n * @public\n */\nexport interface IFilterOption<TValue> {\n /** Unique value for this option */\n readonly value: TValue;\n /** Display label */\n readonly label: string;\n /** Optional count badge */\n readonly count?: number;\n}\n\n// ============================================================================\n// FilterRow Props\n// ============================================================================\n\n/**\n * Props for the FilterRow component.\n * @public\n */\nexport interface IFilterRowProps<TValue> {\n /** Filter label (e.g., 'Categories', 'Tags') */\n readonly label: string;\n /** Available options */\n readonly options: ReadonlyArray<IFilterOption<TValue>>;\n /** Currently selected values */\n readonly selected: ReadonlyArray<TValue>;\n /** Callback when selection changes */\n readonly onSelectionChange: (selected: ReadonlyArray<TValue>) => void;\n /** Whether multiple selections are allowed (default: true) */\n readonly multiple?: boolean;\n /** Custom equality check for values (default: ===) */\n readonly isEqual?: (a: TValue, b: TValue) => boolean;\n}\n\n// ============================================================================\n// FilterRow Component\n// ============================================================================\n\n/** Minimum number of options before showing the search-within-filter input */\nconst SEARCH_THRESHOLD: number = 8;\n\n/**\n * A compact filter row with a right-side flyout overlay for selecting filter values.\n *\n * Collapsed: shows the label, status summary text, and a `\\u203A` chevron.\n * Expanded: a flyout panel slides out to the right of the sidebar, overlaying the main pane.\n *\n * @public\n */\nexport function FilterRow<TValue>(props: IFilterRowProps<TValue>): React.ReactElement {\n const { label, options, selected, onSelectionChange, multiple = true, isEqual = defaultIsEqual } = props;\n const { layoutMode } = useResponsive();\n const isMobile = layoutMode === 'mobile';\n\n const [open, setOpen] = useState(false);\n const [flyoutSearch, setFlyoutSearch] = useState('');\n const rowRef = useRef<HTMLDivElement>(null);\n const flyoutRef = useRef<HTMLDivElement>(null);\n\n // Close flyout on outside click (check both row and flyout)\n useEffect(() => {\n if (!open) {\n return undefined;\n }\n const handleClick = (e: MouseEvent): void => {\n const target = e.target as Node;\n if (\n rowRef.current &&\n !rowRef.current.contains(target) &&\n flyoutRef.current &&\n !flyoutRef.current.contains(target)\n ) {\n setOpen(false);\n setFlyoutSearch('');\n }\n };\n document.addEventListener('mousedown', handleClick);\n return (): void => document.removeEventListener('mousedown', handleClick);\n }, [open]);\n\n // Close flyout on Escape\n useEffect(() => {\n if (!open) {\n return undefined;\n }\n const handleKey = (e: KeyboardEvent): void => {\n if (e.key === 'Escape') {\n setOpen(false);\n setFlyoutSearch('');\n }\n };\n document.addEventListener('keydown', handleKey);\n return (): void => document.removeEventListener('keydown', handleKey);\n }, [open]);\n\n const isSelected = useCallback(\n (value: TValue): boolean => selected.some((s) => isEqual(s, value)),\n [selected, isEqual]\n );\n\n const toggleValue = useCallback(\n (value: TValue): void => {\n if (multiple) {\n if (isSelected(value)) {\n onSelectionChange(selected.filter((s) => !isEqual(s, value)));\n } else {\n onSelectionChange([...selected, value]);\n }\n } else {\n // Single-select: toggle off if already selected, otherwise select\n if (isSelected(value)) {\n onSelectionChange([]);\n } else {\n onSelectionChange([value]);\n }\n }\n },\n [multiple, selected, onSelectionChange, isSelected, isEqual]\n );\n\n const clearSelection = useCallback(\n (e: React.MouseEvent): void => {\n e.stopPropagation();\n onSelectionChange([]);\n },\n [onSelectionChange]\n );\n\n const activeCount = selected.length;\n\n // Status summary text for the collapsed row\n const statusText = useMemo((): string => {\n if (activeCount === 0) {\n return 'All';\n }\n if (activeCount === 1) {\n // Show the label of the single selected option\n const sel = selected[0];\n const match = options.find((o) => isEqual(o.value, sel));\n return match ? match.label : '1 selected';\n }\n return `${activeCount} selected`;\n }, [activeCount, selected, options, isEqual]);\n\n // Filter options by flyout search\n const filteredOptions = useMemo(() => {\n const q = flyoutSearch.trim().toLowerCase();\n if (q.length === 0) {\n return options;\n }\n return options.filter((o) => o.label.toLowerCase().includes(q));\n }, [options, flyoutSearch]);\n\n // Compute flyout position: anchored to the right edge of the sidebar row, aligned vertically\n const [flyoutStyle, setFlyoutStyle] = useState<React.CSSProperties>({});\n useEffect(() => {\n if (!open || !rowRef.current) {\n return;\n }\n const rect = rowRef.current.getBoundingClientRect();\n // Find the sidebar container (the <aside> ancestor)\n let sidebar: HTMLElement | null = rowRef.current;\n while (sidebar && sidebar.tagName !== 'ASIDE') {\n sidebar = sidebar.parentElement;\n }\n const rightEdge = sidebar ? sidebar.getBoundingClientRect().right : rect.right;\n\n // Flyout appears at the right edge of the sidebar, vertically aligned with the row\n const maxHeight = Math.min(400, window.innerHeight - rect.top - 8);\n setFlyoutStyle({\n position: 'fixed',\n top: rect.top,\n left: rightEdge,\n width: 280,\n maxHeight,\n zIndex: 50\n });\n }, [open]);\n\n const handleToggle = useCallback((): void => {\n setOpen((prev) => {\n if (prev) {\n setFlyoutSearch('');\n }\n return !prev;\n });\n }, []);\n\n return (\n <div ref={rowRef}>\n {/* Collapsed row */}\n <button\n onClick={handleToggle}\n className={`flex items-center justify-between w-full px-3 text-sm hover:bg-hover transition-colors ${\n isMobile ? 'py-3' : 'py-1.5'\n } ${activeCount > 0 ? 'text-brand-primary font-medium' : 'text-secondary'}`}\n >\n <span className=\"truncate\">{label}</span>\n <span className=\"flex items-center gap-1.5 shrink-0 ml-2\">\n {activeCount > 0 && (\n <span\n onClick={clearSelection}\n className=\"text-muted hover:text-secondary cursor-pointer text-xs\"\n role=\"button\"\n aria-label={`Clear ${label} filter`}\n >\n {'\\u00D7'}\n </span>\n )}\n <span className={`text-xs ${activeCount > 0 ? 'text-brand-accent' : 'text-muted'}`}>\n {statusText}\n </span>\n <span className=\"text-muted text-xs\">{'\\u203A'}</span>\n </span>\n </button>\n\n {/* Right-side flyout overlay */}\n {open && (\n <div\n ref={flyoutRef}\n className=\"bg-surface border border-border rounded-r-lg shadow-xl overflow-hidden flex flex-col\"\n style={flyoutStyle}\n >\n {/* Flyout header */}\n <div className=\"flex items-center justify-between px-3 py-2 border-b border-border-subtle bg-surface-alt\">\n <span className=\"text-xs font-medium text-secondary\">{label}</span>\n {activeCount > 0 && (\n <button\n onClick={(): void => onSelectionChange([])}\n className=\"text-xs text-brand-accent hover:text-brand-primary\"\n >\n Clear\n </button>\n )}\n </div>\n\n {/* Search within filter (for long lists) */}\n {options.length >= SEARCH_THRESHOLD && (\n <div className=\"px-3 py-1.5 border-b border-border-subtle\">\n <input\n type=\"text\"\n value={flyoutSearch}\n onChange={(e): void => setFlyoutSearch(e.target.value)}\n placeholder={`Search ${label.toLowerCase()}...`}\n className=\"w-full px-2 py-1 text-xs border border-border rounded bg-surface text-primary focus:outline-none focus:border-focus-ring\"\n autoFocus\n />\n </div>\n )}\n\n {/* Options list */}\n <div className=\"flex-1 overflow-y-auto\">\n {filteredOptions.length === 0 ? (\n <div className=\"px-3 py-2 text-xs text-muted\">\n {options.length === 0 ? 'No options available' : 'No matches'}\n </div>\n ) : (\n filteredOptions.map((option, idx) => {\n const checked = isSelected(option.value);\n return (\n <button\n key={idx}\n onClick={(): void => toggleValue(option.value)}\n className={`flex items-center gap-2 w-full px-3 text-sm text-left hover:bg-hover transition-colors ${\n isMobile ? 'py-2.5' : 'py-1.5'\n } ${checked ? 'text-brand-primary font-medium' : 'text-secondary'}`}\n >\n <span\n className={`inline-flex items-center justify-center w-4 h-4 border rounded ${\n multiple ? 'rounded' : 'rounded-full'\n } ${checked ? 'bg-brand-accent border-brand-accent text-white' : 'border-border'}`}\n >\n {checked && <span className=\"text-[10px]\">{'\\u2713'}</span>}\n </span>\n <span className=\"flex-1 truncate\">{option.label}</span>\n {option.count !== undefined && <span className=\"text-xs text-muted\">{option.count}</span>}\n </button>\n );\n })\n )}\n </div>\n </div>\n )}\n </div>\n );\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\nfunction defaultIsEqual<TValue>(a: TValue, b: TValue): boolean {\n return a === b;\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"GroupedEntityList.js","sourceRoot":"","sources":["../../../src/packlets/sidebar/GroupedEntityList.tsx"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n\nimport React, { useCallback, useEffect, useMemo, useRef } from 'react';\nimport type { IEntityDescriptor, IEmptyStateConfig, IEntityStatus } from './EntityList';\n\n// ============================================================================\n// Group Descriptor\n// ============================================================================\n\n/**\n * Extends {@link IEntityDescriptor} with grouping support.\n * @public\n */\nexport interface IEntityGroupDescriptor<TEntity, TId extends string = string>\n extends IEntityDescriptor<TEntity, TId> {\n /** Extract a group key from an entity. Entities with the same key are grouped together. */\n readonly getGroupKey: (entity: TEntity) => string;\n /** Extract a display label for a group. Called on the first entity in each group. */\n readonly getGroupLabel: (entity: TEntity) => string;\n /** Optional sort comparator for group ordering. Receives group keys. */\n readonly compareGroups?: (a: string, b: string) => number;\n}\n\n// ============================================================================\n// GroupedEntityList Props\n// ============================================================================\n\n/**\n * Props for the {@link GroupedEntityList} component.\n * @public\n */\nexport interface IGroupedEntityListProps<TEntity, TId extends string = string> {\n /** The entities to display (pre-sorted within groups by the caller). */\n readonly entities: ReadonlyArray<TEntity>;\n /** Descriptor for extracting display and grouping properties. */\n readonly descriptor: IEntityGroupDescriptor<TEntity, TId>;\n /** Currently selected entity ID (if any). */\n readonly selectedId?: TId;\n /** Callback when an entity is selected (browse — list stays open). */\n readonly onSelect: (id: TId) => void;\n /** Callback when the user drills into the selected entity (Enter/Arrow Right — collapses list). */\n readonly onDrill?: () => void;\n /** Whether compare mode is active. */\n readonly compareMode?: boolean;\n /** Entity IDs currently checked for comparison. */\n readonly checkedIds?: ReadonlySet<string>;\n /** Callback to toggle an entity ID in/out of compare selection. */\n readonly onCheckedChange?: (id: TId) => void;\n /** Empty state configuration. */\n readonly emptyState?: IEmptyStateConfig;\n /** Callback to toggle compare mode. */\n readonly onToggleCompare?: () => void;\n /** Number of items selected for comparison. */\n readonly compareCount?: number;\n /** Callback to start comparison view. */\n readonly onStartComparison?: () => void;\n /** Callback to delete an entity. */\n readonly onDelete?: (id: TId) => void;\n /** Predicate to control per-entity delete button visibility. */\n readonly canDelete?: (id: TId) => boolean;\n}\n\n// ============================================================================\n// Internal types\n// ============================================================================\n\ninterface IEntityGroup<TEntity> {\n readonly key: string;\n readonly label: string;\n readonly items: ReadonlyArray<TEntity>;\n}\n\n// ============================================================================\n// Row Component (mirrors EntityList row markup)\n// ============================================================================\n\ninterface IGroupedEntityRowProps<TEntity, TId extends string> {\n readonly entity: TEntity;\n readonly descriptor: IEntityGroupDescriptor<TEntity, TId>;\n readonly selectedId?: TId;\n readonly compareMode?: boolean;\n readonly checkedIds?: ReadonlySet<string>;\n readonly onSelect: (id: TId) => void;\n readonly onDrill?: () => void;\n readonly onCheckedChange?: (id: TId) => void;\n readonly onDelete?: (id: TId) => void;\n readonly canDelete?: (id: TId) => boolean;\n}\n\nfunction GroupedEntityRowInner<TEntity, TId extends string>(\n props: IGroupedEntityRowProps<TEntity, TId>\n): React.ReactElement {\n const {\n entity,\n descriptor,\n selectedId,\n compareMode,\n checkedIds,\n onSelect,\n onDrill,\n onCheckedChange,\n onDelete,\n canDelete\n } = props;\n\n const id = descriptor.getId(entity);\n const label = descriptor.getLabel(entity);\n const sublabel = descriptor.getSublabel?.(entity);\n const status: IEntityStatus | undefined = descriptor.getStatus?.(entity);\n const isSelected = id === selectedId;\n const isChecked = compareMode === true && checkedIds !== undefined && checkedIds.has(id);\n\n return (\n <div\n className={`group flex items-center gap-2 w-full border-b border-border-subtle transition-colors ${\n isChecked\n ? 'bg-selected border-l-2 border-l-selected-border'\n : isSelected && !compareMode\n ? 'bg-selected border-l-2 border-l-selected-border'\n : 'hover:bg-hover border-l-2 border-l-transparent'\n }`}\n >\n <button\n data-entity-id={id}\n onClick={(): void => {\n if (compareMode && onCheckedChange) {\n onCheckedChange(id);\n } else if (isSelected && onDrill) {\n onDrill();\n } else {\n onSelect(id);\n }\n }}\n className=\"flex items-center gap-2 flex-1 min-w-0 px-3 py-2 text-left\"\n >\n {compareMode && (\n <span\n className={`flex items-center justify-center w-4 h-4 rounded border shrink-0 transition-colors ${\n isChecked ? 'bg-brand-accent border-brand-accent text-white' : 'border-border bg-surface'\n }`}\n >\n {isChecked && (\n <svg className=\"w-3 h-3\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" strokeWidth={3}>\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" d=\"M5 13l4 4L19 7\" />\n </svg>\n )}\n </span>\n )}\n <div className=\"flex-1 min-w-0\">\n <div\n className={`text-sm truncate ${\n (isSelected && !compareMode) || isChecked ? 'font-medium text-brand-primary' : 'text-primary'\n }`}\n >\n {label}\n </div>\n {sublabel && <div className=\"text-xs text-muted truncate mt-0.5\">{sublabel}</div>}\n </div>\n {status && (\n <span className=\"flex items-center gap-1 shrink-0 mt-0.5\">\n <span className={`w-2 h-2 rounded-full ${status.colorClass}`} />\n <span className=\"text-xs text-muted\">{status.label}</span>\n </span>\n )}\n </button>\n\n {onDelete && !compareMode && (!canDelete || canDelete(id)) && (\n <button\n onClick={(e): void => {\n e.stopPropagation();\n onDelete(id);\n }}\n className=\"shrink-0 mr-1 w-6 h-6 flex items-center justify-center text-faint hover:text-status-error-icon opacity-0 group-hover:opacity-100 focus:opacity-100 transition-opacity rounded\"\n title={`Delete ${label}`}\n aria-label={`Delete ${label}`}\n >\n <svg className=\"w-3.5 h-3.5\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" strokeWidth={2}>\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n 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\"\n />\n </svg>\n </button>\n )}\n </div>\n );\n}\n\nconst GroupedEntityRow: typeof GroupedEntityRowInner = React.memo(\n GroupedEntityRowInner\n) as typeof GroupedEntityRowInner;\n\n// ============================================================================\n// GroupedEntityList Component\n// ============================================================================\n\n/**\n * Entity list with sticky group headers.\n *\n * Groups entities by a key extracted via the descriptor, renders a sticky\n * header per group, and delegates item rendering to the same visual pattern\n * as {@link EntityList}.\n *\n * @public\n */\nexport function GroupedEntityList<TEntity, TId extends string = string>(\n props: IGroupedEntityListProps<TEntity, TId>\n): React.ReactElement {\n const {\n entities,\n descriptor,\n selectedId,\n onSelect,\n onDrill,\n compareMode,\n checkedIds,\n onCheckedChange,\n emptyState,\n onDelete,\n canDelete\n } = props;\n const listRef = useRef<HTMLDivElement>(null);\n\n // Build groups and flat entity list for keyboard navigation\n const { groups, flatEntities } = useMemo(() => {\n const groupMap = new Map<string, { label: string; items: TEntity[] }>();\n\n for (const entity of entities) {\n const key = descriptor.getGroupKey(entity);\n let group = groupMap.get(key);\n if (!group) {\n group = { label: descriptor.getGroupLabel(entity), items: [] };\n groupMap.set(key, group);\n }\n group.items.push(entity);\n }\n\n const sortedKeys = Array.from(groupMap.keys());\n if (descriptor.compareGroups) {\n const cmp = descriptor.compareGroups;\n sortedKeys.sort((a, b) => cmp(a, b));\n }\n\n const builtGroups: IEntityGroup<TEntity>[] = [];\n const flat: TEntity[] = [];\n\n for (const key of sortedKeys) {\n const group = groupMap.get(key)!;\n builtGroups.push({ key, label: group.label, items: group.items });\n flat.push(...group.items);\n }\n\n return { groups: builtGroups, flatEntities: flat };\n }, [entities, descriptor]);\n\n // Scroll selected item into view\n useEffect(() => {\n if (selectedId && listRef.current) {\n const selectedButton = listRef.current.querySelector(`[data-entity-id=\"${selectedId}\"]`);\n if (selectedButton) {\n selectedButton.scrollIntoView({ block: 'nearest' });\n }\n }\n }, [selectedId]);\n\n const selectedIndex = flatEntities.findIndex((e) => descriptor.getId(e) === selectedId);\n\n const handleKeyDown = useCallback(\n (e: React.KeyboardEvent): void => {\n switch (e.key) {\n case 'ArrowDown': {\n e.preventDefault();\n const nextIndex = selectedIndex < flatEntities.length - 1 ? selectedIndex + 1 : 0;\n onSelect(descriptor.getId(flatEntities[nextIndex]));\n break;\n }\n case 'ArrowUp': {\n e.preventDefault();\n const prevIndex = selectedIndex > 0 ? selectedIndex - 1 : flatEntities.length - 1;\n onSelect(descriptor.getId(flatEntities[prevIndex]));\n break;\n }\n case 'Enter':\n case 'ArrowRight': {\n if (selectedId !== undefined && onDrill) {\n e.preventDefault();\n onDrill();\n }\n break;\n }\n default:\n break;\n }\n },\n [flatEntities, descriptor, selectedId, selectedIndex, onSelect, onDrill]\n );\n\n // Empty state\n if (entities.length === 0 && emptyState) {\n return (\n <div className=\"flex flex-col items-center justify-center flex-1 p-6 text-center\">\n <h3 className=\"text-sm font-medium text-secondary mb-1\">{emptyState.title}</h3>\n <p className=\"text-xs text-muted mb-4\">{emptyState.description}</p>\n {emptyState.action && (\n <button\n onClick={emptyState.action.onClick}\n className=\"px-3 py-1.5 text-xs font-medium text-white bg-brand-accent rounded-md hover:bg-brand-primary transition-colors\"\n >\n {emptyState.action.label}\n </button>\n )}\n </div>\n );\n }\n\n return (\n <div className=\"flex flex-col flex-1 overflow-hidden\" onKeyDown={handleKeyDown}>\n {/* Header with total count */}\n {flatEntities.length > 0 && (\n <div className=\"flex items-center justify-between px-3 py-1.5 border-b border-border-subtle\">\n <span className=\"text-xs text-muted\">\n {flatEntities.length} item{flatEntities.length !== 1 ? 's' : ''}\n {compareMode && props.compareCount !== undefined && props.compareCount > 0 && (\n <span className=\"ml-1.5 text-brand-accent\">· {props.compareCount} selected</span>\n )}\n </span>\n <div className=\"flex items-center gap-1\">\n {compareMode &&\n props.onStartComparison &&\n props.compareCount !== undefined &&\n props.compareCount >= 2 && (\n <button\n onClick={(e): void => {\n e.stopPropagation();\n props.onStartComparison?.();\n }}\n 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\"\n >\n Compare Now\n </button>\n )}\n {props.onToggleCompare && (\n <button\n onClick={(e): void => {\n e.stopPropagation();\n props.onToggleCompare?.();\n }}\n className={`px-2 py-0.5 text-[11px] rounded border transition-colors ${\n compareMode\n ? 'bg-brand-accent text-white border-brand-accent'\n : 'bg-surface text-muted border-border hover:border-brand-accent hover:text-brand-accent'\n }`}\n >\n {compareMode ? 'Cancel' : 'Compare'}\n </button>\n )}\n </div>\n </div>\n )}\n\n {/* Grouped list */}\n <div ref={listRef} className=\"flex-1 overflow-y-auto\">\n {groups.map((group) => (\n <div key={group.key}>\n {/* Sticky group header */}\n <div className=\"px-3 py-1.5 text-xs font-medium text-muted uppercase tracking-wider bg-surface-alt border-b border-border-subtle sticky top-0 z-10\">\n {group.label}\n <span className=\"text-muted normal-case font-normal ml-1\">({group.items.length})</span>\n </div>\n\n {/* Items in group */}\n {group.items.map((entity) => (\n <GroupedEntityRow\n key={descriptor.getId(entity)}\n entity={entity}\n descriptor={descriptor}\n selectedId={selectedId}\n compareMode={compareMode}\n checkedIds={checkedIds}\n onSelect={onSelect}\n onDrill={onDrill}\n onCheckedChange={onCheckedChange}\n onDelete={onDelete}\n canDelete={canDelete}\n />\n ))}\n </div>\n ))}\n </div>\n </div>\n );\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SearchBar.js","sourceRoot":"","sources":["../../../src/packlets/sidebar/SearchBar.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,MAAM,OAAO,CAAC;AAe1B;;;GAGG;AACH,MAAM,UAAU,SAAS,CAAC,KAAsB;IAC9C,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,WAAW,GAAG,WAAW,EAAE,GAAG,KAAK,CAAC;IAE7D,OAAO,CACL,6BAAK,SAAS,EAAC,WAAW;QACxB,6BAAK,SAAS,EAAC,UAAU;YACvB,+BACE,IAAI,EAAC,MAAM,EACX,KAAK,EAAE,KAAK,EACZ,QAAQ,EAAE,CAAC,CAAC,EAAQ,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAC/C,WAAW,EAAE,WAAW,EACxB,SAAS,EAAC,uKAAuK,GACjL;YACD,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI,CACnB,gCACE,OAAO,EAAE,GAAS,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,EACjC,SAAS,EAAC,mFAAmF,gBAClF,cAAc,aAGlB,CACV,CACG,CACF,CACP,CAAC;AACJ,CAAC","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n\nimport React from 'react';\n\n/**\n * Props for the SearchBar component.\n * @public\n */\nexport interface ISearchBarProps {\n /** Current search query */\n readonly value: string;\n /** Callback when the search query changes */\n readonly onChange: (value: string) => void;\n /** Placeholder text (default: 'Search...') */\n readonly placeholder?: string;\n}\n\n/**\n * Search input bar for the sidebar.\n * @public\n */\nexport function SearchBar(props: ISearchBarProps): React.ReactElement {\n const { value, onChange, placeholder = 'Search...' } = props;\n\n return (\n <div className=\"px-3 py-2\">\n <div className=\"relative\">\n <input\n type=\"text\"\n value={value}\n onChange={(e): void => onChange(e.target.value)}\n placeholder={placeholder}\n className=\"w-full pl-3 pr-8 py-1.5 text-sm border border-border rounded-md bg-surface text-primary focus:outline-none focus:ring-1 focus:ring-focus-ring focus:border-focus-ring\"\n />\n {value.length > 0 && (\n <button\n onClick={(): void => onChange('')}\n className=\"absolute right-2 top-1/2 -translate-y-1/2 text-muted hover:text-secondary text-xs\"\n aria-label=\"Clear search\"\n >\n ×\n </button>\n )}\n </div>\n </div>\n );\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SidebarLayout.js","sourceRoot":"","sources":["../../../src/packlets/sidebar/SidebarLayout.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAE,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACzC,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAmB9C;;;;;;;;GAQG;AACH,MAAM,UAAU,aAAa,CAAC,KAA0B;IACtD,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,GAAG,OAAO,EAAE,aAAa,GAAG,KAAK,EAAE,cAAc,EAAE,GAAG,KAAK,CAAC;IACnG,MAAM,EAAE,UAAU,EAAE,GAAG,aAAa,EAAE,CAAC;IACvC,MAAM,QAAQ,GAAG,UAAU,KAAK,MAAM,CAAC;IAEvC,6BAA6B;IAC7B,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,CAAC,QAAQ,IAAI,CAAC,aAAa,EAAE,CAAC;YAChC,OAAO;QACT,CAAC;QAED,MAAM,aAAa,GAAG,CAAC,CAAgB,EAAQ,EAAE;YAC/C,IAAI,CAAC,CAAC,GAAG,KAAK,QAAQ,EAAE,CAAC;gBACvB,cAAc,aAAd,cAAc,uBAAd,cAAc,EAAI,CAAC;YACrB,CAAC;QACH,CAAC,CAAC;QAEF,QAAQ,CAAC,gBAAgB,CAAC,SAAS,EAAE,aAAa,CAAC,CAAC;QACpD,OAAO,GAAG,EAAE,CAAC,QAAQ,CAAC,mBAAmB,CAAC,SAAS,EAAE,aAAa,CAAC,CAAC;IACtE,CAAC,EAAE,CAAC,QAAQ,EAAE,aAAa,EAAE,cAAc,CAAC,CAAC,CAAC;IAE9C,4BAA4B;IAC5B,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,OAAO,CACL,6BAAK,SAAS,EAAC,6BAA6B;YAC1C,+BACE,SAAS,EAAC,0EAA0E,EACpF,KAAK,EAAE,EAAE,KAAK,EAAE,YAAY,EAAE,IAE7B,OAAO,CACF;YACR,6BAAK,SAAS,EAAC,sCAAsC,IAAE,QAAQ,CAAO,CAClE,CACP,CAAC;IACJ,CAAC;IAED,iCAAiC;IACjC,OAAO,CACL,6BAAK,SAAS,EAAC,+CAA+C;QAE5D,6BAAK,SAAS,EAAC,sCAAsC,IAAE,QAAQ,CAAO;QAGrE,aAAa,IAAI,CAChB,6BACE,SAAS,EAAC,mDAAmD,EAC7D,OAAO,EAAE,cAAc,iBACX,MAAM,GAClB,CACH;QAGD,+BACE,SAAS,EAAE,uJACT,aAAa,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,mBACpC,EAAE,EACF,KAAK,EAAE,EAAE,KAAK,EAAE,YAAY,EAAE,IAE7B,OAAO,CACF,CACJ,CACP,CAAC;AACJ,CAAC","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n\nimport React, { useEffect } from 'react';\nimport { useResponsive } from '../responsive';\n\n/**\n * Props for the SidebarLayout component.\n * @public\n */\nexport interface ISidebarLayoutProps {\n /** Sidebar content (filter bar, collection section, etc.) */\n readonly sidebar: React.ReactNode;\n /** Main content area */\n readonly children: React.ReactNode;\n /** Sidebar width in CSS units (default: '280px') */\n readonly sidebarWidth?: string;\n /** Whether the sidebar drawer is open (compact/mobile only). */\n readonly isSidebarOpen?: boolean;\n /** Called when the drawer should close (backdrop click, etc.). */\n readonly onSidebarClose?: () => void;\n}\n\n/**\n * Layout component that renders a persistent left sidebar alongside a main content area.\n *\n * On `full` layout mode, the sidebar is a fixed-width panel.\n * On `compact` and `mobile` layout modes, the sidebar becomes a slide-out drawer\n * controlled by {@link ISidebarLayoutProps.isSidebarOpen | isSidebarOpen} and\n * {@link ISidebarLayoutProps.onSidebarClose | onSidebarClose}.\n * @public\n */\nexport function SidebarLayout(props: ISidebarLayoutProps): React.ReactElement {\n const { sidebar, children, sidebarWidth = '280px', isSidebarOpen = false, onSidebarClose } = props;\n const { layoutMode } = useResponsive();\n const isDrawer = layoutMode !== 'full';\n\n // Close drawer on Escape key\n useEffect(() => {\n if (!isDrawer || !isSidebarOpen) {\n return;\n }\n\n const handleKeyDown = (e: KeyboardEvent): void => {\n if (e.key === 'Escape') {\n onSidebarClose?.();\n }\n };\n\n document.addEventListener('keydown', handleKeyDown);\n return () => document.removeEventListener('keydown', handleKeyDown);\n }, [isDrawer, isSidebarOpen, onSidebarClose]);\n\n // Full mode: static sidebar\n if (!isDrawer) {\n return (\n <div className=\"flex flex-1 overflow-hidden\">\n <aside\n className=\"flex flex-col border-r border-border bg-surface overflow-y-auto shrink-0\"\n style={{ width: sidebarWidth }}\n >\n {sidebar}\n </aside>\n <div className=\"flex flex-col flex-1 overflow-hidden\">{children}</div>\n </div>\n );\n }\n\n // Compact/mobile: drawer overlay\n return (\n <div className=\"flex flex-col flex-1 overflow-hidden relative\">\n {/* Main content — always visible */}\n <div className=\"flex flex-col flex-1 overflow-hidden\">{children}</div>\n\n {/* Backdrop */}\n {isSidebarOpen && (\n <div\n className=\"fixed inset-0 bg-backdrop z-40 transition-opacity\"\n onClick={onSidebarClose}\n aria-hidden=\"true\"\n />\n )}\n\n {/* Drawer */}\n <aside\n className={`fixed inset-y-0 left-0 z-50 flex flex-col bg-surface border-r border-border overflow-y-auto shadow-lg transition-transform duration-200 ease-in-out ${\n isSidebarOpen ? 'translate-x-0' : '-translate-x-full'\n }`}\n style={{ width: sidebarWidth }}\n >\n {sidebar}\n </aside>\n </div>\n );\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/packlets/sidebar/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,aAAa,EAA4B,MAAM,iBAAiB,CAAC;AAE1E,OAAO,EAAE,SAAS,EAAwB,MAAM,aAAa,CAAC;AAE9D,OAAO,EAAE,SAAS,EAA4C,MAAM,aAAa,CAAC;AAElF,OAAO,EAAE,SAAS,EAAwB,MAAM,aAAa,CAAC;AAE9D,OAAO,EACL,UAAU,EAMX,MAAM,cAAc,CAAC;AAEtB,OAAO,EACL,iBAAiB,EAGlB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,iBAAiB,EAKlB,MAAM,qBAAqB,CAAC","sourcesContent":["/**\n * Sidebar packlet - layout, search, filter rows, and entity list.\n * @packageDocumentation\n */\n\nexport { SidebarLayout, type ISidebarLayoutProps } from './SidebarLayout';\n\nexport { SearchBar, type ISearchBarProps } from './SearchBar';\n\nexport { FilterRow, type IFilterRowProps, type IFilterOption } from './FilterRow';\n\nexport { FilterBar, type IFilterBarProps } from './FilterBar';\n\nexport {\n EntityList,\n type IEntityListProps,\n type IEntityDescriptor,\n type IEntityStatus,\n type IEmptyStateConfig,\n type IEmptyStateAction\n} from './EntityList';\n\nexport {\n GroupedEntityList,\n type IGroupedEntityListProps,\n type IEntityGroupDescriptor\n} from './GroupedEntityList';\n\nexport {\n CollectionSection,\n type ICollectionBadge,\n type ICollectionSectionProps,\n type ICollectionRowItem,\n type SourceColorMap\n} from './CollectionSection';\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ThemeProvider.js","sourceRoot":"","sources":["../../../src/packlets/theme/ThemeProvider.tsx"],"names":[],"mappings":"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","sourcesContent":["// Copyright (c) 2026 Erik Fortune\n//\n// Permission is hereby granted, free of charge, to any person obtaining a copy\n// of this software and associated documentation files (the \"Software\"), to deal\n// in the Software without restriction, including without limitation the rights\n// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n// copies of the Software, and to permit persons to whom the Software is\n// furnished to do so, subject to the following conditions:\n//\n// The above copyright notice and this permission notice shall be included in all\n// copies or substantial portions of the Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n// SOFTWARE.\n\nimport React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';\n\n/**\n * A theme identifier string. Built-in values are 'light', 'dark', and 'system'.\n * Custom themes use arbitrary strings that map to CSS classes (e.g., 'ocean' maps to '.theme-ocean').\n * @public\n */\nexport type ThemeId = string;\n\n/**\n * Describes an available theme for display in selectors.\n * @public\n */\nexport interface IThemeOption {\n /** Theme identifier used in settings and CSS class mapping */\n readonly id: ThemeId;\n /** Human-readable label for display */\n readonly label: string;\n}\n\n/**\n * Value exposed by ThemeContext.\n * @public\n */\nexport interface IThemeContext {\n /** The currently active theme ID */\n readonly theme: ThemeId;\n /** Whether the resolved (effective) appearance is dark */\n readonly isDark: boolean;\n /** Set the active theme */\n readonly setTheme: (theme: ThemeId) => void;\n /** All available theme options */\n readonly availableThemes: ReadonlyArray<IThemeOption>;\n}\n\n/**\n * Default built-in themes.\n */\nconst BUILT_IN_THEMES: ReadonlyArray<IThemeOption> = [\n { id: 'light', label: 'Light' },\n { id: 'dark', label: 'Dark' },\n { id: 'system', label: 'System' }\n];\n\nconst ThemeContext: React.Context<IThemeContext | undefined> = createContext<IThemeContext | undefined>(\n undefined\n);\n\n/**\n * Maps a theme ID to the CSS class applied to the document element.\n * - 'light': no class (uses :root defaults)\n * - 'dark': 'dark' class\n * - 'system': 'system-theme' class (uses prefers-color-scheme media query)\n * - custom: 'theme-\\{id\\}' class\n */\nfunction themeIdToCssClass(id: ThemeId): string | undefined {\n switch (id) {\n case 'light':\n return undefined;\n case 'dark':\n return 'dark';\n case 'system':\n return 'system-theme';\n default:\n return `theme-${id}`;\n }\n}\n\n/**\n * Determines whether the effective appearance is dark.\n */\nfunction resolveIsDark(themeId: ThemeId): boolean {\n if (themeId === 'dark') {\n return true;\n }\n if (themeId === 'system' && typeof window !== 'undefined') {\n return window.matchMedia('(prefers-color-scheme: dark)').matches;\n }\n return false;\n}\n\n/**\n * All CSS classes that ThemeProvider may apply.\n * Used to cleanly remove the previous theme before applying a new one.\n */\nconst MANAGED_CLASSES: ReadonlyArray<string> = ['dark', 'system-theme'];\n\n/**\n * Props for {@link ThemeProvider}.\n * @public\n */\nexport interface IThemeProviderProps {\n /** Initial theme ID (typically from persisted settings). Defaults to 'light'. */\n readonly initialTheme?: ThemeId;\n /** Additional custom themes beyond the built-in light/dark/system. */\n readonly customThemes?: ReadonlyArray<IThemeOption>;\n /** Called when the user changes the theme. Use this to persist the choice. */\n readonly onThemeChange?: (theme: ThemeId) => void;\n /** Children */\n readonly children: React.ReactNode;\n}\n\n/**\n * Provides theme context to the application and manages the CSS class on `<html>`.\n *\n * Wrap your app (or a subtree) with this provider. Components use {@link useTheme}\n * to read or change the active theme.\n *\n * @example\n * ```tsx\n * <ThemeProvider initialTheme={settings.appearance?.theme} onThemeChange={saveTheme}>\n * <App />\n * </ThemeProvider>\n * ```\n * @public\n */\nexport function ThemeProvider({\n initialTheme = 'light',\n customThemes,\n onThemeChange,\n children\n}: IThemeProviderProps): React.JSX.Element {\n const [theme, setThemeState] = useState<ThemeId>(initialTheme);\n const [isDark, setIsDark] = useState(() => resolveIsDark(initialTheme));\n\n const availableThemes = useMemo<ReadonlyArray<IThemeOption>>(() => {\n if (!customThemes || customThemes.length === 0) {\n return BUILT_IN_THEMES;\n }\n return [...BUILT_IN_THEMES, ...customThemes];\n }, [customThemes]);\n\n const setTheme = useCallback(\n (newTheme: ThemeId) => {\n setThemeState(newTheme);\n onThemeChange?.(newTheme);\n },\n [onThemeChange]\n );\n\n // Apply CSS class to document element when theme changes\n useEffect(() => {\n const root = document.documentElement;\n\n // Remove all managed theme classes\n for (const cls of MANAGED_CLASSES) {\n root.classList.remove(cls);\n }\n // Also remove any custom theme classes\n for (const cls of Array.from(root.classList)) {\n if (cls.startsWith('theme-')) {\n root.classList.remove(cls);\n }\n }\n\n // Apply new theme class\n const cssClass = themeIdToCssClass(theme);\n if (cssClass) {\n root.classList.add(cssClass);\n }\n\n setIsDark(resolveIsDark(theme));\n }, [theme]);\n\n // Listen for OS preference changes when using 'system' theme\n useEffect(() => {\n if (theme !== 'system') {\n return;\n }\n\n const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');\n const handler = (e: MediaQueryListEvent): void => {\n setIsDark(e.matches);\n };\n\n mediaQuery.addEventListener('change', handler);\n return () => mediaQuery.removeEventListener('change', handler);\n }, [theme]);\n\n const value = useMemo<IThemeContext>(\n () => ({ theme, isDark, setTheme, availableThemes }),\n [theme, isDark, setTheme, availableThemes]\n );\n\n return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;\n}\n\n/**\n * Access the current theme and theme-switching controls.\n *\n * Must be called within a {@link ThemeProvider}.\n * @public\n */\nexport function useTheme(): IThemeContext {\n const context = useContext(ThemeContext);\n if (!context) {\n throw new Error('useTheme must be used within a ThemeProvider');\n }\n return context;\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/packlets/theme/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EACL,aAAa,EACb,QAAQ,EAKT,MAAM,iBAAiB,CAAC","sourcesContent":["/**\n * Theme provider and hook for runtime theme switching.\n * @packageDocumentation\n */\n\nexport {\n ThemeProvider,\n useTheme,\n type ThemeId,\n type IThemeOption,\n type IThemeContext,\n type IThemeProviderProps\n} from './ThemeProvider';\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ModeSelector.js","sourceRoot":"","sources":["../../../src/packlets/top-bar/ModeSelector.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAgC9C;;;;;;GAMG;AACH,MAAM,UAAU,YAAY,CAAuB,KAAgC;IACjF,MAAM,EAAE,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,YAAY,EAAE,KAAK,EAAE,YAAY,EAAE,GAAG,KAAK,CAAC;IACrF,MAAM,EAAE,UAAU,EAAE,GAAG,aAAa,EAAE,CAAC;IACvC,MAAM,QAAQ,GAAG,UAAU,KAAK,QAAQ,CAAC;IAEzC,OAAO,CACL,6BAAK,SAAS,EAAC,yEAAyE;QACtF,6BAAK,SAAS,EAAE,qBAAqB,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,EAAE;YAEhE,YAAY,IAAI,CACf,gCACE,OAAO,EAAE,YAAY,EACrB,SAAS,EAAC,6FAA6F,gBAC5F,WAAW;gBAEtB,6BAAK,SAAS,EAAC,SAAS,EAAC,IAAI,EAAC,MAAM,EAAC,OAAO,EAAC,WAAW,EAAC,WAAW,EAAE,GAAG,EAAE,MAAM,EAAC,cAAc;oBAC9F,8BACE,aAAa,EAAC,OAAO,EACrB,cAAc,EAAC,OAAO,EACtB,CAAC,EAAC,8CAA8C,GAChD,CACE,CACC,CACV;YACA,CAAC,QAAQ,IAAI,4BAAI,SAAS,EAAC,yCAAyC,IAAE,KAAK,CAAM;YAClF,6BAAK,SAAS,EAAC,YAAY,IACxB,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CACnB,gCACE,GAAG,EAAE,IAAI,CAAC,EAAE,EACZ,OAAO,EAAE,GAAS,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,EAAE,CAAC,EAC1C,SAAS,EAAE,4CACT,QAAQ,CAAC,CAAC,CAAC,mBAAmB,CAAC,CAAC,CAAC,qBACnC,IACE,UAAU,KAAK,IAAI,CAAC,EAAE;oBACpB,CAAC,CAAC,wBAAwB;oBAC1B,CAAC,CAAC,kDACN,EAAE,kBACY,UAAU,KAAK,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,IAExD,IAAI,CAAC,KAAK,CACJ,CACV,CAAC,CACE,CACF;QACL,YAAY,IAAI,6BAAK,SAAS,EAAC,yBAAyB,IAAE,YAAY,CAAO,CAC1E,CACP,CAAC;AACJ,CAAC","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n\nimport React from 'react';\nimport { useResponsive } from '../responsive';\n\n/**\n * Configuration for a single mode in the mode selector.\n * @public\n */\nexport interface IModeConfig<TMode extends string> {\n /** Mode identifier */\n readonly id: TMode;\n /** Display label */\n readonly label: string;\n}\n\n/**\n * Props for the ModeSelector component.\n * @public\n */\nexport interface IModeSelectorProps<TMode extends string> {\n /** Available modes */\n readonly modes: ReadonlyArray<IModeConfig<TMode>>;\n /** Currently active mode */\n readonly activeMode: TMode;\n /** Callback when a mode is selected */\n readonly onModeChange: (mode: TMode) => void;\n /** Optional right-side content (e.g., settings gear icon) */\n readonly rightContent?: React.ReactNode;\n /** Application title */\n readonly title: string;\n /** Optional callback to toggle sidebar drawer (shows hamburger icon when provided). */\n readonly onMenuToggle?: () => void;\n}\n\n/**\n * Top-level mode selector bar.\n * Renders the application title, mode toggle buttons, and optional right-side content.\n * When {@link IModeSelectorProps.onMenuToggle | onMenuToggle} is provided, a hamburger\n * menu button is shown at the left edge (for compact/mobile layouts).\n * @public\n */\nexport function ModeSelector<TMode extends string>(props: IModeSelectorProps<TMode>): React.ReactElement {\n const { modes, activeMode, onModeChange, rightContent, title, onMenuToggle } = props;\n const { layoutMode } = useResponsive();\n const isMobile = layoutMode === 'mobile';\n\n return (\n <div className=\"flex items-center justify-between px-4 py-2 bg-brand-primary text-white\">\n <div className={`flex items-center ${isMobile ? 'gap-2' : 'gap-6'}`}>\n {/* Hamburger menu button (compact/mobile only) */}\n {onMenuToggle && (\n <button\n onClick={onMenuToggle}\n className=\"p-1.5 -ml-1.5 rounded-md text-white/70 hover:text-white hover:bg-white/10 transition-colors\"\n aria-label=\"Open menu\"\n >\n <svg className=\"w-5 h-5\" fill=\"none\" viewBox=\"0 0 24 24\" strokeWidth={1.5} stroke=\"currentColor\">\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n d=\"M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5\"\n />\n </svg>\n </button>\n )}\n {!isMobile && <h1 className=\"text-lg font-semibold whitespace-nowrap\">{title}</h1>}\n <div className=\"flex gap-1\">\n {modes.map((mode) => (\n <button\n key={mode.id}\n onClick={(): void => onModeChange(mode.id)}\n className={`rounded-md font-medium transition-colors ${\n isMobile ? 'px-2 py-1 text-xs' : 'px-4 py-1.5 text-sm'\n } ${\n activeMode === mode.id\n ? 'bg-white/20 text-white'\n : 'text-white/70 hover:text-white hover:bg-white/10'\n }`}\n aria-current={activeMode === mode.id ? 'true' : undefined}\n >\n {mode.label}\n </button>\n ))}\n </div>\n </div>\n {rightContent && <div className=\"flex items-center gap-2\">{rightContent}</div>}\n </div>\n );\n}\n"]}
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
* SOFTWARE.
|
|
21
21
|
*/
|
|
22
22
|
import React from 'react';
|
|
23
|
+
import { renderStatusBadge } from '../badge';
|
|
23
24
|
/**
|
|
24
25
|
* Second-level tab bar for switching views within a mode.
|
|
25
26
|
* @public
|
|
@@ -29,7 +30,9 @@ export function TabBar(props) {
|
|
|
29
30
|
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
31
|
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
32
|
? 'bg-white/20 text-white'
|
|
32
|
-
: 'text-white/60 hover:text-white hover:bg-white/10'}`, "aria-current": activeTab === tab.id ? 'page' : undefined },
|
|
33
|
+
: 'text-white/60 hover:text-white hover:bg-white/10'}`, "aria-current": activeTab === tab.id ? 'page' : undefined },
|
|
34
|
+
React.createElement("span", null, tab.label),
|
|
35
|
+
tab.badge && renderStatusBadge(tab.badge)))),
|
|
33
36
|
rightContent !== undefined && (React.createElement(React.Fragment, null,
|
|
34
37
|
React.createElement("div", { className: "flex-1" }),
|
|
35
38
|
rightContent))));
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TabBar.js","sourceRoot":"","sources":["../../../src/packlets/top-bar/TabBar.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B,OAAO,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAyC7C;;;GAGG;AACH,MAAM,UAAU,MAAM,CAAsB,KAAyB;IACnE,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,WAAW,EAAE,YAAY,EAAE,GAAG,KAAK,CAAC;IAE7D,OAAO,CACL,6BAAK,SAAS,EAAC,0GAA0G;QACtH,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CACjB,gCACE,GAAG,EAAE,GAAG,CAAC,EAAE,EACX,OAAO,EAAE,GAAS,EAAE,CAAC,WAAW,CAAC,GAAG,CAAC,EAAE,CAAC,EACxC,SAAS,EAAE,yEACT,SAAS,KAAK,GAAG,CAAC,EAAE;gBAClB,CAAC,CAAC,wBAAwB;gBAC1B,CAAC,CAAC,kDACN,EAAE,kBACY,SAAS,KAAK,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS;YAEvD,kCAAO,GAAG,CAAC,KAAK,CAAQ;YACvB,GAAG,CAAC,KAAK,IAAI,iBAAiB,CAAC,GAAG,CAAC,KAAK,CAAC,CACnC,CACV,CAAC;QACD,YAAY,KAAK,SAAS,IAAI,CAC7B;YACE,6BAAK,SAAS,EAAC,QAAQ,GAAG;YACzB,YAAY,CACZ,CACJ,CACG,CACP,CAAC;AACJ,CAAC","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n\nimport React from 'react';\n\nimport { renderStatusBadge } from '../badge';\n\n/**\n * Optional badge shown alongside a tab label.\n * @public\n */\nexport interface ITabBadge {\n readonly kind: 'dot' | 'count';\n readonly tone?: 'info' | 'warning' | 'danger';\n readonly count?: number;\n readonly ariaLabel?: string;\n}\n\n/**\n * Configuration for a single tab.\n * @public\n */\nexport interface ITabConfig<TTab extends string> {\n /** Tab identifier */\n readonly id: TTab;\n /** Display label */\n readonly label: string;\n /** Optional badge rendered next to the tab label */\n readonly badge?: ITabBadge;\n}\n\n/**\n * Props for the TabBar component.\n * @public\n */\nexport interface ITabBarProps<TTab extends string> {\n /** Available tabs */\n readonly tabs: ReadonlyArray<ITabConfig<TTab>>;\n /** Currently active tab */\n readonly activeTab: TTab;\n /** Callback when a tab is selected */\n readonly onTabChange: (tab: TTab) => void;\n /** Optional content pinned to the far right of the tab bar */\n readonly rightContent?: React.ReactNode;\n}\n\n/**\n * Second-level tab bar for switching views within a mode.\n * @public\n */\nexport function TabBar<TTab extends string>(props: ITabBarProps<TTab>): React.ReactElement {\n const { tabs, activeTab, onTabChange, rightContent } = props;\n\n return (\n <div className=\"flex items-center gap-1 px-4 py-1 bg-brand-secondary text-white border-t border-white/10 overflow-x-auto\">\n {tabs.map((tab) => (\n <button\n key={tab.id}\n onClick={(): void => onTabChange(tab.id)}\n className={`px-3 py-1.5 rounded-md text-sm font-medium transition-colors shrink-0 ${\n activeTab === tab.id\n ? 'bg-white/20 text-white'\n : 'text-white/60 hover:text-white hover:bg-white/10'\n }`}\n aria-current={activeTab === tab.id ? 'page' : undefined}\n >\n <span>{tab.label}</span>\n {tab.badge && renderStatusBadge(tab.badge)}\n </button>\n ))}\n {rightContent !== undefined && (\n <>\n <div className=\"flex-1\" />\n {rightContent}\n </>\n )}\n </div>\n );\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/packlets/top-bar/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,YAAY,EAA6C,MAAM,gBAAgB,CAAC;AACzF,OAAO,EAAE,MAAM,EAAsD,MAAM,UAAU,CAAC","sourcesContent":["/**\n * Top bar components: mode selector and tab bar.\n * @packageDocumentation\n */\n\nexport { ModeSelector, type IModeConfig, type IModeSelectorProps } from './ModeSelector';\nexport { TabBar, type ITabBadge, type ITabConfig, type ITabBarProps } from './TabBar';\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/packlets/url-sync/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAKL,aAAa,EACb,YAAY,EACZ,UAAU,EACX,MAAM,cAAc,CAAC","sourcesContent":["/**\n * URL sync packlet - bidirectional URL hash synchronization for mode/tab navigation.\n * @packageDocumentation\n */\n\nexport {\n type IUrlSyncConfig,\n type IUrlSyncCallbacks,\n type IUrlSyncState,\n type IParsedHash,\n encodeUrlHash,\n parseUrlHash,\n useUrlSync\n} from './useUrlSync';\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useUrlSync.js","sourceRoot":"","sources":["../../../src/packlets/url-sync/useUrlSync.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n\n/**\n * Generic URL hash synchronization for two-tier mode/tab navigation.\n *\n * Encodes mode + tab in the URL hash: `#/{mode}/{tab}`\n * Syncs bidirectionally between application state and the URL.\n * Handles browser back/forward via popstate.\n *\n * @packageDocumentation\n */\n\nimport { useEffect, useRef } from 'react';\n\n// ============================================================================\n// Configuration\n// ============================================================================\n\n/**\n * Configuration for the URL sync hook.\n * Provides the validation tables and default values needed to\n * parse and validate URL hash segments.\n * @public\n */\nexport interface IUrlSyncConfig<TMode extends string, TTab extends string> {\n /** All valid mode identifiers */\n readonly validModes: ReadonlyArray<TMode>;\n /** Valid tabs per mode */\n readonly validTabs: Record<TMode, ReadonlyArray<TTab>>;\n /** Default tab for each mode (used when hash has a valid mode but no valid tab) */\n readonly defaultTabs: Record<TMode, TTab>;\n}\n\n/**\n * Callbacks that connect the URL sync hook to the application's state management.\n * @public\n */\nexport interface IUrlSyncCallbacks<TMode extends string, TTab extends string> {\n /** Set the active mode */\n readonly setMode: (mode: TMode) => void;\n /** Set the active tab */\n readonly setTab: (tab: TTab) => void;\n}\n\n/**\n * Current navigation state to sync to the URL.\n * @public\n */\nexport interface IUrlSyncState<TMode extends string, TTab extends string> {\n /** Current mode */\n readonly mode: TMode;\n /** Current active tab */\n readonly activeTab: TTab;\n}\n\n// ============================================================================\n// URL Encoding / Decoding\n// ============================================================================\n\n/**\n * Result of parsing a URL hash.\n * @public\n */\nexport interface IParsedHash<TMode extends string, TTab extends string> {\n readonly mode: TMode;\n readonly tab: TTab;\n}\n\n/**\n * Encodes mode + tab into a URL hash string.\n * @param mode - The current mode\n * @param tab - The current tab\n * @returns Hash string (e.g., '#/library/ingredients')\n * @public\n */\nexport function encodeUrlHash<TMode extends string, TTab extends string>(mode: TMode, tab: TTab): string {\n return `#/${mode}/${tab}`;\n}\n\n/**\n * Parses a URL hash string into mode + tab, validated against the config.\n * Returns undefined if the hash is invalid or empty.\n * @param hash - The hash string (with or without leading '#')\n * @param config - Validation configuration\n * @returns Parsed mode and tab, or undefined\n * @public\n */\nexport function parseUrlHash<TMode extends string, TTab extends string>(\n hash: string,\n config: IUrlSyncConfig<TMode, TTab>\n): IParsedHash<TMode, TTab> | undefined {\n const cleaned = hash.replace(/^#\\/?/, '');\n if (cleaned.length === 0) {\n return undefined;\n }\n\n const parts = cleaned.split('/');\n if (parts.length < 1) {\n return undefined;\n }\n\n const mode = parts[0] as TMode;\n if (!config.validModes.includes(mode)) {\n return undefined;\n }\n\n const validTabs = config.validTabs[mode];\n if (parts.length >= 2) {\n const tab = parts[1] as TTab;\n if (validTabs.includes(tab)) {\n return { mode, tab };\n }\n }\n\n // Valid mode but no valid tab — use default\n return { mode, tab: config.defaultTabs[mode] };\n}\n\n// ============================================================================\n// URL Sync Hook\n// ============================================================================\n\n/**\n * Hook that synchronizes two-tier mode/tab navigation state with the URL hash.\n *\n * - On mount: reads the URL hash and applies it to the store\n * - On state change: updates the URL hash (pushes history entry)\n * - On popstate (back/forward): reads the URL hash and applies it to the store\n *\n * Should be called once at the app root level.\n *\n * @param config - Validation configuration (modes, tabs, defaults)\n * @param state - Current navigation state\n * @param callbacks - State mutation callbacks\n * @public\n */\nexport function useUrlSync<TMode extends string, TTab extends string>(\n config: IUrlSyncConfig<TMode, TTab>,\n state: IUrlSyncState<TMode, TTab>,\n callbacks: IUrlSyncCallbacks<TMode, TTab>\n): void {\n const { mode, activeTab } = state;\n const { setMode, setTab } = callbacks;\n\n // Track whether we're currently applying a URL change to avoid circular updates\n const isApplyingUrl = useRef(false);\n // Track whether we've done the initial load\n const initialized = useRef(false);\n\n // Keep refs to the latest values so the mount effect captures them without\n // needing them in its dependency array (the effect must only run once).\n const configRef = useRef(config);\n configRef.current = config;\n const modeRef = useRef(mode);\n modeRef.current = mode;\n const activeTabRef = useRef(activeTab);\n activeTabRef.current = activeTab;\n const setModeRef = useRef(setMode);\n setModeRef.current = setMode;\n const setTabRef = useRef(setTab);\n setTabRef.current = setTab;\n\n // ---- Initial load: URL → store ----\n useEffect(() => {\n if (initialized.current) {\n return;\n }\n initialized.current = true;\n\n const parsed = parseUrlHash(window.location.hash, configRef.current);\n if (parsed) {\n isApplyingUrl.current = true;\n setModeRef.current(parsed.mode);\n setTabRef.current(parsed.tab);\n isApplyingUrl.current = false;\n } else {\n // No valid hash — write current state to URL\n const hash = encodeUrlHash(modeRef.current, activeTabRef.current);\n window.history.replaceState(null, '', hash);\n }\n // Intentionally empty: this effect must run exactly once on mount.\n // All values are accessed via refs to avoid stale closures.\n }, []);\n\n // ---- Store → URL: push history on navigation changes ----\n useEffect(() => {\n if (isApplyingUrl.current) {\n return;\n }\n\n const hash = encodeUrlHash(mode, activeTab);\n if (window.location.hash !== hash) {\n window.history.pushState(null, '', hash);\n }\n }, [mode, activeTab]);\n\n // ---- URL → store: handle popstate (back/forward) ----\n useEffect(() => {\n const handlePopState = (): void => {\n const parsed = parseUrlHash(window.location.hash, config);\n if (parsed) {\n isApplyingUrl.current = true;\n setMode(parsed.mode);\n setTab(parsed.tab);\n isApplyingUrl.current = false;\n }\n };\n\n window.addEventListener('popstate', handlePopState);\n return (): void => {\n window.removeEventListener('popstate', handlePopState);\n };\n }, [config, setMode, setTab]);\n}\n"]}
|