@bendyline/squisq 2.7.0 → 2.8.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.
@@ -2,28 +2,38 @@ import {
2
2
  SHAPE_NAMES,
3
3
  buildChartData,
4
4
  buildNarrationScript,
5
+ buildRegistry,
5
6
  coerceTemplateParams,
6
7
  deriveTemplateInputs,
8
+ expandPersistentLayers,
7
9
  extractBodyPlainText,
8
10
  extractRichListItems,
9
11
  flattenBlocks,
10
12
  flattenRenderableBlocks,
11
13
  getBlockBodyText,
12
14
  getPinnedBlockMeta,
15
+ getThemeFont,
16
+ hasTemplate,
13
17
  isDataFence,
14
18
  isShapeName,
15
19
  lintTemplateParams,
16
20
  markdownToDoc,
21
+ materializeBlockLayersWithRuntime,
17
22
  nearestName,
18
23
  normalizeShapeKind,
19
24
  parseDataFence,
20
25
  parseNarrationTimingJson,
21
26
  parseStandaloneAnnotation,
27
+ placeLayersInRect,
22
28
  readCustomThemesFromFrontmatter,
29
+ resolvePersistentLayers,
23
30
  templateRegistry,
24
31
  writeCustomTemplatesToFrontmatter,
25
32
  writeCustomThemesToFrontmatter
26
- } from "./chunk-GSJEGMKF.js";
33
+ } from "./chunk-V6NV4GDE.js";
34
+ import {
35
+ iconMarker
36
+ } from "./chunk-7ZAAICW4.js";
27
37
  import {
28
38
  ASCII_TREE_VOCAB,
29
39
  ASCII_VOCAB,
@@ -49,11 +59,18 @@ import {
49
59
  FRONTMATTER_CUSTOM_TEMPLATES_KEY,
50
60
  FRONTMATTER_CUSTOM_THEMES_KEY,
51
61
  hexHueDegrees,
62
+ oklchDarken,
63
+ oklchLighten,
64
+ relativeLuminance,
52
65
  resolveFontFamily,
53
- resolveTheme
66
+ resolveTheme,
67
+ withAlpha
54
68
  } from "./chunk-SBAX4ZPO.js";
55
69
  import {
56
70
  VIEWPORT_PRESETS,
71
+ calculateFontScale,
72
+ createTemplateContext,
73
+ getViewportOrientation,
57
74
  isTemplateBlock
58
75
  } from "./chunk-BAOV476U.js";
59
76
  import {
@@ -662,6 +679,332 @@ function removeTransitionParams(attrs) {
662
679
  };
663
680
  }
664
681
 
682
+ // src/doc/buildPreviewDoc.ts
683
+ function extractRichText(node) {
684
+ if (node.type === "inlineIcon") {
685
+ const icon = node;
686
+ return iconMarker(icon.family, icon.name);
687
+ }
688
+ if ("value" in node && typeof node.value === "string") {
689
+ return node.value;
690
+ }
691
+ const children = getChildren(node);
692
+ const separator = node.type === "list" || node.type === "listItem" ? "\n" : "";
693
+ return children.map(extractRichText).join(separator);
694
+ }
695
+ function extractBodyText(contents) {
696
+ if (!contents || contents.length === 0) return "";
697
+ const parts = [];
698
+ for (const node of contents) {
699
+ if (node.type === "code" && node.lang?.trim().toLowerCase() === "mermaid") continue;
700
+ parts.push(extractRichText(node));
701
+ }
702
+ return parts.join("\n").trim();
703
+ }
704
+ function parseDim(raw) {
705
+ if (raw === void 0) return void 0;
706
+ const n = parseFloat(raw);
707
+ return Number.isFinite(n) && n > 0 ? n : void 0;
708
+ }
709
+ function extractBlockImages(contents) {
710
+ if (!contents || contents.length === 0) return [];
711
+ const images = [];
712
+ function walkHtml(node) {
713
+ if (!node || typeof node !== "object") return;
714
+ const n = node;
715
+ if (n.type === "htmlElement" && n.tagName.toLowerCase() === "img") {
716
+ const attrs = n.attributes;
717
+ const src = attrs?.src;
718
+ if (typeof src === "string" && src) {
719
+ images.push({
720
+ src,
721
+ alt: typeof attrs?.alt === "string" ? attrs.alt : "",
722
+ width: parseDim(attrs?.width),
723
+ height: parseDim(attrs?.height)
724
+ });
725
+ }
726
+ }
727
+ if (Array.isArray(n.children)) {
728
+ for (const child of n.children) walkHtml(child);
729
+ }
730
+ }
731
+ function walk(node) {
732
+ if ("type" in node && node.type === "image" && "url" in node) {
733
+ const img = node;
734
+ if (img.url) {
735
+ images.push({ src: img.url, alt: img.alt ?? "" });
736
+ }
737
+ }
738
+ if ("type" in node && (node.type === "htmlBlock" || node.type === "htmlInline")) {
739
+ const html = node;
740
+ for (const child of html.htmlChildren ?? []) walkHtml(child);
741
+ }
742
+ for (const child of getChildren(node)) {
743
+ walk(child);
744
+ }
745
+ }
746
+ for (const node of contents) {
747
+ walk(node);
748
+ }
749
+ return images;
750
+ }
751
+ function collectAllDocImages(blocks) {
752
+ const seen = /* @__PURE__ */ new Set();
753
+ const images = [];
754
+ function walkBlocks(blockList) {
755
+ for (const block of blockList) {
756
+ for (const img of extractBlockImages(block.contents)) {
757
+ if (!seen.has(img.src)) {
758
+ seen.add(img.src);
759
+ images.push(img);
760
+ }
761
+ }
762
+ if (block.children) {
763
+ walkBlocks(block.children);
764
+ }
765
+ }
766
+ }
767
+ walkBlocks(blocks);
768
+ return images;
769
+ }
770
+ function extractListItems(contents) {
771
+ if (!contents) return [];
772
+ const items = [];
773
+ for (const node of contents) {
774
+ if (node.type === "list") {
775
+ for (const item of node.children) {
776
+ const text = extractPlainText(item).trim();
777
+ if (text) items.push(text);
778
+ }
779
+ }
780
+ }
781
+ return items;
782
+ }
783
+ function getTemplateDefaults(templateName, headingText, block) {
784
+ const body = extractBodyText(block.contents);
785
+ switch (templateName) {
786
+ case "statHighlight":
787
+ return deriveTemplateInputs(templateName, headingText, block.contents) ?? {
788
+ stat: headingText,
789
+ description: body || headingText
790
+ };
791
+ case "quote":
792
+ return { quote: body || headingText };
793
+ case "fullBleedQuote":
794
+ case "pullQuote":
795
+ return deriveTemplateInputs(templateName, headingText, block.contents) ?? {
796
+ text: body || headingText
797
+ };
798
+ case "factCard":
799
+ return { fact: headingText, explanation: body || headingText };
800
+ case "comparisonBar":
801
+ return { leftLabel: "A", leftValue: 60, rightLabel: "B", rightValue: 40 };
802
+ case "list": {
803
+ const items = extractListItems(block.contents);
804
+ return { items: items.length > 0 ? items : ["Item 1", "Item 2", "Item 3"] };
805
+ }
806
+ case "definitionCard":
807
+ return { term: headingText, definition: body || headingText };
808
+ case "dateEvent":
809
+ return { date: headingText, description: body || headingText };
810
+ case "leftFeature":
811
+ case "rightFeature": {
812
+ const images = extractBlockImages(block.contents);
813
+ const img = images[0];
814
+ return {
815
+ imageSrc: img?.src ?? "",
816
+ imageAlt: img?.alt || headingText,
817
+ imageWidth: img?.width,
818
+ imageHeight: img?.height,
819
+ title: headingText,
820
+ body: body || headingText
821
+ };
822
+ }
823
+ default:
824
+ return {};
825
+ }
826
+ }
827
+ function blockToSlide(block, index, knownTemplates, documentTitle) {
828
+ const headingText = block.sourceHeading ? extractPlainText(block.sourceHeading) : block.title || documentTitle || "";
829
+ const implicitSectionHeader = block.template === "sectionHeader" && block.autoTemplate !== true && !!block.sourceHeading && !block.sourceHeading.templateAnnotation?.template;
830
+ const requestedTemplate = implicitSectionHeader ? "content" : block.template ?? "content";
831
+ const isCustomTemplate = knownTemplates?.has(requestedTemplate) ?? false;
832
+ const recognized = hasTemplate(requestedTemplate) || isCustomTemplate;
833
+ const template = recognized ? requestedTemplate : "sectionHeader";
834
+ const defaults = getTemplateDefaults(template, headingText, block);
835
+ const templateOverrides = omitStringBlockMeta(block.templateOverrides);
836
+ const coercedTemplateOverrides = templateOverrides ? coerceTemplateParams(template, templateOverrides).input : void 0;
837
+ const {
838
+ id: _id,
839
+ startTime: _st,
840
+ duration: _d,
841
+ audioSegment: _as,
842
+ layers: _l,
843
+ transition: _tr,
844
+ template: _t,
845
+ title: _ti,
846
+ children: _c,
847
+ contents: _co,
848
+ sourceHeading: _sh,
849
+ templateOverrides: _to,
850
+ templateData: _td,
851
+ ...extraFields
852
+ } = block;
853
+ return {
854
+ id: block.id,
855
+ template,
856
+ duration: block.duration,
857
+ audioSegment: 0,
858
+ // Respect the block's authored transition (set via the toolbar / on-canvas
859
+ // properties palette → `{…}` block attrs). Only fall back to a default fade
860
+ // for blocks past the first when the author hasn't chosen one; the first
861
+ // block has no previous slide to transition in from.
862
+ transition: block.transition ?? (index > 0 ? { type: "fade", duration: 0.5 } : void 0),
863
+ title: headingText,
864
+ // Preserve body nodes on every slide. Built-in templates ignore this
865
+ // structural field, while the canonical materializer uses it to retain
866
+ // authored rich elements (Mermaid fences today; other media can follow)
867
+ // independently of the selected visual template.
868
+ ...block.contents ? { contents: block.contents } : {},
869
+ // Custom templates additionally consume child blocks through tokens.
870
+ ...isCustomTemplate && block.children ? { children: block.children } : {},
871
+ ...defaults,
872
+ ...extraFields,
873
+ // Structured body data (```json data fences, GFM tables for dataTable)
874
+ // carries typed values; `{[…]}` string overrides win last so an explicit
875
+ // annotation param can still pin any field.
876
+ //
877
+ // Block-meta keys (transition, startTime, duration, …) are the exception:
878
+ // they were already coerced to typed block fields above (e.g.
879
+ // `block.transition` → `{ type, duration, direction }`). Their raw string
880
+ // form also rides along in `templateData`/`templateOverrides` because the
881
+ // author wrote them inside `{[…]}`; left un-stripped, that string would
882
+ // spread back over the typed value here and clobber it — turning
883
+ // `transition=vortex` into the string `"vortex"`, which the player can't
884
+ // animate. Omit them from the content spreads so the typed fields win.
885
+ ...omitBlockMeta(block.templateData),
886
+ ...coercedTemplateOverrides
887
+ };
888
+ }
889
+ var BLOCK_META_KEYS = new Set(Object.keys(KNOWN_BLOCK_META_KEYS));
890
+ function omitBlockMeta(data) {
891
+ if (!data) return data;
892
+ let hit = false;
893
+ const out = {};
894
+ for (const key of Object.keys(data)) {
895
+ if (BLOCK_META_KEYS.has(key)) {
896
+ hit = true;
897
+ continue;
898
+ }
899
+ out[key] = data[key];
900
+ }
901
+ return hit ? out : data;
902
+ }
903
+ function omitStringBlockMeta(data) {
904
+ if (!data) return data;
905
+ let hit = false;
906
+ const out = {};
907
+ for (const key of Object.keys(data)) {
908
+ if (BLOCK_META_KEYS.has(key)) {
909
+ hit = true;
910
+ continue;
911
+ }
912
+ out[key] = data[key];
913
+ }
914
+ return hit ? out : data;
915
+ }
916
+ var IMAGE_MOTIONS = [
917
+ "zoomIn",
918
+ "zoomOut",
919
+ "panLeft",
920
+ "panRight"
921
+ ];
922
+ function documentTitleFromFileName(fileName) {
923
+ if (!fileName) return "";
924
+ const base = fileName.split(/[\\/]/).pop() ?? "";
925
+ return base.replace(/\.[^.]+$/, "").trim();
926
+ }
927
+ function resolveDocumentTitle(doc, provided) {
928
+ const frontmatterTitle = doc.frontmatter?.title;
929
+ if (typeof frontmatterTitle === "string" && frontmatterTitle.trim()) {
930
+ return frontmatterTitle.trim();
931
+ }
932
+ return provided?.trim() ?? "";
933
+ }
934
+ function buildPreviewDoc(doc, options) {
935
+ const flat = flattenRenderableBlocks(doc.blocks);
936
+ const allImages = collectAllDocImages(doc.blocks);
937
+ const usedImageSrcs = /* @__PURE__ */ new Set();
938
+ const knownTemplates = doc.customTemplates ? new Set(doc.customTemplates.map((d) => d.name)) : void 0;
939
+ const documentTitle = resolveDocumentTitle(doc, options?.documentTitle);
940
+ const slides = [];
941
+ let motionIndex = 0;
942
+ for (let i = 0; i < flat.length; i++) {
943
+ const block = flat[i];
944
+ const blockImages = extractBlockImages(block.contents);
945
+ const slide = blockToSlide(block, i, knownTemplates, documentTitle);
946
+ if (blockImages.length > 0 && slide.template === "sectionHeader") {
947
+ const img = blockImages[0];
948
+ usedImageSrcs.add(img.src);
949
+ slide.template = "imageWithCaption";
950
+ slide.imageSrc = img.src;
951
+ slide.imageAlt = img.alt;
952
+ slide.caption = slide.title;
953
+ slide.captionPosition = "bottom";
954
+ slide.ambientMotion = IMAGE_MOTIONS[motionIndex++ % IMAGE_MOTIONS.length];
955
+ } else if (blockImages.length > 0) {
956
+ const img = blockImages[0];
957
+ usedImageSrcs.add(img.src);
958
+ if (!slide.accentImage) {
959
+ slide.accentImage = {
960
+ src: img.src,
961
+ alt: img.alt,
962
+ position: "left-strip",
963
+ ambientMotion: IMAGE_MOTIONS[motionIndex++ % IMAGE_MOTIONS.length]
964
+ };
965
+ }
966
+ }
967
+ slides.push(slide);
968
+ }
969
+ const unusedImages = options?.interleaveImages ?? true ? allImages.filter((img) => !usedImageSrcs.has(img.src)) : [];
970
+ if (unusedImages.length > 0 && slides.length > 0) {
971
+ const interval = Math.max(2, Math.floor(slides.length / (unusedImages.length + 1)));
972
+ let insertOffset = 0;
973
+ for (let imgIdx = 0; imgIdx < unusedImages.length; imgIdx++) {
974
+ const insertAt = Math.min((imgIdx + 1) * interval + insertOffset, slides.length);
975
+ const img = unusedImages[imgIdx];
976
+ slides.splice(insertAt, 0, {
977
+ id: `img-interleave-${imgIdx}`,
978
+ template: "imageWithCaption",
979
+ duration: 5,
980
+ audioSegment: 0,
981
+ imageSrc: img.src,
982
+ imageAlt: img.alt,
983
+ ambientMotion: IMAGE_MOTIONS[motionIndex++ % IMAGE_MOTIONS.length],
984
+ transition: { type: "fade", duration: 0.5 }
985
+ });
986
+ insertOffset++;
987
+ }
988
+ }
989
+ let t = 0;
990
+ for (const slide of slides) {
991
+ slide.startTime = t;
992
+ t += slide.duration;
993
+ }
994
+ const audio = doc.audio?.segments?.length > 0 ? doc.audio : {
995
+ segments: t > 0 ? [{ src: "", name: "preview", duration: t, startTime: 0 }] : []
996
+ };
997
+ return {
998
+ // Preserve document-wide capabilities (custom themes, persistent layers,
999
+ // scheduled media, frontmatter, captions, and future schema fields).
1000
+ // Preview preparation should replace only the slide/timing projection.
1001
+ ...doc,
1002
+ duration: t,
1003
+ blocks: slides,
1004
+ audio
1005
+ };
1006
+ }
1007
+
665
1008
  // src/doc/resolveDocTheme.ts
666
1009
  function resolveThemeForDoc(doc, explicitId, registry) {
667
1010
  const id = explicitId ?? doc?.themeId ?? readFrontmatterThemeId(doc?.frontmatter);
@@ -1914,21 +2257,29 @@ var PAGE_BASE_CSS = `
1914
2257
  .squisq-page-items { list-style: none; margin: 0; padding: 0; counter-reset: squisq-item; max-width: 40em; }
1915
2258
  .squisq-page-items li {
1916
2259
  counter-increment: squisq-item;
1917
- position: relative;
1918
- padding: 0.9em 0 0.9em 3.4em;
2260
+ /* Grid + baseline alignment keeps the marker locked to the first text line
2261
+ regardless of marker size, body font, or line-height \u2014 an absolutely
2262
+ positioned marker with a fixed top offset drifts as soon as either
2263
+ changes. */
2264
+ display: grid;
2265
+ grid-template-columns: 3.4em 1fr;
2266
+ align-items: baseline;
2267
+ padding: 0.9em 0;
1919
2268
  font-size: 1.1rem;
1920
2269
  }
1921
2270
  .squisq-page-items li + li { border-top: 1px solid var(--squisq-page-divider-color); }
1922
2271
  .squisq-page-items li::before {
1923
2272
  content: counter(squisq-item, decimal-leading-zero);
1924
- position: absolute;
1925
- left: 0;
1926
- top: 0.85em;
1927
2273
  font-family: var(--squisq-page-title-font);
1928
2274
  font-weight: 700;
1929
2275
  color: var(--squisq-page-accent);
1930
2276
  font-size: 1.15em;
1931
2277
  }
2278
+ /* Item bodies are rendered markdown: neutralize UA paragraph margins (which
2279
+ would otherwise push the first line below the marker) and space blocks. */
2280
+ .squisq-page-item-body { min-width: 0; }
2281
+ .squisq-page-item-body > * { margin: 0; }
2282
+ .squisq-page-item-body > * + * { margin-top: 0.6em; }
1932
2283
  .squisq-page[data-numerals='mono'] .squisq-page-items li::before { font-family: var(--squisq-page-mono-font); }
1933
2284
  .squisq-page-items-title { font-size: 1.9rem; }
1934
2285
 
@@ -2101,6 +2452,1186 @@ ${varLines}
2101
2452
  ${PAGE_BASE_CSS}`;
2102
2453
  }
2103
2454
 
2455
+ // src/doc/dashboard/dashboardZoom.ts
2456
+ var DASHBOARD_ZOOM_LEVELS = [1, 1.5, 2];
2457
+ function normalizeDashboardZoom(value) {
2458
+ const raw = typeof value === "number" ? value : typeof value === "string" ? Number(value.trim().replace(/%$/, "")) : Number.NaN;
2459
+ if (!Number.isFinite(raw)) return void 0;
2460
+ const multiplier = raw > 3 ? raw / 100 : raw;
2461
+ return DASHBOARD_ZOOM_LEVELS.find((level) => Math.abs(level - multiplier) < 1e-3);
2462
+ }
2463
+ var ZOOM_ELIGIBLE_TEMPLATES = /* @__PURE__ */ new Set([
2464
+ "content",
2465
+ "list",
2466
+ "quote",
2467
+ "definitionCard",
2468
+ "factCard",
2469
+ "dateEvent"
2470
+ ]);
2471
+ var ZOOM_200_MAX_CHARS = 160;
2472
+ var ZOOM_150_MAX_CHARS = 360;
2473
+ function desiredCellZoom(candidate) {
2474
+ if (!candidate.template || !ZOOM_ELIGIBLE_TEMPLATES.has(candidate.template)) return 1;
2475
+ if (candidate.textLength < ZOOM_200_MAX_CHARS) return 2;
2476
+ if (candidate.textLength < ZOOM_150_MAX_CHARS) return 1.5;
2477
+ return 1;
2478
+ }
2479
+ function resolveDashboardZooms(candidates, mode) {
2480
+ const levels = candidates.map((candidate) => {
2481
+ if (candidate.explicit !== void 0) return candidate.explicit;
2482
+ return mode === "auto" ? desiredCellZoom(candidate) : 1;
2483
+ });
2484
+ const boostVotes = /* @__PURE__ */ new Map();
2485
+ candidates.forEach((candidate, index) => {
2486
+ const level = levels[index];
2487
+ if (level === 1) return;
2488
+ const weight = candidate.explicit !== void 0 ? 2 : 1;
2489
+ boostVotes.set(level, (boostVotes.get(level) ?? 0) + weight);
2490
+ });
2491
+ if (boostVotes.size <= 1) return levels;
2492
+ let winner = 2;
2493
+ let winnerVotes = -1;
2494
+ for (const [level, votes] of boostVotes) {
2495
+ if (votes > winnerVotes || votes === winnerVotes && level > winner) {
2496
+ winner = level;
2497
+ winnerVotes = votes;
2498
+ }
2499
+ }
2500
+ return candidates.map((candidate, index) => {
2501
+ if (candidate.explicit !== void 0) return candidate.explicit;
2502
+ return levels[index] === 1 ? 1 : winner;
2503
+ });
2504
+ }
2505
+
2506
+ // src/doc/dashboard/DashboardLayout.ts
2507
+ var MAX_CELLS = 32;
2508
+ var MAX_BLOCK_ASSIGNMENT = 64;
2509
+ var NAME_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
2510
+ function parsePercent(value) {
2511
+ if (typeof value === "number") {
2512
+ return Number.isFinite(value) ? value : void 0;
2513
+ }
2514
+ if (typeof value !== "string") return void 0;
2515
+ const trimmed = value.trim().replace(/%$/, "");
2516
+ if (trimmed.length === 0) return void 0;
2517
+ const parsed = Number(trimmed);
2518
+ return Number.isFinite(parsed) ? parsed : void 0;
2519
+ }
2520
+ function formatPercent(value) {
2521
+ const rounded = Math.round(value * 1e3) / 1e3;
2522
+ return `${rounded}%`;
2523
+ }
2524
+ function validateCell(input, path, errors) {
2525
+ if (!input || typeof input !== "object") {
2526
+ errors.push({ path, message: "cell must be an object" });
2527
+ return void 0;
2528
+ }
2529
+ const cell = input;
2530
+ const x = parsePercent(cell.x);
2531
+ const y = parsePercent(cell.y);
2532
+ const width = parsePercent(cell.width);
2533
+ const height = parsePercent(cell.height);
2534
+ if (x === void 0 || x < 0 || x > 100) {
2535
+ errors.push({ path: `${path}.x`, message: "x must be a percent between 0 and 100" });
2536
+ return void 0;
2537
+ }
2538
+ if (y === void 0 || y < 0 || y > 100) {
2539
+ errors.push({ path: `${path}.y`, message: "y must be a percent between 0 and 100" });
2540
+ return void 0;
2541
+ }
2542
+ if (width === void 0 || width <= 0 || width > 100) {
2543
+ errors.push({ path: `${path}.width`, message: "width must be a percent greater than 0" });
2544
+ return void 0;
2545
+ }
2546
+ if (height === void 0 || height <= 0 || height > 100) {
2547
+ errors.push({ path: `${path}.height`, message: "height must be a percent greater than 0" });
2548
+ return void 0;
2549
+ }
2550
+ const out = {
2551
+ x: formatPercent(x),
2552
+ y: formatPercent(y),
2553
+ width: formatPercent(width),
2554
+ height: formatPercent(height)
2555
+ };
2556
+ if (cell.block !== void 0) {
2557
+ const block = typeof cell.block === "string" ? Number(cell.block) : cell.block;
2558
+ if (typeof block !== "number" || !Number.isInteger(block) || block < 1 || block > MAX_BLOCK_ASSIGNMENT) {
2559
+ errors.push({
2560
+ path: `${path}.block`,
2561
+ message: `block must be an integer between 1 and ${MAX_BLOCK_ASSIGNMENT}`
2562
+ });
2563
+ return void 0;
2564
+ }
2565
+ out.block = block;
2566
+ }
2567
+ if (cell.zoom !== void 0) {
2568
+ const zoom = normalizeDashboardZoom(cell.zoom);
2569
+ if (zoom === void 0) {
2570
+ errors.push({
2571
+ path: `${path}.zoom`,
2572
+ message: "zoom must be 1, 1.5, or 2 (or 100/150/200 percent)"
2573
+ });
2574
+ return void 0;
2575
+ }
2576
+ out.zoom = zoom;
2577
+ }
2578
+ return out;
2579
+ }
2580
+ function validateCellArray(input, path, errors) {
2581
+ if (!Array.isArray(input)) {
2582
+ errors.push({ path, message: "must be an array of cells" });
2583
+ return void 0;
2584
+ }
2585
+ if (input.length < 1 || input.length > MAX_CELLS) {
2586
+ errors.push({ path, message: `must contain between 1 and ${MAX_CELLS} cells` });
2587
+ return void 0;
2588
+ }
2589
+ const cells = [];
2590
+ for (let i = 0; i < input.length; i++) {
2591
+ const cell = validateCell(input[i], `${path}[${i}]`, errors);
2592
+ if (!cell) return void 0;
2593
+ cells.push(cell);
2594
+ }
2595
+ return cells;
2596
+ }
2597
+ function validateTitleSlot(input, errors) {
2598
+ if (!input || typeof input !== "object") {
2599
+ errors.push({ path: "titleSlot", message: "titleSlot must be an object" });
2600
+ return void 0;
2601
+ }
2602
+ const slot = input;
2603
+ const placement = slot.placement === "bottom" ? "bottom" : slot.placement === "top" ? "top" : void 0;
2604
+ if (placement === void 0 && slot.placement !== void 0) {
2605
+ errors.push({ path: "titleSlot.placement", message: "placement must be 'top' or 'bottom'" });
2606
+ return void 0;
2607
+ }
2608
+ const height = parsePercent(slot.height);
2609
+ if (height === void 0 || height <= 0 || height > 40) {
2610
+ errors.push({ path: "titleSlot.height", message: "height must be a percent between 0 and 40" });
2611
+ return void 0;
2612
+ }
2613
+ return { placement: placement ?? "top", height: formatPercent(height) };
2614
+ }
2615
+ function validateDashboardLayoutDefinition(input) {
2616
+ const errors = [];
2617
+ if (!input || typeof input !== "object") {
2618
+ return { valid: false, errors: [{ path: "", message: "definition must be an object" }] };
2619
+ }
2620
+ const def = input;
2621
+ const name = typeof def.name === "string" ? def.name.trim().toLowerCase() : "";
2622
+ if (!NAME_PATTERN.test(name)) {
2623
+ errors.push({
2624
+ path: "name",
2625
+ message: "name must be a slug (lowercase letters, digits, hyphens)"
2626
+ });
2627
+ }
2628
+ const label = typeof def.label === "string" ? def.label.trim() : "";
2629
+ if (label.length === 0) {
2630
+ errors.push({ path: "label", message: "label must be a non-empty string" });
2631
+ }
2632
+ const cellsInput = def.cells;
2633
+ let landscape;
2634
+ let portrait;
2635
+ let square;
2636
+ if (!cellsInput || typeof cellsInput !== "object") {
2637
+ errors.push({ path: "cells", message: "cells must be an object with a landscape array" });
2638
+ } else {
2639
+ landscape = validateCellArray(cellsInput.landscape, "cells.landscape", errors);
2640
+ if (cellsInput.portrait !== void 0) {
2641
+ portrait = validateCellArray(cellsInput.portrait, "cells.portrait", errors);
2642
+ }
2643
+ if (cellsInput.square !== void 0) {
2644
+ square = validateCellArray(cellsInput.square, "cells.square", errors);
2645
+ }
2646
+ if (landscape) {
2647
+ if (portrait && portrait.length !== landscape.length) {
2648
+ errors.push({
2649
+ path: "cells.portrait",
2650
+ message: "portrait must define the same number of cells as landscape"
2651
+ });
2652
+ }
2653
+ if (square && square.length !== landscape.length) {
2654
+ errors.push({
2655
+ path: "cells.square",
2656
+ message: "square must define the same number of cells as landscape"
2657
+ });
2658
+ }
2659
+ }
2660
+ }
2661
+ let titleSlot;
2662
+ if (def.titleSlot !== void 0) {
2663
+ titleSlot = validateTitleSlot(def.titleSlot, errors);
2664
+ }
2665
+ if (errors.length > 0 || !landscape) {
2666
+ return { valid: false, errors };
2667
+ }
2668
+ const layout2 = {
2669
+ name,
2670
+ label,
2671
+ cells: {
2672
+ landscape,
2673
+ ...portrait ? { portrait } : {},
2674
+ ...square ? { square } : {}
2675
+ }
2676
+ };
2677
+ if (typeof def.description === "string" && def.description.trim()) {
2678
+ layout2.description = def.description.trim();
2679
+ }
2680
+ if (titleSlot) layout2.titleSlot = titleSlot;
2681
+ if (def.auto === false) layout2.auto = false;
2682
+ return { valid: true, errors, layout: layout2 };
2683
+ }
2684
+ function layoutCapacity(def) {
2685
+ return def.cells.landscape.length;
2686
+ }
2687
+ function transposeCells(cells) {
2688
+ return cells.map((cell) => ({
2689
+ x: cell.y,
2690
+ y: cell.x,
2691
+ width: cell.height,
2692
+ height: cell.width,
2693
+ ...cell.block !== void 0 ? { block: cell.block } : {},
2694
+ ...cell.zoom !== void 0 ? { zoom: cell.zoom } : {}
2695
+ }));
2696
+ }
2697
+ function resolveLayoutCells(def, orientation, contentRect) {
2698
+ const cells = orientation === "portrait" ? def.cells.portrait ?? transposeCells(def.cells.landscape) : orientation === "square" ? def.cells.square ?? def.cells.landscape : def.cells.landscape;
2699
+ return cells.map((cell) => {
2700
+ const x = parsePercent(cell.x) ?? 0;
2701
+ const y = parsePercent(cell.y) ?? 0;
2702
+ const width = parsePercent(cell.width) ?? 0;
2703
+ const height = parsePercent(cell.height) ?? 0;
2704
+ const resolved = {
2705
+ rect: {
2706
+ x: contentRect.x + x / 100 * contentRect.width,
2707
+ y: contentRect.y + y / 100 * contentRect.height,
2708
+ width: Math.max(1, width / 100 * contentRect.width),
2709
+ height: Math.max(1, height / 100 * contentRect.height)
2710
+ }
2711
+ };
2712
+ if (cell.block !== void 0) resolved.block = cell.block;
2713
+ const zoom = normalizeDashboardZoom(cell.zoom);
2714
+ if (zoom !== void 0) resolved.zoom = zoom;
2715
+ return resolved;
2716
+ });
2717
+ }
2718
+
2719
+ // src/doc/dashboard/dashboardLayoutsFrontmatter.ts
2720
+ var FRONTMATTER_DASHBOARD_LAYOUTS_KEY = "squisq-dashboard-layouts";
2721
+ var LONG_TO_SHORT = {
2722
+ label: "lb",
2723
+ description: "ds",
2724
+ cells: "ce",
2725
+ landscape: "ls",
2726
+ portrait: "pt",
2727
+ square: "sq",
2728
+ width: "wd",
2729
+ height: "hg",
2730
+ block: "bk",
2731
+ zoom: "zo",
2732
+ titleSlot: "ts",
2733
+ placement: "pl",
2734
+ auto: "au"
2735
+ };
2736
+ var SHORT_TO_LONG = Object.fromEntries(
2737
+ Object.entries(LONG_TO_SHORT).map(([long, short]) => [short, long])
2738
+ );
2739
+ function renameKeys(value, map2) {
2740
+ if (Array.isArray(value)) return value.map((v) => renameKeys(v, map2));
2741
+ if (value && typeof value === "object") {
2742
+ const out = {};
2743
+ for (const [k, v] of Object.entries(value)) {
2744
+ out[map2[k] ?? k] = renameKeys(v, map2);
2745
+ }
2746
+ return out;
2747
+ }
2748
+ return value;
2749
+ }
2750
+ function readDashboardLayoutsFromFrontmatter(frontmatter) {
2751
+ if (!frontmatter) return void 0;
2752
+ const candidates = normalizeCandidates(frontmatter[FRONTMATTER_DASHBOARD_LAYOUTS_KEY]);
2753
+ if (!candidates) return void 0;
2754
+ const out = [];
2755
+ for (const entry of candidates) {
2756
+ const result = validateDashboardLayoutDefinition(entry);
2757
+ if (result.layout) out.push(result.layout);
2758
+ }
2759
+ return out.length > 0 ? out : void 0;
2760
+ }
2761
+ function writeDashboardLayoutsToFrontmatter(layouts, options) {
2762
+ if (!layouts || layouts.length === 0) return void 0;
2763
+ const map2 = {};
2764
+ for (const def of layouts) {
2765
+ const { name, ...rest } = def;
2766
+ map2[name] = renameKeys(rest, LONG_TO_SHORT);
2767
+ }
2768
+ return JSON.stringify(map2, null, options?.pretty ? 2 : void 0);
2769
+ }
2770
+ function expandCompactMap(map2) {
2771
+ return Object.entries(map2).map(([name, raw]) => {
2772
+ const expanded = renameKeys(raw && typeof raw === "object" ? raw : {}, SHORT_TO_LONG);
2773
+ return { name, ...expanded };
2774
+ });
2775
+ }
2776
+ function fromParsed(parsed) {
2777
+ if (Array.isArray(parsed)) return parsed;
2778
+ if (parsed && typeof parsed === "object")
2779
+ return expandCompactMap(parsed);
2780
+ return null;
2781
+ }
2782
+ function tryJson(s) {
2783
+ try {
2784
+ return JSON.parse(s);
2785
+ } catch {
2786
+ return void 0;
2787
+ }
2788
+ }
2789
+ function normalizeCandidates(raw) {
2790
+ if (raw == null) return null;
2791
+ if (Array.isArray(raw)) return raw;
2792
+ if (typeof raw === "object") return expandCompactMap(raw);
2793
+ if (typeof raw !== "string") return null;
2794
+ const parsed = tryJson(raw.trim());
2795
+ if (parsed !== void 0) return fromParsed(parsed);
2796
+ return null;
2797
+ }
2798
+
2799
+ // src/doc/dashboard/builtinDashboardLayouts.ts
2800
+ var GAP = 2;
2801
+ function pct(value) {
2802
+ return `${Math.round(value * 1e3) / 1e3}%`;
2803
+ }
2804
+ function gridCells(cols, rows, gap = GAP) {
2805
+ const cellW = (100 - (cols - 1) * gap) / cols;
2806
+ const cellH = (100 - (rows - 1) * gap) / rows;
2807
+ const cells = [];
2808
+ for (let row = 0; row < rows; row++) {
2809
+ for (let col = 0; col < cols; col++) {
2810
+ cells.push({
2811
+ x: pct(col * (cellW + gap)),
2812
+ y: pct(row * (cellH + gap)),
2813
+ width: pct(cellW),
2814
+ height: pct(cellH)
2815
+ });
2816
+ }
2817
+ }
2818
+ return cells;
2819
+ }
2820
+ function heroRow(heroWidth, companions, gap = GAP) {
2821
+ const railX = heroWidth + gap;
2822
+ const railW = 100 - railX;
2823
+ const cellH = (100 - (companions - 1) * gap) / companions;
2824
+ const cells = [
2825
+ { x: "0%", y: "0%", width: pct(heroWidth), height: "100%" }
2826
+ ];
2827
+ for (let i = 0; i < companions; i++) {
2828
+ cells.push({
2829
+ x: pct(railX),
2830
+ y: pct(i * (cellH + gap)),
2831
+ width: pct(railW),
2832
+ height: pct(cellH)
2833
+ });
2834
+ }
2835
+ return cells;
2836
+ }
2837
+ function heroBand(heroHeight, below, gap = GAP) {
2838
+ const rowY = heroHeight + gap;
2839
+ const rowH = 100 - rowY;
2840
+ const cellW = (100 - (below - 1) * gap) / below;
2841
+ const cells = [
2842
+ { x: "0%", y: "0%", width: "100%", height: pct(heroHeight) }
2843
+ ];
2844
+ for (let i = 0; i < below; i++) {
2845
+ cells.push({
2846
+ x: pct(i * (cellW + gap)),
2847
+ y: pct(rowY),
2848
+ width: pct(cellW),
2849
+ height: pct(rowH)
2850
+ });
2851
+ }
2852
+ return cells;
2853
+ }
2854
+ function mosaicCells(heroWidth, gap = GAP) {
2855
+ const gridX = heroWidth + gap;
2856
+ const gridW = 100 - gridX;
2857
+ const cellW = (gridW - gap) / 2;
2858
+ const cellH = (100 - gap) / 2;
2859
+ const cells = [
2860
+ { x: "0%", y: "0%", width: pct(heroWidth), height: "100%" }
2861
+ ];
2862
+ for (let row = 0; row < 2; row++) {
2863
+ for (let col = 0; col < 2; col++) {
2864
+ cells.push({
2865
+ x: pct(gridX + col * (cellW + gap)),
2866
+ y: pct(row * (cellH + gap)),
2867
+ width: pct(cellW),
2868
+ height: pct(cellH)
2869
+ });
2870
+ }
2871
+ }
2872
+ return cells;
2873
+ }
2874
+ var BUILTIN_DASHBOARD_LAYOUTS = Object.freeze([
2875
+ {
2876
+ name: "focus-1",
2877
+ label: "Focus",
2878
+ description: "One block fills the dashboard.",
2879
+ cells: { landscape: [{ x: "0%", y: "0%", width: "100%", height: "100%" }] }
2880
+ },
2881
+ {
2882
+ name: "split-2",
2883
+ label: "Split",
2884
+ description: "Two blocks side by side.",
2885
+ cells: { landscape: gridCells(2, 1) }
2886
+ },
2887
+ {
2888
+ name: "hero-left",
2889
+ label: "Hero + 2",
2890
+ description: "A large lead block with two companions.",
2891
+ cells: {
2892
+ landscape: heroRow(58, 2),
2893
+ portrait: [
2894
+ { x: "0%", y: "0%", width: "100%", height: pct(46) },
2895
+ { x: "0%", y: pct(48), width: "100%", height: pct(25) },
2896
+ { x: "0%", y: pct(75), width: "100%", height: pct(25) }
2897
+ ]
2898
+ }
2899
+ },
2900
+ {
2901
+ name: "grid-2x2",
2902
+ label: "Grid 2\xD72",
2903
+ description: "Four blocks in a balanced grid.",
2904
+ cells: { landscape: gridCells(2, 2) }
2905
+ },
2906
+ {
2907
+ name: "hero-top",
2908
+ label: "Hero band",
2909
+ description: "A wide lead band with three blocks below.",
2910
+ cells: {
2911
+ landscape: heroBand(46, 3),
2912
+ portrait: [
2913
+ { x: "0%", y: "0%", width: "100%", height: pct(40) },
2914
+ { x: "0%", y: pct(42), width: "100%", height: pct(18) },
2915
+ { x: "0%", y: pct(62), width: "100%", height: pct(18) },
2916
+ { x: "0%", y: pct(82), width: "100%", height: pct(18) }
2917
+ ]
2918
+ },
2919
+ auto: false
2920
+ },
2921
+ {
2922
+ name: "mosaic-5",
2923
+ label: "Mosaic",
2924
+ description: "A hero block beside a 2\xD72 grid.",
2925
+ cells: {
2926
+ landscape: mosaicCells(58),
2927
+ portrait: [
2928
+ { x: "0%", y: "0%", width: "100%", height: pct(40) },
2929
+ { x: "0%", y: pct(42), width: pct(49), height: pct(27) },
2930
+ { x: pct(51), y: pct(42), width: pct(49), height: pct(27) },
2931
+ { x: "0%", y: pct(71), width: pct(49), height: pct(29) },
2932
+ { x: pct(51), y: pct(71), width: pct(49), height: pct(29) }
2933
+ ]
2934
+ }
2935
+ },
2936
+ {
2937
+ name: "grid-3x2",
2938
+ label: "Grid 3\xD72",
2939
+ description: "Six blocks in three columns.",
2940
+ cells: { landscape: gridCells(3, 2) }
2941
+ },
2942
+ {
2943
+ name: "grid-3x3",
2944
+ label: "Grid 3\xD73",
2945
+ description: "Nine blocks in a square grid.",
2946
+ cells: { landscape: gridCells(3, 3), square: gridCells(3, 3) }
2947
+ },
2948
+ {
2949
+ name: "grid-4x3",
2950
+ label: "Grid 4\xD73",
2951
+ description: "Twelve blocks in four columns.",
2952
+ cells: { landscape: gridCells(4, 3) }
2953
+ },
2954
+ {
2955
+ name: "grid-4x4",
2956
+ label: "Grid 4\xD74",
2957
+ description: "Sixteen blocks \u2014 the densest wall.",
2958
+ cells: { landscape: gridCells(4, 4) }
2959
+ }
2960
+ ]);
2961
+ function summarize(def, custom) {
2962
+ return {
2963
+ id: def.name,
2964
+ label: def.label,
2965
+ ...def.description ? { description: def.description } : {},
2966
+ capacity: layoutCapacity(def),
2967
+ custom
2968
+ };
2969
+ }
2970
+ function getDashboardLayoutSummaries() {
2971
+ return BUILTIN_DASHBOARD_LAYOUTS.map((def) => summarize(def, false));
2972
+ }
2973
+ function listDashboardLayouts(doc) {
2974
+ const customs = readDashboardLayoutsFromFrontmatter(doc?.frontmatter) ?? [];
2975
+ const customNames = new Set(customs.map((def) => def.name));
2976
+ return [
2977
+ ...customs.map((def) => summarize(def, true)),
2978
+ ...BUILTIN_DASHBOARD_LAYOUTS.filter((def) => !customNames.has(def.name)).map(
2979
+ (def) => summarize(def, false)
2980
+ )
2981
+ ];
2982
+ }
2983
+
2984
+ // src/doc/dashboard/chooseDashboardLayout.ts
2985
+ var DASHBOARD_AUTO_LAYOUT_ID = "auto";
2986
+ function resolveDashboardLayoutDefinition(id, customLayouts) {
2987
+ const normalized = id.trim().toLowerCase();
2988
+ if (!normalized) return void 0;
2989
+ return customLayouts?.find((def) => def.name === normalized) ?? BUILTIN_DASHBOARD_LAYOUTS.find((def) => def.name === normalized);
2990
+ }
2991
+ function chooseDashboardLayout(blockCount, _orientation, customLayouts) {
2992
+ const pool = [
2993
+ ...(customLayouts ?? []).filter((def) => def.auto !== false),
2994
+ ...BUILTIN_DASHBOARD_LAYOUTS.filter((def) => def.auto !== false)
2995
+ ];
2996
+ const ladder = [...pool].sort((a, b) => layoutCapacity(a) - layoutCapacity(b));
2997
+ const needed = Math.max(1, blockCount);
2998
+ const fit = ladder.find((def) => layoutCapacity(def) >= needed);
2999
+ if (fit) return fit;
3000
+ const max = ladder.reduce((acc, def) => Math.max(acc, layoutCapacity(def)), 0);
3001
+ return ladder.find((def) => layoutCapacity(def) === max) ?? BUILTIN_DASHBOARD_LAYOUTS[0];
3002
+ }
3003
+
3004
+ // src/doc/dashboard/dashboardStyle.ts
3005
+ var DASHBOARD_STYLE_IDS = ["basic", "card", "panel", "accent"];
3006
+ var DEFAULT_DASHBOARD_STYLE = "basic";
3007
+ var DASHBOARD_STYLES = Object.freeze([
3008
+ {
3009
+ id: "basic",
3010
+ label: "Basic",
3011
+ description: "Blocks fill their cells edge to edge."
3012
+ },
3013
+ {
3014
+ id: "card",
3015
+ label: "Cards",
3016
+ description: "Each block sits on a raised, rounded card."
3017
+ },
3018
+ {
3019
+ id: "panel",
3020
+ label: "Panels",
3021
+ description: "Flat outlined panels with an accent rule."
3022
+ },
3023
+ {
3024
+ id: "accent",
3025
+ label: "Accent cards",
3026
+ description: "Cards tinted with the theme's accent colors in rotation."
3027
+ }
3028
+ ]);
3029
+ function resolveDashboardStyleId(value) {
3030
+ if (typeof value !== "string") return void 0;
3031
+ const normalized = value.trim().toLowerCase();
3032
+ if (!normalized) return void 0;
3033
+ if (normalized === "none" || normalized === "default" || normalized === "flat") return "basic";
3034
+ if (normalized === "cards") return "card";
3035
+ if (normalized === "panels" || normalized === "outline") return "panel";
3036
+ if (normalized === "accents" || normalized === "accent-cards") return "accent";
3037
+ return DASHBOARD_STYLE_IDS.includes(normalized) ? normalized : void 0;
3038
+ }
3039
+ function clamp(value, min, max) {
3040
+ return Math.min(max, Math.max(min, value));
3041
+ }
3042
+ function isLight(color) {
3043
+ return relativeLuminance(color) > 0.45;
3044
+ }
3045
+ function dashboardCanvasFill(style, theme) {
3046
+ const background = theme.colors.background;
3047
+ if (style === "basic" || style === "panel") return background;
3048
+ return isLight(background) ? oklchDarken(background, 0.05) : oklchLighten(background, 0.04);
3049
+ }
3050
+ function dashboardCellAccent(theme, index) {
3051
+ const schemes = Object.values(theme.colorSchemes ?? {});
3052
+ if (schemes.length === 0) return theme.colors.primary;
3053
+ const scheme = schemes[(index % schemes.length + schemes.length) % schemes.length];
3054
+ return scheme.accent || scheme.text || theme.colors.primary;
3055
+ }
3056
+ function cellMetrics(style, theme, rect) {
3057
+ const minAxis = Math.max(1, Math.min(rect.width, rect.height));
3058
+ const themeRadius = theme.style?.borderRadius ?? 14;
3059
+ if (style === "panel") {
3060
+ return {
3061
+ edge: clamp(minAxis * 0.014, 3, 12),
3062
+ radius: clamp(themeRadius * 0.45, 0, minAxis * 0.05)
3063
+ };
3064
+ }
3065
+ return {
3066
+ // Card styles reserve a wider ring: the elevation is painted inside the
3067
+ // layout cell (a cell renders as its own clipped SVG), so the shadow
3068
+ // needs room to fall without being cut flat at the cell boundary.
3069
+ edge: clamp(minAxis * 0.03, 6, 26),
3070
+ radius: clamp(themeRadius, 6, minAxis * 0.1)
3071
+ };
3072
+ }
3073
+ function radiusPct(radius, box) {
3074
+ if (radius <= 0) return void 0;
3075
+ const x = Math.round(radius / Math.max(1, box.width) * 1e5) / 1e3;
3076
+ const y = Math.round(radius / Math.max(1, box.height) * 1e5) / 1e3;
3077
+ return `${x}% / ${y}%`;
3078
+ }
3079
+ function buildDashboardCellChrome(style, options) {
3080
+ const { theme, rect, index } = options;
3081
+ if (style === "basic") return null;
3082
+ const { edge, radius } = cellMetrics(style, theme, rect);
3083
+ const cardRect = {
3084
+ x: rect.x + edge,
3085
+ y: rect.y + edge,
3086
+ width: Math.max(1, rect.width - edge * 2),
3087
+ height: Math.max(1, rect.height - edge * 2)
3088
+ };
3089
+ const local = {
3090
+ x: edge,
3091
+ y: edge,
3092
+ width: cardRect.width,
3093
+ height: cardRect.height
3094
+ };
3095
+ const minAxis = Math.max(1, Math.min(rect.width, rect.height));
3096
+ const hairline = Math.max(1, Math.round(minAxis * 4e-3));
3097
+ const accent = dashboardCellAccent(theme, index);
3098
+ const layers = [];
3099
+ if (style !== "panel") {
3100
+ const steps = [
3101
+ { spread: 0.45, dy: 0.5, alpha: 0.05 },
3102
+ { spread: 0.25, dy: 0.42, alpha: 0.05 },
3103
+ { spread: 0.08, dy: 0.3, alpha: 0.06 }
3104
+ ];
3105
+ steps.forEach((step, stepIndex) => {
3106
+ const spread = edge * step.spread;
3107
+ const dy = edge * step.dy;
3108
+ layers.push({
3109
+ type: "shape",
3110
+ id: `cell-shadow-${stepIndex}`,
3111
+ content: {
3112
+ shape: "rect",
3113
+ fill: withAlpha(theme.colors.text, step.alpha),
3114
+ borderRadius: radius + spread
3115
+ },
3116
+ position: {
3117
+ x: local.x - spread,
3118
+ y: local.y - spread + dy,
3119
+ width: local.width + spread * 2,
3120
+ height: local.height + spread * 2
3121
+ }
3122
+ });
3123
+ });
3124
+ }
3125
+ layers.push({
3126
+ type: "shape",
3127
+ id: "cell-surface",
3128
+ content: {
3129
+ shape: "rect",
3130
+ // The surface stays the theme background; the canvas is what moves
3131
+ // (see `dashboardCanvasFill`), so cards read as raised paper in both
3132
+ // light and dark themes without inventing a palette slot.
3133
+ fill: theme.colors.background,
3134
+ borderRadius: radius
3135
+ },
3136
+ position: { x: local.x, y: local.y, width: local.width, height: local.height }
3137
+ });
3138
+ const overlayLayers = [];
3139
+ if (style === "accent") {
3140
+ overlayLayers.push({
3141
+ type: "shape",
3142
+ id: "cell-accent-wash",
3143
+ content: { shape: "rect", fill: withAlpha(accent, 0.1), borderRadius: radius },
3144
+ position: { x: 0, y: 0, width: "100%", height: "100%" }
3145
+ });
3146
+ }
3147
+ if (style === "accent" || style === "panel") {
3148
+ const barHeight = Math.max(2, Math.round(minAxis * 0.014));
3149
+ overlayLayers.push({
3150
+ type: "shape",
3151
+ id: "cell-accent-bar",
3152
+ content: { shape: "rect", fill: accent },
3153
+ position: { x: 0, y: 0, width: "100%", height: barHeight }
3154
+ });
3155
+ }
3156
+ const strokeWidth = style === "panel" ? hairline : Math.max(1, hairline * 0.6);
3157
+ overlayLayers.push({
3158
+ type: "shape",
3159
+ id: "cell-border",
3160
+ content: {
3161
+ shape: "rect",
3162
+ fill: "none",
3163
+ stroke: style === "panel" ? withAlpha(accent, 0.45) : withAlpha(theme.colors.text, 0.14),
3164
+ strokeWidth,
3165
+ borderRadius: radius
3166
+ },
3167
+ position: {
3168
+ x: strokeWidth / 2,
3169
+ y: strokeWidth / 2,
3170
+ width: Math.max(1, local.width - strokeWidth),
3171
+ height: Math.max(1, local.height - strokeWidth)
3172
+ }
3173
+ });
3174
+ const chrome = {
3175
+ cardRect,
3176
+ // The block fills the card: templates bring their own padding, and a
3177
+ // padded well inside the card reads as a box inside a box.
3178
+ contentRect: cardRect,
3179
+ layers,
3180
+ overlayLayers,
3181
+ radius
3182
+ };
3183
+ const contentRadius = radiusPct(radius, cardRect);
3184
+ if (contentRadius) chrome.contentRadiusPct = contentRadius;
3185
+ return chrome;
3186
+ }
3187
+ function stripsBlockBackdrop(style) {
3188
+ return style !== "basic";
3189
+ }
3190
+ function stripBlockBackdropLayer(layers, theme) {
3191
+ const first = layers[0];
3192
+ if (!first || first.type !== "shape") return [...layers];
3193
+ const { content: content2, position } = first;
3194
+ const fill = typeof content2.fill === "string" ? content2.fill.trim().toLowerCase() : "";
3195
+ const fullBleed = content2.shape === "rect" && (position.x === 0 || position.x === "0%") && (position.y === 0 || position.y === "0%") && position.width === "100%" && position.height === "100%";
3196
+ if (!fullBleed || content2.gradient || content2.pattern) return [...layers];
3197
+ if (fill !== theme.colors.background.trim().toLowerCase()) return [...layers];
3198
+ return layers.slice(1);
3199
+ }
3200
+
3201
+ // src/doc/dashboard/dashboardSettings.ts
3202
+ var DASHBOARD_FRONTMATTER_KEYS = Object.freeze({
3203
+ layout: { canonical: "squisq-dashboard-layout", legacy: "dashboard-layout" },
3204
+ showTitle: { canonical: "squisq-dashboard-title", legacy: "dashboard-title" },
3205
+ zoom: { canonical: "squisq-dashboard-zoom", legacy: "dashboard-zoom" },
3206
+ style: { canonical: "squisq-dashboard-style", legacy: "dashboard-style" }
3207
+ });
3208
+ var DEFAULT_DASHBOARD_SETTINGS = Object.freeze({
3209
+ layout: DASHBOARD_AUTO_LAYOUT_ID,
3210
+ showTitle: true,
3211
+ zoom: "auto",
3212
+ style: DEFAULT_DASHBOARD_STYLE
3213
+ });
3214
+ function readSetting2(frontmatter, keys) {
3215
+ if (!frontmatter) return void 0;
3216
+ return Object.prototype.hasOwnProperty.call(frontmatter, keys.canonical) ? frontmatter[keys.canonical] : frontmatter[keys.legacy];
3217
+ }
3218
+ function resolveBoolean2(value) {
3219
+ if (typeof value === "boolean") return value;
3220
+ if (typeof value !== "string") return void 0;
3221
+ const normalized = value.trim().toLowerCase();
3222
+ if (["true", "yes", "on", "show", "visible"].includes(normalized)) return true;
3223
+ if (["false", "no", "off", "hide", "hidden"].includes(normalized)) return false;
3224
+ return void 0;
3225
+ }
3226
+ function resolveLayoutId(value) {
3227
+ if (typeof value !== "string") return void 0;
3228
+ const normalized = value.trim().toLowerCase();
3229
+ if (!normalized) return void 0;
3230
+ if (normalized === "auto" || normalized === "default") return DASHBOARD_AUTO_LAYOUT_ID;
3231
+ return normalized;
3232
+ }
3233
+ function resolveZoomMode(value) {
3234
+ if (typeof value === "boolean") return value ? "auto" : "off";
3235
+ if (typeof value !== "string") return void 0;
3236
+ const normalized = value.trim().toLowerCase();
3237
+ if (["auto", "on", "true", "yes"].includes(normalized)) return "auto";
3238
+ if (["off", "false", "no", "none", "100", "100%", "1"].includes(normalized)) return "off";
3239
+ return void 0;
3240
+ }
3241
+ function resolveDashboardSettings(frontmatter, overrides = {}) {
3242
+ const resolved = {
3243
+ layout: resolveLayoutId(readSetting2(frontmatter, DASHBOARD_FRONTMATTER_KEYS.layout)) ?? DEFAULT_DASHBOARD_SETTINGS.layout,
3244
+ showTitle: resolveBoolean2(readSetting2(frontmatter, DASHBOARD_FRONTMATTER_KEYS.showTitle)) ?? DEFAULT_DASHBOARD_SETTINGS.showTitle,
3245
+ zoom: resolveZoomMode(readSetting2(frontmatter, DASHBOARD_FRONTMATTER_KEYS.zoom)) ?? DEFAULT_DASHBOARD_SETTINGS.zoom,
3246
+ style: resolveDashboardStyleId(readSetting2(frontmatter, DASHBOARD_FRONTMATTER_KEYS.style)) ?? DEFAULT_DASHBOARD_SETTINGS.style
3247
+ };
3248
+ return {
3249
+ layout: resolveLayoutId(overrides.layout) ?? resolved.layout,
3250
+ showTitle: overrides.showTitle ?? resolved.showTitle,
3251
+ zoom: overrides.zoom ?? resolved.zoom,
3252
+ style: resolveDashboardStyleId(overrides.style) ?? resolved.style
3253
+ };
3254
+ }
3255
+
3256
+ // src/doc/dashboard/materializeDashboard.ts
3257
+ var DEFAULT_TITLE_SLOT = { placement: "top", height: "9%" };
3258
+ var TITLE_CONTENT_GAP = 0.015;
3259
+ var CANVAS_MARGIN = 0.02;
3260
+ function percentNumber(value) {
3261
+ const parsed = Number(value.trim().replace(/%$/, ""));
3262
+ return Number.isFinite(parsed) ? parsed : 0;
3263
+ }
3264
+ function pctOf(value, total) {
3265
+ return `${Math.round(value / Math.max(1, total) * 1e5) / 1e3}%`;
3266
+ }
3267
+ function rectToPct(rect, viewport) {
3268
+ return {
3269
+ left: pctOf(rect.x, viewport.width),
3270
+ top: pctOf(rect.y, viewport.height),
3271
+ width: pctOf(rect.width, viewport.width),
3272
+ height: pctOf(rect.height, viewport.height)
3273
+ };
3274
+ }
3275
+ function resolveDashboardTitle(doc, provided) {
3276
+ const frontmatterTitle = doc.frontmatter?.title;
3277
+ if (typeof frontmatterTitle === "string" && frontmatterTitle.trim()) {
3278
+ return frontmatterTitle.trim();
3279
+ }
3280
+ return provided?.trim() ?? "";
3281
+ }
3282
+ function dedupeLeadingTitleBlock(candidates, titleText) {
3283
+ if (candidates.length === 0 || !titleText) return candidates;
3284
+ const first = candidates[0];
3285
+ const firstTitle = typeof first.title === "string" ? first.title.trim().toLowerCase() : "";
3286
+ if (!firstTitle || firstTitle !== titleText.trim().toLowerCase()) return candidates;
3287
+ const template = first.template;
3288
+ const titleShaped = template === "title" || template === "sectionHeader" || template === "cover";
3289
+ const hasBody = Array.isArray(first.contents) && first.contents.length > 0;
3290
+ return titleShaped || !hasBody ? candidates.slice(1) : candidates;
3291
+ }
3292
+ function buildTitleLayers(titleText, bandViewport, slot, theme, canvasViewport, margin) {
3293
+ const context = createTemplateContext(theme, 0, 1, canvasViewport);
3294
+ const canvasFontScale = calculateFontScale(canvasViewport);
3295
+ const fontSize = Math.min(44 * canvasFontScale, bandViewport.height * 0.52);
3296
+ const accentWidth = Math.max(4, Math.round(6 * canvasFontScale));
3297
+ const textX = margin + accentWidth + Math.max(10, Math.round(accentWidth * 2.5));
3298
+ return [
3299
+ {
3300
+ type: "shape",
3301
+ id: "dashboard-title-accent",
3302
+ content: { shape: "rect", fill: theme.colors.primary },
3303
+ position: {
3304
+ x: margin,
3305
+ y: "26%",
3306
+ width: accentWidth,
3307
+ height: "48%"
3308
+ }
3309
+ },
3310
+ {
3311
+ type: "text",
3312
+ id: "dashboard-title-text",
3313
+ content: {
3314
+ text: titleText,
3315
+ style: {
3316
+ fontSize,
3317
+ fontFamily: getThemeFont(context, "title"),
3318
+ fontWeight: "bold",
3319
+ color: theme.colors.text,
3320
+ textAlign: "left",
3321
+ verticalAlign: "middle",
3322
+ maxLines: 1
3323
+ }
3324
+ },
3325
+ position: {
3326
+ x: textX,
3327
+ y: 0,
3328
+ width: Math.max(1, bandViewport.width - textX - margin),
3329
+ height: "100%"
3330
+ }
3331
+ },
3332
+ {
3333
+ // Hairline separating the band from the content area.
3334
+ type: "shape",
3335
+ id: "dashboard-title-rule",
3336
+ content: { shape: "rect", fill: withAlpha(theme.colors.text, 0.14) },
3337
+ position: {
3338
+ x: 0,
3339
+ y: slot.placement === "bottom" ? 0 : bandViewport.height - 2,
3340
+ width: "100%",
3341
+ height: 2
3342
+ }
3343
+ }
3344
+ ];
3345
+ }
3346
+ function materializeDashboard(doc, options = {}) {
3347
+ const theme = options.theme ?? DEFAULT_THEME;
3348
+ const viewport = options.viewport ?? VIEWPORT_PRESETS.landscape;
3349
+ const orientation = getViewportOrientation(viewport);
3350
+ const customLayouts = options.customLayouts ?? readDashboardLayoutsFromFrontmatter(doc.frontmatter);
3351
+ const customTemplates = options.customTemplates ?? doc.customTemplates;
3352
+ const diagnostics = [];
3353
+ const settings = resolveDashboardSettings(doc.frontmatter, {
3354
+ layout: typeof options.layout === "string" ? options.layout : void 0,
3355
+ showTitle: options.showTitle,
3356
+ zoom: options.zoom,
3357
+ ...options.style !== void 0 ? { style: options.style } : {}
3358
+ });
3359
+ const style = settings.style;
3360
+ let layoutDef;
3361
+ let layoutSource = "auto";
3362
+ if (options.layout && typeof options.layout === "object") {
3363
+ const validated = validateDashboardLayoutDefinition(options.layout);
3364
+ if (validated.layout) {
3365
+ layoutDef = validated.layout;
3366
+ layoutSource = "option";
3367
+ } else {
3368
+ diagnostics.push({
3369
+ type: "unknown-layout",
3370
+ message: "Inline dashboard layout definition is invalid; falling back to auto.",
3371
+ requestedLayout: options.layout.name
3372
+ });
3373
+ }
3374
+ } else if (settings.layout !== DASHBOARD_AUTO_LAYOUT_ID) {
3375
+ const resolved = resolveDashboardLayoutDefinition(settings.layout, customLayouts);
3376
+ if (resolved) {
3377
+ layoutDef = resolved;
3378
+ layoutSource = typeof options.layout === "string" && options.layout.trim() ? "option" : "frontmatter";
3379
+ } else {
3380
+ diagnostics.push({
3381
+ type: "unknown-layout",
3382
+ message: `Unknown dashboard layout "${settings.layout}"; falling back to auto.`,
3383
+ requestedLayout: settings.layout
3384
+ });
3385
+ }
3386
+ }
3387
+ const preview = buildPreviewDoc(doc, {
3388
+ documentTitle: options.documentTitle,
3389
+ interleaveImages: false
3390
+ });
3391
+ let candidates = preview.blocks ?? [];
3392
+ if (candidates.length === 0) {
3393
+ diagnostics.push({
3394
+ type: "empty-doc",
3395
+ message: "The document has no renderable blocks; the dashboard has no cells."
3396
+ });
3397
+ }
3398
+ const titleText = settings.showTitle ? resolveDashboardTitle(doc, options.documentTitle) : "";
3399
+ const hasTitleBand = titleText.length > 0;
3400
+ if (hasTitleBand) {
3401
+ candidates = dedupeLeadingTitleBlock(candidates, titleText);
3402
+ }
3403
+ if (!layoutDef) {
3404
+ layoutDef = chooseDashboardLayout(candidates.length, orientation, customLayouts);
3405
+ layoutSource = "auto";
3406
+ }
3407
+ const margin = CANVAS_MARGIN * Math.min(viewport.width, viewport.height);
3408
+ let contentRect = {
3409
+ x: margin,
3410
+ y: margin,
3411
+ width: Math.max(1, viewport.width - margin * 2),
3412
+ height: Math.max(1, viewport.height - margin * 2)
3413
+ };
3414
+ let title2 = null;
3415
+ if (hasTitleBand) {
3416
+ const slot = layoutDef.titleSlot ?? DEFAULT_TITLE_SLOT;
3417
+ const bandPct = Math.min(40, Math.max(2, percentNumber(slot.height)));
3418
+ const bandHeight = bandPct / 100 * viewport.height;
3419
+ const gap = TITLE_CONTENT_GAP * viewport.height;
3420
+ const titleRect = slot.placement === "bottom" ? { x: 0, y: viewport.height - bandHeight, width: viewport.width, height: bandHeight } : { x: 0, y: 0, width: viewport.width, height: bandHeight };
3421
+ contentRect = slot.placement === "bottom" ? {
3422
+ x: margin,
3423
+ y: margin,
3424
+ width: Math.max(1, viewport.width - margin * 2),
3425
+ height: Math.max(1, viewport.height - bandHeight - gap - margin)
3426
+ } : {
3427
+ x: margin,
3428
+ y: bandHeight + gap,
3429
+ width: Math.max(1, viewport.width - margin * 2),
3430
+ height: Math.max(1, viewport.height - bandHeight - gap - margin)
3431
+ };
3432
+ const bandViewport = {
3433
+ width: Math.max(1, Math.round(titleRect.width)),
3434
+ height: Math.max(1, Math.round(titleRect.height)),
3435
+ name: "Dashboard title"
3436
+ };
3437
+ title2 = {
3438
+ text: titleText,
3439
+ rect: titleRect,
3440
+ rectPct: rectToPct(titleRect, viewport),
3441
+ viewport: bandViewport,
3442
+ // The band spans the canvas, but its type starts on the cell area's
3443
+ // left edge so the title and the first column share one margin.
3444
+ layers: buildTitleLayers(titleText, bandViewport, slot, theme, viewport, margin)
3445
+ };
3446
+ }
3447
+ const resolvedCells = resolveLayoutCells(layoutDef, orientation, contentRect);
3448
+ const assignments = [];
3449
+ const explicitlyClaimed = /* @__PURE__ */ new Set();
3450
+ resolvedCells.forEach((cell, cellIndex) => {
3451
+ if (cell.block === void 0) return;
3452
+ const candidateIndex = cell.block - 1;
3453
+ if (candidateIndex < 0 || candidateIndex >= candidates.length) {
3454
+ diagnostics.push({
3455
+ type: "invalid-cell-assignment",
3456
+ message: `Cell ${cellIndex + 1} of layout "${layoutDef.name}" requests block ${cell.block}, but the document has ${candidates.length} block(s); the cell is left empty.`
3457
+ });
3458
+ return;
3459
+ }
3460
+ explicitlyClaimed.add(candidateIndex);
3461
+ assignments.push({ cellIndex, rect: cell.rect, candidateIndex, explicitZoom: cell.zoom });
3462
+ });
3463
+ const filled = /* @__PURE__ */ new Set();
3464
+ let cursor = 0;
3465
+ resolvedCells.forEach((cell, cellIndex) => {
3466
+ if (cell.block !== void 0) return;
3467
+ while (cursor < candidates.length && explicitlyClaimed.has(cursor)) cursor++;
3468
+ if (cursor >= candidates.length) return;
3469
+ filled.add(cursor);
3470
+ assignments.push({
3471
+ cellIndex,
3472
+ rect: cell.rect,
3473
+ candidateIndex: cursor,
3474
+ explicitZoom: cell.zoom
3475
+ });
3476
+ cursor++;
3477
+ });
3478
+ const hidden = [];
3479
+ for (let i = 0; i < candidates.length; i++) {
3480
+ if (!explicitlyClaimed.has(i) && !filled.has(i)) hidden.push(i);
3481
+ }
3482
+ if (hidden.length > 0) {
3483
+ diagnostics.push({
3484
+ type: "overflow",
3485
+ message: `${hidden.length} block(s) exceed the "${layoutDef.name}" layout's ${resolvedCells.length} cell(s) and are not rendered.`,
3486
+ hiddenBlockIds: hidden.map((i) => String(candidates[i].id ?? i))
3487
+ });
3488
+ }
3489
+ const orderedAssignments = assignments.sort((a, b) => a.cellIndex - b.cellIndex);
3490
+ const zooms = resolveDashboardZooms(
3491
+ orderedAssignments.map(({ candidateIndex, explicitZoom }) => {
3492
+ const block = candidates[candidateIndex];
3493
+ const title3 = typeof block.title === "string" ? block.title : "";
3494
+ return {
3495
+ template: resolveTemplateName(block.template ?? ""),
3496
+ textLength: title3.length + getBlockBodyText(block).length,
3497
+ ...explicitZoom !== void 0 ? { explicit: explicitZoom } : {}
3498
+ };
3499
+ }),
3500
+ settings.zoom
3501
+ );
3502
+ const registry = customTemplates && customTemplates.length > 0 ? buildRegistry(customTemplates) : templateRegistry;
3503
+ const cells = orderedAssignments.map(
3504
+ ({ cellIndex, rect, candidateIndex }, orderIndex) => {
3505
+ const block = candidates[candidateIndex];
3506
+ const zoom = zooms[orderIndex];
3507
+ const chrome = buildDashboardCellChrome(style, { theme, rect, index: cellIndex });
3508
+ const contentRect2 = chrome ? chrome.contentRect : rect;
3509
+ const cellViewport = {
3510
+ width: Math.max(1, Math.round(contentRect2.width)),
3511
+ height: Math.max(1, Math.round(contentRect2.height)),
3512
+ name: `Dashboard cell ${cellIndex + 1}`
3513
+ };
3514
+ const cellContext = createTemplateContext(
3515
+ theme,
3516
+ candidateIndex,
3517
+ candidates.length,
3518
+ cellViewport
3519
+ );
3520
+ if (zoom !== 1) cellContext.fontScale *= zoom;
3521
+ const result = materializeBlockLayersWithRuntime(
3522
+ block,
3523
+ {
3524
+ theme,
3525
+ viewport: cellViewport,
3526
+ persistentLayers: false,
3527
+ blockIndex: candidateIndex,
3528
+ totalBlocks: candidates.length,
3529
+ failureMode: options.failureMode
3530
+ },
3531
+ { registry, templateContext: cellContext }
3532
+ );
3533
+ const layers = stripsBlockBackdrop(style) ? stripBlockBackdropLayer(result.layers, theme) : result.layers;
3534
+ let frame;
3535
+ if (chrome) {
3536
+ const frameViewport = {
3537
+ width: Math.max(1, Math.round(rect.width)),
3538
+ height: Math.max(1, Math.round(rect.height)),
3539
+ name: `Dashboard cell ${cellIndex + 1} frame`
3540
+ };
3541
+ frame = {
3542
+ rect,
3543
+ rectPct: rectToPct(rect, viewport),
3544
+ viewport: frameViewport,
3545
+ layers: chrome.layers,
3546
+ overlayLayers: chrome.overlayLayers,
3547
+ overlayViewport: {
3548
+ width: cellViewport.width,
3549
+ height: cellViewport.height,
3550
+ name: `Dashboard cell ${cellIndex + 1} overlay`
3551
+ },
3552
+ ...chrome.contentRadiusPct ? { contentRadiusPct: chrome.contentRadiusPct } : {}
3553
+ };
3554
+ }
3555
+ return {
3556
+ index: cellIndex,
3557
+ block,
3558
+ blockIndex: candidateIndex,
3559
+ layers,
3560
+ rect: contentRect2,
3561
+ rectPct: rectToPct(contentRect2, viewport),
3562
+ ...frame ? { frame } : {},
3563
+ viewport: cellViewport,
3564
+ zoom,
3565
+ source: result.source,
3566
+ ...result.diagnostic ? { diagnostic: result.diagnostic } : {}
3567
+ };
3568
+ }
3569
+ );
3570
+ const persistent = resolvePersistentLayers(doc, theme);
3571
+ const canvasFill = dashboardCanvasFill(style, theme);
3572
+ const backdrop = {
3573
+ fill: canvasFill,
3574
+ bottomLayers: [
3575
+ {
3576
+ type: "shape",
3577
+ id: "dashboard-backdrop",
3578
+ content: { shape: "rect", fill: canvasFill },
3579
+ position: { x: 0, y: 0, width: "100%", height: "100%" }
3580
+ },
3581
+ ...expandPersistentLayers(persistent?.bottomLayers, theme)
3582
+ ],
3583
+ topLayers: expandPersistentLayers(persistent?.topLayers, theme)
3584
+ };
3585
+ return {
3586
+ layout: layoutDef,
3587
+ layoutSource,
3588
+ style,
3589
+ viewport,
3590
+ cells,
3591
+ title: title2,
3592
+ backdrop,
3593
+ diagnostics
3594
+ };
3595
+ }
3596
+ function composeDashboardLayers(materialization) {
3597
+ const layers = [...materialization.backdrop.bottomLayers];
3598
+ for (const cell of materialization.cells) {
3599
+ if (cell.frame) {
3600
+ layers.push(
3601
+ ...placeLayersInRect(
3602
+ cell.frame.layers,
3603
+ cell.frame.viewport,
3604
+ cell.frame.rect,
3605
+ `cell-${cell.index}-frame`
3606
+ )
3607
+ );
3608
+ }
3609
+ layers.push(...placeLayersInRect(cell.layers, cell.viewport, cell.rect, `cell-${cell.index}`));
3610
+ if (cell.frame && cell.frame.overlayLayers.length > 0) {
3611
+ layers.push(
3612
+ ...placeLayersInRect(
3613
+ cell.frame.overlayLayers,
3614
+ cell.frame.overlayViewport,
3615
+ cell.rect,
3616
+ `cell-${cell.index}-overlay`
3617
+ )
3618
+ );
3619
+ }
3620
+ }
3621
+ if (materialization.title) {
3622
+ layers.push(
3623
+ ...placeLayersInRect(
3624
+ materialization.title.layers,
3625
+ materialization.title.viewport,
3626
+ materialization.title.rect,
3627
+ "dashboard-title"
3628
+ )
3629
+ );
3630
+ }
3631
+ layers.push(...materialization.backdrop.topLayers);
3632
+ return layers;
3633
+ }
3634
+
2104
3635
  // src/doc/applyNarrationTiming.ts
2105
3636
  var MIN_BLOCK_SIMILARITY = 0.5;
2106
3637
  var MIN_V1_SCRIPT_SIMILARITY = 0.8;
@@ -3215,7 +4746,7 @@ function middleOut(from, to) {
3215
4746
  const mid = Math.floor((lo + hi) / 2);
3216
4747
  return rows.sort((a, b) => Math.abs(a - mid) - Math.abs(b - mid) || a - b);
3217
4748
  }
3218
- function clamp(value, lo, hi) {
4749
+ function clamp2(value, lo, hi) {
3219
4750
  return Math.max(lo, Math.min(hi, value));
3220
4751
  }
3221
4752
  function drawVertical(args, s, t, exitCol, enterCol, dir) {
@@ -3232,8 +4763,8 @@ function drawVertical(args, s, t, exitCol, enterCol, dir) {
3232
4763
  };
3233
4764
  const straightOk = exitCol >= t.c0 + 1 && exitCol <= t.c1 - 1 && exitCol >= s.c0 + 1 && exitCol <= s.c1 - 1;
3234
4765
  if (straightOk || gap < 2) {
3235
- const sCol = clamp(exitCol, s.c0 + 1, s.c1 - 1);
3236
- const tCol = clamp(exitCol, t.c0 + 1, t.c1 - 1);
4766
+ const sCol = clamp2(exitCol, s.c0 + 1, s.c1 - 1);
4767
+ const tCol = clamp2(exitCol, t.c0 + 1, t.c1 - 1);
3237
4768
  junction(exitRow, sCol, dir === 1 ? "bottom" : "top");
3238
4769
  junction(enterRow, tCol, dir === 1 ? "top" : "bottom");
3239
4770
  if (sCol === tCol && gap >= 1) {
@@ -3265,7 +4796,7 @@ function drawVertical(args, s, t, exitCol, enterCol, dir) {
3265
4796
  }
3266
4797
  return;
3267
4798
  }
3268
- const midRow = clamp(
4799
+ const midRow = clamp2(
3269
4800
  Math.floor((exitRow + enterRow) / 2) + stagger,
3270
4801
  Math.min(firstFree, lastFree),
3271
4802
  Math.max(firstFree, lastFree)
@@ -3366,8 +4897,8 @@ function drawHorizontal(args, s, t, exitRow, enterRow, dir) {
3366
4897
  };
3367
4898
  const straightOk = exitRow >= t.r0 + 1 && exitRow <= t.r1 - 1 && exitRow >= s.r0 + 1 && exitRow <= s.r1 - 1;
3368
4899
  if (straightOk || gap < 2) {
3369
- const sRow = clamp(exitRow, s.r0 + 1, s.r1 - 1);
3370
- const tRow = clamp(exitRow, t.r0 + 1, t.r1 - 1);
4900
+ const sRow = clamp2(exitRow, s.r0 + 1, s.r1 - 1);
4901
+ const tRow = clamp2(exitRow, t.r0 + 1, t.r1 - 1);
3371
4902
  junction(sRow, exitCol, dir === 1 ? "right" : "left");
3372
4903
  junction(tRow, enterCol, dir === 1 ? "left" : "right");
3373
4904
  if (sRow === tRow && gap >= 1) {
@@ -3400,7 +4931,7 @@ function drawHorizontal(args, s, t, exitRow, enterRow, dir) {
3400
4931
  }
3401
4932
  return;
3402
4933
  }
3403
- const midCol = clamp(
4934
+ const midCol = clamp2(
3404
4935
  Math.floor((exitCol + enterCol) / 2) + args.stagger,
3405
4936
  Math.min(firstFree, lastFree),
3406
4937
  Math.max(firstFree, lastFree)
@@ -4028,6 +5559,8 @@ export {
4028
5559
  MAX_COVER_SLIDE_DURATION_SECONDS,
4029
5560
  resolveCoverSlideSettings,
4030
5561
  docToMarkdown,
5562
+ documentTitleFromFileName,
5563
+ buildPreviewDoc,
4031
5564
  resolveThemeForDoc,
4032
5565
  isTemplatedPageBlock,
4033
5566
  resolvePageBlock,
@@ -4039,6 +5572,37 @@ export {
4039
5572
  pageStyleDataAttributes,
4040
5573
  PAGE_BASE_CSS,
4041
5574
  buildPageCss,
5575
+ DASHBOARD_ZOOM_LEVELS,
5576
+ normalizeDashboardZoom,
5577
+ desiredCellZoom,
5578
+ resolveDashboardZooms,
5579
+ validateDashboardLayoutDefinition,
5580
+ layoutCapacity,
5581
+ transposeCells,
5582
+ resolveLayoutCells,
5583
+ FRONTMATTER_DASHBOARD_LAYOUTS_KEY,
5584
+ readDashboardLayoutsFromFrontmatter,
5585
+ writeDashboardLayoutsToFrontmatter,
5586
+ BUILTIN_DASHBOARD_LAYOUTS,
5587
+ getDashboardLayoutSummaries,
5588
+ listDashboardLayouts,
5589
+ DASHBOARD_AUTO_LAYOUT_ID,
5590
+ resolveDashboardLayoutDefinition,
5591
+ chooseDashboardLayout,
5592
+ DASHBOARD_STYLE_IDS,
5593
+ DEFAULT_DASHBOARD_STYLE,
5594
+ DASHBOARD_STYLES,
5595
+ resolveDashboardStyleId,
5596
+ dashboardCanvasFill,
5597
+ dashboardCellAccent,
5598
+ buildDashboardCellChrome,
5599
+ stripsBlockBackdrop,
5600
+ stripBlockBackdropLayer,
5601
+ DASHBOARD_FRONTMATTER_KEYS,
5602
+ DEFAULT_DASHBOARD_SETTINGS,
5603
+ resolveDashboardSettings,
5604
+ materializeDashboard,
5605
+ composeDashboardLayers,
4042
5606
  applyNarrationTiming,
4043
5607
  scoreTextSimilarity,
4044
5608
  resolveAudioMapping,