@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.
- package/README.md +46 -18
- package/THIRD_PARTY_NOTICES.md +35 -0
- package/dist/canvas-editor-native.d.ts +4 -1
- package/dist/canvas-editor-native.js +8 -6
- package/dist/canvas-editor.js +1 -1
- package/dist/canvas-proposals.d.ts +3 -1
- package/dist/canvas-proposals.js +4 -3
- package/dist/experience-document-viewer.d.ts +3 -0
- package/dist/experience-document-viewer.js +6 -5
- package/dist/experience.d.ts +3 -0
- package/dist/experience.js +2 -1
- package/dist/file-tree-view.d.ts +76 -3
- package/dist/file-tree-view.js +172 -39
- package/dist/file-tree.d.ts +3 -0
- package/dist/file-tree.js +17 -2
- package/dist/html-viewer.d.ts +4 -0
- package/dist/html-viewer.js +5 -3
- package/dist/markdown-autosave.d.ts +13 -0
- package/dist/markdown-autosave.js +94 -0
- package/dist/markdown-editor.d.ts +16 -0
- package/dist/markdown-editor.js +81 -17
- package/dist/markdown-extensions.js +9 -1
- package/dist/markdown-proposal-diff.d.ts +2 -1
- package/dist/markdown-proposal-diff.js +3 -2
- package/dist/markdown-safety.js +3 -1
- package/dist/native-chat.js +1 -1
- package/dist/remote-chat.js +5 -1
- package/dist/text-buffer.js +3 -2
- package/dist/text-draft-controls.d.ts +3 -1
- package/dist/text-draft-controls.js +3 -2
- package/dist/viewer-components.d.ts +14 -0
- package/dist/viewer-components.js +1 -0
- package/package.json +4 -4
- package/r/app-shell.json +39 -20
- package/r/button.json +19 -11
- package/r/dock.json +2 -2
- package/r/documents.json +17 -14
- package/r/feedback-preview.json +8 -2
- package/r/feedback.json +46 -325
- package/r/file-tree.json +14 -6
- package/r/html-viewer.json +8 -42
- package/r/markdown-editor.json +38 -87
- package/r/pi-ambient.json +15 -8
- package/r/pi-app.json +29 -18
- package/r/pi-chat.json +88 -69
- package/r/pi-workspace.json +7 -5
- package/r/provider-setup.json +9 -4
- package/r/registry.json +460 -888
- package/r/task-list-viewer.json +11 -21
- package/r/theme.json +23 -4
- package/r/version-history.json +24 -0
- package/r/viewers.json +139 -320
- package/styles/file-tree.css +41 -42
- package/dist/file-tree-shadcn.d.ts +0 -8
- 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
|
-
|
|
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
|
|
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
|
|
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
|
|
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 `
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
280
|
-
import { FileTree } from '
|
|
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
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
|
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
|
|
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
|
package/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -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(
|
|
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(
|
|
96
|
-
editor.setStyleForNextShapes(GeoShapeGeoStyle, 'rectangle'); editor.setCurrentTool(id); }, id !== 'hand' && id !== 'select'), children: label }, id)), _jsx(
|
|
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();
|
package/dist/canvas-editor.js
CHANGED
|
@@ -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
|
-
|
|
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;
|
package/dist/canvas-proposals.js
CHANGED
|
@@ -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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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 });
|
package/dist/experience.d.ts
CHANGED
|
@@ -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
|
package/dist/experience.js
CHANGED
|
@@ -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(
|
|
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
|
}
|
package/dist/file-tree-view.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import type {
|
|
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
|
|
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
|