diff --git a/demo/layout_showcase/.superdeck/build_status.json b/demo/layout_showcase/.superdeck/build_status.json index 5d533521..fa1e4389 100644 --- a/demo/layout_showcase/.superdeck/build_status.json +++ b/demo/layout_showcase/.superdeck/build_status.json @@ -1,6 +1,5 @@ { "status": "success", - "timestamp": "2026-07-13T04:01:58.442714Z", - "slideCount": 13, - "error": null + "timestamp": "2026-09-25T21:32:53.564719Z", + "slideCount": 13 } \ No newline at end of file diff --git a/demo/layout_showcase/.superdeck/superdeck.json b/demo/layout_showcase/.superdeck/superdeck.json index 2e72ba89..629248dd 100644 --- a/demo/layout_showcase/.superdeck/superdeck.json +++ b/demo/layout_showcase/.superdeck/superdeck.json @@ -10,7 +10,7 @@ { "blocks": [ { - "content": "\n\n#### NATIVE AUTHORING / VISUAL PROOF\n\n# The system makes room.\n\n## Thirteen frames for layout, image control, and rich Markdown.\n\n**Sections. Blocks. Images. Type.** One coherent visual language.\n", + "type": "block", "align": "centerLeft", "flex": 5, "padding": { @@ -20,10 +20,10 @@ "left": 34.0 }, "scrollable": false, - "type": "block" + "content": "\n\n#### NATIVE AUTHORING / VISUAL PROOF\n\n# The system makes room.\n\n## Thirteen frames for layout, image control, and rich Markdown.\n\n**Sections. Blocks. Images. Type.** One coherent visual language.\n" }, { - "name": "image", + "type": "widget", "align": "center", "flex": 6, "margin": { @@ -39,7 +39,7 @@ "left": 0.0 }, "scrollable": false, - "type": "widget", + "name": "image", "src": "layout_showcase/assets/architectural_steps.png", "fit": "cover", "scale": 1.04 @@ -63,7 +63,7 @@ { "blocks": [ { - "content": "\n\n#### 01 / PREDICTABLE GEOMETRY\n\n## The layout model explains itself.\n", + "type": "block", "align": "centerLeft", "flex": 1, "padding": { @@ -73,7 +73,7 @@ "left": 24.0 }, "scrollable": false, - "type": "block" + "content": "\n\n#### 01 / PREDICTABLE GEOMETRY\n\n## The layout model explains itself.\n" } ], "flex": 1, @@ -83,22 +83,22 @@ { "blocks": [ { - "content": "\n\n### SECTION = HEIGHT\n\nSections stack **vertically**. Their flex values decide how much of the slide each row receives.\n", + "type": "block", "flex": 2, "scrollable": false, - "type": "block" + "content": "\n\n### SECTION = HEIGHT\n\nSections stack **vertically**. Their flex values decide how much of the slide each row receives.\n" }, { - "content": "\n\n### 2×\n\nThis block gets twice the width of its neighbor.\n", + "type": "block", "flex": 1, "scrollable": false, - "type": "block" + "content": "\n\n### 2×\n\nThis block gets twice the width of its neighbor.\n" }, { - "content": "\n\n### 1×\n\nThe ratio stays stable as the canvas scales.\n", + "type": "block", "flex": 1, "scrollable": false, - "type": "block" + "content": "\n\n### 1×\n\nThe ratio stays stable as the canvas scales.\n" } ], "align": "center", @@ -109,23 +109,23 @@ { "blocks": [ { - "content": "\n\n#### MARGIN\n\nSpace outside the framed block.\n", + "type": "block", "flex": 1, "scrollable": false, - "type": "block" + "content": "\n\n#### MARGIN\n\nSpace outside the framed block.\n" }, { - "content": "\n\n#### PADDING\n\nSpace inside the frame. **Decoration stays between them**, so the box model remains visible and teachable.\n", + "type": "block", "flex": 2, "scrollable": false, - "type": "block" + "content": "\n\n#### PADDING\n\nSpace inside the frame. **Decoration stays between them**, so the box model remains visible and teachable.\n" }, { - "content": "#### ALIGN\n\nContent lands at any of nine anchor points.", + "type": "block", "align": "bottomRight", "flex": 1, "scrollable": false, - "type": "block" + "content": "#### ALIGN\n\nContent lands at any of nine anchor points." } ], "align": "center", @@ -137,7 +137,7 @@ "comments": [] }, { - "key": "7BuNcOQM", + "key": "LQmwzcJ0", "options": { "title": "Asymmetric composition", "style": "compact" @@ -146,11 +146,11 @@ { "blocks": [ { - "content": "\n\n#### 02 / SECTIONS + BLOCKS + IMAGES\n\n## Editorial, asymmetric, still deterministic.\n", + "type": "block", "align": "centerLeft", "flex": 1, "scrollable": false, - "type": "block" + "content": "\n\n#### 02 / SECTIONS + BLOCKS + IMAGES\n\n## Editorial, asymmetric, still deterministic.\n" } ], "flex": 1, @@ -160,7 +160,7 @@ { "blocks": [ { - "content": "\n\n### Build hierarchy before decoration.\n\nA strong composition begins with **one dominant relationship**. Here, a 4:7 split gives the image authority while the copy keeps a clear reading edge.\n\n- **Flex** sets the proportion.\n- **Spacing** creates the gutter.\n- **Alignment** pins the content.\n\n> The result feels art-directed because every constraint has a job.\n", + "type": "block", "align": "centerLeft", "flex": 4, "padding": { @@ -170,16 +170,16 @@ "left": 0.0 }, "scrollable": false, - "type": "block" + "content": "\n\n### Build hierarchy before decoration.\n\nA strong composition begins with **one dominant relationship**. Here, a 4:7 split gives the image authority while the copy keeps a clear reading edge.\n\n- **Flex** sets the proportion.\n- **Spacing** creates the gutter.\n- **Alignment** pins the content.\n\n> The result feels art-directed because every constraint has a job.\n" }, { - "name": "image", + "type": "widget", "align": "centerRight", "flex": 7, "margin": { - "top": 6.0, + "top": 10.0, "right": 0.0, - "bottom": 6.0, + "bottom": 10.0, "left": 0.0 }, "padding": { @@ -189,10 +189,10 @@ "left": 0.0 }, "scrollable": false, - "type": "widget", + "name": "image", "src": "layout_showcase/assets/architectural_steps.png", "fit": "cover", - "scale": 1.12 + "scale": 1 } ], "align": "center", @@ -203,7 +203,7 @@ { "blocks": [ { - "name": "image", + "type": "widget", "align": "centerLeft", "flex": 2, "margin": { @@ -219,12 +219,12 @@ "left": 0.0 }, "scrollable": false, - "type": "widget", + "name": "image", "src": "layout_showcase/assets/material_detail.png", "fit": "cover" }, { - "content": "\n\n#### SECONDARY RHYTHM\n\nRepeated image frames can change proportion without changing the rules around them.\n", + "type": "block", "align": "centerLeft", "flex": 4, "margin": { @@ -240,10 +240,10 @@ "left": 10.0 }, "scrollable": false, - "type": "block" + "content": "\n\n#### SECONDARY RHYTHM\n\nRepeated image frames can change proportion without changing the rules around them.\n" }, { - "name": "image", + "type": "widget", "align": "centerRight", "flex": 2, "margin": { @@ -259,7 +259,7 @@ "left": 0.0 }, "scrollable": false, - "type": "widget", + "name": "image", "src": "layout_showcase/assets/momentum_ribbon.png", "fit": "cover", "scale": 1.18 @@ -283,11 +283,11 @@ { "blocks": [ { - "content": "\n\n#### 03 / IMAGE FIT\n\n## Same source. Two honest choices.\n", + "type": "block", "align": "centerLeft", "flex": 1, "scrollable": false, - "type": "block" + "content": "\n\n#### 03 / IMAGE FIT\n\n## Same source. Two honest choices.\n" } ], "flex": 1, @@ -297,16 +297,16 @@ { "blocks": [ { - "content": "\n\n#### CONTAIN / FULL FRAME\n\n**Nothing is cropped.** Empty space is preserved.\n", + "type": "block", "flex": 1, "scrollable": false, - "type": "block" + "content": "\n\n#### CONTAIN / FULL FRAME\n\n**Nothing is cropped.** Empty space is preserved.\n" }, { - "content": "\n\n#### COVER / FULL BLEED\n\n**The frame is filled.** Edges are intentionally cropped.\n", + "type": "block", "flex": 1, "scrollable": false, - "type": "block" + "content": "\n\n#### COVER / FULL BLEED\n\n**The frame is filled.** Edges are intentionally cropped.\n" } ], "align": "center", @@ -317,7 +317,7 @@ { "blocks": [ { - "name": "image", + "type": "widget", "align": "center", "flex": 1, "padding": { @@ -327,14 +327,14 @@ "left": 0.0 }, "scrollable": false, - "type": "widget", + "name": "image", "src": "layout_showcase/assets/architectural_steps.png", "fit": "contain", "width": 560, "height": 420 }, { - "name": "image", + "type": "widget", "align": "center", "flex": 1, "padding": { @@ -344,7 +344,7 @@ "left": 0.0 }, "scrollable": false, - "type": "widget", + "name": "image", "src": "layout_showcase/assets/architectural_steps.png", "fit": "cover", "width": 560, @@ -369,11 +369,11 @@ { "blocks": [ { - "content": "\n\n#### 04 / SCALE + ALIGNMENT\n\n## Placement is a controlled variable.\n", + "type": "block", "align": "centerLeft", "flex": 1, "scrollable": false, - "type": "block" + "content": "\n\n#### 04 / SCALE + ALIGNMENT\n\n## Placement is a controlled variable.\n" } ], "flex": 1, @@ -383,7 +383,7 @@ { "blocks": [ { - "name": "image", + "type": "widget", "align": "topLeft", "flex": 1, "padding": { @@ -393,13 +393,13 @@ "left": 0.0 }, "scrollable": false, - "type": "widget", + "name": "image", "src": "layout_showcase/assets/architectural_steps.png", "fit": "contain", "scale": 0.72 }, { - "name": "image", + "type": "widget", "align": "center", "flex": 1, "padding": { @@ -409,13 +409,13 @@ "left": 0.0 }, "scrollable": false, - "type": "widget", + "name": "image", "src": "layout_showcase/assets/architectural_steps.png", "fit": "cover", "scale": 1 }, { - "name": "image", + "type": "widget", "align": "bottomRight", "flex": 1, "padding": { @@ -425,7 +425,7 @@ "left": 0.0 }, "scrollable": false, - "type": "widget", + "name": "image", "src": "layout_showcase/assets/architectural_steps.png", "fit": "cover", "scale": 1.35 @@ -439,7 +439,7 @@ { "blocks": [ { - "content": "\n\n#### 72% / TOP LEFT\n\nFull image, deliberate quiet space.\n", + "type": "block", "flex": 1, "margin": { "top": 14.0, @@ -448,10 +448,10 @@ "left": 0.0 }, "scrollable": false, - "type": "block" + "content": "\n\n#### 72% / TOP LEFT\n\nFull image, deliberate quiet space.\n" }, { - "content": "\n\n#### 100% / CENTER\n\nThe reliable default crop.\n", + "type": "block", "flex": 1, "margin": { "top": 14.0, @@ -460,10 +460,10 @@ "left": 0.0 }, "scrollable": false, - "type": "block" + "content": "\n\n#### 100% / CENTER\n\nThe reliable default crop.\n" }, { - "content": "#### 135% / BOTTOM RIGHT\n\nAn intentional detail view.", + "type": "block", "flex": 1, "margin": { "top": 14.0, @@ -472,7 +472,7 @@ "left": 0.0 }, "scrollable": false, - "type": "block" + "content": "#### 135% / BOTTOM RIGHT\n\nAn intentional detail view." } ], "align": "topLeft", @@ -493,11 +493,11 @@ { "blocks": [ { - "content": "\n\n#### 05 / FRAME SIZE + BLOCK MARGIN\n\n## Frame size is authored, not guessed.\n", + "type": "block", "align": "centerLeft", "flex": 1, "scrollable": false, - "type": "block" + "content": "\n\n#### 05 / FRAME SIZE + BLOCK MARGIN\n\n## Frame size is authored, not guessed.\n" } ], "flex": 1, @@ -507,7 +507,7 @@ { "blocks": [ { - "name": "image", + "type": "widget", "align": "center", "flex": 1, "margin": { @@ -523,13 +523,13 @@ "left": 0.0 }, "scrollable": false, - "type": "widget", + "name": "image", "src": "layout_showcase/assets/material_detail.png", "fit": "cover", "scale": 1.06 }, { - "name": "image", + "type": "widget", "align": "center", "flex": 1, "margin": { @@ -545,12 +545,12 @@ "left": 0.0 }, "scrollable": false, - "type": "widget", + "name": "image", "src": "layout_showcase/assets/architectural_steps.png", "fit": "cover" }, { - "name": "image", + "type": "widget", "align": "centerRight", "flex": 1, "margin": { @@ -566,7 +566,7 @@ "left": 0.0 }, "scrollable": false, - "type": "widget", + "name": "image", "src": "layout_showcase/assets/momentum_ribbon.png", "fit": "cover", "scale": 1.14 @@ -580,22 +580,22 @@ { "blocks": [ { - "content": "\n\n#### PORTRAIT / TALL\n\nHorizontal margin narrows the decorated image block.\n", + "type": "block", "flex": 1, "scrollable": false, - "type": "block" + "content": "\n\n#### PORTRAIT / TALL\n\nHorizontal margin narrows the decorated image block.\n" }, { - "content": "\n\n#### SQUARE / BALANCED\n\nTwo-axis margin creates a compact focal tile.\n", + "type": "block", "flex": 1, "scrollable": false, - "type": "block" + "content": "\n\n#### SQUARE / BALANCED\n\nTwo-axis margin creates a compact focal tile.\n" }, { - "content": "#### LANDSCAPE / WIDE\n\nVertical margin produces a cinematic banner.", + "type": "block", "flex": 1, "scrollable": false, - "type": "block" + "content": "#### LANDSCAPE / WIDE\n\nVertical margin produces a cinematic banner." } ], "align": "topLeft", @@ -607,7 +607,7 @@ "comments": [] }, { - "key": "UFjcGQIQ", + "key": "X18HmHk2", "options": { "title": "Image treatment matrix", "style": "compact" @@ -616,11 +616,11 @@ { "blocks": [ { - "content": "\n\n#### 06 / FIT + SURFACE TREATMENT\n\n## One image block can carry many behaviors.\n", + "type": "block", "align": "centerLeft", "flex": 1, "scrollable": false, - "type": "block" + "content": "\n\n#### 06 / FIT + SURFACE TREATMENT\n\n## One image block can carry many behaviors.\n" } ], "flex": 1, @@ -630,7 +630,7 @@ { "blocks": [ { - "content": "\n\n#### FIT WIDTH\n\n### Width leads.\n\nThe source fills the horizontal axis first.\n", + "type": "block", "align": "centerLeft", "flex": 2, "margin": { @@ -640,10 +640,10 @@ "left": 0.0 }, "scrollable": false, - "type": "block" + "content": "\n\n#### FIT WIDTH\n\n### Width leads.\n\nThe source fills the horizontal axis first.\n" }, { - "name": "image", + "type": "widget", "align": "center", "flex": 4, "margin": { @@ -659,14 +659,14 @@ "left": 0.0 }, "scrollable": false, - "type": "widget", + "name": "image", "src": "layout_showcase/assets/material_detail.png", "fit": "fitWidth", "width": 320, "height": 205 }, { - "content": "\n\n#### FIT HEIGHT\n\n### Height leads.\n\nPadding adds an inset inside the frame.\n", + "type": "block", "align": "centerLeft", "flex": 2, "margin": { @@ -676,10 +676,10 @@ "left": 0.0 }, "scrollable": false, - "type": "block" + "content": "\n\n#### FIT HEIGHT\n\n### Height leads.\n\nPadding adds an inset inside the frame.\n" }, { - "name": "image", + "type": "widget", "align": "centerRight", "flex": 4, "margin": { @@ -695,7 +695,7 @@ "left": 12.0 }, "scrollable": false, - "type": "widget", + "name": "image", "src": "layout_showcase/assets/architectural_steps.png", "fit": "fitHeight", "width": 320, @@ -710,7 +710,7 @@ { "blocks": [ { - "content": "\n\n#### FILL\n\n### Frame leads.\n\nPixels stretch to occupy both axes.\n", + "type": "block", "align": "centerLeft", "flex": 2, "margin": { @@ -720,10 +720,10 @@ "left": 0.0 }, "scrollable": false, - "type": "block" + "content": "\n\n#### FILL\n\n### Frame leads.\n\nPixels stretch to occupy both axes.\n" }, { - "name": "image", + "type": "widget", "align": "center", "flex": 4, "margin": { @@ -739,14 +739,14 @@ "left": 0.0 }, "scrollable": false, - "type": "widget", + "name": "image", "src": "layout_showcase/assets/momentum_ribbon.png", "fit": "fill", "width": 320, "height": 205 }, { - "content": "\n\n#### SCALE DOWN\n\n### Source leads.\n\nMargin and padding create a floating study.\n", + "type": "block", "align": "centerLeft", "flex": 2, "margin": { @@ -756,31 +756,31 @@ "left": 0.0 }, "scrollable": false, - "type": "block" + "content": "\n\n#### FULL BLEED\n\n### Frame leads.\n\nZero margin and padding let the image fill its card.\n" }, { - "name": "image", - "align": "bottomRight", + "type": "widget", + "align": "center", "flex": 4, "margin": { - "top": 16.0, - "right": 8.0, - "bottom": 16.0, - "left": 8.0 + "top": 0.0, + "right": 0.0, + "bottom": 0.0, + "left": 0.0 }, "padding": { - "top": 18.0, - "right": 18.0, - "bottom": 18.0, - "left": 18.0 + "top": 0.0, + "right": 0.0, + "bottom": 0.0, + "left": 0.0 }, "scrollable": false, - "type": "widget", + "name": "image", "src": "layout_showcase/assets/material_detail.png", - "fit": "scaleDown", + "fit": "cover", "width": 320, "height": 205, - "scale": 0.82 + "scale": 1 } ], "align": "center", @@ -801,11 +801,11 @@ { "blocks": [ { - "content": "\n\n#### 07 / RICH MARKDOWN\n\n## The content model has range.\n", + "type": "block", "align": "centerLeft", "flex": 1, "scrollable": false, - "type": "block" + "content": "\n\n#### 07 / RICH MARKDOWN\n\n## The content model has range.\n" } ], "flex": 1, @@ -815,16 +815,16 @@ { "blocks": [ { - "content": "\n\n### Inline hierarchy\n\n**Bold establishes consequence.** *Italic adds a second voice.* ~~Strikethrough retires the old idea.~~ A [link creates a path](https://superdeck.dev), and `inline code` names the mechanism.\n\n> Typography can carry structure before a single custom widget enters the frame.\n", + "type": "block", "flex": 5, "scrollable": false, - "type": "block" + "content": "\n\n### Inline hierarchy\n\n**Bold establishes consequence.** *Italic adds a second voice.* ~~Strikethrough retires the old idea.~~ A [link creates a path](https://superdeck.dev), and `inline code` names the mechanism.\n\n> Typography can carry structure before a single custom widget enters the frame.\n" }, { - "content": "\n\n### List rhythm\n\n- **Lead** with the decision.\n- *Qualify* it with context.\n- Use `code` for exact syntax.\n\n1. Establish the frame.\n2. Repeat the cadence.\n3. Break the pattern once.\n", + "type": "block", "flex": 4, "scrollable": false, - "type": "block" + "content": "\n\n### List rhythm\n\n- **Lead** with the decision.\n- *Qualify* it with context.\n- Use `code` for exact syntax.\n\n1. Establish the frame.\n2. Repeat the cadence.\n3. Break the pattern once.\n" } ], "align": "topLeft", @@ -835,22 +835,22 @@ { "blocks": [ { - "content": "\n\n#### STRONG\n\n**Make the signal unmistakable.**\n", + "type": "block", "flex": 1, "scrollable": false, - "type": "block" + "content": "\n\n#### STRONG\n\n**Make the signal unmistakable.**\n" }, { - "content": "\n\n#### EMPHASIS\n\n*Change the tone without changing size.*\n", + "type": "block", "flex": 1, "scrollable": false, - "type": "block" + "content": "\n\n#### EMPHASIS\n\n*Change the tone without changing size.*\n" }, { - "content": "#### LINK + CODE\n\n[Open the docs](https://superdeck.dev) or use `@section`.", + "type": "block", "flex": 1, "scrollable": false, - "type": "block" + "content": "#### LINK + CODE\n\n[Open the docs](https://superdeck.dev) or use `@section`." } ], "align": "center", @@ -871,11 +871,11 @@ { "blocks": [ { - "content": "\n\n#### 08 / TABLES\n\n## Dense information can still breathe.\n", + "type": "block", "align": "centerLeft", "flex": 1, "scrollable": false, - "type": "block" + "content": "\n\n#### 08 / TABLES\n\n## Dense information can still breathe.\n" } ], "flex": 1, @@ -885,14 +885,14 @@ { "blocks": [ { - "content": "\n\n| Layout signal | Before | After | Movement |\n| :-- | --: | --: | --: |\n| Time to first read | 8.4 s | **2.6 s** | **−69%** |\n| Competing focal points | 7 | **2** | **−71%** |\n| Grid exceptions | 12 | **1** | **−92%** |\n| Review confidence | 54% | **91%** | **+37 pt** |\n", + "type": "block", "align": "centerLeft", "flex": 7, "scrollable": false, - "type": "block" + "content": "\n\n| Layout signal | Before | After | Movement |\n| :-- | --: | --: | --: |\n| Time to first read | 8.4 s | **2.6 s** | **−69%** |\n| Competing focal points | 7 | **2** | **−71%** |\n| Grid exceptions | 12 | **1** | **−92%** |\n| Review confidence | 54% | **91%** | **+37 pt** |\n" }, { - "content": "#### THE READING\n\n## 3.2×\n\nfaster orientation after the hierarchy was simplified.\n\n> A table earns attention when the comparison—not the border—is the story.", + "type": "block", "align": "centerLeft", "flex": 3, "padding": { @@ -902,7 +902,7 @@ "left": 10.0 }, "scrollable": false, - "type": "block" + "content": "#### THE READING\n\n## 3.2×\n\nfaster orientation after the hierarchy was simplified.\n\n> A table earns attention when the comparison—not the border—is the story." } ], "align": "center", @@ -923,11 +923,11 @@ { "blocks": [ { - "content": "\n\n#### 09 / GITHUB ALERTS\n\n## Guidance with the right degree of urgency.\n", + "type": "block", "align": "centerLeft", "flex": 1, "scrollable": false, - "type": "block" + "content": "\n\n#### 09 / GITHUB ALERTS\n\n## Guidance with the right degree of urgency.\n" } ], "flex": 1, @@ -937,16 +937,16 @@ { "blocks": [ { - "content": "\n\n> [!NOTE]\n> **Sections own height.** Blocks inside them divide the available width.\n", + "type": "block", "flex": 1, "scrollable": false, - "type": "block" + "content": "\n\n> [!NOTE]\n> **Sections own height.** Blocks inside them divide the available width.\n" }, { - "content": "\n\n> [!TIP]\n> Reuse a stable spacing rhythm, then change only the value that carries meaning.\n", + "type": "block", "flex": 1, "scrollable": false, - "type": "block" + "content": "\n\n> [!TIP]\n> Reuse a stable spacing rhythm, then change only the value that carries meaning.\n" } ], "align": "topLeft", @@ -957,22 +957,22 @@ { "blocks": [ { - "content": "\n\n> [!IMPORTANT]\n> Keep the focal relationship obvious at thumbnail size and at full screen.\n", + "type": "block", "flex": 1, "scrollable": false, - "type": "block" + "content": "\n\n> [!IMPORTANT]\n> Keep the focal relationship obvious at thumbnail size and at full screen.\n" }, { - "content": "\n\n> [!WARNING]\n> Paint-only image scale can crop beyond its original fit. Review the final frame.\n", + "type": "block", "flex": 1, "scrollable": false, - "type": "block" + "content": "\n\n> [!WARNING]\n> Paint-only image scale can crop beyond its original fit. Review the final frame.\n" }, { - "content": "> [!CAUTION]\n> Do not hide critical state in color alone. Pair the accent with a clear label.", + "type": "block", "flex": 1, "scrollable": false, - "type": "block" + "content": "> [!CAUTION]\n> Do not hide critical state in color alone. Pair the accent with a clear label." } ], "align": "topLeft", @@ -993,11 +993,11 @@ { "blocks": [ { - "content": "\n\n#### 10 / INDICATORS\n\n## Status should scan before it explains.\n", + "type": "block", "align": "centerLeft", "flex": 1, "scrollable": false, - "type": "block" + "content": "\n\n#### 10 / INDICATORS\n\n## Status should scan before it explains.\n" } ], "flex": 1, @@ -1007,16 +1007,16 @@ { "blocks": [ { - "content": "\n\n### Release readiness\n\n- [x] Layout contract locked\n- [x] Image crops reviewed\n- [x] Contrast checked at 1280 × 720\n- [ ] Speaker notes approved\n- [ ] Final export published\n\n> **3 / 5 complete.** The remaining work is visible without opening another tool.\n", + "type": "block", "flex": 5, "scrollable": false, - "type": "block" + "content": "\n\n### Release readiness\n\n- [x] Layout contract locked\n- [x] Image crops reviewed\n- [x] Contrast checked at 1280 × 720\n- [ ] Speaker notes approved\n- [ ] Final export published\n\n> **3 / 5 complete.** The remaining work is visible without opening another tool.\n" }, { - "content": "### Signal board\n\n| Workstream | State | Owner |\n| :-- | :-- | :-- |\n| Layout | **● READY** | Design |\n| Content | **● REVIEW** | Editorial |\n| Capture | **● READY** | QA |\n| Export | **○ QUEUED** | Release |\n\n**READY** means verified. *REVIEW* means a decision remains. `QUEUED` means the path is known.", + "type": "block", "flex": 5, "scrollable": false, - "type": "block" + "content": "### Signal board\n\n| Workstream | State | Owner |\n| :-- | :-- | :-- |\n| Layout | **● READY** | Design |\n| Content | **● REVIEW** | Editorial |\n| Capture | **● READY** | QA |\n| Export | **○ QUEUED** | Release |\n\n**READY** means verified. *REVIEW* means a decision remains. `QUEUED` means the path is known." } ], "align": "topLeft", @@ -1037,11 +1037,11 @@ { "blocks": [ { - "content": "\n\n#### 11 / NATIVE FIRST, EXTENSIBLE WHEN NEEDED\n\n## One escape hatch, clearly bounded.\n", + "type": "block", "align": "centerLeft", "flex": 1, "scrollable": false, - "type": "block" + "content": "\n\n#### 11 / NATIVE FIRST, EXTENSIBLE WHEN NEEDED\n\n## One escape hatch, clearly bounded.\n" } ], "flex": 1, @@ -1051,7 +1051,7 @@ { "blocks": [ { - "content": "\n\n#### BUILT-IN AUTHORING\n\n### Most of this deck is only three primitives.\n\n- `@section` for vertical rhythm\n- `@block` for Markdown content\n- `@image` for visual media\n\n**No custom content widget is required** for the layouts, tables, alerts, checklists, or image studies.\n", + "type": "block", "align": "centerLeft", "flex": 3, "padding": { @@ -1061,10 +1061,10 @@ "left": 0.0 }, "scrollable": false, - "type": "block" + "content": "\n\n#### BUILT-IN AUTHORING\n\n### Most of this deck is only three primitives.\n\n- `@section` for vertical rhythm\n- `@block` for Markdown content\n- `@image` for visual media\n\n**No custom content widget is required** for the layouts, tables, alerts, checklists, or image studies.\n" }, { - "name": "image", + "type": "widget", "align": "center", "flex": 4, "padding": { @@ -1074,16 +1074,16 @@ "left": 0.0 }, "scrollable": false, - "type": "widget", + "name": "image", "src": "layout_showcase/assets/material_detail.png", "fit": "cover", "scale": 1.08 }, { - "name": "showcaseMetric", + "type": "widget", "flex": 3, "scrollable": false, - "type": "widget", + "name": "showcaseMetric", "value": "01", "label": "Registered widget", "detail": "This single metric card is the deliberately custom extension point in the content deck.", @@ -1109,7 +1109,7 @@ { "blocks": [ { - "name": "image", + "type": "widget", "align": "centerLeft", "flex": 4, "margin": { @@ -1125,13 +1125,13 @@ "left": 0.0 }, "scrollable": false, - "type": "widget", + "name": "image", "src": "layout_showcase/assets/momentum_ribbon.png", "fit": "cover", "scale": 1.24 }, { - "content": "#### 12 / THE OUTCOME\n\n# Built to be seen.\n\n## Every frame can be authored, inspected, and captured.\n\n**Native layouts. Rich Markdown. Controlled imagery.** One bounded widget extension and three original image assets.", + "type": "block", "align": "centerLeft", "flex": 5, "padding": { @@ -1141,7 +1141,7 @@ "left": 0.0 }, "scrollable": false, - "type": "block" + "content": "#### 12 / THE OUTCOME\n\n# Built to be seen.\n\n## Every frame can be authored, inspected, and captured.\n\n**Native layouts. Rich Markdown. Controlled imagery.** One bounded widget extension and three original image assets." } ], "align": "center", diff --git a/demo/layout_showcase/.superdeck/superdeck_full.json b/demo/layout_showcase/.superdeck/superdeck_full.json index e742e901..03b2e3eb 100644 --- a/demo/layout_showcase/.superdeck/superdeck_full.json +++ b/demo/layout_showcase/.superdeck/superdeck_full.json @@ -10,6 +10,16 @@ { "blocks": [ { + "type": "block", + "align": "centerLeft", + "flex": 5, + "padding": { + "top": 28.0, + "right": 34.0, + "bottom": 28.0, + "left": 34.0 + }, + "scrollable": false, "content": { "type": "document", "children": [ @@ -70,20 +80,10 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "align": "centerLeft", - "flex": 5, - "padding": { - "top": 28.0, - "right": 34.0, - "bottom": 28.0, - "left": 34.0 - }, - "scrollable": false, - "type": "block" + } }, { - "name": "image", + "type": "widget", "align": "center", "flex": 6, "margin": { @@ -99,7 +99,7 @@ "left": 0.0 }, "scrollable": false, - "type": "widget", + "name": "image", "src": "layout_showcase/assets/architectural_steps.png", "fit": "cover", "scale": 1.04 @@ -123,6 +123,16 @@ { "blocks": [ { + "type": "block", + "align": "centerLeft", + "flex": 1, + "padding": { + "top": 14.0, + "right": 24.0, + "bottom": 14.0, + "left": 24.0 + }, + "scrollable": false, "content": { "type": "document", "children": [ @@ -152,17 +162,7 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "align": "centerLeft", - "flex": 1, - "padding": { - "top": 14.0, - "right": 24.0, - "bottom": 14.0, - "left": 24.0 - }, - "scrollable": false, - "type": "block" + } } ], "flex": 1, @@ -172,6 +172,9 @@ { "blocks": [ { + "type": "block", + "flex": 2, + "scrollable": false, "content": { "type": "document", "children": [ @@ -214,12 +217,12 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 2, - "scrollable": false, - "type": "block" + } }, { + "type": "block", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -248,12 +251,12 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 1, - "scrollable": false, - "type": "block" + } }, { + "type": "block", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -282,10 +285,7 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 1, - "scrollable": false, - "type": "block" + } } ], "align": "center", @@ -296,6 +296,9 @@ { "blocks": [ { + "type": "block", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -324,12 +327,12 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 1, - "scrollable": false, - "type": "block" + } }, { + "type": "block", + "flex": 2, + "scrollable": false, "content": { "type": "document", "children": [ @@ -372,12 +375,13 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 2, - "scrollable": false, - "type": "block" + } }, { + "type": "block", + "align": "bottomRight", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -406,11 +410,7 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "align": "bottomRight", - "flex": 1, - "scrollable": false, - "type": "block" + } } ], "align": "center", @@ -422,7 +422,7 @@ "comments": [] }, { - "key": "7BuNcOQM", + "key": "LQmwzcJ0", "options": { "title": "Asymmetric composition", "style": "compact" @@ -431,6 +431,10 @@ { "blocks": [ { + "type": "block", + "align": "centerLeft", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -460,11 +464,7 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "align": "centerLeft", - "flex": 1, - "scrollable": false, - "type": "block" + } } ], "flex": 1, @@ -474,6 +474,16 @@ { "blocks": [ { + "type": "block", + "align": "centerLeft", + "flex": 4, + "padding": { + "top": 0.0, + "right": 18.0, + "bottom": 0.0, + "left": 0.0 + }, + "scrollable": false, "content": { "type": "document", "children": [ @@ -598,26 +608,16 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "align": "centerLeft", - "flex": 4, - "padding": { - "top": 0.0, - "right": 18.0, - "bottom": 0.0, - "left": 0.0 - }, - "scrollable": false, - "type": "block" + } }, { - "name": "image", + "type": "widget", "align": "centerRight", "flex": 7, "margin": { - "top": 6.0, + "top": 10.0, "right": 0.0, - "bottom": 6.0, + "bottom": 10.0, "left": 0.0 }, "padding": { @@ -627,10 +627,10 @@ "left": 0.0 }, "scrollable": false, - "type": "widget", + "name": "image", "src": "layout_showcase/assets/architectural_steps.png", "fit": "cover", - "scale": 1.12 + "scale": 1 } ], "align": "center", @@ -641,7 +641,7 @@ { "blocks": [ { - "name": "image", + "type": "widget", "align": "centerLeft", "flex": 2, "margin": { @@ -657,11 +657,27 @@ "left": 0.0 }, "scrollable": false, - "type": "widget", + "name": "image", "src": "layout_showcase/assets/material_detail.png", "fit": "cover" }, { + "type": "block", + "align": "centerLeft", + "flex": 4, + "margin": { + "top": 12.0, + "right": 0.0, + "bottom": 0.0, + "left": 0.0 + }, + "padding": { + "top": 0.0, + "right": 10.0, + "bottom": 0.0, + "left": 10.0 + }, + "scrollable": false, "content": { "type": "document", "children": [ @@ -690,26 +706,10 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "align": "centerLeft", - "flex": 4, - "margin": { - "top": 12.0, - "right": 0.0, - "bottom": 0.0, - "left": 0.0 - }, - "padding": { - "top": 0.0, - "right": 10.0, - "bottom": 0.0, - "left": 10.0 - }, - "scrollable": false, - "type": "block" + } }, { - "name": "image", + "type": "widget", "align": "centerRight", "flex": 2, "margin": { @@ -725,7 +725,7 @@ "left": 0.0 }, "scrollable": false, - "type": "widget", + "name": "image", "src": "layout_showcase/assets/momentum_ribbon.png", "fit": "cover", "scale": 1.18 @@ -749,6 +749,10 @@ { "blocks": [ { + "type": "block", + "align": "centerLeft", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -778,11 +782,7 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "align": "centerLeft", - "flex": 1, - "scrollable": false, - "type": "block" + } } ], "flex": 1, @@ -792,6 +792,9 @@ { "blocks": [ { + "type": "block", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -830,12 +833,12 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 1, - "scrollable": false, - "type": "block" + } }, { + "type": "block", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -874,10 +877,7 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 1, - "scrollable": false, - "type": "block" + } } ], "align": "center", @@ -888,7 +888,7 @@ { "blocks": [ { - "name": "image", + "type": "widget", "align": "center", "flex": 1, "padding": { @@ -898,14 +898,14 @@ "left": 0.0 }, "scrollable": false, - "type": "widget", + "name": "image", "src": "layout_showcase/assets/architectural_steps.png", "fit": "contain", "width": 560, "height": 420 }, { - "name": "image", + "type": "widget", "align": "center", "flex": 1, "padding": { @@ -915,7 +915,7 @@ "left": 0.0 }, "scrollable": false, - "type": "widget", + "name": "image", "src": "layout_showcase/assets/architectural_steps.png", "fit": "cover", "width": 560, @@ -940,6 +940,10 @@ { "blocks": [ { + "type": "block", + "align": "centerLeft", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -969,11 +973,7 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "align": "centerLeft", - "flex": 1, - "scrollable": false, - "type": "block" + } } ], "flex": 1, @@ -983,7 +983,7 @@ { "blocks": [ { - "name": "image", + "type": "widget", "align": "topLeft", "flex": 1, "padding": { @@ -993,13 +993,13 @@ "left": 0.0 }, "scrollable": false, - "type": "widget", + "name": "image", "src": "layout_showcase/assets/architectural_steps.png", "fit": "contain", "scale": 0.72 }, { - "name": "image", + "type": "widget", "align": "center", "flex": 1, "padding": { @@ -1009,13 +1009,13 @@ "left": 0.0 }, "scrollable": false, - "type": "widget", + "name": "image", "src": "layout_showcase/assets/architectural_steps.png", "fit": "cover", "scale": 1 }, { - "name": "image", + "type": "widget", "align": "bottomRight", "flex": 1, "padding": { @@ -1025,7 +1025,7 @@ "left": 0.0 }, "scrollable": false, - "type": "widget", + "name": "image", "src": "layout_showcase/assets/architectural_steps.png", "fit": "cover", "scale": 1.35 @@ -1039,6 +1039,15 @@ { "blocks": [ { + "type": "block", + "flex": 1, + "margin": { + "top": 14.0, + "right": 0.0, + "bottom": 0.0, + "left": 0.0 + }, + "scrollable": false, "content": { "type": "document", "children": [ @@ -1067,7 +1076,10 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, + } + }, + { + "type": "block", "flex": 1, "margin": { "top": 14.0, @@ -1076,9 +1088,6 @@ "left": 0.0 }, "scrollable": false, - "type": "block" - }, - { "content": { "type": "document", "children": [ @@ -1107,7 +1116,10 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, + } + }, + { + "type": "block", "flex": 1, "margin": { "top": 14.0, @@ -1116,9 +1128,6 @@ "left": 0.0 }, "scrollable": false, - "type": "block" - }, - { "content": { "type": "document", "children": [ @@ -1147,16 +1156,7 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 1, - "margin": { - "top": 14.0, - "right": 0.0, - "bottom": 0.0, - "left": 0.0 - }, - "scrollable": false, - "type": "block" + } } ], "align": "topLeft", @@ -1177,6 +1177,10 @@ { "blocks": [ { + "type": "block", + "align": "centerLeft", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -1206,11 +1210,7 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "align": "centerLeft", - "flex": 1, - "scrollable": false, - "type": "block" + } } ], "flex": 1, @@ -1220,7 +1220,7 @@ { "blocks": [ { - "name": "image", + "type": "widget", "align": "center", "flex": 1, "margin": { @@ -1236,13 +1236,13 @@ "left": 0.0 }, "scrollable": false, - "type": "widget", + "name": "image", "src": "layout_showcase/assets/material_detail.png", "fit": "cover", "scale": 1.06 }, { - "name": "image", + "type": "widget", "align": "center", "flex": 1, "margin": { @@ -1258,12 +1258,12 @@ "left": 0.0 }, "scrollable": false, - "type": "widget", + "name": "image", "src": "layout_showcase/assets/architectural_steps.png", "fit": "cover" }, { - "name": "image", + "type": "widget", "align": "centerRight", "flex": 1, "margin": { @@ -1279,7 +1279,7 @@ "left": 0.0 }, "scrollable": false, - "type": "widget", + "name": "image", "src": "layout_showcase/assets/momentum_ribbon.png", "fit": "cover", "scale": 1.14 @@ -1293,6 +1293,9 @@ { "blocks": [ { + "type": "block", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -1321,12 +1324,12 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 1, - "scrollable": false, - "type": "block" + } }, { + "type": "block", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -1355,12 +1358,12 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 1, - "scrollable": false, - "type": "block" + } }, { + "type": "block", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -1389,10 +1392,7 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 1, - "scrollable": false, - "type": "block" + } } ], "align": "topLeft", @@ -1404,7 +1404,7 @@ "comments": [] }, { - "key": "UFjcGQIQ", + "key": "X18HmHk2", "options": { "title": "Image treatment matrix", "style": "compact" @@ -1413,6 +1413,10 @@ { "blocks": [ { + "type": "block", + "align": "centerLeft", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -1442,11 +1446,7 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "align": "centerLeft", - "flex": 1, - "scrollable": false, - "type": "block" + } } ], "flex": 1, @@ -1456,6 +1456,16 @@ { "blocks": [ { + "type": "block", + "align": "centerLeft", + "flex": 2, + "margin": { + "top": 16.0, + "right": 0.0, + "bottom": 16.0, + "left": 0.0 + }, + "scrollable": false, "content": { "type": "document", "children": [ @@ -1495,20 +1505,10 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "align": "centerLeft", - "flex": 2, - "margin": { - "top": 16.0, - "right": 0.0, - "bottom": 16.0, - "left": 0.0 - }, - "scrollable": false, - "type": "block" + } }, { - "name": "image", + "type": "widget", "align": "center", "flex": 4, "margin": { @@ -1524,13 +1524,23 @@ "left": 0.0 }, "scrollable": false, - "type": "widget", + "name": "image", "src": "layout_showcase/assets/material_detail.png", "fit": "fitWidth", "width": 320, "height": 205 }, { + "type": "block", + "align": "centerLeft", + "flex": 2, + "margin": { + "top": 16.0, + "right": 0.0, + "bottom": 16.0, + "left": 0.0 + }, + "scrollable": false, "content": { "type": "document", "children": [ @@ -1570,20 +1580,10 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "align": "centerLeft", - "flex": 2, - "margin": { - "top": 16.0, - "right": 0.0, - "bottom": 16.0, - "left": 0.0 - }, - "scrollable": false, - "type": "block" + } }, { - "name": "image", + "type": "widget", "align": "centerRight", "flex": 4, "margin": { @@ -1599,7 +1599,7 @@ "left": 12.0 }, "scrollable": false, - "type": "widget", + "name": "image", "src": "layout_showcase/assets/architectural_steps.png", "fit": "fitHeight", "width": 320, @@ -1614,6 +1614,16 @@ { "blocks": [ { + "type": "block", + "align": "centerLeft", + "flex": 2, + "margin": { + "top": 16.0, + "right": 0.0, + "bottom": 16.0, + "left": 0.0 + }, + "scrollable": false, "content": { "type": "document", "children": [ @@ -1653,20 +1663,10 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "align": "centerLeft", - "flex": 2, - "margin": { - "top": 16.0, - "right": 0.0, - "bottom": 16.0, - "left": 0.0 - }, - "scrollable": false, - "type": "block" + } }, { - "name": "image", + "type": "widget", "align": "center", "flex": 4, "margin": { @@ -1682,13 +1682,23 @@ "left": 0.0 }, "scrollable": false, - "type": "widget", + "name": "image", "src": "layout_showcase/assets/momentum_ribbon.png", "fit": "fill", "width": 320, "height": 205 }, { + "type": "block", + "align": "centerLeft", + "flex": 2, + "margin": { + "top": 16.0, + "right": 0.0, + "bottom": 16.0, + "left": 0.0 + }, + "scrollable": false, "content": { "type": "document", "children": [ @@ -1698,10 +1708,10 @@ "children": [ { "type": "text", - "text": "SCALE DOWN" + "text": "FULL BLEED" } ], - "generatedId": "scale-down" + "generatedId": "full-bleed" }, { "type": "element", @@ -1709,10 +1719,10 @@ "children": [ { "type": "text", - "text": "Source leads." + "text": "Frame leads." } ], - "generatedId": "source-leads" + "generatedId": "frame-leads" }, { "type": "element", @@ -1720,7 +1730,7 @@ "children": [ { "type": "text", - "text": "Margin and padding create a floating study." + "text": "Zero margin and padding let the image fill its card." } ] } @@ -1728,41 +1738,31 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "align": "centerLeft", - "flex": 2, - "margin": { - "top": 16.0, - "right": 0.0, - "bottom": 16.0, - "left": 0.0 - }, - "scrollable": false, - "type": "block" + } }, { - "name": "image", - "align": "bottomRight", + "type": "widget", + "align": "center", "flex": 4, "margin": { - "top": 16.0, - "right": 8.0, - "bottom": 16.0, - "left": 8.0 + "top": 0.0, + "right": 0.0, + "bottom": 0.0, + "left": 0.0 }, "padding": { - "top": 18.0, - "right": 18.0, - "bottom": 18.0, - "left": 18.0 + "top": 0.0, + "right": 0.0, + "bottom": 0.0, + "left": 0.0 }, "scrollable": false, - "type": "widget", + "name": "image", "src": "layout_showcase/assets/material_detail.png", - "fit": "scaleDown", + "fit": "cover", "width": 320, "height": 205, - "scale": 0.82 + "scale": 1 } ], "align": "center", @@ -1783,6 +1783,10 @@ { "blocks": [ { + "type": "block", + "align": "centerLeft", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -1812,11 +1816,7 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "align": "centerLeft", - "flex": 1, - "scrollable": false, - "type": "block" + } } ], "flex": 1, @@ -1826,6 +1826,9 @@ { "blocks": [ { + "type": "block", + "flex": 5, + "scrollable": false, "content": { "type": "document", "children": [ @@ -1939,12 +1942,12 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 5, - "scrollable": false, - "type": "block" + } }, { + "type": "block", + "flex": 4, + "scrollable": false, "content": { "type": "document", "children": [ @@ -2069,10 +2072,7 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 4, - "scrollable": false, - "type": "block" + } } ], "align": "topLeft", @@ -2083,6 +2083,9 @@ { "blocks": [ { + "type": "block", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -2125,12 +2128,12 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 1, - "scrollable": false, - "type": "block" + } }, { + "type": "block", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -2181,12 +2184,12 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 1, - "scrollable": false, - "type": "block" + } }, { + "type": "block", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -2242,10 +2245,7 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 1, - "scrollable": false, - "type": "block" + } } ], "align": "center", @@ -2266,6 +2266,10 @@ { "blocks": [ { + "type": "block", + "align": "centerLeft", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -2295,11 +2299,7 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "align": "centerLeft", - "flex": 1, - "scrollable": false, - "type": "block" + } } ], "flex": 1, @@ -2309,6 +2309,10 @@ { "blocks": [ { + "type": "block", + "align": "centerLeft", + "flex": 7, + "scrollable": false, "content": { "type": "document", "children": [ @@ -2680,13 +2684,19 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "align": "centerLeft", - "flex": 7, - "scrollable": false, - "type": "block" + } }, { + "type": "block", + "align": "centerLeft", + "flex": 3, + "padding": { + "top": 0.0, + "right": 0.0, + "bottom": 0.0, + "left": 10.0 + }, + "scrollable": false, "content": { "type": "document", "children": [ @@ -2742,17 +2752,7 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "align": "centerLeft", - "flex": 3, - "padding": { - "top": 0.0, - "right": 0.0, - "bottom": 0.0, - "left": 10.0 - }, - "scrollable": false, - "type": "block" + } } ], "align": "center", @@ -2773,6 +2773,10 @@ { "blocks": [ { + "type": "block", + "align": "centerLeft", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -2802,11 +2806,7 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "align": "centerLeft", - "flex": 1, - "scrollable": false, - "type": "block" + } } ], "flex": 1, @@ -2816,6 +2816,9 @@ { "blocks": [ { + "type": "block", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -2865,12 +2868,12 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 1, - "scrollable": false, - "type": "block" + } }, { + "type": "block", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -2910,10 +2913,7 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 1, - "scrollable": false, - "type": "block" + } } ], "align": "topLeft", @@ -2924,6 +2924,9 @@ { "blocks": [ { + "type": "block", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -2963,12 +2966,12 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 1, - "scrollable": false, - "type": "block" + } }, { + "type": "block", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -3008,12 +3011,12 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 1, - "scrollable": false, - "type": "block" + } }, { + "type": "block", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -3053,10 +3056,7 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 1, - "scrollable": false, - "type": "block" + } } ], "align": "topLeft", @@ -3077,6 +3077,10 @@ { "blocks": [ { + "type": "block", + "align": "centerLeft", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -3106,11 +3110,7 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "align": "centerLeft", - "flex": 1, - "scrollable": false, - "type": "block" + } } ], "flex": 1, @@ -3120,6 +3120,9 @@ { "blocks": [ { + "type": "block", + "flex": 5, + "scrollable": false, "content": { "type": "document", "children": [ @@ -3281,12 +3284,12 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 5, - "scrollable": false, - "type": "block" + } }, { + "type": "block", + "flex": 5, + "scrollable": false, "content": { "type": "document", "children": [ @@ -3620,10 +3623,7 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 5, - "scrollable": false, - "type": "block" + } } ], "align": "topLeft", @@ -3644,6 +3644,10 @@ { "blocks": [ { + "type": "block", + "align": "centerLeft", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -3673,11 +3677,7 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "align": "centerLeft", - "flex": 1, - "scrollable": false, - "type": "block" + } } ], "flex": 1, @@ -3687,6 +3687,16 @@ { "blocks": [ { + "type": "block", + "align": "centerLeft", + "flex": 3, + "padding": { + "top": 0.0, + "right": 8.0, + "bottom": 0.0, + "left": 0.0 + }, + "scrollable": false, "content": { "type": "document", "children": [ @@ -3802,20 +3812,10 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "align": "centerLeft", - "flex": 3, - "padding": { - "top": 0.0, - "right": 8.0, - "bottom": 0.0, - "left": 0.0 - }, - "scrollable": false, - "type": "block" + } }, { - "name": "image", + "type": "widget", "align": "center", "flex": 4, "padding": { @@ -3825,16 +3825,16 @@ "left": 0.0 }, "scrollable": false, - "type": "widget", + "name": "image", "src": "layout_showcase/assets/material_detail.png", "fit": "cover", "scale": 1.08 }, { - "name": "showcaseMetric", + "type": "widget", "flex": 3, "scrollable": false, - "type": "widget", + "name": "showcaseMetric", "value": "01", "label": "Registered widget", "detail": "This single metric card is the deliberately custom extension point in the content deck.", @@ -3860,7 +3860,7 @@ { "blocks": [ { - "name": "image", + "type": "widget", "align": "centerLeft", "flex": 4, "margin": { @@ -3876,12 +3876,22 @@ "left": 0.0 }, "scrollable": false, - "type": "widget", + "name": "image", "src": "layout_showcase/assets/momentum_ribbon.png", "fit": "cover", "scale": 1.24 }, { + "type": "block", + "align": "centerLeft", + "flex": 5, + "padding": { + "top": 0.0, + "right": 54.0, + "bottom": 0.0, + "left": 0.0 + }, + "scrollable": false, "content": { "type": "document", "children": [ @@ -3942,17 +3952,7 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "align": "centerLeft", - "flex": 5, - "padding": { - "top": 0.0, - "right": 54.0, - "bottom": 0.0, - "left": 0.0 - }, - "scrollable": false, - "type": "block" + } } ], "align": "center", diff --git a/demo/layout_showcase/slides.md b/demo/layout_showcase/slides.md index 37c34544..626775a7 100644 --- a/demo/layout_showcase/slides.md +++ b/demo/layout_showcase/slides.md @@ -138,11 +138,11 @@ A strong composition begins with **one dominant relationship**. Here, a 4:7 spli @image { src: layout_showcase/assets/architectural_steps.png fit: cover - scale: 1.12 + scale: 1 flex: 7 align: centerRight padding: 0 - margin: { vertical: 6 } + margin: { vertical: 10 } } @section { @@ -489,22 +489,22 @@ Pixels stretch to occupy both axes. margin: { vertical: 16 } } -#### SCALE DOWN +#### FULL BLEED -### Source leads. +### Frame leads. -Margin and padding create a floating study. +Zero margin and padding let the image fill its card. @image { src: layout_showcase/assets/material_detail.png - fit: scaleDown + fit: cover width: 320 height: 205 - scale: 0.82 + scale: 1 flex: 4 - align: bottomRight - padding: 18 - margin: { horizontal: 8, vertical: 16 } + align: center + padding: 0 + margin: 0 } --- diff --git a/demo/lib/src/layout_showcase/layout_showcase.dart b/demo/lib/src/layout_showcase/layout_showcase.dart index e64f808a..9cdd3852 100644 --- a/demo/lib/src/layout_showcase/layout_showcase.dart +++ b/demo/lib/src/layout_showcase/layout_showcase.dart @@ -30,7 +30,7 @@ class LayoutShowcaseApp extends StatelessWidget { const LayoutShowcaseApp({ super.key, this.deckLoader, - this.transitionDuration = const Duration(milliseconds: 650), + this.transitionDuration = const Duration(milliseconds: 500), }); final DeckLoader? deckLoader; diff --git a/demo/lib/src/layout_showcase/showcase_parts.dart b/demo/lib/src/layout_showcase/showcase_parts.dart index 0d8362a5..8b94d4cb 100644 --- a/demo/lib/src/layout_showcase/showcase_parts.dart +++ b/demo/lib/src/layout_showcase/showcase_parts.dart @@ -9,10 +9,7 @@ class ShowcaseBackground extends StatelessWidget { static const _accents = [ Color(0xFFFF7A59), Color(0xFF8B7CFF), - Color(0xFFFFB15A), - Color(0xFFDF6FFF), Color(0xFF58D5C9), - Color(0xFFFF876B), ]; @override @@ -77,7 +74,7 @@ class ShowcaseFooter extends StatelessWidget implements PreferredSizeWidget { const Text( 'SUPERDECK / LAYOUT STUDY', style: TextStyle( - color: Color(0xFF77737D), + color: Color(0xFFA6A1AA), fontSize: 11, fontWeight: FontWeight.w600, letterSpacing: 2.2, diff --git a/demo/lib/src/layout_showcase/showcase_style.dart b/demo/lib/src/layout_showcase/showcase_style.dart index 1f9db006..fca341d8 100644 --- a/demo/lib/src/layout_showcase/showcase_style.dart +++ b/demo/lib/src/layout_showcase/showcase_style.dart @@ -16,6 +16,8 @@ const _mutedInk = Color(0xFFC9C6CE); const _coral = Color(0xFFFF8A65); const _violet = Color(0xFFA890FF); const _teal = Color(0xFF59D6C8); +const _cardRadius = 24.0; +const _cardBorder = Color(0x3DFFFFFF); SlideStyler showcaseBaseStyle() { return SlideStyler( @@ -163,9 +165,9 @@ SlideStyler showcaseBaseStyle() { decoration: BoxDecorationMix( color: const Color(0xFF121219), border: BorderMix.all( - BorderSideMix(color: const Color(0x2EFFFFFF), width: 1), + BorderSideMix(color: _cardBorder, width: 1), ), - borderRadius: BorderRadiusMix.circular(30), + borderRadius: BorderRadiusMix.circular(_cardRadius), ), ), ), @@ -269,7 +271,7 @@ SlideStyler showcasePanelStyle() { blockContainer: BlockStyler( padding: EdgeInsetsGeometryMix.symmetric(horizontal: 24, vertical: 22), margin: EdgeInsetsGeometryMix.symmetric(vertical: 8), - decoration: _panelDecoration(radius: 22), + decoration: _panelDecoration(radius: _cardRadius), ), ), ); @@ -333,9 +335,7 @@ BoxDecorationMix _panelDecoration({required double radius}) { end: Alignment.bottomRight, colors: const [Color(0x14FFFFFF), Color(0x08FFFFFF)], ), - border: BorderMix.all( - BorderSideMix(color: const Color(0x24FFFFFF), width: 1), - ), + border: BorderMix.all(BorderSideMix(color: _cardBorder, width: 1)), borderRadius: BorderRadiusMix.circular(radius), ); } diff --git a/docs/guides/markdown-authoring.mdx b/docs/guides/markdown-authoring.mdx index a013aa3b..6ff51f96 100644 --- a/docs/guides/markdown-authoring.mdx +++ b/docs/guides/markdown-authoring.mdx @@ -281,6 +281,12 @@ Add class names to animate elements between slides: Elements with matching class names animate smoothly during slide transitions. +For images, matching sources remain continuous when they can share a renderer; +pre-rendered endpoints and different sources cross-fade during the Hero flight. +Standalone images can also participate in +automatic image transitions by position; see the [`animateImages`](../reference/deck-options#animateimages) +reference for configuration details. + --- ## Overflow diagnostics diff --git a/docs/reference/deck-options.mdx b/docs/reference/deck-options.mdx index 4a35f9b1..b0ad7cf2 100644 --- a/docs/reference/deck-options.mdx +++ b/docs/reference/deck-options.mdx @@ -219,7 +219,9 @@ Pairs standalone Markdown images and `@image` blocks by order across consecutive slides so they animate through Hero transitions. Images nested in blockquotes, alerts, or list items do not take part. Set it to `false` to turn automatic image transitions off for the whole deck. Explicit Markdown Hero tags, -such as `![Diagram](assets/roadmap.png) {.hero-visual}`, are unaffected. +such as `![Diagram](assets/roadmap.png) {.hero-visual}`, are unaffected. When +paired images share a source, the flight keeps one continuous image when it can; +pre-rendered endpoints and different sources cross-fade. ```dart DeckOptions( diff --git a/docs/reference/markdown-syntax.mdx b/docs/reference/markdown-syntax.mdx index a7129769..1c691c8e 100644 --- a/docs/reference/markdown-syntax.mdx +++ b/docs/reference/markdown-syntax.mdx @@ -227,6 +227,6 @@ Use Markdown class markers to animate matching elements between slides. The firs Use the same tag for the element that should transition from one slide to the next. For example, one `{.heading}` on slide A can animate to one `{.heading}` on slide B. -Standalone Markdown images and `@image` blocks transition automatically by image order: the first image on one slide moves to the first on the next slide, the second to the second, and so on. This also blends different image sources. Use an explicit Markdown class marker when an image needs to match by a named identity instead of its position. Images without a partner on the adjacent slide render normally. Images nested in blockquotes, alerts, or list items do not take part, and `DeckOptions(animateImages: false)` turns automatic image transitions off for the whole deck. +Standalone Markdown images and `@image` blocks transition automatically by image order: the first image on one slide moves to the first on the next slide, the second to the second, and so on. Images with the same source keep one continuous image when they can share a renderer; pre-rendered endpoints and different sources cross-fade. Use an explicit Markdown class marker when an image needs to match by a named identity instead of its position. Images without a partner on the adjacent slide render normally. Images nested in blockquotes, alerts, or list items do not take part, and `DeckOptions(animateImages: false)` turns automatic image transitions off for the whole deck. Do not use the same tag twice on one slide. Each Hero tag must identify one element per slide, so use separate tags like `{.heading}` and `{.subheading}` when multiple elements should animate. diff --git a/packages/superdeck/lib/src/builtins/image_widget.dart b/packages/superdeck/lib/src/builtins/image_widget.dart index 4569a726..45e2871c 100644 --- a/packages/superdeck/lib/src/builtins/image_widget.dart +++ b/packages/superdeck/lib/src/builtins/image_widget.dart @@ -10,7 +10,7 @@ import '../rendering/blocks/block_provider.dart'; import '../rendering/blocks/image_hero_positions.dart'; import '../ui/widgets/cache_image_widget.dart'; import '../ui/widgets/hero_element.dart'; -import '../ui/widgets/image_hero_flight.dart'; +import '../ui/widgets/image_hero_flight_widget.dart'; import '../ui/widgets/provider.dart'; import '../ui/widgets/resolved_asset_image.dart'; import '../utils/converters.dart'; @@ -171,7 +171,7 @@ class ImageWidget extends StatelessWidget { child: buildElementHero( tag: automaticImageHeroTag(block.imageHeroStart), child: image, - buildFlight: buildImageHeroFlight, + flightShuttleBuilder: ImageHeroFlight.new, ), ); } diff --git a/packages/superdeck/lib/src/markdown/builders/image_element_builder.dart b/packages/superdeck/lib/src/markdown/builders/image_element_builder.dart index 27b3a5bc..1eeaed63 100644 --- a/packages/superdeck/lib/src/markdown/builders/image_element_builder.dart +++ b/packages/superdeck/lib/src/markdown/builders/image_element_builder.dart @@ -8,7 +8,7 @@ import '../../rendering/blocks/image_hero_positions.dart'; import '../../ui/widgets/cache_image_widget.dart'; import '../../ui/widgets/error_widgets.dart'; import '../../ui/widgets/hero_element.dart'; -import '../../ui/widgets/image_hero_flight.dart'; +import '../../ui/widgets/image_hero_flight_widget.dart'; import '../../ui/widgets/provider.dart'; import '../../ui/widgets/resolved_asset_image.dart'; import '../../utils/uri_validator.dart'; @@ -106,7 +106,7 @@ class ImageElementBuilder extends MarkdownElementBuilder size: totalSize, flightImage: image, ), - buildFlight: buildImageHeroFlight, + flightShuttleBuilder: ImageHeroFlight.new, ); } @@ -122,7 +122,7 @@ class ImageElementBuilder extends MarkdownElementBuilder child: imageWidget, heroTag: heroTag, heroData: ImageElement(spec: spec, uri: uri, size: totalSize), - buildFlight: buildImageHeroFlight, + flightShuttleBuilder: ImageHeroFlight.new, ); }, styleSpec: styleSpec, diff --git a/packages/superdeck/lib/src/markdown/markdown_hero_mixin.dart b/packages/superdeck/lib/src/markdown/markdown_hero_mixin.dart index 3dc557cd..15a8fa7d 100644 --- a/packages/superdeck/lib/src/markdown/markdown_hero_mixin.dart +++ b/packages/superdeck/lib/src/markdown/markdown_hero_mixin.dart @@ -14,8 +14,7 @@ mixin MarkdownHeroMixin on MarkdownElementBuilder { required Widget child, required String? heroTag, required T heroData, - Widget Function(BuildContext, T, T, double)? buildFlight, - HeroFlightShuttleBuilder? flightShuttleBuilder, + required HeroFlightShuttleBuilder flightShuttleBuilder, }) { final shouldAnimate = heroTag != null && !SlideConfiguration.of(context).isStaticRendering; @@ -27,7 +26,6 @@ mixin MarkdownHeroMixin on MarkdownElementBuilder { child: buildElementHero( tag: heroTag, child: child, - buildFlight: buildFlight, flightShuttleBuilder: flightShuttleBuilder, ), ); diff --git a/packages/superdeck/lib/src/ui/widgets/hero_element.dart b/packages/superdeck/lib/src/ui/widgets/hero_element.dart index e4d18ac9..2e395bcb 100644 --- a/packages/superdeck/lib/src/ui/widgets/hero_element.dart +++ b/packages/superdeck/lib/src/ui/widgets/hero_element.dart @@ -32,7 +32,8 @@ class ImageElement { final Uri uri; final Size size; - /// Rendered endpoint when an image uses an asset cache or `@image` layout. + /// Reuses an endpoint that cannot be reconstructed from [uri] and [spec], + /// such as a resolved asset, decoded image, or scaled `@image` layout. final Widget? flightImage; const ImageElement({ @@ -88,31 +89,24 @@ class HeroElement extends InheritedWidget { } } -/// Generic helper to build a Hero widget with custom flight animation. +/// Generic helper to build a Hero widget with a custom flight shuttle. /// -/// This eliminates duplication across text/code/image element builders by -/// providing a single implementation of the Hero + flightShuttleBuilder pattern. +/// This shares the Hero placeholder behavior across text, code, and image +/// element builders. The supplied shuttle builder owns its flight animation. /// /// Usage: /// ```dart /// buildElementHero( /// tag: 'myHero', /// child: StyledText('content'), -/// buildFlight: (context, from, to, t) { -/// return StyledText( -/// lerpString(from.text, to.text, t), -/// styleSpec: StyleSpec(spec: from.spec.lerp(to.spec, t)), -/// ); -/// }, +/// flightShuttleBuilder: buildTextHeroFlight, /// ) /// ``` Widget buildElementHero({ required String tag, required Widget child, - Widget Function(BuildContext context, T from, T to, double t)? buildFlight, - HeroFlightShuttleBuilder? flightShuttleBuilder, + required HeroFlightShuttleBuilder flightShuttleBuilder, }) { - assert(buildFlight != null || flightShuttleBuilder != null); return Hero( tag: tag, // A redirected page transition may rebuild the shuttle while either @@ -124,43 +118,7 @@ Widget buildElementHero({ size: size, child: Offstage(child: TickerMode(enabled: false, child: child)), ), - flightShuttleBuilder: - flightShuttleBuilder ?? - ( - BuildContext flightContext, - Animation animation, - HeroFlightDirection flightDirection, - BuildContext fromHeroContext, - BuildContext toHeroContext, - ) { - final to = HeroElement.of(toHeroContext); - final from = HeroElement.maybeOf(fromHeroContext) ?? to; - - // The shuttle is built inside the Navigator's Overlay, which sits - // outside the route's Material and therefore exposes a different - // DefaultTextStyle than the slide. Text properties an element spec - // leaves unset (letterSpacing, leadingDistribution, ...) would - // otherwise resolve against the Overlay's bare default and make the - // text change size the instant the Hero hands off to/from the real - // widget. Re-apply the source slide's DefaultTextStyle so the - // shuttle resolves identically to the widget it stands in for. - final slideTextStyle = DefaultTextStyle.of(fromHeroContext).style; - - return AnimatedBuilder( - animation: animation, - builder: (context, _) => DefaultTextStyle.merge( - style: slideTextStyle, - child: buildFlight!( - context, - from, - to, - flightDirection == HeroFlightDirection.push - ? animation.value - : 1 - animation.value, - ), - ), - ); - }, + flightShuttleBuilder: flightShuttleBuilder, child: child, ); } diff --git a/packages/superdeck/lib/src/ui/widgets/image_hero_flight.dart b/packages/superdeck/lib/src/ui/widgets/image_hero_flight.dart deleted file mode 100644 index 782ca567..00000000 --- a/packages/superdeck/lib/src/ui/widgets/image_hero_flight.dart +++ /dev/null @@ -1,41 +0,0 @@ -import 'package:flutter/widgets.dart'; -import 'package:mix/mix.dart'; - -import 'cache_image_widget.dart'; -import 'hero_element.dart'; - -/// Moves an image between frames while blending different image sources. -Widget buildImageHeroFlight( - BuildContext context, - ImageElement from, - ImageElement to, - double t, -) { - final spec = from.spec.lerp(to.spec, t); - final blend = Curves.easeInOut.transform(t); - - Widget image(ImageElement element) => - element.flightImage ?? - CachedImage( - key: ValueKey(element.uri), - uri: element.uri, - styleSpec: StyleSpec(spec: spec), - ); - - // Hero's overlay already interpolates the measured endpoint rectangles. - // A second size tween from block dimensions is wrong for intrinsically sized - // images and images with only one explicit dimension. - return SizedBox.expand( - key: const ValueKey('image-hero-flight'), - child: - from.uri == to.uri && from.flightImage == null && to.flightImage == null - ? image(from) - : Stack( - fit: StackFit.expand, - children: [ - Opacity(opacity: 1 - blend, child: image(from)), - Opacity(opacity: blend, child: image(to)), - ], - ), - ); -} diff --git a/packages/superdeck/lib/src/ui/widgets/image_hero_flight_widget.dart b/packages/superdeck/lib/src/ui/widgets/image_hero_flight_widget.dart new file mode 100644 index 00000000..33b2eefa --- /dev/null +++ b/packages/superdeck/lib/src/ui/widgets/image_hero_flight_widget.dart @@ -0,0 +1,75 @@ +import 'package:flutter/widgets.dart'; +import 'package:mix/mix.dart'; + +import 'cache_image_widget.dart'; +import 'hero_element.dart'; + +/// Moves an image between frames while blending different image sources. +class ImageHeroFlight extends StatelessWidget { + final ImageElement from; + final ImageElement to; + final Animation animation; + + /// Builds the shuttle directly from Flutter's Hero flight arguments. + factory ImageHeroFlight( + BuildContext context, + Animation animation, + HeroFlightDirection direction, + BuildContext fromContext, + BuildContext toContext, + ) { + final to = HeroElement.of(toContext); + final from = HeroElement.maybeOf(fromContext) ?? to; + return ImageHeroFlight._( + from: from, + to: to, + animation: direction == HeroFlightDirection.push + ? animation + : ReverseAnimation(animation), + ); + } + + const ImageHeroFlight._({ + required this.from, + required this.to, + required this.animation, + }); + + Widget _image(ImageElement element, ImageSpec spec) => + element.flightImage ?? + CachedImage( + key: ValueKey(element.uri), + uri: element.uri, + styleSpec: StyleSpec(spec: spec), + ); + + @override + Widget build(BuildContext context) => AnimatedBuilder( + animation: animation, + builder: (context, _) { + final t = animation.value; + final spec = from.spec.lerp(to.spec, t); + final blend = Curves.easeInOut.transform(t); + + // Hero's overlay already interpolates the measured endpoint rectangles. + // A second size tween from block dimensions is wrong for intrinsically sized + // images and images with only one explicit dimension. + final canReuseSource = + from.uri == to.uri && + from.flightImage == null && + to.flightImage == null; + return SizedBox.expand( + key: const ValueKey('image-hero-flight'), + child: canReuseSource + ? _image(from, spec) + : Stack( + fit: StackFit.expand, + children: [ + Opacity(opacity: 1 - blend, child: _image(from, spec)), + Opacity(opacity: blend, child: _image(to, spec)), + ], + ), + ); + }, + ); +} diff --git a/packages/superdeck/test/src/markdown/hero_transition_test.dart b/packages/superdeck/test/src/markdown/hero_transition_test.dart index a254f43e..6c74dd0b 100644 --- a/packages/superdeck/test/src/markdown/hero_transition_test.dart +++ b/packages/superdeck/test/src/markdown/hero_transition_test.dart @@ -9,7 +9,7 @@ import 'package:superdeck/src/styling/components/slide.dart'; import 'package:superdeck/src/styling/default_style.dart'; import 'package:superdeck/src/ui/widgets/cache_image_widget.dart'; import 'package:superdeck/src/ui/widgets/hero_element.dart'; -import 'package:superdeck/src/ui/widgets/image_hero_flight.dart'; +import 'package:superdeck/src/ui/widgets/image_hero_flight_widget.dart'; import 'package:superdeck/src/ui/widgets/provider.dart'; import 'package:superdeck_core/superdeck_core.dart'; @@ -232,6 +232,12 @@ void main() { await tester.pump(_transitionDuration ~/ 2); expect(tester.takeException(), isNull); + expect( + tester + .widget(find.byKey(const ValueKey('image-hero-flight'))) + .child, + isA(), + ); await tester.pumpAndSettle(); @@ -506,25 +512,56 @@ void main() { size: const Size(240, 540), flightImage: const ColoredBox(color: Colors.blue), ); + final fromKey = GlobalKey(); + final toKey = GlobalKey(); await tester.pumpWidget( MaterialApp( - home: Center( - child: SizedBox( - width: 180, - height: 120, - child: Builder( - builder: (context) => - buildImageHeroFlight(context, from, to, 0.5), + home: Column( + children: [ + HeroElement( + data: from, + child: Builder( + key: fromKey, + builder: (_) => const SizedBox.shrink(), + ), ), - ), + HeroElement( + data: to, + child: Builder( + key: toKey, + builder: (_) => const SizedBox.shrink(), + ), + ), + ], ), ), ); + final shuttle = ImageHeroFlight( + toKey.currentContext!, + const AlwaysStoppedAnimation(0.5), + HeroFlightDirection.push, + fromKey.currentContext!, + toKey.currentContext!, + ); + await tester.pumpWidget( + MaterialApp( + home: Center(child: SizedBox(width: 180, height: 120, child: shuttle)), + ), + ); expect( tester.getSize(find.byKey(const ValueKey('image-hero-flight'))), const Size(180, 120), ); + final flight = tester.widget( + find.byKey(const ValueKey('image-hero-flight')), + ); + final blend = flight.child! as Stack; + expect(blend.children, hasLength(2)); + expect((blend.children[0] as Opacity).child, same(from.flightImage)); + expect((blend.children[0] as Opacity).opacity, 0.5); + expect((blend.children[1] as Opacity).child, same(to.flightImage)); + expect((blend.children[1] as Opacity).opacity, 0.5); expect(tester.takeException(), isNull); }); diff --git a/packages/superdeck/test/src/ui/widgets/hero_element_test.dart b/packages/superdeck/test/src/ui/widgets/hero_element_test.dart index d1bd783c..56f9a0c2 100644 --- a/packages/superdeck/test/src/ui/widgets/hero_element_test.dart +++ b/packages/superdeck/test/src/ui/widgets/hero_element_test.dart @@ -2,16 +2,13 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:superdeck/src/ui/widgets/hero_element.dart'; +import 'package:superdeck/src/ui/widgets/text_hero_flight.dart'; /// Regression coverage for the "text size pops at the start/end of a hero /// transition" bug. /// -/// `buildElementHero` renders the flight shuttle inside the Navigator's -/// `Overlay`, which sits outside the route's `Material` and so exposes a -/// different `DefaultTextStyle` than the slide. Text properties an element -/// spec leaves unset (`letterSpacing`, `leadingDistribution`, ...) resolve -/// against that ambient style, so the shuttle used to render the text at a -/// different size than the real widget — a visible pop at the flight handoff. +/// A flight shuttle must resolve its own text style because it is built inside +/// the Navigator's Overlay, outside the route's Material. void main() { // A heading-like style (mirrors default_style.dart): sets the obvious // properties but leaves letterSpacing / leadingDistribution unset so they @@ -23,26 +20,11 @@ void main() { ); const text = 'Heading Sample'; const routeKey = ValueKey('route-text'); - const shuttleKey = ValueKey('shuttle-text'); - - Widget heroWidget() => HeroElement( - data: text, - child: buildElementHero( - tag: 'h', - // Mirrors StyledText -> Text(text, style: spec.style). - child: const Text(text, style: headingStyle, key: routeKey), - buildFlight: (context, from, to, t) { - // Mirrors TextElementBuilder._buildStableFlight at a flight endpoint: - // a bare Text.rich with no `style:` argument. - return const Text.rich( - TextSpan( - style: headingStyle, - children: [TextSpan(text: text)], - ), - key: shuttleKey, - ); - }, - ), + Widget heroWidget() => buildElementHero( + tag: 'h', + // Mirrors StyledText -> Text(text, style: spec.style). + child: const Text(text, style: headingStyle, key: routeKey), + flightShuttleBuilder: buildTextHeroFlight, ); testWidgets( @@ -69,10 +51,12 @@ void main() { await tester.pump(const Duration(milliseconds: 1)); // first flight frame final shuttlePara = tester.renderObject( - find.descendant( - of: find.byKey(shuttleKey), - matching: find.byType(RichText), - ), + find + .descendant( + of: find.byType(FittedBox), + matching: find.byType(RichText), + ) + .first, ); final shuttleWidth = shuttlePara.getMaxIntrinsicWidth(double.infinity); final shuttleHeight = shuttlePara.getMinIntrinsicHeight(double.infinity);