@bettercms-ai/kit 2.0.0-next.0 → 2.0.0-next.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/lossy.d.ts +5 -1
- package/dist/adapters/lossy.js +10 -5
- package/dist/adapters/lossy.js.map +1 -1
- package/dist/adapters/prod-types.d.ts +9 -22
- package/dist/adapters/prod-types.js +36 -35
- package/dist/adapters/prod-types.js.map +1 -1
- package/dist/adapters/schema.js +56 -39
- 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/link.js +112 -196
- 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/rich-text/article-shell.d.ts +145 -0
- package/dist/controls/rich-text/article-shell.js +510 -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 +104 -0
- package/dist/controls/rich-text/author.js.map +1 -0
- package/dist/controls/rich-text/blocks.d.ts +13 -2
- package/dist/controls/rich-text/blocks.js +86 -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 +85 -0
- package/dist/controls/rich-text/button.js.map +1 -0
- package/dist/controls/rich-text/catalog.d.ts +3 -3
- package/dist/controls/rich-text/catalog.js +20 -4
- 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 +1 -1
- package/dist/controls/rich-text/codec.js +4 -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/dom.d.ts +5 -4
- package/dist/controls/rich-text/dom.js +13 -4
- package/dist/controls/rich-text/dom.js.map +1 -1
- package/dist/controls/rich-text/editor.js +92 -10
- 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 +3 -1
- package/dist/controls/rich-text/link-form.js +2 -2
- package/dist/controls/rich-text/link-form.js.map +1 -1
- package/dist/controls/rich-text/related.d.ts +36 -0
- package/dist/controls/rich-text/related.js +213 -0
- package/dist/controls/rich-text/related.js.map +1 -0
- package/dist/controls/rich-text/toc.d.ts +46 -0
- package/dist/controls/rich-text/toc.js +161 -0
- package/dist/controls/rich-text/toc.js.map +1 -0
- package/dist/core/core.d.ts +10 -3
- package/dist/core/core.js +22 -6
- package/dist/core/core.js.map +1 -1
- package/dist/core/facets.d.ts +8 -0
- package/dist/core/facets.js +9 -1
- package/dist/core/facets.js.map +1 -1
- package/dist/core/index.js +2 -2
- package/dist/index.js +4 -4
- package/dist/library/kit-catalog.json +45 -44
- package/dist/schema/types.d.ts +7 -0
- package/dist/schema/types.js.map +1 -1
- package/dist/styles.css +1 -1
- 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/toast.js +1 -1
- package/package.json +3 -1
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { cn } from "../../lib/cn.js";
|
|
2
|
+
import { ArticleEmpty, ArticlePanelBlock } from "./article-shell.js";
|
|
3
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
import { IconPhoto } from "@tabler/icons-react";
|
|
5
|
+
//#region src/controls/rich-text/gallery.tsx
|
|
6
|
+
/**
|
|
7
|
+
* RICH TEXT · Gallery (kit-richtext-ui-spec.md §2): V2's article block, stored as the PT block object
|
|
8
|
+
* `{ _type: "gallery", layout: "grid" | "carousel", images: { _key, image, caption? }[] }`; each image is the rich-text
|
|
9
|
+
* image's own shape (src/alt + media id), so media rewrites and alt audits reach it.
|
|
10
|
+
*
|
|
11
|
+
* form V2 article.ts:75-114: Images (a grid list, 2–24, each a required image + caption ≤160), Layout
|
|
12
|
+
* (Grid | Carousel, segmented). A list of objects → the floating properties panel (D54); a fresh insert opens it.
|
|
13
|
+
* preview V2 article-blocks.tsx:451-480: Grid in 2 columns for 2 or 4 images, else 3, square crops; Carousel a
|
|
14
|
+
* 72%-wide scroll-snap row of 4:3 crops with "N images · carousel"; "Add images" when there are none.
|
|
15
|
+
*/
|
|
16
|
+
var GALLERY_LABEL = "Gallery";
|
|
17
|
+
var galleryDefault = () => ({
|
|
18
|
+
layout: "grid",
|
|
19
|
+
images: []
|
|
20
|
+
});
|
|
21
|
+
var GALLERY_SCHEMA = {
|
|
22
|
+
name: "gallery",
|
|
23
|
+
title: GALLERY_LABEL,
|
|
24
|
+
kind: "object",
|
|
25
|
+
icon: "photo",
|
|
26
|
+
fields: [{
|
|
27
|
+
name: "images",
|
|
28
|
+
title: "Images",
|
|
29
|
+
type: "array",
|
|
30
|
+
itemNoun: "image",
|
|
31
|
+
arrayLayout: "grid",
|
|
32
|
+
validation: {
|
|
33
|
+
min: 2,
|
|
34
|
+
max: 24
|
|
35
|
+
},
|
|
36
|
+
of: [{
|
|
37
|
+
name: "photo",
|
|
38
|
+
title: "Image",
|
|
39
|
+
type: "object",
|
|
40
|
+
icon: "photo",
|
|
41
|
+
preview: {
|
|
42
|
+
title: "caption",
|
|
43
|
+
media: "image",
|
|
44
|
+
fallback: "Untitled image"
|
|
45
|
+
},
|
|
46
|
+
fields: [{
|
|
47
|
+
name: "image",
|
|
48
|
+
title: "Image",
|
|
49
|
+
type: "image",
|
|
50
|
+
validation: { required: true }
|
|
51
|
+
}, {
|
|
52
|
+
name: "caption",
|
|
53
|
+
title: "Caption",
|
|
54
|
+
type: "string",
|
|
55
|
+
validation: { max: 160 }
|
|
56
|
+
}]
|
|
57
|
+
}]
|
|
58
|
+
}, {
|
|
59
|
+
name: "layout",
|
|
60
|
+
title: "Layout",
|
|
61
|
+
type: "select",
|
|
62
|
+
layout: "segmented",
|
|
63
|
+
initialValue: "grid",
|
|
64
|
+
options: [{
|
|
65
|
+
value: "grid",
|
|
66
|
+
label: "Grid"
|
|
67
|
+
}, {
|
|
68
|
+
value: "carousel",
|
|
69
|
+
label: "Carousel"
|
|
70
|
+
}]
|
|
71
|
+
}]
|
|
72
|
+
};
|
|
73
|
+
function GalleryPreview({ m, onAdd, ro }) {
|
|
74
|
+
const imgs = m.images.filter((x) => x.image?.url);
|
|
75
|
+
if (!imgs.length) return /* @__PURE__ */ jsx(ArticleEmpty, {
|
|
76
|
+
icon: /* @__PURE__ */ jsx(IconPhoto, { stroke: 1.5 }),
|
|
77
|
+
title: GALLERY_LABEL,
|
|
78
|
+
action: "Add images",
|
|
79
|
+
onAction: onAdd,
|
|
80
|
+
ro
|
|
81
|
+
});
|
|
82
|
+
if (m.layout === "carousel") return /* @__PURE__ */ jsxs("figure", {
|
|
83
|
+
className: "m-0",
|
|
84
|
+
"data-field": "images",
|
|
85
|
+
children: [/* @__PURE__ */ jsx("ul", {
|
|
86
|
+
className: "m-0 flex snap-x gap-2 overflow-x-auto p-0 pb-1",
|
|
87
|
+
"aria-label": GALLERY_LABEL,
|
|
88
|
+
children: imgs.map((x) => /* @__PURE__ */ jsxs("li", {
|
|
89
|
+
className: "w-[72%] shrink-0 snap-start list-none",
|
|
90
|
+
children: [/* @__PURE__ */ jsx("img", {
|
|
91
|
+
src: x.image.url,
|
|
92
|
+
alt: x.image.alt ?? "",
|
|
93
|
+
draggable: false,
|
|
94
|
+
className: "block aspect-[4/3] w-full rounded-kit-lg border border-kit-line object-cover"
|
|
95
|
+
}), x.caption ? /* @__PURE__ */ jsx("p", {
|
|
96
|
+
className: "mt-1 truncate text-kit-xs text-kit-fg-3",
|
|
97
|
+
children: x.caption
|
|
98
|
+
}) : null]
|
|
99
|
+
}, x._key))
|
|
100
|
+
}), /* @__PURE__ */ jsxs("figcaption", {
|
|
101
|
+
className: "mt-1 text-kit-2xs text-kit-fg-3",
|
|
102
|
+
children: [
|
|
103
|
+
imgs.length,
|
|
104
|
+
" ",
|
|
105
|
+
imgs.length === 1 ? "image" : "images",
|
|
106
|
+
" · carousel"
|
|
107
|
+
]
|
|
108
|
+
})]
|
|
109
|
+
});
|
|
110
|
+
return /* @__PURE__ */ jsx("figure", {
|
|
111
|
+
className: "m-0",
|
|
112
|
+
"data-field": "images",
|
|
113
|
+
children: /* @__PURE__ */ jsx("ul", {
|
|
114
|
+
className: cn("m-0 grid gap-2 p-0", imgs.length === 2 || imgs.length === 4 ? "grid-cols-2" : "grid-cols-3"),
|
|
115
|
+
"aria-label": GALLERY_LABEL,
|
|
116
|
+
children: imgs.map((x) => /* @__PURE__ */ jsxs("li", {
|
|
117
|
+
className: "list-none",
|
|
118
|
+
children: [/* @__PURE__ */ jsx("img", {
|
|
119
|
+
src: x.image.url,
|
|
120
|
+
alt: x.image.alt ?? "",
|
|
121
|
+
draggable: false,
|
|
122
|
+
className: "block aspect-square w-full rounded-kit-lg border border-kit-line object-cover"
|
|
123
|
+
}), x.caption ? /* @__PURE__ */ jsx("p", {
|
|
124
|
+
className: "mt-1 truncate text-kit-xs text-kit-fg-3",
|
|
125
|
+
children: x.caption
|
|
126
|
+
}) : null]
|
|
127
|
+
}, x._key))
|
|
128
|
+
})
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
var asImages = (v) => Array.isArray(v) ? v.map((x, i) => {
|
|
132
|
+
const o = x ?? {};
|
|
133
|
+
return {
|
|
134
|
+
_key: typeof o._key === "string" ? o._key : `g${i}`,
|
|
135
|
+
image: o.image ?? null,
|
|
136
|
+
caption: typeof o.caption === "string" ? o.caption : ""
|
|
137
|
+
};
|
|
138
|
+
}) : [];
|
|
139
|
+
function GalleryBlock({ m, set, el, ctx, autoFocus }) {
|
|
140
|
+
return /* @__PURE__ */ jsx(ArticlePanelBlock, {
|
|
141
|
+
label: GALLERY_LABEL,
|
|
142
|
+
kind: "gallery",
|
|
143
|
+
schema: GALLERY_SCHEMA,
|
|
144
|
+
m,
|
|
145
|
+
set,
|
|
146
|
+
el,
|
|
147
|
+
ctx,
|
|
148
|
+
autoFocus,
|
|
149
|
+
toForm: (x) => ({
|
|
150
|
+
images: x.images,
|
|
151
|
+
layout: x.layout
|
|
152
|
+
}),
|
|
153
|
+
fromForm: (v) => ({
|
|
154
|
+
images: asImages(v.images),
|
|
155
|
+
layout: v.layout === "carousel" ? "carousel" : "grid"
|
|
156
|
+
}),
|
|
157
|
+
children: (open, ro) => /* @__PURE__ */ jsx(GalleryPreview, {
|
|
158
|
+
m,
|
|
159
|
+
onAdd: () => open("images"),
|
|
160
|
+
ro
|
|
161
|
+
})
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
//#endregion
|
|
165
|
+
export { GALLERY_LABEL, GALLERY_SCHEMA, GalleryBlock, GalleryPreview, galleryDefault };
|
|
166
|
+
|
|
167
|
+
//# sourceMappingURL=gallery.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gallery.js","names":[],"sources":["../../../src/controls/rich-text/gallery.tsx"],"sourcesContent":["/**\n * RICH TEXT · Gallery (kit-richtext-ui-spec.md §2): V2's article block, stored as the PT block object\n * `{ _type: \"gallery\", layout: \"grid\" | \"carousel\", images: { _key, image, caption? }[] }`; each image is the rich-text\n * image's own shape (src/alt + media id), so media rewrites and alt audits reach it.\n *\n * form V2 article.ts:75-114: Images (a grid list, 2–24, each a required image + caption ≤160), Layout\n * (Grid | Carousel, segmented). A list of objects → the floating properties panel (D54); a fresh insert opens it.\n * preview V2 article-blocks.tsx:451-480: Grid in 2 columns for 2 or 4 images, else 3, square crops; Carousel a\n * 72%-wide scroll-snap row of 4:3 crops with \"N images · carousel\"; \"Add images\" when there are none.\n */\nimport { IconPhoto } from \"@tabler/icons-react\";\nimport { cn } from \"../../lib/cn\";\nimport type { TypeSchema } from \"../../schema/types\";\nimport type { ImageValue } from \"../media\";\nimport { ArticleEmpty, ArticlePanelBlock } from \"./article-shell\";\nimport type { IslandCtx } from \"./blocks\";\nimport type { IslandEl } from \"./dom\";\n\nexport interface GalleryImage {\n _key: string;\n image: ImageValue | null;\n caption: string;\n}\nexport interface GalleryModel {\n layout: \"grid\" | \"carousel\";\n images: GalleryImage[];\n}\nexport const GALLERY_LABEL = \"Gallery\";\nexport const galleryDefault = (): GalleryModel => ({ layout: \"grid\", images: [] });\n\nexport const GALLERY_SCHEMA: TypeSchema = {\n name: \"gallery\",\n title: GALLERY_LABEL,\n kind: \"object\",\n icon: \"photo\",\n fields: [\n {\n name: \"images\",\n title: \"Images\",\n type: \"array\",\n itemNoun: \"image\",\n arrayLayout: \"grid\",\n validation: { min: 2, max: 24 },\n of: [\n {\n name: \"photo\",\n title: \"Image\",\n type: \"object\",\n icon: \"photo\",\n preview: { title: \"caption\", media: \"image\", fallback: \"Untitled image\" },\n fields: [\n { name: \"image\", title: \"Image\", type: \"image\", validation: { required: true } },\n { name: \"caption\", title: \"Caption\", type: \"string\", validation: { max: 160 } },\n ],\n },\n ],\n },\n {\n name: \"layout\",\n title: \"Layout\",\n type: \"select\",\n layout: \"segmented\",\n initialValue: \"grid\",\n options: [\n { value: \"grid\", label: \"Grid\" },\n { value: \"carousel\", label: \"Carousel\" },\n ],\n },\n ],\n};\n\nexport function GalleryPreview({ m, onAdd, ro }: { m: GalleryModel; onAdd: () => void; ro: boolean }) {\n const imgs = m.images.filter((x) => x.image?.url);\n if (!imgs.length) return <ArticleEmpty icon={<IconPhoto stroke={1.5} />} title={GALLERY_LABEL} action=\"Add images\" onAction={onAdd} ro={ro} />;\n if (m.layout === \"carousel\")\n return (\n <figure className=\"m-0\" data-field=\"images\">\n <ul className=\"m-0 flex snap-x gap-2 overflow-x-auto p-0 pb-1\" aria-label={GALLERY_LABEL}>\n {imgs.map((x) => (\n <li key={x._key} className=\"w-[72%] shrink-0 snap-start list-none\">\n <img src={x.image!.url} alt={x.image!.alt ?? \"\"} draggable={false} className=\"block aspect-[4/3] w-full rounded-kit-lg border border-kit-line object-cover\" />\n {x.caption ? <p className=\"mt-1 truncate text-kit-xs text-kit-fg-3\">{x.caption}</p> : null}\n </li>\n ))}\n </ul>\n <figcaption className=\"mt-1 text-kit-2xs text-kit-fg-3\">\n {imgs.length} {imgs.length === 1 ? \"image\" : \"images\"} · carousel\n </figcaption>\n </figure>\n );\n return (\n <figure className=\"m-0\" data-field=\"images\">\n <ul className={cn(\"m-0 grid gap-2 p-0\", imgs.length === 2 || imgs.length === 4 ? \"grid-cols-2\" : \"grid-cols-3\")} aria-label={GALLERY_LABEL}>\n {imgs.map((x) => (\n <li key={x._key} className=\"list-none\">\n <img src={x.image!.url} alt={x.image!.alt ?? \"\"} draggable={false} className=\"block aspect-square w-full rounded-kit-lg border border-kit-line object-cover\" />\n {x.caption ? <p className=\"mt-1 truncate text-kit-xs text-kit-fg-3\">{x.caption}</p> : null}\n </li>\n ))}\n </ul>\n </figure>\n );\n}\n\nconst asImages = (v: unknown): GalleryImage[] =>\n Array.isArray(v)\n ? v.map((x, i) => {\n const o = (x ?? {}) as Record<string, unknown>;\n return { _key: typeof o._key === \"string\" ? o._key : `g${i}`, image: (o.image as ImageValue | null) ?? null, caption: typeof o.caption === \"string\" ? o.caption : \"\" };\n })\n : [];\n\nexport function GalleryBlock({\n m, set, el, ctx, autoFocus,\n}: {\n m: GalleryModel;\n set: (fn: (m: GalleryModel) => GalleryModel, label?: string) => void;\n el: IslandEl;\n ctx: IslandCtx;\n autoFocus: boolean;\n}) {\n return (\n <ArticlePanelBlock<GalleryModel>\n label={GALLERY_LABEL}\n kind=\"gallery\"\n schema={GALLERY_SCHEMA}\n m={m}\n set={set}\n el={el}\n ctx={ctx}\n autoFocus={autoFocus}\n toForm={(x) => ({ images: x.images, layout: x.layout })}\n fromForm={(v) => ({ images: asImages(v.images), layout: v.layout === \"carousel\" ? \"carousel\" : \"grid\" })}\n >\n {(open, ro) => <GalleryPreview m={m} onAdd={() => open(\"images\")} ro={ro} />}\n </ArticlePanelBlock>\n );\n}\n"],"mappings":"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"}
|
|
@@ -4,10 +4,12 @@ export interface TextLink {
|
|
|
4
4
|
}
|
|
5
5
|
/** An address a link may carry: http(s), mailto, tel, a site path or an in-page anchor. */
|
|
6
6
|
export declare function linkProblem(href: string): string | null;
|
|
7
|
-
export declare function LinkForm({ initial, allowNewTab, onSave, onCancel, onDetach, }: {
|
|
7
|
+
export declare function LinkForm({ initial, allowNewTab, onSave, onCancel, onDetach, dirty, }: {
|
|
8
8
|
initial: TextLink;
|
|
9
9
|
allowNewTab: boolean;
|
|
10
10
|
onSave: (v: TextLink) => void;
|
|
11
11
|
onCancel: () => void;
|
|
12
12
|
onDetach?: () => void;
|
|
13
|
+
/** something the host draws beside the form changed (a button's label), so Apply has something to save */
|
|
14
|
+
dirty?: boolean;
|
|
13
15
|
}): import("react/jsx-runtime").JSX.Element;
|
|
@@ -21,7 +21,7 @@ function linkProblem(href) {
|
|
|
21
21
|
if (/^[^\s/]+\.[^\s]+$/.test(h)) return `Add https:// in front: https://${h}`;
|
|
22
22
|
return "Use a full address (https://…), a page path (/about) or an anchor (#section).";
|
|
23
23
|
}
|
|
24
|
-
function LinkForm({ initial, allowNewTab, onSave, onCancel, onDetach }) {
|
|
24
|
+
function LinkForm({ initial, allowNewTab, onSave, onCancel, onDetach, dirty = false }) {
|
|
25
25
|
const [href, setHref] = React.useState(initial.href);
|
|
26
26
|
const [newTab, setNewTab] = React.useState(!!initial.newTab);
|
|
27
27
|
const [problem, setProblem] = React.useState(null);
|
|
@@ -29,7 +29,7 @@ function LinkForm({ initial, allowNewTab, onSave, onCancel, onDetach }) {
|
|
|
29
29
|
const tabId = React.useId();
|
|
30
30
|
const errId = React.useId();
|
|
31
31
|
const start = initial.href.trim();
|
|
32
|
-
const changed = href.trim() !== start || allowNewTab && newTab !== !!initial.newTab;
|
|
32
|
+
const changed = dirty || href.trim() !== start || allowNewTab && newTab !== !!initial.newTab;
|
|
33
33
|
const ok = !linkProblem(href);
|
|
34
34
|
return /* @__PURE__ */ jsxs("form", {
|
|
35
35
|
className: "flex flex-col gap-3",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"link-form.js","names":[],"sources":["../../../src/controls/rich-text/link-form.tsx"],"sourcesContent":["/**\n * RICH TEXT · the link popover's body for a link inside the text, in V2's link panel chrome (controls/link.tsx\n * LinkPopoverBody: what it links to now · the address · Remove link · Cancel · Apply): the address, and \"Open in new tab\"\n * when the field allows it. A text link stores its address and target only (production's link mark), so V2's link type\n * select and rel choice, which belong to the link FIELD, are not offered here.\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 } from \"../link\";\n\nexport interface TextLink {\n href: string;\n newTab?: boolean;\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}: {\n initial: TextLink;\n allowNewTab: boolean;\n onSave: (v: TextLink) => void;\n onCancel: () => void;\n onDetach?: () => void;\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 = 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":";;;;;;;;;;;;;;;AAmBA,SAAgB,YAAY,MAA6B;CACvD,MAAM,IAAI,KAAK,KAAK;CACpB,IAAI,CAAC,GAAG,OAAO;CACf,IAAI,kFAAkF,KAAK,CAAC,GAAG,OAAO;CACtG,IAAI,uBAAuB,KAAK,CAAC,GAAG,OAAO;CAC3C,IAAI,oBAAoB,KAAK,CAAC,GAAG,OAAO,kCAAkC;CAC1E,OAAO;AACT;AAEA,SAAgB,SAAS,EACvB,SACA,aACA,QACA,UACA,
|
|
1
|
+
{"version":3,"file":"link-form.js","names":[],"sources":["../../../src/controls/rich-text/link-form.tsx"],"sourcesContent":["/**\n * RICH TEXT · the link popover's body for a link inside the text, in V2's link panel chrome (controls/link.tsx\n * LinkPopoverBody: what it links to now · the address · Remove link · Cancel · Apply): the address, and \"Open in new tab\"\n * when the field allows it. A text link stores its address and target only (production's link mark), so V2's link type\n * select and rel choice, which belong to the link FIELD, are not offered here.\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 } from \"../link\";\n\nexport interface TextLink {\n href: string;\n newTab?: boolean;\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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"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type KitRichTextRefRequest } from "../../context";
|
|
2
|
+
import type { TypeSchema } from "../../schema/types";
|
|
3
|
+
import type { IslandCtx } from "./blocks";
|
|
4
|
+
import type { IslandEl } from "./dom";
|
|
5
|
+
export type RelatedMode = "topic" | "newest" | "manual";
|
|
6
|
+
export interface RelatedModel {
|
|
7
|
+
title: string;
|
|
8
|
+
mode: RelatedMode;
|
|
9
|
+
count: number;
|
|
10
|
+
/** entry ids, Hand-picked only; the PT form wraps each as `{ _key, _ref }` */
|
|
11
|
+
posts: string[];
|
|
12
|
+
}
|
|
13
|
+
export declare const RELATED_LABEL = "Related posts";
|
|
14
|
+
export declare const relatedDefault: () => RelatedModel;
|
|
15
|
+
/** The panel form; `to` is the field's collection. "How many" explains the chosen automatic mode. */
|
|
16
|
+
export declare const relatedSchema: (to: string[], mode: RelatedMode) => TypeSchema;
|
|
17
|
+
/** What the preview asks the host for: the picked ids, or the automatic mode's query. */
|
|
18
|
+
export declare function relatedRequest(m: RelatedModel, collection?: string, excludeId?: string): KitRichTextRefRequest | null;
|
|
19
|
+
export declare function RelatedPreview({ m, onPick, ro, collection, excludeId }: {
|
|
20
|
+
m: RelatedModel;
|
|
21
|
+
onPick: () => void;
|
|
22
|
+
ro: boolean;
|
|
23
|
+
collection?: string;
|
|
24
|
+
excludeId?: string;
|
|
25
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
26
|
+
export declare function RelatedBlock({ m, set, el, ctx, autoFocus, collection, excludeId, }: {
|
|
27
|
+
m: RelatedModel;
|
|
28
|
+
set: (fn: (m: RelatedModel) => RelatedModel, label?: string) => void;
|
|
29
|
+
el: IslandEl;
|
|
30
|
+
ctx: IslandCtx;
|
|
31
|
+
autoFocus: boolean;
|
|
32
|
+
/** the posts collection (the field's config) */
|
|
33
|
+
collection: string;
|
|
34
|
+
/** the entry being edited, left out of the automatic modes */
|
|
35
|
+
excludeId?: string;
|
|
36
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { useRichTextRefs } from "../../context.js";
|
|
2
|
+
import { ArticleEmpty, ArticlePanelBlock } from "./article-shell.js";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
import { IconPlus } from "@tabler/icons-react";
|
|
6
|
+
//#region src/controls/rich-text/related.tsx
|
|
7
|
+
/**
|
|
8
|
+
* RICH TEXT · Related posts (kit-richtext-ui-spec.md §6): V2's article block, stored as the PT block object
|
|
9
|
+
* `{ _type: "relatedPosts", title?, mode, count, posts: { _ref }[] }`. Each post's title, cover, category and URL are
|
|
10
|
+
* derived from the referenced or queried entries, never stored.
|
|
11
|
+
*
|
|
12
|
+
* modes e1 2026-10-24 (signed deviation from V2's Automatic | Hand-picked): Same topic (topic) · Newest (newest) ·
|
|
13
|
+
* Hand-picked (manual). "How many" shows for the two automatic modes, Posts for Hand-picked. The schema's
|
|
14
|
+
* `hidden` is "show only when" (schema/types.ts), as V2's was.
|
|
15
|
+
* form V2 article.ts:167-195: Title (≤40, default "Related stories"), Stories (segmented), How many (1–6),
|
|
16
|
+
* Posts (1–6 unique references to the field's collection). Four settings → the floating panel (D54).
|
|
17
|
+
* preview V2 article-blocks.tsx:572-597: the title with the automatic mode's tag, three post cards (4:3 cover,
|
|
18
|
+
* two-line title, category); "Pick stories" when Hand-picked holds nothing that resolves.
|
|
19
|
+
* data KitServices.resolveRichTextRefs (useRichTextRefs): ids for Hand-picked, a query for the automatic modes.
|
|
20
|
+
* A fresh insert focuses the card (V2 article-blocks.tsx:131).
|
|
21
|
+
*/
|
|
22
|
+
var RELATED_LABEL = "Related posts";
|
|
23
|
+
var relatedDefault = () => ({
|
|
24
|
+
title: "Related stories",
|
|
25
|
+
mode: "topic",
|
|
26
|
+
count: 3,
|
|
27
|
+
posts: []
|
|
28
|
+
});
|
|
29
|
+
var MODE_TAG = {
|
|
30
|
+
topic: "Same topic",
|
|
31
|
+
newest: "Newest"
|
|
32
|
+
};
|
|
33
|
+
var asMode = (v) => v === "newest" || v === "manual" ? v : "topic";
|
|
34
|
+
var clampCount = (v) => Math.min(6, Math.max(1, Math.round(Number(v) || 3)));
|
|
35
|
+
/** The panel form; `to` is the field's collection. "How many" explains the chosen automatic mode. */
|
|
36
|
+
var relatedSchema = (to, mode) => ({
|
|
37
|
+
name: "relatedPosts",
|
|
38
|
+
title: RELATED_LABEL,
|
|
39
|
+
kind: "object",
|
|
40
|
+
icon: "news",
|
|
41
|
+
fields: [
|
|
42
|
+
{
|
|
43
|
+
name: "title",
|
|
44
|
+
title: "Title",
|
|
45
|
+
type: "string",
|
|
46
|
+
validation: { max: 40 }
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: "mode",
|
|
50
|
+
title: "Stories",
|
|
51
|
+
type: "select",
|
|
52
|
+
layout: "segmented",
|
|
53
|
+
initialValue: "topic",
|
|
54
|
+
options: [
|
|
55
|
+
{
|
|
56
|
+
value: "topic",
|
|
57
|
+
label: "Same topic"
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
value: "newest",
|
|
61
|
+
label: "Newest"
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
value: "manual",
|
|
65
|
+
label: "Hand-picked"
|
|
66
|
+
}
|
|
67
|
+
]
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
name: "count",
|
|
71
|
+
title: "How many",
|
|
72
|
+
type: "number",
|
|
73
|
+
hidden: {
|
|
74
|
+
field: "mode",
|
|
75
|
+
op: "in",
|
|
76
|
+
value: ["topic", "newest"]
|
|
77
|
+
},
|
|
78
|
+
numeric: {
|
|
79
|
+
min: 1,
|
|
80
|
+
max: 6,
|
|
81
|
+
step: 1
|
|
82
|
+
},
|
|
83
|
+
description: mode === "newest" ? "Newest first." : "Same topic, newest first."
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
name: "posts",
|
|
87
|
+
title: "Posts",
|
|
88
|
+
type: "array",
|
|
89
|
+
hidden: {
|
|
90
|
+
field: "mode",
|
|
91
|
+
equals: "manual"
|
|
92
|
+
},
|
|
93
|
+
validation: {
|
|
94
|
+
min: 1,
|
|
95
|
+
max: 6,
|
|
96
|
+
unique: true
|
|
97
|
+
},
|
|
98
|
+
of: [{
|
|
99
|
+
name: "post",
|
|
100
|
+
title: "Post",
|
|
101
|
+
type: "reference",
|
|
102
|
+
reference: { to }
|
|
103
|
+
}]
|
|
104
|
+
}
|
|
105
|
+
]
|
|
106
|
+
});
|
|
107
|
+
/** What the preview asks the host for: the picked ids, or the automatic mode's query. */
|
|
108
|
+
function relatedRequest(m, collection, excludeId) {
|
|
109
|
+
if (m.mode === "manual") return m.posts.length ? {
|
|
110
|
+
kind: "relatedPosts",
|
|
111
|
+
ids: m.posts
|
|
112
|
+
} : null;
|
|
113
|
+
return {
|
|
114
|
+
kind: "relatedPosts",
|
|
115
|
+
query: {
|
|
116
|
+
collection,
|
|
117
|
+
mode: m.mode,
|
|
118
|
+
count: m.count,
|
|
119
|
+
excludeId
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
function RelatedPreview({ m, onPick, ro, collection, excludeId }) {
|
|
124
|
+
const { items, loading, error } = useRichTextRefs(relatedRequest(m, collection, excludeId));
|
|
125
|
+
const posts = m.mode === "manual" ? m.posts.map((id) => items.find((x) => x.id === id)).filter((x) => !!x) : items;
|
|
126
|
+
if (m.mode === "manual" && !loading && !error && !posts.length) return /* @__PURE__ */ jsx(ArticleEmpty, {
|
|
127
|
+
icon: /* @__PURE__ */ jsx(IconPlus, { stroke: 1.5 }),
|
|
128
|
+
title: m.title || "Related posts",
|
|
129
|
+
action: "Pick stories",
|
|
130
|
+
onAction: onPick,
|
|
131
|
+
ro
|
|
132
|
+
});
|
|
133
|
+
return /* @__PURE__ */ jsxs("aside", {
|
|
134
|
+
className: "rounded-kit-lg border border-kit-line bg-kit-surface px-4 py-3",
|
|
135
|
+
"aria-label": m.title || "Related posts",
|
|
136
|
+
"aria-busy": loading || void 0,
|
|
137
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
138
|
+
className: "mb-2 flex items-baseline gap-2",
|
|
139
|
+
children: [m.title ? /* @__PURE__ */ jsx("p", {
|
|
140
|
+
"data-field": "title",
|
|
141
|
+
className: "text-kit-sm font-semibold text-kit-fg",
|
|
142
|
+
children: m.title
|
|
143
|
+
}) : null, m.mode !== "manual" ? /* @__PURE__ */ jsx("span", {
|
|
144
|
+
"data-field": "mode",
|
|
145
|
+
className: "text-kit-2xs text-kit-fg-3",
|
|
146
|
+
children: MODE_TAG[m.mode]
|
|
147
|
+
}) : null]
|
|
148
|
+
}), error ? /* @__PURE__ */ jsxs("p", {
|
|
149
|
+
role: "status",
|
|
150
|
+
className: "text-kit-sm text-kit-warning-fg",
|
|
151
|
+
children: ["The stories couldn't load: ", error]
|
|
152
|
+
}) : /* @__PURE__ */ jsx("ul", {
|
|
153
|
+
className: "m-0 grid grid-cols-3 gap-3 p-0",
|
|
154
|
+
"data-field": m.mode === "manual" ? "posts" : "count",
|
|
155
|
+
children: posts.map((e) => /* @__PURE__ */ jsxs("li", {
|
|
156
|
+
className: "min-w-0 list-none",
|
|
157
|
+
children: [
|
|
158
|
+
e.cover ? /* @__PURE__ */ jsx("img", {
|
|
159
|
+
src: e.cover,
|
|
160
|
+
alt: "",
|
|
161
|
+
draggable: false,
|
|
162
|
+
className: "block aspect-[4/3] w-full rounded-kit-md border border-kit-line object-cover"
|
|
163
|
+
}) : /* @__PURE__ */ jsx("div", { className: "aspect-[4/3] rounded-kit-md bg-kit-muted" }),
|
|
164
|
+
/* @__PURE__ */ jsx("p", {
|
|
165
|
+
className: "mt-1.5 line-clamp-2 text-kit-xs font-medium leading-snug text-kit-fg",
|
|
166
|
+
children: e.title
|
|
167
|
+
}),
|
|
168
|
+
e.category ? /* @__PURE__ */ jsx("p", {
|
|
169
|
+
className: "mt-0.5 truncate text-kit-2xs text-kit-fg-3",
|
|
170
|
+
children: e.category
|
|
171
|
+
}) : null
|
|
172
|
+
]
|
|
173
|
+
}, e.id))
|
|
174
|
+
})]
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
function RelatedBlock({ m, set, el, ctx, autoFocus, collection, excludeId }) {
|
|
178
|
+
const schema = React.useMemo(() => relatedSchema([collection], m.mode), [collection, m.mode]);
|
|
179
|
+
return /* @__PURE__ */ jsx(ArticlePanelBlock, {
|
|
180
|
+
label: RELATED_LABEL,
|
|
181
|
+
kind: "related",
|
|
182
|
+
schema,
|
|
183
|
+
m,
|
|
184
|
+
set,
|
|
185
|
+
el,
|
|
186
|
+
ctx,
|
|
187
|
+
autoFocus,
|
|
188
|
+
openOnInsert: false,
|
|
189
|
+
toForm: (x) => ({
|
|
190
|
+
title: x.title,
|
|
191
|
+
mode: x.mode,
|
|
192
|
+
count: x.count,
|
|
193
|
+
posts: x.posts
|
|
194
|
+
}),
|
|
195
|
+
fromForm: (v) => ({
|
|
196
|
+
title: String(v.title ?? ""),
|
|
197
|
+
mode: asMode(v.mode),
|
|
198
|
+
count: clampCount(v.count),
|
|
199
|
+
posts: Array.isArray(v.posts) ? v.posts.filter((x) => typeof x === "string") : []
|
|
200
|
+
}),
|
|
201
|
+
children: (open, ro) => /* @__PURE__ */ jsx(RelatedPreview, {
|
|
202
|
+
m,
|
|
203
|
+
onPick: () => open("posts"),
|
|
204
|
+
ro,
|
|
205
|
+
collection,
|
|
206
|
+
excludeId
|
|
207
|
+
})
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
//#endregion
|
|
211
|
+
export { RELATED_LABEL, RelatedBlock, RelatedPreview, relatedDefault, relatedRequest, relatedSchema };
|
|
212
|
+
|
|
213
|
+
//# sourceMappingURL=related.js.map
|
|
@@ -0,0 +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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"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { TypeSchema } from "../../schema/types";
|
|
2
|
+
import type { IslandCtx } from "./blocks";
|
|
3
|
+
import { type IslandEl } from "./dom";
|
|
4
|
+
export interface TocModel {
|
|
5
|
+
title: string;
|
|
6
|
+
depth: "2" | "3";
|
|
7
|
+
}
|
|
8
|
+
export declare const TOC_LABEL = "Table of contents";
|
|
9
|
+
export declare const tocDefault: () => TocModel;
|
|
10
|
+
/** The popover form (V2 ARTICLE_SCHEMAS.toc). Two settings → the shell's popover (D54). */
|
|
11
|
+
export declare const TOC_SCHEMA: TypeSchema;
|
|
12
|
+
export interface OutlineItem {
|
|
13
|
+
id: string;
|
|
14
|
+
text: string;
|
|
15
|
+
depth: 2 | 3;
|
|
16
|
+
}
|
|
17
|
+
/** V2 article.ts:250-251. ponytail: richtext's one slugify replaces this when it lands (plan RT5); it must stay equal. */
|
|
18
|
+
export declare const slugify: (s: string) => string;
|
|
19
|
+
/** The body's H2 and H3, top level only, in order, with stable de-duplicated ids. */
|
|
20
|
+
export declare function outlineOf(root: ParentNode): OutlineItem[];
|
|
21
|
+
export declare const tocVisible: (outline: OutlineItem[], depth: TocModel["depth"]) => OutlineItem[];
|
|
22
|
+
/** One live outline per editor: the editor sets it after each input, every TOC preview subscribes. */
|
|
23
|
+
export interface OutlineStore {
|
|
24
|
+
get: () => OutlineItem[];
|
|
25
|
+
subscribe: (fn: () => void) => () => void;
|
|
26
|
+
set: (next: OutlineItem[]) => void;
|
|
27
|
+
}
|
|
28
|
+
export declare function createOutlineStore(): OutlineStore;
|
|
29
|
+
export declare const NO_OUTLINE: OutlineStore;
|
|
30
|
+
/** `host` is the TOC's island element; its parent is the editor surface whose headings it lists. */
|
|
31
|
+
export declare function TocPreview({ m, outline, host }: {
|
|
32
|
+
m: TocModel;
|
|
33
|
+
outline: OutlineStore;
|
|
34
|
+
host: HTMLElement;
|
|
35
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
36
|
+
export interface TocBlockProps {
|
|
37
|
+
m: TocModel;
|
|
38
|
+
set: (fn: (m: TocModel) => TocModel, label?: string) => void;
|
|
39
|
+
el: IslandEl;
|
|
40
|
+
/** the editor's live outline rides the island context */
|
|
41
|
+
ctx: IslandCtx & {
|
|
42
|
+
outline?: OutlineStore;
|
|
43
|
+
};
|
|
44
|
+
autoFocus: boolean;
|
|
45
|
+
}
|
|
46
|
+
export declare function TocBlock({ m, set, el, ctx, autoFocus }: TocBlockProps): import("react/jsx-runtime").JSX.Element;
|