@databricks/appkit-ui 0.18.0 → 0.19.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/CLAUDE.md +8 -1
  2. package/dist/cli/commands/plugin/create/scaffold.js +2 -8
  3. package/dist/cli/commands/plugin/create/scaffold.js.map +1 -1
  4. package/dist/react/file-browser/directory-list.d.ts +54 -0
  5. package/dist/react/file-browser/directory-list.d.ts.map +1 -0
  6. package/dist/react/file-browser/directory-list.js +74 -0
  7. package/dist/react/file-browser/directory-list.js.map +1 -0
  8. package/dist/react/file-browser/file-breadcrumb.d.ts +25 -0
  9. package/dist/react/file-browser/file-breadcrumb.d.ts.map +1 -0
  10. package/dist/react/file-browser/file-breadcrumb.js +27 -0
  11. package/dist/react/file-browser/file-breadcrumb.js.map +1 -0
  12. package/dist/react/file-browser/file-entry.d.ts +27 -0
  13. package/dist/react/file-browser/file-entry.d.ts.map +1 -0
  14. package/dist/react/file-browser/file-entry.js +31 -0
  15. package/dist/react/file-browser/file-entry.js.map +1 -0
  16. package/dist/react/file-browser/file-preview-panel.d.ts +42 -0
  17. package/dist/react/file-browser/file-preview-panel.d.ts.map +1 -0
  18. package/dist/react/file-browser/file-preview-panel.js +135 -0
  19. package/dist/react/file-browser/file-preview-panel.js.map +1 -0
  20. package/dist/react/file-browser/index.d.ts +7 -0
  21. package/dist/react/file-browser/index.js +6 -0
  22. package/dist/react/file-browser/new-folder-input.d.ts +36 -0
  23. package/dist/react/file-browser/new-folder-input.d.ts.map +1 -0
  24. package/dist/react/file-browser/new-folder-input.js +52 -0
  25. package/dist/react/file-browser/new-folder-input.js.map +1 -0
  26. package/dist/react/file-browser/types.d.ts +52 -0
  27. package/dist/react/file-browser/types.d.ts.map +1 -0
  28. package/dist/react/genie/genie-chat-message-list.js +1 -1
  29. package/dist/react/genie/genie-chat-message.js +1 -1
  30. package/dist/react/index.d.ts +9 -1
  31. package/dist/react/index.js +12 -5
  32. package/dist/react/lib/format.d.ts +14 -0
  33. package/dist/react/lib/format.d.ts.map +1 -0
  34. package/dist/react/lib/format.js +17 -1
  35. package/dist/react/lib/format.js.map +1 -1
  36. package/dist/react/table/data-table.js +1 -1
  37. package/dist/react/table/table-wrapper.js +1 -1
  38. package/dist/react/ui/breadcrumb.js +1 -1
  39. package/dist/react/ui/index.js +3 -3
  40. package/dist/react/ui/navigation-menu.js +1 -1
  41. package/dist/react/ui/sidebar.js +1 -1
  42. package/docs/api/appkit/Class.Plugin.md +60 -12
  43. package/docs/api/appkit/Class.ResourceRegistry.md +3 -3
  44. package/docs/api/appkit/Function.createApp.md +3 -3
  45. package/docs/api/appkit/Interface.PluginManifest.md +9 -3
  46. package/docs/api/appkit/TypeAlias.PluginData.md +45 -0
  47. package/docs/api/appkit/TypeAlias.ToPlugin.md +1 -1
  48. package/docs/api/appkit-ui/files/DirectoryList.md +36 -0
  49. package/docs/api/appkit-ui/files/FileBreadcrumb.md +27 -0
  50. package/docs/api/appkit-ui/files/FileEntry.md +27 -0
  51. package/docs/api/appkit-ui/files/FilePreviewPanel.md +32 -0
  52. package/docs/api/appkit-ui/files/NewFolderInput.md +30 -0
  53. package/docs/api/appkit.md +1 -0
  54. package/docs/configuration.md +15 -0
  55. package/docs/plugins/custom-plugins.md +4 -13
  56. package/docs/plugins/files.md +350 -0
  57. package/docs/plugins.md +2 -1
  58. package/llms.txt +8 -1
  59. package/package.json +1 -1
package/CLAUDE.md CHANGED
@@ -44,6 +44,7 @@ npx @databricks/appkit docs <query>
44
44
  - [Caching](./docs/plugins/caching.md): AppKit provides both global and plugin-level caching capabilities.
45
45
  - [Creating custom plugins](./docs/plugins/custom-plugins.md): If you need custom API routes or background logic, implement an AppKit plugin. The fastest way is to use the CLI:
46
46
  - [Execution context](./docs/plugins/execution-context.md): AppKit manages Databricks authentication via two contexts:
47
+ - [Files plugin](./docs/plugins/files.md): File operations against Databricks Unity Catalog Volumes. Supports listing, reading, downloading, uploading, deleting, and previewing files with built-in caching, retry, and timeout handling via the execution interceptor pipeline.
47
48
  - [Genie plugin](./docs/plugins/genie.md): Integrates Databricks AI/BI Genie spaces into your AppKit application, enabling natural language data queries via a conversational interface.
48
49
  - [Lakebase plugin](./docs/plugins/lakebase.md): Currently, the Lakebase plugin currently requires a one-time manual setup to connect your Databricks App with your Lakebase database. An automated setup process is planned for an upcoming future release.
49
50
  - [Plugin management](./docs/plugins/plugin-management.md): AppKit includes a CLI for managing plugins. All commands are available under npx @databricks/appkit plugin.
@@ -83,7 +84,7 @@ npx @databricks/appkit docs <query>
83
84
  - [Interface: GenerateDatabaseCredentialRequest](./docs/api/appkit/Interface.GenerateDatabaseCredentialRequest.md): Request parameters for generating database OAuth credentials
84
85
  - [Interface: ITelemetry](./docs/api/appkit/Interface.ITelemetry.md): Plugin-facing interface for OpenTelemetry instrumentation.
85
86
  - [Interface: LakebasePoolConfig](./docs/api/appkit/Interface.LakebasePoolConfig.md): Configuration for creating a Lakebase connection pool
86
- - [Interface: PluginManifest](./docs/api/appkit/Interface.PluginManifest.md): Plugin manifest that declares metadata and resource requirements.
87
+ - [Interface: PluginManifest<TName>](./docs/api/appkit/Interface.PluginManifest.md): Plugin manifest that declares metadata and resource requirements.
87
88
  - [Interface: RequestedClaims](./docs/api/appkit/Interface.RequestedClaims.md): Optional claims for fine-grained Unity Catalog table permissions
88
89
  - [Interface: RequestedResource](./docs/api/appkit/Interface.RequestedResource.md): Resource to request permissions for in Unity Catalog
89
90
  - [Interface: ResourceEntry](./docs/api/appkit/Interface.ResourceEntry.md): Internal representation of a resource in the registry.
@@ -94,6 +95,7 @@ npx @databricks/appkit docs <query>
94
95
  - [Interface: ValidationResult](./docs/api/appkit/Interface.ValidationResult.md): Result of validating all registered resources against the environment.
95
96
  - [Type Alias: ConfigSchema](./docs/api/appkit/TypeAlias.ConfigSchema.md): Configuration schema definition for plugin config.
96
97
  - [Type Alias: IAppRouter](./docs/api/appkit/TypeAlias.IAppRouter.md): Express router type for plugin route registration
98
+ - [Type Alias: PluginData<T, U, N>](./docs/api/appkit/TypeAlias.PluginData.md): Type Parameters
97
99
  - [Type Alias: ResourcePermission](./docs/api/appkit/TypeAlias.ResourcePermission.md): Union of all possible permission levels across all resource types.
98
100
  - [Type Alias: ToPlugin()<T, U, N>](./docs/api/appkit/TypeAlias.ToPlugin.md): Type Parameters
99
101
  - [Variable: sql](./docs/api/appkit/Variable.sql.md): SQL helper namespace
@@ -110,6 +112,11 @@ npx @databricks/appkit docs <query>
110
112
  - [PieChart](./docs/api/appkit-ui/data/PieChart.md): Pie Chart component for proportional data visualization.
111
113
  - [RadarChart](./docs/api/appkit-ui/data/RadarChart.md): Radar Chart component for multi-dimensional data comparison.
112
114
  - [ScatterChart](./docs/api/appkit-ui/data/ScatterChart.md): Scatter Chart component for correlation and distribution visualization.
115
+ - [DirectoryList](./docs/api/appkit-ui/files/DirectoryList.md): Card-wrapped directory listing with loading, error, and empty states
116
+ - [FileBreadcrumb](./docs/api/appkit-ui/files/FileBreadcrumb.md): Path-aware breadcrumb navigation built on top of Breadcrumb primitives
117
+ - [FileEntry](./docs/api/appkit-ui/files/FileEntry.md): Single file or directory row with icon, name, size, and selection state
118
+ - [FilePreviewPanel](./docs/api/appkit-ui/files/FilePreviewPanel.md): Preview panel displaying file metadata, image/text preview, and download/delete actions
119
+ - [NewFolderInput](./docs/api/appkit-ui/files/NewFolderInput.md): Inline folder-name input with create/cancel actions
113
120
  - [GenieChat](./docs/api/appkit-ui/genie/GenieChat.md): Full-featured chat interface for a single Databricks AI/BI Genie space. Handles message streaming, conversation history, and auto-reconnection via SSE.
114
121
  - [GenieChatInput](./docs/api/appkit-ui/genie/GenieChatInput.md): Auto-expanding textarea input with a send button for chat messages. Submits on Enter (Shift+Enter for newline).
115
122
  - [GenieChatMessage](./docs/api/appkit-ui/genie/GenieChatMessage.md): Renders a single Genie message bubble with optional expandable SQL query attachments.
@@ -92,9 +92,7 @@ function scaffoldPlugin(targetDir, answers, options) {
92
92
  import manifest from "./manifest.json";
93
93
 
94
94
  export class ${className} extends Plugin {
95
- name = "${answers.name}";
96
-
97
- static manifest = manifest as PluginManifest;
95
+ static manifest = manifest as PluginManifest<"${answers.name}">;
98
96
 
99
97
  injectRoutes(router: IAppRouter): void {
100
98
  // Add your routes here, e.g.:
@@ -109,11 +107,7 @@ export class ${className} extends Plugin {
109
107
  }
110
108
  }
111
109
 
112
- export const ${exportName} = toPlugin<
113
- typeof ${className},
114
- Record<string, never>,
115
- "${answers.name}"
116
- >(${className}, "${answers.name}");
110
+ export const ${exportName} = toPlugin(${className});
117
111
  `;
118
112
  writeTracked(path.join(targetDir, `${answers.name}.ts`), pluginTs, written);
119
113
  const indexTs = `export { ${className}, ${exportName} } from "./${answers.name}";
@@ -1 +1 @@
1
- {"version":3,"file":"scaffold.js","names":[],"sources":["../../../../../src/cli/commands/plugin/create/scaffold.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { humanizeResourceType, MANIFEST_SCHEMA_ID } from \"./resource-defaults\";\nimport type { CreateAnswers } from \"./types\";\n\n/** Convert kebab-name to PascalCase (e.g. my-plugin -> MyPlugin). */\nfunction toPascalCase(name: string): string {\n return name\n .split(\"-\")\n .map((s) => s.charAt(0).toUpperCase() + s.slice(1).toLowerCase())\n .join(\"\");\n}\n\n/** Convert kebab-name to camelCase (e.g. my-plugin -> myPlugin). */\nfunction toCamelCase(name: string): string {\n const pascal = toPascalCase(name);\n return pascal.charAt(0).toLowerCase() + pascal.slice(1);\n}\n\n/** Build manifest.json resources from selected resources. */\nfunction buildManifestResources(answers: CreateAnswers) {\n const required: unknown[] = [];\n const optional: unknown[] = [];\n\n for (const r of answers.resources) {\n const alias = humanizeResourceType(r.type);\n const entry = {\n type: r.type,\n alias,\n resourceKey: r.resourceKey,\n description: r.description || `Required for ${alias} functionality.`,\n permission: r.permission,\n fields: r.fields,\n };\n if (r.required) {\n required.push(entry);\n } else {\n optional.push(entry);\n }\n }\n\n return { required, optional };\n}\n\n/** Build full manifest object for manifest.json. */\nfunction buildManifest(answers: CreateAnswers): Record<string, unknown> {\n const { required, optional } = buildManifestResources(answers);\n const manifest: Record<string, unknown> = {\n $schema: MANIFEST_SCHEMA_ID,\n name: answers.name,\n displayName: answers.displayName,\n description: answers.description,\n resources: { required, optional },\n };\n if (answers.author) manifest.author = answers.author;\n manifest.version = answers.version || \"0.1.0\";\n if (answers.license) manifest.license = answers.license;\n return manifest;\n}\n\n/** Resolve absolute target directory from cwd and answers. */\nexport function resolveTargetDir(cwd: string, answers: CreateAnswers): string {\n return path.resolve(cwd, answers.targetPath);\n}\n\n/** Track files written during scaffolding for rollback on failure. */\nfunction writeTracked(\n filePath: string,\n content: string,\n written: string[],\n): void {\n fs.writeFileSync(filePath, content);\n written.push(filePath);\n}\n\n/** Remove files written during a failed scaffold attempt. */\nfunction rollback(written: string[], targetDir: string): void {\n for (const filePath of written.reverse()) {\n try {\n fs.unlinkSync(filePath);\n } catch {\n // best-effort cleanup\n }\n }\n try {\n const remaining = fs.readdirSync(targetDir);\n if (remaining.length === 0) fs.rmdirSync(targetDir);\n } catch {\n // directory may not be empty or may have been removed already\n }\n}\n\n/**\n * Scaffold plugin files into targetDir. Pure: no interactive I/O.\n * Writes manifest.json, {name}.ts, index.ts; for isolated also package.json, tsconfig.json, README.md.\n * On failure, rolls back any files already written.\n */\nexport function scaffoldPlugin(\n targetDir: string,\n answers: CreateAnswers,\n options: { isolated: boolean },\n): void {\n fs.mkdirSync(targetDir, { recursive: true });\n\n const written: string[] = [];\n\n try {\n const manifest = buildManifest(answers);\n const className = toPascalCase(answers.name);\n const exportName = toCamelCase(answers.name);\n\n writeTracked(\n path.join(targetDir, \"manifest.json\"),\n `${JSON.stringify(manifest, null, 2)}\\n`,\n written,\n );\n\n const pluginTs = `import {\n Plugin,\n toPlugin,\n type IAppRouter,\n type PluginManifest,\n} from \"@databricks/appkit\";\nimport manifest from \"./manifest.json\";\n\nexport class ${className} extends Plugin {\n name = \"${answers.name}\";\n\n static manifest = manifest as PluginManifest;\n\n injectRoutes(router: IAppRouter): void {\n // Add your routes here, e.g.:\n // this.route(router, {\n // name: \"example\",\n // method: \"get\",\n // path: \"/\",\n // handler: async (_req, res) => {\n // res.json({ message: \"Hello from ${answers.name}\" });\n // },\n // });\n }\n}\n\nexport const ${exportName} = toPlugin<\n typeof ${className},\n Record<string, never>,\n \"${answers.name}\"\n>(${className}, \"${answers.name}\");\n`;\n\n writeTracked(path.join(targetDir, `${answers.name}.ts`), pluginTs, written);\n\n const indexTs = `export { ${className}, ${exportName} } from \"./${answers.name}\";\n`;\n\n writeTracked(path.join(targetDir, \"index.ts\"), indexTs, written);\n\n if (options.isolated) {\n const packageName =\n answers.name.includes(\"/\") || answers.name.startsWith(\"@\")\n ? answers.name\n : `appkit-plugin-${answers.name}`;\n\n const packageJson = {\n name: packageName,\n version: answers.version || \"0.1.0\",\n type: \"module\",\n main: \"./dist/index.js\",\n types: \"./dist/index.d.ts\",\n files: [\"dist\"],\n scripts: {\n build: \"tsc\",\n typecheck: \"tsc --noEmit\",\n },\n peerDependencies: {\n \"@databricks/appkit\": \">=0.5.0\",\n },\n devDependencies: {\n typescript: \"^5.0.0\",\n },\n };\n\n writeTracked(\n path.join(targetDir, \"package.json\"),\n `${JSON.stringify(packageJson, null, 2)}\\n`,\n written,\n );\n\n const tsconfigJson = {\n compilerOptions: {\n target: \"ES2022\",\n module: \"NodeNext\",\n moduleResolution: \"NodeNext\",\n outDir: \"dist\",\n rootDir: \".\",\n declaration: true,\n strict: true,\n skipLibCheck: true,\n },\n include: [\"*.ts\"],\n exclude: [\"node_modules\", \"dist\"],\n };\n\n writeTracked(\n path.join(targetDir, \"tsconfig.json\"),\n `${JSON.stringify(tsconfigJson, null, 2)}\\n`,\n written,\n );\n\n const readme = `# ${answers.displayName}\n\n${answers.description}\n\n## Installation\n\n\\`\\`\\`bash\npnpm add ${packageName} @databricks/appkit\n\\`\\`\\`\n\n## Usage\n\nRegister the plugin in your AppKit app:\n\n\\`\\`\\`ts\nimport { createApp } from \"@databricks/appkit\";\nimport { ${exportName} } from \"${packageName}\";\n\ncreateApp({\n plugins: [\n ${exportName}(),\n // ... other plugins\n ],\n}).then((app) => { /* ... */ });\n\\`\\`\\`\n`;\n\n writeTracked(path.join(targetDir, \"README.md\"), readme, written);\n }\n } catch (err) {\n rollback(written, targetDir);\n throw err;\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"scaffold.js","names":[],"sources":["../../../../../src/cli/commands/plugin/create/scaffold.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { humanizeResourceType, MANIFEST_SCHEMA_ID } from \"./resource-defaults\";\nimport type { CreateAnswers } from \"./types\";\n\n/** Convert kebab-name to PascalCase (e.g. my-plugin -> MyPlugin). */\nfunction toPascalCase(name: string): string {\n return name\n .split(\"-\")\n .map((s) => s.charAt(0).toUpperCase() + s.slice(1).toLowerCase())\n .join(\"\");\n}\n\n/** Convert kebab-name to camelCase (e.g. my-plugin -> myPlugin). */\nfunction toCamelCase(name: string): string {\n const pascal = toPascalCase(name);\n return pascal.charAt(0).toLowerCase() + pascal.slice(1);\n}\n\n/** Build manifest.json resources from selected resources. */\nfunction buildManifestResources(answers: CreateAnswers) {\n const required: unknown[] = [];\n const optional: unknown[] = [];\n\n for (const r of answers.resources) {\n const alias = humanizeResourceType(r.type);\n const entry = {\n type: r.type,\n alias,\n resourceKey: r.resourceKey,\n description: r.description || `Required for ${alias} functionality.`,\n permission: r.permission,\n fields: r.fields,\n };\n if (r.required) {\n required.push(entry);\n } else {\n optional.push(entry);\n }\n }\n\n return { required, optional };\n}\n\n/** Build full manifest object for manifest.json. */\nfunction buildManifest(answers: CreateAnswers): Record<string, unknown> {\n const { required, optional } = buildManifestResources(answers);\n const manifest: Record<string, unknown> = {\n $schema: MANIFEST_SCHEMA_ID,\n name: answers.name,\n displayName: answers.displayName,\n description: answers.description,\n resources: { required, optional },\n };\n if (answers.author) manifest.author = answers.author;\n manifest.version = answers.version || \"0.1.0\";\n if (answers.license) manifest.license = answers.license;\n return manifest;\n}\n\n/** Resolve absolute target directory from cwd and answers. */\nexport function resolveTargetDir(cwd: string, answers: CreateAnswers): string {\n return path.resolve(cwd, answers.targetPath);\n}\n\n/** Track files written during scaffolding for rollback on failure. */\nfunction writeTracked(\n filePath: string,\n content: string,\n written: string[],\n): void {\n fs.writeFileSync(filePath, content);\n written.push(filePath);\n}\n\n/** Remove files written during a failed scaffold attempt. */\nfunction rollback(written: string[], targetDir: string): void {\n for (const filePath of written.reverse()) {\n try {\n fs.unlinkSync(filePath);\n } catch {\n // best-effort cleanup\n }\n }\n try {\n const remaining = fs.readdirSync(targetDir);\n if (remaining.length === 0) fs.rmdirSync(targetDir);\n } catch {\n // directory may not be empty or may have been removed already\n }\n}\n\n/**\n * Scaffold plugin files into targetDir. Pure: no interactive I/O.\n * Writes manifest.json, {name}.ts, index.ts; for isolated also package.json, tsconfig.json, README.md.\n * On failure, rolls back any files already written.\n */\nexport function scaffoldPlugin(\n targetDir: string,\n answers: CreateAnswers,\n options: { isolated: boolean },\n): void {\n fs.mkdirSync(targetDir, { recursive: true });\n\n const written: string[] = [];\n\n try {\n const manifest = buildManifest(answers);\n const className = toPascalCase(answers.name);\n const exportName = toCamelCase(answers.name);\n\n writeTracked(\n path.join(targetDir, \"manifest.json\"),\n `${JSON.stringify(manifest, null, 2)}\\n`,\n written,\n );\n\n const pluginTs = `import {\n Plugin,\n toPlugin,\n type IAppRouter,\n type PluginManifest,\n} from \"@databricks/appkit\";\nimport manifest from \"./manifest.json\";\n\nexport class ${className} extends Plugin {\n static manifest = manifest as PluginManifest<\"${answers.name}\">;\n\n injectRoutes(router: IAppRouter): void {\n // Add your routes here, e.g.:\n // this.route(router, {\n // name: \"example\",\n // method: \"get\",\n // path: \"/\",\n // handler: async (_req, res) => {\n // res.json({ message: \"Hello from ${answers.name}\" });\n // },\n // });\n }\n}\n\nexport const ${exportName} = toPlugin(${className});\n`;\n\n writeTracked(path.join(targetDir, `${answers.name}.ts`), pluginTs, written);\n\n const indexTs = `export { ${className}, ${exportName} } from \"./${answers.name}\";\n`;\n\n writeTracked(path.join(targetDir, \"index.ts\"), indexTs, written);\n\n if (options.isolated) {\n const packageName =\n answers.name.includes(\"/\") || answers.name.startsWith(\"@\")\n ? answers.name\n : `appkit-plugin-${answers.name}`;\n\n const packageJson = {\n name: packageName,\n version: answers.version || \"0.1.0\",\n type: \"module\",\n main: \"./dist/index.js\",\n types: \"./dist/index.d.ts\",\n files: [\"dist\"],\n scripts: {\n build: \"tsc\",\n typecheck: \"tsc --noEmit\",\n },\n peerDependencies: {\n \"@databricks/appkit\": \">=0.5.0\",\n },\n devDependencies: {\n typescript: \"^5.0.0\",\n },\n };\n\n writeTracked(\n path.join(targetDir, \"package.json\"),\n `${JSON.stringify(packageJson, null, 2)}\\n`,\n written,\n );\n\n const tsconfigJson = {\n compilerOptions: {\n target: \"ES2022\",\n module: \"NodeNext\",\n moduleResolution: \"NodeNext\",\n outDir: \"dist\",\n rootDir: \".\",\n declaration: true,\n strict: true,\n skipLibCheck: true,\n },\n include: [\"*.ts\"],\n exclude: [\"node_modules\", \"dist\"],\n };\n\n writeTracked(\n path.join(targetDir, \"tsconfig.json\"),\n `${JSON.stringify(tsconfigJson, null, 2)}\\n`,\n written,\n );\n\n const readme = `# ${answers.displayName}\n\n${answers.description}\n\n## Installation\n\n\\`\\`\\`bash\npnpm add ${packageName} @databricks/appkit\n\\`\\`\\`\n\n## Usage\n\nRegister the plugin in your AppKit app:\n\n\\`\\`\\`ts\nimport { createApp } from \"@databricks/appkit\";\nimport { ${exportName} } from \"${packageName}\";\n\ncreateApp({\n plugins: [\n ${exportName}(),\n // ... other plugins\n ],\n}).then((app) => { /* ... */ });\n\\`\\`\\`\n`;\n\n writeTracked(path.join(targetDir, \"README.md\"), readme, written);\n }\n } catch (err) {\n rollback(written, targetDir);\n throw err;\n }\n}\n"],"mappings":";;;;;;AAMA,SAAS,aAAa,MAAsB;AAC1C,QAAO,KACJ,MAAM,IAAI,CACV,KAAK,MAAM,EAAE,OAAO,EAAE,CAAC,aAAa,GAAG,EAAE,MAAM,EAAE,CAAC,aAAa,CAAC,CAChE,KAAK,GAAG;;;AAIb,SAAS,YAAY,MAAsB;CACzC,MAAM,SAAS,aAAa,KAAK;AACjC,QAAO,OAAO,OAAO,EAAE,CAAC,aAAa,GAAG,OAAO,MAAM,EAAE;;;AAIzD,SAAS,uBAAuB,SAAwB;CACtD,MAAM,WAAsB,EAAE;CAC9B,MAAM,WAAsB,EAAE;AAE9B,MAAK,MAAM,KAAK,QAAQ,WAAW;EACjC,MAAM,QAAQ,qBAAqB,EAAE,KAAK;EAC1C,MAAM,QAAQ;GACZ,MAAM,EAAE;GACR;GACA,aAAa,EAAE;GACf,aAAa,EAAE,eAAe,gBAAgB,MAAM;GACpD,YAAY,EAAE;GACd,QAAQ,EAAE;GACX;AACD,MAAI,EAAE,SACJ,UAAS,KAAK,MAAM;MAEpB,UAAS,KAAK,MAAM;;AAIxB,QAAO;EAAE;EAAU;EAAU;;;AAI/B,SAAS,cAAc,SAAiD;CACtE,MAAM,EAAE,UAAU,aAAa,uBAAuB,QAAQ;CAC9D,MAAM,WAAoC;EACxC,SAAS;EACT,MAAM,QAAQ;EACd,aAAa,QAAQ;EACrB,aAAa,QAAQ;EACrB,WAAW;GAAE;GAAU;GAAU;EAClC;AACD,KAAI,QAAQ,OAAQ,UAAS,SAAS,QAAQ;AAC9C,UAAS,UAAU,QAAQ,WAAW;AACtC,KAAI,QAAQ,QAAS,UAAS,UAAU,QAAQ;AAChD,QAAO;;;AAIT,SAAgB,iBAAiB,KAAa,SAAgC;AAC5E,QAAO,KAAK,QAAQ,KAAK,QAAQ,WAAW;;;AAI9C,SAAS,aACP,UACA,SACA,SACM;AACN,IAAG,cAAc,UAAU,QAAQ;AACnC,SAAQ,KAAK,SAAS;;;AAIxB,SAAS,SAAS,SAAmB,WAAyB;AAC5D,MAAK,MAAM,YAAY,QAAQ,SAAS,CACtC,KAAI;AACF,KAAG,WAAW,SAAS;SACjB;AAIV,KAAI;AAEF,MADkB,GAAG,YAAY,UAAU,CAC7B,WAAW,EAAG,IAAG,UAAU,UAAU;SAC7C;;;;;;;AAUV,SAAgB,eACd,WACA,SACA,SACM;AACN,IAAG,UAAU,WAAW,EAAE,WAAW,MAAM,CAAC;CAE5C,MAAM,UAAoB,EAAE;AAE5B,KAAI;EACF,MAAM,WAAW,cAAc,QAAQ;EACvC,MAAM,YAAY,aAAa,QAAQ,KAAK;EAC5C,MAAM,aAAa,YAAY,QAAQ,KAAK;AAE5C,eACE,KAAK,KAAK,WAAW,gBAAgB,EACrC,GAAG,KAAK,UAAU,UAAU,MAAM,EAAE,CAAC,KACrC,QACD;EAED,MAAM,WAAW;;;;;;;;eAQN,UAAU;kDACyB,QAAQ,KAAK;;;;;;;;;6CASlB,QAAQ,KAAK;;;;;;eAM3C,WAAW,cAAc,UAAU;;AAG9C,eAAa,KAAK,KAAK,WAAW,GAAG,QAAQ,KAAK,KAAK,EAAE,UAAU,QAAQ;EAE3E,MAAM,UAAU,YAAY,UAAU,IAAI,WAAW,aAAa,QAAQ,KAAK;;AAG/E,eAAa,KAAK,KAAK,WAAW,WAAW,EAAE,SAAS,QAAQ;AAEhE,MAAI,QAAQ,UAAU;GACpB,MAAM,cACJ,QAAQ,KAAK,SAAS,IAAI,IAAI,QAAQ,KAAK,WAAW,IAAI,GACtD,QAAQ,OACR,iBAAiB,QAAQ;GAE/B,MAAM,cAAc;IAClB,MAAM;IACN,SAAS,QAAQ,WAAW;IAC5B,MAAM;IACN,MAAM;IACN,OAAO;IACP,OAAO,CAAC,OAAO;IACf,SAAS;KACP,OAAO;KACP,WAAW;KACZ;IACD,kBAAkB,EAChB,sBAAsB,WACvB;IACD,iBAAiB,EACf,YAAY,UACb;IACF;AAED,gBACE,KAAK,KAAK,WAAW,eAAe,EACpC,GAAG,KAAK,UAAU,aAAa,MAAM,EAAE,CAAC,KACxC,QACD;AAiBD,gBACE,KAAK,KAAK,WAAW,gBAAgB,EACrC,GAAG,KAAK,UAjBW;IACnB,iBAAiB;KACf,QAAQ;KACR,QAAQ;KACR,kBAAkB;KAClB,QAAQ;KACR,SAAS;KACT,aAAa;KACb,QAAQ;KACR,cAAc;KACf;IACD,SAAS,CAAC,OAAO;IACjB,SAAS,CAAC,gBAAgB,OAAO;IAClC,EAIiC,MAAM,EAAE,CAAC,KACzC,QACD;GAED,MAAM,SAAS,KAAK,QAAQ,YAAY;;EAE5C,QAAQ,YAAY;;;;;WAKX,YAAY;;;;;;;;;WASZ,WAAW,WAAW,YAAY;;;;MAIvC,WAAW;;;;;;AAOX,gBAAa,KAAK,KAAK,WAAW,YAAY,EAAE,QAAQ,QAAQ;;UAE3D,KAAK;AACZ,WAAS,SAAS,UAAU;AAC5B,QAAM"}
@@ -0,0 +1,54 @@
1
+ import { DirectoryEntry, FileBrowserLabels } from "./types.js";
2
+ import * as react_jsx_runtime0 from "react/jsx-runtime";
3
+
4
+ //#region src/react/file-browser/directory-list.d.ts
5
+ /** Props for the DirectoryList component */
6
+ interface DirectoryListProps extends Omit<React.ComponentProps<"div">, "children"> {
7
+ /** Directory entries to display */
8
+ entries: DirectoryEntry[];
9
+ /** Whether the directory is currently loading */
10
+ loading?: boolean;
11
+ /** Error message to display */
12
+ error?: string | null;
13
+ /** Called when an entry is clicked */
14
+ onEntryClick: (entry: DirectoryEntry) => void;
15
+ /** Called when the back/parent button is clicked */
16
+ onNavigateToParent?: () => void;
17
+ /** Called when the retry button is clicked */
18
+ onRetry?: () => void;
19
+ /** Whether the user is at the root directory (hides back button) */
20
+ isAtRoot?: boolean;
21
+ /** Currently selected file path for highlighting */
22
+ selectedPath?: string | null;
23
+ /** Resolves a DirectoryEntry to its full path */
24
+ resolveEntryPath: (entry: DirectoryEntry) => string;
25
+ /** Content rendered between the back button and the entry list (e.g., NewFolderInput) */
26
+ headerContent?: React.ReactNode;
27
+ /** Whether a current path is set (affects empty state message) */
28
+ hasCurrentPath?: boolean;
29
+ /** Custom file size formatter */
30
+ formatSize?: (bytes: number | undefined) => string;
31
+ /** Customizable labels */
32
+ labels?: Pick<FileBrowserLabels, "backToParent" | "emptyDirectory" | "noVolumeConfigured" | "retry">;
33
+ }
34
+ /** Card-wrapped directory listing with loading, error, and empty states */
35
+ declare function DirectoryList({
36
+ entries,
37
+ loading,
38
+ error,
39
+ onEntryClick,
40
+ onNavigateToParent,
41
+ onRetry,
42
+ isAtRoot,
43
+ selectedPath,
44
+ resolveEntryPath,
45
+ headerContent,
46
+ hasCurrentPath,
47
+ formatSize,
48
+ labels,
49
+ className,
50
+ ...props
51
+ }: DirectoryListProps): react_jsx_runtime0.JSX.Element;
52
+ //#endregion
53
+ export { DirectoryList, DirectoryListProps };
54
+ //# sourceMappingURL=directory-list.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"directory-list.d.ts","names":[],"sources":["../../../src/react/file-browser/directory-list.tsx"],"mappings":";;;;;UAQiB,kBAAA,SACP,IAAA,CAAK,KAAA,CAAM,cAAA;;EAEnB,OAAA,EAAS,cAAA;EAFT;EAIA,OAAA;EAJa;EAMb,KAAA;EAEsB;EAAtB,YAAA,GAAe,KAAA,EAAO,cAAA;EAYN;EAVhB,kBAAA;EAgBS;EAdT,OAAA;EAZY;EAcZ,QAAA;EAdQ;EAgBR,YAAA;EAhBmB;EAkBnB,gBAAA,GAAmB,KAAA,EAAO,cAAA;EAhBjB;EAkBT,aAAA,GAAgB,KAAA,CAAM,SAAA;EAdtB;EAgBA,cAAA;EAdsB;EAgBtB,UAAA,IAAc,KAAA;EAdd;EAgBA,MAAA,GAAS,IAAA,CACP,iBAAA;AAAA;;iBAMY,aAAA,CAAA;EACd,OAAA;EACA,OAAA;EACA,KAAA;EACA,YAAA;EACA,kBAAA;EACA,OAAA;EACA,QAAA;EACA,YAAA;EACA,gBAAA;EACA,aAAA;EACA,cAAA;EACA,UAAA;EACA,MAAA;EACA,SAAA;EAAA,GACG;AAAA,GACF,kBAAA,GAAkB,kBAAA,CAAA,GAAA,CAAA,OAAA"}
@@ -0,0 +1,74 @@
1
+ import { Button } from "../ui/button.js";
2
+ import { Card } from "../ui/card.js";
3
+ import { Skeleton } from "../ui/skeleton.js";
4
+ import { FileEntry } from "./file-entry.js";
5
+ import { jsx, jsxs } from "react/jsx-runtime";
6
+ import { AlertCircle, ArrowLeft, FileIcon } from "lucide-react";
7
+
8
+ //#region src/react/file-browser/directory-list.tsx
9
+ /** Card-wrapped directory listing with loading, error, and empty states */
10
+ function DirectoryList({ entries, loading, error, onEntryClick, onNavigateToParent, onRetry, isAtRoot, selectedPath, resolveEntryPath, headerContent, hasCurrentPath, formatSize, labels, className, ...props }) {
11
+ return /* @__PURE__ */ jsx("div", {
12
+ "data-slot": "directory-list",
13
+ className,
14
+ ...props,
15
+ children: /* @__PURE__ */ jsxs(Card, {
16
+ className: "p-0 overflow-hidden",
17
+ children: [
18
+ !isAtRoot && onNavigateToParent && /* @__PURE__ */ jsxs("button", {
19
+ type: "button",
20
+ onClick: onNavigateToParent,
21
+ className: "flex items-center gap-2 px-4 py-3 w-full text-left hover:bg-muted/50 border-b text-sm text-muted-foreground transition-colors",
22
+ children: [/* @__PURE__ */ jsx(ArrowLeft, { className: "h-4 w-4" }), labels?.backToParent ?? "Back to parent"]
23
+ }),
24
+ headerContent,
25
+ loading && /* @__PURE__ */ jsxs("div", {
26
+ className: "p-4 space-y-3",
27
+ children: [
28
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-10 w-full" }),
29
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-10 w-full" }),
30
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-10 w-full" })
31
+ ]
32
+ }),
33
+ error && /* @__PURE__ */ jsxs("div", {
34
+ className: "p-6 text-center",
35
+ children: [
36
+ /* @__PURE__ */ jsx(AlertCircle, { className: "h-8 w-8 text-destructive mx-auto mb-2" }),
37
+ /* @__PURE__ */ jsx("p", {
38
+ className: "text-sm text-destructive",
39
+ children: error
40
+ }),
41
+ onRetry && /* @__PURE__ */ jsx(Button, {
42
+ variant: "outline",
43
+ size: "sm",
44
+ className: "mt-3",
45
+ onClick: onRetry,
46
+ children: labels?.retry ?? "Retry"
47
+ })
48
+ ]
49
+ }),
50
+ !loading && !error && entries.length === 0 && /* @__PURE__ */ jsxs("div", {
51
+ className: "p-6 text-center text-muted-foreground",
52
+ children: [/* @__PURE__ */ jsx(FileIcon, { className: "h-8 w-8 mx-auto mb-2 opacity-50" }), /* @__PURE__ */ jsx("p", {
53
+ className: "text-sm",
54
+ children: hasCurrentPath ? labels?.emptyDirectory ?? "This directory is empty." : labels?.noVolumeConfigured ?? "No volume configured. Configure volumes in the files plugin to get started."
55
+ })]
56
+ }),
57
+ !loading && !error && entries.map((entry) => {
58
+ const entryPath = resolveEntryPath(entry);
59
+ return /* @__PURE__ */ jsx(FileEntry, {
60
+ entry,
61
+ entryPath,
62
+ isSelected: selectedPath === entryPath,
63
+ formatSize,
64
+ onClick: () => onEntryClick(entry)
65
+ }, entryPath);
66
+ })
67
+ ]
68
+ })
69
+ });
70
+ }
71
+
72
+ //#endregion
73
+ export { DirectoryList };
74
+ //# sourceMappingURL=directory-list.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"directory-list.js","names":[],"sources":["../../../src/react/file-browser/directory-list.tsx"],"sourcesContent":["import { AlertCircle, ArrowLeft, FileIcon } from \"lucide-react\";\nimport { Button } from \"../ui/button\";\nimport { Card } from \"../ui/card\";\nimport { Skeleton } from \"../ui/skeleton\";\nimport { FileEntry } from \"./file-entry\";\nimport type { DirectoryEntry, FileBrowserLabels } from \"./types\";\n\n/** Props for the DirectoryList component */\nexport interface DirectoryListProps\n extends Omit<React.ComponentProps<\"div\">, \"children\"> {\n /** Directory entries to display */\n entries: DirectoryEntry[];\n /** Whether the directory is currently loading */\n loading?: boolean;\n /** Error message to display */\n error?: string | null;\n /** Called when an entry is clicked */\n onEntryClick: (entry: DirectoryEntry) => void;\n /** Called when the back/parent button is clicked */\n onNavigateToParent?: () => void;\n /** Called when the retry button is clicked */\n onRetry?: () => void;\n /** Whether the user is at the root directory (hides back button) */\n isAtRoot?: boolean;\n /** Currently selected file path for highlighting */\n selectedPath?: string | null;\n /** Resolves a DirectoryEntry to its full path */\n resolveEntryPath: (entry: DirectoryEntry) => string;\n /** Content rendered between the back button and the entry list (e.g., NewFolderInput) */\n headerContent?: React.ReactNode;\n /** Whether a current path is set (affects empty state message) */\n hasCurrentPath?: boolean;\n /** Custom file size formatter */\n formatSize?: (bytes: number | undefined) => string;\n /** Customizable labels */\n labels?: Pick<\n FileBrowserLabels,\n \"backToParent\" | \"emptyDirectory\" | \"noVolumeConfigured\" | \"retry\"\n >;\n}\n\n/** Card-wrapped directory listing with loading, error, and empty states */\nexport function DirectoryList({\n entries,\n loading,\n error,\n onEntryClick,\n onNavigateToParent,\n onRetry,\n isAtRoot,\n selectedPath,\n resolveEntryPath,\n headerContent,\n hasCurrentPath,\n formatSize,\n labels,\n className,\n ...props\n}: DirectoryListProps) {\n return (\n <div data-slot=\"directory-list\" className={className} {...props}>\n <Card className=\"p-0 overflow-hidden\">\n {!isAtRoot && onNavigateToParent && (\n <button\n type=\"button\"\n onClick={onNavigateToParent}\n className=\"flex items-center gap-2 px-4 py-3 w-full text-left hover:bg-muted/50 border-b text-sm text-muted-foreground transition-colors\"\n >\n <ArrowLeft className=\"h-4 w-4\" />\n {labels?.backToParent ?? \"Back to parent\"}\n </button>\n )}\n\n {headerContent}\n\n {loading && (\n <div className=\"p-4 space-y-3\">\n <Skeleton className=\"h-10 w-full\" />\n <Skeleton className=\"h-10 w-full\" />\n <Skeleton className=\"h-10 w-full\" />\n </div>\n )}\n\n {error && (\n <div className=\"p-6 text-center\">\n <AlertCircle className=\"h-8 w-8 text-destructive mx-auto mb-2\" />\n <p className=\"text-sm text-destructive\">{error}</p>\n {onRetry && (\n <Button\n variant=\"outline\"\n size=\"sm\"\n className=\"mt-3\"\n onClick={onRetry}\n >\n {labels?.retry ?? \"Retry\"}\n </Button>\n )}\n </div>\n )}\n\n {!loading && !error && entries.length === 0 && (\n <div className=\"p-6 text-center text-muted-foreground\">\n <FileIcon className=\"h-8 w-8 mx-auto mb-2 opacity-50\" />\n <p className=\"text-sm\">\n {hasCurrentPath\n ? (labels?.emptyDirectory ?? \"This directory is empty.\")\n : (labels?.noVolumeConfigured ??\n \"No volume configured. Configure volumes in the files plugin to get started.\")}\n </p>\n </div>\n )}\n\n {!loading &&\n !error &&\n entries.map((entry) => {\n const entryPath = resolveEntryPath(entry);\n return (\n <FileEntry\n key={entryPath}\n entry={entry}\n entryPath={entryPath}\n isSelected={selectedPath === entryPath}\n formatSize={formatSize}\n onClick={() => onEntryClick(entry)}\n />\n );\n })}\n </Card>\n </div>\n );\n}\n"],"mappings":";;;;;;;;;AA0CA,SAAgB,cAAc,EAC5B,SACA,SACA,OACA,cACA,oBACA,SACA,UACA,cACA,kBACA,eACA,gBACA,YACA,QACA,WACA,GAAG,SACkB;AACrB,QACE,oBAAC;EAAI,aAAU;EAA4B;EAAW,GAAI;YACxD,qBAAC;GAAK,WAAU;;IACb,CAAC,YAAY,sBACZ,qBAAC;KACC,MAAK;KACL,SAAS;KACT,WAAU;gBAEV,oBAAC,aAAU,WAAU,YAAY,EAChC,QAAQ,gBAAgB;MAClB;IAGV;IAEA,WACC,qBAAC;KAAI,WAAU;;MACb,oBAAC,YAAS,WAAU,gBAAgB;MACpC,oBAAC,YAAS,WAAU,gBAAgB;MACpC,oBAAC,YAAS,WAAU,gBAAgB;;MAChC;IAGP,SACC,qBAAC;KAAI,WAAU;;MACb,oBAAC,eAAY,WAAU,0CAA0C;MACjE,oBAAC;OAAE,WAAU;iBAA4B;QAAU;MAClD,WACC,oBAAC;OACC,SAAQ;OACR,MAAK;OACL,WAAU;OACV,SAAS;iBAER,QAAQ,SAAS;QACX;;MAEP;IAGP,CAAC,WAAW,CAAC,SAAS,QAAQ,WAAW,KACxC,qBAAC;KAAI,WAAU;gBACb,oBAAC,YAAS,WAAU,oCAAoC,EACxD,oBAAC;MAAE,WAAU;gBACV,iBACI,QAAQ,kBAAkB,6BAC1B,QAAQ,sBACT;OACF;MACA;IAGP,CAAC,WACA,CAAC,SACD,QAAQ,KAAK,UAAU;KACrB,MAAM,YAAY,iBAAiB,MAAM;AACzC,YACE,oBAAC;MAEQ;MACI;MACX,YAAY,iBAAiB;MACjB;MACZ,eAAe,aAAa,MAAM;QAL7B,UAML;MAEJ;;IACC;GACH"}
@@ -0,0 +1,25 @@
1
+ import * as react_jsx_runtime0 from "react/jsx-runtime";
2
+
3
+ //#region src/react/file-browser/file-breadcrumb.d.ts
4
+ /** Props for the FileBreadcrumb component */
5
+ interface FileBreadcrumbProps extends Omit<React.ComponentProps<"nav">, "children"> {
6
+ /** Label for the root breadcrumb item */
7
+ rootLabel: string;
8
+ /** Path segments after the root */
9
+ segments: string[];
10
+ /** Called when the root breadcrumb is clicked */
11
+ onNavigateToRoot: () => void;
12
+ /** Called when a segment breadcrumb is clicked (receives segment index) */
13
+ onNavigateToSegment: (index: number) => void;
14
+ }
15
+ /** Path-aware breadcrumb navigation built on top of Breadcrumb primitives */
16
+ declare function FileBreadcrumb({
17
+ rootLabel,
18
+ segments,
19
+ onNavigateToRoot,
20
+ onNavigateToSegment,
21
+ ...props
22
+ }: FileBreadcrumbProps): react_jsx_runtime0.JSX.Element;
23
+ //#endregion
24
+ export { FileBreadcrumb, FileBreadcrumbProps };
25
+ //# sourceMappingURL=file-breadcrumb.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file-breadcrumb.d.ts","names":[],"sources":["../../../src/react/file-browser/file-breadcrumb.tsx"],"mappings":";;;;UAUiB,mBAAA,SACP,IAAA,CAAK,KAAA,CAAM,cAAA;;EAEnB,SAAA;EAFA;EAIA,QAAA;EAJY;EAMZ,gBAAA;EANa;EAQb,mBAAA,GAAsB,KAAA;AAAA;;iBAIR,cAAA,CAAA;EACd,SAAA;EACA,QAAA;EACA,gBAAA;EACA,mBAAA;EAAA,GACG;AAAA,GACF,mBAAA,GAAmB,kBAAA,CAAA,GAAA,CAAA,OAAA"}
@@ -0,0 +1,27 @@
1
+ import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator } from "../ui/breadcrumb.js";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+
4
+ //#region src/react/file-browser/file-breadcrumb.tsx
5
+ /** Path-aware breadcrumb navigation built on top of Breadcrumb primitives */
6
+ function FileBreadcrumb({ rootLabel, segments, onNavigateToRoot, onNavigateToSegment, ...props }) {
7
+ return /* @__PURE__ */ jsx(Breadcrumb, {
8
+ "data-slot": "file-breadcrumb",
9
+ ...props,
10
+ children: /* @__PURE__ */ jsxs(BreadcrumbList, { children: [/* @__PURE__ */ jsx(BreadcrumbItem, { children: segments.length > 0 ? /* @__PURE__ */ jsx(BreadcrumbLink, {
11
+ className: "cursor-pointer",
12
+ onClick: onNavigateToRoot,
13
+ children: rootLabel
14
+ }) : /* @__PURE__ */ jsx(BreadcrumbPage, { children: rootLabel }) }), segments.map((segment, index) => /* @__PURE__ */ jsxs("span", {
15
+ className: "contents",
16
+ children: [/* @__PURE__ */ jsx(BreadcrumbSeparator, {}), /* @__PURE__ */ jsx(BreadcrumbItem, { children: index === segments.length - 1 ? /* @__PURE__ */ jsx(BreadcrumbPage, { children: segment }) : /* @__PURE__ */ jsx(BreadcrumbLink, {
17
+ className: "cursor-pointer",
18
+ onClick: () => onNavigateToSegment(index),
19
+ children: segment
20
+ }) })]
21
+ }, segment))] })
22
+ });
23
+ }
24
+
25
+ //#endregion
26
+ export { FileBreadcrumb };
27
+ //# sourceMappingURL=file-breadcrumb.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file-breadcrumb.js","names":[],"sources":["../../../src/react/file-browser/file-breadcrumb.tsx"],"sourcesContent":["import {\n Breadcrumb,\n BreadcrumbItem,\n BreadcrumbLink,\n BreadcrumbList,\n BreadcrumbPage,\n BreadcrumbSeparator,\n} from \"../ui/breadcrumb\";\n\n/** Props for the FileBreadcrumb component */\nexport interface FileBreadcrumbProps\n extends Omit<React.ComponentProps<\"nav\">, \"children\"> {\n /** Label for the root breadcrumb item */\n rootLabel: string;\n /** Path segments after the root */\n segments: string[];\n /** Called when the root breadcrumb is clicked */\n onNavigateToRoot: () => void;\n /** Called when a segment breadcrumb is clicked (receives segment index) */\n onNavigateToSegment: (index: number) => void;\n}\n\n/** Path-aware breadcrumb navigation built on top of Breadcrumb primitives */\nexport function FileBreadcrumb({\n rootLabel,\n segments,\n onNavigateToRoot,\n onNavigateToSegment,\n ...props\n}: FileBreadcrumbProps) {\n return (\n <Breadcrumb data-slot=\"file-breadcrumb\" {...props}>\n <BreadcrumbList>\n <BreadcrumbItem>\n {segments.length > 0 ? (\n <BreadcrumbLink\n className=\"cursor-pointer\"\n onClick={onNavigateToRoot}\n >\n {rootLabel}\n </BreadcrumbLink>\n ) : (\n <BreadcrumbPage>{rootLabel}</BreadcrumbPage>\n )}\n </BreadcrumbItem>\n {segments.map((segment, index) => (\n <span key={segment} className=\"contents\">\n <BreadcrumbSeparator />\n <BreadcrumbItem>\n {index === segments.length - 1 ? (\n <BreadcrumbPage>{segment}</BreadcrumbPage>\n ) : (\n <BreadcrumbLink\n className=\"cursor-pointer\"\n onClick={() => onNavigateToSegment(index)}\n >\n {segment}\n </BreadcrumbLink>\n )}\n </BreadcrumbItem>\n </span>\n ))}\n </BreadcrumbList>\n </Breadcrumb>\n );\n}\n"],"mappings":";;;;;AAuBA,SAAgB,eAAe,EAC7B,WACA,UACA,kBACA,qBACA,GAAG,SACmB;AACtB,QACE,oBAAC;EAAW,aAAU;EAAkB,GAAI;YAC1C,qBAAC,6BACC,oBAAC,4BACE,SAAS,SAAS,IACjB,oBAAC;GACC,WAAU;GACV,SAAS;aAER;IACc,GAEjB,oBAAC,4BAAgB,YAA2B,GAE/B,EAChB,SAAS,KAAK,SAAS,UACtB,qBAAC;GAAmB,WAAU;cAC5B,oBAAC,wBAAsB,EACvB,oBAAC,4BACE,UAAU,SAAS,SAAS,IAC3B,oBAAC,4BAAgB,UAAyB,GAE1C,oBAAC;IACC,WAAU;IACV,eAAe,oBAAoB,MAAM;cAExC;KACc,GAEJ;KAbR,QAcJ,CACP,IACa;GACN"}
@@ -0,0 +1,27 @@
1
+ import { DirectoryEntry } from "./types.js";
2
+ import * as react_jsx_runtime0 from "react/jsx-runtime";
3
+
4
+ //#region src/react/file-browser/file-entry.d.ts
5
+ /** Props for the FileEntry component */
6
+ interface FileEntryProps extends Omit<React.ComponentProps<"button">, "children"> {
7
+ /** The directory entry to render */
8
+ entry: DirectoryEntry;
9
+ /** Resolved full path for this entry */
10
+ entryPath: string;
11
+ /** Whether this entry is currently selected */
12
+ isSelected?: boolean;
13
+ /** Custom file size formatter (defaults to formatFileSize) */
14
+ formatSize?: (bytes: number | undefined) => string;
15
+ }
16
+ /** Single file or directory row with icon, name, size, and selection state */
17
+ declare function FileEntry({
18
+ entry,
19
+ entryPath,
20
+ isSelected,
21
+ formatSize,
22
+ className,
23
+ ...props
24
+ }: FileEntryProps): react_jsx_runtime0.JSX.Element;
25
+ //#endregion
26
+ export { FileEntry, FileEntryProps };
27
+ //# sourceMappingURL=file-entry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file-entry.d.ts","names":[],"sources":["../../../src/react/file-browser/file-entry.tsx"],"mappings":";;;;;UAMiB,cAAA,SACP,IAAA,CAAK,KAAA,CAAM,cAAA;;EAEnB,KAAA,EAAO,cAAA;EAFP;EAIA,SAAA;EAJa;EAMb,UAAA;EANQ;EAQR,UAAA,IAAc,KAAA;AAAA;;iBAIA,SAAA,CAAA;EACd,KAAA;EACA,SAAA;EACA,UAAA;EACA,UAAA;EACA,SAAA;EAAA,GACG;AAAA,GACF,cAAA,GAAc,kBAAA,CAAA,GAAA,CAAA,OAAA"}
@@ -0,0 +1,31 @@
1
+ import { formatFileSize } from "../lib/format.js";
2
+ import { cn } from "../lib/utils.js";
3
+ import { jsx, jsxs } from "react/jsx-runtime";
4
+ import { ChevronRight, FileIcon, FolderIcon } from "lucide-react";
5
+
6
+ //#region src/react/file-browser/file-entry.tsx
7
+ /** Single file or directory row with icon, name, size, and selection state */
8
+ function FileEntry({ entry, entryPath, isSelected, formatSize = formatFileSize, className, ...props }) {
9
+ return /* @__PURE__ */ jsxs("button", {
10
+ type: "button",
11
+ "data-slot": "file-entry",
12
+ className: cn("flex items-center gap-3 px-4 py-3 w-full text-left hover:bg-muted/50 border-b last:border-b-0 transition-colors", isSelected && "bg-muted", className),
13
+ ...props,
14
+ children: [
15
+ entry.is_directory ? /* @__PURE__ */ jsx(FolderIcon, { className: "h-5 w-5 text-blue-500 shrink-0" }) : /* @__PURE__ */ jsx(FileIcon, { className: "h-5 w-5 text-muted-foreground shrink-0" }),
16
+ /* @__PURE__ */ jsx("span", {
17
+ className: "flex-1 truncate text-sm text-foreground",
18
+ children: entry.name ?? entryPath.split("/").pop()
19
+ }),
20
+ entry.is_directory && /* @__PURE__ */ jsx(ChevronRight, { className: "h-4 w-4 text-muted-foreground shrink-0" }),
21
+ !entry.is_directory && entry.file_size !== void 0 && /* @__PURE__ */ jsx("span", {
22
+ className: "text-xs text-muted-foreground shrink-0",
23
+ children: formatSize(entry.file_size)
24
+ })
25
+ ]
26
+ });
27
+ }
28
+
29
+ //#endregion
30
+ export { FileEntry };
31
+ //# sourceMappingURL=file-entry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file-entry.js","names":[],"sources":["../../../src/react/file-browser/file-entry.tsx"],"sourcesContent":["import { ChevronRight, FileIcon, FolderIcon } from \"lucide-react\";\nimport { formatFileSize } from \"../lib/format\";\nimport { cn } from \"../lib/utils\";\nimport type { DirectoryEntry } from \"./types\";\n\n/** Props for the FileEntry component */\nexport interface FileEntryProps\n extends Omit<React.ComponentProps<\"button\">, \"children\"> {\n /** The directory entry to render */\n entry: DirectoryEntry;\n /** Resolved full path for this entry */\n entryPath: string;\n /** Whether this entry is currently selected */\n isSelected?: boolean;\n /** Custom file size formatter (defaults to formatFileSize) */\n formatSize?: (bytes: number | undefined) => string;\n}\n\n/** Single file or directory row with icon, name, size, and selection state */\nexport function FileEntry({\n entry,\n entryPath,\n isSelected,\n formatSize = formatFileSize,\n className,\n ...props\n}: FileEntryProps) {\n return (\n <button\n type=\"button\"\n data-slot=\"file-entry\"\n className={cn(\n \"flex items-center gap-3 px-4 py-3 w-full text-left hover:bg-muted/50 border-b last:border-b-0 transition-colors\",\n isSelected && \"bg-muted\",\n className,\n )}\n {...props}\n >\n {entry.is_directory ? (\n <FolderIcon className=\"h-5 w-5 text-blue-500 shrink-0\" />\n ) : (\n <FileIcon className=\"h-5 w-5 text-muted-foreground shrink-0\" />\n )}\n <span className=\"flex-1 truncate text-sm text-foreground\">\n {entry.name ?? entryPath.split(\"/\").pop()}\n </span>\n {entry.is_directory && (\n <ChevronRight className=\"h-4 w-4 text-muted-foreground shrink-0\" />\n )}\n {!entry.is_directory && entry.file_size !== undefined && (\n <span className=\"text-xs text-muted-foreground shrink-0\">\n {formatSize(entry.file_size)}\n </span>\n )}\n </button>\n );\n}\n"],"mappings":";;;;;;;AAmBA,SAAgB,UAAU,EACxB,OACA,WACA,YACA,aAAa,gBACb,WACA,GAAG,SACc;AACjB,QACE,qBAAC;EACC,MAAK;EACL,aAAU;EACV,WAAW,GACT,mHACA,cAAc,YACd,UACD;EACD,GAAI;;GAEH,MAAM,eACL,oBAAC,cAAW,WAAU,mCAAmC,GAEzD,oBAAC,YAAS,WAAU,2CAA2C;GAEjE,oBAAC;IAAK,WAAU;cACb,MAAM,QAAQ,UAAU,MAAM,IAAI,CAAC,KAAK;KACpC;GACN,MAAM,gBACL,oBAAC,gBAAa,WAAU,2CAA2C;GAEpE,CAAC,MAAM,gBAAgB,MAAM,cAAc,UAC1C,oBAAC;IAAK,WAAU;cACb,WAAW,MAAM,UAAU;KACvB;;GAEF"}
@@ -0,0 +1,42 @@
1
+ import { FileBrowserLabels, FilePreview } from "./types.js";
2
+ import * as react_jsx_runtime0 from "react/jsx-runtime";
3
+
4
+ //#region src/react/file-browser/file-preview-panel.d.ts
5
+ /** Props for the FilePreviewPanel component */
6
+ interface FilePreviewPanelProps extends Omit<React.ComponentProps<"div">, "children"> {
7
+ /** Full path of the selected file (null when nothing is selected) */
8
+ selectedFile: string | null;
9
+ /** Preview data for the selected file */
10
+ preview: FilePreview | null;
11
+ /** Whether the preview is loading */
12
+ previewLoading?: boolean;
13
+ /** Called when the download button is clicked */
14
+ onDownload?: (filePath: string) => void;
15
+ /** Called when the delete button is clicked */
16
+ onDelete?: (filePath: string) => void;
17
+ /** Whether a delete operation is in progress */
18
+ deleting?: boolean;
19
+ /** Image preview source — string URL or function that receives the file path */
20
+ imagePreviewSrc?: string | ((filePath: string) => string);
21
+ /** Custom file size formatter (defaults to formatFileSize) */
22
+ formatSize?: (bytes: number | undefined) => string;
23
+ /** Customizable labels */
24
+ labels?: Pick<FileBrowserLabels, "selectFilePrompt" | "previewNotAvailable" | "previewFailed" | "download" | "size" | "type" | "modified" | "unknown">;
25
+ }
26
+ /** Preview panel displaying file metadata, image/text preview, and download/delete actions */
27
+ declare function FilePreviewPanel({
28
+ selectedFile,
29
+ preview,
30
+ previewLoading,
31
+ onDownload,
32
+ onDelete,
33
+ deleting,
34
+ imagePreviewSrc,
35
+ formatSize,
36
+ labels,
37
+ className,
38
+ ...props
39
+ }: FilePreviewPanelProps): react_jsx_runtime0.JSX.Element;
40
+ //#endregion
41
+ export { FilePreviewPanel, FilePreviewPanelProps };
42
+ //# sourceMappingURL=file-preview-panel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file-preview-panel.d.ts","names":[],"sources":["../../../src/react/file-browser/file-preview-panel.tsx"],"mappings":";;;;;UAQiB,qBAAA,SACP,IAAA,CAAK,KAAA,CAAM,cAAA;;EAEnB,YAAA;EAFA;EAIA,OAAA,EAAS,WAAA;EAJI;EAMb,cAAA;EAaE;EAXF,UAAA,IAAc,QAAA;EARN;EAUR,QAAA,IAAY,QAAA;EAVA;EAYZ,QAAA;EAZa;EAcb,eAAA,cAA6B,QAAA;EAZ7B;EAcA,UAAA,IAAc,KAAA;EAZL;EAcT,MAAA,GAAS,IAAA,CACP,iBAAA;AAAA;;iBAaY,gBAAA,CAAA;EACd,YAAA;EACA,OAAA;EACA,cAAA;EACA,UAAA;EACA,QAAA;EACA,QAAA;EACA,eAAA;EACA,UAAA;EACA,MAAA;EACA,SAAA;EAAA,GACG;AAAA,GACF,qBAAA,GAAqB,kBAAA,CAAA,GAAA,CAAA,OAAA"}
@@ -0,0 +1,135 @@
1
+ import { formatFileSize } from "../lib/format.js";
2
+ import { Button } from "../ui/button.js";
3
+ import { Card } from "../ui/card.js";
4
+ import { Skeleton } from "../ui/skeleton.js";
5
+ import { jsx, jsxs } from "react/jsx-runtime";
6
+ import { AlertCircle, Download, FileIcon, Loader2, Trash2 } from "lucide-react";
7
+
8
+ //#region src/react/file-browser/file-preview-panel.tsx
9
+ /** Preview panel displaying file metadata, image/text preview, and download/delete actions */
10
+ function FilePreviewPanel({ selectedFile, preview, previewLoading, onDownload, onDelete, deleting, imagePreviewSrc, formatSize = formatFileSize, labels, className, ...props }) {
11
+ const resolveImageSrc = (filePath) => {
12
+ if (!imagePreviewSrc) return "";
13
+ if (typeof imagePreviewSrc === "string") return imagePreviewSrc;
14
+ return imagePreviewSrc(filePath);
15
+ };
16
+ return /* @__PURE__ */ jsx("div", {
17
+ "data-slot": "file-preview-panel",
18
+ className,
19
+ ...props,
20
+ children: /* @__PURE__ */ jsxs(Card, {
21
+ className: "p-6",
22
+ children: [
23
+ !selectedFile && /* @__PURE__ */ jsxs("div", {
24
+ className: "text-center text-muted-foreground py-8",
25
+ children: [/* @__PURE__ */ jsx(FileIcon, { className: "h-10 w-10 mx-auto mb-3 opacity-40" }), /* @__PURE__ */ jsx("p", {
26
+ className: "text-sm",
27
+ children: labels?.selectFilePrompt ?? "Select a file to preview"
28
+ })]
29
+ }),
30
+ selectedFile && previewLoading && /* @__PURE__ */ jsxs("div", {
31
+ className: "space-y-3",
32
+ children: [
33
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-5 w-3/4" }),
34
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-1/2" }),
35
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-2/3" }),
36
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-32 w-full mt-4" })
37
+ ]
38
+ }),
39
+ selectedFile && !previewLoading && preview && /* @__PURE__ */ jsxs("div", {
40
+ className: "space-y-4",
41
+ children: [
42
+ /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("h3", {
43
+ className: "font-semibold text-foreground truncate mb-1",
44
+ children: selectedFile.split("/").pop()
45
+ }), /* @__PURE__ */ jsx("p", {
46
+ className: "text-xs text-muted-foreground truncate",
47
+ children: selectedFile
48
+ })] }),
49
+ /* @__PURE__ */ jsxs("div", {
50
+ className: "space-y-2 text-sm",
51
+ children: [
52
+ /* @__PURE__ */ jsxs("div", {
53
+ className: "flex justify-between",
54
+ children: [/* @__PURE__ */ jsx("span", {
55
+ className: "text-muted-foreground",
56
+ children: labels?.size ?? "Size"
57
+ }), /* @__PURE__ */ jsx("span", {
58
+ className: "text-foreground",
59
+ children: formatSize(preview.contentLength)
60
+ })]
61
+ }),
62
+ /* @__PURE__ */ jsxs("div", {
63
+ className: "flex justify-between",
64
+ children: [/* @__PURE__ */ jsx("span", {
65
+ className: "text-muted-foreground",
66
+ children: labels?.type ?? "Type"
67
+ }), /* @__PURE__ */ jsx("span", {
68
+ className: "text-foreground truncate ml-2",
69
+ children: preview.contentType ?? labels?.unknown ?? "Unknown"
70
+ })]
71
+ }),
72
+ preview.lastModified && /* @__PURE__ */ jsxs("div", {
73
+ className: "flex justify-between",
74
+ children: [/* @__PURE__ */ jsx("span", {
75
+ className: "text-muted-foreground",
76
+ children: labels?.modified ?? "Modified"
77
+ }), /* @__PURE__ */ jsx("span", {
78
+ className: "text-foreground",
79
+ children: preview.lastModified
80
+ })]
81
+ })
82
+ ]
83
+ }),
84
+ /* @__PURE__ */ jsxs("div", {
85
+ className: "flex gap-2",
86
+ children: [onDownload && /* @__PURE__ */ jsxs(Button, {
87
+ variant: "outline",
88
+ size: "sm",
89
+ className: "flex-1",
90
+ onClick: () => onDownload(selectedFile),
91
+ children: [/* @__PURE__ */ jsx(Download, { className: "h-4 w-4 mr-2" }), labels?.download ?? "Download"]
92
+ }), onDelete && /* @__PURE__ */ jsx(Button, {
93
+ variant: "destructive",
94
+ size: "sm",
95
+ disabled: deleting,
96
+ onClick: () => onDelete(selectedFile),
97
+ children: deleting ? /* @__PURE__ */ jsx(Loader2, { className: "h-4 w-4 animate-spin" }) : /* @__PURE__ */ jsx(Trash2, { className: "h-4 w-4" })
98
+ })]
99
+ }),
100
+ preview.isImage && imagePreviewSrc && /* @__PURE__ */ jsx("div", {
101
+ className: "border rounded overflow-hidden",
102
+ children: /* @__PURE__ */ jsx("img", {
103
+ src: resolveImageSrc(selectedFile),
104
+ alt: selectedFile.split("/").pop() ?? "Preview",
105
+ className: "w-full h-auto"
106
+ })
107
+ }),
108
+ preview.isText && preview.textPreview !== null && /* @__PURE__ */ jsx("div", {
109
+ className: "border rounded",
110
+ children: /* @__PURE__ */ jsx("pre", {
111
+ className: "p-3 text-xs font-mono overflow-auto max-h-80 whitespace-pre-wrap text-foreground bg-muted/30",
112
+ children: preview.textPreview
113
+ })
114
+ }),
115
+ !preview.isText && !preview.isImage && /* @__PURE__ */ jsx("div", {
116
+ className: "text-center py-4 text-sm text-muted-foreground",
117
+ children: labels?.previewNotAvailable ?? "Preview not available for this file type."
118
+ })
119
+ ]
120
+ }),
121
+ selectedFile && !previewLoading && !preview && /* @__PURE__ */ jsxs("div", {
122
+ className: "text-center py-8",
123
+ children: [/* @__PURE__ */ jsx(AlertCircle, { className: "h-8 w-8 text-destructive mx-auto mb-2" }), /* @__PURE__ */ jsx("p", {
124
+ className: "text-sm text-destructive",
125
+ children: labels?.previewFailed ?? "Failed to load preview"
126
+ })]
127
+ })
128
+ ]
129
+ })
130
+ });
131
+ }
132
+
133
+ //#endregion
134
+ export { FilePreviewPanel };
135
+ //# sourceMappingURL=file-preview-panel.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file-preview-panel.js","names":[],"sources":["../../../src/react/file-browser/file-preview-panel.tsx"],"sourcesContent":["import { AlertCircle, Download, FileIcon, Loader2, Trash2 } from \"lucide-react\";\nimport { formatFileSize } from \"../lib/format\";\nimport { Button } from \"../ui/button\";\nimport { Card } from \"../ui/card\";\nimport { Skeleton } from \"../ui/skeleton\";\nimport type { FileBrowserLabels, FilePreview } from \"./types\";\n\n/** Props for the FilePreviewPanel component */\nexport interface FilePreviewPanelProps\n extends Omit<React.ComponentProps<\"div\">, \"children\"> {\n /** Full path of the selected file (null when nothing is selected) */\n selectedFile: string | null;\n /** Preview data for the selected file */\n preview: FilePreview | null;\n /** Whether the preview is loading */\n previewLoading?: boolean;\n /** Called when the download button is clicked */\n onDownload?: (filePath: string) => void;\n /** Called when the delete button is clicked */\n onDelete?: (filePath: string) => void;\n /** Whether a delete operation is in progress */\n deleting?: boolean;\n /** Image preview source — string URL or function that receives the file path */\n imagePreviewSrc?: string | ((filePath: string) => string);\n /** Custom file size formatter (defaults to formatFileSize) */\n formatSize?: (bytes: number | undefined) => string;\n /** Customizable labels */\n labels?: Pick<\n FileBrowserLabels,\n | \"selectFilePrompt\"\n | \"previewNotAvailable\"\n | \"previewFailed\"\n | \"download\"\n | \"size\"\n | \"type\"\n | \"modified\"\n | \"unknown\"\n >;\n}\n\n/** Preview panel displaying file metadata, image/text preview, and download/delete actions */\nexport function FilePreviewPanel({\n selectedFile,\n preview,\n previewLoading,\n onDownload,\n onDelete,\n deleting,\n imagePreviewSrc,\n formatSize = formatFileSize,\n labels,\n className,\n ...props\n}: FilePreviewPanelProps) {\n const resolveImageSrc = (filePath: string) => {\n if (!imagePreviewSrc) return \"\";\n if (typeof imagePreviewSrc === \"string\") return imagePreviewSrc;\n return imagePreviewSrc(filePath);\n };\n\n return (\n <div data-slot=\"file-preview-panel\" className={className} {...props}>\n <Card className=\"p-6\">\n {/* No file selected */}\n {!selectedFile && (\n <div className=\"text-center text-muted-foreground py-8\">\n <FileIcon className=\"h-10 w-10 mx-auto mb-3 opacity-40\" />\n <p className=\"text-sm\">\n {labels?.selectFilePrompt ?? \"Select a file to preview\"}\n </p>\n </div>\n )}\n\n {/* Loading state */}\n {selectedFile && previewLoading && (\n <div className=\"space-y-3\">\n <Skeleton className=\"h-5 w-3/4\" />\n <Skeleton className=\"h-4 w-1/2\" />\n <Skeleton className=\"h-4 w-2/3\" />\n <Skeleton className=\"h-32 w-full mt-4\" />\n </div>\n )}\n\n {/* Preview content */}\n {selectedFile && !previewLoading && preview && (\n <div className=\"space-y-4\">\n <div>\n <h3 className=\"font-semibold text-foreground truncate mb-1\">\n {selectedFile.split(\"/\").pop()}\n </h3>\n <p className=\"text-xs text-muted-foreground truncate\">\n {selectedFile}\n </p>\n </div>\n\n <div className=\"space-y-2 text-sm\">\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">\n {labels?.size ?? \"Size\"}\n </span>\n <span className=\"text-foreground\">\n {formatSize(preview.contentLength)}\n </span>\n </div>\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">\n {labels?.type ?? \"Type\"}\n </span>\n <span className=\"text-foreground truncate ml-2\">\n {preview.contentType ?? labels?.unknown ?? \"Unknown\"}\n </span>\n </div>\n {preview.lastModified && (\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">\n {labels?.modified ?? \"Modified\"}\n </span>\n <span className=\"text-foreground\">\n {preview.lastModified}\n </span>\n </div>\n )}\n </div>\n\n <div className=\"flex gap-2\">\n {onDownload && (\n <Button\n variant=\"outline\"\n size=\"sm\"\n className=\"flex-1\"\n onClick={() => onDownload(selectedFile)}\n >\n <Download className=\"h-4 w-4 mr-2\" />\n {labels?.download ?? \"Download\"}\n </Button>\n )}\n {onDelete && (\n <Button\n variant=\"destructive\"\n size=\"sm\"\n disabled={deleting}\n onClick={() => onDelete(selectedFile)}\n >\n {deleting ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <Trash2 className=\"h-4 w-4\" />\n )}\n </Button>\n )}\n </div>\n\n {preview.isImage && imagePreviewSrc && (\n <div className=\"border rounded overflow-hidden\">\n <img\n src={resolveImageSrc(selectedFile)}\n alt={selectedFile.split(\"/\").pop() ?? \"Preview\"}\n className=\"w-full h-auto\"\n />\n </div>\n )}\n\n {preview.isText && preview.textPreview !== null && (\n <div className=\"border rounded\">\n <pre className=\"p-3 text-xs font-mono overflow-auto max-h-80 whitespace-pre-wrap text-foreground bg-muted/30\">\n {preview.textPreview}\n </pre>\n </div>\n )}\n\n {!preview.isText && !preview.isImage && (\n <div className=\"text-center py-4 text-sm text-muted-foreground\">\n {labels?.previewNotAvailable ??\n \"Preview not available for this file type.\"}\n </div>\n )}\n </div>\n )}\n\n {/* Preview load failed */}\n {selectedFile && !previewLoading && !preview && (\n <div className=\"text-center py-8\">\n <AlertCircle className=\"h-8 w-8 text-destructive mx-auto mb-2\" />\n <p className=\"text-sm text-destructive\">\n {labels?.previewFailed ?? \"Failed to load preview\"}\n </p>\n </div>\n )}\n </Card>\n </div>\n );\n}\n"],"mappings":";;;;;;;;;AAyCA,SAAgB,iBAAiB,EAC/B,cACA,SACA,gBACA,YACA,UACA,UACA,iBACA,aAAa,gBACb,QACA,WACA,GAAG,SACqB;CACxB,MAAM,mBAAmB,aAAqB;AAC5C,MAAI,CAAC,gBAAiB,QAAO;AAC7B,MAAI,OAAO,oBAAoB,SAAU,QAAO;AAChD,SAAO,gBAAgB,SAAS;;AAGlC,QACE,oBAAC;EAAI,aAAU;EAAgC;EAAW,GAAI;YAC5D,qBAAC;GAAK,WAAU;;IAEb,CAAC,gBACA,qBAAC;KAAI,WAAU;gBACb,oBAAC,YAAS,WAAU,sCAAsC,EAC1D,oBAAC;MAAE,WAAU;gBACV,QAAQ,oBAAoB;OAC3B;MACA;IAIP,gBAAgB,kBACf,qBAAC;KAAI,WAAU;;MACb,oBAAC,YAAS,WAAU,cAAc;MAClC,oBAAC,YAAS,WAAU,cAAc;MAClC,oBAAC,YAAS,WAAU,cAAc;MAClC,oBAAC,YAAS,WAAU,qBAAqB;;MACrC;IAIP,gBAAgB,CAAC,kBAAkB,WAClC,qBAAC;KAAI,WAAU;;MACb,qBAAC,oBACC,oBAAC;OAAG,WAAU;iBACX,aAAa,MAAM,IAAI,CAAC,KAAK;QAC3B,EACL,oBAAC;OAAE,WAAU;iBACV;QACC,IACA;MAEN,qBAAC;OAAI,WAAU;;QACb,qBAAC;SAAI,WAAU;oBACb,oBAAC;UAAK,WAAU;oBACb,QAAQ,QAAQ;WACZ,EACP,oBAAC;UAAK,WAAU;oBACb,WAAW,QAAQ,cAAc;WAC7B;UACH;QACN,qBAAC;SAAI,WAAU;oBACb,oBAAC;UAAK,WAAU;oBACb,QAAQ,QAAQ;WACZ,EACP,oBAAC;UAAK,WAAU;oBACb,QAAQ,eAAe,QAAQ,WAAW;WACtC;UACH;QACL,QAAQ,gBACP,qBAAC;SAAI,WAAU;oBACb,oBAAC;UAAK,WAAU;oBACb,QAAQ,YAAY;WAChB,EACP,oBAAC;UAAK,WAAU;oBACb,QAAQ;WACJ;UACH;;QAEJ;MAEN,qBAAC;OAAI,WAAU;kBACZ,cACC,qBAAC;QACC,SAAQ;QACR,MAAK;QACL,WAAU;QACV,eAAe,WAAW,aAAa;mBAEvC,oBAAC,YAAS,WAAU,iBAAiB,EACpC,QAAQ,YAAY;SACd,EAEV,YACC,oBAAC;QACC,SAAQ;QACR,MAAK;QACL,UAAU;QACV,eAAe,SAAS,aAAa;kBAEpC,WACC,oBAAC,WAAQ,WAAU,yBAAyB,GAE5C,oBAAC,UAAO,WAAU,YAAY;SAEzB;QAEP;MAEL,QAAQ,WAAW,mBAClB,oBAAC;OAAI,WAAU;iBACb,oBAAC;QACC,KAAK,gBAAgB,aAAa;QAClC,KAAK,aAAa,MAAM,IAAI,CAAC,KAAK,IAAI;QACtC,WAAU;SACV;QACE;MAGP,QAAQ,UAAU,QAAQ,gBAAgB,QACzC,oBAAC;OAAI,WAAU;iBACb,oBAAC;QAAI,WAAU;kBACZ,QAAQ;SACL;QACF;MAGP,CAAC,QAAQ,UAAU,CAAC,QAAQ,WAC3B,oBAAC;OAAI,WAAU;iBACZ,QAAQ,uBACP;QACE;;MAEJ;IAIP,gBAAgB,CAAC,kBAAkB,CAAC,WACnC,qBAAC;KAAI,WAAU;gBACb,oBAAC,eAAY,WAAU,0CAA0C,EACjE,oBAAC;MAAE,WAAU;gBACV,QAAQ,iBAAiB;OACxB;MACA;;IAEH;GACH"}
@@ -0,0 +1,7 @@
1
+ import { formatFileSize } from "../lib/format.js";
2
+ import { DirectoryEntry, FileBrowserLabels, FilePreview } from "./types.js";
3
+ import { DirectoryList, DirectoryListProps } from "./directory-list.js";
4
+ import { FileBreadcrumb, FileBreadcrumbProps } from "./file-breadcrumb.js";
5
+ import { FileEntry, FileEntryProps } from "./file-entry.js";
6
+ import { FilePreviewPanel, FilePreviewPanelProps } from "./file-preview-panel.js";
7
+ import { NewFolderInput, NewFolderInputProps } from "./new-folder-input.js";
@@ -0,0 +1,6 @@
1
+ import { formatFileSize } from "../lib/format.js";
2
+ import { FileEntry } from "./file-entry.js";
3
+ import { DirectoryList } from "./directory-list.js";
4
+ import { FileBreadcrumb } from "./file-breadcrumb.js";
5
+ import { FilePreviewPanel } from "./file-preview-panel.js";
6
+ import { NewFolderInput } from "./new-folder-input.js";