docblocks-react-ui 0.3.1 → 0.4.0

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
@@ -10393,22 +10362,16 @@ function FrameSlashPicker({ onCommit, onCancel }) {
10393
10362
  //#endregion
10394
10363
  //#region src/editing/slash/SlashEmbedForm.tsx
10395
10364
  /**
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
- ];
10365
+ * The slash `Embed` form (the embed-creation affordance): a URL input
10366
+ * (auto-focused) only — the kind stands the `iframe` default (the renderer's
10367
+ * hint; all kinds render as an iframe). The menu stands open over the phrase
10368
+ * while the form stands; the commit (the URL's Enter, or the check) lands the
10369
+ * phrase strip + the `insertBlock('embed', { kind: "iframe", src })` in the
10370
+ * caller's transact. An empty (or whitespace-only) URL commits nothing (the
10371
+ * menu stands open on the phrase — the Image / Database contract).
10372
+ */
10373
+ /** The embed's kind (the renderer's hint — the form stands the URL only). */
10374
+ const EMBED_KIND_IFRAME = "iframe";
10412
10375
  /** The form's i18n scope: the package's instance stands the provider's seat
10413
10376
  * over the form (the form stands also outside the editor's scope). */
10414
10377
  function SlashEmbedForm(props) {
@@ -10418,7 +10381,6 @@ function SlashEmbedForm(props) {
10418
10381
  function SlashEmbedFormInner({ onCommit, onCancel }) {
10419
10382
  const { t } = useTranslation("slash");
10420
10383
  const [url, setUrl] = useState("");
10421
- const [kind, setKind] = useState("iframe");
10422
10384
  const urlInputRef = useRef(null);
10423
10385
  useEffect(() => {
10424
10386
  urlInputRef.current?.focus();
@@ -10426,18 +10388,15 @@ function SlashEmbedFormInner({ onCommit, onCancel }) {
10426
10388
  const commit = () => {
10427
10389
  const trimmed = url.trim();
10428
10390
  if (trimmed.length === 0) return;
10429
- if (!isSafeEmbedSrc(kind, trimmed)) return;
10430
- onCommit({
10431
- kind,
10432
- src: trimmed
10433
- });
10391
+ if (!isSafeEmbedSrc(EMBED_KIND_IFRAME, trimmed)) return;
10392
+ onCommit(trimmed);
10434
10393
  };
10435
10394
  const stopFormKey = (event) => {
10436
10395
  event.stopPropagation();
10437
10396
  if (event.key === "Escape") onCancel();
10438
10397
  };
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;
10398
+ const canCommit = url.trim().length > 0 && isSafeEmbedSrc(EMBED_KIND_IFRAME, url.trim());
10399
+ const unsafeHint = url.trim().length > 0 && !isSafeEmbedSrc(EMBED_KIND_IFRAME, url.trim()) ? t("embed.unsafeHint") : null;
10441
10400
  const onUrlKeyDown = (event) => {
10442
10401
  stopFormKey(event);
10443
10402
  if (event.key === "Enter") {
@@ -10446,51 +10405,36 @@ function SlashEmbedFormInner({ onCommit, onCancel }) {
10446
10405
  }
10447
10406
  };
10448
10407
  const onUrlChange = (event) => setUrl(event.target.value);
10449
- const onKindChange = (event) => setKind(event.target.value);
10450
10408
  return /* @__PURE__ */ jsxs("div", {
10451
10409
  "data-slot": "docblocks/slash-embed-form",
10452
10410
  className: "flex w-56 flex-col gap-2 p-1",
10453
- children: [
10454
- /* @__PURE__ */ jsx(Input, {
10411
+ children: [/* @__PURE__ */ jsxs("div", {
10412
+ className: "flex items-center gap-1",
10413
+ children: [/* @__PURE__ */ jsx(Input, {
10455
10414
  size: "sm",
10456
10415
  ref: urlInputRef,
10457
10416
  type: "text",
10458
10417
  "data-slot": "docblocks/slash-embed-url",
10459
- className: "rounded-sm",
10418
+ className: "h-7 flex-1 rounded-sm",
10460
10419
  placeholder: t("embed.urlPlaceholder"),
10461
10420
  value: url,
10462
10421
  onKeyDown: onUrlKeyDown,
10463
10422
  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
- ]
10423
+ }), /* @__PURE__ */ jsx(Button, {
10424
+ "data-slot": "docblocks/slash-embed-insert",
10425
+ variant: "outline",
10426
+ size: "icon",
10427
+ className: "\n size-7 shrink-0 cursor-default rounded-sm bg-transparent\n disabled:cursor-default\n dark:bg-transparent\n ",
10428
+ disabled: !canCommit,
10429
+ onMouseDown: preventDefault,
10430
+ onClick: commit,
10431
+ children: /* @__PURE__ */ jsx(Check, { className: "size-4" })
10432
+ })]
10433
+ }), unsafeHint !== null && /* @__PURE__ */ jsx("p", {
10434
+ "data-slot": "docblocks/slash-embed-hint",
10435
+ className: "px-1 text-xs text-destructive",
10436
+ children: unsafeHint
10437
+ })]
10494
10438
  });
10495
10439
  }
10496
10440
  /** The v1 command list (filterable by the typed query). The `label` is the
@@ -10577,8 +10521,8 @@ const SLASH_COMMANDS = [
10577
10521
  type: "divider"
10578
10522
  },
10579
10523
  {
10580
- label: "Image",
10581
- labelKey: "blocks.image",
10524
+ label: "File",
10525
+ labelKey: "blocks.file",
10582
10526
  icon: Image,
10583
10527
  type: "image"
10584
10528
  },
@@ -11263,7 +11207,7 @@ function useSlashMenu(rootRef) {
11263
11207
  doc,
11264
11208
  rootRef
11265
11209
  ]);
11266
- const commitEmbed = useCallback((input) => {
11210
+ const commitEmbed = useCallback((src) => {
11267
11211
  const pending = pendingEmbedRef.current;
11268
11212
  pendingEmbedRef.current = null;
11269
11213
  setEmbedForm(false);
@@ -11284,8 +11228,8 @@ function useSlashMenu(rootRef) {
11284
11228
  doc.deleteText(blockId, slashOffset, queryEnd);
11285
11229
  }
11286
11230
  const embedId = doc.insertBlock("embed", {
11287
- kind: input.kind,
11288
- src: input.src,
11231
+ kind: "iframe",
11232
+ src,
11289
11233
  ...ind
11290
11234
  }, {
11291
11235
  parent: parentId,
@@ -11897,15 +11841,18 @@ function NotPublishedPlaceholder() {
11897
11841
  //#endregion
11898
11842
  //#region src/editor-provider/constants.ts
11899
11843
  /**
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";
11844
+ * The embed sandbox (the default when no `embedSandbox` policy is supplied):
11845
+ * the embedded content runs with its own origin's grants (`allow-same-origin`
11846
+ * — the real-app requirement: a dashboard's own storage / cookies stand,
11847
+ * so the app boots like in a plain tab) + scripts, forms, and popups.
11848
+ * The frame's document stands a separate document either way: a
11849
+ * cross-origin embed cannot reach the parent's DOM or state. The one
11850
+ * posture consequence: a same-origin URL (the page's own origin) reaches
11851
+ * the parent over `window.parent` — the document author's own choice to
11852
+ * embed it. The `embedSandbox` policy stands the restriction (the
11853
+ * opaque-origin stand — no `allow-same-origin` — for the untrusted embeds).
11854
+ */
11855
+ const DEFAULT_EMBED_SANDBOX = "allow-scripts allow-same-origin allow-forms allow-popups";
11909
11856
  const defaultEmbedSandbox = () => DEFAULT_EMBED_SANDBOX;
11910
11857
  //#endregion
11911
11858
  //#region src/editor-provider/context.ts
@@ -12738,20 +12685,34 @@ function DatabaseBlock({ id, block }) {
12738
12685
  * only (all kinds render as an iframe; per-kind card renderers come later),
12739
12686
  * so an unknown kind (newer data) still renders the same iframe. `src` is a
12740
12687
  * 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
12688
+ * iframe stands a `sandbox` attribute — the default grants the same-origin
12689
+ * (the embedded app's own storage / cookies stand, so the real apps boot
12690
+ * like in a plain tab) or the consumer's `embedSandbox` policy (the
12691
+ * opaque-origin's restriction over the untrusted, `null` for the fully
12692
+ * trusted). The block is not text-editable, so the wrapper carries no gutter
12746
12693
  * handle.
12747
12694
  *
12748
- * Pan/zoom (view state): the iframe stands a CSS
12695
+ * Height (the stretched stand): the wrapper's height stands the
12696
+ * `--docblocks-embed-height` token (the default — the minimum). The
12697
+ * bottom-edge resize handle (the hover reveal, the keyboard's arrows)
12698
+ * stretches it over the minimum; the stretched px stand the block's
12699
+ * `height` attr (the null idiom: null / absent = the token's default), so
12700
+ * the reload restores it and the panes share it over the wire (the commit
12701
+ * stands the gesture's rest — one write per drag, one per key press).
12702
+ *
12703
+ * Pan/zoom (the persisted view): the iframe stands a CSS
12749
12704
  * `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:
12705
+ * view stands the block's `zoom` / `panX` / `panY` attrs (the null idiom:
12706
+ * null / absent = the identity) — the reload restores the view and the wire
12707
+ * carries it over the panes (the panes share the persisted view; a pane's
12708
+ * gesture writes over the doc, the others adopt it). The write is debounced
12709
+ * over the gesture's bursts (one doc write per rest). The controls:
12753
12710
  *
12754
12711
  * - the hover toolbar (zoom in / out, the percent, reset) — always stands;
12712
+ * - the toolbar's URL edit (the editing mode's stand): the panel over the
12713
+ * toolbar changes the `src` (the safe non-empty's write — the unsafe's
12714
+ * rejection over the slash form's rule; the iframe's navigation over the
12715
+ * new src, the view's / the height's attrs stand untouched);
12755
12716
  * - the wheel — `ctrl`/`cmd` zooms at the cursor, a plain wheel pans
12756
12717
  * vertically (content opposite to the delta, the canvas idiom);
12757
12718
  * - the modifier drag — a `space` / `alt`-held pointer drag pans (the iframe
@@ -12771,12 +12732,36 @@ const EMBED_MAX_SCALE = 4;
12771
12732
  const EMBED_ZOOM_STEP = 1.2;
12772
12733
  /** The wheel's continuous zoom factor (the exponential of the delta, the canvas pinch idiom). */
12773
12734
  const EMBED_WHEEL_ZOOM = .002;
12735
+ /** The view's persist debounce (the gesture's bursts stand one doc write per rest). */
12736
+ const EMBED_PERSIST_DEBOUNCE_MS = 250;
12737
+ /** The height's fallback minimum (px): the `--docblocks-embed-height` token's default (the non-CSS environment's stand — jsdom). */
12738
+ const EMBED_MIN_HEIGHT = 300;
12739
+ /** The resize handle's key step (px per arrow press). */
12740
+ const EMBED_RESIZE_KEY_STEP = 16;
12774
12741
  /** The identity view: the iframe at its natural size, filling the wrapper. */
12775
12742
  const EMBED_VIEW_IDENTITY = {
12776
12743
  scale: 1,
12777
12744
  x: 0,
12778
12745
  y: 0
12779
12746
  };
12747
+ /** The view over the block's persisted attrs (the null idiom's restore). */
12748
+ function viewOfAttrs(zoom, panX, panY) {
12749
+ return {
12750
+ scale: zoom ?? 1,
12751
+ x: panX ?? 0,
12752
+ y: panY ?? 0
12753
+ };
12754
+ }
12755
+ /**
12756
+ * The view's wire stand: the gesture's floating-point's residue must not
12757
+ * stand a non-identity's persist (the zoom-out/in's round-trip lands the
12758
+ * 0.9999…'s "identity"). The zoom's round to the label's 1%'s precision,
12759
+ * the pan's to the 0.1 px (the sub-pixel's residue stands the identity).
12760
+ */
12761
+ function roundViewValue(value, places) {
12762
+ const factor = 10 ** places;
12763
+ return Math.round(value * factor) / factor;
12764
+ }
12780
12765
  /**
12781
12766
  * Clamp the pan against the scaled embed's overflow. The embed cannot be
12782
12767
  * dragged past the container: at `scale <= 1` it fits (or is smaller) and
@@ -12802,17 +12787,61 @@ function zoomAtPoint(view, cx, cy, nextScale) {
12802
12787
  y: cy - (cy - view.y) * factor
12803
12788
  };
12804
12789
  }
12790
+ /**
12791
+ * The height's wire stand: the token's minimum over the wrapper (the
12792
+ * themed token's stand), the fallback's default over the non-CSS's stand
12793
+ * (jsdom's empty computed value).
12794
+ */
12795
+ function readEmbedMinHeight(wrapper) {
12796
+ const raw = getComputedStyle(wrapper).getPropertyValue("--docblocks-embed-height");
12797
+ const value = Number.parseFloat(raw);
12798
+ return Number.isFinite(value) && value > 0 ? value : EMBED_MIN_HEIGHT;
12799
+ }
12805
12800
  function EmbedBlock({ id, block }) {
12806
12801
  const { t } = useTranslation("blocks");
12807
12802
  const { kind, src, title } = block.attrs;
12808
- const { embedSandbox } = useEditorContext();
12803
+ const { doc, embedSandbox, viewMode } = useEditorContext();
12809
12804
  const sandbox = embedSandbox(src);
12810
12805
  const wrapperRef = useRef(null);
12811
- const [view, setView] = useState(EMBED_VIEW_IDENTITY);
12806
+ const [view, setView] = useState(() => viewOfAttrs(block.attrs.zoom, block.attrs.panX, block.attrs.panY));
12812
12807
  const viewRef = useRef(view);
12813
12808
  viewRef.current = view;
12809
+ const [height, setHeight] = useState(block.attrs.height ?? null);
12810
+ const heightRef = useRef(height);
12811
+ heightRef.current = height;
12812
+ const [tokenMin, setTokenMin] = useState(EMBED_MIN_HEIGHT);
12813
+ const resizeSession = useRef(null);
12814
12814
  const [panModifier, setPanModifier] = useState(false);
12815
12815
  const panSession = useRef(null);
12816
+ const [editOpen, setEditOpen] = useState(false);
12817
+ const [draft, setDraft] = useState(src);
12818
+ const editInputRef = useRef(null);
12819
+ /** The edit's open: the draft stands the block's own src (the cancel's restore). */
12820
+ const openEdit = () => {
12821
+ setDraft(src);
12822
+ setEditOpen(true);
12823
+ };
12824
+ const closeEdit = () => setEditOpen(false);
12825
+ useEffect(() => {
12826
+ if (editOpen) editInputRef.current?.focus();
12827
+ }, [editOpen]);
12828
+ const trimmedDraft = draft.trim();
12829
+ const canSave = trimmedDraft.length > 0 && isSafeEmbedSrc(kind, trimmedDraft);
12830
+ const editHint = trimmedDraft.length > 0 && !isSafeEmbedSrc(kind, trimmedDraft) ? t("embed.unsafeHint") : null;
12831
+ /** The edit's commit: the safe non-empty draft stands the `src`'s write.
12832
+ * The unchanged draft writes nothing (no updatedAt's stamp, no wire update). */
12833
+ const commitEdit = () => {
12834
+ if (!canSave) return;
12835
+ if (trimmedDraft !== src) doc.updateBlock(id, { src: trimmedDraft });
12836
+ setEditOpen(false);
12837
+ };
12838
+ const onEditKeyDown = (event) => {
12839
+ event.stopPropagation();
12840
+ if (event.key === "Enter") {
12841
+ event.preventDefault();
12842
+ commitEdit();
12843
+ } else if (event.key === "Escape") closeEdit();
12844
+ };
12816
12845
  useEffect(() => {
12817
12846
  const isPanModifier = (event) => event.code === "Space" || event.key === "Alt";
12818
12847
  const onKeyDown = (event) => {
@@ -12879,6 +12908,125 @@ function EmbedBlock({ id, block }) {
12879
12908
  const zoomOut = () => zoomBy(1 / EMBED_ZOOM_STEP);
12880
12909
  const resetView = () => setView(EMBED_VIEW_IDENTITY);
12881
12910
  const atIdentity = view.scale === EMBED_VIEW_IDENTITY.scale && view.x === 0 && view.y === 0;
12911
+ /**
12912
+ * The persisted view's write (the debounce over the gesture's bursts): the
12913
+ * non-identity view stands the block's attrs (the zoom / panX / panY — the
12914
+ * null idiom: the identity parts stand `null`, the reader's restore), so
12915
+ * the reload / the new tab's embed stands the same view. The values stand
12916
+ * the wire's precision (the 1%'s zoom, the 0.1 px's pan — the rounding's
12917
+ * guard over the floating-point's residue). The no-op stand: a view equal
12918
+ * to the block's own attrs writes nothing (no updatedAt's stamp, no wire
12919
+ * update).
12920
+ */
12921
+ useEffect(() => {
12922
+ const zoom = roundViewValue(view.scale, 2);
12923
+ const x = roundViewValue(view.x, 1);
12924
+ const y = roundViewValue(view.y, 1);
12925
+ if (zoom === (block.attrs.zoom ?? 1) && x === (block.attrs.panX ?? 0) && y === (block.attrs.panY ?? 0)) return;
12926
+ const timer = window.setTimeout(() => {
12927
+ doc.updateBlock(id, {
12928
+ zoom: zoom === 1 ? null : zoom,
12929
+ panX: x === 0 ? null : x,
12930
+ panY: y === 0 ? null : y
12931
+ });
12932
+ }, EMBED_PERSIST_DEBOUNCE_MS);
12933
+ return () => window.clearTimeout(timer);
12934
+ }, [
12935
+ view,
12936
+ block.attrs,
12937
+ doc,
12938
+ id
12939
+ ]);
12940
+ /**
12941
+ * The remote's stand: the panes share the persisted view over the wire — a
12942
+ * pane's peer lands a view different from the local's (the block's attrs
12943
+ * stand the peer's write), so the local's view adopts it. Own writes land
12944
+ * equal to the local's view and stand no adopt. The adopt clamps the
12945
+ * restored pan to the current wrapper (a stale pan over a narrower pane's
12946
+ * clip must not blank the embed).
12947
+ */
12948
+ useEffect(() => {
12949
+ const scale = block.attrs.zoom ?? 1;
12950
+ const x = block.attrs.panX ?? 0;
12951
+ const y = block.attrs.panY ?? 0;
12952
+ const v = viewRef.current;
12953
+ if (scale === v.scale && x === v.x && y === v.y) return;
12954
+ const rect = wrapperRef.current?.getBoundingClientRect();
12955
+ const clamped = rect === void 0 || rect.width === 0 || rect.height === 0 ? {
12956
+ x,
12957
+ y
12958
+ } : clampEmbedPan(scale, x, y, rect.width, rect.height);
12959
+ setView({
12960
+ scale,
12961
+ x: clamped.x,
12962
+ y: clamped.y
12963
+ });
12964
+ }, [block.attrs]);
12965
+ /** The token's minimum's measure (the themed's stand) over the mount. */
12966
+ useLayoutEffect(() => {
12967
+ const wrapper = wrapperRef.current;
12968
+ if (wrapper === null) return;
12969
+ setTokenMin(readEmbedMinHeight(wrapper));
12970
+ }, []);
12971
+ /**
12972
+ * The remote's stand over the height: the panes share the stretched stand
12973
+ * over the wire — a pane's peer lands a height different from the local's
12974
+ * (the block's `height` attr's stand the peer's write), so the local's
12975
+ * state adopts it. Own commits land equal to the local's state and stand
12976
+ * no adopt.
12977
+ */
12978
+ useEffect(() => {
12979
+ const attrHeight = block.attrs.height ?? null;
12980
+ if (attrHeight === heightRef.current) return;
12981
+ setHeight(attrHeight);
12982
+ }, [block.attrs]);
12983
+ const onResizePointerDown = (event) => {
12984
+ if (event.button !== 0) return;
12985
+ event.stopPropagation();
12986
+ const wrapper = wrapperRef.current;
12987
+ if (wrapper === null) return;
12988
+ const min = readEmbedMinHeight(wrapper);
12989
+ const rect = wrapper.getBoundingClientRect();
12990
+ event.target.setPointerCapture?.(event.pointerId);
12991
+ resizeSession.current = {
12992
+ pointerId: event.pointerId,
12993
+ startY: event.clientY,
12994
+ startHeight: rect.height,
12995
+ min,
12996
+ lastHeight: rect.height
12997
+ };
12998
+ };
12999
+ const onResizePointerMove = (event) => {
13000
+ const session = resizeSession.current;
13001
+ if (session === null || event.pointerId !== session.pointerId) return;
13002
+ const next = Math.max(session.startHeight + (event.clientY - session.startY), session.min);
13003
+ session.lastHeight = next;
13004
+ setHeight(next);
13005
+ };
13006
+ const endResize = (event) => {
13007
+ const session = resizeSession.current;
13008
+ if (session === null || event.pointerId !== session.pointerId) return;
13009
+ event.target.releasePointerCapture?.(event.pointerId);
13010
+ resizeSession.current = null;
13011
+ const committed = Math.round(session.lastHeight);
13012
+ if (committed <= session.min) {
13013
+ if ((block.attrs.height ?? null) !== null) doc.updateBlock(id, { height: null });
13014
+ } else if (committed !== (block.attrs.height ?? null)) doc.updateBlock(id, { height: committed });
13015
+ };
13016
+ const onResizeKeyDown = (event) => {
13017
+ const wrapper = wrapperRef.current;
13018
+ if (wrapper === null) return;
13019
+ const min = readEmbedMinHeight(wrapper);
13020
+ const delta = event.key === "ArrowDown" ? EMBED_RESIZE_KEY_STEP : event.key === "ArrowUp" ? -16 : null;
13021
+ if (delta === null) return;
13022
+ event.preventDefault();
13023
+ const current = Math.max(heightRef.current ?? min, min);
13024
+ const next = Math.max(current + delta, min);
13025
+ setHeight(next <= min ? null : next);
13026
+ if (next <= min) {
13027
+ if ((block.attrs.height ?? null) !== null) doc.updateBlock(id, { height: null });
13028
+ } else if (next !== (block.attrs.height ?? null)) doc.updateBlock(id, { height: next });
13029
+ };
12882
13030
  const onPointerDown = (event) => {
12883
13031
  if (!panModifier || event.button !== 0) return;
12884
13032
  if (event.target.closest("button") !== null) return;
@@ -12920,71 +13068,151 @@ function EmbedBlock({ id, block }) {
12920
13068
  "data-block-id": id,
12921
13069
  "data-kind": kind,
12922
13070
  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 },
13071
+ style: {
13072
+ ...height !== null ? { height: Math.max(height, tokenMin) } : {},
13073
+ cursor: panModifier ? panSession.current !== null ? "grabbing" : "grab" : void 0
13074
+ },
12924
13075
  onPointerDown,
12925
13076
  onPointerMove,
12926
13077
  onPointerUp: endPan,
12927
13078
  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
- })]
13079
+ children: [
13080
+ isSafeEmbedSrc(kind, src) ? /* @__PURE__ */ jsx("iframe", {
13081
+ "data-slot": "docblocks/embed",
13082
+ src,
13083
+ title: title ?? kind,
13084
+ loading: "lazy",
13085
+ sandbox: sandbox ?? void 0,
13086
+ className: "block h-full w-full border-0",
13087
+ style: {
13088
+ transform: `translate(${view.x}px, ${view.y}px) scale(${view.scale})`,
13089
+ transformOrigin: "0 0",
13090
+ pointerEvents: panModifier ? "none" : "auto"
13091
+ }
13092
+ }) : /* @__PURE__ */ jsx("div", {
13093
+ "data-slot": "docblocks/embed-blocked",
13094
+ className: "\n flex h-full w-full items-center justify-center p-4 text-center text-sm text-(--docblocks-muted-text-color)\n ",
13095
+ children: title ?? kind
13096
+ }),
13097
+ /* @__PURE__ */ jsxs("div", {
13098
+ "data-slot": "docblocks/embed-toolbar",
13099
+ 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 ",
13100
+ children: [
13101
+ /* @__PURE__ */ jsx(Button, {
13102
+ "data-slot": "docblocks/embed-zoom-out",
13103
+ variant: "ghost",
13104
+ size: "icon",
13105
+ "aria-label": t("embed.zoomOut"),
13106
+ className: "size-6 cursor-default rounded-sm text-(--docblocks-text-color) disabled:opacity-50",
13107
+ disabled: view.scale <= EMBED_MIN_SCALE,
13108
+ onMouseDown: preventDefault,
13109
+ onClick: zoomOut,
13110
+ children: /* @__PURE__ */ jsx(Minus, { className: "size-4" })
13111
+ }),
13112
+ /* @__PURE__ */ jsxs("span", {
13113
+ "data-slot": "docblocks/embed-zoom",
13114
+ className: "min-w-10 px-1 text-center text-xs text-(--docblocks-text-color) tabular-nums",
13115
+ children: [Math.round(view.scale * 100), "%"]
13116
+ }),
13117
+ /* @__PURE__ */ jsx(Button, {
13118
+ "data-slot": "docblocks/embed-zoom-in",
13119
+ variant: "ghost",
13120
+ size: "icon",
13121
+ "aria-label": t("embed.zoomIn"),
13122
+ className: "size-6 cursor-default rounded-sm text-(--docblocks-text-color) disabled:opacity-50",
13123
+ disabled: view.scale >= EMBED_MAX_SCALE,
13124
+ onMouseDown: preventDefault,
13125
+ onClick: zoomIn,
13126
+ children: /* @__PURE__ */ jsx(Plus, { className: "size-4" })
13127
+ }),
13128
+ /* @__PURE__ */ jsx(Button, {
13129
+ "data-slot": "docblocks/embed-zoom-reset",
13130
+ variant: "ghost",
13131
+ size: "icon",
13132
+ "aria-label": t("embed.zoomReset"),
13133
+ className: "size-6 cursor-default rounded-sm text-(--docblocks-text-color) disabled:opacity-50",
13134
+ disabled: atIdentity,
13135
+ onMouseDown: preventDefault,
13136
+ onClick: resetView,
13137
+ children: /* @__PURE__ */ jsx(RotateCcw, { className: "size-4" })
13138
+ }),
13139
+ !viewMode && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("div", {
13140
+ className: "mx-0.5 h-4 w-px bg-(--docblocks-divider-border-color)",
13141
+ "aria-hidden": true
13142
+ }), /* @__PURE__ */ jsx(Button, {
13143
+ "data-slot": "docblocks/embed-edit-url",
13144
+ variant: "ghost",
13145
+ size: "icon",
13146
+ "aria-label": t("embed.editUrl"),
13147
+ className: "size-6 cursor-default rounded-sm text-(--docblocks-text-color)",
13148
+ onMouseDown: preventDefault,
13149
+ onClick: openEdit,
13150
+ children: /* @__PURE__ */ jsx(Pen, { className: "size-4" })
13151
+ })] })
13152
+ ]
13153
+ }),
13154
+ !viewMode && editOpen && /* @__PURE__ */ jsxs("div", {
13155
+ "data-slot": "docblocks/embed-edit",
13156
+ 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 ",
13157
+ children: [/* @__PURE__ */ jsxs("div", {
13158
+ className: "flex items-center gap-1",
13159
+ children: [
13160
+ /* @__PURE__ */ jsx(Input, {
13161
+ size: "sm",
13162
+ ref: editInputRef,
13163
+ type: "text",
13164
+ "data-slot": "docblocks/embed-edit-input",
13165
+ className: "h-7 flex-1 rounded-sm",
13166
+ placeholder: t("embed.srcPlaceholder"),
13167
+ value: draft,
13168
+ onChange: (event) => setDraft(event.target.value),
13169
+ onKeyDown: onEditKeyDown
13170
+ }),
13171
+ /* @__PURE__ */ jsx(Button, {
13172
+ "data-slot": "docblocks/embed-edit-save",
13173
+ variant: "ghost",
13174
+ size: "icon",
13175
+ "aria-label": t("embed.saveUrl"),
13176
+ className: "size-7 shrink-0 cursor-default rounded-sm text-(--docblocks-text-color) disabled:opacity-50",
13177
+ disabled: !canSave,
13178
+ onMouseDown: preventDefault,
13179
+ onClick: commitEdit,
13180
+ children: /* @__PURE__ */ jsx(Check, { className: "size-4" })
13181
+ }),
13182
+ /* @__PURE__ */ jsx(Button, {
13183
+ "data-slot": "docblocks/embed-edit-cancel",
13184
+ variant: "ghost",
13185
+ size: "icon",
13186
+ "aria-label": t("embed.cancelEdit"),
13187
+ className: "size-7 shrink-0 cursor-default rounded-sm text-(--docblocks-text-color)",
13188
+ onMouseDown: preventDefault,
13189
+ onClick: closeEdit,
13190
+ children: /* @__PURE__ */ jsx(X, { className: "size-4" })
13191
+ })
13192
+ ]
13193
+ }), editHint !== null && /* @__PURE__ */ jsx("p", {
13194
+ "data-slot": "docblocks/embed-edit-hint",
13195
+ className: "px-1 text-xs text-destructive",
13196
+ children: editHint
13197
+ })]
13198
+ }),
13199
+ !viewMode && /* @__PURE__ */ jsx("div", {
13200
+ "data-slot": "docblocks/embed-resize",
13201
+ role: "slider",
13202
+ "aria-orientation": "vertical",
13203
+ "aria-valuemin": tokenMin,
13204
+ "aria-valuenow": Math.max(height ?? tokenMin, tokenMin),
13205
+ "aria-label": t("embed.resizeHeight"),
13206
+ tabIndex: 0,
13207
+ 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 ",
13208
+ onPointerDown: onResizePointerDown,
13209
+ onPointerMove: onResizePointerMove,
13210
+ onPointerUp: endResize,
13211
+ onPointerCancel: endResize,
13212
+ onKeyDown: onResizeKeyDown,
13213
+ children: /* @__PURE__ */ jsx("div", { className: "mb-0.5 h-0.5 w-10 rounded-full bg-(--docblocks-divider-border-color)" })
13214
+ })
13215
+ ]
12988
13216
  });
12989
13217
  }
12990
13218
  //#endregion
@@ -18092,7 +18320,8 @@ function TextBlock({ id, block, tag, nested, collapsible = false, flat = false,
18092
18320
  size: "icon",
18093
18321
  onClick: (event) => deleteBlock(event.currentTarget, id),
18094
18322
  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"),
18323
+ 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"),
18324
+ style: { right: "var(--docblocks-gutter-left)" },
18096
18325
  children: /* @__PURE__ */ jsx(Trash, {})
18097
18326
  }),
18098
18327
  block.type === "list-item" && block.attrs.list === "task" && /* @__PURE__ */ jsx(Checkbox, {
@@ -18660,67 +18889,36 @@ function TitleMenu({ slot, ariaLabel, open, onOpenChange, onCommit }) {
18660
18889
  //#endregion
18661
18890
  //#region src/canvas/FrameBlock.tsx
18662
18891
  /**
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 }) {
18892
+ * The canvas frame's renderer: the frame's boundary — the rectangle that
18893
+ * defines the region the page-frame block renders. The CanvasItem's wrapper
18894
+ * carries the geometry + the selection + the move/resize preview; the box
18895
+ * stands the container's thin rounded border (the selection's color on the
18896
+ * selection) over the canvas's own background. The frame renders the
18897
+ * boundary only — no content copy: the canvas's own figures stand behind the
18898
+ * transparent box (the canvas frame is a region marker, not a window), and
18899
+ * the content's mirror (the surface's children clipped to the frame's
18900
+ * region) stands on the page-frame block's side (FramePageBlock). The
18901
+ * frame's title chip (the container's idiom) stands over the shared
18902
+ * CanvasTitleChip.
18903
+ */
18904
+ function FrameBlock({ id, selected = false }) {
18687
18905
  const { t } = useTranslation("canvas");
18688
- const { parentId, parentType } = useBlockContext();
18906
+ const { parentType } = useBlockContext();
18689
18907
  const block = useBlock(id);
18690
18908
  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
18909
  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
18910
  return /* @__PURE__ */ jsxs("div", {
18711
18911
  "data-slot": "docblocks/frame",
18712
18912
  "data-block-id": id,
18713
18913
  className: "h-full w-full",
18714
18914
  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
- })
18915
+ "data-slot": "docblocks/frame-boundary",
18916
+ className: "absolute inset-0 rounded-(--docblocks-radius)",
18917
+ style: {
18918
+ boxShadow: `inset 0 0 0 1px ${selected ? selectedColor : "var(--docblocks-canvas-group-border-color)"}`,
18919
+ outline: selected ? `var(--docblocks-canvas-selection-border-width) solid ${selectedColor}` : void 0,
18920
+ outlineOffset: 1
18921
+ }
18724
18922
  }), /* @__PURE__ */ jsx(CanvasTitleChip, {
18725
18923
  id,
18726
18924
  title: block.attrs?.title ?? "",
@@ -22768,7 +22966,7 @@ const CANVAS_ITEM_LABEL_KEYS = {
22768
22966
  * order, the line can stand over or under a figure); `interactive`
22769
22967
  * toggles the line's hit path (the page view's surface stands it inert).
22770
22968
  */
22771
- const CanvasItem = memo(function CanvasItem({ id, pendingConnector = null, movePreview = null, resizeBox = null, interactive = true, kbFocused = false, frameChain = EMPTY_FRAME_CHAIN }) {
22969
+ const CanvasItem = memo(function CanvasItem({ id, pendingConnector = null, movePreview = null, resizeBox = null, interactive = true, kbFocused = false }) {
22772
22970
  const { t } = useTranslation("canvas");
22773
22971
  const { store, renderers } = useEditorContext();
22774
22972
  const { siblings } = useBlockContext();
@@ -22803,10 +23001,7 @@ const CanvasItem = memo(function CanvasItem({ id, pendingConnector = null, moveP
22803
23001
  tag: "div"
22804
23002
  }) : block.type === "frame" ? /* @__PURE__ */ jsx(FrameBlock, {
22805
23003
  id,
22806
- movePreview,
22807
- resizeBox,
22808
- selected,
22809
- frameChain
23004
+ selected
22810
23005
  }) : block.type === "group" ? /* @__PURE__ */ jsx(GroupBlock, {
22811
23006
  id,
22812
23007
  block,
@@ -22853,7 +23048,7 @@ const CanvasItem = memo(function CanvasItem({ id, pendingConnector = null, moveP
22853
23048
  style,
22854
23049
  children: content
22855
23050
  });
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);
23051
+ }, (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
23052
  //#endregion
22858
23053
  //#region src/canvas/ConnectorArrowDefs.tsx
22859
23054
  /** The surface's arrowhead defs (the hidden 0x0 svg): the marker ids stand
@@ -22968,7 +23163,7 @@ function ConnectorArrowDefs({ id }) {
22968
23163
  * context providers are not DOM nodes, so several surfaces merge onto the
22969
23164
  * same canvas in root order (the later surface paints on top: the DOM
22970
23165
  * order is the z-order). */
22971
- function SurfaceLayer({ id, children, pendingConnector, movePreview, resizePreview, endpointDrag, endpointPreview, showPorts, endpointSnap, kbFocusId = null, inert = false, excludedIds, frameChain }) {
23166
+ function SurfaceLayer({ id, children, pendingConnector, movePreview, resizePreview, endpointDrag, endpointPreview, showPorts, endpointSnap, kbFocusId = null, inert = false, excludedIds }) {
22972
23167
  const contextValue = useBlockContextValue(id, children, "surface");
22973
23168
  const session = useMemo(() => ({
22974
23169
  movePreview,
@@ -23001,8 +23196,7 @@ function SurfaceLayer({ id, children, pendingConnector, movePreview, resizePrevi
23001
23196
  pendingConnector,
23002
23197
  movePreview: movePreview !== null && movePreview.ids.includes(childId) ? movePreview : null,
23003
23198
  resizeBox: resizePreview !== null && resizePreview.id === childId ? resizePreview.box : null,
23004
- kbFocused: kbFocusId === childId,
23005
- frameChain
23199
+ kbFocused: kbFocusId === childId
23006
23200
  }, childId)),
23007
23201
  showPorts && /* @__PURE__ */ jsx(PortDots, {
23008
23202
  id,
@@ -23097,8 +23291,7 @@ function PortDots({ id, snapped }) {
23097
23291
  //#endregion
23098
23292
  //#region src/canvas/FrameWindowContent.tsx
23099
23293
  const EMPTY_IDS$1 = [];
23100
- const EMPTY_CHAIN = /* @__PURE__ */ new Set();
23101
- function FrameWindowContent({ surfaceId, offset, excludedIds = EMPTY_IDS$1, frameChain = EMPTY_CHAIN, className }) {
23294
+ function FrameWindowContent({ surfaceId, offset, excludedIds = EMPTY_IDS$1, className }) {
23102
23295
  const surface = useBlock(surfaceId);
23103
23296
  const children = surface !== void 0 && surface.type === "surface" ? surface.children ?? EMPTY_IDS$1 : EMPTY_IDS$1;
23104
23297
  return /* @__PURE__ */ jsx("div", {
@@ -23124,8 +23317,7 @@ function FrameWindowContent({ surfaceId, offset, excludedIds = EMPTY_IDS$1, fram
23124
23317
  endpointSnap: null,
23125
23318
  kbFocusId: null,
23126
23319
  inert: true,
23127
- excludedIds,
23128
- frameChain
23320
+ excludedIds
23129
23321
  }), /* @__PURE__ */ jsx(ConnectorLabelChips, { children })]
23130
23322
  })
23131
23323
  });
@@ -23134,11 +23326,13 @@ function FrameWindowContent({ surfaceId, offset, excludedIds = EMPTY_IDS$1, fram
23134
23326
  //#region src/blocks/FramePageBlock.tsx
23135
23327
  /**
23136
23328
  * 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
23329
+ * canvas frame's surface — the content's mirror (FrameWindowContent over
23330
+ * the window's offset; the box's background the canvas's own background +
23331
+ * the dot grid phase-aligned to the world's grid; the container's thin
23332
+ * rounded border), inlined in the page flow at
23333
+ * 1:1 (no fit/zoom controls — the v1 boundary). The canvas frame stands the
23334
+ * boundary only (a region marker) — this page block is the sole renderer
23335
+ * of the frame's content. The box's height is the
23142
23336
  * referenced frame's 1:1; the width is the frame's width clipped to the
23143
23337
  * row's measured width (the window's center stays at the frame's center —
23144
23338
  * a narrow row clips the wide window horizontally, the offset and the dot
@@ -23232,7 +23426,6 @@ function FramePageWindow({ id, frameId, surfaceId, referenced }) {
23232
23426
  };
23233
23427
  const title = referenced.attrs?.title ?? "";
23234
23428
  const excludedIds = useMemo(() => [frameId], [frameId]);
23235
- const frameChain = useMemo(() => /* @__PURE__ */ new Set([frameId]), [frameId]);
23236
23429
  return /* @__PURE__ */ jsx("div", {
23237
23430
  ref: rowRef,
23238
23431
  "data-slot": "docblocks/frame-page",
@@ -23250,8 +23443,7 @@ function FramePageWindow({ id, frameId, surfaceId, referenced }) {
23250
23443
  children: /* @__PURE__ */ jsx(FrameWindowContent, {
23251
23444
  surfaceId,
23252
23445
  offset,
23253
- excludedIds,
23254
- frameChain
23446
+ excludedIds
23255
23447
  })
23256
23448
  })
23257
23449
  });
@@ -27766,6 +27958,15 @@ function positionAtPoint(doc, root, x, y) {
27766
27958
  * soft-break lines) itself — a permanent capture listener, independent of
27767
27959
  * the session (the real pointer's third press starts no session: the
27768
27960
  * pointer's detail stands 3 and the session guard returns).
27961
+ *
27962
+ * The iframe's focus exception: a press while the document's focus stands
27963
+ * in a frame (the embed's iframe) tears the session down and lets the
27964
+ * mousedown's default stand — the default's focus move out of the frame
27965
+ * is the editor's document's focus, and suppressing it would seat the
27966
+ * caret over the unfocused document (no visible caret, the keys' no-
27967
+ * stand). The browser's native caret's seat stands the press; a double
27968
+ * press over the focused frame stands the same stand over the detail's
27969
+ * check.
27769
27970
  */
27770
27971
  function useCrossDrag(rootRef, doc) {
27771
27972
  const sessionRef = useRef(null);
@@ -27799,12 +28000,19 @@ function useCrossDrag(rootRef, doc) {
27799
28000
  if (session === null || root === null || event.button !== 0) return;
27800
28001
  const target = event.target instanceof Element ? event.target : null;
27801
28002
  if (target === null || !root.contains(target)) return;
27802
- if (event.detail >= 2) {
28003
+ const tearDown = () => {
27803
28004
  sessionRef.current = null;
27804
28005
  window.removeEventListener("pointermove", move);
27805
28006
  window.removeEventListener("pointerup", end);
27806
28007
  window.removeEventListener("pointercancel", end);
27807
28008
  window.removeEventListener("mousedown", mousedown, true);
28009
+ };
28010
+ if (event.detail >= 2) {
28011
+ tearDown();
28012
+ return;
28013
+ }
28014
+ if (document.activeElement instanceof HTMLIFrameElement) {
28015
+ tearDown();
27808
28016
  return;
27809
28017
  }
27810
28018
  event.preventDefault();
@@ -27846,6 +28054,7 @@ function useCrossDrag(rootRef, doc) {
27846
28054
  if (target === null || !root.contains(target)) return;
27847
28055
  const inner = target.closest("[data-slot=\"docblocks/block-content\"]");
27848
28056
  if (inner === null || inner.closest("[data-slot=\"docblocks/editor\"]") !== root) return;
28057
+ if (document.activeElement instanceof HTMLIFrameElement) return;
27849
28058
  const blockId = inner.dataset.blockId;
27850
28059
  const head = locatePosition(inner, 0);
27851
28060
  const tail = locatePosition(inner, doc.getTextLength(blockId));
@@ -28357,7 +28566,8 @@ const RowItem = memo(function RowItem({ node, layoutSlide, isDragged, animated,
28357
28566
  size: "icon",
28358
28567
  onClick: (event) => deleteBlock(event.currentTarget, node.id),
28359
28568
  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"),
28569
+ 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"),
28570
+ style: { right: "var(--docblocks-gutter-left)" },
28361
28571
  children: /* @__PURE__ */ jsx(Trash, {})
28362
28572
  }),
28363
28573
  /* @__PURE__ */ jsx(BlockNodeView, {