@hachej/boring-ui-kit 0.3.2 → 0.4.0

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 (55) hide show
  1. package/README.md +46 -18
  2. package/THIRD_PARTY_NOTICES.md +35 -0
  3. package/dist/canvas-editor-native.d.ts +4 -1
  4. package/dist/canvas-editor-native.js +8 -6
  5. package/dist/canvas-editor.js +1 -1
  6. package/dist/canvas-proposals.d.ts +3 -1
  7. package/dist/canvas-proposals.js +4 -3
  8. package/dist/experience-document-viewer.d.ts +3 -0
  9. package/dist/experience-document-viewer.js +6 -5
  10. package/dist/experience.d.ts +3 -0
  11. package/dist/experience.js +2 -1
  12. package/dist/file-tree-view.d.ts +76 -3
  13. package/dist/file-tree-view.js +172 -39
  14. package/dist/file-tree.d.ts +3 -0
  15. package/dist/file-tree.js +17 -2
  16. package/dist/html-viewer.d.ts +4 -0
  17. package/dist/html-viewer.js +5 -3
  18. package/dist/markdown-autosave.d.ts +13 -0
  19. package/dist/markdown-autosave.js +94 -0
  20. package/dist/markdown-editor.d.ts +16 -0
  21. package/dist/markdown-editor.js +81 -17
  22. package/dist/markdown-extensions.js +9 -1
  23. package/dist/markdown-proposal-diff.d.ts +2 -1
  24. package/dist/markdown-proposal-diff.js +3 -2
  25. package/dist/markdown-safety.js +3 -1
  26. package/dist/native-chat.js +1 -1
  27. package/dist/remote-chat.js +5 -1
  28. package/dist/text-buffer.js +3 -2
  29. package/dist/text-draft-controls.d.ts +3 -1
  30. package/dist/text-draft-controls.js +3 -2
  31. package/dist/viewer-components.d.ts +14 -0
  32. package/dist/viewer-components.js +1 -0
  33. package/package.json +4 -4
  34. package/r/app-shell.json +39 -20
  35. package/r/button.json +19 -11
  36. package/r/dock.json +2 -2
  37. package/r/documents.json +17 -14
  38. package/r/feedback-preview.json +8 -2
  39. package/r/feedback.json +46 -325
  40. package/r/file-tree.json +14 -6
  41. package/r/html-viewer.json +8 -42
  42. package/r/markdown-editor.json +38 -87
  43. package/r/pi-ambient.json +15 -8
  44. package/r/pi-app.json +29 -18
  45. package/r/pi-chat.json +88 -69
  46. package/r/pi-workspace.json +7 -5
  47. package/r/provider-setup.json +9 -4
  48. package/r/registry.json +460 -888
  49. package/r/task-list-viewer.json +11 -21
  50. package/r/theme.json +23 -4
  51. package/r/version-history.json +24 -0
  52. package/r/viewers.json +139 -320
  53. package/styles/file-tree.css +41 -42
  54. package/dist/file-tree-shadcn.d.ts +0 -8
  55. package/dist/file-tree-shadcn.js +0 -14
package/README.md CHANGED
@@ -36,11 +36,17 @@ import { MarkdownEditor } from '@hachej/boring-ui-kit/markdown-editor';
36
36
 
37
37
  Install the pinned React, React DOM, Marked and Tiptap peers listed in this package's manifest when selecting this entry. They remain optional for headless consumers. The renderer borrows its controller; unmounting does not dispose it or close its resource client. The host owns selection, identity and teardown.
38
38
 
39
- Rich formatting and Markdown source controls share controller state. Mounting, switching modes and projecting remote text never serialize the original buffer. Genuine rich edits use Tiptap's Markdown serialization and may normalize supported formatting. Save and Ctrl/Cmd+S capture the exact controller selection. Proposal review shows added/removed lines with two lines of context and expandable unchanged runs. It preserves exact line endings and marks a missing final newline. An inner details panel retains exact before/after text. Accept and save delegates to the controller; reviewing or expanding context never publishes. Conflict and unknown outcomes expose explicit discard or reconciliation controls.
39
+ Formatting defaults to the pinned desktop toolbar. The Aa header control toggles it; `formattingToolbar="selection"` selects the native Tiptap selection bubble instead. Phones always use the selection bubble. Keyboard shortcuts and Markdown input rules remain available. Rich formatting and Markdown source controls share controller state. Mounting, switching modes and projecting remote text never serialize the original buffer. Genuine rich edits use Tiptap's Markdown serialization and may normalize supported formatting.
40
+
41
+ Autosave is enabled by default after 1200ms of idle editing and flushes on blur and page hide through the existing conditional `controller.flush`. Untouched new buffers are not automatically published. `autosave={false}` restores explicit Save; `autosave={{ delayMs: 500 }}` changes the idle delay. Ctrl/Cmd+S always saves the exact selected buffer explicitly. Refused, conflicting and unconfirmed saves retain the draft and pause automatic retries until resolved.
42
+
43
+ Use `onAutosaveChange` to register the mounted flush barrier. Await that flush before closing, unmounting or disposing the controller. If it returns false, keep the editor mounted with its refusal/conflict UI unless a real host navigation guard obtains deliberate discard consent. Internal flush registration is not a discard guard. Unmount cleanup and browser page hide are best effort, not substitutes for awaiting the owner teardown barrier. See the [registry recipes](../../registry/README.md) for `MarkdownPane`, viewer-window policy and navigation integration.
44
+
45
+ Proposal review shows added/removed lines with two lines of context and expandable unchanged runs. It preserves exact line endings and marks a missing final newline. An inner details panel retains exact before/after text. Accept and save delegates to the controller; reviewing or expanding context never publishes. Conflict and unknown outcomes expose explicit discard or reconciliation controls.
40
46
 
41
47
  Detailed alignment uses a matrix of at most 250,000 cells after trimming equal prefix/suffix lines. Larger comparisons show all removed/added middle lines with an explicit notice. This bounds the comparison matrix, not total document size, string-comparison cost or rendered rows.
42
48
 
43
- The rich view includes a Document outline built from the native parsed headings. Navigation changes the native selection without editing or saving Markdown. Rich/source switches retain each mode's selection and direction while the buffer version is unchanged. Text projected from outside (load, refresh, discard, source edits) puts the rich selection at the document start, where the browser leaves its caret, so a click or formatting command never acts on a stale selection at the end. External replacement or source edits invalidate older bookmarks; changing controllers starts a new local session. Human heading controls use the mounted presentation command described below.
49
+ The rich view includes a Document outline built from the native parsed headings when `showOutline` is true. By default, the outline follows the built-in `header` visibility. Navigation changes the native selection without editing or saving Markdown. Rich/source switches retain each mode's selection and direction while the buffer version is unchanged. Text projected from outside (load, refresh, discard, source edits) puts the rich selection at the document start, where the browser leaves its caret, so a click or formatting command never acts on a stale selection at the end. External replacement or source edits invalidate older bookmarks; changing controllers starts a new local session. Human heading controls use the mounted presentation command described below.
44
50
 
45
51
  ### Mounted Markdown commands
46
52
 
@@ -78,13 +84,13 @@ A submit the host refused before admission (status 402 `submission-refused`, for
78
84
 
79
85
  ### Native conversation history
80
86
 
81
- `loadEarlier()` reads one native page through `Conversation.entries`. `snapshot.history` distinguishes disabled, idle, loading, ready and error states. Each page contains at most 40 native entries in display order. The controller retains one page and an opaque native continuation cursor. The first request captures the greatest observed active entry ID as an inclusive upper bound; later writes do not move that bound. Parent-owned fork entries remain valid native history.
87
+ `loadEarlier()` reads one native page through `Conversation.entries`. `snapshot.history` distinguishes disabled, idle, loading, ready and error states. Each request contains at most 40 native entries. The controller accumulates pages in display order for the person's current browsing session and retains an opaque native continuation cursor. It clears that cache on history invalidation or `clearHistory()`; loading never restores records to model context. The first request captures the greatest observed active entry ID as an inclusive upper bound; later writes do not move that bound. Parent-owned fork entries remain valid native history.
82
88
 
83
89
  A failed read retains the previous page and continuation for retry. `clearHistory()`, head or non-append active-range changes, reconnect, watch closure and disposal invalidate pending responses. Clearing a page does not cancel native work or promise remote read termination. The controller preserves the exact host Context. Page-count limits do not bound message bytes or native storage allocation.
84
90
 
85
91
  Without a custom `source`, history defaults to the borrowed conversation. Set `history: false` to disable it. With a custom source, history is disabled unless the host supplies an explicit `{ id, entries }` native capability for that conversation. The ID must match before and after a read. Hosts remain responsible for current authorization and must dispose or disconnect presentation when its scope is revoked. Expert mode is not redaction.
86
92
 
87
- `test/packages/ui-native-chat-history.test.mjs` uses real native reset/head/fork records. `node examples/chat-history.mjs` demonstrates three native pages across a reset without model calls.
93
+ `test/packages/ui-native-chat-history.test.mjs` uses real native reset/head/fork records. `node fixtures/chat-history.mjs` demonstrates three native pages across a reset without model calls.
88
94
 
89
95
  ## Headless canvas document edits
90
96
 
@@ -152,6 +158,8 @@ Mounted `tools.propose` accepts the same complete edits as the headless command
152
158
 
153
159
  ## Optional HTML source and preview
154
160
 
161
+ Both `MarkdownEditor` and `HtmlViewer` accept optional `components` bindings for host buttons, inputs, textareas and the selection bubble. Installed registry recipes supply the consumer's unchanged shadcn components. Bindings forward refs and native input events. Omitting them retains unstyled native HTML for direct package consumers; it is not installed-block design qualification. See the [registry theming contract](../../registry/README.md#styling).
162
+
155
163
  Import `createHtmlController` and the concrete `HtmlController` from `@hachej/boring-ui-kit/html`. Import `HtmlViewer` from `@hachej/boring-ui-kit/html-viewer` only when selecting the React renderer. These entries need no agent, native environment, Tiptap or canvas runtime. The headless controller accepts the same resource identity/client and new or saved source shape as the Markdown controller. Saved resources must be `text/html` with valid UTF-8.
156
164
 
157
165
  `actions.edit`, `actions.selection` and `flush(selection)` preserve exact source bytes through the existing conditional publication contract. Refresh preserves dirty text, discard is explicit, and reconciliation handles lost acknowledgements without another write. Late saves retain later edits. The concrete controller has no mounted HTML tools; the native document tools remain Markdown-specific. Unmount borrows the controller and never saves or disposes it.
@@ -160,13 +168,13 @@ The renderer offers HTML source and preview modes with Save, Refresh, Reconcile
160
168
 
161
169
  Preview refuses source beyond 262,144 UTF-16 units, traversal beyond 20,000 visited nodes or depth 128, and output beyond 1,048,576 units. Traversal/output limits do not bound peak allocation inside the browser parser. A parser or Trusted Types refusal leaves source viewing and saving available. No permissive Trusted Types policy or raw preview fallback is created. Server rendering does not parse or embed generated markup.
162
170
 
163
- Run `node examples/html-document.mjs` after building for a fictional authenticated resource save. `npm run test:html-consumer` installs isolated tarballs, checks strict concrete declarations, repeats controller/DOM tests and bundles both entries without native/server or heavy viewer implementations. `npm run test:html-registry-consumer` uses the actual shadcn CLI and drives the copied source recipe before and after restyling. These DOM checks do not qualify native parser inertness, iframe/CSP/Trusted Types security, browser input or network isolation. The attempted Chromium journey currently fails at startup under sandbox EPERM. Browser qualification remains open.
171
+ Run `node fixtures/html-document.mjs` after building for a fictional authenticated resource save. `npm run test:html-consumer` installs isolated tarballs, checks strict concrete declarations, repeats controller/DOM tests and bundles both entries without native/server or heavy viewer implementations. `npm run test:html-registry-consumer` uses the actual shadcn CLI and drives the copied source recipe before and after restyling. These DOM checks do not qualify native parser inertness, iframe/CSP/Trusted Types security, browser input or network isolation. The attempted Chromium journey currently fails at startup under sandbox EPERM. Browser qualification remains open.
164
172
 
165
173
  ## Optional fixed and derived experiences
166
174
 
167
175
  Import `validateExperience` from `@hachej/boring-ui-kit/experience/compose` and `Experience` from `@hachej/boring-ui-kit/experience`. Install exact optional peers `@json-render/core@0.21.0`, `@json-render/react@0.21.0`, `zod@4.6.5` and React 19.3.0. The validation entry loads no React runtime, files, Pi or agent. The renderer uses the native json-render catalog, registry, providers and renderer.
168
176
 
169
- Descriptors use format `boring.experience`, version 1 and source `fixed`, `derived` or `generated`. Generated output comes through the optional composer below. The current installed layout kinds are `boring/stack`, `boring/row`, `boring/grid`, `boring/cell` and `boring/generated`, all version 1. Stack/row accept gap `small`, `medium` or `large`, defaulting to medium. Grid adds integer columns from 1 through 4. Cell props contain only a `ref` in `app/cell` form. Every used cell kind/version must appear in the descriptor's `kinds` map. See `examples/fixed-experience.mjs` for a complete descriptor.
177
+ Descriptors use format `boring.experience`, version 1 and source `fixed`, `derived` or `generated`. Generated output comes through the optional composer below. The current installed layout kinds are `boring/stack`, `boring/row`, `boring/grid`, `boring/cell` and `boring/generated`, all version 1. Stack/row accept gap `small`, `medium` or `large`, defaulting to medium. Grid adds integer columns from 1 through 4. Cell props contain only a `ref` in `app/cell` form. Every used cell kind/version must appear in the descriptor's `kinds` map. See `fixtures/fixed-experience.mjs` for a complete descriptor.
170
178
 
171
179
  The host supplies `cells` with `ref`, `kind`, `version`, optional `maxUses` and a stable `render` React component. That component closes over its concrete controller and owning application's operations. Descriptor props and action parameters never reach it. Default maxUses is 1. Registration accepts only one version per kind and one cell per ref. `canView(ref)` is a synchronous host policy callback and must return literal true. The host rerenders Experience when its authorization or cell registrations change. The library checks access again in the cell renderer; unavailable cells show a placeholder. Each host action must still authorize when invoked.
172
180
 
@@ -191,7 +199,7 @@ The renderer exposes Use proposed layout, Dismiss proposed layout, Keep this lay
191
199
 
192
200
  Cell visibility validation is a current host preflight. A provider that promises atomic authorization of layout publication and its referenced resources must enforce both at its commit boundary. The renderer does not create that cross-resource guarantee. Live model quality, actual browser focus preservation and full A28-A29 remain unqualified. The bounded region/Pin implementation is described below.
193
201
 
194
- Run `node examples/experience-keep.mjs` after building for a fictional offer, adoption, conditional creation and receipt lookup. `npm run test:experience-consumer` additionally checks the concrete Keep declarations, real SQLite/controller and React controls, and the resource-enabled browser bundle from isolated installed archives. These are DOM and artifact checks, not a browser journey.
202
+ Run `node fixtures/experience-keep.mjs` after building for a fictional offer, adoption, conditional creation and receipt lookup. `npm run test:experience-consumer` additionally checks the concrete Keep declarations, real SQLite/controller and React controls, and the resource-enabled browser bundle from isolated installed archives. These are DOM and artifact checks, not a browser journey.
195
203
 
196
204
 
197
205
  ## Optional metadata-only composition
@@ -208,7 +216,7 @@ Iteration captures definitions, candidates, cells and fallback before yielding t
208
216
 
209
217
  The required `signal` belongs to the host. Abort it when the host closes composition. Upstream cancellation releases a pending composition even when an evaluator does not cooperate; it does not prove provider termination. Calling the iterator's `return()` alone cannot interrupt its pending `next()`. On a final result, pass the descriptor to the existing document controller's `propose` with the selection captured before composition. A stale base refuses. Explicit adoption and Keep remain separate human actions.
210
218
 
211
- Run `node examples/generated-experience.mjs` after building for deterministic native composition followed by a real conditional SQLite Keep. The isolated experience consumer checks native evaluator declarations, metadata/cancellation behavior, rendered proposal controls and browser bundles from installed archives. Tests also exercise upstream `experimental_createEvaluator` with fictional fetch responses and host budget denial before each request. Those tests establish transport composition without a live Gateway/local model call, model-quality claim, actual browser journey or complete A27/A31 qualification. Full reference application recipes remain required. Region composition and Pin use the bounded API below.
219
+ Run `node fixtures/generated-experience.mjs` after building for deterministic native composition followed by a real conditional SQLite Keep. The isolated experience consumer checks native evaluator declarations, metadata/cancellation behavior, rendered proposal controls and browser bundles from installed archives. Tests also exercise upstream `experimental_createEvaluator` with fictional fetch responses and host budget denial before each request. Those tests establish transport composition without a live Gateway/local model call, model-quality claim, actual browser journey or complete A27/A31 qualification. Full reference application recipes remain required. Region composition and Pin use the bounded API below.
212
220
 
213
221
 
214
222
  ## Generated regions and conditional Pin
@@ -225,7 +233,7 @@ Call `controller.actions.beginRegion(selection, region, trigger)` before host co
225
233
 
226
234
  `ExperienceDocument` accepts `onRegenerate(request)` for its **Regenerate** controls. The host owns the evaluator and its abort signal. **Use proposed region**, **Dismiss proposed region** and **Pin this region** separate acceptance and publication. Fixed sibling renderer identities remain stable across region edits. Cell and grid minima inherit the strongest enclosing region minimum; narrow regions can scroll. Borrowed resource controllers survive unmount.
227
235
 
228
- Run `node examples/generated-region.mjs` after building for a fictional native composition, offer, adoption and SQLite Pin receipt. Public tests and `npm run test:experience-consumer` exercise strict tarball declarations, real native composition, exact source, request races, DOM identity/selection and unknown-save reconciliation. DOM results do not qualify browser focus, geometry, live model quality or atomic cross-resource authorization.
236
+ Run `node fixtures/generated-region.mjs` after building for a fictional native composition, offer, adoption and SQLite Pin receipt. Public tests and `npm run test:experience-consumer` exercise strict tarball declarations, real native composition, exact source, request races, DOM identity/selection and unknown-save reconciliation. DOM results do not qualify browser focus, geometry, live model quality or atomic cross-resource authorization.
229
237
 
230
238
  ## Consumer-defined text viewers
231
239
 
@@ -276,8 +284,8 @@ Bind the authenticated [revision provider](../files/README.md#frontend-revision-
276
284
  to the tree:
277
285
 
278
286
  ```tsx
279
- import '@hachej/boring-ui-kit/file-tree.css';
280
- import { FileTree } from '@hachej/boring-ui-kit/file-tree-view';
287
+ // Installed file-tree recipe, with the consumer's real shadcn Base UI controls.
288
+ import { FileTree } from '@/components/file-tree/file-tree';
281
289
 
282
290
  <FileTree
283
291
  revisionProvider={revisionProvider}
@@ -290,20 +298,40 @@ The renderer adapts the official shadcn `sidebar-11` tree. Its shipped CSS works
290
298
  without a SidebarProvider or scanning package sources with Tailwind. Attribution
291
299
  and the pinned upstream revision are in [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
292
300
 
301
+ The recipe supplies the consumer's actual shadcn Button and dropdown-menu, plus
302
+ shared version-history rows. Direct callers of `@hachej/boring-ui-kit/file-tree-view`
303
+ pass a required `components` binding with `Button`, `Actions` and `Versions`;
304
+ `FileTreeComponents`, `FileTreeButtonProps`, `FileTreeActionsProps` and
305
+ `FileTreeVersionsProps` describe it. There is no package-owned control fallback.
306
+ The installed wrappers supply this binding automatically. Library history and viewer
307
+ menus share relative time, provenance and Current presentation. Selecting a retained
308
+ version still reads an exact, read-only preview without retargeting an editor.
309
+
310
+ The default `list` variant has a Library header, visible-item count, inset search,
311
+ 48px rows with kind and size, and a file actions menu. The count describes loaded,
312
+ visible items, including folders; it is not a workspace total. A folder shows its
313
+ child count only after its provider listing is complete; unloaded, loading, failed
314
+ or paginated listings show no count. The menu offers
315
+ Open, Version history and Download through the same provider. Rename and Delete
316
+ are absent because `RevisionProvider` exposes neither operation. No edit date or
317
+ author is invented or fetched per row. `variant="plain"` keeps the compact tree.
318
+ `title` overrides the list heading; `onClose` adds a host-owned close button.
319
+
293
320
  The tree supports folder expansion, keyboard navigation, filename search,
294
- pagination, uploads and read-only revision previews. Uploads go to `uploads/`
321
+ pagination, picker or drop uploads and read-only revision previews. Uploads go to `uploads/`
295
322
  by default (`uploadDirectory` overrides it). Duplicate names refuse publication.
296
- Unknown upload results show **Check upload status** and retain the operation ID;
297
- checking never republishes the bytes. Keep the local file until the result is
298
- known. Upload state is in memory and is not a durable cross-reload upload queue.
323
+ Uploads show progress inline on their row. Successful uploads leave no visible
324
+ status log; a visually hidden polite live region announces results. Failures keep
325
+ an inline error and **Retry**. A refused upload retries its captured bytes with the
326
+ same revision condition; an unconfirmed upload only checks its original operation
327
+ and never republishes. Keep the local file until the result is known. Upload state is in memory and is not a durable cross-reload upload queue.
299
328
 
300
329
  Both components take `locale` (`en` by default, or `fr`; the words are exported as
301
330
  `defaultFileTreeLabels` and `frenchFileTreeLabels`) and `labels` to override any word,
302
331
  including the accessible names, or to add another language, `upload={false}` and
303
- `history={false}` to hide those buttons where a host has no uploads or retained
304
- history, and `refreshKey`: when its value changes the open folders and the current
332
+ `history={false}` to hide upload controls or the version-history menu item, and `refreshKey`: when its value changes the open folders and the current
305
333
  search reload, for example after an agent saves a file. A search result shows its
306
- name with its folder beside it. `AgentWorkspace` passes the same options to its
334
+ name with its folder in the metadata line in `list`, or on the same line in `plain`. `AgentWorkspace` passes the same options to its
307
335
  Library through `fileTree`, with its own `locale`.
308
336
 
309
337
  For custom layouts, import `createFileTreeController` from
@@ -50,6 +50,41 @@ SOFTWARE.
50
50
 
51
51
  ```
52
52
 
53
+ ## Workspace demo: Tremor Planner
54
+
55
+ The workspace demo adapts the sidebar, breadcrumbs, flat metric summaries,
56
+ filter bars, tables, status badges and budget progress layouts from
57
+ [tremorlabs/template-planner](https://github.com/tremorlabs/template-planner/tree/7b5d1f10e2705c9a2dca108c41910793185cdbb8),
58
+ pinned at `7b5d1f10e2705c9a2dca108c41910793185cdbb8`. The source's `LICENSE.md`
59
+ contains the MIT licence below. Its README still links a commercial Tremor licence.
60
+ This adaptation uses the explicit repository licence and copies no logos, fonts,
61
+ customer data, images or runtime dependencies. The demo uses the existing
62
+ Base UI shadcn components and fictional initials.
63
+
64
+ ```text
65
+ The MIT License (MIT)
66
+
67
+ Copyright (c) 2025 Tremor Labs, Inc.
68
+
69
+ Permission is hereby granted, free of charge, to any person obtaining a copy
70
+ of this software and associated documentation files (the "Software"), to deal
71
+ in the Software without restriction, including without limitation the rights
72
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
73
+ copies of the Software, and to permit persons to whom the Software is
74
+ furnished to do so, subject to the following conditions:
75
+
76
+ The above copyright notice and this permission notice shall be included in all
77
+ copies or substantial portions of the Software.
78
+
79
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
80
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
81
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
82
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
83
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
84
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
85
+ SOFTWARE.
86
+ ```
87
+
53
88
  ## shadcn/ui file tree
54
89
 
55
90
  The file tree adapts `sidebar-11` and its menu primitives from shadcn/ui at
@@ -3,6 +3,7 @@ import type { TLOnMountHandler } from '@tldraw/editor';
3
3
  import type { TLDefaultFonts, TLUiAssetUrls } from 'tldraw';
4
4
  import type { CanvasController } from './canvas.js';
5
5
  import type { CanvasMountedTools } from './canvas-mounted.js';
6
+ import type { ViewerComponents } from './viewer-components.js';
6
7
  export type CanvasFontUrls = Readonly<Record<`${keyof TLDefaultFonts}${'' | '_italic' | '_bold' | '_italic_bold'}`, string>>;
7
8
  export type CanvasAssetUrls = Omit<TLUiAssetUrls, 'fonts' | 'embedIcons'> & {
8
9
  readonly fonts: CanvasFontUrls;
@@ -17,6 +18,8 @@ export interface CanvasEditorProps {
17
18
  readonly licenseKey?: string;
18
19
  readonly onMount?: TLOnMountHandler;
19
20
  readonly onMountedTools?: (tools: CanvasMountedTools | null) => void;
21
+ /** Host presentation for the canvas controls; without it the package renders unstyled native buttons. */
22
+ readonly components?: ViewerComponents;
20
23
  }
21
- export default function NativeCanvas({ controller, assetUrls, title, className, height, licenseKey, onMount, onMountedTools }: CanvasEditorProps): import("react").JSX.Element;
24
+ export default function NativeCanvas({ controller, assetUrls, title, className, height, licenseKey, onMount, onMountedTools, components }: CanvasEditorProps): import("react").JSX.Element;
22
25
  //# sourceMappingURL=canvas-editor-native.d.ts.map
@@ -23,7 +23,8 @@ function assetSignature(assets) {
23
23
  throw new TypeError(`Canvas asset URL is required: ${key}`);
24
24
  return JSON.stringify(requirements);
25
25
  }
26
- export default function NativeCanvas({ controller, assetUrls, title = 'Canvas', className, height = 480, licenseKey, onMount, onMountedTools }) {
26
+ export default function NativeCanvas({ controller, assetUrls, title = 'Canvas', className, height = 480, licenseKey, onMount, onMountedTools, components }) {
27
+ const Control = components?.button ?? 'button';
27
28
  const state = useSyncExternalStore(controller.subscribe, controller.getSnapshot, controller.getSnapshot);
28
29
  const [error, setError] = useState(null);
29
30
  const [busy, setBusy] = useState(false);
@@ -75,16 +76,17 @@ export default function NativeCanvas({ controller, assetUrls, title = 'Canvas',
75
76
  if (!unavailable && !pending && !uncertain && !busy && !state.problem)
76
77
  void save();
77
78
  }
78
- }, children: [_jsxs("header", { children: [_jsx("strong", { children: title }), state.readOnly && _jsx("span", { children: "Read only" }), _jsx("span", { role: "status", children: state.lifecycle === 'disposed' ? 'Closed' : pending ? 'Saving' : uncertain ? 'Save unconfirmed' : conflict ? 'Changed elsewhere' : state.dirty ? 'Unsaved changes' : 'Saved' }), _jsx("button", { type: "button", disabled: unavailable || !state.dirty || pending || uncertain || busy || state.problem !== null, onClick: () => void save(), children: "Save" }), _jsx("button", { type: "button", disabled: state.lifecycle === 'disposed' || pending || uncertain || busy, onClick: () => void run(controller.actions.refresh), children: "Check saved version" })] }), uncertain && _jsxs("div", { role: "alert", children: ["Save acknowledgement was lost. Your current canvas is retained.", _jsx("button", { type: "button", disabled: busy, onClick: () => void run(controller.actions.reconcile), children: "Check save outcome" }), _jsx("button", { type: "button", disabled: busy || state.lifecycle === 'disposed', onClick: () => void run(controller.actions.abandon), children: "Keep draft and refresh" })] }), conflict && _jsxs("div", { role: "alert", children: ["The saved canvas changed. Your local edits have been kept.", _jsx("button", { type: "button", disabled: pending || uncertain || busy, onClick: () => void run(controller.actions.discardToRemote), children: "Discard local changes and reload" })] }), state.problem && _jsx("p", { role: "alert", children: state.problem }), (outcome?.kind === 'denied' || outcome?.kind === 'unavailable' || outcome?.kind === 'file_too_large') && _jsx("p", { role: "alert", children: outcome.reason }), error && _jsx("p", { role: "alert", children: error }), _jsx(CanvasProposals, { controller: controller, state: state, editor: nativeEditor, busy: busy, run: run }), state.lifecycle === 'active' && _jsx("div", { "data-boring": "canvas-stage", style: { position: 'relative', height }, onClickCapture: event => { if (event.target instanceof Element && event.target.closest('a'))
79
+ }, children: [_jsxs("header", { children: [_jsx("strong", { children: title }), state.readOnly && _jsx("span", { children: "Read only" }), _jsx("span", { role: "status", children: state.lifecycle === 'disposed' ? 'Closed' : pending ? 'Saving' : uncertain ? 'Save unconfirmed' : conflict ? 'Changed elsewhere' : state.dirty ? 'Unsaved changes' : 'Saved' }), _jsx(Control, { type: "button", disabled: unavailable || !state.dirty || pending || uncertain || busy || state.problem !== null, onClick: () => void save(), children: "Save" }), _jsx(Control, { type: "button", disabled: state.lifecycle === 'disposed' || pending || uncertain || busy, onClick: () => void run(controller.actions.refresh), children: "Check saved version" })] }), uncertain && _jsxs("div", { role: "alert", children: ["Save acknowledgement was lost. Your current canvas is retained.", _jsx(Control, { type: "button", disabled: busy, onClick: () => void run(controller.actions.reconcile), children: "Check save outcome" }), _jsx(Control, { type: "button", disabled: busy || state.lifecycle === 'disposed', onClick: () => void run(controller.actions.abandon), children: "Keep draft and refresh" })] }), conflict && _jsxs("div", { role: "alert", children: ["The saved canvas changed. Your local edits have been kept.", _jsx(Control, { type: "button", disabled: pending || uncertain || busy, onClick: () => void run(controller.actions.discardToRemote), children: "Discard local changes and reload" })] }), state.problem && _jsx("p", { role: "alert", children: state.problem }), (outcome?.kind === 'denied' || outcome?.kind === 'unavailable' || outcome?.kind === 'file_too_large') && _jsx("p", { role: "alert", children: outcome.reason }), error && _jsx("p", { role: "alert", children: error }), _jsx(CanvasProposals, { components: components, controller: controller, state: state, editor: nativeEditor, busy: busy, run: run }), state.lifecycle === 'active' && _jsx("div", { "data-boring": "canvas-stage", style: { position: 'relative', height }, onClickCapture: event => { if (event.target instanceof Element && event.target.closest('a'))
79
80
  event.preventDefault(); }, onAuxClickCapture: event => { if (event.target instanceof Element && event.target.closest('a'))
80
- event.preventDefault(); }, children: _jsxs(Tldraw, { store: controller.store, hideUi: true, locale: "en", autoFocus: false, assetUrls: assets, ...(licenseKey === undefined ? {} : { licenseKey }), onMount: attach, children: [_jsx(CanvasTools, { controller: controller }), _jsx(MountedCommands, { controller: controller, revokeTools: revokeTools, ...(onMountedTools ? { onMountedTools } : {}) })] }) })] });
81
+ event.preventDefault(); }, children: _jsxs(Tldraw, { store: controller.store, hideUi: true, locale: "en", autoFocus: false, assetUrls: assets, ...(licenseKey === undefined ? {} : { licenseKey }), onMount: attach, children: [_jsx(CanvasTools, { components: components, controller: controller }), _jsx(MountedCommands, { controller: controller, revokeTools: revokeTools, ...(onMountedTools ? { onMountedTools } : {}) })] }) })] });
81
82
  }
82
83
  const externalContentTypes = ['text', 'files', 'file-replace', 'embed', 'svg-text', 'url', 'tldraw', 'excalidraw'];
83
84
  const choices = [
84
85
  ['select', 'Select'], ['hand', 'Hand'], ['draw', 'Draw'], ['geo', 'Rectangle'], ['text', 'Text'],
85
86
  ['arrow', 'Arrow'], ['line', 'Line'], ['note', 'Note'], ['frame', 'Frame'], ['highlight', 'Highlight'], ['eraser', 'Erase'],
86
87
  ];
87
- function CanvasTools({ controller }) {
88
+ function CanvasTools({ controller, components }) {
89
+ const Control = components?.button ?? 'button';
88
90
  const editor = useEditor();
89
91
  const state = useValue('canvas controls', () => ({ tool: editor.getCurrentToolId(), readOnly: controller.getSnapshot().readOnly || editor.getIsReadonly(), undo: editor.getCanUndo(), redo: editor.getCanRedo() }), [editor]);
90
92
  const act = (action, changesDocument = true) => {
@@ -92,8 +94,8 @@ function CanvasTools({ controller }) {
92
94
  && (!changesDocument || (!controller.getSnapshot().readOnly && !editor.getIsReadonly())))
93
95
  action();
94
96
  };
95
- return _jsxs("div", { role: "toolbar", "aria-label": "Canvas tools", style: { position: 'absolute', top: 8, left: 8, zIndex: 300, display: 'flex', flexWrap: 'wrap', gap: 4 }, children: [choices.map(([id, label]) => _jsx("button", { type: "button", "aria-pressed": state.tool === id, disabled: state.readOnly && id !== 'hand' && id !== 'select', onClick: () => act(() => { if (id === 'geo')
96
- editor.setStyleForNextShapes(GeoShapeGeoStyle, 'rectangle'); editor.setCurrentTool(id); }, id !== 'hand' && id !== 'select'), children: label }, id)), _jsx("button", { type: "button", disabled: state.readOnly || !state.undo, onClick: () => act(() => { editor.undo(); }), children: "Undo" }), _jsx("button", { type: "button", disabled: state.readOnly || !state.redo, onClick: () => act(() => { editor.redo(); }), children: "Redo" }), _jsx("button", { type: "button", onClick: () => act(() => { editor.zoomToFit(); }, false), children: "Fit canvas" })] });
97
+ return _jsxs("div", { role: "toolbar", "aria-label": "Canvas tools", style: { position: 'absolute', top: 8, left: 8, zIndex: 300, display: 'flex', flexWrap: 'wrap', gap: 4 }, children: [choices.map(([id, label]) => _jsx(Control, { type: "button", "aria-pressed": state.tool === id, disabled: state.readOnly && id !== 'hand' && id !== 'select', onClick: () => act(() => { if (id === 'geo')
98
+ editor.setStyleForNextShapes(GeoShapeGeoStyle, 'rectangle'); editor.setCurrentTool(id); }, id !== 'hand' && id !== 'select'), children: label }, id)), _jsx(Control, { type: "button", disabled: state.readOnly || !state.undo, onClick: () => act(() => { editor.undo(); }), children: "Undo" }), _jsx(Control, { type: "button", disabled: state.readOnly || !state.redo, onClick: () => act(() => { editor.redo(); }), children: "Redo" }), _jsx(Control, { type: "button", onClick: () => act(() => { editor.zoomToFit(); }, false), children: "Fit canvas" })] });
97
99
  }
98
100
  function MountedCommands({ controller, onMountedTools, revokeTools }) {
99
101
  const editor = useEditor();
@@ -9,5 +9,5 @@ export function CanvasEditor(props) {
9
9
  const [mount, setMount] = useState({ controller: props.controller, sequence: 0 });
10
10
  if (mount.controller !== props.controller)
11
11
  setMount({ controller: props.controller, sequence: mount.sequence + 1 });
12
- return _jsxs(_Fragment, { children: [_jsx(TextDraftControls, { recovery: state.recovery, actions: props.controller.actions, readOnly: state.readOnly, blocked: state.lifecycle !== 'active' || state.save.kind === 'pending' || (state.save.kind === 'settled' && state.save.result.kind === 'unknown') }, mount.sequence), _jsx(Suspense, { fallback: _jsx("p", { role: "status", children: "Loading canvas" }), children: _jsx(NativeCanvas, { ...props }, mount.sequence) })] });
12
+ return _jsxs(_Fragment, { children: [_jsx(TextDraftControls, { components: props.components, recovery: state.recovery, actions: props.controller.actions, readOnly: state.readOnly, blocked: state.lifecycle !== 'active' || state.save.kind === 'pending' || (state.save.kind === 'settled' && state.save.result.kind === 'unknown') }, mount.sequence), _jsx(Suspense, { fallback: _jsx("p", { role: "status", children: "Loading canvas" }), children: _jsx(NativeCanvas, { ...props }, mount.sequence) })] });
13
13
  }
@@ -1,7 +1,9 @@
1
1
  import type { Editor } from '@tldraw/editor';
2
2
  import type { CanvasController, CanvasState } from './canvas.js';
3
3
  import type { SaveResult } from './resources.js';
4
- export declare function CanvasProposals({ controller, state, editor, busy, run }: {
4
+ import type { ViewerComponents } from './viewer-components.js';
5
+ export declare function CanvasProposals({ components, controller, state, editor, busy, run }: {
6
+ readonly components?: ViewerComponents | undefined;
5
7
  readonly controller: CanvasController;
6
8
  readonly state: CanvasState;
7
9
  readonly editor: Editor | null;
@@ -28,18 +28,19 @@ function ProposalChanges({ proposal }) {
28
28
  }, [proposal]);
29
29
  return _jsxs("details", { children: [_jsxs("summary", { children: ["Review ", changes.length, " changed ", changes.length === 1 ? 'record' : 'records'] }), changes.map(change => _jsxs("div", { children: [_jsxs("h4", { children: [change.kind, ": ", change.id] }), _jsxs("table", { children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { scope: "col", children: "Field" }), _jsx("th", { scope: "col", children: "Before" }), _jsx("th", { scope: "col", children: "After" })] }) }), _jsx("tbody", { children: change.fields.map(row => _jsxs("tr", { children: [_jsx("th", { scope: "row", children: row.field }), _jsx("td", { children: _jsx("pre", { style: { whiteSpace: 'pre-wrap', overflowWrap: 'anywhere' }, children: row.before }) }), _jsx("td", { children: _jsx("pre", { style: { whiteSpace: 'pre-wrap', overflowWrap: 'anywhere' }, children: row.after }) })] }, row.field)) })] })] }, change.id))] });
30
30
  }
31
- export function CanvasProposals({ controller, state, editor, busy, run }) {
31
+ export function CanvasProposals({ components, controller, state, editor, busy, run }) {
32
32
  const nativeReadOnly = useValue('canvas proposal readonly', () => !editor || editor.isDisposed || editor.getIsReadonly(), [editor]);
33
33
  const blocked = busy || nativeReadOnly || state.readOnly || state.lifecycle !== 'active' || state.problem !== null
34
34
  || state.save.kind === 'pending' || (state.save.kind === 'settled' && state.save.result.kind === 'unknown');
35
+ const Control = components?.button ?? 'button';
35
36
  if (!state.proposals.length)
36
37
  return null;
37
38
  return _jsx("aside", { "aria-label": "Canvas proposals", children: state.proposals.map(proposal => {
38
39
  const stale = state.bufferVersion !== proposal.base.target.subject.bufferVersion || !sameBase(state.base, proposal.base.target.subject.base);
39
- return _jsxs("article", { "data-boring": "canvas-proposal", children: [_jsx("h3", { children: proposal.summary || 'Canvas change proposal' }), _jsx(ProposalChanges, { proposal: proposal }), proposal.adopted ? _jsx("p", { children: "Applied locally. Publication status is shown above." }) : stale ? _jsx("p", { children: "Canvas changed since this proposal." }) : null, !proposal.adopted && _jsx("button", { type: "button", disabled: blocked || stale, onClick: () => {
40
+ return _jsxs("article", { "data-boring": "canvas-proposal", children: [_jsx("h3", { children: proposal.summary || 'Canvas change proposal' }), _jsx(ProposalChanges, { proposal: proposal }), proposal.adopted ? _jsx("p", { children: "Applied locally. Publication status is shown above." }) : stale ? _jsx("p", { children: "Canvas changed since this proposal." }) : null, !proposal.adopted && _jsx(Control, { type: "button", disabled: blocked || stale, onClick: () => {
40
41
  if (!editor || editor.isDisposed || editor.getIsReadonly())
41
42
  return;
42
43
  void run(() => controller.actions.accept(proposal.id));
43
- }, children: "Accept and save" }), _jsx("button", { type: "button", disabled: busy || state.lifecycle !== 'active', onClick: () => controller.actions.reject(proposal.id), children: "Dismiss" })] }, proposal.id);
44
+ }, children: "Accept and save" }), _jsx(Control, { type: "button", disabled: busy || state.lifecycle !== 'active', onClick: () => controller.actions.reject(proposal.id), children: "Dismiss" })] }, proposal.id);
44
45
  }) });
45
46
  }
@@ -1,6 +1,7 @@
1
1
  import type { RenderedExperienceCell } from './experience-renderer.js';
2
2
  import type { ExperienceAccess } from './experience-compose.js';
3
3
  import type { ExperienceDocumentController, ExperienceRegionRequest } from './experience-document.js';
4
+ import type { ViewerComponents } from './viewer-components.js';
4
5
  export interface ExperienceDocumentProps {
5
6
  readonly controller: ExperienceDocumentController;
6
7
  readonly cells: readonly RenderedExperienceCell[];
@@ -8,6 +9,8 @@ export interface ExperienceDocumentProps {
8
9
  readonly onRegenerate?: (request: ExperienceRegionRequest) => void | Promise<void>;
9
10
  readonly title?: string;
10
11
  readonly className?: string;
12
+ /** Host presentation for the layout controls; without it the package renders unstyled native buttons. */
13
+ readonly components?: ViewerComponents;
11
14
  }
12
15
  export declare function ExperienceDocument(props: ExperienceDocumentProps): import("react").JSX.Element;
13
16
  //# sourceMappingURL=experience-document-viewer.d.ts.map
@@ -4,7 +4,8 @@ import { TextDraftControls } from './text-draft-controls.js';
4
4
  import { useLayoutEffect, useRef, useState, useSyncExternalStore } from 'react';
5
5
  import { ExperienceRenderer } from './experience-renderer.js';
6
6
  import { layoutProps } from './experience-catalog.js';
7
- function MountedDocument({ controller, cells, canView, onRegenerate, title = 'Experience layout', className }) {
7
+ function MountedDocument({ controller, cells, canView, onRegenerate, title = 'Experience layout', className, components }) {
8
+ const Control = components?.button ?? 'button';
8
9
  const state = useSyncExternalStore(controller.subscribe, controller.getSnapshot, controller.getSnapshot);
9
10
  const [error, setError] = useState();
10
11
  const active = useRef(true);
@@ -26,11 +27,11 @@ function MountedDocument({ controller, cells, canView, onRegenerate, title = 'Ex
26
27
  const status = disposed ? 'Viewer disposed' : state.readOnly ? 'Read only' : pending ? 'Keeping selected layout' : unknown ? 'Keep unconfirmed. Reconcile before keeping again.'
27
28
  : state.save.kind === 'settled' && state.save.result.kind !== 'saved' ? state.save.result.kind === 'partial' ? 'Keep unconfirmed' : state.save.result.reason
28
29
  : state.dirty ? 'Layout not kept' : 'Layout kept';
29
- return _jsxs("section", { "data-boring": "experience-document", className: className, "aria-label": title, children: [_jsxs("header", { children: [_jsx("strong", { children: title }), _jsx("span", { role: "status", children: status })] }), _jsxs("div", { role: "toolbar", "aria-label": "Layout document controls", children: [_jsx("button", { type: "button", disabled: disposed || state.readOnly || !state.dirty || !state.descriptor || pending || unknown, onClick: () => { const selection = controller.actions.selection(); perform(() => controller.flush(selection)); }, children: "Keep this layout" }), _jsx("button", { type: "button", disabled: disposed || pending || unknown, onClick: () => perform(controller.actions.refresh), children: "Refresh layout" }), _jsx("button", { type: "button", disabled: disposed || !unknown, onClick: () => perform(controller.actions.reconcile), children: "Reconcile keep" }), _jsx("button", { type: "button", disabled: disposed || pending || unknown || state.remote === null, onClick: () => perform(controller.actions.discardToRemote), children: "Discard local layout" }), state.pin && _jsx("button", { type: "button", disabled: disposed || state.readOnly || pending || unknown, onClick: () => {
30
+ return _jsxs("section", { "data-boring": "experience-document", className: className, "aria-label": title, children: [_jsxs("header", { children: [_jsx("strong", { children: title }), _jsx("span", { role: "status", children: status })] }), _jsxs("div", { role: "toolbar", "aria-label": "Layout document controls", children: [_jsx(Control, { type: "button", disabled: disposed || state.readOnly || !state.dirty || !state.descriptor || pending || unknown, onClick: () => { const selection = controller.actions.selection(); perform(() => controller.flush(selection)); }, children: "Keep this layout" }), _jsx(Control, { type: "button", disabled: disposed || pending || unknown, onClick: () => perform(controller.actions.refresh), children: "Refresh layout" }), _jsx(Control, { type: "button", disabled: disposed || !unknown, onClick: () => perform(controller.actions.reconcile), children: "Reconcile keep" }), _jsx(Control, { type: "button", disabled: disposed || pending || unknown || state.remote === null, onClick: () => perform(controller.actions.discardToRemote), children: "Discard local layout" }), state.pin && _jsx(Control, { type: "button", disabled: disposed || state.readOnly || pending || unknown, onClick: () => {
30
31
  const pin = state.pin;
31
32
  if (pin)
32
33
  perform(() => controller.actions.pin(pin.selection));
33
- }, children: "Pin this region" }), regions.filter(region => region.regenerate.includes('request')).map(region => _jsxs("button", { type: "button", disabled: disposed || state.readOnly || !onRegenerate, onClick: () => {
34
+ }, children: "Pin this region" }), regions.filter(region => region.regenerate.includes('request')).map(region => _jsxs(Control, { type: "button", disabled: disposed || state.readOnly || !onRegenerate, onClick: () => {
34
35
  const result = controller.actions.beginRegion(controller.actions.selection(), region.region, 'request');
35
36
  if (result.kind !== 'applied') {
36
37
  setError('reason' in result ? result.reason : 'Region request was not started');
@@ -41,10 +42,10 @@ function MountedDocument({ controller, cells, canView, onRegenerate, title = 'Ex
41
42
  if (active.current)
42
43
  setError('The region could not be regenerated. The current layout has been retained.');
43
44
  });
44
- }, children: ["Regenerate ", region.region] }, region.region))] }), _jsx(TextDraftControls, { recovery: state.recovery, actions: controller.actions, readOnly: state.readOnly, blocked: state.lifecycle !== 'active' || state.save.kind === 'pending' || (state.save.kind === 'settled' && state.save.result.kind === 'unknown') }), proposal && _jsxs("div", { role: "group", "aria-label": proposal.kind === 'region' ? 'Proposed region' : 'Proposed layout', children: [_jsx("button", { type: "button", disabled: disposed || state.readOnly, onClick: () => {
45
+ }, children: ["Regenerate ", region.region] }, region.region))] }), _jsx(TextDraftControls, { components: components, recovery: state.recovery, actions: controller.actions, readOnly: state.readOnly, blocked: state.lifecycle !== 'active' || state.save.kind === 'pending' || (state.save.kind === 'settled' && state.save.result.kind === 'unknown') }), proposal && _jsxs("div", { role: "group", "aria-label": proposal.kind === 'region' ? 'Proposed region' : 'Proposed layout', children: [_jsx(Control, { type: "button", disabled: disposed || state.readOnly, onClick: () => {
45
46
  const result = controller.actions.adopt(proposal.id);
46
47
  setError(result.kind === 'applied' ? undefined : 'reason' in result ? result.reason : 'Layout was not adopted');
47
- }, children: proposal.kind === 'region' ? 'Use proposed region' : 'Use proposed layout' }), _jsx("button", { type: "button", disabled: disposed, onClick: () => { controller.actions.reject(proposal.id); setError(undefined); }, children: proposal.kind === 'region' ? 'Dismiss proposed region' : 'Dismiss proposed layout' })] }), (error || state.problem) && _jsx("p", { role: "alert", children: error ?? state.problem }), state.descriptor ? _jsx("section", { "data-boring": "experience", "aria-label": state.descriptor.title ?? state.descriptor.name, children: _jsx(ExperienceRenderer, { descriptor: state.descriptor, cells: cells, canView: canView }) }) : _jsx("p", { children: "No layout selected" })] });
48
+ }, children: proposal.kind === 'region' ? 'Use proposed region' : 'Use proposed layout' }), _jsx(Control, { type: "button", disabled: disposed, onClick: () => { controller.actions.reject(proposal.id); setError(undefined); }, children: proposal.kind === 'region' ? 'Dismiss proposed region' : 'Dismiss proposed layout' })] }), (error || state.problem) && _jsx("p", { role: "alert", children: error ?? state.problem }), state.descriptor ? _jsx("section", { "data-boring": "experience", "aria-label": state.descriptor.title ?? state.descriptor.name, children: _jsx(ExperienceRenderer, { descriptor: state.descriptor, cells: cells, canView: canView }) }) : _jsx("p", { children: "No layout selected" })] });
48
49
  }
49
50
  export function ExperienceDocument(props) {
50
51
  const [mount, setMount] = useState({ controller: props.controller, sequence: 0 });
@@ -1,5 +1,6 @@
1
1
  import type { RenderedExperienceCell } from './experience-renderer.js';
2
2
  import type { ExperienceAccess } from './experience-compose.js';
3
+ import type { ViewerComponents } from './viewer-components.js';
3
4
  export { ExperienceRenderer } from './experience-renderer.js';
4
5
  export type { RenderedExperienceCell } from './experience-renderer.js';
5
6
  export interface ExperienceProps {
@@ -7,6 +8,8 @@ export interface ExperienceProps {
7
8
  readonly cells: readonly RenderedExperienceCell[];
8
9
  readonly canView: ExperienceAccess['canView'];
9
10
  readonly className?: string;
11
+ /** Host presentation for the proposal action; without it the package renders an unstyled native button. */
12
+ readonly components?: ViewerComponents;
10
13
  }
11
14
  export declare function Experience(props: ExperienceProps): import("react").JSX.Element;
12
15
  //# sourceMappingURL=experience.d.ts.map
@@ -13,11 +13,12 @@ function select(props) {
13
13
  }
14
14
  }
15
15
  export function Experience(props) {
16
+ const Control = props.components?.button ?? 'button';
16
17
  const [current, setCurrent] = useState(() => select(props));
17
18
  const candidate = current.input === props.descriptor && current.kind === 'ready' ? null : select(props);
18
19
  const offered = candidate?.kind === 'invalid' && current.kind === 'invalid' ? null : candidate;
19
20
  return _jsxs("section", { "data-boring": "experience", className: props.className, "aria-label": current.kind === 'ready' ? current.descriptor.title ?? current.descriptor.name : 'Experience unavailable', children: [offered && (offered.kind === 'ready'
20
- ? _jsx("button", { type: "button", onClick: () => setCurrent(offered), children: "Use proposed layout" })
21
+ ? _jsx(Control, { type: "button", onClick: () => setCurrent(offered), children: "Use proposed layout" })
21
22
  : _jsx("p", { role: "alert", children: "Proposed layout unavailable. The current layout has been retained." })), current.kind === 'invalid' ? _jsx("p", { role: "alert", children: "Experience unavailable" })
22
23
  : _jsx(ExperienceRenderer, { descriptor: current.descriptor, cells: props.cells, canView: props.canView })] });
23
24
  }
@@ -1,4 +1,6 @@
1
- import type { RevisionProvider } from '@hachej/boring-files/revision';
1
+ import type { Collapsible } from '@base-ui/react/collapsible';
2
+ import type { ComponentProps, ReactNode } from 'react';
3
+ import type { RevisionProvider, SavedRevision } from '@hachej/boring-files/revision';
2
4
  import type { FileTreeController } from './file-tree.js';
3
5
  /** Every word of the tree, so a host can translate it. Functions receive what they name. */
4
6
  export declare const defaultFileTreeLabels: {
@@ -14,6 +16,20 @@ export declare const defaultFileTreeLabels: {
14
16
  retry: string;
15
17
  noMatches: string;
16
18
  empty: string;
19
+ emptyHint: string;
20
+ library: string;
21
+ close: string;
22
+ open: string;
23
+ download: string;
24
+ downloadingFailed: string;
25
+ actionsOf: (path: string) => string;
26
+ shownItems: (count: number) => string;
27
+ folder: string;
28
+ childCount: (count: number) => string;
29
+ file: string;
30
+ image: string;
31
+ document: string;
32
+ dropFiles: string;
17
33
  loadMore: string;
18
34
  history: string;
19
35
  historyOf: (path: string) => string;
@@ -37,7 +53,56 @@ export type FileTreeLabels = typeof defaultFileTreeLabels;
37
53
  export declare const frenchFileTreeLabels: FileTreeLabels;
38
54
  /** The languages the tree ships with; anything else is set word by word through `labels`. */
39
55
  export type FileTreeLocale = 'en' | 'fr';
56
+ export interface FileTreeAction {
57
+ readonly id: 'open' | 'history' | 'download';
58
+ readonly label: string;
59
+ readonly icon: ReactNode;
60
+ readonly onSelect: () => void;
61
+ }
62
+ export interface FileTreeActionsProps {
63
+ readonly label: string;
64
+ readonly icon: ReactNode;
65
+ readonly items: readonly FileTreeAction[];
66
+ }
67
+ export type FileTreeButtonProps = ComponentProps<'button'> & {
68
+ readonly variant?: 'secondary' | 'ghost';
69
+ readonly size?: 'default' | 'sm' | 'icon';
70
+ };
71
+ export interface FileTreeVersionsProps {
72
+ readonly versions: readonly SavedRevision[];
73
+ readonly current: string;
74
+ readonly onSelect: (revision: string) => void;
75
+ readonly locale: FileTreeLocale;
76
+ readonly earlierVersion: string;
77
+ }
78
+ /** Host presentation. The registry supplies real shadcn Base UI controls and shared version rows; no primitive fallback. */
79
+ export interface FileTreeComponents {
80
+ readonly Button: (props: FileTreeButtonProps) => ReactNode;
81
+ readonly Actions: (props: FileTreeActionsProps) => ReactNode;
82
+ readonly Versions: (props: FileTreeVersionsProps) => ReactNode;
83
+ /** The host's shadcn sidebar menu (`SidebarMenu`, `SidebarMenuItem`, `SidebarMenuSub`). */
84
+ readonly Menu: (props: ComponentProps<'ul'>) => ReactNode;
85
+ readonly MenuItem: (props: ComponentProps<'li'>) => ReactNode;
86
+ readonly MenuSub: (props: ComponentProps<'ul'>) => ReactNode;
87
+ /** The host's shadcn `Input`. */
88
+ readonly Input: (props: ComponentProps<'input'>) => ReactNode;
89
+ /** The host's shadcn `Separator`. */
90
+ readonly Separator: () => ReactNode;
91
+ /** The host's shadcn `Collapsible`, `CollapsibleTrigger` and `CollapsibleContent`. */
92
+ readonly Collapsible: (props: Collapsible.Root.Props) => ReactNode;
93
+ readonly CollapsibleTrigger: (props: Collapsible.Trigger.Props) => ReactNode;
94
+ readonly CollapsibleContent: (props: Collapsible.Panel.Props) => ReactNode;
95
+ /** The host's shadcn `Empty`, composed from an icon, a title, an optional description and optional actions. */
96
+ readonly Empty: (props: {
97
+ readonly icon?: ReactNode;
98
+ readonly title: ReactNode;
99
+ readonly description?: ReactNode;
100
+ readonly className?: string;
101
+ readonly children?: ReactNode;
102
+ }) => ReactNode;
103
+ }
40
104
  export interface FileTreeProps {
105
+ readonly components: FileTreeComponents;
41
106
  readonly revisionProvider: RevisionProvider;
42
107
  readonly onOpen?: (path: string) => void;
43
108
  readonly selectedPath?: string;
@@ -49,8 +114,16 @@ export interface FileTreeProps {
49
114
  readonly labels?: Partial<FileTreeLabels>;
50
115
  /** Offer the Upload button. Default true. */
51
116
  readonly upload?: boolean;
52
- /** Offer each file's History button. Default true. */
117
+ /** Offer version history in each file's actions menu. Default true. */
53
118
  readonly history?: boolean;
119
+ /** Offer the Refresh button. Default true; a host whose tree follows the workspace's change feed (`refreshKey`) can hide it. */
120
+ readonly refresh?: boolean;
121
+ /** `list` (default) includes a Library header, inset search, metadata and file actions. `plain` keeps the compact tree. */
122
+ readonly variant?: 'plain' | 'list';
123
+ /** The list header's title, over the locale's Library label. */
124
+ readonly title?: string;
125
+ /** A host-owned dismissal action, shown at the right of the list header. */
126
+ readonly onClose?: () => void;
54
127
  /** Reload the open folders and the current search whenever this value changes (for example after the agent saves a file). */
55
128
  readonly refreshKey?: unknown;
56
129
  /** Reload the open history panel whenever this value changes (for example the seq of the workspace's latest change event). */
@@ -62,5 +135,5 @@ export interface FileTreeViewProps extends Omit<FileTreeProps, 'revisionProvider
62
135
  readonly controller: FileTreeController;
63
136
  }
64
137
  /** Renders a borrowed controller, for a workspace whose chat and tree share upload state. */
65
- export declare function FileTreeView({ controller, onOpen, selectedPath, className, uploadDirectory, locale, labels: given, upload, history, refreshKey, historyKey }: FileTreeViewProps): import("react").JSX.Element;
138
+ export declare function FileTreeView({ components, controller, onOpen, selectedPath, className, uploadDirectory, locale, labels: given, upload, history, refresh, variant, title, onClose, refreshKey, historyKey }: FileTreeViewProps): import("react").JSX.Element;
66
139
  //# sourceMappingURL=file-tree-view.d.ts.map