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 bfae9d8e0..2d2afb73a 100755 Binary files a/bun.lockb and b/bun.lockb differ 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 */}
-