@cocoar/vue-ui 3.1.0 → 3.2.0-beta.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.mjs +53 -0
- package/dist/components/checkbox/CoarCheckbox.vue.d.ts +1 -1
- package/dist/components/checkbox/CoarCheckbox.vue.d.ts.map +1 -1
- package/dist/components/code-block/CoarCodeBlock.vue.d.ts +1 -1
- package/dist/components/code-block/CoarCodeBlock.vue.d.ts.map +1 -1
- package/dist/components/data-list/CoarDataList.vue.d.ts +158 -0
- package/dist/components/data-list/CoarDataList.vue.d.ts.map +1 -0
- package/dist/components/data-list/CoarDataListToolbar.vue.d.ts +30 -0
- package/dist/components/data-list/CoarDataListToolbar.vue.d.ts.map +1 -0
- package/dist/components/data-list/data-list-builder.d.ts +288 -0
- package/dist/components/data-list/data-list-builder.d.ts.map +1 -0
- package/dist/components/data-list/index.d.ts +12 -0
- package/dist/components/data-list/index.d.ts.map +1 -0
- package/dist/components/data-list/internal/compare.d.ts +14 -0
- package/dist/components/data-list/internal/compare.d.ts.map +1 -0
- package/dist/components/data-list/internal/reorder-core.d.ts +46 -0
- package/dist/components/data-list/internal/reorder-core.d.ts.map +1 -0
- package/dist/components/data-list/internal/search.d.ts +16 -0
- package/dist/components/data-list/internal/search.d.ts.map +1 -0
- package/dist/components/data-list/internal/useDataListLines.d.ts +92 -0
- package/dist/components/data-list/internal/useDataListLines.d.ts.map +1 -0
- package/dist/components/data-list/internal/useDataListReorder.d.ts +68 -0
- package/dist/components/data-list/internal/useDataListReorder.d.ts.map +1 -0
- package/dist/components/data-list/internal/useSearchHighlight.d.ts +19 -0
- package/dist/components/data-list/internal/useSearchHighlight.d.ts.map +1 -0
- package/dist/components/data-list/types.d.ts +185 -0
- package/dist/components/data-list/types.d.ts.map +1 -0
- package/dist/components/data-list/useDataListModel.d.ts +79 -0
- package/dist/components/data-list/useDataListModel.d.ts.map +1 -0
- package/dist/components/date-time/_shared/maskito-config.d.ts +7 -2
- package/dist/components/date-time/_shared/maskito-config.d.ts.map +1 -1
- package/dist/components/date-time/_shared/time-helpers.d.ts +11 -4
- package/dist/components/date-time/_shared/time-helpers.d.ts.map +1 -1
- package/dist/components/date-time/_shared/use-date-picker-base.d.ts +1 -1
- package/dist/components/date-time/plain-date-picker/CoarPlainDatePicker.vue.d.ts +1 -1
- package/dist/components/date-time/plain-date-picker/CoarPlainDatePicker.vue.d.ts.map +1 -1
- package/dist/components/date-time/plain-date-time-picker/CoarPlainDateTimePicker.vue.d.ts +1 -1
- package/dist/components/date-time/plain-date-time-picker/CoarPlainDateTimePicker.vue.d.ts.map +1 -1
- package/dist/components/date-time/zoned-date-time-picker/CoarZonedDateTimePicker.vue.d.ts +1 -1
- package/dist/components/date-time/zoned-date-time-picker/CoarZonedDateTimePicker.vue.d.ts.map +1 -1
- package/dist/components/dual-listbox/CoarDualListbox.vue.d.ts +3 -0
- package/dist/components/dual-listbox/CoarDualListbox.vue.d.ts.map +1 -1
- package/dist/components/form-field/CoarFormField.vue.d.ts +1 -1
- package/dist/components/form-field/CoarFormField.vue.d.ts.map +1 -1
- package/dist/components/icon/CoarIcon.vue.d.ts +2 -2
- package/dist/components/icon/CoarIcon.vue.d.ts.map +1 -1
- package/dist/components/listbox/CoarListbox.vue.d.ts +7 -0
- package/dist/components/listbox/CoarListbox.vue.d.ts.map +1 -1
- package/dist/components/number-input/CoarNumberInput.vue.d.ts +1 -1
- package/dist/components/number-input/CoarNumberInput.vue.d.ts.map +1 -1
- package/dist/components/otp-input/CoarOtpInput.vue.d.ts +1 -1
- package/dist/components/otp-input/CoarOtpInput.vue.d.ts.map +1 -1
- package/dist/components/password-input/CoarPasswordInput.vue.d.ts +1 -1
- package/dist/components/password-input/CoarPasswordInput.vue.d.ts.map +1 -1
- package/dist/components/switch/CoarSwitch.vue.d.ts +1 -1
- package/dist/components/switch/CoarSwitch.vue.d.ts.map +1 -1
- package/dist/components/text-input/CoarTextInput.vue.d.ts +1 -1
- package/dist/components/text-input/CoarTextInput.vue.d.ts.map +1 -1
- package/dist/composables/dragRegistry.d.ts +12 -0
- package/dist/composables/dragRegistry.d.ts.map +1 -1
- package/dist/composables/index.d.ts +2 -2
- package/dist/composables/index.d.ts.map +1 -1
- package/dist/composables/useDragDrop.d.ts +62 -6
- package/dist/composables/useDragDrop.d.ts.map +1 -1
- package/dist/composables/useVirtualList.d.ts +21 -1
- package/dist/composables/useVirtualList.d.ts.map +1 -1
- package/dist/index.css +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4508 -2039
- package/package.json +9 -3
- package/skills/cocoar-vue-ui/SKILL.md +244 -0
- package/skills/cocoar-vue-ui/references/components/avatar.md +178 -0
- package/skills/cocoar-vue-ui/references/components/badge.md +290 -0
- package/skills/cocoar-vue-ui/references/components/breadcrumb.md +238 -0
- package/skills/cocoar-vue-ui/references/components/button.md +252 -0
- package/skills/cocoar-vue-ui/references/components/calendar/agenda-view.md +372 -0
- package/skills/cocoar-vue-ui/references/components/calendar/coar-calendar.md +1647 -0
- package/skills/cocoar-vue-ui/references/components/calendar/day-view.md +361 -0
- package/skills/cocoar-vue-ui/references/components/calendar/index.md +312 -0
- package/skills/cocoar-vue-ui/references/components/calendar/month-view.md +486 -0
- package/skills/cocoar-vue-ui/references/components/calendar/performance.md +186 -0
- package/skills/cocoar-vue-ui/references/components/calendar/timeline-view.md +301 -0
- package/skills/cocoar-vue-ui/references/components/calendar/week-view.md +382 -0
- package/skills/cocoar-vue-ui/references/components/calendar/work-week-view.md +145 -0
- package/skills/cocoar-vue-ui/references/components/calendar/year-view.md +63 -0
- package/skills/cocoar-vue-ui/references/components/card.md +151 -0
- package/skills/cocoar-vue-ui/references/components/checkbox-group.md +103 -0
- package/skills/cocoar-vue-ui/references/components/checkbox.md +279 -0
- package/skills/cocoar-vue-ui/references/components/code-block.md +241 -0
- package/skills/cocoar-vue-ui/references/components/context-menu.md +355 -0
- package/skills/cocoar-vue-ui/references/components/data-grid/checkbox.md +213 -0
- package/skills/cocoar-vue-ui/references/components/data-grid/date-columns.md +272 -0
- package/skills/cocoar-vue-ui/references/components/data-grid/editing.md +236 -0
- package/skills/cocoar-vue-ui/references/components/data-grid/multi-select.md +193 -0
- package/skills/cocoar-vue-ui/references/components/data-grid/number.md +147 -0
- package/skills/cocoar-vue-ui/references/components/data-grid/select.md +202 -0
- package/skills/cocoar-vue-ui/references/components/data-grid/text.md +127 -0
- package/skills/cocoar-vue-ui/references/components/data-grid.md +1225 -0
- package/skills/cocoar-vue-ui/references/components/data-list.md +2121 -0
- package/skills/cocoar-vue-ui/references/components/date-or-time-picker.md +200 -0
- package/skills/cocoar-vue-ui/references/components/date-picker.md +203 -0
- package/skills/cocoar-vue-ui/references/components/date-time-picker.md +154 -0
- package/skills/cocoar-vue-ui/references/components/date-views.md +212 -0
- package/skills/cocoar-vue-ui/references/components/dialog.md +148 -0
- package/skills/cocoar-vue-ui/references/components/divider.md +157 -0
- package/skills/cocoar-vue-ui/references/components/document-viewer/annotations.md +400 -0
- package/skills/cocoar-vue-ui/references/components/document-viewer/coar-document-viewer.md +327 -0
- package/skills/cocoar-vue-ui/references/components/document-viewer/index.md +234 -0
- package/skills/cocoar-vue-ui/references/components/document-viewer/toolbar.md +221 -0
- package/skills/cocoar-vue-ui/references/components/drag-drop.md +267 -0
- package/skills/cocoar-vue-ui/references/components/dual-listbox.md +449 -0
- package/skills/cocoar-vue-ui/references/components/file-explorer/asset-store.md +327 -0
- package/skills/cocoar-vue-ui/references/components/file-explorer/in-memory-store.md +176 -0
- package/skills/cocoar-vue-ui/references/components/file-explorer/index.md +311 -0
- package/skills/cocoar-vue-ui/references/components/file-explorer/use-file-explorer.md +300 -0
- package/skills/cocoar-vue-ui/references/components/form-field.md +1025 -0
- package/skills/cocoar-vue-ui/references/components/fragment-parser.md +298 -0
- package/skills/cocoar-vue-ui/references/components/link.md +235 -0
- package/skills/cocoar-vue-ui/references/components/listbox.md +575 -0
- package/skills/cocoar-vue-ui/references/components/map/editor.md +448 -0
- package/skills/cocoar-vue-ui/references/components/map/index.md +351 -0
- package/skills/cocoar-vue-ui/references/components/markdown-diagrams.md +210 -0
- package/skills/cocoar-vue-ui/references/components/markdown-editor.md +1478 -0
- package/skills/cocoar-vue-ui/references/components/markdown-embeds.md +387 -0
- package/skills/cocoar-vue-ui/references/components/markdown-form.md +447 -0
- package/skills/cocoar-vue-ui/references/components/markdown.md +276 -0
- package/skills/cocoar-vue-ui/references/components/menu.md +380 -0
- package/skills/cocoar-vue-ui/references/components/mermaid.md +172 -0
- package/skills/cocoar-vue-ui/references/components/navbar.md +147 -0
- package/skills/cocoar-vue-ui/references/components/note.md +96 -0
- package/skills/cocoar-vue-ui/references/components/notice.md +131 -0
- package/skills/cocoar-vue-ui/references/components/number-input.md +215 -0
- package/skills/cocoar-vue-ui/references/components/otp-input.md +324 -0
- package/skills/cocoar-vue-ui/references/components/page-builder/authoring-contract.md +271 -0
- package/skills/cocoar-vue-ui/references/components/page-builder/coar-page-builder.md +476 -0
- package/skills/cocoar-vue-ui/references/components/page-builder/coar-page-renderer.md +677 -0
- package/skills/cocoar-vue-ui/references/components/page-builder/custom-elements.md +441 -0
- package/skills/cocoar-vue-ui/references/components/page-builder/idp-integration.md +122 -0
- package/skills/cocoar-vue-ui/references/components/page-builder/index.md +573 -0
- package/skills/cocoar-vue-ui/references/components/pagination.md +150 -0
- package/skills/cocoar-vue-ui/references/components/panel-layout.md +372 -0
- package/skills/cocoar-vue-ui/references/components/password-input.md +175 -0
- package/skills/cocoar-vue-ui/references/components/popconfirm.md +186 -0
- package/skills/cocoar-vue-ui/references/components/popover.md +173 -0
- package/skills/cocoar-vue-ui/references/components/progress-bar.md +178 -0
- package/skills/cocoar-vue-ui/references/components/radio-group.md +225 -0
- package/skills/cocoar-vue-ui/references/components/script-editor.md +1271 -0
- package/skills/cocoar-vue-ui/references/components/segmented-control.md +238 -0
- package/skills/cocoar-vue-ui/references/components/select.md +463 -0
- package/skills/cocoar-vue-ui/references/components/sidebar.md +421 -0
- package/skills/cocoar-vue-ui/references/components/spinner.md +132 -0
- package/skills/cocoar-vue-ui/references/components/switch.md +195 -0
- package/skills/cocoar-vue-ui/references/components/table.md +170 -0
- package/skills/cocoar-vue-ui/references/components/tabs.md +382 -0
- package/skills/cocoar-vue-ui/references/components/tag.md +178 -0
- package/skills/cocoar-vue-ui/references/components/text-input.md +256 -0
- package/skills/cocoar-vue-ui/references/components/toast.md +160 -0
- package/skills/cocoar-vue-ui/references/components/tooltip.md +121 -0
- package/skills/cocoar-vue-ui/references/components/transitions.md +193 -0
- package/skills/cocoar-vue-ui/references/components/tree.md +2388 -0
- package/skills/cocoar-vue-ui/references/components/virtual-list.md +212 -0
- package/skills/cocoar-vue-ui/references/components/wizard.md +251 -0
- package/skills/cocoar-vue-ui/references/components/zoned-date-time-picker.md +177 -0
- package/skills/cocoar-vue-ui/references/foundations/colors.md +708 -0
- package/skills/cocoar-vue-ui/references/foundations/design-principles.md +115 -0
- package/skills/cocoar-vue-ui/references/foundations/icons.md +381 -0
- package/skills/cocoar-vue-ui/references/foundations/localization/formatting.md +530 -0
- package/skills/cocoar-vue-ui/references/foundations/localization/setup.md +86 -0
- package/skills/cocoar-vue-ui/references/foundations/localization/timezones.md +182 -0
- package/skills/cocoar-vue-ui/references/foundations/localization/translations.md +305 -0
- package/skills/cocoar-vue-ui/references/foundations/motion.md +549 -0
- package/skills/cocoar-vue-ui/references/foundations/spacing.md +330 -0
- package/skills/cocoar-vue-ui/references/foundations/theming.md +140 -0
- package/skills/cocoar-vue-ui/references/foundations/typography.md +206 -0
- package/skills/cocoar-vue-ui/references/guide/error-handling.md +162 -0
- package/skills/cocoar-vue-ui/references/guide/getting-started.md +116 -0
- package/skills/cocoar-vue-ui/references/guide/migration-page-builder-3.md +207 -0
- package/skills/cocoar-vue-ui/references/guide/migration.md +140 -0
- package/skills/cocoar-vue-ui/references/guide/theming.md +98 -0
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
<!-- Generated from apps/docs/components/file-explorer/index.md by apps/docs/scripts/sync-skill.mjs. Do not edit; edit the docs page. -->
|
|
2
|
+
|
|
3
|
+
# File Explorer (Preview)
|
|
4
|
+
|
|
5
|
+
`@cocoar/vue-file-explorer-core` is the **headless engine** for a VSCode-style file/asset explorer in Vue 3 — the **data + coordination**, not a finished UI. A single composable, `useFileExplorer({store})`, drives a pluggable `AssetStore<T>` backend and returns every ref + op a file explorer needs (tree + tab state machine, selection, async loading, blob-URL leases, dirty tracking, conflict resolution). It ships **no layout** — you compose the chrome with [`@cocoar/vue-ui`](../panel-layout.md) (`CoarPanelLayout`, `CoarSplitPane`, `CoarTree`, …). A batteries-included, layouted `<CoarFileExplorer>` component — under the bare `@cocoar/vue-file-explorer` name — is planned on top.
|
|
6
|
+
|
|
7
|
+
> **Tip: Mental model — engine, not UI**
|
|
8
|
+
>
|
|
9
|
+
> `useFileExplorer` is **headless**: it renders no editors, tabs, breadcrumbs, or layout — it returns reactive state + ops, and your view binds to `fe.*`. The composable IS the bus that wires the panels together: select in the tree → a tab opens → the editor and details panels react, all through one shared `fe.*` instance. For the **layout**, reach for the [panel-layout](../panel-layout.md) primitives; the demos below are worked examples to copy. A batteries-included `<CoarFileExplorer>` component is planned — it'll sit on exactly these pieces.
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import {
|
|
13
|
+
useFileExplorer,
|
|
14
|
+
createInMemoryAssetStore,
|
|
15
|
+
type Asset,
|
|
16
|
+
} from '@cocoar/vue-file-explorer-core';
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
The required peer is [`@cocoar/vue-ui`](../tree.md) for `CoarTree` + `CoarTreeNodeLabel`. `@cocoar/vue-script-editor` is **optional** — only pulled in if you want the Monaco-typed `language` field on the file-meta resolver.
|
|
20
|
+
|
|
21
|
+
## Three building blocks
|
|
22
|
+
|
|
23
|
+
| Piece | Role | Doc |
|
|
24
|
+
|---|---|---|
|
|
25
|
+
| [`useFileExplorer({store})`](./use-file-explorer.md) | The composable. Tree state + tab state machine + async bookkeeping + every imperative op. | reference |
|
|
26
|
+
| [`AssetStore<T>`](./asset-store.md) | The data-plane contract a backend implements (HTTP, IndexedDB, in-memory, …). | contract |
|
|
27
|
+
| [`createInMemoryAssetStore`](./in-memory-store.md) | Reference implementation with reactive latency / failure / lazy / conflict knobs for demos and tests. | knobs |
|
|
28
|
+
|
|
29
|
+
## Full dispatch demo
|
|
30
|
+
|
|
31
|
+
A realistic shell — tree, tab bar (with preview/pinned), breadcrumb, and a dispatched editor area that swaps between `CoarScriptEditor` (Monaco) for code, `CoarMarkdownEditor` (Milkdown) for markdown, and `CoarDocumentViewer` for images. Click around, edit anything, **Ctrl+S** saves.
|
|
32
|
+
|
|
33
|
+
**Demo — `file-explorer/demos/FullDispatch.vue`**
|
|
34
|
+
|
|
35
|
+
```vue
|
|
36
|
+
<template>
|
|
37
|
+
<ClientOnly>
|
|
38
|
+
<component :is="Demo" v-if="Demo" />
|
|
39
|
+
<div v-else class="fe-loading">Loading demo…</div>
|
|
40
|
+
</ClientOnly>
|
|
41
|
+
</template>
|
|
42
|
+
|
|
43
|
+
<script setup lang="ts">
|
|
44
|
+
import { onMounted, shallowRef, type Component } from 'vue';
|
|
45
|
+
const Demo = shallowRef<Component | null>(null);
|
|
46
|
+
onMounted(async () => {
|
|
47
|
+
const [editorWorkerMod, tsWorkerMod, jsonWorkerMod] = await Promise.all([
|
|
48
|
+
import('monaco-editor/esm/vs/editor/editor.worker?worker'),
|
|
49
|
+
import('monaco-editor/esm/vs/language/typescript/ts.worker?worker'),
|
|
50
|
+
import('monaco-editor/esm/vs/language/json/json.worker?worker'),
|
|
51
|
+
]);
|
|
52
|
+
(self as unknown as { MonacoEnvironment: unknown }).MonacoEnvironment = {
|
|
53
|
+
getWorker(_id: string, label: string) {
|
|
54
|
+
if (label === 'typescript' || label === 'javascript') return new tsWorkerMod.default();
|
|
55
|
+
if (label === 'json') return new jsonWorkerMod.default();
|
|
56
|
+
return new editorWorkerMod.default();
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
const mod = await import('./_internal/FullDispatchImpl.vue');
|
|
60
|
+
Demo.value = mod.default;
|
|
61
|
+
});
|
|
62
|
+
</script>
|
|
63
|
+
|
|
64
|
+
<style scoped>
|
|
65
|
+
.fe-loading {
|
|
66
|
+
height: 560px;
|
|
67
|
+
display: flex; align-items: center; justify-content: center;
|
|
68
|
+
color: var(--coar-text-neutral-tertiary, #6b7280); font-size: 13px;
|
|
69
|
+
}
|
|
70
|
+
</style>
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Click a file to **preview** it (italic title) — clicking another file replaces the preview tab. **Double-click** to pin (italic clears). Editing a preview tab auto-pins it the moment it goes dirty — the impossible "italic + unsaved" state never exists.
|
|
74
|
+
|
|
75
|
+
> **Tip: Persistent viewer config across file swaps**
|
|
76
|
+
>
|
|
77
|
+
> Open `logo.svg`, click the thumbnails toggle to open the left rail, then switch to `README.md` and back. The rail stays open. Same for any tool config you pass.
|
|
78
|
+
>
|
|
79
|
+
> That's because in the demo, `viewerSidebarOpen` / `viewerAnnotationsPanelOpen` / `viewerTools` live as refs **outside** the editor `v-if` branch — they're consumer-owned state, just passed into `CoarDocumentViewer` via `v-model` (for the panel toggles) and `:tools` (for toolbar config). When you switch from `.md` (Milkdown) back to `.svg` (DocumentViewer), the viewer is freshly mounted but the props it sees on mount are the persisted values — so the panel comes up open, the same tools are configured. No additional API needed on `useFileExplorer` — the shell owns it.
|
|
80
|
+
|
|
81
|
+
## Minimal shell
|
|
82
|
+
|
|
83
|
+
Same composable, no heavy editors — useful as a starting point or for plain-text use cases:
|
|
84
|
+
|
|
85
|
+
**Demo — `file-explorer/demos/BasicUsage.vue`**
|
|
86
|
+
|
|
87
|
+
```vue
|
|
88
|
+
<template>
|
|
89
|
+
<ClientOnly>
|
|
90
|
+
<component :is="Demo" v-if="Demo" />
|
|
91
|
+
<div v-else class="fe-loading">Loading demo…</div>
|
|
92
|
+
</ClientOnly>
|
|
93
|
+
</template>
|
|
94
|
+
|
|
95
|
+
<script setup lang="ts">
|
|
96
|
+
import { onMounted, shallowRef, type Component } from 'vue';
|
|
97
|
+
|
|
98
|
+
const Demo = shallowRef<Component | null>(null);
|
|
99
|
+
onMounted(async () => {
|
|
100
|
+
const mod = await import('./_internal/BasicUsageImpl.vue');
|
|
101
|
+
Demo.value = mod.default;
|
|
102
|
+
});
|
|
103
|
+
</script>
|
|
104
|
+
|
|
105
|
+
<style scoped>
|
|
106
|
+
.fe-loading {
|
|
107
|
+
height: 360px;
|
|
108
|
+
display: flex;
|
|
109
|
+
align-items: center;
|
|
110
|
+
justify-content: center;
|
|
111
|
+
color: var(--coar-text-neutral-tertiary, #6b7280);
|
|
112
|
+
font-size: 13px;
|
|
113
|
+
}
|
|
114
|
+
</style>
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
The composable returns refs (`rootNodes`, `selectedId`, `expanded`, `openTabs`, `activeTab`, `loadingNodes`, `savingNodes`, `breadcrumbPath`) and ops (`openFile`, `saveTab`, `closeTab`, `addFolder`, `addFiles`, `deleteNode`, `moveNode`, `rename`, `reorderTab`, …). Wire whichever you need into your shell.
|
|
118
|
+
|
|
119
|
+
> **Tip: Where's the `<CoarFileExplorer>` component?**
|
|
120
|
+
>
|
|
121
|
+
> Planned — and reserved under the bare `@cocoar/vue-file-explorer` name. It'll be built on these exact pieces: `useFileExplorer` + the [panel-layout](../panel-layout.md) primitives + `CoarTree`. The engine ships first because the **layout** is the part consumers most want to own (sidebar arrangement, tab bar styling, editor dispatch, context-menu shape); the hard-to-get-right bits (placeholder-then-fill open, optimistic rollback, blob-URL lifecycle, beforeunload warning, drag-to-reorder tabs) already live in the engine.
|
|
122
|
+
|
|
123
|
+
## Details panel
|
|
124
|
+
|
|
125
|
+
The explorer hands you the **data** for a details / info panel; **where** it renders is your layout's call. `useFileExplorer` exposes **`selectedAsset`** (the selected node, resolved reactively from `selectedId`) and **`describeAsset(asset)`** (its framework-known property rows). Drop the panel below the tree, into a [`CoarPanelLayout`](../panel-layout.md) region — wherever.
|
|
126
|
+
|
|
127
|
+
**Demo — `file-explorer/demos/InfoPanel.vue`**
|
|
128
|
+
|
|
129
|
+
```vue
|
|
130
|
+
<template>
|
|
131
|
+
<ClientOnly>
|
|
132
|
+
<component :is="Demo" v-if="Demo" />
|
|
133
|
+
<div v-else class="fe-loading">Loading demo…</div>
|
|
134
|
+
</ClientOnly>
|
|
135
|
+
</template>
|
|
136
|
+
|
|
137
|
+
<script setup lang="ts">
|
|
138
|
+
import { onMounted, shallowRef, type Component } from 'vue';
|
|
139
|
+
|
|
140
|
+
const Demo = shallowRef<Component | null>(null);
|
|
141
|
+
onMounted(async () => {
|
|
142
|
+
const mod = await import('./_internal/InfoPanelImpl.vue');
|
|
143
|
+
Demo.value = mod.default;
|
|
144
|
+
});
|
|
145
|
+
</script>
|
|
146
|
+
|
|
147
|
+
<style scoped>
|
|
148
|
+
.fe-loading {
|
|
149
|
+
height: 360px;
|
|
150
|
+
display: flex;
|
|
151
|
+
align-items: center;
|
|
152
|
+
justify-content: center;
|
|
153
|
+
color: var(--coar-text-neutral-tertiary, #6b7280);
|
|
154
|
+
font-size: 13px;
|
|
155
|
+
}
|
|
156
|
+
</style>
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
`describeAsset` returns only what the framework can know from the `Asset` shape + resolved file-meta — **Name, Type, Language** (script files), **Extension**, and **Path**:
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
const fe = useFileExplorer({ store });
|
|
163
|
+
// fe.selectedAsset: Readonly<Ref<Asset<T> | null>>
|
|
164
|
+
// fe.describeAsset(asset) → [{ key, label, value }, …]
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
```vue
|
|
168
|
+
<dl v-if="fe.selectedAsset.value">
|
|
169
|
+
<div v-for="p in fe.describeAsset(fe.selectedAsset.value)" :key="p.key">
|
|
170
|
+
<dt>{{ p.label }}</dt><dd>{{ p.value }}</dd>
|
|
171
|
+
</div>
|
|
172
|
+
</dl>
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Domain fields (size, modified date, author, …) live in your generic `payload<T>` — the framework can't know them, so **append your own rows**: `[...fe.describeAsset(asset), ...myPayloadRows(asset)]`. Need full control? Skip `describeAsset` and build the panel straight off `selectedAsset`.
|
|
176
|
+
|
|
177
|
+
> **Tip: Resizable tree-over-details sidebar**
|
|
178
|
+
>
|
|
179
|
+
> Want the tree stacked above the details panel with a draggable divider (VS-Code style)? Nest a [`CoarSplitPane`](../panel-layout.md) in your sidebar — tree in `#first`, the `selectedAsset` / `describeAsset` panel in `#second`. See the [panel layout](../panel-layout.md) docs.
|
|
180
|
+
|
|
181
|
+
## Lazy mode
|
|
182
|
+
|
|
183
|
+
Stores that implement `loadChildren(parentId)` opt into lazy loading. The composable detects the capability automatically — no flag needed.
|
|
184
|
+
|
|
185
|
+
**Demo — `file-explorer/demos/LazyMode.vue`**
|
|
186
|
+
|
|
187
|
+
```vue
|
|
188
|
+
<template>
|
|
189
|
+
<ClientOnly>
|
|
190
|
+
<component :is="Demo" v-if="Demo" />
|
|
191
|
+
<div v-else class="fe-loading">Loading demo…</div>
|
|
192
|
+
</ClientOnly>
|
|
193
|
+
</template>
|
|
194
|
+
|
|
195
|
+
<script setup lang="ts">
|
|
196
|
+
import { onMounted, shallowRef, type Component } from 'vue';
|
|
197
|
+
const Demo = shallowRef<Component | null>(null);
|
|
198
|
+
onMounted(async () => {
|
|
199
|
+
const mod = await import('./_internal/LazyModeImpl.vue');
|
|
200
|
+
Demo.value = mod.default;
|
|
201
|
+
});
|
|
202
|
+
</script>
|
|
203
|
+
|
|
204
|
+
<style scoped>
|
|
205
|
+
.fe-loading {
|
|
206
|
+
height: 320px; display: flex; align-items: center; justify-content: center;
|
|
207
|
+
color: var(--coar-text-neutral-tertiary, #6b7280); font-size: 13px;
|
|
208
|
+
}
|
|
209
|
+
</style>
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
`hasChildren` hints control which folders show a chevron before their kids load. `loadingNodes` exposes the per-row spinner state so consumers can swap icon → spinner during a fetch.
|
|
213
|
+
|
|
214
|
+
## Conflict policies
|
|
215
|
+
|
|
216
|
+
Uploads and creates run through a per-store conflict policy. Default `'rename'` mirrors Finder / VSCode auto-suffixing (`foo.txt` → `foo (2).txt`).
|
|
217
|
+
|
|
218
|
+
**Demo — `file-explorer/demos/ConflictPolicies.vue`**
|
|
219
|
+
|
|
220
|
+
```vue
|
|
221
|
+
<template>
|
|
222
|
+
<ClientOnly>
|
|
223
|
+
<component :is="Demo" v-if="Demo" />
|
|
224
|
+
<div v-else class="fe-loading">Loading demo…</div>
|
|
225
|
+
</ClientOnly>
|
|
226
|
+
</template>
|
|
227
|
+
|
|
228
|
+
<script setup lang="ts">
|
|
229
|
+
import { onMounted, shallowRef, type Component } from 'vue';
|
|
230
|
+
const Demo = shallowRef<Component | null>(null);
|
|
231
|
+
onMounted(async () => {
|
|
232
|
+
const mod = await import('./_internal/ConflictPoliciesImpl.vue');
|
|
233
|
+
Demo.value = mod.default;
|
|
234
|
+
});
|
|
235
|
+
</script>
|
|
236
|
+
|
|
237
|
+
<style scoped>
|
|
238
|
+
.fe-loading {
|
|
239
|
+
height: 320px; display: flex; align-items: center; justify-content: center;
|
|
240
|
+
color: var(--coar-text-neutral-tertiary, #6b7280); font-size: 13px;
|
|
241
|
+
}
|
|
242
|
+
</style>
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
`move` and `rename` deliberately **bypass** the policy — those are explicit user intent, not file additions, so silently changing the requested name would be surprising.
|
|
246
|
+
|
|
247
|
+
## Sort modes
|
|
248
|
+
|
|
249
|
+
Sibling ordering lives on the composable (not the store), because filesystem backends can't persist per-entry order. Pick one of three built-in strategies or pass a comparator.
|
|
250
|
+
|
|
251
|
+
**Demo — `file-explorer/demos/SortModes.vue`**
|
|
252
|
+
|
|
253
|
+
```vue
|
|
254
|
+
<template>
|
|
255
|
+
<ClientOnly>
|
|
256
|
+
<component :is="Demo" v-if="Demo" />
|
|
257
|
+
<div v-else class="fe-loading">Loading demo…</div>
|
|
258
|
+
</ClientOnly>
|
|
259
|
+
</template>
|
|
260
|
+
|
|
261
|
+
<script setup lang="ts">
|
|
262
|
+
import { onMounted, shallowRef, type Component } from 'vue';
|
|
263
|
+
const Demo = shallowRef<Component | null>(null);
|
|
264
|
+
onMounted(async () => {
|
|
265
|
+
const mod = await import('./_internal/SortModesImpl.vue');
|
|
266
|
+
Demo.value = mod.default;
|
|
267
|
+
});
|
|
268
|
+
</script>
|
|
269
|
+
|
|
270
|
+
<style scoped>
|
|
271
|
+
.fe-loading {
|
|
272
|
+
height: 320px; display: flex; align-items: center; justify-content: center;
|
|
273
|
+
color: var(--coar-text-neutral-tertiary, #6b7280); font-size: 13px;
|
|
274
|
+
}
|
|
275
|
+
</style>
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
| Mode | Behavior |
|
|
279
|
+
|---|---|
|
|
280
|
+
| `'folders-first'` (default) | Folders alphabetical, then files alphabetical — VSCode / Windows Explorer pattern. |
|
|
281
|
+
| `'alphabetical'` | All entries mixed alphabetical — Finder pattern. |
|
|
282
|
+
| `'manual'` | Array order = visual order. Drop-between-siblings positions persist via `store.move(id, parentId, position)`. |
|
|
283
|
+
| `(a, b) => number` | Custom comparator (e.g. by extension, by mtime). |
|
|
284
|
+
|
|
285
|
+
In any non-manual mode the composable silently drops the `position` argument when forwarding to `store.move()` — the comparator decides the final position after the parent change. `api.reorderable: Ref<boolean>` is reactive, so a toolbar can flip drag modes at runtime.
|
|
286
|
+
|
|
287
|
+
## Architecture
|
|
288
|
+
|
|
289
|
+
```
|
|
290
|
+
useFileExplorer({store})
|
|
291
|
+
├── AssetStore<T> ← your backend
|
|
292
|
+
│ loadTree / loadChildren / loadContent
|
|
293
|
+
│ createFolder / createFile / uploadFile
|
|
294
|
+
│ save / rename / delete / move
|
|
295
|
+
├── tree state ← rootNodes, selectedId, expanded
|
|
296
|
+
├── tab state machine ← openTabs, activeTab, dirty, pin/preview
|
|
297
|
+
├── async state ← loadingNodes, savingNodes
|
|
298
|
+
├── blob-URL leases ← revoked on delete + unmount
|
|
299
|
+
├── beforeunload warning ← active while any tab is dirty
|
|
300
|
+
└── 3-stage file-meta fallback ← asset.editor → getFileMeta → ext heuristic
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
The composable knows nothing about HTTP / IndexedDB / multipart — the `AssetStore<T>` contract is the seam. Swap implementations without touching the view.
|
|
304
|
+
|
|
305
|
+
## What's next
|
|
306
|
+
|
|
307
|
+
| Page | Covers |
|
|
308
|
+
|------|--------|
|
|
309
|
+
| [`useFileExplorer`](./use-file-explorer.md) | Options, return surface, tab state machine, navigation helpers, lifecycle |
|
|
310
|
+
| [`AssetStore<T>` contract](./asset-store.md) | Every method's signature + semantics, conflict pipeline, lazy opt-in, error funnel |
|
|
311
|
+
| [In-memory store](./in-memory-store.md) | `createInMemoryAssetStore` knobs — latency / failure / sort / lazy / conflict |
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
<!-- Generated from apps/docs/components/file-explorer/use-file-explorer.md by apps/docs/scripts/sync-skill.mjs. Do not edit; edit the docs page. -->
|
|
2
|
+
|
|
3
|
+
# useFileExplorer
|
|
4
|
+
|
|
5
|
+
`useFileExplorer<T>({store, ...})` is the composable. It wires a configured [`AssetStore<T>`](./asset-store.md) into reactive tree + tab state, owns the async / dirty / blob-URL bookkeeping, and returns every ref and op a file-explorer shell needs.
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import {
|
|
9
|
+
useFileExplorer,
|
|
10
|
+
type UseFileExplorerOptions,
|
|
11
|
+
type UseFileExplorerReturn,
|
|
12
|
+
type OpenTab,
|
|
13
|
+
} from '@cocoar/vue-file-explorer-core';
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Options
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
interface UseFileExplorerOptions<T = unknown> {
|
|
20
|
+
store: AssetStore<T>;
|
|
21
|
+
onError?: (op: AssetOp, err: unknown, ctx: AssetOpContext) => void;
|
|
22
|
+
getFileMeta?: (asset: Asset<T>) => FileMeta | null;
|
|
23
|
+
confirm?: (message: string) => boolean;
|
|
24
|
+
initialExpandedIds?: readonly string[];
|
|
25
|
+
sortMode?: MaybeRefOrGetter<SortMode<T>>;
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
| Option | Default | Notes |
|
|
30
|
+
|---|---|---|
|
|
31
|
+
| `store` | _required_ | Anything implementing [`AssetStore<T>`](./asset-store.md). The single seam between view and backend. |
|
|
32
|
+
| `onError` | _no-op_ | Single funnel for every store rejection. The composable has already rolled back by the time this fires. |
|
|
33
|
+
| `getFileMeta` | _none_ | Stage-2 of the file-meta fallback chain. Returns `null` to fall through to the extension heuristic. |
|
|
34
|
+
| `confirm` | `window.confirm` | Used by `closeTab` / `closeOthers` / `closeToRight` / `closeAll` to confirm discarding dirty tabs. Override for custom dialogs. |
|
|
35
|
+
| `initialExpandedIds` | top-level folders (eager) / `[]` (lazy) | Folder ids to seed `expanded` with. Defaults to root folders in eager mode so the user sees content immediately; defaults to empty in lazy mode so the canonical click-to-expand UX appears. |
|
|
36
|
+
| `sortMode` | `'folders-first'` | Sibling ordering strategy. `MaybeRefOrGetter` so a toolbar can flip it live. See [sort modes](./index.md#sort-modes). |
|
|
37
|
+
|
|
38
|
+
## Return surface
|
|
39
|
+
|
|
40
|
+
The return is organized into four groups: tree state, tab state, ops, navigation.
|
|
41
|
+
|
|
42
|
+
### Tree state
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
readonly assets: Readonly<Ref<readonly Asset<T>[]>>;
|
|
46
|
+
readonly rootNodes: Readonly<Ref<readonly Asset<T>[]>>;
|
|
47
|
+
selectedId: Ref<string | null>;
|
|
48
|
+
readonly selectedAsset: Readonly<Ref<Asset<T> | null>>;
|
|
49
|
+
expanded: Ref<Set<string>>;
|
|
50
|
+
readonly loading: Readonly<Ref<boolean>>;
|
|
51
|
+
readonly loadingNodes: Readonly<Ref<ReadonlySet<string>>>;
|
|
52
|
+
readonly savingNodes: Readonly<Ref<ReadonlySet<string>>>;
|
|
53
|
+
readonly reorderable: Readonly<Ref<boolean>>;
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
| Ref | Notes |
|
|
57
|
+
|---|---|
|
|
58
|
+
| `assets` | Flat reactive list — the store's underlying projection. Read-only from the consumer's perspective. |
|
|
59
|
+
| `rootNodes` | Already filtered + sorted children of `null`. Pass directly to `<CoarTree :nodes>`. |
|
|
60
|
+
| `selectedId` | Two-way. Single-click on a row sets this; a watcher then opens the file as a preview tab. |
|
|
61
|
+
| `selectedAsset` | Read-only. `selectedId` resolved to the `Asset` (or `null`). Pair with `describeAsset` for a [details panel](./index.md#details-panel). |
|
|
62
|
+
| `expanded` | Two-way `Set<string>` of expanded folder ids. Lazy loading is driven by CoarTree's `loadChildren` hook (bind `:load-children="fe.loadChildren"`) — the tree fires the fetch on first expand. |
|
|
63
|
+
| `loading` | `true` during the **initial** `store.loadTree()` call only. Per-file content loads live on `loadingNodes`; per-folder lazy loads are owned by the tree (`isLoading` slot prop). Stays `false` for stores that surface their own reactive `_assets`. |
|
|
64
|
+
| `loadingNodes` | Per-id Set of files being `loadContent`-fetched. (Folder lazy-loads moved to CoarTree's per-row `isLoading` slot prop.) Bind to row-icon → spinner swap. |
|
|
65
|
+
| `savingNodes` | Per-id Set: any in-flight save / rename / delete / move. Same spinner channel as `loadingNodes`. |
|
|
66
|
+
| `reorderable` | `true` when `sortMode === 'manual'`. Reactive — read it in your CoarTree wiring to gate drop-between-siblings. |
|
|
67
|
+
|
|
68
|
+
### CoarTree wiring helpers
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
getId: (a: Asset<T>) => string;
|
|
72
|
+
getChildren: (a: Asset<T>) => readonly Asset<T>[] | undefined;
|
|
73
|
+
getLabel: (a: Asset<T>) => string;
|
|
74
|
+
isExpandable: (a: Asset<T>) => boolean;
|
|
75
|
+
loadChildren?: (node: Asset<T>) => Promise<void>; // lazy stores only — bind to <CoarTree :load-children>
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
These mirror `<CoarTree>`'s prop signatures so you can pass them straight through:
|
|
79
|
+
|
|
80
|
+
```vue
|
|
81
|
+
<CoarTree
|
|
82
|
+
:nodes="fe.rootNodes.value"
|
|
83
|
+
:get-id="fe.getId"
|
|
84
|
+
:get-children="fe.getChildren"
|
|
85
|
+
:get-label="fe.getLabel"
|
|
86
|
+
:is-expandable="fe.isExpandable"
|
|
87
|
+
:load-children="fe.loadChildren"
|
|
88
|
+
v-model:expanded="fe.expanded.value"
|
|
89
|
+
v-model:selected="fe.selectedId.value"
|
|
90
|
+
draggable
|
|
91
|
+
renamable
|
|
92
|
+
@activate="fe.activateNode"
|
|
93
|
+
@rename="({ node, newName }) => fe.rename(node.id, newName)"
|
|
94
|
+
@node-move="fe.moveNode"
|
|
95
|
+
/>
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
`isExpandable` returns `false` for folders the store reports as `hasChildren: false` — keeps the chevron off known-empty folders in lazy mode.
|
|
99
|
+
|
|
100
|
+
For **lazy stores**, bind `:load-children="fe.loadChildren"` (`undefined` for eager stores, so the binding is a no-op there). CoarTree calls it on first expand of an unloaded folder and owns the loading state (`isLoading` slot prop), the error state (`hasError` + `@load-error`) and retry (`api.reloadChildren`); the composable just supplies the fetch body and caches loaded folders so re-expand never re-fetches. Render the spinner where you like from `isLoading` (e.g. swap the row icon) and pass `hide-loading-spinner` to drop the tree's built-in chevron spinner. See the [tree lazy-loading docs](../tree.md#lazy-loading-async-children). Lazy-load failures reach **both** `onError('loadChildren', …)` (log/toast) and the tree's `@load-error` / `hasError` (row UX) — pick one channel for user-facing messaging to avoid duplicates.
|
|
101
|
+
|
|
102
|
+
### Tab state
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
readonly openTabs: Readonly<Ref<readonly OpenTab[]>>;
|
|
106
|
+
activeId: Ref<string | null>;
|
|
107
|
+
readonly activeTab: Readonly<Ref<OpenTab | null>>;
|
|
108
|
+
readonly anyDirty: Readonly<Ref<boolean>>;
|
|
109
|
+
isDirty: (tab: OpenTab) => boolean;
|
|
110
|
+
setContent: (id: string, content: string) => void;
|
|
111
|
+
|
|
112
|
+
interface OpenTab {
|
|
113
|
+
id: string;
|
|
114
|
+
name: string;
|
|
115
|
+
editor: FileEditor;
|
|
116
|
+
language?: CoarScriptEditorLanguage;
|
|
117
|
+
content: string; // current editor buffer
|
|
118
|
+
savedContent: string; // last persisted — content !== savedContent ⇒ dirty
|
|
119
|
+
pinned: boolean; // false = preview (italic, replaced on next preview)
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
The tab state machine implements the VSCode pattern:
|
|
124
|
+
|
|
125
|
+
| Trigger | Result |
|
|
126
|
+
|---|---|
|
|
127
|
+
| Single-click on a file row | Push a **preview** tab (italic title). At most one preview exists at a time — opening another preview replaces it. |
|
|
128
|
+
| Double-click / Enter / "Open" menu | Open as **pinned**. Existing previews are upgraded in place. |
|
|
129
|
+
| User edits → `setContent` differs from `savedContent` | Auto-pin the tab. Eliminates the impossible "italic + dirty" state. |
|
|
130
|
+
| `closeTab` while dirty | Calls `options.confirm` ("Discard unsaved changes to '{name}'?"). |
|
|
131
|
+
| `closeTab` while `savingNodes.has(id)` | Bails — closing would orphan the in-flight save. |
|
|
132
|
+
|
|
133
|
+
### Imperative ops
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
// CRUD — optimistic; resolve when the backend confirms
|
|
137
|
+
addFolder(parentId: string | null, name: string): Promise<Asset<T> | null>;
|
|
138
|
+
addFiles(parentId: string | null, files: FileList | readonly File[]): Promise<void>;
|
|
139
|
+
deleteNode(asset: Asset<T>): Promise<void>;
|
|
140
|
+
move(id: string, newParentId: string | null, position?: number): Promise<void>;
|
|
141
|
+
moveNode(e: CoarTreeNodeMoveEvent<Asset<T>>): Promise<void>;
|
|
142
|
+
rename(id: string, newName: string): Promise<void>;
|
|
143
|
+
refresh(folderId?: string | null): Promise<void>;
|
|
144
|
+
|
|
145
|
+
// Tab ops
|
|
146
|
+
openFile(asset: Asset<T>, opts?: { pinned: boolean }): Promise<void>;
|
|
147
|
+
activateNode(asset: Asset<T>): void; // dblclick / Enter — opens pinned
|
|
148
|
+
saveTab(id: string): Promise<boolean>;
|
|
149
|
+
saveActive(): Promise<void>;
|
|
150
|
+
closeTab(id: string): void;
|
|
151
|
+
closeOthers(keepId: string): void;
|
|
152
|
+
closeToRight(anchorId: string): void;
|
|
153
|
+
closeAll(): void;
|
|
154
|
+
pinTab(id: string): void;
|
|
155
|
+
unpinTab(id: string): void;
|
|
156
|
+
reorderTab(sourceId: string, targetId: string, position: 'before' | 'after'): void;
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Notes on the trickier ones:
|
|
160
|
+
|
|
161
|
+
- **`addFolder`** is **optimistic**: a temp node is inserted immediately, then reconciled to the backend's real id on resolve (rolled back on error). Pairs with [`<CoarTree>`'s `startCreate`](../tree.md#inline-create) so the draft → real-node handoff has no flicker. (Stores that surface their own reactive `_assets` skip the temp node — their own mutation is the source.)
|
|
162
|
+
- **`addFiles`** is the OS-drop entry point. The composable derives content per file (text or `URL.createObjectURL` for PDF / image), calls `store.uploadFile()`, then `store.save(id, content)` **if the store has `save`**. The merged node is stamped with the target `parentId`, so a folder-filtered grid shows it immediately even if the store's returned asset omitted it. Blob URLs are tracked and revoked on delete + unmount.
|
|
163
|
+
- **`move`** is the plain programmatic move (optimistic + rollback) for sources that aren't a tree drag — a "move to folder" `<select>`, a grid card dropped on a folder row, an undo command. `newParentId: null` moves to the root; `position` (an index within the new parent's children) is honored only in `'manual'` sort mode. **`moveNode`** delegates to it.
|
|
164
|
+
- **`moveNode`** consumes `<CoarTree>`'s `CoarTreeNodeMoveEvent`. For `position: 'inside'`, it auto-expands the target folder. For `'before' / 'after'`, it forwards a computed index only when `reorderable.value`.
|
|
165
|
+
- **`openFile`** is the placeholder-then-fill flow. The placeholder tab is pushed + activated immediately; on `loadContent` rejection the placeholder rolls back so the user isn't stranded. **Browse-only stores** (no `loadContent`) make this a no-op — no editor tabs open, and the single-click-preview watcher stays quiet.
|
|
166
|
+
- **`activateNode`** is meant for `<CoarTree @activate>`. Files open pinned; folders are a no-op (CoarTree itself toggles expansion).
|
|
167
|
+
- **`reorderTab`** is for drag-to-reorder. No-op on self-drop or unknown ids; pinned status is preserved on the moved tab.
|
|
168
|
+
- **`refresh()`** re-runs `store.loadTree()` (or `store.loadChildren(folderId)` in lazy mode when given a folder id). Use it when upstream state can change out-of-band — a SignalR push from the backend, another tab uploading a file, a server-side retention sweep. No-op for stores that surface a reactive `_assets` directly: those are already live.
|
|
169
|
+
|
|
170
|
+
### Navigation
|
|
171
|
+
|
|
172
|
+
```ts
|
|
173
|
+
revealInTree(id: string, focusNode?: (id: string) => void): void;
|
|
174
|
+
readonly breadcrumbPath: Readonly<Ref<readonly string[]>>;
|
|
175
|
+
pathOf(id: string): string[];
|
|
176
|
+
fileMeta(asset: Asset<T>): FileMeta | null;
|
|
177
|
+
describeAsset(asset: Asset<T>): AssetProperty[]; // { key, label, value }
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
| Helper | Notes |
|
|
181
|
+
|---|---|
|
|
182
|
+
| `revealInTree` | Walks the parentId chain, expands every ancestor in one batch, sets `selectedId`, then calls `focusNode?.(id)` after Vue flushes. Pass the tree's `api.focusNode` if you have it. |
|
|
183
|
+
| `breadcrumbPath` | Name-path of the **active tab**. Drives the editor-area breadcrumb. |
|
|
184
|
+
| `pathOf(id)` | Name-path of any asset. Used for quick-open / recent-files matchers. |
|
|
185
|
+
| `fileMeta(asset)` | Runs the 3-stage fallback. Returns `null` for unrecognised binary types — caller skips with a warning. |
|
|
186
|
+
| `describeAsset(asset)` | Framework-known property rows (Name, Type, Language, Extension, Path) for a [details panel](./index.md#details-panel). Append your own `payload`-derived rows. Pure helper also exported as `buildAssetProperties`. |
|
|
187
|
+
|
|
188
|
+
## Persistent viewer config across file swaps
|
|
189
|
+
|
|
190
|
+
When the consumer's shell mounts different editors via `v-if` based on `activeTab.editor`, each editor component is freshly mounted on every editor-type swap (e.g. `.md` → `.ts` → `.md` unmounts + remounts Monaco the second time). Any internal editor state — Monaco's scroll position, `CoarDocumentViewer`'s sidebar open / closed, Milkdown's toolbar collapse — **resets on remount** unless the consumer's shell holds that state as refs and passes them in.
|
|
191
|
+
|
|
192
|
+
The cleanest pattern: hold whatever you want to persist as refs **outside the `v-if` branch**, bind them via `v-model` or pass them via props.
|
|
193
|
+
|
|
194
|
+
```vue
|
|
195
|
+
<script setup lang="ts">
|
|
196
|
+
import { ref } from 'vue';
|
|
197
|
+
import { CoarDocumentViewer, type CoarDocumentViewerTool } from '@cocoar/vue-document-viewer';
|
|
198
|
+
|
|
199
|
+
// These live in the shell, not inside the v-if branch — they survive editor swaps.
|
|
200
|
+
const viewerSidebarOpen = ref(false);
|
|
201
|
+
const viewerAnnotationsPanelOpen = ref(false);
|
|
202
|
+
const viewerTools: CoarDocumentViewerTool[] = [
|
|
203
|
+
'sidebar-toggle', 'annotations-panel', 'separator',
|
|
204
|
+
'zoom-out', 'zoom-reset', 'zoom-in', 'separator',
|
|
205
|
+
'fit-width', 'fit-page',
|
|
206
|
+
];
|
|
207
|
+
</script>
|
|
208
|
+
|
|
209
|
+
<template>
|
|
210
|
+
<CoarDocumentViewer
|
|
211
|
+
v-if="fe.activeTab.value?.editor === 'image' && imageSrc"
|
|
212
|
+
:source="imageSrc"
|
|
213
|
+
:tools="viewerTools"
|
|
214
|
+
:show-thumbnails="true"
|
|
215
|
+
:show-annotations-panel="true"
|
|
216
|
+
v-model:sidebar-open="viewerSidebarOpen"
|
|
217
|
+
v-model:annotations-panel-open="viewerAnnotationsPanelOpen"
|
|
218
|
+
/>
|
|
219
|
+
</template>
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
> **Tip: This is shell-level state, not composable-level**
|
|
223
|
+
>
|
|
224
|
+
> The composable deliberately knows nothing about editors — `useFileExplorer` doesn't render them or hold their config. That keeps it independent of any specific editor package, and lets every consumer's shell define its own dispatch logic (which editor for which file type, fall-through to plain text, custom editors for proprietary asset payload types, …). The trade-off: editor persistence is a shell concern. The pattern above is the canonical way to handle it.
|
|
225
|
+
|
|
226
|
+
This works for every editor: hold a Monaco view-state ref outside the `v-if` and restore it in `onMounted`; hold a Milkdown collapsed-toolbar boolean outside and pass it via prop; hold the position memory ref outside and bind it to `v-model:position`. The composable doesn't care — its job is the tree + tab state, not the editor's internals.
|
|
227
|
+
|
|
228
|
+
## Lifecycle
|
|
229
|
+
|
|
230
|
+
- **Mount — the tree loads itself.** `useFileExplorer` calls `store.loadTree()` automatically on mount (it kicks off the fetch synchronously during setup; `loading` is your "populating" signal). **You don't need a manual `fe.refresh()` in `onMounted`** — that would be a redundant second fetch. Reserve `refresh()` for out-of-band changes (server push, another tab mutating, retention sweep). Stores that surface their own reactive `_assets` (the in-memory impl) are live from creation, so the auto-load is a no-op for them.
|
|
231
|
+
- **Lazy children** — Lazy loading is driven by CoarTree's `loadChildren` hook (bind `:load-children="fe.loadChildren"`): the tree fires the fetch on first expand and on mount for any seeded `initialExpandedIds` (cascading as parents publish). The composable supplies only the fetch body and caches loaded folders so re-expand never re-fetches.
|
|
232
|
+
- **Eager open** — Single-click `selectedId` change is watched; file selections fire `openFile(file, { pinned: false })`.
|
|
233
|
+
- **Unmount** — `onScopeDispose` removes the `beforeunload` listener and revokes every blob URL the composable owns. Consumer doesn't need to clean up.
|
|
234
|
+
- **`beforeunload`** — Active while `anyDirty.value` is true. Browser shows its native "leave site?" prompt.
|
|
235
|
+
|
|
236
|
+
## Example consumer shell
|
|
237
|
+
|
|
238
|
+
A minimal shell — tree + breadcrumb + a single-tab editor area — to show what the composable's surface looks like in practice:
|
|
239
|
+
|
|
240
|
+
```vue
|
|
241
|
+
<script setup lang="ts">
|
|
242
|
+
import {
|
|
243
|
+
createInMemoryAssetStore,
|
|
244
|
+
useFileExplorer,
|
|
245
|
+
type Asset,
|
|
246
|
+
} from '@cocoar/vue-file-explorer-core';
|
|
247
|
+
import { CoarTree, CoarTreeNodeLabel, CoarBreadcrumb, CoarBreadcrumbItem } from '@cocoar/vue-ui';
|
|
248
|
+
|
|
249
|
+
const seed: Asset[] = [
|
|
250
|
+
{ id: 's', name: 'src', kind: 'folder', parentId: null },
|
|
251
|
+
{ id: 'u', name: 'utils.ts', kind: 'file', parentId: 's' },
|
|
252
|
+
];
|
|
253
|
+
const store = createInMemoryAssetStore({
|
|
254
|
+
initialTree: seed,
|
|
255
|
+
initialContent: { u: 'export const clamp = (n, lo, hi) => Math.min(hi, Math.max(lo, n));' },
|
|
256
|
+
});
|
|
257
|
+
const fe = useFileExplorer({
|
|
258
|
+
store,
|
|
259
|
+
onError: (op, err) => console.warn(`[file-explorer] ${op}:`, err),
|
|
260
|
+
});
|
|
261
|
+
</script>
|
|
262
|
+
|
|
263
|
+
<template>
|
|
264
|
+
<div class="shell">
|
|
265
|
+
<aside>
|
|
266
|
+
<CoarTree
|
|
267
|
+
:nodes="fe.rootNodes.value"
|
|
268
|
+
:get-id="fe.getId"
|
|
269
|
+
:get-children="fe.getChildren"
|
|
270
|
+
:get-label="fe.getLabel"
|
|
271
|
+
:is-expandable="fe.isExpandable"
|
|
272
|
+
:load-children="fe.loadChildren"
|
|
273
|
+
v-model:expanded="fe.expanded.value"
|
|
274
|
+
v-model:selected="fe.selectedId.value"
|
|
275
|
+
renamable
|
|
276
|
+
@activate="fe.activateNode"
|
|
277
|
+
@rename="({ node, newName }) => fe.rename(node.id, newName)"
|
|
278
|
+
>
|
|
279
|
+
<template #default="{ node }">
|
|
280
|
+
<CoarTreeNodeLabel :label="node.name" />
|
|
281
|
+
</template>
|
|
282
|
+
</CoarTree>
|
|
283
|
+
</aside>
|
|
284
|
+
<main>
|
|
285
|
+
<CoarBreadcrumb v-if="fe.activeTab.value">
|
|
286
|
+
<CoarBreadcrumbItem v-for="seg in fe.breadcrumbPath.value" :key="seg">
|
|
287
|
+
{{ seg }}
|
|
288
|
+
</CoarBreadcrumbItem>
|
|
289
|
+
</CoarBreadcrumb>
|
|
290
|
+
<textarea
|
|
291
|
+
v-if="fe.activeTab.value"
|
|
292
|
+
:value="fe.activeTab.value.content"
|
|
293
|
+
@input="e => fe.setContent(fe.activeTab.value!.id, (e.target as HTMLTextAreaElement).value)"
|
|
294
|
+
/>
|
|
295
|
+
</main>
|
|
296
|
+
</div>
|
|
297
|
+
</template>
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
The full POC (`apps/playground/src/views/FileExplorerPocView.vue` — ~1280 LoC) wires every feature: tab bar with drag-to-reorder, context menus, `Ctrl+P` quick-open, simulator panel for latency / failure / sort / conflict, editor dispatch across Monaco / Milkdown / `CoarDocumentViewer`, OS file drop, reveal-in-tree. Treat it as the worked reference.
|