@hachej/boring-ui-kit 0.4.24 → 0.4.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/boring-ui-blocks.mjs +50 -13
- package/dist/file-tree.js +5 -0
- package/package.json +2 -2
- package/r/agent-target.json +4 -4
- package/r/app-shell.json +9 -9
- package/r/board-viewer.json +5 -5
- package/r/button.json +1 -1
- package/r/diagram-viewer.json +1 -1
- package/r/dock.json +1 -1
- package/r/documents.json +15 -15
- package/r/feedback-preview.json +1 -1
- package/r/feedback.json +6 -6
- package/r/file-tree.json +5 -5
- package/r/highlights.json +3 -3
- package/r/html-viewer.json +3 -3
- package/r/looks-viewer.json +1 -1
- package/r/maquette-viewer.json +2 -2
- package/r/markdown-editor.json +3 -3
- package/r/mockup-viewer.json +1 -1
- package/r/pi-ambient.json +11 -11
- package/r/pi-app-kinds.json +10 -10
- package/r/pi-app.json +12 -12
- package/r/pi-artifacts.json +1 -1
- package/r/pi-chat-approval.json +3 -2
- package/r/pi-chat-attachments.json +1 -1
- package/r/pi-chat-markdown.json +1 -1
- package/r/pi-chat-mentions.json +1 -1
- package/r/pi-chat-pickers.json +1 -1
- package/r/pi-chat-plugins.json +11 -11
- package/r/pi-chat-question.json +1 -1
- package/r/pi-chat-slash.json +1 -1
- package/r/pi-chat-subagent.json +1 -1
- package/r/pi-chat-voice.json +2 -2
- package/r/pi-chat.json +5 -5
- package/r/pi-workspace.json +4 -4
- package/r/placed-agent.json +4 -4
- package/r/provider-setup.json +2 -2
- package/r/record-viewer.json +1 -1
- package/r/registry.json +169 -168
- package/r/requests-board.json +1 -1
- package/r/shared-chat.json +7 -7
- package/r/task-list-viewer.json +2 -2
- package/r/version-history.json +3 -3
- package/r/viewer-code.json +2 -2
- package/r/viewer-frame.json +5 -5
- package/r/viewer-html.json +4 -4
- package/r/viewer-image.json +2 -2
- package/r/viewer-markdown.json +5 -5
- package/r/viewer-pdf.json +3 -3
- package/r/viewers.json +6 -6
- package/r/windows.json +1 -1
package/bin/boring-ui-blocks.mjs
CHANGED
|
@@ -4,14 +4,14 @@
|
|
|
4
4
|
//
|
|
5
5
|
// npx boring-ui-blocks check [--design <dir>...] [--manifest <path>] [--cwd <app web dir>] [--json]
|
|
6
6
|
// npx boring-ui-blocks update [--version <v>] [--registry <url>] [--dry-run] [--manifest <path>] [--cwd <dir>]
|
|
7
|
-
// npx boring-ui-blocks add <shadcn add arguments...> [--manifest <path>] [--cwd <dir>]
|
|
7
|
+
// npx boring-ui-blocks add <shadcn add arguments...> [--overwrite] [--manifest <path>] [--cwd <dir>]
|
|
8
8
|
// npx boring-ui-blocks manifest [--manifest <path>] [--cwd <dir>]
|
|
9
9
|
// npx boring-ui-blocks help (also --help: the usage and which items to add; exit 0)
|
|
10
10
|
//
|
|
11
11
|
// `check` writes nothing. `add`, `update` and `manifest` write the shadcn files they install and the manifest, nothing else.
|
|
12
12
|
import { createHash } from 'node:crypto';
|
|
13
|
-
import { existsSync, readFileSync, readdirSync, realpathSync, writeFileSync } from 'node:fs';
|
|
14
|
-
import { join, relative, resolve, sep } from 'node:path';
|
|
13
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, writeFileSync } from 'node:fs';
|
|
14
|
+
import { dirname, join, relative, resolve, sep } from 'node:path';
|
|
15
15
|
import { spawnSync } from 'node:child_process';
|
|
16
16
|
import { fileURLToPath } from 'node:url';
|
|
17
17
|
|
|
@@ -158,12 +158,24 @@ export function checkBlocks(app, components = appPaths(app).components) {
|
|
|
158
158
|
/** The generated shadcn files under the `ui` alias (`.js/.jsx/.ts/.tsx/.css`) and their sha256, by path relative to it. */
|
|
159
159
|
const uiHashes = ui => new Map(files(ui).filter(path => /\.(?:[jt]sx?|css)$/.test(path)).map(path => [posix(relative(ui, path)), { path, hash: sha256(path) }]));
|
|
160
160
|
|
|
161
|
+
/** Records the given generated ui files in the manifest (`add` after a kept file: only what it created). Other entries and keys are kept. */
|
|
162
|
+
export function recordFiles(app, paths, manifest = MANIFEST) {
|
|
163
|
+
const { ui } = appPaths(app);
|
|
164
|
+
const path = resolve(app, manifest);
|
|
165
|
+
const kept = existsSync(path) ? JSON.parse(readFileSync(path, 'utf8')) : { generatedBy: 'boring-ui-blocks', dir: posix(relative(app, ui)) };
|
|
166
|
+
const hashes = { ...kept.files };
|
|
167
|
+
for (const file of paths) hashes[posix(relative(ui, file))] = sha256(file);
|
|
168
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
169
|
+
writeFileSync(path, `${JSON.stringify({ ...kept, files: Object.fromEntries(Object.entries(hashes).sort(([a], [b]) => a.localeCompare(b))) }, null, 2)}\n`);
|
|
170
|
+
}
|
|
171
|
+
|
|
161
172
|
/** (Re)writes the manifest (`manifest`, default SHADCN-GENERATED.json next to components.json): `files` is replaced, other keys are kept. */
|
|
162
173
|
export function writeManifest(app, manifest = MANIFEST) {
|
|
163
174
|
const { ui } = appPaths(app);
|
|
164
175
|
const path = resolve(app, manifest);
|
|
165
176
|
const hashes = Object.fromEntries([...uiHashes(ui)].map(([name, { hash }]) => [name, hash]));
|
|
166
177
|
const kept = existsSync(path) ? JSON.parse(readFileSync(path, 'utf8')) : { generatedBy: 'boring-ui-blocks', dir: posix(relative(app, ui)) };
|
|
178
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
167
179
|
writeFileSync(path, `${JSON.stringify({ ...kept, files: hashes }, null, 2)}\n`);
|
|
168
180
|
return Object.keys(hashes).length;
|
|
169
181
|
}
|
|
@@ -215,18 +227,20 @@ export function check(app, design = [], manifest = MANIFEST) {
|
|
|
215
227
|
return { version, blocks: blocks.map(block => block.name), violations };
|
|
216
228
|
}
|
|
217
229
|
|
|
218
|
-
const USAGE = `usage: boring-ui-blocks check [--design <dir>...] [--manifest <path>] [--cwd <dir>] [--json] | update [--version <v>] [--registry <url>] [--dry-run] [--manifest <path>] [--cwd <dir>] | add <shadcn add args...> [--manifest <path>] [--cwd <dir>] | manifest [--manifest <path>] [--cwd <dir>] | help\n\n` +
|
|
230
|
+
const USAGE = `usage: boring-ui-blocks check [--design <dir>...] [--manifest <path>] [--cwd <dir>] [--json] | update [--version <v>] [--registry <url>] [--dry-run] [--manifest <path>] [--cwd <dir>] | add <shadcn add args...> [--overwrite] [--manifest <path>] [--cwd <dir>] | manifest [--manifest <path>] [--cwd <dir>] | help\n\n` +
|
|
219
231
|
'Blocks: pi-app is the whole agent app with every chat feature and viewer. pi-chat is the core chat only: add pi-chat-plugins for every ' +
|
|
220
232
|
'feature, or one of pi-chat-markdown, pi-chat-slash, pi-chat-mentions, pi-chat-pickers, pi-chat-voice, pi-chat-attachments, ' +
|
|
221
233
|
'pi-chat-question, pi-chat-approval, pi-chat-subagent and pi-artifacts. Viewers are viewer-frame plus viewer-markdown, viewer-html, ' +
|
|
222
234
|
'viewer-image, viewer-pdf and viewer-code, loaded by kind. update reinstalls only installed items: ' +
|
|
223
|
-
'add the new ones an app uses once (registry/README.md, "From one item to small items").'
|
|
224
|
-
|
|
235
|
+
'add the new ones an app uses once (registry/README.md, "From one item to small items"). add never prompts: a file that already exists ' +
|
|
236
|
+
'with other content is kept and listed (exit 1) unless --overwrite replaces it; identical files are left alone.';
|
|
237
|
+
const FLAGS = { check: ['design', 'manifest', 'json'], update: ['version', 'registry', 'dry-run', 'manifest'], manifest: ['manifest'], add: ['manifest', 'overwrite'] };
|
|
225
238
|
|
|
226
239
|
function parse(command, rest) {
|
|
227
240
|
const flags = { design: [], pass: [] };
|
|
228
241
|
for (let i = 0; i < rest.length; i++) {
|
|
229
242
|
const name = rest[i].startsWith('--') ? rest[i].slice(2) : null;
|
|
243
|
+
if (command === 'add' && (name === 'overwrite' || rest[i] === '-o')) { flags.overwrite = true; continue; }
|
|
230
244
|
if (command === 'add' && name !== 'cwd' && name !== 'manifest') { flags.pass.push(rest[i]); continue; }
|
|
231
245
|
if (!name || !FLAGS[command]?.includes(name) && name !== 'cwd') throw new Error(`unknown argument ${rest[i]}`);
|
|
232
246
|
if (name === 'json') flags.json = true;
|
|
@@ -267,13 +281,30 @@ function restoreSpecifiers(before, app, log) {
|
|
|
267
281
|
if (restored.size) log(`kept the version specifiers of ${[...restored].sort().join(', ')} (the CLI had rewritten them)`);
|
|
268
282
|
}
|
|
269
283
|
|
|
270
|
-
|
|
271
|
-
|
|
284
|
+
/** The files the shadcn CLI may write for a block (the components, ui, lib and hooks aliases) and their bytes, before it runs. */
|
|
285
|
+
function snapshotFiles(app) {
|
|
286
|
+
const { aliases = {} } = readJson(join(app, 'components.json')) ?? {};
|
|
287
|
+
const alias = (name, fallback) => join(app, existsSync(join(app, 'src')) ? 'src' : '', (aliases[name] ?? fallback).replace(/^[@~]\//, ''));
|
|
288
|
+
const { components, ui } = appPaths(app);
|
|
289
|
+
return new Map([...new Set([components, ui, alias('lib', '@/lib'), alias('hooks', '@/hooks')])].flatMap(dir => files(dir)).map(path => [path, readFileSync(path)]));
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Runs the shadcn CLI without a terminal: stdin is closed and the CLI always gets `--overwrite --yes`, so it never asks. With
|
|
294
|
+
* `overwrite` false a file that existed with other content is put back afterwards and returned in `skipped`; identical files are never touched.
|
|
295
|
+
* `created` is the generated ui files the run newly wrote.
|
|
296
|
+
*/
|
|
297
|
+
function shadcnAdd(app, args, log, overwrite) {
|
|
298
|
+
const argv = ['-y', SHADCN, 'add', ...args.filter(arg => !['--yes', '-y', '--overwrite'].includes(arg)), '--overwrite', '--yes'];
|
|
272
299
|
log(`npx ${argv.join(' ')}`);
|
|
273
|
-
const before = specifiers(app);
|
|
274
|
-
const result = spawnSync('npx', argv, { cwd: app, stdio: 'inherit', shell: process.platform === 'win32' });
|
|
300
|
+
const before = specifiers(app), existing = snapshotFiles(app);
|
|
301
|
+
const result = spawnSync('npx', argv, { cwd: app, stdio: ['ignore', 'inherit', 'inherit'], shell: process.platform === 'win32' });
|
|
275
302
|
restoreSpecifiers(before, app, log);
|
|
276
|
-
|
|
303
|
+
const skipped = overwrite ? [] : [...existing].filter(([path, bytes]) => existsSync(path) && !readFileSync(path).equals(bytes));
|
|
304
|
+
for (const [path, bytes] of skipped) writeFileSync(path, bytes);
|
|
305
|
+
const { ui } = appPaths(app);
|
|
306
|
+
const created = files(ui).filter(path => /\.(?:[jt]sx?|css)$/.test(path) && !existing.has(path));
|
|
307
|
+
return { status: result.status ?? 1, skipped: skipped.map(([path]) => path), created };
|
|
277
308
|
}
|
|
278
309
|
|
|
279
310
|
const line = v => `${v.check} ${v.rule} ${v.file}${v.line ? `:${v.line}:${v.column}` : ''} ${v.detail}`;
|
|
@@ -300,7 +331,13 @@ export function main(argv, cwd = process.cwd(), log = console.log, error = conso
|
|
|
300
331
|
if (command === 'add') {
|
|
301
332
|
appPaths(app);
|
|
302
333
|
if (!flags.pass.length) throw new Error('add needs shadcn add arguments');
|
|
303
|
-
const status = shadcnAdd(app, flags.pass, log);
|
|
334
|
+
const { status, skipped, created } = shadcnAdd(app, flags.pass, log, flags.overwrite);
|
|
335
|
+
if (skipped.length) {
|
|
336
|
+
// The files the run created are the app's new generated files: record them, so `check` does not call them unknown. The kept ones stay unrecorded.
|
|
337
|
+
if (created.length) recordFiles(app, created, flags.manifest ?? MANIFEST);
|
|
338
|
+
error(`kept ${skipped.length} existing file(s) that differ from the registry (pass --overwrite to replace them; only the files add created were recorded in the manifest):\n${skipped.map(path => ` ${posix(relative(app, path))}`).join('\n')}`);
|
|
339
|
+
return status || 1;
|
|
340
|
+
}
|
|
304
341
|
if (status === 0) log(`${flags.manifest ?? MANIFEST}: ${writeManifest(app, flags.manifest)} file(s)`);
|
|
305
342
|
return status;
|
|
306
343
|
}
|
|
@@ -309,7 +346,7 @@ export function main(argv, cwd = process.cwd(), log = console.log, error = conso
|
|
|
309
346
|
const base = (flags.registry ?? itemBase(flags.version ?? installed)).replace(/\/$/, '');
|
|
310
347
|
const items = blocks.map(block => `${base}/${block.name}.json`);
|
|
311
348
|
if (flags.dryRun) { log(`npx -y ${SHADCN} add ${items.join(' ')} --overwrite --yes`); return 0; }
|
|
312
|
-
const status = shadcnAdd(app,
|
|
349
|
+
const { status } = shadcnAdd(app, items, log, true);
|
|
313
350
|
if (status === 0) log(`${flags.manifest ?? MANIFEST}: ${writeManifest(app, flags.manifest)} file(s)`);
|
|
314
351
|
return status;
|
|
315
352
|
} catch (cause) {
|
package/dist/file-tree.js
CHANGED
|
@@ -233,6 +233,11 @@ export function createFileTreeController({ revisionProvider }) {
|
|
|
233
233
|
const signal = input.signal ? AbortSignal.any([input.signal, lifetime.signal]) : lifetime.signal;
|
|
234
234
|
if (signal.aborted)
|
|
235
235
|
return uploadState(operationId, { kind: 'unavailable', reason: 'Upload cancelled before publication' });
|
|
236
|
+
// The host names its file limit in the binding; a larger file is refused here as the server would refuse it, without sending it.
|
|
237
|
+
const limit = revisionProvider.maxFileBytes;
|
|
238
|
+
if (limit !== undefined && bytes.byteLength > limit) {
|
|
239
|
+
return uploadState(operationId, { kind: 'file_too_large', path: input.path, size: bytes.byteLength, limit, reason: `The file is larger than the workspace's ${limit}-byte limit` });
|
|
240
|
+
}
|
|
236
241
|
try {
|
|
237
242
|
return uploadState(operationId, checked(operationId, await revisionProvider.publish(request, signal)));
|
|
238
243
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hachej/boring-ui-kit",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.25",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -138,7 +138,7 @@
|
|
|
138
138
|
}
|
|
139
139
|
},
|
|
140
140
|
"peerDependencies": {
|
|
141
|
-
"@hachej/boring-files": "^0.4.
|
|
141
|
+
"@hachej/boring-files": "^0.4.25",
|
|
142
142
|
"@earendil-works/pi-durable": "~1.2.0",
|
|
143
143
|
"react": "^19.0.0",
|
|
144
144
|
"react-dom": "^19.0.0",
|
package/r/agent-target.json
CHANGED
|
@@ -4,15 +4,15 @@
|
|
|
4
4
|
"title": "Point and dictate on app data",
|
|
5
5
|
"description": "Tap explicitly marked JSON app context, dictate once and show the same app agent’s native reply without opening chat. Host-controlled presentation; no feedback report or second controller.",
|
|
6
6
|
"dependencies": [
|
|
7
|
-
"@hachej/boring-ui-kit@^0.4.
|
|
8
|
-
"@hachej/boring-feedback@^0.4.
|
|
7
|
+
"@hachej/boring-ui-kit@^0.4.25",
|
|
8
|
+
"@hachej/boring-feedback@^0.4.25",
|
|
9
9
|
"@earendil-works/pi-durable@~1.2.0",
|
|
10
10
|
"react@^19.0.0",
|
|
11
11
|
"react-dom@^19.0.0"
|
|
12
12
|
],
|
|
13
13
|
"registryDependencies": [
|
|
14
|
-
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.
|
|
15
|
-
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.
|
|
14
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.25/r/app-shell.json",
|
|
15
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.25/r/pi-chat.json",
|
|
16
16
|
"button",
|
|
17
17
|
"card",
|
|
18
18
|
"message",
|
package/r/app-shell.json
CHANGED
|
@@ -4,21 +4,21 @@
|
|
|
4
4
|
"title": "App shell: layouts, list/detail and agent layer",
|
|
5
5
|
"description": "AppShell with three layouts. B \"workspace\" puts a ListDetail on a screen (phone: the list, then the detail as a stack step with Back; wide: a 20rem list and the detail side by side, sidebar as an icon rail). C \"chat\" wraps the app's own AgentWorkspace (untouched, passed as the nav item's render; install the pi-app item yourself) in the same sidebar, account menu and header; it takes no shell agent. A \"screen\": one screen at a time, every screen kept mounted. On a phone bottom tabs (at most four; more give three plus a « Plus » sheet) and an avatar button that opens the account sheet; on a wide screen the shadcn sidebar (icon rail, NavUser). One header per screen through useScreenHeader (title, subtitle, back, up to two actions, the rest behind « … »). The account menu holds « Améliorer l'app » (the improve agent), the app's own entries and sign out. AppSheet is the one bottom sheet (one open at a time, Back closes it). It mounts the AgentLayer: one agent's PiChat above the app (phone: a drawer with peek, half and full snap points; wide: a right panel), the improve agent in the same layer, optional push-to-talk and point mode. Also useAgentContext and readAgentContext (the screen on show travels as `context` with each submit) and useBackLayer, the one Back manager. Built on explicitly pinned Base UI drawer and dropdown-menu recipes, plus the sidebar and avatar of the host's own shadcn style.",
|
|
6
6
|
"dependencies": [
|
|
7
|
-
"@hachej/boring-feedback@^0.4.
|
|
8
|
-
"@hachej/boring-ui-kit@^0.4.
|
|
7
|
+
"@hachej/boring-feedback@^0.4.25",
|
|
8
|
+
"@hachej/boring-ui-kit@^0.4.25",
|
|
9
9
|
"@base-ui/react@^1.9.0",
|
|
10
10
|
"react@^19.0.0",
|
|
11
11
|
"react-dom@^19.0.0",
|
|
12
12
|
"lucide-react@^1.51.0"
|
|
13
13
|
],
|
|
14
14
|
"registryDependencies": [
|
|
15
|
-
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.
|
|
16
|
-
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.
|
|
17
|
-
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.
|
|
18
|
-
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.
|
|
19
|
-
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.
|
|
20
|
-
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.
|
|
21
|
-
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.
|
|
15
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.25/r/pi-chat.json",
|
|
16
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.25/r/pi-chat-plugins.json",
|
|
17
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.25/r/pi-chat-voice.json",
|
|
18
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.25/r/dock.json",
|
|
19
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.25/r/feedback.json",
|
|
20
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.25/r/utils.json",
|
|
21
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.25/r/button.json",
|
|
22
22
|
"drawer",
|
|
23
23
|
"sidebar",
|
|
24
24
|
"avatar",
|
package/r/board-viewer.json
CHANGED
|
@@ -4,16 +4,16 @@
|
|
|
4
4
|
"title": "Board viewer",
|
|
5
5
|
"description": "A board@1 artifact kind for *.board.json. Lazy native tldraw projection of maquette screens, sandboxed HTML mock-ups, goto arrows and notes. Conditional board-file autosave; phone viewing without accidental frame edits. Hosts supply their resource client, asset URLs and license key.",
|
|
6
6
|
"dependencies": [
|
|
7
|
-
"@hachej/boring-ui-kit@^0.4.
|
|
8
|
-
"@hachej/boring-files@^0.4.
|
|
7
|
+
"@hachej/boring-ui-kit@^0.4.25",
|
|
8
|
+
"@hachej/boring-files@^0.4.25",
|
|
9
9
|
"react@^19.0.0",
|
|
10
10
|
"lucide-react@1.51.0",
|
|
11
11
|
"tldraw@5.5.2"
|
|
12
12
|
],
|
|
13
13
|
"registryDependencies": [
|
|
14
|
-
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.
|
|
15
|
-
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.
|
|
16
|
-
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.
|
|
14
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.25/r/maquette-viewer.json",
|
|
15
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.25/r/mockup-viewer.json",
|
|
16
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.25/r/pi-artifacts.json",
|
|
17
17
|
"button",
|
|
18
18
|
"alert",
|
|
19
19
|
"card",
|
package/r/button.json
CHANGED
package/r/diagram-viewer.json
CHANGED
package/r/dock.json
CHANGED
package/r/documents.json
CHANGED
|
@@ -4,23 +4,23 @@
|
|
|
4
4
|
"title": "Documents: the Library, a file and its revisions",
|
|
5
5
|
"description": "The workspace's Library as the list, the chosen file in its viewer and its retained revisions beside it (an older one opens read-only). DocumentSheet opens one document over the app for a host's « Voir la fiche », with an optional « Toutes les fiches ». Every read goes through one revisionProvider binding.",
|
|
6
6
|
"dependencies": [
|
|
7
|
-
"@hachej/boring-ui-kit@^0.4.
|
|
8
|
-
"@hachej/boring-files@^0.4.
|
|
7
|
+
"@hachej/boring-ui-kit@^0.4.25",
|
|
8
|
+
"@hachej/boring-files@^0.4.25",
|
|
9
9
|
"react@^19.0.0",
|
|
10
10
|
"lucide-react@^1.51.0"
|
|
11
11
|
],
|
|
12
12
|
"registryDependencies": [
|
|
13
|
-
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.
|
|
14
|
-
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.
|
|
15
|
-
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.
|
|
16
|
-
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.
|
|
17
|
-
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.
|
|
18
|
-
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.
|
|
19
|
-
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.
|
|
20
|
-
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.
|
|
21
|
-
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.
|
|
22
|
-
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.
|
|
23
|
-
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.
|
|
13
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.25/r/theme.json",
|
|
14
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.25/r/utils.json",
|
|
15
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.25/r/button.json",
|
|
16
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.25/r/pi-artifacts.json",
|
|
17
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.25/r/pi-app.json",
|
|
18
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.25/r/pi-app-kinds.json",
|
|
19
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.25/r/app-shell.json",
|
|
20
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.25/r/file-tree.json",
|
|
21
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.25/r/viewer-frame.json",
|
|
22
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.25/r/viewer-image.json",
|
|
23
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.25/r/viewer-pdf.json",
|
|
24
24
|
"drawer",
|
|
25
25
|
"button",
|
|
26
26
|
"scroll-area",
|
|
@@ -29,13 +29,13 @@
|
|
|
29
29
|
"files": [
|
|
30
30
|
{
|
|
31
31
|
"path": "registry/documents/documents.tsx",
|
|
32
|
-
"content": "'use client';\n\n// The documents screen: the workspace's Library is the list (`ListDetail`: beside the file on a wide screen, one pane at a time on a phone);\n// the chosen file opens in the same viewer the Library uses (editable where it edits); its Revisions sit beside it. Every revision row opens\n// that exact revision read-only, and « Back to editing » returns to the file. The editor stays mounted while a revision is inspected, so its\n// unsaved edits survive; leaving the file with unsaved edits asks first. Every read goes through the one `revisionProvider` the host binds.\nimport { Suspense, useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport { Button } from '@/components/ui/button';\nimport { Separator } from '@/components/ui/separator';\nimport { createFileTreeController } from '@hachej/boring-ui-kit/file-tree';\nimport type { FileTreeController } from '@hachej/boring-ui-kit/file-tree';\nimport { FileTreeView } from '../file-tree/file-tree';\nimport type { RevisionProvider } from '@hachej/boring-files/revision';\nimport { mediaTypeOf, viewerKindOf } from '@hachej/boring-files/file-types';\nimport { checkBlobBase, withBlobPointers } from '@hachej/boring-files/blob';\nimport type { ViewerKind } from '@hachej/boring-files/file-types';\nimport { ListDetail } from '../app-shell/list-detail';\nimport { ArtifactPanel } from '../pi-app/artifact-panel';\nimport type { ArtifactDescriptor } from '../pi-artifacts/artifact';\nimport type { CustomViewers, ViewerOptions } from '../pi-app/artifact-panel';\nimport { FileViewer } from '../pi-app/file-viewer';\nimport { lazyView, ViewLoading } from '../pi-app/viewer-kinds';\nimport { defaultKinds } from '../pi-app-kinds/pi-app-kinds';\nimport { imageKind } from '../viewer-image/image-kind';\nimport { pdfKind } from '../viewer-pdf/pdf-kind';\nimport { ViewerFrame } from '../viewer-frame/viewer-frame';\nimport { useViewerText } from '../viewer-frame/viewer-window';\nimport { cn } from '../utils/utils';\nimport { Revisions } from './revisions';\nimport type { DocumentsLabels } from './documents-labels';\nimport { documentsLabels, DocumentsWords } from './words';\nimport type { DocumentsLocale } from './words';\n\n/** The viewers' kinds: the pi-app default list, in English. */\nconst kinds = defaultKinds();\n\nexport interface DocumentsProps {\n /** The workspace's revision provider (the files guide's `connectRevisionProvider`, or a host's own binding). Borrowed: the host disposes it. */\n readonly provider: RevisionProvider;\n readonly locale?: DocumentsLocale | undefined;\n /** The block's own words, over `documentsLabels` (the viewers' words follow `locale`). */\n readonly labels?: Partial<DocumentsLabels> | undefined;\n /** Host viewers by kind, as for the Library (for example a canvas). */\n readonly viewers?: CustomViewers | undefined;\n readonly share?: ViewerOptions['share'];\n /** Where the host serves the bytes of large files (blob pointers): a same-origin path starting with `/`, default `/_files/`. */\n readonly blobBase?: string | undefined;\n}\n\nconst nameOf = (path: string) => path.split('/').pop() || path;\n\n/** The workspace's change feed as a counter: it moves on every change, so the listings and the revisions are read again. */\nfunction useWorkspaceVersion(provider: RevisionProvider): number {\n const [version, setVersion] = useState(0);\n useEffect(() => {\n if (!provider.changes) return undefined;\n const abort = new AbortController();\n void (async () => {\n try { for await (const change of provider.changes!({ signal: abort.signal })) { void change; setVersion(current => current + 1); } } catch { /* the binding ended the feed: the listings stay as they are */ }\n })();\n return () => abort.abort();\n }, [provider]);\n return version;\n}\n\n/** The Library's tree over the provider: created and disposed with the binding, read again when the workspace changes. */\nfunction useFileTree(provider: RevisionProvider, version: number, blobBase: string): FileTreeController | undefined {\n const [tree, setTree] = useState<FileTreeController>();\n useEffect(() => {\n const next = createFileTreeController({ revisionProvider: withBlobPointers(provider, blobBase) });\n setTree(next);\n void next.refresh();\n return () => next.dispose();\n }, [provider, blobBase]);\n useEffect(() => {\n if (!tree || version === 0) return;\n const { query, expanded } = tree.getSnapshot();\n if (query) void tree.search(query);\n else for (const directory of ['', ...expanded]) void tree.refresh(directory);\n }, [tree, version]);\n return tree;\n}\n\n/**\n * The provider as it reads one revision: every read of the returned client is the exact `revision`, over the same provider (a reload in a\n * pinned view re-reads that revision, never the latest). It is for read-only views only.\n */\nfunction pinnedProviderOf(provider: RevisionProvider, revision: string): RevisionProvider {\n return { ...provider, read: (request, signal) => provider.read({ target: request.target, revision: { kind: 'exact', value: revision } }, signal) };\n}\n\n/** An image or PDF revision, read exactly and shown in the viewer's read-only pane. */\nfunction BinaryRevision({ provider, path, revision, kind, labels, onClose }: {\n readonly provider: RevisionProvider;\n readonly path: string;\n readonly revision: string;\n readonly kind: 'image' | 'pdf';\n readonly labels: DocumentsLabels;\n readonly onClose: () => void;\n}) {\n const [state, setState] = useState<{ readonly kind: 'loading' } | { readonly kind: 'failed' } | { readonly kind: 'ready'; readonly bytes: Uint8Array; readonly mediaType: string }>({ kind: 'loading' });\n const { labels: viewer } = useViewerText();\n useEffect(() => {\n const abort = new AbortController();\n setState({ kind: 'loading' });\n provider.read({ target: provider.locate(path), revision: { kind: 'exact', value: revision } }, abort.signal).then(result => {\n if (!abort.signal.aborted) setState(result.kind === 'available' ? { kind: 'ready', bytes: result.snapshot.bytes, mediaType: result.snapshot.mediaType } : { kind: 'failed' });\n }, () => { if (!abort.signal.aborted) setState({ kind: 'failed' }); });\n return () => abort.abort();\n }, [provider, path, revision]);\n const status = { label: viewer.readOnly, kind: 'read-only' as const };\n if (state.kind !== 'ready') return <ViewerFrame title={nameOf(path)} status={status} onClose={onClose}>\n <p role=\"status\" className=\"m-0 p-4 text-sm text-muted-foreground\">{state.kind === 'loading' ? labels.revisionsLoading : labels.revisionLoadFailed}</p>\n </ViewerFrame>;\n // The panes load the first time an image or a PDF revision opens (D-257), like the file viewer's.\n const ImagePane = lazyView(imageKind.load), PdfPane = lazyView(pdfKind.load);\n return <Suspense fallback={<ViewerFrame testId=\"viewer-pending\" title={nameOf(path)} status={status} onClose={onClose}><ViewLoading label={viewer.loading} /></ViewerFrame>}>\n {kind === 'image'\n ? <ImagePane name={nameOf(path)} mediaType={state.mediaType} bytes={state.bytes} status={status} onClose={onClose} />\n : <PdfPane name={nameOf(path)} bytes={state.bytes} status={status} onClose={onClose} />}\n </Suspense>;\n}\n\n/** One revision of the file, read-only, with « Back to editing ». Text, Markdown and HTML use the artifact panel's pinned view. */\nfunction OlderVersion({ provider, path, revision, options, labels, onBack, onClose }: {\n readonly provider: RevisionProvider;\n readonly path: string;\n readonly revision: string;\n readonly options: ViewerOptions;\n readonly labels: DocumentsLabels;\n readonly onBack: () => void;\n readonly onClose: () => void;\n}) {\n const kind: ViewerKind = viewerKindOf(path);\n const pinned = useMemo(() => pinnedProviderOf(provider, revision), [provider, revision]);\n const type = kind === 'markdown' ? 'markdown' : kind === 'html' ? 'html' : kind === 'text' ? 'code' : undefined;\n const artifact: ArtifactDescriptor | undefined = type === undefined ? undefined\n : { schema: 'boring.artifact', version: 1, title: nameOf(path), type, mediaType: mediaTypeOf(path), target: provider.locate(path), revision };\n const bar = <>\n <div className=\"flex shrink-0 items-center gap-2 px-3 py-2\">\n <span role=\"status\" className=\"min-w-0 flex-1 truncate text-sm font-medium\">{labels.readOnlyRevision}</span>\n <Button variant=\"ghost\" data-testid=\"back-to-editing\" onClick={onBack}>{labels.backToEditing}</Button>\n </div>\n <Separator />\n </>;\n let body;\n if (artifact) body = <ArtifactPanel active={{ descriptor: artifact, follow: false }} versions={[]} options={{ ...options, client: pinned }} onSelect={onBack} onClose={onClose} />;\n else if (kind === 'image' || kind === 'pdf') body = <BinaryRevision provider={provider} path={path} revision={revision} kind={kind} labels={labels} onClose={onClose} />;\n else body = <ViewerFrame title={nameOf(path)} onClose={onClose}><p role=\"status\" className=\"m-0 p-4 text-sm text-muted-foreground\">{labels.unsupportedPreview}</p></ViewerFrame>;\n return <div data-testid=\"revision-view\" data-kind={kind} data-revision={revision} className=\"flex h-full min-h-0 flex-1 flex-col\">\n {bar}\n <div className=\"flex min-h-0 flex-1 flex-col\">{body}</div>\n </div>;\n}\n\nfunction DocumentDetail({ provider, path, version, options, labels, locale, older, onDirty, onOpenRevision, onBack, onClose }: {\n readonly provider: RevisionProvider;\n readonly path: string;\n readonly version: number;\n readonly options: ViewerOptions;\n readonly labels: DocumentsLabels;\n readonly locale: DocumentsLocale;\n /** The older revision on show, `undefined` for the editable file. */\n readonly older: string | undefined;\n readonly onDirty: (unsaved: boolean) => void;\n readonly onOpenRevision: (revision: string) => void;\n readonly onBack: () => void;\n readonly onClose: () => void;\n}) {\n return <div data-testid=\"documents-detail\" className=\"flex h-full min-h-0 flex-col md:flex-row\">\n <div className=\"flex min-h-0 min-w-0 flex-1 flex-col\">\n {/* The editor stays mounted (hidden) while a revision is on show, so its unsaved edits are kept. */}\n <div className={cn('min-h-0 flex-1 flex-col', older === undefined ? 'flex' : 'hidden')}>\n <FileViewer path={path} locator={provider.locate(path)} options={options} onClose={onClose} onUnsavedChange={onDirty} />\n </div>\n {older !== undefined && <OlderVersion provider={provider} path={path} revision={older} options={options} labels={labels} onBack={onBack} onClose={onClose} />}\n </div>\n <Separator className=\"md:hidden\" />\n <Separator orientation=\"vertical\" className=\"max-md:hidden\" />\n <aside className=\"flex max-h-[40%] min-h-0 shrink-0 flex-col overflow-hidden md:max-h-none md:w-72\">\n <Revisions provider={provider} path={path} version={version} selected={older} locale={locale} labels={labels} onOpen={onOpenRevision} />\n </aside>\n </div>;\n}\n\nexport function Documents({ provider, locale, labels: given, viewers, share, blobBase: givenBase }: DocumentsProps) {\n const labels = documentsLabels(locale, given);\n const language: DocumentsLocale = locale === 'fr' ? 'fr' : 'en';\n const version = useWorkspaceVersion(provider);\n const blobBase = checkBlobBase(givenBase);\n const tree = useFileTree(provider, version, blobBase);\n const [path, setPath] = useState<string>();\n // An older revision of the file on show (`undefined`: the editable file).\n const [pinned, setPinned] = useState<{ readonly path: string; readonly revision: string }>();\n // The file's unsaved edits, as its viewer reports them. A change of file or a close that would drop them asks first.\n const dirty = useRef(false);\n const onDirty = useCallback((unsaved: boolean) => { dirty.current = unsaved; }, []);\n const mayLeave = () => !dirty.current || window.confirm(labels.discardChanges);\n const options = useMemo<ViewerOptions>(() => ({ client: provider, identity: provider.identity, kinds, blobBase, ...(viewers ? { viewers } : {}), ...(share ? { share } : {}) }), [provider, viewers, share, blobBase]);\n const choose = (next: string) => {\n if (next === path || !mayLeave()) return;\n setPath(next);\n setPinned(undefined);\n };\n const closeFile = () => { if (!mayLeave()) return; setPath(undefined); setPinned(undefined); };\n // The file on show was renamed, moved or deleted from the list: follow it to its new path, or close it (its old path is gone).\n const renamed = (from: string, to: string) => { setPath(current => current === from ? to : current); setPinned(undefined); };\n const deleted = (gone: string) => { setPath(current => { if (current !== gone) return current; dirty.current = false; return undefined; }); setPinned(undefined); };\n const list = tree ? <div className=\"min-h-0\">\n <FileTreeView controller={tree} onOpen={choose} onRenamed={renamed} onDeleted={deleted} selectedPath={path} locale={language} upload={false} history={false} />\n </div> : null;\n const older = path !== undefined && pinned?.path === path ? pinned.revision : undefined;\n // `null` (nothing chosen) is what lets a phone show the list alone: ListDetail stacks the detail only when there is one.\n const detail = path === undefined ? null\n : <DocumentDetail key={path} provider={provider} path={path} version={version} options={options} labels={labels} locale={language} older={older}\n onDirty={onDirty} onOpenRevision={revision => setPinned({ path, revision })} onBack={() => setPinned(undefined)} onClose={closeFile} />;\n return <DocumentsWords locale={locale}>\n <ListDetail list={list} detail={detail} onBack={closeFile} />\n </DocumentsWords>;\n}\n",
|
|
32
|
+
"content": "'use client';\n\n// The documents screen: the workspace's Library is the list (`ListDetail`: beside the file on a wide screen, one pane at a time on a phone);\n// the chosen file opens in the same viewer the Library uses (editable where it edits); its Revisions sit beside it. Every revision row opens\n// that exact revision read-only, and « Back to editing » returns to the file. The editor stays mounted while a revision is inspected, so its\n// unsaved edits survive; leaving the file with unsaved edits asks first. Every read goes through the one `revisionProvider` the host binds.\nimport { Suspense, useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport { Button } from '@/components/ui/button';\nimport { Separator } from '@/components/ui/separator';\nimport { createFileTreeController } from '@hachej/boring-ui-kit/file-tree';\nimport type { FileTreeController } from '@hachej/boring-ui-kit/file-tree';\nimport { FileTreeView } from '../file-tree/file-tree';\nimport type { RevisionProvider } from '@hachej/boring-files/revision';\nimport { mediaTypeOf, viewerKindOf } from '@hachej/boring-files/file-types';\nimport { checkBlobBase, withBlobPointers } from '@hachej/boring-files/blob';\nimport type { ViewerKind } from '@hachej/boring-files/file-types';\nimport { ListDetail } from '../app-shell/list-detail';\nimport { ArtifactPanel } from '../pi-app/artifact-panel';\nimport type { ArtifactDescriptor } from '../pi-artifacts/artifact';\nimport type { CustomViewers, ViewerOptions } from '../pi-app/artifact-panel';\nimport { FileViewer } from '../pi-app/file-viewer';\nimport { lazyView, ViewLoading } from '../pi-app/viewer-kinds';\nimport { defaultKinds } from '../pi-app-kinds/pi-app-kinds';\nimport { imageKind } from '../viewer-image/image-kind';\nimport { pdfKind } from '../viewer-pdf/pdf-kind';\nimport { ViewerFrame } from '../viewer-frame/viewer-frame';\nimport { useViewerText } from '../viewer-frame/viewer-window';\nimport { cn } from '../utils/utils';\nimport { Revisions } from './revisions';\nimport type { DocumentsLabels } from './documents-labels';\nimport { documentsLabels, DocumentsWords } from './words';\nimport type { DocumentsLocale } from './words';\n\nexport interface DocumentsProps {\n /** The workspace's revision provider (the files guide's `connectRevisionProvider`, or a host's own binding). Borrowed: the host disposes it. */\n readonly provider: RevisionProvider;\n readonly locale?: DocumentsLocale | undefined;\n /** The block's own words, over `documentsLabels` (the viewers' words follow `locale`). */\n readonly labels?: Partial<DocumentsLabels> | undefined;\n /** Host viewers by kind, as for the Library (for example a canvas). */\n readonly viewers?: CustomViewers | undefined;\n readonly share?: ViewerOptions['share'];\n /** Where the host serves the bytes of large files (blob pointers): a same-origin path starting with `/`, default `/_files/`. */\n readonly blobBase?: string | undefined;\n}\n\nconst nameOf = (path: string) => path.split('/').pop() || path;\n\n/** The workspace's change feed as a counter: it moves on every change, so the listings and the revisions are read again. */\nfunction useWorkspaceVersion(provider: RevisionProvider): number {\n const [version, setVersion] = useState(0);\n useEffect(() => {\n if (!provider.changes) return undefined;\n const abort = new AbortController();\n void (async () => {\n try { for await (const change of provider.changes!({ signal: abort.signal })) { void change; setVersion(current => current + 1); } } catch { /* the binding ended the feed: the listings stay as they are */ }\n })();\n return () => abort.abort();\n }, [provider]);\n return version;\n}\n\n/** The Library's tree over the provider: created and disposed with the binding, read again when the workspace changes. */\nfunction useFileTree(provider: RevisionProvider, version: number, blobBase: string): FileTreeController | undefined {\n const [tree, setTree] = useState<FileTreeController>();\n useEffect(() => {\n const next = createFileTreeController({ revisionProvider: withBlobPointers(provider, blobBase) });\n setTree(next);\n void next.refresh();\n return () => next.dispose();\n }, [provider, blobBase]);\n useEffect(() => {\n if (!tree || version === 0) return;\n const { query, expanded } = tree.getSnapshot();\n if (query) void tree.search(query);\n else for (const directory of ['', ...expanded]) void tree.refresh(directory);\n }, [tree, version]);\n return tree;\n}\n\n/**\n * The provider as it reads one revision: every read of the returned client is the exact `revision`, over the same provider (a reload in a\n * pinned view re-reads that revision, never the latest). It is for read-only views only.\n */\nfunction pinnedProviderOf(provider: RevisionProvider, revision: string): RevisionProvider {\n return { ...provider, read: (request, signal) => provider.read({ target: request.target, revision: { kind: 'exact', value: revision } }, signal) };\n}\n\n/** An image or PDF revision, read exactly and shown in the viewer's read-only pane. */\nfunction BinaryRevision({ provider, path, revision, kind, labels, onClose }: {\n readonly provider: RevisionProvider;\n readonly path: string;\n readonly revision: string;\n readonly kind: 'image' | 'pdf';\n readonly labels: DocumentsLabels;\n readonly onClose: () => void;\n}) {\n const [state, setState] = useState<{ readonly kind: 'loading' } | { readonly kind: 'failed' } | { readonly kind: 'ready'; readonly bytes: Uint8Array; readonly mediaType: string }>({ kind: 'loading' });\n const { labels: viewer } = useViewerText();\n useEffect(() => {\n const abort = new AbortController();\n setState({ kind: 'loading' });\n provider.read({ target: provider.locate(path), revision: { kind: 'exact', value: revision } }, abort.signal).then(result => {\n if (!abort.signal.aborted) setState(result.kind === 'available' ? { kind: 'ready', bytes: result.snapshot.bytes, mediaType: result.snapshot.mediaType } : { kind: 'failed' });\n }, () => { if (!abort.signal.aborted) setState({ kind: 'failed' }); });\n return () => abort.abort();\n }, [provider, path, revision]);\n const status = { label: viewer.readOnly, kind: 'read-only' as const };\n if (state.kind !== 'ready') return <ViewerFrame title={nameOf(path)} status={status} onClose={onClose}>\n <p role=\"status\" className=\"m-0 p-4 text-sm text-muted-foreground\">{state.kind === 'loading' ? labels.revisionsLoading : labels.revisionLoadFailed}</p>\n </ViewerFrame>;\n // The panes load the first time an image or a PDF revision opens (D-257), like the file viewer's.\n const ImagePane = lazyView(imageKind.load), PdfPane = lazyView(pdfKind.load);\n return <Suspense fallback={<ViewerFrame testId=\"viewer-pending\" title={nameOf(path)} status={status} onClose={onClose}><ViewLoading label={viewer.loading} /></ViewerFrame>}>\n {kind === 'image'\n ? <ImagePane name={nameOf(path)} mediaType={state.mediaType} bytes={state.bytes} status={status} onClose={onClose} />\n : <PdfPane name={nameOf(path)} bytes={state.bytes} status={status} onClose={onClose} />}\n </Suspense>;\n}\n\n/** One revision of the file, read-only, with « Back to editing ». Text, Markdown and HTML use the artifact panel's pinned view. */\nfunction OlderVersion({ provider, path, revision, options, labels, onBack, onClose }: {\n readonly provider: RevisionProvider;\n readonly path: string;\n readonly revision: string;\n readonly options: ViewerOptions;\n readonly labels: DocumentsLabels;\n readonly onBack: () => void;\n readonly onClose: () => void;\n}) {\n const kind: ViewerKind = viewerKindOf(path);\n const pinned = useMemo(() => pinnedProviderOf(provider, revision), [provider, revision]);\n const type = kind === 'markdown' ? 'markdown' : kind === 'html' ? 'html' : kind === 'text' ? 'code' : undefined;\n const artifact: ArtifactDescriptor | undefined = type === undefined ? undefined\n : { schema: 'boring.artifact', version: 1, title: nameOf(path), type, mediaType: mediaTypeOf(path), target: provider.locate(path), revision };\n const bar = <>\n <div className=\"flex shrink-0 items-center gap-2 px-3 py-2\">\n <span role=\"status\" className=\"min-w-0 flex-1 truncate text-sm font-medium\">{labels.readOnlyRevision}</span>\n <Button variant=\"ghost\" data-testid=\"back-to-editing\" onClick={onBack}>{labels.backToEditing}</Button>\n </div>\n <Separator />\n </>;\n let body;\n if (artifact) body = <ArtifactPanel active={{ descriptor: artifact, follow: false }} versions={[]} options={{ ...options, client: pinned }} onSelect={onBack} onClose={onClose} />;\n else if (kind === 'image' || kind === 'pdf') body = <BinaryRevision provider={provider} path={path} revision={revision} kind={kind} labels={labels} onClose={onClose} />;\n else body = <ViewerFrame title={nameOf(path)} onClose={onClose}><p role=\"status\" className=\"m-0 p-4 text-sm text-muted-foreground\">{labels.unsupportedPreview}</p></ViewerFrame>;\n return <div data-testid=\"revision-view\" data-kind={kind} data-revision={revision} className=\"flex h-full min-h-0 flex-1 flex-col\">\n {bar}\n <div className=\"flex min-h-0 flex-1 flex-col\">{body}</div>\n </div>;\n}\n\nfunction DocumentDetail({ provider, path, version, options, labels, locale, older, onDirty, onOpenRevision, onBack, onClose }: {\n readonly provider: RevisionProvider;\n readonly path: string;\n readonly version: number;\n readonly options: ViewerOptions;\n readonly labels: DocumentsLabels;\n readonly locale: DocumentsLocale;\n /** The older revision on show, `undefined` for the editable file. */\n readonly older: string | undefined;\n readonly onDirty: (unsaved: boolean) => void;\n readonly onOpenRevision: (revision: string) => void;\n readonly onBack: () => void;\n readonly onClose: () => void;\n}) {\n return <div data-testid=\"documents-detail\" className=\"flex h-full min-h-0 flex-col md:flex-row\">\n <div className=\"flex min-h-0 min-w-0 flex-1 flex-col\">\n {/* The editor stays mounted (hidden) while a revision is on show, so its unsaved edits are kept. */}\n <div className={cn('min-h-0 flex-1 flex-col', older === undefined ? 'flex' : 'hidden')}>\n <FileViewer path={path} locator={provider.locate(path)} options={options} onClose={onClose} onUnsavedChange={onDirty} />\n </div>\n {older !== undefined && <OlderVersion provider={provider} path={path} revision={older} options={options} labels={labels} onBack={onBack} onClose={onClose} />}\n </div>\n <Separator className=\"md:hidden\" />\n <Separator orientation=\"vertical\" className=\"max-md:hidden\" />\n <aside className=\"flex max-h-[40%] min-h-0 shrink-0 flex-col overflow-hidden md:max-h-none md:w-72\">\n <Revisions provider={provider} path={path} version={version} selected={older} locale={locale} labels={labels} onOpen={onOpenRevision} />\n </aside>\n </div>;\n}\n\nexport function Documents({ provider, locale, labels: given, viewers, share, blobBase: givenBase }: DocumentsProps) {\n const labels = documentsLabels(locale, given);\n const language: DocumentsLocale = locale === 'fr' ? 'fr' : 'en';\n // The viewers' kinds: the pi-app default list in the item's locale.\n const kinds = useMemo(() => defaultKinds({ locale: language }), [language]);\n const version = useWorkspaceVersion(provider);\n const blobBase = checkBlobBase(givenBase);\n const tree = useFileTree(provider, version, blobBase);\n const [path, setPath] = useState<string>();\n // An older revision of the file on show (`undefined`: the editable file).\n const [pinned, setPinned] = useState<{ readonly path: string; readonly revision: string }>();\n // The file's unsaved edits, as its viewer reports them. A change of file or a close that would drop them asks first.\n const dirty = useRef(false);\n const onDirty = useCallback((unsaved: boolean) => { dirty.current = unsaved; }, []);\n const mayLeave = () => !dirty.current || window.confirm(labels.discardChanges);\n const options = useMemo<ViewerOptions>(() => ({ client: provider, identity: provider.identity, kinds, blobBase, ...(viewers ? { viewers } : {}), ...(share ? { share } : {}) }), [provider, kinds, viewers, share, blobBase]);\n const choose = (next: string) => {\n if (next === path || !mayLeave()) return;\n setPath(next);\n setPinned(undefined);\n };\n const closeFile = () => { if (!mayLeave()) return; setPath(undefined); setPinned(undefined); };\n // The file on show was renamed, moved or deleted from the list: follow it to its new path, or close it (its old path is gone).\n const renamed = (from: string, to: string) => { setPath(current => current === from ? to : current); setPinned(undefined); };\n const deleted = (gone: string) => { setPath(current => { if (current !== gone) return current; dirty.current = false; return undefined; }); setPinned(undefined); };\n const list = tree ? <div className=\"min-h-0\">\n <FileTreeView controller={tree} onOpen={choose} onRenamed={renamed} onDeleted={deleted} selectedPath={path} locale={language} upload={false} history={false} />\n </div> : null;\n const older = path !== undefined && pinned?.path === path ? pinned.revision : undefined;\n // `null` (nothing chosen) is what lets a phone show the list alone: ListDetail stacks the detail only when there is one.\n const detail = path === undefined ? null\n : <DocumentDetail key={path} provider={provider} path={path} version={version} options={options} labels={labels} locale={language} older={older}\n onDirty={onDirty} onOpenRevision={revision => setPinned({ path, revision })} onBack={() => setPinned(undefined)} onClose={closeFile} />;\n return <DocumentsWords locale={locale}>\n <ListDetail list={list} detail={detail} onBack={closeFile} />\n </DocumentsWords>;\n}\n",
|
|
33
33
|
"type": "registry:component",
|
|
34
34
|
"target": "components/documents/documents.tsx"
|
|
35
35
|
},
|
|
36
36
|
{
|
|
37
37
|
"path": "registry/documents/document-sheet.tsx",
|
|
38
|
-
"content": "'use client';\n\n// One document over the app, for « Voir la fiche »: the host sets `path` from its own handler (for example its `onOpenSpec`) and clears it in\n// `onClose`. The sheet is a full-screen view of the shell (`useShellLayer`): it closes the agent layers when it opens, and Back closes it\n// first. The file is the same viewer the Library opens (editable where the viewer edits). Closing a file with unsaved edits asks first.\n// `onShowAll` adds « Toutes les fiches »: the sheet closes, then the host's navigation runs after the sheet's Back entry is gone.\nimport { useCallback, useEffect, useRef } from 'react';\nimport { Button } from '@/components/ui/button';\nimport { Drawer, DrawerContent, DrawerTitle } from '@/components/ui/drawer';\nimport { afterBack } from '../app-shell/back';\nimport { useShellLayer } from '../app-shell/shell-layer';\nimport { useExclusiveSheet } from '../app-shell/use-exclusive-sheet';\nimport { FileViewer } from '../pi-app/file-viewer';\nimport type { CustomViewers, ViewerOptions } from '../pi-app/artifact-panel';\nimport { defaultKinds } from '../pi-app-kinds/pi-app-kinds';\nimport type { RevisionProvider } from '@hachej/boring-files/revision';\nimport { cn } from '../utils/utils';\nimport { documentsLabels, DocumentsWords } from './words';\nimport type { DocumentsLocale } from './words';\nimport type { DocumentsLabels } from './documents-labels';\n\nexport interface DocumentSheetProps {\n readonly provider: RevisionProvider;\n /** The workspace path of the document on show; `undefined` closes the sheet. */\n readonly path: string | undefined;\n /** Called when the sheet closes (its close button, a swipe, Back, « Toutes les fiches »): the host clears `path`. */\n readonly onClose: () => void;\n /** Adds « Toutes les fiches » to the sheet. The sheet closes first, then this runs. */\n readonly onShowAll?: (() => void) | undefined;\n readonly locale?: DocumentsLocale | undefined;\n readonly labels?: Partial<DocumentsLabels> | undefined;\n /** Host viewers by kind, as for the Library (for example a canvas). */\n readonly viewers?: CustomViewers | undefined;\n readonly share?: ViewerOptions['share'];\n readonly className?: string | undefined;\n}\n\n/** The sheet's name in the shell's layers: one sheet of this kind at a time. */\nconst LAYER = 'document';\
|
|
38
|
+
"content": "'use client';\n\n// One document over the app, for « Voir la fiche »: the host sets `path` from its own handler (for example its `onOpenSpec`) and clears it in\n// `onClose`. The sheet is a full-screen view of the shell (`useShellLayer`): it closes the agent layers when it opens, and Back closes it\n// first. The file is the same viewer the Library opens (editable where the viewer edits). Closing a file with unsaved edits asks first.\n// `onShowAll` adds « Toutes les fiches »: the sheet closes, then the host's navigation runs after the sheet's Back entry is gone.\nimport { useCallback, useEffect, useMemo, useRef } from 'react';\nimport { Button } from '@/components/ui/button';\nimport { Drawer, DrawerContent, DrawerTitle } from '@/components/ui/drawer';\nimport { afterBack } from '../app-shell/back';\nimport { useShellLayer } from '../app-shell/shell-layer';\nimport { useExclusiveSheet } from '../app-shell/use-exclusive-sheet';\nimport { FileViewer } from '../pi-app/file-viewer';\nimport type { CustomViewers, ViewerOptions } from '../pi-app/artifact-panel';\nimport { defaultKinds } from '../pi-app-kinds/pi-app-kinds';\nimport type { RevisionProvider } from '@hachej/boring-files/revision';\nimport { cn } from '../utils/utils';\nimport { documentsLabels, DocumentsWords } from './words';\nimport type { DocumentsLocale } from './words';\nimport type { DocumentsLabels } from './documents-labels';\n\nexport interface DocumentSheetProps {\n readonly provider: RevisionProvider;\n /** The workspace path of the document on show; `undefined` closes the sheet. */\n readonly path: string | undefined;\n /** Called when the sheet closes (its close button, a swipe, Back, « Toutes les fiches »): the host clears `path`. */\n readonly onClose: () => void;\n /** Adds « Toutes les fiches » to the sheet. The sheet closes first, then this runs. */\n readonly onShowAll?: (() => void) | undefined;\n readonly locale?: DocumentsLocale | undefined;\n readonly labels?: Partial<DocumentsLabels> | undefined;\n /** Host viewers by kind, as for the Library (for example a canvas). */\n readonly viewers?: CustomViewers | undefined;\n readonly share?: ViewerOptions['share'];\n readonly className?: string | undefined;\n}\n\n/** The sheet's name in the shell's layers: one sheet of this kind at a time. */\nconst LAYER = 'document';\nexport function DocumentSheet({ provider, path, onClose, onShowAll, locale, labels: given, viewers, share, className }: DocumentSheetProps) {\n const shown = path !== undefined;\n const labels = documentsLabels(locale, given);\n // The viewer's kinds: the pi-app default list in the item's locale.\n const kinds = useMemo(() => defaultKinds({ locale: locale === 'fr' ? 'fr' : 'en' }), [locale]);\n // The viewer reports its unsaved edits; a close that would drop them asks first (the Back path too, through the shell layer).\n const dirty = useRef(false);\n const report = useCallback((unsaved: boolean) => { dirty.current = unsaved; }, []);\n const mayLeave = () => !dirty.current || window.confirm(labels.discardChanges);\n const { open, close, isOpen } = useShellLayer(LAYER, { confirmBack: mayLeave });\n // The host's `path` decides whether the sheet is on show; a close from the sheet itself reports to the host.\n useEffect(() => { if (shown) open(); else close(); }, [shown, open, close]);\n useExclusiveSheet(isOpen, close);\n const wasOpen = useRef(false);\n useEffect(() => {\n if (shown && wasOpen.current && !isOpen) onClose();\n wasOpen.current = isOpen;\n }, [isOpen, shown, onClose]);\n const requestClose = () => { if (mayLeave()) close(); };\n const title = path === undefined ? labels.sheetTitle : path.split('/').pop() || path;\n return <DocumentsWords locale={locale}>\n <Drawer open={isOpen} onOpenChange={next => { if (!next) requestClose(); }} showSwipeHandle swipeDirection=\"down\">\n <DrawerContent data-testid=\"document-sheet\" className={cn('app-shell max-h-[92dvh] min-h-[92dvh]', className)}>\n {/* The title is for assistive technology: the viewer's own bar shows the file name. */}\n <DrawerTitle className=\"sr-only\">{title}</DrawerTitle>\n {onShowAll && <div className=\"flex shrink-0 justify-end px-3 pt-1 pb-2\">\n <Button variant=\"ghost\" data-testid=\"document-sheet-all\" onClick={() => { if (!mayLeave()) return; close(); afterBack(onShowAll); }}>{labels.allDocuments}</Button>\n </div>}\n <div className=\"flex min-h-0 flex-1 flex-col overflow-hidden pb-[env(safe-area-inset-bottom)]\">\n {path !== undefined && <FileViewer key={path} path={path} locator={provider.locate(path)} onUnsavedChange={report}\n options={{ client: provider, identity: provider.identity, kinds, ...(viewers ? { viewers } : {}), ...(share ? { share } : {}) }} onClose={requestClose} />}\n </div>\n </DrawerContent>\n </Drawer>\n </DocumentsWords>;\n}\n",
|
|
39
39
|
"type": "registry:component",
|
|
40
40
|
"target": "components/documents/document-sheet.tsx"
|
|
41
41
|
},
|
package/r/feedback-preview.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"title": "Feedback preview",
|
|
5
5
|
"description": "PreviewBanner: a live, reversible preview of a change in the page (Approve / Discard) over `@hachej/boring-feedback/preview`, whose subagent calls models through a host gateway. Separate from `feedback` so annotation-only apps do not install the model SDK; install `feedback` first (its styles cover the banner).",
|
|
6
6
|
"dependencies": [
|
|
7
|
-
"@hachej/boring-feedback@^0.4.
|
|
7
|
+
"@hachej/boring-feedback@^0.4.25",
|
|
8
8
|
"@earendil-works/pi-ai@~1.2.0",
|
|
9
9
|
"react@^19.0.0"
|
|
10
10
|
],
|
package/r/feedback.json
CHANGED
|
@@ -4,17 +4,17 @@
|
|
|
4
4
|
"title": "Feedback",
|
|
5
5
|
"description": "Feedback from the chat composer: one Feedback button starts feedback mode on the application page (hover, click to pin a numbered note, hold ⌥ to use the app, optional voice), and one chip carries the notes to the next message. FeedbackBar, NoteBubble, FeedbackChip with its review, useComposerFeedback for the pi-chat composer, and PickerOverlay styles. Optional, not mounted by default: PointButton, AnnotateSheet, FeedbackList and FeedbackReport. Picking, masking, anchoring and placement stay in @hachej/boring-feedback/page and /ui, so restyling cannot weaken them.",
|
|
6
6
|
"dependencies": [
|
|
7
|
-
"@hachej/boring-feedback@^0.4.
|
|
8
|
-
"@hachej/boring-ui-kit@^0.4.
|
|
9
|
-
"@hachej/boring-files@^0.4.
|
|
7
|
+
"@hachej/boring-feedback@^0.4.25",
|
|
8
|
+
"@hachej/boring-ui-kit@^0.4.25",
|
|
9
|
+
"@hachej/boring-files@^0.4.25",
|
|
10
10
|
"react@^19.0.0",
|
|
11
11
|
"react-dom@^19.0.0",
|
|
12
12
|
"lucide-react@^1.51.0"
|
|
13
13
|
],
|
|
14
14
|
"registryDependencies": [
|
|
15
|
-
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.
|
|
16
|
-
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.
|
|
17
|
-
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.
|
|
15
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.25/r/pi-chat.json",
|
|
16
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.25/r/pi-chat-voice.json",
|
|
17
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.25/r/utils.json",
|
|
18
18
|
"button",
|
|
19
19
|
"toggle",
|
|
20
20
|
"badge",
|
package/r/file-tree.json
CHANGED
|
@@ -4,15 +4,15 @@
|
|
|
4
4
|
"title": "File tree",
|
|
5
5
|
"description": "Browse, search, upload, rename, move, delete and inspect retained versions through one revisionProvider binding. Uses the consumer's shadcn Button, dropdown-menu and dialog with shared version-history rows; Base UI style only.",
|
|
6
6
|
"dependencies": [
|
|
7
|
-
"@hachej/boring-ui-kit@^0.4.
|
|
8
|
-
"@hachej/boring-files@^0.4.
|
|
7
|
+
"@hachej/boring-ui-kit@^0.4.25",
|
|
8
|
+
"@hachej/boring-files@^0.4.25",
|
|
9
9
|
"react@^19.0.0",
|
|
10
10
|
"lucide-react@^1.51.0"
|
|
11
11
|
],
|
|
12
12
|
"registryDependencies": [
|
|
13
|
-
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.
|
|
14
|
-
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.
|
|
15
|
-
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.
|
|
13
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.25/r/theme.json",
|
|
14
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.25/r/utils.json",
|
|
15
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.25/r/version-history.json",
|
|
16
16
|
"button",
|
|
17
17
|
"dialog",
|
|
18
18
|
"dropdown-menu",
|
package/r/highlights.json
CHANGED
|
@@ -4,13 +4,13 @@
|
|
|
4
4
|
"title": "Anchored highlights",
|
|
5
5
|
"description": "Exact Markdown highlights and comments in a conditional workspace sidecar.",
|
|
6
6
|
"dependencies": [
|
|
7
|
-
"@hachej/boring-ui-kit@^0.4.
|
|
8
|
-
"@hachej/boring-files@^0.4.
|
|
7
|
+
"@hachej/boring-ui-kit@^0.4.25",
|
|
8
|
+
"@hachej/boring-files@^0.4.25",
|
|
9
9
|
"react@^19.0.0",
|
|
10
10
|
"react-dom@^19.0.0"
|
|
11
11
|
],
|
|
12
12
|
"registryDependencies": [
|
|
13
|
-
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.
|
|
13
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.25/r/markdown-editor.json",
|
|
14
14
|
"button",
|
|
15
15
|
"textarea",
|
|
16
16
|
"label",
|
package/r/html-viewer.json
CHANGED
|
@@ -4,13 +4,13 @@
|
|
|
4
4
|
"title": "HTML source and passive preview",
|
|
5
5
|
"description": "Exact HTML source editing with a reconstructed passive preview and borrowed controller. Browser security qualification remains separate.",
|
|
6
6
|
"dependencies": [
|
|
7
|
-
"@hachej/boring-ui-kit@^0.4.
|
|
8
|
-
"@hachej/boring-files@^0.4.
|
|
7
|
+
"@hachej/boring-ui-kit@^0.4.25",
|
|
8
|
+
"@hachej/boring-files@^0.4.25",
|
|
9
9
|
"react@^19.0.0",
|
|
10
10
|
"react-dom@^19.0.0"
|
|
11
11
|
],
|
|
12
12
|
"registryDependencies": [
|
|
13
|
-
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.
|
|
13
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.25/r/button.json"
|
|
14
14
|
],
|
|
15
15
|
"files": [
|
|
16
16
|
{
|
package/r/looks-viewer.json
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"lucide-react@^1.51.0"
|
|
9
9
|
],
|
|
10
10
|
"registryDependencies": [
|
|
11
|
-
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.
|
|
11
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.25/r/mockup-viewer.json",
|
|
12
12
|
"alert",
|
|
13
13
|
"button",
|
|
14
14
|
"card",
|
package/r/maquette-viewer.json
CHANGED
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
"lucide-react@^1.51.0"
|
|
9
9
|
],
|
|
10
10
|
"registryDependencies": [
|
|
11
|
-
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.
|
|
12
|
-
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.
|
|
11
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.25/r/theme.json",
|
|
12
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.25/r/diagram-viewer.json",
|
|
13
13
|
"alert",
|
|
14
14
|
"badge",
|
|
15
15
|
"bubble",
|
package/r/markdown-editor.json
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
"title": "Markdown editor",
|
|
5
5
|
"description": "A host-owned styled wrapper over the concrete borrowed Markdown controller.",
|
|
6
6
|
"dependencies": [
|
|
7
|
-
"@hachej/boring-ui-kit@^0.4.
|
|
8
|
-
"@hachej/boring-files@^0.4.
|
|
7
|
+
"@hachej/boring-ui-kit@^0.4.25",
|
|
8
|
+
"@hachej/boring-files@^0.4.25",
|
|
9
9
|
"react@^19.0.0",
|
|
10
10
|
"react-dom@^19.0.0",
|
|
11
11
|
"marked@^17.0.6",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"lucide-react@^1.51.0"
|
|
25
25
|
],
|
|
26
26
|
"registryDependencies": [
|
|
27
|
-
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.
|
|
27
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.25/r/button.json",
|
|
28
28
|
"button",
|
|
29
29
|
"toggle",
|
|
30
30
|
"dropdown-menu"
|
package/r/mockup-viewer.json
CHANGED