@xenosystem/blocks 0.19.2 → 0.21.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.
@@ -0,0 +1,395 @@
1
+ import {
2
+ Callout,
3
+ DOCUMENT_FORMAT_COMMANDS,
4
+ DOCUMENT_TOOLBAR,
5
+ DocumentController,
6
+ PageBreak,
7
+ SafeImage,
8
+ ToggleBlock,
9
+ WikiLinkMark,
10
+ createDocumentExtensions,
11
+ editorHandle,
12
+ mountDocumentSurface,
13
+ runFormat
14
+ } from "../chunk-5VOA3OSG.js";
15
+ import {
16
+ DOCUMENT_MARK_TYPES,
17
+ DOCUMENT_NODE_TYPES,
18
+ LIMITS,
19
+ SAFE_IMAGE,
20
+ SAFE_LINK,
21
+ XDOC_EXTENSION,
22
+ XDOC_FORMAT,
23
+ XDOC_VERSION,
24
+ XdocError,
25
+ documentToMarkdown,
26
+ emptyContent,
27
+ markdownToDocument,
28
+ nodeTypesIn,
29
+ pageLinksIn,
30
+ parseXdoc,
31
+ plainText,
32
+ serializeXdoc,
33
+ validateContent,
34
+ wordCount
35
+ } from "../chunk-NFOBZJDF.js";
36
+ import "../chunk-2KG3PWR4.js";
37
+
38
+ // src/docs/document/panel.ts
39
+ import { createElement } from "react";
40
+ import { createRoot } from "react-dom/client";
41
+ import { bindConfig } from "@xenosystem/block-sdk";
42
+
43
+ // src/docs/document/manifest.ts
44
+ var DOCUMENT_PANEL_ID = "xeno.core.document";
45
+ var documentManifest = {
46
+ id: DOCUMENT_PANEL_ID,
47
+ version: "0.1.0",
48
+ title: "Document",
49
+ icon: "file-text",
50
+ description: "XENO Docs\u2019 editor over the shared .xdoc format: rich text, lists, tasks, tables, callouts, toggles, page links. The host stores and loads; the block edits and asks. A document containing a block this schema does not know opens read-only, never coerced.",
51
+ defaultSlot: "center",
52
+ inputs: [
53
+ {
54
+ id: "document",
55
+ name: "Document",
56
+ type: "object",
57
+ description: "{id, versionId?, title, xdoc | content, readOnly?}. `xdoc` is the stored file (text or parsed); `versionId` is round-tripped as `expectedVersion` on save so a stale save is refused, not merged.",
58
+ multiple: false
59
+ },
60
+ {
61
+ id: "saved",
62
+ name: "Saved",
63
+ type: "object",
64
+ description: "{versionId} \u2014 the host wrote the version the last `save` asked for.",
65
+ multiple: false
66
+ },
67
+ {
68
+ id: "status",
69
+ name: "Status",
70
+ type: "object",
71
+ description: '{status: "conflict" | "failed", message?} \u2014 the save did not happen, and why.',
72
+ multiple: false
73
+ }
74
+ ],
75
+ outputs: [
76
+ {
77
+ id: "save",
78
+ name: "Save",
79
+ type: "object",
80
+ description: "{id, expectedVersion?, title, xdoc, content, text, pageLinks}. An INTENT \u2014 the host writes `xdoc` (already validated against the format) as the next version and answers on `saved` or `status`."
81
+ },
82
+ {
83
+ id: "openPage",
84
+ name: "Open page",
85
+ type: "object",
86
+ description: "{pageId, pageTitle} \u2014 a page link (the `wikiLink` mark) was followed. The host navigates."
87
+ },
88
+ {
89
+ id: "dirty",
90
+ name: "Dirty",
91
+ type: "object",
92
+ description: "{dirty} \u2014 the document has unsaved changes (true) or was just saved (false)."
93
+ }
94
+ ],
95
+ commands: [
96
+ {
97
+ id: "get_document",
98
+ title: "Get Document",
99
+ description: "Return {id, title, status, readOnly, dirty, text, wordCount, pageLinks, unsupported}. `status` travels with the text so an empty string is never read as an empty document when it was an unsupported one.",
100
+ parameters: {}
101
+ },
102
+ { id: "set_title", title: "Set Title", description: "Rename the document (unsaved until `save`).", parameters: { title: { type: "string", description: "The new title", required: true } } },
103
+ { id: "insert_text", title: "Insert Text", description: "Insert plain text at the cursor.", parameters: { text: { type: "string", description: "The text", required: true } } },
104
+ {
105
+ id: "format",
106
+ title: "Format",
107
+ description: "Apply one format command: paragraph, h1\u2013h3, bold, italic, underline, strike, code, highlight, bulletList, orderedList, taskList, blockquote, codeBlock, callout, toggle, pageBreak, horizontalRule, link (with href), unlink, undo, redo.",
108
+ parameters: {
109
+ command: { type: "string", description: "The command", required: true },
110
+ href: { type: "string", description: "For link: an http(s) or mailto address" }
111
+ }
112
+ },
113
+ {
114
+ id: "insert_page_link",
115
+ title: "Link a Page",
116
+ description: "Insert a link to another document (the `wikiLink` mark) at the cursor.",
117
+ parameters: {
118
+ pageId: { type: "string", description: "The linked document\u2019s id", required: true },
119
+ pageTitle: { type: "string", description: "Its title, shown as the link text", required: true }
120
+ }
121
+ },
122
+ { id: "save", title: "Save", description: "Ask the host to save. Refused while read-only or already saving.", parameters: {} }
123
+ ],
124
+ config: [
125
+ { key: "placeholder", label: "Placeholder", type: "text", defaultValue: "Start writing\u2026", description: "The empty-document hint.", placeholder: "Start writing\u2026" },
126
+ { key: "showTitle", label: "Show title field", type: "boolean", defaultValue: true, description: "Edit the title in the block. Off when the host owns the title." }
127
+ ],
128
+ capabilities: ["storage.local"],
129
+ sdk: "^1.1.0"
130
+ };
131
+
132
+ // src/docs/document/react/DocumentPanelView.tsx
133
+ import { useEffect, useRef, useSyncExternalStore } from "react";
134
+ import { Column, EmptyState, Input, Note, StatusBar, TextButton, Toolbar, ToolbarGroup } from "@xenosystem/components/chrome";
135
+
136
+ // src/docs/document/declaration.ts
137
+ var STATUS = { empty: "", ready: "", saving: "Saving\u2026", saved: "Saved", conflict: "Someone saved a newer version", failed: "Not saved", unsupported: "Read only" };
138
+ function documentViewState(c, options = {}) {
139
+ const s = c.getState();
140
+ const has = s.id !== null;
141
+ const editable = has && !s.readOnly;
142
+ const words = wordCount(c.currentContent());
143
+ const note = s.status === "unsupported" ? `${s.message ?? "This document cannot be edited here."}${s.unsupported.length ? ` Open it in XENO Docs to edit it.` : ""}` : s.status === "failed" || s.status === "conflict" ? s.message ?? STATUS[s.status] : null;
144
+ return {
145
+ hasDocument: has,
146
+ editable,
147
+ readOnly: has && s.readOnly,
148
+ title: s.title,
149
+ titleShown: has && options.showTitle !== false,
150
+ tools: DOCUMENT_TOOLBAR.map((t) => ({ key: t.command, command: t.command, label: t.label, active: c.isActive(t.command) })),
151
+ toolsShown: editable,
152
+ unsupported: s.status === "unsupported",
153
+ note,
154
+ canSave: editable && s.status !== "saving",
155
+ statusLabel: s.dirty && s.status !== "saving" ? "Unsaved changes" : STATUS[s.status] ?? "",
156
+ wordsLabel: `${words} ${words === 1 ? "word" : "words"}`
157
+ };
158
+ }
159
+ function applyDocumentUi(c, intent) {
160
+ switch (intent.kind) {
161
+ case "format":
162
+ if (DOCUMENT_FORMAT_COMMANDS.includes(intent.command)) c.format(intent.command);
163
+ break;
164
+ case "title":
165
+ c.setTitle(intent.value);
166
+ break;
167
+ case "save":
168
+ c.save();
169
+ break;
170
+ }
171
+ }
172
+ var documentViewManifest = {
173
+ ...documentManifest,
174
+ capabilities: [],
175
+ inputs: [{ id: "view", name: "View state", type: "object", description: "The DocumentViewModel the runtime derives from the controller on every draw." }],
176
+ outputs: [{ id: "ui", name: "UI intent", type: "object", description: "{ kind, command?, value? } \u2014 applied by the runtime to the controller." }],
177
+ commands: []
178
+ };
179
+ var EMPTY = { hasDocument: false, editable: false, readOnly: false, title: "", titleShown: false, tools: [], toolsShown: false, unsupported: false, note: null, canSave: false, statusLabel: "", wordsLabel: "0 words" };
180
+ var BEHAVIOURS = {
181
+ "docs/documentPanelView": (inputs) => inputs.view ?? EMPTY
182
+ };
183
+ var DOCUMENT_RUNTIME_UNITS = ["document/Surface"];
184
+ var text = (b) => ({ component: "text", props: { text: b } });
185
+ var ui = (kind, fields = {}) => ({ emit: "ui", value: { object: { kind: { value: kind }, ...fields } } });
186
+ var DOCUMENT_DECLARATION = {
187
+ format: "xeno.block@1",
188
+ manifest: documentViewManifest,
189
+ behaviour: "docs/documentPanelView",
190
+ composition: {
191
+ component: "fragment",
192
+ children: [
193
+ { component: "chrome/EmptyState", when: { not: { input: "view.hasDocument" } }, props: { title: { value: "No document open" }, hint: { value: "Choose a document, or start a new one." } } },
194
+ {
195
+ component: "chrome/Column",
196
+ when: { input: "view.hasDocument" },
197
+ children: [
198
+ {
199
+ component: "chrome/Toolbar",
200
+ when: { input: "view.toolsShown" },
201
+ props: {
202
+ left: {
203
+ node: {
204
+ component: "chrome/ToolbarGroup",
205
+ children: [
206
+ {
207
+ component: "chrome/TextButton",
208
+ each: { input: "view.tools", key: "key" },
209
+ props: { strong: { item: "active" } },
210
+ children: [text({ item: "label" })],
211
+ on: { onClick: ui("format", { command: { item: "command" } }) }
212
+ }
213
+ ]
214
+ }
215
+ },
216
+ right: {
217
+ node: {
218
+ component: "chrome/ToolbarGroup",
219
+ props: { end: { value: true } },
220
+ children: [{ component: "chrome/TextButton", props: { strong: { value: true }, disabled: { not: { input: "view.canSave" } } }, children: [text({ value: "Save" })], on: { onClick: ui("save") } }]
221
+ }
222
+ }
223
+ }
224
+ },
225
+ { component: "chrome/Input", when: { input: "view.titleShown" }, props: { value: { input: "view.title" }, label: { value: "Title" }, readOnly: { input: "view.readOnly" } }, on: { onChange: ui("title", { value: { arg: "" } }) } },
226
+ { component: "chrome/Note", when: { input: "view.note" }, children: [text({ input: "view.note" })] },
227
+ { component: "document/Surface" },
228
+ { component: "chrome/StatusBar", props: { left: { input: "view.wordsLabel" }, right: { input: "view.statusLabel" } } }
229
+ ]
230
+ }
231
+ ]
232
+ }
233
+ };
234
+
235
+ // src/docs/document/react/DocumentPanelView.tsx
236
+ import { jsx, jsxs } from "react/jsx-runtime";
237
+ function DocumentSurface({ controller, surface }) {
238
+ const ref = useRef(null);
239
+ useEffect(() => {
240
+ const el = ref.current;
241
+ if (!el) return;
242
+ return mountDocumentSurface(el, controller, surface);
243
+ }, [controller]);
244
+ return /* @__PURE__ */ jsx("div", { ref, "data-xeno-document-surface-host": "" });
245
+ }
246
+ var documentRuntimeComponents = (controller, surface) => ({
247
+ "document/Surface": () => /* @__PURE__ */ jsx(DocumentSurface, { controller, surface })
248
+ });
249
+ function DocumentPanelView({ controller, surface, ...options }) {
250
+ useSyncExternalStore((fn) => controller.subscribe(fn), () => controller.getState(), () => controller.getState());
251
+ const view = documentViewState(controller, options);
252
+ const ui2 = (intent) => applyDocumentUi(controller, intent);
253
+ if (!view.hasDocument) return /* @__PURE__ */ jsx(EmptyState, { title: "No document open", hint: "Choose a document, or start a new one." });
254
+ return /* @__PURE__ */ jsxs(Column, { children: [
255
+ view.toolsShown ? /* @__PURE__ */ jsx(
256
+ Toolbar,
257
+ {
258
+ left: /* @__PURE__ */ jsx(ToolbarGroup, { children: view.tools.map((t) => /* @__PURE__ */ jsx(TextButton, { strong: t.active, onClick: () => ui2({ kind: "format", command: t.command }), children: t.label }, t.key)) }),
259
+ right: /* @__PURE__ */ jsx(ToolbarGroup, { end: true, children: /* @__PURE__ */ jsx(TextButton, { strong: true, disabled: !view.canSave, onClick: () => ui2({ kind: "save" }), children: "Save" }) })
260
+ }
261
+ ) : null,
262
+ view.titleShown ? /* @__PURE__ */ jsx(Input, { value: view.title, label: "Title", readOnly: view.readOnly, onChange: (value) => ui2({ kind: "title", value }) }) : null,
263
+ view.note ? /* @__PURE__ */ jsx(Note, { children: view.note }) : null,
264
+ /* @__PURE__ */ jsx(DocumentSurface, { controller, surface }),
265
+ /* @__PURE__ */ jsx(StatusBar, { left: view.wordsLabel, right: view.statusLabel })
266
+ ] });
267
+ }
268
+
269
+ // src/docs/document/panel.ts
270
+ function createDocumentPanel(options = {}) {
271
+ return {
272
+ manifest: documentManifest,
273
+ activate(host) {
274
+ const controller = new DocumentController({ host: { emit: (portId, value) => host.emit(portId, value) } });
275
+ const resolve = (config) => ({
276
+ showTitle: config.showTitle !== false,
277
+ placeholder: typeof config.placeholder === "string" ? config.placeholder : "Start writing\u2026"
278
+ });
279
+ let renderConfig = resolve(host.config ?? {});
280
+ let unrender = null;
281
+ let root = null;
282
+ let currentEl = null;
283
+ const draw = (el) => {
284
+ unrender?.();
285
+ if (options.render) {
286
+ unrender = options.render(el, { controller, config: renderConfig });
287
+ return;
288
+ }
289
+ root = createRoot(el);
290
+ root.render(createElement(DocumentPanelView, { controller, showTitle: renderConfig.showTitle, surface: { placeholder: renderConfig.placeholder, ...options.surface } }));
291
+ unrender = () => {
292
+ root?.unmount();
293
+ root = null;
294
+ };
295
+ };
296
+ const unbindConfig = bindConfig(host, (config) => {
297
+ renderConfig = resolve(config);
298
+ if (currentEl) draw(currentEl);
299
+ });
300
+ return {
301
+ render(el) {
302
+ currentEl = el;
303
+ draw(el);
304
+ },
305
+ onInput(portId, value) {
306
+ if (portId === "document" && value && typeof value === "object") controller.load(value);
307
+ else if (portId === "saved") controller.saved(value?.versionId);
308
+ else if (portId === "status") {
309
+ const p = value;
310
+ if (p?.status) controller.setStatus(p.status, p.message);
311
+ }
312
+ },
313
+ async onCommand(commandId, params) {
314
+ switch (commandId) {
315
+ case "get_document":
316
+ return controller.describe();
317
+ case "set_title":
318
+ controller.setTitle(String(params.title ?? ""));
319
+ return true;
320
+ case "insert_text":
321
+ return controller.insertText(String(params.text ?? ""));
322
+ case "format":
323
+ return controller.format(String(params.command ?? ""), params.href);
324
+ case "insert_page_link":
325
+ return controller.insertPageLink(String(params.pageId ?? ""), String(params.pageTitle ?? ""));
326
+ case "save":
327
+ return controller.save();
328
+ default:
329
+ return;
330
+ }
331
+ },
332
+ serialize() {
333
+ return controller.serialize();
334
+ },
335
+ deserialize(state) {
336
+ controller.deserialize(state);
337
+ },
338
+ dispose() {
339
+ unbindConfig();
340
+ currentEl = null;
341
+ unrender?.();
342
+ unrender = null;
343
+ controller.dispose();
344
+ }
345
+ };
346
+ }
347
+ };
348
+ }
349
+ var documentPanel = createDocumentPanel();
350
+ export {
351
+ Callout,
352
+ BEHAVIOURS as DOCUMENT_BEHAVIOURS,
353
+ DOCUMENT_DECLARATION,
354
+ DOCUMENT_FORMAT_COMMANDS,
355
+ DOCUMENT_MARK_TYPES,
356
+ DOCUMENT_NODE_TYPES,
357
+ DOCUMENT_PANEL_ID,
358
+ DOCUMENT_RUNTIME_UNITS,
359
+ DOCUMENT_TOOLBAR,
360
+ DocumentController,
361
+ DocumentPanelView,
362
+ DocumentSurface,
363
+ LIMITS,
364
+ PageBreak,
365
+ SAFE_IMAGE,
366
+ SAFE_LINK,
367
+ SafeImage,
368
+ ToggleBlock,
369
+ WikiLinkMark,
370
+ XDOC_EXTENSION,
371
+ XDOC_FORMAT,
372
+ XDOC_VERSION,
373
+ XdocError,
374
+ applyDocumentUi,
375
+ createDocumentExtensions,
376
+ createDocumentPanel,
377
+ documentManifest,
378
+ documentPanel,
379
+ documentRuntimeComponents,
380
+ documentToMarkdown,
381
+ documentViewManifest,
382
+ documentViewState,
383
+ editorHandle,
384
+ emptyContent,
385
+ markdownToDocument,
386
+ mountDocumentSurface,
387
+ nodeTypesIn,
388
+ pageLinksIn,
389
+ parseXdoc,
390
+ plainText,
391
+ runFormat,
392
+ serializeXdoc,
393
+ validateContent,
394
+ wordCount
395
+ };