diff --git a/.agents/skills/superdeck-presentations/references/runtime-customization.md b/.agents/skills/superdeck-presentations/references/runtime-customization.md index fef3aaf4a..516d184de 100644 --- a/.agents/skills/superdeck-presentations/references/runtime-customization.md +++ b/.agents/skills/superdeck-presentations/references/runtime-customization.md @@ -14,7 +14,7 @@ Future main() async { WidgetsFlutterBinding.ensureInitialized(); await SuperDeckApp.initialize(); - runApp(const SuperDeckApp(options: DeckOptions())); + runApp(SuperDeckApp(options: DeckOptions())); } ``` @@ -109,7 +109,7 @@ final index = slide.slideIndex; final args = slide.options.args; ``` -Use Flutter's `LayoutBuilder` for sizing. SuperDeck does not export a public `MeasureSize` API. +Use Flutter's `LayoutBuilder` for sizing. SuperDeck does not export a public widget-size measurement API. If a widget factory is missing, SuperDeck renders `Widget not found: `. If the factory throws, SuperDeck renders error details on the slide. diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 0a696fc5d..f0a3043d0 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -6,7 +6,9 @@ - `packages/core`: Markdown parsing, schemas, validation (Dart-only) - `packages/superdeck`: Flutter runtime/widgets - `packages/cli`: `superdeck` CLI commands - - `packages/builder`: code generation/build integration + - `packages/builder`: deck parsing, serialization, and the build/watch pipeline + - `packages/playground`: the Wizard app for AI deck generation, presenting, and saving + - `packages/plugins/pdf`: PDF export - `demo/`: reference app and slide content used by CI smoke/integration flows ## First-time setup (required) @@ -30,7 +32,7 @@ fvm dart run melos bootstrap ## Agent workflow expectations 1. Keep changes surgical and package-scoped. 2. Run `fvm dart run melos run build_runner:build` before tests when touching code that can affect generated output. -3. Commit generated files when they change (`*.g.dart`, `*.mapper.dart`). +3. Commit generated files when they change (`*.g.dart`, `*.ack.dart`). 4. Prefer relative imports in Dart files. 5. Follow existing style: two-space indentation, `snake_case.dart` filenames. @@ -45,12 +47,6 @@ fvm dart run melos bootstrap fvm dart run melos bootstrap ``` -- **CI failure while building demo assets (`fvm dart run superdeck_cli:main build`)** - - Observed error: - - `Invalid YAML frontmatter in slide ... Error on line 1, column 1: Unexpected character.` - - Triggering content starts with `@section {`. - - Workaround: fix invalid slide frontmatter/config in demo slide content before rerunning CI. - ## CI notes - Main validation workflow: `.github/workflows/test.yml` - CI sequence is effectively: bootstrap -> build_runner -> contracts check -> tests/integration/web smoke. diff --git a/.gitignore b/.gitignore index eed7f2c16..35ea7a6ee 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # Flutter/Dart/Pub related # Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. pubspec.lock +!/pubspec.lock pubspec_overrides.yaml # Miscellaneous @@ -50,7 +51,6 @@ build/ # Node file for website node_modules -.local-chrome .orbit discovery diff --git a/AGENTS.md b/AGENTS.md index aa004f4f9..02f3edf68 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -185,6 +185,7 @@ lib/src/ - Use `snake_case` for repo-owned non-standard filenames - Prefer `.yaml` for repo-owned configuration and fixture files - Keep ecosystem-standard names as-is, including `README.md`, `CHANGELOG.md`, generated platform files, and existing GitHub workflow `.yml` files +- Vendor forked third-party code under a `vendor/` directory with an upstream attribution header (source, commit, license), a `LICENSE` file beside it, and local patches clearly marked, following `packages/superdeck/lib/src/markdown/mermaid/vendor/` ## Testing Guidelines diff --git a/CHANGELOG.md b/CHANGELOG.md index 9840db8ed..b5b9b3a87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,5 +5,5 @@ SuperDeck is a monorepo; each package keeps its own changelog: - [`superdeck`](packages/superdeck/CHANGELOG.md) — the Flutter presentation framework - [`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_builder`](packages/builder/CHANGELOG.md) — deck parsing and the build pipeline - [`superdeck_pdf`](packages/plugins/pdf/CHANGELOG.md) — PDF export support diff --git a/README.md b/README.md index fef5311c4..f25c06982 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ SuperDeck is a Flutter presentation framework. You write slides in Markdown, and WidgetsFlutterBinding.ensureInitialized(); await SuperDeckApp.initialize(); - runApp(const SuperDeckApp(options: DeckOptions())); + runApp(SuperDeckApp(options: DeckOptions())); } ``` diff --git a/demo/.gitignore b/demo/.gitignore index c4f5177b0..4a014d1b4 100644 --- a/demo/.gitignore +++ b/demo/.gitignore @@ -49,5 +49,4 @@ app.*.map.json .fvm/ .env -lib/env/env.g.dart .superdeck/assets/* diff --git a/demo/analysis_options.yaml b/demo/analysis_options.yaml index 0ed1948dc..5fc59b2c4 100644 --- a/demo/analysis_options.yaml +++ b/demo/analysis_options.yaml @@ -1,3 +1,4 @@ -include: package:flutter_lints/flutter.yaml -extends: ../shared_analysis_options.yaml +include: + - package:flutter_lints/flutter.yaml + - ../shared_analysis_options.yaml diff --git a/demo/lib/src/widgets/demo_widgets.dart b/demo/lib/src/widgets/demo_widgets.dart index a6b8cd535..b104e9804 100644 --- a/demo/lib/src/widgets/demo_widgets.dart +++ b/demo/lib/src/widgets/demo_widgets.dart @@ -26,7 +26,7 @@ import 'ack_metric_card.dart'; /// } /// ``` /// -/// The QR code widget is now a built-in widget available as `@qrcode`. +/// QR codes use the built-in `@qrcode` widget. Map get demoWidgets => { 'mix-simple-box': (_) => _DemoWrapper( child: Transform.scale(scale: 3.0, child: mix_simple_box.Example()), diff --git a/demo/showcase.md b/demo/showcase.md deleted file mode 100644 index d1375170d..000000000 --- a/demo/showcase.md +++ /dev/null @@ -1,318 +0,0 @@ ---- - -@section { - flex: 2 -} -@block { - align: center -} -# SuperDeck {.heading} -# Presentations Reimagined {.subheading} - ---- - -## What is SuperDeck? {.heading} - -@block - -SuperDeck is a presentation framework that transforms how developers create slides. - -- Write slides in **Markdown** - familiar syntax, version-controlled -- Render with **Flutter** - cross-platform, beautiful UI -- Extend with **custom widgets** - interactive, live demos - ---- - -@block { - flex: 2 - align: centerLeft -} -### Traditional Tools {.heading} - -@block { - flex: 3 -} - -#### PowerPoint/Keynote - -- Binary files, poor version control -- Limited code formatting -- No live components -- Platform-dependent - -#### SuperDeck - -- Plain text Markdown -- Git-friendly diffs -- Live Flutter widgets -- Runs everywhere Flutter runs - ---- - -@section { - flex: 2 -} -@block { - align: center -} -## "Write once, present anywhere. Version control everything." {.heading} - -##### The SuperDeck Philosophy {.subheading} - ---- - -@block { - flex: 2 - align: centerLeft -} -### Hot Reload {.heading} - -Edit your slides and see changes instantly - no rebuild needed. - -@block { - flex: 3 -} - -- **Live Preview** - Changes appear in milliseconds -- **State Preserved** - Stay on current slide while editing -- **Error Recovery** - Graceful handling of syntax errors -- **Watch Mode** - CLI monitors file changes automatically - ---- - -### Markdown Syntax {.heading} - -@block { - flex: 2 -} - -```markdown ---- - -## Slide Title {.heading} - -@block - -Your content here with **bold** -and *italic* text. - -- Bullet points -- Code blocks -- Custom widgets - ---- -```{.code} - ---- - -@section - -@block { - flex: 1 -} -## Layout System {.heading} - -How SuperDeck organizes content on slides. - -@section { - flex: 3 - align: topLeft -} - -@block { - flex: 1 -} -#### Sections - -Horizontal rows that divide the slide vertically. - -```markdown -@section { - flex: 2 -} -``` - -@block { - flex: 1 -} -#### Columns - -Vertical divisions within sections. - -```markdown -@block { - flex: 3 - align: center -} -``` - -@block { - flex: 1 -} -#### Blocks - -Content containers with markdown or widgets. - -```markdown -@widget { - name: "chart" -} -``` - ---- - -## Layout Types {.heading} - -@block - -| Layout | Structure | Best For | -|--------|-----------|----------| -| Title | Single centered section | Opening, section dividers | -| Standard | Title row + body row | Default content slides | -| Two-Column | Side-by-side blocks | Comparisons, pros/cons | -| Three-Column | Three equal blocks | Categories, options | -| Title-Left | Title block + content | Feature highlights | -| Quote | Centered large text | Key takeaways, transitions | -| Table | Markdown table | Structured data comparisons | - ---- - -### The Block System {.heading} - -@block - -SuperDeck uses three core block types: - -- **Content Blocks** (`@block`) - Render markdown text, lists, tables -- **Widget Blocks** (`@widget`) - Embed custom Flutter components -- **Built-in Widgets** (`@image`, `@dartpad`, `@qrcode`) - Pre-configured widgets - -Content and widget blocks support: -- `flex` - Relative sizing (default: 1) -- `align` - Content positioning (e.g., `center`, `topLeft`) -- `scrollable` - Enable overflow scrolling - -Sections support `flex` and `align`; set `scrollable` on the child block or widget that overflows. - ---- - -### Build Pipeline {.heading} - -@block - -The CLI processes your markdown through multiple stages: -1. **Parse** - Extract frontmatter and block directives -2. **Transform** - Convert to widget tree -3. **Render** - Flutter builds the UI - ---- - -@block { - flex: 2 - align: centerLeft -} -### Getting Started {.heading} - -@block { - flex: 3 -} - -#### Installation - -```bash -# Add to your Flutter project -flutter pub add superdeck - -# Create your slides -touch slides.md - -# Build and watch -dart run superdeck_cli:main watch -``` - -#### Run - -```bash -flutter run -d macos # or chrome, windows, linux -``` - ---- - -## Styling System {.heading} - -@section { - flex: 3 -} - -@block { - flex: 1 -} - -#### Global Themes - -Define in `DeckOptions.styles`: - -```dart -SuperDeckApp( - options: DeckOptions( - styles: { - 'default': borderedStyle(), - }, - ), -) -``` - -@block { - flex: 1 -} - -#### Per-Slide Styles - -Apply via frontmatter: - -```markdown ---- -style: quote ---- - -> Your quote here -``` - ---- - -## Advanced Features {.heading} - -@block - -- **Code Highlighting** - Syntax-aware formatting -- **Custom Widgets** - Embed any Flutter widget -- **Thumbnails** - Auto-generated previews -- **Responsive** - Adapts to any screen size - ---- - -@section { - flex: 2 -} -@block { - align: center -} -## "The best presentation tool is the one that gets out of your way." {.heading} - -##### Focus on content, not formatting {.subheading} - ---- - -@section { - flex: 2 - align: bottomCenter -} -# Start Building {.heading} -## github.com/btwld/superdeck {.subheading} - -@section - -@block { - align: center -} -MIT License | Flutter & Dart diff --git a/docs.json b/docs.json index fc0998ec5..2294ed9ce 100644 --- a/docs.json +++ b/docs.json @@ -114,6 +114,10 @@ { "title": "Front matter", "href": "/reference/front-matter" + }, + { + "title": "Navigation reference", + "href": "/reference/navigation" } ] }, diff --git a/docs/getting-started.mdx b/docs/getting-started.mdx index dbfe4d2b1..13118a75c 100644 --- a/docs/getting-started.mdx +++ b/docs/getting-started.mdx @@ -53,7 +53,7 @@ Future main() async { WidgetsFlutterBinding.ensureInitialized(); await SuperDeckApp.initialize(); - runApp(const SuperDeckApp(options: DeckOptions())); + runApp(SuperDeckApp(options: DeckOptions())); } ``` diff --git a/docs/guides/markdown-authoring.mdx b/docs/guides/markdown-authoring.mdx index 1652c6bda..a013aa3b1 100644 --- a/docs/guides/markdown-authoring.mdx +++ b/docs/guides/markdown-authoring.mdx @@ -324,15 +324,15 @@ See the [SuperDeck Overview](./superdeck-overview) for more styling options. --- -## Upgrading to the next major +## Upgrading to 1.0 - Replace zero or negative `flex` values with positive integers. - Review `@section { align: ... }`. Section alignment now becomes the inherited default for children that do not set block alignment. - Rebuild `.superdeck/superdeck.json` after adding `spacing`, `padding`, `margin`, or image `scale`. -- To downgrade to 1.x, remove the new authoring properties and rebuild the - contract with the 1.x toolchain. +- To downgrade to 0.x, remove the new authoring properties and rebuild the + contract with the 0.x toolchain. --- diff --git a/docs/guides/widget-size-guide.mdx b/docs/guides/widget-size-guide.mdx index 2a7a3770a..011b8ab49 100644 --- a/docs/guides/widget-size-guide.mdx +++ b/docs/guides/widget-size-guide.mdx @@ -5,7 +5,7 @@ description: SuperDeck does not expose a public widget-size measurement API. # Widget size measurement -SuperDeck does not export a public `MeasureSize` widget from `package:superdeck/superdeck.dart`. +SuperDeck does not export a public widget-size measurement API from `package:superdeck/superdeck.dart`. If you need rendered size information in your app code, use Flutter’s built-in layout and measurement tools instead of relying on SuperDeck internals. diff --git a/docs/reference/block-types.mdx b/docs/reference/block-types.mdx index f7a3f2a89..9a7226bc4 100644 --- a/docs/reference/block-types.mdx +++ b/docs/reference/block-types.mdx @@ -381,15 +381,15 @@ overflow axis. The probe preserves Markdown wrapping. It does not intrinsically measure or rebuild arbitrary custom widgets, and static capture does not display or log diagnostics. -## Next-major compatibility +## 1.0 compatibility - Replace every zero or negative `flex` with a positive integer. - Review section-level `align`: it now becomes the inherited default for child blocks without explicit alignment. - Old generated contracts remain readable because new fields are optional. - Downgrading to 1.x requires removing `spacing`/`padding`/`margin`/image + Downgrading to 0.x requires removing `spacing`/`padding`/`margin`/image `scale` from authoring and rebuilding `.superdeck/superdeck.json` with the - 1.x toolchain. + 0.x toolchain. ## Best practices diff --git a/docs/reference/contracts.mdx b/docs/reference/contracts.mdx index f7690c5ed..770b43066 100644 --- a/docs/reference/contracts.mdx +++ b/docs/reference/contracts.mdx @@ -164,15 +164,15 @@ invalid. Omitted authored edges normalize to zero; an explicitly authored ## Contract evolution and downgrade -`spacing`, `padding`, and `margin` are optional/defaulted, so the next-major -runtime reads existing 1.x contracts. A 1.x runtime is not required to read +`spacing`, `padding`, and `margin` are optional/defaulted, so the 1.0 +runtime reads existing 0.x contracts. A 0.x runtime is not required to read contracts authored with these new fields. Before upgrading, replace non-positive flex values and review every section alignment because section alignment now affects children without explicit block alignment. After using any new authoring property, regenerate the contract. -To downgrade, remove the new properties, restore 1.x-compatible alignment/flex -input, and rebuild `.superdeck/superdeck.json` with the 1.x toolchain. +To downgrade, remove the new properties, restore 0.x-compatible alignment/flex +input, and rebuild `.superdeck/superdeck.json` with the 0.x toolchain. ## Machine-readable schema diff --git a/docs/reference/deck-options.mdx b/docs/reference/deck-options.mdx index e2ef9d13a..638d36fc2 100644 --- a/docs/reference/deck-options.mdx +++ b/docs/reference/deck-options.mdx @@ -10,7 +10,7 @@ description: Complete API reference for configuring SuperDeck presentations ## Constructor ```dart -const DeckOptions({ +DeckOptions({ SlideStyler? baseStyle, Map styles = const {}, Map widgets = const {}, diff --git a/docs/tutorials/block-layouts.mdx b/docs/tutorials/block-layouts.mdx index 935474877..58cf035d7 100644 --- a/docs/tutorials/block-layouts.mdx +++ b/docs/tutorials/block-layouts.mdx @@ -190,16 +190,16 @@ observes the existing Markdown render; it does not change wrapping constraints. It intentionally does not rebuild or intrinsically measure arbitrary custom widgets, and static capture remains diagnostic-free. -## Next-major migration +## Migrating to 1.0 -Two inputs change semantics in the next major release: +Two inputs change semantics in 1.0: - `flex` must be a positive integer. Replace zero or negative values. - Section alignment now applies to children without explicit block alignment. Add an explicit child alignment where the old no-op behavior must remain. After adopting `spacing`, `padding`, or `margin`, rebuild -`.superdeck/superdeck.json`. Downgrading to 1.x requires removing the new -authoring properties and rebuilding the contract with the 1.x toolchain. +`.superdeck/superdeck.json`. Downgrading to 0.x requires removing the new +authoring properties and rebuilding the contract with the 0.x toolchain. Next: [Block types reference](/reference/block-types) diff --git a/packages/builder/CHANGELOG.md b/packages/builder/CHANGELOG.md index f1108bd36..c08aa604a 100644 --- a/packages/builder/CHANGELOG.md +++ b/packages/builder/CHANGELOG.md @@ -11,6 +11,8 @@ (comma-separated entries, one key per line); scalar-only options stay inline. Serializer output is idempotent. - **Breaking:** reject non-positive flex values during Markdown compilation. +- **Breaking:** remove `SlideSerializer.serializeSlide`; it had no production + use. Use `SlideSerializer.serialize` for a list of slides. - Add build-plugin `beginBuild` and `finishBuild` lifecycle hooks. - Make `DeckBuilder.dispose()` wait for queued builds before disposing plugins and reject new builds after disposal. diff --git a/packages/builder/analysis_options.yaml b/packages/builder/analysis_options.yaml index 49645176b..7f6b9f428 100644 --- a/packages/builder/analysis_options.yaml +++ b/packages/builder/analysis_options.yaml @@ -1,5 +1,6 @@ -include: package:lints/recommended.yaml -extends: ../../shared_analysis_options.yaml +include: + - package:lints/recommended.yaml + - ../../shared_analysis_options.yaml linter: rules: diff --git a/packages/builder/lib/src/parsers/slide_serializer.dart b/packages/builder/lib/src/parsers/slide_serializer.dart index cdf12ad25..0bd8241a8 100644 --- a/packages/builder/lib/src/parsers/slide_serializer.dart +++ b/packages/builder/lib/src/parsers/slide_serializer.dart @@ -33,13 +33,6 @@ class SlideSerializer { return '${buffer.toString().trim()}\n'; } - /// Serializes a single [slide] (without a trailing newline). - String serializeSlide(Slide slide) { - final buffer = StringBuffer(); - _writeSlide(buffer, slide); - return buffer.toString().trim(); - } - void _writeSlide(StringBuffer buffer, Slide slide) { // The opening `---` doubles as the slide separator. When the slide has // frontmatter, the matching close `---` follows the YAML body. diff --git a/packages/builder/pubspec.yaml b/packages/builder/pubspec.yaml index ee643cbd9..bf26cc358 100644 --- a/packages/builder/pubspec.yaml +++ b/packages/builder/pubspec.yaml @@ -1,7 +1,7 @@ name: superdeck_builder description: The build logic for SuperDeck presentations version: 1.0.0 -homepage: https://github.com/btwld/superdeck +homepage: https://github.com/conceptadev/superdeck documentation: https://docs.page/btwld/superdeck environment: @@ -12,7 +12,6 @@ resolution: workspace dependencies: superdeck_core: ^1.0.0 logging: ^1.3.0 - meta: ^1.16.0 path: ^1.9.0 dev_dependencies: diff --git a/packages/cli/pubspec.yaml b/packages/cli/pubspec.yaml index 273b3411a..8d4fdd5bc 100644 --- a/packages/cli/pubspec.yaml +++ b/packages/cli/pubspec.yaml @@ -1,7 +1,7 @@ name: superdeck_cli description: Command line interface for SuperDeck version: 1.0.0 -homepage: https://github.com/btwld/superdeck +homepage: https://github.com/conceptadev/superdeck documentation: https://docs.page/btwld/superdeck executables: diff --git a/packages/cli/test/src/utils/extensions_test.dart b/packages/cli/test/src/utils/extensions_test.dart deleted file mode 100644 index 239331b6e..000000000 --- a/packages/cli/test/src/utils/extensions_test.dart +++ /dev/null @@ -1,62 +0,0 @@ -import 'dart:io'; - -import 'package:superdeck_core/superdeck_core.dart'; -import 'package:test/test.dart'; - -import '../../helpers/test_helpers.dart'; - -void main() { - group('FileExt', () { - late File file; - late Directory tempDir; - - setUp(() async { - tempDir = await createTempDirAsync(); - file = File('${tempDir.path}/test.txt'); - }); - - test('ensureWrite creates file if it does not exist', () async { - await file.ensureWrite('test content'); - expect(await file.exists(), isTrue); - expect(await file.readAsString(), 'test content'); - }); - - test('ensureWrite overwrites existing file', () async { - await file.writeAsString('old content'); - await file.ensureWrite('new content'); - expect(await file.readAsString(), 'new content'); - }); - - test('ensureExists creates file if it does not exist', () async { - await file.ensureExists(); - expect(await file.exists(), isTrue); - }); - - test('ensureExists does not modify existing file', () async { - await file.writeAsString('test content'); - await file.ensureExists(); - expect(await file.readAsString(), 'test content'); - }); - }); - - group('DirectoryExt', () { - late Directory dir; - late Directory tempDir; - - setUp(() async { - tempDir = await createTempDirAsync(); - dir = Directory('${tempDir.path}/test_dir'); - }); - - test('ensureExists creates directory if it does not exist', () async { - await dir.ensureExists(); - expect(await dir.exists(), isTrue); - }); - - test('ensureExists does not modify existing directory', () async { - await dir.create(); - await dir.ensureExists(); - expect(await dir.exists(), isTrue); - }); - }); -} diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md index c9263b572..9d7ce499e 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -21,6 +21,12 @@ - Add `aiSlideSchema`, a flattened structured-output projection of the slide contract for AI generation (JSON-Schema unions are not consumable by structured-output adapters). +- **Breaking:** remove the top-level `aiSlideSchema` field; call + `buildAiSlideSchema()` instead. +- **Breaking:** remove `DeckWorkspace.projectDirectory`; it had no production + use. +- **Breaking:** remove `BlockInsets.authoringSchema`; use + `BlockInsets.parseAuthoring` to validate and normalize authoring input. - **Breaking:** require positive integer flex values in schemas and public Dart constructors; zero and negative flex values are no longer accepted. - **Breaking:** remove `Block.resolvedAlign` and diff --git a/packages/core/analysis_options.yaml b/packages/core/analysis_options.yaml index 258dae004..8d1275e09 100644 --- a/packages/core/analysis_options.yaml +++ b/packages/core/analysis_options.yaml @@ -1,2 +1,3 @@ -include: package:lints/recommended.yaml -extends: ../../shared_analysis_options.yaml +include: + - package:lints/recommended.yaml + - ../../shared_analysis_options.yaml diff --git a/packages/core/lib/src/deck/block_insets.dart b/packages/core/lib/src/deck/block_insets.dart index 41c72fd0c..276d34a99 100644 --- a/packages/core/lib/src/deck/block_insets.dart +++ b/packages/core/lib/src/deck/block_insets.dart @@ -20,24 +20,6 @@ AckSchema _normalizedInsetValueSchema() => encode: (value) => value, ); -final _symmetricInsetsSchema = Ack.object( - { - 'horizontal': _authoringInsetValueSchema.optional(), - 'vertical': _authoringInsetValueSchema.optional(), - }, - additionalProperties: false, -).withConstraint(const _NonEmptyInsetsObjectConstraint()); - -final _partialEdgeInsetsSchema = Ack.object( - { - 'top': _authoringInsetValueSchema.optional(), - 'right': _authoringInsetValueSchema.optional(), - 'bottom': _authoringInsetValueSchema.optional(), - 'left': _authoringInsetValueSchema.optional(), - }, - additionalProperties: false, -).withConstraint(const _NonEmptyInsetsObjectConstraint()); - /// Four normalized physical inset edges for a slide block. /// /// Used for both block `padding` and block `margin`. Authoring shorthand @@ -91,13 +73,6 @@ final class BlockInsets with _$BlockInsetsAck { left: horizontal, ); - /// Authoring schema: scalar, symmetric map, or physical-edge map. - static final authoringSchema = Ack.anyOf([ - _authoringInsetValueSchema, - _symmetricInsetsSchema, - _partialEdgeInsetsSchema, - ]); - static final fromJson = BlockInsetsSchema.fromJson; /// Parses one supported authoring form and normalizes it to physical edges. @@ -163,24 +138,3 @@ final class BlockInsets with _$BlockInsetsAck { ); } } - -final class _NonEmptyInsetsObjectConstraint - extends Constraint> - with Validator>, JsonSchemaSpec> { - const _NonEmptyInsetsObjectConstraint() - : super( - constraintKey: 'insets_non_empty_object', - description: 'Inset objects must contain at least one property.', - ); - - @override - String buildMessage(Map value) { - return 'Inset objects must contain at least one property.'; - } - - @override - bool isValid(Map value) => value.isNotEmpty; - - @override - Map toJsonSchema() => const {'minProperties': 1}; -} diff --git a/packages/core/lib/src/deck/deck_workspace.dart b/packages/core/lib/src/deck/deck_workspace.dart index b481f7951..fc58677a3 100644 --- a/packages/core/lib/src/deck/deck_workspace.dart +++ b/packages/core/lib/src/deck/deck_workspace.dart @@ -49,8 +49,6 @@ final class DeckWorkspace with _$DeckWorkspaceAck { } } - Directory get projectDirectory => Directory(p.normalize(projectDir)); - Directory get superdeckDir => Directory(p.normalize(p.join(projectDir, outputDir))); diff --git a/packages/core/lib/src/deck/slide_contract.dart b/packages/core/lib/src/deck/slide_contract.dart index 4539762ab..6afb43e59 100644 --- a/packages/core/lib/src/deck/slide_contract.dart +++ b/packages/core/lib/src/deck/slide_contract.dart @@ -11,7 +11,7 @@ final slidesContractSchema = Ack.list(SlideSchema.schema); List parseSlidesContract(Object? value) => slidesContractSchema.parse(value)!; -/// Flattened slide projection for structured-output AI generation. +/// Builds a flattened slide projection for structured-output AI generation. /// /// The discriminated content/widget union is retained for providers that /// support same-type `anyOf` branches. Every field vocabulary (alignment, @@ -19,10 +19,6 @@ List parseSlidesContract(Object? value) => /// canonical schemas, so a contract change here reaches AI generation /// automatically. Data generated against this projection must still decode /// through [Slide.parse] / [parseSlidesContract]. -final aiSlideSchema = buildAiSlideSchema(); - -/// Builds the canonical AI slide projection with optional widget argument -/// fields supplied by an application generation catalog. /// /// Set [nestWidgetArguments] for model-facing draft payloads so widget-only /// fields cannot collide with content-block fields such as `content`. Those diff --git a/packages/core/lib/src/markdown/markdown_json.dart b/packages/core/lib/src/markdown/markdown_json.dart index cdcc050e4..6e6e2edaa 100644 --- a/packages/core/lib/src/markdown/markdown_json.dart +++ b/packages/core/lib/src/markdown/markdown_json.dart @@ -1,11 +1,9 @@ -import 'dart:convert'; - import 'package:markdown/markdown.dart' as md; -/// Helper for converting Markdown into Dart map or JSON representations. +/// Helper for converting Markdown into a Dart map representation. /// /// Configure an instance with the desired [extensionSet], [blockSyntaxes], -/// and [inlineSyntaxes], then call [toMap] or [toJson]. +/// and [inlineSyntaxes], then call [toMap]. /// /// ```dart /// const converter = MarkdownAstConverter( @@ -13,7 +11,6 @@ import 'package:markdown/markdown.dart' as md; /// ); /// /// final ast = converter.toMap('# Hello', includeMetadata: true); -/// final json = converter.toJson('# Hello', prettyPrint: true); /// ``` class MarkdownAstConverter { const MarkdownAstConverter({ @@ -62,29 +59,6 @@ class MarkdownAstConverter { return result; } - - /// Returns a JSON representation of the Markdown AST. - // ignore: unused-code - String toJson( - String markdown, { - bool prettyPrint = false, - bool includeMetadata = false, - md.ExtensionSet? extensionSet, - List? blockSyntaxes, - List? inlineSyntaxes, - }) { - final map = toMap( - markdown, - includeMetadata: includeMetadata, - extensionSet: extensionSet, - blockSyntaxes: blockSyntaxes, - inlineSyntaxes: inlineSyntaxes, - ); - - return prettyPrint - ? const JsonEncoder.withIndent(' ').convert(map) - : json.encode(map); - } } /// Converts a markdown AST [md.Node] into a `Map`. diff --git a/packages/core/pubspec.yaml b/packages/core/pubspec.yaml index 63dbf5e41..c2ba2aee3 100644 --- a/packages/core/pubspec.yaml +++ b/packages/core/pubspec.yaml @@ -1,7 +1,7 @@ name: superdeck_core description: A core library for SuperDeck. version: 1.0.0 -homepage: https://github.com/btwld/superdeck +homepage: https://github.com/conceptadev/superdeck documentation: https://docs.page/btwld/superdeck diff --git a/packages/core/test/helpers/testing_utils.dart b/packages/core/test/helpers/testing_utils.dart index a94b00620..b1db3406d 100644 --- a/packages/core/test/helpers/testing_utils.dart +++ b/packages/core/test/helpers/testing_utils.dart @@ -35,12 +35,3 @@ Future createTempDirAsync() async { }); return tempDir; } - -/// Verifies that a UUID conforms to the v4 format. -Matcher isValidUuidV4() { - return matches( - RegExp( - r'^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$', - ), - ); -} diff --git a/packages/core/test/src/deck/block_model_test.dart b/packages/core/test/src/deck/block_model_test.dart index 5445edba9..f1cbc32b6 100644 --- a/packages/core/test/src/deck/block_model_test.dart +++ b/packages/core/test/src/deck/block_model_test.dart @@ -373,16 +373,6 @@ void main() { } } - test('authoring schema accepts the three authoring forms', () { - for (final input in accepted.keys) { - expect( - BlockInsets.authoringSchema.safeParse(input).isOk, - isTrue, - reason: 'insets: $input', - ); - } - }); - test('contract schema rejects authoring shorthand', () { for (final field in const ['padding', 'margin']) { for (final input in [ @@ -480,22 +470,6 @@ void main() { }); } - test('authoring schema rejects invalid forms', () { - for (final input in [ - ...structurallyInvalid, - -1, - {'left': -1}, - double.nan, - double.infinity, - ]) { - expect( - BlockInsets.authoringSchema.safeParse(input).isOk, - isFalse, - reason: 'insets: $input', - ); - } - }); - test('public constructors create normalized insets', () { expect(BlockInsets.all(8).toJson(), { 'top': 8.0, diff --git a/packages/core/test/src/markdown/markdown_json_test.dart b/packages/core/test/src/markdown/markdown_json_test.dart index 2b563da05..95511f15f 100644 --- a/packages/core/test/src/markdown/markdown_json_test.dart +++ b/packages/core/test/src/markdown/markdown_json_test.dart @@ -1,5 +1,3 @@ -import 'dart:convert'; - import 'package:markdown/markdown.dart' as md; import 'package:superdeck_core/src/markdown/markdown_json.dart'; import 'package:test/test.dart'; @@ -22,36 +20,20 @@ void main() { expect(viaClass, equals(viaOverride)); }); - - test('toJson respects prettyPrint and metadata flags', () { - const markdown = '[link]: https://example.com\n\nSee [link].'; - final converter = MarkdownAstConverter(); - - final compact = converter.toJson(markdown); - final pretty = converter.toJson(markdown, prettyPrint: true); - final withMetadata = converter.toJson(markdown, includeMetadata: true); - - expect(pretty.contains('\n'), isTrue); - expect(compact.length, lessThan(pretty.length)); - expect(withMetadata.contains('linkReferences'), isTrue); - }); }); - group('MarkdownAstConverter.toJson', () { + group('MarkdownAstConverter feature conversion', () { group('basic conversion', () { - test('converts simple heading to JSON', () { - final json = _converter.toJson('# Hello'); - expect(json, isNotEmpty); + test('converts simple heading', () { + final parsed = _converter.toMap('# Hello'); - final parsed = jsonDecode(json) as Map; expect(parsed['type'], equals('document')); expect(parsed['children'], isList); expect(parsed['children'], isNotEmpty); }); test('converts heading and paragraph', () { - final json = _converter.toJson('# Hello\n\nWorld'); - final parsed = jsonDecode(json) as Map; + final parsed = _converter.toMap('# Hello\n\nWorld'); expect(parsed['type'], equals('document')); final children = parsed['children'] as List; @@ -59,16 +41,14 @@ void main() { }); test('handles empty document', () { - final json = _converter.toJson(''); - final parsed = jsonDecode(json) as Map; + final parsed = _converter.toMap(''); expect(parsed['type'], equals('document')); expect(parsed['children'], isEmpty); }); test('handles whitespace-only document', () { - final json = _converter.toJson(' \n\n '); - final parsed = jsonDecode(json) as Map; + final parsed = _converter.toMap(' \n\n '); expect(parsed['type'], equals('document')); // Whitespace-only content should result in empty children @@ -76,34 +56,9 @@ void main() { }); }); - group('pretty print formatting', () { - test('formats JSON with indentation when prettyPrint is true', () { - final json = _converter.toJson('# Test', prettyPrint: true); - - expect(json, contains('\n')); - expect(json, contains(' ')); // 2-space indent - }); - - test('formats JSON compactly when prettyPrint is false', () { - final json = _converter.toJson('# Test', prettyPrint: false); - - // Compact JSON should not have newlines (except possibly in strings) - final parsed = jsonDecode(json); - expect(parsed, isNotNull); - }); - - test('uses compact format by default', () { - final jsonDefault = _converter.toJson('# Test'); - final jsonExplicit = _converter.toJson('# Test', prettyPrint: false); - - expect(jsonDefault, equals(jsonExplicit)); - }); - }); - group('ExtensionSet support', () { test('handles default markdown without extensionSet', () { - final json = _converter.toJson('# Test\n\n**bold**'); - final parsed = jsonDecode(json) as Map; + final parsed = _converter.toMap('# Test\n\n**bold**'); final children = parsed['children'] as List; expect(children.length, equals(2)); // h1 and p @@ -118,41 +73,37 @@ void main() { }); test('supports ExtensionSet.none', () { - final json = _converter.toJson( + final parsed = _converter.toMap( '# Test\n\n```dart\ncode\n```', extensionSet: md.ExtensionSet.none, ); - final parsed = jsonDecode(json) as Map; expect(parsed['children'], isNotEmpty); }); test('supports ExtensionSet.commonMark', () { - final json = _converter.toJson( + final parsed = _converter.toMap( '# Test\n\n```dart\ncode\n```', extensionSet: md.ExtensionSet.commonMark, ); - final parsed = jsonDecode(json) as Map; expect(parsed['children'], isNotEmpty); }); test('supports ExtensionSet.gitHubWeb', () { - final json = _converter.toJson( + final parsed = _converter.toMap( '# Test\n\n```dart\ncode\n```', extensionSet: md.ExtensionSet.gitHubWeb, ); - final parsed = jsonDecode(json) as Map; expect(parsed['children'], isNotEmpty); }); test('supports ExtensionSet.gitHubFlavored', () { - final json = _converter.toJson( + final parsed = _converter.toMap( '# Test\n\n```dart\ncode\n```', extensionSet: md.ExtensionSet.gitHubFlavored, ); - final parsed = jsonDecode(json) as Map; expect(parsed['children'], isNotEmpty); }); @@ -165,11 +116,10 @@ void main() { |----------|----------| | Cell 1 | Cell 2 | '''; - final json = _converter.toJson( + final parsed = _converter.toMap( markdown, extensionSet: md.ExtensionSet.gitHubWeb, ); - final parsed = jsonDecode(json) as Map; final children = parsed['children'] as List; // Should contain a table element @@ -290,11 +240,10 @@ void main() { - [ ] Unchecked task - [x] Checked task '''; - final json = _converter.toJson( + final parsed = _converter.toMap( markdown, extensionSet: md.ExtensionSet.gitHubWeb, ); - final parsed = jsonDecode(json) as Map; expect(parsed['children'], isNotEmpty); }); @@ -330,11 +279,10 @@ void main() { }); test('converts strikethrough text', () { - final json = _converter.toJson( + final parsed = _converter.toMap( '~~strikethrough~~', extensionSet: md.ExtensionSet.gitHubWeb, ); - final parsed = jsonDecode(json) as Map; expect(parsed['children'], isNotEmpty); }); @@ -388,11 +336,10 @@ void main() { > [!NOTE] > This is a note '''; - final json = _converter.toJson( + final parsed = _converter.toMap( markdown, extensionSet: md.ExtensionSet.gitHubWeb, ); - final parsed = jsonDecode(json) as Map; expect(parsed['children'], isNotEmpty); }); @@ -500,11 +447,10 @@ void main() { } ``` '''; - final json = _converter.toJson( + final parsed = _converter.toMap( markdown, extensionSet: md.ExtensionSet.gitHubWeb, ); - final parsed = jsonDecode(json) as Map; final children = parsed['children'] as List; // Should contain a code block element @@ -1150,8 +1096,7 @@ First[^1] and second[^2] and third[^note]. group('edge cases', () { test('handles CRLF line endings', () { final markdown = '# Heading\r\n\r\nParagraph\r\n'; - final json = _converter.toJson(markdown); - final parsed = jsonDecode(json) as Map; + final parsed = _converter.toMap(markdown); expect(parsed['type'], equals('document')); expect(parsed['children'], isNotEmpty); @@ -1159,8 +1104,7 @@ First[^1] and second[^2] and third[^note]. test('handles mixed line endings', () { final markdown = '# Heading\n\nParagraph\r\n\nAnother\r\n'; - final json = _converter.toJson(markdown); - final parsed = jsonDecode(json) as Map; + final parsed = _converter.toMap(markdown); expect(parsed['type'], equals('document')); expect(parsed['children'], isNotEmpty); @@ -1169,31 +1113,28 @@ First[^1] and second[^2] and third[^note]. test('handles very long text content', () { final longText = 'A' * 10000; final markdown = '# $longText'; - final json = _converter.toJson(markdown); + final parsed = _converter.toMap(markdown); - expect(json, isNotEmpty); + expect(parsed, isNotEmpty); }); test('handles special markdown characters in text', () { final markdown = r'Text with \* escaped \_ characters'; - final json = _converter.toJson(markdown); - final parsed = jsonDecode(json) as Map; + final parsed = _converter.toMap(markdown); expect(parsed['children'], isNotEmpty); }); test('handles inline code with backticks', () { final markdown = 'Text with `inline code` here'; - final json = _converter.toJson(markdown); - final parsed = jsonDecode(json) as Map; + final parsed = _converter.toMap(markdown); expect(parsed['children'], isNotEmpty); }); test('handles links with special characters', () { final markdown = '[Link](https://example.com?foo=bar&baz=qux)'; - final json = _converter.toJson(markdown); - final parsed = jsonDecode(json) as Map; + final parsed = _converter.toMap(markdown); expect(parsed['children'], isNotEmpty); }); @@ -1290,25 +1231,6 @@ void main() { expect(map['type'], equals('document')); expect(map['children'], isNotEmpty); expect(map['linkReferences'], isNotNull); - - final json = _converter.toJson( - markdown, - extensionSet: md.ExtensionSet.gitHubWeb, - prettyPrint: true, - ); - - expect(json, contains('\n')); - final parsed = jsonDecode(json); - expect(parsed, isNotNull); - }); - - test('toMap and toJson produce equivalent results', () { - final markdown = '# Test\n\nParagraph'; - final map = _converter.toMap(markdown); - final jsonString = _converter.toJson(markdown); - final parsedMap = jsonDecode(jsonString); - - expect(map, equals(parsedMap)); }); test('extensionSet affects parsing behavior', () { diff --git a/packages/playground/README.md b/packages/playground/README.md index 2963bbf49..05252f5d4 100644 --- a/packages/playground/README.md +++ b/packages/playground/README.md @@ -4,9 +4,8 @@ The SuperDeck Wizard. One flow: describe a presentation, review the outline it plans, approve it, present the deck, and save it. Package name kept as `playground` for now. -There is no file-backed editor here any more. The app does not open arbitrary -Markdown, watch files, or auto-save; a deck is written only when the reader -asks for it. +Generated decks live in memory. The app does not open arbitrary Markdown, +watch files, or auto-save; a deck is written only when the reader asks for it. ## What a saved deck is @@ -39,7 +38,6 @@ lib/ app/ # router.dart + providers.dart (app-root DI) core/ # shared cross-feature domain + data result.dart # Result - command.dart # Command / Command0 / Command1 domain/ # stores (document, customization), design catalogs data/ # data_sources (asset stores, deck loader), mappers features/ @@ -55,7 +53,6 @@ lib/ | Need | Pattern | Lives in | |------|---------|----------| | Text input, focus, animation, scroll | Ephemeral `StatefulWidget` | the widget | -| Async action (loading/error/success) | `Command0` / `Command1` | `domain/commands/` | | Shared state within a feature | `ChangeNotifier` store | `features//domain/stores/` | | Shared state across features | `ChangeNotifier` store | `core/domain/stores/` | diff --git a/packages/playground/assets/ai_prompts/deck_edit_system.prompt b/packages/playground/assets/ai_prompts/deck_edit_system.prompt deleted file mode 100644 index 7449da5e1..000000000 --- a/packages/playground/assets/ai_prompts/deck_edit_system.prompt +++ /dev/null @@ -1,16 +0,0 @@ -You are editing a live SuperDeck presentation. - -Use the deck-edit tools to inspect and change the deck. Slides are addressed only by zero-based index. Do not ask for, create, preserve, or return slide keys. - -Available deck-edit tools: -- getDeck: returns \{ totalSlides, slides: [\{ index, title? \}] \}. -- createSlide: accepts \{ slide, atIndex? \}. Omit atIndex to append. -- updateSlide: accepts \{ index, slide \}. -- deleteSlide: accepts \{ index \}. -- moveSlide: accepts \{ fromIndex, toIndex \}. -- readSlide: accepts \{ index \} and returns the keyless slide plus thumbnailBase64. -- updateStyle: accepts \{ style \} and applies the deck-wide visual style. - -DeckToolSlide is keyless: \{ options?, comments?, sections \}. Preserve slide options, comments, sections, content blocks, widget blocks, and widget args when updating a slide. If you need visual feedback, call readSlide after a mutation and inspect the returned screenshot. - -Prefer small, incremental edits. After each mutation, use getDeck or readSlide when you need to confirm the result before continuing. diff --git a/packages/playground/dcm_baseline.json b/packages/playground/dcm_baseline.json index e2e133651..10befd580 100644 --- a/packages/playground/dcm_baseline.json +++ b/packages/playground/dcm_baseline.json @@ -19,13 +19,6 @@ "c889ed10350adc5484399a841b281d8d" ] }, - "lib/core/command.dart": { - "member-ordering": [ - "c13cc7d233532d01d38e6c853835c222", - "bbff11a98f162c65b2eba65457aa3054", - "d130e52202b79d8a1e23b22525299046" - ] - }, "lib/core/data/data_sources/memory_asset_cache_store.dart": { "newline-before-return": [ "dbdc5c1de5fd5775af210f68a7bdf4b2", @@ -134,14 +127,7 @@ "00c8c3a7775b4bd83b4c7627c1029a39" ] }, - "lib/core/utils/color_utils.dart": { - "newline-before-return": [ - "ac84b6615d6391fc8cb6a100c0b2aad0", - "3a959d00242e1a6e9483824930ad5858", - "108ceff8715a5f73a480064664b89bb7" - ] - }, - "lib/features/ai/quick_agent/core/debug_logger.dart": { + "lib/features/ai/generation/core/debug_logger.dart": { "member-ordering": [ "c9e8d09211e0b5eaac505b8ea7fb89e6" ], @@ -155,7 +141,7 @@ "c596ee1b5e0ebabc0f8379090abe193c" ] }, - "lib/features/ai/quick_agent/core/engine/prompts/composition_example_library.dart": { + "lib/features/ai/generation/core/engine/prompts/composition_example_library.dart": { "member-ordering": [ "7af8781494750ee27e697cd084ffa06e" ], @@ -168,7 +154,7 @@ "bfee52d03478ce0c30b1ae2015bc758f" ] }, - "lib/features/ai/quick_agent/core/engine/prompts/generation_prompt_provider.dart": { + "lib/features/ai/generation/core/engine/prompts/generation_prompt_provider.dart": { "prefer-shorthands-with-static-fields": [ "6debe9693098b85f1b7030aec538457d" ], @@ -195,7 +181,7 @@ "d30004f9ab953749716b83f7accf68d8" ] }, - "lib/features/ai/quick_agent/core/engine/prompts/prompt_registry.dart": { + "lib/features/ai/generation/core/engine/prompts/prompt_registry.dart": { "member-ordering": [ "09cc316b8b2f1586b294afe3cd657dfb", "e05e31fa115c901db6534be2c491cbd8", @@ -218,7 +204,7 @@ "0668cd491890191c84d4244d4552fa91" ] }, - "lib/features/ai/quick_agent/core/engine/services/deck_generation_request.dart": { + "lib/features/ai/generation/core/engine/services/deck_generation_request.dart": { "member-ordering": [ "dd5fb45f127330e41528848263ee30d2", "8b0dcee6e21c872a5495109d65261c87", @@ -230,7 +216,7 @@ "a4f5c9920c2235bcf03341de5d2e2d58" ] }, - "lib/features/ai/quick_agent/core/engine/services/deck_generator_pipeline.dart": { + "lib/features/ai/generation/core/engine/services/deck_generator_pipeline.dart": { "newline-before-return": [ "bbef19786c08a56d47025fc80a748f53", "005d49e327abc6887bba7647199f4c41", @@ -258,7 +244,7 @@ "698a8a56a2fb9a1a9d5ef1dda2d5989a" ] }, - "lib/features/ai/quick_agent/core/engine/services/deck_generator_pipeline_helpers.dart": { + "lib/features/ai/generation/core/engine/services/deck_generator_pipeline_helpers.dart": { "newline-before-return": [ "37f174f893dbd1037d85918476d1ea24", "159fb2fb3c31f528814f62ecf60487da", @@ -268,7 +254,7 @@ "f23fa8c148ddb010a1ef0db563d595da" ] }, - "lib/features/ai/quick_agent/core/engine/services/deck_generator_service.dart": { + "lib/features/ai/generation/core/engine/services/deck_generator_service.dart": { "prefer-shorthands-with-static-fields": [ "b32d9275721f7438582b42bf815a6d85" ], @@ -280,7 +266,7 @@ "f9b3329ab821e075b201a604ea000621" ] }, - "lib/features/ai/quick_agent/core/engine/services/deck_generator_workflow.dart": { + "lib/features/ai/generation/core/engine/services/deck_generator_workflow.dart": { "prefer-shorthands-with-enums": [ "e0702dd3f4b0189360f889335f47c078", "3d5d2e3c3d76cb3d3bbc677082bff044", @@ -304,7 +290,7 @@ "bfee52d03478ce0c30b1ae2015bc758f" ] }, - "lib/features/ai/quick_agent/core/engine/services/deck_plan_repair.dart": { + "lib/features/ai/generation/core/engine/services/deck_plan_repair.dart": { "newline-before-return": [ "005d49e327abc6887bba7647199f4c41", "bbef19786c08a56d47025fc80a748f53", @@ -327,7 +313,7 @@ "3d5d2e3c3d76cb3d3bbc677082bff044" ] }, - "lib/features/ai/quick_agent/core/engine/services/deck_plan_validator.dart": { + "lib/features/ai/generation/core/engine/services/deck_plan_validator.dart": { "prefer-shorthands-with-enums": [ "c9dc47c9fe3fd49df082e47b2aaf7fd2", "ebcb05238a3f0eb7b46ced43963886f5", @@ -352,7 +338,7 @@ "2e913be2539392ddf88e8ee8b320b20e" ] }, - "lib/features/ai/quick_agent/core/engine/services/design_quality_metrics.dart": { + "lib/features/ai/generation/core/engine/services/design_quality_metrics.dart": { "newline-before-return": [ "882da0c35bc3a6bb638ea2521b141495", "aecc7a118b1650fe049e3deafabb2974", @@ -362,7 +348,7 @@ "6ea2ef7311b482724a9b7b0bc0dd85c6" ] }, - "lib/features/ai/quick_agent/core/engine/services/error_classifier.dart": { + "lib/features/ai/generation/core/engine/services/error_classifier.dart": { "member-ordering": [ "3acce24f1b36cbe21619ebd3112d9289" ], @@ -376,7 +362,7 @@ "beb069993c7bf588df4bc9ad0b9ae18a" ] }, - "lib/features/ai/quick_agent/core/engine/services/generated_slide_validator.dart": { + "lib/features/ai/generation/core/engine/services/generated_slide_validator.dart": { "avoid-inferrable-type-arguments": [ "91fb37915e0b137471ffed358a98df47", "bfee52d03478ce0c30b1ae2015bc758f" @@ -416,7 +402,7 @@ "58dfc4fe116e9efe1e0ae5e54c92fd67" ] }, - "lib/features/ai/quick_agent/core/engine/services/generation_element_catalog.dart": { + "lib/features/ai/generation/core/engine/services/generation_element_catalog.dart": { "member-ordering": [ "fd95321f1ed8a76e53ca4a25c53f0642", "e4e67a4cc9fc7eb46a4cb121a91f0810", @@ -436,12 +422,12 @@ "addd37a8065b52cea0ed98d80d259435" ] }, - "lib/features/ai/quick_agent/core/engine/services/generation_model_client.dart": { + "lib/features/ai/generation/core/engine/services/generation_model_client.dart": { "member-ordering": [ "4981409a7f3ab044ec5b677457aa58d7" ] }, - "lib/features/ai/quick_agent/core/engine/services/generation_progress.dart": { + "lib/features/ai/generation/core/engine/services/generation_progress.dart": { "prefer-shorthands-with-enums": [ "e26fb406d5089a687500bbd71144d876", "bbfc1d5b729389800243cd8eacf3d25a", @@ -453,7 +439,7 @@ "eb3984d433df779e562f5b8284d5909e" ] }, - "lib/features/ai/quick_agent/core/engine/services/generation_quality_report.dart": { + "lib/features/ai/generation/core/engine/services/generation_quality_report.dart": { "member-ordering": [ "3ff13792b3f83562e15a0cf94eff2c08", "93ac312546071a4932c9cb72821b311a", @@ -484,7 +470,7 @@ "2e913be2539392ddf88e8ee8b320b20e" ] }, - "lib/features/ai/quick_agent/core/engine/services/generation_trace.dart": { + "lib/features/ai/generation/core/engine/services/generation_trace.dart": { "prefer-null-aware-elements": [ "c66904102d6d77800b117d207228b251", "fd8b6032d202e81c1896005759922c08", @@ -493,7 +479,7 @@ "920a92511bb8dac13caaa7e770fb8a72" ] }, - "lib/features/ai/quick_agent/core/engine/services/google_schema_adapter.dart": { + "lib/features/ai/generation/core/engine/services/google_schema_adapter.dart": { "member-ordering": [ "93ac312546071a4932c9cb72821b311a", "51cc5f257902301cca542637defeb0a4", @@ -536,7 +522,7 @@ "c6ad57ad6dd31cc333d959e18d3b3358" ] }, - "lib/features/ai/quick_agent/core/engine/services/retry_policy.dart": { + "lib/features/ai/generation/core/engine/services/retry_policy.dart": { "member-ordering": [ "b24769fd1b2d68dd809f5b9f4a9e02f7", "19562df09e962929d946a1b8ee35e347", @@ -553,7 +539,7 @@ "5420701ded68ecea0dfa74dfeadad4b4" ] }, - "lib/features/ai/quick_agent/core/engine/services/source_grounding.dart": { + "lib/features/ai/generation/core/engine/services/source_grounding.dart": { "newline-before-return": [ "1b5e7b2454b1233e4bba6f24f4ea8a70", "566bac91bf51eff670cec6d89acb2e53", diff --git a/packages/playground/lib/app/providers.dart b/packages/playground/lib/app/providers.dart index f21798961..d33f95cbc 100644 --- a/packages/playground/lib/app/providers.dart +++ b/packages/playground/lib/app/providers.dart @@ -22,8 +22,8 @@ const _debugDeckLayout = bool.fromEnvironment('SUPERDECK_DEBUG_LAYOUT'); /// publishes its first Markdown — otherwise the loader's broadcast event would /// be dropped. /// -/// Slides are read straight off `DeckController.slides` (a signal) in the UI — -/// no bridge store. +/// `PresentationPage` hands `DeckController` straight to SuperDeck's own +/// `DeckPresenter`, which reads slides internally — no bridge store. class AppProviders extends StatelessWidget { const AppProviders({required this.child, this.deckLibrary, super.key}); @@ -46,9 +46,7 @@ class AppProviders extends StatelessWidget { dispose: (_, loader) => loader.dispose(), ), Provider(create: (_) => MemoryAssetCacheStore()), - ChangeNotifierProvider( - create: (_) => DeckDocumentStore(markdown: ''), - ), + ChangeNotifierProvider(create: (_) => DeckDocumentStore(markdown: '')), Provider( create: (_) => deckLibrary ?? MacOsDeckLibrary(), dispose: (_, library) => library.dispose(), diff --git a/packages/playground/lib/core/command.dart b/packages/playground/lib/core/command.dart deleted file mode 100644 index 8f87adc73..000000000 --- a/packages/playground/lib/core/command.dart +++ /dev/null @@ -1,78 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/foundation.dart'; - -import 'result.dart'; - -typedef CommandAction0 = Future> Function(); -typedef CommandAction1 = Future> Function(A); - -/// Encapsulates an async action, exposing its running and error states and -/// ensuring it cannot be launched again until it finishes. -/// -/// Use [Command0] for actions without arguments and [Command1] for actions -/// with a single argument. Actions must return a [Result]. -/// -/// A command is *not* a store: it represents an action, not state. Consume its -/// result by listening for changes, then call [clearResult] once handled. -abstract class Command extends ChangeNotifier { - Command(); - - bool _running = false; - - /// True while the action is running. - bool get running => _running; - - Result? _result; - - /// True if the action completed with an error. - bool get error => _result is Failure; - - /// True if the action completed successfully. - bool get completed => _result is Ok; - - /// The last action result, or null if never run / cleared. - Result? get result => _result; - - /// Clears the last action result. - void clearResult() { - _result = null; - notifyListeners(); - } - - Future _execute(CommandAction0 action) async { - // Prevent re-entrancy — e.g. multiple taps on a button. - if (_running) return; - - _running = true; - _result = null; - notifyListeners(); - - try { - _result = await action(); - } finally { - _running = false; - notifyListeners(); - } - } -} - -/// A [Command] without arguments. -abstract class Command0 extends Command { - Command0(); - - Future> action(); - - /// Executes the action. - Future call() => _execute(action); -} - -/// A [Command] with a single argument. -abstract class Command1 extends Command { - Command1(); - - Future> action(A argument); - - /// Executes the action with [argument]. - Future call(A argument) => _execute(() => action(argument)); -} diff --git a/packages/playground/lib/core/data/data_sources/deck_file.dart b/packages/playground/lib/core/data/data_sources/deck_file.dart index f466716f7..96183065d 100644 --- a/packages/playground/lib/core/data/data_sources/deck_file.dart +++ b/packages/playground/lib/core/data/data_sources/deck_file.dart @@ -1,41 +1,23 @@ /// A persistent reference to a deck file. /// -/// [bookmark] is opaque macOS security-scoped bookmark data. Files created in -/// the selected SuperDeck directory inherit its active access scope and do not -/// need individual bookmarks. +/// Files created in the selected SuperDeck directory inherit its active +/// access scope, so this only needs to track the deck's resolved path. final class DeckFileReference { /// Last resolved absolute path of the deck. final String path; - /// Opaque persistent access data, or `null` when directory access covers it. - final String? bookmark; - - const DeckFileReference({required this.path, this.bookmark}); + const DeckFileReference({required this.path}); @override bool operator ==(Object other) { - return other is DeckFileReference && - other.path == path && - other.bookmark == bookmark; + return other is DeckFileReference && other.path == path; } @override - String toString() => - 'DeckFileReference(path: $path, bookmark: ${bookmark != null})'; - - @override - int get hashCode => Object.hash(path, bookmark); -} - -/// Thrown when a new deck's normalised filename already exists. -final class DeckNameCollisionException implements Exception { - /// The `.md` filename that collided. - final String fileName; - - const DeckNameCollisionException(this.fileName); + String toString() => 'DeckFileReference(path: $path)'; @override - String toString() => 'A deck named "$fileName" already exists.'; + int get hashCode => path.hashCode; } /// Thrown when a deck cannot be read. diff --git a/packages/playground/lib/core/data/data_sources/memory_deck_loader.dart b/packages/playground/lib/core/data/data_sources/memory_deck_loader.dart index 2fa7b5152..0a556d5ea 100644 --- a/packages/playground/lib/core/data/data_sources/memory_deck_loader.dart +++ b/packages/playground/lib/core/data/data_sources/memory_deck_loader.dart @@ -6,8 +6,7 @@ import '../mappers/deck_markdown_codec.dart'; /// A [DeckLoader] that parses markdown in-memory for live preview. class MemoryDeckLoader extends DeckLoader { - /// Shared with the deck-edit tools so the preview and the tools can never - /// decode the same markdown differently. + /// Decodes markdown for the live in-memory preview. static const _codec = DeckMarkdownCodec(); final _controller = StreamController.broadcast(); diff --git a/packages/playground/lib/core/data/data_sources/security_scoped_file_access.dart b/packages/playground/lib/core/data/data_sources/security_scoped_file_access.dart index 22f087097..923dc45fb 100644 --- a/packages/playground/lib/core/data/data_sources/security_scoped_file_access.dart +++ b/packages/playground/lib/core/data/data_sources/security_scoped_file_access.dart @@ -1,7 +1,5 @@ import 'package:flutter/services.dart'; -import 'deck_file.dart'; - /// A user-selected directory that remains accessible across app launches. final class SecurityScopedDirectoryReference { const SecurityScopedDirectoryReference({ @@ -38,19 +36,6 @@ class SecurityScopedFileAccess { final MethodChannel _channel; - /// Opens the native panel and returns its path plus persistent access data. - Future pickDeckFile() async { - final result = await _channel.invokeMapMethod( - 'pickDeckFile', - ); - if (result == null) return null; - final reference = _referenceValues(result, operation: 'selection'); - return DeckFileReference( - path: reference.path, - bookmark: reference.bookmark, - ); - } - /// Prompts for the parent directory where the `SuperDeck` folder will live. Future pickDecksDirectory() async { final result = await _channel.invokeMapMethod( @@ -67,22 +52,6 @@ class SecurityScopedFileAccess { ); } - /// Starts access and returns the current path/bookmark after resolution. - Future startAccessing(DeckFileReference reference) async { - final bookmark = reference.bookmark; - if (bookmark == null) return reference; - - final result = await _channel.invokeMapMethod( - 'startAccessing', - bookmark, - ); - if (result == null) { - throw StateError('Native bookmark resolution returned no data.'); - } - final active = _referenceValues(result, operation: 'resolution'); - return DeckFileReference(path: active.path, bookmark: active.bookmark); - } - /// Starts persistent access to a previously selected decks directory. Future startAccessingDirectory( SecurityScopedDirectoryReference reference, @@ -118,13 +87,6 @@ class SecurityScopedFileAccess { return (path: path, bookmark: refreshedBookmark); } - /// Stops access previously started for [reference]. - Future stopAccessing(DeckFileReference reference) async { - final bookmark = reference.bookmark; - if (bookmark == null) return; - await _channel.invokeMethod('stopAccessing', bookmark); - } - /// Stops access to a directory activated by [startAccessingDirectory]. Future stopAccessingDirectory( SecurityScopedDirectoryReference reference, diff --git a/packages/playground/lib/core/data/mappers/deck_markdown_codec.dart b/packages/playground/lib/core/data/mappers/deck_markdown_codec.dart index 9ae82f549..b9687df01 100644 --- a/packages/playground/lib/core/data/mappers/deck_markdown_codec.dart +++ b/packages/playground/lib/core/data/mappers/deck_markdown_codec.dart @@ -20,8 +20,4 @@ class DeckMarkdownCodec { ); } } - - /// Serializes [slides] to canonical SuperDeck Markdown. - String encode(List slides) => - const SlideSerializer().serialize(slides); } diff --git a/packages/playground/lib/core/data/repositories/.gitkeep b/packages/playground/lib/core/data/repositories/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/packages/playground/lib/core/domain/design/generated_deck_theme_factory.dart b/packages/playground/lib/core/domain/design/generated_deck_theme_factory.dart index dfb170492..120058250 100644 --- a/packages/playground/lib/core/domain/design/generated_deck_theme_factory.dart +++ b/packages/playground/lib/core/domain/design/generated_deck_theme_factory.dart @@ -46,8 +46,6 @@ final class PresentationTextStyles { /// Maps one resolved renderer-owned recipe to presentation-scale styling. final class GeneratedDeckThemeFactory { - static const treatmentNames = presentationThemeTreatmentNames; - const GeneratedDeckThemeFactory(); DeckOptions build({ diff --git a/packages/playground/lib/core/domain/design/presentation_theme_catalog.dart b/packages/playground/lib/core/domain/design/presentation_theme_catalog.dart index 038934d9e..5de59394b 100644 --- a/packages/playground/lib/core/domain/design/presentation_theme_catalog.dart +++ b/packages/playground/lib/core/domain/design/presentation_theme_catalog.dart @@ -13,22 +13,7 @@ const presentationThemeDensityProfiles = ['spacious', 'balanced', 'compact']; const presentationThemeTypeScales = ['dramatic', 'balanced', 'dense']; -const defaultPresentationThemeIds = [ - 'editorial-midnight', - 'technical-paper', - 'bold-product', - 'warm-editorial', - 'nordic-air', - 'monochrome-grid', - 'signal-studio', - 'playful-learning', - 'data-noir', - 'civic-blueprint', - 'organic-sage', - 'retro-poster', -]; - -/// Stable representative trio used by static Wizard examples and smoke tests. +/// Stable representative trio used by the theme-selection and live smoke tests. const featuredPresentationThemeIds = [ 'editorial-midnight', 'technical-paper', diff --git a/packages/playground/lib/core/domain/design/presentation_typography_catalog.dart b/packages/playground/lib/core/domain/design/presentation_typography_catalog.dart index a327f4608..f67c532de 100644 --- a/packages/playground/lib/core/domain/design/presentation_typography_catalog.dart +++ b/packages/playground/lib/core/domain/design/presentation_typography_catalog.dart @@ -49,9 +49,6 @@ final class PresentationTypographyCatalog { bool supports(String value, PresentationFontRole role) => resolve(value)?.roles.contains(role) ?? false; - List forRole(PresentationFontRole role) => - fonts.where((font) => font.roles.contains(role)).toList(growable: false); - List get familyNames => fonts.map((font) => font.family).toList(growable: false); diff --git a/packages/playground/lib/core/domain/models/.gitkeep b/packages/playground/lib/core/domain/models/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/packages/playground/lib/core/domain/repositories/.gitkeep b/packages/playground/lib/core/domain/repositories/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/packages/playground/lib/core/domain/stores/deck_customization_store.dart b/packages/playground/lib/core/domain/stores/deck_customization_store.dart index 68bd8c0f1..8f4943a46 100644 --- a/packages/playground/lib/core/domain/stores/deck_customization_store.dart +++ b/packages/playground/lib/core/domain/stores/deck_customization_store.dart @@ -12,10 +12,6 @@ import '../design/presentation_typography_catalog.dart'; const _defaultBackground = Color(0xFF000000); const _defaultForeground = Color(0xFFFFFFFF); -/// Curated font families surfaced in the playground's customization sidebar. -final playgroundFontFamilies = - PresentationTypographyCatalog.withDefaults().familyNames; - /// Renderer-ready style selected by the generation pipeline. final class GeneratedDeckStyle { final Color background; @@ -69,8 +65,7 @@ class TextLevelStyle { /// Owns the playground's deck-wide customization state and pushes a fresh /// [DeckOptions] into the [DeckController] on every change. /// -/// Ported from the signal-based original to a plain [ChangeNotifier]: each -/// mutation updates a field, rebuilds the [DeckOptions], writes it to the +/// Each mutation updates a field, rebuilds the [DeckOptions], writes it to the /// controller, and notifies. All state is in-memory; reloading resets to the /// seeded defaults. class DeckCustomizationStore extends ChangeNotifier { @@ -292,51 +287,8 @@ class DeckCustomizationStore extends ChangeNotifier { Color get background => _background; - set background(Color value) { - if (_background == value) return; - _background = value; - _apply(); - } - TextLevelStyle level(TextLevel level) => _levels[level]!; - void setColor(TextLevel level, Color color) { - final target = _levels[level]!; - if (target.color == color) return; - target.color = color; - _apply(); - } - - void setSize(TextLevel level, double size) { - final target = _levels[level]!; - if (target.size == size) return; - target.size = size; - _apply(); - } - - void setWeight(TextLevel level, int weight) { - final target = _levels[level]!; - final role = level == TextLevel.p - ? PresentationFontRole.body - : PresentationFontRole.headline; - final descriptor = _requireFont(target.family, role); - final supportedWeight = _nearestSupportedWeight(descriptor, weight); - if (target.weight == supportedWeight) return; - target.weight = supportedWeight; - _apply(); - } - - void setFamily(TextLevel level, String family) { - final target = _levels[level]!; - final role = level == TextLevel.p - ? PresentationFontRole.body - : PresentationFontRole.headline; - final descriptor = _requireFont(family, role); - if (target.family == descriptor.family) return; - target.family = descriptor.family; - _apply(); - } - /// Applies one generated visual system and pushes one coherent option update. void applyGeneratedStyle(GeneratedDeckStyle style) { final headline = _requireFont( diff --git a/packages/playground/lib/core/domain/stores/deck_document_store.dart b/packages/playground/lib/core/domain/stores/deck_document_store.dart index b052f566b..e43641850 100644 --- a/packages/playground/lib/core/domain/stores/deck_document_store.dart +++ b/packages/playground/lib/core/domain/stores/deck_document_store.dart @@ -4,9 +4,8 @@ import 'package:flutter/foundation.dart'; /// /// Generation publishes whole documents into it, and the preview and the saved /// deck both read from it. Identical content is intentionally ignored so a -/// republished but unchanged document cannot trigger a preview feedback loop. +/// republished but unchanged document does not trigger redundant work. class DeckDocumentStore extends ChangeNotifier { - int _revision = 0; String _markdown; DeckDocumentStore({required String markdown}) : _markdown = markdown; @@ -14,17 +13,10 @@ class DeckDocumentStore extends ChangeNotifier { /// The current full Markdown document. String get markdown => _markdown; - /// Counts the accepted replacements of this document. - /// - /// A long-running operation captures this value when it starts and compares - /// it before it publishes, so it cannot overwrite a newer document. - int get revision => _revision; - /// Replaces the document and notifies listeners when its text changed. void replaceMarkdown(String markdown) { if (markdown == _markdown) return; _markdown = markdown; - _revision++; notifyListeners(); } } diff --git a/packages/playground/lib/core/utils/color_utils.dart b/packages/playground/lib/core/utils/color_utils.dart deleted file mode 100644 index e97f6996b..000000000 --- a/packages/playground/lib/core/utils/color_utils.dart +++ /dev/null @@ -1,58 +0,0 @@ -import 'dart:developer' as developer; -import 'dart:ui'; - -/// Result of parsing a hex color string. -typedef ColorParseResult = ({Color color, bool isValid}); - -/// Default fallback color when parsing fails. -const _fallbackGray = Color(0xFF808080); - -/// Parses a hex color string and returns both the color and validity status. -/// -/// Supports both 6-digit (RGB) and 8-digit (ARGB) hex strings. -/// The '#' prefix is optional. -/// -/// Returns a [ColorParseResult] with: -/// - `color`: The parsed color, or fallback gray if invalid -/// - `isValid`: Whether the hex string was successfully parsed -/// -/// Examples: -/// ```dart -/// parseHexColor('#FF5733') // (color: Color(0xFFFF5733), isValid: true) -/// parseHexColor('FF5733') // (color: Color(0xFFFF5733), isValid: true) -/// parseHexColor('80FF5733') // (color: Color(0x80FF5733), isValid: true) -/// parseHexColor('invalid') // (color: Color(0xFF808080), isValid: false) -/// ``` -ColorParseResult parseHexColor(String hex) { - try { - hex = hex.replaceFirst('#', '').toUpperCase(); - if (hex.length == 6) hex = 'FF$hex'; - if (hex.length != 8) { - throw FormatException('Invalid hex color length: $hex'); - } - return (color: Color(int.parse(hex, radix: 16)), isValid: true); - } catch (e) { - developer.log('Invalid hex color: $hex', name: 'ColorUtils', error: e); - return (color: _fallbackGray, isValid: false); - } -} - -/// Convenience wrapper that returns only the color. -/// Use [parseHexColor] if you need to check validity. -Color hexToColor(String hex) => parseHexColor(hex).color; - -/// Formats [color] as an opaque, uppercase `#RRGGBB` hex string. -/// -/// Alpha is intentionally dropped — the playground's color controls are -/// opaque-only. The result round-trips through [parseHexColor]. -/// -/// ```dart -/// colorToHex(const Color(0xFF0485F7)) // '#0485F7' -/// colorToHex(const Color(0x800485F7)) // '#0485F7' (alpha dropped) -/// ``` -String colorToHex(Color color) { - String channel(double value) => - (value * 255).round().clamp(0, 255).toRadixString(16).padLeft(2, '0'); - return '#${channel(color.r)}${channel(color.g)}${channel(color.b)}' - .toUpperCase(); -} diff --git a/packages/playground/lib/features/ai/quick_agent/core/constants/gemini_models.dart b/packages/playground/lib/features/ai/generation/core/constants/gemini_models.dart similarity index 100% rename from packages/playground/lib/features/ai/quick_agent/core/constants/gemini_models.dart rename to packages/playground/lib/features/ai/generation/core/constants/gemini_models.dart diff --git a/packages/playground/lib/features/ai/quick_agent/core/constants/paths.dart b/packages/playground/lib/features/ai/generation/core/constants/paths.dart similarity index 100% rename from packages/playground/lib/features/ai/quick_agent/core/constants/paths.dart rename to packages/playground/lib/features/ai/generation/core/constants/paths.dart diff --git a/packages/playground/lib/features/ai/quick_agent/core/debug_logger.dart b/packages/playground/lib/features/ai/generation/core/debug_logger.dart similarity index 100% rename from packages/playground/lib/features/ai/quick_agent/core/debug_logger.dart rename to packages/playground/lib/features/ai/generation/core/debug_logger.dart diff --git a/packages/playground/lib/features/ai/quick_agent/core/engine/prompts/composition_example_library.dart b/packages/playground/lib/features/ai/generation/core/engine/prompts/composition_example_library.dart similarity index 100% rename from packages/playground/lib/features/ai/quick_agent/core/engine/prompts/composition_example_library.dart rename to packages/playground/lib/features/ai/generation/core/engine/prompts/composition_example_library.dart diff --git a/packages/playground/lib/features/ai/quick_agent/core/engine/prompts/generation_prompt_provider.dart b/packages/playground/lib/features/ai/generation/core/engine/prompts/generation_prompt_provider.dart similarity index 100% rename from packages/playground/lib/features/ai/quick_agent/core/engine/prompts/generation_prompt_provider.dart rename to packages/playground/lib/features/ai/generation/core/engine/prompts/generation_prompt_provider.dart diff --git a/packages/playground/lib/features/ai/quick_agent/core/engine/prompts/prompt_registry.dart b/packages/playground/lib/features/ai/generation/core/engine/prompts/prompt_registry.dart similarity index 100% rename from packages/playground/lib/features/ai/quick_agent/core/engine/prompts/prompt_registry.dart rename to packages/playground/lib/features/ai/generation/core/engine/prompts/prompt_registry.dart diff --git a/packages/playground/lib/features/ai/quick_agent/core/engine/schemas/deck_schemas.ack.dart b/packages/playground/lib/features/ai/generation/core/engine/schemas/deck_schemas.ack.dart similarity index 100% rename from packages/playground/lib/features/ai/quick_agent/core/engine/schemas/deck_schemas.ack.dart rename to packages/playground/lib/features/ai/generation/core/engine/schemas/deck_schemas.ack.dart diff --git a/packages/playground/lib/features/ai/quick_agent/core/engine/schemas/deck_schemas.ack.g.dart b/packages/playground/lib/features/ai/generation/core/engine/schemas/deck_schemas.ack.g.dart similarity index 100% rename from packages/playground/lib/features/ai/quick_agent/core/engine/schemas/deck_schemas.ack.g.dart rename to packages/playground/lib/features/ai/generation/core/engine/schemas/deck_schemas.ack.g.dart diff --git a/packages/playground/lib/features/ai/quick_agent/core/engine/schemas/deck_schemas.dart b/packages/playground/lib/features/ai/generation/core/engine/schemas/deck_schemas.dart similarity index 97% rename from packages/playground/lib/features/ai/quick_agent/core/engine/schemas/deck_schemas.dart rename to packages/playground/lib/features/ai/generation/core/engine/schemas/deck_schemas.dart index 2a86cea07..47e696f1d 100644 --- a/packages/playground/lib/features/ai/quick_agent/core/engine/schemas/deck_schemas.dart +++ b/packages/playground/lib/features/ai/generation/core/engine/schemas/deck_schemas.dart @@ -9,7 +9,7 @@ part 'deck_schemas.ack.g.dart'; /// Schema definitions for SuperDeck presentation generation. /// -/// The slide portion comes from `superdeck_core`'s [aiSlideSchema], the +/// The slide portion comes from `superdeck_core`'s `buildAiSlideSchema`, the /// AI-compatible projection of the canonical slide contract — Playground owns /// only the generation theme contract and prompt guidance. Schemas are compatible with /// Google Generative AI via `.toJsonSchemaBuilder()`. diff --git a/packages/playground/lib/features/ai/quick_agent/core/engine/schemas/outline_schema.ack.dart b/packages/playground/lib/features/ai/generation/core/engine/schemas/outline_schema.ack.dart similarity index 100% rename from packages/playground/lib/features/ai/quick_agent/core/engine/schemas/outline_schema.ack.dart rename to packages/playground/lib/features/ai/generation/core/engine/schemas/outline_schema.ack.dart diff --git a/packages/playground/lib/features/ai/quick_agent/core/engine/schemas/outline_schema.ack.g.dart b/packages/playground/lib/features/ai/generation/core/engine/schemas/outline_schema.ack.g.dart similarity index 100% rename from packages/playground/lib/features/ai/quick_agent/core/engine/schemas/outline_schema.ack.g.dart rename to packages/playground/lib/features/ai/generation/core/engine/schemas/outline_schema.ack.g.dart diff --git a/packages/playground/lib/features/ai/quick_agent/core/engine/schemas/outline_schema.dart b/packages/playground/lib/features/ai/generation/core/engine/schemas/outline_schema.dart similarity index 100% rename from packages/playground/lib/features/ai/quick_agent/core/engine/schemas/outline_schema.dart rename to packages/playground/lib/features/ai/generation/core/engine/schemas/outline_schema.dart diff --git a/packages/playground/lib/features/ai/quick_agent/core/engine/services/deck_generation_request.dart b/packages/playground/lib/features/ai/generation/core/engine/services/deck_generation_request.dart similarity index 100% rename from packages/playground/lib/features/ai/quick_agent/core/engine/services/deck_generation_request.dart rename to packages/playground/lib/features/ai/generation/core/engine/services/deck_generation_request.dart diff --git a/packages/playground/lib/features/ai/quick_agent/core/engine/services/deck_generator_images.dart b/packages/playground/lib/features/ai/generation/core/engine/services/deck_generator_images.dart similarity index 100% rename from packages/playground/lib/features/ai/quick_agent/core/engine/services/deck_generator_images.dart rename to packages/playground/lib/features/ai/generation/core/engine/services/deck_generator_images.dart diff --git a/packages/playground/lib/features/ai/quick_agent/core/engine/services/deck_generator_pipeline.dart b/packages/playground/lib/features/ai/generation/core/engine/services/deck_generator_pipeline.dart similarity index 100% rename from packages/playground/lib/features/ai/quick_agent/core/engine/services/deck_generator_pipeline.dart rename to packages/playground/lib/features/ai/generation/core/engine/services/deck_generator_pipeline.dart diff --git a/packages/playground/lib/features/ai/quick_agent/core/engine/services/deck_generator_pipeline_helpers.dart b/packages/playground/lib/features/ai/generation/core/engine/services/deck_generator_pipeline_helpers.dart similarity index 100% rename from packages/playground/lib/features/ai/quick_agent/core/engine/services/deck_generator_pipeline_helpers.dart rename to packages/playground/lib/features/ai/generation/core/engine/services/deck_generator_pipeline_helpers.dart diff --git a/packages/playground/lib/features/ai/quick_agent/core/engine/services/deck_generator_service.dart b/packages/playground/lib/features/ai/generation/core/engine/services/deck_generator_service.dart similarity index 100% rename from packages/playground/lib/features/ai/quick_agent/core/engine/services/deck_generator_service.dart rename to packages/playground/lib/features/ai/generation/core/engine/services/deck_generator_service.dart diff --git a/packages/playground/lib/features/ai/quick_agent/core/engine/services/deck_generator_workflow.dart b/packages/playground/lib/features/ai/generation/core/engine/services/deck_generator_workflow.dart similarity index 100% rename from packages/playground/lib/features/ai/quick_agent/core/engine/services/deck_generator_workflow.dart rename to packages/playground/lib/features/ai/generation/core/engine/services/deck_generator_workflow.dart diff --git a/packages/playground/lib/features/ai/quick_agent/core/engine/services/deck_plan_repair.dart b/packages/playground/lib/features/ai/generation/core/engine/services/deck_plan_repair.dart similarity index 100% rename from packages/playground/lib/features/ai/quick_agent/core/engine/services/deck_plan_repair.dart rename to packages/playground/lib/features/ai/generation/core/engine/services/deck_plan_repair.dart diff --git a/packages/playground/lib/features/ai/quick_agent/core/engine/services/deck_plan_validator.dart b/packages/playground/lib/features/ai/generation/core/engine/services/deck_plan_validator.dart similarity index 97% rename from packages/playground/lib/features/ai/quick_agent/core/engine/services/deck_plan_validator.dart rename to packages/playground/lib/features/ai/generation/core/engine/services/deck_plan_validator.dart index aeb9d6077..4394a9a47 100644 --- a/packages/playground/lib/features/ai/quick_agent/core/engine/services/deck_plan_validator.dart +++ b/packages/playground/lib/features/ai/generation/core/engine/services/deck_plan_validator.dart @@ -10,25 +10,6 @@ import 'generation_validation_issue.dart'; import 'source_grounding.dart'; import 'theme_json_serializer.dart'; -/// Returns semantic errors that are not expressible in the deck-plan schema. -List validateDeckPlan( - DeckPlan plan, { - int? expectedSlideCount, - PresentationTypographyCatalog? typographyCatalog, - PresentationImageStyleCatalog? imageStyleCatalog, - PresentationThemeCatalog? themeCatalog, - DeckGenerationRequest? request, - Set knownGeneratedAssetKeys = const {}, -}) => validateDeckPlanIssues( - plan, - expectedSlideCount: expectedSlideCount, - typographyCatalog: typographyCatalog, - imageStyleCatalog: imageStyleCatalog, - themeCatalog: themeCatalog, - request: request, - knownGeneratedAssetKeys: knownGeneratedAssetKeys, -).messages; - /// Returns typed semantic issues for pipeline decisions and diagnostics. List validateDeckPlanIssues( DeckPlan plan, { diff --git a/packages/playground/lib/features/ai/quick_agent/core/engine/services/deck_theme_resolution.dart b/packages/playground/lib/features/ai/generation/core/engine/services/deck_theme_resolution.dart similarity index 100% rename from packages/playground/lib/features/ai/quick_agent/core/engine/services/deck_theme_resolution.dart rename to packages/playground/lib/features/ai/generation/core/engine/services/deck_theme_resolution.dart diff --git a/packages/playground/lib/features/ai/quick_agent/core/engine/services/design_quality_metrics.dart b/packages/playground/lib/features/ai/generation/core/engine/services/design_quality_metrics.dart similarity index 75% rename from packages/playground/lib/features/ai/quick_agent/core/engine/services/design_quality_metrics.dart rename to packages/playground/lib/features/ai/generation/core/engine/services/design_quality_metrics.dart index cf5ad256f..c1820a646 100644 --- a/packages/playground/lib/features/ai/quick_agent/core/engine/services/design_quality_metrics.dart +++ b/packages/playground/lib/features/ai/generation/core/engine/services/design_quality_metrics.dart @@ -7,21 +7,6 @@ double calculateContrastRatio(String foreground, String background) { return calculatePresentationContrast(foreground, background); } -/// Returns the more readable of black or white for [background]. -/// -/// Generated accent colors are arbitrary, but the foreground placed on an -/// accent is a derived palette value. Choosing the higher-contrast monochrome -/// foreground is deterministic and always provides WCAG AA contrast for normal -/// text, avoiding an unreliable model round trip for color arithmetic. -String mostReadableMonochromeForeground(String background) { - const black = '#000000'; - const white = '#FFFFFF'; - return calculateContrastRatio(black, background) >= - calculateContrastRatio(white, background) - ? black - : white; -} - /// Maximum visible Markdown characters for a density and composition shape. /// /// Density expresses desired pacing, while composition caps reflect the actual diff --git a/packages/playground/lib/features/ai/quick_agent/core/engine/services/error_classifier.dart b/packages/playground/lib/features/ai/generation/core/engine/services/error_classifier.dart similarity index 100% rename from packages/playground/lib/features/ai/quick_agent/core/engine/services/error_classifier.dart rename to packages/playground/lib/features/ai/generation/core/engine/services/error_classifier.dart diff --git a/packages/playground/lib/features/ai/quick_agent/core/engine/services/generated_slide_validator.dart b/packages/playground/lib/features/ai/generation/core/engine/services/generated_slide_validator.dart similarity index 98% rename from packages/playground/lib/features/ai/quick_agent/core/engine/services/generated_slide_validator.dart rename to packages/playground/lib/features/ai/generation/core/engine/services/generated_slide_validator.dart index 2f483748d..59b2919b9 100644 --- a/packages/playground/lib/features/ai/quick_agent/core/engine/services/generated_slide_validator.dart +++ b/packages/playground/lib/features/ai/generation/core/engine/services/generated_slide_validator.dart @@ -218,21 +218,6 @@ Map _normalizeBlockHeading(Map block) { return block; } -/// Validates one generated slide before it becomes context for the next slide. -List validateGeneratedSlide({ - required String expectedKey, - required Map rawSlide, - required GenerationElementCatalog elementCatalog, - DeckPlanSlide? planSlide, - DeckGenerationRequest? request, -}) => validateGeneratedSlideIssues( - expectedKey: expectedKey, - rawSlide: rawSlide, - elementCatalog: elementCatalog, - planSlide: planSlide, - request: request, -).messages; - /// Returns typed issues for one generated slide draft. List validateGeneratedSlideIssues({ required String expectedKey, diff --git a/packages/playground/lib/features/ai/quick_agent/core/engine/services/generation_element_catalog.dart b/packages/playground/lib/features/ai/generation/core/engine/services/generation_element_catalog.dart similarity index 100% rename from packages/playground/lib/features/ai/quick_agent/core/engine/services/generation_element_catalog.dart rename to packages/playground/lib/features/ai/generation/core/engine/services/generation_element_catalog.dart diff --git a/packages/playground/lib/features/ai/quick_agent/core/engine/services/generation_model_call_executor.dart b/packages/playground/lib/features/ai/generation/core/engine/services/generation_model_call_executor.dart similarity index 100% rename from packages/playground/lib/features/ai/quick_agent/core/engine/services/generation_model_call_executor.dart rename to packages/playground/lib/features/ai/generation/core/engine/services/generation_model_call_executor.dart diff --git a/packages/playground/lib/features/ai/quick_agent/core/engine/services/generation_model_client.dart b/packages/playground/lib/features/ai/generation/core/engine/services/generation_model_client.dart similarity index 100% rename from packages/playground/lib/features/ai/quick_agent/core/engine/services/generation_model_client.dart rename to packages/playground/lib/features/ai/generation/core/engine/services/generation_model_client.dart diff --git a/packages/playground/lib/features/ai/quick_agent/core/engine/services/generation_progress.dart b/packages/playground/lib/features/ai/generation/core/engine/services/generation_progress.dart similarity index 100% rename from packages/playground/lib/features/ai/quick_agent/core/engine/services/generation_progress.dart rename to packages/playground/lib/features/ai/generation/core/engine/services/generation_progress.dart diff --git a/packages/playground/lib/features/ai/quick_agent/core/engine/services/generation_quality_report.dart b/packages/playground/lib/features/ai/generation/core/engine/services/generation_quality_report.dart similarity index 100% rename from packages/playground/lib/features/ai/quick_agent/core/engine/services/generation_quality_report.dart rename to packages/playground/lib/features/ai/generation/core/engine/services/generation_quality_report.dart diff --git a/packages/playground/lib/features/ai/quick_agent/core/engine/services/generation_trace.dart b/packages/playground/lib/features/ai/generation/core/engine/services/generation_trace.dart similarity index 100% rename from packages/playground/lib/features/ai/quick_agent/core/engine/services/generation_trace.dart rename to packages/playground/lib/features/ai/generation/core/engine/services/generation_trace.dart diff --git a/packages/playground/lib/features/ai/quick_agent/core/engine/services/generation_validation_issue.dart b/packages/playground/lib/features/ai/generation/core/engine/services/generation_validation_issue.dart similarity index 98% rename from packages/playground/lib/features/ai/quick_agent/core/engine/services/generation_validation_issue.dart rename to packages/playground/lib/features/ai/generation/core/engine/services/generation_validation_issue.dart index 79f9a97e1..4cf12708b 100644 --- a/packages/playground/lib/features/ai/quick_agent/core/engine/services/generation_validation_issue.dart +++ b/packages/playground/lib/features/ai/generation/core/engine/services/generation_validation_issue.dart @@ -6,8 +6,6 @@ enum GenerationValidationCode { slideIdentity, planStructure, themeResolution, - typography, - paletteContrast, elementGrounding, visibleSourceGrounding, numericGrounding, @@ -30,7 +28,6 @@ enum GenerationValidationCategory { structure, grounding, factual, - accessibility, quality, } diff --git a/packages/playground/lib/features/ai/quick_agent/core/engine/services/google_schema_adapter.dart b/packages/playground/lib/features/ai/generation/core/engine/services/google_schema_adapter.dart similarity index 100% rename from packages/playground/lib/features/ai/quick_agent/core/engine/services/google_schema_adapter.dart rename to packages/playground/lib/features/ai/generation/core/engine/services/google_schema_adapter.dart diff --git a/packages/playground/lib/features/ai/quick_agent/core/engine/services/retry_policy.dart b/packages/playground/lib/features/ai/generation/core/engine/services/retry_policy.dart similarity index 100% rename from packages/playground/lib/features/ai/quick_agent/core/engine/services/retry_policy.dart rename to packages/playground/lib/features/ai/generation/core/engine/services/retry_policy.dart diff --git a/packages/playground/lib/features/ai/quick_agent/core/engine/services/source_grounding.dart b/packages/playground/lib/features/ai/generation/core/engine/services/source_grounding.dart similarity index 98% rename from packages/playground/lib/features/ai/quick_agent/core/engine/services/source_grounding.dart rename to packages/playground/lib/features/ai/generation/core/engine/services/source_grounding.dart index 4c7eba586..4a9852917 100644 --- a/packages/playground/lib/features/ai/quick_agent/core/engine/services/source_grounding.dart +++ b/packages/playground/lib/features/ai/generation/core/engine/services/source_grounding.dart @@ -595,15 +595,6 @@ Iterable _sentenceLikeSnippets(String value) sync* { bool _isAsciiDigit(int codeUnit) => codeUnit >= 0x30 && codeUnit <= 0x39; -/// Maps each supplied numeric token to nearby words that preserve its meaning. -Map> extractNumericFactAnchors(String value) { - final anchors = >{}; - for (final entry in _extractNumericFactAnchorGroups(value).entries) { - anchors[entry.key] = {for (final group in entry.value) ...group}; - } - return anchors; -} - Map>> _extractNumericFactAnchorGroups(String value) { final anchors = >>{}; for (final context in _numericFactContexts(value)) { diff --git a/packages/playground/lib/features/ai/quick_agent/core/engine/services/theme_json_serializer.dart b/packages/playground/lib/features/ai/generation/core/engine/services/theme_json_serializer.dart similarity index 100% rename from packages/playground/lib/features/ai/quick_agent/core/engine/services/theme_json_serializer.dart rename to packages/playground/lib/features/ai/generation/core/engine/services/theme_json_serializer.dart diff --git a/packages/playground/lib/features/ai/quick_agent/core/env_config.dart b/packages/playground/lib/features/ai/generation/core/env_config.dart similarity index 100% rename from packages/playground/lib/features/ai/quick_agent/core/env_config.dart rename to packages/playground/lib/features/ai/generation/core/env_config.dart diff --git a/packages/playground/lib/features/ai/quick_agent/domain/generated_deck_result_applier.dart b/packages/playground/lib/features/ai/generation/domain/generated_deck_result_applier.dart similarity index 86% rename from packages/playground/lib/features/ai/quick_agent/domain/generated_deck_result_applier.dart rename to packages/playground/lib/features/ai/generation/domain/generated_deck_result_applier.dart index d415b813e..af634b8a0 100644 --- a/packages/playground/lib/features/ai/quick_agent/domain/generated_deck_result_applier.dart +++ b/packages/playground/lib/features/ai/generation/domain/generated_deck_result_applier.dart @@ -23,8 +23,8 @@ final class GeneratedDeckApplication { /// Failure raised while removing artwork that the published deck replaced. /// - /// The deck is published when this is set. Hosts report it separately from a - /// generation failure. + /// The deck is published when this is set. The host reports it separately + /// from a generation failure. final Object? cleanupError; const GeneratedDeckApplication({required this.published, this.cleanupError}); @@ -35,8 +35,8 @@ final class GeneratedDeckApplication { final class GeneratedDeckResultApplier { final DeckDocumentStore _documentStore; - final MemoryDeckLoader? _deckLoader; - final AssetCacheStore? _assetCacheStore; + final MemoryDeckLoader _deckLoader; + final AssetCacheStore _assetCacheStore; final DeckCustomizationStore _customizationStore; Set _appliedAssetKeys = const {}; @@ -45,8 +45,8 @@ final class GeneratedDeckResultApplier { Future _queue = Future.value(); GeneratedDeckResultApplier({ required DeckDocumentStore documentStore, - MemoryDeckLoader? deckLoader, - AssetCacheStore? assetCacheStore, + required MemoryDeckLoader deckLoader, + required AssetCacheStore assetCacheStore, required DeckCustomizationStore customizationStore, }) : _documentStore = documentStore, _deckLoader = deckLoader, @@ -56,11 +56,10 @@ final class GeneratedDeckResultApplier { /// Deletes only the artwork this attempt staged, and keeps every asset key /// that an earlier attempt already committed. Future _discardStagedAssets(Set stagedAssetKeys) async { - final cache = _assetCacheStore; - if (cache == null || stagedAssetKeys.isEmpty) return; + if (stagedAssetKeys.isEmpty) return; for (final assetKey in stagedAssetKeys) { try { - await cache.delete(assetKey); + await _assetCacheStore.delete(assetKey); } catch (_) { // An abandoned asset that cannot be deleted must not mask the reason // the application stopped. @@ -75,7 +74,6 @@ final class GeneratedDeckResultApplier { const abandoned = GeneratedDeckApplication(published: false); if (!isValid()) return abandoned; - final cache = _assetCacheStore; final nextAssetKeys = {}; final stagedAssetKeys = {}; @@ -83,12 +81,7 @@ final class GeneratedDeckResultApplier { for (final asset in result.generatedImages) { final bytes = asset.bytes; if (bytes == null || bytes.isEmpty) continue; - if (cache == null) { - throw StateError( - 'Generated artwork cannot be loaded without an asset cache.', - ); - } - await cache.write(asset.assetKey, bytes); + await _assetCacheStore.write(asset.assetKey, bytes); nextAssetKeys.add(asset.assetKey); if (!_appliedAssetKeys.contains(asset.assetKey)) { stagedAssetKeys.add(asset.assetKey); @@ -109,7 +102,7 @@ final class GeneratedDeckResultApplier { final markdown = const SlideSerializer().serialize(result.slides); _documentStore.replaceMarkdown(markdown); - _deckLoader?.updateMarkdown(markdown); + _deckLoader.updateMarkdown(markdown); if (result.theme case final theme?) { _customizationStore.applyGeneratedStyle(theme.toGeneratedDeckStyle()); } @@ -125,15 +118,13 @@ final class GeneratedDeckResultApplier { /// Returns the first deletion failure, which the host reports separately /// from a generation failure because the deck is already published. Future _removeObsoleteAssets(Set nextAssetKeys) async { - final cache = _assetCacheStore; final obsoleteAssetKeys = _appliedAssetKeys.difference(nextAssetKeys); _appliedAssetKeys = Set.unmodifiable(nextAssetKeys); - if (cache == null) return null; Object? cleanupError; for (final assetKey in obsoleteAssetKeys) { try { - await cache.delete(assetKey); + await _assetCacheStore.delete(assetKey); } catch (error) { cleanupError ??= error; } diff --git a/packages/playground/lib/features/ai/image_generation/image_generator.dart b/packages/playground/lib/features/ai/image_generation/image_generator.dart index fd1d7c506..acba46d05 100644 --- a/packages/playground/lib/features/ai/image_generation/image_generator.dart +++ b/packages/playground/lib/features/ai/image_generation/image_generator.dart @@ -4,7 +4,7 @@ import 'dart:typed_data'; import 'package:dartantic_ai/dartantic_ai.dart'; import '../../../core/domain/generated_image_asset.dart'; -import '../quick_agent/core/engine/services/error_classifier.dart'; +import '../generation/core/engine/services/error_classifier.dart'; const geminiImagePreviewModel = 'gemini-3.1-flash-lite-image'; const geminiImageGenerationModel = 'gemini-3.1-flash-image'; diff --git a/packages/playground/lib/features/ai/wizard/chat/view/widgets/chat_input.dart b/packages/playground/lib/features/ai/wizard/chat/view/widgets/chat_input.dart index dd496d1fc..19cc1cae3 100644 --- a/packages/playground/lib/features/ai/wizard/chat/view/widgets/chat_input.dart +++ b/packages/playground/lib/features/ai/wizard/chat/view/widgets/chat_input.dart @@ -2,7 +2,8 @@ import 'package:flutter/material.dart'; import 'package:hero_ui/hero_ui.dart'; import 'package:remix/remix.dart'; -/// Shared chat input widget used by both sidebar and inline views. +/// Shared chat input widget used by both the empty-state and in-conversation +/// views. /// /// Provides consistent behavior: disabled while thinking, unified hint text, /// and proper TextInputAction.send for enter-to-submit. diff --git a/packages/playground/lib/features/ai/wizard/core/ai/prompts/prompt_registry.dart b/packages/playground/lib/features/ai/wizard/core/ai/prompts/prompt_registry.dart index 648d95eee..5a37adaff 100644 --- a/packages/playground/lib/features/ai/wizard/core/ai/prompts/prompt_registry.dart +++ b/packages/playground/lib/features/ai/wizard/core/ai/prompts/prompt_registry.dart @@ -1 +1 @@ -export '../../../../quick_agent/core/engine/prompts/prompt_registry.dart'; +export '../../../../generation/core/engine/prompts/prompt_registry.dart'; 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 2ac29c96b..71d0a48ee 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 @@ -5,7 +5,7 @@ import 'package:flutter/foundation.dart'; import 'package:genui/genui.dart' as genui; import 'package:signals/signals_flutter.dart'; -import '../../../../quick_agent/core/constants/gemini_models.dart'; +import '../../../../generation/core/constants/gemini_models.dart'; import '../../../../image_generation/image_style_preview_coordinator.dart'; import '../../../../../../core/domain/design/presentation_theme_catalog.dart'; import '../../../../../../core/domain/design/presentation_image_style_catalog.dart'; diff --git a/packages/playground/lib/features/ai/wizard/core/ai/services/error_classifier.dart b/packages/playground/lib/features/ai/wizard/core/ai/services/error_classifier.dart index bf91c0881..2a36ad9da 100644 --- a/packages/playground/lib/features/ai/wizard/core/ai/services/error_classifier.dart +++ b/packages/playground/lib/features/ai/wizard/core/ai/services/error_classifier.dart @@ -1 +1 @@ -export '../../../../quick_agent/core/engine/services/error_classifier.dart'; +export '../../../../generation/core/engine/services/error_classifier.dart'; diff --git a/packages/playground/lib/features/ai/wizard/core/ai/services/prompt_builder.dart b/packages/playground/lib/features/ai/wizard/core/ai/services/prompt_builder.dart index 6d037e2df..ca193e02a 100644 --- a/packages/playground/lib/features/ai/wizard/core/ai/services/prompt_builder.dart +++ b/packages/playground/lib/features/ai/wizard/core/ai/services/prompt_builder.dart @@ -1,5 +1,5 @@ import '../../../../../../core/domain/design/presentation_image_style_catalog.dart'; -import '../../../../quick_agent/core/engine/services/deck_generation_request.dart'; +import '../../../../generation/core/engine/services/deck_generation_request.dart'; import '../wizard_context.dart'; /// Builds a deck generation prompt from wizard context data. diff --git a/packages/playground/lib/features/ai/wizard/core/debug_logger.dart b/packages/playground/lib/features/ai/wizard/core/debug_logger.dart index 15a2a7d7c..bf833307a 100644 --- a/packages/playground/lib/features/ai/wizard/core/debug_logger.dart +++ b/packages/playground/lib/features/ai/wizard/core/debug_logger.dart @@ -1 +1 @@ -export '../../quick_agent/core/debug_logger.dart'; +export '../../generation/core/debug_logger.dart'; diff --git a/packages/playground/lib/features/ai/wizard/core/env_config.dart b/packages/playground/lib/features/ai/wizard/core/env_config.dart index 9d5058696..0ce7e3afe 100644 --- a/packages/playground/lib/features/ai/wizard/core/env_config.dart +++ b/packages/playground/lib/features/ai/wizard/core/env_config.dart @@ -1 +1 @@ -export '../../quick_agent/core/env_config.dart'; +export '../../generation/core/env_config.dart'; diff --git a/packages/playground/lib/features/ai/wizard/core/ui/components/sd_buttons.dart b/packages/playground/lib/features/ai/wizard/core/ui/components/sd_buttons.dart index c76abad6f..a1633fbe4 100644 --- a/packages/playground/lib/features/ai/wizard/core/ui/components/sd_buttons.dart +++ b/packages/playground/lib/features/ai/wizard/core/ui/components/sd_buttons.dart @@ -53,37 +53,3 @@ class SdButton extends StatelessWidget { ); } } - -/// Pre-styled surface icon button. -class SdIconButton extends StatelessWidget { - const SdIconButton({ - super.key, - required this.icon, - required this.onPressed, - this.loading = false, - this.semanticLabel, - this.style, - }); - - final IconData icon; - final VoidCallback? onPressed; - final bool loading; - final String? semanticLabel; - final RemixIconButtonStyler? style; - - @override - Widget build(BuildContext context) { - final button = HeroIconButton( - icon: icon, - onPressed: onPressed, - loading: loading, - variant: HeroButtonVariant.secondary, - style: style, - ); - - if (semanticLabel != null) { - return Semantics(button: true, label: semanticLabel, child: button); - } - return button; - } -} 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 4ea652fc8..21c5b3bcc 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 @@ -77,22 +77,3 @@ class SdCaption extends StatelessWidget { return defaultStyle.merge(style).call(text); } } - -/// Subtle hint text. -/// -/// Usage: `SdHint('Enter your name')` -/// Override: `SdHint('Hint', style: TextStyler().fontStyle(FontStyle.italic))` -class SdHint extends StatelessWidget { - const SdHint(this.text, {super.key, this.style}); - - final String text; - final TextStyler? style; - - @override - Widget build(BuildContext context) { - final defaultStyle = TextStyler() - .style($paragraphSmall.mix()) - .color($fieldPlaceholder()); - return defaultStyle.merge(style).call(text); - } -} diff --git a/packages/playground/lib/features/ai/wizard/core/ui/ui.dart b/packages/playground/lib/features/ai/wizard/core/ui/ui.dart index 3ef6760c5..f4d9bae96 100644 --- a/packages/playground/lib/features/ai/wizard/core/ui/ui.dart +++ b/packages/playground/lib/features/ai/wizard/core/ui/ui.dart @@ -1,3 +1,2 @@ export 'components/sd_components.dart'; export 'widgets/catalog_next_button.dart'; -export 'widgets/header.dart'; diff --git a/packages/playground/lib/features/ai/wizard/core/ui/widgets/header.dart b/packages/playground/lib/features/ai/wizard/core/ui/widgets/header.dart deleted file mode 100644 index 4050d5bc0..000000000 --- a/packages/playground/lib/features/ai/wizard/core/ui/widgets/header.dart +++ /dev/null @@ -1,39 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hero_ui/hero_ui.dart'; -import 'package:remix/remix.dart'; - -/// Height of the header content area (excluding system padding). -const _kHeaderContentHeight = 60.0; - -class SdHeader extends StatelessWidget implements PreferredSizeWidget { - const SdHeader({super.key, this.leading, this.trailing}); - - final Widget? leading; - final Widget? trailing; - - @override - Widget build(BuildContext context) { - final flex = FlexBoxStyler() - .borderBottom(color: $separator()) - .padding(.symmetric(horizontal: 24, vertical: 12)) - .height(_kHeaderContentHeight) - .crossAxisAlignment(.center) - .mainAxisAlignment(.spaceBetween); - - return SafeArea( - bottom: false, - child: flex( - children: [ - SizedBox(child: leading), - SizedBox(child: trailing), - ], - ), - ); - } - - @override - Size get preferredSize { - // Content height only; Scaffold accounts for top system padding. - return const Size.fromHeight(_kHeaderContentHeight); - } -} 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 fde0d9390..5e8860816 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 @@ -2,13 +2,13 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; -import '../../quick_agent/core/engine/schemas/outline_schema.dart'; -import '../../quick_agent/core/engine/services/deck_generation_request.dart'; -import '../../quick_agent/core/engine/services/deck_generator_service.dart'; -import '../../quick_agent/core/engine/services/deck_plan_validator.dart'; -import '../../quick_agent/core/engine/services/generation_progress.dart'; -import '../../quick_agent/core/engine/services/generation_validation_issue.dart'; -import '../../quick_agent/domain/generated_deck_result_applier.dart'; +import '../../generation/core/engine/schemas/outline_schema.dart'; +import '../../generation/core/engine/services/deck_generation_request.dart'; +import '../../generation/core/engine/services/deck_generator_service.dart'; +import '../../generation/core/engine/services/deck_plan_validator.dart'; +import '../../generation/core/engine/services/generation_progress.dart'; +import '../../generation/core/engine/services/generation_validation_issue.dart'; +import '../../generation/domain/generated_deck_result_applier.dart'; enum WizardGenerationStage { setup, @@ -191,8 +191,6 @@ final class WizardGenerationController extends ChangeNotifier { WizardGenerationStage get stage => _stage; - WizardGenerationPhase? get failedPhase => _failedPhase; - DeckPlan? get plan => _plan; int get planRevision => _planRevision; diff --git a/packages/playground/lib/features/ai/wizard/presentation/wizard_generation_status.dart b/packages/playground/lib/features/ai/wizard/presentation/wizard_generation_status.dart index 464294d60..0b16d97d8 100644 --- a/packages/playground/lib/features/ai/wizard/presentation/wizard_generation_status.dart +++ b/packages/playground/lib/features/ai/wizard/presentation/wizard_generation_status.dart @@ -3,7 +3,7 @@ import 'package:hero_ui/hero_ui.dart'; import 'package:lucide_icons_flutter/lucide_icons.dart'; import 'package:remix/remix.dart'; -import '../../quick_agent/core/engine/services/generation_progress.dart'; +import '../../generation/core/engine/services/generation_progress.dart'; import '../core/ui/ui.dart'; enum WizardGenerationStatusKind { running, completed, failed } 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 2bb2b33e2..de5bd9f78 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 @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:hero_ui/hero_ui.dart'; import 'package:lucide_icons_flutter/lucide_icons.dart'; -import '../../quick_agent/core/engine/schemas/outline_schema.dart'; +import '../../generation/core/engine/schemas/outline_schema.dart'; import '../core/ui/ui.dart'; typedef UpdateOutlineSlide = diff --git a/packages/playground/lib/features/ai/wizard/presentation/wizard_page.dart b/packages/playground/lib/features/ai/wizard/presentation/wizard_page.dart index 4ddfe808d..b79619c9b 100644 --- a/packages/playground/lib/features/ai/wizard/presentation/wizard_page.dart +++ b/packages/playground/lib/features/ai/wizard/presentation/wizard_page.dart @@ -13,9 +13,9 @@ import '../../../library/domain/deck_library.dart'; import '../../../library/domain/deck_library_controller.dart'; import '../../../library/domain/saved_deck.dart'; import '../../../library/presentation/save_deck_dialog.dart'; -import '../../quick_agent/core/engine/services/deck_generator_service.dart'; -import '../../quick_agent/core/env_config.dart'; -import '../../quick_agent/domain/generated_deck_result_applier.dart'; +import '../../generation/core/engine/services/deck_generator_service.dart'; +import '../../generation/core/env_config.dart'; +import '../../generation/domain/generated_deck_result_applier.dart'; import '../../image_generation/image_generator.dart'; import '../../image_generation/image_style_preview_coordinator.dart'; import '../chat/chat_conversation_profile.dart'; @@ -28,9 +28,9 @@ import 'wizard_view.dart'; /// Host for the conversational Wizard, the app's only authoring flow. /// -/// Generated Markdown is kept in memory so opening this screen never requests a -/// deck-storage folder. The production integration can provide its own document -/// destination when the Wizard is embedded outside the playground. +/// Generated Markdown is kept in memory: results are applied straight to the +/// in-memory document and preview stores, and a deck-storage folder is only +/// requested later, when the reader chooses to save. class WizardPage extends StatelessWidget { const WizardPage({ this.isConfigured, diff --git a/packages/playground/lib/features/ai/wizard/presentation/wizard_view.dart b/packages/playground/lib/features/ai/wizard/presentation/wizard_view.dart index 725591fdb..7b7958e0b 100644 --- a/packages/playground/lib/features/ai/wizard/presentation/wizard_view.dart +++ b/packages/playground/lib/features/ai/wizard/presentation/wizard_view.dart @@ -102,7 +102,8 @@ class _WizardBodyState extends State<_WizardBody> { ); // Before the first message: show the empty state with the topic prompt. - // It centers when it fits and scrolls when the sidebar is too short. + // It centers when it fits and scrolls when the available height is too + // short. if (!started) { return LayoutBuilder( builder: (context, constraints) => SingleChildScrollView( diff --git a/packages/playground/lib/features/library/domain/deck_library_controller.dart b/packages/playground/lib/features/library/domain/deck_library_controller.dart index d77663b1e..096474f6e 100644 --- a/packages/playground/lib/features/library/domain/deck_library_controller.dart +++ b/packages/playground/lib/features/library/domain/deck_library_controller.dart @@ -27,7 +27,6 @@ class DeckLibraryController extends ChangeNotifier { final PresentationTypographyCatalog _typographyCatalog; List _decks = const []; - SavedDeckRef? _openDeck; DeckSaveOutcome? _lastSave; String? _errorMessage; bool _isBusy = false; @@ -80,9 +79,6 @@ class DeckLibraryController extends ChangeNotifier { /// Saved decks, newest first. List get decks => _decks; - /// The saved deck being presented, or `null` for a generated one. - SavedDeckRef? get openDeck => _openDeck; - /// What the last save wrote, for the notice the Wizard shows. DeckSaveOutcome? get lastSave => _lastSave; @@ -118,7 +114,6 @@ class DeckLibraryController extends ChangeNotifier { case Ok(:final value): _lastSave = value; // The saved copy, not the run's memory, now owns this artwork. - _openDeck = value.ref; _assetStore.bindTo(value.ref); _decks = [value.ref, ..._decks.where((ref) => ref != value.ref)]; @@ -160,7 +155,6 @@ class DeckLibraryController extends ChangeNotifier { try { switch (await _library.open(ref)) { case Ok(:final value): - _openDeck = value.ref; _assetStore.bindTo(value.ref); _documentStore.replaceMarkdown(value.markdown); _deckLoader.updateMarkdown(value.markdown); @@ -179,7 +173,6 @@ class DeckLibraryController extends ChangeNotifier { /// Forgets the open saved deck, so a new generation owns the runtime again. void releaseOpenDeck() { - _openDeck = null; _lastSave = null; _assetStore.bindTo(null); notifyListeners(); diff --git a/packages/playground/macos/Runner/MainFlutterWindow.swift b/packages/playground/macos/Runner/MainFlutterWindow.swift index d38d0e429..6cc1e8f92 100644 --- a/packages/playground/macos/Runner/MainFlutterWindow.swift +++ b/packages/playground/macos/Runner/MainFlutterWindow.swift @@ -22,10 +22,8 @@ class MainFlutterWindow: NSWindow { self.styleMask.insert(.fullSizeContentView) self.isMovableByWindowBackground = true - // Both editor sidebars are visible by default. The template's 800-point - // window leaves the central editor with almost no width, so start at a - // useful desktop size and prevent the window from shrinking back into that - // unusable layout. + // Start at a useful desktop size instead of the template's 800-point + // window, and keep the window from shrinking below it. self.contentMinSize = Self.minimumContentSize if self.contentLayoutRect.width < Self.minimumContentSize.width || self.contentLayoutRect.height < Self.minimumContentSize.height { diff --git a/packages/playground/macos/Runner/SecurityScopedBookmarkManager.swift b/packages/playground/macos/Runner/SecurityScopedBookmarkManager.swift index c9e24709b..89e4cefb8 100644 --- a/packages/playground/macos/Runner/SecurityScopedBookmarkManager.swift +++ b/packages/playground/macos/Runner/SecurityScopedBookmarkManager.swift @@ -1,6 +1,5 @@ import Cocoa import FlutterMacOS -import UniformTypeIdentifiers private enum SecurityScopedBookmarkError: LocalizedError { case invalidArgument(String) @@ -31,8 +30,6 @@ final class SecurityScopedBookmarkManager { func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { do { switch call.method { - case "pickDeckFile": - result(try pickDeckFile()) case "pickDecksDirectory": result(try pickDecksDirectory()) case "startAccessing": @@ -58,24 +55,6 @@ final class SecurityScopedBookmarkManager { } } - private func pickDeckFile() throws -> [String: String]? { - let panel = NSOpenPanel() - panel.title = "Open deck" - panel.allowsMultipleSelection = false - panel.canChooseDirectories = false - panel.canChooseFiles = true - - if #available(macOS 11.0, *) { - if let markdown = UTType(filenameExtension: "md") { - panel.allowedContentTypes = [markdown] - } - } else { - panel.allowedFileTypes = ["md"] - } - - return try selectURL(with: panel) - } - private func pickDecksDirectory() throws -> [String: String]? { let panel = NSOpenPanel() panel.title = "Choose where SuperDeck saves decks" diff --git a/packages/playground/pubspec.yaml b/packages/playground/pubspec.yaml index 9b27c99d0..05dd4ab64 100644 --- a/packages/playground/pubspec.yaml +++ b/packages/playground/pubspec.yaml @@ -13,12 +13,11 @@ resolution: workspace dependencies: flutter: sdk: flutter - # State management & DI — Provider + Command pattern. + # State management & DI. provider: ^6.1.2 go_router: ^17.3.0 - # signals: core/ bridge stores translate DeckController signals into - # ChangeNotifier; signals_flutter lets the UI watch DeckController.slides - # directly, so slides need no bridge store. + # signals: the Wizard conversation view model's reactive state. + # signals_flutter: main.dart turns off signals' debug logging. signals: ^7.1.0 signals_flutter: ^7.1.0 # UI @@ -38,8 +37,6 @@ dependencies: genui: ^0.9.2 googleai_dart: ^9.0.0 google_cloud_ai_generativelanguage_v1beta: ^0.5.2 - http: ^1.5.0 - gpt_markdown: ^1.1.4 dotprompt_dart: ^0.5.0 path: ^1.9.0 ack: ^1.2.0 @@ -53,17 +50,15 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - integration_test: - sdk: flutter flutter_lints: ^6.0.0 dart_code_metrics_presets: ^2.19.0 build_runner: ^2.5.4 ack_generator: ^1.2.0 + http: ^1.5.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 plugin_platform_interface: ^2.1.8 - golden_toolkit: ^0.15.0 image: ^4.8.0 flutter: diff --git a/packages/playground/test/core/data/data_sources/security_scoped_file_access_test.dart b/packages/playground/test/core/data/data_sources/security_scoped_file_access_test.dart index f21c3c71b..8944d5b53 100644 --- a/packages/playground/test/core/data/data_sources/security_scoped_file_access_test.dart +++ b/packages/playground/test/core/data/data_sources/security_scoped_file_access_test.dart @@ -5,7 +5,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:playground/core/data/data_sources/security_scoped_file_access.dart'; -import 'package:playground/core/data/data_sources/deck_file.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -18,25 +17,14 @@ void main() { TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler(channel, (call) async { calls.add(call); - if (call.method == 'startAccessing' && - call.arguments == 'created-directory-bookmark') { - return { - 'path': '/Users/test/Documents', - 'bookmark': 'refreshed-directory-bookmark', - }; - } return switch (call.method) { - 'pickDeckFile' => { - 'path': '/outside/talk.md', - 'bookmark': 'created-bookmark', - }, 'pickDecksDirectory' => { 'path': '/Users/test/Documents', 'bookmark': 'created-directory-bookmark', }, 'startAccessing' => { - 'path': '/moved/talk.md', - 'bookmark': 'refreshed-bookmark', + 'path': '/Users/test/Documents', + 'bookmark': 'refreshed-directory-bookmark', }, 'stopAccessing' => null, _ => throw MissingPluginException(call.method), @@ -51,48 +39,6 @@ void main() { calls.clear(); }); - test('creates, activates, refreshes, and releases a bookmark', () async { - const access = SecurityScopedFileAccess(); - final picked = await access.pickDeckFile(); - final activated = await access.startAccessing(picked!); - await access.stopAccessing(activated); - - expect( - picked, - const DeckFileReference( - path: '/outside/talk.md', - bookmark: 'created-bookmark', - ), - ); - expect( - activated, - const DeckFileReference( - path: '/moved/talk.md', - bookmark: 'refreshed-bookmark', - ), - ); - expect(calls.map((call) => call.method), [ - 'pickDeckFile', - 'startAccessing', - 'stopAccessing', - ]); - expect(calls.map((call) => call.arguments), [ - null, - 'created-bookmark', - 'refreshed-bookmark', - ]); - }); - - test('is a no-op for a directory-scoped file without a bookmark', () async { - const access = SecurityScopedFileAccess(); - const deck = DeckFileReference(path: '/app-storage/talk.md'); - - expect(await access.startAccessing(deck), deck); - await access.stopAccessing(deck); - - expect(calls, isEmpty); - }); - test('selects, activates, refreshes, and releases a directory', () async { const access = SecurityScopedFileAccess(); final picked = await access.pickDecksDirectory(); 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 5807b25c6..4eba731c4 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 @@ -42,18 +42,6 @@ custom: preserved expect(slide.comments, ['Speaker note']); }); - test('encodes with the canonical SlideSerializer', () { - final slides = [ - Slide( - key: 'transient', - options: SlideOptions(title: 'Canonical'), - sections: [SectionBlock.text('# Canonical')], - ), - ]; - - expect(codec.encode(slides), const SlideSerializer().serialize(slides)); - }); - test('throws a typed deck format error for invalid Markdown', () { const markdown = ''' --- @@ -135,8 +123,8 @@ title: "unclosed Body '''; - /// The editor and the builder run one parser, so both must agree on the - /// slide boundaries and on which text stayed content. + /// The preview loader and the builder run one parser, so both must agree + /// on the slide boundaries and on which text stayed content. void expectParity(String markdown) { final raw = const MarkdownParser().parse(markdown); final decoded = codec.decode(markdown); @@ -152,7 +140,6 @@ Body expect(slides, hasLength(3)); expect(slides.first.options?.args ?? {}, isEmpty); - expect(codec.encode(slides), contains('- First bullet')); expectParity(bulletList); }); @@ -160,7 +147,6 @@ Body final slides = codec.decode(proseAboveMapping); expect(slides, hasLength(1)); - expect(codec.encode(slides), contains('Key: value')); expectParity(proseAboveMapping); }); @@ -178,7 +164,6 @@ Body final slides = codec.decode(markedNote); expect(slides, hasLength(3)); - expect(codec.encode(slides), contains('Note: remember this')); expectParity(markedNote); }); @@ -201,37 +186,4 @@ Body ); }); }); - - test('decode-encode-decode preserves structural slide data', () { - const markdown = ''' ---- -title: Round trip -template: cover -custom: value ---- - -@section { - flex: 2 - align: center -} -@chart { - kind: bar - values: [1, 2, 3] -} - - -'''; - - final decoded = codec.decode(markdown); - final reparsed = codec.decode(codec.encode(decoded)); - - expect(_withoutKeys(reparsed), _withoutKeys(decoded)); - }); -} - -List> _withoutKeys(List slides) { - return [ - for (final slide in slides) - Map.from(slide.toJson())..remove('key'), - ]; } diff --git a/packages/playground/test/core/deck_customization_store_test.dart b/packages/playground/test/core/deck_customization_store_test.dart index 1510a4a5d..7a6ad0a48 100644 --- a/packages/playground/test/core/deck_customization_store_test.dart +++ b/packages/playground/test/core/deck_customization_store_test.dart @@ -31,55 +31,6 @@ void main() { expect(controller.options.value.baseStyle, isNotNull); }); - test('preserves the deck debug flag through theme updates', () { - final controller = DeckController( - deckLoader: MemoryDeckLoader(), - options: DeckOptions(debug: true), - ); - addTearDown(controller.dispose); - - final store = DeckCustomizationStore(controller); - addTearDown(store.dispose); - - expect(controller.options.value.debug, isTrue); - - store.setSize(TextLevel.h1, 64); - - expect(controller.options.value.debug, isTrue); - }); - - test('mutations push new options and notify once', () { - final controller = newController(); - addTearDown(controller.dispose); - final store = DeckCustomizationStore(controller); - addTearDown(store.dispose); - - final seeded = controller.options.value; - var notifications = 0; - store.addListener(() => notifications++); - - store.setSize(TextLevel.h1, 64); - - expect(store.level(TextLevel.h1).size, 64); - expect(notifications, 1); - expect(identical(controller.options.value, seeded), isFalse); - }); - - test('no-op mutation does not notify', () { - final controller = newController(); - addTearDown(controller.dispose); - final store = DeckCustomizationStore(controller); - addTearDown(store.dispose); - - final current = store.level(TextLevel.h1).size; - var notifications = 0; - store.addListener(() => notifications++); - - store.setSize(TextLevel.h1, current); - - expect(notifications, 0); - }); - test('applies a generated palette and font system in one update', () { final controller = newController(); addTearDown(controller.dispose); diff --git a/packages/playground/test/core/domain/stores/deck_document_store_test.dart b/packages/playground/test/core/domain/stores/deck_document_store_test.dart index b0a1f7bc0..6cde5fd54 100644 --- a/packages/playground/test/core/domain/stores/deck_document_store_test.dart +++ b/packages/playground/test/core/domain/stores/deck_document_store_test.dart @@ -31,20 +31,4 @@ void main() { expect(notifications, 0); }); - - test('counts only accepted replacements in its revision', () { - final store = DeckDocumentStore(markdown: '# Initial'); - addTearDown(store.dispose); - - expect(store.revision, 0); - - store.replaceMarkdown('# Updated'); - expect(store.revision, 1); - - store.replaceMarkdown('# Updated'); - expect(store.revision, 1); - - store.replaceMarkdown('# Initial'); - expect(store.revision, 2); - }); } diff --git a/packages/playground/test/core/presentation_theme_catalog_test.dart b/packages/playground/test/core/presentation_theme_catalog_test.dart index 4733b979a..40aeb96f6 100644 --- a/packages/playground/test/core/presentation_theme_catalog_test.dart +++ b/packages/playground/test/core/presentation_theme_catalog_test.dart @@ -3,6 +3,23 @@ import 'package:playground/core/domain/design/presentation_color_contrast.dart'; import 'package:playground/core/domain/design/presentation_theme_catalog.dart'; import 'package:playground/core/domain/design/presentation_typography_catalog.dart'; +/// Golden order for the twelve built-in themes, so a reorder or rename is +/// caught even though nothing in production reads it back. +const _expectedThemeIds = [ + 'editorial-midnight', + 'technical-paper', + 'bold-product', + 'warm-editorial', + 'nordic-air', + 'monochrome-grid', + 'signal-studio', + 'playful-learning', + 'data-noir', + 'civic-blueprint', + 'organic-sage', + 'retro-poster', +]; + void main() { group('PresentationThemeCatalog', () { test('ships twelve described, materially distinct versioned themes', () { @@ -11,7 +28,7 @@ void main() { expect(catalog.themes, hasLength(12)); expect( catalog.currentThemes.map((theme) => theme.id), - defaultPresentationThemeIds, + _expectedThemeIds, ); expect( catalog.themes.map((theme) => '${theme.id}@${theme.version}').toSet(), diff --git a/packages/playground/test/core/presentation_typography_catalog_test.dart b/packages/playground/test/core/presentation_typography_catalog_test.dart index 17cd4d57c..50e43eeac 100644 --- a/packages/playground/test/core/presentation_typography_catalog_test.dart +++ b/packages/playground/test/core/presentation_typography_catalog_test.dart @@ -9,7 +9,9 @@ void main() { expect(catalog.resolve('Playfair Display')?.id, 'playfair-display'); expect(catalog.resolve('definitely invented'), isNull); expect( - catalog.forRole(PresentationFontRole.body).map((font) => font.family), + catalog.fonts + .where((font) => font.roles.contains(PresentationFontRole.body)) + .map((font) => font.family), containsAll(['Inter', 'Source Serif 4', 'DM Sans']), ); }); diff --git a/packages/playground/test/core/utils/color_utils_test.dart b/packages/playground/test/core/utils/color_utils_test.dart deleted file mode 100644 index a222906b5..000000000 --- a/packages/playground/test/core/utils/color_utils_test.dart +++ /dev/null @@ -1,75 +0,0 @@ -import 'dart:ui'; - -import 'package:flutter_test/flutter_test.dart'; -import 'package:playground/core/utils/color_utils.dart'; - -void main() { - group('parseHexColor', () { - test('parses a 6-digit hex and assumes full opacity', () { - final result = parseHexColor('#FF5733'); - expect(result.isValid, isTrue); - expect(result.color, const Color(0xFFFF5733)); - }); - - test('parses without the leading #', () { - final result = parseHexColor('FF5733'); - expect(result.isValid, isTrue); - expect(result.color, const Color(0xFFFF5733)); - }); - - test('parses an 8-digit ARGB hex preserving alpha', () { - final result = parseHexColor('80FF5733'); - expect(result.isValid, isTrue); - expect(result.color, const Color(0x80FF5733)); - }); - - test('is case-insensitive', () { - expect(parseHexColor('ff5733').color, const Color(0xFFFF5733)); - }); - - test('returns fallback gray and isValid=false for bad input', () { - final result = parseHexColor('not-a-color'); - expect(result.isValid, isFalse); - expect(result.color, const Color(0xFF808080)); - }); - - test('rejects hex strings of the wrong length', () { - expect(parseHexColor('FFF').isValid, isFalse); - expect(parseHexColor('FF57330000').isValid, isFalse); - }); - - test('rejects an empty string', () { - expect(parseHexColor('').isValid, isFalse); - }); - }); - - group('hexToColor', () { - test('returns the parsed color', () { - expect(hexToColor('#0485F7'), const Color(0xFF0485F7)); - }); - - test('returns fallback gray for invalid input', () { - expect(hexToColor('invalid'), const Color(0xFF808080)); - }); - }); - - group('colorToHex', () { - test('formats an opaque color as #RRGGBB', () { - expect(colorToHex(const Color(0xFF0485F7)), '#0485F7'); - }); - - test('drops alpha', () { - expect(colorToHex(const Color(0x800485F7)), '#0485F7'); - }); - - test('uppercases the output', () { - expect(colorToHex(const Color(0xFFabcdef)), '#ABCDEF'); - }); - - test('round-trips through parseHexColor', () { - const color = Color(0xFF123456); - final roundTripped = parseHexColor(colorToHex(color)).color; - expect(roundTripped, color); - }); - }); -} diff --git a/packages/playground/test/features/ai/quick_agent/core/engine/prompts/composition_example_library_test.dart b/packages/playground/test/features/ai/generation/core/engine/prompts/composition_example_library_test.dart similarity index 93% rename from packages/playground/test/features/ai/quick_agent/core/engine/prompts/composition_example_library_test.dart rename to packages/playground/test/features/ai/generation/core/engine/prompts/composition_example_library_test.dart index 1d13953c9..ef74556fc 100644 --- a/packages/playground/test/features/ai/quick_agent/core/engine/prompts/composition_example_library_test.dart +++ b/packages/playground/test/features/ai/generation/core/engine/prompts/composition_example_library_test.dart @@ -1,8 +1,8 @@ import 'package:flutter_test/flutter_test.dart'; -import 'package:playground/features/ai/quick_agent/core/engine/prompts/composition_example_library.dart'; +import 'package:playground/features/ai/generation/core/engine/prompts/composition_example_library.dart'; import 'package:superdeck_core/superdeck_core.dart'; -import 'package:playground/features/ai/quick_agent/core/engine/schemas/outline_schema.dart'; -import 'package:playground/features/ai/quick_agent/core/engine/services/generation_element_catalog.dart'; +import 'package:playground/features/ai/generation/core/engine/schemas/outline_schema.dart'; +import 'package:playground/features/ai/generation/core/engine/services/generation_element_catalog.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); diff --git a/packages/playground/test/features/ai/quick_agent/core/engine/prompts/generation_prompt_provider_test.dart b/packages/playground/test/features/ai/generation/core/engine/prompts/generation_prompt_provider_test.dart similarity index 97% rename from packages/playground/test/features/ai/quick_agent/core/engine/prompts/generation_prompt_provider_test.dart rename to packages/playground/test/features/ai/generation/core/engine/prompts/generation_prompt_provider_test.dart index 9f26709bd..c98e8cd41 100644 --- a/packages/playground/test/features/ai/quick_agent/core/engine/prompts/generation_prompt_provider_test.dart +++ b/packages/playground/test/features/ai/generation/core/engine/prompts/generation_prompt_provider_test.dart @@ -2,10 +2,10 @@ import 'dart:convert'; import 'package:flutter_test/flutter_test.dart'; import 'package:playground/core/domain/design/presentation_theme_catalog.dart'; -import 'package:playground/features/ai/quick_agent/core/engine/prompts/generation_prompt_provider.dart'; -import 'package:playground/features/ai/quick_agent/core/engine/schemas/outline_schema.dart'; -import 'package:playground/features/ai/quick_agent/core/engine/services/generation_element_catalog.dart'; -import 'package:playground/features/ai/quick_agent/core/engine/services/generation_validation_issue.dart'; +import 'package:playground/features/ai/generation/core/engine/prompts/generation_prompt_provider.dart'; +import 'package:playground/features/ai/generation/core/engine/schemas/outline_schema.dart'; +import 'package:playground/features/ai/generation/core/engine/services/generation_element_catalog.dart'; +import 'package:playground/features/ai/generation/core/engine/services/generation_validation_issue.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); diff --git a/packages/playground/test/features/ai/quick_agent/core/engine/schemas/deck_schemas_test.dart b/packages/playground/test/features/ai/generation/core/engine/schemas/deck_schemas_test.dart similarity index 92% rename from packages/playground/test/features/ai/quick_agent/core/engine/schemas/deck_schemas_test.dart rename to packages/playground/test/features/ai/generation/core/engine/schemas/deck_schemas_test.dart index 49ceecb85..e33695e52 100644 --- a/packages/playground/test/features/ai/quick_agent/core/engine/schemas/deck_schemas_test.dart +++ b/packages/playground/test/features/ai/generation/core/engine/schemas/deck_schemas_test.dart @@ -4,9 +4,9 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:google_cloud_ai_generativelanguage_v1beta/generativelanguage.dart' as google_ai; import 'package:json_schema_builder/json_schema_builder.dart' as dsb; -import 'package:playground/features/ai/quick_agent/core/engine/services/google_schema_adapter.dart'; +import 'package:playground/features/ai/generation/core/engine/services/google_schema_adapter.dart'; import 'package:superdeck_core/superdeck_core.dart' - show Slide, aiSlideSchema, buildAiSlideSchema; + show Slide, buildAiSlideSchema; Map validSlide({ Map? block, @@ -195,7 +195,7 @@ void main() { }); }); - group('aiSlideSchema parity with the canonical contract', () { + group('buildAiSlideSchema() parity with the canonical contract', () { test( 'valid layout data passes the projection and the canonical parser', () { @@ -210,7 +210,7 @@ void main() { }, ); - expect(aiSlideSchema.safeParse(slide).isOk, isTrue); + expect(buildAiSlideSchema().safeParse(slide).isOk, isTrue); final parsed = Slide.parse(slide); final block = parsed.sections.single.blocks.single; expect(block.margin?.top, 8); @@ -244,12 +244,16 @@ void main() { test('rejects non-positive flex like core', () { for (final flex in [0, -1]) { expect( - aiSlideSchema.safeParse(validSlide(block: {'flex': flex})).isOk, + buildAiSlideSchema() + .safeParse(validSlide(block: {'flex': flex})) + .isOk, isFalse, reason: 'block flex: $flex', ); expect( - aiSlideSchema.safeParse(validSlide(section: {'flex': flex})).isOk, + buildAiSlideSchema() + .safeParse(validSlide(section: {'flex': flex})) + .isOk, isFalse, reason: 'section flex: $flex', ); @@ -258,11 +262,13 @@ void main() { test('spacing is section-only like core', () { expect( - aiSlideSchema.safeParse(validSlide(section: {'spacing': 40})).isOk, + buildAiSlideSchema() + .safeParse(validSlide(section: {'spacing': 40})) + .isOk, isTrue, ); expect( - aiSlideSchema.safeParse(validSlide(block: {'spacing': 40})).isOk, + buildAiSlideSchema().safeParse(validSlide(block: {'spacing': 40})).isOk, isFalse, ); }); @@ -273,7 +279,7 @@ void main() { {'horizontal': 24}, ]) { expect( - aiSlideSchema + buildAiSlideSchema() .safeParse(validSlide(block: {'padding': shorthand})) .isOk, isFalse, @@ -289,7 +295,8 @@ void main() { test('block surface matches the core reserved widget fields', () { final blockProperties = - ((((aiSlideSchema.toJsonSchema()['properties'] as Map)['sections'] + ((((buildAiSlideSchema().toJsonSchema()['properties'] + as Map)['sections'] as Map)['items'] as Map)['properties'] as Map)['blocks'] diff --git a/packages/playground/test/features/ai/quick_agent/core/engine/schemas/outline_schema_test.dart b/packages/playground/test/features/ai/generation/core/engine/schemas/outline_schema_test.dart similarity index 92% rename from packages/playground/test/features/ai/quick_agent/core/engine/schemas/outline_schema_test.dart rename to packages/playground/test/features/ai/generation/core/engine/schemas/outline_schema_test.dart index 94055b48f..f6e0740da 100644 --- a/packages/playground/test/features/ai/quick_agent/core/engine/schemas/outline_schema_test.dart +++ b/packages/playground/test/features/ai/generation/core/engine/schemas/outline_schema_test.dart @@ -1,11 +1,11 @@ import 'package:ack_json_schema_builder/ack_json_schema_builder.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:playground/core/domain/design/presentation_typography_catalog.dart'; -import 'package:playground/features/ai/quick_agent/core/engine/schemas/outline_schema.dart'; -import 'package:playground/features/ai/quick_agent/core/engine/services/deck_generation_request.dart'; -import 'package:playground/features/ai/quick_agent/core/engine/services/deck_plan_validator.dart'; -import 'package:playground/features/ai/quick_agent/core/engine/services/google_schema_adapter.dart'; -import 'package:playground/features/ai/quick_agent/core/engine/services/generation_validation_issue.dart'; +import 'package:playground/features/ai/generation/core/engine/schemas/outline_schema.dart'; +import 'package:playground/features/ai/generation/core/engine/services/deck_generation_request.dart'; +import 'package:playground/features/ai/generation/core/engine/services/deck_plan_validator.dart'; +import 'package:playground/features/ai/generation/core/engine/services/google_schema_adapter.dart'; +import 'package:playground/features/ai/generation/core/engine/services/generation_validation_issue.dart'; void main() { test('accepts a theme-referenced plan with narrative and element intent', () { @@ -169,7 +169,7 @@ void main() { }); expect( - validateDeckPlan(plan), + validateDeckPlanIssues(plan).messages, containsAll([ contains('Duplicate slide key "same"'), contains('Slide 3 has an empty key'), @@ -195,7 +195,7 @@ void main() { }); expect( - validateDeckPlan(plan, expectedSlideCount: 3), + validateDeckPlanIssues(plan, expectedSlideCount: 3).messages, contains('Deck plan has 2 slides; expected exactly 3.'), ); }); @@ -209,11 +209,11 @@ void main() { 'action', ]); expect( - validateDeckPlan( + validateDeckPlanIssues( plan, expectedSlideCount: 10, typographyCatalog: PresentationTypographyCatalog.withDefaults(), - ), + ).messages, isEmpty, ); }); @@ -223,7 +223,7 @@ void main() { final plan = DeckPlan.parse(_scaledHierarchicalPlan(count)); expect( - validateDeckPlan(plan, expectedSlideCount: count), + validateDeckPlanIssues(plan, expectedSlideCount: count).messages, isEmpty, reason: '$count-slide blueprint should satisfy the large-deck contract', ); @@ -248,7 +248,7 @@ void main() { slides[index]['treatment'] = index < 4 ? 'content' : 'data'; } - final errors = validateDeckPlan(DeckPlan.parse(data)); + final errors = validateDeckPlanIssues(DeckPlan.parse(data)).messages; expect(errors.where((error) => error.contains('treatment')), isEmpty); }); @@ -292,7 +292,7 @@ void main() { finalSectionKeys.removeLast(); final plan = DeckPlan.parse(data); - final errors = validateDeckPlan(plan); + final errors = validateDeckPlanIssues(plan).messages; expect( errors, @@ -306,7 +306,7 @@ void main() { test('rejects changes to exact requested theme and font selections', () { final plan = DeckPlan.parse(_hierarchicalPlan()); - final errors = validateDeckPlan( + final errors = validateDeckPlanIssues( plan, request: const DeckGenerationRequest( userIntent: 'Explain reliable delivery.', @@ -314,7 +314,7 @@ void main() { themeId: 'technical-paper', headlineFont: 'Lora', ), - ); + ).messages; expect( errors, @@ -333,7 +333,7 @@ void main() { final plan = DeckPlan.parse(data); expect( - validateDeckPlan(plan), + validateDeckPlanIssues(plan).messages, contains( 'Slide "cost" uses composition "imageRight" but does not plan ' 'the required image element.', @@ -356,7 +356,7 @@ void main() { final plan = DeckPlan.parse(data); expect( - validateDeckPlan(plan), + validateDeckPlanIssues(plan).messages, contains( 'Slide "cost" plans an image element but uses incompatible ' 'composition "content".', @@ -380,7 +380,7 @@ void main() { ]; final plan = DeckPlan.parse(data); - final errors = validateDeckPlan( + final errors = validateDeckPlanIssues( plan, request: const DeckGenerationRequest( userIntent: 'Explain reliable delivery.', @@ -388,7 +388,7 @@ void main() { imageStyleId: 'minimalist', imageStyleVersion: 1, ), - ); + ).messages; expect(plan.slides[1].elements!.single.generationPrompt, isNotEmpty); expect( @@ -419,14 +419,14 @@ void main() { } final plan = DeckPlan.parse(data); - final withoutStyle = validateDeckPlan( + final withoutStyle = validateDeckPlanIssues( plan, request: const DeckGenerationRequest( userIntent: 'Explain reliable delivery.', slideCount: 10, ), - ); - final overBudget = validateDeckPlan( + ).messages; + final overBudget = validateDeckPlanIssues( plan, request: const DeckGenerationRequest( userIntent: 'Explain reliable delivery.', @@ -434,7 +434,7 @@ void main() { imageStyleId: 'watercolor', imageStyleVersion: 1, ), - ); + ).messages; expect( withoutStyle, @@ -454,14 +454,14 @@ void main() { slides.last['contentUnits'] = ['Continue at signal-canvas.com/launch']; final plan = DeckPlan.parse(data); - final rejected = validateDeckPlan( + final rejected = validateDeckPlanIssues( plan, request: const DeckGenerationRequest( userIntent: 'Create a ten-slide product narrative without URLs.', slideCount: 10, ), - ); - final allowed = validateDeckPlan( + ).messages; + final allowed = validateDeckPlanIssues( plan, request: const DeckGenerationRequest( userIntent: 'Create a ten-slide product narrative.', @@ -474,7 +474,7 @@ void main() { ), ], ), - ); + ).messages; expect( rejected, @@ -496,24 +496,24 @@ void main() { slides.last['contentUnits'] = ['100% retention after 90 days']; final unqualified = DeckPlan.parse(data); - final rejected = validateDeckPlan( + final rejected = validateDeckPlanIssues( unqualified, request: const DeckGenerationRequest( userIntent: 'Create a ten-slide story using the supplied 42% result.', slideCount: 10, ), - ); + ).messages; slides.last['contentUnits'] = [ 'Projected scenario: 100% retention after an estimated 90 days', ]; final qualified = DeckPlan.parse(data); - final allowed = validateDeckPlan( + final allowed = validateDeckPlanIssues( qualified, request: const DeckGenerationRequest( userIntent: 'Create a ten-slide story using the supplied 42% result.', slideCount: 10, ), - ); + ).messages; expect( rejected, @@ -551,27 +551,27 @@ void main() { slides.last['contentUnits'] = [ 'Planned target: 50% adoption after the proposed 90-day pilot', ]; - final plannedTarget = validateDeckPlan( + final plannedTarget = validateDeckPlanIssues( DeckPlan.parse(data), request: const DeckGenerationRequest( userIntent: 'Describe a future pilot without claiming observed results.', slideCount: 10, ), - ); + ).messages; expect( plannedTarget.where((error) => error.contains('uses numeric claim(s)')), isEmpty, ); slides.last['contentUnits'] = ['6 design partners']; - final normalizedNumberWords = validateDeckPlan( + final normalizedNumberWords = validateDeckPlanIssues( DeckPlan.parse(data), request: const DeckGenerationRequest( userIntent: 'Describe the six design partners.', slideCount: 10, ), - ); + ).messages; expect( normalizedNumberWords.where( (error) => error.contains('uses numeric claim(s)'), @@ -582,13 +582,13 @@ void main() { slides.last['contentUnits'] = [ '3 onboarding steps in Section 3 with no change to source systems', ]; - final structuralCounts = validateDeckPlan( + final structuralCounts = validateDeckPlanIssues( DeckPlan.parse(data), request: const DeckGenerationRequest( userIntent: 'Describe onboarding with no change to source systems.', slideCount: 10, ), - ); + ).messages; expect( structuralCounts.where( (error) => error.contains('uses numeric claim(s)'), @@ -596,13 +596,13 @@ void main() { isEmpty, ); slides.last['contentUnits'] = ['0% disruption to source systems']; - final inventedZero = validateDeckPlan( + final inventedZero = validateDeckPlanIssues( DeckPlan.parse(data), request: const DeckGenerationRequest( userIntent: 'Describe onboarding with no change to source systems.', slideCount: 10, ), - ); + ).messages; expect( inventedZero.singleWhere( (error) => error.contains('uses numeric claim(s)'), @@ -613,37 +613,37 @@ void main() { ), ); slides.last['contentUnits'] = ['A zero-friction onboarding workflow']; - final qualitativeZero = validateDeckPlan( + final qualitativeZero = validateDeckPlanIssues( DeckPlan.parse(data), request: const DeckGenerationRequest( userIntent: 'Describe a smooth onboarding workflow.', slideCount: 10, ), - ); + ).messages; expect( qualitativeZero.where((error) => error.contains('uses numeric claim(s)')), isEmpty, ); slides.last['contentUnits'] = ['Planned direction for next quarter']; - final calendarQuarter = validateDeckPlan( + final calendarQuarter = validateDeckPlanIssues( DeckPlan.parse(data), request: const DeckGenerationRequest( userIntent: 'Describe a roadmap direction.', slideCount: 10, ), - ); + ).messages; expect( calendarQuarter.where((error) => error.contains('uses numeric claim(s)')), isEmpty, ); slides.last['contentUnits'] = ['A quarter of teams changed direction']; - final fractionalQuarter = validateDeckPlan( + final fractionalQuarter = validateDeckPlanIssues( DeckPlan.parse(data), request: const DeckGenerationRequest( userIntent: 'Describe a roadmap direction.', slideCount: 10, ), - ); + ).messages; expect( fractionalQuarter.singleWhere( (error) => error.contains('uses numeric claim(s)'), @@ -651,23 +651,23 @@ void main() { contains('numeric claim(s) 25%'), ); slides.last['contentUnits'] = ['Setup completes in one afternoon']; - final structuralSourceDoesNotGroundDuration = validateDeckPlan( + final structuralSourceDoesNotGroundDuration = validateDeckPlanIssues( DeckPlan.parse(data), request: const DeckGenerationRequest( userIntent: 'Bring the evidence into one workspace.', slideCount: 10, ), - ); + ).messages; slides.last['contentUnits'] = [ 'Illustrative scenario: initial insight in one afternoon', ]; - final qualifiedDuration = validateDeckPlan( + final qualifiedDuration = validateDeckPlanIssues( DeckPlan.parse(data), request: const DeckGenerationRequest( userIntent: 'Bring the evidence into one workspace.', slideCount: 10, ), - ); + ).messages; expect( structuralSourceDoesNotGroundDuration.singleWhere( (error) => error.contains('uses numeric claim(s)'), @@ -682,13 +682,13 @@ void main() { ); slides.last['contentUnits'] = ['1 beta outcome']; - final structuralSourceDoesNotGroundMetric = validateDeckPlan( + final structuralSourceDoesNotGroundMetric = validateDeckPlanIssues( DeckPlan.parse(data), request: const DeckGenerationRequest( userIntent: 'Turn fragmented evidence into one workspace.', slideCount: 10, ), - ); + ).messages; expect( structuralSourceDoesNotGroundMetric.singleWhere( (error) => error.contains('uses numeric claim(s)'), @@ -731,13 +731,13 @@ void main() { final plan = DeckPlan.parse(data); expect( - validateDeckPlan( + validateDeckPlanIssues( plan, request: const DeckGenerationRequest( userIntent: 'Invite the audience to learn more.', slideCount: 10, ), - ), + ).messages, contains( 'Slide "close" introduces unsupported commitment claim(s): free ' 'trial. Remove them unless userIntent supplied the exact claim.', @@ -745,13 +745,13 @@ void main() { ); slides.last['contentUnits'] = ['SOC2 Compliant governance']; expect( - validateDeckPlan( + validateDeckPlanIssues( DeckPlan.parse(data), request: const DeckGenerationRequest( userIntent: 'Describe the governance model.', slideCount: 10, ), - ).singleWhere((error) => error.contains('soc2')), + ).messages.singleWhere((error) => error.contains('soc2')), allOf(contains('unsupported commitment claim(s)'), contains('soc2')), ); }); @@ -794,13 +794,13 @@ void main() { 'Enterprise-grade security validated with one-click setup', ]; - final errors = validateDeckPlan( + final errors = validateDeckPlanIssues( DeckPlan.parse(data), request: const DeckGenerationRequest( userIntent: 'Describe a fictional product beta and adoption options.', slideCount: 10, ), - ); + ).messages; final commitmentError = errors.singleWhere( (error) => error.contains('unsupported commitment claim(s)'), ); @@ -819,25 +819,25 @@ void main() { } slides.last['contentUnits'] = ['Omitting real-time claims from this plan']; - final negated = validateDeckPlan( + final negated = validateDeckPlanIssues( DeckPlan.parse(data), request: const DeckGenerationRequest( userIntent: 'Describe a fictional product beta.', slideCount: 10, ), - ); + ).messages; expect(negated.where((error) => error.contains('real-time')), isEmpty); slides.last['contentUnits'] = [ 'Illustrative option: SSO, data residency, and automated workflows', ]; - final explicitlyHypothetical = validateDeckPlan( + final explicitlyHypothetical = validateDeckPlanIssues( DeckPlan.parse(data), request: const DeckGenerationRequest( userIntent: 'Describe possible enterprise governance directions.', slideCount: 10, ), - ); + ).messages; expect( explicitlyHypothetical.where( (error) => error.contains('unsupported commitment claim(s)'), @@ -861,7 +861,7 @@ void main() { 'Technical documentation and launch team access', ]; - final errors = validateDeckPlan( + final errors = validateDeckPlanIssues( DeckPlan.parse(data), request: const DeckGenerationRequest( userIntent: @@ -870,7 +870,7 @@ void main() { 'time.', slideCount: 10, ), - ); + ).messages; final error = errors.singleWhere( (candidate) => candidate.contains('unsupported commitment claim(s)'), ); @@ -915,7 +915,7 @@ void main() { 'Frame the context caused by disconnected evidence sources.'; sections[1]['purpose'] = 'Introduce observations from six design partners.'; - final errors = validateDeckPlan( + final errors = validateDeckPlanIssues( DeckPlan.parse(data), request: const DeckGenerationRequest( userIntent: @@ -923,7 +923,7 @@ void main() { 'partners.', slideCount: 10, ), - ); + ).messages; expect(errors.where((error) => error.contains('caused by')), isEmpty); }); @@ -939,7 +939,7 @@ void main() { 'Exclusive launch event access', ]; - final rejected = validateDeckPlan( + final rejected = validateDeckPlanIssues( DeckPlan.parse(data), request: const DeckGenerationRequest( userIntent: @@ -947,7 +947,7 @@ void main() { 'shape, and roadmap for a fictional beta product.', slideCount: 10, ), - ); + ).messages; final error = rejected.singleWhere( (candidate) => candidate.contains('unsupported commitment claim(s)'), ); @@ -969,7 +969,7 @@ void main() { 'Proposed tier — Free: Core inbox and triage', 'Proposed tier — Pro: Advanced linked insights', ]; - final qualified = validateDeckPlan( + final qualified = validateDeckPlanIssues( DeckPlan.parse(data), request: const DeckGenerationRequest( userIntent: @@ -977,7 +977,7 @@ void main() { 'shape, and roadmap for a fictional beta product.', slideCount: 10, ), - ); + ).messages; expect( qualified.where( (candidate) => candidate.contains('unsupported commitment claim(s)'), @@ -997,7 +997,7 @@ void main() { 'Data integrity', ]; - final errors = validateDeckPlan( + final errors = validateDeckPlanIssues( DeckPlan.parse(data), request: const DeckGenerationRequest( userIntent: @@ -1005,7 +1005,7 @@ void main() { 'integrity without inventing implementation details.', slideCount: 10, ), - ); + ).messages; expect( errors.where( @@ -1056,20 +1056,20 @@ void main() { final slides = data['slides']! as List>; slides.last['contentUnits'] = ['38+ design partners']; - final exactOnly = validateDeckPlan( + final exactOnly = validateDeckPlanIssues( DeckPlan.parse(data), request: const DeckGenerationRequest( userIntent: 'The beta included 38 design partners.', slideCount: 10, ), - ); - final explicitlyOpenEnded = validateDeckPlan( + ).messages; + final explicitlyOpenEnded = validateDeckPlanIssues( DeckPlan.parse(data), request: const DeckGenerationRequest( userIntent: 'The beta included 38+ design partners.', slideCount: 10, ), - ); + ).messages; expect( exactOnly.singleWhere( @@ -1089,13 +1089,13 @@ void main() { final data = _hierarchicalPlan(); final slides = data['slides']! as List>; slides.last['contentUnits'] = ['Reclaiming 42% of the work week']; - final changedMeaning = validateDeckPlan( + final changedMeaning = validateDeckPlanIssues( DeckPlan.parse(data), request: const DeckGenerationRequest( userIntent: 'Teams spent 42% less weekly synthesis time.', slideCount: 10, ), - ); + ).messages; final changedMeaningIssues = validateDeckPlanIssues( DeckPlan.parse(data), request: const DeckGenerationRequest( @@ -1116,27 +1116,27 @@ void main() { ); expect(changedMeaningIssues.where((issue) => issue.isBlocking), isEmpty); slides.last['contentUnits'] = ['42% less weekly synthesis time']; - final preservedMeaning = validateDeckPlan( + final preservedMeaning = validateDeckPlanIssues( DeckPlan.parse(data), request: const DeckGenerationRequest( userIntent: 'Teams spent 42% less weekly synthesis time.', slideCount: 10, ), - ); + ).messages; slides.last['title'] = '42% productivity gain'; slides.last['contentUnits'] = ['Less weekly synthesis time']; - final standaloneMetric = validateDeckPlan( + final standaloneMetric = validateDeckPlanIssues( DeckPlan.parse(data), request: const DeckGenerationRequest( userIntent: 'Teams spent 42% less weekly synthesis time.', slideCount: 10, ), - ); + ).messages; slides.last['contentUnits'] = [ '19% decision velocity in the experiment', 'No change to source systems', ]; - final normalizedWording = validateDeckPlan( + final normalizedWording = validateDeckPlanIssues( DeckPlan.parse(data), request: const DeckGenerationRequest( userIntent: @@ -1144,20 +1144,20 @@ void main() { 'change to source-of-truth systems.', slideCount: 10, ), - ); + ).messages; slides.last['title'] = 'Beta cohort'; slides.last['contentUnits'] = [ 'One unified view', 'Six enterprise beta cohort', ]; - final changedCohortMeaning = validateDeckPlan( + final changedCohortMeaning = validateDeckPlanIssues( DeckPlan.parse(data), request: const DeckGenerationRequest( userIntent: 'Create one evidence workspace validated by six design partners.', slideCount: 10, ), - ); + ).messages; expect( changedMeaning, @@ -1223,13 +1223,13 @@ void main() { final plan = DeckPlan.parse(_hierarchicalPlan()); expect( - validateDeckPlan( + validateDeckPlanIssues( plan, request: const DeckGenerationRequest( userIntent: 'Teams spent 42% less weekly synthesis time.', slideCount: 10, ), - ), + ).messages, contains( 'Slide "signal" selects composition "metric" without an explicit ' 'audience-facing numeric fact. Put the exact grounded value and what ' diff --git a/packages/playground/test/features/ai/quick_agent/core/engine/schemas/theme_plan_contract_test.dart b/packages/playground/test/features/ai/generation/core/engine/schemas/theme_plan_contract_test.dart similarity index 92% rename from packages/playground/test/features/ai/quick_agent/core/engine/schemas/theme_plan_contract_test.dart rename to packages/playground/test/features/ai/generation/core/engine/schemas/theme_plan_contract_test.dart index 812cfa297..307587a62 100644 --- a/packages/playground/test/features/ai/quick_agent/core/engine/schemas/theme_plan_contract_test.dart +++ b/packages/playground/test/features/ai/generation/core/engine/schemas/theme_plan_contract_test.dart @@ -4,10 +4,10 @@ import 'dart:io'; import 'package:flutter_test/flutter_test.dart'; import 'package:playground/core/domain/design/presentation_theme_catalog.dart'; import 'package:playground/core/domain/design/presentation_typography_catalog.dart'; -import 'package:playground/features/ai/quick_agent/core/engine/schemas/deck_schemas.dart'; -import 'package:playground/features/ai/quick_agent/core/engine/schemas/outline_schema.dart'; -import 'package:playground/features/ai/quick_agent/core/engine/services/deck_theme_resolution.dart'; -import 'package:playground/features/ai/quick_agent/core/engine/services/theme_json_serializer.dart'; +import 'package:playground/features/ai/generation/core/engine/schemas/deck_schemas.dart'; +import 'package:playground/features/ai/generation/core/engine/schemas/outline_schema.dart'; +import 'package:playground/features/ai/generation/core/engine/services/deck_theme_resolution.dart'; +import 'package:playground/features/ai/generation/core/engine/services/theme_json_serializer.dart'; void main() { test('model draft accepts only an eligible theme ID', () { diff --git a/packages/playground/test/features/ai/quick_agent/core/engine/services/deck_generation_request_test.dart b/packages/playground/test/features/ai/generation/core/engine/services/deck_generation_request_test.dart similarity index 97% rename from packages/playground/test/features/ai/quick_agent/core/engine/services/deck_generation_request_test.dart rename to packages/playground/test/features/ai/generation/core/engine/services/deck_generation_request_test.dart index 3b14036f8..7484d2b3a 100644 --- a/packages/playground/test/features/ai/quick_agent/core/engine/services/deck_generation_request_test.dart +++ b/packages/playground/test/features/ai/generation/core/engine/services/deck_generation_request_test.dart @@ -1,7 +1,7 @@ import 'dart:convert'; import 'package:flutter_test/flutter_test.dart'; -import 'package:playground/features/ai/quick_agent/core/engine/services/deck_generation_request.dart'; +import 'package:playground/features/ai/generation/core/engine/services/deck_generation_request.dart'; import 'package:playground/features/ai/wizard/core/ai/services/prompt_builder.dart'; import 'package:playground/features/ai/wizard/core/ai/wizard_context.dart'; diff --git a/packages/playground/test/features/ai/quick_agent/core/engine/services/deck_generator_images_test.dart b/packages/playground/test/features/ai/generation/core/engine/services/deck_generator_images_test.dart similarity index 96% rename from packages/playground/test/features/ai/quick_agent/core/engine/services/deck_generator_images_test.dart rename to packages/playground/test/features/ai/generation/core/engine/services/deck_generator_images_test.dart index e5792d9fa..31e4af7c3 100644 --- a/packages/playground/test/features/ai/quick_agent/core/engine/services/deck_generator_images_test.dart +++ b/packages/playground/test/features/ai/generation/core/engine/services/deck_generator_images_test.dart @@ -3,8 +3,8 @@ import 'dart:typed_data'; import 'package:flutter_test/flutter_test.dart'; import 'package:playground/core/domain/design/presentation_image_style_catalog.dart'; import 'package:playground/features/ai/image_generation/image_generator.dart'; -import 'package:playground/features/ai/quick_agent/core/engine/schemas/outline_schema.dart'; -import 'package:playground/features/ai/quick_agent/core/engine/services/deck_generator_service.dart'; +import 'package:playground/features/ai/generation/core/engine/schemas/outline_schema.dart'; +import 'package:playground/features/ai/generation/core/engine/services/deck_generator_service.dart'; void main() { test( diff --git a/packages/playground/test/features/ai/quick_agent/core/engine/services/deck_generator_pipeline_helpers_test.dart b/packages/playground/test/features/ai/generation/core/engine/services/deck_generator_pipeline_helpers_test.dart similarity index 96% rename from packages/playground/test/features/ai/quick_agent/core/engine/services/deck_generator_pipeline_helpers_test.dart rename to packages/playground/test/features/ai/generation/core/engine/services/deck_generator_pipeline_helpers_test.dart index 55d624663..86602f812 100644 --- a/packages/playground/test/features/ai/quick_agent/core/engine/services/deck_generator_pipeline_helpers_test.dart +++ b/packages/playground/test/features/ai/generation/core/engine/services/deck_generator_pipeline_helpers_test.dart @@ -1,5 +1,5 @@ import 'package:flutter_test/flutter_test.dart'; -import 'package:playground/features/ai/quick_agent/core/engine/services/deck_generator_pipeline_helpers.dart'; +import 'package:playground/features/ai/generation/core/engine/services/deck_generator_pipeline_helpers.dart'; import 'package:superdeck_core/superdeck_core.dart' show Slide; void main() { diff --git a/packages/playground/test/features/ai/quick_agent/core/engine/services/deck_generator_service_test.dart b/packages/playground/test/features/ai/generation/core/engine/services/deck_generator_service_test.dart similarity index 98% rename from packages/playground/test/features/ai/quick_agent/core/engine/services/deck_generator_service_test.dart rename to packages/playground/test/features/ai/generation/core/engine/services/deck_generator_service_test.dart index 7b76bfc16..1c61aea54 100644 --- a/packages/playground/test/features/ai/quick_agent/core/engine/services/deck_generator_service_test.dart +++ b/packages/playground/test/features/ai/generation/core/engine/services/deck_generator_service_test.dart @@ -8,14 +8,14 @@ import 'package:google_cloud_ai_generativelanguage_v1beta/generativelanguage.dar 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'; -import 'package:playground/features/ai/quick_agent/core/engine/services/deck_generator_service.dart'; -import 'package:playground/features/ai/quick_agent/core/engine/services/deck_theme_resolution.dart'; -import 'package:playground/features/ai/quick_agent/core/engine/services/generation_model_client.dart'; -import 'package:playground/features/ai/quick_agent/core/engine/services/generation_progress.dart'; -import 'package:playground/features/ai/quick_agent/core/engine/services/generation_trace.dart'; -import 'package:playground/features/ai/quick_agent/core/engine/services/generation_validation_issue.dart'; -import 'package:playground/features/ai/quick_agent/core/engine/services/retry_policy.dart'; +import 'package:playground/features/ai/generation/core/engine/services/deck_generation_request.dart'; +import 'package:playground/features/ai/generation/core/engine/services/deck_generator_service.dart'; +import 'package:playground/features/ai/generation/core/engine/services/deck_theme_resolution.dart'; +import 'package:playground/features/ai/generation/core/engine/services/generation_model_client.dart'; +import 'package:playground/features/ai/generation/core/engine/services/generation_progress.dart'; +import 'package:playground/features/ai/generation/core/engine/services/generation_trace.dart'; +import 'package:playground/features/ai/generation/core/engine/services/generation_validation_issue.dart'; +import 'package:playground/features/ai/generation/core/engine/services/retry_policy.dart'; import 'package:superdeck_core/superdeck_core.dart'; void main() { @@ -724,7 +724,10 @@ void main() { ]); }); - for (final themeId in defaultPresentationThemeIds) { + for (final themeId + in PresentationThemeCatalog.withDefaults().currentThemes.map( + (theme) => theme.id, + )) { test('resolves $themeId through plan, slide, and runtime output', () async { final client = _FakeGenerationModelClient([ _jsonResponse({ diff --git a/packages/playground/test/features/ai/quick_agent/core/engine/services/error_classifier_test.dart b/packages/playground/test/features/ai/generation/core/engine/services/error_classifier_test.dart similarity index 97% rename from packages/playground/test/features/ai/quick_agent/core/engine/services/error_classifier_test.dart rename to packages/playground/test/features/ai/generation/core/engine/services/error_classifier_test.dart index ad069e6ab..9474b28c5 100644 --- a/packages/playground/test/features/ai/quick_agent/core/engine/services/error_classifier_test.dart +++ b/packages/playground/test/features/ai/generation/core/engine/services/error_classifier_test.dart @@ -1,6 +1,6 @@ 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'; +import 'package:playground/features/ai/generation/core/engine/services/error_classifier.dart'; void main() { const classifier = ErrorClassifier(); diff --git a/packages/playground/test/features/ai/quick_agent/core/engine/services/generated_slide_validator_test.dart b/packages/playground/test/features/ai/generation/core/engine/services/generated_slide_validator_test.dart similarity index 93% rename from packages/playground/test/features/ai/quick_agent/core/engine/services/generated_slide_validator_test.dart rename to packages/playground/test/features/ai/generation/core/engine/services/generated_slide_validator_test.dart index e255672d2..1ea9e9acd 100644 --- a/packages/playground/test/features/ai/quick_agent/core/engine/services/generated_slide_validator_test.dart +++ b/packages/playground/test/features/ai/generation/core/engine/services/generated_slide_validator_test.dart @@ -1,10 +1,10 @@ import 'package:flutter_test/flutter_test.dart'; -import 'package:playground/features/ai/quick_agent/core/engine/services/deck_generation_request.dart'; -import 'package:playground/features/ai/quick_agent/core/engine/services/deck_generator_pipeline_helpers.dart'; -import 'package:playground/features/ai/quick_agent/core/engine/services/generated_slide_validator.dart'; -import 'package:playground/features/ai/quick_agent/core/engine/services/generation_element_catalog.dart'; -import 'package:playground/features/ai/quick_agent/core/engine/services/generation_validation_issue.dart'; -import 'package:playground/features/ai/quick_agent/core/engine/schemas/outline_schema.dart'; +import 'package:playground/features/ai/generation/core/engine/services/deck_generation_request.dart'; +import 'package:playground/features/ai/generation/core/engine/services/deck_generator_pipeline_helpers.dart'; +import 'package:playground/features/ai/generation/core/engine/services/generated_slide_validator.dart'; +import 'package:playground/features/ai/generation/core/engine/services/generation_element_catalog.dart'; +import 'package:playground/features/ai/generation/core/engine/services/generation_validation_issue.dart'; +import 'package:playground/features/ai/generation/core/engine/schemas/outline_schema.dart'; import 'package:superdeck_core/superdeck_core.dart'; void main() { @@ -16,11 +16,11 @@ void main() { 'flex': 2, }); - final errors = validateGeneratedSlide( + final errors = validateGeneratedSlideIssues( expectedKey: 'test-slide', rawSlide: slide, elementCatalog: GenerationElementCatalog.builtIn(), - ); + ).messages; expect(errors, isEmpty); final normalized = sanitizeGeneratedSlides([slide]).single; @@ -31,7 +31,7 @@ void main() { }); test('reports widget arguments copied from another catalog entry', () { - final errors = validateGeneratedSlide( + final errors = validateGeneratedSlideIssues( expectedKey: 'test-slide', rawSlide: _slideWithBlock({ 'type': 'widget', @@ -43,7 +43,7 @@ void main() { }, }), elementCatalog: GenerationElementCatalog.builtIn(), - ); + ).messages; expect(errors.single, contains('Remove unsupported arguments: text, url')); expect(errors.single, contains('Allowed arguments:')); @@ -51,19 +51,19 @@ void main() { test('rejects unregistered widgets and invalid built-in arguments', () { expect( - validateGeneratedSlide( + validateGeneratedSlideIssues( expectedKey: 'test-slide', rawSlide: _slideWithBlock({'type': 'widget', 'name': 'unknown-chart'}), elementCatalog: GenerationElementCatalog.builtIn(), - ), + ).messages, contains('Widget "unknown-chart" is not registered for generation.'), ); expect( - validateGeneratedSlide( + validateGeneratedSlideIssues( expectedKey: 'test-slide', rawSlide: _slideWithBlock({'type': 'widget', 'name': 'webview'}), elementCatalog: GenerationElementCatalog.builtIn(), - ).single, + ).messages.single, contains('invalid arguments'), ); }); @@ -80,7 +80,7 @@ void main() { ], ); - final errors = validateGeneratedSlide( + final errors = validateGeneratedSlideIssues( expectedKey: 'test-slide', rawSlide: _slideWithBlock({ 'type': 'widget', @@ -88,7 +88,7 @@ void main() { 'value': 42, }), elementCatalog: catalog, - ); + ).messages; expect(errors, isEmpty); expect(catalog.formatForPrompt(), contains('metric-card')); @@ -96,7 +96,7 @@ void main() { }); test('rejects inconsistent or overly dense Markdown tables', () { - final errors = validateGeneratedSlide( + final errors = validateGeneratedSlideIssues( expectedKey: 'test-slide', rawSlide: _slideWithBlock({ 'type': 'block', @@ -106,7 +106,7 @@ void main() { '| one | two |\n', }), elementCatalog: GenerationElementCatalog.builtIn(), - ); + ).messages; expect( errors, @@ -119,7 +119,7 @@ void main() { }); test('does not drop a valid table for a long cell', () { - final errors = validateGeneratedSlide( + final errors = validateGeneratedSlideIssues( expectedKey: 'test-slide', rawSlide: _slideWithBlock({ 'type': 'block', @@ -130,36 +130,36 @@ void main() { 'A deliberately descriptive comparison that remains valid copy |', }), elementCatalog: GenerationElementCatalog.builtIn(), - ); + ).messages; expect(errors, isEmpty); }); test('rejects slide separators but allows them inside fenced code', () { final catalog = GenerationElementCatalog.builtIn(); - final unsafeErrors = validateGeneratedSlide( + final unsafeErrors = validateGeneratedSlideIssues( expectedKey: 'test-slide', rawSlide: _slideWithBlock({ 'type': 'block', 'content': 'Before\n\n---\n\nAfter', }), elementCatalog: catalog, - ); - final fencedErrors = validateGeneratedSlide( + ).messages; + final fencedErrors = validateGeneratedSlideIssues( expectedKey: 'test-slide', rawSlide: _slideWithBlock({ 'type': 'block', 'content': '```text\n---\n```', }), elementCatalog: catalog, - ); + ).messages; expect(unsafeErrors.single, contains('reserved for slide boundaries')); expect(fencedErrors, isEmpty); }); test('rejects raw content blocks that collapse during sanitization', () { - final errors = validateGeneratedSlide( + final errors = validateGeneratedSlideIssues( expectedKey: 'test-slide', rawSlide: { 'key': 'test-slide', @@ -181,7 +181,7 @@ void main() { }, planSlide: _planSlide(composition: 'table'), elementCatalog: GenerationElementCatalog.builtIn(), - ); + ).messages; expect( errors, @@ -193,7 +193,7 @@ void main() { }); test('rejects duplicate and unplanned generated elements', () { - final errors = validateGeneratedSlide( + final errors = validateGeneratedSlideIssues( expectedKey: 'test-slide', rawSlide: { 'key': 'test-slide', @@ -233,14 +233,14 @@ void main() { ], ), elementCatalog: GenerationElementCatalog.builtIn(), - ); + ).messages; expect(errors, contains('Expected exactly 1 "webview" widget; found 5.')); expect(errors, contains('Widget "qrcode" was not planned for this slide.')); }); test('rejects a heading-only non-title composition', () { - final errors = validateGeneratedSlide( + final errors = validateGeneratedSlideIssues( expectedKey: 'test-slide', rawSlide: _slideWithBlock({ 'type': 'block', @@ -248,7 +248,7 @@ void main() { }), planSlide: _planSlide(composition: 'content'), elementCatalog: GenerationElementCatalog.builtIn(), - ); + ).messages; expect( errors, @@ -259,7 +259,7 @@ void main() { }); test('rejects a slide that ignores its planned visual treatment', () { - final errors = validateGeneratedSlide( + final errors = validateGeneratedSlideIssues( expectedKey: 'test-slide', rawSlide: { 'key': 'test-slide', @@ -278,7 +278,7 @@ void main() { }, planSlide: _planSlide(composition: 'content'), elementCatalog: GenerationElementCatalog.builtIn(), - ); + ).messages; expect( errors, @@ -287,7 +287,7 @@ void main() { }); test('rejects content beyond the planned density budget', () { - final errors = validateGeneratedSlide( + final errors = validateGeneratedSlideIssues( expectedKey: 'test-slide', rawSlide: _slideWithBlock({ 'type': 'block', @@ -295,7 +295,7 @@ void main() { }), planSlide: _planSlide(composition: 'content', density: 'spacious'), elementCatalog: GenerationElementCatalog.builtIn(), - ); + ).messages; expect( errors, @@ -407,7 +407,7 @@ void main() { }); test('reserves H1 for display treatments and numeric metrics', () { - final errors = validateGeneratedSlide( + final errors = validateGeneratedSlideIssues( expectedKey: 'test-slide', rawSlide: _slideWithBlock({ 'type': 'block', @@ -415,7 +415,7 @@ void main() { }), planSlide: _planSlide(composition: 'content'), elementCatalog: GenerationElementCatalog.builtIn(), - ); + ).messages; expect( errors, @@ -711,12 +711,12 @@ void main() { ], }; - final errors = validateGeneratedSlide( + final errors = validateGeneratedSlideIssues( expectedKey: 'test-slide', rawSlide: raw, planSlide: _planSlide(composition: 'content'), elementCatalog: GenerationElementCatalog.builtIn(), - ); + ).messages; expect( errors, @@ -728,7 +728,7 @@ void main() { }); test('keeps title compositions free of dense structured content', () { - final errors = validateGeneratedSlide( + final errors = validateGeneratedSlideIssues( expectedKey: 'test-slide', rawSlide: _slideWithBlock({ 'type': 'block', @@ -736,7 +736,7 @@ void main() { }), planSlide: _planSlide(composition: 'title'), elementCatalog: GenerationElementCatalog.builtIn(), - ); + ).messages; expect( errors, @@ -748,7 +748,7 @@ void main() { }); test('keeps title display type in one full-height section', () { - final errors = validateGeneratedSlide( + final errors = validateGeneratedSlideIssues( expectedKey: 'test-slide', rawSlide: { 'key': 'test-slide', @@ -770,7 +770,7 @@ void main() { }, planSlide: _planSlide(composition: 'title'), elementCatalog: GenerationElementCatalog.builtIn(), - ); + ).messages; expect( errors, @@ -783,7 +783,7 @@ void main() { }); test('requires the table composition for Markdown tables', () { - final errors = validateGeneratedSlide( + final errors = validateGeneratedSlideIssues( expectedKey: 'test-slide', rawSlide: _slideWithBlock({ 'type': 'block', @@ -792,7 +792,7 @@ void main() { }), planSlide: _planSlide(composition: 'content'), elementCatalog: GenerationElementCatalog.builtIn(), - ); + ).messages; expect( errors, @@ -804,7 +804,7 @@ void main() { }); test('rejects a visible domain that was not supplied by the plan', () { - final errors = validateGeneratedSlide( + final errors = validateGeneratedSlideIssues( expectedKey: 'test-slide', rawSlide: _slideWithBlock({ 'type': 'block', @@ -813,7 +813,7 @@ void main() { }), planSlide: _planSlide(composition: 'content'), elementCatalog: GenerationElementCatalog.builtIn(), - ); + ).messages; expect( errors, @@ -829,7 +829,7 @@ void main() { userIntent: 'Explain the six-part beta with its supplied 42% result.', slideCount: 1, ); - final rejected = validateGeneratedSlide( + final rejected = validateGeneratedSlideIssues( expectedKey: 'test-slide', rawSlide: _slideWithBlock({ 'type': 'block', @@ -838,8 +838,8 @@ void main() { planSlide: _planSlide(composition: 'content'), elementCatalog: GenerationElementCatalog.builtIn(), request: request, - ); - final allowed = validateGeneratedSlide( + ).messages; + final allowed = validateGeneratedSlideIssues( expectedKey: 'test-slide', rawSlide: _slideWithBlock({ 'type': 'block', @@ -849,7 +849,7 @@ void main() { planSlide: _planSlide(composition: 'content'), elementCatalog: GenerationElementCatalog.builtIn(), request: request, - ); + ).messages; expect( rejected, @@ -889,7 +889,7 @@ void main() { isEmpty, ); - final structuralCounts = validateGeneratedSlide( + final structuralCounts = validateGeneratedSlideIssues( expectedKey: 'test-slide', rawSlide: _slideWithBlock({ 'type': 'block', @@ -901,14 +901,14 @@ void main() { userIntent: 'Explain onboarding with no change to source systems.', slideCount: 1, ), - ); + ).messages; expect( structuralCounts.where( (error) => error.contains('Visible content uses numeric claim(s)'), ), isEmpty, ); - final inventedZero = validateGeneratedSlide( + final inventedZero = validateGeneratedSlideIssues( expectedKey: 'test-slide', rawSlide: _slideWithBlock({ 'type': 'block', @@ -920,14 +920,14 @@ void main() { userIntent: 'Explain onboarding with no change to source systems.', slideCount: 1, ), - ); + ).messages; expect( inventedZero.singleWhere( (error) => error.contains('Visible content uses numeric claim(s)'), ), contains('numeric claim(s) 0%'), ); - final enumeratedSteps = validateGeneratedSlide( + final enumeratedSteps = validateGeneratedSlideIssues( expectedKey: 'test-slide', rawSlide: _slideWithBlock({ 'type': 'block', @@ -940,14 +940,14 @@ void main() { userIntent: 'Explain the onboarding workflow.', slideCount: 1, ), - ); + ).messages; expect( enumeratedSteps.where( (error) => error.contains('Visible content uses numeric claim(s)'), ), isEmpty, ); - final inventedDuration = validateGeneratedSlide( + final inventedDuration = validateGeneratedSlideIssues( expectedKey: 'test-slide', rawSlide: _slideWithBlock({ 'type': 'block', @@ -959,7 +959,7 @@ void main() { userIntent: 'Bring evidence into one workspace.', slideCount: 1, ), - ); + ).messages; expect( inventedDuration.singleWhere( (error) => error.contains('uses numeric claim(s)'), @@ -982,7 +982,7 @@ void main() { slideCount: 1, ), ); - final errors = validateGeneratedSlide( + final errors = validateGeneratedSlideIssues( expectedKey: 'test-slide', rawSlide: _slideWithBlock({ 'type': 'block', @@ -994,9 +994,9 @@ void main() { userIntent: 'Invite the audience to continue.', slideCount: 1, ), - ); + ).messages; - final waitlistErrors = validateGeneratedSlide( + final waitlistErrors = validateGeneratedSlideIssues( expectedKey: 'test-slide', rawSlide: _slideWithBlock({ 'type': 'block', @@ -1008,8 +1008,8 @@ void main() { userIntent: 'Invite the audience to continue.', slideCount: 1, ), - ); - final complianceErrors = validateGeneratedSlide( + ).messages; + final complianceErrors = validateGeneratedSlideIssues( expectedKey: 'test-slide', rawSlide: _slideWithBlock({ 'type': 'block', @@ -1021,7 +1021,7 @@ void main() { userIntent: 'Describe a governance model.', slideCount: 1, ), - ); + ).messages; final planComplianceIssues = validateGeneratedSlideIssues( expectedKey: 'test-slide', rawSlide: _slideWithBlock({ @@ -1084,7 +1084,7 @@ void main() { }); rawSlide['comments'] = ['Results came from a 12-week beta.']; - final errors = validateGeneratedSlide( + final errors = validateGeneratedSlideIssues( expectedKey: 'test-slide', rawSlide: rawSlide, planSlide: _planSlide(composition: 'content'), @@ -1093,7 +1093,7 @@ void main() { userIntent: 'Summarize the design-partner beta.', slideCount: 1, ), - ); + ).messages; expect( errors.singleWhere( @@ -1113,7 +1113,7 @@ void main() { 'Teams spend hours rebuilding context.', ]; - final errors = validateGeneratedSlide( + final errors = validateGeneratedSlideIssues( expectedKey: 'test-slide', rawSlide: rawSlide, planSlide: _planSlide(composition: 'content'), @@ -1122,7 +1122,7 @@ void main() { userIntent: 'Describe a product roadmap direction.', slideCount: 1, ), - ); + ).messages; expect( errors.singleWhere( @@ -1152,7 +1152,7 @@ void main() { 'documentation and launch team access are available.', ]; - final errors = validateGeneratedSlide( + final errors = validateGeneratedSlideIssues( expectedKey: 'test-slide', rawSlide: rawSlide, planSlide: _planSlide(composition: 'content'), @@ -1163,7 +1163,7 @@ void main() { 'synthesis time.', slideCount: 1, ), - ); + ).messages; final visible = errors.singleWhere( (error) => error.startsWith('Visible content introduces'), ); @@ -1203,7 +1203,7 @@ void main() { }); test('treats a dozen as an unsupported audience-facing number', () { - final errors = validateGeneratedSlide( + final errors = validateGeneratedSlideIssues( expectedKey: 'test-slide', rawSlide: _slideWithBlock({ 'type': 'block', @@ -1215,7 +1215,7 @@ void main() { userIntent: 'Show that evidence is fragmented across tools.', slideCount: 1, ), - ); + ).messages; expect( errors.singleWhere((error) => error.contains('uses numeric claim(s)')), @@ -1228,7 +1228,7 @@ void main() { userIntent: 'Teams spent 42% less weekly synthesis time.', slideCount: 1, ); - final changedMeaning = validateGeneratedSlide( + final changedMeaning = validateGeneratedSlideIssues( expectedKey: 'test-slide', rawSlide: _slideWithBlock({ 'type': 'block', @@ -1237,7 +1237,7 @@ void main() { planSlide: _planSlide(composition: 'content'), elementCatalog: GenerationElementCatalog.builtIn(), request: request, - ); + ).messages; final changedMeaningIssues = validateGeneratedSlideIssues( expectedKey: 'test-slide', rawSlide: _slideWithBlock({ @@ -1260,7 +1260,7 @@ void main() { ), ); expect(changedMeaningIssues.where((issue) => issue.isBlocking), isEmpty); - final inventedCausalMeaning = validateGeneratedSlide( + final inventedCausalMeaning = validateGeneratedSlideIssues( expectedKey: 'test-slide', rawSlide: _slideWithBlock({ 'type': 'block', @@ -1269,8 +1269,8 @@ void main() { planSlide: _planSlide(composition: 'content'), elementCatalog: GenerationElementCatalog.builtIn(), request: request, - ); - final preservedMeaning = validateGeneratedSlide( + ).messages; + final preservedMeaning = validateGeneratedSlideIssues( expectedKey: 'test-slide', rawSlide: _slideWithBlock({ 'type': 'block', @@ -1279,8 +1279,8 @@ void main() { planSlide: _planSlide(composition: 'content'), elementCatalog: GenerationElementCatalog.builtIn(), request: request, - ); - final broadenedFraction = validateGeneratedSlide( + ).messages; + final broadenedFraction = validateGeneratedSlideIssues( expectedKey: 'test-slide', rawSlide: _slideWithBlock({ 'type': 'block', @@ -1291,7 +1291,7 @@ void main() { planSlide: _planSlide(composition: 'content'), elementCatalog: GenerationElementCatalog.builtIn(), request: request, - ); + ).messages; expect( changedMeaning, @@ -1328,7 +1328,7 @@ void main() { }); rawSlide['comments'] = ['A 19% increase in general decision velocity.']; - final errors = validateGeneratedSlide( + final errors = validateGeneratedSlideIssues( expectedKey: 'test-slide', rawSlide: rawSlide, planSlide: _planSlide(composition: 'metric', treatment: 'data'), @@ -1337,7 +1337,7 @@ void main() { userIntent: 'The beta produced 19% faster experiment decisions.', slideCount: 1, ), - ); + ).messages; expect( errors, @@ -1353,7 +1353,7 @@ void main() { }); test('rejects open-ended metric inflation unless explicitly supplied', () { - final exactOnly = validateGeneratedSlide( + final exactOnly = validateGeneratedSlideIssues( expectedKey: 'test-slide', rawSlide: _slideWithBlock({ 'type': 'block', @@ -1365,8 +1365,8 @@ void main() { userIntent: 'The beta included 38 design partners.', slideCount: 1, ), - ); - final explicitlyOpenEnded = validateGeneratedSlide( + ).messages; + final explicitlyOpenEnded = validateGeneratedSlideIssues( expectedKey: 'test-slide', rawSlide: _slideWithBlock({ 'type': 'block', @@ -1378,7 +1378,7 @@ void main() { userIntent: 'The beta included 38+ design partners.', slideCount: 1, ), - ); + ).messages; expect( exactOnly, @@ -1396,7 +1396,7 @@ void main() { }); test('rejects a metric detached from its experimental context', () { - final errors = validateGeneratedSlide( + final errors = validateGeneratedSlideIssues( expectedKey: 'test-slide', rawSlide: _slideWithBlock({ 'type': 'block', @@ -1408,7 +1408,7 @@ void main() { userIntent: 'A controlled experiment produced 19% faster decisions.', slideCount: 1, ), - ); + ).messages; expect( errors, @@ -1420,7 +1420,7 @@ void main() { }); test('does not treat one collaborative inbox as a metric claim', () { - final errors = validateGeneratedSlide( + final errors = validateGeneratedSlideIssues( expectedKey: 'test-slide', rawSlide: _slideWithBlock({ 'type': 'block', @@ -1436,7 +1436,7 @@ void main() { 'teams.', slideCount: 1, ), - ); + ).messages; expect( errors.where((error) => error.contains('numeric claim(s) 1')), @@ -1445,7 +1445,7 @@ void main() { }); test('requires unsupported capability claims to remain hypothetical', () { - final unqualified = validateGeneratedSlide( + final unqualified = validateGeneratedSlideIssues( expectedKey: 'test-slide', rawSlide: _slideWithBlock({ 'type': 'block', @@ -1459,8 +1459,8 @@ void main() { userIntent: 'Explore possible enterprise governance directions.', slideCount: 1, ), - ); - final qualified = validateGeneratedSlide( + ).messages; + final qualified = validateGeneratedSlideIssues( expectedKey: 'test-slide', rawSlide: _slideWithBlock({ 'type': 'block', @@ -1474,7 +1474,7 @@ void main() { userIntent: 'Explore possible enterprise governance directions.', slideCount: 1, ), - ); + ).messages; final unsupportedError = unqualified.singleWhere( (error) => error.contains('unsupported commitment claim(s)'), @@ -1545,7 +1545,7 @@ void main() { test( 'rejects unqualified implementation details and invented pricing tiers', () { - final rejected = validateGeneratedSlide( + final rejected = validateGeneratedSlideIssues( expectedKey: 'test-slide', rawSlide: _slideWithBlock({ 'type': 'block', @@ -1565,7 +1565,7 @@ void main() { 'for a fictional beta product.', slideCount: 1, ), - ); + ).messages; final error = rejected.singleWhere( (candidate) => candidate.contains('unsupported commitment claim(s)'), @@ -1582,7 +1582,7 @@ void main() { expect(error, contains(phrase)); } - final qualified = validateGeneratedSlide( + final qualified = validateGeneratedSlideIssues( expectedKey: 'test-slide', rawSlide: _slideWithBlock({ 'type': 'block', @@ -1601,7 +1601,7 @@ void main() { 'for a fictional beta product.', slideCount: 1, ), - ); + ).messages; expect( qualified.where( (candidate) => candidate.contains('unsupported commitment claim(s)'), diff --git a/packages/playground/test/features/ai/quick_agent/core/engine/services/generation_model_call_executor_test.dart b/packages/playground/test/features/ai/generation/core/engine/services/generation_model_call_executor_test.dart similarity index 93% rename from packages/playground/test/features/ai/quick_agent/core/engine/services/generation_model_call_executor_test.dart rename to packages/playground/test/features/ai/generation/core/engine/services/generation_model_call_executor_test.dart index 7a1a28335..326b77f36 100644 --- a/packages/playground/test/features/ai/quick_agent/core/engine/services/generation_model_call_executor_test.dart +++ b/packages/playground/test/features/ai/generation/core/engine/services/generation_model_call_executor_test.dart @@ -1,5 +1,5 @@ import 'package:flutter_test/flutter_test.dart'; -import 'package:playground/features/ai/quick_agent/core/engine/services/generation_model_call_executor.dart'; +import 'package:playground/features/ai/generation/core/engine/services/generation_model_call_executor.dart'; void main() { test('caps provider requests across a generation run', () { diff --git a/packages/playground/test/features/ai/quick_agent/core/engine/services/generation_model_client_test.dart b/packages/playground/test/features/ai/generation/core/engine/services/generation_model_client_test.dart similarity index 88% rename from packages/playground/test/features/ai/quick_agent/core/engine/services/generation_model_client_test.dart rename to packages/playground/test/features/ai/generation/core/engine/services/generation_model_client_test.dart index 86d4465a3..6111744d7 100644 --- a/packages/playground/test/features/ai/quick_agent/core/engine/services/generation_model_client_test.dart +++ b/packages/playground/test/features/ai/generation/core/engine/services/generation_model_client_test.dart @@ -5,9 +5,9 @@ import 'package:google_cloud_ai_generativelanguage_v1beta/generativelanguage.dar 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'; +import 'package:playground/features/ai/generation/core/constants/gemini_models.dart'; +import 'package:playground/features/ai/generation/core/engine/services/error_classifier.dart'; +import 'package:playground/features/ai/generation/core/engine/services/generation_model_client.dart'; void main() { const classifier = ErrorClassifier(); diff --git a/packages/playground/test/features/ai/quick_agent/core/engine/services/generation_progress_test.dart b/packages/playground/test/features/ai/generation/core/engine/services/generation_progress_test.dart similarity index 94% rename from packages/playground/test/features/ai/quick_agent/core/engine/services/generation_progress_test.dart rename to packages/playground/test/features/ai/generation/core/engine/services/generation_progress_test.dart index 9f6f5b342..6b31085de 100644 --- a/packages/playground/test/features/ai/quick_agent/core/engine/services/generation_progress_test.dart +++ b/packages/playground/test/features/ai/generation/core/engine/services/generation_progress_test.dart @@ -1,5 +1,5 @@ import 'package:flutter_test/flutter_test.dart'; -import 'package:playground/features/ai/quick_agent/core/engine/services/generation_progress.dart'; +import 'package:playground/features/ai/generation/core/engine/services/generation_progress.dart'; void main() { group('GenerationPhaseLabel', () { diff --git a/packages/playground/test/features/ai/quick_agent/core/engine/services/generation_quality_report_test.dart b/packages/playground/test/features/ai/generation/core/engine/services/generation_quality_report_test.dart similarity index 94% rename from packages/playground/test/features/ai/quick_agent/core/engine/services/generation_quality_report_test.dart rename to packages/playground/test/features/ai/generation/core/engine/services/generation_quality_report_test.dart index 777067544..4825b32da 100644 --- a/packages/playground/test/features/ai/quick_agent/core/engine/services/generation_quality_report_test.dart +++ b/packages/playground/test/features/ai/generation/core/engine/services/generation_quality_report_test.dart @@ -1,9 +1,9 @@ import 'package:flutter_test/flutter_test.dart'; -import 'package:playground/features/ai/quick_agent/core/engine/schemas/outline_schema.dart'; -import 'package:playground/features/ai/quick_agent/core/engine/services/deck_generation_request.dart'; -import 'package:playground/features/ai/quick_agent/core/engine/services/generation_quality_report.dart'; -import 'package:playground/features/ai/quick_agent/core/engine/services/generation_trace.dart'; -import 'package:playground/features/ai/quick_agent/core/engine/services/generation_validation_issue.dart'; +import 'package:playground/features/ai/generation/core/engine/schemas/outline_schema.dart'; +import 'package:playground/features/ai/generation/core/engine/services/deck_generation_request.dart'; +import 'package:playground/features/ai/generation/core/engine/services/generation_quality_report.dart'; +import 'package:playground/features/ai/generation/core/engine/services/generation_trace.dart'; +import 'package:playground/features/ai/generation/core/engine/services/generation_validation_issue.dart'; import 'package:superdeck_core/superdeck_core.dart'; void main() { diff --git a/packages/playground/test/features/ai/quick_agent/core/engine/services/retry_policy_test.dart b/packages/playground/test/features/ai/generation/core/engine/services/retry_policy_test.dart similarity index 97% rename from packages/playground/test/features/ai/quick_agent/core/engine/services/retry_policy_test.dart rename to packages/playground/test/features/ai/generation/core/engine/services/retry_policy_test.dart index 421651922..1c290ca2e 100644 --- a/packages/playground/test/features/ai/quick_agent/core/engine/services/retry_policy_test.dart +++ b/packages/playground/test/features/ai/generation/core/engine/services/retry_policy_test.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'dart:math'; import 'package:flutter_test/flutter_test.dart'; -import 'package:playground/features/ai/quick_agent/core/engine/services/retry_policy.dart'; +import 'package:playground/features/ai/generation/core/engine/services/retry_policy.dart'; void main() { group('run', () { diff --git a/packages/playground/test/features/ai/quick_agent/core/engine/services/source_grounding_test.dart b/packages/playground/test/features/ai/generation/core/engine/services/source_grounding_test.dart similarity index 98% rename from packages/playground/test/features/ai/quick_agent/core/engine/services/source_grounding_test.dart rename to packages/playground/test/features/ai/generation/core/engine/services/source_grounding_test.dart index 3b4db9bc4..5959f90d8 100644 --- a/packages/playground/test/features/ai/quick_agent/core/engine/services/source_grounding_test.dart +++ b/packages/playground/test/features/ai/generation/core/engine/services/source_grounding_test.dart @@ -1,5 +1,5 @@ import 'package:flutter_test/flutter_test.dart'; -import 'package:playground/features/ai/quick_agent/core/engine/services/source_grounding.dart'; +import 'package:playground/features/ai/generation/core/engine/services/source_grounding.dart'; void main() { const businessCase = diff --git a/packages/playground/test/features/ai/quick_agent/domain/generated_deck_result_applier_test.dart b/packages/playground/test/features/ai/generation/domain/generated_deck_result_applier_test.dart similarity index 98% rename from packages/playground/test/features/ai/quick_agent/domain/generated_deck_result_applier_test.dart rename to packages/playground/test/features/ai/generation/domain/generated_deck_result_applier_test.dart index df2726ce7..d1e6310b4 100644 --- a/packages/playground/test/features/ai/quick_agent/domain/generated_deck_result_applier_test.dart +++ b/packages/playground/test/features/ai/generation/domain/generated_deck_result_applier_test.dart @@ -9,9 +9,9 @@ import 'package:playground/core/domain/design/presentation_theme_catalog.dart'; import 'package:playground/core/domain/design/presentation_typography_catalog.dart'; import 'package:playground/core/domain/generated_image_asset.dart'; import 'package:playground/core/domain/stores/deck_customization_store.dart'; -import 'package:playground/features/ai/quick_agent/core/engine/schemas/outline_schema.dart'; -import 'package:playground/features/ai/quick_agent/core/engine/services/deck_generator_service.dart'; -import 'package:playground/features/ai/quick_agent/domain/generated_deck_result_applier.dart'; +import 'package:playground/features/ai/generation/core/engine/schemas/outline_schema.dart'; +import 'package:playground/features/ai/generation/core/engine/services/deck_generator_service.dart'; +import 'package:playground/features/ai/generation/domain/generated_deck_result_applier.dart'; import 'package:playground/core/domain/stores/deck_document_store.dart'; import 'package:superdeck/superdeck.dart'; import 'package:superdeck_builder/superdeck_builder.dart'; diff --git a/packages/playground/test/features/ai/wizard/core/ai/catalog/presentation_theme_selection_test.dart b/packages/playground/test/features/ai/wizard/core/ai/catalog/presentation_theme_selection_test.dart index a0acd2eb9..6ab00dc62 100644 --- a/packages/playground/test/features/ai/wizard/core/ai/catalog/presentation_theme_selection_test.dart +++ b/packages/playground/test/features/ai/wizard/core/ai/catalog/presentation_theme_selection_test.dart @@ -50,7 +50,10 @@ void main() { PresentationThemeCatalog.withDefaults(), ); - for (final themeId in defaultPresentationThemeIds) { + for (final themeId + in PresentationThemeCatalog.withDefaults().currentThemes.map( + (theme) => theme.id, + )) { expect(prompt, contains(themeId)); } expect(prompt, contains('description')); 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 8f1459b50..3f71403e0 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 @@ -3,12 +3,12 @@ import 'dart:async'; import 'package:flutter_test/flutter_test.dart'; import 'package:playground/core/domain/design/presentation_theme_catalog.dart'; import 'package:playground/core/domain/design/presentation_typography_catalog.dart'; -import 'package:playground/features/ai/quick_agent/core/engine/schemas/outline_schema.dart'; -import 'package:playground/features/ai/quick_agent/core/engine/services/deck_generation_request.dart'; -import 'package:playground/features/ai/quick_agent/core/engine/services/deck_generator_service.dart'; -import 'package:playground/features/ai/quick_agent/core/engine/services/deck_theme_resolution.dart'; -import 'package:playground/features/ai/quick_agent/core/engine/services/generation_validation_issue.dart'; -import 'package:playground/features/ai/quick_agent/domain/generated_deck_result_applier.dart'; +import 'package:playground/features/ai/generation/core/engine/schemas/outline_schema.dart'; +import 'package:playground/features/ai/generation/core/engine/services/deck_generation_request.dart'; +import 'package:playground/features/ai/generation/core/engine/services/deck_generator_service.dart'; +import 'package:playground/features/ai/generation/core/engine/services/deck_theme_resolution.dart'; +import 'package:playground/features/ai/generation/core/engine/services/generation_validation_issue.dart'; +import 'package:playground/features/ai/generation/domain/generated_deck_result_applier.dart'; import 'package:playground/features/ai/wizard/presentation/wizard_generation_controller.dart'; import 'package:superdeck_core/superdeck_core.dart'; @@ -79,10 +79,15 @@ void main() { await controller.generateSlides(); expect(controller.stage, WizardGenerationStage.failed); - expect(controller.failedPhase, WizardGenerationPhase.composition); expect(controller.errorMessage, 'Composition failed.'); expect(controller.plan, isNotNull); + // Retrying a composition failure re-runs composition instead of + // regenerating the outline, so it fails the same way again. + await controller.retry(); + expect(controller.stage, WizardGenerationStage.failed); + expect(controller.errorMessage, 'Composition failed.'); + controller.returnToOutline(); expect(controller.stage, WizardGenerationStage.outlineReview); }, 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 ee25775a9..30cdab831 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 @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:hero_ui/hero_ui.dart'; -import 'package:playground/features/ai/quick_agent/core/engine/schemas/outline_schema.dart'; +import 'package:playground/features/ai/generation/core/engine/schemas/outline_schema.dart'; import 'package:playground/features/ai/wizard/presentation/wizard_outline_review.dart'; void main() { 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 8d8e71594..c663454f3 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 @@ -8,12 +8,12 @@ import 'package:playground/app/router.dart'; import 'package:playground/core/data/data_sources/memory_deck_loader.dart'; import 'package:playground/core/domain/design/presentation_theme_catalog.dart'; import 'package:playground/core/domain/design/presentation_typography_catalog.dart'; -import 'package:playground/features/ai/quick_agent/core/engine/schemas/outline_schema.dart'; -import 'package:playground/features/ai/quick_agent/core/engine/services/deck_generation_request.dart'; +import 'package:playground/features/ai/generation/core/engine/schemas/outline_schema.dart'; +import 'package:playground/features/ai/generation/core/engine/services/deck_generation_request.dart'; import 'package:playground/features/ai/wizard/presentation/wizard_page.dart'; import 'package:playground/features/ai/wizard/presentation/wizard_view.dart'; -import 'package:playground/features/ai/quick_agent/core/engine/services/deck_generator_service.dart'; -import 'package:playground/features/ai/quick_agent/core/engine/services/deck_theme_resolution.dart'; +import 'package:playground/features/ai/generation/core/engine/services/deck_generator_service.dart'; +import 'package:playground/features/ai/generation/core/engine/services/deck_theme_resolution.dart'; import 'package:playground/features/ai/wizard/presentation/wizard_generation_controller.dart'; import 'package:provider/provider.dart'; import 'package:superdeck_core/superdeck_core.dart'; diff --git a/packages/playground/test/features/ai/wizard/presentation/wizard_save_flow_test.dart b/packages/playground/test/features/ai/wizard/presentation/wizard_save_flow_test.dart index 5fe1776a9..a7d7efb04 100644 --- a/packages/playground/test/features/ai/wizard/presentation/wizard_save_flow_test.dart +++ b/packages/playground/test/features/ai/wizard/presentation/wizard_save_flow_test.dart @@ -4,11 +4,12 @@ import 'package:go_router/go_router.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:hero_ui/hero_ui.dart'; import 'package:playground/app/providers.dart'; +import 'package:playground/core/data/data_sources/deck_library_asset_store.dart'; import 'package:playground/core/domain/generated_image_asset.dart'; -import 'package:playground/features/ai/quick_agent/core/engine/schemas/outline_schema.dart'; -import 'package:playground/features/ai/quick_agent/core/engine/services/deck_generation_request.dart'; -import 'package:playground/features/ai/quick_agent/core/engine/services/deck_generator_service.dart'; -import 'package:playground/features/ai/quick_agent/core/engine/services/deck_theme_resolution.dart'; +import 'package:playground/features/ai/generation/core/engine/schemas/outline_schema.dart'; +import 'package:playground/features/ai/generation/core/engine/services/deck_generation_request.dart'; +import 'package:playground/features/ai/generation/core/engine/services/deck_generator_service.dart'; +import 'package:playground/features/ai/generation/core/engine/services/deck_theme_resolution.dart'; import 'package:playground/core/domain/design/presentation_theme_catalog.dart'; import 'package:playground/core/domain/design/presentation_typography_catalog.dart'; import 'package:playground/features/ai/wizard/presentation/wizard_generation_controller.dart'; @@ -36,7 +37,13 @@ void main() { TestWidgetsFlutterBinding.ensureInitialized(); setUpAll(() => GoogleFonts.config.allowRuntimeFetching = false); - Future<({WizardGenerationController wizard, DeckLibraryController library})> + Future< + ({ + WizardGenerationController wizard, + DeckLibraryController library, + DeckLibraryAssetStore assets, + }) + > pumpCompletedDeck(WidgetTester tester, FakeDeckLibrary library) async { final router = GoRouter( routes: [ @@ -85,6 +92,7 @@ void main() { return ( wizard: wizard, library: Provider.of(wizardContext, listen: false), + assets: Provider.of(wizardContext, listen: false), ); } @@ -131,7 +139,11 @@ void main() { {'id': 'technical-paper', 'version': 1, 'density': 'balanced'}, reason: 'the saved deck carries canonical, versioned theme metadata', ); - expect(scope.library.openDeck?.name, 'Urban gardens 2026'); + expect( + (await scope.assets.resolve('hero.png'))?.scheme, + 'file', + reason: 'the saved deck now answers for artwork', + ); expect( find.textContaining('Saved "Urban gardens 2026"'), findsOneWidget, @@ -183,7 +195,7 @@ void main() { await tester.pumpAndSettle(); await tester.tap(find.widgetWithText(GestureDetector, 'Save deck').last); await tester.pumpAndSettle(); - expect(scope.library.openDeck, isNotNull); + expect((await scope.assets.resolve('hero.png'))?.scheme, 'file'); // Generating again publishes a new deck into the same runtime. await scope.wizard.createOutline(_request); @@ -192,8 +204,8 @@ void main() { await tester.pumpAndSettle(); expect( - scope.library.openDeck, - isNull, + (await scope.assets.resolve('hero.png'))?.scheme, + 'data', reason: 'the new deck owns the runtime, so the saved deck must not ' 'answer for artwork any more', ); diff --git a/packages/playground/test/features/ai/wizard/wizard_polish_test.dart b/packages/playground/test/features/ai/wizard/wizard_polish_test.dart index 18b005478..32ca46005 100644 --- a/packages/playground/test/features/ai/wizard/wizard_polish_test.dart +++ b/packages/playground/test/features/ai/wizard/wizard_polish_test.dart @@ -3,7 +3,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:hero_ui/hero_ui.dart'; import 'package:playground/core/domain/design/presentation_theme_catalog.dart'; -import 'package:playground/features/ai/quick_agent/core/engine/services/generation_progress.dart'; +import 'package:playground/features/ai/generation/core/engine/services/generation_progress.dart'; import 'package:playground/features/ai/wizard/chat/view/widgets/chat_input.dart'; import 'package:playground/features/ai/wizard/chat/view/widgets/chat_genui_panels.dart'; import 'package:playground/features/ai/wizard/chat/view/widgets/empty_state.dart'; diff --git a/packages/playground/test/features/library/domain/deck_library_controller_test.dart b/packages/playground/test/features/library/domain/deck_library_controller_test.dart index a2428a84e..e2d071e25 100644 --- a/packages/playground/test/features/library/domain/deck_library_controller_test.dart +++ b/packages/playground/test/features/library/domain/deck_library_controller_test.dart @@ -90,7 +90,6 @@ void main() { expect(saved, isTrue); expect(scope.library.markdown['/decks/My talk.md'], '# Generated\n'); expect(scope.controller.lastSave?.isComplete, isTrue); - expect(scope.controller.openDeck?.name, 'My talk'); expect(scope.controller.decks.single.name, 'My talk'); // The saved copy now answers for the artwork, not the run's memory. expect(await scope.assets.resolve('hero.png'), isNotNull); @@ -117,7 +116,7 @@ void main() { expect(saved, isFalse); expect(scope.controller.errorMessage, contains('disk full')); - expect(scope.controller.openDeck, isNull); + expect(scope.controller.decks, isEmpty); expect(scope.controller.lastSave, isNull); }); @@ -138,7 +137,13 @@ void main() { group('open', () { test('publishes a saved deck and its theme', () async { final scope = newScope(); - await scope.controller.save(name: 'My talk', theme: _theme); + await scope.controller.save( + name: 'My talk', + images: [ + GeneratedImageAsset.success(assetKey: 'hero.png', bytes: [4]), + ], + theme: _theme, + ); scope.document.replaceMarkdown('# Something else\n'); scope.controller.releaseOpenDeck(); final ref = scope.controller.decks.single; @@ -147,7 +152,11 @@ void main() { expect(opened, isTrue); expect(scope.document.markdown, '# Generated\n'); - expect(scope.controller.openDeck, ref); + expect( + await scope.assets.resolve('hero.png'), + isNotNull, + reason: 'the reopened deck answers for its own artwork', + ); expect(scope.controller.errorMessage, isNull); expect(scope.customization.level(TextLevel.h1).family, isNotEmpty); }); @@ -180,7 +189,7 @@ void main() { expect(opened, isFalse); expect(scope.controller.errorMessage, contains('gone')); - expect(scope.controller.openDeck, isNull); + expect(scope.document.markdown, '# Generated\n'); }); test('releasing the open deck returns artwork to the run', () async { @@ -195,8 +204,13 @@ void main() { scope.controller.releaseOpenDeck(); - expect(scope.controller.openDeck, isNull); - expect(await scope.assets.resolve('hero.png'), isNotNull); + final resolved = await scope.assets.resolve('hero.png'); + expect(resolved, isNotNull); + expect( + resolved!.scheme, + 'data', + reason: 'the run\'s own artwork answers again, not the saved deck\'s', + ); }); }); diff --git a/packages/playground/test/features/library/presentation/saved_decks_page_test.dart b/packages/playground/test/features/library/presentation/saved_decks_page_test.dart index d3d83d2c0..160dd905e 100644 --- a/packages/playground/test/features/library/presentation/saved_decks_page_test.dart +++ b/packages/playground/test/features/library/presentation/saved_decks_page_test.dart @@ -19,10 +19,8 @@ void main() { TestWidgetsFlutterBinding.ensureInitialized(); setUpAll(() => GoogleFonts.config.allowRuntimeFetching = false); - Future pumpPage( - WidgetTester tester, - FakeDeckLibrary library, - ) async { + Future<({DeckLibraryController controller, DeckDocumentStore document})> + pumpPage(WidgetTester tester, FakeDeckLibrary library) async { final memory = MemoryAssetCacheStore(); final assets = DeckLibraryAssetStore(library: library, fallback: memory); final document = DeckDocumentStore(markdown: ''); @@ -75,7 +73,7 @@ void main() { ); await tester.pumpAndSettle(); - return controller; + return (controller: controller, document: document); } testWidgets('offers nothing to present before a deck is saved', ( @@ -90,7 +88,7 @@ void main() { testWidgets('lists saved decks and presents the one chosen', (tester) async { final library = FakeDeckLibrary(); await library.save(name: 'Quarterly review', markdown: '# Q3\n'); - final controller = await pumpPage(tester, library); + final scope = await pumpPage(tester, library); expect(find.text('Quarterly review'), findsOneWidget); expect(find.textContaining(RegExp(r'Saved \d{4}-')), findsOneWidget); @@ -99,7 +97,7 @@ void main() { await tester.pumpAndSettle(); expect(find.text('presenting'), findsOneWidget); - expect(controller.openDeck?.name, 'Quarterly review'); + expect(scope.document.markdown, '# Q3\n'); }); testWidgets('a library it cannot read says so instead of looking empty', ( diff --git a/packages/playground/test/helpers/fake_security_scoped_file_access.dart b/packages/playground/test/helpers/fake_security_scoped_file_access.dart index 105ef2958..9458217a2 100644 --- a/packages/playground/test/helpers/fake_security_scoped_file_access.dart +++ b/packages/playground/test/helpers/fake_security_scoped_file_access.dart @@ -1,9 +1,8 @@ import 'package:path_provider_platform_interface/path_provider_platform_interface.dart'; import 'package:playground/core/data/data_sources/security_scoped_file_access.dart'; -import 'package:playground/core/data/data_sources/deck_file.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; -/// Points the repository's settings and default deck folder at a temp root. +/// Points the library's settings and default deck folder at a temp root. class FakePathProvider extends PathProviderPlatform with MockPlatformInterfaceMixin { FakePathProvider(this.root); @@ -20,41 +19,19 @@ class FakePathProvider extends PathProviderPlatform /// Stands in for the macOS picker and bookmark APIs, which need a real user /// and a real sandbox. Everything below it is ordinary file I/O. class FakeSecurityScopedFileAccess extends SecurityScopedFileAccess { - DeckFileReference? picked; SecurityScopedDirectoryReference? pickedDirectory; - final Map restored = {}; final Map restoredDirectories = {}; - final List started = []; - final List stopped = []; final List startedDirectories = []; final List stoppedDirectories = []; Object? startError; Object? stopError; - @override - Future pickDeckFile() async => picked; - @override Future pickDecksDirectory() async { return pickedDirectory; } - @override - Future startAccessing(DeckFileReference reference) async { - started.add(reference); - final error = startError; - if (error != null) throw error; - return restored[reference] ?? reference; - } - - @override - Future stopAccessing(DeckFileReference reference) async { - stopped.add(reference); - final error = stopError; - if (error != null) throw error; - } - @override Future startAccessingDirectory( SecurityScopedDirectoryReference reference, 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 22b427c01..180a6a4bf 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 @@ -19,18 +19,18 @@ import 'package:playground/core/domain/design/presentation_theme_catalog.dart'; import 'package:playground/core/domain/design/presentation_typography_catalog.dart'; import 'package:playground/core/domain/generated_image_asset.dart'; import 'package:playground/core/domain/stores/deck_customization_store.dart'; -import 'package:playground/features/ai/quick_agent/core/engine/schemas/outline_schema.dart'; -import 'package:playground/features/ai/quick_agent/core/engine/services/deck_generation_request.dart'; -import 'package:playground/features/ai/quick_agent/core/engine/services/deck_generator_service.dart'; -import 'package:playground/features/ai/quick_agent/core/engine/services/deck_plan_validator.dart'; -import 'package:playground/features/ai/quick_agent/core/engine/services/deck_theme_resolution.dart'; -import 'package:playground/features/ai/quick_agent/core/engine/services/generation_element_catalog.dart'; -import 'package:playground/features/ai/quick_agent/core/engine/services/generation_model_client.dart'; -import 'package:playground/features/ai/quick_agent/core/engine/services/generated_slide_validator.dart'; -import 'package:playground/features/ai/quick_agent/core/engine/services/generation_quality_report.dart'; -import 'package:playground/features/ai/quick_agent/core/engine/services/generation_trace.dart'; -import 'package:playground/features/ai/quick_agent/core/engine/services/generation_validation_issue.dart'; -import 'package:playground/features/ai/quick_agent/core/engine/services/theme_json_serializer.dart'; +import 'package:playground/features/ai/generation/core/engine/schemas/outline_schema.dart'; +import 'package:playground/features/ai/generation/core/engine/services/deck_generation_request.dart'; +import 'package:playground/features/ai/generation/core/engine/services/deck_generator_service.dart'; +import 'package:playground/features/ai/generation/core/engine/services/deck_plan_validator.dart'; +import 'package:playground/features/ai/generation/core/engine/services/deck_theme_resolution.dart'; +import 'package:playground/features/ai/generation/core/engine/services/generation_element_catalog.dart'; +import 'package:playground/features/ai/generation/core/engine/services/generation_model_client.dart'; +import 'package:playground/features/ai/generation/core/engine/services/generated_slide_validator.dart'; +import 'package:playground/features/ai/generation/core/engine/services/generation_quality_report.dart'; +import 'package:playground/features/ai/generation/core/engine/services/generation_trace.dart'; +import 'package:playground/features/ai/generation/core/engine/services/generation_validation_issue.dart'; +import 'package:playground/features/ai/generation/core/engine/services/theme_json_serializer.dart'; import 'package:playground/core/domain/design/generated_deck_style_mapper.dart'; import 'package:playground/features/ai/image_generation/image_generator.dart'; import 'package:playground/features/ai/wizard/presentation/wizard_generation_controller.dart'; diff --git a/packages/plugins/pdf/analysis_options.yaml b/packages/plugins/pdf/analysis_options.yaml index b93cbb974..775b8d974 100644 --- a/packages/plugins/pdf/analysis_options.yaml +++ b/packages/plugins/pdf/analysis_options.yaml @@ -1,5 +1,6 @@ -include: package:flutter_lints/flutter.yaml -extends: ../../../shared_analysis_options.yaml +include: + - package:flutter_lints/flutter.yaml + - ../../../shared_analysis_options.yaml linter: rules: diff --git a/packages/plugins/pdf/lib/src/pdf_controller.dart b/packages/plugins/pdf/lib/src/pdf_controller.dart index 5448b2758..15e30c82f 100644 --- a/packages/plugins/pdf/lib/src/pdf_controller.dart +++ b/packages/plugins/pdf/lib/src/pdf_controller.dart @@ -116,9 +116,6 @@ class PdfController { final Duration _renderAttachmentTimeout; final Duration _slideReadinessTimeout; - /// Whether this controller has been disposed. - bool get disposed => _disposed; // ignore: unused-code - /// The page controller used during export. PageController get pageController => _pageController; @@ -137,52 +134,52 @@ class PdfController { Future waitForRenderBoundaryPaint(GlobalKey key) => _waitForRenderBoundaryPaint(key); - /// Waits for [key]'s render boundary to attach. - Future _waitForRenderBoundaryPaint(GlobalKey key) async { + /// Polls [isDone] until it holds, or returns `false` once [timeout] passes. + /// + /// Stops the export as soon as it is cancelled or the controller disposed. + Future _pollUntil(bool Function() isDone, Duration timeout) async { var elapsed = Duration.zero; - var hasSeenContext = false; - - while (elapsed < _renderAttachmentTimeout) { + while (true) { _checkExportAllowed(); - - if (key.currentContext != null) { - hasSeenContext = true; - } - - final repaintBoundary = key.currentContext?.findRenderObject(); - if (repaintBoundary != null && repaintBoundary.attached) { - await WidgetsBinding.instance.endOfFrame; - return; - } + if (isDone()) return true; + if (elapsed >= timeout) return false; await Future.delayed(_kPollInterval); elapsed += _kPollInterval; } + } - throw StateError( - hasSeenContext - ? 'RenderObject not attached within $_renderAttachmentTimeout' - : 'RenderObject context not available within $_renderAttachmentTimeout', - ); + /// Waits for [key]'s render boundary to attach. + Future _waitForRenderBoundaryPaint(GlobalKey key) async { + var hasSeenContext = false; + final attached = await _pollUntil(() { + final context = key.currentContext; + if (context == null) return false; + hasSeenContext = true; + return context.findRenderObject()?.attached ?? false; + }, _renderAttachmentTimeout); + + if (!attached) { + throw StateError( + hasSeenContext + ? 'RenderObject not attached within $_renderAttachmentTimeout' + : 'RenderObject context not available within $_renderAttachmentTimeout', + ); + } + await WidgetsBinding.instance.endOfFrame; } Future _waitForPageControllerAttachment() async { - var elapsed = Duration.zero; - - while (elapsed < _renderAttachmentTimeout) { - _checkExportAllowed(); - - if (_pageController.hasClients) { - await WidgetsBinding.instance.endOfFrame; - return; - } + final attached = await _pollUntil( + () => _pageController.hasClients, + _renderAttachmentTimeout, + ); - await Future.delayed(_kPollInterval); - elapsed += _kPollInterval; + if (!attached) { + throw StateError( + 'PageController not attached within $_renderAttachmentTimeout', + ); } - - throw StateError( - 'PageController not attached within $_renderAttachmentTimeout', - ); + await WidgetsBinding.instance.endOfFrame; } /// Waits until every asynchronous visual on [slide] has finished. @@ -195,23 +192,18 @@ class PdfController { Future _waitForSlideReadiness(int index) async { final slide = slides[index]; final readiness = _slideReadiness[slide.key]!; - var elapsed = Duration.zero; + final settled = await _pollUntil( + () => readiness.isReady || readiness.failures.isNotEmpty, + _slideReadinessTimeout, + ); - while (!readiness.isReady) { - _checkExportAllowed(); - if (readiness.failures.isNotEmpty) break; - if (elapsed >= _slideReadinessTimeout) { - throw StateError( - '${_describeSlide(index)} was not ready within ' - '$_slideReadinessTimeout. Still waiting for: ' - '${readiness.pendingLabels.join(', ')}.', - ); - } - await Future.delayed(_kPollInterval); - elapsed += _kPollInterval; + if (!settled) { + throw StateError( + '${_describeSlide(index)} was not ready within ' + '$_slideReadinessTimeout. Still waiting for: ' + '${readiness.pendingLabels.join(', ')}.', + ); } - - _checkExportAllowed(); if (readiness.failures.isNotEmpty) { final failures = readiness.failures .map((failure) => '${failure.label ?? 'visual'} (${failure.reason})') @@ -379,7 +371,7 @@ class PdfController { Future _defaultPdfSaver(Uint8List pdf, {required String fileName}) async { final saver = FileSaver.instance; - return savePdfWithFileSaverForTesting( + return savePdfWithFileSaver( pdf: pdf, fileName: fileName, isWeb: kIsWeb, @@ -417,10 +409,9 @@ Future _defaultPdfSaver(Uint8List pdf, {required String fileName}) async { /// Saves PDF bytes through [FileSaver] platform operations. /// -/// This is public only for package tests; callers should use -/// [PdfExportOptions.pdfSaver] to customize saving behavior. -@visibleForTesting -Future savePdfWithFileSaverForTesting({ +/// This is the default [PdfSaver] used when [PdfExportOptions.pdfSaver] is +/// not set; callers can supply that option to customize saving behavior. +Future savePdfWithFileSaver({ required Uint8List pdf, required String fileName, required bool isWeb, diff --git a/packages/plugins/pdf/pubspec.yaml b/packages/plugins/pdf/pubspec.yaml index 883ac3ce1..2fb8e4364 100644 --- a/packages/plugins/pdf/pubspec.yaml +++ b/packages/plugins/pdf/pubspec.yaml @@ -1,7 +1,7 @@ name: superdeck_pdf description: PDF export support for SuperDeck presentations. version: 1.0.0 -homepage: https://github.com/btwld/superdeck +homepage: https://github.com/conceptadev/superdeck documentation: https://docs.page/btwld/superdeck environment: diff --git a/packages/plugins/pdf/test/src/pdf_controller_test.dart b/packages/plugins/pdf/test/src/pdf_controller_test.dart index a2d32efe6..b76ba9cec 100644 --- a/packages/plugins/pdf/test/src/pdf_controller_test.dart +++ b/packages/plugins/pdf/test/src/pdf_controller_test.dart @@ -204,10 +204,6 @@ void main() { expect(controller.pageController.initialPage, 0); }); - test('disposed is false initially', () { - expect(controller.disposed, false); - }); - test('progress starts at zero', () { expect(controller.progress.value, 0.0); }); @@ -288,7 +284,7 @@ void main() { ) async { final exportController = createExportController( pdfSaver: (pdf) { - return savePdfWithFileSaverForTesting( + return savePdfWithFileSaver( pdf: pdf, fileName: 'slides', isWeb: true, @@ -710,7 +706,7 @@ void main() { test('uses saveFile instead of saveAs on Linux', () async { final calls = []; - final saved = await savePdfWithFileSaverForTesting( + final saved = await savePdfWithFileSaver( pdf: Uint8List.fromList([1, 2, 3]), fileName: 'slides', isWeb: false, diff --git a/packages/superdeck/.gitignore b/packages/superdeck/.gitignore index 9d6873959..c609fb58b 100644 --- a/packages/superdeck/.gitignore +++ b/packages/superdeck/.gitignore @@ -45,7 +45,6 @@ build/ # Node file for website node_modules -.local-chrome # Golden test failure artifacts test/goldens/failures/ diff --git a/packages/superdeck/CHANGELOG.md b/packages/superdeck/CHANGELOG.md index b45fec240..4f10b64df 100644 --- a/packages/superdeck/CHANGELOG.md +++ b/packages/superdeck/CHANGELOG.md @@ -1,12 +1,15 @@ ## 1.0.0 +- **Breaking:** remove `superDeckAspectRatio`; it had no production use. Use + `superDeckSlideSize` for slide geometry. + - **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. + `mermaid_core` instead of the removed browser-backed build plugin. Mermaid diagrams no longer require generated image assets, cache management, or custom runner registration; invalid and unsupported syntax is reported inline at runtime. Diagrams use a transparent background and follow the @@ -30,7 +33,7 @@ foreground decoration, clipping, variants, and animation — widget modifiers, constraints, transforms, and box alignment are unrepresentable for the framework-owned block frame. Raw variant lists reject non-`BlockStyler` - values eagerly and are snapshotted to preserve that invariant; low-level + values eagerly and are snapshotted to preserve that invariant; low-level `SlideStyler.create` input is reduced to the same allow-list at the render boundary. - Add image paint scaling with aligned clipping and an unchanged `scale: 1` @@ -61,8 +64,8 @@ - Stylers gain the full generated fluent API: per-field setter methods (`SlideStyler().h1(...)`, `.strong(...)`), field factories, and widget-state variant helpers (`onHovered`, `onFocused`, ...). -- Deprecate `MarkdownTextSpec`/`MarkdownTextStyle`: they are not wired into - rendering. Use `SlideStyler` / Mix `TextStyler` (`p`, `h1`–`h6`, `strong`, +- **Breaking:** remove `MarkdownTextSpec`/`MarkdownTextStyle`: they were not + wired into rendering. Use `SlideStyler` / Mix `TextStyler` (`p`, `h1`–`h6`, `strong`, `em`, `del`, `link`) instead. - Add `BlockVariant` for opt-in, name-based `WidgetBlock` styling in Dart stylesheets. diff --git a/packages/superdeck/README.md b/packages/superdeck/README.md index 2c479c050..a50459120 100644 --- a/packages/superdeck/README.md +++ b/packages/superdeck/README.md @@ -3,7 +3,7 @@ SuperDeck renders Markdown slides in Flutter. - Live demo: https://superdeck-dev.web.app -- Documentation: https://github.com/btwld/superdeck/tree/main/docs +- Documentation: https://github.com/conceptadev/superdeck/tree/main/docs ## Install @@ -29,7 +29,7 @@ Future main() async { WidgetsFlutterBinding.ensureInitialized(); await SuperDeckApp.initialize(); - runApp(const SuperDeckApp(options: DeckOptions())); + runApp(SuperDeckApp(options: DeckOptions())); } ``` @@ -76,7 +76,7 @@ title: Welcome 2. Reference it by name in Markdown. See the custom widgets guide: -https://github.com/btwld/superdeck/blob/main/docs/guides/custom-widgets.mdx +https://github.com/conceptadev/superdeck/blob/main/docs/guides/custom-widgets.mdx ## License diff --git a/packages/superdeck/analysis_options.yaml b/packages/superdeck/analysis_options.yaml index b21c0e91e..6ccc13a06 100644 --- a/packages/superdeck/analysis_options.yaml +++ b/packages/superdeck/analysis_options.yaml @@ -1,5 +1,6 @@ -include: package:flutter_lints/flutter.yaml -extends: ../../shared_analysis_options.yaml +include: + - package:flutter_lints/flutter.yaml + - ../../shared_analysis_options.yaml linter: rules: diff --git a/packages/superdeck/assets/iframe_template.html b/packages/superdeck/assets/iframe_template.html deleted file mode 100644 index ffde02f71..000000000 --- a/packages/superdeck/assets/iframe_template.html +++ /dev/null @@ -1,104 +0,0 @@ - - - - - - iframe template - - - - - - - - \ No newline at end of file diff --git a/packages/superdeck/lib/src/builtins/image_widget.dart b/packages/superdeck/lib/src/builtins/image_widget.dart index 7be0660da..0621a9498 100644 --- a/packages/superdeck/lib/src/builtins/image_widget.dart +++ b/packages/superdeck/lib/src/builtins/image_widget.dart @@ -174,11 +174,7 @@ class ImageWidget extends StatelessWidget { targetSize: data.size, styleSpec: styleSpec, ), - null => CachedImage( - uri: _data.src, - targetSize: data.size, - styleSpec: styleSpec, - ), + null => CachedImage(uri: _data.src, styleSpec: styleSpec), }; final hasExplicitSize = _data.width != null || _data.height != null; diff --git a/packages/superdeck/lib/src/deck/deck_presentation_state.dart b/packages/superdeck/lib/src/deck/deck_presentation_state.dart index 52f121322..13ae7caa0 100644 --- a/packages/superdeck/lib/src/deck/deck_presentation_state.dart +++ b/packages/superdeck/lib/src/deck/deck_presentation_state.dart @@ -211,13 +211,6 @@ final class DeckPresentationState { _thumbnails.value = cache; } - /// Makes the route the single authority for the active slide. - /// - /// The route decides [currentIndex]. A route outside a loaded deck is - /// corrected once, by replacing it with the nearest slide that exists; - /// `replace` keeps that correction out of the browser history on web, which - /// `go` would add. While the deck is empty the route is left alone, so a - /// deep link opened before the deck loads still lands on its slide. void _syncCurrentIndexFromDelegate() { _syncCurrentIndexFromPath( router.routerDelegate.currentConfiguration.uri.path, @@ -228,6 +221,13 @@ final class DeckPresentationState { _syncCurrentIndexFromPath(router.routeInformationProvider.value.uri.path); } + /// Makes the route the single authority for the active slide. + /// + /// The route decides [currentIndex]. A route outside a loaded deck is + /// corrected once, by replacing it with the nearest slide that exists; + /// `replace` keeps that correction out of the browser history on web, which + /// `go` would add. While the deck is empty the route is left alone, so a + /// deep link opened before the deck loads still lands on its slide. void _syncCurrentIndexFromPath(String path) { if (_disposed) return; const prefix = '/slides/'; diff --git a/packages/superdeck/lib/src/deck/navigation_events.dart b/packages/superdeck/lib/src/deck/navigation_events.dart index 4d15ff150..ebcbdb0dc 100644 --- a/packages/superdeck/lib/src/deck/navigation_events.dart +++ b/packages/superdeck/lib/src/deck/navigation_events.dart @@ -7,12 +7,6 @@ class NextSlideEvent extends NavigationEvent {} class PreviousSlideEvent extends NavigationEvent {} -class GoToSlideEvent extends NavigationEvent { - final int index; - - GoToSlideEvent(this.index); // ignore: unused-code -} - /// Maps Meta+arrow keyboard shortcuts to navigation events. /// /// Returns `null` for keys that don't trigger navigation. diff --git a/packages/superdeck/lib/src/deck/navigation_input_listener.dart b/packages/superdeck/lib/src/deck/navigation_input_listener.dart index 1dc18a795..a693f6bac 100644 --- a/packages/superdeck/lib/src/deck/navigation_input_listener.dart +++ b/packages/superdeck/lib/src/deck/navigation_input_listener.dart @@ -60,8 +60,6 @@ class _NavigationInputListenerState extends State { presentation.nextSlide(); case PreviousSlideEvent(): presentation.previousSlide(); - case GoToSlideEvent(:final index): - presentation.goToSlide(index); } } diff --git a/packages/superdeck/lib/src/deck/template_resolver.dart b/packages/superdeck/lib/src/deck/template_resolver.dart index 848013c91..0fa019dc0 100644 --- a/packages/superdeck/lib/src/deck/template_resolver.dart +++ b/packages/superdeck/lib/src/deck/template_resolver.dart @@ -14,14 +14,7 @@ class TemplateResolutionResult { /// The chrome parts (header, footer, background) for the slide. final SlideParts parts; - /// Whether a template was used in the resolution. - final bool usingTemplate; // ignore: unused-code - - const TemplateResolutionResult({ - required this.style, - required this.parts, - required this.usingTemplate, - }); + const TemplateResolutionResult({required this.style, required this.parts}); } /// Resolves slide templates and styles from [DeckOptions]. @@ -125,7 +118,6 @@ class TemplateResolver { return TemplateResolutionResult( style: mergedStyle, parts: _resolveParts(template.parts, layout), - usingTemplate: true, ); } @@ -151,7 +143,6 @@ class TemplateResolver { return TemplateResolutionResult( style: mergedStyle, parts: _resolveParts(_options.parts, layout), - usingTemplate: false, ); } 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 544beecdf..953436bdf 100644 --- a/packages/superdeck/lib/src/markdown/builders/image_element_builder.dart +++ b/packages/superdeck/lib/src/markdown/builders/image_element_builder.dart @@ -90,11 +90,7 @@ class ImageElementBuilder extends MarkdownElementBuilder builder: (builderContext, spec) { Widget imageWidget = ConstrainedBox( constraints: BoxConstraints.tight(totalSize), - child: CachedImage( - uri: uri, - targetSize: totalSize, - styleSpec: styleSpec, - ), + child: CachedImage(uri: uri, styleSpec: styleSpec), ); return applyHeroIfNeeded( @@ -116,7 +112,6 @@ class ImageElementBuilder extends MarkdownElementBuilder constraints: BoxConstraints.tight(interpolatedSize), child: CachedImage( uri: displayUri, - targetSize: interpolatedSize, styleSpec: StyleSpec(spec: interpolatedSpec), ), ); diff --git a/packages/superdeck/lib/src/markdown/markdown_helpers.dart b/packages/superdeck/lib/src/markdown/markdown_helpers.dart index c8e2699db..91c68ac79 100644 --- a/packages/superdeck/lib/src/markdown/markdown_helpers.dart +++ b/packages/superdeck/lib/src/markdown/markdown_helpers.dart @@ -36,20 +36,11 @@ class LerpStringResult { required this.text, // fully visible prefix this.fadingChar, // single grapheme (may be null) this.fadeOpacity = 0.0, // 0..1 opacity for fadingChar - this.isFadingOut = false, // phase flag - this.ghostSuffix = '', // zero-opacity suffix to pin layout }); final String text; final String? fadingChar; final double fadeOpacity; - final bool isFadingOut; // ignore: unused-code - - /// Remainder drawn with alpha=0 to keep wrapping stable. - /// By rendering the full final string with invisible characters, we reserve - /// the correct line-wrapping layout even during transitions, preventing the - /// "last word flicker" bug where text position shifts as characters fade in. - final String ghostSuffix; // ignore: unused-code bool get hasFadingChar => fadingChar != null && fadeOpacity > 0.0; } @@ -58,11 +49,10 @@ class LerpStringResult { /// /// - First half (t<0.5): fade out the start suffix (left→right) /// - Second half (t>0.5): fade in the end suffix (left→right) -/// - Always returns: -/// * `text` (committed prefix), -/// * optional `fadingChar` + `fadeOpacity`, -/// * `ghostSuffix` (alpha=0) so total width is stable per phase. -/// +/// - Always returns `text` (committed prefix) plus an optional `fadingChar` +/// and `fadeOpacity`. Internally, the remainder of the source string past +/// the fading grapheme is tracked so that a whitespace fading grapheme can +/// be committed immediately without losing animation time. LerpStringResult lerpStringWithFade(String start, String end, double t) { t = t.clamp(0.0, 1.0); @@ -89,7 +79,6 @@ LerpStringResult lerpStringWithFade(String start, String end, double t) { String committed = prefix; String? fadingChar; double fadeOpacity = 0.0; - bool isFadingOut = false; List ghostSuffixG = const []; if (t < 0.5 && startSuffix.isNotEmpty) { @@ -104,7 +93,6 @@ LerpStringResult lerpStringWithFade(String start, String end, double t) { if (remaining < startSuffix.length) { fadingChar = startSuffix[remaining]; fadeOpacity = frac; // 0..1 - isFadingOut = true; // Reserve width for the rest of the start string after the fading grapheme. ghostSuffixG = startSuffix.skip(remaining + 1).toList(); } else { @@ -123,7 +111,6 @@ LerpStringResult lerpStringWithFade(String start, String end, double t) { if (added < endSuffix.length && frac > 0.0) { fadingChar = endSuffix[added]; fadeOpacity = frac; // 0..1 - isFadingOut = false; // Reserve width for what remains in the end string after the fading grapheme. ghostSuffixG = endSuffix.skip(added + 1).toList(); } else { @@ -135,7 +122,6 @@ LerpStringResult lerpStringWithFade(String start, String end, double t) { // Middle: nothing committed beyond prefix; show first end grapheme at 0 opacity. fadingChar = endSuffix.first; fadeOpacity = 0.0; - isFadingOut = false; ghostSuffixG = endSuffix.skip(1).toList(); } @@ -165,7 +151,5 @@ LerpStringResult lerpStringWithFade(String start, String end, double t) { text: committed, fadingChar: fadingChar, fadeOpacity: fadeOpacity.clamp(0.0, 1.0), - isFadingOut: isFadingOut, - ghostSuffix: ghostSuffixG.join(), ); } diff --git a/packages/superdeck/lib/src/rendering/blocks/markdown_render_scope.dart b/packages/superdeck/lib/src/rendering/blocks/markdown_render_scope.dart index d1fa05688..b32980dc3 100644 --- a/packages/superdeck/lib/src/rendering/blocks/markdown_render_scope.dart +++ b/packages/superdeck/lib/src/rendering/blocks/markdown_render_scope.dart @@ -23,15 +23,6 @@ class MarkdownRenderScope extends InheritedWidget { final MarkdownStyleSheet styleSheet; final md.ExtensionSet extensionSet; - // ignore: unused-code - static MarkdownRenderScope of(BuildContext context) { - final scope = maybeOf(context); - if (scope == null) { - throw FlutterError('MarkdownRenderScope not found in the widget tree.'); - } - return scope; - } - static MarkdownRenderScope? maybeOf(BuildContext context) { return context.dependOnInheritedWidgetOfExactType(); } diff --git a/packages/superdeck/lib/src/rendering/slides/header.dart b/packages/superdeck/lib/src/rendering/slides/header.dart index 6012c43df..9c89c1464 100644 --- a/packages/superdeck/lib/src/rendering/slides/header.dart +++ b/packages/superdeck/lib/src/rendering/slides/header.dart @@ -12,13 +12,13 @@ class HeaderPart extends StatelessWidget implements PreferredSizeWidget { final slide = SlideConfiguration.of(context); final index = slide.slideIndex; + final title = slide.options.title; return Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), child: Row( mainAxisAlignment: MainAxisAlignment.end, children: [ - Text(slide.options.title ?? 'Generative UI with Flutter'), - const SizedBox(width: 20), + if (title != null) ...[Text(title), const SizedBox(width: 20)], Text('${index + 1}'), ], ), diff --git a/packages/superdeck/lib/src/rendering/slides/slide_constants.dart b/packages/superdeck/lib/src/rendering/slides/slide_constants.dart index d059d29d5..46f15ac46 100644 --- a/packages/superdeck/lib/src/rendering/slides/slide_constants.dart +++ b/packages/superdeck/lib/src/rendering/slides/slide_constants.dart @@ -2,8 +2,5 @@ import 'package:flutter/widgets.dart'; import '../../utils/constants.dart'; -/// Fixed aspect ratio used by SuperDeck slides. -const double superDeckAspectRatio = kAspectRatio; - /// Fixed logical size used by SuperDeck slides. const Size superDeckSlideSize = kResolution; diff --git a/packages/superdeck/lib/src/ui/widgets/cache_image_widget.dart b/packages/superdeck/lib/src/ui/widgets/cache_image_widget.dart index 12ff43555..14479d306 100644 --- a/packages/superdeck/lib/src/ui/widgets/cache_image_widget.dart +++ b/packages/superdeck/lib/src/ui/widgets/cache_image_widget.dart @@ -39,13 +39,10 @@ ImageProvider getImageProvider(Uri uri) { class CachedImage extends StatefulWidget { final Uri uri; - final Size? targetSize; // ignore: unused-code - final StyleSpec styleSpec; const CachedImage({ super.key, - this.targetSize, required this.uri, this.styleSpec = const StyleSpec(spec: ImageSpec()), }); diff --git a/packages/superdeck/lib/src/ui/widgets/measure_size.dart b/packages/superdeck/lib/src/ui/widgets/measure_size.dart deleted file mode 100644 index 158344683..000000000 --- a/packages/superdeck/lib/src/ui/widgets/measure_size.dart +++ /dev/null @@ -1,247 +0,0 @@ -// Documented measurement utility with a dedicated test suite; production -// paths use other layout tools today. Keep for package consumers and tests. -// ignore_for_file: unused-code - -import 'package:flutter/rendering.dart'; -import 'package:flutter/scheduler.dart'; -import 'package:flutter/widgets.dart'; - -typedef OnMeasureChange = - void Function(Size size, BoxConstraints parentConstraints); - -/// RenderObject that measures size and constraints. -/// -/// This implementation follows Flutter best practices for size measurement: -/// - Uses [SchedulerBinding.instance.addPostFrameCallback] to defer callbacks -/// until after the render pipeline completes, preventing layout feedback loops -/// - Coalesces multiple layout passes within a single frame to avoid duplicate -/// notifications -/// - Guards against notifications after the render object detaches, preventing -/// stale callbacks to disposed listeners -/// - Reads [size] directly (safe even when child is null) rather than relying -/// on child?.size -/// -/// See also: -/// - The Widget Size Measurement Guide in docs/guides/widget-size-guide.mdx -/// - [SchedulerBinding.addPostFrameCallback] for callback timing -/// - [RenderProxyBox.performLayout] for layout pass integration -class _MeasureSizeRenderObject extends RenderProxyBox { - _MeasureSizeRenderObject({required this.onChange}); - - OnMeasureChange onChange; - - // Change detection and reporting: Track last reported size/constraints to - // determine when a new value should be notified - Size? _lastReportedSize; - BoxConstraints? _lastReportedConstraints; - - // Pending state: Store the latest values seen during layout to report - // in the post-frame callback - Size? _latestSize; - BoxConstraints? _latestConstraints; - - // Coalescing flag: Prevents scheduling multiple callbacks within the same frame - bool _callbackScheduled = false; - - @override - void performLayout() { - super.performLayout(); - - // Read size directly from the render object (safe even when child is null). - // This is more reliable than child?.size ?? Size.zero because it reflects - // the actual laid-out size of this render object. - final Size newSize = size; - final BoxConstraints newConstraints = constraints; - - // Update latest values and schedule callback if changed from last report - if (_lastReportedSize != newSize || - _lastReportedConstraints != newConstraints) { - _latestSize = newSize; - _latestConstraints = newConstraints; - _scheduleCallback(); - } - } - - /// Schedules a post-frame callback to notify listeners of size changes. - /// - /// This method implements callback coalescing: if a callback is already - /// scheduled for the current frame, it does nothing. Multiple layout passes - /// within a frame will update [_latestSize] and [_latestConstraints] but - /// only trigger a single notification. - /// - /// The callback is deferred until after the frame completes to prevent - /// "setState() or markNeedsLayout() called during layout" errors. - void _scheduleCallback() { - if (_callbackScheduled) return; - _callbackScheduled = true; - - SchedulerBinding.instance.addPostFrameCallback((_) { - _callbackScheduled = false; - - // Guard against notifications after the render object has been removed - // from the tree. This prevents stale callbacks to disposed listeners. - if (!attached) return; - - // Verify we have pending values to report - final s = _latestSize; - final c = _latestConstraints; - if (s == null || c == null) return; - - // Extra safety: check if values actually changed from last report - // This prevents duplicate notifications if the same values appear again - if (_lastReportedSize == s && _lastReportedConstraints == c) return; - - // Update tracking and fire callback - _lastReportedSize = s; - _lastReportedConstraints = c; - onChange(s, c); - }); - } - - @override - void detach() { - // Cancel any pending callback when the render object is removed from the tree. - // This prevents callbacks from firing after disposal. - _callbackScheduled = false; - super.detach(); - } - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties.add( - DiagnosticsProperty('lastReportedSize', _lastReportedSize), - ); - properties.add( - DiagnosticsProperty( - 'lastReportedConstraints', - _lastReportedConstraints, - ), - ); - properties.add( - FlagProperty( - 'callbackScheduled', - value: _callbackScheduled, - ifTrue: 'callback pending', - ifFalse: 'no callback pending', - ), - ); - } -} - -/// Widget that measures its child's laid-out size and parent constraints. -/// -/// [MeasureSize] wraps a child widget and notifies [onChange] with the child's -/// final [Size] and the parent [BoxConstraints] (constraints applied to this -/// widget) after each frame where they change. -/// -/// This widget is useful when you need to know the actual rendered dimensions -/// of a widget after layout completes, such as for: -/// - Positioning overlays or tooltips relative to measured content -/// - Analytics or debugging (tracking widget sizes) -/// - Coordinating layouts across disconnected widget subtrees -/// -/// ## When NOT to use MeasureSize -/// -/// Consider these built-in alternatives before using [MeasureSize]: -/// -/// - **[LayoutBuilder]** - When you need parent constraints (not final size) -/// - **[SizeChangedLayoutNotifier]** - When you only need change notifications -/// - **[AnimatedSize]** - For smooth size transitions -/// - **Custom layout delegates** - When you control both parent and children -/// - **[CompositedTransformFollower]** - For anchoring overlays -/// -/// See the Widget Size Measurement Guide (docs/guides/widget-size-guide.mdx) -/// for detailed guidance on when measurement is appropriate. -/// -/// ## Implementation Details -/// -/// - Notifications are deferred to post-frame callbacks, preventing layout -/// feedback loops -/// - Multiple layout passes within a frame are coalesced into a single -/// notification -/// - Callbacks are not fired after the widget is disposed -/// - The measured size is always available, even when the child is null -/// (reports [Size.zero]) -/// -/// ## Example -/// -/// ```dart -/// class ResizablePanel extends StatefulWidget { -/// @override -/// State createState() => _ResizablePanelState(); -/// } -/// -/// class _ResizablePanelState extends State { -/// Size? _size; -/// -/// @override -/// Widget build(BuildContext context) { -/// return Column( -/// children: [ -/// Text('Panel size: ${_size ?? "measuring..."}'), -/// MeasureSize( -/// onChange: (size, parentConstraints) => setState(() => _size = size), -/// child: Container( -/// padding: EdgeInsets.all(16), -/// child: Text('Dynamic content'), -/// ), -/// ), -/// ], -/// ); -/// } -/// } -/// ``` -/// -/// See also: -/// - [LayoutBuilder] for accessing parent constraints during build -/// - [SizeChangedLayoutNotifier] for size change notifications without values -/// - The Widget Size Measurement Guide for best practices -class MeasureSize extends SingleChildRenderObjectWidget { - /// Called when the child size or parent constraints change. - /// - /// Receives the laid-out [Size] of the child and the parent [BoxConstraints] - /// that were applied to this [MeasureSize] widget. - /// - /// This callback is invoked after the frame completes, ensuring it's safe - /// to call [setState] or trigger other state updates without causing layout - /// feedback loops. - /// - /// The callback fires: - /// - Once on the first layout (establishing initial size) - /// - Whenever the size or parent constraints change - /// - At most once per frame (even if layout runs multiple times) - /// - /// The callback will NOT fire: - /// - After the widget is disposed - /// - When the size and parent constraints remain unchanged - final OnMeasureChange onChange; - - /// Creates a widget that measures its child's size and constraints. - /// - /// The [onChange] callback is required and will be invoked after each frame - /// where the child's size or constraints change. - /// - /// The [child] is required. If you need to conditionally render content, - /// wrap [MeasureSize] in a conditional or provide a [SizedBox.shrink] as - /// the child (which will report [Size.zero]). - const MeasureSize({ - super.key, - required this.onChange, - required Widget super.child, - }); - - @override - RenderObject createRenderObject(BuildContext context) { - return _MeasureSizeRenderObject(onChange: onChange); - } - - @override - void updateRenderObject( - BuildContext context, - covariant RenderObject renderObject, - ) { - final measureRenderObject = renderObject as _MeasureSizeRenderObject; - measureRenderObject.onChange = onChange; - } -} diff --git a/packages/superdeck/lib/src/ui/widgets/resolved_asset_image.dart b/packages/superdeck/lib/src/ui/widgets/resolved_asset_image.dart index dd738917d..1b74b206f 100644 --- a/packages/superdeck/lib/src/ui/widgets/resolved_asset_image.dart +++ b/packages/superdeck/lib/src/ui/widgets/resolved_asset_image.dart @@ -116,11 +116,7 @@ class _ResolvedAssetImageState extends State { return SizedBox.fromSize(size: widget.targetSize); } final uri = snapshot.data ?? widget.fallback; - return CachedImage( - uri: uri, - targetSize: widget.targetSize, - styleSpec: widget.styleSpec, - ); + return CachedImage(uri: uri, styleSpec: widget.styleSpec); }, ); } diff --git a/packages/superdeck/pubspec.yaml b/packages/superdeck/pubspec.yaml index 5f29cc26d..dd2e5d3bc 100644 --- a/packages/superdeck/pubspec.yaml +++ b/packages/superdeck/pubspec.yaml @@ -1,7 +1,7 @@ name: superdeck description: A Flutter presentation framework that renders slides written in Markdown. version: 1.0.0 -homepage: https://github.com/btwld/superdeck +homepage: https://github.com/conceptadev/superdeck documentation: https://docs.page/btwld/superdeck environment: @@ -33,7 +33,6 @@ dependencies: webview_flutter: ^4.10.0 webview_flutter_web: ^0.2.3 google_fonts: ^8.1.0 - meta: ^1.16.0 qr_flutter: ^4.1.0 signals: ^7.1.0 signals_flutter: ^7.1.0 @@ -41,7 +40,6 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - mocktail: ^1.0.4 flutter_lints: ^6.0.0 dart_code_metrics_presets: ^2.19.0 build_runner: ^2.5.4 @@ -53,5 +51,4 @@ dev_dependencies: flutter: uses-material-design: true assets: - - assets/ - assets/grammars/ diff --git a/packages/superdeck/test/fixtures/complete_config.yaml b/packages/superdeck/test/fixtures/complete_config.yaml deleted file mode 100644 index b4a63a19e..000000000 --- a/packages/superdeck/test/fixtures/complete_config.yaml +++ /dev/null @@ -1,8 +0,0 @@ -transition: - type: fade_in - duration: 500 - curve: ease_in_out - delay: 300 -background: background.jpg -style: custom - \ No newline at end of file diff --git a/packages/superdeck/test/fixtures/empty_config.yaml b/packages/superdeck/test/fixtures/empty_config.yaml deleted file mode 100644 index e69de29bb..000000000 diff --git a/packages/superdeck/test/goldens/block_aligned_center.png b/packages/superdeck/test/goldens/block_aligned_center.png index 4d0bb2d1d..0aa16cd91 100644 Binary files a/packages/superdeck/test/goldens/block_aligned_center.png and b/packages/superdeck/test/goldens/block_aligned_center.png differ diff --git a/packages/superdeck/test/goldens/decorated_block_frame.png b/packages/superdeck/test/goldens/decorated_block_frame.png index 9a0abae1d..c98ffe19c 100644 Binary files a/packages/superdeck/test/goldens/decorated_block_frame.png and b/packages/superdeck/test/goldens/decorated_block_frame.png differ diff --git a/packages/superdeck/test/goldens/image_scale_framing.png b/packages/superdeck/test/goldens/image_scale_framing.png index 72e3f978e..fa54677d0 100644 Binary files a/packages/superdeck/test/goldens/image_scale_framing.png and b/packages/superdeck/test/goldens/image_scale_framing.png differ diff --git a/packages/superdeck/test/goldens/section_two_blocks.png b/packages/superdeck/test/goldens/section_two_blocks.png index bdc4219e6..8eabe0ae4 100644 Binary files a/packages/superdeck/test/goldens/section_two_blocks.png and b/packages/superdeck/test/goldens/section_two_blocks.png differ diff --git a/packages/superdeck/test/helpers/test_helpers.dart b/packages/superdeck/test/helpers/test_helpers.dart index a0499dec5..d02abfa07 100644 --- a/packages/superdeck/test/helpers/test_helpers.dart +++ b/packages/superdeck/test/helpers/test_helpers.dart @@ -35,27 +35,6 @@ List createTestSlides(int count) { ); } -/// Creates a test slide configuration with custom content. -SlideConfiguration createTestSlide({ - required int index, - String? content, - SlideStyler? style, - String? thumbnailKey, -}) { - final slideKey = 'slide-$index'; - return SlideConfiguration( - slideIndex: index, - style: style ?? SlideStyler(), - slide: Slide( - key: slideKey, - sections: [ - SectionBlock([ContentBlock(content ?? 'Test slide $index content')]), - ], - ), - thumbnailKey: thumbnailKey ?? buildThumbnailKey(slideKey), - ); -} - /// Creates a test slide payload with the given slides. List createTestSlidesPayload({List? slides}) { return slides ?? @@ -70,53 +49,10 @@ List createTestSlidesPayload({List? slides}) { ); } -/// Pumps a widget and settles all animations. -Future pumpAndSettleWidget( - WidgetTester tester, - Widget widget, { - Duration? duration, -}) async { - await tester.pumpWidget(widget); - if (duration != null) { - await tester.pumpAndSettle(duration); - } else { - await tester.pumpAndSettle(); - } -} - -/// Finds a widget by its key. -Finder findByKey(String key) => find.byKey(Key(key)); - -/// Finds a widget by its text content. -Finder findByText(String text) => find.text(text); - -/// Verifies that a widget exists and is visible. -void expectWidgetVisible(Finder finder) { - expect(finder, findsOneWidget); -} - -/// Verifies that a widget does not exist. -void expectWidgetNotFound(Finder finder) { - expect(finder, findsNothing); -} - int _testSlideId = 0; String _nextKey(String prefix) => '$prefix-${_testSlideId++}'; -/// Creates a Slide from a list of sections for inline test setup. -Slide createSlideFromSections( - List sections, { - String? key, - SlideOptions? options, -}) { - return Slide( - key: key ?? _nextKey('test-slide'), - sections: sections, - options: options, - ); -} - /// Creates a simple single-section slide with given blocks. Slide createSlideFromBlocks( List blocks, { @@ -129,21 +65,6 @@ Slide createSlideFromBlocks( ); } -/// Creates a content block with common defaults. -ContentBlock createContentBlock( - String content, { - int flex = 1, - ContentAlignment? align, - bool scrollable = false, -}) { - return ContentBlock( - content, - flex: flex, - align: align, - scrollable: scrollable, - ); -} - extension WidgetTesterX on WidgetTester { Future pumpWithScaffold(Widget widget) async { await pumpWidget(MaterialApp(home: Scaffold(body: widget))); diff --git a/packages/superdeck/test/public_api_test.dart b/packages/superdeck/test/public_api_test.dart index 04f396d3f..fcd4c18e0 100644 --- a/packages/superdeck/test/public_api_test.dart +++ b/packages/superdeck/test/public_api_test.dart @@ -12,7 +12,6 @@ void main() { expect(AsyncThumbnail, isNotNull); expect(ThumbnailService, isNotNull); expect(superDeckSlideSize, isNotNull); - expect(superDeckAspectRatio, isNotNull); expect(DeckLoader, isNotNull); expect(DeckWorkspace, isNotNull); expect(FileDeckLoader, isNotNull); diff --git a/packages/superdeck/test/src/builtins/image_widget_test.dart b/packages/superdeck/test/src/builtins/image_widget_test.dart index f4d98ee7e..230b8c07d 100644 --- a/packages/superdeck/test/src/builtins/image_widget_test.dart +++ b/packages/superdeck/test/src/builtins/image_widget_test.dart @@ -225,7 +225,6 @@ void main() { final image = tester.widget(find.byType(CachedImage)); expect(image.uri.scheme, 'data'); - expect(image.targetSize, size); final explicitSize = tester.widget( find diff --git a/packages/superdeck/test/src/deck/navigation_events_test.dart b/packages/superdeck/test/src/deck/navigation_events_test.dart index 9d32c5750..f6706ff34 100644 --- a/packages/superdeck/test/src/deck/navigation_events_test.dart +++ b/packages/superdeck/test/src/deck/navigation_events_test.dart @@ -15,17 +15,6 @@ void main() { final event = PreviousSlideEvent(); expect(event, isA()); }); - - test('GoToSlideEvent stores index', () { - final event = GoToSlideEvent(5); - expect(event, isA()); - expect(event.index, 5); - }); - - test('GoToSlideEvent stores zero index', () { - final event = GoToSlideEvent(0); - expect(event.index, 0); - }); }); group('KeyboardNavigationHandler', () { diff --git a/packages/superdeck/test/src/deck/template_resolver_test.dart b/packages/superdeck/test/src/deck/template_resolver_test.dart index d6301d9b0..c9bf70d51 100644 --- a/packages/superdeck/test/src/deck/template_resolver_test.dart +++ b/packages/superdeck/test/src/deck/template_resolver_test.dart @@ -17,7 +17,6 @@ void main() { final result = resolver.resolve(null); - expect(result.usingTemplate, isFalse); expect(result.parts, options.parts); expect(result.style, defaultSlideStyle.merge(baseStyle).merge(null)); }, @@ -31,7 +30,6 @@ void main() { final result = resolver.resolve(null); - expect(result.usingTemplate, isFalse); expect(result.style, defaultSlideStyle.merge(null).merge(null)); expect(result.parts, options.parts); }, @@ -47,7 +45,6 @@ void main() { final result = resolver.resolve(slideOptions); - expect(result.usingTemplate, isFalse); expect(result.style, defaultSlideStyle.merge(null).merge(namedStyle)); }, ); @@ -202,7 +199,6 @@ void main() { final result = resolver.resolve(slideOptions); - expect(result.usingTemplate, isTrue); expect(result.parts, template.parts); expect( result.style, @@ -218,7 +214,6 @@ void main() { final result = resolver.resolve(slideOptions); - expect(result.usingTemplate, isTrue); expect(result.style, defaultSlideStyle.merge(null).merge(null)); }); @@ -231,7 +226,6 @@ void main() { final result = resolver.resolve(slideOptions); - expect(result.usingTemplate, isTrue); expect( result.style, defaultSlideStyle.merge(null).merge(templateStyle), @@ -341,7 +335,6 @@ void main() { final result = resolver.resolve(null); - expect(result.usingTemplate, isTrue); expect(result.parts, defaultTemplate.parts); expect( result.style, @@ -367,7 +360,6 @@ void main() { final result = resolver.resolve(slideOptions); - expect(result.usingTemplate, isTrue); expect(result.parts, explicitTemplate.parts); expect( result.style, @@ -383,7 +375,6 @@ void main() { final result = resolver.resolve(slideOptions); - expect(result.usingTemplate, isTrue); expect(result.parts, defaultTemplate.parts); }); @@ -399,7 +390,6 @@ void main() { final result = resolver.resolve(slideOptions); - expect(result.usingTemplate, isFalse); expect(result.parts, options.parts); expect(result.style, defaultSlideStyle.merge(baseStyle).merge(null)); }); @@ -416,7 +406,6 @@ void main() { final result = resolver.resolve(slideOptions); - expect(result.usingTemplate, isFalse); expect(result.style, defaultSlideStyle.merge(null).merge(deckStyle)); }); @@ -521,60 +510,30 @@ void main() { }, ); - test('options.baseStyle is not applied when a named template is used', () { - final optionsBaseStyle = SlideStyler(); - final templateBase = SlideStyler(); - final template = SlideTemplate(baseStyle: templateBase); - final options = DeckOptions( - baseStyle: optionsBaseStyle, - templates: {'t': template}, - ); - final resolver = TemplateResolver(options); - final slideOptions = SlideOptions(template: 't'); - - final result = resolver.resolve(slideOptions); - - // Template path: usingTemplate=true, parts from template (not options) - expect(result.usingTemplate, isTrue); - expect(result.parts, template.parts); - - // Without template: options.parts would be used - final noTemplateResult = TemplateResolver( - DeckOptions(baseStyle: optionsBaseStyle), - ).resolve(null); - expect(noTemplateResult.usingTemplate, isFalse); - expect(noTemplateResult.parts, options.parts); - }); - }); - - group('usingTemplate flag', () { - test('is false when no template and no defaultTemplate', () { - final options = DeckOptions(); - final resolver = TemplateResolver(options); - - final result = resolver.resolve(null); - - expect(result.usingTemplate, isFalse); - }); - - test('is true when explicit template is resolved', () { - final options = DeckOptions(templates: {'t': SlideTemplate()}); - final resolver = TemplateResolver(options); - final slideOptions = SlideOptions(template: 't'); - - final result = resolver.resolve(slideOptions); - - expect(result.usingTemplate, isTrue); - }); + test( + 'options.baseStyle is not applied when a named template is used', + () { + final optionsBaseStyle = SlideStyler(); + final templateBase = SlideStyler(); + final template = SlideTemplate(baseStyle: templateBase); + final options = DeckOptions( + baseStyle: optionsBaseStyle, + templates: {'t': template}, + ); + final resolver = TemplateResolver(options); + final slideOptions = SlideOptions(template: 't'); - test('is true when defaultTemplate is used', () { - final options = DeckOptions(defaultTemplate: SlideTemplate()); - final resolver = TemplateResolver(options); + final result = resolver.resolve(slideOptions); - final result = resolver.resolve(null); + expect(result.parts, template.parts); - expect(result.usingTemplate, isTrue); - }); + // Without template: options.parts would be used + final noTemplateResult = TemplateResolver( + DeckOptions(baseStyle: optionsBaseStyle), + ).resolve(null); + expect(noTemplateResult.parts, options.parts); + }, + ); }); group('Parts resolution', () { @@ -617,9 +576,7 @@ void main() { void expectFullscreenChrome( TemplateResolutionResult result, { required Widget background, - required bool usingTemplate, }) { - expect(result.usingTemplate, usingTemplate); expect(result.parts.header, isNull); expect(result.parts.footer, isNull); expect(result.parts.background, same(background)); @@ -635,7 +592,6 @@ void main() { SlideOptions(layout: SlideLayout.normal), ); - expect(normal.usingTemplate, omitted.usingTemplate); expect(normal.parts, same(omitted.parts)); expect(normal.style, omitted.style); }); @@ -652,7 +608,6 @@ void main() { expectFullscreenChrome( resolver.resolve(SlideOptions(layout: SlideLayout.fullscreen)), background: background, - usingTemplate: false, ); }, ); @@ -672,7 +627,6 @@ void main() { SlideOptions(template: 'cover', layout: SlideLayout.fullscreen), ), background: background, - usingTemplate: true, ); }, ); @@ -691,7 +645,6 @@ void main() { expectFullscreenChrome( resolver.resolve(SlideOptions(layout: SlideLayout.fullscreen)), background: background, - usingTemplate: true, ); }, ); @@ -711,7 +664,6 @@ void main() { SlideOptions(template: 'none', layout: SlideLayout.fullscreen), ), background: background, - usingTemplate: false, ); }, ); diff --git a/packages/superdeck/test/src/markdown/hero_transition_test.dart b/packages/superdeck/test/src/markdown/hero_transition_test.dart index 99706688e..8581a86ae 100644 --- a/packages/superdeck/test/src/markdown/hero_transition_test.dart +++ b/packages/superdeck/test/src/markdown/hero_transition_test.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:superdeck/src/deck/slide_configuration.dart'; import 'package:superdeck/src/rendering/slides/slide_view.dart'; -import 'package:superdeck/src/ui/widgets/cache_image_widget.dart'; import 'package:superdeck/src/ui/widgets/hero_element.dart'; import 'package:superdeck/src/ui/widgets/provider.dart'; import 'package:superdeck_core/superdeck_core.dart'; @@ -95,19 +94,6 @@ void main() { await tester.pump(_transitionDuration ~/ 2); expect(tester.takeException(), isNull); - final targetSizes = tester - .widgetList(find.byType(CachedImage)) - .map((image) => image.targetSize) - .whereType(); - final shuttleSize = targetSizes.singleWhere( - (size) => - size.width > toData.size.width && - size.width < fromData.size.width && - size.height > toData.size.height && - size.height < fromData.size.height, - ); - expect(shuttleSize.width.isFinite, isTrue); - expect(shuttleSize.height.isFinite, isTrue); await tester.pumpAndSettle(); diff --git a/packages/superdeck/test/src/markdown/markdown_helpers_test.dart b/packages/superdeck/test/src/markdown/markdown_helpers_test.dart index 8f4c3c3e5..da75bf36a 100644 --- a/packages/superdeck/test/src/markdown/markdown_helpers_test.dart +++ b/packages/superdeck/test/src/markdown/markdown_helpers_test.dart @@ -234,7 +234,6 @@ void main() { // At midpoint, first character of end suffix starts fading in (at opacity 0) expect(result.fadingChar, equals('p')); expect(result.fadeOpacity, equals(0.0)); - expect(result.isFadingOut, isFalse); }); test('at t=1.0 returns end string fully visible', () { @@ -260,7 +259,6 @@ void main() { final result = lerpStringWithFade('ABC', 'XYZ', 0.25); expect(result.hasFadingChar, isTrue); - expect(result.isFadingOut, isTrue); expect(result.fadeOpacity, greaterThan(0.0)); expect(result.fadeOpacity, lessThan(1.0)); }); @@ -296,7 +294,6 @@ void main() { final result = lerpStringWithFade('ABC', 'XYZ', 0.75); expect(result.hasFadingChar, isTrue); - expect(result.isFadingOut, isFalse); expect(result.fadeOpacity, greaterThan(0.0)); expect(result.fadeOpacity, lessThan(1.0)); }); @@ -336,7 +333,6 @@ void main() { reason: 'Next non-space grapheme should start fading immediately', ); expect(result.hasFadingChar, isTrue); - expect(result.ghostSuffix, equals('I')); }); }); diff --git a/packages/superdeck/test/src/markdown/mermaid/vendor/flowchart_strict_parser_test.dart b/packages/superdeck/test/src/markdown/mermaid/vendor/flowchart_strict_parser_test.dart new file mode 100644 index 000000000..98186e399 --- /dev/null +++ b/packages/superdeck/test/src/markdown/mermaid/vendor/flowchart_strict_parser_test.dart @@ -0,0 +1,56 @@ +import 'dart:io'; + +import 'package:flutter_test/flutter_test.dart'; + +/// Path (relative to the `superdeck` package root) of the vendored parser. +const _vendorFilePath = + 'lib/src/markdown/mermaid/vendor/flowchart_strict_parser.dart'; + +/// Path (relative to the `superdeck` package root) of the workspace lockfile +/// that pins the resolved `mermaid_core` version. +const _workspaceLockfilePath = '../../pubspec.lock'; + +final _headerVersionPattern = RegExp(r'Vendored from mermaid_core ([\d.]+),'); + +final _lockfileVersionPattern = RegExp( + r'\n mermaid_core:[\s\S]*?\n version: "([\d.]+)"', +); + +void main() { + test( + 'vendored flowchart parser header matches the resolved mermaid_core version', + () { + final headerVersion = _headerVersionPattern + .firstMatch(File(_vendorFilePath).readAsStringSync()) + ?.group(1); + expect( + headerVersion, + isNotNull, + reason: + 'Could not find a "Vendored from mermaid_core X.Y.Z" header ' + 'comment in $_vendorFilePath.', + ); + + final resolvedVersion = _lockfileVersionPattern + .firstMatch(File(_workspaceLockfilePath).readAsStringSync()) + ?.group(1); + expect( + resolvedVersion, + isNotNull, + reason: + 'Could not find a resolved mermaid_core version in ' + '$_workspaceLockfilePath.', + ); + + expect( + headerVersion, + resolvedVersion, + reason: + 'The workspace resolves mermaid_core $resolvedVersion but ' + '$_vendorFilePath was forked from mermaid_core $headerVersion. ' + 'Re-sync the fork against the resolved version and update the ' + 'header comment.', + ); + }, + ); +} diff --git a/packages/superdeck/test/src/utils/measure_size_test.dart b/packages/superdeck/test/src/utils/measure_size_test.dart deleted file mode 100644 index 317b00df4..000000000 --- a/packages/superdeck/test/src/utils/measure_size_test.dart +++ /dev/null @@ -1,494 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:superdeck/src/ui/widgets/measure_size.dart'; - -import '../../helpers/test_helpers.dart'; - -void main() { - group('MeasureSize', () { - group('Basic Functionality', () { - testWidgets('calls onChange with correct size when child size changes', ( - WidgetTester tester, - ) async { - Size? size; - await tester.pumpWithScaffold( - MeasureSize( - onChange: (newSize, parentConstraints) => size = newSize, - child: const SizedBox(width: 100, height: 100), - ), - ); - - expect(size, equals(const Size(100, 100))); - - await tester.pumpWithScaffold( - MeasureSize( - onChange: (newSize, parentConstraints) => size = newSize, - child: const SizedBox(width: 200, height: 200), - ), - ); - - expect(size, equals(const Size(200, 200))); - }); - - testWidgets('does not call onChange when child size remains the same', ( - WidgetTester tester, - ) async { - int onChangeCalls = 0; - await tester.pumpWithScaffold( - MeasureSize( - onChange: (newSize, parentConstraints) => onChangeCalls++, - child: const SizedBox(width: 100, height: 100), - ), - ); - - expect(onChangeCalls, equals(1)); - - await tester.pumpWithScaffold( - MeasureSize( - onChange: (newSize, parentConstraints) => onChangeCalls++, - child: const SizedBox(width: 100, height: 100), - ), - ); - - expect(onChangeCalls, equals(1)); - }); - - testWidgets('calls onChange with Size.zero when child is null', ( - WidgetTester tester, - ) async { - Size? size; - await tester.pumpWithScaffold( - MeasureSize( - onChange: (newSize, parentConstraints) => size = newSize, - child: const SizedBox.shrink(), - ), - ); - - expect(size, equals(Size.zero)); - }); - - testWidgets('provides both size and parent constraints in onChange', ( - WidgetTester tester, - ) async { - BoxConstraints? parentConstraints; - Size? size; - - await tester.pumpWithScaffold( - MeasureSize( - onChange: (newSize, newParentConstraints) { - size = newSize; - parentConstraints = newParentConstraints; - }, - child: const SizedBox(width: 100, height: 100), - ), - ); - - expect(size, equals(const Size(100, 100))); - expect(parentConstraints, isNotNull); - expect(parentConstraints!.maxWidth, greaterThan(0)); - expect(parentConstraints!.maxHeight, greaterThan(0)); - }); - - testWidgets('provides parent constraint utility methods', ( - WidgetTester tester, - ) async { - BoxConstraints? parentConstraints; - - await tester.pumpWithScaffold( - MeasureSize( - onChange: (size, newParentConstraints) { - parentConstraints = newParentConstraints; - }, - child: const SizedBox(width: 150, height: 150), - ), - ); - - expect(parentConstraints, isNotNull); - expect(parentConstraints!.maxWidth.isInfinite, isFalse); - expect(parentConstraints!.maxHeight.isInfinite, isFalse); - expect(parentConstraints!.isTight, isFalse); - }); - }); - - group('Callback Coalescing', () { - testWidgets( - 'coalesces multiple layout passes within a frame into single callback', - (WidgetTester tester) async { - int callCount = 0; - final sizes = []; - - // Create a widget that triggers multiple layouts by using setState - await tester.pumpWidget( - StatefulBuilder( - builder: (context, setState) { - return Directionality( - textDirection: TextDirection.ltr, - child: Center( - child: MeasureSize( - onChange: (size, parentConstraints) { - callCount++; - sizes.add(size); - }, - child: const SizedBox(width: 100, height: 100), - ), - ), - ); - }, - ), - ); - - // Initial layout should trigger one callback - expect(callCount, equals(1)); - expect(sizes.last, equals(const Size(100, 100))); - - // Pump another frame with the same widget - should NOT trigger callback - await tester.pump(); - expect( - callCount, - equals(1), - reason: 'Same size should not trigger callback', - ); - }, - ); - - testWidgets('detects constraint changes that affect final size', ( - WidgetTester tester, - ) async { - final sizes = []; - - await tester.pumpWithScaffold( - SizedBox( - width: 200, - height: 200, - child: MeasureSize( - onChange: (size, parentConstraints) { - sizes.add(size); - }, - // SizedBox.expand fills available space - child: const SizedBox.expand(), - ), - ), - ); - - expect(sizes.length, equals(1)); - expect(sizes.first.width, equals(200)); - expect(sizes.first.height, equals(200)); - - // Change parent constraints - this should trigger size change - await tester.pumpWithScaffold( - SizedBox( - width: 300, - height: 300, - child: MeasureSize( - onChange: (size, parentConstraints) { - sizes.add(size); - }, - child: const SizedBox.expand(), - ), - ), - ); - - // Should fire again due to size change from constraint change - expect(sizes.length, equals(2)); - expect(sizes.last.width, equals(300)); - expect(sizes.last.height, equals(300)); - }); - }); - - group('Detachment Guards', () { - testWidgets('does not fire callback after widget is disposed', ( - WidgetTester tester, - ) async { - int callCount = 0; - bool widgetPresent = true; - - await tester.pumpWidget( - StatefulBuilder( - builder: (context, setState) { - return Directionality( - textDirection: TextDirection.ltr, - child: Center( - child: widgetPresent - ? MeasureSize( - onChange: (size, parentConstraints) { - callCount++; - }, - child: const SizedBox(width: 100, height: 100), - ) - : const SizedBox.shrink(), - ), - ); - }, - ), - ); - - expect(callCount, equals(1), reason: 'Initial callback fired'); - - // Remove the widget from the tree - widgetPresent = false; - await tester.pumpWidget( - const Directionality( - textDirection: TextDirection.ltr, - child: Center(child: SizedBox.shrink()), - ), - ); - - // Pump additional frames to ensure no stale callbacks fire - await tester.pump(); - await tester.pump(); - - expect( - callCount, - equals(1), - reason: 'No additional callbacks after disposal', - ); - }); - - testWidgets('handles rapid mount/unmount cycles safely', ( - WidgetTester tester, - ) async { - int callCount = 0; - - for (int i = 0; i < 5; i++) { - // Mount - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: MeasureSize( - onChange: (size, parentConstraints) { - callCount++; - }, - child: const SizedBox(width: 100, height: 100), - ), - ), - ); - - // Unmount - await tester.pumpWidget( - const Directionality( - textDirection: TextDirection.ltr, - child: SizedBox.shrink(), - ), - ); - } - - // Should have exactly 5 callbacks (one per mount) - expect(callCount, equals(5)); - }); - }); - - group('Edge Cases', () { - testWidgets('handles zero-sized children correctly', ( - WidgetTester tester, - ) async { - Size? size; - await tester.pumpWithScaffold( - MeasureSize( - onChange: (newSize, parentConstraints) => size = newSize, - child: const SizedBox.shrink(), - ), - ); - - expect(size, equals(Size.zero)); - }); - - testWidgets('handles animated size changes', (WidgetTester tester) async { - final sizes = []; - double width = 100; - - await tester.pumpWidget( - StatefulBuilder( - builder: (context, setState) { - return Directionality( - textDirection: TextDirection.ltr, - child: Center( - child: MeasureSize( - onChange: (size, parentConstraints) { - sizes.add(size); - }, - child: SizedBox(width: width, height: 100), - ), - ), - ); - }, - ), - ); - - expect(sizes.length, equals(1)); - expect(sizes.last.width, equals(100)); - - // Animate width change - width = 200; - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: Center( - child: MeasureSize( - onChange: (size, parentConstraints) { - sizes.add(size); - }, - child: SizedBox(width: width, height: 100), - ), - ), - ), - ); - - expect(sizes.length, equals(2)); - expect(sizes.last.width, equals(200)); - }); - - testWidgets('works correctly with LayoutBuilder parent', ( - WidgetTester tester, - ) async { - Size? measuredSize; - BoxConstraints? measuredConstraints; - BoxConstraints? parentConstraints; - - await tester.pumpWithScaffold( - Center( - child: LayoutBuilder( - builder: (context, constraints) { - parentConstraints = constraints; - return MeasureSize( - onChange: (size, childConstraints) { - measuredSize = size; - measuredConstraints = childConstraints; - }, - child: const SizedBox(width: 100, height: 100), - ); - }, - ), - ), - ); - - expect(measuredSize, isNotNull); - expect(measuredConstraints, isNotNull); - expect(parentConstraints, isNotNull); - - // MeasureSize measures its own size, which matches its child - expect(measuredSize!.width, equals(100)); - expect(measuredSize!.height, equals(100)); - - // Verify it received proper constraints from LayoutBuilder - expect(measuredConstraints, isNotNull); - }); - - testWidgets('reports size changes during constraint changes', ( - WidgetTester tester, - ) async { - final events = []; - - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: SizedBox( - width: 200, - height: 200, - child: MeasureSize( - onChange: (size, parentConstraints) { - events.add('${size.width}x${size.height}'); - }, - child: const SizedBox.expand(), - ), - ), - ), - ), - ); - - expect(events.length, equals(1)); - expect(events.first, equals('200.0x200.0')); - - // Change parent size - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: SizedBox( - width: 300, - height: 300, - child: MeasureSize( - onChange: (size, parentConstraints) { - events.add('${size.width}x${size.height}'); - }, - child: const SizedBox.expand(), - ), - ), - ), - ), - ); - - expect(events.length, equals(2)); - expect(events.last, equals('300.0x300.0')); - }); - }); - - group('RenderObject Integration', () { - testWidgets('updateRenderObject updates onChange callback', ( - WidgetTester tester, - ) async { - int firstCallbackCount = 0; - int secondCallbackCount = 0; - - void firstCallback(Size size, BoxConstraints constraints) { - firstCallbackCount++; - } - - void secondCallback(Size size, BoxConstraints constraints) { - secondCallbackCount++; - } - - // Initial render with first callback - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: Center( - child: MeasureSize( - onChange: firstCallback, - child: const SizedBox(width: 100, height: 100), - ), - ), - ), - ); - - expect(firstCallbackCount, equals(1)); - expect(secondCallbackCount, equals(0)); - - // Update to use second callback with same size - // The second callback should NOT fire because size hasn't changed - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: Center( - child: MeasureSize( - onChange: secondCallback, - child: const SizedBox(width: 100, height: 100), - ), - ), - ), - ); - - // First callback should not be called again - expect(firstCallbackCount, equals(1)); - // Second callback should not fire yet (size unchanged from last report) - expect(secondCallbackCount, equals(0)); - - // Change size to trigger the new callback - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: Center( - child: MeasureSize( - onChange: secondCallback, - child: const SizedBox(width: 200, height: 200), - ), - ), - ), - ); - - // First callback still at 1 - expect(firstCallbackCount, equals(1)); - // Second callback fires now due to size change - expect(secondCallbackCount, equals(1)); - }); - }); - }); -} diff --git a/pubspec.lock b/pubspec.lock index a89ecd1f3..22f36559f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -571,14 +571,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.12" - flutter_math_fork: - dependency: transitive - description: - name: flutter_math_fork - sha256: "6d5f2f1aa57ae539ffb0a04bb39d2da67af74601d685a161aff7ce5bda5fa407" - url: "https://pub.dev" - source: hosted - version: "0.7.4" flutter_shaders: dependency: transitive description: @@ -587,14 +579,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.1.3" - flutter_svg: - dependency: transitive - description: - name: flutter_svg - sha256: "35882981abcbfb8c15b286f0cd690ff25bac12d95eff3e25ee207f37d4c42e7f" - url: "https://pub.dev" - source: hosted - version: "2.3.0" flutter_test: dependency: transitive description: flutter @@ -650,14 +634,6 @@ packages: url: "https://pub.dev" source: hosted version: "17.3.0" - golden_toolkit: - dependency: transitive - description: - name: golden_toolkit - sha256: "8f74adab33154fe7b731395782797021f97d2edc52f7bfb85ff4f1b5c4a215f0" - url: "https://pub.dev" - source: hosted - version: "0.15.0" google_cloud: dependency: transitive description: @@ -738,14 +714,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.3" - gpt_markdown: - dependency: transitive - description: - name: gpt_markdown - sha256: c14c2a4599a67df5b6a984808cbb7631b8d15c91984ffdd7998597b93a6ff136 - url: "https://pub.dev" - source: hosted - version: "1.1.7" graphs: dependency: transitive description: @@ -1072,14 +1040,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.0-beta.0" - mocktail: - dependency: transitive - description: - name: mocktail - sha256: "5e1bf53cc7baa8062a33b84424deb61513858ea05c601b8509e683815b5914aa" - url: "https://pub.dev" - source: hosted - version: "1.0.5" mustache_template: dependency: transitive description: @@ -1717,14 +1677,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.6.17" - tuple: - dependency: transitive - description: - name: tuple - sha256: a97ce2013f240b2f3807bcbaf218765b6f301c3eff91092bcfa23a039e7dd151 - url: "https://pub.dev" - source: hosted - version: "2.0.2" typed_data: dependency: transitive description: @@ -1813,30 +1765,6 @@ packages: url: "https://pub.dev" source: hosted version: "4.5.3" - vector_graphics: - dependency: transitive - description: - name: vector_graphics - sha256: "2306c03da2ba81724afeb589c351ebbc0aa7d86005925be8f8735856dbe5e42d" - url: "https://pub.dev" - source: hosted - version: "1.2.2" - vector_graphics_codec: - dependency: transitive - description: - name: vector_graphics_codec - sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146" - url: "https://pub.dev" - source: hosted - version: "1.1.13" - vector_graphics_compiler: - dependency: transitive - description: - name: vector_graphics_compiler - sha256: "142a9146f447d15b10bdc00e21d5f4d83e5b32bb5f8f8f5a04c75311344923a3" - url: "https://pub.dev" - source: hosted - version: "1.2.6" vector_math: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 145982ace..aa8d96fae 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,8 +11,7 @@ workspace: - packages/plugins/* dev_dependencies: - # Melos 7.8.2+ requires cli_util 0.5.x, while custom_lint 0.8.1 - # requires cli_util 0.4.x. 7.8.1 is the latest compatible workspace release. + # Keep in sync with `melos-version` in .github/workflows/*.yml. melos: 7.8.1 dependency_overrides: @@ -22,9 +21,6 @@ dependency_overrides: mix_annotations: 2.2.0-beta.0 remix: 1.0.0-beta.1 -lint_staged: - '**.dart': fvm dart fix --apply && dcm fix - melos: sdkPath: .fvm/flutter_sdk ide: @@ -46,17 +42,17 @@ melos: description: Run Dart static analysis checks. analyze:dcm: - run: fvm dart run melos exec -c 10 -- "dcm analyze . --fatal-style --fatal-warnings -e '{**/*.g.dart,**/*.freezed.dart,**/*.ack.dart}'" + run: fvm dart run melos exec -c 10 -- "dcm analyze . --fatal-style --fatal-warnings -e '{**/*.g.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-file excludes cover *.g.dart, *.freezed.dart, and *.ack.dart. + # Generated-file excludes cover *.g.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,**/*.ack.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,**/*.ack.dart,test/**,**/*_web.dart,**/*_stub.dart,**/*_io.dart}'" description: Check for unused files using DCM (public API, generated, platform stubs excluded). packageFilters: dependsOn: dart_code_metrics_presets @@ -64,7 +60,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,**/*.ack.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,**/*.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 477c3b604..2a225d740 100644 --- a/shared_analysis_options.yaml +++ b/shared_analysis_options.yaml @@ -2,8 +2,6 @@ analyzer: errors: invalid_annotation_target: ignore body_might_complete_normally_nullable: ignore - plugins: - - custom_lint exclude: - '**/generated_plugin_registrant.dart' - '**/*.g.dart'