@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
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { TypeSchema } from "../../schema/types";
|
|
2
|
+
import type { IslandCtx } from "./blocks";
|
|
3
|
+
import { type IslandEl } from "./dom";
|
|
4
|
+
export type EmbedAspect = "16:9" | "4:3" | "1:1" | "9:16";
|
|
5
|
+
export declare const EMBED_ASPECTS: readonly EmbedAspect[];
|
|
6
|
+
export interface EmbedBlockModel {
|
|
7
|
+
url: string;
|
|
8
|
+
title: string;
|
|
9
|
+
aspect: EmbedAspect;
|
|
10
|
+
}
|
|
11
|
+
export declare const EMBED_LABEL = "Embed";
|
|
12
|
+
export declare const embedDefault: () => EmbedBlockModel;
|
|
13
|
+
export declare const EMBED_SCHEMA: TypeSchema;
|
|
14
|
+
/** A faithful placeholder: provider, title and shape, no third-party frame in the editor. */
|
|
15
|
+
export declare function EmbedPreview({ m, onAdd, ro }: {
|
|
16
|
+
m: EmbedBlockModel;
|
|
17
|
+
onAdd: () => void;
|
|
18
|
+
ro: boolean;
|
|
19
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
20
|
+
export declare function EmbedBlock({ m, set, el, ctx, autoFocus, shape, }: {
|
|
21
|
+
/** the Shape setting is drawn only where the host stores it (e1 2026-10-24: no control that does not persist) */
|
|
22
|
+
shape?: boolean;
|
|
23
|
+
m: EmbedBlockModel;
|
|
24
|
+
set: (fn: (m: EmbedBlockModel) => EmbedBlockModel, label?: string) => void;
|
|
25
|
+
el: IslandEl;
|
|
26
|
+
ctx: IslandCtx;
|
|
27
|
+
autoFocus: boolean;
|
|
28
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { cn } from "../../lib/cn.js";
|
|
2
|
+
import { ArticleEmpty, ArticlePopoverBlock } from "./article-shell.js";
|
|
3
|
+
import { detectEmbed } from "./dom.js";
|
|
4
|
+
import "react";
|
|
5
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
+
import { IconBrandCodepen, IconBrandFigma, IconBrandSpotify, IconBrandVimeo, IconBrandX, IconBrandYoutube, IconHeadphones, IconLink, IconMap, IconMicrophone, IconPlayerPlayFilled, IconWorld } from "@tabler/icons-react";
|
|
7
|
+
//#region src/controls/rich-text/embed.tsx
|
|
8
|
+
var EMBED_ASPECTS = [
|
|
9
|
+
"16:9",
|
|
10
|
+
"4:3",
|
|
11
|
+
"1:1",
|
|
12
|
+
"9:16"
|
|
13
|
+
];
|
|
14
|
+
var EMBED_LABEL = "Embed";
|
|
15
|
+
var embedDefault = () => ({
|
|
16
|
+
url: "",
|
|
17
|
+
title: "",
|
|
18
|
+
aspect: "16:9"
|
|
19
|
+
});
|
|
20
|
+
var asAspect = (v) => EMBED_ASPECTS.includes(v) ? v : "16:9";
|
|
21
|
+
var EMBED_SCHEMA = {
|
|
22
|
+
name: "embed",
|
|
23
|
+
title: EMBED_LABEL,
|
|
24
|
+
kind: "object",
|
|
25
|
+
icon: "code",
|
|
26
|
+
fields: [
|
|
27
|
+
{
|
|
28
|
+
name: "url",
|
|
29
|
+
title: "Link",
|
|
30
|
+
type: "url",
|
|
31
|
+
placeholder: "Paste a YouTube, X, CodePen, Figma or map link",
|
|
32
|
+
validation: { required: true }
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: "title",
|
|
36
|
+
title: "Title",
|
|
37
|
+
type: "string",
|
|
38
|
+
validation: {
|
|
39
|
+
required: true,
|
|
40
|
+
max: 120
|
|
41
|
+
},
|
|
42
|
+
description: "Not shown. Read by screen readers."
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: "aspect",
|
|
46
|
+
title: "Shape",
|
|
47
|
+
type: "select",
|
|
48
|
+
initialValue: "16:9",
|
|
49
|
+
options: [
|
|
50
|
+
{
|
|
51
|
+
value: "16:9",
|
|
52
|
+
label: "16:9",
|
|
53
|
+
description: "Landscape"
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
value: "4:3",
|
|
57
|
+
label: "4:3",
|
|
58
|
+
description: "Classic"
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
value: "1:1",
|
|
62
|
+
label: "1:1",
|
|
63
|
+
description: "Square"
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
value: "9:16",
|
|
67
|
+
label: "9:16",
|
|
68
|
+
description: "Portrait"
|
|
69
|
+
}
|
|
70
|
+
]
|
|
71
|
+
}
|
|
72
|
+
]
|
|
73
|
+
};
|
|
74
|
+
/** EMBED_SCHEMA without Shape, for a host whose stored embed keeps no aspect (a stored one still draws). */
|
|
75
|
+
var NO_SHAPE_SCHEMA = {
|
|
76
|
+
...EMBED_SCHEMA,
|
|
77
|
+
fields: EMBED_SCHEMA.fields.filter((f) => f.name !== "aspect")
|
|
78
|
+
};
|
|
79
|
+
var PROVIDER_ICON = {
|
|
80
|
+
youtube: /* @__PURE__ */ jsx(IconBrandYoutube, { stroke: 1.5 }),
|
|
81
|
+
vimeo: /* @__PURE__ */ jsx(IconBrandVimeo, { stroke: 1.5 }),
|
|
82
|
+
x: /* @__PURE__ */ jsx(IconBrandX, { stroke: 1.5 }),
|
|
83
|
+
codepen: /* @__PURE__ */ jsx(IconBrandCodepen, { stroke: 1.5 }),
|
|
84
|
+
figma: /* @__PURE__ */ jsx(IconBrandFigma, { stroke: 1.5 }),
|
|
85
|
+
maps: /* @__PURE__ */ jsx(IconMap, { stroke: 1.5 }),
|
|
86
|
+
spotify: /* @__PURE__ */ jsx(IconBrandSpotify, { stroke: 1.5 }),
|
|
87
|
+
podcast: /* @__PURE__ */ jsx(IconMicrophone, { stroke: 1.5 }),
|
|
88
|
+
audio: /* @__PURE__ */ jsx(IconHeadphones, { stroke: 1.5 }),
|
|
89
|
+
link: /* @__PURE__ */ jsx(IconWorld, { stroke: 1.5 })
|
|
90
|
+
};
|
|
91
|
+
var ASPECT_RATIO = {
|
|
92
|
+
"16:9": "16 / 9",
|
|
93
|
+
"4:3": "4 / 3",
|
|
94
|
+
"1:1": "1 / 1",
|
|
95
|
+
"9:16": "9 / 16"
|
|
96
|
+
};
|
|
97
|
+
var bare = (url) => url.replace(/^https?:\/\/(www\.)?/, "");
|
|
98
|
+
/** A faithful placeholder: provider, title and shape, no third-party frame in the editor. */
|
|
99
|
+
function EmbedPreview({ m, onAdd, ro }) {
|
|
100
|
+
if (!m.url) return /* @__PURE__ */ jsx(ArticleEmpty, {
|
|
101
|
+
icon: /* @__PURE__ */ jsx(IconLink, { stroke: 1.5 }),
|
|
102
|
+
title: EMBED_LABEL,
|
|
103
|
+
action: "Paste a link",
|
|
104
|
+
onAction: onAdd,
|
|
105
|
+
ro
|
|
106
|
+
});
|
|
107
|
+
const info = detectEmbed(m.url);
|
|
108
|
+
const icon = PROVIDER_ICON[info.provider];
|
|
109
|
+
const title = m.title || /* @__PURE__ */ jsx("span", {
|
|
110
|
+
className: "text-kit-warning-fg",
|
|
111
|
+
children: "Add a title"
|
|
112
|
+
});
|
|
113
|
+
if (info.shape === "frame") return /* @__PURE__ */ jsx("figure", {
|
|
114
|
+
className: "m-0",
|
|
115
|
+
"data-field": "url",
|
|
116
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
117
|
+
className: cn("relative mx-auto flex w-full flex-col items-center justify-center gap-2 overflow-hidden rounded-kit-lg border border-kit-line bg-kit-subtle text-center", m.aspect === "9:16" && "max-w-[300px]"),
|
|
118
|
+
style: { aspectRatio: ASPECT_RATIO[m.aspect] },
|
|
119
|
+
children: [
|
|
120
|
+
/* @__PURE__ */ jsx("span", {
|
|
121
|
+
className: "flex size-11 items-center justify-center rounded-full bg-kit-inverse text-kit-fg-inverse [&_svg]:size-5",
|
|
122
|
+
children: info.provider === "youtube" || info.provider === "vimeo" ? /* @__PURE__ */ jsx(IconPlayerPlayFilled, {}) : icon
|
|
123
|
+
}),
|
|
124
|
+
/* @__PURE__ */ jsx("span", {
|
|
125
|
+
"data-field": "title",
|
|
126
|
+
className: "max-w-[80%] truncate text-kit-sm font-medium text-kit-fg",
|
|
127
|
+
children: title
|
|
128
|
+
}),
|
|
129
|
+
/* @__PURE__ */ jsxs("span", {
|
|
130
|
+
className: "absolute left-3 top-2.5 flex items-center gap-1.5 text-kit-xs text-kit-fg-2 [&_svg]:size-3.5",
|
|
131
|
+
children: [icon, info.label]
|
|
132
|
+
})
|
|
133
|
+
]
|
|
134
|
+
})
|
|
135
|
+
});
|
|
136
|
+
if (info.shape === "audio") return /* @__PURE__ */ jsxs("figure", {
|
|
137
|
+
className: "m-0 flex items-center gap-3 rounded-kit-lg border border-kit-line bg-kit-subtle px-3 py-2.5",
|
|
138
|
+
"data-field": "url",
|
|
139
|
+
children: [
|
|
140
|
+
/* @__PURE__ */ jsx("span", {
|
|
141
|
+
className: "flex size-9 shrink-0 items-center justify-center rounded-full bg-kit-inverse text-kit-fg-inverse [&_svg]:size-4",
|
|
142
|
+
children: /* @__PURE__ */ jsx(IconPlayerPlayFilled, {})
|
|
143
|
+
}),
|
|
144
|
+
/* @__PURE__ */ jsxs("span", {
|
|
145
|
+
className: "min-w-0 flex-1",
|
|
146
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
147
|
+
"data-field": "title",
|
|
148
|
+
className: "block truncate text-kit-sm font-medium text-kit-fg",
|
|
149
|
+
children: title
|
|
150
|
+
}), /* @__PURE__ */ jsxs("span", {
|
|
151
|
+
className: "flex items-center gap-1 text-kit-xs text-kit-fg-3 [&_svg]:size-3",
|
|
152
|
+
children: [icon, info.label]
|
|
153
|
+
})]
|
|
154
|
+
}),
|
|
155
|
+
/* @__PURE__ */ jsx("span", {
|
|
156
|
+
"aria-hidden": true,
|
|
157
|
+
className: "hidden h-1 w-28 rounded-full bg-kit-line-strong sm:block"
|
|
158
|
+
})
|
|
159
|
+
]
|
|
160
|
+
});
|
|
161
|
+
return /* @__PURE__ */ jsxs("figure", {
|
|
162
|
+
className: "m-0 flex items-center gap-3 rounded-kit-lg border border-kit-line bg-kit-surface px-3 py-2.5",
|
|
163
|
+
"data-field": "url",
|
|
164
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
165
|
+
className: "flex size-9 shrink-0 items-center justify-center rounded-kit-md bg-kit-muted text-kit-fg-2 [&_svg]:size-4",
|
|
166
|
+
children: icon
|
|
167
|
+
}), /* @__PURE__ */ jsxs("span", {
|
|
168
|
+
className: "min-w-0 flex-1",
|
|
169
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
170
|
+
"data-field": "title",
|
|
171
|
+
className: "block truncate text-kit-sm font-medium text-kit-fg",
|
|
172
|
+
children: title
|
|
173
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
174
|
+
className: "block truncate text-kit-xs text-kit-fg-3",
|
|
175
|
+
children: info.shape === "post" ? `${info.label} · ${bare(m.url)}` : bare(m.url)
|
|
176
|
+
})]
|
|
177
|
+
})]
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
function EmbedBlock({ m, set, el, ctx, autoFocus, shape = true }) {
|
|
181
|
+
return /* @__PURE__ */ jsx(ArticlePopoverBlock, {
|
|
182
|
+
label: EMBED_LABEL,
|
|
183
|
+
kind: "embed",
|
|
184
|
+
schema: shape ? EMBED_SCHEMA : NO_SHAPE_SCHEMA,
|
|
185
|
+
m,
|
|
186
|
+
set,
|
|
187
|
+
el,
|
|
188
|
+
ctx,
|
|
189
|
+
autoFocus,
|
|
190
|
+
toForm: (x) => ({
|
|
191
|
+
url: x.url,
|
|
192
|
+
title: x.title,
|
|
193
|
+
aspect: x.aspect
|
|
194
|
+
}),
|
|
195
|
+
fromForm: (v, prev) => ({
|
|
196
|
+
url: String(v.url ?? ""),
|
|
197
|
+
title: String(v.title ?? ""),
|
|
198
|
+
aspect: shape ? asAspect(v.aspect) : prev.aspect
|
|
199
|
+
}),
|
|
200
|
+
children: (open, ro) => /* @__PURE__ */ jsx(EmbedPreview, {
|
|
201
|
+
m,
|
|
202
|
+
onAdd: () => open("url"),
|
|
203
|
+
ro
|
|
204
|
+
})
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
//#endregion
|
|
208
|
+
export { EMBED_ASPECTS, EMBED_LABEL, EMBED_SCHEMA, EmbedBlock, EmbedPreview, embedDefault };
|
|
209
|
+
|
|
210
|
+
//# sourceMappingURL=embed.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"embed.js","names":[],"sources":["../../../src/controls/rich-text/embed.tsx"],"sourcesContent":["/**\n * RICH TEXT · Embed (kit-richtext-ui-spec.md §3): V2's article block, stored as the PT block object\n * `{ _type: \"embed\", url, title, aspect }`. The provider and its shape are detected from the link (dom.ts detectEmbed),\n * never stored. Supersedes blocks.tsx EmbedBlock/EmbedPreview, which had no stored Shape (kit-40 removes those).\n *\n * form V2 article.ts:115-136: Link (required), Title (required, max 120, screen-reader only), Shape (a select:\n * 16:9 Landscape, 4:3 Classic, 1:1 Square, 9:16 Portrait). Three settings → the shell's popover (D54).\n * preview V2 article-blocks.tsx:482-535: a faithful placeholder, nothing third-party in the editor. A frame is\n * drawn at its Shape (9:16 capped at 300px wide), audio as a slim player, an X post or any other link as a\n * card. On the site an X post is a static card by default (owner, 2026-10-24); the editor shows V2's card.\n */\nimport * as React from \"react\";\nimport {\n IconBrandCodepen, IconBrandFigma, IconBrandSpotify, IconBrandVimeo, IconBrandX, IconBrandYoutube, IconHeadphones, IconLink, IconMap, IconMicrophone,\n IconPlayerPlayFilled, IconWorld,\n} from \"@tabler/icons-react\";\nimport { cn } from \"../../lib/cn\";\nimport type { TypeSchema } from \"../../schema/types\";\nimport { ArticleEmpty, ArticlePopoverBlock } from \"./article-shell\";\nimport type { IslandCtx } from \"./blocks\";\nimport { detectEmbed, type IslandEl } from \"./dom\";\n\nexport type EmbedAspect = \"16:9\" | \"4:3\" | \"1:1\" | \"9:16\";\nexport const EMBED_ASPECTS: readonly EmbedAspect[] = [\"16:9\", \"4:3\", \"1:1\", \"9:16\"];\nexport interface EmbedBlockModel {\n url: string;\n title: string;\n aspect: EmbedAspect;\n}\nexport const EMBED_LABEL = \"Embed\";\nexport const embedDefault = (): EmbedBlockModel => ({ url: \"\", title: \"\", aspect: \"16:9\" });\nconst asAspect = (v: unknown): EmbedAspect => (EMBED_ASPECTS.includes(v as EmbedAspect) ? (v as EmbedAspect) : \"16:9\");\n\nexport const EMBED_SCHEMA: TypeSchema = {\n name: \"embed\",\n title: EMBED_LABEL,\n kind: \"object\",\n icon: \"code\",\n fields: [\n { name: \"url\", title: \"Link\", type: \"url\", placeholder: \"Paste a YouTube, X, CodePen, Figma or map link\", validation: { required: true } },\n { name: \"title\", title: \"Title\", type: \"string\", validation: { required: true, max: 120 }, description: \"Not shown. Read by screen readers.\" },\n {\n name: \"aspect\",\n title: \"Shape\",\n type: \"select\",\n initialValue: \"16:9\",\n options: [\n { value: \"16:9\", label: \"16:9\", description: \"Landscape\" },\n { value: \"4:3\", label: \"4:3\", description: \"Classic\" },\n { value: \"1:1\", label: \"1:1\", description: \"Square\" },\n { value: \"9:16\", label: \"9:16\", description: \"Portrait\" },\n ],\n },\n ],\n};\n\n/** EMBED_SCHEMA without Shape, for a host whose stored embed keeps no aspect (a stored one still draws). */\nconst NO_SHAPE_SCHEMA: TypeSchema = { ...EMBED_SCHEMA, fields: EMBED_SCHEMA.fields.filter((f) => f.name !== \"aspect\") };\n\nconst PROVIDER_ICON: Record<string, React.ReactNode> = {\n youtube: <IconBrandYoutube stroke={1.5} />, vimeo: <IconBrandVimeo stroke={1.5} />, x: <IconBrandX stroke={1.5} />, codepen: <IconBrandCodepen stroke={1.5} />,\n figma: <IconBrandFigma stroke={1.5} />, maps: <IconMap stroke={1.5} />, spotify: <IconBrandSpotify stroke={1.5} />, podcast: <IconMicrophone stroke={1.5} />,\n audio: <IconHeadphones stroke={1.5} />, link: <IconWorld stroke={1.5} />,\n};\nconst ASPECT_RATIO: Record<EmbedAspect, string> = { \"16:9\": \"16 / 9\", \"4:3\": \"4 / 3\", \"1:1\": \"1 / 1\", \"9:16\": \"9 / 16\" };\nconst bare = (url: string) => url.replace(/^https?:\\/\\/(www\\.)?/, \"\");\n\n/** A faithful placeholder: provider, title and shape, no third-party frame in the editor. */\nexport function EmbedPreview({ m, onAdd, ro }: { m: EmbedBlockModel; onAdd: () => void; ro: boolean }) {\n if (!m.url) return <ArticleEmpty icon={<IconLink stroke={1.5} />} title={EMBED_LABEL} action=\"Paste a link\" onAction={onAdd} ro={ro} />;\n const info = detectEmbed(m.url);\n const icon = PROVIDER_ICON[info.provider];\n const title = m.title || <span className=\"text-kit-warning-fg\">Add a title</span>;\n if (info.shape === \"frame\")\n return (\n <figure className=\"m-0\" data-field=\"url\">\n <div\n className={cn(\n \"relative mx-auto flex w-full flex-col items-center justify-center gap-2 overflow-hidden rounded-kit-lg border border-kit-line bg-kit-subtle text-center\",\n m.aspect === \"9:16\" && \"max-w-[300px]\",\n )}\n style={{ aspectRatio: ASPECT_RATIO[m.aspect] }}\n >\n <span className=\"flex size-11 items-center justify-center rounded-full bg-kit-inverse text-kit-fg-inverse [&_svg]:size-5\">\n {info.provider === \"youtube\" || info.provider === \"vimeo\" ? <IconPlayerPlayFilled /> : icon}\n </span>\n <span data-field=\"title\" className=\"max-w-[80%] truncate text-kit-sm font-medium text-kit-fg\">{title}</span>\n <span className=\"absolute left-3 top-2.5 flex items-center gap-1.5 text-kit-xs text-kit-fg-2 [&_svg]:size-3.5\">\n {icon}\n {info.label}\n </span>\n </div>\n </figure>\n );\n if (info.shape === \"audio\")\n return (\n <figure className=\"m-0 flex items-center gap-3 rounded-kit-lg border border-kit-line bg-kit-subtle px-3 py-2.5\" data-field=\"url\">\n <span className=\"flex size-9 shrink-0 items-center justify-center rounded-full bg-kit-inverse text-kit-fg-inverse [&_svg]:size-4\">\n <IconPlayerPlayFilled />\n </span>\n <span className=\"min-w-0 flex-1\">\n <span data-field=\"title\" className=\"block truncate text-kit-sm font-medium text-kit-fg\">{title}</span>\n <span className=\"flex items-center gap-1 text-kit-xs text-kit-fg-3 [&_svg]:size-3\">\n {icon}\n {info.label}\n </span>\n </span>\n <span aria-hidden className=\"hidden h-1 w-28 rounded-full bg-kit-line-strong sm:block\" />\n </figure>\n );\n return (\n <figure className=\"m-0 flex items-center gap-3 rounded-kit-lg border border-kit-line bg-kit-surface px-3 py-2.5\" data-field=\"url\">\n <span className=\"flex size-9 shrink-0 items-center justify-center rounded-kit-md bg-kit-muted text-kit-fg-2 [&_svg]:size-4\">{icon}</span>\n <span className=\"min-w-0 flex-1\">\n <span data-field=\"title\" className=\"block truncate text-kit-sm font-medium text-kit-fg\">{title}</span>\n <span className=\"block truncate text-kit-xs text-kit-fg-3\">{info.shape === \"post\" ? `${info.label} · ${bare(m.url)}` : bare(m.url)}</span>\n </span>\n </figure>\n );\n}\n\nexport function EmbedBlock({\n m, set, el, ctx, autoFocus, shape = true,\n}: {\n /** the Shape setting is drawn only where the host stores it (e1 2026-10-24: no control that does not persist) */\n shape?: boolean;\n m: EmbedBlockModel;\n set: (fn: (m: EmbedBlockModel) => EmbedBlockModel, label?: string) => void;\n el: IslandEl;\n ctx: IslandCtx;\n autoFocus: boolean;\n}) {\n return (\n <ArticlePopoverBlock<EmbedBlockModel>\n label={EMBED_LABEL}\n kind=\"embed\"\n schema={shape ? EMBED_SCHEMA : NO_SHAPE_SCHEMA}\n m={m}\n set={set}\n el={el}\n ctx={ctx}\n autoFocus={autoFocus}\n toForm={(x) => ({ url: x.url, title: x.title, aspect: x.aspect })}\n fromForm={(v, prev) => ({ url: String(v.url ?? \"\"), title: String(v.title ?? \"\"), aspect: shape ? asAspect(v.aspect) : prev.aspect })}\n >\n {(open, ro) => <EmbedPreview m={m} onAdd={() => open(\"url\")} ro={ro} />}\n </ArticlePopoverBlock>\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export declare const FN_MARK = "sup[data-rt=fn]";
|
|
2
|
+
/** The toast when a footnote cannot go where the caret is (V2 rich-text-editor.tsx:613). */
|
|
3
|
+
export declare const FOOTNOTE_REFUSED: {
|
|
4
|
+
readonly title: "Put the caret in the text first";
|
|
5
|
+
readonly description: "Footnotes number the story's own text, not text inside a block.";
|
|
6
|
+
};
|
|
7
|
+
/** The popover's chrome (V2 rich-text-editor.tsx:1650). */
|
|
8
|
+
export declare const FOOTNOTE_POPOVER_CLASS = "w-[320px] rounded-kit-lg p-3";
|
|
9
|
+
/** The body's own markers in text order (markers inside a rich block are not the body's). */
|
|
10
|
+
export declare const footnoteMarkers: (root: HTMLElement) => HTMLElement[];
|
|
11
|
+
/** Numbers the markers 1…n in text order; returns them with their notes, for the list. */
|
|
12
|
+
export declare function renumberFootnotes(root: HTMLElement): {
|
|
13
|
+
el: HTMLElement;
|
|
14
|
+
note: string;
|
|
15
|
+
}[];
|
|
16
|
+
/** A new, empty marker at the caret. Null when the caret is outside the body's text (a rich block, code, elsewhere). */
|
|
17
|
+
export declare function insertFootnote(root: HTMLElement): HTMLElement | null;
|
|
18
|
+
/** The caret right after a marker, in the body. */
|
|
19
|
+
export declare function caretAfterMarker(root: HTMLElement, m: HTMLElement): void;
|
|
20
|
+
/** "Back to the text": scroll the marker to the centre, caret after it, and flash it (1200ms). */
|
|
21
|
+
export declare function backToMarker(root: HTMLElement, m: HTMLElement): void;
|
|
22
|
+
/** The note of one footnote: one setting, edited in a popover at its marker (V2 D54 / D55). */
|
|
23
|
+
export declare function FootnoteEditor({ n, note, onChange, onRemove, onDone }: {
|
|
24
|
+
n: number;
|
|
25
|
+
note: string;
|
|
26
|
+
onChange: (v: string) => void;
|
|
27
|
+
onRemove: () => void;
|
|
28
|
+
onDone: () => void;
|
|
29
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
30
|
+
/** The notes under the body, numbered in text order, each with a way back to its marker. Renders nothing when empty. */
|
|
31
|
+
export declare function FootnotesList({ notes, readOnly, density, onEdit, onBack, }: {
|
|
32
|
+
notes: {
|
|
33
|
+
el: HTMLElement;
|
|
34
|
+
note: string;
|
|
35
|
+
}[];
|
|
36
|
+
readOnly?: boolean;
|
|
37
|
+
/** the editor's spacing: bare (no chrome), dense (panel), default */
|
|
38
|
+
density?: "bare" | "dense" | "default";
|
|
39
|
+
onEdit: (el: HTMLElement) => void;
|
|
40
|
+
onBack: (el: HTMLElement) => void;
|
|
41
|
+
}): import("react/jsx-runtime").JSX.Element | null;
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { cn } from "../../lib/cn.js";
|
|
2
|
+
import { Button } from "../../ui/button.js";
|
|
3
|
+
import { Tooltip } from "../../ui/overlay.js";
|
|
4
|
+
import { Textarea } from "../../ui/input.js";
|
|
5
|
+
import * as React from "react";
|
|
6
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
7
|
+
import { IconTrash } from "@tabler/icons-react";
|
|
8
|
+
//#region src/controls/rich-text/footnote.tsx
|
|
9
|
+
/**
|
|
10
|
+
* RICH TEXT · Footnotes (kit-richtext-ui-spec.md §1): V2's inline footnote, stored as the PT inline object
|
|
11
|
+
* `{ _type: "footnote", note }`, written in the editor as `<sup data-rt="fn" data-note contenteditable="false">N</sup>`.
|
|
12
|
+
* The number, the notes list and the way back are derived from the markers' text order, never stored.
|
|
13
|
+
*
|
|
14
|
+
* Editor chrome, not an island: the editor owns the clicks and the popover's anchor; this module is the V2 parts.
|
|
15
|
+
* insert V2 ops.ts:182-205: at the caret through the browser's insert (⌘Z takes it back), never inside a rich
|
|
16
|
+
* block or code; null then, and the editor toasts FOOTNOTE_REFUSED (V2 rich-text-editor.tsx:610-615)
|
|
17
|
+
* numbers V2 dom.ts:429-436: the body's own markers in text order, 1…n
|
|
18
|
+
* popover V2 FootnoteEditor (rich-text-editor.tsx:1736-1777) at the marker, w-[320px] rounded p-3
|
|
19
|
+
* list V2 notes section (rich-text-editor.tsx:1533-1560): each note a button that edits it, and ↩ back to it
|
|
20
|
+
* back V2 backToMarker (rich-text-editor.tsx:1095-1100): scroll to it, caret after it, a 1200ms accent flash
|
|
21
|
+
*/
|
|
22
|
+
var FN_MARK = "sup[data-rt=fn]";
|
|
23
|
+
/** The toast when a footnote cannot go where the caret is (V2 rich-text-editor.tsx:613). */
|
|
24
|
+
var FOOTNOTE_REFUSED = {
|
|
25
|
+
title: "Put the caret in the text first",
|
|
26
|
+
description: "Footnotes number the story's own text, not text inside a block."
|
|
27
|
+
};
|
|
28
|
+
/** The popover's chrome (V2 rich-text-editor.tsx:1650). */
|
|
29
|
+
var FOOTNOTE_POPOVER_CLASS = "w-[320px] rounded-kit-lg p-3";
|
|
30
|
+
/** The body's own markers in text order (markers inside a rich block are not the body's). */
|
|
31
|
+
var footnoteMarkers = (root) => Array.from(root.querySelectorAll(FN_MARK)).filter((m) => !m.closest("[data-rt-island],[data-rt-isolate]"));
|
|
32
|
+
/** Numbers the markers 1…n in text order; returns them with their notes, for the list. */
|
|
33
|
+
function renumberFootnotes(root) {
|
|
34
|
+
return footnoteMarkers(root).map((m, i) => {
|
|
35
|
+
if (m.textContent !== String(i + 1)) m.textContent = String(i + 1);
|
|
36
|
+
return {
|
|
37
|
+
el: m,
|
|
38
|
+
note: m.getAttribute("data-note") ?? ""
|
|
39
|
+
};
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
/** A new, empty marker at the caret. Null when the caret is outside the body's text (a rich block, code, elsewhere). */
|
|
43
|
+
function insertFootnote(root) {
|
|
44
|
+
const s = window.getSelection();
|
|
45
|
+
if (!s || !s.rangeCount || !root.contains(s.anchorNode)) return null;
|
|
46
|
+
const at = s.anchorNode?.nodeType === 1 ? s.anchorNode : s.anchorNode?.parentElement;
|
|
47
|
+
if (!at || at.closest("[data-rt-island],[data-rt-isolate]") || at.closest("pre")) return null;
|
|
48
|
+
root.focus({ preventScroll: true });
|
|
49
|
+
if (typeof document.execCommand === "function") document.execCommand("insertHTML", false, `<sup data-rt="fn" data-note="" contenteditable="false" data-fn-new="">1</sup>`);
|
|
50
|
+
let sup = root.querySelector("sup[data-fn-new]");
|
|
51
|
+
if (!sup) {
|
|
52
|
+
sup = document.createElement("sup");
|
|
53
|
+
sup.setAttribute("data-rt", "fn");
|
|
54
|
+
sup.setAttribute("data-note", "");
|
|
55
|
+
sup.setAttribute("contenteditable", "false");
|
|
56
|
+
sup.textContent = "1";
|
|
57
|
+
const r = s.getRangeAt(0);
|
|
58
|
+
r.collapse(false);
|
|
59
|
+
r.insertNode(sup);
|
|
60
|
+
const after = document.createTextNode("");
|
|
61
|
+
sup.after(after);
|
|
62
|
+
const c = document.createRange();
|
|
63
|
+
c.setStart(after, after.length);
|
|
64
|
+
c.collapse(true);
|
|
65
|
+
s.removeAllRanges();
|
|
66
|
+
s.addRange(c);
|
|
67
|
+
}
|
|
68
|
+
sup.removeAttribute("data-fn-new");
|
|
69
|
+
renumberFootnotes(root);
|
|
70
|
+
return sup;
|
|
71
|
+
}
|
|
72
|
+
/** The caret right after a marker, in the body. */
|
|
73
|
+
function caretAfterMarker(root, m) {
|
|
74
|
+
const s = window.getSelection();
|
|
75
|
+
if (!s || !m.isConnected) return;
|
|
76
|
+
root.focus({ preventScroll: true });
|
|
77
|
+
const r = document.createRange();
|
|
78
|
+
r.setStartAfter(m);
|
|
79
|
+
r.collapse(true);
|
|
80
|
+
s.removeAllRanges();
|
|
81
|
+
s.addRange(r);
|
|
82
|
+
}
|
|
83
|
+
/** "Back to the text": scroll the marker to the centre, caret after it, and flash it (1200ms). */
|
|
84
|
+
function backToMarker(root, m) {
|
|
85
|
+
if (!m.isConnected) return;
|
|
86
|
+
m.scrollIntoView?.({
|
|
87
|
+
block: "center",
|
|
88
|
+
behavior: "smooth"
|
|
89
|
+
});
|
|
90
|
+
caretAfterMarker(root, m);
|
|
91
|
+
m.animate?.([{ boxShadow: "0 0 0 3px var(--kit-accent-soft)" }, { boxShadow: "0 0 0 0 transparent" }], { duration: 1200 });
|
|
92
|
+
}
|
|
93
|
+
/** The note of one footnote: one setting, edited in a popover at its marker (V2 D54 / D55). */
|
|
94
|
+
function FootnoteEditor({ n, note, onChange, onRemove, onDone }) {
|
|
95
|
+
const [v, setV] = React.useState(note);
|
|
96
|
+
const id = React.useId();
|
|
97
|
+
return /* @__PURE__ */ jsxs("form", {
|
|
98
|
+
className: "flex flex-col gap-2",
|
|
99
|
+
onSubmit: (e) => {
|
|
100
|
+
e.preventDefault();
|
|
101
|
+
onDone();
|
|
102
|
+
},
|
|
103
|
+
children: [
|
|
104
|
+
/* @__PURE__ */ jsxs("label", {
|
|
105
|
+
htmlFor: id,
|
|
106
|
+
className: "text-kit-xs font-medium text-kit-fg-2",
|
|
107
|
+
children: ["Footnote ", n]
|
|
108
|
+
}),
|
|
109
|
+
/* @__PURE__ */ jsx(Textarea, {
|
|
110
|
+
id,
|
|
111
|
+
autoFocus: true,
|
|
112
|
+
rows: 3,
|
|
113
|
+
value: v,
|
|
114
|
+
placeholder: "Write the note",
|
|
115
|
+
onChange: (e) => {
|
|
116
|
+
setV(e.target.value);
|
|
117
|
+
onChange(e.target.value);
|
|
118
|
+
},
|
|
119
|
+
onKeyDown: (e) => {
|
|
120
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
121
|
+
e.preventDefault();
|
|
122
|
+
onDone();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}),
|
|
126
|
+
/* @__PURE__ */ jsxs("div", {
|
|
127
|
+
className: "flex items-center justify-between",
|
|
128
|
+
children: [/* @__PURE__ */ jsx(Button, {
|
|
129
|
+
size: "xs",
|
|
130
|
+
variant: "ghost",
|
|
131
|
+
leftIcon: /* @__PURE__ */ jsx(IconTrash, { stroke: 1.5 }),
|
|
132
|
+
onClick: onRemove,
|
|
133
|
+
className: "hover:text-kit-danger-fg",
|
|
134
|
+
children: "Remove"
|
|
135
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
136
|
+
size: "xs",
|
|
137
|
+
variant: "primary",
|
|
138
|
+
type: "submit",
|
|
139
|
+
children: "Done"
|
|
140
|
+
})]
|
|
141
|
+
})
|
|
142
|
+
]
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
/** The notes under the body, numbered in text order, each with a way back to its marker. Renders nothing when empty. */
|
|
146
|
+
function FootnotesList({ notes, readOnly, density = "default", onEdit, onBack }) {
|
|
147
|
+
if (!notes.length) return null;
|
|
148
|
+
return /* @__PURE__ */ jsx("section", {
|
|
149
|
+
"aria-label": "Footnotes",
|
|
150
|
+
className: cn("border-t border-kit-line-subtle text-kit-sm text-kit-fg-2", density === "bare" ? "mt-4 pt-2" : density === "dense" ? "mx-2.5 mb-2 pt-2" : "mx-4 mb-3 pt-2.5"),
|
|
151
|
+
children: /* @__PURE__ */ jsx("ol", {
|
|
152
|
+
className: "m-0 flex list-decimal flex-col gap-1 pl-5 marker:text-kit-fg-3",
|
|
153
|
+
children: notes.map((n, i) => /* @__PURE__ */ jsx("li", {
|
|
154
|
+
className: "pl-1",
|
|
155
|
+
children: /* @__PURE__ */ jsxs("span", {
|
|
156
|
+
className: "flex items-start gap-1",
|
|
157
|
+
children: [/* @__PURE__ */ jsx("button", {
|
|
158
|
+
type: "button",
|
|
159
|
+
disabled: readOnly,
|
|
160
|
+
"aria-label": `Footnote ${i + 1}: ${n.note || "empty"}. Edit`,
|
|
161
|
+
onClick: () => onEdit(n.el),
|
|
162
|
+
className: "min-w-0 flex-1 rounded-kit-sm text-left leading-relaxed enabled:hover:text-kit-fg",
|
|
163
|
+
children: n.note || /* @__PURE__ */ jsx("span", {
|
|
164
|
+
className: "text-kit-warning-fg",
|
|
165
|
+
children: "Empty note"
|
|
166
|
+
})
|
|
167
|
+
}), /* @__PURE__ */ jsx(Tooltip, {
|
|
168
|
+
content: "Back to the text",
|
|
169
|
+
children: /* @__PURE__ */ jsx("button", {
|
|
170
|
+
type: "button",
|
|
171
|
+
"aria-label": `Back to reference ${i + 1}`,
|
|
172
|
+
onClick: () => onBack(n.el),
|
|
173
|
+
className: "shrink-0 rounded-kit-sm px-1 text-kit-fg-3 hover:bg-kit-hover hover:text-kit-fg",
|
|
174
|
+
children: "↩"
|
|
175
|
+
})
|
|
176
|
+
})]
|
|
177
|
+
})
|
|
178
|
+
}, i))
|
|
179
|
+
})
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
//#endregion
|
|
183
|
+
export { FN_MARK, FOOTNOTE_POPOVER_CLASS, FOOTNOTE_REFUSED, FootnoteEditor, FootnotesList, backToMarker, caretAfterMarker, footnoteMarkers, insertFootnote, renumberFootnotes };
|
|
184
|
+
|
|
185
|
+
//# sourceMappingURL=footnote.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"footnote.js","names":[],"sources":["../../../src/controls/rich-text/footnote.tsx"],"sourcesContent":["/**\n * RICH TEXT · Footnotes (kit-richtext-ui-spec.md §1): V2's inline footnote, stored as the PT inline object\n * `{ _type: \"footnote\", note }`, written in the editor as `<sup data-rt=\"fn\" data-note contenteditable=\"false\">N</sup>`.\n * The number, the notes list and the way back are derived from the markers' text order, never stored.\n *\n * Editor chrome, not an island: the editor owns the clicks and the popover's anchor; this module is the V2 parts.\n * insert V2 ops.ts:182-205: at the caret through the browser's insert (⌘Z takes it back), never inside a rich\n * block or code; null then, and the editor toasts FOOTNOTE_REFUSED (V2 rich-text-editor.tsx:610-615)\n * numbers V2 dom.ts:429-436: the body's own markers in text order, 1…n\n * popover V2 FootnoteEditor (rich-text-editor.tsx:1736-1777) at the marker, w-[320px] rounded p-3\n * list V2 notes section (rich-text-editor.tsx:1533-1560): each note a button that edits it, and ↩ back to it\n * back V2 backToMarker (rich-text-editor.tsx:1095-1100): scroll to it, caret after it, a 1200ms accent flash\n */\nimport * as React from \"react\";\nimport { IconTrash } from \"@tabler/icons-react\";\nimport { cn } from \"../../lib/cn\";\nimport { Button } from \"../../ui/button\";\nimport { Textarea } from \"../../ui/input\";\nimport { Tooltip } from \"../../ui/overlay\";\n\nexport const FN_MARK = \"sup[data-rt=fn]\";\n/** The toast when a footnote cannot go where the caret is (V2 rich-text-editor.tsx:613). */\nexport const FOOTNOTE_REFUSED = {\n title: \"Put the caret in the text first\",\n description: \"Footnotes number the story's own text, not text inside a block.\",\n} as const;\n/** The popover's chrome (V2 rich-text-editor.tsx:1650). */\nexport const FOOTNOTE_POPOVER_CLASS = \"w-[320px] rounded-kit-lg p-3\";\n\n/** The body's own markers in text order (markers inside a rich block are not the body's). */\nexport const footnoteMarkers = (root: HTMLElement): HTMLElement[] =>\n Array.from(root.querySelectorAll<HTMLElement>(FN_MARK)).filter((m) => !m.closest(\"[data-rt-island],[data-rt-isolate]\"));\n\n/** Numbers the markers 1…n in text order; returns them with their notes, for the list. */\nexport function renumberFootnotes(root: HTMLElement): { el: HTMLElement; note: string }[] {\n return footnoteMarkers(root).map((m, i) => {\n if (m.textContent !== String(i + 1)) m.textContent = String(i + 1);\n return { el: m, note: m.getAttribute(\"data-note\") ?? \"\" };\n });\n}\n\n/** A new, empty marker at the caret. Null when the caret is outside the body's text (a rich block, code, elsewhere). */\nexport function insertFootnote(root: HTMLElement): HTMLElement | null {\n const s = window.getSelection();\n if (!s || !s.rangeCount || !root.contains(s.anchorNode)) return null;\n const at = s.anchorNode?.nodeType === 1 ? (s.anchorNode as HTMLElement) : s.anchorNode?.parentElement;\n if (!at || at.closest(\"[data-rt-island],[data-rt-isolate]\") || at.closest(\"pre\")) return null;\n root.focus({ preventScroll: true });\n if (typeof document.execCommand === \"function\") document.execCommand(\"insertHTML\", false, `<sup data-rt=\"fn\" data-note=\"\" contenteditable=\"false\" data-fn-new=\"\">1</sup>`);\n let sup = root.querySelector<HTMLElement>(\"sup[data-fn-new]\");\n if (!sup) {\n // the browser dropped the marker (or has no execCommand): place it by hand\n sup = document.createElement(\"sup\");\n sup.setAttribute(\"data-rt\", \"fn\");\n sup.setAttribute(\"data-note\", \"\");\n sup.setAttribute(\"contenteditable\", \"false\");\n sup.textContent = \"1\";\n const r = s.getRangeAt(0);\n r.collapse(false);\n r.insertNode(sup);\n const after = document.createTextNode(\"\");\n sup.after(after);\n const c = document.createRange();\n c.setStart(after, after.length);\n c.collapse(true);\n s.removeAllRanges();\n s.addRange(c);\n }\n sup.removeAttribute(\"data-fn-new\");\n renumberFootnotes(root);\n return sup;\n}\n\n/** The caret right after a marker, in the body. */\nexport function caretAfterMarker(root: HTMLElement, m: HTMLElement) {\n const s = window.getSelection();\n if (!s || !m.isConnected) return;\n root.focus({ preventScroll: true });\n const r = document.createRange();\n r.setStartAfter(m);\n r.collapse(true);\n s.removeAllRanges();\n s.addRange(r);\n}\n\n/** \"Back to the text\": scroll the marker to the centre, caret after it, and flash it (1200ms). */\nexport function backToMarker(root: HTMLElement, m: HTMLElement) {\n if (!m.isConnected) return;\n m.scrollIntoView?.({ block: \"center\", behavior: \"smooth\" });\n caretAfterMarker(root, m);\n m.animate?.([{ boxShadow: \"0 0 0 3px var(--kit-accent-soft)\" }, { boxShadow: \"0 0 0 0 transparent\" }], { duration: 1200 });\n}\n\n/** The note of one footnote: one setting, edited in a popover at its marker (V2 D54 / D55). */\nexport function FootnoteEditor({ n, note, onChange, onRemove, onDone }: { n: number; note: string; onChange: (v: string) => void; onRemove: () => void; onDone: () => void }) {\n const [v, setV] = React.useState(note);\n const id = React.useId();\n return (\n <form\n className=\"flex flex-col gap-2\"\n onSubmit={(e) => {\n e.preventDefault();\n onDone();\n }}\n >\n <label htmlFor={id} className=\"text-kit-xs font-medium text-kit-fg-2\">\n Footnote {n}\n </label>\n <Textarea\n id={id}\n autoFocus\n rows={3}\n value={v}\n placeholder=\"Write the note\"\n onChange={(e) => {\n setV(e.target.value);\n onChange(e.target.value);\n }}\n onKeyDown={(e) => {\n if (e.key === \"Enter\" && !e.shiftKey) {\n e.preventDefault();\n onDone();\n }\n }}\n />\n <div className=\"flex items-center justify-between\">\n <Button size=\"xs\" variant=\"ghost\" leftIcon={<IconTrash stroke={1.5} />} onClick={onRemove} className=\"hover:text-kit-danger-fg\">\n Remove\n </Button>\n <Button size=\"xs\" variant=\"primary\" type=\"submit\">\n Done\n </Button>\n </div>\n </form>\n );\n}\n\n/** The notes under the body, numbered in text order, each with a way back to its marker. Renders nothing when empty. */\nexport function FootnotesList({\n notes,\n readOnly,\n density = \"default\",\n onEdit,\n onBack,\n}: {\n notes: { el: HTMLElement; note: string }[];\n readOnly?: boolean;\n /** the editor's spacing: bare (no chrome), dense (panel), default */\n density?: \"bare\" | \"dense\" | \"default\";\n onEdit: (el: HTMLElement) => void;\n onBack: (el: HTMLElement) => void;\n}) {\n if (!notes.length) return null;\n return (\n <section\n aria-label=\"Footnotes\"\n className={cn(\"border-t border-kit-line-subtle text-kit-sm text-kit-fg-2\", density === \"bare\" ? \"mt-4 pt-2\" : density === \"dense\" ? \"mx-2.5 mb-2 pt-2\" : \"mx-4 mb-3 pt-2.5\")}\n >\n <ol className=\"m-0 flex list-decimal flex-col gap-1 pl-5 marker:text-kit-fg-3\">\n {notes.map((n, i) => (\n <li key={i} className=\"pl-1\">\n <span className=\"flex items-start gap-1\">\n <button\n type=\"button\"\n disabled={readOnly}\n aria-label={`Footnote ${i + 1}: ${n.note || \"empty\"}. Edit`}\n onClick={() => onEdit(n.el)}\n className=\"min-w-0 flex-1 rounded-kit-sm text-left leading-relaxed enabled:hover:text-kit-fg\"\n >\n {n.note || <span className=\"text-kit-warning-fg\">Empty note</span>}\n </button>\n <Tooltip content=\"Back to the text\">\n <button type=\"button\" aria-label={`Back to reference ${i + 1}`} onClick={() => onBack(n.el)} className=\"shrink-0 rounded-kit-sm px-1 text-kit-fg-3 hover:bg-kit-hover hover:text-kit-fg\">\n ↩\n </button>\n </Tooltip>\n </span>\n </li>\n ))}\n </ol>\n </section>\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { TypeSchema } from "../../schema/types";
|
|
2
|
+
import type { ImageValue } from "../media";
|
|
3
|
+
import type { IslandCtx } from "./blocks";
|
|
4
|
+
import type { IslandEl } from "./dom";
|
|
5
|
+
export interface GalleryImage {
|
|
6
|
+
_key: string;
|
|
7
|
+
image: ImageValue | null;
|
|
8
|
+
caption: string;
|
|
9
|
+
}
|
|
10
|
+
export interface GalleryModel {
|
|
11
|
+
layout: "grid" | "carousel";
|
|
12
|
+
images: GalleryImage[];
|
|
13
|
+
}
|
|
14
|
+
export declare const GALLERY_LABEL = "Gallery";
|
|
15
|
+
export declare const galleryDefault: () => GalleryModel;
|
|
16
|
+
export declare const GALLERY_SCHEMA: TypeSchema;
|
|
17
|
+
export declare function GalleryPreview({ m, onAdd, ro }: {
|
|
18
|
+
m: GalleryModel;
|
|
19
|
+
onAdd: () => void;
|
|
20
|
+
ro: boolean;
|
|
21
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
22
|
+
export declare function GalleryBlock({ m, set, el, ctx, autoFocus, }: {
|
|
23
|
+
m: GalleryModel;
|
|
24
|
+
set: (fn: (m: GalleryModel) => GalleryModel, label?: string) => void;
|
|
25
|
+
el: IslandEl;
|
|
26
|
+
ctx: IslandCtx;
|
|
27
|
+
autoFocus: boolean;
|
|
28
|
+
}): import("react/jsx-runtime").JSX.Element;
|