@hachej/boring-ui-kit 0.3.2 → 0.4.1
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/bin/boring-ui-blocks.mjs +286 -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.d.ts +18 -0
- package/dist/remote-chat.js +149 -7
- 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 +8 -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 +30 -24
- package/r/pi-chat.json +101 -70
- package/r/pi-workspace.json +7 -5
- package/r/provider-setup.json +9 -4
- package/r/registry.json +471 -893
- 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/dist/remote-chat.js
CHANGED
|
@@ -37,8 +37,31 @@ export async function createRemoteChat(options) {
|
|
|
37
37
|
target.searchParams.set(key, value);
|
|
38
38
|
return target;
|
|
39
39
|
};
|
|
40
|
+
// The transport in use: `websocket` until its first socket fails to open, then `http` for good.
|
|
41
|
+
let mode = options.transport ?? 'http', current, seq = 0;
|
|
42
|
+
/** Calls sent over a socket and not answered yet, by call ID: sent again unchanged on the next socket. */
|
|
43
|
+
const outbox = new Map();
|
|
44
|
+
function exchange(op, init, extra) {
|
|
45
|
+
const http = () => new Request(url(op, extra), init);
|
|
46
|
+
if (mode !== 'websocket')
|
|
47
|
+
return send(http());
|
|
48
|
+
const id = `c${++seq}`;
|
|
49
|
+
const text = JSON.stringify({ kind: 'call', id, op, params: extra, ...(typeof init.body === 'string' ? { body: JSON.parse(init.body) } : {}) });
|
|
50
|
+
return new Promise((resolve, reject) => { outbox.set(id, { text, http, resolve, reject }); try {
|
|
51
|
+
current?.send(text);
|
|
52
|
+
}
|
|
53
|
+
catch { /* sent again on the next socket */ } });
|
|
54
|
+
}
|
|
55
|
+
/** Leave the socket for HTTP: calls still waiting for a socket go over HTTP, unchanged (same request ID, so the host deduplicates). */
|
|
56
|
+
function fallBack() {
|
|
57
|
+
mode = 'http';
|
|
58
|
+
const pending = [...outbox.values()];
|
|
59
|
+
outbox.clear();
|
|
60
|
+
for (const call of pending)
|
|
61
|
+
send(call.http()).then(call.resolve, call.reject);
|
|
62
|
+
}
|
|
40
63
|
async function call(op, init = {}, extra = {}) {
|
|
41
|
-
const response = await
|
|
64
|
+
const response = await exchange(op, init, extra);
|
|
42
65
|
if (response.status === 402 && op === 'submit') {
|
|
43
66
|
const refused = await response.json().catch(() => ({}));
|
|
44
67
|
if (refused.reason === 'submission-refused')
|
|
@@ -50,13 +73,107 @@ export async function createRemoteChat(options) {
|
|
|
50
73
|
}
|
|
51
74
|
const post = (op, value) => call(op, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(value) });
|
|
52
75
|
const lookup = async (requestId) => (await call('submission', {}, { requestId })).record ?? undefined;
|
|
53
|
-
|
|
76
|
+
const ping = options.webSocket?.pingMs ?? 15_000;
|
|
77
|
+
const openSocket = options.webSocket?.open ?? ((target) => new WebSocket(target, 'boring.chat'));
|
|
78
|
+
function socketSource() {
|
|
79
|
+
return new Promise((ready, refuse) => {
|
|
80
|
+
const target = new URL(base);
|
|
81
|
+
target.protocol = target.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
82
|
+
let socket;
|
|
83
|
+
try {
|
|
84
|
+
socket = openSocket(target);
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
refuse(error);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const frames = [];
|
|
91
|
+
let wake, closed = false, opened = false, heard = Date.now();
|
|
92
|
+
const notify = () => { const resume = wake; wake = undefined; resume?.(); };
|
|
93
|
+
const finish = () => {
|
|
94
|
+
if (closed)
|
|
95
|
+
return;
|
|
96
|
+
closed = true;
|
|
97
|
+
clearInterval(timer);
|
|
98
|
+
if (current === socket)
|
|
99
|
+
current = undefined;
|
|
100
|
+
notify();
|
|
101
|
+
if (!opened)
|
|
102
|
+
refuse(new Error('Remote chat socket closed before its first view'));
|
|
103
|
+
};
|
|
104
|
+
const timer = setInterval(() => {
|
|
105
|
+
if (Date.now() - heard > ping * 2.5) {
|
|
106
|
+
try {
|
|
107
|
+
socket.close();
|
|
108
|
+
}
|
|
109
|
+
catch { /* closing */ }
|
|
110
|
+
finish();
|
|
111
|
+
}
|
|
112
|
+
else if (socket.readyState === 1)
|
|
113
|
+
socket.send('ping');
|
|
114
|
+
}, ping);
|
|
115
|
+
const next = async () => { while (frames.length === 0 && !closed)
|
|
116
|
+
await new Promise(resolve => { wake = resolve; }); return frames.shift(); };
|
|
117
|
+
const cancel = () => { try {
|
|
118
|
+
socket.close(1000);
|
|
119
|
+
}
|
|
120
|
+
catch { /* closing */ } finish(); };
|
|
121
|
+
socket.onclose = finish;
|
|
122
|
+
socket.onerror = () => { try {
|
|
123
|
+
socket.close();
|
|
124
|
+
}
|
|
125
|
+
catch { /* closing */ } finish(); };
|
|
126
|
+
socket.onmessage = event => {
|
|
127
|
+
heard = Date.now();
|
|
128
|
+
if (typeof event.data !== 'string' || event.data === 'pong')
|
|
129
|
+
return;
|
|
130
|
+
let frame;
|
|
131
|
+
try {
|
|
132
|
+
frame = JSON.parse(event.data);
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (frame.kind === 'reply') {
|
|
138
|
+
const pending = outbox.get(frame.id);
|
|
139
|
+
if (pending) {
|
|
140
|
+
outbox.delete(frame.id);
|
|
141
|
+
pending.resolve(new Response(JSON.stringify(frame.body), { status: frame.status, headers: { 'content-type': 'application/json' } }));
|
|
142
|
+
}
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (frame.kind === 'event') {
|
|
146
|
+
try {
|
|
147
|
+
options.onEvent?.(frame.event);
|
|
148
|
+
}
|
|
149
|
+
catch { /* a host listener never breaks the chat */ }
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (!opened) {
|
|
153
|
+
if (frame.kind !== 'view') {
|
|
154
|
+
cancel();
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
opened = true;
|
|
158
|
+
current = socket;
|
|
159
|
+
// Unanswered calls go again, unchanged: a submit keeps its request ID, so the host runs it once.
|
|
160
|
+
for (const pending of outbox.values())
|
|
161
|
+
socket.send(pending.text);
|
|
162
|
+
ready({ first: frame.view, next, cancel });
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
frames.push(frame);
|
|
166
|
+
notify();
|
|
167
|
+
};
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
async function httpSource() {
|
|
54
171
|
const stopped = new AbortController();
|
|
55
172
|
const response = await send(new Request(url('watch'), { signal: stopped.signal }));
|
|
56
173
|
if (!response.ok || !response.body)
|
|
57
174
|
throw new WatchRefused(`Remote chat watch failed: ${await reason(response)}`, response.status);
|
|
58
175
|
const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
|
|
59
|
-
let pending = '', staleMs = 0
|
|
176
|
+
let pending = '', staleMs = 0;
|
|
60
177
|
// Once the server has announced its heartbeat interval, a read waiting 2.5 intervals means the stream is dead even if the
|
|
61
178
|
// socket still looks open (a proxy that dropped it silently).
|
|
62
179
|
async function read() {
|
|
@@ -65,7 +182,7 @@ export async function createRemoteChat(options) {
|
|
|
65
182
|
let timer;
|
|
66
183
|
try {
|
|
67
184
|
return await Promise.race([reader.read(), new Promise((_, reject) => {
|
|
68
|
-
timer = setTimeout(() => {
|
|
185
|
+
timer = setTimeout(() => { stopped.abort(); reject(new Error('Remote chat stream went silent')); }, staleMs);
|
|
69
186
|
})]);
|
|
70
187
|
}
|
|
71
188
|
finally {
|
|
@@ -97,11 +214,32 @@ export async function createRemoteChat(options) {
|
|
|
97
214
|
stopped.abort();
|
|
98
215
|
throw new Error('Remote chat watch ended before its first view');
|
|
99
216
|
}
|
|
100
|
-
|
|
217
|
+
return { first: first.view, next, cancel: () => stopped.abort() };
|
|
218
|
+
}
|
|
219
|
+
let socketWorked = false;
|
|
220
|
+
async function watch() {
|
|
221
|
+
let source;
|
|
222
|
+
if (mode === 'websocket') {
|
|
223
|
+
try {
|
|
224
|
+
source = await socketSource();
|
|
225
|
+
socketWorked = true;
|
|
226
|
+
}
|
|
227
|
+
catch (error) {
|
|
228
|
+
if (socketWorked)
|
|
229
|
+
throw error;
|
|
230
|
+
fallBack();
|
|
231
|
+
source = await httpSource();
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
else
|
|
235
|
+
source = await httpSource();
|
|
236
|
+
const { next } = source;
|
|
237
|
+
let lost = false;
|
|
238
|
+
let value = source.first, started = false, settle, ended;
|
|
101
239
|
const closed = new Promise(resolve => { settle = resolve; });
|
|
102
240
|
const finish = (end) => { if (!ended) {
|
|
103
241
|
ended = end;
|
|
104
|
-
|
|
242
|
+
source.cancel();
|
|
105
243
|
settle(end);
|
|
106
244
|
} };
|
|
107
245
|
const handle = {
|
|
@@ -316,7 +454,11 @@ export async function createRemoteChat(options) {
|
|
|
316
454
|
// The controller only reconciles a submission by request ID; no other transaction work crosses the wire.
|
|
317
455
|
commit: async (change, _context) => change({ submissionByRequest: (_conversationId, requestId) => lookup(requestId) }),
|
|
318
456
|
abort: async () => { await post('abort', {}); },
|
|
319
|
-
entries: async (
|
|
457
|
+
entries: async (query, limit, cursor) => (await call('entries', {}, { limit: String(limit),
|
|
458
|
+
...(query.maxEntryId === undefined ? {} : { maxEntryId: String(query.maxEntryId) }),
|
|
459
|
+
...(query.minEntryId === undefined ? {} : { minEntryId: String(query.minEntryId) }),
|
|
460
|
+
...(query.order === undefined ? {} : { order: query.order }),
|
|
461
|
+
...(cursor === undefined ? {} : { cursor: JSON.stringify(cursor) }) })).page,
|
|
320
462
|
};
|
|
321
463
|
const context = Object.freeze({});
|
|
322
464
|
const close = async () => { const first = unclaimed; unclaimed = undefined; await first?.stop(); };
|
package/dist/text-buffer.js
CHANGED
|
@@ -233,8 +233,9 @@ export function createTextBuffer(options) {
|
|
|
233
233
|
return read;
|
|
234
234
|
}
|
|
235
235
|
if (sameBase(remote, state.base) && !discard) {
|
|
236
|
-
|
|
237
|
-
|
|
236
|
+
const resolved = state.save.kind === 'settled' && state.save.result.kind !== 'saved';
|
|
237
|
+
if (state.remote !== null || resolved)
|
|
238
|
+
update({ ...state, remote: null, save: resolved ? { kind: 'idle' } : state.save });
|
|
238
239
|
return read;
|
|
239
240
|
}
|
|
240
241
|
if (state.dirty && (!discard || state.bufferVersion !== previousVersion)) {
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import type { TextDraftActions, TextDraftRecoveryState } from './text-draft-types.js';
|
|
2
|
+
import type { ViewerComponents } from './viewer-components.js';
|
|
2
3
|
export interface TextDraftControlsProps {
|
|
4
|
+
readonly components?: ViewerComponents | undefined;
|
|
3
5
|
readonly recovery: TextDraftRecoveryState;
|
|
4
6
|
readonly actions: TextDraftActions;
|
|
5
7
|
readonly readOnly?: boolean;
|
|
6
8
|
readonly blocked?: boolean;
|
|
7
9
|
}
|
|
8
|
-
export declare function TextDraftControls({ recovery, actions, readOnly, blocked }: TextDraftControlsProps): import("react").JSX.Element | null;
|
|
10
|
+
export declare function TextDraftControls({ components, recovery, actions, readOnly, blocked }: TextDraftControlsProps): import("react").JSX.Element | null;
|
|
9
11
|
//# sourceMappingURL=text-draft-controls.d.ts.map
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import { useLayoutEffect, useRef, useState } from 'react';
|
|
4
|
-
export function TextDraftControls({ recovery, actions, readOnly = false, blocked = false }) {
|
|
4
|
+
export function TextDraftControls({ components, recovery, actions, readOnly = false, blocked = false }) {
|
|
5
|
+
const Control = components?.button ?? 'button';
|
|
5
6
|
const owner = useRef({ actions, busy: false });
|
|
6
7
|
const [pending, setPending] = useState(false);
|
|
7
8
|
const [notice, setNotice] = useState();
|
|
@@ -29,5 +30,5 @@ export function TextDraftControls({ recovery, actions, readOnly = false, blocked
|
|
|
29
30
|
const discovery = active ? recovery.discovery : undefined;
|
|
30
31
|
const checkpoint = active ? recovery.checkpoint : undefined;
|
|
31
32
|
const status = !active ? `Draft recovery ${recovery.kind}` : discovery?.kind === 'checking' ? 'Checking stored drafts' : checkpoint?.kind === 'pending' ? 'Storing draft' : checkpoint?.kind === 'stored' ? 'Draft stored for recovery. Storage is not a save receipt.' : checkpoint?.kind === 'failed' ? 'Draft recovery storage unavailable' : 'Draft recovery enabled';
|
|
32
|
-
return _jsxs("section", { "data-boring": "text-draft-controls", "aria-label": "Draft recovery", children: [_jsx("p", { role: "status", children: status }), _jsx(
|
|
33
|
+
return _jsxs("section", { "data-boring": "text-draft-controls", "aria-label": "Draft recovery", children: [_jsx("p", { role: "status", children: status }), _jsx(Control, { type: "button", disabled: !active || pending || blocked || discovery?.kind === 'checking', onClick: () => run(actions.checkDrafts), children: "Check stored drafts" }), _jsx(Control, { type: "button", disabled: !active || pending || readOnly, onClick: () => run(actions.checkpointDraft), children: "Store draft for recovery" }), discovery?.kind === 'empty' && _jsx("p", { children: "No stored drafts" }), discovery && 'reason' in discovery && _jsx("p", { role: "alert", children: discovery.reason }), checkpoint?.kind === 'failed' && _jsx("p", { role: "alert", children: checkpoint.result.reason }), discovery?.kind === 'offered' && _jsxs(_Fragment, { children: [discovery.truncated && _jsx("p", { children: "More stored drafts exist than this listing shows." }), discovery.choices.map(choice => _jsxs("div", { role: "group", "aria-label": "Stored draft", children: [_jsx("p", { children: choice.compatibility === 'conflict' ? 'Saved revision changed. Review this draft separately.' : 'Stored draft matches the checked saved revision.' }), _jsxs("details", { children: [_jsx("summary", { children: "Review stored draft" }), _jsx("pre", { children: choice.text })] }), _jsx(Control, { type: "button", disabled: pending || blocked || readOnly || choice.compatibility !== 'exact', onClick: () => run(() => actions.restoreDraft(choice.selection)), children: "Restore draft" }), _jsx(Control, { type: "button", disabled: pending || blocked, onClick: () => run(() => actions.discardDraft(choice.selection)), children: "Discard stored draft" })] }, `${choice.selection.offerId}:${choice.selection.draft.writerId}:${choice.selection.draft.sequence}`))] }), notice && _jsx("p", { role: "alert", children: notice })] });
|
|
33
34
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ComponentPropsWithRef, ComponentType, PropsWithChildren } from 'react';
|
|
2
|
+
/** Host presentation for the document renderers. Bindings must forward refs and native input events. */
|
|
3
|
+
export interface ViewerComponents {
|
|
4
|
+
readonly button: ComponentType<ComponentPropsWithRef<'button'>>;
|
|
5
|
+
readonly input: ComponentType<ComponentPropsWithRef<'input'>>;
|
|
6
|
+
readonly textarea: ComponentType<ComponentPropsWithRef<'textarea'>>;
|
|
7
|
+
readonly bubble?: ComponentType<PropsWithChildren>;
|
|
8
|
+
/** The host's separator: vertical between toolbar groups, horizontal above the phone formatting bar. */
|
|
9
|
+
readonly separator?: ComponentType<{
|
|
10
|
+
readonly orientation?: 'horizontal' | 'vertical';
|
|
11
|
+
readonly className?: string;
|
|
12
|
+
}>;
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=viewer-components.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hachej/boring-ui-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -12,12 +12,16 @@
|
|
|
12
12
|
"dist/**/*.js",
|
|
13
13
|
"dist/**/*.d.ts",
|
|
14
14
|
"r/*.json",
|
|
15
|
+
"bin/*.mjs",
|
|
15
16
|
"README.md",
|
|
16
17
|
"LICENSE",
|
|
17
18
|
"THIRD_PARTY_NOTICES.md",
|
|
18
19
|
"styles"
|
|
19
20
|
],
|
|
20
21
|
"types": "./dist/index.d.ts",
|
|
22
|
+
"bin": {
|
|
23
|
+
"boring-ui-blocks": "./bin/boring-ui-blocks.mjs"
|
|
24
|
+
},
|
|
21
25
|
"exports": {
|
|
22
26
|
".": {
|
|
23
27
|
"types": "./dist/index.d.ts",
|
|
@@ -110,7 +114,7 @@
|
|
|
110
114
|
"./file-tree.css": "./styles/file-tree.css"
|
|
111
115
|
},
|
|
112
116
|
"peerDependencies": {
|
|
113
|
-
"@hachej/boring-files": "^0.
|
|
117
|
+
"@hachej/boring-files": "^0.4.1",
|
|
114
118
|
"@earendil-works/pi-durable": "~1.2.0",
|
|
115
119
|
"react": "^19.0.0",
|
|
116
120
|
"react-dom": "^19.0.0",
|
|
@@ -137,7 +141,7 @@
|
|
|
137
141
|
"zod": "^4.6.5",
|
|
138
142
|
"@tldraw/tlschema": "^5.5.2",
|
|
139
143
|
"lucide-react": "^1.51.0",
|
|
140
|
-
"
|
|
144
|
+
"@base-ui/react": "1.9.0"
|
|
141
145
|
},
|
|
142
146
|
"peerDependenciesMeta": {
|
|
143
147
|
"@hachej/boring-files": {
|
|
@@ -221,7 +225,7 @@
|
|
|
221
225
|
"lucide-react": {
|
|
222
226
|
"optional": true
|
|
223
227
|
},
|
|
224
|
-
"
|
|
228
|
+
"@base-ui/react": {
|
|
225
229
|
"optional": true
|
|
226
230
|
}
|
|
227
231
|
},
|