@bettercms-ai/kit 2.0.0 → 2.0.2
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/dist/adapters/lossy.d.ts +14 -6
- package/dist/adapters/lossy.js +23 -11
- package/dist/adapters/lossy.js.map +1 -1
- package/dist/adapters/prod-types.d.ts +46 -14
- package/dist/adapters/prod-types.js +37 -35
- package/dist/adapters/prod-types.js.map +1 -1
- package/dist/adapters/schema.d.ts +39 -0
- package/dist/adapters/schema.js +281 -82
- package/dist/adapters/schema.js.map +1 -1
- package/dist/context.d.ts +39 -0
- package/dist/context.js +40 -1
- package/dist/context.js.map +1 -1
- package/dist/controls/form-picker-register.d.ts +7 -1
- package/dist/controls/form-picker-register.js +1 -5
- package/dist/controls/form-picker-register.js.map +1 -1
- package/dist/controls/icon.d.ts +1 -1
- package/dist/controls/icon.js +1 -1
- package/dist/controls/icon.js.map +1 -1
- package/dist/controls/link.d.ts +11 -4
- package/dist/controls/link.js +120 -202
- package/dist/controls/link.js.map +1 -1
- package/dist/controls/location.js +3 -8
- package/dist/controls/location.js.map +1 -1
- package/dist/controls/media.d.ts +0 -14
- package/dist/controls/media.js +52 -95
- package/dist/controls/media.js.map +1 -1
- package/dist/controls/reference-services.d.ts +23 -2
- package/dist/controls/reference.d.ts +2 -2
- package/dist/controls/reference.js +79 -28
- package/dist/controls/reference.js.map +1 -1
- package/dist/controls/rich-text/article-shell.d.ts +145 -0
- package/dist/controls/rich-text/article-shell.js +514 -0
- package/dist/controls/rich-text/article-shell.js.map +1 -0
- package/dist/controls/rich-text/author.d.ts +25 -0
- package/dist/controls/rich-text/author.js +107 -0
- package/dist/controls/rich-text/author.js.map +1 -0
- package/dist/controls/rich-text/blocks.d.ts +19 -2
- package/dist/controls/rich-text/blocks.js +275 -638
- package/dist/controls/rich-text/blocks.js.map +1 -1
- package/dist/controls/rich-text/button.d.ts +23 -0
- package/dist/controls/rich-text/button.js +81 -0
- package/dist/controls/rich-text/button.js.map +1 -0
- package/dist/controls/rich-text/catalog.d.ts +2 -5
- package/dist/controls/rich-text/catalog.js +121 -7
- package/dist/controls/rich-text/catalog.js.map +1 -1
- package/dist/controls/rich-text/checklist.d.ts +24 -0
- package/dist/controls/rich-text/checklist.js +115 -0
- package/dist/controls/rich-text/checklist.js.map +1 -0
- package/dist/controls/rich-text/codec.d.ts +15 -1
- package/dist/controls/rich-text/codec.js +11 -1
- package/dist/controls/rich-text/codec.js.map +1 -1
- package/dist/controls/rich-text/columns.d.ts +41 -0
- package/dist/controls/rich-text/columns.js +181 -0
- package/dist/controls/rich-text/columns.js.map +1 -0
- package/dist/controls/rich-text/control.js +1 -0
- package/dist/controls/rich-text/control.js.map +1 -1
- package/dist/controls/rich-text/dom.d.ts +84 -4
- package/dist/controls/rich-text/dom.js +171 -10
- package/dist/controls/rich-text/dom.js.map +1 -1
- package/dist/controls/rich-text/editor.d.ts +4 -2
- package/dist/controls/rich-text/editor.js +199 -26
- package/dist/controls/rich-text/editor.js.map +1 -1
- package/dist/controls/rich-text/embed.d.ts +28 -0
- package/dist/controls/rich-text/embed.js +210 -0
- package/dist/controls/rich-text/embed.js.map +1 -0
- package/dist/controls/rich-text/footnote.d.ts +41 -0
- package/dist/controls/rich-text/footnote.js +185 -0
- package/dist/controls/rich-text/footnote.js.map +1 -0
- package/dist/controls/rich-text/gallery.d.ts +28 -0
- package/dist/controls/rich-text/gallery.js +167 -0
- package/dist/controls/rich-text/gallery.js.map +1 -0
- package/dist/controls/rich-text/link-form.d.ts +13 -1
- package/dist/controls/rich-text/link-form.js +41 -7
- package/dist/controls/rich-text/link-form.js.map +1 -1
- package/dist/controls/rich-text/ops.js +6 -0
- package/dist/controls/rich-text/ops.js.map +1 -1
- package/dist/controls/rich-text/related.d.ts +49 -0
- package/dist/controls/rich-text/related.js +233 -0
- package/dist/controls/rich-text/related.js.map +1 -0
- package/dist/controls/rich-text/toc.d.ts +47 -0
- package/dist/controls/rich-text/toc.js +160 -0
- package/dist/controls/rich-text/toc.js.map +1 -0
- package/dist/controls/slot-register.d.ts +6 -1
- package/dist/controls/slot-register.js +1 -4
- package/dist/controls/slot-register.js.map +1 -1
- package/dist/core/core.d.ts +11 -3
- package/dist/core/core.js +24 -6
- package/dist/core/core.js.map +1 -1
- package/dist/core/facets.d.ts +16 -0
- package/dist/core/facets.js +17 -1
- package/dist/core/facets.js.map +1 -1
- package/dist/core/index.js +2 -2
- package/dist/index.js +16 -5
- package/dist/index.js.map +1 -1
- package/dist/ledgers/exclusions.v1.js +141 -0
- package/dist/ledgers/exclusions.v1.js.map +1 -0
- package/dist/library/coverage.d.ts +13 -1
- package/dist/library/coverage.js +12 -1
- package/dist/library/coverage.js.map +1 -1
- package/dist/library/kit-catalog.json +158 -154
- package/dist/schema/types.d.ts +17 -0
- package/dist/schema/types.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/testing/coverage-extras.d.ts +8 -0
- package/dist/testing/coverage-extras.js +44 -0
- package/dist/testing/coverage-extras.js.map +1 -0
- package/dist/testing/exclusions.d.ts +30 -0
- package/dist/testing/exclusions.js +13 -0
- package/dist/testing/exclusions.js.map +1 -0
- package/dist/ui/autocomplete.d.ts +29 -1
- package/dist/ui/autocomplete.js +102 -47
- package/dist/ui/autocomplete.js.map +1 -1
- package/dist/ui/button.js +3 -5
- package/dist/ui/button.js.map +1 -1
- package/dist/ui/index.js +2 -2
- package/dist/ui/overlay.d.ts +4 -1
- package/dist/ui/overlay.js +2 -1
- package/dist/ui/overlay.js.map +1 -1
- package/dist/ui/toast.js +1 -1
- package/package.json +8 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"codec.js","names":[],"sources":["../../../src/controls/rich-text/codec.ts"],"sourcesContent":["/**\n * RICH TEXT · the storage contract between the kit editor and its host.\n *\n * The V2 editor edits HTML (contentEditable). Production stores Portable Text, in the shape the classic editor of the\n * same surface writes (a block array for an entry field, the `{ content, html }` envelope for a component prop). The\n * conversion lives in the host, which owns the real converters, schema conformance and the field's Accept config; the\n * kit never imports them. A host with no codec gets its own control for rich text (LegacyFallback), never a guess.\n *\n * The control opens a value only when it survives the trip editor → codec unchanged (`same`), so nothing the HTML\n * editor cannot hold (a checklist's ticks, a link's rel, an image's layout) is dropped by a first keystroke: such a\n * value is edited by the host's own control instead, with the reason named.\n */\nimport type { FieldPath, FieldSchema } from \"../../schema/types\";\nimport type { UnknownValue } from \"../unknown-value\";\n\n/** What a field may hold, in the editor's vocabulary. The editor offers nothing outside it. */\nexport type RichTextFeature =\n | \"h1\" | \"h2\" | \"h3\" | \"h4\" | \"h5\" | \"h6\"\n | \"blockquote\" | \"ul\" | \"ol\" | \"pre\" | \"hr\"\n | \"bold\" | \"italic\" | \"underline\" | \"strike\" | \"sup\" | \"sub\" | \"code\"\n | \"link\" | \"image\"\n /* blocks, each stored as its own Portable Text block (see dom.ts) */\n | \"table\" | \"pullquote\" | \"takeaways\" | \"callout\" | \"toggle\" | \"video\" | \"file\";\n\nexport const ALL_FEATURES: readonly RichTextFeature[] = [\n \"h1\", \"h2\", \"h3\", \"h4\", \"h5\", \"h6\", \"blockquote\", \"ul\", \"ol\", \"pre\", \"hr\", \"bold\", \"italic\", \"underline\", \"strike\", \"sup\", \"sub\", \"code\", \"link\", \"image\",\n \"table\", \"pullquote\", \"takeaways\", \"callout\", \"toggle\", \"video\", \"file\",\n];\n\nexport interface RichTextRead {\n /** the HTML the editor opens on */\n html: string;\n /** the stored value as the classic editor would open it (compared against the editor's round trip) */\n seed: unknown;\n /** what this value already cannot carry from an earlier editor, in words (\"2 links\"); shown before the first edit */\n losses: string[];\n /** what the field accepts */\n features: RichTextFeature[];\n /** one paragraph, stored inline (allowMultipleParagraphs: false) */\n single: boolean;\n /** links may open in a new tab */\n newTab: boolean;\n /**\n * the styles, marks and lists the value holds that the field no longer declares: kept by every write, flagged above\n * the editor with change-to and remove (H4; see `remap`). Absent or empty = none.\n */\n undeclared?: UnknownValue[];\n}\n\nexport interface RichTextWrite {\n /** the value to store, in the surface's own shape */\n value: unknown;\n /** what the stored shape would drop from this HTML, in words; non-empty = the write is refused */\n losses: string[];\n}\n\nexport interface RichTextAt {\n field: FieldSchema;\n path: FieldPath;\n}\n\nexport interface KitRichTextCodec {\n read(value: unknown, at: RichTextAt): RichTextRead;\n /** `prev` is the stored value being replaced (its keys are reused so an edit does not re-key the document) */\n write(html: string, at: RichTextAt, prev: unknown): RichTextWrite;\n /** the same document, ignoring keys, span splits and storage wrapper */\n same(a: unknown, b: unknown): boolean;\n /** the stored value with one `undeclared` value (its `key`) changed to `to`, or removed (`to` null); the words stay */\n remap?(value: unknown, at: RichTextAt, key: string, to: string | null): unknown;\n}\n\ndeclare module \"../../context\" {\n interface KitServices {\n /**\n * Stored rich text ↔ the HTML the kit editor edits (see controls/rich-text/codec.ts). Absent → a rich text field\n * draws the host's own control for it (LegacyControl).\n */\n richText?: KitRichTextCodec;\n }\n}\n"],"mappings":";
|
|
1
|
+
{"version":3,"file":"codec.js","names":[],"sources":["../../../src/controls/rich-text/codec.ts"],"sourcesContent":["/**\n * RICH TEXT · the storage contract between the kit editor and its host.\n *\n * The V2 editor edits HTML (contentEditable). Production stores Portable Text, in the shape the classic editor of the\n * same surface writes (a block array for an entry field, the `{ content, html }` envelope for a component prop). The\n * conversion lives in the host, which owns the real converters, schema conformance and the field's Accept config; the\n * kit never imports them. A host with no codec gets its own control for rich text (LegacyFallback), never a guess.\n *\n * The control opens a value only when it survives the trip editor → codec unchanged (`same`), so nothing the HTML\n * editor cannot hold (a checklist's ticks, a link's rel, an image's layout) is dropped by a first keystroke: such a\n * value is edited by the host's own control instead, with the reason named.\n */\nimport type { FieldPath, FieldSchema } from \"../../schema/types\";\nimport type { UnknownValue } from \"../unknown-value\";\n\n/** What a field may hold, in the editor's vocabulary. The editor offers nothing outside it. */\nexport type RichTextFeature =\n | \"h1\" | \"h2\" | \"h3\" | \"h4\" | \"h5\" | \"h6\"\n | \"blockquote\" | \"ul\" | \"ol\" | \"pre\" | \"hr\"\n | \"bold\" | \"italic\" | \"underline\" | \"strike\" | \"sup\" | \"sub\" | \"code\"\n | \"link\" | \"image\"\n /* blocks, each stored as its own Portable Text block (see dom.ts) */\n | \"table\" | \"pullquote\" | \"takeaways\" | \"callout\" | \"toggle\" | \"video\" | \"file\"\n /* a task list (PT listItem \"check\" + checked) and a call-to-action link (the link markDef with button: true) */\n | \"checklist\" | \"button\"\n /* V2's article blocks, each a Portable Text block of its own (plan RT3–RT5): embed, gallery, table of contents */\n | \"embed\" | \"gallery\" | \"toc\"\n /* plan RT6–RT8: a footnote (inline, in the text), columns of text, an author bio and related posts (references) */\n | \"footnote\" | \"columns\" | \"author\" | \"related\"\n /* an embed's Shape (aspect): the host declares it once its stored embed keeps one (plan RT3) */\n | \"embedAspect\";\n\nexport const ALL_FEATURES: readonly RichTextFeature[] = [\n \"h1\", \"h2\", \"h3\", \"h4\", \"h5\", \"h6\", \"blockquote\", \"ul\", \"ol\", \"pre\", \"hr\", \"bold\", \"italic\", \"underline\", \"strike\", \"sup\", \"sub\", \"code\", \"link\", \"image\",\n \"table\", \"pullquote\", \"takeaways\", \"callout\", \"toggle\", \"video\", \"file\", \"checklist\", \"button\", \"embed\", \"gallery\", \"toc\", \"footnote\", \"columns\", \"author\", \"related\", \"embedAspect\",\n];\n\nexport interface RichTextRead {\n /** the HTML the editor opens on */\n html: string;\n /** the stored value as the classic editor would open it (compared against the editor's round trip) */\n seed: unknown;\n /** what this value already cannot carry from an earlier editor, in words (\"2 links\"); shown before the first edit */\n losses: string[];\n /** what the field accepts */\n features: RichTextFeature[];\n /** one paragraph, stored inline (allowMultipleParagraphs: false) */\n single: boolean;\n /** links may open in a new tab */\n newTab: boolean;\n /**\n * the styles, marks and lists the value holds that the field no longer declares: kept by every write, flagged above\n * the editor with change-to and remove (H4; see `remap`). Absent or empty = none.\n */\n undeclared?: UnknownValue[];\n /**\n * Where an author bio and related posts point (plan RT8), from the field's config: the authors and posts collections,\n * and the entry being edited (left out of the automatic related modes). Author bio is offered only with `authors`.\n * `topic: false`: the posts collection has no tags or category field, so \"Same topic\" cannot rank there (the entry\n * write refuses it, relatedPosts.no_topic_field); a new block is Newest and Same topic is not offered. Absent = has one.\n */\n /** `postsNoun`: what one post is called (the posts collection's singular, \"story\"), so Hand-picked says \"Create new story\" */\n references?: { authors?: string; posts?: string; entryId?: string; topic?: boolean; postsNoun?: string };\n}\n\nexport interface RichTextWrite {\n /** the value to store, in the surface's own shape */\n value: unknown;\n /** what the stored shape would drop from this HTML, in words; non-empty = the write is refused */\n losses: string[];\n}\n\nexport interface RichTextAt {\n field: FieldSchema;\n path: FieldPath;\n}\n\nexport interface KitRichTextCodec {\n read(value: unknown, at: RichTextAt): RichTextRead;\n /** `prev` is the stored value being replaced (its keys are reused so an edit does not re-key the document) */\n write(html: string, at: RichTextAt, prev: unknown): RichTextWrite;\n /** the same document, ignoring keys, span splits and storage wrapper */\n same(a: unknown, b: unknown): boolean;\n /** the stored value with one `undeclared` value (its `key`) changed to `to`, or removed (`to` null); the words stay */\n remap?(value: unknown, at: RichTextAt, key: string, to: string | null): unknown;\n}\n\ndeclare module \"../../context\" {\n interface KitServices {\n /**\n * Stored rich text ↔ the HTML the kit editor edits (see controls/rich-text/codec.ts). Absent → a rich text field\n * draws the host's own control for it (LegacyControl).\n */\n richText?: KitRichTextCodec;\n }\n}\n"],"mappings":";AAgCA,IAAa,eAA2C;CACtD;CAAM;CAAM;CAAM;CAAM;CAAM;CAAM;CAAc;CAAM;CAAM;CAAO;CAAM;CAAQ;CAAU;CAAa;CAAU;CAAO;CAAO;CAAQ;CAAQ;CAClJ;CAAS;CAAa;CAAa;CAAW;CAAU;CAAS;CAAQ;CAAa;CAAU;CAAS;CAAW;CAAO;CAAY;CAAW;CAAU;CAAW;AACzK"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RICH TEXT · Columns (kit-richtext-ui-spec.md §4): V2's columns island, stored as the PT block object
|
|
3
|
+
* `{ _type: "columns", gap: "s" | "m" | "l", columns: { _key, content: PortableTextBlock[] }[] }`: 2–4 columns of
|
|
4
|
+
* text blocks only, depth 1 (plan R5). The editor holds each column as its HTML, as V2 does; the codec maps it.
|
|
5
|
+
*
|
|
6
|
+
* block V2 rich-text/blocks.tsx:536-570: a grid of dashed columns (gap s 8px · m 16px · l 28px), each a nested
|
|
7
|
+
* Editable (paragraphs and marks; paste is plain text), and the bar under it: Add column (≤4), Remove column
|
|
8
|
+
* (the last, ≥2), Gap S | M | L, Remove columns. Escape inside a column leaves the island (Island onKeyDown).
|
|
9
|
+
* editable V2 rich-text/blocks.tsx:54-120, ported here (the kit's own PlainEditable is plain text only).
|
|
10
|
+
*/
|
|
11
|
+
import * as React from "react";
|
|
12
|
+
import type { IslandCtx } from "./blocks";
|
|
13
|
+
import { type IslandEl } from "./dom";
|
|
14
|
+
export type ColumnsGap = "s" | "m" | "l";
|
|
15
|
+
export interface ColumnsModel {
|
|
16
|
+
gap: ColumnsGap;
|
|
17
|
+
cols: {
|
|
18
|
+
id: string;
|
|
19
|
+
html: string;
|
|
20
|
+
}[];
|
|
21
|
+
}
|
|
22
|
+
export declare const COLUMNS_MIN = 2;
|
|
23
|
+
export declare const COLUMNS_MAX = 4;
|
|
24
|
+
export declare const columnsDefault: () => ColumnsModel;
|
|
25
|
+
/** A small rich-text region inside a block (V2 Editable): paragraphs and marks, plain-text paste, a placeholder. */
|
|
26
|
+
export declare function Editable({ html, onChange, placeholder, className, readOnly, label, autoFocus, ...rest }: {
|
|
27
|
+
html: string;
|
|
28
|
+
onChange: (v: string) => void;
|
|
29
|
+
placeholder?: string;
|
|
30
|
+
className?: string;
|
|
31
|
+
readOnly?: boolean;
|
|
32
|
+
label: string;
|
|
33
|
+
autoFocus?: boolean;
|
|
34
|
+
} & Omit<React.HTMLAttributes<HTMLDivElement>, "onChange">): import("react/jsx-runtime").JSX.Element;
|
|
35
|
+
export declare function ColumnsBlock({ m, set, el, ctx, autoFocus, }: {
|
|
36
|
+
m: ColumnsModel;
|
|
37
|
+
set: (fn: (m: ColumnsModel) => ColumnsModel, label?: string) => void;
|
|
38
|
+
el: IslandEl;
|
|
39
|
+
ctx: IslandCtx;
|
|
40
|
+
autoFocus: boolean;
|
|
41
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { cn } from "../../lib/cn.js";
|
|
2
|
+
import { Button, IconButton } from "../../ui/button.js";
|
|
3
|
+
import { Segmented } from "../../ui/controls.js";
|
|
4
|
+
import { Tooltip } from "../../ui/overlay.js";
|
|
5
|
+
import { placeCaret, rid } from "./dom.js";
|
|
6
|
+
import * as React from "react";
|
|
7
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
8
|
+
import { IconMinus, IconPlus, IconTrash } from "@tabler/icons-react";
|
|
9
|
+
//#region src/controls/rich-text/columns.tsx
|
|
10
|
+
/**
|
|
11
|
+
* RICH TEXT · Columns (kit-richtext-ui-spec.md §4): V2's columns island, stored as the PT block object
|
|
12
|
+
* `{ _type: "columns", gap: "s" | "m" | "l", columns: { _key, content: PortableTextBlock[] }[] }`: 2–4 columns of
|
|
13
|
+
* text blocks only, depth 1 (plan R5). The editor holds each column as its HTML, as V2 does; the codec maps it.
|
|
14
|
+
*
|
|
15
|
+
* block V2 rich-text/blocks.tsx:536-570: a grid of dashed columns (gap s 8px · m 16px · l 28px), each a nested
|
|
16
|
+
* Editable (paragraphs and marks; paste is plain text), and the bar under it: Add column (≤4), Remove column
|
|
17
|
+
* (the last, ≥2), Gap S | M | L, Remove columns. Escape inside a column leaves the island (Island onKeyDown).
|
|
18
|
+
* editable V2 rich-text/blocks.tsx:54-120, ported here (the kit's own PlainEditable is plain text only).
|
|
19
|
+
*/
|
|
20
|
+
var COLUMNS_MIN = 2;
|
|
21
|
+
var COLUMNS_MAX = 4;
|
|
22
|
+
var columnsDefault = () => ({
|
|
23
|
+
gap: "m",
|
|
24
|
+
cols: [{
|
|
25
|
+
id: rid("col"),
|
|
26
|
+
html: ""
|
|
27
|
+
}, {
|
|
28
|
+
id: rid("col"),
|
|
29
|
+
html: ""
|
|
30
|
+
}]
|
|
31
|
+
});
|
|
32
|
+
var GAP = {
|
|
33
|
+
s: "8px",
|
|
34
|
+
m: "16px",
|
|
35
|
+
l: "28px"
|
|
36
|
+
};
|
|
37
|
+
/** V2 blocks.tsx:191: the controls row under a block, muted at rest, full strength on hover / focus */
|
|
38
|
+
var BAR = "mt-1.5 flex flex-wrap items-center gap-1 text-kit-fg-3 opacity-70 transition-opacity group-hover/blk:opacity-100 group-focus-within/blk:opacity-100 pointer-coarse:opacity-100";
|
|
39
|
+
var Sep = () => /* @__PURE__ */ jsx("span", {
|
|
40
|
+
"aria-hidden": true,
|
|
41
|
+
className: "mx-0.5 h-4 w-px shrink-0 bg-kit-line"
|
|
42
|
+
});
|
|
43
|
+
/** A small rich-text region inside a block (V2 Editable): paragraphs and marks, plain-text paste, a placeholder. */
|
|
44
|
+
function Editable({ html, onChange, placeholder, className, readOnly, label, autoFocus, ...rest }) {
|
|
45
|
+
const ref = React.useRef(null);
|
|
46
|
+
React.useLayoutEffect(() => {
|
|
47
|
+
const el = ref.current;
|
|
48
|
+
if (!el || document.activeElement === el) return;
|
|
49
|
+
if (el.innerHTML !== html) el.innerHTML = html;
|
|
50
|
+
}, [html]);
|
|
51
|
+
React.useEffect(() => {
|
|
52
|
+
if (!autoFocus || !ref.current) return;
|
|
53
|
+
ref.current.focus({ preventScroll: false });
|
|
54
|
+
placeCaret(ref.current);
|
|
55
|
+
}, []);
|
|
56
|
+
return /* @__PURE__ */ jsx("div", {
|
|
57
|
+
...rest,
|
|
58
|
+
ref,
|
|
59
|
+
role: "textbox",
|
|
60
|
+
"aria-label": label,
|
|
61
|
+
"aria-multiline": true,
|
|
62
|
+
tabIndex: readOnly ? -1 : 0,
|
|
63
|
+
contentEditable: !readOnly,
|
|
64
|
+
suppressContentEditableWarning: true,
|
|
65
|
+
spellCheck: true,
|
|
66
|
+
"data-placeholder": placeholder,
|
|
67
|
+
onInput: (e) => {
|
|
68
|
+
const el = e.currentTarget;
|
|
69
|
+
if (!el.textContent && !el.querySelector("img,br + br")) el.innerHTML = "";
|
|
70
|
+
onChange(el.innerHTML);
|
|
71
|
+
},
|
|
72
|
+
onPaste: (e) => {
|
|
73
|
+
e.preventDefault();
|
|
74
|
+
document.execCommand?.("insertText", false, e.clipboardData.getData("text/plain"));
|
|
75
|
+
},
|
|
76
|
+
className: cn("min-w-0 cursor-text whitespace-pre-wrap break-words outline-none focus-visible:shadow-none empty:before:pointer-events-none empty:before:text-kit-fg-4 empty:before:content-[attr(data-placeholder)]", className)
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
function ColumnsBlock({ m, set, el, ctx, autoFocus }) {
|
|
80
|
+
const ro = !!ctx.readOnly;
|
|
81
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
82
|
+
className: "group/blk",
|
|
83
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
84
|
+
className: "grid",
|
|
85
|
+
style: {
|
|
86
|
+
gridTemplateColumns: `repeat(${m.cols.length}, minmax(0, 1fr))`,
|
|
87
|
+
gap: GAP[m.gap]
|
|
88
|
+
},
|
|
89
|
+
children: m.cols.map((c, i) => /* @__PURE__ */ jsx("div", {
|
|
90
|
+
className: "min-h-14 rounded-kit-md border border-dashed border-kit-line px-2.5 py-2 transition-colors focus-within:border-kit-line-strong hover:border-kit-line-strong",
|
|
91
|
+
children: /* @__PURE__ */ jsx(Editable, {
|
|
92
|
+
label: `Column ${i + 1}`,
|
|
93
|
+
html: c.html,
|
|
94
|
+
readOnly: ro,
|
|
95
|
+
autoFocus: autoFocus && i === 0,
|
|
96
|
+
placeholder: `Column ${i + 1}`,
|
|
97
|
+
onChange: (html) => set((x) => ({
|
|
98
|
+
...x,
|
|
99
|
+
cols: x.cols.map((y) => y.id === c.id ? {
|
|
100
|
+
...y,
|
|
101
|
+
html
|
|
102
|
+
} : y)
|
|
103
|
+
}), `Edit column ${i + 1}`),
|
|
104
|
+
className: "min-h-10 [&_p+p]:mt-2"
|
|
105
|
+
})
|
|
106
|
+
}, c.id))
|
|
107
|
+
}), ro ? null : /* @__PURE__ */ jsxs("div", {
|
|
108
|
+
className: BAR,
|
|
109
|
+
children: [
|
|
110
|
+
/* @__PURE__ */ jsx(Button, {
|
|
111
|
+
size: "xs",
|
|
112
|
+
variant: "ghost",
|
|
113
|
+
leftIcon: /* @__PURE__ */ jsx(IconPlus, { stroke: 1.5 }),
|
|
114
|
+
disabled: m.cols.length >= 4,
|
|
115
|
+
onClick: () => set((x) => ({
|
|
116
|
+
...x,
|
|
117
|
+
cols: [...x.cols, {
|
|
118
|
+
id: rid("col"),
|
|
119
|
+
html: ""
|
|
120
|
+
}]
|
|
121
|
+
}), "Add column"),
|
|
122
|
+
children: "Add column"
|
|
123
|
+
}),
|
|
124
|
+
/* @__PURE__ */ jsx(Button, {
|
|
125
|
+
size: "xs",
|
|
126
|
+
variant: "ghost",
|
|
127
|
+
leftIcon: /* @__PURE__ */ jsx(IconMinus, { stroke: 1.5 }),
|
|
128
|
+
disabled: m.cols.length <= 2,
|
|
129
|
+
onClick: () => set((x) => ({
|
|
130
|
+
...x,
|
|
131
|
+
cols: x.cols.slice(0, -1)
|
|
132
|
+
}), "Remove column"),
|
|
133
|
+
children: "Remove column"
|
|
134
|
+
}),
|
|
135
|
+
/* @__PURE__ */ jsx(Sep, {}),
|
|
136
|
+
/* @__PURE__ */ jsx("span", {
|
|
137
|
+
className: "px-1 text-kit-xs text-kit-fg-3",
|
|
138
|
+
children: "Gap"
|
|
139
|
+
}),
|
|
140
|
+
/* @__PURE__ */ jsx(Segmented, {
|
|
141
|
+
"aria-label": "Gap",
|
|
142
|
+
size: "sm",
|
|
143
|
+
value: m.gap,
|
|
144
|
+
onChange: (gap) => set((x) => ({
|
|
145
|
+
...x,
|
|
146
|
+
gap
|
|
147
|
+
}), "Change column gap"),
|
|
148
|
+
items: [
|
|
149
|
+
{
|
|
150
|
+
value: "s",
|
|
151
|
+
label: "S"
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
value: "m",
|
|
155
|
+
label: "M"
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
value: "l",
|
|
159
|
+
label: "L"
|
|
160
|
+
}
|
|
161
|
+
]
|
|
162
|
+
}),
|
|
163
|
+
/* @__PURE__ */ jsx("span", { className: "flex-1" }),
|
|
164
|
+
/* @__PURE__ */ jsx(Tooltip, {
|
|
165
|
+
content: "Remove columns",
|
|
166
|
+
children: /* @__PURE__ */ jsx(IconButton, {
|
|
167
|
+
size: "xs",
|
|
168
|
+
label: "Remove columns",
|
|
169
|
+
onClick: () => ctx.remove(el),
|
|
170
|
+
className: "hover:text-kit-danger-fg",
|
|
171
|
+
children: /* @__PURE__ */ jsx(IconTrash, { stroke: 1.5 })
|
|
172
|
+
})
|
|
173
|
+
})
|
|
174
|
+
]
|
|
175
|
+
})]
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
//#endregion
|
|
179
|
+
export { COLUMNS_MAX, COLUMNS_MIN, ColumnsBlock, Editable, columnsDefault };
|
|
180
|
+
|
|
181
|
+
//# sourceMappingURL=columns.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"columns.js","names":[],"sources":["../../../src/controls/rich-text/columns.tsx"],"sourcesContent":["/**\n * RICH TEXT · Columns (kit-richtext-ui-spec.md §4): V2's columns island, stored as the PT block object\n * `{ _type: \"columns\", gap: \"s\" | \"m\" | \"l\", columns: { _key, content: PortableTextBlock[] }[] }`: 2–4 columns of\n * text blocks only, depth 1 (plan R5). The editor holds each column as its HTML, as V2 does; the codec maps it.\n *\n * block V2 rich-text/blocks.tsx:536-570: a grid of dashed columns (gap s 8px · m 16px · l 28px), each a nested\n * Editable (paragraphs and marks; paste is plain text), and the bar under it: Add column (≤4), Remove column\n * (the last, ≥2), Gap S | M | L, Remove columns. Escape inside a column leaves the island (Island onKeyDown).\n * editable V2 rich-text/blocks.tsx:54-120, ported here (the kit's own PlainEditable is plain text only).\n */\nimport * as React from \"react\";\nimport { IconMinus, IconPlus, IconTrash } from \"@tabler/icons-react\";\nimport { cn } from \"../../lib/cn\";\nimport { Button, IconButton } from \"../../ui/button\";\nimport { Segmented } from \"../../ui/controls\";\nimport { Tooltip } from \"../../ui/overlay\";\nimport type { IslandCtx } from \"./blocks\";\nimport { placeCaret, rid, type IslandEl } from \"./dom\";\n\nexport type ColumnsGap = \"s\" | \"m\" | \"l\";\nexport interface ColumnsModel {\n gap: ColumnsGap;\n cols: { id: string; html: string }[];\n}\nexport const COLUMNS_MIN = 2;\nexport const COLUMNS_MAX = 4;\nexport const columnsDefault = (): ColumnsModel => ({ gap: \"m\", cols: [{ id: rid(\"col\"), html: \"\" }, { id: rid(\"col\"), html: \"\" }] });\nconst GAP: Record<ColumnsGap, string> = { s: \"8px\", m: \"16px\", l: \"28px\" };\n/** V2 blocks.tsx:191: the controls row under a block, muted at rest, full strength on hover / focus */\nconst BAR = \"mt-1.5 flex flex-wrap items-center gap-1 text-kit-fg-3 opacity-70 transition-opacity group-hover/blk:opacity-100 group-focus-within/blk:opacity-100 pointer-coarse:opacity-100\";\nconst Sep = () => <span aria-hidden className=\"mx-0.5 h-4 w-px shrink-0 bg-kit-line\" />;\n\n/** A small rich-text region inside a block (V2 Editable): paragraphs and marks, plain-text paste, a placeholder. */\nexport function Editable({\n html, onChange, placeholder, className, readOnly, label, autoFocus, ...rest\n}: {\n html: string;\n onChange: (v: string) => void;\n placeholder?: string;\n className?: string;\n readOnly?: boolean;\n label: string;\n autoFocus?: boolean;\n} & Omit<React.HTMLAttributes<HTMLDivElement>, \"onChange\">) {\n const ref = React.useRef<HTMLDivElement>(null);\n React.useLayoutEffect(() => {\n const el = ref.current;\n if (!el || document.activeElement === el) return;\n if (el.innerHTML !== html) el.innerHTML = html;\n }, [html]);\n React.useEffect(() => {\n if (!autoFocus || !ref.current) return;\n ref.current.focus({ preventScroll: false });\n placeCaret(ref.current);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n return (\n <div\n {...rest}\n ref={ref}\n role=\"textbox\"\n aria-label={label}\n aria-multiline\n tabIndex={readOnly ? -1 : 0}\n contentEditable={!readOnly}\n suppressContentEditableWarning\n spellCheck\n data-placeholder={placeholder}\n onInput={(e) => {\n const el = e.currentTarget;\n if (!el.textContent && !el.querySelector(\"img,br + br\")) el.innerHTML = \"\";\n onChange(el.innerHTML);\n }}\n onPaste={(e) => {\n e.preventDefault();\n document.execCommand?.(\"insertText\", false, e.clipboardData.getData(\"text/plain\"));\n }}\n className={cn(\n \"min-w-0 cursor-text whitespace-pre-wrap break-words outline-none focus-visible:shadow-none empty:before:pointer-events-none empty:before:text-kit-fg-4 empty:before:content-[attr(data-placeholder)]\",\n className,\n )}\n />\n );\n}\n\nexport function ColumnsBlock({\n m, set, el, ctx, autoFocus,\n}: {\n m: ColumnsModel;\n set: (fn: (m: ColumnsModel) => ColumnsModel, label?: string) => void;\n el: IslandEl;\n ctx: IslandCtx;\n autoFocus: boolean;\n}) {\n const ro = !!ctx.readOnly;\n return (\n <div className=\"group/blk\">\n <div className=\"grid\" style={{ gridTemplateColumns: `repeat(${m.cols.length}, minmax(0, 1fr))`, gap: GAP[m.gap] }}>\n {m.cols.map((c, i) => (\n <div key={c.id} className=\"min-h-14 rounded-kit-md border border-dashed border-kit-line px-2.5 py-2 transition-colors focus-within:border-kit-line-strong hover:border-kit-line-strong\">\n <Editable\n label={`Column ${i + 1}`}\n html={c.html}\n readOnly={ro}\n autoFocus={autoFocus && i === 0}\n placeholder={`Column ${i + 1}`}\n onChange={(html) => set((x) => ({ ...x, cols: x.cols.map((y) => (y.id === c.id ? { ...y, html } : y)) }), `Edit column ${i + 1}`)}\n className=\"min-h-10 [&_p+p]:mt-2\"\n />\n </div>\n ))}\n </div>\n {ro ? null : (\n <div className={BAR}>\n <Button\n size=\"xs\"\n variant=\"ghost\"\n leftIcon={<IconPlus stroke={1.5} />}\n disabled={m.cols.length >= COLUMNS_MAX}\n onClick={() => set((x) => ({ ...x, cols: [...x.cols, { id: rid(\"col\"), html: \"\" }] }), \"Add column\")}\n >\n Add column\n </Button>\n <Button\n size=\"xs\"\n variant=\"ghost\"\n leftIcon={<IconMinus stroke={1.5} />}\n disabled={m.cols.length <= COLUMNS_MIN}\n onClick={() => set((x) => ({ ...x, cols: x.cols.slice(0, -1) }), \"Remove column\")}\n >\n Remove column\n </Button>\n <Sep />\n <span className=\"px-1 text-kit-xs text-kit-fg-3\">Gap</span>\n <Segmented<ColumnsGap>\n aria-label=\"Gap\"\n size=\"sm\"\n value={m.gap}\n onChange={(gap) => set((x) => ({ ...x, gap }), \"Change column gap\")}\n items={[{ value: \"s\", label: \"S\" }, { value: \"m\", label: \"M\" }, { value: \"l\", label: \"L\" }]}\n />\n <span className=\"flex-1\" />\n <Tooltip content=\"Remove columns\">\n <IconButton size=\"xs\" label=\"Remove columns\" onClick={() => ctx.remove(el)} className=\"hover:text-kit-danger-fg\">\n <IconTrash stroke={1.5} />\n </IconButton>\n </Tooltip>\n </div>\n )}\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAwBA,IAAa,cAAc;AAC3B,IAAa,cAAc;AAC3B,IAAa,wBAAsC;CAAE,KAAK;CAAK,MAAM,CAAC;EAAE,IAAI,IAAI,KAAK;EAAG,MAAM;CAAG,GAAG;EAAE,IAAI,IAAI,KAAK;EAAG,MAAM;CAAG,CAAC;AAAE;AAClI,IAAM,MAAkC;CAAE,GAAG;CAAO,GAAG;CAAQ,GAAG;AAAO;;AAEzE,IAAM,MAAM;AACZ,IAAM,YAAY,oBAAC,QAAD;CAAM,eAAA;CAAY,WAAU;AAAwC,CAAA;;AAGtF,SAAgB,SAAS,EACvB,MAAM,UAAU,aAAa,WAAW,UAAU,OAAO,WAAW,GAAG,QASb;CAC1D,MAAM,MAAM,MAAM,OAAuB,IAAI;CAC7C,MAAM,sBAAsB;EAC1B,MAAM,KAAK,IAAI;EACf,IAAI,CAAC,MAAM,SAAS,kBAAkB,IAAI;EAC1C,IAAI,GAAG,cAAc,MAAM,GAAG,YAAY;CAC5C,GAAG,CAAC,IAAI,CAAC;CACT,MAAM,gBAAgB;EACpB,IAAI,CAAC,aAAa,CAAC,IAAI,SAAS;EAChC,IAAI,QAAQ,MAAM,EAAE,eAAe,MAAM,CAAC;EAC1C,WAAW,IAAI,OAAO;CAExB,GAAG,CAAC,CAAC;CACL,OACE,oBAAC,OAAD;EACE,GAAI;EACC;EACL,MAAK;EACL,cAAY;EACZ,kBAAA;EACA,UAAU,WAAW,KAAK;EAC1B,iBAAiB,CAAC;EAClB,gCAAA;EACA,YAAA;EACA,oBAAkB;EAClB,UAAU,MAAM;GACd,MAAM,KAAK,EAAE;GACb,IAAI,CAAC,GAAG,eAAe,CAAC,GAAG,cAAc,aAAa,GAAG,GAAG,YAAY;GACxE,SAAS,GAAG,SAAS;EACvB;EACA,UAAU,MAAM;GACd,EAAE,eAAe;GACjB,SAAS,cAAc,cAAc,OAAO,EAAE,cAAc,QAAQ,YAAY,CAAC;EACnF;EACA,WAAW,GACT,wMACA,SACF;CACD,CAAA;AAEL;AAEA,SAAgB,aAAa,EAC3B,GAAG,KAAK,IAAI,KAAK,aAOhB;CACD,MAAM,KAAK,CAAC,CAAC,IAAI;CACjB,OACE,qBAAC,OAAD;EAAK,WAAU;EAAf,UAAA,CACE,oBAAC,OAAD;GAAK,WAAU;GAAO,OAAO;IAAE,qBAAqB,UAAU,EAAE,KAAK,OAAO;IAAoB,KAAK,IAAI,EAAE;GAAK;GAC7G,UAAA,EAAE,KAAK,KAAK,GAAG,MACd,oBAAC,OAAD;IAAgB,WAAU;IACxB,UAAA,oBAAC,UAAD;KACE,OAAO,UAAU,IAAI;KACrB,MAAM,EAAE;KACR,UAAU;KACV,WAAW,aAAa,MAAM;KAC9B,aAAa,UAAU,IAAI;KAC3B,WAAW,SAAS,KAAK,OAAO;MAAE,GAAG;MAAG,MAAM,EAAE,KAAK,KAAK,MAAO,EAAE,OAAO,EAAE,KAAK;OAAE,GAAG;OAAG;MAAK,IAAI,CAAE;KAAE,IAAI,eAAe,IAAI,GAAG;KAChI,WAAU;IACX,CAAA;GACE,GAVK,EAAE,EAUP,CACN;EACE,CAAA,GACJ,KAAK,OACJ,qBAAC,OAAD;GAAK,WAAW;GAAhB,UAAA;IACE,oBAAC,QAAD;KACE,MAAK;KACL,SAAQ;KACR,UAAU,oBAAC,UAAD,EAAU,QAAQ,IAAM,CAAA;KAClC,UAAU,EAAE,KAAK,UAAA;KACjB,eAAe,KAAK,OAAO;MAAE,GAAG;MAAG,MAAM,CAAC,GAAG,EAAE,MAAM;OAAE,IAAI,IAAI,KAAK;OAAG,MAAM;MAAG,CAAC;KAAE,IAAI,YAAY;KACpG,UAAA;IAEO,CAAA;IACR,oBAAC,QAAD;KACE,MAAK;KACL,SAAQ;KACR,UAAU,oBAAC,WAAD,EAAW,QAAQ,IAAM,CAAA;KACnC,UAAU,EAAE,KAAK,UAAA;KACjB,eAAe,KAAK,OAAO;MAAE,GAAG;MAAG,MAAM,EAAE,KAAK,MAAM,GAAG,EAAE;KAAE,IAAI,eAAe;KACjF,UAAA;IAEO,CAAA;IACR,oBAAC,KAAD,CAAM,CAAA;IACN,oBAAC,QAAD;KAAM,WAAU;KAAiC,UAAA;IAAS,CAAA;IAC1D,oBAAC,WAAD;KACE,cAAW;KACX,MAAK;KACL,OAAO,EAAE;KACT,WAAW,QAAQ,KAAK,OAAO;MAAE,GAAG;MAAG;KAAI,IAAI,mBAAmB;KAClE,OAAO;MAAC;OAAE,OAAO;OAAK,OAAO;MAAI;MAAG;OAAE,OAAO;OAAK,OAAO;MAAI;MAAG;OAAE,OAAO;OAAK,OAAO;MAAI;KAAC;IAC3F,CAAA;IACD,oBAAC,QAAD,EAAM,WAAU,SAAU,CAAA;IAC1B,oBAAC,SAAD;KAAS,SAAQ;KACf,UAAA,oBAAC,YAAD;MAAY,MAAK;MAAK,OAAM;MAAiB,eAAe,IAAI,OAAO,EAAE;MAAG,WAAU;MACpF,UAAA,oBAAC,WAAD,EAAW,QAAQ,IAAM,CAAA;KACf,CAAA;IACL,CAAA;GACN;EAEJ,CAAA,CAAA;;AAET"}
|
|
@@ -238,6 +238,7 @@ function CodecRichText({ codec, field, value, onChange, readOnly, path, id, inva
|
|
|
238
238
|
id,
|
|
239
239
|
invalid: invalid || !!refusal,
|
|
240
240
|
features: read.features,
|
|
241
|
+
references: read.references,
|
|
241
242
|
single: read.single,
|
|
242
243
|
allowNewTab: read.newTab,
|
|
243
244
|
components: insertable && !read.single && !readOnly ? insertable : void 0,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"control.js","names":[],"sources":["../../../src/controls/rich-text/control.tsx"],"sourcesContent":["/**\n * Rich text field (V2 HEAD controls/richtext.tsx, CPO D49).\n * - The editor stays in place: the document variant is the full editor in the column, the panel variant (inspector)\n * the same editor height-capped.\n * - Expanding (the toolbar's expand button, ⌘↵, or the expand icon in the field's label row at rest) opens the shared\n * ExpandedEditorModal: breadcrumb \"Section › Item › Field\", word count, Done. Esc closes and focus returns.\n *\n * STORAGE. The editor edits HTML; the stored value stays what production stores for the field, through the host's\n * codec (KitServices.richText, see ./codec.ts):\n * - read: stored value → the HTML the editor opens on (+ what the field accepts)\n * - write: edited HTML → the stored value, in the surface's own shape; a write that would drop something is refused,\n * named, and nothing is stored\n * - probe: a value the editor cannot carry unchanged (open → no edit → write ≠ stored) is not opened here at all: the\n * host's own control edits it (LegacyFallback), with the reason above it. Opening never writes.\n */\nimport * as React from \"react\";\nimport { IconArrowsMaximize } from \"@tabler/icons-react\";\nimport { cn } from \"../../lib/cn\";\nimport { useForm, useKitEnv, useKitServices } from \"../../context\";\nimport { resolveFrames } from \"../../lib/frames\";\nimport { pathKey } from \"../../lib/path\";\nimport type { ControlProps } from \"../../types\";\nimport { Tooltip } from \"../../ui/overlay\";\nimport { LegacyFallback } from \"../../engine/kit-field\";\nimport type { KitRichTextCodec, RichTextRead } from \"./codec\";\nimport { editorRoundTrip } from \"./dom\";\nimport { RichTextEditor } from \"./editor\";\nimport { paragraphsHtml, plainParagraphs, usePlainRichText } from \"./plain\";\nimport { Textarea } from \"../../ui/input\";\nimport { TEXT_FOCUS } from \"../text\";\nimport { UnknownValueNote } from \"../unknown-value\";\n\n/* ── which rich-text field (docKey) has its full editor open — shared with the label-row action ── */\nlet openKey: string | null = null;\nconst subs = new Set<() => void>();\nexport function setFullEditor(key: string | null) {\n openKey = key;\n subs.forEach((s) => s());\n}\nfunction useFullEditorKey() {\n return React.useSyncExternalStore(\n (cb) => {\n subs.add(cb);\n return () => subs.delete(cb);\n },\n () => openKey,\n () => openKey,\n );\n}\n\n/* ── fields whose value went to the host's own control: the label-row expand button is not theirs to draw ── */\nconst handedOff = new Set<string>();\nfunction setHandedOff(key: string, on: boolean) {\n if (handedOff.has(key) === on) return;\n if (on) handedOff.add(key);\n else handedOff.delete(key);\n subs.forEach((s) => s());\n}\nfunction useHandedOff(key: string) {\n return React.useSyncExternalStore(\n (cb) => {\n subs.add(cb);\n return () => subs.delete(cb);\n },\n () => handedOff.has(key),\n () => handedOff.has(key),\n );\n}\n\n/** The expanded editor's breadcrumb: the form's frames (root › drilled items) and this field. */\nfunction useExpandCrumbs(fieldTitle: string): string[] {\n const form = useForm();\n const env = useKitEnv();\n if (!form) return [env.docTitle, fieldTitle].filter(Boolean) as string[];\n const { frames } = resolveFrames(form.schema, form.value, form.path, form.resolveSchema);\n const titles = frames.filter((f) => f.kind !== \"history\").map((f, i) => (i === 0 ? env.docTitle || f.title : f.title));\n return [...titles, fieldTitle];\n}\n\n/** A stored block array, or the `{ content }` / `{ format, value }` envelope around one, holding a pull quote block. */\nconst holdsPullQuote = (seed: unknown): boolean =>\n Array.isArray(seed)\n ? seed.some((b) => !!b && typeof b === \"object\" && (b as { _type?: unknown })._type === \"pullQuote\")\n : !!seed && typeof seed === \"object\" && holdsPullQuote((seed as { content?: unknown; value?: unknown }).content ?? (seed as { value?: unknown }).value);\n\n/** What the control decided for one stored value. */\ntype Opened =\n | { ok: true; read: RichTextRead; html: string }\n | { ok: false; reason: string };\n\n/**\n * Open a stored value: read it, then prove the editor carries it unchanged (load → serialize → write === stored).\n * A codec that throws, or a value that does not survive, is handed to the host's own control with the reason.\n */\nexport function openValue(codec: KitRichTextCodec, value: unknown, at: Parameters<KitRichTextCodec[\"read\"]>[1]): Opened {\n let read: RichTextRead;\n try {\n read = codec.read(value, at);\n } catch (err) {\n return { ok: false, reason: `the stored text could not be read (${err instanceof Error ? err.message : String(err)})` };\n }\n // a pull quote (V2 D55 article block) opens as the editor's pull-quote island, but only when the codec renders the\n // stored block as its markup in a field that offers it: a codec that renders nothing for it, or a field with no place\n // for one (a single paragraph), would lose it, so the standard editor edits it\n if (holdsPullQuote(read.seed) && (!/data-rt=\"pullquote\"/.test(read.html) || !read.features.includes(\"pullquote\")))\n return { ok: false, reason: \"it holds a pull quote, which the standard editor edits\" };\n // tables and pull quotes are islands (V2 TableBlock, PullQuote); the probe below proves they survive unchanged\n const html = editorRoundTrip(read.html);\n try {\n const back = codec.write(html, at, value);\n if (back.losses.length) return { ok: false, reason: `this editor would drop ${back.losses.join(\", \")}` };\n if (!codec.same(back.value, read.seed)) return { ok: false, reason: \"it holds formatting this editor cannot keep\" };\n } catch (err) {\n return { ok: false, reason: `the text could not be checked (${err instanceof Error ? err.message : String(err)})` };\n }\n return { ok: true, read, html };\n}\n\nexport function RichTextControl(props: ControlProps) {\n const { richText } = useKitServices();\n if (!richText) return <LegacyFallback {...props} />;\n return <CodecRichText codec={richText} {...props} />;\n}\n\nfunction CodecRichText({ codec, field, value, onChange, readOnly, path, id, invalid }: ControlProps & { codec: KitRichTextCodec }) {\n const env = useKitEnv();\n const docKey = `${env.docId}:${pathKey(path)}`;\n const open = useFullEditorKey() === docKey;\n const crumbs = useExpandCrumbs(field.title);\n const plainScope = usePlainRichText();\n const at = React.useMemo(() => ({ field, path }), [field, path]);\n // the components this field may embed (More components): a hook with one identity for the form's life, so calling it\n // only when the host provides it keeps the hook order fixed\n const { useInsertableComponents } = useKitServices();\n const insertable = useInsertableComponents?.(at);\n /* our own last write: its value comes back as `value`, and the editor keeps the HTML it emitted (no reload, no probe) */\n const mine = React.useRef<{ value: unknown; html: string } | null>(null);\n const [refusal, setRefusal] = React.useState<string | null>(null);\n const opened = React.useMemo<Opened>(() => {\n if (mine.current && mine.current.value === value) {\n try {\n return { ok: true, read: codec.read(value, at), html: mine.current.html };\n } catch {\n /* judged afresh below, which names the failure */\n }\n }\n return openValue(codec, value, at);\n // `at` changes identity with the field object; the field's content is what matters\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [codec, value, JSON.stringify(field), pathKey(path)]);\n\n // close when this field goes away (block deselected, drilled elsewhere)\n React.useEffect(\n () => () => {\n if (openKey === docKey) setFullEditor(null);\n setHandedOff(docKey, false);\n },\n [docKey],\n );\n React.useLayoutEffect(() => {\n setHandedOff(docKey, !opened.ok);\n if (!opened.ok && openKey === docKey) setFullEditor(null);\n }, [docKey, opened.ok]);\n\n if (!opened.ok)\n return (\n <div className=\"flex min-w-0 flex-col gap-1.5\">\n <p role=\"note\" data-slot=\"kit-richtext-handoff\" className=\"rounded-kit-md bg-kit-subtle px-2.5 py-1.5 text-kit-xs text-kit-fg-2\">\n Edited with the standard editor: {opened.reason}.\n </p>\n <LegacyFallback field={field} value={value} onChange={onChange} readOnly={readOnly} path={path} id={id} invalid={invalid} />\n </div>\n );\n\n const { read, html } = opened;\n const write = (next: string) => {\n let res;\n try {\n res = codec.write(next, at, value);\n } catch (err) {\n setRefusal(`Not saved: the text could not be stored (${err instanceof Error ? err.message : String(err)}). Undo the last change with ⌘Z; leaving the field puts back the saved text.`);\n return;\n }\n if (res.losses.length) {\n setRefusal(`Not saved: this field cannot keep ${res.losses.join(\", \")}. Undo the last change with ⌘Z; leaving the field puts back the saved text.`);\n return;\n }\n setRefusal(null);\n if (codec.same(res.value, value)) return;\n mine.current = { value: res.value, html: next };\n onChange(res.value);\n };\n\n // The Values popover (PlainRichTextScope): plain paragraphs are V2's growing textarea (its `text` control), written\n // back through the same codec, one `<p>` per line. Anything richer keeps the editor below.\n const plain = plainScope && !read.losses.length ? plainParagraphs(html) : null;\n if (plain !== null)\n return (\n <div className=\"flex min-w-0 flex-col gap-1.5\">\n <Textarea\n id={id}\n rows={3}\n value={plain}\n readOnly={readOnly}\n aria-invalid={invalid || !!refusal}\n placeholder={field.placeholder}\n onChange={(e) => write(paragraphsHtml(e.target.value))}\n className={cn(TEXT_FOCUS, \"leading-5\")}\n />\n {refusal ? (\n <p role=\"alert\" className=\"text-kit-xs text-kit-danger-fg\">\n {refusal}\n </p>\n ) : null}\n </div>\n );\n\n /* V2 richtext.tsx:57: the editor sits in a plain <div>; the dash-only notes keep the 6px gap as their own margin */\n return (\n <div>\n {read.losses.length ? (\n <p role=\"status\" className=\"mb-1.5 rounded-kit-md bg-kit-warning-soft px-2.5 py-1.5 text-kit-xs text-kit-warning-fg\">\n Saved by an earlier editor. Editing here would drop {read.losses.join(\", \")}. Nothing changes until you edit.\n </p>\n ) : null}\n {read.undeclared?.length && codec.remap ? (\n <div className=\"mb-1.5\">\n <UnknownValueNote\n items={read.undeclared}\n onRemap={(key, to) => onChange(codec.remap!(value, at, key, to))}\n onRemove={(key) => onChange(codec.remap!(value, at, key, null))}\n readOnly={readOnly}\n />\n </div>\n ) : null}\n <RichTextEditor\n value={html}\n onChange={write}\n readOnly={readOnly}\n placeholder={field.placeholder}\n docKey={docKey}\n id={id}\n invalid={invalid || !!refusal}\n features={read.features}\n single={read.single}\n allowNewTab={read.newTab}\n components={insertable && !read.single && !readOnly ? insertable : undefined}\n variant={env.variant === \"document\" ? \"document\" : \"panel\"}\n maxHeight={env.variant === \"document\" ? undefined : 320}\n expandCrumbs={crumbs}\n expandedOpen={open}\n onExpandedOpenChange={(o) => setFullEditor(o ? docKey : null)}\n // select words → Comment in the toolbar: the host's comment popover opens on this field, quoting them\n onComment={env.onComment ? (quote) => env.onComment?.(path, field, quote) : undefined}\n />\n {refusal ? (\n <p role=\"alert\" className=\"mt-1.5 text-kit-xs text-kit-danger-fg\">\n {refusal}\n </p>\n ) : null}\n </div>\n );\n}\n\n/** Label-row action (at rest): the field's toolbar holds the same button while it's open. */\nfunction readsPlain(codec: KitRichTextCodec, value: unknown, field: ControlProps[\"field\"], path: ControlProps[\"path\"]) {\n try {\n const read = codec.read(value, { field, path });\n return !read.losses.length && plainParagraphs(editorRoundTrip(read.html)) !== null;\n } catch {\n return false; // the control names the failure itself\n }\n}\n\nexport function RichTextExpandAction({ path, field, value }: ControlProps) {\n const env = useKitEnv();\n const { richText } = useKitServices();\n const handed = useHandedOff(`${env.docId}:${pathKey(path)}`);\n const plainScope = usePlainRichText();\n // the host's own control (no codec, or a value handed to it) has its own full-screen button\n if (env.variant === \"document\" || !richText || handed) return null;\n // drawn as a plain textarea (V2's `text` control has no ⤢)\n if (plainScope && readsPlain(richText, value, field, path)) return null;\n return (\n <Tooltip content=\"Expand editor\" shortcut=\"⌘↵\">\n <button\n type=\"button\"\n aria-label={`Expand ${field.title}`}\n onClick={() => setFullEditor(`${env.docId}:${pathKey(path)}`)}\n className={cn(\n \"-my-0.5 hidden size-6 items-center justify-center rounded-kit-md text-kit-fg-3 hover:bg-kit-hover hover:text-kit-fg\",\n \"group-hover/field:inline-flex group-focus-within/field:!hidden [@media(pointer:coarse)]:inline-flex\",\n )}\n >\n <IconArrowsMaximize className=\"size-3.5\" stroke={1.5} aria-hidden />\n </button>\n </Tooltip>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"control.js","names":[],"sources":["../../../src/controls/rich-text/control.tsx"],"sourcesContent":["/**\n * Rich text field (V2 HEAD controls/richtext.tsx, CPO D49).\n * - The editor stays in place: the document variant is the full editor in the column, the panel variant (inspector)\n * the same editor height-capped.\n * - Expanding (the toolbar's expand button, ⌘↵, or the expand icon in the field's label row at rest) opens the shared\n * ExpandedEditorModal: breadcrumb \"Section › Item › Field\", word count, Done. Esc closes and focus returns.\n *\n * STORAGE. The editor edits HTML; the stored value stays what production stores for the field, through the host's\n * codec (KitServices.richText, see ./codec.ts):\n * - read: stored value → the HTML the editor opens on (+ what the field accepts)\n * - write: edited HTML → the stored value, in the surface's own shape; a write that would drop something is refused,\n * named, and nothing is stored\n * - probe: a value the editor cannot carry unchanged (open → no edit → write ≠ stored) is not opened here at all: the\n * host's own control edits it (LegacyFallback), with the reason above it. Opening never writes.\n */\nimport * as React from \"react\";\nimport { IconArrowsMaximize } from \"@tabler/icons-react\";\nimport { cn } from \"../../lib/cn\";\nimport { useForm, useKitEnv, useKitServices } from \"../../context\";\nimport { resolveFrames } from \"../../lib/frames\";\nimport { pathKey } from \"../../lib/path\";\nimport type { ControlProps } from \"../../types\";\nimport { Tooltip } from \"../../ui/overlay\";\nimport { LegacyFallback } from \"../../engine/kit-field\";\nimport type { KitRichTextCodec, RichTextRead } from \"./codec\";\nimport { editorRoundTrip } from \"./dom\";\nimport { RichTextEditor } from \"./editor\";\nimport { paragraphsHtml, plainParagraphs, usePlainRichText } from \"./plain\";\nimport { Textarea } from \"../../ui/input\";\nimport { TEXT_FOCUS } from \"../text\";\nimport { UnknownValueNote } from \"../unknown-value\";\n\n/* ── which rich-text field (docKey) has its full editor open — shared with the label-row action ── */\nlet openKey: string | null = null;\nconst subs = new Set<() => void>();\nexport function setFullEditor(key: string | null) {\n openKey = key;\n subs.forEach((s) => s());\n}\nfunction useFullEditorKey() {\n return React.useSyncExternalStore(\n (cb) => {\n subs.add(cb);\n return () => subs.delete(cb);\n },\n () => openKey,\n () => openKey,\n );\n}\n\n/* ── fields whose value went to the host's own control: the label-row expand button is not theirs to draw ── */\nconst handedOff = new Set<string>();\nfunction setHandedOff(key: string, on: boolean) {\n if (handedOff.has(key) === on) return;\n if (on) handedOff.add(key);\n else handedOff.delete(key);\n subs.forEach((s) => s());\n}\nfunction useHandedOff(key: string) {\n return React.useSyncExternalStore(\n (cb) => {\n subs.add(cb);\n return () => subs.delete(cb);\n },\n () => handedOff.has(key),\n () => handedOff.has(key),\n );\n}\n\n/** The expanded editor's breadcrumb: the form's frames (root › drilled items) and this field. */\nfunction useExpandCrumbs(fieldTitle: string): string[] {\n const form = useForm();\n const env = useKitEnv();\n if (!form) return [env.docTitle, fieldTitle].filter(Boolean) as string[];\n const { frames } = resolveFrames(form.schema, form.value, form.path, form.resolveSchema);\n const titles = frames.filter((f) => f.kind !== \"history\").map((f, i) => (i === 0 ? env.docTitle || f.title : f.title));\n return [...titles, fieldTitle];\n}\n\n/** A stored block array, or the `{ content }` / `{ format, value }` envelope around one, holding a pull quote block. */\nconst holdsPullQuote = (seed: unknown): boolean =>\n Array.isArray(seed)\n ? seed.some((b) => !!b && typeof b === \"object\" && (b as { _type?: unknown })._type === \"pullQuote\")\n : !!seed && typeof seed === \"object\" && holdsPullQuote((seed as { content?: unknown; value?: unknown }).content ?? (seed as { value?: unknown }).value);\n\n/** What the control decided for one stored value. */\ntype Opened =\n | { ok: true; read: RichTextRead; html: string }\n | { ok: false; reason: string };\n\n/**\n * Open a stored value: read it, then prove the editor carries it unchanged (load → serialize → write === stored).\n * A codec that throws, or a value that does not survive, is handed to the host's own control with the reason.\n */\nexport function openValue(codec: KitRichTextCodec, value: unknown, at: Parameters<KitRichTextCodec[\"read\"]>[1]): Opened {\n let read: RichTextRead;\n try {\n read = codec.read(value, at);\n } catch (err) {\n return { ok: false, reason: `the stored text could not be read (${err instanceof Error ? err.message : String(err)})` };\n }\n // a pull quote (V2 D55 article block) opens as the editor's pull-quote island, but only when the codec renders the\n // stored block as its markup in a field that offers it: a codec that renders nothing for it, or a field with no place\n // for one (a single paragraph), would lose it, so the standard editor edits it\n if (holdsPullQuote(read.seed) && (!/data-rt=\"pullquote\"/.test(read.html) || !read.features.includes(\"pullquote\")))\n return { ok: false, reason: \"it holds a pull quote, which the standard editor edits\" };\n // tables and pull quotes are islands (V2 TableBlock, PullQuote); the probe below proves they survive unchanged\n const html = editorRoundTrip(read.html);\n try {\n const back = codec.write(html, at, value);\n if (back.losses.length) return { ok: false, reason: `this editor would drop ${back.losses.join(\", \")}` };\n if (!codec.same(back.value, read.seed)) return { ok: false, reason: \"it holds formatting this editor cannot keep\" };\n } catch (err) {\n return { ok: false, reason: `the text could not be checked (${err instanceof Error ? err.message : String(err)})` };\n }\n return { ok: true, read, html };\n}\n\nexport function RichTextControl(props: ControlProps) {\n const { richText } = useKitServices();\n if (!richText) return <LegacyFallback {...props} />;\n return <CodecRichText codec={richText} {...props} />;\n}\n\nfunction CodecRichText({ codec, field, value, onChange, readOnly, path, id, invalid }: ControlProps & { codec: KitRichTextCodec }) {\n const env = useKitEnv();\n const docKey = `${env.docId}:${pathKey(path)}`;\n const open = useFullEditorKey() === docKey;\n const crumbs = useExpandCrumbs(field.title);\n const plainScope = usePlainRichText();\n const at = React.useMemo(() => ({ field, path }), [field, path]);\n // the components this field may embed (More components): a hook with one identity for the form's life, so calling it\n // only when the host provides it keeps the hook order fixed\n const { useInsertableComponents } = useKitServices();\n const insertable = useInsertableComponents?.(at);\n /* our own last write: its value comes back as `value`, and the editor keeps the HTML it emitted (no reload, no probe) */\n const mine = React.useRef<{ value: unknown; html: string } | null>(null);\n const [refusal, setRefusal] = React.useState<string | null>(null);\n const opened = React.useMemo<Opened>(() => {\n if (mine.current && mine.current.value === value) {\n try {\n return { ok: true, read: codec.read(value, at), html: mine.current.html };\n } catch {\n /* judged afresh below, which names the failure */\n }\n }\n return openValue(codec, value, at);\n // `at` changes identity with the field object; the field's content is what matters\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [codec, value, JSON.stringify(field), pathKey(path)]);\n\n // close when this field goes away (block deselected, drilled elsewhere)\n React.useEffect(\n () => () => {\n if (openKey === docKey) setFullEditor(null);\n setHandedOff(docKey, false);\n },\n [docKey],\n );\n React.useLayoutEffect(() => {\n setHandedOff(docKey, !opened.ok);\n if (!opened.ok && openKey === docKey) setFullEditor(null);\n }, [docKey, opened.ok]);\n\n if (!opened.ok)\n return (\n <div className=\"flex min-w-0 flex-col gap-1.5\">\n <p role=\"note\" data-slot=\"kit-richtext-handoff\" className=\"rounded-kit-md bg-kit-subtle px-2.5 py-1.5 text-kit-xs text-kit-fg-2\">\n Edited with the standard editor: {opened.reason}.\n </p>\n <LegacyFallback field={field} value={value} onChange={onChange} readOnly={readOnly} path={path} id={id} invalid={invalid} />\n </div>\n );\n\n const { read, html } = opened;\n const write = (next: string) => {\n let res;\n try {\n res = codec.write(next, at, value);\n } catch (err) {\n setRefusal(`Not saved: the text could not be stored (${err instanceof Error ? err.message : String(err)}). Undo the last change with ⌘Z; leaving the field puts back the saved text.`);\n return;\n }\n if (res.losses.length) {\n setRefusal(`Not saved: this field cannot keep ${res.losses.join(\", \")}. Undo the last change with ⌘Z; leaving the field puts back the saved text.`);\n return;\n }\n setRefusal(null);\n if (codec.same(res.value, value)) return;\n mine.current = { value: res.value, html: next };\n onChange(res.value);\n };\n\n // The Values popover (PlainRichTextScope): plain paragraphs are V2's growing textarea (its `text` control), written\n // back through the same codec, one `<p>` per line. Anything richer keeps the editor below.\n const plain = plainScope && !read.losses.length ? plainParagraphs(html) : null;\n if (plain !== null)\n return (\n <div className=\"flex min-w-0 flex-col gap-1.5\">\n <Textarea\n id={id}\n rows={3}\n value={plain}\n readOnly={readOnly}\n aria-invalid={invalid || !!refusal}\n placeholder={field.placeholder}\n onChange={(e) => write(paragraphsHtml(e.target.value))}\n className={cn(TEXT_FOCUS, \"leading-5\")}\n />\n {refusal ? (\n <p role=\"alert\" className=\"text-kit-xs text-kit-danger-fg\">\n {refusal}\n </p>\n ) : null}\n </div>\n );\n\n /* V2 richtext.tsx:57: the editor sits in a plain <div>; the dash-only notes keep the 6px gap as their own margin */\n return (\n <div>\n {read.losses.length ? (\n <p role=\"status\" className=\"mb-1.5 rounded-kit-md bg-kit-warning-soft px-2.5 py-1.5 text-kit-xs text-kit-warning-fg\">\n Saved by an earlier editor. Editing here would drop {read.losses.join(\", \")}. Nothing changes until you edit.\n </p>\n ) : null}\n {read.undeclared?.length && codec.remap ? (\n <div className=\"mb-1.5\">\n <UnknownValueNote\n items={read.undeclared}\n onRemap={(key, to) => onChange(codec.remap!(value, at, key, to))}\n onRemove={(key) => onChange(codec.remap!(value, at, key, null))}\n readOnly={readOnly}\n />\n </div>\n ) : null}\n <RichTextEditor\n value={html}\n onChange={write}\n readOnly={readOnly}\n placeholder={field.placeholder}\n docKey={docKey}\n id={id}\n invalid={invalid || !!refusal}\n features={read.features}\n references={read.references}\n single={read.single}\n allowNewTab={read.newTab}\n components={insertable && !read.single && !readOnly ? insertable : undefined}\n variant={env.variant === \"document\" ? \"document\" : \"panel\"}\n maxHeight={env.variant === \"document\" ? undefined : 320}\n expandCrumbs={crumbs}\n expandedOpen={open}\n onExpandedOpenChange={(o) => setFullEditor(o ? docKey : null)}\n // select words → Comment in the toolbar: the host's comment popover opens on this field, quoting them\n onComment={env.onComment ? (quote) => env.onComment?.(path, field, quote) : undefined}\n />\n {refusal ? (\n <p role=\"alert\" className=\"mt-1.5 text-kit-xs text-kit-danger-fg\">\n {refusal}\n </p>\n ) : null}\n </div>\n );\n}\n\n/** Label-row action (at rest): the field's toolbar holds the same button while it's open. */\nfunction readsPlain(codec: KitRichTextCodec, value: unknown, field: ControlProps[\"field\"], path: ControlProps[\"path\"]) {\n try {\n const read = codec.read(value, { field, path });\n return !read.losses.length && plainParagraphs(editorRoundTrip(read.html)) !== null;\n } catch {\n return false; // the control names the failure itself\n }\n}\n\nexport function RichTextExpandAction({ path, field, value }: ControlProps) {\n const env = useKitEnv();\n const { richText } = useKitServices();\n const handed = useHandedOff(`${env.docId}:${pathKey(path)}`);\n const plainScope = usePlainRichText();\n // the host's own control (no codec, or a value handed to it) has its own full-screen button\n if (env.variant === \"document\" || !richText || handed) return null;\n // drawn as a plain textarea (V2's `text` control has no ⤢)\n if (plainScope && readsPlain(richText, value, field, path)) return null;\n return (\n <Tooltip content=\"Expand editor\" shortcut=\"⌘↵\">\n <button\n type=\"button\"\n aria-label={`Expand ${field.title}`}\n onClick={() => setFullEditor(`${env.docId}:${pathKey(path)}`)}\n className={cn(\n \"-my-0.5 hidden size-6 items-center justify-center rounded-kit-md text-kit-fg-3 hover:bg-kit-hover hover:text-kit-fg\",\n \"group-hover/field:inline-flex group-focus-within/field:!hidden [@media(pointer:coarse)]:inline-flex\",\n )}\n >\n <IconArrowsMaximize className=\"size-3.5\" stroke={1.5} aria-hidden />\n </button>\n </Tooltip>\n );\n}\n"],"mappings":"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"}
|
|
@@ -28,9 +28,9 @@
|
|
|
28
28
|
* <div data-body><p>…` (the message is plain text: paragraphs and line breaks)
|
|
29
29
|
* toggle `bcmsBlock` builtin:toggle {title, body}: `<details data-rt="toggle"><summary>…<div data-body><p>…`
|
|
30
30
|
* video the `video` block {src, title}: `<div data-rt="video" data-url data-title>`
|
|
31
|
-
* embed
|
|
32
|
-
*
|
|
33
|
-
*
|
|
31
|
+
* embed V2's article Embed {url, title, aspect}: `<figure data-rt="embed" data-url data-title data-aspect><a href>…`
|
|
32
|
+
* (embed.tsx). Stored today as `bcmsBlock` builtin:video {url, title}, which keeps no aspect; the PT
|
|
33
|
+
* `embed` block (plan RT3) does.
|
|
34
34
|
* file the `file` block {src, name, size}: `<div data-rt="file" data-url data-name data-size>`
|
|
35
35
|
* component `bcmsBlock` "component" {componentId, overrides, variant} (CPO D28): `<div data-rt="component" data-ref
|
|
36
36
|
* data-id data-name data-overrides data-variant>` — a linked instance of a project component, shown as a card.
|
|
@@ -58,9 +58,35 @@ export interface ImageModel {
|
|
|
58
58
|
align?: ImageAlign;
|
|
59
59
|
size?: ImageSize;
|
|
60
60
|
href?: string;
|
|
61
|
+
/** RT9: how much is cut from each edge, 0–1 of the source (image-url's crop) */
|
|
62
|
+
crop?: ImageCrop;
|
|
63
|
+
/** RT9: the subject, 0–1 of the source (image-url's focal) */
|
|
64
|
+
focal?: {
|
|
65
|
+
x: number;
|
|
66
|
+
y: number;
|
|
67
|
+
};
|
|
61
68
|
/** the stored block's other keys, opaque to the editor */
|
|
62
69
|
extra?: string;
|
|
63
70
|
}
|
|
71
|
+
/** RT9: image-url's crop, each edge a 0–1 fraction of the source. */
|
|
72
|
+
export interface ImageCrop {
|
|
73
|
+
top: number;
|
|
74
|
+
right: number;
|
|
75
|
+
bottom: number;
|
|
76
|
+
left: number;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* RT9: an image's framing as production writes it on the `<img>` (richtext richTextImageFrame): `data-src` the stored
|
|
80
|
+
* url when the drawn one carries the crop, `data-crop` "top,right,bottom,left" and `data-focal` "x,y", each 0–1.
|
|
81
|
+
*/
|
|
82
|
+
export declare function readImageFrame(img: Element | null): {
|
|
83
|
+
src: string;
|
|
84
|
+
crop?: ImageCrop;
|
|
85
|
+
focal?: {
|
|
86
|
+
x: number;
|
|
87
|
+
y: number;
|
|
88
|
+
};
|
|
89
|
+
};
|
|
64
90
|
/** A table island IS the kit's table value (V2 TableModel); only what production's table keeps is stored (see above). */
|
|
65
91
|
export type TableModel = TableValue;
|
|
66
92
|
/** V2 PullQuoteModel (article.ts). */
|
|
@@ -95,10 +121,13 @@ export interface VideoModel {
|
|
|
95
121
|
title?: string;
|
|
96
122
|
extra?: string;
|
|
97
123
|
}
|
|
98
|
-
/** V2 EmbedModel (article.ts)
|
|
124
|
+
/** V2 EmbedModel (article.ts); `aspect` absent draws 16:9 (embed.tsx). */
|
|
99
125
|
export interface EmbedModel {
|
|
100
126
|
url?: string;
|
|
101
127
|
title?: string;
|
|
128
|
+
aspect?: "16:9" | "4:3" | "1:1" | "9:16";
|
|
129
|
+
/** the title is also the figure's aria-label, as production writes it (RT3) and a new embed does; kept as found */
|
|
130
|
+
labelled?: true;
|
|
102
131
|
extra?: string;
|
|
103
132
|
}
|
|
104
133
|
export type EmbedProvider = "youtube" | "vimeo" | "x" | "codepen" | "figma" | "maps" | "spotify" | "podcast" | "audio" | "link";
|
|
@@ -110,6 +139,52 @@ export interface EmbedInfo {
|
|
|
110
139
|
host: string;
|
|
111
140
|
}
|
|
112
141
|
export declare function detectEmbed(url: string): EmbedInfo;
|
|
142
|
+
/** V2's article Gallery (plan RT4): `<figure data-rt="gallery" data-layout><ul><li><figure><img src alt><figcaption>`. */
|
|
143
|
+
export interface GalleryIslandModel {
|
|
144
|
+
layout?: "grid" | "carousel";
|
|
145
|
+
/** `_key` is the editor's own (rows keep their identity while edited); src, alt and caption are what is stored */
|
|
146
|
+
images: {
|
|
147
|
+
_key: string;
|
|
148
|
+
src: string;
|
|
149
|
+
alt?: string;
|
|
150
|
+
caption?: string;
|
|
151
|
+
crop?: ImageCrop;
|
|
152
|
+
focal?: {
|
|
153
|
+
x: number;
|
|
154
|
+
y: number;
|
|
155
|
+
};
|
|
156
|
+
}[];
|
|
157
|
+
extra?: string;
|
|
158
|
+
}
|
|
159
|
+
/** V2's article Table of contents (plan RT5): `<nav data-rt="toc" data-depth><p data-title>`; the list is derived. */
|
|
160
|
+
export interface TocIslandModel {
|
|
161
|
+
title?: string;
|
|
162
|
+
depth?: "2" | "3";
|
|
163
|
+
extra?: string;
|
|
164
|
+
}
|
|
165
|
+
/** V2's Columns (plan RT7): `<div data-rt="columns" data-gap data-cols><div data-col="true">…</div></div>`, text only. */
|
|
166
|
+
export interface ColumnsIslandModel {
|
|
167
|
+
gap?: "s" | "m" | "l";
|
|
168
|
+
/** each column's HTML (paragraphs and marks); `id` is the editor's own */
|
|
169
|
+
cols: {
|
|
170
|
+
id: string;
|
|
171
|
+
html: string;
|
|
172
|
+
}[];
|
|
173
|
+
extra?: string;
|
|
174
|
+
}
|
|
175
|
+
/** V2's Author bio (plan RT8): `<aside data-rt="author" data-author>`; name, avatar and bio are resolved, never stored. */
|
|
176
|
+
export interface AuthorIslandModel {
|
|
177
|
+
author?: string;
|
|
178
|
+
extra?: string;
|
|
179
|
+
}
|
|
180
|
+
/** V2's Related posts (plan RT8): `<aside data-rt="related" data-mode data-count data-posts>`; the list is resolved. */
|
|
181
|
+
export interface RelatedIslandModel {
|
|
182
|
+
title?: string;
|
|
183
|
+
mode?: "topic" | "newest" | "manual";
|
|
184
|
+
count?: number;
|
|
185
|
+
posts: string[];
|
|
186
|
+
extra?: string;
|
|
187
|
+
}
|
|
113
188
|
export interface FileModel {
|
|
114
189
|
url?: string;
|
|
115
190
|
name?: string;
|
|
@@ -149,6 +224,11 @@ export interface IslandModels {
|
|
|
149
224
|
embed: EmbedModel;
|
|
150
225
|
file: FileModel;
|
|
151
226
|
component: ComponentModel;
|
|
227
|
+
gallery: GalleryIslandModel;
|
|
228
|
+
toc: TocIslandModel;
|
|
229
|
+
columns: ColumnsIslandModel;
|
|
230
|
+
author: AuthorIslandModel;
|
|
231
|
+
related: RelatedIslandModel;
|
|
152
232
|
}
|
|
153
233
|
export type IslandKind = keyof IslandModels;
|
|
154
234
|
export declare const ISLAND_KINDS: IslandKind[];
|