docblocks-react-ui 0.3.1 → 0.4.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.
@@ -1,4 +1,4 @@
1
- import { a as isSafeDownloadHref, c as cn, i as clamp, n as TriangleAlert, o as preventDefault, r as assertNever, s as stopPropagation, t as isEmbeddablePdfUrl } from "./constants-D4FTL0ph.mjs";
1
+ import { a as ChevronLeft, c as isSafeDownloadHref, d as cn, i as ChevronRight, l as preventDefault, n as X, o as assertNever, r as TriangleAlert, s as clamp, t as isEmbeddablePdfUrl, u as stopPropagation } from "./constants-BaAj3jDA.mjs";
2
2
  import { useStore } from "zustand";
3
3
  import * as React from "react";
4
4
  import { Component, Fragment, createContext, memo, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
@@ -2023,40 +2023,6 @@ function ChevronDown(props) {
2023
2023
  });
2024
2024
  }
2025
2025
  //#endregion
2026
- //#region src/shared/ui/icons/ChevronLeft.tsx
2027
- function ChevronLeft(props) {
2028
- return /* @__PURE__ */ jsx("svg", {
2029
- xmlns: "http://www.w3.org/2000/svg",
2030
- width: 24,
2031
- height: 24,
2032
- viewBox: "0 0 24 24",
2033
- fill: "none",
2034
- stroke: "currentColor",
2035
- strokeWidth: 2,
2036
- strokeLinecap: "round",
2037
- strokeLinejoin: "round",
2038
- ...props,
2039
- children: /* @__PURE__ */ jsx("path", { d: "m15 18-6-6 6-6" })
2040
- });
2041
- }
2042
- //#endregion
2043
- //#region src/shared/ui/icons/ChevronRight.tsx
2044
- function ChevronRight(props) {
2045
- return /* @__PURE__ */ jsx("svg", {
2046
- xmlns: "http://www.w3.org/2000/svg",
2047
- width: 24,
2048
- height: 24,
2049
- viewBox: "0 0 24 24",
2050
- fill: "none",
2051
- stroke: "currentColor",
2052
- strokeWidth: 2,
2053
- strokeLinecap: "round",
2054
- strokeLinejoin: "round",
2055
- ...props,
2056
- children: /* @__PURE__ */ jsx("path", { d: "m9 18 6-6-6-6" })
2057
- });
2058
- }
2059
- //#endregion
2060
2026
  //#region src/shared/ui/icons/ChevronUp.tsx
2061
2027
  function ChevronUp(props) {
2062
2028
  return /* @__PURE__ */ jsx("svg", {
@@ -3767,23 +3733,6 @@ function Underline(props) {
3767
3733
  });
3768
3734
  }
3769
3735
  //#endregion
3770
- //#region src/shared/ui/icons/X.tsx
3771
- function X(props) {
3772
- return /* @__PURE__ */ jsxs("svg", {
3773
- xmlns: "http://www.w3.org/2000/svg",
3774
- width: 24,
3775
- height: 24,
3776
- viewBox: "0 0 24 24",
3777
- fill: "none",
3778
- stroke: "currentColor",
3779
- strokeWidth: 2,
3780
- strokeLinecap: "round",
3781
- strokeLinejoin: "round",
3782
- ...props,
3783
- children: [/* @__PURE__ */ jsx("path", { d: "M18 6 6 18" }), /* @__PURE__ */ jsx("path", { d: "m6 6 12 12" })]
3784
- });
3785
- }
3786
- //#endregion
3787
3736
  //#region src/shared/ui/icons/Zap.tsx
3788
3737
  function Zap(props) {
3789
3738
  return /* @__PURE__ */ jsx("svg", {
@@ -4137,7 +4086,11 @@ var blocks_default$1 = {
4137
4086
  "wrap": "Wrap long lines",
4138
4087
  "rendering": "rendering…",
4139
4088
  "pdfPreviewUnavailable": "PDF preview unavailable ({error}). Showing the built-in viewer.",
4140
- "pdfLoading": "loading preview…"
4089
+ "pdfLoading": "loading preview…",
4090
+ "pdfPrevPage": "Previous page",
4091
+ "pdfNextPage": "Next page",
4092
+ "pdfFullscreen": "Expand to fullscreen",
4093
+ "pdfCloseFullscreen": "Close fullscreen"
4141
4094
  },
4142
4095
  image: {
4143
4096
  "placeholder": "image",
@@ -4149,7 +4102,13 @@ var blocks_default$1 = {
4149
4102
  embed: {
4150
4103
  "zoomIn": "Zoom in",
4151
4104
  "zoomOut": "Zoom out",
4152
- "zoomReset": "Reset zoom"
4105
+ "zoomReset": "Reset zoom",
4106
+ "resizeHeight": "Resize embed height",
4107
+ "editUrl": "Edit URL",
4108
+ "saveUrl": "Save URL",
4109
+ "cancelEdit": "Cancel",
4110
+ "srcPlaceholder": "Embed URL (https://…)",
4111
+ "unsafeHint": "Only absolute http(s):// URLs are supported."
4153
4112
  },
4154
4113
  syncEmbed: {
4155
4114
  "broken": "The master document for this synced block is unavailable.",
@@ -4479,7 +4438,7 @@ var slash_default$1 = {
4479
4438
  "code": "Code block",
4480
4439
  "codeMermaid": "Code block (mermaid)",
4481
4440
  "divider": "Divider",
4482
- "image": "Image",
4441
+ "file": "File",
4483
4442
  "embed": "Embed",
4484
4443
  "database": "Database",
4485
4444
  "frame": "Frame",
@@ -4507,7 +4466,7 @@ var slash_default$1 = {
4507
4466
  },
4508
4467
  embed: {
4509
4468
  "urlPlaceholder": "Embed URL (https://…)",
4510
- "unsafeHint": "Only absolute https://… (the html kind stands the data:text/html snippet)."
4469
+ "unsafeHint": "Only absolute http(s):// URLs are supported."
4511
4470
  }
4512
4471
  };
4513
4472
  //#endregion
@@ -4579,7 +4538,11 @@ var blocks_default = {
4579
4538
  "wrap": "Переносить длинные строки",
4580
4539
  "rendering": "Рисование…",
4581
4540
  "pdfPreviewUnavailable": "Превью PDF недоступно ({error}). Показан встроенный просмотрщик.",
4582
- "pdfLoading": "Загрузка превью…"
4541
+ "pdfLoading": "Загрузка превью…",
4542
+ "pdfPrevPage": "Предыдущая страница",
4543
+ "pdfNextPage": "Следующая страница",
4544
+ "pdfFullscreen": "Развернуть на весь экран",
4545
+ "pdfCloseFullscreen": "Закрыть полноэкранный режим"
4583
4546
  },
4584
4547
  image: {
4585
4548
  "placeholder": "изображение",
@@ -4591,7 +4554,13 @@ var blocks_default = {
4591
4554
  embed: {
4592
4555
  "zoomIn": "Приблизить",
4593
4556
  "zoomOut": "Отдалить",
4594
- "zoomReset": "Сбросить масштаб"
4557
+ "zoomReset": "Сбросить масштаб",
4558
+ "resizeHeight": "Изменить высоту встраивания",
4559
+ "editUrl": "Изменить ссылку",
4560
+ "saveUrl": "Сохранить ссылку",
4561
+ "cancelEdit": "Отмена",
4562
+ "srcPlaceholder": "URL встраивания (https://…)",
4563
+ "unsafeHint": "Поддерживаются только абсолютные URL http(s)://."
4595
4564
  },
4596
4565
  syncEmbed: {
4597
4566
  "broken": "Мастер-документ для этого синхронизируемого блока недоступен.",
@@ -4921,7 +4890,7 @@ var slash_default = {
4921
4890
  "code": "Блок кода",
4922
4891
  "codeMermaid": "Блок кода (mermaid)",
4923
4892
  "divider": "Разделитель",
4924
- "image": "Изображение",
4893
+ "file": "Файл",
4925
4894
  "embed": "Встраивание",
4926
4895
  "database": "База данных",
4927
4896
  "frame": "Фрейм",
@@ -4949,7 +4918,7 @@ var slash_default = {
4949
4918
  },
4950
4919
  embed: {
4951
4920
  "urlPlaceholder": "URL встраивания (https://…)",
4952
- "unsafeHint": "Только абсолютный https://… (вид html принимает сниппет data:text/html)."
4921
+ "unsafeHint": "Поддерживаются только абсолютные URL http(s)://."
4953
4922
  }
4954
4923
  };
4955
4924
  //#endregion
@@ -9575,6 +9544,7 @@ function GridBody({ blockId, bridge, tableId, filter, columnOrder, hiddenColumns
9575
9544
  "data-slot": "docblocks/block",
9576
9545
  "data-block-type": "database",
9577
9546
  "data-block-id": blockId,
9547
+ className: "min-h-0 flex-1",
9578
9548
  children: /* @__PURE__ */ jsx(CardContent, {
9579
9549
  "data-slot": "docblocks/database",
9580
9550
  "data-state": "loading",
@@ -9590,7 +9560,7 @@ function GridBody({ blockId, bridge, tableId, filter, columnOrder, hiddenColumns
9590
9560
  "data-slot": "docblocks/block",
9591
9561
  "data-block-type": "database",
9592
9562
  "data-block-id": blockId,
9593
- className: "\n my-(--docblocks-block-spacing) rounded-(--docblocks-radius) border-(length:--docblocks-table-border-width)\n border-(--docblocks-table-border-color) outline-none\n ",
9563
+ className: "\n my-(--docblocks-block-spacing) min-h-0 flex-1 rounded-(--docblocks-radius)\n border-(length:--docblocks-table-border-width) border-(--docblocks-table-border-color) outline-none\n ",
9594
9564
  children: /* @__PURE__ */ jsxs(CardContent, {
9595
9565
  "data-slot": "docblocks/database",
9596
9566
  "data-state": "broken",
@@ -9682,10 +9652,10 @@ function GridBody({ blockId, bridge, tableId, filter, columnOrder, hiddenColumns
9682
9652
  "data-block-type": "database",
9683
9653
  "data-block-id": blockId,
9684
9654
  "data-state": "ready",
9685
- className: "\n my-(--docblocks-block-spacing) rounded-(--docblocks-radius) border-(length:--docblocks-table-border-width)\n border-(--docblocks-table-border-color) outline-none\n ",
9655
+ className: "\n my-(--docblocks-block-spacing) flex min-h-0 flex-1 flex-col rounded-(--docblocks-radius)\n border-(length:--docblocks-table-border-width) border-(--docblocks-table-border-color) outline-none\n ",
9686
9656
  children: /* @__PURE__ */ jsxs(CardContent, {
9687
9657
  "data-slot": "docblocks/database",
9688
- className: "flex flex-col gap-(--docblocks-block-spacing) p-(--docblocks-block-spacing)",
9658
+ className: "flex min-h-0 flex-1 flex-col gap-(--docblocks-block-spacing) p-(--docblocks-block-spacing)",
9689
9659
  children: [
9690
9660
  /* @__PURE__ */ jsxs("div", {
9691
9661
  "data-slot": "docblocks/database-toolbar",
@@ -9792,6 +9762,7 @@ function GridBody({ blockId, bridge, tableId, filter, columnOrder, hiddenColumns
9792
9762
  }),
9793
9763
  /* @__PURE__ */ jsxs(ScrollArea, {
9794
9764
  "data-slot": "docblocks/database-grid",
9765
+ className: "min-h-0 flex-1",
9795
9766
  children: [/* @__PURE__ */ jsxs("div", {
9796
9767
  ref: tableRef,
9797
9768
  "data-slot": "docblocks/database-table",
@@ -10393,22 +10364,16 @@ function FrameSlashPicker({ onCommit, onCancel }) {
10393
10364
  //#endregion
10394
10365
  //#region src/editing/slash/SlashEmbedForm.tsx
10395
10366
  /**
10396
- * The slash `Embed` form (the embed-creation affordance):
10397
- * a URL input (auto-focused) + a kind selector (default `iframe` — all kinds
10398
- * render as an iframe in v1; the kind is a renderer hint). The menu stands
10399
- * open over the phrase while the form stands; the commit (the URL's Enter, or
10400
- * the check) lands the phrase strip + the `insertBlock('embed', { kind, src })`
10401
- * in the caller's transact. An empty (or whitespace-only) URL commits nothing
10402
- * (the menu stands open on the phrase — the Image / Database contract).
10403
- */
10404
- /** The embed's kind catalog (the core `EmbedAttrs.kind` enum). */
10405
- const EMBED_KINDS = [
10406
- "iframe",
10407
- "github",
10408
- "figma",
10409
- "loom",
10410
- "html"
10411
- ];
10367
+ * The slash `Embed` form (the embed-creation affordance): a URL input
10368
+ * (auto-focused) only — the kind stands the `iframe` default (the renderer's
10369
+ * hint; all kinds render as an iframe). The menu stands open over the phrase
10370
+ * while the form stands; the commit (the URL's Enter, or the check) lands the
10371
+ * phrase strip + the `insertBlock('embed', { kind: "iframe", src })` in the
10372
+ * caller's transact. An empty (or whitespace-only) URL commits nothing (the
10373
+ * menu stands open on the phrase — the Image / Database contract).
10374
+ */
10375
+ /** The embed's kind (the renderer's hint — the form stands the URL only). */
10376
+ const EMBED_KIND_IFRAME = "iframe";
10412
10377
  /** The form's i18n scope: the package's instance stands the provider's seat
10413
10378
  * over the form (the form stands also outside the editor's scope). */
10414
10379
  function SlashEmbedForm(props) {
@@ -10418,7 +10383,6 @@ function SlashEmbedForm(props) {
10418
10383
  function SlashEmbedFormInner({ onCommit, onCancel }) {
10419
10384
  const { t } = useTranslation("slash");
10420
10385
  const [url, setUrl] = useState("");
10421
- const [kind, setKind] = useState("iframe");
10422
10386
  const urlInputRef = useRef(null);
10423
10387
  useEffect(() => {
10424
10388
  urlInputRef.current?.focus();
@@ -10426,18 +10390,15 @@ function SlashEmbedFormInner({ onCommit, onCancel }) {
10426
10390
  const commit = () => {
10427
10391
  const trimmed = url.trim();
10428
10392
  if (trimmed.length === 0) return;
10429
- if (!isSafeEmbedSrc(kind, trimmed)) return;
10430
- onCommit({
10431
- kind,
10432
- src: trimmed
10433
- });
10393
+ if (!isSafeEmbedSrc(EMBED_KIND_IFRAME, trimmed)) return;
10394
+ onCommit(trimmed);
10434
10395
  };
10435
10396
  const stopFormKey = (event) => {
10436
10397
  event.stopPropagation();
10437
10398
  if (event.key === "Escape") onCancel();
10438
10399
  };
10439
- const canCommit = url.trim().length > 0 && isSafeEmbedSrc(kind, url.trim());
10440
- const unsafeHint = url.trim().length > 0 && !isSafeEmbedSrc(kind, url.trim()) ? t("embed.unsafeHint") : null;
10400
+ const canCommit = url.trim().length > 0 && isSafeEmbedSrc(EMBED_KIND_IFRAME, url.trim());
10401
+ const unsafeHint = url.trim().length > 0 && !isSafeEmbedSrc(EMBED_KIND_IFRAME, url.trim()) ? t("embed.unsafeHint") : null;
10441
10402
  const onUrlKeyDown = (event) => {
10442
10403
  stopFormKey(event);
10443
10404
  if (event.key === "Enter") {
@@ -10446,51 +10407,36 @@ function SlashEmbedFormInner({ onCommit, onCancel }) {
10446
10407
  }
10447
10408
  };
10448
10409
  const onUrlChange = (event) => setUrl(event.target.value);
10449
- const onKindChange = (event) => setKind(event.target.value);
10450
10410
  return /* @__PURE__ */ jsxs("div", {
10451
10411
  "data-slot": "docblocks/slash-embed-form",
10452
10412
  className: "flex w-56 flex-col gap-2 p-1",
10453
- children: [
10454
- /* @__PURE__ */ jsx(Input, {
10413
+ children: [/* @__PURE__ */ jsxs("div", {
10414
+ className: "flex items-center gap-1",
10415
+ children: [/* @__PURE__ */ jsx(Input, {
10455
10416
  size: "sm",
10456
10417
  ref: urlInputRef,
10457
10418
  type: "text",
10458
10419
  "data-slot": "docblocks/slash-embed-url",
10459
- className: "rounded-sm",
10420
+ className: "h-7 flex-1 rounded-sm",
10460
10421
  placeholder: t("embed.urlPlaceholder"),
10461
10422
  value: url,
10462
10423
  onKeyDown: onUrlKeyDown,
10463
10424
  onChange: onUrlChange
10464
- }),
10465
- /* @__PURE__ */ jsxs("div", {
10466
- className: "flex items-center gap-1",
10467
- children: [/* @__PURE__ */ jsx(BareSelect, {
10468
- "data-slot": "docblocks/slash-embed-kind",
10469
- className: "h-7 min-w-0 flex-1 cursor-default rounded-sm px-1",
10470
- value: kind,
10471
- onKeyDown: stopFormKey,
10472
- onChange: onKindChange,
10473
- children: EMBED_KINDS.map((value) => /* @__PURE__ */ jsx("option", {
10474
- value,
10475
- children: value
10476
- }, value))
10477
- }), /* @__PURE__ */ jsx(Button, {
10478
- "data-slot": "docblocks/slash-embed-insert",
10479
- variant: "outline",
10480
- size: "icon",
10481
- className: "size-7 cursor-default rounded-sm bg-transparent disabled:cursor-default dark:bg-transparent",
10482
- disabled: !canCommit,
10483
- onMouseDown: preventDefault,
10484
- onClick: commit,
10485
- children: /* @__PURE__ */ jsx(Check, { className: "size-4" })
10486
- })]
10487
- }),
10488
- unsafeHint !== null && /* @__PURE__ */ jsx("p", {
10489
- "data-slot": "docblocks/slash-embed-hint",
10490
- className: "px-1 text-xs text-destructive",
10491
- children: unsafeHint
10492
- })
10493
- ]
10425
+ }), /* @__PURE__ */ jsx(Button, {
10426
+ "data-slot": "docblocks/slash-embed-insert",
10427
+ variant: "outline",
10428
+ size: "icon",
10429
+ className: "\n size-7 shrink-0 cursor-default rounded-sm bg-transparent\n disabled:cursor-default\n dark:bg-transparent\n ",
10430
+ disabled: !canCommit,
10431
+ onMouseDown: preventDefault,
10432
+ onClick: commit,
10433
+ children: /* @__PURE__ */ jsx(Check, { className: "size-4" })
10434
+ })]
10435
+ }), unsafeHint !== null && /* @__PURE__ */ jsx("p", {
10436
+ "data-slot": "docblocks/slash-embed-hint",
10437
+ className: "px-1 text-xs text-destructive",
10438
+ children: unsafeHint
10439
+ })]
10494
10440
  });
10495
10441
  }
10496
10442
  /** The v1 command list (filterable by the typed query). The `label` is the
@@ -10577,8 +10523,8 @@ const SLASH_COMMANDS = [
10577
10523
  type: "divider"
10578
10524
  },
10579
10525
  {
10580
- label: "Image",
10581
- labelKey: "blocks.image",
10526
+ label: "File",
10527
+ labelKey: "blocks.file",
10582
10528
  icon: Image,
10583
10529
  type: "image"
10584
10530
  },
@@ -11263,7 +11209,7 @@ function useSlashMenu(rootRef) {
11263
11209
  doc,
11264
11210
  rootRef
11265
11211
  ]);
11266
- const commitEmbed = useCallback((input) => {
11212
+ const commitEmbed = useCallback((src) => {
11267
11213
  const pending = pendingEmbedRef.current;
11268
11214
  pendingEmbedRef.current = null;
11269
11215
  setEmbedForm(false);
@@ -11284,8 +11230,8 @@ function useSlashMenu(rootRef) {
11284
11230
  doc.deleteText(blockId, slashOffset, queryEnd);
11285
11231
  }
11286
11232
  const embedId = doc.insertBlock("embed", {
11287
- kind: input.kind,
11288
- src: input.src,
11233
+ kind: "iframe",
11234
+ src,
11289
11235
  ...ind
11290
11236
  }, {
11291
11237
  parent: parentId,
@@ -11897,15 +11843,18 @@ function NotPublishedPlaceholder() {
11897
11843
  //#endregion
11898
11844
  //#region src/editor-provider/constants.ts
11899
11845
  /**
11900
- * The safe embed sandbox (the default when no `embedSandbox` policy is
11901
- * supplied): the embedded content stands an opaque origin (no
11902
- * `allow-same-origin`), so it cannot read the parent's DOM, cookies, or state;
11903
- * scripts, forms, and popups still run, so interactive embeds keep working.
11904
- * Grant `allow-same-origin` — and `allow-scripts` together with it — only for
11905
- * trusted origins via the `embedSandbox` policy: that combination lets the
11906
- * frame lift its own sandbox.
11907
- */
11908
- const DEFAULT_EMBED_SANDBOX = "allow-scripts allow-forms allow-popups";
11846
+ * The embed sandbox (the default when no `embedSandbox` policy is supplied):
11847
+ * the embedded content runs with its own origin's grants (`allow-same-origin`
11848
+ * — the real-app requirement: a dashboard's own storage / cookies stand,
11849
+ * so the app boots like in a plain tab) + scripts, forms, and popups.
11850
+ * The frame's document stands a separate document either way: a
11851
+ * cross-origin embed cannot reach the parent's DOM or state. The one
11852
+ * posture consequence: a same-origin URL (the page's own origin) reaches
11853
+ * the parent over `window.parent` — the document author's own choice to
11854
+ * embed it. The `embedSandbox` policy stands the restriction (the
11855
+ * opaque-origin stand — no `allow-same-origin` — for the untrusted embeds).
11856
+ */
11857
+ const DEFAULT_EMBED_SANDBOX = "allow-scripts allow-same-origin allow-forms allow-popups";
11909
11858
  const defaultEmbedSandbox = () => DEFAULT_EMBED_SANDBOX;
11910
11859
  //#endregion
11911
11860
  //#region src/editor-provider/context.ts
@@ -12738,20 +12687,34 @@ function DatabaseBlock({ id, block }) {
12738
12687
  * only (all kinds render as an iframe; per-kind card renderers come later),
12739
12688
  * so an unknown kind (newer data) still renders the same iframe. `src` is a
12740
12689
  * URL, not a blob key: the consumer's blob resolver does not apply. The
12741
- * iframe stands a `sandbox` attribute — the safe default (no
12742
- * `allow-same-origin`, so the embedded content is an opaque origin and cannot
12743
- * read the parent's DOM / state; scripts, forms, and popups still run) or the
12744
- * consumer's `embedSandbox` policy (grant `allow-same-origin` only for trusted
12745
- * origins). The block is not text-editable, so the wrapper carries no gutter
12690
+ * iframe stands a `sandbox` attribute — the default grants the same-origin
12691
+ * (the embedded app's own storage / cookies stand, so the real apps boot
12692
+ * like in a plain tab) or the consumer's `embedSandbox` policy (the
12693
+ * opaque-origin's restriction over the untrusted, `null` for the fully
12694
+ * trusted). The block is not text-editable, so the wrapper carries no gutter
12746
12695
  * handle.
12747
12696
  *
12748
- * Pan/zoom (view state): the iframe stands a CSS
12697
+ * Height (the stretched stand): the wrapper's height stands the
12698
+ * `--docblocks-embed-height` token (the default — the minimum). The
12699
+ * bottom-edge resize handle (the hover reveal, the keyboard's arrows)
12700
+ * stretches it over the minimum; the stretched px stand the block's
12701
+ * `height` attr (the null idiom: null / absent = the token's default), so
12702
+ * the reload restores it and the panes share it over the wire (the commit
12703
+ * stands the gesture's rest — one write per drag, one per key press).
12704
+ *
12705
+ * Pan/zoom (the persisted view): the iframe stands a CSS
12749
12706
  * `translate(x, y) scale(s)` transform (the wrapper clips the overflow). The
12750
- * transform is PURE view state — it is not document data, so `toJSON()`, the
12751
- * wire, and the clipboard never see it (each editor instance owns its own
12752
- * view; the two panes pan/zoom independently). The controls:
12707
+ * view stands the block's `zoom` / `panX` / `panY` attrs (the null idiom:
12708
+ * null / absent = the identity) — the reload restores the view and the wire
12709
+ * carries it over the panes (the panes share the persisted view; a pane's
12710
+ * gesture writes over the doc, the others adopt it). The write is debounced
12711
+ * over the gesture's bursts (one doc write per rest). The controls:
12753
12712
  *
12754
12713
  * - the hover toolbar (zoom in / out, the percent, reset) — always stands;
12714
+ * - the toolbar's URL edit (the editing mode's stand): the panel over the
12715
+ * toolbar changes the `src` (the safe non-empty's write — the unsafe's
12716
+ * rejection over the slash form's rule; the iframe's navigation over the
12717
+ * new src, the view's / the height's attrs stand untouched);
12755
12718
  * - the wheel — `ctrl`/`cmd` zooms at the cursor, a plain wheel pans
12756
12719
  * vertically (content opposite to the delta, the canvas idiom);
12757
12720
  * - the modifier drag — a `space` / `alt`-held pointer drag pans (the iframe
@@ -12771,12 +12734,36 @@ const EMBED_MAX_SCALE = 4;
12771
12734
  const EMBED_ZOOM_STEP = 1.2;
12772
12735
  /** The wheel's continuous zoom factor (the exponential of the delta, the canvas pinch idiom). */
12773
12736
  const EMBED_WHEEL_ZOOM = .002;
12737
+ /** The view's persist debounce (the gesture's bursts stand one doc write per rest). */
12738
+ const EMBED_PERSIST_DEBOUNCE_MS = 250;
12739
+ /** The height's fallback minimum (px): the `--docblocks-embed-height` token's default (the non-CSS environment's stand — jsdom). */
12740
+ const EMBED_MIN_HEIGHT = 300;
12741
+ /** The resize handle's key step (px per arrow press). */
12742
+ const EMBED_RESIZE_KEY_STEP = 16;
12774
12743
  /** The identity view: the iframe at its natural size, filling the wrapper. */
12775
12744
  const EMBED_VIEW_IDENTITY = {
12776
12745
  scale: 1,
12777
12746
  x: 0,
12778
12747
  y: 0
12779
12748
  };
12749
+ /** The view over the block's persisted attrs (the null idiom's restore). */
12750
+ function viewOfAttrs(zoom, panX, panY) {
12751
+ return {
12752
+ scale: zoom ?? 1,
12753
+ x: panX ?? 0,
12754
+ y: panY ?? 0
12755
+ };
12756
+ }
12757
+ /**
12758
+ * The view's wire stand: the gesture's floating-point's residue must not
12759
+ * stand a non-identity's persist (the zoom-out/in's round-trip lands the
12760
+ * 0.9999…'s "identity"). The zoom's round to the label's 1%'s precision,
12761
+ * the pan's to the 0.1 px (the sub-pixel's residue stands the identity).
12762
+ */
12763
+ function roundViewValue(value, places) {
12764
+ const factor = 10 ** places;
12765
+ return Math.round(value * factor) / factor;
12766
+ }
12780
12767
  /**
12781
12768
  * Clamp the pan against the scaled embed's overflow. The embed cannot be
12782
12769
  * dragged past the container: at `scale <= 1` it fits (or is smaller) and
@@ -12802,17 +12789,61 @@ function zoomAtPoint(view, cx, cy, nextScale) {
12802
12789
  y: cy - (cy - view.y) * factor
12803
12790
  };
12804
12791
  }
12792
+ /**
12793
+ * The height's wire stand: the token's minimum over the wrapper (the
12794
+ * themed token's stand), the fallback's default over the non-CSS's stand
12795
+ * (jsdom's empty computed value).
12796
+ */
12797
+ function readEmbedMinHeight(wrapper) {
12798
+ const raw = getComputedStyle(wrapper).getPropertyValue("--docblocks-embed-height");
12799
+ const value = Number.parseFloat(raw);
12800
+ return Number.isFinite(value) && value > 0 ? value : EMBED_MIN_HEIGHT;
12801
+ }
12805
12802
  function EmbedBlock({ id, block }) {
12806
12803
  const { t } = useTranslation("blocks");
12807
12804
  const { kind, src, title } = block.attrs;
12808
- const { embedSandbox } = useEditorContext();
12805
+ const { doc, embedSandbox, viewMode } = useEditorContext();
12809
12806
  const sandbox = embedSandbox(src);
12810
12807
  const wrapperRef = useRef(null);
12811
- const [view, setView] = useState(EMBED_VIEW_IDENTITY);
12808
+ const [view, setView] = useState(() => viewOfAttrs(block.attrs.zoom, block.attrs.panX, block.attrs.panY));
12812
12809
  const viewRef = useRef(view);
12813
12810
  viewRef.current = view;
12811
+ const [height, setHeight] = useState(block.attrs.height ?? null);
12812
+ const heightRef = useRef(height);
12813
+ heightRef.current = height;
12814
+ const [tokenMin, setTokenMin] = useState(EMBED_MIN_HEIGHT);
12815
+ const resizeSession = useRef(null);
12814
12816
  const [panModifier, setPanModifier] = useState(false);
12815
12817
  const panSession = useRef(null);
12818
+ const [editOpen, setEditOpen] = useState(false);
12819
+ const [draft, setDraft] = useState(src);
12820
+ const editInputRef = useRef(null);
12821
+ /** The edit's open: the draft stands the block's own src (the cancel's restore). */
12822
+ const openEdit = () => {
12823
+ setDraft(src);
12824
+ setEditOpen(true);
12825
+ };
12826
+ const closeEdit = () => setEditOpen(false);
12827
+ useEffect(() => {
12828
+ if (editOpen) editInputRef.current?.focus();
12829
+ }, [editOpen]);
12830
+ const trimmedDraft = draft.trim();
12831
+ const canSave = trimmedDraft.length > 0 && isSafeEmbedSrc(kind, trimmedDraft);
12832
+ const editHint = trimmedDraft.length > 0 && !isSafeEmbedSrc(kind, trimmedDraft) ? t("embed.unsafeHint") : null;
12833
+ /** The edit's commit: the safe non-empty draft stands the `src`'s write.
12834
+ * The unchanged draft writes nothing (no updatedAt's stamp, no wire update). */
12835
+ const commitEdit = () => {
12836
+ if (!canSave) return;
12837
+ if (trimmedDraft !== src) doc.updateBlock(id, { src: trimmedDraft });
12838
+ setEditOpen(false);
12839
+ };
12840
+ const onEditKeyDown = (event) => {
12841
+ event.stopPropagation();
12842
+ if (event.key === "Enter") {
12843
+ event.preventDefault();
12844
+ commitEdit();
12845
+ } else if (event.key === "Escape") closeEdit();
12846
+ };
12816
12847
  useEffect(() => {
12817
12848
  const isPanModifier = (event) => event.code === "Space" || event.key === "Alt";
12818
12849
  const onKeyDown = (event) => {
@@ -12879,6 +12910,125 @@ function EmbedBlock({ id, block }) {
12879
12910
  const zoomOut = () => zoomBy(1 / EMBED_ZOOM_STEP);
12880
12911
  const resetView = () => setView(EMBED_VIEW_IDENTITY);
12881
12912
  const atIdentity = view.scale === EMBED_VIEW_IDENTITY.scale && view.x === 0 && view.y === 0;
12913
+ /**
12914
+ * The persisted view's write (the debounce over the gesture's bursts): the
12915
+ * non-identity view stands the block's attrs (the zoom / panX / panY — the
12916
+ * null idiom: the identity parts stand `null`, the reader's restore), so
12917
+ * the reload / the new tab's embed stands the same view. The values stand
12918
+ * the wire's precision (the 1%'s zoom, the 0.1 px's pan — the rounding's
12919
+ * guard over the floating-point's residue). The no-op stand: a view equal
12920
+ * to the block's own attrs writes nothing (no updatedAt's stamp, no wire
12921
+ * update).
12922
+ */
12923
+ useEffect(() => {
12924
+ const zoom = roundViewValue(view.scale, 2);
12925
+ const x = roundViewValue(view.x, 1);
12926
+ const y = roundViewValue(view.y, 1);
12927
+ if (zoom === (block.attrs.zoom ?? 1) && x === (block.attrs.panX ?? 0) && y === (block.attrs.panY ?? 0)) return;
12928
+ const timer = window.setTimeout(() => {
12929
+ doc.updateBlock(id, {
12930
+ zoom: zoom === 1 ? null : zoom,
12931
+ panX: x === 0 ? null : x,
12932
+ panY: y === 0 ? null : y
12933
+ });
12934
+ }, EMBED_PERSIST_DEBOUNCE_MS);
12935
+ return () => window.clearTimeout(timer);
12936
+ }, [
12937
+ view,
12938
+ block.attrs,
12939
+ doc,
12940
+ id
12941
+ ]);
12942
+ /**
12943
+ * The remote's stand: the panes share the persisted view over the wire — a
12944
+ * pane's peer lands a view different from the local's (the block's attrs
12945
+ * stand the peer's write), so the local's view adopts it. Own writes land
12946
+ * equal to the local's view and stand no adopt. The adopt clamps the
12947
+ * restored pan to the current wrapper (a stale pan over a narrower pane's
12948
+ * clip must not blank the embed).
12949
+ */
12950
+ useEffect(() => {
12951
+ const scale = block.attrs.zoom ?? 1;
12952
+ const x = block.attrs.panX ?? 0;
12953
+ const y = block.attrs.panY ?? 0;
12954
+ const v = viewRef.current;
12955
+ if (scale === v.scale && x === v.x && y === v.y) return;
12956
+ const rect = wrapperRef.current?.getBoundingClientRect();
12957
+ const clamped = rect === void 0 || rect.width === 0 || rect.height === 0 ? {
12958
+ x,
12959
+ y
12960
+ } : clampEmbedPan(scale, x, y, rect.width, rect.height);
12961
+ setView({
12962
+ scale,
12963
+ x: clamped.x,
12964
+ y: clamped.y
12965
+ });
12966
+ }, [block.attrs]);
12967
+ /** The token's minimum's measure (the themed's stand) over the mount. */
12968
+ useLayoutEffect(() => {
12969
+ const wrapper = wrapperRef.current;
12970
+ if (wrapper === null) return;
12971
+ setTokenMin(readEmbedMinHeight(wrapper));
12972
+ }, []);
12973
+ /**
12974
+ * The remote's stand over the height: the panes share the stretched stand
12975
+ * over the wire — a pane's peer lands a height different from the local's
12976
+ * (the block's `height` attr's stand the peer's write), so the local's
12977
+ * state adopts it. Own commits land equal to the local's state and stand
12978
+ * no adopt.
12979
+ */
12980
+ useEffect(() => {
12981
+ const attrHeight = block.attrs.height ?? null;
12982
+ if (attrHeight === heightRef.current) return;
12983
+ setHeight(attrHeight);
12984
+ }, [block.attrs]);
12985
+ const onResizePointerDown = (event) => {
12986
+ if (event.button !== 0) return;
12987
+ event.stopPropagation();
12988
+ const wrapper = wrapperRef.current;
12989
+ if (wrapper === null) return;
12990
+ const min = readEmbedMinHeight(wrapper);
12991
+ const rect = wrapper.getBoundingClientRect();
12992
+ event.target.setPointerCapture?.(event.pointerId);
12993
+ resizeSession.current = {
12994
+ pointerId: event.pointerId,
12995
+ startY: event.clientY,
12996
+ startHeight: rect.height,
12997
+ min,
12998
+ lastHeight: rect.height
12999
+ };
13000
+ };
13001
+ const onResizePointerMove = (event) => {
13002
+ const session = resizeSession.current;
13003
+ if (session === null || event.pointerId !== session.pointerId) return;
13004
+ const next = Math.max(session.startHeight + (event.clientY - session.startY), session.min);
13005
+ session.lastHeight = next;
13006
+ setHeight(next);
13007
+ };
13008
+ const endResize = (event) => {
13009
+ const session = resizeSession.current;
13010
+ if (session === null || event.pointerId !== session.pointerId) return;
13011
+ event.target.releasePointerCapture?.(event.pointerId);
13012
+ resizeSession.current = null;
13013
+ const committed = Math.round(session.lastHeight);
13014
+ if (committed <= session.min) {
13015
+ if ((block.attrs.height ?? null) !== null) doc.updateBlock(id, { height: null });
13016
+ } else if (committed !== (block.attrs.height ?? null)) doc.updateBlock(id, { height: committed });
13017
+ };
13018
+ const onResizeKeyDown = (event) => {
13019
+ const wrapper = wrapperRef.current;
13020
+ if (wrapper === null) return;
13021
+ const min = readEmbedMinHeight(wrapper);
13022
+ const delta = event.key === "ArrowDown" ? EMBED_RESIZE_KEY_STEP : event.key === "ArrowUp" ? -16 : null;
13023
+ if (delta === null) return;
13024
+ event.preventDefault();
13025
+ const current = Math.max(heightRef.current ?? min, min);
13026
+ const next = Math.max(current + delta, min);
13027
+ setHeight(next <= min ? null : next);
13028
+ if (next <= min) {
13029
+ if ((block.attrs.height ?? null) !== null) doc.updateBlock(id, { height: null });
13030
+ } else if (next !== (block.attrs.height ?? null)) doc.updateBlock(id, { height: next });
13031
+ };
12882
13032
  const onPointerDown = (event) => {
12883
13033
  if (!panModifier || event.button !== 0) return;
12884
13034
  if (event.target.closest("button") !== null) return;
@@ -12920,71 +13070,151 @@ function EmbedBlock({ id, block }) {
12920
13070
  "data-block-id": id,
12921
13071
  "data-kind": kind,
12922
13072
  className: "\n group relative my-(--docblocks-block-spacing) h-(--docblocks-embed-height) overflow-hidden\n rounded-(--docblocks-radius) border-(length:--docblocks-divider-border-width)\n border-(--docblocks-divider-border-color) outline-none\n ",
12923
- style: { cursor: panModifier ? panSession.current !== null ? "grabbing" : "grab" : void 0 },
13073
+ style: {
13074
+ ...height !== null ? { height: Math.max(height, tokenMin) } : {},
13075
+ cursor: panModifier ? panSession.current !== null ? "grabbing" : "grab" : void 0
13076
+ },
12924
13077
  onPointerDown,
12925
13078
  onPointerMove,
12926
13079
  onPointerUp: endPan,
12927
13080
  onPointerCancel: endPan,
12928
- children: [isSafeEmbedSrc(kind, src) ? /* @__PURE__ */ jsx("iframe", {
12929
- "data-slot": "docblocks/embed",
12930
- src,
12931
- title: title ?? kind,
12932
- loading: "lazy",
12933
- sandbox: sandbox ?? void 0,
12934
- className: "block h-full w-full border-0",
12935
- style: {
12936
- transform: `translate(${view.x}px, ${view.y}px) scale(${view.scale})`,
12937
- transformOrigin: "0 0",
12938
- pointerEvents: panModifier ? "none" : "auto"
12939
- }
12940
- }) : /* @__PURE__ */ jsx("div", {
12941
- "data-slot": "docblocks/embed-blocked",
12942
- className: "\n flex h-full w-full items-center justify-center p-4 text-center text-sm text-(--docblocks-muted-text-color)\n ",
12943
- children: title ?? kind
12944
- }), /* @__PURE__ */ jsxs("div", {
12945
- "data-slot": "docblocks/embed-toolbar",
12946
- className: "\n pointer-events-none absolute top-1.5 right-1.5 z-10 flex items-center gap-0.5 rounded-(--docblocks-radius)\n border-(length:--docblocks-divider-border-width) border-(--docblocks-divider-border-color)\n bg-(--docblocks-page-card-background) p-0.5 text-(--docblocks-text-color) opacity-0\n shadow-(--docblocks-formatbar-shadow) transition-opacity\n group-focus-within:pointer-events-auto group-focus-within:opacity-100\n group-hover:pointer-events-auto group-hover:opacity-100\n ",
12947
- children: [
12948
- /* @__PURE__ */ jsx(Button, {
12949
- "data-slot": "docblocks/embed-zoom-out",
12950
- variant: "ghost",
12951
- size: "icon",
12952
- "aria-label": t("embed.zoomOut"),
12953
- className: "size-6 cursor-default rounded-sm text-(--docblocks-text-color) disabled:opacity-50",
12954
- disabled: view.scale <= EMBED_MIN_SCALE,
12955
- onMouseDown: preventDefault,
12956
- onClick: zoomOut,
12957
- children: /* @__PURE__ */ jsx(Minus, { className: "size-4" })
12958
- }),
12959
- /* @__PURE__ */ jsxs("span", {
12960
- "data-slot": "docblocks/embed-zoom",
12961
- className: "min-w-10 px-1 text-center text-xs text-(--docblocks-text-color) tabular-nums",
12962
- children: [Math.round(view.scale * 100), "%"]
12963
- }),
12964
- /* @__PURE__ */ jsx(Button, {
12965
- "data-slot": "docblocks/embed-zoom-in",
12966
- variant: "ghost",
12967
- size: "icon",
12968
- "aria-label": t("embed.zoomIn"),
12969
- className: "size-6 cursor-default rounded-sm text-(--docblocks-text-color) disabled:opacity-50",
12970
- disabled: view.scale >= EMBED_MAX_SCALE,
12971
- onMouseDown: preventDefault,
12972
- onClick: zoomIn,
12973
- children: /* @__PURE__ */ jsx(Plus, { className: "size-4" })
12974
- }),
12975
- /* @__PURE__ */ jsx(Button, {
12976
- "data-slot": "docblocks/embed-zoom-reset",
12977
- variant: "ghost",
12978
- size: "icon",
12979
- "aria-label": t("embed.zoomReset"),
12980
- className: "size-6 cursor-default rounded-sm text-(--docblocks-text-color) disabled:opacity-50",
12981
- disabled: atIdentity,
12982
- onMouseDown: preventDefault,
12983
- onClick: resetView,
12984
- children: /* @__PURE__ */ jsx(RotateCcw, { className: "size-4" })
12985
- })
12986
- ]
12987
- })]
13081
+ children: [
13082
+ isSafeEmbedSrc(kind, src) ? /* @__PURE__ */ jsx("iframe", {
13083
+ "data-slot": "docblocks/embed",
13084
+ src,
13085
+ title: title ?? kind,
13086
+ loading: "lazy",
13087
+ sandbox: sandbox ?? void 0,
13088
+ className: "block h-full w-full border-0",
13089
+ style: {
13090
+ transform: `translate(${view.x}px, ${view.y}px) scale(${view.scale})`,
13091
+ transformOrigin: "0 0",
13092
+ pointerEvents: panModifier ? "none" : "auto"
13093
+ }
13094
+ }) : /* @__PURE__ */ jsx("div", {
13095
+ "data-slot": "docblocks/embed-blocked",
13096
+ className: "\n flex h-full w-full items-center justify-center p-4 text-center text-sm text-(--docblocks-muted-text-color)\n ",
13097
+ children: title ?? kind
13098
+ }),
13099
+ /* @__PURE__ */ jsxs("div", {
13100
+ "data-slot": "docblocks/embed-toolbar",
13101
+ className: "\n pointer-events-none absolute top-1.5 right-1.5 z-10 flex items-center gap-0.5 rounded-(--docblocks-radius)\n border-(length:--docblocks-divider-border-width) border-(--docblocks-divider-border-color)\n bg-(--docblocks-page-card-background) p-0.5 text-(--docblocks-text-color) opacity-0\n shadow-(--docblocks-formatbar-shadow) transition-opacity\n group-focus-within:pointer-events-auto group-focus-within:opacity-100\n group-hover:pointer-events-auto group-hover:opacity-100\n ",
13102
+ children: [
13103
+ /* @__PURE__ */ jsx(Button, {
13104
+ "data-slot": "docblocks/embed-zoom-out",
13105
+ variant: "ghost",
13106
+ size: "icon",
13107
+ "aria-label": t("embed.zoomOut"),
13108
+ className: "size-6 cursor-default rounded-sm text-(--docblocks-text-color) disabled:opacity-50",
13109
+ disabled: view.scale <= EMBED_MIN_SCALE,
13110
+ onMouseDown: preventDefault,
13111
+ onClick: zoomOut,
13112
+ children: /* @__PURE__ */ jsx(Minus, { className: "size-4" })
13113
+ }),
13114
+ /* @__PURE__ */ jsxs("span", {
13115
+ "data-slot": "docblocks/embed-zoom",
13116
+ className: "min-w-10 px-1 text-center text-xs text-(--docblocks-text-color) tabular-nums",
13117
+ children: [Math.round(view.scale * 100), "%"]
13118
+ }),
13119
+ /* @__PURE__ */ jsx(Button, {
13120
+ "data-slot": "docblocks/embed-zoom-in",
13121
+ variant: "ghost",
13122
+ size: "icon",
13123
+ "aria-label": t("embed.zoomIn"),
13124
+ className: "size-6 cursor-default rounded-sm text-(--docblocks-text-color) disabled:opacity-50",
13125
+ disabled: view.scale >= EMBED_MAX_SCALE,
13126
+ onMouseDown: preventDefault,
13127
+ onClick: zoomIn,
13128
+ children: /* @__PURE__ */ jsx(Plus, { className: "size-4" })
13129
+ }),
13130
+ /* @__PURE__ */ jsx(Button, {
13131
+ "data-slot": "docblocks/embed-zoom-reset",
13132
+ variant: "ghost",
13133
+ size: "icon",
13134
+ "aria-label": t("embed.zoomReset"),
13135
+ className: "size-6 cursor-default rounded-sm text-(--docblocks-text-color) disabled:opacity-50",
13136
+ disabled: atIdentity,
13137
+ onMouseDown: preventDefault,
13138
+ onClick: resetView,
13139
+ children: /* @__PURE__ */ jsx(RotateCcw, { className: "size-4" })
13140
+ }),
13141
+ !viewMode && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("div", {
13142
+ className: "mx-0.5 h-4 w-px bg-(--docblocks-divider-border-color)",
13143
+ "aria-hidden": true
13144
+ }), /* @__PURE__ */ jsx(Button, {
13145
+ "data-slot": "docblocks/embed-edit-url",
13146
+ variant: "ghost",
13147
+ size: "icon",
13148
+ "aria-label": t("embed.editUrl"),
13149
+ className: "size-6 cursor-default rounded-sm text-(--docblocks-text-color)",
13150
+ onMouseDown: preventDefault,
13151
+ onClick: openEdit,
13152
+ children: /* @__PURE__ */ jsx(Pen, { className: "size-4" })
13153
+ })] })
13154
+ ]
13155
+ }),
13156
+ !viewMode && editOpen && /* @__PURE__ */ jsxs("div", {
13157
+ "data-slot": "docblocks/embed-edit",
13158
+ className: "\n absolute top-8 right-1.5 z-10 flex w-56 flex-col gap-1.5 rounded-(--docblocks-radius)\n border-(length:--docblocks-divider-border-width) border-(--docblocks-divider-border-color)\n bg-(--docblocks-page-card-background) p-1.5 text-(--docblocks-text-color)\n shadow-(--docblocks-formatbar-shadow)\n ",
13159
+ children: [/* @__PURE__ */ jsxs("div", {
13160
+ className: "flex items-center gap-1",
13161
+ children: [
13162
+ /* @__PURE__ */ jsx(Input, {
13163
+ size: "sm",
13164
+ ref: editInputRef,
13165
+ type: "text",
13166
+ "data-slot": "docblocks/embed-edit-input",
13167
+ className: "h-7 flex-1 rounded-sm",
13168
+ placeholder: t("embed.srcPlaceholder"),
13169
+ value: draft,
13170
+ onChange: (event) => setDraft(event.target.value),
13171
+ onKeyDown: onEditKeyDown
13172
+ }),
13173
+ /* @__PURE__ */ jsx(Button, {
13174
+ "data-slot": "docblocks/embed-edit-save",
13175
+ variant: "ghost",
13176
+ size: "icon",
13177
+ "aria-label": t("embed.saveUrl"),
13178
+ className: "size-7 shrink-0 cursor-default rounded-sm text-(--docblocks-text-color) disabled:opacity-50",
13179
+ disabled: !canSave,
13180
+ onMouseDown: preventDefault,
13181
+ onClick: commitEdit,
13182
+ children: /* @__PURE__ */ jsx(Check, { className: "size-4" })
13183
+ }),
13184
+ /* @__PURE__ */ jsx(Button, {
13185
+ "data-slot": "docblocks/embed-edit-cancel",
13186
+ variant: "ghost",
13187
+ size: "icon",
13188
+ "aria-label": t("embed.cancelEdit"),
13189
+ className: "size-7 shrink-0 cursor-default rounded-sm text-(--docblocks-text-color)",
13190
+ onMouseDown: preventDefault,
13191
+ onClick: closeEdit,
13192
+ children: /* @__PURE__ */ jsx(X, { className: "size-4" })
13193
+ })
13194
+ ]
13195
+ }), editHint !== null && /* @__PURE__ */ jsx("p", {
13196
+ "data-slot": "docblocks/embed-edit-hint",
13197
+ className: "px-1 text-xs text-destructive",
13198
+ children: editHint
13199
+ })]
13200
+ }),
13201
+ !viewMode && /* @__PURE__ */ jsx("div", {
13202
+ "data-slot": "docblocks/embed-resize",
13203
+ role: "slider",
13204
+ "aria-orientation": "vertical",
13205
+ "aria-valuemin": tokenMin,
13206
+ "aria-valuenow": Math.max(height ?? tokenMin, tokenMin),
13207
+ "aria-label": t("embed.resizeHeight"),
13208
+ tabIndex: 0,
13209
+ className: "\n absolute inset-x-0 bottom-0 z-10 flex h-2 cursor-row-resize items-end justify-center opacity-0\n transition-opacity outline-none\n group-focus-within:opacity-100\n group-hover:opacity-100\n ",
13210
+ onPointerDown: onResizePointerDown,
13211
+ onPointerMove: onResizePointerMove,
13212
+ onPointerUp: endResize,
13213
+ onPointerCancel: endResize,
13214
+ onKeyDown: onResizeKeyDown,
13215
+ children: /* @__PURE__ */ jsx("div", { className: "mb-0.5 h-0.5 w-10 rounded-full bg-(--docblocks-divider-border-color)" })
13216
+ })
13217
+ ]
12988
13218
  });
12989
13219
  }
12990
13220
  //#endregion
@@ -18092,7 +18322,8 @@ function TextBlock({ id, block, tag, nested, collapsible = false, flat = false,
18092
18322
  size: "icon",
18093
18323
  onClick: (event) => deleteBlock(event.currentTarget, id),
18094
18324
  onMouseDown: onControlMouseDown,
18095
- className: cn("absolute top-[calc((var(--docblocks-line-box-height)-var(--docblocks-gutter-size))/2)] right-0", "cursor-pointer rounded-full opacity-0 group-hover:opacity-100", "text-(--docblocks-gutter-color)", "size-5", selected && "opacity-100"),
18325
+ className: cn("absolute top-[calc((var(--docblocks-line-box-height)-var(--docblocks-gutter-size))/2)]", "cursor-pointer rounded-full opacity-0 group-hover:opacity-100", "text-(--docblocks-gutter-color)", "size-5", selected && "opacity-100"),
18326
+ style: { right: "var(--docblocks-gutter-left)" },
18096
18327
  children: /* @__PURE__ */ jsx(Trash, {})
18097
18328
  }),
18098
18329
  block.type === "list-item" && block.attrs.list === "task" && /* @__PURE__ */ jsx(Checkbox, {
@@ -18660,67 +18891,36 @@ function TitleMenu({ slot, ariaLabel, open, onOpenChange, onCommit }) {
18660
18891
  //#endregion
18661
18892
  //#region src/canvas/FrameBlock.tsx
18662
18893
  /**
18663
- * The canvas frame's renderer: the viewport window (the "camera") into the
18664
- * frame's own surface's content. The CanvasItem's wrapper carries the
18665
- * geometry + the selection + the move/resize preview; inside it: the full
18666
- * box (the true window — overflow hidden) over the canvas's own background
18667
- * (the background's color + the dot grid phase-aligned to the world's
18668
- * grid — the seamless canvas continuation that hides the content under
18669
- * the window) and the container's thin rounded border (the selection's
18670
- * color on the selection). The window's content is the inert clipped
18671
- * copy of the surface's children (FrameWindowContent) at the window's
18672
- * offset (the surface's world origin over the box's local point): moving
18673
- * the frame pans the window — the content stands at its world coordinates
18674
- * (the offset re-derives over the effective geometry — the committed box
18675
- * plus the move/resize preview) while the box slides. The frame's title
18676
- * chip (the container's idiom) stands over the shared CanvasTitleChip.
18677
- * The frame-chain's cycle guard: a frame rendered inside another frame's
18678
- * window carries the chain's ids and excludes the chain + itself from its
18679
- * window's content (the chain grows strictly down the nesting — the
18680
- * recursion is finite, no depth limit needed).
18681
- */
18682
- /** The frame chain's stand of a direct surface child (no window above).
18683
- * The stable module constant stands the default — the memo's identity
18684
- * comparison bails over the resting items. */
18685
- const EMPTY_FRAME_CHAIN = /* @__PURE__ */ new Set();
18686
- function FrameBlock({ id, movePreview = null, resizeBox = null, selected = false, frameChain = EMPTY_FRAME_CHAIN }) {
18894
+ * The canvas frame's renderer: the frame's boundary — the rectangle that
18895
+ * defines the region the page-frame block renders. The CanvasItem's wrapper
18896
+ * carries the geometry + the selection + the move/resize preview; the box
18897
+ * stands the container's thin rounded border (the selection's color on the
18898
+ * selection) over the canvas's own background. The frame renders the
18899
+ * boundary only — no content copy: the canvas's own figures stand behind the
18900
+ * transparent box (the canvas frame is a region marker, not a window), and
18901
+ * the content's mirror (the surface's children clipped to the frame's
18902
+ * region) stands on the page-frame block's side (FramePageBlock). The
18903
+ * frame's title chip (the container's idiom) stands over the shared
18904
+ * CanvasTitleChip.
18905
+ */
18906
+ function FrameBlock({ id, selected = false }) {
18687
18907
  const { t } = useTranslation("canvas");
18688
- const { parentId, parentType } = useBlockContext();
18908
+ const { parentType } = useBlockContext();
18689
18909
  const block = useBlock(id);
18690
18910
  if (parentType !== "surface" || block === void 0 || block.type !== "frame") return /* @__PURE__ */ jsx(PlaceholderBlock, { type: "frame" });
18691
- const committed = effectiveGeometry(block);
18692
- const effective = {
18693
- x: (resizeBox?.x ?? committed.x) + (movePreview?.dx ?? 0),
18694
- y: (resizeBox?.y ?? committed.y) + (movePreview?.dy ?? 0),
18695
- w: resizeBox?.w ?? committed.w,
18696
- h: resizeBox?.h ?? committed.h
18697
- };
18698
- const offset = frameWindowOffset(effective);
18699
18911
  const selectedColor = "var(--docblocks-canvas-selection-border-color)";
18700
- const ringColor = selected ? selectedColor : "var(--docblocks-canvas-group-border-color)";
18701
- const boxStyle = {
18702
- backgroundColor: "var(--docblocks-canvas-background)",
18703
- ...frameDotBackground(effective.x - effective.w / 2, effective.y - effective.h / 2),
18704
- boxShadow: `inset 0 0 0 1px ${ringColor}`,
18705
- outline: selected ? `var(--docblocks-canvas-selection-border-width) solid ${selectedColor}` : void 0,
18706
- outlineOffset: 1
18707
- };
18708
- const excludedIds = useMemo(() => [...frameChain, id], [frameChain, id]);
18709
- const childFrameChain = useMemo(() => /* @__PURE__ */ new Set([...frameChain, id]), [frameChain, id]);
18710
18912
  return /* @__PURE__ */ jsxs("div", {
18711
18913
  "data-slot": "docblocks/frame",
18712
18914
  "data-block-id": id,
18713
18915
  className: "h-full w-full",
18714
18916
  children: [/* @__PURE__ */ jsx("div", {
18715
- "data-slot": "docblocks/frame-window",
18716
- className: "absolute inset-0 overflow-hidden rounded-(--docblocks-radius)",
18717
- style: boxStyle,
18718
- children: /* @__PURE__ */ jsx(FrameWindowContent, {
18719
- surfaceId: parentId,
18720
- offset,
18721
- excludedIds,
18722
- frameChain: childFrameChain
18723
- })
18917
+ "data-slot": "docblocks/frame-boundary",
18918
+ className: "absolute inset-0 rounded-(--docblocks-radius)",
18919
+ style: {
18920
+ boxShadow: `inset 0 0 0 1px ${selected ? selectedColor : "var(--docblocks-canvas-group-border-color)"}`,
18921
+ outline: selected ? `var(--docblocks-canvas-selection-border-width) solid ${selectedColor}` : void 0,
18922
+ outlineOffset: 1
18923
+ }
18724
18924
  }), /* @__PURE__ */ jsx(CanvasTitleChip, {
18725
18925
  id,
18726
18926
  title: block.attrs?.title ?? "",
@@ -22768,7 +22968,7 @@ const CANVAS_ITEM_LABEL_KEYS = {
22768
22968
  * order, the line can stand over or under a figure); `interactive`
22769
22969
  * toggles the line's hit path (the page view's surface stands it inert).
22770
22970
  */
22771
- const CanvasItem = memo(function CanvasItem({ id, pendingConnector = null, movePreview = null, resizeBox = null, interactive = true, kbFocused = false, frameChain = EMPTY_FRAME_CHAIN }) {
22971
+ const CanvasItem = memo(function CanvasItem({ id, pendingConnector = null, movePreview = null, resizeBox = null, interactive = true, kbFocused = false }) {
22772
22972
  const { t } = useTranslation("canvas");
22773
22973
  const { store, renderers } = useEditorContext();
22774
22974
  const { siblings } = useBlockContext();
@@ -22803,10 +23003,7 @@ const CanvasItem = memo(function CanvasItem({ id, pendingConnector = null, moveP
22803
23003
  tag: "div"
22804
23004
  }) : block.type === "frame" ? /* @__PURE__ */ jsx(FrameBlock, {
22805
23005
  id,
22806
- movePreview,
22807
- resizeBox,
22808
- selected,
22809
- frameChain
23006
+ selected
22810
23007
  }) : block.type === "group" ? /* @__PURE__ */ jsx(GroupBlock, {
22811
23008
  id,
22812
23009
  block,
@@ -22853,7 +23050,7 @@ const CanvasItem = memo(function CanvasItem({ id, pendingConnector = null, moveP
22853
23050
  style,
22854
23051
  children: content
22855
23052
  });
22856
- }, (prev, next) => prev.id === next.id && prev.pendingConnector === next.pendingConnector && prev.movePreview === next.movePreview && prev.resizeBox === next.resizeBox && prev.interactive === next.interactive && prev.kbFocused === next.kbFocused && prev.frameChain === next.frameChain);
23053
+ }, (prev, next) => prev.id === next.id && prev.pendingConnector === next.pendingConnector && prev.movePreview === next.movePreview && prev.resizeBox === next.resizeBox && prev.interactive === next.interactive && prev.kbFocused === next.kbFocused);
22857
23054
  //#endregion
22858
23055
  //#region src/canvas/ConnectorArrowDefs.tsx
22859
23056
  /** The surface's arrowhead defs (the hidden 0x0 svg): the marker ids stand
@@ -22968,7 +23165,7 @@ function ConnectorArrowDefs({ id }) {
22968
23165
  * context providers are not DOM nodes, so several surfaces merge onto the
22969
23166
  * same canvas in root order (the later surface paints on top: the DOM
22970
23167
  * order is the z-order). */
22971
- function SurfaceLayer({ id, children, pendingConnector, movePreview, resizePreview, endpointDrag, endpointPreview, showPorts, endpointSnap, kbFocusId = null, inert = false, excludedIds, frameChain }) {
23168
+ function SurfaceLayer({ id, children, pendingConnector, movePreview, resizePreview, endpointDrag, endpointPreview, showPorts, endpointSnap, kbFocusId = null, inert = false, excludedIds }) {
22972
23169
  const contextValue = useBlockContextValue(id, children, "surface");
22973
23170
  const session = useMemo(() => ({
22974
23171
  movePreview,
@@ -23001,8 +23198,7 @@ function SurfaceLayer({ id, children, pendingConnector, movePreview, resizePrevi
23001
23198
  pendingConnector,
23002
23199
  movePreview: movePreview !== null && movePreview.ids.includes(childId) ? movePreview : null,
23003
23200
  resizeBox: resizePreview !== null && resizePreview.id === childId ? resizePreview.box : null,
23004
- kbFocused: kbFocusId === childId,
23005
- frameChain
23201
+ kbFocused: kbFocusId === childId
23006
23202
  }, childId)),
23007
23203
  showPorts && /* @__PURE__ */ jsx(PortDots, {
23008
23204
  id,
@@ -23097,8 +23293,7 @@ function PortDots({ id, snapped }) {
23097
23293
  //#endregion
23098
23294
  //#region src/canvas/FrameWindowContent.tsx
23099
23295
  const EMPTY_IDS$1 = [];
23100
- const EMPTY_CHAIN = /* @__PURE__ */ new Set();
23101
- function FrameWindowContent({ surfaceId, offset, excludedIds = EMPTY_IDS$1, frameChain = EMPTY_CHAIN, className }) {
23296
+ function FrameWindowContent({ surfaceId, offset, excludedIds = EMPTY_IDS$1, className }) {
23102
23297
  const surface = useBlock(surfaceId);
23103
23298
  const children = surface !== void 0 && surface.type === "surface" ? surface.children ?? EMPTY_IDS$1 : EMPTY_IDS$1;
23104
23299
  return /* @__PURE__ */ jsx("div", {
@@ -23124,8 +23319,7 @@ function FrameWindowContent({ surfaceId, offset, excludedIds = EMPTY_IDS$1, fram
23124
23319
  endpointSnap: null,
23125
23320
  kbFocusId: null,
23126
23321
  inert: true,
23127
- excludedIds,
23128
- frameChain
23322
+ excludedIds
23129
23323
  }), /* @__PURE__ */ jsx(ConnectorLabelChips, { children })]
23130
23324
  })
23131
23325
  });
@@ -23134,11 +23328,13 @@ function FrameWindowContent({ surfaceId, offset, excludedIds = EMPTY_IDS$1, fram
23134
23328
  //#region src/blocks/FramePageBlock.tsx
23135
23329
  /**
23136
23330
  * The page-frame block's renderer: the inline window into the referenced
23137
- * canvas frame's surface — the same window the canvas frame stands
23138
- * (FrameWindowContent over the window's offset; the box's background the
23139
- * canvas's own background + the dot grid phase-aligned to the world's
23140
- * grid; the container's thin rounded border), inlined in the page flow at
23141
- * 1:1 (no fit/zoom controls — the v1 boundary). The box's height is the
23331
+ * canvas frame's surface — the content's mirror (FrameWindowContent over
23332
+ * the window's offset; the box's background the canvas's own background +
23333
+ * the dot grid phase-aligned to the world's grid; the container's thin
23334
+ * rounded border), inlined in the page flow at
23335
+ * 1:1 (no fit/zoom controls — the v1 boundary). The canvas frame stands the
23336
+ * boundary only (a region marker) — this page block is the sole renderer
23337
+ * of the frame's content. The box's height is the
23142
23338
  * referenced frame's 1:1; the width is the frame's width clipped to the
23143
23339
  * row's measured width (the window's center stays at the frame's center —
23144
23340
  * a narrow row clips the wide window horizontally, the offset and the dot
@@ -23232,7 +23428,6 @@ function FramePageWindow({ id, frameId, surfaceId, referenced }) {
23232
23428
  };
23233
23429
  const title = referenced.attrs?.title ?? "";
23234
23430
  const excludedIds = useMemo(() => [frameId], [frameId]);
23235
- const frameChain = useMemo(() => /* @__PURE__ */ new Set([frameId]), [frameId]);
23236
23431
  return /* @__PURE__ */ jsx("div", {
23237
23432
  ref: rowRef,
23238
23433
  "data-slot": "docblocks/frame-page",
@@ -23250,8 +23445,7 @@ function FramePageWindow({ id, frameId, surfaceId, referenced }) {
23250
23445
  children: /* @__PURE__ */ jsx(FrameWindowContent, {
23251
23446
  surfaceId,
23252
23447
  offset,
23253
- excludedIds,
23254
- frameChain
23448
+ excludedIds
23255
23449
  })
23256
23450
  })
23257
23451
  });
@@ -27766,6 +27960,15 @@ function positionAtPoint(doc, root, x, y) {
27766
27960
  * soft-break lines) itself — a permanent capture listener, independent of
27767
27961
  * the session (the real pointer's third press starts no session: the
27768
27962
  * pointer's detail stands 3 and the session guard returns).
27963
+ *
27964
+ * The iframe's focus exception: a press while the document's focus stands
27965
+ * in a frame (the embed's iframe) tears the session down and lets the
27966
+ * mousedown's default stand — the default's focus move out of the frame
27967
+ * is the editor's document's focus, and suppressing it would seat the
27968
+ * caret over the unfocused document (no visible caret, the keys' no-
27969
+ * stand). The browser's native caret's seat stands the press; a double
27970
+ * press over the focused frame stands the same stand over the detail's
27971
+ * check.
27769
27972
  */
27770
27973
  function useCrossDrag(rootRef, doc) {
27771
27974
  const sessionRef = useRef(null);
@@ -27799,12 +28002,19 @@ function useCrossDrag(rootRef, doc) {
27799
28002
  if (session === null || root === null || event.button !== 0) return;
27800
28003
  const target = event.target instanceof Element ? event.target : null;
27801
28004
  if (target === null || !root.contains(target)) return;
27802
- if (event.detail >= 2) {
28005
+ const tearDown = () => {
27803
28006
  sessionRef.current = null;
27804
28007
  window.removeEventListener("pointermove", move);
27805
28008
  window.removeEventListener("pointerup", end);
27806
28009
  window.removeEventListener("pointercancel", end);
27807
28010
  window.removeEventListener("mousedown", mousedown, true);
28011
+ };
28012
+ if (event.detail >= 2) {
28013
+ tearDown();
28014
+ return;
28015
+ }
28016
+ if (document.activeElement instanceof HTMLIFrameElement) {
28017
+ tearDown();
27808
28018
  return;
27809
28019
  }
27810
28020
  event.preventDefault();
@@ -27846,6 +28056,7 @@ function useCrossDrag(rootRef, doc) {
27846
28056
  if (target === null || !root.contains(target)) return;
27847
28057
  const inner = target.closest("[data-slot=\"docblocks/block-content\"]");
27848
28058
  if (inner === null || inner.closest("[data-slot=\"docblocks/editor\"]") !== root) return;
28059
+ if (document.activeElement instanceof HTMLIFrameElement) return;
27849
28060
  const blockId = inner.dataset.blockId;
27850
28061
  const head = locatePosition(inner, 0);
27851
28062
  const tail = locatePosition(inner, doc.getTextLength(blockId));
@@ -28357,7 +28568,8 @@ const RowItem = memo(function RowItem({ node, layoutSlide, isDragged, animated,
28357
28568
  size: "icon",
28358
28569
  onClick: (event) => deleteBlock(event.currentTarget, node.id),
28359
28570
  onMouseDown: onHandleMouseDown,
28360
- className: cn("absolute top-[calc((var(--docblocks-line-box-height)-var(--docblocks-gutter-size))/2)] right-0", "cursor-pointer rounded-full opacity-0 group-hover:opacity-100", "text-(--docblocks-gutter-color)", "size-5", selected && "opacity-100"),
28571
+ className: cn("absolute top-[calc((var(--docblocks-line-box-height)-var(--docblocks-gutter-size))/2)]", "cursor-pointer rounded-full opacity-0 group-hover:opacity-100", "text-(--docblocks-gutter-color)", "size-5", selected && "opacity-100"),
28572
+ style: { right: "var(--docblocks-gutter-left)" },
28361
28573
  children: /* @__PURE__ */ jsx(Trash, {})
28362
28574
  }),
28363
28575
  /* @__PURE__ */ jsx(BlockNodeView, {