@cocoar/vue-ui 3.1.0 → 3.2.0-beta.6

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 (179) hide show
  1. package/dist/components/checkbox/CoarCheckbox.vue.d.ts +1 -1
  2. package/dist/components/checkbox/CoarCheckbox.vue.d.ts.map +1 -1
  3. package/dist/components/code-block/CoarCodeBlock.vue.d.ts +1 -1
  4. package/dist/components/code-block/CoarCodeBlock.vue.d.ts.map +1 -1
  5. package/dist/components/data-list/CoarDataList.vue.d.ts +158 -0
  6. package/dist/components/data-list/CoarDataList.vue.d.ts.map +1 -0
  7. package/dist/components/data-list/CoarDataListToolbar.vue.d.ts +30 -0
  8. package/dist/components/data-list/CoarDataListToolbar.vue.d.ts.map +1 -0
  9. package/dist/components/data-list/data-list-builder.d.ts +288 -0
  10. package/dist/components/data-list/data-list-builder.d.ts.map +1 -0
  11. package/dist/components/data-list/index.d.ts +12 -0
  12. package/dist/components/data-list/index.d.ts.map +1 -0
  13. package/dist/components/data-list/internal/compare.d.ts +14 -0
  14. package/dist/components/data-list/internal/compare.d.ts.map +1 -0
  15. package/dist/components/data-list/internal/reorder-core.d.ts +46 -0
  16. package/dist/components/data-list/internal/reorder-core.d.ts.map +1 -0
  17. package/dist/components/data-list/internal/search.d.ts +16 -0
  18. package/dist/components/data-list/internal/search.d.ts.map +1 -0
  19. package/dist/components/data-list/internal/useDataListLines.d.ts +92 -0
  20. package/dist/components/data-list/internal/useDataListLines.d.ts.map +1 -0
  21. package/dist/components/data-list/internal/useDataListReorder.d.ts +68 -0
  22. package/dist/components/data-list/internal/useDataListReorder.d.ts.map +1 -0
  23. package/dist/components/data-list/internal/useSearchHighlight.d.ts +19 -0
  24. package/dist/components/data-list/internal/useSearchHighlight.d.ts.map +1 -0
  25. package/dist/components/data-list/types.d.ts +185 -0
  26. package/dist/components/data-list/types.d.ts.map +1 -0
  27. package/dist/components/data-list/useDataListModel.d.ts +79 -0
  28. package/dist/components/data-list/useDataListModel.d.ts.map +1 -0
  29. package/dist/components/date-time/_shared/maskito-config.d.ts +7 -2
  30. package/dist/components/date-time/_shared/maskito-config.d.ts.map +1 -1
  31. package/dist/components/date-time/_shared/time-helpers.d.ts +11 -4
  32. package/dist/components/date-time/_shared/time-helpers.d.ts.map +1 -1
  33. package/dist/components/date-time/_shared/use-date-picker-base.d.ts +1 -1
  34. package/dist/components/date-time/plain-date-picker/CoarPlainDatePicker.vue.d.ts +1 -1
  35. package/dist/components/date-time/plain-date-picker/CoarPlainDatePicker.vue.d.ts.map +1 -1
  36. package/dist/components/date-time/plain-date-time-picker/CoarPlainDateTimePicker.vue.d.ts +1 -1
  37. package/dist/components/date-time/plain-date-time-picker/CoarPlainDateTimePicker.vue.d.ts.map +1 -1
  38. package/dist/components/date-time/zoned-date-time-picker/CoarZonedDateTimePicker.vue.d.ts +1 -1
  39. package/dist/components/date-time/zoned-date-time-picker/CoarZonedDateTimePicker.vue.d.ts.map +1 -1
  40. package/dist/components/dual-listbox/CoarDualListbox.vue.d.ts +3 -0
  41. package/dist/components/dual-listbox/CoarDualListbox.vue.d.ts.map +1 -1
  42. package/dist/components/form-field/CoarFormField.vue.d.ts +1 -1
  43. package/dist/components/form-field/CoarFormField.vue.d.ts.map +1 -1
  44. package/dist/components/icon/CoarIcon.vue.d.ts +2 -2
  45. package/dist/components/icon/CoarIcon.vue.d.ts.map +1 -1
  46. package/dist/components/listbox/CoarListbox.vue.d.ts +7 -0
  47. package/dist/components/listbox/CoarListbox.vue.d.ts.map +1 -1
  48. package/dist/components/number-input/CoarNumberInput.vue.d.ts +1 -1
  49. package/dist/components/number-input/CoarNumberInput.vue.d.ts.map +1 -1
  50. package/dist/components/otp-input/CoarOtpInput.vue.d.ts +1 -1
  51. package/dist/components/otp-input/CoarOtpInput.vue.d.ts.map +1 -1
  52. package/dist/components/password-input/CoarPasswordInput.vue.d.ts +1 -1
  53. package/dist/components/password-input/CoarPasswordInput.vue.d.ts.map +1 -1
  54. package/dist/components/switch/CoarSwitch.vue.d.ts +1 -1
  55. package/dist/components/switch/CoarSwitch.vue.d.ts.map +1 -1
  56. package/dist/components/text-input/CoarTextInput.vue.d.ts +1 -1
  57. package/dist/components/text-input/CoarTextInput.vue.d.ts.map +1 -1
  58. package/dist/composables/dragRegistry.d.ts +12 -0
  59. package/dist/composables/dragRegistry.d.ts.map +1 -1
  60. package/dist/composables/index.d.ts +2 -2
  61. package/dist/composables/index.d.ts.map +1 -1
  62. package/dist/composables/useDragDrop.d.ts +62 -6
  63. package/dist/composables/useDragDrop.d.ts.map +1 -1
  64. package/dist/composables/useVirtualList.d.ts +21 -1
  65. package/dist/composables/useVirtualList.d.ts.map +1 -1
  66. package/dist/index.css +1 -1
  67. package/dist/index.d.ts +4 -2
  68. package/dist/index.d.ts.map +1 -1
  69. package/dist/index.js +4508 -2039
  70. package/package.json +5 -3
  71. package/skills/cocoar-vue-ui/SKILL.md +244 -0
  72. package/skills/cocoar-vue-ui/references/components/avatar.md +178 -0
  73. package/skills/cocoar-vue-ui/references/components/badge.md +290 -0
  74. package/skills/cocoar-vue-ui/references/components/breadcrumb.md +238 -0
  75. package/skills/cocoar-vue-ui/references/components/button.md +252 -0
  76. package/skills/cocoar-vue-ui/references/components/calendar/agenda-view.md +353 -0
  77. package/skills/cocoar-vue-ui/references/components/calendar/coar-calendar.md +1645 -0
  78. package/skills/cocoar-vue-ui/references/components/calendar/day-view.md +361 -0
  79. package/skills/cocoar-vue-ui/references/components/calendar/index.md +312 -0
  80. package/skills/cocoar-vue-ui/references/components/calendar/month-view.md +475 -0
  81. package/skills/cocoar-vue-ui/references/components/calendar/performance.md +186 -0
  82. package/skills/cocoar-vue-ui/references/components/calendar/timeline-view.md +301 -0
  83. package/skills/cocoar-vue-ui/references/components/calendar/week-view.md +382 -0
  84. package/skills/cocoar-vue-ui/references/components/calendar/work-week-view.md +145 -0
  85. package/skills/cocoar-vue-ui/references/components/calendar/year-view.md +63 -0
  86. package/skills/cocoar-vue-ui/references/components/card.md +151 -0
  87. package/skills/cocoar-vue-ui/references/components/checkbox-group.md +103 -0
  88. package/skills/cocoar-vue-ui/references/components/checkbox.md +279 -0
  89. package/skills/cocoar-vue-ui/references/components/code-block.md +241 -0
  90. package/skills/cocoar-vue-ui/references/components/context-menu.md +355 -0
  91. package/skills/cocoar-vue-ui/references/components/data-grid/checkbox.md +213 -0
  92. package/skills/cocoar-vue-ui/references/components/data-grid/date-columns.md +272 -0
  93. package/skills/cocoar-vue-ui/references/components/data-grid/editing.md +236 -0
  94. package/skills/cocoar-vue-ui/references/components/data-grid/multi-select.md +193 -0
  95. package/skills/cocoar-vue-ui/references/components/data-grid/number.md +147 -0
  96. package/skills/cocoar-vue-ui/references/components/data-grid/select.md +202 -0
  97. package/skills/cocoar-vue-ui/references/components/data-grid/text.md +127 -0
  98. package/skills/cocoar-vue-ui/references/components/data-grid.md +1225 -0
  99. package/skills/cocoar-vue-ui/references/components/data-list.md +2121 -0
  100. package/skills/cocoar-vue-ui/references/components/date-or-time-picker.md +200 -0
  101. package/skills/cocoar-vue-ui/references/components/date-picker.md +203 -0
  102. package/skills/cocoar-vue-ui/references/components/date-time-picker.md +154 -0
  103. package/skills/cocoar-vue-ui/references/components/date-views.md +212 -0
  104. package/skills/cocoar-vue-ui/references/components/dialog.md +148 -0
  105. package/skills/cocoar-vue-ui/references/components/divider.md +157 -0
  106. package/skills/cocoar-vue-ui/references/components/document-viewer/annotations.md +400 -0
  107. package/skills/cocoar-vue-ui/references/components/document-viewer/coar-document-viewer.md +327 -0
  108. package/skills/cocoar-vue-ui/references/components/document-viewer/index.md +234 -0
  109. package/skills/cocoar-vue-ui/references/components/document-viewer/toolbar.md +221 -0
  110. package/skills/cocoar-vue-ui/references/components/drag-drop.md +267 -0
  111. package/skills/cocoar-vue-ui/references/components/dual-listbox.md +449 -0
  112. package/skills/cocoar-vue-ui/references/components/file-explorer/asset-store.md +327 -0
  113. package/skills/cocoar-vue-ui/references/components/file-explorer/in-memory-store.md +176 -0
  114. package/skills/cocoar-vue-ui/references/components/file-explorer/index.md +311 -0
  115. package/skills/cocoar-vue-ui/references/components/file-explorer/use-file-explorer.md +300 -0
  116. package/skills/cocoar-vue-ui/references/components/form-field.md +1025 -0
  117. package/skills/cocoar-vue-ui/references/components/fragment-parser.md +298 -0
  118. package/skills/cocoar-vue-ui/references/components/link.md +235 -0
  119. package/skills/cocoar-vue-ui/references/components/listbox.md +575 -0
  120. package/skills/cocoar-vue-ui/references/components/map/editor.md +448 -0
  121. package/skills/cocoar-vue-ui/references/components/map/index.md +351 -0
  122. package/skills/cocoar-vue-ui/references/components/markdown-diagrams.md +210 -0
  123. package/skills/cocoar-vue-ui/references/components/markdown-editor.md +1478 -0
  124. package/skills/cocoar-vue-ui/references/components/markdown-embeds.md +387 -0
  125. package/skills/cocoar-vue-ui/references/components/markdown-form.md +447 -0
  126. package/skills/cocoar-vue-ui/references/components/markdown.md +276 -0
  127. package/skills/cocoar-vue-ui/references/components/menu.md +380 -0
  128. package/skills/cocoar-vue-ui/references/components/mermaid.md +172 -0
  129. package/skills/cocoar-vue-ui/references/components/navbar.md +147 -0
  130. package/skills/cocoar-vue-ui/references/components/note.md +96 -0
  131. package/skills/cocoar-vue-ui/references/components/notice.md +131 -0
  132. package/skills/cocoar-vue-ui/references/components/number-input.md +215 -0
  133. package/skills/cocoar-vue-ui/references/components/otp-input.md +324 -0
  134. package/skills/cocoar-vue-ui/references/components/page-builder/authoring-contract.md +271 -0
  135. package/skills/cocoar-vue-ui/references/components/page-builder/coar-page-builder.md +476 -0
  136. package/skills/cocoar-vue-ui/references/components/page-builder/coar-page-renderer.md +677 -0
  137. package/skills/cocoar-vue-ui/references/components/page-builder/custom-elements.md +441 -0
  138. package/skills/cocoar-vue-ui/references/components/page-builder/idp-integration.md +122 -0
  139. package/skills/cocoar-vue-ui/references/components/page-builder/index.md +573 -0
  140. package/skills/cocoar-vue-ui/references/components/pagination.md +150 -0
  141. package/skills/cocoar-vue-ui/references/components/panel-layout.md +372 -0
  142. package/skills/cocoar-vue-ui/references/components/password-input.md +175 -0
  143. package/skills/cocoar-vue-ui/references/components/popconfirm.md +186 -0
  144. package/skills/cocoar-vue-ui/references/components/popover.md +173 -0
  145. package/skills/cocoar-vue-ui/references/components/progress-bar.md +178 -0
  146. package/skills/cocoar-vue-ui/references/components/radio-group.md +225 -0
  147. package/skills/cocoar-vue-ui/references/components/script-editor.md +1271 -0
  148. package/skills/cocoar-vue-ui/references/components/segmented-control.md +238 -0
  149. package/skills/cocoar-vue-ui/references/components/select.md +463 -0
  150. package/skills/cocoar-vue-ui/references/components/sidebar.md +421 -0
  151. package/skills/cocoar-vue-ui/references/components/spinner.md +132 -0
  152. package/skills/cocoar-vue-ui/references/components/switch.md +195 -0
  153. package/skills/cocoar-vue-ui/references/components/table.md +170 -0
  154. package/skills/cocoar-vue-ui/references/components/tabs.md +382 -0
  155. package/skills/cocoar-vue-ui/references/components/tag.md +178 -0
  156. package/skills/cocoar-vue-ui/references/components/text-input.md +256 -0
  157. package/skills/cocoar-vue-ui/references/components/toast.md +160 -0
  158. package/skills/cocoar-vue-ui/references/components/tooltip.md +121 -0
  159. package/skills/cocoar-vue-ui/references/components/transitions.md +193 -0
  160. package/skills/cocoar-vue-ui/references/components/tree.md +2388 -0
  161. package/skills/cocoar-vue-ui/references/components/virtual-list.md +212 -0
  162. package/skills/cocoar-vue-ui/references/components/wizard.md +251 -0
  163. package/skills/cocoar-vue-ui/references/components/zoned-date-time-picker.md +177 -0
  164. package/skills/cocoar-vue-ui/references/foundations/colors.md +708 -0
  165. package/skills/cocoar-vue-ui/references/foundations/design-principles.md +115 -0
  166. package/skills/cocoar-vue-ui/references/foundations/icons.md +381 -0
  167. package/skills/cocoar-vue-ui/references/foundations/localization/formatting.md +530 -0
  168. package/skills/cocoar-vue-ui/references/foundations/localization/setup.md +86 -0
  169. package/skills/cocoar-vue-ui/references/foundations/localization/timezones.md +182 -0
  170. package/skills/cocoar-vue-ui/references/foundations/localization/translations.md +305 -0
  171. package/skills/cocoar-vue-ui/references/foundations/motion.md +549 -0
  172. package/skills/cocoar-vue-ui/references/foundations/spacing.md +330 -0
  173. package/skills/cocoar-vue-ui/references/foundations/theming.md +140 -0
  174. package/skills/cocoar-vue-ui/references/foundations/typography.md +206 -0
  175. package/skills/cocoar-vue-ui/references/guide/error-handling.md +162 -0
  176. package/skills/cocoar-vue-ui/references/guide/getting-started.md +119 -0
  177. package/skills/cocoar-vue-ui/references/guide/migration-page-builder-3.md +207 -0
  178. package/skills/cocoar-vue-ui/references/guide/migration.md +140 -0
  179. package/skills/cocoar-vue-ui/references/guide/theming.md +98 -0
@@ -0,0 +1,327 @@
1
+ <!-- Generated from apps/docs/components/file-explorer/asset-store.md by apps/docs/scripts/sync-skill.mjs. Do not edit; edit the docs page. -->
2
+
3
+ # AssetStore&lt;T&gt; contract
4
+
5
+ `AssetStore<T>` is the data-plane interface that decouples the file-explorer UX from any specific backend. Implement these methods over HTTP, IndexedDB, S3, the OPFS, an in-memory `ref` — the composable doesn't care. `createAssetStore(config)` returns a typed store from a flat config struct; this is the recommended factory because it gives future cross-cutting wrappers (request dedup, retry, telemetry) a place to land.
6
+
7
+ ```ts
8
+ import {
9
+ createAssetStore,
10
+ type AssetStore,
11
+ type AssetStoreConfig,
12
+ type Asset,
13
+ } from '@cocoar/vue-file-explorer-core';
14
+ ```
15
+
16
+ ## The shape
17
+
18
+ ```ts
19
+ interface AssetStore<T = unknown> {
20
+ // read
21
+ loadTree(): Promise<Asset<T>[]>;
22
+ loadChildren?(parentId: string): Promise<Asset<T>[]>; // optional — opts into lazy mode
23
+ loadContent?(id: string): Promise<string | Blob>; // optional — omit for browse-only
24
+
25
+ // write
26
+ createFolder(parentId: string | null, name: string): Promise<Asset<T>>;
27
+ createFile?(parentId: string | null, name: string): Promise<Asset<T>>; // optional — composable never calls it
28
+ uploadFile(parentId: string | null, file: File): Promise<Asset<T>>;
29
+ save?(id: string, content: string | Blob): Promise<void>; // optional — omit for browse-only
30
+ rename(id: string, newName: string): Promise<void>;
31
+ delete(id: string): Promise<void>;
32
+ move(id: string, newParentId: string | null, position?: number): Promise<void>;
33
+ }
34
+ ```
35
+
36
+ > **Tip: Browse-only stores**
37
+ >
38
+ > `loadContent`, `save` and `createFile` are **optional**. A consumer that only browses + edits metadata (e.g. an image library) can omit all three: the composable then skips the post-upload `save` (no spurious "saving not supported" error), treats `openFile` as a no-op (no editor tabs), and `saveTab` reports failure cleanly. `createAssetStore` attaches optional methods only when you supply them, so `'save' in store`-style capability probes stay reliable.
39
+
40
+ > **Tip: Throw on failure**
41
+ >
42
+ > The composable funnels every store rejection through `onError(op, err, ctx)` and rolls back the optimistic local mutation before the callback fires. Throw — don't return a tagged error object. The composable interprets a resolved promise as success.
43
+
44
+ ## The asset shape
45
+
46
+ ```ts
47
+ interface Asset<T = unknown> {
48
+ id: string; // stable across mutations — used for tree identity
49
+ name: string;
50
+ kind: 'folder' | 'file';
51
+ parentId: string | null; // null = root; flat structure, NOT nested children[]
52
+ hasChildren?: boolean; // lazy hint — set on folders in loadTree() / loadChildren()
53
+ editor?: FileEditor; // optional explicit override (wins the 3-stage fallback)
54
+ language?: CoarScriptEditorLanguage;
55
+ payload?: T; // your domain data — MIME, size, owner, etc.
56
+ }
57
+ ```
58
+
59
+ The hierarchy is flat with a `parentId` link, **not** nested with embedded `children[]`. That's deliberate: backends can return root + descendants in any order, the composable filters at render time, and mutations (move / delete / rename) operate on a single object instead of walking and re-stitching trees.
60
+
61
+ ## Reading
62
+
63
+ ### `loadTree(): Promise<Asset<T>[]>`
64
+
65
+ **Called once on mount** by `useFileExplorer`. The composable holds the returned array as its internal projection and patches it after each successful CRUD op (`createFolder`, `createFile`, `uploadFile`, `delete`, `rename`, `move`). Call [`api.refresh()`](./use-file-explorer.md#imperative-ops) to re-fetch when upstream state changes out-of-band.
66
+
67
+ Two contracts depending on whether `loadChildren` is present:
68
+
69
+ | `loadChildren` | What `loadTree` returns |
70
+ |---|---|
71
+ | absent | The **full hierarchy** — every asset the user can reach. Eager mode. |
72
+ | present | Just **root-level** entries. Mark folders with `hasChildren: true` if they have children that aren't yet loaded. Lazy mode. |
73
+
74
+ Folders with `hasChildren: false` (explicit) hide the chevron and are non-expandable. Leave it `undefined` to treat every folder as potentially-expandable.
75
+
76
+ ### `loadChildren?(parentId): Promise<Asset<T>[]>`
77
+
78
+ Implement this to opt into lazy loading. Called on first expand of each folder. Subsequent expansions of the same folder don't re-call — the composable caches the loaded set per session.
79
+
80
+ Return one level of children. The composable merges them into the projection so siblings render in the correct sort order.
81
+
82
+ ### `loadContent(id): Promise<string | Blob>`
83
+
84
+ Called when a file is opened in a tab. Return type depends on the editor — picked via the 3-stage fallback `asset.editor → config.getFileMeta(asset) → defaultFileMetaFromName(asset.name)`:
85
+
86
+ | Editor | Expected content |
87
+ |---|---|
88
+ | `'script'` | `string` — Monaco grammar is picked from `language`. |
89
+ | `'markdown'` | `string` — Milkdown WYSIWYG content. |
90
+ | `'pdf'` | `string` (URL) or `Blob` — handed to `pdfSource()`. |
91
+ | `'image'` | `string` (URL) or `Blob` — handed to `imageSource()`. |
92
+
93
+ The composable caches loaded content per tab; reopening a closed file re-calls `loadContent` (caches are tab-scoped, not session-scoped).
94
+
95
+ > **Warning: Placeholder-then-fill**
96
+ >
97
+ > A tab is pushed + activated **immediately** when the user clicks a file; `loadContent` runs concurrently and the editor only mounts when `loadingNodes.has(activeId)` flips false. A rejection rolls the placeholder back so the user isn't stranded on an empty editor for a file that never loaded.
98
+
99
+ ## Writing
100
+
101
+ All write ops are optimistic — the composable mutates local state synchronously, awaits the store, and rolls back on rejection. Each method emits a `savingNodes` entry while in flight so the row icon can swap to a spinner.
102
+
103
+ ### `createFolder(parentId, name)` &middot; `createFile(parentId, name)`
104
+
105
+ Create an empty folder / file under `parentId` (`null` = root). Returns the persisted asset with its stable id assigned. Name collisions run through the conflict pipeline (see below).
106
+
107
+ ### `uploadFile(parentId, file)`
108
+
109
+ Distinct from `createFile` so backends can pick multipart / signed-URL transports. **The store creates the entry only** — the composable owns the byte → string/Blob conversion (it knows the editor and when to use `URL.createObjectURL`) and follows up with `save(id, content)`. Keeps the store framework-agnostic over blob-URL leases.
110
+
111
+ ### `save(id, content)`
112
+
113
+ Persist new file content. Called on `Ctrl+S` (via `saveTab` / `saveActive`) and after `uploadFile`. The composable blocks tab close while a save is in flight (`closeTab` bails early if `savingNodes.has(id)`).
114
+
115
+ ### `rename(id, newName)`
116
+
117
+ Rename in-place. Throws on sibling-name collision — `rename` deliberately **bypasses** the conflict policy because the user just typed a name and silently changing it would be surprising.
118
+
119
+ ### `delete(id)`
120
+
121
+ Recursive — folder deletes cascade. The composable closes any open tab for the deleted asset (or any descendant) and revokes blob URLs it owns for those assets.
122
+
123
+ ### `move(id, newParentId, position?)`
124
+
125
+ Reparent. `position` is the destination index inside the new parent's children; `undefined` means append. `newParentId === null` moves to root.
126
+
127
+ The composable only passes `position` when `sortMode === 'manual'` — in non-manual modes the comparator decides where the moved node lands, so `position` is silently dropped. Implementations should still accept the optional arg for forwards compatibility.
128
+
129
+ ## Conflict policy
130
+
131
+ Create + upload run through a conflict policy on sibling-name collision. The policy is configured on the store (see [`createInMemoryAssetStore`'s `onConflict` knob](./in-memory-store.md#onconflict)) and resolves to one of:
132
+
133
+ ```ts
134
+ type ConflictResolution =
135
+ | { action: 'overwrite' }
136
+ | { action: 'rename'; newName: string }
137
+ | { action: 'cancel' };
138
+ ```
139
+
140
+ Built-in policies:
141
+
142
+ | Policy | Behavior |
143
+ |---|---|
144
+ | `'rename'` (default) | Auto-suffix: `foo.txt` → `foo (2).txt` → `foo (3).txt`. Matches Finder / VSCode. |
145
+ | `'overwrite'` | Delete the existing entry (recursively) and proceed with the requested name. |
146
+ | `'prompt'` | `window.prompt` for an alternative name, default to the auto-suggestion. Cancel → throws. |
147
+ | `'error'` | Never resolves — always throws the conflict error. |
148
+ | `(info) => ConflictResolution \| Promise<ConflictResolution>` | Custom resolver. Async OK. |
149
+
150
+ The function form receives a `ConflictInfo<T>`:
151
+
152
+ ```ts
153
+ interface ConflictInfo<T = unknown> {
154
+ existing: Asset<T>;
155
+ incoming: { name: string; kind: 'folder' | 'file' };
156
+ parentId: string | null;
157
+ suggestedRename: string; // 'foo (2).txt'
158
+ }
159
+ ```
160
+
161
+ > **Info: Policy runs BEFORE latency**
162
+ >
163
+ > Resolution runs synchronously (relative to the store's own simulated latency in the in-memory impl), so a `'prompt'` dialog isn't blocked behind an artificial 1-second wait.
164
+
165
+ ## Error funnel
166
+
167
+ All store rejections funnel through one consumer-supplied callback:
168
+
169
+ ```ts
170
+ type AssetOp =
171
+ | 'loadTree' | 'loadChildren' | 'loadContent'
172
+ | 'createFolder' | 'createFile' | 'uploadFile'
173
+ | 'save' | 'rename' | 'delete' | 'move';
174
+
175
+ interface AssetOpContext {
176
+ id?: string; // affected asset id
177
+ parentId?: string | null; // for create / move
178
+ name?: string; // for create / rename
179
+ file?: File; // for uploadFile
180
+ }
181
+
182
+ onError?: (op: AssetOp, error: unknown, ctx: AssetOpContext) => void;
183
+ ```
184
+
185
+ By the time `onError` fires the composable has already rolled back the optimistic mutation (e.g. a failed `loadContent` removes the placeholder tab, a failed `uploadFile` skips the follow-up `save`). The callback's job is presentation — toast, dialog, inline banner. No return value.
186
+
187
+ ## File-meta resolution
188
+
189
+ The store can supply a `getFileMeta(asset)` override that runs in the middle of a 3-stage fallback chain:
190
+
191
+ ```ts
192
+ getFileMeta?: (asset: Asset<T>) => FileMeta | null;
193
+
194
+ interface FileMeta {
195
+ editor: 'script' | 'markdown' | 'pdf' | 'image';
196
+ language?: CoarScriptEditorLanguage; // only meaningful when editor === 'script'
197
+ }
198
+ ```
199
+
200
+ | Stage | Source | Use when |
201
+ |---|---|---|
202
+ | 1 | `asset.editor` (+ `asset.language`) on the asset itself | The backend knows the editor authoritatively (e.g. a typed asset DB). |
203
+ | 2 | `config.getFileMeta(asset)` | The editor choice depends on something other than the filename — MIME, a `type` field, user preferences. Return `null` to fall through. |
204
+ | 3 | `defaultFileMetaFromName(asset.name)` | Extension heuristic — handles ~40 Monaco grammars + markdown + pdf + common image formats. |
205
+
206
+ `resolveFileMeta(asset, {getFileMeta})` is exported if you need to run the same fallback outside the composable.
207
+
208
+ ## Wiring a real backend
209
+
210
+ ```ts
211
+ import { createAssetStore, type Asset } from '@cocoar/vue-file-explorer-core';
212
+
213
+ interface MyAsset { mimeType: string; size: number }
214
+
215
+ const store = createAssetStore<MyAsset>({
216
+ async loadTree() {
217
+ const res = await fetch('/api/assets');
218
+ return await res.json() as Asset<MyAsset>[];
219
+ },
220
+ async loadContent(id) {
221
+ return await fetch(`/api/assets/${id}/content`).then(r => r.text());
222
+ },
223
+ async createFolder(parentId, name) {
224
+ return await api.post('/api/folders', { parentId, name });
225
+ },
226
+ async createFile(parentId, name) {
227
+ return await api.post('/api/files', { parentId, name });
228
+ },
229
+ async uploadFile(parentId, file) {
230
+ const body = new FormData();
231
+ body.set('parentId', parentId ?? '');
232
+ body.set('file', file);
233
+ return await api.post('/api/files/upload', body);
234
+ },
235
+ async save(id, content) { await api.put(`/api/files/${id}`, content); },
236
+ async rename(id, newName) { await api.patch(`/api/assets/${id}`, { name: newName }); },
237
+ async delete(id) { await api.delete(`/api/assets/${id}`); },
238
+ async move(id, parentId, position) {
239
+ await api.patch(`/api/assets/${id}`, { parentId, position });
240
+ },
241
+
242
+ // optional — opt into lazy loading
243
+ async loadChildren(parentId) {
244
+ return await fetch(`/api/assets?parent=${parentId}`).then(r => r.json());
245
+ },
246
+
247
+ onError: (op, err, ctx) => toast.error(`${op} failed: ${(err as Error).message}`),
248
+
249
+ getFileMeta: (asset) =>
250
+ asset.payload?.mimeType === 'application/pdf'
251
+ ? { editor: 'pdf' }
252
+ : null,
253
+ });
254
+ ```
255
+
256
+ That's the entire seam. Hand `store` to `useFileExplorer({store})` and the UX is identical to the in-memory demo — lazy / sort / conflict behaviors are all driven from the composable + store config.
257
+
258
+ ## Reacting to out-of-band updates
259
+
260
+ For backends with server push (WebSocket / SignalR / SSE) or multi-tab consistency requirements, you have two patterns. Pick whichever fits your store's data flow.
261
+
262
+ ### Pattern A — call `api.refresh()` on the signal
263
+
264
+ The pragmatic default. Subscribe to your push channel in the consumer; call `api.refresh()` to re-pull the tree (or `api.refresh(folderId)` for one branch in lazy mode). Open tabs + selection + expansion state survive — only the underlying asset list re-loads.
265
+
266
+ ```ts
267
+ import { onMounted, onUnmounted } from 'vue';
268
+ import { useFileExplorer } from '@cocoar/vue-file-explorer-core';
269
+
270
+ const fe = useFileExplorer({ store: myHttpStore });
271
+
272
+ let unsub: (() => void) | undefined;
273
+ onMounted(() => {
274
+ unsub = signalrClient.on('assets-changed', (msg) => {
275
+ if (msg.folderId) void fe.refresh(msg.folderId);
276
+ else void fe.refresh();
277
+ });
278
+ });
279
+ onUnmounted(() => unsub?.());
280
+ ```
281
+
282
+ ### Pattern B — surface a reactive `_assets` ref directly
283
+
284
+ When your store is already backed by a reactive source (a Pinia store fed by SignalR, an `IndexedDB` query reactive ref, a derived `computed`), expose it as `_assets: Ref<Asset<T>[]>` on the store object. The composable picks it up and skips its internal projection entirely — every upstream mutation reflects in the tree on the next tick, no `refresh()` call required.
285
+
286
+ ```ts
287
+ import { computed, type Ref } from 'vue';
288
+ import {
289
+ type Asset,
290
+ type AssetStore,
291
+ useFileExplorer,
292
+ } from '@cocoar/vue-file-explorer-core';
293
+ import { useKnowledgeAssetStore } from '@/composables/useKnowledgeAssetStore';
294
+
295
+ interface MyAsset { mimeType: string }
296
+
297
+ function createReactiveHttpStore(): AssetStore<MyAsset> & { _assets: Ref<Asset<MyAsset>[]> } {
298
+ const pinia = useKnowledgeAssetStore();
299
+
300
+ return {
301
+ // Reactive escape hatch — composable reads from here, no `loadTree()` ever.
302
+ _assets: computed(() => pinia.assets) as unknown as Ref<Asset<MyAsset>[]>,
303
+
304
+ // `loadTree` is still required by the type; it can be a no-op or
305
+ // trigger an initial pinia.refresh() if the consumer hasn't already.
306
+ async loadTree() {
307
+ await pinia.refreshIfStale();
308
+ return pinia.assets;
309
+ },
310
+
311
+ async loadContent(id) { return await pinia.loadContent(id); },
312
+ async createFolder(parentId, name) { return await pinia.createFolder(parentId, name); },
313
+ async createFile(parentId, name) { return await pinia.createFile(parentId, name); },
314
+ async uploadFile(parentId, file) { return await pinia.uploadFile(parentId, file); },
315
+ async save(id, content) { await pinia.save(id, content); },
316
+ async rename(id, newName) { await pinia.rename(id, newName); },
317
+ async delete(id) { await pinia.delete(id); },
318
+ async move(id, parentId, position) { await pinia.move(id, parentId, position); },
319
+ };
320
+ }
321
+
322
+ const fe = useFileExplorer({ store: createReactiveHttpStore() });
323
+ ```
324
+
325
+ > **Info: When to pick which pattern**
326
+ >
327
+ > **A** is the right default — works with any `AssetStore<T>`, no extra machinery. Reach for **B** when you're already pushing reactivity through the layers (Pinia / live-query stores) and want zero round-trips between push and tree update. `_assets` is also how [`createInMemoryAssetStore`](./in-memory-store.md) opts into instant reactivity for its mutations — same mechanism, different motivation.
@@ -0,0 +1,176 @@
1
+ <!-- Generated from apps/docs/components/file-explorer/in-memory-store.md by apps/docs/scripts/sync-skill.mjs. Do not edit; edit the docs page. -->
2
+
3
+ # In-memory store
4
+
5
+ `createInMemoryAssetStore()` is the reference [`AssetStore<T>`](./asset-store.md) implementation. It's a browser-only, fully-reactive backend backed by a `ref<Asset<T>[]>` plus an `id → content` map. Reach for it for:
6
+
7
+ - demos + documentation pages
8
+ - unit / E2E tests that need a deterministic backend
9
+ - prototyping a consumer shell before the real HTTP backend exists
10
+ - **exercising your UX under degraded conditions** — the latency / failure / lazy / conflict knobs are reactive, so a toolbar can dial them at runtime
11
+
12
+ ```ts
13
+ import { createInMemoryAssetStore } from '@cocoar/vue-file-explorer-core';
14
+ ```
15
+
16
+ ## Quick start
17
+
18
+ ```ts
19
+ const store = createInMemoryAssetStore({
20
+ initialTree: [
21
+ { id: 's', name: 'src', kind: 'folder', parentId: null },
22
+ { id: 'u', name: 'utils.ts', kind: 'file', parentId: 's' },
23
+ ],
24
+ initialContent: { u: 'export const clamp = ...' },
25
+ });
26
+ ```
27
+
28
+ That's all you need for a working in-memory file system. Hand `store` to [`useFileExplorer({store})`](./use-file-explorer.md) and the explorer is fully functional — every CRUD op mutates the underlying ref, content is read out of the map.
29
+
30
+ ## Options
31
+
32
+ ```ts
33
+ interface InMemoryAssetStoreOptions<T = unknown> {
34
+ initialTree?: readonly Asset<T>[];
35
+ initialContent?: Readonly<Record<string, string | Blob>>;
36
+ latencyMs?: MaybeRefOrGetter<number>;
37
+ failureRate?: MaybeRefOrGetter<number>;
38
+ idFactory?: () => string;
39
+ onConflict?: MaybeRefOrGetter<ConflictPolicy<T>>;
40
+ lazy?: boolean;
41
+ }
42
+ ```
43
+
44
+ ### `initialTree` &middot; `initialContent`
45
+
46
+ Seed data. `initialTree` is a flat list of `Asset<T>` with `parentId` links; `initialContent` is an `{ [id]: string | Blob }` map keyed by asset id. Both default empty.
47
+
48
+ ### `latencyMs`
49
+
50
+ Artificial delay (ms) added to every operation. **Reactive** — pass a `Ref<number>` or getter and the simulator picks up changes per call:
51
+
52
+ ```ts
53
+ const latency = ref(0);
54
+ const store = createInMemoryAssetStore({ latencyMs: latency });
55
+ // later: latency.value = 1000 → every subsequent op waits 1s
56
+ ```
57
+
58
+ Use it to make loading states visible during development. `loadTree` latency exposes the global `loading` ref; `loadContent` latency exposes the per-tab placeholder + spinner overlay; `loadChildren` latency exposes the row-icon spinner on lazy expand.
59
+
60
+ Default `0`.
61
+
62
+ ### `failureRate`
63
+
64
+ Probability (0..1) that any operation rejects with `[InMemoryAssetStore] simulated failure in {op}`. Reactive, like `latencyMs`. Use it to exercise the `onError` path — toast styling, rollback correctness, retry UX.
65
+
66
+ Default `0`.
67
+
68
+ > **Tip: Combine latency + failure**
69
+ >
70
+ > The most useful simulator config is `latencyMs: 800, failureRate: 0.15`. Long enough to see every spinner; failures often enough to verify rollback without making the demo unusable.
71
+
72
+ ### `idFactory`
73
+
74
+ Override the id generator. Default `crypto.randomUUID()`. Pass a counter for stable ids in tests:
75
+
76
+ ```ts
77
+ let n = 0;
78
+ const store = createInMemoryAssetStore({ idFactory: () => `asset-${++n}` });
79
+ ```
80
+
81
+ ### `onConflict`
82
+
83
+ Conflict policy applied to `createFolder` / `createFile` / `uploadFile` on sibling-name collision. **Reactive** — flip live from a toolbar.
84
+
85
+ ```ts
86
+ type ConflictPolicy<T = unknown> =
87
+ | 'rename' // default — auto-suffix
88
+ | 'overwrite' // delete existing, then create
89
+ | 'prompt' // window.prompt for a new name
90
+ | 'error' // always throw
91
+ | ((info: ConflictInfo<T>) => ConflictResolution | Promise<ConflictResolution>);
92
+ ```
93
+
94
+ `move` and `rename` deliberately bypass the policy — those are explicit user intent. See [conflict policy](./asset-store.md#conflict-policy) for the full semantics.
95
+
96
+ Default `'rename'`.
97
+
98
+ ### `lazy`
99
+
100
+ Opt into lazy mode. When `true`:
101
+
102
+ - `loadTree()` returns **only root-level** entries (still enriched with `hasChildren: boolean`).
103
+ - `loadChildren(parentId)` is exposed on the store. The composable detects the method and switches to lazy behavior automatically.
104
+ - The store's published-asset projection is gated by a `_publishedIds` Set — only loaded subtrees are visible to the composable. The complete dataset still lives in the store's internal bookkeeping for move / delete / cycle-guard.
105
+
106
+ Default `false`.
107
+
108
+ ```ts
109
+ const store = createInMemoryAssetStore({
110
+ initialTree: bigSeed,
111
+ lazy: true,
112
+ latencyMs: 600, // make the lazy spinners visible
113
+ });
114
+ ```
115
+
116
+ ## Inspection escape hatches
117
+
118
+ `createInMemoryAssetStore` returns an `InMemoryAssetStore<T>` (extends `AssetStore<T>`) with two extra read-only properties for tests and devtools:
119
+
120
+ ```ts
121
+ interface InMemoryAssetStore<T = unknown> extends AssetStore<T> {
122
+ readonly _assets: Ref<Asset<T>[]> | ComputedRef<Asset<T>[]>;
123
+ readonly _contents: Map<string, string | Blob>;
124
+ }
125
+ ```
126
+
127
+ | Property | Notes |
128
+ |---|---|
129
+ | `_assets` | Reactive projection. Eager: equals the full data set. Lazy: equals only the published subset. The composable reads from this for its tree projection. |
130
+ | `_contents` | Raw `id → content` map. Mutate at your own risk — bypasses the async API and ignores subscribers. Useful for test setup / snapshots. |
131
+
132
+ The underscore prefix marks them as non-portable. A real backend's store won't have these — write your tests against `useFileExplorer`'s public refs (`assets`, `openTabs`, etc.) if you want them to transfer to production code.
133
+
134
+ ## Runtime simulator pattern
135
+
136
+ The four reactive knobs (`latencyMs`, `failureRate`, `onConflict`, plus `sortMode` on the composable) are designed to be wired to UI controls. The pattern looks like this:
137
+
138
+ ```vue
139
+ <script setup lang="ts">
140
+ import { ref } from 'vue';
141
+ import { CoarSelect, CoarSegmentedControl } from '@cocoar/vue-ui';
142
+ import {
143
+ createInMemoryAssetStore,
144
+ useFileExplorer,
145
+ type ConflictPolicy,
146
+ type SortMode,
147
+ } from '@cocoar/vue-file-explorer-core';
148
+
149
+ const latency = ref<number>(0);
150
+ const failure = ref<number>(0);
151
+ const conflict = ref<ConflictPolicy>('rename');
152
+ const sortMode = ref<SortMode>('folders-first');
153
+
154
+ const store = createInMemoryAssetStore({
155
+ initialTree,
156
+ initialContent,
157
+ latencyMs: latency,
158
+ failureRate: failure,
159
+ onConflict: conflict,
160
+ });
161
+
162
+ const fe = useFileExplorer({ store, sortMode });
163
+ </script>
164
+ ```
165
+
166
+ Every knob flips live — no store recreation, no state loss. The playground POC's simulator panel persists each setting to `localStorage` so a refresh keeps the configured scenario; `lazy` is the only construction-time switch (changing it requires recreating the store, so the POC persists the flag and reloads the page).
167
+
168
+ ## Why ship a reference impl
169
+
170
+ The composable's contract is non-trivial — placeholder-then-fill open, optimistic rollback, conflict pipeline, blob-URL leases, lazy capability probing. Shipping a fully-working in-memory implementation makes the contract concrete:
171
+
172
+ - documentation pages get **real, interactive demos** without a backend
173
+ - consumers can study `create-asset-store.ts` to see exactly what their HTTP store needs to do
174
+ - tests run against the same code paths the demo does — no mock divergence
175
+
176
+ When the in-memory store and a real HTTP backend behave identically from the composable's perspective, you know the `AssetStore<T>` seam is doing its job.