@bettercms-ai/kit 2.0.0-next.0 → 2.0.0-next.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.
Files changed (90) hide show
  1. package/dist/adapters/lossy.d.ts +9 -1
  2. package/dist/adapters/lossy.js +16 -5
  3. package/dist/adapters/lossy.js.map +1 -1
  4. package/dist/adapters/prod-types.d.ts +9 -22
  5. package/dist/adapters/prod-types.js +36 -35
  6. package/dist/adapters/prod-types.js.map +1 -1
  7. package/dist/adapters/schema.js +84 -39
  8. package/dist/adapters/schema.js.map +1 -1
  9. package/dist/context.d.ts +39 -0
  10. package/dist/context.js +40 -1
  11. package/dist/context.js.map +1 -1
  12. package/dist/controls/link.js +112 -196
  13. package/dist/controls/link.js.map +1 -1
  14. package/dist/controls/location.js +3 -8
  15. package/dist/controls/location.js.map +1 -1
  16. package/dist/controls/media.d.ts +0 -14
  17. package/dist/controls/media.js +52 -95
  18. package/dist/controls/media.js.map +1 -1
  19. package/dist/controls/rich-text/article-shell.d.ts +145 -0
  20. package/dist/controls/rich-text/article-shell.js +510 -0
  21. package/dist/controls/rich-text/article-shell.js.map +1 -0
  22. package/dist/controls/rich-text/author.d.ts +25 -0
  23. package/dist/controls/rich-text/author.js +104 -0
  24. package/dist/controls/rich-text/author.js.map +1 -0
  25. package/dist/controls/rich-text/blocks.d.ts +19 -2
  26. package/dist/controls/rich-text/blocks.js +273 -638
  27. package/dist/controls/rich-text/blocks.js.map +1 -1
  28. package/dist/controls/rich-text/button.d.ts +23 -0
  29. package/dist/controls/rich-text/button.js +85 -0
  30. package/dist/controls/rich-text/button.js.map +1 -0
  31. package/dist/controls/rich-text/catalog.d.ts +2 -5
  32. package/dist/controls/rich-text/catalog.js +120 -6
  33. package/dist/controls/rich-text/catalog.js.map +1 -1
  34. package/dist/controls/rich-text/checklist.d.ts +24 -0
  35. package/dist/controls/rich-text/checklist.js +115 -0
  36. package/dist/controls/rich-text/checklist.js.map +1 -0
  37. package/dist/controls/rich-text/codec.d.ts +10 -1
  38. package/dist/controls/rich-text/codec.js +11 -1
  39. package/dist/controls/rich-text/codec.js.map +1 -1
  40. package/dist/controls/rich-text/columns.d.ts +41 -0
  41. package/dist/controls/rich-text/columns.js +181 -0
  42. package/dist/controls/rich-text/columns.js.map +1 -0
  43. package/dist/controls/rich-text/control.js +1 -0
  44. package/dist/controls/rich-text/control.js.map +1 -1
  45. package/dist/controls/rich-text/dom.d.ts +84 -4
  46. package/dist/controls/rich-text/dom.js +171 -10
  47. package/dist/controls/rich-text/dom.js.map +1 -1
  48. package/dist/controls/rich-text/editor.d.ts +4 -2
  49. package/dist/controls/rich-text/editor.js +184 -12
  50. package/dist/controls/rich-text/editor.js.map +1 -1
  51. package/dist/controls/rich-text/embed.d.ts +28 -0
  52. package/dist/controls/rich-text/embed.js +210 -0
  53. package/dist/controls/rich-text/embed.js.map +1 -0
  54. package/dist/controls/rich-text/footnote.d.ts +41 -0
  55. package/dist/controls/rich-text/footnote.js +185 -0
  56. package/dist/controls/rich-text/footnote.js.map +1 -0
  57. package/dist/controls/rich-text/gallery.d.ts +28 -0
  58. package/dist/controls/rich-text/gallery.js +167 -0
  59. package/dist/controls/rich-text/gallery.js.map +1 -0
  60. package/dist/controls/rich-text/link-form.d.ts +3 -1
  61. package/dist/controls/rich-text/link-form.js +2 -2
  62. package/dist/controls/rich-text/link-form.js.map +1 -1
  63. package/dist/controls/rich-text/ops.js +6 -0
  64. package/dist/controls/rich-text/ops.js.map +1 -1
  65. package/dist/controls/rich-text/related.d.ts +36 -0
  66. package/dist/controls/rich-text/related.js +213 -0
  67. package/dist/controls/rich-text/related.js.map +1 -0
  68. package/dist/controls/rich-text/toc.d.ts +47 -0
  69. package/dist/controls/rich-text/toc.js +160 -0
  70. package/dist/controls/rich-text/toc.js.map +1 -0
  71. package/dist/core/core.d.ts +11 -3
  72. package/dist/core/core.js +24 -6
  73. package/dist/core/core.js.map +1 -1
  74. package/dist/core/facets.d.ts +16 -0
  75. package/dist/core/facets.js +17 -1
  76. package/dist/core/facets.js.map +1 -1
  77. package/dist/core/index.js +2 -2
  78. package/dist/index.js +4 -4
  79. package/dist/library/kit-catalog.json +47 -44
  80. package/dist/schema/types.d.ts +15 -0
  81. package/dist/schema/types.js.map +1 -1
  82. package/dist/styles.css +1 -1
  83. package/dist/ui/autocomplete.d.ts +29 -1
  84. package/dist/ui/autocomplete.js +102 -47
  85. package/dist/ui/autocomplete.js.map +1 -1
  86. package/dist/ui/button.js +3 -5
  87. package/dist/ui/button.js.map +1 -1
  88. package/dist/ui/index.js +2 -2
  89. package/dist/ui/toast.js +1 -1
  90. package/package.json +3 -1
@@ -1,18 +1,24 @@
1
+ import { isCropRect } from "../../adapters/values.js";
1
2
  import { cn } from "../../lib/cn.js";
2
3
  import { useKitServices } from "../../context.js";
3
4
  import { Button, IconButton } from "../../ui/button.js";
4
- import { Menu, MenuContent, MenuItem, MenuSeparator, MenuTrigger, Popover, PopoverAnchor, PopoverContent, PopoverTrigger, Tooltip } from "../../ui/overlay.js";
5
+ import { Menu, MenuContent, MenuItem, MenuTrigger, Popover, PopoverContent, PopoverTrigger, Tooltip } from "../../ui/overlay.js";
5
6
  import { Input, Textarea } from "../../ui/input.js";
6
7
  import { TableEditor } from "../table/table-editor.js";
7
- import { SchemaForm } from "../../engine/schema-form.js";
8
- import { eventKey } from "../../lib/key-event.js";
9
- import { FloatingPanel } from "../../ui/floating-panel.js";
10
- import { CALLOUT_TONES, detectEmbed, placeCaret } from "./dom.js";
8
+ import { VideoPreview } from "../media.js";
9
+ import { ArticleActions, ArticleCard, ArticleForm, ArticlePanel, articleCardKeyDown, useArticleOpen } from "./article-shell.js";
10
+ import { AuthorBlock } from "./author.js";
11
+ import { CALLOUT_TONES, placeCaret } from "./dom.js";
11
12
  import { LinkForm } from "./link-form.js";
13
+ import { EmbedBlock } from "./embed.js";
14
+ import { GalleryBlock } from "./gallery.js";
15
+ import { TocBlock } from "./toc.js";
16
+ import { ColumnsBlock } from "./columns.js";
17
+ import { RelatedBlock } from "./related.js";
12
18
  import { ComponentCard } from "./component-card.js";
13
19
  import * as React from "react";
14
20
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
15
- import { IconAlertTriangle, IconAlt, IconArrowsHorizontal, IconArrowsMove, IconBrandCodepen, IconBrandFigma, IconBrandSpotify, IconBrandVimeo, IconBrandX, IconBrandYoutube, IconBulb, IconCheck, IconChevronDown, IconChevronRight, IconComponents, IconCopy, IconDimensions, IconDots, IconFileText, IconFileUpload, IconHeadphones, IconInfoCircle, IconLayoutAlignCenter, IconLayoutAlignLeft, IconLayoutAlignRight, IconLayoutSidebar, IconLayoutSidebarRight, IconLibraryPhoto, IconLink, IconMap, IconMicrophone, IconPhoto, IconPlayerPlayFilled, IconQuote, IconReplace, IconSettings, IconTrash, IconWorld, IconX } from "@tabler/icons-react";
21
+ import { IconAlertTriangle, IconAlt, IconArrowsHorizontal, IconBulb, IconCheck, IconChevronDown, IconChevronRight, IconComponents, IconCopy, IconFileText, IconFileUpload, IconInfoCircle, IconLayoutAlignCenter, IconLayoutAlignLeft, IconLayoutAlignRight, IconLibraryPhoto, IconLink, IconPhoto, IconQuote, IconReplace, IconTrash, IconWorld } from "@tabler/icons-react";
16
22
  //#region src/controls/rich-text/blocks.tsx
17
23
  /**
18
24
  * RICH TEXT · rich blocks ("islands", V2 HEAD controls/rich-text/blocks.tsx). Each renders through a portal into a
@@ -29,8 +35,7 @@ import { IconAlertTriangle, IconAlt, IconArrowsHorizontal, IconArrowsMove, IconB
29
35
  * block (article-blocks.tsx ArticleBlock + PullQuotePreview): drawn as the reader sees it, edited in the floating
30
36
  * properties panel (4 settings → panel, CPO D54) through the kit SchemaForm. Key takeaways are V2's article block too,
31
37
  * written in the text itself (article-blocks.tsx TakeawaysEditor, surface "inline"): a title line and a line per point.
32
- * The embed is V2's article block too (ArticleBlock surface "popover" + EmbedPreview): a placeholder card of the link's
33
- * provider, edited in a popover beside it (Link, Title; V2's Shape has no stored key, see dom.ts).
38
+ * The embed island draws embed.tsx's block (V2's article Embed: Link, Title, Shape), keeping the keys it lacks absent.
34
39
  */
35
40
  function Island({ el, ctx }) {
36
41
  const kind = el.getAttribute("data-rt-island");
@@ -114,7 +119,37 @@ function Island({ el, ctx }) {
114
119
  autoFocus,
115
120
  m: model,
116
121
  set
117
- }) : kind === "embed" ? /* @__PURE__ */ jsx(EmbedBlock, {
122
+ }) : kind === "embed" ? /* @__PURE__ */ jsx(EmbedIsland, {
123
+ el,
124
+ ctx,
125
+ autoFocus,
126
+ m: model,
127
+ set
128
+ }) : kind === "gallery" ? /* @__PURE__ */ jsx(GalleryIsland, {
129
+ el,
130
+ ctx,
131
+ autoFocus,
132
+ m: model,
133
+ set
134
+ }) : kind === "toc" ? /* @__PURE__ */ jsx(TocIsland, {
135
+ el,
136
+ ctx,
137
+ autoFocus,
138
+ m: model,
139
+ set
140
+ }) : kind === "columns" ? /* @__PURE__ */ jsx(ColumnsIsland, {
141
+ el,
142
+ ctx,
143
+ autoFocus,
144
+ m: model,
145
+ set
146
+ }) : kind === "author" ? /* @__PURE__ */ jsx(AuthorIsland, {
147
+ el,
148
+ ctx,
149
+ autoFocus,
150
+ m: model,
151
+ set
152
+ }) : kind === "related" ? /* @__PURE__ */ jsx(RelatedIsland, {
118
153
  el,
119
154
  ctx,
120
155
  autoFocus,
@@ -135,6 +170,183 @@ function Island({ el, ctx }) {
135
170
  }) : null
136
171
  });
137
172
  }
173
+ /** The island's stored embed (keys absent when the block lacks them) through embed.tsx's block (V2's model). */
174
+ function EmbedIsland({ m, set, el, ctx, autoFocus }) {
175
+ return /* @__PURE__ */ jsx(EmbedBlock, {
176
+ m: {
177
+ url: m.url ?? "",
178
+ title: m.title ?? "",
179
+ aspect: m.aspect ?? "16:9"
180
+ },
181
+ set: (fn, label) => set((prev) => {
182
+ const next = fn({
183
+ url: prev.url ?? "",
184
+ title: prev.title ?? "",
185
+ aspect: prev.aspect ?? "16:9"
186
+ });
187
+ const out = { ...prev };
188
+ if (next.url !== (prev.url ?? "")) out.url = next.url;
189
+ if (next.title !== (prev.title ?? "")) out.title = next.title;
190
+ if (next.aspect !== (prev.aspect ?? "16:9")) out.aspect = next.aspect;
191
+ return out;
192
+ }, label),
193
+ el,
194
+ ctx,
195
+ autoFocus,
196
+ shape: ctx.allows?.("embedAspect") ?? false
197
+ });
198
+ }
199
+ /** The island's stored gallery (src, alt, caption per image) through gallery.tsx's block (V2's model, an image value each). */
200
+ /** RT9: a gallery image's framing, image-url's 0–1 focal point ⇄ the kit image value's percent */
201
+ var toGallery = (m) => ({
202
+ layout: m.layout ?? "grid",
203
+ images: m.images.map((x) => ({
204
+ _key: x._key,
205
+ image: x.src ? {
206
+ url: x.src,
207
+ ...x.alt !== void 0 ? { alt: x.alt } : {},
208
+ ...x.crop ? { crop: x.crop } : {},
209
+ ...x.focal ? { focal: {
210
+ x: x.focal.x * 100,
211
+ y: x.focal.y * 100
212
+ } } : {}
213
+ } : null,
214
+ caption: x.caption ?? ""
215
+ }))
216
+ });
217
+ var cropOf = (c) => isCropRect(c) ? {
218
+ top: c.top,
219
+ right: c.right,
220
+ bottom: c.bottom,
221
+ left: c.left
222
+ } : void 0;
223
+ var focalOf = (f) => {
224
+ const p = f;
225
+ return typeof p?.x === "number" && typeof p?.y === "number" ? {
226
+ x: Math.round(p.x) / 100,
227
+ y: Math.round(p.y) / 100
228
+ } : void 0;
229
+ };
230
+ function GalleryIsland({ m, set, el, ctx, autoFocus }) {
231
+ return /* @__PURE__ */ jsx(GalleryBlock, {
232
+ m: toGallery(m),
233
+ set: (fn, label) => set((prev) => {
234
+ const next = fn(toGallery(prev));
235
+ const out = {
236
+ ...prev,
237
+ images: next.images.map((x) => {
238
+ const crop = cropOf(x.image?.crop);
239
+ const focal = focalOf(x.image?.focal);
240
+ return {
241
+ _key: x._key,
242
+ src: x.image?.url ?? "",
243
+ ...x.image?.alt !== void 0 ? { alt: x.image.alt } : {},
244
+ ...x.caption ? { caption: x.caption } : {},
245
+ ...crop ? { crop } : {},
246
+ ...focal ? { focal } : {}
247
+ };
248
+ })
249
+ };
250
+ if (next.layout !== (prev.layout ?? "grid")) out.layout = next.layout;
251
+ return out;
252
+ }, label),
253
+ el,
254
+ ctx,
255
+ autoFocus
256
+ });
257
+ }
258
+ /** The island's stored table of contents (title, depth) through toc.tsx's block, which lists the editor's live outline. */
259
+ function TocIsland({ m, set, el, ctx, autoFocus }) {
260
+ return /* @__PURE__ */ jsx(TocBlock, {
261
+ m: {
262
+ title: m.title ?? "",
263
+ depth: m.depth ?? "3"
264
+ },
265
+ set: (fn, label) => set((prev) => {
266
+ const next = fn({
267
+ title: prev.title ?? "",
268
+ depth: prev.depth ?? "3"
269
+ });
270
+ const out = { ...prev };
271
+ if (next.title !== (prev.title ?? "")) out.title = next.title;
272
+ if (next.depth !== (prev.depth ?? "3")) out.depth = next.depth;
273
+ return out;
274
+ }, label),
275
+ el,
276
+ ctx,
277
+ autoFocus
278
+ });
279
+ }
280
+ /** The island's stored columns (gap, each column's HTML) through columns.tsx's block. */
281
+ function ColumnsIsland({ m, set, el, ctx, autoFocus }) {
282
+ return /* @__PURE__ */ jsx(ColumnsBlock, {
283
+ m: {
284
+ gap: m.gap ?? "m",
285
+ cols: m.cols
286
+ },
287
+ set: (fn, label) => set((prev) => {
288
+ const next = fn({
289
+ gap: prev.gap ?? "m",
290
+ cols: prev.cols
291
+ });
292
+ const out = {
293
+ ...prev,
294
+ cols: next.cols
295
+ };
296
+ if (next.gap !== (prev.gap ?? "m")) out.gap = next.gap;
297
+ return out;
298
+ }, label),
299
+ el,
300
+ ctx,
301
+ autoFocus
302
+ });
303
+ }
304
+ /** The island's stored author reference through author.tsx's block, picking from the field's authors collection. */
305
+ function AuthorIsland({ m, set, el, ctx, autoFocus }) {
306
+ return /* @__PURE__ */ jsx(AuthorBlock, {
307
+ m: { author: m.author ?? "" },
308
+ set: (fn, label) => set((prev) => {
309
+ const next = fn({ author: prev.author ?? "" });
310
+ return next.author === (prev.author ?? "") ? prev : {
311
+ ...prev,
312
+ author: next.author
313
+ };
314
+ }, label),
315
+ el,
316
+ ctx,
317
+ autoFocus,
318
+ collection: ctx.references?.authors ?? ""
319
+ });
320
+ }
321
+ /** The island's stored related posts through related.tsx's block, over the field's posts collection. */
322
+ var toRelated = (m) => ({
323
+ title: m.title ?? "",
324
+ mode: m.mode ?? "topic",
325
+ count: m.count ?? 3,
326
+ posts: m.posts
327
+ });
328
+ function RelatedIsland({ m, set, el, ctx, autoFocus }) {
329
+ return /* @__PURE__ */ jsx(RelatedBlock, {
330
+ m: toRelated(m),
331
+ set: (fn, label) => set((prev) => {
332
+ const was = toRelated(prev);
333
+ const next = fn(was);
334
+ const out = {
335
+ ...prev,
336
+ posts: next.posts
337
+ };
338
+ if (next.title !== was.title) out.title = next.title;
339
+ if (next.mode !== was.mode) out.mode = next.mode;
340
+ if (next.count !== was.count) out.count = next.count;
341
+ return out;
342
+ }, label),
343
+ el,
344
+ ctx,
345
+ autoFocus,
346
+ collection: ctx.references?.posts ?? "",
347
+ excludeId: ctx.references?.entryId
348
+ });
349
+ }
138
350
  var Sep = () => /* @__PURE__ */ jsx("span", {
139
351
  "aria-hidden": true,
140
352
  className: "mx-0.5 h-4 w-px shrink-0 bg-kit-line"
@@ -744,13 +956,6 @@ function ToggleBlock({ m, set, el, ctx, autoFocus }) {
744
956
  });
745
957
  }
746
958
  var videoUrlOk = (u) => /^https?:\/\/\S+$/i.test(u) || /^\/(?!\/)\S*$/.test(u);
747
- function videoEmbedSrc(url) {
748
- const yt = /(?:youtube\.com\/(?:watch\?(?:.*&)?v=|embed\/|shorts\/|live\/)|youtu\.be\/)([\w-]{6,})/i.exec(url);
749
- if (yt) return `https://www.youtube-nocookie.com/embed/${yt[1]}`;
750
- const vimeo = /vimeo\.com\/(?:video\/)?(\d+)/i.exec(url);
751
- if (vimeo) return `https://player.vimeo.com/video/${vimeo[1]}`;
752
- return null;
753
- }
754
959
  function VideoBlock({ m, set, el, ctx, autoFocus }) {
755
960
  const [draft, setDraft] = React.useState(m.url ?? "");
756
961
  React.useEffect(() => setDraft(m.url ?? ""), [m.url]);
@@ -764,7 +969,6 @@ function VideoBlock({ m, set, el, ctx, autoFocus }) {
764
969
  set({ url: u }, "Set video link");
765
970
  };
766
971
  const ro = ctx.readOnly;
767
- const frame = m.url ? videoEmbedSrc(m.url) : null;
768
972
  return /* @__PURE__ */ jsxs("div", {
769
973
  className: "group/blk rounded-kit-lg border border-kit-line bg-kit-subtle p-2.5",
770
974
  "data-rt-isolate": "",
@@ -802,20 +1006,9 @@ function VideoBlock({ m, set, el, ctx, autoFocus }) {
802
1006
  className: "mb-2 px-1 text-kit-xs text-kit-danger-fg",
803
1007
  children: error
804
1008
  }) : null,
805
- frame ? /* @__PURE__ */ jsx("iframe", {
806
- src: frame,
807
- title: m.title || "Video preview",
808
- loading: "lazy",
809
- tabIndex: -1,
810
- sandbox: "allow-scripts allow-same-origin allow-presentation",
811
- referrerPolicy: "strict-origin-when-cross-origin",
812
- className: "pointer-events-none aspect-video w-full rounded-kit-md border-0 bg-kit-muted"
813
- }) : m.url ? /* @__PURE__ */ jsx("video", {
814
- src: m.url,
815
- title: m.title || "Video preview",
816
- controls: true,
817
- preload: "metadata",
818
- className: "aspect-video w-full rounded-kit-md bg-kit-muted"
1009
+ m.url ? /* @__PURE__ */ jsx(VideoPreview, {
1010
+ url: m.url,
1011
+ title: "Video"
819
1012
  }) : /* @__PURE__ */ jsx("p", {
820
1013
  className: "px-1 pb-0.5 text-kit-xs text-kit-fg-3",
821
1014
  children: "No video yet."
@@ -1101,132 +1294,52 @@ var PULL_QUOTE_SCHEMA = {
1101
1294
  }
1102
1295
  ]
1103
1296
  };
1104
- var openArticle = null;
1105
- var articleSubs = /* @__PURE__ */ new Set();
1106
- var setOpenArticle = (next) => {
1107
- openArticle = next;
1108
- articleSubs.forEach((f) => f());
1109
- };
1110
- function useOpenArticle() {
1111
- return React.useSyncExternalStore((cb) => (articleSubs.add(cb), () => void articleSubs.delete(cb)), () => openArticle, () => openArticle);
1112
- }
1113
- var controlOf = (el) => Array.from(el.querySelectorAll("textarea, input:not([type=hidden]), [contenteditable=true], [role=combobox]")).find((n) => n.getClientRects().length) ?? Array.from(el.querySelectorAll(":scope > :not(:first-child) button")).find((n) => n.getClientRects().length);
1114
- function focusField(root, key) {
1115
- if (!root) return;
1116
- const rows = Array.from(root.querySelectorAll("[data-field-key]"));
1117
- const row = key && rows.find((r) => r.getAttribute("data-field-key") === key) || rows[0];
1118
- if (!row) return;
1119
- row.scrollIntoView({ block: "nearest" });
1120
- controlOf(row)?.focus({ preventScroll: true });
1121
- }
1122
1297
  function PullQuoteBlock({ m, set, el, ctx, autoFocus }) {
1123
1298
  const label = PULL_QUOTE_LABEL;
1124
- const uid = React.useId();
1125
1299
  const card = React.useRef(null);
1126
1300
  const [cardEl, setCardEl] = React.useState(null);
1127
- const open = useOpenArticle();
1128
- const editing = open?.id === uid;
1301
+ const ro = !!ctx.readOnly;
1302
+ const { open, editing, openPanel, close } = useArticleOpen({
1303
+ ro,
1304
+ autoFocus
1305
+ });
1129
1306
  const cardRef = React.useCallback((n) => {
1130
1307
  card.current = n;
1131
1308
  if (n) setCardEl(n);
1132
1309
  }, []);
1133
- const ro = !!ctx.readOnly;
1134
1310
  const modelRef = React.useRef(m);
1135
1311
  modelRef.current = m;
1136
- const openPanel = React.useCallback((k) => {
1137
- if (!ro) setOpenArticle({
1138
- id: uid,
1139
- focusKey: k ?? null,
1140
- nonce: (openArticle?.nonce ?? 0) + 1
1141
- });
1142
- }, [uid, ro]);
1143
- const close = React.useCallback(() => {
1144
- if (openArticle?.id === uid) setOpenArticle(null);
1145
- }, [uid]);
1146
1312
  const focusCard = React.useCallback(() => requestAnimationFrame(() => card.current?.focus({ preventScroll: true })), []);
1147
- React.useEffect(() => () => {
1148
- if (openArticle?.id === uid) setOpenArticle(null);
1149
- }, [uid]);
1150
- React.useEffect(() => {
1151
- if (autoFocus && !ro) openPanel();
1152
- }, []);
1153
1313
  /** a form edit joins the body's one undo history (D54); typing coalesces per field */
1154
1314
  const commit = (next) => {
1155
1315
  const prev = modelRef.current;
1156
1316
  const field = Object.keys(next).find((k) => next[k] !== prev[k]) ?? "";
1157
1317
  set(() => next, `Edit ${label.toLowerCase()} ${field}`);
1158
1318
  };
1159
- const onKeyDown = (e) => {
1160
- if (e.defaultPrevented || e.target !== card.current) return;
1161
- const mod = e.metaKey || e.ctrlKey;
1162
- const k = eventKey(e);
1163
- if (mod && k === "z") {
1164
- e.preventDefault();
1165
- e.stopPropagation();
1166
- if (e.shiftKey) ctx.redo?.();
1167
- else ctx.undo?.();
1168
- return;
1169
- }
1170
- if (e.key === "Enter" || e.key === " ") {
1171
- e.preventDefault();
1172
- openPanel();
1173
- return;
1174
- }
1175
- if (e.key === "Escape") {
1176
- e.preventDefault();
1177
- e.stopPropagation();
1178
- if (editing) close();
1179
- else ctx.exit(el);
1180
- return;
1181
- }
1182
- if (ro) return;
1183
- if (mod && k === "d" && ctx.duplicate) {
1184
- e.preventDefault();
1185
- e.stopPropagation();
1186
- ctx.duplicate(el);
1187
- } else if (e.altKey && (e.key === "ArrowUp" || e.key === "ArrowDown") && ctx.move) {
1188
- e.preventDefault();
1189
- ctx.move(el, e.key === "ArrowUp" ? -1 : 1);
1190
- focusCard();
1191
- } else if (e.key === "Backspace" || e.key === "Delete") {
1192
- e.preventDefault();
1193
- ctx.remove(el);
1194
- }
1195
- };
1196
- const actions = ro ? null : /* @__PURE__ */ jsxs("div", {
1197
- className: cn("absolute -top-3.5 right-2 z-[2] flex items-center gap-0.5 rounded-kit-md border border-kit-line bg-kit-surface-raised p-0.5 opacity-0 shadow-kit-sm transition-opacity", "group-hover/art:opacity-100 group-focus-within/art:opacity-100 pointer-coarse:opacity-100", editing && "opacity-100"),
1198
- children: [/* @__PURE__ */ jsx(IconButton, {
1199
- size: "xs",
1200
- label: `Edit ${label.toLowerCase()}`,
1201
- onClick: () => editing ? close() : openPanel(),
1202
- children: /* @__PURE__ */ jsx(IconSettings, { stroke: 1.5 })
1203
- }), /* @__PURE__ */ jsx(IconButton, {
1204
- size: "xs",
1205
- label: `Remove ${label.toLowerCase()}`,
1206
- onClick: () => ctx.remove(el),
1207
- className: "hover:text-kit-danger-fg",
1208
- children: /* @__PURE__ */ jsx(IconTrash, { stroke: 1.5 })
1209
- })]
1210
- });
1211
- return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("div", {
1319
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs(ArticleCard, {
1212
1320
  ref: cardRef,
1213
- role: "group",
1214
- tabIndex: 0,
1215
- "aria-label": `${label}${editing ? ", editing" : ""}`,
1216
- "aria-roledescription": "block",
1217
- "aria-haspopup": "dialog",
1218
- "aria-expanded": editing,
1219
- "data-rt-isolate": "",
1220
- "data-article-block": "pullquote",
1221
- onKeyDown,
1222
- onClick: (e) => {
1223
- if (ro || !e.currentTarget.contains(e.target)) return;
1224
- const t = e.target;
1225
- if (t.closest("button,a,[role=menuitem]")) return;
1226
- openPanel(t.closest("[data-field]")?.getAttribute("data-field") ?? null);
1227
- },
1228
- className: cn("group/art relative rounded-kit-lg outline-none transition-shadow duration-150", !ro && "cursor-pointer", "focus-visible:shadow-[0_0_0_1px_var(--kit-line-strong)]", editing ? "shadow-[0_0_0_2px_var(--kit-accent)] focus-visible:shadow-[0_0_0_2px_var(--kit-accent)]" : !ro && "hover:shadow-[0_0_0_1px_var(--kit-line)]"),
1229
- children: [/* @__PURE__ */ jsx(PullQuotePreview, { m }), actions]
1321
+ label,
1322
+ kind: "pullquote",
1323
+ ro,
1324
+ editing,
1325
+ onOpen: openPanel,
1326
+ onKeyDown: articleCardKeyDown({
1327
+ card,
1328
+ ctx,
1329
+ el,
1330
+ ro,
1331
+ editing,
1332
+ onOpen: openPanel,
1333
+ onClose: close,
1334
+ focusCard
1335
+ }),
1336
+ children: [/* @__PURE__ */ jsx(PullQuotePreview, { m }), /* @__PURE__ */ jsx(ArticleActions, {
1337
+ label,
1338
+ ctx,
1339
+ el,
1340
+ editing,
1341
+ onEdit: () => editing ? close() : openPanel()
1342
+ })]
1230
1343
  }), editing ? /* @__PURE__ */ jsx(ArticlePanel, {
1231
1344
  label,
1232
1345
  anchor: cardEl,
@@ -1236,22 +1349,12 @@ function PullQuoteBlock({ m, set, el, ctx, autoFocus }) {
1236
1349
  focusBack: focusCard,
1237
1350
  ctx,
1238
1351
  el,
1239
- children: /* @__PURE__ */ jsxs("div", {
1240
- "data-article-form": "",
1241
- className: "min-w-0",
1242
- children: [/* @__PURE__ */ jsx(SchemaForm, {
1243
- schema: PULL_QUOTE_SCHEMA,
1244
- value: m,
1245
- onChange: (v) => commit(v),
1246
- variant: "panel",
1247
- scroll: false,
1248
- readOnly: ro,
1249
- rootLabel: label,
1250
- env: {
1251
- docId: `article:${PULL_QUOTE_SCHEMA.name}`,
1252
- docTitle: label
1253
- }
1254
- }), /* @__PURE__ */ jsx("style", { children: `[data-article-form] [data-kit-form] section:first-of-type { border-top-width: 0; margin-top: 0; padding-top: 0; }` })]
1352
+ children: /* @__PURE__ */ jsx(ArticleForm, {
1353
+ schema: PULL_QUOTE_SCHEMA,
1354
+ label,
1355
+ value: m,
1356
+ onChange: (v) => commit(v),
1357
+ ro
1255
1358
  })
1256
1359
  }) : null] });
1257
1360
  }
@@ -1274,47 +1377,19 @@ function TakeawaysBlock({ m, set, el, ctx, autoFocus }) {
1274
1377
  });
1275
1378
  const setPoint = (i, v) => set({ points: m.points.map((p, j) => j === i ? v : p) }, `Edit key takeaways point ${i + 1}`);
1276
1379
  /** V2 ArticleBlock onKeyDown, on the block itself (its lines handle their own keys) */
1277
- const onKeyDown = (e) => {
1278
- if (e.defaultPrevented || e.target !== card.current) return;
1279
- const mod = e.metaKey || e.ctrlKey;
1280
- const k = eventKey(e);
1281
- if (mod && k === "z") {
1282
- e.preventDefault();
1283
- e.stopPropagation();
1284
- if (e.shiftKey) ctx.redo?.();
1285
- else ctx.undo?.();
1286
- return;
1287
- }
1288
- if (e.key === "Escape") {
1289
- e.preventDefault();
1290
- e.stopPropagation();
1291
- ctx.exit(el);
1292
- return;
1293
- }
1294
- if (ro) return;
1295
- if (mod && k === "d" && ctx.duplicate) {
1296
- e.preventDefault();
1297
- e.stopPropagation();
1298
- ctx.duplicate(el);
1299
- } else if (e.altKey && (e.key === "ArrowUp" || e.key === "ArrowDown") && ctx.move) {
1300
- e.preventDefault();
1301
- ctx.move(el, e.key === "ArrowUp" ? -1 : 1);
1302
- focusCard();
1303
- } else if (e.key === "Backspace" || e.key === "Delete") {
1304
- e.preventDefault();
1305
- ctx.remove(el);
1306
- }
1307
- };
1308
- return /* @__PURE__ */ jsxs("div", {
1380
+ const onKeyDown = articleCardKeyDown({
1381
+ card,
1382
+ ctx,
1383
+ el,
1384
+ ro,
1385
+ focusCard
1386
+ });
1387
+ return /* @__PURE__ */ jsxs(ArticleCard, {
1309
1388
  ref: card,
1310
- role: "group",
1311
- tabIndex: 0,
1312
- "aria-label": label,
1313
- "aria-roledescription": "block",
1314
- "data-rt-isolate": "",
1315
- "data-article-block": "takeaways",
1389
+ label,
1390
+ kind: "takeaways",
1391
+ ro,
1316
1392
  onKeyDown,
1317
- className: "group/art relative rounded-kit-lg outline-none transition-shadow duration-150 focus-visible:shadow-[0_0_0_1px_var(--kit-line-strong)]",
1318
1393
  children: [/* @__PURE__ */ jsxs("aside", {
1319
1394
  className: "rounded-kit-lg border border-kit-line bg-kit-subtle px-4 py-3",
1320
1395
  "aria-label": m.title || label,
@@ -1370,453 +1445,13 @@ function TakeawaysBlock({ m, set, el, ctx, autoFocus }) {
1370
1445
  })]
1371
1446
  }, i))
1372
1447
  })]
1373
- }), ro ? null : /* @__PURE__ */ jsx("div", {
1374
- className: cn("absolute -top-3.5 right-2 z-[2] flex items-center gap-0.5 rounded-kit-md border border-kit-line bg-kit-surface-raised p-0.5 opacity-0 shadow-kit-sm transition-opacity", "group-hover/art:opacity-100 group-focus-within/art:opacity-100 pointer-coarse:opacity-100"),
1375
- children: /* @__PURE__ */ jsx(IconButton, {
1376
- size: "xs",
1377
- label: `Remove ${label.toLowerCase()}`,
1378
- onClick: () => ctx.remove(el),
1379
- className: "hover:text-kit-danger-fg",
1380
- children: /* @__PURE__ */ jsx(IconTrash, { stroke: 1.5 })
1381
- })
1382
- })]
1383
- });
1384
- }
1385
- var EMBED_LABEL = "Embed";
1386
- /** V2 article.ts ARTICLE_SCHEMAS.embed without `aspect` (the stored block keeps no shape): the popover is built from it. */
1387
- var EMBED_SCHEMA = {
1388
- name: "embed",
1389
- title: EMBED_LABEL,
1390
- kind: "object",
1391
- icon: "code",
1392
- fields: [{
1393
- name: "url",
1394
- title: "Link",
1395
- type: "url",
1396
- placeholder: "Paste a YouTube, X, CodePen, Figma or map link",
1397
- validation: { required: true }
1398
- }, {
1399
- name: "title",
1400
- title: "Title",
1401
- type: "string",
1402
- validation: {
1403
- required: true,
1404
- max: 120
1405
- },
1406
- description: "Not shown. Read by screen readers."
1407
- }]
1408
- };
1409
- function EmbedBlock({ m, set, el, ctx, autoFocus }) {
1410
- const label = EMBED_LABEL;
1411
- const uid = React.useId();
1412
- const card = React.useRef(null);
1413
- const form = React.useRef(null);
1414
- const open = useOpenArticle();
1415
- const editing = open?.id === uid;
1416
- const ro = !!ctx.readOnly;
1417
- const modelRef = React.useRef(m);
1418
- modelRef.current = m;
1419
- const openPopover = React.useCallback((k) => {
1420
- if (!ro) setOpenArticle({
1421
- id: uid,
1422
- focusKey: k ?? null,
1423
- nonce: (openArticle?.nonce ?? 0) + 1
1424
- });
1425
- }, [uid, ro]);
1426
- const close = React.useCallback(() => {
1427
- if (openArticle?.id === uid) setOpenArticle(null);
1428
- }, [uid]);
1429
- const focusCard = React.useCallback(() => requestAnimationFrame(() => card.current?.focus({ preventScroll: true })), []);
1430
- React.useEffect(() => () => {
1431
- if (openArticle?.id === uid) setOpenArticle(null);
1432
- }, [uid]);
1433
- React.useEffect(() => {
1434
- if (autoFocus && !ro) openPopover();
1435
- }, []);
1436
- const nonce = editing ? open.nonce : 0;
1437
- React.useEffect(() => {
1438
- if (!nonce) return;
1439
- const id = requestAnimationFrame(() => focusField(form.current, openArticle?.focusKey ?? null));
1440
- return () => cancelAnimationFrame(id);
1441
- }, [nonce]);
1442
- /** a form edit joins the body's one undo history (D54); typing coalesces per field */
1443
- const commit = (next) => {
1444
- const prev = modelRef.current;
1445
- const field = ["url", "title"].find((k) => (next[k] ?? "") !== (prev[k] ?? "")) ?? "";
1446
- set(() => next, `Edit ${label.toLowerCase()} ${field}`);
1447
- };
1448
- const onKeyDown = (e) => {
1449
- if (e.defaultPrevented || e.target !== card.current) return;
1450
- const mod = e.metaKey || e.ctrlKey;
1451
- const k = eventKey(e);
1452
- if (mod && k === "z") {
1453
- e.preventDefault();
1454
- e.stopPropagation();
1455
- if (e.shiftKey) ctx.redo?.();
1456
- else ctx.undo?.();
1457
- return;
1458
- }
1459
- if (e.key === "Enter" || e.key === " ") {
1460
- e.preventDefault();
1461
- openPopover();
1462
- return;
1463
- }
1464
- if (e.key === "Escape") {
1465
- e.preventDefault();
1466
- e.stopPropagation();
1467
- if (editing) close();
1468
- else ctx.exit(el);
1469
- return;
1470
- }
1471
- if (ro) return;
1472
- if (mod && k === "d" && ctx.duplicate) {
1473
- e.preventDefault();
1474
- e.stopPropagation();
1475
- ctx.duplicate(el);
1476
- } else if (e.altKey && (e.key === "ArrowUp" || e.key === "ArrowDown") && ctx.move) {
1477
- e.preventDefault();
1478
- ctx.move(el, e.key === "ArrowUp" ? -1 : 1);
1479
- focusCard();
1480
- } else if (e.key === "Backspace" || e.key === "Delete") {
1481
- e.preventDefault();
1482
- ctx.remove(el);
1483
- }
1484
- };
1485
- const actions = ro ? null : /* @__PURE__ */ jsxs("div", {
1486
- className: cn("absolute -top-3.5 right-2 z-[2] flex items-center gap-0.5 rounded-kit-md border border-kit-line bg-kit-surface-raised p-0.5 opacity-0 shadow-kit-sm transition-opacity", "group-hover/art:opacity-100 group-focus-within/art:opacity-100 pointer-coarse:opacity-100", editing && "opacity-100"),
1487
- children: [/* @__PURE__ */ jsx(Tooltip, {
1488
- content: `Edit ${label.toLowerCase()}`,
1489
- children: /* @__PURE__ */ jsx(IconButton, {
1490
- size: "xs",
1491
- label: `Edit ${label.toLowerCase()}`,
1492
- onClick: () => editing ? close() : openPopover(),
1493
- children: /* @__PURE__ */ jsx(IconSettings, { stroke: 1.5 })
1494
- })
1495
- }), /* @__PURE__ */ jsx(Tooltip, {
1496
- content: `Remove ${label.toLowerCase()}`,
1497
- children: /* @__PURE__ */ jsx(IconButton, {
1498
- size: "xs",
1499
- label: `Remove ${label.toLowerCase()}`,
1500
- onClick: () => ctx.remove(el),
1501
- className: "hover:text-kit-danger-fg",
1502
- children: /* @__PURE__ */ jsx(IconTrash, { stroke: 1.5 })
1503
- })
1504
- })]
1505
- });
1506
- return /* @__PURE__ */ jsxs(Popover, {
1507
- open: editing,
1508
- onOpenChange: (o, d) => {
1509
- if (o) return;
1510
- close();
1511
- if (d?.reason !== "outside-press") focusCard();
1512
- },
1513
- children: [/* @__PURE__ */ jsxs("div", {
1514
- ref: card,
1515
- role: "group",
1516
- tabIndex: 0,
1517
- "aria-label": `${label}${editing ? ", editing" : ""}`,
1518
- "aria-roledescription": "block",
1519
- "aria-haspopup": "dialog",
1520
- "aria-expanded": editing,
1521
- "data-rt-isolate": "",
1522
- "data-article-block": "embed",
1523
- onKeyDown,
1524
- onClick: (e) => {
1525
- if (ro || !e.currentTarget.contains(e.target)) return;
1526
- const t = e.target;
1527
- if (t.closest("button,a,[role=menuitem]")) return;
1528
- openPopover(t.closest("[data-field]")?.getAttribute("data-field") ?? null);
1529
- },
1530
- className: cn("group/art relative rounded-kit-lg outline-none transition-shadow duration-150", !ro && "cursor-pointer", "focus-visible:shadow-[0_0_0_1px_var(--kit-line-strong)]", editing ? "shadow-[0_0_0_2px_var(--kit-accent)] focus-visible:shadow-[0_0_0_2px_var(--kit-accent)]" : !ro && "hover:shadow-[0_0_0_1px_var(--kit-line)]"),
1531
- children: [
1532
- /* @__PURE__ */ jsx(EmbedPreview, {
1533
- m,
1534
- onAdd: () => openPopover("url"),
1535
- ro
1536
- }),
1537
- actions,
1538
- /* @__PURE__ */ jsx(PopoverAnchor, { render: /* @__PURE__ */ jsx("span", {
1539
- "aria-hidden": true,
1540
- className: "pointer-events-none absolute right-2 top-2 size-px"
1541
- }) })
1542
- ]
1543
- }), editing ? /* @__PURE__ */ jsxs(PopoverContent, {
1544
- align: "end",
1545
- side: "bottom",
1546
- className: "w-[340px] p-0",
1547
- initialFocus: false,
1548
- finalFocus: false,
1549
- onKeyDown: (e) => {
1550
- if ((e.metaKey || e.ctrlKey) && !e.altKey && eventKey(e) === "z" && !e.defaultPrevented) {
1551
- e.preventDefault();
1552
- if (e.shiftKey) ctx.redo?.();
1553
- else ctx.undo?.();
1554
- }
1555
- },
1556
- children: [/* @__PURE__ */ jsxs("div", {
1557
- className: "flex h-10 items-center gap-2 border-b border-kit-line-subtle pl-4 pr-1.5",
1558
- children: [/* @__PURE__ */ jsx("span", {
1559
- className: "flex-1 truncate text-kit-sm font-medium text-kit-fg",
1560
- children: label
1561
- }), /* @__PURE__ */ jsx(IconButton, {
1562
- size: "sm",
1563
- label: "Close",
1564
- onClick: () => (close(), focusCard()),
1565
- children: /* @__PURE__ */ jsx(IconX, { stroke: 1.5 })
1566
- })]
1567
- }), /* @__PURE__ */ jsx("div", {
1568
- ref: form,
1569
- className: "max-h-[min(480px,70vh)] overflow-y-auto pb-2",
1570
- children: /* @__PURE__ */ jsxs("div", {
1571
- "data-article-form": "",
1572
- className: "min-w-0",
1573
- children: [/* @__PURE__ */ jsx(SchemaForm, {
1574
- schema: EMBED_SCHEMA,
1575
- value: {
1576
- url: m.url ?? "",
1577
- title: m.title ?? ""
1578
- },
1579
- onChange: (v) => commit({
1580
- ...modelRef.current,
1581
- url: String(v.url ?? ""),
1582
- title: String(v.title ?? "")
1583
- }),
1584
- variant: "panel",
1585
- scroll: false,
1586
- readOnly: ro,
1587
- rootLabel: label,
1588
- env: {
1589
- docId: `article:${EMBED_SCHEMA.name}`,
1590
- docTitle: label
1591
- }
1592
- }), /* @__PURE__ */ jsx("style", { children: `[data-article-form] [data-kit-form] section:first-of-type { border-top-width: 0; margin-top: 0; padding-top: 0; }` })]
1593
- })
1594
- })]
1595
- }) : null]
1596
- });
1597
- }
1598
- /** V2 article-blocks.tsx Empty: a block with nothing to show yet. */
1599
- var ArticleEmpty = ({ icon, title, action, onAction, ro }) => /* @__PURE__ */ jsxs("div", {
1600
- className: "flex items-center gap-3 rounded-kit-lg border border-dashed border-kit-line-strong bg-kit-subtle px-4 py-3.5",
1601
- children: [
1602
- /* @__PURE__ */ jsx("span", {
1603
- className: "flex size-8 shrink-0 items-center justify-center rounded-kit-md border border-kit-line bg-kit-control-bg text-kit-fg-2 [&_svg]:size-4",
1604
- children: icon
1605
- }),
1606
- /* @__PURE__ */ jsx("span", {
1607
- className: "min-w-0 flex-1 text-kit-sm font-medium text-kit-fg",
1608
- children: title
1609
- }),
1610
- ro ? null : /* @__PURE__ */ jsx(Button, {
1611
- size: "sm",
1612
- onClick: onAction,
1613
- children: action
1614
- })
1615
- ]
1616
- });
1617
- var PROVIDER_ICON = {
1618
- youtube: /* @__PURE__ */ jsx(IconBrandYoutube, { stroke: 1.5 }),
1619
- vimeo: /* @__PURE__ */ jsx(IconBrandVimeo, { stroke: 1.5 }),
1620
- x: /* @__PURE__ */ jsx(IconBrandX, { stroke: 1.5 }),
1621
- codepen: /* @__PURE__ */ jsx(IconBrandCodepen, { stroke: 1.5 }),
1622
- figma: /* @__PURE__ */ jsx(IconBrandFigma, { stroke: 1.5 }),
1623
- maps: /* @__PURE__ */ jsx(IconMap, { stroke: 1.5 }),
1624
- spotify: /* @__PURE__ */ jsx(IconBrandSpotify, { stroke: 1.5 }),
1625
- podcast: /* @__PURE__ */ jsx(IconMicrophone, { stroke: 1.5 }),
1626
- audio: /* @__PURE__ */ jsx(IconHeadphones, { stroke: 1.5 }),
1627
- link: /* @__PURE__ */ jsx(IconWorld, { stroke: 1.5 })
1628
- };
1629
- /** V2 EmbedPreview: a faithful placeholder (provider, title and shape), no third-party frame in the editor. Drawn 16:9. */
1630
- function EmbedPreview({ m, onAdd, ro }) {
1631
- if (!m.url) return /* @__PURE__ */ jsx(ArticleEmpty, {
1632
- icon: /* @__PURE__ */ jsx(IconLink, { stroke: 1.5 }),
1633
- title: "Embed",
1634
- action: "Paste a link",
1635
- onAction: onAdd,
1636
- ro
1637
- });
1638
- const info = detectEmbed(m.url);
1639
- const icon = PROVIDER_ICON[info.provider];
1640
- const title = m.title || /* @__PURE__ */ jsx("span", {
1641
- className: "text-kit-warning-fg",
1642
- children: "Add a title"
1643
- });
1644
- if (info.shape === "frame") return /* @__PURE__ */ jsx("figure", {
1645
- className: "m-0",
1646
- "data-field": "url",
1647
- children: /* @__PURE__ */ jsxs("div", {
1648
- className: "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",
1649
- style: { aspectRatio: "16 / 9" },
1650
- children: [
1651
- /* @__PURE__ */ jsx("span", {
1652
- className: "flex size-11 items-center justify-center rounded-full bg-kit-inverse text-kit-fg-inverse [&_svg]:size-5",
1653
- children: info.provider === "youtube" || info.provider === "vimeo" ? /* @__PURE__ */ jsx(IconPlayerPlayFilled, {}) : icon
1654
- }),
1655
- /* @__PURE__ */ jsx("span", {
1656
- "data-field": "title",
1657
- className: "max-w-[80%] truncate text-kit-sm font-medium text-kit-fg",
1658
- children: title
1659
- }),
1660
- /* @__PURE__ */ jsxs("span", {
1661
- className: "absolute left-3 top-2.5 flex items-center gap-1.5 text-kit-xs text-kit-fg-2 [&_svg]:size-3.5",
1662
- children: [icon, info.label]
1663
- })
1664
- ]
1665
- })
1666
- });
1667
- if (info.shape === "audio") return /* @__PURE__ */ jsxs("figure", {
1668
- className: "m-0 flex items-center gap-3 rounded-kit-lg border border-kit-line bg-kit-subtle px-3 py-2.5",
1669
- "data-field": "url",
1670
- children: [
1671
- /* @__PURE__ */ jsx("span", {
1672
- className: "flex size-9 shrink-0 items-center justify-center rounded-full bg-kit-inverse text-kit-fg-inverse [&_svg]:size-4",
1673
- children: /* @__PURE__ */ jsx(IconPlayerPlayFilled, {})
1674
- }),
1675
- /* @__PURE__ */ jsxs("span", {
1676
- className: "min-w-0 flex-1",
1677
- children: [/* @__PURE__ */ jsx("span", {
1678
- "data-field": "title",
1679
- className: "block truncate text-kit-sm font-medium text-kit-fg",
1680
- children: title
1681
- }), /* @__PURE__ */ jsxs("span", {
1682
- className: "flex items-center gap-1 text-kit-xs text-kit-fg-3 [&_svg]:size-3",
1683
- children: [icon, info.label]
1684
- })]
1685
- }),
1686
- /* @__PURE__ */ jsx("span", {
1687
- "aria-hidden": true,
1688
- className: "hidden h-1 w-28 rounded-full bg-kit-line-strong sm:block"
1689
- })
1690
- ]
1691
- });
1692
- return /* @__PURE__ */ jsxs("figure", {
1693
- className: "m-0 flex items-center gap-3 rounded-kit-lg border border-kit-line bg-kit-surface px-3 py-2.5",
1694
- "data-field": "url",
1695
- children: [/* @__PURE__ */ jsx("span", {
1696
- className: "flex size-9 shrink-0 items-center justify-center rounded-kit-md bg-kit-muted text-kit-fg-2 [&_svg]:size-4",
1697
- children: icon
1698
- }), /* @__PURE__ */ jsxs("span", {
1699
- className: "min-w-0 flex-1",
1700
- children: [/* @__PURE__ */ jsx("span", {
1701
- "data-field": "title",
1702
- className: "block truncate text-kit-sm font-medium text-kit-fg",
1703
- children: title
1704
- }), /* @__PURE__ */ jsx("span", {
1705
- className: "block truncate text-kit-xs text-kit-fg-3",
1706
- children: info.shape === "post" ? `${info.label} · ${m.url.replace(/^https?:\/\/(www\.)?/, "")}` : m.url.replace(/^https?:\/\/(www\.)?/, "")
1707
- })]
1448
+ }), /* @__PURE__ */ jsx(ArticleActions, {
1449
+ label,
1450
+ ctx,
1451
+ el
1708
1452
  })]
1709
1453
  });
1710
1454
  }
1711
- function ArticlePanel({ label, anchor, nonce, focusKey, onClose, focusBack, ctx, el, children }) {
1712
- const panel = React.useRef(null);
1713
- const titleId = React.useId();
1714
- const container = anchor?.closest("[data-expanded-editor]") ?? null;
1715
- React.useEffect(() => {
1716
- const timers = [];
1717
- let landed = false;
1718
- for (const ms of [
1719
- 40,
1720
- 90,
1721
- 160,
1722
- 260,
1723
- 420,
1724
- 640
1725
- ]) timers.push(window.setTimeout(() => {
1726
- const root = panel.current?.el() ?? null;
1727
- if (!root?.querySelector("[data-field-key]")) return;
1728
- const a = document.activeElement;
1729
- const lost = !a || a === document.body || a.hasAttribute("data-expanded-editor") || a.hasAttribute("data-article-block");
1730
- if (root.contains(a)) return void (landed = true);
1731
- if (!landed || lost) focusField(root, focusKey);
1732
- if (root.contains(document.activeElement)) landed = true;
1733
- }, ms));
1734
- return () => timers.forEach((t) => window.clearTimeout(t));
1735
- }, [nonce]);
1736
- React.useEffect(() => panel.current?.announce(`${label} properties.`), [label]);
1737
- const header = /* @__PURE__ */ jsxs(Fragment, { children: [
1738
- /* @__PURE__ */ jsx("span", {
1739
- id: titleId,
1740
- className: "min-w-0 flex-1 truncate text-kit-sm font-medium text-kit-fg",
1741
- children: label
1742
- }),
1743
- /* @__PURE__ */ jsxs(Menu, { children: [/* @__PURE__ */ jsx(MenuTrigger, { render: /* @__PURE__ */ jsx(IconButton, {
1744
- size: "sm",
1745
- label: `${label} actions`,
1746
- children: /* @__PURE__ */ jsx(IconDots, { stroke: 1.5 })
1747
- }) }), /* @__PURE__ */ jsxs(MenuContent, {
1748
- align: "end",
1749
- className: "w-52",
1750
- children: [
1751
- ctx.readOnly || !ctx.duplicate ? null : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(MenuItem, {
1752
- icon: /* @__PURE__ */ jsx(IconCopy, { stroke: 1.5 }),
1753
- onClick: () => ctx.duplicate?.(el),
1754
- children: "Duplicate"
1755
- }), /* @__PURE__ */ jsx(MenuSeparator, {})] }),
1756
- /* @__PURE__ */ jsx(MenuItem, {
1757
- icon: /* @__PURE__ */ jsx(IconArrowsMove, { stroke: 1.5 }),
1758
- onClick: () => panel.current?.keyboard("move"),
1759
- children: "Move panel"
1760
- }),
1761
- /* @__PURE__ */ jsx(MenuItem, {
1762
- icon: /* @__PURE__ */ jsx(IconDimensions, { stroke: 1.5 }),
1763
- onClick: () => panel.current?.keyboard("resize"),
1764
- children: "Resize panel"
1765
- }),
1766
- /* @__PURE__ */ jsx(MenuItem, {
1767
- icon: /* @__PURE__ */ jsx(IconLayoutSidebar, { stroke: 1.5 }),
1768
- onClick: () => panel.current?.dock("left"),
1769
- children: "Dock left"
1770
- }),
1771
- /* @__PURE__ */ jsx(MenuItem, {
1772
- icon: /* @__PURE__ */ jsx(IconLayoutSidebarRight, { stroke: 1.5 }),
1773
- onClick: () => panel.current?.dock("right"),
1774
- children: "Dock right"
1775
- }),
1776
- ctx.readOnly ? null : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(MenuSeparator, {}), /* @__PURE__ */ jsx(MenuItem, {
1777
- danger: true,
1778
- icon: /* @__PURE__ */ jsx(IconTrash, { stroke: 1.5 }),
1779
- onClick: () => ctx.remove(el),
1780
- children: "Remove"
1781
- })] })
1782
- ]
1783
- })] }),
1784
- /* @__PURE__ */ jsx(IconButton, {
1785
- size: "sm",
1786
- label: "Close properties",
1787
- tooltip: "Close",
1788
- onClick: () => (onClose(), focusBack()),
1789
- children: /* @__PURE__ */ jsx(IconX, { stroke: 1.5 })
1790
- })
1791
- ] });
1792
- return /* @__PURE__ */ jsx(FloatingPanel, {
1793
- ref: panel,
1794
- id: "article-properties",
1795
- labelledBy: titleId,
1796
- anchor,
1797
- container,
1798
- header,
1799
- onClose,
1800
- focusBack,
1801
- onPanelKeyDown: (e) => {
1802
- if ((e.metaKey || e.ctrlKey) && !e.altKey && eventKey(e) === "z" && !e.defaultPrevented) {
1803
- e.preventDefault();
1804
- if (e.shiftKey) ctx.redo?.();
1805
- else ctx.undo?.();
1806
- }
1807
- },
1808
- sheetFooter: /* @__PURE__ */ jsx(Button, {
1809
- size: "sm",
1810
- variant: "primary",
1811
- onClick: () => (onClose(), focusBack()),
1812
- children: "Done"
1813
- }),
1814
- children: /* @__PURE__ */ jsx("div", {
1815
- className: "pb-3 pt-1",
1816
- children
1817
- })
1818
- });
1819
- }
1820
1455
  function PullQuotePreview({ m }) {
1821
1456
  return /* @__PURE__ */ jsxs("figure", {
1822
1457
  className: "m-0 py-2 pl-5 pr-10",