From bbb059c7826106b2a9211b89997ad96f28dcc521 Mon Sep 17 00:00:00 2001 From: Leo Farias Date: Tue, 22 Sep 2026 18:43:54 -0500 Subject: [PATCH 01/16] refactor(playground): remove code left over from the editor The Wizard became the only authoring flow in #114, but code that only the file-backed editor used stayed behind: the Command pattern, a duplicate color utility, per-file bookmark access on both the Dart and Swift sides, unused store setters and revision tracking, DeckMarkdownCodec.encode, the deck-edit system prompt, and empty scaffold directories. Comments that still described editor sidebars and deck-edit tools now describe the current app. --- packages/playground/README.md | 7 +- .../assets/ai_prompts/deck_edit_system.prompt | 16 ---- packages/playground/dcm_baseline.json | 14 ---- packages/playground/lib/app/providers.dart | 8 +- packages/playground/lib/core/command.dart | 78 ------------------- .../lib/core/data/data_sources/deck_file.dart | 30 ++----- .../data/data_sources/memory_deck_loader.dart | 3 +- .../security_scoped_file_access.dart | 38 --------- .../data/mappers/deck_markdown_codec.dart | 4 - .../lib/core/data/repositories/.gitkeep | 0 .../lib/core/domain/models/.gitkeep | 0 .../lib/core/domain/repositories/.gitkeep | 0 .../stores/deck_customization_store.dart | 50 +----------- .../domain/stores/deck_document_store.dart | 10 +-- .../lib/core/utils/color_utils.dart | 58 -------------- .../domain/generated_deck_result_applier.dart | 31 +++----- .../wizard/chat/view/widgets/chat_input.dart | 3 +- .../ai/wizard/presentation/wizard_page.dart | 6 +- .../ai/wizard/presentation/wizard_view.dart | 3 +- .../macos/Runner/MainFlutterWindow.swift | 6 +- .../SecurityScopedBookmarkManager.swift | 21 ----- .../security_scoped_file_access_test.dart | 58 +------------- .../mappers/deck_markdown_codec_test.dart | 52 +------------ .../core/deck_customization_store_test.dart | 49 ------------ .../stores/deck_document_store_test.dart | 16 ---- .../test/core/utils/color_utils_test.dart | 75 ------------------ .../fake_security_scoped_file_access.dart | 25 +----- 27 files changed, 39 insertions(+), 622 deletions(-) delete mode 100644 packages/playground/assets/ai_prompts/deck_edit_system.prompt delete mode 100644 packages/playground/lib/core/command.dart delete mode 100644 packages/playground/lib/core/data/repositories/.gitkeep delete mode 100644 packages/playground/lib/core/domain/models/.gitkeep delete mode 100644 packages/playground/lib/core/domain/repositories/.gitkeep delete mode 100644 packages/playground/lib/core/utils/color_utils.dart delete mode 100644 packages/playground/test/core/utils/color_utils_test.dart 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..be8e8a36d 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,13 +127,6 @@ "00c8c3a7775b4bd83b4c7627c1029a39" ] }, - "lib/core/utils/color_utils.dart": { - "newline-before-return": [ - "ac84b6615d6391fc8cb6a100c0b2aad0", - "3a959d00242e1a6e9483824930ad5858", - "108ceff8715a5f73a480064664b89bb7" - ] - }, "lib/features/ai/quick_agent/core/debug_logger.dart": { "member-ordering": [ "c9e8d09211e0b5eaac505b8ea7fb89e6" 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/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/domain/generated_deck_result_applier.dart b/packages/playground/lib/features/ai/quick_agent/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/quick_agent/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/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/presentation/wizard_page.dart b/packages/playground/lib/features/ai/wizard/presentation/wizard_page.dart index 4ddfe808d..89dca34c8 100644 --- a/packages/playground/lib/features/ai/wizard/presentation/wizard_page.dart +++ b/packages/playground/lib/features/ai/wizard/presentation/wizard_page.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/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/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/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/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, From 71db48ad819f6e367985cdeee2a688d661186047 Mon Sep 17 00:00:00 2001 From: Leo Farias Date: Tue, 22 Sep 2026 18:43:54 -0500 Subject: [PATCH 02/16] refactor(pdf): share one polling loop across export waits --- .../plugins/pdf/lib/src/pdf_controller.dart | 95 +++++++++---------- 1 file changed, 45 insertions(+), 50 deletions(-) diff --git a/packages/plugins/pdf/lib/src/pdf_controller.dart b/packages/plugins/pdf/lib/src/pdf_controller.dart index 5448b2758..16b5c2176 100644 --- a/packages/plugins/pdf/lib/src/pdf_controller.dart +++ b/packages/plugins/pdf/lib/src/pdf_controller.dart @@ -137,52 +137,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 +195,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})') From 59651fca8a1cc38245e8901dba11ce48412912df Mon Sep 17 00:00:00 2001 From: Leo Farias Date: Tue, 22 Sep 2026 18:43:54 -0500 Subject: [PATCH 03/16] docs(superdeck): document route authority on the method that owns it --- .../lib/src/deck/deck_presentation_state.dart | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) 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/'; From 35b6a474f686de833955aeb04db932380a14709f Mon Sep 17 00:00:00 2001 From: Leo Farias Date: Tue, 22 Sep 2026 18:44:04 -0500 Subject: [PATCH 04/16] refactor: remove unused code and test helpers Delete symbols, files, and fixtures that nothing references outside their own tests: MeasureSize, GoToSlideEvent, MarkdownRenderScope.of, unused Wizard UI components, generation helpers, test helpers, and config fixtures. None of them is exported. Correct comments that pointed at the wrong schema builder or narrated history. --- demo/lib/src/widgets/demo_widgets.dart | 2 +- packages/core/test/helpers/testing_utils.dart | 9 - .../design/generated_deck_theme_factory.dart | 2 - .../design/presentation_theme_catalog.dart | 2 +- .../core/engine/schemas/deck_schemas.dart | 2 +- .../services/design_quality_metrics.dart | 15 - .../engine/services/source_grounding.dart | 9 - .../wizard/core/ui/components/sd_buttons.dart | 34 -- .../core/ui/components/sd_typography.dart | 19 - .../lib/features/ai/wizard/core/ui/ui.dart | 1 - .../ai/wizard/core/ui/widgets/header.dart | 39 -- .../lib/src/deck/navigation_events.dart | 6 - .../src/deck/navigation_input_listener.dart | 2 - .../blocks/markdown_render_scope.dart | 9 - .../lib/src/ui/widgets/measure_size.dart | 247 --------- .../test/fixtures/complete_config.yaml | 8 - .../superdeck/test/fixtures/empty_config.yaml | 0 .../superdeck/test/helpers/test_helpers.dart | 79 --- .../test/src/deck/navigation_events_test.dart | 11 - .../test/src/utils/measure_size_test.dart | 494 ------------------ 20 files changed, 3 insertions(+), 987 deletions(-) delete mode 100644 packages/playground/lib/features/ai/wizard/core/ui/widgets/header.dart delete mode 100644 packages/superdeck/lib/src/ui/widgets/measure_size.dart delete mode 100644 packages/superdeck/test/fixtures/complete_config.yaml delete mode 100644 packages/superdeck/test/fixtures/empty_config.yaml delete mode 100644 packages/superdeck/test/src/utils/measure_size_test.dart 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/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/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..973129ff5 100644 --- a/packages/playground/lib/core/domain/design/presentation_theme_catalog.dart +++ b/packages/playground/lib/core/domain/design/presentation_theme_catalog.dart @@ -28,7 +28,7 @@ const defaultPresentationThemeIds = [ '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/features/ai/quick_agent/core/engine/schemas/deck_schemas.dart b/packages/playground/lib/features/ai/quick_agent/core/engine/schemas/deck_schemas.dart index 2a86cea07..47e696f1d 100644 --- a/packages/playground/lib/features/ai/quick_agent/core/engine/schemas/deck_schemas.dart +++ b/packages/playground/lib/features/ai/quick_agent/core/engine/schemas/deck_schemas.dart @@ -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/services/design_quality_metrics.dart b/packages/playground/lib/features/ai/quick_agent/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/quick_agent/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/source_grounding.dart b/packages/playground/lib/features/ai/quick_agent/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/quick_agent/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/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/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/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/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/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/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/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/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)); - }); - }); - }); -} From 396422125fd435c9dbe2c81ba8d9588fb7db2313 Mon Sep 17 00:00:00 2001 From: Leo Farias Date: Tue, 22 Sep 2026 18:44:04 -0500 Subject: [PATCH 05/16] chore: drop unused dependencies and bundled assets Remove gpt_markdown, golden_toolkit, and integration_test from playground, meta from superdeck and builder, and mocktail from superdeck; move http to playground dev_dependencies. Stop bundling the unused iframe template. --- packages/builder/pubspec.yaml | 1 - packages/playground/pubspec.yaml | 13 +-- .../superdeck/assets/iframe_template.html | 104 ------------------ packages/superdeck/pubspec.yaml | 3 - pubspec.lock | 72 ------------ 5 files changed, 4 insertions(+), 189 deletions(-) delete mode 100644 packages/superdeck/assets/iframe_template.html diff --git a/packages/builder/pubspec.yaml b/packages/builder/pubspec.yaml index ee643cbd9..a3de22e48 100644 --- a/packages/builder/pubspec.yaml +++ b/packages/builder/pubspec.yaml @@ -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/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/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/pubspec.yaml b/packages/superdeck/pubspec.yaml index 5f29cc26d..e76f7e259 100644 --- a/packages/superdeck/pubspec.yaml +++ b/packages/superdeck/pubspec.yaml @@ -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/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: From 3f7245dd8666673fefa3cd943df04c2d321ba694 Mon Sep 17 00:00:00 2001 From: Leo Farias Date: Tue, 22 Sep 2026 18:44:04 -0500 Subject: [PATCH 06/16] docs: fix stale examples, versions, and references DeckOptions has no const constructor, so the quick-start examples did not compile. The block-layout migration notes called 1.0 the next major and 0.x releases 1.x. The superdeck changelog named flutter_mermaid and a deprecation that became a removal. Copilot instructions listed a removed generator and a fixed frontmatter pitfall. --- .../references/runtime-customization.md | 2 +- .github/copilot-instructions.md | 12 ++++-------- CHANGELOG.md | 2 +- README.md | 2 +- docs/getting-started.mdx | 2 +- docs/guides/markdown-authoring.mdx | 6 +++--- docs/reference/block-types.mdx | 6 +++--- docs/reference/contracts.mdx | 8 ++++---- docs/reference/deck-options.mdx | 2 +- docs/tutorials/block-layouts.mdx | 8 ++++---- packages/superdeck/CHANGELOG.md | 8 ++++---- packages/superdeck/README.md | 2 +- 12 files changed, 28 insertions(+), 32 deletions(-) diff --git a/.agents/skills/superdeck-presentations/references/runtime-customization.md b/.agents/skills/superdeck-presentations/references/runtime-customization.md index fef3aaf4a..ccdc1f4db 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())); } ``` 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/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/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/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/superdeck/CHANGELOG.md b/packages/superdeck/CHANGELOG.md index b45fec240..6c430f065 100644 --- a/packages/superdeck/CHANGELOG.md +++ b/packages/superdeck/CHANGELOG.md @@ -6,7 +6,7 @@ `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 +30,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 +61,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..469af2bdd 100644 --- a/packages/superdeck/README.md +++ b/packages/superdeck/README.md @@ -29,7 +29,7 @@ Future main() async { WidgetsFlutterBinding.ensureInitialized(); await SuperDeckApp.initialize(); - runApp(const SuperDeckApp(options: DeckOptions())); + runApp(SuperDeckApp(options: DeckOptions())); } ``` From c3c19cf534d3aae18bff3c5f1bc75b18bc10a425 Mon Sep 17 00:00:00 2001 From: Leo Farias Date: Tue, 22 Sep 2026 18:44:04 -0500 Subject: [PATCH 07/16] chore: remove stale tooling config Drop the uninstalled custom_lint analyzer plugin, the lint_staged block with no runner, freezed and MeasureSize globs in the DCM scripts, and ignore entries for puppeteer and a removed env file. --- .gitignore | 1 - demo/.gitignore | 1 - packages/superdeck/.gitignore | 1 - pubspec.yaml | 11 ++++------- shared_analysis_options.yaml | 2 -- 5 files changed, 4 insertions(+), 12 deletions(-) diff --git a/.gitignore b/.gitignore index eed7f2c16..aa815ee0e 100644 --- a/.gitignore +++ b/.gitignore @@ -50,7 +50,6 @@ build/ # Node file for website node_modules -.local-chrome .orbit discovery 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/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/pubspec.yaml b/pubspec.yaml index 145982ace..e865b8deb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -22,9 +22,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 +43,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 +61,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' From 9cb14e9ecd54afc07fa368724daf9875161907e2 Mon Sep 17 00:00:00 2001 From: Leo Farias Date: Tue, 22 Sep 2026 19:02:36 -0500 Subject: [PATCH 08/16] chore: apply shared analysis options to every package Five packages loaded shared_analysis_options.yaml through an 'extends:' key that the analyzer does not recognize, so only cli applied it. Include it alongside the lint preset instead; no new issues surface. --- demo/analysis_options.yaml | 5 +++-- packages/builder/analysis_options.yaml | 5 +++-- packages/core/analysis_options.yaml | 5 +++-- packages/plugins/pdf/analysis_options.yaml | 5 +++-- packages/superdeck/analysis_options.yaml | 5 +++-- 5 files changed, 15 insertions(+), 10 deletions(-) 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/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/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/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/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: From 61e9024e9a58894f6db23b225ea4562626f12396 Mon Sep 17 00:00:00 2001 From: Leo Farias Date: Tue, 22 Sep 2026 19:32:42 -0500 Subject: [PATCH 09/16] refactor(playground): rename quick_agent to generation and drop test-only state quick_agent is the Wizard's deck-generation engine, so name the folder for what it is. Remove validation codes nothing produces, the write-only open-deck field, and accessors and string wrappers only tests called; those tests now assert through what production exposes. --- packages/playground/dcm_baseline.json | 44 ++-- .../design/presentation_theme_catalog.dart | 15 -- .../presentation_typography_catalog.dart | 3 - .../core/constants/gemini_models.dart | 0 .../core/constants/paths.dart | 0 .../core/debug_logger.dart | 0 .../prompts/composition_example_library.dart | 0 .../prompts/generation_prompt_provider.dart | 0 .../core/engine/prompts/prompt_registry.dart | 0 .../core/engine/schemas/deck_schemas.ack.dart | 0 .../engine/schemas/deck_schemas.ack.g.dart | 0 .../core/engine/schemas/deck_schemas.dart | 0 .../engine/schemas/outline_schema.ack.dart | 0 .../engine/schemas/outline_schema.ack.g.dart | 0 .../core/engine/schemas/outline_schema.dart | 0 .../services/deck_generation_request.dart | 0 .../services/deck_generator_images.dart | 0 .../services/deck_generator_pipeline.dart | 0 .../deck_generator_pipeline_helpers.dart | 0 .../services/deck_generator_service.dart | 0 .../services/deck_generator_workflow.dart | 0 .../engine/services/deck_plan_repair.dart | 0 .../engine/services/deck_plan_validator.dart | 19 -- .../services/deck_theme_resolution.dart | 0 .../services/design_quality_metrics.dart | 0 .../engine/services/error_classifier.dart | 0 .../services/generated_slide_validator.dart | 15 -- .../services/generation_element_catalog.dart | 0 .../generation_model_call_executor.dart | 0 .../services/generation_model_client.dart | 0 .../engine/services/generation_progress.dart | 0 .../services/generation_quality_report.dart | 0 .../engine/services/generation_trace.dart | 0 .../services/generation_validation_issue.dart | 3 - .../services/google_schema_adapter.dart | 0 .../core/engine/services/retry_policy.dart | 0 .../engine/services/source_grounding.dart | 0 .../services/theme_json_serializer.dart | 0 .../core/env_config.dart | 0 .../domain/generated_deck_result_applier.dart | 0 .../ai/image_generation/image_generator.dart | 2 +- .../core/ai/prompts/prompt_registry.dart | 2 +- .../services/ai_conversation_viewmodel.dart | 2 +- .../core/ai/services/error_classifier.dart | 2 +- .../core/ai/services/prompt_builder.dart | 2 +- .../features/ai/wizard/core/debug_logger.dart | 2 +- .../features/ai/wizard/core/env_config.dart | 2 +- .../wizard_generation_controller.dart | 16 +- .../wizard_generation_status.dart | 2 +- .../presentation/wizard_outline_review.dart | 2 +- .../ai/wizard/presentation/wizard_page.dart | 6 +- .../domain/deck_library_controller.dart | 7 - .../core/presentation_theme_catalog_test.dart | 19 +- .../presentation_typography_catalog_test.dart | 4 +- .../composition_example_library_test.dart | 6 +- .../generation_prompt_provider_test.dart | 8 +- .../engine/schemas/deck_schemas_test.dart | 27 ++- .../engine/schemas/outline_schema_test.dart | 172 +++++++-------- .../schemas/theme_plan_contract_test.dart | 8 +- .../deck_generation_request_test.dart | 2 +- .../services/deck_generator_images_test.dart | 4 +- .../deck_generator_pipeline_helpers_test.dart | 2 +- .../services/deck_generator_service_test.dart | 21 +- .../services/error_classifier_test.dart | 2 +- .../generated_slide_validator_test.dart | 196 +++++++++--------- .../generation_model_call_executor_test.dart | 2 +- .../generation_model_client_test.dart | 6 +- .../services/generation_progress_test.dart | 2 +- .../generation_quality_report_test.dart | 10 +- .../engine/services/retry_policy_test.dart | 2 +- .../services/source_grounding_test.dart | 2 +- .../generated_deck_result_applier_test.dart | 6 +- .../presentation_theme_selection_test.dart | 5 +- .../wizard_generation_controller_test.dart | 19 +- .../wizard_outline_review_test.dart | 2 +- .../wizard/presentation/wizard_page_test.dart | 8 +- .../presentation/wizard_save_flow_test.dart | 30 ++- .../ai/wizard/wizard_polish_test.dart | 2 +- .../domain/deck_library_controller_test.dart | 28 ++- .../presentation/saved_decks_page_test.dart | 12 +- .../ai_generation_smoke_test.dart | 24 +-- 81 files changed, 387 insertions(+), 390 deletions(-) rename packages/playground/lib/features/ai/{quick_agent => generation}/core/constants/gemini_models.dart (100%) rename packages/playground/lib/features/ai/{quick_agent => generation}/core/constants/paths.dart (100%) rename packages/playground/lib/features/ai/{quick_agent => generation}/core/debug_logger.dart (100%) rename packages/playground/lib/features/ai/{quick_agent => generation}/core/engine/prompts/composition_example_library.dart (100%) rename packages/playground/lib/features/ai/{quick_agent => generation}/core/engine/prompts/generation_prompt_provider.dart (100%) rename packages/playground/lib/features/ai/{quick_agent => generation}/core/engine/prompts/prompt_registry.dart (100%) rename packages/playground/lib/features/ai/{quick_agent => generation}/core/engine/schemas/deck_schemas.ack.dart (100%) rename packages/playground/lib/features/ai/{quick_agent => generation}/core/engine/schemas/deck_schemas.ack.g.dart (100%) rename packages/playground/lib/features/ai/{quick_agent => generation}/core/engine/schemas/deck_schemas.dart (100%) rename packages/playground/lib/features/ai/{quick_agent => generation}/core/engine/schemas/outline_schema.ack.dart (100%) rename packages/playground/lib/features/ai/{quick_agent => generation}/core/engine/schemas/outline_schema.ack.g.dart (100%) rename packages/playground/lib/features/ai/{quick_agent => generation}/core/engine/schemas/outline_schema.dart (100%) rename packages/playground/lib/features/ai/{quick_agent => generation}/core/engine/services/deck_generation_request.dart (100%) rename packages/playground/lib/features/ai/{quick_agent => generation}/core/engine/services/deck_generator_images.dart (100%) rename packages/playground/lib/features/ai/{quick_agent => generation}/core/engine/services/deck_generator_pipeline.dart (100%) rename packages/playground/lib/features/ai/{quick_agent => generation}/core/engine/services/deck_generator_pipeline_helpers.dart (100%) rename packages/playground/lib/features/ai/{quick_agent => generation}/core/engine/services/deck_generator_service.dart (100%) rename packages/playground/lib/features/ai/{quick_agent => generation}/core/engine/services/deck_generator_workflow.dart (100%) rename packages/playground/lib/features/ai/{quick_agent => generation}/core/engine/services/deck_plan_repair.dart (100%) rename packages/playground/lib/features/ai/{quick_agent => generation}/core/engine/services/deck_plan_validator.dart (97%) rename packages/playground/lib/features/ai/{quick_agent => generation}/core/engine/services/deck_theme_resolution.dart (100%) rename packages/playground/lib/features/ai/{quick_agent => generation}/core/engine/services/design_quality_metrics.dart (100%) rename packages/playground/lib/features/ai/{quick_agent => generation}/core/engine/services/error_classifier.dart (100%) rename packages/playground/lib/features/ai/{quick_agent => generation}/core/engine/services/generated_slide_validator.dart (98%) rename packages/playground/lib/features/ai/{quick_agent => generation}/core/engine/services/generation_element_catalog.dart (100%) rename packages/playground/lib/features/ai/{quick_agent => generation}/core/engine/services/generation_model_call_executor.dart (100%) rename packages/playground/lib/features/ai/{quick_agent => generation}/core/engine/services/generation_model_client.dart (100%) rename packages/playground/lib/features/ai/{quick_agent => generation}/core/engine/services/generation_progress.dart (100%) rename packages/playground/lib/features/ai/{quick_agent => generation}/core/engine/services/generation_quality_report.dart (100%) rename packages/playground/lib/features/ai/{quick_agent => generation}/core/engine/services/generation_trace.dart (100%) rename packages/playground/lib/features/ai/{quick_agent => generation}/core/engine/services/generation_validation_issue.dart (98%) rename packages/playground/lib/features/ai/{quick_agent => generation}/core/engine/services/google_schema_adapter.dart (100%) rename packages/playground/lib/features/ai/{quick_agent => generation}/core/engine/services/retry_policy.dart (100%) rename packages/playground/lib/features/ai/{quick_agent => generation}/core/engine/services/source_grounding.dart (100%) rename packages/playground/lib/features/ai/{quick_agent => generation}/core/engine/services/theme_json_serializer.dart (100%) rename packages/playground/lib/features/ai/{quick_agent => generation}/core/env_config.dart (100%) rename packages/playground/lib/features/ai/{quick_agent => generation}/domain/generated_deck_result_applier.dart (100%) rename packages/playground/test/features/ai/{quick_agent => generation}/core/engine/prompts/composition_example_library_test.dart (93%) rename packages/playground/test/features/ai/{quick_agent => generation}/core/engine/prompts/generation_prompt_provider_test.dart (97%) rename packages/playground/test/features/ai/{quick_agent => generation}/core/engine/schemas/deck_schemas_test.dart (92%) rename packages/playground/test/features/ai/{quick_agent => generation}/core/engine/schemas/outline_schema_test.dart (92%) rename packages/playground/test/features/ai/{quick_agent => generation}/core/engine/schemas/theme_plan_contract_test.dart (92%) rename packages/playground/test/features/ai/{quick_agent => generation}/core/engine/services/deck_generation_request_test.dart (97%) rename packages/playground/test/features/ai/{quick_agent => generation}/core/engine/services/deck_generator_images_test.dart (96%) rename packages/playground/test/features/ai/{quick_agent => generation}/core/engine/services/deck_generator_pipeline_helpers_test.dart (96%) rename packages/playground/test/features/ai/{quick_agent => generation}/core/engine/services/deck_generator_service_test.dart (98%) rename packages/playground/test/features/ai/{quick_agent => generation}/core/engine/services/error_classifier_test.dart (97%) rename packages/playground/test/features/ai/{quick_agent => generation}/core/engine/services/generated_slide_validator_test.dart (93%) rename packages/playground/test/features/ai/{quick_agent => generation}/core/engine/services/generation_model_call_executor_test.dart (93%) rename packages/playground/test/features/ai/{quick_agent => generation}/core/engine/services/generation_model_client_test.dart (88%) rename packages/playground/test/features/ai/{quick_agent => generation}/core/engine/services/generation_progress_test.dart (94%) rename packages/playground/test/features/ai/{quick_agent => generation}/core/engine/services/generation_quality_report_test.dart (94%) rename packages/playground/test/features/ai/{quick_agent => generation}/core/engine/services/retry_policy_test.dart (97%) rename packages/playground/test/features/ai/{quick_agent => generation}/core/engine/services/source_grounding_test.dart (98%) rename packages/playground/test/features/ai/{quick_agent => generation}/domain/generated_deck_result_applier_test.dart (98%) diff --git a/packages/playground/dcm_baseline.json b/packages/playground/dcm_baseline.json index be8e8a36d..10befd580 100644 --- a/packages/playground/dcm_baseline.json +++ b/packages/playground/dcm_baseline.json @@ -127,7 +127,7 @@ "00c8c3a7775b4bd83b4c7627c1029a39" ] }, - "lib/features/ai/quick_agent/core/debug_logger.dart": { + "lib/features/ai/generation/core/debug_logger.dart": { "member-ordering": [ "c9e8d09211e0b5eaac505b8ea7fb89e6" ], @@ -141,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" ], @@ -154,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" ], @@ -181,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", @@ -204,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", @@ -216,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", @@ -244,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", @@ -254,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" ], @@ -266,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", @@ -290,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", @@ -313,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", @@ -338,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", @@ -348,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" ], @@ -362,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" @@ -402,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", @@ -422,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", @@ -439,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", @@ -470,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", @@ -479,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", @@ -522,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", @@ -539,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/core/domain/design/presentation_theme_catalog.dart b/packages/playground/lib/core/domain/design/presentation_theme_catalog.dart index 973129ff5..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,21 +13,6 @@ 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 the theme-selection and live smoke tests. const featuredPresentationThemeIds = [ 'editorial-midnight', 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/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 100% 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 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 100% 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 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 100% 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 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 100% 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 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/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/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 89dca34c8..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'; 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/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/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_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'; From d55a2d854de4a7c26d0fca6ee0e0f13e70a84c4e Mon Sep 17 00:00:00 2001 From: Leo Farias Date: Tue, 22 Sep 2026 19:32:42 -0500 Subject: [PATCH 10/16] refactor!: remove unused public API before 1.0 Remove aiSlideSchema, DeckWorkspace.projectDirectory, BlockInsets.authoringSchema, MarkdownAstConverter.toJson, SlideSerializer.serializeSlide, and superDeckAspectRatio. Nothing in production used them and each has a replacement noted in the changelog. --- packages/builder/CHANGELOG.md | 2 + .../lib/src/parsers/slide_serializer.dart | 7 - packages/core/CHANGELOG.md | 6 + packages/core/lib/src/deck/block_insets.dart | 46 ------- .../core/lib/src/deck/deck_workspace.dart | 2 - .../core/lib/src/deck/slide_contract.dart | 6 +- .../core/lib/src/markdown/markdown_json.dart | 30 +---- .../core/test/src/deck/block_model_test.dart | 26 ---- .../test/src/markdown/markdown_json_test.dart | 124 ++++-------------- packages/superdeck/CHANGELOG.md | 3 + .../src/rendering/slides/slide_constants.dart | 3 - packages/superdeck/test/public_api_test.dart | 1 - 12 files changed, 37 insertions(+), 219 deletions(-) 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/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/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/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/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/superdeck/CHANGELOG.md b/packages/superdeck/CHANGELOG.md index 6c430f065..4f10b64df 100644 --- a/packages/superdeck/CHANGELOG.md +++ b/packages/superdeck/CHANGELOG.md @@ -1,5 +1,8 @@ ## 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 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/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); From 7778fb6fc451f6dc2505b4d9eaf27b3e9907507b Mon Sep 17 00:00:00 2001 From: Leo Farias Date: Tue, 22 Sep 2026 19:32:42 -0500 Subject: [PATCH 11/16] refactor: remove fields only tests read and name the default PDF saver Drop usingTemplate, the duplicate ghost-suffix state in markdown helpers, CachedImage.targetSize, and PdfController.disposed. Rename savePdfWithFileSaverForTesting to savePdfWithFileSaver, since production uses it as the default saver. --- .../plugins/pdf/lib/src/pdf_controller.dart | 12 +-- .../pdf/test/src/pdf_controller_test.dart | 8 +- .../lib/src/builtins/image_widget.dart | 6 +- .../lib/src/deck/template_resolver.dart | 11 +-- .../builders/image_element_builder.dart | 7 +- .../lib/src/markdown/markdown_helpers.dart | 24 +---- .../src/ui/widgets/cache_image_widget.dart | 3 - .../src/ui/widgets/resolved_asset_image.dart | 6 +- .../test/src/builtins/image_widget_test.dart | 1 - .../test/src/deck/template_resolver_test.dart | 90 +++++-------------- .../src/markdown/hero_transition_test.dart | 14 --- .../src/markdown/markdown_helpers_test.dart | 4 - 12 files changed, 35 insertions(+), 151 deletions(-) diff --git a/packages/plugins/pdf/lib/src/pdf_controller.dart b/packages/plugins/pdf/lib/src/pdf_controller.dart index 16b5c2176..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; @@ -374,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, @@ -412,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/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/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/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/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/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/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/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')); }); }); From 11f4fb3191a8316183b721840bb6fb4548bd235e Mon Sep 17 00:00:00 2001 From: Leo Farias Date: Tue, 22 Sep 2026 19:32:42 -0500 Subject: [PATCH 12/16] fix(superdeck): drop the leftover talk title from the default header Slides without a title showed 'Generative UI with Flutter'. Show only the slide number instead, and update the affected goldens. --- .../lib/src/rendering/slides/header.dart | 4 ++-- .../test/goldens/block_aligned_center.png | Bin 6309 -> 6048 bytes .../test/goldens/decorated_block_frame.png | Bin 8764 -> 8512 bytes .../test/goldens/image_scale_framing.png | Bin 41648 -> 41389 bytes .../test/goldens/section_two_blocks.png | Bin 6762 -> 6504 bytes 5 files changed, 2 insertions(+), 2 deletions(-) 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/test/goldens/block_aligned_center.png b/packages/superdeck/test/goldens/block_aligned_center.png index 4d0bb2d1d2fcc4bd30a785119f8b7e7da01be328..0aa16cd91b2572e6280e2701f453fc5f26197866 100644 GIT binary patch delta 1042 zcmb_aZA@Bc6uyA7Q#VLvR^vizGq)`1GIVJ(ZBbm8HmhTWC3B^XQ?VbHt~1=WEP#OC zyJgvu{eW&H8dpQRWQ|KzV7;9*WlEQp8RO*=F>6O5!n9mOKxhGb<#YGi^v{0pB=4K^ zyyxRR=RD`?+gBPczS~vvLHof29qQ4z?|^7j^2#?07Up&P#NH!6n_m5uakui~_16s3 z-%Zr}q(ikQSci|*);~DX$Lz3NIr;M4VtfoA30!+p*A@AG{p@r%R1Q#bINKXx(8}%M zdgGU6ig%_QX&M5*bWR>ktz3cy(b%{45(c9E8*mjuTawO6A`{J^qQRTF#yxHhVggvk zURKAhXQ8V;3{D7Iaxvk=y=Z2Ba4SCv_M?q2I+*B3EC~yyToHGFZX@ogR^Ib^y%^^A z`>{|cHS07&02is}e&dD~jVd2oeMV@){b3O{hX75)sMdIS|C%ZW`Zo6w3=a$b zyC{T%uI#Kg*PGLmB-yOhYCA7$)woP)LX4BO_z3#k@7E38+iq zg6>x7W@aCqaw@e9kZXTMy#ZfQww`EnjY}*xf$dhQ+0tD&8C7r$95)*YEH(*bVGLVY zxf2_3Lb?YnuCc1zq=BZw`nnh~{#4n#MaTUcHk*wij}r2layL*+Ti~{+pNb32JV(FI zl#3BMYS)JV?S)ut+89Ohk^Ja*oWHzCoG0qsf>GZ9A^pG;lFT27=9<%oE{^qeKi(-^Lpv0s-$ReTH>-bo^4Iv5tA%v>(u1L&_r5`$XtLMjel@9%s&_G*E;5PKNC2?75pD6U$tNOE zD%*sI;F$*x*JIBuBh++jNe=k}8AhEgC983wvT4L<&GrX`NcgT$K^OGcHOKp1^UZWl z`p*|w3*iQd${s}r6wMrmRhR6EsP6)m+BL&#<#M?uIHKBazwS@`6OVSK2X<=^bL3rB zCN9m9{V~2jz7@a$K(?T;u(>djicOD0f`&?OC}g;fD(QA;(EP;A>vu+$xz#VC(aR+S zCbu5_Min>mJ*4+`RG2(VL0zDX`KT%qzJUCuaJiQZ)f%KUNfmlFKMvRQT+F!OTtdkH zgCMHFt(Ap~j$K-2y}RQ8F939I$g>CAK0AQN+OixeqeY^9Dh(Pg2BFt}K=O;h` zAS*5^gY$L>?QT6+&N#z4&0yH9)=VZdH#fI0uLTIIWHK41#r^w@{JuVj&nIy>9E@jI z7ll9|(6qlFl}M%N;$kzKja?hMveN4Nu1z8sWm=?Q_Fl9Y0Mb)|^78UKa>gR9qOwxX zzyYjkuE%(ij~##3i_2SN#2-SaWW;g%b}|t?gVf~_Ya>ovC=`nASmtKmX=tcKvHbsEsHt$& z>?qT_AU1o5ugq|Ho_1ersTuFAkxPtt14c zXlqq@NEXDb1iE`I^g=y;Lmu-CekTCPnh57hXFvctvaigqbnMMH4t;p|pyqwn GwSNIAu&T-c diff --git a/packages/superdeck/test/goldens/decorated_block_frame.png b/packages/superdeck/test/goldens/decorated_block_frame.png index 9a0abae1d78f38db0ad0c6f6e37e41cf1902cb92..c98ffe19cf066cae5b1a965856c83f1dd462db3b 100644 GIT binary patch literal 8512 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 literal 8764 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 diff --git a/packages/superdeck/test/goldens/image_scale_framing.png b/packages/superdeck/test/goldens/image_scale_framing.png index 72e3f978e804c24cb66d215267c67610d19fe269..fa54677d0e78a202ab30b002bbf4e27ada0eb56d 100644 GIT binary patch literal 41389 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 diff --git a/packages/superdeck/test/goldens/section_two_blocks.png b/packages/superdeck/test/goldens/section_two_blocks.png index bdc4219e6f993c70c67072880b8eb45e5fc5bc51..8eabe0ae4270d027c27b6bc43f762609fb920bb6 100644 GIT binary patch delta 1575 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 From f2bc613ee43fb0743ff4f10f3015378b20584055 Mon Sep 17 00:00:00 2001 From: Leo Farias Date: Tue, 22 Sep 2026 19:32:42 -0500 Subject: [PATCH 13/16] test(superdeck): fail when the Mermaid fork drifts from mermaid_core --- .../vendor/flowchart_strict_parser_test.dart | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 packages/superdeck/test/src/markdown/mermaid/vendor/flowchart_strict_parser_test.dart 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.', + ); + }, + ); +} From 3a8158053587689903e44e7a370f6dac9ab1c90c Mon Sep 17 00:00:00 2001 From: Leo Farias Date: Tue, 22 Sep 2026 19:32:42 -0500 Subject: [PATCH 14/16] chore: remove duplicate cli tests and the unused demo showcase --- demo/showcase.md | 318 ------------------ .../cli/test/src/utils/extensions_test.dart | 62 ---- 2 files changed, 380 deletions(-) delete mode 100644 demo/showcase.md delete mode 100644 packages/cli/test/src/utils/extensions_test.dart 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/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); - }); - }); -} From 7d63d47af23207a0b1ff3ef9fc9e517866792819 Mon Sep 17 00:00:00 2001 From: Leo Farias Date: Tue, 22 Sep 2026 19:32:42 -0500 Subject: [PATCH 15/16] docs: fix repository links, navigation, and vendoring notes Point package homepages and README links at conceptadev/superdeck, list the navigation reference in the docs sidebar, stop naming the deleted MeasureSize, and document vendor/ directories. --- .../references/runtime-customization.md | 2 +- AGENTS.md | 1 + docs.json | 4 ++++ docs/guides/widget-size-guide.mdx | 2 +- packages/builder/pubspec.yaml | 2 +- packages/cli/pubspec.yaml | 2 +- packages/core/pubspec.yaml | 2 +- packages/plugins/pdf/pubspec.yaml | 2 +- packages/superdeck/README.md | 4 ++-- packages/superdeck/pubspec.yaml | 2 +- 10 files changed, 14 insertions(+), 9 deletions(-) diff --git a/.agents/skills/superdeck-presentations/references/runtime-customization.md b/.agents/skills/superdeck-presentations/references/runtime-customization.md index ccdc1f4db..516d184de 100644 --- a/.agents/skills/superdeck-presentations/references/runtime-customization.md +++ b/.agents/skills/superdeck-presentations/references/runtime-customization.md @@ -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/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/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/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/packages/builder/pubspec.yaml b/packages/builder/pubspec.yaml index a3de22e48..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: 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/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/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/superdeck/README.md b/packages/superdeck/README.md index 469af2bdd..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 @@ -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/pubspec.yaml b/packages/superdeck/pubspec.yaml index e76f7e259..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: From 56297b2fff2c724096fd736fb8dd9842c4cd7673 Mon Sep 17 00:00:00 2001 From: Leo Farias Date: Tue, 22 Sep 2026 19:32:42 -0500 Subject: [PATCH 16/16] chore: explain the melos pin and track the root lockfile explicitly --- .gitignore | 1 + pubspec.yaml | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index aa815ee0e..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 diff --git a/pubspec.yaml b/pubspec.yaml index e865b8deb..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: