From 14dbd8da61e088b3d5d57593dbacd4e5776df4bc Mon Sep 17 00:00:00 2001 From: Jamie Pine Date: Wed, 7 Jan 2026 21:58:17 -0800 Subject: [PATCH] lint --- .claude/CLAUDE.md | 123 + .cursor/hooks.json | 10 + .../actions/publish-artifacts/.eslintrc.cjs | 43 +- .github/actions/publish-artifacts/index.ts | 125 +- .../actions/publish-artifacts/package.json | 36 +- .../actions/publish-artifacts/tsconfig.json | 50 +- .tasks/task.schema.json | 90 +- .vscode/extensions.json | 16 +- .vscode/launch.json | 258 +- .vscode/settings.json | 67 +- .vscode/tasks.json | 165 +- apps/mobile/app.json | 5 +- apps/mobile/babel.config.js | 18 +- .../AppIcon.appiconset/Contents.json | 2 +- .../Spacedrive/Images.xcassets/Contents.json | 6 +- .../Contents.json | 2 +- apps/mobile/metro.config.js | 60 +- .../sd-mobile-core/expo-module.config.json | 15 +- .../modules/sd-mobile-core/src/index.ts | 10 +- apps/mobile/react-native.config.js | 14 +- apps/mobile/src/App.tsx | 43 +- .../src/app/(drawer)/(tabs)/_layout.tsx | 34 +- .../mobile/src/app/(drawer)/(tabs)/browse.tsx | 2 +- .../src/app/(drawer)/(tabs)/network.tsx | 7 +- .../src/app/(drawer)/(tabs)/overview.tsx | 2 +- .../src/app/(drawer)/(tabs)/settings.tsx | 2 +- apps/mobile/src/app/(drawer)/_layout.tsx | 2 +- apps/mobile/src/app/_layout.tsx | 20 +- apps/mobile/src/app/index.tsx | 2 +- apps/mobile/src/app/search.tsx | 7 +- apps/mobile/src/client/SpacedriveClient.ts | 18 +- apps/mobile/src/client/hooks/useClient.tsx | 33 +- apps/mobile/src/client/hooks/useQuery.ts | 78 +- apps/mobile/src/client/index.ts | 21 +- apps/mobile/src/client/subscriptionManager.ts | 290 +- apps/mobile/src/client/transport.ts | 271 +- .../src/components/LibrarySwitcherPanel.tsx | 494 +- apps/mobile/src/components/PairingPanel.tsx | 252 +- .../src/components/primitive/Button.tsx | 226 +- apps/mobile/src/components/primitive/Card.tsx | 60 +- .../src/components/primitive/Divider.tsx | 15 +- .../src/components/primitive/InfoPill.tsx | 90 +- .../mobile/src/components/primitive/Input.tsx | 165 +- .../components/primitive/ScreenContainer.tsx | 83 +- .../components/primitive/SettingsGroup.tsx | 76 +- .../src/components/primitive/SettingsLink.tsx | 19 +- .../src/components/primitive/SettingsList.tsx | 38 +- .../components/primitive/SettingsOption.tsx | 23 +- .../src/components/primitive/SettingsRow.tsx | 112 +- .../components/primitive/SettingsSlider.tsx | 75 +- .../components/primitive/SettingsToggle.tsx | 28 +- .../src/components/primitive/Switch.tsx | 60 +- apps/mobile/src/components/primitive/index.ts | 16 +- .../src/components/sidebar/SidebarContent.tsx | 288 +- apps/mobile/src/contexts/AppResetContext.tsx | 12 +- .../mobile/src/navigation/DrawerNavigator.tsx | 45 +- apps/mobile/src/navigation/RootNavigator.tsx | 51 +- apps/mobile/src/navigation/TabNavigator.tsx | 145 +- .../src/navigation/stacks/BrowseStack.tsx | 13 +- .../src/navigation/stacks/NetworkStack.tsx | 11 +- .../src/navigation/stacks/OverviewStack.tsx | 11 +- .../src/navigation/stacks/SettingsStack.tsx | 11 +- apps/mobile/src/navigation/types.ts | 52 +- .../src/screens/browse/BrowseScreen.tsx | 362 +- .../src/screens/network/NetworkScreen.tsx | 175 +- .../src/screens/overview/OverviewScreen.tsx | 296 +- .../screens/overview/components/HeroStats.tsx | 147 +- .../overview/components/PairedDevices.tsx | 206 +- .../overview/components/StorageOverview.tsx | 366 +- .../src/screens/settings/SettingsScreen.tsx | 1238 +++-- apps/mobile/src/stores/explorer.ts | 160 +- apps/mobile/src/stores/index.ts | 12 +- apps/mobile/src/stores/preferences.ts | 143 +- apps/mobile/src/stores/sidebar.ts | 103 +- apps/mobile/src/utils/cn.ts | 4 +- apps/mobile/tailwind.config.js | 67 +- apps/mobile/tsconfig.json | 12 +- apps/tauri/Spacedrive.icon/icon.json | 108 +- apps/tauri/index.html | 18 +- apps/tauri/package.json | 90 +- apps/tauri/postcss.config.cjs | 9 +- apps/tauri/scripts/dev-with-daemon.ts | 268 +- .../tauri/src-tauri/capabilities/default.json | 48 +- apps/tauri/src/App.tsx | 506 +- apps/tauri/src/components/DragDemo.tsx | 481 +- apps/tauri/src/contextMenu.ts | 83 +- apps/tauri/src/hooks/useDragOperation.ts | 20 +- apps/tauri/src/hooks/useDropZone.ts | 25 +- apps/tauri/src/index.css | 68 +- apps/tauri/src/keybinds.ts | 260 +- apps/tauri/src/lib/drag.ts | 34 +- apps/tauri/src/main.tsx | 22 +- apps/tauri/src/platform.ts | 499 +- apps/tauri/src/routes/ContextMenuWindow.tsx | 253 +- apps/tauri/src/routes/DragOverlay.tsx | 22 +- apps/tauri/src/routes/Spacedrop.tsx | 28 +- apps/tauri/tailwind.config.cjs | 5 +- apps/tauri/tsconfig.json | 50 +- apps/tauri/tsconfig.node.json | 18 +- apps/tauri/vite.config.ts | 66 +- apps/web/index.html | 18 +- apps/web/package.json | 42 +- apps/web/src/main.tsx | 22 +- apps/web/src/platform.ts | 18 +- apps/web/tsconfig.json | 42 +- apps/web/tsconfig.node.json | 17 +- apps/web/vite.config.ts | 34 +- biome.jsonc | 4 + bun.lockb | Bin 1033362 -> 1046314 bytes .../results/shape_large-aggregation-hdd.json | 6 +- .../results/shape_large-aggregation-ssd.json | 2 +- ...hape_large-content_identification-hdd.json | 60 +- ...hape_large-content_identification-ssd.json | 2 +- .../shape_large-indexing_discovery-hdd.json | 6 +- .../shape_large-indexing_discovery-ssd.json | 2 +- .../results/shape_medium-aggregation-hdd.json | 6 +- .../results/shape_medium-aggregation-ssd.json | 2 +- ...ape_medium-content_identification-hdd.json | 6 +- ...ape_medium-content_identification-ssd.json | 2 +- .../shape_medium-indexing_discovery-hdd.json | 6 +- .../shape_medium-indexing_discovery-ssd.json | 2 +- .../results/shape_small-aggregation-hdd.json | 6 +- .../results/shape_small-aggregation-ssd.json | 2 +- ...hape_small-content_identification-hdd.json | 6 +- ...hape_small-content_identification-ssd.json | 2 +- .../shape_small-indexing_discovery-hdd.json | 6 +- .../shape_small-indexing_discovery-ssd.json | 2 +- core/device.json | 2 +- docs/custom.css | 8 +- docs/mint.json | 8 +- extensions/photos/manifest.json | 126 +- extensions/photos/ui_manifest.json | 291 +- extensions/test-extension/manifest.json | 41 +- package.json | 8 +- packages/assets/icons/index.ts | 76 +- packages/assets/images/index.ts | 2 +- packages/assets/lottie/loading-pulse.json | 538 +- packages/assets/package.json | 44 +- packages/assets/scripts/generate.mjs | 114 +- packages/assets/sounds/index.ts | 54 +- packages/assets/svgs/ext/Extras/urls.ts | 18 +- packages/assets/svgs/ext/icons.json | 4331 ++++++++++++++++- packages/assets/svgs/ext/index.ts | 18 +- packages/assets/util/index.ts | 134 +- packages/config/app.tsconfig.json | 10 +- packages/config/base.tsconfig.json | 36 +- packages/config/package.json | 12 +- packages/config/tsconfig.json | 11 +- packages/interface/package.json | 128 +- packages/interface/src/Shell.tsx | 146 +- packages/interface/src/ShellLayout.tsx | 412 +- packages/interface/src/TopBar/Context.tsx | 322 +- packages/interface/src/TopBar/Item.tsx | 83 +- .../interface/src/TopBar/OverflowMenu.tsx | 104 +- packages/interface/src/TopBar/Portal.tsx | 44 +- packages/interface/src/TopBar/Section.tsx | 135 +- packages/interface/src/TopBar/TopBar.tsx | 80 +- packages/interface/src/TopBar/index.ts | 4 +- .../src/TopBar/useOverflowCalculation.ts | 234 +- .../interface/src/components/DndProvider.tsx | 764 ++- .../src/components/ErrorBoundary.tsx | 111 +- .../src/components/Inspector/Inspector.tsx | 93 +- .../Inspector/primitives/Divider.tsx | 11 +- .../Inspector/primitives/InfoRow.tsx | 12 +- .../Inspector/primitives/Section.tsx | 13 +- .../Inspector/primitives/TabContent.tsx | 10 +- .../components/Inspector/primitives/Tabs.tsx | 30 +- .../components/Inspector/primitives/Tag.tsx | 6 +- .../Inspector/primitives/Thumbnail.tsx | 25 +- .../components/Inspector/primitives/index.ts | 8 +- .../Inspector/variants/FileInspector.tsx | 2502 +++++----- .../Inspector/variants/KnowledgeInspector.tsx | 68 +- .../Inspector/variants/LocationInspector.tsx | 1384 +++--- .../Inspector/variants/MultiFileInspector.tsx | 400 +- .../components/Inspector/variants/index.ts | 2 +- .../JobManager/JobManagerPopover.tsx | 65 +- .../JobManager/JobsScreen/JobRow.tsx | 326 +- .../JobManager/JobsScreen/index.tsx | 346 +- .../JobManager/components/JobCard.tsx | 36 +- .../JobManager/components/JobList.tsx | 13 +- .../JobManager/components/JobProgressBar.tsx | 14 +- .../components/JobStatusIndicator.tsx | 35 +- .../JobManager/hooks/useJobCount.ts | 101 +- .../JobManager/hooks/useJobManager.ts | 35 +- .../components/JobManager/hooks/useJobs.ts | 37 +- .../src/components/JobManager/index.ts | 2 +- .../src/components/JobManager/types.ts | 36 +- packages/interface/src/components/Orb.tsx | 237 +- .../components/QuickPreview/AudioPlayer.tsx | 750 ++- .../QuickPreview/ContentRenderer.tsx | 1171 +++-- .../components/QuickPreview/Controller.tsx | 87 +- .../QuickPreview/DirectoryPreview.tsx | 176 +- .../components/QuickPreview/MeshViewer.tsx | 2049 ++++---- .../components/QuickPreview/QuickPreview.tsx | 276 +- .../QuickPreview/QuickPreviewFullscreen.tsx | 515 +- .../QuickPreview/QuickPreviewModal.tsx | 308 +- .../QuickPreview/QuickPreviewOverlay.tsx | 266 +- .../QuickPreview/SplatShimmerEffect.tsx | 160 +- .../QuickPreview/SubtitleSettingsMenu.tsx | 233 +- .../src/components/QuickPreview/Subtitles.tsx | 313 +- .../src/components/QuickPreview/Syncer.tsx | 28 +- .../components/QuickPreview/TextViewer.tsx | 267 +- .../QuickPreview/TimelineScrubber.tsx | 218 +- .../components/QuickPreview/VideoControls.tsx | 502 +- .../components/QuickPreview/VideoPlayer.tsx | 628 ++- .../src/components/QuickPreview/index.ts | 19 +- .../src/components/QuickPreview/prism-lazy.ts | 98 +- .../src/components/QuickPreview/prism.tsx | 85 +- .../src/components/QuickPreview/useZoomPan.ts | 267 +- .../SpacesSidebar/AddGroupButton.tsx | 26 +- .../SpacesSidebar/AddGroupModal.tsx | 127 +- .../SpacesSidebar/CreateSpaceModal.tsx | 202 +- .../components/SpacesSidebar/DevicesGroup.tsx | 283 +- .../components/SpacesSidebar/GroupHeader.tsx | 36 +- .../SpacesSidebar/LocationsGroup.tsx | 10 +- .../SpacesSidebar/SpaceCustomizationPanel.tsx | 512 +- .../components/SpacesSidebar/SpaceGroup.tsx | 297 +- .../components/SpacesSidebar/SpaceItem.tsx | 535 +- .../SpacesSidebar/SpaceSwitcher.tsx | 154 +- .../components/SpacesSidebar/TagsGroup.tsx | 379 +- .../components/SpacesSidebar/VolumesGroup.tsx | 154 +- .../src/components/SpacesSidebar/dnd.ts | 37 +- .../components/SpacesSidebar/hooks/index.ts | 42 +- .../SpacesSidebar/hooks/spaceItemUtils.ts | 344 +- .../SpacesSidebar/hooks/useSpaceItemActive.ts | 150 +- .../hooks/useSpaceItemContextMenu.ts | 222 +- .../hooks/useSpaceItemDropZones.ts | 156 +- .../SpacesSidebar/hooks/useSpaces.ts | 151 +- .../src/components/SpacesSidebar/index.tsx | 235 +- .../SyncMonitor/SyncMonitorPopover.tsx | 261 +- .../SyncMonitor/components/ActivityFeed.tsx | 155 +- .../SyncMonitor/components/PeerList.tsx | 24 +- .../SyncMonitor/hooks/useSyncCount.ts | 42 +- .../SyncMonitor/hooks/useSyncMonitor.ts | 334 +- .../src/components/SyncMonitor/index.ts | 8 +- .../src/components/SyncMonitor/types.ts | 37 +- .../src/components/SyncMonitor/utils.ts | 28 +- .../src/components/TabManager/TabBar.tsx | 149 +- .../components/TabManager/TabDefaultsSync.tsx | 48 +- .../TabManager/TabKeyboardHandler.tsx | 79 +- .../TabManager/TabManagerContext.tsx | 563 ++- .../TabManager/TabNavigationSync.tsx | 162 +- .../src/components/TabManager/TabView.tsx | 20 +- .../src/components/TabManager/index.ts | 20 +- .../components/TabManager/useTabManager.ts | 12 +- .../interface/src/components/Tags/TagDot.tsx | 36 +- .../interface/src/components/Tags/TagPill.tsx | 104 +- .../src/components/Tags/TagSelector.tsx | 461 +- .../interface/src/components/Tags/index.tsx | 6 +- packages/interface/src/components/index.ts | 8 +- .../components/modals/CreateLibraryModal.tsx | 589 ++- .../components/modals/FileOperationModal.tsx | 755 +-- .../src/components/modals/PairingModal.tsx | 371 +- .../src/components/modals/SyncSetupModal.tsx | 108 +- .../overlays/DaemonDisconnectedOverlay.tsx | 377 +- .../overlays/DaemonStartupOverlay.tsx | 174 +- .../src/contexts/PlatformContext.tsx | 296 +- .../interface/src/contexts/ServerContext.tsx | 257 +- .../src/contexts/SpacedriveContext.tsx | 35 +- packages/interface/src/hooks/useClipboard.ts | 110 +- .../interface/src/hooks/useContextMenu.ts | 204 +- .../interface/src/hooks/useDaemonStatus.ts | 361 +- packages/interface/src/hooks/useEvent.ts | 40 +- .../interface/src/hooks/useJobDispatch.ts | 62 +- packages/interface/src/hooks/useKeybind.ts | 129 +- .../interface/src/hooks/useKeybindMeta.ts | 66 +- .../interface/src/hooks/useKeybindScope.ts | 28 +- packages/interface/src/hooks/useLibraries.ts | 16 +- packages/interface/src/hooks/useOpenWith.ts | 148 +- packages/interface/src/index.tsx | 91 +- packages/interface/src/router.tsx | 120 +- .../interface/src/routes/daemon/index.tsx | 477 +- .../src/routes/explorer/ExplorerView.tsx | 545 +-- .../src/routes/explorer/File/File.tsx | 106 +- .../src/routes/explorer/File/FileStack.tsx | 58 +- .../src/routes/explorer/File/Metadata.tsx | 16 +- .../src/routes/explorer/File/Thumb.tsx | 58 +- .../explorer/File/ThumbstripScrubber.tsx | 327 +- .../src/routes/explorer/File/Title.tsx | 21 +- .../src/routes/explorer/File/index.ts | 4 +- .../src/routes/explorer/KeyboardHandler.tsx | 2 +- .../src/routes/explorer/SearchToolbar.tsx | 160 +- .../src/routes/explorer/SelectionContext.tsx | 510 +- .../interface/src/routes/explorer/Sidebar.tsx | 88 +- .../src/routes/explorer/SortMenu.tsx | 93 +- .../routes/explorer/TabNavigationGuard.tsx | 58 +- .../src/routes/explorer/TagAssignmentMode.tsx | 310 +- .../src/routes/explorer/ViewModeMenu.tsx | 369 +- .../src/routes/explorer/ViewSettings.tsx | 283 +- .../explorer/components/AddLocationModal.tsx | 147 +- .../explorer/components/AddStorageModal.tsx | 2861 ++++++----- .../routes/explorer/components/Breadcrumb.tsx | 14 +- .../components/DragSelect/context.tsx | 46 +- .../components/DragSelect/useDragSelection.ts | 98 +- .../explorer/components/DragSelect/utils.ts | 16 +- .../components/ExpandableSearchButton.tsx | 205 +- .../explorer/components/InlineNameEdit.tsx | 188 +- .../explorer/components/LocationsSection.tsx | 40 +- .../routes/explorer/components/PathBar.tsx | 923 ++-- .../routes/explorer/components/Section.tsx | 4 +- .../explorer/components/SidebarItem.tsx | 14 +- .../explorer/components/VirtualPathBar.tsx | 82 +- .../explorer/components/VolumeSizeBar.tsx | 48 +- .../interface/src/routes/explorer/context.tsx | 1270 +++-- .../routes/explorer/hooks/useDraggableFile.ts | 92 +- .../hooks/useEmptySpaceContextMenu.ts | 143 +- .../explorer/hooks/useExplorerKeyboard.ts | 454 +- .../explorer/hooks/useFileContextMenu.ts | 1082 ++-- .../explorer/hooks/useTypeaheadSearch.ts | 128 +- .../explorer/hooks/useVirtualListing.ts | 190 +- .../interface/src/routes/explorer/index.ts | 20 +- .../interface/src/routes/explorer/utils.ts | 96 +- .../src/routes/explorer/utils/virtualFiles.ts | 200 +- .../explorer/views/ColumnView/Column.tsx | 367 +- .../explorer/views/ColumnView/ColumnItem.tsx | 173 +- .../explorer/views/ColumnView/ColumnView.tsx | 839 ++-- .../explorer/views/ColumnView/DragSelect.tsx | 266 +- .../routes/explorer/views/ColumnView/index.ts | 2 +- .../src/routes/explorer/views/EmptyView.tsx | 4 +- .../explorer/views/GridView/DragSelect.tsx | 279 +- .../explorer/views/GridView/FileCard.tsx | 444 +- .../explorer/views/GridView/GridView.tsx | 590 ++- .../routes/explorer/views/GridView/index.ts | 2 +- .../routes/explorer/views/KnowledgeView.tsx | 735 ++- .../explorer/views/ListView/DragSelect.tsx | 266 +- .../explorer/views/ListView/ListView.tsx | 543 +-- .../explorer/views/ListView/TableRow.tsx | 424 +- .../routes/explorer/views/ListView/index.ts | 2 +- .../explorer/views/ListView/useTable.tsx | 15 +- .../explorer/views/MediaView/DateHeader.tsx | 18 +- .../explorer/views/MediaView/MediaView.tsx | 739 ++- .../views/MediaView/MediaViewItem.tsx | 197 +- .../routes/explorer/views/MediaView/index.ts | 4 +- .../routes/explorer/views/MediaView/utils.ts | 60 +- .../explorer/views/SearchView/SearchView.tsx | 619 ++- .../routes/explorer/views/SearchView/index.ts | 2 +- .../explorer/views/SizeView/SizeCircle.tsx | 346 +- .../explorer/views/SizeView/SizeView.tsx | 1233 +++-- .../routes/explorer/views/SizeView/index.ts | 2 +- .../interface/src/routes/file-kinds/index.tsx | 267 +- .../src/routes/overview/ContentBreakdown.tsx | 262 +- .../src/routes/overview/DeviceJobActivity.tsx | 50 +- .../src/routes/overview/DevicePanel.tsx | 1278 +++-- .../src/routes/overview/HeroStats.tsx | 231 +- .../src/routes/overview/OverviewTopBar.tsx | 569 ++- .../src/routes/overview/ProjectCards.tsx | 365 +- .../interface/src/routes/overview/index.tsx | 193 +- .../interface/src/routes/overview/mockData.ts | 352 +- .../interface/src/routes/settings/index.tsx | 89 +- packages/interface/src/routes/tag/index.tsx | 43 +- packages/interface/src/styles.css | 172 +- packages/interface/src/util/keybinds/index.ts | 70 +- .../interface/src/util/keybinds/listener.ts | 257 +- .../interface/src/util/keybinds/platform.ts | 244 +- .../interface/src/util/keybinds/registry.ts | 769 ++- packages/interface/src/util/keybinds/types.ts | 128 +- packages/interface/src/windows/DemoWindow.tsx | 436 +- .../src/windows/FloatingControls.tsx | 16 +- packages/interface/src/windows/Spacedrop.tsx | 318 +- packages/interface/tsconfig.json | 35 +- packages/ts-client/jest.config.js | 43 +- packages/ts-client/package.json | 154 +- packages/ts-client/src/__tests__/setup.ts | 9 +- packages/ts-client/src/client.ts | 446 +- packages/ts-client/src/deviceIcons.ts | 164 +- packages/ts-client/src/event-filter.ts | 116 +- .../src/hooks/__tests__/eventReplay.ts | 177 +- .../ts-client/src/hooks/__tests__/setup.ts | 7 +- .../__tests__/useNormalizedQuery.test.tsx | 360 +- packages/ts-client/src/hooks/index.ts | 15 +- packages/ts-client/src/hooks/useClient.tsx | 61 +- packages/ts-client/src/hooks/useMutation.ts | 114 +- .../ts-client/src/hooks/useNormalizedQuery.ts | 1124 +++-- packages/ts-client/src/hooks/useQuery.ts | 76 +- packages/ts-client/src/index.ts | 39 +- packages/ts-client/src/stores/sidebar.ts | 96 +- .../ts-client/src/stores/sortPreferences.ts | 46 +- .../ts-client/src/stores/syncPreferences.ts | 34 +- .../ts-client/src/stores/viewPreferences.ts | 91 +- packages/ts-client/src/subscriptionManager.ts | 290 +- packages/ts-client/src/transport.ts | 467 +- packages/ts-client/src/types.ts | 1237 +++-- packages/ts-client/src/volumeIcons.ts | 109 +- .../tests/integration/search.test.ts | 604 +-- packages/ts-client/tests/integration/setup.ts | 22 +- .../useNormalizedQuery.bulk-moves.test.ts | 705 ++- .../useNormalizedQuery.file-delete.test.ts | 344 +- .../useNormalizedQuery.folder-rename.test.ts | 340 +- .../integration/useNormalizedQuery.test.ts | 370 +- packages/ts-client/tsconfig.json | 48 +- packages/ui/.eslintrc.js | 173 +- packages/ui/package.json | 172 +- packages/ui/postcss.config.js | 2 +- packages/ui/src/Button.stories.tsx | 137 +- packages/ui/src/Button.tsx | 194 +- packages/ui/src/CheckBox.tsx | 88 +- packages/ui/src/CircularProgress.tsx | 265 +- packages/ui/src/ContextMenu.tsx | 370 +- packages/ui/src/Dialog.tsx | 176 +- packages/ui/src/Divider.tsx | 2 +- packages/ui/src/Dropdown.stories.tsx | 20 +- packages/ui/src/Dropdown.tsx | 234 +- packages/ui/src/DropdownMenu.tsx | 163 +- packages/ui/src/InfoBanner.tsx | 8 +- packages/ui/src/Input.stories.tsx | 40 +- packages/ui/src/Input.tsx | 54 +- packages/ui/src/Layout.tsx | 2 +- packages/ui/src/Loader.tsx | 22 +- packages/ui/src/Popover.tsx | 123 +- packages/ui/src/ProgressBar.tsx | 70 +- packages/ui/src/RadioGroup.tsx | 76 +- packages/ui/src/Resizable.tsx | 174 +- packages/ui/src/SearchBar.tsx | 137 +- packages/ui/src/Select.stories.tsx | 54 +- packages/ui/src/Select.tsx | 197 +- packages/ui/src/ShinyButton.tsx | 174 +- packages/ui/src/ShinyToggle.tsx | 78 +- packages/ui/src/Shortcut.tsx | 36 +- packages/ui/src/Slider.tsx | 33 +- packages/ui/src/Switch.stories.tsx | 36 +- packages/ui/src/Switch.tsx | 94 +- packages/ui/src/Tabs.tsx | 6 +- packages/ui/src/Toast.tsx | 656 +-- packages/ui/src/Tooltip.tsx | 137 +- packages/ui/src/TopBarButton.tsx | 67 +- packages/ui/src/TopBarButtonGroup.tsx | 71 +- packages/ui/src/Typography.tsx | 2 +- packages/ui/src/forms/CheckBoxField.tsx | 24 +- packages/ui/src/forms/Form.tsx | 177 +- packages/ui/src/forms/FormField.tsx | 76 +- packages/ui/src/forms/InputField.tsx | 229 +- packages/ui/src/forms/RadioGroupField.tsx | 82 +- packages/ui/src/forms/SelectField.tsx | 44 +- packages/ui/src/forms/SwitchField.tsx | 44 +- packages/ui/src/forms/TextAreaField.tsx | 33 +- packages/ui/src/forms/index.ts | 18 +- packages/ui/src/forms/zxcvbn.ts | 6 +- packages/ui/src/index.ts | 70 +- packages/ui/src/keys.ts | 125 +- packages/ui/src/utils.tsx | 36 +- packages/ui/style/colors.js | 92 +- packages/ui/style/index.js | 16 +- packages/ui/style/postcss.config.js | 2 +- packages/ui/style/tailwind.js | 372 +- packages/ui/tailwind.config.js | 2 +- packages/ui/tsconfig.json | 40 +- packages/ui/tsup.config.ts | 56 +- scripts/.eslintrc.cjs | 139 +- scripts/package.json | 86 +- scripts/tsconfig.json | 62 +- scripts/utils/consts.mjs | 74 +- scripts/utils/fetch.mjs | 238 +- scripts/utils/flock.mjs | 48 +- scripts/utils/machineId.mjs | 108 +- scripts/utils/patchTauri.mjs | 227 +- scripts/utils/rustup.mjs | 10 +- scripts/utils/shared.mjs | 141 +- scripts/utils/spawn.mjs | 44 +- scripts/utils/spinner.mjs | 48 +- scripts/utils/which.mjs | 50 +- tsconfig.json | 27 +- 461 files changed, 45109 insertions(+), 40620 deletions(-) create mode 100644 .claude/CLAUDE.md create mode 100644 .cursor/hooks.json create mode 100644 biome.jsonc diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md new file mode 100644 index 000000000..0a7bb4228 --- /dev/null +++ b/.claude/CLAUDE.md @@ -0,0 +1,123 @@ +# Ultracite Code Standards + +This project uses **Ultracite**, a zero-config preset that enforces strict code quality standards through automated formatting and linting. + +## Quick Reference + +- **Format code**: `bun x ultracite fix` +- **Check for issues**: `bun x ultracite check` +- **Diagnose setup**: `bun x ultracite doctor` + +Biome (the underlying engine) provides robust linting and formatting. Most issues are automatically fixable. + +--- + +## Core Principles + +Write code that is **accessible, performant, type-safe, and maintainable**. Focus on clarity and explicit intent over brevity. + +### Type Safety & Explicitness + +- Use explicit types for function parameters and return values when they enhance clarity +- Prefer `unknown` over `any` when the type is genuinely unknown +- Use const assertions (`as const`) for immutable values and literal types +- Leverage TypeScript's type narrowing instead of type assertions +- Use meaningful variable names instead of magic numbers - extract constants with descriptive names + +### Modern JavaScript/TypeScript + +- Use arrow functions for callbacks and short functions +- Prefer `for...of` loops over `.forEach()` and indexed `for` loops +- Use optional chaining (`?.`) and nullish coalescing (`??`) for safer property access +- Prefer template literals over string concatenation +- Use destructuring for object and array assignments +- Use `const` by default, `let` only when reassignment is needed, never `var` + +### Async & Promises + +- Always `await` promises in async functions - don't forget to use the return value +- Use `async/await` syntax instead of promise chains for better readability +- Handle errors appropriately in async code with try-catch blocks +- Don't use async functions as Promise executors + +### React & JSX + +- Use function components over class components +- Call hooks at the top level only, never conditionally +- Specify all dependencies in hook dependency arrays correctly +- Use the `key` prop for elements in iterables (prefer unique IDs over array indices) +- Nest children between opening and closing tags instead of passing as props +- Don't define components inside other components +- Use semantic HTML and ARIA attributes for accessibility: + - Provide meaningful alt text for images + - Use proper heading hierarchy + - Add labels for form inputs + - Include keyboard event handlers alongside mouse events + - Use semantic elements (` + + - + - + - + - + - + - - - - - - + + + + + + - - - + + + - {/* Cards Section */} - - Cards + {/* Cards Section */} + + Cards - - Default Card - - With subtitle text - - + + Default Card + With subtitle text + - - Accent Card - + + Accent Card + - - Sidebar Card - - + + Sidebar Card + + - {/* Inputs Section */} - - Inputs + {/* Inputs Section */} + + Inputs - - + + - + - + - + - + - - - + + + - {/* Switch Section */} - - Switches + {/* Switch Section */} + + Switches - - - Enabled Switch - - + + + Enabled Switch + + - - Always On - {}} /> - + + Always On + {}} value={true} /> + - - Always Off - {}} /> - - - + + Always Off + {}} value={false} /> + + + - {/* Dividers Section */} - - Dividers + {/* Dividers Section */} + + Dividers - Section One - - Section Two - - Section Three - + Section One + + Section Two + + Section Three + - {/* Typography Section */} - - Typography + {/* Typography Section */} + + Typography - - - Heading 1 - - - Heading 2 - - - Heading 3 - - - Heading 4 - - Body Text - - Secondary Text - - - Label Text - - - + + Heading 1 + Heading 2 + Heading 3 + Heading 4 + Body Text + Secondary Text + + Label Text + + + - {/* Colors Section */} - - Color System + {/* Colors Section */} + + Color System - - {/* Accent Colors */} - - - Accent - - - - - DEFAULT - - - - faint - - - - deep - - - + + {/* Accent Colors */} + + Accent + + + + DEFAULT + + + + faint + + + + deep + + + - {/* Ink Colors */} - - - Ink (Text) - - - - - DEFAULT - - - - dull - - - - faint - - - + {/* Ink Colors */} + + + Ink (Text) + + + + + DEFAULT + + + + dull + + + + faint + + + - {/* Sidebar Colors */} - - - Sidebar - - - - - DEFAULT - - - - box - - - - line - - - - ink - - - - inkDull - - - - inkFaint - - - - divider - - - - button - - - - selected - - - + {/* Sidebar Colors */} + + + Sidebar + + + + + DEFAULT + + + + box + + + + line + + + + ink + + + + inkDull + + + + inkFaint + + + + divider + + + + button + + + + selected + + + - {/* App Colors */} - - - App - - - - - DEFAULT - - - - box - - - - darkBox - - - - overlay - - - - line - - - - frame - - - - button - - - - hover - - - - selected - - - + {/* App Colors */} + + App + + + + DEFAULT + + + + box + + + + darkBox + + + + overlay + + + + line + + + + frame + + + + button + + + + hover + + + + selected + + + - {/* Menu Colors */} - - - Menu - - - - - DEFAULT - - - - line - - - - hover - - - - selected - - - - shade - - - - ink - - - - faint - - - - - + {/* Menu Colors */} + + Menu + + + + DEFAULT + + + + line + + + + hover + + + + selected + + + + shade + + + + ink + + + + faint + + + + + - {/* Spacing Section */} - - Spacing Scale + {/* Spacing Section */} + + Spacing Scale - - - - 4px (1) - - - - 8px (2) - - - - 12px (3) - - - - 16px (4) - - - - 24px (6) - - - - 32px (8) - - - + + + + 4px (1) + + + + 8px (2) + + + + 12px (3) + + + + 16px (4) + + + + 24px (6) + + + + 32px (8) + + + - {/* Border Radius Section */} - - - Border Radius - + {/* Border Radius Section */} + + Border Radius - - - - Small (2px) - - - - Medium (6px) - - - - Large (8px) - - - - XL (12px) - - - - Full (9999px) - - - + + + + Small (2px) + + + + Medium (6px) + + + + Large (8px) + + + + XL (12px) + + + + Full (9999px) + + + - {/* Interactive Demo */} - - - Interactive Demo - + {/* Interactive Demo */} + + Interactive Demo - - - Pressable Card - - Tap to see active state - - + + + Pressable Card + + Tap to see active state + + - - - Accent Pressable - - - - + + Accent Pressable + + + - {/* Settings Primitives Section */} - - - iOS Settings Style - + {/* Settings Primitives Section */} + + iOS Settings Style - - } - label="Profile" - description="View and edit your profile" - onPress={() => console.log("Profile")} - /> - } - label="Security" - onPress={() => console.log("Security")} - /> - } - label="Notifications" - description="Push notifications for this library" - value={notificationsEnabled} - onValueChange={setNotificationsEnabled} - /> - + + } + label="Profile" + onPress={() => console.log("Profile")} + /> + } + label="Security" + onPress={() => console.log("Security")} + /> + } + label="Notifications" + onValueChange={setNotificationsEnabled} + value={notificationsEnabled} + /> + - - } - label="Dark Mode" - value={darkModeEnabled} - onValueChange={setDarkModeEnabled} - /> - } - label="Theme" - value="System" - onPress={() => console.log("Theme picker")} - /> - + + } + label="Dark Mode" + onValueChange={setDarkModeEnabled} + value={darkModeEnabled} + /> + } + label="Theme" + onPress={() => console.log("Theme picker")} + value="System" + /> + - - } - label="Cache Size" - description="Maximum cache size in GB" - value={sliderValue} - minimumValue={10} - maximumValue={100} - onValueChange={setSliderValue} - /> - } - label="Clear Cache" - onPress={() => console.log("Clear cache")} - /> - } - label="Reset All Data" - description="Permanently delete all libraries and settings" - onPress={handleResetData} - /> - - + + } + label="Cache Size" + maximumValue={100} + minimumValue={10} + onValueChange={setSliderValue} + value={sliderValue} + /> + } + label="Clear Cache" + onPress={() => console.log("Clear cache")} + /> + } + label="Reset All Data" + onPress={handleResetData} + /> + + - {/* Footer */} - - - Spacedrive Mobile v2 - - - UI Primitives Showcase - - - - ); + {/* Footer */} + + Spacedrive Mobile v2 + + UI Primitives Showcase + + + + ); } diff --git a/apps/mobile/src/stores/explorer.ts b/apps/mobile/src/stores/explorer.ts index 346ed01f8..2092fe5c9 100644 --- a/apps/mobile/src/stores/explorer.ts +++ b/apps/mobile/src/stores/explorer.ts @@ -2,100 +2,100 @@ import { create } from "zustand"; export type LayoutMode = "grid" | "list" | "media"; export type SortBy = - | "name" - | "size" - | "date_created" - | "date_modified" - | "kind"; + | "name" + | "size" + | "date_created" + | "date_modified" + | "kind"; export type SortOrder = "asc" | "desc"; interface ExplorerStore { - // View mode - layoutMode: LayoutMode; - setLayoutMode: (mode: LayoutMode) => void; + // View mode + layoutMode: LayoutMode; + setLayoutMode: (mode: LayoutMode) => void; - // Grid configuration - gridColumns: number; - setGridColumns: (columns: number) => void; + // Grid configuration + gridColumns: number; + setGridColumns: (columns: number) => void; - // Sorting - sortBy: SortBy; - sortOrder: SortOrder; - setSortBy: (sort: SortBy) => void; - setSortOrder: (order: SortOrder) => void; + // Sorting + sortBy: SortBy; + sortOrder: SortOrder; + setSortBy: (sort: SortBy) => void; + setSortOrder: (order: SortOrder) => void; - // Selection - selectedItems: Set; - isSelectionMode: boolean; - selectItem: (id: string) => void; - deselectItem: (id: string) => void; - toggleItem: (id: string) => void; - clearSelection: () => void; - setSelectionMode: (enabled: boolean) => void; + // Selection + selectedItems: Set; + isSelectionMode: boolean; + selectItem: (id: string) => void; + deselectItem: (id: string) => void; + toggleItem: (id: string) => void; + clearSelection: () => void; + setSelectionMode: (enabled: boolean) => void; - // Current path - currentPath: string; - setCurrentPath: (path: string) => void; + // Current path + currentPath: string; + setCurrentPath: (path: string) => void; - // Current location - currentLocationId: string | null; - setCurrentLocation: (id: string | null) => void; + // Current location + currentLocationId: string | null; + setCurrentLocation: (id: string | null) => void; } export const useExplorerStore = create((set, get) => ({ - // View mode - layoutMode: "grid", - setLayoutMode: (mode) => set({ layoutMode: mode }), + // View mode + layoutMode: "grid", + setLayoutMode: (mode) => set({ layoutMode: mode }), - // Grid configuration - gridColumns: 3, - setGridColumns: (columns) => set({ gridColumns: columns }), + // Grid configuration + gridColumns: 3, + setGridColumns: (columns) => set({ gridColumns: columns }), - // Sorting - sortBy: "name", - sortOrder: "asc", - setSortBy: (sort) => set({ sortBy: sort }), - setSortOrder: (order) => set({ sortOrder: order }), + // Sorting + sortBy: "name", + sortOrder: "asc", + setSortBy: (sort) => set({ sortBy: sort }), + setSortOrder: (order) => set({ sortOrder: order }), - // Selection - selectedItems: new Set(), - isSelectionMode: false, - selectItem: (id) => - set((state) => ({ - selectedItems: new Set([...state.selectedItems, id]), - })), - deselectItem: (id) => - set((state) => { - const newSet = new Set(state.selectedItems); - newSet.delete(id); - return { selectedItems: newSet }; - }), - toggleItem: (id) => - set((state) => { - const newSet = new Set(state.selectedItems); - if (newSet.has(id)) { - newSet.delete(id); - } else { - newSet.add(id); - } - return { - selectedItems: newSet, - isSelectionMode: newSet.size > 0, - }; - }), - clearSelection: () => - set({ selectedItems: new Set(), isSelectionMode: false }), - setSelectionMode: (enabled) => - set({ - isSelectionMode: enabled, - selectedItems: enabled ? get().selectedItems : new Set(), - }), + // Selection + selectedItems: new Set(), + isSelectionMode: false, + selectItem: (id) => + set((state) => ({ + selectedItems: new Set([...state.selectedItems, id]), + })), + deselectItem: (id) => + set((state) => { + const newSet = new Set(state.selectedItems); + newSet.delete(id); + return { selectedItems: newSet }; + }), + toggleItem: (id) => + set((state) => { + const newSet = new Set(state.selectedItems); + if (newSet.has(id)) { + newSet.delete(id); + } else { + newSet.add(id); + } + return { + selectedItems: newSet, + isSelectionMode: newSet.size > 0, + }; + }), + clearSelection: () => + set({ selectedItems: new Set(), isSelectionMode: false }), + setSelectionMode: (enabled) => + set({ + isSelectionMode: enabled, + selectedItems: enabled ? get().selectedItems : new Set(), + }), - // Current path - currentPath: "/", - setCurrentPath: (path) => set({ currentPath: path }), + // Current path + currentPath: "/", + setCurrentPath: (path) => set({ currentPath: path }), - // Current location - currentLocationId: null, - setCurrentLocation: (id) => set({ currentLocationId: id }), + // Current location + currentLocationId: null, + setCurrentLocation: (id) => set({ currentLocationId: id }), })); diff --git a/apps/mobile/src/stores/index.ts b/apps/mobile/src/stores/index.ts index cf8d5b9be..d91ffb3c9 100644 --- a/apps/mobile/src/stores/index.ts +++ b/apps/mobile/src/stores/index.ts @@ -1,8 +1,8 @@ -export { useSidebarStore } from "./sidebar"; export { - useExplorerStore, - type LayoutMode, - type SortBy, - type SortOrder, + type LayoutMode, + type SortBy, + type SortOrder, + useExplorerStore, } from "./explorer"; -export { usePreferencesStore, type ThemeMode } from "./preferences"; +export { type ThemeMode, usePreferencesStore } from "./preferences"; +export { useSidebarStore } from "./sidebar"; diff --git a/apps/mobile/src/stores/preferences.ts b/apps/mobile/src/stores/preferences.ts index d53f59fb7..20e127bfa 100644 --- a/apps/mobile/src/stores/preferences.ts +++ b/apps/mobile/src/stores/preferences.ts @@ -1,97 +1,100 @@ -import { create } from "zustand"; -import { persist, createJSONStorage, StateStorage } from "zustand/middleware"; import AsyncStorage from "@react-native-async-storage/async-storage"; +import { create } from "zustand"; +import { + createJSONStorage, + persist, + type StateStorage, +} from "zustand/middleware"; // AsyncStorage adapter for Zustand const asyncStorageAdapter: StateStorage = { - getItem: async (name: string) => { - return await AsyncStorage.getItem(name); - }, - setItem: async (name: string, value: string) => { - await AsyncStorage.setItem(name, value); - }, - removeItem: async (name: string) => { - await AsyncStorage.removeItem(name); - }, + getItem: async (name: string) => { + return await AsyncStorage.getItem(name); + }, + setItem: async (name: string, value: string) => { + await AsyncStorage.setItem(name, value); + }, + removeItem: async (name: string) => { + await AsyncStorage.removeItem(name); + }, }; export type ThemeMode = "dark" | "light" | "system"; interface ViewPreferences { - viewMode: "grid" | "list" | "media"; - gridSize: number; - showHiddenFiles: boolean; + viewMode: "grid" | "list" | "media"; + gridSize: number; + showHiddenFiles: boolean; } interface PreferencesStore { - // Theme - themeMode: ThemeMode; - setThemeMode: (mode: ThemeMode) => void; + // Theme + themeMode: ThemeMode; + setThemeMode: (mode: ThemeMode) => void; - // Haptics - hapticsEnabled: boolean; - setHapticsEnabled: (enabled: boolean) => void; + // Haptics + hapticsEnabled: boolean; + setHapticsEnabled: (enabled: boolean) => void; - // View preferences per location/path - viewPreferences: Record; - getViewPreferences: (key: string) => ViewPreferences; - setViewPreferences: (key: string, prefs: Partial) => void; + // View preferences per location/path + viewPreferences: Record; + getViewPreferences: (key: string) => ViewPreferences; + setViewPreferences: (key: string, prefs: Partial) => void; - // Onboarding - hasCompletedOnboarding: boolean; - setHasCompletedOnboarding: (completed: boolean) => void; + // Onboarding + hasCompletedOnboarding: boolean; + setHasCompletedOnboarding: (completed: boolean) => void; - // Sync preferences - autoSwitchOnSync: boolean; - setAutoSwitchOnSync: (enabled: boolean) => void; + // Sync preferences + autoSwitchOnSync: boolean; + setAutoSwitchOnSync: (enabled: boolean) => void; } const defaultViewPreferences: ViewPreferences = { - viewMode: "grid", - gridSize: 120, - showHiddenFiles: false, + viewMode: "grid", + gridSize: 120, + showHiddenFiles: false, }; export const usePreferencesStore = create()( - persist( - (set, get) => ({ - // Theme - themeMode: "dark", - setThemeMode: (mode) => set({ themeMode: mode }), + persist( + (set, get) => ({ + // Theme + themeMode: "dark", + setThemeMode: (mode) => set({ themeMode: mode }), - // Haptics - hapticsEnabled: true, - setHapticsEnabled: (enabled) => set({ hapticsEnabled: enabled }), + // Haptics + hapticsEnabled: true, + setHapticsEnabled: (enabled) => set({ hapticsEnabled: enabled }), - // View preferences - viewPreferences: {}, - getViewPreferences: (key) => { - return get().viewPreferences[key] ?? defaultViewPreferences; - }, - setViewPreferences: (key, prefs) => - set((state) => ({ - viewPreferences: { - ...state.viewPreferences, - [key]: { - ...(state.viewPreferences[key] ?? - defaultViewPreferences), - ...prefs, - }, - }, - })), + // View preferences + viewPreferences: {}, + getViewPreferences: (key) => { + return get().viewPreferences[key] ?? defaultViewPreferences; + }, + setViewPreferences: (key, prefs) => + set((state) => ({ + viewPreferences: { + ...state.viewPreferences, + [key]: { + ...(state.viewPreferences[key] ?? defaultViewPreferences), + ...prefs, + }, + }, + })), - // Onboarding - hasCompletedOnboarding: false, - setHasCompletedOnboarding: (completed) => - set({ hasCompletedOnboarding: completed }), + // Onboarding + hasCompletedOnboarding: false, + setHasCompletedOnboarding: (completed) => + set({ hasCompletedOnboarding: completed }), - // Sync preferences - autoSwitchOnSync: true, - setAutoSwitchOnSync: (enabled) => set({ autoSwitchOnSync: enabled }), - }), - { - name: "spacedrive-preferences", - storage: createJSONStorage(() => asyncStorageAdapter), - }, - ), + // Sync preferences + autoSwitchOnSync: true, + setAutoSwitchOnSync: (enabled) => set({ autoSwitchOnSync: enabled }), + }), + { + name: "spacedrive-preferences", + storage: createJSONStorage(() => asyncStorageAdapter), + } + ) ); diff --git a/apps/mobile/src/stores/sidebar.ts b/apps/mobile/src/stores/sidebar.ts index 851d25607..9645031dc 100644 --- a/apps/mobile/src/stores/sidebar.ts +++ b/apps/mobile/src/stores/sidebar.ts @@ -1,66 +1,67 @@ -import { create } from "zustand"; -import { persist, createJSONStorage, StateStorage } from "zustand/middleware"; import AsyncStorage from "@react-native-async-storage/async-storage"; +import { create } from "zustand"; +import { + createJSONStorage, + persist, + type StateStorage, +} from "zustand/middleware"; // AsyncStorage adapter for Zustand const asyncStorageAdapter: StateStorage = { - getItem: async (name: string) => { - return await AsyncStorage.getItem(name); - }, - setItem: async (name: string, value: string) => { - await AsyncStorage.setItem(name, value); - }, - removeItem: async (name: string) => { - await AsyncStorage.removeItem(name); - }, + getItem: async (name: string) => { + return await AsyncStorage.getItem(name); + }, + setItem: async (name: string, value: string) => { + await AsyncStorage.setItem(name, value); + }, + removeItem: async (name: string) => { + await AsyncStorage.removeItem(name); + }, }; interface SidebarStore { - // Current library selection - currentLibraryId: string | null; - setCurrentLibrary: (id: string | null) => void; + // Current library selection + currentLibraryId: string | null; + setCurrentLibrary: (id: string | null) => void; - // Collapsed section groups - collapsedGroups: string[]; - isGroupCollapsed: (groupId: string) => boolean; - toggleGroup: (groupId: string) => void; + // Collapsed section groups + collapsedGroups: string[]; + isGroupCollapsed: (groupId: string) => boolean; + toggleGroup: (groupId: string) => void; - // Drawer state - isDrawerOpen: boolean; - setDrawerOpen: (open: boolean) => void; + // Drawer state + isDrawerOpen: boolean; + setDrawerOpen: (open: boolean) => void; } export const useSidebarStore = create()( - persist( - (set, get) => ({ - currentLibraryId: null, - setCurrentLibrary: (id) => set({ currentLibraryId: id }), + persist( + (set, get) => ({ + currentLibraryId: null, + setCurrentLibrary: (id) => set({ currentLibraryId: id }), - collapsedGroups: [], - isGroupCollapsed: (groupId) => - get().collapsedGroups.includes(groupId), - toggleGroup: (groupId) => - set((state) => { - const isCollapsed = state.collapsedGroups.includes(groupId); - return { - collapsedGroups: isCollapsed - ? state.collapsedGroups.filter( - (id) => id !== groupId, - ) - : [...state.collapsedGroups, groupId], - }; - }), + collapsedGroups: [], + isGroupCollapsed: (groupId) => get().collapsedGroups.includes(groupId), + toggleGroup: (groupId) => + set((state) => { + const isCollapsed = state.collapsedGroups.includes(groupId); + return { + collapsedGroups: isCollapsed + ? state.collapsedGroups.filter((id) => id !== groupId) + : [...state.collapsedGroups, groupId], + }; + }), - isDrawerOpen: false, - setDrawerOpen: (open) => set({ isDrawerOpen: open }), - }), - { - name: "spacedrive-sidebar", - storage: createJSONStorage(() => asyncStorageAdapter), - partialize: (state) => ({ - currentLibraryId: state.currentLibraryId, - collapsedGroups: state.collapsedGroups, - }), - }, - ), + isDrawerOpen: false, + setDrawerOpen: (open) => set({ isDrawerOpen: open }), + }), + { + name: "spacedrive-sidebar", + storage: createJSONStorage(() => asyncStorageAdapter), + partialize: (state) => ({ + currentLibraryId: state.currentLibraryId, + collapsedGroups: state.collapsedGroups, + }), + } + ) ); diff --git a/apps/mobile/src/utils/cn.ts b/apps/mobile/src/utils/cn.ts index 662a3570e..ca0cbf7ad 100644 --- a/apps/mobile/src/utils/cn.ts +++ b/apps/mobile/src/utils/cn.ts @@ -1,9 +1,9 @@ -import { clsx, type ClassValue } from "clsx"; +import { type ClassValue, clsx } from "clsx"; /** * Utility function for combining class names. * Similar to clsx but optimized for NativeWind. */ export function cn(...inputs: ClassValue[]) { - return clsx(inputs); + return clsx(inputs); } diff --git a/apps/mobile/tailwind.config.js b/apps/mobile/tailwind.config.js index 83e960922..aa46ca039 100644 --- a/apps/mobile/tailwind.config.js +++ b/apps/mobile/tailwind.config.js @@ -1,4 +1,4 @@ -const sharedColors = require('@sd/ui/style/colors'); +const sharedColors = require("@sd/ui/style/colors"); /** * Convert shared color format (HSL string) to NativeWind format (hsl() function) @@ -8,41 +8,42 @@ const sharedColors = require('@sd/ui/style/colors'); * Also converts camelCase keys to kebab-case for NativeWind compatibility */ function toHSL(colorValue) { - if (typeof colorValue === 'string') { - return `hsl(${colorValue})`; - } + if (typeof colorValue === "string") { + return `hsl(${colorValue})`; + } - // Handle nested objects (like accent.DEFAULT) - const result = {}; - for (const [key, value] of Object.entries(colorValue)) { - // Preserve DEFAULT (must be uppercase for Tailwind) - // Convert camelCase to kebab-case for everything else - const kebabKey = key === 'DEFAULT' - ? key - : key.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase(); - result[kebabKey] = toHSL(value); - } - return result; + // Handle nested objects (like accent.DEFAULT) + const result = {}; + for (const [key, value] of Object.entries(colorValue)) { + // Preserve DEFAULT (must be uppercase for Tailwind) + // Convert camelCase to kebab-case for everything else + const kebabKey = + key === "DEFAULT" + ? key + : key.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase(); + result[kebabKey] = toHSL(value); + } + return result; } /** @type {import('tailwindcss').Config} */ module.exports = { - content: ["./src/**/*.{ts,tsx}", "./index.js"], - presets: [require("nativewind/preset")], - theme: { - extend: { - colors: { - // Use shared colors from @sd/ui - accent: toHSL(sharedColors.accent), - ink: toHSL(sharedColors.ink), - sidebar: toHSL(sharedColors.sidebar), - app: toHSL(sharedColors.app), - menu: toHSL(sharedColors.menu), - }, - fontSize: { - md: "16px", - }, - }, - }, - plugins: [], + content: ["./src/**/*.{ts,tsx}", "./index.js"], + presets: [require("nativewind/preset")], + theme: { + extend: { + colors: { + // Use shared colors from @sd/ui + accent: toHSL(sharedColors.accent), + ink: toHSL(sharedColors.ink), + sidebar: toHSL(sharedColors.sidebar), + app: toHSL(sharedColors.app), + menu: toHSL(sharedColors.menu), + }, + fontSize: { + md: "16px", + }, + }, + }, + plugins: [], }; diff --git a/apps/mobile/tsconfig.json b/apps/mobile/tsconfig.json index 46b391c2f..745be6a8b 100644 --- a/apps/mobile/tsconfig.json +++ b/apps/mobile/tsconfig.json @@ -3,9 +3,7 @@ "compilerOptions": { "target": "ES2020", "module": "ESNext", - "lib": [ - "ES2020" - ], + "lib": ["ES2020"], "jsx": "react-native", "strict": true, "moduleResolution": "bundler", @@ -13,9 +11,7 @@ "esModuleInterop": true, "skipLibCheck": true, "paths": { - "~/*": [ - "./src/*" - ] + "~/*": ["./src/*"] } }, "include": [ @@ -25,7 +21,5 @@ ".expo/types/**/*.ts", "expo-env.d.ts" ], - "exclude": [ - "node_modules" - ] + "exclude": ["node_modules"] } diff --git a/apps/tauri/Spacedrive.icon/icon.json b/apps/tauri/Spacedrive.icon/icon.json index 86ee04bd4..d51f2f324 100644 --- a/apps/tauri/Spacedrive.icon/icon.json +++ b/apps/tauri/Spacedrive.icon/icon.json @@ -1,98 +1,90 @@ { - "fill-specializations" : [ + "fill-specializations": [ { - "value" : "automatic" + "value": "automatic" }, { - "appearance" : "dark", - "value" : "system-dark" + "appearance": "dark", + "value": "system-dark" } ], - "groups" : [ + "groups": [ { - "layers" : [ + "layers": [ { - "blend-mode-specializations" : [ + "blend-mode-specializations": [ { - "appearance" : "tinted", - "value" : "screen" + "appearance": "tinted", + "value": "screen" } ], - "fill-specializations" : [ + "fill-specializations": [ { - "appearance" : "tinted", - "value" : { - "solid" : "display-p3:1.00000,0.72781,0.41766,1.00000" + "appearance": "tinted", + "value": { + "solid": "display-p3:1.00000,0.72781,0.41766,1.00000" } } ], - "glass" : true, - "hidden" : false, - "image-name" : "Ball.png", - "name" : "Ball", - "opacity-specializations" : [ + "glass": true, + "hidden": false, + "image-name": "Ball.png", + "name": "Ball", + "opacity-specializations": [ { - "value" : 0.4 + "value": 0.4 }, { - "appearance" : "dark", - "value" : 0 + "appearance": "dark", + "value": 0 }, { - "appearance" : "tinted", - "value" : 0.53 + "appearance": "tinted", + "value": 0.53 } ], - "position" : { - "scale" : 2, - "translation-in-points" : [ - 1.7218333746113785, - 2.7640092574830533 - ] + "position": { + "scale": 2, + "translation-in-points": [1.7218333746113785, 2.7640092574830533] } }, { - "blend-mode-specializations" : [ + "blend-mode-specializations": [ { - "appearance" : "tinted", - "value" : "normal" + "appearance": "tinted", + "value": "normal" } ], - "fill-specializations" : [ + "fill-specializations": [ { - "appearance" : "tinted", - "value" : { - "solid" : "display-p3:1.00000,0.72781,0.41766,1.00000" + "appearance": "tinted", + "value": { + "solid": "display-p3:1.00000,0.72781,0.41766,1.00000" } } ], - "glass" : true, - "hidden" : false, - "image-name" : "Ball.png", - "name" : "Ball", - "position" : { - "scale" : 2, - "translation-in-points" : [ - 1.7218333746113785, - 2.7640092574830533 - ] + "glass": true, + "hidden": false, + "image-name": "Ball.png", + "name": "Ball", + "position": { + "scale": 2, + "translation-in-points": [1.7218333746113785, 2.7640092574830533] } } ], - "shadow" : { - "kind" : "neutral", - "opacity" : 0.5 + "shadow": { + "kind": "neutral", + "opacity": 0.5 }, - "translucency" : { - "enabled" : true, - "value" : 0.5 + "translucency": { + "enabled": true, + "value": 0.5 } } ], - "supported-platforms" : { - "circles" : [ - "watchOS" - ], - "squares" : "shared" + "supported-platforms": { + "circles": ["watchOS"], + "squares": "shared" } -} \ No newline at end of file +} diff --git a/apps/tauri/index.html b/apps/tauri/index.html index a8e400e0c..38d38247a 100644 --- a/apps/tauri/index.html +++ b/apps/tauri/index.html @@ -1,12 +1,12 @@ - - - - Spacedrive - - -
- - + + + + Spacedrive + + +
+ + diff --git a/apps/tauri/package.json b/apps/tauri/package.json index d13fae5a1..94c4c0723 100644 --- a/apps/tauri/package.json +++ b/apps/tauri/package.json @@ -1,47 +1,47 @@ { - "name": "@sd/tauri", - "private": true, - "version": "2.0.0", - "type": "module", - "engines": { - "bun": ">=1.3.0" - }, - "scripts": { - "dev": "vite dev", - "dev:with-daemon": "bun ./scripts/dev-with-daemon.ts", - "build": "vite build", - "build:daemon": "cargo build --bin sd-daemon --manifest-path ../../Cargo.toml", - "build:daemon:release": "cargo build --release --bin sd-daemon --manifest-path ../../Cargo.toml", - "preview": "vite preview", - "typecheck": "tsc -b", - "tauri": "bunx tauri", - "tauri:dev": "bunx tauri dev", - "tauri:dev:no-watch": "bunx tauri dev --no-watch", - "tauri:build": "bunx tauri build && ./scripts/fix-daemon-entitlements.sh ../../target/release/bundle/macos/Spacedrive.app || true" - }, - "dependencies": { - "@phosphor-icons/react": "^2.1.0", - "@sd/assets": "workspace:*", - "@sd/interface": "workspace:*", - "@sd/ts-client": "workspace:*", - "@sd/ui": "workspace:*", - "@tauri-apps/api": "^2.1.1", - "@tauri-apps/plugin-dialog": "^2.4.2", - "@tauri-apps/plugin-fs": "^2.0.1", - "@tauri-apps/plugin-shell": "^2.0.1", - "react": "^19.0.0", - "react-dom": "^19.0.0", - "react-scan": "^0.4.3" - }, - "devDependencies": { - "@tauri-apps/cli": "^2.1.0", - "@types/react": "npm:types-react@rc", - "@types/react-dom": "npm:types-react-dom@rc", - "@vitejs/plugin-react-swc": "^3.7.1", - "autoprefixer": "^10.4.18", - "postcss": "^8.4.36", - "tailwindcss": "^3.4.1", - "typescript": "^5.6.2", - "vite": "^5.4.9" - } + "name": "@sd/tauri", + "private": true, + "version": "2.0.0", + "type": "module", + "engines": { + "bun": ">=1.3.0" + }, + "scripts": { + "dev": "vite dev", + "dev:with-daemon": "bun ./scripts/dev-with-daemon.ts", + "build": "vite build", + "build:daemon": "cargo build --bin sd-daemon --manifest-path ../../Cargo.toml", + "build:daemon:release": "cargo build --release --bin sd-daemon --manifest-path ../../Cargo.toml", + "preview": "vite preview", + "typecheck": "tsc -b", + "tauri": "bunx tauri", + "tauri:dev": "bunx tauri dev", + "tauri:dev:no-watch": "bunx tauri dev --no-watch", + "tauri:build": "bunx tauri build && ./scripts/fix-daemon-entitlements.sh ../../target/release/bundle/macos/Spacedrive.app || true" + }, + "dependencies": { + "@phosphor-icons/react": "^2.1.0", + "@sd/assets": "workspace:*", + "@sd/interface": "workspace:*", + "@sd/ts-client": "workspace:*", + "@sd/ui": "workspace:*", + "@tauri-apps/api": "^2.1.1", + "@tauri-apps/plugin-dialog": "^2.4.2", + "@tauri-apps/plugin-fs": "^2.0.1", + "@tauri-apps/plugin-shell": "^2.0.1", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-scan": "^0.4.3" + }, + "devDependencies": { + "@tauri-apps/cli": "^2.1.0", + "@types/react": "npm:types-react@rc", + "@types/react-dom": "npm:types-react-dom@rc", + "@vitejs/plugin-react-swc": "^3.7.1", + "autoprefixer": "^10.4.18", + "postcss": "^8.4.36", + "tailwindcss": "^3.4.1", + "typescript": "^5.6.2", + "vite": "^5.4.9" + } } diff --git a/apps/tauri/postcss.config.cjs b/apps/tauri/postcss.config.cjs index e873f1a4f..cf0fb6c53 100644 --- a/apps/tauri/postcss.config.cjs +++ b/apps/tauri/postcss.config.cjs @@ -1,6 +1,7 @@ +"use strict"; module.exports = { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, }; diff --git a/apps/tauri/scripts/dev-with-daemon.ts b/apps/tauri/scripts/dev-with-daemon.ts index 720f71482..7432ff9d8 100755 --- a/apps/tauri/scripts/dev-with-daemon.ts +++ b/apps/tauri/scripts/dev-with-daemon.ts @@ -10,9 +10,9 @@ */ import { spawn } from "child_process"; -import { existsSync, unlinkSync } from "fs"; -import { join, resolve, dirname } from "path"; +import { existsSync } from "fs"; import { homedir, platform } from "os"; +import { dirname, join, resolve } from "path"; import { fileURLToPath } from "url"; // Get script directory @@ -35,9 +35,9 @@ const DAEMON_PORT = 6969; const DAEMON_ADDR = `127.0.0.1:${DAEMON_PORT}`; // Fix Data Directory for Windows (Optional but recommended) -const DATA_DIR = IS_WIN - ? join(homedir(), "AppData/Roaming/spacedrive") - : join(homedir(), "Library/Application Support/spacedrive"); +const DATA_DIR = IS_WIN + ? join(homedir(), "AppData/Roaming/spacedrive") + : join(homedir(), "Library/Application Support/spacedrive"); let daemonProcess: any = null; let viteProcess: any = null; @@ -45,21 +45,21 @@ let startedDaemon = false; // Cleanup function function cleanup() { - console.log("\nCleaning up..."); + console.log("\nCleaning up..."); - if (viteProcess) { - console.log("Stopping Vite..."); - viteProcess.kill(); - } + if (viteProcess) { + console.log("Stopping Vite..."); + viteProcess.kill(); + } - if (daemonProcess && startedDaemon) { - console.log("Stopping daemon (started by us)..."); - daemonProcess.kill(); - } else if (!startedDaemon) { - console.log("Leaving existing daemon running..."); - } + if (daemonProcess && startedDaemon) { + console.log("Stopping daemon (started by us)..."); + daemonProcess.kill(); + } else if (!startedDaemon) { + console.log("Leaving existing daemon running..."); + } - process.exit(0); + process.exit(0); } // Handle signals @@ -67,137 +67,137 @@ process.on("SIGINT", cleanup); process.on("SIGTERM", cleanup); async function main() { - console.log("Building daemon (dev profile)..."); - console.log("Project root:", PROJECT_ROOT); - console.log("Daemon binary:", DAEMON_BIN); + console.log("Building daemon (dev profile)..."); + console.log("Project root:", PROJECT_ROOT); + console.log("Daemon binary:", DAEMON_BIN); - // Build daemon - // On Windows, the binary target name is still just "sd-daemon" (Cargo handles the .exe) - const build = spawn("cargo", ["build", "--bin", "sd-daemon"], { - cwd: PROJECT_ROOT, - stdio: "inherit", - shell: IS_WIN, // shell: true is often needed on Windows for spawn to work correctly + // Build daemon + // On Windows, the binary target name is still just "sd-daemon" (Cargo handles the .exe) + const build = spawn("cargo", ["build", "--bin", "sd-daemon"], { + cwd: PROJECT_ROOT, + stdio: "inherit", + shell: IS_WIN, // shell: true is often needed on Windows for spawn to work correctly + }); + + await new Promise((resolve, reject) => { + build.on("exit", (code) => { + if (code === 0) { + resolve(); + } else { + reject(new Error(`Daemon build failed with code ${code}`)); + } }); + }); + console.log("Daemon built successfully"); + + // Check if daemon is already running by trying to connect to TCP port + let daemonAlreadyRunning = false; + console.log(`Checking if daemon is running on ${DAEMON_ADDR}...`); + try { + const { connect } = await import("net"); await new Promise((resolve, reject) => { - build.on("exit", (code) => { - if (code === 0) { - resolve(); - } else { - reject(new Error(`Daemon build failed with code ${code}`)); - } - }); + const client = connect(DAEMON_PORT, "127.0.0.1"); + client.on("connect", () => { + daemonAlreadyRunning = true; + client.end(); + resolve(); + }); + client.on("error", () => { + reject(); + }); + setTimeout(() => reject(), 1000); + }); + } catch (e) { + // Connection failed, daemon not running + daemonAlreadyRunning = false; + } + + if (daemonAlreadyRunning) { + console.log("Daemon already running, will connect to existing instance"); + startedDaemon = false; + } else { + // Start daemon + console.log("Starting daemon..."); + startedDaemon = true; + + // Verify binary exists + if (!existsSync(DAEMON_BIN)) { + throw new Error(`Daemon binary not found at: ${DAEMON_BIN}`); + } + + const depsLibPath = join(PROJECT_ROOT, "apps/.deps/lib"); + const depsBinPath = join(PROJECT_ROOT, "apps/.deps/bin"); + + daemonProcess = spawn(DAEMON_BIN, ["--data-dir", DATA_DIR], { + cwd: PROJECT_ROOT, + stdio: ["ignore", "pipe", "pipe"], + env: { + ...process.env, + // macOS library path + DYLD_LIBRARY_PATH: depsLibPath, + // Windows: Add DLLs directory to PATH + PATH: IS_WIN + ? `${depsBinPath};${process.env.PATH || ""}` + : process.env.PATH, + }, }); - console.log("Daemon built successfully"); + // Log daemon output + daemonProcess.stdout.on("data", (data: Buffer) => { + const lines = data.toString().trim().split("\n"); + for (const line of lines) { + console.log(`[daemon] ${line}`); + } + }); - // Check if daemon is already running by trying to connect to TCP port - let daemonAlreadyRunning = false; - console.log(`Checking if daemon is running on ${DAEMON_ADDR}...`); - try { + daemonProcess.stderr.on("data", (data: Buffer) => { + const lines = data.toString().trim().split("\n"); + for (const line of lines) { + console.log(`[daemon] ${line}`); + } + }); + + // Wait for daemon to be ready + console.log("Waiting for daemon to be ready..."); + for (let i = 0; i < 30; i++) { + try { const { connect } = await import("net"); await new Promise((resolve, reject) => { - const client = connect(DAEMON_PORT, "127.0.0.1"); - client.on("connect", () => { - daemonAlreadyRunning = true; - client.end(); - resolve(); - }); - client.on("error", () => { - reject(); - }); - setTimeout(() => reject(), 1000); + const client = connect(DAEMON_PORT, "127.0.0.1"); + client.on("connect", () => { + client.end(); + resolve(); + }); + client.on("error", reject); + setTimeout(() => reject(), 500); }); - } catch (e) { - // Connection failed, daemon not running - daemonAlreadyRunning = false; - } - - if (daemonAlreadyRunning) { - console.log("Daemon already running, will connect to existing instance"); - startedDaemon = false; - } else { - // Start daemon - console.log("Starting daemon..."); - startedDaemon = true; - - // Verify binary exists - if (!existsSync(DAEMON_BIN)) { - throw new Error(`Daemon binary not found at: ${DAEMON_BIN}`); - } - - const depsLibPath = join(PROJECT_ROOT, "apps/.deps/lib"); - const depsBinPath = join(PROJECT_ROOT, "apps/.deps/bin"); - - daemonProcess = spawn(DAEMON_BIN, ["--data-dir", DATA_DIR], { - cwd: PROJECT_ROOT, - stdio: ["ignore", "pipe", "pipe"], - env: { - ...process.env, - // macOS library path - DYLD_LIBRARY_PATH: depsLibPath, - // Windows: Add DLLs directory to PATH - PATH: IS_WIN - ? `${depsBinPath};${process.env.PATH || ""}` - : process.env.PATH, - }, - }); - - // Log daemon output - daemonProcess.stdout.on("data", (data: Buffer) => { - const lines = data.toString().trim().split("\n"); - for (const line of lines) { - console.log(`[daemon] ${line}`); - } - }); - - daemonProcess.stderr.on("data", (data: Buffer) => { - const lines = data.toString().trim().split("\n"); - for (const line of lines) { - console.log(`[daemon] ${line}`); - } - }); - - // Wait for daemon to be ready - console.log("Waiting for daemon to be ready..."); - for (let i = 0; i < 30; i++) { - try { - const { connect } = await import("net"); - await new Promise((resolve, reject) => { - const client = connect(DAEMON_PORT, "127.0.0.1"); - client.on("connect", () => { - client.end(); - resolve(); - }); - client.on("error", reject); - setTimeout(() => reject(), 500); - }); - console.log(`Daemon ready at ${DAEMON_ADDR}`); - break; - } catch (e) { - if (i === 29) { - throw new Error("Daemon failed to start (connection not available)"); - } - await new Promise((resolve) => setTimeout(resolve, 1000)); - } + console.log(`Daemon ready at ${DAEMON_ADDR}`); + break; + } catch (e) { + if (i === 29) { + throw new Error("Daemon failed to start (connection not available)"); } + await new Promise((resolve) => setTimeout(resolve, 1000)); + } } + } - // Start Vite - console.log("Starting Vite dev server..."); - - // Use 'bun' explicitly, with shell true for Windows compatibility - viteProcess = spawn("bun", ["run", "dev"], { - stdio: "inherit", - shell: IS_WIN, - }); + // Start Vite + console.log("Starting Vite dev server..."); - // Keep running - await new Promise(() => {}); + // Use 'bun' explicitly, with shell true for Windows compatibility + viteProcess = spawn("bun", ["run", "dev"], { + stdio: "inherit", + shell: IS_WIN, + }); + + // Keep running + await new Promise(() => {}); } main().catch((error) => { - console.error("Error:", error); - cleanup(); - process.exit(1); -}); \ No newline at end of file + console.error("Error:", error); + cleanup(); + process.exit(1); +}); diff --git a/apps/tauri/src-tauri/capabilities/default.json b/apps/tauri/src-tauri/capabilities/default.json index cc4eee9f3..5838ff87a 100644 --- a/apps/tauri/src-tauri/capabilities/default.json +++ b/apps/tauri/src-tauri/capabilities/default.json @@ -1,23 +1,29 @@ { - "$schema": "../gen/schemas/desktop-schema.json", - "identifier": "default", - "description": "Default permissions for Spacedrive", - "windows": ["main", "inspector-*", "quick-preview-*", "settings-*", "job-manager"], - "permissions": [ - "core:default", - "core:event:allow-listen", - "core:event:allow-emit", - "core:window:allow-create", - "core:window:allow-close", - "core:window:allow-get-all-windows", - "core:window:allow-start-dragging", - "core:webview:allow-create-webview-window", - "core:path:default", - "dialog:allow-open", - "dialog:allow-save", - "shell:allow-open", - "fs:allow-home-read-recursive", - "clipboard-manager:allow-read-text", - "clipboard-manager:allow-write-text" - ] + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "default", + "description": "Default permissions for Spacedrive", + "windows": [ + "main", + "inspector-*", + "quick-preview-*", + "settings-*", + "job-manager" + ], + "permissions": [ + "core:default", + "core:event:allow-listen", + "core:event:allow-emit", + "core:window:allow-create", + "core:window:allow-close", + "core:window:allow-get-all-windows", + "core:window:allow-start-dragging", + "core:webview:allow-create-webview-window", + "core:path:default", + "dialog:allow-open", + "dialog:allow-save", + "shell:allow-open", + "fs:allow-home-read-recursive", + "clipboard-manager:allow-read-text", + "clipboard-manager:allow-write-text" + ] } diff --git a/apps/tauri/src/App.tsx b/apps/tauri/src/App.tsx index 2822b0eb3..d5589f0ca 100644 --- a/apps/tauri/src/App.tsx +++ b/apps/tauri/src/App.tsx @@ -1,301 +1,293 @@ +import { + FloatingControls, + JobsScreen, + LocationCacheDemo, + PlatformProvider, + PopoutInspector, + QuickPreview, + ServerProvider, + Settings, + Shell, + SpacedriveProvider, +} from "@sd/interface"; +import type { Event as CoreEvent } from "@sd/ts-client"; +import { + SpacedriveClient, + TauriTransport, + useSyncPreferencesStore, +} from "@sd/ts-client"; import { invoke } from "@tauri-apps/api/core"; import { listen } from "@tauri-apps/api/event"; import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow"; -import { - Shell, - FloatingControls, - LocationCacheDemo, - PopoutInspector, - QuickPreview, - JobsScreen, - Settings, - PlatformProvider, - SpacedriveProvider, - ServerProvider, -} from "@sd/interface"; -import { - SpacedriveClient, - TauriTransport, - useSyncPreferencesStore, -} from "@sd/ts-client"; -import type { Event as CoreEvent } from "@sd/ts-client"; -import { sounds } from "@sd/assets/sounds"; import { useEffect, useState } from "react"; -import { DragOverlay } from "./routes/DragOverlay"; -import { ContextMenuWindow } from "./routes/ContextMenuWindow"; import { DragDemo } from "./components/DragDemo"; -import { SpacedropWindow } from "./routes/Spacedrop"; -import { platform } from "./platform"; import { initializeContextMenuHandler } from "./contextMenu"; import { initializeKeybindGlobal } from "./keybinds"; +import { platform } from "./platform"; +import { ContextMenuWindow } from "./routes/ContextMenuWindow"; +import { DragOverlay } from "./routes/DragOverlay"; +import { SpacedropWindow } from "./routes/Spacedrop"; function App() { - const [client, setClient] = useState(null); - const [error, setError] = useState(null); - const [route, setRoute] = useState("/"); + const [client, setClient] = useState(null); + const [error, setError] = useState(null); + const [route, setRoute] = useState("/"); - useEffect(() => { - // React Scan disabled - too heavy for development - // Uncomment if you need to debug render performance: - if (import.meta.env.DEV) { - // setTimeout(() => { - // import("react-scan").then(({ scan }) => { - // scan({ enabled: true, log: false }); - // }); - // }, 2000); - } + useEffect(() => { + // React Scan disabled - too heavy for development + // Uncomment if you need to debug render performance: + if (import.meta.env.DEV) { + // setTimeout(() => { + // import("react-scan").then(({ scan }) => { + // scan({ enabled: true, log: false }); + // }); + // }, 2000); + } - // Initialize Tauri native context menu handler - initializeContextMenuHandler(); + // Initialize Tauri native context menu handler + initializeContextMenuHandler(); - // Initialize Tauri keybind handler - initializeKeybindGlobal(); + // Initialize Tauri keybind handler + initializeKeybindGlobal(); - // Prevent default context menu globally (except in context menu windows) - const currentWindow = getCurrentWebviewWindow(); - const label = currentWindow.label; + // Prevent default context menu globally (except in context menu windows) + const currentWindow = getCurrentWebviewWindow(); + const label = currentWindow.label; - // Prevent default browser context menu globally (except in context menu windows) - if (!label.startsWith("context-menu")) { - const preventContextMenu = (e: Event) => { - // Default behavior: prevent browser context menu - // React's onContextMenu handlers can override this with their own preventDefault - e.preventDefault(); - }; - document.addEventListener("contextmenu", preventContextMenu, { - capture: false, - }); - } + // Prevent default browser context menu globally (except in context menu windows) + if (!label.startsWith("context-menu")) { + const preventContextMenu = (e: Event) => { + // Default behavior: prevent browser context menu + // React's onContextMenu handlers can override this with their own preventDefault + e.preventDefault(); + }; + document.addEventListener("contextmenu", preventContextMenu, { + capture: false, + }); + } - // Set route based on window label - if (label === "floating-controls") { - setRoute("/floating-controls"); - } else if (label.startsWith("drag-overlay")) { - setRoute("/drag-overlay"); - } else if (label.startsWith("context-menu")) { - setRoute("/contextmenu"); - } else if (label.startsWith("drag-demo")) { - setRoute("/drag-demo"); - } else if (label.startsWith("spacedrop")) { - setRoute("/spacedrop"); - } else if (label.startsWith("settings")) { - setRoute("/settings"); - } else if (label.startsWith("inspector")) { - setRoute("/inspector"); - } else if (label.startsWith("quick-preview")) { - setRoute("/quick-preview"); - } else if (label.startsWith("cache-demo")) { - setRoute("/cache-demo"); - } else if (label.startsWith("job-manager")) { - setRoute("/job-manager"); - } + // Set route based on window label + if (label === "floating-controls") { + setRoute("/floating-controls"); + } else if (label.startsWith("drag-overlay")) { + setRoute("/drag-overlay"); + } else if (label.startsWith("context-menu")) { + setRoute("/contextmenu"); + } else if (label.startsWith("drag-demo")) { + setRoute("/drag-demo"); + } else if (label.startsWith("spacedrop")) { + setRoute("/spacedrop"); + } else if (label.startsWith("settings")) { + setRoute("/settings"); + } else if (label.startsWith("inspector")) { + setRoute("/inspector"); + } else if (label.startsWith("quick-preview")) { + setRoute("/quick-preview"); + } else if (label.startsWith("cache-demo")) { + setRoute("/cache-demo"); + } else if (label.startsWith("job-manager")) { + setRoute("/job-manager"); + } - // Tell Tauri window is ready to be shown - invoke("app_ready").catch(console.error); + // Tell Tauri window is ready to be shown + invoke("app_ready").catch(console.error); - // Play startup sound - // sounds.startup(); + // Play startup sound + // sounds.startup(); - let unsubscribePromise: Promise<() => void> | null = null; + let unsubscribePromise: Promise<() => void> | null = null; - // Create Tauri-based client - try { - const transport = new TauriTransport(invoke, listen); - const spacedrive = new SpacedriveClient(transport); - setClient(spacedrive); + // Create Tauri-based client + try { + const transport = new TauriTransport(invoke, listen); + const spacedrive = new SpacedriveClient(transport); + setClient(spacedrive); - // Query current library ID from platform state (for popout windows) - if (platform.getCurrentLibraryId) { - platform - .getCurrentLibraryId() - .then((libraryId) => { - if (libraryId) { - spacedrive.setCurrentLibrary(libraryId, false); // Don't emit - already in sync - } - }) - .catch(() => { - // Library not selected yet - this is fine for initial load - }); - } + // Query current library ID from platform state (for popout windows) + if (platform.getCurrentLibraryId) { + platform + .getCurrentLibraryId() + .then((libraryId) => { + if (libraryId) { + spacedrive.setCurrentLibrary(libraryId, false); // Don't emit - already in sync + } + }) + .catch(() => { + // Library not selected yet - this is fine for initial load + }); + } - // Listen for library-changed events via platform (emitted when library switches) - if (platform.onLibraryIdChanged) { - platform.onLibraryIdChanged((newLibraryId) => { - spacedrive.setCurrentLibrary(newLibraryId, true); // DO emit - hooks need to know! - }); - } + // Listen for library-changed events via platform (emitted when library switches) + if (platform.onLibraryIdChanged) { + platform.onLibraryIdChanged((newLibraryId) => { + spacedrive.setCurrentLibrary(newLibraryId, true); // DO emit - hooks need to know! + }); + } - // Subscribe to core events for auto-switching on synced library creation - unsubscribePromise = spacedrive.subscribe((event: CoreEvent) => { - // Check if this is a LibraryCreated event from sync - if ( - typeof event === "object" && - "LibraryCreated" in event && - (event.LibraryCreated as any).source === "Sync" - ) { - const { id, name } = event.LibraryCreated; + // Subscribe to core events for auto-switching on synced library creation + unsubscribePromise = spacedrive.subscribe((event: CoreEvent) => { + // Check if this is a LibraryCreated event from sync + if ( + typeof event === "object" && + "LibraryCreated" in event && + (event.LibraryCreated as any).source === "Sync" + ) { + const { id, name } = event.LibraryCreated; - // Check user preference for auto-switching - const autoSwitchEnabled = - useSyncPreferencesStore.getState().autoSwitchOnSync; + // Check user preference for auto-switching + const autoSwitchEnabled = + useSyncPreferencesStore.getState().autoSwitchOnSync; - if (autoSwitchEnabled) { - console.log( - `[Auto-Switch] Received synced library "${name}", switching...`, - ); + if (autoSwitchEnabled) { + console.log( + `[Auto-Switch] Received synced library "${name}", switching...` + ); - // Switch to the new library via platform (syncs across all windows) - if (platform.setCurrentLibraryId) { - platform.setCurrentLibraryId(id).catch((err) => { - console.error( - "[Auto-Switch] Failed to switch library:", - err, - ); - }); - } else { - // Fallback: just update the client - spacedrive.setCurrentLibrary(id); - } - } else { - console.log( - `[Auto-Switch] Received synced library "${name}", but auto-switch is disabled`, - ); - } - } - }); + // Switch to the new library via platform (syncs across all windows) + if (platform.setCurrentLibraryId) { + platform.setCurrentLibraryId(id).catch((err) => { + console.error("[Auto-Switch] Failed to switch library:", err); + }); + } else { + // Fallback: just update the client + spacedrive.setCurrentLibrary(id); + } + } else { + console.log( + `[Auto-Switch] Received synced library "${name}", but auto-switch is disabled` + ); + } + } + }); - // No global subscription needed - each useNormalizedCache creates its own filtered subscription - } catch (err) { - console.error("Failed to create client:", err); - setError(err instanceof Error ? err.message : String(err)); - } + // No global subscription needed - each useNormalizedCache creates its own filtered subscription + } catch (err) { + console.error("Failed to create client:", err); + setError(err instanceof Error ? err.message : String(err)); + } - return () => { - if (unsubscribePromise) { - unsubscribePromise.then((unsubscribe) => unsubscribe()); - } + return () => { + if (unsubscribePromise) { + unsubscribePromise.then((unsubscribe) => unsubscribe()); + } - // Clean up all backend TCP connections to prevent connection leaks - // This is especially important during development hot reloads - invoke("cleanup_all_connections").catch((err) => { - console.warn("Failed to cleanup connections:", err); - }); - }; - }, []); + // Clean up all backend TCP connections to prevent connection leaks + // This is especially important during development hot reloads + invoke("cleanup_all_connections").catch((err) => { + console.warn("Failed to cleanup connections:", err); + }); + }; + }, []); - // Routes that don't need the client - if (route === "/floating-controls") { - return ; - } + // Routes that don't need the client + if (route === "/floating-controls") { + return ; + } - if (route === "/drag-overlay") { - return ; - } + if (route === "/drag-overlay") { + return ; + } - if (route === "/contextmenu") { - return ; - } + if (route === "/contextmenu") { + return ; + } - if (route === "/drag-demo") { - return ; - } + if (route === "/drag-demo") { + return ; + } - if (route === "/spacedrop") { - return ; - } + if (route === "/spacedrop") { + return ; + } - if (error) { - console.log("Rendering error state"); - return ( -
-
-

Error

-

{error}

-
-
- ); - } + if (error) { + console.log("Rendering error state"); + return ( +
+
+

Error

+

{error}

+
+
+ ); + } - if (!client) { - console.log("Rendering loading state"); - return ( -
-
-
- Initializing client... -
-

- Check console for logs -

-
-
- ); - } + if (!client) { + console.log("Rendering loading state"); + return ( +
+
+
Initializing client...
+

Check console for logs

+
+
+ ); + } - console.log("Rendering Interface with client"); + console.log("Rendering Interface with client"); - // Route to different UIs based on window type - if (route === "/settings") { - return ( - - - - - - ); - } + // Route to different UIs based on window type + if (route === "/settings") { + return ( + + + + + + ); + } - if (route === "/inspector") { - return ( - - - -
- -
-
-
-
- ); - } + if (route === "/inspector") { + return ( + + + +
+ +
+
+
+
+ ); + } - if (route === "/cache-demo") { - return ; - } + if (route === "/cache-demo") { + return ; + } - if (route === "/quick-preview") { - return ( - - - -
- -
-
-
-
- ); - } + if (route === "/quick-preview") { + return ( + + + +
+ +
+
+
+
+ ); + } - if (route === "/job-manager") { - return ( - - - -
- -
-
-
-
- ); - } + if (route === "/job-manager") { + return ( + + + +
+ +
+
+
+
+ ); + } - return ( - - - - ); + return ( + + + + ); } -export default App; \ No newline at end of file +export default App; diff --git a/apps/tauri/src/components/DragDemo.tsx b/apps/tauri/src/components/DragDemo.tsx index 0ac711ae1..b764bd906 100644 --- a/apps/tauri/src/components/DragDemo.tsx +++ b/apps/tauri/src/components/DragDemo.tsx @@ -1,274 +1,249 @@ -import { useState, useRef } from "react"; -import { Copy, Trash, Eye, Share } from "@phosphor-icons/react"; +import { Copy, Eye, Share, Trash } from "@phosphor-icons/react"; +import { useContextMenu } from "@sd/interface"; +import { useRef, useState } from "react"; import { useDragOperation } from "../hooks/useDragOperation"; import { useDropZone } from "../hooks/useDropZone"; -import { useContextMenu } from "@sd/interface"; import type { DragItem } from "../lib/drag"; export function DragDemo() { - const [selectedFiles, setSelectedFiles] = useState([ - "/Users/example/Documents/report.pdf", - "/Users/example/Pictures/photo.jpg", - ]); - const [selectedFile, setSelectedFile] = useState(null); - const [draggingFile, setDraggingFile] = useState(null); - const dragStartPos = useRef<{ x: number; y: number } | null>(null); + const [selectedFiles, setSelectedFiles] = useState([ + "/Users/example/Documents/report.pdf", + "/Users/example/Pictures/photo.jpg", + ]); + const [selectedFile, setSelectedFile] = useState(null); + const [draggingFile, setDraggingFile] = useState(null); + const dragStartPos = useRef<{ x: number; y: number } | null>(null); - // Context menu for files - const contextMenu = useContextMenu({ - items: [ - { - icon: Copy, - label: "Copy", - onClick: () => alert(`Copying: ${selectedFile}`), - keybind: "⌘C", - condition: () => selectedFile !== null, - }, - { - icon: Eye, - label: "Quick Look", - onClick: () => alert(`Quick Look: ${selectedFile}`), - keybind: "Space", - }, - { type: "separator" }, - { - icon: Share, - label: "Share", - submenu: [ - { - label: "AirDrop", - onClick: () => alert("AirDrop share"), - }, - { - label: "Messages", - onClick: () => alert("Messages share"), - }, - ], - }, - { type: "separator" }, - { - icon: Trash, - label: "Delete", - onClick: () => { - if (selectedFile && confirm(`Delete ${selectedFile}?`)) { - setSelectedFiles((files) => - files.filter((f) => f !== selectedFile), - ); - setSelectedFile(null); - } - }, - keybind: "⌘⌫", - variant: "danger" as const, - }, - ], - }); + // Context menu for files + const contextMenu = useContextMenu({ + items: [ + { + icon: Copy, + label: "Copy", + onClick: () => alert(`Copying: ${selectedFile}`), + keybind: "⌘C", + condition: () => selectedFile !== null, + }, + { + icon: Eye, + label: "Quick Look", + onClick: () => alert(`Quick Look: ${selectedFile}`), + keybind: "Space", + }, + { type: "separator" }, + { + icon: Share, + label: "Share", + submenu: [ + { + label: "AirDrop", + onClick: () => alert("AirDrop share"), + }, + { + label: "Messages", + onClick: () => alert("Messages share"), + }, + ], + }, + { type: "separator" }, + { + icon: Trash, + label: "Delete", + onClick: () => { + if (selectedFile && confirm(`Delete ${selectedFile}?`)) { + setSelectedFiles((files) => + files.filter((f) => f !== selectedFile) + ); + setSelectedFile(null); + } + }, + keybind: "⌘⌫", + variant: "danger" as const, + }, + ], + }); - const { isDragging, startDrag, cursorPosition } = useDragOperation({ - onDragStart: (sessionId) => { - console.log("Drag started:", sessionId); - }, - onDragEnd: (result) => { - console.log("Drag ended:", result); - setDraggingFile(null); - dragStartPos.current = null; - }, - }); + const { isDragging, startDrag, cursorPosition } = useDragOperation({ + onDragStart: (sessionId) => { + console.log("Drag started:", sessionId); + }, + onDragEnd: (result) => { + console.log("Drag ended:", result); + setDraggingFile(null); + dragStartPos.current = null; + }, + }); - const { isHovered, dropZoneProps } = useDropZone({ - onDrop: (items) => { - console.log("Files dropped:", items); - }, - onDragEnter: () => { - console.log("Drag entered drop zone"); - }, - onDragLeave: () => { - console.log("Drag left drop zone"); - }, - }); + const { isHovered, dropZoneProps } = useDropZone({ + onDrop: (items) => { + console.log("Files dropped:", items); + }, + onDragEnter: () => { + console.log("Drag entered drop zone"); + }, + onDragLeave: () => { + console.log("Drag left drop zone"); + }, + }); - const handleMouseDown = (file: string, e: React.MouseEvent) => { - setDraggingFile(file); - dragStartPos.current = { x: e.clientX, y: e.clientY }; - }; + const handleMouseDown = (file: string, e: React.MouseEvent) => { + setDraggingFile(file); + dragStartPos.current = { x: e.clientX, y: e.clientY }; + }; - const handleMouseMove = async (e: React.MouseEvent) => { - if (!draggingFile || !dragStartPos.current || isDragging) return; + const handleMouseMove = async (e: React.MouseEvent) => { + if (!(draggingFile && dragStartPos.current) || isDragging) return; - const distance = Math.sqrt( - Math.pow(e.clientX - dragStartPos.current.x, 2) + - Math.pow(e.clientY - dragStartPos.current.y, 2), - ); + const distance = Math.sqrt( + (e.clientX - dragStartPos.current.x) ** 2 + + (e.clientY - dragStartPos.current.y) ** 2 + ); - // Start native drag after moving 10px - if (distance > 10) { - const items: DragItem[] = [ - { - id: `file-${draggingFile}`, - kind: { - type: "file" as const, - path: draggingFile, - }, - }, - ]; + // Start native drag after moving 10px + if (distance > 10) { + const items: DragItem[] = [ + { + id: `file-${draggingFile}`, + kind: { + type: "file" as const, + path: draggingFile, + }, + }, + ]; - try { - await startDrag({ - items, - allowedOperations: ["copy", "move"], - }); - } catch (error) { - console.error("Failed to start drag:", error); - setDraggingFile(null); - } - } - }; + try { + await startDrag({ + items, + allowedOperations: ["copy", "move"], + }); + } catch (error) { + console.error("Failed to start drag:", error); + setDraggingFile(null); + } + } + }; - const handleMouseUp = () => { - setDraggingFile(null); - dragStartPos.current = null; - }; + const handleMouseUp = () => { + setDraggingFile(null); + dragStartPos.current = null; + }; - return ( -
-

Native Drag & Drop Demo

+ return ( +
+

Native Drag & Drop Demo

- {/* Draggable items */} -
-

Draggable Files

-
- {selectedFiles.map((file, idx) => ( -
{ - e.preventDefault(); - handleMouseDown(file, e); - }} - onClick={() => setSelectedFile(file)} - onContextMenu={(e) => { - setSelectedFile(file); - contextMenu.show(e); - }} - > -
-
-
-
- {file.split("/").pop()} -
-
- {file} -
-
-
-
- ))} -
-

- Click and drag these files - move them out of the window to - start native drag! -
- Right-click on a file to test the native context menu. -

-
+ {/* Draggable items */} +
+

Draggable Files

+
+ {selectedFiles.map((file, idx) => ( +
setSelectedFile(file)} + onContextMenu={(e) => { + setSelectedFile(file); + contextMenu.show(e); + }} + onMouseDown={(e) => { + e.preventDefault(); + handleMouseDown(file, e); + }} + > +
+
+
+
{file.split("/").pop()}
+
{file}
+
+
+
+ ))} +
+

+ Click and drag these files - move them out of the window to start + native drag! +
+ Right-click on a file to test the native context menu. +

+
- {/* Drop zone */} -
-

Drop Zone

-
+

Drop Zone

+
-
{isHovered ? "" : ""}
-
- {isHovered ? "Drop files here" : "Drag files here"} -
-
- This drop zone accepts files from other Spacedrive - windows -
-
-
+ > +
{isHovered ? "" : ""}
+
+ {isHovered ? "Drop files here" : "Drag files here"} +
+
+ This drop zone accepts files from other Spacedrive windows +
+
+
- {/* Status */} -
-

Status

-
-
- Dragging:{" "} - - {isDragging ? "Yes" : "No"} - -
-
- - Drop zone hovered: - {" "} - - {isHovered ? "Yes" : "No"} - -
- {cursorPosition && ( -
- Cursor:{" "} - - ({Math.round(cursorPosition.x)},{" "} - {Math.round(cursorPosition.y)}) - -
- )} -
-
+ {/* Status */} +
+

Status

+
+
+ Dragging:{" "} + + {isDragging ? "Yes" : "No"} + +
+
+ Drop zone hovered:{" "} + + {isHovered ? "Yes" : "No"} + +
+ {cursorPosition && ( +
+ Cursor:{" "} + + ({Math.round(cursorPosition.x)}, {Math.round(cursorPosition.y)}) + +
+ )} +
+
-
-

How it works:

-
    -
  • - Drag files from the list above to Finder - they'll - appear as real files -
  • -
  • - The custom overlay window follows your cursor during the - drag -
  • -
  • - Drop zones in other Spacedrive windows can receive the - dragged files -
  • -
  • - All drag state is synchronized across windows via Tauri - events -
  • -
  • - - Right-click files for native context menu - {" "} - - transparent window positioned at cursor -
  • -
-
-
- ); +
+

How it works:

+
    +
  • + Drag files from the list above to Finder - they'll appear as real + files +
  • +
  • The custom overlay window follows your cursor during the drag
  • +
  • + Drop zones in other Spacedrive windows can receive the dragged files +
  • +
  • + All drag state is synchronized across windows via Tauri events +
  • +
  • + Right-click files for native context menu - + transparent window positioned at cursor +
  • +
+
+
+ ); } diff --git a/apps/tauri/src/contextMenu.ts b/apps/tauri/src/contextMenu.ts index 9ced5ddb2..3ee132734 100644 --- a/apps/tauri/src/contextMenu.ts +++ b/apps/tauri/src/contextMenu.ts @@ -1,63 +1,68 @@ -import { Menu, MenuItem, Submenu, PredefinedMenuItem } from '@tauri-apps/api/menu'; -import type { ContextMenuItem } from '@sd/interface'; +import type { ContextMenuItem } from "@sd/interface"; +import { + Menu, + MenuItem, + PredefinedMenuItem, + Submenu, +} from "@tauri-apps/api/menu"; /** * Convert platform-agnostic menu items to Tauri's native Menu API */ export async function showNativeContextMenu( - items: ContextMenuItem[], - position: { x: number; y: number } + items: ContextMenuItem[], + position: { x: number; y: number } ) { - console.log('[Tauri ContextMenu] Building native menu from items:', items); + console.log("[Tauri ContextMenu] Building native menu from items:", items); - const menuItems = await buildMenuItems(items); - const menu = await Menu.new({ items: menuItems }); + const menuItems = await buildMenuItems(items); + const menu = await Menu.new({ items: menuItems }); - console.log('[Tauri ContextMenu] Showing menu at position:', position); - await menu.popup(); + console.log("[Tauri ContextMenu] Showing menu at position:", position); + await menu.popup(); } /** * Recursively build Tauri menu items from platform-agnostic definitions */ async function buildMenuItems(items: ContextMenuItem[]): Promise { - const menuItems = []; + const menuItems = []; - for (const item of items) { - if (item.type === 'separator') { - // Add separator - menuItems.push(await PredefinedMenuItem.new({ item: 'Separator' })); - } else if (item.submenu) { - // Add submenu - const subItems = await buildMenuItems(item.submenu); - const submenu = await Submenu.new({ - text: item.label || 'Submenu', - items: subItems, - }); - menuItems.push(submenu); - } else { - // Add regular menu item - const menuItem = await MenuItem.new({ - text: item.label || '', - enabled: !item.disabled, - accelerator: item.keybind, - action: item.onClick, - }); - menuItems.push(menuItem); - } - } + for (const item of items) { + if (item.type === "separator") { + // Add separator + menuItems.push(await PredefinedMenuItem.new({ item: "Separator" })); + } else if (item.submenu) { + // Add submenu + const subItems = await buildMenuItems(item.submenu); + const submenu = await Submenu.new({ + text: item.label || "Submenu", + items: subItems, + }); + menuItems.push(submenu); + } else { + // Add regular menu item + const menuItem = await MenuItem.new({ + text: item.label || "", + enabled: !item.disabled, + accelerator: item.keybind, + action: item.onClick, + }); + menuItems.push(menuItem); + } + } - return menuItems; + return menuItems; } /** * Initialize the context menu handler on the window global */ export function initializeContextMenuHandler() { - if (!window.__SPACEDRIVE__) { - (window as any).__SPACEDRIVE__ = {}; - } + if (!window.__SPACEDRIVE__) { + (window as any).__SPACEDRIVE__ = {}; + } - window.__SPACEDRIVE__.showContextMenu = showNativeContextMenu; - console.log('[Tauri ContextMenu] Handler initialized'); + window.__SPACEDRIVE__.showContextMenu = showNativeContextMenu; + console.log("[Tauri ContextMenu] Handler initialized"); } diff --git a/apps/tauri/src/hooks/useDragOperation.ts b/apps/tauri/src/hooks/useDragOperation.ts index 53ffc2bfc..0eeaf7612 100644 --- a/apps/tauri/src/hooks/useDragOperation.ts +++ b/apps/tauri/src/hooks/useDragOperation.ts @@ -1,16 +1,16 @@ -import { useCallback, useEffect, useState, useRef } from 'react'; -import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'; +import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow"; +import { useCallback, useEffect, useRef, useState } from "react"; import { beginDrag, + type DragConfig, + type DragMoveEvent, + type DragResult, + type DragSession, endDrag, onDragBegan, onDragEnded, onDragMoved, - type DragConfig, - type DragSession, - type DragResult, - type DragMoveEvent, -} from '../lib/drag'; +} from "../lib/drag"; export interface UseDragOperationOptions { onDragStart?: (sessionId: string) => void; @@ -64,12 +64,12 @@ export function useDragOperation(options: UseDragOperationOptions = {}) { }, []); const startDrag = useCallback( - async (config: Omit) => { + async (config: Omit) => { const currentWindow = getCurrentWebviewWindow(); const sessionId = await beginDrag( { ...config, - overlayUrl: '/drag-overlay', + overlayUrl: "/drag-overlay", overlaySize: [200, 150], }, currentWindow.label @@ -80,7 +80,7 @@ export function useDragOperation(options: UseDragOperationOptions = {}) { ); const cancelDrag = useCallback(async (sessionId: string) => { - await endDrag(sessionId, { type: 'Cancelled' }); + await endDrag(sessionId, { type: "Cancelled" }); }, []); return { diff --git a/apps/tauri/src/hooks/useDropZone.ts b/apps/tauri/src/hooks/useDropZone.ts index 7bf21a9c2..26b284f69 100644 --- a/apps/tauri/src/hooks/useDropZone.ts +++ b/apps/tauri/src/hooks/useDropZone.ts @@ -1,12 +1,11 @@ -import { useEffect, useState, useCallback, useRef } from 'react'; -import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'; +import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow"; +import { useEffect, useRef, useState } from "react"; import { + type DragItem, + onDragEnded, onDragEntered, onDragLeft, - onDragEnded, - type DragItem, - type DragResult, -} from '../lib/drag'; +} from "../lib/drag"; export interface UseDropZoneOptions { onDrop?: (items: DragItem[]) => void; @@ -48,10 +47,12 @@ export function useDropZone(options: UseDropZoneOptions = {}) { const unlistenEnded = onDragEnded((event) => { setIsHovered((prevHovered) => { setDragItems((prevItems) => { - if (currentSessionRef.current === event.sessionId && prevHovered) { - if (event.result.type === 'Dropped') { - onDropRef.current?.(prevItems); - } + if ( + currentSessionRef.current === event.sessionId && + prevHovered && + event.result.type === "Dropped" + ) { + onDropRef.current?.(prevItems); } return []; }); @@ -68,8 +69,8 @@ export function useDropZone(options: UseDropZoneOptions = {}) { }, [currentWindowLabel]); const dropZoneProps = { - 'data-drop-zone': true, - 'data-hovered': isHovered, + "data-drop-zone": true, + "data-hovered": isHovered, }; return { diff --git a/apps/tauri/src/index.css b/apps/tauri/src/index.css index 97efa2717..d5ce4d243 100644 --- a/apps/tauri/src/index.css +++ b/apps/tauri/src/index.css @@ -4,53 +4,53 @@ /* Utility classes */ .top-bar-blur { - backdrop-filter: saturate(120%) blur(18px); + backdrop-filter: saturate(120%) blur(18px); } .frame::before { - content: ""; - pointer-events: none; - user-select: none; - position: absolute; - inset: 0px; - border-radius: inherit; - padding: 1px; - background: var(--color-app-frame); - mask: - linear-gradient(black, black) content-box content-box, - linear-gradient(black, black); - mask-composite: xor; - -webkit-mask-composite: xor; - z-index: 9999; + content: ""; + pointer-events: none; + user-select: none; + position: absolute; + inset: 0px; + border-radius: inherit; + padding: 1px; + background: var(--color-app-frame); + mask: + linear-gradient(black, black) content-box content-box, + linear-gradient(black, black); + mask-composite: xor; + -webkit-mask-composite: xor; + z-index: 9999; } .no-scrollbar::-webkit-scrollbar { - display: none; + display: none; } .no-scrollbar { - -ms-overflow-style: none; - scrollbar-width: none; + -ms-overflow-style: none; + scrollbar-width: none; } .mask-fade-out { - mask-image: linear-gradient( - to bottom, - black calc(100% - 40px), - transparent 100% - ); - -webkit-mask-image: linear-gradient( - to bottom, - black calc(100% - 40px), - transparent 100% - ); + mask-image: linear-gradient( + to bottom, + black calc(100% - 40px), + transparent 100% + ); + -webkit-mask-image: linear-gradient( + to bottom, + black calc(100% - 40px), + transparent 100% + ); } body { - margin: 0; - font-family: - -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", - "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; + margin: 0; + font-family: + -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", + "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } diff --git a/apps/tauri/src/keybinds.ts b/apps/tauri/src/keybinds.ts index feb01c4c0..5d0563964 100644 --- a/apps/tauri/src/keybinds.ts +++ b/apps/tauri/src/keybinds.ts @@ -1,8 +1,8 @@ -import { listen, type UnlistenFn } from '@tauri-apps/api/event'; -import { invoke } from '@tauri-apps/api/core'; +import { invoke } from "@tauri-apps/api/core"; +import { listen, type UnlistenFn } from "@tauri-apps/api/event"; interface KeybindEvent { - id: string; + id: string; } type KeybindHandler = () => void | Promise; @@ -13,166 +13,178 @@ let clipboardUnlisten: UnlistenFn | null = null; // Check if an input element is currently focused function isInputFocused(): boolean { - const activeElement = document.activeElement; - console.log('[Clipboard] Active element:', { - element: activeElement, - tagName: activeElement?.tagName, - type: (activeElement as HTMLInputElement)?.type, - contenteditable: activeElement?.getAttribute('contenteditable') - }); + const activeElement = document.activeElement; + console.log("[Clipboard] Active element:", { + element: activeElement, + tagName: activeElement?.tagName, + type: (activeElement as HTMLInputElement)?.type, + contenteditable: activeElement?.getAttribute("contenteditable"), + }); - if (!activeElement) { - console.log('[Clipboard] No active element'); - return false; - } + if (!activeElement) { + console.log("[Clipboard] No active element"); + return false; + } - const tagName = activeElement.tagName.toLowerCase(); - if (tagName === 'input' || tagName === 'textarea' || tagName === 'select') { - console.log('[Clipboard] Input element focused:', tagName); - return true; - } + const tagName = activeElement.tagName.toLowerCase(); + if (tagName === "input" || tagName === "textarea" || tagName === "select") { + console.log("[Clipboard] Input element focused:", tagName); + return true; + } - // Check for contenteditable - if (activeElement.getAttribute('contenteditable') === 'true') { - console.log('[Clipboard] Contenteditable element focused'); - return true; - } + // Check for contenteditable + if (activeElement.getAttribute("contenteditable") === "true") { + console.log("[Clipboard] Contenteditable element focused"); + return true; + } - console.log('[Clipboard] Non-input element focused:', tagName); - return false; + console.log("[Clipboard] Non-input element focused:", tagName); + return false; } // Execute native clipboard operation (for text inputs) -function executeNativeClipboard(action: 'copy' | 'cut' | 'paste'): void { - console.log(`[Clipboard] Executing native ${action} operation`); - try { - // Use execCommand for compatibility (deprecated but still works) - const result = document.execCommand(action); - console.log(`[Clipboard] execCommand('${action}') result:`, result); - } catch (err) { - console.error(`[Clipboard] Failed to execute native ${action}:`, err); - } +function executeNativeClipboard(action: "copy" | "cut" | "paste"): void { + console.log(`[Clipboard] Executing native ${action} operation`); + try { + // Use execCommand for compatibility (deprecated but still works) + const result = document.execCommand(action); + console.log(`[Clipboard] execCommand('${action}') result:`, result); + } catch (err) { + console.error(`[Clipboard] Failed to execute native ${action}:`, err); + } } // Initialize Tauri keybind listener export async function initializeKeybindHandler(): Promise { - // Only initialize once - if (eventUnlisten !== null) return; + // Only initialize once + if (eventUnlisten !== null) return; - // Listen for keybind events from Rust - eventUnlisten = await listen('keybind-triggered', async (event) => { - const handler = keybindHandlers.get(event.payload.id); - if (handler) { - try { - await handler(); - } catch (err) { - console.error(`[Keybind] Handler error for ${event.payload.id}:`, err); - } - } - }); + // Listen for keybind events from Rust + eventUnlisten = await listen( + "keybind-triggered", + async (event) => { + const handler = keybindHandlers.get(event.payload.id); + if (handler) { + try { + await handler(); + } catch (err) { + console.error( + `[Keybind] Handler error for ${event.payload.id}:`, + err + ); + } + } + } + ); - // Listen for clipboard actions from native menu - clipboardUnlisten = await listen('clipboard-action', async (event) => { - const action = event.payload as 'copy' | 'cut' | 'paste'; - console.log(`[Clipboard] Received clipboard-action event:`, action); + // Listen for clipboard actions from native menu + clipboardUnlisten = await listen( + "clipboard-action", + async (event) => { + const action = event.payload as "copy" | "cut" | "paste"; + console.log("[Clipboard] Received clipboard-action event:", action); - // Check if an input is focused - if (isInputFocused()) { - // Execute native browser clipboard operation - console.log('[Clipboard] Input focused, executing native operation'); - executeNativeClipboard(action); - } else { - // Trigger file operation via keybind system - const keybindId = `explorer.${action}`; - console.log('[Clipboard] No input focused, triggering file operation:', keybindId); - const handler = keybindHandlers.get(keybindId); - if (handler) { - try { - await handler(); - console.log(`[Clipboard] File operation ${keybindId} completed`); - } catch (err) { - console.error(`[Clipboard] Handler error for ${keybindId}:`, err); - } - } else { - console.warn(`[Clipboard] No handler registered for ${keybindId}`); - } - } - }); + // Check if an input is focused + if (isInputFocused()) { + // Execute native browser clipboard operation + console.log("[Clipboard] Input focused, executing native operation"); + executeNativeClipboard(action); + } else { + // Trigger file operation via keybind system + const keybindId = `explorer.${action}`; + console.log( + "[Clipboard] No input focused, triggering file operation:", + keybindId + ); + const handler = keybindHandlers.get(keybindId); + if (handler) { + try { + await handler(); + console.log(`[Clipboard] File operation ${keybindId} completed`); + } catch (err) { + console.error(`[Clipboard] Handler error for ${keybindId}:`, err); + } + } else { + console.warn(`[Clipboard] No handler registered for ${keybindId}`); + } + } + } + ); - console.log('[Clipboard] Action listener initialized'); + console.log("[Clipboard] Action listener initialized"); - console.log('[Keybind] Handler initialized'); + console.log("[Keybind] Handler initialized"); } // Register a keybind with Tauri export async function registerTauriKeybind( - id: string, - accelerator: string, - handler: KeybindHandler + id: string, + accelerator: string, + handler: KeybindHandler ): Promise { - keybindHandlers.set(id, handler); + keybindHandlers.set(id, handler); - try { - await invoke('register_keybind', { - id, - accelerator - }); - console.log(`[Keybind] Registered: ${id} (${accelerator})`); - } catch (error) { - console.error(`[Keybind] Failed to register ${id}:`, error); - // Keep the handler registered for web fallback - } + try { + await invoke("register_keybind", { + id, + accelerator, + }); + console.log(`[Keybind] Registered: ${id} (${accelerator})`); + } catch (error) { + console.error(`[Keybind] Failed to register ${id}:`, error); + // Keep the handler registered for web fallback + } } // Unregister a keybind export async function unregisterTauriKeybind(id: string): Promise { - keybindHandlers.delete(id); + keybindHandlers.delete(id); - try { - await invoke('unregister_keybind', { id }); - console.log(`[Keybind] Unregistered: ${id}`); - } catch (error) { - console.error(`[Keybind] Failed to unregister ${id}:`, error); - } + try { + await invoke("unregister_keybind", { id }); + console.log(`[Keybind] Unregistered: ${id}`); + } catch (error) { + console.error(`[Keybind] Failed to unregister ${id}:`, error); + } } // Cleanup function export async function cleanupKeybindHandler(): Promise { - if (eventUnlisten) { - eventUnlisten(); - eventUnlisten = null; - } + if (eventUnlisten) { + eventUnlisten(); + eventUnlisten = null; + } - if (clipboardUnlisten) { - clipboardUnlisten(); - clipboardUnlisten = null; - } + if (clipboardUnlisten) { + clipboardUnlisten(); + clipboardUnlisten = null; + } - // Unregister all keybinds - const ids = Array.from(keybindHandlers.keys()); - for (const id of ids) { - try { - await invoke('unregister_keybind', { id }); - } catch { - // Ignore errors during cleanup - } - } - keybindHandlers.clear(); + // Unregister all keybinds + const ids = Array.from(keybindHandlers.keys()); + for (const id of ids) { + try { + await invoke("unregister_keybind", { id }); + } catch { + // Ignore errors during cleanup + } + } + keybindHandlers.clear(); - console.log('[Keybind] Handler cleaned up'); + console.log("[Keybind] Handler cleaned up"); } // Initialize keybind handler on window global (same pattern as context menu) export function initializeKeybindGlobal(): void { - if (!window.__SPACEDRIVE__) { - (window as any).__SPACEDRIVE__ = {}; - } + if (!window.__SPACEDRIVE__) { + (window as any).__SPACEDRIVE__ = {}; + } - window.__SPACEDRIVE__.registerKeybind = registerTauriKeybind; - window.__SPACEDRIVE__.unregisterKeybind = unregisterTauriKeybind; + window.__SPACEDRIVE__.registerKeybind = registerTauriKeybind; + window.__SPACEDRIVE__.unregisterKeybind = unregisterTauriKeybind; - // Initialize the event listener - initializeKeybindHandler().catch(console.error); + // Initialize the event listener + initializeKeybindHandler().catch(console.error); - console.log('[Keybind] Global handlers initialized'); + console.log("[Keybind] Global handlers initialized"); } diff --git a/apps/tauri/src/lib/drag.ts b/apps/tauri/src/lib/drag.ts index 41e8609fa..177ae46f1 100644 --- a/apps/tauri/src/lib/drag.ts +++ b/apps/tauri/src/lib/drag.ts @@ -1,18 +1,18 @@ -import { invoke } from '@tauri-apps/api/core'; -import { listen, type UnlistenFn } from '@tauri-apps/api/event'; +import { invoke } from "@tauri-apps/api/core"; +import { listen, type UnlistenFn } from "@tauri-apps/api/event"; // Types matching Rust definitions (lowercase to match serde rename_all = "camelCase") export type DragItemKind = - | { type: 'file'; path: string } - | { type: 'filePromise'; name: string; mimeType: string } - | { type: 'text'; content: string }; + | { type: "file"; path: string } + | { type: "filePromise"; name: string; mimeType: string } + | { type: "text"; content: string }; export interface DragItem { kind: DragItemKind; id: string; } -export type DragOperation = 'copy' | 'move' | 'link'; +export type DragOperation = "copy" | "move" | "link"; export interface DragConfig { items: DragItem[]; @@ -29,9 +29,9 @@ export interface DragSession { } export type DragResult = - | { type: 'Dropped'; operation: DragOperation; target?: string } - | { type: 'Cancelled' } - | { type: 'Failed'; error: string }; + | { type: "Dropped"; operation: DragOperation; target?: string } + | { type: "Cancelled" } + | { type: "Failed"; error: string }; // Event types export interface DragBeganEvent { @@ -67,37 +67,37 @@ export async function beginDrag( config: DragConfig, sourceWindowLabel: string ): Promise { - return await invoke('begin_drag', { config, sourceWindowLabel }); + return await invoke("begin_drag", { config, sourceWindowLabel }); } export async function endDrag( sessionId: string, result: DragResult ): Promise { - return await invoke('end_drag', { sessionId, result }); + return await invoke("end_drag", { sessionId, result }); } export async function getDragSession(): Promise { - return await invoke('get_drag_session'); + return await invoke("get_drag_session"); } // Event listeners export async function onDragBegan( handler: (event: DragBeganEvent) => void ): Promise { - return await listen('drag:began', (e) => handler(e.payload)); + return await listen("drag:began", (e) => handler(e.payload)); } export async function onDragMoved( handler: (event: DragMoveEvent) => void ): Promise { - return await listen('drag:moved', (e) => handler(e.payload)); + return await listen("drag:moved", (e) => handler(e.payload)); } export async function onDragEntered( handler: (event: DragWindowEvent) => void ): Promise { - return await listen('drag:entered', (e) => + return await listen("drag:entered", (e) => handler(e.payload) ); } @@ -105,11 +105,11 @@ export async function onDragEntered( export async function onDragLeft( handler: (event: DragWindowEvent) => void ): Promise { - return await listen('drag:left', (e) => handler(e.payload)); + return await listen("drag:left", (e) => handler(e.payload)); } export async function onDragEnded( handler: (event: DragEndEvent) => void ): Promise { - return await listen('drag:ended', (e) => handler(e.payload)); + return await listen("drag:ended", (e) => handler(e.payload)); } diff --git a/apps/tauri/src/main.tsx b/apps/tauri/src/main.tsx index bd6261abe..542b2ffbc 100644 --- a/apps/tauri/src/main.tsx +++ b/apps/tauri/src/main.tsx @@ -1,13 +1,13 @@ -import { ErrorBoundary } from '@sd/interface'; -import React from 'react'; -import ReactDOM from 'react-dom/client'; -import App from './App'; -import './index.css'; +import { ErrorBoundary } from "@sd/interface"; +import React from "react"; +import ReactDOM from "react-dom/client"; +import App from "./App"; +import "./index.css"; -ReactDOM.createRoot(document.getElementById('root')!).render( - - - - - +ReactDOM.createRoot(document.getElementById("root")!).render( + + + + + ); diff --git a/apps/tauri/src/platform.ts b/apps/tauri/src/platform.ts index 80019a0df..c4ad3b2fa 100644 --- a/apps/tauri/src/platform.ts +++ b/apps/tauri/src/platform.ts @@ -1,10 +1,20 @@ -import { open, save } from "@tauri-apps/plugin-dialog"; -import { open as shellOpen } from "@tauri-apps/plugin-shell"; -import { convertFileSrc as tauriConvertFileSrc, invoke } from "@tauri-apps/api/core"; +import type { Platform } from "@sd/interface/platform"; +import { + invoke, + convertFileSrc as tauriConvertFileSrc, +} from "@tauri-apps/api/core"; import { listen } from "@tauri-apps/api/event"; import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow"; -import type { Platform } from "@sd/interface/platform"; -import { beginDrag, onDragBegan, onDragMoved, onDragEntered, onDragLeft, onDragEnded } from "./lib/drag"; +import { open, save } from "@tauri-apps/plugin-dialog"; +import { open as shellOpen } from "@tauri-apps/plugin-shell"; +import { + beginDrag, + onDragBegan, + onDragEnded, + onDragEntered, + onDragLeft, + onDragMoved, +} from "./lib/drag"; let _isDragging = false; @@ -12,285 +22,288 @@ let _isDragging = false; * Tauri platform implementation */ export const platform: Platform = { - platform: "tauri", + platform: "tauri", - async openDirectoryPickerDialog(opts) { - const result = await open({ - directory: true, - multiple: opts?.multiple ?? false, - title: opts?.title ?? "Choose a folder", - }); + async openDirectoryPickerDialog(opts) { + const result = await open({ + directory: true, + multiple: opts?.multiple ?? false, + title: opts?.title ?? "Choose a folder", + }); - return result; - }, + return result; + }, - async openFilePickerDialog(opts) { - const result = await open({ - directory: false, - multiple: opts?.multiple ?? false, - title: opts?.title ?? "Choose a file", - }); + async openFilePickerDialog(opts) { + const result = await open({ + directory: false, + multiple: opts?.multiple ?? false, + title: opts?.title ?? "Choose a file", + }); - return result; - }, + return result; + }, - async saveFilePickerDialog(opts) { - const result = await save({ - title: opts?.title ?? "Save file", - defaultPath: opts?.defaultPath, - }); + async saveFilePickerDialog(opts) { + const result = await save({ + title: opts?.title ?? "Save file", + defaultPath: opts?.defaultPath, + }); - return result; - }, + return result; + }, - openLink(url: string) { - shellOpen(url); - }, + openLink(url: string) { + shellOpen(url); + }, - confirm(message: string, callback: (result: boolean) => void) { - // Use browser confirm for now - could be replaced with custom dialog - callback(window.confirm(message)); - }, + confirm(message: string, callback: (result: boolean) => void) { + // Use browser confirm for now - could be replaced with custom dialog + callback(window.confirm(message)); + }, - convertFileSrc(filePath: string) { - return tauriConvertFileSrc(filePath); - }, + convertFileSrc(filePath: string) { + return tauriConvertFileSrc(filePath); + }, - async revealFile(filePath: string) { - await invoke("reveal_file", { path: filePath }); - }, + async revealFile(filePath: string) { + await invoke("reveal_file", { path: filePath }); + }, - async getAppsForPaths(paths: string[]) { - return await invoke>( - "get_apps_for_paths", - { paths } - ); - }, + async getAppsForPaths(paths: string[]) { + return await invoke>( + "get_apps_for_paths", + { paths } + ); + }, - async openPathDefault(path: string) { - return await invoke< - | { status: "success" } - | { status: "file_not_found"; path: string } - | { status: "app_not_found"; app_id: string } - | { status: "permission_denied"; path: string } - | { status: "platform_error"; message: string } - >("open_path_default", { path }); - }, + async openPathDefault(path: string) { + return await invoke< + | { status: "success" } + | { status: "file_not_found"; path: string } + | { status: "app_not_found"; app_id: string } + | { status: "permission_denied"; path: string } + | { status: "platform_error"; message: string } + >("open_path_default", { path }); + }, - async openPathWithApp(path: string, appId: string) { - return await invoke< - | { status: "success" } - | { status: "file_not_found"; path: string } - | { status: "app_not_found"; app_id: string } - | { status: "permission_denied"; path: string } - | { status: "platform_error"; message: string } - >("open_path_with_app", { path, appId }); - }, + async openPathWithApp(path: string, appId: string) { + return await invoke< + | { status: "success" } + | { status: "file_not_found"; path: string } + | { status: "app_not_found"; app_id: string } + | { status: "permission_denied"; path: string } + | { status: "platform_error"; message: string } + >("open_path_with_app", { path, appId }); + }, - async openPathsWithApp(paths: string[], appId: string) { - return await invoke< - Array< - | { status: "success" } - | { status: "file_not_found"; path: string } - | { status: "app_not_found"; app_id: string } - | { status: "permission_denied"; path: string } - | { status: "platform_error"; message: string } - > - >("open_paths_with_app", { paths, appId }); - }, + async openPathsWithApp(paths: string[], appId: string) { + return await invoke< + Array< + | { status: "success" } + | { status: "file_not_found"; path: string } + | { status: "app_not_found"; app_id: string } + | { status: "permission_denied"; path: string } + | { status: "platform_error"; message: string } + > + >("open_paths_with_app", { paths, appId }); + }, - async getSidecarPath( - libraryId: string, - contentUuid: string, - kind: string, - variant: string, - format: string - ) { - return await invoke("get_sidecar_path", { - libraryId, - contentUuid, - kind, - variant, - format, - }); - }, + async getSidecarPath( + libraryId: string, + contentUuid: string, + kind: string, + variant: string, + format: string + ) { + return await invoke("get_sidecar_path", { + libraryId, + contentUuid, + kind, + variant, + format, + }); + }, - async updateMenuItems(items) { - await invoke("update_menu_items", { items }); - }, + async updateMenuItems(items) { + await invoke("update_menu_items", { items }); + }, - async getCurrentLibraryId() { - try { - return await invoke("get_current_library_id"); - } catch { - return null; - } - }, + async getCurrentLibraryId() { + try { + return await invoke("get_current_library_id"); + } catch { + return null; + } + }, - async setCurrentLibraryId(libraryId: string) { - await invoke("set_current_library_id", { libraryId }); - }, + async setCurrentLibraryId(libraryId: string) { + await invoke("set_current_library_id", { libraryId }); + }, - async onLibraryIdChanged(callback: (libraryId: string) => void) { - const unlisten = await listen("library-changed", (event) => { - callback(event.payload); - }); - return unlisten; - }, + async onLibraryIdChanged(callback: (libraryId: string) => void) { + const unlisten = await listen("library-changed", (event) => { + callback(event.payload); + }); + return unlisten; + }, - async showWindow(window: any) { - await invoke("show_window", { window }); - }, + async showWindow(window: any) { + await invoke("show_window", { window }); + }, - async closeWindow(label: string) { - await invoke("close_window", { label }); - }, + async closeWindow(label: string) { + await invoke("close_window", { label }); + }, - async onWindowEvent(event: string, callback: () => void) { - const unlisten = await listen(event, () => { - callback(); - }); - return unlisten; - }, + async onWindowEvent(event: string, callback: () => void) { + const unlisten = await listen(event, () => { + callback(); + }); + return unlisten; + }, - getCurrentWindowLabel() { - const window = getCurrentWebviewWindow(); - return window.label; - }, + getCurrentWindowLabel() { + const window = getCurrentWebviewWindow(); + return window.label; + }, - async closeCurrentWindow() { - const window = getCurrentWebviewWindow(); - await window.close(); - }, + async closeCurrentWindow() { + const window = getCurrentWebviewWindow(); + await window.close(); + }, - async getSelectedFileIds() { - return await invoke("get_selected_file_ids"); - }, + async getSelectedFileIds() { + return await invoke("get_selected_file_ids"); + }, - async setSelectedFileIds(fileIds: string[]) { - await invoke("set_selected_file_ids", { fileIds }); - }, + async setSelectedFileIds(fileIds: string[]) { + await invoke("set_selected_file_ids", { fileIds }); + }, - async onSelectedFilesChanged(callback: (fileIds: string[]) => void) { - const unlisten = await listen("selected-files-changed", (event) => { - callback(event.payload); - }); - return unlisten; - }, + async onSelectedFilesChanged(callback: (fileIds: string[]) => void) { + const unlisten = await listen( + "selected-files-changed", + (event) => { + callback(event.payload); + } + ); + return unlisten; + }, - async getAppVersion() { - const { getVersion } = await import("@tauri-apps/api/app"); - return await getVersion(); - }, + async getAppVersion() { + const { getVersion } = await import("@tauri-apps/api/app"); + return await getVersion(); + }, - async getDaemonStatus() { - return await invoke<{ - is_running: boolean; - socket_path: string; - server_url: string | null; - started_by_us: boolean; - }>("get_daemon_status"); - }, + async getDaemonStatus() { + return await invoke<{ + is_running: boolean; + socket_path: string; + server_url: string | null; + started_by_us: boolean; + }>("get_daemon_status"); + }, - async startDaemonProcess() { - await invoke("start_daemon_process"); - }, + async startDaemonProcess() { + await invoke("start_daemon_process"); + }, - async stopDaemonProcess() { - await invoke("stop_daemon_process"); - }, + async stopDaemonProcess() { + await invoke("stop_daemon_process"); + }, - async onDaemonConnected(callback: () => void) { - const unlisten = await listen("daemon-connected", () => { - callback(); - }); - return unlisten; - }, + async onDaemonConnected(callback: () => void) { + const unlisten = await listen("daemon-connected", () => { + callback(); + }); + return unlisten; + }, - async onDaemonDisconnected(callback: () => void) { - const unlisten = await listen("daemon-disconnected", () => { - callback(); - }); - return unlisten; - }, + async onDaemonDisconnected(callback: () => void) { + const unlisten = await listen("daemon-disconnected", () => { + callback(); + }); + return unlisten; + }, - async onDaemonStarting(callback: () => void) { - const unlisten = await listen("daemon-starting", () => { - callback(); - }); - return unlisten; - }, + async onDaemonStarting(callback: () => void) { + const unlisten = await listen("daemon-starting", () => { + callback(); + }); + return unlisten; + }, - async checkDaemonInstalled() { - return await invoke("check_daemon_installed"); - }, + async checkDaemonInstalled() { + return await invoke("check_daemon_installed"); + }, - async installDaemonService() { - await invoke("install_daemon_service"); - }, + async installDaemonService() { + await invoke("install_daemon_service"); + }, - async uninstallDaemonService() { - await invoke("uninstall_daemon_service"); - }, + async uninstallDaemonService() { + await invoke("uninstall_daemon_service"); + }, - async openMacOSSettings() { - await invoke("open_macos_settings"); - }, + async openMacOSSettings() { + await invoke("open_macos_settings"); + }, - async startDrag(config) { - const currentWindow = getCurrentWebviewWindow(); - const sessionId = await beginDrag( - { - items: config.items.map(item => ({ - id: item.id, - kind: item.kind, - })), - overlayUrl: "/drag-overlay", - overlaySize: [200, 150], - allowedOperations: config.allowedOperations, - }, - currentWindow.label - ); - _isDragging = true; - return sessionId; - }, + async startDrag(config) { + const currentWindow = getCurrentWebviewWindow(); + const sessionId = await beginDrag( + { + items: config.items.map((item) => ({ + id: item.id, + kind: item.kind, + })), + overlayUrl: "/drag-overlay", + overlaySize: [200, 150], + allowedOperations: config.allowedOperations, + }, + currentWindow.label + ); + _isDragging = true; + return sessionId; + }, - async onDragEvent(event, callback) { - const handlers: Record = { - began: onDragBegan, - moved: onDragMoved, - entered: onDragEntered, - left: onDragLeft, - ended: onDragEnded, - }; - const handler = handlers[event]; - if (!handler) { - throw new Error(`Unknown drag event: ${event}`); - } - const unlisten = await handler((payload: any) => { - if (event === "ended") { - _isDragging = false; - } - callback(payload); - }); - return unlisten; - }, + async onDragEvent(event, callback) { + const handlers: Record = { + began: onDragBegan, + moved: onDragMoved, + entered: onDragEntered, + left: onDragLeft, + ended: onDragEnded, + }; + const handler = handlers[event]; + if (!handler) { + throw new Error(`Unknown drag event: ${event}`); + } + const unlisten = await handler((payload: any) => { + if (event === "ended") { + _isDragging = false; + } + callback(payload); + }); + return unlisten; + }, - isDragging() { - return _isDragging; - }, + isDragging() { + return _isDragging; + }, - async registerKeybind(id, accelerator, handler) { - // Use the global handler if available (initialized in keybinds.ts) - if (window.__SPACEDRIVE__?.registerKeybind) { - await window.__SPACEDRIVE__.registerKeybind(id, accelerator, handler); - } - }, + async registerKeybind(id, accelerator, handler) { + // Use the global handler if available (initialized in keybinds.ts) + if (window.__SPACEDRIVE__?.registerKeybind) { + await window.__SPACEDRIVE__.registerKeybind(id, accelerator, handler); + } + }, - async unregisterKeybind(id) { - // Use the global handler if available (initialized in keybinds.ts) - if (window.__SPACEDRIVE__?.unregisterKeybind) { - await window.__SPACEDRIVE__.unregisterKeybind(id); - } - }, + async unregisterKeybind(id) { + // Use the global handler if available (initialized in keybinds.ts) + if (window.__SPACEDRIVE__?.unregisterKeybind) { + await window.__SPACEDRIVE__.unregisterKeybind(id); + } + }, }; diff --git a/apps/tauri/src/routes/ContextMenuWindow.tsx b/apps/tauri/src/routes/ContextMenuWindow.tsx index cfdef47b0..e6ba6d11a 100644 --- a/apps/tauri/src/routes/ContextMenuWindow.tsx +++ b/apps/tauri/src/routes/ContextMenuWindow.tsx @@ -1,155 +1,160 @@ +import { ContextMenu } from "@sd/ui"; import { invoke } from "@tauri-apps/api/core"; import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow"; -import { ContextMenu } from "@sd/ui"; import { useEffect, useRef, useState } from "react"; export interface MenuItem { - type?: "separator"; - icon?: React.ElementType; - label?: string; - onClick?: () => void; - keybind?: string; - variant?: "default" | "dull" | "danger"; - disabled?: boolean; - submenu?: MenuItem[]; + type?: "separator"; + icon?: React.ElementType; + label?: string; + onClick?: () => void; + keybind?: string; + variant?: "default" | "dull" | "danger"; + disabled?: boolean; + submenu?: MenuItem[]; } export interface ContextMenuData { - items: MenuItem[]; - x: number; - y: number; + items: MenuItem[]; + x: number; + y: number; } export function ContextMenuWindow() { - const [items, setItems] = useState([]); - const [contextId, setContextId] = useState(null); - const menuRef = useRef(null); - const window = getCurrentWebviewWindow(); + const [items, setItems] = useState([]); + const [contextId, setContextId] = useState(null); + const menuRef = useRef(null); + const window = getCurrentWebviewWindow(); - useEffect(() => { - console.log('[ContextMenuWindow] Component mounted'); - console.log('[ContextMenuWindow] Window location:', window.location.href); + useEffect(() => { + console.log("[ContextMenuWindow] Component mounted"); + console.log("[ContextMenuWindow] Window location:", window.location.href); - // Extract context ID from URL params - const params = new URLSearchParams(window.location.search); - const id = params.get("context"); - console.log('[ContextMenuWindow] Context ID from params:', id); - console.log('[ContextMenuWindow] All params:', Array.from(params.entries())); - setContextId(id); + // Extract context ID from URL params + const params = new URLSearchParams(window.location.search); + const id = params.get("context"); + console.log("[ContextMenuWindow] Context ID from params:", id); + console.log( + "[ContextMenuWindow] All params:", + Array.from(params.entries()) + ); + setContextId(id); - if (!id) { - console.error("[ContextMenuWindow] No context ID provided"); - return; - } + if (!id) { + console.error("[ContextMenuWindow] No context ID provided"); + return; + } - // Listen for menu data event - const setupMenu = async () => { - console.log('[ContextMenuWindow] Setting up menu listener...'); - const { listen } = await import("@tauri-apps/api/event"); + // Listen for menu data event + const setupMenu = async () => { + console.log("[ContextMenuWindow] Setting up menu listener..."); + const { listen } = await import("@tauri-apps/api/event"); - const eventName = `context-menu-data-${id}`; - console.log('[ContextMenuWindow] Listening for event:', eventName); + const eventName = `context-menu-data-${id}`; + console.log("[ContextMenuWindow] Listening for event:", eventName); - const unlisten = await listen( - eventName, - (event) => { - console.log('[ContextMenuWindow] Received menu data:', event.payload); - const data = event.payload; - setItems(data.items); + const unlisten = await listen(eventName, (event) => { + console.log("[ContextMenuWindow] Received menu data:", event.payload); + const data = event.payload; + setItems(data.items); - // Measure actual size and adjust window after render - requestAnimationFrame(() => { - if (menuRef.current) { - const { width, height } = menuRef.current.getBoundingClientRect(); - console.log('[ContextMenuWindow] Positioning menu:', { width, height, x: data.x, y: data.y }); + // Measure actual size and adjust window after render + requestAnimationFrame(() => { + if (menuRef.current) { + const { width, height } = menuRef.current.getBoundingClientRect(); + console.log("[ContextMenuWindow] Positioning menu:", { + width, + height, + x: data.x, + y: data.y, + }); - // Position the menu at the cursor - invoke("position_context_menu", { - label: window.label, - x: data.x, - y: data.y, - menuWidth: width, - menuHeight: height, - }).catch(console.error); - } - }); - } - ); + // Position the menu at the cursor + invoke("position_context_menu", { + label: window.label, + x: data.x, + y: data.y, + menuWidth: width, + menuHeight: height, + }).catch(console.error); + } + }); + }); - console.log('[ContextMenuWindow] Listener set up successfully'); - return unlisten; - }; + console.log("[ContextMenuWindow] Listener set up successfully"); + return unlisten; + }; - setupMenu(); + setupMenu(); - // Close on blur (when clicking outside) - const handleBlur = async () => { - invoke("close_window", { label: window.label }).catch(console.error); - }; + // Close on blur (when clicking outside) + const handleBlur = async () => { + invoke("close_window", { label: window.label }).catch(console.error); + }; - window.listen("tauri://blur", handleBlur); + window.listen("tauri://blur", handleBlur); - return () => { - // Cleanup handled by Tauri - }; - }, []); + return () => { + // Cleanup handled by Tauri + }; + }, []); - const handleItemClick = (item: MenuItem) => { - if (item.onClick && !item.disabled) { - item.onClick(); - } - // Close menu after click - invoke("close_window", { label: window.label }).catch(console.error); - }; + const handleItemClick = (item: MenuItem) => { + if (item.onClick && !item.disabled) { + item.onClick(); + } + // Close menu after click + invoke("close_window", { label: window.label }).catch(console.error); + }; - const renderItem = (item: MenuItem, index: number) => { - if (item.type === "separator") { - return ; - } + const renderItem = (item: MenuItem, index: number) => { + if (item.type === "separator") { + return ; + } - if (item.submenu) { - return ( - - {item.submenu.map((sub, subIndex) => renderItem(sub, subIndex))} - - ); - } + if (item.submenu) { + return ( + + {item.submenu.map((sub, subIndex) => renderItem(sub, subIndex))} + + ); + } - return ( - handleItemClick(item)} - /> - ); - }; + return ( + handleItemClick(item)} + variant={item.variant} + /> + ); + }; - // Don't render until we have items - if (items.length === 0) { - return null; - } + // Don't render until we have items + if (items.length === 0) { + return null; + } - return ( -
-
- {items.map((item, index) => renderItem(item, index))} -
-
- ); + return ( +
+
+ {items.map((item, index) => renderItem(item, index))} +
+
+ ); } diff --git a/apps/tauri/src/routes/DragOverlay.tsx b/apps/tauri/src/routes/DragOverlay.tsx index 6090062e0..24dcaa2a1 100644 --- a/apps/tauri/src/routes/DragOverlay.tsx +++ b/apps/tauri/src/routes/DragOverlay.tsx @@ -1,5 +1,5 @@ -import { useEffect, useState } from 'react'; -import { getDragSession, onDragMoved, type DragSession } from '../lib/drag'; +import { useEffect, useState } from "react"; +import { type DragSession, getDragSession, onDragMoved } from "../lib/drag"; export function DragOverlay() { const [session, setSession] = useState(null); @@ -8,7 +8,7 @@ export function DragOverlay() { useEffect(() => { // Get the session from query params const params = new URLSearchParams(window.location.search); - const sessionId = params.get('session'); + const sessionId = params.get("session"); if (sessionId) { getDragSession().then((s) => setSession(s)); @@ -30,18 +30,18 @@ export function DragOverlay() { const itemCount = session.config.items.length; return ( -
-
+
+
-
+
- {itemCount} {itemCount === 1 ? 'file' : 'files'} + {itemCount} {itemCount === 1 ? "file" : "files"}
-
- {session.config.items[0]?.kind.type === 'file' - ? session.config.items[0].kind.path.split('/').pop() - : 'Dragging...'} +
+ {session.config.items[0]?.kind.type === "file" + ? session.config.items[0].kind.path.split("/").pop() + : "Dragging..."}
diff --git a/apps/tauri/src/routes/Spacedrop.tsx b/apps/tauri/src/routes/Spacedrop.tsx index eb480b5f4..9219336be 100644 --- a/apps/tauri/src/routes/Spacedrop.tsx +++ b/apps/tauri/src/routes/Spacedrop.tsx @@ -1,20 +1,20 @@ -import { Spacedrop } from '@sd/interface'; +import { Spacedrop } from "@sd/interface"; const samplePeople = [ - { id: '1', name: 'Jamie', initials: 'JP', status: 'online' as const }, - { id: '2', name: 'Alex', initials: 'AB', status: 'online' as const }, - { id: '3', name: 'Sam', initials: 'SC', status: 'offline' as const }, - { id: '4', name: 'Morgan', initials: 'MJ', status: 'online' as const }, - { id: '5', name: 'Taylor', initials: 'TW', status: 'online' as const }, - { id: '6', name: 'Jordan', initials: 'JK', status: 'offline' as const }, - { id: '7', name: 'Casey', initials: 'CD', status: 'online' as const }, - { id: '8', name: 'Riley', initials: 'RM', status: 'online' as const } + { id: "1", name: "Jamie", initials: "JP", status: "online" as const }, + { id: "2", name: "Alex", initials: "AB", status: "online" as const }, + { id: "3", name: "Sam", initials: "SC", status: "offline" as const }, + { id: "4", name: "Morgan", initials: "MJ", status: "online" as const }, + { id: "5", name: "Taylor", initials: "TW", status: "online" as const }, + { id: "6", name: "Jordan", initials: "JK", status: "offline" as const }, + { id: "7", name: "Casey", initials: "CD", status: "online" as const }, + { id: "8", name: "Riley", initials: "RM", status: "online" as const }, ]; export function SpacedropWindow() { - return ( -
- window.close()} /> -
- ); + return ( +
+ window.close()} people={samplePeople} /> +
+ ); } diff --git a/apps/tauri/tailwind.config.cjs b/apps/tauri/tailwind.config.cjs index 7f6023188..0158e4be8 100644 --- a/apps/tauri/tailwind.config.cjs +++ b/apps/tauri/tailwind.config.cjs @@ -1,4 +1,5 @@ -const config = require('@sd/ui/tailwind'); +"use strict"; +const config = require("@sd/ui/tailwind"); /** @type {import('tailwindcss').Config} */ -module.exports = config('tauri'); +module.exports = config("tauri"); diff --git a/apps/tauri/tsconfig.json b/apps/tauri/tsconfig.json index 2117c1047..3fb2a1709 100644 --- a/apps/tauri/tsconfig.json +++ b/apps/tauri/tsconfig.json @@ -1,30 +1,30 @@ { - "compilerOptions": { - "target": "ES2020", - "useDefineForClassFields": true, - "lib": ["ES2020", "DOM", "DOM.Iterable"], - "module": "ESNext", - "skipLibCheck": true, + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, - /* Bundler mode */ - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "resolveJsonModule": true, - "isolatedModules": true, - "noEmit": true, - "jsx": "react-jsx", + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", - /* Linting */ - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true, + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, - /* Path aliases */ - "baseUrl": ".", - "paths": { - "~/*": ["./src/*"] - } - }, - "include": ["src", "vite.config.ts"] + /* Path aliases */ + "baseUrl": ".", + "paths": { + "~/*": ["./src/*"] + } + }, + "include": ["src", "vite.config.ts"] } diff --git a/apps/tauri/tsconfig.node.json b/apps/tauri/tsconfig.node.json index 5ef118721..6841fc156 100644 --- a/apps/tauri/tsconfig.node.json +++ b/apps/tauri/tsconfig.node.json @@ -1,11 +1,11 @@ { - "compilerOptions": { - "skipLibCheck": true, - "module": "ESNext", - "moduleResolution": "bundler", - "allowSyntheticDefaultImports": true, - "strict": true, - "noEmit": true - }, - "include": ["vite.config.ts"] + "compilerOptions": { + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true, + "strict": true, + "noEmit": true + }, + "include": ["vite.config.ts"] } diff --git a/apps/tauri/vite.config.ts b/apps/tauri/vite.config.ts index def3f1910..6c9bffc2f 100644 --- a/apps/tauri/vite.config.ts +++ b/apps/tauri/vite.config.ts @@ -1,47 +1,41 @@ -import { defineConfig } from "vite"; import react from "@vitejs/plugin-react-swc"; import path from "path"; +import { defineConfig } from "vite"; const COMMANDS = ["initialize_core", "core_rpc", "subscribe_events"]; export default defineConfig(async () => ({ - plugins: [react()], + plugins: [react()], - css: { - postcss: "./postcss.config.cjs", - }, + css: { + postcss: "./postcss.config.cjs", + }, - resolve: { - alias: { - "@sd/interface": path.resolve( - __dirname, - "../../packages/interface/src", - ), - "@sd/ts-client": path.resolve( - __dirname, - "../../packages/ts-client/src", - ), - "@sd/ui/style": path.resolve(__dirname, "../../packages/ui/style"), - "@sd/ui": path.resolve(__dirname, "../../packages/ui/src"), - }, - }, + resolve: { + alias: { + "@sd/interface": path.resolve(__dirname, "../../packages/interface/src"), + "@sd/ts-client": path.resolve(__dirname, "../../packages/ts-client/src"), + "@sd/ui/style": path.resolve(__dirname, "../../packages/ui/style"), + "@sd/ui": path.resolve(__dirname, "../../packages/ui/src"), + }, + }, - optimizeDeps: { - include: ["rooks"], - }, + optimizeDeps: { + include: ["rooks"], + }, - clearScreen: false, - server: { - port: 1420, - strictPort: true, - watch: { - ignored: ["**/src-tauri/**"], - }, - }, - envPrefix: ["VITE_", "TAURI_ENV_*"], - build: { - target: ["es2021", "chrome100", "safari13"], - minify: !process.env.TAURI_ENV_DEBUG ? "esbuild" : false, - sourcemap: !!process.env.TAURI_ENV_DEBUG, - }, + clearScreen: false, + server: { + port: 1420, + strictPort: true, + watch: { + ignored: ["**/src-tauri/**"], + }, + }, + envPrefix: ["VITE_", "TAURI_ENV_*"], + build: { + target: ["es2021", "chrome100", "safari13"], + minify: process.env.TAURI_ENV_DEBUG ? false : "esbuild", + sourcemap: !!process.env.TAURI_ENV_DEBUG, + }, })); diff --git a/apps/web/index.html b/apps/web/index.html index a8e400e0c..38d38247a 100644 --- a/apps/web/index.html +++ b/apps/web/index.html @@ -1,12 +1,12 @@ - - - - Spacedrive - - -
- - + + + + Spacedrive + + +
+ + diff --git a/apps/web/package.json b/apps/web/package.json index 45aea4f09..6cd4c1167 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,23 +1,23 @@ { - "name": "@sd/web", - "version": "0.1.0", - "private": true, - "type": "module", - "scripts": { - "dev": "vite", - "build": "vite build", - "preview": "vite preview" - }, - "dependencies": { - "@sd/interface": "workspace:*", - "react": "^19.0.0", - "react-dom": "^19.0.0" - }, - "devDependencies": { - "@types/react": "^19.0.0", - "@types/react-dom": "^19.0.0", - "@vitejs/plugin-react": "^4.3.4", - "typescript": "^5.7.2", - "vite": "^6.0.0" - } + "name": "@sd/web", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "@sd/interface": "workspace:*", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "@vitejs/plugin-react": "^4.3.4", + "typescript": "^5.7.2", + "vite": "^6.0.0" + } } diff --git a/apps/web/src/main.tsx b/apps/web/src/main.tsx index 1c8ff9871..482b0e207 100644 --- a/apps/web/src/main.tsx +++ b/apps/web/src/main.tsx @@ -1,7 +1,7 @@ +import { Shell } from "@sd/interface"; +import { PlatformProvider } from "@sd/interface/platform"; import React from "react"; import ReactDOM from "react-dom/client"; -import { PlatformProvider } from "@sd/interface/platform"; -import { Shell } from "@sd/interface"; import { platform } from "./platform"; import "@sd/interface/styles.css"; @@ -9,15 +9,15 @@ import "@sd/interface/styles.css"; * Web entry point for Spacedrive server interface */ function App() { - return ( - - - - ); + return ( + + + + ); } ReactDOM.createRoot(document.getElementById("root")!).render( - - - -); \ No newline at end of file + + + +); diff --git a/apps/web/src/platform.ts b/apps/web/src/platform.ts index 697c39973..ccd1e657b 100644 --- a/apps/web/src/platform.ts +++ b/apps/web/src/platform.ts @@ -7,16 +7,16 @@ import type { Platform } from "@sd/interface/platform"; * Unlike Tauri, web platform cannot access native file system or daemon state directly. */ export const platform: Platform = { - platform: "web", + platform: "web", - openLink(url: string) { - window.open(url, "_blank", "noopener,noreferrer"); - }, + openLink(url: string) { + window.open(url, "_blank", "noopener,noreferrer"); + }, - confirm(message: string, callback: (result: boolean) => void) { - callback(window.confirm(message)); - }, + confirm(message: string, callback: (result: boolean) => void) { + callback(window.confirm(message)); + }, - // Web-specific implementations (no native capabilities) - // File pickers, daemon control, etc. are not available on web + // Web-specific implementations (no native capabilities) + // File pickers, daemon control, etc. are not available on web }; diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index aca48955d..a7fc6fbf2 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -1,25 +1,25 @@ { - "compilerOptions": { - "target": "ES2020", - "useDefineForClassFields": true, - "lib": ["ES2020", "DOM", "DOM.Iterable"], - "module": "ESNext", - "skipLibCheck": true, + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, - /* Bundler mode */ - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "resolveJsonModule": true, - "isolatedModules": true, - "noEmit": true, - "jsx": "react-jsx", + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", - /* Linting */ - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true - }, - "include": ["src"], - "references": [{ "path": "./tsconfig.node.json" }] + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] } diff --git a/apps/web/tsconfig.node.json b/apps/web/tsconfig.node.json index eca66688d..ef0fd3f50 100644 --- a/apps/web/tsconfig.node.json +++ b/apps/web/tsconfig.node.json @@ -1,10 +1,11 @@ { - "compilerOptions": { - "composite": true, - "skipLibCheck": true, - "module": "ESNext", - "moduleResolution": "bundler", - "allowSyntheticDefaultImports": true - }, - "include": ["vite.config.ts"] + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true, + "strictNullChecks": true + }, + "include": ["vite.config.ts"] } diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts index 3da66324d..da3ad0e10 100644 --- a/apps/web/vite.config.ts +++ b/apps/web/vite.config.ts @@ -1,21 +1,21 @@ -import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; +import { defineConfig } from "vite"; export default defineConfig({ - plugins: [react()], - server: { - port: 3000, - proxy: { - // Proxy RPC requests to server - "/rpc": { - target: "http://localhost:8080", - changeOrigin: true, - }, - }, - }, - build: { - outDir: "dist", - emptyOutDir: true, - sourcemap: true, - }, + plugins: [react()], + server: { + port: 3000, + proxy: { + // Proxy RPC requests to server + "/rpc": { + target: "http://localhost:8080", + changeOrigin: true, + }, + }, + }, + build: { + outDir: "dist", + emptyOutDir: true, + sourcemap: true, + }, }); diff --git a/biome.jsonc b/biome.jsonc new file mode 100644 index 000000000..2451b6afa --- /dev/null +++ b/biome.jsonc @@ -0,0 +1,4 @@ +{ + "$schema": "./node_modules/@biomejs/biome/configuration_schema.json", + "extends": ["ultracite/biome/core", "ultracite/biome/react"] +} diff --git a/bun.lockb b/bun.lockb index bfae9d8e03cb974d584eef36b039d72918bdac4a..2d2afb73a002825b9f32713f3a37ec87f6af34b0 100755 GIT binary patch delta 220215 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̗Si>i1d}^tp%CO$$eXF=8X6 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

(HX34m2wZX4Rzvy~?ui_N`Sx;_KtMDKGE1E-Niv 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

    LI>g1iHNte6_?s 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 Hi~9cn6|`E} diff --git a/core/benchmarks/results/shape_large-aggregation-hdd.json b/core/benchmarks/results/shape_large-aggregation-hdd.json index 998111c4b..eea8ea179 100644 --- a/core/benchmarks/results/shape_large-aggregation-hdd.json +++ b/core/benchmarks/results/shape_large-aggregation-hdd.json @@ -20,9 +20,7 @@ "memory_total_gb": 48 }, "id": "f01681c8-bc82-4e05-a26e-eeaedc3e1239", - "location_paths": [ - "/Volumes/Seagate/benchdata/shape_large" - ], + "location_paths": ["/Volumes/Seagate/benchdata/shape_large"], "recipe_name": "shape_large", "timestamp_utc": "2025-08-10T09:46:25.346840+00:00" }, @@ -30,4 +28,4 @@ "total_gb": 11882.38 } ] -} \ No newline at end of file +} diff --git a/core/benchmarks/results/shape_large-aggregation-ssd.json b/core/benchmarks/results/shape_large-aggregation-ssd.json index 7cb6dc483..047d02a0f 100644 --- a/core/benchmarks/results/shape_large-aggregation-ssd.json +++ b/core/benchmarks/results/shape_large-aggregation-ssd.json @@ -30,4 +30,4 @@ "total_gb": 11882.38 } ] -} \ No newline at end of file +} diff --git a/core/benchmarks/results/shape_large-content_identification-hdd.json b/core/benchmarks/results/shape_large-content_identification-hdd.json index 6cb399ba4..086a5b88c 100644 --- a/core/benchmarks/results/shape_large-content_identification-hdd.json +++ b/core/benchmarks/results/shape_large-content_identification-hdd.json @@ -1,33 +1,31 @@ { - "runs": [ - { - "dirs": 16507, - "dirs_per_s": 5.647859855612961, - "durations": { - "content_s": 2922.7, - "discovery_s": 2990.7, - "processing_s": 2984.5, - "total_s": 2922.7 - }, - "errors": 0, - "files": 100000, - "files_per_s": 34.214938, - "meta": { - "hardware_label": "External HDD (Seagate)", - "host": { - "cpu_model": "Apple M3 Max", - "cpu_physical_cores": 16, - "memory_total_gb": 48 - }, - "id": "f84809a7-b9bd-4647-b724-d837643831d7", - "location_paths": [ - "/Volumes/Seagate/benchdata/shape_large" - ], - "recipe_name": "shape_large", - "timestamp_utc": "2025-08-10T10:36:35.604531+00:00" - }, - "scenario": "content-identification", - "total_gb": 11882.38 - } - ] + "runs": [ + { + "dirs": 16507, + "dirs_per_s": 5.647859855612961, + "durations": { + "content_s": 2922.7, + "discovery_s": 2990.7, + "processing_s": 2984.5, + "total_s": 2922.7 + }, + "errors": 0, + "files": 100000, + "files_per_s": 34.214938, + "meta": { + "hardware_label": "External HDD (Seagate)", + "host": { + "cpu_model": "Apple M3 Max", + "cpu_physical_cores": 16, + "memory_total_gb": 48 + }, + "id": "f84809a7-b9bd-4647-b724-d837643831d7", + "location_paths": ["/Volumes/Seagate/benchdata/shape_large"], + "recipe_name": "shape_large", + "timestamp_utc": "2025-08-10T10:36:35.604531+00:00" + }, + "scenario": "content-identification", + "total_gb": 11882.38 + } + ] } diff --git a/core/benchmarks/results/shape_large-content_identification-ssd.json b/core/benchmarks/results/shape_large-content_identification-ssd.json index 5a9156209..4a86f840e 100644 --- a/core/benchmarks/results/shape_large-content_identification-ssd.json +++ b/core/benchmarks/results/shape_large-content_identification-ssd.json @@ -30,4 +30,4 @@ "total_gb": 11882.38 } ] -} \ No newline at end of file +} diff --git a/core/benchmarks/results/shape_large-indexing_discovery-hdd.json b/core/benchmarks/results/shape_large-indexing_discovery-hdd.json index b63a0d234..72d8d3a77 100644 --- a/core/benchmarks/results/shape_large-indexing_discovery-hdd.json +++ b/core/benchmarks/results/shape_large-indexing_discovery-hdd.json @@ -20,9 +20,7 @@ "memory_total_gb": 48 }, "id": "8085806f-d7e3-4cfc-9c66-9b0ec782e3be", - "location_paths": [ - "/Volumes/Seagate/benchdata/shape_large" - ], + "location_paths": ["/Volumes/Seagate/benchdata/shape_large"], "recipe_name": "shape_large", "timestamp_utc": "2025-08-10T09:42:47.682209+00:00" }, @@ -30,4 +28,4 @@ "total_gb": 11882.38 } ] -} \ No newline at end of file +} diff --git a/core/benchmarks/results/shape_large-indexing_discovery-ssd.json b/core/benchmarks/results/shape_large-indexing_discovery-ssd.json index 489cdf5d9..ee770d4c6 100644 --- a/core/benchmarks/results/shape_large-indexing_discovery-ssd.json +++ b/core/benchmarks/results/shape_large-indexing_discovery-ssd.json @@ -30,4 +30,4 @@ "total_gb": 11882.38 } ] -} \ No newline at end of file +} diff --git a/core/benchmarks/results/shape_medium-aggregation-hdd.json b/core/benchmarks/results/shape_medium-aggregation-hdd.json index 3ffa89fa2..ce2598209 100644 --- a/core/benchmarks/results/shape_medium-aggregation-hdd.json +++ b/core/benchmarks/results/shape_medium-aggregation-hdd.json @@ -20,9 +20,7 @@ "memory_total_gb": 48 }, "id": "d209e68e-f366-44cb-8249-d6fecc68e6d8", - "location_paths": [ - "/Volumes/Seagate/benchdata/shape_medium" - ], + "location_paths": ["/Volumes/Seagate/benchdata/shape_medium"], "recipe_name": "shape_medium", "timestamp_utc": "2025-08-10T09:46:36.101659+00:00" }, @@ -30,4 +28,4 @@ "total_gb": 375.63 } ] -} \ No newline at end of file +} diff --git a/core/benchmarks/results/shape_medium-aggregation-ssd.json b/core/benchmarks/results/shape_medium-aggregation-ssd.json index 34001b9ba..6de97cad1 100644 --- a/core/benchmarks/results/shape_medium-aggregation-ssd.json +++ b/core/benchmarks/results/shape_medium-aggregation-ssd.json @@ -30,4 +30,4 @@ "total_gb": 375.63 } ] -} \ No newline at end of file +} diff --git a/core/benchmarks/results/shape_medium-content_identification-hdd.json b/core/benchmarks/results/shape_medium-content_identification-hdd.json index 33ce3b663..e1717d3d2 100644 --- a/core/benchmarks/results/shape_medium-content_identification-hdd.json +++ b/core/benchmarks/results/shape_medium-content_identification-hdd.json @@ -20,9 +20,7 @@ "memory_total_gb": 48 }, "id": "209bd316-ca8e-472e-995b-723b94a5b666", - "location_paths": [ - "/Volumes/Seagate/benchdata/shape_medium" - ], + "location_paths": ["/Volumes/Seagate/benchdata/shape_medium"], "recipe_name": "shape_medium", "timestamp_utc": "2025-08-10T10:40:35.582507+00:00" }, @@ -30,4 +28,4 @@ "total_gb": 375.63 } ] -} \ No newline at end of file +} diff --git a/core/benchmarks/results/shape_medium-content_identification-ssd.json b/core/benchmarks/results/shape_medium-content_identification-ssd.json index 4afc7e824..ad6bcedff 100644 --- a/core/benchmarks/results/shape_medium-content_identification-ssd.json +++ b/core/benchmarks/results/shape_medium-content_identification-ssd.json @@ -30,4 +30,4 @@ "total_gb": 375.63 } ] -} \ No newline at end of file +} diff --git a/core/benchmarks/results/shape_medium-indexing_discovery-hdd.json b/core/benchmarks/results/shape_medium-indexing_discovery-hdd.json index 35a4d854c..455399231 100644 --- a/core/benchmarks/results/shape_medium-indexing_discovery-hdd.json +++ b/core/benchmarks/results/shape_medium-indexing_discovery-hdd.json @@ -20,9 +20,7 @@ "memory_total_gb": 48 }, "id": "0b40d364-b963-436a-88fd-5f9f2ba9de3b", - "location_paths": [ - "/Volumes/Seagate/benchdata/shape_medium" - ], + "location_paths": ["/Volumes/Seagate/benchdata/shape_medium"], "recipe_name": "shape_medium", "timestamp_utc": "2025-08-10T09:43:15.290493+00:00" }, @@ -30,4 +28,4 @@ "total_gb": 375.63 } ] -} \ No newline at end of file +} diff --git a/core/benchmarks/results/shape_medium-indexing_discovery-ssd.json b/core/benchmarks/results/shape_medium-indexing_discovery-ssd.json index 5dca3979c..bc37e47f0 100644 --- a/core/benchmarks/results/shape_medium-indexing_discovery-ssd.json +++ b/core/benchmarks/results/shape_medium-indexing_discovery-ssd.json @@ -30,4 +30,4 @@ "total_gb": 375.63 } ] -} \ No newline at end of file +} diff --git a/core/benchmarks/results/shape_small-aggregation-hdd.json b/core/benchmarks/results/shape_small-aggregation-hdd.json index 1742cf187..59f38dee6 100644 --- a/core/benchmarks/results/shape_small-aggregation-hdd.json +++ b/core/benchmarks/results/shape_small-aggregation-hdd.json @@ -20,9 +20,7 @@ "memory_total_gb": 48 }, "id": "acc2002e-ac20-4e83-a772-e8e15a46abe7", - "location_paths": [ - "/Volumes/Seagate/benchdata/shape_small" - ], + "location_paths": ["/Volumes/Seagate/benchdata/shape_small"], "recipe_name": "shape_small", "timestamp_utc": "2025-08-10T09:46:40.825505+00:00" }, @@ -30,4 +28,4 @@ "total_gb": 8.21 } ] -} \ No newline at end of file +} diff --git a/core/benchmarks/results/shape_small-aggregation-ssd.json b/core/benchmarks/results/shape_small-aggregation-ssd.json index cb16c99c2..ebee699c0 100644 --- a/core/benchmarks/results/shape_small-aggregation-ssd.json +++ b/core/benchmarks/results/shape_small-aggregation-ssd.json @@ -30,4 +30,4 @@ "total_gb": 8.21 } ] -} \ No newline at end of file +} diff --git a/core/benchmarks/results/shape_small-content_identification-hdd.json b/core/benchmarks/results/shape_small-content_identification-hdd.json index cdca9b42c..f886ba2c1 100644 --- a/core/benchmarks/results/shape_small-content_identification-hdd.json +++ b/core/benchmarks/results/shape_small-content_identification-hdd.json @@ -20,9 +20,7 @@ "memory_total_gb": 48 }, "id": "597e376f-7ae5-4f08-acfd-3a5eeffb1a5c", - "location_paths": [ - "/Volumes/Seagate/benchdata/shape_small" - ], + "location_paths": ["/Volumes/Seagate/benchdata/shape_small"], "recipe_name": "shape_small", "timestamp_utc": "2025-08-10T10:41:31.601581+00:00" }, @@ -30,4 +28,4 @@ "total_gb": 8.21 } ] -} \ No newline at end of file +} diff --git a/core/benchmarks/results/shape_small-content_identification-ssd.json b/core/benchmarks/results/shape_small-content_identification-ssd.json index d9cc4b67e..f90de1693 100644 --- a/core/benchmarks/results/shape_small-content_identification-ssd.json +++ b/core/benchmarks/results/shape_small-content_identification-ssd.json @@ -30,4 +30,4 @@ "total_gb": 8.21 } ] -} \ No newline at end of file +} diff --git a/core/benchmarks/results/shape_small-indexing_discovery-hdd.json b/core/benchmarks/results/shape_small-indexing_discovery-hdd.json index 869725133..e10cdac51 100644 --- a/core/benchmarks/results/shape_small-indexing_discovery-hdd.json +++ b/core/benchmarks/results/shape_small-indexing_discovery-hdd.json @@ -20,9 +20,7 @@ "memory_total_gb": 48 }, "id": "edb6828c-3605-4dde-9147-e97d2546a9b3", - "location_paths": [ - "/Volumes/Seagate/benchdata/shape_small" - ], + "location_paths": ["/Volumes/Seagate/benchdata/shape_small"], "recipe_name": "shape_small", "timestamp_utc": "2025-08-10T09:43:23.958382+00:00" }, @@ -30,4 +28,4 @@ "total_gb": 8.21 } ] -} \ No newline at end of file +} diff --git a/core/benchmarks/results/shape_small-indexing_discovery-ssd.json b/core/benchmarks/results/shape_small-indexing_discovery-ssd.json index cc8416da0..45dcfbd8a 100644 --- a/core/benchmarks/results/shape_small-indexing_discovery-ssd.json +++ b/core/benchmarks/results/shape_small-indexing_discovery-ssd.json @@ -30,4 +30,4 @@ "total_gb": 8.21 } ] -} \ No newline at end of file +} diff --git a/core/device.json b/core/device.json index ba5597c14..8e4e674ed 100644 --- a/core/device.json +++ b/core/device.json @@ -6,4 +6,4 @@ "hardware_model": null, "os": "macOS", "version": "0.1.0" -} \ No newline at end of file +} diff --git a/docs/custom.css b/docs/custom.css index 445ea69c0..b186c9fb5 100644 --- a/docs/custom.css +++ b/docs/custom.css @@ -1,15 +1,15 @@ /* Anchor hover styles */ .nav-anchor:hover { - @apply text-[#36a3ff]; + @apply text-[#36a3ff]; } /* Icon wrapper on hover */ .nav-anchor:hover div { - background: #36A3FF !important; - filter: brightness(1) !important; + background: #36a3ff !important; + filter: brightness(1) !important; } /* Icon SVG on hover */ .nav-anchor:hover svg { - @apply bg-white !important; + @apply bg-white !important; } diff --git a/docs/mint.json b/docs/mint.json index 677c0baf3..0836d5a05 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -104,7 +104,13 @@ { "group": "Development", "icon": "flask", - "pages": ["core/database", "core/testing", "core/sync-event-log", "core/task-tracking", "core/cli"] + "pages": [ + "core/database", + "core/testing", + "core/sync-event-log", + "core/task-tracking", + "core/cli" + ] }, { "group": "Extension SDK", diff --git a/extensions/photos/manifest.json b/extensions/photos/manifest.json index f6985ea0b..1d3e2c68b 100644 --- a/extensions/photos/manifest.json +++ b/extensions/photos/manifest.json @@ -1,72 +1,58 @@ { - "id": "com.spacedrive.photos", - "name": "Photos", - "version": "1.0.0", - "description": "Advanced photo management with AI-powered face recognition, place identification, and intelligent organization", - "author": "Spacedrive", - "wasm_file": "photos.wasm", - "min_core_version": "2.0.0", - "required_features": [ - "exif_extraction", - "ai_models", - "semantic_search" - ], - "permissions": { - "read_entries": { - "glob": "**/*.{jpg,jpeg,png,heic,heif,raw,cr2,nef,dng,webp}" - }, - "read_sidecars": [ - "exif", - "thumbnail" - ], - "write_sidecars": [ - "faces", - "places", - "scene", - "aesthetics" - ], - "write_tags": true, - "write_custom_fields": [ - "photos" - ], - "dispatch_jobs": true, - "use_models": [ - { - "category": "face_detection", - "preference": "local" - }, - { - "category": "scene_classification", - "preference": "local" - }, - { - "category": "llm", - "preference": "local" - } - ] - }, - "models": [ - { - "name": "photos_face_detection_v1", - "category": "face_detection", - "source": { - "type": "download", - "url": "https://models.spacedrive.com/photos/face_detection_v1.onnx", - "sha256": "placeholder_hash", - "size_mb": 12 - }, - "description": "RetinaFace model for face detection" - }, - { - "name": "photos_scene_v1", - "category": "scene_classification", - "source": { - "type": "download", - "url": "https://models.spacedrive.com/photos/scene_resnet50.onnx", - "sha256": "placeholder_hash", - "size_mb": 95 - }, - "description": "ResNet50 trained on Places365 dataset" - } - ] + "id": "com.spacedrive.photos", + "name": "Photos", + "version": "1.0.0", + "description": "Advanced photo management with AI-powered face recognition, place identification, and intelligent organization", + "author": "Spacedrive", + "wasm_file": "photos.wasm", + "min_core_version": "2.0.0", + "required_features": ["exif_extraction", "ai_models", "semantic_search"], + "permissions": { + "read_entries": { + "glob": "**/*.{jpg,jpeg,png,heic,heif,raw,cr2,nef,dng,webp}" + }, + "read_sidecars": ["exif", "thumbnail"], + "write_sidecars": ["faces", "places", "scene", "aesthetics"], + "write_tags": true, + "write_custom_fields": ["photos"], + "dispatch_jobs": true, + "use_models": [ + { + "category": "face_detection", + "preference": "local" + }, + { + "category": "scene_classification", + "preference": "local" + }, + { + "category": "llm", + "preference": "local" + } + ] + }, + "models": [ + { + "name": "photos_face_detection_v1", + "category": "face_detection", + "source": { + "type": "download", + "url": "https://models.spacedrive.com/photos/face_detection_v1.onnx", + "sha256": "placeholder_hash", + "size_mb": 12 + }, + "description": "RetinaFace model for face detection" + }, + { + "name": "photos_scene_v1", + "category": "scene_classification", + "source": { + "type": "download", + "url": "https://models.spacedrive.com/photos/scene_resnet50.onnx", + "sha256": "placeholder_hash", + "size_mb": 95 + }, + "description": "ResNet50 trained on Places365 dataset" + } + ] } diff --git a/extensions/photos/ui_manifest.json b/extensions/photos/ui_manifest.json index 2ab107416..8b8b26156 100644 --- a/extensions/photos/ui_manifest.json +++ b/extensions/photos/ui_manifest.json @@ -1,154 +1,141 @@ { - "sidebar": { - "section": "Photos", - "icon": "assets/photos_icon.svg", - "order": 10, - "views": [ - { - "id": "library", - "title": "Library", - "component": "photo_grid", - "query": "list_all_photos", - "default": true - }, - { - "id": "albums", - "title": "Albums", - "component": "album_grid", - "query": "list_albums" - }, - { - "id": "people", - "title": "People", - "component": "person_cluster_grid", - "query": "list_people" - }, - { - "id": "places", - "title": "Places", - "component": "map_view", - "query": "list_places" - }, - { - "id": "moments", - "title": "Moments", - "component": "moment_timeline", - "query": "list_moments" - }, - { - "id": "favorites", - "title": "Favorites", - "component": "photo_grid", - "query": "tags LIKE '#favorite'", - "icon": "heart" - } - ] - }, - "context_menu": [ - { - "id": "add_to_album", - "label": "Add to Album...", - "icon": "plus_circle", - "action": "add_to_album", - "applies_to": [ - "image/*" - ], - "keyboard_shortcut": "cmd+shift+a" - }, - { - "id": "identify_person", - "label": "This is...", - "icon": "person", - "action": "identify_person", - "applies_to": [ - "image/*" - ], - "requires": "has_detected_faces" - }, - { - "id": "set_as_cover", - "label": "Set as Album Cover", - "icon": "star", - "action": "set_album_cover", - "applies_to": [ - "image/*" - ], - "context": "album_view" - }, - { - "id": "hide_photo", - "label": "Hide", - "icon": "eye_slash", - "action": "hide_photo", - "applies_to": [ - "image/*" - ] - } - ], - "toolbar_actions": [ - { - "id": "analyze_location", - "label": "Analyze for Faces", - "icon": "sparkles", - "action": "analyze_photos_batch", - "context": "location_view" - }, - { - "id": "identify_places", - "label": "Identify Places", - "icon": "map_pin", - "action": "identify_places_in_location", - "context": "location_view" - }, - { - "id": "create_moment", - "label": "Create Moment", - "icon": "calendar", - "action": "create_moments_from_selection" - } - ], - "file_viewers": [ - { - "mime_types": [ - "image/jpeg", - "image/png", - "image/heic", - "image/webp" - ], - "component": "photo_viewer", - "features": { - "slideshow": true, - "metadata_panel": true, - "face_indicators": true, - "related_photos": true - } - } - ], - "search_filters": [ - { - "id": "has_people", - "label": "Has People", - "type": "boolean", - "query": "tags LIKE '#person:%'" - }, - { - "id": "location", - "label": "Location", - "type": "place_picker", - "query_template": "tags LIKE '#place:{value}'" - }, - { - "id": "date_range", - "label": "Date", - "type": "date_range", - "query_template": "exif.taken_at BETWEEN '{start}' AND '{end}'" - }, - { - "id": "camera", - "label": "Camera", - "type": "select", - "options_query": "SELECT DISTINCT exif.camera_model FROM photos", - "query_template": "exif.camera_model = '{value}'" - } - ] + "sidebar": { + "section": "Photos", + "icon": "assets/photos_icon.svg", + "order": 10, + "views": [ + { + "id": "library", + "title": "Library", + "component": "photo_grid", + "query": "list_all_photos", + "default": true + }, + { + "id": "albums", + "title": "Albums", + "component": "album_grid", + "query": "list_albums" + }, + { + "id": "people", + "title": "People", + "component": "person_cluster_grid", + "query": "list_people" + }, + { + "id": "places", + "title": "Places", + "component": "map_view", + "query": "list_places" + }, + { + "id": "moments", + "title": "Moments", + "component": "moment_timeline", + "query": "list_moments" + }, + { + "id": "favorites", + "title": "Favorites", + "component": "photo_grid", + "query": "tags LIKE '#favorite'", + "icon": "heart" + } + ] + }, + "context_menu": [ + { + "id": "add_to_album", + "label": "Add to Album...", + "icon": "plus_circle", + "action": "add_to_album", + "applies_to": ["image/*"], + "keyboard_shortcut": "cmd+shift+a" + }, + { + "id": "identify_person", + "label": "This is...", + "icon": "person", + "action": "identify_person", + "applies_to": ["image/*"], + "requires": "has_detected_faces" + }, + { + "id": "set_as_cover", + "label": "Set as Album Cover", + "icon": "star", + "action": "set_album_cover", + "applies_to": ["image/*"], + "context": "album_view" + }, + { + "id": "hide_photo", + "label": "Hide", + "icon": "eye_slash", + "action": "hide_photo", + "applies_to": ["image/*"] + } + ], + "toolbar_actions": [ + { + "id": "analyze_location", + "label": "Analyze for Faces", + "icon": "sparkles", + "action": "analyze_photos_batch", + "context": "location_view" + }, + { + "id": "identify_places", + "label": "Identify Places", + "icon": "map_pin", + "action": "identify_places_in_location", + "context": "location_view" + }, + { + "id": "create_moment", + "label": "Create Moment", + "icon": "calendar", + "action": "create_moments_from_selection" + } + ], + "file_viewers": [ + { + "mime_types": ["image/jpeg", "image/png", "image/heic", "image/webp"], + "component": "photo_viewer", + "features": { + "slideshow": true, + "metadata_panel": true, + "face_indicators": true, + "related_photos": true + } + } + ], + "search_filters": [ + { + "id": "has_people", + "label": "Has People", + "type": "boolean", + "query": "tags LIKE '#person:%'" + }, + { + "id": "location", + "label": "Location", + "type": "place_picker", + "query_template": "tags LIKE '#place:{value}'" + }, + { + "id": "date_range", + "label": "Date", + "type": "date_range", + "query_template": "exif.taken_at BETWEEN '{start}' AND '{end}'" + }, + { + "id": "camera", + "label": "Camera", + "type": "select", + "options_query": "SELECT DISTINCT exif.camera_model FROM photos", + "query_template": "exif.camera_model = '{value}'" + } + ] } diff --git a/extensions/test-extension/manifest.json b/extensions/test-extension/manifest.json index 3791fc9c8..07d7926f5 100644 --- a/extensions/test-extension/manifest.json +++ b/extensions/test-extension/manifest.json @@ -1,24 +1,19 @@ { - "id": "test-extension", - "name": "Test Extension", - "version": "0.1.0", - "description": "Minimal extension demonstrating beautiful SDK API", - "author": "Spacedrive Team", - "homepage": "https://spacedrive.com", - "wasm_file": "test_extension.wasm", - "permissions": { - "methods": [ - "query:", - "action:" - ], - "libraries": [ - "*" - ], - "rate_limits": { - "requests_per_minute": 1000, - "concurrent_jobs": 10 - }, - "network_access": [], - "max_memory_mb": 256 - } -} \ No newline at end of file + "id": "test-extension", + "name": "Test Extension", + "version": "0.1.0", + "description": "Minimal extension demonstrating beautiful SDK API", + "author": "Spacedrive Team", + "homepage": "https://spacedrive.com", + "wasm_file": "test_extension.wasm", + "permissions": { + "methods": ["query:", "action:"], + "libraries": ["*"], + "rate_limits": { + "requests_per_minute": 1000, + "concurrent_jobs": 10 + }, + "network_access": [], + "max_memory_mb": 256 + } +} diff --git a/package.json b/package.json index 11e968f4e..f767dddeb 100644 --- a/package.json +++ b/package.json @@ -3,20 +3,24 @@ "private": true, "scripts": { "preinstall": "npx only-allow bun", - "typecheck": "bun run --filter @sd/tauri typecheck" + "typecheck": "bun run --filter @sd/tauri typecheck", + "prepare": "husky" }, "devDependencies": { "@babel/plugin-syntax-import-assertions": "^7.24.0", + "@biomejs/biome": "2.3.11", "@cspell/dict-rust": "^4.0.2", "@cspell/dict-typescript": "^3.1.2", "@ianvs/prettier-plugin-sort-imports": "^4.3.1", "@taplo/cli": "^0.7.0", "cspell": "^8.6.0", + "husky": "^9.1.7", "prettier": "^3.3.3", "prettier-plugin-tailwindcss": "^0.6.6", "turbo": "^1.12.5", "turbo-ignore": "^1.12.5", "typescript": "^5.6.2", + "ultracite": "7.0.9", "vite": "^5.4.9" }, "engines": { @@ -49,4 +53,4 @@ "gray-matter": "^4.0.3", "react": "19.1.0" } -} \ No newline at end of file +} diff --git a/packages/assets/icons/index.ts b/packages/assets/icons/index.ts index 48027be5a..7884992dd 100644 --- a/packages/assets/icons/index.ts +++ b/packages/assets/icons/index.ts @@ -3,50 +3,49 @@ * To regenerate this file, run: pnpm assets gen */ -import Album20 from "./Album-20.png"; import Album from "./Album.png"; import Album_Light from "./Album_Light.png"; -import Alias20 from "./Alias-20.png"; +import Album20 from "./Album-20.png"; import Alias from "./Alias.png"; import Alias_Light from "./Alias_Light.png"; +import Alias20 from "./Alias-20.png"; import AmazonS3 from "./AmazonS3.png"; import AndroidPhotos from "./AndroidPhotos.png"; import AppleFiles from "./AppleFiles.png"; import ApplePhotos from "./ApplePhotos.png"; import Application from "./Application.png"; import Application_Light from "./Application_Light.png"; -import Archive20 from "./Archive-20.png"; import Archive from "./Archive.png"; import Archive_Light from "./Archive_Light.png"; -import Audio20 from "./Audio-20.png"; +import Archive20 from "./Archive-20.png"; import Audio from "./Audio.png"; import Audio_Light from "./Audio_Light.png"; +import Audio20 from "./Audio-20.png"; import BackBlaze from "./BackBlaze.png"; import Ball from "./Ball.png"; -import Book20 from "./Book-20.png"; import Book from "./Book.png"; -import BookBlue from "./BookBlue.png"; import Book_Light from "./Book_Light.png"; +import Book20 from "./Book-20.png"; +import BookBlue from "./BookBlue.png"; import Box from "./Box.png"; import CloudSync from "./CloudSync.png"; import CloudSync_Light from "./CloudSync_Light.png"; import Code20 from "./Code-20.png"; -import Collection20 from "./Collection-20.png"; import Collection from "./Collection.png"; +import Collection_Light from "./Collection_Light.png"; +import Collection20 from "./Collection-20.png"; import CollectionSparkle from "./CollectionSparkle.png"; import CollectionSparkle_Light from "./CollectionSparkle_Light.png"; -import Collection_Light from "./Collection_Light.png"; import Config20 from "./Config-20.png"; import DAV from "./DAV.png"; -import Database20 from "./Database-20.png"; import Database from "./Database.png"; import Database_Light from "./Database_Light.png"; +import Database20 from "./Database-20.png"; import DeleteLocation from "./DeleteLocation.png"; -import Document20 from "./Document-20.png"; import Document from "./Document.png"; -import Document_Light from "./Document_Light.png"; import Document_doc from "./Document_doc.png"; import Document_doc_Light from "./Document_doc_Light.png"; +import Document_Light from "./Document_Light.png"; import Document_memory from "./Document_memory.png"; import Document_pdf from "./Document_pdf.png"; import Document_pdf_Light from "./Document_pdf_Light.png"; @@ -54,12 +53,16 @@ import Document_srt from "./Document_srt.png"; import Document_xls from "./Document_xls.png"; import Document_xls_Light from "./Document_xls_Light.png"; import Document_xmp from "./Document_xmp.png"; +import Document20 from "./Document-20.png"; import Dotfile20 from "./Dotfile-20.png"; +import Drive from "./Drive.png"; +import Drive_Light from "./Drive_Light.png"; import DriveAmazonS3 from "./Drive-AmazonS3.png"; import DriveAmazonS3_Light from "./Drive-AmazonS3_Light.png"; import DriveBackBlaze from "./Drive-BackBlaze.png"; import DriveBackBlaze_Light from "./Drive-BackBlaze_Light.png"; import DriveBox from "./Drive-Box.png"; +import Drivebox_Light from "./Drive-box_Light.png"; import DriveDAV from "./Drive-DAV.png"; import DriveDAV_Light from "./Drive-DAV_Light.png"; import DriveDarker from "./Drive-Darker.png"; @@ -75,35 +78,32 @@ import DriveOpenStack from "./Drive-OpenStack.png"; import DriveOpenStack_Light from "./Drive-OpenStack_Light.png"; import DrivePCloud from "./Drive-PCloud.png"; import DrivePCloud_Light from "./Drive-PCloud_Light.png"; -import Drivebox_Light from "./Drive-box_Light.png"; -import Drive from "./Drive.png"; -import Drive_Light from "./Drive_Light.png"; import Dropbox from "./Dropbox.png"; -import Encrypted20 from "./Encrypted-20.png"; import Encrypted from "./Encrypted.png"; import Encrypted_Light from "./Encrypted_Light.png"; +import Encrypted20 from "./Encrypted-20.png"; import Entity from "./Entity.png"; import Entity_Light from "./Entity_Light.png"; -import Executable20 from "./Executable-20.png"; import Executable from "./Executable.png"; import Executable_Light from "./Executable_Light.png"; import Executable_Light_old from "./Executable_Light_old.png"; import Executable_old from "./Executable_old.png"; +import Executable20 from "./Executable-20.png"; import Face_Light from "./Face_Light.png"; +import Folder from "./Folder.png"; +import Folder_Light from "./Folder_Light.png"; import Folder20 from "./Folder-20.png"; import Foldertagxmp from "./Folder-tag-xmp.png"; -import Folder from "./Folder.png"; import FolderGrey from "./FolderGrey.png"; import FolderGrey_Light from "./FolderGrey_Light.png"; import FolderNoSpace from "./FolderNoSpace.png"; import FolderNoSpace_Light from "./FolderNoSpace_Light.png"; -import Folder_Light from "./Folder_Light.png"; import Font20 from "./Font-20.png"; import Game from "./Game.png"; import Game_Light from "./Game_Light.png"; import Globe from "./Globe.png"; -import GlobeAlt from "./GlobeAlt.png"; import Globe_Light from "./Globe_Light.png"; +import GlobeAlt from "./GlobeAlt.png"; import GoogleDrive from "./GoogleDrive.png"; import HDD from "./HDD.png"; import HDD_Light from "./HDD_Light.png"; @@ -111,32 +111,32 @@ import Heart from "./Heart.png"; import Heart_Light from "./Heart_Light.png"; import Home from "./Home.png"; import Home_Light from "./Home_Light.png"; -import Image20 from "./Image-20.png"; import Image from "./Image.png"; import Image_Light from "./Image_Light.png"; -import Key20 from "./Key-20.png"; +import Image20 from "./Image-20.png"; import Key from "./Key.png"; import Key_Light from "./Key_Light.png"; +import Key20 from "./Key-20.png"; import Keys from "./Keys.png"; import Keys_Light from "./Keys_Light.png"; import Laptop from "./Laptop.png"; import Laptop_Light from "./Laptop_Light.png"; -import Link20 from "./Link-20.png"; import Link from "./Link.png"; import Link_Light from "./Link_Light.png"; +import Link20 from "./Link-20.png"; import Location from "./Location.png"; import LocationManaged from "./LocationManaged.png"; import LocationReplica from "./LocationReplica.png"; import Lock from "./Lock.png"; import Lock_Light from "./Lock_Light.png"; import Mega from "./Mega.png"; -import Mesh20 from "./Mesh-20.png"; import Mesh from "./Mesh.png"; import Mesh_Light from "./Mesh_Light.png"; +import Mesh20 from "./Mesh-20.png"; import MiniSilverBox from "./MiniSilverBox.png"; -import MobileAndroid from "./Mobile-Android.png"; import Mobile from "./Mobile.png"; import Mobile_Light from "./Mobile_Light.png"; +import MobileAndroid from "./Mobile-Android.png"; import MoveLocation from "./MoveLocation.png"; import MoveLocation_Light from "./MoveLocation_Light.png"; import Movie from "./Movie.png"; @@ -146,28 +146,28 @@ import Node from "./Node.png"; import Node_Light from "./Node_Light.png"; import OneDrive from "./OneDrive.png"; import OpenStack from "./OpenStack.png"; -import PC from "./PC.png"; -import PCloud from "./PCloud.png"; -import Package20 from "./Package-20.png"; import Package from "./Package.png"; import Package_Light from "./Package_Light.png"; -import SD from "./SD.png"; -import SD_Light from "./SD_Light.png"; +import Package20 from "./Package-20.png"; +import PC from "./PC.png"; +import PCloud from "./PCloud.png"; import Scrapbook from "./Scrapbook.png"; import Scrapbook_Light from "./Scrapbook_Light.png"; -import Screenshot20 from "./Screenshot-20.png"; import Screenshot from "./Screenshot.png"; -import ScreenshotAlt from "./ScreenshotAlt.png"; import Screenshot_Light from "./Screenshot_Light.png"; +import Screenshot20 from "./Screenshot-20.png"; +import ScreenshotAlt from "./ScreenshotAlt.png"; +import SD from "./SD.png"; +import SD_Light from "./SD_Light.png"; import Search from "./Search.png"; -import SearchAlt from "./SearchAlt.png"; import Search_Light from "./Search_Light.png"; +import SearchAlt from "./SearchAlt.png"; import Server from "./Server.png"; import Server_Light from "./Server_Light.png"; import SilverBox from "./SilverBox.png"; -import Spacedrop1 from "./Spacedrop-1.png"; import Spacedrop from "./Spacedrop.png"; import Spacedrop_Light from "./Spacedrop_Light.png"; +import Spacedrop1 from "./Spacedrop-1.png"; import Sync from "./Sync.png"; import Sync_Light from "./Sync_Light.png"; import Tablet from "./Tablet.png"; @@ -176,12 +176,12 @@ import Tags from "./Tags.png"; import Tags_Light from "./Tags_Light.png"; import Terminal from "./Terminal.png"; import Terminal_Light from "./Terminal_Light.png"; -import Text20 from "./Text-20.png"; import Text from "./Text.png"; -import TextAlt from "./TextAlt.png"; -import TextAlt_Light from "./TextAlt_Light.png"; import Text_Light from "./Text_Light.png"; import Text_txt from "./Text_txt.png"; +import Text20 from "./Text-20.png"; +import TextAlt from "./TextAlt.png"; +import TextAlt_Light from "./TextAlt_Light.png"; import TexturedMesh from "./TexturedMesh.png"; import TexturedMesh_Light from "./TexturedMesh_Light.png"; import Trash from "./Trash.png"; @@ -189,13 +189,13 @@ import Trash_Light from "./Trash_Light.png"; import Undefined from "./Undefined.png"; import Undefined_Light from "./Undefined_Light.png"; import Unknown20 from "./Unknown-20.png"; -import Video20 from "./Video-20.png"; import Video from "./Video.png"; import Video_Light from "./Video_Light.png"; +import Video20 from "./Video-20.png"; import WebPageArchive20 from "./WebPageArchive-20.png"; -import Widget20 from "./Widget-20.png"; import Widget from "./Widget.png"; import Widget_Light from "./Widget_Light.png"; +import Widget20 from "./Widget-20.png"; export { Album20, diff --git a/packages/assets/images/index.ts b/packages/assets/images/index.ts index f90204358..cdbf6ff9e 100644 --- a/packages/assets/images/index.ts +++ b/packages/assets/images/index.ts @@ -13,9 +13,9 @@ import BloomThree from "./BloomThree.png"; import BloomTwo from "./BloomTwo.png"; import Dropbox from "./Dropbox.png"; import GoogleDrive from "./GoogleDrive.png"; +import iCloud from "./iCloud.png"; import Mega from "./Mega.png"; import Transparent from "./Transparent.png"; -import iCloud from "./iCloud.png"; export { AlphaBg, diff --git a/packages/assets/lottie/loading-pulse.json b/packages/assets/lottie/loading-pulse.json index 70706e76a..3afd46765 100644 --- a/packages/assets/lottie/loading-pulse.json +++ b/packages/assets/lottie/loading-pulse.json @@ -1,271 +1,271 @@ { - "nm": "Pre-comp 2", - "ddd": 0, - "h": 200, - "w": 200, - "meta": { "g": "@lottiefiles/toolkit-js 0.26.1" }, - "layers": [ - { - "ty": 0, - "nm": "Pre-comp 1", - "sr": 1, - "st": -37.0000015070409, - "op": 39.0000015885026, - "ip": 0, - "hd": false, - "ddd": 0, - "bm": 0, - "hasMask": false, - "ao": 0, - "ks": { - "a": { "a": 0, "k": [100, 100, 0], "ix": 1 }, - "s": { "a": 0, "k": [100, 100, 100], "ix": 6 }, - "sk": { "a": 0, "k": 0 }, - "p": { "a": 0, "k": [100, 100, 0], "ix": 2 }, - "r": { "a": 0, "k": 0, "ix": 10 }, - "sa": { "a": 0, "k": 0 }, - "o": { "a": 0, "k": 100, "ix": 11 } - }, - "ef": [], - "w": 200, - "h": 200, - "refId": "comp_0", - "ind": 1 - }, - { - "ty": 0, - "nm": "Pre-comp 1", - "sr": 1, - "st": 23.0000009368092, - "op": 60.0000024438501, - "ip": 23.0000009368092, - "hd": false, - "ddd": 0, - "bm": 0, - "hasMask": false, - "ao": 0, - "ks": { - "a": { "a": 0, "k": [100, 100, 0], "ix": 1 }, - "s": { "a": 0, "k": [100, 100, 100], "ix": 6 }, - "sk": { "a": 0, "k": 0 }, - "p": { "a": 0, "k": [100, 100, 0], "ix": 2 }, - "r": { "a": 0, "k": 0, "ix": 10 }, - "sa": { "a": 0, "k": 0 }, - "o": { "a": 0, "k": 100, "ix": 11 } - }, - "ef": [], - "w": 200, - "h": 200, - "refId": "comp_0", - "ind": 2 - } - ], - "v": "5.1.16", - "fr": 29.9700012207031, - "op": 60.0000024438501, - "ip": 0, - "assets": [ - { - "nm": "", - "id": "comp_0", - "layers": [ - { - "ty": 4, - "nm": "Shape Layer 2", - "sr": 1, - "st": 16.0000006516934, - "op": 76.0000030955435, - "ip": 16.0000006516934, - "hd": false, - "ddd": 0, - "bm": 0, - "hasMask": false, - "ao": 0, - "ks": { - "a": { "a": 0, "k": [0.309, -0.979, 0], "ix": 1 }, - "s": { - "a": 1, - "k": [ - { - "o": { "x": 0.333, "y": 0 }, - "i": { "x": 0.667, "y": 1 }, - "s": [0, 0, 100], - "t": 16 - }, - { "s": [100, 100, 100], "t": 76.0000030955435 } - ], - "ix": 6 - }, - "sk": { "a": 0, "k": 0 }, - "p": { "a": 0, "k": [100.309, 99.021, 0], "ix": 2 }, - "r": { "a": 0, "k": 0, "ix": 10 }, - "sa": { "a": 0, "k": 0 }, - "o": { - "a": 1, - "k": [ - { - "o": { "x": 0.333, "y": 0 }, - "i": { "x": 0.667, "y": 1 }, - "s": [0], - "t": 16 - }, - { - "o": { "x": 0.333, "y": 0 }, - "i": { "x": 0.667, "y": 1 }, - "s": [40], - "t": 46 - }, - { "s": [0], "t": 76.0000030955435 } - ], - "ix": 11 - } - }, - "ef": [], - "shapes": [ - { - "ty": "gr", - "bm": 0, - "hd": false, - "mn": "ADBE Vector Group", - "nm": "Ellipse 1", - "ix": 1, - "cix": 2, - "np": 3, - "it": [ - { - "ty": "el", - "bm": 0, - "hd": false, - "mn": "ADBE Vector Shape - Ellipse", - "nm": "Ellipse Path 1", - "d": 1, - "p": { "a": 0, "k": [0, 0], "ix": 3 }, - "s": { "a": 0, "k": [148.156, 148.156], "ix": 2 } - }, - { - "ty": "fl", - "bm": 0, - "hd": false, - "mn": "ADBE Vector Graphic - Fill", - "nm": "Fill 1", - "c": { "a": 0, "k": [0.1412, 0.6, 1], "ix": 4 }, - "r": 1, - "o": { "a": 0, "k": 100, "ix": 5 } - }, - { - "ty": "tr", - "a": { "a": 0, "k": [0, 0], "ix": 1 }, - "s": { "a": 0, "k": [100, 100], "ix": 3 }, - "sk": { "a": 0, "k": 0, "ix": 4 }, - "p": { "a": 0, "k": [0.309, -0.979], "ix": 2 }, - "r": { "a": 0, "k": 0, "ix": 6 }, - "sa": { "a": 0, "k": 0, "ix": 5 }, - "o": { "a": 0, "k": 100, "ix": 7 } - } - ] - } - ], - "ind": 1 - }, - { - "ty": 4, - "nm": "Shape Layer 1", - "sr": 1, - "st": 0, - "op": 76.0000030955435, - "ip": 0, - "hd": false, - "ddd": 0, - "bm": 0, - "hasMask": false, - "ao": 0, - "ks": { - "a": { "a": 0, "k": [0.309, -0.979, 0], "ix": 1 }, - "s": { - "a": 1, - "k": [ - { - "o": { "x": 0.333, "y": 0 }, - "i": { "x": 0.667, "y": 1 }, - "s": [0, 0, 100], - "t": 0 - }, - { "s": [100, 100, 100], "t": 60.0000024438501 } - ], - "ix": 6 - }, - "sk": { "a": 0, "k": 0 }, - "p": { "a": 0, "k": [100.309, 99.021, 0], "ix": 2 }, - "r": { "a": 0, "k": 0, "ix": 10 }, - "sa": { "a": 0, "k": 0 }, - "o": { - "a": 1, - "k": [ - { - "o": { "x": 0.333, "y": 0 }, - "i": { "x": 0.667, "y": 1 }, - "s": [0], - "t": 0 - }, - { - "o": { "x": 0.333, "y": 0 }, - "i": { "x": 0.667, "y": 1 }, - "s": [40], - "t": 30 - }, - { "s": [0], "t": 60.0000024438501 } - ], - "ix": 11 - } - }, - "ef": [], - "shapes": [ - { - "ty": "gr", - "bm": 0, - "hd": false, - "mn": "ADBE Vector Group", - "nm": "Ellipse 1", - "ix": 1, - "cix": 2, - "np": 3, - "it": [ - { - "ty": "el", - "bm": 0, - "hd": false, - "mn": "ADBE Vector Shape - Ellipse", - "nm": "Ellipse Path 1", - "d": 1, - "p": { "a": 0, "k": [0, 0], "ix": 3 }, - "s": { "a": 0, "k": [148.156, 148.156], "ix": 2 } - }, - { - "ty": "fl", - "bm": 0, - "hd": false, - "mn": "ADBE Vector Graphic - Fill", - "nm": "Fill 1", - "c": { "a": 0, "k": [0.1412, 0.6, 1], "ix": 4 }, - "r": 1, - "o": { "a": 0, "k": 100, "ix": 5 } - }, - { - "ty": "tr", - "a": { "a": 0, "k": [0, 0], "ix": 1 }, - "s": { "a": 0, "k": [100, 100], "ix": 3 }, - "sk": { "a": 0, "k": 0, "ix": 4 }, - "p": { "a": 0, "k": [0.309, -0.979], "ix": 2 }, - "r": { "a": 0, "k": 0, "ix": 6 }, - "sa": { "a": 0, "k": 0, "ix": 5 }, - "o": { "a": 0, "k": 100, "ix": 7 } - } - ] - } - ], - "ind": 2 - } - ] - } - ] + "nm": "Pre-comp 2", + "ddd": 0, + "h": 200, + "w": 200, + "meta": { "g": "@lottiefiles/toolkit-js 0.26.1" }, + "layers": [ + { + "ty": 0, + "nm": "Pre-comp 1", + "sr": 1, + "st": -37.0000015070409, + "op": 39.0000015885026, + "ip": 0, + "hd": false, + "ddd": 0, + "bm": 0, + "hasMask": false, + "ao": 0, + "ks": { + "a": { "a": 0, "k": [100, 100, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6 }, + "sk": { "a": 0, "k": 0 }, + "p": { "a": 0, "k": [100, 100, 0], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "sa": { "a": 0, "k": 0 }, + "o": { "a": 0, "k": 100, "ix": 11 } + }, + "ef": [], + "w": 200, + "h": 200, + "refId": "comp_0", + "ind": 1 + }, + { + "ty": 0, + "nm": "Pre-comp 1", + "sr": 1, + "st": 23.0000009368092, + "op": 60.0000024438501, + "ip": 23.0000009368092, + "hd": false, + "ddd": 0, + "bm": 0, + "hasMask": false, + "ao": 0, + "ks": { + "a": { "a": 0, "k": [100, 100, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6 }, + "sk": { "a": 0, "k": 0 }, + "p": { "a": 0, "k": [100, 100, 0], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "sa": { "a": 0, "k": 0 }, + "o": { "a": 0, "k": 100, "ix": 11 } + }, + "ef": [], + "w": 200, + "h": 200, + "refId": "comp_0", + "ind": 2 + } + ], + "v": "5.1.16", + "fr": 29.9700012207031, + "op": 60.0000024438501, + "ip": 0, + "assets": [ + { + "nm": "", + "id": "comp_0", + "layers": [ + { + "ty": 4, + "nm": "Shape Layer 2", + "sr": 1, + "st": 16.0000006516934, + "op": 76.0000030955435, + "ip": 16.0000006516934, + "hd": false, + "ddd": 0, + "bm": 0, + "hasMask": false, + "ao": 0, + "ks": { + "a": { "a": 0, "k": [0.309, -0.979, 0], "ix": 1 }, + "s": { + "a": 1, + "k": [ + { + "o": { "x": 0.333, "y": 0 }, + "i": { "x": 0.667, "y": 1 }, + "s": [0, 0, 100], + "t": 16 + }, + { "s": [100, 100, 100], "t": 76.0000030955435 } + ], + "ix": 6 + }, + "sk": { "a": 0, "k": 0 }, + "p": { "a": 0, "k": [100.309, 99.021, 0], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "sa": { "a": 0, "k": 0 }, + "o": { + "a": 1, + "k": [ + { + "o": { "x": 0.333, "y": 0 }, + "i": { "x": 0.667, "y": 1 }, + "s": [0], + "t": 16 + }, + { + "o": { "x": 0.333, "y": 0 }, + "i": { "x": 0.667, "y": 1 }, + "s": [40], + "t": 46 + }, + { "s": [0], "t": 76.0000030955435 } + ], + "ix": 11 + } + }, + "ef": [], + "shapes": [ + { + "ty": "gr", + "bm": 0, + "hd": false, + "mn": "ADBE Vector Group", + "nm": "Ellipse 1", + "ix": 1, + "cix": 2, + "np": 3, + "it": [ + { + "ty": "el", + "bm": 0, + "hd": false, + "mn": "ADBE Vector Shape - Ellipse", + "nm": "Ellipse Path 1", + "d": 1, + "p": { "a": 0, "k": [0, 0], "ix": 3 }, + "s": { "a": 0, "k": [148.156, 148.156], "ix": 2 } + }, + { + "ty": "fl", + "bm": 0, + "hd": false, + "mn": "ADBE Vector Graphic - Fill", + "nm": "Fill 1", + "c": { "a": 0, "k": [0.1412, 0.6, 1], "ix": 4 }, + "r": 1, + "o": { "a": 0, "k": 100, "ix": 5 } + }, + { + "ty": "tr", + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "p": { "a": 0, "k": [0.309, -0.979], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "o": { "a": 0, "k": 100, "ix": 7 } + } + ] + } + ], + "ind": 1 + }, + { + "ty": 4, + "nm": "Shape Layer 1", + "sr": 1, + "st": 0, + "op": 76.0000030955435, + "ip": 0, + "hd": false, + "ddd": 0, + "bm": 0, + "hasMask": false, + "ao": 0, + "ks": { + "a": { "a": 0, "k": [0.309, -0.979, 0], "ix": 1 }, + "s": { + "a": 1, + "k": [ + { + "o": { "x": 0.333, "y": 0 }, + "i": { "x": 0.667, "y": 1 }, + "s": [0, 0, 100], + "t": 0 + }, + { "s": [100, 100, 100], "t": 60.0000024438501 } + ], + "ix": 6 + }, + "sk": { "a": 0, "k": 0 }, + "p": { "a": 0, "k": [100.309, 99.021, 0], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "sa": { "a": 0, "k": 0 }, + "o": { + "a": 1, + "k": [ + { + "o": { "x": 0.333, "y": 0 }, + "i": { "x": 0.667, "y": 1 }, + "s": [0], + "t": 0 + }, + { + "o": { "x": 0.333, "y": 0 }, + "i": { "x": 0.667, "y": 1 }, + "s": [40], + "t": 30 + }, + { "s": [0], "t": 60.0000024438501 } + ], + "ix": 11 + } + }, + "ef": [], + "shapes": [ + { + "ty": "gr", + "bm": 0, + "hd": false, + "mn": "ADBE Vector Group", + "nm": "Ellipse 1", + "ix": 1, + "cix": 2, + "np": 3, + "it": [ + { + "ty": "el", + "bm": 0, + "hd": false, + "mn": "ADBE Vector Shape - Ellipse", + "nm": "Ellipse Path 1", + "d": 1, + "p": { "a": 0, "k": [0, 0], "ix": 3 }, + "s": { "a": 0, "k": [148.156, 148.156], "ix": 2 } + }, + { + "ty": "fl", + "bm": 0, + "hd": false, + "mn": "ADBE Vector Graphic - Fill", + "nm": "Fill 1", + "c": { "a": 0, "k": [0.1412, 0.6, 1], "ix": 4 }, + "r": 1, + "o": { "a": 0, "k": 100, "ix": 5 } + }, + { + "ty": "tr", + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "p": { "a": 0, "k": [0.309, -0.979], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "o": { "a": 0, "k": 100, "ix": 7 } + } + ] + } + ], + "ind": 2 + } + ] + } + ] } diff --git a/packages/assets/package.json b/packages/assets/package.json index 16671675b..289f9f5f9 100644 --- a/packages/assets/package.json +++ b/packages/assets/package.json @@ -1,24 +1,24 @@ { - "name": "@sd/assets", - "version": "1.0.0", - "license": "GPL-3.0-only", - "publishConfig": { - "name": "@spacedriveapp/assets" - }, - "sideEffects": false, - "files": [ - "icons", - "images", - "lottie", - "sounds", - "svgs", - "videos", - "util" - ], - "scripts": { - "gen": "node ./scripts/generate.mjs" - }, - "dependencies": { - "react": "^19.0.0" - } + "name": "@sd/assets", + "version": "1.0.0", + "license": "GPL-3.0-only", + "publishConfig": { + "name": "@spacedriveapp/assets" + }, + "sideEffects": false, + "files": [ + "icons", + "images", + "lottie", + "sounds", + "svgs", + "videos", + "util" + ], + "scripts": { + "gen": "node ./scripts/generate.mjs" + }, + "dependencies": { + "react": "^19.0.0" + } } diff --git a/packages/assets/scripts/generate.mjs b/packages/assets/scripts/generate.mjs index 90525882a..a07718b7b 100644 --- a/packages/assets/scripts/generate.mjs +++ b/packages/assets/scripts/generate.mjs @@ -8,80 +8,88 @@ * * The generated index files will have the name `index.ts` and will be located in the root of each asset folder. */ -import fs from 'node:fs/promises'; -import { dirname, join } from 'node:path'; -import { fileURLToPath } from 'node:url'; -import prettier from 'prettier'; +import fs from "node:fs/promises"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import prettier from "prettier"; -const assetFolders = ['icons', 'images', 'videos']; +const assetFolders = ["icons", "images", "videos"]; -const lazyAssetFolders = ['svgs/brands', 'svgs/ext/Extras', 'svgs/ext/Code']; +const lazyAssetFolders = ["svgs/brands", "svgs/ext/Extras", "svgs/ext/Code"]; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); -prettier.resolveConfig(join(__dirname, '..', '..', '..', '.prettierrc.js')).then((options) => - Promise.all( - [ - ...assetFolders.map((e) => /** @type {const} */ ([e, false])), - ...lazyAssetFolders.map((e) => /** @type {const} */ ([e, true])) - ].map(async ([folder, lazy]) => { - const indexFilePath = join(__dirname, '..', folder, 'index.ts'); - const assetsFolderPath = join(__dirname, '..', folder); +prettier + .resolveConfig(join(__dirname, "..", "..", "..", ".prettierrc.js")) + .then((options) => + Promise.all( + [ + ...assetFolders.map((e) => /** @type {const} */ ([e, false])), + ...lazyAssetFolders.map((e) => /** @type {const} */ ([e, true])), + ].map(async ([folder, lazy]) => { + const indexFilePath = join(__dirname, "..", folder, "index.ts"); + const assetsFolderPath = join(__dirname, "..", folder); - if ( - await fs.access(indexFilePath).then( - () => true, - () => false - ) - ) { - // Delete the index file if it already exists. - await fs.unlink(indexFilePath); - } + if ( + await fs.access(indexFilePath).then( + () => true, + () => false + ) + ) { + // Delete the index file if it already exists. + await fs.unlink(indexFilePath); + } - const fileNames = await fs.readdir(assetsFolderPath); + const fileNames = await fs.readdir(assetsFolderPath); - // Generate the import statements for each asset. - const assetImports = fileNames - .filter((fileName) => fileName !== 'index.ts' && !/(^|\/)\.[^\/\.]/g.test(fileName)) - .map((fileName) => { - const variableName = fileName.split('.')[0].replace(/-/g, ''); - if (folder.startsWith('svgs')) { - if (lazy) - return `const ${variableName} = React.lazy(async () => ({ default: (await import('./${fileName}')).ReactComponent }));`; + // Generate the import statements for each asset. + const assetImports = fileNames + .filter( + (fileName) => + fileName !== "index.ts" && !/(^|\/)\.[^/.]/g.test(fileName) + ) + .map((fileName) => { + const variableName = fileName.split(".")[0].replace(/-/g, ""); + if (folder.startsWith("svgs")) { + if (lazy) + return `const ${variableName} = React.lazy(async () => ({ default: (await import('./${fileName}')).ReactComponent }));`; - return `import { ReactComponent as ${variableName} } from './${fileName}';`; - } - return `import ${variableName} from './${fileName}';`; - }) - .join('\n'); + return `import { ReactComponent as ${variableName} } from './${fileName}';`; + } + return `import ${variableName} from './${fileName}';`; + }) + .join("\n"); - // Generate the export statements for each asset. - const assetExports = fileNames - .filter((fileName) => fileName !== 'index.ts' && !/(^|\/)\.[^\/\.]/g.test(fileName)) - .map((fileName) => `${fileName.split('.')[0].replace(/-/g, '')}`) - .join(',\n'); + // Generate the export statements for each asset. + const assetExports = fileNames + .filter( + (fileName) => + fileName !== "index.ts" && !/(^|\/)\.[^/.]/g.test(fileName) + ) + .map((fileName) => `${fileName.split(".")[0].replace(/-/g, "")}`) + .join(",\n"); - // Generate the index file content. - const indexFileContent = await prettier.format( - ` + // Generate the index file content. + const indexFileContent = await prettier.format( + ` /* * This file was automatically generated by a script. * To regenerate this file, run: pnpm assets gen */ - ${lazy ? `import React from 'react';` : ''} + ${lazy ? `import React from 'react';` : ""} ${assetImports} export { ${assetExports} };`, - { ...options, parser: 'typescript' } - ); + { ...options, parser: "typescript" } + ); - // Write the index file. - await fs.writeFile(indexFilePath, indexFileContent); - }) - ) -); + // Write the index file. + await fs.writeFile(indexFilePath, indexFileContent); + }) + ) + ); diff --git a/packages/assets/sounds/index.ts b/packages/assets/sounds/index.ts index 459bcab32..3d2e14fbd 100644 --- a/packages/assets/sounds/index.ts +++ b/packages/assets/sounds/index.ts @@ -1,41 +1,41 @@ -import copyOgg from "./copy.ogg"; import copyMp3 from "./copy.mp3"; -import startupOgg from "./startup.ogg"; -import startupMp3 from "./startup.mp3"; -import pairingOgg from "./pairing.ogg"; -import pairingMp3 from "./pairing.mp3"; -import splatOgg from "./splat.ogg"; -import splatMp3 from "./splat.mp3"; -import splatTriggerOgg from "./splat-trigger.ogg"; -import splatTriggerMp3 from "./splat-trigger.mp3"; -import jobDoneOgg from "./job-done.ogg"; +import copyOgg from "./copy.ogg"; import jobDoneMp3 from "./job-done.mp3"; +import jobDoneOgg from "./job-done.ogg"; +import pairingMp3 from "./pairing.mp3"; +import pairingOgg from "./pairing.ogg"; +import splatMp3 from "./splat.mp3"; +import splatOgg from "./splat.ogg"; +import splatTriggerMp3 from "./splat-trigger.mp3"; +import splatTriggerOgg from "./splat-trigger.ogg"; +import startupMp3 from "./startup.mp3"; +import startupOgg from "./startup.ogg"; /** * Play a sound effect * Uses OGG with MP3 fallback for broad compatibility */ function playSound(oggSrc: string, mp3Src: string, volume = 0.5) { - const audio = new Audio(); + const audio = new Audio(); - // Try OGG first (better quality, smaller size) - if (audio.canPlayType("audio/ogg; codecs=vorbis")) { - audio.src = oggSrc; - } else { - audio.src = mp3Src; - } + // Try OGG first (better quality, smaller size) + if (audio.canPlayType("audio/ogg; codecs=vorbis")) { + audio.src = oggSrc; + } else { + audio.src = mp3Src; + } - audio.volume = volume; - audio.play().catch((err) => { - console.warn("Failed to play sound:", err); - }); + audio.volume = volume; + audio.play().catch((err) => { + console.warn("Failed to play sound:", err); + }); } export const sounds = { - copy: () => playSound(copyOgg, copyMp3, 0.3), - startup: () => playSound(startupOgg, startupMp3, 0.5), - pairing: () => playSound(pairingOgg, pairingMp3, 0.5), - splat: () => playSound(splatOgg, splatMp3, 0.05), - splatTrigger: () => playSound(splatTriggerOgg, splatTriggerMp3, 0.3), - jobDone: () => playSound(jobDoneOgg, jobDoneMp3, 0.4), + copy: () => playSound(copyOgg, copyMp3, 0.3), + startup: () => playSound(startupOgg, startupMp3, 0.5), + pairing: () => playSound(pairingOgg, pairingMp3, 0.5), + splat: () => playSound(splatOgg, splatMp3, 0.05), + splatTrigger: () => playSound(splatTriggerOgg, splatTriggerMp3, 0.3), + jobDone: () => playSound(jobDoneOgg, jobDoneMp3, 0.4), }; diff --git a/packages/assets/svgs/ext/Extras/urls.ts b/packages/assets/svgs/ext/Extras/urls.ts index 83ea42e4c..0b69fa9e8 100644 --- a/packages/assets/svgs/ext/Extras/urls.ts +++ b/packages/assets/svgs/ext/Extras/urls.ts @@ -4,18 +4,18 @@ */ // Use glob to import all SVGs as URLs -const modules = import.meta.glob('./*.svg', { - eager: true, - query: '?url', - import: 'default' +const modules = import.meta.glob("./*.svg", { + eager: true, + query: "?url", + import: "default", }); // Create a clean mapping: filename -> URL export const beardedIconUrls: Record = {}; -Object.keys(modules).forEach(path => { - // Extract filename without path and extension - // "./typescript.svg" -> "typescript" - const name = path.replace('./', '').replace('.svg', ''); - beardedIconUrls[name] = modules[path]; +Object.keys(modules).forEach((path) => { + // Extract filename without path and extension + // "./typescript.svg" -> "typescript" + const name = path.replace("./", "").replace(".svg", ""); + beardedIconUrls[name] = modules[path]; }); diff --git a/packages/assets/svgs/ext/icons.json b/packages/assets/svgs/ext/icons.json index d17698f05..25b1cbdb3 100644 --- a/packages/assets/svgs/ext/icons.json +++ b/packages/assets/svgs/ext/icons.json @@ -1 +1,4330 @@ -{"hidesExplorerArrows":true,"iconDefinitions":{"_file":{"iconPath":"./icons/file.svg"},"_folder":{"iconPath":"./icons/folder.svg"},"_folder_open":{"iconPath":"./icons/folder_open.svg"},"_root_folder":{"iconPath":"./icons/root_folder.svg"},"_root_folder_open":{"iconPath":"./icons/root_folder_open.svg"},"_root_folder_light":{"iconPath":"./icons/root_folder_light.svg"},"_root_folder_light_open":{"iconPath":"./icons/root_folder_light_open.svg"},"ace":{"iconPath":"./icons/ace.svg"},"acemanifest":{"iconPath":"./icons/acemanifest.svg"},"adoc":{"iconPath":"./icons/adoc.svg"},"adonis":{"iconPath":"./icons/adonis.svg"},"adonisconfig":{"iconPath":"./icons/adonisconfig.svg"},"afdesign":{"iconPath":"./icons/afdesign.svg"},"afphoto":{"iconPath":"./icons/afphoto.svg"},"afpub":{"iconPath":"./icons/afpub.svg"},"ai":{"iconPath":"./icons/ai.svg"},"air":{"iconPath":"./icons/air.svg"},"angular":{"iconPath":"./icons/angular.svg"},"anim":{"iconPath":"./icons/anim.svg"},"astro":{"iconPath":"./icons/astro.svg"},"astroconfig":{"iconPath":"./icons/astroconfig.svg"},"atomizer":{"iconPath":"./icons/atomizer.svg"},"audio":{"iconPath":"./icons/audio.svg"},"audiomp3":{"iconPath":"./icons/audiomp3.svg"},"audioogg":{"iconPath":"./icons/audioogg.svg"},"audiowav":{"iconPath":"./icons/audiowav.svg"},"audiowv":{"iconPath":"./icons/audiowv.svg"},"azure":{"iconPath":"./icons/azure.svg"},"babel":{"iconPath":"./icons/babel.svg"},"ballerina":{"iconPath":"./icons/ballerina.svg"},"ballerinaconfig":{"iconPath":"./icons/ballerinaconfig.svg"},"bat":{"iconPath":"./icons/bat.svg"},"bazel":{"iconPath":"./icons/bazel.svg"},"bazelignore":{"iconPath":"./icons/bazelignore.svg"},"bicep":{"iconPath":"./icons/bicep.svg"},"bicepconfig":{"iconPath":"./icons/bicepconfig.svg"},"bicepparam":{"iconPath":"./icons/bicepparam.svg"},"binary":{"iconPath":"./icons/binary.svg"},"biome":{"iconPath":"./icons/biome.svg"},"blade":{"iconPath":"./icons/blade.svg"},"brotli":{"iconPath":"./icons/brotli.svg"},"browserslist":{"iconPath":"./icons/browserslist.svg"},"bruno":{"iconPath":"./icons/bruno.svg"},"bsconfig":{"iconPath":"./icons/bsconfig.svg"},"buck":{"iconPath":"./icons/buck.svg"},"bun":{"iconPath":"./icons/bun.svg"},"bundler":{"iconPath":"./icons/bundler.svg"},"bunlock":{"iconPath":"./icons/bunlock.svg"},"c":{"iconPath":"./icons/c.svg"},"cargo":{"iconPath":"./icons/cargo.svg"},"cargolock":{"iconPath":"./icons/cargolock.svg"},"cert":{"iconPath":"./icons/cert.svg"},"cheader":{"iconPath":"./icons/cheader.svg"},"civet":{"iconPath":"./icons/civet.svg"},"claude":{"iconPath":"./icons/claude.svg"},"cli":{"iconPath":"./icons/cli.svg"},"clojure":{"iconPath":"./icons/clojure.svg"},"cmake":{"iconPath":"./icons/cmake.svg"},"codeworkspace":{"iconPath":"./icons/codeworkspace.svg"},"coffeescript":{"iconPath":"./icons/coffeescript.svg"},"commitlint":{"iconPath":"./icons/commitlint.svg"},"compodoc":{"iconPath":"./icons/compodoc.svg"},"composer":{"iconPath":"./icons/composer.svg"},"composerlock":{"iconPath":"./icons/composerlock.svg"},"conan":{"iconPath":"./icons/conan.svg"},"conf":{"iconPath":"./icons/conf.svg"},"copilot":{"iconPath":"./icons/copilot.svg"},"cpp":{"iconPath":"./icons/cpp.svg"},"crystal":{"iconPath":"./icons/crystal.svg"},"csharp":{"iconPath":"./icons/csharp.svg"},"cshtml":{"iconPath":"./icons/cshtml.svg"},"csproj":{"iconPath":"./icons/csproj.svg"},"css":{"iconPath":"./icons/css.svg"},"cssmap":{"iconPath":"./icons/cssmap.svg"},"csv":{"iconPath":"./icons/csv.svg"},"cucumber":{"iconPath":"./icons/cucumber.svg"},"cursor":{"iconPath":"./icons/cursor.svg"},"cypress":{"iconPath":"./icons/cypress.svg"},"cypressjs":{"iconPath":"./icons/cypressjs.svg"},"cypressts":{"iconPath":"./icons/cypressts.svg"},"d":{"iconPath":"./icons/d.svg"},"dartlang":{"iconPath":"./icons/dartlang.svg"},"delphiproject":{"iconPath":"./icons/delphiproject.svg"},"diff":{"iconPath":"./icons/diff.svg"},"docker":{"iconPath":"./icons/docker.svg"},"dockerdebug":{"iconPath":"./icons/dockerdebug.svg"},"dockerignore":{"iconPath":"./icons/dockerignore.svg"},"drawio":{"iconPath":"./icons/drawio.svg"},"drizzle":{"iconPath":"./icons/drizzle.svg"},"dsstore":{"iconPath":"./icons/dsstore.svg"},"dune":{"iconPath":"./icons/dune.svg"},"duneproject":{"iconPath":"./icons/duneproject.svg"},"edge":{"iconPath":"./icons/edge.svg"},"editorconfig":{"iconPath":"./icons/editorconfig.svg"},"eex":{"iconPath":"./icons/eex.svg"},"elixir":{"iconPath":"./icons/elixir.svg"},"elm":{"iconPath":"./icons/elm.svg"},"env":{"iconPath":"./icons/env.svg"},"eraser":{"iconPath":"./icons/eraser.svg"},"erb":{"iconPath":"./icons/erb.svg"},"erlang":{"iconPath":"./icons/erlang.svg"},"esbuild":{"iconPath":"./icons/esbuild.svg"},"eslint":{"iconPath":"./icons/eslint.svg"},"eslintignore":{"iconPath":"./icons/eslintignore.svg"},"excalidraw":{"iconPath":"./icons/excalidraw.svg"},"exs":{"iconPath":"./icons/exs.svg"},"exx":{"iconPath":"./icons/exx.svg"},"farm":{"iconPath":"./icons/farm.svg"},"figma":{"iconPath":"./icons/figma.svg"},"file":{"iconPath":"./icons/file.svg"},"file_light":{"iconPath":"./icons/file_light.svg"},"flakelock":{"iconPath":"./icons/flakelock.svg"},"flutter":{"iconPath":"./icons/flutter.svg"},"flutterlock":{"iconPath":"./icons/flutterlock.svg"},"flutterpackage":{"iconPath":"./icons/flutterpackage.svg"},"folder":{"iconPath":"./icons/folder.svg"},"folder_open":{"iconPath":"./icons/folder_open.svg"},"fonteot":{"iconPath":"./icons/fonteot.svg"},"fontotf":{"iconPath":"./icons/fontotf.svg"},"fontttf":{"iconPath":"./icons/fontttf.svg"},"fontwoff":{"iconPath":"./icons/fontwoff.svg"},"fontwoff2":{"iconPath":"./icons/fontwoff2.svg"},"freemarker":{"iconPath":"./icons/freemarker.svg"},"fsharp":{"iconPath":"./icons/fsharp.svg"},"gbl":{"iconPath":"./icons/gbl.svg"},"git":{"iconPath":"./icons/git.svg"},"gitlab":{"iconPath":"./icons/gitlab.svg"},"gleam":{"iconPath":"./icons/gleam.svg"},"gleamconfig":{"iconPath":"./icons/gleamconfig.svg"},"go":{"iconPath":"./icons/go.svg"},"godot":{"iconPath":"./icons/godot.svg"},"go_package":{"iconPath":"./icons/go_package.svg"},"gradle":{"iconPath":"./icons/gradle.svg"},"gradlebat":{"iconPath":"./icons/gradlebat.svg"},"gradlekotlin":{"iconPath":"./icons/gradlekotlin.svg"},"grain":{"iconPath":"./icons/grain.svg"},"graphql":{"iconPath":"./icons/graphql.svg"},"groovy":{"iconPath":"./icons/groovy.svg"},"grunt":{"iconPath":"./icons/grunt.svg"},"gulp":{"iconPath":"./icons/gulp.svg"},"h":{"iconPath":"./icons/h.svg"},"haml":{"iconPath":"./icons/haml.svg"},"handlebars":{"iconPath":"./icons/handlebars.svg"},"hardhat":{"iconPath":"./icons/hardhat.svg"},"hash":{"iconPath":"./icons/hash.svg"},"hashicorp":{"iconPath":"./icons/hashicorp.svg"},"haskell":{"iconPath":"./icons/haskell.svg"},"haxe":{"iconPath":"./icons/haxe.svg"},"haxeml":{"iconPath":"./icons/haxeml.svg"},"hpp":{"iconPath":"./icons/hpp.svg"},"htaccess":{"iconPath":"./icons/htaccess.svg"},"html":{"iconPath":"./icons/html.svg"},"http":{"iconPath":"./icons/http.svg"},"identifier":{"iconPath":"./icons/identifier.svg"},"image":{"iconPath":"./icons/image.svg"},"imagegif":{"iconPath":"./icons/imagegif.svg"},"imageico":{"iconPath":"./icons/imageico.svg"},"imagejpg":{"iconPath":"./icons/imagejpg.svg"},"imagepng":{"iconPath":"./icons/imagepng.svg"},"imagewebp":{"iconPath":"./icons/imagewebp.svg"},"imba":{"iconPath":"./icons/imba.svg"},"info":{"iconPath":"./icons/info.svg"},"instructions":{"iconPath":"./icons/instructions.svg"},"ipynb":{"iconPath":"./icons/ipynb.svg"},"jar":{"iconPath":"./icons/jar.svg"},"java":{"iconPath":"./icons/java.svg"},"jenkins":{"iconPath":"./icons/jenkins.svg"},"jest":{"iconPath":"./icons/jest.svg"},"jinja":{"iconPath":"./icons/jinja.svg"},"js":{"iconPath":"./icons/js.svg"},"jsmap":{"iconPath":"./icons/jsmap.svg"},"json":{"iconPath":"./icons/json.svg"},"jsp":{"iconPath":"./icons/jsp.svg"},"julia":{"iconPath":"./icons/julia.svg"},"karma":{"iconPath":"./icons/karma.svg"},"keep":{"iconPath":"./icons/keep.svg"},"key":{"iconPath":"./icons/key.svg"},"knex":{"iconPath":"./icons/knex.svg"},"knip":{"iconPath":"./icons/knip.svg"},"kotlin":{"iconPath":"./icons/kotlin.svg"},"kotlins":{"iconPath":"./icons/kotlins.svg"},"krita":{"iconPath":"./icons/krita.svg"},"latex":{"iconPath":"./icons/latex.svg"},"launch":{"iconPath":"./icons/launch.svg"},"lazarusproject":{"iconPath":"./icons/lazarusproject.svg"},"less":{"iconPath":"./icons/less.svg"},"license":{"iconPath":"./icons/license.svg"},"light_editorconfig":{"iconPath":"./icons/light_editorconfig.svg"},"liquid":{"iconPath":"./icons/liquid.svg"},"llvm":{"iconPath":"./icons/llvm.svg"},"lock":{"iconPath":"./icons/lock.svg"},"log":{"iconPath":"./icons/log.svg"},"lua":{"iconPath":"./icons/lua.svg"},"m":{"iconPath":"./icons/m.svg"},"makefile":{"iconPath":"./icons/makefile.svg"},"manifest":{"iconPath":"./icons/manifest.svg"},"markdown":{"iconPath":"./icons/markdown.svg"},"markdownx":{"iconPath":"./icons/markdownx.svg"},"maven":{"iconPath":"./icons/maven.svg"},"mermaid":{"iconPath":"./icons/mermaid.svg"},"mesh":{"iconPath":"./icons/mesh.svg"},"mgcb":{"iconPath":"./icons/mgcb.svg"},"mint":{"iconPath":"./icons/mint.svg"},"mix":{"iconPath":"./icons/mix.svg"},"mixlock":{"iconPath":"./icons/mixlock.svg"},"mjml":{"iconPath":"./icons/mjml.svg"},"mkdocs":{"iconPath":"./icons/mkdocs.svg"},"mockoon":{"iconPath":"./icons/mockoon.svg"},"motoko":{"iconPath":"./icons/motoko.svg"},"mov":{"iconPath":"./icons/mov.svg"},"mp4":{"iconPath":"./icons/mp4.svg"},"mtl":{"iconPath":"./icons/mtl.svg"},"mustache":{"iconPath":"./icons/mustache.svg"},"nelua":{"iconPath":"./icons/nelua.svg"},"neon":{"iconPath":"./icons/neon.svg"},"nestjs":{"iconPath":"./icons/nestjs.svg"},"nestjscontroller":{"iconPath":"./icons/nestjscontroller.svg"},"nestjsdecorator":{"iconPath":"./icons/nestjsdecorator.svg"},"nestjsdto":{"iconPath":"./icons/nestjsdto.svg"},"nestjsentity":{"iconPath":"./icons/nestjsentity.svg"},"nestjsfilter":{"iconPath":"./icons/nestjsfilter.svg"},"nestjsguard":{"iconPath":"./icons/nestjsguard.svg"},"nestjsinterceptor":{"iconPath":"./icons/nestjsinterceptor.svg"},"nestjsmodule":{"iconPath":"./icons/nestjsmodule.svg"},"nestjsrepository":{"iconPath":"./icons/nestjsrepository.svg"},"nestjsresolver":{"iconPath":"./icons/nestjsresolver.svg"},"nestjsservice":{"iconPath":"./icons/nestjsservice.svg"},"nestscheduler":{"iconPath":"./icons/nestscheduler.svg"},"netlify":{"iconPath":"./icons/netlify.svg"},"nextconfig":{"iconPath":"./icons/nextconfig.svg"},"nextron":{"iconPath":"./icons/nextron.svg"},"nginx":{"iconPath":"./icons/nginx.svg"},"nim":{"iconPath":"./icons/nim.svg"},"nix":{"iconPath":"./icons/nix.svg"},"njk":{"iconPath":"./icons/njk.svg"},"node":{"iconPath":"./icons/node.svg"},"nodemon":{"iconPath":"./icons/nodemon.svg"},"npm":{"iconPath":"./icons/npm.svg"},"npmlock":{"iconPath":"./icons/npmlock.svg"},"nuxt":{"iconPath":"./icons/nuxt.svg"},"nvidia":{"iconPath":"./icons/nvidia.svg"},"nvim":{"iconPath":"./icons/nvim.svg"},"nvm":{"iconPath":"./icons/nvm.svg"},"nx":{"iconPath":"./icons/nx.svg"},"obj":{"iconPath":"./icons/obj.svg"},"ocaml":{"iconPath":"./icons/ocaml.svg"},"ocamli":{"iconPath":"./icons/ocamli.svg"},"ocamll":{"iconPath":"./icons/ocamll.svg"},"ocamly":{"iconPath":"./icons/ocamly.svg"},"odin":{"iconPath":"./icons/odin.svg"},"opengl":{"iconPath":"./icons/opengl.svg"},"oxlint":{"iconPath":"./icons/oxlint.svg"},"panda":{"iconPath":"./icons/panda.svg"},"parcel":{"iconPath":"./icons/parcel.svg"},"pascal":{"iconPath":"./icons/pascal.svg"},"pdf":{"iconPath":"./icons/pdf.svg"},"perl":{"iconPath":"./icons/perl.svg"},"perlm":{"iconPath":"./icons/perlm.svg"},"pfx":{"iconPath":"./icons/pfx.svg"},"photoshop":{"iconPath":"./icons/photoshop.svg"},"php":{"iconPath":"./icons/php.svg"},"plantuml":{"iconPath":"./icons/plantuml.svg"},"playright":{"iconPath":"./icons/playright.svg"},"plop":{"iconPath":"./icons/plop.svg"},"pnpm":{"iconPath":"./icons/pnpm.svg"},"pnpmlock":{"iconPath":"./icons/pnpmlock.svg"},"poetry":{"iconPath":"./icons/poetry.svg"},"poetrylock":{"iconPath":"./icons/poetrylock.svg"},"postcssconfig":{"iconPath":"./icons/postcssconfig.svg"},"powershell":{"iconPath":"./icons/powershell.svg"},"powershelldata":{"iconPath":"./icons/powershelldata.svg"},"powershellmodule":{"iconPath":"./icons/powershellmodule.svg"},"precommit":{"iconPath":"./icons/precommit.svg"},"prettier":{"iconPath":"./icons/prettier.svg"},"prettierignore":{"iconPath":"./icons/prettierignore.svg"},"prisma":{"iconPath":"./icons/prisma.svg"},"prolog":{"iconPath":"./icons/prolog.svg"},"prompt":{"iconPath":"./icons/prompt.svg"},"properties":{"iconPath":"./icons/properties.svg"},"proto":{"iconPath":"./icons/proto.svg"},"pug":{"iconPath":"./icons/pug.svg"},"pvk":{"iconPath":"./icons/pvk.svg"},"pyproject":{"iconPath":"./icons/pyproject.svg"},"python":{"iconPath":"./icons/python.svg"},"qt":{"iconPath":"./icons/qt.svg"},"quarkus":{"iconPath":"./icons/quarkus.svg"},"quasar":{"iconPath":"./icons/quasar.svg"},"r":{"iconPath":"./icons/r.svg"},"racket":{"iconPath":"./icons/racket.svg"},"raku":{"iconPath":"./icons/raku.svg"},"razor":{"iconPath":"./icons/razor.svg"},"reactjs":{"iconPath":"./icons/reactjs.svg"},"reactts":{"iconPath":"./icons/reactts.svg"},"readme":{"iconPath":"./icons/readme.svg"},"redis":{"iconPath":"./icons/redis.svg"},"rego":{"iconPath":"./icons/rego.svg"},"remix":{"iconPath":"./icons/remix.svg"},"rescript":{"iconPath":"./icons/rescript.svg"},"rescriptinterface":{"iconPath":"./icons/rescriptinterface.svg"},"restructuredtext":{"iconPath":"./icons/restructuredtext.svg"},"rjson":{"iconPath":"./icons/rjson.svg"},"robots":{"iconPath":"./icons/robots.svg"},"rollup":{"iconPath":"./icons/rollup.svg"},"rome":{"iconPath":"./icons/rome.svg"},"ron":{"iconPath":"./icons/ron.svg"},"root_folder":{"iconPath":"./icons/root_folder.svg"},"root_folder_light":{"iconPath":"./icons/root_folder_light.svg"},"root_folder_light_open":{"iconPath":"./icons/root_folder_light_open.svg"},"root_folder_open":{"iconPath":"./icons/root_folder_open.svg"},"ruby":{"iconPath":"./icons/ruby.svg"},"rust":{"iconPath":"./icons/rust.svg"},"rustfmt":{"iconPath":"./icons/rustfmt.svg"},"sails":{"iconPath":"./icons/sails.svg"},"salesforce":{"iconPath":"./icons/salesforce.svg"},"sass":{"iconPath":"./icons/sass.svg"},"scala":{"iconPath":"./icons/scala.svg"},"scss":{"iconPath":"./icons/scss.svg"},"sentinel":{"iconPath":"./icons/sentinel.svg"},"sequelize":{"iconPath":"./icons/sequelize.svg"},"shaderlab":{"iconPath":"./icons/shaderlab.svg"},"shell":{"iconPath":"./icons/shell.svg"},"silq":{"iconPath":"./icons/silq.svg"},"slim":{"iconPath":"./icons/slim.svg"},"sln":{"iconPath":"./icons/sln.svg"},"smarty":{"iconPath":"./icons/smarty.svg"},"sol":{"iconPath":"./icons/sol.svg"},"spc":{"iconPath":"./icons/spc.svg"},"sql":{"iconPath":"./icons/sql.svg"},"sqlite":{"iconPath":"./icons/sqlite.svg"},"storybook":{"iconPath":"./icons/storybook.svg"},"stylelint":{"iconPath":"./icons/stylelint.svg"},"stylelintignore":{"iconPath":"./icons/stylelintignore.svg"},"stylus":{"iconPath":"./icons/stylus.svg"},"suo":{"iconPath":"./icons/suo.svg"},"svelte":{"iconPath":"./icons/svelte.svg"},"svelteconfig":{"iconPath":"./icons/svelteconfig.svg"},"svg":{"iconPath":"./icons/svg.svg"},"swift":{"iconPath":"./icons/swift.svg"},"symfony":{"iconPath":"./icons/symfony.svg"},"tailwind":{"iconPath":"./icons/tailwind.svg"},"tauri":{"iconPath":"./icons/tauri.svg"},"taze":{"iconPath":"./icons/taze.svg"},"terrafile":{"iconPath":"./icons/terrafile.svg"},"terraform":{"iconPath":"./icons/terraform.svg"},"terraformvars":{"iconPath":"./icons/terraformvars.svg"},"terraformversion":{"iconPath":"./icons/terraformversion.svg"},"testjs":{"iconPath":"./icons/testjs.svg"},"testts":{"iconPath":"./icons/testts.svg"},"tmpl":{"iconPath":"./icons/tmpl.svg"},"todo":{"iconPath":"./icons/todo.svg"},"toml":{"iconPath":"./icons/toml.svg"},"toolversions":{"iconPath":"./icons/toolversions.svg"},"tox":{"iconPath":"./icons/tox.svg"},"travis":{"iconPath":"./icons/travis.svg"},"tres":{"iconPath":"./icons/tres.svg"},"tscn":{"iconPath":"./icons/tscn.svg"},"tsconfig":{"iconPath":"./icons/tsconfig.svg"},"tsx":{"iconPath":"./icons/tsx.svg"},"turbo":{"iconPath":"./icons/turbo.svg"},"twig":{"iconPath":"./icons/twig.svg"},"txt":{"iconPath":"./icons/txt.svg"},"typescript":{"iconPath":"./icons/typescript.svg"},"typescriptdef":{"iconPath":"./icons/typescriptdef.svg"},"ui":{"iconPath":"./icons/ui.svg"},"unocss":{"iconPath":"./icons/unocss.svg"},"user":{"iconPath":"./icons/user.svg"},"v":{"iconPath":"./icons/v.svg"},"vanillaextract":{"iconPath":"./icons/vanillaextract.svg"},"vb":{"iconPath":"./icons/vb.svg"},"vercel":{"iconPath":"./icons/vercel.svg"},"version":{"iconPath":"./icons/version.svg"},"vhd":{"iconPath":"./icons/vhd.svg"},"vhdl":{"iconPath":"./icons/vhdl.svg"},"video":{"iconPath":"./icons/video.svg"},"vite":{"iconPath":"./icons/vite.svg"},"viteenv":{"iconPath":"./icons/viteenv.svg"},"vitest":{"iconPath":"./icons/vitest.svg"},"vmod":{"iconPath":"./icons/vmod.svg"},"vscode":{"iconPath":"./icons/vscode.svg"},"vue":{"iconPath":"./icons/vue.svg"},"vueconfig":{"iconPath":"./icons/vueconfig.svg"},"wasm":{"iconPath":"./icons/wasm.svg"},"webpack":{"iconPath":"./icons/webpack.svg"},"wgsl":{"iconPath":"./icons/wgsl.svg"},"windi":{"iconPath":"./icons/windi.svg"},"wren":{"iconPath":"./icons/wren.svg"},"xmake":{"iconPath":"./icons/xmake.svg"},"xml":{"iconPath":"./icons/xml.svg"},"yaml":{"iconPath":"./icons/yaml.svg"},"yang":{"iconPath":"./icons/yang.svg"},"yarn":{"iconPath":"./icons/yarn.svg"},"yarnerror":{"iconPath":"./icons/yarnerror.svg"},"yarnignore":{"iconPath":"./icons/yarnignore.svg"},"yarnlock":{"iconPath":"./icons/yarnlock.svg"},"yin":{"iconPath":"./icons/yin.svg"},"zig":{"iconPath":"./icons/zig.svg"},"zip":{"iconPath":"./icons/zip.svg"}},"file":"_file","folder":"_folder","folderExpanded":"_folder_open","rootFolder":"_root_folder","rootFolderExpanded":"_root_folder_open","fileExtensions":{"wma":"audio","wav":"audiowav","vox":"audio","tta":"audio","raw":"audio","ra":"audio","opus":"audio","ogg":"audioogg","oga":"audio","msv":"audio","mpc":"audio","mp3":"audiomp3","mogg":"audio","mmf":"audio","m4p":"audio","m4b":"audio","m4a":"audio","ivs":"audio","iklax":"audio","gsm":"audio","flac":"audio","dvf":"audio","dss":"audio","dct":"audio","au":"audio","ape":"audio","amr":"audio","aiff":"audio","act":"audio","aac":"audio","wmv":"video","webm":"video","vob":"video","svi":"video","rmvb":"video","rm":"video","ogv":"video","nsv":"video","mpv":"video","mpg":"video","mpeg2":"video","mpeg":"video","mpe":"video","mp4":"mp4","mp2":"video","mov":"mov","mk3d":"video","mkv":"video","m4v":"video","m2v":"video","flv":"video","f4v":"video","f4p":"video","f4b":"video","f4a":"video","qt":"video","divx":"video","avi":"video","amv":"video","asf":"video","3gp":"video","3g2":"video","ico":"imageico","tiff":"image","bmp":"image","png":"imagepng","gif":"imagegif","jpg":"imagejpg","jpeg":"imagejpg","7z":"zip","7zip":"zip","blade.php":"blade","cfg.dist":"conf","cjs.map":"jsmap","controller.js":"nestjscontroller","controller.ts":"nestjscontroller","repository.js":"nestjsrepository","repository.ts":"nestjsrepository","scheduler.js":"nestscheduler","scheduler.ts":"nestscheduler","css.js":"vanillaextract","css.ts":"vanillaextract","css.map":"cssmap","d.ts":"typescriptdef","decorator.js":"nestjsdecorator","decorator.ts":"nestjsdecorator","drawio.png":"drawio","drawio.svg":"drawio","e2e-spec.ts":"testts","e2e-spec.tsx":"testts","e2e-test.ts":"testts","e2e-test.tsx":"testts","filter.js":"nestjsfilter","filter.ts":"nestjsfilter","format.ps1xml":"powershell_format","gemfile.lock":"bundler","gradle.kts":"gradlekotlin","guard.js":"nestjsguard","guard.ts":"nestjsguard","jar.old":"jar","js.flow":"flow","js.map":"jsmap","js.snap":"jest_snapshot","json-ld":"jsonld","jsx.snap":"jest_snapshot","layout.htm":"layout","layout.html":"layout","marko.js":"markojs","mjs.map":"jsmap","module.ts":"nestjsmodule","resolver.js":"nestjsresolver","resolver.ts":"nestjsresolver","service.js":"nestjsservice","service.ts":"nestjsservice","entity.js":"nestjsentity","entity.ts":"nestjsentity","interceptor.js":"nestjsinterceptor","interceptor.ts":"nestjsinterceptor","dto.js":"nestjsdto","dto.ts":"nestjsdto","spec.js":"testjs","spec.jsx":"testjs","spec.mjs":"testjs","spec.ts":"testts","spec.tsx":"testts","stories.js":"storybook","stories.jsx":"storybook","stories.ts":"storybook","stories.tsx":"storybook","stories.svelte":"storybook","story.js":"storybook","story.jsx":"storybook","story.ts":"storybook","story.tsx":"storybook","story.svelte":"storybook","test.cjs":"testjs","test.cts":"testts","test.js":"testjs","test.jsx":"testjs","test.mjs":"testjs","test.mts":"testts","test.ts":"testts","test.tsx":"testts","ts.snap":"jest_snapshot","tsx.snap":"jest_snapshot","types.ps1xml":"powershell_types","a":"binary","accda":"access","accdb":"access","accdc":"access","accde":"access","accdp":"access","accdr":"access","accdt":"access","accdu":"access","ade":"access","adoc":"adoc","adp":"access","afdesign":"afdesign","affinitydesigner":"afdesign","affinityphoto":"afphoto","affinitypublisher":"afpub","afphoto":"afphoto","afpub":"afpub","ai":"ai","app":"binary","ascx":"aspx","asm":"binary","aspx":"aspx","astro":"astro","awk":"awk","bat":"bat","bc":"llvm","bcmx":"outlook","bicep":"bicep","bin":"binary","blade":"blade","bz2":"zip","bzip2":"zip","c":"c","cake":"cake","cer":"cert","pvk":"pvk","pfx":"pfx","spc":"spc","cfg":"conf","civet":"civet","cjm":"clojure","cl":"opencl","class":"class","cli":"cli","clj":"clojure","cljc":"clojure","cljs":"clojure","cljx":"clojure","cma":"binary","cmd":"cli","cmi":"binary","cmo":"binary","cmx":"binary","cmxa":"binary","comp":"opengl","conf":"conf","cpp":"cpp","cr":"crystal","crec":"lync","crl":"cert","crt":"cert","cs":"csharp","cshtml":"cshtml","csproj":"csproj","csr":"cert","css":"css","csv":"csv","csx":"csharp","d":"d","dart":"dartlang","db":"sqlite","db3":"sqlite","der":"cert","diff":"diff","dio":"drawio","djt":"django","dll":"binary","dmp":"log","doc":"word","docm":"word","docx":"word","dot":"word","dotm":"word","dotx":"word","drawio":"drawio","dta":"stata","eco":"docpad","edge":"edge","edn":"clojure","eex":"eex","ejs":"ejs","el":"emacs","elc":"emacs","elm":"elm","enc":"license","ensime":"ensime","env":"env","eps":"eps","erb":"erb","erl":"erlang","eskip":"skipper","ex":"elixir","exe":"binary","exp":"tcl","exs":"exs","fbx":"fbx","feature":"cucumber","fig":"figma","fish":"shell","fla":"fla","fods":"excel","frag":"opengl","fs":"fsharp","fsproj":"fsproj","ftl":"freemarker","gbl":"gbl","gd":"godot","gemfile":"bundler","geom":"opengl","glsl":"opengl","gmx":"gamemaker","go":"go","godot":"godot","gql":"graphql","gradle":"gradle","groovy":"groovy","gz":"zip","h":"cheader","haml":"haml","hbs":"handlebars","hcl":"hashicorp","hl":"binary","hlsl":"opengl","hpp":"hpp","hs":"haskell","html":"html","hxp":"lime","hxproj":"haxedevelop","ibc":"idrisbin","idr":"idris","ilk":"binary","imba":"imba","inc":"inc","include":"inc","info":"info","infopathxml":"infopath","ini":"conf","ino":"arduino","ipkg":"idrispkg","ipynb":"ipynb","iuml":"plantuml","jar":"jar","java":"java","jbuilder":"jbuilder","j2":"jinja","jinja":"jinja","jinja2":"jinja","jl":"julia","json5":"json5","jsonld":"jsonld","jsp":"jsp","jss":"jss","key":"key","kit":"codekit","kt":"kotlin","kts":"kotlins","laccdb":"access","ldb":"access","less":"less","lib":"binary","lidr":"idris","liquid":"liquid","ll":"llvm","lnk":"lnk","log":"log","ls":"livescript","lucee":"cf","m":"m","makefile":"makefile","mam":"access","map":"map","maq":"access","markdown":"markdown","master":"layout","mdb":"access","mdown":"markdown","mdw":"access","mdx":"markdownx","mesh":"mesh","mex":"matlab","mexn":"matlab","mexrs6":"matlab","mf":"manifest","mint":"mint","mjml":"mjml","ml":"ocaml","mli":"ocamli","mll":"ocamll","mly":"ocamly","mn":"matlab","mo":"motoko","msg":"outlook","mst":"mustache","mum":"matlab","mustache":"mustache","mx":"matlab","mx3":"matlab","n":"binary","ndll":"binary","neon":"neon","nim":"nim","nix":"nix","njk":"njk","njs":"nunjucks","njsproj":"njsproj","nunj":"nunjucks","nupkg":"nuget","nuspec":"nuget","nvim":"nvim","o":"binary","ocrec":"lync","ods":"excel","oft":"outlook","one":"onenote","onepkg":"onenote","onetoc":"onenote","onetoc2":"onenote","opencl":"opencl","org":"org","otf":"fontotf","otm":"outlook","ovpn":"ovpn","P":"prolog","p12":"cert","p7b":"cert","p7r":"cert","pa":"powerpoint","patch":"diff","pcd":"pcl","pck":"plsql_package","pdb":"binary","pde":"arduino","pdf":"pdf","pem":"key","pex":"xml","phar":"php","php1":"php","php2":"php","php3":"php","php4":"php","php5":"php","php6":"php","phps":"php","phpsa":"php","phpt":"php","phtml":"php","pkb":"plsql_package_body","pkg":"package","pkh":"plsql_package_header","pks":"plsql_package_spec","pl":"perl","plantuml":"plantuml","plist":"config","pm":"perlm","po":"poedit","postcss":"postcssconfig","pcss":"postcssconfig","pot":"powerpoint","potm":"powerpoint","potx":"powerpoint","ppa":"powerpoint","ppam":"powerpoint","pps":"powerpoint","ppsm":"powerpoint","ppsx":"powerpoint","ppt":"powerpoint","pptm":"powerpoint","pptx":"powerpoint","pri":"qt","prisma":"prisma","pro":"prolog","properties":"properties","ps1":"powershell","psd":"photoshop","psd1":"powershelldata","psm1":"powershellmodule","psmdcp":"nuget","pst":"outlook","pu":"plantuml","pub":"publisher","puml":"plantuml","puz":"publisher","pyc":"binary","pyd":"binary","pyo":"binary","q":"q","qbs":"qbs","qvd":"qlikview","qvw":"qlikview","rake":"rake","rar":"zip","gzip":"zip","razor":"razor","rb":"ruby","reg":"registry","rego":"rego","res":"rescript","resi":"rescriptinterface","rjson":"rjson","rproj":"rproj","rs":"rust","rsx":"rust","ron":"ron","odin":"odin","rt":"reacttemplate","rwd":"matlab","pas":"pascal","pp":"pascal","p":"pascal","lpr":"lazarusproject","lps":"lazarusproject","lpi":"lazarusproject","lfm":"lazarusproject","lrs":"lazarusproject","lpk":"lazarusproject","dpr":"delphiproject","dproj":"delphiproject","dfm":"delphiproject","sass":"scss","sc":"scala","scala":"scala","scpt":"binary","scptd":"binary","scss":"scss","sentinel":"sentinel","sig":"onenote","sketch":"sketch","slddc":"matlab","sldm":"powerpoint","sldx":"powerpoint","sln":"sln","sls":"saltstack","slx":"matlab","smv":"matlab","so":"binary","sol":"sol","sql":"sql","sqlite":"sqlite","sqlite3":"sqlite","src":"cert","sss":"sss","sst":"cert","stl":"cert","storyboard":"storyboard","styl":"stylus","suo":"suo","svelte":"svelte","svg":"svg","swc":"flash","swf":"flash","swift":"swift","tar":"zip","tcl":"tcl","templ":"tmpl","tesc":"opengl","tese":"opengl","tex":"latex","texi":"tex","tf":"terraform","tfstate":"terraform","tfvars":"terraformvars","tgz":"zip","tikz":"tex","tlg":"log","tmlanguage":"xml","tmpl":"tmpl","todo":"todo","toml":"toml","tpl":"smarty","tres":"tres","tscn":"tscn","tst":"test","tsx":"reactts","jsx":"reactjs","tt2":"tt","ttf":"fontttf","twig":"twig","txt":"txt","ui":"ui","unity":"shaderlab","user":"user","v":"v","vala":"vala","vapi":"vapi","vash":"vash","vbhtml":"vbhtml","vbproj":"vbproj","vcxproj":"vcxproj","vert":"opengl","vhd":"vhd","vhdl":"vhdl","vsix":"vscode","vsixmanifest":"manifest","wasm":"wasm","webp":"imagewebp","wgsl":"wgsl","wll":"word","woff":"fontwoff","eot":"fonteot","woff2":"fontwoff2","wv":"audiowv","wxml":"wxml","wxss":"wxss","xcodeproj":"xcode","xfl":"xfl","xib":"xib","xlf":"xliff","xliff":"xliff","xls":"excel","xlsm":"excel","xlsx":"excel","xsf":"infopath","xsn":"infopath","xtp2":"infopath","xvc":"matlab","xz":"zip","yy":"gamemaker2","yyp":"gamemaker2","zig":"zig","zip":"zip","zipx":"zip","zz":"zip","deflate":"zip","brotli":"brotli","kra":"krita","mgcb":"mgcb","anim":"anim","cy.ts":"cypressts","cy.js":"cypressjs","hx":"haxe","hxml":"haxeml","gr":"grain","slim":"slim","obj":"obj","mtl":"mtl","bicepparam":"bicepparam","proto":"proto","wren":"wren","docker-compose.yml":"docker","excalidraw":"excalidraw","excalidraw.json":"excalidraw","excalidraw.svg":"excalidraw","excalidraw.png":"excalidraw","bazel":"bazel","bzl":"bazel","bazelignore":"bazelignore","bazelrc":"bazel","http":"http","rkt":"racket","rktl":"racket","bru":"bruno","nelua":"nelua","mermaid":"mermaid","mmd":"mermaid","bal":"ballerina","hash":"hash","gleam":"gleam","lock":"lock","yang":"yang","yin":"yin","mdc":"cursor","uml":"plantuml","Identifier":"identifier","cls":"salesforce",".instructions.md":"instructions",".instructions.txt":"instructions",".instructions.json":"instructions",".instructions.yaml":"instructions",".instructions.yml":"instructions","silq":"silq","eraserdiagram":"eraser"},"fileNames":{"webpack.config.images.js":"webpack","webpack.test.conf.ts":"webpack","webpack.test.conf.coffee":"webpack","webpack.test.conf.js":"webpack","webpack.rules.ts":"webpack","webpack.rules.coffee":"webpack","webpack.rules.js":"webpack","webpack.renderer.config.ts":"webpack","webpack.renderer.config.coffee":"webpack","webpack.renderer.config.js":"webpack","webpack.plugins.ts":"webpack","webpack.plugins.coffee":"webpack","webpack.plugins.js":"webpack","webpack.mix.ts":"webpack","webpack.mix.coffee":"webpack","webpack.mix.js":"webpack","webpack.main.config.ts":"webpack","webpack.main.config.coffee":"webpack","webpack.main.config.js":"webpack","webpack.prod.conf.ts":"webpack","webpack.prod.conf.coffee":"webpack","webpack.prod.conf.js":"webpack","webpack.prod.ts":"webpack","webpack.prod.coffee":"webpack","webpack.prod.js":"webpack","webpack.dev.conf.ts":"webpack","webpack.dev.conf.coffee":"webpack","webpack.dev.conf.js":"webpack","webpack.dev.ts":"webpack","webpack.dev.coffee":"webpack","webpack.dev.js":"webpack","webpack.config.production.babel.ts":"webpack","webpack.config.production.babel.coffee":"webpack","webpack.config.production.babel.js":"webpack","webpack.config.prod.babel.ts":"webpack","webpack.config.prod.babel.coffee":"webpack","webpack.config.prod.babel.js":"webpack","webpack.config.test.babel.ts":"webpack","webpack.config.test.babel.coffee":"webpack","webpack.config.test.babel.js":"webpack","webpack.config.staging.babel.ts":"webpack","webpack.config.staging.babel.coffee":"webpack","webpack.config.staging.babel.js":"webpack","webpack.config.development.babel.ts":"webpack","webpack.config.development.babel.coffee":"webpack","webpack.config.development.babel.js":"webpack","webpack.config.dev.babel.ts":"webpack","webpack.config.dev.babel.coffee":"webpack","webpack.config.dev.babel.js":"webpack","webpack.config.common.babel.ts":"webpack","webpack.config.common.babel.coffee":"webpack","webpack.config.common.babel.js":"webpack","webpack.config.base.babel.ts":"webpack","webpack.config.base.babel.coffee":"webpack","webpack.config.base.babel.js":"webpack","webpack.config.babel.ts":"webpack","webpack.config.babel.coffee":"webpack","webpack.config.babel.js":"webpack","webpack.config.production.ts":"webpack","webpack.config.production.coffee":"webpack","webpack.config.production.js":"webpack","webpack.config.prod.ts":"webpack","webpack.config.prod.coffee":"webpack","webpack.config.prod.js":"webpack","webpack.config.test.ts":"webpack","webpack.config.test.coffee":"webpack","webpack.config.test.js":"webpack","webpack.config.staging.ts":"webpack","webpack.config.staging.coffee":"webpack","webpack.config.staging.js":"webpack","webpack.config.development.ts":"webpack","webpack.config.development.coffee":"webpack","webpack.config.development.js":"webpack","webpack.config.dev.ts":"webpack","webpack.config.dev.coffee":"webpack","webpack.config.dev.js":"webpack","webpack.config.common.ts":"webpack","webpack.config.common.coffee":"webpack","webpack.config.common.js":"webpack","webpack.config.base.ts":"webpack","webpack.config.base.coffee":"webpack","webpack.config.base.js":"webpack","webpack.config.ts":"webpack","webpack.config.coffee":"webpack","webpack.config.js":"webpack","webpack.common.ts":"webpack","webpack.common.coffee":"webpack","webpack.common.js":"webpack","webpack.base.conf.ts":"webpack","webpack.base.conf.coffee":"webpack","webpack.base.conf.js":"webpack",".angular-cli.json":"angular","angular-cli.json":"angular","angular.json":"angular",".angular.json":"angular","api-extractor.json":"api_extractor","api-extractor-base.json":"api_extractor","appveyor.yml":"appveyor",".appveyor.yml":"appveyor","aurelia.json":"aurelia","azure-pipelines.yml":"azure",".vsts-ci.yml":"azure",".babelrc":"babel",".babelignore":"babel",".babelrc.js":"babel",".babelrc.cjs":"babel",".babelrc.mjs":"babel",".babelrc.json":"babel","babel.config.js":"babel","babel.config.cjs":"babel","babel.config.mjs":"babel","babel.config.json":"babel","vetur.config.js":"vue","vetur.config.ts":"vue",".bzrignore":"bazaar",".bazelrc":"bazel","bazel.rc":"bazel","bazel.bazelrc":"bazel","BUILD":"bazel","bitbucket-pipelines.yml":"bitbucketpipeline",".bithoundrc":"bithound",".bowerrc":"bower","bower.json":"bower",".browserslistrc":"browserslist","browserslist":"browserslist","gemfile":"bundler","gemfile.lock":"bundler",".ruby-version":"bundler","capacitor.config.json":"capacitor","cargo.toml":"cargo","cargo.lock":"cargo","chefignore":"chef","berksfile":"chef","berksfile.lock":"chef","policyfile":"chef","circle.yml":"circleci",".cfignore":"cloudfoundry",".codacy.yml":"codacy",".codacy.yaml":"codacy",".codeclimate.yml":"codeclimate","codecov.yml":"codecov",".codecov.yml":"codecov","config.codekit":"codekit","config.codekit2":"codekit","config.codekit3":"codekit",".config.codekit":"codekit",".config.codekit2":"codekit",".config.codekit3":"codekit","coffeelint.json":"coffeelint",".coffeelintignore":"coffeelint","composer.json":"composer","composer.lock":"composerlock","conanfile.txt":"conan","conanfile.py":"conan",".condarc":"conda",".coveralls.yml":"coveralls","crowdin.yml":"crowdin",".csscomb.json":"csscomb",".csslintrc":"csslint",".cvsignore":"cvs",".boringignore":"darcs","dependabot.yml":"dependabot","dependencies.yml":"dependencies","devcontainer.json":"devcontainer","docker-compose-prod.yml":"docker","docker-compose.alpha.yaml":"docker","docker-compose.alpha.yml":"docker","docker-compose.beta.yaml":"docker","docker-compose.beta.yml":"docker","docker-compose.ci-build.yml":"docker","docker-compose.ci.yaml":"docker","docker-compose.ci.yml":"docker","docker-compose.dev.yaml":"docker","docker-compose.dev.yml":"docker","docker-compose.development.yaml":"docker","docker-compose.development.yml":"docker","docker-compose.local.yaml":"docker","docker-compose.local.yml":"docker","docker-compose.override.yaml":"docker","docker-compose.override.yml":"docker","docker-compose.prod.yaml":"docker","docker-compose.prod.yml":"docker","docker-compose.production.yaml":"docker","docker-compose.production.yml":"docker","docker-compose.stage.yaml":"docker","docker-compose.stage.yml":"docker","docker-compose.staging.yaml":"docker","docker-compose.staging.yml":"docker","docker-compose.test.yaml":"docker","docker-compose.test.yml":"docker","docker-compose.testing.yaml":"docker","docker-compose.testing.yml":"docker","docker-compose.vs.debug.yml":"docker","docker-compose.vs.release.yml":"docker","docker-compose.web.yaml":"docker","docker-compose.web.yml":"docker","docker-compose.worker.yaml":"docker","docker-compose.worker.yml":"docker","docker-compose.yaml":"docker","docker-compose.yml":"docker","Dockerfile-production":"docker","dockerfile.alpha":"docker","dockerfile.beta":"docker","dockerfile.ci":"docker","dockerfile.dev":"docker","dockerfile.development":"docker","dockerfile.local":"docker","dockerfile.prod":"docker","dockerfile.production":"docker","dockerfile.stage":"docker","dockerfile.staging":"docker","dockerfile.test":"docker","dockerfile.testing":"docker","dockerfile.web":"docker","dockerfile.worker":"docker","dockerfile":"docker","docker-compose.debug.yml":"dockerdebug","docker-cloud.yml":"docker",".dockerignore":"dockerignore",".doczrc":"docz","docz.js":"docz","docz.json":"docz",".docz.js":"docz",".docz.json":"docz","doczrc.js":"docz","doczrc.json":"docz","docz.config.js":"docz","docz.config.json":"docz",".dojorc":"dojo",".drone.yml":"drone",".drone.yml.sig":"drone",".dvc":"dvc",".editorconfig":"editorconfig","elm-package.json":"elm",".ember-cli":"ember","emakefile":"erlang",".emakerfile":"erlang",".eslintrc":"eslint",".eslintignore":"eslintignore",".eslintcache":"eslint",".eslintrc.js":"eslint",".eslintrc.mjs":"eslint",".eslintrc.cjs":"eslint",".eslintrc.json":"eslint",".eslintrc.yaml":"eslint",".eslintrc.yml":"eslint",".eslintrc.browser.json":"eslint",".eslintrc.base.json":"eslint","eslint-preset.js":"eslint","eslint.config.js":"eslint","eslint.config.cjs":"eslint","eslint.config.mjs":"eslint","eslint.config.ts":"eslint","_eslintrc.cjs":"eslint","app.json":"expo","app.config.js":"expo","app.config.json":"expo","app.config.json5":"expo","favicon.ico":"favicon",".firebaserc":"firebase","firebase.json":"firebasehosting","firestore.rules":"firestore","firestore.indexes.json":"firestore",".flooignore":"floobits",".flowconfig":"flow",".flutter-plugins":"flutter",".metadata":"flutter",".fossaignore":"fossa","ignore-glob":"fossil","fuse.js":"fusebox","gatsby-config.js":"gatsby","gatsby-config.ts":"gatsby","gatsby-node.js":"gatsby","gatsby-node.ts":"gatsby","gatsby-browser.js":"gatsby","gatsby-browser.ts":"gatsby","gatsby-ssr.js":"gatsby","gatsby-ssr.ts":"gatsby",".git-blame-ignore-revs":"git",".gitattributes":"git",".gitconfig":"git",".gitignore":"git",".gitmodules":"git",".gitkeep":"git",".mailmap":"git",".gitlab-ci.yml":"gitlab","glide.yml":"glide","go.sum":"go_package","go.mod":"go_package","go.work":"go_package",".gqlconfig":"graphql",".graphqlconfig":"graphql_config",".graphqlconfig.yml":"graphql_config",".graphqlconfig.yaml":"graphql_config","greenkeeper.json":"greenkeeper","gridsome.config.js":"gridsome","gridsome.config.ts":"gridsome","gridsome.server.js":"gridsome","gridsome.server.ts":"gridsome","gridsome.client.js":"gridsome","gridsome.client.ts":"gridsome","gruntfile.js":"grunt","gruntfile.cjs":"grunt","gruntfile.mjs":"grunt","gruntfile.coffee":"grunt","gruntfile.ts":"grunt","gruntfile.cts":"grunt","gruntfile.mts":"grunt","gruntfile.babel.js":"grunt","gruntfile.babel.coffee":"grunt","gruntfile.babel.ts":"grunt","gulpfile.js":"gulp","gulpfile.coffee":"gulp","gulpfile.ts":"gulp","gulpfile.esm.js":"gulp","gulpfile.esm.coffee":"gulp","gulpfile.esm.ts":"gulp","gulpfile.babel.js":"gulp","gulpfile.babel.coffee":"gulp","gulpfile.babel.ts":"gulp","haxelib.json":"haxe","checkstyle.json":"haxecheckstyle",".p4ignore":"helix",".htmlhintrc":"htmlhint",".huskyrc":"husky","husky.config.js":"husky",".huskyrc.js":"husky",".huskyrc.json":"husky",".huskyrc.yaml":"husky",".huskyrc.yml":"husky","ionic.project":"ionic","ionic.config.json":"ionic","jakefile":"jake","jakefile.js":"jake","jest.config.json":"jest","jest.json":"jest",".jestrc":"jest",".jestrc.js":"jest",".jestrc.json":"jest","jest.config.js":"jest","jest.config.cjs":"jest","jest.config.mjs":"jest","jest.config.babel.js":"jest","jest.config.babel.cjs":"jest","jest.config.babel.mjs":"jest","jest.preset.js":"jest","jest.preset.ts":"jest","jest.preset.cjs":"jest","jest.preset.mjs":"jest",".jpmignore":"jpm",".jsbeautifyrc":"jsbeautify","jsbeautifyrc":"jsbeautify",".jsbeautify":"jsbeautify","jsbeautify":"jsbeautify","jsconfig.json":"jsconfig",".jscpd.json":"jscpd","jscpd-report.xml":"jscpd","jscpd-report.json":"jscpd","jscpd-report.html":"jscpd",".jshintrc":"jshint",".jshintignore":"jshint","karma.conf.js":"karma","karma.conf.coffee":"karma","karma.conf.ts":"karma",".kitchen.yml":"kitchenci","kitchen.yml":"kitchenci",".kiteignore":"kite","layout.html":"layout","layout.htm":"layout","lerna.json":"lerna","license":"license","licence":"license","license.md":"license","license.txt":"license","licence.md":"license","licence.txt":"license",".lighthouserc.js":"lighthouse",".lighthouserc.json":"lighthouse",".lighthouserc.yaml":"lighthouse",".lighthouserc.yml":"lighthouse","include.xml":"lime",".lintstagedrc":"lintstagedrc","lint-staged.config.js":"lintstagedrc",".lintstagedrc.js":"lintstagedrc",".lintstagedrc.json":"lintstagedrc",".lintstagedrc.yaml":"lintstagedrc",".lintstagedrc.yml":"lintstagedrc","manifest":"manifest","manifest.bak":"manifest","manifest.json":"manifest","manifest.skip":"manifes",".markdownlint.json":"markdownlint","maven.config":"maven","pom.xml":"maven","extensions.xml":"maven","settings.xml":"maven","pom.properties":"maven",".hgignore":"mercurial","mocha.opts":"mocha",".mocharc.js":"mocha",".mocharc.json":"mocha",".mocharc.jsonc":"mocha",".mocharc.yaml":"mocha",".mocharc.yml":"mocha","modernizr":"modernizr","modernizr.js":"modernizr","modernizrrc.js":"modernizr",".modernizr.js":"modernizr",".modernizrrc.js":"modernizr","moleculer.config.js":"moleculer","moleculer.config.json":"moleculer","moleculer.config.ts":"moleculer",".mtn-ignore":"monotone",".nest-cli.json":"nestjs","nest-cli.json":"nestjs","nestconfig.json":"nestjs",".nestconfig.json":"nestjs","netlify.toml":"netlify","_redirects":"netlify","ng-tailwind.js":"ng_tailwind","nginx.conf":"nginx","build.ninja":"ninja",".node-version":"node",".node_repl_history":"node",".node-gyp":"node","node_modules":"node","node_modules.json":"node","node-inspect.json":"node","node-inspect.js":"node","node-inspect.mjs":"node","node-inspect.cjs":"node","node-inspect.ts":"node","node-inspect.config.js":"node","node-inspect.config.ts":"node","node-inspect.config.cjs":"node","node-inspect.config.mjs":"node","node-inspect.config.json":"node","node-inspect.config.yaml":"node","node-inspect.config.yml":"node","node-inspectrc":"node",".node-inspectrc":"node",".node-inspectrc.json":"node",".node-inspectrc.yaml":"node",".node-inspectrc.yml":"node",".node-inspectrc.js":"node",".node-inspectrc.ts":"node",".node-inspectrc.cjs":"node",".node-inspectrc.mjs":"node","nodemon.json":"nodemon",".npmignore":"npm",".npmrc":"npm","package.json":"npm","package-lock.json":"npmlock","npm-shrinkwrap.json":"npm",".nsrirc":"nsri",".nsriignore":"nsri","nsri.config.js":"nsri",".nsrirc.js":"nsri",".nsrirc.json":"nsri",".nsrirc.yaml":"nsri",".nsrirc.yml":"nsri",".integrity.json":"nsri-integrity","nuxt.config.js":"nuxt","nuxt.config.ts":"nuxt",".nycrc":"nyc",".nycrc.json":"nyc",".merlin":"ocaml","paket.dependencies":"paket","paket.lock":"paket","paket.references":"paket","paket.template":"paket","paket.local":"paket",".php_cs":"phpcsfixer",".php_cs.dist":"phpcsfixer","phpunit":"phpunit","phpunit.xml":"phpunit","phpunit.xml.dist":"phpunit",".phraseapp.yml":"phraseapp","pipfile":"pip","pipfile.lock":"pip","platformio.ini":"platformio","pnpmfile.js":"pnpm","pnpm-workspace.yaml":"pnpm",".postcssrc":"postcssconfig",".postcssrc.json":"postcssconfig",".postcssrc.yml":"postcssconfig",".postcssrc.js":"postcssconfig",".postcssrc.cjs":"postcssconfig",".postcssrc.mjs":"postcssconfig",".postcssrc.ts":"postcssconfig",".postcssrc.cts":"postcssconfig",".postcssrc.mts":"postcssconfig","postcss.config.js":"postcssconfig","postcss.config.cjs":"postcssconfig","postcss.config.mjs":"postcssconfig","postcss.config.ts":"postcssconfig","postcss.config.cts":"postcssconfig","postcss.config.mts":"postcssconfig",".pre-commit-config.yaml":"precommit",".pre-commit-hooks.yaml":"precommit",".prettierrc":"prettier",".prettierignore":"prettierignore","prettier.config.js":"prettier","prettier.config.cjs":"prettier","prettier.config.mjs":"prettier","prettier.config.ts":"prettier","prettier.config.coffee":"prettier",".prettierrc.js":"prettier",".prettierrc.json":"prettier",".prettierrc.yml":"prettier",".prettierrc.yaml":"prettier","procfile":"procfile","protractor.conf.js":"protractor","protractor.conf.coffee":"protractor","protractor.conf.ts":"protractor",".jade-lintrc":"pug",".pug-lintrc":"pug",".jade-lint.json":"pug",".pug-lintrc.js":"pug",".pug-lintrc.json":"pug",".pyup":"pyup",".pyup.yml":"pyup","qmldir":"qmldir","quasar.conf.js":"quasar","rakefile":"rake","razzle.config.js":"razzle","readme.md":"readme","readme.txt":"readme",".rehyperc":"rehype",".rehypeignore":"rehype",".rehyperc.js":"rehype",".rehyperc.json":"rehype",".rehyperc.yml":"rehype",".rehyperc.yaml":"rehype",".remarkrc":"remark",".remarkignore":"remark",".remarkrc.js":"remark",".remarkrc.json":"remark",".remarkrc.yml":"remark",".remarkrc.yaml":"remark",".renovaterc":"renovate","renovate.json":"renovate",".renovaterc.json":"renovate",".retextrc":"retext",".retextignore":"retext",".retextrc.js":"retext",".retextrc.json":"retext",".retextrc.yml":"retext",".retextrc.yaml":"retext","robots.txt":"robots","rollup.config.js":"rollup","rollup.config.mjs":"rollup","rollup.config.coffee":"rollup","rollup.config.ts":"rollup","rollup.config.common.js":"rollup","rollup.config.common.mjs":"rollup","rollup.config.common.coffee":"rollup","rollup.config.common.ts":"rollup","rollup.config.dev.js":"rollup","rollup.config.dev.mjs":"rollup","rollup.config.dev.coffee":"rollup","rollup.config.dev.ts":"rollup","rollup.config.prod.js":"rollup","rollup.config.prod.mjs":"rollup","rollup.config.prod.coffee":"rollup","rollup.config.prod.ts":"rollup",".rspec":"rspec",".rubocop.yml":"rubocop",".rubocop_todo.yml":"rubocop","rust-toolchain":"rust_toolchain",".sentryclirc":"sentry","serverless.yml":"serverless","snapcraft.yaml":"snapcraft",".snyk":"snyk",".solidarity":"solidarity",".solidarity.json":"solidarity",".stylelintrc":"stylelint",".stylelintignore":"stylelintignore",".stylelintcache":"stylelint","stylelint.config.js":"stylelint","stylelint.config.cjs":"stylelint","stylelint.config.mjs":"stylelint","stylelint.config.json":"stylelint","stylelint.config.yaml":"stylelint","stylelint.config.yml":"stylelint","stylelint.config.ts":"stylelint",".stylelintrc.js":"stylelint",".stylelintrc.json":"stylelint",".stylelintrc.yaml":"stylelint",".stylelintrc.yml":"stylelint",".stylelintrc.ts":"stylelint",".stylelintrc.cjs":"stylelint",".stylelintrc.mjs":"stylelint",".stylish-haskell.yaml":"stylish_haskell",".svnignore":"subversion","package.pins":"swift","symfony.lock":"symfony","windi.config.ts":"windi","windi.config.js":"windi","tailwind.js":"tailwind","tailwind.mjs":"tailwind","tailwind.cjs":"tailwind","tailwind.coffee":"tailwind","tailwind.ts":"tailwind","tailwind.cts":"tailwind","tailwind.mts":"tailwind","tailwind.config.mjs":"tailwind","tailwind.config.cjs":"tailwind","tailwind.config.js":"tailwind","tailwind.config.coffee":"tailwind","tailwind.config.ts":"tailwind","tailwind.config.cts":"tailwind","tailwind.config.mts":"tailwind",".testcaferc.json":"testcafe",".tfignore":"tfs","tox.ini":"tox",".travis.yml":"travis","tsconfig.json":"tsconfig","tsconfig.app.json":"tsconfig","tsconfig.base.json":"tsconfig","tsconfig.common.json":"tsconfig","tsconfig.dev.json":"tsconfig","tsconfig.development.json":"tsconfig","tsconfig.e2e.json":"tsconfig","tsconfig.prod.json":"tsconfig","tsconfig.production.json":"tsconfig","tsconfig.server.json":"tsconfig","tsconfig.spec.json":"tsconfig","tsconfig.staging.json":"tsconfig","tsconfig.test.json":"tsconfig","tsconfig.tsd.json":"tsconfig","tsconfig.node.json":"tsconfig","tsconfig.lib.json":"tsconfig","tsconfig.eslint.json":"tsconfig","tsconfig.storybook.json":"tsconfig","tsconfig.tsbuildinfo":"tsconfig","tslint.json":"tslint","tslint.yaml":"tslint","tslint.yml":"tslint",".unibeautifyrc":"unibeautify","unibeautify.config.js":"unibeautify",".unibeautifyrc.js":"unibeautify",".unibeautifyrc.json":"unibeautify",".unibeautifyrc.yaml":"unibeautify",".unibeautifyrc.yml":"unibeautify","vagrantfile":"vagrant",".vimrc":"vim",".gvimrc":"vim",".vscodeignore":"vscode","tasks.json":"vscode","vscodeignore.json":"vscode",".vuerc":"vueconfig","vue.config.js":"vueconfig","vue.config.ts":"vueconfig","wallaby.json":"wallaby","wallaby.js":"wallaby","wallaby.ts":"wallaby","wallaby.coffee":"wallaby","wallaby.conf.json":"wallaby","wallaby.conf.js":"wallaby","wallaby.conf.ts":"wallaby","wallaby.conf.coffee":"wallaby",".wallaby.json":"wallaby",".wallaby.js":"wallaby",".wallaby.ts":"wallaby",".wallaby.coffee":"wallaby",".wallaby.conf.json":"wallaby",".wallaby.conf.js":"wallaby",".wallaby.conf.ts":"wallaby",".wallaby.conf.coffee":"wallaby",".watchmanconfig":"watchmanconfig","wercker.yml":"wercker","wpml-config.xml":"wpml",".yamllint":"yamllint",".yaspellerrc":"yandex",".yaspeller.json":"yandex","yarn.lock":"yarnlock",".yarnrc":"yarn",".yarn.installed":"yarn",".yarnclean":"yarn",".yarn-integrity":"yarn",".yarn-metadata.json":"yarn",".yarnignore":"yarnignore",".yarnrc.yml":"yarn",".yarnrc.yaml":"yarn",".yarnrc.json":"yarn",".yarnrc.json5":"yarn",".yarnrc.cjs":"yarn",".yarnrc.js":"yarn",".yarnrc.lock":"yarn",".yarnrc.txt":"yarn","yarn-error.log":"yarnerror",".yo-rc.json":"yeoman","now.json":"vercel",".nowignore":"vercel","vercel.json":"vercel",".vercel":"vercel",".vercelignore":"vercel","vite.config.js":"vite","vite.config.mjs":"vite","vite.config.cjs":"vite","vite.config.ts":"vite","vite.config.mts":"vite","vite.config.cts":"vite",".nvmrc":"nvm","example.env":"env",".env.staging":"env",".env.sample":"env",".env.preprod":"env",".env.prod":"env",".env.production":"env",".env.local":"env",".env.dev":"env",".env.dev.local":"env",".env.dev.prod":"env",".env.dev.preprod":"env",".env.dev.production":"env",".env.dev.staging":"env",".env.development":"env",".env.example":"env",".env.test":"env",".env.dist":"env",".env.default":"env",".jinja":"jinja","jenkins.yaml":"jenkins","jenkins.yml":"jenkins",".compodocrc":"compodoc",".compodocrc.json":"compodoc",".compodocrc.yaml":"compodoc",".compodocrc.yml":"compodoc","bsconfig.json":"bsconfig",".clang-format":"llvm",".clang-tidy":"llvm",".clangd":"llvm",".parcelrc":"parcel","dune":"dune","dune-project":"duneproject",".adonisrc.json":"adonis","astro.config.js":"astroconfig","astro.config.cjs":"astroconfig","astro.config.mjs":"astroconfig","astro.config.ts":"astroconfig","astro.config.cts":"astroconfig","astro.config.mts":"astroconfig","svelte.config.js":"svelteconfig","svelte.config.ts":"svelteconfig",".tool-versions":"toolversions","CMakeSettings.json":"cmake","CMakeLists.txt":"cmake","toolchain.cmake":"cmake",".cmake":"cmake","Cargo.toml":"cargo","Cargo.lock":"cargolock","pnpm-lock.yaml":"pnpmlock","tauri.conf.json":"tauri","tauri.conf.json5":"tauri","tauri.linux.conf.json":"tauri","tauri.windows.conf.json":"tauri","tauri.macos.conf.json":"tauri","next.config.js":"nextconfig","next.config.mjs":"nextconfig","next.config.ts":"nextconfig","nextron.config.js":"nextron","nextron.config.ts":"nextron","poetry.toml":"poetry","poetry.lock":"poetrylock","pyproject.toml":"pyproject","rustfmt.toml":"rustfmt",".rustfmt.toml":"rustfmt","cucumber.yml":"cucumber","cucumber.yaml":"cucumber","cucumber.js":"cucumber","cucumber.ts":"cucumber","cucumber.cjs":"cucumber","cucumber.mjs":"cucumber","cucumber.json":"cucumber","flake.lock":"flakelock","ace":"ace","ace-manifest.json":"acemanifest","knexfile.js":"knex","knexfile.ts":"knex","launch.json":"launch","redis.conf":"redis","sequelize.js":"sequelize","sequelize.ts":"sequelize","sequelize.cjs":"sequelize",".sequelizerc":"sequelize",".sequelizerc.js":"sequelize",".sequelizerc.json":"sequelize","cypress.json":"cypress","cypress.env.json":"cypress","cypress.config.js":"cypress","cypress.config.ts":"cypress","cypress.config.cjs":"cypress","playwright.config.ts":"playright","playwright.config.js":"playright","playwright.config.cjs":"playright","vitest.config.ts":"vitest","vitest.config.cts":"vitest","vitest.config.mts":"vitest","vitest.config.js":"vitest","vitest.config.cjs":"vitest","vitest.config.mjs":"vitest","vitest.workspace.ts":"vitest","vitest.workspace.cts":"vitest","vitest.workspace.mts":"vitest","vitest.workspace.js":"vitest","vitest.workspace.cjs":"vitest","vitest.workspace.mjs":"vitest","vite-env.d.ts":"viteenv","vite-env.d.js":"viteenv","pubspec.lock":"flutterlock","pubspec.yaml":"flutter",".packages":"flutterpackage",".htaccess":"htaccess","nx.json":"nx","project.json":"nx","nx.instructions.md":"nx","nx.jsonc":"nx","v.mod":"vmod","quasar.config.js":"quasar","quasar.config.ts":"quasar","quasar.config.cjs":"quasar","quasar.config.mjs":"quasar","quarkus.properties":"quarkus","theme.properties":"ui","gradlew":"gradle","gradle-wrapper.properties":"gradle","gradlew.bat":"gradlebat","makefile.win":"makefile","makefile":"makefile","make":"makefile","version":"version","server":"sql","migrate":"sql",".commitlintrc":"commitlint",".commitlintrc.json":"commitlint",".commitlintrc.yaml":"commitlint",".commitlintrc.yml":"commitlint",".commitlintrc.js":"commitlint",".commitlintrc.cjs":"commitlint",".commitlintrc.ts":"commitlint",".commitlintrc.cts":"commitlint","commitlint.config.js":"commitlint","commitlint.config.cjs":"commitlint","commitlint.config.ts":"commitlint","commitlint.config.cts":"commitlint",".terraform-version":"terraformversion","TerraFile":"terrafile","tfstate.backup":"terraform",".code-workspace":"codeworkspace","hardhat.config.js":"hardhat","hardhat.config.ts":"hardhat","hardhat.config.cts":"hardhat","hardhat.config.cjs":"hardhat","hardhat.config.mjs":"hardhat","taze.config.js":"taze","taze.config.ts":"taze","taze.config.cjs":"taze","taze.config.mjs":"taze",".tazerc.json":"taze","turbo.json":"turbo","turbo.jsonc":"turbo","uno.config.ts":"unocss","uno.config.js":"unocss","uno.config.mjs":"unocss","uno.config.mts":"unocss","unocss.config.ts":"unocss","unocss.config.js":"unocss","unocss.config.mjs":"unocss","unocss.config.mts":"unocss","atomizer.config.js":"atomizer","atomizer.config.cjs":"atomizer","atomizer.config.mjs":"atomizer","atomizer.config.ts":"atomizer","esbuild.js":"esbuild","esbuild.mjs":"esbuild","esbuild.cjs":"esbuild","esbuild.ts":"esbuild","mix.exs":"mix","mix.lock":"mixlock",".DS_Store":"dsstore","remix.config.js":"remix","remix.config.cjs":"remix","remix.config.mjs":"remix","remix.config.ts":"remix","xmake.lua":"xmake",".sailsrc":"sails","farm.config.ts":"farm","farm.config.js":"farm","bunfig.toml":"bun",".bunfig.toml":"bun","bun.lockb":"bunlock","bun.lock":"bunlock",".air.toml":"air","rome.json":"rome","biome.json":"biome","bicepconfig.json":"bicepconfig","drizzle.config.ts":"drizzle","drizzle.config.js":"drizzle","drizzle.config.json":"drizzle","panda.config.ts":"panda","panda.config.js":"panda","panda.config.json":"panda","panda.config.cjs":"panda","panda.config.mjs":"panda","panda.config.cts":"panda","panda.config.mts":"panda",".buckconfig":"buck","Ballerina.toml":"ballerinaconfig","knip.json":"knip","knip.jsonc":"knip",".knip.json":"knip",".knip.jsonc":"knip","knip.ts":"knip","knip.js":"knip","knip.config.ts":"knip","knip.config.js":"knip","todo.md":"todo",".todo.md":"todo","todo.txt":"todo",".todo.txt":"todo","todo":"todo","mkdocs.yml":"mkdocs","mkdocs.yaml":"mkdocs","gleam.toml":"gleamconfig",".oxlintrc.json":"oxlint","oxlint.json":"oxlint","oxlint.config.js":"oxlint","oxlint.config.ts":"oxlint","oxlint.config.cjs":"oxlint","oxlint.config.mjs":"oxlint","oxlint.config.cts":"oxlint","oxlint.config.mts":"oxlint",".cursorrules":"cursor","plopfile.js":"plop","plopfile.cjs":"plop","plopfile.mjs":"plop","plopfile.ts":"plop","plopfile.cts":"plop","config.mockoon.json":"mockoon","mockoon.json":"mockoon","mockoon.yaml":"mockoon","mockoon.yml":"mockoon","mockoon.env":"mockoon","mockoon.env.json":"mockoon","mockoon.env.yaml":"mockoon","mockoon.env.yml":"mockoon","mockoon.env.js":"mockoon","mockoon.env.ts":"mockoon","mockoon.env.cjs":"mockoon","mockoon.env.mjs":"mockoon","mockoon.env.cts":"mockoon","mockoon.env.mts":"mockoon","copilot-instructions.md":"copilot",".copilot-instructions":"copilot",".instructions":"instructions","instructions.md":"instructions","instructions.txt":"instructions","instructions":"instructions","instructions.json":"instructions","instructions.yaml":"instructions","instructions.yml":"instructions",".keep":"keep",".keepignore":"keep","CLAUDE.md":"claude","claude.md":"claude","claude.txt":"claude","claude":"claude","claude.json":"claude","claude.yaml":"claude",".claude_code_config":"claude",".claude":"claude","claude.config.js":"claude",".claude.yaml":"claude",".clauderc":"claude","claude-instructions.md":"claude",".claude-code":"claude","claude-code.config":"claude"},"languageIds":{"actionscript":"actionscript","ada":"ada","advpl":"advpl","affectscript":"affectscript","al":"al","ansible":"ansible","antlr":"antlr","anyscript":"anyscript","apacheconf":"apache","apex":"apex","apiblueprint":"apib","apl":"apl","applescript":"applescript","asciidoc":"asciidoc","asp":"asp","asp (html)":"asp","arm":"assembly","asm":"assembly","ats":"ats","ahk":"autohotkey","autoit":"autoit","avro":"avro","azcli":"azure","azure-pipelines":"azurepipelines","ballerina":"ballerina","bat":"bat","bats":"bats","bazel":"bazel","befunge":"befunge","befunge98":"befunge","biml":"biml","blade":"blade","laravel-blade":"blade","bolt":"bolt","bosque":"bosque","c":"c","c-al":"c_al","cabal":"cabal","caddyfile":"caddy","cddl":"cddl","ceylon":"ceylon","cfml":"cf","lang-cfml":"cf","cfc":"cfc","cfmhtml":"cfm","cookbook":"chef_cookbook","clojure":"clojure","clojurescript":"clojurescript","manifest-yaml":"cloudfoundry","cmake":"cmake","cmake-cache":"cmake","cobol":"cobol","coffeescript":"coffeescript","properties":"properties","dotenv":"config","confluence":"confluence","cpp":"cpp","crystal":"crystal","csharp":"csharp","css":"css","feature":"cucumber","cuda":"cuda","cython":"cython","dal":"dal","dart":"dartlang","pascal":"pascal","objectpascal":"pascal","diff":"diff","django-html":"django","django-txt":"django","d":"dlang","dscript":"dlang","dml":"dlang","diet":"dlang","dockerfile":"docker","ignore":"docker","dotjs":"dotjs","doxygen":"doxygen","drools":"drools","dustjs":"dustjs","dylan":"dylan","dylan-lid":"dylan","edge":"edge","eex":"eex","html-eex":"eex","es":"elastic","elixir":"elixir","elm":"elm","erb":"erb","erlang":"erlang","falcon":"falcon","fortran":"fortran","fortran-modern":"fortran","FortranFreeForm":"fortran","fortran_fixed-form":"fortran","ftl":"freemarker","fsharp":"fsharp","fthtml":"fthtml","galen":"galen","gml-gms":"gamemaker","gml-gms2":"gamemaker2","gml-gm81":"gamemaker81","gcode":"gcode","genstat":"genstat","git-commit":"git","git-rebase":"git","glsl":"glsl","glyphs":"glyphs","gnuplot":"gnuplot","go":"go","golang":"go","go-sum":"go","go-mod":"go","go-xml":"go","gdscript":"godot","graphql":"graphql","dot":"graphviz","groovy":"groovy","haml":"haml","handlebars":"handlebars","harbour":"harbour","haskell":"haskell","literate haskell":"haskell","haxe":"haxe","hxml":"haxe","Haxe AST dump":"haxe","helm":"helm","hjson":"hjson","hlsl":"opengl","home-assistant":"homeassistant","hosts":"host","html":"html","http":"http","hunspell.aff":"hunspell","hunspell.dic":"hunspell","hy":"hy","icl":"icl","imba":"imba","4GL":"informix","ini":"conf","ink":"ink","innosetup":"innosetup","io":"io","iodine":"iodine","janet":"janet","java":"java","raku":"raku","jekyll":"jekyll","jenkins":"jenkins","declarative":"jenkins","jenkinsfile":"jenkins","jinja":"jinja","code-referencing":"vscode","search-result":"vscode","type":"vscode","javascript":"js","json":"json","jsonl":"json","json-tmlanguage":"json","jsonc":"json","json5":"json5","jsonnet":"jsonnet","julia":"julia","juliamarkdown":"julia","kivy":"kivy","kos":"kos","kotlin":"kotlin","kusto":"kusto","latino":"latino","less":"less","lex":"lex","lisp":"lisp","lolcode":"lolcode","code-text-binary":"binary","lsl":"lsl","lua":"lua","makefile":"makefile","markdown":"markdown","marko":"marko","matlab":"matlab","maxscript":"maxscript","mel":"maya","mediawiki":"mediawiki","meson":"meson","mjml":"mjml","mlang":"mlang","powerquerymlanguage":"mlang","mojolicious":"mojolicious","mongo":"mongo","mson":"mson","nearley":"nearly","nim":"nim","nimble":"nimble","nix":"nix","nsis":"nsi","nfl":"nsi","nsl":"nsi","bridlensis":"nsi","nunjucks":"nunjucks","objective-c":"c","objective-cpp":"cpp","ocaml":"ocaml","ocamllex":"ocaml","menhir":"ocaml","openhab":"openHAB","pddl":"pddl","happenings":"pddl_happenings","plan":"pddl_plan","phoenix-heex":"eex","perl":"perl","perl6":"perl6","pgsql":"pgsql","php":"php","pine":"pine","pinescript":"pine","pip-requirements":"python","platformio-debug.disassembly":"platformio","platformio-debug.memoryview":"platformio","platformio-debug.asm":"platformio","plsql":"plsql","oracle":"plsql","polymer":"polymer","pony":"pony","postcss":"postcss","powershell":"powershell","prisma":"prisma","pde":"processinglang","abl":"progress","prolog":"prolog","prometheus":"prometheus","proto3":"protobuf","proto":"protobuf","jade":"pug","pug":"pug","puppet":"puppet","purescript":"purescript","pyret":"pyret","python":"python","qlik":"qlikview","qml":"qml","qsharp":"qsharp","r":"r","racket":"racket","raml":"raml","razor":"razor","aspnetcorerazor":"razor","javascriptreact":"reactjs","typescriptreact":"reactts","reason":"reason","red":"red","restructuredtext":"restructuredtext","rexx":"rexx","riot":"riot","rmd":"rmd","mdx":"markdownx","robot":"robotframework","ruby":"ruby","rust":"rust","san":"san","SAS":"sas","sbt":"sbt","scala":"scala","scilab":"scilab","vbscript":"script","scss":"scss","sdl":"sdlang","shaderlab":"shaderlab","shellscript":"shell","silverstripe":"silverstripe","eskip":"skipper","slang":"slang","slice":"slice","slim":"slim","smarty":"smarty","snort":"snort","solidity":"solidity","snippets":"vscode","sqf":"sqf","sql":"sql","squirrel":"squirrel","stan":"stan","stata":"stata","stencil":"stencil","stencil-html":"stencil","stylable":"stylable","source.css.styled":"styled","stylus":"stylus","svelte":"svelte","Swagger":"swagger","swagger":"swagger","swift":"swift","swig":"swig","cuda-cpp":"nvidia","systemd-unit-file":"systemd","systemverilog":"systemverilog","t4":"t4tt","tera":"tera","terraform":"terraform","tex":"latex","log":"log","dockercompose":"docker","latex":"latex","vue-directives":"vue","vue-injection-markdown":"vue","vue-interpolations":"vue","vue-sfc-style-variable-injection":"vue","bibtex":"latex","doctex":"tex","plaintext":"text","textile":"textile","toml":"toml","tt":"tt","ttcn":"ttcn","twig":"twig","typescript":"typescript","typoscript":"typo3","vb":"vb","vba":"vba","velocity":"velocity","verilog":"verilog","vhdl":"vhdl","viml":"vim","v":"vlang","volt":"volt","vue":"vue","wasm":"wasm","wat":"wasm","wenyan":"wenyan","wolfram":"wolfram","wurstlang":"wurst","wurst":"wurst","xmake":"xmake","xml":"xml","xquery":"xquery","xsl":"xml","yacc":"yacc","yaml":"yaml","yaml-tmlanguage":"yaml","yang":"yang","zig":"zig","vitest-snapshot":"vitest","instructions":"instructions","prompt":"prompt"},"light":{"file":"_file_light","folder":"_folder_light","folderExpanded":"_folder_light_open","rootFolder":"_root_folder_light","rootFolderExpanded":"_root_folder_light_open","fileExtensions":{"wma":"audio","wav":"audiowav","vox":"audio","tta":"audio","raw":"audio","ra":"audio","opus":"audio","ogg":"audioogg","oga":"audio","msv":"audio","mpc":"audio","mp3":"audiomp3","mogg":"audio","mmf":"audio","m4p":"audio","m4b":"audio","m4a":"audio","ivs":"audio","iklax":"audio","gsm":"audio","flac":"audio","dvf":"audio","dss":"audio","dct":"audio","au":"audio","ape":"audio","amr":"audio","aiff":"audio","act":"audio","aac":"audio","wmv":"video","webm":"video","vob":"video","svi":"video","rmvb":"video","rm":"video","ogv":"video","nsv":"video","mpv":"video","mpg":"video","mpeg2":"video","mpeg":"video","mpe":"video","mp4":"mp4","mp2":"video","mov":"mov","mk3d":"video","mkv":"video","m4v":"video","m2v":"video","flv":"video","f4v":"video","f4p":"video","f4b":"video","f4a":"video","qt":"video","divx":"video","avi":"video","amv":"video","asf":"video","3gp":"video","3g2":"video","ico":"imageico","tiff":"image","bmp":"image","png":"imagepng","gif":"imagegif","jpg":"imagejpg","jpeg":"imagejpg","7z":"zip","7zip":"zip","blade.php":"blade","cfg.dist":"conf","cjs.map":"jsmap","controller.js":"nestjscontroller","controller.ts":"nestjscontroller","repository.js":"nestjsrepository","repository.ts":"nestjsrepository","scheduler.js":"nestscheduler","scheduler.ts":"nestscheduler","css.js":"vanillaextract","css.ts":"vanillaextract","css.map":"cssmap","d.ts":"typescriptdef","decorator.js":"nestjsdecorator","decorator.ts":"nestjsdecorator","drawio.png":"drawio","drawio.svg":"drawio","e2e-spec.ts":"testts","e2e-spec.tsx":"testts","e2e-test.ts":"testts","e2e-test.tsx":"testts","filter.js":"nestjsfilter","filter.ts":"nestjsfilter","format.ps1xml":"powershell_format","gemfile.lock":"bundler","gradle.kts":"gradlekotlin","guard.js":"nestjsguard","guard.ts":"nestjsguard","jar.old":"jar","js.flow":"flow","js.map":"jsmap","js.snap":"jest_snapshot","json-ld":"jsonld","jsx.snap":"jest_snapshot","layout.htm":"layout","layout.html":"layout","marko.js":"markojs","mjs.map":"jsmap","module.ts":"nestjsmodule","resolver.js":"nestjsresolver","resolver.ts":"nestjsresolver","service.js":"nestjsservice","service.ts":"nestjsservice","entity.js":"nestjsentity","entity.ts":"nestjsentity","interceptor.js":"nestjsinterceptor","interceptor.ts":"nestjsinterceptor","dto.js":"nestjsdto","dto.ts":"nestjsdto","spec.js":"testjs","spec.jsx":"testjs","spec.mjs":"testjs","spec.ts":"testts","spec.tsx":"testts","stories.js":"storybook","stories.jsx":"storybook","stories.ts":"storybook","stories.tsx":"storybook","stories.svelte":"storybook","story.js":"storybook","story.jsx":"storybook","story.ts":"storybook","story.tsx":"storybook","story.svelte":"storybook","test.cjs":"testjs","test.cts":"testts","test.js":"testjs","test.jsx":"testjs","test.mjs":"testjs","test.mts":"testts","test.ts":"testts","test.tsx":"testts","ts.snap":"jest_snapshot","tsx.snap":"jest_snapshot","types.ps1xml":"powershell_types","a":"binary","accda":"access","accdb":"access","accdc":"access","accde":"access","accdp":"access","accdr":"access","accdt":"access","accdu":"access","ade":"access","adoc":"adoc","adp":"access","afdesign":"afdesign","affinitydesigner":"afdesign","affinityphoto":"afphoto","affinitypublisher":"afpub","afphoto":"afphoto","afpub":"afpub","ai":"ai","app":"binary","ascx":"aspx","asm":"binary","aspx":"aspx","astro":"astro","awk":"awk","bat":"bat","bc":"llvm","bcmx":"outlook","bicep":"bicep","bin":"binary","blade":"blade","bz2":"zip","bzip2":"zip","c":"c","cake":"cake","cer":"cert","pvk":"pvk","pfx":"pfx","spc":"spc","cfg":"conf","civet":"civet","cjm":"clojure","cl":"opencl","class":"class","cli":"cli","clj":"clojure","cljc":"clojure","cljs":"clojure","cljx":"clojure","cma":"binary","cmd":"cli","cmi":"binary","cmo":"binary","cmx":"binary","cmxa":"binary","comp":"opengl","conf":"conf","cpp":"cpp","cr":"crystal","crec":"lync","crl":"cert","crt":"cert","cs":"csharp","cshtml":"cshtml","csproj":"csproj","csr":"cert","css":"css","csv":"csv","csx":"csharp","d":"d","dart":"dartlang","db":"sqlite","db3":"sqlite","der":"cert","diff":"diff","dio":"drawio","djt":"django","dll":"binary","dmp":"log","doc":"word","docm":"word","docx":"word","dot":"word","dotm":"word","dotx":"word","drawio":"drawio","dta":"stata","eco":"docpad","edge":"edge","edn":"clojure","eex":"eex","ejs":"ejs","el":"emacs","elc":"emacs","elm":"elm","enc":"license","ensime":"ensime","env":"env","eps":"eps","erb":"erb","erl":"erlang","eskip":"skipper","ex":"elixir","exe":"binary","exp":"tcl","exs":"exs","fbx":"fbx","feature":"cucumber","fig":"figma","fish":"shell","fla":"fla","fods":"excel","frag":"opengl","fs":"fsharp","fsproj":"fsproj","ftl":"freemarker","gbl":"gbl","gd":"godot","gemfile":"bundler","geom":"opengl","glsl":"opengl","gmx":"gamemaker","go":"go","godot":"godot","gql":"graphql","gradle":"gradle","groovy":"groovy","gz":"zip","h":"cheader","haml":"haml","hbs":"handlebars","hcl":"hashicorp","hl":"binary","hlsl":"opengl","hpp":"hpp","hs":"haskell","html":"html","hxp":"lime","hxproj":"haxedevelop","ibc":"idrisbin","idr":"idris","ilk":"binary","imba":"imba","inc":"inc","include":"inc","info":"info","infopathxml":"infopath","ini":"conf","ino":"arduino","ipkg":"idrispkg","ipynb":"ipynb","iuml":"plantuml","jar":"jar","java":"java","jbuilder":"jbuilder","j2":"jinja","jinja":"jinja","jinja2":"jinja","jl":"julia","json5":"json5","jsonld":"jsonld","jsp":"jsp","jss":"jss","key":"key","kit":"codekit","kt":"kotlin","kts":"kotlins","laccdb":"access","ldb":"access","less":"less","lib":"binary","lidr":"idris","liquid":"liquid","ll":"llvm","lnk":"lnk","log":"log","ls":"livescript","lucee":"cf","m":"m","makefile":"makefile","mam":"access","map":"map","maq":"access","markdown":"markdown","master":"layout","mdb":"access","mdown":"markdown","mdw":"access","mdx":"markdownx","mesh":"mesh","mex":"matlab","mexn":"matlab","mexrs6":"matlab","mf":"manifest","mint":"mint","mjml":"mjml","ml":"ocaml","mli":"ocamli","mll":"ocamll","mly":"ocamly","mn":"matlab","mo":"motoko","msg":"outlook","mst":"mustache","mum":"matlab","mustache":"mustache","mx":"matlab","mx3":"matlab","n":"binary","ndll":"binary","neon":"neon","nim":"nim","nix":"nix","njk":"njk","njs":"nunjucks","njsproj":"njsproj","nunj":"nunjucks","nupkg":"nuget","nuspec":"nuget","nvim":"nvim","o":"binary","ocrec":"lync","ods":"excel","oft":"outlook","one":"onenote","onepkg":"onenote","onetoc":"onenote","onetoc2":"onenote","opencl":"opencl","org":"org","otf":"fontotf","otm":"outlook","ovpn":"ovpn","P":"prolog","p12":"cert","p7b":"cert","p7r":"cert","pa":"powerpoint","patch":"diff","pcd":"pcl","pck":"plsql_package","pdb":"binary","pde":"arduino","pdf":"pdf","pem":"key","pex":"xml","phar":"php","php1":"php","php2":"php","php3":"php","php4":"php","php5":"php","php6":"php","phps":"php","phpsa":"php","phpt":"php","phtml":"php","pkb":"plsql_package_body","pkg":"package","pkh":"plsql_package_header","pks":"plsql_package_spec","pl":"perl","plantuml":"plantuml","plist":"config","pm":"perlm","po":"poedit","postcss":"postcssconfig","pcss":"postcssconfig","pot":"powerpoint","potm":"powerpoint","potx":"powerpoint","ppa":"powerpoint","ppam":"powerpoint","pps":"powerpoint","ppsm":"powerpoint","ppsx":"powerpoint","ppt":"powerpoint","pptm":"powerpoint","pptx":"powerpoint","pri":"qt","prisma":"prisma","pro":"prolog","properties":"properties","ps1":"powershell","psd":"photoshop","psd1":"powershelldata","psm1":"powershellmodule","psmdcp":"nuget","pst":"outlook","pu":"plantuml","pub":"publisher","puml":"plantuml","puz":"publisher","pyc":"binary","pyd":"binary","pyo":"binary","q":"q","qbs":"qbs","qvd":"qlikview","qvw":"qlikview","rake":"rake","rar":"zip","gzip":"zip","razor":"razor","rb":"ruby","reg":"registry","rego":"rego","res":"rescript","resi":"rescriptinterface","rjson":"rjson","rproj":"rproj","rs":"rust","rsx":"rust","ron":"ron","odin":"odin","rt":"reacttemplate","rwd":"matlab","pas":"pascal","pp":"pascal","p":"pascal","lpr":"lazarusproject","lps":"lazarusproject","lpi":"lazarusproject","lfm":"lazarusproject","lrs":"lazarusproject","lpk":"lazarusproject","dpr":"delphiproject","dproj":"delphiproject","dfm":"delphiproject","sass":"scss","sc":"scala","scala":"scala","scpt":"binary","scptd":"binary","scss":"scss","sentinel":"sentinel","sig":"onenote","sketch":"sketch","slddc":"matlab","sldm":"powerpoint","sldx":"powerpoint","sln":"sln","sls":"saltstack","slx":"matlab","smv":"matlab","so":"binary","sol":"sol","sql":"sql","sqlite":"sqlite","sqlite3":"sqlite","src":"cert","sss":"sss","sst":"cert","stl":"cert","storyboard":"storyboard","styl":"stylus","suo":"suo","svelte":"svelte","svg":"svg","swc":"flash","swf":"flash","swift":"swift","tar":"zip","tcl":"tcl","templ":"tmpl","tesc":"opengl","tese":"opengl","tex":"latex","texi":"tex","tf":"terraform","tfstate":"terraform","tfvars":"terraformvars","tgz":"zip","tikz":"tex","tlg":"log","tmlanguage":"xml","tmpl":"tmpl","todo":"todo","toml":"toml","tpl":"smarty","tres":"tres","tscn":"tscn","tst":"test","tsx":"reactts","jsx":"reactjs","tt2":"tt","ttf":"fontttf","twig":"twig","txt":"txt","ui":"ui","unity":"shaderlab","user":"user","v":"v","vala":"vala","vapi":"vapi","vash":"vash","vbhtml":"vbhtml","vbproj":"vbproj","vcxproj":"vcxproj","vert":"opengl","vhd":"vhd","vhdl":"vhdl","vsix":"vscode","vsixmanifest":"manifest","wasm":"wasm","webp":"imagewebp","wgsl":"wgsl","wll":"word","woff":"fontwoff","eot":"fonteot","woff2":"fontwoff2","wv":"audiowv","wxml":"wxml","wxss":"wxss","xcodeproj":"xcode","xfl":"xfl","xib":"xib","xlf":"xliff","xliff":"xliff","xls":"excel","xlsm":"excel","xlsx":"excel","xsf":"infopath","xsn":"infopath","xtp2":"infopath","xvc":"matlab","xz":"zip","yy":"gamemaker2","yyp":"gamemaker2","zig":"zig","zip":"zip","zipx":"zip","zz":"zip","deflate":"zip","brotli":"brotli","kra":"krita","mgcb":"mgcb","anim":"anim","cy.ts":"cypressts","cy.js":"cypressjs","hx":"haxe","hxml":"haxeml","gr":"grain","slim":"slim","obj":"obj","mtl":"mtl","bicepparam":"bicepparam","proto":"proto","wren":"wren","docker-compose.yml":"docker","excalidraw":"excalidraw","excalidraw.json":"excalidraw","excalidraw.svg":"excalidraw","excalidraw.png":"excalidraw","bazel":"bazel","bzl":"bazel","bazelignore":"bazelignore","bazelrc":"bazel","http":"http","rkt":"racket","rktl":"racket","bru":"bruno","nelua":"nelua","mermaid":"mermaid","mmd":"mermaid","bal":"ballerina","hash":"hash","gleam":"gleam","lock":"lock","yang":"yang","yin":"yin","mdc":"cursor","uml":"plantuml","Identifier":"identifier","cls":"salesforce",".instructions.md":"instructions",".instructions.txt":"instructions",".instructions.json":"instructions",".instructions.yaml":"instructions",".instructions.yml":"instructions","silq":"silq","eraserdiagram":"eraser"},"fileNames":{"webpack.config.images.js":"webpack","webpack.test.conf.ts":"webpack","webpack.test.conf.coffee":"webpack","webpack.test.conf.js":"webpack","webpack.rules.ts":"webpack","webpack.rules.coffee":"webpack","webpack.rules.js":"webpack","webpack.renderer.config.ts":"webpack","webpack.renderer.config.coffee":"webpack","webpack.renderer.config.js":"webpack","webpack.plugins.ts":"webpack","webpack.plugins.coffee":"webpack","webpack.plugins.js":"webpack","webpack.mix.ts":"webpack","webpack.mix.coffee":"webpack","webpack.mix.js":"webpack","webpack.main.config.ts":"webpack","webpack.main.config.coffee":"webpack","webpack.main.config.js":"webpack","webpack.prod.conf.ts":"webpack","webpack.prod.conf.coffee":"webpack","webpack.prod.conf.js":"webpack","webpack.prod.ts":"webpack","webpack.prod.coffee":"webpack","webpack.prod.js":"webpack","webpack.dev.conf.ts":"webpack","webpack.dev.conf.coffee":"webpack","webpack.dev.conf.js":"webpack","webpack.dev.ts":"webpack","webpack.dev.coffee":"webpack","webpack.dev.js":"webpack","webpack.config.production.babel.ts":"webpack","webpack.config.production.babel.coffee":"webpack","webpack.config.production.babel.js":"webpack","webpack.config.prod.babel.ts":"webpack","webpack.config.prod.babel.coffee":"webpack","webpack.config.prod.babel.js":"webpack","webpack.config.test.babel.ts":"webpack","webpack.config.test.babel.coffee":"webpack","webpack.config.test.babel.js":"webpack","webpack.config.staging.babel.ts":"webpack","webpack.config.staging.babel.coffee":"webpack","webpack.config.staging.babel.js":"webpack","webpack.config.development.babel.ts":"webpack","webpack.config.development.babel.coffee":"webpack","webpack.config.development.babel.js":"webpack","webpack.config.dev.babel.ts":"webpack","webpack.config.dev.babel.coffee":"webpack","webpack.config.dev.babel.js":"webpack","webpack.config.common.babel.ts":"webpack","webpack.config.common.babel.coffee":"webpack","webpack.config.common.babel.js":"webpack","webpack.config.base.babel.ts":"webpack","webpack.config.base.babel.coffee":"webpack","webpack.config.base.babel.js":"webpack","webpack.config.babel.ts":"webpack","webpack.config.babel.coffee":"webpack","webpack.config.babel.js":"webpack","webpack.config.production.ts":"webpack","webpack.config.production.coffee":"webpack","webpack.config.production.js":"webpack","webpack.config.prod.ts":"webpack","webpack.config.prod.coffee":"webpack","webpack.config.prod.js":"webpack","webpack.config.test.ts":"webpack","webpack.config.test.coffee":"webpack","webpack.config.test.js":"webpack","webpack.config.staging.ts":"webpack","webpack.config.staging.coffee":"webpack","webpack.config.staging.js":"webpack","webpack.config.development.ts":"webpack","webpack.config.development.coffee":"webpack","webpack.config.development.js":"webpack","webpack.config.dev.ts":"webpack","webpack.config.dev.coffee":"webpack","webpack.config.dev.js":"webpack","webpack.config.common.ts":"webpack","webpack.config.common.coffee":"webpack","webpack.config.common.js":"webpack","webpack.config.base.ts":"webpack","webpack.config.base.coffee":"webpack","webpack.config.base.js":"webpack","webpack.config.ts":"webpack","webpack.config.coffee":"webpack","webpack.config.js":"webpack","webpack.common.ts":"webpack","webpack.common.coffee":"webpack","webpack.common.js":"webpack","webpack.base.conf.ts":"webpack","webpack.base.conf.coffee":"webpack","webpack.base.conf.js":"webpack",".angular-cli.json":"angular","angular-cli.json":"angular","angular.json":"angular",".angular.json":"angular","api-extractor.json":"api_extractor","api-extractor-base.json":"api_extractor","appveyor.yml":"appveyor",".appveyor.yml":"appveyor","aurelia.json":"aurelia","azure-pipelines.yml":"azure",".vsts-ci.yml":"azure",".babelrc":"babel",".babelignore":"babel",".babelrc.js":"babel",".babelrc.cjs":"babel",".babelrc.mjs":"babel",".babelrc.json":"babel","babel.config.js":"babel","babel.config.cjs":"babel","babel.config.mjs":"babel","babel.config.json":"babel","vetur.config.js":"vue","vetur.config.ts":"vue",".bzrignore":"bazaar",".bazelrc":"bazel","bazel.rc":"bazel","bazel.bazelrc":"bazel","BUILD":"bazel","bitbucket-pipelines.yml":"bitbucketpipeline",".bithoundrc":"bithound",".bowerrc":"bower","bower.json":"bower",".browserslistrc":"browserslist","browserslist":"browserslist","gemfile":"bundler","gemfile.lock":"bundler",".ruby-version":"bundler","capacitor.config.json":"capacitor","cargo.toml":"cargo","cargo.lock":"cargo","chefignore":"chef","berksfile":"chef","berksfile.lock":"chef","policyfile":"chef","circle.yml":"circleci",".cfignore":"cloudfoundry",".codacy.yml":"codacy",".codacy.yaml":"codacy",".codeclimate.yml":"codeclimate","codecov.yml":"codecov",".codecov.yml":"codecov","config.codekit":"codekit","config.codekit2":"codekit","config.codekit3":"codekit",".config.codekit":"codekit",".config.codekit2":"codekit",".config.codekit3":"codekit","coffeelint.json":"coffeelint",".coffeelintignore":"coffeelint","composer.json":"composer","composer.lock":"composerlock","conanfile.txt":"conan","conanfile.py":"conan",".condarc":"conda",".coveralls.yml":"coveralls","crowdin.yml":"crowdin",".csscomb.json":"csscomb",".csslintrc":"csslint",".cvsignore":"cvs",".boringignore":"darcs","dependabot.yml":"dependabot","dependencies.yml":"dependencies","devcontainer.json":"devcontainer","docker-compose-prod.yml":"docker","docker-compose.alpha.yaml":"docker","docker-compose.alpha.yml":"docker","docker-compose.beta.yaml":"docker","docker-compose.beta.yml":"docker","docker-compose.ci-build.yml":"docker","docker-compose.ci.yaml":"docker","docker-compose.ci.yml":"docker","docker-compose.dev.yaml":"docker","docker-compose.dev.yml":"docker","docker-compose.development.yaml":"docker","docker-compose.development.yml":"docker","docker-compose.local.yaml":"docker","docker-compose.local.yml":"docker","docker-compose.override.yaml":"docker","docker-compose.override.yml":"docker","docker-compose.prod.yaml":"docker","docker-compose.prod.yml":"docker","docker-compose.production.yaml":"docker","docker-compose.production.yml":"docker","docker-compose.stage.yaml":"docker","docker-compose.stage.yml":"docker","docker-compose.staging.yaml":"docker","docker-compose.staging.yml":"docker","docker-compose.test.yaml":"docker","docker-compose.test.yml":"docker","docker-compose.testing.yaml":"docker","docker-compose.testing.yml":"docker","docker-compose.vs.debug.yml":"docker","docker-compose.vs.release.yml":"docker","docker-compose.web.yaml":"docker","docker-compose.web.yml":"docker","docker-compose.worker.yaml":"docker","docker-compose.worker.yml":"docker","docker-compose.yaml":"docker","docker-compose.yml":"docker","Dockerfile-production":"docker","dockerfile.alpha":"docker","dockerfile.beta":"docker","dockerfile.ci":"docker","dockerfile.dev":"docker","dockerfile.development":"docker","dockerfile.local":"docker","dockerfile.prod":"docker","dockerfile.production":"docker","dockerfile.stage":"docker","dockerfile.staging":"docker","dockerfile.test":"docker","dockerfile.testing":"docker","dockerfile.web":"docker","dockerfile.worker":"docker","dockerfile":"docker","docker-compose.debug.yml":"dockerdebug","docker-cloud.yml":"docker",".dockerignore":"dockerignore",".doczrc":"docz","docz.js":"docz","docz.json":"docz",".docz.js":"docz",".docz.json":"docz","doczrc.js":"docz","doczrc.json":"docz","docz.config.js":"docz","docz.config.json":"docz",".dojorc":"dojo",".drone.yml":"drone",".drone.yml.sig":"drone",".dvc":"dvc",".editorconfig":"editorconfig","elm-package.json":"elm",".ember-cli":"ember","emakefile":"erlang",".emakerfile":"erlang",".eslintrc":"eslint",".eslintignore":"eslintignore",".eslintcache":"eslint",".eslintrc.js":"eslint",".eslintrc.mjs":"eslint",".eslintrc.cjs":"eslint",".eslintrc.json":"eslint",".eslintrc.yaml":"eslint",".eslintrc.yml":"eslint",".eslintrc.browser.json":"eslint",".eslintrc.base.json":"eslint","eslint-preset.js":"eslint","eslint.config.js":"eslint","eslint.config.cjs":"eslint","eslint.config.mjs":"eslint","eslint.config.ts":"eslint","_eslintrc.cjs":"eslint","app.json":"expo","app.config.js":"expo","app.config.json":"expo","app.config.json5":"expo","favicon.ico":"favicon",".firebaserc":"firebase","firebase.json":"firebasehosting","firestore.rules":"firestore","firestore.indexes.json":"firestore",".flooignore":"floobits",".flowconfig":"flow",".flutter-plugins":"flutter",".metadata":"flutter",".fossaignore":"fossa","ignore-glob":"fossil","fuse.js":"fusebox","gatsby-config.js":"gatsby","gatsby-config.ts":"gatsby","gatsby-node.js":"gatsby","gatsby-node.ts":"gatsby","gatsby-browser.js":"gatsby","gatsby-browser.ts":"gatsby","gatsby-ssr.js":"gatsby","gatsby-ssr.ts":"gatsby",".git-blame-ignore-revs":"git",".gitattributes":"git",".gitconfig":"git",".gitignore":"git",".gitmodules":"git",".gitkeep":"git",".mailmap":"git",".gitlab-ci.yml":"gitlab","glide.yml":"glide","go.sum":"go_package","go.mod":"go_package","go.work":"go_package",".gqlconfig":"graphql",".graphqlconfig":"graphql_config",".graphqlconfig.yml":"graphql_config",".graphqlconfig.yaml":"graphql_config","greenkeeper.json":"greenkeeper","gridsome.config.js":"gridsome","gridsome.config.ts":"gridsome","gridsome.server.js":"gridsome","gridsome.server.ts":"gridsome","gridsome.client.js":"gridsome","gridsome.client.ts":"gridsome","gruntfile.js":"grunt","gruntfile.cjs":"grunt","gruntfile.mjs":"grunt","gruntfile.coffee":"grunt","gruntfile.ts":"grunt","gruntfile.cts":"grunt","gruntfile.mts":"grunt","gruntfile.babel.js":"grunt","gruntfile.babel.coffee":"grunt","gruntfile.babel.ts":"grunt","gulpfile.js":"gulp","gulpfile.coffee":"gulp","gulpfile.ts":"gulp","gulpfile.esm.js":"gulp","gulpfile.esm.coffee":"gulp","gulpfile.esm.ts":"gulp","gulpfile.babel.js":"gulp","gulpfile.babel.coffee":"gulp","gulpfile.babel.ts":"gulp","haxelib.json":"haxe","checkstyle.json":"haxecheckstyle",".p4ignore":"helix",".htmlhintrc":"htmlhint",".huskyrc":"husky","husky.config.js":"husky",".huskyrc.js":"husky",".huskyrc.json":"husky",".huskyrc.yaml":"husky",".huskyrc.yml":"husky","ionic.project":"ionic","ionic.config.json":"ionic","jakefile":"jake","jakefile.js":"jake","jest.config.json":"jest","jest.json":"jest",".jestrc":"jest",".jestrc.js":"jest",".jestrc.json":"jest","jest.config.js":"jest","jest.config.cjs":"jest","jest.config.mjs":"jest","jest.config.babel.js":"jest","jest.config.babel.cjs":"jest","jest.config.babel.mjs":"jest","jest.preset.js":"jest","jest.preset.ts":"jest","jest.preset.cjs":"jest","jest.preset.mjs":"jest",".jpmignore":"jpm",".jsbeautifyrc":"jsbeautify","jsbeautifyrc":"jsbeautify",".jsbeautify":"jsbeautify","jsbeautify":"jsbeautify","jsconfig.json":"jsconfig",".jscpd.json":"jscpd","jscpd-report.xml":"jscpd","jscpd-report.json":"jscpd","jscpd-report.html":"jscpd",".jshintrc":"jshint",".jshintignore":"jshint","karma.conf.js":"karma","karma.conf.coffee":"karma","karma.conf.ts":"karma",".kitchen.yml":"kitchenci","kitchen.yml":"kitchenci",".kiteignore":"kite","layout.html":"layout","layout.htm":"layout","lerna.json":"lerna","license":"license","licence":"license","license.md":"license","license.txt":"license","licence.md":"license","licence.txt":"license",".lighthouserc.js":"lighthouse",".lighthouserc.json":"lighthouse",".lighthouserc.yaml":"lighthouse",".lighthouserc.yml":"lighthouse","include.xml":"lime",".lintstagedrc":"lintstagedrc","lint-staged.config.js":"lintstagedrc",".lintstagedrc.js":"lintstagedrc",".lintstagedrc.json":"lintstagedrc",".lintstagedrc.yaml":"lintstagedrc",".lintstagedrc.yml":"lintstagedrc","manifest":"manifest","manifest.bak":"manifest","manifest.json":"manifest","manifest.skip":"manifes",".markdownlint.json":"markdownlint","maven.config":"maven","pom.xml":"maven","extensions.xml":"maven","settings.xml":"maven","pom.properties":"maven",".hgignore":"mercurial","mocha.opts":"mocha",".mocharc.js":"mocha",".mocharc.json":"mocha",".mocharc.jsonc":"mocha",".mocharc.yaml":"mocha",".mocharc.yml":"mocha","modernizr":"modernizr","modernizr.js":"modernizr","modernizrrc.js":"modernizr",".modernizr.js":"modernizr",".modernizrrc.js":"modernizr","moleculer.config.js":"moleculer","moleculer.config.json":"moleculer","moleculer.config.ts":"moleculer",".mtn-ignore":"monotone",".nest-cli.json":"nestjs","nest-cli.json":"nestjs","nestconfig.json":"nestjs",".nestconfig.json":"nestjs","netlify.toml":"netlify","_redirects":"netlify","ng-tailwind.js":"ng_tailwind","nginx.conf":"nginx","build.ninja":"ninja",".node-version":"node",".node_repl_history":"node",".node-gyp":"node","node_modules":"node","node_modules.json":"node","node-inspect.json":"node","node-inspect.js":"node","node-inspect.mjs":"node","node-inspect.cjs":"node","node-inspect.ts":"node","node-inspect.config.js":"node","node-inspect.config.ts":"node","node-inspect.config.cjs":"node","node-inspect.config.mjs":"node","node-inspect.config.json":"node","node-inspect.config.yaml":"node","node-inspect.config.yml":"node","node-inspectrc":"node",".node-inspectrc":"node",".node-inspectrc.json":"node",".node-inspectrc.yaml":"node",".node-inspectrc.yml":"node",".node-inspectrc.js":"node",".node-inspectrc.ts":"node",".node-inspectrc.cjs":"node",".node-inspectrc.mjs":"node","nodemon.json":"nodemon",".npmignore":"npm",".npmrc":"npm","package.json":"npm","package-lock.json":"npmlock","npm-shrinkwrap.json":"npm",".nsrirc":"nsri",".nsriignore":"nsri","nsri.config.js":"nsri",".nsrirc.js":"nsri",".nsrirc.json":"nsri",".nsrirc.yaml":"nsri",".nsrirc.yml":"nsri",".integrity.json":"nsri-integrity","nuxt.config.js":"nuxt","nuxt.config.ts":"nuxt",".nycrc":"nyc",".nycrc.json":"nyc",".merlin":"ocaml","paket.dependencies":"paket","paket.lock":"paket","paket.references":"paket","paket.template":"paket","paket.local":"paket",".php_cs":"phpcsfixer",".php_cs.dist":"phpcsfixer","phpunit":"phpunit","phpunit.xml":"phpunit","phpunit.xml.dist":"phpunit",".phraseapp.yml":"phraseapp","pipfile":"pip","pipfile.lock":"pip","platformio.ini":"platformio","pnpmfile.js":"pnpm","pnpm-workspace.yaml":"pnpm",".postcssrc":"postcssconfig",".postcssrc.json":"postcssconfig",".postcssrc.yml":"postcssconfig",".postcssrc.js":"postcssconfig",".postcssrc.cjs":"postcssconfig",".postcssrc.mjs":"postcssconfig",".postcssrc.ts":"postcssconfig",".postcssrc.cts":"postcssconfig",".postcssrc.mts":"postcssconfig","postcss.config.js":"postcssconfig","postcss.config.cjs":"postcssconfig","postcss.config.mjs":"postcssconfig","postcss.config.ts":"postcssconfig","postcss.config.cts":"postcssconfig","postcss.config.mts":"postcssconfig",".pre-commit-config.yaml":"precommit",".pre-commit-hooks.yaml":"precommit",".prettierrc":"prettier",".prettierignore":"prettierignore","prettier.config.js":"prettier","prettier.config.cjs":"prettier","prettier.config.mjs":"prettier","prettier.config.ts":"prettier","prettier.config.coffee":"prettier",".prettierrc.js":"prettier",".prettierrc.json":"prettier",".prettierrc.yml":"prettier",".prettierrc.yaml":"prettier","procfile":"procfile","protractor.conf.js":"protractor","protractor.conf.coffee":"protractor","protractor.conf.ts":"protractor",".jade-lintrc":"pug",".pug-lintrc":"pug",".jade-lint.json":"pug",".pug-lintrc.js":"pug",".pug-lintrc.json":"pug",".pyup":"pyup",".pyup.yml":"pyup","qmldir":"qmldir","quasar.conf.js":"quasar","rakefile":"rake","razzle.config.js":"razzle","readme.md":"readme","readme.txt":"readme",".rehyperc":"rehype",".rehypeignore":"rehype",".rehyperc.js":"rehype",".rehyperc.json":"rehype",".rehyperc.yml":"rehype",".rehyperc.yaml":"rehype",".remarkrc":"remark",".remarkignore":"remark",".remarkrc.js":"remark",".remarkrc.json":"remark",".remarkrc.yml":"remark",".remarkrc.yaml":"remark",".renovaterc":"renovate","renovate.json":"renovate",".renovaterc.json":"renovate",".retextrc":"retext",".retextignore":"retext",".retextrc.js":"retext",".retextrc.json":"retext",".retextrc.yml":"retext",".retextrc.yaml":"retext","robots.txt":"robots","rollup.config.js":"rollup","rollup.config.mjs":"rollup","rollup.config.coffee":"rollup","rollup.config.ts":"rollup","rollup.config.common.js":"rollup","rollup.config.common.mjs":"rollup","rollup.config.common.coffee":"rollup","rollup.config.common.ts":"rollup","rollup.config.dev.js":"rollup","rollup.config.dev.mjs":"rollup","rollup.config.dev.coffee":"rollup","rollup.config.dev.ts":"rollup","rollup.config.prod.js":"rollup","rollup.config.prod.mjs":"rollup","rollup.config.prod.coffee":"rollup","rollup.config.prod.ts":"rollup",".rspec":"rspec",".rubocop.yml":"rubocop",".rubocop_todo.yml":"rubocop","rust-toolchain":"rust_toolchain",".sentryclirc":"sentry","serverless.yml":"serverless","snapcraft.yaml":"snapcraft",".snyk":"snyk",".solidarity":"solidarity",".solidarity.json":"solidarity",".stylelintrc":"stylelint",".stylelintignore":"stylelintignore",".stylelintcache":"stylelint","stylelint.config.js":"stylelint","stylelint.config.cjs":"stylelint","stylelint.config.mjs":"stylelint","stylelint.config.json":"stylelint","stylelint.config.yaml":"stylelint","stylelint.config.yml":"stylelint","stylelint.config.ts":"stylelint",".stylelintrc.js":"stylelint",".stylelintrc.json":"stylelint",".stylelintrc.yaml":"stylelint",".stylelintrc.yml":"stylelint",".stylelintrc.ts":"stylelint",".stylelintrc.cjs":"stylelint",".stylelintrc.mjs":"stylelint",".stylish-haskell.yaml":"stylish_haskell",".svnignore":"subversion","package.pins":"swift","symfony.lock":"symfony","windi.config.ts":"windi","windi.config.js":"windi","tailwind.js":"tailwind","tailwind.mjs":"tailwind","tailwind.cjs":"tailwind","tailwind.coffee":"tailwind","tailwind.ts":"tailwind","tailwind.cts":"tailwind","tailwind.mts":"tailwind","tailwind.config.mjs":"tailwind","tailwind.config.cjs":"tailwind","tailwind.config.js":"tailwind","tailwind.config.coffee":"tailwind","tailwind.config.ts":"tailwind","tailwind.config.cts":"tailwind","tailwind.config.mts":"tailwind",".testcaferc.json":"testcafe",".tfignore":"tfs","tox.ini":"tox",".travis.yml":"travis","tsconfig.json":"tsconfig","tsconfig.app.json":"tsconfig","tsconfig.base.json":"tsconfig","tsconfig.common.json":"tsconfig","tsconfig.dev.json":"tsconfig","tsconfig.development.json":"tsconfig","tsconfig.e2e.json":"tsconfig","tsconfig.prod.json":"tsconfig","tsconfig.production.json":"tsconfig","tsconfig.server.json":"tsconfig","tsconfig.spec.json":"tsconfig","tsconfig.staging.json":"tsconfig","tsconfig.test.json":"tsconfig","tsconfig.tsd.json":"tsconfig","tsconfig.node.json":"tsconfig","tsconfig.lib.json":"tsconfig","tsconfig.eslint.json":"tsconfig","tsconfig.storybook.json":"tsconfig","tsconfig.tsbuildinfo":"tsconfig","tslint.json":"tslint","tslint.yaml":"tslint","tslint.yml":"tslint",".unibeautifyrc":"unibeautify","unibeautify.config.js":"unibeautify",".unibeautifyrc.js":"unibeautify",".unibeautifyrc.json":"unibeautify",".unibeautifyrc.yaml":"unibeautify",".unibeautifyrc.yml":"unibeautify","vagrantfile":"vagrant",".vimrc":"vim",".gvimrc":"vim",".vscodeignore":"vscode","tasks.json":"vscode","vscodeignore.json":"vscode",".vuerc":"vueconfig","vue.config.js":"vueconfig","vue.config.ts":"vueconfig","wallaby.json":"wallaby","wallaby.js":"wallaby","wallaby.ts":"wallaby","wallaby.coffee":"wallaby","wallaby.conf.json":"wallaby","wallaby.conf.js":"wallaby","wallaby.conf.ts":"wallaby","wallaby.conf.coffee":"wallaby",".wallaby.json":"wallaby",".wallaby.js":"wallaby",".wallaby.ts":"wallaby",".wallaby.coffee":"wallaby",".wallaby.conf.json":"wallaby",".wallaby.conf.js":"wallaby",".wallaby.conf.ts":"wallaby",".wallaby.conf.coffee":"wallaby",".watchmanconfig":"watchmanconfig","wercker.yml":"wercker","wpml-config.xml":"wpml",".yamllint":"yamllint",".yaspellerrc":"yandex",".yaspeller.json":"yandex","yarn.lock":"yarnlock",".yarnrc":"yarn",".yarn.installed":"yarn",".yarnclean":"yarn",".yarn-integrity":"yarn",".yarn-metadata.json":"yarn",".yarnignore":"yarnignore",".yarnrc.yml":"yarn",".yarnrc.yaml":"yarn",".yarnrc.json":"yarn",".yarnrc.json5":"yarn",".yarnrc.cjs":"yarn",".yarnrc.js":"yarn",".yarnrc.lock":"yarn",".yarnrc.txt":"yarn","yarn-error.log":"yarnerror",".yo-rc.json":"yeoman","now.json":"vercel",".nowignore":"vercel","vercel.json":"vercel",".vercel":"vercel",".vercelignore":"vercel","vite.config.js":"vite","vite.config.mjs":"vite","vite.config.cjs":"vite","vite.config.ts":"vite","vite.config.mts":"vite","vite.config.cts":"vite",".nvmrc":"nvm","example.env":"env",".env.staging":"env",".env.sample":"env",".env.preprod":"env",".env.prod":"env",".env.production":"env",".env.local":"env",".env.dev":"env",".env.dev.local":"env",".env.dev.prod":"env",".env.dev.preprod":"env",".env.dev.production":"env",".env.dev.staging":"env",".env.development":"env",".env.example":"env",".env.test":"env",".env.dist":"env",".env.default":"env",".jinja":"jinja","jenkins.yaml":"jenkins","jenkins.yml":"jenkins",".compodocrc":"compodoc",".compodocrc.json":"compodoc",".compodocrc.yaml":"compodoc",".compodocrc.yml":"compodoc","bsconfig.json":"bsconfig",".clang-format":"llvm",".clang-tidy":"llvm",".clangd":"llvm",".parcelrc":"parcel","dune":"dune","dune-project":"duneproject",".adonisrc.json":"adonis","astro.config.js":"astroconfig","astro.config.cjs":"astroconfig","astro.config.mjs":"astroconfig","astro.config.ts":"astroconfig","astro.config.cts":"astroconfig","astro.config.mts":"astroconfig","svelte.config.js":"svelteconfig","svelte.config.ts":"svelteconfig",".tool-versions":"toolversions","CMakeSettings.json":"cmake","CMakeLists.txt":"cmake","toolchain.cmake":"cmake",".cmake":"cmake","Cargo.toml":"cargo","Cargo.lock":"cargolock","pnpm-lock.yaml":"pnpmlock","tauri.conf.json":"tauri","tauri.conf.json5":"tauri","tauri.linux.conf.json":"tauri","tauri.windows.conf.json":"tauri","tauri.macos.conf.json":"tauri","next.config.js":"nextconfig","next.config.mjs":"nextconfig","next.config.ts":"nextconfig","nextron.config.js":"nextron","nextron.config.ts":"nextron","poetry.toml":"poetry","poetry.lock":"poetrylock","pyproject.toml":"pyproject","rustfmt.toml":"rustfmt",".rustfmt.toml":"rustfmt","cucumber.yml":"cucumber","cucumber.yaml":"cucumber","cucumber.js":"cucumber","cucumber.ts":"cucumber","cucumber.cjs":"cucumber","cucumber.mjs":"cucumber","cucumber.json":"cucumber","flake.lock":"flakelock","ace":"ace","ace-manifest.json":"acemanifest","knexfile.js":"knex","knexfile.ts":"knex","launch.json":"launch","redis.conf":"redis","sequelize.js":"sequelize","sequelize.ts":"sequelize","sequelize.cjs":"sequelize",".sequelizerc":"sequelize",".sequelizerc.js":"sequelize",".sequelizerc.json":"sequelize","cypress.json":"cypress","cypress.env.json":"cypress","cypress.config.js":"cypress","cypress.config.ts":"cypress","cypress.config.cjs":"cypress","playwright.config.ts":"playright","playwright.config.js":"playright","playwright.config.cjs":"playright","vitest.config.ts":"vitest","vitest.config.cts":"vitest","vitest.config.mts":"vitest","vitest.config.js":"vitest","vitest.config.cjs":"vitest","vitest.config.mjs":"vitest","vitest.workspace.ts":"vitest","vitest.workspace.cts":"vitest","vitest.workspace.mts":"vitest","vitest.workspace.js":"vitest","vitest.workspace.cjs":"vitest","vitest.workspace.mjs":"vitest","vite-env.d.ts":"viteenv","vite-env.d.js":"viteenv","pubspec.lock":"flutterlock","pubspec.yaml":"flutter",".packages":"flutterpackage",".htaccess":"htaccess","nx.json":"nx","project.json":"nx","nx.instructions.md":"nx","nx.jsonc":"nx","v.mod":"vmod","quasar.config.js":"quasar","quasar.config.ts":"quasar","quasar.config.cjs":"quasar","quasar.config.mjs":"quasar","quarkus.properties":"quarkus","theme.properties":"ui","gradlew":"gradle","gradle-wrapper.properties":"gradle","gradlew.bat":"gradlebat","makefile.win":"makefile","makefile":"makefile","make":"makefile","version":"version","server":"sql","migrate":"sql",".commitlintrc":"commitlint",".commitlintrc.json":"commitlint",".commitlintrc.yaml":"commitlint",".commitlintrc.yml":"commitlint",".commitlintrc.js":"commitlint",".commitlintrc.cjs":"commitlint",".commitlintrc.ts":"commitlint",".commitlintrc.cts":"commitlint","commitlint.config.js":"commitlint","commitlint.config.cjs":"commitlint","commitlint.config.ts":"commitlint","commitlint.config.cts":"commitlint",".terraform-version":"terraformversion","TerraFile":"terrafile","tfstate.backup":"terraform",".code-workspace":"codeworkspace","hardhat.config.js":"hardhat","hardhat.config.ts":"hardhat","hardhat.config.cts":"hardhat","hardhat.config.cjs":"hardhat","hardhat.config.mjs":"hardhat","taze.config.js":"taze","taze.config.ts":"taze","taze.config.cjs":"taze","taze.config.mjs":"taze",".tazerc.json":"taze","turbo.json":"turbo","turbo.jsonc":"turbo","uno.config.ts":"unocss","uno.config.js":"unocss","uno.config.mjs":"unocss","uno.config.mts":"unocss","unocss.config.ts":"unocss","unocss.config.js":"unocss","unocss.config.mjs":"unocss","unocss.config.mts":"unocss","atomizer.config.js":"atomizer","atomizer.config.cjs":"atomizer","atomizer.config.mjs":"atomizer","atomizer.config.ts":"atomizer","esbuild.js":"esbuild","esbuild.mjs":"esbuild","esbuild.cjs":"esbuild","esbuild.ts":"esbuild","mix.exs":"mix","mix.lock":"mixlock",".DS_Store":"dsstore","remix.config.js":"remix","remix.config.cjs":"remix","remix.config.mjs":"remix","remix.config.ts":"remix","xmake.lua":"xmake",".sailsrc":"sails","farm.config.ts":"farm","farm.config.js":"farm","bunfig.toml":"bun",".bunfig.toml":"bun","bun.lockb":"bunlock","bun.lock":"bunlock",".air.toml":"air","rome.json":"rome","biome.json":"biome","bicepconfig.json":"bicepconfig","drizzle.config.ts":"drizzle","drizzle.config.js":"drizzle","drizzle.config.json":"drizzle","panda.config.ts":"panda","panda.config.js":"panda","panda.config.json":"panda","panda.config.cjs":"panda","panda.config.mjs":"panda","panda.config.cts":"panda","panda.config.mts":"panda",".buckconfig":"buck","Ballerina.toml":"ballerinaconfig","knip.json":"knip","knip.jsonc":"knip",".knip.json":"knip",".knip.jsonc":"knip","knip.ts":"knip","knip.js":"knip","knip.config.ts":"knip","knip.config.js":"knip","todo.md":"todo",".todo.md":"todo","todo.txt":"todo",".todo.txt":"todo","todo":"todo","mkdocs.yml":"mkdocs","mkdocs.yaml":"mkdocs","gleam.toml":"gleamconfig",".oxlintrc.json":"oxlint","oxlint.json":"oxlint","oxlint.config.js":"oxlint","oxlint.config.ts":"oxlint","oxlint.config.cjs":"oxlint","oxlint.config.mjs":"oxlint","oxlint.config.cts":"oxlint","oxlint.config.mts":"oxlint",".cursorrules":"cursor","plopfile.js":"plop","plopfile.cjs":"plop","plopfile.mjs":"plop","plopfile.ts":"plop","plopfile.cts":"plop","config.mockoon.json":"mockoon","mockoon.json":"mockoon","mockoon.yaml":"mockoon","mockoon.yml":"mockoon","mockoon.env":"mockoon","mockoon.env.json":"mockoon","mockoon.env.yaml":"mockoon","mockoon.env.yml":"mockoon","mockoon.env.js":"mockoon","mockoon.env.ts":"mockoon","mockoon.env.cjs":"mockoon","mockoon.env.mjs":"mockoon","mockoon.env.cts":"mockoon","mockoon.env.mts":"mockoon","copilot-instructions.md":"copilot",".copilot-instructions":"copilot",".instructions":"instructions","instructions.md":"instructions","instructions.txt":"instructions","instructions":"instructions","instructions.json":"instructions","instructions.yaml":"instructions","instructions.yml":"instructions",".keep":"keep",".keepignore":"keep","CLAUDE.md":"claude","claude.md":"claude","claude.txt":"claude","claude":"claude","claude.json":"claude","claude.yaml":"claude",".claude_code_config":"claude",".claude":"claude","claude.config.js":"claude",".claude.yaml":"claude",".clauderc":"claude","claude-instructions.md":"claude",".claude-code":"claude","claude-code.config":"claude"},"languageIds":{"actionscript":"actionscript","ada":"ada","advpl":"advpl","affectscript":"affectscript","al":"al","ansible":"ansible","antlr":"antlr","anyscript":"anyscript","apacheconf":"apache","apex":"apex","apiblueprint":"apib","apl":"apl","applescript":"applescript","asciidoc":"asciidoc","asp":"asp","asp (html)":"asp","arm":"assembly","asm":"assembly","ats":"ats","ahk":"autohotkey","autoit":"autoit","avro":"avro","azcli":"azure","azure-pipelines":"azurepipelines","ballerina":"ballerina","bat":"bat","bats":"bats","bazel":"bazel","befunge":"befunge","befunge98":"befunge","biml":"biml","blade":"blade","laravel-blade":"blade","bolt":"bolt","bosque":"bosque","c":"c","c-al":"c_al","cabal":"cabal","caddyfile":"caddy","cddl":"cddl","ceylon":"ceylon","cfml":"cf","lang-cfml":"cf","cfc":"cfc","cfmhtml":"cfm","cookbook":"chef_cookbook","clojure":"clojure","clojurescript":"clojurescript","manifest-yaml":"cloudfoundry","cmake":"cmake","cmake-cache":"cmake","cobol":"cobol","coffeescript":"coffeescript","properties":"properties","dotenv":"config","confluence":"confluence","cpp":"cpp","crystal":"crystal","csharp":"csharp","css":"css","feature":"cucumber","cuda":"cuda","cython":"cython","dal":"dal","dart":"dartlang","pascal":"pascal","objectpascal":"pascal","diff":"diff","django-html":"django","django-txt":"django","d":"dlang","dscript":"dlang","dml":"dlang","diet":"dlang","dockerfile":"docker","ignore":"docker","dotjs":"dotjs","doxygen":"doxygen","drools":"drools","dustjs":"dustjs","dylan":"dylan","dylan-lid":"dylan","edge":"edge","eex":"eex","html-eex":"eex","es":"elastic","elixir":"elixir","elm":"elm","erb":"erb","erlang":"erlang","falcon":"falcon","fortran":"fortran","fortran-modern":"fortran","FortranFreeForm":"fortran","fortran_fixed-form":"fortran","ftl":"freemarker","fsharp":"fsharp","fthtml":"fthtml","galen":"galen","gml-gms":"gamemaker","gml-gms2":"gamemaker2","gml-gm81":"gamemaker81","gcode":"gcode","genstat":"genstat","git-commit":"git","git-rebase":"git","glsl":"glsl","glyphs":"glyphs","gnuplot":"gnuplot","go":"go","golang":"go","go-sum":"go","go-mod":"go","go-xml":"go","gdscript":"godot","graphql":"graphql","dot":"graphviz","groovy":"groovy","haml":"haml","handlebars":"handlebars","harbour":"harbour","haskell":"haskell","literate haskell":"haskell","haxe":"haxe","hxml":"haxe","Haxe AST dump":"haxe","helm":"helm","hjson":"hjson","hlsl":"opengl","home-assistant":"homeassistant","hosts":"host","html":"html","http":"http","hunspell.aff":"hunspell","hunspell.dic":"hunspell","hy":"hy","icl":"icl","imba":"imba","4GL":"informix","ini":"conf","ink":"ink","innosetup":"innosetup","io":"io","iodine":"iodine","janet":"janet","java":"java","raku":"raku","jekyll":"jekyll","jenkins":"jenkins","declarative":"jenkins","jenkinsfile":"jenkins","jinja":"jinja","code-referencing":"vscode","search-result":"vscode","type":"vscode","javascript":"js","json":"json","jsonl":"json","json-tmlanguage":"json","jsonc":"json","json5":"json5","jsonnet":"jsonnet","julia":"julia","juliamarkdown":"julia","kivy":"kivy","kos":"kos","kotlin":"kotlin","kusto":"kusto","latino":"latino","less":"less","lex":"lex","lisp":"lisp","lolcode":"lolcode","code-text-binary":"binary","lsl":"lsl","lua":"lua","makefile":"makefile","markdown":"markdown","marko":"marko","matlab":"matlab","maxscript":"maxscript","mel":"maya","mediawiki":"mediawiki","meson":"meson","mjml":"mjml","mlang":"mlang","powerquerymlanguage":"mlang","mojolicious":"mojolicious","mongo":"mongo","mson":"mson","nearley":"nearly","nim":"nim","nimble":"nimble","nix":"nix","nsis":"nsi","nfl":"nsi","nsl":"nsi","bridlensis":"nsi","nunjucks":"nunjucks","objective-c":"c","objective-cpp":"cpp","ocaml":"ocaml","ocamllex":"ocaml","menhir":"ocaml","openhab":"openHAB","pddl":"pddl","happenings":"pddl_happenings","plan":"pddl_plan","phoenix-heex":"eex","perl":"perl","perl6":"perl6","pgsql":"pgsql","php":"php","pine":"pine","pinescript":"pine","pip-requirements":"python","platformio-debug.disassembly":"platformio","platformio-debug.memoryview":"platformio","platformio-debug.asm":"platformio","plsql":"plsql","oracle":"plsql","polymer":"polymer","pony":"pony","postcss":"postcss","powershell":"powershell","prisma":"prisma","pde":"processinglang","abl":"progress","prolog":"prolog","prometheus":"prometheus","proto3":"protobuf","proto":"protobuf","jade":"pug","pug":"pug","puppet":"puppet","purescript":"purescript","pyret":"pyret","python":"python","qlik":"qlikview","qml":"qml","qsharp":"qsharp","r":"r","racket":"racket","raml":"raml","razor":"razor","aspnetcorerazor":"razor","javascriptreact":"reactjs","typescriptreact":"reactts","reason":"reason","red":"red","restructuredtext":"restructuredtext","rexx":"rexx","riot":"riot","rmd":"rmd","mdx":"markdownx","robot":"robotframework","ruby":"ruby","rust":"rust","san":"san","SAS":"sas","sbt":"sbt","scala":"scala","scilab":"scilab","vbscript":"script","scss":"scss","sdl":"sdlang","shaderlab":"shaderlab","shellscript":"shell","silverstripe":"silverstripe","eskip":"skipper","slang":"slang","slice":"slice","slim":"slim","smarty":"smarty","snort":"snort","solidity":"solidity","snippets":"vscode","sqf":"sqf","sql":"sql","squirrel":"squirrel","stan":"stan","stata":"stata","stencil":"stencil","stencil-html":"stencil","stylable":"stylable","source.css.styled":"styled","stylus":"stylus","svelte":"svelte","Swagger":"swagger","swagger":"swagger","swift":"swift","swig":"swig","cuda-cpp":"nvidia","systemd-unit-file":"systemd","systemverilog":"systemverilog","t4":"t4tt","tera":"tera","terraform":"terraform","tex":"latex","log":"log","dockercompose":"docker","latex":"latex","vue-directives":"vue","vue-injection-markdown":"vue","vue-interpolations":"vue","vue-sfc-style-variable-injection":"vue","bibtex":"latex","doctex":"tex","plaintext":"text","textile":"textile","toml":"toml","tt":"tt","ttcn":"ttcn","twig":"twig","typescript":"typescript","typoscript":"typo3","vb":"vb","vba":"vba","velocity":"velocity","verilog":"verilog","vhdl":"vhdl","viml":"vim","v":"vlang","volt":"volt","vue":"vue","wasm":"wasm","wat":"wasm","wenyan":"wenyan","wolfram":"wolfram","wurstlang":"wurst","wurst":"wurst","xmake":"xmake","xml":"xml","xquery":"xquery","xsl":"xml","yacc":"yacc","yaml":"yaml","yaml-tmlanguage":"yaml","yang":"yang","zig":"zig","vitest-snapshot":"vitest","instructions":"instructions","prompt":"prompt"}}} \ No newline at end of file +{ + "hidesExplorerArrows": true, + "iconDefinitions": { + "_file": { "iconPath": "./icons/file.svg" }, + "_folder": { "iconPath": "./icons/folder.svg" }, + "_folder_open": { "iconPath": "./icons/folder_open.svg" }, + "_root_folder": { "iconPath": "./icons/root_folder.svg" }, + "_root_folder_open": { "iconPath": "./icons/root_folder_open.svg" }, + "_root_folder_light": { "iconPath": "./icons/root_folder_light.svg" }, + "_root_folder_light_open": { + "iconPath": "./icons/root_folder_light_open.svg" + }, + "ace": { "iconPath": "./icons/ace.svg" }, + "acemanifest": { "iconPath": "./icons/acemanifest.svg" }, + "adoc": { "iconPath": "./icons/adoc.svg" }, + "adonis": { "iconPath": "./icons/adonis.svg" }, + "adonisconfig": { "iconPath": "./icons/adonisconfig.svg" }, + "afdesign": { "iconPath": "./icons/afdesign.svg" }, + "afphoto": { "iconPath": "./icons/afphoto.svg" }, + "afpub": { "iconPath": "./icons/afpub.svg" }, + "ai": { "iconPath": "./icons/ai.svg" }, + "air": { "iconPath": "./icons/air.svg" }, + "angular": { "iconPath": "./icons/angular.svg" }, + "anim": { "iconPath": "./icons/anim.svg" }, + "astro": { "iconPath": "./icons/astro.svg" }, + "astroconfig": { "iconPath": "./icons/astroconfig.svg" }, + "atomizer": { "iconPath": "./icons/atomizer.svg" }, + "audio": { "iconPath": "./icons/audio.svg" }, + "audiomp3": { "iconPath": "./icons/audiomp3.svg" }, + "audioogg": { "iconPath": "./icons/audioogg.svg" }, + "audiowav": { "iconPath": "./icons/audiowav.svg" }, + "audiowv": { "iconPath": "./icons/audiowv.svg" }, + "azure": { "iconPath": "./icons/azure.svg" }, + "babel": { "iconPath": "./icons/babel.svg" }, + "ballerina": { "iconPath": "./icons/ballerina.svg" }, + "ballerinaconfig": { "iconPath": "./icons/ballerinaconfig.svg" }, + "bat": { "iconPath": "./icons/bat.svg" }, + "bazel": { "iconPath": "./icons/bazel.svg" }, + "bazelignore": { "iconPath": "./icons/bazelignore.svg" }, + "bicep": { "iconPath": "./icons/bicep.svg" }, + "bicepconfig": { "iconPath": "./icons/bicepconfig.svg" }, + "bicepparam": { "iconPath": "./icons/bicepparam.svg" }, + "binary": { "iconPath": "./icons/binary.svg" }, + "biome": { "iconPath": "./icons/biome.svg" }, + "blade": { "iconPath": "./icons/blade.svg" }, + "brotli": { "iconPath": "./icons/brotli.svg" }, + "browserslist": { "iconPath": "./icons/browserslist.svg" }, + "bruno": { "iconPath": "./icons/bruno.svg" }, + "bsconfig": { "iconPath": "./icons/bsconfig.svg" }, + "buck": { "iconPath": "./icons/buck.svg" }, + "bun": { "iconPath": "./icons/bun.svg" }, + "bundler": { "iconPath": "./icons/bundler.svg" }, + "bunlock": { "iconPath": "./icons/bunlock.svg" }, + "c": { "iconPath": "./icons/c.svg" }, + "cargo": { "iconPath": "./icons/cargo.svg" }, + "cargolock": { "iconPath": "./icons/cargolock.svg" }, + "cert": { "iconPath": "./icons/cert.svg" }, + "cheader": { "iconPath": "./icons/cheader.svg" }, + "civet": { "iconPath": "./icons/civet.svg" }, + "claude": { "iconPath": "./icons/claude.svg" }, + "cli": { "iconPath": "./icons/cli.svg" }, + "clojure": { "iconPath": "./icons/clojure.svg" }, + "cmake": { "iconPath": "./icons/cmake.svg" }, + "codeworkspace": { "iconPath": "./icons/codeworkspace.svg" }, + "coffeescript": { "iconPath": "./icons/coffeescript.svg" }, + "commitlint": { "iconPath": "./icons/commitlint.svg" }, + "compodoc": { "iconPath": "./icons/compodoc.svg" }, + "composer": { "iconPath": "./icons/composer.svg" }, + "composerlock": { "iconPath": "./icons/composerlock.svg" }, + "conan": { "iconPath": "./icons/conan.svg" }, + "conf": { "iconPath": "./icons/conf.svg" }, + "copilot": { "iconPath": "./icons/copilot.svg" }, + "cpp": { "iconPath": "./icons/cpp.svg" }, + "crystal": { "iconPath": "./icons/crystal.svg" }, + "csharp": { "iconPath": "./icons/csharp.svg" }, + "cshtml": { "iconPath": "./icons/cshtml.svg" }, + "csproj": { "iconPath": "./icons/csproj.svg" }, + "css": { "iconPath": "./icons/css.svg" }, + "cssmap": { "iconPath": "./icons/cssmap.svg" }, + "csv": { "iconPath": "./icons/csv.svg" }, + "cucumber": { "iconPath": "./icons/cucumber.svg" }, + "cursor": { "iconPath": "./icons/cursor.svg" }, + "cypress": { "iconPath": "./icons/cypress.svg" }, + "cypressjs": { "iconPath": "./icons/cypressjs.svg" }, + "cypressts": { "iconPath": "./icons/cypressts.svg" }, + "d": { "iconPath": "./icons/d.svg" }, + "dartlang": { "iconPath": "./icons/dartlang.svg" }, + "delphiproject": { "iconPath": "./icons/delphiproject.svg" }, + "diff": { "iconPath": "./icons/diff.svg" }, + "docker": { "iconPath": "./icons/docker.svg" }, + "dockerdebug": { "iconPath": "./icons/dockerdebug.svg" }, + "dockerignore": { "iconPath": "./icons/dockerignore.svg" }, + "drawio": { "iconPath": "./icons/drawio.svg" }, + "drizzle": { "iconPath": "./icons/drizzle.svg" }, + "dsstore": { "iconPath": "./icons/dsstore.svg" }, + "dune": { "iconPath": "./icons/dune.svg" }, + "duneproject": { "iconPath": "./icons/duneproject.svg" }, + "edge": { "iconPath": "./icons/edge.svg" }, + "editorconfig": { "iconPath": "./icons/editorconfig.svg" }, + "eex": { "iconPath": "./icons/eex.svg" }, + "elixir": { "iconPath": "./icons/elixir.svg" }, + "elm": { "iconPath": "./icons/elm.svg" }, + "env": { "iconPath": "./icons/env.svg" }, + "eraser": { "iconPath": "./icons/eraser.svg" }, + "erb": { "iconPath": "./icons/erb.svg" }, + "erlang": { "iconPath": "./icons/erlang.svg" }, + "esbuild": { "iconPath": "./icons/esbuild.svg" }, + "eslint": { "iconPath": "./icons/eslint.svg" }, + "eslintignore": { "iconPath": "./icons/eslintignore.svg" }, + "excalidraw": { "iconPath": "./icons/excalidraw.svg" }, + "exs": { "iconPath": "./icons/exs.svg" }, + "exx": { "iconPath": "./icons/exx.svg" }, + "farm": { "iconPath": "./icons/farm.svg" }, + "figma": { "iconPath": "./icons/figma.svg" }, + "file": { "iconPath": "./icons/file.svg" }, + "file_light": { "iconPath": "./icons/file_light.svg" }, + "flakelock": { "iconPath": "./icons/flakelock.svg" }, + "flutter": { "iconPath": "./icons/flutter.svg" }, + "flutterlock": { "iconPath": "./icons/flutterlock.svg" }, + "flutterpackage": { "iconPath": "./icons/flutterpackage.svg" }, + "folder": { "iconPath": "./icons/folder.svg" }, + "folder_open": { "iconPath": "./icons/folder_open.svg" }, + "fonteot": { "iconPath": "./icons/fonteot.svg" }, + "fontotf": { "iconPath": "./icons/fontotf.svg" }, + "fontttf": { "iconPath": "./icons/fontttf.svg" }, + "fontwoff": { "iconPath": "./icons/fontwoff.svg" }, + "fontwoff2": { "iconPath": "./icons/fontwoff2.svg" }, + "freemarker": { "iconPath": "./icons/freemarker.svg" }, + "fsharp": { "iconPath": "./icons/fsharp.svg" }, + "gbl": { "iconPath": "./icons/gbl.svg" }, + "git": { "iconPath": "./icons/git.svg" }, + "gitlab": { "iconPath": "./icons/gitlab.svg" }, + "gleam": { "iconPath": "./icons/gleam.svg" }, + "gleamconfig": { "iconPath": "./icons/gleamconfig.svg" }, + "go": { "iconPath": "./icons/go.svg" }, + "godot": { "iconPath": "./icons/godot.svg" }, + "go_package": { "iconPath": "./icons/go_package.svg" }, + "gradle": { "iconPath": "./icons/gradle.svg" }, + "gradlebat": { "iconPath": "./icons/gradlebat.svg" }, + "gradlekotlin": { "iconPath": "./icons/gradlekotlin.svg" }, + "grain": { "iconPath": "./icons/grain.svg" }, + "graphql": { "iconPath": "./icons/graphql.svg" }, + "groovy": { "iconPath": "./icons/groovy.svg" }, + "grunt": { "iconPath": "./icons/grunt.svg" }, + "gulp": { "iconPath": "./icons/gulp.svg" }, + "h": { "iconPath": "./icons/h.svg" }, + "haml": { "iconPath": "./icons/haml.svg" }, + "handlebars": { "iconPath": "./icons/handlebars.svg" }, + "hardhat": { "iconPath": "./icons/hardhat.svg" }, + "hash": { "iconPath": "./icons/hash.svg" }, + "hashicorp": { "iconPath": "./icons/hashicorp.svg" }, + "haskell": { "iconPath": "./icons/haskell.svg" }, + "haxe": { "iconPath": "./icons/haxe.svg" }, + "haxeml": { "iconPath": "./icons/haxeml.svg" }, + "hpp": { "iconPath": "./icons/hpp.svg" }, + "htaccess": { "iconPath": "./icons/htaccess.svg" }, + "html": { "iconPath": "./icons/html.svg" }, + "http": { "iconPath": "./icons/http.svg" }, + "identifier": { "iconPath": "./icons/identifier.svg" }, + "image": { "iconPath": "./icons/image.svg" }, + "imagegif": { "iconPath": "./icons/imagegif.svg" }, + "imageico": { "iconPath": "./icons/imageico.svg" }, + "imagejpg": { "iconPath": "./icons/imagejpg.svg" }, + "imagepng": { "iconPath": "./icons/imagepng.svg" }, + "imagewebp": { "iconPath": "./icons/imagewebp.svg" }, + "imba": { "iconPath": "./icons/imba.svg" }, + "info": { "iconPath": "./icons/info.svg" }, + "instructions": { "iconPath": "./icons/instructions.svg" }, + "ipynb": { "iconPath": "./icons/ipynb.svg" }, + "jar": { "iconPath": "./icons/jar.svg" }, + "java": { "iconPath": "./icons/java.svg" }, + "jenkins": { "iconPath": "./icons/jenkins.svg" }, + "jest": { "iconPath": "./icons/jest.svg" }, + "jinja": { "iconPath": "./icons/jinja.svg" }, + "js": { "iconPath": "./icons/js.svg" }, + "jsmap": { "iconPath": "./icons/jsmap.svg" }, + "json": { "iconPath": "./icons/json.svg" }, + "jsp": { "iconPath": "./icons/jsp.svg" }, + "julia": { "iconPath": "./icons/julia.svg" }, + "karma": { "iconPath": "./icons/karma.svg" }, + "keep": { "iconPath": "./icons/keep.svg" }, + "key": { "iconPath": "./icons/key.svg" }, + "knex": { "iconPath": "./icons/knex.svg" }, + "knip": { "iconPath": "./icons/knip.svg" }, + "kotlin": { "iconPath": "./icons/kotlin.svg" }, + "kotlins": { "iconPath": "./icons/kotlins.svg" }, + "krita": { "iconPath": "./icons/krita.svg" }, + "latex": { "iconPath": "./icons/latex.svg" }, + "launch": { "iconPath": "./icons/launch.svg" }, + "lazarusproject": { "iconPath": "./icons/lazarusproject.svg" }, + "less": { "iconPath": "./icons/less.svg" }, + "license": { "iconPath": "./icons/license.svg" }, + "light_editorconfig": { "iconPath": "./icons/light_editorconfig.svg" }, + "liquid": { "iconPath": "./icons/liquid.svg" }, + "llvm": { "iconPath": "./icons/llvm.svg" }, + "lock": { "iconPath": "./icons/lock.svg" }, + "log": { "iconPath": "./icons/log.svg" }, + "lua": { "iconPath": "./icons/lua.svg" }, + "m": { "iconPath": "./icons/m.svg" }, + "makefile": { "iconPath": "./icons/makefile.svg" }, + "manifest": { "iconPath": "./icons/manifest.svg" }, + "markdown": { "iconPath": "./icons/markdown.svg" }, + "markdownx": { "iconPath": "./icons/markdownx.svg" }, + "maven": { "iconPath": "./icons/maven.svg" }, + "mermaid": { "iconPath": "./icons/mermaid.svg" }, + "mesh": { "iconPath": "./icons/mesh.svg" }, + "mgcb": { "iconPath": "./icons/mgcb.svg" }, + "mint": { "iconPath": "./icons/mint.svg" }, + "mix": { "iconPath": "./icons/mix.svg" }, + "mixlock": { "iconPath": "./icons/mixlock.svg" }, + "mjml": { "iconPath": "./icons/mjml.svg" }, + "mkdocs": { "iconPath": "./icons/mkdocs.svg" }, + "mockoon": { "iconPath": "./icons/mockoon.svg" }, + "motoko": { "iconPath": "./icons/motoko.svg" }, + "mov": { "iconPath": "./icons/mov.svg" }, + "mp4": { "iconPath": "./icons/mp4.svg" }, + "mtl": { "iconPath": "./icons/mtl.svg" }, + "mustache": { "iconPath": "./icons/mustache.svg" }, + "nelua": { "iconPath": "./icons/nelua.svg" }, + "neon": { "iconPath": "./icons/neon.svg" }, + "nestjs": { "iconPath": "./icons/nestjs.svg" }, + "nestjscontroller": { "iconPath": "./icons/nestjscontroller.svg" }, + "nestjsdecorator": { "iconPath": "./icons/nestjsdecorator.svg" }, + "nestjsdto": { "iconPath": "./icons/nestjsdto.svg" }, + "nestjsentity": { "iconPath": "./icons/nestjsentity.svg" }, + "nestjsfilter": { "iconPath": "./icons/nestjsfilter.svg" }, + "nestjsguard": { "iconPath": "./icons/nestjsguard.svg" }, + "nestjsinterceptor": { "iconPath": "./icons/nestjsinterceptor.svg" }, + "nestjsmodule": { "iconPath": "./icons/nestjsmodule.svg" }, + "nestjsrepository": { "iconPath": "./icons/nestjsrepository.svg" }, + "nestjsresolver": { "iconPath": "./icons/nestjsresolver.svg" }, + "nestjsservice": { "iconPath": "./icons/nestjsservice.svg" }, + "nestscheduler": { "iconPath": "./icons/nestscheduler.svg" }, + "netlify": { "iconPath": "./icons/netlify.svg" }, + "nextconfig": { "iconPath": "./icons/nextconfig.svg" }, + "nextron": { "iconPath": "./icons/nextron.svg" }, + "nginx": { "iconPath": "./icons/nginx.svg" }, + "nim": { "iconPath": "./icons/nim.svg" }, + "nix": { "iconPath": "./icons/nix.svg" }, + "njk": { "iconPath": "./icons/njk.svg" }, + "node": { "iconPath": "./icons/node.svg" }, + "nodemon": { "iconPath": "./icons/nodemon.svg" }, + "npm": { "iconPath": "./icons/npm.svg" }, + "npmlock": { "iconPath": "./icons/npmlock.svg" }, + "nuxt": { "iconPath": "./icons/nuxt.svg" }, + "nvidia": { "iconPath": "./icons/nvidia.svg" }, + "nvim": { "iconPath": "./icons/nvim.svg" }, + "nvm": { "iconPath": "./icons/nvm.svg" }, + "nx": { "iconPath": "./icons/nx.svg" }, + "obj": { "iconPath": "./icons/obj.svg" }, + "ocaml": { "iconPath": "./icons/ocaml.svg" }, + "ocamli": { "iconPath": "./icons/ocamli.svg" }, + "ocamll": { "iconPath": "./icons/ocamll.svg" }, + "ocamly": { "iconPath": "./icons/ocamly.svg" }, + "odin": { "iconPath": "./icons/odin.svg" }, + "opengl": { "iconPath": "./icons/opengl.svg" }, + "oxlint": { "iconPath": "./icons/oxlint.svg" }, + "panda": { "iconPath": "./icons/panda.svg" }, + "parcel": { "iconPath": "./icons/parcel.svg" }, + "pascal": { "iconPath": "./icons/pascal.svg" }, + "pdf": { "iconPath": "./icons/pdf.svg" }, + "perl": { "iconPath": "./icons/perl.svg" }, + "perlm": { "iconPath": "./icons/perlm.svg" }, + "pfx": { "iconPath": "./icons/pfx.svg" }, + "photoshop": { "iconPath": "./icons/photoshop.svg" }, + "php": { "iconPath": "./icons/php.svg" }, + "plantuml": { "iconPath": "./icons/plantuml.svg" }, + "playright": { "iconPath": "./icons/playright.svg" }, + "plop": { "iconPath": "./icons/plop.svg" }, + "pnpm": { "iconPath": "./icons/pnpm.svg" }, + "pnpmlock": { "iconPath": "./icons/pnpmlock.svg" }, + "poetry": { "iconPath": "./icons/poetry.svg" }, + "poetrylock": { "iconPath": "./icons/poetrylock.svg" }, + "postcssconfig": { "iconPath": "./icons/postcssconfig.svg" }, + "powershell": { "iconPath": "./icons/powershell.svg" }, + "powershelldata": { "iconPath": "./icons/powershelldata.svg" }, + "powershellmodule": { "iconPath": "./icons/powershellmodule.svg" }, + "precommit": { "iconPath": "./icons/precommit.svg" }, + "prettier": { "iconPath": "./icons/prettier.svg" }, + "prettierignore": { "iconPath": "./icons/prettierignore.svg" }, + "prisma": { "iconPath": "./icons/prisma.svg" }, + "prolog": { "iconPath": "./icons/prolog.svg" }, + "prompt": { "iconPath": "./icons/prompt.svg" }, + "properties": { "iconPath": "./icons/properties.svg" }, + "proto": { "iconPath": "./icons/proto.svg" }, + "pug": { "iconPath": "./icons/pug.svg" }, + "pvk": { "iconPath": "./icons/pvk.svg" }, + "pyproject": { "iconPath": "./icons/pyproject.svg" }, + "python": { "iconPath": "./icons/python.svg" }, + "qt": { "iconPath": "./icons/qt.svg" }, + "quarkus": { "iconPath": "./icons/quarkus.svg" }, + "quasar": { "iconPath": "./icons/quasar.svg" }, + "r": { "iconPath": "./icons/r.svg" }, + "racket": { "iconPath": "./icons/racket.svg" }, + "raku": { "iconPath": "./icons/raku.svg" }, + "razor": { "iconPath": "./icons/razor.svg" }, + "reactjs": { "iconPath": "./icons/reactjs.svg" }, + "reactts": { "iconPath": "./icons/reactts.svg" }, + "readme": { "iconPath": "./icons/readme.svg" }, + "redis": { "iconPath": "./icons/redis.svg" }, + "rego": { "iconPath": "./icons/rego.svg" }, + "remix": { "iconPath": "./icons/remix.svg" }, + "rescript": { "iconPath": "./icons/rescript.svg" }, + "rescriptinterface": { "iconPath": "./icons/rescriptinterface.svg" }, + "restructuredtext": { "iconPath": "./icons/restructuredtext.svg" }, + "rjson": { "iconPath": "./icons/rjson.svg" }, + "robots": { "iconPath": "./icons/robots.svg" }, + "rollup": { "iconPath": "./icons/rollup.svg" }, + "rome": { "iconPath": "./icons/rome.svg" }, + "ron": { "iconPath": "./icons/ron.svg" }, + "root_folder": { "iconPath": "./icons/root_folder.svg" }, + "root_folder_light": { "iconPath": "./icons/root_folder_light.svg" }, + "root_folder_light_open": { + "iconPath": "./icons/root_folder_light_open.svg" + }, + "root_folder_open": { "iconPath": "./icons/root_folder_open.svg" }, + "ruby": { "iconPath": "./icons/ruby.svg" }, + "rust": { "iconPath": "./icons/rust.svg" }, + "rustfmt": { "iconPath": "./icons/rustfmt.svg" }, + "sails": { "iconPath": "./icons/sails.svg" }, + "salesforce": { "iconPath": "./icons/salesforce.svg" }, + "sass": { "iconPath": "./icons/sass.svg" }, + "scala": { "iconPath": "./icons/scala.svg" }, + "scss": { "iconPath": "./icons/scss.svg" }, + "sentinel": { "iconPath": "./icons/sentinel.svg" }, + "sequelize": { "iconPath": "./icons/sequelize.svg" }, + "shaderlab": { "iconPath": "./icons/shaderlab.svg" }, + "shell": { "iconPath": "./icons/shell.svg" }, + "silq": { "iconPath": "./icons/silq.svg" }, + "slim": { "iconPath": "./icons/slim.svg" }, + "sln": { "iconPath": "./icons/sln.svg" }, + "smarty": { "iconPath": "./icons/smarty.svg" }, + "sol": { "iconPath": "./icons/sol.svg" }, + "spc": { "iconPath": "./icons/spc.svg" }, + "sql": { "iconPath": "./icons/sql.svg" }, + "sqlite": { "iconPath": "./icons/sqlite.svg" }, + "storybook": { "iconPath": "./icons/storybook.svg" }, + "stylelint": { "iconPath": "./icons/stylelint.svg" }, + "stylelintignore": { "iconPath": "./icons/stylelintignore.svg" }, + "stylus": { "iconPath": "./icons/stylus.svg" }, + "suo": { "iconPath": "./icons/suo.svg" }, + "svelte": { "iconPath": "./icons/svelte.svg" }, + "svelteconfig": { "iconPath": "./icons/svelteconfig.svg" }, + "svg": { "iconPath": "./icons/svg.svg" }, + "swift": { "iconPath": "./icons/swift.svg" }, + "symfony": { "iconPath": "./icons/symfony.svg" }, + "tailwind": { "iconPath": "./icons/tailwind.svg" }, + "tauri": { "iconPath": "./icons/tauri.svg" }, + "taze": { "iconPath": "./icons/taze.svg" }, + "terrafile": { "iconPath": "./icons/terrafile.svg" }, + "terraform": { "iconPath": "./icons/terraform.svg" }, + "terraformvars": { "iconPath": "./icons/terraformvars.svg" }, + "terraformversion": { "iconPath": "./icons/terraformversion.svg" }, + "testjs": { "iconPath": "./icons/testjs.svg" }, + "testts": { "iconPath": "./icons/testts.svg" }, + "tmpl": { "iconPath": "./icons/tmpl.svg" }, + "todo": { "iconPath": "./icons/todo.svg" }, + "toml": { "iconPath": "./icons/toml.svg" }, + "toolversions": { "iconPath": "./icons/toolversions.svg" }, + "tox": { "iconPath": "./icons/tox.svg" }, + "travis": { "iconPath": "./icons/travis.svg" }, + "tres": { "iconPath": "./icons/tres.svg" }, + "tscn": { "iconPath": "./icons/tscn.svg" }, + "tsconfig": { "iconPath": "./icons/tsconfig.svg" }, + "tsx": { "iconPath": "./icons/tsx.svg" }, + "turbo": { "iconPath": "./icons/turbo.svg" }, + "twig": { "iconPath": "./icons/twig.svg" }, + "txt": { "iconPath": "./icons/txt.svg" }, + "typescript": { "iconPath": "./icons/typescript.svg" }, + "typescriptdef": { "iconPath": "./icons/typescriptdef.svg" }, + "ui": { "iconPath": "./icons/ui.svg" }, + "unocss": { "iconPath": "./icons/unocss.svg" }, + "user": { "iconPath": "./icons/user.svg" }, + "v": { "iconPath": "./icons/v.svg" }, + "vanillaextract": { "iconPath": "./icons/vanillaextract.svg" }, + "vb": { "iconPath": "./icons/vb.svg" }, + "vercel": { "iconPath": "./icons/vercel.svg" }, + "version": { "iconPath": "./icons/version.svg" }, + "vhd": { "iconPath": "./icons/vhd.svg" }, + "vhdl": { "iconPath": "./icons/vhdl.svg" }, + "video": { "iconPath": "./icons/video.svg" }, + "vite": { "iconPath": "./icons/vite.svg" }, + "viteenv": { "iconPath": "./icons/viteenv.svg" }, + "vitest": { "iconPath": "./icons/vitest.svg" }, + "vmod": { "iconPath": "./icons/vmod.svg" }, + "vscode": { "iconPath": "./icons/vscode.svg" }, + "vue": { "iconPath": "./icons/vue.svg" }, + "vueconfig": { "iconPath": "./icons/vueconfig.svg" }, + "wasm": { "iconPath": "./icons/wasm.svg" }, + "webpack": { "iconPath": "./icons/webpack.svg" }, + "wgsl": { "iconPath": "./icons/wgsl.svg" }, + "windi": { "iconPath": "./icons/windi.svg" }, + "wren": { "iconPath": "./icons/wren.svg" }, + "xmake": { "iconPath": "./icons/xmake.svg" }, + "xml": { "iconPath": "./icons/xml.svg" }, + "yaml": { "iconPath": "./icons/yaml.svg" }, + "yang": { "iconPath": "./icons/yang.svg" }, + "yarn": { "iconPath": "./icons/yarn.svg" }, + "yarnerror": { "iconPath": "./icons/yarnerror.svg" }, + "yarnignore": { "iconPath": "./icons/yarnignore.svg" }, + "yarnlock": { "iconPath": "./icons/yarnlock.svg" }, + "yin": { "iconPath": "./icons/yin.svg" }, + "zig": { "iconPath": "./icons/zig.svg" }, + "zip": { "iconPath": "./icons/zip.svg" } + }, + "file": "_file", + "folder": "_folder", + "folderExpanded": "_folder_open", + "rootFolder": "_root_folder", + "rootFolderExpanded": "_root_folder_open", + "fileExtensions": { + "wma": "audio", + "wav": "audiowav", + "vox": "audio", + "tta": "audio", + "raw": "audio", + "ra": "audio", + "opus": "audio", + "ogg": "audioogg", + "oga": "audio", + "msv": "audio", + "mpc": "audio", + "mp3": "audiomp3", + "mogg": "audio", + "mmf": "audio", + "m4p": "audio", + "m4b": "audio", + "m4a": "audio", + "ivs": "audio", + "iklax": "audio", + "gsm": "audio", + "flac": "audio", + "dvf": "audio", + "dss": "audio", + "dct": "audio", + "au": "audio", + "ape": "audio", + "amr": "audio", + "aiff": "audio", + "act": "audio", + "aac": "audio", + "wmv": "video", + "webm": "video", + "vob": "video", + "svi": "video", + "rmvb": "video", + "rm": "video", + "ogv": "video", + "nsv": "video", + "mpv": "video", + "mpg": "video", + "mpeg2": "video", + "mpeg": "video", + "mpe": "video", + "mp4": "mp4", + "mp2": "video", + "mov": "mov", + "mk3d": "video", + "mkv": "video", + "m4v": "video", + "m2v": "video", + "flv": "video", + "f4v": "video", + "f4p": "video", + "f4b": "video", + "f4a": "video", + "qt": "video", + "divx": "video", + "avi": "video", + "amv": "video", + "asf": "video", + "3gp": "video", + "3g2": "video", + "ico": "imageico", + "tiff": "image", + "bmp": "image", + "png": "imagepng", + "gif": "imagegif", + "jpg": "imagejpg", + "jpeg": "imagejpg", + "7z": "zip", + "7zip": "zip", + "blade.php": "blade", + "cfg.dist": "conf", + "cjs.map": "jsmap", + "controller.js": "nestjscontroller", + "controller.ts": "nestjscontroller", + "repository.js": "nestjsrepository", + "repository.ts": "nestjsrepository", + "scheduler.js": "nestscheduler", + "scheduler.ts": "nestscheduler", + "css.js": "vanillaextract", + "css.ts": "vanillaextract", + "css.map": "cssmap", + "d.ts": "typescriptdef", + "decorator.js": "nestjsdecorator", + "decorator.ts": "nestjsdecorator", + "drawio.png": "drawio", + "drawio.svg": "drawio", + "e2e-spec.ts": "testts", + "e2e-spec.tsx": "testts", + "e2e-test.ts": "testts", + "e2e-test.tsx": "testts", + "filter.js": "nestjsfilter", + "filter.ts": "nestjsfilter", + "format.ps1xml": "powershell_format", + "gemfile.lock": "bundler", + "gradle.kts": "gradlekotlin", + "guard.js": "nestjsguard", + "guard.ts": "nestjsguard", + "jar.old": "jar", + "js.flow": "flow", + "js.map": "jsmap", + "js.snap": "jest_snapshot", + "json-ld": "jsonld", + "jsx.snap": "jest_snapshot", + "layout.htm": "layout", + "layout.html": "layout", + "marko.js": "markojs", + "mjs.map": "jsmap", + "module.ts": "nestjsmodule", + "resolver.js": "nestjsresolver", + "resolver.ts": "nestjsresolver", + "service.js": "nestjsservice", + "service.ts": "nestjsservice", + "entity.js": "nestjsentity", + "entity.ts": "nestjsentity", + "interceptor.js": "nestjsinterceptor", + "interceptor.ts": "nestjsinterceptor", + "dto.js": "nestjsdto", + "dto.ts": "nestjsdto", + "spec.js": "testjs", + "spec.jsx": "testjs", + "spec.mjs": "testjs", + "spec.ts": "testts", + "spec.tsx": "testts", + "stories.js": "storybook", + "stories.jsx": "storybook", + "stories.ts": "storybook", + "stories.tsx": "storybook", + "stories.svelte": "storybook", + "story.js": "storybook", + "story.jsx": "storybook", + "story.ts": "storybook", + "story.tsx": "storybook", + "story.svelte": "storybook", + "test.cjs": "testjs", + "test.cts": "testts", + "test.js": "testjs", + "test.jsx": "testjs", + "test.mjs": "testjs", + "test.mts": "testts", + "test.ts": "testts", + "test.tsx": "testts", + "ts.snap": "jest_snapshot", + "tsx.snap": "jest_snapshot", + "types.ps1xml": "powershell_types", + "a": "binary", + "accda": "access", + "accdb": "access", + "accdc": "access", + "accde": "access", + "accdp": "access", + "accdr": "access", + "accdt": "access", + "accdu": "access", + "ade": "access", + "adoc": "adoc", + "adp": "access", + "afdesign": "afdesign", + "affinitydesigner": "afdesign", + "affinityphoto": "afphoto", + "affinitypublisher": "afpub", + "afphoto": "afphoto", + "afpub": "afpub", + "ai": "ai", + "app": "binary", + "ascx": "aspx", + "asm": "binary", + "aspx": "aspx", + "astro": "astro", + "awk": "awk", + "bat": "bat", + "bc": "llvm", + "bcmx": "outlook", + "bicep": "bicep", + "bin": "binary", + "blade": "blade", + "bz2": "zip", + "bzip2": "zip", + "c": "c", + "cake": "cake", + "cer": "cert", + "pvk": "pvk", + "pfx": "pfx", + "spc": "spc", + "cfg": "conf", + "civet": "civet", + "cjm": "clojure", + "cl": "opencl", + "class": "class", + "cli": "cli", + "clj": "clojure", + "cljc": "clojure", + "cljs": "clojure", + "cljx": "clojure", + "cma": "binary", + "cmd": "cli", + "cmi": "binary", + "cmo": "binary", + "cmx": "binary", + "cmxa": "binary", + "comp": "opengl", + "conf": "conf", + "cpp": "cpp", + "cr": "crystal", + "crec": "lync", + "crl": "cert", + "crt": "cert", + "cs": "csharp", + "cshtml": "cshtml", + "csproj": "csproj", + "csr": "cert", + "css": "css", + "csv": "csv", + "csx": "csharp", + "d": "d", + "dart": "dartlang", + "db": "sqlite", + "db3": "sqlite", + "der": "cert", + "diff": "diff", + "dio": "drawio", + "djt": "django", + "dll": "binary", + "dmp": "log", + "doc": "word", + "docm": "word", + "docx": "word", + "dot": "word", + "dotm": "word", + "dotx": "word", + "drawio": "drawio", + "dta": "stata", + "eco": "docpad", + "edge": "edge", + "edn": "clojure", + "eex": "eex", + "ejs": "ejs", + "el": "emacs", + "elc": "emacs", + "elm": "elm", + "enc": "license", + "ensime": "ensime", + "env": "env", + "eps": "eps", + "erb": "erb", + "erl": "erlang", + "eskip": "skipper", + "ex": "elixir", + "exe": "binary", + "exp": "tcl", + "exs": "exs", + "fbx": "fbx", + "feature": "cucumber", + "fig": "figma", + "fish": "shell", + "fla": "fla", + "fods": "excel", + "frag": "opengl", + "fs": "fsharp", + "fsproj": "fsproj", + "ftl": "freemarker", + "gbl": "gbl", + "gd": "godot", + "gemfile": "bundler", + "geom": "opengl", + "glsl": "opengl", + "gmx": "gamemaker", + "go": "go", + "godot": "godot", + "gql": "graphql", + "gradle": "gradle", + "groovy": "groovy", + "gz": "zip", + "h": "cheader", + "haml": "haml", + "hbs": "handlebars", + "hcl": "hashicorp", + "hl": "binary", + "hlsl": "opengl", + "hpp": "hpp", + "hs": "haskell", + "html": "html", + "hxp": "lime", + "hxproj": "haxedevelop", + "ibc": "idrisbin", + "idr": "idris", + "ilk": "binary", + "imba": "imba", + "inc": "inc", + "include": "inc", + "info": "info", + "infopathxml": "infopath", + "ini": "conf", + "ino": "arduino", + "ipkg": "idrispkg", + "ipynb": "ipynb", + "iuml": "plantuml", + "jar": "jar", + "java": "java", + "jbuilder": "jbuilder", + "j2": "jinja", + "jinja": "jinja", + "jinja2": "jinja", + "jl": "julia", + "json5": "json5", + "jsonld": "jsonld", + "jsp": "jsp", + "jss": "jss", + "key": "key", + "kit": "codekit", + "kt": "kotlin", + "kts": "kotlins", + "laccdb": "access", + "ldb": "access", + "less": "less", + "lib": "binary", + "lidr": "idris", + "liquid": "liquid", + "ll": "llvm", + "lnk": "lnk", + "log": "log", + "ls": "livescript", + "lucee": "cf", + "m": "m", + "makefile": "makefile", + "mam": "access", + "map": "map", + "maq": "access", + "markdown": "markdown", + "master": "layout", + "mdb": "access", + "mdown": "markdown", + "mdw": "access", + "mdx": "markdownx", + "mesh": "mesh", + "mex": "matlab", + "mexn": "matlab", + "mexrs6": "matlab", + "mf": "manifest", + "mint": "mint", + "mjml": "mjml", + "ml": "ocaml", + "mli": "ocamli", + "mll": "ocamll", + "mly": "ocamly", + "mn": "matlab", + "mo": "motoko", + "msg": "outlook", + "mst": "mustache", + "mum": "matlab", + "mustache": "mustache", + "mx": "matlab", + "mx3": "matlab", + "n": "binary", + "ndll": "binary", + "neon": "neon", + "nim": "nim", + "nix": "nix", + "njk": "njk", + "njs": "nunjucks", + "njsproj": "njsproj", + "nunj": "nunjucks", + "nupkg": "nuget", + "nuspec": "nuget", + "nvim": "nvim", + "o": "binary", + "ocrec": "lync", + "ods": "excel", + "oft": "outlook", + "one": "onenote", + "onepkg": "onenote", + "onetoc": "onenote", + "onetoc2": "onenote", + "opencl": "opencl", + "org": "org", + "otf": "fontotf", + "otm": "outlook", + "ovpn": "ovpn", + "P": "prolog", + "p12": "cert", + "p7b": "cert", + "p7r": "cert", + "pa": "powerpoint", + "patch": "diff", + "pcd": "pcl", + "pck": "plsql_package", + "pdb": "binary", + "pde": "arduino", + "pdf": "pdf", + "pem": "key", + "pex": "xml", + "phar": "php", + "php1": "php", + "php2": "php", + "php3": "php", + "php4": "php", + "php5": "php", + "php6": "php", + "phps": "php", + "phpsa": "php", + "phpt": "php", + "phtml": "php", + "pkb": "plsql_package_body", + "pkg": "package", + "pkh": "plsql_package_header", + "pks": "plsql_package_spec", + "pl": "perl", + "plantuml": "plantuml", + "plist": "config", + "pm": "perlm", + "po": "poedit", + "postcss": "postcssconfig", + "pcss": "postcssconfig", + "pot": "powerpoint", + "potm": "powerpoint", + "potx": "powerpoint", + "ppa": "powerpoint", + "ppam": "powerpoint", + "pps": "powerpoint", + "ppsm": "powerpoint", + "ppsx": "powerpoint", + "ppt": "powerpoint", + "pptm": "powerpoint", + "pptx": "powerpoint", + "pri": "qt", + "prisma": "prisma", + "pro": "prolog", + "properties": "properties", + "ps1": "powershell", + "psd": "photoshop", + "psd1": "powershelldata", + "psm1": "powershellmodule", + "psmdcp": "nuget", + "pst": "outlook", + "pu": "plantuml", + "pub": "publisher", + "puml": "plantuml", + "puz": "publisher", + "pyc": "binary", + "pyd": "binary", + "pyo": "binary", + "q": "q", + "qbs": "qbs", + "qvd": "qlikview", + "qvw": "qlikview", + "rake": "rake", + "rar": "zip", + "gzip": "zip", + "razor": "razor", + "rb": "ruby", + "reg": "registry", + "rego": "rego", + "res": "rescript", + "resi": "rescriptinterface", + "rjson": "rjson", + "rproj": "rproj", + "rs": "rust", + "rsx": "rust", + "ron": "ron", + "odin": "odin", + "rt": "reacttemplate", + "rwd": "matlab", + "pas": "pascal", + "pp": "pascal", + "p": "pascal", + "lpr": "lazarusproject", + "lps": "lazarusproject", + "lpi": "lazarusproject", + "lfm": "lazarusproject", + "lrs": "lazarusproject", + "lpk": "lazarusproject", + "dpr": "delphiproject", + "dproj": "delphiproject", + "dfm": "delphiproject", + "sass": "scss", + "sc": "scala", + "scala": "scala", + "scpt": "binary", + "scptd": "binary", + "scss": "scss", + "sentinel": "sentinel", + "sig": "onenote", + "sketch": "sketch", + "slddc": "matlab", + "sldm": "powerpoint", + "sldx": "powerpoint", + "sln": "sln", + "sls": "saltstack", + "slx": "matlab", + "smv": "matlab", + "so": "binary", + "sol": "sol", + "sql": "sql", + "sqlite": "sqlite", + "sqlite3": "sqlite", + "src": "cert", + "sss": "sss", + "sst": "cert", + "stl": "cert", + "storyboard": "storyboard", + "styl": "stylus", + "suo": "suo", + "svelte": "svelte", + "svg": "svg", + "swc": "flash", + "swf": "flash", + "swift": "swift", + "tar": "zip", + "tcl": "tcl", + "templ": "tmpl", + "tesc": "opengl", + "tese": "opengl", + "tex": "latex", + "texi": "tex", + "tf": "terraform", + "tfstate": "terraform", + "tfvars": "terraformvars", + "tgz": "zip", + "tikz": "tex", + "tlg": "log", + "tmlanguage": "xml", + "tmpl": "tmpl", + "todo": "todo", + "toml": "toml", + "tpl": "smarty", + "tres": "tres", + "tscn": "tscn", + "tst": "test", + "tsx": "reactts", + "jsx": "reactjs", + "tt2": "tt", + "ttf": "fontttf", + "twig": "twig", + "txt": "txt", + "ui": "ui", + "unity": "shaderlab", + "user": "user", + "v": "v", + "vala": "vala", + "vapi": "vapi", + "vash": "vash", + "vbhtml": "vbhtml", + "vbproj": "vbproj", + "vcxproj": "vcxproj", + "vert": "opengl", + "vhd": "vhd", + "vhdl": "vhdl", + "vsix": "vscode", + "vsixmanifest": "manifest", + "wasm": "wasm", + "webp": "imagewebp", + "wgsl": "wgsl", + "wll": "word", + "woff": "fontwoff", + "eot": "fonteot", + "woff2": "fontwoff2", + "wv": "audiowv", + "wxml": "wxml", + "wxss": "wxss", + "xcodeproj": "xcode", + "xfl": "xfl", + "xib": "xib", + "xlf": "xliff", + "xliff": "xliff", + "xls": "excel", + "xlsm": "excel", + "xlsx": "excel", + "xsf": "infopath", + "xsn": "infopath", + "xtp2": "infopath", + "xvc": "matlab", + "xz": "zip", + "yy": "gamemaker2", + "yyp": "gamemaker2", + "zig": "zig", + "zip": "zip", + "zipx": "zip", + "zz": "zip", + "deflate": "zip", + "brotli": "brotli", + "kra": "krita", + "mgcb": "mgcb", + "anim": "anim", + "cy.ts": "cypressts", + "cy.js": "cypressjs", + "hx": "haxe", + "hxml": "haxeml", + "gr": "grain", + "slim": "slim", + "obj": "obj", + "mtl": "mtl", + "bicepparam": "bicepparam", + "proto": "proto", + "wren": "wren", + "docker-compose.yml": "docker", + "excalidraw": "excalidraw", + "excalidraw.json": "excalidraw", + "excalidraw.svg": "excalidraw", + "excalidraw.png": "excalidraw", + "bazel": "bazel", + "bzl": "bazel", + "bazelignore": "bazelignore", + "bazelrc": "bazel", + "http": "http", + "rkt": "racket", + "rktl": "racket", + "bru": "bruno", + "nelua": "nelua", + "mermaid": "mermaid", + "mmd": "mermaid", + "bal": "ballerina", + "hash": "hash", + "gleam": "gleam", + "lock": "lock", + "yang": "yang", + "yin": "yin", + "mdc": "cursor", + "uml": "plantuml", + "Identifier": "identifier", + "cls": "salesforce", + ".instructions.md": "instructions", + ".instructions.txt": "instructions", + ".instructions.json": "instructions", + ".instructions.yaml": "instructions", + ".instructions.yml": "instructions", + "silq": "silq", + "eraserdiagram": "eraser" + }, + "fileNames": { + "webpack.config.images.js": "webpack", + "webpack.test.conf.ts": "webpack", + "webpack.test.conf.coffee": "webpack", + "webpack.test.conf.js": "webpack", + "webpack.rules.ts": "webpack", + "webpack.rules.coffee": "webpack", + "webpack.rules.js": "webpack", + "webpack.renderer.config.ts": "webpack", + "webpack.renderer.config.coffee": "webpack", + "webpack.renderer.config.js": "webpack", + "webpack.plugins.ts": "webpack", + "webpack.plugins.coffee": "webpack", + "webpack.plugins.js": "webpack", + "webpack.mix.ts": "webpack", + "webpack.mix.coffee": "webpack", + "webpack.mix.js": "webpack", + "webpack.main.config.ts": "webpack", + "webpack.main.config.coffee": "webpack", + "webpack.main.config.js": "webpack", + "webpack.prod.conf.ts": "webpack", + "webpack.prod.conf.coffee": "webpack", + "webpack.prod.conf.js": "webpack", + "webpack.prod.ts": "webpack", + "webpack.prod.coffee": "webpack", + "webpack.prod.js": "webpack", + "webpack.dev.conf.ts": "webpack", + "webpack.dev.conf.coffee": "webpack", + "webpack.dev.conf.js": "webpack", + "webpack.dev.ts": "webpack", + "webpack.dev.coffee": "webpack", + "webpack.dev.js": "webpack", + "webpack.config.production.babel.ts": "webpack", + "webpack.config.production.babel.coffee": "webpack", + "webpack.config.production.babel.js": "webpack", + "webpack.config.prod.babel.ts": "webpack", + "webpack.config.prod.babel.coffee": "webpack", + "webpack.config.prod.babel.js": "webpack", + "webpack.config.test.babel.ts": "webpack", + "webpack.config.test.babel.coffee": "webpack", + "webpack.config.test.babel.js": "webpack", + "webpack.config.staging.babel.ts": "webpack", + "webpack.config.staging.babel.coffee": "webpack", + "webpack.config.staging.babel.js": "webpack", + "webpack.config.development.babel.ts": "webpack", + "webpack.config.development.babel.coffee": "webpack", + "webpack.config.development.babel.js": "webpack", + "webpack.config.dev.babel.ts": "webpack", + "webpack.config.dev.babel.coffee": "webpack", + "webpack.config.dev.babel.js": "webpack", + "webpack.config.common.babel.ts": "webpack", + "webpack.config.common.babel.coffee": "webpack", + "webpack.config.common.babel.js": "webpack", + "webpack.config.base.babel.ts": "webpack", + "webpack.config.base.babel.coffee": "webpack", + "webpack.config.base.babel.js": "webpack", + "webpack.config.babel.ts": "webpack", + "webpack.config.babel.coffee": "webpack", + "webpack.config.babel.js": "webpack", + "webpack.config.production.ts": "webpack", + "webpack.config.production.coffee": "webpack", + "webpack.config.production.js": "webpack", + "webpack.config.prod.ts": "webpack", + "webpack.config.prod.coffee": "webpack", + "webpack.config.prod.js": "webpack", + "webpack.config.test.ts": "webpack", + "webpack.config.test.coffee": "webpack", + "webpack.config.test.js": "webpack", + "webpack.config.staging.ts": "webpack", + "webpack.config.staging.coffee": "webpack", + "webpack.config.staging.js": "webpack", + "webpack.config.development.ts": "webpack", + "webpack.config.development.coffee": "webpack", + "webpack.config.development.js": "webpack", + "webpack.config.dev.ts": "webpack", + "webpack.config.dev.coffee": "webpack", + "webpack.config.dev.js": "webpack", + "webpack.config.common.ts": "webpack", + "webpack.config.common.coffee": "webpack", + "webpack.config.common.js": "webpack", + "webpack.config.base.ts": "webpack", + "webpack.config.base.coffee": "webpack", + "webpack.config.base.js": "webpack", + "webpack.config.ts": "webpack", + "webpack.config.coffee": "webpack", + "webpack.config.js": "webpack", + "webpack.common.ts": "webpack", + "webpack.common.coffee": "webpack", + "webpack.common.js": "webpack", + "webpack.base.conf.ts": "webpack", + "webpack.base.conf.coffee": "webpack", + "webpack.base.conf.js": "webpack", + ".angular-cli.json": "angular", + "angular-cli.json": "angular", + "angular.json": "angular", + ".angular.json": "angular", + "api-extractor.json": "api_extractor", + "api-extractor-base.json": "api_extractor", + "appveyor.yml": "appveyor", + ".appveyor.yml": "appveyor", + "aurelia.json": "aurelia", + "azure-pipelines.yml": "azure", + ".vsts-ci.yml": "azure", + ".babelrc": "babel", + ".babelignore": "babel", + ".babelrc.js": "babel", + ".babelrc.cjs": "babel", + ".babelrc.mjs": "babel", + ".babelrc.json": "babel", + "babel.config.js": "babel", + "babel.config.cjs": "babel", + "babel.config.mjs": "babel", + "babel.config.json": "babel", + "vetur.config.js": "vue", + "vetur.config.ts": "vue", + ".bzrignore": "bazaar", + ".bazelrc": "bazel", + "bazel.rc": "bazel", + "bazel.bazelrc": "bazel", + "BUILD": "bazel", + "bitbucket-pipelines.yml": "bitbucketpipeline", + ".bithoundrc": "bithound", + ".bowerrc": "bower", + "bower.json": "bower", + ".browserslistrc": "browserslist", + "browserslist": "browserslist", + "gemfile": "bundler", + "gemfile.lock": "bundler", + ".ruby-version": "bundler", + "capacitor.config.json": "capacitor", + "cargo.toml": "cargo", + "cargo.lock": "cargo", + "chefignore": "chef", + "berksfile": "chef", + "berksfile.lock": "chef", + "policyfile": "chef", + "circle.yml": "circleci", + ".cfignore": "cloudfoundry", + ".codacy.yml": "codacy", + ".codacy.yaml": "codacy", + ".codeclimate.yml": "codeclimate", + "codecov.yml": "codecov", + ".codecov.yml": "codecov", + "config.codekit": "codekit", + "config.codekit2": "codekit", + "config.codekit3": "codekit", + ".config.codekit": "codekit", + ".config.codekit2": "codekit", + ".config.codekit3": "codekit", + "coffeelint.json": "coffeelint", + ".coffeelintignore": "coffeelint", + "composer.json": "composer", + "composer.lock": "composerlock", + "conanfile.txt": "conan", + "conanfile.py": "conan", + ".condarc": "conda", + ".coveralls.yml": "coveralls", + "crowdin.yml": "crowdin", + ".csscomb.json": "csscomb", + ".csslintrc": "csslint", + ".cvsignore": "cvs", + ".boringignore": "darcs", + "dependabot.yml": "dependabot", + "dependencies.yml": "dependencies", + "devcontainer.json": "devcontainer", + "docker-compose-prod.yml": "docker", + "docker-compose.alpha.yaml": "docker", + "docker-compose.alpha.yml": "docker", + "docker-compose.beta.yaml": "docker", + "docker-compose.beta.yml": "docker", + "docker-compose.ci-build.yml": "docker", + "docker-compose.ci.yaml": "docker", + "docker-compose.ci.yml": "docker", + "docker-compose.dev.yaml": "docker", + "docker-compose.dev.yml": "docker", + "docker-compose.development.yaml": "docker", + "docker-compose.development.yml": "docker", + "docker-compose.local.yaml": "docker", + "docker-compose.local.yml": "docker", + "docker-compose.override.yaml": "docker", + "docker-compose.override.yml": "docker", + "docker-compose.prod.yaml": "docker", + "docker-compose.prod.yml": "docker", + "docker-compose.production.yaml": "docker", + "docker-compose.production.yml": "docker", + "docker-compose.stage.yaml": "docker", + "docker-compose.stage.yml": "docker", + "docker-compose.staging.yaml": "docker", + "docker-compose.staging.yml": "docker", + "docker-compose.test.yaml": "docker", + "docker-compose.test.yml": "docker", + "docker-compose.testing.yaml": "docker", + "docker-compose.testing.yml": "docker", + "docker-compose.vs.debug.yml": "docker", + "docker-compose.vs.release.yml": "docker", + "docker-compose.web.yaml": "docker", + "docker-compose.web.yml": "docker", + "docker-compose.worker.yaml": "docker", + "docker-compose.worker.yml": "docker", + "docker-compose.yaml": "docker", + "docker-compose.yml": "docker", + "Dockerfile-production": "docker", + "dockerfile.alpha": "docker", + "dockerfile.beta": "docker", + "dockerfile.ci": "docker", + "dockerfile.dev": "docker", + "dockerfile.development": "docker", + "dockerfile.local": "docker", + "dockerfile.prod": "docker", + "dockerfile.production": "docker", + "dockerfile.stage": "docker", + "dockerfile.staging": "docker", + "dockerfile.test": "docker", + "dockerfile.testing": "docker", + "dockerfile.web": "docker", + "dockerfile.worker": "docker", + "dockerfile": "docker", + "docker-compose.debug.yml": "dockerdebug", + "docker-cloud.yml": "docker", + ".dockerignore": "dockerignore", + ".doczrc": "docz", + "docz.js": "docz", + "docz.json": "docz", + ".docz.js": "docz", + ".docz.json": "docz", + "doczrc.js": "docz", + "doczrc.json": "docz", + "docz.config.js": "docz", + "docz.config.json": "docz", + ".dojorc": "dojo", + ".drone.yml": "drone", + ".drone.yml.sig": "drone", + ".dvc": "dvc", + ".editorconfig": "editorconfig", + "elm-package.json": "elm", + ".ember-cli": "ember", + "emakefile": "erlang", + ".emakerfile": "erlang", + ".eslintrc": "eslint", + ".eslintignore": "eslintignore", + ".eslintcache": "eslint", + ".eslintrc.js": "eslint", + ".eslintrc.mjs": "eslint", + ".eslintrc.cjs": "eslint", + ".eslintrc.json": "eslint", + ".eslintrc.yaml": "eslint", + ".eslintrc.yml": "eslint", + ".eslintrc.browser.json": "eslint", + ".eslintrc.base.json": "eslint", + "eslint-preset.js": "eslint", + "eslint.config.js": "eslint", + "eslint.config.cjs": "eslint", + "eslint.config.mjs": "eslint", + "eslint.config.ts": "eslint", + "_eslintrc.cjs": "eslint", + "app.json": "expo", + "app.config.js": "expo", + "app.config.json": "expo", + "app.config.json5": "expo", + "favicon.ico": "favicon", + ".firebaserc": "firebase", + "firebase.json": "firebasehosting", + "firestore.rules": "firestore", + "firestore.indexes.json": "firestore", + ".flooignore": "floobits", + ".flowconfig": "flow", + ".flutter-plugins": "flutter", + ".metadata": "flutter", + ".fossaignore": "fossa", + "ignore-glob": "fossil", + "fuse.js": "fusebox", + "gatsby-config.js": "gatsby", + "gatsby-config.ts": "gatsby", + "gatsby-node.js": "gatsby", + "gatsby-node.ts": "gatsby", + "gatsby-browser.js": "gatsby", + "gatsby-browser.ts": "gatsby", + "gatsby-ssr.js": "gatsby", + "gatsby-ssr.ts": "gatsby", + ".git-blame-ignore-revs": "git", + ".gitattributes": "git", + ".gitconfig": "git", + ".gitignore": "git", + ".gitmodules": "git", + ".gitkeep": "git", + ".mailmap": "git", + ".gitlab-ci.yml": "gitlab", + "glide.yml": "glide", + "go.sum": "go_package", + "go.mod": "go_package", + "go.work": "go_package", + ".gqlconfig": "graphql", + ".graphqlconfig": "graphql_config", + ".graphqlconfig.yml": "graphql_config", + ".graphqlconfig.yaml": "graphql_config", + "greenkeeper.json": "greenkeeper", + "gridsome.config.js": "gridsome", + "gridsome.config.ts": "gridsome", + "gridsome.server.js": "gridsome", + "gridsome.server.ts": "gridsome", + "gridsome.client.js": "gridsome", + "gridsome.client.ts": "gridsome", + "gruntfile.js": "grunt", + "gruntfile.cjs": "grunt", + "gruntfile.mjs": "grunt", + "gruntfile.coffee": "grunt", + "gruntfile.ts": "grunt", + "gruntfile.cts": "grunt", + "gruntfile.mts": "grunt", + "gruntfile.babel.js": "grunt", + "gruntfile.babel.coffee": "grunt", + "gruntfile.babel.ts": "grunt", + "gulpfile.js": "gulp", + "gulpfile.coffee": "gulp", + "gulpfile.ts": "gulp", + "gulpfile.esm.js": "gulp", + "gulpfile.esm.coffee": "gulp", + "gulpfile.esm.ts": "gulp", + "gulpfile.babel.js": "gulp", + "gulpfile.babel.coffee": "gulp", + "gulpfile.babel.ts": "gulp", + "haxelib.json": "haxe", + "checkstyle.json": "haxecheckstyle", + ".p4ignore": "helix", + ".htmlhintrc": "htmlhint", + ".huskyrc": "husky", + "husky.config.js": "husky", + ".huskyrc.js": "husky", + ".huskyrc.json": "husky", + ".huskyrc.yaml": "husky", + ".huskyrc.yml": "husky", + "ionic.project": "ionic", + "ionic.config.json": "ionic", + "jakefile": "jake", + "jakefile.js": "jake", + "jest.config.json": "jest", + "jest.json": "jest", + ".jestrc": "jest", + ".jestrc.js": "jest", + ".jestrc.json": "jest", + "jest.config.js": "jest", + "jest.config.cjs": "jest", + "jest.config.mjs": "jest", + "jest.config.babel.js": "jest", + "jest.config.babel.cjs": "jest", + "jest.config.babel.mjs": "jest", + "jest.preset.js": "jest", + "jest.preset.ts": "jest", + "jest.preset.cjs": "jest", + "jest.preset.mjs": "jest", + ".jpmignore": "jpm", + ".jsbeautifyrc": "jsbeautify", + "jsbeautifyrc": "jsbeautify", + ".jsbeautify": "jsbeautify", + "jsbeautify": "jsbeautify", + "jsconfig.json": "jsconfig", + ".jscpd.json": "jscpd", + "jscpd-report.xml": "jscpd", + "jscpd-report.json": "jscpd", + "jscpd-report.html": "jscpd", + ".jshintrc": "jshint", + ".jshintignore": "jshint", + "karma.conf.js": "karma", + "karma.conf.coffee": "karma", + "karma.conf.ts": "karma", + ".kitchen.yml": "kitchenci", + "kitchen.yml": "kitchenci", + ".kiteignore": "kite", + "layout.html": "layout", + "layout.htm": "layout", + "lerna.json": "lerna", + "license": "license", + "licence": "license", + "license.md": "license", + "license.txt": "license", + "licence.md": "license", + "licence.txt": "license", + ".lighthouserc.js": "lighthouse", + ".lighthouserc.json": "lighthouse", + ".lighthouserc.yaml": "lighthouse", + ".lighthouserc.yml": "lighthouse", + "include.xml": "lime", + ".lintstagedrc": "lintstagedrc", + "lint-staged.config.js": "lintstagedrc", + ".lintstagedrc.js": "lintstagedrc", + ".lintstagedrc.json": "lintstagedrc", + ".lintstagedrc.yaml": "lintstagedrc", + ".lintstagedrc.yml": "lintstagedrc", + "manifest": "manifest", + "manifest.bak": "manifest", + "manifest.json": "manifest", + "manifest.skip": "manifes", + ".markdownlint.json": "markdownlint", + "maven.config": "maven", + "pom.xml": "maven", + "extensions.xml": "maven", + "settings.xml": "maven", + "pom.properties": "maven", + ".hgignore": "mercurial", + "mocha.opts": "mocha", + ".mocharc.js": "mocha", + ".mocharc.json": "mocha", + ".mocharc.jsonc": "mocha", + ".mocharc.yaml": "mocha", + ".mocharc.yml": "mocha", + "modernizr": "modernizr", + "modernizr.js": "modernizr", + "modernizrrc.js": "modernizr", + ".modernizr.js": "modernizr", + ".modernizrrc.js": "modernizr", + "moleculer.config.js": "moleculer", + "moleculer.config.json": "moleculer", + "moleculer.config.ts": "moleculer", + ".mtn-ignore": "monotone", + ".nest-cli.json": "nestjs", + "nest-cli.json": "nestjs", + "nestconfig.json": "nestjs", + ".nestconfig.json": "nestjs", + "netlify.toml": "netlify", + "_redirects": "netlify", + "ng-tailwind.js": "ng_tailwind", + "nginx.conf": "nginx", + "build.ninja": "ninja", + ".node-version": "node", + ".node_repl_history": "node", + ".node-gyp": "node", + "node_modules": "node", + "node_modules.json": "node", + "node-inspect.json": "node", + "node-inspect.js": "node", + "node-inspect.mjs": "node", + "node-inspect.cjs": "node", + "node-inspect.ts": "node", + "node-inspect.config.js": "node", + "node-inspect.config.ts": "node", + "node-inspect.config.cjs": "node", + "node-inspect.config.mjs": "node", + "node-inspect.config.json": "node", + "node-inspect.config.yaml": "node", + "node-inspect.config.yml": "node", + "node-inspectrc": "node", + ".node-inspectrc": "node", + ".node-inspectrc.json": "node", + ".node-inspectrc.yaml": "node", + ".node-inspectrc.yml": "node", + ".node-inspectrc.js": "node", + ".node-inspectrc.ts": "node", + ".node-inspectrc.cjs": "node", + ".node-inspectrc.mjs": "node", + "nodemon.json": "nodemon", + ".npmignore": "npm", + ".npmrc": "npm", + "package.json": "npm", + "package-lock.json": "npmlock", + "npm-shrinkwrap.json": "npm", + ".nsrirc": "nsri", + ".nsriignore": "nsri", + "nsri.config.js": "nsri", + ".nsrirc.js": "nsri", + ".nsrirc.json": "nsri", + ".nsrirc.yaml": "nsri", + ".nsrirc.yml": "nsri", + ".integrity.json": "nsri-integrity", + "nuxt.config.js": "nuxt", + "nuxt.config.ts": "nuxt", + ".nycrc": "nyc", + ".nycrc.json": "nyc", + ".merlin": "ocaml", + "paket.dependencies": "paket", + "paket.lock": "paket", + "paket.references": "paket", + "paket.template": "paket", + "paket.local": "paket", + ".php_cs": "phpcsfixer", + ".php_cs.dist": "phpcsfixer", + "phpunit": "phpunit", + "phpunit.xml": "phpunit", + "phpunit.xml.dist": "phpunit", + ".phraseapp.yml": "phraseapp", + "pipfile": "pip", + "pipfile.lock": "pip", + "platformio.ini": "platformio", + "pnpmfile.js": "pnpm", + "pnpm-workspace.yaml": "pnpm", + ".postcssrc": "postcssconfig", + ".postcssrc.json": "postcssconfig", + ".postcssrc.yml": "postcssconfig", + ".postcssrc.js": "postcssconfig", + ".postcssrc.cjs": "postcssconfig", + ".postcssrc.mjs": "postcssconfig", + ".postcssrc.ts": "postcssconfig", + ".postcssrc.cts": "postcssconfig", + ".postcssrc.mts": "postcssconfig", + "postcss.config.js": "postcssconfig", + "postcss.config.cjs": "postcssconfig", + "postcss.config.mjs": "postcssconfig", + "postcss.config.ts": "postcssconfig", + "postcss.config.cts": "postcssconfig", + "postcss.config.mts": "postcssconfig", + ".pre-commit-config.yaml": "precommit", + ".pre-commit-hooks.yaml": "precommit", + ".prettierrc": "prettier", + ".prettierignore": "prettierignore", + "prettier.config.js": "prettier", + "prettier.config.cjs": "prettier", + "prettier.config.mjs": "prettier", + "prettier.config.ts": "prettier", + "prettier.config.coffee": "prettier", + ".prettierrc.js": "prettier", + ".prettierrc.json": "prettier", + ".prettierrc.yml": "prettier", + ".prettierrc.yaml": "prettier", + "procfile": "procfile", + "protractor.conf.js": "protractor", + "protractor.conf.coffee": "protractor", + "protractor.conf.ts": "protractor", + ".jade-lintrc": "pug", + ".pug-lintrc": "pug", + ".jade-lint.json": "pug", + ".pug-lintrc.js": "pug", + ".pug-lintrc.json": "pug", + ".pyup": "pyup", + ".pyup.yml": "pyup", + "qmldir": "qmldir", + "quasar.conf.js": "quasar", + "rakefile": "rake", + "razzle.config.js": "razzle", + "readme.md": "readme", + "readme.txt": "readme", + ".rehyperc": "rehype", + ".rehypeignore": "rehype", + ".rehyperc.js": "rehype", + ".rehyperc.json": "rehype", + ".rehyperc.yml": "rehype", + ".rehyperc.yaml": "rehype", + ".remarkrc": "remark", + ".remarkignore": "remark", + ".remarkrc.js": "remark", + ".remarkrc.json": "remark", + ".remarkrc.yml": "remark", + ".remarkrc.yaml": "remark", + ".renovaterc": "renovate", + "renovate.json": "renovate", + ".renovaterc.json": "renovate", + ".retextrc": "retext", + ".retextignore": "retext", + ".retextrc.js": "retext", + ".retextrc.json": "retext", + ".retextrc.yml": "retext", + ".retextrc.yaml": "retext", + "robots.txt": "robots", + "rollup.config.js": "rollup", + "rollup.config.mjs": "rollup", + "rollup.config.coffee": "rollup", + "rollup.config.ts": "rollup", + "rollup.config.common.js": "rollup", + "rollup.config.common.mjs": "rollup", + "rollup.config.common.coffee": "rollup", + "rollup.config.common.ts": "rollup", + "rollup.config.dev.js": "rollup", + "rollup.config.dev.mjs": "rollup", + "rollup.config.dev.coffee": "rollup", + "rollup.config.dev.ts": "rollup", + "rollup.config.prod.js": "rollup", + "rollup.config.prod.mjs": "rollup", + "rollup.config.prod.coffee": "rollup", + "rollup.config.prod.ts": "rollup", + ".rspec": "rspec", + ".rubocop.yml": "rubocop", + ".rubocop_todo.yml": "rubocop", + "rust-toolchain": "rust_toolchain", + ".sentryclirc": "sentry", + "serverless.yml": "serverless", + "snapcraft.yaml": "snapcraft", + ".snyk": "snyk", + ".solidarity": "solidarity", + ".solidarity.json": "solidarity", + ".stylelintrc": "stylelint", + ".stylelintignore": "stylelintignore", + ".stylelintcache": "stylelint", + "stylelint.config.js": "stylelint", + "stylelint.config.cjs": "stylelint", + "stylelint.config.mjs": "stylelint", + "stylelint.config.json": "stylelint", + "stylelint.config.yaml": "stylelint", + "stylelint.config.yml": "stylelint", + "stylelint.config.ts": "stylelint", + ".stylelintrc.js": "stylelint", + ".stylelintrc.json": "stylelint", + ".stylelintrc.yaml": "stylelint", + ".stylelintrc.yml": "stylelint", + ".stylelintrc.ts": "stylelint", + ".stylelintrc.cjs": "stylelint", + ".stylelintrc.mjs": "stylelint", + ".stylish-haskell.yaml": "stylish_haskell", + ".svnignore": "subversion", + "package.pins": "swift", + "symfony.lock": "symfony", + "windi.config.ts": "windi", + "windi.config.js": "windi", + "tailwind.js": "tailwind", + "tailwind.mjs": "tailwind", + "tailwind.cjs": "tailwind", + "tailwind.coffee": "tailwind", + "tailwind.ts": "tailwind", + "tailwind.cts": "tailwind", + "tailwind.mts": "tailwind", + "tailwind.config.mjs": "tailwind", + "tailwind.config.cjs": "tailwind", + "tailwind.config.js": "tailwind", + "tailwind.config.coffee": "tailwind", + "tailwind.config.ts": "tailwind", + "tailwind.config.cts": "tailwind", + "tailwind.config.mts": "tailwind", + ".testcaferc.json": "testcafe", + ".tfignore": "tfs", + "tox.ini": "tox", + ".travis.yml": "travis", + "tsconfig.json": "tsconfig", + "tsconfig.app.json": "tsconfig", + "tsconfig.base.json": "tsconfig", + "tsconfig.common.json": "tsconfig", + "tsconfig.dev.json": "tsconfig", + "tsconfig.development.json": "tsconfig", + "tsconfig.e2e.json": "tsconfig", + "tsconfig.prod.json": "tsconfig", + "tsconfig.production.json": "tsconfig", + "tsconfig.server.json": "tsconfig", + "tsconfig.spec.json": "tsconfig", + "tsconfig.staging.json": "tsconfig", + "tsconfig.test.json": "tsconfig", + "tsconfig.tsd.json": "tsconfig", + "tsconfig.node.json": "tsconfig", + "tsconfig.lib.json": "tsconfig", + "tsconfig.eslint.json": "tsconfig", + "tsconfig.storybook.json": "tsconfig", + "tsconfig.tsbuildinfo": "tsconfig", + "tslint.json": "tslint", + "tslint.yaml": "tslint", + "tslint.yml": "tslint", + ".unibeautifyrc": "unibeautify", + "unibeautify.config.js": "unibeautify", + ".unibeautifyrc.js": "unibeautify", + ".unibeautifyrc.json": "unibeautify", + ".unibeautifyrc.yaml": "unibeautify", + ".unibeautifyrc.yml": "unibeautify", + "vagrantfile": "vagrant", + ".vimrc": "vim", + ".gvimrc": "vim", + ".vscodeignore": "vscode", + "tasks.json": "vscode", + "vscodeignore.json": "vscode", + ".vuerc": "vueconfig", + "vue.config.js": "vueconfig", + "vue.config.ts": "vueconfig", + "wallaby.json": "wallaby", + "wallaby.js": "wallaby", + "wallaby.ts": "wallaby", + "wallaby.coffee": "wallaby", + "wallaby.conf.json": "wallaby", + "wallaby.conf.js": "wallaby", + "wallaby.conf.ts": "wallaby", + "wallaby.conf.coffee": "wallaby", + ".wallaby.json": "wallaby", + ".wallaby.js": "wallaby", + ".wallaby.ts": "wallaby", + ".wallaby.coffee": "wallaby", + ".wallaby.conf.json": "wallaby", + ".wallaby.conf.js": "wallaby", + ".wallaby.conf.ts": "wallaby", + ".wallaby.conf.coffee": "wallaby", + ".watchmanconfig": "watchmanconfig", + "wercker.yml": "wercker", + "wpml-config.xml": "wpml", + ".yamllint": "yamllint", + ".yaspellerrc": "yandex", + ".yaspeller.json": "yandex", + "yarn.lock": "yarnlock", + ".yarnrc": "yarn", + ".yarn.installed": "yarn", + ".yarnclean": "yarn", + ".yarn-integrity": "yarn", + ".yarn-metadata.json": "yarn", + ".yarnignore": "yarnignore", + ".yarnrc.yml": "yarn", + ".yarnrc.yaml": "yarn", + ".yarnrc.json": "yarn", + ".yarnrc.json5": "yarn", + ".yarnrc.cjs": "yarn", + ".yarnrc.js": "yarn", + ".yarnrc.lock": "yarn", + ".yarnrc.txt": "yarn", + "yarn-error.log": "yarnerror", + ".yo-rc.json": "yeoman", + "now.json": "vercel", + ".nowignore": "vercel", + "vercel.json": "vercel", + ".vercel": "vercel", + ".vercelignore": "vercel", + "vite.config.js": "vite", + "vite.config.mjs": "vite", + "vite.config.cjs": "vite", + "vite.config.ts": "vite", + "vite.config.mts": "vite", + "vite.config.cts": "vite", + ".nvmrc": "nvm", + "example.env": "env", + ".env.staging": "env", + ".env.sample": "env", + ".env.preprod": "env", + ".env.prod": "env", + ".env.production": "env", + ".env.local": "env", + ".env.dev": "env", + ".env.dev.local": "env", + ".env.dev.prod": "env", + ".env.dev.preprod": "env", + ".env.dev.production": "env", + ".env.dev.staging": "env", + ".env.development": "env", + ".env.example": "env", + ".env.test": "env", + ".env.dist": "env", + ".env.default": "env", + ".jinja": "jinja", + "jenkins.yaml": "jenkins", + "jenkins.yml": "jenkins", + ".compodocrc": "compodoc", + ".compodocrc.json": "compodoc", + ".compodocrc.yaml": "compodoc", + ".compodocrc.yml": "compodoc", + "bsconfig.json": "bsconfig", + ".clang-format": "llvm", + ".clang-tidy": "llvm", + ".clangd": "llvm", + ".parcelrc": "parcel", + "dune": "dune", + "dune-project": "duneproject", + ".adonisrc.json": "adonis", + "astro.config.js": "astroconfig", + "astro.config.cjs": "astroconfig", + "astro.config.mjs": "astroconfig", + "astro.config.ts": "astroconfig", + "astro.config.cts": "astroconfig", + "astro.config.mts": "astroconfig", + "svelte.config.js": "svelteconfig", + "svelte.config.ts": "svelteconfig", + ".tool-versions": "toolversions", + "CMakeSettings.json": "cmake", + "CMakeLists.txt": "cmake", + "toolchain.cmake": "cmake", + ".cmake": "cmake", + "Cargo.toml": "cargo", + "Cargo.lock": "cargolock", + "pnpm-lock.yaml": "pnpmlock", + "tauri.conf.json": "tauri", + "tauri.conf.json5": "tauri", + "tauri.linux.conf.json": "tauri", + "tauri.windows.conf.json": "tauri", + "tauri.macos.conf.json": "tauri", + "next.config.js": "nextconfig", + "next.config.mjs": "nextconfig", + "next.config.ts": "nextconfig", + "nextron.config.js": "nextron", + "nextron.config.ts": "nextron", + "poetry.toml": "poetry", + "poetry.lock": "poetrylock", + "pyproject.toml": "pyproject", + "rustfmt.toml": "rustfmt", + ".rustfmt.toml": "rustfmt", + "cucumber.yml": "cucumber", + "cucumber.yaml": "cucumber", + "cucumber.js": "cucumber", + "cucumber.ts": "cucumber", + "cucumber.cjs": "cucumber", + "cucumber.mjs": "cucumber", + "cucumber.json": "cucumber", + "flake.lock": "flakelock", + "ace": "ace", + "ace-manifest.json": "acemanifest", + "knexfile.js": "knex", + "knexfile.ts": "knex", + "launch.json": "launch", + "redis.conf": "redis", + "sequelize.js": "sequelize", + "sequelize.ts": "sequelize", + "sequelize.cjs": "sequelize", + ".sequelizerc": "sequelize", + ".sequelizerc.js": "sequelize", + ".sequelizerc.json": "sequelize", + "cypress.json": "cypress", + "cypress.env.json": "cypress", + "cypress.config.js": "cypress", + "cypress.config.ts": "cypress", + "cypress.config.cjs": "cypress", + "playwright.config.ts": "playright", + "playwright.config.js": "playright", + "playwright.config.cjs": "playright", + "vitest.config.ts": "vitest", + "vitest.config.cts": "vitest", + "vitest.config.mts": "vitest", + "vitest.config.js": "vitest", + "vitest.config.cjs": "vitest", + "vitest.config.mjs": "vitest", + "vitest.workspace.ts": "vitest", + "vitest.workspace.cts": "vitest", + "vitest.workspace.mts": "vitest", + "vitest.workspace.js": "vitest", + "vitest.workspace.cjs": "vitest", + "vitest.workspace.mjs": "vitest", + "vite-env.d.ts": "viteenv", + "vite-env.d.js": "viteenv", + "pubspec.lock": "flutterlock", + "pubspec.yaml": "flutter", + ".packages": "flutterpackage", + ".htaccess": "htaccess", + "nx.json": "nx", + "project.json": "nx", + "nx.instructions.md": "nx", + "nx.jsonc": "nx", + "v.mod": "vmod", + "quasar.config.js": "quasar", + "quasar.config.ts": "quasar", + "quasar.config.cjs": "quasar", + "quasar.config.mjs": "quasar", + "quarkus.properties": "quarkus", + "theme.properties": "ui", + "gradlew": "gradle", + "gradle-wrapper.properties": "gradle", + "gradlew.bat": "gradlebat", + "makefile.win": "makefile", + "makefile": "makefile", + "make": "makefile", + "version": "version", + "server": "sql", + "migrate": "sql", + ".commitlintrc": "commitlint", + ".commitlintrc.json": "commitlint", + ".commitlintrc.yaml": "commitlint", + ".commitlintrc.yml": "commitlint", + ".commitlintrc.js": "commitlint", + ".commitlintrc.cjs": "commitlint", + ".commitlintrc.ts": "commitlint", + ".commitlintrc.cts": "commitlint", + "commitlint.config.js": "commitlint", + "commitlint.config.cjs": "commitlint", + "commitlint.config.ts": "commitlint", + "commitlint.config.cts": "commitlint", + ".terraform-version": "terraformversion", + "TerraFile": "terrafile", + "tfstate.backup": "terraform", + ".code-workspace": "codeworkspace", + "hardhat.config.js": "hardhat", + "hardhat.config.ts": "hardhat", + "hardhat.config.cts": "hardhat", + "hardhat.config.cjs": "hardhat", + "hardhat.config.mjs": "hardhat", + "taze.config.js": "taze", + "taze.config.ts": "taze", + "taze.config.cjs": "taze", + "taze.config.mjs": "taze", + ".tazerc.json": "taze", + "turbo.json": "turbo", + "turbo.jsonc": "turbo", + "uno.config.ts": "unocss", + "uno.config.js": "unocss", + "uno.config.mjs": "unocss", + "uno.config.mts": "unocss", + "unocss.config.ts": "unocss", + "unocss.config.js": "unocss", + "unocss.config.mjs": "unocss", + "unocss.config.mts": "unocss", + "atomizer.config.js": "atomizer", + "atomizer.config.cjs": "atomizer", + "atomizer.config.mjs": "atomizer", + "atomizer.config.ts": "atomizer", + "esbuild.js": "esbuild", + "esbuild.mjs": "esbuild", + "esbuild.cjs": "esbuild", + "esbuild.ts": "esbuild", + "mix.exs": "mix", + "mix.lock": "mixlock", + ".DS_Store": "dsstore", + "remix.config.js": "remix", + "remix.config.cjs": "remix", + "remix.config.mjs": "remix", + "remix.config.ts": "remix", + "xmake.lua": "xmake", + ".sailsrc": "sails", + "farm.config.ts": "farm", + "farm.config.js": "farm", + "bunfig.toml": "bun", + ".bunfig.toml": "bun", + "bun.lockb": "bunlock", + "bun.lock": "bunlock", + ".air.toml": "air", + "rome.json": "rome", + "biome.json": "biome", + "bicepconfig.json": "bicepconfig", + "drizzle.config.ts": "drizzle", + "drizzle.config.js": "drizzle", + "drizzle.config.json": "drizzle", + "panda.config.ts": "panda", + "panda.config.js": "panda", + "panda.config.json": "panda", + "panda.config.cjs": "panda", + "panda.config.mjs": "panda", + "panda.config.cts": "panda", + "panda.config.mts": "panda", + ".buckconfig": "buck", + "Ballerina.toml": "ballerinaconfig", + "knip.json": "knip", + "knip.jsonc": "knip", + ".knip.json": "knip", + ".knip.jsonc": "knip", + "knip.ts": "knip", + "knip.js": "knip", + "knip.config.ts": "knip", + "knip.config.js": "knip", + "todo.md": "todo", + ".todo.md": "todo", + "todo.txt": "todo", + ".todo.txt": "todo", + "todo": "todo", + "mkdocs.yml": "mkdocs", + "mkdocs.yaml": "mkdocs", + "gleam.toml": "gleamconfig", + ".oxlintrc.json": "oxlint", + "oxlint.json": "oxlint", + "oxlint.config.js": "oxlint", + "oxlint.config.ts": "oxlint", + "oxlint.config.cjs": "oxlint", + "oxlint.config.mjs": "oxlint", + "oxlint.config.cts": "oxlint", + "oxlint.config.mts": "oxlint", + ".cursorrules": "cursor", + "plopfile.js": "plop", + "plopfile.cjs": "plop", + "plopfile.mjs": "plop", + "plopfile.ts": "plop", + "plopfile.cts": "plop", + "config.mockoon.json": "mockoon", + "mockoon.json": "mockoon", + "mockoon.yaml": "mockoon", + "mockoon.yml": "mockoon", + "mockoon.env": "mockoon", + "mockoon.env.json": "mockoon", + "mockoon.env.yaml": "mockoon", + "mockoon.env.yml": "mockoon", + "mockoon.env.js": "mockoon", + "mockoon.env.ts": "mockoon", + "mockoon.env.cjs": "mockoon", + "mockoon.env.mjs": "mockoon", + "mockoon.env.cts": "mockoon", + "mockoon.env.mts": "mockoon", + "copilot-instructions.md": "copilot", + ".copilot-instructions": "copilot", + ".instructions": "instructions", + "instructions.md": "instructions", + "instructions.txt": "instructions", + "instructions": "instructions", + "instructions.json": "instructions", + "instructions.yaml": "instructions", + "instructions.yml": "instructions", + ".keep": "keep", + ".keepignore": "keep", + "CLAUDE.md": "claude", + "claude.md": "claude", + "claude.txt": "claude", + "claude": "claude", + "claude.json": "claude", + "claude.yaml": "claude", + ".claude_code_config": "claude", + ".claude": "claude", + "claude.config.js": "claude", + ".claude.yaml": "claude", + ".clauderc": "claude", + "claude-instructions.md": "claude", + ".claude-code": "claude", + "claude-code.config": "claude" + }, + "languageIds": { + "actionscript": "actionscript", + "ada": "ada", + "advpl": "advpl", + "affectscript": "affectscript", + "al": "al", + "ansible": "ansible", + "antlr": "antlr", + "anyscript": "anyscript", + "apacheconf": "apache", + "apex": "apex", + "apiblueprint": "apib", + "apl": "apl", + "applescript": "applescript", + "asciidoc": "asciidoc", + "asp": "asp", + "asp (html)": "asp", + "arm": "assembly", + "asm": "assembly", + "ats": "ats", + "ahk": "autohotkey", + "autoit": "autoit", + "avro": "avro", + "azcli": "azure", + "azure-pipelines": "azurepipelines", + "ballerina": "ballerina", + "bat": "bat", + "bats": "bats", + "bazel": "bazel", + "befunge": "befunge", + "befunge98": "befunge", + "biml": "biml", + "blade": "blade", + "laravel-blade": "blade", + "bolt": "bolt", + "bosque": "bosque", + "c": "c", + "c-al": "c_al", + "cabal": "cabal", + "caddyfile": "caddy", + "cddl": "cddl", + "ceylon": "ceylon", + "cfml": "cf", + "lang-cfml": "cf", + "cfc": "cfc", + "cfmhtml": "cfm", + "cookbook": "chef_cookbook", + "clojure": "clojure", + "clojurescript": "clojurescript", + "manifest-yaml": "cloudfoundry", + "cmake": "cmake", + "cmake-cache": "cmake", + "cobol": "cobol", + "coffeescript": "coffeescript", + "properties": "properties", + "dotenv": "config", + "confluence": "confluence", + "cpp": "cpp", + "crystal": "crystal", + "csharp": "csharp", + "css": "css", + "feature": "cucumber", + "cuda": "cuda", + "cython": "cython", + "dal": "dal", + "dart": "dartlang", + "pascal": "pascal", + "objectpascal": "pascal", + "diff": "diff", + "django-html": "django", + "django-txt": "django", + "d": "dlang", + "dscript": "dlang", + "dml": "dlang", + "diet": "dlang", + "dockerfile": "docker", + "ignore": "docker", + "dotjs": "dotjs", + "doxygen": "doxygen", + "drools": "drools", + "dustjs": "dustjs", + "dylan": "dylan", + "dylan-lid": "dylan", + "edge": "edge", + "eex": "eex", + "html-eex": "eex", + "es": "elastic", + "elixir": "elixir", + "elm": "elm", + "erb": "erb", + "erlang": "erlang", + "falcon": "falcon", + "fortran": "fortran", + "fortran-modern": "fortran", + "FortranFreeForm": "fortran", + "fortran_fixed-form": "fortran", + "ftl": "freemarker", + "fsharp": "fsharp", + "fthtml": "fthtml", + "galen": "galen", + "gml-gms": "gamemaker", + "gml-gms2": "gamemaker2", + "gml-gm81": "gamemaker81", + "gcode": "gcode", + "genstat": "genstat", + "git-commit": "git", + "git-rebase": "git", + "glsl": "glsl", + "glyphs": "glyphs", + "gnuplot": "gnuplot", + "go": "go", + "golang": "go", + "go-sum": "go", + "go-mod": "go", + "go-xml": "go", + "gdscript": "godot", + "graphql": "graphql", + "dot": "graphviz", + "groovy": "groovy", + "haml": "haml", + "handlebars": "handlebars", + "harbour": "harbour", + "haskell": "haskell", + "literate haskell": "haskell", + "haxe": "haxe", + "hxml": "haxe", + "Haxe AST dump": "haxe", + "helm": "helm", + "hjson": "hjson", + "hlsl": "opengl", + "home-assistant": "homeassistant", + "hosts": "host", + "html": "html", + "http": "http", + "hunspell.aff": "hunspell", + "hunspell.dic": "hunspell", + "hy": "hy", + "icl": "icl", + "imba": "imba", + "4GL": "informix", + "ini": "conf", + "ink": "ink", + "innosetup": "innosetup", + "io": "io", + "iodine": "iodine", + "janet": "janet", + "java": "java", + "raku": "raku", + "jekyll": "jekyll", + "jenkins": "jenkins", + "declarative": "jenkins", + "jenkinsfile": "jenkins", + "jinja": "jinja", + "code-referencing": "vscode", + "search-result": "vscode", + "type": "vscode", + "javascript": "js", + "json": "json", + "jsonl": "json", + "json-tmlanguage": "json", + "jsonc": "json", + "json5": "json5", + "jsonnet": "jsonnet", + "julia": "julia", + "juliamarkdown": "julia", + "kivy": "kivy", + "kos": "kos", + "kotlin": "kotlin", + "kusto": "kusto", + "latino": "latino", + "less": "less", + "lex": "lex", + "lisp": "lisp", + "lolcode": "lolcode", + "code-text-binary": "binary", + "lsl": "lsl", + "lua": "lua", + "makefile": "makefile", + "markdown": "markdown", + "marko": "marko", + "matlab": "matlab", + "maxscript": "maxscript", + "mel": "maya", + "mediawiki": "mediawiki", + "meson": "meson", + "mjml": "mjml", + "mlang": "mlang", + "powerquerymlanguage": "mlang", + "mojolicious": "mojolicious", + "mongo": "mongo", + "mson": "mson", + "nearley": "nearly", + "nim": "nim", + "nimble": "nimble", + "nix": "nix", + "nsis": "nsi", + "nfl": "nsi", + "nsl": "nsi", + "bridlensis": "nsi", + "nunjucks": "nunjucks", + "objective-c": "c", + "objective-cpp": "cpp", + "ocaml": "ocaml", + "ocamllex": "ocaml", + "menhir": "ocaml", + "openhab": "openHAB", + "pddl": "pddl", + "happenings": "pddl_happenings", + "plan": "pddl_plan", + "phoenix-heex": "eex", + "perl": "perl", + "perl6": "perl6", + "pgsql": "pgsql", + "php": "php", + "pine": "pine", + "pinescript": "pine", + "pip-requirements": "python", + "platformio-debug.disassembly": "platformio", + "platformio-debug.memoryview": "platformio", + "platformio-debug.asm": "platformio", + "plsql": "plsql", + "oracle": "plsql", + "polymer": "polymer", + "pony": "pony", + "postcss": "postcss", + "powershell": "powershell", + "prisma": "prisma", + "pde": "processinglang", + "abl": "progress", + "prolog": "prolog", + "prometheus": "prometheus", + "proto3": "protobuf", + "proto": "protobuf", + "jade": "pug", + "pug": "pug", + "puppet": "puppet", + "purescript": "purescript", + "pyret": "pyret", + "python": "python", + "qlik": "qlikview", + "qml": "qml", + "qsharp": "qsharp", + "r": "r", + "racket": "racket", + "raml": "raml", + "razor": "razor", + "aspnetcorerazor": "razor", + "javascriptreact": "reactjs", + "typescriptreact": "reactts", + "reason": "reason", + "red": "red", + "restructuredtext": "restructuredtext", + "rexx": "rexx", + "riot": "riot", + "rmd": "rmd", + "mdx": "markdownx", + "robot": "robotframework", + "ruby": "ruby", + "rust": "rust", + "san": "san", + "SAS": "sas", + "sbt": "sbt", + "scala": "scala", + "scilab": "scilab", + "vbscript": "script", + "scss": "scss", + "sdl": "sdlang", + "shaderlab": "shaderlab", + "shellscript": "shell", + "silverstripe": "silverstripe", + "eskip": "skipper", + "slang": "slang", + "slice": "slice", + "slim": "slim", + "smarty": "smarty", + "snort": "snort", + "solidity": "solidity", + "snippets": "vscode", + "sqf": "sqf", + "sql": "sql", + "squirrel": "squirrel", + "stan": "stan", + "stata": "stata", + "stencil": "stencil", + "stencil-html": "stencil", + "stylable": "stylable", + "source.css.styled": "styled", + "stylus": "stylus", + "svelte": "svelte", + "Swagger": "swagger", + "swagger": "swagger", + "swift": "swift", + "swig": "swig", + "cuda-cpp": "nvidia", + "systemd-unit-file": "systemd", + "systemverilog": "systemverilog", + "t4": "t4tt", + "tera": "tera", + "terraform": "terraform", + "tex": "latex", + "log": "log", + "dockercompose": "docker", + "latex": "latex", + "vue-directives": "vue", + "vue-injection-markdown": "vue", + "vue-interpolations": "vue", + "vue-sfc-style-variable-injection": "vue", + "bibtex": "latex", + "doctex": "tex", + "plaintext": "text", + "textile": "textile", + "toml": "toml", + "tt": "tt", + "ttcn": "ttcn", + "twig": "twig", + "typescript": "typescript", + "typoscript": "typo3", + "vb": "vb", + "vba": "vba", + "velocity": "velocity", + "verilog": "verilog", + "vhdl": "vhdl", + "viml": "vim", + "v": "vlang", + "volt": "volt", + "vue": "vue", + "wasm": "wasm", + "wat": "wasm", + "wenyan": "wenyan", + "wolfram": "wolfram", + "wurstlang": "wurst", + "wurst": "wurst", + "xmake": "xmake", + "xml": "xml", + "xquery": "xquery", + "xsl": "xml", + "yacc": "yacc", + "yaml": "yaml", + "yaml-tmlanguage": "yaml", + "yang": "yang", + "zig": "zig", + "vitest-snapshot": "vitest", + "instructions": "instructions", + "prompt": "prompt" + }, + "light": { + "file": "_file_light", + "folder": "_folder_light", + "folderExpanded": "_folder_light_open", + "rootFolder": "_root_folder_light", + "rootFolderExpanded": "_root_folder_light_open", + "fileExtensions": { + "wma": "audio", + "wav": "audiowav", + "vox": "audio", + "tta": "audio", + "raw": "audio", + "ra": "audio", + "opus": "audio", + "ogg": "audioogg", + "oga": "audio", + "msv": "audio", + "mpc": "audio", + "mp3": "audiomp3", + "mogg": "audio", + "mmf": "audio", + "m4p": "audio", + "m4b": "audio", + "m4a": "audio", + "ivs": "audio", + "iklax": "audio", + "gsm": "audio", + "flac": "audio", + "dvf": "audio", + "dss": "audio", + "dct": "audio", + "au": "audio", + "ape": "audio", + "amr": "audio", + "aiff": "audio", + "act": "audio", + "aac": "audio", + "wmv": "video", + "webm": "video", + "vob": "video", + "svi": "video", + "rmvb": "video", + "rm": "video", + "ogv": "video", + "nsv": "video", + "mpv": "video", + "mpg": "video", + "mpeg2": "video", + "mpeg": "video", + "mpe": "video", + "mp4": "mp4", + "mp2": "video", + "mov": "mov", + "mk3d": "video", + "mkv": "video", + "m4v": "video", + "m2v": "video", + "flv": "video", + "f4v": "video", + "f4p": "video", + "f4b": "video", + "f4a": "video", + "qt": "video", + "divx": "video", + "avi": "video", + "amv": "video", + "asf": "video", + "3gp": "video", + "3g2": "video", + "ico": "imageico", + "tiff": "image", + "bmp": "image", + "png": "imagepng", + "gif": "imagegif", + "jpg": "imagejpg", + "jpeg": "imagejpg", + "7z": "zip", + "7zip": "zip", + "blade.php": "blade", + "cfg.dist": "conf", + "cjs.map": "jsmap", + "controller.js": "nestjscontroller", + "controller.ts": "nestjscontroller", + "repository.js": "nestjsrepository", + "repository.ts": "nestjsrepository", + "scheduler.js": "nestscheduler", + "scheduler.ts": "nestscheduler", + "css.js": "vanillaextract", + "css.ts": "vanillaextract", + "css.map": "cssmap", + "d.ts": "typescriptdef", + "decorator.js": "nestjsdecorator", + "decorator.ts": "nestjsdecorator", + "drawio.png": "drawio", + "drawio.svg": "drawio", + "e2e-spec.ts": "testts", + "e2e-spec.tsx": "testts", + "e2e-test.ts": "testts", + "e2e-test.tsx": "testts", + "filter.js": "nestjsfilter", + "filter.ts": "nestjsfilter", + "format.ps1xml": "powershell_format", + "gemfile.lock": "bundler", + "gradle.kts": "gradlekotlin", + "guard.js": "nestjsguard", + "guard.ts": "nestjsguard", + "jar.old": "jar", + "js.flow": "flow", + "js.map": "jsmap", + "js.snap": "jest_snapshot", + "json-ld": "jsonld", + "jsx.snap": "jest_snapshot", + "layout.htm": "layout", + "layout.html": "layout", + "marko.js": "markojs", + "mjs.map": "jsmap", + "module.ts": "nestjsmodule", + "resolver.js": "nestjsresolver", + "resolver.ts": "nestjsresolver", + "service.js": "nestjsservice", + "service.ts": "nestjsservice", + "entity.js": "nestjsentity", + "entity.ts": "nestjsentity", + "interceptor.js": "nestjsinterceptor", + "interceptor.ts": "nestjsinterceptor", + "dto.js": "nestjsdto", + "dto.ts": "nestjsdto", + "spec.js": "testjs", + "spec.jsx": "testjs", + "spec.mjs": "testjs", + "spec.ts": "testts", + "spec.tsx": "testts", + "stories.js": "storybook", + "stories.jsx": "storybook", + "stories.ts": "storybook", + "stories.tsx": "storybook", + "stories.svelte": "storybook", + "story.js": "storybook", + "story.jsx": "storybook", + "story.ts": "storybook", + "story.tsx": "storybook", + "story.svelte": "storybook", + "test.cjs": "testjs", + "test.cts": "testts", + "test.js": "testjs", + "test.jsx": "testjs", + "test.mjs": "testjs", + "test.mts": "testts", + "test.ts": "testts", + "test.tsx": "testts", + "ts.snap": "jest_snapshot", + "tsx.snap": "jest_snapshot", + "types.ps1xml": "powershell_types", + "a": "binary", + "accda": "access", + "accdb": "access", + "accdc": "access", + "accde": "access", + "accdp": "access", + "accdr": "access", + "accdt": "access", + "accdu": "access", + "ade": "access", + "adoc": "adoc", + "adp": "access", + "afdesign": "afdesign", + "affinitydesigner": "afdesign", + "affinityphoto": "afphoto", + "affinitypublisher": "afpub", + "afphoto": "afphoto", + "afpub": "afpub", + "ai": "ai", + "app": "binary", + "ascx": "aspx", + "asm": "binary", + "aspx": "aspx", + "astro": "astro", + "awk": "awk", + "bat": "bat", + "bc": "llvm", + "bcmx": "outlook", + "bicep": "bicep", + "bin": "binary", + "blade": "blade", + "bz2": "zip", + "bzip2": "zip", + "c": "c", + "cake": "cake", + "cer": "cert", + "pvk": "pvk", + "pfx": "pfx", + "spc": "spc", + "cfg": "conf", + "civet": "civet", + "cjm": "clojure", + "cl": "opencl", + "class": "class", + "cli": "cli", + "clj": "clojure", + "cljc": "clojure", + "cljs": "clojure", + "cljx": "clojure", + "cma": "binary", + "cmd": "cli", + "cmi": "binary", + "cmo": "binary", + "cmx": "binary", + "cmxa": "binary", + "comp": "opengl", + "conf": "conf", + "cpp": "cpp", + "cr": "crystal", + "crec": "lync", + "crl": "cert", + "crt": "cert", + "cs": "csharp", + "cshtml": "cshtml", + "csproj": "csproj", + "csr": "cert", + "css": "css", + "csv": "csv", + "csx": "csharp", + "d": "d", + "dart": "dartlang", + "db": "sqlite", + "db3": "sqlite", + "der": "cert", + "diff": "diff", + "dio": "drawio", + "djt": "django", + "dll": "binary", + "dmp": "log", + "doc": "word", + "docm": "word", + "docx": "word", + "dot": "word", + "dotm": "word", + "dotx": "word", + "drawio": "drawio", + "dta": "stata", + "eco": "docpad", + "edge": "edge", + "edn": "clojure", + "eex": "eex", + "ejs": "ejs", + "el": "emacs", + "elc": "emacs", + "elm": "elm", + "enc": "license", + "ensime": "ensime", + "env": "env", + "eps": "eps", + "erb": "erb", + "erl": "erlang", + "eskip": "skipper", + "ex": "elixir", + "exe": "binary", + "exp": "tcl", + "exs": "exs", + "fbx": "fbx", + "feature": "cucumber", + "fig": "figma", + "fish": "shell", + "fla": "fla", + "fods": "excel", + "frag": "opengl", + "fs": "fsharp", + "fsproj": "fsproj", + "ftl": "freemarker", + "gbl": "gbl", + "gd": "godot", + "gemfile": "bundler", + "geom": "opengl", + "glsl": "opengl", + "gmx": "gamemaker", + "go": "go", + "godot": "godot", + "gql": "graphql", + "gradle": "gradle", + "groovy": "groovy", + "gz": "zip", + "h": "cheader", + "haml": "haml", + "hbs": "handlebars", + "hcl": "hashicorp", + "hl": "binary", + "hlsl": "opengl", + "hpp": "hpp", + "hs": "haskell", + "html": "html", + "hxp": "lime", + "hxproj": "haxedevelop", + "ibc": "idrisbin", + "idr": "idris", + "ilk": "binary", + "imba": "imba", + "inc": "inc", + "include": "inc", + "info": "info", + "infopathxml": "infopath", + "ini": "conf", + "ino": "arduino", + "ipkg": "idrispkg", + "ipynb": "ipynb", + "iuml": "plantuml", + "jar": "jar", + "java": "java", + "jbuilder": "jbuilder", + "j2": "jinja", + "jinja": "jinja", + "jinja2": "jinja", + "jl": "julia", + "json5": "json5", + "jsonld": "jsonld", + "jsp": "jsp", + "jss": "jss", + "key": "key", + "kit": "codekit", + "kt": "kotlin", + "kts": "kotlins", + "laccdb": "access", + "ldb": "access", + "less": "less", + "lib": "binary", + "lidr": "idris", + "liquid": "liquid", + "ll": "llvm", + "lnk": "lnk", + "log": "log", + "ls": "livescript", + "lucee": "cf", + "m": "m", + "makefile": "makefile", + "mam": "access", + "map": "map", + "maq": "access", + "markdown": "markdown", + "master": "layout", + "mdb": "access", + "mdown": "markdown", + "mdw": "access", + "mdx": "markdownx", + "mesh": "mesh", + "mex": "matlab", + "mexn": "matlab", + "mexrs6": "matlab", + "mf": "manifest", + "mint": "mint", + "mjml": "mjml", + "ml": "ocaml", + "mli": "ocamli", + "mll": "ocamll", + "mly": "ocamly", + "mn": "matlab", + "mo": "motoko", + "msg": "outlook", + "mst": "mustache", + "mum": "matlab", + "mustache": "mustache", + "mx": "matlab", + "mx3": "matlab", + "n": "binary", + "ndll": "binary", + "neon": "neon", + "nim": "nim", + "nix": "nix", + "njk": "njk", + "njs": "nunjucks", + "njsproj": "njsproj", + "nunj": "nunjucks", + "nupkg": "nuget", + "nuspec": "nuget", + "nvim": "nvim", + "o": "binary", + "ocrec": "lync", + "ods": "excel", + "oft": "outlook", + "one": "onenote", + "onepkg": "onenote", + "onetoc": "onenote", + "onetoc2": "onenote", + "opencl": "opencl", + "org": "org", + "otf": "fontotf", + "otm": "outlook", + "ovpn": "ovpn", + "P": "prolog", + "p12": "cert", + "p7b": "cert", + "p7r": "cert", + "pa": "powerpoint", + "patch": "diff", + "pcd": "pcl", + "pck": "plsql_package", + "pdb": "binary", + "pde": "arduino", + "pdf": "pdf", + "pem": "key", + "pex": "xml", + "phar": "php", + "php1": "php", + "php2": "php", + "php3": "php", + "php4": "php", + "php5": "php", + "php6": "php", + "phps": "php", + "phpsa": "php", + "phpt": "php", + "phtml": "php", + "pkb": "plsql_package_body", + "pkg": "package", + "pkh": "plsql_package_header", + "pks": "plsql_package_spec", + "pl": "perl", + "plantuml": "plantuml", + "plist": "config", + "pm": "perlm", + "po": "poedit", + "postcss": "postcssconfig", + "pcss": "postcssconfig", + "pot": "powerpoint", + "potm": "powerpoint", + "potx": "powerpoint", + "ppa": "powerpoint", + "ppam": "powerpoint", + "pps": "powerpoint", + "ppsm": "powerpoint", + "ppsx": "powerpoint", + "ppt": "powerpoint", + "pptm": "powerpoint", + "pptx": "powerpoint", + "pri": "qt", + "prisma": "prisma", + "pro": "prolog", + "properties": "properties", + "ps1": "powershell", + "psd": "photoshop", + "psd1": "powershelldata", + "psm1": "powershellmodule", + "psmdcp": "nuget", + "pst": "outlook", + "pu": "plantuml", + "pub": "publisher", + "puml": "plantuml", + "puz": "publisher", + "pyc": "binary", + "pyd": "binary", + "pyo": "binary", + "q": "q", + "qbs": "qbs", + "qvd": "qlikview", + "qvw": "qlikview", + "rake": "rake", + "rar": "zip", + "gzip": "zip", + "razor": "razor", + "rb": "ruby", + "reg": "registry", + "rego": "rego", + "res": "rescript", + "resi": "rescriptinterface", + "rjson": "rjson", + "rproj": "rproj", + "rs": "rust", + "rsx": "rust", + "ron": "ron", + "odin": "odin", + "rt": "reacttemplate", + "rwd": "matlab", + "pas": "pascal", + "pp": "pascal", + "p": "pascal", + "lpr": "lazarusproject", + "lps": "lazarusproject", + "lpi": "lazarusproject", + "lfm": "lazarusproject", + "lrs": "lazarusproject", + "lpk": "lazarusproject", + "dpr": "delphiproject", + "dproj": "delphiproject", + "dfm": "delphiproject", + "sass": "scss", + "sc": "scala", + "scala": "scala", + "scpt": "binary", + "scptd": "binary", + "scss": "scss", + "sentinel": "sentinel", + "sig": "onenote", + "sketch": "sketch", + "slddc": "matlab", + "sldm": "powerpoint", + "sldx": "powerpoint", + "sln": "sln", + "sls": "saltstack", + "slx": "matlab", + "smv": "matlab", + "so": "binary", + "sol": "sol", + "sql": "sql", + "sqlite": "sqlite", + "sqlite3": "sqlite", + "src": "cert", + "sss": "sss", + "sst": "cert", + "stl": "cert", + "storyboard": "storyboard", + "styl": "stylus", + "suo": "suo", + "svelte": "svelte", + "svg": "svg", + "swc": "flash", + "swf": "flash", + "swift": "swift", + "tar": "zip", + "tcl": "tcl", + "templ": "tmpl", + "tesc": "opengl", + "tese": "opengl", + "tex": "latex", + "texi": "tex", + "tf": "terraform", + "tfstate": "terraform", + "tfvars": "terraformvars", + "tgz": "zip", + "tikz": "tex", + "tlg": "log", + "tmlanguage": "xml", + "tmpl": "tmpl", + "todo": "todo", + "toml": "toml", + "tpl": "smarty", + "tres": "tres", + "tscn": "tscn", + "tst": "test", + "tsx": "reactts", + "jsx": "reactjs", + "tt2": "tt", + "ttf": "fontttf", + "twig": "twig", + "txt": "txt", + "ui": "ui", + "unity": "shaderlab", + "user": "user", + "v": "v", + "vala": "vala", + "vapi": "vapi", + "vash": "vash", + "vbhtml": "vbhtml", + "vbproj": "vbproj", + "vcxproj": "vcxproj", + "vert": "opengl", + "vhd": "vhd", + "vhdl": "vhdl", + "vsix": "vscode", + "vsixmanifest": "manifest", + "wasm": "wasm", + "webp": "imagewebp", + "wgsl": "wgsl", + "wll": "word", + "woff": "fontwoff", + "eot": "fonteot", + "woff2": "fontwoff2", + "wv": "audiowv", + "wxml": "wxml", + "wxss": "wxss", + "xcodeproj": "xcode", + "xfl": "xfl", + "xib": "xib", + "xlf": "xliff", + "xliff": "xliff", + "xls": "excel", + "xlsm": "excel", + "xlsx": "excel", + "xsf": "infopath", + "xsn": "infopath", + "xtp2": "infopath", + "xvc": "matlab", + "xz": "zip", + "yy": "gamemaker2", + "yyp": "gamemaker2", + "zig": "zig", + "zip": "zip", + "zipx": "zip", + "zz": "zip", + "deflate": "zip", + "brotli": "brotli", + "kra": "krita", + "mgcb": "mgcb", + "anim": "anim", + "cy.ts": "cypressts", + "cy.js": "cypressjs", + "hx": "haxe", + "hxml": "haxeml", + "gr": "grain", + "slim": "slim", + "obj": "obj", + "mtl": "mtl", + "bicepparam": "bicepparam", + "proto": "proto", + "wren": "wren", + "docker-compose.yml": "docker", + "excalidraw": "excalidraw", + "excalidraw.json": "excalidraw", + "excalidraw.svg": "excalidraw", + "excalidraw.png": "excalidraw", + "bazel": "bazel", + "bzl": "bazel", + "bazelignore": "bazelignore", + "bazelrc": "bazel", + "http": "http", + "rkt": "racket", + "rktl": "racket", + "bru": "bruno", + "nelua": "nelua", + "mermaid": "mermaid", + "mmd": "mermaid", + "bal": "ballerina", + "hash": "hash", + "gleam": "gleam", + "lock": "lock", + "yang": "yang", + "yin": "yin", + "mdc": "cursor", + "uml": "plantuml", + "Identifier": "identifier", + "cls": "salesforce", + ".instructions.md": "instructions", + ".instructions.txt": "instructions", + ".instructions.json": "instructions", + ".instructions.yaml": "instructions", + ".instructions.yml": "instructions", + "silq": "silq", + "eraserdiagram": "eraser" + }, + "fileNames": { + "webpack.config.images.js": "webpack", + "webpack.test.conf.ts": "webpack", + "webpack.test.conf.coffee": "webpack", + "webpack.test.conf.js": "webpack", + "webpack.rules.ts": "webpack", + "webpack.rules.coffee": "webpack", + "webpack.rules.js": "webpack", + "webpack.renderer.config.ts": "webpack", + "webpack.renderer.config.coffee": "webpack", + "webpack.renderer.config.js": "webpack", + "webpack.plugins.ts": "webpack", + "webpack.plugins.coffee": "webpack", + "webpack.plugins.js": "webpack", + "webpack.mix.ts": "webpack", + "webpack.mix.coffee": "webpack", + "webpack.mix.js": "webpack", + "webpack.main.config.ts": "webpack", + "webpack.main.config.coffee": "webpack", + "webpack.main.config.js": "webpack", + "webpack.prod.conf.ts": "webpack", + "webpack.prod.conf.coffee": "webpack", + "webpack.prod.conf.js": "webpack", + "webpack.prod.ts": "webpack", + "webpack.prod.coffee": "webpack", + "webpack.prod.js": "webpack", + "webpack.dev.conf.ts": "webpack", + "webpack.dev.conf.coffee": "webpack", + "webpack.dev.conf.js": "webpack", + "webpack.dev.ts": "webpack", + "webpack.dev.coffee": "webpack", + "webpack.dev.js": "webpack", + "webpack.config.production.babel.ts": "webpack", + "webpack.config.production.babel.coffee": "webpack", + "webpack.config.production.babel.js": "webpack", + "webpack.config.prod.babel.ts": "webpack", + "webpack.config.prod.babel.coffee": "webpack", + "webpack.config.prod.babel.js": "webpack", + "webpack.config.test.babel.ts": "webpack", + "webpack.config.test.babel.coffee": "webpack", + "webpack.config.test.babel.js": "webpack", + "webpack.config.staging.babel.ts": "webpack", + "webpack.config.staging.babel.coffee": "webpack", + "webpack.config.staging.babel.js": "webpack", + "webpack.config.development.babel.ts": "webpack", + "webpack.config.development.babel.coffee": "webpack", + "webpack.config.development.babel.js": "webpack", + "webpack.config.dev.babel.ts": "webpack", + "webpack.config.dev.babel.coffee": "webpack", + "webpack.config.dev.babel.js": "webpack", + "webpack.config.common.babel.ts": "webpack", + "webpack.config.common.babel.coffee": "webpack", + "webpack.config.common.babel.js": "webpack", + "webpack.config.base.babel.ts": "webpack", + "webpack.config.base.babel.coffee": "webpack", + "webpack.config.base.babel.js": "webpack", + "webpack.config.babel.ts": "webpack", + "webpack.config.babel.coffee": "webpack", + "webpack.config.babel.js": "webpack", + "webpack.config.production.ts": "webpack", + "webpack.config.production.coffee": "webpack", + "webpack.config.production.js": "webpack", + "webpack.config.prod.ts": "webpack", + "webpack.config.prod.coffee": "webpack", + "webpack.config.prod.js": "webpack", + "webpack.config.test.ts": "webpack", + "webpack.config.test.coffee": "webpack", + "webpack.config.test.js": "webpack", + "webpack.config.staging.ts": "webpack", + "webpack.config.staging.coffee": "webpack", + "webpack.config.staging.js": "webpack", + "webpack.config.development.ts": "webpack", + "webpack.config.development.coffee": "webpack", + "webpack.config.development.js": "webpack", + "webpack.config.dev.ts": "webpack", + "webpack.config.dev.coffee": "webpack", + "webpack.config.dev.js": "webpack", + "webpack.config.common.ts": "webpack", + "webpack.config.common.coffee": "webpack", + "webpack.config.common.js": "webpack", + "webpack.config.base.ts": "webpack", + "webpack.config.base.coffee": "webpack", + "webpack.config.base.js": "webpack", + "webpack.config.ts": "webpack", + "webpack.config.coffee": "webpack", + "webpack.config.js": "webpack", + "webpack.common.ts": "webpack", + "webpack.common.coffee": "webpack", + "webpack.common.js": "webpack", + "webpack.base.conf.ts": "webpack", + "webpack.base.conf.coffee": "webpack", + "webpack.base.conf.js": "webpack", + ".angular-cli.json": "angular", + "angular-cli.json": "angular", + "angular.json": "angular", + ".angular.json": "angular", + "api-extractor.json": "api_extractor", + "api-extractor-base.json": "api_extractor", + "appveyor.yml": "appveyor", + ".appveyor.yml": "appveyor", + "aurelia.json": "aurelia", + "azure-pipelines.yml": "azure", + ".vsts-ci.yml": "azure", + ".babelrc": "babel", + ".babelignore": "babel", + ".babelrc.js": "babel", + ".babelrc.cjs": "babel", + ".babelrc.mjs": "babel", + ".babelrc.json": "babel", + "babel.config.js": "babel", + "babel.config.cjs": "babel", + "babel.config.mjs": "babel", + "babel.config.json": "babel", + "vetur.config.js": "vue", + "vetur.config.ts": "vue", + ".bzrignore": "bazaar", + ".bazelrc": "bazel", + "bazel.rc": "bazel", + "bazel.bazelrc": "bazel", + "BUILD": "bazel", + "bitbucket-pipelines.yml": "bitbucketpipeline", + ".bithoundrc": "bithound", + ".bowerrc": "bower", + "bower.json": "bower", + ".browserslistrc": "browserslist", + "browserslist": "browserslist", + "gemfile": "bundler", + "gemfile.lock": "bundler", + ".ruby-version": "bundler", + "capacitor.config.json": "capacitor", + "cargo.toml": "cargo", + "cargo.lock": "cargo", + "chefignore": "chef", + "berksfile": "chef", + "berksfile.lock": "chef", + "policyfile": "chef", + "circle.yml": "circleci", + ".cfignore": "cloudfoundry", + ".codacy.yml": "codacy", + ".codacy.yaml": "codacy", + ".codeclimate.yml": "codeclimate", + "codecov.yml": "codecov", + ".codecov.yml": "codecov", + "config.codekit": "codekit", + "config.codekit2": "codekit", + "config.codekit3": "codekit", + ".config.codekit": "codekit", + ".config.codekit2": "codekit", + ".config.codekit3": "codekit", + "coffeelint.json": "coffeelint", + ".coffeelintignore": "coffeelint", + "composer.json": "composer", + "composer.lock": "composerlock", + "conanfile.txt": "conan", + "conanfile.py": "conan", + ".condarc": "conda", + ".coveralls.yml": "coveralls", + "crowdin.yml": "crowdin", + ".csscomb.json": "csscomb", + ".csslintrc": "csslint", + ".cvsignore": "cvs", + ".boringignore": "darcs", + "dependabot.yml": "dependabot", + "dependencies.yml": "dependencies", + "devcontainer.json": "devcontainer", + "docker-compose-prod.yml": "docker", + "docker-compose.alpha.yaml": "docker", + "docker-compose.alpha.yml": "docker", + "docker-compose.beta.yaml": "docker", + "docker-compose.beta.yml": "docker", + "docker-compose.ci-build.yml": "docker", + "docker-compose.ci.yaml": "docker", + "docker-compose.ci.yml": "docker", + "docker-compose.dev.yaml": "docker", + "docker-compose.dev.yml": "docker", + "docker-compose.development.yaml": "docker", + "docker-compose.development.yml": "docker", + "docker-compose.local.yaml": "docker", + "docker-compose.local.yml": "docker", + "docker-compose.override.yaml": "docker", + "docker-compose.override.yml": "docker", + "docker-compose.prod.yaml": "docker", + "docker-compose.prod.yml": "docker", + "docker-compose.production.yaml": "docker", + "docker-compose.production.yml": "docker", + "docker-compose.stage.yaml": "docker", + "docker-compose.stage.yml": "docker", + "docker-compose.staging.yaml": "docker", + "docker-compose.staging.yml": "docker", + "docker-compose.test.yaml": "docker", + "docker-compose.test.yml": "docker", + "docker-compose.testing.yaml": "docker", + "docker-compose.testing.yml": "docker", + "docker-compose.vs.debug.yml": "docker", + "docker-compose.vs.release.yml": "docker", + "docker-compose.web.yaml": "docker", + "docker-compose.web.yml": "docker", + "docker-compose.worker.yaml": "docker", + "docker-compose.worker.yml": "docker", + "docker-compose.yaml": "docker", + "docker-compose.yml": "docker", + "Dockerfile-production": "docker", + "dockerfile.alpha": "docker", + "dockerfile.beta": "docker", + "dockerfile.ci": "docker", + "dockerfile.dev": "docker", + "dockerfile.development": "docker", + "dockerfile.local": "docker", + "dockerfile.prod": "docker", + "dockerfile.production": "docker", + "dockerfile.stage": "docker", + "dockerfile.staging": "docker", + "dockerfile.test": "docker", + "dockerfile.testing": "docker", + "dockerfile.web": "docker", + "dockerfile.worker": "docker", + "dockerfile": "docker", + "docker-compose.debug.yml": "dockerdebug", + "docker-cloud.yml": "docker", + ".dockerignore": "dockerignore", + ".doczrc": "docz", + "docz.js": "docz", + "docz.json": "docz", + ".docz.js": "docz", + ".docz.json": "docz", + "doczrc.js": "docz", + "doczrc.json": "docz", + "docz.config.js": "docz", + "docz.config.json": "docz", + ".dojorc": "dojo", + ".drone.yml": "drone", + ".drone.yml.sig": "drone", + ".dvc": "dvc", + ".editorconfig": "editorconfig", + "elm-package.json": "elm", + ".ember-cli": "ember", + "emakefile": "erlang", + ".emakerfile": "erlang", + ".eslintrc": "eslint", + ".eslintignore": "eslintignore", + ".eslintcache": "eslint", + ".eslintrc.js": "eslint", + ".eslintrc.mjs": "eslint", + ".eslintrc.cjs": "eslint", + ".eslintrc.json": "eslint", + ".eslintrc.yaml": "eslint", + ".eslintrc.yml": "eslint", + ".eslintrc.browser.json": "eslint", + ".eslintrc.base.json": "eslint", + "eslint-preset.js": "eslint", + "eslint.config.js": "eslint", + "eslint.config.cjs": "eslint", + "eslint.config.mjs": "eslint", + "eslint.config.ts": "eslint", + "_eslintrc.cjs": "eslint", + "app.json": "expo", + "app.config.js": "expo", + "app.config.json": "expo", + "app.config.json5": "expo", + "favicon.ico": "favicon", + ".firebaserc": "firebase", + "firebase.json": "firebasehosting", + "firestore.rules": "firestore", + "firestore.indexes.json": "firestore", + ".flooignore": "floobits", + ".flowconfig": "flow", + ".flutter-plugins": "flutter", + ".metadata": "flutter", + ".fossaignore": "fossa", + "ignore-glob": "fossil", + "fuse.js": "fusebox", + "gatsby-config.js": "gatsby", + "gatsby-config.ts": "gatsby", + "gatsby-node.js": "gatsby", + "gatsby-node.ts": "gatsby", + "gatsby-browser.js": "gatsby", + "gatsby-browser.ts": "gatsby", + "gatsby-ssr.js": "gatsby", + "gatsby-ssr.ts": "gatsby", + ".git-blame-ignore-revs": "git", + ".gitattributes": "git", + ".gitconfig": "git", + ".gitignore": "git", + ".gitmodules": "git", + ".gitkeep": "git", + ".mailmap": "git", + ".gitlab-ci.yml": "gitlab", + "glide.yml": "glide", + "go.sum": "go_package", + "go.mod": "go_package", + "go.work": "go_package", + ".gqlconfig": "graphql", + ".graphqlconfig": "graphql_config", + ".graphqlconfig.yml": "graphql_config", + ".graphqlconfig.yaml": "graphql_config", + "greenkeeper.json": "greenkeeper", + "gridsome.config.js": "gridsome", + "gridsome.config.ts": "gridsome", + "gridsome.server.js": "gridsome", + "gridsome.server.ts": "gridsome", + "gridsome.client.js": "gridsome", + "gridsome.client.ts": "gridsome", + "gruntfile.js": "grunt", + "gruntfile.cjs": "grunt", + "gruntfile.mjs": "grunt", + "gruntfile.coffee": "grunt", + "gruntfile.ts": "grunt", + "gruntfile.cts": "grunt", + "gruntfile.mts": "grunt", + "gruntfile.babel.js": "grunt", + "gruntfile.babel.coffee": "grunt", + "gruntfile.babel.ts": "grunt", + "gulpfile.js": "gulp", + "gulpfile.coffee": "gulp", + "gulpfile.ts": "gulp", + "gulpfile.esm.js": "gulp", + "gulpfile.esm.coffee": "gulp", + "gulpfile.esm.ts": "gulp", + "gulpfile.babel.js": "gulp", + "gulpfile.babel.coffee": "gulp", + "gulpfile.babel.ts": "gulp", + "haxelib.json": "haxe", + "checkstyle.json": "haxecheckstyle", + ".p4ignore": "helix", + ".htmlhintrc": "htmlhint", + ".huskyrc": "husky", + "husky.config.js": "husky", + ".huskyrc.js": "husky", + ".huskyrc.json": "husky", + ".huskyrc.yaml": "husky", + ".huskyrc.yml": "husky", + "ionic.project": "ionic", + "ionic.config.json": "ionic", + "jakefile": "jake", + "jakefile.js": "jake", + "jest.config.json": "jest", + "jest.json": "jest", + ".jestrc": "jest", + ".jestrc.js": "jest", + ".jestrc.json": "jest", + "jest.config.js": "jest", + "jest.config.cjs": "jest", + "jest.config.mjs": "jest", + "jest.config.babel.js": "jest", + "jest.config.babel.cjs": "jest", + "jest.config.babel.mjs": "jest", + "jest.preset.js": "jest", + "jest.preset.ts": "jest", + "jest.preset.cjs": "jest", + "jest.preset.mjs": "jest", + ".jpmignore": "jpm", + ".jsbeautifyrc": "jsbeautify", + "jsbeautifyrc": "jsbeautify", + ".jsbeautify": "jsbeautify", + "jsbeautify": "jsbeautify", + "jsconfig.json": "jsconfig", + ".jscpd.json": "jscpd", + "jscpd-report.xml": "jscpd", + "jscpd-report.json": "jscpd", + "jscpd-report.html": "jscpd", + ".jshintrc": "jshint", + ".jshintignore": "jshint", + "karma.conf.js": "karma", + "karma.conf.coffee": "karma", + "karma.conf.ts": "karma", + ".kitchen.yml": "kitchenci", + "kitchen.yml": "kitchenci", + ".kiteignore": "kite", + "layout.html": "layout", + "layout.htm": "layout", + "lerna.json": "lerna", + "license": "license", + "licence": "license", + "license.md": "license", + "license.txt": "license", + "licence.md": "license", + "licence.txt": "license", + ".lighthouserc.js": "lighthouse", + ".lighthouserc.json": "lighthouse", + ".lighthouserc.yaml": "lighthouse", + ".lighthouserc.yml": "lighthouse", + "include.xml": "lime", + ".lintstagedrc": "lintstagedrc", + "lint-staged.config.js": "lintstagedrc", + ".lintstagedrc.js": "lintstagedrc", + ".lintstagedrc.json": "lintstagedrc", + ".lintstagedrc.yaml": "lintstagedrc", + ".lintstagedrc.yml": "lintstagedrc", + "manifest": "manifest", + "manifest.bak": "manifest", + "manifest.json": "manifest", + "manifest.skip": "manifes", + ".markdownlint.json": "markdownlint", + "maven.config": "maven", + "pom.xml": "maven", + "extensions.xml": "maven", + "settings.xml": "maven", + "pom.properties": "maven", + ".hgignore": "mercurial", + "mocha.opts": "mocha", + ".mocharc.js": "mocha", + ".mocharc.json": "mocha", + ".mocharc.jsonc": "mocha", + ".mocharc.yaml": "mocha", + ".mocharc.yml": "mocha", + "modernizr": "modernizr", + "modernizr.js": "modernizr", + "modernizrrc.js": "modernizr", + ".modernizr.js": "modernizr", + ".modernizrrc.js": "modernizr", + "moleculer.config.js": "moleculer", + "moleculer.config.json": "moleculer", + "moleculer.config.ts": "moleculer", + ".mtn-ignore": "monotone", + ".nest-cli.json": "nestjs", + "nest-cli.json": "nestjs", + "nestconfig.json": "nestjs", + ".nestconfig.json": "nestjs", + "netlify.toml": "netlify", + "_redirects": "netlify", + "ng-tailwind.js": "ng_tailwind", + "nginx.conf": "nginx", + "build.ninja": "ninja", + ".node-version": "node", + ".node_repl_history": "node", + ".node-gyp": "node", + "node_modules": "node", + "node_modules.json": "node", + "node-inspect.json": "node", + "node-inspect.js": "node", + "node-inspect.mjs": "node", + "node-inspect.cjs": "node", + "node-inspect.ts": "node", + "node-inspect.config.js": "node", + "node-inspect.config.ts": "node", + "node-inspect.config.cjs": "node", + "node-inspect.config.mjs": "node", + "node-inspect.config.json": "node", + "node-inspect.config.yaml": "node", + "node-inspect.config.yml": "node", + "node-inspectrc": "node", + ".node-inspectrc": "node", + ".node-inspectrc.json": "node", + ".node-inspectrc.yaml": "node", + ".node-inspectrc.yml": "node", + ".node-inspectrc.js": "node", + ".node-inspectrc.ts": "node", + ".node-inspectrc.cjs": "node", + ".node-inspectrc.mjs": "node", + "nodemon.json": "nodemon", + ".npmignore": "npm", + ".npmrc": "npm", + "package.json": "npm", + "package-lock.json": "npmlock", + "npm-shrinkwrap.json": "npm", + ".nsrirc": "nsri", + ".nsriignore": "nsri", + "nsri.config.js": "nsri", + ".nsrirc.js": "nsri", + ".nsrirc.json": "nsri", + ".nsrirc.yaml": "nsri", + ".nsrirc.yml": "nsri", + ".integrity.json": "nsri-integrity", + "nuxt.config.js": "nuxt", + "nuxt.config.ts": "nuxt", + ".nycrc": "nyc", + ".nycrc.json": "nyc", + ".merlin": "ocaml", + "paket.dependencies": "paket", + "paket.lock": "paket", + "paket.references": "paket", + "paket.template": "paket", + "paket.local": "paket", + ".php_cs": "phpcsfixer", + ".php_cs.dist": "phpcsfixer", + "phpunit": "phpunit", + "phpunit.xml": "phpunit", + "phpunit.xml.dist": "phpunit", + ".phraseapp.yml": "phraseapp", + "pipfile": "pip", + "pipfile.lock": "pip", + "platformio.ini": "platformio", + "pnpmfile.js": "pnpm", + "pnpm-workspace.yaml": "pnpm", + ".postcssrc": "postcssconfig", + ".postcssrc.json": "postcssconfig", + ".postcssrc.yml": "postcssconfig", + ".postcssrc.js": "postcssconfig", + ".postcssrc.cjs": "postcssconfig", + ".postcssrc.mjs": "postcssconfig", + ".postcssrc.ts": "postcssconfig", + ".postcssrc.cts": "postcssconfig", + ".postcssrc.mts": "postcssconfig", + "postcss.config.js": "postcssconfig", + "postcss.config.cjs": "postcssconfig", + "postcss.config.mjs": "postcssconfig", + "postcss.config.ts": "postcssconfig", + "postcss.config.cts": "postcssconfig", + "postcss.config.mts": "postcssconfig", + ".pre-commit-config.yaml": "precommit", + ".pre-commit-hooks.yaml": "precommit", + ".prettierrc": "prettier", + ".prettierignore": "prettierignore", + "prettier.config.js": "prettier", + "prettier.config.cjs": "prettier", + "prettier.config.mjs": "prettier", + "prettier.config.ts": "prettier", + "prettier.config.coffee": "prettier", + ".prettierrc.js": "prettier", + ".prettierrc.json": "prettier", + ".prettierrc.yml": "prettier", + ".prettierrc.yaml": "prettier", + "procfile": "procfile", + "protractor.conf.js": "protractor", + "protractor.conf.coffee": "protractor", + "protractor.conf.ts": "protractor", + ".jade-lintrc": "pug", + ".pug-lintrc": "pug", + ".jade-lint.json": "pug", + ".pug-lintrc.js": "pug", + ".pug-lintrc.json": "pug", + ".pyup": "pyup", + ".pyup.yml": "pyup", + "qmldir": "qmldir", + "quasar.conf.js": "quasar", + "rakefile": "rake", + "razzle.config.js": "razzle", + "readme.md": "readme", + "readme.txt": "readme", + ".rehyperc": "rehype", + ".rehypeignore": "rehype", + ".rehyperc.js": "rehype", + ".rehyperc.json": "rehype", + ".rehyperc.yml": "rehype", + ".rehyperc.yaml": "rehype", + ".remarkrc": "remark", + ".remarkignore": "remark", + ".remarkrc.js": "remark", + ".remarkrc.json": "remark", + ".remarkrc.yml": "remark", + ".remarkrc.yaml": "remark", + ".renovaterc": "renovate", + "renovate.json": "renovate", + ".renovaterc.json": "renovate", + ".retextrc": "retext", + ".retextignore": "retext", + ".retextrc.js": "retext", + ".retextrc.json": "retext", + ".retextrc.yml": "retext", + ".retextrc.yaml": "retext", + "robots.txt": "robots", + "rollup.config.js": "rollup", + "rollup.config.mjs": "rollup", + "rollup.config.coffee": "rollup", + "rollup.config.ts": "rollup", + "rollup.config.common.js": "rollup", + "rollup.config.common.mjs": "rollup", + "rollup.config.common.coffee": "rollup", + "rollup.config.common.ts": "rollup", + "rollup.config.dev.js": "rollup", + "rollup.config.dev.mjs": "rollup", + "rollup.config.dev.coffee": "rollup", + "rollup.config.dev.ts": "rollup", + "rollup.config.prod.js": "rollup", + "rollup.config.prod.mjs": "rollup", + "rollup.config.prod.coffee": "rollup", + "rollup.config.prod.ts": "rollup", + ".rspec": "rspec", + ".rubocop.yml": "rubocop", + ".rubocop_todo.yml": "rubocop", + "rust-toolchain": "rust_toolchain", + ".sentryclirc": "sentry", + "serverless.yml": "serverless", + "snapcraft.yaml": "snapcraft", + ".snyk": "snyk", + ".solidarity": "solidarity", + ".solidarity.json": "solidarity", + ".stylelintrc": "stylelint", + ".stylelintignore": "stylelintignore", + ".stylelintcache": "stylelint", + "stylelint.config.js": "stylelint", + "stylelint.config.cjs": "stylelint", + "stylelint.config.mjs": "stylelint", + "stylelint.config.json": "stylelint", + "stylelint.config.yaml": "stylelint", + "stylelint.config.yml": "stylelint", + "stylelint.config.ts": "stylelint", + ".stylelintrc.js": "stylelint", + ".stylelintrc.json": "stylelint", + ".stylelintrc.yaml": "stylelint", + ".stylelintrc.yml": "stylelint", + ".stylelintrc.ts": "stylelint", + ".stylelintrc.cjs": "stylelint", + ".stylelintrc.mjs": "stylelint", + ".stylish-haskell.yaml": "stylish_haskell", + ".svnignore": "subversion", + "package.pins": "swift", + "symfony.lock": "symfony", + "windi.config.ts": "windi", + "windi.config.js": "windi", + "tailwind.js": "tailwind", + "tailwind.mjs": "tailwind", + "tailwind.cjs": "tailwind", + "tailwind.coffee": "tailwind", + "tailwind.ts": "tailwind", + "tailwind.cts": "tailwind", + "tailwind.mts": "tailwind", + "tailwind.config.mjs": "tailwind", + "tailwind.config.cjs": "tailwind", + "tailwind.config.js": "tailwind", + "tailwind.config.coffee": "tailwind", + "tailwind.config.ts": "tailwind", + "tailwind.config.cts": "tailwind", + "tailwind.config.mts": "tailwind", + ".testcaferc.json": "testcafe", + ".tfignore": "tfs", + "tox.ini": "tox", + ".travis.yml": "travis", + "tsconfig.json": "tsconfig", + "tsconfig.app.json": "tsconfig", + "tsconfig.base.json": "tsconfig", + "tsconfig.common.json": "tsconfig", + "tsconfig.dev.json": "tsconfig", + "tsconfig.development.json": "tsconfig", + "tsconfig.e2e.json": "tsconfig", + "tsconfig.prod.json": "tsconfig", + "tsconfig.production.json": "tsconfig", + "tsconfig.server.json": "tsconfig", + "tsconfig.spec.json": "tsconfig", + "tsconfig.staging.json": "tsconfig", + "tsconfig.test.json": "tsconfig", + "tsconfig.tsd.json": "tsconfig", + "tsconfig.node.json": "tsconfig", + "tsconfig.lib.json": "tsconfig", + "tsconfig.eslint.json": "tsconfig", + "tsconfig.storybook.json": "tsconfig", + "tsconfig.tsbuildinfo": "tsconfig", + "tslint.json": "tslint", + "tslint.yaml": "tslint", + "tslint.yml": "tslint", + ".unibeautifyrc": "unibeautify", + "unibeautify.config.js": "unibeautify", + ".unibeautifyrc.js": "unibeautify", + ".unibeautifyrc.json": "unibeautify", + ".unibeautifyrc.yaml": "unibeautify", + ".unibeautifyrc.yml": "unibeautify", + "vagrantfile": "vagrant", + ".vimrc": "vim", + ".gvimrc": "vim", + ".vscodeignore": "vscode", + "tasks.json": "vscode", + "vscodeignore.json": "vscode", + ".vuerc": "vueconfig", + "vue.config.js": "vueconfig", + "vue.config.ts": "vueconfig", + "wallaby.json": "wallaby", + "wallaby.js": "wallaby", + "wallaby.ts": "wallaby", + "wallaby.coffee": "wallaby", + "wallaby.conf.json": "wallaby", + "wallaby.conf.js": "wallaby", + "wallaby.conf.ts": "wallaby", + "wallaby.conf.coffee": "wallaby", + ".wallaby.json": "wallaby", + ".wallaby.js": "wallaby", + ".wallaby.ts": "wallaby", + ".wallaby.coffee": "wallaby", + ".wallaby.conf.json": "wallaby", + ".wallaby.conf.js": "wallaby", + ".wallaby.conf.ts": "wallaby", + ".wallaby.conf.coffee": "wallaby", + ".watchmanconfig": "watchmanconfig", + "wercker.yml": "wercker", + "wpml-config.xml": "wpml", + ".yamllint": "yamllint", + ".yaspellerrc": "yandex", + ".yaspeller.json": "yandex", + "yarn.lock": "yarnlock", + ".yarnrc": "yarn", + ".yarn.installed": "yarn", + ".yarnclean": "yarn", + ".yarn-integrity": "yarn", + ".yarn-metadata.json": "yarn", + ".yarnignore": "yarnignore", + ".yarnrc.yml": "yarn", + ".yarnrc.yaml": "yarn", + ".yarnrc.json": "yarn", + ".yarnrc.json5": "yarn", + ".yarnrc.cjs": "yarn", + ".yarnrc.js": "yarn", + ".yarnrc.lock": "yarn", + ".yarnrc.txt": "yarn", + "yarn-error.log": "yarnerror", + ".yo-rc.json": "yeoman", + "now.json": "vercel", + ".nowignore": "vercel", + "vercel.json": "vercel", + ".vercel": "vercel", + ".vercelignore": "vercel", + "vite.config.js": "vite", + "vite.config.mjs": "vite", + "vite.config.cjs": "vite", + "vite.config.ts": "vite", + "vite.config.mts": "vite", + "vite.config.cts": "vite", + ".nvmrc": "nvm", + "example.env": "env", + ".env.staging": "env", + ".env.sample": "env", + ".env.preprod": "env", + ".env.prod": "env", + ".env.production": "env", + ".env.local": "env", + ".env.dev": "env", + ".env.dev.local": "env", + ".env.dev.prod": "env", + ".env.dev.preprod": "env", + ".env.dev.production": "env", + ".env.dev.staging": "env", + ".env.development": "env", + ".env.example": "env", + ".env.test": "env", + ".env.dist": "env", + ".env.default": "env", + ".jinja": "jinja", + "jenkins.yaml": "jenkins", + "jenkins.yml": "jenkins", + ".compodocrc": "compodoc", + ".compodocrc.json": "compodoc", + ".compodocrc.yaml": "compodoc", + ".compodocrc.yml": "compodoc", + "bsconfig.json": "bsconfig", + ".clang-format": "llvm", + ".clang-tidy": "llvm", + ".clangd": "llvm", + ".parcelrc": "parcel", + "dune": "dune", + "dune-project": "duneproject", + ".adonisrc.json": "adonis", + "astro.config.js": "astroconfig", + "astro.config.cjs": "astroconfig", + "astro.config.mjs": "astroconfig", + "astro.config.ts": "astroconfig", + "astro.config.cts": "astroconfig", + "astro.config.mts": "astroconfig", + "svelte.config.js": "svelteconfig", + "svelte.config.ts": "svelteconfig", + ".tool-versions": "toolversions", + "CMakeSettings.json": "cmake", + "CMakeLists.txt": "cmake", + "toolchain.cmake": "cmake", + ".cmake": "cmake", + "Cargo.toml": "cargo", + "Cargo.lock": "cargolock", + "pnpm-lock.yaml": "pnpmlock", + "tauri.conf.json": "tauri", + "tauri.conf.json5": "tauri", + "tauri.linux.conf.json": "tauri", + "tauri.windows.conf.json": "tauri", + "tauri.macos.conf.json": "tauri", + "next.config.js": "nextconfig", + "next.config.mjs": "nextconfig", + "next.config.ts": "nextconfig", + "nextron.config.js": "nextron", + "nextron.config.ts": "nextron", + "poetry.toml": "poetry", + "poetry.lock": "poetrylock", + "pyproject.toml": "pyproject", + "rustfmt.toml": "rustfmt", + ".rustfmt.toml": "rustfmt", + "cucumber.yml": "cucumber", + "cucumber.yaml": "cucumber", + "cucumber.js": "cucumber", + "cucumber.ts": "cucumber", + "cucumber.cjs": "cucumber", + "cucumber.mjs": "cucumber", + "cucumber.json": "cucumber", + "flake.lock": "flakelock", + "ace": "ace", + "ace-manifest.json": "acemanifest", + "knexfile.js": "knex", + "knexfile.ts": "knex", + "launch.json": "launch", + "redis.conf": "redis", + "sequelize.js": "sequelize", + "sequelize.ts": "sequelize", + "sequelize.cjs": "sequelize", + ".sequelizerc": "sequelize", + ".sequelizerc.js": "sequelize", + ".sequelizerc.json": "sequelize", + "cypress.json": "cypress", + "cypress.env.json": "cypress", + "cypress.config.js": "cypress", + "cypress.config.ts": "cypress", + "cypress.config.cjs": "cypress", + "playwright.config.ts": "playright", + "playwright.config.js": "playright", + "playwright.config.cjs": "playright", + "vitest.config.ts": "vitest", + "vitest.config.cts": "vitest", + "vitest.config.mts": "vitest", + "vitest.config.js": "vitest", + "vitest.config.cjs": "vitest", + "vitest.config.mjs": "vitest", + "vitest.workspace.ts": "vitest", + "vitest.workspace.cts": "vitest", + "vitest.workspace.mts": "vitest", + "vitest.workspace.js": "vitest", + "vitest.workspace.cjs": "vitest", + "vitest.workspace.mjs": "vitest", + "vite-env.d.ts": "viteenv", + "vite-env.d.js": "viteenv", + "pubspec.lock": "flutterlock", + "pubspec.yaml": "flutter", + ".packages": "flutterpackage", + ".htaccess": "htaccess", + "nx.json": "nx", + "project.json": "nx", + "nx.instructions.md": "nx", + "nx.jsonc": "nx", + "v.mod": "vmod", + "quasar.config.js": "quasar", + "quasar.config.ts": "quasar", + "quasar.config.cjs": "quasar", + "quasar.config.mjs": "quasar", + "quarkus.properties": "quarkus", + "theme.properties": "ui", + "gradlew": "gradle", + "gradle-wrapper.properties": "gradle", + "gradlew.bat": "gradlebat", + "makefile.win": "makefile", + "makefile": "makefile", + "make": "makefile", + "version": "version", + "server": "sql", + "migrate": "sql", + ".commitlintrc": "commitlint", + ".commitlintrc.json": "commitlint", + ".commitlintrc.yaml": "commitlint", + ".commitlintrc.yml": "commitlint", + ".commitlintrc.js": "commitlint", + ".commitlintrc.cjs": "commitlint", + ".commitlintrc.ts": "commitlint", + ".commitlintrc.cts": "commitlint", + "commitlint.config.js": "commitlint", + "commitlint.config.cjs": "commitlint", + "commitlint.config.ts": "commitlint", + "commitlint.config.cts": "commitlint", + ".terraform-version": "terraformversion", + "TerraFile": "terrafile", + "tfstate.backup": "terraform", + ".code-workspace": "codeworkspace", + "hardhat.config.js": "hardhat", + "hardhat.config.ts": "hardhat", + "hardhat.config.cts": "hardhat", + "hardhat.config.cjs": "hardhat", + "hardhat.config.mjs": "hardhat", + "taze.config.js": "taze", + "taze.config.ts": "taze", + "taze.config.cjs": "taze", + "taze.config.mjs": "taze", + ".tazerc.json": "taze", + "turbo.json": "turbo", + "turbo.jsonc": "turbo", + "uno.config.ts": "unocss", + "uno.config.js": "unocss", + "uno.config.mjs": "unocss", + "uno.config.mts": "unocss", + "unocss.config.ts": "unocss", + "unocss.config.js": "unocss", + "unocss.config.mjs": "unocss", + "unocss.config.mts": "unocss", + "atomizer.config.js": "atomizer", + "atomizer.config.cjs": "atomizer", + "atomizer.config.mjs": "atomizer", + "atomizer.config.ts": "atomizer", + "esbuild.js": "esbuild", + "esbuild.mjs": "esbuild", + "esbuild.cjs": "esbuild", + "esbuild.ts": "esbuild", + "mix.exs": "mix", + "mix.lock": "mixlock", + ".DS_Store": "dsstore", + "remix.config.js": "remix", + "remix.config.cjs": "remix", + "remix.config.mjs": "remix", + "remix.config.ts": "remix", + "xmake.lua": "xmake", + ".sailsrc": "sails", + "farm.config.ts": "farm", + "farm.config.js": "farm", + "bunfig.toml": "bun", + ".bunfig.toml": "bun", + "bun.lockb": "bunlock", + "bun.lock": "bunlock", + ".air.toml": "air", + "rome.json": "rome", + "biome.json": "biome", + "bicepconfig.json": "bicepconfig", + "drizzle.config.ts": "drizzle", + "drizzle.config.js": "drizzle", + "drizzle.config.json": "drizzle", + "panda.config.ts": "panda", + "panda.config.js": "panda", + "panda.config.json": "panda", + "panda.config.cjs": "panda", + "panda.config.mjs": "panda", + "panda.config.cts": "panda", + "panda.config.mts": "panda", + ".buckconfig": "buck", + "Ballerina.toml": "ballerinaconfig", + "knip.json": "knip", + "knip.jsonc": "knip", + ".knip.json": "knip", + ".knip.jsonc": "knip", + "knip.ts": "knip", + "knip.js": "knip", + "knip.config.ts": "knip", + "knip.config.js": "knip", + "todo.md": "todo", + ".todo.md": "todo", + "todo.txt": "todo", + ".todo.txt": "todo", + "todo": "todo", + "mkdocs.yml": "mkdocs", + "mkdocs.yaml": "mkdocs", + "gleam.toml": "gleamconfig", + ".oxlintrc.json": "oxlint", + "oxlint.json": "oxlint", + "oxlint.config.js": "oxlint", + "oxlint.config.ts": "oxlint", + "oxlint.config.cjs": "oxlint", + "oxlint.config.mjs": "oxlint", + "oxlint.config.cts": "oxlint", + "oxlint.config.mts": "oxlint", + ".cursorrules": "cursor", + "plopfile.js": "plop", + "plopfile.cjs": "plop", + "plopfile.mjs": "plop", + "plopfile.ts": "plop", + "plopfile.cts": "plop", + "config.mockoon.json": "mockoon", + "mockoon.json": "mockoon", + "mockoon.yaml": "mockoon", + "mockoon.yml": "mockoon", + "mockoon.env": "mockoon", + "mockoon.env.json": "mockoon", + "mockoon.env.yaml": "mockoon", + "mockoon.env.yml": "mockoon", + "mockoon.env.js": "mockoon", + "mockoon.env.ts": "mockoon", + "mockoon.env.cjs": "mockoon", + "mockoon.env.mjs": "mockoon", + "mockoon.env.cts": "mockoon", + "mockoon.env.mts": "mockoon", + "copilot-instructions.md": "copilot", + ".copilot-instructions": "copilot", + ".instructions": "instructions", + "instructions.md": "instructions", + "instructions.txt": "instructions", + "instructions": "instructions", + "instructions.json": "instructions", + "instructions.yaml": "instructions", + "instructions.yml": "instructions", + ".keep": "keep", + ".keepignore": "keep", + "CLAUDE.md": "claude", + "claude.md": "claude", + "claude.txt": "claude", + "claude": "claude", + "claude.json": "claude", + "claude.yaml": "claude", + ".claude_code_config": "claude", + ".claude": "claude", + "claude.config.js": "claude", + ".claude.yaml": "claude", + ".clauderc": "claude", + "claude-instructions.md": "claude", + ".claude-code": "claude", + "claude-code.config": "claude" + }, + "languageIds": { + "actionscript": "actionscript", + "ada": "ada", + "advpl": "advpl", + "affectscript": "affectscript", + "al": "al", + "ansible": "ansible", + "antlr": "antlr", + "anyscript": "anyscript", + "apacheconf": "apache", + "apex": "apex", + "apiblueprint": "apib", + "apl": "apl", + "applescript": "applescript", + "asciidoc": "asciidoc", + "asp": "asp", + "asp (html)": "asp", + "arm": "assembly", + "asm": "assembly", + "ats": "ats", + "ahk": "autohotkey", + "autoit": "autoit", + "avro": "avro", + "azcli": "azure", + "azure-pipelines": "azurepipelines", + "ballerina": "ballerina", + "bat": "bat", + "bats": "bats", + "bazel": "bazel", + "befunge": "befunge", + "befunge98": "befunge", + "biml": "biml", + "blade": "blade", + "laravel-blade": "blade", + "bolt": "bolt", + "bosque": "bosque", + "c": "c", + "c-al": "c_al", + "cabal": "cabal", + "caddyfile": "caddy", + "cddl": "cddl", + "ceylon": "ceylon", + "cfml": "cf", + "lang-cfml": "cf", + "cfc": "cfc", + "cfmhtml": "cfm", + "cookbook": "chef_cookbook", + "clojure": "clojure", + "clojurescript": "clojurescript", + "manifest-yaml": "cloudfoundry", + "cmake": "cmake", + "cmake-cache": "cmake", + "cobol": "cobol", + "coffeescript": "coffeescript", + "properties": "properties", + "dotenv": "config", + "confluence": "confluence", + "cpp": "cpp", + "crystal": "crystal", + "csharp": "csharp", + "css": "css", + "feature": "cucumber", + "cuda": "cuda", + "cython": "cython", + "dal": "dal", + "dart": "dartlang", + "pascal": "pascal", + "objectpascal": "pascal", + "diff": "diff", + "django-html": "django", + "django-txt": "django", + "d": "dlang", + "dscript": "dlang", + "dml": "dlang", + "diet": "dlang", + "dockerfile": "docker", + "ignore": "docker", + "dotjs": "dotjs", + "doxygen": "doxygen", + "drools": "drools", + "dustjs": "dustjs", + "dylan": "dylan", + "dylan-lid": "dylan", + "edge": "edge", + "eex": "eex", + "html-eex": "eex", + "es": "elastic", + "elixir": "elixir", + "elm": "elm", + "erb": "erb", + "erlang": "erlang", + "falcon": "falcon", + "fortran": "fortran", + "fortran-modern": "fortran", + "FortranFreeForm": "fortran", + "fortran_fixed-form": "fortran", + "ftl": "freemarker", + "fsharp": "fsharp", + "fthtml": "fthtml", + "galen": "galen", + "gml-gms": "gamemaker", + "gml-gms2": "gamemaker2", + "gml-gm81": "gamemaker81", + "gcode": "gcode", + "genstat": "genstat", + "git-commit": "git", + "git-rebase": "git", + "glsl": "glsl", + "glyphs": "glyphs", + "gnuplot": "gnuplot", + "go": "go", + "golang": "go", + "go-sum": "go", + "go-mod": "go", + "go-xml": "go", + "gdscript": "godot", + "graphql": "graphql", + "dot": "graphviz", + "groovy": "groovy", + "haml": "haml", + "handlebars": "handlebars", + "harbour": "harbour", + "haskell": "haskell", + "literate haskell": "haskell", + "haxe": "haxe", + "hxml": "haxe", + "Haxe AST dump": "haxe", + "helm": "helm", + "hjson": "hjson", + "hlsl": "opengl", + "home-assistant": "homeassistant", + "hosts": "host", + "html": "html", + "http": "http", + "hunspell.aff": "hunspell", + "hunspell.dic": "hunspell", + "hy": "hy", + "icl": "icl", + "imba": "imba", + "4GL": "informix", + "ini": "conf", + "ink": "ink", + "innosetup": "innosetup", + "io": "io", + "iodine": "iodine", + "janet": "janet", + "java": "java", + "raku": "raku", + "jekyll": "jekyll", + "jenkins": "jenkins", + "declarative": "jenkins", + "jenkinsfile": "jenkins", + "jinja": "jinja", + "code-referencing": "vscode", + "search-result": "vscode", + "type": "vscode", + "javascript": "js", + "json": "json", + "jsonl": "json", + "json-tmlanguage": "json", + "jsonc": "json", + "json5": "json5", + "jsonnet": "jsonnet", + "julia": "julia", + "juliamarkdown": "julia", + "kivy": "kivy", + "kos": "kos", + "kotlin": "kotlin", + "kusto": "kusto", + "latino": "latino", + "less": "less", + "lex": "lex", + "lisp": "lisp", + "lolcode": "lolcode", + "code-text-binary": "binary", + "lsl": "lsl", + "lua": "lua", + "makefile": "makefile", + "markdown": "markdown", + "marko": "marko", + "matlab": "matlab", + "maxscript": "maxscript", + "mel": "maya", + "mediawiki": "mediawiki", + "meson": "meson", + "mjml": "mjml", + "mlang": "mlang", + "powerquerymlanguage": "mlang", + "mojolicious": "mojolicious", + "mongo": "mongo", + "mson": "mson", + "nearley": "nearly", + "nim": "nim", + "nimble": "nimble", + "nix": "nix", + "nsis": "nsi", + "nfl": "nsi", + "nsl": "nsi", + "bridlensis": "nsi", + "nunjucks": "nunjucks", + "objective-c": "c", + "objective-cpp": "cpp", + "ocaml": "ocaml", + "ocamllex": "ocaml", + "menhir": "ocaml", + "openhab": "openHAB", + "pddl": "pddl", + "happenings": "pddl_happenings", + "plan": "pddl_plan", + "phoenix-heex": "eex", + "perl": "perl", + "perl6": "perl6", + "pgsql": "pgsql", + "php": "php", + "pine": "pine", + "pinescript": "pine", + "pip-requirements": "python", + "platformio-debug.disassembly": "platformio", + "platformio-debug.memoryview": "platformio", + "platformio-debug.asm": "platformio", + "plsql": "plsql", + "oracle": "plsql", + "polymer": "polymer", + "pony": "pony", + "postcss": "postcss", + "powershell": "powershell", + "prisma": "prisma", + "pde": "processinglang", + "abl": "progress", + "prolog": "prolog", + "prometheus": "prometheus", + "proto3": "protobuf", + "proto": "protobuf", + "jade": "pug", + "pug": "pug", + "puppet": "puppet", + "purescript": "purescript", + "pyret": "pyret", + "python": "python", + "qlik": "qlikview", + "qml": "qml", + "qsharp": "qsharp", + "r": "r", + "racket": "racket", + "raml": "raml", + "razor": "razor", + "aspnetcorerazor": "razor", + "javascriptreact": "reactjs", + "typescriptreact": "reactts", + "reason": "reason", + "red": "red", + "restructuredtext": "restructuredtext", + "rexx": "rexx", + "riot": "riot", + "rmd": "rmd", + "mdx": "markdownx", + "robot": "robotframework", + "ruby": "ruby", + "rust": "rust", + "san": "san", + "SAS": "sas", + "sbt": "sbt", + "scala": "scala", + "scilab": "scilab", + "vbscript": "script", + "scss": "scss", + "sdl": "sdlang", + "shaderlab": "shaderlab", + "shellscript": "shell", + "silverstripe": "silverstripe", + "eskip": "skipper", + "slang": "slang", + "slice": "slice", + "slim": "slim", + "smarty": "smarty", + "snort": "snort", + "solidity": "solidity", + "snippets": "vscode", + "sqf": "sqf", + "sql": "sql", + "squirrel": "squirrel", + "stan": "stan", + "stata": "stata", + "stencil": "stencil", + "stencil-html": "stencil", + "stylable": "stylable", + "source.css.styled": "styled", + "stylus": "stylus", + "svelte": "svelte", + "Swagger": "swagger", + "swagger": "swagger", + "swift": "swift", + "swig": "swig", + "cuda-cpp": "nvidia", + "systemd-unit-file": "systemd", + "systemverilog": "systemverilog", + "t4": "t4tt", + "tera": "tera", + "terraform": "terraform", + "tex": "latex", + "log": "log", + "dockercompose": "docker", + "latex": "latex", + "vue-directives": "vue", + "vue-injection-markdown": "vue", + "vue-interpolations": "vue", + "vue-sfc-style-variable-injection": "vue", + "bibtex": "latex", + "doctex": "tex", + "plaintext": "text", + "textile": "textile", + "toml": "toml", + "tt": "tt", + "ttcn": "ttcn", + "twig": "twig", + "typescript": "typescript", + "typoscript": "typo3", + "vb": "vb", + "vba": "vba", + "velocity": "velocity", + "verilog": "verilog", + "vhdl": "vhdl", + "viml": "vim", + "v": "vlang", + "volt": "volt", + "vue": "vue", + "wasm": "wasm", + "wat": "wasm", + "wenyan": "wenyan", + "wolfram": "wolfram", + "wurstlang": "wurst", + "wurst": "wurst", + "xmake": "xmake", + "xml": "xml", + "xquery": "xquery", + "xsl": "xml", + "yacc": "yacc", + "yaml": "yaml", + "yaml-tmlanguage": "yaml", + "yang": "yang", + "zig": "zig", + "vitest-snapshot": "vitest", + "instructions": "instructions", + "prompt": "prompt" + } + } +} diff --git a/packages/assets/svgs/ext/index.ts b/packages/assets/svgs/ext/index.ts index 5430cf2f5..615cb4a29 100644 --- a/packages/assets/svgs/ext/index.ts +++ b/packages/assets/svgs/ext/index.ts @@ -1,11 +1,21 @@ /* * This file exports a object which contains Different Kinds of Icons. */ -import { type FC as FunctionComponent, type LazyExoticComponent, type SVGProps } from 'react'; +import type { + FC as FunctionComponent, + LazyExoticComponent, + SVGProps, +} from "react"; -import * as Code from './Code'; -import * as Extras from './Extras'; +import * as Code from "./Code"; +import * as Extras from "./Extras"; export const LayeredIcons: Partial< - Record>>>> + Record< + string, + Record< + string, + LazyExoticComponent>> + > + > > = { Code, Extras }; diff --git a/packages/assets/util/index.ts b/packages/assets/util/index.ts index df76148ab..63f72d61d 100644 --- a/packages/assets/util/index.ts +++ b/packages/assets/util/index.ts @@ -7,21 +7,21 @@ export { beardedIconUrls } from "../svgs/ext/Extras/urls"; // Define a type for icon names. This filters out any names with underscores in them. // The use of 'never' is to make sure that icon types with underscores are not included. export type IconTypes = K extends `${string}_${string}` - ? never - : K; + ? never + : K; // Create a record of icon names that don't contain underscores. export const iconNames = Object.fromEntries( - Object.keys(icons) - .filter((key) => !key.includes("_")) // Filter out any keys with underscores - .map((key) => [key, key]), // Map key to [key, key] format + Object.keys(icons) + .filter((key) => !key.includes("_")) // Filter out any keys with underscores + .map((key) => [key, key]) // Map key to [key, key] format ) as Record; export type IconName = keyof typeof iconNames; export const getIconByName = (name: IconTypes, isDark?: boolean) => { - if (!isDark) name = (name + "_Light") as IconTypes; - return icons[name]; + if (!isDark) name = (name + "_Light") as IconTypes; + return icons[name]; }; /** @@ -33,52 +33,52 @@ export const getIconByName = (name: IconTypes, isDark?: boolean) => { * @param isDir - If true, the request is for a directory/folder icon. */ export const getIcon = ( - kind: string, - isDark?: boolean, - extension?: string | null, - isDir?: boolean, + kind: string, + isDark?: boolean, + extension?: string | null, + isDir?: boolean ) => { - // If the request is for a directory/folder, return the appropriate version. - if (isDir) return icons[isDark ? "Folder" : "Folder_Light"]; + // If the request is for a directory/folder, return the appropriate version. + if (isDir) return icons[isDark ? "Folder" : "Folder_Light"]; - // Default document icon. - let document: Extract = - "Document"; + // Default document icon. + let document: Extract = + "Document"; - // Modify the extension based on kind and theme (dark/light). - if (extension) extension = `${kind}_${extension.toLowerCase()}`; - if (!isDark) { - document = "Document_Light"; - if (extension) extension += "_Light"; - } + // Modify the extension based on kind and theme (dark/light). + if (extension) extension = `${kind}_${extension.toLowerCase()}`; + if (!isDark) { + document = "Document_Light"; + if (extension) extension += "_Light"; + } - const lightKind = kind + "_Light"; + const lightKind = kind + "_Light"; - // Select the icon based on the given parameters. - return icons[ - // 1. Check if the specific extension icon exists. - (extension && extension in icons - ? extension - : // 2. If in light mode, check if the specific kind in light exists. - !isDark && lightKind in icons - ? lightKind - : // 3. Check if a general kind icon exists. - kind in icons - ? kind - : // 4. Default to the document (or document light) icon. - document) as keyof typeof icons - ]; + // Select the icon based on the given parameters. + return icons[ + // 1. Check if the specific extension icon exists. + (extension && extension in icons + ? extension + : // 2. If in light mode, check if the specific kind in light exists. + !isDark && lightKind in icons + ? lightKind + : // 3. Check if a general kind icon exists. + kind in icons + ? kind + : // 4. Default to the document (or document light) icon. + document) as keyof typeof icons + ]; }; export const getLayeredIcon = (kind: string, extension?: string | null) => { - const iconKind = - LayeredIcons[ - // Check if specific kind exists. - kind && kind in LayeredIcons ? kind : "Extras" - ]; - return extension - ? iconKind?.[extension] || LayeredIcons["Extras"]?.[extension] - : null; + const iconKind = + LayeredIcons[ + // Check if specific kind exists. + kind && kind in LayeredIcons ? kind : "Extras" + ]; + return extension + ? iconKind?.[extension] || LayeredIcons["Extras"]?.[extension] + : null; }; /** @@ -89,27 +89,27 @@ export const getLayeredIcon = (kind: string, extension?: string | null) => { * @param fileName - Optional full filename for specific file name mappings */ export const getBeardedIcon = ( - extension?: string | null, - fileName?: string | null, + extension?: string | null, + fileName?: string | null ): string | null => { - if (!extension && !fileName) return null; + if (!(extension || fileName)) return null; - const mapping = beardedIconsMapping as { - fileExtensions: Record; - fileNames: Record; - }; + const mapping = beardedIconsMapping as { + fileExtensions: Record; + fileNames: Record; + }; - // Try filename match first (e.g., "package.json" -> "npm") - if (fileName && mapping.fileNames[fileName.toLowerCase()]) { - return mapping.fileNames[fileName.toLowerCase()]; - } - // Then try extension match (e.g., "ts" -> "typescript") - else if (extension) { - const ext = extension.toLowerCase().replace(/^\./, ""); // Remove leading dot if present - return mapping.fileExtensions[ext] || null; - } + // Try filename match first (e.g., "package.json" -> "npm") + if (fileName && mapping.fileNames[fileName.toLowerCase()]) { + return mapping.fileNames[fileName.toLowerCase()]; + } + // Then try extension match (e.g., "ts" -> "typescript") + if (extension) { + const ext = extension.toLowerCase().replace(/^\./, ""); // Remove leading dot if present + return mapping.fileExtensions[ext] || null; + } - return null; + return null; }; /** @@ -120,10 +120,10 @@ export const getBeardedIcon = ( * @param isDir - If true, returns the Folder20 icon */ export const getIcon20 = (kind: string, isDir?: boolean): string | null => { - if (isDir) { - return icons["Folder20" as keyof typeof icons] || null; - } + if (isDir) { + return icons["Folder20" as keyof typeof icons] || null; + } - const icon20Key = `${kind}20` as keyof typeof icons; - return icons[icon20Key] || null; + const icon20Key = `${kind}20` as keyof typeof icons; + return icons[icon20Key] || null; }; diff --git a/packages/config/app.tsconfig.json b/packages/config/app.tsconfig.json index e17622d5a..db80d623f 100644 --- a/packages/config/app.tsconfig.json +++ b/packages/config/app.tsconfig.json @@ -1,7 +1,7 @@ { - "extends": "./base.tsconfig.json", - "compilerOptions": { - "noEmit": true, - "emitDeclarationOnly": false - } + "extends": "./base.tsconfig.json", + "compilerOptions": { + "noEmit": true, + "emitDeclarationOnly": false + } } diff --git a/packages/config/base.tsconfig.json b/packages/config/base.tsconfig.json index f43c27ad2..c4a8229b0 100644 --- a/packages/config/base.tsconfig.json +++ b/packages/config/base.tsconfig.json @@ -1,20 +1,20 @@ { - "$schema": "https://json.schemastore.org/tsconfig", - "display": "Base", - "compilerOptions": { - "strict": true, - "jsx": "preserve", - "esModuleInterop": true, - "skipLibCheck": true, - "preserveWatchOutput": true, - "forceConsistentCasingInFileNames": true, - "allowSyntheticDefaultImports": true, - "composite": true, - "declaration": true, - "emitDeclarationOnly": true, - "moduleResolution": "node", - "resolveJsonModule": true, - "module": "ESNext", - "target": "ESNext" - } + "$schema": "https://json.schemastore.org/tsconfig", + "display": "Base", + "compilerOptions": { + "strict": true, + "jsx": "preserve", + "esModuleInterop": true, + "skipLibCheck": true, + "preserveWatchOutput": true, + "forceConsistentCasingInFileNames": true, + "allowSyntheticDefaultImports": true, + "composite": true, + "declaration": true, + "emitDeclarationOnly": true, + "moduleResolution": "node", + "resolveJsonModule": true, + "module": "ESNext", + "target": "ESNext" + } } diff --git a/packages/config/package.json b/packages/config/package.json index 86e7747ec..42fa95a56 100644 --- a/packages/config/package.json +++ b/packages/config/package.json @@ -1,8 +1,8 @@ { - "name": "@sd/config", - "version": "0.0.0", - "private": true, - "exports": { - "./*": "./*" - } + "name": "@sd/config", + "version": "0.0.0", + "private": true, + "exports": { + "./*": "./*" + } } diff --git a/packages/config/tsconfig.json b/packages/config/tsconfig.json index 93dcddaa5..bffaeec74 100644 --- a/packages/config/tsconfig.json +++ b/packages/config/tsconfig.json @@ -1,7 +1,8 @@ { - "extends": "./base.tsconfig.json", - "compilerOptions": { - "noEmit": true - }, - "include": ["."] + "extends": "./base.tsconfig.json", + "compilerOptions": { + "noEmit": true, + "strictNullChecks": true + }, + "include": ["."] } diff --git a/packages/interface/package.json b/packages/interface/package.json index a7c733de4..0d39a8082 100644 --- a/packages/interface/package.json +++ b/packages/interface/package.json @@ -1,66 +1,66 @@ { - "name": "@sd/interface", - "version": "0.0.0", - "private": true, - "license": "GPL-3.0-only", - "main": "src/index.tsx", - "types": "src/index.tsx", - "sideEffects": false, - "exports": { - ".": "./src/index.tsx", - "./app": "./src/App.tsx", - "./platform": "./src/platform.tsx", - "./styles.css": "./src/styles.css" - }, - "scripts": { - "lint": "eslint src --cache", - "typecheck": "tsc -b" - }, - "dependencies": { - "@dnd-kit/core": "^6.3.1", - "@dnd-kit/sortable": "^10.0.0", - "@dnd-kit/utilities": "^3.2.2", - "@mkkellogg/gaussian-splats-3d": "^0.4.7", - "@phosphor-icons/react": "^2.1.0", - "@radix-ui/react-dialog": "^1.0.5", - "@radix-ui/react-dropdown-menu": "^2.0.6", - "@radix-ui/react-tooltip": "^1.0.7", - "@react-three/drei": "^9.122.0", - "@react-three/fiber": "^9.4.2", - "@sd/assets": "workspace:*", - "@sd/ts-client": "workspace:*", - "@sd/ui": "workspace:*", - "@tanstack/react-query": "^5.90.7", - "@tanstack/react-query-devtools": "^5.90.2", - "@tanstack/react-table": "^8.21.3", - "@tanstack/react-virtual": "^3.13.12", - "@types/d3": "^7.4.3", - "class-variance-authority": "^0.7.0", - "clsx": "^2.0.0", - "d3": "^7.9.0", - "framer-motion": "^12.23.24", - "ogl": "^1.0.11", - "prismjs": "^1.30.0", - "qrcode": "^1.5.4", - "react": "^19.0.0", - "react-dom": "^19.0.0", - "react-hook-form": "^7.53.2", - "react-masonry-css": "^1.0.16", - "react-router-dom": "^6.20.1", - "react-scan": "^0.4.3", - "react-selecto": "^1.26.3", - "rooks": "^9.3.0", - "sonner": "^1.0.3", - "tailwind-merge": "^1.14.0", - "three": "^0.160.0", - "zod": "^3.23", - "zustand": "^5.0.8" - }, - "devDependencies": { - "@types/prismjs": "^1.26.5", - "@types/react": "npm:types-react@rc", - "@types/react-dom": "npm:types-react-dom@rc", - "@types/three": "^0.182.0", - "typescript": "^5.6.2" - } + "name": "@sd/interface", + "version": "0.0.0", + "private": true, + "license": "GPL-3.0-only", + "main": "src/index.tsx", + "types": "src/index.tsx", + "sideEffects": false, + "exports": { + ".": "./src/index.tsx", + "./app": "./src/App.tsx", + "./platform": "./src/platform.tsx", + "./styles.css": "./src/styles.css" + }, + "scripts": { + "lint": "eslint src --cache", + "typecheck": "tsc -b" + }, + "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", + "@mkkellogg/gaussian-splats-3d": "^0.4.7", + "@phosphor-icons/react": "^2.1.0", + "@radix-ui/react-dialog": "^1.0.5", + "@radix-ui/react-dropdown-menu": "^2.0.6", + "@radix-ui/react-tooltip": "^1.0.7", + "@react-three/drei": "^9.122.0", + "@react-three/fiber": "^9.4.2", + "@sd/assets": "workspace:*", + "@sd/ts-client": "workspace:*", + "@sd/ui": "workspace:*", + "@tanstack/react-query": "^5.90.7", + "@tanstack/react-query-devtools": "^5.90.2", + "@tanstack/react-table": "^8.21.3", + "@tanstack/react-virtual": "^3.13.12", + "@types/d3": "^7.4.3", + "class-variance-authority": "^0.7.0", + "clsx": "^2.0.0", + "d3": "^7.9.0", + "framer-motion": "^12.23.24", + "ogl": "^1.0.11", + "prismjs": "^1.30.0", + "qrcode": "^1.5.4", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-hook-form": "^7.53.2", + "react-masonry-css": "^1.0.16", + "react-router-dom": "^6.20.1", + "react-scan": "^0.4.3", + "react-selecto": "^1.26.3", + "rooks": "^9.3.0", + "sonner": "^1.0.3", + "tailwind-merge": "^1.14.0", + "three": "^0.160.0", + "zod": "^3.23", + "zustand": "^5.0.8" + }, + "devDependencies": { + "@types/prismjs": "^1.26.5", + "@types/react": "npm:types-react@rc", + "@types/react-dom": "npm:types-react-dom@rc", + "@types/three": "^0.182.0", + "typescript": "^5.6.2" + } } diff --git a/packages/interface/src/Shell.tsx b/packages/interface/src/Shell.tsx index 260492225..e72073048 100644 --- a/packages/interface/src/Shell.tsx +++ b/packages/interface/src/Shell.tsx @@ -1,33 +1,35 @@ -import { SpacedriveProvider, type SpacedriveClient } from "./contexts/SpacedriveContext"; -import { ServerProvider } from "./contexts/ServerContext"; +import { Dialogs } from "@sd/ui"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; import { RouterProvider } from "react-router-dom"; -import { Dialogs } from "@sd/ui"; -import { ShellLayout } from "./ShellLayout"; -import { explorerRoutes } from "./router"; -import { useDaemonStatus } from "./hooks/useDaemonStatus"; +import { DndProvider } from "./components/DndProvider"; import { DaemonDisconnectedOverlay } from "./components/overlays/DaemonDisconnectedOverlay"; import { DaemonStartupOverlay } from "./components/overlays/DaemonStartupOverlay"; -import { DndProvider } from "./components/DndProvider"; import { - TabManagerProvider, - TabKeyboardHandler, - useTabManager, + TabKeyboardHandler, + TabManagerProvider, + useTabManager, } from "./components/TabManager"; import { usePlatform } from "./contexts/PlatformContext"; +import { ServerProvider } from "./contexts/ServerContext"; +import { + type SpacedriveClient, + SpacedriveProvider, +} from "./contexts/SpacedriveContext"; +import { useDaemonStatus } from "./hooks/useDaemonStatus"; +import { explorerRoutes } from "./router"; interface ShellProps { - client: SpacedriveClient; + client: SpacedriveClient; } function ShellWithTabs() { - const { router } = useTabManager(); + const { router } = useTabManager(); - return ( - - - - ); + return ( + + + + ); } /** @@ -35,64 +37,62 @@ function ShellWithTabs() { * This avoids the connection storm where hundreds of queries try to execute before daemon is ready. */ function ShellWithDaemonCheck() { - const daemonStatus = useDaemonStatus(); - const { isConnected, isStarting } = daemonStatus; + const daemonStatus = useDaemonStatus(); + const { isConnected, isStarting } = daemonStatus; - return ( - <> - {isConnected ? ( - // Daemon connected - render full app - <> - - - - - - - - ) : ( - // Daemon not connected - show appropriate overlay - <> - - {!isStarting && ( - - )} - - )} - - ); + return ( + <> + {isConnected ? ( + // Daemon connected - render full app + <> + + + + + + + + ) : ( + // Daemon not connected - show appropriate overlay + <> + + {!isStarting && ( + + )} + + )} + + ); } export function Shell({ client }: ShellProps) { - const platform = usePlatform(); - const isTauri = platform.platform === "tauri"; + const platform = usePlatform(); + const isTauri = platform.platform === "tauri"; - return ( - - - {isTauri ? ( - // Tauri: Wait for daemon connection before rendering content - - ) : ( - // Web: Render immediately (daemon connection handled differently) - <> - - - - - - - - )} - - - ); -} \ No newline at end of file + return ( + + + {isTauri ? ( + // Tauri: Wait for daemon connection before rendering content + + ) : ( + // Web: Render immediately (daemon connection handled differently) + <> + + + + + + + + )} + + + ); +} diff --git a/packages/interface/src/ShellLayout.tsx b/packages/interface/src/ShellLayout.tsx index 4cb44779f..d6165f739 100644 --- a/packages/interface/src/ShellLayout.tsx +++ b/packages/interface/src/ShellLayout.tsx @@ -1,238 +1,234 @@ -import { Outlet, useLocation, useParams } from "react-router-dom"; -import { useEffect, useMemo } from "react"; -import { motion, AnimatePresence } from "framer-motion"; -import { TopBarProvider, TopBar } from "./TopBar"; -import { ExplorerProvider, useExplorer } from "./routes/explorer"; -import { SelectionProvider } from "./routes/explorer/SelectionContext"; -import { KeyboardHandler } from "./routes/explorer/KeyboardHandler"; -import { TagAssignmentMode } from "./routes/explorer/TagAssignmentMode"; -import { SpacesSidebar } from "./components/SpacesSidebar"; -import { QuickPreviewController, QuickPreviewSyncer, PREVIEW_LAYER_ID } from "./components/QuickPreview"; -import { useNormalizedQuery } from "./contexts/SpacedriveContext"; -import { usePlatform } from "./contexts/PlatformContext"; import type { Location } from "@sd/ts-client"; +import { AnimatePresence, motion } from "framer-motion"; +import { useEffect, useMemo } from "react"; +import { Outlet, useLocation, useParams } from "react-router-dom"; import { Inspector } from "./components/Inspector/Inspector"; -import { TabBar, TabNavigationSync, TabDefaultsSync } from "./components/TabManager"; +import { + PREVIEW_LAYER_ID, + QuickPreviewController, + QuickPreviewSyncer, +} from "./components/QuickPreview"; +import { SpacesSidebar } from "./components/SpacesSidebar"; +import { + TabBar, + TabDefaultsSync, + TabNavigationSync, +} from "./components/TabManager"; +import { usePlatform } from "./contexts/PlatformContext"; +import { useNormalizedQuery } from "./contexts/SpacedriveContext"; +import { ExplorerProvider, useExplorer } from "./routes/explorer"; +import { KeyboardHandler } from "./routes/explorer/KeyboardHandler"; +import { SelectionProvider } from "./routes/explorer/SelectionContext"; +import { TagAssignmentMode } from "./routes/explorer/TagAssignmentMode"; +import { TopBar, TopBarProvider } from "./TopBar"; function ShellLayoutContent() { - const location = useLocation(); - const params = useParams(); - const platform = usePlatform(); - const { - sidebarVisible, - inspectorVisible, - setInspectorVisible, - quickPreviewFileId, - tagModeActive, - setTagModeActive, - viewMode, - currentPath, - } = useExplorer(); + const location = useLocation(); + const params = useParams(); + const platform = usePlatform(); + const { + sidebarVisible, + inspectorVisible, + setInspectorVisible, + quickPreviewFileId, + tagModeActive, + setTagModeActive, + viewMode, + currentPath, + } = useExplorer(); - // Check if we're on Overview (hide inspector) or in Knowledge view (has its own inspector) - const isOverview = location.pathname === "/"; - const isKnowledgeView = viewMode === "knowledge"; + // Check if we're on Overview (hide inspector) or in Knowledge view (has its own inspector) + const isOverview = location.pathname === "/"; + const isKnowledgeView = viewMode === "knowledge"; - // Fetch locations to get current location info - const locationsQuery = useNormalizedQuery< - null, - { locations: Location[] } - >({ - wireMethod: "query:locations.list", - input: null, - resourceType: "location", - }); + // Fetch locations to get current location info + const locationsQuery = useNormalizedQuery({ + wireMethod: "query:locations.list", + input: null, + resourceType: "location", + }); - // Get current location if we're on a location route or browsing within a location - const currentLocation = useMemo(() => { - const locations = locationsQuery.data?.locations || []; + // Get current location if we're on a location route or browsing within a location + const currentLocation = useMemo(() => { + const locations = locationsQuery.data?.locations || []; - // First try to match by route param (for /location/:id routes) - if (params.locationId) { - const loc = locations.find((loc) => loc.id === params.locationId); - if (loc) return loc; - } + // First try to match by route param (for /location/:id routes) + if (params.locationId) { + const loc = locations.find((loc) => loc.id === params.locationId); + if (loc) return loc; + } - // If no route match, try to find location by matching current path - if (currentPath && "Physical" in currentPath) { - const pathStr = currentPath.Physical.path; - // Find location with longest matching prefix - return ( - locations - .filter((loc) => { - if (!loc.sd_path || !("Physical" in loc.sd_path)) - return false; - const locPath = loc.sd_path.Physical.path; - return pathStr.startsWith(locPath); - }) - .sort((a, b) => { - const aPath = - "Physical" in a.sd_path! - ? a.sd_path!.Physical.path - : ""; - const bPath = - "Physical" in b.sd_path! - ? b.sd_path!.Physical.path - : ""; - return bPath.length - aPath.length; - })[0] || null - ); - } + // If no route match, try to find location by matching current path + if (currentPath && "Physical" in currentPath) { + const pathStr = currentPath.Physical.path; + // Find location with longest matching prefix + return ( + locations + .filter((loc) => { + if (!(loc.sd_path && "Physical" in loc.sd_path)) return false; + const locPath = loc.sd_path.Physical.path; + return pathStr.startsWith(locPath); + }) + .sort((a, b) => { + const aPath = + "Physical" in a.sd_path! ? a.sd_path!.Physical.path : ""; + const bPath = + "Physical" in b.sd_path! ? b.sd_path!.Physical.path : ""; + return bPath.length - aPath.length; + })[0] || null + ); + } - return null; - }, [params.locationId, locationsQuery.data, currentPath]); + return null; + }, [params.locationId, locationsQuery.data, currentPath]); - useEffect(() => { - // Listen for inspector window close events - if (!platform.onWindowEvent) return; + useEffect(() => { + // Listen for inspector window close events + if (!platform.onWindowEvent) return; - let unlisten: (() => void) | undefined; + let unlisten: (() => void) | undefined; - (async () => { - try { - unlisten = await platform.onWindowEvent( - "inspector-window-closed", - () => { - // Show embedded inspector when floating window closes - setInspectorVisible(true); - }, - ); - } catch (err) { - console.error("Failed to setup inspector close listener:", err); - } - })(); + (async () => { + try { + unlisten = await platform.onWindowEvent( + "inspector-window-closed", + () => { + // Show embedded inspector when floating window closes + setInspectorVisible(true); + } + ); + } catch (err) { + console.error("Failed to setup inspector close listener:", err); + } + })(); - return () => { - unlisten?.(); - }; - }, [platform, setInspectorVisible]); + return () => { + unlisten?.(); + }; + }, [platform, setInspectorVisible]); - const handlePopOutInspector = async () => { - if (!platform.showWindow) return; + const handlePopOutInspector = async () => { + if (!platform.showWindow) return; - try { - await platform.showWindow({ - type: "Inspector", - item_id: null, - }); - // Hide the embedded inspector when popped out - setInspectorVisible(false); - } catch (err) { - console.error("Failed to pop out inspector:", err); - } - }; + try { + await platform.showWindow({ + type: "Inspector", + item_id: null, + }); + // Hide the embedded inspector when popped out + setInspectorVisible(false); + } catch (err) { + console.error("Failed to pop out inspector:", err); + } + }; - const isPreviewActive = !!quickPreviewFileId; + const isPreviewActive = !!quickPreviewFileId; - return ( -
    - {/* Preview layer - portal target for fullscreen preview, sits between content and sidebar/inspector */} -
    + return ( +
    + {/* Preview layer - portal target for fullscreen preview, sits between content and sidebar/inspector */} +
    - + - {/* Main content area with sidebar and content */} -
    - - {sidebarVisible && ( - - - - )} - + {/* Main content area with sidebar and content */} +
    + + {sidebarVisible && ( + + + + )} + - {/* Content area with tabs - positioned between sidebar and inspector */} -
    - {/* Tab Bar - nested inside content area like Finder */} - + {/* Content area with tabs - positioned between sidebar and inspector */} +
    + {/* Tab Bar - nested inside content area like Finder */} + - {/* Router content renders here */} -
    - + {/* Router content renders here */} +
    + - {/* Tag Assignment Mode - positioned at bottom of main content area */} - setTagModeActive(false)} - /> -
    -
    + {/* Tag Assignment Mode - positioned at bottom of main content area */} + setTagModeActive(false)} + /> +
    +
    - {/* Keyboard handler (invisible, doesn't cause parent rerenders) */} - + {/* Keyboard handler (invisible, doesn't cause parent rerenders) */} + - {/* Syncs selection to QuickPreview - isolated to prevent frame rerenders */} - + {/* Syncs selection to QuickPreview - isolated to prevent frame rerenders */} + - - {/* Hide inspector on Overview screen and Knowledge view (has its own) */} - {inspectorVisible && !isOverview && !isKnowledgeView && ( - -
    - -
    -
    - )} -
    -
    + + {/* Hide inspector on Overview screen and Knowledge view (has its own) */} + {inspectorVisible && !isOverview && !isKnowledgeView && ( + +
    + +
    +
    + )} +
    +
    - {/* Quick Preview - isolated component to prevent frame rerenders on selection change */} - -
    - ); + {/* Quick Preview - isolated component to prevent frame rerenders on selection change */} + +
    + ); } export function ShellLayout() { - return ( - - - - {/* Sync tab navigation and defaults with router */} - - - - - - - ); -} \ No newline at end of file + return ( + + + + {/* Sync tab navigation and defaults with router */} + + + + + + + ); +} diff --git a/packages/interface/src/TopBar/Context.tsx b/packages/interface/src/TopBar/Context.tsx index 0bef11f27..0c8faea91 100644 --- a/packages/interface/src/TopBar/Context.tsx +++ b/packages/interface/src/TopBar/Context.tsx @@ -1,196 +1,218 @@ -import { createContext, useContext, useState, useCallback, useRef } from "react"; +import { + createContext, + useCallback, + useContext, + useRef, + useState, +} from "react"; export type TopBarPriority = "high" | "normal" | "low"; export type TopBarPosition = "left" | "center" | "right"; export interface TopBarItem { - id: string; - label: string; - priority: TopBarPriority; - position: TopBarPosition; - width: number; - onClick?: () => void; - element: React.ReactNode; - elementVersion: number; - submenuContent?: React.ReactNode; // Optional: custom submenu content for overflow + id: string; + label: string; + priority: TopBarPriority; + position: TopBarPosition; + width: number; + onClick?: () => void; + element: React.ReactNode; + elementVersion: number; + submenuContent?: React.ReactNode; // Optional: custom submenu content for overflow } interface TopBarContextValue { - items: Map; - visibleItems: Set; - overflowItems: Map; + items: Map; + visibleItems: Set; + overflowItems: Map; - registerItem: (item: Omit) => void; - unregisterItem: (id: string) => void; - updateItemWidth: (id: string, width: number) => void; + registerItem: (item: Omit) => void; + unregisterItem: (id: string) => void; + updateItemWidth: (id: string, width: number) => void; - leftContainerRef: React.RefObject | null; - rightContainerRef: React.RefObject | null; - setLeftContainerRef: (ref: React.RefObject) => void; - setRightContainerRef: (ref: React.RefObject) => void; + leftContainerRef: React.RefObject | null; + rightContainerRef: React.RefObject | null; + setLeftContainerRef: (ref: React.RefObject) => void; + setRightContainerRef: (ref: React.RefObject) => void; - recalculate: () => void; + recalculate: () => void; } const TopBarContext = createContext(null); export function TopBarProvider({ children }: { children: React.ReactNode }) { - const [items, setItems] = useState>(new Map()); - const [visibleItems, setVisibleItemsState] = useState>(new Set()); - const [overflowItems, setOverflowItemsState] = useState>(new Map()); - const [leftContainerRef, setLeftContainerRef] = useState | null>(null); - const [rightContainerRef, setRightContainerRef] = useState | null>(null); - const [recalculationTrigger, setRecalculationTrigger] = useState(0); - const elementsRef = useRef>(new Map()); - const submenuContentRef = useRef>(new Map()); + const [items, setItems] = useState>(new Map()); + const [visibleItems, setVisibleItemsState] = useState>(new Set()); + const [overflowItems, setOverflowItemsState] = useState< + Map + >(new Map()); + const [leftContainerRef, setLeftContainerRef] = + useState | null>(null); + const [rightContainerRef, setRightContainerRef] = + useState | null>(null); + const [recalculationTrigger, setRecalculationTrigger] = useState(0); + const elementsRef = useRef>(new Map()); + const submenuContentRef = useRef>(new Map()); - const registerItem = useCallback((item: Omit) => { - // Store element and submenuContent in refs (don't trigger re-render) - elementsRef.current.set(item.id, item.element); - if (item.submenuContent) { - submenuContentRef.current.set(item.id, item.submenuContent); - } + const registerItem = useCallback( + (item: Omit) => { + // Store element and submenuContent in refs (don't trigger re-render) + elementsRef.current.set(item.id, item.element); + if (item.submenuContent) { + submenuContentRef.current.set(item.id, item.submenuContent); + } - setItems(prev => { - const existingItem = prev.get(item.id); + setItems((prev) => { + const existingItem = prev.get(item.id); - // Check if structural properties changed - const structureChanged = !existingItem || - existingItem.label !== item.label || - existingItem.priority !== item.priority || - existingItem.position !== item.position || - existingItem.onClick !== item.onClick; + // Check if structural properties changed + const structureChanged = + !existingItem || + existingItem.label !== item.label || + existingItem.priority !== item.priority || + existingItem.position !== item.position || + existingItem.onClick !== item.onClick; - // Always create new Map so consumers re-render with updated element/submenuContent - const newItems = new Map(prev); + // Always create new Map so consumers re-render with updated element/submenuContent + const newItems = new Map(prev); - if (!structureChanged) { - // Only element/submenuContent changed - update without triggering recalculation - newItems.set(item.id, { - ...existingItem, - element: item.element, - submenuContent: item.submenuContent, - }); - } else { - // Structure changed - full update and trigger recalculation - newItems.set(item.id, { - ...item, - width: existingItem?.width || 0, - elementVersion: 0 - }); + if (structureChanged) { + // Structure changed - full update and trigger recalculation + newItems.set(item.id, { + ...item, + width: existingItem?.width || 0, + elementVersion: 0, + }); - // Initially add to visible so it can be measured - if (!existingItem) { - setVisibleItemsState(prev2 => { - const newVisible = new Set(prev2); - newVisible.add(item.id); - return newVisible; - }); - } - } + // Initially add to visible so it can be measured + if (!existingItem) { + setVisibleItemsState((prev2) => { + const newVisible = new Set(prev2); + newVisible.add(item.id); + return newVisible; + }); + } + } else { + // Only element/submenuContent changed - update without triggering recalculation + newItems.set(item.id, { + ...existingItem, + element: item.element, + submenuContent: item.submenuContent, + }); + } - return newItems; - }); - }, []); + return newItems; + }); + }, + [] + ); - const unregisterItem = useCallback((id: string) => { - setItems(prev => { - const newItems = new Map(prev); - newItems.delete(id); - return newItems; - }); - setVisibleItemsState(prev => { - const newVisible = new Set(prev); - newVisible.delete(id); - return newVisible; - }); - }, []); + const unregisterItem = useCallback((id: string) => { + setItems((prev) => { + const newItems = new Map(prev); + newItems.delete(id); + return newItems; + }); + setVisibleItemsState((prev) => { + const newVisible = new Set(prev); + newVisible.delete(id); + return newVisible; + }); + }, []); - const updateItemWidth = useCallback((id: string, width: number) => { - setItems(prev => { - const item = prev.get(id); - if (!item || item.width === width) return prev; + const updateItemWidth = useCallback((id: string, width: number) => { + setItems((prev) => { + const item = prev.get(id); + if (!item || item.width === width) return prev; - const newItems = new Map(prev); - newItems.set(id, { ...item, width }); + const newItems = new Map(prev); + newItems.set(id, { ...item, width }); - // Trigger recalculation only when we actually update - setRecalculationTrigger(t => t + 1); + // Trigger recalculation only when we actually update + setRecalculationTrigger((t) => t + 1); - return newItems; - }); - }, []); + return newItems; + }); + }, []); - const recalculate = useCallback(() => { - setRecalculationTrigger(prev => prev + 1); - }, []); + const recalculate = useCallback(() => { + setRecalculationTrigger((prev) => prev + 1); + }, []); - return ( - - - {children} - - - ); + return ( + + + {children} + + + ); } // Internal context for state setters interface TopBarInternalContextValue { - setVisibleItems: React.Dispatch>>; - setOverflowItems: React.Dispatch>>; - recalculationTrigger: number; + setVisibleItems: React.Dispatch>>; + setOverflowItems: React.Dispatch< + React.SetStateAction> + >; + recalculationTrigger: number; } -const TopBarInternalContext = createContext(null); +const TopBarInternalContext = createContext( + null +); function TopBarInternalProvider({ - children, - setVisibleItems, - setOverflowItems, - recalculationTrigger, + children, + setVisibleItems, + setOverflowItems, + recalculationTrigger, }: { - children: React.ReactNode; - setVisibleItems: React.Dispatch>>; - setOverflowItems: React.Dispatch>>; - recalculationTrigger: number; + children: React.ReactNode; + setVisibleItems: React.Dispatch>>; + setOverflowItems: React.Dispatch< + React.SetStateAction> + >; + recalculationTrigger: number; }) { - return ( - - {children} - - ); + return ( + + {children} + + ); } export function useTopBar() { - const context = useContext(TopBarContext); - if (!context) { - throw new Error("useTopBar must be used within TopBarProvider"); - } - return context; + const context = useContext(TopBarContext); + if (!context) { + throw new Error("useTopBar must be used within TopBarProvider"); + } + return context; } export function useTopBarInternal() { - const context = useContext(TopBarInternalContext); - if (!context) { - throw new Error("useTopBarInternal must be used within TopBarProvider"); - } - return context; -} \ No newline at end of file + const context = useContext(TopBarInternalContext); + if (!context) { + throw new Error("useTopBarInternal must be used within TopBarProvider"); + } + return context; +} diff --git a/packages/interface/src/TopBar/Item.tsx b/packages/interface/src/TopBar/Item.tsx index 61b3af3b7..470f4b56d 100644 --- a/packages/interface/src/TopBar/Item.tsx +++ b/packages/interface/src/TopBar/Item.tsx @@ -1,53 +1,62 @@ -import { useEffect, useRef, createContext, useContext } from "react"; -import { useTopBar, TopBarPriority } from "./Context"; +import { createContext, useContext, useEffect } from "react"; +import { type TopBarPriority, useTopBar } from "./Context"; const PositionContext = createContext<"left" | "center" | "right">("left"); export function useTopBarPosition() { - return useContext(PositionContext); + return useContext(PositionContext); } export { PositionContext }; interface TopBarItemProps { - id: string; - label: string; - priority?: TopBarPriority; - onClick?: () => void; - children: React.ReactNode; - submenuContent?: React.ReactNode; + id: string; + label: string; + priority?: TopBarPriority; + onClick?: () => void; + children: React.ReactNode; + submenuContent?: React.ReactNode; } export function TopBarItem({ - id, - label, - priority = "normal", - onClick, - children, - submenuContent, + id, + label, + priority = "normal", + onClick, + children, + submenuContent, }: TopBarItemProps) { - const { registerItem, unregisterItem } = useTopBar(); - const position = useTopBarPosition(); + const { registerItem, unregisterItem } = useTopBar(); + const position = useTopBarPosition(); - // Register on mount, update when props change, unregister on unmount - // Note: children and submenuContent should be memoized by parent to prevent infinite loops - useEffect(() => { - registerItem({ - id, - label, - priority, - position, - onClick, - element: children, - submenuContent, - }); - }, [id, label, priority, position, onClick, registerItem, children, submenuContent]); + // Register on mount, update when props change, unregister on unmount + // Note: children and submenuContent should be memoized by parent to prevent infinite loops + useEffect(() => { + registerItem({ + id, + label, + priority, + position, + onClick, + element: children, + submenuContent, + }); + }, [ + id, + label, + priority, + position, + onClick, + registerItem, + children, + submenuContent, + ]); - // Unregister on unmount - useEffect(() => { - return () => unregisterItem(id); - }, [id, unregisterItem]); + // Unregister on unmount + useEffect(() => { + return () => unregisterItem(id); + }, [id, unregisterItem]); - // Don't render anything - items are rendered by TopBarSection - return null; -} \ No newline at end of file + // Don't render anything - items are rendered by TopBarSection + return null; +} diff --git a/packages/interface/src/TopBar/OverflowMenu.tsx b/packages/interface/src/TopBar/OverflowMenu.tsx index 19a5e102e..29c34bdcc 100644 --- a/packages/interface/src/TopBar/OverflowMenu.tsx +++ b/packages/interface/src/TopBar/OverflowMenu.tsx @@ -1,68 +1,62 @@ -import { useState } from "react"; import { DotsThree } from "@phosphor-icons/react"; import * as DropdownMenu from "@radix-ui/react-dropdown-menu"; import { TopBarButton } from "@sd/ui"; -import { TopBarItem } from "./Context"; +import { useState } from "react"; +import type { TopBarItem } from "./Context"; interface OverflowButtonProps { - items: TopBarItem[]; + items: TopBarItem[]; } export function OverflowButton({ items }: OverflowButtonProps) { - const [isOpen, setIsOpen] = useState(false); + const [isOpen, setIsOpen] = useState(false); - if (items.length === 0) return null; + if (items.length === 0) return null; - return ( - - - - + return ( + + + + - - - {items.map((item) => { - const isSimpleAction = !!item.onClick; - const hasSubmenu = !isSimpleAction; + + + {items.map((item) => { + const isSimpleAction = !!item.onClick; + const hasSubmenu = !isSimpleAction; - if (isSimpleAction) { - return ( - item.onClick?.()} - className="px-3 py-2 text-sm text-menu-ink hover:bg-app-hover/50 transition-colors outline-none cursor-pointer" - > - {item.label} - - ); - } + if (isSimpleAction) { + return ( + item.onClick?.()} + > + {item.label} + + ); + } - return ( - - - {item.label} - - - - - {item.submenuContent || item.element} - - - - ); - })} - - - - ); -} \ No newline at end of file + return ( + + + {item.label} + + + + + {item.submenuContent || item.element} + + + + ); + })} + + + + ); +} diff --git a/packages/interface/src/TopBar/Portal.tsx b/packages/interface/src/TopBar/Portal.tsx index af154f6bc..21f7aa212 100644 --- a/packages/interface/src/TopBar/Portal.tsx +++ b/packages/interface/src/TopBar/Portal.tsx @@ -1,29 +1,27 @@ import { PositionContext } from "./Item"; interface TopBarPortalProps { - left?: React.ReactNode; - center?: React.ReactNode; - right?: React.ReactNode; + left?: React.ReactNode; + center?: React.ReactNode; + right?: React.ReactNode; } export function TopBarPortal({ left, center, right }: TopBarPortalProps) { - return ( - <> - {left && ( - - {left} - - )} - {center && ( - - {center} - - )} - {right && ( - - {right} - - )} - - ); -} \ No newline at end of file + return ( + <> + {left && ( + {left} + )} + {center && ( + + {center} + + )} + {right && ( + + {right} + + )} + + ); +} diff --git a/packages/interface/src/TopBar/Section.tsx b/packages/interface/src/TopBar/Section.tsx index 30d35fc74..102f7cc4c 100644 --- a/packages/interface/src/TopBar/Section.tsx +++ b/packages/interface/src/TopBar/Section.tsx @@ -1,79 +1,96 @@ -import { useRef, useEffect, useLayoutEffect, Fragment } from "react"; -import { useTopBar, TopBarPosition } from "./Context"; +import { useLayoutEffect, useRef } from "react"; +import { type TopBarPosition, useTopBar } from "./Context"; import { OverflowButton } from "./OverflowMenu"; interface TopBarSectionProps { - position: TopBarPosition; + position: TopBarPosition; } -function ItemWrapper({ id, children }: { id: string; children: React.ReactNode }) { - const ref = useRef(null); - const { updateItemWidth } = useTopBar(); - const lastWidthRef = useRef(0); +function ItemWrapper({ + id, + children, +}: { + id: string; + children: React.ReactNode; +}) { + const ref = useRef(null); + const { updateItemWidth } = useTopBar(); + const lastWidthRef = useRef(0); - useLayoutEffect(() => { - const element = ref.current; - if (!element) return; + useLayoutEffect(() => { + const element = ref.current; + if (!element) return; - const updateWidth = () => { - const width = element.offsetWidth; - if (width !== lastWidthRef.current) { - lastWidthRef.current = width; - updateItemWidth(id, width); - } - }; + const updateWidth = () => { + const width = element.offsetWidth; + if (width !== lastWidthRef.current) { + lastWidthRef.current = width; + updateItemWidth(id, width); + } + }; - // Initial measurement - updateWidth(); + // Initial measurement + updateWidth(); - // Observe size changes - const resizeObserver = new ResizeObserver(() => { - updateWidth(); - }); + // Observe size changes + const resizeObserver = new ResizeObserver(() => { + updateWidth(); + }); - resizeObserver.observe(element); + resizeObserver.observe(element); - return () => { - resizeObserver.disconnect(); - }; - }, [id, updateItemWidth]); + return () => { + resizeObserver.disconnect(); + }; + }, [id, updateItemWidth]); - return ( -
    - {children} -
    - ); + return ( +
    + {children} +
    + ); } export function TopBarSection({ position }: TopBarSectionProps) { - const containerRef = useRef(null); - const { items, visibleItems, overflowItems, setLeftContainerRef, setRightContainerRef } = useTopBar(); + const containerRef = useRef(null); + const { + items, + visibleItems, + overflowItems, + setLeftContainerRef, + setRightContainerRef, + } = useTopBar(); - useLayoutEffect(() => { - if (position === "left") { - setLeftContainerRef(containerRef); - } else if (position === "right") { - setRightContainerRef(containerRef); - } - }, [position, setLeftContainerRef, setRightContainerRef]); + useLayoutEffect(() => { + if (position === "left") { + setLeftContainerRef(containerRef); + } else if (position === "right") { + setRightContainerRef(containerRef); + } + }, [position, setLeftContainerRef, setRightContainerRef]); - const positionItems = Array.from(items.values()).filter((item) => item.position === position); + const positionItems = Array.from(items.values()).filter( + (item) => item.position === position + ); - const visible = positionItems.filter((item) => visibleItems.has(item.id)); - const overflow = overflowItems.get(position) || []; + const visible = positionItems.filter((item) => visibleItems.has(item.id)); + const overflow = overflowItems.get(position) || []; - const containerClass = position === "center" - ? "flex-1 flex items-center justify-center gap-2" - : "flex items-center gap-2"; + const containerClass = + position === "center" + ? "flex-1 flex items-center justify-center gap-2" + : "flex items-center gap-2"; - return ( -
    - {visible.map((item) => ( - - {item.element} - - ))} - {overflow.length > 0 && position !== "center" && } -
    - ); -} \ No newline at end of file + return ( +
    + {visible.map((item) => ( + + {item.element} + + ))} + {overflow.length > 0 && position !== "center" && ( + + )} +
    + ); +} diff --git a/packages/interface/src/TopBar/TopBar.tsx b/packages/interface/src/TopBar/TopBar.tsx index 8fb208eac..6cd6c559b 100644 --- a/packages/interface/src/TopBar/TopBar.tsx +++ b/packages/interface/src/TopBar/TopBar.tsx @@ -3,50 +3,56 @@ import { TopBarSection } from "./Section"; import { useOverflowCalculation } from "./useOverflowCalculation"; interface TopBarProps { - sidebarWidth?: number; - inspectorWidth?: number; - isPreviewActive?: boolean; + sidebarWidth?: number; + inspectorWidth?: number; + isPreviewActive?: boolean; } // Traffic lights on macOS are ~80px from left edge when sidebar is collapsed const MACOS_TRAFFIC_LIGHT_WIDTH = 90; // Detect macOS once -const isMacOS = typeof navigator !== 'undefined' && - (navigator.platform.toLowerCase().includes('mac') || navigator.userAgent.includes('Mac')); +const isMacOS = + typeof navigator !== "undefined" && + (navigator.platform.toLowerCase().includes("mac") || + navigator.userAgent.includes("Mac")); -export const TopBar = memo(function TopBar({ sidebarWidth = 0, inspectorWidth = 0, isPreviewActive = false }: TopBarProps) { - const containerRef = useOverflowCalculation(); +export const TopBar = memo(function TopBar({ + sidebarWidth = 0, + inspectorWidth = 0, + isPreviewActive = false, +}: TopBarProps) { + const containerRef = useOverflowCalculation(); - const isSidebarCollapsed = sidebarWidth === 0; + const isSidebarCollapsed = sidebarWidth === 0; - // Add padding for macOS traffic lights when sidebar is collapsed - const leftPadding = useMemo( - () => (isMacOS && isSidebarCollapsed ? MACOS_TRAFFIC_LIGHT_WIDTH : 0), - [isSidebarCollapsed] - ); + // Add padding for macOS traffic lights when sidebar is collapsed + const leftPadding = useMemo( + () => (isMacOS && isSidebarCollapsed ? MACOS_TRAFFIC_LIGHT_WIDTH : 0), + [isSidebarCollapsed] + ); - return ( -
    -
    - - - -
    -
    - ); -}); \ No newline at end of file + return ( +
    +
    + + + +
    +
    + ); +}); diff --git a/packages/interface/src/TopBar/index.ts b/packages/interface/src/TopBar/index.ts index 24e63743a..9cee8ce4b 100644 --- a/packages/interface/src/TopBar/index.ts +++ b/packages/interface/src/TopBar/index.ts @@ -1,5 +1,5 @@ +export type { TopBarPosition, TopBarPriority } from "./Context"; export { TopBarProvider, useTopBar } from "./Context"; -export type { TopBarPriority, TopBarPosition } from "./Context"; +export { TopBarItem } from "./Item"; export { TopBarPortal } from "./Portal"; export { TopBar } from "./TopBar"; -export { TopBarItem } from "./Item"; \ No newline at end of file diff --git a/packages/interface/src/TopBar/useOverflowCalculation.ts b/packages/interface/src/TopBar/useOverflowCalculation.ts index 119e982ce..2acea4b4d 100644 --- a/packages/interface/src/TopBar/useOverflowCalculation.ts +++ b/packages/interface/src/TopBar/useOverflowCalculation.ts @@ -1,142 +1,170 @@ -import { useCallback, useEffect, useLayoutEffect, useRef } from "react"; -import { useTopBar, useTopBarInternal, TopBarItem, TopBarPosition } from "./Context"; +import { useCallback, useLayoutEffect, useRef } from "react"; +import { + type TopBarItem, + type TopBarPosition, + useTopBar, + useTopBarInternal, +} from "./Context"; const GAP = 8; const OVERFLOW_BUTTON_WIDTH = 44; function sortByPriority(a: TopBarItem, b: TopBarItem): number { - const priorityOrder = { high: 0, normal: 1, low: 2 }; - return priorityOrder[a.priority] - priorityOrder[b.priority]; + const priorityOrder = { high: 0, normal: 1, low: 2 }; + return priorityOrder[a.priority] - priorityOrder[b.priority]; } function calculateFitting( - items: TopBarItem[], - containerWidth: number + items: TopBarItem[], + containerWidth: number ): { visible: TopBarItem[]; overflow: TopBarItem[] } { - const visible: TopBarItem[] = []; - const overflow: TopBarItem[] = []; + const visible: TopBarItem[] = []; + const overflow: TopBarItem[] = []; - let usedWidth = 0; - let willHaveOverflow = false; - const SAFETY_MARGIN = 60; // Extra buffer to prevent items from going off-screen + let usedWidth = 0; + let willHaveOverflow = false; + const SAFETY_MARGIN = 60; // Extra buffer to prevent items from going off-screen - // First pass: add all high-priority items - for (const item of items) { - if (item.priority === "high") { - visible.push(item); - usedWidth += item.width + GAP; - } - } + // First pass: add all high-priority items + for (const item of items) { + if (item.priority === "high") { + visible.push(item); + usedWidth += item.width + GAP; + } + } - // Second pass: add normal/low priority items until we run out of space - for (const item of items) { - if (item.priority === "high") continue; + // Second pass: add normal/low priority items until we run out of space + for (const item of items) { + if (item.priority === "high") continue; - const itemWidth = item.width + GAP; - const reservedSpace = overflow.length > 0 || willHaveOverflow ? OVERFLOW_BUTTON_WIDTH : 0; + const itemWidth = item.width + GAP; + const reservedSpace = + overflow.length > 0 || willHaveOverflow ? OVERFLOW_BUTTON_WIDTH : 0; - if (usedWidth + itemWidth + reservedSpace + SAFETY_MARGIN <= containerWidth) { - visible.push(item); - usedWidth += itemWidth; - } else { - overflow.push(item); - willHaveOverflow = true; - } - } + if ( + usedWidth + itemWidth + reservedSpace + SAFETY_MARGIN <= + containerWidth + ) { + visible.push(item); + usedWidth += itemWidth; + } else { + overflow.push(item); + willHaveOverflow = true; + } + } - return { visible, overflow }; + return { visible, overflow }; } export function useOverflowCalculation() { - const { items, leftContainerRef, rightContainerRef } = useTopBar(); - const { setVisibleItems, setOverflowItems, recalculationTrigger } = useTopBarInternal(); - const parentContainerRef = useRef(null); + const { items, leftContainerRef, rightContainerRef } = useTopBar(); + const { setVisibleItems, setOverflowItems, recalculationTrigger } = + useTopBarInternal(); + const parentContainerRef = useRef(null); - const lastVisibleRef = useRef>(new Set()); - const lastOverflowRef = useRef>(new Map()); + const lastVisibleRef = useRef>(new Set()); + const lastOverflowRef = useRef>(new Map()); - const calculateOverflow = useCallback(() => { - if (!leftContainerRef?.current || !rightContainerRef?.current || !parentContainerRef.current) return; + const calculateOverflow = useCallback(() => { + if ( + !( + leftContainerRef?.current && + rightContainerRef?.current && + parentContainerRef.current + ) + ) + return; - const parentWidth = parentContainerRef.current.offsetWidth; - const PADDING = 24; // px-3 = 12px on each side - const SECTION_GAPS = 24; // gap-3 between 3 sections = 12px * 2 + const parentWidth = parentContainerRef.current.offsetWidth; + const PADDING = 24; // px-3 = 12px on each side + const SECTION_GAPS = 24; // gap-3 between 3 sections = 12px * 2 - // Calculate how much space each side can use - // We split available width: left takes what it needs, right takes what it needs, - // center (flex-1) takes the rest - const totalAvailable = parentWidth - PADDING - SECTION_GAPS; + // Calculate how much space each side can use + // We split available width: left takes what it needs, right takes what it needs, + // center (flex-1) takes the rest + const totalAvailable = parentWidth - PADDING - SECTION_GAPS; - const leftItems = Array.from(items.values()) - .filter(item => item.position === "left") - .sort(sortByPriority); + const leftItems = Array.from(items.values()) + .filter((item) => item.position === "left") + .sort(sortByPriority); - const rightItems = Array.from(items.values()) - .filter(item => item.position === "right") - .sort(sortByPriority); + const rightItems = Array.from(items.values()) + .filter((item) => item.position === "right") + .sort(sortByPriority); - const centerItems = Array.from(items.values()) - .filter(item => item.position === "center"); + const centerItems = Array.from(items.values()).filter( + (item) => item.position === "center" + ); - // Each side gets up to 45% of total space, but we need to account for the overflow button - // when items start overflowing - const maxSideWidth = totalAvailable * 0.45; + // Each side gets up to 45% of total space, but we need to account for the overflow button + // when items start overflowing + const maxSideWidth = totalAvailable * 0.45; - const leftResult = calculateFitting(leftItems, maxSideWidth); - const rightResult = calculateFitting(rightItems, maxSideWidth); + const leftResult = calculateFitting(leftItems, maxSideWidth); + const rightResult = calculateFitting(rightItems, maxSideWidth); - const newVisibleItems = new Set([ - ...leftResult.visible.map(item => item.id), - ...rightResult.visible.map(item => item.id), - ...centerItems.map(item => item.id), - ]); + const newVisibleItems = new Set([ + ...leftResult.visible.map((item) => item.id), + ...rightResult.visible.map((item) => item.id), + ...centerItems.map((item) => item.id), + ]); - const newOverflowItems = new Map([ - ["left", leftResult.overflow], - ["right", rightResult.overflow], - ["center", []], - ]); + const newOverflowItems = new Map([ + ["left", leftResult.overflow], + ["right", rightResult.overflow], + ["center", []], + ]); - // Only update if visible items actually changed - const visibleChanged = - newVisibleItems.size !== lastVisibleRef.current.size || - !Array.from(newVisibleItems).every(id => lastVisibleRef.current.has(id)); + // Only update if visible items actually changed + const visibleChanged = + newVisibleItems.size !== lastVisibleRef.current.size || + !Array.from(newVisibleItems).every((id) => + lastVisibleRef.current.has(id) + ); - // Only update if overflow items actually changed - const overflowChanged = - leftResult.overflow.length !== (lastOverflowRef.current.get("left")?.length ?? 0) || - rightResult.overflow.length !== (lastOverflowRef.current.get("right")?.length ?? 0); + // Only update if overflow items actually changed + const overflowChanged = + leftResult.overflow.length !== + (lastOverflowRef.current.get("left")?.length ?? 0) || + rightResult.overflow.length !== + (lastOverflowRef.current.get("right")?.length ?? 0); - if (visibleChanged) { - lastVisibleRef.current = newVisibleItems; - setVisibleItems(newVisibleItems); - } + if (visibleChanged) { + lastVisibleRef.current = newVisibleItems; + setVisibleItems(newVisibleItems); + } - if (overflowChanged) { - lastOverflowRef.current = newOverflowItems; - setOverflowItems(newOverflowItems); - } - }, [items, leftContainerRef, rightContainerRef, setVisibleItems, setOverflowItems]); + if (overflowChanged) { + lastOverflowRef.current = newOverflowItems; + setOverflowItems(newOverflowItems); + } + }, [ + items, + leftContainerRef, + rightContainerRef, + setVisibleItems, + setOverflowItems, + ]); - useLayoutEffect(() => { - calculateOverflow(); - }, [calculateOverflow, recalculationTrigger]); + useLayoutEffect(() => { + calculateOverflow(); + }, [calculateOverflow, recalculationTrigger]); - // Watch parent container size changes with ResizeObserver - useLayoutEffect(() => { - const parentEl = parentContainerRef.current; - if (!parentEl) return; + // Watch parent container size changes with ResizeObserver + useLayoutEffect(() => { + const parentEl = parentContainerRef.current; + if (!parentEl) return; - const resizeObserver = new ResizeObserver(() => { - calculateOverflow(); - }); + const resizeObserver = new ResizeObserver(() => { + calculateOverflow(); + }); - resizeObserver.observe(parentEl); + resizeObserver.observe(parentEl); - return () => { - resizeObserver.disconnect(); - }; - }, [calculateOverflow]); + return () => { + resizeObserver.disconnect(); + }; + }, [calculateOverflow]); - return parentContainerRef; -} \ No newline at end of file + return parentContainerRef; +} diff --git a/packages/interface/src/components/DndProvider.tsx b/packages/interface/src/components/DndProvider.tsx index 7a3ceb4fe..c7eb10e4a 100644 --- a/packages/interface/src/components/DndProvider.tsx +++ b/packages/interface/src/components/DndProvider.tsx @@ -1,21 +1,24 @@ -import { - DndContext, - DragOverlay, - PointerSensor, - useSensor, - useSensors, - pointerWithin, -} from "@dnd-kit/core"; import type { CollisionDetection } from "@dnd-kit/core"; -import { useState } from "react"; -import { House, Clock, Heart, Folders } from "@phosphor-icons/react"; -import { useQueryClient } from "@tanstack/react-query"; -import { useLibraryMutation, useSpacedriveClient } from "../contexts/SpacedriveContext"; -import { useSidebarStore } from "@sd/ts-client"; +import { + DndContext, + DragOverlay, + PointerSensor, + pointerWithin, + useSensor, + useSensors, +} from "@dnd-kit/core"; +import { Clock, Folders, Heart, House } from "@phosphor-icons/react"; import type { File, SdPath } from "@sd/ts-client"; -import { useSpaces } from "./SpacesSidebar/hooks/useSpaces"; -import { useFileOperationDialog } from "./modals/FileOperationModal"; +import { useSidebarStore } from "@sd/ts-client"; +import { useQueryClient } from "@tanstack/react-query"; +import { useState } from "react"; +import { + useLibraryMutation, + useSpacedriveClient, +} from "../contexts/SpacedriveContext"; import { File as FileComponent } from "../routes/explorer/File"; +import { useFileOperationDialog } from "./modals/FileOperationModal"; +import { useSpaces } from "./SpacesSidebar/hooks/useSpaces"; /** * DndProvider - Global drag-and-drop coordinator @@ -42,434 +45,407 @@ import { File as FileComponent } from "../routes/explorer/File"; * - Data: { type, spaceId, groupId? } */ export function DndProvider({ children }: { children: React.ReactNode }) { - const sensors = useSensors( - useSensor(PointerSensor, { - activationConstraint: { - distance: 8, // Require 8px movement before activating drag - }, - }), - ); - const addItem = useLibraryMutation("spaces.add_item"); - const reorderItems = useLibraryMutation("spaces.reorder_items"); - const reorderGroups = useLibraryMutation("spaces.reorder_groups"); - const openFileOperation = useFileOperationDialog(); - const [activeItem, setActiveItem] = useState(null); - const client = useSpacedriveClient(); - const queryClient = useQueryClient(); - const { currentSpaceId } = useSidebarStore(); - const { data: spacesData } = useSpaces(); - const spaces = spacesData?.spaces; + const sensors = useSensors( + useSensor(PointerSensor, { + activationConstraint: { + distance: 8, // Require 8px movement before activating drag + }, + }) + ); + const addItem = useLibraryMutation("spaces.add_item"); + const reorderItems = useLibraryMutation("spaces.reorder_items"); + const reorderGroups = useLibraryMutation("spaces.reorder_groups"); + const openFileOperation = useFileOperationDialog(); + const [activeItem, setActiveItem] = useState(null); + const client = useSpacedriveClient(); + const queryClient = useQueryClient(); + const { currentSpaceId } = useSidebarStore(); + const { data: spacesData } = useSpaces(); + const spaces = spacesData?.spaces; - // Custom collision detection: prefer -top zones over -bottom zones to avoid double lines - const customCollision: CollisionDetection = (args) => { - const collisions = pointerWithin(args); - if (!collisions || collisions.length === 0) return collisions; + // Custom collision detection: prefer -top zones over -bottom zones to avoid double lines + const customCollision: CollisionDetection = (args) => { + const collisions = pointerWithin(args); + if (!collisions || collisions.length === 0) return collisions; - // If we have multiple collisions, prefer -top over -bottom - const hasTop = collisions.find((c) => String(c.id).endsWith("-top")); - const hasMiddle = collisions.find((c) => - String(c.id).endsWith("-middle"), - ); + // If we have multiple collisions, prefer -top over -bottom + const hasTop = collisions.find((c) => String(c.id).endsWith("-top")); + const hasMiddle = collisions.find((c) => String(c.id).endsWith("-middle")); - if (hasMiddle) return [hasMiddle]; // Middle zone takes priority - if (hasTop) return [hasTop]; // Top zone over bottom - return [collisions[0]]; // First collision - }; + if (hasMiddle) return [hasMiddle]; // Middle zone takes priority + if (hasTop) return [hasTop]; // Top zone over bottom + return [collisions[0]]; // First collision + }; - const handleDragStart = (event: any) => { - setActiveItem(event.active.data.current); - }; + const handleDragStart = (event: any) => { + setActiveItem(event.active.data.current); + }; - const handleDragEnd = async (event: any) => { - const { active, over } = event; + const handleDragEnd = async (event: any) => { + const { active, over } = event; - setActiveItem(null); + setActiveItem(null); - if (!over) return; + if (!over) return; - // Handle sortable reordering (no drag data, just active/over IDs) - if (active.id !== over.id && !active.data.current?.type) { - console.log("[DnD] Sortable reorder:", { - activeId: active.id, - overId: over.id, - }); + // Handle sortable reordering (no drag data, just active/over IDs) + if (active.id !== over.id && !active.data.current?.type) { + console.log("[DnD] Sortable reorder:", { + activeId: active.id, + overId: over.id, + }); - const libraryId = client.getCurrentLibraryId(); - const currentSpace = - spaces?.find((s: any) => s.id === currentSpaceId) ?? - spaces?.[0]; + const libraryId = client.getCurrentLibraryId(); + const currentSpace = + spaces?.find((s: any) => s.id === currentSpaceId) ?? spaces?.[0]; - if (!currentSpace || !libraryId) return; + if (!(currentSpace && libraryId)) return; - const queryKey = [ - "query:spaces.get_layout", - libraryId, - { space_id: currentSpace.id }, - ]; - const layout = queryClient.getQueryData(queryKey) as any; + const queryKey = [ + "query:spaces.get_layout", + libraryId, + { space_id: currentSpace.id }, + ]; + const layout = queryClient.getQueryData(queryKey) as any; - if (!layout) return; + if (!layout) return; - // Check if we're reordering groups - const groups = layout.groups?.map((g: any) => g.group) || []; - const isGroupReorder = groups.some((g: any) => g.id === active.id); + // Check if we're reordering groups + const groups = layout.groups?.map((g: any) => g.group) || []; + const isGroupReorder = groups.some((g: any) => g.id === active.id); - if (isGroupReorder) { - console.log("[DnD] Reordering groups"); + if (isGroupReorder) { + console.log("[DnD] Reordering groups"); - const oldIndex = groups.findIndex( - (g: any) => g.id === active.id, - ); - const newIndex = groups.findIndex((g: any) => g.id === over.id); + const oldIndex = groups.findIndex((g: any) => g.id === active.id); + const newIndex = groups.findIndex((g: any) => g.id === over.id); - if ( - oldIndex !== -1 && - newIndex !== -1 && - oldIndex !== newIndex - ) { - // Optimistically update the UI - const newGroups = [...layout.groups]; - const [movedGroup] = newGroups.splice(oldIndex, 1); - newGroups.splice(newIndex, 0, movedGroup); + if (oldIndex !== -1 && newIndex !== -1 && oldIndex !== newIndex) { + // Optimistically update the UI + const newGroups = [...layout.groups]; + const [movedGroup] = newGroups.splice(oldIndex, 1); + newGroups.splice(newIndex, 0, movedGroup); - queryClient.setQueryData(queryKey, { - ...layout, - groups: newGroups, - }); + queryClient.setQueryData(queryKey, { + ...layout, + groups: newGroups, + }); - // Send reorder mutation - try { - await reorderGroups.mutateAsync({ - space_id: currentSpace.id, - group_ids: newGroups.map((g: any) => g.group.id), - }); - console.log("[DnD] Group reorder successful"); - } catch (err) { - console.error("[DnD] Group reorder failed:", err); - // Revert on error - queryClient.setQueryData(queryKey, layout); - } - } + // Send reorder mutation + try { + await reorderGroups.mutateAsync({ + space_id: currentSpace.id, + group_ids: newGroups.map((g: any) => g.group.id), + }); + console.log("[DnD] Group reorder successful"); + } catch (err) { + console.error("[DnD] Group reorder failed:", err); + // Revert on error + queryClient.setQueryData(queryKey, layout); + } + } - return; - } + return; + } - // Reordering space items - if (layout?.space_items) { - const items = layout.space_items; - const oldIndex = items.findIndex( - (item: any) => item.id === active.id, - ); + // Reordering space items + if (layout?.space_items) { + const items = layout.space_items; + const oldIndex = items.findIndex((item: any) => item.id === active.id); - // Extract item ID from over.id (could be a drop zone ID like "space-item-{id}-top") - let overItemId = String(over.id); - if (overItemId.startsWith("space-item-")) { - // Extract the UUID from "space-item-{uuid}-top/bottom/middle" - const parts = overItemId.split("-"); - // Remove "space" and "item" and the last part (top/bottom/middle) - overItemId = parts.slice(2, -1).join("-"); - } + // Extract item ID from over.id (could be a drop zone ID like "space-item-{id}-top") + let overItemId = String(over.id); + if (overItemId.startsWith("space-item-")) { + // Extract the UUID from "space-item-{uuid}-top/bottom/middle" + const parts = overItemId.split("-"); + // Remove "space" and "item" and the last part (top/bottom/middle) + overItemId = parts.slice(2, -1).join("-"); + } - const newIndex = items.findIndex( - (item: any) => item.id === overItemId, - ); + const newIndex = items.findIndex((item: any) => item.id === overItemId); - console.log("[DnD] Reorder space items:", { - oldIndex, - newIndex, - activeId: active.id, - extractedOverId: overItemId, - }); + console.log("[DnD] Reorder space items:", { + oldIndex, + newIndex, + activeId: active.id, + extractedOverId: overItemId, + }); - if ( - oldIndex !== -1 && - newIndex !== -1 && - oldIndex !== newIndex - ) { - // Optimistically update the UI - const newItems = [...items]; - const [movedItem] = newItems.splice(oldIndex, 1); - newItems.splice(newIndex, 0, movedItem); + if (oldIndex !== -1 && newIndex !== -1 && oldIndex !== newIndex) { + // Optimistically update the UI + const newItems = [...items]; + const [movedItem] = newItems.splice(oldIndex, 1); + newItems.splice(newIndex, 0, movedItem); - queryClient.setQueryData(queryKey, { - ...layout, - space_items: newItems, - }); + queryClient.setQueryData(queryKey, { + ...layout, + space_items: newItems, + }); - // Send reorder mutation - try { - await reorderItems.mutateAsync({ - group_id: null, // Space-level items - item_ids: newItems.map((item: any) => item.id), - }); - console.log("[DnD] Space items reorder successful"); - } catch (err) { - console.error("[DnD] Space items reorder failed:", err); - // Revert on error - queryClient.setQueryData(queryKey, layout); - } - } - } + // Send reorder mutation + try { + await reorderItems.mutateAsync({ + group_id: null, // Space-level items + item_ids: newItems.map((item: any) => item.id), + }); + console.log("[DnD] Space items reorder successful"); + } catch (err) { + console.error("[DnD] Space items reorder failed:", err); + // Revert on error + queryClient.setQueryData(queryKey, layout); + } + } + } - return; - } + return; + } - if (!active.data.current) return; + if (!active.data.current) return; - const dragData = active.data.current; - const dropData = over.data.current; + const dragData = active.data.current; + const dropData = over.data.current; - // Handle palette item drops (from customization panel) - if (dragData?.type === "palette-item") { - const libraryId = client.getCurrentLibraryId(); - const currentSpace = - spaces?.find((s: any) => s.id === currentSpaceId) ?? - spaces?.[0]; + // Handle palette item drops (from customization panel) + if (dragData?.type === "palette-item") { + const libraryId = client.getCurrentLibraryId(); + const currentSpace = + spaces?.find((s: any) => s.id === currentSpaceId) ?? spaces?.[0]; - if (!currentSpace || !libraryId) return; + if (!(currentSpace && libraryId)) return; - try { - await addItem.mutateAsync({ - space_id: currentSpace.id, - group_id: dropData?.groupId || null, - item_type: dragData.itemType, - }); - console.log("[DnD] Successfully added palette item"); - } catch (err) { - console.error("[DnD] Failed to add palette item:", err); - } - return; - } + try { + await addItem.mutateAsync({ + space_id: currentSpace.id, + group_id: dropData?.groupId || null, + item_type: dragData.itemType, + }); + console.log("[DnD] Successfully added palette item"); + } catch (err) { + console.error("[DnD] Failed to add palette item:", err); + } + return; + } - if (!dragData || dragData.type !== "explorer-file") return; + if (!dragData || dragData.type !== "explorer-file") return; - // Add to space (root-level drop zones between groups) - if (dropData?.action === "add-to-space") { - if (!dropData.spaceId) return; + // Add to space (root-level drop zones between groups) + if (dropData?.action === "add-to-space") { + if (!dropData.spaceId) return; - try { - await addItem.mutateAsync({ - space_id: dropData.spaceId, - group_id: null, - item_type: { Path: { sd_path: dragData.sdPath } }, - }); - console.log("[DnD] Successfully added to space root"); - } catch (err) { - console.error("[DnD] Failed to add to space:", err); - } - return; - } + try { + await addItem.mutateAsync({ + space_id: dropData.spaceId, + group_id: null, + item_type: { Path: { sd_path: dragData.sdPath } }, + }); + console.log("[DnD] Successfully added to space root"); + } catch (err) { + console.error("[DnD] Failed to add to space:", err); + } + return; + } - // Add to group (empty group drop zone) - if (dropData?.action === "add-to-group") { - if (!dropData.spaceId || !dropData.groupId) return; + // Add to group (empty group drop zone) + if (dropData?.action === "add-to-group") { + if (!(dropData.spaceId && dropData.groupId)) return; - console.log("[DnD] Adding to group:", { - spaceId: dropData.spaceId, - groupId: dropData.groupId, - sdPath: dragData.sdPath, - }); + console.log("[DnD] Adding to group:", { + spaceId: dropData.spaceId, + groupId: dropData.groupId, + sdPath: dragData.sdPath, + }); - try { - await addItem.mutateAsync({ - space_id: dropData.spaceId, - group_id: dropData.groupId, - item_type: { Path: { sd_path: dragData.sdPath } }, - }); - console.log("[DnD] Successfully added to group"); - } catch (err) { - console.error("[DnD] Failed to add to group:", err); - } - return; - } + try { + await addItem.mutateAsync({ + space_id: dropData.spaceId, + group_id: dropData.groupId, + item_type: { Path: { sd_path: dragData.sdPath } }, + }); + console.log("[DnD] Successfully added to group"); + } catch (err) { + console.error("[DnD] Failed to add to group:", err); + } + return; + } - // Insert before/after sidebar items (adds item to space/group) - if ( - dropData?.action === "insert-before" || - dropData?.action === "insert-after" - ) { - if (!dropData.spaceId) return; + // Insert before/after sidebar items (adds item to space/group) + if ( + dropData?.action === "insert-before" || + dropData?.action === "insert-after" + ) { + if (!dropData.spaceId) return; - console.log("[DnD] Inserting item:", { - action: dropData.action, - spaceId: dropData.spaceId, - groupId: dropData.groupId, - sdPath: dragData.sdPath, - }); + console.log("[DnD] Inserting item:", { + action: dropData.action, + spaceId: dropData.spaceId, + groupId: dropData.groupId, + sdPath: dragData.sdPath, + }); - try { - await addItem.mutateAsync({ - space_id: dropData.spaceId, - group_id: dropData.groupId || null, - item_type: { Path: { sd_path: dragData.sdPath } }, - }); - console.log("[DnD] Successfully inserted item"); - // TODO: Implement proper ordering relative to itemId - } catch (err) { - console.error("[DnD] Failed to add item:", err); - } - return; - } + try { + await addItem.mutateAsync({ + space_id: dropData.spaceId, + group_id: dropData.groupId || null, + item_type: { Path: { sd_path: dragData.sdPath } }, + }); + console.log("[DnD] Successfully inserted item"); + // TODO: Implement proper ordering relative to itemId + } catch (err) { + console.error("[DnD] Failed to add item:", err); + } + return; + } - // Move file into location/volume/folder - if (dropData?.action === "move-into") { - console.log("[DnD] Move-into action:", { - targetType: dropData.targetType, - targetId: dropData.targetId, - targetPath: dropData.targetPath, - hasTargetPath: !!dropData.targetPath, - draggedFile: dragData.name, - }); + // Move file into location/volume/folder + if (dropData?.action === "move-into") { + console.log("[DnD] Move-into action:", { + targetType: dropData.targetType, + targetId: dropData.targetId, + targetPath: dropData.targetPath, + hasTargetPath: !!dropData.targetPath, + draggedFile: dragData.name, + }); - const sources: SdPath[] = dragData.selectedFiles - ? dragData.selectedFiles.map((f: File) => f.sd_path) - : [dragData.sdPath]; + const sources: SdPath[] = dragData.selectedFiles + ? dragData.selectedFiles.map((f: File) => f.sd_path) + : [dragData.sdPath]; - const destination: SdPath = dropData.targetPath; + const destination: SdPath = dropData.targetPath; - if (!destination) { - console.error("[DnD] No target path for move-into action"); - return; - } + if (!destination) { + console.error("[DnD] No target path for move-into action"); + return; + } - // Determine operation based on modifier keys - // For now default to copy (user can choose in modal) - const operation = "copy"; + // Determine operation based on modifier keys + // For now default to copy (user can choose in modal) + const operation = "copy"; - openFileOperation({ - operation, - sources, - destination, - }); - return; - } + openFileOperation({ + operation, + sources, + destination, + }); + return; + } - // Drop on space root area (adds to space) - if (dropData?.type === "space" && dragData.type === "explorer-file") { - console.log("[DnD] Adding to space (type=space):", { - spaceId: dropData.spaceId, - sdPath: dragData.sdPath, - }); + // Drop on space root area (adds to space) + if (dropData?.type === "space" && dragData.type === "explorer-file") { + console.log("[DnD] Adding to space (type=space):", { + spaceId: dropData.spaceId, + sdPath: dragData.sdPath, + }); - try { - await addItem.mutateAsync({ - space_id: dropData.spaceId, - group_id: null, - item_type: { Path: { sd_path: dragData.sdPath } }, - }); - console.log("[DnD] Successfully added to space"); - } catch (err) { - console.error("[DnD] Failed to add item:", err); - } - } + try { + await addItem.mutateAsync({ + space_id: dropData.spaceId, + group_id: null, + item_type: { Path: { sd_path: dragData.sdPath } }, + }); + console.log("[DnD] Successfully added to space"); + } catch (err) { + console.error("[DnD] Failed to add item:", err); + } + } - // Drop on group area (adds to group) - if (dropData?.type === "group" && dragData.type === "explorer-file") { - console.log("[DnD] Adding to group (type=group):", { - spaceId: dropData.spaceId, - groupId: dropData.groupId, - sdPath: dragData.sdPath, - }); + // Drop on group area (adds to group) + if (dropData?.type === "group" && dragData.type === "explorer-file") { + console.log("[DnD] Adding to group (type=group):", { + spaceId: dropData.spaceId, + groupId: dropData.groupId, + sdPath: dragData.sdPath, + }); - try { - await addItem.mutateAsync({ - space_id: dropData.spaceId, - group_id: dropData.groupId, - item_type: { Path: { sd_path: dragData.sdPath } }, - }); - console.log("[DnD] Successfully added to group"); - } catch (err) { - console.error("[DnD] Failed to add item to group:", err); - } - } - }; + try { + await addItem.mutateAsync({ + space_id: dropData.spaceId, + group_id: dropData.groupId, + item_type: { Path: { sd_path: dragData.sdPath } }, + }); + console.log("[DnD] Successfully added to group"); + } catch (err) { + console.error("[DnD] Failed to add item to group:", err); + } + } + }; - return ( - - {children} - - {activeItem?.type === "palette-item" ? ( - // Palette item preview -
    - {activeItem.itemType === "Overview" && ( - - )} - {activeItem.itemType === "Recents" && ( - - )} - {activeItem.itemType === "Favorites" && ( - - )} - {activeItem.itemType === "FileKinds" && ( - - )} - - {activeItem.itemType === "Overview" && "Overview"} - {activeItem.itemType === "Recents" && "Recents"} - {activeItem.itemType === "Favorites" && "Favorites"} - {activeItem.itemType === "FileKinds" && - "File Kinds"} - -
    - ) : activeItem?.label ? ( - // Group or SpaceItem preview (from sortable context) -
    - - {activeItem.label} - -
    - ) : activeItem?.file ? ( - activeItem.gridSize ? ( - // Grid view preview -
    -
    -
    - -
    -
    - {activeItem.name} -
    - {/* Show count badge if dragging multiple files */} - {activeItem.selectedFiles && - activeItem.selectedFiles.length > 1 && ( -
    - {activeItem.selectedFiles.length} -
    - )} -
    -
    - ) : ( - // Column/List view preview -
    - - - {activeItem.name} - - {/* Show count badge if dragging multiple files */} - {activeItem.selectedFiles && - activeItem.selectedFiles.length > 1 && ( -
    - {activeItem.selectedFiles.length} -
    - )} -
    - ) - ) : null} -
    -
    - ); -} \ No newline at end of file + return ( + + {children} + + {activeItem?.type === "palette-item" ? ( + // Palette item preview +
    + {activeItem.itemType === "Overview" && ( + + )} + {activeItem.itemType === "Recents" && ( + + )} + {activeItem.itemType === "Favorites" && ( + + )} + {activeItem.itemType === "FileKinds" && ( + + )} + + {activeItem.itemType === "Overview" && "Overview"} + {activeItem.itemType === "Recents" && "Recents"} + {activeItem.itemType === "Favorites" && "Favorites"} + {activeItem.itemType === "FileKinds" && "File Kinds"} + +
    + ) : activeItem?.label ? ( + // Group or SpaceItem preview (from sortable context) +
    + {activeItem.label} +
    + ) : activeItem?.file ? ( + activeItem.gridSize ? ( + // Grid view preview +
    +
    +
    + +
    +
    + {activeItem.name} +
    + {/* Show count badge if dragging multiple files */} + {activeItem.selectedFiles && + activeItem.selectedFiles.length > 1 && ( +
    + {activeItem.selectedFiles.length} +
    + )} +
    +
    + ) : ( + // Column/List view preview +
    + + + {activeItem.name} + + {/* Show count badge if dragging multiple files */} + {activeItem.selectedFiles && + activeItem.selectedFiles.length > 1 && ( +
    + {activeItem.selectedFiles.length} +
    + )} +
    + ) + ) : null} +
    +
    + ); +} diff --git a/packages/interface/src/components/ErrorBoundary.tsx b/packages/interface/src/components/ErrorBoundary.tsx index 88bb876ca..78d544e01 100644 --- a/packages/interface/src/components/ErrorBoundary.tsx +++ b/packages/interface/src/components/ErrorBoundary.tsx @@ -1,47 +1,47 @@ import { Component, type ErrorInfo, type ReactNode } from "react"; interface ErrorBoundaryProps { - children: ReactNode; + children: ReactNode; } interface ErrorBoundaryState { - error: Error | null; + error: Error | null; } function ErrorFallback({ - error, - onReset, + error, + onReset, }: { - error: Error; - onReset: () => void; + error: Error; + onReset: () => void; }) { - return ( -
    -
    -

    - Something went wrong -

    -

    - The application encountered an error. Please try restarting. -

    -
    - - Error details - -
    -						{error.toString()}
    -						{error.stack}
    -					
    -
    - -
    -
    - ); + return ( +
    +
    +

    + Something went wrong +

    +

    + The application encountered an error. Please try restarting. +

    +
    + + Error details + +
    +            {error.toString()}
    +            {error.stack}
    +          
    +
    + +
    +
    + ); } /** @@ -51,33 +51,30 @@ function ErrorFallback({ * The fallback UI is extracted as a functional component for cleaner code. */ export class ErrorBoundary extends Component< - ErrorBoundaryProps, - ErrorBoundaryState + ErrorBoundaryProps, + ErrorBoundaryState > { - state: ErrorBoundaryState = { error: null }; + state: ErrorBoundaryState = { error: null }; - static getDerivedStateFromError(error: Error): ErrorBoundaryState { - return { error }; - } + static getDerivedStateFromError(error: Error): ErrorBoundaryState { + return { error }; + } - componentDidCatch(error: Error, errorInfo: ErrorInfo) { - console.error("ErrorBoundary caught an error:", error, errorInfo); - } + componentDidCatch(error: Error, errorInfo: ErrorInfo) { + console.error("ErrorBoundary caught an error:", error, errorInfo); + } - handleReset = () => { - window.location.reload(); - }; + handleReset = () => { + window.location.reload(); + }; - render() { - if (this.state.error) { - return ( - - ); - } + render() { + if (this.state.error) { + return ( + + ); + } - return this.props.children; - } -} \ No newline at end of file + return this.props.children; + } +} diff --git a/packages/interface/src/components/Inspector/Inspector.tsx b/packages/interface/src/components/Inspector/Inspector.tsx index b761426e9..6abe00742 100644 --- a/packages/interface/src/components/Inspector/Inspector.tsx +++ b/packages/interface/src/components/Inspector/Inspector.tsx @@ -1,24 +1,23 @@ import { ArrowSquareOut } from "@phosphor-icons/react"; -import { useEffect, useState, useMemo } from "react"; -import { useParams } from "react-router-dom"; import type { File, Location } from "@sd/ts-client"; -import { useLibraryQuery, useNormalizedQuery } from "../../contexts/SpacedriveContext"; -import { usePlatform } from "../../contexts/PlatformContext"; -import { useSelection } from "../../routes/explorer/SelectionContext"; -import { FileInspector } from "./variants/FileInspector"; -import { MultiFileInspector } from "./variants/MultiFileInspector"; -import { LocationInspector } from "./variants/LocationInspector"; -import { isVirtualFile } from "../../routes/explorer/utils/virtualFiles"; import clsx from "clsx"; +import { useEffect, useMemo, useState } from "react"; +import { usePlatform } from "../../contexts/PlatformContext"; +import { useLibraryQuery } from "../../contexts/SpacedriveContext"; +import { useSelection } from "../../routes/explorer/SelectionContext"; +import { isVirtualFile } from "../../routes/explorer/utils/virtualFiles"; +import { FileInspector } from "./variants/FileInspector"; +import { LocationInspector } from "./variants/LocationInspector"; +import { MultiFileInspector } from "./variants/MultiFileInspector"; // Re-export primitives for convenience export { - InfoRow, - Tag, - Section, - Divider, - Tabs, - TabContent, + Divider, + InfoRow, + Section, + TabContent, + Tabs, + Tag, } from "./primitives"; export type InspectorVariant = @@ -70,10 +69,10 @@ export function Inspector({ return ( ); } @@ -94,8 +93,8 @@ function InspectorView({ return (
    @@ -112,14 +111,14 @@ function InspectorView({ {/* Footer with pop-out button */} {showPopOutButton && onPopOut && ( -
    +
    @@ -132,8 +131,8 @@ function InspectorView({ function EmptyState() { return ( -
    -

    +

    +

    Select an item to view details

    @@ -151,7 +150,8 @@ export function PopoutInspector() { // Query selected file IDs from platform on mount useEffect(() => { if (platform.getSelectedFileIds) { - platform.getSelectedFileIds() + platform + .getSelectedFileIds() .then((fileIds) => { setSelectedFileIds(fileIds); }) @@ -168,19 +168,22 @@ export function PopoutInspector() { let unlisten: (() => void) | undefined; let mounted = true; - platform.onSelectedFilesChanged((fileIds) => { - if (mounted) { - setSelectedFileIds(fileIds); - } - }).then((unlistenFn) => { - if (mounted) { - unlisten = unlistenFn; - } else { - unlistenFn(); - } - }).catch((err) => { - console.error("Failed to listen for selected files changes:", err); - }); + platform + .onSelectedFilesChanged((fileIds) => { + if (mounted) { + setSelectedFileIds(fileIds); + } + }) + .then((unlistenFn) => { + if (mounted) { + unlisten = unlistenFn; + } else { + unlistenFn(); + } + }) + .catch((err) => { + console.error("Failed to listen for selected files changes:", err); + }); return () => { mounted = false; @@ -205,18 +208,18 @@ export function PopoutInspector() { const variant: InspectorVariant = file ? { type: "file", file } : selectedFileIds.length > 0 - ? { type: "empty" } // Loading state - : { type: "empty" }; // No selection + ? { type: "empty" } // Loading state + : { type: "empty" }; // No selection if (isLoading) { return ( -
    -
    -

    Loading...

    +
    +
    +

    Loading...

    ); } - return ; -} \ No newline at end of file + return ; +} diff --git a/packages/interface/src/components/Inspector/primitives/Divider.tsx b/packages/interface/src/components/Inspector/primitives/Divider.tsx index 1df46dfc2..3e3da490d 100644 --- a/packages/interface/src/components/Inspector/primitives/Divider.tsx +++ b/packages/interface/src/components/Inspector/primitives/Divider.tsx @@ -5,12 +5,5 @@ interface DividerProps { } export function Divider({ className }: DividerProps) { - return ( -
    - ); -} \ No newline at end of file + return
    ; +} diff --git a/packages/interface/src/components/Inspector/primitives/InfoRow.tsx b/packages/interface/src/components/Inspector/primitives/InfoRow.tsx index 2b80ba15d..e09a92c89 100644 --- a/packages/interface/src/components/Inspector/primitives/InfoRow.tsx +++ b/packages/interface/src/components/Inspector/primitives/InfoRow.tsx @@ -11,19 +11,19 @@ export function InfoRow({ label, value, mono, className }: InfoRowProps) { return (
    - {label} + {label} {value}
    ); -} \ No newline at end of file +} diff --git a/packages/interface/src/components/Inspector/primitives/Section.tsx b/packages/interface/src/components/Inspector/primitives/Section.tsx index 0c15f1c2c..aad8af563 100644 --- a/packages/interface/src/components/Inspector/primitives/Section.tsx +++ b/packages/interface/src/components/Inspector/primitives/Section.tsx @@ -1,5 +1,5 @@ -import clsx from "clsx"; import type { Icon } from "@phosphor-icons/react"; +import clsx from "clsx"; interface SectionProps { title: string; @@ -8,16 +8,21 @@ interface SectionProps { className?: string; } -export function Section({ title, icon: Icon, children, className }: SectionProps) { +export function Section({ + title, + icon: Icon, + children, + className, +}: SectionProps) { return (
    {Icon && } - + {title}
    {children}
    ); -} \ No newline at end of file +} diff --git a/packages/interface/src/components/Inspector/primitives/TabContent.tsx b/packages/interface/src/components/Inspector/primitives/TabContent.tsx index ea8d0ccf6..0f2d41312 100644 --- a/packages/interface/src/components/Inspector/primitives/TabContent.tsx +++ b/packages/interface/src/components/Inspector/primitives/TabContent.tsx @@ -12,15 +12,15 @@ export function TabContent({ id, activeTab, children }: TabContentProps) { return ( {children} ); -} \ No newline at end of file +} diff --git a/packages/interface/src/components/Inspector/primitives/Tabs.tsx b/packages/interface/src/components/Inspector/primitives/Tabs.tsx index c5402d12c..05496d1f6 100644 --- a/packages/interface/src/components/Inspector/primitives/Tabs.tsx +++ b/packages/interface/src/components/Inspector/primitives/Tabs.tsx @@ -1,6 +1,6 @@ +import type { Icon } from "@phosphor-icons/react"; import clsx from "clsx"; import { motion } from "framer-motion"; -import type { Icon } from "@phosphor-icons/react"; import { useState } from "react"; interface Tab { @@ -21,38 +21,40 @@ export function Tabs({ tabs, activeTab, onChange, className }: TabsProps) { const [hoveredTab, setHoveredTab] = useState(null); return ( -
    +
    {tabs.map((tab) => { const Icon = tab.icon; const isActive = activeTab === tab.id; const isHovered = hoveredTab === tab.id; return ( -
    +
    - } - /> -
    - + {/* Add Tag Button */} + { + // Use content-based tagging by default (tags all instances) + // Fall back to entry-based if no content identity + await applyTag.mutateAsync({ + targets: file.content_identity?.uuid + ? { + type: "Content", + ids: [file.content_identity.uuid], + } + : { + type: "Entry", + ids: [Number.parseInt(file.id)], + }, + tag_ids: [tag.id], + source: "User", + confidence: 1.0, + }); + }} + trigger={ + + } + /> +
    + - {/* AI Processing */} - {(isImage || isVideo || isAudio) && ( -
    -
    - {/* OCR for images */} - {isImage && ( - - )} + {/* AI Processing */} + {(isImage || isVideo || isAudio) && ( +
    +
    + {/* OCR for images */} + {isImage && ( + + )} - {/* Gaussian Splat for images */} - {isImage && ( - - )} + {/* Gaussian Splat for images */} + {isImage && ( + + )} - {/* Speech-to-text for audio/video */} - {(isVideo || isAudio) && ( - - )} + {/* Speech-to-text for audio/video */} + {(isVideo || isAudio) && ( + + )} - {/* Regenerate thumbnails */} - {(isImage || isVideo) && ( - - )} + {/* Regenerate thumbnails */} + {(isImage || isVideo) && ( + + )} - {/* Generate thumbstrip (for videos) */} - {isVideo && ( - - )} + {/* Generate thumbstrip (for videos) */} + {isVideo && ( + + )} - {/* Generate proxy (for videos) */} - {isVideo && ( - - )} + {/* Generate proxy (for videos) */} + {isVideo && ( + + )} - {/* Show extracted text if available */} - {hasText && ( -
    -
    - - - - - Extracted Text - -
    -
    -									{file.content_identity.text_content}
    -								
    -
    - )} -
    -
    - )} -
    - ); + {/* Show extracted text if available */} + {hasText && ( +
    +
    + + + + + Extracted Text + +
    +
    +                  {file.content_identity.text_content}
    +                
    +
    + )} +
    + + )} +
    + ); } function SidecarsTab({ file }: { file: File }) { - const sidecars = file.sidecars || []; - const platform = usePlatform(); - const { buildSidecarUrl, libraryId } = useServer(); + const sidecars = file.sidecars || []; + const platform = usePlatform(); + const { buildSidecarUrl, libraryId } = useServer(); - // Helper to get sidecar URL - const getSidecarUrl = (sidecar: any) => { - if (!file.content_identity) return null; + // Helper to get sidecar URL + const getSidecarUrl = (sidecar: any) => { + if (!file.content_identity) return null; - return buildSidecarUrl( - file.content_identity.uuid, - sidecar.kind, - sidecar.variant, - sidecar.format, - ); - }; + return buildSidecarUrl( + file.content_identity.uuid, + sidecar.kind, + sidecar.variant, + sidecar.format + ); + }; - return ( -
    -

    - Derivative files and associated content generated by Spacedrive -

    + return ( +
    +

    + Derivative files and associated content generated by Spacedrive +

    - {sidecars.length === 0 ? ( -
    - No sidecars generated yet -
    - ) : ( -
    - {sidecars.map((sidecar, i) => ( - - ))} -
    - )} -
    - ); + {sidecars.length === 0 ? ( +
    + No sidecars generated yet +
    + ) : ( +
    + {sidecars.map((sidecar, i) => ( + + ))} +
    + )} +
    + ); } function SidecarItem({ - sidecar, - file, - sidecarUrl, - platform, - libraryId, + sidecar, + file, + sidecarUrl, + platform, + libraryId, }: { - sidecar: any; - file: File; - sidecarUrl: string | null; - platform: ReturnType; - libraryId: string | null; + sidecar: any; + file: File; + sidecarUrl: string | null; + platform: ReturnType; + libraryId: string | null; }) { - const isImage = - (sidecar.kind === "thumb" || sidecar.kind === "thumbstrip") && - (sidecar.format === "webp" || - sidecar.format === "jpg" || - sidecar.format === "png"); + const isImage = + (sidecar.kind === "thumb" || sidecar.kind === "thumbstrip") && + (sidecar.format === "webp" || + sidecar.format === "jpg" || + sidecar.format === "png"); - // Get appropriate Spacedrive icon based on sidecar format/kind - const getSidecarIcon = () => { - const format = String(sidecar.format).toLowerCase(); + // Get appropriate Spacedrive icon based on sidecar format/kind + const getSidecarIcon = () => { + const format = String(sidecar.format).toLowerCase(); - // PLY files (3D mesh) use Mesh icon - if (format === "ply") { - return getIcon("Mesh", true); - } + // PLY files (3D mesh) use Mesh icon + if (format === "ply") { + return getIcon("Mesh", true); + } - // Text files use Text icon - if (format === "text" || format === "txt" || format === "srt") { - return getIcon("Text", true); - } + // Text files use Text icon + if (format === "text" || format === "txt" || format === "srt") { + return getIcon("Text", true); + } - // Thumbs/thumbstrips use Image icon - if (sidecar.kind === "thumb" || sidecar.kind === "thumbstrip") { - return getIcon("Image", true); - } + // Thumbs/thumbstrips use Image icon + if (sidecar.kind === "thumb" || sidecar.kind === "thumbstrip") { + return getIcon("Image", true); + } - // Default to Document icon - return getIcon("Document", true); - }; + // Default to Document icon + return getIcon("Document", true); + }; - const sidecarIcon = getSidecarIcon(); + const sidecarIcon = getSidecarIcon(); - const contextMenu = useContextMenu({ - items: [ - { - icon: MagnifyingGlass, - label: "Show in Finder", - onClick: async () => { - if ( - platform.getSidecarPath && - platform.revealFile && - file.content_identity && - libraryId - ) { - try { - // Convert "text" format to "txt" extension (matches actual file on disk) - const format = - sidecar.format === "text" - ? "txt" - : sidecar.format; - const sidecarPath = await platform.getSidecarPath( - libraryId, - file.content_identity.uuid, - sidecar.kind, - sidecar.variant, - format, - ); + const contextMenu = useContextMenu({ + items: [ + { + icon: MagnifyingGlass, + label: "Show in Finder", + onClick: async () => { + if ( + platform.getSidecarPath && + platform.revealFile && + file.content_identity && + libraryId + ) { + try { + // Convert "text" format to "txt" extension (matches actual file on disk) + const format = sidecar.format === "text" ? "txt" : sidecar.format; + const sidecarPath = await platform.getSidecarPath( + libraryId, + file.content_identity.uuid, + sidecar.kind, + sidecar.variant, + format + ); - await platform.revealFile(sidecarPath); - } catch (err) { - console.error("Failed to reveal sidecar:", err); - } - } - }, - condition: () => - !!platform.getSidecarPath && - !!platform.revealFile && - !!file.content_identity && - !!libraryId, - }, - { - icon: Trash, - label: "Delete Sidecar", - onClick: () => { - console.log("Delete sidecar:", sidecar); - // TODO: Implement sidecar deletion - }, - variant: "danger" as const, - }, - ], - }); + await platform.revealFile(sidecarPath); + } catch (err) { + console.error("Failed to reveal sidecar:", err); + } + } + }, + condition: () => + !!platform.getSidecarPath && + !!platform.revealFile && + !!file.content_identity && + !!libraryId, + }, + { + icon: Trash, + label: "Delete Sidecar", + onClick: () => { + console.log("Delete sidecar:", sidecar); + // TODO: Implement sidecar deletion + }, + variant: "danger" as const, + }, + ], + }); - const handleContextMenu = async (e: React.MouseEvent) => { - e.preventDefault(); - e.stopPropagation(); - await contextMenu.show(e); - }; + const handleContextMenu = async (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + await contextMenu.show(e); + }; - return ( -
    - {/* Preview thumbnail for image sidecars */} - {isImage && sidecarUrl ? ( -
    - {`${sidecar.variant} { - // Fallback to icon on error - e.currentTarget.style.display = "none"; - if (e.currentTarget.nextElementSibling) { - ( - e.currentTarget - .nextElementSibling as HTMLElement - ).style.display = "flex"; - } - }} - /> -
    - -
    -
    - ) : ( -
    - -
    - )} + return ( +
    + {/* Preview thumbnail for image sidecars */} + {isImage && sidecarUrl ? ( +
    + {`${sidecar.variant} { + // Fallback to icon on error + e.currentTarget.style.display = "none"; + if (e.currentTarget.nextElementSibling) { + ( + e.currentTarget.nextElementSibling as HTMLElement + ).style.display = "flex"; + } + }} + src={sidecarUrl} + /> +
    + +
    +
    + ) : ( +
    + +
    + )} -
    -
    - {String(sidecar.kind)} -
    -
    - {String(sidecar.variant)} · {formatBytes(sidecar.size)} -
    -
    - {String(sidecar.format).toUpperCase()} -
    -
    - {/* +
    + {String(sidecar.kind)} +
    +
    + {String(sidecar.variant)} · {formatBytes(sidecar.size)} +
    +
    + {String(sidecar.format).toUpperCase()} +
    +
    + {/* {String(sidecar.status)} */} -
    - ); +
    + ); } function InstancesTab({ file }: { file: File }) { - // Query for alternate instances with full File data - const instancesQuery = useNormalizedQuery< - { entry_uuid: string }, - { instances: File[]; total_count: number } - >({ - wireMethod: "query:files.alternate_instances", - input: { entry_uuid: file?.id || "" }, - enabled: !!file?.id && !!file?.content_identity, - }); + // Query for alternate instances with full File data + const instancesQuery = useNormalizedQuery< + { entry_uuid: string }, + { instances: File[]; total_count: number } + >({ + wireMethod: "query:files.alternate_instances", + input: { entry_uuid: file?.id || "" }, + enabled: !!file?.id && !!file?.content_identity, + }); - const instances = instancesQuery.data?.instances || []; + const instances = instancesQuery.data?.instances || []; - // Query devices to get proper names and icons - const devicesQuery = useNormalizedQuery({ - wireMethod: "query:devices.list", - input: { - include_offline: true, - include_details: false, - show_paired: true, - }, - resourceType: "device", - }); + // Query devices to get proper names and icons + const devicesQuery = useNormalizedQuery({ + wireMethod: "query:devices.list", + input: { + include_offline: true, + include_details: false, + show_paired: true, + }, + resourceType: "device", + }); - const devices = devicesQuery.data || []; + const devices = devicesQuery.data || []; - // Group instances by device_slug - const instancesByDevice = instances.reduce( - (acc, instance) => { - let deviceSlug = "unknown"; - if ("Physical" in instance.sd_path) { - deviceSlug = instance.sd_path.Physical.device_slug; - } else if ("Cloud" in instance.sd_path) { - deviceSlug = "cloud"; - } + // Group instances by device_slug + const instancesByDevice = instances.reduce( + (acc, instance) => { + let deviceSlug = "unknown"; + if ("Physical" in instance.sd_path) { + deviceSlug = instance.sd_path.Physical.device_slug; + } else if ("Cloud" in instance.sd_path) { + deviceSlug = "cloud"; + } - if (!acc[deviceSlug]) { - acc[deviceSlug] = []; - } - acc[deviceSlug].push(instance); - return acc; - }, - {} as Record, - ); + if (!acc[deviceSlug]) { + acc[deviceSlug] = []; + } + acc[deviceSlug].push(instance); + return acc; + }, + {} as Record + ); - const getDeviceName = (deviceSlug: string) => { - const device = devices.find((d) => d.slug === deviceSlug); - return device?.name || deviceSlug; - }; + const getDeviceName = (deviceSlug: string) => { + const device = devices.find((d) => d.slug === deviceSlug); + return device?.name || deviceSlug; + }; - const getDeviceInfo = (deviceSlug: string) => { - return devices.find((d) => d.slug === deviceSlug); - }; + const getDeviceInfo = (deviceSlug: string) => { + return devices.find((d) => d.slug === deviceSlug); + }; - if (instancesQuery.isLoading) { - return ( -
    - Loading instances... -
    - ); - } + if (instancesQuery.isLoading) { + return ( +
    + Loading instances... +
    + ); + } - if (!file.content_identity) { - return ( -
    -

    - This file has not been content-hashed yet. Instances will - appear after indexing completes. -

    -
    - ); - } + if (!file.content_identity) { + return ( +
    +

    + This file has not been content-hashed yet. Instances will appear after + indexing completes. +

    +
    + ); + } - return ( -
    -

    - All copies of this file across your devices and locations -

    + return ( +
    +

    + All copies of this file across your devices and locations +

    - {instances.length === 0 || instances.length === 1 ? ( -
    - No alternate instances found -
    - ) : ( -
    - {Object.entries(instancesByDevice).map( - ([deviceSlug, deviceInstances]) => { - const deviceInfo = getDeviceInfo(deviceSlug); - const deviceName = getDeviceName(deviceSlug); + {instances.length === 0 || instances.length === 1 ? ( +
    + No alternate instances found +
    + ) : ( +
    + {Object.entries(instancesByDevice).map( + ([deviceSlug, deviceInstances]) => { + const deviceInfo = getDeviceInfo(deviceSlug); + const deviceName = getDeviceName(deviceSlug); - return ( -
    - {/* Device Header */} -
    - - - {deviceName} - -
    -
    - {deviceInstances.length} -
    -
    + return ( +
    + {/* Device Header */} +
    + + + {deviceName} + +
    +
    + {deviceInstances.length} +
    +
    - {/* List of instances */} -
    - {deviceInstances.map((instance, i) => ( - - ))} -
    -
    - ); - }, - )} -
    - )} -
    - ); + {/* List of instances */} +
    + {deviceInstances.map((instance, i) => ( + + ))} +
    +
    + ); + } + )} +
    + )} +
    + ); } function InstanceRow({ instance }: { instance: File }) { - const getPathDisplay = (sdPath: typeof instance.sd_path) => { - if ("Physical" in sdPath) { - return sdPath.Physical.path; - } else if ("Cloud" in sdPath) { - return sdPath.Cloud.path; - } else { - return "Content"; - } - }; + const getPathDisplay = (sdPath: typeof instance.sd_path) => { + if ("Physical" in sdPath) { + return sdPath.Physical.path; + } + if ("Cloud" in sdPath) { + return sdPath.Cloud.path; + } + return "Content"; + }; - const formatDate = (dateStr: string) => { - const date = new Date(dateStr); - return date.toLocaleDateString("en-US", { - month: "short", - day: "numeric", - }); - }; + const formatDate = (dateStr: string) => { + const date = new Date(dateStr); + return date.toLocaleDateString("en-US", { + month: "short", + day: "numeric", + }); + }; - return ( -
    - {/* Thumbnail */} -
    - -
    + return ( +
    + {/* Thumbnail */} +
    + +
    - {/* File info */} -
    - - {instance.name} - {instance.extension && `.${instance.extension}`} - -
    + {/* File info */} +
    + + {instance.name} + {instance.extension && `.${instance.extension}`} + +
    - {/* Metadata */} -
    - {/* Tags */} - {instance.tags && instance.tags.length > 0 && ( -
    t.canonical_name) - .join(", ")} - > - {instance.tags.slice(0, 3).map((tag) => ( -
    - ))} - {instance.tags.length > 3 && ( - - +{instance.tags.length - 3} - - )} -
    - )} + {/* Metadata */} +
    + {/* Tags */} + {instance.tags && instance.tags.length > 0 && ( +
    t.canonical_name).join(", ")} + > + {instance.tags.slice(0, 3).map((tag) => ( +
    + ))} + {instance.tags.length > 3 && ( + + +{instance.tags.length - 3} + + )} +
    + )} - {/* Modified date */} - - {formatDate(instance.modified_at)} - + {/* Modified date */} + + {formatDate(instance.modified_at)} + - {/* Size */} - - {formatBytes(instance.size)} - + {/* Size */} + + {formatBytes(instance.size)} + - {/* Local indicator */} -
    -
    -
    - ); + {/* Local indicator */} +
    +
    +
    + ); } function ChatTab() { - const [message, setMessage] = useState(""); + const [message, setMessage] = useState(""); - const messages = [ - { - id: 1, - sender: "Sarah", - avatar: "S", - content: "Can you check if this photo is also on the NAS?", - time: "2:34 PM", - isUser: false, - }, - { - id: 2, - sender: "You", - avatar: "J", - content: "Yeah, it's synced. Shows 3 instances across devices.", - time: "2:35 PM", - isUser: true, - }, - { - id: 3, - sender: "AI Assistant", - avatar: "", - content: - "I found 2 similar photos in your library from the same location. Would you like me to create a collection?", - time: "2:36 PM", - isUser: false, - isAI: true, - unread: true, - }, - { - id: 4, - sender: "Sarah", - avatar: "S", - content: "Perfect, thanks! Can you share the collection with me?", - time: "2:37 PM", - isUser: false, - unread: true, - }, - { - id: 5, - sender: "Alex", - avatar: "A", - content: "I just tagged this as Summer 2025 btw", - time: "2:38 PM", - isUser: false, - unread: true, - }, - ]; + const messages = [ + { + id: 1, + sender: "Sarah", + avatar: "S", + content: "Can you check if this photo is also on the NAS?", + time: "2:34 PM", + isUser: false, + }, + { + id: 2, + sender: "You", + avatar: "J", + content: "Yeah, it's synced. Shows 3 instances across devices.", + time: "2:35 PM", + isUser: true, + }, + { + id: 3, + sender: "AI Assistant", + avatar: "", + content: + "I found 2 similar photos in your library from the same location. Would you like me to create a collection?", + time: "2:36 PM", + isUser: false, + isAI: true, + unread: true, + }, + { + id: 4, + sender: "Sarah", + avatar: "S", + content: "Perfect, thanks! Can you share the collection with me?", + time: "2:37 PM", + isUser: false, + unread: true, + }, + { + id: 5, + sender: "Alex", + avatar: "A", + content: "I just tagged this as Summer 2025 btw", + time: "2:38 PM", + isUser: false, + unread: true, + }, + ]; - return ( -
    - {/* Messages */} -
    - {messages.map((msg) => ( -
    - {/* Avatar */} -
    - {msg.avatar} -
    + return ( +
    + {/* Messages */} +
    + {messages.map((msg) => ( +
    + {/* Avatar */} +
    + {msg.avatar} +
    - {/* Message bubble */} -
    -
    - {!msg.isUser && ( -
    - {msg.sender} -
    - )} -

    - {msg.content} -

    -
    - - {msg.time} - -
    -
    - ))} -
    + {/* Message bubble */} +
    +
    + {!msg.isUser && ( +
    + {msg.sender} +
    + )} +

    + {msg.content} +

    +
    + + {msg.time} + +
    +
    + ))} +
    - {/* Input */} -
    -
    - + {/* Input */} +
    +
    + -
    - setMessage(e.target.value)} - placeholder="Type a message..." - className="flex-1 bg-transparent text-xs text-sidebar-ink placeholder:text-sidebar-inkDull outline-none" - /> -
    +
    + setMessage(e.target.value)} + placeholder="Type a message..." + type="text" + value={message} + /> +
    - -
    + +
    -
    - - - -
    -
    -
    - ); +
    + + + +
    +
    +
    + ); } function ActivityTab() { - const activity = [ - { action: "Synced to NAS", time: "2 min ago", device: "MacBook Pro" }, - { action: "Uploaded to S3", time: "1 hour ago", device: "MacBook Pro" }, - { - action: "Thumbnail generated", - time: "2 hours ago", - device: "MacBook Pro", - }, - { action: "Tagged as 'Travel'", time: "3 hours ago", device: "iPhone" }, - { action: "Created", time: "Jan 15, 2025", device: "iPhone" }, - ]; + const activity = [ + { action: "Synced to NAS", time: "2 min ago", device: "MacBook Pro" }, + { action: "Uploaded to S3", time: "1 hour ago", device: "MacBook Pro" }, + { + action: "Thumbnail generated", + time: "2 hours ago", + device: "MacBook Pro", + }, + { action: "Tagged as 'Travel'", time: "3 hours ago", device: "iPhone" }, + { action: "Created", time: "Jan 15, 2025", device: "iPhone" }, + ]; - return ( -
    -

    - History of changes and sync operations -

    + return ( +
    +

    + History of changes and sync operations +

    -
    - {activity.map((item, i) => ( -
    - - - -
    -
    - {item.action} -
    -
    - {item.time} · {item.device} -
    -
    -
    - ))} -
    -
    - ); +
    + {activity.map((item, i) => ( +
    + + + +
    +
    {item.action}
    +
    + {item.time} · {item.device} +
    +
    +
    + ))} +
    +
    + ); } function DetailsTab({ file }: { file: File }) { - return ( -
    - {/* Content Identity */} - {file.content_identity && ( -
    - - {file.content_identity.integrity_hash && ( - - )} - {file.content_identity.mime_type_id !== null && ( - - )} -
    - )} + return ( +
    + {/* Content Identity */} + {file.content_identity && ( +
    + + {file.content_identity.integrity_hash && ( + + )} + {file.content_identity.mime_type_id !== null && ( + + )} +
    + )} - {/* Metadata */} -
    - - - {file.extension && ( - - )} -
    + {/* Metadata */} +
    + + + {file.extension && ( + + )} +
    - {/* System */} -
    - - - -
    -
    - ); -} \ No newline at end of file + {/* System */} +
    + + + +
    +
    + ); +} diff --git a/packages/interface/src/components/Inspector/variants/KnowledgeInspector.tsx b/packages/interface/src/components/Inspector/variants/KnowledgeInspector.tsx index 3d54c5a4e..f4df921c7 100644 --- a/packages/interface/src/components/Inspector/variants/KnowledgeInspector.tsx +++ b/packages/interface/src/components/Inspector/variants/KnowledgeInspector.tsx @@ -1,6 +1,6 @@ -import { Sparkle, PaperPlaneRight, Paperclip } from "@phosphor-icons/react"; -import { useState } from "react"; +import { Paperclip, PaperPlaneRight, Sparkle } from "@phosphor-icons/react"; import clsx from "clsx"; +import { useState } from "react"; interface Message { id: number; @@ -47,15 +47,15 @@ export function KnowledgeInspector() { }; return ( -
    +
    {/* Header */} -
    +
    -
    +
    -
    +
    AI Assistant
    @@ -66,51 +66,51 @@ export function KnowledgeInspector() {
    {/* Messages */} -
    +
    {messages.map((msg) => (
    {/* Avatar */}
    {msg.role === "assistant" ? ( ) : ( -
    U
    +
    U
    )}
    {/* Message content */}
    -

    +

    {msg.content}

    - + {msg.timestamp.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", @@ -122,19 +122,18 @@ export function KnowledgeInspector() {
    {/* Input */} -
    +
    -
    +
    setMessage(e.target.value)} onKeyPress={(e) => { if (e.key === "Enter" && !e.shiftKey) { @@ -143,19 +142,20 @@ export function KnowledgeInspector() { } }} placeholder="Ask me anything..." - className="flex-1 bg-transparent text-sm text-sidebar-ink placeholder:text-sidebar-inkDull outline-none" + type="text" + value={message} />
    - -
    ); -} \ No newline at end of file +} diff --git a/packages/interface/src/components/Inspector/variants/LocationInspector.tsx b/packages/interface/src/components/Inspector/variants/LocationInspector.tsx index 4aada693d..e0b428fc9 100644 --- a/packages/interface/src/components/Inspector/variants/LocationInspector.tsx +++ b/packages/interface/src/components/Inspector/variants/LocationInspector.tsx @@ -1,796 +1,782 @@ import { - Info, - Gear, - Briefcase, - ClockCounterClockwise, - HardDrive, - DotsThree, - Hash, - Sparkle, - Image, - MagnifyingGlass, - Trash, - FunnelX, - ToggleLeft, - ToggleRight, - X, - Play, - FilmStrip, - VideoCamera, + Briefcase, + ClockCounterClockwise, + DotsThree, + FilmStrip, + FunnelX, + Gear, + HardDrive, + Image, + Info, + MagnifyingGlass, + Play, + Sparkle, + ToggleLeft, + ToggleRight, + Trash, + VideoCamera, + X, } from "@phosphor-icons/react"; +import LocationIcon from "@sd/assets/icons/Location.png"; +import type { Location } from "@sd/ts-client"; +import { + Button, + Dialog, + dialogManager, + type UseDialogProps, + useDialog, +} from "@sd/ui"; +import { useQueryClient } from "@tanstack/react-query"; +import clsx from "clsx"; import { useState } from "react"; import { useForm } from "react-hook-form"; -import { useQueryClient } from "@tanstack/react-query"; -import { - InfoRow, - Section, - Divider, - Tabs, - TabContent, -} from "../Inspector"; -import clsx from "clsx"; -import type { Location } from "@sd/ts-client"; -import { Button, Dialog, dialogManager, useDialog, type UseDialogProps } from "@sd/ui"; import { useLibraryMutation } from "../../../contexts/SpacedriveContext"; -import LocationIcon from "@sd/assets/icons/Location.png"; +import { Divider, InfoRow, Section, TabContent, Tabs } from "../Inspector"; interface LocationInspectorProps { - location: Location; + location: Location; } export function LocationInspector({ location }: LocationInspectorProps) { - const [activeTab, setActiveTab] = useState("overview"); + const [activeTab, setActiveTab] = useState("overview"); - const tabs = [ - { id: "overview", label: "Overview", icon: Info }, - { id: "indexing", label: "Indexing", icon: Gear }, - { id: "jobs", label: "Jobs", icon: Briefcase }, - { id: "activity", label: "Activity", icon: ClockCounterClockwise }, - { id: "devices", label: "Devices", icon: HardDrive }, - { id: "more", label: "More", icon: DotsThree }, - ]; + const tabs = [ + { id: "overview", label: "Overview", icon: Info }, + { id: "indexing", label: "Indexing", icon: Gear }, + { id: "jobs", label: "Jobs", icon: Briefcase }, + { id: "activity", label: "Activity", icon: ClockCounterClockwise }, + { id: "devices", label: "Devices", icon: HardDrive }, + { id: "more", label: "More", icon: DotsThree }, + ]; - return ( - <> - {/* Tabs */} - + return ( + <> + {/* Tabs */} + - {/* Tab Content */} -
    - - - + {/* Tab Content */} +
    + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - -
    - - ); + + + +
    + + ); } function OverviewTab({ location }: { location: LocationInfo }) { - const rescanLocation = useLibraryMutation("locations.rescan"); + const rescanLocation = useLibraryMutation("locations.rescan"); - const formatBytes = (bytes: number | null | undefined) => { - if (!bytes || bytes === 0) return "0 B"; - const k = 1024; - const sizes = ["B", "KB", "MB", "GB", "TB"]; - const i = Math.floor(Math.log(bytes) / Math.log(k)); - return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`; - }; + const formatBytes = (bytes: number | null | undefined) => { + if (!bytes || bytes === 0) return "0 B"; + const k = 1024; + const sizes = ["B", "KB", "MB", "GB", "TB"]; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return `${Number.parseFloat((bytes / k ** i).toFixed(2))} ${sizes[i]}`; + }; - const formatDate = (dateStr: string) => { - const date = new Date(dateStr); - return date.toLocaleDateString("en-US", { - month: "short", - day: "numeric", - year: "numeric", - hour: "2-digit", - minute: "2-digit", - }); - }; + const formatDate = (dateStr: string) => { + const date = new Date(dateStr); + return date.toLocaleDateString("en-US", { + month: "short", + day: "numeric", + year: "numeric", + hour: "2-digit", + minute: "2-digit", + }); + }; - const formatScanState = (scanState: any) => { - if (!scanState) return "Unknown"; - if (scanState.Idle) return "Idle"; - if (scanState.Scanning) return `Scanning ${scanState.Scanning.progress}%`; - if (scanState.Completed) return "Completed"; - if (scanState.Failed) return "Failed"; - return "Unknown"; - }; + const formatScanState = (scanState: any) => { + if (!scanState) return "Unknown"; + if (scanState.Idle) return "Idle"; + if (scanState.Scanning) return `Scanning ${scanState.Scanning.progress}%`; + if (scanState.Completed) return "Completed"; + if (scanState.Failed) return "Failed"; + return "Unknown"; + }; - return ( -
    - {/* Location icon */} -
    - Location -
    + return ( +
    + {/* Location icon */} +
    + Location +
    - {/* Location name */} -
    -

    - {location.name || "Unnamed Location"} -

    -

    - Local Storage -

    -
    + {/* Location name */} +
    +

    + {location.name || "Unnamed Location"} +

    +

    Local Storage

    +
    - + - {/* Details */} -
    - - {location.total_file_count != null && ( - - )} - - - {location.last_scan_at && ( - - )} -
    + {/* Details */} +
    + + {location.total_file_count != null && ( + + )} + + + {location.last_scan_at && ( + + )} +
    - {/* Index Mode */} -
    - -
    + {/* Index Mode */} +
    + +
    - {/* Quick Actions */} -
    -
    - - -
    -
    -
    - ); + {/* Quick Actions */} +
    +
    + + +
    +
    +
    + ); } function IndexingTab({ location }: { location: LocationInfo }) { - const [indexMode, setIndexMode] = useState<"shallow" | "content" | "deep">( - location.index_mode as "shallow" | "content" | "deep", - ); - const [ignoreRules, setIgnoreRules] = useState([ - ".git", - "node_modules", - "*.tmp", - ".DS_Store", - ]); + const [indexMode, setIndexMode] = useState<"shallow" | "content" | "deep">( + location.index_mode as "shallow" | "content" | "deep" + ); + const [ignoreRules, setIgnoreRules] = useState([ + ".git", + "node_modules", + "*.tmp", + ".DS_Store", + ]); - return ( -
    -
    -

    - Controls how deeply this location is indexed -

    + return ( +
    +
    +

    + Controls how deeply this location is indexed +

    -
    - setIndexMode("shallow")} - /> - setIndexMode("content")} - /> - setIndexMode("deep")} - /> -
    -
    +
    + setIndexMode("shallow")} + value="shallow" + /> + setIndexMode("content")} + value="content" + /> + setIndexMode("deep")} + value="deep" + /> +
    +
    -
    -

    - Files and folders matching these patterns will be ignored -

    +
    +

    + Files and folders matching these patterns will be ignored +

    -
    - {ignoreRules.map((pattern, i) => ( - { - setIgnoreRules( - ignoreRules.filter((_, idx) => idx !== i), - ); - }} - /> - ))} -
    +
    + {ignoreRules.map((pattern, i) => ( + { + setIgnoreRules(ignoreRules.filter((_, idx) => idx !== i)); + }} + pattern={pattern} + /> + ))} +
    - -
    -
    - ); + + +
    + ); } function JobsTab({ location }: { location: LocationInfo }) { - const updateLocation = useLibraryMutation("locations.update"); - const triggerJob = useLibraryMutation("locations.triggerJob"); + const updateLocation = useLibraryMutation("locations.update"); + const triggerJob = useLibraryMutation("locations.triggerJob"); - const updatePolicy = async ( - updates: Partial, - ) => { - await updateLocation.mutateAsync({ - id: location.id, - job_policies: { - ...location.job_policies, - ...updates, - }, - }); - }; + const updatePolicy = async ( + updates: Partial + ) => { + await updateLocation.mutateAsync({ + id: location.id, + job_policies: { + ...location.job_policies, + ...updates, + }, + }); + }; - const thumbnails = location.job_policies?.thumbnail?.enabled ?? true; - const thumbstrips = location.job_policies?.thumbstrip?.enabled ?? true; - const proxies = location.job_policies?.proxy?.enabled ?? false; - const ocr = location.job_policies?.ocr?.enabled ?? false; - const speech = location.job_policies?.speech_to_text?.enabled ?? false; + const thumbnails = location.job_policies?.thumbnail?.enabled ?? true; + const thumbstrips = location.job_policies?.thumbstrip?.enabled ?? true; + const proxies = location.job_policies?.proxy?.enabled ?? false; + const ocr = location.job_policies?.ocr?.enabled ?? false; + const speech = location.job_policies?.speech_to_text?.enabled ?? false; - return ( -
    -

    - Configure which processing jobs run automatically for this - location -

    + return ( +
    +

    + Configure which processing jobs run automatically for this location +

    -
    -
    - - updatePolicy({ - thumbnail: { - ...(location.job_policies?.thumbnail ?? {}), - enabled, - }, - }) - } - onTrigger={() => - triggerJob.mutate({ - location_id: location.id, - job_type: "thumbnail", - force: false, - }) - } - isTriggering={triggerJob.isPending} - /> - - updatePolicy({ - thumbstrip: { - ...(location.job_policies?.thumbstrip ?? {}), - enabled, - }, - }) - } - onTrigger={() => - triggerJob.mutate({ - location_id: location.id, - job_type: "thumbstrip", - force: false, - }) - } - isTriggering={triggerJob.isPending} - icon={FilmStrip} - /> - - updatePolicy({ - proxy: { - ...(location.job_policies?.proxy ?? {}), - enabled, - }, - }) - } - onTrigger={() => - triggerJob.mutate({ - location_id: location.id, - job_type: "proxy", - force: false, - }) - } - isTriggering={triggerJob.isPending} - icon={VideoCamera} - /> -
    -
    +
    +
    + + updatePolicy({ + thumbnail: { + ...(location.job_policies?.thumbnail ?? {}), + enabled, + }, + }) + } + onTrigger={() => + triggerJob.mutate({ + location_id: location.id, + job_type: "thumbnail", + force: false, + }) + } + /> + + updatePolicy({ + thumbstrip: { + ...(location.job_policies?.thumbstrip ?? {}), + enabled, + }, + }) + } + onTrigger={() => + triggerJob.mutate({ + location_id: location.id, + job_type: "thumbstrip", + force: false, + }) + } + /> + + updatePolicy({ + proxy: { + ...(location.job_policies?.proxy ?? {}), + enabled, + }, + }) + } + onTrigger={() => + triggerJob.mutate({ + location_id: location.id, + job_type: "proxy", + force: false, + }) + } + /> +
    +
    -
    -
    - - updatePolicy({ - ocr: { ...(location.job_policies?.ocr ?? {}), enabled }, - }) - } - onTrigger={() => - triggerJob.mutate({ - location_id: location.id, - job_type: "ocr", - force: false, - }) - } - isTriggering={triggerJob.isPending} - /> - - updatePolicy({ - speech_to_text: { - ...(location.job_policies?.speech_to_text ?? {}), - enabled, - }, - }) - } - onTrigger={() => - triggerJob.mutate({ - location_id: location.id, - job_type: "speech_to_text", - force: false, - }) - } - isTriggering={triggerJob.isPending} - /> -
    -
    -
    - ); +
    +
    + + updatePolicy({ + ocr: { ...(location.job_policies?.ocr ?? {}), enabled }, + }) + } + onTrigger={() => + triggerJob.mutate({ + location_id: location.id, + job_type: "ocr", + force: false, + }) + } + /> + + updatePolicy({ + speech_to_text: { + ...(location.job_policies?.speech_to_text ?? {}), + enabled, + }, + }) + } + onTrigger={() => + triggerJob.mutate({ + location_id: location.id, + job_type: "speech_to_text", + force: false, + }) + } + /> +
    +
    +
    + ); } function ActivityTab({ location }: { location: LocationInfo }) { - const activity = [ - { action: "Full Scan Completed", time: "10 min ago", files: 12456 }, - { action: "Thumbnails Generated", time: "1 hour ago", files: 234 }, - { action: "Content Hashes Updated", time: "3 hours ago", files: 5678 }, - { action: "Metadata Extracted", time: "5 hours ago", files: 890 }, - { action: "Location Added", time: "Jan 15, 2025", files: 0 }, - ]; + const activity = [ + { action: "Full Scan Completed", time: "10 min ago", files: 12_456 }, + { action: "Thumbnails Generated", time: "1 hour ago", files: 234 }, + { action: "Content Hashes Updated", time: "3 hours ago", files: 5678 }, + { action: "Metadata Extracted", time: "5 hours ago", files: 890 }, + { action: "Location Added", time: "Jan 15, 2025", files: 0 }, + ]; - return ( -
    -

    - Recent indexing activity and job history -

    + return ( +
    +

    + Recent indexing activity and job history +

    -
    - {activity.map((item, i) => ( -
    - -
    -
    - {item.action} -
    -
    - {item.time} - {item.files > 0 && - ` · ${item.files.toLocaleString()} files`} -
    -
    -
    - ))} -
    -
    - ); +
    + {activity.map((item, i) => ( +
    + +
    +
    {item.action}
    +
    + {item.time} + {item.files > 0 && ` · ${item.files.toLocaleString()} files`} +
    +
    +
    + ))} +
    +
    + ); } function DevicesTab({ location }: { location: LocationInfo }) { - const devices = [ - { - name: "MacBook Pro", - status: "online" as const, - lastSeen: "2 min ago", - }, - { - name: "Desktop PC", - status: "offline" as const, - lastSeen: "2 days ago", - }, - { - name: "Home Server", - status: "online" as const, - lastSeen: "5 min ago", - }, - ]; + const devices = [ + { + name: "MacBook Pro", + status: "online" as const, + lastSeen: "2 min ago", + }, + { + name: "Desktop PC", + status: "offline" as const, + lastSeen: "2 days ago", + }, + { + name: "Home Server", + status: "online" as const, + lastSeen: "5 min ago", + }, + ]; - return ( -
    -

    - Devices that have access to this location -

    + return ( +
    +

    + Devices that have access to this location +

    -
    - {devices.map((device, i) => ( -
    -
    - -
    -
    - {device.name} -
    -
    -
    - - {device.status === "online" - ? "Online" - : "Offline"}{" "} - · {device.lastSeen} - -
    -
    -
    -
    - ))} -
    -
    - ); +
    + {devices.map((device, i) => ( +
    +
    + +
    +
    + {device.name} +
    +
    +
    + + {device.status === "online" ? "Online" : "Offline"} ·{" "} + {device.lastSeen} + +
    +
    +
    +
    + ))} +
    +
    + ); } interface DeleteLocationDialogProps extends UseDialogProps { - locationId: number; - locationName: string; + locationId: number; + locationName: string; } function useDeleteLocationDialog() { - return (locationId: number, locationName: string) => - dialogManager.create((props: DeleteLocationDialogProps) => ( - - )); + return (locationId: number, locationName: string) => + dialogManager.create((props: DeleteLocationDialogProps) => ( + + )); } -function DeleteLocationDialog({ locationId, locationName, ...props }: DeleteLocationDialogProps) { - const dialog = useDialog(props); - const form = useForm(); - const queryClient = useQueryClient(); - const removeLocation = useLibraryMutation("locations.remove", { - onSuccess: () => { - // Manually invalidate the locations query until the backend emits ResourceDeleted events - // This forces a refetch so the location disappears from the sidebar immediately - queryClient.invalidateQueries({ - predicate: (query) => { - const key = query.queryKey; - return Array.isArray(key) && key[0] === "query:locations.list"; - }, - }); +function DeleteLocationDialog({ + locationId, + locationName, + ...props +}: DeleteLocationDialogProps) { + const dialog = useDialog(props); + const form = useForm(); + const queryClient = useQueryClient(); + const removeLocation = useLibraryMutation("locations.remove", { + onSuccess: () => { + // Manually invalidate the locations query until the backend emits ResourceDeleted events + // This forces a refetch so the location disappears from the sidebar immediately + queryClient.invalidateQueries({ + predicate: (query) => { + const key = query.queryKey; + return Array.isArray(key) && key[0] === "query:locations.list"; + }, + }); - // Close the dialog - dialogManager.setState(dialog.id, { open: false }); - }, - }); + // Close the dialog + dialogManager.setState(dialog.id, { open: false }); + }, + }); - const handleDelete = async () => { - try { - await removeLocation.mutateAsync({ - location_id: String(locationId), - }); - } catch (error) { - console.error("Failed to remove location:", error); - } - }; + const handleDelete = async () => { + try { + await removeLocation.mutateAsync({ + location_id: String(locationId), + }); + } catch (error) { + console.error("Failed to remove location:", error); + } + }; - return ( - } - ctaLabel="Remove Location" - ctaDanger - cancelLabel="Cancel" - cancelBtn - onSubmit={handleDelete} - loading={removeLocation.isPending} - /> - ); + return ( + } + loading={removeLocation.isPending} + onSubmit={handleDelete} + title="Remove Location" + /> + ); } function MoreTab({ location }: { location: LocationInfo }) { - const openDeleteDialog = useDeleteLocationDialog(); + const openDeleteDialog = useDeleteLocationDialog(); - const formatDate = (dateStr: string) => { - const date = new Date(dateStr); - return date.toLocaleDateString("en-US", { - month: "short", - day: "numeric", - year: "numeric", - hour: "2-digit", - minute: "2-digit", - }); - }; + const formatDate = (dateStr: string) => { + const date = new Date(dateStr); + return date.toLocaleDateString("en-US", { + month: "short", + day: "numeric", + year: "numeric", + hour: "2-digit", + minute: "2-digit", + }); + }; - return ( -
    -
    - - {location.created_at && ( - - )} - {location.last_scan_at && ( - - )} -
    + return ( +
    +
    + + {location.created_at && ( + + )} + {location.last_scan_at && ( + + )} +
    -
    -

    - Removing this location will not delete your files -

    - -
    -
    - ); +
    +

    + Removing this location will not delete your files +

    + +
    +
    + ); } // Helper Components interface RadioOptionProps { - value: string; - label: string; - description: string; - checked: boolean; - onChange: () => void; + value: string; + label: string; + description: string; + checked: boolean; + onChange: () => void; } function RadioOption({ - value, - label, - description, - checked, - onChange, + value, + label, + description, + checked, + onChange, }: RadioOptionProps) { - return ( - - ); + return ( + + ); } interface IgnoreRuleProps { - pattern: string; - onRemove: () => void; + pattern: string; + onRemove: () => void; } function IgnoreRule({ pattern, onRemove }: IgnoreRuleProps) { - return ( -
    - - {pattern} - - -
    - ); + return ( +
    + + {pattern} + + +
    + ); } interface JobConfigRowProps { - label: string; - description: string; - enabled: boolean; - onToggle: (enabled: boolean) => void; - onTrigger: () => void; - isTriggering: boolean; - icon?: React.ComponentType; + label: string; + description: string; + enabled: boolean; + onToggle: (enabled: boolean) => void; + onTrigger: () => void; + isTriggering: boolean; + icon?: React.ComponentType; } function JobConfigRow({ - label, - description, - enabled, - onToggle, - onTrigger, - isTriggering, - icon: Icon, + label, + description, + enabled, + onToggle, + onTrigger, + isTriggering, + icon: Icon, }: JobConfigRowProps) { - return ( -
    - {/* Header with toggle and icon */} -
    - + return ( +
    + {/* Header with toggle and icon */} +
    + - {/* Description */} -

    - {description} -

    -
    + {/* Description */} +

    + {description} +

    +
    - {/* Run button */} - -
    - ); -} \ No newline at end of file + {/* Run button */} + +
    + ); +} diff --git a/packages/interface/src/components/Inspector/variants/MultiFileInspector.tsx b/packages/interface/src/components/Inspector/variants/MultiFileInspector.tsx index dd92a828b..8a6a2f036 100644 --- a/packages/interface/src/components/Inspector/variants/MultiFileInspector.tsx +++ b/packages/interface/src/components/Inspector/variants/MultiFileInspector.tsx @@ -1,225 +1,215 @@ -import { - Files, - Tag as TagIcon, - Calendar, - HardDrive, - Folder, -} from "@phosphor-icons/react"; -import { useMemo } from "react"; -import { InfoRow, Section, Divider, Tag } from "../Inspector"; -import clsx from "clsx"; +import { Calendar, Files, Folder, Tag as TagIcon } from "@phosphor-icons/react"; import type { File } from "@sd/ts-client"; -import { formatBytes, getContentKind } from "../../../routes/explorer/utils"; +import clsx from "clsx"; +import { useMemo } from "react"; import { File as FileComponent } from "../../../routes/explorer/File"; +import { formatBytes, getContentKind } from "../../../routes/explorer/utils"; +import { Divider, InfoRow, Section, Tag } from "../Inspector"; interface MultiFileInspectorProps { - files: File[]; + files: File[]; } export function MultiFileInspector({ files }: MultiFileInspectorProps) { - // Get last 3 files for thumbnail stacking (v1 style) - const thumbnailFiles = useMemo(() => { - return files.slice(-3).reverse(); - }, [files]); + // Get last 3 files for thumbnail stacking (v1 style) + const thumbnailFiles = useMemo(() => { + return files.slice(-3).reverse(); + }, [files]); - // Calculate aggregated metadata - const aggregatedData = useMemo(() => { - const totalSize = files.reduce((sum, file) => sum + (file.size || 0), 0); + // Calculate aggregated metadata + const aggregatedData = useMemo(() => { + const totalSize = files.reduce((sum, file) => sum + (file.size || 0), 0); - // Group by content kind - const kindCounts = new Map(); - files.forEach((file) => { - const kind = getContentKind(file) || "unknown"; - kindCounts.set(kind, (kindCounts.get(kind) || 0) + 1); - }); + // Group by content kind + const kindCounts = new Map(); + files.forEach((file) => { + const kind = getContentKind(file) || "unknown"; + kindCounts.set(kind, (kindCounts.get(kind) || 0) + 1); + }); - // Get all tags with counts - const tagMap = new Map< - string, - { id: string; name: string; color: string; count: number } - >(); - files.forEach((file) => { - file.tags?.forEach((tag) => { - if (tagMap.has(tag.id)) { - tagMap.get(tag.id)!.count++; - } else { - tagMap.set(tag.id, { - id: tag.id, - name: tag.canonical_name, - color: tag.color || "#3B82F6", - count: 1, - }); - } - }); - }); + // Get all tags with counts + const tagMap = new Map< + string, + { id: string; name: string; color: string; count: number } + >(); + files.forEach((file) => { + file.tags?.forEach((tag) => { + if (tagMap.has(tag.id)) { + tagMap.get(tag.id)!.count++; + } else { + tagMap.set(tag.id, { + id: tag.id, + name: tag.canonical_name, + color: tag.color || "#3B82F6", + count: 1, + }); + } + }); + }); - // Calculate date ranges - const dates = { - created: files - .map((f) => f.created_at) - .filter(Boolean) - .sort(), - modified: files - .map((f) => f.modified_at) - .filter(Boolean) - .sort(), - }; + // Calculate date ranges + const dates = { + created: files + .map((f) => f.created_at) + .filter(Boolean) + .sort(), + modified: files + .map((f) => f.modified_at) + .filter(Boolean) + .sort(), + }; - return { - totalSize, - kindCounts: Array.from(kindCounts.entries()).sort( - (a, b) => b[1] - a[1], - ), - tags: Array.from(tagMap.values()).sort((a, b) => b.count - a.count), - dateRanges: { - created: - dates.created.length > 0 - ? { - earliest: dates.created[0], - latest: dates.created[dates.created.length - 1], - } - : null, - modified: - dates.modified.length > 0 - ? { - earliest: dates.modified[0], - latest: dates.modified[dates.modified.length - 1], - } - : null, - }, - }; - }, [files]); + return { + totalSize, + kindCounts: Array.from(kindCounts.entries()).sort((a, b) => b[1] - a[1]), + tags: Array.from(tagMap.values()).sort((a, b) => b.count - a.count), + dateRanges: { + created: + dates.created.length > 0 + ? { + earliest: dates.created[0], + latest: dates.created[dates.created.length - 1], + } + : null, + modified: + dates.modified.length > 0 + ? { + earliest: dates.modified[0], + latest: dates.modified[dates.modified.length - 1], + } + : null, + }, + }; + }, [files]); - return ( -
    - {/* Stacked thumbnails (v1 style) */} -
    -
    - {thumbnailFiles.map((file, i, thumbs) => ( -
    1 && "!absolute", - i === 0 && - thumbs.length > 1 && - "z-30 !h-[76%] !w-[76%]", - i === 1 && "z-20 !h-4/5 !w-4/5 rotate-[-5deg]", - i === 2 && "z-10 !h-[84%] !w-[84%] rotate-[7deg]", - )} - > - 1 && - "shadow-md shadow-black/20", - )} - /> -
    - ))} -
    -
    + return ( +
    + {/* Stacked thumbnails (v1 style) */} +
    +
    + {thumbnailFiles.map((file, i, thumbs) => ( +
    1 && "!absolute", + i === 0 && thumbs.length > 1 && "!h-[76%] !w-[76%] z-30", + i === 1 && "!h-4/5 !w-4/5 z-20 rotate-[-5deg]", + i === 2 && "!h-[84%] !w-[84%] z-10 rotate-[7deg]" + )} + key={file.id} + > + 1 && "shadow-black/20 shadow-md" + )} + file={file} + size={thumbs.length === 1 ? 240 : 180} + /> +
    + ))} +
    +
    - {/* File count header */} -
    -
    -

    - {files.length} Items Selected -

    -
    -
    + {/* File count header */} +
    +
    +

    + {files.length} Items Selected +

    +
    +
    - + - {/* Summary section */} -
    - - -
    + {/* Summary section */} +
    + + +
    - {/* File types breakdown */} - {aggregatedData.kindCounts.length > 0 && ( -
    - {aggregatedData.kindCounts.slice(0, 5).map(([kind, count]) => ( - - ))} -
    - )} + {/* File types breakdown */} + {aggregatedData.kindCounts.length > 0 && ( +
    + {aggregatedData.kindCounts.slice(0, 5).map(([kind, count]) => ( + + ))} +
    + )} - {/* Tags with opacity based on coverage */} - {aggregatedData.tags.length > 0 && ( -
    -
    - {aggregatedData.tags.map((tag) => { - const coverage = tag.count / files.length; - const opacity = coverage === 1 ? 1 : 0.5; + {/* Tags with opacity based on coverage */} + {aggregatedData.tags.length > 0 && ( +
    +
    + {aggregatedData.tags.map((tag) => { + const coverage = tag.count / files.length; + const opacity = coverage === 1 ? 1 : 0.5; - return ( -
    - - {tag.name} - {coverage < 1 && ( - - ({tag.count}) - - )} - -
    - ); - })} -
    -
    - )} + return ( +
    + + {tag.name} + {coverage < 1 && ( + ({tag.count}) + )} + +
    + ); + })} +
    +
    + )} - {/* Date ranges */} - {(aggregatedData.dateRanges.created || - aggregatedData.dateRanges.modified) && ( -
    - {aggregatedData.dateRanges.created && ( - - )} - {aggregatedData.dateRanges.modified && ( - - )} -
    - )} -
    - ); -} \ No newline at end of file + {/* Date ranges */} + {(aggregatedData.dateRanges.created || + aggregatedData.dateRanges.modified) && ( +
    + {aggregatedData.dateRanges.created && ( + + )} + {aggregatedData.dateRanges.modified && ( + + )} +
    + )} +
    + ); +} diff --git a/packages/interface/src/components/Inspector/variants/index.ts b/packages/interface/src/components/Inspector/variants/index.ts index dc86f3142..2b16724b7 100644 --- a/packages/interface/src/components/Inspector/variants/index.ts +++ b/packages/interface/src/components/Inspector/variants/index.ts @@ -1,2 +1,2 @@ export { FileInspector } from "./FileInspector"; -export { LocationInspector } from "./LocationInspector"; \ No newline at end of file +export { LocationInspector } from "./LocationInspector"; diff --git a/packages/interface/src/components/JobManager/JobManagerPopover.tsx b/packages/interface/src/components/JobManager/JobManagerPopover.tsx index 1b06d06b3..b5d2204b4 100644 --- a/packages/interface/src/components/JobManager/JobManagerPopover.tsx +++ b/packages/interface/src/components/JobManager/JobManagerPopover.tsx @@ -1,9 +1,14 @@ -import { ListBullets, CircleNotch, FunnelSimple, ArrowsOut } from "@phosphor-icons/react"; -import { Popover, usePopover, TopBarButton } from "@sd/ui"; +import { + ArrowsOut, + CircleNotch, + FunnelSimple, + ListBullets, +} from "@phosphor-icons/react"; +import { Popover, TopBarButton, usePopover } from "@sd/ui"; import clsx from "clsx"; -import { useState, useEffect } from "react"; +import { motion } from "framer-motion"; +import { useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; -import { motion, AnimatePresence } from "framer-motion"; import { JobList } from "./components/JobList"; import { useJobs } from "./hooks/useJobs"; import { CARD_HEIGHT } from "./types"; @@ -18,7 +23,8 @@ export function JobManagerPopover({ className }: JobManagerPopoverProps) { const [showOnlyRunning, setShowOnlyRunning] = useState(true); // Unified hook for job data and badge/icon - const { activeJobCount, hasRunningJobs, jobs, pause, resume, cancel } = useJobs(); + const { activeJobCount, hasRunningJobs, jobs, pause, resume, cancel } = + useJobs(); // Reset filter to "active only" when popover opens useEffect(() => { @@ -29,45 +35,45 @@ export function JobManagerPopover({ className }: JobManagerPopoverProps) { return (
    {hasRunningJobs ? ( - + ) : ( - + )}
    Jobs {activeJobCount > 0 && ( - + {activeJobCount} )} } - side="top" - align="start" - sideOffset={8} - className={clsx( - "w-[360px] max-h-[480px] z-50", - "!p-0 !bg-app !rounded-xl" - )} > {/* Header */} -
    -

    Job Manager

    +
    +

    Job Manager

    {activeJobCount > 0 && ( - + {activeJobCount} active )} @@ -81,8 +87,8 @@ export function JobManagerPopover({ className }: JobManagerPopoverProps) { {/* Filter toggle button */} setShowOnlyRunning(!showOnlyRunning)} title={showOnlyRunning ? "Show all jobs" : "Show only active jobs"} /> @@ -92,12 +98,12 @@ export function JobManagerPopover({ className }: JobManagerPopoverProps) { {/* Popover content with full job manager */} {popover.open && ( )} @@ -125,17 +131,22 @@ function JobManagerPopoverContent({ return ( - + ); } diff --git a/packages/interface/src/components/JobManager/JobsScreen/JobRow.tsx b/packages/interface/src/components/JobManager/JobsScreen/JobRow.tsx index 9080778b3..2e40fa96c 100644 --- a/packages/interface/src/components/JobManager/JobsScreen/JobRow.tsx +++ b/packages/interface/src/components/JobManager/JobsScreen/JobRow.tsx @@ -1,187 +1,181 @@ import { Pause, Play, X } from "@phosphor-icons/react"; -import { useState } from "react"; import clsx from "clsx"; -import type { JobListItem } from "../types"; -import { getJobDisplayName, formatDuration, timeAgo } from "../types"; +import { useState } from "react"; import { JobStatusIndicator } from "../components/JobStatusIndicator"; +import type { JobListItem } from "../types"; +import { formatDuration, getJobDisplayName, timeAgo } from "../types"; interface JobRowProps { - job: JobListItem; - onPause?: (jobId: string) => void; - onResume?: (jobId: string) => void; - onCancel?: (jobId: string) => void; + job: JobListItem; + onPause?: (jobId: string) => void; + onResume?: (jobId: string) => void; + onCancel?: (jobId: string) => void; } export function JobRow({ job, onPause, onResume, onCancel }: JobRowProps) { - const [isHovered, setIsHovered] = useState(false); + const [isHovered, setIsHovered] = useState(false); - const displayName = getJobDisplayName(job); - const showActionButton = - job.status === "running" || job.status === "paused"; - const canPause = job.status === "running" && onPause; - const canResume = job.status === "paused" && onResume; - const canCancel = (job.status === "running" || job.status === "paused") && onCancel; + const displayName = getJobDisplayName(job); + const showActionButton = job.status === "running" || job.status === "paused"; + const canPause = job.status === "running" && onPause; + const canResume = job.status === "paused" && onResume; + const canCancel = + (job.status === "running" || job.status === "paused") && onCancel; - const handleAction = (e: React.MouseEvent) => { - e.stopPropagation(); - if (canPause) { - onPause(job.id); - } else if (canResume) { - onResume(job.id); - } - }; + const handleAction = (e: React.MouseEvent) => { + e.stopPropagation(); + if (canPause) { + onPause(job.id); + } else if (canResume) { + onResume(job.id); + } + }; - const handleCancel = (e: React.MouseEvent) => { - e.stopPropagation(); - if (canCancel) { - onCancel(job.id); - } - }; + const handleCancel = (e: React.MouseEvent) => { + e.stopPropagation(); + if (canCancel) { + onCancel(job.id); + } + }; - // Format progress percentage - const progressPercent = Math.round(job.progress * 100); + // Format progress percentage + const progressPercent = Math.round(job.progress * 100); - // Get phase and message - const phase = job.current_phase; - const message = job.status_message; + // Get phase and message + const phase = job.current_phase; + const message = job.status_message; - // Calculate duration - prefer started_at for accuracy, fallback to created_at - const startTime = job.started_at || job.created_at; - const duration = startTime - ? job.completed_at - ? new Date(job.completed_at).getTime() - - new Date(startTime).getTime() - : Date.now() - new Date(startTime).getTime() - : 0; + // Calculate duration - prefer started_at for accuracy, fallback to created_at + const startTime = job.started_at || job.created_at; + const duration = startTime + ? job.completed_at + ? new Date(job.completed_at).getTime() - new Date(startTime).getTime() + : Date.now() - new Date(startTime).getTime() + : 0; - return ( -
    setIsHovered(true)} - onMouseLeave={() => setIsHovered(false)} - > - {/* Icon */} -
    - -
    + return ( +
    setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > + {/* Icon */} +
    + +
    - {/* Main info */} -
    - {/* Job name and details */} -
    -
    -

    - {displayName} -

    - {phase && ( - - {phase} - - )} -
    - {message && ( -

    - {message} -

    - )} -
    + {/* Main info */} +
    + {/* Job name and details */} +
    +
    +

    + {displayName} +

    + {phase && ( + + {phase} + + )} +
    + {message && ( +

    {message}

    + )} +
    - {/* Progress / Duration column */} -
    - {job.status === "running" || job.status === "paused" ? ( - // Show progress bar for active jobs -
    -
    -
    -
    - - {progressPercent}% - -
    - ) : job.status === "completed" ? ( - // Show duration for completed jobs - - {formatDuration(duration)} - - ) : job.status === "queued" ? ( - // Show waiting status for queued jobs - - Waiting... - - ) : ( - // Show dash for failed/cancelled jobs - - )} -
    + {/* Progress / Duration column */} +
    + {job.status === "running" || job.status === "paused" ? ( + // Show progress bar for active jobs +
    +
    +
    +
    + + {progressPercent}% + +
    + ) : job.status === "completed" ? ( + // Show duration for completed jobs + + {formatDuration(duration)} + + ) : job.status === "queued" ? ( + // Show waiting status for queued jobs + Waiting... + ) : ( + // Show dash for failed/cancelled jobs + + )} +
    - {/* Completed/Started time */} -
    - - {job.status === "completed" && job.completed_at - ? timeAgo(job.completed_at) - : job.status === "running" && job.started_at - ? timeAgo(job.started_at) - : job.created_at - ? timeAgo(job.created_at) - : "—"} - -
    + {/* Completed/Started time */} +
    + + {job.status === "completed" && job.completed_at + ? timeAgo(job.completed_at) + : job.status === "running" && job.started_at + ? timeAgo(job.started_at) + : job.created_at + ? timeAgo(job.created_at) + : "—"} + +
    - {/* Status */} -
    - - {job.status} - -
    -
    + {/* Status */} +
    + + {job.status} + +
    +
    - {/* Action buttons */} - {isHovered && ( -
    - {showActionButton && (canPause || canResume) && ( - - )} - {canCancel && ( - - )} -
    - )} -
    - ); + {/* Action buttons */} + {isHovered && ( +
    + {showActionButton && (canPause || canResume) && ( + + )} + {canCancel && ( + + )} +
    + )} +
    + ); } diff --git a/packages/interface/src/components/JobManager/JobsScreen/index.tsx b/packages/interface/src/components/JobManager/JobsScreen/index.tsx index 1ffe25f6e..bea17e501 100644 --- a/packages/interface/src/components/JobManager/JobsScreen/index.tsx +++ b/packages/interface/src/components/JobManager/JobsScreen/index.tsx @@ -1,4 +1,4 @@ -import { X, FunnelSimple } from "@phosphor-icons/react"; +import { FunnelSimple, X } from "@phosphor-icons/react"; import { TopBarButton } from "@sd/ui"; import { useState } from "react"; import { useNavigate } from "react-router-dom"; @@ -6,206 +6,180 @@ import { useJobs } from "../hooks/useJobs"; import { JobRow } from "./JobRow"; export function JobsScreen() { - const navigate = useNavigate(); - const { jobs, pause, resume, cancel } = useJobs(); - const [showOnlyRunning, setShowOnlyRunning] = useState(false); + const navigate = useNavigate(); + const { jobs, pause, resume, cancel } = useJobs(); + const [showOnlyRunning, setShowOnlyRunning] = useState(false); - // Filter jobs based on toggle - const filteredJobs = showOnlyRunning - ? jobs.filter( - (job) => job.status === "running" || job.status === "paused", - ) - : jobs; + // Filter jobs based on toggle + const filteredJobs = showOnlyRunning + ? jobs.filter((job) => job.status === "running" || job.status === "paused") + : jobs; - // Group jobs by status - const runningJobs = filteredJobs.filter((j) => j.status === "running"); - const pausedJobs = filteredJobs.filter((j) => j.status === "paused"); - const queuedJobs = filteredJobs.filter((j) => j.status === "queued"); - const completedJobs = filteredJobs.filter((j) => j.status === "completed"); - const failedJobs = filteredJobs.filter((j) => j.status === "failed"); + // Group jobs by status + const runningJobs = filteredJobs.filter((j) => j.status === "running"); + const pausedJobs = filteredJobs.filter((j) => j.status === "paused"); + const queuedJobs = filteredJobs.filter((j) => j.status === "queued"); + const completedJobs = filteredJobs.filter((j) => j.status === "completed"); + const failedJobs = filteredJobs.filter((j) => j.status === "failed"); - return ( -
    - {/* Header */} -
    -
    -
    -

    Jobs

    -
    - {jobs.length} total - {runningJobs.length > 0 && ( - <> - - {runningJobs.length} running - - )} -
    -
    + return ( +
    + {/* Header */} +
    +
    +
    +

    Jobs

    +
    + {jobs.length} total + {runningJobs.length > 0 && ( + <> + + {runningJobs.length} running + + )} +
    +
    -
    - {/* Filter toggle */} - setShowOnlyRunning(!showOnlyRunning)} - title={ - showOnlyRunning - ? "Show all jobs" - : "Show only active jobs" - } - /> +
    + {/* Filter toggle */} + setShowOnlyRunning(!showOnlyRunning)} + title={ + showOnlyRunning ? "Show all jobs" : "Show only active jobs" + } + /> - {/* Back button */} - navigate(-1)} - title="Go back" - /> -
    -
    + {/* Back button */} + navigate(-1)} + title="Go back" + /> +
    +
    - {/* Column headers */} -
    -
    {/* Icon spacer */} -
    -
    Name
    -
    Duration
    -
    - Time -
    -
    - Status -
    -
    -
    {" "} - {/* Action button spacer */} -
    -
    + {/* Column headers */} +
    +
    {/* Icon spacer */} +
    +
    Name
    +
    Duration
    +
    Time
    +
    Status
    +
    +
    {/* Action button spacer */} +
    +
    - {/* Content */} -
    - {filteredJobs.length === 0 ? ( -
    -
    -

    - No jobs found -

    -
    -
    - ) : ( -
    - {/* Running Jobs */} - {runningJobs.length > 0 && ( - - {runningJobs.map((job) => ( - - ))} - - )} + {/* Content */} +
    + {filteredJobs.length === 0 ? ( +
    +
    +

    No jobs found

    +
    +
    + ) : ( +
    + {/* Running Jobs */} + {runningJobs.length > 0 && ( + + {runningJobs.map((job) => ( + + ))} + + )} - {/* Paused Jobs */} - {pausedJobs.length > 0 && ( - - {pausedJobs.map((job) => ( - - ))} - - )} + {/* Paused Jobs */} + {pausedJobs.length > 0 && ( + + {pausedJobs.map((job) => ( + + ))} + + )} - {/* Queued Jobs */} - {queuedJobs.length > 0 && ( - - {queuedJobs.map((job) => ( - - ))} - - )} + {/* Queued Jobs */} + {queuedJobs.length > 0 && ( + + {queuedJobs.map((job) => ( + + ))} + + )} - {/* Completed Jobs */} - {completedJobs.length > 0 && ( - - {completedJobs.map((job) => ( - - ))} - - )} + {/* Completed Jobs */} + {completedJobs.length > 0 && ( + + {completedJobs.map((job) => ( + + ))} + + )} - {/* Failed Jobs */} - {failedJobs.length > 0 && ( - - {failedJobs.map((job) => ( - - ))} - - )} -
    - )} -
    -
    - ); + {/* Failed Jobs */} + {failedJobs.length > 0 && ( + + {failedJobs.map((job) => ( + + ))} + + )} +
    + )} +
    +
    + ); } interface JobSectionProps { - title: string; - count: number; - children: React.ReactNode; + title: string; + count: number; + children: React.ReactNode; } function JobSection({ title, count, children }: JobSectionProps) { - return ( -
    -
    -

    - {title} -

    - ({count}) -
    -
    {children}
    -
    - ); + return ( +
    +
    +

    + {title} +

    + ({count}) +
    +
    {children}
    +
    + ); } diff --git a/packages/interface/src/components/JobManager/components/JobCard.tsx b/packages/interface/src/components/JobManager/components/JobCard.tsx index 9edf1896d..f2d1ec419 100644 --- a/packages/interface/src/components/JobManager/components/JobCard.tsx +++ b/packages/interface/src/components/JobManager/components/JobCard.tsx @@ -1,6 +1,5 @@ -import { useState } from "react"; import { Pause, Play, X } from "@phosphor-icons/react"; -import clsx from "clsx"; +import { useState } from "react"; import type { JobListItem } from "../types"; import { CARD_HEIGHT, @@ -8,8 +7,8 @@ import { getJobSubtext, getStatusBadge, } from "../types"; -import { JobStatusIndicator } from "./JobStatusIndicator"; import { JobProgressBar } from "./JobProgressBar"; +import { JobStatusIndicator } from "./JobStatusIndicator"; interface JobCardProps { job: JobListItem; @@ -28,7 +27,8 @@ export function JobCard({ job, onPause, onResume, onCancel }: JobCardProps) { const showActionButton = job.status === "running" || job.status === "paused"; const canPause = job.status === "running" && onPause; const canResume = job.status === "paused" && onResume; - const canCancel = (job.status === "running" || job.status === "paused") && onCancel; + const canCancel = + (job.status === "running" || job.status === "paused") && onCancel; const handleAction = (e: React.MouseEvent) => { e.stopPropagation(); @@ -48,10 +48,10 @@ export function JobCard({ job, onPause, onResume, onCancel }: JobCardProps) { return (
    setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} + style={{ height: CARD_HEIGHT }} > {/* Left icon area */} @@ -60,14 +60,14 @@ export function JobCard({ job, onPause, onResume, onCancel }: JobCardProps) {
    {/* Main content area */} -
    +
    {/* Row 1: Title, badge, action button */} -
    - +
    + {displayName} - + {statusBadge} @@ -75,24 +75,28 @@ export function JobCard({ job, onPause, onResume, onCancel }: JobCardProps) {
    {showActionButton && (canPause || canResume) && ( )} {canCancel && ( )}
    @@ -102,7 +106,7 @@ export function JobCard({ job, onPause, onResume, onCancel }: JobCardProps) { {/* Row 2: Subtext */}
    {subtext} diff --git a/packages/interface/src/components/JobManager/components/JobList.tsx b/packages/interface/src/components/JobManager/components/JobList.tsx index b9e9b954d..2361f1baa 100644 --- a/packages/interface/src/components/JobManager/components/JobList.tsx +++ b/packages/interface/src/components/JobManager/components/JobList.tsx @@ -1,7 +1,7 @@ import { AnimatePresence, motion } from "framer-motion"; import type { JobListItem } from "../types"; -import { JobCard } from "./JobCard"; import { EmptyState } from "./EmptyState"; +import { JobCard } from "./JobCard"; interface JobListProps { jobs: JobListItem[]; @@ -20,13 +20,18 @@ export function JobList({ jobs, onPause, onResume, onCancel }: JobListProps) { {jobs.map((job) => ( - + ))} diff --git a/packages/interface/src/components/JobManager/components/JobProgressBar.tsx b/packages/interface/src/components/JobManager/components/JobProgressBar.tsx index 9e4412f4c..835291b97 100644 --- a/packages/interface/src/components/JobManager/components/JobProgressBar.tsx +++ b/packages/interface/src/components/JobManager/components/JobProgressBar.tsx @@ -8,7 +8,8 @@ interface JobProgressBarProps { export function JobProgressBar({ progress, status }: JobProgressBarProps) { // Only show progress bar for running, paused jobs, or completed jobs with some visual feedback - const showProgress = status === "running" || status === "paused" || status === "completed"; + const showProgress = + status === "running" || status === "paused" || status === "completed"; if (!showProgress) { return
    ; @@ -17,20 +18,23 @@ export function JobProgressBar({ progress, status }: JobProgressBarProps) { const isCompleted = status === "completed"; const isPending = status === "running" && progress === 0; // Use gray for completed jobs, status color for running/paused - const color = isCompleted ? "rgba(255, 255, 255, 0.2)" : JOB_STATUS_COLORS[status]; + const color = isCompleted + ? "rgba(255, 255, 255, 0.2)" + : JOB_STATUS_COLORS[status]; const displayProgress = Math.min(Math.max(progress, 0), 1); return (
    {isPending ? (
    ) : ( diff --git a/packages/interface/src/components/JobManager/components/JobStatusIndicator.tsx b/packages/interface/src/components/JobManager/components/JobStatusIndicator.tsx index efd456541..a984960a6 100644 --- a/packages/interface/src/components/JobManager/components/JobStatusIndicator.tsx +++ b/packages/interface/src/components/JobManager/components/JobStatusIndicator.tsx @@ -1,16 +1,13 @@ -import type { JobStatus } from "@sd/ts-client"; import { - MagnifyingGlass, - Image, + CheckCircle, + Database, Files, FolderOpen, - Database, - HardDrive, - FolderPlus, + Image, + MagnifyingGlass, Sparkle, - CheckCircle, } from "@phosphor-icons/react"; -import { motion, AnimatePresence } from "framer-motion"; +import { AnimatePresence, motion } from "framer-motion"; import type { JobListItem } from "../types"; interface JobStatusIndicatorProps { @@ -66,7 +63,7 @@ export function JobStatusIndicator({ job }: JobStatusIndicatorProps) { // If job has phases and we know the current phase, show carousel if (phases && currentPhase) { - const currentIndex = phases.findIndex(p => p.name === currentPhase); + const currentIndex = phases.findIndex((p) => p.name === currentPhase); // Show 3 icons: previous, current, next const PrevIcon = phases[currentIndex - 1]?.icon; @@ -74,41 +71,41 @@ export function JobStatusIndicator({ job }: JobStatusIndicatorProps) { const NextIcon = phases[currentIndex + 1]?.icon; return ( -
    -
    +
    +
    {/* Previous phase (dimmed) */} {PrevIcon && ( )} {/* Current phase (highlighted) */} {CurrentIcon && ( )} {/* Next phase (dimmed) */} {NextIcon && ( )} @@ -121,8 +118,8 @@ export function JobStatusIndicator({ job }: JobStatusIndicatorProps) { // No phases - show single icon const Icon = getJobIcon(job.name); return ( -
    - +
    +
    ); } diff --git a/packages/interface/src/components/JobManager/hooks/useJobCount.ts b/packages/interface/src/components/JobManager/hooks/useJobCount.ts index 7f7ffbd4e..c6edf7aa5 100644 --- a/packages/interface/src/components/JobManager/hooks/useJobCount.ts +++ b/packages/interface/src/components/JobManager/hooks/useJobCount.ts @@ -1,5 +1,8 @@ import { useEffect, useRef } from "react"; -import { useLibraryQuery, useSpacedriveClient } from "../../../contexts/SpacedriveContext"; +import { + useLibraryQuery, + useSpacedriveClient, +} from "../../../contexts/SpacedriveContext"; /** * Lightweight hook for job count indicator. @@ -7,60 +10,60 @@ import { useLibraryQuery, useSpacedriveClient } from "../../../contexts/Spacedri * Events trigger a refetch rather than incrementing/decrementing counts manually. */ export function useJobCount() { - const client = useSpacedriveClient(); + const client = useSpacedriveClient(); - const { data, refetch } = useLibraryQuery({ - type: "jobs.list", - input: { status: null }, - }); + const { data, refetch } = useLibraryQuery({ + type: "jobs.list", + input: { status: null }, + }); - // Ref for stable refetch access (prevents effect re-runs when refetch reference changes) - const refetchRef = useRef(refetch); - useEffect(() => { - refetchRef.current = refetch; - }, [refetch]); + // Ref for stable refetch access (prevents effect re-runs when refetch reference changes) + const refetchRef = useRef(refetch); + useEffect(() => { + refetchRef.current = refetch; + }, [refetch]); - // Subscribe to job state changes and refetch when they occur - useEffect(() => { - if (!client) return; + // Subscribe to job state changes and refetch when they occur + useEffect(() => { + if (!client) return; - let unsubscribe: (() => void) | undefined; - let isCancelled = false; + let unsubscribe: (() => void) | undefined; + let isCancelled = false; - const filter = { - event_types: [ - "JobQueued", - "JobStarted", - "JobCompleted", - "JobFailed", - "JobCancelled", - "JobPaused", - "JobResumed", - ], - }; + const filter = { + event_types: [ + "JobQueued", + "JobStarted", + "JobCompleted", + "JobFailed", + "JobCancelled", + "JobPaused", + "JobResumed", + ], + }; - client - .subscribeFiltered(filter, () => refetchRef.current()) - .then((unsub) => { - if (isCancelled) { - unsub(); - } else { - unsubscribe = unsub; - } - }); + client + .subscribeFiltered(filter, () => refetchRef.current()) + .then((unsub) => { + if (isCancelled) { + unsub(); + } else { + unsubscribe = unsub; + } + }); - return () => { - isCancelled = true; - unsubscribe?.(); - }; - }, [client]); + return () => { + isCancelled = true; + unsubscribe?.(); + }; + }, [client]); - const jobs = data?.jobs ?? []; - const runningCount = jobs.filter((j) => j.status === "running").length; - const pausedCount = jobs.filter((j) => j.status === "paused").length; + const jobs = data?.jobs ?? []; + const runningCount = jobs.filter((j) => j.status === "running").length; + const pausedCount = jobs.filter((j) => j.status === "paused").length; - return { - activeJobCount: runningCount + pausedCount, - hasRunningJobs: runningCount > 0, - }; -} \ No newline at end of file + return { + activeJobCount: runningCount + pausedCount, + hasRunningJobs: runningCount > 0, + }; +} diff --git a/packages/interface/src/components/JobManager/hooks/useJobManager.ts b/packages/interface/src/components/JobManager/hooks/useJobManager.ts index 9f11425b2..f02f43541 100644 --- a/packages/interface/src/components/JobManager/hooks/useJobManager.ts +++ b/packages/interface/src/components/JobManager/hooks/useJobManager.ts @@ -1,5 +1,9 @@ -import { useState, useEffect, useRef } from "react"; -import { useLibraryQuery, useLibraryMutation, useSpacedriveClient } from "../../../contexts/SpacedriveContext"; +import { useEffect, useRef, useState } from "react"; +import { + useLibraryMutation, + useLibraryQuery, + useSpacedriveClient, +} from "../../../contexts/SpacedriveContext"; import type { JobListItem } from "../types"; export function useJobManager() { @@ -34,9 +38,15 @@ export function useJobManager() { let isCancelled = false; const handleEvent = (event: any) => { - if ("JobQueued" in event || "JobStarted" in event || "JobCompleted" in event || - "JobFailed" in event || "JobPaused" in event || "JobResumed" in event || - "JobCancelled" in event) { + if ( + "JobQueued" in event || + "JobStarted" in event || + "JobCompleted" in event || + "JobFailed" in event || + "JobPaused" in event || + "JobResumed" in event || + "JobCancelled" in event + ) { refetchRef.current(); } else if ("JobProgress" in event) { const progressData = event.JobProgress; @@ -57,13 +67,22 @@ export function useJobManager() { status_message: generic.message, }), }; - }), + }) ); } }; const filter = { - event_types: ["JobQueued", "JobStarted", "JobProgress", "JobCompleted", "JobFailed", "JobPaused", "JobResumed", "JobCancelled"], + event_types: [ + "JobQueued", + "JobStarted", + "JobProgress", + "JobCompleted", + "JobFailed", + "JobPaused", + "JobResumed", + "JobCancelled", + ], }; client.subscribeFiltered(filter, handleEvent).then((unsub) => { @@ -96,4 +115,4 @@ export function useJobManager() { isLoading, error, }; -} \ No newline at end of file +} diff --git a/packages/interface/src/components/JobManager/hooks/useJobs.ts b/packages/interface/src/components/JobManager/hooks/useJobs.ts index 29de6f5d4..a90576954 100644 --- a/packages/interface/src/components/JobManager/hooks/useJobs.ts +++ b/packages/interface/src/components/JobManager/hooks/useJobs.ts @@ -1,7 +1,11 @@ -import { useState, useEffect, useRef, useMemo } from "react"; -import { useLibraryQuery, useLibraryMutation, useSpacedriveClient } from "../../../contexts/SpacedriveContext"; -import type { JobListItem } from "../types"; import { sounds } from "@sd/assets/sounds"; +import { useEffect, useMemo, useRef, useState } from "react"; +import { + useLibraryMutation, + useLibraryQuery, + useSpacedriveClient, +} from "../../../contexts/SpacedriveContext"; +import type { JobListItem } from "../types"; // Global set to track which jobs have already played their completion sound // This prevents multiple hook instances from playing the sound multiple times @@ -47,9 +51,15 @@ export function useJobs() { let isCancelled = false; const handleEvent = (event: any) => { - if ("JobQueued" in event || "JobStarted" in event || "JobCompleted" in event || - "JobFailed" in event || "JobPaused" in event || "JobResumed" in event || - "JobCancelled" in event) { + if ( + "JobQueued" in event || + "JobStarted" in event || + "JobCompleted" in event || + "JobFailed" in event || + "JobPaused" in event || + "JobResumed" in event || + "JobCancelled" in event + ) { if ("JobCompleted" in event) { const jobId = event.JobCompleted?.job_id; const jobType = event.JobCompleted?.job_type; @@ -87,13 +97,22 @@ export function useJobs() { status_message: generic.message, }), }; - }), + }) ); } }; const filter = { - event_types: ["JobQueued", "JobStarted", "JobProgress", "JobCompleted", "JobFailed", "JobPaused", "JobResumed", "JobCancelled"], + event_types: [ + "JobQueued", + "JobStarted", + "JobProgress", + "JobCompleted", + "JobFailed", + "JobPaused", + "JobResumed", + "JobCancelled", + ], }; client.subscribeFiltered(filter, handleEvent).then((unsub) => { @@ -156,4 +175,4 @@ export function useJobs() { isLoading, error, }; -} \ No newline at end of file +} diff --git a/packages/interface/src/components/JobManager/index.ts b/packages/interface/src/components/JobManager/index.ts index ff51ccd9e..c59f66986 100644 --- a/packages/interface/src/components/JobManager/index.ts +++ b/packages/interface/src/components/JobManager/index.ts @@ -1,4 +1,4 @@ +export { useJobs } from "./hooks/useJobs"; export { JobManagerPopover } from "./JobManagerPopover"; export { JobsScreen } from "./JobsScreen"; -export { useJobs } from "./hooks/useJobs"; export type { JobListItem } from "./types"; diff --git a/packages/interface/src/components/JobManager/types.ts b/packages/interface/src/components/JobManager/types.ts index 7b21c48ef..8f9b3af59 100644 --- a/packages/interface/src/components/JobManager/types.ts +++ b/packages/interface/src/components/JobManager/types.ts @@ -1,10 +1,14 @@ -import type { JobStatus, JobListItem as GeneratedJobListItem, JsonValue, SdPath } from "@sd/ts-client"; +import type { + JobListItem as GeneratedJobListItem, + JsonValue, + SdPath, +} from "@sd/ts-client"; // Extend the generated type with runtime fields from JobProgress events export type JobListItem = GeneratedJobListItem & { - current_phase?: string; - current_path?: SdPath; - status_message?: string; + current_phase?: string; + current_path?: SdPath; + status_message?: string; }; export const JOB_STATUS_COLORS = { @@ -32,9 +36,10 @@ export function getJobDisplayName(job: JobListItem): string { if (job.name === "thumbnail_generation") { return "Generating Thumbnails"; } - return job.name.split("_").map(word => - word.charAt(0).toUpperCase() + word.slice(1) - ).join(" "); + return job.name + .split("_") + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(" "); } const { action_type, action_input } = job.action_context; @@ -105,12 +110,15 @@ export function getJobSubtext(job: JobListItem): string { if (job.status_message) return job.status_message; if (job.current_phase) return job.current_phase; if (job.current_path) { - const pathStr = typeof job.current_path === 'string' - ? job.current_path - : JSON.stringify(job.current_path); + const pathStr = + typeof job.current_path === "string" + ? job.current_path + : JSON.stringify(job.current_path); return pathStr; } - return job.progress > 0 ? `${Math.round(job.progress * 100)}%` : "Processing..."; + return job.progress > 0 + ? `${Math.round(job.progress * 100)}%` + : "Processing..."; } case "completed": return "Completed"; @@ -197,7 +205,11 @@ function extractPath(input: JsonValue): string | null { // Handle Physical path: { Physical: { device_slug: "...", path: "..." } } if (typeof path === "object" && path !== null && "Physical" in path) { const physical = path.Physical; - if (typeof physical === "object" && physical !== null && "path" in physical) { + if ( + typeof physical === "object" && + physical !== null && + "path" in physical + ) { return String(physical.path); } } diff --git a/packages/interface/src/components/Orb.tsx b/packages/interface/src/components/Orb.tsx index e1c4a913a..2443e7dcc 100644 --- a/packages/interface/src/components/Orb.tsx +++ b/packages/interface/src/components/Orb.tsx @@ -1,24 +1,24 @@ "use client"; +import { Mesh, Program, Renderer, Triangle, Vec3 } from "ogl"; import { useEffect, useRef } from "react"; -import { Renderer, Program, Mesh, Triangle, Vec3 } from "ogl"; interface OrbProps { - hue?: number; - hoverIntensity?: number; - rotateOnHover?: boolean; - forceHoverState?: boolean; + hue?: number; + hoverIntensity?: number; + rotateOnHover?: boolean; + forceHoverState?: boolean; } export default function Orb({ - hue = 0, - hoverIntensity = 0.2, - rotateOnHover = true, - forceHoverState = false, + hue = 0, + hoverIntensity = 0.2, + rotateOnHover = true, + forceHoverState = false, }: OrbProps) { - const ctnDom = useRef(null); + const ctnDom = useRef(null); - const vert = /* glsl */ ` + const vert = /* glsl */ ` precision highp float; attribute vec2 position; attribute vec2 uv; @@ -29,7 +29,7 @@ export default function Orb({ } `; - const frag = /* glsl */ ` + const frag = /* glsl */ ` precision highp float; uniform float iTime; @@ -177,126 +177,125 @@ export default function Orb({ } `; - useEffect(() => { - const container = ctnDom.current; - if (!container) return; + useEffect(() => { + const container = ctnDom.current; + if (!container) return; - const renderer = new Renderer({ - alpha: true, - premultipliedAlpha: false, - }); - const gl = renderer.gl; - gl.clearColor(0, 0, 0, 0); - container.appendChild(gl.canvas); + const renderer = new Renderer({ + alpha: true, + premultipliedAlpha: false, + }); + const gl = renderer.gl; + gl.clearColor(0, 0, 0, 0); + container.appendChild(gl.canvas); - const geometry = new Triangle(gl); - const program = new Program(gl, { - vertex: vert, - fragment: frag, - uniforms: { - iTime: { value: 0 }, - iResolution: { - value: new Vec3( - gl.canvas.width, - gl.canvas.height, - gl.canvas.width / gl.canvas.height, - ), - }, - hue: { value: hue }, - hover: { value: 0 }, - rot: { value: 0 }, - hoverIntensity: { value: hoverIntensity }, - }, - }); + const geometry = new Triangle(gl); + const program = new Program(gl, { + vertex: vert, + fragment: frag, + uniforms: { + iTime: { value: 0 }, + iResolution: { + value: new Vec3( + gl.canvas.width, + gl.canvas.height, + gl.canvas.width / gl.canvas.height + ), + }, + hue: { value: hue }, + hover: { value: 0 }, + rot: { value: 0 }, + hoverIntensity: { value: hoverIntensity }, + }, + }); - const mesh = new Mesh(gl, { geometry, program }); + const mesh = new Mesh(gl, { geometry, program }); - function resize() { - if (!container) return; - const dpr = window.devicePixelRatio || 1; - const width = container.clientWidth; - const height = container.clientHeight; - renderer.setSize(width * dpr, height * dpr); - gl.canvas.style.width = width + "px"; - gl.canvas.style.height = height + "px"; - program.uniforms.iResolution.value.set( - gl.canvas.width, - gl.canvas.height, - gl.canvas.width / gl.canvas.height, - ); - } - window.addEventListener("resize", resize); - resize(); + function resize() { + if (!container) return; + const dpr = window.devicePixelRatio || 1; + const width = container.clientWidth; + const height = container.clientHeight; + renderer.setSize(width * dpr, height * dpr); + gl.canvas.style.width = width + "px"; + gl.canvas.style.height = height + "px"; + program.uniforms.iResolution.value.set( + gl.canvas.width, + gl.canvas.height, + gl.canvas.width / gl.canvas.height + ); + } + window.addEventListener("resize", resize); + resize(); - let targetHover = 0; - let lastTime = 0; - let currentRot = 0; - const rotationSpeed = 0.3; + let targetHover = 0; + let lastTime = 0; + let currentRot = 0; + const rotationSpeed = 0.3; - const handleMouseMove = (e: MouseEvent) => { - const rect = container.getBoundingClientRect(); - const x = e.clientX - rect.left; - const y = e.clientY - rect.top; - const width = rect.width; - const height = rect.height; - const size = Math.min(width, height); - const centerX = width / 2; - const centerY = height / 2; - const uvX = ((x - centerX) / size) * 2.0; - const uvY = ((y - centerY) / size) * 2.0; + const handleMouseMove = (e: MouseEvent) => { + const rect = container.getBoundingClientRect(); + const x = e.clientX - rect.left; + const y = e.clientY - rect.top; + const width = rect.width; + const height = rect.height; + const size = Math.min(width, height); + const centerX = width / 2; + const centerY = height / 2; + const uvX = ((x - centerX) / size) * 2.0; + const uvY = ((y - centerY) / size) * 2.0; - if (Math.sqrt(uvX * uvX + uvY * uvY) < 0.8) { - targetHover = 1; - } else { - targetHover = 0; - } - }; + if (Math.sqrt(uvX * uvX + uvY * uvY) < 0.8) { + targetHover = 1; + } else { + targetHover = 0; + } + }; - const handleMouseLeave = () => { - targetHover = 0; - }; + const handleMouseLeave = () => { + targetHover = 0; + }; - container.addEventListener("mousemove", handleMouseMove); - container.addEventListener("mouseleave", handleMouseLeave); + container.addEventListener("mousemove", handleMouseMove); + container.addEventListener("mouseleave", handleMouseLeave); - let rafId: number; - const update = (t: number) => { - rafId = requestAnimationFrame(update); - const dt = (t - lastTime) * 0.001; - lastTime = t; - program.uniforms.iTime.value = t * 0.001; - program.uniforms.hue.value = hue; - program.uniforms.hoverIntensity.value = hoverIntensity; + let rafId: number; + const update = (t: number) => { + rafId = requestAnimationFrame(update); + const dt = (t - lastTime) * 0.001; + lastTime = t; + program.uniforms.iTime.value = t * 0.001; + program.uniforms.hue.value = hue; + program.uniforms.hoverIntensity.value = hoverIntensity; - const effectiveHover = forceHoverState ? 1 : targetHover; - program.uniforms.hover.value += - (effectiveHover - program.uniforms.hover.value) * 0.1; + const effectiveHover = forceHoverState ? 1 : targetHover; + program.uniforms.hover.value += + (effectiveHover - program.uniforms.hover.value) * 0.1; - if (rotateOnHover && effectiveHover > 0.5) { - currentRot += dt * rotationSpeed; - } - program.uniforms.rot.value = currentRot; + if (rotateOnHover && effectiveHover > 0.5) { + currentRot += dt * rotationSpeed; + } + program.uniforms.rot.value = currentRot; - renderer.render({ scene: mesh }); - }; - rafId = requestAnimationFrame(update); + renderer.render({ scene: mesh }); + }; + rafId = requestAnimationFrame(update); - return () => { - cancelAnimationFrame(rafId); - window.removeEventListener("resize", resize); - container.removeEventListener("mousemove", handleMouseMove); - container.removeEventListener("mouseleave", handleMouseLeave); - container.removeChild(gl.canvas); - gl.getExtension("WEBGL_lose_context")?.loseContext(); - }; - }, [hue, hoverIntensity, rotateOnHover, forceHoverState]); + return () => { + cancelAnimationFrame(rafId); + window.removeEventListener("resize", resize); + container.removeEventListener("mousemove", handleMouseMove); + container.removeEventListener("mouseleave", handleMouseLeave); + container.removeChild(gl.canvas); + gl.getExtension("WEBGL_lose_context")?.loseContext(); + }; + }, [hue, hoverIntensity, rotateOnHover, forceHoverState]); - return ( -
    - ); + return ( +
    + ); } - diff --git a/packages/interface/src/components/QuickPreview/AudioPlayer.tsx b/packages/interface/src/components/QuickPreview/AudioPlayer.tsx index 8bd60c9bb..3c2c5be55 100644 --- a/packages/interface/src/components/QuickPreview/AudioPlayer.tsx +++ b/packages/interface/src/components/QuickPreview/AudioPlayer.tsx @@ -1,441 +1,423 @@ -import { useState, useRef, useEffect } from "react"; import { - Play, - Pause, - SpeakerHigh, - SpeakerSlash, - SkipBack, - SkipForward, + Pause, + Play, + SkipBack, + SkipForward, + SpeakerHigh, + SpeakerSlash, } from "@phosphor-icons/react"; -import { motion } from "framer-motion"; import type { File } from "@sd/ts-client"; +import { motion } from "framer-motion"; +import { useEffect, useRef, useState } from "react"; import { useServer } from "../../contexts/ServerContext"; interface SubtitleCue { - index: number; - startTime: number; - endTime: number; - text: string; + index: number; + startTime: number; + endTime: number; + text: string; } interface AudioPlayerProps { - src: string; - file: File; + src: string; + file: File; } function formatTime(seconds: number): string { - const mins = Math.floor(seconds / 60); - const secs = Math.floor(seconds % 60); - return `${mins}:${secs.toString().padStart(2, "0")}`; + const mins = Math.floor(seconds / 60); + const secs = Math.floor(seconds % 60); + return `${mins}:${secs.toString().padStart(2, "0")}`; } function parseSRT(srtContent: string): SubtitleCue[] { - const cues: SubtitleCue[] = []; - const blocks = srtContent.trim().split(/\n\s*\n/); + const cues: SubtitleCue[] = []; + const blocks = srtContent.trim().split(/\n\s*\n/); - for (const block of blocks) { - const lines = block.trim().split("\n"); - if (lines.length < 3) continue; + for (const block of blocks) { + const lines = block.trim().split("\n"); + if (lines.length < 3) continue; - const index = parseInt(lines[0], 10); - const timecodeMatch = lines[1].match( - /(\d{2}):(\d{2}):(\d{2}),(\d{3})\s*-->\s*(\d{2}):(\d{2}):(\d{2}),(\d{3})/, - ); + const index = Number.parseInt(lines[0], 10); + const timecodeMatch = lines[1].match( + /(\d{2}):(\d{2}):(\d{2}),(\d{3})\s*-->\s*(\d{2}):(\d{2}):(\d{2}),(\d{3})/ + ); - if (!timecodeMatch) continue; + if (!timecodeMatch) continue; - const startTime = - parseInt(timecodeMatch[1]) * 3600 + - parseInt(timecodeMatch[2]) * 60 + - parseInt(timecodeMatch[3]) + - parseInt(timecodeMatch[4]) / 1000; + const startTime = + Number.parseInt(timecodeMatch[1]) * 3600 + + Number.parseInt(timecodeMatch[2]) * 60 + + Number.parseInt(timecodeMatch[3]) + + Number.parseInt(timecodeMatch[4]) / 1000; - const endTime = - parseInt(timecodeMatch[5]) * 3600 + - parseInt(timecodeMatch[6]) * 60 + - parseInt(timecodeMatch[7]) + - parseInt(timecodeMatch[8]) / 1000; + const endTime = + Number.parseInt(timecodeMatch[5]) * 3600 + + Number.parseInt(timecodeMatch[6]) * 60 + + Number.parseInt(timecodeMatch[7]) + + Number.parseInt(timecodeMatch[8]) / 1000; - const text = lines.slice(2).join("\n"); + const text = lines.slice(2).join("\n"); - cues.push({ index, startTime, endTime, text }); - } + cues.push({ index, startTime, endTime, text }); + } - return cues; + return cues; } export function AudioPlayer({ src, file }: AudioPlayerProps) { - const audioRef = useRef(null); - const lyricsContainerRef = useRef(null); - const { buildSidecarUrl } = useServer(); - const [playing, setPlaying] = useState(false); - const [currentTime, setCurrentTime] = useState(0); - const [duration, setDuration] = useState(0); - const [volume, setVolume] = useState(1); - const [muted, setMuted] = useState(false); - const [seeking, setSeeking] = useState(false); - const [cues, setCues] = useState([]); - const [currentCueIndex, setCurrentCueIndex] = useState(-1); + const audioRef = useRef(null); + const lyricsContainerRef = useRef(null); + const { buildSidecarUrl } = useServer(); + const [playing, setPlaying] = useState(false); + const [currentTime, setCurrentTime] = useState(0); + const [duration, setDuration] = useState(0); + const [volume, setVolume] = useState(1); + const [muted, setMuted] = useState(false); + const [seeking, setSeeking] = useState(false); + const [cues, setCues] = useState([]); + const [currentCueIndex, setCurrentCueIndex] = useState(-1); - // Load SRT transcripts if available - useEffect(() => { - const srtSidecar = file.sidecars?.find( - (s) => s.kind === "transcript" && s.variant === "srt", - ); + // Load SRT transcripts if available + useEffect(() => { + const srtSidecar = file.sidecars?.find( + (s) => s.kind === "transcript" && s.variant === "srt" + ); - if (!srtSidecar || !file.content_identity?.uuid) { - return; - } + if (!(srtSidecar && file.content_identity?.uuid)) { + return; + } - const extension = - srtSidecar.format === "text" ? "txt" : srtSidecar.format; - const srtUrl = buildSidecarUrl( - file.content_identity.uuid, - srtSidecar.kind, - srtSidecar.variant, - extension, - ); + const extension = srtSidecar.format === "text" ? "txt" : srtSidecar.format; + const srtUrl = buildSidecarUrl( + file.content_identity.uuid, + srtSidecar.kind, + srtSidecar.variant, + extension + ); - if (!srtUrl) return; + if (!srtUrl) return; - fetch(srtUrl) - .then(async (res) => { - if (!res.ok) return null; - return res.text(); - }) - .then((srtContent) => { - if (!srtContent) return; - const parsed = parseSRT(srtContent); - console.log( - "[AudioPlayer] Loaded", - parsed.length, - "lyric lines", - ); - setCues(parsed); - }) - .catch((err) => - console.log("[AudioPlayer] Lyrics not available:", err.message), - ); - }, [file, buildSidecarUrl]); + fetch(srtUrl) + .then(async (res) => { + if (!res.ok) return null; + return res.text(); + }) + .then((srtContent) => { + if (!srtContent) return; + const parsed = parseSRT(srtContent); + console.log("[AudioPlayer] Loaded", parsed.length, "lyric lines"); + setCues(parsed); + }) + .catch((err) => + console.log("[AudioPlayer] Lyrics not available:", err.message) + ); + }, [file, buildSidecarUrl]); - // Sync lyrics with audio playback - useEffect(() => { - if (!audioRef.current || cues.length === 0) return; + // Sync lyrics with audio playback + useEffect(() => { + if (!audioRef.current || cues.length === 0) return; - const updateLyrics = () => { - const time = audioRef.current!.currentTime; - const index = cues.findIndex( - (cue) => time >= cue.startTime && time <= cue.endTime, - ); + const updateLyrics = () => { + const time = audioRef.current!.currentTime; + const index = cues.findIndex( + (cue) => time >= cue.startTime && time <= cue.endTime + ); - if (index !== currentCueIndex) { - setCurrentCueIndex(index); + if (index !== currentCueIndex) { + setCurrentCueIndex(index); - // Auto-scroll to active lyric - if (index >= 0 && lyricsContainerRef.current) { - const activeElement = lyricsContainerRef.current.children[ - index - ] as HTMLElement; - if (activeElement) { - activeElement.scrollIntoView({ - behavior: "smooth", - block: "center", - }); - } - } - } - }; + // Auto-scroll to active lyric + if (index >= 0 && lyricsContainerRef.current) { + const activeElement = lyricsContainerRef.current.children[ + index + ] as HTMLElement; + if (activeElement) { + activeElement.scrollIntoView({ + behavior: "smooth", + block: "center", + }); + } + } + } + }; - audioRef.current.addEventListener("timeupdate", updateLyrics); - audioRef.current.addEventListener("seeked", updateLyrics); + audioRef.current.addEventListener("timeupdate", updateLyrics); + audioRef.current.addEventListener("seeked", updateLyrics); - return () => { - audioRef.current?.removeEventListener("timeupdate", updateLyrics); - audioRef.current?.removeEventListener("seeked", updateLyrics); - }; - }, [audioRef.current, cues, currentCueIndex]); + return () => { + audioRef.current?.removeEventListener("timeupdate", updateLyrics); + audioRef.current?.removeEventListener("seeked", updateLyrics); + }; + }, [audioRef.current, cues, currentCueIndex]); - useEffect(() => { - if (!audioRef.current) return; - audioRef.current.volume = volume; - }, [volume]); + useEffect(() => { + if (!audioRef.current) return; + audioRef.current.volume = volume; + }, [volume]); - useEffect(() => { - if (!audioRef.current) return; - audioRef.current.muted = muted; - }, [muted]); + useEffect(() => { + if (!audioRef.current) return; + audioRef.current.muted = muted; + }, [muted]); - // Keyboard shortcuts - useEffect(() => { - const handleKeyDown = (e: KeyboardEvent) => { - if (!audioRef.current) return; + // Keyboard shortcuts + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (!audioRef.current) return; - switch (e.code) { - case "Space": - e.preventDefault(); - togglePlay(); - break; - case "ArrowLeft": - e.preventDefault(); - audioRef.current.currentTime = Math.max( - 0, - audioRef.current.currentTime - 10, - ); - break; - case "ArrowRight": - e.preventDefault(); - audioRef.current.currentTime = Math.min( - duration, - audioRef.current.currentTime + 10, - ); - break; - case "ArrowUp": - e.preventDefault(); - setVolume((v) => Math.min(1, v + 0.1)); - break; - case "ArrowDown": - e.preventDefault(); - setVolume((v) => Math.max(0, v - 0.1)); - break; - case "KeyM": - e.preventDefault(); - setMuted((m) => !m); - break; - } - }; + switch (e.code) { + case "Space": + e.preventDefault(); + togglePlay(); + break; + case "ArrowLeft": + e.preventDefault(); + audioRef.current.currentTime = Math.max( + 0, + audioRef.current.currentTime - 10 + ); + break; + case "ArrowRight": + e.preventDefault(); + audioRef.current.currentTime = Math.min( + duration, + audioRef.current.currentTime + 10 + ); + break; + case "ArrowUp": + e.preventDefault(); + setVolume((v) => Math.min(1, v + 0.1)); + break; + case "ArrowDown": + e.preventDefault(); + setVolume((v) => Math.max(0, v - 0.1)); + break; + case "KeyM": + e.preventDefault(); + setMuted((m) => !m); + break; + } + }; - window.addEventListener("keydown", handleKeyDown); - return () => window.removeEventListener("keydown", handleKeyDown); - }, [duration, playing]); + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, [duration, playing]); - const togglePlay = () => { - if (!audioRef.current) return; - if (playing) { - audioRef.current.pause(); - } else { - audioRef.current.play(); - } - }; + const togglePlay = () => { + if (!audioRef.current) return; + if (playing) { + audioRef.current.pause(); + } else { + audioRef.current.play(); + } + }; - const handleSeek = (e: React.MouseEvent) => { - if (!audioRef.current) return; - const rect = e.currentTarget.getBoundingClientRect(); - const percent = (e.clientX - rect.left) / rect.width; - audioRef.current.currentTime = percent * duration; - }; + const handleSeek = (e: React.MouseEvent) => { + if (!audioRef.current) return; + const rect = e.currentTarget.getBoundingClientRect(); + const percent = (e.clientX - rect.left) / rect.width; + audioRef.current.currentTime = percent * duration; + }; - const skipBack = () => { - if (!audioRef.current) return; - audioRef.current.currentTime = Math.max( - 0, - audioRef.current.currentTime - 10, - ); - }; + const skipBack = () => { + if (!audioRef.current) return; + audioRef.current.currentTime = Math.max( + 0, + audioRef.current.currentTime - 10 + ); + }; - const skipForward = () => { - if (!audioRef.current) return; - audioRef.current.currentTime = Math.min( - duration, - audioRef.current.currentTime + 10, - ); - }; + const skipForward = () => { + if (!audioRef.current) return; + audioRef.current.currentTime = Math.min( + duration, + audioRef.current.currentTime + 10 + ); + }; - return ( -
    - {/* Hidden audio element */} -
    + } + > + + + ); + case "document": + case "book": + case "spreadsheet": + case "presentation": + return ; + case "text": + case "code": + case "config": + return ; + default: + return ; + } +} diff --git a/packages/interface/src/components/QuickPreview/Controller.tsx b/packages/interface/src/components/QuickPreview/Controller.tsx index 96bb9db70..d564615f6 100644 --- a/packages/interface/src/components/QuickPreview/Controller.tsx +++ b/packages/interface/src/components/QuickPreview/Controller.tsx @@ -10,59 +10,48 @@ import { QuickPreviewFullscreen } from "./QuickPreviewFullscreen"; * Only re-renders when quickPreviewFileId changes, not on every selection change. */ export const QuickPreviewController = memo(function QuickPreviewController({ - sidebarWidth, - inspectorWidth, + sidebarWidth, + inspectorWidth, }: { - sidebarWidth: number; - inspectorWidth: number; + sidebarWidth: number; + inspectorWidth: number; }) { - const { quickPreviewFileId, closeQuickPreview, currentFiles } = - useExplorer(); - const { selectFile } = useSelection(); + const { quickPreviewFileId, closeQuickPreview, currentFiles } = useExplorer(); + const { selectFile } = useSelection(); - // Early return if no preview - this component won't re-render on selection changes - // because it's memoized and doesn't read selectedFiles directly - if (!quickPreviewFileId) return null; + // Early return if no preview - this component won't re-render on selection changes + // because it's memoized and doesn't read selectedFiles directly + if (!quickPreviewFileId) return null; - const currentIndex = currentFiles.findIndex( - (f) => f.id === quickPreviewFileId, - ); - const hasPrevious = currentIndex > 0; - const hasNext = currentIndex < currentFiles.length - 1; + const currentIndex = currentFiles.findIndex( + (f) => f.id === quickPreviewFileId + ); + const hasPrevious = currentIndex > 0; + const hasNext = currentIndex < currentFiles.length - 1; - const handleNext = () => { - if (hasNext && currentFiles[currentIndex + 1]) { - selectFile( - currentFiles[currentIndex + 1], - currentFiles, - false, - false, - ); - } - }; + const handleNext = () => { + if (hasNext && currentFiles[currentIndex + 1]) { + selectFile(currentFiles[currentIndex + 1], currentFiles, false, false); + } + }; - const handlePrevious = () => { - if (hasPrevious && currentFiles[currentIndex - 1]) { - selectFile( - currentFiles[currentIndex - 1], - currentFiles, - false, - false, - ); - } - }; + const handlePrevious = () => { + if (hasPrevious && currentFiles[currentIndex - 1]) { + selectFile(currentFiles[currentIndex - 1], currentFiles, false, false); + } + }; - return ( - - ); -}); \ No newline at end of file + return ( + + ); +}); diff --git a/packages/interface/src/components/QuickPreview/DirectoryPreview.tsx b/packages/interface/src/components/QuickPreview/DirectoryPreview.tsx index 601b5f1a6..a180bd9ca 100644 --- a/packages/interface/src/components/QuickPreview/DirectoryPreview.tsx +++ b/packages/interface/src/components/QuickPreview/DirectoryPreview.tsx @@ -1,105 +1,99 @@ -import { useMemo } from "react"; -import type { File } from "@sd/ts-client"; -import { File as FileComponent } from "../../routes/explorer/File"; -import { useNormalizedQuery } from "../../contexts/SpacedriveContext"; import { Folder } from "@sd/assets/icons"; +import type { File } from "@sd/ts-client"; +import { useMemo } from "react"; +import { useNormalizedQuery } from "../../contexts/SpacedriveContext"; +import { File as FileComponent } from "../../routes/explorer/File"; interface DirectoryPreviewProps { - file: File; + file: File; } export function DirectoryPreview({ file }: DirectoryPreviewProps) { - const directoryQuery = useNormalizedQuery({ - wireMethod: "query:files.directory_listing", - input: { - path: file.sd_path, - limit: null, - include_hidden: false, - sort_by: "modified" as any, - folders_first: true, - }, - resourceType: "file", - pathScope: file.sd_path, - enabled: true, - }); + const directoryQuery = useNormalizedQuery({ + wireMethod: "query:files.directory_listing", + input: { + path: file.sd_path, + limit: null, + include_hidden: false, + sort_by: "modified" as any, + folders_first: true, + }, + resourceType: "file", + pathScope: file.sd_path, + enabled: true, + }); - const allFiles = (directoryQuery.data as any)?.files || []; + const allFiles = (directoryQuery.data as any)?.files || []; - const directories = useMemo(() => { - return allFiles; - }, [allFiles]); + const directories = useMemo(() => { + return allFiles; + }, [allFiles]); - const gridSize = 120; - const gapSize = 12; + const gridSize = 120; + const gapSize = 12; - if (directoryQuery.isLoading) { - return ( -
    -
    - Folder Icon -
    - {file.name} -
    -
    - Loading directories... -
    -
    -
    - ); - } + if (directoryQuery.isLoading) { + return ( +
    +
    + Folder Icon +
    {file.name}
    +
    + Loading directories... +
    +
    +
    + ); + } - if (directories.length === 0) { - return ( -
    -
    - Folder Icon -
    - {file.name} -
    -
    - No subdirectories -
    -
    -
    - ); - } + if (directories.length === 0) { + return ( +
    +
    + Folder Icon +
    {file.name}
    +
    No subdirectories
    +
    +
    + ); + } - const thumbSize = Math.max(gridSize * 0.6, 60); + const thumbSize = Math.max(gridSize * 0.6, 60); - return ( -
    -
    - {directories.map((dir) => ( -
    -
    - -
    -
    -
    - {dir.name} -
    -
    -
    - ))} -
    -
    - ); + return ( +
    +
    + {directories.map((dir) => ( +
    +
    + +
    +
    +
    + {dir.name} +
    +
    +
    + ))} +
    +
    + ); } diff --git a/packages/interface/src/components/QuickPreview/MeshViewer.tsx b/packages/interface/src/components/QuickPreview/MeshViewer.tsx index cd7d54356..46012bac9 100644 --- a/packages/interface/src/components/QuickPreview/MeshViewer.tsx +++ b/packages/interface/src/components/QuickPreview/MeshViewer.tsx @@ -1,1137 +1,1040 @@ /// -import { Canvas } from "@react-three/fiber"; +import * as GaussianSplats3D from "@mkkellogg/gaussian-splats-3d"; +import { + ArrowCounterClockwise, + Pause, + Play, + Sliders, +} from "@phosphor-icons/react"; import { OrbitControls, PerspectiveCamera } from "@react-three/drei"; -import { useState, useEffect, useRef, Suspense, useCallback } from "react"; +import { Canvas } from "@react-three/fiber"; import type { File } from "@sd/ts-client"; +import { TopBarButton } from "@sd/ui"; +import { Suspense, useCallback, useEffect, useRef, useState } from "react"; +import type * as THREE from "three"; +import { PLYLoader } from "three/examples/jsm/loaders/PLYLoader.js"; import { usePlatform } from "../../contexts/PlatformContext"; import { File as FileComponent } from "../../routes/explorer/File"; -import { PLYLoader } from "three/examples/jsm/loaders/PLYLoader.js"; -import * as GaussianSplats3D from "@mkkellogg/gaussian-splats-3d"; -import * as THREE from "three"; -import { TopBarButton, TopBarButtonGroup } from "@sd/ui"; -import { - Play, - Pause, - ArrowCounterClockwise, - Sliders, -} from "@phosphor-icons/react"; interface MeshViewerProps { - file: File; - onZoomChange?: (isZoomed: boolean) => void; - splatUrl?: string | null; // Optional URL to Gaussian splat sidecar - onSplatLoaded?: () => void; // Callback when Gaussian splat finishes loading - // Control values (controlled component) - autoRotate?: boolean; - swayAmount?: number; - swaySpeed?: number; - cameraDistance?: number; - onControlsChange?: (controls: { - autoRotate: boolean; - swayAmount: number; - swaySpeed: number; - cameraDistance: number; - isGaussianSplat: boolean; - onResetFocalPoint?: () => void; // Reset to initial raycast focal point - }) => void; + file: File; + onZoomChange?: (isZoomed: boolean) => void; + splatUrl?: string | null; // Optional URL to Gaussian splat sidecar + onSplatLoaded?: () => void; // Callback when Gaussian splat finishes loading + // Control values (controlled component) + autoRotate?: boolean; + swayAmount?: number; + swaySpeed?: number; + cameraDistance?: number; + onControlsChange?: (controls: { + autoRotate: boolean; + swayAmount: number; + swaySpeed: number; + cameraDistance: number; + isGaussianSplat: boolean; + onResetFocalPoint?: () => void; // Reset to initial raycast focal point + }) => void; } interface MeshSceneProps { - url: string; + url: string; } function MeshScene({ url }: MeshSceneProps) { - const meshRef = useRef(null); - const [geometry, setGeometry] = useState(null); + const meshRef = useRef(null); + const [geometry, setGeometry] = useState(null); - useEffect(() => { - const loader = new PLYLoader(); - loader.load( - url, - (loadedGeometry) => { - loadedGeometry.computeVertexNormals(); - loadedGeometry.center(); - setGeometry(loadedGeometry); - }, - undefined, - (error) => { - console.error("[MeshScene] PLY load error:", error); - }, - ); + useEffect(() => { + const loader = new PLYLoader(); + loader.load( + url, + (loadedGeometry) => { + loadedGeometry.computeVertexNormals(); + loadedGeometry.center(); + setGeometry(loadedGeometry); + }, + undefined, + (error) => { + console.error("[MeshScene] PLY load error:", error); + } + ); - return () => { - if (geometry) { - geometry.dispose(); - } - }; - }, [url]); + return () => { + if (geometry) { + geometry.dispose(); + } + }; + }, [url]); - if (!geometry) { - return null; - } + if (!geometry) { + return null; + } - return ( - // @ts-expect-error - React Three Fiber JSX types - - {/* @ts-expect-error - React Three Fiber JSX types */} - - {/* @ts-expect-error - React Three Fiber JSX types */} - - ); + return ( + // @ts-expect-error - React Three Fiber JSX types + + {/* @ts-expect-error - React Three Fiber JSX types */} + + {/* @ts-expect-error - React Three Fiber JSX types */} + + ); } -const CAMERA_LOOK_AT = [-0.00697, -0.00533, -0.61858] as const; +const CAMERA_LOOK_AT = [-0.006_97, -0.005_33, -0.618_58] as const; function GaussianSplatViewer({ - url, - onFallback, - onLoaded, - autoRotate = false, - swayAmount = 0.25, - swaySpeed = 0.5, - cameraDistance = 0.5, - onResetReady, - onDistanceCalculated, + url, + onFallback, + onLoaded, + autoRotate = false, + swayAmount = 0.25, + swaySpeed = 0.5, + cameraDistance = 0.5, + onResetReady, + onDistanceCalculated, }: { - url: string; - onFallback: () => void; - onLoaded?: () => void; - autoRotate?: boolean; - swayAmount?: number; - swaySpeed?: number; - cameraDistance?: number; - onResetReady?: (resetFn: () => void) => void; - onDistanceCalculated?: (distance: number) => void; + url: string; + onFallback: () => void; + onLoaded?: () => void; + autoRotate?: boolean; + swayAmount?: number; + swaySpeed?: number; + cameraDistance?: number; + onResetReady?: (resetFn: () => void) => void; + onDistanceCalculated?: (distance: number) => void; }) { - const containerRef = useRef(null); - const viewerRef = useRef(null); - const animationFrameRef = useRef(null); - const viewerReadyRef = useRef(false); - const raycastCompleteRef = useRef(false); - const swayAmountRef = useRef(swayAmount); - const swaySpeedRef = useRef(swaySpeed); - const cameraDistanceRef = useRef(cameraDistance); - const currentCameraDistanceRef = useRef(cameraDistance); // Actual interpolated distance - const focalPointRef = useRef({ x: 0, y: 0, z: 0 }); - const targetFocalPointRef = useRef({ x: 0, y: 0, z: 0 }); - const initialRaycastFocalPointRef = useRef<{ - x: number; - y: number; - z: number; - } | null>(null); - const focalPointTransitionRef = useRef({ - active: false, - startTime: 0, - duration: 800, - startFocalPoint: { x: 0, y: 0, z: 0 }, - cameraOffset: { x: 0, y: 0, z: 0 }, - }); - - // Update refs when props change (doesn't restart animation) - useEffect(() => { - swayAmountRef.current = swayAmount; - swaySpeedRef.current = swaySpeed; - cameraDistanceRef.current = cameraDistance; - }, [swayAmount, swaySpeed, cameraDistance]); - - useEffect(() => { - if (!containerRef.current) return; - - let cancelled = false; - viewerReadyRef.current = false; - - const initViewer = async () => { - try { - const container = containerRef.current; - if (!container) return; - - const viewer = new GaussianSplats3D.Viewer({ - rootElement: container, - cameraUp: [0, -1, 0], - initialCameraPosition: [0, 0, -0.5], - initialCameraLookAt: [...CAMERA_LOOK_AT], - selfDrivenMode: true, - sphericalHarmonicsDegree: 2, - sharedMemoryForWorkers: false, - }); - - viewerRef.current = viewer; - - await viewer.addSplatScene(url, { - format: GaussianSplats3D.SceneFormat.Ply, - showLoadingUI: false, - progressiveLoad: true, - splatAlphaRemovalThreshold: 5, - onProgress: (percent, label, status) => { - console.log( - `[GaussianSplatViewer] Load progress: ${percent}% - ${label}`, - ); - }, - }); - - if (!cancelled) { - viewer.start(); - console.log("[GaussianSplatViewer] Viewer started"); - - // Set controls.target to the calculated center - const splatMesh = viewer.splatMesh; - if (splatMesh?.calculatedSceneCenter) { - const center = splatMesh.calculatedSceneCenter; - - // Initialize focal point refs with calculated center - focalPointRef.current = { - x: center.x, - y: center.y, - z: center.z, - }; - targetFocalPointRef.current = { - x: center.x, - y: center.y, - z: center.z, - }; - - // Update controls target first - viewer.controls.target.copy(center); - - // Position camera to look at center - viewer.camera.position.set( - center.x, - center.y, - center.z - 0.5, - ); - viewer.camera.lookAt(center.x, center.y, center.z); - viewer.camera.updateProjectionMatrix(); - viewer.controls.update(); - - console.log( - "[GaussianSplatViewer] Setup for raycast:", - { - center: { - x: center.x, - y: center.y, - z: center.z, - }, - cameraPos: { - x: viewer.camera.position.x, - y: viewer.camera.position.y, - z: viewer.camera.position.z, - }, - controlsTarget: { - x: viewer.controls.target.x, - y: viewer.controls.target.y, - z: viewer.controls.target.z, - }, - }, - ); - - // Try raycast from screen center to find actual visual focal point - // Retry multiple times since splat mesh needs time to be ready - const container = containerRef.current; - if (container && viewer.raycaster) { - let retryCount = 0; - const maxRetries = 50; - const retryDelay = 100; - - const attemptRaycast = () => { - retryCount++; - - const renderDimensions = { - x: container.offsetWidth, - y: container.offsetHeight, - }; - const centerPosition = { - x: renderDimensions.x / 2, - y: renderDimensions.y / 2, - }; - - const outHits: any[] = []; - viewer.raycaster.setFromCameraAndScreenPosition( - viewer.camera, - centerPosition, - renderDimensions, - ); - viewer.raycaster.intersectSplatMesh( - viewer.splatMesh, - outHits, - ); - - if (outHits.length > 0) { - console.log( - `[GaussianSplatViewer] ✓ Raycast SUCCESS (attempt ${retryCount})!`, - { - hitCount: outHits.length, - allHits: outHits.map( - (h: any, i: number) => ({ - index: i, - origin: { - x: h.origin?.x, - y: h.origin?.y, - z: h.origin?.z, - }, - distance: h.distance, - }), - ), - cameraPosition: { - x: viewer.camera.position.x, - y: viewer.camera.position.y, - z: viewer.camera.position.z, - }, - calculatedCenter: { - x: viewer.splatMesh - .calculatedSceneCenter.x, - y: viewer.splatMesh - .calculatedSceneCenter.y, - z: viewer.splatMesh - .calculatedSceneCenter.z, - }, - }, - ); - - // Use the CLOSEST hit (smallest distance) - const closestHit = outHits.reduce( - (closest: any, hit: any) => - hit.distance < closest.distance - ? hit - : closest, - outHits[0], - ); - - const intersectionPoint = closestHit.origin; - - console.log( - `[GaussianSplatViewer] Using closest hit:`, - { - origin: { - x: intersectionPoint.x, - y: intersectionPoint.y, - z: intersectionPoint.z, - }, - distance: closestHit.distance, - }, - ); - - // Set the focal point directly - no transition needed since animation hasn't started - focalPointRef.current = { - x: intersectionPoint.x, - y: intersectionPoint.y, - z: intersectionPoint.z, - }; - targetFocalPointRef.current = { - ...focalPointRef.current, - }; - - // Save as initial raycast focal point for reset functionality - if (!initialRaycastFocalPointRef.current) { - initialRaycastFocalPointRef.current = { - ...focalPointRef.current, - }; - console.log( - "[GaussianSplatViewer] Saved initial raycast focal point:", - initialRaycastFocalPointRef.current, - ); - - // Provide reset function to parent - onResetReady?.(() => { - if ( - initialRaycastFocalPointRef.current && - viewerRef.current - ) { - const viewer = - viewerRef.current; - const initial = - initialRaycastFocalPointRef.current; - const current = - viewer.controls.target; - - // Check if we're already at the initial point (avoid unnecessary transition) - const distance = Math.sqrt( - Math.pow( - current.x - initial.x, - 2, - ) + - Math.pow( - current.y - - initial.y, - 2, - ) + - Math.pow( - current.z - - initial.z, - 2, - ), - ); - - if (distance < 0.01) { - console.log( - "[GaussianSplatViewer] Already at initial focal point, skipping reset", - ); - return; - } - - console.log( - "[GaussianSplatViewer] Resetting from", - { - x: current.x, - y: current.y, - z: current.z, - }, - "to initial:", - initial, - ); - viewer.previousCameraTarget.copy( - current, - ); - viewer.nextCameraTarget.copy( - initial, - ); - viewer.transitioningCameraTarget = true; - viewer.transitioningCameraTargetStartTime = - performance.now() / 1000; - } - }); - } - - // Calculate ACTUAL distance from camera to new focal point - // Use this as the orbital radius to prevent zoom - const currentCameraPos = - viewer.camera.position; - const actualDistance = Math.sqrt( - Math.pow( - currentCameraPos.x - - intersectionPoint.x, - 2, - ) + - Math.pow( - currentCameraPos.y - - intersectionPoint.y, - 2, - ) + - Math.pow( - currentCameraPos.z - - intersectionPoint.z, - 2, - ), - ); - - // Set both distance refs to the actual current distance - cameraDistanceRef.current = actualDistance; - currentCameraDistanceRef.current = - actualDistance; - - // Notify parent to sync the distance slider - onDistanceCalculated?.(actualDistance); - - console.log( - "[GaussianSplatViewer] Calculated orbital distance:", - actualDistance, - ); - - // Update controls target - viewer.controls.target.copy( - intersectionPoint, - ); - viewer.controls.update(); - - // Mark raycast as complete so animation can start - raycastCompleteRef.current = true; - - console.log( - `[GaussianSplatViewer] Raycast complete! Focal point:`, - focalPointRef.current, - "Orbital distance:", - actualDistance, - ); - } else if (retryCount < maxRetries) { - // Retry - console.log( - `[GaussianSplatViewer] Raycast attempt ${retryCount} failed, retrying...`, - ); - setTimeout(attemptRaycast, retryDelay); - } else { - console.log( - `[GaussianSplatViewer] ✗ Raycast failed after ${maxRetries} attempts - using calculatedSceneCenter`, - ); - // Mark as complete anyway so animation can start - raycastCompleteRef.current = true; - } - }; - - // Start first attempt after a short delay - setTimeout(attemptRaycast, 200); - } - } - - // Promise resolution means splat is loaded and rendering has begun - // Call onLoaded immediately so overlay fades out as splat fades in - viewerReadyRef.current = true; - onLoaded?.(); - } - } catch (err) { - if ( - !cancelled && - err instanceof Error && - err.name !== "AbortError" - ) { - console.error("[GaussianSplatViewer] Error:", err); - onFallback(); - } - } - }; - - initViewer(); - - return () => { - cancelled = true; - if (viewerRef.current) { - viewerRef.current.dispose(); - viewerRef.current = null; - } - }; - }, [url, onFallback, onLoaded]); - - // Separate effect for managing camera animation - useEffect(() => { - if (!autoRotate) { - // Stop animation if it's running - if (animationFrameRef.current) { - cancelAnimationFrame(animationFrameRef.current); - animationFrameRef.current = null; - } - return; - } - - let startTimeoutId: number | null = null; - - // Wait for viewer to be ready AND raycast to complete, then start animation - const startAnimation = () => { - if ( - !viewerReadyRef.current || - !viewerRef.current || - !raycastCompleteRef.current - ) { - // Not ready yet, check again soon - startTimeoutId = setTimeout( - startAnimation, - 100, - ) as unknown as number; - return; - } - - const viewer = viewerRef.current; - const camera = viewer.camera; - const controls = viewer.controls; - const startTime = Date.now(); - - // Clear any accumulated damping/panning that might interfere with our animation - if (controls) { - controls.clearDampedRotation(); - controls.clearDampedPan(); - // Save current state as the "home" position - controls.saveState(); - } - - // DEBUG: Log everything about the controls state - console.log("[Animation Start] Controls state:", { - target: controls - ? { - x: controls.target.x, - y: controls.target.y, - z: controls.target.z, - } - : null, - cameraPosition: { - x: camera.position.x, - y: camera.position.y, - z: camera.position.z, - }, - cameraUp: { x: camera.up.x, y: camera.up.y, z: camera.up.z }, - enabled: controls?.enabled, - enableDamping: controls?.enableDamping, - dampingFactor: controls?.dampingFactor, - }); - - // Get the initial focal point from controls (already set to splat center) - const initialFocalPoint = controls - ? controls.target.clone() - : { x: 0, y: 0, z: 0 }; - - console.log( - "[Animation Start] Initial focal point:", - initialFocalPoint, - ); - - const animate = () => { - // If user clicked and library's camera transition is active, don't interfere - if (viewer.transitioningCameraTarget) { - // Update our focal point ref to match the library's transitioning target - const currentTarget = { - x: viewer.controls.target.x, - y: viewer.controls.target.y, - z: viewer.controls.target.z, - }; - focalPointRef.current = currentTarget; - targetFocalPointRef.current = currentTarget; - - animationFrameRef.current = requestAnimationFrame(animate); - return; - } - - const elapsed = (Date.now() - startTime) / 1000; - // Back and forth sway, not continuous rotation - // Read from refs so values update live without restarting animation - const angle = - Math.sin(elapsed * swaySpeedRef.current) * - swayAmountRef.current; - - // Handle smooth focal point transition - const transition = focalPointTransitionRef.current; - let focalPoint = focalPointRef.current; - - if (transition.active) { - const now = Date.now(); - const progress = Math.min( - (now - transition.startTime) / transition.duration, - 1, - ); - // Smooth easing function - const eased = - progress < 0.5 - ? 2 * progress * progress - : 1 - Math.pow(-2 * progress + 2, 2) / 2; - - // Lerp focal point - const from = transition.startFocalPoint; - const to = targetFocalPointRef.current; - focalPoint = { - x: from.x + (to.x - from.x) * eased, - y: from.y + (to.y - from.y) * eased, - z: from.z + (to.z - from.z) * eased, - }; - focalPointRef.current = focalPoint; - - // During transition, maintain the camera's initial offset - // This prevents zoom - camera moves with the focal point - camera.position.x = - focalPoint.x + transition.cameraOffset.x; - camera.position.y = - focalPoint.y + transition.cameraOffset.y; - camera.position.z = - focalPoint.z + transition.cameraOffset.z; - - // DON'T update controls.target during transition - let it happen after - // This prevents OrbitControls from calculating wrong spherical radius - - if (progress >= 1) { - transition.active = false; - // NOW update controls.target after camera is positioned correctly - if (controls) { - controls.target.set( - focalPoint.x, - focalPoint.y, - focalPoint.z, - ); - } - console.log( - "[GaussianSplatViewer] Focal point transition complete, controls.target updated", - ); - } - } else { - // Smoothly interpolate distance when slider changes - const targetDistance = cameraDistanceRef.current; - const currentDistance = currentCameraDistanceRef.current; - const distanceDiff = targetDistance - currentDistance; - - if (Math.abs(distanceDiff) > 0.001) { - // Smooth interpolation (20% per frame) - const oldDistance = currentCameraDistanceRef.current; - currentCameraDistanceRef.current += distanceDiff * 0.2; - - if (Math.abs(distanceDiff) > 0.1) { - console.log( - "[Animation] Distance interpolating from", - oldDistance.toFixed(3), - "to", - targetDistance.toFixed(3), - ); - } - } else { - currentCameraDistanceRef.current = targetDistance; - } - - // Normal orbital animation with interpolated distance - camera.position.x = - focalPoint.x + - Math.sin(angle) * currentCameraDistanceRef.current; - camera.position.z = - focalPoint.z + - -Math.cos(angle) * currentCameraDistanceRef.current; - camera.position.y = focalPoint.y; - } - - camera.lookAt(focalPoint.x, focalPoint.y, focalPoint.z); - - // Only update controls.target when NOT transitioning - // During normal animation, keep it synced to prevent drift - if (!transition.active && controls) { - const oldTarget = { - x: controls.target.x, - y: controls.target.y, - z: controls.target.z, - }; - controls.target.set( - focalPoint.x, - focalPoint.y, - focalPoint.z, - ); - - // Log if target changed significantly - const targetChanged = - Math.abs(oldTarget.x - focalPoint.x) > 0.001 || - Math.abs(oldTarget.y - focalPoint.y) > 0.001 || - Math.abs(oldTarget.z - focalPoint.z) > 0.001; - if (targetChanged) { - console.log( - "[Animation] Controls.target changed from", - oldTarget, - "to", - { - x: focalPoint.x, - y: focalPoint.y, - z: focalPoint.z, - }, - ); - } - } - - animationFrameRef.current = requestAnimationFrame(animate); - }; - - // Set initial camera position relative to focal point, then start animation - requestAnimationFrame(() => { - const fp = focalPointRef.current; - camera.position.set( - fp.x, - fp.y, - fp.z - cameraDistanceRef.current, - ); - camera.lookAt(fp.x, fp.y, fp.z); - camera.updateProjectionMatrix(); - - // Update controls to sync with new camera position - if (controls) { - controls.update(); - } - - console.log("[Animation Start] Camera positioned at:", { - x: camera.position.x, - y: camera.position.y, - z: camera.position.z, - }); - - animate(); - }); - }; - - startAnimation(); - - return () => { - // Clear any pending start timeout - if (startTimeoutId !== null) { - clearTimeout(startTimeoutId); - } - // Cancel animation frame - if (animationFrameRef.current) { - cancelAnimationFrame(animationFrameRef.current); - animationFrameRef.current = null; - } - }; - }, [autoRotate]); - - return ( -
    - ); + const containerRef = useRef(null); + const viewerRef = useRef(null); + const animationFrameRef = useRef(null); + const viewerReadyRef = useRef(false); + const raycastCompleteRef = useRef(false); + const swayAmountRef = useRef(swayAmount); + const swaySpeedRef = useRef(swaySpeed); + const cameraDistanceRef = useRef(cameraDistance); + const currentCameraDistanceRef = useRef(cameraDistance); // Actual interpolated distance + const focalPointRef = useRef({ x: 0, y: 0, z: 0 }); + const targetFocalPointRef = useRef({ x: 0, y: 0, z: 0 }); + const initialRaycastFocalPointRef = useRef<{ + x: number; + y: number; + z: number; + } | null>(null); + const focalPointTransitionRef = useRef({ + active: false, + startTime: 0, + duration: 800, + startFocalPoint: { x: 0, y: 0, z: 0 }, + cameraOffset: { x: 0, y: 0, z: 0 }, + }); + + // Update refs when props change (doesn't restart animation) + useEffect(() => { + swayAmountRef.current = swayAmount; + swaySpeedRef.current = swaySpeed; + cameraDistanceRef.current = cameraDistance; + }, [swayAmount, swaySpeed, cameraDistance]); + + useEffect(() => { + if (!containerRef.current) return; + + let cancelled = false; + viewerReadyRef.current = false; + + const initViewer = async () => { + try { + const container = containerRef.current; + if (!container) return; + + const viewer = new GaussianSplats3D.Viewer({ + rootElement: container, + cameraUp: [0, -1, 0], + initialCameraPosition: [0, 0, -0.5], + initialCameraLookAt: [...CAMERA_LOOK_AT], + selfDrivenMode: true, + sphericalHarmonicsDegree: 2, + sharedMemoryForWorkers: false, + }); + + viewerRef.current = viewer; + + await viewer.addSplatScene(url, { + format: GaussianSplats3D.SceneFormat.Ply, + showLoadingUI: false, + progressiveLoad: true, + splatAlphaRemovalThreshold: 5, + onProgress: (percent, label, status) => { + console.log( + `[GaussianSplatViewer] Load progress: ${percent}% - ${label}` + ); + }, + }); + + if (!cancelled) { + viewer.start(); + console.log("[GaussianSplatViewer] Viewer started"); + + // Set controls.target to the calculated center + const splatMesh = viewer.splatMesh; + if (splatMesh?.calculatedSceneCenter) { + const center = splatMesh.calculatedSceneCenter; + + // Initialize focal point refs with calculated center + focalPointRef.current = { + x: center.x, + y: center.y, + z: center.z, + }; + targetFocalPointRef.current = { + x: center.x, + y: center.y, + z: center.z, + }; + + // Update controls target first + viewer.controls.target.copy(center); + + // Position camera to look at center + viewer.camera.position.set(center.x, center.y, center.z - 0.5); + viewer.camera.lookAt(center.x, center.y, center.z); + viewer.camera.updateProjectionMatrix(); + viewer.controls.update(); + + console.log("[GaussianSplatViewer] Setup for raycast:", { + center: { + x: center.x, + y: center.y, + z: center.z, + }, + cameraPos: { + x: viewer.camera.position.x, + y: viewer.camera.position.y, + z: viewer.camera.position.z, + }, + controlsTarget: { + x: viewer.controls.target.x, + y: viewer.controls.target.y, + z: viewer.controls.target.z, + }, + }); + + // Try raycast from screen center to find actual visual focal point + // Retry multiple times since splat mesh needs time to be ready + const container = containerRef.current; + if (container && viewer.raycaster) { + let retryCount = 0; + const maxRetries = 50; + const retryDelay = 100; + + const attemptRaycast = () => { + retryCount++; + + const renderDimensions = { + x: container.offsetWidth, + y: container.offsetHeight, + }; + const centerPosition = { + x: renderDimensions.x / 2, + y: renderDimensions.y / 2, + }; + + const outHits: any[] = []; + viewer.raycaster.setFromCameraAndScreenPosition( + viewer.camera, + centerPosition, + renderDimensions + ); + viewer.raycaster.intersectSplatMesh(viewer.splatMesh, outHits); + + if (outHits.length > 0) { + console.log( + `[GaussianSplatViewer] ✓ Raycast SUCCESS (attempt ${retryCount})!`, + { + hitCount: outHits.length, + allHits: outHits.map((h: any, i: number) => ({ + index: i, + origin: { + x: h.origin?.x, + y: h.origin?.y, + z: h.origin?.z, + }, + distance: h.distance, + })), + cameraPosition: { + x: viewer.camera.position.x, + y: viewer.camera.position.y, + z: viewer.camera.position.z, + }, + calculatedCenter: { + x: viewer.splatMesh.calculatedSceneCenter.x, + y: viewer.splatMesh.calculatedSceneCenter.y, + z: viewer.splatMesh.calculatedSceneCenter.z, + }, + } + ); + + // Use the CLOSEST hit (smallest distance) + const closestHit = outHits.reduce( + (closest: any, hit: any) => + hit.distance < closest.distance ? hit : closest, + outHits[0] + ); + + const intersectionPoint = closestHit.origin; + + console.log("[GaussianSplatViewer] Using closest hit:", { + origin: { + x: intersectionPoint.x, + y: intersectionPoint.y, + z: intersectionPoint.z, + }, + distance: closestHit.distance, + }); + + // Set the focal point directly - no transition needed since animation hasn't started + focalPointRef.current = { + x: intersectionPoint.x, + y: intersectionPoint.y, + z: intersectionPoint.z, + }; + targetFocalPointRef.current = { + ...focalPointRef.current, + }; + + // Save as initial raycast focal point for reset functionality + if (!initialRaycastFocalPointRef.current) { + initialRaycastFocalPointRef.current = { + ...focalPointRef.current, + }; + console.log( + "[GaussianSplatViewer] Saved initial raycast focal point:", + initialRaycastFocalPointRef.current + ); + + // Provide reset function to parent + onResetReady?.(() => { + if ( + initialRaycastFocalPointRef.current && + viewerRef.current + ) { + const viewer = viewerRef.current; + const initial = initialRaycastFocalPointRef.current; + const current = viewer.controls.target; + + // Check if we're already at the initial point (avoid unnecessary transition) + const distance = Math.sqrt( + (current.x - initial.x) ** 2 + + (current.y - initial.y) ** 2 + + (current.z - initial.z) ** 2 + ); + + if (distance < 0.01) { + console.log( + "[GaussianSplatViewer] Already at initial focal point, skipping reset" + ); + return; + } + + console.log( + "[GaussianSplatViewer] Resetting from", + { + x: current.x, + y: current.y, + z: current.z, + }, + "to initial:", + initial + ); + viewer.previousCameraTarget.copy(current); + viewer.nextCameraTarget.copy(initial); + viewer.transitioningCameraTarget = true; + viewer.transitioningCameraTargetStartTime = + performance.now() / 1000; + } + }); + } + + // Calculate ACTUAL distance from camera to new focal point + // Use this as the orbital radius to prevent zoom + const currentCameraPos = viewer.camera.position; + const actualDistance = Math.sqrt( + (currentCameraPos.x - intersectionPoint.x) ** 2 + + (currentCameraPos.y - intersectionPoint.y) ** 2 + + (currentCameraPos.z - intersectionPoint.z) ** 2 + ); + + // Set both distance refs to the actual current distance + cameraDistanceRef.current = actualDistance; + currentCameraDistanceRef.current = actualDistance; + + // Notify parent to sync the distance slider + onDistanceCalculated?.(actualDistance); + + console.log( + "[GaussianSplatViewer] Calculated orbital distance:", + actualDistance + ); + + // Update controls target + viewer.controls.target.copy(intersectionPoint); + viewer.controls.update(); + + // Mark raycast as complete so animation can start + raycastCompleteRef.current = true; + + console.log( + "[GaussianSplatViewer] Raycast complete! Focal point:", + focalPointRef.current, + "Orbital distance:", + actualDistance + ); + } else if (retryCount < maxRetries) { + // Retry + console.log( + `[GaussianSplatViewer] Raycast attempt ${retryCount} failed, retrying...` + ); + setTimeout(attemptRaycast, retryDelay); + } else { + console.log( + `[GaussianSplatViewer] ✗ Raycast failed after ${maxRetries} attempts - using calculatedSceneCenter` + ); + // Mark as complete anyway so animation can start + raycastCompleteRef.current = true; + } + }; + + // Start first attempt after a short delay + setTimeout(attemptRaycast, 200); + } + } + + // Promise resolution means splat is loaded and rendering has begun + // Call onLoaded immediately so overlay fades out as splat fades in + viewerReadyRef.current = true; + onLoaded?.(); + } + } catch (err) { + if (!cancelled && err instanceof Error && err.name !== "AbortError") { + console.error("[GaussianSplatViewer] Error:", err); + onFallback(); + } + } + }; + + initViewer(); + + return () => { + cancelled = true; + if (viewerRef.current) { + viewerRef.current.dispose(); + viewerRef.current = null; + } + }; + }, [url, onFallback, onLoaded]); + + // Separate effect for managing camera animation + useEffect(() => { + if (!autoRotate) { + // Stop animation if it's running + if (animationFrameRef.current) { + cancelAnimationFrame(animationFrameRef.current); + animationFrameRef.current = null; + } + return; + } + + let startTimeoutId: number | null = null; + + // Wait for viewer to be ready AND raycast to complete, then start animation + const startAnimation = () => { + if ( + !( + viewerReadyRef.current && + viewerRef.current && + raycastCompleteRef.current + ) + ) { + // Not ready yet, check again soon + startTimeoutId = setTimeout(startAnimation, 100) as unknown as number; + return; + } + + const viewer = viewerRef.current; + const camera = viewer.camera; + const controls = viewer.controls; + const startTime = Date.now(); + + // Clear any accumulated damping/panning that might interfere with our animation + if (controls) { + controls.clearDampedRotation(); + controls.clearDampedPan(); + // Save current state as the "home" position + controls.saveState(); + } + + // DEBUG: Log everything about the controls state + console.log("[Animation Start] Controls state:", { + target: controls + ? { + x: controls.target.x, + y: controls.target.y, + z: controls.target.z, + } + : null, + cameraPosition: { + x: camera.position.x, + y: camera.position.y, + z: camera.position.z, + }, + cameraUp: { x: camera.up.x, y: camera.up.y, z: camera.up.z }, + enabled: controls?.enabled, + enableDamping: controls?.enableDamping, + dampingFactor: controls?.dampingFactor, + }); + + // Get the initial focal point from controls (already set to splat center) + const initialFocalPoint = controls + ? controls.target.clone() + : { x: 0, y: 0, z: 0 }; + + console.log("[Animation Start] Initial focal point:", initialFocalPoint); + + const animate = () => { + // If user clicked and library's camera transition is active, don't interfere + if (viewer.transitioningCameraTarget) { + // Update our focal point ref to match the library's transitioning target + const currentTarget = { + x: viewer.controls.target.x, + y: viewer.controls.target.y, + z: viewer.controls.target.z, + }; + focalPointRef.current = currentTarget; + targetFocalPointRef.current = currentTarget; + + animationFrameRef.current = requestAnimationFrame(animate); + return; + } + + const elapsed = (Date.now() - startTime) / 1000; + // Back and forth sway, not continuous rotation + // Read from refs so values update live without restarting animation + const angle = + Math.sin(elapsed * swaySpeedRef.current) * swayAmountRef.current; + + // Handle smooth focal point transition + const transition = focalPointTransitionRef.current; + let focalPoint = focalPointRef.current; + + if (transition.active) { + const now = Date.now(); + const progress = Math.min( + (now - transition.startTime) / transition.duration, + 1 + ); + // Smooth easing function + const eased = + progress < 0.5 + ? 2 * progress * progress + : 1 - (-2 * progress + 2) ** 2 / 2; + + // Lerp focal point + const from = transition.startFocalPoint; + const to = targetFocalPointRef.current; + focalPoint = { + x: from.x + (to.x - from.x) * eased, + y: from.y + (to.y - from.y) * eased, + z: from.z + (to.z - from.z) * eased, + }; + focalPointRef.current = focalPoint; + + // During transition, maintain the camera's initial offset + // This prevents zoom - camera moves with the focal point + camera.position.x = focalPoint.x + transition.cameraOffset.x; + camera.position.y = focalPoint.y + transition.cameraOffset.y; + camera.position.z = focalPoint.z + transition.cameraOffset.z; + + // DON'T update controls.target during transition - let it happen after + // This prevents OrbitControls from calculating wrong spherical radius + + if (progress >= 1) { + transition.active = false; + // NOW update controls.target after camera is positioned correctly + if (controls) { + controls.target.set(focalPoint.x, focalPoint.y, focalPoint.z); + } + console.log( + "[GaussianSplatViewer] Focal point transition complete, controls.target updated" + ); + } + } else { + // Smoothly interpolate distance when slider changes + const targetDistance = cameraDistanceRef.current; + const currentDistance = currentCameraDistanceRef.current; + const distanceDiff = targetDistance - currentDistance; + + if (Math.abs(distanceDiff) > 0.001) { + // Smooth interpolation (20% per frame) + const oldDistance = currentCameraDistanceRef.current; + currentCameraDistanceRef.current += distanceDiff * 0.2; + + if (Math.abs(distanceDiff) > 0.1) { + console.log( + "[Animation] Distance interpolating from", + oldDistance.toFixed(3), + "to", + targetDistance.toFixed(3) + ); + } + } else { + currentCameraDistanceRef.current = targetDistance; + } + + // Normal orbital animation with interpolated distance + camera.position.x = + focalPoint.x + Math.sin(angle) * currentCameraDistanceRef.current; + camera.position.z = + focalPoint.z + -Math.cos(angle) * currentCameraDistanceRef.current; + camera.position.y = focalPoint.y; + } + + camera.lookAt(focalPoint.x, focalPoint.y, focalPoint.z); + + // Only update controls.target when NOT transitioning + // During normal animation, keep it synced to prevent drift + if (!transition.active && controls) { + const oldTarget = { + x: controls.target.x, + y: controls.target.y, + z: controls.target.z, + }; + controls.target.set(focalPoint.x, focalPoint.y, focalPoint.z); + + // Log if target changed significantly + const targetChanged = + Math.abs(oldTarget.x - focalPoint.x) > 0.001 || + Math.abs(oldTarget.y - focalPoint.y) > 0.001 || + Math.abs(oldTarget.z - focalPoint.z) > 0.001; + if (targetChanged) { + console.log( + "[Animation] Controls.target changed from", + oldTarget, + "to", + { + x: focalPoint.x, + y: focalPoint.y, + z: focalPoint.z, + } + ); + } + } + + animationFrameRef.current = requestAnimationFrame(animate); + }; + + // Set initial camera position relative to focal point, then start animation + requestAnimationFrame(() => { + const fp = focalPointRef.current; + camera.position.set(fp.x, fp.y, fp.z - cameraDistanceRef.current); + camera.lookAt(fp.x, fp.y, fp.z); + camera.updateProjectionMatrix(); + + // Update controls to sync with new camera position + if (controls) { + controls.update(); + } + + console.log("[Animation Start] Camera positioned at:", { + x: camera.position.x, + y: camera.position.y, + z: camera.position.z, + }); + + animate(); + }); + }; + + startAnimation(); + + return () => { + // Clear any pending start timeout + if (startTimeoutId !== null) { + clearTimeout(startTimeoutId); + } + // Cancel animation frame + if (animationFrameRef.current) { + cancelAnimationFrame(animationFrameRef.current); + animationFrameRef.current = null; + } + }; + }, [autoRotate]); + + return ( +
    + ); } // Props for the UI controls component interface MeshViewerUIProps { - autoRotate: boolean; - setAutoRotate: (value: boolean) => void; - swayAmount: number; - setSwayAmount: (value: number) => void; - swaySpeed: number; - setSwaySpeed: (value: number) => void; - cameraDistance: number; - setCameraDistance: (value: number) => void; - isGaussianSplat: boolean; - onResetFocalPoint?: () => void; + autoRotate: boolean; + setAutoRotate: (value: boolean) => void; + swayAmount: number; + setSwayAmount: (value: number) => void; + swaySpeed: number; + setSwaySpeed: (value: number) => void; + cameraDistance: number; + setCameraDistance: (value: number) => void; + isGaussianSplat: boolean; + onResetFocalPoint?: () => void; } // Export UI controls as a separate component export function MeshViewerUI({ - autoRotate, - setAutoRotate, - swayAmount, - setSwayAmount, - swaySpeed, - setSwaySpeed, - cameraDistance, - setCameraDistance, - isGaussianSplat, - onResetFocalPoint, + autoRotate, + setAutoRotate, + swayAmount, + setSwayAmount, + swaySpeed, + setSwaySpeed, + cameraDistance, + setCameraDistance, + isGaussianSplat, + onResetFocalPoint, }: MeshViewerUIProps) { - const [showSettings, setShowSettings] = useState(false); + const [showSettings, setShowSettings] = useState(false); - if (!isGaussianSplat) { - return ( - <> -
    - 3D Mesh -
    -
    -
    Left drag: Rotate
    -
    Right drag: Pan
    -
    Scroll: Zoom
    -
    - - ); - } + if (!isGaussianSplat) { + return ( + <> +
    + 3D Mesh +
    +
    +
    Left drag: Rotate
    +
    Right drag: Pan
    +
    Scroll: Zoom
    +
    + + ); + } - return ( - <> - {/* Button controls */} -
    - setShowSettings(!showSettings)} - title="Settings" - active={showSettings} - activeAccent={true} - /> - {onResetFocalPoint && ( - - )} - setAutoRotate(!autoRotate)} - title={autoRotate ? "Pause" : "Play"} - active={autoRotate} - activeAccent={true} - /> -
    + return ( + <> + {/* Button controls */} +
    + setShowSettings(!showSettings)} + title="Settings" + /> + {onResetFocalPoint && ( + + )} + setAutoRotate(!autoRotate)} + title={autoRotate ? "Pause" : "Play"} + /> +
    - {/* Settings panel (only shown when button is clicked) */} - {showSettings && ( -
    - {/* Sway amount slider */} -
    -
    - - - {swayAmount.toFixed(2)} - -
    - - setSwayAmount(parseFloat(e.target.value)) - } - className="w-full h-1.5 bg-app-button rounded-full appearance-none cursor-pointer [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-3 [&::-webkit-slider-thumb]:h-3 [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-accent [&::-webkit-slider-thumb]:cursor-pointer" - /> -
    + {/* Settings panel (only shown when button is clicked) */} + {showSettings && ( +
    + {/* Sway amount slider */} +
    +
    + + + {swayAmount.toFixed(2)} + +
    + setSwayAmount(Number.parseFloat(e.target.value))} + step="0.01" + type="range" + value={swayAmount} + /> +
    - {/* Speed slider */} -
    -
    - - - {swaySpeed.toFixed(2)} - -
    - - setSwaySpeed(parseFloat(e.target.value)) - } - className="w-full h-1.5 bg-app-button rounded-full appearance-none cursor-pointer [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-3 [&::-webkit-slider-thumb]:h-3 [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-accent [&::-webkit-slider-thumb]:cursor-pointer" - /> -
    + {/* Speed slider */} +
    +
    + + + {swaySpeed.toFixed(2)} + +
    + setSwaySpeed(Number.parseFloat(e.target.value))} + step="0.1" + type="range" + value={swaySpeed} + /> +
    - {/* Distance slider */} -
    -
    - - - {cameraDistance.toFixed(2)} - -
    - - setCameraDistance(parseFloat(e.target.value)) - } - className="w-full h-1.5 bg-app-button rounded-full appearance-none cursor-pointer [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-3 [&::-webkit-slider-thumb]:h-3 [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-accent [&::-webkit-slider-thumb]:cursor-pointer" - /> -
    -
    - )} - - ); + {/* Distance slider */} +
    +
    + + + {cameraDistance.toFixed(2)} + +
    + + setCameraDistance(Number.parseFloat(e.target.value)) + } + step="0.05" + type="range" + value={cameraDistance} + /> +
    +
    + )} + + ); } export function MeshViewer({ - file, - onZoomChange, - splatUrl, - onSplatLoaded, - autoRotate: autoRotateProp = true, - swayAmount: swayAmountProp = 0.25, - swaySpeed: swaySpeedProp = 0.5, - cameraDistance: cameraDistanceProp = 0.5, - onControlsChange, + file, + onZoomChange, + splatUrl, + onSplatLoaded, + autoRotate: autoRotateProp = true, + swayAmount: swayAmountProp = 0.25, + swaySpeed: swaySpeedProp = 0.5, + cameraDistance: cameraDistanceProp = 0.5, + onControlsChange, }: MeshViewerProps) { - const platform = usePlatform(); - const [meshUrl, setMeshUrl] = useState(splatUrl || null); - const [isGaussianSplat, setIsGaussianSplat] = useState(!!splatUrl); - const [splatFailed, setSplatFailed] = useState(false); - const [shouldLoad, setShouldLoad] = useState(false); - const [loading, setLoading] = useState(!splatUrl); - const resetFocalPointRef = useRef<(() => void) | null>(null); - const [internalCameraDistance, setInternalCameraDistance] = - useState(cameraDistanceProp); + const platform = usePlatform(); + const [meshUrl, setMeshUrl] = useState(splatUrl || null); + const [isGaussianSplat, setIsGaussianSplat] = useState(!!splatUrl); + const [splatFailed, setSplatFailed] = useState(false); + const [shouldLoad, setShouldLoad] = useState(false); + const [loading, setLoading] = useState(!splatUrl); + const resetFocalPointRef = useRef<(() => void) | null>(null); + const [internalCameraDistance, setInternalCameraDistance] = + useState(cameraDistanceProp); - // Use props for control values, but use internal state for distance (can be overridden by raycast) - const autoRotate = autoRotateProp; - const swayAmount = swayAmountProp; - const swaySpeed = swaySpeedProp; - const cameraDistance = internalCameraDistance; + // Use props for control values, but use internal state for distance (can be overridden by raycast) + const autoRotate = autoRotateProp; + const swayAmount = swayAmountProp; + const swaySpeed = swaySpeedProp; + const cameraDistance = internalCameraDistance; - // Sync internal distance with prop changes (unless we've overridden it) - useEffect(() => { - setInternalCameraDistance(cameraDistanceProp); - }, [cameraDistanceProp]); + // Sync internal distance with prop changes (unless we've overridden it) + useEffect(() => { + setInternalCameraDistance(cameraDistanceProp); + }, [cameraDistanceProp]); - // Notify parent when controls change - useEffect(() => { - onControlsChange?.({ - autoRotate, - swayAmount, - swaySpeed, - cameraDistance: internalCameraDistance, - isGaussianSplat, - onResetFocalPoint: resetFocalPointRef.current || undefined, - }); - }, [ - isGaussianSplat, - autoRotate, - swayAmount, - swaySpeed, - internalCameraDistance, - onControlsChange, - ]); + // Notify parent when controls change + useEffect(() => { + onControlsChange?.({ + autoRotate, + swayAmount, + swaySpeed, + cameraDistance: internalCameraDistance, + isGaussianSplat, + onResetFocalPoint: resetFocalPointRef.current || undefined, + }); + }, [ + isGaussianSplat, + autoRotate, + swayAmount, + swaySpeed, + internalCameraDistance, + onControlsChange, + ]); - const fileId = file.content_identity?.uuid || file.id; + const fileId = file.content_identity?.uuid || file.id; - const handleSplatFallback = useCallback(() => { - setSplatFailed(true); - }, []); + const handleSplatFallback = useCallback(() => { + setSplatFailed(true); + }, []); - useEffect(() => { - setShouldLoad(false); - setMeshUrl(null); - setLoading(true); + useEffect(() => { + setShouldLoad(false); + setMeshUrl(null); + setLoading(true); - const timer = setTimeout(() => { - setShouldLoad(true); - }, 50); + const timer = setTimeout(() => { + setShouldLoad(true); + }, 50); - return () => clearTimeout(timer); - }, [fileId, splatUrl]); + return () => clearTimeout(timer); + }, [fileId, splatUrl]); - useEffect(() => { - // If splatUrl is provided, use it directly (it's a Gaussian splat sidecar) - if (splatUrl) { - setMeshUrl(splatUrl); - setIsGaussianSplat(true); - setLoading(false); - return; - } + useEffect(() => { + // If splatUrl is provided, use it directly (it's a Gaussian splat sidecar) + if (splatUrl) { + setMeshUrl(splatUrl); + setIsGaussianSplat(true); + setLoading(false); + return; + } - if (!shouldLoad || !platform.convertFileSrc) { - return; - } + if (!(shouldLoad && platform.convertFileSrc)) { + return; + } - const sdPath = file.sd_path as any; - const physicalPath = sdPath?.Physical?.path; + const sdPath = file.sd_path as any; + const physicalPath = sdPath?.Physical?.path; - if (!physicalPath) { - console.log("[MeshViewer] No physical path available"); - setLoading(false); - return; - } + if (!physicalPath) { + console.log("[MeshViewer] No physical path available"); + setLoading(false); + return; + } - const url = platform.convertFileSrc(physicalPath); - setMeshUrl(url); + const url = platform.convertFileSrc(physicalPath); + setMeshUrl(url); - // Only run detection if not using splatUrl (splatUrl is already known to be a Gaussian splat) - if (splatUrl) { - return; - } + // Only run detection if not using splatUrl (splatUrl is already known to be a Gaussian splat) + if (splatUrl) { + return; + } - // Create an AbortController to cancel the detection fetch if component unmounts - const abortController = new AbortController(); + // Create an AbortController to cancel the detection fetch if component unmounts + const abortController = new AbortController(); - fetch(url, { signal: abortController.signal }) - .then((res) => res.arrayBuffer()) - .then((buffer) => { - const header = new TextDecoder().decode(buffer.slice(0, 3000)); + fetch(url, { signal: abortController.signal }) + .then((res) => res.arrayBuffer()) + .then((buffer) => { + const header = new TextDecoder().decode(buffer.slice(0, 3000)); - // Gaussian Splat detection - const hasSH = - header.includes("f_dc_0") || - header.includes("sh0") || - header.includes("sh_0"); - const hasScale = - header.includes("scale_0") || - header.includes("scale_1") || - header.includes("scale_2"); - const hasOpacity = header.includes("opacity"); - const hasRotation = - header.includes("rot_0") || - header.includes("rot_1") || - header.includes("rot_2") || - header.includes("rot_3"); + // Gaussian Splat detection + const hasSH = + header.includes("f_dc_0") || + header.includes("sh0") || + header.includes("sh_0"); + const hasScale = + header.includes("scale_0") || + header.includes("scale_1") || + header.includes("scale_2"); + const hasOpacity = header.includes("opacity"); + const hasRotation = + header.includes("rot_0") || + header.includes("rot_1") || + header.includes("rot_2") || + header.includes("rot_3"); - const isGS = hasSH && (hasScale || hasOpacity || hasRotation); + const isGS = hasSH && (hasScale || hasOpacity || hasRotation); - setIsGaussianSplat(isGS); - setLoading(false); - }) - .catch((error) => { - // Ignore abort errors (expected when component unmounts) - if (error.name !== "AbortError") { - console.error( - "[MeshViewer] Error detecting format:", - error, - ); - } - setLoading(false); - }); + setIsGaussianSplat(isGS); + setLoading(false); + }) + .catch((error) => { + // Ignore abort errors (expected when component unmounts) + if (error.name !== "AbortError") { + console.error("[MeshViewer] Error detecting format:", error); + } + setLoading(false); + }); - return () => { - abortController.abort(); - }; - }, [shouldLoad, fileId, file.sd_path, platform, splatUrl]); + return () => { + abortController.abort(); + }; + }, [shouldLoad, fileId, file.sd_path, platform, splatUrl]); - if (!meshUrl || loading) { - return ( -
    -
    - - {loading && ( -
    - Loading 3D model... -
    - )} -
    -
    - ); - } + if (!meshUrl || loading) { + return ( +
    +
    + + {loading && ( +
    + Loading 3D model... +
    + )} +
    +
    + ); + } - // Just render the canvas, UI will be handled by ContentRenderer - return ( -
    - {isGaussianSplat && !splatFailed ? ( - { - resetFocalPointRef.current = resetFn; - // Trigger controls change update to notify parent - onControlsChange?.({ - autoRotate, - swayAmount, - swaySpeed, - cameraDistance: internalCameraDistance, - isGaussianSplat, - onResetFocalPoint: resetFn, - }); - }} - onDistanceCalculated={(distance) => { - setInternalCameraDistance(distance); - }} - /> - ) : ( - - - {/* @ts-expect-error - React Three Fiber JSX types */} - - {/* @ts-expect-error - React Three Fiber JSX types */} - - {/* @ts-expect-error - React Three Fiber JSX types */} - - - - - - - )} -
    - ); -} \ No newline at end of file + // Just render the canvas, UI will be handled by ContentRenderer + return ( +
    + {isGaussianSplat && !splatFailed ? ( + { + setInternalCameraDistance(distance); + }} + onFallback={handleSplatFallback} + onLoaded={onSplatLoaded} + onResetReady={(resetFn) => { + resetFocalPointRef.current = resetFn; + // Trigger controls change update to notify parent + onControlsChange?.({ + autoRotate, + swayAmount, + swaySpeed, + cameraDistance: internalCameraDistance, + isGaussianSplat, + onResetFocalPoint: resetFn, + }); + }} + swayAmount={swayAmount} + swaySpeed={swaySpeed} + url={meshUrl} + /> + ) : ( + + + {/* @ts-expect-error - React Three Fiber JSX types */} + + {/* @ts-expect-error - React Three Fiber JSX types */} + + {/* @ts-expect-error - React Three Fiber JSX types */} + + + + + + + )} +
    + ); +} diff --git a/packages/interface/src/components/QuickPreview/QuickPreview.tsx b/packages/interface/src/components/QuickPreview/QuickPreview.tsx index 05e4c57a0..ade9196d3 100644 --- a/packages/interface/src/components/QuickPreview/QuickPreview.tsx +++ b/packages/interface/src/components/QuickPreview/QuickPreview.tsx @@ -1,171 +1,157 @@ -import { useNormalizedQuery } from "../../contexts/SpacedriveContext"; -import { usePlatform } from "../../contexts/PlatformContext"; +import { X } from "@phosphor-icons/react"; import type { File } from "@sd/ts-client"; import { useEffect, useState } from "react"; +import { usePlatform } from "../../contexts/PlatformContext"; +import { useNormalizedQuery } from "../../contexts/SpacedriveContext"; import { formatBytes, getContentKind } from "../../routes/explorer/utils"; -import { X } from "@phosphor-icons/react"; import { ContentRenderer } from "./ContentRenderer"; function MetadataPanel({ file }: { file: File }) { - return ( -
    -
    -
    -
    Name
    -
    - {file.name} -
    -
    + return ( +
    +
    +
    +
    Name
    +
    {file.name}
    +
    -
    -
    Kind
    -
    - {getContentKind(file)} -
    -
    +
    +
    Kind
    +
    + {getContentKind(file)} +
    +
    -
    -
    Size
    -
    - {formatBytes(file.size || 0)} -
    -
    +
    +
    Size
    +
    {formatBytes(file.size || 0)}
    +
    - {file.extension && ( -
    -
    - Extension -
    -
    {file.extension}
    -
    - )} + {file.extension && ( +
    +
    Extension
    +
    {file.extension}
    +
    + )} - {file.created_at && ( -
    -
    - Created -
    -
    - {new Date(file.created_at).toLocaleString()} -
    -
    - )} + {file.created_at && ( +
    +
    Created
    +
    + {new Date(file.created_at).toLocaleString()} +
    +
    + )} - {file.modified_at && ( -
    -
    - Modified -
    -
    - {new Date(file.modified_at).toLocaleString()} -
    -
    - )} -
    -
    - ); + {file.modified_at && ( +
    +
    Modified
    +
    + {new Date(file.modified_at).toLocaleString()} +
    +
    + )} +
    +
    + ); } export function QuickPreview() { - const platform = usePlatform(); - const [fileId, setFileId] = useState(null); + const platform = usePlatform(); + const [fileId, setFileId] = useState(null); - useEffect(() => { - // Extract file_id from window label - if (platform.getCurrentWindowLabel) { - const label = platform.getCurrentWindowLabel(); + useEffect(() => { + // Extract file_id from window label + if (platform.getCurrentWindowLabel) { + const label = platform.getCurrentWindowLabel(); - // Label format: "quick-preview-{file_id}" - const match = label.match(/^quick-preview-(.+)$/); - if (match) { - setFileId(match[1]); - } - } - }, [platform]); + // Label format: "quick-preview-{file_id}" + const match = label.match(/^quick-preview-(.+)$/); + if (match) { + setFileId(match[1]); + } + } + }, [platform]); - const { - data: file, - isLoading, - error, - } = useNormalizedQuery<{ file_id: string }, File>({ - wireMethod: "query:files.by_id", - input: { file_id: fileId! }, - resourceType: "file", - resourceId: fileId!, - enabled: !!fileId, - }); + const { + data: file, + isLoading, + error, + } = useNormalizedQuery<{ file_id: string }, File>({ + wireMethod: "query:files.by_id", + input: { file_id: fileId! }, + resourceType: "file", + resourceId: fileId!, + enabled: !!fileId, + }); - const handleClose = () => { - if (platform.closeCurrentWindow) { - platform.closeCurrentWindow(); - } - }; + const handleClose = () => { + if (platform.closeCurrentWindow) { + platform.closeCurrentWindow(); + } + }; - // Keyboard shortcuts - useEffect(() => { - const handleKeyDown = (e: KeyboardEvent) => { - if (e.code === "Escape") { - handleClose(); - } - }; + // Keyboard shortcuts + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.code === "Escape") { + handleClose(); + } + }; - window.addEventListener("keydown", handleKeyDown); - return () => window.removeEventListener("keydown", handleKeyDown); - }, []); + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, []); - if (isLoading || !file) { - return ( -
    -
    Loading...
    -
    - ); - } + if (isLoading || !file) { + return ( +
    +
    Loading...
    +
    + ); + } - if (error) { - return ( -
    -
    -
    - Error loading file -
    -
    {error.message}
    -
    -
    - ); - } + if (error) { + return ( +
    +
    +
    Error loading file
    +
    {error.message}
    +
    +
    + ); + } - return ( -
    - {/* Header */} -
    -
    - {file.name} -
    - -
    + return ( +
    + {/* Header */} +
    +
    {file.name}
    + +
    - {/* Content Area */} -
    - {/* File Content */} -
    - -
    + {/* Content Area */} +
    + {/* File Content */} +
    + +
    - {/* Metadata Sidebar */} - -
    + {/* Metadata Sidebar */} + +
    - {/* Footer with keyboard hints */} -
    -
    - Press ESC to close -
    -
    -
    - ); -} \ No newline at end of file + {/* Footer with keyboard hints */} +
    +
    + Press ESC to close +
    +
    +
    + ); +} diff --git a/packages/interface/src/components/QuickPreview/QuickPreviewFullscreen.tsx b/packages/interface/src/components/QuickPreview/QuickPreviewFullscreen.tsx index c696bd2b3..3282e312d 100644 --- a/packages/interface/src/components/QuickPreview/QuickPreviewFullscreen.tsx +++ b/packages/interface/src/components/QuickPreview/QuickPreviewFullscreen.tsx @@ -1,296 +1,283 @@ +import { ArrowLeft, ArrowRight, X } from "@phosphor-icons/react"; +import { AnimatePresence, motion } from "framer-motion"; +import { useEffect, useMemo, useState } from "react"; import { createPortal } from "react-dom"; -import { motion, AnimatePresence } from "framer-motion"; -import { X, ArrowLeft, ArrowRight } from "@phosphor-icons/react"; -import { useEffect, useState, useMemo } from "react"; -import type { File } from "@sd/ts-client"; +import { useExplorer } from "../../routes/explorer/context"; +import { getContentKind } from "../../routes/explorer/utils"; +import { TopBarItem, TopBarPortal } from "../../TopBar"; import { ContentRenderer } from "./ContentRenderer"; import { - VideoControls, - type VideoControlsState, - type VideoControlsCallbacks, + VideoControls, + type VideoControlsCallbacks, + type VideoControlsState, } from "./VideoControls"; -import { TopBarPortal, TopBarItem } from "../../TopBar"; -import { getContentKind } from "../../routes/explorer/utils"; -import { useExplorer } from "../../routes/explorer/context"; interface QuickPreviewFullscreenProps { - fileId: string; - isOpen: boolean; - onClose: () => void; - onNext?: () => void; - onPrevious?: () => void; - hasPrevious?: boolean; - hasNext?: boolean; - sidebarWidth?: number; - inspectorWidth?: number; + fileId: string; + isOpen: boolean; + onClose: () => void; + onNext?: () => void; + onPrevious?: () => void; + hasPrevious?: boolean; + hasNext?: boolean; + sidebarWidth?: number; + inspectorWidth?: number; } const PREVIEW_LAYER_ID = "quick-preview-layer"; export function QuickPreviewFullscreen({ - fileId, - isOpen, - onClose, - onNext, - onPrevious, - hasPrevious, - hasNext, - sidebarWidth = 0, - inspectorWidth = 0, + fileId, + isOpen, + onClose, + onNext, + onPrevious, + hasPrevious, + hasNext, + sidebarWidth = 0, + inspectorWidth = 0, }: QuickPreviewFullscreenProps) { - const [portalTarget, setPortalTarget] = useState(null); - const [isZoomed, setIsZoomed] = useState(false); - const [videoControlsState, setVideoControlsState] = - useState(null); - const [showVideoControls, setShowVideoControls] = useState(false); - const [videoCallbacks, setVideoCallbacks] = - useState(null); - const { currentFiles } = useExplorer(); + const [portalTarget, setPortalTarget] = useState(null); + const [isZoomed, setIsZoomed] = useState(false); + const [videoControlsState, setVideoControlsState] = + useState(null); + const [showVideoControls, setShowVideoControls] = useState(false); + const [videoCallbacks, setVideoCallbacks] = + useState(null); + const { currentFiles } = useExplorer(); - // Reset zoom when file changes - useEffect(() => { - setIsZoomed(false); - }, [fileId]); + // Reset zoom when file changes + useEffect(() => { + setIsZoomed(false); + }, [fileId]); - // Get file directly from currentFiles - instant, no network request - const file = useMemo( - () => currentFiles.find((f) => f.id === fileId) ?? null, - [currentFiles, fileId], - ); + // Get file directly from currentFiles - instant, no network request + const file = useMemo( + () => currentFiles.find((f) => f.id === fileId) ?? null, + [currentFiles, fileId] + ); - // No query needed - files are already loaded by the explorer views - const isLoading = false; - const error = null; + // No query needed - files are already loaded by the explorer views + const isLoading = false; + const error = null; - // Find portal target on mount - useEffect(() => { - const target = document.getElementById(PREVIEW_LAYER_ID); - setPortalTarget(target); - }, []); + // Find portal target on mount + useEffect(() => { + const target = document.getElementById(PREVIEW_LAYER_ID); + setPortalTarget(target); + }, []); - useEffect(() => { - if (!isOpen) return; + useEffect(() => { + if (!isOpen) return; - const handleKeyDown = (e: KeyboardEvent) => { - // Only handle close events - let Explorer handle navigation - if (e.code === "Escape" || e.code === "Space") { - e.preventDefault(); - e.stopImmediatePropagation(); - onClose(); - } - }; + const handleKeyDown = (e: KeyboardEvent) => { + // Only handle close events - let Explorer handle navigation + if (e.code === "Escape" || e.code === "Space") { + e.preventDefault(); + e.stopImmediatePropagation(); + onClose(); + } + }; - window.addEventListener("keydown", handleKeyDown, { capture: true }); - return () => - window.removeEventListener("keydown", handleKeyDown, { - capture: true, - }); - }, [isOpen, onClose]); + window.addEventListener("keydown", handleKeyDown, { capture: true }); + return () => + window.removeEventListener("keydown", handleKeyDown, { + capture: true, + }); + }, [isOpen, onClose]); - // Get background style based on content type - const getBackgroundClass = () => { - if (!file) return "bg-black/90"; + // Get background style based on content type + const getBackgroundClass = () => { + if (!file) return "bg-black/90"; - switch (getContentKind(file)) { - case "video": - return "bg-black"; - case "audio": - return "audio-gradient"; - case "image": - return "bg-black/95"; - default: - return "bg-black/90"; - } - }; + switch (getContentKind(file)) { + case "video": + return "bg-black"; + case "audio": + return "audio-gradient"; + case "image": + return "bg-black/95"; + default: + return "bg-black/90"; + } + }; - // Memoize TopBarItem children to prevent infinite re-renders - const navigationButtons = useMemo( - () => ( -
    - - -
    -
    - ), - [onPrevious, onNext, hasPrevious, hasNext] - ); + // Memoize TopBarItem children to prevent infinite re-renders + const navigationButtons = useMemo( + () => ( +
    + + +
    +
    + ), + [onPrevious, onNext, hasPrevious, hasNext] + ); - const filenameDisplay = useMemo( - () => ( -
    - {file?.name} -
    - ), - [file?.name] - ); + const filenameDisplay = useMemo( + () => ( +
    + {file?.name} +
    + ), + [file?.name] + ); - const closeButton = useMemo( - () => ( - - ), - [onClose] - ); + const closeButton = useMemo( + () => ( + + ), + [onClose] + ); - if (!portalTarget) return null; + if (!portalTarget) return null; - const content = ( - - {isOpen && ( - - {!file && isLoading ? ( -
    -
    Loading...
    -
    - ) : !file && error ? ( -
    -
    -
    - Error loading file -
    -
    {error.message}
    -
    -
    - ) : !file ? ( -
    -
    File not found
    -
    - ) : ( - <> - {/* TopBar content via portal */} - - {(hasPrevious || hasNext) && ( - - {navigationButtons} - - )} - - } - center={ - - {filenameDisplay} - - } - right={ - - {closeButton} - - } - /> + const content = ( + + {isOpen && ( + + {!file && isLoading ? ( +
    +
    Loading...
    +
    + ) : !file && error ? ( +
    +
    +
    + Error loading file +
    +
    {error.message}
    +
    +
    + ) : file ? ( + <> + {/* TopBar content via portal */} + + {filenameDisplay} + + } + left={ + <> + {(hasPrevious || hasNext) && ( + + {navigationButtons} + + )} + + } + right={ + + {closeButton} + + } + /> - {/* Content Area - padded to fit between sidebar/inspector, expands on zoom */} -
    - -
    + {/* Content Area - padded to fit between sidebar/inspector, expands on zoom */} +
    + +
    - {/* Video Controls Overlay - fixed position, always uses sidebar/inspector padding */} - {videoControlsState && - videoCallbacks && - getContentKind(file) === "video" && ( -
    - -
    - )} + {/* Video Controls Overlay - fixed position, always uses sidebar/inspector padding */} + {videoControlsState && + videoCallbacks && + getContentKind(file) === "video" && ( +
    + +
    + )} - {/* Footer with keyboard hints */} -
    -
    - ESC{" "} - or{" "} - Space{" "} - to close - {(hasPrevious || hasNext) && ( - <> - {" · "} - - ← - {" "} - /{" "} - - → - {" "} - to navigate - - )} -
    -
    - - )} -
    - )} -
    - ); + {/* Footer with keyboard hints */} +
    +
    + ESC or{" "} + Space to close + {(hasPrevious || hasNext) && ( + <> + {" · "} + /{" "} + to navigate + + )} +
    +
    + + ) : ( +
    +
    File not found
    +
    + )} +
    + )} +
    + ); - return createPortal(content, portalTarget); + return createPortal(content, portalTarget); } -export { PREVIEW_LAYER_ID }; \ No newline at end of file +export { PREVIEW_LAYER_ID }; diff --git a/packages/interface/src/components/QuickPreview/QuickPreviewModal.tsx b/packages/interface/src/components/QuickPreview/QuickPreviewModal.tsx index d3680f855..5de370610 100644 --- a/packages/interface/src/components/QuickPreview/QuickPreviewModal.tsx +++ b/packages/interface/src/components/QuickPreview/QuickPreviewModal.tsx @@ -1,166 +1,176 @@ -import { motion, AnimatePresence } from 'framer-motion'; -import { X, ArrowLeft, ArrowRight } from '@phosphor-icons/react'; -import { useEffect } from 'react'; -import type { File } from '@sd/ts-client'; -import { useLibraryQuery } from '../../contexts/SpacedriveContext'; -import { Inspector } from '../Inspector/Inspector'; -import { ContentRenderer } from './ContentRenderer'; +import { ArrowLeft, ArrowRight, X } from "@phosphor-icons/react"; +import { AnimatePresence, motion } from "framer-motion"; +import { useEffect } from "react"; +import { useLibraryQuery } from "../../contexts/SpacedriveContext"; +import { Inspector } from "../Inspector/Inspector"; +import { ContentRenderer } from "./ContentRenderer"; interface QuickPreviewModalProps { - fileId: string; - isOpen: boolean; - onClose: () => void; - onNext?: () => void; - onPrevious?: () => void; - hasPrevious?: boolean; - hasNext?: boolean; + fileId: string; + isOpen: boolean; + onClose: () => void; + onNext?: () => void; + onPrevious?: () => void; + hasPrevious?: boolean; + hasNext?: boolean; } export function QuickPreviewModal({ - fileId, - isOpen, - onClose, - onNext, - onPrevious, - hasPrevious, - hasNext + fileId, + isOpen, + onClose, + onNext, + onPrevious, + hasPrevious, + hasNext, }: QuickPreviewModalProps) { - const { data: file, isLoading, error } = useLibraryQuery( - { - type: 'files.by_id', - input: { file_id: fileId } - }, - { - enabled: !!fileId && isOpen - } - ); + const { + data: file, + isLoading, + error, + } = useLibraryQuery( + { + type: "files.by_id", + input: { file_id: fileId }, + }, + { + enabled: !!fileId && isOpen, + } + ); - useEffect(() => { - if (!isOpen) return; + useEffect(() => { + if (!isOpen) return; - const handleKeyDown = (e: KeyboardEvent) => { - if (e.code === 'Escape' || e.code === 'Space') { - e.preventDefault(); - onClose(); - } - if (e.code === 'ArrowLeft' && hasPrevious && onPrevious) { - e.preventDefault(); - onPrevious(); - } - if (e.code === 'ArrowRight' && hasNext && onNext) { - e.preventDefault(); - onNext(); - } - }; + const handleKeyDown = (e: KeyboardEvent) => { + if (e.code === "Escape" || e.code === "Space") { + e.preventDefault(); + onClose(); + } + if (e.code === "ArrowLeft" && hasPrevious && onPrevious) { + e.preventDefault(); + onPrevious(); + } + if (e.code === "ArrowRight" && hasNext && onNext) { + e.preventDefault(); + onNext(); + } + }; - window.addEventListener('keydown', handleKeyDown); - return () => window.removeEventListener('keydown', handleKeyDown); - }, [isOpen, onClose, onNext, onPrevious, hasPrevious, hasNext]); + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, [isOpen, onClose, onNext, onPrevious, hasPrevious, hasNext]); - return ( - - {isOpen && ( - <> - {/* Backdrop */} - + return ( + + {isOpen && ( + <> + {/* Backdrop */} + - {/* Modal - key stays constant so it doesn't remount on file change */} - e.stopPropagation()} - > - {isLoading || !file ? ( -
    -
    Loading...
    -
    - ) : error ? ( -
    -
    -
    Error loading file
    -
    {error.message}
    -
    -
    - ) : ( - <> - {/* Header */} -
    -
    - {/* Navigation Arrows */} -
    - - -
    + {/* Modal - key stays constant so it doesn't remount on file change */} + e.stopPropagation()} + transition={{ duration: 0.2, ease: [0.25, 1, 0.5, 1] }} + > + {isLoading || !file ? ( +
    +
    Loading...
    +
    + ) : error ? ( +
    +
    +
    + Error loading file +
    +
    {error.message}
    +
    +
    + ) : ( + <> + {/* Header */} +
    +
    + {/* Navigation Arrows */} +
    + + +
    -
    +
    -
    {file.name}
    -
    +
    + {file.name} +
    +
    - -
    + +
    - {/* Content Area */} -
    - {/* File Content */} -
    - -
    + {/* Content Area */} +
    + {/* File Content */} +
    + +
    - {/* Inspector Sidebar */} -
    - -
    -
    + {/* Inspector Sidebar */} +
    + +
    +
    - {/* Footer with keyboard hints */} -
    -
    - ESC or{' '} - Space to close - {(hasPrevious || hasNext) && ( - <> - {' • '} - /{' '} - to navigate - - )} -
    -
    - - )} -
    - - )} - - ); -} \ No newline at end of file + {/* Footer with keyboard hints */} +
    +
    + ESC or{" "} + Space to close + {(hasPrevious || hasNext) && ( + <> + {" • "} + /{" "} + to navigate + + )} +
    +
    + + )} + + + )} + + ); +} diff --git a/packages/interface/src/components/QuickPreview/QuickPreviewOverlay.tsx b/packages/interface/src/components/QuickPreview/QuickPreviewOverlay.tsx index 4c838c20c..224df82a1 100644 --- a/packages/interface/src/components/QuickPreview/QuickPreviewOverlay.tsx +++ b/packages/interface/src/components/QuickPreview/QuickPreviewOverlay.tsx @@ -1,142 +1,150 @@ -import { motion, AnimatePresence } from 'framer-motion'; -import { X, ArrowLeft, ArrowRight } from '@phosphor-icons/react'; -import { useEffect } from 'react'; -import type { File } from '@sd/ts-client'; -import { useNormalizedQuery } from '../../contexts/SpacedriveContext'; -import { ContentRenderer } from './ContentRenderer'; +import { ArrowLeft, ArrowRight, X } from "@phosphor-icons/react"; +import type { File } from "@sd/ts-client"; +import { AnimatePresence, motion } from "framer-motion"; +import { useEffect } from "react"; +import { useNormalizedQuery } from "../../contexts/SpacedriveContext"; +import { ContentRenderer } from "./ContentRenderer"; interface QuickPreviewOverlayProps { - fileId: string; - isOpen: boolean; - onClose: () => void; - onNext?: () => void; - onPrevious?: () => void; - hasPrevious?: boolean; - hasNext?: boolean; + fileId: string; + isOpen: boolean; + onClose: () => void; + onNext?: () => void; + onPrevious?: () => void; + hasPrevious?: boolean; + hasNext?: boolean; } export function QuickPreviewOverlay({ - fileId, - isOpen, - onClose, - onNext, - onPrevious, - hasPrevious, - hasNext + fileId, + isOpen, + onClose, + onNext, + onPrevious, + hasPrevious, + hasNext, }: QuickPreviewOverlayProps) { - const { data: file, isLoading, error } = useNormalizedQuery<{ file_id: string }, File>({ - wireMethod: 'query:files.by_id', - input: { file_id: fileId }, - resourceType: 'file', - resourceId: fileId, - enabled: !!fileId && isOpen, - }); + const { + data: file, + isLoading, + error, + } = useNormalizedQuery<{ file_id: string }, File>({ + wireMethod: "query:files.by_id", + input: { file_id: fileId }, + resourceType: "file", + resourceId: fileId, + enabled: !!fileId && isOpen, + }); - useEffect(() => { - if (!isOpen) return; + useEffect(() => { + if (!isOpen) return; - const handleKeyDown = (e: KeyboardEvent) => { - if (e.code === 'Escape' || e.code === 'Space') { - e.preventDefault(); - onClose(); - } - if (e.code === 'ArrowLeft' && hasPrevious && onPrevious) { - e.preventDefault(); - onPrevious(); - } - if (e.code === 'ArrowRight' && hasNext && onNext) { - e.preventDefault(); - onNext(); - } - }; + const handleKeyDown = (e: KeyboardEvent) => { + if (e.code === "Escape" || e.code === "Space") { + e.preventDefault(); + onClose(); + } + if (e.code === "ArrowLeft" && hasPrevious && onPrevious) { + e.preventDefault(); + onPrevious(); + } + if (e.code === "ArrowRight" && hasNext && onNext) { + e.preventDefault(); + onNext(); + } + }; - window.addEventListener('keydown', handleKeyDown); - return () => window.removeEventListener('keydown', handleKeyDown); - }, [isOpen, onClose, onNext, onPrevious, hasPrevious, hasNext]); + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, [isOpen, onClose, onNext, onPrevious, hasPrevious, hasNext]); - return ( - - {isOpen && ( - - {isLoading || !file ? ( -
    -
    Loading...
    -
    - ) : error ? ( -
    -
    -
    Error loading file
    -
    {error.message}
    -
    -
    - ) : ( - <> - {/* Header */} -
    -
    - {/* Navigation Arrows */} - {(hasPrevious || hasNext) && ( - <> -
    - - -
    -
    - - )} -
    {file.name}
    -
    + return ( + + {isOpen && ( + + {isLoading || !file ? ( +
    +
    Loading...
    +
    + ) : error ? ( +
    +
    +
    + Error loading file +
    +
    {error.message}
    +
    +
    + ) : ( + <> + {/* Header */} +
    +
    + {/* Navigation Arrows */} + {(hasPrevious || hasNext) && ( + <> +
    + + +
    +
    + + )} +
    + {file.name} +
    +
    - -
    + +
    - {/* Content Area - full width, no inspector */} -
    - -
    + {/* Content Area - full width, no inspector */} +
    + +
    - {/* Footer with keyboard hints */} -
    -
    - ESC or{' '} - Space to close - {(hasPrevious || hasNext) && ( - <> - {' · '} - /{' '} - to navigate - - )} -
    -
    - - )} -
    - )} -
    - ); -} \ No newline at end of file + {/* Footer with keyboard hints */} +
    +
    + ESC or{" "} + Space to close + {(hasPrevious || hasNext) && ( + <> + {" · "} + /{" "} + to navigate + + )} +
    +
    + + )} + + )} + + ); +} diff --git a/packages/interface/src/components/QuickPreview/SplatShimmerEffect.tsx b/packages/interface/src/components/QuickPreview/SplatShimmerEffect.tsx index bba694947..1c0b505df 100644 --- a/packages/interface/src/components/QuickPreview/SplatShimmerEffect.tsx +++ b/packages/interface/src/components/QuickPreview/SplatShimmerEffect.tsx @@ -1,50 +1,50 @@ -import { useRef, useEffect } from "react"; +import { useEffect, useRef } from "react"; import * as THREE from "three"; interface SplatShimmerEffectProps { - children: React.ReactNode; - maskImage?: string; // Optional image URL to use as mask + children: React.ReactNode; + maskImage?: string; // Optional image URL to use as mask } export function SplatShimmerEffect({ - children, - maskImage, + children, + maskImage, }: SplatShimmerEffectProps) { - const canvasRef = useRef(null); - const rafRef = useRef(null); + const canvasRef = useRef(null); + const rafRef = useRef(null); - useEffect(() => { - if (!canvasRef.current) return; + useEffect(() => { + if (!canvasRef.current) return; - const container = canvasRef.current; - const width = container.clientWidth; - const height = container.clientHeight; + const container = canvasRef.current; + const width = container.clientWidth; + const height = container.clientHeight; - const scene = new THREE.Scene(); - const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1); + const scene = new THREE.Scene(); + const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1); - const renderer = new THREE.WebGLRenderer({ - antialias: false, - alpha: true, - powerPreference: "high-performance", - precision: "lowp", - stencil: false, - depth: false, - }); - renderer.setSize(width, height); - renderer.setPixelRatio(0.25); // Ultra low resolution - 16x fewer pixels - container.appendChild(renderer.domElement); + const renderer = new THREE.WebGLRenderer({ + antialias: false, + alpha: true, + powerPreference: "high-performance", + precision: "lowp", + stencil: false, + depth: false, + }); + renderer.setSize(width, height); + renderer.setPixelRatio(0.25); // Ultra low resolution - 16x fewer pixels + container.appendChild(renderer.domElement); - // Ultra simple shader - const material = new THREE.ShaderMaterial({ - vertexShader: ` + // Ultra simple shader + const material = new THREE.ShaderMaterial({ + vertexShader: ` varying vec2 vUv; void main() { vUv = uv; gl_Position = vec4(position, 1.0); } `, - fragmentShader: ` + fragmentShader: ` uniform float uTime; varying vec2 vUv; void main() { @@ -54,59 +54,59 @@ export function SplatShimmerEffect({ gl_FragColor = vec4(0.4, 0.65, 0.95, intensity); } `, - uniforms: { uTime: { value: 0 } }, - transparent: true, - depthWrite: false, - depthTest: false, - }); + uniforms: { uTime: { value: 0 } }, + transparent: true, + depthWrite: false, + depthTest: false, + }); - const mesh = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), material); - scene.add(mesh); + const mesh = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), material); + scene.add(mesh); - // Throttled animation - only update every 3rd frame - let frameCount = 0; - const animate = () => { - frameCount++; - if (frameCount % 3 === 0) { - material.uniforms.uTime.value += 0.05; - renderer.render(scene, camera); - } - rafRef.current = requestAnimationFrame(animate); - }; - rafRef.current = requestAnimationFrame(animate); + // Throttled animation - only update every 3rd frame + let frameCount = 0; + const animate = () => { + frameCount++; + if (frameCount % 3 === 0) { + material.uniforms.uTime.value += 0.05; + renderer.render(scene, camera); + } + rafRef.current = requestAnimationFrame(animate); + }; + rafRef.current = requestAnimationFrame(animate); - return () => { - if (rafRef.current) cancelAnimationFrame(rafRef.current); - renderer.dispose(); - material.dispose(); - mesh.geometry.dispose(); - if (container.contains(renderer.domElement)) { - container.removeChild(renderer.domElement); - } - }; - }, []); + return () => { + if (rafRef.current) cancelAnimationFrame(rafRef.current); + renderer.dispose(); + material.dispose(); + mesh.geometry.dispose(); + if (container.contains(renderer.domElement)) { + container.removeChild(renderer.domElement); + } + }; + }, []); - return ( -
    - {children} -
    -
    - ); + return ( +
    + {children} +
    +
    + ); } diff --git a/packages/interface/src/components/QuickPreview/SubtitleSettingsMenu.tsx b/packages/interface/src/components/QuickPreview/SubtitleSettingsMenu.tsx index d29d5bba4..286fb7c9b 100644 --- a/packages/interface/src/components/QuickPreview/SubtitleSettingsMenu.tsx +++ b/packages/interface/src/components/QuickPreview/SubtitleSettingsMenu.tsx @@ -1,125 +1,130 @@ -import { motion, AnimatePresence } from 'framer-motion'; -import type { SubtitleSettings } from './Subtitles'; +import { AnimatePresence, motion } from "framer-motion"; +import type { SubtitleSettings } from "./Subtitles"; interface SubtitleSettingsMenuProps { - isOpen: boolean; - settings: SubtitleSettings; - onSettingsChange: (settings: SubtitleSettings) => void; - onClose: () => void; + isOpen: boolean; + settings: SubtitleSettings; + onSettingsChange: (settings: SubtitleSettings) => void; + onClose: () => void; } export function SubtitleSettingsMenu({ - isOpen, - settings, - onSettingsChange, - onClose + isOpen, + settings, + onSettingsChange, + onClose, }: SubtitleSettingsMenuProps) { - return ( - - {isOpen && ( - <> - {/* Backdrop */} -
    + return ( + + {isOpen && ( + <> + {/* Backdrop */} +
    - {/* Settings Menu */} - e.stopPropagation()} - > -

    Subtitle Settings

    + {/* Settings Menu */} + e.stopPropagation()} + transition={{ duration: 0.15 }} + > +

    + Subtitle Settings +

    -
    - {/* Font Size */} -
    - - - onSettingsChange({ - ...settings, - fontSize: parseFloat(e.target.value) - }) - } - className="h-1.5 w-full cursor-pointer appearance-none rounded-full bg-sidebar-line [&::-webkit-slider-thumb]:size-3.5 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-accent [&::-webkit-slider-thumb]:shadow-lg" - /> -
    +
    + {/* Font Size */} +
    + + + onSettingsChange({ + ...settings, + fontSize: Number.parseFloat(e.target.value), + }) + } + step="0.1" + type="range" + value={settings.fontSize} + /> +
    - {/* Background Opacity */} -
    - - - onSettingsChange({ - ...settings, - backgroundOpacity: parseFloat(e.target.value) - }) - } - className="h-1.5 w-full cursor-pointer appearance-none rounded-full bg-sidebar-line [&::-webkit-slider-thumb]:size-3.5 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-accent [&::-webkit-slider-thumb]:shadow-lg" - /> -
    + {/* Background Opacity */} +
    + + + onSettingsChange({ + ...settings, + backgroundOpacity: Number.parseFloat(e.target.value), + }) + } + step="0.1" + type="range" + value={settings.backgroundOpacity} + /> +
    - {/* Position */} -
    - -
    - - -
    -
    -
    - - - )} - - ); + {/* Position */} +
    + +
    + + +
    +
    +
    +
    + + )} + + ); } diff --git a/packages/interface/src/components/QuickPreview/Subtitles.tsx b/packages/interface/src/components/QuickPreview/Subtitles.tsx index 1477eaffd..48983c9a6 100644 --- a/packages/interface/src/components/QuickPreview/Subtitles.tsx +++ b/packages/interface/src/components/QuickPreview/Subtitles.tsx @@ -1,30 +1,30 @@ -import { useEffect, useState, useRef } from "react"; import type { File } from "@sd/ts-client"; +import { useEffect, useState } from "react"; import { useServer } from "../../contexts/ServerContext"; interface SubtitleCue { - index: number; - startTime: number; - endTime: number; - text: string; + index: number; + startTime: number; + endTime: number; + text: string; } export interface SubtitleSettings { - fontSize: number; // 0.8 to 2.0 - position: "bottom" | "top"; - backgroundOpacity: number; // 0 to 1 + fontSize: number; // 0.8 to 2.0 + position: "bottom" | "top"; + backgroundOpacity: number; // 0 to 1 } interface SubtitlesProps { - file: File; - videoElement: HTMLVideoElement | null; - settings?: SubtitleSettings; + file: File; + videoElement: HTMLVideoElement | null; + settings?: SubtitleSettings; } const DEFAULT_SETTINGS: SubtitleSettings = { - fontSize: 1.5, - position: "bottom", - backgroundOpacity: 0.9, + fontSize: 1.5, + position: "bottom", + backgroundOpacity: 0.9, }; /** @@ -39,178 +39,169 @@ const DEFAULT_SETTINGS: SubtitleSettings = { * Next subtitle */ function parseSRT(srtContent: string): SubtitleCue[] { - const cues: SubtitleCue[] = []; - const blocks = srtContent.trim().split(/\n\s*\n/); + const cues: SubtitleCue[] = []; + const blocks = srtContent.trim().split(/\n\s*\n/); - for (const block of blocks) { - const lines = block.trim().split("\n"); - if (lines.length < 3) continue; + for (const block of blocks) { + const lines = block.trim().split("\n"); + if (lines.length < 3) continue; - const index = parseInt(lines[0], 10); - const timecodeMatch = lines[1].match( - /(\d{2}):(\d{2}):(\d{2}),(\d{3})\s*-->\s*(\d{2}):(\d{2}):(\d{2}),(\d{3})/, - ); + const index = Number.parseInt(lines[0], 10); + const timecodeMatch = lines[1].match( + /(\d{2}):(\d{2}):(\d{2}),(\d{3})\s*-->\s*(\d{2}):(\d{2}):(\d{2}),(\d{3})/ + ); - if (!timecodeMatch) continue; + if (!timecodeMatch) continue; - const startTime = - parseInt(timecodeMatch[1]) * 3600 + - parseInt(timecodeMatch[2]) * 60 + - parseInt(timecodeMatch[3]) + - parseInt(timecodeMatch[4]) / 1000; + const startTime = + Number.parseInt(timecodeMatch[1]) * 3600 + + Number.parseInt(timecodeMatch[2]) * 60 + + Number.parseInt(timecodeMatch[3]) + + Number.parseInt(timecodeMatch[4]) / 1000; - const endTime = - parseInt(timecodeMatch[5]) * 3600 + - parseInt(timecodeMatch[6]) * 60 + - parseInt(timecodeMatch[7]) + - parseInt(timecodeMatch[8]) / 1000; + const endTime = + Number.parseInt(timecodeMatch[5]) * 3600 + + Number.parseInt(timecodeMatch[6]) * 60 + + Number.parseInt(timecodeMatch[7]) + + Number.parseInt(timecodeMatch[8]) / 1000; - const text = lines.slice(2).join("\n"); + const text = lines.slice(2).join("\n"); - cues.push({ index, startTime, endTime, text }); - } + cues.push({ index, startTime, endTime, text }); + } - return cues; + return cues; } export function Subtitles({ - file, - videoElement, - settings = DEFAULT_SETTINGS, + file, + videoElement, + settings = DEFAULT_SETTINGS, }: SubtitlesProps) { - const [cues, setCues] = useState([]); - const [currentCue, setCurrentCue] = useState(null); - const { buildSidecarUrl } = useServer(); + const [cues, setCues] = useState([]); + const [currentCue, setCurrentCue] = useState(null); + const { buildSidecarUrl } = useServer(); - // Load SRT sidecar if available - useEffect(() => { - const srtSidecar = file.sidecars?.find( - (s) => s.kind === "transcript" && s.variant === "srt", - ); + // Load SRT sidecar if available + useEffect(() => { + const srtSidecar = file.sidecars?.find( + (s) => s.kind === "transcript" && s.variant === "srt" + ); - if (!srtSidecar || !file.content_identity?.uuid) { - return; - } + if (!(srtSidecar && file.content_identity?.uuid)) { + return; + } - // Map "text" format to "txt" extension (DB stores "text", file is .txt) - const extension = - srtSidecar.format === "text" ? "txt" : srtSidecar.format; - const srtUrl = buildSidecarUrl( - file.content_identity.uuid, - srtSidecar.kind, - srtSidecar.variant, - extension, - ); + // Map "text" format to "txt" extension (DB stores "text", file is .txt) + const extension = srtSidecar.format === "text" ? "txt" : srtSidecar.format; + const srtUrl = buildSidecarUrl( + file.content_identity.uuid, + srtSidecar.kind, + srtSidecar.variant, + extension + ); - if (!srtUrl) { - console.warn("[Subtitles] Server URL or Library ID not available"); - return; - } + if (!srtUrl) { + console.warn("[Subtitles] Server URL or Library ID not available"); + return; + } - console.log("[Subtitles] Loading SRT from:", srtUrl); + console.log("[Subtitles] Loading SRT from:", srtUrl); - fetch(srtUrl) - .then(async (res) => { - if (!res.ok) { - if (res.status === 404) { - console.log( - "[Subtitles] No subtitle file found (not generated yet)", - ); - } else { - console.error( - "[Subtitles] Failed to fetch SRT, status:", - res.status, - ); - } - return null; - } - return res.text(); - }) - .then((srtContent) => { - if (!srtContent) return; - const parsed = parseSRT(srtContent); - console.log( - "[Subtitles] Loaded and parsed", - parsed.length, - "subtitle cues", - ); - setCues(parsed); - }) - .catch((err) => { - console.log( - "[Subtitles] Subtitles not available:", - err.message, - ); - }); - }, [file, buildSidecarUrl]); + fetch(srtUrl) + .then(async (res) => { + if (!res.ok) { + if (res.status === 404) { + console.log( + "[Subtitles] No subtitle file found (not generated yet)" + ); + } else { + console.error( + "[Subtitles] Failed to fetch SRT, status:", + res.status + ); + } + return null; + } + return res.text(); + }) + .then((srtContent) => { + if (!srtContent) return; + const parsed = parseSRT(srtContent); + console.log( + "[Subtitles] Loaded and parsed", + parsed.length, + "subtitle cues" + ); + setCues(parsed); + }) + .catch((err) => { + console.log("[Subtitles] Subtitles not available:", err.message); + }); + }, [file, buildSidecarUrl]); - // Sync with video playback - useEffect(() => { - if (!videoElement || cues.length === 0) { - console.log( - "[Subtitles] Not setting up sync - videoElement:", - !!videoElement, - "cues:", - cues.length, - ); - return; - } + // Sync with video playback + useEffect(() => { + if (!videoElement || cues.length === 0) { + console.log( + "[Subtitles] Not setting up sync - videoElement:", + !!videoElement, + "cues:", + cues.length + ); + return; + } - console.log( - "[Subtitles] Setting up video sync with", - cues.length, - "cues", - ); + console.log("[Subtitles] Setting up video sync with", cues.length, "cues"); - const updateSubtitle = () => { - const currentTime = videoElement.currentTime; - const activeCue = cues.find( - (cue) => - currentTime >= cue.startTime && currentTime <= cue.endTime, - ); + const updateSubtitle = () => { + const currentTime = videoElement.currentTime; + const activeCue = cues.find( + (cue) => currentTime >= cue.startTime && currentTime <= cue.endTime + ); - if (activeCue !== currentCue) { - setCurrentCue(activeCue || null); - } - }; + if (activeCue !== currentCue) { + setCurrentCue(activeCue || null); + } + }; - // Update on time change - videoElement.addEventListener("timeupdate", updateSubtitle); + // Update on time change + videoElement.addEventListener("timeupdate", updateSubtitle); - // Also update when seeking - videoElement.addEventListener("seeked", updateSubtitle); + // Also update when seeking + videoElement.addEventListener("seeked", updateSubtitle); - return () => { - videoElement.removeEventListener("timeupdate", updateSubtitle); - videoElement.removeEventListener("seeked", updateSubtitle); - }; - }, [videoElement, cues, currentCue]); + return () => { + videoElement.removeEventListener("timeupdate", updateSubtitle); + videoElement.removeEventListener("seeked", updateSubtitle); + }; + }, [videoElement, cues, currentCue]); - if (!currentCue) { - return null; - } + if (!currentCue) { + return null; + } - const positionClass = settings.position === "top" ? "top-16" : "bottom-16"; + const positionClass = settings.position === "top" ? "top-16" : "bottom-16"; - return ( -
    -
    -

    - {currentCue.text} -

    -
    -
    - ); -} \ No newline at end of file + return ( +
    +
    +

    + {currentCue.text} +

    +
    +
    + ); +} diff --git a/packages/interface/src/components/QuickPreview/Syncer.tsx b/packages/interface/src/components/QuickPreview/Syncer.tsx index ab46c6a04..36d59330b 100644 --- a/packages/interface/src/components/QuickPreview/Syncer.tsx +++ b/packages/interface/src/components/QuickPreview/Syncer.tsx @@ -10,20 +10,20 @@ import { useSelection } from "../../routes/explorer/SelectionContext"; * we update the preview to show the newly selected file. */ export function QuickPreviewSyncer() { - const { quickPreviewFileId, openQuickPreview } = useExplorer(); - const { selectedFiles } = useSelection(); + const { quickPreviewFileId, openQuickPreview } = useExplorer(); + const { selectedFiles } = useSelection(); - useEffect(() => { - if (!quickPreviewFileId) return; + useEffect(() => { + if (!quickPreviewFileId) return; - // When selection changes and QuickPreview is open, update preview to match selection - if ( - selectedFiles.length === 1 && - selectedFiles[0].id !== quickPreviewFileId - ) { - openQuickPreview(selectedFiles[0].id); - } - }, [selectedFiles, quickPreviewFileId, openQuickPreview]); + // When selection changes and QuickPreview is open, update preview to match selection + if ( + selectedFiles.length === 1 && + selectedFiles[0].id !== quickPreviewFileId + ) { + openQuickPreview(selectedFiles[0].id); + } + }, [selectedFiles, quickPreviewFileId, openQuickPreview]); - return null; -} \ No newline at end of file + return null; +} diff --git a/packages/interface/src/components/QuickPreview/TextViewer.tsx b/packages/interface/src/components/QuickPreview/TextViewer.tsx index 79f3a533c..3f8173137 100644 --- a/packages/interface/src/components/QuickPreview/TextViewer.tsx +++ b/packages/interface/src/components/QuickPreview/TextViewer.tsx @@ -1,155 +1,168 @@ -import { useVirtualizer, VirtualItem } from '@tanstack/react-virtual'; -import clsx from 'clsx'; -import { memo, useEffect, useRef, useState } from 'react'; +import { useVirtualizer, type VirtualItem } from "@tanstack/react-virtual"; +import clsx from "clsx"; +import { memo, useEffect, useRef, useState } from "react"; -import { languageMapping } from './prism'; +import { languageMapping } from "./prism"; -const prismaLazy = import('./prism-lazy'); -prismaLazy.catch((e) => console.error('Failed to load prism-lazy', e)); +const prismaLazy = import("./prism-lazy"); +prismaLazy.catch((e) => console.error("Failed to load prism-lazy", e)); export interface TextViewerProps { - src: string; - className?: string; - onLoad?: (event: HTMLElementEventMap['load']) => void; - onError?: (event: HTMLElementEventMap['error']) => void; - codeExtension?: string; - isSidebarPreview?: boolean; + src: string; + className?: string; + onLoad?: (event: HTMLElementEventMap["load"]) => void; + onError?: (event: HTMLElementEventMap["error"]) => void; + codeExtension?: string; + isSidebarPreview?: boolean; } export const TextViewer = memo( - ({ src, className, onLoad, onError, codeExtension, isSidebarPreview }: TextViewerProps) => { - const [lines, setLines] = useState([]); - const parentRef = useRef(null); - const rowVirtualizer = useVirtualizer({ - count: lines.length, - getScrollElement: () => parentRef.current, - estimateSize: () => 22 - }); + ({ + src, + className, + onLoad, + onError, + codeExtension, + isSidebarPreview, + }: TextViewerProps) => { + const [lines, setLines] = useState([]); + const parentRef = useRef(null); + const rowVirtualizer = useVirtualizer({ + count: lines.length, + getScrollElement: () => parentRef.current, + estimateSize: () => 22, + }); - useEffect(() => { - if (!src || src === '#') return; + useEffect(() => { + if (!src || src === "#") return; - const controller = new AbortController(); - fetch(src, { - mode: 'cors', - signal: controller.signal - }) - .then((response) => { - if (!response.ok) throw new Error(`Invalid response: ${response.statusText}`); - if (!response.body) return; - onLoad?.(new UIEvent('load', {})); + const controller = new AbortController(); + fetch(src, { + mode: "cors", + signal: controller.signal, + }) + .then((response) => { + if (!response.ok) + throw new Error(`Invalid response: ${response.statusText}`); + if (!response.body) return; + onLoad?.(new UIEvent("load", {})); - const reader = response.body.pipeThrough(new TextDecoderStream()).getReader(); - return reader.read().then(function ingestLines({ - done, - value - }): void | Promise { - if (done) return; + const reader = response.body + .pipeThrough(new TextDecoderStream()) + .getReader(); + return reader.read().then(function ingestLines({ + done, + value, + }): void | Promise { + if (done) return; - const chunks = value.split('\n'); - setLines([...chunks]); + const chunks = value.split("\n"); + setLines([...chunks]); - if (isSidebarPreview) return; + if (isSidebarPreview) return; - return reader.read().then(ingestLines); - }); - }) - .catch((error) => { - if (!controller.signal.aborted) - onError?.(new ErrorEvent('error', { message: `${error}` })); - }); + return reader.read().then(ingestLines); + }); + }) + .catch((error) => { + if (!controller.signal.aborted) + onError?.(new ErrorEvent("error", { message: `${error}` })); + }); - return () => controller.abort(); - }, [src, onError, onLoad, codeExtension, isSidebarPreview]); + return () => controller.abort(); + }, [src, onError, onLoad, codeExtension, isSidebarPreview]); - return ( -
    -				
    - {rowVirtualizer.getVirtualItems().map((row) => ( - - ))} -
    -
    - ); - } + return ( +
    +        
    + {rowVirtualizer.getVirtualItems().map((row) => ( + + ))} +
    +
    + ); + } ); function TextRow({ - codeExtension, - row, - content + codeExtension, + row, + content, }: { - codeExtension?: string; - row: VirtualItem; - content: string; + codeExtension?: string; + row: VirtualItem; + content: string; }) { - const contentRef = useRef(null); + const contentRef = useRef(null); - useEffect(() => { - const ref = contentRef.current; - if (ref == null) return; + useEffect(() => { + const ref = contentRef.current; + if (ref == null) return; - let intersectionObserver: null | IntersectionObserver = null; + let intersectionObserver: null | IntersectionObserver = null; - prismaLazy.then(({ highlightElement }) => { - intersectionObserver = new IntersectionObserver((events) => { - for (const event of events) { - if (!event.isIntersecting || ref.getAttribute('data-highlighted') === 'true') - continue; + prismaLazy.then(({ highlightElement }) => { + intersectionObserver = new IntersectionObserver((events) => { + for (const event of events) { + if ( + !event.isIntersecting || + ref.getAttribute("data-highlighted") === "true" + ) + continue; - ref.setAttribute('data-highlighted', 'true'); - highlightElement(event.target, false); + ref.setAttribute("data-highlighted", "true"); + highlightElement(event.target, false); - const children = ref.children; - if (children) { - for (const elem of children) { - elem.classList.remove('table'); - } - } - } - }); - intersectionObserver.observe(ref); - }); + const children = ref.children; + if (children) { + for (const elem of children) { + elem.classList.remove("table"); + } + } + } + }); + intersectionObserver.observe(ref); + }); - return () => intersectionObserver?.disconnect(); - }, []); + return () => intersectionObserver?.disconnect(); + }, []); - return ( -
    - {codeExtension && ( -
    - {row.index + 1} -
    - )} - - {content} - -
    - ); + return ( +
    + {codeExtension && ( +
    + {row.index + 1} +
    + )} + + {content} + +
    + ); } diff --git a/packages/interface/src/components/QuickPreview/TimelineScrubber.tsx b/packages/interface/src/components/QuickPreview/TimelineScrubber.tsx index 9d3573479..cbd98e79d 100644 --- a/packages/interface/src/components/QuickPreview/TimelineScrubber.tsx +++ b/packages/interface/src/components/QuickPreview/TimelineScrubber.tsx @@ -1,14 +1,14 @@ -import { memo } from "react"; import type { File } from "@sd/ts-client"; +import { memo } from "react"; import { useServer } from "../../contexts/ServerContext"; interface TimelineScrubberProps { - file: File; - hoverPercent: number; - mouseX: number; - duration: number; - sidebarWidth?: number; - inspectorWidth?: number; + file: File; + hoverPercent: number; + mouseX: number; + duration: number; + sidebarWidth?: number; + inspectorWidth?: number; } /** @@ -18,127 +18,125 @@ interface TimelineScrubberProps { * Similar to YouTube's timeline preview feature */ export const TimelineScrubber = memo(function TimelineScrubber({ - file, - hoverPercent, - mouseX, - duration, - sidebarWidth = 0, - inspectorWidth = 0, + file, + hoverPercent, + mouseX, + duration, + sidebarWidth = 0, + inspectorWidth = 0, }: TimelineScrubberProps) { - const { buildSidecarUrl } = useServer(); + const { buildSidecarUrl } = useServer(); - // Find thumbstrip sidecar - const thumbstripSidecar = file.sidecars?.find( - (s) => s.kind === "thumbstrip", - ); + // Find thumbstrip sidecar + const thumbstripSidecar = file.sidecars?.find((s) => s.kind === "thumbstrip"); - if (!thumbstripSidecar) { - return null; - } + if (!thumbstripSidecar) { + return null; + } - // Parse grid dimensions - const getGridDimensions = (variant: string) => { - if (variant.includes("detailed")) return { columns: 10, rows: 10 }; - if (variant.includes("mobile")) return { columns: 3, rows: 3 }; - return { columns: 5, rows: 5 }; - }; + // Parse grid dimensions + const getGridDimensions = (variant: string) => { + if (variant.includes("detailed")) return { columns: 10, rows: 10 }; + if (variant.includes("mobile")) return { columns: 3, rows: 3 }; + return { columns: 5, rows: 5 }; + }; - const grid = getGridDimensions(thumbstripSidecar.variant); - const totalFrames = grid.columns * grid.rows; + const grid = getGridDimensions(thumbstripSidecar.variant); + const totalFrames = grid.columns * grid.rows; - // Build thumbstrip URL - if (!file.content_identity?.uuid) { - return null; - } + // Build thumbstrip URL + if (!file.content_identity?.uuid) { + return null; + } - const thumbstripUrl = buildSidecarUrl( - file.content_identity.uuid, - thumbstripSidecar.kind, - thumbstripSidecar.variant, - thumbstripSidecar.format, - ); + const thumbstripUrl = buildSidecarUrl( + file.content_identity.uuid, + thumbstripSidecar.kind, + thumbstripSidecar.variant, + thumbstripSidecar.format + ); - if (!thumbstripUrl) { - return null; - } + if (!thumbstripUrl) { + return null; + } - // Calculate which frame to show - const frameIndex = Math.min( - Math.floor(hoverPercent * totalFrames), - totalFrames - 1, - ); + // Calculate which frame to show + const frameIndex = Math.min( + Math.floor(hoverPercent * totalFrames), + totalFrames - 1 + ); - const row = Math.floor(frameIndex / grid.columns); - const col = frameIndex % grid.columns; + const row = Math.floor(frameIndex / grid.columns); + const col = frameIndex % grid.columns; - // Calculate sprite position - const spriteX = grid.columns > 1 ? (col / (grid.columns - 1)) * 100 : 0; - const spriteY = grid.rows > 1 ? (row / (grid.rows - 1)) * 100 : 0; + // Calculate sprite position + const spriteX = grid.columns > 1 ? (col / (grid.columns - 1)) * 100 : 0; + const spriteY = grid.rows > 1 ? (row / (grid.rows - 1)) * 100 : 0; - // Preview dimensions (fixed width, 16:9 aspect ratio) - const previewWidth = 160; - const previewHeight = 90; + // Preview dimensions (fixed width, 16:9 aspect ratio) + const previewWidth = 160; + const previewHeight = 90; - // Position horizontally following mouse, clamped to controls bounds - // Adjust for sidebar offset and clamp within the controls area - const controlsWidth = window.innerWidth - sidebarWidth - inspectorWidth; - const mouseXRelativeToControls = mouseX - sidebarWidth; - const leftPosition = Math.max( - 10, - Math.min( - mouseXRelativeToControls - previewWidth / 2, - controlsWidth - previewWidth - 10, - ), - ); + // Position horizontally following mouse, clamped to controls bounds + // Adjust for sidebar offset and clamp within the controls area + const controlsWidth = window.innerWidth - sidebarWidth - inspectorWidth; + const mouseXRelativeToControls = mouseX - sidebarWidth; + const leftPosition = Math.max( + 10, + Math.min( + mouseXRelativeToControls - previewWidth / 2, + controlsWidth - previewWidth - 10 + ) + ); - // Format timestamp - const timestamp = formatTime(hoverPercent * duration); + // Format timestamp + const timestamp = formatTime(hoverPercent * duration); - return ( -
    - {/* Preview frame */} -
    + return ( +
    + {/* Preview frame */} +
    - {/* Timestamp below preview */} -
    -
    - {timestamp} -
    -
    + {/* Timestamp below preview */} +
    +
    + {timestamp} +
    +
    - {/* Pointer arrow */} -
    -
    -
    -
    - ); + {/* Pointer arrow */} +
    +
    +
    +
    + ); }); function formatTime(seconds: number): string { - const hours = Math.floor(seconds / 3600); - const mins = Math.floor((seconds % 3600) / 60); - const secs = Math.floor(seconds % 60); + const hours = Math.floor(seconds / 3600); + const mins = Math.floor((seconds % 3600) / 60); + const secs = Math.floor(seconds % 60); - if (hours > 0) { - return `${hours}:${mins.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`; - } - return `${mins}:${secs.toString().padStart(2, "0")}`; -} \ No newline at end of file + if (hours > 0) { + return `${hours}:${mins.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`; + } + return `${mins}:${secs.toString().padStart(2, "0")}`; +} diff --git a/packages/interface/src/components/QuickPreview/VideoControls.tsx b/packages/interface/src/components/QuickPreview/VideoControls.tsx index 3ea94c0a8..a92fcdfa3 100644 --- a/packages/interface/src/components/QuickPreview/VideoControls.tsx +++ b/packages/interface/src/components/QuickPreview/VideoControls.tsx @@ -1,288 +1,280 @@ import { - Play, - Pause, - SpeakerHigh, - SpeakerSlash, - ArrowsOut, - ClosedCaptioning, - MagnifyingGlassPlus, - MagnifyingGlassMinus, - ArrowCounterClockwise, - Gear, - Repeat, + ArrowCounterClockwise, + ArrowsOut, + ClosedCaptioning, + Gear, + MagnifyingGlassMinus, + MagnifyingGlassPlus, + Pause, + Play, + Repeat, + SpeakerHigh, + SpeakerSlash, } from "@phosphor-icons/react"; -import { motion, AnimatePresence } from "framer-motion"; import type { File } from "@sd/ts-client"; +import { AnimatePresence, motion } from "framer-motion"; import { TimelineScrubber } from "./TimelineScrubber"; export interface VideoControlsState { - playing: boolean; - currentTime: number; - duration: number; - volume: number; - muted: boolean; - loop: boolean; - zoom: number; - subtitlesEnabled: boolean; - showSubtitleSettings: boolean; - seeking: boolean; - timelineHover: { percent: number; mouseX: number } | null; + playing: boolean; + currentTime: number; + duration: number; + volume: number; + muted: boolean; + loop: boolean; + zoom: number; + subtitlesEnabled: boolean; + showSubtitleSettings: boolean; + seeking: boolean; + timelineHover: { percent: number; mouseX: number } | null; } export interface VideoControlsCallbacks { - onTogglePlay: () => void; - onSeek: (e: React.MouseEvent) => void; - onTimelineHover: (e: React.MouseEvent) => void; - onTimelineLeave: () => void; - onSeekingStart: () => void; - onSeekingEnd: () => void; - onVolumeChange: (volume: number) => void; - onMuteToggle: () => void; - onLoopToggle: () => void; - onZoomIn: () => void; - onZoomOut: () => void; - onZoomReset: () => void; - onSubtitlesToggle: () => void; - onSubtitleSettingsToggle: () => void; - onFullscreenToggle: () => void; - onMouseMove: () => void; + onTogglePlay: () => void; + onSeek: (e: React.MouseEvent) => void; + onTimelineHover: (e: React.MouseEvent) => void; + onTimelineLeave: () => void; + onSeekingStart: () => void; + onSeekingEnd: () => void; + onVolumeChange: (volume: number) => void; + onMuteToggle: () => void; + onLoopToggle: () => void; + onZoomIn: () => void; + onZoomOut: () => void; + onZoomReset: () => void; + onSubtitlesToggle: () => void; + onSubtitleSettingsToggle: () => void; + onFullscreenToggle: () => void; + onMouseMove: () => void; } interface VideoControlsProps { - file: File; - state: VideoControlsState; - callbacks: VideoControlsCallbacks; - showControls: boolean; - sidebarWidth?: number; - inspectorWidth?: number; + file: File; + state: VideoControlsState; + callbacks: VideoControlsCallbacks; + showControls: boolean; + sidebarWidth?: number; + inspectorWidth?: number; } function formatTime(seconds: number): string { - const hours = Math.floor(seconds / 3600); - const mins = Math.floor((seconds % 3600) / 60); - const secs = Math.floor(seconds % 60); + const hours = Math.floor(seconds / 3600); + const mins = Math.floor((seconds % 3600) / 60); + const secs = Math.floor(seconds % 60); - if (hours > 0) { - return `${hours}:${mins.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`; - } - return `${mins}:${secs.toString().padStart(2, "0")}`; + if (hours > 0) { + return `${hours}:${mins.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`; + } + return `${mins}:${secs.toString().padStart(2, "0")}`; } export function VideoControls({ - file, - state, - callbacks, - showControls, - sidebarWidth = 0, - inspectorWidth = 0, + file, + state, + callbacks, + showControls, + sidebarWidth = 0, + inspectorWidth = 0, }: VideoControlsProps) { - const hasSubs = file.sidecars?.some( - (s) => s.kind === "transcript" && s.variant === "srt", - ); + const hasSubs = file.sidecars?.some( + (s) => s.kind === "transcript" && s.variant === "srt" + ); - return ( - - {showControls && ( - - {/* Timeline Scrubber Preview */} - {state.timelineHover && ( - - )} + return ( + + {showControls && ( + + {/* Timeline Scrubber Preview */} + {state.timelineHover && ( + + )} - {/* Progress Bar with Thick Hover Area */} -
    { - callbacks.onSeekingStart(); - callbacks.onSeek(e); - }} - onMouseMove={(e) => { - if (state.seeking) { - callbacks.onSeek(e); - } else { - callbacks.onTimelineHover(e); - } - }} - onMouseEnter={callbacks.onTimelineHover} - onMouseUp={callbacks.onSeekingEnd} - onMouseLeave={callbacks.onTimelineLeave} - > -
    - {/* Progress */} -
    + {/* Progress Bar with Thick Hover Area */} +
    { + callbacks.onSeekingStart(); + callbacks.onSeek(e); + }} + onMouseEnter={callbacks.onTimelineHover} + onMouseLeave={callbacks.onTimelineLeave} + onMouseMove={(e) => { + if (state.seeking) { + callbacks.onSeek(e); + } else { + callbacks.onTimelineHover(e); + } + }} + onMouseUp={callbacks.onSeekingEnd} + > +
    + {/* Progress */} +
    - {/* Scrubber */} -
    -
    -
    -
    -
    + {/* Scrubber */} +
    +
    +
    +
    +
    - {/* Controls Bar */} -
    - {/* Play/Pause */} - + {/* Controls Bar */} +
    + {/* Play/Pause */} + - {/* Loop */} - + {/* Loop */} + - {/* Time */} -
    - {formatTime(state.currentTime)} /{" "} - {formatTime(state.duration)} -
    + {/* Time */} +
    + {formatTime(state.currentTime)} / {formatTime(state.duration)} +
    -
    +
    - {/* Subtitles Controls */} - {hasSubs && ( -
    - - {state.subtitlesEnabled && ( - - )} -
    - )} + {/* Subtitles Controls */} + {hasSubs && ( +
    + + {state.subtitlesEnabled && ( + + )} +
    + )} - {/* Zoom Controls */} -
    - - - {state.zoom > 1 && ( - - )} -
    + {/* Zoom Controls */} +
    + + + {state.zoom > 1 && ( + + )} +
    - {/* Volume */} -
    - + {/* Volume */} +
    + - {/* Volume Slider */} -
    - - callbacks.onVolumeChange( - parseFloat(e.target.value), - ) - } - className="h-1 w-full cursor-pointer appearance-none rounded-full bg-white/20 [&::-webkit-slider-thumb]:size-3 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-white" - /> -
    -
    + {/* Volume Slider */} +
    + + callbacks.onVolumeChange(Number.parseFloat(e.target.value)) + } + step="0.01" + type="range" + value={state.volume} + /> +
    +
    - {/* Fullscreen */} - -
    - - )} - - ); + {/* Fullscreen */} + +
    + + )} + + ); } diff --git a/packages/interface/src/components/QuickPreview/VideoPlayer.tsx b/packages/interface/src/components/QuickPreview/VideoPlayer.tsx index 47c54b31c..0da9387d3 100644 --- a/packages/interface/src/components/QuickPreview/VideoPlayer.tsx +++ b/packages/interface/src/components/QuickPreview/VideoPlayer.tsx @@ -1,351 +1,343 @@ -import { useState, useRef, useEffect, useCallback } from "react"; import type { File } from "@sd/ts-client"; -import { Subtitles, type SubtitleSettings } from "./Subtitles"; +import { useCallback, useEffect, useRef, useState } from "react"; import { SubtitleSettingsMenu } from "./SubtitleSettingsMenu"; +import { type SubtitleSettings, Subtitles } from "./Subtitles"; import { useZoomPan } from "./useZoomPan"; import type { - VideoControlsState, - VideoControlsCallbacks, + VideoControlsCallbacks, + VideoControlsState, } from "./VideoControls"; interface VideoPlayerProps { - src: string; - file: File; - onZoomChange?: (isZoomed: boolean) => void; - onControlsStateChange?: (state: VideoControlsState) => void; - onShowControlsChange?: (show: boolean) => void; - getCallbacks?: (callbacks: VideoControlsCallbacks) => void; + src: string; + file: File; + onZoomChange?: (isZoomed: boolean) => void; + onControlsStateChange?: (state: VideoControlsState) => void; + onShowControlsChange?: (show: boolean) => void; + getCallbacks?: (callbacks: VideoControlsCallbacks) => void; } export function VideoPlayer({ - src, - file, - onZoomChange, - onControlsStateChange, - onShowControlsChange, - getCallbacks, + src, + file, + onZoomChange, + onControlsStateChange, + onShowControlsChange, + getCallbacks, }: VideoPlayerProps) { - const videoRef = useRef(null); - const containerRef = useRef(null); - const videoContainerRef = useRef(null); - const [playing, setPlaying] = useState(false); - const [currentTime, setCurrentTime] = useState(0); - const [duration, setDuration] = useState(0); - const [volume, setVolume] = useState(() => { - const saved = localStorage.getItem("sd-video-volume"); - return saved ? parseFloat(saved) : 1; - }); - const [muted, setMuted] = useState(() => { - const saved = localStorage.getItem("sd-video-muted"); - return saved === "true"; - }); - const [loop, setLoop] = useState(false); - const [showControls, setShowControls] = useState(true); - const [seeking, setSeeking] = useState(false); - const [subtitlesEnabled, setSubtitlesEnabled] = useState(true); - const [showSubtitleSettings, setShowSubtitleSettings] = useState(false); - const [subtitleSettings, setSubtitleSettings] = useState({ - fontSize: 1.5, - position: "bottom", - backgroundOpacity: 0.9, - }); - const [timelineHover, setTimelineHover] = useState<{ - percent: number; - mouseX: number; - } | null>(null); - const hideControlsTimeout = useRef(undefined); - const { zoom, zoomIn, zoomOut, reset, isZoomed, transform } = - useZoomPan(videoContainerRef); + const videoRef = useRef(null); + const containerRef = useRef(null); + const videoContainerRef = useRef(null); + const [playing, setPlaying] = useState(false); + const [currentTime, setCurrentTime] = useState(0); + const [duration, setDuration] = useState(0); + const [volume, setVolume] = useState(() => { + const saved = localStorage.getItem("sd-video-volume"); + return saved ? Number.parseFloat(saved) : 1; + }); + const [muted, setMuted] = useState(() => { + const saved = localStorage.getItem("sd-video-muted"); + return saved === "true"; + }); + const [loop, setLoop] = useState(false); + const [showControls, setShowControls] = useState(true); + const [seeking, setSeeking] = useState(false); + const [subtitlesEnabled, setSubtitlesEnabled] = useState(true); + const [showSubtitleSettings, setShowSubtitleSettings] = useState(false); + const [subtitleSettings, setSubtitleSettings] = useState({ + fontSize: 1.5, + position: "bottom", + backgroundOpacity: 0.9, + }); + const [timelineHover, setTimelineHover] = useState<{ + percent: number; + mouseX: number; + } | null>(null); + const hideControlsTimeout = useRef(undefined); + const { zoom, zoomIn, zoomOut, reset, isZoomed, transform } = + useZoomPan(videoContainerRef); - // Expose controls state to parent - useEffect(() => { - onControlsStateChange?.({ - playing, - currentTime, - duration, - volume, - muted, - loop, - zoom, - subtitlesEnabled, - showSubtitleSettings, - seeking, - timelineHover, - }); - }, [ - playing, - currentTime, - duration, - volume, - muted, - loop, - zoom, - subtitlesEnabled, - showSubtitleSettings, - seeking, - timelineHover, - onControlsStateChange, - ]); + // Expose controls state to parent + useEffect(() => { + onControlsStateChange?.({ + playing, + currentTime, + duration, + volume, + muted, + loop, + zoom, + subtitlesEnabled, + showSubtitleSettings, + seeking, + timelineHover, + }); + }, [ + playing, + currentTime, + duration, + volume, + muted, + loop, + zoom, + subtitlesEnabled, + showSubtitleSettings, + seeking, + timelineHover, + onControlsStateChange, + ]); - // Expose showControls state to parent - useEffect(() => { - onShowControlsChange?.(showControls); - }, [showControls, onShowControlsChange]); + // Expose showControls state to parent + useEffect(() => { + onShowControlsChange?.(showControls); + }, [showControls, onShowControlsChange]); - // Notify parent of zoom state changes - useEffect(() => { - onZoomChange?.(isZoomed); - }, [isZoomed, onZoomChange]); + // Notify parent of zoom state changes + useEffect(() => { + onZoomChange?.(isZoomed); + }, [isZoomed, onZoomChange]); - const togglePlay = useCallback(() => { - if (!videoRef.current) return; - if (playing) { - videoRef.current.pause(); - } else { - videoRef.current.play(); - } - }, [playing]); + const togglePlay = useCallback(() => { + if (!videoRef.current) return; + if (playing) { + videoRef.current.pause(); + } else { + videoRef.current.play(); + } + }, [playing]); - const handleSeek = useCallback( - (e: React.MouseEvent) => { - if (!videoRef.current) return; - const rect = e.currentTarget.getBoundingClientRect(); - const percent = (e.clientX - rect.left) / rect.width; - videoRef.current.currentTime = percent * duration; - }, - [duration], - ); + const handleSeek = useCallback( + (e: React.MouseEvent) => { + if (!videoRef.current) return; + const rect = e.currentTarget.getBoundingClientRect(); + const percent = (e.clientX - rect.left) / rect.width; + videoRef.current.currentTime = percent * duration; + }, + [duration] + ); - const handleTimelineHover = useCallback( - (e: React.MouseEvent) => { - const rect = e.currentTarget.getBoundingClientRect(); - const percent = (e.clientX - rect.left) / rect.width; - setTimelineHover({ percent, mouseX: e.clientX }); - }, - [], - ); + const handleTimelineHover = useCallback( + (e: React.MouseEvent) => { + const rect = e.currentTarget.getBoundingClientRect(); + const percent = (e.clientX - rect.left) / rect.width; + setTimelineHover({ percent, mouseX: e.clientX }); + }, + [] + ); - const toggleFullscreen = useCallback(() => { - if (!containerRef.current) return; - if (document.fullscreenElement) { - document.exitFullscreen(); - } else { - containerRef.current.requestFullscreen(); - } - }, []); + const toggleFullscreen = useCallback(() => { + if (!containerRef.current) return; + if (document.fullscreenElement) { + document.exitFullscreen(); + } else { + containerRef.current.requestFullscreen(); + } + }, []); - const handleTimelineLeave = useCallback(() => { - setSeeking(false); - setTimelineHover(null); - }, []); + const handleTimelineLeave = useCallback(() => { + setSeeking(false); + setTimelineHover(null); + }, []); - const handleSeekingStart = useCallback(() => setSeeking(true), []); - const handleSeekingEnd = useCallback(() => setSeeking(false), []); - const handleMuteToggle = useCallback(() => setMuted((m) => !m), []); - const handleLoopToggle = useCallback(() => setLoop((l) => !l), []); - const handleSubtitlesToggle = useCallback( - () => setSubtitlesEnabled((s) => !s), - [], - ); - const handleSubtitleSettingsToggle = useCallback( - () => setShowSubtitleSettings((s) => !s), - [], - ); + const handleSeekingStart = useCallback(() => setSeeking(true), []); + const handleSeekingEnd = useCallback(() => setSeeking(false), []); + const handleMuteToggle = useCallback(() => setMuted((m) => !m), []); + const handleLoopToggle = useCallback(() => setLoop((l) => !l), []); + const handleSubtitlesToggle = useCallback( + () => setSubtitlesEnabled((s) => !s), + [] + ); + const handleSubtitleSettingsToggle = useCallback( + () => setShowSubtitleSettings((s) => !s), + [] + ); - // Show controls on mouse move, hide after 1s of inactivity - const handleMouseMove = useCallback(() => { - setShowControls(true); - if (hideControlsTimeout.current) { - clearTimeout(hideControlsTimeout.current); - } - if (playing) { - hideControlsTimeout.current = setTimeout(() => { - setShowControls(false); - }, 1000); - } - }, [playing]); + // Show controls on mouse move, hide after 1s of inactivity + const handleMouseMove = useCallback(() => { + setShowControls(true); + if (hideControlsTimeout.current) { + clearTimeout(hideControlsTimeout.current); + } + if (playing) { + hideControlsTimeout.current = setTimeout(() => { + setShowControls(false); + }, 1000); + } + }, [playing]); - // Provide callbacks to parent - useEffect(() => { - getCallbacks?.({ - onTogglePlay: togglePlay, - onSeek: handleSeek, - onTimelineHover: handleTimelineHover, - onTimelineLeave: handleTimelineLeave, - onSeekingStart: handleSeekingStart, - onSeekingEnd: handleSeekingEnd, - onVolumeChange: setVolume, - onMuteToggle: handleMuteToggle, - onLoopToggle: handleLoopToggle, - onZoomIn: zoomIn, - onZoomOut: zoomOut, - onZoomReset: reset, - onSubtitlesToggle: handleSubtitlesToggle, - onSubtitleSettingsToggle: handleSubtitleSettingsToggle, - onFullscreenToggle: toggleFullscreen, - onMouseMove: handleMouseMove, - }); - }, [ - togglePlay, - handleSeek, - handleTimelineHover, - handleTimelineLeave, - handleSeekingStart, - handleSeekingEnd, - handleMuteToggle, - handleLoopToggle, - handleSubtitlesToggle, - handleSubtitleSettingsToggle, - toggleFullscreen, - handleMouseMove, - zoomIn, - zoomOut, - reset, - getCallbacks, - ]); + // Provide callbacks to parent + useEffect(() => { + getCallbacks?.({ + onTogglePlay: togglePlay, + onSeek: handleSeek, + onTimelineHover: handleTimelineHover, + onTimelineLeave: handleTimelineLeave, + onSeekingStart: handleSeekingStart, + onSeekingEnd: handleSeekingEnd, + onVolumeChange: setVolume, + onMuteToggle: handleMuteToggle, + onLoopToggle: handleLoopToggle, + onZoomIn: zoomIn, + onZoomOut: zoomOut, + onZoomReset: reset, + onSubtitlesToggle: handleSubtitlesToggle, + onSubtitleSettingsToggle: handleSubtitleSettingsToggle, + onFullscreenToggle: toggleFullscreen, + onMouseMove: handleMouseMove, + }); + }, [ + togglePlay, + handleSeek, + handleTimelineHover, + handleTimelineLeave, + handleSeekingStart, + handleSeekingEnd, + handleMuteToggle, + handleLoopToggle, + handleSubtitlesToggle, + handleSubtitleSettingsToggle, + toggleFullscreen, + handleMouseMove, + zoomIn, + zoomOut, + reset, + getCallbacks, + ]); - // Keyboard shortcuts - useEffect(() => { - const handleKeyDown = (e: KeyboardEvent) => { - if (!videoRef.current) return; + // Keyboard shortcuts + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (!videoRef.current) return; - switch (e.code) { - case "Space": - e.preventDefault(); - togglePlay(); - break; - case "ArrowLeft": - e.preventDefault(); - videoRef.current.currentTime = Math.max( - 0, - videoRef.current.currentTime - 5, - ); - break; - case "ArrowRight": - e.preventDefault(); - videoRef.current.currentTime = Math.min( - duration, - videoRef.current.currentTime + 5, - ); - break; - case "ArrowUp": - e.preventDefault(); - setVolume((v) => Math.min(1, v + 0.1)); - break; - case "ArrowDown": - e.preventDefault(); - setVolume((v) => Math.max(0, v - 0.1)); - break; - case "KeyM": - e.preventDefault(); - handleMuteToggle(); - break; - case "KeyF": - e.preventDefault(); - toggleFullscreen(); - break; - case "KeyC": - e.preventDefault(); - handleSubtitlesToggle(); - break; - case "KeyL": - e.preventDefault(); - handleLoopToggle(); - break; - } - }; + switch (e.code) { + case "Space": + e.preventDefault(); + togglePlay(); + break; + case "ArrowLeft": + e.preventDefault(); + videoRef.current.currentTime = Math.max( + 0, + videoRef.current.currentTime - 5 + ); + break; + case "ArrowRight": + e.preventDefault(); + videoRef.current.currentTime = Math.min( + duration, + videoRef.current.currentTime + 5 + ); + break; + case "ArrowUp": + e.preventDefault(); + setVolume((v) => Math.min(1, v + 0.1)); + break; + case "ArrowDown": + e.preventDefault(); + setVolume((v) => Math.max(0, v - 0.1)); + break; + case "KeyM": + e.preventDefault(); + handleMuteToggle(); + break; + case "KeyF": + e.preventDefault(); + toggleFullscreen(); + break; + case "KeyC": + e.preventDefault(); + handleSubtitlesToggle(); + break; + case "KeyL": + e.preventDefault(); + handleLoopToggle(); + break; + } + }; - window.addEventListener("keydown", handleKeyDown); - return () => window.removeEventListener("keydown", handleKeyDown); - }, [ - duration, - togglePlay, - toggleFullscreen, - handleMuteToggle, - handleSubtitlesToggle, - handleLoopToggle, - ]); + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, [ + duration, + togglePlay, + toggleFullscreen, + handleMuteToggle, + handleSubtitlesToggle, + handleLoopToggle, + ]); - // Sync video element state and persist to localStorage - useEffect(() => { - if (!videoRef.current) return; - videoRef.current.volume = volume; - localStorage.setItem("sd-video-volume", volume.toString()); - }, [volume]); + // Sync video element state and persist to localStorage + useEffect(() => { + if (!videoRef.current) return; + videoRef.current.volume = volume; + localStorage.setItem("sd-video-volume", volume.toString()); + }, [volume]); - useEffect(() => { - if (!videoRef.current) return; - videoRef.current.muted = muted; - localStorage.setItem("sd-video-muted", muted.toString()); - }, [muted]); + useEffect(() => { + if (!videoRef.current) return; + videoRef.current.muted = muted; + localStorage.setItem("sd-video-muted", muted.toString()); + }, [muted]); - useEffect(() => { - if (!videoRef.current) return; - videoRef.current.loop = loop; - }, [loop]); + useEffect(() => { + if (!videoRef.current) return; + videoRef.current.loop = loop; + }, [loop]); - return ( -
    - {/* Zoom level indicator */} - {zoom > 1 && ( -
    - {Math.round(zoom * 100)}% -
    - )} + return ( +
    + {/* Zoom level indicator */} + {zoom > 1 && ( +
    + {Math.round(zoom * 100)}% +
    + )} - {/* Video container with zoom/pan */} -
    -
    -
    -
    + {/* Video container with zoom/pan */} +
    +
    +
    +
    - {/* Subtitles */} - {subtitlesEnabled && ( -
    - -
    - )} + {/* Subtitles */} + {subtitlesEnabled && ( +
    + +
    + )} - {/* Subtitle Settings Menu */} - setShowSubtitleSettings(false)} - /> -
    - ); + {/* Subtitle Settings Menu */} + setShowSubtitleSettings(false)} + onSettingsChange={setSubtitleSettings} + settings={subtitleSettings} + /> +
    + ); } diff --git a/packages/interface/src/components/QuickPreview/index.ts b/packages/interface/src/components/QuickPreview/index.ts index 5dbb5af6f..6427d425c 100644 --- a/packages/interface/src/components/QuickPreview/index.ts +++ b/packages/interface/src/components/QuickPreview/index.ts @@ -1,8 +1,11 @@ -export { QuickPreview } from './QuickPreview'; -export { QuickPreviewModal } from './QuickPreviewModal'; -export { QuickPreviewOverlay } from './QuickPreviewOverlay'; -export { QuickPreviewFullscreen, PREVIEW_LAYER_ID } from './QuickPreviewFullscreen'; -export { QuickPreviewController } from './Controller'; -export { QuickPreviewSyncer } from './Syncer'; -export { TextViewer } from './TextViewer'; -export { WithPrismTheme } from './prism'; \ No newline at end of file +export { QuickPreviewController } from "./Controller"; +export { WithPrismTheme } from "./prism"; +export { QuickPreview } from "./QuickPreview"; +export { + PREVIEW_LAYER_ID, + QuickPreviewFullscreen, +} from "./QuickPreviewFullscreen"; +export { QuickPreviewModal } from "./QuickPreviewModal"; +export { QuickPreviewOverlay } from "./QuickPreviewOverlay"; +export { QuickPreviewSyncer } from "./Syncer"; +export { TextViewer } from "./TextViewer"; diff --git a/packages/interface/src/components/QuickPreview/prism-lazy.ts b/packages/interface/src/components/QuickPreview/prism-lazy.ts index 4d157cf3f..422d8f9fe 100644 --- a/packages/interface/src/components/QuickPreview/prism-lazy.ts +++ b/packages/interface/src/components/QuickPreview/prism-lazy.ts @@ -9,53 +9,53 @@ window.Prism.manual = true; import "prismjs"; // Languages -import 'prismjs/components/prism-applescript.js'; -import 'prismjs/components/prism-bash.js'; -import 'prismjs/components/prism-c.js'; -import 'prismjs/components/prism-cpp.js'; -import 'prismjs/components/prism-ruby.js'; -import 'prismjs/components/prism-crystal.js'; -import 'prismjs/components/prism-csharp.js'; -import 'prismjs/components/prism-css-extras.js'; -import 'prismjs/components/prism-csv.js'; -import 'prismjs/components/prism-d.js'; -import 'prismjs/components/prism-dart.js'; -import 'prismjs/components/prism-docker.js'; -import 'prismjs/components/prism-go-module.js'; -import 'prismjs/components/prism-go.js'; -import 'prismjs/components/prism-haskell.js'; -import 'prismjs/components/prism-ini.js'; -import 'prismjs/components/prism-java.js'; -import 'prismjs/components/prism-js-extras.js'; -import 'prismjs/components/prism-json.js'; -import 'prismjs/components/prism-jsx.js'; -import 'prismjs/components/prism-kotlin.js'; -import 'prismjs/components/prism-less.js'; -import 'prismjs/components/prism-lua.js'; -import 'prismjs/components/prism-makefile.js'; -import 'prismjs/components/prism-markdown.js'; -import 'prismjs/components/prism-markup-templating.js'; -import 'prismjs/components/prism-nim.js'; -import 'prismjs/components/prism-objectivec.js'; -import 'prismjs/components/prism-ocaml.js'; -import 'prismjs/components/prism-perl.js'; -import 'prismjs/components/prism-php.js'; -import 'prismjs/components/prism-powershell.js'; -import 'prismjs/components/prism-python.js'; -import 'prismjs/components/prism-qml.js'; -import 'prismjs/components/prism-r.js'; -import 'prismjs/components/prism-rust.js'; -import 'prismjs/components/prism-sass.js'; -import 'prismjs/components/prism-scss.js'; -import 'prismjs/components/prism-solidity.js'; -import 'prismjs/components/prism-sql.js'; -import 'prismjs/components/prism-swift.js'; -import 'prismjs/components/prism-toml.js'; -import 'prismjs/components/prism-tsx.js'; -import 'prismjs/components/prism-typescript.js'; -import 'prismjs/components/prism-typoscript.js'; -import 'prismjs/components/prism-vala.js'; -import 'prismjs/components/prism-yaml.js'; -import 'prismjs/components/prism-zig.js'; +import "prismjs/components/prism-applescript.js"; +import "prismjs/components/prism-bash.js"; +import "prismjs/components/prism-c.js"; +import "prismjs/components/prism-cpp.js"; +import "prismjs/components/prism-ruby.js"; +import "prismjs/components/prism-crystal.js"; +import "prismjs/components/prism-csharp.js"; +import "prismjs/components/prism-css-extras.js"; +import "prismjs/components/prism-csv.js"; +import "prismjs/components/prism-d.js"; +import "prismjs/components/prism-dart.js"; +import "prismjs/components/prism-docker.js"; +import "prismjs/components/prism-go-module.js"; +import "prismjs/components/prism-go.js"; +import "prismjs/components/prism-haskell.js"; +import "prismjs/components/prism-ini.js"; +import "prismjs/components/prism-java.js"; +import "prismjs/components/prism-js-extras.js"; +import "prismjs/components/prism-json.js"; +import "prismjs/components/prism-jsx.js"; +import "prismjs/components/prism-kotlin.js"; +import "prismjs/components/prism-less.js"; +import "prismjs/components/prism-lua.js"; +import "prismjs/components/prism-makefile.js"; +import "prismjs/components/prism-markdown.js"; +import "prismjs/components/prism-markup-templating.js"; +import "prismjs/components/prism-nim.js"; +import "prismjs/components/prism-objectivec.js"; +import "prismjs/components/prism-ocaml.js"; +import "prismjs/components/prism-perl.js"; +import "prismjs/components/prism-php.js"; +import "prismjs/components/prism-powershell.js"; +import "prismjs/components/prism-python.js"; +import "prismjs/components/prism-qml.js"; +import "prismjs/components/prism-r.js"; +import "prismjs/components/prism-rust.js"; +import "prismjs/components/prism-sass.js"; +import "prismjs/components/prism-scss.js"; +import "prismjs/components/prism-solidity.js"; +import "prismjs/components/prism-sql.js"; +import "prismjs/components/prism-swift.js"; +import "prismjs/components/prism-toml.js"; +import "prismjs/components/prism-tsx.js"; +import "prismjs/components/prism-typescript.js"; +import "prismjs/components/prism-typoscript.js"; +import "prismjs/components/prism-vala.js"; +import "prismjs/components/prism-yaml.js"; +import "prismjs/components/prism-zig.js"; -export { highlightElement } from 'prismjs'; +export { highlightElement } from "prismjs"; diff --git a/packages/interface/src/components/QuickPreview/prism.tsx b/packages/interface/src/components/QuickPreview/prism.tsx index 4812918b8..aa9b71fac 100644 --- a/packages/interface/src/components/QuickPreview/prism.tsx +++ b/packages/interface/src/components/QuickPreview/prism.tsx @@ -1,47 +1,58 @@ -import { useEffect, useState } from 'react'; +import { useEffect, useState } from "react"; -// @ts-ignore - SCSS imports -import oneDarkCss from './one-dark.scss?url'; -// @ts-ignore - SCSS imports -import oneLightCss from './one-light.scss?url'; +// @ts-expect-error - SCSS imports +import oneDarkCss from "./one-dark.scss?url"; +// @ts-expect-error - SCSS imports +import oneLightCss from "./one-light.scss?url"; export const languageMapping = Object.entries({ - applescript: ['scpt', 'scptd'], - sh: ['zsh', 'fish'], - c: ['h'], - cpp: ['hpp'], - js: ['mjs'], - crystal: ['cr'], - cs: ['csx'], - makefile: ['make'], - nim: ['nims'], - objc: ['m', 'mm'], - ocaml: ['ml', 'mli', 'mll', 'mly'], - perl: ['pl'], - php: ['php', 'php1', 'php2', 'php3', 'php4', 'php5', 'php6', 'phps', 'phpt', 'phtml'], - powershell: ['ps1', 'psd1', 'psm1'], - rust: ['rs'] + applescript: ["scpt", "scptd"], + sh: ["zsh", "fish"], + c: ["h"], + cpp: ["hpp"], + js: ["mjs"], + crystal: ["cr"], + cs: ["csx"], + makefile: ["make"], + nim: ["nims"], + objc: ["m", "mm"], + ocaml: ["ml", "mli", "mll", "mly"], + perl: ["pl"], + php: [ + "php", + "php1", + "php2", + "php3", + "php4", + "php5", + "php6", + "phps", + "phpt", + "phtml", + ], + powershell: ["ps1", "psd1", "psm1"], + rust: ["rs"], }).reduce>((mapping, [id, exts]) => { - for (const ext of exts) mapping.set(ext, id); - return mapping; + for (const ext of exts) mapping.set(ext, id); + return mapping; }, new Map()); export function WithPrismTheme() { - const [isDark, setIsDark] = useState(() => - window.matchMedia('(prefers-color-scheme: dark)').matches - ); + const [isDark, setIsDark] = useState( + () => window.matchMedia("(prefers-color-scheme: dark)").matches + ); - useEffect(() => { - const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); - const handleChange = (e: MediaQueryListEvent) => setIsDark(e.matches); + useEffect(() => { + const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)"); + const handleChange = (e: MediaQueryListEvent) => setIsDark(e.matches); - mediaQuery.addEventListener('change', handleChange); - return () => mediaQuery.removeEventListener('change', handleChange); - }, []); + mediaQuery.addEventListener("change", handleChange); + return () => mediaQuery.removeEventListener("change", handleChange); + }, []); - return isDark ? ( - - ) : ( - - ); -} \ No newline at end of file + return isDark ? ( + + ) : ( + + ); +} diff --git a/packages/interface/src/components/QuickPreview/useZoomPan.ts b/packages/interface/src/components/QuickPreview/useZoomPan.ts index ef1685d31..1de456d08 100644 --- a/packages/interface/src/components/QuickPreview/useZoomPan.ts +++ b/packages/interface/src/components/QuickPreview/useZoomPan.ts @@ -1,169 +1,158 @@ -import { useState, useCallback, useEffect, RefObject } from "react"; +import { type RefObject, useCallback, useEffect, useState } from "react"; interface UseZoomPanOptions { - minZoom?: number; - maxZoom?: number; - zoomStep?: number; + minZoom?: number; + maxZoom?: number; + zoomStep?: number; } export function useZoomPan( - containerRef: RefObject, - options: UseZoomPanOptions = {}, + containerRef: RefObject, + options: UseZoomPanOptions = {} ) { - const { minZoom = 1, maxZoom = 5, zoomStep = 0.1 } = options; + const { minZoom = 1, maxZoom = 5, zoomStep = 0.1 } = options; - const [zoom, setZoom] = useState(1); - const [pan, setPan] = useState({ x: 0, y: 0 }); - const [isDragging, setIsDragging] = useState(false); - const [dragStart, setDragStart] = useState({ x: 0, y: 0 }); + const [zoom, setZoom] = useState(1); + const [pan, setPan] = useState({ x: 0, y: 0 }); + const [isDragging, setIsDragging] = useState(false); + const [dragStart, setDragStart] = useState({ x: 0, y: 0 }); - // Reset zoom and pan - const reset = useCallback(() => { - setZoom(1); - setPan({ x: 0, y: 0 }); - }, []); + // Reset zoom and pan + const reset = useCallback(() => { + setZoom(1); + setPan({ x: 0, y: 0 }); + }, []); - // Zoom in/out - const zoomIn = useCallback(() => { - setZoom((z) => Math.min(maxZoom, z + zoomStep)); - }, [maxZoom, zoomStep]); + // Zoom in/out + const zoomIn = useCallback(() => { + setZoom((z) => Math.min(maxZoom, z + zoomStep)); + }, [maxZoom, zoomStep]); - const zoomOut = useCallback(() => { - setZoom((z) => { - const newZoom = Math.max(minZoom, z - zoomStep); - // Reset pan when zooming back to 1x - if (newZoom === 1) { - setPan({ x: 0, y: 0 }); - } - return newZoom; - }); - }, [minZoom, zoomStep]); + const zoomOut = useCallback(() => { + setZoom((z) => { + const newZoom = Math.max(minZoom, z - zoomStep); + // Reset pan when zooming back to 1x + if (newZoom === 1) { + setPan({ x: 0, y: 0 }); + } + return newZoom; + }); + }, [minZoom, zoomStep]); - // Mouse wheel zoom - useEffect(() => { - const container = containerRef.current; - if (!container) return; + // Mouse wheel zoom + useEffect(() => { + const container = containerRef.current; + if (!container) return; - const handleWheel = (e: WheelEvent) => { - // Only zoom if not scrolling controls or other UI - if ( - (e.target as HTMLElement).closest( - 'input, button, [role="slider"]', - ) - ) { - return; - } + const handleWheel = (e: WheelEvent) => { + // Only zoom if not scrolling controls or other UI + if ((e.target as HTMLElement).closest('input, button, [role="slider"]')) { + return; + } - e.preventDefault(); + e.preventDefault(); - // Scale the wheel delta proportionally (typical deltaY is ~100 per notch) - // Divide by 500 for responsive zoom: 100 deltaY = 0.2 zoom change - const zoomChange = -e.deltaY / 500; + // Scale the wheel delta proportionally (typical deltaY is ~100 per notch) + // Divide by 500 for responsive zoom: 100 deltaY = 0.2 zoom change + const zoomChange = -e.deltaY / 500; - setZoom((z) => { - const newZoom = Math.max( - minZoom, - Math.min(maxZoom, z + zoomChange), - ); - // Reset pan when zooming back to 1x - if (newZoom === 1) { - setPan({ x: 0, y: 0 }); - } - return newZoom; - }); - }; + setZoom((z) => { + const newZoom = Math.max(minZoom, Math.min(maxZoom, z + zoomChange)); + // Reset pan when zooming back to 1x + if (newZoom === 1) { + setPan({ x: 0, y: 0 }); + } + return newZoom; + }); + }; - container.addEventListener("wheel", handleWheel, { passive: false }); - return () => container.removeEventListener("wheel", handleWheel); - }, [containerRef, minZoom, maxZoom]); + container.addEventListener("wheel", handleWheel, { passive: false }); + return () => container.removeEventListener("wheel", handleWheel); + }, [containerRef, minZoom, maxZoom]); - // Pan with mouse drag (only when zoomed in) - useEffect(() => { - const container = containerRef.current; - if (!container || zoom <= 1) return; + // Pan with mouse drag (only when zoomed in) + useEffect(() => { + const container = containerRef.current; + if (!container || zoom <= 1) return; - const handleMouseDown = (e: MouseEvent) => { - // Don't pan if clicking on controls - if ( - (e.target as HTMLElement).closest( - 'button, input, [role="slider"]', - ) - ) { - return; - } + const handleMouseDown = (e: MouseEvent) => { + // Don't pan if clicking on controls + if ((e.target as HTMLElement).closest('button, input, [role="slider"]')) { + return; + } - setIsDragging(true); - setDragStart({ x: e.clientX - pan.x, y: e.clientY - pan.y }); - container.style.cursor = "grabbing"; - }; + setIsDragging(true); + setDragStart({ x: e.clientX - pan.x, y: e.clientY - pan.y }); + container.style.cursor = "grabbing"; + }; - const handleMouseMove = (e: MouseEvent) => { - if (!isDragging) return; + const handleMouseMove = (e: MouseEvent) => { + if (!isDragging) return; - setPan({ - x: e.clientX - dragStart.x, - y: e.clientY - dragStart.y, - }); - }; + setPan({ + x: e.clientX - dragStart.x, + y: e.clientY - dragStart.y, + }); + }; - const handleMouseUp = () => { - setIsDragging(false); - if (zoom > 1) { - container.style.cursor = "grab"; - } else { - container.style.cursor = "default"; - } - }; + const handleMouseUp = () => { + setIsDragging(false); + if (zoom > 1) { + container.style.cursor = "grab"; + } else { + container.style.cursor = "default"; + } + }; - container.addEventListener("mousedown", handleMouseDown); - window.addEventListener("mousemove", handleMouseMove); - window.addEventListener("mouseup", handleMouseUp); + container.addEventListener("mousedown", handleMouseDown); + window.addEventListener("mousemove", handleMouseMove); + window.addEventListener("mouseup", handleMouseUp); - // Set cursor - container.style.cursor = zoom > 1 ? "grab" : "default"; + // Set cursor + container.style.cursor = zoom > 1 ? "grab" : "default"; - return () => { - container.removeEventListener("mousedown", handleMouseDown); - window.removeEventListener("mousemove", handleMouseMove); - window.removeEventListener("mouseup", handleMouseUp); - container.style.cursor = "default"; - }; - }, [containerRef, zoom, pan, isDragging, dragStart]); + return () => { + container.removeEventListener("mousedown", handleMouseDown); + window.removeEventListener("mousemove", handleMouseMove); + window.removeEventListener("mouseup", handleMouseUp); + container.style.cursor = "default"; + }; + }, [containerRef, zoom, pan, isDragging, dragStart]); - // Keyboard shortcuts - useEffect(() => { - const handleKeyDown = (e: KeyboardEvent) => { - // Don't interfere with inputs - if ((e.target as HTMLElement).tagName === "INPUT") { - return; - } + // Keyboard shortcuts + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + // Don't interfere with inputs + if ((e.target as HTMLElement).tagName === "INPUT") { + return; + } - if (e.key === "=" || e.key === "+") { - e.preventDefault(); - zoomIn(); - } else if (e.key === "-" || e.key === "_") { - e.preventDefault(); - zoomOut(); - } else if (e.key === "0") { - e.preventDefault(); - reset(); - } - }; + if (e.key === "=" || e.key === "+") { + e.preventDefault(); + zoomIn(); + } else if (e.key === "-" || e.key === "_") { + e.preventDefault(); + zoomOut(); + } else if (e.key === "0") { + e.preventDefault(); + reset(); + } + }; - window.addEventListener("keydown", handleKeyDown); - return () => window.removeEventListener("keydown", handleKeyDown); - }, [zoomIn, zoomOut, reset]); + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, [zoomIn, zoomOut, reset]); - return { - zoom, - pan, - zoomIn, - zoomOut, - reset, - isZoomed: zoom > 1, - transform: { - transform: `translate(${pan.x}px, ${pan.y}px) scale(${zoom})`, - transition: isDragging ? "none" : "transform 0.05s ease-out", - }, - }; + return { + zoom, + pan, + zoomIn, + zoomOut, + reset, + isZoomed: zoom > 1, + transform: { + transform: `translate(${pan.x}px, ${pan.y}px) scale(${zoom})`, + transition: isDragging ? "none" : "transform 0.05s ease-out", + }, + }; } diff --git a/packages/interface/src/components/SpacesSidebar/AddGroupButton.tsx b/packages/interface/src/components/SpacesSidebar/AddGroupButton.tsx index 7035dbbf6..ac50a2606 100644 --- a/packages/interface/src/components/SpacesSidebar/AddGroupButton.tsx +++ b/packages/interface/src/components/SpacesSidebar/AddGroupButton.tsx @@ -1,20 +1,20 @@ -import { Plus } from '@phosphor-icons/react'; -import { useAddGroupDialog } from './AddGroupModal'; +import { Plus } from "@phosphor-icons/react"; +import { useAddGroupDialog } from "./AddGroupModal"; interface AddGroupButtonProps { - spaceId: string; + spaceId: string; } export function AddGroupButton({ spaceId }: AddGroupButtonProps) { - const addGroupDialog = useAddGroupDialog; + const addGroupDialog = useAddGroupDialog; - return ( - - ); + return ( + + ); } diff --git a/packages/interface/src/components/SpacesSidebar/AddGroupModal.tsx b/packages/interface/src/components/SpacesSidebar/AddGroupModal.tsx index 31fb7da00..a8500473e 100644 --- a/packages/interface/src/components/SpacesSidebar/AddGroupModal.tsx +++ b/packages/interface/src/components/SpacesSidebar/AddGroupModal.tsx @@ -1,77 +1,84 @@ -import { useState } from 'react'; -import { Input, Label, dialogManager, useDialog, Dialog } from '@sd/ui'; -import { useLibraryMutation } from '@sd/ts-client'; -import { useForm } from 'react-hook-form'; -import type { GroupType } from '@sd/ts-client'; +import type { GroupType } from "@sd/ts-client"; +import { useLibraryMutation } from "@sd/ts-client"; +import { Dialog, dialogManager, Input, Label, useDialog } from "@sd/ui"; +import { useState } from "react"; +import { useForm } from "react-hook-form"; interface FormData { - groupName: string; + groupName: string; } export function useAddGroupDialog(spaceId: string) { - return dialogManager.create((props) => ); + return dialogManager.create((props) => ( + + )); } function AddGroupDialog(props: { id: number; spaceId: string }) { - const dialog = useDialog(props); - const [groupType, setGroupType] = useState('Custom'); + const dialog = useDialog(props); + const [groupType, setGroupType] = useState("Custom"); - const form = useForm({ - defaultValues: { groupName: '' }, - }); + const form = useForm({ + defaultValues: { groupName: "" }, + }); - const addGroup = useLibraryMutation('spaces.add_group'); + const addGroup = useLibraryMutation("spaces.add_group"); - const onSubmit = form.handleSubmit(async (data) => { - await addGroup.mutateAsync({ - space_id: props.spaceId, - name: data.groupName || getDefaultName(groupType), - group_type: groupType, - }); - form.reset(); - setGroupType('Custom'); - dialog.state.open = false; - }); + const onSubmit = form.handleSubmit(async (data) => { + await addGroup.mutateAsync({ + space_id: props.spaceId, + name: data.groupName || getDefaultName(groupType), + group_type: groupType, + }); + form.reset(); + setGroupType("Custom"); + dialog.state.open = false; + }); - return ( - -
    -
    - - -
    + return ( + +
    +
    + + +
    - {groupType === 'Custom' && ( -
    - - -
    - )} -
    -
    - ); + {groupType === "Custom" && ( +
    + + +
    + )} +
    +
    + ); } function getDefaultName(groupType: GroupType): string { - if (groupType === 'Devices') return 'Devices'; - if (groupType === 'Locations') return 'Locations'; - if (groupType === 'Tags') return 'Tags'; - if (groupType === 'Cloud') return 'Cloud'; - if (groupType === 'Custom') return 'Custom Group'; - if (typeof groupType === 'object' && 'Device' in groupType) return 'Device'; - return 'Group'; + if (groupType === "Devices") return "Devices"; + if (groupType === "Locations") return "Locations"; + if (groupType === "Tags") return "Tags"; + if (groupType === "Cloud") return "Cloud"; + if (groupType === "Custom") return "Custom Group"; + if (typeof groupType === "object" && "Device" in groupType) return "Device"; + return "Group"; } - diff --git a/packages/interface/src/components/SpacesSidebar/CreateSpaceModal.tsx b/packages/interface/src/components/SpacesSidebar/CreateSpaceModal.tsx index 20a3c5a74..f1e32a66c 100644 --- a/packages/interface/src/components/SpacesSidebar/CreateSpaceModal.tsx +++ b/packages/interface/src/components/SpacesSidebar/CreateSpaceModal.tsx @@ -1,123 +1,123 @@ -import { useState } from 'react'; -import clsx from 'clsx'; -import { Input, Label, dialogManager, useDialog, Dialog } from '@sd/ui'; -import { useLibraryMutation } from '@sd/ts-client'; -import { useForm } from 'react-hook-form'; +import { useLibraryMutation } from "@sd/ts-client"; +import { Dialog, dialogManager, Input, Label, useDialog } from "@sd/ui"; +import clsx from "clsx"; +import { useState } from "react"; +import { useForm } from "react-hook-form"; interface FormData { - name: string; + name: string; } const PRESET_COLORS = [ - '#3B82F6', // Blue - '#8B5CF6', // Purple - '#EC4899', // Pink - '#10B981', // Green - '#F59E0B', // Amber - '#EF4444', // Red - '#06B6D4', // Cyan - '#6366F1', // Indigo + "#3B82F6", // Blue + "#8B5CF6", // Purple + "#EC4899", // Pink + "#10B981", // Green + "#F59E0B", // Amber + "#EF4444", // Red + "#06B6D4", // Cyan + "#6366F1", // Indigo ]; const PRESET_ICONS = [ - 'Planet', - 'Folder', - 'Briefcase', - 'House', - 'Camera', - 'MusicNotes', - 'GameController', - 'Code', + "Planet", + "Folder", + "Briefcase", + "House", + "Camera", + "MusicNotes", + "GameController", + "Code", ]; export function useCreateSpaceDialog() { - return dialogManager.create((props) => ); + return dialogManager.create((props) => ); } function CreateSpaceDialog(props: { id: number }) { - const dialog = useDialog(props); - const [selectedColor, setSelectedColor] = useState(PRESET_COLORS[0]); - const [selectedIcon, setSelectedIcon] = useState(PRESET_ICONS[0]); + const dialog = useDialog(props); + const [selectedColor, setSelectedColor] = useState(PRESET_COLORS[0]); + const [selectedIcon, setSelectedIcon] = useState(PRESET_ICONS[0]); - const form = useForm({ - defaultValues: { name: '' }, - }); + const form = useForm({ + defaultValues: { name: "" }, + }); - const createSpace = useLibraryMutation('spaces.create'); + const createSpace = useLibraryMutation("spaces.create"); - const onSubmit = form.handleSubmit(async (data) => { - if (!data.name?.trim()) return; + const onSubmit = form.handleSubmit(async (data) => { + if (!data.name?.trim()) return; - await createSpace.mutateAsync({ - name: data.name, - icon: selectedIcon, - color: selectedColor, - }); - form.reset(); - setSelectedColor(PRESET_COLORS[0]); - setSelectedIcon(PRESET_ICONS[0]); - dialog.state.open = false; - }); + await createSpace.mutateAsync({ + name: data.name, + icon: selectedIcon, + color: selectedColor, + }); + form.reset(); + setSelectedColor(PRESET_COLORS[0]); + setSelectedIcon(PRESET_ICONS[0]); + dialog.state.open = false; + }); - return ( - -
    -
    - - -
    + return ( + +
    +
    + + +
    -
    - -
    - {PRESET_COLORS.map((color) => ( -
    -
    +
    + +
    + {PRESET_COLORS.map((color) => ( +
    +
    -
    - -
    - {PRESET_ICONS.map((icon) => ( - - ))} -
    -
    -
    -
    - ); +
    + +
    + {PRESET_ICONS.map((icon) => ( + + ))} +
    +
    +
    +
    + ); } diff --git a/packages/interface/src/components/SpacesSidebar/DevicesGroup.tsx b/packages/interface/src/components/SpacesSidebar/DevicesGroup.tsx index 36625470d..b66f1969a 100644 --- a/packages/interface/src/components/SpacesSidebar/DevicesGroup.tsx +++ b/packages/interface/src/components/SpacesSidebar/DevicesGroup.tsx @@ -1,157 +1,160 @@ -import { WifiHigh, WifiNoneIcon, WifiSlashIcon, Trash } from "@phosphor-icons/react"; -import { useNormalizedQuery, getDeviceIcon, useCoreMutation } from "../../contexts/SpacedriveContext"; +import { Trash, WifiHigh, WifiSlashIcon } from "@phosphor-icons/react"; +import type { Device, ListLibraryDevicesInput } from "@sd/ts-client"; +import { + getDeviceIcon, + useCoreMutation, + useNormalizedQuery, +} from "../../contexts/SpacedriveContext"; import { useExplorer } from "../../routes/explorer/context"; -import { SpaceItem } from "./SpaceItem"; import { GroupHeader } from "./GroupHeader"; -import type { ListLibraryDevicesInput, Device } from "@sd/ts-client"; +import { SpaceItem } from "./SpaceItem"; interface DevicesGroupProps { - isCollapsed: boolean; - onToggle: () => void; - sortableAttributes?: any; - sortableListeners?: any; + isCollapsed: boolean; + onToggle: () => void; + sortableAttributes?: any; + sortableListeners?: any; } export function DevicesGroup({ - isCollapsed, - onToggle, - sortableAttributes, - sortableListeners, + isCollapsed, + onToggle, + sortableAttributes, + sortableListeners, }: DevicesGroupProps) { - const { navigateToView, loadPreferencesForSpaceItem } = useExplorer(); + const { navigateToView, loadPreferencesForSpaceItem } = useExplorer(); - // Use normalized query for automatic updates when device events are emitted - const { data: devices, isLoading } = useNormalizedQuery< - ListLibraryDevicesInput, - Device[] - >({ - wireMethod: "query:devices.list", - input: { - include_offline: true, - include_details: false, - show_paired: true, - }, - resourceType: "device", - }); + // Use normalized query for automatic updates when device events are emitted + const { data: devices, isLoading } = useNormalizedQuery< + ListLibraryDevicesInput, + Device[] + >({ + wireMethod: "query:devices.list", + input: { + include_offline: true, + include_details: false, + show_paired: true, + }, + resourceType: "device", + }); - // Mutation for unpairing devices - const revokeDevice = useCoreMutation("network.device.revoke"); + // Mutation for unpairing devices + const revokeDevice = useCoreMutation("network.device.revoke"); - // Handler for device context menu - const handleDeviceContextMenu = (device: Device) => async (e: React.MouseEvent) => { - e.preventDefault(); - e.stopPropagation(); + // Handler for device context menu + const handleDeviceContextMenu = + (device: Device) => async (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); - // Only show context menu for non-current devices - if (device.is_current) return; + // Only show context menu for non-current devices + if (device.is_current) return; - // Create context menu items for this device - const items = [ - { - icon: Trash, - label: "Unpair Device", - onClick: async () => { - await revokeDevice.mutateAsync({ - device_id: device.id, - remove_from_library: false, // Keep device in library - }); - }, - variant: "default" as const, - }, - { - icon: Trash, - label: "Remove Device Completely", - onClick: async () => { - await revokeDevice.mutateAsync({ - device_id: device.id, - remove_from_library: true, // Remove from library too - }); - }, - variant: "danger" as const, - }, - ]; + // Create context menu items for this device + const items = [ + { + icon: Trash, + label: "Unpair Device", + onClick: async () => { + await revokeDevice.mutateAsync({ + device_id: device.id, + remove_from_library: false, // Keep device in library + }); + }, + variant: "default" as const, + }, + { + icon: Trash, + label: "Remove Device Completely", + onClick: async () => { + await revokeDevice.mutateAsync({ + device_id: device.id, + remove_from_library: true, // Remove from library too + }); + }, + variant: "danger" as const, + }, + ]; - // Show platform-appropriate context menu - if (window.__SPACEDRIVE__?.showContextMenu) { - // Tauri native menu - await window.__SPACEDRIVE__.showContextMenu(items, { - x: e.clientX, - y: e.clientY, - }); - } - // For web, we'd need to implement a Radix-based context menu - // but for now, just call the action directly or show an alert - }; + // Show platform-appropriate context menu + if (window.__SPACEDRIVE__?.showContextMenu) { + // Tauri native menu + await window.__SPACEDRIVE__.showContextMenu(items, { + x: e.clientX, + y: e.clientY, + }); + } + // For web, we'd need to implement a Radix-based context menu + // but for now, just call the action directly or show an alert + }; - return ( -
    - + return ( +
    + - {/* Items */} - {!isCollapsed && ( -
    - {isLoading ? ( -
    - Loading... -
    - ) : !devices || devices.length === 0 ? ( -
    - No devices -
    - ) : ( - devices.map((device, index) => { - // Create a minimal SpaceItem structure for the device - const deviceItem = { - id: device.id, - item_type: "Overview" as const, - }; + {/* Items */} + {!isCollapsed && ( +
    + {isLoading ? ( +
    + Loading... +
    + ) : !devices || devices.length === 0 ? ( +
    + No devices +
    + ) : ( + devices.map((device, index) => { + // Create a minimal SpaceItem structure for the device + const deviceItem = { + id: device.id, + item_type: "Overview" as const, + }; - return ( - { - loadPreferencesForSpaceItem(`device:${device.id}`); - navigateToView("device", device.id); - }} - onContextMenu={handleDeviceContextMenu(device)} - allowInsertion={false} - isLastItem={index === devices.length - 1} - className="text-sidebar-inkDull" - rightComponent={ -
    - {!device.is_current && - !device.is_connected && ( - - )} - {!device.is_current && - device.is_connected && ( - - )} -
    - } - /> - ); - }) - )} -
    - )} -
    - ); -} \ No newline at end of file + return ( + { + loadPreferencesForSpaceItem(`device:${device.id}`); + navigateToView("device", device.id); + }} + onContextMenu={handleDeviceContextMenu(device)} + rightComponent={ +
    + {!(device.is_current || device.is_connected) && ( + + )} + {!device.is_current && device.is_connected && ( + + )} +
    + } + /> + ); + }) + )} +
    + )} +
    + ); +} diff --git a/packages/interface/src/components/SpacesSidebar/GroupHeader.tsx b/packages/interface/src/components/SpacesSidebar/GroupHeader.tsx index 708d3d3ef..3d9e63afc 100644 --- a/packages/interface/src/components/SpacesSidebar/GroupHeader.tsx +++ b/packages/interface/src/components/SpacesSidebar/GroupHeader.tsx @@ -1,9 +1,14 @@ -import { CaretRight, DotsSixVertical, PencilSimple, Trash } from "@phosphor-icons/react"; +import { + CaretRight, + DotsSixVertical, + PencilSimple, + Trash, +} from "@phosphor-icons/react"; +import type { SpaceGroup } from "@sd/ts-client"; +import { useLibraryMutation } from "@sd/ts-client"; import clsx from "clsx"; import { useState } from "react"; import { useContextMenu } from "../../hooks/useContextMenu"; -import { useLibraryMutation } from "@sd/ts-client"; -import type { SpaceGroup } from "@sd/ts-client"; interface GroupHeaderProps { label: string; @@ -29,12 +34,12 @@ export function GroupHeader({ const hasSortable = sortableAttributes && sortableListeners; const [isRenaming, setIsRenaming] = useState(false); const [newName, setNewName] = useState(label); - + const updateGroup = useLibraryMutation("spaces.update_group"); const deleteGroup = useLibraryMutation("spaces.delete_group"); const handleRename = async () => { - if (!group || !newName.trim() || newName === label) { + if (!(group && newName.trim()) || newName === label) { setIsRenaming(false); setNewName(label); return; @@ -55,7 +60,7 @@ export function GroupHeader({ const handleDelete = async () => { if (!group) return; - + try { await deleteGroup.mutateAsync({ group_id: group.id }); } catch (error) { @@ -99,7 +104,7 @@ export function GroupHeader({
    @@ -108,8 +113,9 @@ export function GroupHeader({ {/* Collapsible Button or Rename Input */} {isRenaming ? ( setNewName(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") { @@ -119,18 +125,20 @@ export function GroupHeader({ setNewName(label); } }} - onBlur={handleRename} - autoFocus - className="flex-1 px-2 py-1 text-tiny font-semibold tracking-wider rounded-md bg-sidebar-box border border-sidebar-line text-sidebar-ink placeholder:text-sidebar-ink-faint outline-none focus:border-accent" + type="text" + value={newName} /> ) : ( -
    + {/* Panel */} + +
    + {/* Header */} +
    +
    +

    + Customize +

    +

    + Drag to sidebar +

    +
    + +
    - {/* Content */} -
    - {/* Quick Access Items */} -
    - {PALETTE_ITEMS.map((item) => ( - - ))} -
    + {/* Content */} +
    + {/* Quick Access Items */} +
    + {PALETTE_ITEMS.map((item) => ( + + ))} +
    - {/* Add Group Section */} -
    -
    - - Groups - -
    + {/* Add Group Section */} +
    +
    + + Groups + +
    - {!isAddingGroup ? ( - - ) : ( -
    - + {isAddingGroup ? ( +
    + - {groupType === "Custom" && ( - - setGroupName(e.target.value) - } - placeholder="Group name" - className="text-xs" - onKeyDown={(e) => { - if (e.key === "Enter") { - handleAddGroup(); - } else if (e.key === "Escape") { - setIsAddingGroup(false); - setGroupName(""); - } - }} - autoFocus - /> - )} + {groupType === "Custom" && ( + setGroupName(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") { + handleAddGroup(); + } else if (e.key === "Escape") { + setIsAddingGroup(false); + setGroupName(""); + } + }} + placeholder="Group name" + value={groupName} + /> + )} -
    - - -
    -
    - )} -
    -
    +
    + + +
    +
    + ) : ( + + )} +
    +
    - {/* Footer */} -
    -

    - Drag items to your space -

    -
    -
    -
    - - )} - - ); + {/* Footer */} +
    +

    + Drag items to your space +

    +
    +
    + + + )} + + ); - return createPortal(content, document.body); + return createPortal(content, document.body); } diff --git a/packages/interface/src/components/SpacesSidebar/SpaceGroup.tsx b/packages/interface/src/components/SpacesSidebar/SpaceGroup.tsx index 2d1ad007d..9fefdf359 100644 --- a/packages/interface/src/components/SpacesSidebar/SpaceGroup.tsx +++ b/packages/interface/src/components/SpacesSidebar/SpaceGroup.tsx @@ -1,170 +1,167 @@ +import { useDndContext, useDroppable } from "@dnd-kit/core"; import type { - SpaceGroup as SpaceGroupType, - SpaceItem as SpaceItemType, + SpaceGroup as SpaceGroupType, + SpaceItem as SpaceItemType, } from "@sd/ts-client"; -import { useSidebarStore, useLibraryMutation } from "@sd/ts-client"; -import { SpaceItem } from "./SpaceItem"; +import { useLibraryMutation, useSidebarStore } from "@sd/ts-client"; import { DevicesGroup } from "./DevicesGroup"; -import { LocationsGroup } from "./LocationsGroup"; -import { VolumesGroup } from "./VolumesGroup"; -import { TagsGroup } from "./TagsGroup"; import { GroupHeader } from "./GroupHeader"; -import { useDroppable, useDndContext } from "@dnd-kit/core"; +import { LocationsGroup } from "./LocationsGroup"; +import { SpaceItem } from "./SpaceItem"; +import { TagsGroup } from "./TagsGroup"; +import { VolumesGroup } from "./VolumesGroup"; interface SpaceGroupProps { - group: SpaceGroupType; - items: SpaceItemType[]; - spaceId?: string; - sortableAttributes?: any; - sortableListeners?: any; + group: SpaceGroupType; + items: SpaceItemType[]; + spaceId?: string; + sortableAttributes?: any; + sortableListeners?: any; } export function SpaceGroup({ - group, - items, - spaceId, - sortableAttributes, - sortableListeners, + group, + items, + spaceId, + sortableAttributes, + sortableListeners, }: SpaceGroupProps) { - const { collapsedGroups, toggleGroup: toggleGroupLocal } = useSidebarStore(); - const { active } = useDndContext(); - const updateGroup = useLibraryMutation("spaces.update_group"); - - // Use backend's is_collapsed value as the source of truth, fallback to local state - const isCollapsed = group.is_collapsed ?? collapsedGroups.has(group.id); - - // Toggle handler that updates both local and backend state - const handleToggle = async () => { - // Optimistically update local state for immediate UI feedback - toggleGroupLocal(group.id); - - // Update backend - try { - await updateGroup.mutateAsync({ - group_id: group.id, - is_collapsed: !isCollapsed, - }); - } catch (error) { - console.error("Failed to update group collapse state:", error); - // Revert local state on error - toggleGroupLocal(group.id); - } - }; + const { collapsedGroups, toggleGroup: toggleGroupLocal } = useSidebarStore(); + const { active } = useDndContext(); + const updateGroup = useLibraryMutation("spaces.update_group"); - // Disable insertion drop zones when dragging groups or space items (they have 'label' in their data) - const isDraggingSortableItem = active?.data?.current?.label != null; + // Use backend's is_collapsed value as the source of truth, fallback to local state + const isCollapsed = group.is_collapsed ?? collapsedGroups.has(group.id); - // System groups (Locations, Volumes, etc.) are dynamic - don't allow insertion/reordering - // Custom/QuickAccess groups allow insertion - const allowInsertion = - group.group_type === "QuickAccess" || group.group_type === "Custom"; + // Toggle handler that updates both local and backend state + const handleToggle = async () => { + // Optimistically update local state for immediate UI feedback + toggleGroupLocal(group.id); - // Devices group - fetches all devices (library + paired) - if (group.group_type === "Devices") { - return ( -
    - -
    - ); - } + // Update backend + try { + await updateGroup.mutateAsync({ + group_id: group.id, + is_collapsed: !isCollapsed, + }); + } catch (error) { + console.error("Failed to update group collapse state:", error); + // Revert local state on error + toggleGroupLocal(group.id); + } + }; - // Locations group - fetches all locations - if (group.group_type === "Locations") { - return ( -
    - -
    - ); - } + // Disable insertion drop zones when dragging groups or space items (they have 'label' in their data) + const isDraggingSortableItem = active?.data?.current?.label != null; - // Volumes group - fetches all volumes - if (group.group_type === "Volumes") { - return ( -
    - -
    - ); - } + // System groups (Locations, Volumes, etc.) are dynamic - don't allow insertion/reordering + // Custom/QuickAccess groups allow insertion + const allowInsertion = + group.group_type === "QuickAccess" || group.group_type === "Custom"; - // Tags group - fetches all tags - if (group.group_type === "Tags") { - return ( -
    - -
    - ); - } + // Devices group - fetches all devices (library + paired) + if (group.group_type === "Devices") { + return ( +
    + +
    + ); + } - // Empty drop zone for groups with no items - const { setNodeRef: setEmptyRef, isOver: isOverEmpty } = useDroppable({ - id: `group-${group.id}-empty`, - disabled: !allowInsertion || isCollapsed || isDraggingSortableItem, - data: { - action: "add-to-group", - groupId: group.id, - spaceId, - }, - }); + // Locations group - fetches all locations + if (group.group_type === "Locations") { + return ( +
    + +
    + ); + } - // QuickAccess and Custom groups render stored items - return ( -
    - + // Volumes group - fetches all volumes + if (group.group_type === "Volumes") { + return ( +
    + +
    + ); + } - {/* Items */} - {!isCollapsed && ( -
    - {items.length > 0 ? ( - items.map((item, index) => ( - - )) - ) : ( -
    - {isOverEmpty && !isDraggingSortableItem && ( -
    - )} -
    - )} -
    - )} -
    - ); + // Tags group - fetches all tags + if (group.group_type === "Tags") { + return ( +
    + +
    + ); + } + + // Empty drop zone for groups with no items + const { setNodeRef: setEmptyRef, isOver: isOverEmpty } = useDroppable({ + id: `group-${group.id}-empty`, + disabled: !allowInsertion || isCollapsed || isDraggingSortableItem, + data: { + action: "add-to-group", + groupId: group.id, + spaceId, + }, + }); + + // QuickAccess and Custom groups render stored items + return ( +
    + + + {/* Items */} + {!isCollapsed && ( +
    + {items.length > 0 ? ( + items.map((item, index) => ( + + )) + ) : ( +
    + {isOverEmpty && !isDraggingSortableItem && ( +
    + )} +
    + )} +
    + )} +
    + ); } diff --git a/packages/interface/src/components/SpacesSidebar/SpaceItem.tsx b/packages/interface/src/components/SpacesSidebar/SpaceItem.tsx index 3a8f3dee0..1378d21f8 100644 --- a/packages/interface/src/components/SpacesSidebar/SpaceItem.tsx +++ b/packages/interface/src/components/SpacesSidebar/SpaceItem.tsx @@ -1,329 +1,320 @@ -import { useNavigate } from "react-router-dom"; -import clsx from "clsx"; -import type { SpaceItem as SpaceItemType } from "@sd/ts-client"; -import { Thumb } from "../../routes/explorer/File/Thumb"; import { useSortable } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; - +import type { SpaceItem as SpaceItemType } from "@sd/ts-client"; +import clsx from "clsx"; +import { useNavigate } from "react-router-dom"; import { - resolveItemMetadata, - isRawLocation, - type IconData, + getSpaceItemKeyFromRoute, + useExplorer, +} from "../../routes/explorer/context"; +import { Thumb } from "../../routes/explorer/File/Thumb"; +import { + type IconData, + isRawLocation, + resolveItemMetadata, } from "./hooks/spaceItemUtils"; import { useSpaceItemActive } from "./hooks/useSpaceItemActive"; -import { useSpaceItemDropZones } from "./hooks/useSpaceItemDropZones"; import { useSpaceItemContextMenu } from "./hooks/useSpaceItemContextMenu"; -import { useExplorer, getSpaceItemKeyFromRoute } from "../../routes/explorer/context"; +import { useSpaceItemDropZones } from "./hooks/useSpaceItemDropZones"; // Overrides for customizing item appearance and behavior export interface SpaceItemOverrides { - label?: string; - icon?: string; - onClick?: (e?: React.MouseEvent) => void; - onContextMenu?: (e: React.MouseEvent) => void; + label?: string; + icon?: string; + onClick?: (e?: React.MouseEvent) => void; + onContextMenu?: (e: React.MouseEvent) => void; } export interface SpaceItemProps { - item: SpaceItemType; - spaceId?: string; - groupId?: string | null; - // Behavior flags - sortable?: boolean; - allowInsertion?: boolean; - isLastItem?: boolean; - // Overrides - overrides?: SpaceItemOverrides; - rightComponent?: React.ReactNode; - // Legacy props (for backwards compatibility during migration) - volumeData?: { device_slug: string; mount_path: string }; - customIcon?: string; - customLabel?: string; - onClick?: (e?: React.MouseEvent) => void; - onContextMenu?: (e: React.MouseEvent) => void; - className?: string; + item: SpaceItemType; + spaceId?: string; + groupId?: string | null; + // Behavior flags + sortable?: boolean; + allowInsertion?: boolean; + isLastItem?: boolean; + // Overrides + overrides?: SpaceItemOverrides; + rightComponent?: React.ReactNode; + // Legacy props (for backwards compatibility during migration) + volumeData?: { device_slug: string; mount_path: string }; + customIcon?: string; + customLabel?: string; + onClick?: (e?: React.MouseEvent) => void; + onContextMenu?: (e: React.MouseEvent) => void; + className?: string; } // Icon component that handles both component icons and image icons function ItemIcon({ icon }: { icon: IconData }) { - if (icon.type === "image") { - return ; - } - const IconComponent = icon.icon; - return ( - - - - ); + if (icon.type === "image") { + return ; + } + const IconComponent = icon.icon; + return ( + + + + ); } // Insertion line indicator function InsertionLine({ visible }: { visible: boolean }) { - if (!visible) return null; - return ( -
    - ); + if (!visible) return null; + return ( +
    + ); } // Bottom insertion line (for last items) function BottomInsertionLine({ visible }: { visible: boolean }) { - if (!visible) return null; - return ( -
    - ); + if (!visible) return null; + return ( +
    + ); } // Drop highlight ring for drop-into targets function DropHighlight({ visible }: { visible: boolean }) { - if (!visible) return null; - return ( -
    - ); + if (!visible) return null; + return ( +
    + ); } // Drop zone overlays (invisible hit areas) interface DropZoneOverlaysProps { - isDropTarget: boolean; - setTopRef: (node: HTMLElement | null) => void; - setBottomRef: (node: HTMLElement | null) => void; - setMiddleRef: (node: HTMLElement | null) => void; + isDropTarget: boolean; + setTopRef: (node: HTMLElement | null) => void; + setBottomRef: (node: HTMLElement | null) => void; + setMiddleRef: (node: HTMLElement | null) => void; } function DropZoneOverlays({ - isDropTarget, - setTopRef, - setBottomRef, - setMiddleRef, + isDropTarget, + setTopRef, + setBottomRef, + setMiddleRef, }: DropZoneOverlaysProps) { - if (isDropTarget) { - return ( - <> - {/* Top zone - insertion above */} -
    - {/* Middle zone - drop into folder */} -
    - {/* Bottom zone - insertion below */} -
    - - ); - } + if (isDropTarget) { + return ( + <> + {/* Top zone - insertion above */} +
    + {/* Middle zone - drop into folder */} +
    + {/* Bottom zone - insertion below */} +
    + + ); + } - return ( - <> - {/* Top zone - insertion above */} -
    - {/* Bottom zone - insertion below */} -
    - - ); + return ( + <> + {/* Top zone - insertion above */} +
    + {/* Bottom zone - insertion below */} +
    + + ); } export function SpaceItem({ - item, - spaceId, - groupId, - sortable = false, - allowInsertion = true, - isLastItem = false, - overrides, - rightComponent, - // Legacy props - volumeData, - customIcon, - customLabel, - onClick: legacyOnClick, - onContextMenu: legacyOnContextMenu, - className, + item, + spaceId, + groupId, + sortable = false, + allowInsertion = true, + isLastItem = false, + overrides, + rightComponent, + // Legacy props + volumeData, + customIcon, + customLabel, + onClick: legacyOnClick, + onContextMenu: legacyOnContextMenu, + className, }: SpaceItemProps) { - const navigate = useNavigate(); - const { loadPreferencesForSpaceItem } = useExplorer(); + const navigate = useNavigate(); + const { loadPreferencesForSpaceItem } = useExplorer(); - // Merge legacy props into overrides - const effectiveOverrides: SpaceItemOverrides = { - ...overrides, - label: overrides?.label ?? customLabel, - icon: overrides?.icon ?? customIcon, - onClick: overrides?.onClick ?? legacyOnClick, - onContextMenu: overrides?.onContextMenu ?? legacyOnContextMenu, - }; + // Merge legacy props into overrides + const effectiveOverrides: SpaceItemOverrides = { + ...overrides, + label: overrides?.label ?? customLabel, + icon: overrides?.icon ?? customIcon, + onClick: overrides?.onClick ?? legacyOnClick, + onContextMenu: overrides?.onContextMenu ?? legacyOnContextMenu, + }; - // Resolve metadata (icon, label, path) - const { icon, label, path } = resolveItemMetadata(item, { - volumeData, - customIcon: effectiveOverrides.icon, - customLabel: effectiveOverrides.label, - }); + // Resolve metadata (icon, label, path) + const { icon, label, path } = resolveItemMetadata(item, { + volumeData, + customIcon: effectiveOverrides.icon, + customLabel: effectiveOverrides.label, + }); - // Get resolved file for thumbnail rendering - const resolvedFile = isRawLocation(item) - ? undefined - : (item as SpaceItemType).resolved_file; + // Get resolved file for thumbnail rendering + const resolvedFile = isRawLocation(item) + ? undefined + : (item as SpaceItemType).resolved_file; - // Active state detection - const isActive = useSpaceItemActive({ - item: item as SpaceItemType, - path, - hasCustomOnClick: !!effectiveOverrides.onClick, - }); + // Active state detection + const isActive = useSpaceItemActive({ + item: item as SpaceItemType, + path, + hasCustomOnClick: !!effectiveOverrides.onClick, + }); - // Drop zone management - const dropZones = useSpaceItemDropZones({ - item: item as SpaceItemType, - allowInsertion, - spaceId, - groupId, - volumeData, - }); + // Drop zone management + const dropZones = useSpaceItemDropZones({ + item: item as SpaceItemType, + allowInsertion, + spaceId, + groupId, + volumeData, + }); - // Context menu - const contextMenu = useSpaceItemContextMenu({ - item: item as SpaceItemType, - path, - spaceId, - }); + // Context menu + const contextMenu = useSpaceItemContextMenu({ + item: item as SpaceItemType, + path, + spaceId, + }); - // Sortable drag/drop - const { - attributes: sortableAttributes, - listeners: sortableListeners, - setNodeRef: setSortableRef, - transform, - transition, - isDragging: isSortableDragging, - } = useSortable({ - id: (item as SpaceItemType).id, - disabled: !sortable, - data: { label }, - }); + // Sortable drag/drop + const { + attributes: sortableAttributes, + listeners: sortableListeners, + setNodeRef: setSortableRef, + transform, + transition, + isDragging: isSortableDragging, + } = useSortable({ + id: (item as SpaceItemType).id, + disabled: !sortable, + data: { label }, + }); - const style = sortable - ? { - transform: CSS.Transform.toString(transform), - transition, - } - : undefined; + const style = sortable + ? { + transform: CSS.Transform.toString(transform), + transition, + } + : undefined; - // Event handlers - const handleClick = (e: React.MouseEvent) => { - if (effectiveOverrides.onClick) { - effectiveOverrides.onClick(e); - } else if (path) { - // Extract pathname and search from the path - const [pathname, search] = path.includes("?") - ? [path.split("?")[0], "?" + path.split("?")[1]] - : [path, ""]; - const spaceItemKey = getSpaceItemKeyFromRoute(pathname, search); - loadPreferencesForSpaceItem(spaceItemKey); - navigate(path); - } - }; + // Event handlers + const handleClick = (e: React.MouseEvent) => { + if (effectiveOverrides.onClick) { + effectiveOverrides.onClick(e); + } else if (path) { + // Extract pathname and search from the path + const [pathname, search] = path.includes("?") + ? [path.split("?")[0], "?" + path.split("?")[1]] + : [path, ""]; + const spaceItemKey = getSpaceItemKeyFromRoute(pathname, search); + loadPreferencesForSpaceItem(spaceItemKey); + navigate(path); + } + }; - const handleContextMenu = async (e: React.MouseEvent) => { - if (effectiveOverrides.onContextMenu) { - effectiveOverrides.onContextMenu(e); - return; - } + const handleContextMenu = async (e: React.MouseEvent) => { + if (effectiveOverrides.onContextMenu) { + effectiveOverrides.onContextMenu(e); + return; + } - e.preventDefault(); - e.stopPropagation(); - await contextMenu.show(e); - }; + e.preventDefault(); + e.stopPropagation(); + await contextMenu.show(e); + }; - // Computed visibility for indicators - const showTopLine = - dropZones.isOverTop && - !isSortableDragging && - !dropZones.isDraggingSortableItem; - const showBottomLine = - dropZones.isOverBottom && - isLastItem && - !dropZones.isDraggingSortableItem; - const showDropHighlight = - dropZones.isOverMiddle && - dropZones.isDropTarget && - !isSortableDragging && - !dropZones.isDraggingSortableItem; + // Computed visibility for indicators + const showTopLine = + dropZones.isOverTop && + !isSortableDragging && + !dropZones.isDraggingSortableItem; + const showBottomLine = + dropZones.isOverBottom && isLastItem && !dropZones.isDraggingSortableItem; + const showDropHighlight = + dropZones.isOverMiddle && + dropZones.isDropTarget && + !isSortableDragging && + !dropZones.isDraggingSortableItem; - return ( -
    - - + return ( +
    + + -
    - +
    + - -
    + +
    - -
    - ); -} \ No newline at end of file + +
    + ); +} diff --git a/packages/interface/src/components/SpacesSidebar/SpaceSwitcher.tsx b/packages/interface/src/components/SpacesSidebar/SpaceSwitcher.tsx index 182d261bc..6b2db605f 100644 --- a/packages/interface/src/components/SpacesSidebar/SpaceSwitcher.tsx +++ b/packages/interface/src/components/SpacesSidebar/SpaceSwitcher.tsx @@ -1,81 +1,85 @@ -import clsx from 'clsx'; -import { CaretDown, Plus, GearSix } from '@phosphor-icons/react'; -import { DropdownMenu } from '@sd/ui'; -import type { Space } from '@sd/ts-client'; -import { useCreateSpaceDialog } from './CreateSpaceModal'; +import { CaretDown, GearSix, Plus } from "@phosphor-icons/react"; +import type { Space } from "@sd/ts-client"; +import { DropdownMenu } from "@sd/ui"; +import clsx from "clsx"; +import { useCreateSpaceDialog } from "./CreateSpaceModal"; interface SpaceSwitcherProps { - spaces: Space[] | undefined; - currentSpace: Space | undefined; - onSwitch: (spaceId: string) => void; + spaces: Space[] | undefined; + currentSpace: Space | undefined; + onSwitch: (spaceId: string) => void; } -export function SpaceSwitcher({ spaces, currentSpace, onSwitch }: SpaceSwitcherProps) { - const createSpaceDialog = useCreateSpaceDialog; +export function SpaceSwitcher({ + spaces, + currentSpace, + onSwitch, +}: SpaceSwitcherProps) { + const createSpaceDialog = useCreateSpaceDialog; - return ( - -
    - - {currentSpace?.name || 'Select Space'} - - - - } - className="p-1 bg-sidebar-box border border-sidebar-line rounded-lg shadow-sm overflow-hidden" - > - {spaces && spaces.length > 1 - ? spaces.map((space) => ( - onSwitch(space.id)} - className={clsx( - "px-2 py-1 text-sm rounded-md", - space.id === currentSpace?.id - ? "bg-accent text-white" - : "text-sidebar-ink hover:bg-sidebar-selected" - )} - > -
    -
    - {space.name} -
    - - )) - : null} - {spaces && spaces.length > 1 && ( - - )} - createSpaceDialog()} - className="px-2 py-1 text-sm rounded-md hover:bg-sidebar-selected text-sidebar-ink font-medium" - > - New Space - - - Space Settings - - - ); + return ( + +
    + + {currentSpace?.name || "Select Space"} + + + + } + > + {spaces && spaces.length > 1 + ? spaces.map((space) => ( + onSwitch(space.id)} + > +
    +
    + {space.name} +
    + + )) + : null} + {spaces && spaces.length > 1 && ( + + )} + createSpaceDialog()} + > + New Space + + + Space Settings + + + ); } diff --git a/packages/interface/src/components/SpacesSidebar/TagsGroup.tsx b/packages/interface/src/components/SpacesSidebar/TagsGroup.tsx index fc061720a..6bf53a6bf 100644 --- a/packages/interface/src/components/SpacesSidebar/TagsGroup.tsx +++ b/packages/interface/src/components/SpacesSidebar/TagsGroup.tsx @@ -1,208 +1,227 @@ -import { Tag as TagIcon, Plus, CaretRight } from '@phosphor-icons/react'; -import { useState } from 'react'; -import { useNavigate } from 'react-router-dom'; -import clsx from 'clsx'; -import { useNormalizedQuery, useLibraryMutation } from '../../contexts/SpacedriveContext'; -import type { Tag } from '@sd/ts-client'; -import { GroupHeader } from './GroupHeader'; -import { useExplorer } from '../../routes/explorer/context'; +import { CaretRight, Plus, Tag as TagIcon } from "@phosphor-icons/react"; +import type { Tag } from "@sd/ts-client"; +import clsx from "clsx"; +import { useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { + useLibraryMutation, + useNormalizedQuery, +} from "../../contexts/SpacedriveContext"; +import { useExplorer } from "../../routes/explorer/context"; +import { GroupHeader } from "./GroupHeader"; interface TagsGroupProps { - isCollapsed: boolean; - onToggle: () => void; - sortableAttributes?: any; - sortableListeners?: any; + isCollapsed: boolean; + onToggle: () => void; + sortableAttributes?: any; + sortableListeners?: any; } interface TagItemProps { - tag: Tag; - depth?: number; + tag: Tag; + depth?: number; } function TagItem({ tag, depth = 0 }: TagItemProps) { - const navigate = useNavigate(); - const { loadPreferencesForSpaceItem } = useExplorer(); - const [isExpanded, setIsExpanded] = useState(false); + const navigate = useNavigate(); + const { loadPreferencesForSpaceItem } = useExplorer(); + const [isExpanded, setIsExpanded] = useState(false); - // TODO: Fetch children when hierarchy is implemented - const children: Tag[] = []; - const hasChildren = children.length > 0; + // TODO: Fetch children when hierarchy is implemented + const children: Tag[] = []; + const hasChildren = children.length > 0; - const handleClick = () => { - loadPreferencesForSpaceItem(`tag:${tag.id}`); - navigate(`/tag/${tag.id}`); - }; + const handleClick = () => { + loadPreferencesForSpaceItem(`tag:${tag.id}`); + navigate(`/tag/${tag.id}`); + }; - return ( -
    - + {/* File count badge (if available) */} + {/* TODO: Add file count when available from backend */} + - {/* Children (recursive) */} - {isExpanded && - children.map((child) => )} -
    - ); + {/* Children (recursive) */} + {isExpanded && + children.map((child) => ( + + ))} +
    + ); } export function TagsGroup({ - isCollapsed, - onToggle, - sortableAttributes, - sortableListeners, + isCollapsed, + onToggle, + sortableAttributes, + sortableListeners, }: TagsGroupProps) { - const navigate = useNavigate(); - const { loadPreferencesForSpaceItem } = useExplorer(); - const [isCreating, setIsCreating] = useState(false); - const [newTagName, setNewTagName] = useState(''); + const navigate = useNavigate(); + const { loadPreferencesForSpaceItem } = useExplorer(); + const [isCreating, setIsCreating] = useState(false); + const [newTagName, setNewTagName] = useState(""); - const createTag = useLibraryMutation('tags.create'); + const createTag = useLibraryMutation("tags.create"); - // Fetch tags with real-time updates using search with empty query - // Using select to normalize TagSearchResult[] to Tag[] for consistent cache structure - const { data: tags = [], isLoading } = useNormalizedQuery({ - wireMethod: 'query:tags.search', - input: { query: '' }, - resourceType: 'tag', - select: (data: any) => data?.tags?.map((result: any) => result.tag || result).filter(Boolean) ?? [] - }); + // Fetch tags with real-time updates using search with empty query + // Using select to normalize TagSearchResult[] to Tag[] for consistent cache structure + const { data: tags = [], isLoading } = useNormalizedQuery({ + wireMethod: "query:tags.search", + input: { query: "" }, + resourceType: "tag", + select: (data: any) => + data?.tags?.map((result: any) => result.tag || result).filter(Boolean) ?? + [], + }); - const handleCreateTag = async () => { - if (!newTagName.trim()) return; + const handleCreateTag = async () => { + if (!newTagName.trim()) return; - try { - const result = await createTag.mutateAsync({ - canonical_name: newTagName.trim(), - display_name: null, - formal_name: null, - abbreviation: null, - aliases: [], - namespace: null, - tag_type: null, - color: `#${Math.floor(Math.random() * 16777215).toString(16).padStart(6, '0')}`, - icon: null, - description: null, - is_organizational_anchor: null, - privacy_level: null, - search_weight: null, - attributes: null, - apply_to: null - }); + try { + const result = await createTag.mutateAsync({ + canonical_name: newTagName.trim(), + display_name: null, + formal_name: null, + abbreviation: null, + aliases: [], + namespace: null, + tag_type: null, + color: `#${Math.floor(Math.random() * 16_777_215) + .toString(16) + .padStart(6, "0")}`, + icon: null, + description: null, + is_organizational_anchor: null, + privacy_level: null, + search_weight: null, + attributes: null, + apply_to: null, + }); - // Navigate to the new tag - if (result?.tag_id) { - loadPreferencesForSpaceItem(`tag:${result.tag_id}`); - navigate(`/tag/${result.tag_id}`); - } + // Navigate to the new tag + if (result?.tag_id) { + loadPreferencesForSpaceItem(`tag:${result.tag_id}`); + navigate(`/tag/${result.tag_id}`); + } - setNewTagName(''); - setIsCreating(false); - } catch (err) { - console.error('Failed to create tag:', err); - } - }; + setNewTagName(""); + setIsCreating(false); + } catch (err) { + console.error("Failed to create tag:", err); + } + }; - return ( -
    - 0 && ( - {tags.length} - ) - } - /> + return ( +
    + 0 && ( + + {tags.length} + + ) + } + sortableAttributes={sortableAttributes} + sortableListeners={sortableListeners} + /> - {/* Items */} - {!isCollapsed && ( -
    - {isLoading ? ( -
    Loading...
    - ) : tags.length === 0 ? ( -
    No tags yet
    - ) : ( - tags.map((tag) => ) - )} + {/* Items */} + {!isCollapsed && ( +
    + {isLoading ? ( +
    + Loading... +
    + ) : tags.length === 0 ? ( +
    + No tags yet +
    + ) : ( + tags.map((tag) => ) + )} - {/* Create Tag Button/Input */} - {isCreating ? ( -
    - setNewTagName(e.target.value)} - onKeyDown={(e) => { - if (e.key === 'Enter') { - handleCreateTag(); - } else if (e.key === 'Escape') { - setIsCreating(false); - setNewTagName(''); - } - }} - onBlur={() => { - if (!newTagName.trim()) { - setIsCreating(false); - } - }} - placeholder="Tag name..." - autoFocus - className="w-full px-2 py-1 text-xs rounded-md bg-sidebar-box border border-sidebar-line text-sidebar-ink placeholder:text-sidebar-ink-faint outline-none focus:border-accent" - /> -
    - ) : ( - - )} -
    - )} -
    - ); -} \ No newline at end of file + {/* Create Tag Button/Input */} + {isCreating ? ( +
    + { + if (!newTagName.trim()) { + setIsCreating(false); + } + }} + onChange={(e) => setNewTagName(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") { + handleCreateTag(); + } else if (e.key === "Escape") { + setIsCreating(false); + setNewTagName(""); + } + }} + placeholder="Tag name..." + type="text" + value={newTagName} + /> +
    + ) : ( + + )} +
    + )} +
    + ); +} diff --git a/packages/interface/src/components/SpacesSidebar/VolumesGroup.tsx b/packages/interface/src/components/SpacesSidebar/VolumesGroup.tsx index 75c4e63fa..474a24fbd 100644 --- a/packages/interface/src/components/SpacesSidebar/VolumesGroup.tsx +++ b/packages/interface/src/components/SpacesSidebar/VolumesGroup.tsx @@ -1,92 +1,88 @@ -import { useNavigate } from "react-router-dom"; -import { Plugs, WifiSlash } from "@phosphor-icons/react"; -import { useNormalizedQuery, getVolumeIcon } from "@sd/ts-client"; -import { SpaceItem } from "./SpaceItem"; -import { GroupHeader } from "./GroupHeader"; +import { Plugs } from "@phosphor-icons/react"; import type { VolumeItem } from "@sd/ts-client"; +import { getVolumeIcon, useNormalizedQuery } from "@sd/ts-client"; +import { useNavigate } from "react-router-dom"; +import { GroupHeader } from "./GroupHeader"; +import { SpaceItem } from "./SpaceItem"; interface VolumesGroupProps { - isCollapsed: boolean; - onToggle: () => void; - /** Filter to show tracked, untracked, or all volumes (default: "All") */ - filter?: "TrackedOnly" | "UntrackedOnly" | "All"; - sortableAttributes?: any; - sortableListeners?: any; + isCollapsed: boolean; + onToggle: () => void; + /** Filter to show tracked, untracked, or all volumes (default: "All") */ + filter?: "TrackedOnly" | "UntrackedOnly" | "All"; + sortableAttributes?: any; + sortableListeners?: any; } export function VolumesGroup({ - isCollapsed, - onToggle, - filter = "All", - sortableAttributes, - sortableListeners, + isCollapsed, + onToggle, + filter = "All", + sortableAttributes, + sortableListeners, }: VolumesGroupProps) { - const navigate = useNavigate(); + const navigate = useNavigate(); - const { data: volumesData } = useNormalizedQuery({ - wireMethod: "query:volumes.list", - input: { filter }, - resourceType: "volume", - }); + const { data: volumesData } = useNormalizedQuery({ + wireMethod: "query:volumes.list", + input: { filter }, + resourceType: "volume", + }); - const volumes = volumesData?.volumes || []; + const volumes = volumesData?.volumes || []; - // Helper to render volume status indicator - const getVolumeIndicator = (volume: VolumeItem) => ( - <> - {!volume.is_tracked && ( - - )} - - ); + // Helper to render volume status indicator + const getVolumeIndicator = (volume: VolumeItem) => ( + <> + {!volume.is_tracked && ( + + )} + + ); - return ( -
    - + return ( +
    + - {/* Volumes List */} - {!isCollapsed && ( -
    - {volumes.length === 0 ? ( -
    - No volumes -
    - ) : ( - volumes.map((volume, index) => ( - - )) - )} -
    - )} -
    - ); + {/* Volumes List */} + {!isCollapsed && ( +
    + {volumes.length === 0 ? ( +
    No volumes
    + ) : ( + volumes.map((volume, index) => ( + + )) + )} +
    + )} +
    + ); } diff --git a/packages/interface/src/components/SpacesSidebar/dnd.ts b/packages/interface/src/components/SpacesSidebar/dnd.ts index a096a1a7c..0af3f87ec 100644 --- a/packages/interface/src/components/SpacesSidebar/dnd.ts +++ b/packages/interface/src/components/SpacesSidebar/dnd.ts @@ -2,9 +2,9 @@ import type { SdPath } from "@sd/ts-client"; // Data transferred during drag operations export interface SidebarDragData { - type: "explorer-file"; - sdPath: SdPath; - name: string; + type: "explorer-file"; + sdPath: SdPath; + name: string; } // Global state for tracking internal app drag data @@ -15,30 +15,35 @@ type DragStateListener = (isDragging: boolean) => void; const dragStateListeners = new Set(); export function setDragData(data: SidebarDragData | null) { - console.log("[DnD] setDragData called, data:", data, "listeners:", dragStateListeners.size); - currentDragData = data; - const isDragging = data !== null; + console.log( + "[DnD] setDragData called, data:", + data, + "listeners:", + dragStateListeners.size + ); + currentDragData = data; + const isDragging = data !== null; - // Always notify listeners immediately (sync) - dragStateListeners.forEach(listener => { - console.log("[DnD] Calling listener with isDragging:", isDragging); - listener(isDragging); - }); + // Always notify listeners immediately (sync) + dragStateListeners.forEach((listener) => { + console.log("[DnD] Calling listener with isDragging:", isDragging); + listener(isDragging); + }); } export function getDragData(): SidebarDragData | null { - return currentDragData; + return currentDragData; } export function clearDragData() { - setDragData(null); + setDragData(null); } export function isDragging(): boolean { - return currentDragData !== null; + return currentDragData !== null; } export function subscribeToDragState(listener: DragStateListener): () => void { - dragStateListeners.add(listener); - return () => dragStateListeners.delete(listener); + dragStateListeners.add(listener); + return () => dragStateListeners.delete(listener); } diff --git a/packages/interface/src/components/SpacesSidebar/hooks/index.ts b/packages/interface/src/components/SpacesSidebar/hooks/index.ts index d53d3b149..0adc66e00 100644 --- a/packages/interface/src/components/SpacesSidebar/hooks/index.ts +++ b/packages/interface/src/components/SpacesSidebar/hooks/index.ts @@ -1,29 +1,31 @@ // Space item utilities export { - isOverviewItem, - isRecentsItem, - isFavoritesItem, - isFileKindsItem, - isLocationItem, - isVolumeItem, - isTagItem, - isPathItem, - isRawLocation, - isDropTargetItem, - getDropTargetType, - buildDropTargetPath, - resolveItemMetadata, - type IconData, - type ItemMetadata, - type ResolveMetadataOptions, - type DropTargetType, + buildDropTargetPath, + type DropTargetType, + getDropTargetType, + type IconData, + type ItemMetadata, + isDropTargetItem, + isFavoritesItem, + isFileKindsItem, + isLocationItem, + isOverviewItem, + isPathItem, + isRawLocation, + isRecentsItem, + isTagItem, + isVolumeItem, + type ResolveMetadataOptions, + resolveItemMetadata, } from "./spaceItemUtils"; // Space item hooks export { useSpaceItemActive } from "./useSpaceItemActive"; -export { useSpaceItemDropZones, type UseSpaceItemDropZonesResult } from "./useSpaceItemDropZones"; export { useSpaceItemContextMenu } from "./useSpaceItemContextMenu"; +export { + type UseSpaceItemDropZonesResult, + useSpaceItemDropZones, +} from "./useSpaceItemDropZones"; // Space data hooks -export { useSpaces, useSpaceLayout } from "./useSpaces"; - +export { useSpaceLayout, useSpaces } from "./useSpaces"; diff --git a/packages/interface/src/components/SpacesSidebar/hooks/spaceItemUtils.ts b/packages/interface/src/components/SpacesSidebar/hooks/spaceItemUtils.ts index f6c2e8d11..120a81501 100644 --- a/packages/interface/src/components/SpacesSidebar/hooks/spaceItemUtils.ts +++ b/packages/interface/src/components/SpacesSidebar/hooks/spaceItemUtils.ts @@ -1,277 +1,277 @@ +import type { Icon } from "@phosphor-icons/react"; import { - House, - Clock, - Heart, - Folder, - HardDrive, - Tag as TagIcon, - Folders, + Clock, + Folders, + HardDrive, + Heart, + House, + Tag as TagIcon, } from "@phosphor-icons/react"; import { Location } from "@sd/assets/icons"; import type { - SpaceItem as SpaceItemType, - ItemType, - File, - SdPath, + File, + ItemType, + SdPath, + SpaceItem as SpaceItemType, } from "@sd/ts-client"; -import type { Icon } from "@phosphor-icons/react"; // Icon data returned from metadata resolution export type IconData = - | { type: "component"; icon: Icon } - | { type: "image"; icon: string }; + | { type: "component"; icon: Icon } + | { type: "image"; icon: string }; // Metadata resolved for a space item export interface ItemMetadata { - icon: IconData; - label: string; - path: string | null; + icon: IconData; + label: string; + path: string | null; } // Type guards for ItemType discrimination export function isOverviewItem(t: ItemType): t is "Overview" { - return t === "Overview"; + return t === "Overview"; } export function isRecentsItem(t: ItemType): t is "Recents" { - return t === "Recents"; + return t === "Recents"; } export function isFavoritesItem(t: ItemType): t is "Favorites" { - return t === "Favorites"; + return t === "Favorites"; } export function isFileKindsItem(t: ItemType): t is "FileKinds" { - return t === "FileKinds"; + return t === "FileKinds"; } export function isLocationItem( - t: ItemType, + t: ItemType ): t is { Location: { location_id: string } } { - return typeof t === "object" && "Location" in t; + return typeof t === "object" && "Location" in t; } export function isVolumeItem( - t: ItemType, + t: ItemType ): t is { Volume: { volume_id: string } } { - return typeof t === "object" && "Volume" in t; + return typeof t === "object" && "Volume" in t; } export function isTagItem(t: ItemType): t is { Tag: { tag_id: string } } { - return typeof t === "object" && "Tag" in t; + return typeof t === "object" && "Tag" in t; } export function isPathItem(t: ItemType): t is { Path: { sd_path: SdPath } } { - return typeof t === "object" && "Path" in t; + return typeof t === "object" && "Path" in t; } // Check if item is a "raw" location (legacy format with name/sd_path but no item_type) export function isRawLocation( - item: SpaceItemType | Record, + item: SpaceItemType | Record ): boolean { - return "name" in item && "sd_path" in item && !("item_type" in item); + return "name" in item && "sd_path" in item && !("item_type" in item); } // Get icon data for an item type function getItemIcon(itemType: ItemType): IconData { - if (isOverviewItem(itemType)) return { type: "component", icon: House }; - if (isRecentsItem(itemType)) return { type: "component", icon: Clock }; - if (isFavoritesItem(itemType)) return { type: "component", icon: Heart }; - if (isFileKindsItem(itemType)) return { type: "component", icon: Folders }; - if (isLocationItem(itemType)) return { type: "image", icon: Location }; - if (isVolumeItem(itemType)) return { type: "component", icon: HardDrive }; - if (isTagItem(itemType)) return { type: "component", icon: TagIcon }; - if (isPathItem(itemType)) return { type: "image", icon: Location }; - return { type: "image", icon: Location }; + if (isOverviewItem(itemType)) return { type: "component", icon: House }; + if (isRecentsItem(itemType)) return { type: "component", icon: Clock }; + if (isFavoritesItem(itemType)) return { type: "component", icon: Heart }; + if (isFileKindsItem(itemType)) return { type: "component", icon: Folders }; + if (isLocationItem(itemType)) return { type: "image", icon: Location }; + if (isVolumeItem(itemType)) return { type: "component", icon: HardDrive }; + if (isTagItem(itemType)) return { type: "component", icon: TagIcon }; + if (isPathItem(itemType)) return { type: "image", icon: Location }; + return { type: "image", icon: Location }; } // Get label for an item type function getItemLabel(itemType: ItemType, resolvedFile?: File | null): string { - if (isOverviewItem(itemType)) return "Overview"; - if (isRecentsItem(itemType)) return "Recents"; - if (isFavoritesItem(itemType)) return "Favorites"; - if (isFileKindsItem(itemType)) return "File Kinds"; - if (isLocationItem(itemType)) return itemType.Location.name || "Unnamed Location"; - if (isVolumeItem(itemType)) return itemType.Volume.name || "Unnamed Volume"; - if (isTagItem(itemType)) return itemType.Tag.name || "Unnamed Tag"; - if (isPathItem(itemType)) { - // Use resolved file name if available, otherwise extract from path - if (resolvedFile?.name) return resolvedFile.name; - const sdPath = itemType.Path.sd_path; - if (typeof sdPath === "object" && "Physical" in sdPath) { - const parts = ( - sdPath as { Physical: { path: string } } - ).Physical.path.split("/"); - return parts[parts.length - 1] || "Path"; - } - return "Path"; - } - return "Unknown"; + if (isOverviewItem(itemType)) return "Overview"; + if (isRecentsItem(itemType)) return "Recents"; + if (isFavoritesItem(itemType)) return "Favorites"; + if (isFileKindsItem(itemType)) return "File Kinds"; + if (isLocationItem(itemType)) + return itemType.Location.name || "Unnamed Location"; + if (isVolumeItem(itemType)) return itemType.Volume.name || "Unnamed Volume"; + if (isTagItem(itemType)) return itemType.Tag.name || "Unnamed Tag"; + if (isPathItem(itemType)) { + // Use resolved file name if available, otherwise extract from path + if (resolvedFile?.name) return resolvedFile.name; + const sdPath = itemType.Path.sd_path; + if (typeof sdPath === "object" && "Physical" in sdPath) { + const parts = ( + sdPath as { Physical: { path: string } } + ).Physical.path.split("/"); + return parts[parts.length - 1] || "Path"; + } + return "Path"; + } + return "Unknown"; } // Build navigation path for an item function getItemPath( - itemType: ItemType, - volumeData?: { device_slug: string; mount_path: string }, - itemSdPath?: SdPath, + itemType: ItemType, + volumeData?: { device_slug: string; mount_path: string }, + itemSdPath?: SdPath ): string | null { - if (isOverviewItem(itemType)) return "/"; - if (isRecentsItem(itemType)) return "/recents"; - if (isFavoritesItem(itemType)) return "/favorites"; - if (isFileKindsItem(itemType)) return "/file-kinds"; + if (isOverviewItem(itemType)) return "/"; + if (isRecentsItem(itemType)) return "/recents"; + if (isFavoritesItem(itemType)) return "/favorites"; + if (isFileKindsItem(itemType)) return "/file-kinds"; - if (isLocationItem(itemType)) { - // Use explorer route with location's SD path (passed from item.sd_path) - if (itemSdPath) { - return `/explorer?path=${encodeURIComponent(JSON.stringify(itemSdPath))}`; - } - return null; - } + if (isLocationItem(itemType)) { + // Use explorer route with location's SD path (passed from item.sd_path) + if (itemSdPath) { + return `/explorer?path=${encodeURIComponent(JSON.stringify(itemSdPath))}`; + } + return null; + } - if (isVolumeItem(itemType)) { - // Navigate to explorer with volume's root path - if (volumeData) { - const sdPath = { - Physical: { - device_slug: volumeData.device_slug, - path: volumeData.mount_path || "/", - }, - }; - return `/explorer?path=${encodeURIComponent(JSON.stringify(sdPath))}`; - } - return null; - } + if (isVolumeItem(itemType)) { + // Navigate to explorer with volume's root path + if (volumeData) { + const sdPath = { + Physical: { + device_slug: volumeData.device_slug, + path: volumeData.mount_path || "/", + }, + }; + return `/explorer?path=${encodeURIComponent(JSON.stringify(sdPath))}`; + } + return null; + } - if (isTagItem(itemType)) { - return `/tag/${itemType.Tag.tag_id}`; - } + if (isTagItem(itemType)) { + return `/tag/${itemType.Tag.tag_id}`; + } - if (isPathItem(itemType)) { - // Navigate to explorer with the SD path - return `/explorer?path=${encodeURIComponent(JSON.stringify(itemType.Path.sd_path))}`; - } + if (isPathItem(itemType)) { + // Navigate to explorer with the SD path + return `/explorer?path=${encodeURIComponent(JSON.stringify(itemType.Path.sd_path))}`; + } - return null; + return null; } // Options for resolving item metadata export interface ResolveMetadataOptions { - volumeData?: { device_slug: string; mount_path: string }; - customIcon?: string; - customLabel?: string; + volumeData?: { device_slug: string; mount_path: string }; + customIcon?: string; + customLabel?: string; } // Resolve all metadata for a space item in one call export function resolveItemMetadata( - item: SpaceItemType | Record, - options: ResolveMetadataOptions = {}, + item: SpaceItemType | Record, + options: ResolveMetadataOptions = {} ): ItemMetadata { - const { volumeData, customIcon, customLabel } = options; + const { volumeData, customIcon, customLabel } = options; - // Handle raw location object (legacy format) - if (isRawLocation(item)) { - const rawItem = item as { name?: string; sd_path?: SdPath }; - const label = customLabel || rawItem.name || "Unnamed Location"; - const path = rawItem.sd_path - ? `/explorer?path=${encodeURIComponent(JSON.stringify(rawItem.sd_path))}` - : null; + // Handle raw location object (legacy format) + if (isRawLocation(item)) { + const rawItem = item as { name?: string; sd_path?: SdPath }; + const label = customLabel || rawItem.name || "Unnamed Location"; + const path = rawItem.sd_path + ? `/explorer?path=${encodeURIComponent(JSON.stringify(rawItem.sd_path))}` + : null; - return { - icon: customIcon - ? { type: "image", icon: customIcon } - : { type: "image", icon: Location }, - label, - path, - }; - } + return { + icon: customIcon + ? { type: "image", icon: customIcon } + : { type: "image", icon: Location }, + label, + path, + }; + } - // Handle proper SpaceItem - const spaceItem = item as SpaceItemType; - const resolvedFile = spaceItem.resolved_file; - const itemSdPath = (spaceItem as SpaceItemType & { sd_path?: SdPath }) - .sd_path; + // Handle proper SpaceItem + const spaceItem = item as SpaceItemType; + const resolvedFile = spaceItem.resolved_file; + const itemSdPath = (spaceItem as SpaceItemType & { sd_path?: SdPath }) + .sd_path; - const icon: IconData = customIcon - ? { type: "image", icon: customIcon } - : getItemIcon(spaceItem.item_type); + const icon: IconData = customIcon + ? { type: "image", icon: customIcon } + : getItemIcon(spaceItem.item_type); - const label = - customLabel || - resolvedFile?.name || - getItemLabel(spaceItem.item_type, resolvedFile); + const label = + customLabel || + resolvedFile?.name || + getItemLabel(spaceItem.item_type, resolvedFile); - const path = getItemPath(spaceItem.item_type, volumeData, itemSdPath); + const path = getItemPath(spaceItem.item_type, volumeData, itemSdPath); - return { icon, label, path }; + return { icon, label, path }; } // Determine if an item can be a drop target (for files to be moved into) export function isDropTargetItem( - item: SpaceItemType | Record, + item: SpaceItemType | Record ): boolean { - if (isRawLocation(item)) return true; + if (isRawLocation(item)) return true; - const spaceItem = item as SpaceItemType; - const itemType = spaceItem.item_type; - const resolvedFile = spaceItem.resolved_file; + const spaceItem = item as SpaceItemType; + const itemType = spaceItem.item_type; + const resolvedFile = spaceItem.resolved_file; - return ( - isLocationItem(itemType) || - isVolumeItem(itemType) || - (isPathItem(itemType) && resolvedFile?.kind === "Directory") - ); + return ( + isLocationItem(itemType) || + isVolumeItem(itemType) || + (isPathItem(itemType) && resolvedFile?.kind === "Directory") + ); } // Get the target type for drop operations export type DropTargetType = "location" | "volume" | "folder" | "other"; export function getDropTargetType( - item: SpaceItemType | Record, + item: SpaceItemType | Record ): DropTargetType { - if (isRawLocation(item)) return "location"; + if (isRawLocation(item)) return "location"; - const spaceItem = item as SpaceItemType; - const itemType = spaceItem.item_type; - const resolvedFile = spaceItem.resolved_file; + const spaceItem = item as SpaceItemType; + const itemType = spaceItem.item_type; + const resolvedFile = spaceItem.resolved_file; - if (isLocationItem(itemType)) return "location"; - if (isVolumeItem(itemType)) return "volume"; - if (isPathItem(itemType) && resolvedFile?.kind === "Directory") - return "folder"; + if (isLocationItem(itemType)) return "location"; + if (isVolumeItem(itemType)) return "volume"; + if (isPathItem(itemType) && resolvedFile?.kind === "Directory") + return "folder"; - return "other"; + return "other"; } // Build target path for drop operations export function buildDropTargetPath( - item: SpaceItemType | Record, - volumeData?: { device_slug: string; mount_path: string }, + item: SpaceItemType | Record, + volumeData?: { device_slug: string; mount_path: string } ): SdPath | undefined { - if (isRawLocation(item)) { - return (item as { sd_path?: SdPath }).sd_path; - } + if (isRawLocation(item)) { + return (item as { sd_path?: SdPath }).sd_path; + } - const spaceItem = item as SpaceItemType; - const itemType = spaceItem.item_type; - const itemSdPath = (spaceItem as SpaceItemType & { sd_path?: SdPath }) - .sd_path; + const spaceItem = item as SpaceItemType; + const itemType = spaceItem.item_type; + const itemSdPath = (spaceItem as SpaceItemType & { sd_path?: SdPath }) + .sd_path; - if (isPathItem(itemType)) { - return itemType.Path.sd_path; - } + if (isPathItem(itemType)) { + return itemType.Path.sd_path; + } - if (isVolumeItem(itemType) && volumeData) { - return { - Physical: { - device_slug: volumeData.device_slug, - path: volumeData.mount_path || "/", - }, - }; - } + if (isVolumeItem(itemType) && volumeData) { + return { + Physical: { + device_slug: volumeData.device_slug, + path: volumeData.mount_path || "/", + }, + }; + } - if (isLocationItem(itemType) && itemSdPath) { - return itemSdPath; - } + if (isLocationItem(itemType) && itemSdPath) { + return itemSdPath; + } - return undefined; + return undefined; } diff --git a/packages/interface/src/components/SpacesSidebar/hooks/useSpaceItemActive.ts b/packages/interface/src/components/SpacesSidebar/hooks/useSpaceItemActive.ts index cadb62342..7fc4c2de1 100644 --- a/packages/interface/src/components/SpacesSidebar/hooks/useSpaceItemActive.ts +++ b/packages/interface/src/components/SpacesSidebar/hooks/useSpaceItemActive.ts @@ -1,11 +1,11 @@ -import { useLocation } from "react-router-dom"; import type { SpaceItem as SpaceItemType } from "@sd/ts-client"; +import { useLocation } from "react-router-dom"; import { useExplorer } from "../../../routes/explorer/context"; interface UseSpaceItemActiveOptions { - item: SpaceItemType; - path: string | null; - hasCustomOnClick: boolean; + item: SpaceItemType; + path: string | null; + hasCustomOnClick: boolean; } /** @@ -17,93 +17,79 @@ interface UseSpaceItemActiveOptions { * - Special routes (/, /recents, etc.) match by exact pathname */ export function useSpaceItemActive({ - item, - path, - hasCustomOnClick, + item, + path, + hasCustomOnClick, }: UseSpaceItemActiveOptions): boolean { - const location = useLocation(); - const { currentView, currentPath } = useExplorer(); + const location = useLocation(); + const { currentView, currentPath } = useExplorer(); - // Items with custom onClick represent virtual views (like device views). - // They should ONLY match via virtual view state, never path-based matching. - if (hasCustomOnClick) { - if (!currentView) return false; + // Items with custom onClick represent virtual views (like device views). + // They should ONLY match via virtual view state, never path-based matching. + if (hasCustomOnClick) { + if (!currentView) return false; - const itemIdStr = String(item.id); - return currentView.view === "device" && currentView.id === itemIdStr; - } + const itemIdStr = String(item.id); + return currentView.view === "device" && currentView.id === itemIdStr; + } - // Check virtual view state for items without custom onClick - if (currentView) { - const itemIdStr = String(item.id); - const isViewMatch = - currentView.view === "device" && currentView.id === itemIdStr; + // Check virtual view state for items without custom onClick + if (currentView) { + const itemIdStr = String(item.id); + const isViewMatch = + currentView.view === "device" && currentView.id === itemIdStr; - if (isViewMatch) return true; + if (isViewMatch) return true; - // When a virtual view is active, regular items should NOT be active - // even if their path happens to match. Virtual views own the display. - return false; - } + // When a virtual view is active, regular items should NOT be active + // even if their path happens to match. Virtual views own the display. + return false; + } - // Check path-based navigation via explorer context - // Only use currentPath matching when we're actually on the explorer route - if ( - location.pathname === "/explorer" && - currentPath && - path && - path.startsWith("/explorer?") - ) { - const itemPathParam = new URLSearchParams(path.split("?")[1]).get( - "path", - ); - if (itemPathParam) { - try { - const itemSdPath = JSON.parse( - decodeURIComponent(itemPathParam), - ); - if ( - JSON.stringify(currentPath) === JSON.stringify(itemSdPath) - ) { - return true; - } - } catch { - // Fall through to URL-based comparison - } - } - } + // Check path-based navigation via explorer context + // Only use currentPath matching when we're actually on the explorer route + if ( + location.pathname === "/explorer" && + currentPath && + path && + path.startsWith("/explorer?") + ) { + const itemPathParam = new URLSearchParams(path.split("?")[1]).get("path"); + if (itemPathParam) { + try { + const itemSdPath = JSON.parse(decodeURIComponent(itemPathParam)); + if (JSON.stringify(currentPath) === JSON.stringify(itemSdPath)) { + return true; + } + } catch { + // Fall through to URL-based comparison + } + } + } - if (!path) return false; + if (!path) return false; - // Special routes (/, /recents, /favorites, etc.): exact pathname match - if (!path.startsWith("/explorer?")) { - return location.pathname === path; - } + // Special routes (/, /recents, /favorites, etc.): exact pathname match + if (!path.startsWith("/explorer?")) { + return location.pathname === path; + } - // Explorer routes: compare SD paths via URL - if (location.pathname === "/explorer") { - const currentSearchParams = new URLSearchParams(location.search); - const currentPathParam = currentSearchParams.get("path"); - const itemPathParam = new URLSearchParams(path.split("?")[1]).get( - "path", - ); + // Explorer routes: compare SD paths via URL + if (location.pathname === "/explorer") { + const currentSearchParams = new URLSearchParams(location.search); + const currentPathParam = currentSearchParams.get("path"); + const itemPathParam = new URLSearchParams(path.split("?")[1]).get("path"); - if (currentPathParam && itemPathParam) { - try { - const currentSdPath = JSON.parse( - decodeURIComponent(currentPathParam), - ); - const itemSdPath = JSON.parse( - decodeURIComponent(itemPathParam), - ); - return ( - JSON.stringify(currentSdPath) === JSON.stringify(itemSdPath) - ); - } catch { - return currentPathParam === itemPathParam; - } - } - } + if (currentPathParam && itemPathParam) { + try { + const currentSdPath = JSON.parse(decodeURIComponent(currentPathParam)); + const itemSdPath = JSON.parse(decodeURIComponent(itemPathParam)); + return JSON.stringify(currentSdPath) === JSON.stringify(itemSdPath); + } catch { + return currentPathParam === itemPathParam; + } + } + } - return false; -} \ No newline at end of file + return false; +} diff --git a/packages/interface/src/components/SpacesSidebar/hooks/useSpaceItemContextMenu.ts b/packages/interface/src/components/SpacesSidebar/hooks/useSpaceItemContextMenu.ts index bb0122622..6d2a4e637 100644 --- a/packages/interface/src/components/SpacesSidebar/hooks/useSpaceItemContextMenu.ts +++ b/packages/interface/src/components/SpacesSidebar/hooks/useSpaceItemContextMenu.ts @@ -1,25 +1,28 @@ -import { useNavigate } from "react-router-dom"; import { - FolderOpen, - MagnifyingGlass, - Trash, - Database, + Database, + FolderOpen, + MagnifyingGlass, + Trash, } from "@phosphor-icons/react"; import type { SpaceItem as SpaceItemType } from "@sd/ts-client"; -import { - useContextMenu, - type ContextMenuItem, - type ContextMenuResult, -} from "../../../hooks/useContextMenu"; +import { useNavigate } from "react-router-dom"; import { usePlatform } from "../../../contexts/PlatformContext"; import { useLibraryMutation } from "../../../contexts/SpacedriveContext"; -import { isVolumeItem, isPathItem } from "./spaceItemUtils"; -import { useExplorer, getSpaceItemKeyFromRoute } from "../../../routes/explorer/context"; +import { + type ContextMenuItem, + type ContextMenuResult, + useContextMenu, +} from "../../../hooks/useContextMenu"; +import { + getSpaceItemKeyFromRoute, + useExplorer, +} from "../../../routes/explorer/context"; +import { isPathItem, isVolumeItem } from "./spaceItemUtils"; interface UseSpaceItemContextMenuOptions { - item: SpaceItemType; - path: string | null; - spaceId?: string; + item: SpaceItemType; + path: string | null; + spaceId?: string; } /** @@ -32,102 +35,101 @@ interface UseSpaceItemContextMenuOptions { * - Remove from Space: Delete the item from the current space */ export function useSpaceItemContextMenu({ - item, - path, - spaceId, + item, + path, + spaceId, }: UseSpaceItemContextMenuOptions): ContextMenuResult { - const navigate = useNavigate(); - const platform = usePlatform(); - const { loadPreferencesForSpaceItem } = useExplorer(); - const deleteItem = useLibraryMutation("spaces.delete_item"); - const indexVolume = useLibraryMutation("volumes.index"); + const navigate = useNavigate(); + const platform = usePlatform(); + const { loadPreferencesForSpaceItem } = useExplorer(); + const deleteItem = useLibraryMutation("spaces.delete_item"); + const indexVolume = useLibraryMutation("volumes.index"); - const items: ContextMenuItem[] = [ - { - icon: FolderOpen, - label: "Open", - onClick: () => { - if (path) { - // Extract pathname and search from the path - const [pathname, search] = path.includes("?") - ? [path.split("?")[0], "?" + path.split("?")[1]] - : [path, ""]; - const spaceItemKey = getSpaceItemKeyFromRoute(pathname, search); - loadPreferencesForSpaceItem(spaceItemKey); - navigate(path); - } - }, - condition: () => !!path, - }, - { - icon: Database, - label: "Index Volume", - onClick: async () => { - if (isVolumeItem(item.item_type)) { - const fingerprint = - (item as SpaceItemType & { fingerprint?: string }) - .fingerprint || item.item_type.Volume.volume_id; + const items: ContextMenuItem[] = [ + { + icon: FolderOpen, + label: "Open", + onClick: () => { + if (path) { + // Extract pathname and search from the path + const [pathname, search] = path.includes("?") + ? [path.split("?")[0], "?" + path.split("?")[1]] + : [path, ""]; + const spaceItemKey = getSpaceItemKeyFromRoute(pathname, search); + loadPreferencesForSpaceItem(spaceItemKey); + navigate(path); + } + }, + condition: () => !!path, + }, + { + icon: Database, + label: "Index Volume", + onClick: async () => { + if (isVolumeItem(item.item_type)) { + const fingerprint = + (item as SpaceItemType & { fingerprint?: string }).fingerprint || + item.item_type.Volume.volume_id; - try { - const result = await indexVolume.mutateAsync({ - fingerprint: fingerprint.toString(), - scope: "Recursive", - }); - console.log("Volume indexed:", result.message); - } catch (err) { - console.error("Failed to index volume:", err); - } - } - }, - condition: () => isVolumeItem(item.item_type), - }, - { type: "separator" }, - { - icon: MagnifyingGlass, - label: "Show in Finder", - onClick: async () => { - if (isPathItem(item.item_type)) { - const sdPath = item.item_type.Path.sd_path; - if (typeof sdPath === "object" && "Physical" in sdPath) { - const physicalPath = ( - sdPath as { Physical: { path: string } } - ).Physical.path; - if (platform.revealFile) { - try { - await platform.revealFile(physicalPath); - } catch (err) { - console.error("Failed to reveal file:", err); - } - } - } - } - }, - keybind: "⌘⇧R", - condition: () => { - if (!isPathItem(item.item_type)) return false; - const sdPath = item.item_type.Path.sd_path; - return ( - typeof sdPath === "object" && - "Physical" in sdPath && - !!platform.revealFile - ); - }, - }, - { type: "separator" }, - { - icon: Trash, - label: "Remove from Space", - onClick: async () => { - try { - await deleteItem.mutateAsync({ item_id: item.id }); - } catch (err) { - console.error("Failed to remove item:", err); - } - }, - variant: "danger" as const, - condition: () => spaceId != null, - }, - ]; + try { + const result = await indexVolume.mutateAsync({ + fingerprint: fingerprint.toString(), + scope: "Recursive", + }); + console.log("Volume indexed:", result.message); + } catch (err) { + console.error("Failed to index volume:", err); + } + } + }, + condition: () => isVolumeItem(item.item_type), + }, + { type: "separator" }, + { + icon: MagnifyingGlass, + label: "Show in Finder", + onClick: async () => { + if (isPathItem(item.item_type)) { + const sdPath = item.item_type.Path.sd_path; + if (typeof sdPath === "object" && "Physical" in sdPath) { + const physicalPath = (sdPath as { Physical: { path: string } }) + .Physical.path; + if (platform.revealFile) { + try { + await platform.revealFile(physicalPath); + } catch (err) { + console.error("Failed to reveal file:", err); + } + } + } + } + }, + keybind: "⌘⇧R", + condition: () => { + if (!isPathItem(item.item_type)) return false; + const sdPath = item.item_type.Path.sd_path; + return ( + typeof sdPath === "object" && + "Physical" in sdPath && + !!platform.revealFile + ); + }, + }, + { type: "separator" }, + { + icon: Trash, + label: "Remove from Space", + onClick: async () => { + try { + await deleteItem.mutateAsync({ item_id: item.id }); + } catch (err) { + console.error("Failed to remove item:", err); + } + }, + variant: "danger" as const, + condition: () => spaceId != null, + }, + ]; - return useContextMenu({ items }); -} \ No newline at end of file + return useContextMenu({ items }); +} diff --git a/packages/interface/src/components/SpacesSidebar/hooks/useSpaceItemDropZones.ts b/packages/interface/src/components/SpacesSidebar/hooks/useSpaceItemDropZones.ts index e37844d64..752d5c91a 100644 --- a/packages/interface/src/components/SpacesSidebar/hooks/useSpaceItemDropZones.ts +++ b/packages/interface/src/components/SpacesSidebar/hooks/useSpaceItemDropZones.ts @@ -1,34 +1,34 @@ -import { useDroppable, useDndContext } from "@dnd-kit/core"; -import type { SpaceItem as SpaceItemType, SdPath } from "@sd/ts-client"; +import { useDndContext, useDroppable } from "@dnd-kit/core"; +import type { SdPath, SpaceItem as SpaceItemType } from "@sd/ts-client"; import { - isDropTargetItem, - getDropTargetType, - buildDropTargetPath, - type DropTargetType, + buildDropTargetPath, + type DropTargetType, + getDropTargetType, + isDropTargetItem, } from "./spaceItemUtils"; interface UseSpaceItemDropZonesOptions { - item: SpaceItemType; - allowInsertion: boolean; - spaceId?: string; - groupId?: string | null; - volumeData?: { device_slug: string; mount_path: string }; + item: SpaceItemType; + allowInsertion: boolean; + spaceId?: string; + groupId?: string | null; + volumeData?: { device_slug: string; mount_path: string }; } interface DropZoneRefs { - setTopRef: (node: HTMLElement | null) => void; - setBottomRef: (node: HTMLElement | null) => void; - setMiddleRef: (node: HTMLElement | null) => void; + setTopRef: (node: HTMLElement | null) => void; + setBottomRef: (node: HTMLElement | null) => void; + setMiddleRef: (node: HTMLElement | null) => void; } interface DropZoneState { - isOverTop: boolean; - isOverBottom: boolean; - isOverMiddle: boolean; - isDropTarget: boolean; - targetType: DropTargetType; - targetPath: SdPath | undefined; - isDraggingSortableItem: boolean; + isOverTop: boolean; + isOverBottom: boolean; + isOverMiddle: boolean; + isDropTarget: boolean; + targetType: DropTargetType; + targetPath: SdPath | undefined; + isDraggingSortableItem: boolean; } export type UseSpaceItemDropZonesResult = DropZoneRefs & DropZoneState; @@ -41,68 +41,68 @@ export type UseSpaceItemDropZonesResult = DropZoneRefs & DropZoneState; * 2. Move-into (file operations): Blue ring for moving files into location/folder */ export function useSpaceItemDropZones({ - item, - allowInsertion, - spaceId, - groupId, - volumeData, + item, + allowInsertion, + spaceId, + groupId, + volumeData, }: UseSpaceItemDropZonesOptions): UseSpaceItemDropZonesResult { - const { active } = useDndContext(); + const { active } = useDndContext(); - // Disable insertion zones when dragging groups or space items (they have 'label' in data) - const isDraggingSortableItem = active?.data?.current?.label != null; + // Disable insertion zones when dragging groups or space items (they have 'label' in data) + const isDraggingSortableItem = active?.data?.current?.label != null; - // Determine if this item can receive file drops - const isDropTarget = isDropTargetItem(item); - const targetType = getDropTargetType(item); - const targetPath = buildDropTargetPath(item, volumeData); + // Determine if this item can receive file drops + const isDropTarget = isDropTargetItem(item); + const targetType = getDropTargetType(item); + const targetPath = buildDropTargetPath(item, volumeData); - // Top zone: insertion above - const { setNodeRef: setTopRef, isOver: isOverTop } = useDroppable({ - id: `space-item-${item.id}-top`, - disabled: !allowInsertion || isDraggingSortableItem, - data: { - action: "insert-before", - itemId: item.id, - spaceId, - groupId, - }, - }); + // Top zone: insertion above + const { setNodeRef: setTopRef, isOver: isOverTop } = useDroppable({ + id: `space-item-${item.id}-top`, + disabled: !allowInsertion || isDraggingSortableItem, + data: { + action: "insert-before", + itemId: item.id, + spaceId, + groupId, + }, + }); - // Bottom zone: insertion below - const { setNodeRef: setBottomRef, isOver: isOverBottom } = useDroppable({ - id: `space-item-${item.id}-bottom`, - disabled: !allowInsertion || isDraggingSortableItem, - data: { - action: "insert-after", - itemId: item.id, - spaceId, - groupId, - }, - }); + // Bottom zone: insertion below + const { setNodeRef: setBottomRef, isOver: isOverBottom } = useDroppable({ + id: `space-item-${item.id}-bottom`, + disabled: !allowInsertion || isDraggingSortableItem, + data: { + action: "insert-after", + itemId: item.id, + spaceId, + groupId, + }, + }); - // Middle zone: drop into folder/location - const { setNodeRef: setMiddleRef, isOver: isOverMiddle } = useDroppable({ - id: `space-item-${item.id}-middle`, - disabled: !isDropTarget || isDraggingSortableItem, - data: { - action: "move-into", - targetType, - targetId: item.id, - targetPath, - }, - }); + // Middle zone: drop into folder/location + const { setNodeRef: setMiddleRef, isOver: isOverMiddle } = useDroppable({ + id: `space-item-${item.id}-middle`, + disabled: !isDropTarget || isDraggingSortableItem, + data: { + action: "move-into", + targetType, + targetId: item.id, + targetPath, + }, + }); - return { - setTopRef, - setBottomRef, - setMiddleRef, - isOverTop, - isOverBottom, - isOverMiddle, - isDropTarget, - targetType, - targetPath, - isDraggingSortableItem, - }; + return { + setTopRef, + setBottomRef, + setMiddleRef, + isOverTop, + isOverBottom, + isOverMiddle, + isDropTarget, + targetType, + targetPath, + isDraggingSortableItem, + }; } diff --git a/packages/interface/src/components/SpacesSidebar/hooks/useSpaces.ts b/packages/interface/src/components/SpacesSidebar/hooks/useSpaces.ts index 029b4689f..6b13a2338 100644 --- a/packages/interface/src/components/SpacesSidebar/hooks/useSpaces.ts +++ b/packages/interface/src/components/SpacesSidebar/hooks/useSpaces.ts @@ -1,87 +1,100 @@ -import { useNormalizedQuery } from '@sd/ts-client'; -import { useSpacedriveClient } from '../../../contexts/SpacedriveContext'; -import { useQueryClient } from '@tanstack/react-query'; -import { useEffect } from 'react'; -import type { Event } from '@sd/ts-client'; +import type { Event } from "@sd/ts-client"; +import { useNormalizedQuery } from "@sd/ts-client"; +import { useQueryClient } from "@tanstack/react-query"; +import { useEffect } from "react"; +import { useSpacedriveClient } from "../../../contexts/SpacedriveContext"; export function useSpaces() { - return useNormalizedQuery({ - wireMethod: 'query:spaces.list', - input: null, // Unit struct serializes as null, not {} - resourceType: 'space', - }); + return useNormalizedQuery({ + wireMethod: "query:spaces.list", + input: null, // Unit struct serializes as null, not {} + resourceType: "space", + }); } export function useSpaceLayout(spaceId: string | null) { - const client = useSpacedriveClient(); - const queryClient = useQueryClient(); - const libraryId = client.getCurrentLibraryId(); + const client = useSpacedriveClient(); + const queryClient = useQueryClient(); + const libraryId = client.getCurrentLibraryId(); - const query = useNormalizedQuery({ - wireMethod: 'query:spaces.get_layout', - input: spaceId ? { space_id: spaceId } : null, - resourceType: 'space_layout', - resourceId: spaceId || undefined, - enabled: !!spaceId, - }); + const query = useNormalizedQuery({ + wireMethod: "query:spaces.get_layout", + input: spaceId ? { space_id: spaceId } : null, + resourceType: "space_layout", + resourceId: spaceId || undefined, + enabled: !!spaceId, + }); - // Subscribe to space_item deletions to update the layout - // (space_item sends its own ResourceDeleted events, separate from space_layout) - useEffect(() => { - if (!spaceId || !libraryId) return; + // Subscribe to space_item deletions to update the layout + // (space_item sends its own ResourceDeleted events, separate from space_layout) + useEffect(() => { + if (!(spaceId && libraryId)) return; - const handleEvent = (event: Event) => { - if (typeof event === 'string') return; + const handleEvent = (event: Event) => { + if (typeof event === "string") return; - if ('ResourceDeleted' in event) { - const { resource_type, resource_id } = (event as any).ResourceDeleted; + if ("ResourceDeleted" in event) { + const { resource_type, resource_id } = (event as any).ResourceDeleted; - if (resource_type === 'space_item') { - console.log('[useSpaceLayout] Space item deleted, updating layout:', resource_id); + if (resource_type === "space_item") { + console.log( + "[useSpaceLayout] Space item deleted, updating layout:", + resource_id + ); - // Remove the item from the layout cache - const queryKey = ['query:spaces.get_layout', libraryId, { space_id: spaceId }]; - queryClient.setQueryData(queryKey, (oldData: any) => { - if (!oldData) return oldData; + // Remove the item from the layout cache + const queryKey = [ + "query:spaces.get_layout", + libraryId, + { space_id: spaceId }, + ]; + queryClient.setQueryData(queryKey, (oldData: any) => { + if (!oldData) return oldData; - // Remove from space_items array - const updatedSpaceItems = oldData.space_items?.filter( - (item: any) => item.id !== resource_id - ) || []; + // Remove from space_items array + const updatedSpaceItems = + oldData.space_items?.filter( + (item: any) => item.id !== resource_id + ) || []; - // Remove from groups - const updatedGroups = oldData.groups?.map((group: any) => ({ - ...group, - items: group.items.filter((item: any) => item.id !== resource_id), - })) || []; + // Remove from groups + const updatedGroups = + oldData.groups?.map((group: any) => ({ + ...group, + items: group.items.filter( + (item: any) => item.id !== resource_id + ), + })) || []; - return { - ...oldData, - space_items: updatedSpaceItems, - groups: updatedGroups, - }; - }); - } - } - }; + return { + ...oldData, + space_items: updatedSpaceItems, + groups: updatedGroups, + }; + }); + } + } + }; - let unsubscribe: (() => void) | undefined; + let unsubscribe: (() => void) | undefined; - client.subscribeFiltered( - { - resource_type: 'space_item', - library_id: libraryId, - include_descendants: false, - }, - handleEvent - ).then((unsub) => { - unsubscribe = unsub; - }); + client + .subscribeFiltered( + { + resource_type: "space_item", + library_id: libraryId, + include_descendants: false, + }, + handleEvent + ) + .then((unsub) => { + unsubscribe = unsub; + }); - return () => { - unsubscribe?.(); - }; - }, [client, queryClient, spaceId, libraryId]); + return () => { + unsubscribe?.(); + }; + }, [client, queryClient, spaceId, libraryId]); - return query; -} \ No newline at end of file + return query; +} diff --git a/packages/interface/src/components/SpacesSidebar/index.tsx b/packages/interface/src/components/SpacesSidebar/index.tsx index f65700888..c94219b20 100644 --- a/packages/interface/src/components/SpacesSidebar/index.tsx +++ b/packages/interface/src/components/SpacesSidebar/index.tsx @@ -1,30 +1,45 @@ -import { useState, useEffect } from "react"; -import { GearSix, Palette, ArrowsClockwise, ListBullets, CircleNotch, ArrowsOut, FunnelSimple } from "@phosphor-icons/react"; -import { useSidebarStore, useLibraryMutation } from "@sd/ts-client"; -import type { SpaceGroup as SpaceGroupType, SpaceItem as SpaceItemType } from "@sd/ts-client"; -import { TopBarButton, Popover, usePopover } from "@sd/ui"; -import { useSpaces, useSpaceLayout } from "./hooks/useSpaces"; -import { SpaceSwitcher } from "./SpaceSwitcher"; -import { SpaceGroup } from "./SpaceGroup"; -import { SpaceItem } from "./SpaceItem"; -import { AddGroupButton } from "./AddGroupButton"; -import { SpaceCustomizationPanel } from "./SpaceCustomizationPanel"; +import { useDndContext, useDroppable } from "@dnd-kit/core"; +import { + SortableContext, + useSortable, + verticalListSortingStrategy, +} from "@dnd-kit/sortable"; +import { CSS } from "@dnd-kit/utilities"; +import { + ArrowsClockwise, + ArrowsOut, + CircleNotch, + FunnelSimple, + GearSix, + ListBullets, + Palette, +} from "@phosphor-icons/react"; +import type { + SpaceGroup as SpaceGroupType, + SpaceItem as SpaceItemType, +} from "@sd/ts-client"; +import { useLibraryMutation, useSidebarStore } from "@sd/ts-client"; +import { Popover, TopBarButton, usePopover } from "@sd/ui"; +import clsx from "clsx"; +import { motion } from "framer-motion"; +import { useEffect, useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { usePlatform } from "../../contexts/PlatformContext"; import { useSpacedriveClient } from "../../contexts/SpacedriveContext"; import { useLibraries } from "../../hooks/useLibraries"; -import { usePlatform } from "../../contexts/PlatformContext"; +import { JobList } from "../JobManager/components/JobList"; import { useJobs } from "../JobManager/hooks/useJobs"; +import { CARD_HEIGHT } from "../JobManager/types"; +import { ActivityFeed } from "../SyncMonitor/components/ActivityFeed"; +import { PeerList } from "../SyncMonitor/components/PeerList"; import { useSyncCount } from "../SyncMonitor/hooks/useSyncCount"; import { useSyncMonitor } from "../SyncMonitor/hooks/useSyncMonitor"; -import { PeerList } from "../SyncMonitor/components/PeerList"; -import { ActivityFeed } from "../SyncMonitor/components/ActivityFeed"; -import { JobList } from "../JobManager/components/JobList"; -import { motion } from "framer-motion"; -import { CARD_HEIGHT } from "../JobManager/types"; -import clsx from "clsx"; -import { useDroppable, useDndContext } from "@dnd-kit/core"; -import { SortableContext, verticalListSortingStrategy, useSortable } from "@dnd-kit/sortable"; -import { CSS } from "@dnd-kit/utilities"; -import { useNavigate } from "react-router-dom"; +import { AddGroupButton } from "./AddGroupButton"; +import { useSpaceLayout, useSpaces } from "./hooks/useSpaces"; +import { SpaceCustomizationPanel } from "./SpaceCustomizationPanel"; +import { SpaceGroup } from "./SpaceGroup"; +import { SpaceItem } from "./SpaceItem"; +import { SpaceSwitcher } from "./SpaceSwitcher"; // Wrapper that adds a space-level drop zone before each group and makes it sortable function SpaceGroupWithDropZone({ @@ -39,16 +54,16 @@ function SpaceGroupWithDropZone({ isFirst: boolean; }) { const { active } = useDndContext(); - + // Disable drop zone when dragging groups or space items (they have 'label' in their data) // This allows sortable collision detection to work for reordering const isDraggingSortableItem = active?.data?.current?.label != null; - + const { setNodeRef: setDropRef, isOver } = useDroppable({ id: `space-root-before-${group.id}`, disabled: !spaceId || isDraggingSortableItem, data: { - action: 'add-to-space', + action: "add-to-space", spaceId, groupId: null, }, @@ -76,19 +91,26 @@ function SpaceGroupWithDropZone({ }; return ( -
    +
    {/* Drop zone before this group (for adding root-level items) */} -
    +
    {isOver && !isDragging && !isDraggingSortableItem && ( -
    +
    )}
    ); @@ -127,12 +149,19 @@ function SyncButton() { return ( + icon={({ className, ...props }) => isSyncing ? ( - + ) : ( ) @@ -140,18 +169,15 @@ function SyncButton() { title="Sync Monitor" /> } - side="top" - align="start" - sideOffset={8} - className="w-[380px] max-h-[520px] z-50 !p-0 !bg-app !rounded-xl" > -
    -

    Sync Monitor

    +
    +

    Sync Monitor

    {onlinePeerCount > 0 && ( - - {onlinePeerCount} {onlinePeerCount === 1 ? "peer" : "peers"} online + + {onlinePeerCount} {onlinePeerCount === 1 ? "peer" : "peers"}{" "} + online )} @@ -162,8 +188,8 @@ function SyncButton() { /> setShowActivityFeed(!showActivityFeed)} title={showActivityFeed ? "Show peers" : "Show activity feed"} /> @@ -172,26 +198,30 @@ function SyncButton() { {popover.open && ( <> -
    +
    -
    - {sync.currentState} +
    + + {sync.currentState} +
    {showActivityFeed ? ( ) : ( - + )} @@ -201,15 +231,15 @@ function SyncButton() { } // Jobs Button with Popover -function JobsButton({ - activeJobCount, - hasRunningJobs, - jobs, - pause, - resume, +function JobsButton({ + activeJobCount, + hasRunningJobs, + jobs, + pause, + resume, cancel, - navigate -}: { + navigate, +}: { activeJobCount: number; hasRunningJobs: boolean; jobs: any[]; @@ -233,12 +263,19 @@ function JobsButton({ return ( + icon={({ className, ...props }) => hasRunningJobs ? ( - + ) : ( ) @@ -246,17 +283,15 @@ function JobsButton({ title="Job Manager" /> } - side="top" - align="start" - sideOffset={8} - className="w-[360px] max-h-[480px] z-50 !p-0 !bg-app !rounded-xl" > -
    -

    Job Manager

    +
    +

    Job Manager

    {activeJobCount > 0 && ( - {activeJobCount} active + + {activeJobCount} active + )} setShowOnlyRunning(!showOnlyRunning)} title={showOnlyRunning ? "Show all jobs" : "Show only active jobs"} /> @@ -276,17 +311,22 @@ function JobsButton({ {popover.open && ( - + )} @@ -302,14 +342,15 @@ export function SpacesSidebar({ isPreviewActive = false }: SpacesSidebarProps) { const platform = usePlatform(); const navigate = useNavigate(); const { data: libraries } = useLibraries(); - const [currentLibraryId, setCurrentLibraryId] = useState( - () => client.getCurrentLibraryId(), + const [currentLibraryId, setCurrentLibraryId] = useState(() => + client.getCurrentLibraryId() ); const [customizePanelOpen, setCustomizePanelOpen] = useState(false); // Get sync and job status for icons const { onlinePeerCount, isSyncing } = useSyncCount(); - const { activeJobCount, hasRunningJobs, jobs, pause, resume, cancel } = useJobs(); + const { activeJobCount, hasRunningJobs, jobs, pause, resume, cancel } = + useJobs(); const { currentSpaceId, setCurrentSpace } = useSidebarStore(); const { data: spacesData } = useSpaces(); @@ -334,9 +375,9 @@ export function SpacesSidebar({ isPreviewActive = false }: SpacesSidebarProps) { // Set library ID via platform (syncs to all windows on Tauri) if (platform.setCurrentLibraryId) { - platform.setCurrentLibraryId(firstLib.id).catch((err) => - console.error("Failed to set library ID:", err), - ); + platform + .setCurrentLibraryId(firstLib.id) + .catch((err) => console.error("Failed to set library ID:", err)); } else { // Web fallback - just update client client.setCurrentLibrary(firstLib.id); @@ -359,39 +400,39 @@ export function SpacesSidebar({ isPreviewActive = false }: SpacesSidebarProps) { const addItem = useLibraryMutation("spaces.add_item"); return ( -
    +
    -
    ); -} \ No newline at end of file +} diff --git a/packages/interface/src/components/SyncMonitor/SyncMonitorPopover.tsx b/packages/interface/src/components/SyncMonitor/SyncMonitorPopover.tsx index ae1d2558f..1dea7abe3 100644 --- a/packages/interface/src/components/SyncMonitor/SyncMonitorPopover.tsx +++ b/packages/interface/src/components/SyncMonitor/SyncMonitorPopover.tsx @@ -1,163 +1,152 @@ import { - ArrowsClockwise, - CircleNotch, - ArrowsOut, - FunnelSimple, + ArrowsClockwise, + ArrowsOut, + CircleNotch, + FunnelSimple, } from "@phosphor-icons/react"; -import { Popover, usePopover, TopBarButton } from "@sd/ui"; +import { Popover, TopBarButton, usePopover } from "@sd/ui"; import clsx from "clsx"; -import { useState, useEffect } from "react"; -import { useNavigate } from "react-router-dom"; import { motion } from "framer-motion"; -import { PeerList } from "./components/PeerList"; +import { useEffect, useState } from "react"; +import { useNavigate } from "react-router-dom"; import { ActivityFeed } from "./components/ActivityFeed"; +import { PeerList } from "./components/PeerList"; import { useSyncCount } from "./hooks/useSyncCount"; import { useSyncMonitor } from "./hooks/useSyncMonitor"; interface SyncMonitorPopoverProps { - className?: string; + className?: string; } export function SyncMonitorPopover({ className }: SyncMonitorPopoverProps) { - const navigate = useNavigate(); - const popover = usePopover(); - const [showActivityFeed, setShowActivityFeed] = useState(false); + const navigate = useNavigate(); + const popover = usePopover(); + const [showActivityFeed, setShowActivityFeed] = useState(false); - const { onlinePeerCount, isSyncing } = useSyncCount(); + const { onlinePeerCount, isSyncing } = useSyncCount(); - useEffect(() => { - if (popover.open) { - setShowActivityFeed(false); - } - }, [popover.open]); + useEffect(() => { + if (popover.open) { + setShowActivityFeed(false); + } + }, [popover.open]); - return ( - -
    - {isSyncing ? ( - - ) : ( - - )} -
    - Sync - {onlinePeerCount > 0 && ( - - {onlinePeerCount} - - )} - - } - side="top" - align="start" - sideOffset={8} - className="w-[380px] max-h-[520px] z-50 !p-0 !bg-app !rounded-xl" - > -
    -

    Sync Monitor

    + return ( + +
    + {isSyncing ? ( + + ) : ( + + )} +
    + Sync + {onlinePeerCount > 0 && ( + + {onlinePeerCount} + + )} + + } + > +
    +

    Sync Monitor

    -
    - {onlinePeerCount > 0 && ( - - {onlinePeerCount}{" "} - {onlinePeerCount === 1 ? "peer" : "peers"} online - - )} +
    + {onlinePeerCount > 0 && ( + + {onlinePeerCount} {onlinePeerCount === 1 ? "peer" : "peers"}{" "} + online + + )} - navigate("/sync")} - title="Open full sync monitor" - /> + navigate("/sync")} + title="Open full sync monitor" + /> - setShowActivityFeed(!showActivityFeed)} - title={ - showActivityFeed - ? "Show peers" - : "Show activity feed" - } - /> -
    -
    + setShowActivityFeed(!showActivityFeed)} + title={showActivityFeed ? "Show peers" : "Show activity feed"} + /> +
    +
    - {popover.open && ( - - )} -
    - ); + {popover.open && ( + + )} + + ); } function SyncMonitorContent({ - showActivityFeed, + showActivityFeed, }: { - showActivityFeed: boolean; + showActivityFeed: boolean; }) { - const sync = useSyncMonitor(); + const sync = useSyncMonitor(); - const getStateColor = (state: string) => { - switch (state) { - case "Ready": - return "bg-green-500"; - case "Backfilling": - return "bg-yellow-500"; - case "CatchingUp": - return "bg-accent"; - case "Uninitialized": - return "bg-ink-faint"; - case "Paused": - return "bg-ink-dull"; - default: - return "bg-ink-faint"; - } - }; + const getStateColor = (state: string) => { + switch (state) { + case "Ready": + return "bg-green-500"; + case "Backfilling": + return "bg-yellow-500"; + case "CatchingUp": + return "bg-accent"; + case "Uninitialized": + return "bg-ink-faint"; + case "Paused": + return "bg-ink-dull"; + default: + return "bg-ink-faint"; + } + }; - return ( - <> -
    -
    -
    - - {sync.currentState} - -
    -
    - - {showActivityFeed ? ( - - ) : ( - - )} - - - ); + return ( + <> +
    +
    +
    + + {sync.currentState} + +
    +
    + + {showActivityFeed ? ( + + ) : ( + + )} + + + ); } diff --git a/packages/interface/src/components/SyncMonitor/components/ActivityFeed.tsx b/packages/interface/src/components/SyncMonitor/components/ActivityFeed.tsx index 31f126b79..92b4d2f23 100644 --- a/packages/interface/src/components/SyncMonitor/components/ActivityFeed.tsx +++ b/packages/interface/src/components/SyncMonitor/components/ActivityFeed.tsx @@ -1,99 +1,90 @@ import { - ArrowsClockwise, - ArrowDown, - CheckCircle, - Warning, - PlugsConnected, - Circle, + ArrowDown, + ArrowsClockwise, + CheckCircle, + Circle, + PlugsConnected, + Warning, } from "@phosphor-icons/react"; import clsx from "clsx"; import type { SyncActivity } from "../types"; import { timeAgo } from "../utils"; interface ActivityFeedProps { - activities: SyncActivity[]; + activities: SyncActivity[]; } export function ActivityFeed({ activities }: ActivityFeedProps) { - if (activities.length === 0) { - return ( -
    - -

    - No recent activity -

    -

    - Activity will appear here when syncing -

    -
    - ); - } + if (activities.length === 0) { + return ( +
    + +

    No recent activity

    +

    + Activity will appear here when syncing +

    +
    + ); + } - return ( -
    - {activities.map((activity, index) => ( - - ))} -
    - ); + return ( +
    + {activities.map((activity, index) => ( + + ))} +
    + ); } function ActivityItem({ activity }: { activity: SyncActivity }) { - const getIcon = () => { - switch (activity.eventType) { - case "broadcast": - return ; - case "received": - return ; - case "applied": - return ; - case "backfill": - return ; - case "connection": - return ; - case "error": - return ; - default: - return ; - } - }; + const getIcon = () => { + switch (activity.eventType) { + case "broadcast": + return ; + case "received": + return ; + case "applied": + return ; + case "backfill": + return ; + case "connection": + return ; + case "error": + return ; + default: + return ; + } + }; - const getIconColor = () => { - switch (activity.eventType) { - case "broadcast": - return "text-accent"; - case "received": - return "text-accent"; - case "applied": - return "text-green-500"; - case "backfill": - return "text-purple-500"; - case "connection": - return "text-ink-dull"; - case "error": - return "text-red-500"; - default: - return "text-ink-faint"; - } - }; + const getIconColor = () => { + switch (activity.eventType) { + case "broadcast": + return "text-accent"; + case "received": + return "text-accent"; + case "applied": + return "text-green-500"; + case "backfill": + return "text-purple-500"; + case "connection": + return "text-ink-dull"; + case "error": + return "text-red-500"; + default: + return "text-ink-faint"; + } + }; - return ( -
    -
    {getIcon()}
    -
    -

    - {activity.description} -

    -

    - {timeAgo(activity.timestamp)} -

    -
    -
    - ); + return ( +
    +
    {getIcon()}
    +
    +

    {activity.description}

    +

    {timeAgo(activity.timestamp)}

    +
    +
    + ); } diff --git a/packages/interface/src/components/SyncMonitor/components/PeerList.tsx b/packages/interface/src/components/SyncMonitor/components/PeerList.tsx index 310c20a70..347349c3a 100644 --- a/packages/interface/src/components/SyncMonitor/components/PeerList.tsx +++ b/packages/interface/src/components/SyncMonitor/components/PeerList.tsx @@ -11,10 +11,10 @@ interface PeerListProps { export function PeerList({ peers }: PeerListProps) { if (peers.length === 0) { return ( -
    - -

    No paired devices

    -

    +

    + +

    No paired devices

    +

    Pair a device to start syncing

    @@ -38,21 +38,21 @@ function PeerCard({ peer }: { peer: SyncPeerActivity }) { }; return ( -
    -
    +
    +
    - + {peer.deviceName}
    - {peer.watermarkLagMs && peer.watermarkLagMs > 60000 && ( + {peer.watermarkLagMs && peer.watermarkLagMs > 60_000 && ( )} @@ -60,12 +60,12 @@ function PeerCard({ peer }: { peer: SyncPeerActivity }) {
    -
    -
    +
    +
    {formatBytes(peer.bytesReceived)} received
    -
    +
    {peer.entriesReceived.toLocaleString()} changes
    diff --git a/packages/interface/src/components/SyncMonitor/hooks/useSyncCount.ts b/packages/interface/src/components/SyncMonitor/hooks/useSyncCount.ts index 07b0609ff..d17e5e473 100644 --- a/packages/interface/src/components/SyncMonitor/hooks/useSyncCount.ts +++ b/packages/interface/src/components/SyncMonitor/hooks/useSyncCount.ts @@ -1,27 +1,27 @@ -import { useState, useEffect } from 'react'; -import { useLibraryQuery } from '../../../contexts/SpacedriveContext'; +import { useEffect, useState } from "react"; +import { useLibraryQuery } from "../../../contexts/SpacedriveContext"; export function useSyncCount() { - const [onlinePeerCount, setOnlinePeerCount] = useState(0); - const [isSyncing, setIsSyncing] = useState(false); + const [onlinePeerCount, setOnlinePeerCount] = useState(0); + const [isSyncing, setIsSyncing] = useState(false); - const { data } = useLibraryQuery({ - type: 'sync.activity', - input: {}, - }); + const { data } = useLibraryQuery({ + type: "sync.activity", + input: {}, + }); - useEffect(() => { - if (data) { - const onlineCount = data.peers.filter((p) => p.isOnline).length; - setOnlinePeerCount(onlineCount); + useEffect(() => { + if (data) { + const onlineCount = data.peers.filter((p) => p.isOnline).length; + setOnlinePeerCount(onlineCount); - const state = data.currentState; - const syncing = - (typeof state === 'object' && ('Backfilling' in state || 'CatchingUp' in state)) || - false; - setIsSyncing(syncing); - } - }, [data]); + const state = data.currentState; + const syncing = + typeof state === "object" && + ("Backfilling" in state || "CatchingUp" in state); + setIsSyncing(syncing); + } + }, [data]); - return { onlinePeerCount, isSyncing }; -} \ No newline at end of file + return { onlinePeerCount, isSyncing }; +} diff --git a/packages/interface/src/components/SyncMonitor/hooks/useSyncMonitor.ts b/packages/interface/src/components/SyncMonitor/hooks/useSyncMonitor.ts index e9aa611ef..696a3fd0a 100644 --- a/packages/interface/src/components/SyncMonitor/hooks/useSyncMonitor.ts +++ b/packages/interface/src/components/SyncMonitor/hooks/useSyncMonitor.ts @@ -1,187 +1,191 @@ -import { useState, useEffect, useRef } from 'react'; -import { useLibraryQuery, useSpacedriveClient } from '../../../contexts/SpacedriveContext'; -import type { SyncPeerActivity, SyncActivity, SyncState } from '../types'; +import { useEffect, useRef, useState } from "react"; +import { + useLibraryQuery, + useSpacedriveClient, +} from "../../../contexts/SpacedriveContext"; +import type { SyncActivity, SyncPeerActivity, SyncState } from "../types"; interface SyncMonitorState { - currentState: SyncState; - peers: SyncPeerActivity[]; - recentActivity: SyncActivity[]; - errorCount: number; - hasActivity: boolean; + currentState: SyncState; + peers: SyncPeerActivity[]; + recentActivity: SyncActivity[]; + errorCount: number; + hasActivity: boolean; } export function useSyncMonitor() { - const [state, setState] = useState({ - currentState: 'Uninitialized', - peers: [], - recentActivity: [], - errorCount: 0, - hasActivity: false, - }); + const [state, setState] = useState({ + currentState: "Uninitialized", + peers: [], + recentActivity: [], + errorCount: 0, + hasActivity: false, + }); - const client = useSpacedriveClient(); + const client = useSpacedriveClient(); - const { data, refetch } = useLibraryQuery({ - type: 'sync.activity', - input: {}, - }); + const { data, refetch } = useLibraryQuery({ + type: "sync.activity", + input: {}, + }); - const refetchRef = useRef(refetch); - useEffect(() => { - refetchRef.current = refetch; - }, [refetch]); + const refetchRef = useRef(refetch); + useEffect(() => { + refetchRef.current = refetch; + }, [refetch]); - useEffect(() => { - if (data) { - const stateValue = data.currentState; - let normalizedState: SyncState; + useEffect(() => { + if (data) { + const stateValue = data.currentState; + let normalizedState: SyncState; - if (typeof stateValue === 'string') { - normalizedState = stateValue as SyncState; - } else if (typeof stateValue === 'object' && stateValue !== null) { - if ('Backfilling' in stateValue) { - normalizedState = 'Backfilling'; - } else if ('CatchingUp' in stateValue) { - normalizedState = 'CatchingUp'; - } else { - normalizedState = 'Uninitialized'; - } - } else { - normalizedState = 'Uninitialized'; - } + if (typeof stateValue === "string") { + normalizedState = stateValue as SyncState; + } else if (typeof stateValue === "object" && stateValue !== null) { + if ("Backfilling" in stateValue) { + normalizedState = "Backfilling"; + } else if ("CatchingUp" in stateValue) { + normalizedState = "CatchingUp"; + } else { + normalizedState = "Uninitialized"; + } + } else { + normalizedState = "Uninitialized"; + } - setState((prev) => ({ - ...prev, - currentState: normalizedState, - peers: data.peers.map((p) => ({ - deviceId: p.deviceId, - deviceName: p.deviceName, - isOnline: p.isOnline, - lastSeen: p.lastSeen, - entriesReceived: p.entriesReceived, - bytesReceived: p.bytesReceived, - bytesSent: p.bytesSent, - watermarkLagMs: p.watermarkLagMs, - })), - errorCount: data.errorCount, - hasActivity: data.peers.some((p) => p.isOnline), - })); - } - }, [data]); + setState((prev) => ({ + ...prev, + currentState: normalizedState, + peers: data.peers.map((p) => ({ + deviceId: p.deviceId, + deviceName: p.deviceName, + isOnline: p.isOnline, + lastSeen: p.lastSeen, + entriesReceived: p.entriesReceived, + bytesReceived: p.bytesReceived, + bytesSent: p.bytesSent, + watermarkLagMs: p.watermarkLagMs, + })), + errorCount: data.errorCount, + hasActivity: data.peers.some((p) => p.isOnline), + })); + } + }, [data]); - useEffect(() => { - if (!client) return; + useEffect(() => { + if (!client) return; - let unsubscribe: (() => void) | undefined; - let isCancelled = false; + let unsubscribe: (() => void) | undefined; + let isCancelled = false; - const handleEvent = (event: any) => { - if ('SyncStateChanged' in event) { - const { newState } = event.SyncStateChanged; - setState((prev) => ({ ...prev, currentState: newState })); - } else if ('SyncActivity' in event) { - const activity = event.SyncActivity; - const activityType = activity.activityType; + const handleEvent = (event: any) => { + if ("SyncStateChanged" in event) { + const { newState } = event.SyncStateChanged; + setState((prev) => ({ ...prev, currentState: newState })); + } else if ("SyncActivity" in event) { + const activity = event.SyncActivity; + const activityType = activity.activityType; - let eventType: SyncActivity['eventType'] = 'broadcast'; - let description = 'Activity'; + let eventType: SyncActivity["eventType"] = "broadcast"; + let description = "Activity"; - if ('BroadcastSent' in activityType) { - eventType = 'broadcast'; - description = `Broadcast ${activityType.BroadcastSent.changes} changes`; - } else if ('ChangesReceived' in activityType) { - eventType = 'received'; - description = `Received ${activityType.ChangesReceived.changes} changes`; - } else if ('ChangesApplied' in activityType) { - eventType = 'applied'; - description = `Applied ${activityType.ChangesApplied.changes} changes`; - } else if ('BackfillStarted' in activityType) { - eventType = 'backfill'; - description = 'Backfill started'; - } else if ('BackfillCompleted' in activityType) { - eventType = 'backfill'; - description = `Backfill completed (${activityType.BackfillCompleted.records} records)`; - } else if ('CatchUpStarted' in activityType) { - eventType = 'backfill'; - description = 'Catch-up started'; - } else if ('CatchUpCompleted' in activityType) { - eventType = 'backfill'; - description = 'Catch-up completed'; - } + if ("BroadcastSent" in activityType) { + eventType = "broadcast"; + description = `Broadcast ${activityType.BroadcastSent.changes} changes`; + } else if ("ChangesReceived" in activityType) { + eventType = "received"; + description = `Received ${activityType.ChangesReceived.changes} changes`; + } else if ("ChangesApplied" in activityType) { + eventType = "applied"; + description = `Applied ${activityType.ChangesApplied.changes} changes`; + } else if ("BackfillStarted" in activityType) { + eventType = "backfill"; + description = "Backfill started"; + } else if ("BackfillCompleted" in activityType) { + eventType = "backfill"; + description = `Backfill completed (${activityType.BackfillCompleted.records} records)`; + } else if ("CatchUpStarted" in activityType) { + eventType = "backfill"; + description = "Catch-up started"; + } else if ("CatchUpCompleted" in activityType) { + eventType = "backfill"; + description = "Catch-up completed"; + } - setState((prev) => ({ - ...prev, - recentActivity: [ - { - timestamp: activity.timestamp, - eventType, - peerDeviceId: activity.peerDeviceId, - description, - }, - ...prev.recentActivity.slice(0, 49), - ], - })); - } else if ('SyncConnectionChanged' in event) { - const { peerDeviceId, peerName, connected } = event.SyncConnectionChanged; + setState((prev) => ({ + ...prev, + recentActivity: [ + { + timestamp: activity.timestamp, + eventType, + peerDeviceId: activity.peerDeviceId, + description, + }, + ...prev.recentActivity.slice(0, 49), + ], + })); + } else if ("SyncConnectionChanged" in event) { + const { peerDeviceId, peerName, connected } = + event.SyncConnectionChanged; - setState((prev) => ({ - ...prev, - peers: prev.peers.map((p) => - p.deviceId === peerDeviceId ? { ...p, isOnline: connected } : p - ), - hasActivity: connected || prev.peers.some((p) => p.isOnline), - recentActivity: [ - { - timestamp: event.SyncConnectionChanged.timestamp, - eventType: 'connection', - peerDeviceId, - description: `${peerName} ${connected ? 'connected' : 'disconnected'}`, - }, - ...prev.recentActivity.slice(0, 49), - ], - })); - } else if ('SyncError' in event) { - const { message } = event.SyncError; - setState((prev) => ({ - ...prev, - errorCount: prev.errorCount + 1, - recentActivity: [ - { - timestamp: event.SyncError.timestamp, - eventType: 'error', - peerDeviceId: event.SyncError.peerDeviceId, - description: message, - }, - ...prev.recentActivity.slice(0, 49), - ], - })); - } else { - refetchRef.current(); - } - }; + setState((prev) => ({ + ...prev, + peers: prev.peers.map((p) => + p.deviceId === peerDeviceId ? { ...p, isOnline: connected } : p + ), + hasActivity: connected || prev.peers.some((p) => p.isOnline), + recentActivity: [ + { + timestamp: event.SyncConnectionChanged.timestamp, + eventType: "connection", + peerDeviceId, + description: `${peerName} ${connected ? "connected" : "disconnected"}`, + }, + ...prev.recentActivity.slice(0, 49), + ], + })); + } else if ("SyncError" in event) { + const { message } = event.SyncError; + setState((prev) => ({ + ...prev, + errorCount: prev.errorCount + 1, + recentActivity: [ + { + timestamp: event.SyncError.timestamp, + eventType: "error", + peerDeviceId: event.SyncError.peerDeviceId, + description: message, + }, + ...prev.recentActivity.slice(0, 49), + ], + })); + } else { + refetchRef.current(); + } + }; - const filter = { - event_types: [ - 'SyncStateChanged', - 'SyncActivity', - 'SyncConnectionChanged', - 'SyncError', - ], - }; + const filter = { + event_types: [ + "SyncStateChanged", + "SyncActivity", + "SyncConnectionChanged", + "SyncError", + ], + }; - client.subscribeFiltered(filter, handleEvent).then((unsub) => { - if (isCancelled) { - unsub(); - } else { - unsubscribe = unsub; - } - }); + client.subscribeFiltered(filter, handleEvent).then((unsub) => { + if (isCancelled) { + unsub(); + } else { + unsubscribe = unsub; + } + }); - return () => { - isCancelled = true; - unsubscribe?.(); - }; - }, [client]); + return () => { + isCancelled = true; + unsubscribe?.(); + }; + }, [client]); - return state; -} \ No newline at end of file + return state; +} diff --git a/packages/interface/src/components/SyncMonitor/index.ts b/packages/interface/src/components/SyncMonitor/index.ts index 481f85889..9965871e6 100644 --- a/packages/interface/src/components/SyncMonitor/index.ts +++ b/packages/interface/src/components/SyncMonitor/index.ts @@ -1,4 +1,4 @@ -export { SyncMonitorPopover } from './SyncMonitorPopover'; -export { useSyncMonitor } from './hooks/useSyncMonitor'; -export { useSyncCount } from './hooks/useSyncCount'; -export type { SyncPeerActivity, SyncActivity, SyncState } from './types'; +export { useSyncCount } from "./hooks/useSyncCount"; +export { useSyncMonitor } from "./hooks/useSyncMonitor"; +export { SyncMonitorPopover } from "./SyncMonitorPopover"; +export type { SyncActivity, SyncPeerActivity, SyncState } from "./types"; diff --git a/packages/interface/src/components/SyncMonitor/types.ts b/packages/interface/src/components/SyncMonitor/types.ts index 93f9d426e..66c39e30b 100644 --- a/packages/interface/src/components/SyncMonitor/types.ts +++ b/packages/interface/src/components/SyncMonitor/types.ts @@ -1,22 +1,33 @@ export interface SyncPeerActivity { - deviceId: string; - deviceName: string; - isOnline: boolean; - lastSeen: string; - entriesReceived: number; - bytesReceived: number; - bytesSent: number; - watermarkLagMs?: number; + deviceId: string; + deviceName: string; + isOnline: boolean; + lastSeen: string; + entriesReceived: number; + bytesReceived: number; + bytesSent: number; + watermarkLagMs?: number; } export interface SyncActivity { - timestamp: string; - eventType: 'broadcast' | 'received' | 'applied' | 'backfill' | 'error' | 'connection'; - peerDeviceId?: string; - description: string; + timestamp: string; + eventType: + | "broadcast" + | "received" + | "applied" + | "backfill" + | "error" + | "connection"; + peerDeviceId?: string; + description: string; } -export type SyncState = 'Uninitialized' | 'Backfilling' | 'CatchingUp' | 'Ready' | 'Paused'; +export type SyncState = + | "Uninitialized" + | "Backfilling" + | "CatchingUp" + | "Ready" + | "Paused"; export const PEER_CARD_HEIGHT = 80; export const ACTIVITY_HEIGHT = 40; diff --git a/packages/interface/src/components/SyncMonitor/utils.ts b/packages/interface/src/components/SyncMonitor/utils.ts index f1b194b15..148d8f674 100644 --- a/packages/interface/src/components/SyncMonitor/utils.ts +++ b/packages/interface/src/components/SyncMonitor/utils.ts @@ -2,22 +2,22 @@ * Formats a date to time ago (e.g., "2m ago", "1h ago") */ export function timeAgo(date: string | Date | undefined): string { - if (!date) return '—'; + if (!date) return "—"; - const now = new Date(); - const past = typeof date === 'string' ? new Date(date) : date; + const now = new Date(); + const past = typeof date === "string" ? new Date(date) : date; - // Check if date is valid - if (isNaN(past.getTime())) return '—'; + // Check if date is valid + if (isNaN(past.getTime())) return "—"; - const diffMs = now.getTime() - past.getTime(); - const diffSeconds = Math.floor(diffMs / 1000); - const diffMinutes = Math.floor(diffSeconds / 60); - const diffHours = Math.floor(diffMinutes / 60); - const diffDays = Math.floor(diffHours / 24); + const diffMs = now.getTime() - past.getTime(); + const diffSeconds = Math.floor(diffMs / 1000); + const diffMinutes = Math.floor(diffSeconds / 60); + const diffHours = Math.floor(diffMinutes / 60); + const diffDays = Math.floor(diffHours / 24); - if (diffDays > 0) return `${diffDays}d ago`; - if (diffHours > 0) return `${diffHours}h ago`; - if (diffMinutes > 0) return `${diffMinutes}m ago`; - return 'just now'; + if (diffDays > 0) return `${diffDays}d ago`; + if (diffHours > 0) return `${diffHours}h ago`; + if (diffMinutes > 0) return `${diffMinutes}m ago`; + return "just now"; } diff --git a/packages/interface/src/components/TabManager/TabBar.tsx b/packages/interface/src/components/TabManager/TabBar.tsx index 2d98fe26d..df7c9c3bd 100644 --- a/packages/interface/src/components/TabManager/TabBar.tsx +++ b/packages/interface/src/components/TabManager/TabBar.tsx @@ -1,85 +1,82 @@ -import clsx from "clsx"; -import { motion, LayoutGroup } from "framer-motion"; import { Plus, X } from "@phosphor-icons/react"; -import { useTabManager } from "./useTabManager"; +import clsx from "clsx"; +import { LayoutGroup, motion } from "framer-motion"; import { useMemo } from "react"; +import { useTabManager } from "./useTabManager"; export function TabBar() { - const { tabs, activeTabId, switchTab, closeTab, createTab } = - useTabManager(); + const { tabs, activeTabId, switchTab, closeTab, createTab } = useTabManager(); - // Don't show tab bar if only one tab - if (tabs.length <= 1) { - return null; - } + // Don't show tab bar if only one tab + if (tabs.length <= 1) { + return null; + } - // Ensure activeTabId exists in tabs array, fallback to first tab - // Memoize to prevent unnecessary rerenders during rapid state updates - const safeActiveTabId = useMemo(() => { - return tabs.find((t) => t.id === activeTabId)?.id ?? tabs[0]?.id; - }, [tabs, activeTabId]); + // Ensure activeTabId exists in tabs array, fallback to first tab + // Memoize to prevent unnecessary rerenders during rapid state updates + const safeActiveTabId = useMemo(() => { + return tabs.find((t) => t.id === activeTabId)?.id ?? tabs[0]?.id; + }, [tabs, activeTabId]); - return ( -
    - -
    - {tabs.map((tab) => { - const isActive = tab.id === safeActiveTabId; + return ( +
    + +
    + {tabs.map((tab) => { + const isActive = tab.id === safeActiveTabId; - return ( - - ); - })} -
    -
    - -
    - ); + return ( + + ); + })} +
    +
    + +
    + ); } diff --git a/packages/interface/src/components/TabManager/TabDefaultsSync.tsx b/packages/interface/src/components/TabManager/TabDefaultsSync.tsx index 700e33be1..0cb603c17 100644 --- a/packages/interface/src/components/TabManager/TabDefaultsSync.tsx +++ b/packages/interface/src/components/TabManager/TabDefaultsSync.tsx @@ -1,7 +1,7 @@ +import type { Device, ListLibraryDevicesInput } from "@sd/ts-client"; import { useEffect, useMemo } from "react"; import { useNormalizedQuery } from "../../contexts/SpacedriveContext"; import { useTabManager } from "./useTabManager"; -import type { ListLibraryDevicesInput, Device } from "@sd/ts-client"; /** * TabDefaultsSync - Sets the default new tab path to the current device @@ -10,30 +10,30 @@ import type { ListLibraryDevicesInput, Device } from "@sd/ts-client"; * default path so new tabs open to the device's virtual view. */ export function TabDefaultsSync() { - const { setDefaultNewTabPath } = useTabManager(); + const { setDefaultNewTabPath } = useTabManager(); - // Fetch all devices and find the current one - const { data: devices } = useNormalizedQuery< - ListLibraryDevicesInput, - Device[] - >({ - wireMethod: "query:devices.list", - input: { include_offline: true, include_details: false }, - resourceType: "device", - }); + // Fetch all devices and find the current one + const { data: devices } = useNormalizedQuery< + ListLibraryDevicesInput, + Device[] + >({ + wireMethod: "query:devices.list", + input: { include_offline: true, include_details: false }, + resourceType: "device", + }); - // Find the current device - const currentDevice = useMemo(() => { - return devices?.find((d) => d.is_current) ?? null; - }, [devices]); + // Find the current device + const currentDevice = useMemo(() => { + return devices?.find((d) => d.is_current) ?? null; + }, [devices]); - // Set default new tab path when current device is known - useEffect(() => { - if (currentDevice?.id) { - const deviceViewPath = `/explorer?view=device&id=${currentDevice.id}`; - setDefaultNewTabPath(deviceViewPath); - } - }, [currentDevice?.id, setDefaultNewTabPath]); + // Set default new tab path when current device is known + useEffect(() => { + if (currentDevice?.id) { + const deviceViewPath = `/explorer?view=device&id=${currentDevice.id}`; + setDefaultNewTabPath(deviceViewPath); + } + }, [currentDevice?.id, setDefaultNewTabPath]); - return null; -} \ No newline at end of file + return null; +} diff --git a/packages/interface/src/components/TabManager/TabKeyboardHandler.tsx b/packages/interface/src/components/TabManager/TabKeyboardHandler.tsx index befa36e01..7a91eab60 100644 --- a/packages/interface/src/components/TabManager/TabKeyboardHandler.tsx +++ b/packages/interface/src/components/TabManager/TabKeyboardHandler.tsx @@ -1,5 +1,5 @@ -import { useTabManager } from "./useTabManager"; import { useKeybind } from "../../hooks/useKeybind"; +import { useTabManager } from "./useTabManager"; /** * TabKeyboardHandler - Handles keyboard shortcuts for tab operations @@ -7,45 +7,52 @@ import { useKeybind } from "../../hooks/useKeybind"; * Uses the keybind system to listen for tab-related shortcuts and trigger actions. */ export function TabKeyboardHandler() { - const { createTab, closeTab, nextTab, previousTab, selectTabAtIndex, tabs, activeTabId } = - useTabManager(); + const { + createTab, + closeTab, + nextTab, + previousTab, + selectTabAtIndex, + tabs, + activeTabId, + } = useTabManager(); - // New Tab (Cmd+T) - useKeybind("global.newTab", () => { - createTab(); - }); + // New Tab (Cmd+T) + useKeybind("global.newTab", () => { + createTab(); + }); - // Close Tab (Cmd+W) - useKeybind( - "global.closeTab", - () => { - if (tabs.length > 1) { - closeTab(activeTabId); - } - }, - { enabled: tabs.length > 1 }, - ); + // Close Tab (Cmd+W) + useKeybind( + "global.closeTab", + () => { + if (tabs.length > 1) { + closeTab(activeTabId); + } + }, + { enabled: tabs.length > 1 } + ); - // Next Tab (Cmd+Shift+]) - useKeybind("global.nextTab", () => { - nextTab(); - }); + // Next Tab (Cmd+Shift+]) + useKeybind("global.nextTab", () => { + nextTab(); + }); - // Previous Tab (Cmd+Shift+[) - useKeybind("global.previousTab", () => { - previousTab(); - }); + // Previous Tab (Cmd+Shift+[) + useKeybind("global.previousTab", () => { + previousTab(); + }); - // Select Tab 1-9 (Cmd+1-9) - useKeybind("global.selectTab1", () => selectTabAtIndex(0)); - useKeybind("global.selectTab2", () => selectTabAtIndex(1)); - useKeybind("global.selectTab3", () => selectTabAtIndex(2)); - useKeybind("global.selectTab4", () => selectTabAtIndex(3)); - useKeybind("global.selectTab5", () => selectTabAtIndex(4)); - useKeybind("global.selectTab6", () => selectTabAtIndex(5)); - useKeybind("global.selectTab7", () => selectTabAtIndex(6)); - useKeybind("global.selectTab8", () => selectTabAtIndex(7)); - useKeybind("global.selectTab9", () => selectTabAtIndex(8)); + // Select Tab 1-9 (Cmd+1-9) + useKeybind("global.selectTab1", () => selectTabAtIndex(0)); + useKeybind("global.selectTab2", () => selectTabAtIndex(1)); + useKeybind("global.selectTab3", () => selectTabAtIndex(2)); + useKeybind("global.selectTab4", () => selectTabAtIndex(3)); + useKeybind("global.selectTab5", () => selectTabAtIndex(4)); + useKeybind("global.selectTab6", () => selectTabAtIndex(5)); + useKeybind("global.selectTab7", () => selectTabAtIndex(6)); + useKeybind("global.selectTab8", () => selectTabAtIndex(7)); + useKeybind("global.selectTab9", () => selectTabAtIndex(8)); - return null; + return null; } diff --git a/packages/interface/src/components/TabManager/TabManagerContext.tsx b/packages/interface/src/components/TabManager/TabManagerContext.tsx index c90fb498d..b705a9792 100644 --- a/packages/interface/src/components/TabManager/TabManagerContext.tsx +++ b/packages/interface/src/components/TabManager/TabManagerContext.tsx @@ -1,61 +1,62 @@ import { - createContext, - useState, - useCallback, - useMemo, - type ReactNode, + createContext, + type ReactNode, + useCallback, + useMemo, + useState, } from "react"; import { createBrowserRouter, type RouteObject } from "react-router-dom"; + type Router = ReturnType; /** * Derives a tab title from the current route pathname and search params */ function deriveTitleFromPath(pathname: string, search: string): string { - const routeTitles: Record = { - "/": "Overview", - "/favorites": "Favorites", - "/recents": "Recents", - "/file-kinds": "File Kinds", - "/search": "Search", - "/jobs": "Jobs", - "/daemon": "Daemon", - }; + const routeTitles: Record = { + "/": "Overview", + "/favorites": "Favorites", + "/recents": "Recents", + "/file-kinds": "File Kinds", + "/search": "Search", + "/jobs": "Jobs", + "/daemon": "Daemon", + }; - if (routeTitles[pathname]) { - return routeTitles[pathname]; - } + if (routeTitles[pathname]) { + return routeTitles[pathname]; + } - if (pathname.startsWith("/tag/")) { - const tagId = pathname.split("/")[2]; - return tagId ? `Tag: ${tagId.slice(0, 8)}...` : "Tag"; - } + if (pathname.startsWith("/tag/")) { + const tagId = pathname.split("/")[2]; + return tagId ? `Tag: ${tagId.slice(0, 8)}...` : "Tag"; + } - if (pathname === "/explorer" && search) { - const params = new URLSearchParams(search); + if (pathname === "/explorer" && search) { + const params = new URLSearchParams(search); - const view = params.get("view"); - if (view === "device") { - return "This Device"; - } + const view = params.get("view"); + if (view === "device") { + return "This Device"; + } - const pathParam = params.get("path"); - if (pathParam) { - try { - const sdPath = JSON.parse(decodeURIComponent(pathParam)); - if (sdPath?.Physical?.path) { - const fullPath = sdPath.Physical.path as string; - const parts = fullPath.split("/").filter(Boolean); - return parts[parts.length - 1] || "Explorer"; - } - } catch { - // Fall through - } - } - return "Explorer"; - } + const pathParam = params.get("path"); + if (pathParam) { + try { + const sdPath = JSON.parse(decodeURIComponent(pathParam)); + if (sdPath?.Physical?.path) { + const fullPath = sdPath.Physical.path as string; + const parts = fullPath.split("/").filter(Boolean); + return parts[parts.length - 1] || "Explorer"; + } + } catch { + // Fall through + } + } + return "Explorer"; + } - return "Spacedrive"; + return "Spacedrive"; } // ============================================================================ @@ -64,19 +65,19 @@ function deriveTitleFromPath(pathname: string, search: string): string { export type ViewMode = "grid" | "list" | "column" | "media" | "size"; export type SortBy = - | "name" - | "size" - | "date_modified" - | "date_created" - | "kind"; + | "name" + | "size" + | "date_modified" + | "date_created" + | "kind"; export interface Tab { - id: string; - title: string; - icon: string | null; - isPinned: boolean; - lastActive: number; - savedPath: string; + id: string; + title: string; + icon: string | null; + isPinned: boolean; + lastActive: number; + savedPath: string; } /** @@ -84,31 +85,31 @@ export interface Tab { * This is the single source of truth - no sync effects needed. */ export interface TabExplorerState { - // View settings - viewMode: ViewMode; - sortBy: SortBy; - gridSize: number; - gapSize: number; - foldersFirst: boolean; + // View settings + viewMode: ViewMode; + sortBy: SortBy; + gridSize: number; + gapSize: number; + foldersFirst: boolean; - // Column view state (serialized SdPath[] as JSON strings) - columnStack: string[]; + // Column view state (serialized SdPath[] as JSON strings) + columnStack: string[]; - // Scroll position - scrollTop: number; - scrollLeft: number; + // Scroll position + scrollTop: number; + scrollLeft: number; } /** Default explorer state for new tabs */ const DEFAULT_EXPLORER_STATE: TabExplorerState = { - viewMode: "grid", - sortBy: "name", - gridSize: 120, - gapSize: 16, - foldersFirst: true, - columnStack: [], - scrollTop: 0, - scrollLeft: 0, + viewMode: "grid", + sortBy: "name", + gridSize: 120, + gapSize: 16, + foldersFirst: true, + columnStack: [], + scrollTop: 0, + scrollLeft: 0, }; // ============================================================================ @@ -116,26 +117,26 @@ const DEFAULT_EXPLORER_STATE: TabExplorerState = { // ============================================================================ interface TabManagerContextValue { - // Tab management - tabs: Tab[]; - activeTabId: string; - router: RemixRouter; - createTab: (title?: string, path?: string) => void; - closeTab: (tabId: string) => void; - switchTab: (tabId: string) => void; - updateTabTitle: (tabId: string, title: string) => void; - updateTabPath: (tabId: string, path: string) => void; - nextTab: () => void; - previousTab: () => void; - selectTabAtIndex: (index: number) => void; - setDefaultNewTabPath: (path: string) => void; + // Tab management + tabs: Tab[]; + activeTabId: string; + router: RemixRouter; + createTab: (title?: string, path?: string) => void; + closeTab: (tabId: string) => void; + switchTab: (tabId: string) => void; + updateTabTitle: (tabId: string, title: string) => void; + updateTabPath: (tabId: string, path: string) => void; + nextTab: () => void; + previousTab: () => void; + selectTabAtIndex: (index: number) => void; + setDefaultNewTabPath: (path: string) => void; - // Explorer state (per-tab) - getExplorerState: (tabId: string) => TabExplorerState; - updateExplorerState: ( - tabId: string, - updates: Partial, - ) => void; + // Explorer state (per-tab) + getExplorerState: (tabId: string) => TabExplorerState; + updateExplorerState: ( + tabId: string, + updates: Partial + ) => void; } const TabManagerContext = createContext(null); @@ -145,229 +146,223 @@ const TabManagerContext = createContext(null); // ============================================================================ interface TabManagerProviderProps { - children: ReactNode; - routes: RouteObject[]; + children: ReactNode; + routes: RouteObject[]; } export function TabManagerProvider({ - children, - routes, + children, + routes, }: TabManagerProviderProps) { - const router = useMemo(() => createBrowserRouter(routes), [routes]); + const router = useMemo(() => createBrowserRouter(routes), [routes]); - const [tabs, setTabs] = useState(() => { - const initialTabId = crypto.randomUUID(); - return [ - { - id: initialTabId, - title: "Overview", - icon: null, - isPinned: false, - lastActive: Date.now(), - savedPath: "/", - }, - ]; - }); + const [tabs, setTabs] = useState(() => { + const initialTabId = crypto.randomUUID(); + return [ + { + id: initialTabId, + title: "Overview", + icon: null, + isPinned: false, + lastActive: Date.now(), + savedPath: "/", + }, + ]; + }); - const [activeTabId, setActiveTabId] = useState(tabs[0].id); + const [activeTabId, setActiveTabId] = useState(tabs[0].id); - // Initialize explorerStates with the first tab's state - const [explorerStates, setExplorerStates] = useState< - Map - >(() => { - const initialMap = new Map(); - initialMap.set(tabs[0].id, { ...DEFAULT_EXPLORER_STATE }); - return initialMap; - }); - const [defaultNewTabPath, setDefaultNewTabPathState] = - useState("/"); + // Initialize explorerStates with the first tab's state + const [explorerStates, setExplorerStates] = useState< + Map + >(() => { + const initialMap = new Map(); + initialMap.set(tabs[0].id, { ...DEFAULT_EXPLORER_STATE }); + return initialMap; + }); + const [defaultNewTabPath, setDefaultNewTabPathState] = useState("/"); - // ======================================================================== - // Tab management - // ======================================================================== + // ======================================================================== + // Tab management + // ======================================================================== - const setDefaultNewTabPath = useCallback((path: string) => { - setDefaultNewTabPathState(path); - }, []); + const setDefaultNewTabPath = useCallback((path: string) => { + setDefaultNewTabPathState(path); + }, []); - const createTab = useCallback( - (title?: string, path?: string) => { - const tabPath = path ?? defaultNewTabPath; - const [pathname, search = ""] = tabPath.split("?"); - const derivedTitle = - title || - deriveTitleFromPath(pathname, search ? `?${search}` : ""); + const createTab = useCallback( + (title?: string, path?: string) => { + const tabPath = path ?? defaultNewTabPath; + const [pathname, search = ""] = tabPath.split("?"); + const derivedTitle = + title || deriveTitleFromPath(pathname, search ? `?${search}` : ""); - const newTab: Tab = { - id: crypto.randomUUID(), - title: derivedTitle, - icon: null, - isPinned: false, - lastActive: Date.now(), - savedPath: tabPath, - }; + const newTab: Tab = { + id: crypto.randomUUID(), + title: derivedTitle, + icon: null, + isPinned: false, + lastActive: Date.now(), + savedPath: tabPath, + }; - // Initialize explorer state for the new tab - setExplorerStates((prev) => - new Map(prev).set(newTab.id, { ...DEFAULT_EXPLORER_STATE }), - ); + // Initialize explorer state for the new tab + setExplorerStates((prev) => + new Map(prev).set(newTab.id, { ...DEFAULT_EXPLORER_STATE }) + ); - setTabs((prev) => [...prev, newTab]); - setActiveTabId(newTab.id); - }, - [defaultNewTabPath], - ); + setTabs((prev) => [...prev, newTab]); + setActiveTabId(newTab.id); + }, + [defaultNewTabPath] + ); - const closeTab = useCallback( - (tabId: string) => { - setTabs((prev) => { - const filtered = prev.filter((t) => t.id !== tabId); + const closeTab = useCallback( + (tabId: string) => { + setTabs((prev) => { + const filtered = prev.filter((t) => t.id !== tabId); - if (filtered.length === 0) { - return prev; - } + if (filtered.length === 0) { + return prev; + } - if (tabId === activeTabId) { - const currentIndex = prev.findIndex((t) => t.id === tabId); - const newIndex = Math.max(0, currentIndex - 1); - const newActiveTab = filtered[newIndex] || filtered[0]; - if (newActiveTab) { - setActiveTabId(newActiveTab.id); - } - } + if (tabId === activeTabId) { + const currentIndex = prev.findIndex((t) => t.id === tabId); + const newIndex = Math.max(0, currentIndex - 1); + const newActiveTab = filtered[newIndex] || filtered[0]; + if (newActiveTab) { + setActiveTabId(newActiveTab.id); + } + } - return filtered; - }); + return filtered; + }); - // Clean up explorer state for closed tab - setExplorerStates((prev) => { - const next = new Map(prev); - next.delete(tabId); - return next; - }); - }, - [activeTabId], - ); + // Clean up explorer state for closed tab + setExplorerStates((prev) => { + const next = new Map(prev); + next.delete(tabId); + return next; + }); + }, + [activeTabId] + ); - const switchTab = useCallback( - (newTabId: string) => { - if (newTabId === activeTabId) return; + const switchTab = useCallback( + (newTabId: string) => { + if (newTabId === activeTabId) return; - setTabs((prev) => - prev.map((tab) => - tab.id === newTabId - ? { ...tab, lastActive: Date.now() } - : tab, - ), - ); + setTabs((prev) => + prev.map((tab) => + tab.id === newTabId ? { ...tab, lastActive: Date.now() } : tab + ) + ); - setActiveTabId(newTabId); - }, - [activeTabId], - ); + setActiveTabId(newTabId); + }, + [activeTabId] + ); - const updateTabTitle = useCallback((tabId: string, title: string) => { - setTabs((prev) => - prev.map((tab) => (tab.id === tabId ? { ...tab, title } : tab)), - ); - }, []); + const updateTabTitle = useCallback((tabId: string, title: string) => { + setTabs((prev) => + prev.map((tab) => (tab.id === tabId ? { ...tab, title } : tab)) + ); + }, []); - const updateTabPath = useCallback((tabId: string, path: string) => { - setTabs((prev) => - prev.map((tab) => - tab.id === tabId ? { ...tab, savedPath: path } : tab, - ), - ); - }, []); + const updateTabPath = useCallback((tabId: string, path: string) => { + setTabs((prev) => + prev.map((tab) => (tab.id === tabId ? { ...tab, savedPath: path } : tab)) + ); + }, []); - const nextTab = useCallback(() => { - const currentIndex = tabs.findIndex((t) => t.id === activeTabId); - const nextIndex = (currentIndex + 1) % tabs.length; - switchTab(tabs[nextIndex].id); - }, [tabs, activeTabId, switchTab]); + const nextTab = useCallback(() => { + const currentIndex = tabs.findIndex((t) => t.id === activeTabId); + const nextIndex = (currentIndex + 1) % tabs.length; + switchTab(tabs[nextIndex].id); + }, [tabs, activeTabId, switchTab]); - const previousTab = useCallback(() => { - const currentIndex = tabs.findIndex((t) => t.id === activeTabId); - const prevIndex = (currentIndex - 1 + tabs.length) % tabs.length; - switchTab(tabs[prevIndex].id); - }, [tabs, activeTabId, switchTab]); + const previousTab = useCallback(() => { + const currentIndex = tabs.findIndex((t) => t.id === activeTabId); + const prevIndex = (currentIndex - 1 + tabs.length) % tabs.length; + switchTab(tabs[prevIndex].id); + }, [tabs, activeTabId, switchTab]); - const selectTabAtIndex = useCallback( - (index: number) => { - if (index >= 0 && index < tabs.length) { - switchTab(tabs[index].id); - } - }, - [tabs, switchTab], - ); + const selectTabAtIndex = useCallback( + (index: number) => { + if (index >= 0 && index < tabs.length) { + switchTab(tabs[index].id); + } + }, + [tabs, switchTab] + ); - // ======================================================================== - // Explorer state (per-tab) - // ======================================================================== + // ======================================================================== + // Explorer state (per-tab) + // ======================================================================== - const getExplorerState = useCallback( - (tabId: string): TabExplorerState => { - return explorerStates.get(tabId) ?? { ...DEFAULT_EXPLORER_STATE }; - }, - [explorerStates], - ); + const getExplorerState = useCallback( + (tabId: string): TabExplorerState => { + return explorerStates.get(tabId) ?? { ...DEFAULT_EXPLORER_STATE }; + }, + [explorerStates] + ); - const updateExplorerState = useCallback( - (tabId: string, updates: Partial) => { - setExplorerStates((prev) => { - const current = prev.get(tabId) ?? { - ...DEFAULT_EXPLORER_STATE, - }; - return new Map(prev).set(tabId, { ...current, ...updates }); - }); - }, - [], - ); + const updateExplorerState = useCallback( + (tabId: string, updates: Partial) => { + setExplorerStates((prev) => { + const current = prev.get(tabId) ?? { + ...DEFAULT_EXPLORER_STATE, + }; + return new Map(prev).set(tabId, { ...current, ...updates }); + }); + }, + [] + ); - // ======================================================================== - // Context value - // ======================================================================== + // ======================================================================== + // Context value + // ======================================================================== - const value = useMemo( - () => ({ - tabs, - activeTabId, - router, - createTab, - closeTab, - switchTab, - updateTabTitle, - updateTabPath, - nextTab, - previousTab, - selectTabAtIndex, - setDefaultNewTabPath, - getExplorerState, - updateExplorerState, - }), - [ - tabs, - activeTabId, - router, - createTab, - closeTab, - switchTab, - updateTabTitle, - updateTabPath, - nextTab, - previousTab, - selectTabAtIndex, - setDefaultNewTabPath, - getExplorerState, - updateExplorerState, - ], - ); + const value = useMemo( + () => ({ + tabs, + activeTabId, + router, + createTab, + closeTab, + switchTab, + updateTabTitle, + updateTabPath, + nextTab, + previousTab, + selectTabAtIndex, + setDefaultNewTabPath, + getExplorerState, + updateExplorerState, + }), + [ + tabs, + activeTabId, + router, + createTab, + closeTab, + switchTab, + updateTabTitle, + updateTabPath, + nextTab, + previousTab, + selectTabAtIndex, + setDefaultNewTabPath, + getExplorerState, + updateExplorerState, + ] + ); - return ( - - {children} - - ); + return ( + + {children} + + ); } -export { TabManagerContext }; \ No newline at end of file +export { TabManagerContext }; diff --git a/packages/interface/src/components/TabManager/TabNavigationSync.tsx b/packages/interface/src/components/TabManager/TabNavigationSync.tsx index 1bcdbda86..4ec9f005b 100644 --- a/packages/interface/src/components/TabManager/TabNavigationSync.tsx +++ b/packages/interface/src/components/TabManager/TabNavigationSync.tsx @@ -6,58 +6,58 @@ import { useTabManager } from "./useTabManager"; * Derives a tab title from the current route pathname and search params */ function deriveTitleFromPath(pathname: string, search: string): string { - // Static route mappings - const routeTitles: Record = { - "/": "Overview", - "/favorites": "Favorites", - "/recents": "Recents", - "/file-kinds": "File Kinds", - "/search": "Search", - "/jobs": "Jobs", - "/daemon": "Daemon", - }; + // Static route mappings + const routeTitles: Record = { + "/": "Overview", + "/favorites": "Favorites", + "/recents": "Recents", + "/file-kinds": "File Kinds", + "/search": "Search", + "/jobs": "Jobs", + "/daemon": "Daemon", + }; - // Check static routes first - if (routeTitles[pathname]) { - return routeTitles[pathname]; - } + // Check static routes first + if (routeTitles[pathname]) { + return routeTitles[pathname]; + } - // Handle tag routes: /tag/:tagId - if (pathname.startsWith("/tag/")) { - const tagId = pathname.split("/")[2]; - return tagId ? `Tag: ${tagId.slice(0, 8)}...` : "Tag"; - } + // Handle tag routes: /tag/:tagId + if (pathname.startsWith("/tag/")) { + const tagId = pathname.split("/")[2]; + return tagId ? `Tag: ${tagId.slice(0, 8)}...` : "Tag"; + } - // Handle explorer routes - if (pathname === "/explorer" && search) { - const params = new URLSearchParams(search); + // Handle explorer routes + if (pathname === "/explorer" && search) { + const params = new URLSearchParams(search); - // Handle virtual views: /explorer?view=device&id=abc123 - const view = params.get("view"); - if (view === "device") { - return "This Device"; - } + // Handle virtual views: /explorer?view=device&id=abc123 + const view = params.get("view"); + if (view === "device") { + return "This Device"; + } - // Handle path-based navigation - const pathParam = params.get("path"); - if (pathParam) { - try { - const sdPath = JSON.parse(decodeURIComponent(pathParam)); - // Extract the last component of the path for the title - if (sdPath?.Physical?.path) { - const fullPath = sdPath.Physical.path as string; - const parts = fullPath.split("/").filter(Boolean); - return parts[parts.length - 1] || "Explorer"; - } - } catch { - // Fall through to default - } - } - return "Explorer"; - } + // Handle path-based navigation + const pathParam = params.get("path"); + if (pathParam) { + try { + const sdPath = JSON.parse(decodeURIComponent(pathParam)); + // Extract the last component of the path for the title + if (sdPath?.Physical?.path) { + const fullPath = sdPath.Physical.path as string; + const parts = fullPath.split("/").filter(Boolean); + return parts[parts.length - 1] || "Explorer"; + } + } catch { + // Fall through to default + } + } + return "Explorer"; + } - // Default fallback - return "Spacedrive"; + // Default fallback + return "Spacedrive"; } /** @@ -69,42 +69,50 @@ function deriveTitleFromPath(pathname: string, search: string): string { * 3. Navigates to the saved location when switching to a different tab */ export function TabNavigationSync() { - const location = useLocation(); - const navigate = useNavigate(); - const { activeTabId, tabs, updateTabPath, updateTabTitle } = useTabManager(); + const location = useLocation(); + const navigate = useNavigate(); + const { activeTabId, tabs, updateTabPath, updateTabTitle } = useTabManager(); - const activeTab = tabs.find((t) => t.id === activeTabId); - const currentPath = location.pathname + location.search; + const activeTab = tabs.find((t) => t.id === activeTabId); + const currentPath = location.pathname + location.search; - // Track previous activeTabId to detect tab switches - const prevActiveTabIdRef = useRef(activeTabId); + // Track previous activeTabId to detect tab switches + const prevActiveTabIdRef = useRef(activeTabId); - // Save current location and update title for active tab (only for in-tab navigation) - useEffect(() => { - // Skip saving during tab switch - currentPath belongs to the old tab - if (prevActiveTabIdRef.current !== activeTabId) { - prevActiveTabIdRef.current = activeTabId; - return; - } + // Save current location and update title for active tab (only for in-tab navigation) + useEffect(() => { + // Skip saving during tab switch - currentPath belongs to the old tab + if (prevActiveTabIdRef.current !== activeTabId) { + prevActiveTabIdRef.current = activeTabId; + return; + } - if (activeTab && currentPath !== activeTab.savedPath) { - updateTabPath(activeTabId, currentPath); - } + if (activeTab && currentPath !== activeTab.savedPath) { + updateTabPath(activeTabId, currentPath); + } - // Always update title based on current location - const newTitle = deriveTitleFromPath(location.pathname, location.search); - if (activeTab && newTitle !== activeTab.title) { - updateTabTitle(activeTabId, newTitle); - } - }, [currentPath, activeTab, activeTabId, updateTabPath, updateTabTitle, location.pathname, location.search]); + // Always update title based on current location + const newTitle = deriveTitleFromPath(location.pathname, location.search); + if (activeTab && newTitle !== activeTab.title) { + updateTabTitle(activeTabId, newTitle); + } + }, [ + currentPath, + activeTab, + activeTabId, + updateTabPath, + updateTabTitle, + location.pathname, + location.search, + ]); - // Navigate to saved location when switching tabs - useEffect(() => { - if (activeTab && currentPath !== activeTab.savedPath) { - navigate(activeTab.savedPath, { replace: true }); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [activeTabId]); + // Navigate to saved location when switching tabs + useEffect(() => { + if (activeTab && currentPath !== activeTab.savedPath) { + navigate(activeTab.savedPath, { replace: true }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [activeTabId]); - return null; + return null; } diff --git a/packages/interface/src/components/TabManager/TabView.tsx b/packages/interface/src/components/TabManager/TabView.tsx index e8f4a1429..969f276b8 100644 --- a/packages/interface/src/components/TabManager/TabView.tsx +++ b/packages/interface/src/components/TabManager/TabView.tsx @@ -6,17 +6,17 @@ */ interface TabViewProps { - isActive: boolean; - children: React.ReactNode; + isActive: boolean; + children: React.ReactNode; } export function TabView({ isActive, children }: TabViewProps) { - return ( -
    - {children} -
    - ); + return ( +
    + {children} +
    + ); } diff --git a/packages/interface/src/components/TabManager/index.ts b/packages/interface/src/components/TabManager/index.ts index 572f37c41..b37685d09 100644 --- a/packages/interface/src/components/TabManager/index.ts +++ b/packages/interface/src/components/TabManager/index.ts @@ -1,13 +1,13 @@ -export { TabManagerProvider } from "./TabManagerContext"; -export type { - Tab, - TabExplorerState, - ViewMode, - SortBy, -} from "./TabManagerContext"; -export { useTabManager } from "./useTabManager"; export { TabBar } from "./TabBar"; -export { TabView } from "./TabView"; -export { TabNavigationSync } from "./TabNavigationSync"; export { TabDefaultsSync } from "./TabDefaultsSync"; export { TabKeyboardHandler } from "./TabKeyboardHandler"; +export type { + SortBy, + Tab, + TabExplorerState, + ViewMode, +} from "./TabManagerContext"; +export { TabManagerProvider } from "./TabManagerContext"; +export { TabNavigationSync } from "./TabNavigationSync"; +export { TabView } from "./TabView"; +export { useTabManager } from "./useTabManager"; diff --git a/packages/interface/src/components/TabManager/useTabManager.ts b/packages/interface/src/components/TabManager/useTabManager.ts index 67c2937d8..d7564f74a 100644 --- a/packages/interface/src/components/TabManager/useTabManager.ts +++ b/packages/interface/src/components/TabManager/useTabManager.ts @@ -2,11 +2,9 @@ import { useContext } from "react"; import { TabManagerContext } from "./TabManagerContext"; export function useTabManager() { - const context = useContext(TabManagerContext); - if (!context) { - throw new Error( - "useTabManager must be used within a TabManagerProvider", - ); - } - return context; + const context = useContext(TabManagerContext); + if (!context) { + throw new Error("useTabManager must be used within a TabManagerProvider"); + } + return context; } diff --git a/packages/interface/src/components/Tags/TagDot.tsx b/packages/interface/src/components/Tags/TagDot.tsx index 3462d72af..f4da6ec14 100644 --- a/packages/interface/src/components/Tags/TagDot.tsx +++ b/packages/interface/src/components/Tags/TagDot.tsx @@ -1,10 +1,10 @@ -import clsx from 'clsx'; +import clsx from "clsx"; interface TagDotProps { - color: string; - tooltip?: string; - onClick?: (e: React.MouseEvent) => void; - className?: string; + color: string; + tooltip?: string; + onClick?: (e: React.MouseEvent) => void; + className?: string; } /** @@ -12,18 +12,18 @@ interface TagDotProps { * Used in FileCard and compact layouts */ export function TagDot({ color, tooltip, onClick, className }: TagDotProps) { - const Component = onClick ? 'button' : 'span'; + const Component = onClick ? "button" : "span"; - return ( - - ); + return ( + + ); } diff --git a/packages/interface/src/components/Tags/TagPill.tsx b/packages/interface/src/components/Tags/TagPill.tsx index b9a3aff41..6721e2bb2 100644 --- a/packages/interface/src/components/Tags/TagPill.tsx +++ b/packages/interface/src/components/Tags/TagPill.tsx @@ -1,12 +1,12 @@ -import clsx from 'clsx'; +import clsx from "clsx"; interface TagPillProps { - color: string; - children: React.ReactNode; - size?: 'xs' | 'sm' | 'md'; - onClick?: (e: React.MouseEvent) => void; - onRemove?: (e: React.MouseEvent) => void; - className?: string; + color: string; + children: React.ReactNode; + size?: "xs" | "sm" | "md"; + onClick?: (e: React.MouseEvent) => void; + onRemove?: (e: React.MouseEvent) => void; + className?: string; } /** @@ -14,52 +14,52 @@ interface TagPillProps { * Supports multiple sizes and optional click/remove actions */ export function TagPill({ - color, - children, - size = 'sm', - onClick, - onRemove, - className + color, + children, + size = "sm", + onClick, + onRemove, + className, }: TagPillProps) { - return ( - - ); + {/* Remove Button */} + {onRemove && ( + { + e.stopPropagation(); + onRemove(e); + }} + > + × + + )} + + ); } diff --git a/packages/interface/src/components/Tags/TagSelector.tsx b/packages/interface/src/components/Tags/TagSelector.tsx index 4218a531a..a6f411d56 100644 --- a/packages/interface/src/components/Tags/TagSelector.tsx +++ b/packages/interface/src/components/Tags/TagSelector.tsx @@ -1,20 +1,23 @@ -import { useState, useEffect } from 'react'; -import { MagnifyingGlass, Plus } from '@phosphor-icons/react'; -import clsx from 'clsx'; -import { Popover, usePopover } from '@sd/ui'; -import { useNormalizedQuery, useLibraryMutation } from '../../contexts/SpacedriveContext'; -import type { Tag } from '@sd/ts-client'; +import { MagnifyingGlass, Plus } from "@phosphor-icons/react"; +import type { Tag } from "@sd/ts-client"; +import { Popover, usePopover } from "@sd/ui"; +import clsx from "clsx"; +import { useEffect, useState } from "react"; +import { + useLibraryMutation, + useNormalizedQuery, +} from "../../contexts/SpacedriveContext"; interface TagSelectorProps { - onSelect: (tag: Tag) => void; - onClose?: () => void; - contextTags?: Tag[]; - autoFocus?: boolean; - className?: string; - /** Optional file ID to apply newly created tags to */ - fileId?: string; - /** Optional content identity UUID (preferred for content-based tagging) */ - contentId?: string; + onSelect: (tag: Tag) => void; + onClose?: () => void; + contextTags?: Tag[]; + autoFocus?: boolean; + className?: string; + /** Optional file ID to apply newly created tags to */ + fileId?: string; + /** Optional content identity UUID (preferred for content-based tagging) */ + contentId?: string; } /** @@ -22,237 +25,249 @@ interface TagSelectorProps { * Features fuzzy search, context-aware suggestions, and keyboard navigation */ export function TagSelector({ - onSelect, - onClose, - contextTags = [], - autoFocus = true, - className, - fileId, - contentId + onSelect, + onClose, + contextTags = [], + autoFocus = true, + className, + fileId, + contentId, }: TagSelectorProps) { - const [query, setQuery] = useState(''); - const [selectedIndex, setSelectedIndex] = useState(0); + const [query, setQuery] = useState(""); + const [selectedIndex, setSelectedIndex] = useState(0); - const createTag = useLibraryMutation('tags.create'); + const createTag = useLibraryMutation("tags.create"); - // Fetch all tags using search with empty query - // Using select to normalize TagSearchResult[] to Tag[] for consistent cache structure - const { data: allTags = [] } = useNormalizedQuery({ - wireMethod: 'query:tags.search', - input: { query: '' }, - resourceType: 'tag', - select: (data: any) => data?.tags?.map((result: any) => result.tag || result).filter(Boolean) ?? [] - }); + // Fetch all tags using search with empty query + // Using select to normalize TagSearchResult[] to Tag[] for consistent cache structure + const { data: allTags = [] } = useNormalizedQuery({ + wireMethod: "query:tags.search", + input: { query: "" }, + resourceType: "tag", + select: (data: any) => + data?.tags?.map((result: any) => result.tag || result).filter(Boolean) ?? + [], + }); - // Check if query matches an existing tag - const exactMatch = allTags.find( - tag => tag.canonical_name.toLowerCase() === query.toLowerCase() - ); + // Check if query matches an existing tag + const exactMatch = allTags.find( + (tag) => tag.canonical_name.toLowerCase() === query.toLowerCase() + ); - // Filter tags based on search query - const filteredTags = query.length > 0 - ? allTags.filter(tag => - tag.canonical_name.toLowerCase().includes(query.toLowerCase()) || - tag.aliases?.some(alias => alias.toLowerCase().includes(query.toLowerCase())) || - tag.abbreviation?.toLowerCase().includes(query.toLowerCase()) - ) - : allTags; + // Filter tags based on search query + const filteredTags = + query.length > 0 + ? allTags.filter( + (tag) => + tag.canonical_name.toLowerCase().includes(query.toLowerCase()) || + tag.aliases?.some((alias) => + alias.toLowerCase().includes(query.toLowerCase()) + ) || + tag.abbreviation?.toLowerCase().includes(query.toLowerCase()) + ) + : allTags; - // Reset selected index when filtered tags change - useEffect(() => { - setSelectedIndex(0); - }, [filteredTags.length]); + // Reset selected index when filtered tags change + useEffect(() => { + setSelectedIndex(0); + }, [filteredTags.length]); - // Keyboard navigation - const handleKeyDown = async (e: React.KeyboardEvent) => { - if (e.key === 'ArrowDown') { - e.preventDefault(); - setSelectedIndex(prev => Math.min(prev + 1, filteredTags.length - 1)); - } else if (e.key === 'ArrowUp') { - e.preventDefault(); - setSelectedIndex(prev => Math.max(prev - 1, 0)); - } else if (e.key === 'Enter') { - e.preventDefault(); - // If there's a match, select it - if (filteredTags[selectedIndex]) { - handleSelect(filteredTags[selectedIndex]!); - } - // If there's text but no match, create new tag - else if (query.trim().length > 0 && !exactMatch) { - await handleCreateTag(); - } - } else if (e.key === 'Escape') { - e.preventDefault(); - onClose?.(); - } - }; + // Keyboard navigation + const handleKeyDown = async (e: React.KeyboardEvent) => { + if (e.key === "ArrowDown") { + e.preventDefault(); + setSelectedIndex((prev) => Math.min(prev + 1, filteredTags.length - 1)); + } else if (e.key === "ArrowUp") { + e.preventDefault(); + setSelectedIndex((prev) => Math.max(prev - 1, 0)); + } else if (e.key === "Enter") { + e.preventDefault(); + // If there's a match, select it + if (filteredTags[selectedIndex]) { + handleSelect(filteredTags[selectedIndex]!); + } + // If there's text but no match, create new tag + else if (query.trim().length > 0 && !exactMatch) { + await handleCreateTag(); + } + } else if (e.key === "Escape") { + e.preventDefault(); + onClose?.(); + } + }; - const handleSelect = (tag: Tag) => { - onSelect(tag); - setQuery(''); - onClose?.(); - }; + const handleSelect = (tag: Tag) => { + onSelect(tag); + setQuery(""); + onClose?.(); + }; - const handleCreateTag = async () => { - if (!query.trim()) return; + const handleCreateTag = async () => { + if (!query.trim()) return; - try { - const color = `#${Math.floor(Math.random() * 16777215).toString(16).padStart(6, '0')}`; - const result = await createTag.mutateAsync({ - canonical_name: query.trim(), - aliases: [], - color, - apply_to: contentId - ? { type: 'Content', ids: [contentId] } - : fileId - ? { type: 'Entry', ids: [parseInt(fileId)] } - : undefined, - }); + try { + const color = `#${Math.floor(Math.random() * 16_777_215) + .toString(16) + .padStart(6, "0")}`; + const result = await createTag.mutateAsync({ + canonical_name: query.trim(), + aliases: [], + color, + apply_to: contentId + ? { type: "Content", ids: [contentId] } + : fileId + ? { type: "Entry", ids: [Number.parseInt(fileId)] } + : undefined, + }); - // Construct a Tag object from the result to pass to onSelect - // The full tag will be available in the cache shortly via resource events - const newTag: Tag = { - id: result.tag_id, - canonical_name: result.canonical_name, - display_name: null, - formal_name: null, - abbreviation: null, - aliases: [], - namespace: result.namespace || null, - tag_type: 'Standard', - color, - icon: null, - description: null, - is_organizational_anchor: false, - privacy_level: 'Normal', - search_weight: 0, - attributes: {}, - composition_rules: [], - created_at: new Date().toISOString(), - updated_at: new Date().toISOString(), - created_by_device: result.tag_id // Placeholder - }; + // Construct a Tag object from the result to pass to onSelect + // The full tag will be available in the cache shortly via resource events + const newTag: Tag = { + id: result.tag_id, + canonical_name: result.canonical_name, + display_name: null, + formal_name: null, + abbreviation: null, + aliases: [], + namespace: result.namespace || null, + tag_type: "Standard", + color, + icon: null, + description: null, + is_organizational_anchor: false, + privacy_level: "Normal", + search_weight: 0, + attributes: {}, + composition_rules: [], + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + created_by_device: result.tag_id, // Placeholder + }; - onSelect(newTag); - setQuery(''); - onClose?.(); - } catch (err) { - console.error('Failed to create tag:', err); - } - }; + onSelect(newTag); + setQuery(""); + onClose?.(); + } catch (err) { + console.error("Failed to create tag:", err); + } + }; - return ( -
    - {/* Search Input */} -
    - - setQuery(e.target.value)} - onKeyDown={handleKeyDown} - placeholder="Search tags..." - autoFocus={autoFocus} - className="flex-1 bg-transparent text-sm text-ink placeholder:text-ink-faint outline-none" - /> -
    + return ( +
    + {/* Search Input */} +
    + + setQuery(e.target.value)} + onKeyDown={handleKeyDown} + placeholder="Search tags..." + type="text" + value={query} + /> +
    - {/* Results */} -
    - {/* Create new tag option */} - {query.trim().length > 0 && !exactMatch && ( - - )} + {/* Results */} +
    + {/* Create new tag option */} + {query.trim().length > 0 && !exactMatch && ( + + )} - {filteredTags.length === 0 && !query.trim() ? ( -
    - No tags yet -
    - ) : filteredTags.length === 0 && query.trim() ? null : ( - filteredTags.map((tag, index) => ( - - )) - )} -
    -
    - ); + {/* Namespace badge */} + {tag.namespace && ( + + {tag.namespace} + + )} + + )) + )} +
    +
    + ); } interface TagSelectorButtonProps { - onSelect: (tag: Tag) => void; - trigger: React.ReactNode; - contextTags?: Tag[]; - /** Optional file ID to apply newly created tags to */ - fileId?: string; - /** Optional content identity UUID (preferred for content-based tagging) */ - contentId?: string; + onSelect: (tag: Tag) => void; + trigger: React.ReactNode; + contextTags?: Tag[]; + /** Optional file ID to apply newly created tags to */ + fileId?: string; + /** Optional content identity UUID (preferred for content-based tagging) */ + contentId?: string; } /** * Wrapper component that shows TagSelector in a dropdown when trigger is clicked */ -export function TagSelectorButton({ onSelect, trigger, contextTags, fileId, contentId }: TagSelectorButtonProps) { - const popover = usePopover(); +export function TagSelectorButton({ + onSelect, + trigger, + contextTags, + fileId, + contentId, +}: TagSelectorButtonProps) { + const popover = usePopover(); - return ( - - { - onSelect(tag); - popover.setOpen(false); - }} - onClose={() => popover.setOpen(false)} - contextTags={contextTags} - fileId={fileId} - contentId={contentId} - /> - - ); -} \ No newline at end of file + return ( + + popover.setOpen(false)} + onSelect={(tag) => { + onSelect(tag); + popover.setOpen(false); + }} + /> + + ); +} diff --git a/packages/interface/src/components/Tags/index.tsx b/packages/interface/src/components/Tags/index.tsx index 374299347..705a166f2 100644 --- a/packages/interface/src/components/Tags/index.tsx +++ b/packages/interface/src/components/Tags/index.tsx @@ -1,3 +1,3 @@ -export { TagDot } from './TagDot'; -export { TagPill } from './TagPill'; -export { TagSelector, TagSelectorButton } from './TagSelector'; +export { TagDot } from "./TagDot"; +export { TagPill } from "./TagPill"; +export { TagSelector, TagSelectorButton } from "./TagSelector"; diff --git a/packages/interface/src/components/index.ts b/packages/interface/src/components/index.ts index d52364f48..7c04427e9 100644 --- a/packages/interface/src/components/index.ts +++ b/packages/interface/src/components/index.ts @@ -1,8 +1,8 @@ -export { ExplorerView } from "../routes/explorer/ExplorerView"; -export { Sidebar } from "../routes/explorer/Sidebar"; export { ExplorerProvider, useExplorer } from "../routes/explorer/context"; -export * from "../routes/explorer/utils"; +export { ExplorerView } from "../routes/explorer/ExplorerView"; export * from "../routes/explorer/File"; +export { Sidebar } from "../routes/explorer/Sidebar"; +export * from "../routes/explorer/utils"; export * from "./Inspector/Inspector"; export { useCreateLibraryDialog } from "./modals/CreateLibraryModal"; -export { useFileOperationDialog } from "./modals/FileOperationModal"; \ No newline at end of file +export { useFileOperationDialog } from "./modals/FileOperationModal"; diff --git a/packages/interface/src/components/modals/CreateLibraryModal.tsx b/packages/interface/src/components/modals/CreateLibraryModal.tsx index e3632d479..2ac6221aa 100644 --- a/packages/interface/src/components/modals/CreateLibraryModal.tsx +++ b/packages/interface/src/components/modals/CreateLibraryModal.tsx @@ -1,33 +1,29 @@ -import { useState, useEffect, useRef } from "react"; -import { useForm } from "react-hook-form"; import { - Books, - FolderOpen, - CircleNotch, - CheckCircle, - Warning, + Books, + CheckCircle, + CircleNotch, + FolderOpen, + Warning, } from "@phosphor-icons/react"; -import { - Button, - Input, - Label, - Dialog, - dialogManager, - useDialog, -} from "@sd/ui"; -import { queryClient } from "@sd/ts-client/hooks"; import type { Event } from "@sd/ts-client"; -import { useCoreMutation, useSpacedriveClient } from "../../contexts/SpacedriveContext"; +import { queryClient } from "@sd/ts-client/hooks"; +import { Button, Dialog, dialogManager, Input, Label, useDialog } from "@sd/ui"; +import { useEffect, useRef, useState } from "react"; +import { useForm } from "react-hook-form"; import { usePlatform } from "../../contexts/PlatformContext"; +import { + useCoreMutation, + useSpacedriveClient, +} from "../../contexts/SpacedriveContext"; interface CreateLibraryDialogProps { - id: number; - onLibraryCreated?: (libraryId: string) => void; + id: number; + onLibraryCreated?: (libraryId: string) => void; } interface CreateLibraryFormData { - name: string; - path: string | null; + name: string; + path: string | null; } type DialogStep = "form" | "creating" | "success" | "error"; @@ -45,315 +41,300 @@ type DialogStep = "form" | "creating" | "success" | "error"; * ``` */ export function useCreateLibraryDialog( - onLibraryCreated?: (libraryId: string) => void, + onLibraryCreated?: (libraryId: string) => void ) { - return dialogManager.create((props: CreateLibraryDialogProps) => ( - - )); + return dialogManager.create((props: CreateLibraryDialogProps) => ( + + )); } function CreateLibraryDialog(props: CreateLibraryDialogProps) { - const dialog = useDialog(props); - const client = useSpacedriveClient(); - const platform = usePlatform(); + const dialog = useDialog(props); + const client = useSpacedriveClient(); + const platform = usePlatform(); - const [step, setStep] = useState("form"); - const [errorMessage, setErrorMessage] = useState(null); + const [step, setStep] = useState("form"); + const [errorMessage, setErrorMessage] = useState(null); - const createLibrary = useCoreMutation("libraries.create"); + const createLibrary = useCoreMutation("libraries.create"); - // Track unsubscribe function and pending library ID in refs - const unsubscribeRef = useRef<(() => void) | null>(null); - const pendingLibraryIdRef = useRef(null); - // Buffer to store events received before we know the library ID - const receivedEventsRef = useRef>([]); + // Track unsubscribe function and pending library ID in refs + const unsubscribeRef = useRef<(() => void) | null>(null); + const pendingLibraryIdRef = useRef(null); + // Buffer to store events received before we know the library ID + const receivedEventsRef = useRef< + Array<{ id: string; name: string; path: string }> + >([]); - const form = useForm({ - defaultValues: { - name: "", - path: null, - }, - }); + const form = useForm({ + defaultValues: { + name: "", + path: null, + }, + }); - // Clean up subscription on unmount - useEffect(() => { - return () => { - if (unsubscribeRef.current) { - unsubscribeRef.current(); - unsubscribeRef.current = null; - } - }; - }, []); + // Clean up subscription on unmount + useEffect(() => { + return () => { + if (unsubscribeRef.current) { + unsubscribeRef.current(); + unsubscribeRef.current = null; + } + }; + }, []); - const handleBrowse = async () => { - if (!platform.openDirectoryPickerDialog) { - console.error("Directory picker not available on this platform"); - return; - } + const handleBrowse = async () => { + if (!platform.openDirectoryPickerDialog) { + console.error("Directory picker not available on this platform"); + return; + } - const selected = await platform.openDirectoryPickerDialog({ - title: "Choose library location", - multiple: false, - }); + const selected = await platform.openDirectoryPickerDialog({ + title: "Choose library location", + multiple: false, + }); - if (selected && typeof selected === "string") { - form.setValue("path", selected); - } - }; + if (selected && typeof selected === "string") { + form.setValue("path", selected); + } + }; - const onSubmit = form.handleSubmit(async (data) => { - if (!data.name.trim()) { - form.setError("name", { - type: "manual", - message: "Library name is required", - }); - return; - } + const onSubmit = form.handleSubmit(async (data) => { + if (!data.name.trim()) { + form.setError("name", { + type: "manual", + message: "Library name is required", + }); + return; + } - setStep("creating"); - setErrorMessage(null); - receivedEventsRef.current = []; + setStep("creating"); + setErrorMessage(null); + receivedEventsRef.current = []; - // Set up event subscription BEFORE making the mutation - // This ensures we don't miss the LibraryCreated event - try { - const unsubscribe = await client.subscribe((event: Event) => { - if ( - typeof event === "object" && - "LibraryCreated" in event - ) { - const libraryEvent = event.LibraryCreated; + // Set up event subscription BEFORE making the mutation + // This ensures we don't miss the LibraryCreated event + try { + const unsubscribe = await client.subscribe((event: Event) => { + if (typeof event === "object" && "LibraryCreated" in event) { + const libraryEvent = event.LibraryCreated; - // If we already know the library ID, check for match and close - if (pendingLibraryIdRef.current === libraryEvent.id) { - dialog.state.open = false; - if (unsubscribeRef.current) { - unsubscribeRef.current(); - unsubscribeRef.current = null; - } - } else { - // Buffer the event in case it arrives before mutation resolves - receivedEventsRef.current.push(libraryEvent); - } - } - }); - unsubscribeRef.current = unsubscribe; - } catch (err) { - console.error("Failed to subscribe to events:", err); - } + // If we already know the library ID, check for match and close + if (pendingLibraryIdRef.current === libraryEvent.id) { + dialog.state.open = false; + if (unsubscribeRef.current) { + unsubscribeRef.current(); + unsubscribeRef.current = null; + } + } else { + // Buffer the event in case it arrives before mutation resolves + receivedEventsRef.current.push(libraryEvent); + } + } + }); + unsubscribeRef.current = unsubscribe; + } catch (err) { + console.error("Failed to subscribe to events:", err); + } - try { - const result = await createLibrary.mutateAsync({ - name: data.name.trim(), - path: data.path, - }); + try { + const result = await createLibrary.mutateAsync({ + name: data.name.trim(), + path: data.path, + }); - // Store the library ID we're waiting for - pendingLibraryIdRef.current = result.library_id; + // Store the library ID we're waiting for + pendingLibraryIdRef.current = result.library_id; - // Check if we already received the event (race condition handling) - const alreadyReceived = receivedEventsRef.current.some( - (e) => e.id === result.library_id - ); + // Check if we already received the event (race condition handling) + const alreadyReceived = receivedEventsRef.current.some( + (e) => e.id === result.library_id + ); - // Invalidate the libraries list query to refresh UI - // Query key format is [query.type, query.input], so we match on the type prefix - await queryClient.invalidateQueries({ queryKey: ["libraries.list"] }); - // Also invalidate core.status which includes library list - await queryClient.invalidateQueries({ queryKey: ["core.status"] }); + // Invalidate the libraries list query to refresh UI + // Query key format is [query.type, query.input], so we match on the type prefix + await queryClient.invalidateQueries({ queryKey: ["libraries.list"] }); + // Also invalidate core.status which includes library list + await queryClient.invalidateQueries({ queryKey: ["core.status"] }); - // Switch to the new library - if (platform.setCurrentLibraryId) { - // Tauri: Use platform method to sync across all windows - await platform.setCurrentLibraryId(result.library_id); - } else { - // Web fallback: Just update the client - client.setCurrentLibrary(result.library_id); - } + // Switch to the new library + if (platform.setCurrentLibraryId) { + // Tauri: Use platform method to sync across all windows + await platform.setCurrentLibraryId(result.library_id); + } else { + // Web fallback: Just update the client + client.setCurrentLibrary(result.library_id); + } - // Call the callback if provided - if (props.onLibraryCreated) { - props.onLibraryCreated(result.library_id); - } + // Call the callback if provided + if (props.onLibraryCreated) { + props.onLibraryCreated(result.library_id); + } - if (alreadyReceived) { - // Event was already received, close immediately - dialog.state.open = false; - if (unsubscribeRef.current) { - unsubscribeRef.current(); - unsubscribeRef.current = null; - } - } else { - // Show success state while waiting for event - setStep("success"); - // Dialog will close when LibraryCreated event is received - } - } catch (error) { - console.error("Failed to create library:", error); - setErrorMessage( - error instanceof Error ? error.message : "Failed to create library", - ); - setStep("error"); + if (alreadyReceived) { + // Event was already received, close immediately + dialog.state.open = false; + if (unsubscribeRef.current) { + unsubscribeRef.current(); + unsubscribeRef.current = null; + } + } else { + // Show success state while waiting for event + setStep("success"); + // Dialog will close when LibraryCreated event is received + } + } catch (error) { + console.error("Failed to create library:", error); + setErrorMessage( + error instanceof Error ? error.message : "Failed to create library" + ); + setStep("error"); - // Clean up subscription on error - if (unsubscribeRef.current) { - unsubscribeRef.current(); - unsubscribeRef.current = null; - } - } - }); + // Clean up subscription on error + if (unsubscribeRef.current) { + unsubscribeRef.current(); + unsubscribeRef.current = null; + } + } + }); - // Creating state - if (step === "creating") { - return ( - } - hideButtons - > -
    - -
    -

    - Creating your library... -

    -

    - This may take a moment -

    -
    -
    -
    - ); - } + // Creating state + if (step === "creating") { + return ( + } + title="Creating Library" + > +
    + +
    +

    + Creating your library... +

    +

    This may take a moment

    +
    +
    +
    + ); + } - // Success state - waiting for LibraryCreated event - if (step === "success") { - return ( - } - hideButtons - > -
    - -
    -

    - Library created successfully! -

    -

    - Initializing... -

    -
    -
    -
    - ); - } + // Success state - waiting for LibraryCreated event + if (step === "success") { + return ( + } + title="Library Created" + > +
    + +
    +

    + Library created successfully! +

    +

    Initializing...

    +
    +
    +
    + ); + } - // Error state - if (step === "error") { - return ( - } - ctaLabel="Try Again" - onSubmit={async () => { - setStep("form"); - setErrorMessage(null); - }} - onCancelled={true} - > -
    - -
    -

    - Failed to create library -

    -

    - {errorMessage} -

    -
    -
    -
    - ); - } + // Error state + if (step === "error") { + return ( + } + onCancelled={true} + onSubmit={async () => { + setStep("form"); + setErrorMessage(null); + }} + title="Error" + > +
    + +
    +

    + Failed to create library +

    +

    {errorMessage}

    +
    +
    +
    + ); + } - // Form state (default) - return ( - } - description="A library is a container for your files, tags, and organization" - ctaLabel="Create Library" - onCancelled={true} - loading={createLibrary.isPending} - > -
    -
    - - - {form.formState.errors.name && ( -

    - {form.formState.errors.name.message} -

    - )} -
    + // Form state (default) + return ( + } + loading={createLibrary.isPending} + onCancelled={true} + onSubmit={onSubmit} + title="Create New Library" + > +
    +
    + + + {form.formState.errors.name && ( +

    + {form.formState.errors.name.message} +

    + )} +
    -
    - -
    - - form.setValue("path", e.target.value || null) - } - size="md" - placeholder="Default location" - className="pr-12 bg-app-input" - /> - {platform.openDirectoryPickerDialog && ( - - )} -
    -

    - Leave empty to use the default location -

    -
    -
    -
    - ); -} \ No newline at end of file +
    + +
    + form.setValue("path", e.target.value || null)} + placeholder="Default location" + size="md" + value={form.watch("path") || ""} + /> + {platform.openDirectoryPickerDialog && ( + + )} +
    +

    + Leave empty to use the default location +

    +
    +
    +
    + ); +} diff --git a/packages/interface/src/components/modals/FileOperationModal.tsx b/packages/interface/src/components/modals/FileOperationModal.tsx index 1e29b39fb..7171a3fbb 100644 --- a/packages/interface/src/components/modals/FileOperationModal.tsx +++ b/packages/interface/src/components/modals/FileOperationModal.tsx @@ -1,429 +1,438 @@ -import { useState, useEffect } from "react"; +import { + ArrowRight, + ArrowsLeftRight, + CircleNotch, + Copy as CopyIcon, + Files, + FolderOpen, + Warning, +} from "@phosphor-icons/react"; +import type { File as FileType, SdPath } from "@sd/ts-client"; +import { Dialog, dialogManager, useDialog } from "@sd/ui"; +import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { - Files, - FolderOpen, - Warning, - CheckCircle, - CircleNotch, - ArrowRight, - Copy as CopyIcon, - ArrowsLeftRight, - File as FileIcon, - Image, - FileText, - FilmStrip, - MusicNote, -} from "@phosphor-icons/react"; -import { - Dialog, - dialogManager, - useDialog, -} from "@sd/ui"; -import type { SdPath, File as FileType } from "@sd/ts-client"; -import { useLibraryMutation, useLibraryQuery } from "../../contexts/SpacedriveContext"; + useLibraryMutation, + useLibraryQuery, +} from "../../contexts/SpacedriveContext"; import { File, FileStack } from "../../routes/explorer/File"; interface FileOperationDialogProps { - id: number; - operation: "copy" | "move"; - sources: SdPath[]; - destination: SdPath; - onComplete?: () => void; + id: number; + operation: "copy" | "move"; + sources: SdPath[]; + destination: SdPath; + onComplete?: () => void; } type ConflictResolution = "Overwrite" | "AutoModifyName" | "Skip" | "Abort"; type DialogPhase = - | { type: "form" } - | { type: "executing" } - | { type: "error"; message: string }; + | { type: "form" } + | { type: "executing" } + | { type: "error"; message: string }; export function useFileOperationDialog() { - return (options: Omit) => { - return dialogManager.create((props: FileOperationDialogProps) => ( - - )); - }; + return (options: Omit) => { + return dialogManager.create((props: FileOperationDialogProps) => ( + + )); + }; } function FileOperationDialog(props: FileOperationDialogProps) { - const dialog = useDialog(props); - const form = useForm(); - const [phase, setPhase] = useState({ type: "form" }); - const [operation, setOperation] = useState<"copy" | "move">(props.operation); - const [conflictResolution, setConflictResolution] = useState("Skip"); + const dialog = useDialog(props); + const form = useForm(); + const [phase, setPhase] = useState({ type: "form" }); + const [operation, setOperation] = useState<"copy" | "move">(props.operation); + const [conflictResolution, setConflictResolution] = + useState("Skip"); - const copyFiles = useLibraryMutation("files.copy"); + const copyFiles = useLibraryMutation("files.copy"); - // Fetch file info for sources (up to 3 for FileStack) - const sourcePaths = props.sources.slice(0, 3).map(s => - "Physical" in s ? s.Physical.path : null - ).filter(Boolean); + // Fetch file info for sources (up to 3 for FileStack) + const sourcePaths = props.sources + .slice(0, 3) + .map((s) => ("Physical" in s ? s.Physical.path : null)) + .filter(Boolean); - const sourceFileQueries = sourcePaths.map(path => - useLibraryQuery({ - type: "files.by_path", - input: { path }, - enabled: !!path, - }) - ); + const sourceFileQueries = sourcePaths.map((path) => + useLibraryQuery({ + type: "files.by_path", + input: { path }, + enabled: !!path, + }) + ); - const sourceFiles = sourceFileQueries - .map(q => q.data) - .filter((f): f is FileType => f !== undefined && f !== null); + const sourceFiles = sourceFileQueries + .map((q) => q.data) + .filter((f): f is FileType => f !== undefined && f !== null); - // Fetch destination folder info - const destPath = "Physical" in props.destination - ? props.destination.Physical.path - : null; + // Fetch destination folder info + const destPath = + "Physical" in props.destination ? props.destination.Physical.path : null; - const { data: destFile } = useLibraryQuery({ - type: "files.by_path", - input: { path: destPath }, - enabled: !!destPath, - }); + const { data: destFile } = useLibraryQuery({ + type: "files.by_path", + input: { path: destPath }, + enabled: !!destPath, + }); - // Check if any source is the same as destination - const hasSameSourceDest = props.sources.some((source) => { - if ("Physical" in source && "Physical" in props.destination) { - return source.Physical.path === props.destination.Physical.path; - } - return false; - }); + // Check if any source is the same as destination + const hasSameSourceDest = props.sources.some((source) => { + if ("Physical" in source && "Physical" in props.destination) { + return source.Physical.path === props.destination.Physical.path; + } + return false; + }); - // Auto-close if invalid operation (must be in useEffect to avoid render loop) - useEffect(() => { - if (hasSameSourceDest) { - dialogManager.setState(props.id, { open: false }); - } - }, [hasSameSourceDest, props.id]); + // Auto-close if invalid operation (must be in useEffect to avoid render loop) + useEffect(() => { + if (hasSameSourceDest) { + dialogManager.setState(props.id, { open: false }); + } + }, [hasSameSourceDest, props.id]); - if (hasSameSourceDest) { - return null; - } + if (hasSameSourceDest) { + return null; + } - const handleSubmit = async () => { - try { - setPhase({ type: "executing" }); + const handleSubmit = async () => { + try { + setPhase({ type: "executing" }); - // Execute with the user's chosen operation and conflict resolution - await copyFiles.mutateAsync({ - sources: { paths: props.sources }, - destination: props.destination, - overwrite: conflictResolution === "Overwrite", - verify_checksum: false, - preserve_timestamps: true, - move_files: operation === "move", - copy_method: "Auto", - on_conflict: conflictResolution, - }); + // Execute with the user's chosen operation and conflict resolution + await copyFiles.mutateAsync({ + sources: { paths: props.sources }, + destination: props.destination, + overwrite: conflictResolution === "Overwrite", + verify_checksum: false, + preserve_timestamps: true, + move_files: operation === "move", + copy_method: "Auto", + on_conflict: conflictResolution, + }); - // Close immediately on success - dialogManager.setState(props.id, { open: false }); - props.onComplete?.(); - } catch (error) { - setPhase({ - type: "error", - message: error instanceof Error ? error.message : "Operation failed", - }); - } - }; + // Close immediately on success + dialogManager.setState(props.id, { open: false }); + props.onComplete?.(); + } catch (error) { + setPhase({ + type: "error", + message: error instanceof Error ? error.message : "Operation failed", + }); + } + }; - const handleCancel = () => { - dialogManager.setState(props.id, { open: false }); - }; + const handleCancel = () => { + dialogManager.setState(props.id, { open: false }); + }; - // Keyboard shortcuts - useEffect(() => { - if (phase.type !== "form") return; + // Keyboard shortcuts + useEffect(() => { + if (phase.type !== "form") return; - const handleKeyDown = (e: KeyboardEvent) => { - // Enter - Submit - if (e.key === "Enter" && !e.shiftKey) { - e.preventDefault(); - handleSubmit(); - return; - } + const handleKeyDown = (e: KeyboardEvent) => { + // Enter - Submit + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault(); + handleSubmit(); + return; + } - // Only handle other shortcuts if not typing in an input - if ((e.target as HTMLElement)?.tagName === "INPUT") return; + // Only handle other shortcuts if not typing in an input + if ((e.target as HTMLElement)?.tagName === "INPUT") return; - // ⌘1 / Ctrl+1 - Copy mode - if ((e.metaKey || e.ctrlKey) && e.key === "1") { - e.preventDefault(); - e.stopPropagation(); - setOperation("copy"); - } - // ⌘2 / Ctrl+2 - Move mode - if ((e.metaKey || e.ctrlKey) && e.key === "2") { - e.preventDefault(); - e.stopPropagation(); - setOperation("move"); - } - // S - Skip - if (e.key === "s" && !e.metaKey && !e.ctrlKey) { - e.preventDefault(); - setConflictResolution("Skip"); - } - // K - Keep both - if (e.key === "k" && !e.metaKey && !e.ctrlKey) { - e.preventDefault(); - setConflictResolution("AutoModifyName"); - } - // O - Overwrite - if (e.key === "o" && !e.metaKey && !e.ctrlKey) { - e.preventDefault(); - setConflictResolution("Overwrite"); - } - }; + // ⌘1 / Ctrl+1 - Copy mode + if ((e.metaKey || e.ctrlKey) && e.key === "1") { + e.preventDefault(); + e.stopPropagation(); + setOperation("copy"); + } + // ⌘2 / Ctrl+2 - Move mode + if ((e.metaKey || e.ctrlKey) && e.key === "2") { + e.preventDefault(); + e.stopPropagation(); + setOperation("move"); + } + // S - Skip + if (e.key === "s" && !e.metaKey && !e.ctrlKey) { + e.preventDefault(); + setConflictResolution("Skip"); + } + // K - Keep both + if (e.key === "k" && !e.metaKey && !e.ctrlKey) { + e.preventDefault(); + setConflictResolution("AutoModifyName"); + } + // O - Overwrite + if (e.key === "o" && !e.metaKey && !e.ctrlKey) { + e.preventDefault(); + setConflictResolution("Overwrite"); + } + }; - window.addEventListener("keydown", handleKeyDown); - return () => window.removeEventListener("keydown", handleKeyDown); - }, [phase.type, operation, conflictResolution]); + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, [phase.type, operation, conflictResolution]); - // Executing state - if (phase.type === "executing") { - return ( - } - hideButtons - > -
    -
    - - - {operation === "copy" ? "Copying files..." : "Moving files..."} - -
    -
    -
    - ); - } + // Executing state + if (phase.type === "executing") { + return ( + } + title={operation === "copy" ? "Copying Files" : "Moving Files"} + > +
    +
    + + + {operation === "copy" ? "Copying files..." : "Moving files..."} + +
    +
    +
    + ); + } - // Error state - if (phase.type === "error") { - return ( - } - ctaLabel="Close" - onSubmit={handleCancel} - > -
    -
    - -
    -
    Error
    -
    {phase.message}
    -
    -
    -
    -
    - ); - } + // Error state + if (phase.type === "error") { + return ( + } + onSubmit={handleCancel} + title="Operation Failed" + > +
    +
    + +
    +
    Error
    +
    {phase.message}
    +
    +
    +
    +
    + ); + } - const sourceCount = props.sources.length; - const pluralItems = sourceCount === 1 ? "item" : "items"; + const sourceCount = props.sources.length; + const pluralItems = sourceCount === 1 ? "item" : "items"; - // Form state - let user choose operation and conflict resolution - return ( - } - ctaLabel={operation === "copy" ? "Copy" : "Move"} - onSubmit={handleSubmit} - onCancelled={handleCancel} - formClassName="!min-w-[400px] !max-w-[400px]" - > -
    - {/* Source → Destination visual */} -
    - {/* Source */} -
    - {sourceFiles.length > 0 ? ( - <> - {sourceFiles.length === 1 ? ( - - ) : ( - - )} -
    -
    Source
    - {sourceFiles.length === 1 ? ( -
    - {sourceFiles[0].name} -
    - ) : ( -
    - {sourceCount} {pluralItems} -
    - )} -
    - - ) : ( - <> - -
    -
    Source
    -
    - {sourceCount} {pluralItems} -
    -
    - - )} -
    + // Form state - let user choose operation and conflict resolution + return ( + } + onCancelled={handleCancel} + onSubmit={handleSubmit} + title="File Operation" + > +
    + {/* Source → Destination visual */} +
    + {/* Source */} +
    + {sourceFiles.length > 0 ? ( + <> + {sourceFiles.length === 1 ? ( + + ) : ( + + )} +
    +
    Source
    + {sourceFiles.length === 1 ? ( +
    + {sourceFiles[0].name} +
    + ) : ( +
    + {sourceCount} {pluralItems} +
    + )} +
    + + ) : ( + <> + +
    +
    Source
    +
    + {sourceCount} {pluralItems} +
    +
    + + )} +
    - {/* Arrow */} -
    - -
    + {/* Arrow */} +
    + +
    - {/* Destination */} -
    - {destFile ? ( - <> - -
    -
    To
    -
    - {destFile.name} -
    -
    - - ) : ( - <> - -
    -
    To
    -
    - {getFileName(props.destination)} -
    -
    - - )} -
    -
    + {/* Destination */} +
    + {destFile ? ( + <> + +
    +
    To
    +
    + {destFile.name} +
    +
    + + ) : ( + <> + +
    +
    To
    +
    + {getFileName(props.destination)} +
    +
    + + )} +
    +
    - {/* Operation type selection */} -
    -
    - Operation: -
    -
    - - -
    -
    + {/* Operation type selection */} +
    +
    + Operation: +
    +
    + + +
    +
    - {/* Conflict resolution options */} -
    -
    - If files already exist: -
    -
    - {[ - { value: "Skip", label: "Skip existing files", key: "S" }, - { value: "AutoModifyName", label: "Keep both (rename new files)", key: "K" }, - { value: "Overwrite", label: "Overwrite existing files", key: "O" }, - ].map((option) => ( - - ))} -
    -
    -
    -
    - ); + {/* Conflict resolution options */} +
    +
    + If files already exist: +
    +
    + {[ + { value: "Skip", label: "Skip existing files", key: "S" }, + { + value: "AutoModifyName", + label: "Keep both (rename new files)", + key: "K", + }, + { + value: "Overwrite", + label: "Overwrite existing files", + key: "O", + }, + ].map((option) => ( + + ))} +
    +
    +
    +
    + ); } // Utility functions function getFileName(path: SdPath): string { - if (!path || typeof path !== "object") { - return "Unknown"; - } + if (!path || typeof path !== "object") { + return "Unknown"; + } - if ("Physical" in path && path.Physical) { - const pathStr = path.Physical.path || ""; - const parts = pathStr.split("/"); - return parts[parts.length - 1] || pathStr; - } + if ("Physical" in path && path.Physical) { + const pathStr = path.Physical.path || ""; + const parts = pathStr.split("/"); + return parts[parts.length - 1] || pathStr; + } - if ("Cloud" in path && path.Cloud) { - const pathStr = path.Cloud.path || ""; - const parts = pathStr.split("/"); - return parts[parts.length - 1] || pathStr; - } + if ("Cloud" in path && path.Cloud) { + const pathStr = path.Cloud.path || ""; + const parts = pathStr.split("/"); + return parts[parts.length - 1] || pathStr; + } - return "Unknown"; + return "Unknown"; } function formatDestination(path: SdPath): string { - if (!path || typeof path !== "object") { - return "Unknown"; - } + if (!path || typeof path !== "object") { + return "Unknown"; + } - if ("Physical" in path && path.Physical) { - return path.Physical.path || "Unknown"; - } + if ("Physical" in path && path.Physical) { + return path.Physical.path || "Unknown"; + } - if ("Cloud" in path && path.Cloud) { - return path.Cloud.path || "Unknown"; - } + if ("Cloud" in path && path.Cloud) { + return path.Cloud.path || "Unknown"; + } - if ("Content" in path && path.Content) { - return `Content: ${path.Content.content_id}`; - } + if ("Content" in path && path.Content) { + return `Content: ${path.Content.content_id}`; + } - if ("Sidecar" in path && path.Sidecar) { - return `Sidecar: ${path.Sidecar.entry_id}`; - } + if ("Sidecar" in path && path.Sidecar) { + return `Sidecar: ${path.Sidecar.entry_id}`; + } - return "Unknown"; -} \ No newline at end of file + return "Unknown"; +} diff --git a/packages/interface/src/components/modals/PairingModal.tsx b/packages/interface/src/components/modals/PairingModal.tsx index 88150d98d..d64616ae1 100644 --- a/packages/interface/src/components/modals/PairingModal.tsx +++ b/packages/interface/src/components/modals/PairingModal.tsx @@ -1,19 +1,22 @@ -import { useState, useEffect, useRef } from "react"; import { - QrCode, - X, ArrowsClockwise, - Check, - Warning, - DeviceMobile, - Copy, CaretDown, + Check, + Copy, + DeviceMobile, + QrCode, + Warning, + X, } from "@phosphor-icons/react"; -import { motion, AnimatePresence } from "framer-motion"; -import clsx from "clsx"; -import QRCode from "qrcode"; -import { useCoreMutation, useCoreQuery } from "../../contexts/SpacedriveContext"; import { sounds } from "@sd/assets/sounds"; +import clsx from "clsx"; +import { AnimatePresence, motion } from "framer-motion"; +import QRCode from "qrcode"; +import { useEffect, useRef, useState } from "react"; +import { + useCoreMutation, + useCoreQuery, +} from "../../contexts/SpacedriveContext"; interface PairingModalProps { isOpen: boolean; @@ -21,7 +24,11 @@ interface PairingModalProps { mode?: "generate" | "join"; } -export function PairingModal({ isOpen, onClose, mode: initialMode = "generate" }: PairingModalProps) { +export function PairingModal({ + isOpen, + onClose, + mode: initialMode = "generate", +}: PairingModalProps) { const [mode, setMode] = useState<"generate" | "join">(initialMode); const [joinCode, setJoinCode] = useState(""); const [joinNodeId, setJoinNodeId] = useState(""); @@ -82,7 +89,8 @@ export function PairingModal({ isOpen, onClose, mode: initialMode = "generate" } }; // Check if pairing completed - const isCompleted = currentSession?.state === "Completed" || joinPairing.isSuccess; + const isCompleted = + currentSession?.state === "Completed" || joinPairing.isSuccess; useEffect(() => { if (isCompleted) { @@ -101,99 +109,107 @@ export function PairingModal({ isOpen, onClose, mode: initialMode = "generate" }
    {/* Backdrop */} {/* Modal */} {/* Header */} -
    +
    -

    Device Pairing

    -

    Connect another device to share files

    +

    + Device Pairing +

    +

    + Connect another device to share files +

    {/* Mode Tabs */} -
    +
    {/* Content */} -
    +
    {mode === "generate" ? ( ) : ( )} {/* Success State */} {isCompleted && (
    -

    Pairing successful!

    -

    - {joinPairing.data ? `Connected to ${joinPairing.data.device_name}` : "Device paired"} +

    + Pairing successful! +

    +

    + {joinPairing.data + ? `Connected to ${joinPairing.data.device_name}` + : "Device paired"}

    @@ -225,62 +241,30 @@ function GenerateMode({ return ( <> - {!hasCode ? ( - <> - {/* Setup */} -
    -
    -
    - -
    -
    -

    How it works

    -

    - Generate a secure code to share with another device. They'll enter the code to establish a trusted connection. -

    -
    -
    -
    - - {/* Generate Button */} - - - ) : ( + {hasCode ? ( <> {/* Generated Code Display */}
    {/* QR Code */}
    -

    Scan with mobile device

    -
    +

    Scan with mobile device

    +
    {/* Word Code */}
    -