diff --git a/.agents/skills/superdeck-presentations/references/runtime-customization.md b/.agents/skills/superdeck-presentations/references/runtime-customization.md index 0c4d632c0..fe93fcd16 100644 --- a/.agents/skills/superdeck-presentations/references/runtime-customization.md +++ b/.agents/skills/superdeck-presentations/references/runtime-customization.md @@ -174,7 +174,10 @@ class MetricCard extends StatelessWidget { ### Ack-Generated Args Wrappers -Ack can also generate typed extension wrappers over a validated `Map`. This helps when you want typed getters without maintaining a manual args class. It does not change SuperDeck's widget factory contract; the factory still receives `Map args`. +Ack can also generate immutable typed models from a top-level schema. This +avoids maintaining a manual args class while preserving SuperDeck's widget +factory contract: the factory still receives `Map args` and +the generated model validates it at the boundary. Use this pattern only when the target app already has Ack codegen configured, or when you are intentionally adding it: @@ -191,9 +194,10 @@ import 'package:ack_annotations/ack_annotations.dart'; import 'package:flutter/widgets.dart'; import 'package:superdeck/superdeck.dart'; -part 'metric_card.g.dart'; +part 'metric_card.ack.dart'; +part 'metric_card.ack.g.dart'; -@AckType(name: 'MetricCardArgs') +@AckInfer() final metricCardArgsSchema = Ack.object({ 'label': Ack.string().notEmpty(), 'value': Ack.string().notEmpty(), @@ -201,10 +205,10 @@ final metricCardArgsSchema = Ack.object({ }); class MetricCard extends StatelessWidget { - final MetricCardArgsType data; + final MetricCardArgs data; MetricCard(Map args, {super.key}) - : data = MetricCardArgsType.parse(args); + : data = MetricCardArgs.parse(args); @override Widget build(BuildContext context) { @@ -221,8 +225,13 @@ dart run build_runner build --delete-conflicting-outputs Ack generation constraints that matter for SuperDeck widgets: -- Annotate top-level schema variables or getters with `@AckType()`. -- Generated extension types implement `Map` and expose `parse`, `safeParse`, and typed getters. +- Annotate top-level schema variables or getters with `@AckInfer()` and include + both generated part files. +- Let Ack derive the model name when the schema declaration already expresses + it (`metricCardArgsSchema` generates `MetricCardArgs`). Use `name:` only when + the desired public type cannot be derived from the declaration. +- Generated immutable models expose `parse`, `safeParse`, `fromJson`, `toJson`, + and typed fields. - Nested object fields should reference named top-level schemas when you need typed nested getters. - Do not expect `Ack.any()`/`Ack.anyOf()` or inline anonymous object branches to generate useful typed wrappers. - Keep `align`, `flex`, `margin`, `padding`, `scrollable`, and `name` out of diff --git a/.agents/skills/superdeck-presentations/references/verification.md b/.agents/skills/superdeck-presentations/references/verification.md index 5ae0695bd..262ce8739 100644 --- a/.agents/skills/superdeck-presentations/references/verification.md +++ b/.agents/skills/superdeck-presentations/references/verification.md @@ -113,7 +113,10 @@ Widget does not render: - Check that widget arguments do not collide with reserved block keys: `name`, `align`, `flex`, `margin`, `padding`, `scrollable`. - Remember that reserved block keys are consumed by SuperDeck and are not passed to custom widget args. - If using shorthand, verify the directive name exactly matches the registered widget name. -- If using Ack-generated args wrappers, confirm the schema is a top-level `@AckType()` declaration, the `part` file is present, generator dependencies are installed, and `build_runner` has regenerated the `.g.dart` file. +- If using Ack-generated args models, confirm the schema is a top-level + `@AckInfer()` declaration, both `.ack.dart` and `.ack.g.dart` part directives + are present, generator dependencies are installed, and `build_runner` has + regenerated both files. - Let the on-slide error guide factory parse/build failures. DartPad fails: diff --git a/AGENTS.md b/AGENTS.md index a425d8e69..cf85268bd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -119,7 +119,7 @@ fvm dart run melos run clean # Clean all Flutter build artifacts 8. `build` method (last) ### Generated Files -- Files matching `*.g.dart`, `*.mapper.dart` are auto-generated +- Files matching `*.g.dart` and `*.ack.dart` are auto-generated - Regenerate with `melos run build_runner:build` before testing - Commit generated files when they change and keep them synchronized with source updates @@ -170,10 +170,9 @@ lib/src/ ## Key Dependencies -- **dart_mappable**: Model serialization and discriminated unions - **mix/remix**: UI styling framework used throughout - **signals/signals_flutter**: Reactive state management -- **ack**: Schema validation for YAML configuration +- **ack**: Schema validation plus generated JSON models and discriminated unions - **markdown**: Markdown parsing - **go_router**: Navigation/routing @@ -187,8 +186,8 @@ The project uses Signals for reactive state management. `DeckController` is the Use the current stable Gemini model split for the Playground deck-generation pipeline: -- `gemini-3.5-flash` for the single global outline/planning request -- `gemini-3.1-flash-lite` for concurrent narrative-section composition and +- `gemini-3.7-flash` for the single global outline/planning request +- `gemini-3.5-flash-lite` for concurrent narrative-section composition and targeted outline/slide repair Keep model thinking at the lowest supported setting for this latency-sensitive diff --git a/CHANGELOG.md b/CHANGELOG.md index 267a5e7eb..9840db8ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,3 +6,4 @@ SuperDeck is a monorepo; each package keeps its own changelog: - [`superdeck_core`](packages/core/CHANGELOG.md) — parsing, models, and schema validation - [`superdeck_cli`](packages/cli/CHANGELOG.md) — the `superdeck` command-line tool - [`superdeck_builder`](packages/builder/CHANGELOG.md) — build_runner integration +- [`superdeck_pdf`](packages/plugins/pdf/CHANGELOG.md) — PDF export support diff --git a/demo/.superdeck/build_status.json b/demo/.superdeck/build_status.json index e2b3dcfab..5afce62ff 100644 --- a/demo/.superdeck/build_status.json +++ b/demo/.superdeck/build_status.json @@ -1,6 +1,5 @@ { "status": "success", - "timestamp": "2026-07-12T23:35:50.974090Z", - "slideCount": 34, - "error": null + "timestamp": "2026-08-25T18:24:50.927825Z", + "slideCount": 34 } \ No newline at end of file diff --git a/demo/.superdeck/superdeck.json b/demo/.superdeck/superdeck.json index f4178f682..3af9e3a12 100644 --- a/demo/.superdeck/superdeck.json +++ b/demo/.superdeck/superdeck.json @@ -6,11 +6,11 @@ { "blocks": [ { - "content": "# SuperDeck {.heading}\n# Build presentations with Flutter {.subheading}", + "type": "block", "align": "center", "flex": 1, "scrollable": false, - "type": "block" + "content": "# SuperDeck {.heading}\n# Build presentations with Flutter {.subheading}" } ], "flex": 2, @@ -27,18 +27,18 @@ { "blocks": [ { - "content": "\n\n#### Leo Farias {.heading}\n#### @leoafarias {.subheading}\n", + "type": "block", "align": "center", "flex": 1, "scrollable": false, - "type": "block" + "content": "\n\n#### Leo Farias {.heading}\n#### @leoafarias {.subheading}\n" }, { - "content": "- Founder/CEO/CTO\n- Open Source Contributor (fvm, mix, superdeck, others..)\n- Flutter & Dart GDE\n- Passionate about UI/UX/DX", + "type": "block", "align": "centerLeft", "flex": 1, "scrollable": false, - "type": "block" + "content": "- Founder/CEO/CTO\n- Open Source Contributor (fvm, mix, superdeck, others..)\n- Flutter & Dart GDE\n- Passionate about UI/UX/DX" } ], "flex": 1, @@ -55,16 +55,16 @@ { "blocks": [ { - "content": "## What is SuperDeck? {.heading}\n", + "type": "block", "flex": 1, "scrollable": false, - "type": "block" + "content": "## What is SuperDeck? {.heading}\n" }, { - "content": "- Write slides in **Markdown**\n- Render with **Flutter**\n- Use **custom widgets** in your slides", + "type": "block", "flex": 1, "scrollable": false, - "type": "block" + "content": "- Write slides in **Markdown**\n- Render with **Flutter**\n- Use **custom widgets** in your slides" } ], "flex": 1, @@ -81,23 +81,23 @@ { "blocks": [ { - "content": "", + "type": "block", "flex": 1, "scrollable": false, - "type": "block" + "content": "" }, { - "content": "\n### A developer-first presentation framework that combines the simplicity of Markdown with the power of Flutter. {.heading}\n", + "type": "block", "align": "center", "flex": 5, "scrollable": false, - "type": "block" + "content": "\n### A developer-first presentation framework that combines the simplicity of Markdown with the power of Flutter. {.heading}\n" }, { - "content": "", + "type": "block", "flex": 1, "scrollable": false, - "type": "block" + "content": "" } ], "flex": 1, @@ -114,10 +114,10 @@ { "blocks": [ { - "content": "## Key Features {.heading}\n", + "type": "block", "flex": 1, "scrollable": false, - "type": "block" + "content": "## Key Features {.heading}\n" } ], "flex": 1, @@ -127,25 +127,25 @@ { "blocks": [ { - "content": "\n### Markdown {.feat-markdown}\n\n- Simple syntax\n- Code blocks\n- Version control friendly\n", + "type": "block", "align": "topCenter", "flex": 1, "scrollable": false, - "type": "block" + "content": "\n### Markdown {.feat-markdown}\n\n- Simple syntax\n- Code blocks\n- Version control friendly\n" }, { - "content": "\n### Flutter {.feat-flutter}\n\n- Custom widgets\n- Hot reload\n- Cross-platform\n", + "type": "block", "align": "topCenter", "flex": 1, "scrollable": false, - "type": "block" + "content": "\n### Flutter {.feat-flutter}\n\n- Custom widgets\n- Hot reload\n- Cross-platform\n" }, { - "content": "### Styling {.feat-styling}\n\n- Themes\n- Custom styles\n- Responsive layouts", + "type": "block", "align": "topCenter", "flex": 1, "scrollable": false, - "type": "block" + "content": "### Styling {.feat-styling}\n\n- Themes\n- Custom styles\n- Responsive layouts" } ], "flex": 1, @@ -162,16 +162,16 @@ { "blocks": [ { - "content": "### Markdown-First {.heading}\n", + "type": "block", "flex": 1, "scrollable": false, - "type": "block" + "content": "### Markdown-First {.heading}\n" }, { - "content": "Write your presentations in familiar Markdown syntax:\n\n- Headers and text formatting\n- Code blocks with syntax highlighting\n- Lists and blockquotes\n- Custom widgets via `@widget` syntax", + "type": "block", "flex": 1, "scrollable": false, - "type": "block" + "content": "Write your presentations in familiar Markdown syntax:\n\n- Headers and text formatting\n- Code blocks with syntax highlighting\n- Lists and blockquotes\n- Custom widgets via `@widget` syntax" } ], "flex": 1, @@ -188,10 +188,10 @@ { "blocks": [ { - "content": "## Slide Layouts {.heading}\n\nSuperDeck supports flexible layouts using sections and columns.", + "type": "block", "flex": 1, "scrollable": false, - "type": "block" + "content": "## Slide Layouts {.heading}\n\nSuperDeck supports flexible layouts using sections and columns." } ], "flex": 1, @@ -208,10 +208,10 @@ { "blocks": [ { - "content": "## Predictable Layout Primitives {.heading}\n", + "type": "block", "flex": 1, "scrollable": false, - "type": "block" + "content": "## Predictable Layout Primitives {.heading}\n" } ], "flex": 1, @@ -221,7 +221,7 @@ { "blocks": [ { - "content": "\n\n### Edge to edge\n`padding: 0`\n", + "type": "block", "flex": 1, "padding": { "top": 0.0, @@ -230,10 +230,10 @@ "left": 0.0 }, "scrollable": false, - "type": "block" + "content": "\n\n### Edge to edge\n`padding: 0`\n" }, { - "content": "\n\n### Uniform inset\n`padding: 16`\n", + "type": "block", "flex": 1, "padding": { "top": 16.0, @@ -242,10 +242,10 @@ "left": 16.0 }, "scrollable": false, - "type": "block" + "content": "\n\n### Uniform inset\n`padding: 16`\n" }, { - "content": "### Independent axes\n`48 × 24`", + "type": "block", "align": "bottomRight", "flex": 1, "padding": { @@ -255,7 +255,7 @@ "left": 48.0 }, "scrollable": false, - "type": "block" + "content": "### Independent axes\n`48 × 24`" } ], "align": "center", @@ -275,10 +275,10 @@ { "blocks": [ { - "content": "## Spacing vs Margin vs Padding {.heading}\n", + "type": "block", "flex": 1, "scrollable": false, - "type": "block" + "content": "## Spacing vs Margin vs Padding {.heading}\n" } ], "flex": 1, @@ -288,13 +288,13 @@ { "blocks": [ { - "content": "\n\n### Spacing\n`spacing: 40` is the shared gap between sibling block frames.\n", + "type": "block", "flex": 1, "scrollable": false, - "type": "block" + "content": "\n\n### Spacing\n`spacing: 40` is the shared gap between sibling block frames.\n" }, { - "content": "\n\n### Margin\n`margin: 24` is consumed inside this block's frame, outside its border.\n", + "type": "block", "flex": 1, "margin": { "top": 24.0, @@ -303,10 +303,10 @@ "left": 24.0 }, "scrollable": false, - "type": "block" + "content": "\n\n### Margin\n`margin: 24` is consumed inside this block's frame, outside its border.\n" }, { - "content": "### Padding\n`padding` sits between the border and this content.", + "type": "block", "flex": 1, "padding": { "top": 24.0, @@ -315,7 +315,7 @@ "left": 48.0 }, "scrollable": false, - "type": "block" + "content": "### Padding\n`padding` sits between the border and this content." } ], "align": "center", @@ -333,10 +333,10 @@ { "blocks": [ { - "content": "## Image Framing Scale {.heading}\n", + "type": "block", "flex": 1, "scrollable": false, - "type": "block" + "content": "## Image Framing Scale {.heading}\n" } ], "flex": 1, @@ -346,7 +346,7 @@ { "blocks": [ { - "name": "image", + "type": "widget", "flex": 1, "padding": { "top": 0.0, @@ -355,7 +355,7 @@ "left": 0.0 }, "scrollable": false, - "type": "widget", + "name": "image", "src": "assets/concepta-icon.png", "fit": "contain", "width": 300, @@ -363,7 +363,7 @@ "scale": 1 }, { - "name": "image", + "type": "widget", "flex": 1, "padding": { "top": 0.0, @@ -372,7 +372,7 @@ "left": 0.0 }, "scrollable": false, - "type": "widget", + "name": "image", "src": "assets/concepta-icon.png", "fit": "contain", "width": 300, @@ -395,17 +395,17 @@ { "blocks": [ { - "content": "\n### Two Columns {.heading}\n", + "type": "block", "align": "centerRight", "flex": 2, "scrollable": false, - "type": "block" + "content": "\n### Two Columns {.heading}\n" }, { - "content": "```markdown\n@block {\n flex: 2\n}\nLeft content here\n\n@block {\n flex: 3\n}\nRight content here\n```", + "type": "block", "flex": 3, "scrollable": false, - "type": "block" + "content": "```markdown\n@block {\n flex: 2\n}\nLeft content here\n\n@block {\n flex: 3\n}\nRight content here\n```" } ], "flex": 1, @@ -422,11 +422,11 @@ { "blocks": [ { - "content": "\n### Top Section\n", + "type": "block", "align": "center", "flex": 1, "scrollable": false, - "type": "block" + "content": "\n### Top Section\n" } ], "flex": 1, @@ -436,11 +436,11 @@ { "blocks": [ { - "content": "\n### Middle Section (flex: 2)\n", + "type": "block", "align": "center", "flex": 1, "scrollable": false, - "type": "block" + "content": "\n### Middle Section (flex: 2)\n" } ], "flex": 2, @@ -450,11 +450,11 @@ { "blocks": [ { - "content": "### Bottom Section", + "type": "block", "align": "center", "flex": 1, "scrollable": false, - "type": "block" + "content": "### Bottom Section" } ], "flex": 1, @@ -471,16 +471,16 @@ { "blocks": [ { - "content": "### Code Blocks {.heading}\n", + "type": "block", "flex": 1, "scrollable": false, - "type": "block" + "content": "### Code Blocks {.heading}\n" }, { - "content": "```dart\nimport 'package:superdeck/superdeck.dart';\n\nvoid main() {\n runApp(\n SuperDeckApp(\n options: DeckOptions(\n widgets: {\n 'my-widget': myWidgetFactory,\n },\n ),\n ),\n );\n}\n```{.code}", + "type": "block", "flex": 2, "scrollable": false, - "type": "block" + "content": "```dart\nimport 'package:superdeck/superdeck.dart';\n\nvoid main() {\n runApp(\n SuperDeckApp(\n options: DeckOptions(\n widgets: {\n 'my-widget': myWidgetFactory,\n },\n ),\n ),\n );\n}\n```{.code}" } ], "flex": 1, @@ -497,16 +497,16 @@ { "blocks": [ { - "content": "## Custom Widgets {.heading}\n", + "type": "block", "flex": 1, "scrollable": false, - "type": "block" + "content": "## Custom Widgets {.heading}\n" }, { - "content": "Embed interactive Flutter widgets directly in your slides!", + "type": "block", "flex": 1, "scrollable": false, - "type": "block" + "content": "Embed interactive Flutter widgets directly in your slides!" } ], "flex": 1, @@ -523,10 +523,10 @@ { "blocks": [ { - "content": "### Ack-Generated Widget Args {.heading}\n", + "type": "block", "flex": 1, "scrollable": false, - "type": "block" + "content": "### Ack-Generated Widget Args {.heading}\n" } ], "flex": 1, @@ -536,11 +536,11 @@ { "blocks": [ { - "content": "\n\nAck can validate custom widget arguments and generate typed getters over the same `Map` payload.\n", + "type": "block", "align": "center", "flex": 1, "scrollable": false, - "type": "block" + "content": "\n\nAck can validate custom widget arguments and generate typed getters over the same `Map` payload.\n" } ], "flex": 1, @@ -550,17 +550,17 @@ { "blocks": [ { - "content": "\n\n```markdown\n@ack-metric-card {\n flex: 2\n align: center\n label: Activation\n value: \"72%\"\n caption: \"Parsed through Ack-generated typed getters\"\n tone: green\n}\n```\n", + "type": "block", "flex": 3, "scrollable": false, - "type": "block" + "content": "\n\n```markdown\n@ack-metric-card {\n flex: 2\n align: center\n label: Activation\n value: \"72%\"\n caption: \"Parsed through Ack-generated typed getters\"\n tone: green\n}\n```\n" }, { - "name": "ack-metric-card", + "type": "widget", "align": "center", "flex": 2, "scrollable": false, - "type": "widget", + "name": "ack-metric-card", "label": "Activation", "value": "72%", "caption": "Parsed through Ack-generated typed getters", @@ -581,29 +581,29 @@ { "blocks": [ { - "content": "### Mix Box Example {.heading}\n", + "type": "block", "flex": 1, "scrollable": false, - "type": "block" + "content": "### Mix Box Example {.heading}\n" }, { - "content": "\n\n```markdown\n@mix-simple-box\n```\n", + "type": "block", "flex": 2, "scrollable": false, - "type": "block" + "content": "\n\n```markdown\n@mix-simple-box\n```\n" }, { - "content": "", + "type": "block", "align": "center", "flex": 3, "scrollable": false, - "type": "block" + "content": "" }, { - "name": "mix-simple-box", + "type": "widget", "flex": 1, "scrollable": false, - "type": "widget" + "name": "mix-simple-box" } ], "flex": 1, @@ -620,29 +620,29 @@ { "blocks": [ { - "content": "### Interactive Variants {.heading}\n", + "type": "block", "flex": 1, "scrollable": false, - "type": "block" + "content": "### Interactive Variants {.heading}\n" }, { - "content": "\n\nHover and press interactions using Mix variants.\n", + "type": "block", "flex": 2, "scrollable": false, - "type": "block" + "content": "\n\nHover and press interactions using Mix variants.\n" }, { - "content": "", + "type": "block", "align": "center", "flex": 3, "scrollable": false, - "type": "block" + "content": "" }, { - "name": "mix-variants", + "type": "widget", "flex": 1, "scrollable": false, - "type": "widget" + "name": "mix-variants" } ], "flex": 1, @@ -659,29 +659,29 @@ { "blocks": [ { - "content": "### Remix Buttons {.heading}\n", + "type": "block", "flex": 1, "scrollable": false, - "type": "block" + "content": "### Remix Buttons {.heading}\n" }, { - "content": "\n\nDesign system components with Remix.\n", + "type": "block", "flex": 2, "scrollable": false, - "type": "block" + "content": "\n\nDesign system components with Remix.\n" }, { - "content": "", + "type": "block", "align": "center", "flex": 3, "scrollable": false, - "type": "block" + "content": "" }, { - "name": "remix-button", + "type": "widget", "flex": 1, "scrollable": false, - "type": "widget" + "name": "remix-button" } ], "flex": 1, @@ -698,29 +698,29 @@ { "blocks": [ { - "content": "### Animations {.heading}\n", + "type": "block", "flex": 1, "scrollable": false, - "type": "block" + "content": "### Animations {.heading}\n" }, { - "content": "\n\nImplicit and keyframe animations with Mix.\n", + "type": "block", "flex": 2, "scrollable": false, - "type": "block" + "content": "\n\nImplicit and keyframe animations with Mix.\n" }, { - "content": "", + "type": "block", "align": "center", "flex": 3, "scrollable": false, - "type": "block" + "content": "" }, { - "name": "mix-animation", + "type": "widget", "flex": 1, "scrollable": false, - "type": "widget" + "name": "mix-animation" } ], "flex": 1, @@ -737,16 +737,16 @@ { "blocks": [ { - "content": "## Styling Options {.heading}\n", + "type": "block", "flex": 1, "scrollable": false, - "type": "block" + "content": "## Styling Options {.heading}\n" }, { - "content": "SuperDeck supports custom themes and per-slide styling.", + "type": "block", "flex": 1, "scrollable": false, - "type": "block" + "content": "SuperDeck supports custom themes and per-slide styling." } ], "flex": 1, @@ -763,10 +763,10 @@ { "blocks": [ { - "content": "### Style Configuration\n\n```dart\nSuperDeckApp(\n options: DeckOptions(\n styles: {\n 'default': borderedStyle(),\n 'quote': quoteStyle(),\n },\n ),\n)\n```", + "type": "block", "flex": 1, "scrollable": true, - "type": "block" + "content": "### Style Configuration\n\n```dart\nSuperDeckApp(\n options: DeckOptions(\n styles: {\n 'default': borderedStyle(),\n 'quote': quoteStyle(),\n },\n ),\n)\n```" } ], "flex": 1, @@ -783,16 +783,16 @@ { "blocks": [ { - "content": "### Per-Slide Styles\n", + "type": "block", "flex": 1, "scrollable": false, - "type": "block" + "content": "### Per-Slide Styles\n" }, { - "content": "```markdown\n---\nstyle: quote\n---\n\n> Your quote here\n```", + "type": "block", "flex": 1, "scrollable": false, - "type": "block" + "content": "```markdown\n---\nstyle: quote\n---\n\n> Your quote here\n```" } ], "flex": 1, @@ -811,10 +811,10 @@ { "blocks": [ { - "content": "> SuperDeck makes presentations feel like coding - simple, version-controlled, and powerful.", + "type": "block", "flex": 1, "scrollable": false, - "type": "block" + "content": "> SuperDeck makes presentations feel like coding - simple, version-controlled, and powerful." } ], "flex": 1, @@ -831,16 +831,16 @@ { "blocks": [ { - "content": "## Architecture {.heading}\n", + "type": "block", "flex": 1, "scrollable": false, - "type": "block" + "content": "## Architecture {.heading}\n" }, { - "content": "1. Write slides in `slides.md`\n2. Run the CLI build command\n3. SuperDeck parses Markdown into slide data\n4. Flutter renders the presentation UI\n5. Runtime services generate previews as needed", + "type": "block", "flex": 1, "scrollable": false, - "type": "block" + "content": "1. Write slides in `slides.md`\n2. Run the CLI build command\n3. SuperDeck parses Markdown into slide data\n4. Flutter renders the presentation UI\n5. Runtime services generate previews as needed" } ], "flex": 1, @@ -857,22 +857,22 @@ { "blocks": [ { - "content": "## Getting Started {.heading}\n", + "type": "block", "flex": 1, "scrollable": false, - "type": "block" + "content": "## Getting Started {.heading}\n" }, { - "content": "\n\n1. Add SuperDeck to your project\n2. Create `slides.md`\n3. Run the CLI\n4. Present!\n", + "type": "block", "flex": 2, "scrollable": false, - "type": "block" + "content": "\n\n1. Add SuperDeck to your project\n2. Create `slides.md`\n3. Run the CLI\n4. Present!\n" }, { - "content": "```bash\n# Add dependency\nflutter pub add superdeck\n\n# Build slides\ndart run superdeck_cli:main build\n\n# Run presentation\nflutter run\n```", + "type": "block", "flex": 3, "scrollable": false, - "type": "block" + "content": "```bash\n# Add dependency\nflutter pub add superdeck\n\n# Build slides\ndart run superdeck_cli:main build\n\n# Run presentation\nflutter run\n```" } ], "flex": 1, @@ -889,16 +889,16 @@ { "blocks": [ { - "content": "### Project Structure {.heading}\n", + "type": "block", "flex": 1, "scrollable": false, - "type": "block" + "content": "### Project Structure {.heading}\n" }, { - "content": "```\nmy_presentation/\n├── lib/\n│ └── main.dart\n├── slides.md\n└── pubspec.yaml\n```", + "type": "block", "flex": 1, "scrollable": false, - "type": "block" + "content": "```\nmy_presentation/\n├── lib/\n│ └── main.dart\n├── slides.md\n└── pubspec.yaml\n```" } ], "flex": 1, @@ -915,23 +915,23 @@ { "blocks": [ { - "content": "", + "type": "block", "flex": 1, "scrollable": false, - "type": "block" + "content": "" }, { - "content": "\n### Why SuperDeck? {.heading}\n\n- Version control your presentations\n- Use your favorite editor\n- Leverage Flutter's ecosystem\n- Hot reload while editing\n- Cross-platform output\n", + "type": "block", "align": "center", "flex": 3, "scrollable": false, - "type": "block" + "content": "\n### Why SuperDeck? {.heading}\n\n- Version control your presentations\n- Use your favorite editor\n- Leverage Flutter's ecosystem\n- Hot reload while editing\n- Cross-platform output\n" }, { - "content": "", + "type": "block", "flex": 1, "scrollable": false, - "type": "block" + "content": "" } ], "flex": 1, @@ -948,16 +948,16 @@ { "blocks": [ { - "content": "## Slide Templates {.heading}\n", + "type": "block", "flex": 1, "scrollable": false, - "type": "block" + "content": "## Slide Templates {.heading}\n" }, { - "content": "Templates bundle **chrome** (header, footer, background) with an **isolated style system** — like Keynote master slides.", + "type": "block", "flex": 1, "scrollable": false, - "type": "block" + "content": "Templates bundle **chrome** (header, footer, background) with an **isolated style system** — like Keynote master slides." } ], "flex": 1, @@ -976,11 +976,11 @@ { "blocks": [ { - "content": "# Corporate Template {.heading}\n\nThis slide uses the `corporate` template with branded header and footer.", + "type": "block", "align": "center", "flex": 1, "scrollable": false, - "type": "block" + "content": "# Corporate Template {.heading}\n\nThis slide uses the `corporate` template with branded header and footer." } ], "flex": 1, @@ -1000,11 +1000,11 @@ { "blocks": [ { - "content": "# Highlight Style {.heading}\n\nTemplates can have their own named style variants.", + "type": "block", "align": "center", "flex": 1, "scrollable": false, - "type": "block" + "content": "# Highlight Style {.heading}\n\nTemplates can have their own named style variants." } ], "flex": 1, @@ -1023,11 +1023,11 @@ { "blocks": [ { - "content": "# Minimal Template {.heading}\n\nA clean, typography-focused template with no chrome distractions.", + "type": "block", "align": "center", "flex": 1, "scrollable": false, - "type": "block" + "content": "# Minimal Template {.heading}\n\nA clean, typography-focused template with no chrome distractions." } ], "flex": 1, @@ -1046,10 +1046,10 @@ { "blocks": [ { - "name": "webview", + "type": "widget", "flex": 1, "scrollable": false, - "type": "widget", + "name": "webview", "url": "https://www.fluttermix.com", "title": "Flutter Mix", "cacheKey": "fluttermix-normal" @@ -1072,10 +1072,10 @@ { "blocks": [ { - "name": "webview", + "type": "widget", "flex": 1, "scrollable": false, - "type": "widget", + "name": "webview", "url": "https://www.fluttermix.com", "title": "Flutter Mix", "cacheKey": "fluttermix-fullscreen" @@ -1095,10 +1095,10 @@ { "blocks": [ { - "content": "\n# Thank You {.heading}\n", + "type": "block", "flex": 1, "scrollable": false, - "type": "block" + "content": "\n# Thank You {.heading}\n" } ], "align": "bottomCenter", @@ -1109,28 +1109,28 @@ { "blocks": [ { - "content": "\nLeo Farias\n", + "type": "block", "flex": 1, "scrollable": false, - "type": "block" + "content": "\nLeo Farias\n" }, { - "name": "leoafarias", + "type": "widget", "flex": 1, "scrollable": false, - "type": "widget" + "name": "leoafarias" }, { - "content": "\n(GitHub, Twitter/X)\n", + "type": "block", "flex": 1, "scrollable": false, - "type": "block" + "content": "\n(GitHub, Twitter/X)\n" }, { - "content": "#### Source Code\nhttps://github.com/btwld/superdeck", + "type": "block", "flex": 1, "scrollable": false, - "type": "block" + "content": "#### Source Code\nhttps://github.com/btwld/superdeck" } ], "flex": 1, diff --git a/demo/.superdeck/superdeck_full.json b/demo/.superdeck/superdeck_full.json index 1313981a9..9f99e267b 100644 --- a/demo/.superdeck/superdeck_full.json +++ b/demo/.superdeck/superdeck_full.json @@ -6,6 +6,10 @@ { "blocks": [ { + "type": "block", + "align": "center", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -35,11 +39,7 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "align": "center", - "flex": 1, - "scrollable": false, - "type": "block" + } } ], "flex": 2, @@ -56,6 +56,10 @@ { "blocks": [ { + "type": "block", + "align": "center", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -85,13 +89,13 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "align": "center", - "flex": 1, - "scrollable": false, - "type": "block" + } }, { + "type": "block", + "align": "centerLeft", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -145,11 +149,7 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "align": "centerLeft", - "flex": 1, - "scrollable": false, - "type": "block" + } } ], "flex": 1, @@ -166,6 +166,9 @@ { "blocks": [ { + "type": "block", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -184,12 +187,12 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 1, - "scrollable": false, - "type": "block" + } }, { + "type": "block", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -267,10 +270,7 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 1, - "scrollable": false, - "type": "block" + } } ], "flex": 1, @@ -287,18 +287,22 @@ { "blocks": [ { + "type": "block", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [], "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 1, - "scrollable": false, - "type": "block" + } }, { + "type": "block", + "align": "center", + "flex": 5, + "scrollable": false, "content": { "type": "document", "children": [ @@ -317,23 +321,19 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "align": "center", - "flex": 5, - "scrollable": false, - "type": "block" + } }, { + "type": "block", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [], "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 1, - "scrollable": false, - "type": "block" + } } ], "flex": 1, @@ -350,6 +350,9 @@ { "blocks": [ { + "type": "block", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -368,10 +371,7 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 1, - "scrollable": false, - "type": "block" + } } ], "flex": 1, @@ -381,6 +381,10 @@ { "blocks": [ { + "type": "block", + "align": "topCenter", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -435,13 +439,13 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, + } + }, + { + "type": "block", "align": "topCenter", "flex": 1, "scrollable": false, - "type": "block" - }, - { "content": { "type": "document", "children": [ @@ -496,13 +500,13 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, + } + }, + { + "type": "block", "align": "topCenter", "flex": 1, "scrollable": false, - "type": "block" - }, - { "content": { "type": "document", "children": [ @@ -557,11 +561,7 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "align": "topCenter", - "flex": 1, - "scrollable": false, - "type": "block" + } } ], "flex": 1, @@ -578,6 +578,9 @@ { "blocks": [ { + "type": "block", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -596,12 +599,12 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 1, - "scrollable": false, - "type": "block" + } }, { + "type": "block", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -679,10 +682,7 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 1, - "scrollable": false, - "type": "block" + } } ], "flex": 1, @@ -699,6 +699,9 @@ { "blocks": [ { + "type": "block", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -727,10 +730,7 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 1, - "scrollable": false, - "type": "block" + } } ], "flex": 1, @@ -747,6 +747,9 @@ { "blocks": [ { + "type": "block", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -765,10 +768,7 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 1, - "scrollable": false, - "type": "block" + } } ], "flex": 1, @@ -778,6 +778,15 @@ { "blocks": [ { + "type": "block", + "flex": 1, + "padding": { + "top": 0.0, + "right": 0.0, + "bottom": 0.0, + "left": 0.0 + }, + "scrollable": false, "content": { "type": "document", "children": [ @@ -812,18 +821,18 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, + } + }, + { + "type": "block", "flex": 1, "padding": { - "top": 0.0, - "right": 0.0, - "bottom": 0.0, - "left": 0.0 + "top": 16.0, + "right": 16.0, + "bottom": 16.0, + "left": 16.0 }, "scrollable": false, - "type": "block" - }, - { "content": { "type": "document", "children": [ @@ -858,18 +867,19 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, + } + }, + { + "type": "block", + "align": "bottomRight", "flex": 1, "padding": { - "top": 16.0, - "right": 16.0, - "bottom": 16.0, - "left": 16.0 + "top": 24.0, + "right": 48.0, + "bottom": 24.0, + "left": 48.0 }, "scrollable": false, - "type": "block" - }, - { "content": { "type": "document", "children": [ @@ -904,17 +914,7 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "align": "bottomRight", - "flex": 1, - "padding": { - "top": 24.0, - "right": 48.0, - "bottom": 24.0, - "left": 48.0 - }, - "scrollable": false, - "type": "block" + } } ], "align": "center", @@ -934,6 +934,9 @@ { "blocks": [ { + "type": "block", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -952,10 +955,7 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 1, - "scrollable": false, - "type": "block" + } } ], "flex": 1, @@ -965,6 +965,9 @@ { "blocks": [ { + "type": "block", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -1003,12 +1006,18 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 1, - "scrollable": false, - "type": "block" + } }, { + "type": "block", + "flex": 1, + "margin": { + "top": 24.0, + "right": 24.0, + "bottom": 24.0, + "left": 24.0 + }, + "scrollable": false, "content": { "type": "document", "children": [ @@ -1047,18 +1056,18 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, + } + }, + { + "type": "block", "flex": 1, - "margin": { + "padding": { "top": 24.0, - "right": 24.0, + "right": 48.0, "bottom": 24.0, - "left": 24.0 + "left": 48.0 }, "scrollable": false, - "type": "block" - }, - { "content": { "type": "document", "children": [ @@ -1097,16 +1106,7 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 1, - "padding": { - "top": 24.0, - "right": 48.0, - "bottom": 24.0, - "left": 48.0 - }, - "scrollable": false, - "type": "block" + } } ], "align": "center", @@ -1124,6 +1124,9 @@ { "blocks": [ { + "type": "block", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -1142,10 +1145,7 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 1, - "scrollable": false, - "type": "block" + } } ], "flex": 1, @@ -1155,7 +1155,7 @@ { "blocks": [ { - "name": "image", + "type": "widget", "flex": 1, "padding": { "top": 0.0, @@ -1164,7 +1164,7 @@ "left": 0.0 }, "scrollable": false, - "type": "widget", + "name": "image", "src": "assets/concepta-icon.png", "fit": "contain", "width": 300, @@ -1172,7 +1172,7 @@ "scale": 1 }, { - "name": "image", + "type": "widget", "flex": 1, "padding": { "top": 0.0, @@ -1181,7 +1181,7 @@ "left": 0.0 }, "scrollable": false, - "type": "widget", + "name": "image", "src": "assets/concepta-icon.png", "fit": "contain", "width": 300, @@ -1204,6 +1204,10 @@ { "blocks": [ { + "type": "block", + "align": "centerRight", + "flex": 2, + "scrollable": false, "content": { "type": "document", "children": [ @@ -1222,13 +1226,12 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "align": "centerRight", - "flex": 2, - "scrollable": false, - "type": "block" + } }, { + "type": "block", + "flex": 3, + "scrollable": false, "content": { "type": "document", "children": [ @@ -1255,10 +1258,7 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 3, - "scrollable": false, - "type": "block" + } } ], "flex": 1, @@ -1275,6 +1275,10 @@ { "blocks": [ { + "type": "block", + "align": "center", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -1293,11 +1297,7 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "align": "center", - "flex": 1, - "scrollable": false, - "type": "block" + } } ], "flex": 1, @@ -1307,6 +1307,10 @@ { "blocks": [ { + "type": "block", + "align": "center", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -1325,11 +1329,7 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "align": "center", - "flex": 1, - "scrollable": false, - "type": "block" + } } ], "flex": 2, @@ -1339,6 +1339,10 @@ { "blocks": [ { + "type": "block", + "align": "center", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -1357,11 +1361,7 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "align": "center", - "flex": 1, - "scrollable": false, - "type": "block" + } } ], "flex": 1, @@ -1378,6 +1378,9 @@ { "blocks": [ { + "type": "block", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -1396,12 +1399,12 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 1, - "scrollable": false, - "type": "block" + } }, { + "type": "block", + "flex": 2, + "scrollable": false, "content": { "type": "document", "children": [ @@ -1428,10 +1431,7 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 2, - "scrollable": false, - "type": "block" + } } ], "flex": 1, @@ -1448,6 +1448,9 @@ { "blocks": [ { + "type": "block", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -1466,12 +1469,12 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 1, - "scrollable": false, - "type": "block" + } }, { + "type": "block", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -1489,10 +1492,7 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 1, - "scrollable": false, - "type": "block" + } } ], "flex": 1, @@ -1509,6 +1509,9 @@ { "blocks": [ { + "type": "block", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -1527,10 +1530,7 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 1, - "scrollable": false, - "type": "block" + } } ], "flex": 1, @@ -1540,6 +1540,10 @@ { "blocks": [ { + "type": "block", + "align": "center", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -1571,11 +1575,7 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "align": "center", - "flex": 1, - "scrollable": false, - "type": "block" + } } ], "flex": 1, @@ -1585,6 +1585,9 @@ { "blocks": [ { + "type": "block", + "flex": 3, + "scrollable": false, "content": { "type": "document", "children": [ @@ -1611,17 +1614,14 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 3, - "scrollable": false, - "type": "block" + } }, { - "name": "ack-metric-card", + "type": "widget", "align": "center", "flex": 2, "scrollable": false, - "type": "widget", + "name": "ack-metric-card", "label": "Activation", "value": "72%", "caption": "Parsed through Ack-generated typed getters", @@ -1642,6 +1642,9 @@ { "blocks": [ { + "type": "block", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -1660,12 +1663,12 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 1, - "scrollable": false, - "type": "block" + } }, { + "type": "block", + "flex": 2, + "scrollable": false, "content": { "type": "document", "children": [ @@ -1692,29 +1695,26 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 2, - "scrollable": false, - "type": "block" + } }, { + "type": "block", + "align": "center", + "flex": 3, + "scrollable": false, "content": { "type": "document", "children": [], "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "align": "center", - "flex": 3, - "scrollable": false, - "type": "block" + } }, { - "name": "mix-simple-box", + "type": "widget", "flex": 1, "scrollable": false, - "type": "widget" + "name": "mix-simple-box" } ], "flex": 1, @@ -1731,6 +1731,9 @@ { "blocks": [ { + "type": "block", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -1749,12 +1752,12 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 1, - "scrollable": false, - "type": "block" + } }, { + "type": "block", + "flex": 2, + "scrollable": false, "content": { "type": "document", "children": [ @@ -1772,29 +1775,26 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 2, - "scrollable": false, - "type": "block" + } }, { + "type": "block", + "align": "center", + "flex": 3, + "scrollable": false, "content": { "type": "document", "children": [], "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "align": "center", - "flex": 3, - "scrollable": false, - "type": "block" + } }, { - "name": "mix-variants", + "type": "widget", "flex": 1, "scrollable": false, - "type": "widget" + "name": "mix-variants" } ], "flex": 1, @@ -1811,6 +1811,9 @@ { "blocks": [ { + "type": "block", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -1829,12 +1832,12 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 1, - "scrollable": false, - "type": "block" + } }, { + "type": "block", + "flex": 2, + "scrollable": false, "content": { "type": "document", "children": [ @@ -1852,29 +1855,26 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 2, - "scrollable": false, - "type": "block" + } }, { + "type": "block", + "align": "center", + "flex": 3, + "scrollable": false, "content": { "type": "document", "children": [], "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "align": "center", - "flex": 3, - "scrollable": false, - "type": "block" + } }, { - "name": "remix-button", + "type": "widget", "flex": 1, "scrollable": false, - "type": "widget" + "name": "remix-button" } ], "flex": 1, @@ -1891,6 +1891,9 @@ { "blocks": [ { + "type": "block", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -1909,12 +1912,12 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 1, - "scrollable": false, - "type": "block" + } }, { + "type": "block", + "flex": 2, + "scrollable": false, "content": { "type": "document", "children": [ @@ -1932,29 +1935,26 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 2, - "scrollable": false, - "type": "block" + } }, { + "type": "block", + "align": "center", + "flex": 3, + "scrollable": false, "content": { "type": "document", "children": [], "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "align": "center", - "flex": 3, - "scrollable": false, - "type": "block" + } }, { - "name": "mix-animation", + "type": "widget", "flex": 1, "scrollable": false, - "type": "widget" + "name": "mix-animation" } ], "flex": 1, @@ -1971,6 +1971,9 @@ { "blocks": [ { + "type": "block", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -1989,12 +1992,12 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 1, - "scrollable": false, - "type": "block" + } }, { + "type": "block", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -2012,10 +2015,7 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 1, - "scrollable": false, - "type": "block" + } } ], "flex": 1, @@ -2032,6 +2032,9 @@ { "blocks": [ { + "type": "block", + "flex": 1, + "scrollable": true, "content": { "type": "document", "children": [ @@ -2069,10 +2072,7 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 1, - "scrollable": true, - "type": "block" + } } ], "flex": 1, @@ -2089,6 +2089,9 @@ { "blocks": [ { + "type": "block", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -2107,12 +2110,12 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 1, - "scrollable": false, - "type": "block" + } }, { + "type": "block", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -2139,10 +2142,7 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 1, - "scrollable": false, - "type": "block" + } } ], "flex": 1, @@ -2161,6 +2161,9 @@ { "blocks": [ { + "type": "block", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -2184,10 +2187,7 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 1, - "scrollable": false, - "type": "block" + } } ], "flex": 1, @@ -2204,6 +2204,9 @@ { "blocks": [ { + "type": "block", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -2222,12 +2225,12 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 1, - "scrollable": false, - "type": "block" + } }, { + "type": "block", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -2301,10 +2304,7 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 1, - "scrollable": false, - "type": "block" + } } ], "flex": 1, @@ -2321,6 +2321,9 @@ { "blocks": [ { + "type": "block", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -2339,12 +2342,12 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 1, - "scrollable": false, - "type": "block" + } }, { + "type": "block", + "flex": 2, + "scrollable": false, "content": { "type": "document", "children": [ @@ -2408,12 +2411,12 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 2, - "scrollable": false, - "type": "block" + } }, { + "type": "block", + "flex": 3, + "scrollable": false, "content": { "type": "document", "children": [ @@ -2440,10 +2443,7 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 3, - "scrollable": false, - "type": "block" + } } ], "flex": 1, @@ -2460,6 +2460,9 @@ { "blocks": [ { + "type": "block", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -2478,12 +2481,12 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 1, - "scrollable": false, - "type": "block" + } }, { + "type": "block", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -2507,10 +2510,7 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 1, - "scrollable": false, - "type": "block" + } } ], "flex": 1, @@ -2527,18 +2527,22 @@ { "blocks": [ { + "type": "block", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [], "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 1, - "scrollable": false, - "type": "block" + } }, { + "type": "block", + "align": "center", + "flex": 3, + "scrollable": false, "content": { "type": "document", "children": [ @@ -2613,23 +2617,19 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "align": "center", - "flex": 3, - "scrollable": false, - "type": "block" + } }, { + "type": "block", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [], "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 1, - "scrollable": false, - "type": "block" + } } ], "flex": 1, @@ -2646,6 +2646,9 @@ { "blocks": [ { + "type": "block", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -2664,12 +2667,12 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 1, - "scrollable": false, - "type": "block" + } }, { + "type": "block", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -2715,10 +2718,7 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 1, - "scrollable": false, - "type": "block" + } } ], "flex": 1, @@ -2737,6 +2737,10 @@ { "blocks": [ { + "type": "block", + "align": "center", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -2779,11 +2783,7 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "align": "center", - "flex": 1, - "scrollable": false, - "type": "block" + } } ], "flex": 1, @@ -2803,6 +2803,10 @@ { "blocks": [ { + "type": "block", + "align": "center", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -2831,11 +2835,7 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "align": "center", - "flex": 1, - "scrollable": false, - "type": "block" + } } ], "flex": 1, @@ -2854,6 +2854,10 @@ { "blocks": [ { + "type": "block", + "align": "center", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -2882,11 +2886,7 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "align": "center", - "flex": 1, - "scrollable": false, - "type": "block" + } } ], "flex": 1, @@ -2905,10 +2905,10 @@ { "blocks": [ { - "name": "webview", + "type": "widget", "flex": 1, "scrollable": false, - "type": "widget", + "name": "webview", "url": "https://www.fluttermix.com", "title": "Flutter Mix", "cacheKey": "fluttermix-normal" @@ -2931,10 +2931,10 @@ { "blocks": [ { - "name": "webview", + "type": "widget", "flex": 1, "scrollable": false, - "type": "widget", + "name": "webview", "url": "https://www.fluttermix.com", "title": "Flutter Mix", "cacheKey": "fluttermix-fullscreen" @@ -2954,6 +2954,9 @@ { "blocks": [ { + "type": "block", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -2972,10 +2975,7 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 1, - "scrollable": false, - "type": "block" + } } ], "align": "bottomCenter", @@ -2986,6 +2986,9 @@ { "blocks": [ { + "type": "block", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -3003,18 +3006,18 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 1, - "scrollable": false, - "type": "block" + } }, { - "name": "leoafarias", + "type": "widget", "flex": 1, "scrollable": false, - "type": "widget" + "name": "leoafarias" }, { + "type": "block", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -3032,12 +3035,12 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 1, - "scrollable": false, - "type": "block" + } }, { + "type": "block", + "flex": 1, + "scrollable": false, "content": { "type": "document", "children": [ @@ -3075,10 +3078,7 @@ "linkReferences": {}, "footnoteLabels": [], "footnoteReferences": {} - }, - "flex": 1, - "scrollable": false, - "type": "block" + } } ], "flex": 1, diff --git a/demo/e2e/playwright.config.ts b/demo/e2e/playwright.config.ts index 96e533132..befe4efd4 100644 --- a/demo/e2e/playwright.config.ts +++ b/demo/e2e/playwright.config.ts @@ -10,7 +10,14 @@ export default defineConfig({ }, reporter: 'list', projects: [ - {name: 'chromium', use: {...devices['Desktop Chrome']}}, + { + name: 'chromium', + use: { + ...devices['Desktop Chrome'], + // Keep CanvasKit on WebGL when headless Chromium cannot use the GPU. + launchOptions: {args: ['--enable-unsafe-swiftshader']}, + }, + }, {name: 'webkit', use: {...devices['Desktop Safari']}}, ], use: { diff --git a/demo/integration_test/helpers/test_helpers.dart b/demo/integration_test/helpers/test_helpers.dart index d4be3a445..313b2a4a4 100644 --- a/demo/integration_test/helpers/test_helpers.dart +++ b/demo/integration_test/helpers/test_helpers.dart @@ -411,7 +411,7 @@ List makeSlides(int count) { /// Serializes slides to a JSON string suitable for `superdeck.json`. String buildSlideJson(List slides) { - return jsonEncode(slides.map((s) => s.toMap()).toList()); + return jsonEncode(slides.map((s) => s.toJson()).toList()); } String _statusJson(String status, int seq, {String? errorJson}) { diff --git a/demo/integration_test/layout_matrix_test.dart b/demo/integration_test/layout_matrix_test.dart index 445c71243..961b56e26 100644 --- a/demo/integration_test/layout_matrix_test.dart +++ b/demo/integration_test/layout_matrix_test.dart @@ -749,12 +749,7 @@ WidgetBlock _image( }) { return WidgetBlock( name: 'image', - args: { - 'src': src, - 'fit': fit, - 'width': ?width, - 'height': ?height, - }, + args: {'src': src, 'fit': fit, 'width': ?width, 'height': ?height}, ); } diff --git a/demo/lib/src/widgets/ack_metric_card.ack.dart b/demo/lib/src/widgets/ack_metric_card.ack.dart new file mode 100644 index 000000000..a5b22e8c2 --- /dev/null +++ b/demo/lib/src/widgets/ack_metric_card.ack.dart @@ -0,0 +1,115 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// dart format width=80 + +part of 'ack_metric_card.dart'; + +// ************************************************************************** +// AckModelGenerator +// ************************************************************************** + +final class _MetricCardArgsCopyWithUnset { + const _MetricCardArgsCopyWithUnset(); +} + +/// Immutable model generated from `metricCardArgsSchema`. +@AckInfer.jsonSerializable +final class MetricCardArgs { + MetricCardArgs({ + required this.label, + required this.value, + this.caption, + this.tone, + }); + + factory MetricCardArgs.parse(Object? input) { + return $ack.parse(input); + } + + factory MetricCardArgs.fromJson(Map json) { + return $ack.parse(json); + } + + static const _MetricCardArgsCopyWithUnset _ackCopyWithUnset = + _MetricCardArgsCopyWithUnset(); + + final String label; + + final String value; + + final String? caption; + + final String? tone; + + static final $ack = AckModelAdapter( + schema: () => metricCardArgsSchema, + fromRuntime: MetricCardArgs._fromAckRuntime, + toRuntime: (model) => model._toAckRuntime(), + ); + + static SchemaResult safeParse(Object? input) => + $ack.safeParse(input); + + Map toJson() => Map.from($ack.encode(this)); + + SchemaResult> safeToJson() => $ack.safeEncode(this); + + MetricCardArgs copyWith({ + String? label, + String? value, + Object? caption = _ackCopyWithUnset, + Object? tone = _ackCopyWithUnset, + }) => MetricCardArgs( + label: label ?? this.label, + value: value ?? this.value, + caption: identical(caption, _ackCopyWithUnset) + ? this.caption + : caption as String?, + tone: identical(tone, _ackCopyWithUnset) ? this.tone : tone as String?, + ); + + @override + bool operator ==(Object other) => + identical(this, other) || + (other is MetricCardArgs && + runtimeType == other.runtimeType && + deepEquals(label, other.label) && + deepEquals(value, other.value) && + deepEquals(caption, other.caption) && + deepEquals(tone, other.tone)); + + @override + int get hashCode => Object.hashAll([ + runtimeType, + deepHashCode(label), + deepHashCode(value), + deepHashCode(caption), + deepHashCode(tone), + ]); + + @override + String toString() => + 'MetricCardArgs(label: $label, value: $value, caption: $caption, tone: $tone)'; + + static MetricCardArgs _fromAckRuntime(Map value) => + _$MetricCardArgsFromJson(Map.from(value)); + + Map _toAckRuntime() => { + ..._$MetricCardArgsToJson(this), + }; + + static String _ackFromRuntimeLabel(Object? value) => value as String; + + static Object? _ackToRuntimeLabel(String value) => value; + + static String _ackFromRuntimeValue(Object? value) => value as String; + + static Object? _ackToRuntimeValue(String value) => value; + + static String? _ackFromRuntimeCaption(Object? value) => value as String?; + + static Object? _ackToRuntimeCaption(String? value) => value; + + static String? _ackFromRuntimeTone(Object? value) => value as String?; + + static Object? _ackToRuntimeTone(String? value) => value; +} diff --git a/demo/lib/src/widgets/ack_metric_card.ack.g.dart b/demo/lib/src/widgets/ack_metric_card.ack.g.dart new file mode 100644 index 000000000..21c8751f1 --- /dev/null +++ b/demo/lib/src/widgets/ack_metric_card.ack.g.dart @@ -0,0 +1,24 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// dart format width=80 + +part of 'ack_metric_card.dart'; + +// ************************************************************************** +// AckJsonSerializableGenerator +// ************************************************************************** + +MetricCardArgs _$MetricCardArgsFromJson(Map json) => + MetricCardArgs( + label: MetricCardArgs._ackFromRuntimeLabel(json['label']), + value: MetricCardArgs._ackFromRuntimeValue(json['value']), + caption: MetricCardArgs._ackFromRuntimeCaption(json['caption']), + tone: MetricCardArgs._ackFromRuntimeTone(json['tone']), + ); + +Map _$MetricCardArgsToJson(MetricCardArgs instance) => + { + 'label': MetricCardArgs._ackToRuntimeLabel(instance.label), + 'value': MetricCardArgs._ackToRuntimeValue(instance.value), + 'caption': ?MetricCardArgs._ackToRuntimeCaption(instance.caption), + 'tone': ?MetricCardArgs._ackToRuntimeTone(instance.tone), + }; diff --git a/demo/lib/src/widgets/ack_metric_card.dart b/demo/lib/src/widgets/ack_metric_card.dart index 4a4bad160..cef07f6cc 100644 --- a/demo/lib/src/widgets/ack_metric_card.dart +++ b/demo/lib/src/widgets/ack_metric_card.dart @@ -2,10 +2,11 @@ import 'package:ack/ack.dart'; import 'package:ack_annotations/ack_annotations.dart'; import 'package:flutter/material.dart'; -part 'ack_metric_card.g.dart'; +part 'ack_metric_card.ack.dart'; +part 'ack_metric_card.ack.g.dart'; -@AckType(name: 'AckMetricCardArgs') -final ackMetricCardArgsSchema = Ack.object({ +@AckInfer() +final metricCardArgsSchema = Ack.object({ 'label': Ack.string().notEmpty(), 'value': Ack.string().notEmpty(), 'caption': Ack.string().optional(), @@ -13,10 +14,10 @@ final ackMetricCardArgsSchema = Ack.object({ }); class AckMetricCard extends StatelessWidget { - final AckMetricCardArgsType data; + final MetricCardArgs data; AckMetricCard(Map args, {super.key}) - : data = AckMetricCardArgsType.parse(args); + : data = MetricCardArgs.parse(args); Color _toneColor(String? tone) { return switch (tone) { diff --git a/demo/lib/src/widgets/ack_metric_card.g.dart b/demo/lib/src/widgets/ack_metric_card.g.dart deleted file mode 100644 index 772a58c97..000000000 --- a/demo/lib/src/widgets/ack_metric_card.g.dart +++ /dev/null @@ -1,34 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND -// dart format width=80 - -// ************************************************************************** -// AckSchemaGenerator -// ************************************************************************** - -part of 'ack_metric_card.dart'; - -/// Extension type for AckMetricCardArgs -extension type AckMetricCardArgsType(Map _data) - implements Map { - static AckMetricCardArgsType parse(Object? data) { - return ackMetricCardArgsSchema.parseAs( - data, - (validated) => AckMetricCardArgsType(validated as Map), - ); - } - - static SchemaResult safeParse(Object? data) { - return ackMetricCardArgsSchema.safeParseAs( - data, - (validated) => AckMetricCardArgsType(validated as Map), - ); - } - - String get label => _data['label'] as String; - - String get value => _data['value'] as String; - - String? get caption => _data['caption'] as String?; - - String? get tone => _data['tone'] as String?; -} diff --git a/demo/macos/Podfile.lock b/demo/macos/Podfile.lock index 490922ae7..86c3a2b61 100644 --- a/demo/macos/Podfile.lock +++ b/demo/macos/Podfile.lock @@ -1,65 +1,27 @@ PODS: - - device_info_plus (0.0.1): - - FlutterMacOS - - file_saver (0.3.1): - - FlutterMacOS - FlutterMacOS (1.0.0) - irondash_engine_context (0.0.1): - FlutterMacOS - - screen_retriever_macos (0.0.1): - - FlutterMacOS - - sqflite_darwin (0.0.4): - - Flutter - - FlutterMacOS - super_native_extensions (0.0.1): - FlutterMacOS - - webview_flutter_wkwebview (0.0.1): - - Flutter - - FlutterMacOS - - window_manager (0.5.0): - - FlutterMacOS DEPENDENCIES: - - device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`) - - file_saver (from `Flutter/ephemeral/.symlinks/plugins/file_saver/macos`) - FlutterMacOS (from `Flutter/ephemeral`) - irondash_engine_context (from `Flutter/ephemeral/.symlinks/plugins/irondash_engine_context/macos`) - - screen_retriever_macos (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever_macos/macos`) - - sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin`) - super_native_extensions (from `Flutter/ephemeral/.symlinks/plugins/super_native_extensions/macos`) - - webview_flutter_wkwebview (from `Flutter/ephemeral/.symlinks/plugins/webview_flutter_wkwebview/darwin`) - - window_manager (from `Flutter/ephemeral/.symlinks/plugins/window_manager/macos`) EXTERNAL SOURCES: - device_info_plus: - :path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos - file_saver: - :path: Flutter/ephemeral/.symlinks/plugins/file_saver/macos FlutterMacOS: :path: Flutter/ephemeral irondash_engine_context: :path: Flutter/ephemeral/.symlinks/plugins/irondash_engine_context/macos - screen_retriever_macos: - :path: Flutter/ephemeral/.symlinks/plugins/screen_retriever_macos/macos - sqflite_darwin: - :path: Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin super_native_extensions: :path: Flutter/ephemeral/.symlinks/plugins/super_native_extensions/macos - webview_flutter_wkwebview: - :path: Flutter/ephemeral/.symlinks/plugins/webview_flutter_wkwebview/darwin - window_manager: - :path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos SPEC CHECKSUMS: - device_info_plus: 4fb280989f669696856f8b129e4a5e3cd6c48f76 - file_saver: 0b49fcd0f67d6e24737b757ee1eb1d3d8ca392fb FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 irondash_engine_context: 893c7d96d20ce361d7e996f39d360c4c2f9869ba - screen_retriever_macos: c5508cc3c66ff0d4db650480cf0ab691e220d933 - sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 super_native_extensions: c2795d6d9aedf4a79fae25cb6160b71b50549189 - webview_flutter_wkwebview: 8ebf4fded22593026f7dbff1fbff31ea98573c8d - window_manager: b729e31d38fb04905235df9ea896128991cad99e PODFILE CHECKSUM: 9ebaf0ce3d369aaa26a9ea0e159195ed94724cf3 diff --git a/demo/pubspec.yaml b/demo/pubspec.yaml index 5f48f8e56..30b132f37 100644 --- a/demo/pubspec.yaml +++ b/demo/pubspec.yaml @@ -11,8 +11,8 @@ resolution: workspace dependencies: flutter: sdk: flutter - ack: 1.0.1 - ack_annotations: 1.0.1 + ack: ^1.2.0 + ack_annotations: ^1.2.0 google_fonts: ^8.1.0 mesh: ^0.5.0 mix: ^2.2.0-beta.0 @@ -28,7 +28,7 @@ dev_dependencies: sdk: flutter flutter_lints: ^6.0.0 build_runner: ^2.5.4 - ack_generator: 1.0.1 + ack_generator: ^1.2.0 superdeck_builder: ^1.0.0 superdeck_cli: ^1.0.0 superdeck_pdf: ^1.0.0 diff --git a/demo/test/ack_metric_card_test.dart b/demo/test/ack_metric_card_test.dart index c404d8d03..ee7e344e5 100644 --- a/demo/test/ack_metric_card_test.dart +++ b/demo/test/ack_metric_card_test.dart @@ -5,17 +5,16 @@ import 'package:superdeck_example/src/widgets/ack_metric_card.dart'; void main() { testWidgets('AckMetricCard renders generated typed args', (tester) async { - await tester.pumpWidget( - MaterialApp( - home: AckMetricCard({ - 'label': 'Activation', - 'value': '72%', - 'caption': 'Parsed through Ack-generated typed getters', - 'tone': 'green', - }), - ), - ); + final card = AckMetricCard({ + 'label': 'Activation', + 'value': '72%', + 'caption': 'Parsed through Ack-generated typed getters', + 'tone': 'green', + }); + + await tester.pumpWidget(MaterialApp(home: card)); + expect(card.data, isA()); expect(find.text('ACTIVATION'), findsOneWidget); expect(find.text('72%'), findsOneWidget); expect( diff --git a/docs/guides/plugins.mdx b/docs/guides/plugins.mdx index bef26bd35..e8dc65b09 100644 --- a/docs/guides/plugins.mdx +++ b/docs/guides/plugins.mdx @@ -173,7 +173,7 @@ Register it in a custom runner: ```dart import 'dart:io'; -import 'package:superdeck_cli/runner.dart'; +import 'package:superdeck_cli/superdeck_cli.dart'; Future main(List args) async { final exitCode = await SuperDeckRunner( diff --git a/packages/builder/CHANGELOG.md b/packages/builder/CHANGELOG.md index a525a484f..f1108bd36 100644 --- a/packages/builder/CHANGELOG.md +++ b/packages/builder/CHANGELOG.md @@ -1,4 +1,7 @@ -## Unreleased +## 1.0.0 + +- **Breaking:** consume the Ack 1.2 JSON APIs from `superdeck_core` and require + `superdeck_core` 1.0.0. - Round-trip the `layout` slide frontmatter option in Markdown serialization. - Parse and round-trip section `spacing` plus all supported block `padding` @@ -20,6 +23,4 @@ widget-shorthand escaping, so a `WidgetBlock` named `section`, `block`, `widget`, or `column` always serializes as `@widget`. -## 1.0.0 - - First stable release of superdeck_builder diff --git a/packages/builder/lib/src/parsers/slide_serializer.dart b/packages/builder/lib/src/parsers/slide_serializer.dart index 7f453721d..cdf12ad25 100644 --- a/packages/builder/lib/src/parsers/slide_serializer.dart +++ b/packages/builder/lib/src/parsers/slide_serializer.dart @@ -184,8 +184,8 @@ class SlideSerializer { final options = {}; if (block.flex != 1) options['flex'] = block.flex; if (block.align != null) options['align'] = block.align!.name; - if (block.padding != null) options['padding'] = block.padding!.toMap(); - if (block.margin != null) options['margin'] = block.margin!.toMap(); + if (block.padding != null) options['padding'] = block.padding!.toJson(); + if (block.margin != null) options['margin'] = block.margin!.toJson(); if (block.scrollable) options['scrollable'] = true; return options; } diff --git a/packages/builder/test/src/parsers/section_parser_test.dart b/packages/builder/test/src/parsers/section_parser_test.dart index 415b34a21..758130696 100644 --- a/packages/builder/test/src/parsers/section_parser_test.dart +++ b/packages/builder/test/src/parsers/section_parser_test.dart @@ -176,7 +176,7 @@ Right final sections = sectionParser.parse(markdown); - expect(sections.single.toMap()['spacing'], 40); + expect(sections.single.toJson()['spacing'], 40); }); group('Column Attributes', () { diff --git a/packages/builder/test/src/parsers/slide_serializer_test.dart b/packages/builder/test/src/parsers/slide_serializer_test.dart index 82dded868..2105ca345 100644 --- a/packages/builder/test/src/parsers/slide_serializer_test.dart +++ b/packages/builder/test/src/parsers/slide_serializer_test.dart @@ -54,8 +54,8 @@ Map canonicalBlock(Block block) { 'type': 'block', 'flex': block.flex, 'align': block.align?.name, - 'margin': block.toMap()['margin'], - 'padding': block.toMap()['padding'], + 'margin': block.toJson()['margin'], + 'padding': block.toJson()['padding'], 'scrollable': block.scrollable, 'content': block.content.trim(), }, @@ -64,8 +64,8 @@ Map canonicalBlock(Block block) { 'name': block.name, 'flex': block.flex, 'align': block.align?.name, - 'margin': block.toMap()['margin'], - 'padding': block.toMap()['padding'], + 'margin': block.toJson()['margin'], + 'padding': block.toJson()['padding'], 'scrollable': block.scrollable, 'args': block.args, }, @@ -166,14 +166,14 @@ void main() { ); final blocks = slides.single.sections.single.blocks; - expect(blocks[0].toMap()['padding'], { + expect(blocks[0].toJson()['padding'], { 'top': 16.0, 'right': 16.0, 'bottom': 16.0, 'left': 16.0, }); expect((blocks[1] as WidgetBlock).args.containsKey('padding'), isFalse); - expect(blocks[1].toMap()['padding'], { + expect(blocks[1].toJson()['padding'], { 'top': 12.0, 'right': 24.0, 'bottom': 12.0, @@ -366,7 +366,7 @@ void main() { final slides = parseDeck('@block { margin: 8 }\n\nContent'); final block = slides.single.sections.single.blocks.single; - expect(block.toMap()['margin'], { + expect(block.toJson()['margin'], { 'top': 8.0, 'right': 8.0, 'bottom': 8.0, diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index c5e5f25c8..480c9c03b 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,8 @@ ## 1.0.0 +- Require `superdeck_core` and `superdeck_builder` 1.0.0 as part of the + coordinated Ack 1.2 migration. + - First stable release of superdeck_cli ## 0.0.1 diff --git a/packages/cli/bin/main.dart b/packages/cli/bin/main.dart index c5be5d10c..909878530 100755 --- a/packages/cli/bin/main.dart +++ b/packages/cli/bin/main.dart @@ -3,7 +3,7 @@ import 'dart:io'; import 'package:mason_logger/mason_logger.dart'; -import 'package:superdeck_cli/runner.dart'; +import 'package:superdeck_cli/superdeck_cli.dart'; import 'package:superdeck_cli/src/utils/constants.dart'; /// Main entry point for the SuperDeck CLI when run as a global command diff --git a/packages/cli/lib/runner.dart b/packages/cli/lib/superdeck_cli.dart similarity index 100% rename from packages/cli/lib/runner.dart rename to packages/cli/lib/superdeck_cli.dart diff --git a/packages/cli/test/integration/cli_workflow_test.dart b/packages/cli/test/integration/cli_workflow_test.dart index 1a7ccc05a..6e6443c2c 100644 --- a/packages/cli/test/integration/cli_workflow_test.dart +++ b/packages/cli/test/integration/cli_workflow_test.dart @@ -3,7 +3,7 @@ import 'dart:io'; import 'package:mason_logger/mason_logger.dart'; import 'package:path/path.dart' as path; -import 'package:superdeck_cli/runner.dart'; +import 'package:superdeck_cli/superdeck_cli.dart'; import 'package:superdeck_core/superdeck_core.dart'; import 'package:test/test.dart'; diff --git a/packages/cli/test/src/runner_test.dart b/packages/cli/test/src/runner_test.dart index 37f81e481..5fd78a1c7 100644 --- a/packages/cli/test/src/runner_test.dart +++ b/packages/cli/test/src/runner_test.dart @@ -3,7 +3,7 @@ import 'dart:io'; import 'package:mason_logger/mason_logger.dart'; import 'package:path/path.dart' as path; import 'package:superdeck_builder/superdeck_builder.dart'; -import 'package:superdeck_cli/runner.dart'; +import 'package:superdeck_cli/superdeck_cli.dart'; import 'package:superdeck_cli/src/commands/build_command.dart'; import 'package:superdeck_cli/src/commands/setup_command.dart'; import 'package:superdeck_cli/src/utils/constants.dart'; diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md index 74929f3ec..c9263b572 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -1,4 +1,12 @@ -## Unreleased +## 1.0.0 + +- **Breaking:** replace `dart_mappable` models and mapper APIs with Ack 1.2 + class-first models. Use the generated `*Schema` facades and model + `fromJson`/`toJson` methods; unknown-field handling is now explicit per + model. Omitted generated `copyWith` arguments retain their current values, + while explicit `null` clears nullable fields. +- Preserve boundary maps and lists from generated `wireSchema` validation + instead of retaining values decoded by nested codecs. - Add `SlideLayout` and the `SlideOptions.layout` field to the slide contract. - Add optional section `spacing`, block `padding` and `margin`, and inherited @@ -29,8 +37,6 @@ zero and close with exactly the opening run length. Indented fences and closing fences longer than their opener are now recognized. -## 1.0.0 - - First stable release of superdeck_core - Remove provisional setext hero syntax so core and Flutter stay scoped to ATX headings - Fix image hero parsing by delegating to the shared helper for safe marker consumption diff --git a/packages/core/lib/src/deck/block_insets.ack.dart b/packages/core/lib/src/deck/block_insets.ack.dart new file mode 100644 index 000000000..4507a2e93 --- /dev/null +++ b/packages/core/lib/src/deck/block_insets.ack.dart @@ -0,0 +1,123 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// dart format width=80 + +part of 'block_insets.dart'; + +// ************************************************************************** +// AckModelGenerator +// ************************************************************************** + +final _blockInsetsObject = Ack.object({ + 'top': _normalizedInsetValueSchema(), + 'right': _normalizedInsetValueSchema(), + 'bottom': _normalizedInsetValueSchema(), + 'left': _normalizedInsetValueSchema(), +}); + +final _blockInsetsWireSchema = Ack.preserveBoundary(_blockInsetsObject); + +final _blockInsetsSchema = _blockInsetsObject.codec( + decode: _$BlockInsetsFromRuntime, + encode: _$BlockInsetsToRuntime, +); + +abstract final class BlockInsetsSchema { + static AckSchema, BlockInsets> get schema => + _blockInsetsSchema; + + static AckSchema, Map> get wireSchema => + _blockInsetsWireSchema; + + static BlockInsets parse(Object? value, {String? debugName}) => + _blockInsetsSchema.parse(value, debugName: debugName)!; + + static SchemaResult safeParse( + Object? value, { + String? debugName, + }) => _blockInsetsSchema.safeParse(value, debugName: debugName); + + static BlockInsets fromJson(Map json) => parse(json); + + static Map encode(BlockInsets value, {String? debugName}) => + _blockInsetsSchema.encode(value, debugName: debugName)!; + + static SchemaResult> safeEncode( + BlockInsets value, { + String? debugName, + }) => _blockInsetsSchema.safeEncode(value, debugName: debugName); + + static Map toJsonSchema() => + _blockInsetsSchema.toJsonSchema(); + + static AckSchemaModel toSchemaModel() => + AckSchemaModelExtension(_blockInsetsSchema).toSchemaModel(); +} + +BlockInsets _$BlockInsetsFromRuntime(Map value) => + _$BlockInsetsFromJson(Map.from(value)); + +Map _$BlockInsetsToRuntime(BlockInsets model) => + {..._$BlockInsetsToJson(model)}; + +mixin _$BlockInsetsAck { + BlockInsets copyWith({ + double? top, + double? right, + double? bottom, + double? left, + }) { + final self = this as BlockInsets; + return BlockInsets( + top: top ?? self.top, + right: right ?? self.right, + bottom: bottom ?? self.bottom, + left: left ?? self.left, + ); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! BlockInsets || runtimeType != other.runtimeType) { + return false; + } + final self = this as BlockInsets; + return deepEquals(self.top, other.top) && + deepEquals(self.right, other.right) && + deepEquals(self.bottom, other.bottom) && + deepEquals(self.left, other.left); + } + + @override + int get hashCode { + final self = this as BlockInsets; + return Object.hashAll([ + runtimeType, + deepHashCode(self.top), + deepHashCode(self.right), + deepHashCode(self.bottom), + deepHashCode(self.left), + ]); + } + + @override + String toString() { + final self = this as BlockInsets; + return 'BlockInsets(top: ${self.top}, right: ${self.right}, bottom: ${self.bottom}, left: ${self.left})'; + } + + Map toJson() => + Map.from(BlockInsetsSchema.encode(this as BlockInsets)); + + SchemaResult> safeToJson() => + BlockInsetsSchema.safeEncode(this as BlockInsets); +} + +double? _ackBlockInsetsFromRuntimeTop(Object? value) => value as double?; +Object? _ackBlockInsetsToRuntimeTop(double value) => value; +double? _ackBlockInsetsFromRuntimeRight(Object? value) => value as double?; +Object? _ackBlockInsetsToRuntimeRight(double value) => value; +double? _ackBlockInsetsFromRuntimeBottom(Object? value) => value as double?; +Object? _ackBlockInsetsToRuntimeBottom(double value) => value; +double? _ackBlockInsetsFromRuntimeLeft(Object? value) => value as double?; +Object? _ackBlockInsetsToRuntimeLeft(double value) => value; diff --git a/packages/core/lib/src/deck/block_insets.ack.g.dart b/packages/core/lib/src/deck/block_insets.ack.g.dart new file mode 100644 index 000000000..18885a3d9 --- /dev/null +++ b/packages/core/lib/src/deck/block_insets.ack.g.dart @@ -0,0 +1,23 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// dart format width=80 + +part of 'block_insets.dart'; + +// ************************************************************************** +// AckJsonSerializableGenerator +// ************************************************************************** + +BlockInsets _$BlockInsetsFromJson(Map json) => BlockInsets( + top: _ackBlockInsetsFromRuntimeTop(json['top']) ?? 0, + right: _ackBlockInsetsFromRuntimeRight(json['right']) ?? 0, + bottom: _ackBlockInsetsFromRuntimeBottom(json['bottom']) ?? 0, + left: _ackBlockInsetsFromRuntimeLeft(json['left']) ?? 0, +); + +Map _$BlockInsetsToJson(BlockInsets instance) => + { + 'top': _ackBlockInsetsToRuntimeTop(instance.top), + 'right': _ackBlockInsetsToRuntimeRight(instance.right), + 'bottom': _ackBlockInsetsToRuntimeBottom(instance.bottom), + 'left': _ackBlockInsetsToRuntimeLeft(instance.left), + }; diff --git a/packages/core/lib/src/deck/block_insets.dart b/packages/core/lib/src/deck/block_insets.dart index c8feeab76..41c72fd0c 100644 --- a/packages/core/lib/src/deck/block_insets.dart +++ b/packages/core/lib/src/deck/block_insets.dart @@ -1,7 +1,8 @@ import 'package:ack/ack.dart'; -import 'package:dart_mappable/dart_mappable.dart'; +import 'package:ack_annotations/ack_annotations.dart'; -part 'block_insets.mapper.dart'; +part 'block_insets.ack.dart'; +part 'block_insets.ack.g.dart'; String _authoringMessage(String field) => '$field must use a finite non-negative scalar, a non-empty object with ' @@ -11,22 +12,28 @@ String _authoringMessage(String field) => const _symmetricKeys = {'horizontal', 'vertical'}; const _edgeKeys = {'top', 'right', 'bottom', 'left'}; -final _insetValueSchema = Ack.number().min(0).finite(); +final _authoringInsetValueSchema = Ack.number().min(0).finite(); + +AckSchema _normalizedInsetValueSchema() => + _authoringInsetValueSchema.codec( + decode: (value) => value.toDouble(), + encode: (value) => value, + ); final _symmetricInsetsSchema = Ack.object( { - 'horizontal': _insetValueSchema.optional(), - 'vertical': _insetValueSchema.optional(), + 'horizontal': _authoringInsetValueSchema.optional(), + 'vertical': _authoringInsetValueSchema.optional(), }, additionalProperties: false, ).withConstraint(const _NonEmptyInsetsObjectConstraint()); final _partialEdgeInsetsSchema = Ack.object( { - 'top': _insetValueSchema.optional(), - 'right': _insetValueSchema.optional(), - 'bottom': _insetValueSchema.optional(), - 'left': _insetValueSchema.optional(), + 'top': _authoringInsetValueSchema.optional(), + 'right': _authoringInsetValueSchema.optional(), + 'bottom': _authoringInsetValueSchema.optional(), + 'left': _authoringInsetValueSchema.optional(), }, additionalProperties: false, ).withConstraint(const _NonEmptyInsetsObjectConstraint()); @@ -36,12 +43,31 @@ final _partialEdgeInsetsSchema = Ack.object( /// Used for both block `padding` and block `margin`. Authoring shorthand /// (scalar, symmetric, or partial physical edges) is normalized by /// [parseAuthoring]; compiled contracts only carry the normalized -/// four-edge form validated by [schema]. -@MappableClass() -final class BlockInsets with BlockInsetsMappable { +/// four-edge form validated by [BlockInsetsSchema.schema]. +@AckModel() +final class BlockInsets with _$BlockInsetsAck { + @AckField( + schema: _normalizedInsetValueSchema, + presence: AckFieldPresence.required, + ) final double top; + + @AckField( + schema: _normalizedInsetValueSchema, + presence: AckFieldPresence.required, + ) final double right; + + @AckField( + schema: _normalizedInsetValueSchema, + presence: AckFieldPresence.required, + ) final double bottom; + + @AckField( + schema: _normalizedInsetValueSchema, + presence: AckFieldPresence.required, + ) final double left; BlockInsets({ @@ -65,22 +91,14 @@ final class BlockInsets with BlockInsetsMappable { left: horizontal, ); - /// Normalized contract schema: a closed object with all four physical edges. - static final schema = Ack.object({ - 'top': _insetValueSchema, - 'right': _insetValueSchema, - 'bottom': _insetValueSchema, - 'left': _insetValueSchema, - }, additionalProperties: false); - /// Authoring schema: scalar, symmetric map, or physical-edge map. static final authoringSchema = Ack.anyOf([ - _insetValueSchema, + _authoringInsetValueSchema, _symmetricInsetsSchema, _partialEdgeInsetsSchema, ]); - static final fromMap = BlockInsetsMapper.fromMap; + static final fromJson = BlockInsetsSchema.fromJson; /// Parses one supported authoring form and normalizes it to physical edges. /// diff --git a/packages/core/lib/src/deck/block_insets.mapper.dart b/packages/core/lib/src/deck/block_insets.mapper.dart deleted file mode 100644 index b3c55cc98..000000000 --- a/packages/core/lib/src/deck/block_insets.mapper.dart +++ /dev/null @@ -1,164 +0,0 @@ -// coverage:ignore-file -// GENERATED CODE - DO NOT MODIFY BY HAND -// dart format off -// ignore_for_file: type=lint -// ignore_for_file: invalid_use_of_protected_member -// ignore_for_file: unused_element, unnecessary_cast, override_on_non_overriding_member -// ignore_for_file: strict_raw_type, inference_failure_on_untyped_parameter - -part of 'block_insets.dart'; - -class BlockInsetsMapper extends ClassMapperBase { - BlockInsetsMapper._(); - - static BlockInsetsMapper? _instance; - static BlockInsetsMapper ensureInitialized() { - if (_instance == null) { - MapperContainer.globals.use(_instance = BlockInsetsMapper._()); - } - return _instance!; - } - - @override - final String id = 'BlockInsets'; - - static double _$top(BlockInsets v) => v.top; - static const Field _f$top = Field( - 'top', - _$top, - opt: true, - def: 0, - ); - static double _$right(BlockInsets v) => v.right; - static const Field _f$right = Field( - 'right', - _$right, - opt: true, - def: 0, - ); - static double _$bottom(BlockInsets v) => v.bottom; - static const Field _f$bottom = Field( - 'bottom', - _$bottom, - opt: true, - def: 0, - ); - static double _$left(BlockInsets v) => v.left; - static const Field _f$left = Field( - 'left', - _$left, - opt: true, - def: 0, - ); - - @override - final MappableFields fields = const { - #top: _f$top, - #right: _f$right, - #bottom: _f$bottom, - #left: _f$left, - }; - - static BlockInsets _instantiate(DecodingData data) { - return BlockInsets( - top: data.dec(_f$top), - right: data.dec(_f$right), - bottom: data.dec(_f$bottom), - left: data.dec(_f$left), - ); - } - - @override - final Function instantiate = _instantiate; - - static BlockInsets fromMap(Map map) { - return ensureInitialized().decodeMap(map); - } - - static BlockInsets fromJson(String json) { - return ensureInitialized().decodeJson(json); - } -} - -mixin BlockInsetsMappable { - String toJson() { - return BlockInsetsMapper.ensureInitialized().encodeJson( - this as BlockInsets, - ); - } - - Map toMap() { - return BlockInsetsMapper.ensureInitialized().encodeMap( - this as BlockInsets, - ); - } - - BlockInsetsCopyWith get copyWith => - _BlockInsetsCopyWithImpl( - this as BlockInsets, - $identity, - $identity, - ); - @override - String toString() { - return BlockInsetsMapper.ensureInitialized().stringifyValue( - this as BlockInsets, - ); - } - - @override - bool operator ==(Object other) { - return BlockInsetsMapper.ensureInitialized().equalsValue( - this as BlockInsets, - other, - ); - } - - @override - int get hashCode { - return BlockInsetsMapper.ensureInitialized().hashValue(this as BlockInsets); - } -} - -extension BlockInsetsValueCopy<$R, $Out> - on ObjectCopyWith<$R, BlockInsets, $Out> { - BlockInsetsCopyWith<$R, BlockInsets, $Out> get $asBlockInsets => - $base.as((v, t, t2) => _BlockInsetsCopyWithImpl<$R, $Out>(v, t, t2)); -} - -abstract class BlockInsetsCopyWith<$R, $In extends BlockInsets, $Out> - implements ClassCopyWith<$R, $In, $Out> { - $R call({double? top, double? right, double? bottom, double? left}); - BlockInsetsCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>(Then<$Out2, $R2> t); -} - -class _BlockInsetsCopyWithImpl<$R, $Out> - extends ClassCopyWithBase<$R, BlockInsets, $Out> - implements BlockInsetsCopyWith<$R, BlockInsets, $Out> { - _BlockInsetsCopyWithImpl(super.value, super.then, super.then2); - - @override - late final ClassMapperBase $mapper = - BlockInsetsMapper.ensureInitialized(); - @override - $R call({double? top, double? right, double? bottom, double? left}) => $apply( - FieldCopyWithData({ - if (top != null) #top: top, - if (right != null) #right: right, - if (bottom != null) #bottom: bottom, - if (left != null) #left: left, - }), - ); - @override - BlockInsets $make(CopyWithData data) => BlockInsets( - top: data.get(#top, or: $value.top), - right: data.get(#right, or: $value.right), - bottom: data.get(#bottom, or: $value.bottom), - left: data.get(#left, or: $value.left), - ); - - @override - BlockInsetsCopyWith<$R2, BlockInsets, $Out2> $chain<$R2, $Out2>( - Then<$Out2, $R2> t, - ) => _BlockInsetsCopyWithImpl<$R2, $Out2>($value, $cast, t); -} diff --git a/packages/core/lib/src/deck/block_model.ack.dart b/packages/core/lib/src/deck/block_model.ack.dart new file mode 100644 index 000000000..fd3e6a5dc --- /dev/null +++ b/packages/core/lib/src/deck/block_model.ack.dart @@ -0,0 +1,537 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// dart format width=80 + +part of 'block_model.dart'; + +// ************************************************************************** +// AckModelGenerator +// ************************************************************************** + +final _contentBlockObject = Ack.object({ + 'type': Ack.literal('block').optional(), + 'align': Ack.enumValues(ContentAlignment.values).optional().nullable(), + 'flex': positiveFlexSchema().withDefault(1), + 'margin': BlockInsetsSchema.schema.optional().nullable(), + 'padding': BlockInsetsSchema.schema.optional().nullable(), + 'scrollable': Ack.boolean().withDefault(false), + 'content': Ack.string().optional(), +}); + +final _contentBlockWireSchema = Ack.preserveBoundary(_contentBlockObject); + +final _contentBlockSchema = _contentBlockObject.codec( + decode: _$ContentBlockFromRuntime, + encode: _$ContentBlockToRuntime, +); + +abstract final class ContentBlockSchema { + static AckSchema, ContentBlock> get schema => + _contentBlockSchema; + + static AckSchema, Map> get wireSchema => + _contentBlockWireSchema; + + static ContentBlock parse(Object? value, {String? debugName}) => + _contentBlockSchema.parse(value, debugName: debugName)!; + + static SchemaResult safeParse( + Object? value, { + String? debugName, + }) => _contentBlockSchema.safeParse(value, debugName: debugName); + + static ContentBlock fromJson(Map json) => parse(json); + + static Map encode(ContentBlock value, {String? debugName}) => + _contentBlockSchema.encode(value, debugName: debugName)!; + + static SchemaResult> safeEncode( + ContentBlock value, { + String? debugName, + }) => _contentBlockSchema.safeEncode(value, debugName: debugName); + + static Map toJsonSchema() => + _contentBlockSchema.toJsonSchema(); + + static AckSchemaModel toSchemaModel() => + AckSchemaModelExtension(_contentBlockSchema).toSchemaModel(); +} + +ContentBlock _$ContentBlockFromRuntime(Map value) => + _$ContentBlockFromJson(Map.from(value)); + +Map _$ContentBlockToRuntime(ContentBlock model) { + final result = {..._$ContentBlockToJson(model)}; + return {...result, 'type': 'block'}; +} + +final class _ContentBlockCopyWithUnset { + const _ContentBlockCopyWithUnset(); +} + +mixin _$ContentBlockAck { + static const _ContentBlockCopyWithUnset _ackCopyWithUnset = + _ContentBlockCopyWithUnset(); + + ContentBlock copyWith({ + Object? content = _ackCopyWithUnset, + Object? align = _ackCopyWithUnset, + int? flex, + Object? margin = _ackCopyWithUnset, + Object? padding = _ackCopyWithUnset, + bool? scrollable, + }) { + final self = this as ContentBlock; + return ContentBlock( + identical(content, _ackCopyWithUnset) ? self.content : content as String?, + align: identical(align, _ackCopyWithUnset) + ? self.align + : align as ContentAlignment?, + flex: flex ?? self.flex, + margin: identical(margin, _ackCopyWithUnset) + ? self.margin + : margin as BlockInsets?, + padding: identical(padding, _ackCopyWithUnset) + ? self.padding + : padding as BlockInsets?, + scrollable: scrollable ?? self.scrollable, + ); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! ContentBlock || runtimeType != other.runtimeType) { + return false; + } + final self = this as ContentBlock; + return deepEquals(self.align, other.align) && + deepEquals(self.flex, other.flex) && + deepEquals(self.margin, other.margin) && + deepEquals(self.padding, other.padding) && + deepEquals(self.scrollable, other.scrollable) && + deepEquals(self.content, other.content); + } + + @override + int get hashCode { + final self = this as ContentBlock; + return Object.hashAll([ + runtimeType, + deepHashCode(self.align), + deepHashCode(self.flex), + deepHashCode(self.margin), + deepHashCode(self.padding), + deepHashCode(self.scrollable), + deepHashCode(self.content), + ]); + } + + @override + String toString() { + final self = this as ContentBlock; + return 'ContentBlock(align: ${self.align}, flex: ${self.flex}, margin: ${self.margin}, padding: ${self.padding}, scrollable: ${self.scrollable}, content: ${self.content})'; + } + + Map toJson() => Map.from( + ContentBlockSchema.encode(this as ContentBlock), + ); + + SchemaResult> safeToJson() => + ContentBlockSchema.safeEncode(this as ContentBlock); +} + +ContentAlignment? _ackContentBlockFromRuntimeAlign(Object? value) => + value as ContentAlignment?; +Object? _ackContentBlockToRuntimeAlign(ContentAlignment? value) => value; +int? _ackContentBlockFromRuntimeFlex(Object? value) => value as int?; +Object? _ackContentBlockToRuntimeFlex(int value) => value; +BlockInsets? _ackContentBlockFromRuntimeMargin(Object? value) => + value as BlockInsets?; +Object? _ackContentBlockToRuntimeMargin(BlockInsets? value) => value; +BlockInsets? _ackContentBlockFromRuntimePadding(Object? value) => + value as BlockInsets?; +Object? _ackContentBlockToRuntimePadding(BlockInsets? value) => value; +bool? _ackContentBlockFromRuntimeScrollable(Object? value) => value as bool?; +Object? _ackContentBlockToRuntimeScrollable(bool value) => value; +String? _ackContentBlockFromRuntimeContent(Object? value) => value as String?; +Object? _ackContentBlockToRuntimeContent(String value) => value; + +final _widgetBlockObject = Ack.object({ + 'type': Ack.literal('widget').optional(), + 'align': Ack.enumValues(ContentAlignment.values).optional().nullable(), + 'flex': positiveFlexSchema().withDefault(1), + 'margin': BlockInsetsSchema.schema.optional().nullable(), + 'padding': BlockInsetsSchema.schema.optional().nullable(), + 'scrollable': Ack.boolean().withDefault(false), + 'name': Ack.string(), +}, additionalProperties: true); + +final _widgetBlockWireSchema = Ack.preserveBoundary(_widgetBlockObject); + +final _widgetBlockSchema = _widgetBlockObject.codec( + decode: _$WidgetBlockFromRuntime, + encode: _$WidgetBlockToRuntime, +); + +abstract final class WidgetBlockSchema { + static AckSchema, WidgetBlock> get schema => + _widgetBlockSchema; + + static AckSchema, Map> get wireSchema => + _widgetBlockWireSchema; + + static WidgetBlock parse(Object? value, {String? debugName}) => + _widgetBlockSchema.parse(value, debugName: debugName)!; + + static SchemaResult safeParse( + Object? value, { + String? debugName, + }) => _widgetBlockSchema.safeParse(value, debugName: debugName); + + static WidgetBlock fromJson(Map json) => parse(json); + + static Map encode(WidgetBlock value, {String? debugName}) => + _widgetBlockSchema.encode(value, debugName: debugName)!; + + static SchemaResult> safeEncode( + WidgetBlock value, { + String? debugName, + }) => _widgetBlockSchema.safeEncode(value, debugName: debugName); + + static Map toJsonSchema() => + _widgetBlockSchema.toJsonSchema(); + + static AckSchemaModel toSchemaModel() => + AckSchemaModelExtension(_widgetBlockSchema).toSchemaModel(); +} + +WidgetBlock _$WidgetBlockFromRuntime(Map value) { + const declared = { + 'align', + 'flex', + 'margin', + 'padding', + 'scrollable', + 'name', + 'type', + }; + return _$WidgetBlockFromJson({ + ...value, + 'args': Map.fromEntries( + value.entries.where((entry) => !declared.contains(entry.key)), + ), + }); +} + +Map _$WidgetBlockToRuntime(WidgetBlock model) { + const declared = { + 'align', + 'flex', + 'margin', + 'padding', + 'scrollable', + 'name', + 'type', + }; + final result = {..._$WidgetBlockToJson(model)}; + result.remove('args'); + return { + for (final entry in model.args.entries) + if (!declared.contains(entry.key)) entry.key: entry.value, + ...result, + 'type': 'widget', + }; +} + +final class _WidgetBlockCopyWithUnset { + const _WidgetBlockCopyWithUnset(); +} + +mixin _$WidgetBlockAck { + static const _WidgetBlockCopyWithUnset _ackCopyWithUnset = + _WidgetBlockCopyWithUnset(); + + WidgetBlock copyWith({ + String? name, + Map? args, + Object? align = _ackCopyWithUnset, + int? flex, + Object? margin = _ackCopyWithUnset, + Object? padding = _ackCopyWithUnset, + bool? scrollable, + }) { + final self = this as WidgetBlock; + return WidgetBlock( + name: name ?? self.name, + args: args ?? self.args, + align: identical(align, _ackCopyWithUnset) + ? self.align + : align as ContentAlignment?, + flex: flex ?? self.flex, + margin: identical(margin, _ackCopyWithUnset) + ? self.margin + : margin as BlockInsets?, + padding: identical(padding, _ackCopyWithUnset) + ? self.padding + : padding as BlockInsets?, + scrollable: scrollable ?? self.scrollable, + ); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! WidgetBlock || runtimeType != other.runtimeType) { + return false; + } + final self = this as WidgetBlock; + return deepEquals(self.align, other.align) && + deepEquals(self.flex, other.flex) && + deepEquals(self.margin, other.margin) && + deepEquals(self.padding, other.padding) && + deepEquals(self.scrollable, other.scrollable) && + deepEquals(self.name, other.name) && + deepEquals(self.args, other.args); + } + + @override + int get hashCode { + final self = this as WidgetBlock; + return Object.hashAll([ + runtimeType, + deepHashCode(self.align), + deepHashCode(self.flex), + deepHashCode(self.margin), + deepHashCode(self.padding), + deepHashCode(self.scrollable), + deepHashCode(self.name), + deepHashCode(self.args), + ]); + } + + @override + String toString() { + final self = this as WidgetBlock; + return 'WidgetBlock(align: ${self.align}, flex: ${self.flex}, margin: ${self.margin}, padding: ${self.padding}, scrollable: ${self.scrollable}, name: ${self.name}, args: ${self.args})'; + } + + Map toJson() => + Map.from(WidgetBlockSchema.encode(this as WidgetBlock)); + + SchemaResult> safeToJson() => + WidgetBlockSchema.safeEncode(this as WidgetBlock); +} + +ContentAlignment? _ackWidgetBlockFromRuntimeAlign(Object? value) => + value as ContentAlignment?; +Object? _ackWidgetBlockToRuntimeAlign(ContentAlignment? value) => value; +int? _ackWidgetBlockFromRuntimeFlex(Object? value) => value as int?; +Object? _ackWidgetBlockToRuntimeFlex(int value) => value; +BlockInsets? _ackWidgetBlockFromRuntimeMargin(Object? value) => + value as BlockInsets?; +Object? _ackWidgetBlockToRuntimeMargin(BlockInsets? value) => value; +BlockInsets? _ackWidgetBlockFromRuntimePadding(Object? value) => + value as BlockInsets?; +Object? _ackWidgetBlockToRuntimePadding(BlockInsets? value) => value; +bool? _ackWidgetBlockFromRuntimeScrollable(Object? value) => value as bool?; +Object? _ackWidgetBlockToRuntimeScrollable(bool value) => value; +String _ackWidgetBlockFromRuntimeName(Object? value) => value as String; +Object? _ackWidgetBlockToRuntimeName(String value) => value; +Map? _ackWidgetBlockFromRuntimeArgs(Object? value) => + value == null + ? null + : deepUnmodifiableJsonMap(value as Map); +Object? _ackWidgetBlockToRuntimeArgs(Map value) => value; + +final _blockObject = Ack.discriminated( + discriminatorKey: 'type', + schemas: {'block': _contentBlockObject, 'widget': _widgetBlockObject}, +); + +final _blockWireSchema = Ack.preserveBoundary(_blockObject); + +final _blockSchema = _blockObject.codec( + decode: (value) => switch (value['type']) { + 'block' => _$ContentBlockFromRuntime(value), + 'widget' => _$WidgetBlockFromRuntime(value), + final unknown => throw StateError('Unknown type: $unknown'), + }, + encode: (model) => switch (model) { + ContentBlock() => _$ContentBlockToRuntime(model), + WidgetBlock() => _$WidgetBlockToRuntime(model), + }, +); + +abstract final class BlockSchema { + static AckSchema, Block> get schema => _blockSchema; + + static AckSchema, Map> get wireSchema => + _blockWireSchema; + + static Block parse(Object? value, {String? debugName}) => + _blockSchema.parse(value, debugName: debugName)!; + + static SchemaResult safeParse(Object? value, {String? debugName}) => + _blockSchema.safeParse(value, debugName: debugName); + + static Block fromJson(Map json) => parse(json); + + static Map encode(Block value, {String? debugName}) => + _blockSchema.encode(value, debugName: debugName)!; + + static SchemaResult> safeEncode( + Block value, { + String? debugName, + }) => _blockSchema.safeEncode(value, debugName: debugName); + + static Map toJsonSchema() => _blockSchema.toJsonSchema(); + + static AckSchemaModel toSchemaModel() => + AckSchemaModelExtension(_blockSchema).toSchemaModel(); +} + +mixin _$BlockAck { + Map toJson() => + Map.from(BlockSchema.encode(this as Block)); + + SchemaResult> safeToJson() => + BlockSchema.safeEncode(this as Block); +} + +final _sectionBlockObject = Ack.object({ + 'blocks': Ack.list(BlockSchema.schema).optional(), + 'align': Ack.enumValues(ContentAlignment.values).optional().nullable(), + 'flex': positiveFlexSchema().withDefault(1), + 'spacing': nonNegativeSpacingSchema().withDefault(0), + 'type': _sectionTypeSchema().withDefault('section'), +}); + +final _sectionBlockWireSchema = Ack.preserveBoundary(_sectionBlockObject); + +final _sectionBlockSchema = _sectionBlockObject.codec( + decode: _$SectionBlockFromRuntime, + encode: _$SectionBlockToRuntime, +); + +abstract final class SectionBlockSchema { + static AckSchema, SectionBlock> get schema => + _sectionBlockSchema; + + static AckSchema, Map> get wireSchema => + _sectionBlockWireSchema; + + static SectionBlock parse(Object? value, {String? debugName}) => + _sectionBlockSchema.parse(value, debugName: debugName)!; + + static SchemaResult safeParse( + Object? value, { + String? debugName, + }) => _sectionBlockSchema.safeParse(value, debugName: debugName); + + static SectionBlock fromJson(Map json) => parse(json); + + static Map encode(SectionBlock value, {String? debugName}) => + _sectionBlockSchema.encode(value, debugName: debugName)!; + + static SchemaResult> safeEncode( + SectionBlock value, { + String? debugName, + }) => _sectionBlockSchema.safeEncode(value, debugName: debugName); + + static Map toJsonSchema() => + _sectionBlockSchema.toJsonSchema(); + + static AckSchemaModel toSchemaModel() => + AckSchemaModelExtension(_sectionBlockSchema).toSchemaModel(); +} + +SectionBlock _$SectionBlockFromRuntime(Map value) => + _$SectionBlockFromJson(Map.from(value)); + +Map _$SectionBlockToRuntime(SectionBlock model) => + {..._$SectionBlockToJson(model)}; + +final class _SectionBlockCopyWithUnset { + const _SectionBlockCopyWithUnset(); +} + +mixin _$SectionBlockAck { + static const _SectionBlockCopyWithUnset _ackCopyWithUnset = + _SectionBlockCopyWithUnset(); + + SectionBlock copyWith({ + Object? blocks = _ackCopyWithUnset, + Object? align = _ackCopyWithUnset, + int? flex, + double? spacing, + String? type, + }) { + final self = this as SectionBlock; + return SectionBlock( + identical(blocks, _ackCopyWithUnset) + ? self.blocks + : blocks as List?, + align: identical(align, _ackCopyWithUnset) + ? self.align + : align as ContentAlignment?, + flex: flex ?? self.flex, + spacing: spacing ?? self.spacing, + type: type ?? self.type, + ); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! SectionBlock || runtimeType != other.runtimeType) { + return false; + } + final self = this as SectionBlock; + return deepEquals(self.blocks, other.blocks) && + deepEquals(self.align, other.align) && + deepEquals(self.flex, other.flex) && + deepEquals(self.spacing, other.spacing) && + deepEquals(self.type, other.type); + } + + @override + int get hashCode { + final self = this as SectionBlock; + return Object.hashAll([ + runtimeType, + deepHashCode(self.blocks), + deepHashCode(self.align), + deepHashCode(self.flex), + deepHashCode(self.spacing), + deepHashCode(self.type), + ]); + } + + @override + String toString() { + final self = this as SectionBlock; + return 'SectionBlock(blocks: ${self.blocks}, align: ${self.align}, flex: ${self.flex}, spacing: ${self.spacing}, type: ${self.type})'; + } + + Map toJson() => Map.from( + SectionBlockSchema.encode(this as SectionBlock), + ); + + SchemaResult> safeToJson() => + SectionBlockSchema.safeEncode(this as SectionBlock); +} + +List? _ackSectionBlockFromRuntimeBlocks(Object? value) => value == null + ? null + : List.unmodifiable((value as List).map((item) => item as Block)); +Object? _ackSectionBlockToRuntimeBlocks(List value) => + value.map((item) => item).toList(growable: false); +ContentAlignment? _ackSectionBlockFromRuntimeAlign(Object? value) => + value as ContentAlignment?; +Object? _ackSectionBlockToRuntimeAlign(ContentAlignment? value) => value; +int? _ackSectionBlockFromRuntimeFlex(Object? value) => value as int?; +Object? _ackSectionBlockToRuntimeFlex(int value) => value; +double? _ackSectionBlockFromRuntimeSpacing(Object? value) => value as double?; +Object? _ackSectionBlockToRuntimeSpacing(double value) => value; +String? _ackSectionBlockFromRuntimeType(Object? value) => value as String?; +Object? _ackSectionBlockToRuntimeType(String value) => value; diff --git a/packages/core/lib/src/deck/block_model.ack.g.dart b/packages/core/lib/src/deck/block_model.ack.g.dart new file mode 100644 index 000000000..29b61c852 --- /dev/null +++ b/packages/core/lib/src/deck/block_model.ack.g.dart @@ -0,0 +1,66 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// dart format width=80 + +part of 'block_model.dart'; + +// ************************************************************************** +// AckJsonSerializableGenerator +// ************************************************************************** + +ContentBlock _$ContentBlockFromJson(Map json) => ContentBlock( + _ackContentBlockFromRuntimeContent(json['content']), + align: _ackContentBlockFromRuntimeAlign(json['align']), + flex: _ackContentBlockFromRuntimeFlex(json['flex']) ?? 1, + margin: _ackContentBlockFromRuntimeMargin(json['margin']), + padding: _ackContentBlockFromRuntimePadding(json['padding']), + scrollable: + _ackContentBlockFromRuntimeScrollable(json['scrollable']) ?? false, +); + +Map _$ContentBlockToJson(ContentBlock instance) => + { + 'align': ?_ackContentBlockToRuntimeAlign(instance.align), + 'flex': _ackContentBlockToRuntimeFlex(instance.flex), + 'margin': ?_ackContentBlockToRuntimeMargin(instance.margin), + 'padding': ?_ackContentBlockToRuntimePadding(instance.padding), + 'scrollable': _ackContentBlockToRuntimeScrollable(instance.scrollable), + 'content': _ackContentBlockToRuntimeContent(instance.content), + }; + +WidgetBlock _$WidgetBlockFromJson(Map json) => WidgetBlock( + name: _ackWidgetBlockFromRuntimeName(json['name']), + args: _ackWidgetBlockFromRuntimeArgs(json['args']), + align: _ackWidgetBlockFromRuntimeAlign(json['align']), + flex: _ackWidgetBlockFromRuntimeFlex(json['flex']) ?? 1, + margin: _ackWidgetBlockFromRuntimeMargin(json['margin']), + padding: _ackWidgetBlockFromRuntimePadding(json['padding']), + scrollable: _ackWidgetBlockFromRuntimeScrollable(json['scrollable']) ?? false, +); + +Map _$WidgetBlockToJson(WidgetBlock instance) => + { + 'align': ?_ackWidgetBlockToRuntimeAlign(instance.align), + 'flex': _ackWidgetBlockToRuntimeFlex(instance.flex), + 'margin': ?_ackWidgetBlockToRuntimeMargin(instance.margin), + 'padding': ?_ackWidgetBlockToRuntimePadding(instance.padding), + 'scrollable': _ackWidgetBlockToRuntimeScrollable(instance.scrollable), + 'args': _ackWidgetBlockToRuntimeArgs(instance.args), + 'name': _ackWidgetBlockToRuntimeName(instance.name), + }; + +SectionBlock _$SectionBlockFromJson(Map json) => SectionBlock( + _ackSectionBlockFromRuntimeBlocks(json['blocks']), + align: _ackSectionBlockFromRuntimeAlign(json['align']), + flex: _ackSectionBlockFromRuntimeFlex(json['flex']) ?? 1, + spacing: _ackSectionBlockFromRuntimeSpacing(json['spacing']) ?? 0, + type: _ackSectionBlockFromRuntimeType(json['type']) ?? 'section', +); + +Map _$SectionBlockToJson(SectionBlock instance) => + { + 'blocks': _ackSectionBlockToRuntimeBlocks(instance.blocks), + 'align': ?_ackSectionBlockToRuntimeAlign(instance.align), + 'flex': _ackSectionBlockToRuntimeFlex(instance.flex), + 'spacing': _ackSectionBlockToRuntimeSpacing(instance.spacing), + 'type': _ackSectionBlockToRuntimeType(instance.type), + }; diff --git a/packages/core/lib/src/deck/block_model.dart b/packages/core/lib/src/deck/block_model.dart index b883f13ad..43002318e 100644 --- a/packages/core/lib/src/deck/block_model.dart +++ b/packages/core/lib/src/deck/block_model.dart @@ -1,17 +1,26 @@ import 'package:ack/ack.dart'; -import 'package:dart_mappable/dart_mappable.dart'; +import 'package:ack_annotations/ack_annotations.dart'; import 'block_insets.dart'; -part 'block_model.mapper.dart'; +part 'block_model.ack.dart'; +part 'block_model.ack.g.dart'; /// Positive flex weight shared by the canonical block/section schemas and the /// AI-generation projection in `slide_contract.dart`. -final positiveFlexSchema = Ack.integer().positive(); +IntegerSchema positiveFlexSchema() => Ack.integer().positive(); /// Finite non-negative number shared by section `spacing` and the /// AI-generation projection in `slide_contract.dart`. -final nonNegativeSpacingSchema = Ack.number().min(0).finite(); +AckSchema nonNegativeSpacingSchema() => Ack.number() + .min(0) + .finite() + .codec( + decode: (value) => value.toDouble(), + encode: (value) => value, + ); + +StringSchema _sectionTypeSchema() => Ack.literal(SectionBlock.key); int _validateFlex(int flex) { if (flex > 0) return flex; @@ -31,25 +40,13 @@ double _validateSpacing(double spacing) { ); } -void _validateFlexInput(Map map) { - if (map['flex'] case final int flex) { - _validateFlex(flex); - } -} - -void _validateSpacingInput(Map map) { - if (map['spacing'] case final num spacing) { - _validateSpacing(spacing.toDouble()); - } -} - Map _normalizeAuthoringInsets(Map map) { var normalized = map; for (final field in const ['margin', 'padding']) { final value = normalized[field]; if (value == null) continue; final insets = BlockInsets.parseAuthoring(value, field: field); - normalized = {...normalized, field: insets.toMap()}; + normalized = {...normalized, field: insets.toJson()}; } return normalized; @@ -59,17 +56,19 @@ Map _normalizeAuthoringInsets(Map map) { /// /// Blocks are leaf content units inside sections. They support alignment, /// flexible sizing, and scrolling. -@MappableClass(discriminatorKey: 'type', ignoreNull: true) -sealed class Block with BlockMappable { - final String type; +@AckModel(discriminatorKey: 'type') +sealed class Block with _$BlockAck { + String get type; + final ContentAlignment? align; + + @AckField(schema: positiveFlexSchema) final int flex; final BlockInsets? margin; final BlockInsets? padding; final bool scrollable; Block({ - required this.type, this.align, int flex = 1, this.margin, @@ -77,25 +76,12 @@ sealed class Block with BlockMappable { this.scrollable = false, }) : flex = _validateFlex(flex); - /// Base schema for all block types - static final schema = Ack.object({ - 'type': Ack.string(), - 'align': ContentAlignment.schema.optional(), - 'flex': positiveFlexSchema.optional(), - 'margin': BlockInsets.schema.optional(), - 'padding': BlockInsets.schema.optional(), - 'scrollable': Ack.boolean().optional(), - }, additionalProperties: true); - /// Parses a block from normalized contract data. /// /// Automatically determines the block type from the discriminator key. /// Insets must already be normalized physical edges; use [parseAuthoring] /// for Markdown directive input. - static Block parse(Map map) { - _validateFlexInput(map); - return fromMap(discriminatedSchema.parse(map)!); - } + static Block parse(Map map) => BlockSchema.parse(map); /// Parses a block from authored directive options. /// @@ -105,30 +91,25 @@ sealed class Block with BlockMappable { return parse(_normalizeAuthoringInsets(map)); } - /// Schema for discriminated union of block types. - /// - /// Note: SectionBlock is intentionally not included here as it is a container - /// for discriminated blocks, not a discriminated type itself. - static final discriminatedSchema = Ack.discriminated( - discriminatorKey: 'type', - schemas: { - ContentBlock.key: ContentBlock.schema, - WidgetBlock.key: WidgetBlock.schema, - }, - ); - - static final fromMap = BlockMapper.fromMap; + static final fromJson = BlockSchema.fromJson; } /// A section that contains multiple child blocks arranged horizontally. /// /// Sections are used to create multi-column layouts within a slide. -@MappableClass(ignoreNull: true) -class SectionBlock with SectionBlockMappable { +@AckModel() +final class SectionBlock with _$SectionBlockAck { + @AckField(presence: AckFieldPresence.optional) final List blocks; final ContentAlignment? align; + + @AckField(schema: positiveFlexSchema) final int flex; + + @AckField(schema: nonNegativeSpacingSchema) final double spacing; + + @AckField(schema: _sectionTypeSchema) final String type; static const key = 'section'; @@ -138,7 +119,7 @@ class SectionBlock with SectionBlockMappable { this.align, int flex = 1, double spacing = 0, - String type = key, + String type = 'section', }) : blocks = List.unmodifiable(blocks ?? const []), flex = _validateFlex(flex), spacing = _validateSpacing(spacing), @@ -152,14 +133,11 @@ class SectionBlock with SectionBlockMappable { return block.align ?? align ?? ContentAlignment.centerLeft; } - static final fromMap = SectionBlockMapper.fromMap; + static final fromJson = SectionBlockSchema.fromJson; /// Parses a section block from a JSON map. - static SectionBlock parse(Map map) { - _validateFlexInput(map); - _validateSpacingInput(map); - return fromMap(schema.parse(map)!); - } + static SectionBlock parse(Map map) => + SectionBlockSchema.parse(map); /// Creates a section block with a single text block. static SectionBlock text(String content) { @@ -170,27 +148,19 @@ class SectionBlock with SectionBlockMappable { if (type == key) return type; throw ArgumentError.value(type, 'type', 'SectionBlock type must be "$key"'); } - - /// Validation schema for section blocks. - static final schema = Ack.object({ - 'type': Ack.literal(key).optional(), - 'align': ContentAlignment.schema.optional(), - 'flex': positiveFlexSchema.optional(), - 'spacing': nonNegativeSpacingSchema.optional(), - 'blocks': Ack.list(Block.discriminatedSchema).optional(), - }, additionalProperties: false); } -/// Alias used by generated Ack model schemas for [SectionBlock] references. -final sectionBlockSchema = SectionBlock.schema; - /// A block that displays markdown content. /// /// This is the most common block type, used for text and markdown content. -@MappableClass(discriminatorValue: ContentBlock.key) -class ContentBlock extends Block with ContentBlockMappable { +@AckModel( + discriminatorValue: ContentBlock.key, + unknownProperties: AckUnknownPropertyPolicy.reject, +) +final class ContentBlock extends Block with _$ContentBlockAck { static const key = 'block'; + @AckField(presence: AckFieldPresence.optional) final String content; ContentBlock( @@ -201,29 +171,18 @@ class ContentBlock extends Block with ContentBlockMappable { super.padding, super.scrollable, }) : content = content ?? '', - super(type: key); + super(); - static final fromMap = ContentBlockMapper.fromMap; + @override + String get type => 'block'; - /// Validation schema for content blocks. - static final schema = Ack.object({ - 'type': Ack.literal(key).optional(), - 'align': ContentAlignment.schema.optional(), - 'flex': positiveFlexSchema.optional(), - 'margin': BlockInsets.schema.optional(), - 'padding': BlockInsets.schema.optional(), - 'scrollable': Ack.boolean().optional(), - 'content': Ack.string().optional(), - }, additionalProperties: true); + static final fromJson = ContentBlockSchema.fromJson; /// Parses a content block from normalized contract data. - static ContentBlock parse(Map map) { - _validateFlexInput(map); - return fromMap(schema.parse(map)!); - } + static ContentBlock parse(Map map) => + ContentBlockSchema.parse(map); } -@MappableEnum() enum DartPadTheme { dark, light; @@ -245,7 +204,6 @@ enum DartPadTheme { } } -@MappableEnum() enum ImageFit { fill, contain, @@ -272,11 +230,12 @@ enum ImageFit { } } -@MappableClass( +@AckModel( discriminatorValue: WidgetBlock.key, - hook: UnmappedPropertiesHook('args'), + unknownProperties: AckUnknownPropertyPolicy.capture, + captureField: 'args', ) -class WidgetBlock extends Block with WidgetBlockMappable { +final class WidgetBlock extends Block with _$WidgetBlockAck { static const key = 'widget'; static const _reservedKeys = { 'name', @@ -299,14 +258,17 @@ class WidgetBlock extends Block with WidgetBlockMappable { super.padding, super.scrollable, }) : args = _validateArgs(args), - super(type: key); + super(); + + @override + String get type => 'widget'; static Map _validateArgs(Map? args) { - if (args == null) return const {}; + if (args == null) return deepUnmodifiableJsonMap(const {}); - // Single pass: strip the 'type' discriminator key leaked by - // UnmappedPropertiesHook during deserialization, and reject any - // other reserved keys that indicate a caller mistake. + // The discriminator belongs to the wire shape, not widget arguments. + // Strip it while rejecting other reserved keys that indicate a caller + // mistake. final filtered = {}; final collisions = []; @@ -324,28 +286,16 @@ class WidgetBlock extends Block with WidgetBlockMappable { 'args must not contain reserved keys: ${collisions.join(', ')}', ); } - return Map.unmodifiable(filtered); + return deepUnmodifiableJsonMap(filtered); } - static final fromMap = WidgetBlockMapper.fromMap; - - static final schema = Ack.object({ - 'align': ContentAlignment.schema.optional(), - 'flex': positiveFlexSchema.optional(), - 'margin': BlockInsets.schema.optional(), - 'padding': BlockInsets.schema.optional(), - 'scrollable': Ack.boolean().optional(), - 'name': Ack.string(), - }, additionalProperties: true); + static final fromJson = WidgetBlockSchema.fromJson; /// Parses a widget block from normalized contract data. - static WidgetBlock parse(Map map) { - _validateFlexInput(map); - return fromMap(schema.parse(map)!); - } + static WidgetBlock parse(Map map) => + WidgetBlockSchema.parse(map); } -@MappableEnum() enum ContentAlignment { topLeft, topCenter, diff --git a/packages/core/lib/src/deck/block_model.mapper.dart b/packages/core/lib/src/deck/block_model.mapper.dart deleted file mode 100644 index 3783adf00..000000000 --- a/packages/core/lib/src/deck/block_model.mapper.dart +++ /dev/null @@ -1,944 +0,0 @@ -// coverage:ignore-file -// GENERATED CODE - DO NOT MODIFY BY HAND -// dart format off -// ignore_for_file: type=lint -// ignore_for_file: invalid_use_of_protected_member -// ignore_for_file: unused_element, unnecessary_cast, override_on_non_overriding_member -// ignore_for_file: strict_raw_type, inference_failure_on_untyped_parameter - -part of 'block_model.dart'; - -class DartPadThemeMapper extends EnumMapper { - DartPadThemeMapper._(); - - static DartPadThemeMapper? _instance; - static DartPadThemeMapper ensureInitialized() { - if (_instance == null) { - MapperContainer.globals.use(_instance = DartPadThemeMapper._()); - } - return _instance!; - } - - static DartPadTheme fromValue(dynamic value) { - ensureInitialized(); - return MapperContainer.globals.fromValue(value); - } - - @override - DartPadTheme decode(dynamic value) { - switch (value) { - case r'dark': - return DartPadTheme.dark; - case r'light': - return DartPadTheme.light; - default: - throw MapperException.unknownEnumValue(value); - } - } - - @override - dynamic encode(DartPadTheme self) { - switch (self) { - case DartPadTheme.dark: - return r'dark'; - case DartPadTheme.light: - return r'light'; - } - } -} - -extension DartPadThemeMapperExtension on DartPadTheme { - String toValue() { - DartPadThemeMapper.ensureInitialized(); - return MapperContainer.globals.toValue(this) as String; - } -} - -class ImageFitMapper extends EnumMapper { - ImageFitMapper._(); - - static ImageFitMapper? _instance; - static ImageFitMapper ensureInitialized() { - if (_instance == null) { - MapperContainer.globals.use(_instance = ImageFitMapper._()); - } - return _instance!; - } - - static ImageFit fromValue(dynamic value) { - ensureInitialized(); - return MapperContainer.globals.fromValue(value); - } - - @override - ImageFit decode(dynamic value) { - switch (value) { - case r'fill': - return ImageFit.fill; - case r'contain': - return ImageFit.contain; - case r'cover': - return ImageFit.cover; - case r'fitWidth': - return ImageFit.fitWidth; - case r'fitHeight': - return ImageFit.fitHeight; - case r'none': - return ImageFit.none; - case r'scaleDown': - return ImageFit.scaleDown; - default: - throw MapperException.unknownEnumValue(value); - } - } - - @override - dynamic encode(ImageFit self) { - switch (self) { - case ImageFit.fill: - return r'fill'; - case ImageFit.contain: - return r'contain'; - case ImageFit.cover: - return r'cover'; - case ImageFit.fitWidth: - return r'fitWidth'; - case ImageFit.fitHeight: - return r'fitHeight'; - case ImageFit.none: - return r'none'; - case ImageFit.scaleDown: - return r'scaleDown'; - } - } -} - -extension ImageFitMapperExtension on ImageFit { - String toValue() { - ImageFitMapper.ensureInitialized(); - return MapperContainer.globals.toValue(this) as String; - } -} - -class ContentAlignmentMapper extends EnumMapper { - ContentAlignmentMapper._(); - - static ContentAlignmentMapper? _instance; - static ContentAlignmentMapper ensureInitialized() { - if (_instance == null) { - MapperContainer.globals.use(_instance = ContentAlignmentMapper._()); - } - return _instance!; - } - - static ContentAlignment fromValue(dynamic value) { - ensureInitialized(); - return MapperContainer.globals.fromValue(value); - } - - @override - ContentAlignment decode(dynamic value) { - switch (value) { - case r'topLeft': - return ContentAlignment.topLeft; - case r'topCenter': - return ContentAlignment.topCenter; - case r'topRight': - return ContentAlignment.topRight; - case r'centerLeft': - return ContentAlignment.centerLeft; - case r'center': - return ContentAlignment.center; - case r'centerRight': - return ContentAlignment.centerRight; - case r'bottomLeft': - return ContentAlignment.bottomLeft; - case r'bottomCenter': - return ContentAlignment.bottomCenter; - case r'bottomRight': - return ContentAlignment.bottomRight; - default: - throw MapperException.unknownEnumValue(value); - } - } - - @override - dynamic encode(ContentAlignment self) { - switch (self) { - case ContentAlignment.topLeft: - return r'topLeft'; - case ContentAlignment.topCenter: - return r'topCenter'; - case ContentAlignment.topRight: - return r'topRight'; - case ContentAlignment.centerLeft: - return r'centerLeft'; - case ContentAlignment.center: - return r'center'; - case ContentAlignment.centerRight: - return r'centerRight'; - case ContentAlignment.bottomLeft: - return r'bottomLeft'; - case ContentAlignment.bottomCenter: - return r'bottomCenter'; - case ContentAlignment.bottomRight: - return r'bottomRight'; - } - } -} - -extension ContentAlignmentMapperExtension on ContentAlignment { - String toValue() { - ContentAlignmentMapper.ensureInitialized(); - return MapperContainer.globals.toValue(this) as String; - } -} - -class BlockMapper extends ClassMapperBase { - BlockMapper._(); - - static BlockMapper? _instance; - static BlockMapper ensureInitialized() { - if (_instance == null) { - MapperContainer.globals.use(_instance = BlockMapper._()); - ContentBlockMapper.ensureInitialized(); - WidgetBlockMapper.ensureInitialized(); - ContentAlignmentMapper.ensureInitialized(); - BlockInsetsMapper.ensureInitialized(); - } - return _instance!; - } - - @override - final String id = 'Block'; - - static String _$type(Block v) => v.type; - static const Field _f$type = Field('type', _$type); - static ContentAlignment? _$align(Block v) => v.align; - static const Field _f$align = Field( - 'align', - _$align, - opt: true, - ); - static int _$flex(Block v) => v.flex; - static const Field _f$flex = Field( - 'flex', - _$flex, - opt: true, - def: 1, - ); - static BlockInsets? _$margin(Block v) => v.margin; - static const Field _f$margin = Field( - 'margin', - _$margin, - opt: true, - ); - static BlockInsets? _$padding(Block v) => v.padding; - static const Field _f$padding = Field( - 'padding', - _$padding, - opt: true, - ); - static bool _$scrollable(Block v) => v.scrollable; - static const Field _f$scrollable = Field( - 'scrollable', - _$scrollable, - opt: true, - def: false, - ); - - @override - final MappableFields fields = const { - #type: _f$type, - #align: _f$align, - #flex: _f$flex, - #margin: _f$margin, - #padding: _f$padding, - #scrollable: _f$scrollable, - }; - @override - final bool ignoreNull = true; - - static Block _instantiate(DecodingData data) { - throw MapperException.missingSubclass( - 'Block', - 'type', - '${data.value['type']}', - ); - } - - @override - final Function instantiate = _instantiate; - - static Block fromMap(Map map) { - return ensureInitialized().decodeMap(map); - } - - static Block fromJson(String json) { - return ensureInitialized().decodeJson(json); - } -} - -mixin BlockMappable { - String toJson(); - Map toMap(); - BlockCopyWith get copyWith; -} - -abstract class BlockCopyWith<$R, $In extends Block, $Out> - implements ClassCopyWith<$R, $In, $Out> { - BlockInsetsCopyWith<$R, BlockInsets, BlockInsets>? get margin; - BlockInsetsCopyWith<$R, BlockInsets, BlockInsets>? get padding; - $R call({ - ContentAlignment? align, - int? flex, - BlockInsets? margin, - BlockInsets? padding, - bool? scrollable, - }); - BlockCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>(Then<$Out2, $R2> t); -} - -class SectionBlockMapper extends ClassMapperBase { - SectionBlockMapper._(); - - static SectionBlockMapper? _instance; - static SectionBlockMapper ensureInitialized() { - if (_instance == null) { - MapperContainer.globals.use(_instance = SectionBlockMapper._()); - BlockMapper.ensureInitialized(); - ContentAlignmentMapper.ensureInitialized(); - } - return _instance!; - } - - @override - final String id = 'SectionBlock'; - - static List _$blocks(SectionBlock v) => v.blocks; - static const Field> _f$blocks = Field( - 'blocks', - _$blocks, - ); - static ContentAlignment? _$align(SectionBlock v) => v.align; - static const Field _f$align = Field( - 'align', - _$align, - opt: true, - ); - static int _$flex(SectionBlock v) => v.flex; - static const Field _f$flex = Field( - 'flex', - _$flex, - opt: true, - def: 1, - ); - static double _$spacing(SectionBlock v) => v.spacing; - static const Field _f$spacing = Field( - 'spacing', - _$spacing, - opt: true, - def: 0, - ); - static String _$type(SectionBlock v) => v.type; - static const Field _f$type = Field( - 'type', - _$type, - opt: true, - def: SectionBlock.key, - ); - - @override - final MappableFields fields = const { - #blocks: _f$blocks, - #align: _f$align, - #flex: _f$flex, - #spacing: _f$spacing, - #type: _f$type, - }; - @override - final bool ignoreNull = true; - - static SectionBlock _instantiate(DecodingData data) { - return SectionBlock( - data.dec(_f$blocks), - align: data.dec(_f$align), - flex: data.dec(_f$flex), - spacing: data.dec(_f$spacing), - type: data.dec(_f$type), - ); - } - - @override - final Function instantiate = _instantiate; - - static SectionBlock fromMap(Map map) { - return ensureInitialized().decodeMap(map); - } - - static SectionBlock fromJson(String json) { - return ensureInitialized().decodeJson(json); - } -} - -mixin SectionBlockMappable { - String toJson() { - return SectionBlockMapper.ensureInitialized().encodeJson( - this as SectionBlock, - ); - } - - Map toMap() { - return SectionBlockMapper.ensureInitialized().encodeMap( - this as SectionBlock, - ); - } - - SectionBlockCopyWith get copyWith => - _SectionBlockCopyWithImpl( - this as SectionBlock, - $identity, - $identity, - ); - @override - String toString() { - return SectionBlockMapper.ensureInitialized().stringifyValue( - this as SectionBlock, - ); - } - - @override - bool operator ==(Object other) { - return SectionBlockMapper.ensureInitialized().equalsValue( - this as SectionBlock, - other, - ); - } - - @override - int get hashCode { - return SectionBlockMapper.ensureInitialized().hashValue( - this as SectionBlock, - ); - } -} - -extension SectionBlockValueCopy<$R, $Out> - on ObjectCopyWith<$R, SectionBlock, $Out> { - SectionBlockCopyWith<$R, SectionBlock, $Out> get $asSectionBlock => - $base.as((v, t, t2) => _SectionBlockCopyWithImpl<$R, $Out>(v, t, t2)); -} - -abstract class SectionBlockCopyWith<$R, $In extends SectionBlock, $Out> - implements ClassCopyWith<$R, $In, $Out> { - ListCopyWith<$R, Block, BlockCopyWith<$R, Block, Block>> get blocks; - $R call({ - List? blocks, - ContentAlignment? align, - int? flex, - double? spacing, - String? type, - }); - SectionBlockCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>(Then<$Out2, $R2> t); -} - -class _SectionBlockCopyWithImpl<$R, $Out> - extends ClassCopyWithBase<$R, SectionBlock, $Out> - implements SectionBlockCopyWith<$R, SectionBlock, $Out> { - _SectionBlockCopyWithImpl(super.value, super.then, super.then2); - - @override - late final ClassMapperBase $mapper = - SectionBlockMapper.ensureInitialized(); - @override - ListCopyWith<$R, Block, BlockCopyWith<$R, Block, Block>> get blocks => - ListCopyWith( - $value.blocks, - (v, t) => v.copyWith.$chain(t), - (v) => call(blocks: v), - ); - @override - $R call({ - Object? blocks = $none, - Object? align = $none, - int? flex, - double? spacing, - String? type, - }) => $apply( - FieldCopyWithData({ - if (blocks != $none) #blocks: blocks, - if (align != $none) #align: align, - if (flex != null) #flex: flex, - if (spacing != null) #spacing: spacing, - if (type != null) #type: type, - }), - ); - @override - SectionBlock $make(CopyWithData data) => SectionBlock( - data.get(#blocks, or: $value.blocks), - align: data.get(#align, or: $value.align), - flex: data.get(#flex, or: $value.flex), - spacing: data.get(#spacing, or: $value.spacing), - type: data.get(#type, or: $value.type), - ); - - @override - SectionBlockCopyWith<$R2, SectionBlock, $Out2> $chain<$R2, $Out2>( - Then<$Out2, $R2> t, - ) => _SectionBlockCopyWithImpl<$R2, $Out2>($value, $cast, t); -} - -class ContentBlockMapper extends SubClassMapperBase { - ContentBlockMapper._(); - - static ContentBlockMapper? _instance; - static ContentBlockMapper ensureInitialized() { - if (_instance == null) { - MapperContainer.globals.use(_instance = ContentBlockMapper._()); - BlockMapper.ensureInitialized().addSubMapper(_instance!); - ContentAlignmentMapper.ensureInitialized(); - BlockInsetsMapper.ensureInitialized(); - } - return _instance!; - } - - @override - final String id = 'ContentBlock'; - - static String _$content(ContentBlock v) => v.content; - static const Field _f$content = Field( - 'content', - _$content, - ); - static ContentAlignment? _$align(ContentBlock v) => v.align; - static const Field _f$align = Field( - 'align', - _$align, - opt: true, - ); - static int _$flex(ContentBlock v) => v.flex; - static const Field _f$flex = Field( - 'flex', - _$flex, - opt: true, - def: 1, - ); - static BlockInsets? _$margin(ContentBlock v) => v.margin; - static const Field _f$margin = Field( - 'margin', - _$margin, - opt: true, - ); - static BlockInsets? _$padding(ContentBlock v) => v.padding; - static const Field _f$padding = Field( - 'padding', - _$padding, - opt: true, - ); - static bool _$scrollable(ContentBlock v) => v.scrollable; - static const Field _f$scrollable = Field( - 'scrollable', - _$scrollable, - opt: true, - def: false, - ); - static String _$type(ContentBlock v) => v.type; - static const Field _f$type = Field( - 'type', - _$type, - mode: FieldMode.member, - ); - - @override - final MappableFields fields = const { - #content: _f$content, - #align: _f$align, - #flex: _f$flex, - #margin: _f$margin, - #padding: _f$padding, - #scrollable: _f$scrollable, - #type: _f$type, - }; - @override - final bool ignoreNull = true; - - @override - final String discriminatorKey = 'type'; - @override - final dynamic discriminatorValue = ContentBlock.key; - @override - late final ClassMapperBase superMapper = BlockMapper.ensureInitialized(); - - static ContentBlock _instantiate(DecodingData data) { - return ContentBlock( - data.dec(_f$content), - align: data.dec(_f$align), - flex: data.dec(_f$flex), - margin: data.dec(_f$margin), - padding: data.dec(_f$padding), - scrollable: data.dec(_f$scrollable), - ); - } - - @override - final Function instantiate = _instantiate; - - static ContentBlock fromMap(Map map) { - return ensureInitialized().decodeMap(map); - } - - static ContentBlock fromJson(String json) { - return ensureInitialized().decodeJson(json); - } -} - -mixin ContentBlockMappable { - String toJson() { - return ContentBlockMapper.ensureInitialized().encodeJson( - this as ContentBlock, - ); - } - - Map toMap() { - return ContentBlockMapper.ensureInitialized().encodeMap( - this as ContentBlock, - ); - } - - ContentBlockCopyWith get copyWith => - _ContentBlockCopyWithImpl( - this as ContentBlock, - $identity, - $identity, - ); - @override - String toString() { - return ContentBlockMapper.ensureInitialized().stringifyValue( - this as ContentBlock, - ); - } - - @override - bool operator ==(Object other) { - return ContentBlockMapper.ensureInitialized().equalsValue( - this as ContentBlock, - other, - ); - } - - @override - int get hashCode { - return ContentBlockMapper.ensureInitialized().hashValue( - this as ContentBlock, - ); - } -} - -extension ContentBlockValueCopy<$R, $Out> - on ObjectCopyWith<$R, ContentBlock, $Out> { - ContentBlockCopyWith<$R, ContentBlock, $Out> get $asContentBlock => - $base.as((v, t, t2) => _ContentBlockCopyWithImpl<$R, $Out>(v, t, t2)); -} - -abstract class ContentBlockCopyWith<$R, $In extends ContentBlock, $Out> - implements BlockCopyWith<$R, $In, $Out> { - @override - BlockInsetsCopyWith<$R, BlockInsets, BlockInsets>? get margin; - @override - BlockInsetsCopyWith<$R, BlockInsets, BlockInsets>? get padding; - @override - $R call({ - String? content, - ContentAlignment? align, - int? flex, - BlockInsets? margin, - BlockInsets? padding, - bool? scrollable, - }); - ContentBlockCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>(Then<$Out2, $R2> t); -} - -class _ContentBlockCopyWithImpl<$R, $Out> - extends ClassCopyWithBase<$R, ContentBlock, $Out> - implements ContentBlockCopyWith<$R, ContentBlock, $Out> { - _ContentBlockCopyWithImpl(super.value, super.then, super.then2); - - @override - late final ClassMapperBase $mapper = - ContentBlockMapper.ensureInitialized(); - @override - BlockInsetsCopyWith<$R, BlockInsets, BlockInsets>? get margin => - $value.margin?.copyWith.$chain((v) => call(margin: v)); - @override - BlockInsetsCopyWith<$R, BlockInsets, BlockInsets>? get padding => - $value.padding?.copyWith.$chain((v) => call(padding: v)); - @override - $R call({ - Object? content = $none, - Object? align = $none, - int? flex, - Object? margin = $none, - Object? padding = $none, - bool? scrollable, - }) => $apply( - FieldCopyWithData({ - if (content != $none) #content: content, - if (align != $none) #align: align, - if (flex != null) #flex: flex, - if (margin != $none) #margin: margin, - if (padding != $none) #padding: padding, - if (scrollable != null) #scrollable: scrollable, - }), - ); - @override - ContentBlock $make(CopyWithData data) => ContentBlock( - data.get(#content, or: $value.content), - align: data.get(#align, or: $value.align), - flex: data.get(#flex, or: $value.flex), - margin: data.get(#margin, or: $value.margin), - padding: data.get(#padding, or: $value.padding), - scrollable: data.get(#scrollable, or: $value.scrollable), - ); - - @override - ContentBlockCopyWith<$R2, ContentBlock, $Out2> $chain<$R2, $Out2>( - Then<$Out2, $R2> t, - ) => _ContentBlockCopyWithImpl<$R2, $Out2>($value, $cast, t); -} - -class WidgetBlockMapper extends SubClassMapperBase { - WidgetBlockMapper._(); - - static WidgetBlockMapper? _instance; - static WidgetBlockMapper ensureInitialized() { - if (_instance == null) { - MapperContainer.globals.use(_instance = WidgetBlockMapper._()); - BlockMapper.ensureInitialized().addSubMapper(_instance!); - ContentAlignmentMapper.ensureInitialized(); - BlockInsetsMapper.ensureInitialized(); - } - return _instance!; - } - - @override - final String id = 'WidgetBlock'; - - static String _$name(WidgetBlock v) => v.name; - static const Field _f$name = Field('name', _$name); - static Map _$args(WidgetBlock v) => v.args; - static const Field> _f$args = Field( - 'args', - _$args, - opt: true, - ); - static ContentAlignment? _$align(WidgetBlock v) => v.align; - static const Field _f$align = Field( - 'align', - _$align, - opt: true, - ); - static int _$flex(WidgetBlock v) => v.flex; - static const Field _f$flex = Field( - 'flex', - _$flex, - opt: true, - def: 1, - ); - static BlockInsets? _$margin(WidgetBlock v) => v.margin; - static const Field _f$margin = Field( - 'margin', - _$margin, - opt: true, - ); - static BlockInsets? _$padding(WidgetBlock v) => v.padding; - static const Field _f$padding = Field( - 'padding', - _$padding, - opt: true, - ); - static bool _$scrollable(WidgetBlock v) => v.scrollable; - static const Field _f$scrollable = Field( - 'scrollable', - _$scrollable, - opt: true, - def: false, - ); - static String _$type(WidgetBlock v) => v.type; - static const Field _f$type = Field( - 'type', - _$type, - mode: FieldMode.member, - ); - - @override - final MappableFields fields = const { - #name: _f$name, - #args: _f$args, - #align: _f$align, - #flex: _f$flex, - #margin: _f$margin, - #padding: _f$padding, - #scrollable: _f$scrollable, - #type: _f$type, - }; - @override - final bool ignoreNull = true; - - @override - final String discriminatorKey = 'type'; - @override - final dynamic discriminatorValue = WidgetBlock.key; - @override - late final ClassMapperBase superMapper = BlockMapper.ensureInitialized(); - - @override - final MappingHook hook = const UnmappedPropertiesHook('args'); - static WidgetBlock _instantiate(DecodingData data) { - return WidgetBlock( - name: data.dec(_f$name), - args: data.dec(_f$args), - align: data.dec(_f$align), - flex: data.dec(_f$flex), - margin: data.dec(_f$margin), - padding: data.dec(_f$padding), - scrollable: data.dec(_f$scrollable), - ); - } - - @override - final Function instantiate = _instantiate; - - static WidgetBlock fromMap(Map map) { - return ensureInitialized().decodeMap(map); - } - - static WidgetBlock fromJson(String json) { - return ensureInitialized().decodeJson(json); - } -} - -mixin WidgetBlockMappable { - String toJson() { - return WidgetBlockMapper.ensureInitialized().encodeJson( - this as WidgetBlock, - ); - } - - Map toMap() { - return WidgetBlockMapper.ensureInitialized().encodeMap( - this as WidgetBlock, - ); - } - - WidgetBlockCopyWith get copyWith => - _WidgetBlockCopyWithImpl( - this as WidgetBlock, - $identity, - $identity, - ); - @override - String toString() { - return WidgetBlockMapper.ensureInitialized().stringifyValue( - this as WidgetBlock, - ); - } - - @override - bool operator ==(Object other) { - return WidgetBlockMapper.ensureInitialized().equalsValue( - this as WidgetBlock, - other, - ); - } - - @override - int get hashCode { - return WidgetBlockMapper.ensureInitialized().hashValue(this as WidgetBlock); - } -} - -extension WidgetBlockValueCopy<$R, $Out> - on ObjectCopyWith<$R, WidgetBlock, $Out> { - WidgetBlockCopyWith<$R, WidgetBlock, $Out> get $asWidgetBlock => - $base.as((v, t, t2) => _WidgetBlockCopyWithImpl<$R, $Out>(v, t, t2)); -} - -abstract class WidgetBlockCopyWith<$R, $In extends WidgetBlock, $Out> - implements BlockCopyWith<$R, $In, $Out> { - MapCopyWith<$R, String, Object?, ObjectCopyWith<$R, Object?, Object?>?> - get args; - @override - BlockInsetsCopyWith<$R, BlockInsets, BlockInsets>? get margin; - @override - BlockInsetsCopyWith<$R, BlockInsets, BlockInsets>? get padding; - @override - $R call({ - String? name, - Map? args, - ContentAlignment? align, - int? flex, - BlockInsets? margin, - BlockInsets? padding, - bool? scrollable, - }); - WidgetBlockCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>(Then<$Out2, $R2> t); -} - -class _WidgetBlockCopyWithImpl<$R, $Out> - extends ClassCopyWithBase<$R, WidgetBlock, $Out> - implements WidgetBlockCopyWith<$R, WidgetBlock, $Out> { - _WidgetBlockCopyWithImpl(super.value, super.then, super.then2); - - @override - late final ClassMapperBase $mapper = - WidgetBlockMapper.ensureInitialized(); - @override - MapCopyWith<$R, String, Object?, ObjectCopyWith<$R, Object?, Object?>?> - get args => MapCopyWith( - $value.args, - (v, t) => ObjectCopyWith(v, $identity, t), - (v) => call(args: v), - ); - @override - BlockInsetsCopyWith<$R, BlockInsets, BlockInsets>? get margin => - $value.margin?.copyWith.$chain((v) => call(margin: v)); - @override - BlockInsetsCopyWith<$R, BlockInsets, BlockInsets>? get padding => - $value.padding?.copyWith.$chain((v) => call(padding: v)); - @override - $R call({ - String? name, - Object? args = $none, - Object? align = $none, - int? flex, - Object? margin = $none, - Object? padding = $none, - bool? scrollable, - }) => $apply( - FieldCopyWithData({ - if (name != null) #name: name, - if (args != $none) #args: args, - if (align != $none) #align: align, - if (flex != null) #flex: flex, - if (margin != $none) #margin: margin, - if (padding != $none) #padding: padding, - if (scrollable != null) #scrollable: scrollable, - }), - ); - @override - WidgetBlock $make(CopyWithData data) => WidgetBlock( - name: data.get(#name, or: $value.name), - args: data.get(#args, or: $value.args), - align: data.get(#align, or: $value.align), - flex: data.get(#flex, or: $value.flex), - margin: data.get(#margin, or: $value.margin), - padding: data.get(#padding, or: $value.padding), - scrollable: data.get(#scrollable, or: $value.scrollable), - ); - - @override - WidgetBlockCopyWith<$R2, WidgetBlock, $Out2> $chain<$R2, $Out2>( - Then<$Out2, $R2> t, - ) => _WidgetBlockCopyWithImpl<$R2, $Out2>($value, $cast, t); -} - diff --git a/packages/core/lib/src/deck/deck_build_status.ack.dart b/packages/core/lib/src/deck/deck_build_status.ack.dart new file mode 100644 index 000000000..039020a55 --- /dev/null +++ b/packages/core/lib/src/deck/deck_build_status.ack.dart @@ -0,0 +1,230 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// dart format width=80 + +part of 'deck_build_status.dart'; + +// ************************************************************************** +// AckModelGenerator +// ************************************************************************** + +final _deckBuildErrorObject = Ack.object({ + 'message': Ack.string(), +}, additionalProperties: true); + +final _deckBuildErrorWireSchema = Ack.preserveBoundary(_deckBuildErrorObject); + +final _deckBuildErrorSchema = _deckBuildErrorObject.codec( + decode: _$DeckBuildErrorFromRuntime, + encode: _$DeckBuildErrorToRuntime, +); + +abstract final class DeckBuildErrorSchema { + static AckSchema, DeckBuildError> get schema => + _deckBuildErrorSchema; + + static AckSchema, Map> get wireSchema => + _deckBuildErrorWireSchema; + + static DeckBuildError parse(Object? value, {String? debugName}) => + _deckBuildErrorSchema.parse(value, debugName: debugName)!; + + static SchemaResult safeParse( + Object? value, { + String? debugName, + }) => _deckBuildErrorSchema.safeParse(value, debugName: debugName); + + static DeckBuildError fromJson(Map json) => parse(json); + + static Map encode( + DeckBuildError value, { + String? debugName, + }) => _deckBuildErrorSchema.encode(value, debugName: debugName)!; + + static SchemaResult> safeEncode( + DeckBuildError value, { + String? debugName, + }) => _deckBuildErrorSchema.safeEncode(value, debugName: debugName); + + static Map toJsonSchema() => + _deckBuildErrorSchema.toJsonSchema(); + + static AckSchemaModel toSchemaModel() => + AckSchemaModelExtension(_deckBuildErrorSchema).toSchemaModel(); +} + +DeckBuildError _$DeckBuildErrorFromRuntime(Map value) => + _$DeckBuildErrorFromJson(Map.from(value)); + +Map _$DeckBuildErrorToRuntime(DeckBuildError model) => + {..._$DeckBuildErrorToJson(model)}; + +mixin _$DeckBuildErrorAck { + DeckBuildError copyWith({String? message}) { + final self = this as DeckBuildError; + return DeckBuildError(message: message ?? self.message); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! DeckBuildError || runtimeType != other.runtimeType) { + return false; + } + final self = this as DeckBuildError; + return deepEquals(self.message, other.message); + } + + @override + int get hashCode { + final self = this as DeckBuildError; + return Object.hashAll([runtimeType, deepHashCode(self.message)]); + } + + @override + String toString() { + final self = this as DeckBuildError; + return 'DeckBuildError(message: ${self.message})'; + } + + Map toJson() => Map.from( + DeckBuildErrorSchema.encode(this as DeckBuildError), + ); + + SchemaResult> safeToJson() => + DeckBuildErrorSchema.safeEncode(this as DeckBuildError); +} + +String _ackDeckBuildErrorFromRuntimeMessage(Object? value) => value as String; +Object? _ackDeckBuildErrorToRuntimeMessage(String value) => value; + +final _deckBuildStatusObject = Ack.object({ + 'status': Ack.enumValues(DeckBuildPhase.values), + 'timestamp': Ack.datetime(), + 'slideCount': Ack.integer().optional().nullable(), + 'error': DeckBuildErrorSchema.schema.optional().nullable(), +}, additionalProperties: true); + +final _deckBuildStatusWireSchema = Ack.preserveBoundary(_deckBuildStatusObject); + +final _deckBuildStatusSchema = _deckBuildStatusObject.codec( + decode: _$DeckBuildStatusFromRuntime, + encode: _$DeckBuildStatusToRuntime, +); + +abstract final class DeckBuildStatusSchema { + static AckSchema, DeckBuildStatus> get schema => + _deckBuildStatusSchema; + + static AckSchema, Map> get wireSchema => + _deckBuildStatusWireSchema; + + static DeckBuildStatus parse(Object? value, {String? debugName}) => + _deckBuildStatusSchema.parse(value, debugName: debugName)!; + + static SchemaResult safeParse( + Object? value, { + String? debugName, + }) => _deckBuildStatusSchema.safeParse(value, debugName: debugName); + + static DeckBuildStatus fromJson(Map json) => parse(json); + + static Map encode( + DeckBuildStatus value, { + String? debugName, + }) => _deckBuildStatusSchema.encode(value, debugName: debugName)!; + + static SchemaResult> safeEncode( + DeckBuildStatus value, { + String? debugName, + }) => _deckBuildStatusSchema.safeEncode(value, debugName: debugName); + + static Map toJsonSchema() => + _deckBuildStatusSchema.toJsonSchema(); + + static AckSchemaModel toSchemaModel() => + AckSchemaModelExtension(_deckBuildStatusSchema).toSchemaModel(); +} + +DeckBuildStatus _$DeckBuildStatusFromRuntime(Map value) => + _$DeckBuildStatusFromJson(Map.from(value)); + +Map _$DeckBuildStatusToRuntime(DeckBuildStatus model) => + {..._$DeckBuildStatusToJson(model)}; + +final class _DeckBuildStatusCopyWithUnset { + const _DeckBuildStatusCopyWithUnset(); +} + +mixin _$DeckBuildStatusAck { + static const _DeckBuildStatusCopyWithUnset _ackCopyWithUnset = + _DeckBuildStatusCopyWithUnset(); + + DeckBuildStatus copyWith({ + DeckBuildPhase? phase, + DateTime? timestamp, + Object? slideCount = _ackCopyWithUnset, + Object? error = _ackCopyWithUnset, + }) { + final self = this as DeckBuildStatus; + return DeckBuildStatus( + phase: phase ?? self.phase, + timestamp: timestamp ?? self.timestamp, + slideCount: identical(slideCount, _ackCopyWithUnset) + ? self.slideCount + : slideCount as int?, + error: identical(error, _ackCopyWithUnset) + ? self.error + : error as DeckBuildError?, + ); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! DeckBuildStatus || runtimeType != other.runtimeType) { + return false; + } + final self = this as DeckBuildStatus; + return deepEquals(self.phase, other.phase) && + deepEquals(self.timestamp, other.timestamp) && + deepEquals(self.slideCount, other.slideCount) && + deepEquals(self.error, other.error); + } + + @override + int get hashCode { + final self = this as DeckBuildStatus; + return Object.hashAll([ + runtimeType, + deepHashCode(self.phase), + deepHashCode(self.timestamp), + deepHashCode(self.slideCount), + deepHashCode(self.error), + ]); + } + + @override + String toString() { + final self = this as DeckBuildStatus; + return 'DeckBuildStatus(phase: ${self.phase}, timestamp: ${self.timestamp}, slideCount: ${self.slideCount}, error: ${self.error})'; + } + + Map toJson() => Map.from( + DeckBuildStatusSchema.encode(this as DeckBuildStatus), + ); + + SchemaResult> safeToJson() => + DeckBuildStatusSchema.safeEncode(this as DeckBuildStatus); +} + +DeckBuildPhase _ackDeckBuildStatusFromRuntimePhase(Object? value) => + value as DeckBuildPhase; +Object? _ackDeckBuildStatusToRuntimePhase(DeckBuildPhase value) => value; +DateTime _ackDeckBuildStatusFromRuntimeTimestamp(Object? value) => + value as DateTime; +Object? _ackDeckBuildStatusToRuntimeTimestamp(DateTime value) => value; +int? _ackDeckBuildStatusFromRuntimeSlideCount(Object? value) => value as int?; +Object? _ackDeckBuildStatusToRuntimeSlideCount(int? value) => value; +DeckBuildError? _ackDeckBuildStatusFromRuntimeError(Object? value) => + value as DeckBuildError?; +Object? _ackDeckBuildStatusToRuntimeError(DeckBuildError? value) => value; diff --git a/packages/core/lib/src/deck/deck_build_status.ack.g.dart b/packages/core/lib/src/deck/deck_build_status.ack.g.dart new file mode 100644 index 000000000..14b420103 --- /dev/null +++ b/packages/core/lib/src/deck/deck_build_status.ack.g.dart @@ -0,0 +1,35 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// dart format width=80 + +part of 'deck_build_status.dart'; + +// ************************************************************************** +// AckJsonSerializableGenerator +// ************************************************************************** + +DeckBuildError _$DeckBuildErrorFromJson(Map json) => + DeckBuildError( + message: _ackDeckBuildErrorFromRuntimeMessage(json['message']), + ); + +Map _$DeckBuildErrorToJson(DeckBuildError instance) => + { + 'message': _ackDeckBuildErrorToRuntimeMessage(instance.message), + }; + +DeckBuildStatus _$DeckBuildStatusFromJson(Map json) => + DeckBuildStatus( + phase: _ackDeckBuildStatusFromRuntimePhase(json['status']), + timestamp: _ackDeckBuildStatusFromRuntimeTimestamp(json['timestamp']), + slideCount: _ackDeckBuildStatusFromRuntimeSlideCount(json['slideCount']), + error: _ackDeckBuildStatusFromRuntimeError(json['error']), + ); + +Map _$DeckBuildStatusToJson( + DeckBuildStatus instance, +) => { + 'status': _ackDeckBuildStatusToRuntimePhase(instance.phase), + 'timestamp': _ackDeckBuildStatusToRuntimeTimestamp(instance.timestamp), + 'slideCount': ?_ackDeckBuildStatusToRuntimeSlideCount(instance.slideCount), + 'error': ?_ackDeckBuildStatusToRuntimeError(instance.error), +}; diff --git a/packages/core/lib/src/deck/deck_build_status.dart b/packages/core/lib/src/deck/deck_build_status.dart index 5b835e327..dd0196ef7 100644 --- a/packages/core/lib/src/deck/deck_build_status.dart +++ b/packages/core/lib/src/deck/deck_build_status.dart @@ -1,51 +1,40 @@ -import 'package:dart_mappable/dart_mappable.dart'; +import 'package:ack/ack.dart'; +import 'package:ack_annotations/ack_annotations.dart'; -part 'deck_build_status.mapper.dart'; +part 'deck_build_status.ack.dart'; +part 'deck_build_status.ack.g.dart'; -@MappableEnum() enum DeckBuildPhase { unknown, building, success, failure } -@MappableClass() -final class DeckBuildError with DeckBuildErrorMappable { +@AckModel(unknownProperties: AckUnknownPropertyPolicy.discard) +final class DeckBuildError with _$DeckBuildErrorAck { final String message; const DeckBuildError({required this.message}); - static final fromMap = DeckBuildErrorMapper.fromMap; + static final fromJson = DeckBuildErrorSchema.fromJson; - static DeckBuildError? fromObject(Object? value) { - if (value is! Map) return null; - try { - return DeckBuildErrorMapper.fromMap(Map.from(value)); - } on Object { - return null; - } - } + static DeckBuildError? fromObject(Object? value) => + DeckBuildErrorSchema.safeParse(value).getOrNull(); } -@MappableClass() -final class DeckBuildStatus with DeckBuildStatusMappable { - @MappableField(key: 'status') +@AckModel(unknownProperties: AckUnknownPropertyPolicy.discard) +final class DeckBuildStatus with _$DeckBuildStatusAck { + @JsonKey(name: 'status') final DeckBuildPhase phase; final DateTime timestamp; final int? slideCount; final DeckBuildError? error; - const DeckBuildStatus({ + DeckBuildStatus({ required this.phase, - required this.timestamp, + required DateTime timestamp, this.slideCount, this.error, - }); - - static final fromMap = DeckBuildStatusMapper.fromMap; - - static DeckBuildStatus? fromObject(Object? value) { - if (value is! Map) return null; - try { - return DeckBuildStatusMapper.fromMap(Map.from(value)); - } on Object { - return null; - } - } + }) : timestamp = timestamp.toUtc(); + + static final fromJson = DeckBuildStatusSchema.fromJson; + + static DeckBuildStatus? fromObject(Object? value) => + DeckBuildStatusSchema.safeParse(value).getOrNull(); } diff --git a/packages/core/lib/src/deck/deck_build_status.mapper.dart b/packages/core/lib/src/deck/deck_build_status.mapper.dart deleted file mode 100644 index eb63a2461..000000000 --- a/packages/core/lib/src/deck/deck_build_status.mapper.dart +++ /dev/null @@ -1,350 +0,0 @@ -// coverage:ignore-file -// GENERATED CODE - DO NOT MODIFY BY HAND -// dart format off -// ignore_for_file: type=lint -// ignore_for_file: invalid_use_of_protected_member -// ignore_for_file: unused_element, unnecessary_cast, override_on_non_overriding_member -// ignore_for_file: strict_raw_type, inference_failure_on_untyped_parameter - -part of 'deck_build_status.dart'; - -class DeckBuildPhaseMapper extends EnumMapper { - DeckBuildPhaseMapper._(); - - static DeckBuildPhaseMapper? _instance; - static DeckBuildPhaseMapper ensureInitialized() { - if (_instance == null) { - MapperContainer.globals.use(_instance = DeckBuildPhaseMapper._()); - } - return _instance!; - } - - static DeckBuildPhase fromValue(dynamic value) { - ensureInitialized(); - return MapperContainer.globals.fromValue(value); - } - - @override - DeckBuildPhase decode(dynamic value) { - switch (value) { - case r'unknown': - return DeckBuildPhase.unknown; - case r'building': - return DeckBuildPhase.building; - case r'success': - return DeckBuildPhase.success; - case r'failure': - return DeckBuildPhase.failure; - default: - throw MapperException.unknownEnumValue(value); - } - } - - @override - dynamic encode(DeckBuildPhase self) { - switch (self) { - case DeckBuildPhase.unknown: - return r'unknown'; - case DeckBuildPhase.building: - return r'building'; - case DeckBuildPhase.success: - return r'success'; - case DeckBuildPhase.failure: - return r'failure'; - } - } -} - -extension DeckBuildPhaseMapperExtension on DeckBuildPhase { - String toValue() { - DeckBuildPhaseMapper.ensureInitialized(); - return MapperContainer.globals.toValue(this) as String; - } -} - -class DeckBuildErrorMapper extends ClassMapperBase { - DeckBuildErrorMapper._(); - - static DeckBuildErrorMapper? _instance; - static DeckBuildErrorMapper ensureInitialized() { - if (_instance == null) { - MapperContainer.globals.use(_instance = DeckBuildErrorMapper._()); - } - return _instance!; - } - - @override - final String id = 'DeckBuildError'; - - static String _$message(DeckBuildError v) => v.message; - static const Field _f$message = Field( - 'message', - _$message, - ); - - @override - final MappableFields fields = const {#message: _f$message}; - - static DeckBuildError _instantiate(DecodingData data) { - return DeckBuildError(message: data.dec(_f$message)); - } - - @override - final Function instantiate = _instantiate; - - static DeckBuildError fromMap(Map map) { - return ensureInitialized().decodeMap(map); - } - - static DeckBuildError fromJson(String json) { - return ensureInitialized().decodeJson(json); - } -} - -mixin DeckBuildErrorMappable { - String toJson() { - return DeckBuildErrorMapper.ensureInitialized().encodeJson( - this as DeckBuildError, - ); - } - - Map toMap() { - return DeckBuildErrorMapper.ensureInitialized().encodeMap( - this as DeckBuildError, - ); - } - - DeckBuildErrorCopyWith - get copyWith => _DeckBuildErrorCopyWithImpl( - this as DeckBuildError, - $identity, - $identity, - ); - @override - String toString() { - return DeckBuildErrorMapper.ensureInitialized().stringifyValue( - this as DeckBuildError, - ); - } - - @override - bool operator ==(Object other) { - return DeckBuildErrorMapper.ensureInitialized().equalsValue( - this as DeckBuildError, - other, - ); - } - - @override - int get hashCode { - return DeckBuildErrorMapper.ensureInitialized().hashValue( - this as DeckBuildError, - ); - } -} - -extension DeckBuildErrorValueCopy<$R, $Out> - on ObjectCopyWith<$R, DeckBuildError, $Out> { - DeckBuildErrorCopyWith<$R, DeckBuildError, $Out> get $asDeckBuildError => - $base.as((v, t, t2) => _DeckBuildErrorCopyWithImpl<$R, $Out>(v, t, t2)); -} - -abstract class DeckBuildErrorCopyWith<$R, $In extends DeckBuildError, $Out> - implements ClassCopyWith<$R, $In, $Out> { - $R call({String? message}); - DeckBuildErrorCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>( - Then<$Out2, $R2> t, - ); -} - -class _DeckBuildErrorCopyWithImpl<$R, $Out> - extends ClassCopyWithBase<$R, DeckBuildError, $Out> - implements DeckBuildErrorCopyWith<$R, DeckBuildError, $Out> { - _DeckBuildErrorCopyWithImpl(super.value, super.then, super.then2); - - @override - late final ClassMapperBase $mapper = - DeckBuildErrorMapper.ensureInitialized(); - @override - $R call({String? message}) => - $apply(FieldCopyWithData({if (message != null) #message: message})); - @override - DeckBuildError $make(CopyWithData data) => - DeckBuildError(message: data.get(#message, or: $value.message)); - - @override - DeckBuildErrorCopyWith<$R2, DeckBuildError, $Out2> $chain<$R2, $Out2>( - Then<$Out2, $R2> t, - ) => _DeckBuildErrorCopyWithImpl<$R2, $Out2>($value, $cast, t); -} - -class DeckBuildStatusMapper extends ClassMapperBase { - DeckBuildStatusMapper._(); - - static DeckBuildStatusMapper? _instance; - static DeckBuildStatusMapper ensureInitialized() { - if (_instance == null) { - MapperContainer.globals.use(_instance = DeckBuildStatusMapper._()); - DeckBuildPhaseMapper.ensureInitialized(); - DeckBuildErrorMapper.ensureInitialized(); - } - return _instance!; - } - - @override - final String id = 'DeckBuildStatus'; - - static DeckBuildPhase _$phase(DeckBuildStatus v) => v.phase; - static const Field _f$phase = Field( - 'phase', - _$phase, - key: r'status', - ); - static DateTime _$timestamp(DeckBuildStatus v) => v.timestamp; - static const Field _f$timestamp = Field( - 'timestamp', - _$timestamp, - ); - static int? _$slideCount(DeckBuildStatus v) => v.slideCount; - static const Field _f$slideCount = Field( - 'slideCount', - _$slideCount, - opt: true, - ); - static DeckBuildError? _$error(DeckBuildStatus v) => v.error; - static const Field _f$error = Field( - 'error', - _$error, - opt: true, - ); - - @override - final MappableFields fields = const { - #phase: _f$phase, - #timestamp: _f$timestamp, - #slideCount: _f$slideCount, - #error: _f$error, - }; - - static DeckBuildStatus _instantiate(DecodingData data) { - return DeckBuildStatus( - phase: data.dec(_f$phase), - timestamp: data.dec(_f$timestamp), - slideCount: data.dec(_f$slideCount), - error: data.dec(_f$error), - ); - } - - @override - final Function instantiate = _instantiate; - - static DeckBuildStatus fromMap(Map map) { - return ensureInitialized().decodeMap(map); - } - - static DeckBuildStatus fromJson(String json) { - return ensureInitialized().decodeJson(json); - } -} - -mixin DeckBuildStatusMappable { - String toJson() { - return DeckBuildStatusMapper.ensureInitialized() - .encodeJson(this as DeckBuildStatus); - } - - Map toMap() { - return DeckBuildStatusMapper.ensureInitialized().encodeMap( - this as DeckBuildStatus, - ); - } - - DeckBuildStatusCopyWith - get copyWith => - _DeckBuildStatusCopyWithImpl( - this as DeckBuildStatus, - $identity, - $identity, - ); - @override - String toString() { - return DeckBuildStatusMapper.ensureInitialized().stringifyValue( - this as DeckBuildStatus, - ); - } - - @override - bool operator ==(Object other) { - return DeckBuildStatusMapper.ensureInitialized().equalsValue( - this as DeckBuildStatus, - other, - ); - } - - @override - int get hashCode { - return DeckBuildStatusMapper.ensureInitialized().hashValue( - this as DeckBuildStatus, - ); - } -} - -extension DeckBuildStatusValueCopy<$R, $Out> - on ObjectCopyWith<$R, DeckBuildStatus, $Out> { - DeckBuildStatusCopyWith<$R, DeckBuildStatus, $Out> get $asDeckBuildStatus => - $base.as((v, t, t2) => _DeckBuildStatusCopyWithImpl<$R, $Out>(v, t, t2)); -} - -abstract class DeckBuildStatusCopyWith<$R, $In extends DeckBuildStatus, $Out> - implements ClassCopyWith<$R, $In, $Out> { - DeckBuildErrorCopyWith<$R, DeckBuildError, DeckBuildError>? get error; - $R call({ - DeckBuildPhase? phase, - DateTime? timestamp, - int? slideCount, - DeckBuildError? error, - }); - DeckBuildStatusCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>( - Then<$Out2, $R2> t, - ); -} - -class _DeckBuildStatusCopyWithImpl<$R, $Out> - extends ClassCopyWithBase<$R, DeckBuildStatus, $Out> - implements DeckBuildStatusCopyWith<$R, DeckBuildStatus, $Out> { - _DeckBuildStatusCopyWithImpl(super.value, super.then, super.then2); - - @override - late final ClassMapperBase $mapper = - DeckBuildStatusMapper.ensureInitialized(); - @override - DeckBuildErrorCopyWith<$R, DeckBuildError, DeckBuildError>? get error => - $value.error?.copyWith.$chain((v) => call(error: v)); - @override - $R call({ - DeckBuildPhase? phase, - DateTime? timestamp, - Object? slideCount = $none, - Object? error = $none, - }) => $apply( - FieldCopyWithData({ - if (phase != null) #phase: phase, - if (timestamp != null) #timestamp: timestamp, - if (slideCount != $none) #slideCount: slideCount, - if (error != $none) #error: error, - }), - ); - @override - DeckBuildStatus $make(CopyWithData data) => DeckBuildStatus( - phase: data.get(#phase, or: $value.phase), - timestamp: data.get(#timestamp, or: $value.timestamp), - slideCount: data.get(#slideCount, or: $value.slideCount), - error: data.get(#error, or: $value.error), - ); - - @override - DeckBuildStatusCopyWith<$R2, DeckBuildStatus, $Out2> $chain<$R2, $Out2>( - Then<$Out2, $R2> t, - ) => _DeckBuildStatusCopyWithImpl<$R2, $Out2>($value, $cast, t); -} - diff --git a/packages/core/lib/src/deck/deck_build_store.dart b/packages/core/lib/src/deck/deck_build_store.dart index e5fc3b7a1..81576b669 100644 --- a/packages/core/lib/src/deck/deck_build_store.dart +++ b/packages/core/lib/src/deck/deck_build_store.dart @@ -26,7 +26,7 @@ class DeckBuildStore { DeckBuildStatus( phase: DeckBuildPhase.unknown, timestamp: DateTime.now(), - ).toMap(), + ).toJson(), ), ); await workspace.slidesFile.ensureExists(content: ''); @@ -38,7 +38,7 @@ class DeckBuildStore { Future saveReferences(List slides) async { final deckJson = prettyJson( - slides.map((slide) => slide.toMap()).toList(growable: false), + slides.map((slide) => slide.toJson()).toList(growable: false), ); await workspace.deckJson.writeAsString(deckJson); @@ -64,7 +64,7 @@ class DeckBuildStore { : null, ); - await workspace.buildStatusJson.ensureWrite(prettyJson(status.toMap())); + await workspace.buildStatusJson.ensureWrite(prettyJson(status.toJson())); } Future _saveFullDeckReference(List slides) async { @@ -73,7 +73,7 @@ class DeckBuildStore { ); final slidesWithMarkdownJson = slides.map((slide) { - final slideMap = slide.toMap(); + final slideMap = slide.toJson(); final sections = slideMap['sections'] as List; final processedSections = sections.map((section) { diff --git a/packages/core/lib/src/deck/deck_workspace.ack.dart b/packages/core/lib/src/deck/deck_workspace.ack.dart new file mode 100644 index 000000000..8b970fa85 --- /dev/null +++ b/packages/core/lib/src/deck/deck_workspace.ack.dart @@ -0,0 +1,135 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// dart format width=80 + +part of 'deck_workspace.dart'; + +// ************************************************************************** +// AckModelGenerator +// ************************************************************************** + +final _deckWorkspaceObject = Ack.object({ + 'projectDir': Ack.string().optional(), + 'slidesPath': _safeWorkspacePathSchema().optional(), + 'outputDir': _safeWorkspacePathSchema().optional(), +}); + +final _deckWorkspaceWireSchema = Ack.preserveBoundary(_deckWorkspaceObject); + +final _deckWorkspaceSchema = _deckWorkspaceObject.codec( + decode: _$DeckWorkspaceFromRuntime, + encode: _$DeckWorkspaceToRuntime, +); + +abstract final class DeckWorkspaceSchema { + static AckSchema, DeckWorkspace> get schema => + _deckWorkspaceSchema; + + static AckSchema, Map> get wireSchema => + _deckWorkspaceWireSchema; + + static DeckWorkspace parse(Object? value, {String? debugName}) => + _deckWorkspaceSchema.parse(value, debugName: debugName)!; + + static SchemaResult safeParse( + Object? value, { + String? debugName, + }) => _deckWorkspaceSchema.safeParse(value, debugName: debugName); + + static DeckWorkspace fromJson(Map json) => parse(json); + + static Map encode( + DeckWorkspace value, { + String? debugName, + }) => _deckWorkspaceSchema.encode(value, debugName: debugName)!; + + static SchemaResult> safeEncode( + DeckWorkspace value, { + String? debugName, + }) => _deckWorkspaceSchema.safeEncode(value, debugName: debugName); + + static Map toJsonSchema() => + _deckWorkspaceSchema.toJsonSchema(); + + static AckSchemaModel toSchemaModel() => + AckSchemaModelExtension(_deckWorkspaceSchema).toSchemaModel(); +} + +DeckWorkspace _$DeckWorkspaceFromRuntime(Map value) => + _$DeckWorkspaceFromJson(Map.from(value)); + +Map _$DeckWorkspaceToRuntime(DeckWorkspace model) => + {..._$DeckWorkspaceToJson(model)}; + +final class _DeckWorkspaceCopyWithUnset { + const _DeckWorkspaceCopyWithUnset(); +} + +mixin _$DeckWorkspaceAck { + static const _DeckWorkspaceCopyWithUnset _ackCopyWithUnset = + _DeckWorkspaceCopyWithUnset(); + + DeckWorkspace copyWith({ + Object? projectDir = _ackCopyWithUnset, + Object? slidesPath = _ackCopyWithUnset, + Object? outputDir = _ackCopyWithUnset, + }) { + final self = this as DeckWorkspace; + return DeckWorkspace( + projectDir: identical(projectDir, _ackCopyWithUnset) + ? self.projectDir + : projectDir as String?, + slidesPath: identical(slidesPath, _ackCopyWithUnset) + ? self.slidesPath + : slidesPath as String?, + outputDir: identical(outputDir, _ackCopyWithUnset) + ? self.outputDir + : outputDir as String?, + ); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! DeckWorkspace || runtimeType != other.runtimeType) { + return false; + } + final self = this as DeckWorkspace; + return deepEquals(self.projectDir, other.projectDir) && + deepEquals(self.slidesPath, other.slidesPath) && + deepEquals(self.outputDir, other.outputDir); + } + + @override + int get hashCode { + final self = this as DeckWorkspace; + return Object.hashAll([ + runtimeType, + deepHashCode(self.projectDir), + deepHashCode(self.slidesPath), + deepHashCode(self.outputDir), + ]); + } + + @override + String toString() { + final self = this as DeckWorkspace; + return 'DeckWorkspace(projectDir: ${self.projectDir}, slidesPath: ${self.slidesPath}, outputDir: ${self.outputDir})'; + } + + Map toJson() => Map.from( + DeckWorkspaceSchema.encode(this as DeckWorkspace), + ); + + SchemaResult> safeToJson() => + DeckWorkspaceSchema.safeEncode(this as DeckWorkspace); +} + +String? _ackDeckWorkspaceFromRuntimeProjectDir(Object? value) => + value as String?; +Object? _ackDeckWorkspaceToRuntimeProjectDir(String value) => value; +String? _ackDeckWorkspaceFromRuntimeSlidesPath(Object? value) => + value as String?; +Object? _ackDeckWorkspaceToRuntimeSlidesPath(String value) => value; +String? _ackDeckWorkspaceFromRuntimeOutputDir(Object? value) => + value as String?; +Object? _ackDeckWorkspaceToRuntimeOutputDir(String value) => value; diff --git a/packages/core/lib/src/deck/deck_workspace.ack.g.dart b/packages/core/lib/src/deck/deck_workspace.ack.g.dart new file mode 100644 index 000000000..1f5d68c06 --- /dev/null +++ b/packages/core/lib/src/deck/deck_workspace.ack.g.dart @@ -0,0 +1,22 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// dart format width=80 + +part of 'deck_workspace.dart'; + +// ************************************************************************** +// AckJsonSerializableGenerator +// ************************************************************************** + +DeckWorkspace _$DeckWorkspaceFromJson(Map json) => + DeckWorkspace( + projectDir: _ackDeckWorkspaceFromRuntimeProjectDir(json['projectDir']), + slidesPath: _ackDeckWorkspaceFromRuntimeSlidesPath(json['slidesPath']), + outputDir: _ackDeckWorkspaceFromRuntimeOutputDir(json['outputDir']), + ); + +Map _$DeckWorkspaceToJson(DeckWorkspace instance) => + { + 'projectDir': _ackDeckWorkspaceToRuntimeProjectDir(instance.projectDir), + 'slidesPath': _ackDeckWorkspaceToRuntimeSlidesPath(instance.slidesPath), + 'outputDir': _ackDeckWorkspaceToRuntimeOutputDir(instance.outputDir), + }; diff --git a/packages/core/lib/src/deck/deck_workspace.dart b/packages/core/lib/src/deck/deck_workspace.dart index 082c0ad3c..b481f7951 100644 --- a/packages/core/lib/src/deck/deck_workspace.dart +++ b/packages/core/lib/src/deck/deck_workspace.dart @@ -1,15 +1,34 @@ import 'dart:io'; import 'package:ack/ack.dart'; -import 'package:dart_mappable/dart_mappable.dart'; +import 'package:ack_annotations/ack_annotations.dart'; import 'package:path/path.dart' as p; -part 'deck_workspace.mapper.dart'; +part 'deck_workspace.ack.dart'; +part 'deck_workspace.ack.g.dart'; + +StringSchema _safeWorkspacePathSchema() => Ack.string().refine( + _isRelativeWithoutTraversal, + message: + 'must be a relative path without ".." traversal segments' + ' (absolute paths and parent-directory traversal are not allowed)', +); + +/// Returns whether a relative path contains no parent-directory segments. +/// Filenames containing `..` (e.g. `my..file.md`) remain valid. +bool _isRelativeWithoutTraversal(String value) { + if (p.isAbsolute(value)) return false; + return !p.split(value).contains('..'); +} -@MappableClass() -final class DeckWorkspace with DeckWorkspaceMappable { +@AckModel() +final class DeckWorkspace with _$DeckWorkspaceAck { final String projectDir; + + @AckField(schema: _safeWorkspacePathSchema) final String slidesPath; + + @AckField(schema: _safeWorkspacePathSchema) final String outputDir; DeckWorkspace({String? projectDir, String? slidesPath, String? outputDir}) @@ -53,32 +72,10 @@ final class DeckWorkspace with DeckWorkspaceMappable { File get pubspecFile => File(p.join(projectDir, 'pubspec.yaml')); - static final fromMap = DeckWorkspaceMapper.fromMap; + static final fromJson = DeckWorkspaceSchema.fromJson; static DeckWorkspace parse(Map map) => - fromMap(Map.from(schema.parse(map)!)); - - static final _safePath = Ack.string().refine( - _isRelativeWithoutTraversal, - message: - 'must be a relative path without ".." traversal segments' - ' (absolute paths and parent-directory traversal are not allowed)', - ); - - static final schema = Ack.object({ - 'projectDir': Ack.string().optional(), - 'slidesPath': _safePath.optional(), - 'outputDir': _safePath.optional(), - }).passthrough(); - - /// Returns `true` when [value] is a relative path that does not contain - /// `..` as a path segment. Filenames that happen to contain `..` (e.g. - /// `my..file.md`) are allowed because `p.split` only yields `..` for an - /// actual traversal segment. - static bool _isRelativeWithoutTraversal(String value) { - if (p.isAbsolute(value)) return false; - return !p.split(value).contains('..'); - } + DeckWorkspaceSchema.parse(map); static String _normalizeBundledPath(String path) { final normalized = p.posix.normalize(path.replaceAll('\\', '/')); diff --git a/packages/core/lib/src/deck/deck_workspace.mapper.dart b/packages/core/lib/src/deck/deck_workspace.mapper.dart deleted file mode 100644 index 27eda8acd..000000000 --- a/packages/core/lib/src/deck/deck_workspace.mapper.dart +++ /dev/null @@ -1,157 +0,0 @@ -// coverage:ignore-file -// GENERATED CODE - DO NOT MODIFY BY HAND -// dart format off -// ignore_for_file: type=lint -// ignore_for_file: invalid_use_of_protected_member -// ignore_for_file: unused_element, unnecessary_cast, override_on_non_overriding_member -// ignore_for_file: strict_raw_type, inference_failure_on_untyped_parameter - -part of 'deck_workspace.dart'; - -class DeckWorkspaceMapper extends ClassMapperBase { - DeckWorkspaceMapper._(); - - static DeckWorkspaceMapper? _instance; - static DeckWorkspaceMapper ensureInitialized() { - if (_instance == null) { - MapperContainer.globals.use(_instance = DeckWorkspaceMapper._()); - } - return _instance!; - } - - @override - final String id = 'DeckWorkspace'; - - static String _$projectDir(DeckWorkspace v) => v.projectDir; - static const Field _f$projectDir = Field( - 'projectDir', - _$projectDir, - opt: true, - ); - static String _$slidesPath(DeckWorkspace v) => v.slidesPath; - static const Field _f$slidesPath = Field( - 'slidesPath', - _$slidesPath, - opt: true, - ); - static String _$outputDir(DeckWorkspace v) => v.outputDir; - static const Field _f$outputDir = Field( - 'outputDir', - _$outputDir, - opt: true, - ); - - @override - final MappableFields fields = const { - #projectDir: _f$projectDir, - #slidesPath: _f$slidesPath, - #outputDir: _f$outputDir, - }; - - static DeckWorkspace _instantiate(DecodingData data) { - return DeckWorkspace( - projectDir: data.dec(_f$projectDir), - slidesPath: data.dec(_f$slidesPath), - outputDir: data.dec(_f$outputDir), - ); - } - - @override - final Function instantiate = _instantiate; - - static DeckWorkspace fromMap(Map map) { - return ensureInitialized().decodeMap(map); - } - - static DeckWorkspace fromJson(String json) { - return ensureInitialized().decodeJson(json); - } -} - -mixin DeckWorkspaceMappable { - String toJson() { - return DeckWorkspaceMapper.ensureInitialized().encodeJson( - this as DeckWorkspace, - ); - } - - Map toMap() { - return DeckWorkspaceMapper.ensureInitialized().encodeMap( - this as DeckWorkspace, - ); - } - - DeckWorkspaceCopyWith - get copyWith => _DeckWorkspaceCopyWithImpl( - this as DeckWorkspace, - $identity, - $identity, - ); - @override - String toString() { - return DeckWorkspaceMapper.ensureInitialized().stringifyValue( - this as DeckWorkspace, - ); - } - - @override - bool operator ==(Object other) { - return DeckWorkspaceMapper.ensureInitialized().equalsValue( - this as DeckWorkspace, - other, - ); - } - - @override - int get hashCode { - return DeckWorkspaceMapper.ensureInitialized().hashValue( - this as DeckWorkspace, - ); - } -} - -extension DeckWorkspaceValueCopy<$R, $Out> - on ObjectCopyWith<$R, DeckWorkspace, $Out> { - DeckWorkspaceCopyWith<$R, DeckWorkspace, $Out> get $asDeckWorkspace => - $base.as((v, t, t2) => _DeckWorkspaceCopyWithImpl<$R, $Out>(v, t, t2)); -} - -abstract class DeckWorkspaceCopyWith<$R, $In extends DeckWorkspace, $Out> - implements ClassCopyWith<$R, $In, $Out> { - $R call({String? projectDir, String? slidesPath, String? outputDir}); - DeckWorkspaceCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>(Then<$Out2, $R2> t); -} - -class _DeckWorkspaceCopyWithImpl<$R, $Out> - extends ClassCopyWithBase<$R, DeckWorkspace, $Out> - implements DeckWorkspaceCopyWith<$R, DeckWorkspace, $Out> { - _DeckWorkspaceCopyWithImpl(super.value, super.then, super.then2); - - @override - late final ClassMapperBase $mapper = - DeckWorkspaceMapper.ensureInitialized(); - @override - $R call({ - Object? projectDir = $none, - Object? slidesPath = $none, - Object? outputDir = $none, - }) => $apply( - FieldCopyWithData({ - if (projectDir != $none) #projectDir: projectDir, - if (slidesPath != $none) #slidesPath: slidesPath, - if (outputDir != $none) #outputDir: outputDir, - }), - ); - @override - DeckWorkspace $make(CopyWithData data) => DeckWorkspace( - projectDir: data.get(#projectDir, or: $value.projectDir), - slidesPath: data.get(#slidesPath, or: $value.slidesPath), - outputDir: data.get(#outputDir, or: $value.outputDir), - ); - - @override - DeckWorkspaceCopyWith<$R2, DeckWorkspace, $Out2> $chain<$R2, $Out2>( - Then<$Out2, $R2> t, - ) => _DeckWorkspaceCopyWithImpl<$R2, $Out2>($value, $cast, t); -} - diff --git a/packages/core/lib/src/deck/slide_contract.dart b/packages/core/lib/src/deck/slide_contract.dart index b8b93081e..4539762ab 100644 --- a/packages/core/lib/src/deck/slide_contract.dart +++ b/packages/core/lib/src/deck/slide_contract.dart @@ -5,16 +5,11 @@ import 'block_model.dart'; import 'slide_model.dart'; /// Canonical top-level JSON contract for compiled slide payloads. -final slidesContractSchema = Ack.list(Slide.schema); +final slidesContractSchema = Ack.list(SlideSchema.schema); /// Parses a compiled slide payload from a raw JSON array. -List parseSlidesContract(Object? value) { - final validated = slidesContractSchema.parse(value)! as List; - return validated - .cast>() - .map((slide) => Slide.fromMap(Map.from(slide))) - .toList(growable: false); -} +List parseSlidesContract(Object? value) => + slidesContractSchema.parse(value)!; /// Flattened slide projection for structured-output AI generation. /// @@ -52,14 +47,14 @@ ObjectSchema buildAiSlideSchema({ }; final commonBlockProperties = >{ 'align': ContentAlignment.schema.optional().describe('Content alignment'), - 'flex': positiveFlexSchema.optional().describe( + 'flex': positiveFlexSchema().optional().describe( 'Flex weight for proportional sizing. Higher values take more space.', ), - 'margin': BlockInsets.schema.optional().describe( + 'margin': BlockInsetsSchema.wireSchema.optional().describe( 'Space inside the block frame but outside its decoration, as normalized ' 'physical edges', ), - 'padding': BlockInsets.schema.optional().describe( + 'padding': BlockInsetsSchema.wireSchema.optional().describe( 'Space between the block decoration and its content, as normalized ' 'physical edges', ), @@ -96,10 +91,10 @@ ObjectSchema buildAiSlideSchema({ 'align': ContentAlignment.schema.optional().describe( 'Content alignment within the section', ), - 'flex': positiveFlexSchema.optional().describe( + 'flex': positiveFlexSchema().optional().describe( 'Flex weight for proportional sizing. Higher values take more space.', ), - 'spacing': nonNegativeSpacingSchema.optional().describe( + 'spacing': nonNegativeSpacingSchema().optional().describe( 'Gap in logical pixels between sibling blocks', ), 'blocks': Ack.list( @@ -121,7 +116,7 @@ ObjectSchema buildAiSlideSchema({ ? generationOptionsSchema.describe( 'Required presentation metadata for generated slides', ) - : SlideOptions.schema.optional().describe('Slide options'), + : SlideOptionsSchema.wireSchema.optional().describe('Slide options'), 'comments': Ack.list( Ack.string().describe('A speaker note or talking point for this slide'), ).optional().describe('Speaker notes'), diff --git a/packages/core/lib/src/deck/slide_model.ack.dart b/packages/core/lib/src/deck/slide_model.ack.dart new file mode 100644 index 000000000..fa3be6669 --- /dev/null +++ b/packages/core/lib/src/deck/slide_model.ack.dart @@ -0,0 +1,293 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// dart format width=80 + +part of 'slide_model.dart'; + +// ************************************************************************** +// AckModelGenerator +// ************************************************************************** + +final _slideObject = Ack.object({ + 'key': Ack.string(), + 'options': SlideOptionsSchema.schema.optional().nullable(), + 'sections': Ack.list(SectionBlockSchema.schema).withDefault(const []), + 'comments': Ack.list(Ack.string()).withDefault(const []), +}); + +final _slideWireSchema = Ack.preserveBoundary(_slideObject); + +final _slideSchema = _slideObject.codec( + decode: _$SlideFromRuntime, + encode: _$SlideToRuntime, +); + +abstract final class SlideSchema { + static AckSchema, Slide> get schema => _slideSchema; + + static AckSchema, Map> get wireSchema => + _slideWireSchema; + + static Slide parse(Object? value, {String? debugName}) => + _slideSchema.parse(value, debugName: debugName)!; + + static SchemaResult safeParse(Object? value, {String? debugName}) => + _slideSchema.safeParse(value, debugName: debugName); + + static Slide fromJson(Map json) => parse(json); + + static Map encode(Slide value, {String? debugName}) => + _slideSchema.encode(value, debugName: debugName)!; + + static SchemaResult> safeEncode( + Slide value, { + String? debugName, + }) => _slideSchema.safeEncode(value, debugName: debugName); + + static Map toJsonSchema() => _slideSchema.toJsonSchema(); + + static AckSchemaModel toSchemaModel() => + AckSchemaModelExtension(_slideSchema).toSchemaModel(); +} + +Slide _$SlideFromRuntime(Map value) => + _$SlideFromJson(Map.from(value)); + +Map _$SlideToRuntime(Slide model) => { + ..._$SlideToJson(model), +}; + +final class _SlideCopyWithUnset { + const _SlideCopyWithUnset(); +} + +mixin _$SlideAck { + static const _SlideCopyWithUnset _ackCopyWithUnset = _SlideCopyWithUnset(); + + Slide copyWith({ + String? key, + Object? options = _ackCopyWithUnset, + List? sections, + List? comments, + }) { + final self = this as Slide; + return Slide( + key: key ?? self.key, + options: identical(options, _ackCopyWithUnset) + ? self.options + : options as SlideOptions?, + sections: sections ?? self.sections, + comments: comments ?? self.comments, + ); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! Slide || runtimeType != other.runtimeType) { + return false; + } + final self = this as Slide; + return deepEquals(self.key, other.key) && + deepEquals(self.options, other.options) && + deepEquals(self.sections, other.sections) && + deepEquals(self.comments, other.comments); + } + + @override + int get hashCode { + final self = this as Slide; + return Object.hashAll([ + runtimeType, + deepHashCode(self.key), + deepHashCode(self.options), + deepHashCode(self.sections), + deepHashCode(self.comments), + ]); + } + + @override + String toString() { + final self = this as Slide; + return 'Slide(key: ${self.key}, options: ${self.options}, sections: ${self.sections}, comments: ${self.comments})'; + } + + Map toJson() => + Map.from(SlideSchema.encode(this as Slide)); + + SchemaResult> safeToJson() => + SlideSchema.safeEncode(this as Slide); +} + +String _ackSlideFromRuntimeKey(Object? value) => value as String; +Object? _ackSlideToRuntimeKey(String value) => value; +SlideOptions? _ackSlideFromRuntimeOptions(Object? value) => + value as SlideOptions?; +Object? _ackSlideToRuntimeOptions(SlideOptions? value) => value; +List? _ackSlideFromRuntimeSections(Object? value) => value == null + ? null + : List.unmodifiable( + (value as List).map((item) => item as SectionBlock), + ); +Object? _ackSlideToRuntimeSections(List value) => + value.map((item) => item).toList(growable: false); +List? _ackSlideFromRuntimeComments(Object? value) => value == null + ? null + : List.unmodifiable((value as List).map((item) => item as String)); +Object? _ackSlideToRuntimeComments(List value) => + value.map((item) => item).toList(growable: false); + +final _slideOptionsObject = Ack.object({ + 'title': Ack.string().optional().nullable(), + 'style': Ack.string().optional().nullable(), + 'layout': Ack.enumValues(SlideLayout.values).optional().nullable(), + 'template': Ack.string().optional().nullable(), +}, additionalProperties: true); + +final _slideOptionsWireSchema = Ack.preserveBoundary(_slideOptionsObject); + +final _slideOptionsSchema = _slideOptionsObject.codec( + decode: _$SlideOptionsFromRuntime, + encode: _$SlideOptionsToRuntime, +); + +abstract final class SlideOptionsSchema { + static AckSchema, SlideOptions> get schema => + _slideOptionsSchema; + + static AckSchema, Map> get wireSchema => + _slideOptionsWireSchema; + + static SlideOptions parse(Object? value, {String? debugName}) => + _slideOptionsSchema.parse(value, debugName: debugName)!; + + static SchemaResult safeParse( + Object? value, { + String? debugName, + }) => _slideOptionsSchema.safeParse(value, debugName: debugName); + + static SlideOptions fromJson(Map json) => parse(json); + + static Map encode(SlideOptions value, {String? debugName}) => + _slideOptionsSchema.encode(value, debugName: debugName)!; + + static SchemaResult> safeEncode( + SlideOptions value, { + String? debugName, + }) => _slideOptionsSchema.safeEncode(value, debugName: debugName); + + static Map toJsonSchema() => + _slideOptionsSchema.toJsonSchema(); + + static AckSchemaModel toSchemaModel() => + AckSchemaModelExtension(_slideOptionsSchema).toSchemaModel(); +} + +SlideOptions _$SlideOptionsFromRuntime(Map value) { + const declared = {'title', 'style', 'layout', 'template'}; + return _$SlideOptionsFromJson({ + ...value, + 'args': Map.fromEntries( + value.entries.where((entry) => !declared.contains(entry.key)), + ), + }); +} + +Map _$SlideOptionsToRuntime(SlideOptions model) { + const declared = {'title', 'style', 'layout', 'template'}; + final result = {..._$SlideOptionsToJson(model)}; + result.remove('args'); + return { + for (final entry in model.args.entries) + if (!declared.contains(entry.key)) entry.key: entry.value, + ...result, + }; +} + +final class _SlideOptionsCopyWithUnset { + const _SlideOptionsCopyWithUnset(); +} + +mixin _$SlideOptionsAck { + static const _SlideOptionsCopyWithUnset _ackCopyWithUnset = + _SlideOptionsCopyWithUnset(); + + SlideOptions copyWith({ + Object? title = _ackCopyWithUnset, + Object? style = _ackCopyWithUnset, + Object? layout = _ackCopyWithUnset, + Object? template = _ackCopyWithUnset, + Map? args, + }) { + final self = this as SlideOptions; + return SlideOptions( + title: identical(title, _ackCopyWithUnset) + ? self.title + : title as String?, + style: identical(style, _ackCopyWithUnset) + ? self.style + : style as String?, + layout: identical(layout, _ackCopyWithUnset) + ? self.layout + : layout as SlideLayout?, + template: identical(template, _ackCopyWithUnset) + ? self.template + : template as String?, + args: args ?? self.args, + ); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! SlideOptions || runtimeType != other.runtimeType) { + return false; + } + final self = this as SlideOptions; + return deepEquals(self.title, other.title) && + deepEquals(self.style, other.style) && + deepEquals(self.layout, other.layout) && + deepEquals(self.template, other.template) && + deepEquals(self.args, other.args); + } + + @override + int get hashCode { + final self = this as SlideOptions; + return Object.hashAll([ + runtimeType, + deepHashCode(self.title), + deepHashCode(self.style), + deepHashCode(self.layout), + deepHashCode(self.template), + deepHashCode(self.args), + ]); + } + + @override + String toString() { + final self = this as SlideOptions; + return 'SlideOptions(title: ${self.title}, style: ${self.style}, layout: ${self.layout}, template: ${self.template}, args: ${self.args})'; + } + + Map toJson() => Map.from( + SlideOptionsSchema.encode(this as SlideOptions), + ); + + SchemaResult> safeToJson() => + SlideOptionsSchema.safeEncode(this as SlideOptions); +} + +String? _ackSlideOptionsFromRuntimeTitle(Object? value) => value as String?; +Object? _ackSlideOptionsToRuntimeTitle(String? value) => value; +String? _ackSlideOptionsFromRuntimeStyle(Object? value) => value as String?; +Object? _ackSlideOptionsToRuntimeStyle(String? value) => value; +SlideLayout? _ackSlideOptionsFromRuntimeLayout(Object? value) => + value as SlideLayout?; +Object? _ackSlideOptionsToRuntimeLayout(SlideLayout? value) => value; +String? _ackSlideOptionsFromRuntimeTemplate(Object? value) => value as String?; +Object? _ackSlideOptionsToRuntimeTemplate(String? value) => value; +Map? _ackSlideOptionsFromRuntimeArgs(Object? value) => + value == null + ? null + : deepUnmodifiableJsonMap(value as Map); +Object? _ackSlideOptionsToRuntimeArgs(Map value) => value; diff --git a/packages/core/lib/src/deck/slide_model.ack.g.dart b/packages/core/lib/src/deck/slide_model.ack.g.dart new file mode 100644 index 000000000..1b2514e2b --- /dev/null +++ b/packages/core/lib/src/deck/slide_model.ack.g.dart @@ -0,0 +1,39 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// dart format width=80 + +part of 'slide_model.dart'; + +// ************************************************************************** +// AckJsonSerializableGenerator +// ************************************************************************** + +Slide _$SlideFromJson(Map json) => Slide( + key: _ackSlideFromRuntimeKey(json['key']), + options: _ackSlideFromRuntimeOptions(json['options']), + sections: _ackSlideFromRuntimeSections(json['sections']) ?? const [], + comments: _ackSlideFromRuntimeComments(json['comments']) ?? const [], +); + +Map _$SlideToJson(Slide instance) => { + 'key': _ackSlideToRuntimeKey(instance.key), + 'options': ?_ackSlideToRuntimeOptions(instance.options), + 'sections': _ackSlideToRuntimeSections(instance.sections), + 'comments': _ackSlideToRuntimeComments(instance.comments), +}; + +SlideOptions _$SlideOptionsFromJson(Map json) => SlideOptions( + title: _ackSlideOptionsFromRuntimeTitle(json['title']), + style: _ackSlideOptionsFromRuntimeStyle(json['style']), + layout: _ackSlideOptionsFromRuntimeLayout(json['layout']), + template: _ackSlideOptionsFromRuntimeTemplate(json['template']), + args: _ackSlideOptionsFromRuntimeArgs(json['args']) ?? const {}, +); + +Map _$SlideOptionsToJson(SlideOptions instance) => + { + 'title': ?_ackSlideOptionsToRuntimeTitle(instance.title), + 'style': ?_ackSlideOptionsToRuntimeStyle(instance.style), + 'layout': ?_ackSlideOptionsToRuntimeLayout(instance.layout), + 'template': ?_ackSlideOptionsToRuntimeTemplate(instance.template), + 'args': _ackSlideOptionsToRuntimeArgs(instance.args), + }; diff --git a/packages/core/lib/src/deck/slide_model.dart b/packages/core/lib/src/deck/slide_model.dart index c2891a8f0..cbf4e8ad6 100644 --- a/packages/core/lib/src/deck/slide_model.dart +++ b/packages/core/lib/src/deck/slide_model.dart @@ -1,30 +1,17 @@ import 'package:ack/ack.dart'; -import 'package:dart_mappable/dart_mappable.dart'; +import 'package:ack_annotations/ack_annotations.dart'; import 'block_model.dart'; -part 'slide_model.mapper.dart'; - -final slideOptionsSchema = Ack.object({ - 'title': Ack.string().optional(), - 'style': Ack.string().optional(), - 'layout': SlideLayout.schema.optional(), - 'template': Ack.string().optional(), -}, additionalProperties: true); - -final slideSchema = Ack.object({ - 'key': Ack.string(), - 'options': slideOptionsSchema.optional(), - 'sections': Ack.list(sectionBlockSchema).optional(), - 'comments': Ack.list(Ack.string()).optional(), -}, additionalProperties: true); +part 'slide_model.ack.dart'; +part 'slide_model.ack.g.dart'; /// Represents a single slide in a presentation. /// /// A slide contains sections of content blocks, optional configuration options, /// and any speaker notes or comments. Each slide is uniquely identified by a key. -@MappableClass(ignoreNull: true) -class Slide with SlideMappable { +@AckModel() +final class Slide with _$SlideAck { /// Unique identifier for this slide, typically generated from content hash. final String key; @@ -43,15 +30,12 @@ class Slide with SlideMappable { }) : sections = List.unmodifiable(sections), comments = List.unmodifiable(comments); - static final fromMap = SlideMapper.fromMap; - - static final schema = slideSchema; + static final fromJson = SlideSchema.fromJson; /// Validates [map] against the schema and constructs a [Slide]. - static Slide parse(Map map) => fromMap(schema.parse(map)!); + static Slide parse(Map map) => SlideSchema.parse(map); } -@MappableEnum() enum SlideLayout { normal, fullscreen; @@ -64,8 +48,11 @@ enum SlideLayout { /// Configuration options for a slide. /// /// Provides metadata and styling information for individual slides. -@MappableClass(hook: UnmappedPropertiesHook('args'), ignoreNull: true) -class SlideOptions with SlideOptionsMappable { +@AckModel( + unknownProperties: AckUnknownPropertyPolicy.capture, + captureField: 'args', +) +final class SlideOptions with _$SlideOptionsAck { static const _knownFields = {'title', 'style', 'layout', 'template'}; final String? title; @@ -94,17 +81,15 @@ class SlideOptions with SlideOptionsMappable { this.layout, this.template, Map args = const {}, - }) : args = Map.unmodifiable( + }) : args = deepUnmodifiableJsonMap( Map.fromEntries( args.entries.where((e) => !_knownFields.contains(e.key)), ), ); - static final fromMap = SlideOptionsMapper.fromMap; - - static final schema = slideOptionsSchema; + static final fromJson = SlideOptionsSchema.fromJson; /// Validates [map] against the schema and constructs [SlideOptions]. static SlideOptions parse(Map map) => - fromMap(schema.parse(map)!); + SlideOptionsSchema.parse(map); } diff --git a/packages/core/lib/src/deck/slide_model.mapper.dart b/packages/core/lib/src/deck/slide_model.mapper.dart deleted file mode 100644 index 20ad328ac..000000000 --- a/packages/core/lib/src/deck/slide_model.mapper.dart +++ /dev/null @@ -1,421 +0,0 @@ -// coverage:ignore-file -// GENERATED CODE - DO NOT MODIFY BY HAND -// dart format off -// ignore_for_file: type=lint -// ignore_for_file: invalid_use_of_protected_member -// ignore_for_file: unused_element, unnecessary_cast, override_on_non_overriding_member -// ignore_for_file: strict_raw_type, inference_failure_on_untyped_parameter - -part of 'slide_model.dart'; - -class SlideLayoutMapper extends EnumMapper { - SlideLayoutMapper._(); - - static SlideLayoutMapper? _instance; - static SlideLayoutMapper ensureInitialized() { - if (_instance == null) { - MapperContainer.globals.use(_instance = SlideLayoutMapper._()); - } - return _instance!; - } - - static SlideLayout fromValue(dynamic value) { - ensureInitialized(); - return MapperContainer.globals.fromValue(value); - } - - @override - SlideLayout decode(dynamic value) { - switch (value) { - case r'normal': - return SlideLayout.normal; - case r'fullscreen': - return SlideLayout.fullscreen; - default: - throw MapperException.unknownEnumValue(value); - } - } - - @override - dynamic encode(SlideLayout self) { - switch (self) { - case SlideLayout.normal: - return r'normal'; - case SlideLayout.fullscreen: - return r'fullscreen'; - } - } -} - -extension SlideLayoutMapperExtension on SlideLayout { - String toValue() { - SlideLayoutMapper.ensureInitialized(); - return MapperContainer.globals.toValue(this) as String; - } -} - -class SlideMapper extends ClassMapperBase { - SlideMapper._(); - - static SlideMapper? _instance; - static SlideMapper ensureInitialized() { - if (_instance == null) { - MapperContainer.globals.use(_instance = SlideMapper._()); - SlideOptionsMapper.ensureInitialized(); - SectionBlockMapper.ensureInitialized(); - } - return _instance!; - } - - @override - final String id = 'Slide'; - - static String _$key(Slide v) => v.key; - static const Field _f$key = Field('key', _$key); - static SlideOptions? _$options(Slide v) => v.options; - static const Field _f$options = Field( - 'options', - _$options, - opt: true, - ); - static List _$sections(Slide v) => v.sections; - static const Field> _f$sections = Field( - 'sections', - _$sections, - opt: true, - def: const [], - ); - static List _$comments(Slide v) => v.comments; - static const Field> _f$comments = Field( - 'comments', - _$comments, - opt: true, - def: const [], - ); - - @override - final MappableFields fields = const { - #key: _f$key, - #options: _f$options, - #sections: _f$sections, - #comments: _f$comments, - }; - @override - final bool ignoreNull = true; - - static Slide _instantiate(DecodingData data) { - return Slide( - key: data.dec(_f$key), - options: data.dec(_f$options), - sections: data.dec(_f$sections), - comments: data.dec(_f$comments), - ); - } - - @override - final Function instantiate = _instantiate; - - static Slide fromMap(Map map) { - return ensureInitialized().decodeMap(map); - } - - static Slide fromJson(String json) { - return ensureInitialized().decodeJson(json); - } -} - -mixin SlideMappable { - String toJson() { - return SlideMapper.ensureInitialized().encodeJson(this as Slide); - } - - Map toMap() { - return SlideMapper.ensureInitialized().encodeMap(this as Slide); - } - - SlideCopyWith get copyWith => - _SlideCopyWithImpl(this as Slide, $identity, $identity); - @override - String toString() { - return SlideMapper.ensureInitialized().stringifyValue(this as Slide); - } - - @override - bool operator ==(Object other) { - return SlideMapper.ensureInitialized().equalsValue(this as Slide, other); - } - - @override - int get hashCode { - return SlideMapper.ensureInitialized().hashValue(this as Slide); - } -} - -extension SlideValueCopy<$R, $Out> on ObjectCopyWith<$R, Slide, $Out> { - SlideCopyWith<$R, Slide, $Out> get $asSlide => - $base.as((v, t, t2) => _SlideCopyWithImpl<$R, $Out>(v, t, t2)); -} - -abstract class SlideCopyWith<$R, $In extends Slide, $Out> - implements ClassCopyWith<$R, $In, $Out> { - SlideOptionsCopyWith<$R, SlideOptions, SlideOptions>? get options; - ListCopyWith< - $R, - SectionBlock, - SectionBlockCopyWith<$R, SectionBlock, SectionBlock> - > - get sections; - ListCopyWith<$R, String, ObjectCopyWith<$R, String, String>> get comments; - $R call({ - String? key, - SlideOptions? options, - List? sections, - List? comments, - }); - SlideCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>(Then<$Out2, $R2> t); -} - -class _SlideCopyWithImpl<$R, $Out> extends ClassCopyWithBase<$R, Slide, $Out> - implements SlideCopyWith<$R, Slide, $Out> { - _SlideCopyWithImpl(super.value, super.then, super.then2); - - @override - late final ClassMapperBase $mapper = SlideMapper.ensureInitialized(); - @override - SlideOptionsCopyWith<$R, SlideOptions, SlideOptions>? get options => - $value.options?.copyWith.$chain((v) => call(options: v)); - @override - ListCopyWith< - $R, - SectionBlock, - SectionBlockCopyWith<$R, SectionBlock, SectionBlock> - > - get sections => ListCopyWith( - $value.sections, - (v, t) => v.copyWith.$chain(t), - (v) => call(sections: v), - ); - @override - ListCopyWith<$R, String, ObjectCopyWith<$R, String, String>> get comments => - ListCopyWith( - $value.comments, - (v, t) => ObjectCopyWith(v, $identity, t), - (v) => call(comments: v), - ); - @override - $R call({ - String? key, - Object? options = $none, - List? sections, - List? comments, - }) => $apply( - FieldCopyWithData({ - if (key != null) #key: key, - if (options != $none) #options: options, - if (sections != null) #sections: sections, - if (comments != null) #comments: comments, - }), - ); - @override - Slide $make(CopyWithData data) => Slide( - key: data.get(#key, or: $value.key), - options: data.get(#options, or: $value.options), - sections: data.get(#sections, or: $value.sections), - comments: data.get(#comments, or: $value.comments), - ); - - @override - SlideCopyWith<$R2, Slide, $Out2> $chain<$R2, $Out2>(Then<$Out2, $R2> t) => - _SlideCopyWithImpl<$R2, $Out2>($value, $cast, t); -} - -class SlideOptionsMapper extends ClassMapperBase { - SlideOptionsMapper._(); - - static SlideOptionsMapper? _instance; - static SlideOptionsMapper ensureInitialized() { - if (_instance == null) { - MapperContainer.globals.use(_instance = SlideOptionsMapper._()); - SlideLayoutMapper.ensureInitialized(); - } - return _instance!; - } - - @override - final String id = 'SlideOptions'; - - static String? _$title(SlideOptions v) => v.title; - static const Field _f$title = Field( - 'title', - _$title, - opt: true, - ); - static String? _$style(SlideOptions v) => v.style; - static const Field _f$style = Field( - 'style', - _$style, - opt: true, - ); - static SlideLayout? _$layout(SlideOptions v) => v.layout; - static const Field _f$layout = Field( - 'layout', - _$layout, - opt: true, - ); - static String? _$template(SlideOptions v) => v.template; - static const Field _f$template = Field( - 'template', - _$template, - opt: true, - ); - static Map _$args(SlideOptions v) => v.args; - static const Field> _f$args = Field( - 'args', - _$args, - opt: true, - def: const {}, - ); - - @override - final MappableFields fields = const { - #title: _f$title, - #style: _f$style, - #layout: _f$layout, - #template: _f$template, - #args: _f$args, - }; - @override - final bool ignoreNull = true; - - @override - final MappingHook hook = const UnmappedPropertiesHook('args'); - static SlideOptions _instantiate(DecodingData data) { - return SlideOptions( - title: data.dec(_f$title), - style: data.dec(_f$style), - layout: data.dec(_f$layout), - template: data.dec(_f$template), - args: data.dec(_f$args), - ); - } - - @override - final Function instantiate = _instantiate; - - static SlideOptions fromMap(Map map) { - return ensureInitialized().decodeMap(map); - } - - static SlideOptions fromJson(String json) { - return ensureInitialized().decodeJson(json); - } -} - -mixin SlideOptionsMappable { - String toJson() { - return SlideOptionsMapper.ensureInitialized().encodeJson( - this as SlideOptions, - ); - } - - Map toMap() { - return SlideOptionsMapper.ensureInitialized().encodeMap( - this as SlideOptions, - ); - } - - SlideOptionsCopyWith get copyWith => - _SlideOptionsCopyWithImpl( - this as SlideOptions, - $identity, - $identity, - ); - @override - String toString() { - return SlideOptionsMapper.ensureInitialized().stringifyValue( - this as SlideOptions, - ); - } - - @override - bool operator ==(Object other) { - return SlideOptionsMapper.ensureInitialized().equalsValue( - this as SlideOptions, - other, - ); - } - - @override - int get hashCode { - return SlideOptionsMapper.ensureInitialized().hashValue( - this as SlideOptions, - ); - } -} - -extension SlideOptionsValueCopy<$R, $Out> - on ObjectCopyWith<$R, SlideOptions, $Out> { - SlideOptionsCopyWith<$R, SlideOptions, $Out> get $asSlideOptions => - $base.as((v, t, t2) => _SlideOptionsCopyWithImpl<$R, $Out>(v, t, t2)); -} - -abstract class SlideOptionsCopyWith<$R, $In extends SlideOptions, $Out> - implements ClassCopyWith<$R, $In, $Out> { - MapCopyWith<$R, String, Object?, ObjectCopyWith<$R, Object?, Object?>?> - get args; - $R call({ - String? title, - String? style, - SlideLayout? layout, - String? template, - Map? args, - }); - SlideOptionsCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>(Then<$Out2, $R2> t); -} - -class _SlideOptionsCopyWithImpl<$R, $Out> - extends ClassCopyWithBase<$R, SlideOptions, $Out> - implements SlideOptionsCopyWith<$R, SlideOptions, $Out> { - _SlideOptionsCopyWithImpl(super.value, super.then, super.then2); - - @override - late final ClassMapperBase $mapper = - SlideOptionsMapper.ensureInitialized(); - @override - MapCopyWith<$R, String, Object?, ObjectCopyWith<$R, Object?, Object?>?> - get args => MapCopyWith( - $value.args, - (v, t) => ObjectCopyWith(v, $identity, t), - (v) => call(args: v), - ); - @override - $R call({ - Object? title = $none, - Object? style = $none, - Object? layout = $none, - Object? template = $none, - Map? args, - }) => $apply( - FieldCopyWithData({ - if (title != $none) #title: title, - if (style != $none) #style: style, - if (layout != $none) #layout: layout, - if (template != $none) #template: template, - if (args != null) #args: args, - }), - ); - @override - SlideOptions $make(CopyWithData data) => SlideOptions( - title: data.get(#title, or: $value.title), - style: data.get(#style, or: $value.style), - layout: data.get(#layout, or: $value.layout), - template: data.get(#template, or: $value.template), - args: data.get(#args, or: $value.args), - ); - - @override - SlideOptionsCopyWith<$R2, SlideOptions, $Out2> $chain<$R2, $Out2>( - Then<$Out2, $R2> t, - ) => _SlideOptionsCopyWithImpl<$R2, $Out2>($value, $cast, t); -} - diff --git a/packages/core/pubspec.yaml b/packages/core/pubspec.yaml index 9b901b475..63dbf5e41 100644 --- a/packages/core/pubspec.yaml +++ b/packages/core/pubspec.yaml @@ -15,8 +15,8 @@ dependencies: yaml: ^3.1.2 collection: ^1.18.0 path: ^1.9.0 - ack: 1.0.1 - dart_mappable: ^4.7.0 + ack: ^1.2.0 + ack_annotations: ^1.2.0 markdown: ^7.3.0 logging: ^1.3.0 @@ -25,4 +25,4 @@ dev_dependencies: test: ^1.24.0 dart_code_metrics_presets: ^2.19.0 build_runner: ^2.5.4 - dart_mappable_builder: ^4.7.0 + ack_generator: ^1.2.0 diff --git a/packages/core/schema/superdeck.slides.schema.json b/packages/core/schema/superdeck.slides.schema.json index 6741e7ae0..cc95f1a0f 100644 --- a/packages/core/schema/superdeck.slides.schema.json +++ b/packages/core/schema/superdeck.slides.schema.json @@ -2,9 +2,10 @@ "$id": "https://superdeck.dev/schema/superdeck.slides.schema.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "items": { - "additionalProperties": true, + "additionalProperties": false, "properties": { "comments": { + "default": [], "items": { "type": "string" }, @@ -14,129 +15,206 @@ "type": "string" }, "options": { - "additionalProperties": true, - "properties": { - "layout": { - "enum": [ - "normal", - "fullscreen" - ], - "type": "string" - }, - "style": { - "type": "string" - }, - "template": { - "type": "string" + "anyOf": [ + { + "additionalProperties": true, + "properties": { + "layout": { + "anyOf": [ + { + "enum": [ + "normal", + "fullscreen" + ], + "type": "string" + }, + { + "type": "null" + } + ] + }, + "style": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "template": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "title": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "type": "object", + "x-transformed": true }, - "title": { - "type": "string" + { + "type": "null" } - }, - "type": "object" + ] }, "sections": { + "default": [], "items": { "additionalProperties": false, "properties": { "align": { - "enum": [ - "topLeft", - "topCenter", - "topRight", - "centerLeft", - "center", - "centerRight", - "bottomLeft", - "bottomCenter", - "bottomRight" - ], - "type": "string" + "anyOf": [ + { + "enum": [ + "topLeft", + "topCenter", + "topRight", + "centerLeft", + "center", + "centerRight", + "bottomLeft", + "bottomCenter", + "bottomRight" + ], + "type": "string" + }, + { + "type": "null" + } + ] }, "blocks": { "items": { "anyOf": [ { - "additionalProperties": true, + "additionalProperties": false, "properties": { "align": { - "enum": [ - "topLeft", - "topCenter", - "topRight", - "centerLeft", - "center", - "centerRight", - "bottomLeft", - "bottomCenter", - "bottomRight" - ], - "type": "string" + "anyOf": [ + { + "enum": [ + "topLeft", + "topCenter", + "topRight", + "centerLeft", + "center", + "centerRight", + "bottomLeft", + "bottomCenter", + "bottomRight" + ], + "type": "string" + }, + { + "type": "null" + } + ] }, "content": { "type": "string" }, "flex": { + "default": 1, "exclusiveMinimum": 0, "type": "integer" }, "margin": { - "additionalProperties": false, - "properties": { - "bottom": { - "minimum": 0, - "type": "number" + "anyOf": [ + { + "additionalProperties": false, + "properties": { + "bottom": { + "minimum": 0, + "type": "number", + "x-transformed": true + }, + "left": { + "minimum": 0, + "type": "number", + "x-transformed": true + }, + "right": { + "minimum": 0, + "type": "number", + "x-transformed": true + }, + "top": { + "minimum": 0, + "type": "number", + "x-transformed": true + } + }, + "required": [ + "top", + "right", + "bottom", + "left" + ], + "type": "object", + "x-transformed": true }, - "left": { - "minimum": 0, - "type": "number" - }, - "right": { - "minimum": 0, - "type": "number" - }, - "top": { - "minimum": 0, - "type": "number" + { + "type": "null" } - }, - "required": [ - "top", - "right", - "bottom", - "left" - ], - "type": "object" + ] }, "padding": { - "additionalProperties": false, - "properties": { - "bottom": { - "minimum": 0, - "type": "number" - }, - "left": { - "minimum": 0, - "type": "number" + "anyOf": [ + { + "additionalProperties": false, + "properties": { + "bottom": { + "minimum": 0, + "type": "number", + "x-transformed": true + }, + "left": { + "minimum": 0, + "type": "number", + "x-transformed": true + }, + "right": { + "minimum": 0, + "type": "number", + "x-transformed": true + }, + "top": { + "minimum": 0, + "type": "number", + "x-transformed": true + } + }, + "required": [ + "top", + "right", + "bottom", + "left" + ], + "type": "object", + "x-transformed": true }, - "right": { - "minimum": 0, - "type": "number" - }, - "top": { - "minimum": 0, - "type": "number" + { + "type": "null" } - }, - "required": [ - "top", - "right", - "bottom", - "left" - ], - "type": "object" + ] }, "scrollable": { + "default": false, "type": "boolean" }, "type": { @@ -153,83 +231,116 @@ "additionalProperties": true, "properties": { "align": { - "enum": [ - "topLeft", - "topCenter", - "topRight", - "centerLeft", - "center", - "centerRight", - "bottomLeft", - "bottomCenter", - "bottomRight" - ], - "type": "string" + "anyOf": [ + { + "enum": [ + "topLeft", + "topCenter", + "topRight", + "centerLeft", + "center", + "centerRight", + "bottomLeft", + "bottomCenter", + "bottomRight" + ], + "type": "string" + }, + { + "type": "null" + } + ] }, "flex": { + "default": 1, "exclusiveMinimum": 0, "type": "integer" }, "margin": { - "additionalProperties": false, - "properties": { - "bottom": { - "minimum": 0, - "type": "number" - }, - "left": { - "minimum": 0, - "type": "number" - }, - "right": { - "minimum": 0, - "type": "number" + "anyOf": [ + { + "additionalProperties": false, + "properties": { + "bottom": { + "minimum": 0, + "type": "number", + "x-transformed": true + }, + "left": { + "minimum": 0, + "type": "number", + "x-transformed": true + }, + "right": { + "minimum": 0, + "type": "number", + "x-transformed": true + }, + "top": { + "minimum": 0, + "type": "number", + "x-transformed": true + } + }, + "required": [ + "top", + "right", + "bottom", + "left" + ], + "type": "object", + "x-transformed": true }, - "top": { - "minimum": 0, - "type": "number" + { + "type": "null" } - }, - "required": [ - "top", - "right", - "bottom", - "left" - ], - "type": "object" + ] }, "name": { "type": "string" }, "padding": { - "additionalProperties": false, - "properties": { - "bottom": { - "minimum": 0, - "type": "number" + "anyOf": [ + { + "additionalProperties": false, + "properties": { + "bottom": { + "minimum": 0, + "type": "number", + "x-transformed": true + }, + "left": { + "minimum": 0, + "type": "number", + "x-transformed": true + }, + "right": { + "minimum": 0, + "type": "number", + "x-transformed": true + }, + "top": { + "minimum": 0, + "type": "number", + "x-transformed": true + } + }, + "required": [ + "top", + "right", + "bottom", + "left" + ], + "type": "object", + "x-transformed": true }, - "left": { - "minimum": 0, - "type": "number" - }, - "right": { - "minimum": 0, - "type": "number" - }, - "top": { - "minimum": 0, - "type": "number" + { + "type": "null" } - }, - "required": [ - "top", - "right", - "bottom", - "left" - ], - "type": "object" + ] }, "scrollable": { + "default": false, "type": "boolean" }, "type": { @@ -243,24 +354,30 @@ ], "type": "object" } - ] + ], + "x-transformed": true }, "type": "array" }, "flex": { + "default": 1, "exclusiveMinimum": 0, "type": "integer" }, "spacing": { + "default": 0.0, "minimum": 0, - "type": "number" + "type": "number", + "x-transformed": true }, "type": { "const": "section", + "default": "section", "type": "string" } }, - "type": "object" + "type": "object", + "x-transformed": true }, "type": "array" } @@ -268,7 +385,8 @@ "required": [ "key" ], - "type": "object" + "type": "object", + "x-transformed": true }, "title": "Slides Contract", "type": "array" diff --git a/packages/core/test/public_api_test.dart b/packages/core/test/public_api_test.dart index 0642638dc..f7593b599 100644 --- a/packages/core/test/public_api_test.dart +++ b/packages/core/test/public_api_test.dart @@ -11,7 +11,10 @@ void main() { expect(workspace.deckJson.path, contains('superdeck.json')); expect(DeckPlugin, isNotNull); expect(slidesContractSchema, isNotNull); - expect(parseSlidesContract([slide.toMap()]), hasLength(1)); + expect(SectionBlockSchema.wireSchema, isNotNull); + expect(SlideSchema.wireSchema, isNotNull); + expect(Slide.fromJson(slide.toJson()), slide); + expect(parseSlidesContract([slide.toJson()]), hasLength(1)); expect(padding.left, 8); }); }); diff --git a/packages/core/test/src/deck/block_model_test.dart b/packages/core/test/src/deck/block_model_test.dart index 79903d5b8..5445edba9 100644 --- a/packages/core/test/src/deck/block_model_test.dart +++ b/packages/core/test/src/deck/block_model_test.dart @@ -1,3 +1,4 @@ +import 'package:ack/ack.dart'; import 'package:superdeck_core/src/deck/block_insets.dart'; import 'package:superdeck_core/src/deck/block_model.dart'; import 'package:test/test.dart'; @@ -14,15 +15,38 @@ Matcher _throwsInvalidFlex() { ); } -Matcher _throwsMappedInvalidFlex() { +Matcher _throwsMappedInvalidFlex() => + _throwsConstraintError('#/flex', 'number_positive'); + +Matcher _throwsConstraintError(String path, String constraintKey) { return throwsA( - predicate((error) { - final message = error.toString(); - return message.contains('flex') && message.contains('greater than zero'); - }, 'an error naming flex and requiring a value greater than zero'), + isA().having( + (exception) => exception.errors.expand(_flattenSchemaErrors), + 'validation errors', + contains( + isA() + .having((error) => error.path, 'path', path) + .having( + (error) => error.constraints.map( + (constraint) => constraint.constraint.constraintKey, + ), + 'constraint keys', + contains(constraintKey), + ), + ), + ), ); } +Iterable _flattenSchemaErrors(SchemaError error) sync* { + yield error; + if (error case SchemaNestedError(errors: final nestedErrors)) { + for (final nestedError in nestedErrors) { + yield* _flattenSchemaErrors(nestedError); + } + } +} + Matcher _throwsInvalidSpacing() { return throwsA( isA() @@ -344,7 +368,7 @@ void main() { test('$field: $input normalizes to four physical edges', () { final block = Block.parseAuthoring({'type': 'block', field: input}); - expect(block.toMap()[field], normalized); + expect(block.toJson()[field], normalized); }); } } @@ -367,7 +391,7 @@ void main() { {'top': 12, 'right': 24}, ]) { expect( - ContentBlock.schema.safeParse({ + ContentBlockSchema.wireSchema.safeParse({ 'type': 'block', field: input, }).isOk, @@ -381,7 +405,7 @@ void main() { test('contract schema accepts normalized four-edge insets', () { for (final field in const ['padding', 'margin']) { expect( - ContentBlock.schema.safeParse({ + ContentBlockSchema.wireSchema.safeParse({ 'type': 'block', field: {'top': 1, 'right': 2, 'bottom': 3, 'left': 4}, }).isOk, @@ -447,7 +471,7 @@ void main() { field: {'top': 12}, }); - expect(block.toMap()[field], { + expect(block.toJson()[field], { 'top': 12.0, 'right': 0.0, 'bottom': 0.0, @@ -473,13 +497,13 @@ void main() { }); test('public constructors create normalized insets', () { - expect(BlockInsets.all(8).toMap(), { + expect(BlockInsets.all(8).toJson(), { 'top': 8.0, 'right': 8.0, 'bottom': 8.0, 'left': 8.0, }); - expect(BlockInsets.symmetric(horizontal: 12, vertical: 6).toMap(), { + expect(BlockInsets.symmetric(horizontal: 12, vertical: 6).toJson(), { 'top': 6.0, 'right': 12.0, 'bottom': 6.0, @@ -505,14 +529,14 @@ void main() { }); test('generated map and copy paths preserve invariants', () { - final insets = BlockInsets.fromMap({ + final insets = BlockInsets.fromJson({ 'top': 1, 'right': 2, 'bottom': 3, 'left': 4, }); - expect(insets.toMap(), { + expect(insets.toJson(), { 'top': 1.0, 'right': 2.0, 'bottom': 3.0, @@ -525,6 +549,19 @@ void main() { ); }); + test('compiled model rejects unknown inset fields', () { + expect( + () => BlockInsets.fromJson({ + 'top': 1, + 'right': 2, + 'bottom': 3, + 'left': 4, + 'unexpected': true, + }), + throwsA(isA()), + ); + }); + test('absent insets stay null; explicit zero stays representable', () { final inherited = Block.parseAuthoring({'type': 'block'}); expect(inherited.margin, isNull); @@ -590,9 +627,9 @@ void main() { expect(() => block.copyWith(flex: 0), _throwsInvalidFlex()); }); - test('fromMap rejects non-positive flex', () { + test('fromJson rejects non-positive flex', () { expect( - () => ContentBlock.fromMap({'type': 'block', 'flex': -1}), + () => ContentBlock.fromJson({'type': 'block', 'flex': -1}), _throwsMappedInvalidFlex(), ); }); @@ -600,7 +637,7 @@ void main() { test('parse reports the flex field and accepted range', () { expect( () => ContentBlock.parse({'type': 'block', 'flex': 0}), - _throwsInvalidFlex(), + _throwsMappedInvalidFlex(), ); }); }); @@ -654,10 +691,10 @@ void main() { }); }); - group('toMap', () { + group('toJson', () { test('serializes minimal block', () { final block = ContentBlock(''); - final map = block.toMap(); + final map = block.toJson(); expect(map['type'], 'block'); expect(map['flex'], 1); @@ -674,7 +711,7 @@ void main() { padding: BlockInsets.symmetric(horizontal: 12, vertical: 8), scrollable: true, ); - final map = block.toMap(); + final map = block.toJson(); expect(map['type'], 'block'); expect(map['content'], 'Content'); @@ -690,10 +727,10 @@ void main() { }); }); - group('fromMap', () { + group('fromJson', () { test('deserializes minimal map', () { final map = {'type': 'block'}; - final block = ContentBlock.fromMap(map); + final block = ContentBlock.fromJson(map); expect(block.content, ''); expect(block.flex, 1); @@ -701,6 +738,24 @@ void main() { expect(block.align, isNull); }); + test('direct branch accepts an omitted discriminator', () { + final block = ContentBlock.fromJson({'content': 'Direct'}); + + expect(block.content, 'Direct'); + expect(block.toJson()['type'], ContentBlock.key); + }); + + test('rejects unknown fields', () { + expect( + () => ContentBlock.fromJson({ + 'type': 'block', + 'content': 'Known', + 'unknown': {'nested': true}, + }), + throwsA(isA()), + ); + }); + test('deserializes full map', () { final map = { 'type': 'block', @@ -710,7 +765,7 @@ void main() { 'padding': {'top': 1, 'right': 2, 'bottom': 3, 'left': 4}, 'scrollable': true, }; - final block = ContentBlock.fromMap(map); + final block = ContentBlock.fromJson(map); expect(block.content, 'Content'); expect(block.align, ContentAlignment.center); @@ -724,27 +779,35 @@ void main() { test('deserializes new block type', () { final map = {'type': 'block', 'content': 'New format'}; - final block = ContentBlock.fromMap(map); + final block = ContentBlock.fromJson(map); expect(block.content, 'New format'); expect(block.type, 'block'); }); - test('handles numeric flex as double', () { + test('normalizes an integral numeric flex', () { final map = {'type': 'block', 'flex': 2.0}; - final block = ContentBlock.fromMap(map); - expect(block.flex, 2); + expect(ContentBlock.fromJson(map).flex, 2); + }); + + test('rejects a fractional numeric flex', () { + final map = {'type': 'block', 'flex': 2.5}; + + expect( + () => ContentBlock.fromJson(map), + throwsA(isA()), + ); }); test('throws on invalid alignment', () { final map = {'type': 'block', 'align': 'invalid'}; - expect(() => ContentBlock.fromMap(map), throwsA(anything)); + expect(() => ContentBlock.fromJson(map), throwsA(anything)); }); }); group('round-trip serialization', () { - test('preserves data through toMap/fromMap', () { + test('preserves data through toJson/fromJson', () { final original = ContentBlock( 'Test content', align: ContentAlignment.bottomRight, @@ -753,7 +816,7 @@ void main() { scrollable: true, ); - final restored = ContentBlock.fromMap(original.toMap()); + final restored = ContentBlock.fromJson(original.toJson()); expect(restored, original); }); @@ -786,7 +849,7 @@ void main() { group('schema', () { test('validates minimal block', () { // Note: 'content' is required to satisfy Google AI schema requirements - final result = ContentBlock.schema.safeParse({ + final result = ContentBlockSchema.wireSchema.safeParse({ 'type': 'block', 'content': '', }); @@ -794,7 +857,7 @@ void main() { }); test('validates full block', () { - final result = ContentBlock.schema.safeParse({ + final result = ContentBlockSchema.wireSchema.safeParse({ 'type': 'block', 'content': 'Content', 'align': 'center', @@ -805,7 +868,7 @@ void main() { }); test('rejects unsupported column type', () { - final result = ContentBlock.schema.safeParse({ + final result = ContentBlockSchema.wireSchema.safeParse({ 'type': 'column', 'content': 'Content', }); @@ -815,7 +878,7 @@ void main() { test('rejects non-positive flex', () { for (final flex in [0, -1]) { - final result = ContentBlock.schema.safeParse({ + final result = ContentBlockSchema.wireSchema.safeParse({ 'type': 'block', 'flex': flex, }); @@ -879,9 +942,9 @@ void main() { expect(() => section.copyWith(flex: 0), _throwsInvalidFlex()); }); - test('fromMap rejects non-positive flex', () { + test('fromJson rejects non-positive flex', () { expect( - () => SectionBlock.fromMap({'type': 'section', 'flex': -1}), + () => SectionBlock.fromJson({'type': 'section', 'flex': -1}), _throwsMappedInvalidFlex(), ); }); @@ -889,7 +952,7 @@ void main() { test('parse reports the flex field and accepted range', () { expect( () => SectionBlock.parse({'type': 'section', 'flex': 0}), - _throwsInvalidFlex(), + _throwsMappedInvalidFlex(), ); }); @@ -912,10 +975,14 @@ void main() { }); test('parse reports the spacing field and accepted range', () { - for (final spacing in [-1.0, double.nan, double.infinity]) { + for (final (spacing, constraintKey) in [ + (-1.0, 'number_min'), + (double.nan, 'number.isFinite'), + (double.infinity, 'number.isFinite'), + ]) { expect( () => SectionBlock.parse({'spacing': spacing}), - _throwsInvalidSpacing(), + _throwsConstraintError('#/spacing', constraintKey), reason: 'spacing: $spacing', ); } @@ -986,10 +1053,10 @@ void main() { }); }); - group('toMap', () { + group('toJson', () { test('serializes empty section', () { final section = SectionBlock([]); - final map = section.toMap(); + final map = section.toJson(); expect(map['type'], 'section'); expect(map['blocks'], isEmpty); @@ -999,7 +1066,7 @@ void main() { test('serializes section with blocks', () { final section = SectionBlock([ContentBlock('Test')], spacing: 20); - final map = section.toMap(); + final map = section.toJson(); expect(map['type'], 'section'); expect(map['blocks'], isA()); @@ -1009,10 +1076,10 @@ void main() { }); }); - group('fromMap', () { + group('fromJson', () { test('deserializes empty section', () { final map = {'type': 'section'}; - final section = SectionBlock.fromMap(map); + final section = SectionBlock.fromJson(map); expect(section.blocks, isEmpty); }); @@ -1025,7 +1092,7 @@ void main() { ], 'spacing': 18, }; - final section = SectionBlock.fromMap(map); + final section = SectionBlock.fromJson(map); expect(section.blocks.length, 1); expect((section.blocks[0] as ContentBlock).content, 'Test'); @@ -1035,7 +1102,7 @@ void main() { group('schema', () { test('validates full section', () { - final result = SectionBlock.schema.safeParse({ + final result = SectionBlockSchema.wireSchema.safeParse({ 'align': 'center', 'flex': 2, 'blocks': [ @@ -1046,8 +1113,27 @@ void main() { expect(result.isOk, isTrue); }); + test('preserves nested blocks as wire maps', () { + final section = SectionBlockSchema.wireSchema.parse({ + 'blocks': [ + {'type': 'block', 'content': 'Test'}, + ], + }); + + final blocks = section!['blocks']! as List; + final block = blocks.single; + expect( + block, + isA>().having( + (value) => value['content'], + 'content', + 'Test', + ), + ); + }); + test('rejects section-level scrollable', () { - final result = SectionBlock.schema.safeParse({ + final result = SectionBlockSchema.wireSchema.safeParse({ 'scrollable': true, 'blocks': [ {'type': 'block', 'content': 'Test'}, @@ -1058,7 +1144,7 @@ void main() { }); test('rejects unknown section-level fields', () { - final result = SectionBlock.schema.safeParse({ + final result = SectionBlockSchema.wireSchema.safeParse({ 'customSectionArg': 'value', 'blocks': [ {'type': 'block', 'content': 'Test'}, @@ -1070,7 +1156,7 @@ void main() { test('accepts finite non-negative spacing', () { for (final spacing in [0, 12.5, 40]) { - final result = SectionBlock.schema.safeParse({ + final result = SectionBlockSchema.wireSchema.safeParse({ 'spacing': spacing, 'blocks': [], }); @@ -1082,14 +1168,16 @@ void main() { test('rejects non-positive flex and invalid spacing', () { for (final flex in [0, -1]) { expect( - SectionBlock.schema.safeParse({'flex': flex}).isOk, + SectionBlockSchema.wireSchema.safeParse({'flex': flex}).isOk, isFalse, reason: 'flex: $flex', ); } for (final spacing in [-1, double.nan, double.infinity]) { expect( - SectionBlock.schema.safeParse({'spacing': spacing}).isOk, + SectionBlockSchema.wireSchema.safeParse({ + 'spacing': spacing, + }).isOk, isFalse, reason: 'spacing: $spacing', ); @@ -1151,6 +1239,60 @@ void main() { expect(() => widget.args['newKey'] = 'fail', throwsUnsupportedError); }); + test('args snapshot nested source collections', () { + final nestedMap = {'enabled': true}; + final nestedList = ['first']; + final nestedSet = {'alpha'}; + final source = { + 'map': nestedMap, + 'list': nestedList, + 'set': nestedSet, + }; + final widget = WidgetBlock(name: 'Test', args: source); + final equalSnapshot = WidgetBlock( + name: 'Test', + args: { + 'map': {'enabled': true}, + 'list': ['first'], + 'set': {'alpha'}, + }, + ); + final originalHash = widget.hashCode; + + source['later'] = true; + nestedMap['enabled'] = false; + nestedList.add('second'); + nestedSet.add('beta'); + + expect(widget, equalSnapshot); + expect(widget.hashCode, originalHash); + expect(widget.args.containsKey('later'), isFalse); + }); + + test('args nested collections are unmodifiable', () { + final widget = WidgetBlock( + name: 'Test', + args: { + 'map': {'enabled': true}, + 'list': ['first'], + 'set': {'alpha'}, + }, + ); + + expect( + () => (widget.args['map']! as Map)['later'] = true, + throwsUnsupportedError, + ); + expect( + () => (widget.args['list']! as List).add('second'), + throwsUnsupportedError, + ); + expect( + () => (widget.args['set']! as Set).add('beta'), + throwsUnsupportedError, + ); + }); + group('constructor validation', () { for (final flex in [0, -1]) { test('rejects flex $flex', () { @@ -1213,13 +1355,13 @@ void main() { expect(widget.args.containsKey('padding'), isFalse); expect(widget.args.containsKey('margin'), isFalse); expect(widget.args['custom'], 'value'); - expect(widget.toMap()['padding'], { + expect(widget.toJson()['padding'], { 'top': 8.0, 'right': 12.0, 'bottom': 8.0, 'left': 12.0, }); - expect(widget.toMap()['margin'], { + expect(widget.toJson()['margin'], { 'top': 4.0, 'right': 4.0, 'bottom': 4.0, @@ -1243,6 +1385,28 @@ void main() { expect(copy.args, {'b': 2}); }); + test('snapshots replacement args deeply', () { + final nested = {'value': 1}; + final replacement = {'nested': nested}; + final copy = WidgetBlock( + name: 'Test', + args: {'old': true}, + ).copyWith(args: replacement); + final originalHash = copy.hashCode; + + nested['value'] = 2; + replacement['later'] = true; + + expect(copy.args, { + 'nested': {'value': 1}, + }); + expect(copy.hashCode, originalHash); + expect( + () => (copy.args['nested']! as Map)['value'] = 3, + throwsUnsupportedError, + ); + }); + test('preserves values when not specified', () { final original = WidgetBlock( name: 'Test', @@ -1261,10 +1425,10 @@ void main() { }); }); - group('toMap', () { + group('toJson', () { test('serializes widget without args', () { final widget = WidgetBlock(name: 'Test'); - final map = widget.toMap(); + final map = widget.toJson(); expect(map['type'], 'widget'); expect(map['name'], 'Test'); @@ -1277,7 +1441,7 @@ void main() { name: 'Test', args: {'customKey': 'customValue', 'count': 5}, ); - final map = widget.toMap(); + final map = widget.toJson(); expect(map['customKey'], 'customValue'); expect(map['count'], 5); @@ -1291,7 +1455,7 @@ void main() { scrollable: true, args: {'custom': 'value'}, ); - final map = widget.toMap(); + final map = widget.toJson(); expect(map['type'], 'widget'); expect(map['name'], 'ReservedName'); @@ -1302,7 +1466,7 @@ void main() { }); }); - group('fromMap', () { + group('fromJson', () { test('extracts known fields', () { final map = { 'type': 'widget', @@ -1311,7 +1475,7 @@ void main() { 'scrollable': true, 'align': 'center', }; - final widget = WidgetBlock.fromMap(map); + final widget = WidgetBlock.fromJson(map); expect(widget.name, 'MyWidget'); expect(widget.flex, 3); @@ -1326,7 +1490,7 @@ void main() { 'customKey': 'customValue', 'otherKey': 123, }; - final widget = WidgetBlock.fromMap(map); + final widget = WidgetBlock.fromJson(map); expect(widget.args['customKey'], 'customValue'); expect(widget.args['otherKey'], 123); @@ -1335,7 +1499,7 @@ void main() { }); test('strips reserved fields from args', () { - final widget = WidgetBlock.fromMap({ + final widget = WidgetBlock.fromJson({ 'type': 'widget', 'name': 'MyWidget', 'align': 'center', @@ -1354,7 +1518,7 @@ void main() { }); group('round-trip serialization', () { - test('preserves data through toMap/fromMap', () { + test('preserves data through toJson/fromJson', () { final original = WidgetBlock( name: 'TestWidget', args: {'config': 'value'}, @@ -1363,7 +1527,7 @@ void main() { scrollable: true, ); - final restored = WidgetBlock.fromMap(original.toMap()); + final restored = WidgetBlock.fromJson(original.toJson()); expect(restored, original); }); @@ -1395,30 +1559,37 @@ void main() { }); group('Block', () { - group('fromMap', () { + group('fromJson', () { test('creates ContentBlock from block type', () { final map = {'type': 'block', 'content': 'Test'}; - final block = Block.fromMap(map); + final block = Block.fromJson(map); expect(block, isA()); expect((block as ContentBlock).content, 'Test'); }); + test('sealed union requires its discriminator', () { + expect( + () => Block.fromJson({'content': 'Missing type'}), + throwsA(isA()), + ); + }); + test('rejects unsupported column type', () { final map = {'type': 'column', 'content': 'Test'}; - expect(() => Block.fromMap(map), throwsA(anything)); + expect(() => Block.fromJson(map), throwsA(anything)); }); test('rejects SectionBlock from section type', () { final map = {'type': 'section', 'blocks': []}; - expect(() => Block.fromMap(map), throwsA(anything)); + expect(() => Block.fromJson(map), throwsA(anything)); }); test('creates WidgetBlock from widget type', () { final map = {'type': 'widget', 'name': 'Test'}; - final block = Block.fromMap(map); + final block = Block.fromJson(map); expect(block, isA()); expect((block as WidgetBlock).name, 'Test'); @@ -1426,7 +1597,7 @@ void main() { test('throws for unknown type', () { final map = {'type': 'unknown'}; - expect(() => Block.fromMap(map), throwsA(anything)); + expect(() => Block.fromJson(map), throwsA(anything)); }); }); @@ -1454,14 +1625,14 @@ void main() { test('reports the flex field and accepted range', () { expect( () => Block.parse({'type': 'widget', 'name': 'Test', 'flex': -1}), - _throwsInvalidFlex(), + _throwsMappedInvalidFlex(), ); }); }); group('schema', () { test('validates content block', () { - final result = Block.schema.safeParse({ + final result = BlockSchema.wireSchema.safeParse({ 'type': 'block', 'content': 'Test', }); @@ -1469,7 +1640,7 @@ void main() { }); test('validates widget block', () { - final result = Block.schema.safeParse({ + final result = BlockSchema.wireSchema.safeParse({ 'type': 'widget', 'name': 'Test', }); @@ -1485,8 +1656,8 @@ void main() { WidgetBlock(name: 'W', args: {'x': 1}), ]); - final map = original.toMap(); - final restored = SectionBlock.fromMap(map); + final map = original.toJson(); + final restored = SectionBlock.fromJson(map); expect(restored.blocks.length, 2); expect(restored.blocks[0], isA()); @@ -1494,7 +1665,7 @@ void main() { }); test('schema rejects nested sections', () { - final result = SectionBlock.schema.safeParse({ + final result = SectionBlockSchema.wireSchema.safeParse({ 'blocks': [ {'type': 'section', 'blocks': []}, ], diff --git a/packages/core/test/src/deck/deck_build_status_test.dart b/packages/core/test/src/deck/deck_build_status_test.dart index 4f3862850..f030560f0 100644 --- a/packages/core/test/src/deck/deck_build_status_test.dart +++ b/packages/core/test/src/deck/deck_build_status_test.dart @@ -12,17 +12,26 @@ void main() { }); group('DeckBuildError', () { - test('toMap/fromObject round-trip', () { + test('toJson/fromObject round-trip', () { const error = DeckBuildError(message: 'Build failed'); - final parsed = DeckBuildError.fromObject(error.toMap()); + final parsed = DeckBuildError.fromObject(error.toJson()); expect(parsed, error); }); + + test('discards unknown persisted fields', () { + final parsed = DeckBuildError.fromJson({ + 'message': 'Build failed', + 'legacyCode': 42, + }); + + expect(parsed.toJson(), {'message': 'Build failed'}); + }); }); group('DeckBuildStatus', () { - test('toMap/fromObject round-trip', () { + test('toJson/fromObject round-trip', () { final timestamp = DateTime.parse('2026-03-10T12:00:00.000Z'); final status = DeckBuildStatus( phase: DeckBuildPhase.success, @@ -30,7 +39,7 @@ void main() { slideCount: 7, ); - final parsed = DeckBuildStatus.fromObject(status.toMap()); + final parsed = DeckBuildStatus.fromObject(status.toJson()); expect(parsed, status); }); @@ -54,5 +63,25 @@ void main() { expect(missing, isNull); expect(invalid, isNull); }); + + test('discards unknown persisted fields', () { + final parsed = DeckBuildStatus.fromJson({ + 'status': 'success', + 'timestamp': '2026-03-10T12:00:00.000Z', + 'legacyProgress': 100, + }); + + expect(parsed.toJson(), isNot(contains('legacyProgress'))); + }); + + test('wire schema retains fields discarded by the typed model', () { + final wire = DeckBuildStatusSchema.wireSchema.parse({ + 'status': 'success', + 'timestamp': '2026-03-10T12:00:00.000Z', + 'legacyProgress': 100, + }); + + expect(wire!['legacyProgress'], 100); + }); }); } diff --git a/packages/core/test/src/deck/deck_workspace_test.dart b/packages/core/test/src/deck/deck_workspace_test.dart index 79a814dd9..823771e5e 100644 --- a/packages/core/test/src/deck/deck_workspace_test.dart +++ b/packages/core/test/src/deck/deck_workspace_test.dart @@ -1,3 +1,4 @@ +import 'package:ack/ack.dart'; import 'package:superdeck_core/src/deck/deck_workspace.dart'; import 'package:test/test.dart'; @@ -245,10 +246,10 @@ void main() { }); }); - group('toMap', () { + group('toJson', () { test('serializes default values', () { final config = DeckWorkspace(); - final map = config.toMap(); + final map = config.toJson(); expect(map['projectDir'], '.'); expect(map['slidesPath'], 'slides.md'); @@ -257,7 +258,7 @@ void main() { test('serializes updated values alongside defaults', () { final config = DeckWorkspace(projectDir: '/project'); - final map = config.toMap(); + final map = config.toJson(); expect(map['projectDir'], '/project'); expect(map['slidesPath'], 'slides.md'); @@ -270,7 +271,7 @@ void main() { slidesPath: 'slides.md', outputDir: 'output', ); - final map = config.toMap(); + final map = config.toJson(); expect(map['projectDir'], '/project'); expect(map['slidesPath'], 'slides.md'); @@ -278,29 +279,28 @@ void main() { }); }); - group('fromMap', () { + group('fromJson', () { test('deserializes empty map', () { - final config = DeckWorkspace.fromMap({}); + final config = DeckWorkspace.fromJson({}); expect(config.projectDir, '.'); expect(config.slidesPath, 'slides.md'); expect(config.outputDir, '.superdeck'); }); - test('deserializes null values using constructor defaults', () { - final config = DeckWorkspace.fromMap({ - 'projectDir': null, - 'slidesPath': null, - 'outputDir': null, - }); - - expect(config.projectDir, '.'); - expect(config.slidesPath, 'slides.md'); - expect(config.outputDir, '.superdeck'); + test('rejects explicit null values', () { + expect( + () => DeckWorkspace.fromJson({ + 'projectDir': null, + 'slidesPath': null, + 'outputDir': null, + }), + throwsA(isA()), + ); }); test('deserializes partial map', () { - final config = DeckWorkspace.fromMap({ + final config = DeckWorkspace.fromJson({ 'projectDir': '/project', 'slidesPath': 'deck.md', }); @@ -311,7 +311,7 @@ void main() { }); test('deserializes full map', () { - final config = DeckWorkspace.fromMap({ + final config = DeckWorkspace.fromJson({ 'projectDir': '/project', 'slidesPath': 'slides.md', 'outputDir': 'output', @@ -324,14 +324,14 @@ void main() { }); group('round-trip serialization', () { - test('preserves data through toMap/fromMap', () { + test('preserves data through toJson/fromJson', () { final original = DeckWorkspace( projectDir: '/roundtrip', slidesPath: 'rt.md', outputDir: 'rt-out', ); - final restored = DeckWorkspace.fromMap(original.toMap()); + final restored = DeckWorkspace.fromJson(original.toJson()); expect(restored, original); }); @@ -342,7 +342,7 @@ void main() { outputDir: 'out', ); - final restored = DeckWorkspace.fromMap(original.toMap()); + final restored = DeckWorkspace.fromJson(original.toJson()); expect(restored.projectDir, '/partial'); expect(restored.outputDir, 'out'); @@ -378,25 +378,25 @@ void main() { } }); - test('ignores unknown keys after validation', () { - final config = DeckWorkspace.parse({ - 'slidesPath': 'parsed.md', - 'extra': {'keep': 'passthrough'}, - }); - - expect(config.slidesPath, 'parsed.md'); - expect(config.toMap().containsKey('extra'), isFalse); + test('rejects unknown keys', () { + expect( + () => DeckWorkspace.parse({ + 'slidesPath': 'parsed.md', + 'extra': {'keep': 'passthrough'}, + }), + throwsA(isA()), + ); }); }); group('schema', () { test('validates empty map', () { - final result = DeckWorkspace.schema.safeParse({}); + final result = DeckWorkspaceSchema.wireSchema.safeParse({}); expect(result.isOk, isTrue); }); test('validates all string fields', () { - final result = DeckWorkspace.schema.safeParse({ + final result = DeckWorkspaceSchema.wireSchema.safeParse({ 'projectDir': '/project', 'slidesPath': 'slides.md', 'outputDir': 'output', @@ -405,7 +405,7 @@ void main() { }); test('validates partial map', () { - final result = DeckWorkspace.schema.safeParse({ + final result = DeckWorkspaceSchema.wireSchema.safeParse({ 'projectDir': '/only-project', }); expect(result.isOk, isTrue); @@ -413,81 +413,85 @@ void main() { test('fails when optional fields are explicitly null', () { for (final field in ['projectDir', 'slidesPath', 'outputDir']) { - final result = DeckWorkspace.schema.safeParse({field: null}); + final result = DeckWorkspaceSchema.wireSchema.safeParse({ + field: null, + }); expect(result.isOk, isFalse); } }); - test('allows unknown keys while validating known fields', () { - final result = DeckWorkspace.schema.safeParse({ + test('rejects unknown keys while validating known fields', () { + final result = DeckWorkspaceSchema.wireSchema.safeParse({ 'projectDir': '/project', 'extra': {'nested': true}, }); - expect(result.isOk, isTrue); + expect(result.isOk, isFalse); }); group('path validation', () { for (final field in ['slidesPath', 'outputDir']) { group(field, () { test('accepts simple relative path', () { - final result = DeckWorkspace.schema.safeParse({ + final result = DeckWorkspaceSchema.wireSchema.safeParse({ field: 'my_file.md', }); expect(result.isOk, isTrue); }); test('accepts nested relative path', () { - final result = DeckWorkspace.schema.safeParse({ + final result = DeckWorkspaceSchema.wireSchema.safeParse({ field: 'sub/dir/file.md', }); expect(result.isOk, isTrue); }); test('accepts filename containing ".."', () { - final result = DeckWorkspace.schema.safeParse({ + final result = DeckWorkspaceSchema.wireSchema.safeParse({ field: 'my..file.md', }); expect(result.isOk, isTrue); }); test('accepts dot-prefixed relative path', () { - final result = DeckWorkspace.schema.safeParse({ + final result = DeckWorkspaceSchema.wireSchema.safeParse({ field: '.superdeck', }); expect(result.isOk, isTrue); }); test('rejects absolute path', () { - final result = DeckWorkspace.schema.safeParse({ + final result = DeckWorkspaceSchema.wireSchema.safeParse({ field: '/etc/passwd', }); expect(result.isOk, isFalse); }); test('rejects ".." traversal segment', () { - final result = DeckWorkspace.schema.safeParse({ + final result = DeckWorkspaceSchema.wireSchema.safeParse({ field: '../outside', }); expect(result.isOk, isFalse); }); test('rejects nested ".." traversal segment', () { - final result = DeckWorkspace.schema.safeParse({ + final result = DeckWorkspaceSchema.wireSchema.safeParse({ field: 'sub/../../outside', }); expect(result.isOk, isFalse); }); test('rejects bare ".." path', () { - final result = DeckWorkspace.schema.safeParse({field: '..'}); + final result = DeckWorkspaceSchema.wireSchema.safeParse({ + field: '..', + }); expect(result.isOk, isFalse); }); }); } test('projectDir still allows absolute paths', () { - final result = DeckWorkspace.schema.safeParse({ + final result = DeckWorkspaceSchema.wireSchema.safeParse({ 'projectDir': '/absolute/project', }); expect(result.isOk, isTrue); diff --git a/packages/core/test/src/deck/slide_contract_test.dart b/packages/core/test/src/deck/slide_contract_test.dart index bb0c7a3e6..993a2e55a 100644 --- a/packages/core/test/src/deck/slide_contract_test.dart +++ b/packages/core/test/src/deck/slide_contract_test.dart @@ -8,10 +8,22 @@ Map _propertySchema( Map schema, String property, ) { - final properties = schema['properties'] as Map; + final properties = + _nonNullSchema(schema)['properties'] as Map; return Map.from(properties[property] as Map); } +Map _nonNullSchema(Map schema) { + final anyOf = schema['anyOf']; + if (anyOf is! List) return schema; + for (final candidate in anyOf.whereType()) { + if (candidate['type'] != 'null') { + return Map.from(candidate); + } + } + return schema; +} + Map _arrayItemSchema( Map schema, String property, @@ -20,19 +32,10 @@ Map _arrayItemSchema( return Map.from(arraySchema['items'] as Map); } -void _expectSchemaIsNotNullable(Map schema) { - expect(schema['type'], isNot('null')); - +void _expectSchemaIsNullable(Map schema) { final anyOf = schema['anyOf'] as List?; - if (anyOf == null) { - return; - } - - final hasNullType = anyOf - .whereType() - .map((item) => item['type']) - .contains('null'); - expect(hasNullType, isFalse); + expect(anyOf, isNotNull); + expect(anyOf!.whereType().map((item) => item['type']), contains('null')); } void main() { @@ -66,7 +69,7 @@ void main() { expect(options.args.containsKey('template'), isFalse); }); - test('round-trips slide payloads through toMap', () { + test('round-trips slide payloads through toJson', () { final original = [ Slide( key: 'rt-slide', @@ -81,7 +84,7 @@ void main() { ]; final restored = parseSlidesContract( - original.map((slide) => slide.toMap()).toList(), + original.map((slide) => slide.toJson()).toList(), ); expect(restored, original); @@ -102,7 +105,7 @@ void main() { slideOptionsSchema, 'title', ); - _expectSchemaIsNotNullable(slideOptionsTitleSchema); + _expectSchemaIsNullable(slideOptionsTitleSchema); }); test('json schema exports closed section options', () { @@ -134,10 +137,12 @@ void main() { test('json schema exports normalized four-edge insets only', () { for (final field in const ['padding', 'margin']) { - final insetsSchema = _propertySchema( - ContentBlock.schema.toJsonSchema(), + final nullableInsetsSchema = _propertySchema( + ContentBlockSchema.wireSchema.toJsonSchema(), field, ); + _expectSchemaIsNullable(nullableInsetsSchema); + final insetsSchema = _nonNullSchema(nullableInsetsSchema); expect(insetsSchema['type'], 'object', reason: field); expect(insetsSchema['additionalProperties'], isFalse, reason: field); @@ -184,7 +189,7 @@ void main() { ]; final restored = parseSlidesContract( - original.map((slide) => slide.toMap()).toList(), + original.map((slide) => slide.toJson()).toList(), ); expect(restored, original); diff --git a/packages/core/test/src/deck/slide_model_test.dart b/packages/core/test/src/deck/slide_model_test.dart index f221c3ae5..c0b0938e1 100644 --- a/packages/core/test/src/deck/slide_model_test.dart +++ b/packages/core/test/src/deck/slide_model_test.dart @@ -1,4 +1,3 @@ -import 'package:ack/ack.dart'; import 'package:superdeck_core/src/deck/block_model.dart'; import 'package:superdeck_core/src/deck/slide_model.dart'; import 'package:test/test.dart'; @@ -75,6 +74,15 @@ void main() { expect(copy.options?.title, 'New Title'); }); + test('null clears existing nullable options', () { + final options = SlideOptions(title: 'Existing'); + final original = Slide(key: 'key', options: options); + + final copy = original.copyWith(options: null); + + expect(copy.options, isNull); + }); + test('copies with new sections', () { final original = Slide(key: 'key'); final newSections = [ @@ -110,10 +118,10 @@ void main() { }); }); - group('toMap', () { + group('toJson', () { test('serializes minimal slide', () { final slide = Slide(key: 'minimal'); - final map = slide.toMap(); + final map = slide.toJson(); expect(map['key'], 'minimal'); expect(map['sections'], isEmpty); @@ -130,7 +138,7 @@ void main() { key: 'with-opts', options: SlideOptions(title: 'My Title', style: 'dark'), ); - final map = slide.toMap(); + final map = slide.toJson(); expect(map['options'], isA()); expect((map['options'] as Map)['title'], 'My Title'); @@ -145,7 +153,7 @@ void main() { SectionBlock([ContentBlock('Second')]), ], ); - final map = slide.toMap(); + final map = slide.toJson(); expect(map['sections'], isA()); expect((map['sections'] as List).length, 2); @@ -153,16 +161,16 @@ void main() { test('serializes slide with comments', () { final slide = Slide(key: 'with-comments', comments: ['Note 1']); - final map = slide.toMap(); + final map = slide.toJson(); expect(map['comments'], ['Note 1']); }); }); - group('fromMap', () { + group('fromJson', () { test('deserializes minimal map', () { final map = {'key': 'from-map'}; - final slide = Slide.fromMap(map); + final slide = Slide.fromJson(map); expect(slide.key, 'from-map'); expect(slide.options, isNull); @@ -175,7 +183,7 @@ void main() { 'key': 'opts-key', 'options': {'title': 'Parsed Title', 'style': 'light'}, }; - final slide = Slide.fromMap(map); + final slide = Slide.fromJson(map); expect(slide.options?.title, 'Parsed Title'); expect(slide.options?.style, 'light'); @@ -193,7 +201,7 @@ void main() { }, ], }; - final slide = Slide.fromMap(map); + final slide = Slide.fromJson(map); expect(slide.sections.length, 1); expect(slide.sections[0].blocks.length, 1); @@ -204,29 +212,25 @@ void main() { 'key': 'comments-key', 'comments': ['Comment 1', 'Comment 2'], }; - final slide = Slide.fromMap(map); + final slide = Slide.fromJson(map); expect(slide.comments, ['Comment 1', 'Comment 2']); }); - test( - 'throws AckException when options optional fields are explicitly null', - () { - for (final field in ['title', 'style', 'layout', 'template']) { - expect( - () => Slide.parse({ - 'key': 'invalid-options', - 'options': {field: null}, - }), - throwsA(isA()), - ); - } - }, - ); + test('accepts explicit null for nullable option fields', () { + for (final field in ['title', 'style', 'layout', 'template']) { + final slide = Slide.parse({ + 'key': 'nullable-options', + 'options': {field: null}, + }); + + expect(slide.options, SlideOptions()); + } + }); }); group('round-trip serialization', () { - test('preserves data through toMap/fromMap', () { + test('preserves data through toJson/fromJson', () { final original = Slide( key: 'roundtrip', options: SlideOptions(title: 'RT Title', style: 'rt-style'), @@ -236,7 +240,7 @@ void main() { comments: ['RT Comment'], ); - final restored = Slide.fromMap(original.toMap()); + final restored = Slide.fromJson(original.toJson()); expect(restored.key, original.key); expect(restored.options?.title, original.options?.title); @@ -268,6 +272,13 @@ void main() { expect(slide.key, 'full'); expect(slide.options?.title, 'Title'); }); + + test('rejects unknown fields', () { + expect( + () => Slide.parse({'key': 'strict', 'unexpected': true}), + throwsA(anything), + ); + }); }); group('equality', () { @@ -326,12 +337,12 @@ void main() { group('schema', () { test('validates minimal slide', () { - final result = Slide.schema.safeParse({'key': 'valid'}); + final result = SlideSchema.wireSchema.safeParse({'key': 'valid'}); expect(result.isOk, isTrue); }); test('validates slide with all fields', () { - final result = Slide.schema.safeParse({ + final result = SlideSchema.wireSchema.safeParse({ 'key': 'full', 'options': {'title': 'T'}, 'sections': [], @@ -340,22 +351,22 @@ void main() { expect(result.isOk, isTrue); }); - test('fails validation when options is explicitly null', () { - final result = Slide.schema.safeParse({ + test('accepts options when explicitly null', () { + final result = SlideSchema.wireSchema.safeParse({ 'key': 'full', 'options': null, }); - expect(result.isOk, isFalse); + expect(result.isOk, isTrue); }); - test('fails validation when options fields are explicitly null', () { + test('accepts nullable option fields when explicitly null', () { for (final field in ['title', 'style', 'layout', 'template']) { - final result = Slide.schema.safeParse({ + final result = SlideSchema.wireSchema.safeParse({ 'key': 'full', 'options': {field: null}, }); - expect(result.isOk, isFalse); + expect(result.isOk, isTrue); } }); }); @@ -394,6 +405,58 @@ void main() { ); }); + test('args snapshot nested source collections', () { + final nestedMap = {'enabled': true}; + final nestedList = ['first']; + final nestedSet = {'alpha'}; + final source = { + 'map': nestedMap, + 'list': nestedList, + 'set': nestedSet, + }; + final options = SlideOptions(args: source); + final equalSnapshot = SlideOptions( + args: { + 'map': {'enabled': true}, + 'list': ['first'], + 'set': {'alpha'}, + }, + ); + final originalHash = options.hashCode; + + source['later'] = true; + nestedMap['enabled'] = false; + nestedList.add('second'); + nestedSet.add('beta'); + + expect(options, equalSnapshot); + expect(options.hashCode, originalHash); + expect(options.args.containsKey('later'), isFalse); + }); + + test('args nested collections are unmodifiable', () { + final options = SlideOptions( + args: { + 'map': {'enabled': true}, + 'list': ['first'], + 'set': {'alpha'}, + }, + ); + + expect( + () => (options.args['map']! as Map)['later'] = true, + throwsUnsupportedError, + ); + expect( + () => (options.args['list']! as List).add('second'), + throwsUnsupportedError, + ); + expect( + () => (options.args['set']! as Set).add('beta'), + throwsUnsupportedError, + ); + }); + test('creates with template parameter', () { final options = SlideOptions(template: 'my-template'); @@ -430,6 +493,27 @@ void main() { expect(copy.args, {'b': 2}); }); + test('snapshots replacement args deeply', () { + final nested = {'value': 1}; + final replacement = {'nested': nested}; + final copy = SlideOptions( + args: {'old': true}, + ).copyWith(args: replacement); + final originalHash = copy.hashCode; + + nested['value'] = 2; + replacement['later'] = true; + + expect(copy.args, { + 'nested': {'value': 1}, + }); + expect(copy.hashCode, originalHash); + expect( + () => (copy.args['nested']! as Map)['value'] = 3, + throwsUnsupportedError, + ); + }); + test('copies with new template', () { final original = SlideOptions(template: 'original-template'); final copy = original.copyWith(template: 'new-template'); @@ -437,6 +521,27 @@ void main() { expect(copy.template, 'new-template'); }); + test('null clears existing nullable values', () { + final original = SlideOptions( + title: 'Title', + style: 'Style', + layout: SlideLayout.fullscreen, + template: 'template', + ); + + final copy = original.copyWith( + title: null, + style: null, + layout: null, + template: null, + ); + + expect(copy.title, isNull); + expect(copy.style, isNull); + expect(copy.layout, isNull); + expect(copy.template, isNull); + }); + test('preserves values when not specified', () { final original = SlideOptions( title: 'T', @@ -455,10 +560,10 @@ void main() { }); }); - group('toMap', () { + group('toJson', () { test('serializes empty options', () { final options = SlideOptions(); - final map = options.toMap(); + final map = options.toJson(); expect(map.containsKey('title'), isFalse); expect(map.containsKey('style'), isFalse); @@ -470,7 +575,7 @@ void main() { style: 'S', layout: SlideLayout.fullscreen, ); - final map = options.toMap(); + final map = options.toJson(); expect(map['title'], 'T'); expect(map['style'], 'S'); @@ -479,14 +584,14 @@ void main() { test('serializes template when present', () { final options = SlideOptions(template: 'my-template'); - final map = options.toMap(); + final map = options.toJson(); expect(map['template'], 'my-template'); }); test('omits template when null', () { final options = SlideOptions(title: 'T'); - final map = options.toMap(); + final map = options.toJson(); expect(map.containsKey('template'), isFalse); }); @@ -496,7 +601,7 @@ void main() { title: 'T', args: {'custom1': 'val1', 'custom2': 42}, ); - final map = options.toMap(); + final map = options.toJson(); expect(map['title'], 'T'); expect(map['custom1'], 'val1'); @@ -517,7 +622,7 @@ void main() { 'custom': 'value', }, ); - final map = options.toMap(); + final map = options.toJson(); expect(map['title'], 'Reserved title'); expect(map['style'], 'reserved-style'); @@ -527,9 +632,9 @@ void main() { }); }); - group('fromMap', () { + group('fromJson', () { test('deserializes empty map', () { - final options = SlideOptions.fromMap({}); + final options = SlideOptions.fromJson({}); expect(options.title, isNull); expect(options.style, isNull); @@ -542,7 +647,7 @@ void main() { 'style': 'parsed-style', 'layout': 'fullscreen', }; - final options = SlideOptions.fromMap(map); + final options = SlideOptions.fromJson(map); expect(options.title, 'Parsed'); expect(options.style, 'parsed-style'); @@ -551,14 +656,14 @@ void main() { test('deserializes template field', () { final map = {'template': 'parsed-template'}; - final options = SlideOptions.fromMap(map); + final options = SlideOptions.fromJson(map); expect(options.template, 'parsed-template'); }); test('removes template from args', () { final map = {'template': 'tmpl', 'extra': 'val'}; - final options = SlideOptions.fromMap(map); + final options = SlideOptions.fromJson(map); expect(options.template, 'tmpl'); expect(options.args.containsKey('template'), isFalse); @@ -571,7 +676,7 @@ void main() { 'customKey': 'customValue', 'anotherKey': 123, }; - final options = SlideOptions.fromMap(map); + final options = SlideOptions.fromJson(map); expect(options.title, 'T'); expect(options.args['customKey'], 'customValue'); @@ -580,7 +685,7 @@ void main() { }); test('strips all reserved keys from args', () { - final options = SlideOptions.fromMap({ + final options = SlideOptions.fromJson({ 'title': 'T', 'style': 'S', 'layout': 'normal', @@ -598,7 +703,7 @@ void main() { }); group('round-trip serialization', () { - test('preserves data through toMap/fromMap', () { + test('preserves data through toJson/fromJson', () { final original = SlideOptions( title: 'RT', style: 'rt-style', @@ -606,7 +711,7 @@ void main() { args: {'k': 'v'}, ); - final restored = SlideOptions.fromMap(original.toMap()); + final restored = SlideOptions.fromJson(original.toJson()); expect(restored.title, original.title); expect(restored.style, original.style); @@ -615,10 +720,10 @@ void main() { expect(restored.args['k'], original.args['k']); }); - test('preserves template through toMap/fromMap', () { + test('preserves template through toJson/fromJson', () { final original = SlideOptions(title: 'RT', template: 'rt-template'); - final restored = SlideOptions.fromMap(original.toMap()); + final restored = SlideOptions.fromJson(original.toJson()); expect(restored.template, original.template); expect(restored.args.containsKey('template'), isFalse); @@ -710,12 +815,12 @@ void main() { group('schema', () { test('validates empty map', () { - final result = SlideOptions.schema.safeParse({}); + final result = SlideOptionsSchema.wireSchema.safeParse({}); expect(result.isOk, isTrue); }); test('validates map with title and style', () { - final result = SlideOptions.schema.safeParse({ + final result = SlideOptionsSchema.wireSchema.safeParse({ 'title': 'T', 'style': 'S', }); @@ -723,7 +828,7 @@ void main() { }); test('allows additional properties', () { - final result = SlideOptions.schema.safeParse({ + final result = SlideOptionsSchema.wireSchema.safeParse({ 'title': 'T', 'custom': 'value', }); @@ -731,7 +836,7 @@ void main() { }); test('validates map with template field', () { - final result = SlideOptions.schema.safeParse({ + final result = SlideOptionsSchema.wireSchema.safeParse({ 'title': 'T', 'template': 'my-template', }); @@ -739,7 +844,7 @@ void main() { }); test('validates map with only template field', () { - final result = SlideOptions.schema.safeParse({ + final result = SlideOptionsSchema.wireSchema.safeParse({ 'template': 'standalone-template', }); expect(result.isOk, isTrue); @@ -747,25 +852,31 @@ void main() { test('validates normal and fullscreen layout values', () { expect( - SlideOptions.schema.safeParse({'layout': 'normal'}).isOk, + SlideOptionsSchema.wireSchema.safeParse({'layout': 'normal'}).isOk, isTrue, ); expect( - SlideOptions.schema.safeParse({'layout': 'fullscreen'}).isOk, + SlideOptionsSchema.wireSchema.safeParse({ + 'layout': 'fullscreen', + }).isOk, isTrue, ); }); test('rejects invalid layout values', () { - final result = SlideOptions.schema.safeParse({'layout': 'wide'}); + final result = SlideOptionsSchema.wireSchema.safeParse({ + 'layout': 'wide', + }); expect(result.isOk, isFalse); }); - test('fails validation when optional fields are explicitly null', () { + test('accepts nullable optional fields when explicitly null', () { for (final field in ['title', 'style', 'layout', 'template']) { - final result = SlideOptions.schema.safeParse({field: null}); - expect(result.isOk, isFalse); + final result = SlideOptionsSchema.wireSchema.safeParse({ + field: null, + }); + expect(result.isOk, isTrue); } }); }); diff --git a/packages/playground/README.md b/packages/playground/README.md index 6fc93ff83..95bfe5150 100644 --- a/packages/playground/README.md +++ b/packages/playground/README.md @@ -49,10 +49,10 @@ missing. ## AI generation smoke lab -Generation uses a plan-first pipeline. `gemini-3.5-flash` creates the shared -narrative/style plan, then `gemini-3.1-flash-lite` composes each narrative -section concurrently. Every call uses the lowest thinking-budget compatibility -setting exposed by the pinned client. Gemini structured output constrains each +Generation uses a plan-first pipeline. `gemini-3.7-flash` creates the shared +narrative/style plan, then `gemini-3.5-flash-lite` composes each narrative +section concurrently. Every call uses the lowest thinking level supported by +its model. Gemini structured output constrains each response to JSON and a response schema; Dart still performs semantic, grounding, layout, density, and canonical parsing checks before accepting it. Invalid slides remain isolated failures so valid slides and later sections can diff --git a/packages/playground/lib/features/ai/deck_editor/ai/deck_tool_schemas.dart b/packages/playground/lib/features/ai/deck_editor/ai/deck_tool_schemas.dart index dd1796b6a..55b6506a0 100644 --- a/packages/playground/lib/features/ai/deck_editor/ai/deck_tool_schemas.dart +++ b/packages/playground/lib/features/ai/deck_editor/ai/deck_tool_schemas.dart @@ -4,9 +4,9 @@ import 'package:superdeck_core/superdeck_core.dart'; /// Strict slide contract exposed to the model. Runtime-only keys are forbidden. final keylessSlideSchema = Ack.object({ - 'options': slideOptionsSchema.optional(), + 'options': SlideOptionsSchema.wireSchema.optional(), 'comments': Ack.list(Ack.string()).optional(), - 'sections': Ack.list(sectionBlockSchema), + 'sections': Ack.list(SectionBlockSchema.wireSchema), }, additionalProperties: false); final getDeckArgumentsSchema = Ack.object({}, additionalProperties: false); @@ -36,10 +36,9 @@ final readSlideArgumentsSchema = Ack.object({ /// Validates and constructs a core slide with a private transient key. Slide parseKeylessSlide(Object? value) { - final validated = keylessSlideSchema.parse(value)!; - final map = Map.of(validated); + final map = keylessSlideSchema.parse(value)!; - return Slide.fromMap({ + return Slide.fromJson({ 'key': 'tool_${generateValueHash(jsonEncode(map))}', ...map, }); @@ -47,7 +46,7 @@ Slide parseKeylessSlide(Object? value) { /// Serializes [slide] without exposing its runtime key. Map slideToKeylessMap(Slide slide) { - return Map.from(slide.toMap())..remove('key'); + return slide.toJson()..remove('key'); } /// Returns the style-free, key-free deck summary used in tool results. diff --git a/packages/playground/lib/features/ai/deck_editor/domain/deck_tools_service.dart b/packages/playground/lib/features/ai/deck_editor/domain/deck_tools_service.dart index 583931d91..527b339ea 100644 --- a/packages/playground/lib/features/ai/deck_editor/domain/deck_tools_service.dart +++ b/packages/playground/lib/features/ai/deck_editor/domain/deck_tools_service.dart @@ -80,7 +80,7 @@ final class DeckToolsService { } Map _keyless(Slide slide) { - return Map.from(slide.toMap())..remove('key'); + return slide.toJson()..remove('key'); } Map _snapshot(List slides) { diff --git a/packages/playground/lib/features/ai/quick_agent/core/constants/gemini_models.dart b/packages/playground/lib/features/ai/quick_agent/core/constants/gemini_models.dart index fdf2eeaf1..20405112e 100644 --- a/packages/playground/lib/features/ai/quick_agent/core/constants/gemini_models.dart +++ b/packages/playground/lib/features/ai/quick_agent/core/constants/gemini_models.dart @@ -1,5 +1,5 @@ /// Gemini model API paths used by the generation pipeline. abstract final class GeminiModelNames { - static const gemini31FlashLite = 'models/gemini-3.1-flash-lite'; - static const gemini35Flash = 'models/gemini-3.5-flash'; + static const gemini35FlashLite = 'models/gemini-3.5-flash-lite'; + static const gemini37Flash = 'models/gemini-3.7-flash'; } diff --git a/packages/playground/lib/features/ai/quick_agent/core/debug_logger.dart b/packages/playground/lib/features/ai/quick_agent/core/debug_logger.dart index 49cfc5c95..fe907509b 100644 --- a/packages/playground/lib/features/ai/quick_agent/core/debug_logger.dart +++ b/packages/playground/lib/features/ai/quick_agent/core/debug_logger.dart @@ -50,7 +50,6 @@ class DebugLogger { if (!kDebugMode) return; debugPrint('\n--- $title ---'); } - } /// Global shortcut for logging. diff --git a/packages/playground/lib/features/ai/quick_agent/core/engine/prompts/composition_example_library.dart b/packages/playground/lib/features/ai/quick_agent/core/engine/prompts/composition_example_library.dart index ffc49771c..2383aa0a9 100644 --- a/packages/playground/lib/features/ai/quick_agent/core/engine/prompts/composition_example_library.dart +++ b/packages/playground/lib/features/ai/quick_agent/core/engine/prompts/composition_example_library.dart @@ -41,7 +41,7 @@ final class AssetCompositionExampleLibrary { } Map buildFor({ - required DeckPlanSlideType current, + required DeckPlanSlide current, required GenerationElementCatalog elementCatalog, }) { if (_templates.isEmpty) { @@ -95,7 +95,7 @@ Map _clone(Map value) => void _hydrateElement( Map example, { - required DeckPlanElementType element, + required DeckPlanElement element, required GenerationElementCatalog elementCatalog, }) { final plannedName = element.type == 'custom' @@ -144,7 +144,7 @@ void _reverseDominantRow(Map example) { } } -void _hydrateMetric(Map example, DeckPlanSlideType current) { +void _hydrateMetric(Map example, DeckPlanSlide current) { final metric = extractAudienceNumericClaims([ current.title, current.assertion, diff --git a/packages/playground/lib/features/ai/quick_agent/core/engine/prompts/generation_prompt_provider.dart b/packages/playground/lib/features/ai/quick_agent/core/engine/prompts/generation_prompt_provider.dart index 86b195453..dc39edd90 100644 --- a/packages/playground/lib/features/ai/quick_agent/core/engine/prompts/generation_prompt_provider.dart +++ b/packages/playground/lib/features/ai/quick_agent/core/engine/prompts/generation_prompt_provider.dart @@ -22,27 +22,27 @@ abstract interface class GenerationPromptProvider { }); String buildSlidePrompt({ - required DeckPlanType plan, - required DeckPlanSlideType current, + required DeckPlan plan, + required DeckPlanSlide current, required Map? previousSlide, - required DeckPlanSlideType? next, + required DeckPlanSlide? next, required GenerationElementCatalog elementCatalog, List validationIssues = const [], Map? invalidSlide, }); String buildSectionPrompt({ - required DeckPlanType plan, - required DeckPlanSectionType section, - required List slides, - required DeckPlanSlideType? previous, - required DeckPlanSlideType? next, + required DeckPlan plan, + required DeckPlanSection section, + required List slides, + required DeckPlanSlide? previous, + required DeckPlanSlide? next, required GenerationElementCatalog elementCatalog, }); String buildOutlineSlideRepairPrompt({ - required DeckPlanType plan, - required DeckPlanSlideType current, + required DeckPlan plan, + required DeckPlanSlide current, required List validationIssues, Map? invalidSlide, }); @@ -140,10 +140,10 @@ ${jsonEncode(themeCandidates.map((theme) => theme.toModelCandidate()).toList())} @override String buildSlidePrompt({ - required DeckPlanType plan, - required DeckPlanSlideType current, + required DeckPlan plan, + required DeckPlanSlide current, required Map? previousSlide, - required DeckPlanSlideType? next, + required DeckPlanSlide? next, required GenerationElementCatalog elementCatalog, List validationIssues = const [], Map? invalidSlide, @@ -179,11 +179,11 @@ ${jsonEncode(themeCandidates.map((theme) => theme.toModelCandidate()).toList())} @override String buildSectionPrompt({ - required DeckPlanType plan, - required DeckPlanSectionType section, - required List slides, - required DeckPlanSlideType? previous, - required DeckPlanSlideType? next, + required DeckPlan plan, + required DeckPlanSection section, + required List slides, + required DeckPlanSlide? previous, + required DeckPlanSlide? next, required GenerationElementCatalog elementCatalog, }) { if (slides.isEmpty) { @@ -246,16 +246,16 @@ Theme: ${encoder.convert(serializeDeckThemeForSlidePrompt(plan.theme))} ## Current narrative section -${encoder.convert(Map.from(section))} +${encoder.convert(section.toJson())} ## Ordered slide plans -${encoder.convert(slides.map(Map.from).toList())} +${encoder.convert(slides.map((slide) => slide.toJson()).toList())} ## Boundary context -Previous plan item: ${previous == null ? 'None.' : encoder.convert(Map.from(previous))} -Next plan item: ${next == null ? 'None.' : encoder.convert(Map.from(next))} +Previous plan item: ${previous == null ? 'None.' : encoder.convert(previous.toJson())} +Next plan item: ${next == null ? 'None.' : encoder.convert(next.toJson())} ## Per-slide visible-content budgets @@ -294,8 +294,8 @@ plan item without borrowing facts or elements from another slide. @override String buildOutlineSlideRepairPrompt({ - required DeckPlanType plan, - required DeckPlanSlideType current, + required DeckPlan plan, + required DeckPlanSlide current, required List validationIssues, Map? invalidSlide, }) { @@ -308,6 +308,7 @@ plan item without borrowing facts or elements from another slide. ); final previous = index > 0 ? plan.slides[index - 1] : null; final next = index + 1 < plan.slides.length ? plan.slides[index + 1] : null; + return ''' You repair exactly one slide inside an already structured presentation plan. Return one complete deck-plan slide JSON object matching the response schema. @@ -347,13 +348,13 @@ only `title`, `purpose`, `assertion`, `contentUnits`, `contentBrief`, and Topic: ${plan.topic} Story: ${plan.story} -Section: ${encoder.convert(Map.from(section))} -Previous slide: ${previous == null ? 'None' : encoder.convert(Map.from(previous))} -Next slide: ${next == null ? 'None' : encoder.convert(Map.from(next))} +Section: ${encoder.convert(section.toJson())} +Previous slide: ${previous == null ? 'None' : encoder.convert(previous.toJson())} +Next slide: ${next == null ? 'None' : encoder.convert(next.toJson())} ## Repair base -${encoder.convert(invalidSlide ?? Map.from(current))} +${encoder.convert(invalidSlide ?? current.toJson())} ## Validation errors @@ -365,9 +366,9 @@ Return only the corrected single-slide plan object. } String buildSingleSlideRepairPrompt({ - required DeckPlanSlideType current, + required DeckPlanSlide current, required Map? previousSlide, - required DeckPlanSlideType? next, + required DeckPlanSlide? next, required List validationIssues, required Map? invalidSlide, required GenerationElementCatalog elementCatalog, @@ -424,14 +425,14 @@ Hard invariants: status, security/compliance claims, availability, or commercial commitments ## Current slide plan -${encoder.convert(Map.from(current))} +${encoder.convert(current.toJson())} ## Neighbor context Previous accepted slide: ${previousSlide == null ? 'None.' : encoder.convert(previousSlide)} Next plan item: -${next == null ? 'None.' : encoder.convert(Map.from(next))} +${next == null ? 'None.' : encoder.convert(next.toJson())} ## Invalid draft ${invalidSlide == null ? 'No parseable draft was returned.' : encoder.convert(invalidSlide)} @@ -511,10 +512,10 @@ steps, future targets, or another feature. String buildSingleSlidePrompt({ required String basePrompt, required String fieldGuidance, - required DeckPlanType plan, - required DeckPlanSlideType current, + required DeckPlan plan, + required DeckPlanSlide current, required Map? previousSlide, - required DeckPlanSlideType? next, + required DeckPlanSlide? next, required GenerationElementCatalog elementCatalog, required Map compositionExample, List validationIssues = const [], @@ -670,13 +671,13 @@ Deck story: ${plan.story} Deck theme reference: ${encoder.convert(serializeDeckThemeForSlidePrompt(plan.theme))} ## Current narrative section -${encoder.convert(Map.from(currentSection))} +${encoder.convert(currentSection.toJson())} ## Recent design ledger ${recentLedger.isEmpty ? 'None (this is the first planned slide).' : encoder.convert(recentLedger)} ## Current slide plan -${encoder.convert(Map.from(current))} +${encoder.convert(current.toJson())} $numericCopyContract ## Neighbor context @@ -684,7 +685,7 @@ Previous canonical slide: ${previousContext == null ? 'None (this is the first slide).' : encoder.convert(previousContext)} Next plan item: -${next == null ? 'None (this is the final slide).' : encoder.convert(Map.from(next))} +${next == null ? 'None (this is the final slide).' : encoder.convert(next.toJson())} ## Available elements $elementContext diff --git a/packages/playground/lib/features/ai/quick_agent/core/engine/schemas/deck_schemas.ack.dart b/packages/playground/lib/features/ai/quick_agent/core/engine/schemas/deck_schemas.ack.dart new file mode 100644 index 000000000..8417f17b5 --- /dev/null +++ b/packages/playground/lib/features/ai/quick_agent/core/engine/schemas/deck_schemas.ack.dart @@ -0,0 +1,459 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// dart format width=80 + +part of 'deck_schemas.dart'; + +// ************************************************************************** +// AckModelGenerator +// ************************************************************************** + +final class _DeckBrandColorsCopyWithUnset { + const _DeckBrandColorsCopyWithUnset(); +} + +/// Immutable model generated from `deckBrandColorsSchema`. +/// Only exact palette roles supplied by the user +@AckInfer.jsonSerializable +final class DeckBrandColors { + DeckBrandColors({ + this.background, + this.surface, + this.surfaceAlt, + this.heading, + this.body, + this.accent, + this.accentContrast, + }); + + factory DeckBrandColors.parse(Object? input) { + return $ack.parse(input); + } + + factory DeckBrandColors.fromJson(Map json) { + return $ack.parse(json); + } + + static const _DeckBrandColorsCopyWithUnset _ackCopyWithUnset = + _DeckBrandColorsCopyWithUnset(); + + final String? background; + + final String? surface; + + final String? surfaceAlt; + + final String? heading; + + final String? body; + + final String? accent; + + final String? accentContrast; + + static final $ack = AckModelAdapter( + schema: () => deckBrandColorsSchema, + fromRuntime: DeckBrandColors._fromAckRuntime, + toRuntime: (model) => model._toAckRuntime(), + ); + + static SchemaResult safeParse(Object? input) => + $ack.safeParse(input); + + Map toJson() => Map.from($ack.encode(this)); + + SchemaResult> safeToJson() => $ack.safeEncode(this); + + DeckBrandColors copyWith({ + Object? background = _ackCopyWithUnset, + Object? surface = _ackCopyWithUnset, + Object? surfaceAlt = _ackCopyWithUnset, + Object? heading = _ackCopyWithUnset, + Object? body = _ackCopyWithUnset, + Object? accent = _ackCopyWithUnset, + Object? accentContrast = _ackCopyWithUnset, + }) => DeckBrandColors( + background: identical(background, _ackCopyWithUnset) + ? this.background + : background as String?, + surface: identical(surface, _ackCopyWithUnset) + ? this.surface + : surface as String?, + surfaceAlt: identical(surfaceAlt, _ackCopyWithUnset) + ? this.surfaceAlt + : surfaceAlt as String?, + heading: identical(heading, _ackCopyWithUnset) + ? this.heading + : heading as String?, + body: identical(body, _ackCopyWithUnset) ? this.body : body as String?, + accent: identical(accent, _ackCopyWithUnset) + ? this.accent + : accent as String?, + accentContrast: identical(accentContrast, _ackCopyWithUnset) + ? this.accentContrast + : accentContrast as String?, + ); + + @override + bool operator ==(Object other) => + identical(this, other) || + (other is DeckBrandColors && + runtimeType == other.runtimeType && + deepEquals(background, other.background) && + deepEquals(surface, other.surface) && + deepEquals(surfaceAlt, other.surfaceAlt) && + deepEquals(heading, other.heading) && + deepEquals(body, other.body) && + deepEquals(accent, other.accent) && + deepEquals(accentContrast, other.accentContrast)); + + @override + int get hashCode => Object.hashAll([ + runtimeType, + deepHashCode(background), + deepHashCode(surface), + deepHashCode(surfaceAlt), + deepHashCode(heading), + deepHashCode(body), + deepHashCode(accent), + deepHashCode(accentContrast), + ]); + + @override + String toString() => + 'DeckBrandColors(background: $background, surface: $surface, surfaceAlt: $surfaceAlt, heading: $heading, body: $body, accent: $accent, accentContrast: $accentContrast)'; + + static DeckBrandColors _fromAckRuntime(Map value) => + _$DeckBrandColorsFromJson(Map.from(value)); + + Map _toAckRuntime() => { + ..._$DeckBrandColorsToJson(this), + }; + + static String? _ackFromRuntimeBackground(Object? value) => value as String?; + + static Object? _ackToRuntimeBackground(String? value) => value; + + static String? _ackFromRuntimeSurface(Object? value) => value as String?; + + static Object? _ackToRuntimeSurface(String? value) => value; + + static String? _ackFromRuntimeSurfaceAlt(Object? value) => value as String?; + + static Object? _ackToRuntimeSurfaceAlt(String? value) => value; + + static String? _ackFromRuntimeHeading(Object? value) => value as String?; + + static Object? _ackToRuntimeHeading(String? value) => value; + + static String? _ackFromRuntimeBody(Object? value) => value as String?; + + static Object? _ackToRuntimeBody(String? value) => value; + + static String? _ackFromRuntimeAccent(Object? value) => value as String?; + + static Object? _ackToRuntimeAccent(String? value) => value; + + static String? _ackFromRuntimeAccentContrast(Object? value) => + value as String?; + + static Object? _ackToRuntimeAccentContrast(String? value) => value; +} + +final class _DeckBrandFontsCopyWithUnset { + const _DeckBrandFontsCopyWithUnset(); +} + +/// Immutable model generated from `deckBrandFontsSchema`. +/// Only exact registered font families supplied by the user +@AckInfer.jsonSerializable +final class DeckBrandFonts { + DeckBrandFonts({this.headline, this.body}); + + factory DeckBrandFonts.parse(Object? input) { + return $ack.parse(input); + } + + factory DeckBrandFonts.fromJson(Map json) { + return $ack.parse(json); + } + + static const _DeckBrandFontsCopyWithUnset _ackCopyWithUnset = + _DeckBrandFontsCopyWithUnset(); + + final String? headline; + + final String? body; + + static final $ack = AckModelAdapter( + schema: () => deckBrandFontsSchema, + fromRuntime: DeckBrandFonts._fromAckRuntime, + toRuntime: (model) => model._toAckRuntime(), + ); + + static SchemaResult safeParse(Object? input) => + $ack.safeParse(input); + + Map toJson() => Map.from($ack.encode(this)); + + SchemaResult> safeToJson() => $ack.safeEncode(this); + + DeckBrandFonts copyWith({ + Object? headline = _ackCopyWithUnset, + Object? body = _ackCopyWithUnset, + }) => DeckBrandFonts( + headline: identical(headline, _ackCopyWithUnset) + ? this.headline + : headline as String?, + body: identical(body, _ackCopyWithUnset) ? this.body : body as String?, + ); + + @override + bool operator ==(Object other) => + identical(this, other) || + (other is DeckBrandFonts && + runtimeType == other.runtimeType && + deepEquals(headline, other.headline) && + deepEquals(body, other.body)); + + @override + int get hashCode => + Object.hashAll([runtimeType, deepHashCode(headline), deepHashCode(body)]); + + @override + String toString() => 'DeckBrandFonts(headline: $headline, body: $body)'; + + static DeckBrandFonts _fromAckRuntime(Map value) => + _$DeckBrandFontsFromJson(Map.from(value)); + + Map _toAckRuntime() => { + ..._$DeckBrandFontsToJson(this), + }; + + static String? _ackFromRuntimeHeadline(Object? value) => value as String?; + + static Object? _ackToRuntimeHeadline(String? value) => value; + + static String? _ackFromRuntimeBody(Object? value) => value as String?; + + static Object? _ackToRuntimeBody(String? value) => value; +} + +final class _DeckBrandOverrideCopyWithUnset { + const _DeckBrandOverrideCopyWithUnset(); +} + +/// Immutable model generated from `deckBrandOverrideSchema`. +/// Validated user-only overrides layered on the selected theme +@AckInfer.jsonSerializable +final class DeckBrandOverride { + DeckBrandOverride({this.colors, this.fonts}); + + factory DeckBrandOverride.parse(Object? input) { + return $ack.parse(input); + } + + factory DeckBrandOverride.fromJson(Map json) { + return $ack.parse(json); + } + + static const _DeckBrandOverrideCopyWithUnset _ackCopyWithUnset = + _DeckBrandOverrideCopyWithUnset(); + + final DeckBrandColors? colors; + + final DeckBrandFonts? fonts; + + static final $ack = AckModelAdapter( + schema: () => deckBrandOverrideSchema, + fromRuntime: DeckBrandOverride._fromAckRuntime, + toRuntime: (model) => model._toAckRuntime(), + ); + + static SchemaResult safeParse(Object? input) => + $ack.safeParse(input); + + Map toJson() => Map.from($ack.encode(this)); + + SchemaResult> safeToJson() => $ack.safeEncode(this); + + DeckBrandOverride copyWith({ + Object? colors = _ackCopyWithUnset, + Object? fonts = _ackCopyWithUnset, + }) => DeckBrandOverride( + colors: identical(colors, _ackCopyWithUnset) + ? this.colors + : colors as DeckBrandColors?, + fonts: identical(fonts, _ackCopyWithUnset) + ? this.fonts + : fonts as DeckBrandFonts?, + ); + + @override + bool operator ==(Object other) => + identical(this, other) || + (other is DeckBrandOverride && + runtimeType == other.runtimeType && + deepEquals(colors, other.colors) && + deepEquals(fonts, other.fonts)); + + @override + int get hashCode => + Object.hashAll([runtimeType, deepHashCode(colors), deepHashCode(fonts)]); + + @override + String toString() => 'DeckBrandOverride(colors: $colors, fonts: $fonts)'; + + static DeckBrandOverride _fromAckRuntime(Map value) => + _$DeckBrandOverrideFromJson(Map.from(value)); + + Map _toAckRuntime() => { + ..._$DeckBrandOverrideToJson(this), + }; + + static DeckBrandColors? _ackFromRuntimeColors(Object? value) => + switch (value) { + null => null, + final fieldValue => DeckBrandColors.$ack.fromRuntime( + fieldValue as Map, + ), + }; + + static Object? _ackToRuntimeColors(DeckBrandColors? value) => switch (value) { + null => null, + final fieldValue => DeckBrandColors.$ack.toRuntime(fieldValue), + }; + + static DeckBrandFonts? _ackFromRuntimeFonts(Object? value) => switch (value) { + null => null, + final fieldValue => DeckBrandFonts.$ack.fromRuntime( + fieldValue as Map, + ), + }; + + static Object? _ackToRuntimeFonts(DeckBrandFonts? value) => switch (value) { + null => null, + final fieldValue => DeckBrandFonts.$ack.toRuntime(fieldValue), + }; +} + +final class _DeckThemeReferenceCopyWithUnset { + const _DeckThemeReferenceCopyWithUnset(); +} + +/// Immutable model generated from `deckThemeReferenceSchema`. +/// Canonical versioned presentation-theme reference +@AckInfer.jsonSerializable +final class DeckThemeReference { + DeckThemeReference({ + required this.id, + required this.version, + required this.density, + this.brandOverride, + }); + + factory DeckThemeReference.parse(Object? input) { + return $ack.parse(input); + } + + factory DeckThemeReference.fromJson(Map json) { + return $ack.parse(json); + } + + static const _DeckThemeReferenceCopyWithUnset _ackCopyWithUnset = + _DeckThemeReferenceCopyWithUnset(); + + /// Stable catalog theme ID + final String id; + + /// Exact catalog version attached by the application + final int version; + + /// Resolved deck density supported by the selected theme + final String density; + + /// Exact user-supplied palette or typography constraints, if any + final DeckBrandOverride? brandOverride; + + static final $ack = AckModelAdapter( + schema: () => deckThemeReferenceSchema, + fromRuntime: DeckThemeReference._fromAckRuntime, + toRuntime: (model) => model._toAckRuntime(), + ); + + static SchemaResult safeParse(Object? input) => + $ack.safeParse(input); + + Map toJson() => Map.from($ack.encode(this)); + + SchemaResult> safeToJson() => $ack.safeEncode(this); + + DeckThemeReference copyWith({ + String? id, + int? version, + String? density, + Object? brandOverride = _ackCopyWithUnset, + }) => DeckThemeReference( + id: id ?? this.id, + version: version ?? this.version, + density: density ?? this.density, + brandOverride: identical(brandOverride, _ackCopyWithUnset) + ? this.brandOverride + : brandOverride as DeckBrandOverride?, + ); + + @override + bool operator ==(Object other) => + identical(this, other) || + (other is DeckThemeReference && + runtimeType == other.runtimeType && + deepEquals(id, other.id) && + deepEquals(version, other.version) && + deepEquals(density, other.density) && + deepEquals(brandOverride, other.brandOverride)); + + @override + int get hashCode => Object.hashAll([ + runtimeType, + deepHashCode(id), + deepHashCode(version), + deepHashCode(density), + deepHashCode(brandOverride), + ]); + + @override + String toString() => + 'DeckThemeReference(id: $id, version: $version, density: $density, brandOverride: $brandOverride)'; + + static DeckThemeReference _fromAckRuntime(Map value) => + _$DeckThemeReferenceFromJson(Map.from(value)); + + Map _toAckRuntime() => { + ..._$DeckThemeReferenceToJson(this), + }; + + static String _ackFromRuntimeId(Object? value) => value as String; + + static Object? _ackToRuntimeId(String value) => value; + + static int _ackFromRuntimeVersion(Object? value) => value as int; + + static Object? _ackToRuntimeVersion(int value) => value; + + static String _ackFromRuntimeDensity(Object? value) => value as String; + + static Object? _ackToRuntimeDensity(String value) => value; + + static DeckBrandOverride? _ackFromRuntimeBrandOverride(Object? value) => + switch (value) { + null => null, + final fieldValue => DeckBrandOverride.$ack.fromRuntime( + fieldValue as Map, + ), + }; + + static Object? _ackToRuntimeBrandOverride(DeckBrandOverride? value) => + switch (value) { + null => null, + final fieldValue => DeckBrandOverride.$ack.toRuntime(fieldValue), + }; +} diff --git a/packages/playground/lib/features/ai/quick_agent/core/engine/schemas/deck_schemas.ack.g.dart b/packages/playground/lib/features/ai/quick_agent/core/engine/schemas/deck_schemas.ack.g.dart new file mode 100644 index 000000000..ce395f6e7 --- /dev/null +++ b/packages/playground/lib/features/ai/quick_agent/core/engine/schemas/deck_schemas.ack.g.dart @@ -0,0 +1,79 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// dart format width=80 + +part of 'deck_schemas.dart'; + +// ************************************************************************** +// AckJsonSerializableGenerator +// ************************************************************************** + +DeckBrandColors _$DeckBrandColorsFromJson(Map json) => + DeckBrandColors( + background: DeckBrandColors._ackFromRuntimeBackground(json['background']), + surface: DeckBrandColors._ackFromRuntimeSurface(json['surface']), + surfaceAlt: DeckBrandColors._ackFromRuntimeSurfaceAlt(json['surfaceAlt']), + heading: DeckBrandColors._ackFromRuntimeHeading(json['heading']), + body: DeckBrandColors._ackFromRuntimeBody(json['body']), + accent: DeckBrandColors._ackFromRuntimeAccent(json['accent']), + accentContrast: DeckBrandColors._ackFromRuntimeAccentContrast( + json['accentContrast'], + ), + ); + +Map _$DeckBrandColorsToJson( + DeckBrandColors instance, +) => { + 'background': ?DeckBrandColors._ackToRuntimeBackground(instance.background), + 'surface': ?DeckBrandColors._ackToRuntimeSurface(instance.surface), + 'surfaceAlt': ?DeckBrandColors._ackToRuntimeSurfaceAlt(instance.surfaceAlt), + 'heading': ?DeckBrandColors._ackToRuntimeHeading(instance.heading), + 'body': ?DeckBrandColors._ackToRuntimeBody(instance.body), + 'accent': ?DeckBrandColors._ackToRuntimeAccent(instance.accent), + 'accentContrast': ?DeckBrandColors._ackToRuntimeAccentContrast( + instance.accentContrast, + ), +}; + +DeckBrandFonts _$DeckBrandFontsFromJson(Map json) => + DeckBrandFonts( + headline: DeckBrandFonts._ackFromRuntimeHeadline(json['headline']), + body: DeckBrandFonts._ackFromRuntimeBody(json['body']), + ); + +Map _$DeckBrandFontsToJson(DeckBrandFonts instance) => + { + 'headline': ?DeckBrandFonts._ackToRuntimeHeadline(instance.headline), + 'body': ?DeckBrandFonts._ackToRuntimeBody(instance.body), + }; + +DeckBrandOverride _$DeckBrandOverrideFromJson(Map json) => + DeckBrandOverride( + colors: DeckBrandOverride._ackFromRuntimeColors(json['colors']), + fonts: DeckBrandOverride._ackFromRuntimeFonts(json['fonts']), + ); + +Map _$DeckBrandOverrideToJson(DeckBrandOverride instance) => + { + 'colors': ?DeckBrandOverride._ackToRuntimeColors(instance.colors), + 'fonts': ?DeckBrandOverride._ackToRuntimeFonts(instance.fonts), + }; + +DeckThemeReference _$DeckThemeReferenceFromJson(Map json) => + DeckThemeReference( + id: DeckThemeReference._ackFromRuntimeId(json['id']), + version: DeckThemeReference._ackFromRuntimeVersion(json['version']), + density: DeckThemeReference._ackFromRuntimeDensity(json['density']), + brandOverride: DeckThemeReference._ackFromRuntimeBrandOverride( + json['brandOverride'], + ), + ); + +Map _$DeckThemeReferenceToJson(DeckThemeReference instance) => + { + 'id': DeckThemeReference._ackToRuntimeId(instance.id), + 'version': DeckThemeReference._ackToRuntimeVersion(instance.version), + 'density': DeckThemeReference._ackToRuntimeDensity(instance.density), + 'brandOverride': ?DeckThemeReference._ackToRuntimeBrandOverride( + instance.brandOverride, + ), + }; diff --git a/packages/playground/lib/features/ai/quick_agent/core/engine/schemas/deck_schemas.dart b/packages/playground/lib/features/ai/quick_agent/core/engine/schemas/deck_schemas.dart index 85c5ddbf8..2a86cea07 100644 --- a/packages/playground/lib/features/ai/quick_agent/core/engine/schemas/deck_schemas.dart +++ b/packages/playground/lib/features/ai/quick_agent/core/engine/schemas/deck_schemas.dart @@ -4,7 +4,8 @@ import 'package:superdeck_core/superdeck_core.dart' show HexColorValidation; import '../../../../../../../core/domain/design/presentation_theme_catalog.dart'; -part 'deck_schemas.g.dart'; +part 'deck_schemas.ack.dart'; +part 'deck_schemas.ack.g.dart'; /// Schema definitions for SuperDeck presentation generation. /// @@ -19,7 +20,7 @@ part 'deck_schemas.g.dart'; const deckDensityProfiles = presentationThemeDensityProfiles; -@AckType(name: 'DeckBrandColors') +@AckInfer() final deckBrandColorsSchema = Ack.object({ 'background': Ack.string().hexColor().optional(), 'surface': Ack.string().hexColor().optional(), @@ -30,19 +31,19 @@ final deckBrandColorsSchema = Ack.object({ 'accentContrast': Ack.string().hexColor().optional(), }).describe('Only exact palette roles supplied by the user'); -@AckType(name: 'DeckBrandFonts') +@AckInfer() final deckBrandFontsSchema = Ack.object({ 'headline': Ack.string().optional(), 'body': Ack.string().optional(), }).describe('Only exact registered font families supplied by the user'); -@AckType(name: 'DeckBrandOverride') +@AckInfer() final deckBrandOverrideSchema = Ack.object({ 'colors': deckBrandColorsSchema.optional(), 'fonts': deckBrandFontsSchema.optional(), }).describe('Validated user-only overrides layered on the selected theme'); -@AckType(name: 'DeckThemeReference') +@AckInfer() final deckThemeReferenceSchema = Ack.object({ 'id': Ack.string().notEmpty().describe('Stable catalog theme ID'), 'version': Ack.integer().positive().describe( diff --git a/packages/playground/lib/features/ai/quick_agent/core/engine/schemas/deck_schemas.g.dart b/packages/playground/lib/features/ai/quick_agent/core/engine/schemas/deck_schemas.g.dart deleted file mode 100644 index b66d1a6a3..000000000 --- a/packages/playground/lib/features/ai/quick_agent/core/engine/schemas/deck_schemas.g.dart +++ /dev/null @@ -1,116 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND -// dart format width=80 - -// ************************************************************************** -// AckSchemaGenerator -// ************************************************************************** - -part of 'deck_schemas.dart'; - -/// Extension type for DeckBrandColors -extension type DeckBrandColorsType(Map _data) - implements Map { - static DeckBrandColorsType parse(Object? data) { - return deckBrandColorsSchema.parseAs( - data, - (validated) => DeckBrandColorsType(validated as Map), - ); - } - - static SchemaResult safeParse(Object? data) { - return deckBrandColorsSchema.safeParseAs( - data, - (validated) => DeckBrandColorsType(validated as Map), - ); - } - - String? get background => _data['background'] as String?; - - String? get surface => _data['surface'] as String?; - - String? get surfaceAlt => _data['surfaceAlt'] as String?; - - String? get heading => _data['heading'] as String?; - - String? get body => _data['body'] as String?; - - String? get accent => _data['accent'] as String?; - - String? get accentContrast => _data['accentContrast'] as String?; -} - -/// Extension type for DeckBrandFonts -extension type DeckBrandFontsType(Map _data) - implements Map { - static DeckBrandFontsType parse(Object? data) { - return deckBrandFontsSchema.parseAs( - data, - (validated) => DeckBrandFontsType(validated as Map), - ); - } - - static SchemaResult safeParse(Object? data) { - return deckBrandFontsSchema.safeParseAs( - data, - (validated) => DeckBrandFontsType(validated as Map), - ); - } - - String? get headline => _data['headline'] as String?; - - String? get body => _data['body'] as String?; -} - -/// Extension type for DeckBrandOverride -extension type DeckBrandOverrideType(Map _data) - implements Map { - static DeckBrandOverrideType parse(Object? data) { - return deckBrandOverrideSchema.parseAs( - data, - (validated) => DeckBrandOverrideType(validated as Map), - ); - } - - static SchemaResult safeParse(Object? data) { - return deckBrandOverrideSchema.safeParseAs( - data, - (validated) => DeckBrandOverrideType(validated as Map), - ); - } - - DeckBrandColorsType? get colors => _data['colors'] != null - ? DeckBrandColorsType(_data['colors'] as Map) - : null; - - DeckBrandFontsType? get fonts => _data['fonts'] != null - ? DeckBrandFontsType(_data['fonts'] as Map) - : null; -} - -/// Extension type for DeckThemeReference -extension type DeckThemeReferenceType(Map _data) - implements Map { - static DeckThemeReferenceType parse(Object? data) { - return deckThemeReferenceSchema.parseAs( - data, - (validated) => DeckThemeReferenceType(validated as Map), - ); - } - - static SchemaResult safeParse(Object? data) { - return deckThemeReferenceSchema.safeParseAs( - data, - (validated) => DeckThemeReferenceType(validated as Map), - ); - } - - String get id => _data['id'] as String; - - int get version => _data['version'] as int; - - String get density => _data['density'] as String; - - DeckBrandOverrideType? get brandOverride => _data['brandOverride'] != null - ? DeckBrandOverrideType(_data['brandOverride'] as Map) - : null; -} diff --git a/packages/playground/lib/features/ai/quick_agent/core/engine/schemas/outline_schema.ack.dart b/packages/playground/lib/features/ai/quick_agent/core/engine/schemas/outline_schema.ack.dart new file mode 100644 index 000000000..d94a788d5 --- /dev/null +++ b/packages/playground/lib/features/ai/quick_agent/core/engine/schemas/outline_schema.ack.dart @@ -0,0 +1,638 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// dart format width=80 + +part of 'outline_schema.dart'; + +// ************************************************************************** +// AckModelGenerator +// ************************************************************************** + +/// Immutable model generated from `deckPlanSectionSchema`. +/// A narrative section or act in the deck blueprint +@AckInfer.jsonSerializable +final class DeckPlanSection { + DeckPlanSection({ + required this.key, + required this.title, + required this.purpose, + required this.transition, + required List slideKeys, + }) : slideKeys = List.unmodifiable(slideKeys.map((item) => item)); + + factory DeckPlanSection.parse(Object? input) { + return $ack.parse(input); + } + + factory DeckPlanSection.fromJson(Map json) { + return $ack.parse(json); + } + + /// Unique section or act identifier + final String key; + + /// Short internal title for this story section + final String title; + + /// Narrative job performed by this section + final String purpose; + + /// How this section hands the story to the next section + final String transition; + + /// Ordered slide keys belonging to this section + final List slideKeys; + + static final $ack = AckModelAdapter( + schema: () => deckPlanSectionSchema, + fromRuntime: DeckPlanSection._fromAckRuntime, + toRuntime: (model) => model._toAckRuntime(), + ); + + static SchemaResult safeParse(Object? input) => + $ack.safeParse(input); + + Map toJson() => Map.from($ack.encode(this)); + + SchemaResult> safeToJson() => $ack.safeEncode(this); + + DeckPlanSection copyWith({ + String? key, + String? title, + String? purpose, + String? transition, + List? slideKeys, + }) => DeckPlanSection( + key: key ?? this.key, + title: title ?? this.title, + purpose: purpose ?? this.purpose, + transition: transition ?? this.transition, + slideKeys: slideKeys ?? this.slideKeys, + ); + + @override + bool operator ==(Object other) => + identical(this, other) || + (other is DeckPlanSection && + runtimeType == other.runtimeType && + deepEquals(key, other.key) && + deepEquals(title, other.title) && + deepEquals(purpose, other.purpose) && + deepEquals(transition, other.transition) && + deepEquals(slideKeys, other.slideKeys)); + + @override + int get hashCode => Object.hashAll([ + runtimeType, + deepHashCode(key), + deepHashCode(title), + deepHashCode(purpose), + deepHashCode(transition), + deepHashCode(slideKeys), + ]); + + @override + String toString() => + 'DeckPlanSection(key: $key, title: $title, purpose: $purpose, transition: $transition, slideKeys: $slideKeys)'; + + static DeckPlanSection _fromAckRuntime(Map value) => + _$DeckPlanSectionFromJson(Map.from(value)); + + Map _toAckRuntime() => { + ..._$DeckPlanSectionToJson(this), + }; + + static String _ackFromRuntimeKey(Object? value) => value as String; + + static Object? _ackToRuntimeKey(String value) => value; + + static String _ackFromRuntimeTitle(Object? value) => value as String; + + static Object? _ackToRuntimeTitle(String value) => value; + + static String _ackFromRuntimePurpose(Object? value) => value as String; + + static Object? _ackToRuntimePurpose(String value) => value; + + static String _ackFromRuntimeTransition(Object? value) => value as String; + + static Object? _ackToRuntimeTransition(String value) => value; + + static List _ackFromRuntimeSlideKeys(Object? value) => + (value as List).map((item) => item as String).toList(); + + static Object? _ackToRuntimeSlideKeys(List value) => + value.map((item) => item).toList(growable: false); +} + +final class _DeckPlanElementCopyWithUnset { + const _DeckPlanElementCopyWithUnset(); +} + +/// Immutable model generated from `deckPlanElementSchema`. +/// An element requirement for later slide composition +@AckInfer.jsonSerializable +final class DeckPlanElement { + DeckPlanElement({ + required this.type, + required this.purpose, + this.source, + this.generationPrompt, + this.widgetName, + }); + + factory DeckPlanElement.parse(Object? input) { + return $ack.parse(input); + } + + factory DeckPlanElement.fromJson(Map json) { + return $ack.parse(json); + } + + static const _DeckPlanElementCopyWithUnset _ackCopyWithUnset = + _DeckPlanElementCopyWithUnset(); + + /// Generation-capable element needed by the slide + final String type; + + /// Why this element belongs on the slide + final String purpose; + + /// User-supplied asset path, URL, text, or gist identifier when available + final String? source; + + /// Concrete visual subject to generate when an image style is configured + final String? generationPrompt; + + /// Registered widget name when type is custom + final String? widgetName; + + static final $ack = AckModelAdapter( + schema: () => deckPlanElementSchema, + fromRuntime: DeckPlanElement._fromAckRuntime, + toRuntime: (model) => model._toAckRuntime(), + ); + + static SchemaResult safeParse(Object? input) => + $ack.safeParse(input); + + Map toJson() => Map.from($ack.encode(this)); + + SchemaResult> safeToJson() => $ack.safeEncode(this); + + DeckPlanElement copyWith({ + String? type, + String? purpose, + Object? source = _ackCopyWithUnset, + Object? generationPrompt = _ackCopyWithUnset, + Object? widgetName = _ackCopyWithUnset, + }) => DeckPlanElement( + type: type ?? this.type, + purpose: purpose ?? this.purpose, + source: identical(source, _ackCopyWithUnset) + ? this.source + : source as String?, + generationPrompt: identical(generationPrompt, _ackCopyWithUnset) + ? this.generationPrompt + : generationPrompt as String?, + widgetName: identical(widgetName, _ackCopyWithUnset) + ? this.widgetName + : widgetName as String?, + ); + + @override + bool operator ==(Object other) => + identical(this, other) || + (other is DeckPlanElement && + runtimeType == other.runtimeType && + deepEquals(type, other.type) && + deepEquals(purpose, other.purpose) && + deepEquals(source, other.source) && + deepEquals(generationPrompt, other.generationPrompt) && + deepEquals(widgetName, other.widgetName)); + + @override + int get hashCode => Object.hashAll([ + runtimeType, + deepHashCode(type), + deepHashCode(purpose), + deepHashCode(source), + deepHashCode(generationPrompt), + deepHashCode(widgetName), + ]); + + @override + String toString() => + 'DeckPlanElement(type: $type, purpose: $purpose, source: $source, generationPrompt: $generationPrompt, widgetName: $widgetName)'; + + static DeckPlanElement _fromAckRuntime(Map value) => + _$DeckPlanElementFromJson(Map.from(value)); + + Map _toAckRuntime() => { + ..._$DeckPlanElementToJson(this), + }; + + static String _ackFromRuntimeType(Object? value) => value as String; + + static Object? _ackToRuntimeType(String value) => value; + + static String _ackFromRuntimePurpose(Object? value) => value as String; + + static Object? _ackToRuntimePurpose(String value) => value; + + static String? _ackFromRuntimeSource(Object? value) => value as String?; + + static Object? _ackToRuntimeSource(String? value) => value; + + static String? _ackFromRuntimeGenerationPrompt(Object? value) => + value as String?; + + static Object? _ackToRuntimeGenerationPrompt(String? value) => value; + + static String? _ackFromRuntimeWidgetName(Object? value) => value as String?; + + static Object? _ackToRuntimeWidgetName(String? value) => value; +} + +final class _DeckPlanSlideCopyWithUnset { + const _DeckPlanSlideCopyWithUnset(); +} + +/// Immutable model generated from `deckPlanSlideSchema`. +/// A single slide in the presentation deck plan +@AckInfer.jsonSerializable +final class DeckPlanSlide { + DeckPlanSlide({ + required this.key, + required this.title, + required this.purpose, + required this.sectionKey, + required this.assertion, + required List contentUnits, + required this.narrativeRole, + required this.contentBrief, + required this.continuity, + required this.composition, + required this.treatment, + required this.density, + List? elements, + }) : contentUnits = List.unmodifiable( + contentUnits.map((item) => item), + ), + elements = switch (elements) { + null => null, + final fieldValue => List.unmodifiable( + fieldValue.map((item) => item), + ), + }; + + factory DeckPlanSlide.parse(Object? input) { + return $ack.parse(input); + } + + factory DeckPlanSlide.fromJson(Map json) { + return $ack.parse(json); + } + + static const _DeckPlanSlideCopyWithUnset _ackCopyWithUnset = + _DeckPlanSlideCopyWithUnset(); + + /// Unique identifier for this slide (e.g., "intro", "slide-1", "conclusion") + final String key; + + /// Working title for this slide (may be refined in final generation) + final String title; + + /// Brief description of what this slide will communicate (1-2 sentences) + final String purpose; + + /// Key of the narrative section containing this slide + final String sectionKey; + + /// The single audience-facing claim this slide must make + final String assertion; + + /// Concrete evidence, examples, or implications to compose + final List contentUnits; + + /// The job this slide performs in the presentation story + final String narrativeRole; + + /// Specific facts, examples, and emphasis the composed slide must include + final String contentBrief; + + /// How this slide connects the previous and next ideas + final String continuity; + + /// Semantic composition intent; the slide composer owns exact geometry + final String composition; + + /// Semantic theme treatment selected for this slide + final String treatment; + + /// Slide-specific density override within the shared system + final String density; + + /// Optional non-Markdown elements required by this slide + final List? elements; + + static final $ack = AckModelAdapter( + schema: () => deckPlanSlideSchema, + fromRuntime: DeckPlanSlide._fromAckRuntime, + toRuntime: (model) => model._toAckRuntime(), + ); + + static SchemaResult safeParse(Object? input) => + $ack.safeParse(input); + + Map toJson() => Map.from($ack.encode(this)); + + SchemaResult> safeToJson() => $ack.safeEncode(this); + + DeckPlanSlide copyWith({ + String? key, + String? title, + String? purpose, + String? sectionKey, + String? assertion, + List? contentUnits, + String? narrativeRole, + String? contentBrief, + String? continuity, + String? composition, + String? treatment, + String? density, + Object? elements = _ackCopyWithUnset, + }) => DeckPlanSlide( + key: key ?? this.key, + title: title ?? this.title, + purpose: purpose ?? this.purpose, + sectionKey: sectionKey ?? this.sectionKey, + assertion: assertion ?? this.assertion, + contentUnits: contentUnits ?? this.contentUnits, + narrativeRole: narrativeRole ?? this.narrativeRole, + contentBrief: contentBrief ?? this.contentBrief, + continuity: continuity ?? this.continuity, + composition: composition ?? this.composition, + treatment: treatment ?? this.treatment, + density: density ?? this.density, + elements: identical(elements, _ackCopyWithUnset) + ? this.elements + : elements as List?, + ); + + @override + bool operator ==(Object other) => + identical(this, other) || + (other is DeckPlanSlide && + runtimeType == other.runtimeType && + deepEquals(key, other.key) && + deepEquals(title, other.title) && + deepEquals(purpose, other.purpose) && + deepEquals(sectionKey, other.sectionKey) && + deepEquals(assertion, other.assertion) && + deepEquals(contentUnits, other.contentUnits) && + deepEquals(narrativeRole, other.narrativeRole) && + deepEquals(contentBrief, other.contentBrief) && + deepEquals(continuity, other.continuity) && + deepEquals(composition, other.composition) && + deepEquals(treatment, other.treatment) && + deepEquals(density, other.density) && + deepEquals(elements, other.elements)); + + @override + int get hashCode => Object.hashAll([ + runtimeType, + deepHashCode(key), + deepHashCode(title), + deepHashCode(purpose), + deepHashCode(sectionKey), + deepHashCode(assertion), + deepHashCode(contentUnits), + deepHashCode(narrativeRole), + deepHashCode(contentBrief), + deepHashCode(continuity), + deepHashCode(composition), + deepHashCode(treatment), + deepHashCode(density), + deepHashCode(elements), + ]); + + @override + String toString() => + 'DeckPlanSlide(key: $key, title: $title, purpose: $purpose, sectionKey: $sectionKey, assertion: $assertion, contentUnits: $contentUnits, narrativeRole: $narrativeRole, contentBrief: $contentBrief, continuity: $continuity, composition: $composition, treatment: $treatment, density: $density, elements: $elements)'; + + static DeckPlanSlide _fromAckRuntime(Map value) => + _$DeckPlanSlideFromJson(Map.from(value)); + + Map _toAckRuntime() => { + ..._$DeckPlanSlideToJson(this), + }; + + static String _ackFromRuntimeKey(Object? value) => value as String; + + static Object? _ackToRuntimeKey(String value) => value; + + static String _ackFromRuntimeTitle(Object? value) => value as String; + + static Object? _ackToRuntimeTitle(String value) => value; + + static String _ackFromRuntimePurpose(Object? value) => value as String; + + static Object? _ackToRuntimePurpose(String value) => value; + + static String _ackFromRuntimeSectionKey(Object? value) => value as String; + + static Object? _ackToRuntimeSectionKey(String value) => value; + + static String _ackFromRuntimeAssertion(Object? value) => value as String; + + static Object? _ackToRuntimeAssertion(String value) => value; + + static List _ackFromRuntimeContentUnits(Object? value) => + (value as List).map((item) => item as String).toList(); + + static Object? _ackToRuntimeContentUnits(List value) => + value.map((item) => item).toList(growable: false); + + static String _ackFromRuntimeNarrativeRole(Object? value) => value as String; + + static Object? _ackToRuntimeNarrativeRole(String value) => value; + + static String _ackFromRuntimeContentBrief(Object? value) => value as String; + + static Object? _ackToRuntimeContentBrief(String value) => value; + + static String _ackFromRuntimeContinuity(Object? value) => value as String; + + static Object? _ackToRuntimeContinuity(String value) => value; + + static String _ackFromRuntimeComposition(Object? value) => value as String; + + static Object? _ackToRuntimeComposition(String value) => value; + + static String _ackFromRuntimeTreatment(Object? value) => value as String; + + static Object? _ackToRuntimeTreatment(String value) => value; + + static String _ackFromRuntimeDensity(Object? value) => value as String; + + static Object? _ackToRuntimeDensity(String value) => value; + + static List? _ackFromRuntimeElements(Object? value) => + switch (value) { + null => null, + final fieldValue => + (fieldValue as List) + .map( + (item) => DeckPlanElement.$ack.fromRuntime( + item as Map, + ), + ) + .toList(), + }; + + static Object? _ackToRuntimeElements(List? value) => + switch (value) { + null => null, + final fieldValue => + fieldValue + .map((item) => DeckPlanElement.$ack.toRuntime(item)) + .toList(growable: false), + }; +} + +/// Immutable model generated from `deckPlanSchema`. +/// Presentation deck plan with narrative, theme, and composition intent +@AckInfer.jsonSerializable +final class DeckPlan { + DeckPlan({ + required this.topic, + required this.story, + required this.theme, + required List sections, + required List slides, + }) : sections = List.unmodifiable( + sections.map((item) => item), + ), + slides = List.unmodifiable(slides.map((item) => item)); + + factory DeckPlan.parse(Object? input) { + return $ack.parse(input); + } + + factory DeckPlan.fromJson(Map json) { + return $ack.parse(json); + } + + /// Main topic of the presentation + final String topic; + + /// One-sentence narrative through-line for the complete presentation + final String story; + + /// Application-resolved theme reference for the complete deck + final DeckThemeReference theme; + + /// Ordered narrative sections whose slide keys partition the deck + final List sections; + + /// Ordered list of slides in the presentation + final List slides; + + static final $ack = AckModelAdapter( + schema: () => deckPlanSchema, + fromRuntime: DeckPlan._fromAckRuntime, + toRuntime: (model) => model._toAckRuntime(), + ); + + static SchemaResult safeParse(Object? input) => + $ack.safeParse(input); + + Map toJson() => Map.from($ack.encode(this)); + + SchemaResult> safeToJson() => $ack.safeEncode(this); + + DeckPlan copyWith({ + String? topic, + String? story, + DeckThemeReference? theme, + List? sections, + List? slides, + }) => DeckPlan( + topic: topic ?? this.topic, + story: story ?? this.story, + theme: theme ?? this.theme, + sections: sections ?? this.sections, + slides: slides ?? this.slides, + ); + + @override + bool operator ==(Object other) => + identical(this, other) || + (other is DeckPlan && + runtimeType == other.runtimeType && + deepEquals(topic, other.topic) && + deepEquals(story, other.story) && + deepEquals(theme, other.theme) && + deepEquals(sections, other.sections) && + deepEquals(slides, other.slides)); + + @override + int get hashCode => Object.hashAll([ + runtimeType, + deepHashCode(topic), + deepHashCode(story), + deepHashCode(theme), + deepHashCode(sections), + deepHashCode(slides), + ]); + + @override + String toString() => + 'DeckPlan(topic: $topic, story: $story, theme: $theme, sections: $sections, slides: $slides)'; + + static DeckPlan _fromAckRuntime(Map value) => + _$DeckPlanFromJson(Map.from(value)); + + Map _toAckRuntime() => { + ..._$DeckPlanToJson(this), + }; + + static String _ackFromRuntimeTopic(Object? value) => value as String; + + static Object? _ackToRuntimeTopic(String value) => value; + + static String _ackFromRuntimeStory(Object? value) => value as String; + + static Object? _ackToRuntimeStory(String value) => value; + + static DeckThemeReference _ackFromRuntimeTheme(Object? value) => + DeckThemeReference.$ack.fromRuntime(value as Map); + + static Object? _ackToRuntimeTheme(DeckThemeReference value) => + DeckThemeReference.$ack.toRuntime(value); + + static List _ackFromRuntimeSections(Object? value) => + (value as List) + .map( + (item) => + DeckPlanSection.$ack.fromRuntime(item as Map), + ) + .toList(); + + static Object? _ackToRuntimeSections(List value) => value + .map((item) => DeckPlanSection.$ack.toRuntime(item)) + .toList(growable: false); + + static List _ackFromRuntimeSlides(Object? value) => + (value as List) + .map( + (item) => + DeckPlanSlide.$ack.fromRuntime(item as Map), + ) + .toList(); + + static Object? _ackToRuntimeSlides(List value) => value + .map((item) => DeckPlanSlide.$ack.toRuntime(item)) + .toList(growable: false); +} diff --git a/packages/playground/lib/features/ai/quick_agent/core/engine/schemas/outline_schema.ack.g.dart b/packages/playground/lib/features/ai/quick_agent/core/engine/schemas/outline_schema.ack.g.dart new file mode 100644 index 000000000..3e40985ab --- /dev/null +++ b/packages/playground/lib/features/ai/quick_agent/core/engine/schemas/outline_schema.ack.g.dart @@ -0,0 +1,110 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// dart format width=80 + +part of 'outline_schema.dart'; + +// ************************************************************************** +// AckJsonSerializableGenerator +// ************************************************************************** + +DeckPlanSection _$DeckPlanSectionFromJson(Map json) => + DeckPlanSection( + key: DeckPlanSection._ackFromRuntimeKey(json['key']), + title: DeckPlanSection._ackFromRuntimeTitle(json['title']), + purpose: DeckPlanSection._ackFromRuntimePurpose(json['purpose']), + transition: DeckPlanSection._ackFromRuntimeTransition(json['transition']), + slideKeys: DeckPlanSection._ackFromRuntimeSlideKeys(json['slideKeys']), + ); + +Map _$DeckPlanSectionToJson( + DeckPlanSection instance, +) => { + 'key': DeckPlanSection._ackToRuntimeKey(instance.key), + 'title': DeckPlanSection._ackToRuntimeTitle(instance.title), + 'purpose': DeckPlanSection._ackToRuntimePurpose(instance.purpose), + 'transition': DeckPlanSection._ackToRuntimeTransition(instance.transition), + 'slideKeys': DeckPlanSection._ackToRuntimeSlideKeys(instance.slideKeys), +}; + +DeckPlanElement _$DeckPlanElementFromJson(Map json) => + DeckPlanElement( + type: DeckPlanElement._ackFromRuntimeType(json['type']), + purpose: DeckPlanElement._ackFromRuntimePurpose(json['purpose']), + source: DeckPlanElement._ackFromRuntimeSource(json['source']), + generationPrompt: DeckPlanElement._ackFromRuntimeGenerationPrompt( + json['generationPrompt'], + ), + widgetName: DeckPlanElement._ackFromRuntimeWidgetName(json['widgetName']), + ); + +Map _$DeckPlanElementToJson( + DeckPlanElement instance, +) => { + 'type': DeckPlanElement._ackToRuntimeType(instance.type), + 'purpose': DeckPlanElement._ackToRuntimePurpose(instance.purpose), + 'source': ?DeckPlanElement._ackToRuntimeSource(instance.source), + 'generationPrompt': ?DeckPlanElement._ackToRuntimeGenerationPrompt( + instance.generationPrompt, + ), + 'widgetName': ?DeckPlanElement._ackToRuntimeWidgetName(instance.widgetName), +}; + +DeckPlanSlide _$DeckPlanSlideFromJson( + Map json, +) => DeckPlanSlide( + key: DeckPlanSlide._ackFromRuntimeKey(json['key']), + title: DeckPlanSlide._ackFromRuntimeTitle(json['title']), + purpose: DeckPlanSlide._ackFromRuntimePurpose(json['purpose']), + sectionKey: DeckPlanSlide._ackFromRuntimeSectionKey(json['sectionKey']), + assertion: DeckPlanSlide._ackFromRuntimeAssertion(json['assertion']), + contentUnits: DeckPlanSlide._ackFromRuntimeContentUnits(json['contentUnits']), + narrativeRole: DeckPlanSlide._ackFromRuntimeNarrativeRole( + json['narrativeRole'], + ), + contentBrief: DeckPlanSlide._ackFromRuntimeContentBrief(json['contentBrief']), + continuity: DeckPlanSlide._ackFromRuntimeContinuity(json['continuity']), + composition: DeckPlanSlide._ackFromRuntimeComposition(json['composition']), + treatment: DeckPlanSlide._ackFromRuntimeTreatment(json['treatment']), + density: DeckPlanSlide._ackFromRuntimeDensity(json['density']), + elements: DeckPlanSlide._ackFromRuntimeElements(json['elements']), +); + +Map _$DeckPlanSlideToJson( + DeckPlanSlide instance, +) => { + 'key': DeckPlanSlide._ackToRuntimeKey(instance.key), + 'title': DeckPlanSlide._ackToRuntimeTitle(instance.title), + 'purpose': DeckPlanSlide._ackToRuntimePurpose(instance.purpose), + 'sectionKey': DeckPlanSlide._ackToRuntimeSectionKey(instance.sectionKey), + 'assertion': DeckPlanSlide._ackToRuntimeAssertion(instance.assertion), + 'contentUnits': DeckPlanSlide._ackToRuntimeContentUnits( + instance.contentUnits, + ), + 'narrativeRole': DeckPlanSlide._ackToRuntimeNarrativeRole( + instance.narrativeRole, + ), + 'contentBrief': DeckPlanSlide._ackToRuntimeContentBrief( + instance.contentBrief, + ), + 'continuity': DeckPlanSlide._ackToRuntimeContinuity(instance.continuity), + 'composition': DeckPlanSlide._ackToRuntimeComposition(instance.composition), + 'treatment': DeckPlanSlide._ackToRuntimeTreatment(instance.treatment), + 'density': DeckPlanSlide._ackToRuntimeDensity(instance.density), + 'elements': ?DeckPlanSlide._ackToRuntimeElements(instance.elements), +}; + +DeckPlan _$DeckPlanFromJson(Map json) => DeckPlan( + topic: DeckPlan._ackFromRuntimeTopic(json['topic']), + story: DeckPlan._ackFromRuntimeStory(json['story']), + theme: DeckPlan._ackFromRuntimeTheme(json['theme']), + sections: DeckPlan._ackFromRuntimeSections(json['sections']), + slides: DeckPlan._ackFromRuntimeSlides(json['slides']), +); + +Map _$DeckPlanToJson(DeckPlan instance) => { + 'topic': DeckPlan._ackToRuntimeTopic(instance.topic), + 'story': DeckPlan._ackToRuntimeStory(instance.story), + 'theme': DeckPlan._ackToRuntimeTheme(instance.theme), + 'sections': DeckPlan._ackToRuntimeSections(instance.sections), + 'slides': DeckPlan._ackToRuntimeSlides(instance.slides), +}; diff --git a/packages/playground/lib/features/ai/quick_agent/core/engine/schemas/outline_schema.dart b/packages/playground/lib/features/ai/quick_agent/core/engine/schemas/outline_schema.dart index d13caddc9..71c8c7150 100644 --- a/packages/playground/lib/features/ai/quick_agent/core/engine/schemas/outline_schema.dart +++ b/packages/playground/lib/features/ai/quick_agent/core/engine/schemas/outline_schema.dart @@ -3,7 +3,8 @@ import 'package:ack_annotations/ack_annotations.dart'; import 'deck_schemas.dart'; -part 'outline_schema.g.dart'; +part 'outline_schema.ack.dart'; +part 'outline_schema.ack.g.dart'; /// Schema definitions for presentation planning (Phase 1). /// @@ -60,7 +61,7 @@ const deckPlanTreatments = [ 'closing', ]; -@AckType(name: 'DeckPlanSection') +@AckInfer() final deckPlanSectionSchema = Ack.object({ 'key': Ack.string().describe('Unique section or act identifier'), 'title': Ack.string().describe('Short internal title for this story section'), @@ -74,7 +75,7 @@ final deckPlanSectionSchema = Ack.object({ }).describe('A narrative section or act in the deck blueprint'); /// Optional generated-element requirement attached to a slide plan. -@AckType(name: 'DeckPlanElement') +@AckInfer() final deckPlanElementSchema = Ack.object({ 'type': Ack.enumString( deckPlanElementTypes, @@ -98,7 +99,7 @@ final deckPlanElementSchema = Ack.object({ /// - title: Working title (may be refined during slide composition) /// - purpose: What the slide will communicate /// - composition: Semantic layout intent for the slide composer -@AckType(name: 'DeckPlanSlide') +@AckInfer() final deckPlanSlideSchema = Ack.object({ 'key': Ack.string().describe( 'Unique identifier for this slide (e.g., "intro", "slide-1", "conclusion")', @@ -149,7 +150,7 @@ final deckPlanSlideSchema = Ack.object({ /// /// Root schema for Phase 1 generation, containing the topic /// and ordered list of slide outlines. -@AckType(name: 'DeckPlan') +@AckInfer() final deckPlanSchema = Ack.object({ 'topic': Ack.string().describe('Main topic of the presentation'), diff --git a/packages/playground/lib/features/ai/quick_agent/core/engine/schemas/outline_schema.g.dart b/packages/playground/lib/features/ai/quick_agent/core/engine/schemas/outline_schema.g.dart deleted file mode 100644 index 66a1c7144..000000000 --- a/packages/playground/lib/features/ai/quick_agent/core/engine/schemas/outline_schema.g.dart +++ /dev/null @@ -1,147 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND -// dart format width=80 - -// ************************************************************************** -// AckSchemaGenerator -// ************************************************************************** - -part of 'outline_schema.dart'; - -List _$ackListCast(Object? value) => (value as List).cast(); - -/// Extension type for DeckPlanSection -extension type DeckPlanSectionType(Map _data) - implements Map { - static DeckPlanSectionType parse(Object? data) { - return deckPlanSectionSchema.parseAs( - data, - (validated) => DeckPlanSectionType(validated as Map), - ); - } - - static SchemaResult safeParse(Object? data) { - return deckPlanSectionSchema.safeParseAs( - data, - (validated) => DeckPlanSectionType(validated as Map), - ); - } - - String get key => _data['key'] as String; - - String get title => _data['title'] as String; - - String get purpose => _data['purpose'] as String; - - String get transition => _data['transition'] as String; - - List get slideKeys => _$ackListCast(_data['slideKeys']); -} - -/// Extension type for DeckPlanElement -extension type DeckPlanElementType(Map _data) - implements Map { - static DeckPlanElementType parse(Object? data) { - return deckPlanElementSchema.parseAs( - data, - (validated) => DeckPlanElementType(validated as Map), - ); - } - - static SchemaResult safeParse(Object? data) { - return deckPlanElementSchema.safeParseAs( - data, - (validated) => DeckPlanElementType(validated as Map), - ); - } - - String get type => _data['type'] as String; - - String get purpose => _data['purpose'] as String; - - String? get source => _data['source'] as String?; - - String? get generationPrompt => _data['generationPrompt'] as String?; - - String? get widgetName => _data['widgetName'] as String?; -} - -/// Extension type for DeckPlanSlide -extension type DeckPlanSlideType(Map _data) - implements Map { - static DeckPlanSlideType parse(Object? data) { - return deckPlanSlideSchema.parseAs( - data, - (validated) => DeckPlanSlideType(validated as Map), - ); - } - - static SchemaResult safeParse(Object? data) { - return deckPlanSlideSchema.safeParseAs( - data, - (validated) => DeckPlanSlideType(validated as Map), - ); - } - - String get key => _data['key'] as String; - - String get title => _data['title'] as String; - - String get purpose => _data['purpose'] as String; - - String get sectionKey => _data['sectionKey'] as String; - - String get assertion => _data['assertion'] as String; - - List get contentUnits => _$ackListCast(_data['contentUnits']); - - String get narrativeRole => _data['narrativeRole'] as String; - - String get contentBrief => _data['contentBrief'] as String; - - String get continuity => _data['continuity'] as String; - - String get composition => _data['composition'] as String; - - String get treatment => _data['treatment'] as String; - - String get density => _data['density'] as String; - - List? get elements => _data['elements'] != null - ? (_data['elements'] as List) - .map((e) => DeckPlanElementType(e as Map)) - .toList() - : null; -} - -/// Extension type for DeckPlan -extension type DeckPlanType(Map _data) - implements Map { - static DeckPlanType parse(Object? data) { - return deckPlanSchema.parseAs( - data, - (validated) => DeckPlanType(validated as Map), - ); - } - - static SchemaResult safeParse(Object? data) { - return deckPlanSchema.safeParseAs( - data, - (validated) => DeckPlanType(validated as Map), - ); - } - - String get topic => _data['topic'] as String; - - String get story => _data['story'] as String; - - DeckThemeReferenceType get theme => - DeckThemeReferenceType(_data['theme'] as Map); - - List get sections => (_data['sections'] as List) - .map((e) => DeckPlanSectionType(e as Map)) - .toList(); - - List get slides => (_data['slides'] as List) - .map((e) => DeckPlanSlideType(e as Map)) - .toList(); -} diff --git a/packages/playground/lib/features/ai/quick_agent/core/engine/services/deck_generator_images.dart b/packages/playground/lib/features/ai/quick_agent/core/engine/services/deck_generator_images.dart index bb24bb3d4..6c2bc0f63 100644 --- a/packages/playground/lib/features/ai/quick_agent/core/engine/services/deck_generator_images.dart +++ b/packages/playground/lib/features/ai/quick_agent/core/engine/services/deck_generator_images.dart @@ -5,7 +5,7 @@ typedef ImageGenerationProgressCallback = /// Rewritten composition plan and ordered image outcomes for one run. final class DeckImageGenerationResult { - final DeckPlanType plan; + final DeckPlan plan; final List assets; const DeckImageGenerationResult({required this.plan, required this.assets}); @@ -31,7 +31,7 @@ final class _PlannedImage { Future _runImagePhase( DeckGeneratorService owner, { - required DeckPlanType plan, + required DeckPlan plan, required DeckGenerationRequest request, required GenerationProgressCallback? onProgress, required GenerationTraceEmitter trace, @@ -90,7 +90,7 @@ Future _runImagePhase( /// Generates planned artwork with bounded concurrency and returns a plan that /// references only successful assets. Failed visuals fall back to text layouts. Future generateImagesForPlan({ - required DeckPlanType plan, + required DeckPlan plan, required PresentationImageStyleDescriptor imageStyle, required ImageGenerator generator, required String runId, @@ -171,7 +171,7 @@ Future generateImagesForPlan({ } List<_PlannedImage> _plannedImages( - DeckPlanType plan, + DeckPlan plan, String runId, { required String? backgroundColor, required Map backgroundColorsByTreatment, @@ -179,7 +179,7 @@ List<_PlannedImage> _plannedImages( final planned = <_PlannedImage>[]; for (final (slideIndex, slide) in plan.slides.indexed) { for (final (elementIndex, element) - in (slide.elements ?? const []).indexed) { + in (slide.elements ?? const []).indexed) { final subject = element.generationPrompt?.trim(); if (element.type != 'image' || subject == null || subject.isEmpty) { continue; @@ -222,36 +222,36 @@ String _imageBackgroundForTreatment( }; } -DeckPlanType _rewriteGeneratedImageSources( - DeckPlanType plan, +DeckPlan _rewriteGeneratedImageSources( + DeckPlan plan, List<_PlannedImage> planned, List assets, ) { - final rewritten = jsonDecode(jsonEncode(plan)) as Map; - final slides = rewritten['slides']! as List; + final slides = plan.slides.toList(); - for (final (index, image) in planned.indexed.toList().reversed) { - final slide = slides[image.slideIndex] as Map; - final elements = slide['elements']! as List; + // Remove failed elements from the end so earlier element indexes stay valid. + for (var index = planned.length - 1; index >= 0; index--) { + final image = planned[index]; + var slide = slides[image.slideIndex]; + final elements = slide.elements!.toList(); final asset = assets[index]; if (asset.bytes case final bytes? when bytes.isNotEmpty) { - final element = elements[image.elementIndex] as Map; - element['source'] = image.assetKey; - element.remove('generationPrompt'); - continue; - } - - elements.removeAt(image.elementIndex); - if (slide['composition'] case final String composition - when composition == 'imageLeft' || - composition == 'imageRight' || - composition == 'imageFullBleed') { - slide['composition'] = 'content'; - slide['treatment'] = 'content'; + elements[image.elementIndex] = elements[image.elementIndex].copyWith( + source: image.assetKey, + generationPrompt: null, + ); + } else { + elements.removeAt(image.elementIndex); + if (slide.composition == 'imageLeft' || + slide.composition == 'imageRight' || + slide.composition == 'imageFullBleed') { + slide = slide.copyWith(composition: 'content', treatment: 'content'); + } } + slides[image.slideIndex] = slide.copyWith(elements: elements); } - return DeckPlanType.parse(rewritten); + return plan.copyWith(slides: slides); } String buildGeneratedAssetKey({ diff --git a/packages/playground/lib/features/ai/quick_agent/core/engine/services/deck_generator_pipeline.dart b/packages/playground/lib/features/ai/quick_agent/core/engine/services/deck_generator_pipeline.dart index 08f29e219..ccef38cc5 100644 --- a/packages/playground/lib/features/ai/quick_agent/core/engine/services/deck_generator_pipeline.dart +++ b/packages/playground/lib/features/ai/quick_agent/core/engine/services/deck_generator_pipeline.dart @@ -8,7 +8,7 @@ extension _DeckGeneratorPipeline on DeckGeneratorService { /// Generates a lightweight presentation outline. /// /// Returns the outline JSON or null on failure. - Future _generateOutline( + Future _generateOutline( GenerationModelCallExecutor executor, String prompt, GenerationTraceEmitter trace, @@ -68,7 +68,6 @@ extension _DeckGeneratorPipeline on DeckGeneratorService { generationConfig: google_ai.GenerationConfig( responseMimeType: 'application/json', responseSchema: adaptResult.schema, - thinkingConfig: google_ai.ThinkingConfig(thinkingBudget: 0), ), ); @@ -194,7 +193,7 @@ extension _DeckGeneratorPipeline on DeckGeneratorService { Future<_SlideCompositionResult?> _composeSlides( GenerationModelCallExecutor executor, String prompt, - DeckPlanType plan, + DeckPlan plan, DeckGenerationRequest request, GenerationTraceEmitter trace, GenerationProgressCallback? onProgress, @@ -228,7 +227,7 @@ extension _DeckGeneratorPipeline on DeckGeneratorService { Future<_SlideCompositionResult?> _composeSlidesBySection( GenerationModelCallExecutor executor, String prompt, - DeckPlanType plan, + DeckPlan plan, DeckGenerationRequest request, GenerationTraceEmitter trace, GenerationProgressCallback? onProgress, @@ -287,8 +286,8 @@ extension _DeckGeneratorPipeline on DeckGeneratorService { Future<_SlideCompositionResult> _composeSection({ required GenerationModelCallExecutor executor, required String originalPrompt, - required DeckPlanType plan, - required DeckPlanSectionType section, + required DeckPlan plan, + required DeckPlanSection section, required int sectionIndex, required DeckGenerationRequest request, required GenerationTraceEmitter trace, @@ -360,7 +359,6 @@ extension _DeckGeneratorPipeline on DeckGeneratorService { generationConfig: google_ai.GenerationConfig( responseMimeType: 'application/json', responseSchema: adaptResult.schema, - thinkingConfig: google_ai.ThinkingConfig(thinkingBudget: 0), ), ); @@ -476,8 +474,8 @@ extension _DeckGeneratorPipeline on DeckGeneratorService { } _SlideCompositionResult _failedSection({ - required DeckPlanType plan, - required List slides, + required DeckPlan plan, + required List slides, required GenerationTraceEmitter trace, required String message, }) { @@ -524,7 +522,7 @@ extension _DeckGeneratorPipeline on DeckGeneratorService { ({Map? canonical, List issues}) _validateSectionSlide({ required Map draft, - required DeckPlanSlideType planSlide, + required DeckPlanSlide planSlide, required DeckGenerationRequest request, }) { var normalized = hydrateGeneratedElementSources( @@ -600,7 +598,7 @@ extension _DeckGeneratorPipeline on DeckGeneratorService { Future<_SlideCompositionResult?> _composeSlidesSequentially( GenerationModelCallExecutor executor, String prompt, - DeckPlanType plan, + DeckPlan plan, DeckGenerationRequest request, GenerationTraceEmitter trace, GenerationProgressCallback? onProgress, @@ -818,12 +816,11 @@ extension _DeckGeneratorPipeline on DeckGeneratorService { Future?> _generateSingleSlide({ required GenerationModelCallExecutor executor, required String originalPrompt, - required DeckPlanType plan, - required DeckPlanSlideType current, + required DeckPlan plan, + required DeckPlanSlide current, required Map? previousSlide, // The final slide intentionally has no next-slide context. - // ignore: avoid-unnecessary-nullable-parameters - required DeckPlanSlideType? next, + required DeckPlanSlide? next, required List validationIssues, required Map? invalidSlide, required int repairAttempt, @@ -864,7 +861,7 @@ extension _DeckGeneratorPipeline on DeckGeneratorService { 'DECK_GEN', 'Slide $slideIndex/$slideCount prompt (${systemPrompt.length} chars)', ); - debugLog.log('DECK_GEN', 'Thinking budget disabled for fast composition'); + debugLog.log('DECK_GEN', 'Thinking level set to minimal for composition'); final request = google_ai.GenerateContentRequest( model: modelName, @@ -880,7 +877,6 @@ extension _DeckGeneratorPipeline on DeckGeneratorService { generationConfig: google_ai.GenerationConfig( responseMimeType: 'application/json', responseSchema: adaptResult.schema, - thinkingConfig: google_ai.ThinkingConfig(thinkingBudget: 0), ), ); diff --git a/packages/playground/lib/features/ai/quick_agent/core/engine/services/deck_generator_pipeline_helpers.dart b/packages/playground/lib/features/ai/quick_agent/core/engine/services/deck_generator_pipeline_helpers.dart index c6028e59e..c6c668b27 100644 --- a/packages/playground/lib/features/ai/quick_agent/core/engine/services/deck_generator_pipeline_helpers.dart +++ b/packages/playground/lib/features/ai/quick_agent/core/engine/services/deck_generator_pipeline_helpers.dart @@ -11,7 +11,7 @@ List> sanitizeGeneratedSlides( Map hydrateGeneratedElementSources({ required Map slide, - required DeckPlanSlideType planSlide, + required DeckPlanSlide planSlide, required GenerationElementCatalog elementCatalog, }) { final hydrated = Map.of(slide); @@ -34,7 +34,7 @@ Map hydrateGeneratedElementSources({ Map _hydrateSectionElementSources( Map rawSection, { - required DeckPlanSlideType planSlide, + required DeckPlanSlide planSlide, required GenerationElementCatalog elementCatalog, }) { final section = Map.from(rawSection); @@ -56,7 +56,7 @@ Map _hydrateSectionElementSources( Map _hydrateBlockElementSource( Map rawBlock, { - required DeckPlanSlideType planSlide, + required DeckPlanSlide planSlide, required GenerationElementCatalog elementCatalog, }) { final block = Map.from(rawBlock); diff --git a/packages/playground/lib/features/ai/quick_agent/core/engine/services/deck_generator_service.dart b/packages/playground/lib/features/ai/quick_agent/core/engine/services/deck_generator_service.dart index 9345e89e5..e6af54107 100644 --- a/packages/playground/lib/features/ai/quick_agent/core/engine/services/deck_generator_service.dart +++ b/packages/playground/lib/features/ai/quick_agent/core/engine/services/deck_generator_service.dart @@ -65,7 +65,7 @@ class DeckGenerationResult { final ResolvedPresentationTheme? theme; /// The validated, mechanically normalized plan used to compose the deck. - final DeckPlanType? plan; + final DeckPlan? plan; /// Ordered slide slots that remain unresolved and can be retried. final List slideFailures; @@ -91,7 +91,7 @@ class DeckGenerationResult { DeckGenerationResult.success({ required List slides, - required DeckPlanType plan, + required DeckPlan plan, required ResolvedPresentationTheme theme, List generatedImages = const [], }) : this._( @@ -107,7 +107,7 @@ class DeckGenerationResult { DeckGenerationResult.partial({ required List slides, required List slideFailures, - required DeckPlanType plan, + required DeckPlan plan, required ResolvedPresentationTheme theme, List generatedImages = const [], }) : this._( @@ -151,13 +151,13 @@ final class _SlideCompositionResult { final class DeckPlanningResult { final bool success; - final DeckPlanType? plan; + final DeckPlan? plan; final String? error; const DeckPlanningResult._({required this.success, this.plan, this.error}); - const DeckPlanningResult.success(DeckPlanType plan) + const DeckPlanningResult.success(DeckPlan plan) : this._(success: true, plan: plan); const DeckPlanningResult.failure(String error) @@ -240,9 +240,9 @@ class DeckGeneratorService { DeckGeneratorService({ required this.apiKey, - this.modelName = GeminiModelNames.gemini31FlashLite, - this.outlineModelName = GeminiModelNames.gemini35Flash, - this.outlineRepairModelName = GeminiModelNames.gemini31FlashLite, + this.modelName = GeminiModelNames.gemini35FlashLite, + this.outlineModelName = GeminiModelNames.gemini37Flash, + this.outlineRepairModelName = GeminiModelNames.gemini35FlashLite, this.sectionBatchThreshold = 5, this.requestTimeout = const Duration(seconds: 45), this.maxOutlineValidationAttempts = 2, @@ -429,7 +429,7 @@ class DeckGeneratorService { /// Composes slides from the exact plan approved by the user. Future generateFromPlan( DeckGenerationRequest request, - DeckPlanType approvedPlan, { + DeckPlan approvedPlan, { GenerationProgressCallback? onProgress, GenerationTraceCallback? onTrace, bool Function()? isCancelled, @@ -594,7 +594,7 @@ class DeckGeneratorService { onProgress?.call(const GenerationProgress(.composingSlides)); final existingSlidesByKey = { for (final slide in partialResult.slides) - slide.key: Map.of(slide.toMap()), + slide.key: Map.of(slide.toJson()), }; final retried = await _composeSlidesSequentially( executor, diff --git a/packages/playground/lib/features/ai/quick_agent/core/engine/services/deck_generator_workflow.dart b/packages/playground/lib/features/ai/quick_agent/core/engine/services/deck_generator_workflow.dart index 3f455571c..af954cd12 100644 --- a/packages/playground/lib/features/ai/quick_agent/core/engine/services/deck_generator_workflow.dart +++ b/packages/playground/lib/features/ai/quick_agent/core/engine/services/deck_generator_workflow.dart @@ -6,7 +6,8 @@ void _logPipelineConfig(DeckGeneratorService owner, {required String prompt}) { 'DECK_GEN', 'Config: outlineModel=${owner.outlineModelName}, ' 'outlineRepairModel=${owner.outlineRepairModelName}, ' - 'slideModel=${owner.modelName}, thinkingBudget=0', + 'slideModel=${owner.modelName}, ' + 'thinkingLevels=outline:low,repair:minimal,slides:minimal', ); debugLog.log('DECK_GEN', 'Prompt (${prompt.length} chars):\n$prompt'); } @@ -17,7 +18,7 @@ int _defaultRepairBudget(int slideCount) { return proportionalBudget < 3 ? 3 : proportionalBudget; } -Future _runOutlinePhase( +Future _runOutlinePhase( DeckGeneratorService owner, { required GenerationModelCallExecutor executor, required String prompt, @@ -68,7 +69,7 @@ Future<_SlideCompositionResult?> _runSlideCompositionPhase( required GenerationModelCallExecutor executor, required String prompt, required DeckGenerationRequest request, - required DeckPlanType outline, + required DeckPlan outline, required GenerationProgressCallback? onProgress, required GenerationTraceEmitter trace, required bool Function()? isCancelled, @@ -116,7 +117,7 @@ Future<_SlideCompositionResult?> _runSlideCompositionPhase( DeckGenerationResult _finalizeDeck( DeckGeneratorService owner, { required _SlideCompositionResult composition, - required DeckPlanType plan, + required DeckPlan plan, List generatedImages = const [], required DateTime pipelineStart, required GenerationProgressCallback? onProgress, diff --git a/packages/playground/lib/features/ai/quick_agent/core/engine/services/deck_plan_repair.dart b/packages/playground/lib/features/ai/quick_agent/core/engine/services/deck_plan_repair.dart index 61bda0316..4113ad2f5 100644 --- a/packages/playground/lib/features/ai/quick_agent/core/engine/services/deck_plan_repair.dart +++ b/packages/playground/lib/features/ai/quick_agent/core/engine/services/deck_plan_repair.dart @@ -14,10 +14,10 @@ bool _onlySlideScopedPlanIssues(List issues) { } extension _DeckPlanRepair on DeckGeneratorService { - Future _repairInvalidOutlineSlides({ + Future _repairInvalidOutlineSlides({ required GenerationModelCallExecutor executor, required String originalPrompt, - required DeckPlanType plan, + required DeckPlan plan, required DeckGenerationRequest request, }) async { var repairedPlan = plan; @@ -85,11 +85,8 @@ extension _DeckPlanRepair on DeckGeneratorService { continue; } - final candidatePlan = _replacePlanSlide( - repairedPlan, - index: index, - slide: candidate, - ); + final slides = repairedPlan.slides.toList()..[index] = candidate; + final candidatePlan = repairedPlan.copyWith(slides: slides); final candidateIssues = validateDeckPlanIssues( candidatePlan, typographyCatalog: typographyCatalog, @@ -110,11 +107,11 @@ extension _DeckPlanRepair on DeckGeneratorService { return repairedPlan; } - Future _generateOutlineSlideRepair({ + Future _generateOutlineSlideRepair({ required GenerationModelCallExecutor executor, required String originalPrompt, - required DeckPlanType plan, - required DeckPlanSlideType current, + required DeckPlan plan, + required DeckPlanSlide current, required List validationIssues, required Map invalidSlide, required int localAttempt, @@ -144,7 +141,6 @@ extension _DeckPlanRepair on DeckGeneratorService { generationConfig: google_ai.GenerationConfig( responseMimeType: 'application/json', responseSchema: adapted.schema, - thinkingConfig: google_ai.ThinkingConfig(thinkingBudget: 0), ), ); @@ -174,7 +170,7 @@ extension _DeckPlanRepair on DeckGeneratorService { ]; slides[slideIndex] = Map.of(parsed); - return DeckPlanSlideType.parse( + return DeckPlanSlide.parse( enrichDeckPlanDraftSlide( slides[slideIndex], index: slideIndex, @@ -222,8 +218,8 @@ void _appendUniqueIssues( } List _outlineSlideInvariantErrors({ - required DeckPlanSlideType original, - required DeckPlanSlideType candidate, + required DeckPlanSlide original, + required DeckPlanSlide candidate, }) { final errors = []; void requireSame(String field, Object before, Object after) { @@ -243,21 +239,8 @@ List _outlineSlideInvariantErrors({ requireSame('density', original.density, candidate.density); requireSame( 'elements', - original.elements ?? const [], - candidate.elements ?? const [], + original.elements ?? const [], + candidate.elements ?? const [], ); return errors; } - -DeckPlanType _replacePlanSlide( - DeckPlanType plan, { - required int index, - required DeckPlanSlideType slide, -}) { - final slides = [ - for (final existing in plan.slides) Map.of(existing), - ]; - slides[index] = Map.of(slide); - final data = Map.of(plan)..['slides'] = slides; - return DeckPlanType.parse(data); -} diff --git a/packages/playground/lib/features/ai/quick_agent/core/engine/services/deck_plan_validator.dart b/packages/playground/lib/features/ai/quick_agent/core/engine/services/deck_plan_validator.dart index e24ec7c0c..aeb9d6077 100644 --- a/packages/playground/lib/features/ai/quick_agent/core/engine/services/deck_plan_validator.dart +++ b/packages/playground/lib/features/ai/quick_agent/core/engine/services/deck_plan_validator.dart @@ -12,7 +12,7 @@ import 'theme_json_serializer.dart'; /// Returns semantic errors that are not expressible in the deck-plan schema. List validateDeckPlan( - DeckPlanType plan, { + DeckPlan plan, { int? expectedSlideCount, PresentationTypographyCatalog? typographyCatalog, PresentationImageStyleCatalog? imageStyleCatalog, @@ -31,7 +31,7 @@ List validateDeckPlan( /// Returns typed semantic issues for pipeline decisions and diagnostics. List validateDeckPlanIssues( - DeckPlanType plan, { + DeckPlan plan, { int? expectedSlideCount, PresentationTypographyCatalog? typographyCatalog, PresentationImageStyleCatalog? imageStyleCatalog, @@ -183,7 +183,7 @@ List validateDeckPlanIssues( } void _validateGeneratedImageIntent( - DeckPlanType plan, + DeckPlan plan, DeckGenerationRequest? request, PresentationImageStyleCatalog imageStyleCatalog, GenerationValidationCollector errors, @@ -200,7 +200,7 @@ void _validateGeneratedImageIntent( location: GenerationValidationLocation.planSlide, slideKey: slide.key, ); - final elements = slide.elements ?? const []; + final elements = slide.elements ?? const []; final imageCount = elements .where((element) => element.type == 'image') .length; @@ -280,7 +280,7 @@ bool _hasResolvedImageStyle( } void _validateMetricIntent( - DeckPlanType plan, + DeckPlan plan, DeckGenerationRequest? request, GenerationValidationCollector errors, ) { @@ -310,7 +310,7 @@ void _validateMetricIntent( } void _validateNumericClaimContext( - DeckPlanType plan, + DeckPlan plan, DeckGenerationRequest? request, GenerationValidationCollector errors, ) { @@ -337,7 +337,7 @@ void _validateNumericClaimContext( } void _validateCommitmentGrounding( - DeckPlanType plan, + DeckPlan plan, DeckGenerationRequest? request, GenerationValidationCollector errors, ) { @@ -392,7 +392,7 @@ void _validateCommitmentGrounding( } void _validateNumericClaimGrounding( - DeckPlanType plan, + DeckPlan plan, DeckGenerationRequest? request, GenerationValidationCollector errors, ) { @@ -419,7 +419,7 @@ void _validateNumericClaimGrounding( } void _validateTreatmentIntent( - DeckPlanType plan, + DeckPlan plan, GenerationValidationCollector errors, ) { for (final slide in plan.slides) { @@ -446,7 +446,7 @@ void _validateTreatmentIntent( } void _validateVisibleSourceGrounding( - DeckPlanType plan, + DeckPlan plan, DeckGenerationRequest? request, GenerationValidationCollector errors, ) { @@ -474,16 +474,13 @@ void _validateVisibleSourceGrounding( } } -List _audienceFacingPlanCopy(DeckPlanSlideType slide) => [ +List _audienceFacingPlanCopy(DeckPlanSlide slide) => [ slide.title, slide.assertion, ...slide.contentUnits, ]; -void _validateSections( - DeckPlanType plan, - GenerationValidationCollector errors, -) { +void _validateSections(DeckPlan plan, GenerationValidationCollector errors) { if (plan.sections.isEmpty) { errors.add('Deck plan has no narrative sections.'); return; @@ -536,7 +533,7 @@ void _validateSections( } void _validateTheme( - DeckPlanType plan, + DeckPlan plan, PresentationThemeCatalog themeCatalog, PresentationTypographyCatalog typographyCatalog, DeckGenerationRequest? request, @@ -584,12 +581,13 @@ void _validateTheme( } void _validateElementGrounding( - DeckPlanType plan, + DeckPlan plan, DeckGenerationRequest? request, Set knownGeneratedAssetKeys, GenerationValidationCollector errors, ) { for (final slide in plan.slides) { + final elements = slide.elements ?? const []; final requiredType = switch (slide.composition) { 'imageLeft' || 'imageRight' || 'imageFullBleed' => 'image', 'webview' => 'webview', @@ -598,9 +596,7 @@ void _validateElementGrounding( _ => null, }; if (requiredType != null && - !(slide.elements ?? const []).any( - (element) => element.type == requiredType, - )) { + !elements.any((element) => element.type == requiredType)) { final slideErrors = errors.scoped( location: GenerationValidationLocation.planSlide, slideKey: slide.key, @@ -610,6 +606,26 @@ void _validateElementGrounding( 'does not plan the required $requiredType element.', ); } + + for (final element in elements) { + final hasCompatibleComposition = switch (element.type) { + 'image' => + slide.composition == 'imageLeft' || + slide.composition == 'imageRight' || + slide.composition == 'imageFullBleed', + 'webview' || 'dartpad' || 'custom' => slide.composition == element.type, + _ => true, + }; + if (hasCompatibleComposition) continue; + final slideErrors = errors.scoped( + location: GenerationValidationLocation.planSlide, + slideKey: slide.key, + ); + slideErrors.add( + 'Slide "${slide.key}" plans an ${element.type} element but uses ' + 'incompatible composition "${slide.composition}".', + ); + } } if (request == null) return; @@ -621,7 +637,7 @@ void _validateElementGrounding( location: GenerationValidationLocation.planSlide, slideKey: slide.key, ); - for (final element in slide.elements ?? const []) { + for (final element in slide.elements ?? const []) { final source = element.source; if (source == null || source.trim().isEmpty) continue; if (knownGeneratedAssetKeys.contains(source)) continue; @@ -680,7 +696,7 @@ bool _isAudienceHandoffElement(String type) => type == 'webview' || type == 'dartpad' || type == 'custom'; void _validateDesignRhythm( - DeckPlanType plan, + DeckPlan plan, GenerationValidationCollector errors, ) { _rejectLongRuns( diff --git a/packages/playground/lib/features/ai/quick_agent/core/engine/services/deck_theme_resolution.dart b/packages/playground/lib/features/ai/quick_agent/core/engine/services/deck_theme_resolution.dart index 7550fc38b..129493157 100644 --- a/packages/playground/lib/features/ai/quick_agent/core/engine/services/deck_theme_resolution.dart +++ b/packages/playground/lib/features/ai/quick_agent/core/engine/services/deck_theme_resolution.dart @@ -50,7 +50,7 @@ List themeCandidatesForRequest({ } /// Parses a model-facing draft and attaches the canonical theme reference. -DeckPlanType resolveDeckPlanDraft({ +DeckPlan resolveDeckPlanDraft({ required Map draft, required List candidates, required DeckGenerationRequest request, @@ -85,7 +85,7 @@ DeckPlanType resolveDeckPlanDraft({ .map((slide) => Map.from(slide! as Map)) .toList(growable: false); - return DeckPlanType.parse( + return DeckPlan.parse( Map.of(parsed) ..['theme'] = reference ..['slides'] = [ @@ -111,7 +111,7 @@ Map enrichDeckPlanDraftSlide( final role = slide['narrativeRole']! as String; final composition = slide['composition']! as String; - return Map.of(slide) + return Map.of(slide) ..['purpose'] = assertion ..['contentBrief'] = contentUnits.join(' ') ..['continuity'] = _deriveContinuity(index, slides) @@ -200,11 +200,11 @@ Map buildDeckThemeReference({ /// Resolves a generated canonical reference into renderer-ready theme values. ResolvedPresentationTheme resolveDeckThemeReference( - DeckThemeReferenceType theme, { + DeckThemeReference theme, { required PresentationThemeCatalog themeCatalog, required PresentationTypographyCatalog typographyCatalog, }) => resolveDeckThemeMap( - Map.of(theme), + theme.toJson(), themeCatalog: themeCatalog, typographyCatalog: typographyCatalog, ); @@ -215,7 +215,7 @@ ResolvedPresentationTheme resolveDeckThemeMap( required PresentationThemeCatalog themeCatalog, required PresentationTypographyCatalog typographyCatalog, }) { - final parsed = DeckThemeReferenceType.parse(theme); + final parsed = DeckThemeReference.parse(theme); final override = parsed.brandOverride; final colors = override?.colors; final fonts = override?.fonts; diff --git a/packages/playground/lib/features/ai/quick_agent/core/engine/services/error_classifier.dart b/packages/playground/lib/features/ai/quick_agent/core/engine/services/error_classifier.dart index 5be6c7e4a..d0e3bc739 100644 --- a/packages/playground/lib/features/ai/quick_agent/core/engine/services/error_classifier.dart +++ b/packages/playground/lib/features/ai/quick_agent/core/engine/services/error_classifier.dart @@ -1,3 +1,5 @@ +import 'package:googleai_dart/googleai_dart.dart' as google_ai; + /// Classified error categories for user-facing messages. enum ErrorCategory { /// Rate limiting, quota exhaustion, or service overload. @@ -83,17 +85,23 @@ class ErrorClassifier { /// Classifies an error into a user-friendly category. /// - /// Examines the error's string representation (case-insensitive) - /// for known patterns. Returns [ErrorCategory.unknown] if no patterns match. + /// Uses structured SDK status codes when available, then falls back to + /// case-insensitive message patterns for other errors. + /// Returns [ErrorCategory.unknown] if neither identifies a category. ErrorCategory classify(Object error) { + final statusCategory = switch (error) { + google_ai.ApiException(statusCode: 401 || 403) => + ErrorCategory.authentication, + google_ai.ApiException(statusCode: 408 || 504) => ErrorCategory.network, + google_ai.ApiException(statusCode: 429) => ErrorCategory.rateLimit, + _ => null, + }; + if (statusCategory != null) return statusCategory; + final errorString = error.toString().toLowerCase(); for (final entry in _patterns.entries) { - for (final pattern in entry.value) { - if (errorString.contains(pattern)) { - return entry.key; - } - } + if (entry.value.any(errorString.contains)) return entry.key; } return ErrorCategory.unknown; diff --git a/packages/playground/lib/features/ai/quick_agent/core/engine/services/generated_slide_validator.dart b/packages/playground/lib/features/ai/quick_agent/core/engine/services/generated_slide_validator.dart index 43ecf7928..2f483748d 100644 --- a/packages/playground/lib/features/ai/quick_agent/core/engine/services/generated_slide_validator.dart +++ b/packages/playground/lib/features/ai/quick_agent/core/engine/services/generated_slide_validator.dart @@ -8,14 +8,16 @@ import 'generation_element_catalog.dart'; import 'generation_validation_issue.dart'; import 'source_grounding.dart'; -/// Applies renderer-safe mechanical normalization without rewriting content. +/// Applies renderer-safe mechanical normalization from the approved plan. /// /// Models occasionally introduce H1 while repairing another heading rule. The /// planned treatment already determines whether H1 is legal, so demoting that /// marker to H2 is deterministic and avoids spending another model request. +/// Likewise, an overlong title H1 falls back to the approved concise title so +/// the hero treatment cannot clip while rendering. Map normalizeGeneratedSlideForPlan({ required Map rawSlide, - required DeckPlanSlideType planSlide, + required DeckPlanSlide planSlide, }) { final normalized = Map.of(rawSlide); final rawOptions = rawSlide['options']; @@ -54,7 +56,7 @@ Map normalizeGeneratedSlideForPlan({ Map _normalizeImageSplitFlex( Map slide, - DeckPlanSlideType planSlide, + DeckPlanSlide planSlide, ) { if (planSlide.composition != 'imageLeft' && planSlide.composition != 'imageRight') { @@ -81,26 +83,40 @@ Map _normalizeImageSplitFlex( Map _normalizeBlockForPlan( Map block, - DeckPlanSlideType planSlide, + DeckPlanSlide planSlide, ) { var normalized = _planPermitsH1(planSlide) ? block : _normalizeBlockHeading(block); if (planSlide.composition == 'title') { - normalized = _flattenTitleListMarkers(normalized); + normalized = _normalizeTitleBlock(normalized, planSlide); } return normalized; } -Map _flattenTitleListMarkers(Map block) { +Map _normalizeTitleBlock( + Map block, + DeckPlanSlide planSlide, +) { if (block['type'] != ContentBlock.key || block['content'] is! String) { return block; } - block['content'] = (block['content'] as String).replaceAllMapped( + var content = (block['content'] as String).replaceAllMapped( RegExp(r'^(\s*)(?:[-+*]|\d+[.)])\s+', multiLine: true), (match) => match.group(1)!, ); + final plannedTitle = planSlide.title.trim(); + final plannedTitleWords = _displayHeadingWordCount(plannedTitle); + if (plannedTitleWords > 0 && plannedTitleWords <= 8) { + content = content.replaceAllMapped( + RegExp(r'^([ \t]*)#[ \t]+(.+?)[ \t]*$', multiLine: true), + (match) => _displayHeadingWordCount(match.group(2)!) > 8 + ? '${match.group(1)!}# $plannedTitle' + : match.group(0)!, + ); + } + block['content'] = content; return block; } @@ -128,7 +144,7 @@ Map removeInvalidOptionalSpeakerComments({ Map _normalizeImplicitVerticalAlignment( Map slide, - DeckPlanSlideType planSlide, + DeckPlanSlide planSlide, ) { const supportedCompositions = { 'content', @@ -207,7 +223,7 @@ List validateGeneratedSlide({ required String expectedKey, required Map rawSlide, required GenerationElementCatalog elementCatalog, - DeckPlanSlideType? planSlide, + DeckPlanSlide? planSlide, DeckGenerationRequest? request, }) => validateGeneratedSlideIssues( expectedKey: expectedKey, @@ -222,7 +238,7 @@ List validateGeneratedSlideIssues({ required String expectedKey, required Map rawSlide, required GenerationElementCatalog elementCatalog, - DeckPlanSlideType? planSlide, + DeckPlanSlide? planSlide, DeckGenerationRequest? request, }) { final issues = GenerationValidationCollector( @@ -316,7 +332,7 @@ List _validateRawDraftStructure(Map rawSlide) { List _validatePlanFulfillment( Slide slide, - DeckPlanSlideType planSlide, + DeckPlanSlide planSlide, DeckGenerationRequest? request, ) { final errors = GenerationValidationCollector( @@ -366,7 +382,7 @@ List _validatePlanFulfillment( } final expectedWidgetCounts = {}; - for (final element in planSlide.elements ?? const []) { + for (final element in planSlide.elements ?? const []) { final name = element.type == 'custom' ? element.widgetName : element.type; if (name == null || name.trim().isEmpty) continue; expectedWidgetCounts.update(name, (count) => count + 1, ifAbsent: () => 1); @@ -617,12 +633,9 @@ List _validatePlanFulfillment( return errors.issues.uniqueIssues; } -List _validateHandoffPurpose( - String markdown, - DeckPlanSlideType planSlide, -) { +List _validateHandoffPurpose(String markdown, DeckPlanSlide planSlide) { final errors = []; - for (final element in planSlide.elements ?? const []) { + for (final element in planSlide.elements ?? const []) { if (!_requiresVisibleHandoffPurpose(element.type)) continue; final missingTerms = findMissingGroundedPurposeTerms( purpose: element.purpose, @@ -720,7 +733,7 @@ List _validateNumericClaimGrounding( List _validateVisibleSourceGrounding( Iterable values, - DeckPlanSlideType planSlide, { + DeckPlanSlide planSlide, { required String label, }) { final allowedDomains = extractReferencedDomains([ @@ -730,7 +743,7 @@ List _validateVisibleSourceGrounding( ...planSlide.contentUnits, planSlide.contentBrief, planSlide.continuity, - for (final element in planSlide.elements ?? const []) + for (final element in planSlide.elements ?? const []) ?element.source, ]); final ungrounded = extractReferencedDomains([ @@ -756,10 +769,7 @@ List _validateDisplayHeadings( if (match == null) continue; final level = match.group(1)!.length; final heading = match.group(2)!; - final wordCount = RegExp( - r"[\p{L}\p{N}]+(?:['’\-][\p{L}\p{N}]+)*", - unicode: true, - ).allMatches(heading).length; + final wordCount = _displayHeadingWordCount(heading); if (wordCount > 8) { errors.add( 'Display heading "$heading" has $wordCount words; use at most 8.', @@ -776,7 +786,12 @@ List _validateDisplayHeadings( return errors; } -bool _planPermitsH1(DeckPlanSlideType planSlide) => +int _displayHeadingWordCount(String value) => RegExp( + r"[\p{L}\p{N}]+(?:['’\-][\p{L}\p{N}]+)*", + unicode: true, +).allMatches(value).length; + +bool _planPermitsH1(DeckPlanSlide planSlide) => _permitsH1(planSlide.composition); bool _permitsH1(String composition) => diff --git a/packages/playground/lib/features/ai/quick_agent/core/engine/services/generation_model_client.dart b/packages/playground/lib/features/ai/quick_agent/core/engine/services/generation_model_client.dart index 0d3807450..2d1fe6b7b 100644 --- a/packages/playground/lib/features/ai/quick_agent/core/engine/services/generation_model_client.dart +++ b/packages/playground/lib/features/ai/quick_agent/core/engine/services/generation_model_client.dart @@ -1,5 +1,10 @@ +import 'dart:convert'; + import 'package:google_cloud_ai_generativelanguage_v1beta/generativelanguage.dart' as google_ai; +import 'package:googleai_dart/googleai_dart.dart' as modern_google_ai; + +import '../../constants/gemini_models.dart'; /// Boundary around the model transport used by deck generation. /// @@ -19,16 +24,68 @@ typedef GenerationModelClientFactory = /// Production client backed by Google Generative Language. final class GoogleGenerationModelClient implements GenerationModelClient { - GoogleGenerationModelClient.fromApiKey(String apiKey) - : _service = google_ai.GenerativeService.fromApiKey(apiKey); + final modern_google_ai.GoogleAIClient _client; - final google_ai.GenerativeService _service; + GoogleGenerationModelClient.fromApiKey(String apiKey) + : _client = modern_google_ai.GoogleAIClient( + config: modern_google_ai.GoogleAIConfig.googleAI( + authProvider: modern_google_ai.ApiKeyProvider(apiKey), + retryPolicy: const modern_google_ai.RetryPolicy(maxRetries: 0), + ), + ); @override - void close() => _service.close(); + void close() => _client.close(); @override Future generateContent( google_ai.GenerateContentRequest request, - ) => _service.generateContent(request); + ) async { + final adapted = adaptGenerationRequest(request); + final response = await _client.models.generateContent( + model: adapted.model, + request: adapted.request, + ); + + return google_ai.GenerateContentResponse.fromJson(response.toJson()); + } +} + +/// A request prepared for the current Gemini Developer API transport. +typedef AdaptedGenerationRequest = ({ + String model, + modern_google_ai.GenerateContentRequest request, +}); + +/// Converts the generated v1beta request types used by the schema adapter to +/// the current transport types, including Gemini 3 thinking levels. +/// +/// The generated client does not yet expose `thinkingLevel`, while Gemini 3.7 +/// rejects the legacy `thinkingBudget` setting. Keeping the conversion here +/// lets the deck pipeline retain its strongly typed schemas without sending a +/// stale request shape to Google. +AdaptedGenerationRequest adaptGenerationRequest( + google_ai.GenerateContentRequest request, +) { + final json = jsonDecode(jsonEncode(request.toJson())) as Map; + json.remove('model'); + + final generationConfig = switch (json['generationConfig']) { + final Map value => value, + _ => {}, + }; + final thinkingLevel = switch (request.model) { + GeminiModelNames.gemini37Flash => 'LOW', + GeminiModelNames.gemini35FlashLite => 'MINIMAL', + _ => null, + }; + if (thinkingLevel != null) { + generationConfig['thinkingConfig'] = {'thinkingLevel': thinkingLevel}; + } + json['generationConfig'] = generationConfig; + + return ( + model: request.model.replaceFirst(RegExp(r'^models/'), ''), + request: modern_google_ai.GenerateContentRequest.fromJson(json), + ); } diff --git a/packages/playground/lib/features/ai/quick_agent/core/engine/services/generation_quality_report.dart b/packages/playground/lib/features/ai/quick_agent/core/engine/services/generation_quality_report.dart index c61814722..b31af3e03 100644 --- a/packages/playground/lib/features/ai/quick_agent/core/engine/services/generation_quality_report.dart +++ b/packages/playground/lib/features/ai/quick_agent/core/engine/services/generation_quality_report.dart @@ -81,7 +81,7 @@ final class GenerationQualityReport { factory GenerationQualityReport.evaluate({ required DeckGenerationRequest request, - required DeckPlanType plan, + required DeckPlan plan, required List slides, required List traces, required int replayedSlideCount, diff --git a/packages/playground/lib/features/ai/quick_agent/core/engine/services/theme_json_serializer.dart b/packages/playground/lib/features/ai/quick_agent/core/engine/services/theme_json_serializer.dart index 94ec88e2f..b9e6c65f0 100644 --- a/packages/playground/lib/features/ai/quick_agent/core/engine/services/theme_json_serializer.dart +++ b/packages/playground/lib/features/ai/quick_agent/core/engine/services/theme_json_serializer.dart @@ -2,7 +2,7 @@ import '../schemas/deck_schemas.dart'; import '../schemas/outline_schema.dart'; /// Serializes the canonical theme reference for persisted artifacts. -Map serializeDeckThemeReference(DeckThemeReferenceType theme) { +Map serializeDeckThemeReference(DeckThemeReference theme) { final result = { 'id': theme.id, 'version': theme.version, @@ -35,21 +35,21 @@ Map serializeDeckThemeReference(DeckThemeReferenceType theme) { /// Compact semantic reference supplied to each slide-composition request. Map serializeDeckThemeForSlidePrompt( - DeckThemeReferenceType theme, + DeckThemeReference theme, ) => {'id': theme.id, 'version': theme.version, 'density': theme.density}; /// Projects a canonical plan back into the model's repair-only draft shape. -Map serializeDeckPlanDraftForRepair(DeckPlanType plan) => { +Map serializeDeckPlanDraftForRepair(DeckPlan plan) => { 'topic': plan.topic, 'story': plan.story, 'theme': {'id': plan.theme.id}, - 'sections': [for (final section in plan.sections) Map.of(section)], + 'sections': [for (final section in plan.sections) section.toJson()], 'slides': [ for (final slide in plan.slides) serializeDeckPlanSlideDraft(slide), ], }; -Map serializeDeckPlanSlideDraft(DeckPlanSlideType slide) => { +Map serializeDeckPlanSlideDraft(DeckPlanSlide slide) => { 'key': slide.key, 'title': slide.title, 'sectionKey': slide.sectionKey, @@ -58,5 +58,5 @@ Map serializeDeckPlanSlideDraft(DeckPlanSlideType slide) => { 'narrativeRole': slide.narrativeRole, 'composition': slide.composition, if (slide.elements case final elements?) - 'elements': [for (final element in elements) Map.of(element)], + 'elements': [for (final element in elements) element.toJson()], }; diff --git a/packages/playground/lib/features/ai/quick_agent/presentation/pages/generation_lab_page.dart b/packages/playground/lib/features/ai/quick_agent/presentation/pages/generation_lab_page.dart index 75d71b21d..d843dcb0b 100644 --- a/packages/playground/lib/features/ai/quick_agent/presentation/pages/generation_lab_page.dart +++ b/packages/playground/lib/features/ai/quick_agent/presentation/pages/generation_lab_page.dart @@ -46,7 +46,7 @@ class _GenerationLabPageState extends State { late final DeckGeneratorService? _service; _GenerationPreset _preset = _presets.first; GenerationProgress _progress = const GenerationProgress(GenerationPhase.idle); - DeckPlanType? _plan; + DeckPlan? _plan; DeckGenerationResult? _result; Duration? _planningDuration; Duration? _compositionDuration; @@ -515,7 +515,7 @@ class _ColorSwatch extends StatelessWidget { class _StoryBeatReview extends StatelessWidget { const _StoryBeatReview({required this.plan}); - final DeckPlanType plan; + final DeckPlan plan; @override Widget build(BuildContext context) { diff --git a/packages/playground/lib/features/ai/wizard/core/ai/catalog/ask_user_checkbox.ack.dart b/packages/playground/lib/features/ai/wizard/core/ai/catalog/ask_user_checkbox.ack.dart new file mode 100644 index 000000000..f8c55d91f --- /dev/null +++ b/packages/playground/lib/features/ai/wizard/core/ai/catalog/ask_user_checkbox.ack.dart @@ -0,0 +1,181 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// dart format width=80 + +part of 'ask_user_checkbox.dart'; + +// ************************************************************************** +// AckModelGenerator +// ************************************************************************** + +final class _AskUserCheckboxCopyWithUnset { + const _AskUserCheckboxCopyWithUnset(); +} + +/// Immutable model generated from `_askUserCheckboxSchema`. +/// A question with checkbox items. User selects one or more items. +@AckInfer.jsonSerializable +final class AskUserCheckbox { + AskUserCheckbox({ + required this.question, + this.description, + required List items, + List? selectedItems, + this.minSelections, + this.maxSelections, + required this.action, + }) : items = List.unmodifiable(items.map((item) => item)), + selectedItems = switch (selectedItems) { + null => null, + final fieldValue => List.unmodifiable( + fieldValue.map((item) => item), + ), + }; + + factory AskUserCheckbox.parse(Object? input) { + return $ack.parse(input); + } + + factory AskUserCheckbox.fromJson(Map json) { + return $ack.parse(json); + } + + static const _AskUserCheckboxCopyWithUnset _ackCopyWithUnset = + _AskUserCheckboxCopyWithUnset(); + + /// The question to display to the user + final String question; + + /// Additional context or instructions + final String? description; + + /// Unique, non-empty checkbox items for multiple selection + final List items; + + /// Initially selected items + final List? selectedItems; + + /// Minimum selections required, default 1 + final int? minSelections; + + /// Maximum selections allowed + final int? maxSelections; + + final GenUiAction action; + + static final $ack = AckModelAdapter( + schema: () => _askUserCheckboxSchema, + fromRuntime: AskUserCheckbox._fromAckRuntime, + toRuntime: (model) => model._toAckRuntime(), + ); + + static SchemaResult safeParse(Object? input) => + $ack.safeParse(input); + + Map toJson() => Map.from($ack.encode(this)); + + SchemaResult> safeToJson() => $ack.safeEncode(this); + + AskUserCheckbox copyWith({ + String? question, + Object? description = _ackCopyWithUnset, + List? items, + Object? selectedItems = _ackCopyWithUnset, + Object? minSelections = _ackCopyWithUnset, + Object? maxSelections = _ackCopyWithUnset, + GenUiAction? action, + }) => AskUserCheckbox( + question: question ?? this.question, + description: identical(description, _ackCopyWithUnset) + ? this.description + : description as String?, + items: items ?? this.items, + selectedItems: identical(selectedItems, _ackCopyWithUnset) + ? this.selectedItems + : selectedItems as List?, + minSelections: identical(minSelections, _ackCopyWithUnset) + ? this.minSelections + : minSelections as int?, + maxSelections: identical(maxSelections, _ackCopyWithUnset) + ? this.maxSelections + : maxSelections as int?, + action: action ?? this.action, + ); + + @override + bool operator ==(Object other) => + identical(this, other) || + (other is AskUserCheckbox && + runtimeType == other.runtimeType && + deepEquals(question, other.question) && + deepEquals(description, other.description) && + deepEquals(items, other.items) && + deepEquals(selectedItems, other.selectedItems) && + deepEquals(minSelections, other.minSelections) && + deepEquals(maxSelections, other.maxSelections) && + deepEquals(action, other.action)); + + @override + int get hashCode => Object.hashAll([ + runtimeType, + deepHashCode(question), + deepHashCode(description), + deepHashCode(items), + deepHashCode(selectedItems), + deepHashCode(minSelections), + deepHashCode(maxSelections), + deepHashCode(action), + ]); + + @override + String toString() => + 'AskUserCheckbox(question: $question, description: $description, items: $items, selectedItems: $selectedItems, minSelections: $minSelections, maxSelections: $maxSelections, action: $action)'; + + static AskUserCheckbox _fromAckRuntime(Map value) => + _$AskUserCheckboxFromJson(Map.from(value)); + + Map _toAckRuntime() => { + ..._$AskUserCheckboxToJson(this), + }; + + static String _ackFromRuntimeQuestion(Object? value) => value as String; + + static Object? _ackToRuntimeQuestion(String value) => value; + + static String? _ackFromRuntimeDescription(Object? value) => value as String?; + + static Object? _ackToRuntimeDescription(String? value) => value; + + static List _ackFromRuntimeItems(Object? value) => + (value as List).map((item) => item as String).toList(); + + static Object? _ackToRuntimeItems(List value) => + value.map((item) => item).toList(growable: false); + + static List? _ackFromRuntimeSelectedItems(Object? value) => + switch (value) { + null => null, + final fieldValue => + (fieldValue as List).map((item) => item as String).toList(), + }; + + static Object? _ackToRuntimeSelectedItems(List? value) => + switch (value) { + null => null, + final fieldValue => + fieldValue.map((item) => item).toList(growable: false), + }; + + static int? _ackFromRuntimeMinSelections(Object? value) => value as int?; + + static Object? _ackToRuntimeMinSelections(int? value) => value; + + static int? _ackFromRuntimeMaxSelections(Object? value) => value as int?; + + static Object? _ackToRuntimeMaxSelections(int? value) => value; + + static GenUiAction _ackFromRuntimeAction(Object? value) => + GenUiAction.$ack.fromRuntime(value as Map); + + static Object? _ackToRuntimeAction(GenUiAction value) => + GenUiAction.$ack.toRuntime(value); +} diff --git a/packages/playground/lib/features/ai/wizard/core/ai/catalog/ask_user_checkbox.ack.g.dart b/packages/playground/lib/features/ai/wizard/core/ai/catalog/ask_user_checkbox.ack.g.dart new file mode 100644 index 000000000..5b211fc47 --- /dev/null +++ b/packages/playground/lib/features/ai/wizard/core/ai/catalog/ask_user_checkbox.ack.g.dart @@ -0,0 +1,46 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// dart format width=80 + +part of 'ask_user_checkbox.dart'; + +// ************************************************************************** +// AckJsonSerializableGenerator +// ************************************************************************** + +AskUserCheckbox _$AskUserCheckboxFromJson(Map json) => + AskUserCheckbox( + question: AskUserCheckbox._ackFromRuntimeQuestion(json['question']), + description: AskUserCheckbox._ackFromRuntimeDescription( + json['description'], + ), + items: AskUserCheckbox._ackFromRuntimeItems(json['items']), + selectedItems: AskUserCheckbox._ackFromRuntimeSelectedItems( + json['selectedItems'], + ), + minSelections: AskUserCheckbox._ackFromRuntimeMinSelections( + json['minSelections'], + ), + maxSelections: AskUserCheckbox._ackFromRuntimeMaxSelections( + json['maxSelections'], + ), + action: AskUserCheckbox._ackFromRuntimeAction(json['action']), + ); + +Map _$AskUserCheckboxToJson(AskUserCheckbox instance) => + { + 'question': AskUserCheckbox._ackToRuntimeQuestion(instance.question), + 'description': ?AskUserCheckbox._ackToRuntimeDescription( + instance.description, + ), + 'items': AskUserCheckbox._ackToRuntimeItems(instance.items), + 'selectedItems': ?AskUserCheckbox._ackToRuntimeSelectedItems( + instance.selectedItems, + ), + 'minSelections': ?AskUserCheckbox._ackToRuntimeMinSelections( + instance.minSelections, + ), + 'maxSelections': ?AskUserCheckbox._ackToRuntimeMaxSelections( + instance.maxSelections, + ), + 'action': AskUserCheckbox._ackToRuntimeAction(instance.action), + }; diff --git a/packages/playground/lib/features/ai/wizard/core/ai/catalog/ask_user_checkbox.dart b/packages/playground/lib/features/ai/wizard/core/ai/catalog/ask_user_checkbox.dart index 26fde2716..9893e76b1 100644 --- a/packages/playground/lib/features/ai/wizard/core/ai/catalog/ask_user_checkbox.dart +++ b/packages/playground/lib/features/ai/wizard/core/ai/catalog/ask_user_checkbox.dart @@ -8,46 +8,93 @@ import 'package:remix/remix.dart'; import '../schemas/genui_action_schema.dart'; import 'user_action_dispatch.dart'; -import '../../debug_logger.dart'; -import '../../ui/ui.dart'; import 'ask_user_question_cards.dart'; import 'catalog_question_step.dart'; import 'component_schema.dart'; import 'typed_catalog_item.dart'; -part 'ask_user_checkbox.g.dart'; +part 'ask_user_checkbox.ack.dart'; +part 'ask_user_checkbox.ack.g.dart'; // ─────────────────────────────────── SCHEMA ─────────────────────────────────── +const _defaultMinSelections = 1; + /// Schema for AskUserCheckbox component. /// /// Displays a question with checkbox items for multiple selection. -@AckType(name: 'AskUserCheckbox') -final _askUserCheckboxSchema = Ack.object({ - 'question': Ack.string().describe('The question to display to the user'), - 'description': Ack.string().optional().describe( - 'Additional context or instructions', - ), - 'items': Ack.list( - Ack.string(), - ).describe('Checkbox items as strings for multiple selection'), - 'selectedItems': Ack.list( - Ack.string(), - ).optional().describe('Initially selected items'), - 'minSelections': Ack.integer().optional().describe( - 'Minimum selections required, default 1', - ), - 'maxSelections': Ack.integer().optional().describe( - 'Maximum selections allowed', - ), - 'action': actionSchema, -}).describe('A question with checkbox items. User selects one or more items.'); +@AckInfer(name: 'AskUserCheckbox') +final _askUserCheckboxSchema = + Ack.object({ + 'question': Ack.string().describe( + 'The question to display to the user', + ), + 'description': Ack.string().optional().describe( + 'Additional context or instructions', + ), + 'items': Ack.list(Ack.string().minLength(1)) + .nonEmpty() + .unique() + .describe( + 'Unique, non-empty checkbox items for multiple selection', + ), + 'selectedItems': Ack.list( + Ack.string(), + ).unique().optional().describe('Initially selected items'), + 'minSelections': Ack.integer() + .min(0) + .optional() + .describe('Minimum selections required, default 1'), + 'maxSelections': Ack.integer() + .min(0) + .optional() + .describe('Maximum selections allowed'), + 'action': actionSchema, + }) + .withConstraint(const _CheckboxSelectionConstraint()) + .describe( + 'A question with checkbox items. User selects one or more items.', + ); + +final class _CheckboxSelectionConstraint + extends Constraint> + with Validator> { + const _CheckboxSelectionConstraint() + : super( + constraintKey: 'checkbox_selection_relationships', + description: 'Checkbox selections and bounds must match the items.', + ); + + @override + bool isValid(Map value) { + final items = value['items']! as List; + final selectedItems = value['selectedItems'] as List?; + final minSelections = + value['minSelections'] as int? ?? _defaultMinSelections; + final maxSelections = value['maxSelections'] as int? ?? items.length; + + if (maxSelections > items.length || minSelections > maxSelections) { + return false; + } + + return selectedItems == null || + (selectedItems.length <= maxSelections && + selectedItems.every(items.contains)); + } + + @override + String buildMessage(Map value) { + return 'Selections must belong to items; bounds must fit the item count, ' + 'minimum must not exceed maximum, and the initial selection must not ' + 'exceed the maximum.'; + } +} // ─────────────────────────────────── CATALOG ITEM ─────────────────────────────────── /// AskUserCheckbox catalog component for multiple-selection questions. -final askUserCheckbox = typedCatalogItem( +final askUserCheckbox = typedCatalogItem( name: 'AskUserCheckbox', dataSchema: componentSchema(_askUserCheckboxSchema.toJsonSchemaBuilder()), exampleData: [ @@ -66,7 +113,7 @@ final askUserCheckbox = typedCatalogItem( ] ''', ], - parse: AskUserCheckboxType.parse, + parse: AskUserCheckbox.parse, widgetBuilder: (context, data) => _AskUserCheckboxContent(data: data, itemContext: context), ); @@ -74,7 +121,7 @@ final askUserCheckbox = typedCatalogItem( // ─────────────────────────────────── WIDGET ─────────────────────────────────── class _AskUserCheckboxContent extends StatefulWidget { - final AskUserCheckboxType data; + final AskUserCheckbox data; final CatalogItemContext itemContext; const _AskUserCheckboxContent({ @@ -97,7 +144,7 @@ class _AskUserCheckboxContentState extends State<_AskUserCheckboxContent> { } bool get _canSubmit { - final minSelections = widget.data.minSelections ?? 1; + final minSelections = widget.data.minSelections ?? _defaultMinSelections; final maxSelections = widget.data.maxSelections; final count = _selectedChoices.length; if (count < minSelections) return false; @@ -116,26 +163,10 @@ class _AskUserCheckboxContentState extends State<_AskUserCheckboxContent> { contextBuilder: _buildActionContext, ); - @override - Widget build(BuildContext context) { - return CatalogQuestionStep( - question: widget.data.question, - description: widget.data.description, - body: _buildItems(), - canSubmit: _canSubmit, - onSubmit: _submitAction, - ); - } - Widget _buildItems() { final items = widget.data.items; final column = FlexBoxStyler().column().spacing(8); - if (items.isEmpty) { - debugLog.log('AskUserCheckbox', 'WARNING: checkbox input has no items.'); - return const SdBody('No options available'); - } - return column( children: items.map((choice) { final isSelected = _selectedChoices.contains(choice); @@ -155,4 +186,15 @@ class _AskUserCheckboxContentState extends State<_AskUserCheckboxContent> { }).toList(), ); } + + @override + Widget build(BuildContext context) { + return CatalogQuestionStep( + question: widget.data.question, + description: widget.data.description, + body: _buildItems(), + canSubmit: _canSubmit, + onSubmit: _submitAction, + ); + } } diff --git a/packages/playground/lib/features/ai/wizard/core/ai/catalog/ask_user_checkbox.g.dart b/packages/playground/lib/features/ai/wizard/core/ai/catalog/ask_user_checkbox.g.dart deleted file mode 100644 index d0bf7c27a..000000000 --- a/packages/playground/lib/features/ai/wizard/core/ai/catalog/ask_user_checkbox.g.dart +++ /dev/null @@ -1,44 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND -// dart format width=80 - -// ************************************************************************** -// AckSchemaGenerator -// ************************************************************************** - -part of 'ask_user_checkbox.dart'; - -List _$ackListCast(Object? value) => (value as List).cast(); - -/// Extension type for AskUserCheckbox -extension type AskUserCheckboxType(Map _data) - implements Map { - static AskUserCheckboxType parse(Object? data) { - return _askUserCheckboxSchema.parseAs( - data, - (validated) => AskUserCheckboxType(validated as Map), - ); - } - - static SchemaResult safeParse(Object? data) { - return _askUserCheckboxSchema.safeParseAs( - data, - (validated) => AskUserCheckboxType(validated as Map), - ); - } - - String get question => _data['question'] as String; - - String? get description => _data['description'] as String?; - - List get items => _$ackListCast(_data['items']); - - List? get selectedItems => _data['selectedItems'] != null - ? _$ackListCast(_data['selectedItems']) - : null; - - int? get minSelections => _data['minSelections'] as int?; - - int? get maxSelections => _data['maxSelections'] as int?; - - ActionType get action => ActionType(_data['action'] as Map); -} diff --git a/packages/playground/lib/features/ai/wizard/core/ai/catalog/ask_user_image_style.ack.dart b/packages/playground/lib/features/ai/wizard/core/ai/catalog/ask_user_image_style.ack.dart new file mode 100644 index 000000000..e98742d57 --- /dev/null +++ b/packages/playground/lib/features/ai/wizard/core/ai/catalog/ask_user_image_style.ack.dart @@ -0,0 +1,109 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// dart format width=80 + +part of 'ask_user_image_style.dart'; + +// ************************************************************************** +// AckModelGenerator +// ************************************************************************** + +final class _AskUserImageStyleCopyWithUnset { + const _AskUserImageStyleCopyWithUnset(); +} + +/// Immutable model generated from `_askUserImageStyleSchema`. +/// An application-owned generated image-style selection step. +@AckInfer.jsonSerializable +final class AskUserImageStyle { + AskUserImageStyle({ + required this.question, + this.description, + required this.action, + }); + + factory AskUserImageStyle.parse(Object? input) { + return $ack.parse(input); + } + + factory AskUserImageStyle.fromJson(Map json) { + return $ack.parse(json); + } + + static const _AskUserImageStyleCopyWithUnset _ackCopyWithUnset = + _AskUserImageStyleCopyWithUnset(); + + /// The question to display to the user + final String question; + + /// Additional context or instructions + final String? description; + + final GenUiAction action; + + static final $ack = AckModelAdapter( + schema: () => _askUserImageStyleSchema, + fromRuntime: AskUserImageStyle._fromAckRuntime, + toRuntime: (model) => model._toAckRuntime(), + ); + + static SchemaResult safeParse(Object? input) => + $ack.safeParse(input); + + Map toJson() => Map.from($ack.encode(this)); + + SchemaResult> safeToJson() => $ack.safeEncode(this); + + AskUserImageStyle copyWith({ + String? question, + Object? description = _ackCopyWithUnset, + GenUiAction? action, + }) => AskUserImageStyle( + question: question ?? this.question, + description: identical(description, _ackCopyWithUnset) + ? this.description + : description as String?, + action: action ?? this.action, + ); + + @override + bool operator ==(Object other) => + identical(this, other) || + (other is AskUserImageStyle && + runtimeType == other.runtimeType && + deepEquals(question, other.question) && + deepEquals(description, other.description) && + deepEquals(action, other.action)); + + @override + int get hashCode => Object.hashAll([ + runtimeType, + deepHashCode(question), + deepHashCode(description), + deepHashCode(action), + ]); + + @override + String toString() => + 'AskUserImageStyle(question: $question, description: $description, action: $action)'; + + static AskUserImageStyle _fromAckRuntime(Map value) => + _$AskUserImageStyleFromJson(Map.from(value)); + + Map _toAckRuntime() => { + ..._$AskUserImageStyleToJson(this), + }; + + static String _ackFromRuntimeQuestion(Object? value) => value as String; + + static Object? _ackToRuntimeQuestion(String value) => value; + + static String? _ackFromRuntimeDescription(Object? value) => value as String?; + + static Object? _ackToRuntimeDescription(String? value) => value; + + static GenUiAction _ackFromRuntimeAction(Object? value) => + GenUiAction.$ack.fromRuntime(value as Map); + + static Object? _ackToRuntimeAction(GenUiAction value) => + GenUiAction.$ack.toRuntime(value); +} diff --git a/packages/playground/lib/features/ai/wizard/core/ai/catalog/ask_user_image_style.ack.g.dart b/packages/playground/lib/features/ai/wizard/core/ai/catalog/ask_user_image_style.ack.g.dart new file mode 100644 index 000000000..b1b330db8 --- /dev/null +++ b/packages/playground/lib/features/ai/wizard/core/ai/catalog/ask_user_image_style.ack.g.dart @@ -0,0 +1,26 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// dart format width=80 + +part of 'ask_user_image_style.dart'; + +// ************************************************************************** +// AckJsonSerializableGenerator +// ************************************************************************** + +AskUserImageStyle _$AskUserImageStyleFromJson(Map json) => + AskUserImageStyle( + question: AskUserImageStyle._ackFromRuntimeQuestion(json['question']), + description: AskUserImageStyle._ackFromRuntimeDescription( + json['description'], + ), + action: AskUserImageStyle._ackFromRuntimeAction(json['action']), + ); + +Map _$AskUserImageStyleToJson(AskUserImageStyle instance) => + { + 'question': AskUserImageStyle._ackToRuntimeQuestion(instance.question), + 'description': ?AskUserImageStyle._ackToRuntimeDescription( + instance.description, + ), + 'action': AskUserImageStyle._ackToRuntimeAction(instance.action), + }; diff --git a/packages/playground/lib/features/ai/wizard/core/ai/catalog/ask_user_image_style.dart b/packages/playground/lib/features/ai/wizard/core/ai/catalog/ask_user_image_style.dart index a7cf7cd1e..8edfa256a 100644 --- a/packages/playground/lib/features/ai/wizard/core/ai/catalog/ask_user_image_style.dart +++ b/packages/playground/lib/features/ai/wizard/core/ai/catalog/ask_user_image_style.dart @@ -18,11 +18,12 @@ import 'component_schema.dart'; import 'typed_catalog_item.dart'; import 'user_action_dispatch.dart'; -part 'ask_user_image_style.g.dart'; +part 'ask_user_image_style.ack.dart'; +part 'ask_user_image_style.ack.g.dart'; /// The model owns only the question copy. The application owns the exact /// preview subject, styles, generation, and versioned selection payload. -@AckType(name: 'AskUserImageStyle') +@AckInfer(name: 'AskUserImageStyle') final _askUserImageStyleSchema = Ack.object({ 'question': Ack.string().describe('The question to display to the user'), 'description': Ack.string().optional().describe( @@ -40,7 +41,7 @@ CatalogItem askUserImageStyleFor( } } - return typedCatalogItem( + return typedCatalogItem( name: 'AskUserImageStyle', dataSchema: componentSchema(_askUserImageStyleSchema.toJsonSchemaBuilder()), exampleData: [ @@ -55,7 +56,7 @@ CatalogItem askUserImageStyleFor( }, ]), ], - parse: AskUserImageStyleType.parse, + parse: AskUserImageStyle.parse, widgetBuilder: (context, data) => _AskUserImageStyleContent(data: data, itemContext: context), ); @@ -67,7 +68,7 @@ class _AskUserImageStyleContent extends StatefulWidget { required this.itemContext, }); - final AskUserImageStyleType data; + final AskUserImageStyle data; final CatalogItemContext itemContext; @override diff --git a/packages/playground/lib/features/ai/wizard/core/ai/catalog/ask_user_image_style.g.dart b/packages/playground/lib/features/ai/wizard/core/ai/catalog/ask_user_image_style.g.dart deleted file mode 100644 index 77b7fdaaa..000000000 --- a/packages/playground/lib/features/ai/wizard/core/ai/catalog/ask_user_image_style.g.dart +++ /dev/null @@ -1,32 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND -// dart format width=80 - -// ************************************************************************** -// AckSchemaGenerator -// ************************************************************************** - -part of 'ask_user_image_style.dart'; - -/// Extension type for AskUserImageStyle -extension type AskUserImageStyleType(Map _data) - implements Map { - static AskUserImageStyleType parse(Object? data) { - return _askUserImageStyleSchema.parseAs( - data, - (validated) => AskUserImageStyleType(validated as Map), - ); - } - - static SchemaResult safeParse(Object? data) { - return _askUserImageStyleSchema.safeParseAs( - data, - (validated) => AskUserImageStyleType(validated as Map), - ); - } - - String get question => _data['question'] as String; - - String? get description => _data['description'] as String?; - - ActionType get action => ActionType(_data['action'] as Map); -} diff --git a/packages/playground/lib/features/ai/wizard/core/ai/catalog/ask_user_question_cards.dart b/packages/playground/lib/features/ai/wizard/core/ai/catalog/ask_user_question_cards.dart index 9e7586e47..7e4f94140 100644 --- a/packages/playground/lib/features/ai/wizard/core/ai/catalog/ask_user_question_cards.dart +++ b/packages/playground/lib/features/ai/wizard/core/ai/catalog/ask_user_question_cards.dart @@ -274,8 +274,9 @@ class ImageStyleOptionCard extends StatelessWidget { if (imageBytes != null) { return Image.memory( imageBytes!, - fit: BoxFit.cover, errorBuilder: (ctx, error, stackTrace) => _buildPlaceholder(ctx), + semanticLabel: '${style.title} image style preview', + fit: BoxFit.cover, ); } diff --git a/packages/playground/lib/features/ai/wizard/core/ai/catalog/ask_user_radio.ack.dart b/packages/playground/lib/features/ai/wizard/core/ai/catalog/ask_user_radio.ack.dart new file mode 100644 index 000000000..34dc741a2 --- /dev/null +++ b/packages/playground/lib/features/ai/wizard/core/ai/catalog/ask_user_radio.ack.dart @@ -0,0 +1,228 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// dart format width=80 + +part of 'ask_user_radio.dart'; + +// ************************************************************************** +// AckModelGenerator +// ************************************************************************** + +final class _InputOptionCopyWithUnset { + const _InputOptionCopyWithUnset(); +} + +/// Immutable model generated from `_inputOptionSchema`. +/// Option with title and optional description +@AckInfer.jsonSerializable +final class InputOption { + InputOption({required this.title, this.description, this.icon}); + + factory InputOption.parse(Object? input) { + return $ack.parse(input); + } + + factory InputOption.fromJson(Map json) { + return $ack.parse(json); + } + + static const _InputOptionCopyWithUnset _ackCopyWithUnset = + _InputOptionCopyWithUnset(); + + /// Option title displayed to user + final String title; + + /// Optional description text + final String? description; + + /// Semantic icon for this option card + final WizardOptionIcon? icon; + + static final $ack = AckModelAdapter( + schema: () => _inputOptionSchema, + fromRuntime: InputOption._fromAckRuntime, + toRuntime: (model) => model._toAckRuntime(), + ); + + static SchemaResult safeParse(Object? input) => + $ack.safeParse(input); + + Map toJson() => Map.from($ack.encode(this)); + + SchemaResult> safeToJson() => $ack.safeEncode(this); + + InputOption copyWith({ + String? title, + Object? description = _ackCopyWithUnset, + Object? icon = _ackCopyWithUnset, + }) => InputOption( + title: title ?? this.title, + description: identical(description, _ackCopyWithUnset) + ? this.description + : description as String?, + icon: identical(icon, _ackCopyWithUnset) + ? this.icon + : icon as WizardOptionIcon?, + ); + + @override + bool operator ==(Object other) => + identical(this, other) || + (other is InputOption && + runtimeType == other.runtimeType && + deepEquals(title, other.title) && + deepEquals(description, other.description) && + deepEquals(icon, other.icon)); + + @override + int get hashCode => Object.hashAll([ + runtimeType, + deepHashCode(title), + deepHashCode(description), + deepHashCode(icon), + ]); + + @override + String toString() => + 'InputOption(title: $title, description: $description, icon: $icon)'; + + static InputOption _fromAckRuntime(Map value) => + _$InputOptionFromJson(Map.from(value)); + + Map _toAckRuntime() => { + ..._$InputOptionToJson(this), + }; + + static String _ackFromRuntimeTitle(Object? value) => value as String; + + static Object? _ackToRuntimeTitle(String value) => value; + + static String? _ackFromRuntimeDescription(Object? value) => value as String?; + + static Object? _ackToRuntimeDescription(String? value) => value; + + static WizardOptionIcon? _ackFromRuntimeIcon(Object? value) => + value as WizardOptionIcon?; + + static Object? _ackToRuntimeIcon(WizardOptionIcon? value) => value; +} + +final class _AskUserRadioCopyWithUnset { + const _AskUserRadioCopyWithUnset(); +} + +/// Immutable model generated from `_askUserRadioSchema`. +/// A question with radio button options. User selects one option. +@AckInfer.jsonSerializable +final class AskUserRadio { + AskUserRadio({ + required this.question, + this.description, + required List options, + required this.action, + }) : options = List.unmodifiable(options.map((item) => item)); + + factory AskUserRadio.parse(Object? input) { + return $ack.parse(input); + } + + factory AskUserRadio.fromJson(Map json) { + return $ack.parse(json); + } + + static const _AskUserRadioCopyWithUnset _ackCopyWithUnset = + _AskUserRadioCopyWithUnset(); + + /// The question to display to the user + final String question; + + /// Additional context or instructions + final String? description; + + /// Radio options with title and description for single selection + final List options; + + final GenUiAction action; + + static final $ack = AckModelAdapter( + schema: () => _askUserRadioSchema, + fromRuntime: AskUserRadio._fromAckRuntime, + toRuntime: (model) => model._toAckRuntime(), + ); + + static SchemaResult safeParse(Object? input) => + $ack.safeParse(input); + + Map toJson() => Map.from($ack.encode(this)); + + SchemaResult> safeToJson() => $ack.safeEncode(this); + + AskUserRadio copyWith({ + String? question, + Object? description = _ackCopyWithUnset, + List? options, + GenUiAction? action, + }) => AskUserRadio( + question: question ?? this.question, + description: identical(description, _ackCopyWithUnset) + ? this.description + : description as String?, + options: options ?? this.options, + action: action ?? this.action, + ); + + @override + bool operator ==(Object other) => + identical(this, other) || + (other is AskUserRadio && + runtimeType == other.runtimeType && + deepEquals(question, other.question) && + deepEquals(description, other.description) && + deepEquals(options, other.options) && + deepEquals(action, other.action)); + + @override + int get hashCode => Object.hashAll([ + runtimeType, + deepHashCode(question), + deepHashCode(description), + deepHashCode(options), + deepHashCode(action), + ]); + + @override + String toString() => + 'AskUserRadio(question: $question, description: $description, options: $options, action: $action)'; + + static AskUserRadio _fromAckRuntime(Map value) => + _$AskUserRadioFromJson(Map.from(value)); + + Map _toAckRuntime() => { + ..._$AskUserRadioToJson(this), + }; + + static String _ackFromRuntimeQuestion(Object? value) => value as String; + + static Object? _ackToRuntimeQuestion(String value) => value; + + static String? _ackFromRuntimeDescription(Object? value) => value as String?; + + static Object? _ackToRuntimeDescription(String? value) => value; + + static List _ackFromRuntimeOptions(Object? value) => + (value as List) + .map( + (item) => + InputOption.$ack.fromRuntime(item as Map), + ) + .toList(); + + static Object? _ackToRuntimeOptions(List value) => value + .map((item) => InputOption.$ack.toRuntime(item)) + .toList(growable: false); + + static GenUiAction _ackFromRuntimeAction(Object? value) => + GenUiAction.$ack.fromRuntime(value as Map); + + static Object? _ackToRuntimeAction(GenUiAction value) => + GenUiAction.$ack.toRuntime(value); +} diff --git a/packages/playground/lib/features/ai/wizard/core/ai/catalog/ask_user_radio.ack.g.dart b/packages/playground/lib/features/ai/wizard/core/ai/catalog/ask_user_radio.ack.g.dart new file mode 100644 index 000000000..90f66f6e8 --- /dev/null +++ b/packages/playground/lib/features/ai/wizard/core/ai/catalog/ask_user_radio.ack.g.dart @@ -0,0 +1,38 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// dart format width=80 + +part of 'ask_user_radio.dart'; + +// ************************************************************************** +// AckJsonSerializableGenerator +// ************************************************************************** + +InputOption _$InputOptionFromJson(Map json) => InputOption( + title: InputOption._ackFromRuntimeTitle(json['title']), + description: InputOption._ackFromRuntimeDescription(json['description']), + icon: InputOption._ackFromRuntimeIcon(json['icon']), +); + +Map _$InputOptionToJson( + InputOption instance, +) => { + 'title': InputOption._ackToRuntimeTitle(instance.title), + 'description': ?InputOption._ackToRuntimeDescription(instance.description), + 'icon': ?InputOption._ackToRuntimeIcon(instance.icon), +}; + +AskUserRadio _$AskUserRadioFromJson(Map json) => AskUserRadio( + question: AskUserRadio._ackFromRuntimeQuestion(json['question']), + description: AskUserRadio._ackFromRuntimeDescription(json['description']), + options: AskUserRadio._ackFromRuntimeOptions(json['options']), + action: AskUserRadio._ackFromRuntimeAction(json['action']), +); + +Map _$AskUserRadioToJson( + AskUserRadio instance, +) => { + 'question': AskUserRadio._ackToRuntimeQuestion(instance.question), + 'description': ?AskUserRadio._ackToRuntimeDescription(instance.description), + 'options': AskUserRadio._ackToRuntimeOptions(instance.options), + 'action': AskUserRadio._ackToRuntimeAction(instance.action), +}; diff --git a/packages/playground/lib/features/ai/wizard/core/ai/catalog/ask_user_radio.dart b/packages/playground/lib/features/ai/wizard/core/ai/catalog/ask_user_radio.dart index 17d2c8fc9..5c54aa326 100644 --- a/packages/playground/lib/features/ai/wizard/core/ai/catalog/ask_user_radio.dart +++ b/packages/playground/lib/features/ai/wizard/core/ai/catalog/ask_user_radio.dart @@ -7,8 +7,6 @@ import 'package:genui/genui.dart'; import '../schemas/genui_action_schema.dart'; import 'user_action_dispatch.dart'; -import '../../debug_logger.dart'; -import '../../ui/ui.dart'; import 'ask_user_question_cards.dart'; import 'catalog_question_step.dart'; @@ -16,12 +14,13 @@ import 'component_schema.dart'; import 'typed_catalog_item.dart'; import 'wizard_option_icon.dart'; -part 'ask_user_radio.g.dart'; +part 'ask_user_radio.ack.dart'; +part 'ask_user_radio.ack.g.dart'; // ─────────────────────────────────── SCHEMA ─────────────────────────────────── /// Schema for a radio option with title and optional description. -@AckType(name: 'InputOption') +@AckInfer(name: 'InputOption') final _inputOptionSchema = Ack.object({ 'title': Ack.string().describe('Option title displayed to user'), 'description': Ack.string().optional().describe('Optional description text'), @@ -33,22 +32,22 @@ final _inputOptionSchema = Ack.object({ /// Schema for AskUserRadio component. /// /// Displays a question with radio button options for single selection. -@AckType(name: 'AskUserRadio') +@AckInfer(name: 'AskUserRadio') final _askUserRadioSchema = Ack.object({ 'question': Ack.string().describe('The question to display to the user'), 'description': Ack.string().optional().describe( 'Additional context or instructions', ), - 'options': Ack.list( - _inputOptionSchema, - ).describe('Radio options with title and description for single selection'), + 'options': Ack.list(_inputOptionSchema).nonEmpty().describe( + 'Radio options with title and description for single selection', + ), 'action': actionSchema, }).describe('A question with radio button options. User selects one option.'); // ─────────────────────────────────── CATALOG ITEM ─────────────────────────────────── /// AskUserRadio catalog component for single-selection questions. -final askUserRadio = typedCatalogItem( +final askUserRadio = typedCatalogItem( name: 'AskUserRadio', dataSchema: componentSchema(_askUserRadioSchema.toJsonSchemaBuilder()), exampleData: [ @@ -69,7 +68,7 @@ final askUserRadio = typedCatalogItem( ] ''', ], - parse: AskUserRadioType.parse, + parse: AskUserRadio.parse, widgetBuilder: (context, data) => _AskUserRadioContent(data: data, itemContext: context), ); @@ -77,7 +76,7 @@ final askUserRadio = typedCatalogItem( // ─────────────────────────────────── WIDGET ─────────────────────────────────── class _AskUserRadioContent extends StatefulWidget { - final AskUserRadioType data; + final AskUserRadio data; final CatalogItemContext itemContext; const _AskUserRadioContent({required this.data, required this.itemContext}); @@ -111,11 +110,6 @@ class _AskUserRadioContentState extends State<_AskUserRadioContent> { Widget _buildOptions() { final options = widget.data.options; - if (options.isEmpty) { - debugLog.log('AskUserRadio', 'WARNING: radio input has no options.'); - return const SdBody('No options available'); - } - return LayoutBuilder( builder: (context, constraints) { final columnCount = constraints.maxWidth >= 620 diff --git a/packages/playground/lib/features/ai/wizard/core/ai/catalog/ask_user_radio.g.dart b/packages/playground/lib/features/ai/wizard/core/ai/catalog/ask_user_radio.g.dart deleted file mode 100644 index dd111ddac..000000000 --- a/packages/playground/lib/features/ai/wizard/core/ai/catalog/ask_user_radio.g.dart +++ /dev/null @@ -1,60 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND -// dart format width=80 - -// ************************************************************************** -// AckSchemaGenerator -// ************************************************************************** - -part of 'ask_user_radio.dart'; - -/// Extension type for InputOption -extension type InputOptionType(Map _data) - implements Map { - static InputOptionType parse(Object? data) { - return _inputOptionSchema.parseAs( - data, - (validated) => InputOptionType(validated as Map), - ); - } - - static SchemaResult safeParse(Object? data) { - return _inputOptionSchema.safeParseAs( - data, - (validated) => InputOptionType(validated as Map), - ); - } - - String get title => _data['title'] as String; - - String? get description => _data['description'] as String?; - - WizardOptionIcon? get icon => _data['icon'] as WizardOptionIcon?; -} - -/// Extension type for AskUserRadio -extension type AskUserRadioType(Map _data) - implements Map { - static AskUserRadioType parse(Object? data) { - return _askUserRadioSchema.parseAs( - data, - (validated) => AskUserRadioType(validated as Map), - ); - } - - static SchemaResult safeParse(Object? data) { - return _askUserRadioSchema.safeParseAs( - data, - (validated) => AskUserRadioType(validated as Map), - ); - } - - String get question => _data['question'] as String; - - String? get description => _data['description'] as String?; - - List get options => (_data['options'] as List) - .map((e) => InputOptionType(e as Map)) - .toList(); - - ActionType get action => ActionType(_data['action'] as Map); -} diff --git a/packages/playground/lib/features/ai/wizard/core/ai/catalog/ask_user_slider.ack.dart b/packages/playground/lib/features/ai/wizard/core/ai/catalog/ask_user_slider.ack.dart new file mode 100644 index 000000000..524fbabda --- /dev/null +++ b/packages/playground/lib/features/ai/wizard/core/ai/catalog/ask_user_slider.ack.dart @@ -0,0 +1,157 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// dart format width=80 + +part of 'ask_user_slider.dart'; + +// ************************************************************************** +// AckModelGenerator +// ************************************************************************** + +final class _AskUserSliderCopyWithUnset { + const _AskUserSliderCopyWithUnset(); +} + +/// Immutable model generated from `_askUserSliderSchema`. +/// A question with a counter and quick choices between min and max. +@AckInfer.jsonSerializable +final class AskUserSlider { + AskUserSlider({ + required this.question, + this.description, + required this.minValue, + required this.maxValue, + required this.defaultValue, + this.unit, + required this.action, + }); + + factory AskUserSlider.parse(Object? input) { + return $ack.parse(input); + } + + factory AskUserSlider.fromJson(Map json) { + return $ack.parse(json); + } + + static const _AskUserSliderCopyWithUnset _ackCopyWithUnset = + _AskUserSliderCopyWithUnset(); + + /// The question to display to the user + final String question; + + /// Additional context or instructions + final String? description; + + /// Minimum value + final int minValue; + + /// Maximum value + final int maxValue; + + /// Default/initial value + final int defaultValue; + + /// Unit label e.g. "slides", "minutes" + final String? unit; + + final GenUiAction action; + + static final $ack = AckModelAdapter( + schema: () => _askUserSliderSchema, + fromRuntime: AskUserSlider._fromAckRuntime, + toRuntime: (model) => model._toAckRuntime(), + ); + + static SchemaResult safeParse(Object? input) => + $ack.safeParse(input); + + Map toJson() => Map.from($ack.encode(this)); + + SchemaResult> safeToJson() => $ack.safeEncode(this); + + AskUserSlider copyWith({ + String? question, + Object? description = _ackCopyWithUnset, + int? minValue, + int? maxValue, + int? defaultValue, + Object? unit = _ackCopyWithUnset, + GenUiAction? action, + }) => AskUserSlider( + question: question ?? this.question, + description: identical(description, _ackCopyWithUnset) + ? this.description + : description as String?, + minValue: minValue ?? this.minValue, + maxValue: maxValue ?? this.maxValue, + defaultValue: defaultValue ?? this.defaultValue, + unit: identical(unit, _ackCopyWithUnset) ? this.unit : unit as String?, + action: action ?? this.action, + ); + + @override + bool operator ==(Object other) => + identical(this, other) || + (other is AskUserSlider && + runtimeType == other.runtimeType && + deepEquals(question, other.question) && + deepEquals(description, other.description) && + deepEquals(minValue, other.minValue) && + deepEquals(maxValue, other.maxValue) && + deepEquals(defaultValue, other.defaultValue) && + deepEquals(unit, other.unit) && + deepEquals(action, other.action)); + + @override + int get hashCode => Object.hashAll([ + runtimeType, + deepHashCode(question), + deepHashCode(description), + deepHashCode(minValue), + deepHashCode(maxValue), + deepHashCode(defaultValue), + deepHashCode(unit), + deepHashCode(action), + ]); + + @override + String toString() => + 'AskUserSlider(question: $question, description: $description, minValue: $minValue, maxValue: $maxValue, defaultValue: $defaultValue, unit: $unit, action: $action)'; + + static AskUserSlider _fromAckRuntime(Map value) => + _$AskUserSliderFromJson(Map.from(value)); + + Map _toAckRuntime() => { + ..._$AskUserSliderToJson(this), + }; + + static String _ackFromRuntimeQuestion(Object? value) => value as String; + + static Object? _ackToRuntimeQuestion(String value) => value; + + static String? _ackFromRuntimeDescription(Object? value) => value as String?; + + static Object? _ackToRuntimeDescription(String? value) => value; + + static int _ackFromRuntimeMinValue(Object? value) => value as int; + + static Object? _ackToRuntimeMinValue(int value) => value; + + static int _ackFromRuntimeMaxValue(Object? value) => value as int; + + static Object? _ackToRuntimeMaxValue(int value) => value; + + static int _ackFromRuntimeDefaultValue(Object? value) => value as int; + + static Object? _ackToRuntimeDefaultValue(int value) => value; + + static String? _ackFromRuntimeUnit(Object? value) => value as String?; + + static Object? _ackToRuntimeUnit(String? value) => value; + + static GenUiAction _ackFromRuntimeAction(Object? value) => + GenUiAction.$ack.fromRuntime(value as Map); + + static Object? _ackToRuntimeAction(GenUiAction value) => + GenUiAction.$ack.toRuntime(value); +} diff --git a/packages/playground/lib/features/ai/wizard/core/ai/catalog/ask_user_slider.ack.g.dart b/packages/playground/lib/features/ai/wizard/core/ai/catalog/ask_user_slider.ack.g.dart new file mode 100644 index 000000000..a82f099d1 --- /dev/null +++ b/packages/playground/lib/features/ai/wizard/core/ai/catalog/ask_user_slider.ack.g.dart @@ -0,0 +1,34 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// dart format width=80 + +part of 'ask_user_slider.dart'; + +// ************************************************************************** +// AckJsonSerializableGenerator +// ************************************************************************** + +AskUserSlider _$AskUserSliderFromJson( + Map json, +) => AskUserSlider( + question: AskUserSlider._ackFromRuntimeQuestion(json['question']), + description: AskUserSlider._ackFromRuntimeDescription(json['description']), + minValue: AskUserSlider._ackFromRuntimeMinValue(json['minValue']), + maxValue: AskUserSlider._ackFromRuntimeMaxValue(json['maxValue']), + defaultValue: AskUserSlider._ackFromRuntimeDefaultValue(json['defaultValue']), + unit: AskUserSlider._ackFromRuntimeUnit(json['unit']), + action: AskUserSlider._ackFromRuntimeAction(json['action']), +); + +Map _$AskUserSliderToJson( + AskUserSlider instance, +) => { + 'question': AskUserSlider._ackToRuntimeQuestion(instance.question), + 'description': ?AskUserSlider._ackToRuntimeDescription(instance.description), + 'minValue': AskUserSlider._ackToRuntimeMinValue(instance.minValue), + 'maxValue': AskUserSlider._ackToRuntimeMaxValue(instance.maxValue), + 'defaultValue': AskUserSlider._ackToRuntimeDefaultValue( + instance.defaultValue, + ), + 'unit': ?AskUserSlider._ackToRuntimeUnit(instance.unit), + 'action': AskUserSlider._ackToRuntimeAction(instance.action), +}; diff --git a/packages/playground/lib/features/ai/wizard/core/ai/catalog/ask_user_slider.dart b/packages/playground/lib/features/ai/wizard/core/ai/catalog/ask_user_slider.dart index 9566e9ff7..7f92909c5 100644 --- a/packages/playground/lib/features/ai/wizard/core/ai/catalog/ask_user_slider.dart +++ b/packages/playground/lib/features/ai/wizard/core/ai/catalog/ask_user_slider.dart @@ -15,32 +15,66 @@ import 'catalog_question_step.dart'; import 'component_schema.dart'; import 'typed_catalog_item.dart'; -part 'ask_user_slider.g.dart'; +part 'ask_user_slider.ack.dart'; +part 'ask_user_slider.ack.g.dart'; // ─────────────────────────────────── SCHEMA ─────────────────────────────────── /// Schema for AskUserSlider component. /// /// Displays a question with a focused numeric selector. -@AckType(name: 'AskUserSlider') -final _askUserSliderSchema = Ack.object({ - 'question': Ack.string().describe('The question to display to the user'), - 'description': Ack.string().optional().describe( - 'Additional context or instructions', - ), - 'minValue': Ack.integer().describe('Minimum value'), - 'maxValue': Ack.integer().describe('Maximum value'), - 'defaultValue': Ack.integer().describe('Default/initial value'), - 'unit': Ack.string().optional().describe( - 'Unit label e.g. "slides", "minutes"', - ), - 'action': actionSchema, -}).describe('A question with a counter and quick choices between min and max.'); +@AckInfer(name: 'AskUserSlider') +final _askUserSliderSchema = + Ack.object({ + 'question': Ack.string().describe( + 'The question to display to the user', + ), + 'description': Ack.string().optional().describe( + 'Additional context or instructions', + ), + 'minValue': Ack.integer().describe('Minimum value'), + 'maxValue': Ack.integer().describe('Maximum value'), + 'defaultValue': Ack.integer().describe('Default/initial value'), + 'unit': Ack.string().optional().describe( + 'Unit label e.g. "slides", "minutes"', + ), + 'action': actionSchema, + }) + .withConstraint(const _SliderRangeConstraint()) + .describe( + 'A question with a counter and quick choices between min and max.', + ); + +final class _SliderRangeConstraint extends Constraint> + with Validator> { + const _SliderRangeConstraint() + : super( + constraintKey: 'slider_range_relationships', + description: 'Slider bounds and default value must form a valid range.', + ); + + @override + bool isValid(Map value) { + final minValue = value['minValue']! as int; + final maxValue = value['maxValue']! as int; + final defaultValue = value['defaultValue']! as int; + + return minValue <= maxValue && + defaultValue >= minValue && + defaultValue <= maxValue; + } + + @override + String buildMessage(Map value) { + return 'minValue must not exceed maxValue, and defaultValue must be ' + 'between them inclusively.'; + } +} // ─────────────────────────────────── CATALOG ITEM ─────────────────────────────────── /// AskUserSlider catalog component for numeric input questions. -final askUserSlider = typedCatalogItem( +final askUserSlider = typedCatalogItem( name: 'AskUserSlider', dataSchema: componentSchema(_askUserSliderSchema.toJsonSchemaBuilder()), exampleData: [ @@ -59,7 +93,7 @@ final askUserSlider = typedCatalogItem( ] ''', ], - parse: AskUserSliderType.parse, + parse: AskUserSlider.parse, widgetBuilder: (context, data) => _AskUserSliderContent(data: data, itemContext: context), ); @@ -195,7 +229,7 @@ class _DeckLengthPreset extends StatelessWidget { } class _AskUserSliderContent extends StatefulWidget { - final AskUserSliderType data; + final AskUserSlider data; final CatalogItemContext itemContext; const _AskUserSliderContent({required this.data, required this.itemContext}); diff --git a/packages/playground/lib/features/ai/wizard/core/ai/catalog/ask_user_slider.g.dart b/packages/playground/lib/features/ai/wizard/core/ai/catalog/ask_user_slider.g.dart deleted file mode 100644 index b58e67bc7..000000000 --- a/packages/playground/lib/features/ai/wizard/core/ai/catalog/ask_user_slider.g.dart +++ /dev/null @@ -1,40 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND -// dart format width=80 - -// ************************************************************************** -// AckSchemaGenerator -// ************************************************************************** - -part of 'ask_user_slider.dart'; - -/// Extension type for AskUserSlider -extension type AskUserSliderType(Map _data) - implements Map { - static AskUserSliderType parse(Object? data) { - return _askUserSliderSchema.parseAs( - data, - (validated) => AskUserSliderType(validated as Map), - ); - } - - static SchemaResult safeParse(Object? data) { - return _askUserSliderSchema.safeParseAs( - data, - (validated) => AskUserSliderType(validated as Map), - ); - } - - String get question => _data['question'] as String; - - String? get description => _data['description'] as String?; - - int get minValue => _data['minValue'] as int; - - int get maxValue => _data['maxValue'] as int; - - int get defaultValue => _data['defaultValue'] as int; - - String? get unit => _data['unit'] as String?; - - ActionType get action => ActionType(_data['action'] as Map); -} diff --git a/packages/playground/lib/features/ai/wizard/core/ai/catalog/ask_user_style.ack.dart b/packages/playground/lib/features/ai/wizard/core/ai/catalog/ask_user_style.ack.dart new file mode 100644 index 000000000..8008510fc --- /dev/null +++ b/packages/playground/lib/features/ai/wizard/core/ai/catalog/ask_user_style.ack.dart @@ -0,0 +1,123 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// dart format width=80 + +part of 'ask_user_style.dart'; + +// ************************************************************************** +// AckModelGenerator +// ************************************************************************** + +final class _AskUserStyleCopyWithUnset { + const _AskUserStyleCopyWithUnset(); +} + +/// Immutable model generated from `_askUserStyleSchema`. +/// A question with exact catalog-backed presentation theme options. +@AckInfer.jsonSerializable +final class AskUserStyle { + AskUserStyle({ + required this.question, + this.description, + required List themeIds, + required this.action, + }) : themeIds = List.unmodifiable(themeIds.map((item) => item)); + + factory AskUserStyle.parse(Object? input) { + return $ack.parse(input); + } + + factory AskUserStyle.fromJson(Map json) { + return $ack.parse(json); + } + + static const _AskUserStyleCopyWithUnset _ackCopyWithUnset = + _AskUserStyleCopyWithUnset(); + + /// The question to display to the user + final String question; + + /// Additional context or instructions + final String? description; + + /// Exactly three registered theme IDs to offer in catalog order + final List themeIds; + + final GenUiAction action; + + static final $ack = AckModelAdapter( + schema: () => _askUserStyleSchema, + fromRuntime: AskUserStyle._fromAckRuntime, + toRuntime: (model) => model._toAckRuntime(), + ); + + static SchemaResult safeParse(Object? input) => + $ack.safeParse(input); + + Map toJson() => Map.from($ack.encode(this)); + + SchemaResult> safeToJson() => $ack.safeEncode(this); + + AskUserStyle copyWith({ + String? question, + Object? description = _ackCopyWithUnset, + List? themeIds, + GenUiAction? action, + }) => AskUserStyle( + question: question ?? this.question, + description: identical(description, _ackCopyWithUnset) + ? this.description + : description as String?, + themeIds: themeIds ?? this.themeIds, + action: action ?? this.action, + ); + + @override + bool operator ==(Object other) => + identical(this, other) || + (other is AskUserStyle && + runtimeType == other.runtimeType && + deepEquals(question, other.question) && + deepEquals(description, other.description) && + deepEquals(themeIds, other.themeIds) && + deepEquals(action, other.action)); + + @override + int get hashCode => Object.hashAll([ + runtimeType, + deepHashCode(question), + deepHashCode(description), + deepHashCode(themeIds), + deepHashCode(action), + ]); + + @override + String toString() => + 'AskUserStyle(question: $question, description: $description, themeIds: $themeIds, action: $action)'; + + static AskUserStyle _fromAckRuntime(Map value) => + _$AskUserStyleFromJson(Map.from(value)); + + Map _toAckRuntime() => { + ..._$AskUserStyleToJson(this), + }; + + static String _ackFromRuntimeQuestion(Object? value) => value as String; + + static Object? _ackToRuntimeQuestion(String value) => value; + + static String? _ackFromRuntimeDescription(Object? value) => value as String?; + + static Object? _ackToRuntimeDescription(String? value) => value; + + static List _ackFromRuntimeThemeIds(Object? value) => + (value as List).map((item) => item as String).toList(); + + static Object? _ackToRuntimeThemeIds(List value) => + value.map((item) => item).toList(growable: false); + + static GenUiAction _ackFromRuntimeAction(Object? value) => + GenUiAction.$ack.fromRuntime(value as Map); + + static Object? _ackToRuntimeAction(GenUiAction value) => + GenUiAction.$ack.toRuntime(value); +} diff --git a/packages/playground/lib/features/ai/wizard/core/ai/catalog/ask_user_style.ack.g.dart b/packages/playground/lib/features/ai/wizard/core/ai/catalog/ask_user_style.ack.g.dart new file mode 100644 index 000000000..3025da75a --- /dev/null +++ b/packages/playground/lib/features/ai/wizard/core/ai/catalog/ask_user_style.ack.g.dart @@ -0,0 +1,24 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// dart format width=80 + +part of 'ask_user_style.dart'; + +// ************************************************************************** +// AckJsonSerializableGenerator +// ************************************************************************** + +AskUserStyle _$AskUserStyleFromJson(Map json) => AskUserStyle( + question: AskUserStyle._ackFromRuntimeQuestion(json['question']), + description: AskUserStyle._ackFromRuntimeDescription(json['description']), + themeIds: AskUserStyle._ackFromRuntimeThemeIds(json['themeIds']), + action: AskUserStyle._ackFromRuntimeAction(json['action']), +); + +Map _$AskUserStyleToJson( + AskUserStyle instance, +) => { + 'question': AskUserStyle._ackToRuntimeQuestion(instance.question), + 'description': ?AskUserStyle._ackToRuntimeDescription(instance.description), + 'themeIds': AskUserStyle._ackToRuntimeThemeIds(instance.themeIds), + 'action': AskUserStyle._ackToRuntimeAction(instance.action), +}; diff --git a/packages/playground/lib/features/ai/wizard/core/ai/catalog/ask_user_style.dart b/packages/playground/lib/features/ai/wizard/core/ai/catalog/ask_user_style.dart index 962c9a582..796d5177a 100644 --- a/packages/playground/lib/features/ai/wizard/core/ai/catalog/ask_user_style.dart +++ b/packages/playground/lib/features/ai/wizard/core/ai/catalog/ask_user_style.dart @@ -19,14 +19,15 @@ import 'presentation_theme_component_schema.dart'; import 'typed_catalog_item.dart'; import 'user_action_dispatch.dart'; -part 'ask_user_style.g.dart'; +part 'ask_user_style.ack.dart'; +part 'ask_user_style.ack.g.dart'; // ─────────────────────────────────── SCHEMA ─────────────────────────────────── /// Schema for AskUserStyle component. /// /// Displays exact catalog-backed presentation themes for selection. -@AckType(name: 'AskUserStyle') +@AckInfer(name: 'AskUserStyle') final _askUserStyleSchema = Ack.object({ 'question': Ack.string().describe('The question to display to the user'), 'description': Ack.string().optional().describe( @@ -48,7 +49,7 @@ CatalogItem askUserStyleFor(PresentationThemeCatalog themeCatalog) { .map((theme) => theme.id) .toList(growable: false); - return typedCatalogItem( + return typedCatalogItem( name: 'AskUserStyle', dataSchema: componentSchema( schemaWithPresentationThemeIds( @@ -81,11 +82,11 @@ CatalogItem askUserStyleFor(PresentationThemeCatalog themeCatalog) { ); } -AskUserStyleType parseAskUserStyle( +AskUserStyle parseAskUserStyle( Object? data, { required PresentationThemeCatalog themeCatalog, }) { - final parsed = AskUserStyleType.parse(data); + final parsed = AskUserStyle.parse(data); final unknownIds = parsed.themeIds .where((themeId) => themeCatalog.current(themeId) == null) .toList(growable: false); @@ -107,7 +108,7 @@ class _AskUserStyleContent extends StatefulWidget { required this.themeCatalog, }); - final AskUserStyleType data; + final AskUserStyle data; final CatalogItemContext itemContext; final PresentationThemeCatalog themeCatalog; diff --git a/packages/playground/lib/features/ai/wizard/core/ai/catalog/ask_user_style.g.dart b/packages/playground/lib/features/ai/wizard/core/ai/catalog/ask_user_style.g.dart deleted file mode 100644 index eced07a9b..000000000 --- a/packages/playground/lib/features/ai/wizard/core/ai/catalog/ask_user_style.g.dart +++ /dev/null @@ -1,36 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND -// dart format width=80 - -// ************************************************************************** -// AckSchemaGenerator -// ************************************************************************** - -part of 'ask_user_style.dart'; - -List _$ackListCast(Object? value) => (value as List).cast(); - -/// Extension type for AskUserStyle -extension type AskUserStyleType(Map _data) - implements Map { - static AskUserStyleType parse(Object? data) { - return _askUserStyleSchema.parseAs( - data, - (validated) => AskUserStyleType(validated as Map), - ); - } - - static SchemaResult safeParse(Object? data) { - return _askUserStyleSchema.safeParseAs( - data, - (validated) => AskUserStyleType(validated as Map), - ); - } - - String get question => _data['question'] as String; - - String? get description => _data['description'] as String?; - - List get themeIds => _$ackListCast(_data['themeIds']); - - ActionType get action => ActionType(_data['action'] as Map); -} diff --git a/packages/playground/lib/features/ai/wizard/core/ai/catalog/user_action_dispatch.dart b/packages/playground/lib/features/ai/wizard/core/ai/catalog/user_action_dispatch.dart index bf76cbe2b..5984d636c 100644 --- a/packages/playground/lib/features/ai/wizard/core/ai/catalog/user_action_dispatch.dart +++ b/packages/playground/lib/features/ai/wizard/core/ai/catalog/user_action_dispatch.dart @@ -8,7 +8,7 @@ typedef CatalogActionContextBuilder = Map Function(); /// Dispatches a user action event with merged catalog + component context. Future dispatchCatalogAction({ required CatalogItemContext itemContext, - required ActionType action, + required GenUiAction action, required Map actionContext, }) async { final resolvedContext = await resolveCatalogActionContext( @@ -28,7 +28,7 @@ Future dispatchCatalogAction({ Future resolveCatalogActionContext({ required CatalogItemContext itemContext, - required ActionType action, + required GenUiAction action, }) { return resolveContext( itemContext.dataContext, @@ -43,7 +43,7 @@ Future resolveCatalogActionContext({ void submitCatalogActionIfValid({ required bool canSubmit, required CatalogItemContext itemContext, - required ActionType action, + required GenUiAction action, required CatalogActionContextBuilder contextBuilder, }) { if (!canSubmit) return; @@ -56,14 +56,14 @@ void submitCatalogActionIfValid({ ); } -JsonMap actionContextDefinitionFromAction(ActionType action) { +JsonMap actionContextDefinitionFromAction(GenUiAction action) { final entries = action.context; if (entries == null) return {}; return {for (final entry in entries) entry.key: _contextValue(entry.value)}; } -Object? _contextValue(ActionContextValueType value) { +Object? _contextValue(ActionContextValue value) { if (value.path case final path?) { return {'path': path}; } diff --git a/packages/playground/lib/features/ai/wizard/core/ai/catalog/wizard_option_icon.dart b/packages/playground/lib/features/ai/wizard/core/ai/catalog/wizard_option_icon.dart index 3b57cb8bc..d2ab87d61 100644 --- a/packages/playground/lib/features/ai/wizard/core/ai/catalog/wizard_option_icon.dart +++ b/packages/playground/lib/features/ai/wizard/core/ai/catalog/wizard_option_icon.dart @@ -35,5 +35,5 @@ enum WizardOptionIcon { }; static WizardOptionIcon fallbackFor(int index) => - values[index % values.length]; + values.elementAt(index % values.length); } diff --git a/packages/playground/lib/features/ai/wizard/core/ai/schemas/genui_action_schema.ack.dart b/packages/playground/lib/features/ai/wizard/core/ai/schemas/genui_action_schema.ack.dart new file mode 100644 index 000000000..b5ce0732e --- /dev/null +++ b/packages/playground/lib/features/ai/wizard/core/ai/schemas/genui_action_schema.ack.dart @@ -0,0 +1,297 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// dart format width=80 + +part of 'genui_action_schema.dart'; + +// ************************************************************************** +// AckModelGenerator +// ************************************************************************** + +final class _ActionContextValueCopyWithUnset { + const _ActionContextValueCopyWithUnset(); +} + +/// Immutable model generated from `_actionContextValueSchema`. +/// Context value - use path or one of the literal types +@AckInfer.jsonSerializable +final class ActionContextValue { + ActionContextValue({ + this.path, + this.literalString, + this.literalNumber, + this.literalBoolean, + }); + + factory ActionContextValue.parse(Object? input) { + return $ack.parse(input); + } + + factory ActionContextValue.fromJson(Map json) { + return $ack.parse(json); + } + + static const _ActionContextValueCopyWithUnset _ackCopyWithUnset = + _ActionContextValueCopyWithUnset(); + + /// Data model path binding + final String? path; + + /// Literal string value + final String? literalString; + + /// Literal number value + final double? literalNumber; + + /// Literal boolean value + final bool? literalBoolean; + + static final $ack = AckModelAdapter( + schema: () => _actionContextValueSchema, + fromRuntime: ActionContextValue._fromAckRuntime, + toRuntime: (model) => model._toAckRuntime(), + ); + + static SchemaResult safeParse(Object? input) => + $ack.safeParse(input); + + Map toJson() => Map.from($ack.encode(this)); + + SchemaResult> safeToJson() => $ack.safeEncode(this); + + ActionContextValue copyWith({ + Object? path = _ackCopyWithUnset, + Object? literalString = _ackCopyWithUnset, + Object? literalNumber = _ackCopyWithUnset, + Object? literalBoolean = _ackCopyWithUnset, + }) => ActionContextValue( + path: identical(path, _ackCopyWithUnset) ? this.path : path as String?, + literalString: identical(literalString, _ackCopyWithUnset) + ? this.literalString + : literalString as String?, + literalNumber: identical(literalNumber, _ackCopyWithUnset) + ? this.literalNumber + : literalNumber as double?, + literalBoolean: identical(literalBoolean, _ackCopyWithUnset) + ? this.literalBoolean + : literalBoolean as bool?, + ); + + @override + bool operator ==(Object other) => + identical(this, other) || + (other is ActionContextValue && + runtimeType == other.runtimeType && + deepEquals(path, other.path) && + deepEquals(literalString, other.literalString) && + deepEquals(literalNumber, other.literalNumber) && + deepEquals(literalBoolean, other.literalBoolean)); + + @override + int get hashCode => Object.hashAll([ + runtimeType, + deepHashCode(path), + deepHashCode(literalString), + deepHashCode(literalNumber), + deepHashCode(literalBoolean), + ]); + + @override + String toString() => + 'ActionContextValue(path: $path, literalString: $literalString, literalNumber: $literalNumber, literalBoolean: $literalBoolean)'; + + static ActionContextValue _fromAckRuntime(Map value) => + _$ActionContextValueFromJson(Map.from(value)); + + Map _toAckRuntime() => { + ..._$ActionContextValueToJson(this), + }; + + static String? _ackFromRuntimePath(Object? value) => value as String?; + + static Object? _ackToRuntimePath(String? value) => value; + + static String? _ackFromRuntimeLiteralString(Object? value) => + value as String?; + + static Object? _ackToRuntimeLiteralString(String? value) => value; + + static double? _ackFromRuntimeLiteralNumber(Object? value) => + value as double?; + + static Object? _ackToRuntimeLiteralNumber(double? value) => value; + + static bool? _ackFromRuntimeLiteralBoolean(Object? value) => value as bool?; + + static Object? _ackToRuntimeLiteralBoolean(bool? value) => value; +} + +/// Immutable model generated from `_actionContextEntrySchema`. +/// Context entry with key and value +@AckInfer.jsonSerializable +final class ActionContextEntry { + ActionContextEntry({required this.key, required this.value}); + + factory ActionContextEntry.parse(Object? input) { + return $ack.parse(input); + } + + factory ActionContextEntry.fromJson(Map json) { + return $ack.parse(json); + } + + /// Context key + final String key; + + final ActionContextValue value; + + static final $ack = AckModelAdapter( + schema: () => _actionContextEntrySchema, + fromRuntime: ActionContextEntry._fromAckRuntime, + toRuntime: (model) => model._toAckRuntime(), + ); + + static SchemaResult safeParse(Object? input) => + $ack.safeParse(input); + + Map toJson() => Map.from($ack.encode(this)); + + SchemaResult> safeToJson() => $ack.safeEncode(this); + + ActionContextEntry copyWith({String? key, ActionContextValue? value}) => + ActionContextEntry(key: key ?? this.key, value: value ?? this.value); + + @override + bool operator ==(Object other) => + identical(this, other) || + (other is ActionContextEntry && + runtimeType == other.runtimeType && + deepEquals(key, other.key) && + deepEquals(value, other.value)); + + @override + int get hashCode => + Object.hashAll([runtimeType, deepHashCode(key), deepHashCode(value)]); + + @override + String toString() => 'ActionContextEntry(key: $key, value: $value)'; + + static ActionContextEntry _fromAckRuntime(Map value) => + _$ActionContextEntryFromJson(Map.from(value)); + + Map _toAckRuntime() => { + ..._$ActionContextEntryToJson(this), + }; + + static String _ackFromRuntimeKey(Object? value) => value as String; + + static Object? _ackToRuntimeKey(String value) => value; + + static ActionContextValue _ackFromRuntimeValue(Object? value) => + ActionContextValue.$ack.fromRuntime(value as Map); + + static Object? _ackToRuntimeValue(ActionContextValue value) => + ActionContextValue.$ack.toRuntime(value); +} + +final class _GenUiActionCopyWithUnset { + const _GenUiActionCopyWithUnset(); +} + +/// Immutable model generated from `actionSchema`. +/// GenUI action with name and context binding +@AckInfer.jsonSerializable +final class GenUiAction { + GenUiAction({required this.name, List? context}) + : context = switch (context) { + null => null, + final fieldValue => List.unmodifiable( + fieldValue.map((item) => item), + ), + }; + + factory GenUiAction.parse(Object? input) { + return $ack.parse(input); + } + + factory GenUiAction.fromJson(Map json) { + return $ack.parse(json); + } + + static const _GenUiActionCopyWithUnset _ackCopyWithUnset = + _GenUiActionCopyWithUnset(); + + /// Action name to dispatch + final String name; + + /// List of context data to include with the action + final List? context; + + static final $ack = AckModelAdapter( + schema: () => actionSchema, + fromRuntime: GenUiAction._fromAckRuntime, + toRuntime: (model) => model._toAckRuntime(), + ); + + static SchemaResult safeParse(Object? input) => + $ack.safeParse(input); + + Map toJson() => Map.from($ack.encode(this)); + + SchemaResult> safeToJson() => $ack.safeEncode(this); + + GenUiAction copyWith({String? name, Object? context = _ackCopyWithUnset}) => + GenUiAction( + name: name ?? this.name, + context: identical(context, _ackCopyWithUnset) + ? this.context + : context as List?, + ); + + @override + bool operator ==(Object other) => + identical(this, other) || + (other is GenUiAction && + runtimeType == other.runtimeType && + deepEquals(name, other.name) && + deepEquals(context, other.context)); + + @override + int get hashCode => + Object.hashAll([runtimeType, deepHashCode(name), deepHashCode(context)]); + + @override + String toString() => 'GenUiAction(name: $name, context: $context)'; + + static GenUiAction _fromAckRuntime(Map value) => + _$GenUiActionFromJson(Map.from(value)); + + Map _toAckRuntime() => { + ..._$GenUiActionToJson(this), + }; + + static String _ackFromRuntimeName(Object? value) => value as String; + + static Object? _ackToRuntimeName(String value) => value; + + static List? _ackFromRuntimeContext(Object? value) => + switch (value) { + null => null, + final fieldValue => + (fieldValue as List) + .map( + (item) => ActionContextEntry.$ack.fromRuntime( + item as Map, + ), + ) + .toList(), + }; + + static Object? _ackToRuntimeContext(List? value) => + switch (value) { + null => null, + final fieldValue => + fieldValue + .map((item) => ActionContextEntry.$ack.toRuntime(item)) + .toList(growable: false), + }; +} diff --git a/packages/playground/lib/features/ai/wizard/core/ai/schemas/genui_action_schema.ack.g.dart b/packages/playground/lib/features/ai/wizard/core/ai/schemas/genui_action_schema.ack.g.dart new file mode 100644 index 000000000..8449d4624 --- /dev/null +++ b/packages/playground/lib/features/ai/wizard/core/ai/schemas/genui_action_schema.ack.g.dart @@ -0,0 +1,59 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// dart format width=80 + +part of 'genui_action_schema.dart'; + +// ************************************************************************** +// AckJsonSerializableGenerator +// ************************************************************************** + +ActionContextValue _$ActionContextValueFromJson(Map json) => + ActionContextValue( + path: ActionContextValue._ackFromRuntimePath(json['path']), + literalString: ActionContextValue._ackFromRuntimeLiteralString( + json['literalString'], + ), + literalNumber: ActionContextValue._ackFromRuntimeLiteralNumber( + json['literalNumber'], + ), + literalBoolean: ActionContextValue._ackFromRuntimeLiteralBoolean( + json['literalBoolean'], + ), + ); + +Map _$ActionContextValueToJson(ActionContextValue instance) => + { + 'path': ?ActionContextValue._ackToRuntimePath(instance.path), + 'literalString': ?ActionContextValue._ackToRuntimeLiteralString( + instance.literalString, + ), + 'literalNumber': ?ActionContextValue._ackToRuntimeLiteralNumber( + instance.literalNumber, + ), + 'literalBoolean': ?ActionContextValue._ackToRuntimeLiteralBoolean( + instance.literalBoolean, + ), + }; + +ActionContextEntry _$ActionContextEntryFromJson(Map json) => + ActionContextEntry( + key: ActionContextEntry._ackFromRuntimeKey(json['key']), + value: ActionContextEntry._ackFromRuntimeValue(json['value']), + ); + +Map _$ActionContextEntryToJson(ActionContextEntry instance) => + { + 'key': ActionContextEntry._ackToRuntimeKey(instance.key), + 'value': ActionContextEntry._ackToRuntimeValue(instance.value), + }; + +GenUiAction _$GenUiActionFromJson(Map json) => GenUiAction( + name: GenUiAction._ackFromRuntimeName(json['name']), + context: GenUiAction._ackFromRuntimeContext(json['context']), +); + +Map _$GenUiActionToJson(GenUiAction instance) => + { + 'name': GenUiAction._ackToRuntimeName(instance.name), + 'context': ?GenUiAction._ackToRuntimeContext(instance.context), + }; diff --git a/packages/playground/lib/features/ai/wizard/core/ai/schemas/genui_action_schema.dart b/packages/playground/lib/features/ai/wizard/core/ai/schemas/genui_action_schema.dart index f02a86543..16e79b38b 100644 --- a/packages/playground/lib/features/ai/wizard/core/ai/schemas/genui_action_schema.dart +++ b/packages/playground/lib/features/ai/wizard/core/ai/schemas/genui_action_schema.dart @@ -1,9 +1,10 @@ import 'package:ack/ack.dart'; import 'package:ack_annotations/ack_annotations.dart'; -part 'genui_action_schema.g.dart'; +part 'genui_action_schema.ack.dart'; +part 'genui_action_schema.ack.g.dart'; -@AckType(name: 'ActionContextValue') +@AckInfer(name: 'ActionContextValue') final _actionContextValueSchema = Ack.object({ 'path': Ack.string().optional().describe('Data model path binding'), 'literalString': Ack.string().optional().describe('Literal string value'), @@ -11,7 +12,7 @@ final _actionContextValueSchema = Ack.object({ 'literalBoolean': Ack.boolean().optional().describe('Literal boolean value'), }).describe('Context value - use path or one of the literal types'); -@AckType(name: 'ActionContextEntry') +@AckInfer(name: 'ActionContextEntry') final _actionContextEntrySchema = Ack.object({ 'key': Ack.string().describe('Context key'), 'value': _actionContextValueSchema, @@ -20,7 +21,7 @@ final _actionContextEntrySchema = Ack.object({ /// Shared GenUI action schema for catalog components. /// /// This schema defines the structure for user actions dispatched by GenUI -/// components. It's defined with @AckType() for code generation support. +/// components. It's defined with @AckInfer() for code generation support. /// /// Usage: /// ```dart @@ -28,10 +29,10 @@ final _actionContextEntrySchema = Ack.object({ /// 'action': actionSchema, /// /// // Parse action data: -/// final action = ActionType.parse(data.action); +/// final action = GenUiAction.parse(data.action); /// final name = action.name; /// ``` -@AckType(name: 'Action') +@AckInfer(name: 'GenUiAction') final actionSchema = Ack.object({ 'name': Ack.string().describe('Action name to dispatch'), 'context': Ack.list( diff --git a/packages/playground/lib/features/ai/wizard/core/ai/schemas/genui_action_schema.g.dart b/packages/playground/lib/features/ai/wizard/core/ai/schemas/genui_action_schema.g.dart deleted file mode 100644 index 14437cdbb..000000000 --- a/packages/playground/lib/features/ai/wizard/core/ai/schemas/genui_action_schema.g.dart +++ /dev/null @@ -1,83 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND -// dart format width=80 - -// ************************************************************************** -// AckSchemaGenerator -// ************************************************************************** - -part of 'genui_action_schema.dart'; - -/// Extension type for ActionContextValue -extension type ActionContextValueType(Map _data) - implements Map { - static ActionContextValueType parse(Object? data) { - return _actionContextValueSchema.parseAs( - data, - (validated) => ActionContextValueType(validated as Map), - ); - } - - static SchemaResult safeParse(Object? data) { - return _actionContextValueSchema.safeParseAs( - data, - (validated) => ActionContextValueType(validated as Map), - ); - } - - String? get path => _data['path'] as String?; - - String? get literalString => _data['literalString'] as String?; - - double? get literalNumber => _data['literalNumber'] as double?; - - bool? get literalBoolean => _data['literalBoolean'] as bool?; -} - -/// Extension type for ActionContextEntry -extension type ActionContextEntryType(Map _data) - implements Map { - static ActionContextEntryType parse(Object? data) { - return _actionContextEntrySchema.parseAs( - data, - (validated) => ActionContextEntryType(validated as Map), - ); - } - - static SchemaResult safeParse(Object? data) { - return _actionContextEntrySchema.safeParseAs( - data, - (validated) => ActionContextEntryType(validated as Map), - ); - } - - String get key => _data['key'] as String; - - ActionContextValueType get value => - ActionContextValueType(_data['value'] as Map); -} - -/// Extension type for Action -extension type ActionType(Map _data) - implements Map { - static ActionType parse(Object? data) { - return actionSchema.parseAs( - data, - (validated) => ActionType(validated as Map), - ); - } - - static SchemaResult safeParse(Object? data) { - return actionSchema.safeParseAs( - data, - (validated) => ActionType(validated as Map), - ); - } - - String get name => _data['name'] as String; - - List? get context => _data['context'] != null - ? (_data['context'] as List) - .map((e) => ActionContextEntryType(e as Map)) - .toList() - : null; -} diff --git a/packages/playground/lib/features/ai/wizard/core/ai/services/ai_conversation_viewmodel.dart b/packages/playground/lib/features/ai/wizard/core/ai/services/ai_conversation_viewmodel.dart index e9812e57f..2ac29c96b 100644 --- a/packages/playground/lib/features/ai/wizard/core/ai/services/ai_conversation_viewmodel.dart +++ b/packages/playground/lib/features/ai/wizard/core/ai/services/ai_conversation_viewmodel.dart @@ -74,7 +74,7 @@ final class AiConversationViewModel extends ChangeNotifier /// The Wizard favors the current Flash Lite model for short, structured /// surface turns. There is intentionally no in-wizard model picker. - static const _modelName = GeminiModelNames.gemini31FlashLite; + static const _modelName = GeminiModelNames.gemini35FlashLite; static const _missingSurfaceError = 'I couldn\'t prepare the next step. Add a detail below to try again.'; final _controller = Signal(null); diff --git a/packages/playground/lib/features/ai/wizard/core/ui/components/sd_typography.dart b/packages/playground/lib/features/ai/wizard/core/ui/components/sd_typography.dart index 60ed48f29..4ea652fc8 100644 --- a/packages/playground/lib/features/ai/wizard/core/ui/components/sd_typography.dart +++ b/packages/playground/lib/features/ai/wizard/core/ui/components/sd_typography.dart @@ -14,7 +14,9 @@ class SdHeadline extends StatelessWidget { @override Widget build(BuildContext context) { - final defaultStyle = TextStyler().style($titleH5.mix()).color($foreground()); + final defaultStyle = TextStyler() + .style($titleH5.mix()) + .color($foreground()); return defaultStyle.merge(style).call(text); } } @@ -31,8 +33,9 @@ class SdTitle extends StatelessWidget { @override Widget build(BuildContext context) { - final defaultStyle = - TextStyler().style($paragraphLarge.mix()).color($foreground()); + final defaultStyle = TextStyler() + .style($paragraphLarge.mix()) + .color($foreground()); return defaultStyle.merge(style).call(text); } } @@ -49,8 +52,9 @@ class SdBody extends StatelessWidget { @override Widget build(BuildContext context) { - final defaultStyle = - TextStyler().style($paragraphMedium.mix()).color($muted()); + final defaultStyle = TextStyler() + .style($paragraphMedium.mix()) + .color($muted()); return defaultStyle.merge(style).call(text); } } @@ -67,8 +71,9 @@ class SdCaption extends StatelessWidget { @override Widget build(BuildContext context) { - final defaultStyle = - TextStyler().style($paragraphSmall.mix()).color($muted()); + final defaultStyle = TextStyler() + .style($paragraphSmall.mix()) + .color($muted()); return defaultStyle.merge(style).call(text); } } @@ -85,8 +90,9 @@ class SdHint extends StatelessWidget { @override Widget build(BuildContext context) { - final defaultStyle = - TextStyler().style($paragraphSmall.mix()).color($fieldPlaceholder()); + final defaultStyle = TextStyler() + .style($paragraphSmall.mix()) + .color($fieldPlaceholder()); return defaultStyle.merge(style).call(text); } } diff --git a/packages/playground/lib/features/ai/wizard/presentation/wizard_generation_controller.dart b/packages/playground/lib/features/ai/wizard/presentation/wizard_generation_controller.dart index 007590a2d..1fea4a800 100644 --- a/packages/playground/lib/features/ai/wizard/presentation/wizard_generation_controller.dart +++ b/packages/playground/lib/features/ai/wizard/presentation/wizard_generation_controller.dart @@ -39,7 +39,7 @@ final class WizardGenerationController extends ChangeNotifier { WizardGenerationPhase? _failedPhase; DeckGenerationRequest? _request; - DeckPlanType? _plan; + DeckPlan? _plan; DeckGenerationResult? _result; String? _errorMessage; GenerationProgress _progress = const GenerationProgress(.idle); @@ -167,7 +167,7 @@ final class WizardGenerationController extends ChangeNotifier { WizardGenerationPhase? get failedPhase => _failedPhase; - DeckPlanType? get plan => _plan; + DeckPlan? get plan => _plan; int get planRevision => _planRevision; @@ -180,7 +180,7 @@ final class WizardGenerationController extends ChangeNotifier { Duration get elapsed => _elapsed + (_stageStartedAt == null - ? Duration.zero + ? .zero : DateTime.now().difference(_stageStartedAt!)); bool get isBusy => _stage == .planning || _stage == .composing; @@ -209,16 +209,14 @@ final class WizardGenerationController extends ChangeNotifier { if (nextTitle.isEmpty || nextAssertion.isEmpty) return false; if (index < 0 || index >= currentPlan.slides.length) return false; - final data = Map.of(currentPlan); - final slides = [ - for (final slide in currentPlan.slides) Map.of(slide), - ]; + final data = currentPlan.toJson(); + final slides = [for (final slide in currentPlan.slides) slide.toJson()]; slides[index] ..['title'] = nextTitle ..['assertion'] = nextAssertion; data['slides'] = slides; - final candidate = DeckPlanType.parse(data); + final candidate = DeckPlan.parse(data); final blockingIssues = validateDeckPlanIssues( candidate, typographyCatalog: _service.typographyCatalog, diff --git a/packages/playground/lib/features/ai/wizard/presentation/wizard_outline_review.dart b/packages/playground/lib/features/ai/wizard/presentation/wizard_outline_review.dart index a5cfdea53..2bb2b33e2 100644 --- a/packages/playground/lib/features/ai/wizard/presentation/wizard_outline_review.dart +++ b/packages/playground/lib/features/ai/wizard/presentation/wizard_outline_review.dart @@ -20,7 +20,7 @@ class WizardOutlineReview extends StatefulWidget { required this.onApprove, }); - final DeckPlanType plan; + final DeckPlan plan; final int planRevision; final UpdateOutlineSlide onSlideChanged; final VoidCallback onBack; @@ -183,8 +183,8 @@ class _OutlineSection extends StatelessWidget { required this.planRevision, }); - final DeckPlanSectionType section; - final List<({int index, DeckPlanSlideType slide})> slides; + final DeckPlanSection section; + final List<({int index, DeckPlanSlide slide})> slides; final String? editingSlideKey; final ValueChanged onEdit; final UpdateOutlineSlide onSlideChanged; @@ -236,7 +236,7 @@ class _OutlineSlideEditor extends StatefulWidget { }); final int index; - final DeckPlanSlideType slide; + final DeckPlanSlide slide; final bool editing; final ValueChanged onEdit; final UpdateOutlineSlide onChanged; diff --git a/packages/playground/lib/features/presentation/presentation/pages/presentation_page.dart b/packages/playground/lib/features/presentation/presentation/pages/presentation_page.dart index c4ed19f22..d33fd743b 100644 --- a/packages/playground/lib/features/presentation/presentation/pages/presentation_page.dart +++ b/packages/playground/lib/features/presentation/presentation/pages/presentation_page.dart @@ -31,7 +31,9 @@ class _PresentationPageState extends State { // it once mounted to open on the slide the author was editing. WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) return; - context.read().presentation.goToSlide(widget.initialIndex); + context.read().presentation.goToSlide( + widget.initialIndex, + ); }); } diff --git a/packages/playground/macos/Podfile.lock b/packages/playground/macos/Podfile.lock index 318eae9aa..6a21de721 100644 --- a/packages/playground/macos/Podfile.lock +++ b/packages/playground/macos/Podfile.lock @@ -1,79 +1,27 @@ PODS: - - audioplayers_darwin (0.0.1): - - Flutter - - FlutterMacOS - - device_info_plus (0.0.1): - - FlutterMacOS - FlutterMacOS (1.0.0) - irondash_engine_context (0.0.1): - FlutterMacOS - - screen_retriever_macos (0.0.1): - - FlutterMacOS - - sqflite_darwin (0.0.4): - - Flutter - - FlutterMacOS - super_native_extensions (0.0.1): - FlutterMacOS - - url_launcher_macos (0.0.1): - - FlutterMacOS - - video_player_avfoundation (0.0.1): - - Flutter - - FlutterMacOS - - webview_flutter_wkwebview (0.0.1): - - Flutter - - FlutterMacOS - - window_manager (0.5.0): - - FlutterMacOS DEPENDENCIES: - - audioplayers_darwin (from `Flutter/ephemeral/.symlinks/plugins/audioplayers_darwin/darwin`) - - device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`) - FlutterMacOS (from `Flutter/ephemeral`) - irondash_engine_context (from `Flutter/ephemeral/.symlinks/plugins/irondash_engine_context/macos`) - - screen_retriever_macos (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever_macos/macos`) - - sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin`) - super_native_extensions (from `Flutter/ephemeral/.symlinks/plugins/super_native_extensions/macos`) - - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) - - video_player_avfoundation (from `Flutter/ephemeral/.symlinks/plugins/video_player_avfoundation/darwin`) - - webview_flutter_wkwebview (from `Flutter/ephemeral/.symlinks/plugins/webview_flutter_wkwebview/darwin`) - - window_manager (from `Flutter/ephemeral/.symlinks/plugins/window_manager/macos`) EXTERNAL SOURCES: - audioplayers_darwin: - :path: Flutter/ephemeral/.symlinks/plugins/audioplayers_darwin/darwin - device_info_plus: - :path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos FlutterMacOS: :path: Flutter/ephemeral irondash_engine_context: :path: Flutter/ephemeral/.symlinks/plugins/irondash_engine_context/macos - screen_retriever_macos: - :path: Flutter/ephemeral/.symlinks/plugins/screen_retriever_macos/macos - sqflite_darwin: - :path: Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin super_native_extensions: :path: Flutter/ephemeral/.symlinks/plugins/super_native_extensions/macos - url_launcher_macos: - :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos - video_player_avfoundation: - :path: Flutter/ephemeral/.symlinks/plugins/video_player_avfoundation/darwin - webview_flutter_wkwebview: - :path: Flutter/ephemeral/.symlinks/plugins/webview_flutter_wkwebview/darwin - window_manager: - :path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos SPEC CHECKSUMS: - audioplayers_darwin: 835ced6edd4c9fc8ebb0a7cc9e294a91d99917d5 - device_info_plus: 4fb280989f669696856f8b129e4a5e3cd6c48f76 FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 irondash_engine_context: 893c7d96d20ce361d7e996f39d360c4c2f9869ba - screen_retriever_macos: c5508cc3c66ff0d4db650480cf0ab691e220d933 - sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 super_native_extensions: c2795d6d9aedf4a79fae25cb6160b71b50549189 - url_launcher_macos: f87a979182d112f911de6820aefddaf56ee9fbfd - video_player_avfoundation: 3453f792138786248960ca029747fcd9f318ef52 - webview_flutter_wkwebview: 8ebf4fded22593026f7dbff1fbff31ea98573c8d - window_manager: b729e31d38fb04905235df9ea896128991cad99e PODFILE CHECKSUM: 54d867c82ac51cbd61b565781b9fada492027009 diff --git a/packages/playground/pubspec.yaml b/packages/playground/pubspec.yaml index 3f2b78c0a..5d8a22621 100644 --- a/packages/playground/pubspec.yaml +++ b/packages/playground/pubspec.yaml @@ -41,9 +41,9 @@ dependencies: gpt_markdown: ^1.1.4 dotprompt_dart: ^0.5.0 path: ^1.9.0 - ack: 1.0.1 - ack_annotations: 1.0.1 - ack_json_schema_builder: 1.0.1 + ack: ^1.2.0 + ack_annotations: ^1.2.0 + ack_json_schema_builder: ^1.2.0 dartantic_ai: ^3.1.0 json_schema_builder: ^0.1.5 # Filesystem persistence for the playground editor (desktop-only). @@ -57,7 +57,7 @@ dev_dependencies: flutter_lints: ^6.0.0 dart_code_metrics_presets: ^2.19.0 build_runner: ^2.5.4 - ack_generator: 1.0.1 + ack_generator: ^1.2.0 # Test-only: mock path_provider so the native deck file store can be exercised # against a temp directory. path_provider_platform_interface: ^2.1.2 diff --git a/packages/playground/test/core/data/mappers/deck_markdown_codec_test.dart b/packages/playground/test/core/data/mappers/deck_markdown_codec_test.dart index b172d607b..4f7bf69fd 100644 --- a/packages/playground/test/core/data/mappers/deck_markdown_codec_test.dart +++ b/packages/playground/test/core/data/mappers/deck_markdown_codec_test.dart @@ -105,6 +105,6 @@ custom: value List> _withoutKeys(List slides) { return [ for (final slide in slides) - Map.from(slide.toMap())..remove('key'), + Map.from(slide.toJson())..remove('key'), ]; } diff --git a/packages/playground/test/features/ai/quick_agent/core/engine/prompts/composition_example_library_test.dart b/packages/playground/test/features/ai/quick_agent/core/engine/prompts/composition_example_library_test.dart index 41be8f32b..1d13953c9 100644 --- a/packages/playground/test/features/ai/quick_agent/core/engine/prompts/composition_example_library_test.dart +++ b/packages/playground/test/features/ai/quick_agent/core/engine/prompts/composition_example_library_test.dart @@ -12,7 +12,7 @@ void main() { () async { final library = AssetCompositionExampleLibrary(); await library.load(); - final current = DeckPlanSlideType.parse( + final current = DeckPlanSlide.parse( _planSlide( composition: 'imageRight', elements: const [ @@ -54,7 +54,7 @@ void main() { await library.load(); final data = _planSlide(composition: 'metric'); data['contentUnits'] = ['19% faster experiment decisions']; - final current = DeckPlanSlideType.parse(data); + final current = DeckPlanSlide.parse(data); final example = library.buildFor( current: current, @@ -79,7 +79,7 @@ void main() { ); for (final composition in deckPlanCompositionIntents) { - final current = DeckPlanSlideType.parse( + final current = DeckPlanSlide.parse( _planSlide( composition: composition, elements: _elementsFor(composition), diff --git a/packages/playground/test/features/ai/quick_agent/core/engine/prompts/generation_prompt_provider_test.dart b/packages/playground/test/features/ai/quick_agent/core/engine/prompts/generation_prompt_provider_test.dart index 659a91934..9f26709bd 100644 --- a/packages/playground/test/features/ai/quick_agent/core/engine/prompts/generation_prompt_provider_test.dart +++ b/packages/playground/test/features/ai/quick_agent/core/engine/prompts/generation_prompt_provider_test.dart @@ -80,7 +80,7 @@ void main() { }); test('assembles bounded single-slide context deterministically', () { - final plan = DeckPlanType.parse({ + final plan = DeckPlan.parse({ 'topic': 'Reliable systems', 'story': 'Move from uncertainty to a reliable operating rhythm.', 'theme': _themeReference, @@ -187,7 +187,7 @@ void main() { test( 'repairs metric identity from original grounded facts, not plan labels', () { - final plan = DeckPlanType.parse({ + final plan = DeckPlan.parse({ 'topic': 'Planning', 'story': 'Replace roadmap theater with continuous planning.', 'theme': _themeReference, @@ -251,7 +251,7 @@ void main() { () async { final provider = AssetGenerationPromptProvider(); await provider.load(); - final plan = DeckPlanType.parse({ + final plan = DeckPlan.parse({ 'topic': 'Adoption', 'story': 'Move from output to adoption.', 'theme': _themeReference, @@ -300,7 +300,7 @@ void main() { test('builds one compact ordered prompt for a narrative section', () async { final provider = AssetGenerationPromptProvider(); await provider.load(); - final plan = DeckPlanType.parse({ + final plan = DeckPlan.parse({ 'topic': 'Adoption', 'story': 'Move from evidence to a practical decision.', 'theme': _themeReference, @@ -368,7 +368,7 @@ void main() { final catalog = PresentationThemeCatalog.withDefaults(); final provider = AssetGenerationPromptProvider(); await provider.load(); - final plan = DeckPlanType.parse({ + final plan = DeckPlan.parse({ 'topic': 'Reliable systems', 'story': 'Move from uncertainty to a reliable operating rhythm.', 'theme': _themeReference, diff --git a/packages/playground/test/features/ai/quick_agent/core/engine/schemas/outline_schema_test.dart b/packages/playground/test/features/ai/quick_agent/core/engine/schemas/outline_schema_test.dart index 8044ac14b..94055b48f 100644 --- a/packages/playground/test/features/ai/quick_agent/core/engine/schemas/outline_schema_test.dart +++ b/packages/playground/test/features/ai/quick_agent/core/engine/schemas/outline_schema_test.dart @@ -148,7 +148,7 @@ void main() { }); test('rejects duplicate or empty slide keys after schema parsing', () { - final plan = DeckPlanType.parse({ + final plan = DeckPlan.parse({ 'topic': 'Duplicate keys', 'story': 'A short story.', 'theme': _validTheme, @@ -178,7 +178,7 @@ void main() { }); test('rejects a plan that does not match the requested slide count', () { - final plan = DeckPlanType.parse({ + final plan = DeckPlan.parse({ 'topic': 'Count contract', 'story': 'Every requested slide must be planned.', 'theme': _validTheme, @@ -201,7 +201,7 @@ void main() { }); test('validates a hierarchical ten-slide blueprint and design rhythm', () { - final plan = DeckPlanType.parse(_hierarchicalPlan()); + final plan = DeckPlan.parse(_hierarchicalPlan()); expect(plan.sections.map((section) => section.key), [ 'tension', @@ -220,7 +220,7 @@ void main() { test('validates deterministic fifteen- and twenty-slide blueprints', () { for (final count in [15, 20]) { - final plan = DeckPlanType.parse(_scaledHierarchicalPlan(count)); + final plan = DeckPlan.parse(_scaledHierarchicalPlan(count)); expect( validateDeckPlan(plan, expectedSlideCount: count), @@ -248,7 +248,7 @@ void main() { slides[index]['treatment'] = index < 4 ? 'content' : 'data'; } - final errors = validateDeckPlan(DeckPlanType.parse(data)); + final errors = validateDeckPlan(DeckPlan.parse(data)); expect(errors.where((error) => error.contains('treatment')), isEmpty); }); @@ -261,7 +261,7 @@ void main() { slide['treatment'] = 'content'; } - final issues = validateDeckPlanIssues(DeckPlanType.parse(data)); + final issues = validateDeckPlanIssues(DeckPlan.parse(data)); final rhythmIssues = issues .where((issue) => issue.code == GenerationValidationCode.designRhythm) .toList(); @@ -290,7 +290,7 @@ void main() { theme['version'] = 999; final finalSectionKeys = sections.last['slideKeys']! as List; finalSectionKeys.removeLast(); - final plan = DeckPlanType.parse(data); + final plan = DeckPlan.parse(data); final errors = validateDeckPlan(plan); @@ -304,7 +304,7 @@ void main() { }); test('rejects changes to exact requested theme and font selections', () { - final plan = DeckPlanType.parse(_hierarchicalPlan()); + final plan = DeckPlan.parse(_hierarchicalPlan()); final errors = validateDeckPlan( plan, @@ -330,7 +330,7 @@ void main() { final slides = data['slides']! as List>; slides[1]['composition'] = 'imageRight'; slides[1]['elements'] = []; - final plan = DeckPlanType.parse(data); + final plan = DeckPlan.parse(data); expect( validateDeckPlan(plan), @@ -341,6 +341,29 @@ void main() { ); }); + test('rejects a planned element without a matching composition', () { + final data = _hierarchicalPlan(); + final slides = data['slides']! as List>; + slides[1] + ..['composition'] = 'content' + ..['elements'] = [ + { + 'type': 'image', + 'purpose': 'Make the operating tension tangible', + 'source': 'assets/operating-tension.png', + }, + ]; + final plan = DeckPlan.parse(data); + + expect( + validateDeckPlan(plan), + contains( + 'Slide "cost" plans an image element but uses incompatible ' + 'composition "content".', + ), + ); + }); + test('accepts generated image intent with an exact style reference', () { final data = _hierarchicalPlan(); final slides = data['slides']! as List>; @@ -355,7 +378,7 @@ void main() { 'an operations team tracing one critical signal through noise', }, ]; - final plan = DeckPlanType.parse(data); + final plan = DeckPlan.parse(data); final errors = validateDeckPlan( plan, @@ -394,7 +417,7 @@ void main() { }, ]; } - final plan = DeckPlanType.parse(data); + final plan = DeckPlan.parse(data); final withoutStyle = validateDeckPlan( plan, @@ -429,7 +452,7 @@ void main() { final data = _hierarchicalPlan(); final slides = data['slides']! as List>; slides.last['contentUnits'] = ['Continue at signal-canvas.com/launch']; - final plan = DeckPlanType.parse(data); + final plan = DeckPlan.parse(data); final rejected = validateDeckPlan( plan, @@ -471,7 +494,7 @@ void main() { final data = _hierarchicalPlan(); final slides = data['slides']! as List>; slides.last['contentUnits'] = ['100% retention after 90 days']; - final unqualified = DeckPlanType.parse(data); + final unqualified = DeckPlan.parse(data); final rejected = validateDeckPlan( unqualified, @@ -483,7 +506,7 @@ void main() { slides.last['contentUnits'] = [ 'Projected scenario: 100% retention after an estimated 90 days', ]; - final qualified = DeckPlanType.parse(data); + final qualified = DeckPlan.parse(data); final allowed = validateDeckPlan( qualified, request: const DeckGenerationRequest( @@ -529,7 +552,7 @@ void main() { 'Planned target: 50% adoption after the proposed 90-day pilot', ]; final plannedTarget = validateDeckPlan( - DeckPlanType.parse(data), + DeckPlan.parse(data), request: const DeckGenerationRequest( userIntent: 'Describe a future pilot without claiming observed results.', @@ -543,7 +566,7 @@ void main() { slides.last['contentUnits'] = ['6 design partners']; final normalizedNumberWords = validateDeckPlan( - DeckPlanType.parse(data), + DeckPlan.parse(data), request: const DeckGenerationRequest( userIntent: 'Describe the six design partners.', slideCount: 10, @@ -560,7 +583,7 @@ void main() { '3 onboarding steps in Section 3 with no change to source systems', ]; final structuralCounts = validateDeckPlan( - DeckPlanType.parse(data), + DeckPlan.parse(data), request: const DeckGenerationRequest( userIntent: 'Describe onboarding with no change to source systems.', slideCount: 10, @@ -574,7 +597,7 @@ void main() { ); slides.last['contentUnits'] = ['0% disruption to source systems']; final inventedZero = validateDeckPlan( - DeckPlanType.parse(data), + DeckPlan.parse(data), request: const DeckGenerationRequest( userIntent: 'Describe onboarding with no change to source systems.', slideCount: 10, @@ -591,7 +614,7 @@ void main() { ); slides.last['contentUnits'] = ['A zero-friction onboarding workflow']; final qualitativeZero = validateDeckPlan( - DeckPlanType.parse(data), + DeckPlan.parse(data), request: const DeckGenerationRequest( userIntent: 'Describe a smooth onboarding workflow.', slideCount: 10, @@ -603,7 +626,7 @@ void main() { ); slides.last['contentUnits'] = ['Planned direction for next quarter']; final calendarQuarter = validateDeckPlan( - DeckPlanType.parse(data), + DeckPlan.parse(data), request: const DeckGenerationRequest( userIntent: 'Describe a roadmap direction.', slideCount: 10, @@ -615,7 +638,7 @@ void main() { ); slides.last['contentUnits'] = ['A quarter of teams changed direction']; final fractionalQuarter = validateDeckPlan( - DeckPlanType.parse(data), + DeckPlan.parse(data), request: const DeckGenerationRequest( userIntent: 'Describe a roadmap direction.', slideCount: 10, @@ -629,7 +652,7 @@ void main() { ); slides.last['contentUnits'] = ['Setup completes in one afternoon']; final structuralSourceDoesNotGroundDuration = validateDeckPlan( - DeckPlanType.parse(data), + DeckPlan.parse(data), request: const DeckGenerationRequest( userIntent: 'Bring the evidence into one workspace.', slideCount: 10, @@ -639,7 +662,7 @@ void main() { 'Illustrative scenario: initial insight in one afternoon', ]; final qualifiedDuration = validateDeckPlan( - DeckPlanType.parse(data), + DeckPlan.parse(data), request: const DeckGenerationRequest( userIntent: 'Bring the evidence into one workspace.', slideCount: 10, @@ -660,7 +683,7 @@ void main() { slides.last['contentUnits'] = ['1 beta outcome']; final structuralSourceDoesNotGroundMetric = validateDeckPlan( - DeckPlanType.parse(data), + DeckPlan.parse(data), request: const DeckGenerationRequest( userIntent: 'Turn fragmented evidence into one workspace.', slideCount: 10, @@ -683,7 +706,7 @@ void main() { ..['continuity'] = 'Transition after the 13 internal checkpoints.'; final issues = validateDeckPlanIssues( - DeckPlanType.parse(data), + DeckPlan.parse(data), request: const DeckGenerationRequest( userIntent: 'Close with a practical decision.', slideCount: 10, @@ -705,7 +728,7 @@ void main() { final data = _hierarchicalPlan(); final slides = data['slides']! as List>; slides.last['contentUnits'] = ['Start your free trial today']; - final plan = DeckPlanType.parse(data); + final plan = DeckPlan.parse(data); expect( validateDeckPlan( @@ -723,7 +746,7 @@ void main() { slides.last['contentUnits'] = ['SOC2 Compliant governance']; expect( validateDeckPlan( - DeckPlanType.parse(data), + DeckPlan.parse(data), request: const DeckGenerationRequest( userIntent: 'Describe the governance model.', slideCount: 10, @@ -739,7 +762,7 @@ void main() { slides.last['contentUnits'] = ['Prioritize evidence before commitments']; final issues = validateDeckPlanIssues( - DeckPlanType.parse(data), + DeckPlan.parse(data), request: const DeckGenerationRequest( userIntent: 'Close with an evidence-led operating decision.', slideCount: 10, @@ -772,7 +795,7 @@ void main() { ]; final errors = validateDeckPlan( - DeckPlanType.parse(data), + DeckPlan.parse(data), request: const DeckGenerationRequest( userIntent: 'Describe a fictional product beta and adoption options.', slideCount: 10, @@ -797,7 +820,7 @@ void main() { slides.last['contentUnits'] = ['Omitting real-time claims from this plan']; final negated = validateDeckPlan( - DeckPlanType.parse(data), + DeckPlan.parse(data), request: const DeckGenerationRequest( userIntent: 'Describe a fictional product beta.', slideCount: 10, @@ -809,7 +832,7 @@ void main() { 'Illustrative option: SSO, data residency, and automated workflows', ]; final explicitlyHypothetical = validateDeckPlan( - DeckPlanType.parse(data), + DeckPlan.parse(data), request: const DeckGenerationRequest( userIntent: 'Describe possible enterprise governance directions.', slideCount: 10, @@ -839,7 +862,7 @@ void main() { ]; final errors = validateDeckPlan( - DeckPlanType.parse(data), + DeckPlan.parse(data), request: const DeckGenerationRequest( userIntent: 'Present beta observations and discuss the evidence inbox without ' @@ -893,7 +916,7 @@ void main() { sections[1]['purpose'] = 'Introduce observations from six design partners.'; final errors = validateDeckPlan( - DeckPlanType.parse(data), + DeckPlan.parse(data), request: const DeckGenerationRequest( userIntent: 'Explain fragmented evidence and observations from six design ' @@ -917,7 +940,7 @@ void main() { ]; final rejected = validateDeckPlan( - DeckPlanType.parse(data), + DeckPlan.parse(data), request: const DeckGenerationRequest( userIntent: 'Cover the evidence inbox, API extensibility, onboarding, pricing ' @@ -947,7 +970,7 @@ void main() { 'Proposed tier — Pro: Advanced linked insights', ]; final qualified = validateDeckPlan( - DeckPlanType.parse(data), + DeckPlan.parse(data), request: const DeckGenerationRequest( userIntent: 'Cover the evidence inbox, API extensibility, onboarding, pricing ' @@ -975,7 +998,7 @@ void main() { ]; final errors = validateDeckPlan( - DeckPlanType.parse(data), + DeckPlan.parse(data), request: const DeckGenerationRequest( userIntent: 'Cover pricing shape, core product capabilities, and system ' @@ -1003,7 +1026,7 @@ void main() { 'The workflow was validated during the fictional beta.'; final issues = validateDeckPlanIssues( - DeckPlanType.parse(data), + DeckPlan.parse(data), request: const DeckGenerationRequest( userIntent: 'Describe a fictional beta using only supplied facts.', slideCount: 10, @@ -1034,14 +1057,14 @@ void main() { slides.last['contentUnits'] = ['38+ design partners']; final exactOnly = validateDeckPlan( - DeckPlanType.parse(data), + DeckPlan.parse(data), request: const DeckGenerationRequest( userIntent: 'The beta included 38 design partners.', slideCount: 10, ), ); final explicitlyOpenEnded = validateDeckPlan( - DeckPlanType.parse(data), + DeckPlan.parse(data), request: const DeckGenerationRequest( userIntent: 'The beta included 38+ design partners.', slideCount: 10, @@ -1067,14 +1090,14 @@ void main() { final slides = data['slides']! as List>; slides.last['contentUnits'] = ['Reclaiming 42% of the work week']; final changedMeaning = validateDeckPlan( - DeckPlanType.parse(data), + DeckPlan.parse(data), request: const DeckGenerationRequest( userIntent: 'Teams spent 42% less weekly synthesis time.', slideCount: 10, ), ); final changedMeaningIssues = validateDeckPlanIssues( - DeckPlanType.parse(data), + DeckPlan.parse(data), request: const DeckGenerationRequest( userIntent: 'Teams spent 42% less weekly synthesis time.', slideCount: 10, @@ -1094,7 +1117,7 @@ void main() { expect(changedMeaningIssues.where((issue) => issue.isBlocking), isEmpty); slides.last['contentUnits'] = ['42% less weekly synthesis time']; final preservedMeaning = validateDeckPlan( - DeckPlanType.parse(data), + DeckPlan.parse(data), request: const DeckGenerationRequest( userIntent: 'Teams spent 42% less weekly synthesis time.', slideCount: 10, @@ -1103,7 +1126,7 @@ void main() { slides.last['title'] = '42% productivity gain'; slides.last['contentUnits'] = ['Less weekly synthesis time']; final standaloneMetric = validateDeckPlan( - DeckPlanType.parse(data), + DeckPlan.parse(data), request: const DeckGenerationRequest( userIntent: 'Teams spent 42% less weekly synthesis time.', slideCount: 10, @@ -1114,7 +1137,7 @@ void main() { 'No change to source systems', ]; final normalizedWording = validateDeckPlan( - DeckPlanType.parse(data), + DeckPlan.parse(data), request: const DeckGenerationRequest( userIntent: 'The beta delivered 19% faster experiment decisions and no ' @@ -1128,7 +1151,7 @@ void main() { 'Six enterprise beta cohort', ]; final changedCohortMeaning = validateDeckPlan( - DeckPlanType.parse(data), + DeckPlan.parse(data), request: const DeckGenerationRequest( userIntent: 'Create one evidence workspace validated by six design partners.', @@ -1173,7 +1196,7 @@ void main() { final data = _hierarchicalPlan(); final slides = data['slides']! as List>; slides[6]['treatment'] = 'hero'; - final plan = DeckPlanType.parse(data); + final plan = DeckPlan.parse(data); final issues = validateDeckPlanIssues(plan); expect( @@ -1197,7 +1220,7 @@ void main() { }); test('requires metric plans to name the grounded numeric fact', () { - final plan = DeckPlanType.parse(_hierarchicalPlan()); + final plan = DeckPlan.parse(_hierarchicalPlan()); expect( validateDeckPlan( diff --git a/packages/playground/test/features/ai/quick_agent/core/engine/schemas/theme_plan_contract_test.dart b/packages/playground/test/features/ai/quick_agent/core/engine/schemas/theme_plan_contract_test.dart index 7cc768976..812cfa297 100644 --- a/packages/playground/test/features/ai/quick_agent/core/engine/schemas/theme_plan_contract_test.dart +++ b/packages/playground/test/features/ai/quick_agent/core/engine/schemas/theme_plan_contract_test.dart @@ -112,7 +112,7 @@ void main() { as Map; final themeJson = Map.from(artifact['theme']! as Map); final expected = Map.from(artifact['expected']! as Map); - final reference = DeckThemeReferenceType.parse(themeJson); + final reference = DeckThemeReference.parse(themeJson); final resolved = resolveDeckThemeReference( reference, themeCatalog: PresentationThemeCatalog.withDefaults(), diff --git a/packages/playground/test/features/ai/quick_agent/core/engine/services/deck_generator_images_test.dart b/packages/playground/test/features/ai/quick_agent/core/engine/services/deck_generator_images_test.dart index ff3518fa2..e5792d9fa 100644 --- a/packages/playground/test/features/ai/quick_agent/core/engine/services/deck_generator_images_test.dart +++ b/packages/playground/test/features/ai/quick_agent/core/engine/services/deck_generator_images_test.dart @@ -12,9 +12,11 @@ void main() { () async { final generator = _TrackingImageGenerator(); final progress = <(int, int)>[]; + final plan = _plan(); + final original = plan.toJson(); final result = await generateImagesForPlan( - plan: _plan(), + plan: plan, imageStyle: PresentationImageStyleCatalog.withDefaults().resolve( id: 'watercolor', version: 1, @@ -50,6 +52,11 @@ void main() { expect(failedSlide.elements, isEmpty); expect(failedSlide.composition, 'content'); expect(failedSlide.treatment, 'content'); + expect(result.plan.slides[1], plan.slides[1]); + expect(result.plan.theme, plan.theme); + expect(result.plan.sections, plan.sections); + expect(DeckPlan.parse(result.plan.toJson()), result.plan); + expect(plan.toJson(), original); }, ); @@ -107,7 +114,7 @@ final class _SuccessfulImageGenerator implements ImageGenerator { } } -DeckPlanType _plan() => DeckPlanType.parse({ +DeckPlan _plan() => DeckPlan.parse({ 'topic': 'Ocean systems', 'story': 'Move from ocean risk to practical restoration.', 'theme': {'id': 'technical-paper', 'version': 1, 'density': 'balanced'}, diff --git a/packages/playground/test/features/ai/quick_agent/core/engine/services/deck_generator_service_test.dart b/packages/playground/test/features/ai/quick_agent/core/engine/services/deck_generator_service_test.dart index 1918320ec..7b76bfc16 100644 --- a/packages/playground/test/features/ai/quick_agent/core/engine/services/deck_generator_service_test.dart +++ b/packages/playground/test/features/ai/quick_agent/core/engine/services/deck_generator_service_test.dart @@ -5,6 +5,7 @@ import 'dart:convert'; import 'package:flutter_test/flutter_test.dart'; import 'package:google_cloud_ai_generativelanguage_v1beta/generativelanguage.dart' as google_ai; +import 'package:googleai_dart/googleai_dart.dart' as modern_google_ai; import 'package:playground/core/domain/design/presentation_theme_catalog.dart'; import 'package:playground/features/ai/image_generation/image_generator.dart'; import 'package:playground/features/ai/quick_agent/core/engine/services/deck_generation_request.dart'; @@ -54,7 +55,7 @@ void main() { expect(planning.success, isTrue); expect(planning.plan, isNotNull); expect(planningClient.requests, hasLength(1)); - expect(planningClient.requests.single.model, 'models/gemini-3.5-flash'); + expect(planningClient.requests.single.model, 'models/gemini-3.7-flash'); expect(planningClient.isClosed, isTrue); final compositionClient = _FakeGenerationModelClient([ @@ -78,7 +79,7 @@ void main() { expect(compositionClient.requests, hasLength(1)); expect( compositionClient.requests.single.model, - 'models/gemini-3.1-flash-lite', + 'models/gemini-3.5-flash-lite', ); expect(compositionClient.isClosed, isTrue); }); @@ -396,10 +397,10 @@ void main() { expect(result.slides, hasLength(2)); expect(client.requests, hasLength(4)); expect(client.requests.map((request) => request.model), [ - 'models/gemini-3.5-flash', - 'models/gemini-3.1-flash-lite', - 'models/gemini-3.1-flash-lite', - 'models/gemini-3.1-flash-lite', + 'models/gemini-3.7-flash', + 'models/gemini-3.5-flash-lite', + 'models/gemini-3.5-flash-lite', + 'models/gemini-3.5-flash-lite', ]); final repairPrompt = client.requests[1].systemInstruction!.parts.single.text!; @@ -877,8 +878,8 @@ void main() { expect(result.success, isTrue); expect(client.requests, hasLength(2)); expect(client.requests.map((request) => request.model), [ - 'models/gemini-3.5-flash', - 'models/gemini-3.1-flash-lite', + 'models/gemini-3.7-flash', + 'models/gemini-3.5-flash-lite', ]); final outlinePrompt = client.requests.first.systemInstruction!.parts.single.text; @@ -919,16 +920,15 @@ void main() { expect(widgetSchema.properties, isNot(contains('text'))); expect( client.requests.map( - (request) => request.generationConfig!.thinkingConfig, + (request) => adaptGenerationRequest( + request, + ).request.generationConfig!.thinkingConfig!.thinkingLevel, ), - everyElement( - isA().having( - (config) => config.thinkingBudget, - 'thinking budget', - 0, - ), - ), - reason: 'Deck generation must explicitly disable thinking.', + [ + modern_google_ai.ThinkingLevel.low, + modern_google_ai.ThinkingLevel.minimal, + ], + reason: 'Deck generation must use each model\'s lowest thinking level.', ); expect(client.isClosed, isTrue); final requests = traces @@ -980,9 +980,9 @@ void main() { ]); expect(client.requests, hasLength(3)); expect(client.requests.map((request) => request.model), [ - 'models/gemini-3.5-flash', - 'models/gemini-3.1-flash-lite', - 'models/gemini-3.1-flash-lite', + 'models/gemini-3.7-flash', + 'models/gemini-3.5-flash-lite', + 'models/gemini-3.5-flash-lite', ]); expect( progress @@ -1261,10 +1261,10 @@ void main() { 'evidence', 'closing', ]); - expect(recovered.slides.first.toMap(), partial.slides.first.toMap()); - expect(recovered.slides.last.toMap(), partial.slides.last.toMap()); + expect(recovered.slides.first.toJson(), partial.slides.first.toJson()); + expect(recovered.slides.last.toJson(), partial.slides.last.toJson()); expect(retryClient.requests, hasLength(1)); - expect(retryClient.requests.single.model, 'models/gemini-3.1-flash-lite'); + expect(retryClient.requests.single.model, 'models/gemini-3.5-flash-lite'); expect( retryClient.requests.single.systemInstruction!.parts.single.text, contains('Start here'), diff --git a/packages/playground/test/features/ai/quick_agent/core/engine/services/error_classifier_test.dart b/packages/playground/test/features/ai/quick_agent/core/engine/services/error_classifier_test.dart index 1e75acb5b..ad069e6ab 100644 --- a/packages/playground/test/features/ai/quick_agent/core/engine/services/error_classifier_test.dart +++ b/packages/playground/test/features/ai/quick_agent/core/engine/services/error_classifier_test.dart @@ -1,45 +1,86 @@ import 'package:flutter_test/flutter_test.dart'; +import 'package:googleai_dart/googleai_dart.dart' as google_ai; import 'package:playground/features/ai/quick_agent/core/engine/services/error_classifier.dart'; void main() { const classifier = ErrorClassifier(); group('classify', () { + test('uses SDK status codes before incidental message patterns', () { + expect( + classifier.classify( + const google_ai.AuthenticationException( + message: 'A connection could not be authenticated.', + ), + ), + ErrorCategory.authentication, + ); + expect( + classifier.classify( + const google_ai.ApiException( + statusCode: 504, + message: 'Deadline expired while processing 401 items.', + ), + ), + ErrorCategory.network, + ); + }); + test('detects rate-limit / overload errors', () { - expect(classifier.classify('Error 429: quota exceeded'), - ErrorCategory.rateLimit); - expect(classifier.classify('RESOURCE_EXHAUSTED'), - ErrorCategory.rateLimit); - expect(classifier.classify('The model is overloaded'), - ErrorCategory.rateLimit); + expect( + classifier.classify('Error 429: quota exceeded'), + ErrorCategory.rateLimit, + ); + expect( + classifier.classify('RESOURCE_EXHAUSTED'), + ErrorCategory.rateLimit, + ); + expect( + classifier.classify('The model is overloaded'), + ErrorCategory.rateLimit, + ); }); test('detects authentication errors', () { - expect(classifier.classify('401 Unauthorized'), - ErrorCategory.authentication); - expect(classifier.classify('API key not valid'), - ErrorCategory.authentication); expect( - classifier.classify('403 forbidden'), ErrorCategory.authentication); + classifier.classify('401 Unauthorized'), + ErrorCategory.authentication, + ); + expect( + classifier.classify('API key not valid'), + ErrorCategory.authentication, + ); + expect( + classifier.classify('403 forbidden'), + ErrorCategory.authentication, + ); }); test('detects network errors', () { - expect(classifier.classify('SocketException: failed'), - ErrorCategory.network); + expect( + classifier.classify('SocketException: failed'), + ErrorCategory.network, + ); expect(classifier.classify('Connection timeout'), ErrorCategory.network); expect(classifier.classify('Failed host lookup'), ErrorCategory.network); }); test('detects safety-filter errors', () { - expect(classifier.classify('Response blocked by safety'), - ErrorCategory.safetyFilter); - expect(classifier.classify('flagged as harmful'), - ErrorCategory.safetyFilter); + expect( + classifier.classify('Response blocked by safety'), + ErrorCategory.safetyFilter, + ); + expect( + classifier.classify('flagged as harmful'), + ErrorCategory.safetyFilter, + ); }); test('falls back to unknown', () { - expect(classifier.classify('some unexpected failure'), - ErrorCategory.unknown); + expect( + classifier.classify('some unexpected failure'), + ErrorCategory.unknown, + ); }); test('is case-insensitive', () { @@ -47,23 +88,31 @@ void main() { }); test('classifies non-string error objects via toString', () { - expect(classifier.classify(Exception('429 rate limit')), - ErrorCategory.rateLimit); + expect( + classifier.classify(Exception('429 rate limit')), + ErrorCategory.rateLimit, + ); }); test('applies patterns in priority order (rate limit before auth)', () { // Contains both a quota and a 403 marker; rate limit is checked first. - expect(classifier.classify('quota exceeded (403)'), - ErrorCategory.rateLimit); + expect( + classifier.classify('quota exceeded (403)'), + ErrorCategory.rateLimit, + ); }); }); group('getUserMessage', () { test('returns the category user message', () { - expect(classifier.getUserMessage('429 quota'), - ErrorCategory.rateLimit.userMessage); - expect(classifier.getUserMessage('mystery'), - ErrorCategory.unknown.userMessage); + expect( + classifier.getUserMessage('429 quota'), + ErrorCategory.rateLimit.userMessage, + ); + expect( + classifier.getUserMessage('mystery'), + ErrorCategory.unknown.userMessage, + ); }); }); } diff --git a/packages/playground/test/features/ai/quick_agent/core/engine/services/generated_slide_validator_test.dart b/packages/playground/test/features/ai/quick_agent/core/engine/services/generated_slide_validator_test.dart index bfef33824..e255672d2 100644 --- a/packages/playground/test/features/ai/quick_agent/core/engine/services/generated_slide_validator_test.dart +++ b/packages/playground/test/features/ai/quick_agent/core/engine/services/generated_slide_validator_test.dart @@ -496,6 +496,34 @@ void main() { expect(content, isNot(contains('- **Load:**'))); }); + test('uses the planned title when a generated hero heading is too long', () { + final raw = _slideWithBlock({ + 'type': 'block', + 'content': + '# SuperDeck transforms rough ideas into presentation-ready decks ' + 'through structured story architecture\n\n' + 'A concise live demonstration.', + }); + + final normalized = normalizeGeneratedSlideForPlan( + rawSlide: raw, + planSlide: _planSlide(composition: 'title', title: 'SuperDeck Overview'), + ); + final content = + ((((normalized['sections'] as List).single as Map)['blocks'] as List) + .single + as Map)['content'] + as String; + + expect(content, '# SuperDeck Overview\n\nA concise live demonstration.'); + expect( + ((((raw['sections'] as List).single as Map)['blocks'] as List).single + as Map)['content'], + startsWith('# SuperDeck transforms rough ideas'), + reason: 'Normalization must not mutate the traceable raw draft.', + ); + }); + test('anchors implicit title and body rows in a vertical composition', () { final raw = { 'key': 'test-slide', @@ -1605,14 +1633,15 @@ Map _slideWithBlock(Map block) => { ], }; -DeckPlanSlideType _planSlide({ +DeckPlanSlide _planSlide({ required String composition, String density = 'balanced', String? treatment, + String title = 'Test slide', List> elements = const [], -}) => DeckPlanSlideType.parse({ +}) => DeckPlanSlide.parse({ 'key': 'test-slide', - 'title': 'Test slide', + 'title': title, 'purpose': 'Exercise the generated slide contract.', 'sectionKey': 'main', 'assertion': 'The planned assertion must be visible.', diff --git a/packages/playground/test/features/ai/quick_agent/core/engine/services/generation_model_client_test.dart b/packages/playground/test/features/ai/quick_agent/core/engine/services/generation_model_client_test.dart new file mode 100644 index 000000000..86d4465a3 --- /dev/null +++ b/packages/playground/test/features/ai/quick_agent/core/engine/services/generation_model_client_test.dart @@ -0,0 +1,73 @@ +import 'dart:convert'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:google_cloud_ai_generativelanguage_v1beta/generativelanguage.dart' + as google_ai; +import 'package:http/http.dart' as http; +import 'package:http/testing.dart'; +import 'package:playground/features/ai/quick_agent/core/constants/gemini_models.dart'; +import 'package:playground/features/ai/quick_agent/core/engine/services/error_classifier.dart'; +import 'package:playground/features/ai/quick_agent/core/engine/services/generation_model_client.dart'; + +void main() { + const classifier = ErrorClassifier(); + + group('Google transport error messages', () { + for (final (status, message, category) in const [ + ( + 401, + 'Request had invalid authentication credentials.', + ErrorCategory.authentication, + ), + (403, 'Permission denied.', ErrorCategory.authentication), + (408, 'Deadline expired.', ErrorCategory.network), + (429, 'Request rejected.', ErrorCategory.rateLimit), + ( + 504, + 'Deadline expired before operation could complete.', + ErrorCategory.network, + ), + (400, 'Response blocked by safety filters.', ErrorCategory.safetyFilter), + (503, 'The model is overloaded.', ErrorCategory.rateLimit), + (503, 'The service is currently unavailable.', ErrorCategory.unknown), + ]) { + test('HTTP $status ($message) gives ${category.name} guidance', () async { + await http.runWithClient( + () async { + final client = GoogleGenerationModelClient.fromApiKey('test-key'); + addTearDown(client.close); + + await expectLater( + client.generateContent( + google_ai.GenerateContentRequest( + model: GeminiModelNames.gemini37Flash, + contents: [ + google_ai.Content( + role: 'user', + parts: [google_ai.Part(text: 'Generate a test outline.')], + ), + ], + ), + ), + throwsA( + isA().having( + classifier.getUserMessage, + 'user message', + category.userMessage, + ), + ), + ); + }, + () => MockClient( + (_) async => http.Response( + jsonEncode({ + 'error': {'code': status, 'message': message}, + }), + status, + ), + ), + ); + }); + } + }); +} diff --git a/packages/playground/test/features/ai/quick_agent/core/engine/services/generation_quality_report_test.dart b/packages/playground/test/features/ai/quick_agent/core/engine/services/generation_quality_report_test.dart index 82d7e4114..777067544 100644 --- a/packages/playground/test/features/ai/quick_agent/core/engine/services/generation_quality_report_test.dart +++ b/packages/playground/test/features/ai/quick_agent/core/engine/services/generation_quality_report_test.dart @@ -179,7 +179,7 @@ void main() { }); } -DeckPlanType _plan() => DeckPlanType.parse({ +DeckPlan _plan() => DeckPlan.parse({ 'topic': 'Decision quality', 'story': 'Move from context to evidence to a clear choice.', 'theme': {'id': 'editorial-midnight', 'version': 1, 'density': 'spacious'}, @@ -280,7 +280,7 @@ List _traces() => [ ), ]; -DeckPlanType _rhythmPlan() => DeckPlanType.parse({ +DeckPlan _rhythmPlan() => DeckPlan.parse({ 'topic': 'Decision rhythm', 'story': 'Move through four clear steps.', 'theme': {'id': 'editorial-midnight', 'version': 1, 'density': 'spacious'}, diff --git a/packages/playground/test/features/ai/quick_agent/domain/commands/generate_deck_command_test.dart b/packages/playground/test/features/ai/quick_agent/domain/commands/generate_deck_command_test.dart index 01e5cbb58..7960964af 100644 --- a/packages/playground/test/features/ai/quick_agent/domain/commands/generate_deck_command_test.dart +++ b/packages/playground/test/features/ai/quick_agent/domain/commands/generate_deck_command_test.dart @@ -93,7 +93,7 @@ final class _StubDeckGeneratorService extends DeckGeneratorService { }) async => result; } -DeckPlanType _plan() => DeckPlanType.parse({ +DeckPlan _plan() => DeckPlan.parse({ 'topic': 'Partial deck', 'story': 'Keep accepted work when one slide fails.', 'theme': {'id': 'technical-paper', 'version': 1, 'density': 'balanced'}, diff --git a/packages/playground/test/features/ai/quick_agent/domain/generated_deck_result_applier_test.dart b/packages/playground/test/features/ai/quick_agent/domain/generated_deck_result_applier_test.dart index 57854f61d..345fbd14d 100644 --- a/packages/playground/test/features/ai/quick_agent/domain/generated_deck_result_applier_test.dart +++ b/packages/playground/test/features/ai/quick_agent/domain/generated_deck_result_applier_test.dart @@ -118,7 +118,7 @@ Slide _generatedSlide(String assetKey) => Slide.parse({ ], }); -DeckPlanType _plan(String assetKey) => DeckPlanType.parse({ +DeckPlan _plan(String assetKey) => DeckPlan.parse({ 'topic': 'Generated artwork', 'story': 'One image supports one clear point.', 'theme': {'id': 'technical-paper', 'version': 1, 'density': 'balanced'}, diff --git a/packages/playground/test/features/ai/quick_agent/presentation/generation_lab_page_test.dart b/packages/playground/test/features/ai/quick_agent/presentation/generation_lab_page_test.dart index fe8f9018b..035a0bf9b 100644 --- a/packages/playground/test/features/ai/quick_agent/presentation/generation_lab_page_test.dart +++ b/packages/playground/test/features/ai/quick_agent/presentation/generation_lab_page_test.dart @@ -139,12 +139,12 @@ Finder _buildSlidesButton() => find.ancestor( matching: find.byWidgetPredicate((widget) => widget is OutlinedButton), ); -DeckPlanType _plan(DeckGenerationRequest request) { +DeckPlan _plan(DeckGenerationRequest request) { final themes = PresentationThemeCatalog.withDefaults(); final typography = PresentationTypographyCatalog.withDefaults(); final descriptor = themes.current(request.themeId!)!; - return DeckPlanType.parse({ + return DeckPlan.parse({ 'topic': 'SuperDeck', 'story': 'A rough idea becomes a presentation-ready story.', 'theme': buildDeckThemeReference( @@ -184,7 +184,7 @@ DeckPlanType _plan(DeckGenerationRequest request) { final class _FakeGenerationLabService extends DeckGeneratorService { _FakeGenerationLabService(this.planned) : super(apiKey: 'test-key'); - final DeckPlanType planned; + final DeckPlan planned; var planCalls = 0; var compositionCalls = 0; @@ -202,7 +202,7 @@ final class _FakeGenerationLabService extends DeckGeneratorService { @override Future generateFromPlan( DeckGenerationRequest request, - DeckPlanType approvedPlan, { + DeckPlan approvedPlan, { onProgress, onTrace, isCancelled, diff --git a/packages/playground/test/features/ai/wizard/core/ai/catalog/catalog_data_normalizer_test.dart b/packages/playground/test/features/ai/wizard/core/ai/catalog/catalog_data_normalizer_test.dart index e1cdc2836..ad53abca6 100644 --- a/packages/playground/test/features/ai/wizard/core/ai/catalog/catalog_data_normalizer_test.dart +++ b/packages/playground/test/features/ai/wizard/core/ai/catalog/catalog_data_normalizer_test.dart @@ -21,17 +21,21 @@ void main() { }); test('recurses into nested maps', () { - final result = normalizeCatalogData({ - 'outer': {'literalNumber': 3}, - }) as Map; + final result = + normalizeCatalogData({ + 'outer': {'literalNumber': 3}, + }) + as Map; expect((result['outer'] as Map)['literalNumber'], 3.0); }); test('recurses into lists', () { - final result = normalizeCatalogData([ - {'literalNumber': 1}, - {'literalNumber': 2}, - ]) as List; + final result = + normalizeCatalogData([ + {'literalNumber': 1}, + {'literalNumber': 2}, + ]) + as List; expect((result[0] as Map)['literalNumber'], 1.0); expect((result[1] as Map)['literalNumber'], 2.0); }); diff --git a/packages/playground/test/features/ai/wizard/core/ai/catalog/wizard_option_icon_test.dart b/packages/playground/test/features/ai/wizard/core/ai/catalog/wizard_option_icon_test.dart index a4089910c..229afd22e 100644 --- a/packages/playground/test/features/ai/wizard/core/ai/catalog/wizard_option_icon_test.dart +++ b/packages/playground/test/features/ai/wizard/core/ai/catalog/wizard_option_icon_test.dart @@ -4,7 +4,7 @@ import 'package:playground/features/ai/wizard/core/ai/catalog/wizard_option_icon void main() { test('radio option icons are constrained to the Wizard vocabulary', () { - final option = InputOptionType.parse({ + final option = InputOption.parse({ 'title': 'Business leaders', 'icon': 'business', }); @@ -12,7 +12,7 @@ void main() { expect(option.icon, WizardOptionIcon.business); expect(WizardOptionIcon.values, hasLength(12)); expect( - () => InputOptionType.parse({ + () => InputOption.parse({ 'title': 'Unknown option', 'icon': 'material-home', }), diff --git a/packages/playground/test/features/ai/wizard/core/ai/catalog/wizard_schema_validation_test.dart b/packages/playground/test/features/ai/wizard/core/ai/catalog/wizard_schema_validation_test.dart new file mode 100644 index 000000000..9954d5a4f --- /dev/null +++ b/packages/playground/test/features/ai/wizard/core/ai/catalog/wizard_schema_validation_test.dart @@ -0,0 +1,209 @@ +import 'package:ack/ack.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:playground/features/ai/wizard/core/ai/catalog/ask_user_checkbox.dart'; +import 'package:playground/features/ai/wizard/core/ai/catalog/ask_user_radio.dart'; +import 'package:playground/features/ai/wizard/core/ai/catalog/ask_user_slider.dart'; + +const _action = { + 'name': 'submit_answer', + 'context': [], +}; + +void main() { + group('AskUserRadio schema', () { + test('accepts one option', () { + final model = AskUserRadio.parse({ + 'question': 'Choose one', + 'options': [ + {'title': 'Only choice'}, + ], + 'action': _action, + }); + + expect(model.options, hasLength(1)); + }); + + test('rejects an empty options list', () { + expect( + () => AskUserRadio.parse({ + 'question': 'Choose one', + 'options': [], + 'action': _action, + }), + throwsA(isA()), + ); + }); + }); + + group('AskUserCheckbox schema', () { + test('accepts zero bounds and an empty initial selection', () { + final model = AskUserCheckbox.parse( + _checkboxData( + items: ['One'], + selectedItems: const [], + minSelections: 0, + maxSelections: 0, + ), + ); + + expect(model.minSelections, 0); + expect(model.maxSelections, 0); + expect(model.selectedItems, isEmpty); + }); + + test('accepts bounds equal to the item count', () { + final model = AskUserCheckbox.parse( + _checkboxData( + items: ['One', 'Two'], + selectedItems: const ['One', 'Two'], + minSelections: 2, + maxSelections: 2, + ), + ); + + expect(model.selectedItems, ['One', 'Two']); + }); + + test('allows an initial selection below the minimum', () { + final model = AskUserCheckbox.parse( + _checkboxData( + items: ['One', 'Two', 'Three'], + selectedItems: const ['One'], + minSelections: 2, + maxSelections: 3, + ), + ); + + expect(model.selectedItems, ['One']); + }); + + test('accepts an initial selection at the maximum', () { + final model = AskUserCheckbox.parse( + _checkboxData( + items: ['One', 'Two', 'Three'], + selectedItems: const ['One', 'Two'], + maxSelections: 2, + ), + ); + + expect(model.selectedItems, ['One', 'Two']); + }); + + for (final (description, data) in <(String, Map)>[ + ('empty items', _checkboxData(items: const [])), + ('empty item labels', _checkboxData(items: const ['One', ''])), + ('duplicate items', _checkboxData(items: const ['One', 'One'])), + ( + 'duplicate selected items', + _checkboxData( + items: const ['One', 'Two'], + selectedItems: const ['One', 'One'], + ), + ), + ( + 'selected items absent from items', + _checkboxData( + items: const ['One', 'Two'], + selectedItems: const ['Three'], + ), + ), + ( + 'negative minimum', + _checkboxData(items: const ['One'], minSelections: -1), + ), + ( + 'negative maximum', + _checkboxData(items: const ['One'], maxSelections: -1), + ), + ( + 'minimum above the item count', + _checkboxData(items: const ['One'], minSelections: 2), + ), + ( + 'maximum above the item count', + _checkboxData(items: const ['One'], maxSelections: 2), + ), + ( + 'maximum below the default minimum', + _checkboxData(items: const ['One'], maxSelections: 0), + ), + ( + 'minimum above the maximum', + _checkboxData( + items: const ['One', 'Two'], + minSelections: 2, + maxSelections: 1, + ), + ), + ( + 'initial selection above the maximum', + _checkboxData( + items: const ['One', 'Two'], + selectedItems: const ['One', 'Two'], + maxSelections: 1, + ), + ), + ]) { + test('rejects $description', () { + expect(() => AskUserCheckbox.parse(data), throwsA(isA())); + }); + } + }); + + group('AskUserSlider schema', () { + for (final (description, min, max, value) in const [ + ('a default at the lower boundary', 1, 5, 1), + ('a default at the upper boundary', 1, 5, 5), + ('equal bounds and matching default', 3, 3, 3), + ]) { + test('accepts $description', () { + final model = AskUserSlider.parse( + _sliderData(min: min, max: max, value: value), + ); + + expect(model.defaultValue, value); + }); + } + + for (final (description, min, max, value) in const [ + ('minimum above maximum', 5, 4, 5), + ('default below minimum', 2, 5, 1), + ('default above maximum', 2, 5, 6), + ]) { + test('rejects $description', () { + expect( + () => AskUserSlider.parse( + _sliderData(min: min, max: max, value: value), + ), + throwsA(isA()), + ); + }); + } + }); +} + +Map _checkboxData({ + required List items, + List? selectedItems, + int? minSelections, + int? maxSelections, +}) => { + 'question': 'Choose any', + 'items': items, + 'selectedItems': ?selectedItems, + 'minSelections': ?minSelections, + 'maxSelections': ?maxSelections, + 'action': _action, +}; + +Map _sliderData({ + required int min, + required int max, + required int value, +}) => { + 'question': 'Choose a number', + 'minValue': min, + 'maxValue': max, + 'defaultValue': value, + 'action': _action, +}; diff --git a/packages/playground/test/features/ai/wizard/presentation/wizard_generation_controller_test.dart b/packages/playground/test/features/ai/wizard/presentation/wizard_generation_controller_test.dart index 070969f52..5b593033b 100644 --- a/packages/playground/test/features/ai/wizard/presentation/wizard_generation_controller_test.dart +++ b/packages/playground/test/features/ai/wizard/presentation/wizard_generation_controller_test.dart @@ -245,14 +245,14 @@ void main() { }); } -DeckPlanType _plan( +DeckPlan _plan( DeckGenerationRequest request, { List slideKeys = const ['opening'], }) { final themes = PresentationThemeCatalog.withDefaults(); final typography = PresentationTypographyCatalog.withDefaults(); final descriptor = themes.current(request.themeId!)!; - return DeckPlanType.parse({ + return DeckPlan.parse({ 'topic': request.userIntent, 'story': 'Small interventions build toward city-scale resilience.', 'theme': buildDeckThemeReference( @@ -297,10 +297,10 @@ final class _FakeWizardGenerationService extends DeckGeneratorService { this.partialComposition = false, }) : super(apiKey: 'test-key'); - final DeckPlanType planned; + final DeckPlan planned; final String? compositionError; final bool partialComposition; - DeckPlanType? approvedPlan; + DeckPlan? approvedPlan; Completer? pendingPlanning; Completer? pendingComposition; String? planningError; @@ -323,7 +323,7 @@ final class _FakeWizardGenerationService extends DeckGeneratorService { @override Future generateFromPlan( DeckGenerationRequest request, - DeckPlanType approvedPlan, { + DeckPlan approvedPlan, { onProgress, onTrace, isCancelled, @@ -381,7 +381,7 @@ final class _FakeWizardGenerationService extends DeckGeneratorService { DeckGenerationResult _successfulResult( DeckGeneratorService service, - DeckPlanType plan, + DeckPlan plan, ) => DeckGenerationResult.success( slides: [_generatedSlide('opening')], plan: plan, diff --git a/packages/playground/test/features/ai/wizard/presentation/wizard_outline_review_test.dart b/packages/playground/test/features/ai/wizard/presentation/wizard_outline_review_test.dart index 9ccff18d6..ee25775a9 100644 --- a/packages/playground/test/features/ai/wizard/presentation/wizard_outline_review_test.dart +++ b/packages/playground/test/features/ai/wizard/presentation/wizard_outline_review_test.dart @@ -170,7 +170,7 @@ void main() { }); } -DeckPlanType _plan() => DeckPlanType.parse({ +DeckPlan _plan() => DeckPlan.parse({ 'topic': 'Urban gardens', 'story': 'Small interventions build city-scale resilience.', 'theme': {'id': 'technical-paper', 'version': 1, 'density': 'balanced'}, @@ -202,8 +202,8 @@ DeckPlanType _plan() => DeckPlanType.parse({ ], }); -DeckPlanType _planWithSlides(int count) { - final data = Map.from(_plan()); +DeckPlan _planWithSlides(int count) { + final data = _plan().toJson(); final section = Map.from( (data['sections']! as List).single! as Map, ); @@ -218,5 +218,5 @@ DeckPlanType _planWithSlides(int count) { data['sections'] = [section]; data['slides'] = slides; - return DeckPlanType.parse(data); + return DeckPlan.parse(data); } diff --git a/packages/playground/test/features/ai/wizard/presentation/wizard_page_test.dart b/packages/playground/test/features/ai/wizard/presentation/wizard_page_test.dart index 00f881bd0..d69291c20 100644 --- a/packages/playground/test/features/ai/wizard/presentation/wizard_page_test.dart +++ b/packages/playground/test/features/ai/wizard/presentation/wizard_page_test.dart @@ -169,7 +169,7 @@ const _pageRequest = DeckGenerationRequest( final class _PageGenerationService extends DeckGeneratorService { _PageGenerationService() : super(apiKey: 'test-key'); - final DeckPlanType _plan = _pagePlan(); + final DeckPlan _plan = _pagePlan(); @override Future plan( @@ -182,7 +182,7 @@ final class _PageGenerationService extends DeckGeneratorService { @override Future generateFromPlan( DeckGenerationRequest request, - DeckPlanType approvedPlan, { + DeckPlan approvedPlan, { onProgress, onTrace, isCancelled, @@ -213,11 +213,11 @@ final class _PageGenerationService extends DeckGeneratorService { ); } -DeckPlanType _pagePlan() { +DeckPlan _pagePlan() { final themes = PresentationThemeCatalog.withDefaults(); final typography = PresentationTypographyCatalog.withDefaults(); final descriptor = themes.current('technical-paper')!; - return DeckPlanType.parse({ + return DeckPlan.parse({ 'topic': _pageRequest.userIntent, 'story': 'Small interventions build city-scale resilience.', 'theme': buildDeckThemeReference( diff --git a/packages/playground/test/features/editor/editor_store_test.dart b/packages/playground/test/features/editor/editor_store_test.dart index 72f3d9366..c018eba56 100644 --- a/packages/playground/test/features/editor/editor_store_test.dart +++ b/packages/playground/test/features/editor/editor_store_test.dart @@ -28,8 +28,10 @@ void main() { group('previewSidebarWidth', () { test('defaults to the minimum width', () { - expect(newStore().previewSidebarWidth, - EditorStore.minPreviewSidebarWidth); + expect( + newStore().previewSidebarWidth, + EditorStore.minPreviewSidebarWidth, + ); }); test('clamps below the minimum', () { @@ -69,22 +71,28 @@ void main() { group('customizationSidebarWidth', () { test('defaults to the maximum width', () { - expect(newStore().customizationSidebarWidth, - EditorStore.maxCustomizationSidebarWidth); + expect( + newStore().customizationSidebarWidth, + EditorStore.maxCustomizationSidebarWidth, + ); }); test('clamps below the minimum', () { final store = newStore(); store.customizationSidebarWidth = 0; - expect(store.customizationSidebarWidth, - EditorStore.minCustomizationSidebarWidth); + expect( + store.customizationSidebarWidth, + EditorStore.minCustomizationSidebarWidth, + ); }); test('clamps above the maximum', () { final store = newStore(); store.customizationSidebarWidth = 10000; - expect(store.customizationSidebarWidth, - EditorStore.maxCustomizationSidebarWidth); + expect( + store.customizationSidebarWidth, + EditorStore.maxCustomizationSidebarWidth, + ); }); }); diff --git a/packages/playground/test_live/ai_generation/ai_generation_smoke_test.dart b/packages/playground/test_live/ai_generation/ai_generation_smoke_test.dart index fe6ca4a7e..9358919a9 100644 --- a/packages/playground/test_live/ai_generation/ai_generation_smoke_test.dart +++ b/packages/playground/test_live/ai_generation/ai_generation_smoke_test.dart @@ -10,6 +10,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:google_cloud_ai_generativelanguage_v1beta/generativelanguage.dart' as google_ai; +import 'package:googleai_dart/googleai_dart.dart' as modern_google_ai; import 'package:image/image.dart' as image; import 'package:path/path.dart' as p; import 'package:playground/core/data/data_sources/memory_asset_cache_store.dart'; @@ -204,16 +205,22 @@ void main() { expect(client.requests, hasLength(4)); expect( client.requests.map( - (modelRequest) => - modelRequest.generationConfig!.thinkingConfig!.thinkingBudget, + (modelRequest) => adaptGenerationRequest( + modelRequest, + ).request.generationConfig!.thinkingConfig!.thinkingLevel, ), - everyElement(0), + [ + modern_google_ai.ThinkingLevel.low, + modern_google_ai.ThinkingLevel.minimal, + modern_google_ai.ThinkingLevel.minimal, + modern_google_ai.ThinkingLevel.minimal, + ], ); expect(client.requests.map((modelRequest) => modelRequest.model), [ - 'models/gemini-3.5-flash', - 'models/gemini-3.1-flash-lite', - 'models/gemini-3.1-flash-lite', - 'models/gemini-3.1-flash-lite', + 'models/gemini-3.7-flash', + 'models/gemini-3.5-flash-lite', + 'models/gemini-3.5-flash-lite', + 'models/gemini-3.5-flash-lite', ]); final sectionRequests = traces @@ -253,7 +260,7 @@ void main() { final markdown = const SlideSerializer().serialize(result.slides); final deckJson = { 'theme': serializeDeckThemeReference(result.plan!.theme), - 'slides': result.slides.map((slide) => slide.toMap()).toList(), + 'slides': result.slides.map((slide) => slide.toJson()).toList(), }; await tester.runAsync(() async { const encoder = JsonEncoder.withIndent(' '); @@ -384,7 +391,7 @@ void main() { ), themeReference: themeReference, slideCount: (deckJson['slides'] as List).length, - plan: DeckPlanType.parse(planJson), + plan: DeckPlan.parse(planJson), request: DeckGenerationRequest.fromMap(requestJson), rawSlides: [ for (final rawSlide in deckJson['slides']! as List) @@ -469,7 +476,7 @@ void main() { late Directory output; late String markdown; late ResolvedPresentationTheme theme; - late DeckPlanType plan; + late DeckPlan plan; late DeckGenerationRequest request; late List slides; List generatedImages = const []; @@ -553,7 +560,7 @@ void main() { } final deckJson = { 'theme': serializeDeckThemeReference(result.plan!.theme), - 'slides': result.slides.map((slide) => slide.toMap()).toList(), + 'slides': result.slides.map((slide) => slide.toJson()).toList(), }; await File(p.join(output.path, 'deck.json')).writeAsString( const JsonEncoder.withIndent(' ').convert(deckJson), @@ -853,7 +860,7 @@ Future _createRunDirectory(String fixture) async { Future _writeTraceArtifacts( Directory output, List traces, { - DeckPlanType? plan, + DeckPlan? plan, }) async { if (plan != null) { await File( @@ -1527,18 +1534,6 @@ Map _checkpointPlanDraft() { 'titleLeft', 'title', ]; - const treatments = [ - 'hero', - 'content', - 'data', - 'data', - 'content', - 'data', - 'quote', - 'content', - 'section', - 'closing', - ]; const roles = [ 'opening', 'problem', @@ -1586,7 +1581,6 @@ Map _checkpointPlanDraft() { { 'key': slideKeys[index], 'title': titles[index], - 'purpose': 'Advance the evidence-led decision story.', 'sectionKey': sectionKeys[index], 'assertion': index == 2 ? 'Teams spent 42% less weekly synthesis time.' @@ -1598,11 +1592,7 @@ Map _checkpointPlanDraft() { 'Practical implication for ${slideKeys[index]}.', ], 'narrativeRole': roles[index], - 'contentBrief': 'Keep the slide concise and decision-oriented.', - 'continuity': 'Connect this idea to the surrounding decision flow.', 'composition': compositions[index], - 'treatment': treatments[index], - 'density': index % 3 == 0 ? 'spacious' : 'balanced', 'elements': [], }, ], diff --git a/packages/plugins/pdf/CHANGELOG.md b/packages/plugins/pdf/CHANGELOG.md index fe15ccf7a..9dea8e28e 100644 --- a/packages/plugins/pdf/CHANGELOG.md +++ b/packages/plugins/pdf/CHANGELOG.md @@ -1,11 +1,12 @@ -## Unreleased +## 1.0.0 + +- **Breaking:** require `superdeck` and `superdeck_core` 1.0.0 as part of the + coordinated Ack 1.2 migration. - Capture PDF slide images with good quality on all platforms. - Use `FileSaver.saveFile` for default PDF saves on web and Linux, and surface unexpected save failures as export failures. -## 1.0.0 - - Extract PDF export support from `superdeck` into `superdeck_pdf`. - Provide the `PdfPlugin` runtime plugin entrypoint, plus `PdfExportOptions` and `PdfSaver` for custom save behavior. diff --git a/packages/superdeck/CHANGELOG.md b/packages/superdeck/CHANGELOG.md index 53aa7514a..b45fec240 100644 --- a/packages/superdeck/CHANGELOG.md +++ b/packages/superdeck/CHANGELOG.md @@ -1,4 +1,9 @@ -## Unreleased +## 1.0.0 + +- **Breaking:** remove `dart_mappable` from runtime deck configuration models. + `SlideTemplate`, `SlideConfiguration`, and `DeckOptions` now expose normal + `copyWith`, equality, hash, and string behavior while preserving explicit + `null` clearing for nullable fields. - **Breaking:** render supported Mermaid fences directly with `flutter_mermaid` instead of the removed browser-backed build plugin. @@ -74,8 +79,6 @@ widgets can signal when their capture-safe visual state is ready, replacing image-specific fixed render delays. -## 1.0.0 - - First stable release of `superdeck`. - Roll back experimental setext-heading hero parsing; ATX headers continue to use the shared helper. - Fix image hero-tag parsing to avoid inline parser overruns and keep Flutter/core paths aligned. diff --git a/packages/superdeck/lib/src/deck/deck_options.dart b/packages/superdeck/lib/src/deck/deck_options.dart index 6b12d6a1c..f8327dbbb 100644 --- a/packages/superdeck/lib/src/deck/deck_options.dart +++ b/packages/superdeck/lib/src/deck/deck_options.dart @@ -1,15 +1,17 @@ -import 'package:flutter/widgets.dart'; -import 'package:dart_mappable/dart_mappable.dart'; +import 'package:collection/collection.dart'; import '../rendering/slides/slide_parts.dart'; import '../styling/components/slide.dart'; import 'slide_template.dart'; import 'widget_factory.dart'; -part 'deck_options.mapper.dart'; +const _undefined = Object(); + +class DeckOptions { + static const _stylesEquality = MapEquality(); + static const _widgetsEquality = MapEquality(); + static const _templatesEquality = MapEquality(); -@MappableClass() -class DeckOptions with DeckOptionsMappable { final SlideStyler? baseStyle; final Map styles; final Map widgets; @@ -36,4 +38,60 @@ class DeckOptions with DeckOptionsMappable { }) : styles = Map.unmodifiable(styles), widgets = Map.unmodifiable(widgets), templates = Map.unmodifiable(templates); + + DeckOptions copyWith({ + Object? baseStyle = _undefined, + Map? styles, + Map? widgets, + SlideParts? parts, + bool? debug, + Map? templates, + Object? defaultTemplate = _undefined, + }) { + return DeckOptions( + baseStyle: identical(baseStyle, _undefined) + ? this.baseStyle + : baseStyle as SlideStyler?, + styles: styles ?? this.styles, + widgets: widgets ?? this.widgets, + parts: parts ?? this.parts, + debug: debug ?? this.debug, + templates: templates ?? this.templates, + defaultTemplate: identical(defaultTemplate, _undefined) + ? this.defaultTemplate + : defaultTemplate as SlideTemplate?, + ); + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + other is DeckOptions && + runtimeType == other.runtimeType && + baseStyle == other.baseStyle && + _stylesEquality.equals(styles, other.styles) && + _widgetsEquality.equals(widgets, other.widgets) && + parts == other.parts && + debug == other.debug && + _templatesEquality.equals(templates, other.templates) && + defaultTemplate == other.defaultTemplate; + } + + @override + int get hashCode => Object.hash( + baseStyle, + _stylesEquality.hash(styles), + _widgetsEquality.hash(widgets), + parts, + debug, + _templatesEquality.hash(templates), + defaultTemplate, + ); + + @override + String toString() { + return 'DeckOptions(baseStyle: $baseStyle, styles: $styles, ' + 'widgets: $widgets, parts: $parts, debug: $debug, ' + 'templates: $templates, defaultTemplate: $defaultTemplate)'; + } } diff --git a/packages/superdeck/lib/src/deck/deck_options.mapper.dart b/packages/superdeck/lib/src/deck/deck_options.mapper.dart deleted file mode 100644 index 7c073204f..000000000 --- a/packages/superdeck/lib/src/deck/deck_options.mapper.dart +++ /dev/null @@ -1,290 +0,0 @@ -// coverage:ignore-file -// GENERATED CODE - DO NOT MODIFY BY HAND -// dart format off -// ignore_for_file: type=lint -// ignore_for_file: invalid_use_of_protected_member -// ignore_for_file: unused_element, unnecessary_cast, override_on_non_overriding_member -// ignore_for_file: strict_raw_type, inference_failure_on_untyped_parameter - -part of 'deck_options.dart'; - -class DeckOptionsMapper extends ClassMapperBase { - DeckOptionsMapper._(); - - static DeckOptionsMapper? _instance; - static DeckOptionsMapper ensureInitialized() { - if (_instance == null) { - MapperContainer.globals.use(_instance = DeckOptionsMapper._()); - SlideTemplateMapper.ensureInitialized(); - } - return _instance!; - } - - @override - final String id = 'DeckOptions'; - - static SlideStyler? _$baseStyle(DeckOptions v) => v.baseStyle; - static const Field _f$baseStyle = Field( - 'baseStyle', - _$baseStyle, - opt: true, - ); - static Map _$styles(DeckOptions v) => v.styles; - static const Field> _f$styles = Field( - 'styles', - _$styles, - opt: true, - def: const {}, - ); - static Map)> _$widgets( - DeckOptions v, - ) => v.widgets; - static const Field< - DeckOptions, - Map)> - > - _f$widgets = Field( - 'widgets', - _$widgets, - opt: true, - def: const {}, - ); - static SlideParts _$parts(DeckOptions v) => v.parts; - static const Field _f$parts = Field( - 'parts', - _$parts, - opt: true, - def: const SlideParts(), - ); - static bool _$debug(DeckOptions v) => v.debug; - static const Field _f$debug = Field( - 'debug', - _$debug, - opt: true, - def: false, - ); - static Map _$templates(DeckOptions v) => v.templates; - static const Field> _f$templates = - Field( - 'templates', - _$templates, - opt: true, - def: const {}, - ); - static SlideTemplate? _$defaultTemplate(DeckOptions v) => v.defaultTemplate; - static const Field _f$defaultTemplate = Field( - 'defaultTemplate', - _$defaultTemplate, - opt: true, - ); - - @override - final MappableFields fields = const { - #baseStyle: _f$baseStyle, - #styles: _f$styles, - #widgets: _f$widgets, - #parts: _f$parts, - #debug: _f$debug, - #templates: _f$templates, - #defaultTemplate: _f$defaultTemplate, - }; - - static DeckOptions _instantiate(DecodingData data) { - return DeckOptions( - baseStyle: data.dec(_f$baseStyle), - styles: data.dec(_f$styles), - widgets: data.dec(_f$widgets), - parts: data.dec(_f$parts), - debug: data.dec(_f$debug), - templates: data.dec(_f$templates), - defaultTemplate: data.dec(_f$defaultTemplate), - ); - } - - @override - final Function instantiate = _instantiate; - - static DeckOptions fromMap(Map map) { - return ensureInitialized().decodeMap(map); - } - - static DeckOptions fromJson(String json) { - return ensureInitialized().decodeJson(json); - } -} - -mixin DeckOptionsMappable { - String toJson() { - return DeckOptionsMapper.ensureInitialized().encodeJson( - this as DeckOptions, - ); - } - - Map toMap() { - return DeckOptionsMapper.ensureInitialized().encodeMap( - this as DeckOptions, - ); - } - - DeckOptionsCopyWith get copyWith => - _DeckOptionsCopyWithImpl( - this as DeckOptions, - $identity, - $identity, - ); - @override - String toString() { - return DeckOptionsMapper.ensureInitialized().stringifyValue( - this as DeckOptions, - ); - } - - @override - bool operator ==(Object other) { - return DeckOptionsMapper.ensureInitialized().equalsValue( - this as DeckOptions, - other, - ); - } - - @override - int get hashCode { - return DeckOptionsMapper.ensureInitialized().hashValue(this as DeckOptions); - } -} - -extension DeckOptionsValueCopy<$R, $Out> - on ObjectCopyWith<$R, DeckOptions, $Out> { - DeckOptionsCopyWith<$R, DeckOptions, $Out> get $asDeckOptions => - $base.as((v, t, t2) => _DeckOptionsCopyWithImpl<$R, $Out>(v, t, t2)); -} - -abstract class DeckOptionsCopyWith<$R, $In extends DeckOptions, $Out> - implements ClassCopyWith<$R, $In, $Out> { - MapCopyWith< - $R, - String, - SlideStyler, - ObjectCopyWith<$R, SlideStyler, SlideStyler> - > - get styles; - MapCopyWith< - $R, - String, - Widget Function(Map), - ObjectCopyWith< - $R, - Widget Function(Map), - Widget Function(Map) - > - > - get widgets; - MapCopyWith< - $R, - String, - SlideTemplate, - SlideTemplateCopyWith<$R, SlideTemplate, SlideTemplate> - > - get templates; - SlideTemplateCopyWith<$R, SlideTemplate, SlideTemplate>? get defaultTemplate; - $R call({ - SlideStyler? baseStyle, - Map? styles, - Map)>? widgets, - SlideParts? parts, - bool? debug, - Map? templates, - SlideTemplate? defaultTemplate, - }); - DeckOptionsCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>(Then<$Out2, $R2> t); -} - -class _DeckOptionsCopyWithImpl<$R, $Out> - extends ClassCopyWithBase<$R, DeckOptions, $Out> - implements DeckOptionsCopyWith<$R, DeckOptions, $Out> { - _DeckOptionsCopyWithImpl(super.value, super.then, super.then2); - - @override - late final ClassMapperBase $mapper = - DeckOptionsMapper.ensureInitialized(); - @override - MapCopyWith< - $R, - String, - SlideStyler, - ObjectCopyWith<$R, SlideStyler, SlideStyler> - > - get styles => MapCopyWith( - $value.styles, - (v, t) => ObjectCopyWith(v, $identity, t), - (v) => call(styles: v), - ); - @override - MapCopyWith< - $R, - String, - Widget Function(Map), - ObjectCopyWith< - $R, - Widget Function(Map), - Widget Function(Map) - > - > - get widgets => MapCopyWith( - $value.widgets, - (v, t) => ObjectCopyWith(v, $identity, t), - (v) => call(widgets: v), - ); - @override - MapCopyWith< - $R, - String, - SlideTemplate, - SlideTemplateCopyWith<$R, SlideTemplate, SlideTemplate> - > - get templates => MapCopyWith( - $value.templates, - (v, t) => v.copyWith.$chain(t), - (v) => call(templates: v), - ); - @override - SlideTemplateCopyWith<$R, SlideTemplate, SlideTemplate>? - get defaultTemplate => - $value.defaultTemplate?.copyWith.$chain((v) => call(defaultTemplate: v)); - @override - $R call({ - Object? baseStyle = $none, - Map? styles, - Map)>? widgets, - SlideParts? parts, - bool? debug, - Map? templates, - Object? defaultTemplate = $none, - }) => $apply( - FieldCopyWithData({ - if (baseStyle != $none) #baseStyle: baseStyle, - if (styles != null) #styles: styles, - if (widgets != null) #widgets: widgets, - if (parts != null) #parts: parts, - if (debug != null) #debug: debug, - if (templates != null) #templates: templates, - if (defaultTemplate != $none) #defaultTemplate: defaultTemplate, - }), - ); - @override - DeckOptions $make(CopyWithData data) => DeckOptions( - baseStyle: data.get(#baseStyle, or: $value.baseStyle), - styles: data.get(#styles, or: $value.styles), - widgets: data.get(#widgets, or: $value.widgets), - parts: data.get(#parts, or: $value.parts), - debug: data.get(#debug, or: $value.debug), - templates: data.get(#templates, or: $value.templates), - defaultTemplate: data.get(#defaultTemplate, or: $value.defaultTemplate), - ); - - @override - DeckOptionsCopyWith<$R2, DeckOptions, $Out2> $chain<$R2, $Out2>( - Then<$Out2, $R2> t, - ) => _DeckOptionsCopyWithImpl<$R2, $Out2>($value, $cast, t); -} - diff --git a/packages/superdeck/lib/src/deck/loaders/file_deck_loader.dart b/packages/superdeck/lib/src/deck/loaders/file_deck_loader.dart index 375f90298..fac26f306 100644 --- a/packages/superdeck/lib/src/deck/loaders/file_deck_loader.dart +++ b/packages/superdeck/lib/src/deck/loaders/file_deck_loader.dart @@ -83,7 +83,7 @@ class FileDeckLoader extends DeckLoader { ); return; } - final status = DeckBuildStatus.fromMap( + final status = DeckBuildStatus.fromJson( Map.from(decoded), ); diff --git a/packages/superdeck/lib/src/deck/slide_configuration.dart b/packages/superdeck/lib/src/deck/slide_configuration.dart index 78f7b2efa..f861ae578 100644 --- a/packages/superdeck/lib/src/deck/slide_configuration.dart +++ b/packages/superdeck/lib/src/deck/slide_configuration.dart @@ -1,5 +1,4 @@ import 'package:flutter/widgets.dart'; -import 'package:dart_mappable/dart_mappable.dart'; import 'package:superdeck_core/superdeck_core.dart'; import '../rendering/slides/slide_parts.dart'; @@ -7,14 +6,13 @@ import '../styling/components/slide.dart'; import '../ui/widgets/provider.dart'; import 'widget_factory.dart'; -part 'slide_configuration.mapper.dart'; +const _undefined = Object(); String buildThumbnailKey(String slideKey) { return 'thumbnail_$slideKey.png'; } -@MappableClass() -class SlideConfiguration with SlideConfigurationMappable { +class SlideConfiguration { final int slideIndex; final SlideStyler style; final Slide slide; @@ -43,6 +41,32 @@ class SlideConfiguration with SlideConfigurationMappable { this.assetCacheStore, }); + SlideConfiguration copyWith({ + int? slideIndex, + SlideStyler? style, + Slide? slide, + bool? debug, + Object? parts = _undefined, + Map? widgets, + String? thumbnailKey, + bool? isStaticRendering, + Object? assetCacheStore = _undefined, + }) { + return SlideConfiguration( + slideIndex: slideIndex ?? this.slideIndex, + style: style ?? this.style, + slide: slide ?? this.slide, + debug: debug ?? this.debug, + parts: identical(parts, _undefined) ? this.parts : parts as SlideParts?, + widgets: widgets ?? this.widgets, + thumbnailKey: thumbnailKey ?? this.thumbnailKey, + isStaticRendering: isStaticRendering ?? this.isStaticRendering, + assetCacheStore: identical(assetCacheStore, _undefined) + ? this.assetCacheStore + : assetCacheStore as AssetCacheStore?, + ); + } + SlideOptions get options => slide.options ?? SlideOptions(); String get key => slide.key; @@ -84,4 +108,12 @@ class SlideConfiguration with SlideConfigurationMappable { isStaticRendering, assetCacheStore, ); + + @override + String toString() { + return 'SlideConfiguration(slideIndex: $slideIndex, style: $style, ' + 'slide: $slide, debug: $debug, parts: $parts, widgets: $widgets, ' + 'thumbnailKey: $thumbnailKey, isStaticRendering: $isStaticRendering, ' + 'assetCacheStore: $assetCacheStore)'; + } } diff --git a/packages/superdeck/lib/src/deck/slide_configuration.mapper.dart b/packages/superdeck/lib/src/deck/slide_configuration.mapper.dart deleted file mode 100644 index 03009e831..000000000 --- a/packages/superdeck/lib/src/deck/slide_configuration.mapper.dart +++ /dev/null @@ -1,277 +0,0 @@ -// coverage:ignore-file -// GENERATED CODE - DO NOT MODIFY BY HAND -// dart format off -// ignore_for_file: type=lint -// ignore_for_file: invalid_use_of_protected_member -// ignore_for_file: unused_element, unnecessary_cast, override_on_non_overriding_member -// ignore_for_file: strict_raw_type, inference_failure_on_untyped_parameter - -part of 'slide_configuration.dart'; - -class SlideConfigurationMapper extends ClassMapperBase { - SlideConfigurationMapper._(); - - static SlideConfigurationMapper? _instance; - static SlideConfigurationMapper ensureInitialized() { - if (_instance == null) { - MapperContainer.globals.use(_instance = SlideConfigurationMapper._()); - SlideMapper.ensureInitialized(); - } - return _instance!; - } - - @override - final String id = 'SlideConfiguration'; - - static int _$slideIndex(SlideConfiguration v) => v.slideIndex; - static const Field _f$slideIndex = Field( - 'slideIndex', - _$slideIndex, - ); - static SlideStyler _$style(SlideConfiguration v) => v.style; - static const Field _f$style = Field( - 'style', - _$style, - ); - static Slide _$slide(SlideConfiguration v) => v.slide; - static const Field _f$slide = Field( - 'slide', - _$slide, - ); - static bool _$debug(SlideConfiguration v) => v.debug; - static const Field _f$debug = Field( - 'debug', - _$debug, - opt: true, - def: false, - ); - static SlideParts? _$parts(SlideConfiguration v) => v.parts; - static const Field _f$parts = Field( - 'parts', - _$parts, - opt: true, - ); - static String _$thumbnailKey(SlideConfiguration v) => v.thumbnailKey; - static const Field _f$thumbnailKey = Field( - 'thumbnailKey', - _$thumbnailKey, - ); - static Map)> _$widgets( - SlideConfiguration v, - ) => v.widgets; - static const Field< - SlideConfiguration, - Map)> - > - _f$widgets = Field('widgets', _$widgets, opt: true, def: const {}); - static bool _$isStaticRendering(SlideConfiguration v) => v.isStaticRendering; - static const Field _f$isStaticRendering = Field( - 'isStaticRendering', - _$isStaticRendering, - opt: true, - def: false, - ); - static AssetCacheStore? _$assetCacheStore(SlideConfiguration v) => - v.assetCacheStore; - static const Field _f$assetCacheStore = - Field('assetCacheStore', _$assetCacheStore, opt: true); - - @override - final MappableFields fields = const { - #slideIndex: _f$slideIndex, - #style: _f$style, - #slide: _f$slide, - #debug: _f$debug, - #parts: _f$parts, - #thumbnailKey: _f$thumbnailKey, - #widgets: _f$widgets, - #isStaticRendering: _f$isStaticRendering, - #assetCacheStore: _f$assetCacheStore, - }; - - static SlideConfiguration _instantiate(DecodingData data) { - return SlideConfiguration( - slideIndex: data.dec(_f$slideIndex), - style: data.dec(_f$style), - slide: data.dec(_f$slide), - debug: data.dec(_f$debug), - parts: data.dec(_f$parts), - thumbnailKey: data.dec(_f$thumbnailKey), - widgets: data.dec(_f$widgets), - isStaticRendering: data.dec(_f$isStaticRendering), - assetCacheStore: data.dec(_f$assetCacheStore), - ); - } - - @override - final Function instantiate = _instantiate; - - static SlideConfiguration fromMap(Map map) { - return ensureInitialized().decodeMap(map); - } - - static SlideConfiguration fromJson(String json) { - return ensureInitialized().decodeJson(json); - } -} - -mixin SlideConfigurationMappable { - String toJson() { - return SlideConfigurationMapper.ensureInitialized() - .encodeJson(this as SlideConfiguration); - } - - Map toMap() { - return SlideConfigurationMapper.ensureInitialized() - .encodeMap(this as SlideConfiguration); - } - - SlideConfigurationCopyWith< - SlideConfiguration, - SlideConfiguration, - SlideConfiguration - > - get copyWith => - _SlideConfigurationCopyWithImpl( - this as SlideConfiguration, - $identity, - $identity, - ); - @override - String toString() { - return SlideConfigurationMapper.ensureInitialized().stringifyValue( - this as SlideConfiguration, - ); - } - - @override - bool operator ==(Object other) { - return SlideConfigurationMapper.ensureInitialized().equalsValue( - this as SlideConfiguration, - other, - ); - } - - @override - int get hashCode { - return SlideConfigurationMapper.ensureInitialized().hashValue( - this as SlideConfiguration, - ); - } -} - -extension SlideConfigurationValueCopy<$R, $Out> - on ObjectCopyWith<$R, SlideConfiguration, $Out> { - SlideConfigurationCopyWith<$R, SlideConfiguration, $Out> - get $asSlideConfiguration => $base.as( - (v, t, t2) => _SlideConfigurationCopyWithImpl<$R, $Out>(v, t, t2), - ); -} - -abstract class SlideConfigurationCopyWith< - $R, - $In extends SlideConfiguration, - $Out -> - implements ClassCopyWith<$R, $In, $Out> { - SlideCopyWith<$R, Slide, Slide> get slide; - MapCopyWith< - $R, - String, - Widget Function(Map), - ObjectCopyWith< - $R, - Widget Function(Map), - Widget Function(Map) - > - > - get widgets; - $R call({ - int? slideIndex, - SlideStyler? style, - Slide? slide, - bool? debug, - SlideParts? parts, - String? thumbnailKey, - Map)>? widgets, - bool? isStaticRendering, - AssetCacheStore? assetCacheStore, - }); - SlideConfigurationCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>( - Then<$Out2, $R2> t, - ); -} - -class _SlideConfigurationCopyWithImpl<$R, $Out> - extends ClassCopyWithBase<$R, SlideConfiguration, $Out> - implements SlideConfigurationCopyWith<$R, SlideConfiguration, $Out> { - _SlideConfigurationCopyWithImpl(super.value, super.then, super.then2); - - @override - late final ClassMapperBase $mapper = - SlideConfigurationMapper.ensureInitialized(); - @override - SlideCopyWith<$R, Slide, Slide> get slide => - $value.slide.copyWith.$chain((v) => call(slide: v)); - @override - MapCopyWith< - $R, - String, - Widget Function(Map), - ObjectCopyWith< - $R, - Widget Function(Map), - Widget Function(Map) - > - > - get widgets => MapCopyWith( - $value.widgets, - (v, t) => ObjectCopyWith(v, $identity, t), - (v) => call(widgets: v), - ); - @override - $R call({ - int? slideIndex, - SlideStyler? style, - Slide? slide, - bool? debug, - Object? parts = $none, - String? thumbnailKey, - Map)>? widgets, - bool? isStaticRendering, - Object? assetCacheStore = $none, - }) => $apply( - FieldCopyWithData({ - if (slideIndex != null) #slideIndex: slideIndex, - if (style != null) #style: style, - if (slide != null) #slide: slide, - if (debug != null) #debug: debug, - if (parts != $none) #parts: parts, - if (thumbnailKey != null) #thumbnailKey: thumbnailKey, - if (widgets != null) #widgets: widgets, - if (isStaticRendering != null) #isStaticRendering: isStaticRendering, - if (assetCacheStore != $none) #assetCacheStore: assetCacheStore, - }), - ); - @override - SlideConfiguration $make(CopyWithData data) => SlideConfiguration( - slideIndex: data.get(#slideIndex, or: $value.slideIndex), - style: data.get(#style, or: $value.style), - slide: data.get(#slide, or: $value.slide), - debug: data.get(#debug, or: $value.debug), - parts: data.get(#parts, or: $value.parts), - thumbnailKey: data.get(#thumbnailKey, or: $value.thumbnailKey), - widgets: data.get(#widgets, or: $value.widgets), - isStaticRendering: data.get( - #isStaticRendering, - or: $value.isStaticRendering, - ), - assetCacheStore: data.get(#assetCacheStore, or: $value.assetCacheStore), - ); - - @override - SlideConfigurationCopyWith<$R2, SlideConfiguration, $Out2> $chain<$R2, $Out2>( - Then<$Out2, $R2> t, - ) => _SlideConfigurationCopyWithImpl<$R2, $Out2>($value, $cast, t); -} - diff --git a/packages/superdeck/lib/src/deck/slide_template.dart b/packages/superdeck/lib/src/deck/slide_template.dart index b2884f98b..3e13e1963 100644 --- a/packages/superdeck/lib/src/deck/slide_template.dart +++ b/packages/superdeck/lib/src/deck/slide_template.dart @@ -1,17 +1,14 @@ -import 'package:dart_mappable/dart_mappable.dart'; - import '../rendering/slides/slide_parts.dart'; import '../styling/components/slide.dart'; -part 'slide_template.mapper.dart'; +const _undefined = Object(); /// A reusable slide template that bundles chrome (header, footer, background) /// with an isolated style system. /// /// Templates act like Keynote master slides — providing consistent visual /// framing across slides without manually applying styles/parts to each slide. -@MappableClass() -final class SlideTemplate with SlideTemplateMappable { +final class SlideTemplate { /// Chrome parts (header, footer, background) for this template. final SlideParts parts; @@ -27,6 +24,20 @@ final class SlideTemplate with SlideTemplateMappable { this.styles = const {}, }); + SlideTemplate copyWith({ + SlideParts? parts, + Object? baseStyle = _undefined, + Map? styles, + }) { + return SlideTemplate( + parts: parts ?? this.parts, + baseStyle: identical(baseStyle, _undefined) + ? this.baseStyle + : baseStyle as SlideStyler?, + styles: styles ?? this.styles, + ); + } + @override bool operator ==(Object other) => identical(this, other) || @@ -38,4 +49,10 @@ final class SlideTemplate with SlideTemplateMappable { @override int get hashCode => Object.hash(parts, baseStyle, styles); + + @override + String toString() { + return 'SlideTemplate(parts: $parts, baseStyle: $baseStyle, ' + 'styles: $styles)'; + } } diff --git a/packages/superdeck/lib/src/deck/slide_template.mapper.dart b/packages/superdeck/lib/src/deck/slide_template.mapper.dart deleted file mode 100644 index 9d9e2d167..000000000 --- a/packages/superdeck/lib/src/deck/slide_template.mapper.dart +++ /dev/null @@ -1,182 +0,0 @@ -// coverage:ignore-file -// GENERATED CODE - DO NOT MODIFY BY HAND -// dart format off -// ignore_for_file: type=lint -// ignore_for_file: invalid_use_of_protected_member -// ignore_for_file: unused_element, unnecessary_cast, override_on_non_overriding_member -// ignore_for_file: strict_raw_type, inference_failure_on_untyped_parameter - -part of 'slide_template.dart'; - -class SlideTemplateMapper extends ClassMapperBase { - SlideTemplateMapper._(); - - static SlideTemplateMapper? _instance; - static SlideTemplateMapper ensureInitialized() { - if (_instance == null) { - MapperContainer.globals.use(_instance = SlideTemplateMapper._()); - } - return _instance!; - } - - @override - final String id = 'SlideTemplate'; - - static SlideParts _$parts(SlideTemplate v) => v.parts; - static const Field _f$parts = Field( - 'parts', - _$parts, - opt: true, - def: const SlideParts(), - ); - static SlideStyler? _$baseStyle(SlideTemplate v) => v.baseStyle; - static const Field _f$baseStyle = Field( - 'baseStyle', - _$baseStyle, - opt: true, - ); - static Map _$styles(SlideTemplate v) => v.styles; - static const Field> _f$styles = Field( - 'styles', - _$styles, - opt: true, - def: const {}, - ); - - @override - final MappableFields fields = const { - #parts: _f$parts, - #baseStyle: _f$baseStyle, - #styles: _f$styles, - }; - - static SlideTemplate _instantiate(DecodingData data) { - return SlideTemplate( - parts: data.dec(_f$parts), - baseStyle: data.dec(_f$baseStyle), - styles: data.dec(_f$styles), - ); - } - - @override - final Function instantiate = _instantiate; - - static SlideTemplate fromMap(Map map) { - return ensureInitialized().decodeMap(map); - } - - static SlideTemplate fromJson(String json) { - return ensureInitialized().decodeJson(json); - } -} - -mixin SlideTemplateMappable { - String toJson() { - return SlideTemplateMapper.ensureInitialized().encodeJson( - this as SlideTemplate, - ); - } - - Map toMap() { - return SlideTemplateMapper.ensureInitialized().encodeMap( - this as SlideTemplate, - ); - } - - SlideTemplateCopyWith - get copyWith => _SlideTemplateCopyWithImpl( - this as SlideTemplate, - $identity, - $identity, - ); - @override - String toString() { - return SlideTemplateMapper.ensureInitialized().stringifyValue( - this as SlideTemplate, - ); - } - - @override - bool operator ==(Object other) { - return SlideTemplateMapper.ensureInitialized().equalsValue( - this as SlideTemplate, - other, - ); - } - - @override - int get hashCode { - return SlideTemplateMapper.ensureInitialized().hashValue( - this as SlideTemplate, - ); - } -} - -extension SlideTemplateValueCopy<$R, $Out> - on ObjectCopyWith<$R, SlideTemplate, $Out> { - SlideTemplateCopyWith<$R, SlideTemplate, $Out> get $asSlideTemplate => - $base.as((v, t, t2) => _SlideTemplateCopyWithImpl<$R, $Out>(v, t, t2)); -} - -abstract class SlideTemplateCopyWith<$R, $In extends SlideTemplate, $Out> - implements ClassCopyWith<$R, $In, $Out> { - MapCopyWith< - $R, - String, - SlideStyler, - ObjectCopyWith<$R, SlideStyler, SlideStyler> - > - get styles; - $R call({ - SlideParts? parts, - SlideStyler? baseStyle, - Map? styles, - }); - SlideTemplateCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>(Then<$Out2, $R2> t); -} - -class _SlideTemplateCopyWithImpl<$R, $Out> - extends ClassCopyWithBase<$R, SlideTemplate, $Out> - implements SlideTemplateCopyWith<$R, SlideTemplate, $Out> { - _SlideTemplateCopyWithImpl(super.value, super.then, super.then2); - - @override - late final ClassMapperBase $mapper = - SlideTemplateMapper.ensureInitialized(); - @override - MapCopyWith< - $R, - String, - SlideStyler, - ObjectCopyWith<$R, SlideStyler, SlideStyler> - > - get styles => MapCopyWith( - $value.styles, - (v, t) => ObjectCopyWith(v, $identity, t), - (v) => call(styles: v), - ); - @override - $R call({ - SlideParts? parts, - Object? baseStyle = $none, - Map? styles, - }) => $apply( - FieldCopyWithData({ - if (parts != null) #parts: parts, - if (baseStyle != $none) #baseStyle: baseStyle, - if (styles != null) #styles: styles, - }), - ); - @override - SlideTemplate $make(CopyWithData data) => SlideTemplate( - parts: data.get(#parts, or: $value.parts), - baseStyle: data.get(#baseStyle, or: $value.baseStyle), - styles: data.get(#styles, or: $value.styles), - ); - - @override - SlideTemplateCopyWith<$R2, SlideTemplate, $Out2> $chain<$R2, $Out2>( - Then<$Out2, $R2> t, - ) => _SlideTemplateCopyWithImpl<$R2, $Out2>($value, $cast, t); -} - 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 af942848f..544beecdf 100644 --- a/packages/superdeck/lib/src/markdown/builders/image_element_builder.dart +++ b/packages/superdeck/lib/src/markdown/builders/image_element_builder.dart @@ -69,8 +69,9 @@ class ImageElementBuilder extends MarkdownElementBuilder // A bare key (e.g. an AI-generated `slide-x-illustration.png`) is resolved // through the slide's asset cache when one is bound. Hero transitions are // not applied for cache-resolved images (they have no hero tag in practice). - final assetCacheStore = - InheritedData.maybeOf(context)?.assetCacheStore; + final assetCacheStore = InheritedData.maybeOf( + context, + )?.assetCacheStore; if (assetCacheStore != null && isBareAssetKey(uri)) { return ConstrainedBox( constraints: BoxConstraints.tight(totalSize), diff --git a/packages/superdeck/pubspec.yaml b/packages/superdeck/pubspec.yaml index af15bf6e6..eff23af0e 100644 --- a/packages/superdeck/pubspec.yaml +++ b/packages/superdeck/pubspec.yaml @@ -16,7 +16,6 @@ dependencies: cached_network_image: ^3.4.1 window_manager: ^0.5.2 collection: ^1.18.0 - dart_mappable: ^4.7.0 path: ^1.9.0 syntax_highlight: ^0.5.0 scrollable_positioned_list: ^0.3.8 @@ -45,10 +44,9 @@ dev_dependencies: flutter_lints: ^6.0.0 dart_code_metrics_presets: ^2.19.0 build_runner: ^2.5.4 - dart_mappable_builder: ^4.7.0 - # 2.2.0-beta.0 requires Analyzer 9, which is incompatible with ack_generator. - mix_generator: 2.1.3 + mix_generator: 2.2.0-beta.0 path_provider_platform_interface: ^2.1.2 + webview_flutter_platform_interface: ^2.15.1 superdeck_builder: ^1.0.0 flutter: diff --git a/packages/superdeck/test/src/deck/deck_options_test.dart b/packages/superdeck/test/src/deck/deck_options_test.dart index bd2e93112..9b716ec88 100644 --- a/packages/superdeck/test/src/deck/deck_options_test.dart +++ b/packages/superdeck/test/src/deck/deck_options_test.dart @@ -91,7 +91,7 @@ void main() { }); test( - 'copyWith(defaultTemplate: null) clears a previously set defaultTemplate', + 'copyWith(defaultTemplate: null) clears the current defaultTemplate', () { const template = SlideTemplate(); final options = DeckOptions(defaultTemplate: template); diff --git a/packages/superdeck/test/src/deck/slide_configuration_test.dart b/packages/superdeck/test/src/deck/slide_configuration_test.dart index 9c8c87b93..5d5a664a8 100644 --- a/packages/superdeck/test/src/deck/slide_configuration_test.dart +++ b/packages/superdeck/test/src/deck/slide_configuration_test.dart @@ -7,6 +7,30 @@ Widget _sameWidget(Map args) => const SizedBox.shrink(); void main() { group('SlideConfiguration', () { + test('copyWith updates values and clears nullable fields on null', () { + final parts = SlideParts(); + final assetCacheStore = _FakeAssetCacheStore(); + final original = SlideConfiguration( + slideIndex: 0, + style: SlideStyler(), + slide: Slide(key: 'slide-1'), + parts: parts, + thumbnailKey: 'thumbnail_slide-1.png', + assetCacheStore: assetCacheStore, + ); + + final copy = original.copyWith( + slideIndex: 1, + parts: null, + assetCacheStore: null, + ); + + expect(copy.slideIndex, 1); + expect(copy.parts, isNull); + expect(copy.assetCacheStore, isNull); + expect(copy.slide, same(original.slide)); + }); + test('configs sharing the same widgets map instance are equal', () { final widgets = {'same': _sameWidget}; final slide = Slide(key: 'slide-1'); @@ -26,6 +50,7 @@ void main() { ); expect(a, equals(b)); + expect(a.hashCode, b.hashCode); }); test('configs with same-content widget maps are not equal', () { @@ -49,3 +74,14 @@ void main() { }); }); } + +final class _FakeAssetCacheStore implements AssetCacheStore { + @override + Future delete(String assetKey) async {} + + @override + Future resolve(String assetKey) async => null; + + @override + Future write(String assetKey, List bytes) async => null; +} diff --git a/packages/superdeck/test/src/deck/slide_template_test.dart b/packages/superdeck/test/src/deck/slide_template_test.dart index 322393bef..7d39776fa 100644 --- a/packages/superdeck/test/src/deck/slide_template_test.dart +++ b/packages/superdeck/test/src/deck/slide_template_test.dart @@ -80,6 +80,15 @@ void main() { expect(copy.baseStyle, same(baseStyle)); }); + test('null clears the existing nullable baseStyle', () { + final baseStyle = SlideStyler(); + final original = SlideTemplate(baseStyle: baseStyle); + + final copy = original.copyWith(baseStyle: null); + + expect(copy.baseStyle, isNull); + }); + test('preserves styles when not specified', () { final styles = {'a': SlideStyler()}; final original = SlideTemplate(styles: styles); diff --git a/packages/superdeck/test/src/rendering/block_widget_test.dart b/packages/superdeck/test/src/rendering/block_widget_test.dart index b378469c5..c4ec05d31 100644 --- a/packages/superdeck/test/src/rendering/block_widget_test.dart +++ b/packages/superdeck/test/src/rendering/block_widget_test.dart @@ -829,7 +829,7 @@ void main() { key: 'zero-padding', sections: [ SectionBlock([ - WidgetBlock.fromMap({ + WidgetBlock.fromJson({ 'type': 'widget', 'name': 'custom', 'padding': {'top': 0, 'right': 0, 'bottom': 0, 'left': 0}, @@ -881,7 +881,7 @@ void main() { key: 'asymmetric-padding', sections: [ SectionBlock([ - WidgetBlock.fromMap({ + WidgetBlock.fromJson({ 'type': 'widget', 'name': 'custom', 'padding': {'top': 30, 'right': 20, 'bottom': 40, 'left': 10}, @@ -913,7 +913,7 @@ void main() { key: 'preserved-variant-geometry', sections: [ SectionBlock([ - WidgetBlock.fromMap({ + WidgetBlock.fromJson({ 'type': 'widget', 'name': 'chart', 'padding': {'top': 4, 'right': 4, 'bottom': 4, 'left': 4}, @@ -977,7 +977,7 @@ void main() { key: '$name-padding', sections: [ SectionBlock([ - WidgetBlock.fromMap({ + WidgetBlock.fromJson({ 'type': 'widget', 'name': name, 'padding': { @@ -1010,7 +1010,7 @@ void main() { key: 'asymmetric-margin', sections: [ SectionBlock([ - WidgetBlock.fromMap({ + WidgetBlock.fromJson({ 'type': 'widget', 'name': 'custom', 'margin': {'top': 10, 'right': 20, 'bottom': 30, 'left': 40}, diff --git a/packages/superdeck/test/src/ui/superdeck_app_test.dart b/packages/superdeck/test/src/ui/superdeck_app_test.dart index f73c8046d..1f830fd6e 100644 --- a/packages/superdeck/test/src/ui/superdeck_app_test.dart +++ b/packages/superdeck/test/src/ui/superdeck_app_test.dart @@ -84,7 +84,7 @@ ByteData _utf8ByteData(String value) { void _mockBundledDeckAsset(DeckWorkspace workspace, List slides) { final payload = jsonEncode( - slides.map((slide) => slide.toMap()).toList(growable: false), + slides.map((slide) => slide.toJson()).toList(growable: false), ); rootBundle.evict(workspace.bundledDeckJsonPath); 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 cbcc4f1b0..d1bd783c5 100644 --- a/packages/superdeck/test/src/ui/widgets/hero_element_test.dart +++ b/packages/superdeck/test/src/ui/widgets/hero_element_test.dart @@ -35,7 +35,10 @@ void main() { // 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)]), + TextSpan( + style: headingStyle, + children: [TextSpan(text: text)], + ), key: shuttleKey, ); }, @@ -48,9 +51,7 @@ void main() { await tester.pumpWidget( MaterialApp( home: Scaffold(body: Center(child: heroWidget())), - routes: { - '/next': (_) => Scaffold(body: Center(child: heroWidget())), - }, + routes: {'/next': (_) => Scaffold(body: Center(child: heroWidget()))}, ), ); diff --git a/pubspec.lock b/pubspec.lock index 1e5a1bee2..eedb7886c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,50 +5,50 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: c209688d9f5a5f26b2fb47a188131a6fb9e876ae9e47af3737c0b4f58a93470d + sha256: "3b19a47f6ea7c2632760777c78174f47f6aec1e05f0cd611380d4593b8af1dbc" url: "https://pub.dev" source: hosted - version: "91.0.0" + version: "96.0.0" ack: dependency: transitive description: name: ack - sha256: da555e043d1f549db783bc534fdff86dc19a205ec97da58eb2745c9121d30797 + sha256: aa3be1f6f337e3edcd550358201dec755046ff891694c4c2010dc6e0334bf702 url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.2.0" ack_annotations: dependency: transitive description: name: ack_annotations - sha256: "082da2332bff04a004f63c0b1997b091adedb35c7bd1bf06814ea6dd94986f67" + sha256: bc5acf1a899999c8d3a9d9d79fa90774554320a47415b30db147f082a526fb84 url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.2.0" ack_generator: dependency: transitive description: name: ack_generator - sha256: "5a2885d4325e7df75d7c57c947bfdf80f833d6c476d3e453a275829a358b2247" + sha256: "82a917e4707f55cf3075ba3b03b09d703cf09354a3d5c8efb337f2b072cd4050" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.2.0" ack_json_schema_builder: dependency: transitive description: name: ack_json_schema_builder - sha256: "541f863050600a7fb96503c2ce46bf64f157c2c761e7d3d13cf06c0d2c27d684" + sha256: "3f1f86b9195e5bf17de6d7326d6c2fa214244d963da7c952f37bec7589c88ab2" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.2.0" analyzer: - dependency: "direct overridden" + dependency: transitive description: name: analyzer - sha256: f51c8499b35f9b26820cfe914828a6a98a94efd5cc78b37bb7d03debae3a1d08 + sha256: "0c516bc4ad36a1a75759e54d5047cb9d15cded4459df01aa35a0b5ec7db2c2a0" url: "https://pub.dev" source: hosted - version: "8.4.1" + version: "10.2.0" ansi_styles: dependency: transitive description: @@ -57,14 +57,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.3.2+1" - ansicolor: - dependency: transitive - description: - name: ansicolor - sha256: "50e982d500bc863e1d703448afdbf9e5a72eb48840a4f766fa361ffd6877055f" - url: "https://pub.dev" - source: hosted - version: "2.0.3" anthropic_sdk_dart: dependency: transitive description: @@ -417,22 +409,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.32.0" - dart_mappable: - dependency: transitive - description: - name: dart_mappable - sha256: "960746478faaa68ed6b9d3c6fd03c87c7b8614e6c33e75fe1b0c6d7a60adcf29" - url: "https://pub.dev" - source: hosted - version: "4.8.0" - dart_mappable_builder: - dependency: transitive - description: - name: dart_mappable_builder - sha256: "6d174a1853c47cf7c1d2a1c80bd56b54f8fd89aa78e0644b6a65860f9af7acf8" - url: "https://pub.dev" - source: hosted - version: "4.7.0" dart_quill_delta: dependency: transitive description: @@ -445,10 +421,10 @@ packages: dependency: transitive description: name: dart_style - sha256: a9c30492da18ff84efe2422ba2d319a89942d93e58eb0b73d32abe822ef54b7b + sha256: "29f7ecc274a86d32920b1d9cfc7502fa87220da41ec60b55f329559d5732e2b2" url: "https://pub.dev" source: hosted - version: "3.1.3" + version: "3.1.7" dartantic_ai: dependency: transitive description: @@ -968,6 +944,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.1.5" + json_serializable: + dependency: transitive + description: + name: json_serializable + sha256: e45aefa0324f08c683caafbb94b72837aa6193c61822799c916e45f4a263113d + url: "https://pub.dev" + source: hosted + version: "6.14.1" leak_tracker: dependency: transitive description: @@ -1124,10 +1108,10 @@ packages: dependency: transitive description: name: mix_generator - sha256: "331a7e96188390833bf7f628f9efbe86ae25743db215b101111865264075aadf" + sha256: "76d888e0845e7bb3bc6bee3d903bbb0611ec7f27b8b6c554deef62775b59654b" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.2.0-beta.0" mockito: dependency: transitive description: @@ -1593,10 +1577,18 @@ packages: dependency: transitive description: name: source_gen - sha256: ec37cc0e6694374cbef59ed79685572c870a54ede6fa30a3e420feb3adffea02 + sha256: a603f1fb984a7391ae5978d1b92bfaaa08b350dca5c825256f925818f7943bf5 url: "https://pub.dev" source: hosted - version: "4.2.3" + version: "4.2.4" + source_helper: + dependency: transitive + description: + name: source_helper + sha256: "5e6f216fdf6376c9f3852381ae037499797a3385377d388b011dac98d303c67c" + url: "https://pub.dev" + source: hosted + version: "1.3.13" source_map_stack_trace: dependency: transitive description: @@ -1821,14 +1813,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.2" - type_plus: - dependency: transitive - description: - name: type_plus - sha256: d5d1019471f0d38b91603adb9b5fd4ce7ab903c879d2fbf1a3f80a630a03fcc9 - url: "https://pub.dev" - source: hosted - version: "2.1.1" typed_data: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 4aec9707e..1e5a034e1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,11 +15,7 @@ dev_dependencies: # requires cli_util 0.4.x. 7.8.1 is the latest compatible workspace release. melos: 7.8.1 -# ack_generator 1.x requires Analyzer 8's element2 API, while mix_generator -# 2.2 requires Analyzer 9 where that API was removed. Keep the proven 2.1 -# generator on Analyzer 8 while the runtime Mix packages use 2.2 beta. dependency_overrides: - analyzer: 8.4.1 # Hero UI's Remix 1.0 migration commit still points at development Git # branches. Resolve its runtime dependencies to the published betas used by # this workspace until that upstream migration is released. @@ -50,17 +46,17 @@ melos: description: Run Dart static analysis checks. analyze:dcm: - run: fvm dart run melos exec -c 10 -- dcm analyze . --fatal-style --fatal-warnings + run: fvm dart run melos exec -c 10 -- "dcm analyze . --fatal-style --fatal-warnings -e '{**/*.g.dart,**/*.freezed.dart,**/*.ack.dart}'" description: Run DCM static analysis checks. packageFilters: dependsOn: dart_code_metrics_presets analyze:dcm:unused-files: # Public barrels/plugin entrypoints are not cleanup targets (--exclude-public-api). - # Generated files use DCM defaults (*.g.dart / *.freezed.dart / *.mapper.dart). + # Generated-file excludes cover *.g.dart, *.freezed.dart, and *.ack.dart. # Extra globs: tests + platform conditional files DCM misclassifies as unused. # Quotes must survive melos exec so brace globs are not shell-expanded. - run: fvm dart run melos exec -c 10 -- "dcm check-unused-files . --fatal-unused --exclude-public-api -e '{**/*.g.dart,**/*.freezed.dart,**/*.mapper.dart,test/**,**/*_web.dart,**/*_stub.dart,**/*_io.dart,**/measure_size.dart}'" + run: fvm dart run melos exec -c 10 -- "dcm check-unused-files . --fatal-unused --exclude-public-api -e '{**/*.g.dart,**/*.freezed.dart,**/*.ack.dart,test/**,**/*_web.dart,**/*_stub.dart,**/*_io.dart,**/measure_size.dart}'" description: Check for unused files using DCM (public API, generated, platform stubs excluded). packageFilters: dependsOn: dart_code_metrics_presets @@ -68,7 +64,7 @@ melos: analyze:dcm:unused-code: # Public API surface stays even if unused in-repo (--exclude-public-api). # Same exclude set as unused-files (generated + platform stubs + tests). - run: fvm dart run melos exec -c 10 -- "dcm check-unused-code . --fatal-unused --exclude-public-api -e '{**/*.g.dart,**/*.freezed.dart,**/*.mapper.dart,test/**,**/*_web.dart,**/*_stub.dart,**/*_io.dart}'" + run: fvm dart run melos exec -c 10 -- "dcm check-unused-code . --fatal-unused --exclude-public-api -e '{**/*.g.dart,**/*.freezed.dart,**/*.ack.dart,test/**,**/*_web.dart,**/*_stub.dart,**/*_io.dart}'" description: Check for unused code using DCM (public API, generated, platform stubs excluded). packageFilters: dependsOn: dart_code_metrics_presets diff --git a/shared_analysis_options.yaml b/shared_analysis_options.yaml index 94fab7bce..477c3b604 100644 --- a/shared_analysis_options.yaml +++ b/shared_analysis_options.yaml @@ -5,7 +5,6 @@ analyzer: plugins: - custom_lint exclude: - - '**.mapper.dart' - '**/generated_plugin_registrant.dart' - '**/*.g.dart' linter: