@bettercms-ai/kit 2.0.0-next.2 → 2.0.0-next.3
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/controls/link.d.ts +11 -4
- package/dist/controls/link.js +8 -6
- package/dist/controls/link.js.map +1 -1
- package/dist/controls/rich-text/article-shell.js +1 -0
- package/dist/controls/rich-text/article-shell.js.map +1 -1
- package/dist/controls/rich-text/blocks.js +2 -1
- package/dist/controls/rich-text/blocks.js.map +1 -1
- package/dist/controls/rich-text/button.d.ts +2 -2
- package/dist/controls/rich-text/button.js +10 -14
- package/dist/controls/rich-text/button.js.map +1 -1
- package/dist/controls/rich-text/catalog.js +1 -1
- package/dist/controls/rich-text/catalog.js.map +1 -1
- package/dist/controls/rich-text/codec.d.ts +3 -0
- package/dist/controls/rich-text/codec.js.map +1 -1
- package/dist/controls/rich-text/editor.js +16 -15
- package/dist/controls/rich-text/editor.js.map +1 -1
- package/dist/controls/rich-text/link-form.d.ts +10 -0
- package/dist/controls/rich-text/link-form.js +39 -5
- package/dist/controls/rich-text/link-form.js.map +1 -1
- package/dist/controls/rich-text/related.d.ts +12 -4
- package/dist/controls/rich-text/related.js +22 -11
- package/dist/controls/rich-text/related.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/ui/overlay.d.ts +4 -1
- package/dist/ui/overlay.js +2 -1
- package/dist/ui/overlay.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,7 +1,17 @@
|
|
|
1
|
+
import { type LinkRel, type LinkValue } from "../link";
|
|
1
2
|
export interface TextLink {
|
|
2
3
|
href: string;
|
|
3
4
|
newTab?: boolean;
|
|
5
|
+
/** absent = follow (V2 writes no rel for it) */
|
|
6
|
+
rel?: Exclude<LinkRel, "follow">;
|
|
4
7
|
}
|
|
8
|
+
/** An anchor's link as stored: href, a new tab, and its rel (a new tab's noopener/noreferrer are the renderer's). */
|
|
9
|
+
export declare function textLinkOf(a: HTMLAnchorElement): TextLink;
|
|
10
|
+
/** Writes a link onto its anchor: href, target and rel (none for follow). */
|
|
11
|
+
export declare function applyTextLink(a: HTMLAnchorElement, l: TextLink): void;
|
|
12
|
+
export declare const toLinkValue: (l: TextLink) => LinkValue;
|
|
13
|
+
/** V2 saveLink (RTE:883-899): the resolved href is what is stored; a page or section pick re-reads from it. */
|
|
14
|
+
export declare const fromLinkValue: (v: LinkValue, allowNewTab: boolean) => TextLink;
|
|
5
15
|
/** An address a link may carry: http(s), mailto, tel, a site path or an in-page anchor. */
|
|
6
16
|
export declare function linkProblem(href: string): string | null;
|
|
7
17
|
export declare function LinkForm({ initial, allowNewTab, onSave, onCancel, onDetach, dirty, }: {
|
|
@@ -7,11 +7,45 @@ import { jsx, jsxs } from "react/jsx-runtime";
|
|
|
7
7
|
import { IconLink } from "@tabler/icons-react";
|
|
8
8
|
//#region src/controls/rich-text/link-form.tsx
|
|
9
9
|
/**
|
|
10
|
-
* RICH TEXT ·
|
|
11
|
-
* LinkPopoverBody:
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* RICH TEXT · a link inside the text. A text link and a button are edited in V2's own link panel (controls/link.tsx
|
|
11
|
+
* LinkPopoverBody: Type, the page/section/address, new tab and Rel; RTE:1696) and stored as production's link mark:
|
|
12
|
+
* the resolved href, target and rel (`toLinkValue` / `fromLinkValue`). `LinkForm` below is the address-only form an
|
|
13
|
+
* image block links with.
|
|
14
14
|
*/
|
|
15
|
+
var RELS = [
|
|
16
|
+
"nofollow",
|
|
17
|
+
"sponsored",
|
|
18
|
+
"ugc"
|
|
19
|
+
];
|
|
20
|
+
/** An anchor's link as stored: href, a new tab, and its rel (a new tab's noopener/noreferrer are the renderer's). */
|
|
21
|
+
function textLinkOf(a) {
|
|
22
|
+
const tokens = (a.getAttribute("rel") ?? "").toLowerCase().split(/\s+/);
|
|
23
|
+
const rel = RELS.find((r) => tokens.includes(r));
|
|
24
|
+
return {
|
|
25
|
+
href: a.getAttribute("href") ?? "",
|
|
26
|
+
newTab: a.getAttribute("target") === "_blank",
|
|
27
|
+
...rel ? { rel } : {}
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/** Writes a link onto its anchor: href, target and rel (none for follow). */
|
|
31
|
+
function applyTextLink(a, l) {
|
|
32
|
+
a.setAttribute("href", l.href);
|
|
33
|
+
if (l.newTab) a.setAttribute("target", "_blank");
|
|
34
|
+
else a.removeAttribute("target");
|
|
35
|
+
if (l.rel) a.setAttribute("rel", l.rel);
|
|
36
|
+
else a.removeAttribute("rel");
|
|
37
|
+
}
|
|
38
|
+
var toLinkValue = (l) => ({
|
|
39
|
+
href: l.href,
|
|
40
|
+
newTab: !!l.newTab,
|
|
41
|
+
rel: l.rel ?? "follow"
|
|
42
|
+
});
|
|
43
|
+
/** V2 saveLink (RTE:883-899): the resolved href is what is stored; a page or section pick re-reads from it. */
|
|
44
|
+
var fromLinkValue = (v, allowNewTab) => ({
|
|
45
|
+
href: v.href,
|
|
46
|
+
newTab: allowNewTab && !!v.newTab,
|
|
47
|
+
...v.rel && v.rel !== "follow" ? { rel: v.rel } : {}
|
|
48
|
+
});
|
|
15
49
|
/** An address a link may carry: http(s), mailto, tel, a site path or an in-page anchor. */
|
|
16
50
|
function linkProblem(href) {
|
|
17
51
|
const h = href.trim();
|
|
@@ -142,6 +176,6 @@ function LinkForm({ initial, allowNewTab, onSave, onCancel, onDetach, dirty = fa
|
|
|
142
176
|
});
|
|
143
177
|
}
|
|
144
178
|
//#endregion
|
|
145
|
-
export { LinkForm, linkProblem };
|
|
179
|
+
export { LinkForm, applyTextLink, fromLinkValue, linkProblem, textLinkOf, toLinkValue };
|
|
146
180
|
|
|
147
181
|
//# sourceMappingURL=link-form.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"link-form.js","names":[],"sources":["../../../src/controls/rich-text/link-form.tsx"],"sourcesContent":["/**\n * RICH TEXT ·
|
|
1
|
+
{"version":3,"file":"link-form.js","names":[],"sources":["../../../src/controls/rich-text/link-form.tsx"],"sourcesContent":["/**\n * RICH TEXT · a link inside the text. A text link and a button are edited in V2's own link panel (controls/link.tsx\n * LinkPopoverBody: Type, the page/section/address, new tab and Rel; RTE:1696) and stored as production's link mark:\n * the resolved href, target and rel (`toLinkValue` / `fromLinkValue`). `LinkForm` below is the address-only form an\n * image block links with.\n */\nimport * as React from \"react\";\nimport { IconLink } from \"@tabler/icons-react\";\nimport { Button } from \"../../ui/button\";\nimport { Switch } from \"../../ui/controls\";\nimport { Input } from \"../../ui/input\";\nimport { LinkLine, normLink, type LinkRel, type LinkValue } from \"../link\";\n\nexport interface TextLink {\n href: string;\n newTab?: boolean;\n /** absent = follow (V2 writes no rel for it) */\n rel?: Exclude<LinkRel, \"follow\">;\n}\n\nconst RELS: Exclude<LinkRel, \"follow\">[] = [\"nofollow\", \"sponsored\", \"ugc\"];\n/** An anchor's link as stored: href, a new tab, and its rel (a new tab's noopener/noreferrer are the renderer's). */\nexport function textLinkOf(a: HTMLAnchorElement): TextLink {\n const tokens = (a.getAttribute(\"rel\") ?? \"\").toLowerCase().split(/\\s+/);\n const rel = RELS.find((r) => tokens.includes(r));\n return { href: a.getAttribute(\"href\") ?? \"\", newTab: a.getAttribute(\"target\") === \"_blank\", ...(rel ? { rel } : {}) };\n}\n/** Writes a link onto its anchor: href, target and rel (none for follow). */\nexport function applyTextLink(a: HTMLAnchorElement, l: TextLink) {\n a.setAttribute(\"href\", l.href);\n if (l.newTab) a.setAttribute(\"target\", \"_blank\");\n else a.removeAttribute(\"target\");\n if (l.rel) a.setAttribute(\"rel\", l.rel);\n else a.removeAttribute(\"rel\");\n}\nexport const toLinkValue = (l: TextLink): LinkValue => ({ href: l.href, newTab: !!l.newTab, rel: l.rel ?? \"follow\" });\n/** V2 saveLink (RTE:883-899): the resolved href is what is stored; a page or section pick re-reads from it. */\nexport const fromLinkValue = (v: LinkValue, allowNewTab: boolean): TextLink => ({\n href: v.href,\n newTab: allowNewTab && !!v.newTab,\n ...(v.rel && v.rel !== \"follow\" ? { rel: v.rel } : {}),\n});\n\n/** An address a link may carry: http(s), mailto, tel, a site path or an in-page anchor. */\nexport function linkProblem(href: string): string | null {\n const h = href.trim();\n if (!h) return \"Add an address first.\";\n if (/^(https?:\\/\\/[^\\s]+|mailto:[^\\s]+|tel:[+\\d][\\d\\s()-]*|\\/(?!\\/)[^\\s]*|#[^\\s]+)$/i.test(h)) return null;\n if (/^[a-z][a-z0-9+.-]*:/i.test(h)) return \"Use an address that starts with https://, mailto: or tel:.\";\n if (/^[^\\s/]+\\.[^\\s]+$/.test(h)) return `Add https:// in front: https://${h}`;\n return \"Use a full address (https://…), a page path (/about) or an anchor (#section).\";\n}\n\nexport function LinkForm({\n initial,\n allowNewTab,\n onSave,\n onCancel,\n onDetach,\n dirty = false,\n}: {\n initial: TextLink;\n allowNewTab: boolean;\n onSave: (v: TextLink) => void;\n onCancel: () => void;\n onDetach?: () => void;\n /** something the host draws beside the form changed (a button's label), so Apply has something to save */\n dirty?: boolean;\n}) {\n const [href, setHref] = React.useState(initial.href);\n const [newTab, setNewTab] = React.useState(!!initial.newTab);\n const [problem, setProblem] = React.useState<string | null>(null);\n const urlId = React.useId();\n const tabId = React.useId();\n const errId = React.useId();\n const start = initial.href.trim();\n // V2 LinkPopoverBody (controls/link.tsx): Apply only when something changed and the link can be saved\n const changed = dirty || href.trim() !== start || (allowNewTab && newTab !== !!initial.newTab);\n const ok = !linkProblem(href);\n return (\n <form\n className=\"flex flex-col gap-3\"\n onSubmit={(e) => {\n e.preventDefault();\n const p = linkProblem(href);\n setProblem(p);\n if (!p) onSave({ href: href.trim(), newTab: allowNewTab && newTab });\n }}\n >\n <div className=\"flex min-w-0 flex-col gap-1\">\n <span className=\"text-kit-sm font-medium text-kit-fg\">Link</span>\n {start ? (\n <span className=\"flex min-w-0 text-kit-fg-2\">\n <LinkLine v={normLink({ href: start, newTab: !!initial.newTab })} />\n </span>\n ) : (\n <span className=\"text-kit-xs text-kit-fg-3\">No link yet</span>\n )}\n </div>\n <div\n className=\"flex flex-col gap-1.5\"\n onBlur={(e) => {\n if (!e.currentTarget.contains(e.relatedTarget as Node) && href.trim()) setProblem(linkProblem(href));\n }}\n >\n <label htmlFor={urlId} className=\"text-kit-xs text-kit-fg-2\">\n Link to\n </label>\n <Input\n id={urlId}\n autoFocus\n size=\"sm\"\n value={href}\n onChange={(e) => {\n setHref(e.target.value);\n if (problem) setProblem(null);\n }}\n onKeyDown={(e) => {\n // Apply is disabled while the address cannot be saved, so Enter says why instead of doing nothing\n if (e.key === \"Enter\" && !ok) {\n e.preventDefault();\n setProblem(linkProblem(href));\n }\n }}\n placeholder=\"https://… or /page\"\n leading={<IconLink stroke={1.5} />}\n aria-invalid={problem ? true : undefined}\n aria-describedby={problem ? errId : undefined}\n />\n {problem ? (\n <p id={errId} role=\"alert\" className=\"text-kit-xs text-kit-danger-fg\">\n {problem}\n </p>\n ) : null}\n </div>\n {allowNewTab ? (\n <label htmlFor={tabId} className=\"flex min-h-7 items-center justify-between gap-3\">\n <span className=\"text-kit-xs text-kit-fg-2\">Open in new tab</span>\n <Switch id={tabId} size=\"sm\" checked={newTab} onCheckedChange={setNewTab} />\n </label>\n ) : null}\n <div className=\"flex items-center gap-2 border-t border-kit-line pt-2.5\">\n {onDetach ? (\n <Button type=\"button\" size=\"xs\" variant=\"danger-ghost\" onClick={onDetach}>\n Remove link\n </Button>\n ) : null}\n <span className=\"flex-1\" />\n <Button type=\"button\" size=\"xs\" variant=\"ghost\" onClick={onCancel}>\n Cancel\n </Button>\n <Button type=\"submit\" size=\"xs\" variant=\"primary\" disabled={!changed || !ok}>\n Apply\n </Button>\n </div>\n </form>\n );\n}\n"],"mappings":"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"}
|
|
@@ -11,9 +11,15 @@ export interface RelatedModel {
|
|
|
11
11
|
posts: string[];
|
|
12
12
|
}
|
|
13
13
|
export declare const RELATED_LABEL = "Related posts";
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
/** `topic: false`: the collection has nothing to rank Same topic by, so a new block is Newest. */
|
|
15
|
+
export declare const relatedDefault: (topic?: boolean) => RelatedModel;
|
|
16
|
+
/** Why Same topic is not offered (RichTextRead.references.topic false; the entry write's relatedPosts.no_topic_field). */
|
|
17
|
+
export declare const NO_TOPIC_REASON = "Same topic needs a tags or category field on this collection.";
|
|
18
|
+
/**
|
|
19
|
+
* The panel form; `to` is the field's collection. "How many" explains the chosen automatic mode. With `topic` false,
|
|
20
|
+
* Same topic is left out and Stories says why (a stored Same topic block keeps the option, so its value still shows).
|
|
21
|
+
*/
|
|
22
|
+
export declare const relatedSchema: (to: string[], mode: RelatedMode, topic?: boolean) => TypeSchema;
|
|
17
23
|
/** What the preview asks the host for: the picked ids, or the automatic mode's query. */
|
|
18
24
|
export declare function relatedRequest(m: RelatedModel, collection?: string, excludeId?: string): KitRichTextRefRequest | null;
|
|
19
25
|
export declare function RelatedPreview({ m, onPick, ro, collection, excludeId }: {
|
|
@@ -23,7 +29,7 @@ export declare function RelatedPreview({ m, onPick, ro, collection, excludeId }:
|
|
|
23
29
|
collection?: string;
|
|
24
30
|
excludeId?: string;
|
|
25
31
|
}): import("react/jsx-runtime").JSX.Element;
|
|
26
|
-
export declare function RelatedBlock({ m, set, el, ctx, autoFocus, collection, excludeId, }: {
|
|
32
|
+
export declare function RelatedBlock({ m, set, el, ctx, autoFocus, collection, excludeId, topic, }: {
|
|
27
33
|
m: RelatedModel;
|
|
28
34
|
set: (fn: (m: RelatedModel) => RelatedModel, label?: string) => void;
|
|
29
35
|
el: IslandEl;
|
|
@@ -33,4 +39,6 @@ export declare function RelatedBlock({ m, set, el, ctx, autoFocus, collection, e
|
|
|
33
39
|
collection: string;
|
|
34
40
|
/** the entry being edited, left out of the automatic modes */
|
|
35
41
|
excludeId?: string;
|
|
42
|
+
/** false: the collection has no topic field, so Same topic is not offered */
|
|
43
|
+
topic?: boolean;
|
|
36
44
|
}): import("react/jsx-runtime").JSX.Element;
|
|
@@ -20,20 +20,26 @@ import { IconPlus } from "@tabler/icons-react";
|
|
|
20
20
|
* A fresh insert focuses the card (V2 article-blocks.tsx:131).
|
|
21
21
|
*/
|
|
22
22
|
var RELATED_LABEL = "Related posts";
|
|
23
|
-
|
|
23
|
+
/** `topic: false`: the collection has nothing to rank Same topic by, so a new block is Newest. */
|
|
24
|
+
var relatedDefault = (topic = true) => ({
|
|
24
25
|
title: "Related stories",
|
|
25
|
-
mode: "topic",
|
|
26
|
+
mode: topic ? "topic" : "newest",
|
|
26
27
|
count: 3,
|
|
27
28
|
posts: []
|
|
28
29
|
});
|
|
30
|
+
/** Why Same topic is not offered (RichTextRead.references.topic false; the entry write's relatedPosts.no_topic_field). */
|
|
31
|
+
var NO_TOPIC_REASON = "Same topic needs a tags or category field on this collection.";
|
|
29
32
|
var MODE_TAG = {
|
|
30
33
|
topic: "Same topic",
|
|
31
34
|
newest: "Newest"
|
|
32
35
|
};
|
|
33
36
|
var asMode = (v) => v === "newest" || v === "manual" ? v : "topic";
|
|
34
37
|
var clampCount = (v) => Math.min(6, Math.max(1, Math.round(Number(v) || 3)));
|
|
35
|
-
/**
|
|
36
|
-
|
|
38
|
+
/**
|
|
39
|
+
* The panel form; `to` is the field's collection. "How many" explains the chosen automatic mode. With `topic` false,
|
|
40
|
+
* Same topic is left out and Stories says why (a stored Same topic block keeps the option, so its value still shows).
|
|
41
|
+
*/
|
|
42
|
+
var relatedSchema = (to, mode, topic = true) => ({
|
|
37
43
|
name: "relatedPosts",
|
|
38
44
|
title: RELATED_LABEL,
|
|
39
45
|
kind: "object",
|
|
@@ -50,12 +56,12 @@ var relatedSchema = (to, mode) => ({
|
|
|
50
56
|
title: "Stories",
|
|
51
57
|
type: "select",
|
|
52
58
|
layout: "segmented",
|
|
53
|
-
initialValue: "topic",
|
|
59
|
+
initialValue: topic ? "topic" : "newest",
|
|
54
60
|
options: [
|
|
55
|
-
{
|
|
61
|
+
...topic || mode === "topic" ? [{
|
|
56
62
|
value: "topic",
|
|
57
63
|
label: "Same topic"
|
|
58
|
-
},
|
|
64
|
+
}] : [],
|
|
59
65
|
{
|
|
60
66
|
value: "newest",
|
|
61
67
|
label: "Newest"
|
|
@@ -64,7 +70,8 @@ var relatedSchema = (to, mode) => ({
|
|
|
64
70
|
value: "manual",
|
|
65
71
|
label: "Hand-picked"
|
|
66
72
|
}
|
|
67
|
-
]
|
|
73
|
+
],
|
|
74
|
+
...topic ? {} : { description: NO_TOPIC_REASON }
|
|
68
75
|
},
|
|
69
76
|
{
|
|
70
77
|
name: "count",
|
|
@@ -174,8 +181,12 @@ function RelatedPreview({ m, onPick, ro, collection, excludeId }) {
|
|
|
174
181
|
})]
|
|
175
182
|
});
|
|
176
183
|
}
|
|
177
|
-
function RelatedBlock({ m, set, el, ctx, autoFocus, collection, excludeId }) {
|
|
178
|
-
const schema = React.useMemo(() => relatedSchema([collection], m.mode), [
|
|
184
|
+
function RelatedBlock({ m, set, el, ctx, autoFocus, collection, excludeId, topic = true }) {
|
|
185
|
+
const schema = React.useMemo(() => relatedSchema([collection], m.mode, topic), [
|
|
186
|
+
collection,
|
|
187
|
+
m.mode,
|
|
188
|
+
topic
|
|
189
|
+
]);
|
|
179
190
|
return /* @__PURE__ */ jsx(ArticlePanelBlock, {
|
|
180
191
|
label: RELATED_LABEL,
|
|
181
192
|
kind: "related",
|
|
@@ -208,6 +219,6 @@ function RelatedBlock({ m, set, el, ctx, autoFocus, collection, excludeId }) {
|
|
|
208
219
|
});
|
|
209
220
|
}
|
|
210
221
|
//#endregion
|
|
211
|
-
export { RELATED_LABEL, RelatedBlock, RelatedPreview, relatedDefault, relatedRequest, relatedSchema };
|
|
222
|
+
export { NO_TOPIC_REASON, RELATED_LABEL, RelatedBlock, RelatedPreview, relatedDefault, relatedRequest, relatedSchema };
|
|
212
223
|
|
|
213
224
|
//# sourceMappingURL=related.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"related.js","names":[],"sources":["../../../src/controls/rich-text/related.tsx"],"sourcesContent":["/**\n * RICH TEXT · Related posts (kit-richtext-ui-spec.md §6): V2's article block, stored as the PT block object\n * `{ _type: \"relatedPosts\", title?, mode, count, posts: { _ref }[] }`. Each post's title, cover, category and URL are\n * derived from the referenced or queried entries, never stored.\n *\n * modes e1 2026-10-24 (signed deviation from V2's Automatic | Hand-picked): Same topic (topic) · Newest (newest) ·\n * Hand-picked (manual). \"How many\" shows for the two automatic modes, Posts for Hand-picked. The schema's\n * `hidden` is \"show only when\" (schema/types.ts), as V2's was.\n * form V2 article.ts:167-195: Title (≤40, default \"Related stories\"), Stories (segmented), How many (1–6),\n * Posts (1–6 unique references to the field's collection). Four settings → the floating panel (D54).\n * preview V2 article-blocks.tsx:572-597: the title with the automatic mode's tag, three post cards (4:3 cover,\n * two-line title, category); \"Pick stories\" when Hand-picked holds nothing that resolves.\n * data KitServices.resolveRichTextRefs (useRichTextRefs): ids for Hand-picked, a query for the automatic modes.\n * A fresh insert focuses the card (V2 article-blocks.tsx:131).\n */\nimport * as React from \"react\";\nimport { IconPlus } from \"@tabler/icons-react\";\nimport { useRichTextRefs, type KitRichTextRefRequest } from \"../../context\";\nimport type { TypeSchema } from \"../../schema/types\";\nimport { ArticleEmpty, ArticlePanelBlock } from \"./article-shell\";\nimport type { IslandCtx } from \"./blocks\";\nimport type { IslandEl } from \"./dom\";\n\nexport type RelatedMode = \"topic\" | \"newest\" | \"manual\";\nexport interface RelatedModel {\n title: string;\n mode: RelatedMode;\n count: number;\n /** entry ids, Hand-picked only; the PT form wraps each as `{ _key, _ref }` */\n posts: string[];\n}\nexport const RELATED_LABEL = \"Related posts\";\nexport const relatedDefault = (): RelatedModel => ({ title: \"Related stories\", mode: \"topic\", count: 3, posts: [] });\nconst MODE_TAG: Record<Exclude<RelatedMode, \"manual\">, string> = { topic: \"Same topic\", newest: \"Newest\" };\nconst asMode = (v: unknown): RelatedMode => (v === \"newest\" || v === \"manual\" ? v : \"topic\");\nconst clampCount = (v: unknown) => Math.min(6, Math.max(1, Math.round(Number(v) || 3)));\n\n/** The panel form; `to` is the field's collection. \"How many\" explains the chosen automatic mode. */\nexport const relatedSchema = (to: string[], mode: RelatedMode): TypeSchema => ({\n name: \"relatedPosts\",\n title: RELATED_LABEL,\n kind: \"object\",\n icon: \"news\",\n fields: [\n { name: \"title\", title: \"Title\", type: \"string\", validation: { max: 40 } },\n {\n name: \"mode\",\n title: \"Stories\",\n type: \"select\",\n layout: \"segmented\",\n initialValue: \"topic\",\n options: [\n { value: \"topic\", label: \"Same topic\" },\n { value: \"newest\", label: \"Newest\" },\n { value: \"manual\", label: \"Hand-picked\" },\n ],\n },\n {\n name: \"count\",\n title: \"How many\",\n type: \"number\",\n hidden: { field: \"mode\", op: \"in\", value: [\"topic\", \"newest\"] },\n numeric: { min: 1, max: 6, step: 1 },\n description: mode === \"newest\" ? \"Newest first.\" : \"Same topic, newest first.\",\n },\n {\n name: \"posts\",\n title: \"Posts\",\n type: \"array\",\n hidden: { field: \"mode\", equals: \"manual\" },\n validation: { min: 1, max: 6, unique: true },\n of: [{ name: \"post\", title: \"Post\", type: \"reference\", reference: { to } }],\n },\n ],\n});\n\n/** What the preview asks the host for: the picked ids, or the automatic mode's query. */\nexport function relatedRequest(m: RelatedModel, collection?: string, excludeId?: string): KitRichTextRefRequest | null {\n if (m.mode === \"manual\") return m.posts.length ? { kind: \"relatedPosts\", ids: m.posts } : null;\n return { kind: \"relatedPosts\", query: { collection, mode: m.mode, count: m.count, excludeId } };\n}\n\nexport function RelatedPreview({ m, onPick, ro, collection, excludeId }: { m: RelatedModel; onPick: () => void; ro: boolean; collection?: string; excludeId?: string }) {\n const { items, loading, error } = useRichTextRefs(relatedRequest(m, collection, excludeId));\n // Hand-picked keeps the picked order; a post that no longer resolves is skipped (V2 .filter)\n const posts = m.mode === \"manual\" ? m.posts.map((id) => items.find((x) => x.id === id)).filter((x): x is NonNullable<typeof x> => !!x) : items;\n if (m.mode === \"manual\" && !loading && !error && !posts.length)\n return <ArticleEmpty icon={<IconPlus stroke={1.5} />} title={m.title || RELATED_LABEL} action=\"Pick stories\" onAction={onPick} ro={ro} />;\n return (\n <aside className=\"rounded-kit-lg border border-kit-line bg-kit-surface px-4 py-3\" aria-label={m.title || RELATED_LABEL} aria-busy={loading || undefined}>\n <div className=\"mb-2 flex items-baseline gap-2\">\n {m.title ? (\n <p data-field=\"title\" className=\"text-kit-sm font-semibold text-kit-fg\">\n {m.title}\n </p>\n ) : null}\n {m.mode !== \"manual\" ? (\n <span data-field=\"mode\" className=\"text-kit-2xs text-kit-fg-3\">\n {MODE_TAG[m.mode]}\n </span>\n ) : null}\n </div>\n {error ? (\n <p role=\"status\" className=\"text-kit-sm text-kit-warning-fg\">\n The stories couldn't load: {error}\n </p>\n ) : (\n <ul className=\"m-0 grid grid-cols-3 gap-3 p-0\" data-field={m.mode === \"manual\" ? \"posts\" : \"count\"}>\n {posts.map((e) => (\n <li key={e.id} className=\"min-w-0 list-none\">\n {e.cover ? (\n <img src={e.cover} alt=\"\" draggable={false} className=\"block aspect-[4/3] w-full rounded-kit-md border border-kit-line object-cover\" />\n ) : (\n <div className=\"aspect-[4/3] rounded-kit-md bg-kit-muted\" />\n )}\n <p className=\"mt-1.5 line-clamp-2 text-kit-xs font-medium leading-snug text-kit-fg\">{e.title}</p>\n {e.category ? <p className=\"mt-0.5 truncate text-kit-2xs text-kit-fg-3\">{e.category}</p> : null}\n </li>\n ))}\n </ul>\n )}\n </aside>\n );\n}\n\nexport function RelatedBlock({\n m, set, el, ctx, autoFocus, collection, excludeId,\n}: {\n m: RelatedModel;\n set: (fn: (m: RelatedModel) => RelatedModel, label?: string) => void;\n el: IslandEl;\n ctx: IslandCtx;\n autoFocus: boolean;\n /** the posts collection (the field's config) */\n collection: string;\n /** the entry being edited, left out of the automatic modes */\n excludeId?: string;\n}) {\n const schema = React.useMemo(() => relatedSchema([collection], m.mode), [collection, m.mode]);\n return (\n <ArticlePanelBlock<RelatedModel>\n label={RELATED_LABEL}\n kind=\"related\"\n schema={schema}\n m={m}\n set={set}\n el={el}\n ctx={ctx}\n autoFocus={autoFocus}\n openOnInsert={false}\n toForm={(x) => ({ title: x.title, mode: x.mode, count: x.count, posts: x.posts })}\n fromForm={(v) => ({\n title: String(v.title ?? \"\"),\n mode: asMode(v.mode),\n count: clampCount(v.count),\n posts: Array.isArray(v.posts) ? v.posts.filter((x): x is string => typeof x === \"string\") : [],\n })}\n >\n {(open, ro) => <RelatedPreview m={m} onPick={() => open(\"posts\")} ro={ro} collection={collection} excludeId={excludeId} />}\n </ArticlePanelBlock>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"related.js","names":[],"sources":["../../../src/controls/rich-text/related.tsx"],"sourcesContent":["/**\n * RICH TEXT · Related posts (kit-richtext-ui-spec.md §6): V2's article block, stored as the PT block object\n * `{ _type: \"relatedPosts\", title?, mode, count, posts: { _ref }[] }`. Each post's title, cover, category and URL are\n * derived from the referenced or queried entries, never stored.\n *\n * modes e1 2026-10-24 (signed deviation from V2's Automatic | Hand-picked): Same topic (topic) · Newest (newest) ·\n * Hand-picked (manual). \"How many\" shows for the two automatic modes, Posts for Hand-picked. The schema's\n * `hidden` is \"show only when\" (schema/types.ts), as V2's was.\n * form V2 article.ts:167-195: Title (≤40, default \"Related stories\"), Stories (segmented), How many (1–6),\n * Posts (1–6 unique references to the field's collection). Four settings → the floating panel (D54).\n * preview V2 article-blocks.tsx:572-597: the title with the automatic mode's tag, three post cards (4:3 cover,\n * two-line title, category); \"Pick stories\" when Hand-picked holds nothing that resolves.\n * data KitServices.resolveRichTextRefs (useRichTextRefs): ids for Hand-picked, a query for the automatic modes.\n * A fresh insert focuses the card (V2 article-blocks.tsx:131).\n */\nimport * as React from \"react\";\nimport { IconPlus } from \"@tabler/icons-react\";\nimport { useRichTextRefs, type KitRichTextRefRequest } from \"../../context\";\nimport type { TypeSchema } from \"../../schema/types\";\nimport { ArticleEmpty, ArticlePanelBlock } from \"./article-shell\";\nimport type { IslandCtx } from \"./blocks\";\nimport type { IslandEl } from \"./dom\";\n\nexport type RelatedMode = \"topic\" | \"newest\" | \"manual\";\nexport interface RelatedModel {\n title: string;\n mode: RelatedMode;\n count: number;\n /** entry ids, Hand-picked only; the PT form wraps each as `{ _key, _ref }` */\n posts: string[];\n}\nexport const RELATED_LABEL = \"Related posts\";\n/** `topic: false`: the collection has nothing to rank Same topic by, so a new block is Newest. */\nexport const relatedDefault = (topic = true): RelatedModel => ({ title: \"Related stories\", mode: topic ? \"topic\" : \"newest\", count: 3, posts: [] });\n/** Why Same topic is not offered (RichTextRead.references.topic false; the entry write's relatedPosts.no_topic_field). */\nexport const NO_TOPIC_REASON = \"Same topic needs a tags or category field on this collection.\";\nconst MODE_TAG: Record<Exclude<RelatedMode, \"manual\">, string> = { topic: \"Same topic\", newest: \"Newest\" };\nconst asMode = (v: unknown): RelatedMode => (v === \"newest\" || v === \"manual\" ? v : \"topic\");\nconst clampCount = (v: unknown) => Math.min(6, Math.max(1, Math.round(Number(v) || 3)));\n\n/**\n * The panel form; `to` is the field's collection. \"How many\" explains the chosen automatic mode. With `topic` false,\n * Same topic is left out and Stories says why (a stored Same topic block keeps the option, so its value still shows).\n */\nexport const relatedSchema = (to: string[], mode: RelatedMode, topic = true): TypeSchema => ({\n name: \"relatedPosts\",\n title: RELATED_LABEL,\n kind: \"object\",\n icon: \"news\",\n fields: [\n { name: \"title\", title: \"Title\", type: \"string\", validation: { max: 40 } },\n {\n name: \"mode\",\n title: \"Stories\",\n type: \"select\",\n layout: \"segmented\",\n initialValue: topic ? \"topic\" : \"newest\",\n options: [\n ...(topic || mode === \"topic\" ? [{ value: \"topic\", label: \"Same topic\" }] : []),\n { value: \"newest\", label: \"Newest\" },\n { value: \"manual\", label: \"Hand-picked\" },\n ],\n ...(topic ? {} : { description: NO_TOPIC_REASON }),\n },\n {\n name: \"count\",\n title: \"How many\",\n type: \"number\",\n hidden: { field: \"mode\", op: \"in\", value: [\"topic\", \"newest\"] },\n numeric: { min: 1, max: 6, step: 1 },\n description: mode === \"newest\" ? \"Newest first.\" : \"Same topic, newest first.\",\n },\n {\n name: \"posts\",\n title: \"Posts\",\n type: \"array\",\n hidden: { field: \"mode\", equals: \"manual\" },\n validation: { min: 1, max: 6, unique: true },\n of: [{ name: \"post\", title: \"Post\", type: \"reference\", reference: { to } }],\n },\n ],\n});\n\n/** What the preview asks the host for: the picked ids, or the automatic mode's query. */\nexport function relatedRequest(m: RelatedModel, collection?: string, excludeId?: string): KitRichTextRefRequest | null {\n if (m.mode === \"manual\") return m.posts.length ? { kind: \"relatedPosts\", ids: m.posts } : null;\n return { kind: \"relatedPosts\", query: { collection, mode: m.mode, count: m.count, excludeId } };\n}\n\nexport function RelatedPreview({ m, onPick, ro, collection, excludeId }: { m: RelatedModel; onPick: () => void; ro: boolean; collection?: string; excludeId?: string }) {\n const { items, loading, error } = useRichTextRefs(relatedRequest(m, collection, excludeId));\n // Hand-picked keeps the picked order; a post that no longer resolves is skipped (V2 .filter)\n const posts = m.mode === \"manual\" ? m.posts.map((id) => items.find((x) => x.id === id)).filter((x): x is NonNullable<typeof x> => !!x) : items;\n if (m.mode === \"manual\" && !loading && !error && !posts.length)\n return <ArticleEmpty icon={<IconPlus stroke={1.5} />} title={m.title || RELATED_LABEL} action=\"Pick stories\" onAction={onPick} ro={ro} />;\n return (\n <aside className=\"rounded-kit-lg border border-kit-line bg-kit-surface px-4 py-3\" aria-label={m.title || RELATED_LABEL} aria-busy={loading || undefined}>\n <div className=\"mb-2 flex items-baseline gap-2\">\n {m.title ? (\n <p data-field=\"title\" className=\"text-kit-sm font-semibold text-kit-fg\">\n {m.title}\n </p>\n ) : null}\n {m.mode !== \"manual\" ? (\n <span data-field=\"mode\" className=\"text-kit-2xs text-kit-fg-3\">\n {MODE_TAG[m.mode]}\n </span>\n ) : null}\n </div>\n {error ? (\n <p role=\"status\" className=\"text-kit-sm text-kit-warning-fg\">\n The stories couldn't load: {error}\n </p>\n ) : (\n <ul className=\"m-0 grid grid-cols-3 gap-3 p-0\" data-field={m.mode === \"manual\" ? \"posts\" : \"count\"}>\n {posts.map((e) => (\n <li key={e.id} className=\"min-w-0 list-none\">\n {e.cover ? (\n <img src={e.cover} alt=\"\" draggable={false} className=\"block aspect-[4/3] w-full rounded-kit-md border border-kit-line object-cover\" />\n ) : (\n <div className=\"aspect-[4/3] rounded-kit-md bg-kit-muted\" />\n )}\n <p className=\"mt-1.5 line-clamp-2 text-kit-xs font-medium leading-snug text-kit-fg\">{e.title}</p>\n {e.category ? <p className=\"mt-0.5 truncate text-kit-2xs text-kit-fg-3\">{e.category}</p> : null}\n </li>\n ))}\n </ul>\n )}\n </aside>\n );\n}\n\nexport function RelatedBlock({\n m, set, el, ctx, autoFocus, collection, excludeId, topic = true,\n}: {\n m: RelatedModel;\n set: (fn: (m: RelatedModel) => RelatedModel, label?: string) => void;\n el: IslandEl;\n ctx: IslandCtx;\n autoFocus: boolean;\n /** the posts collection (the field's config) */\n collection: string;\n /** the entry being edited, left out of the automatic modes */\n excludeId?: string;\n /** false: the collection has no topic field, so Same topic is not offered */\n topic?: boolean;\n}) {\n const schema = React.useMemo(() => relatedSchema([collection], m.mode, topic), [collection, m.mode, topic]);\n return (\n <ArticlePanelBlock<RelatedModel>\n label={RELATED_LABEL}\n kind=\"related\"\n schema={schema}\n m={m}\n set={set}\n el={el}\n ctx={ctx}\n autoFocus={autoFocus}\n openOnInsert={false}\n toForm={(x) => ({ title: x.title, mode: x.mode, count: x.count, posts: x.posts })}\n fromForm={(v) => ({\n title: String(v.title ?? \"\"),\n mode: asMode(v.mode),\n count: clampCount(v.count),\n posts: Array.isArray(v.posts) ? v.posts.filter((x): x is string => typeof x === \"string\") : [],\n })}\n >\n {(open, ro) => <RelatedPreview m={m} onPick={() => open(\"posts\")} ro={ro} collection={collection} excludeId={excludeId} />}\n </ArticlePanelBlock>\n );\n}\n"],"mappings":"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"}
|