@cyber-dash-tech/revela 0.17.18 → 0.17.21

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.
package/README.md CHANGED
@@ -34,7 +34,7 @@ To install globally, add the same entry to `~/.config/opencode/opencode.json`.
34
34
  Requirements:
35
35
 
36
36
  - The Codex CLI must be installed and the `codex` command must be available in your shell.
37
- - Your environment must be able to run `npx`; Revela uses `npx -y @cyber-dash-tech/revela@0.17.12 mcp` to start the MCP server.
37
+ - Your environment must be able to run `npx`; Revela uses `npx -y @cyber-dash-tech/revela@0.17.21 mcp` to start the MCP server.
38
38
  - For interactive Review actions, `codex exec` must also work because the Review UI uses it for Insight and Comment/Apply Fix requests.
39
39
 
40
40
  Optional preflight:
@@ -55,11 +55,11 @@ npm_config_cache=/tmp/revela-npm-cache bun run smoke:mcp-pack
55
55
  Install Revela through the Codex Git marketplace:
56
56
 
57
57
  ```bash
58
- codex plugin marketplace add https://github.com/cyber-dash-tech/revela --ref v0.17.12
58
+ codex plugin marketplace add https://github.com/cyber-dash-tech/revela --ref v0.17.21
59
59
  codex plugin add revela@revela
60
60
  ```
61
61
 
62
- The Git marketplace install provides the Codex plugin shell, skills, hooks, and MCP configuration. When Codex starts the Revela MCP server for the first time, it runs `npx -y @cyber-dash-tech/revela@0.17.12 mcp` so npm can fetch the published package and its dependencies.
62
+ The Git marketplace install provides the Codex plugin shell, skills, hooks, and MCP configuration. When Codex starts the Revela MCP server for the first time, it runs `npx -y @cyber-dash-tech/revela@0.17.21 mcp` so npm can fetch the published package and its dependencies.
63
63
 
64
64
  You do not need to run `bun install` inside the Codex marketplace clone.
65
65
 
@@ -67,6 +67,28 @@ Start a new Codex thread after installing so Codex loads the Revela skills, MCP
67
67
 
68
68
  For release-aligned local validation, run `bun run smoke:mcp-pack`. It packs the current checkout to a temporary npm tarball and starts the MCP server through `npx`, matching the published Codex launcher path without requiring a registry publish.
69
69
 
70
+ #### Codex Upgrade
71
+
72
+ In Codex, ask Revela to check the current runtime version; the plugin calls `revela_doctor` and reports the running `version`.
73
+
74
+ For a fixed release tag, reinstall the plugin from that tag:
75
+
76
+ ```bash
77
+ codex plugin remove revela@revela
78
+ codex plugin marketplace remove revela
79
+ codex plugin marketplace add https://github.com/cyber-dash-tech/revela --ref vX.Y.Z
80
+ codex plugin add revela@revela
81
+ ```
82
+
83
+ For a marketplace entry that intentionally tracks a branch or movable ref, upgrade the marketplace clone and re-add the plugin:
84
+
85
+ ```bash
86
+ codex plugin marketplace upgrade revela
87
+ codex plugin add revela@revela
88
+ ```
89
+
90
+ The Git marketplace ref and `.mcp.json` npm pin are part of the same release artifact. Start a new Codex thread after upgrading so Codex reloads the Revela skills, MCP tools, hooks, and runtime pin.
91
+
70
92
  ## Built-In Designs
71
93
 
72
94
  Revela includes built-in deck designs:
package/README.zh-CN.md CHANGED
@@ -34,7 +34,7 @@ Revela 可在 [OpenCode](https://opencode.ai) 和 Codex 中使用,把来源材
34
34
  环境要求:
35
35
 
36
36
  - 需要已安装 Codex CLI,并且 shell 中可以执行 `codex`。
37
- - 环境中需要可以执行 `npx`;Revela 会用 `npx -y @cyber-dash-tech/revela@0.17.12 mcp` 启动 MCP server。
37
+ - 环境中需要可以执行 `npx`;Revela 会用 `npx -y @cyber-dash-tech/revela@0.17.21 mcp` 启动 MCP server。
38
38
  - 如果使用 Review UI 的 Insight、Comment 或 Apply Fix,需要 `codex exec` 可用。
39
39
 
40
40
  可选的安装前检查:
@@ -55,11 +55,11 @@ npm_config_cache=/tmp/revela-npm-cache bun run smoke:mcp-pack
55
55
  通过 Codex Git marketplace 安装 Revela:
56
56
 
57
57
  ```bash
58
- codex plugin marketplace add https://github.com/cyber-dash-tech/revela --ref v0.17.12
58
+ codex plugin marketplace add https://github.com/cyber-dash-tech/revela --ref v0.17.21
59
59
  codex plugin add revela@revela
60
60
  ```
61
61
 
62
- Git marketplace 安装的是 Codex plugin 壳、skills、hooks 和 MCP 配置。Codex 第一次启动 Revela MCP server 时,会运行 `npx -y @cyber-dash-tech/revela@0.17.12 mcp`,由 npm 获取已发布 package 及其 dependencies。
62
+ Git marketplace 安装的是 Codex plugin 壳、skills、hooks 和 MCP 配置。Codex 第一次启动 Revela MCP server 时,会运行 `npx -y @cyber-dash-tech/revela@0.17.21 mcp`,由 npm 获取已发布 package 及其 dependencies。
63
63
 
64
64
  不需要在 Codex marketplace clone 里运行 `bun install`。
65
65
 
@@ -67,6 +67,28 @@ Git marketplace 安装的是 Codex plugin 壳、skills、hooks 和 MCP 配置。
67
67
 
68
68
  如果要按发布路径做本地验证,运行 `bun run smoke:mcp-pack`。它会把当前 checkout 打成临时 npm tarball,再通过 `npx` 启动 MCP server,不需要先发布到 registry。
69
69
 
70
+ #### Codex 升级
71
+
72
+ 在 Codex 中,可以让 Revela 检查当前 runtime version;plugin 会调用 `revela_doctor` 并报告正在运行的 `version`。
73
+
74
+ 如果要固定到某个 release tag,按该 tag 重新安装 plugin:
75
+
76
+ ```bash
77
+ codex plugin remove revela@revela
78
+ codex plugin marketplace remove revela
79
+ codex plugin marketplace add https://github.com/cyber-dash-tech/revela --ref vX.Y.Z
80
+ codex plugin add revela@revela
81
+ ```
82
+
83
+ 如果 marketplace entry 本来就有意跟踪 branch 或 movable ref,升级 marketplace clone 后重新添加 plugin:
84
+
85
+ ```bash
86
+ codex plugin marketplace upgrade revela
87
+ codex plugin add revela@revela
88
+ ```
89
+
90
+ Git marketplace ref 和 `.mcp.json` npm pin 属于同一个 release artifact。升级后开启一个新的 Codex thread,让 Codex 重新加载 Revela skills、MCP tools、hooks 和 runtime pin。
91
+
70
92
  ## 内置设计
71
93
 
72
94
  Revela 内置多个 deck design:
@@ -480,6 +480,8 @@ These rules are mandatory for Monet.
480
480
  - **Sparse slides depend on image weight.** If content is light, the photo or page framing must hold the composition.
481
481
  - **No glass cards, neon KPI styling, or startup-product chrome.** Monet is editorial and print-adjacent.
482
482
  - **Visual hierarchy is strict:** eyebrow -> heading -> body -> caption.
483
+ - **Content pages need a stable title block.** Except cover, TOC, closing, section divider, and full-bleed hero slides, every normal content slide should include a visible slide-level title block from the upper-left safe area. It should contain a compact chapter/section label plus a slide title written as the page's claim or takeaway.
484
+ - **Do not hide the page title inside a component.** Body components may have their own headings, but `text-panel`, `box`, `toc`, chart/table headers, and editorial module headings do not replace the slide-level title block.
483
485
  - **Icon system is Lucide.** For ordinary UI, semantic, status, category, process, and navigation icons, use Lucide (`data-lucide`). Do not hand-write inline SVG for icons. SVG is allowed only for intentional decorative motifs, illustrations, or design-specific artwork. If any `data-lucide` icon is present, load Lucide via CDN and call `lucide.createIcons()` after `SlidePresentation`.
484
486
  - **Chart system is ECharts.** Data charts default to ECharts inside `echart-panel`. Do not use hand-written SVG, div/CSS shapes, canvas mocks, or static faux charts as data-chart substitutes. SVG remains acceptable for decorative motifs, diagrams, or illustrations, not data charts. Before creating or changing a chart, fetch the `echart-panel` component and `section: "chart-rules"`; if chart rules or runtime are unavailable, report the gap instead of inventing a fake chart fallback.
485
487
  - **Start from foundation.** New deck HTML starts from `@design:foundation`. Do not recreate foundation CSS, JavaScript, or the HTML skeleton from memory. Prefer a foundation helper when available; otherwise fetch `section: "foundation"` before writing a new deck shell. Existing deck edits preserve the current foundation unless the user asks for foundation repair or QA reports a foundation contract problem.
@@ -511,12 +513,14 @@ These rules are mandatory for Monet.
511
513
 
512
514
  ### Layout Types
513
515
 
514
- Each `<section class="slide">` must set `slide-qa="true"` or `slide-qa="false"`. It must also set `data-slide-index="N"`, where `N` is the canonical positive 1-based artifact slide identity from the approved deck plan or DOM order. Indexes must be unique and strictly increase. Use the QA column to decide which `slide-qa` value to write. Fetch any layout with the `revela-designs` tool (`action: "read"`, `layout: "<name>"`).
516
+ Each `<section class="slide">` must set `slide-qa="true"` or `slide-qa="false"`. It must also set `data-slide-index="N"`, where `N` is the canonical positive 1-based artifact slide identity from the approved deck plan or DOM order. Indexes must be unique and strictly increase. Use the QA column to decide which `slide-qa` value to write. Choose layouts by narrative structure first, not by surface geometry. Fetch any layout with the `revela-designs` tool (`action: "read"`, `layout: "<name>"`).
517
+
518
+ Normal `qa=true` content layouts must start with a slide-level title block unless the layout marker explicitly says otherwise. Use an eyebrow for chapter/section context, then an `h2` that states the slide's claim or takeaway. Body boxes, charts, media, tables, and text panels should sit below or beside that title region rather than replacing it.
515
519
 
516
520
  <!-- @layout:fullbleed:start qa=false -->
517
- #### Fullbleed
521
+ Atmospheric cover, closing, or divider layout with one dominant image-title component.
518
522
 
519
- Full-canvas layout for slides where a single image dominates the entire canvas with text composited over it. Use for opening (cover) and closing slides, or atmospheric section dividers.
523
+ #### Fullbleed
520
524
 
521
525
  Structural intent:
522
526
  - Single slot: place one `image-title` component directly inside `.page`. The component is self-contained — it manages its own image, blur, overlay, and text layers internally.
@@ -542,22 +546,26 @@ Structural intent:
542
546
  <!-- @layout:fullbleed:end -->
543
547
 
544
548
  <!-- @layout:narrative:start qa=true -->
545
- #### Narrative
549
+ Asymmetric primary-secondary spread with a dominant visual/evidence zone on the left and a narrower explanatory or action zone on the right.
546
550
 
547
- Asymmetric two-column layout with the left column wider (1.618fr) and the right column narrower (1fr). Use when one side needs more visual or reading weight than the other.
551
+ #### Narrative
548
552
 
549
553
  Structural intent:
550
- - left slot: wider zone (1.618fr) — can hold any component(s)
551
- - right slot: narrower zone (1fr) — can hold any component(s)
554
+ - left slot: primary zone (1.618fr) — dominant evidence, image, chart, table, or other high-weight component(s)
555
+ - right slot: secondary zone (1fr) — explanation, implication, action, TOC, vertical flow, or supporting component(s)
552
556
 
553
- Every slot accepts 1 or more components. The LLM decides what each slot contains there is no text/visual semantic preset.
557
+ Every slot accepts 1 or more components, but the layout should preserve a primary-secondary hierarchy. Do not use `narrative` for two equally weighted items; use `halves` instead.
554
558
 
555
559
 
556
560
  ```html
557
561
  <section class="slide" slide-qa="true" data-slide-index="N">
558
562
  <div class="slide-canvas">
559
563
  <div class="page" style="padding:0;overflow:hidden;">
560
- <div class="narrative-grid">
564
+ <div style="display:flex;flex-direction:column;gap:10px;margin:56px 64px 28px;max-width:760px;position:relative;z-index:2;">
565
+ <p class="eyebrow">Chapter / Section</p>
566
+ <h2>Slide claim or takeaway</h2>
567
+ </div>
568
+ <div class="narrative-grid" style="height:calc(100% - 150px);">
561
569
 
562
570
  <!-- [slot: left] — 1+ components; suggested: image-title, echart-panel, text-panel -->
563
571
  <div>
@@ -596,28 +604,32 @@ Every slot accepts 1 or more components. The LLM decides what each slot contains
596
604
 
597
605
  ##### Tips
598
606
  - **Grid container uses `.narrative-grid` class.** Applies `minmax(0, Nfr)` tracks, `overflow:hidden` on all children, and `align-items:stretch` so both columns fill the full row height. Do not override with `align-items:start` — that collapses columns to content height and exposes the page background.
599
- - **No semantic preset.** Either slot can hold any component. The wider left column naturally suits visually dominant content (full-bleed media, wide charts), but this is not a hard rule.
607
+ - **Primary-secondary intent.** The wider left column should carry the stronger visual or evidentiary weight. If both sides are equally important, use `halves`.
600
608
  - **Dark panel variant.** When a slot uses a dark background, override CSS variables on that container: `--text-primary`, `--text-secondary`, `--text-muted`, `--line`, `--line-strong` — all set to white-family values. Use `.page-number--light`.
601
609
  - **Background image inside a slot.** Use the three-layer z-index pattern: background `z-index:0`, dark overlay `z-index:1`, content `z-index:2`.
602
610
  <!-- @layout:narrative:end -->
603
611
 
604
612
  <!-- @layout:narrative-reverse:start qa=true -->
605
- #### Narrative Reverse
613
+ Asymmetric primary-secondary spread with the narrower explanatory or action zone on the left and the dominant visual/evidence zone on the right.
606
614
 
607
- Asymmetric two-column layout with the left column narrower (1fr) and the right column wider (1.618fr). Mirror of `narrative` — same grid class with `--reverse` modifier.
615
+ #### Narrative Reverse
608
616
 
609
617
  Structural intent:
610
- - left slot: narrower zone (1fr) — can hold any component(s)
611
- - right slot: wider zone (1.618fr) — can hold any component(s)
618
+ - left slot: secondary zone (1fr) — explanation, implication, action, TOC, vertical flow, or supporting component(s)
619
+ - right slot: primary zone (1.618fr) — dominant evidence, image, chart, table, or other high-weight component(s)
612
620
 
613
- Every slot accepts 1 or more components. The LLM decides what each slot contains there is no text/visual semantic preset.
621
+ Every slot accepts 1 or more components, but the layout should preserve a primary-secondary hierarchy. Do not use `narrative-reverse` for two equally weighted items; use `halves` instead.
614
622
 
615
623
 
616
624
  ```html
617
625
  <section class="slide" slide-qa="true" data-slide-index="N">
618
626
  <div class="slide-canvas">
619
627
  <div class="page" style="padding:0;overflow:hidden;">
620
- <div class="narrative-grid narrative-grid--reverse">
628
+ <div style="display:flex;flex-direction:column;gap:10px;margin:56px 64px 28px;max-width:760px;position:relative;z-index:2;">
629
+ <p class="eyebrow">Chapter / Section</p>
630
+ <h2>Slide claim or takeaway</h2>
631
+ </div>
632
+ <div class="narrative-grid narrative-grid--reverse" style="height:calc(100% - 150px);">
621
633
 
622
634
  <!-- [slot: left] — 1+ components; suggested: text-panel, toc, flow-vertical, echart-panel -->
623
635
  <div>
@@ -635,33 +647,33 @@ Every slot accepts 1 or more components. The LLM decides what each slot contains
635
647
 
636
648
  ##### Tips
637
649
  - **Same `.narrative-grid` class as `narrative`, with `--reverse` modifier.** Add both `narrative-grid` and `narrative-grid--reverse` to the grid container. The modifier swaps column proportions to `1fr left / 1.618fr right`.
638
- - **No semantic preset.** Either slot can hold any component — visual on the right, text on the left, or any other combination based on content needs.
650
+ - **Primary-secondary intent.** The wider right column should carry the stronger visual or evidentiary weight. If both sides are equally important, use `halves`.
639
651
  - **Dark panel variant.** Same CSS variable override pattern as `narrative`: set `--text-primary` etc. to white-family values on the panel container, all child components inherit automatically.
640
652
  <!-- @layout:narrative-reverse:end -->
641
653
 
642
654
  <!-- @layout:highlight-cols:start qa=true -->
643
- #### Highlight Cols
655
+ Parallel 3-5 item spread for comparable proof points, options, features, metrics, or evidence blocks.
644
656
 
645
- Equal N-column layout. Use when 3 or more parallel items of roughly equal visual weight should appear side by side — proof blocks, highlights, feature comparisons, stat groups, or any multi-column editorial spread.
657
+ #### Highlight Cols
646
658
 
647
- A short section header is optional but recommended. In Monet, that header should stay lean: eyebrow plus title only, with no intro paragraph competing with the columns below.
659
+ A short section header is required for normal content uses. In Monet, that header should stay lean: eyebrow plus title only, with no intro paragraph competing with the columns below.
648
660
 
649
661
  Structural intent:
650
662
  - each slot: 1fr column — any component(s)
651
663
  - column count: determined by the number of direct child divs in the grid container; `auto-fit` distributes space equally
652
664
 
653
- Every slot accepts 1 or more components. Add or remove child divs to control column count — 3 is the default, but 4 or 5 columns work equally well.
665
+ Every slot accepts 1 or more components. Add or remove child divs to control column count — 3 is the default, but 4 or 5 columns work equally well. Use only for comparable items; do not mix unrelated content types just because the slide needs multiple blocks.
654
666
 
655
667
  ```html
656
668
  <section class="slide" slide-qa="true" data-slide-index="N">
657
669
  <div class="slide-canvas">
658
- <div class="page">
659
- <div style="display:flex;flex-direction:column;gap:10px;margin-bottom:28px;max-width:520px;">
670
+ <div class="page" style="overflow:hidden;">
671
+ <div style="display:flex;flex-direction:column;gap:10px;margin:56px 64px 28px;max-width:760px;position:relative;z-index:2;">
660
672
  <p class="eyebrow">Section Label</p>
661
- <h2 style="font-size:52px;line-height:0.94;text-transform:uppercase;">Short framing title for the parallel columns</h2>
673
+ <h2 style="font-size:52px;line-height:0.94;text-transform:uppercase;">Slide claim or takeaway</h2>
662
674
  </div>
663
675
 
664
- <div class="highlight-cols-grid" style="flex:1;min-height:0;">
676
+ <div class="highlight-cols-grid" style="height:calc(100% - 150px);">
665
677
 
666
678
  <!-- [slot: 1] — 1+ components; suggested: editorial-image-top, editorial-text-top, echart-panel -->
667
679
  <div>
@@ -700,28 +712,32 @@ Every slot accepts 1 or more components. Add or remove child divs to control col
700
712
  - **Grid container needs `flex:1;min-height:0` inline** when inside `.page` (which is flex-column). The class handles column sizing; the inline style handles row stretch.
701
713
  - **Header stays lean.** If you add a section header above the grid, use only `eyebrow + title`. Do not add an intro paragraph; the columns themselves should carry the explanation.
702
714
  - **Column count = number of direct child divs.** `repeat(auto-fit, minmax(0, 1fr))` distributes available width equally across however many children exist. Add a 4th or 5th div to get 4 or 5 columns — no CSS change needed.
703
- - **Equal columns — no hierarchy.** All slots carry the same visual weight. Adjust content density to suit the slide purpose; do not artificially inflate one column to create false hierarchy.
715
+ - **Parallel items only.** All slots carry the same visual weight and should represent the same kind of thing: proof points, options, features, metrics, or evidence blocks.
704
716
  - **When using 4-5 columns, compress the header.** Keep the title to one or two short lines so the grid retains most of the slide height.
705
717
  - **Do not set fixed heights on editorial components.** Let components fill height via flexbox stretch.
706
718
  <!-- @layout:highlight-cols:end -->
707
719
 
708
720
  <!-- @layout:halves:start qa=true -->
709
- #### Halves
721
+ Balanced two-up comparison for two equally important items, charts, cases, evidence blocks, or before/after states.
710
722
 
711
- Equal two-column layout. Use when two items of equal visual weight should appear side by side — paired charts, dual evidence blocks, before/after comparisons, or any two-column editorial spread.
723
+ #### Halves
712
724
 
713
725
  Structural intent:
714
726
  - left slot: 1fr column — any component(s)
715
727
  - right slot: 1fr column — any component(s)
716
728
 
717
- Every slot accepts 1 or more components. The LLM decides what each slot contains both columns are fully equal with no hierarchy preset.
729
+ Every slot accepts 1 or more components. Both columns are fully equal by design. Do not use `halves` when one side should dominate; use `narrative` or `narrative-reverse` instead.
718
730
 
719
731
 
720
732
  ```html
721
733
  <section class="slide" slide-qa="true" data-slide-index="N">
722
734
  <div class="slide-canvas">
723
735
  <div class="page" style="overflow:hidden;">
724
- <div class="halves-grid" style="flex:1;min-height:0;">
736
+ <div style="display:flex;flex-direction:column;gap:10px;margin:56px 64px 28px;max-width:760px;position:relative;z-index:2;">
737
+ <p class="eyebrow">Chapter / Section</p>
738
+ <h2>Slide claim or takeaway</h2>
739
+ </div>
740
+ <div class="halves-grid" style="height:calc(100% - 150px);">
725
741
 
726
742
  <!-- [slot: left] — 1+ components; suggested: echart-panel, data-table, editorial-image-top -->
727
743
  <div>
@@ -755,27 +771,31 @@ Every slot accepts 1 or more components. The LLM decides what each slot contains
755
771
 
756
772
  ##### Tips
757
773
  - **Grid container needs `flex:1;min-height:0` inline** when inside `.page`. The class handles column sizing.
758
- - **Equal columns — no hierarchy.** Both slots carry the same weight. Choose components based on content, not a fixed text/visual assignment.
774
+ - **Equal columns — no hierarchy.** Both slots carry the same weight. Use this for balanced comparison, not primary-secondary explanation.
759
775
  - **Gap `40px` is intentional.** The slightly wider gap than `three-col` (32px) compensates for the larger individual column width.
760
776
  <!-- @layout:halves:end -->
761
777
 
762
778
  <!-- @layout:stacked:start qa=true -->
763
- #### Stacked
779
+ Top-bottom synthesis layout with compact framing, status, or process context above a larger evidence, table, chart, or detail zone.
764
780
 
765
- Two-row vertical layout in a fixed golden-ratio proportion: top row takes 1fr and bottom row takes 1.618fr. Use when a horizontal component (process flow, stat row, header band) should anchor the top, with a taller content zone below.
781
+ #### Stacked
766
782
 
767
783
  Structural intent:
768
- - top slot: `1fr` height — upper zone in golden-ratio proportion
769
- - bottom slot: `1.618fr` height — larger lower zone fills remaining space
784
+ - top slot: `1fr` height — compact framing, status row, process overview, or synthesis component
785
+ - bottom slot: `1.618fr` height — larger evidence, table, chart, media, or detail component
770
786
 
771
- Every slot accepts 1 or more components. The LLM decides what each slot contains there is no semantic preset for either row.
787
+ Every slot accepts 1 or more components, but the layout should preserve a synthesis-over-detail relationship. Do not use `stacked` as a generic vertical layout for unrelated blocks.
772
788
 
773
789
 
774
790
  ```html
775
791
  <section class="slide" slide-qa="true" data-slide-index="N">
776
792
  <div class="slide-canvas">
777
793
  <div class="page" style="padding:0;">
778
- <div class="stacked-grid">
794
+ <div style="display:flex;flex-direction:column;gap:10px;margin:56px 64px 28px;max-width:760px;position:relative;z-index:2;">
795
+ <p class="eyebrow">Chapter / Section</p>
796
+ <h2>Slide claim or takeaway</h2>
797
+ </div>
798
+ <div class="stacked-grid" style="height:calc(100% - 150px);">
779
799
 
780
800
  <!-- [slot: top] — 1+ components; suggested: flow-horizontal, stat-row -->
781
801
  <div class="stacked-top">
@@ -814,7 +834,7 @@ Every slot accepts 1 or more components. The LLM decides what each slot contains
814
834
  ##### Tips
815
835
  - **Top and bottom rows follow a fixed 1 : 1.618 golden-ratio proportion.** The top slot takes 1fr and the bottom takes 1.618fr — both rows are sized relative to the total canvas height, not by their content.
816
836
  - **Both slots clip overflow.** `min-height: 0` on both `.stacked-top` and `.stacked-bottom` ensures content cannot break out of its row.
817
- - **Both slots are fully equal in kind.** There is no preset for which slot holds "process" vs "data" place any combination of components that fits the slide narrative.
837
+ - **Synthesis over detail.** The top row should frame or summarize; the bottom row should carry the larger supporting detail.
818
838
  - **Dark background variant.** Set CSS variable overrides (`--text-primary` etc.) on `.stacked-grid` to cascade into both slots automatically.
819
839
  <!-- @layout:stacked:end -->
820
840
 
@@ -81,6 +81,7 @@ const VISUAL_QUALITY_RULES = `Visual extraction and CSS quality rules:
81
81
 
82
82
  const PREVIEW_REQUIREMENTS = `Preview requirements:
83
83
  - \`preview.html\` must include a cover slide and a closing slide. Mark their \`<section class="slide">\` elements with \`data-slide-role="cover"\` and \`data-slide-role="closing"\`.
84
+ - \`preview.html\` must define an explicit CSS rule for \`.slide-canvas\` with \`width: 1920px\` and \`height: 1080px\`; every direct \`.slide-canvas\` is the fixed 1920px x 1080px export surface.
84
85
  - \`preview.html\` must showcase every \`@component:*\` defined in \`DESIGN.md\`. Mark each showcased component with \`data-preview-component="<component-name>"\`.
85
86
  - Do not save with \`revela-designs-author\` until every component has a corresponding preview marker. If a component is decorative or abstract, include a visible labeled sample state.
86
87
  - When the design supports chart styling, \`preview.html\` should include a 3x3 ECharts gallery with at least 9 chart examples. This is a preview quality requirement, not a validation blocker.`
@@ -133,6 +134,7 @@ Hard requirements:
133
134
  - \`DESIGN.md\` must include valid \`@design\`, \`@layout\`, and \`@component\` markers.
134
135
  - \`DESIGN.md\` must include at least \`@design:foundation\`, \`@design:rules\`, one layout, and one component.
135
136
  - \`preview.html\` must be self-contained and directly openable in a browser.
137
+ - \`preview.html\` must include an explicit CSS rule: \`.slide-canvas { width: 1920px; height: 1080px; }\`.
136
138
  - Every preview slide must include \`slide-qa="true"\` or \`slide-qa="false"\`.
137
139
  - \`preview.html\` must include \`data-slide-role="cover"\` and \`data-slide-role="closing"\` on slide sections.
138
140
  - \`preview.html\` must showcase every \`@component:*\` with \`data-preview-component="<component-name>"\` before saving.
@@ -172,6 +174,7 @@ Hard requirements:
172
174
  - Preserve valid frontmatter and marker structure.
173
175
  - Preserve at least \`@design:foundation\`, \`@design:rules\`, one layout, and one component.
174
176
  - \`preview.html\` must be self-contained and directly openable in a browser.
177
+ - \`preview.html\` must include an explicit CSS rule: \`.slide-canvas { width: 1920px; height: 1080px; }\`.
175
178
  - Every preview slide must include \`slide-qa="true"\` or \`slide-qa="false"\`.
176
179
  - \`preview.html\` must include \`data-slide-role="cover"\` and \`data-slide-role="closing"\` on slide sections.
177
180
  - \`preview.html\` must showcase every \`@component:*\` with \`data-preview-component="<component-name>"\` before saving.
@@ -18,7 +18,7 @@ import {
18
18
  statSync,
19
19
  writeFileSync,
20
20
  } from "fs"
21
- import { join, resolve, basename } from "path"
21
+ import { dirname, join, resolve, basename } from "path"
22
22
  import { tmpdir } from "os"
23
23
  import { parseFrontmatter } from "../frontmatter"
24
24
  import {
@@ -55,6 +55,10 @@ export interface CreateDesignPackageArgs {
55
55
  overwrite?: boolean
56
56
  }
57
57
 
58
+ export interface CreateDesignDraftArgs extends CreateDesignPackageArgs {
59
+ workspaceRoot: string
60
+ }
61
+
58
62
  export interface CreateDesignPackageResult {
59
63
  ok: true
60
64
  name: string
@@ -64,6 +68,16 @@ export interface CreateDesignPackageResult {
64
68
  overwritten: boolean
65
69
  }
66
70
 
71
+ export interface InstallDesignDraftArgs {
72
+ workspaceRoot: string
73
+ name: string
74
+ overwrite?: boolean
75
+ }
76
+
77
+ export interface InstallDesignDraftResult extends CreateDesignPackageResult {
78
+ sourcePath: string
79
+ }
80
+
67
81
  export interface ValidateDesignPackageResult {
68
82
  ok: boolean
69
83
  name: string
@@ -250,6 +264,90 @@ export function createDesignPackage(args: CreateDesignPackageArgs): CreateDesign
250
264
  }
251
265
  }
252
266
 
267
+ /** Create a project-local design draft under .revela/drafts/designs/<name>/. */
268
+ export function createDesignDraftPackage(args: CreateDesignDraftArgs): CreateDesignPackageResult {
269
+ const name = normalizeDesignName(args.name)
270
+ const designMd = args.designMd?.trim()
271
+ const previewHtml = args.previewHtml?.trim()
272
+
273
+ if (!designMd) throw new Error("designMd is required")
274
+ if (!previewHtml) throw new Error("previewHtml is required")
275
+
276
+ const target = designDraftDir(args.workspaceRoot, name)
277
+ const existed = existsSync(target)
278
+ if (existed && !args.overwrite) {
279
+ throw new Error(`Design draft '${name}' already exists. Pass overwrite=true to replace it.`)
280
+ }
281
+
282
+ mkdirSync(dirname(target), { recursive: true })
283
+ if (existed) {
284
+ rmSync(target, { recursive: true, force: true })
285
+ }
286
+ mkdirSync(target, { recursive: true })
287
+ writeFileSync(join(target, "DESIGN.md"), `${designMd}\n`, "utf-8")
288
+ writeFileSync(join(target, "preview.html"), `${previewHtml}\n`, "utf-8")
289
+
290
+ const validation = validateDesignDraftPackage(args.workspaceRoot, name)
291
+ if (!validation.ok) {
292
+ throw new Error(`Created design draft is invalid: ${validation.errors.join("; ")}`)
293
+ }
294
+
295
+ return {
296
+ ok: true,
297
+ name,
298
+ path: target,
299
+ files: ["DESIGN.md", "preview.html"],
300
+ base: args.base,
301
+ overwritten: existed,
302
+ }
303
+ }
304
+
305
+ /** Validate a project-local design draft. */
306
+ export function validateDesignDraftPackage(workspaceRoot: string, nameInput: string): ValidateDesignPackageResult {
307
+ let name = nameInput
308
+ try {
309
+ name = normalizeDesignName(nameInput)
310
+ } catch {
311
+ // validateDesignPackageAt records the invalid-name error.
312
+ }
313
+ return validateDesignPackageAt(nameInput, designDraftDir(workspaceRoot, name))
314
+ }
315
+
316
+ /** Install a validated project-local design draft into the user-level design registry. */
317
+ export function installDesignDraftPackage(args: InstallDesignDraftArgs): InstallDesignDraftResult {
318
+ const name = normalizeDesignName(args.name)
319
+ const sourcePath = designDraftDir(args.workspaceRoot, name)
320
+ const validation = validateDesignDraftPackage(args.workspaceRoot, name)
321
+ if (!validation.ok) {
322
+ throw new Error(`Design draft is invalid: ${validation.errors.join("; ")}`)
323
+ }
324
+
325
+ const target = join(DESIGNS_DIR, name)
326
+ const existed = existsSync(target)
327
+ if (existed && !args.overwrite) {
328
+ throw new Error(`Design '${name}' already exists. Pass overwrite=true to replace it.`)
329
+ }
330
+
331
+ try {
332
+ mkdirSync(DESIGNS_DIR, { recursive: true })
333
+ if (existed) {
334
+ rmSync(target, { recursive: true, force: true })
335
+ }
336
+ cpSync(sourcePath, target, { recursive: true })
337
+ } catch (e) {
338
+ throw new Error(`Installing design draft requires write access to Revela user config at ${DESIGNS_DIR}: ${e instanceof Error ? e.message : String(e)}`)
339
+ }
340
+
341
+ return {
342
+ ok: true,
343
+ name,
344
+ path: target,
345
+ sourcePath,
346
+ files: ["DESIGN.md", "preview.html"],
347
+ overwritten: existed,
348
+ }
349
+ }
350
+
253
351
  function hasDataAttribute(html: string, attr: string, value: string): boolean {
254
352
  const escaped = value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
255
353
  return new RegExp(`${attr}\\s*=\\s*(["'])${escaped}\\1`).test(html)
@@ -264,8 +362,41 @@ function hasSlideRole(html: string, role: string): boolean {
264
362
  return false
265
363
  }
266
364
 
365
+ function cssRuleHasClassSelector(selectors: string, className: string): boolean {
366
+ const escaped = className.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
367
+ return new RegExp(`(^|[^a-zA-Z0-9_-])\\.${escaped}(?![a-zA-Z0-9_-])`).test(selectors)
368
+ }
369
+
370
+ function cssRuleHasFixedCanvasSize(body: string): boolean {
371
+ const width = /(?:^|[;\s])width\s*:\s*1920px(?:\s*!important)?\s*(?:;|$)/i.test(body)
372
+ const height = /(?:^|[;\s])height\s*:\s*1080px(?:\s*!important)?\s*(?:;|$)/i.test(body)
373
+ return width && height
374
+ }
375
+
376
+ function hasFixedSizeCssRule(html: string, className: "slide-canvas"): boolean {
377
+ const withoutComments = html.replace(/\/\*[\s\S]*?\*\//g, "")
378
+ const ruleRe = /([^{}]+)\{([^{}]+)\}/g
379
+ let match: RegExpExecArray | null
380
+ while ((match = ruleRe.exec(withoutComments)) !== null) {
381
+ if (cssRuleHasClassSelector(match[1] ?? "", className) && cssRuleHasFixedCanvasSize(match[2] ?? "")) {
382
+ return true
383
+ }
384
+ }
385
+ return false
386
+ }
387
+
267
388
  /** Validate a local design package for the minimum Revela design contract. */
268
389
  export function validateDesignPackage(nameInput: string): ValidateDesignPackageResult {
390
+ let name = nameInput
391
+ try {
392
+ name = normalizeDesignName(nameInput)
393
+ } catch {
394
+ // validateDesignPackageAt records the invalid-name error.
395
+ }
396
+ return validateDesignPackageAt(nameInput, join(DESIGNS_DIR, name))
397
+ }
398
+
399
+ function validateDesignPackageAt(nameInput: string, dir: string): ValidateDesignPackageResult {
269
400
  let name = nameInput
270
401
  const errors: string[] = []
271
402
  try {
@@ -274,7 +405,6 @@ export function validateDesignPackage(nameInput: string): ValidateDesignPackageR
274
405
  errors.push(e instanceof Error ? e.message : String(e))
275
406
  }
276
407
 
277
- const dir = join(DESIGNS_DIR, name)
278
408
  const mdPath = join(dir, "DESIGN.md")
279
409
  const previewPath = join(dir, "preview.html")
280
410
  const hasDesignMd = existsSync(mdPath)
@@ -312,6 +442,9 @@ export function validateDesignPackage(nameInput: string): ValidateDesignPackageR
312
442
  if (!preview.includes('<section class="slide"')) errors.push("preview.html must include slide sections")
313
443
  if (!preview.includes("slide-qa=")) errors.push("preview.html slides must include slide-qa attributes")
314
444
  if (!preview.includes("slide-canvas")) errors.push("preview.html must include .slide-canvas")
445
+ if (!hasFixedSizeCssRule(preview, "slide-canvas")) {
446
+ errors.push("preview.html must define .slide-canvas CSS with width: 1920px and height: 1080px")
447
+ }
315
448
  if (!hasSlideRole(preview, "cover")) errors.push('preview.html must include a slide section with data-slide-role="cover"')
316
449
  if (!hasSlideRole(preview, "closing")) errors.push('preview.html must include a slide section with data-slide-role="closing"')
317
450
  const missingComponents = components.filter((component) => !hasDataAttribute(preview, "data-preview-component", component))
@@ -334,6 +467,10 @@ export function validateDesignPackage(nameInput: string): ValidateDesignPackageR
334
467
  }
335
468
  }
336
469
 
470
+ function designDraftDir(workspaceRoot: string, name: string): string {
471
+ return resolve(workspaceRoot, ".revela", "drafts", "designs", name)
472
+ }
473
+
337
474
  // ---------------------------------------------------------------------------
338
475
  // Marker-based section / component parsing
339
476
  // ---------------------------------------------------------------------------