@bendyline/squisq 2.7.1 → 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,29 +2,35 @@ 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,
13
16
  hasTemplate,
14
17
  isDataFence,
15
18
  isShapeName,
16
19
  lintTemplateParams,
17
20
  markdownToDoc,
21
+ materializeBlockLayersWithRuntime,
18
22
  nearestName,
19
23
  normalizeShapeKind,
20
24
  parseDataFence,
21
25
  parseNarrationTimingJson,
22
26
  parseStandaloneAnnotation,
27
+ placeLayersInRect,
23
28
  readCustomThemesFromFrontmatter,
29
+ resolvePersistentLayers,
24
30
  templateRegistry,
25
31
  writeCustomTemplatesToFrontmatter,
26
32
  writeCustomThemesToFrontmatter
27
- } from "./chunk-ENNNQIYV.js";
33
+ } from "./chunk-V6NV4GDE.js";
28
34
  import {
29
35
  iconMarker
30
36
  } from "./chunk-7ZAAICW4.js";
@@ -53,11 +59,18 @@ import {
53
59
  FRONTMATTER_CUSTOM_TEMPLATES_KEY,
54
60
  FRONTMATTER_CUSTOM_THEMES_KEY,
55
61
  hexHueDegrees,
62
+ oklchDarken,
63
+ oklchLighten,
64
+ relativeLuminance,
56
65
  resolveFontFamily,
57
- resolveTheme
66
+ resolveTheme,
67
+ withAlpha
58
68
  } from "./chunk-SBAX4ZPO.js";
59
69
  import {
60
70
  VIEWPORT_PRESETS,
71
+ calculateFontScale,
72
+ createTemplateContext,
73
+ getViewportOrientation,
61
74
  isTemplateBlock
62
75
  } from "./chunk-BAOV476U.js";
63
76
  import {
@@ -953,7 +966,7 @@ function buildPreviewDoc(doc, options) {
953
966
  }
954
967
  slides.push(slide);
955
968
  }
956
- const unusedImages = allImages.filter((img) => !usedImageSrcs.has(img.src));
969
+ const unusedImages = options?.interleaveImages ?? true ? allImages.filter((img) => !usedImageSrcs.has(img.src)) : [];
957
970
  if (unusedImages.length > 0 && slides.length > 0) {
958
971
  const interval = Math.max(2, Math.floor(slides.length / (unusedImages.length + 1)));
959
972
  let insertOffset = 0;
@@ -2439,6 +2452,1186 @@ ${varLines}
2439
2452
  ${PAGE_BASE_CSS}`;
2440
2453
  }
2441
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
+
2442
3635
  // src/doc/applyNarrationTiming.ts
2443
3636
  var MIN_BLOCK_SIMILARITY = 0.5;
2444
3637
  var MIN_V1_SCRIPT_SIMILARITY = 0.8;
@@ -3553,7 +4746,7 @@ function middleOut(from, to) {
3553
4746
  const mid = Math.floor((lo + hi) / 2);
3554
4747
  return rows.sort((a, b) => Math.abs(a - mid) - Math.abs(b - mid) || a - b);
3555
4748
  }
3556
- function clamp(value, lo, hi) {
4749
+ function clamp2(value, lo, hi) {
3557
4750
  return Math.max(lo, Math.min(hi, value));
3558
4751
  }
3559
4752
  function drawVertical(args, s, t, exitCol, enterCol, dir) {
@@ -3570,8 +4763,8 @@ function drawVertical(args, s, t, exitCol, enterCol, dir) {
3570
4763
  };
3571
4764
  const straightOk = exitCol >= t.c0 + 1 && exitCol <= t.c1 - 1 && exitCol >= s.c0 + 1 && exitCol <= s.c1 - 1;
3572
4765
  if (straightOk || gap < 2) {
3573
- const sCol = clamp(exitCol, s.c0 + 1, s.c1 - 1);
3574
- 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);
3575
4768
  junction(exitRow, sCol, dir === 1 ? "bottom" : "top");
3576
4769
  junction(enterRow, tCol, dir === 1 ? "top" : "bottom");
3577
4770
  if (sCol === tCol && gap >= 1) {
@@ -3603,7 +4796,7 @@ function drawVertical(args, s, t, exitCol, enterCol, dir) {
3603
4796
  }
3604
4797
  return;
3605
4798
  }
3606
- const midRow = clamp(
4799
+ const midRow = clamp2(
3607
4800
  Math.floor((exitRow + enterRow) / 2) + stagger,
3608
4801
  Math.min(firstFree, lastFree),
3609
4802
  Math.max(firstFree, lastFree)
@@ -3704,8 +4897,8 @@ function drawHorizontal(args, s, t, exitRow, enterRow, dir) {
3704
4897
  };
3705
4898
  const straightOk = exitRow >= t.r0 + 1 && exitRow <= t.r1 - 1 && exitRow >= s.r0 + 1 && exitRow <= s.r1 - 1;
3706
4899
  if (straightOk || gap < 2) {
3707
- const sRow = clamp(exitRow, s.r0 + 1, s.r1 - 1);
3708
- 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);
3709
4902
  junction(sRow, exitCol, dir === 1 ? "right" : "left");
3710
4903
  junction(tRow, enterCol, dir === 1 ? "left" : "right");
3711
4904
  if (sRow === tRow && gap >= 1) {
@@ -3738,7 +4931,7 @@ function drawHorizontal(args, s, t, exitRow, enterRow, dir) {
3738
4931
  }
3739
4932
  return;
3740
4933
  }
3741
- const midCol = clamp(
4934
+ const midCol = clamp2(
3742
4935
  Math.floor((exitCol + enterCol) / 2) + args.stagger,
3743
4936
  Math.min(firstFree, lastFree),
3744
4937
  Math.max(firstFree, lastFree)
@@ -4379,6 +5572,37 @@ export {
4379
5572
  pageStyleDataAttributes,
4380
5573
  PAGE_BASE_CSS,
4381
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,
4382
5606
  applyNarrationTiming,
4383
5607
  scoreTextSimilarity,
4384
5608
  resolveAudioMapping,