diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 00000000..06fcf7e6 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,32 @@ +name: Documentation + +on: + push: + branches: ["main"] + paths: + - "docs/**" + - "docs.json" + - ".github/workflows/docs.yml" + pull_request: + branches: ["main"] + paths: + - "docs/**" + - "docs.json" + - ".github/workflows/docs.yml" + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + + - name: Install docs.page CLI + run: npm install -g @docs.page/cli + + - name: Validate documentation + run: npx @docs.page/cli check diff --git a/.github/workflows/firebase-hosting-merge.yml b/.github/workflows/firebase-hosting-merge.yml index 37842c4c..f75605c7 100644 --- a/.github/workflows/firebase-hosting-merge.yml +++ b/.github/workflows/firebase-hosting-merge.yml @@ -2,38 +2,59 @@ # https://github.com/firebase/firebase-tools name: Deploy to Firebase Hosting on Merge + on: push: branches: - main paths: - - "example/**" + - "demo/**" + - "packages/**" + - "melos.yaml" + - "pubspec.yaml" + - "pubspec.lock" + jobs: test: - name: Test - uses: ./.github/workflows/test.yml - + name: Test + uses: ./.github/workflows/test.yml + build_and_deploy: runs-on: ubuntu-latest needs: test steps: - uses: actions/checkout@v4 - - name: Setup Flutter - uses: subosito/flutter-action@v2 + - uses: kuhnroyal/flutter-fvm-config-action@v2 + id: fvm-config-action - - name: Install dependencies - run: flutter pub get + - uses: subosito/flutter-action@v2 + with: + flutter-version: ${{ steps.fvm-config-action.outputs.FLUTTER_VERSION }} + channel: ${{ steps.fvm-config-action.outputs.FLUTTER_CHANNEL }} + + - name: Align Melos SDK path + run: | + mkdir -p .fvm + ln -sfn "$FLUTTER_ROOT" .fvm/flutter_sdk + + - name: Setup Melos + uses: bluefireteam/melos-action@v3 - name: Build Runner - run: dart run build_runner build --delete-conflicting-outputs + run: melos run build_runner:build + + - name: Build SuperDeck assets + run: | + cd demo + dart run superdeck_cli:main build - name: Deploy Firebase uses: FirebaseExtended/action-hosting-deploy@v0 env: FIREBASE_CLI_EXPERIMENTS: webframeworks with: - entryPoint: ./example + entryPoint: ./demo repoToken: ${{ secrets.GITHUB_TOKEN }} firebaseServiceAccount: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_SUPERDECK_DEV }} channelId: live diff --git a/.github/workflows/firebase-hosting-pull-request.yml b/.github/workflows/firebase-hosting-pull-request.yml index 0c6ca119..6f10f55f 100644 --- a/.github/workflows/firebase-hosting-pull-request.yml +++ b/.github/workflows/firebase-hosting-pull-request.yml @@ -2,14 +2,21 @@ # https://github.com/firebase/firebase-tools name: Deploy to Firebase Hosting on PR -on: + +on: pull_request: paths: - - "example/**" + - "demo/**" + - "packages/**" + - "melos.yaml" + - "pubspec.yaml" + - "pubspec.lock" + permissions: checks: write contents: read pull-requests: write + jobs: build_and_preview: if: ${{ github.event.pull_request.head.repo.full_name == github.repository }} @@ -17,21 +24,36 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Setup Flutter - uses: subosito/flutter-action@v2 + - uses: kuhnroyal/flutter-fvm-config-action@v2 + id: fvm-config-action - - name: Install dependencies - run: flutter pub get + - uses: subosito/flutter-action@v2 + with: + flutter-version: ${{ steps.fvm-config-action.outputs.FLUTTER_VERSION }} + channel: ${{ steps.fvm-config-action.outputs.FLUTTER_CHANNEL }} + + - name: Align Melos SDK path + run: | + mkdir -p .fvm + ln -sfn "$FLUTTER_ROOT" .fvm/flutter_sdk + + - name: Setup Melos + uses: bluefireteam/melos-action@v3 - name: Build Runner - run: dart run build_runner build --delete-conflicting-outputs + run: melos run build_runner:build + + - name: Build SuperDeck assets + run: | + cd demo + dart run superdeck_cli:main build - name: Deploy Firebase uses: FirebaseExtended/action-hosting-deploy@v0 env: FIREBASE_CLI_EXPERIMENTS: webframeworks with: - entryPoint: ./example + entryPoint: ./demo repoToken: ${{ secrets.GITHUB_TOKEN }} firebaseServiceAccount: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_SUPERDECK_DEV }} expires: 30d diff --git a/demo/integration_test/app_test.dart b/demo/integration_test/app_test.dart index 9cce2bf3..416d3119 100644 --- a/demo/integration_test/app_test.dart +++ b/demo/integration_test/app_test.dart @@ -29,8 +29,11 @@ void main() { final exception = tester.takeException(); if (exception != null) { final isLayoutOverflow = exception.toString().contains('overflowed'); - expect(isLayoutOverflow, isTrue, - reason: 'Only layout overflow is acceptable, got: $exception'); + expect( + isLayoutOverflow, + isTrue, + reason: 'Only layout overflow is acceptable, got: $exception', + ); } }); @@ -62,12 +65,28 @@ void main() { testWidgets('slides load and display', (tester) async { final controller = await tester.pumpTestApp(); - expect(controller, isNotNull, reason: 'DeckController should be available'); - expect(controller!.isLoading.value, isFalse, reason: 'Loading should complete'); - expect(controller.hasError.value, isFalse, reason: 'No error should occur'); + expect( + controller, + isNotNull, + reason: 'DeckController should be available', + ); + expect( + controller!.isLoading.value, + isFalse, + reason: 'Loading should complete', + ); + expect( + controller.hasError.value, + isFalse, + reason: 'No error should occur', + ); final slideCount = controller.totalSlides.value; - expect(slideCount, greaterThan(0), reason: 'Should have at least one slide'); + expect( + slideCount, + greaterThan(0), + reason: 'Should have at least one slide', + ); }); testWidgets('demo app has at least 5 slides', (tester) async { @@ -85,7 +104,11 @@ void main() { final controller = await tester.pumpTestApp(); expect(controller, isNotNull); - expect(controller!.currentIndex.value, 0, reason: 'Should start at first slide'); + expect( + controller!.currentIndex.value, + 0, + reason: 'Should start at first slide', + ); expect( controller.currentSlide.value, isNotNull, @@ -100,13 +123,21 @@ void main() { expect(controller, isNotNull); expect(controller!.currentIndex.value, 0); - expect(controller.canGoNext.value, isTrue, reason: 'Should be able to go next'); + expect( + controller.canGoNext.value, + isTrue, + reason: 'Should be able to go next', + ); // Navigate to next slide await controller.nextSlide(); await tester.pumpAndSettle(const Duration(seconds: 2)); - expect(controller.currentIndex.value, 1, reason: 'Should be on second slide'); + expect( + controller.currentIndex.value, + 1, + reason: 'Should be on second slide', + ); }); testWidgets('can navigate to previous slide', (tester) async { @@ -123,7 +154,11 @@ void main() { await controller.previousSlide(); await tester.pumpAndSettle(const Duration(seconds: 2)); - expect(controller.currentIndex.value, 0, reason: 'Should be back on first slide'); + expect( + controller.currentIndex.value, + 0, + reason: 'Should be back on first slide', + ); }); testWidgets('canGoPrevious is false on first slide', (tester) async { diff --git a/demo/integration_test/helpers/test_helpers.dart b/demo/integration_test/helpers/test_helpers.dart index 6000de02..b7fb84a5 100644 --- a/demo/integration_test/helpers/test_helpers.dart +++ b/demo/integration_test/helpers/test_helpers.dart @@ -29,10 +29,7 @@ class TestApp extends StatelessWidget { options: DeckOptions( baseStyle: borderedStyle(), widgets: demoWidgets, - styles: { - 'announcement': announcementStyle(), - 'quote': quoteStyle(), - }, + styles: {'announcement': announcementStyle(), 'quote': quoteStyle()}, parts: const SlideParts( header: HeaderPart(), footer: FooterPart(), diff --git a/demo/lib/src/parts/header.dart b/demo/lib/src/parts/header.dart index 6095a535..0bbf52cc 100644 --- a/demo/lib/src/parts/header.dart +++ b/demo/lib/src/parts/header.dart @@ -17,8 +17,7 @@ class HeaderPart extends StatelessWidget implements PreferredSizeWidget { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - if (slide.options.title != null) - Text(slide.options.title!), + if (slide.options.title != null) Text(slide.options.title!), const Spacer(), Text('${index + 1}'), ], diff --git a/demo/lib/src/widgets/demo_widgets.dart b/demo/lib/src/widgets/demo_widgets.dart index d78cc327..07abdd4b 100644 --- a/demo/lib/src/widgets/demo_widgets.dart +++ b/demo/lib/src/widgets/demo_widgets.dart @@ -22,43 +22,43 @@ import '../examples/button.dart' as remix_button; /// /// The QR code widget is now a built-in widget available as `@qrcode`. Map get demoWidgets => { - // Mix examples - wrapped in simple widget definitions - 'mix-simple-box': _SimpleWidgetDefinition( - (context, args) => _DemoWrapper( - child: Transform.scale(scale: 3.0, child: mix_simple_box.Example()), - ), - ), - 'mix-variants': _SimpleWidgetDefinition( - (context, args) => _DemoWrapper( - child: Transform.scale(scale: 3.0, child: mix_variants.Example()), - ), - ), - 'mix-animation': _SimpleWidgetDefinition( - (context, args) => _DemoWrapper( - child: Transform.scale( - scale: 3.0, - child: mix_animation.SwitchAnimation(), - ), - ), + // Mix examples - wrapped in simple widget definitions + 'mix-simple-box': _SimpleWidgetDefinition( + (context, args) => _DemoWrapper( + child: Transform.scale(scale: 3.0, child: mix_simple_box.Example()), + ), + ), + 'mix-variants': _SimpleWidgetDefinition( + (context, args) => _DemoWrapper( + child: Transform.scale(scale: 3.0, child: mix_variants.Example()), + ), + ), + 'mix-animation': _SimpleWidgetDefinition( + (context, args) => _DemoWrapper( + child: Transform.scale( + scale: 3.0, + child: mix_animation.SwitchAnimation(), ), + ), + ), - // Naked UI examples - 'naked-select': _SimpleWidgetDefinition( - (context, args) => _DemoWrapper( - child: Transform.scale( - scale: 2.0, - child: naked_select.SimpleSelectExample(), - ), - ), + // Naked UI examples + 'naked-select': _SimpleWidgetDefinition( + (context, args) => _DemoWrapper( + child: Transform.scale( + scale: 2.0, + child: naked_select.SimpleSelectExample(), ), + ), + ), - // Remix examples - 'remix-button': _SimpleWidgetDefinition( - (context, args) => _DemoWrapper( - child: Transform.scale(scale: 1.2, child: remix_button.ButtonExample()), - ), - ), - }; + // Remix examples + 'remix-button': _SimpleWidgetDefinition( + (context, args) => _DemoWrapper( + child: Transform.scale(scale: 1.2, child: remix_button.ButtonExample()), + ), + ), +}; /// Simple widget definition for widgets without schemas. /// @@ -66,7 +66,7 @@ Map get demoWidgets => { /// Uses raw `Map` as the argument type (no parsing). class _SimpleWidgetDefinition extends WidgetDefinition> { final Widget Function(BuildContext context, Map args) - _builder; + _builder; const _SimpleWidgetDefinition(this._builder); diff --git a/docs.json b/docs.json index 009c1494..24dd25e7 100644 --- a/docs.json +++ b/docs.json @@ -1,101 +1,118 @@ { + "$schema": "https://docs.page/schema.json", "name": "SuperDeck", "description": "Create beautiful presentations with Flutter and Markdown", - "logo": "/assets/logo.png", "theme": { - "light": { - "primary": "#2563eb" - }, - "dark": { - "primary": "#3b82f6" - } + "defaultTheme": "light", + "primary": "#2563eb", + "primaryDark": "#3b82f6" }, - "navigation": [ - { - "title": "Home", - "path": "/" - }, + "header": { + "showName": true, + "showThemeToggle": true, + "showGitHubCard": true, + "links": [ + { + "title": "Pub.dev", + "href": "https://pub.dev/packages/superdeck", + "cta": true + } + ] + }, + "social": { + "github": "btwld/superdeck" + }, + "search": {}, + "content": { + "showPageTitle": true, + "zoomImages": true, + "automaticallyInferNextPrevious": true + }, + "sidebar": [ { - "title": "Getting Started", - "path": "/docs/getting-started" + "group": "Home", + "pages": [ + { + "title": "Home", + "href": "/" + } + ] }, { - "title": "Tutorials", - "children": [ + "group": "Tutorials", + "pages": [ { - "title": "Block Layouts", - "path": "/docs/tutorials/block-layouts" + "title": "Getting started", + "href": "/getting-started" }, { - "title": "First Presentation", - "path": "/docs/tutorials/first-presentation" + "title": "First presentation", + "href": "/tutorials/first-presentation" } ] }, { - "title": "How-to Guides", - "children": [ - { - "title": "SuperDeck Overview", - "path": "/docs/guides/superdeck-overview" - }, + "group": "How-to guides", + "pages": [ { - "title": "Markdown Authoring", - "path": "/docs/guides/markdown-authoring" + "title": "Block layouts", + "href": "/tutorials/block-layouts" }, { - "title": "CLI Reference", - "path": "/docs/guides/cli-reference" + "title": "Custom widgets", + "href": "/guides/custom-widgets" }, { - "title": "Custom Widgets", - "path": "/docs/guides/custom-widgets" + "title": "Markdown authoring", + "href": "/guides/markdown-authoring" }, { - "title": "Custom Slide Parts", - "path": "/docs/guides/slide-parts" + "title": "Mermaid diagrams", + "href": "/guides/mermaid-diagrams" }, { - "title": "Widget Size Guide", - "path": "/docs/guides/widget-size-guide" + "title": "Slide parts", + "href": "/guides/slide-parts" }, { - "title": "Mermaid Diagrams", - "path": "/docs/guides/mermaid-diagrams" + "title": "Widget size measurement", + "href": "/guides/widget-size-guide" } ] }, { - "title": "Reference", - "children": [ + "group": "Reference", + "pages": [ { - "title": "Block Types", - "path": "/docs/reference/block-types" + "title": "CLI reference", + "href": "/guides/cli-reference" + }, + { + "title": "Block types", + "href": "/reference/block-types" }, { "title": "DeckOptions API", - "path": "/docs/reference/deck-options" + "href": "/reference/deck-options" + }, + { + "title": "Markdown syntax", + "href": "/reference/markdown-syntax" } ] }, { - "title": "Examples", - "path": "/docs/examples" + "group": "Explanation", + "pages": [ + { + "title": "SuperDeck overview", + "href": "/guides/superdeck-overview" + }, + { + "title": "Examples", + "href": "/examples" + } + ] } - ], - "footer": { - "links": [ - { - "title": "GitHub", - "href": "https://github.com/leoafarias/superdeck" - }, - { - "title": "Pub.dev", - "href": "https://pub.dev/packages/superdeck" - } - ] - }, - "social": { - "github": "leoafarias/superdeck" - } + ] } diff --git a/docs/examples.mdx b/docs/examples.mdx index 197a6a21..45d28a6f 100644 --- a/docs/examples.mdx +++ b/docs/examples.mdx @@ -7,9 +7,9 @@ description: Real-world examples showcasing SuperDeck's features and capabilitie Explore these real-world examples that demonstrate SuperDeck's capabilities. All examples are taken from actual SuperDeck presentations. -## Basic Layouts +## Basic layouts -### Centered Title Slide +### Centered title slide Use this for opening slides, section dividers, or any slide where you want the title to be the focal point. @@ -33,7 +33,7 @@ Use this for opening slides, section dividers, or any slide where you want the t - Centered content alignment - Multi-line headings -### Two-Column Content +### Two-column content Use this for comparisons, pros/cons lists, or showing two related concepts side-by-side. @@ -65,9 +65,9 @@ Use this for comparisons, pros/cons lists, or showing two related concepts side- - Mixed alignment options - Comparative layouts -## Code Examples +## Code examples -### Syntax Highlighted Code Blocks +### Syntax highlighted code blocks Use this when demonstrating code snippets, API usage, or implementation details. @@ -96,7 +96,7 @@ Column( - Clean code formatting - Single-column layout for code -### Complex Schema Definition +### Complex schema definition ````markdown @column @@ -123,9 +123,9 @@ final schema = Schema.object(properties: { --- ```` -## Visual Content +## Visual content -### Image with Cover Fit +### Image with cover fit ```markdown @column @@ -140,7 +140,7 @@ final schema = Schema.object(properties: { - CSS class for image fitting - Full-width image display -### Mixed Content Layout +### Mixed content layout ````markdown @section @@ -167,9 +167,9 @@ graph TD - Side-by-side content - Mixed media types -## Interactive Content +## Interactive content -### DartPad Integration +### DartPad integration ```markdown @column @@ -194,7 +194,7 @@ graph TD - Multi-block layouts - Interactive code examples -### Custom Widget Integration +### Custom widget integration ```markdown @widget { @@ -211,9 +211,9 @@ graph TD - Multiple arguments passed to widget - External data integration -## Advanced Layouts +## Advanced layouts -### Complex Multi-Section +### Complex multi-section layout ````markdown @section @@ -253,7 +253,7 @@ Column( - Complex flex layouts - Multiple content types -### Profile/Bio Layout +### Profile and bio layout ```markdown @section @@ -282,9 +282,9 @@ Column( - Alignment-based design - Professional bio formatting -## Special Features +## Special features -### GitHub-Style Alerts +### GitHub-style alerts ```markdown @column { @@ -303,7 +303,7 @@ Column( - Warning callouts - Flexible column sizing -### Generative UI Demo +### Generative UI demo ```markdown @widget { @@ -319,7 +319,7 @@ Column( - AI-generated content integration - Interactive demonstrations -### Interactive Counter Widget +### Interactive counter widget ```markdown @section @@ -342,9 +342,9 @@ Try the interactive counter: - Multiple argument types (int, string) - Interactive user interface elements -## Mermaid Diagrams +## Mermaid diagrams -### Process Flow +### Process flow ````markdown @column @@ -369,9 +369,9 @@ Simple workflow representation using Mermaid syntax. - Automatic diagram rendering - Process visualization -## Styling Examples +## Styling examples -### Custom CSS Classes +### Custom CSS classes ````markdown # Important Heading {.heading} @@ -393,9 +393,9 @@ void main() { - Custom styling hooks - Consistent visual theming -## Complete Slide Examples +## Complete slide examples -### Feature Announcement +### Feature announcement ```markdown @column { @@ -414,7 +414,7 @@ adaptive UIs across devices and platforms. --- ``` -### Technical Explanation +### Technical explanation ````markdown @section @@ -443,7 +443,7 @@ final schema = Schema.array( --- ```` -## Best Practices Shown +## Best practices shown 1. **Flexible Layouts** - Use `flex` properties to control space distribution 2. **Mixed Content** - Combine text, images, code, and widgets effectively @@ -453,7 +453,7 @@ final schema = Schema.array( 6. **Visual Hierarchy** - Use sections and columns to guide attention 7. **External Content** - Integrate images, social media, and external resources -## Common Patterns +## Common patterns - **Hero sections** with centered content and large text - **Side-by-side comparisons** using two-column layouts diff --git a/docs/getting-started.mdx b/docs/getting-started.mdx index db8059a1..d53bfc5f 100644 --- a/docs/getting-started.mdx +++ b/docs/getting-started.mdx @@ -1,9 +1,9 @@ --- -title: Getting Started +title: Getting started description: Set up SuperDeck in your Flutter project and create your first presentation --- -# Getting Started +# Getting started Set up SuperDeck in your Flutter project and create your first presentation. @@ -15,13 +15,13 @@ Set up SuperDeck in your Flutter project and create your first presentation. ## Installation -### 1. Install SuperDeck CLI +### Install SuperDeck CLI ```bash dart pub global activate superdeck_cli ``` -### 2. Set up your project +### Set up your project In your Flutter project directory: @@ -36,13 +36,13 @@ This creates: - macOS entitlements for network access - Custom `index.html` for web with loading indicator -### 3. Add SuperDeck dependency +### Add SuperDeck dependency ```bash flutter pub add superdeck ``` -### 4. Initialize SuperDeck in your app +### Initialize SuperDeck in your app Update your `main.dart`: @@ -63,7 +63,7 @@ void main() async { } ``` -## Your First Presentation +## Your first presentation Edit the generated `slides.md` file: @@ -105,7 +105,7 @@ Create beautiful presentations using: --- ``` -## Build Your Presentation +## Build your presentation ```bash # Build once @@ -115,13 +115,13 @@ superdeck build superdeck build --watch ``` -## Run Your App +## Run your app ```bash flutter run ``` -## Development Workflow +## Development workflow Use two terminals: @@ -137,7 +137,7 @@ flutter run Edit `slides.md`, and the CLI rebuilds automatically. Hot reload Flutter to see changes. -## File Structure +## File structure After setup, your project will have: @@ -153,11 +153,11 @@ your-flutter-project/ └── main.dart # Your Flutter app ``` -## Next Steps +## Next steps -- **[Block Layouts Tutorial](/docs/tutorials/block-layouts)** - Learn SuperDeck's layout system -- **[First Presentation Tutorial](/docs/tutorials/first-presentation)** - Create a complete presentation -- **[CLI Reference](/docs/guides/cli-reference)** - All CLI commands and options +- **[Block Layouts Tutorial](/tutorials/block-layouts)** - Learn SuperDeck's layout system +- **[First Presentation Tutorial](/tutorials/first-presentation)** - Create a complete presentation +- **[CLI Reference](/guides/cli-reference)** - All CLI commands and options ## Troubleshooting diff --git a/docs/guides/cli-reference.mdx b/docs/guides/cli-reference.mdx index fe5b43b4..24a77f54 100644 --- a/docs/guides/cli-reference.mdx +++ b/docs/guides/cli-reference.mdx @@ -1,9 +1,9 @@ --- -title: CLI Reference +title: CLI reference description: Complete reference for SuperDeck CLI commands and options --- -# CLI Reference +# CLI reference The SuperDeck CLI provides tools for setting up, building, and publishing presentations. This reference covers all commands and options. @@ -13,13 +13,14 @@ The SuperDeck CLI provides tools for setting up, building, and publishing presen dart pub global activate superdeck_cli ``` -## Global Options +## Global options These options work with all commands: | Option | Short | Description | |--------|-------|-------------| | `--verbose` | `-v` | Enable verbose logging | +| `--version` | | Print the current version | | `--quiet` | `-q` | Suppress output except errors | | `--help` | `-h` | Show help information | @@ -111,7 +112,7 @@ Print the SuperDeck CLI version. superdeck version ``` -## File Structure +## File structure After running CLI commands, your project will have: @@ -133,7 +134,7 @@ project/ ## Configuration -### pubspec.yaml Updates +### pubspec.yaml updates The CLI automatically adds this to your `pubspec.yaml`: @@ -144,7 +145,7 @@ flutter: - .superdeck/assets/ ``` -### macOS Entitlements +### macOS entitlements For macOS projects, `superdeck setup` updates `macos/Runner/Release.entitlements` and `macos/Runner/DebugProfile.entitlements` to allow network access (required for remote images and DartPad): @@ -157,9 +158,9 @@ For macOS projects, `superdeck setup` updates `macos/Runner/Release.entitlements In debug builds, it also enables `com.apple.security.network.server` and `com.apple.security.cs.allow-jit`. -## Common Workflows +## Common workflows -### Development Workflow +### Development workflow ```bash # Terminal 1: Start CLI in watch mode @@ -169,7 +170,7 @@ superdeck build --watch flutter run ``` -### Publishing Workflow +### Publishing workflow ```bash # Build and test locally @@ -180,7 +181,7 @@ flutter run -d web superdeck publish ``` -### Troubleshooting Workflow +### Troubleshooting workflow ```bash # Verbose output for debugging @@ -193,7 +194,7 @@ superdeck build --force-rebuild superdeck publish --dry-run ``` -## Exit Codes +## Exit codes | Code | Description | |------|-------------| @@ -204,9 +205,9 @@ superdeck publish --dry-run | 70 | Command failed | | 74 | File system error | -## Common Issues +## Common issues -### Build Fails +### Build fails **Problem:** `Error: slides.md not found` **Solution:** Ensure `slides.md` exists in project root or run `superdeck setup` @@ -214,7 +215,7 @@ superdeck publish --dry-run **Problem:** `Error: Invalid markdown syntax` **Solution:** Check your markdown syntax, especially block definitions and frontmatter -### Publish Fails +### Publish fails **Problem:** `Error: Not a git repository` **Solution:** Initialize git repository: `git init && git add . && git commit -m "Initial commit"` @@ -222,7 +223,7 @@ superdeck publish --dry-run **Problem:** `Error: No changes to commit` **Solution:** This is informational. Make a change (or run `superdeck build --force-rebuild`) and publish again. -### Watch Mode Issues +### Watch mode issues **Problem:** Changes not detected **Solution:** Ensure you're editing `slides.md` in the project root @@ -230,7 +231,7 @@ superdeck publish --dry-run **Problem:** Build errors in watch mode **Solution:** Fix the error and save again; watch mode will retry automatically -## Performance Tips +## Performance tips 1. **Use watch mode** - `superdeck build --watch` is optimized for development 2. **Cache remote images** - Remote images are cached at runtime by `cached_network_image` @@ -239,7 +240,7 @@ superdeck publish --dry-run ## Integration with CI/CD -### GitHub Actions Example +### GitHub Actions example ```yaml name: Deploy SuperDeck diff --git a/docs/guides/custom-widgets.mdx b/docs/guides/custom-widgets.mdx index 50812564..fb707f3b 100644 --- a/docs/guides/custom-widgets.mdx +++ b/docs/guides/custom-widgets.mdx @@ -1,13 +1,13 @@ --- -title: Custom Widgets Guide +title: Custom widgets guide description: Learn how to create and use custom Flutter widgets in SuperDeck presentations --- -# Custom Widgets Guide +# Custom widgets guide Render custom Flutter widgets from Markdown. Register widgets in `DeckOptions.widgets`, then use them in `slides.md`. -## Markdown Syntax +## Markdown syntax Two equivalent forms: @@ -16,9 +16,9 @@ Two equivalent forms: All properties pass to `WidgetDefinition.parse()` as arguments. -## 1) Create a `WidgetDefinition` +## Create a `WidgetDefinition` -### Simple: Map args (no validation) +### Simple: map args (no validation) ```dart import 'package:flutter/widgets.dart'; @@ -39,7 +39,7 @@ class TwitterWidgetDefinition extends WidgetDefinition> { } ``` -### Recommended: Typed args with validation +### Recommended: typed args with validation Validate and convert the raw map to a typed args object in `parse()`. Use `Ack` (from `superdeck_core`) for schema validation. @@ -80,7 +80,7 @@ class TwitterDefinition extends WidgetDefinition { } ``` -## 2) Register in `DeckOptions` +## Register widgets in `DeckOptions` ```dart import 'package:flutter/widgets.dart'; @@ -102,7 +102,7 @@ void main() async { } ``` -## 3) Use in `slides.md` +## Use widgets in `slides.md` Shorthand: @@ -123,7 +123,7 @@ Explicit: } ``` -## Access Block and Slide Context +## Access block and slide context In `WidgetDefinition.build()`: diff --git a/docs/guides/markdown-authoring.mdx b/docs/guides/markdown-authoring.mdx index 6fed28b8..8423a141 100644 --- a/docs/guides/markdown-authoring.mdx +++ b/docs/guides/markdown-authoring.mdx @@ -1,15 +1,15 @@ --- -title: Markdown Authoring Guide +title: Markdown authoring guide description: Author Markdown that compiles cleanly into SuperDeck slide decks. --- -# Markdown Authoring Guide +# Markdown authoring guide Write your SuperDeck slides in Markdown. This guide shows you how to structure content and use block syntax. --- -## Quick Start +## Quick start A basic slide with a title and content: @@ -29,7 +29,7 @@ title: My First Slide --- -## Slide Options (Front Matter) +## Slide options (front matter) Each slide can have YAML front matter with these options: @@ -46,21 +46,19 @@ style: overview --- -## Block Types +## Block types -SuperDeck has 3 core block types: +Use these block types when you compose slides: -| Block | Purpose | Key Properties | -|-------|---------|----------------| -| `@section` | Container for horizontal layout | `flex`, `align`, `scrollable` | -| `@column` | Renders markdown content | `flex`, `align`, `scrollable` | -| `@widget` | Embeds custom widgets | `name` + custom args | +- `@section` for horizontal layout containers. +- `@column` for markdown content. +- `@widget` for custom Flutter widgets. -Built-in widgets (`image`, `dartpad`, `qrcode`) use the same syntax. Mermaid diagrams use fenced code blocks (` ```mermaid`). +For the full syntax and argument reference, see the [Markdown syntax reference](../reference/markdown-syntax). --- -## Layout Building Blocks +## Layout building blocks ### `@section` @@ -89,17 +87,19 @@ Renders markdown content. Most slides use one or more columns. - Finish with supporting data ```` -### Common Properties +### Common properties -All blocks support these properties: +Use these properties on blocks: - `flex` – Relative size (default: `1`). A column with `flex: 2` takes twice the space of `flex: 1` -- `align` – Content alignment: `top_left`, `top_center`, `top_right`, `center_left`, `center`, `center_right`, `bottom_left`, `bottom_center`, `bottom_right` +- `align` – Content alignment such as `top_left`, `center`, and `bottom_right` - `scrollable` – Enable scrolling for overflow content (default: `false`) +For complete value definitions, see the [Markdown syntax reference](../reference/markdown-syntax). + --- -## Built-in Widgets +## Built-in widgets ### `@image` @@ -113,7 +113,7 @@ Display images with fit options. } ```` -Properties: `src` (required; asset path, absolute file path, or URL), `fit` (`contain`, `cover`, `fill`, `fitWidth`, `fitHeight`, `none`, `scaleDown`), `width`, `height` +For all `@image` arguments, see the [Markdown syntax reference](../reference/markdown-syntax). ### `@dartpad` @@ -127,11 +127,11 @@ Embed interactive DartPad examples. } ```` -Properties: `id` (required), `theme` (`light` or `dark`), `embed`, `run` +For all `@dartpad` arguments, see the [Markdown syntax reference](../reference/markdown-syntax). > DartPad uses an embedded WebView. Make sure your target platform supports WebViews. -### Custom Widgets +### Custom widgets Register widgets in `DeckOptions.widgets`, then use them with the shorthand syntax: @@ -147,7 +147,7 @@ The widget name becomes the block type (`@metricCard`), and all properties are p --- -## Markdown Features +## Markdown features ### Standard Markdown @@ -183,7 +183,7 @@ Use GitHub-style alerts for callouts: > Helpful suggestions. ```` -### Hero Animations +### Hero animations Add class names to animate elements between slides: @@ -196,7 +196,7 @@ Elements with matching class names animate smoothly during slide transitions. --- -## Mermaid Diagrams +## Mermaid diagrams Add Mermaid diagrams using fenced code blocks: @@ -238,8 +238,9 @@ See the [SuperDeck Overview](./superdeck-overview) for more styling options. --- -## Next Steps +## Next steps -- Explore block properties in the [Block Types reference](../reference/block-types) +- Explore syntax details in the [Markdown syntax reference](../reference/markdown-syntax) +- Explore block behavior in the [Block types reference](../reference/block-types) - Learn about publishing with the [CLI Reference](./cli-reference) - Create custom widgets with the [Custom Widgets Guide](./custom-widgets) diff --git a/docs/guides/mermaid-diagrams.mdx b/docs/guides/mermaid-diagrams.mdx index 4d28f20a..d8a973f5 100644 --- a/docs/guides/mermaid-diagrams.mdx +++ b/docs/guides/mermaid-diagrams.mdx @@ -1,13 +1,13 @@ --- -title: Mermaid Diagrams Guide +title: Mermaid diagrams guide description: Create beautiful diagrams and flowcharts using Mermaid syntax in SuperDeck presentations --- -# Mermaid Diagrams Guide +# Mermaid diagrams guide SuperDeck includes built-in support for [Mermaid](https://mermaid.js.org/) diagrams, allowing you to create flowcharts, sequence diagrams, class diagrams, and more using simple text syntax. During the build process, mermaid diagrams are automatically rendered to high-quality images. -## Supported Diagram Types +## Supported diagram types - **Flowcharts** - Process flows and decision trees - **Sequence Diagrams** - Interactions between participants over time @@ -18,7 +18,7 @@ SuperDeck includes built-in support for [Mermaid](https://mermaid.js.org/) diagr - **Gantt Charts** - Project timelines - **Entity-Relationship Diagrams** - Database relationships -## Basic Usage +## Basic usage Create a mermaid diagram by using a fenced code block with the `mermaid` language: @@ -35,7 +35,7 @@ graph TD ## Flowcharts -### Basic Flowchart +### Basic flowchart ````markdown ```mermaid @@ -58,7 +58,7 @@ flowchart TD ``` ```` -### Node Shapes +### Node shapes SuperDeck supports all Mermaid node shapes: @@ -81,7 +81,7 @@ flowchart LR ``` ```` -### Direction Options +### Direction options Control flowchart direction: @@ -97,7 +97,7 @@ flowchart LR ``` ```` -## Sequence Diagrams +## Sequence diagrams Perfect for showing interactions between different systems or users: @@ -127,7 +127,7 @@ sequenceDiagram ``` ```` -### Sequence Diagram Features +### Sequence diagram features - **Participants**: Define actors in the sequence - **Messages**: Arrows between participants (`->>`, `-->>`, `-x`, `--x`) @@ -135,7 +135,7 @@ sequenceDiagram - **Loops**: Repetitive actions (`loop`, `end`) - **Alternatives**: Conditional flows (`alt`, `else`, `end`) -## Class Diagrams +## Class diagrams Show object-oriented relationships: @@ -174,7 +174,7 @@ classDiagram ``` ```` -## State Diagrams +## State diagrams Model state transitions and workflows: @@ -202,7 +202,7 @@ stateDiagram-v2 ``` ```` -## Pie Charts +## Pie charts Visualize data proportions: @@ -216,7 +216,7 @@ pie title SuperDeck Usage by Platform ``` ```` -## Journey Maps +## Journey maps Show user experience flows: @@ -243,7 +243,7 @@ journey ``` ```` -## Gantt Charts +## Gantt charts Project timeline visualization: @@ -270,7 +270,7 @@ gantt ``` ```` -## Entity-Relationship Diagrams +## Entity-relationship diagrams Database schema visualization: @@ -309,13 +309,13 @@ erDiagram ``` ```` -## Styling and Theming +## Styling and theming SuperDeck renders Mermaid diagrams to PNG during `superdeck build`. By default, the builder uses Mermaid’s `base` theme with a dark set of `themeVariables`. Some diagram types (for example, `timeline` in dark mode) fall back to Mermaid’s `default` theme to keep axes and grid lines readable. -### Per-Diagram Styling +### Per-diagram styling To customize a single diagram, use Mermaid’s `init` directive at the top of the code block: @@ -335,7 +335,7 @@ graph TD ``` ```` -## Advanced Features +## Advanced features ### Subgraphs @@ -386,7 +386,7 @@ flowchart TD ``` ```` -## Build Process +## Build process When you run `superdeck build`, mermaid diagrams are automatically: @@ -399,7 +399,7 @@ Generated images are saved to `.superdeck/assets/` and automatically included in ## Troubleshooting -### Common Issues +### Common issues 1. **Syntax Errors**: Validate your Mermaid syntax using the [official live editor](https://mermaid.live/) 2. **Missing Images**: Check build output for any generation errors @@ -413,16 +413,16 @@ Use verbose mode to see detailed build information: superdeck build --verbose ``` -## Best Practices +## Best practices -### Design Guidelines +### Design guidelines 1. **Keep it Simple**: Avoid overly complex diagrams 2. **Use Colors Wisely**: Consider presentation background when styling 3. **Readable Text**: Ensure text is large enough for presentation viewing 4. **Consistent Style**: Use similar styling across all diagrams -### Performance Tips +### Performance tips 1. **Keep Diagrams Simple**: Avoid overly complex diagrams for better readability 2. **Test Early**: Build regularly to catch syntax issues diff --git a/docs/guides/slide-parts.mdx b/docs/guides/slide-parts.mdx index 84341b46..02811125 100644 --- a/docs/guides/slide-parts.mdx +++ b/docs/guides/slide-parts.mdx @@ -1,9 +1,9 @@ --- -title: Custom Slide Parts +title: Custom slide parts description: Add a header, footer, and background to every slide. --- -# Custom Slide Parts +# Custom slide parts Use slide parts to render shared UI on every slide: @@ -11,7 +11,7 @@ Use slide parts to render shared UI on every slide: - Footer (`PreferredSizeWidget`) - Background (`Widget`) -## 1) Create parts +## Create slide parts Header and footer widgets must implement `PreferredSizeWidget`. @@ -72,7 +72,7 @@ class DeckBackground extends StatelessWidget { } ``` -## 2) Register parts +## Register slide parts Register your parts in `DeckOptions.parts`. diff --git a/docs/guides/superdeck-overview.mdx b/docs/guides/superdeck-overview.mdx index 4c9b5b04..a492baf9 100644 --- a/docs/guides/superdeck-overview.mdx +++ b/docs/guides/superdeck-overview.mdx @@ -1,15 +1,15 @@ --- -title: SuperDeck Overview +title: SuperDeck overview description: Understand how SuperDeck turns Markdown into rich Flutter presentations. --- -# SuperDeck Overview +# SuperDeck overview Create presentations with Flutter and Markdown. --- -## What You Can Build +## What you can build With SuperDeck, you can: @@ -21,9 +21,9 @@ With SuperDeck, you can: --- -## Core Features +## Core features -### Markdown Layout System +### Markdown layout system SuperDeck uses a powerful `@block` syntax for organizing slide content into flexible layouts: @@ -61,7 +61,7 @@ Build presentations with Markdown and Flutter **Alignment options**: `top_left`, `top_center`, `top_right`, `center_left`, `center`, `center_right`, `bottom_left`, `bottom_center`, `bottom_right` -### Custom Widget Support +### Custom widget support Register custom Flutter widgets and use them directly in Markdown: @@ -86,7 +86,7 @@ Use in Markdown: SuperDeck calls `WidgetDefinition.parse()` with the block's arguments and then `WidgetDefinition.build()` to render your widget. For complex widgets, parse into a typed args class and validate with `Ack` (see the Custom Widgets guide). -### Markdown Extensions +### Markdown extensions **Hero animations** – CSS-like tags for seamless slide transitions: @@ -124,9 +124,9 @@ graph TD --- -## Styling & Theming +## Styling & theming -### Comprehensive Styling API +### Comprehensive styling API Control every aspect of your presentation through the `SlideStyle` system: @@ -165,7 +165,7 @@ Import `package:flutter/material.dart` and `package:mix/mix.dart` to access the - Alerts: note, tip, important, warning, caution - Layout: containers, images, flex configurations -### Theme System +### Theme system - **Mix 2.0 integration** – Type-safe, composable styles - **Google Fonts support** – Custom typography with automatic loading @@ -173,7 +173,7 @@ Import `package:flutter/material.dart` and `package:mix/mix.dart` to access the --- -## How It Works +## How it works SuperDeck uses a two-stage build pipeline: @@ -184,7 +184,7 @@ This approach gives you fast hot-reload during development while ensuring optimi --- -## Next Steps +## Next steps - Learn how to author slides with the [Markdown Authoring Guide](./markdown-authoring) - Dive deeper into block behavior in the [Block Types reference](../reference/block-types) diff --git a/docs/guides/widget-size-guide.mdx b/docs/guides/widget-size-guide.mdx index 1f0ec71e..62cec42b 100644 --- a/docs/guides/widget-size-guide.mdx +++ b/docs/guides/widget-size-guide.mdx @@ -1,9 +1,9 @@ --- -title: Widget Size Measurement +title: Widget size measurement description: Measure a widget after layout with MeasureSize. --- -# Widget Size Measurement +# Widget size measurement Use `MeasureSize` when you need a widget’s rendered size after layout (for example, to position an overlay or report sizes to another system). diff --git a/docs/index.mdx b/docs/index.mdx index 23eb537f..27373984 100644 --- a/docs/index.mdx +++ b/docs/index.mdx @@ -1,5 +1,5 @@ --- -title: SuperDeck Documentation +title: SuperDeck documentation description: Create beautiful presentations with Flutter and Markdown --- @@ -9,13 +9,13 @@ Create presentations in Flutter using Markdown. SuperDeck Screenshot -## [View Live Demo](https://superdeck-dev.web.app) +## [View live demo](https://superdeck-dev.web.app) ## Overview SuperDeck is a Flutter presentation framework that combines Markdown simplicity with Flutter's UI power. Create flexible, multi-column layouts with custom widgets and interactive content using a block-based architecture. -### Key Components +### Key components - **CLI Tool** - Set up, build, and publish presentations - **Flutter Package** - UI components and presentation runtime @@ -23,7 +23,7 @@ SuperDeck is a Flutter presentation framework that combines Markdown simplicity - **Mix Styling** - Comprehensive theming system - **Asset Pipeline** - Automatic asset generation and optimization -## Core Features +## Core features - **3 Block Types** - `@column` (content), `@section` (layout), `@widget` (custom components) - **Built-in Widgets** - `image`, `dartpad`, `qrcode` @@ -42,7 +42,7 @@ SuperDeck is a Flutter presentation framework that combines Markdown simplicity - **CLI** (`packages/cli/`) - Command-line interface - **SuperDeck** (`packages/superdeck/`) - Flutter UI components and Mix styling -## Quick Start +## Quick start 1. **Install the CLI**: ```bash @@ -77,15 +77,15 @@ SuperDeck is a Flutter presentation framework that combines Markdown simplicity } ``` -## Documentation Structure +## Documentation structure -- **[Getting Started](/docs/getting-started)** - Complete setup guide -- **[Tutorials](/docs/tutorials/first-presentation)** - Step-by-step learning paths -- **[How-to Guides](/docs/guides/superdeck-overview)** - Task-focused instructions -- **[Reference](/docs/reference/deck-options)** - Complete API documentation -- **[Examples](/docs/examples)** - Real-world use cases +- **[Getting Started](/getting-started)** - Complete setup guide +- **[Tutorials](/tutorials/first-presentation)** - Step-by-step learning paths +- **[How-to Guides](/guides/superdeck-overview)** - Task-focused instructions +- **[Reference](/reference/deck-options)** - Complete API documentation +- **[Examples](/examples)** - Real-world use cases -## What Makes SuperDeck Different +## What makes SuperDeck different - **Flutter-Native** - Built for Flutter developers - **Block-Based** - Flexible layouts beyond traditional slides diff --git a/docs/reference/block-types.mdx b/docs/reference/block-types.mdx index 3a8905a8..2e2259ea 100644 --- a/docs/reference/block-types.mdx +++ b/docs/reference/block-types.mdx @@ -1,13 +1,13 @@ --- -title: Block Types Reference +title: Block types reference description: Complete reference for all SuperDeck block types and their properties --- -# Block Types Reference +# Block types reference SuperDeck has 3 core block types. Built-in widgets (`image`, `dartpad`, `qrcode`) use the same widget system as `@widget`. -## Common Properties +## Common properties All blocks support these properties: @@ -106,7 +106,7 @@ Embeds custom Flutter widgets defined in your app. Widget name from `DeckOptions.widgets`. -### Custom Arguments +### Custom arguments **Type:** JSON-serializable values All additional properties pass to the widget builder. @@ -142,7 +142,7 @@ SuperDeckApp( **Note:** Use explicit (`@widget { name: "chart" }`) or shorthand (`@chart`) syntax. All properties pass to `WidgetDefinition.parse()` as `Map`. -## Built-in Widgets +## Built-in widgets Use shorthand syntax for these widgets. @@ -194,7 +194,7 @@ Generates QR codes. - `errorCorrection` - `low`, `medium`, `high`, `highest` (default: `medium`) - `backgroundColor`, `foregroundColor` - Hex colors (for example, `#ffffff`) -### Mermaid Diagrams +### Mermaid diagrams Use a fenced code block with the `mermaid` language. The CLI renders Mermaid to PNG during `superdeck build`. @@ -205,9 +205,9 @@ graph TD ``` ```` -## Block Syntax +## Block syntax -### Basic Syntax +### Basic syntax ```markdown @blocktype { property1: value1 @@ -217,14 +217,14 @@ graph TD } ``` -### Property Types +### Property types - **String:** Use quotes for string values: `"hello world"` - **Number:** No quotes: `42`, `3.14` - **Boolean:** `true` or `false` - **Object:** Nested braces: `{ key: "value" }` - **Array:** Square brackets: `["item1", "item2"]` -### Common Patterns +### Common patterns **Centered content:** ```markdown @@ -273,7 +273,7 @@ graph TD } ``` -## Best Practices +## Best practices 1. Use `@column` for most content 2. Combine blocks in `@section` for rich layouts diff --git a/docs/reference/deck-options.mdx b/docs/reference/deck-options.mdx index 42dd3094..32b12520 100644 --- a/docs/reference/deck-options.mdx +++ b/docs/reference/deck-options.mdx @@ -1,9 +1,9 @@ --- -title: DeckOptions Reference +title: DeckOptions reference description: Complete API reference for configuring SuperDeck presentations --- -# DeckOptions Reference +# DeckOptions reference `DeckOptions` configures `SuperDeckApp` styling, custom widgets, slide parts, and debug visuals. @@ -16,6 +16,7 @@ const DeckOptions({ Map widgets = const {}, SlideParts parts = const SlideParts(), bool debug = false, + bool watchForChanges = false, }); ``` @@ -85,7 +86,7 @@ Or shorthand: } ``` -#### WidgetDefinition example (simple Map args) +#### WidgetDefinition example (simple map args) ```dart import 'package:flutter/widgets.dart'; @@ -122,7 +123,7 @@ DeckOptions( ) ``` -See [Custom Slide Parts Guide](/docs/guides/slide-parts) for examples. +See [Custom Slide Parts Guide](/guides/slide-parts) for examples. ### `debug` **Type:** `bool` | **Default:** `false` @@ -135,7 +136,19 @@ DeckOptions( ) ``` -## Complete Configuration Example +### `watchForChanges` +**Type:** `bool` | **Default:** `false` + +Enables file watching for automatic deck rebuilds when supported by your +runtime flow. + +```dart +DeckOptions( + watchForChanges: true, +) +``` + +## Complete configuration example ```dart import 'package:flutter/material.dart'; @@ -166,7 +179,7 @@ class MyApp extends StatelessWidget { } ``` -## Best Practices +## Best practices ### Styling 1. Start with `baseStyle` for defaults @@ -174,7 +187,7 @@ class MyApp extends StatelessWidget { 3. Maintain consistent palette and typography 4. Test across different screen sizes -### Custom Widgets +### Custom widgets 1. Validate in `parse()` early 2. Provide explicit defaults 3. Throw helpful errors (rendered on-slide) diff --git a/docs/reference/markdown-syntax.mdx b/docs/reference/markdown-syntax.mdx new file mode 100644 index 00000000..3c580a75 --- /dev/null +++ b/docs/reference/markdown-syntax.mdx @@ -0,0 +1,151 @@ +--- +title: Markdown syntax reference +description: Complete syntax reference for writing SuperDeck slides in Markdown +--- + +# Markdown syntax reference + +Use this reference to look up syntax for front matter, blocks, and built-in widget arguments. + +## Slide front matter + +Each slide can include YAML front matter. + +Supported keys: + +- `title`: Slide title used in navigation and exports. +- `style`: Named style variant from `DeckOptions.styles`. +- Custom keys: Available through `slide.options.args['keyName']`. + +Example: + +````markdown +--- +title: Product Vision +style: overview +owner: Platform Team +--- +```` + +## Block types + +SuperDeck supports three core block types. + +| Block | Purpose | Key properties | +|---|---|---| +| `@section` | Container for horizontal layout | `flex`, `align`, `scrollable` | +| `@column` | Render markdown content | `flex`, `align`, `scrollable` | +| `@widget` | Embed custom Flutter widgets | `name` + custom args | + +Built-in widgets (`image`, `dartpad`, `qrcode`) use the same widget syntax as `@widget`. + +## Common block properties + +### `flex` + +- Type: number +- Default: `1` +- Meaning: relative size in the parent layout. + +### `align` + +- Type: string +- Default: `center` +- Values: `top_left`, `top_center`, `top_right`, `center_left`, `center`, `center_right`, `bottom_left`, `bottom_center`, `bottom_right` + +### `scrollable` + +- Type: boolean +- Default: `false` +- Meaning: enable scrolling for overflow content. + +## Built-in widgets + +### `@image` + +Example: + +````markdown +@image { + src: assets/value-loop.png + fit: contain + height: 420 +} +```` + +Arguments: + +- `src` (required): asset path, absolute file path, or URL. +- `fit`: `contain`, `cover`, `fill`, `fitWidth`, `fitHeight`, `none`, `scaleDown`. +- `width`: number. +- `height`: number. + +### `@dartpad` + +Example: + +````markdown +@dartpad { + id: "d7b09149b0843f2b9d09e081e3cfd5a3" + theme: dark + run: true +} +```` + +Arguments: + +- `id` (required): DartPad snippet identifier. +- `theme`: `light` or `dark`. +- `embed`: boolean. +- `run`: boolean. + +### `@qrcode` + +Example: + +````markdown +@qrcode { + value: "https://superdeck.dev" + size: 220 +} +```` + +Arguments: + +- `value` (required): content to encode. +- `size`: number. +- `padding`: number. + +## Markdown extensions + +### Alerts + +Use GitHub-style alert syntax: + +````markdown +> [!NOTE] +> Additional context. + +> [!WARNING] +> Important warning. +```` + +### Hero classes + +Use classes to animate matching elements between slides: + +````markdown +# Roadmap {.hero-title} +![Diagram](assets/roadmap.png) {.hero-visual} +```` + +### Mermaid blocks + +Use fenced code blocks with `mermaid` as the language: + +````markdown +```mermaid +graph TD + Start --> Build --> Ship +``` +```` diff --git a/docs/tutorials/block-layouts.mdx b/docs/tutorials/block-layouts.mdx index 8f70d8a4..3d033d0f 100644 --- a/docs/tutorials/block-layouts.mdx +++ b/docs/tutorials/block-layouts.mdx @@ -1,13 +1,13 @@ --- -title: Block Layouts +title: Block layouts description: Quick overview of SuperDeck's block-based layout system --- -# Block Layouts +# Block layouts SuperDeck uses a block-based layout system for flexible content arrangement. Blocks are containers that hold different content types. -## Core Concept +## Core concept Blocks automatically arrange themselves: @@ -26,9 +26,9 @@ Blocks automatically arrange themselves: @widget # Custom Flutter widgets ``` -## Layout Examples +## Layout examples -### Single Column (Default) +### Single column (default) ```markdown # Title @@ -37,7 +37,7 @@ Regular markdown content flows vertically. More content below. ``` -### Two Columns +### Two columns ```markdown @section @@ -48,7 +48,7 @@ Left side content Right side content ``` -### Flexible Sizing +### Flexible sizing ```markdown @section @@ -63,7 +63,7 @@ Takes 2/3 of the space Takes 1/3 of the space ``` -### Mixed Content +### Mixed content ```markdown @section @@ -78,7 +78,7 @@ Takes 1/3 of the space } ``` -## Block Properties +## Block properties All blocks support these properties: @@ -94,9 +94,9 @@ All blocks support these properties: } ``` -## Layout Patterns +## Layout patterns -### Hero Section +### Hero section ```markdown @section @@ -107,7 +107,7 @@ All blocks support these properties: ## Subtitle ``` -### Content + Visual +### Content + visual ```markdown @section @@ -121,7 +121,7 @@ Detailed explanation... ![Feature Screenshot](image.png) ``` -### Three Columns +### Three columns ```markdown @section @@ -135,7 +135,7 @@ Detailed explanation... ### Option C ``` -## Key Takeaways +## Key takeaways - Start with `@column` blocks - Use `@section` for side-by-side layouts @@ -143,4 +143,4 @@ Detailed explanation... - Mix content types freely - Layouts adapt to screen size -Next: [First Presentation Tutorial](/docs/tutorials/first-presentation) +Next: [First Presentation Tutorial](/tutorials/first-presentation) diff --git a/docs/tutorials/first-presentation.mdx b/docs/tutorials/first-presentation.mdx index 1cf71eb3..9f2b238e 100644 --- a/docs/tutorials/first-presentation.mdx +++ b/docs/tutorials/first-presentation.mdx @@ -1,13 +1,13 @@ --- -title: First Presentation +title: First presentation description: Step-by-step tutorial to create your first complete SuperDeck presentation --- -# First Presentation Tutorial +# First presentation tutorial Create a complete presentation from scratch while learning SuperDeck's key features. -## What You'll Build +## What you'll build A Flutter development presentation with: - Centered title slide @@ -18,10 +18,10 @@ A Flutter development presentation with: ## Prerequisites -- SuperDeck installed and set up ([Getting Started](/docs/getting-started)) +- SuperDeck installed and set up ([Getting Started](/getting-started)) - Basic understanding of Markdown -## Step 1: Create a Title Slide +## Create a title slide In your `slides.md`: @@ -52,7 +52,7 @@ superdeck build --watch flutter run ``` -## Step 2: Add Content Slides +## Add content slides Add slides introducing Flutter concepts: @@ -79,7 +79,7 @@ Flutter is Google's **UI toolkit** for building natively compiled applications f --- ``` -## Step 3: Create Multi-Column Layout +## Create a multi-column layout ```markdown ## Why Choose Flutter? @@ -113,7 +113,7 @@ Flutter is Google's **UI toolkit** for building natively compiled applications f --- ``` -## Step 4: Add Code Examples +## Add code examples Include syntax-highlighted code: @@ -159,7 +159,7 @@ class MyApp extends StatelessWidget { --- ```` -## Step 5: Interactive Content +## Add interactive content Add a DartPad for live coding: @@ -191,7 +191,7 @@ Experiment with this Flutter code: Replace `your_dartpad_id` with an ID from [dartpad.dev](https://dartpad.dev). -## Step 6: Add Images +## Add images ```markdown ## Flutter in Action @@ -217,7 +217,7 @@ Replace `your_dartpad_id` with an ID from [dartpad.dev](https://dartpad.dev). --- ``` -## Step 7: Add Call-to-Action +## Add a call to action ````markdown ## Get Started Today! @@ -245,7 +245,7 @@ flutter run --- ```` -## Complete Example +## Complete example Here's the full `slides.md` file: @@ -343,14 +343,14 @@ flutter run --- ```` -## Next Steps +## Next steps -- **[Block Types Reference](/docs/reference/block-types)** - Learn all block types -- **[CLI Reference](/docs/guides/cli-reference)** - Master CLI tools -- **[DeckOptions API](/docs/reference/deck-options)** - Customize styles -- **[Examples](/docs/examples)** - View real-world examples +- **[Block Types Reference](/reference/block-types)** - Learn all block types +- **[CLI Reference](/guides/cli-reference)** - Master CLI tools +- **[DeckOptions API](/reference/deck-options)** - Customize styles +- **[Examples](/examples)** - View real-world examples -## Tips for Great Presentations +## Tips for great presentations 1. **Keep slides focused** - One concept per slide 2. **Use visuals** - Images and code enhance understanding diff --git a/packages/builder/lib/src/assets/mermaid_generator.dart b/packages/builder/lib/src/assets/mermaid_generator.dart index faecd37e..3f3f43c0 100644 --- a/packages/builder/lib/src/assets/mermaid_generator.dart +++ b/packages/builder/lib/src/assets/mermaid_generator.dart @@ -412,13 +412,13 @@ class MermaidGenerator implements AssetGenerator { final useFallbackTheme = _shouldUseFallbackTheme(graphDefinition); final theme = useFallbackTheme - ? 'default' // Use Mermaid's default theme for timeline/gantt + ? 'default' // Use Mermaid's default theme for timeline/gantt : (configuration['theme'] as String? ?? 'base'); final themeVariables = useFallbackTheme - ? {} // No custom variables for fallback + ? {} // No custom variables for fallback : (configuration['themeVariables'] ?? {}); final themeCSS = useFallbackTheme - ? '' // No custom CSS for fallback + ? '' // No custom CSS for fallback : (configuration['themeCSS'] as String? ?? ''); final look = configuration['look'] as String? ?? 'classic'; final securityLevel = configuration['securityLevel'] as String? ?? 'strict'; diff --git a/packages/builder/lib/src/deck_builder.dart b/packages/builder/lib/src/deck_builder.dart index e3b60d1c..933a5629 100644 --- a/packages/builder/lib/src/deck_builder.dart +++ b/packages/builder/lib/src/deck_builder.dart @@ -119,8 +119,6 @@ class DeckBuilder { /// This ensures resources like browser instances are properly cleaned up. Future dispose() async { // Convert FutureOr to Future for Future.wait compatibility - await Future.wait( - tasks.map((task) async => task.dispose()), - ); + await Future.wait(tasks.map((task) async => task.dispose())); } } diff --git a/packages/builder/lib/src/utils/process_utils.dart b/packages/builder/lib/src/utils/process_utils.dart index 54a09390..04f92c31 100644 --- a/packages/builder/lib/src/utils/process_utils.dart +++ b/packages/builder/lib/src/utils/process_utils.dart @@ -5,9 +5,5 @@ Future runDartCommand( List args, { Map? environmentOverrides, }) { - return Process.run( - 'dart', - args, - environment: environmentOverrides, - ); + return Process.run('dart', args, environment: environmentOverrides); } diff --git a/packages/builder/test/src/assets/mermaid_generator_test.dart b/packages/builder/test/src/assets/mermaid_generator_test.dart index 714255a6..3098e441 100644 --- a/packages/builder/test/src/assets/mermaid_generator_test.dart +++ b/packages/builder/test/src/assets/mermaid_generator_test.dart @@ -125,9 +125,7 @@ void main() { group('error handling configuration', () { test('timeout configuration is properly set', () { - final generator = MermaidGenerator( - configuration: const {'timeout': 5}, - ); + final generator = MermaidGenerator(configuration: const {'timeout': 5}); expect(generator.configuration['timeout'], equals(5)); }); diff --git a/packages/builder/test/src/deck_builder_test.dart b/packages/builder/test/src/deck_builder_test.dart index 80cfb5d3..82c4e67f 100644 --- a/packages/builder/test/src/deck_builder_test.dart +++ b/packages/builder/test/src/deck_builder_test.dart @@ -13,7 +13,7 @@ final class MockTask extends Task { final bool throwOnDispose; MockTask({this.disposeDelay, this.throwOnDispose = false}) - : super('MockTask'); + : super('MockTask'); @override FutureOr run(SlideContext context) { diff --git a/packages/builder/test/src/parsers/fenced_code_parser_test.dart b/packages/builder/test/src/parsers/fenced_code_parser_test.dart index 2772cc18..60086f50 100644 --- a/packages/builder/test/src/parsers/fenced_code_parser_test.dart +++ b/packages/builder/test/src/parsers/fenced_code_parser_test.dart @@ -161,7 +161,10 @@ code final blocks = parser.parse(content); expect(blocks, hasLength(1)); - expect(blocks[0].content, equals('code with leading/trailing whitespace')); + expect( + blocks[0].content, + equals('code with leading/trailing whitespace'), + ); }); test('handles unclosed code fence', () { diff --git a/packages/builder/test/src/slide_processor_test.dart b/packages/builder/test/src/slide_processor_test.dart index 2cce2a74..05c72659 100644 --- a/packages/builder/test/src/slide_processor_test.dart +++ b/packages/builder/test/src/slide_processor_test.dart @@ -12,11 +12,7 @@ base class MockTask extends Task { final bool shouldFail; final Exception? exceptionToThrow; - MockTask( - super.name, { - this.shouldFail = false, - this.exceptionToThrow, - }); + MockTask(super.name, {this.shouldFail = false, this.exceptionToThrow}); @override Future run(SlideContext context) async { @@ -147,11 +143,18 @@ void main() { final task1 = ContentModifierTask('[Task1]'); final task2 = ContentModifierTask('[Task2]'); - final slides = await processor.processAll([rawSlide], [task1, task2], store); + final slides = await processor.processAll( + [rawSlide], + [task1, task2], + store, + ); expect(slides, hasLength(1)); // Content should be modified by both tasks in order - expect(slides[0].sections[0].blocks[0].content, contains('[Task2][Task1]Original')); + expect( + slides[0].sections[0].blocks[0].content, + contains('[Task2][Task1]Original'), + ); }); test('maintains task execution order', () async { @@ -239,7 +242,9 @@ void main() { ); final task = MockTask('TestTask'); - final slides = await customProcessor.processAll(rawSlides, [task], store); + final slides = await customProcessor.processAll(rawSlides, [ + task, + ], store); expect(slides, hasLength(5)); expect(task.executedSlides, hasLength(5)); @@ -372,11 +377,10 @@ void main() { final trackingTask = MockTask('TrackingTask'); try { - await processor.processAll( - rawSlides, - [failOnThirdTask, trackingTask], - store, - ); + await processor.processAll(rawSlides, [ + failOnThirdTask, + trackingTask, + ], store); fail('Should have thrown TaskException'); } on TaskException catch (e) { expect(e.slideIndex, equals(2)); @@ -403,10 +407,7 @@ void main() { final rawSlide = RawSlideMarkdownType.parse({ 'key': 'slide-1', 'content': 'Content', - 'frontmatter': { - 'title': 'Test Title', - 'style': 'dark', - }, + 'frontmatter': {'title': 'Test Title', 'style': 'dark'}, }); final slides = await processor.processAll([rawSlide], [], store); @@ -480,7 +481,10 @@ Content here expect(slides, hasLength(1)); expect(slides[0].sections, isNotEmpty); - expect(slides[0].sections[0].blocks[0].content, contains('Just a heading')); + expect( + slides[0].sections[0].blocks[0].content, + contains('Just a heading'), + ); }); }); @@ -617,7 +621,10 @@ More content. final slides = await processor.processAll([rawSlide], [], store); expect(slides, hasLength(1)); - expect(slides[0].sections[0].blocks[0].content, contains('void main()')); + expect( + slides[0].sections[0].blocks[0].content, + contains('void main()'), + ); }); test('handles multiple complex frontmatter fields', () async { @@ -656,11 +663,11 @@ More content. final task2 = MockTask('Task2'); final task3 = MockTask('Task3'); - final slides = await processor.processAll( - rawSlides, - [task1, task2, task3], - store, - ); + final slides = await processor.processAll(rawSlides, [ + task1, + task2, + task3, + ], store); expect(slides, hasLength(6)); expect(task1.executedSlides, hasLength(6)); @@ -722,10 +729,7 @@ void example() { ''', - 'frontmatter': { - 'title': 'Complex Slide Title', - 'style': 'dark', - }, + 'frontmatter': {'title': 'Complex Slide Title', 'style': 'dark'}, }); final slides = await processor.processAll([rawSlide], [], store); @@ -741,7 +745,10 @@ void example() { } /// Helper for creating simple mock tasks -Task createMockTask(String name, {required Future Function(SlideContext) run}) { +Task createMockTask( + String name, { + required Future Function(SlideContext) run, +}) { return _SimpleMockTask(name, run); } diff --git a/packages/cli/lib/src/commands/build_command.dart b/packages/cli/lib/src/commands/build_command.dart index 0c73cd94..f2b95fbf 100644 --- a/packages/cli/lib/src/commands/build_command.dart +++ b/packages/cli/lib/src/commands/build_command.dart @@ -138,10 +138,7 @@ class BuildCommand extends SuperDeckCommand { // Track if we created the builder (and thus need to dispose it) final ownsBuilder = builder == null; - builder ??= _createStandardBuilder( - configuration: config, - store: store, - ); + builder ??= _createStandardBuilder(configuration: config, store: store); try { // Run the build process @@ -281,13 +278,21 @@ class BuildCommand extends SuperDeckCommand { case 'rebuild': logger.info('Manual rebuild triggered...'); // Reuse the watch builder to avoid spawning extra browser instances - unawaited(_runBuild(repository, deckConfig, builder: builder)); + unawaited( + _runBuild(repository, deckConfig, builder: builder), + ); break; case 'f': case 'force-rebuild': logger.info('Force rebuild triggered...'); // Reuse the watch builder to avoid spawning extra browser instances - unawaited(_cleanAndRebuild(repository, deckConfig, builder: builder)); + unawaited( + _cleanAndRebuild( + repository, + deckConfig, + builder: builder, + ), + ); break; case 'q': case 'quit': diff --git a/packages/cli/lib/src/commands/publish_command.dart b/packages/cli/lib/src/commands/publish_command.dart index fad2316f..dfed21b9 100644 --- a/packages/cli/lib/src/commands/publish_command.dart +++ b/packages/cli/lib/src/commands/publish_command.dart @@ -30,7 +30,7 @@ class PublishCommand extends Command { ); PublishCommand({Logger? loggerOverride}) - : _logger = loggerOverride ?? logger { + : _logger = loggerOverride ?? logger { argParser ..addOption( 'branch', diff --git a/packages/cli/lib/src/utils/update_pubspec.dart b/packages/cli/lib/src/utils/update_pubspec.dart index 9395c200..ccc3b569 100644 --- a/packages/cli/lib/src/utils/update_pubspec.dart +++ b/packages/cli/lib/src/utils/update_pubspec.dart @@ -45,8 +45,7 @@ String updatePubspecAssets( flutterSection['assets'] = assets; - final updatedYaml = Map.of(parsedYaml) - ..['flutter'] = flutterSection; + final updatedYaml = Map.of(parsedYaml)..['flutter'] = flutterSection; return YamlWriter(allowUnquotedStrings: true).write(updatedYaml); } diff --git a/packages/cli/test/src/commands/build_command_test.dart b/packages/cli/test/src/commands/build_command_test.dart index 016f9982..c171c947 100644 --- a/packages/cli/test/src/commands/build_command_test.dart +++ b/packages/cli/test/src/commands/build_command_test.dart @@ -52,10 +52,7 @@ void main() { }); test('has force-rebuild flag configured correctly', () { - expect( - command.argParser.options.containsKey('force-rebuild'), - isTrue, - ); + expect(command.argParser.options.containsKey('force-rebuild'), isTrue); final forceOption = command.argParser.options['force-rebuild']!; expect(forceOption.abbr, equals('f')); expect(forceOption.negatable, isFalse); @@ -66,9 +63,7 @@ void main() { group('run() - configuration loading', () { test('returns error code when slides file does not exist', () async { // Create a config file but no slides file - final configFile = File( - path.join(tempDir.path, 'superdeck.yaml'), - ); + final configFile = File(path.join(tempDir.path, 'superdeck.yaml')); await configFile.writeAsString('slides_path: slides.md'); final runner = createTestRunner(command); @@ -84,24 +79,26 @@ void main() { ); }); - test('loads default configuration when config file does not exist', - () async { - // Create slides file without config - final slidesFile = File(path.join(tempDir.path, 'slides.md')); - await slidesFile.writeAsString('# Test Slide\n\nContent'); - - final runner = createTestRunner(command); - final result = await runner.run(['build']); - - // Should succeed with default config - expect( - result, - anyOf( - equals(ExitCode.success.code), - equals(ExitCode.software.code), - ), - ); - }); + test( + 'loads default configuration when config file does not exist', + () async { + // Create slides file without config + final slidesFile = File(path.join(tempDir.path, 'slides.md')); + await slidesFile.writeAsString('# Test Slide\n\nContent'); + + final runner = createTestRunner(command); + final result = await runner.run(['build']); + + // Should succeed with default config + expect( + result, + anyOf( + equals(ExitCode.success.code), + equals(ExitCode.software.code), + ), + ); + }, + ); }); group('run() - basic build execution', () { @@ -120,10 +117,7 @@ This is test content. expect( result, - anyOf( - equals(ExitCode.success.code), - equals(ExitCode.software.code), - ), + anyOf(equals(ExitCode.success.code), equals(ExitCode.software.code)), ); }); @@ -155,10 +149,7 @@ This is test content. // Should not crash, may succeed or fail gracefully expect( result, - anyOf( - equals(ExitCode.success.code), - equals(ExitCode.software.code), - ), + anyOf(equals(ExitCode.success.code), equals(ExitCode.software.code)), ); }); }); @@ -213,9 +204,7 @@ version: 1.0.0 final slidesFile = File(path.join(tempDir.path, 'slides.md')); await slidesFile.writeAsString('# Test'); - final configFile = File( - path.join(tempDir.path, 'superdeck.yaml'), - ); + final configFile = File(path.join(tempDir.path, 'superdeck.yaml')); await configFile.writeAsString('invalid: yaml: content:'); createTestPubspec(tempDir); diff --git a/packages/core/lib/src/deck_configuration.dart b/packages/core/lib/src/deck_configuration.dart index 0522f5fc..25544910 100644 --- a/packages/core/lib/src/deck_configuration.dart +++ b/packages/core/lib/src/deck_configuration.dart @@ -60,11 +60,7 @@ final class DeckConfiguration { File(p.join(superdeckDir.path, 'superdeck_full.json')); Directory get assetsDir { - final validated = _validateRelativePath( - assetsPath, - 'assets', - 'assetsPath', - ); + final validated = _validateRelativePath(assetsPath, 'assets', 'assetsPath'); return Directory(p.join(superdeckDir.path, validated)); } diff --git a/packages/core/lib/src/models/asset_model.dart b/packages/core/lib/src/models/asset_model.dart index eac48959..12f0e7b3 100644 --- a/packages/core/lib/src/models/asset_model.dart +++ b/packages/core/lib/src/models/asset_model.dart @@ -127,10 +127,7 @@ class GeneratedAssetsReference { } Map toMap() { - return { - 'last_modified': lastModified.toIso8601String(), - 'files': files, - }; + return {'last_modified': lastModified.toIso8601String(), 'files': files}; } static GeneratedAssetsReference fromMap(Map map) { @@ -151,8 +148,6 @@ class GeneratedAssetsReference { const ListEquality().equals(files, other.files); @override - int get hashCode => Object.hash( - lastModified, - const ListEquality().hash(files), - ); + int get hashCode => + Object.hash(lastModified, const ListEquality().hash(files)); } diff --git a/packages/core/lib/src/models/deck_model.dart b/packages/core/lib/src/models/deck_model.dart index 760bef22..75db14c7 100644 --- a/packages/core/lib/src/models/deck_model.dart +++ b/packages/core/lib/src/models/deck_model.dart @@ -42,9 +42,7 @@ class Deck { /// Note: configuration is intentionally excluded from the schema as it's /// operational metadata (file paths) not content data. The class still /// supports configuration via constructor and fromMap() for backward compat. - static final schema = Ack.object({ - 'slides': Ack.list(Slide.schema), - }); + static final schema = Ack.object({'slides': Ack.list(Slide.schema)}); /// Parses a deck from a JSON map with validation. /// diff --git a/packages/core/lib/src/utils/extensions.dart b/packages/core/lib/src/utils/extensions.dart index 7364926f..31282323 100644 --- a/packages/core/lib/src/utils/extensions.dart +++ b/packages/core/lib/src/utils/extensions.dart @@ -74,9 +74,10 @@ extension HexColorValidation on StringSchema { (value) { final hexCode = value.replaceAll('#', ''); return (hexCode.length == 6 || hexCode.length == 8) && - RegExp(r'^[0-9a-fA-F]+$').hasMatch(hexCode); + RegExp(r'^[0-9a-fA-F]+$').hasMatch(hexCode); }, - message: 'Invalid hex color. Use 6 or 8 hex digits (e.g., "#ff0000" or "#80ff0000")', + message: + 'Invalid hex color. Use 6 or 8 hex digits (e.g., "#ff0000" or "#80ff0000")', ); } } diff --git a/packages/core/test/src/deck_configuration_test.dart b/packages/core/test/src/deck_configuration_test.dart index 04b49a98..50d8e3f3 100644 --- a/packages/core/test/src/deck_configuration_test.dart +++ b/packages/core/test/src/deck_configuration_test.dart @@ -71,10 +71,7 @@ void main() { test('is inside superdeckDir', () { final config = DeckConfiguration(); - expect( - config.deckJson.path, - contains(config.superdeckDir.path), - ); + expect(config.deckJson.path, contains(config.superdeckDir.path)); expect(config.deckJson.path, endsWith('superdeck.json')); }); }); @@ -83,10 +80,7 @@ void main() { test('is inside superdeckDir', () { final config = DeckConfiguration(); - expect( - config.deckFullJson.path, - contains(config.superdeckDir.path), - ); + expect(config.deckFullJson.path, contains(config.superdeckDir.path)); expect(config.deckFullJson.path, endsWith('superdeck_full.json')); }); }); @@ -107,10 +101,7 @@ void main() { test('is inside superdeckDir', () { final config = DeckConfiguration(); - expect( - config.assetsDir.path, - contains(config.superdeckDir.path), - ); + expect(config.assetsDir.path, contains(config.superdeckDir.path)); }); }); @@ -118,14 +109,8 @@ void main() { test('is inside superdeckDir', () { final config = DeckConfiguration(); - expect( - config.assetsRefJson.path, - contains(config.superdeckDir.path), - ); - expect( - config.assetsRefJson.path, - endsWith('generated_assets.json'), - ); + expect(config.assetsRefJson.path, contains(config.superdeckDir.path)); + expect(config.assetsRefJson.path, endsWith('generated_assets.json')); }); }); diff --git a/packages/core/test/src/deck_service_test.dart b/packages/core/test/src/deck_service_test.dart index 4b9605cb..9c73385d 100644 --- a/packages/core/test/src/deck_service_test.dart +++ b/packages/core/test/src/deck_service_test.dart @@ -152,8 +152,7 @@ void main() { final initialJson = jsonDecode(await mockConfig.assetsRefJson.readAsString()) as Map; - final initialLastModified = - initialJson['last_modified'] as String; + final initialLastModified = initialJson['last_modified'] as String; // Delay to ensure DateTime.now would differ if rewriting happens. await Future.delayed(const Duration(milliseconds: 5)); @@ -163,10 +162,7 @@ void main() { jsonDecode(await mockConfig.assetsRefJson.readAsString()) as Map; - expect( - subsequentJson['last_modified'], - equals(initialLastModified), - ); + expect(subsequentJson['last_modified'], equals(initialLastModified)); }, ); diff --git a/packages/core/test/src/models/asset_model_test.dart b/packages/core/test/src/models/asset_model_test.dart index d2f790f2..76a1cb3d 100644 --- a/packages/core/test/src/models/asset_model_test.dart +++ b/packages/core/test/src/models/asset_model_test.dart @@ -390,11 +390,7 @@ void main() { group('schema', () { test('validates correct structure', () { - final valid = { - 'name': 'test', - 'extension': 'png', - 'type': 'thumbnail', - }; + final valid = {'name': 'test', 'extension': 'png', 'type': 'thumbnail'}; expect(GeneratedAsset.schema.safeParse(valid).isOk, isTrue); }); diff --git a/packages/core/test/src/models/block_model_test.dart b/packages/core/test/src/models/block_model_test.dart index 705efe15..738c86b5 100644 --- a/packages/core/test/src/models/block_model_test.dart +++ b/packages/core/test/src/models/block_model_test.dart @@ -193,10 +193,7 @@ void main() { ContentAlignment.fromJson('TOPLEFT'), ContentAlignment.topLeft, ); - expect( - ContentAlignment.fromJson('Center'), - ContentAlignment.center, - ); + expect(ContentAlignment.fromJson('Center'), ContentAlignment.center); expect( ContentAlignment.fromJson('BOTTOM_CENTER'), ContentAlignment.bottomCenter, @@ -368,10 +365,7 @@ void main() { }); test('deserializes new block type', () { - final map = { - 'type': 'block', - 'content': 'New format', - }; + final map = {'type': 'block', 'content': 'New format'}; final block = ContentBlock.fromMap(map); expect(block.content, 'New format'); @@ -862,8 +856,9 @@ void main() { }); test('copyWith can be chained via multiple calls', () { - final block = - ContentBlock('Test').copyWith(flex: 3).copyWith(scrollable: true); + final block = ContentBlock( + 'Test', + ).copyWith(flex: 3).copyWith(scrollable: true); expect(block.flex, 3); expect(block.scrollable, true); diff --git a/packages/core/test/src/models/deck_model_test.dart b/packages/core/test/src/models/deck_model_test.dart index 9a49d0b9..eb02c00f 100644 --- a/packages/core/test/src/models/deck_model_test.dart +++ b/packages/core/test/src/models/deck_model_test.dart @@ -8,10 +8,7 @@ void main() { group('Deck Model', () { group('Deck', () { test('creates with required parameters', () { - final deck = Deck( - slides: const [], - configuration: DeckConfiguration(), - ); + final deck = Deck(slides: const [], configuration: DeckConfiguration()); expect(deck.slides, isEmpty); expect(deck.configuration, isNotNull); @@ -22,10 +19,7 @@ void main() { const Slide(key: 'slide-1'), const Slide(key: 'slide-2'), ]; - final deck = Deck( - slides: slides, - configuration: DeckConfiguration(), - ); + final deck = Deck(slides: slides, configuration: DeckConfiguration()); expect(deck.slides.length, 2); expect(deck.slides[0].key, 'slide-1'); @@ -38,9 +32,7 @@ void main() { slides: const [Slide(key: 'original')], configuration: DeckConfiguration(), ); - final copy = original.copyWith( - slides: const [Slide(key: 'new')], - ); + final copy = original.copyWith(slides: const [Slide(key: 'new')]); expect(copy.slides[0].key, 'new'); }); @@ -86,7 +78,9 @@ void main() { slides: [ Slide( key: 'slide-1', - sections: [SectionBlock([ContentBlock('Content')])], + sections: [ + SectionBlock([ContentBlock('Content')]), + ], ), ], configuration: DeckConfiguration(), @@ -116,9 +110,7 @@ void main() { group('fromMap', () { test('deserializes minimal map', () { - final map = { - 'slides': [], - }; + final map = {'slides': []}; final deck = Deck.fromMap(map); expect(deck.slides, isEmpty); @@ -141,10 +133,7 @@ void main() { test('deserializes map with configuration', () { final map = { 'slides': [], - 'configuration': { - 'projectDir': '/test', - 'outputDir': '.superdeck', - }, + 'configuration': {'projectDir': '/test', 'outputDir': '.superdeck'}, }; final deck = Deck.fromMap(map); @@ -287,9 +276,7 @@ void main() { group('schema', () { test('validates minimal deck', () { - final result = Deck.schema.safeParse({ - 'slides': [], - }); + final result = Deck.schema.safeParse({'slides': []}); expect(result.isOk, isTrue); }); diff --git a/packages/core/test/src/models/slide_model_test.dart b/packages/core/test/src/models/slide_model_test.dart index d7202a66..aae56271 100644 --- a/packages/core/test/src/models/slide_model_test.dart +++ b/packages/core/test/src/models/slide_model_test.dart @@ -15,7 +15,9 @@ void main() { }); test('creates with all parameters', () { - final sections = [SectionBlock([ContentBlock('Content')])]; + final sections = [ + SectionBlock([ContentBlock('Content')]), + ]; final comments = ['Speaker note 1', 'Speaker note 2']; final options = const SlideOptions(title: 'Title', style: 'custom'); @@ -51,7 +53,9 @@ void main() { test('copies with new sections', () { const original = Slide(key: 'key'); - final newSections = [SectionBlock([ContentBlock('New')])]; + final newSections = [ + SectionBlock([ContentBlock('New')]), + ]; final copy = original.copyWith(sections: newSections); expect(copy.sections.length, 1); @@ -68,7 +72,9 @@ void main() { final original = Slide( key: 'key', options: const SlideOptions(title: 'Title'), - sections: [SectionBlock([ContentBlock('Content')])], + sections: [ + SectionBlock([ContentBlock('Content')]), + ], comments: ['Note'], ); final copy = original.copyWith(); @@ -241,8 +247,7 @@ void main() { error: Exception('Details'), ); - final content = - (slide.sections[0].blocks[0] as ContentBlock).content; + final content = (slide.sections[0].blocks[0] as ContentBlock).content; expect(content.contains('Parse Error'), isTrue); expect(content.contains('Could not parse slide'), isTrue); }); @@ -254,8 +259,7 @@ void main() { error: Exception('Detailed error info'), ); - final content = - (slide.sections[0].blocks[0] as ContentBlock).content; + final content = (slide.sections[0].blocks[0] as ContentBlock).content; expect(content.contains('Detailed error info'), isTrue); expect(content.contains('```dart'), isTrue); }); @@ -304,11 +308,15 @@ void main() { test('different sections make slides unequal', () { final slide1 = Slide( key: 'key', - sections: [SectionBlock([ContentBlock('A')])], + sections: [ + SectionBlock([ContentBlock('A')]), + ], ); final slide2 = Slide( key: 'key', - sections: [SectionBlock([ContentBlock('B')])], + sections: [ + SectionBlock([ContentBlock('B')]), + ], ); expect(slide1, isNot(slide2)); @@ -483,10 +491,7 @@ void main() { }); test('parses map with additional properties', () { - final options = SlideOptions.parse({ - 'title': 'T', - 'extra': 'value', - }); + final options = SlideOptions.parse({'title': 'T', 'extra': 'value'}); expect(options.title, 'T'); expect(options.args['extra'], 'value'); diff --git a/packages/core/test/src/tag_tokenizer_test.dart b/packages/core/test/src/tag_tokenizer_test.dart index 89c8a2f7..ed5cf20e 100644 --- a/packages/core/test/src/tag_tokenizer_test.dart +++ b/packages/core/test/src/tag_tokenizer_test.dart @@ -285,9 +285,7 @@ Some paragraph text. expect( () => tokenizer.tokenize(text), throwsA( - predicate( - (e) => e.message.contains('myTag'), - ), + predicate((e) => e.message.contains('myTag')), ), ); }); @@ -379,10 +377,7 @@ not closed'''; // Use clearly invalid YAML syntax const text = '@tag { key: [unclosed }'; - expect( - () => tokenizer.tokenize(text), - throwsA(isA()), - ); + expect(() => tokenizer.tokenize(text), throwsA(isA())); }); test('throws DeckFormatException for unclosed braces', () { @@ -391,9 +386,7 @@ not closed'''; expect( () => tokenizer.tokenize(text), throwsA( - predicate( - (e) => e.message.contains('myTag'), - ), + predicate((e) => e.message.contains('myTag')), ), ); }); diff --git a/packages/core/test/src/utils/extensions_test.dart b/packages/core/test/src/utils/extensions_test.dart index d4fb6666..e4be2638 100644 --- a/packages/core/test/src/utils/extensions_test.dart +++ b/packages/core/test/src/utils/extensions_test.dart @@ -192,10 +192,7 @@ line3'''; await dir.ensureExists(); expect(await dir.exists(), isTrue); - expect( - await Directory('${tempDir.path}/deep').exists(), - isTrue, - ); + expect(await Directory('${tempDir.path}/deep').exists(), isTrue); expect( await Directory('${tempDir.path}/deep/nested').exists(), isTrue, @@ -361,7 +358,6 @@ line3'''; expect(schema.safeParse('rgb(255, 0, 0)').isOk, isFalse); }); }); - }); }); } diff --git a/packages/core/test/src/utils/yaml_utils_test.dart b/packages/core/test/src/utils/yaml_utils_test.dart index 15fe15cd..fc0b6a19 100644 --- a/packages/core/test/src/utils/yaml_utils_test.dart +++ b/packages/core/test/src/utils/yaml_utils_test.dart @@ -230,10 +230,7 @@ key: test('throws for clearly invalid YAML', () { const yaml = '{ key: value, }}}'; - expect( - () => convertYamlToMap(yaml, strict: true), - throwsA(anything), - ); + expect(() => convertYamlToMap(yaml, strict: true), throwsA(anything)); }); }); diff --git a/packages/superdeck/lib/src/deck/bundled_deck_service.dart b/packages/superdeck/lib/src/deck/bundled_deck_service.dart new file mode 100644 index 00000000..95bdbb95 --- /dev/null +++ b/packages/superdeck/lib/src/deck/bundled_deck_service.dart @@ -0,0 +1,50 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:flutter/services.dart'; +import 'package:superdeck_core/superdeck_core.dart'; + +/// Asset-based [DeckService] implementation for runtimes without file processes. +/// +/// The standard [DeckService] relies on local file I/O and file watchers. +/// In web and release-like runtimes, deck data is read from bundled assets. +class BundledDeckService extends DeckService { + BundledDeckService({ + required super.configuration, + this.deckAssetPath = '.superdeck/superdeck.json', + }); + + final String deckAssetPath; + final Logger _logger = Logger('BundledDeckService'); + + @override + Future initialize() async { + // No-op: bundled assets are generated ahead of time by the CLI. + } + + @override + Future loadDeck() async { + try { + final content = await rootBundle.loadString(deckAssetPath); + final data = jsonDecode(content) as Map; + return Deck.fromMap(data); + } on Object catch (error) { + return Deck( + slides: [ + Slide.error( + title: 'Superdeck reference error', + message: deckAssetPath, + error: error is Exception ? error : Exception(error.toString()), + ), + ], + configuration: configuration, + ); + } + } + + @override + Stream loadDeckStream() async* { + _logger.info('Loading bundled deck from assets...'); + yield await loadDeck(); + } +} diff --git a/packages/superdeck/lib/src/deck/deck_controller_builder.dart b/packages/superdeck/lib/src/deck/deck_controller_builder.dart index 65492fa4..9d592555 100644 --- a/packages/superdeck/lib/src/deck/deck_controller_builder.dart +++ b/packages/superdeck/lib/src/deck/deck_controller_builder.dart @@ -8,6 +8,7 @@ import 'package:superdeck_core/superdeck_core.dart'; import '../ui/widgets/provider.dart'; import '../utils/cli_watcher.dart'; import '../utils/constants.dart'; +import 'bundled_deck_service.dart'; import 'deck_controller.dart'; import 'deck_options.dart'; @@ -40,11 +41,15 @@ class _DeckControllerBuilderState extends State { super.initState(); final configuration = DeckConfiguration(); - final deckService = DeckService(configuration: configuration); + final deckService = kCanRunProcess + ? DeckService(configuration: configuration) + : BundledDeckService(configuration: configuration); _deckController = DeckController( deckService: deckService, options: widget.options, + // Asset-based runtimes load once; process-capable runtimes can file-watch. + enableDeckStream: kCanRunProcess, ); // Start CLI watcher in debug mode for auto-rebuild (if enabled) diff --git a/packages/superdeck/lib/src/deck/deck_options.dart b/packages/superdeck/lib/src/deck/deck_options.dart index 805dae6f..50b89531 100644 --- a/packages/superdeck/lib/src/deck/deck_options.dart +++ b/packages/superdeck/lib/src/deck/deck_options.dart @@ -11,9 +11,9 @@ class DeckOptions { /// Whether to watch for file changes and auto-rebuild the deck. /// - /// When `true` (default), starts a CLI watcher process that monitors + /// When `true`, starts a CLI watcher process that monitors /// the slides file and rebuilds automatically on changes. - /// Set to `false` to disable file watching. + /// Defaults to `false`. final bool watchForChanges; const DeckOptions({ diff --git a/packages/superdeck/lib/src/deck/navigation_input_listener.dart b/packages/superdeck/lib/src/deck/navigation_input_listener.dart index dcc2bde2..e3a8ee35 100644 --- a/packages/superdeck/lib/src/deck/navigation_input_listener.dart +++ b/packages/superdeck/lib/src/deck/navigation_input_listener.dart @@ -18,7 +18,8 @@ class NavigationInputListener extends StatefulWidget { final Widget child; @override - State createState() => _NavigationInputListenerState(); + State createState() => + _NavigationInputListenerState(); } class _NavigationInputListenerState extends State { diff --git a/packages/superdeck/lib/src/deck/navigation_service.dart b/packages/superdeck/lib/src/deck/navigation_service.dart index 1d08e165..7a349c74 100644 --- a/packages/superdeck/lib/src/deck/navigation_service.dart +++ b/packages/superdeck/lib/src/deck/navigation_service.dart @@ -18,15 +18,13 @@ class NavigationService { /// /// [transitionDuration] can be shortened for testing or adjusted for UX. NavigationService({Duration? transitionDuration}) - : transitionDuration = transitionDuration ?? _defaultTransitionDuration; + : transitionDuration = transitionDuration ?? _defaultTransitionDuration; /// Creates a GoRouter configured for slide navigation. /// /// The [onIndexChanged] callback is invoked on every page build. /// Deduplication of redundant calls is handled by the controller. - GoRouter createRouter({ - required void Function(int) onIndexChanged, - }) { + GoRouter createRouter({required void Function(int) onIndexChanged}) { return GoRouter( initialLocation: '/slides/0', // Handle root path - can occur on initial load or direct URL access diff --git a/packages/superdeck/lib/src/export/pdf_export_screen.dart b/packages/superdeck/lib/src/export/pdf_export_screen.dart index 5731f5d4..c1c0d3e7 100644 --- a/packages/superdeck/lib/src/export/pdf_export_screen.dart +++ b/packages/superdeck/lib/src/export/pdf_export_screen.dart @@ -99,10 +99,7 @@ class _PdfExportDialogScreenState extends State { return RepaintBoundary( key: _exportController.getSlideKey(slide), - child: InheritedData( - data: slide, - child: SlideView(slide), - ), + child: InheritedData(data: slide, child: SlideView(slide)), ); }, ), @@ -152,20 +149,20 @@ class _PdfExportBar extends StatelessWidget { children: [ switch (status) { PdfExportStatus.complete => Icon( - Icons.check_circle, - color: Theme.of(context).colorScheme.primary, - size: 32, - ), + Icons.check_circle, + color: Theme.of(context).colorScheme.primary, + size: 32, + ), PdfExportStatus.failed => Icon( - Icons.error, - color: Theme.of(context).colorScheme.error, - size: 32, - ), + Icons.error, + color: Theme.of(context).colorScheme.error, + size: 32, + ), _ => SizedBox( - height: 32, - width: 32, - child: IsometricProgressIndicator(progress: progressValue), - ), + height: 32, + width: 32, + child: IsometricProgressIndicator(progress: progressValue), + ), }, const SizedBox(height: 16.0), Text( diff --git a/packages/superdeck/lib/src/markdown/builders/text_element_builder.dart b/packages/superdeck/lib/src/markdown/builders/text_element_builder.dart index 11d7fc66..dfdef391 100644 --- a/packages/superdeck/lib/src/markdown/builders/text_element_builder.dart +++ b/packages/superdeck/lib/src/markdown/builders/text_element_builder.dart @@ -137,7 +137,9 @@ class TextElementBuilder extends MarkdownElementBuilder with MarkdownHeroMixin { children.add( TextSpan( text: lerp.fadingChar!, - style: baseStyle.copyWith(color: baseColor.withValues(alpha: visibleAlpha)), + style: baseStyle.copyWith( + color: baseColor.withValues(alpha: visibleAlpha), + ), ), ); } diff --git a/packages/superdeck/lib/src/rendering/blocks/block_widget.dart b/packages/superdeck/lib/src/rendering/blocks/block_widget.dart index 9053879c..9a4d754d 100644 --- a/packages/superdeck/lib/src/rendering/blocks/block_widget.dart +++ b/packages/superdeck/lib/src/rendering/blocks/block_widget.dart @@ -53,14 +53,12 @@ class _BlockContainerState extends State<_BlockContainer> { Widget content = InheritedData( data: blockData, - child: Box( - styleSpec: spec.blockContainer, - child: widget.child, - ), + child: Box(styleSpec: spec.blockContainer, child: widget.child), ); // Apply scrolling or wrap (for clipping non-scrollable content) - final shouldScroll = widget.block.scrollable && !widget.configuration.isExporting; + final shouldScroll = + widget.block.scrollable && !widget.configuration.isExporting; content = shouldScroll ? SingleChildScrollView(child: content) : Wrap(clipBehavior: Clip.hardEdge, children: [content]); @@ -185,11 +183,7 @@ class CustomBlockWidget extends StatelessWidget { /// Section widget that layouts child blocks horizontally. class SectionWidget extends StatelessWidget { - const SectionWidget({ - super.key, - required this.section, - required this.size, - }); + const SectionWidget({super.key, required this.section, required this.size}); final SectionBlock section; final Size size; @@ -241,10 +235,7 @@ ${size.width.toStringAsFixed(2)} x ${size.height.toStringAsFixed(2)}'''; // Add debug info overlay if needed if (configuration.debug) { blockWidget = Stack( - children: [ - blockWidget, - _renderDebugInfo(block, blockSize), - ], + children: [blockWidget, _renderDebugInfo(block, blockSize)], ); } diff --git a/packages/superdeck/lib/src/styling/components/slide.dart b/packages/superdeck/lib/src/styling/components/slide.dart index 9e2b526e..19a38359 100644 --- a/packages/superdeck/lib/src/styling/components/slide.dart +++ b/packages/superdeck/lib/src/styling/components/slide.dart @@ -463,14 +463,17 @@ final class SlideStyle extends Style StyleSpec resolve(BuildContext context) { // Resolve required StyleSpec fields with fallback defaults // These fields are non-nullable in SlideSpec and must always have a value - final resolvedAlert = MixOps.resolve(context, $alert) ?? + final resolvedAlert = + MixOps.resolve(context, $alert) ?? const StyleSpec(spec: MarkdownAlertSpec()); - final resolvedBlockContainer = MixOps.resolve(context, $blockContainer) ?? + final resolvedBlockContainer = + MixOps.resolve(context, $blockContainer) ?? const StyleSpec(spec: BoxSpec()); - final resolvedSlideContainer = MixOps.resolve(context, $slideContainer) ?? + final resolvedSlideContainer = + MixOps.resolve(context, $slideContainer) ?? const StyleSpec(spec: BoxSpec()); - final resolvedImage = MixOps.resolve(context, $image) ?? - const StyleSpec(spec: ImageSpec()); + final resolvedImage = + MixOps.resolve(context, $image) ?? const StyleSpec(spec: ImageSpec()); return StyleSpec( spec: SlideSpec( diff --git a/packages/superdeck/lib/src/styling/default_style.dart b/packages/superdeck/lib/src/styling/default_style.dart index 1c2dc47f..be8fa72c 100644 --- a/packages/superdeck/lib/src/styling/default_style.dart +++ b/packages/superdeck/lib/src/styling/default_style.dart @@ -18,8 +18,9 @@ TextStyle _safeGoogleFont(TextStyle Function() fontLoader) { } // Base text style for the presentation -TextStyle get _baseTextStyle => - _safeGoogleFont(GoogleFonts.poppins).copyWith(fontSize: 24, color: Colors.white); +TextStyle get _baseTextStyle => _safeGoogleFont( + GoogleFonts.poppins, +).copyWith(fontSize: 24, color: Colors.white); // Custom variants for different block types const onGist = NamedVariant('gist'); @@ -178,8 +179,9 @@ SlideStyle _createDefaultSlideStyle() { // Code blocks code: MarkdownCodeblockStyle( - textStyle: _safeGoogleFont(() => GoogleFonts.jetBrainsMono(fontSize: 18)) - .copyWith(height: 1.8), + textStyle: _safeGoogleFont( + () => GoogleFonts.jetBrainsMono(fontSize: 18), + ).copyWith(height: 1.8), container: BoxStyler( padding: EdgeInsetsMix.all(32), decoration: BoxDecorationMix( @@ -222,13 +224,13 @@ SlideStyle _createDefaultSlideStyle() { ), text: TextStyler() .style( - TextStyleMix( - fontSize: _baseTextStyle.fontSize, - height: 1.6, - color: _baseTextStyle.color, - fontFamily: _baseTextStyle.fontFamily, - ), - ) + TextStyleMix( + fontSize: _baseTextStyle.fontSize, + height: 1.6, + color: _baseTextStyle.color, + fontFamily: _baseTextStyle.fontFamily, + ), + ) .wrap(_pad(EdgeInsetsGeometryMix.only(bottom: 8))), ), @@ -254,7 +256,9 @@ SlideStyle _createDefaultSlideStyle() { // Horizontal rule horizontalRuleDecoration: BoxDecoration( - border: Border(bottom: BorderSide(color: _baseTextStyle.color!, width: 2)), + border: Border( + bottom: BorderSide(color: _baseTextStyle.color!, width: 2), + ), ), ); } diff --git a/packages/superdeck/lib/src/styling/schema/style_config.dart b/packages/superdeck/lib/src/styling/schema/style_config.dart index 0ca1d8ed..d90ea09e 100644 --- a/packages/superdeck/lib/src/styling/schema/style_config.dart +++ b/packages/superdeck/lib/src/styling/schema/style_config.dart @@ -76,7 +76,9 @@ class StyleConfigLoader { ); if (yamlConfig == null) { - _logger.fine('No YAML style configuration loaded, using code options only'); + _logger.fine( + 'No YAML style configuration loaded, using code options only', + ); return codeOptions; } @@ -95,10 +97,7 @@ class StyleConfigLoader { codeOptions.baseStyle, ); - final mergedStyles = _mergeStyleMaps( - yamlConfig.styles, - codeOptions.styles, - ); + final mergedStyles = _mergeStyleMaps(yamlConfig.styles, codeOptions.styles); return codeOptions.copyWith( baseStyle: mergedBaseStyle, @@ -159,7 +158,9 @@ class StyleConfigLoader { final result = StyleSchemas.styleConfigSchema.safeParse(map); if (result.isFail) { final error = result.getError(); - _logger.warning('Style configuration validation failed: ${error.message}'); + _logger.warning( + 'Style configuration validation failed: ${error.message}', + ); return null; } @@ -228,7 +229,10 @@ class StyleConfigLoader { }; } - static SlideStyle _mergeWithCode(SlideStyle? yamlStyle, SlideStyle codeStyle) { + static SlideStyle _mergeWithCode( + SlideStyle? yamlStyle, + SlideStyle codeStyle, + ) { return yamlStyle?.merge(codeStyle) ?? codeStyle; } } diff --git a/packages/superdeck/lib/src/styling/schema/style_schemas.dart b/packages/superdeck/lib/src/styling/schema/style_schemas.dart index ae85cce9..7a705fb1 100644 --- a/packages/superdeck/lib/src/styling/schema/style_schemas.dart +++ b/packages/superdeck/lib/src/styling/schema/style_schemas.dart @@ -295,8 +295,9 @@ class StyleSchemas { /// Validates slide style and transforms to [SlideStyle]. /// All nested values are already their Flutter types! - static final slideStyleSchema = - Ack.object(_slideStyleProperties).transform(_createSlideStyle); + static final slideStyleSchema = Ack.object( + _slideStyleProperties, + ).transform(_createSlideStyle); /// Validates named style and transforms to named tuple. static final namedStyleSchema = Ack.object({ @@ -316,18 +317,16 @@ class StyleSchemas { /// // config.baseStyle is SlideStyle? /// // config.styles is Map /// ``` - static final styleConfigSchema = Ack.object( - { - 'base': slideStyleSchema.optional(), - 'styles': Ack.list(namedStyleSchema).optional(), - }, - additionalProperties: true, - ) - .refine( - _validateUniqueStyleNames, - message: 'Duplicate style names found in styles list', - ) - .transform(_transformToStyleConfig); + static final styleConfigSchema = + Ack.object({ + 'base': slideStyleSchema.optional(), + 'styles': Ack.list(namedStyleSchema).optional(), + }, additionalProperties: true) + .refine( + _validateUniqueStyleNames, + message: 'Duplicate style names found in styles list', + ) + .transform(_transformToStyleConfig); // =========================================================================== // TRANSFORM FUNCTIONS @@ -416,8 +415,9 @@ class StyleSchemas { return BoxDecorationMix( color: color, - borderRadius: - borderRadius != null ? BorderRadiusMix.circular(borderRadius) : null, + borderRadius: borderRadius != null + ? BorderRadiusMix.circular(borderRadius) + : null, ); } @@ -546,8 +546,9 @@ class StyleSchemas { headStyle: _read(data, 'headStyle'), bodyStyle: _read(data, 'bodyStyle'), padding: paddingData != null ? _parseEdgeInsets(paddingData) : null, - cellPadding: - cellPaddingData != null ? _parseEdgeInsets(cellPaddingData) : null, + cellPadding: cellPaddingData != null + ? _parseEdgeInsets(cellPaddingData) + : null, cellDecoration: cellDecorationData != null ? BoxDecoration( color: cellDecorationData['color'] as Color?, @@ -650,9 +651,7 @@ class StyleSchemas { /// Creates a named style tuple from validated data. /// Note: data is non-null in practice (validation ensures valid input). - static ({String name, SlideStyle style}) _createNamedStyle( - _JsonMap? data, - ) { + static ({String name, SlideStyle style}) _createNamedStyle(_JsonMap? data) { final name = data!['name'] as String; // Create SlideStyle from all properties except 'name' final styleData = _JsonMap.from(data)..remove('name'); @@ -685,19 +684,20 @@ class StyleSchemas { /// Transforms to [StyleConfigResult]. /// Note: base is SlideStyle, styles is list of named tuples! - static StyleConfigResult _transformToStyleConfig( - Map? data, - ) { + static StyleConfigResult _transformToStyleConfig(Map? data) { // 'base' is already a SlideStyle from slideStyleSchema transform final baseStyle = data?['base'] as SlideStyle?; // 'styles' is already a List of named tuples from namedStyleSchema transform final styles = switch (data) { {'styles': final List stylesList} => { - for (final item in stylesList) - if (item case (name: final String name, style: final SlideStyle style)) - name: style, - }, + for (final item in stylesList) + if (item case ( + name: final String name, + style: final SlideStyle style, + )) + name: style, + }, _ => {}, }; diff --git a/packages/superdeck/lib/src/ui/panels/bottom_bar.dart b/packages/superdeck/lib/src/ui/panels/bottom_bar.dart index b272b379..9f275b9d 100644 --- a/packages/superdeck/lib/src/ui/panels/bottom_bar.dart +++ b/packages/superdeck/lib/src/ui/panels/bottom_bar.dart @@ -31,12 +31,14 @@ class DeckBottomBar extends StatelessWidget { style: _bottomBarContainer, children: [ // view notes - use Watch for reactive icon - Watch((context) => SDIconButton( - onPressed: deck.toggleNotes, - icon: deck.isNotesOpen.value - ? Icons.comment - : Icons.comments_disabled, - )), + Watch( + (context) => SDIconButton( + onPressed: deck.toggleNotes, + icon: deck.isNotesOpen.value + ? Icons.comment + : Icons.comments_disabled, + ), + ), SDIconButton( icon: Icons.save, @@ -48,21 +50,17 @@ class DeckBottomBar extends StatelessWidget { onPressed: () => deck.generateThumbnails(context, force: true), ), const Spacer(), - SDIconButton( - icon: Icons.arrow_back, - onPressed: deck.previousSlide, - ), - SDIconButton( - icon: Icons.arrow_forward, - onPressed: deck.nextSlide, - ), + SDIconButton(icon: Icons.arrow_back, onPressed: deck.previousSlide), + SDIconButton(icon: Icons.arrow_forward, onPressed: deck.nextSlide), const Spacer(), // Page counter - use Watch for reactive text - Watch((context) => Text( - '${deck.currentIndex.value + 1} of ${deck.totalSlides.value}', - style: const TextStyle(color: Colors.white), - )), + Watch( + (context) => Text( + '${deck.currentIndex.value + 1} of ${deck.totalSlides.value}', + style: const TextStyle(color: Colors.white), + ), + ), SDIconButton(icon: Icons.close, onPressed: deck.closeMenu), ], diff --git a/packages/superdeck/lib/src/utils/converters.dart b/packages/superdeck/lib/src/utils/converters.dart index e63d3f90..35f67122 100644 --- a/packages/superdeck/lib/src/utils/converters.dart +++ b/packages/superdeck/lib/src/utils/converters.dart @@ -17,7 +17,9 @@ class ConverterHelper { : null; return Offset( - padding.horizontal + margin.horizontal + (borderDimensions?.horizontal ?? 0.0), + padding.horizontal + + margin.horizontal + + (borderDimensions?.horizontal ?? 0.0), padding.vertical + margin.vertical + (borderDimensions?.vertical ?? 0.0), ); } @@ -59,28 +61,82 @@ class ConverterHelper { if (isHorizontal) { return switch (alignment) { - ContentAlignment.topLeft => (MainAxisAlignment.start, CrossAxisAlignment.start), - ContentAlignment.topCenter => (MainAxisAlignment.center, CrossAxisAlignment.start), - ContentAlignment.topRight => (MainAxisAlignment.end, CrossAxisAlignment.start), - ContentAlignment.centerLeft => (MainAxisAlignment.start, CrossAxisAlignment.center), - ContentAlignment.center => (MainAxisAlignment.center, CrossAxisAlignment.center), - ContentAlignment.centerRight => (MainAxisAlignment.end, CrossAxisAlignment.center), - ContentAlignment.bottomLeft => (MainAxisAlignment.start, CrossAxisAlignment.end), - ContentAlignment.bottomCenter => (MainAxisAlignment.center, CrossAxisAlignment.end), - ContentAlignment.bottomRight => (MainAxisAlignment.end, CrossAxisAlignment.end), + ContentAlignment.topLeft => ( + MainAxisAlignment.start, + CrossAxisAlignment.start, + ), + ContentAlignment.topCenter => ( + MainAxisAlignment.center, + CrossAxisAlignment.start, + ), + ContentAlignment.topRight => ( + MainAxisAlignment.end, + CrossAxisAlignment.start, + ), + ContentAlignment.centerLeft => ( + MainAxisAlignment.start, + CrossAxisAlignment.center, + ), + ContentAlignment.center => ( + MainAxisAlignment.center, + CrossAxisAlignment.center, + ), + ContentAlignment.centerRight => ( + MainAxisAlignment.end, + CrossAxisAlignment.center, + ), + ContentAlignment.bottomLeft => ( + MainAxisAlignment.start, + CrossAxisAlignment.end, + ), + ContentAlignment.bottomCenter => ( + MainAxisAlignment.center, + CrossAxisAlignment.end, + ), + ContentAlignment.bottomRight => ( + MainAxisAlignment.end, + CrossAxisAlignment.end, + ), }; } else { // Column: vertical main axis, horizontal cross axis return switch (alignment) { - ContentAlignment.topLeft => (MainAxisAlignment.start, CrossAxisAlignment.start), - ContentAlignment.topCenter => (MainAxisAlignment.start, CrossAxisAlignment.center), - ContentAlignment.topRight => (MainAxisAlignment.start, CrossAxisAlignment.end), - ContentAlignment.centerLeft => (MainAxisAlignment.center, CrossAxisAlignment.start), - ContentAlignment.center => (MainAxisAlignment.center, CrossAxisAlignment.center), - ContentAlignment.centerRight => (MainAxisAlignment.center, CrossAxisAlignment.end), - ContentAlignment.bottomLeft => (MainAxisAlignment.end, CrossAxisAlignment.start), - ContentAlignment.bottomCenter => (MainAxisAlignment.end, CrossAxisAlignment.center), - ContentAlignment.bottomRight => (MainAxisAlignment.end, CrossAxisAlignment.end), + ContentAlignment.topLeft => ( + MainAxisAlignment.start, + CrossAxisAlignment.start, + ), + ContentAlignment.topCenter => ( + MainAxisAlignment.start, + CrossAxisAlignment.center, + ), + ContentAlignment.topRight => ( + MainAxisAlignment.start, + CrossAxisAlignment.end, + ), + ContentAlignment.centerLeft => ( + MainAxisAlignment.center, + CrossAxisAlignment.start, + ), + ContentAlignment.center => ( + MainAxisAlignment.center, + CrossAxisAlignment.center, + ), + ContentAlignment.centerRight => ( + MainAxisAlignment.center, + CrossAxisAlignment.end, + ), + ContentAlignment.bottomLeft => ( + MainAxisAlignment.end, + CrossAxisAlignment.start, + ), + ContentAlignment.bottomCenter => ( + MainAxisAlignment.end, + CrossAxisAlignment.center, + ), + ContentAlignment.bottomRight => ( + MainAxisAlignment.end, + CrossAxisAlignment.end, + ), }; } } diff --git a/packages/superdeck/test/behavior/alignment_behavior_test.dart b/packages/superdeck/test/behavior/alignment_behavior_test.dart index a1ad3b84..52258231 100644 --- a/packages/superdeck/test/behavior/alignment_behavior_test.dart +++ b/packages/superdeck/test/behavior/alignment_behavior_test.dart @@ -39,27 +39,23 @@ void main() { group('alignment grid', () { testWidgets('renders all 9 in 3x3 grid', (tester) async { - await SlideTestHarness.pumpSlide( - tester, - SlideFixtures.allAlignments(), - ); + await SlideTestHarness.pumpSlide(tester, SlideFixtures.allAlignments()); tester.expectBlockCount(9); tester.expectSectionCount(3); expect(tester.takeException(), isNull); }); testWidgets('grid blocks do not overlap meaningfully', (tester) async { - await SlideTestHarness.pumpSlide( - tester, - SlideFixtures.allAlignments(), - ); + await SlideTestHarness.pumpSlide(tester, SlideFixtures.allAlignments()); final blocks = find.byType(BlockWidget); final rects = List.generate(9, (i) => tester.getRect(blocks.at(i))); for (int i = 0; i < rects.length; i++) { for (int j = i + 1; j < rects.length; j++) { final intersection = rects[i].intersect(rects[j]); expect( - intersection.isEmpty || intersection.width < 5 || intersection.height < 5, + intersection.isEmpty || + intersection.width < 5 || + intersection.height < 5, true, ); } @@ -72,9 +68,7 @@ void main() { final slide = Slide( key: 'small-centered', sections: [ - SectionBlock([ - ContentBlock('Hi', align: ContentAlignment.center), - ]), + SectionBlock([ContentBlock('Hi', align: ContentAlignment.center)]), ], ); await SlideTestHarness.pumpSlide(tester, slide); @@ -86,7 +80,9 @@ void main() { expect((blockRect.center.dx - textRect.center.dx).abs(), lessThan(50)); }); - testWidgets('large scrollable content respects alignment', (tester) async { + testWidgets('large scrollable content respects alignment', ( + tester, + ) async { final slide = Slide( key: 'large-top-left', sections: [ @@ -107,10 +103,7 @@ void main() { group('alignment in columns', () { testWidgets('different alignments per column', (tester) async { - await SlideTestHarness.pumpSlide( - tester, - SlideFixtures.allAlignments(), - ); + await SlideTestHarness.pumpSlide(tester, SlideFixtures.allAlignments()); tester.expectBlockCount(9); expect(tester.takeException(), isNull); }); @@ -121,8 +114,12 @@ void main() { final slide = Slide( key: 'section-alignments', sections: [ - SectionBlock([ContentBlock('Top', align: ContentAlignment.topCenter)]), - SectionBlock([ContentBlock('Bottom', align: ContentAlignment.bottomCenter)]), + SectionBlock([ + ContentBlock('Top', align: ContentAlignment.topCenter), + ]), + SectionBlock([ + ContentBlock('Bottom', align: ContentAlignment.bottomCenter), + ]), ], ); diff --git a/packages/superdeck/test/behavior/layout_behavior_test.dart b/packages/superdeck/test/behavior/layout_behavior_test.dart index b1ce753c..02fc34a6 100644 --- a/packages/superdeck/test/behavior/layout_behavior_test.dart +++ b/packages/superdeck/test/behavior/layout_behavior_test.dart @@ -40,16 +40,15 @@ void main() { group('column layouts', () { testWidgets('single column fills width', (tester) async { - await SlideTestHarness.pumpSlide( - tester, - SlideFixtures.singleColumn(), - ); + await SlideTestHarness.pumpSlide(tester, SlideFixtures.singleColumn()); final block = find.byType(BlockWidget); final size = tester.getSize(block); expect(size.width, greaterThan(700)); }); - testWidgets('two equal columns split width roughly 50/50', (tester) async { + testWidgets('two equal columns split width roughly 50/50', ( + tester, + ) async { await SlideTestHarness.pumpSlide( tester, SlideFixtures.twoColumnEqual(), @@ -83,7 +82,9 @@ void main() { }); group('section layouts', () { - testWidgets('two equal sections split height roughly 50/50', (tester) async { + testWidgets('two equal sections split height roughly 50/50', ( + tester, + ) async { await SlideTestHarness.pumpSlide( tester, SlideFixtures.twoSectionEqual(), @@ -100,7 +101,13 @@ void main() { SlideFixtures.twoSectionWeighted(topFlex: 1, bottomFlex: 2), ); final sections = find.byType(SectionWidget); - tester.expectFlexRatio(sections.at(0), sections.at(1), 1, 2, axis: Axis.vertical); + tester.expectFlexRatio( + sections.at(0), + sections.at(1), + 1, + 2, + axis: Axis.vertical, + ); }); testWidgets('header/body/footer 1:3:1', (tester) async { @@ -168,7 +175,9 @@ void main() { expect(tester.takeException(), isNull); }); - testWidgets('non-scrollable long content does not throw overflow', (tester) async { + testWidgets('non-scrollable long content does not throw overflow', ( + tester, + ) async { await SlideTestHarness.pumpSlide( tester, SlideFixtures.nonScrollableBlock(lineCount: 200), diff --git a/packages/superdeck/test/deck/deck_controller_test.dart b/packages/superdeck/test/deck/deck_controller_test.dart index 6ad53134..ad9a9968 100644 --- a/packages/superdeck/test/deck/deck_controller_test.dart +++ b/packages/superdeck/test/deck/deck_controller_test.dart @@ -226,13 +226,10 @@ void main() { test('updateOptions does not trigger if options unchanged', () { const options = DeckOptions(); // Verify idempotent behavior - calling twice with same options doesn't throw - expect( - () { - controller.updateOptions(options); - controller.updateOptions(options); - }, - returnsNormally, - ); + expect(() { + controller.updateOptions(options); + controller.updateOptions(options); + }, returnsNormally); }); }); diff --git a/packages/superdeck/test/deck/navigation_service_test.dart b/packages/superdeck/test/deck/navigation_service_test.dart index 2534fcf2..bc2b02f3 100644 --- a/packages/superdeck/test/deck/navigation_service_test.dart +++ b/packages/superdeck/test/deck/navigation_service_test.dart @@ -25,10 +25,7 @@ void main() { final service = NavigationService( transitionDuration: const Duration(milliseconds: -100), ); - expect( - service.transitionDuration, - const Duration(milliseconds: -100), - ); + expect(service.transitionDuration, const Duration(milliseconds: -100)); }); test('accepts very long duration', () { diff --git a/packages/superdeck/test/export/thumbnail_service_test.dart b/packages/superdeck/test/export/thumbnail_service_test.dart index 95912318..d274200c 100644 --- a/packages/superdeck/test/export/thumbnail_service_test.dart +++ b/packages/superdeck/test/export/thumbnail_service_test.dart @@ -16,60 +16,54 @@ void main() { } }); - test( - 'BUG: thumbnail write fails when assets directory does not exist', - () async { - // This test demonstrates the bug in ThumbnailService._generateThumbnail - // At line 75: await file.writeAsBytes(imageData); - // - // The issue: DeckService.initialize() is never called in DeckControllerBuilder, - // so the assets directory is never created. When ThumbnailService tries to - // write the thumbnail file, it fails with FileSystemException. - - // Simulate the path that would be generated for a thumbnail - final assetsDir = Directory('${tempDir.path}/.superdeck/assets'); - final thumbnailPath = '${assetsDir.path}/thumb-slide-abc123.png'; - - // The assets directory does NOT exist (simulating missing initialization) - expect(await assetsDir.exists(), isFalse); - - // This is exactly what happens at ThumbnailService line 75 - final file = File(thumbnailPath); - final imageData = [0x89, 0x50, 0x4E, 0x47]; // PNG header bytes - - // BUG: This throws FileSystemException because parent dir doesn't exist - expect( - () async => await file.writeAsBytes(imageData), - throwsA(isA()), - ); - }, - ); + test('BUG: thumbnail write fails when assets directory does not exist', () async { + // This test demonstrates the bug in ThumbnailService._generateThumbnail + // At line 75: await file.writeAsBytes(imageData); + // + // The issue: DeckService.initialize() is never called in DeckControllerBuilder, + // so the assets directory is never created. When ThumbnailService tries to + // write the thumbnail file, it fails with FileSystemException. + + // Simulate the path that would be generated for a thumbnail + final assetsDir = Directory('${tempDir.path}/.superdeck/assets'); + final thumbnailPath = '${assetsDir.path}/thumb-slide-abc123.png'; + + // The assets directory does NOT exist (simulating missing initialization) + expect(await assetsDir.exists(), isFalse); + + // This is exactly what happens at ThumbnailService line 75 + final file = File(thumbnailPath); + final imageData = [0x89, 0x50, 0x4E, 0x47]; // PNG header bytes + + // BUG: This throws FileSystemException because parent dir doesn't exist + expect( + () async => await file.writeAsBytes(imageData), + throwsA(isA()), + ); + }); - test( - 'FIX: thumbnail write succeeds when assets directory exists', - () async { - // This test shows the expected behavior after the fix. - // When DeckService.initialize() is called, it creates the assets directory, - // and thumbnail generation works correctly. + test('FIX: thumbnail write succeeds when assets directory exists', () async { + // This test shows the expected behavior after the fix. + // When DeckService.initialize() is called, it creates the assets directory, + // and thumbnail generation works correctly. - final assetsDir = Directory('${tempDir.path}/.superdeck/assets'); - final thumbnailPath = '${assetsDir.path}/thumb-slide-abc123.png'; + final assetsDir = Directory('${tempDir.path}/.superdeck/assets'); + final thumbnailPath = '${assetsDir.path}/thumb-slide-abc123.png'; - // FIX: Create the assets directory (what DeckService.initialize() does) - await assetsDir.create(recursive: true); - expect(await assetsDir.exists(), isTrue); + // FIX: Create the assets directory (what DeckService.initialize() does) + await assetsDir.create(recursive: true); + expect(await assetsDir.exists(), isTrue); - // Now the write succeeds - final file = File(thumbnailPath); - final imageData = [0x89, 0x50, 0x4E, 0x47]; // PNG header bytes + // Now the write succeeds + final file = File(thumbnailPath); + final imageData = [0x89, 0x50, 0x4E, 0x47]; // PNG header bytes - await file.writeAsBytes(imageData); + await file.writeAsBytes(imageData); - // Verify the file was created - expect(await file.exists(), isTrue); - expect(await file.length(), equals(4)); - }, - ); + // Verify the file was created + expect(await file.exists(), isTrue); + expect(await file.length(), equals(4)); + }); test( 'TDD FAILING TEST: ThumbnailService should ensure directory exists before writing', diff --git a/packages/superdeck/test/fixtures/slide_fixtures.dart b/packages/superdeck/test/fixtures/slide_fixtures.dart index 96a5ef87..40acf1e8 100644 --- a/packages/superdeck/test/fixtures/slide_fixtures.dart +++ b/packages/superdeck/test/fixtures/slide_fixtures.dart @@ -8,30 +8,19 @@ class SlideFixtures { /// Single column, single section, center aligned. static Slide singleColumn({String content = '# Hello World'}) { - return _slide( - 'fixture-single-column', - [ - SectionBlock([ - ContentBlock( - content, - align: ContentAlignment.center, - ), - ]), - ], - ); + return _slide('fixture-single-column', [ + SectionBlock([ContentBlock(content, align: ContentAlignment.center)]), + ]); } /// Two columns with equal flex (1:1). static Slide twoColumnEqual({String? left, String? right}) { - return _slide( - 'fixture-two-column-equal', - [ - SectionBlock([ - ContentBlock(left ?? '# Left'), - ContentBlock(right ?? '# Right'), - ]), - ], - ); + return _slide('fixture-two-column-equal', [ + SectionBlock([ + ContentBlock(left ?? '# Left'), + ContentBlock(right ?? '# Right'), + ]), + ]); } /// Two columns with unequal flex (1:2). @@ -41,29 +30,23 @@ class SlideFixtures { String? left, String? right, }) { - return _slide( - 'fixture-two-column-weighted', - [ - SectionBlock([ - ContentBlock(left ?? '# Left', flex: leftFlex), - ContentBlock(right ?? '# Right', flex: rightFlex), - ]), - ], - ); + return _slide('fixture-two-column-weighted', [ + SectionBlock([ + ContentBlock(left ?? '# Left', flex: leftFlex), + ContentBlock(right ?? '# Right', flex: rightFlex), + ]), + ]); } /// Three columns (1:1:1) for symmetric layouts. static Slide threeColumn() { - return _slide( - 'fixture-three-column', - [ - SectionBlock([ - ContentBlock('# One'), - ContentBlock('# Two'), - ContentBlock('# Three'), - ]), - ], - ); + return _slide('fixture-three-column', [ + SectionBlock([ + ContentBlock('# One'), + ContentBlock('# Two'), + ContentBlock('# Three'), + ]), + ]); } /// Three columns with custom flex (defaults 1:2:1) to test weighted widths. @@ -72,40 +55,31 @@ class SlideFixtures { int flex2 = 2, int flex3 = 1, }) { - return _slide( - 'fixture-three-column-weighted', - [ - SectionBlock([ - ContentBlock('# One', flex: flex1), - ContentBlock('# Two', flex: flex2), - ContentBlock('# Three', flex: flex3), - ]), - ], - ); + return _slide('fixture-three-column-weighted', [ + SectionBlock([ + ContentBlock('# One', flex: flex1), + ContentBlock('# Two', flex: flex2), + ContentBlock('# Three', flex: flex3), + ]), + ]); } // ---------- Multi-section layouts ---------- /// Two stacked sections with equal flex (1:1). static Slide twoSectionEqual() { - return _slide( - 'fixture-two-section-equal', - [ - SectionBlock([ContentBlock('# Top')], flex: 1), - SectionBlock([ContentBlock('# Bottom')], flex: 1), - ], - ); + return _slide('fixture-two-section-equal', [ + SectionBlock([ContentBlock('# Top')], flex: 1), + SectionBlock([ContentBlock('# Bottom')], flex: 1), + ]); } /// Two stacked sections with unequal flex (1:2). static Slide twoSectionWeighted({int topFlex = 1, int bottomFlex = 2}) { - return _slide( - 'fixture-two-section-weighted', - [ - SectionBlock([ContentBlock('# Top')], flex: topFlex), - SectionBlock([ContentBlock('# Bottom')], flex: bottomFlex), - ], - ); + return _slide('fixture-two-section-weighted', [ + SectionBlock([ContentBlock('# Top')], flex: topFlex), + SectionBlock([ContentBlock('# Bottom')], flex: bottomFlex), + ]); } /// Three stacked sections (header/body/footer pattern). @@ -114,55 +88,51 @@ class SlideFixtures { int bodyFlex = 3, int footerFlex = 1, }) { - return _slide( - 'fixture-three-section', - [ - SectionBlock([ContentBlock('Header')], flex: headerFlex), - SectionBlock([ContentBlock('Body')], flex: bodyFlex), - SectionBlock([ContentBlock('Footer')], flex: footerFlex), - ], - ); + return _slide('fixture-three-section', [ + SectionBlock([ContentBlock('Header')], flex: headerFlex), + SectionBlock([ContentBlock('Body')], flex: bodyFlex), + SectionBlock([ContentBlock('Footer')], flex: footerFlex), + ]); } /// Complex: two sections; first has two columns, second has one column. static Slide multiSectionMultiColumn() { - return _slide( - 'fixture-multi-section-multi-column', - [ - SectionBlock([ - ContentBlock('Top Left'), - ContentBlock('Top Right'), - ], flex: 1), - SectionBlock([ - ContentBlock('Bottom Single'), - ], flex: 1), - ], - ); + return _slide('fixture-multi-section-multi-column', [ + SectionBlock([ + ContentBlock('Top Left'), + ContentBlock('Top Right'), + ], flex: 1), + SectionBlock([ContentBlock('Bottom Single')], flex: 1), + ]); } // ---------- Alignment variations ---------- /// Single block with a specific alignment. static Slide withAlignment(ContentAlignment align) { - return _slide( - 'fixture-with-alignment-${align.name}', - [ - SectionBlock([ - ContentBlock( - '# Aligned ${align.name}', - align: align, - ), - ]), - ], - ); + return _slide('fixture-with-alignment-${align.name}', [ + SectionBlock([ContentBlock('# Aligned ${align.name}', align: align)]), + ]); } /// 3x3 grid showing all 9 alignment points. static Slide allAlignments() { final rows = [ - [ContentAlignment.topLeft, ContentAlignment.topCenter, ContentAlignment.topRight], - [ContentAlignment.centerLeft, ContentAlignment.center, ContentAlignment.centerRight], - [ContentAlignment.bottomLeft, ContentAlignment.bottomCenter, ContentAlignment.bottomRight], + [ + ContentAlignment.topLeft, + ContentAlignment.topCenter, + ContentAlignment.topRight, + ], + [ + ContentAlignment.centerLeft, + ContentAlignment.center, + ContentAlignment.centerRight, + ], + [ + ContentAlignment.bottomLeft, + ContentAlignment.bottomCenter, + ContentAlignment.bottomRight, + ], ]; return _slide( @@ -171,12 +141,7 @@ class SlideFixtures { .map( (row) => SectionBlock( row - .map( - (align) => ContentBlock( - align.name, - align: align, - ), - ) + .map((align) => ContentBlock(align.name, align: align)) .toList(), ), ) @@ -189,33 +154,17 @@ class SlideFixtures { /// Scrollable block with long content to exercise SingleChildScrollView path. static Slide scrollableBlock({int lineCount = 50}) { final content = List.filled(lineCount, 'Scrollable line').join('\n'); - return _slide( - 'fixture-scrollable-block', - [ - SectionBlock([ - ContentBlock( - content, - scrollable: true, - ), - ]), - ], - ); + return _slide('fixture-scrollable-block', [ + SectionBlock([ContentBlock(content, scrollable: true)]), + ]); } /// Non-scrollable block that should clip overflow (no scroll view). static Slide nonScrollableBlock({int lineCount = 50}) { final content = List.filled(lineCount, 'Non-scrollable line').join('\n'); - return _slide( - 'fixture-non-scrollable-block', - [ - SectionBlock([ - ContentBlock( - content, - scrollable: false, - ), - ]), - ], - ); + return _slide('fixture-non-scrollable-block', [ + SectionBlock([ContentBlock(content, scrollable: false)]), + ]); } /// Block that renders a custom widget via WidgetBlock. @@ -223,47 +172,31 @@ class SlideFixtures { required String widgetName, Map args = const {}, }) { - return _slide( - 'fixture-custom-widget-$widgetName', - [ - SectionBlock([ - WidgetBlock( - name: widgetName, - args: args, - ), - ]), - ], - ); + return _slide('fixture-custom-widget-$widgetName', [ + SectionBlock([WidgetBlock(name: widgetName, args: args)]), + ]); } /// Block containing fenced code block. static Slide withCodeBlock({String language = 'dart'}) { - return _slide( - 'fixture-code-block', - [ - SectionBlock([ - ContentBlock( - ''' + return _slide('fixture-code-block', [ + SectionBlock([ + ContentBlock(''' ```$language void main() { print('hello'); } ``` -''', - ), - ]), - ], - ); +'''), + ]), + ]); } /// Block with mixed markdown elements (headings, lists, code, image). static Slide mixedMarkdown() { - return _slide( - 'fixture-mixed-markdown', - [ - SectionBlock([ - ContentBlock( - ''' + return _slide('fixture-mixed-markdown', [ + SectionBlock([ + ContentBlock(''' # Title - Item 1 @@ -275,19 +208,14 @@ void main() { `inline code` ![img](assets/test.png) -''', - ), - ]), - ], - ); +'''), + ]), + ]); } // ---------- Helpers ---------- static Slide _slide(String key, List sections) { - return Slide( - key: key, - sections: sections, - ); + return Slide(key: key, sections: sections); } } diff --git a/packages/superdeck/test/flutter_test_config.dart b/packages/superdeck/test/flutter_test_config.dart index f9ac1ea3..7a9f73d7 100644 --- a/packages/superdeck/test/flutter_test_config.dart +++ b/packages/superdeck/test/flutter_test_config.dart @@ -20,11 +20,11 @@ Future testExecutable(FutureOr Function() testMain) async { // Mock path_provider to return a temp directory TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler( - const MethodChannel('plugins.flutter.io/path_provider'), - (MethodCall methodCall) async { - return Directory.systemTemp.path; - }, - ); + const MethodChannel('plugins.flutter.io/path_provider'), + (MethodCall methodCall) async { + return Directory.systemTemp.path; + }, + ); // Override HTTP to return 200 with minimal data for font requests HttpOverrides.global = _MockHttpOverrides(); @@ -58,30 +58,45 @@ class _MockHttpClient implements HttpClient { String? userAgent; @override - void addCredentials(Uri url, String realm, HttpClientCredentials credentials) {} + void addCredentials( + Uri url, + String realm, + HttpClientCredentials credentials, + ) {} @override void addProxyCredentials( - String host, int port, String realm, HttpClientCredentials credentials) {} + String host, + int port, + String realm, + HttpClientCredentials credentials, + ) {} @override set authenticate( - Future Function(Uri url, String scheme, String? realm)? f) {} + Future Function(Uri url, String scheme, String? realm)? f, + ) {} @override set authenticateProxy( - Future Function(String host, int port, String scheme, String? realm)? - f) {} + Future Function(String host, int port, String scheme, String? realm)? + f, + ) {} @override set badCertificateCallback( - bool Function(X509Certificate cert, String host, int port)? callback) {} + bool Function(X509Certificate cert, String host, int port)? callback, + ) {} @override set connectionFactory( - Future> Function( - Uri url, String? proxyHost, int? proxyPort)? - f) {} + Future> Function( + Uri url, + String? proxyHost, + int? proxyPort, + )? + f, + ) {} @override set findProxy(String Function(Uri url)? f) {} @@ -118,8 +133,11 @@ class _MockHttpClient implements HttpClient { @override Future open( - String method, String host, int port, String path) => - Future.value(_MockHttpClientRequest()); + String method, + String host, + int port, + String path, + ) => Future.value(_MockHttpClientRequest()); @override Future openUrl(String method, Uri url) => @@ -186,7 +204,8 @@ class _MockHttpClientRequest implements HttpClientRequest { List get cookies => []; @override - Future get done => Future.value(_MockHttpClientResponse()); + Future get done => + Future.value(_MockHttpClientResponse()); @override HttpHeaders get headers => _MockHttpHeaders(); @@ -268,8 +287,7 @@ class _MockHttpClientResponse extends Stream> String? method, Uri? url, bool? followLoops, - ]) => - Future.value(_MockHttpClientResponse()); + ]) => Future.value(_MockHttpClientResponse()); @override StreamSubscription> listen( diff --git a/packages/superdeck/test/helpers/layout_assertions.dart b/packages/superdeck/test/helpers/layout_assertions.dart index e79c74e8..f7e0e806 100644 --- a/packages/superdeck/test/helpers/layout_assertions.dart +++ b/packages/superdeck/test/helpers/layout_assertions.dart @@ -26,7 +26,8 @@ extension LayoutAssertions on WidgetTester { expect( actualRatio, closeTo(expectedRatio, tolerance), - reason: 'Flex ratio should be $flex1:$flex2 ($expectedRatio), got $actualRatio', + reason: + 'Flex ratio should be $flex1:$flex2 ($expectedRatio), got $actualRatio', ); } @@ -55,7 +56,8 @@ extension LayoutAssertions on WidgetTester { expect( actualDimension, closeTo(expectedDimension, tolerance), - reason: 'Widget $i should have dimension $expectedDimension, got $actualDimension', + reason: + 'Widget $i should have dimension $expectedDimension, got $actualDimension', ); } } diff --git a/packages/superdeck/test/markdown/builders/text_element_builder_widget_test.dart b/packages/superdeck/test/markdown/builders/text_element_builder_widget_test.dart index 28507335..2b7246c3 100644 --- a/packages/superdeck/test/markdown/builders/text_element_builder_widget_test.dart +++ b/packages/superdeck/test/markdown/builders/text_element_builder_widget_test.dart @@ -81,10 +81,10 @@ void main() { }); group('Code Block Rendering', () { - testWidgets('code blocks access BlockConfiguration from StyleSpecBuilder context', ( - tester, - ) async { - const markdown = ''' + testWidgets( + 'code blocks access BlockConfiguration from StyleSpecBuilder context', + (tester) async { + const markdown = ''' ```dart void main() { print('test'); @@ -92,21 +92,22 @@ void main() { ``` '''; - await tester.pumpWidget(_MarkdownHarness(markdown: markdown)); - await tester.pumpAndSettle(); + await tester.pumpWidget(_MarkdownHarness(markdown: markdown)); + await tester.pumpAndSettle(); - // Verify code renders through CodeElementBuilder (uses RichText) - expect(find.byType(RichText), findsWidgets); + // Verify code renders through CodeElementBuilder (uses RichText) + expect(find.byType(RichText), findsWidgets); - // Verify StyleSpecBuilder is in widget tree (proves BlockConfiguration access succeeded) - final allWidgets = tester.allWidgets.toList(); - final hasStyleSpecBuilder = allWidgets.any( - (widget) => - widget.toString().contains('StyleSpecBuilder') && - widget.toString().contains('MarkdownCodeblockSpec'), - ); - expect(hasStyleSpecBuilder, isTrue); - }); + // Verify StyleSpecBuilder is in widget tree (proves BlockConfiguration access succeeded) + final allWidgets = tester.allWidgets.toList(); + final hasStyleSpecBuilder = allWidgets.any( + (widget) => + widget.toString().contains('StyleSpecBuilder') && + widget.toString().contains('MarkdownCodeblockSpec'), + ); + expect(hasStyleSpecBuilder, isTrue); + }, + ); testWidgets( 'code blocks with Hero tag access BlockConfiguration for size calculation', @@ -132,24 +133,25 @@ void main() {} }); group('visitText Method', () { - testWidgets('text nodes access BlockConfiguration from StyleSpecBuilder context', ( - tester, - ) async { - const markdown = 'Plain text content'; + testWidgets( + 'text nodes access BlockConfiguration from StyleSpecBuilder context', + (tester) async { + const markdown = 'Plain text content'; - await tester.pumpWidget(_MarkdownHarness(markdown: markdown)); - await tester.pumpAndSettle(); + await tester.pumpWidget(_MarkdownHarness(markdown: markdown)); + await tester.pumpAndSettle(); - // Verify text is rendered via visitText method - expect(find.text('Plain text content'), findsOneWidget); + // Verify text is rendered via visitText method + expect(find.text('Plain text content'), findsOneWidget); - // Verify StyleSpecBuilder is in widget tree - final allWidgets = tester.allWidgets.toList(); - final hasStyleSpecBuilder = allWidgets.any( - (widget) => widget.toString().contains('StyleSpecBuilder'), - ); - expect(hasStyleSpecBuilder, isTrue); - }); + // Verify StyleSpecBuilder is in widget tree + final allWidgets = tester.allWidgets.toList(); + final hasStyleSpecBuilder = allWidgets.any( + (widget) => widget.toString().contains('StyleSpecBuilder'), + ); + expect(hasStyleSpecBuilder, isTrue); + }, + ); testWidgets('text nodes with Hero tag access BlockConfiguration correctly', ( tester, diff --git a/packages/superdeck/test/markdown/image_element_rendering_test.dart b/packages/superdeck/test/markdown/image_element_rendering_test.dart index f6974c09..9582fc4f 100644 --- a/packages/superdeck/test/markdown/image_element_rendering_test.dart +++ b/packages/superdeck/test/markdown/image_element_rendering_test.dart @@ -26,35 +26,36 @@ void main() { expect(builder.isBlockElement(), isTrue); }); - testWidgets('image builder callback executes with BlockConfiguration access', ( - tester, - ) async { - const markdown = '![test](assets/test.png)'; - - await tester.pumpWidget(_MarkdownHarness(markdown: markdown)); - await tester.pumpAndSettle(); - - // ASSERTION: The image should render through ImageElementBuilder - // Look for the CachedImage widget which proves the builder was called - expect( - find.byType(ConstrainedBox), - findsWidgets, - reason: - 'Image should render with ConstrainedBox from ImageElementBuilder', - ); - - // Verify StyleSpecBuilder was used (proves builder callback executed) - final allWidgets = tester.allWidgets.toList(); - final hasStyleSpecBuilder = allWidgets.any( - (widget) => widget.toString().contains('StyleSpecBuilder'), - ); - - expect( - hasStyleSpecBuilder, - isTrue, - reason: 'Should have StyleSpecBuilder in widget tree', - ); - }); + testWidgets( + 'image builder callback executes with BlockConfiguration access', + (tester) async { + const markdown = '![test](assets/test.png)'; + + await tester.pumpWidget(_MarkdownHarness(markdown: markdown)); + await tester.pumpAndSettle(); + + // ASSERTION: The image should render through ImageElementBuilder + // Look for the CachedImage widget which proves the builder was called + expect( + find.byType(ConstrainedBox), + findsWidgets, + reason: + 'Image should render with ConstrainedBox from ImageElementBuilder', + ); + + // Verify StyleSpecBuilder was used (proves builder callback executed) + final allWidgets = tester.allWidgets.toList(); + final hasStyleSpecBuilder = allWidgets.any( + (widget) => widget.toString().contains('StyleSpecBuilder'), + ); + + expect( + hasStyleSpecBuilder, + isTrue, + reason: 'Should have StyleSpecBuilder in widget tree', + ); + }, + ); testWidgets('image renders with block-level size constraints', ( tester, diff --git a/packages/superdeck/test/markdown/markdown_helpers_test.dart b/packages/superdeck/test/markdown/markdown_helpers_test.dart index 19e3fad9..12341bc7 100644 --- a/packages/superdeck/test/markdown/markdown_helpers_test.dart +++ b/packages/superdeck/test/markdown/markdown_helpers_test.dart @@ -271,13 +271,11 @@ void main() { // Early in fade-out: more characters visible final result1 = lerpStringWithFade(start, end, 0.1); - final length1 = result1.text.length + - (result1.hasFadingChar ? 1 : 0); + final length1 = result1.text.length + (result1.hasFadingChar ? 1 : 0); // Later in fade-out: fewer characters visible final result2 = lerpStringWithFade(start, end, 0.4); - final length2 = result2.text.length + - (result2.hasFadingChar ? 1 : 0); + final length2 = result2.text.length + (result2.hasFadingChar ? 1 : 0); expect(length1, greaterThan(length2)); }); @@ -309,13 +307,11 @@ void main() { // Early in fade-in: fewer characters visible final result1 = lerpStringWithFade(start, end, 0.6); - final length1 = result1.text.length + - (result1.hasFadingChar ? 1 : 0); + final length1 = result1.text.length + (result1.hasFadingChar ? 1 : 0); // Later in fade-in: more characters visible final result2 = lerpStringWithFade(start, end, 0.9); - final length2 = result2.text.length + - (result2.hasFadingChar ? 1 : 0); + final length2 = result2.text.length + (result2.hasFadingChar ? 1 : 0); expect(length1, lessThan(length2)); }); @@ -334,8 +330,11 @@ void main() { final result = lerpStringWithFade('Hi', 'Hi UI', 0.55); expect(result.text, endsWith(' '), reason: 'Space should be committed'); - expect(result.fadingChar, equals('U'), - reason: 'Next non-space grapheme should start fading immediately'); + expect( + result.fadingChar, + equals('U'), + reason: 'Next non-space grapheme should start fading immediately', + ); expect(result.hasFadingChar, isTrue); expect(result.ghostSuffix, equals('I')); }); @@ -352,8 +351,11 @@ void main() { '${commonPrefix}Universe', t, ); - expect(result.text, startsWith(commonPrefix), - reason: 'Failed at t=$t'); + expect( + result.text, + startsWith(commonPrefix), + reason: 'Failed at t=$t', + ); } }); @@ -371,16 +373,20 @@ void main() { final result = lerpStringWithFade('', 'Hello', 0.75); // Should be fading in characters from 'Hello' - expect(result.text.length + (result.hasFadingChar ? 1 : 0), - greaterThan(0)); + expect( + result.text.length + (result.hasFadingChar ? 1 : 0), + greaterThan(0), + ); }); test('handles empty end string', () { final result = lerpStringWithFade('Hello', '', 0.25); // Should be fading out characters from 'Hello' - expect(result.text.length + (result.hasFadingChar ? 1 : 0), - lessThan('Hello'.length)); + expect( + result.text.length + (result.hasFadingChar ? 1 : 0), + lessThan('Hello'.length), + ); }); test('handles both empty strings', () { @@ -407,8 +413,8 @@ void main() { for (var t = 0.0; t <= 1.0; t += 0.1) { final result = lerpStringWithFade(start, end, t); - final totalLength = result.text.length + - (result.fadingChar?.length ?? 0); + final totalLength = + result.text.length + (result.fadingChar?.length ?? 0); expect( totalLength, diff --git a/packages/superdeck/test/rendering/block_widget_test.dart b/packages/superdeck/test/rendering/block_widget_test.dart index 61877935..6a76b731 100644 --- a/packages/superdeck/test/rendering/block_widget_test.dart +++ b/packages/superdeck/test/rendering/block_widget_test.dart @@ -47,17 +47,16 @@ void main() { } testWidgets('all 9 alignments render in grid', (tester) async { - await SlideTestHarness.pumpSlide( - tester, - SlideFixtures.allAlignments(), - ); + await SlideTestHarness.pumpSlide(tester, SlideFixtures.allAlignments()); tester.expectBlockCount(9); expect(tester.takeException(), isNull); }); }); group('scrollable behavior', () { - testWidgets('scrollable block wraps content in ScrollView', (tester) async { + testWidgets('scrollable block wraps content in ScrollView', ( + tester, + ) async { await SlideTestHarness.pumpSlide( tester, SlideFixtures.scrollableBlock(lineCount: 80), @@ -73,7 +72,9 @@ void main() { tester.expectNotScrollable(find.byType(BlockWidget)); }); - testWidgets('scrollable block is NOT scrollable when exporting', (tester) async { + testWidgets('scrollable block is NOT scrollable when exporting', ( + tester, + ) async { await SlideTestHarness.pumpSlide( tester, SlideFixtures.scrollableBlock(lineCount: 80), @@ -84,7 +85,9 @@ void main() { }); group('error handling', () { - testWidgets('CustomBlockWidget shows error for unknown widget', (tester) async { + testWidgets('CustomBlockWidget shows error for unknown widget', ( + tester, + ) async { await SlideTestHarness.pumpSlide( tester, SlideFixtures.withCustomWidget(widgetName: 'nonexistent_widget_xyz'), @@ -95,34 +98,28 @@ void main() { group('size constraints', () { testWidgets('block fills section width', (tester) async { - await SlideTestHarness.pumpSlide( - tester, - SlideFixtures.singleColumn(), - ); + await SlideTestHarness.pumpSlide(tester, SlideFixtures.singleColumn()); final block = find.byType(BlockWidget); final size = tester.getSize(block); - expect(size.width, greaterThan(700)); // viewport may be smaller than kResolution + expect( + size.width, + greaterThan(700), + ); // viewport may be smaller than kResolution expect(size.height, greaterThan(300)); // header/footer reduce height }); }); group('markdown content types', () { testWidgets('renders headings and lists', (tester) async { - await SlideTestHarness.pumpSlide( - tester, - SlideFixtures.mixedMarkdown(), - ); + await SlideTestHarness.pumpSlide(tester, SlideFixtures.mixedMarkdown()); expect(find.textContaining('Title'), findsOneWidget); expect(find.textContaining('Item 1'), findsOneWidget); }); testWidgets('renders code block', (tester) async { - await SlideTestHarness.pumpSlide( - tester, - SlideFixtures.withCodeBlock(), - ); + await SlideTestHarness.pumpSlide(tester, SlideFixtures.withCodeBlock()); expect(find.byType(BlockWidget), findsOneWidget); expect(tester.takeException(), isNull); }); diff --git a/packages/superdeck/test/rendering/section_widget_test.dart b/packages/superdeck/test/rendering/section_widget_test.dart index b2c4e827..9312472f 100644 --- a/packages/superdeck/test/rendering/section_widget_test.dart +++ b/packages/superdeck/test/rendering/section_widget_test.dart @@ -12,10 +12,7 @@ void main() { group('SectionWidget', () { group('basic rendering', () { testWidgets('renders single block in section', (tester) async { - await SlideTestHarness.pumpSlide( - tester, - SlideFixtures.singleColumn(), - ); + await SlideTestHarness.pumpSlide(tester, SlideFixtures.singleColumn()); tester.expectSectionCount(1); tester.expectBlockCount(1); @@ -32,10 +29,7 @@ void main() { }); testWidgets('handles empty section without error', (tester) async { - final slide = Slide( - key: 'empty-section', - sections: [SectionBlock([])], - ); + final slide = Slide(key: 'empty-section', sections: [SectionBlock([])]); await SlideTestHarness.pumpSlide(tester, slide); expect(tester.takeException(), isNull); @@ -43,7 +37,9 @@ void main() { }); group('horizontal flex distribution', () { - testWidgets('two blocks with equal flex have equal widths', (tester) async { + testWidgets('two blocks with equal flex have equal widths', ( + tester, + ) async { await SlideTestHarness.pumpSlide( tester, SlideFixtures.twoColumnEqual(), @@ -59,7 +55,9 @@ void main() { ); }); - testWidgets('two blocks with 1:2 flex have correct width ratio', (tester) async { + testWidgets('two blocks with 1:2 flex have correct width ratio', ( + tester, + ) async { await SlideTestHarness.pumpSlide( tester, SlideFixtures.twoColumnWeighted(leftFlex: 1, rightFlex: 2), @@ -90,10 +88,7 @@ void main() { }); testWidgets('single block fills section width', (tester) async { - await SlideTestHarness.pumpSlide( - tester, - SlideFixtures.singleColumn(), - ); + await SlideTestHarness.pumpSlide(tester, SlideFixtures.singleColumn()); final section = find.byType(SectionWidget); final block = find.byType(BlockWidget); diff --git a/packages/superdeck/test/rendering/slide_view_test.dart b/packages/superdeck/test/rendering/slide_view_test.dart index 398f5729..d98fba99 100644 --- a/packages/superdeck/test/rendering/slide_view_test.dart +++ b/packages/superdeck/test/rendering/slide_view_test.dart @@ -13,10 +13,7 @@ void main() { group('SlideView', () { group('basic rendering', () { testWidgets('renders single section slide', (tester) async { - await SlideTestHarness.pumpSlide( - tester, - SlideFixtures.singleColumn(), - ); + await SlideTestHarness.pumpSlide(tester, SlideFixtures.singleColumn()); expect(find.byType(SlideView), findsOneWidget); tester.expectSectionCount(1); @@ -43,7 +40,9 @@ void main() { }); group('section layout - vertical flex', () { - testWidgets('two sections with equal flex have equal heights', (tester) async { + testWidgets('two sections with equal flex have equal heights', ( + tester, + ) async { await SlideTestHarness.pumpSlide( tester, SlideFixtures.twoSectionEqual(), @@ -59,7 +58,9 @@ void main() { ); }); - testWidgets('two sections with 1:2 flex have correct height ratio', (tester) async { + testWidgets('two sections with 1:2 flex have correct height ratio', ( + tester, + ) async { await SlideTestHarness.pumpSlide( tester, SlideFixtures.twoSectionWeighted(topFlex: 1, bottomFlex: 2), @@ -75,7 +76,9 @@ void main() { ); }); - testWidgets('three sections follow 1:3:1 flex distribution', (tester) async { + testWidgets('three sections follow 1:3:1 flex distribution', ( + tester, + ) async { await SlideTestHarness.pumpSlide( tester, SlideFixtures.threeSectionLayout(), @@ -103,7 +106,9 @@ void main() { }); group('slide size', () { - testWidgets('renders at default resolution (kResolution)', (tester) async { + testWidgets('renders at default resolution (kResolution)', ( + tester, + ) async { final originalSize = tester.view.physicalSize; final originalDpr = tester.view.devicePixelRatio; tester.view @@ -115,10 +120,7 @@ void main() { ..devicePixelRatio = originalDpr; }); - await SlideTestHarness.pumpSlide( - tester, - SlideFixtures.singleColumn(), - ); + await SlideTestHarness.pumpSlide(tester, SlideFixtures.singleColumn()); final slideView = find.byType(SlideView); final size = tester.getSize(slideView); @@ -139,10 +141,7 @@ void main() { ..devicePixelRatio = originalDpr; }); - await SlideTestHarness.pumpSlide( - tester, - SlideFixtures.singleColumn(), - ); + await SlideTestHarness.pumpSlide(tester, SlideFixtures.singleColumn()); final section = find.byType(SectionWidget); final size = tester.getSize(section); diff --git a/packages/superdeck/test/styling/schema/style_config_test.dart b/packages/superdeck/test/styling/schema/style_config_test.dart index a0d0fbc3..88f7fd43 100644 --- a/packages/superdeck/test/styling/schema/style_config_test.dart +++ b/packages/superdeck/test/styling/schema/style_config_test.dart @@ -24,7 +24,10 @@ void main() { final yamlStyle = SlideStyle( h1: TextStyler().style(TextStyleMix(fontSize: 120)), ); - final yamlConfig = (baseStyle: yamlStyle, styles: {}); + final yamlConfig = ( + baseStyle: yamlStyle, + styles: {}, + ); final codeOptions = const DeckOptions(); @@ -38,7 +41,10 @@ void main() { h1: TextStyler().style(TextStyleMix(fontSize: 96)), h2: TextStyler().style(TextStyleMix(fontSize: 72)), ); - final yamlConfig = (baseStyle: yamlStyle, styles: {}); + final yamlConfig = ( + baseStyle: yamlStyle, + styles: {}, + ); final codeStyle = SlideStyle( h1: TextStyler().style(TextStyleMix(fontSize: 120)), // override @@ -147,21 +153,24 @@ void main() { expect(result.baseStyle, isNotNull); }); - test('merges yaml with code options when loader returns valid yaml', () async { - final yamlContent = ''' + test( + 'merges yaml with code options when loader returns valid yaml', + () async { + final yamlContent = ''' base: h1: fontSize: 120 '''; - final codeOptions = const DeckOptions(); + final codeOptions = const DeckOptions(); - final result = await StyleConfigLoader.loadAndMerge( - codeOptions, - loader: () async => yamlContent, - ); + final result = await StyleConfigLoader.loadAndMerge( + codeOptions, + loader: () async => yamlContent, + ); - expect(result.baseStyle, isNotNull); - }); + expect(result.baseStyle, isNotNull); + }, + ); test('returns code options when yaml parsing fails', () async { final invalidYaml = 'invalid: yaml: content: ['; diff --git a/packages/superdeck/test/styling/schema/style_schemas_test.dart b/packages/superdeck/test/styling/schema/style_schemas_test.dart index afb0600d..f5c5259d 100644 --- a/packages/superdeck/test/styling/schema/style_schemas_test.dart +++ b/packages/superdeck/test/styling/schema/style_schemas_test.dart @@ -31,17 +31,20 @@ void main() { expect(color.b, 0.0); }); - test('accepts valid 8-digit hex color with alpha and transforms to Color', () { - final result = StyleSchemas.colorSchema.safeParse('#FF0000AA'); - expect(result.isOk, isTrue); - final color = result.getOrThrow(); - expect(color, isA()); - // 8-digit: FF0000AA parses as RRGGBBAA → 0xAAFF0000 in ARGB - expect(color!.a, closeTo(0xAA / 255, 0.01)); - expect(color.r, 1.0); - expect(color.g, 0.0); - expect(color.b, 0.0); - }); + test( + 'accepts valid 8-digit hex color with alpha and transforms to Color', + () { + final result = StyleSchemas.colorSchema.safeParse('#FF0000AA'); + expect(result.isOk, isTrue); + final color = result.getOrThrow(); + expect(color, isA()); + // 8-digit: FF0000AA parses as RRGGBBAA → 0xAAFF0000 in ARGB + expect(color!.a, closeTo(0xAA / 255, 0.01)); + expect(color.r, 1.0); + expect(color.g, 0.0); + expect(color.b, 0.0); + }, + ); test('accepts lowercase hex color', () { final result = StyleSchemas.colorSchema.safeParse('#ff0000'); @@ -142,9 +145,7 @@ void main() { // When used in an ObjectSchema, missing keys are handled by ObjectSchema. // Calling safeParse(null) directly tests explicit null, which is different. // Test with a parent object to show proper optional behavior: - final parentSchema = Ack.object({ - 'color': StyleSchemas.colorSchema, - }); + final parentSchema = Ack.object({'color': StyleSchemas.colorSchema}); final result = parentSchema.safeParse({}); expect(result.isOk, isTrue); // Key is absent from output when not provided @@ -180,7 +181,11 @@ void main() { for (final entry in weights.entries) { final result = StyleSchemas.fontWeightSchema.safeParse(entry.key); - expect(result.isOk, isTrue, reason: 'Expected ${entry.key} to be valid'); + expect( + result.isOk, + isTrue, + reason: 'Expected ${entry.key} to be valid', + ); expect(result.getOrThrow(), entry.value); } }); @@ -217,7 +222,11 @@ void main() { for (final entry in decorations.entries) { final result = StyleSchemas.textDecorationSchema.safeParse(entry.key); - expect(result.isOk, isTrue, reason: 'Expected ${entry.key} to be valid'); + expect( + result.isOk, + isTrue, + reason: 'Expected ${entry.key} to be valid', + ); expect(result.getOrThrow(), entry.value); } }); @@ -228,7 +237,9 @@ void main() { }); test('rejects underscore variant', () { - final result = StyleSchemas.textDecorationSchema.safeParse('line_through'); + final result = StyleSchemas.textDecorationSchema.safeParse( + 'line_through', + ); expect(result.isFail, isTrue); }); @@ -251,7 +262,11 @@ void main() { for (final entry in alignments.entries) { final result = StyleSchemas.alignmentSchema.safeParse(entry.key); - expect(result.isOk, isTrue, reason: 'Expected ${entry.key} to be valid'); + expect( + result.isOk, + isTrue, + reason: 'Expected ${entry.key} to be valid', + ); expect(result.getOrThrow(), entry.value); } }); @@ -309,9 +324,7 @@ void main() { }); test('accepts object with only vertical', () { - final result = StyleSchemas.paddingSchema.safeParse({ - 'vertical': 8.0, - }); + final result = StyleSchemas.paddingSchema.safeParse({'vertical': 8.0}); expect(result.isOk, isTrue); }); @@ -366,9 +379,7 @@ void main() { }); test('rejects unknown object keys', () { - final result = StyleSchemas.paddingSchema.safeParse({ - 'invalid': 16.0, - }); + final result = StyleSchemas.paddingSchema.safeParse({'invalid': 16.0}); expect(result.isFail, isTrue); }); }); @@ -440,18 +451,13 @@ void main() { }); test('accepts valid container with margin', () { - final result = StyleSchemas.containerSchema.safeParse({ - 'margin': 8.0, - }); + final result = StyleSchemas.containerSchema.safeParse({'margin': 8.0}); expect(result.isOk, isTrue); }); test('accepts valid container with decoration', () { final result = StyleSchemas.containerSchema.safeParse({ - 'decoration': { - 'color': '#FFFFFF', - 'borderRadius': 12.0, - }, + 'decoration': {'color': '#FFFFFF', 'borderRadius': 12.0}, }); expect(result.isOk, isTrue); }); @@ -460,10 +466,7 @@ void main() { final result = StyleSchemas.containerSchema.safeParse({ 'padding': 16.0, 'margin': 8.0, - 'decoration': { - 'color': '#000000', - 'borderRadius': 4.0, - }, + 'decoration': {'color': '#000000', 'borderRadius': 4.0}, }); expect(result.isOk, isTrue); }); @@ -474,9 +477,7 @@ void main() { }); test('rejects unknown container keys', () { - final result = StyleSchemas.containerSchema.safeParse({ - 'width': 100.0, - }); + final result = StyleSchemas.containerSchema.safeParse({'width': 100.0}); expect(result.isFail, isTrue); }); }); @@ -530,9 +531,7 @@ void main() { }); test('rejects zero fontSize', () { - final result = StyleSchemas.textStyleSchema.safeParse({ - 'fontSize': 0, - }); + final result = StyleSchemas.textStyleSchema.safeParse({'fontSize': 0}); expect(result.isFail, isTrue); }); @@ -649,10 +648,7 @@ void main() { }, 'container': { 'padding': 16.0, - 'decoration': { - 'color': '#000000', - 'borderRadius': 8.0, - }, + 'decoration': {'color': '#000000', 'borderRadius': 8.0}, }, }); expect(result.isOk, isTrue); @@ -661,18 +657,14 @@ void main() { test('accepts code style with only textStyle', () { final result = StyleSchemas.codeStyleSchema.safeParse({ - 'textStyle': { - 'fontSize': 16.0, - }, + 'textStyle': {'fontSize': 16.0}, }); expect(result.isOk, isTrue); }); test('accepts code style with only container', () { final result = StyleSchemas.codeStyleSchema.safeParse({ - 'container': { - 'padding': 32.0, - }, + 'container': {'padding': 32.0}, }); expect(result.isOk, isTrue); }); @@ -686,18 +678,9 @@ void main() { group('blockquoteSchema', () { test('accepts valid blockquote config', () { final result = StyleSchemas.blockquoteSchema.safeParse({ - 'textStyle': { - 'fontSize': 32.0, - 'color': '#CCCCCC', - }, - 'padding': { - 'left': 30.0, - 'bottom': 12.0, - }, - 'decoration': { - 'color': '#888888', - 'borderRadius': 4.0, - }, + 'textStyle': {'fontSize': 32.0, 'color': '#CCCCCC'}, + 'padding': {'left': 30.0, 'bottom': 12.0}, + 'decoration': {'color': '#888888', 'borderRadius': 4.0}, 'alignment': 'start', }); expect(result.isOk, isTrue); @@ -706,18 +689,14 @@ void main() { test('accepts blockquote with minimal config', () { final result = StyleSchemas.blockquoteSchema.safeParse({ - 'textStyle': { - 'fontSize': 24.0, - }, + 'textStyle': {'fontSize': 24.0}, }); expect(result.isOk, isTrue); }); test('accepts blockquote with only padding', () { final result = StyleSchemas.blockquoteSchema.safeParse({ - 'padding': { - 'all': 16.0, - }, + 'padding': {'all': 16.0}, }); expect(result.isOk, isTrue); }); @@ -738,14 +717,8 @@ void main() { group('listSchema', () { test('accepts valid list config', () { final result = StyleSchemas.listSchema.safeParse({ - 'bullet': { - 'fontSize': 24.0, - 'color': '#FFFFFF', - }, - 'text': { - 'fontSize': 24.0, - 'height': 1.6, - }, + 'bullet': {'fontSize': 24.0, 'color': '#FFFFFF'}, + 'text': {'fontSize': 24.0, 'height': 1.6}, 'orderedAlignment': 'start', 'unorderedAlignment': 'start', }); @@ -755,18 +728,14 @@ void main() { test('accepts list with only bullet', () { final result = StyleSchemas.listSchema.safeParse({ - 'bullet': { - 'fontSize': 20.0, - }, + 'bullet': {'fontSize': 20.0}, }); expect(result.isOk, isTrue); }); test('accepts list with only text', () { final result = StyleSchemas.listSchema.safeParse({ - 'text': { - 'fontSize': 22.0, - }, + 'text': {'fontSize': 22.0}, }); expect(result.isOk, isTrue); }); @@ -787,10 +756,7 @@ void main() { group('checkboxSchema', () { test('accepts valid checkbox config', () { final result = StyleSchemas.checkboxSchema.safeParse({ - 'textStyle': { - 'fontSize': 20.0, - 'color': '#FFFFFF', - }, + 'textStyle': {'fontSize': 20.0, 'color': '#FFFFFF'}, }); expect(result.isOk, isTrue); expect(result.getOrThrow(), isA()); @@ -813,23 +779,11 @@ void main() { group('tableSchema', () { test('accepts valid table config', () { final result = StyleSchemas.tableSchema.safeParse({ - 'headStyle': { - 'fontSize': 24.0, - 'fontWeight': 'bold', - }, - 'bodyStyle': { - 'fontSize': 20.0, - }, - 'padding': { - 'all': 8.0, - }, - 'cellPadding': { - 'all': 12.0, - }, - 'cellDecoration': { - 'color': '#F0F0F0', - 'borderRadius': 2.0, - }, + 'headStyle': {'fontSize': 24.0, 'fontWeight': 'bold'}, + 'bodyStyle': {'fontSize': 20.0}, + 'padding': {'all': 8.0}, + 'cellPadding': {'all': 12.0}, + 'cellDecoration': {'color': '#F0F0F0', 'borderRadius': 2.0}, }); expect(result.isOk, isTrue); expect(result.getOrThrow(), isA()); @@ -837,9 +791,7 @@ void main() { test('accepts table with minimal config', () { final result = StyleSchemas.tableSchema.safeParse({ - 'headStyle': { - 'fontWeight': 'bold', - }, + 'headStyle': {'fontWeight': 'bold'}, }); expect(result.isOk, isTrue); }); @@ -861,18 +813,11 @@ void main() { group('alertTypeSchema', () { test('accepts valid alert type config', () { final result = StyleSchemas.alertTypeSchema.safeParse({ - 'heading': { - 'fontSize': 24.0, - 'fontWeight': 'bold', - }, - 'description': { - 'fontSize': 20.0, - }, + 'heading': {'fontSize': 24.0, 'fontWeight': 'bold'}, + 'description': {'fontSize': 20.0}, 'container': { 'padding': 16.0, - 'decoration': { - 'color': '#E3F2FD', - }, + 'decoration': {'color': '#E3F2FD'}, }, }); expect(result.isOk, isTrue); @@ -881,9 +826,7 @@ void main() { test('accepts alert type with only heading', () { final result = StyleSchemas.alertTypeSchema.safeParse({ - 'heading': { - 'fontSize': 28.0, - }, + 'heading': {'fontSize': 28.0}, }); expect(result.isOk, isTrue); }); @@ -956,19 +899,10 @@ void main() { group('slideStyleSchema', () { test('accepts valid slide style config', () { final result = StyleSchemas.slideStyleSchema.safeParse({ - 'h1': { - 'fontSize': 96.0, - 'fontWeight': 'bold', - 'color': '#FFFFFF', - }, - 'p': { - 'fontSize': 24.0, - 'color': '#CCCCCC', - }, + 'h1': {'fontSize': 96.0, 'fontWeight': 'bold', 'color': '#FFFFFF'}, + 'p': {'fontSize': 24.0, 'color': '#CCCCCC'}, 'code': { - 'textStyle': { - 'fontFamily': 'Fira Code', - }, + 'textStyle': {'fontFamily': 'Fira Code'}, }, }); expect(result.isOk, isTrue); @@ -1001,13 +935,25 @@ void main() { test('accepts all block elements', () { final result = StyleSchemas.slideStyleSchema.safeParse({ - 'code': {'textStyle': {'fontSize': 16.0}}, - 'blockquote': {'textStyle': {'fontSize': 32.0}}, - 'list': {'text': {'fontSize': 24.0}}, - 'checkbox': {'textStyle': {'fontSize': 20.0}}, - 'table': {'headStyle': {'fontWeight': 'bold'}}, + 'code': { + 'textStyle': {'fontSize': 16.0}, + }, + 'blockquote': { + 'textStyle': {'fontSize': 32.0}, + }, + 'list': { + 'text': {'fontSize': 24.0}, + }, + 'checkbox': { + 'textStyle': {'fontSize': 20.0}, + }, + 'table': { + 'headStyle': {'fontWeight': 'bold'}, + }, 'alert': { - 'note': {'heading': {'fontSize': 24.0}}, + 'note': { + 'heading': {'fontSize': 24.0}, + }, }, }); expect(result.isOk, isTrue); @@ -1015,22 +961,15 @@ void main() { test('accepts containers', () { final result = StyleSchemas.slideStyleSchema.safeParse({ - 'blockContainer': { - 'padding': 40.0, - }, - 'slideContainer': { - 'padding': 20.0, - }, + 'blockContainer': {'padding': 40.0}, + 'slideContainer': {'padding': 20.0}, }); expect(result.isOk, isTrue); }); test('accepts horizontalRuleDecoration', () { final result = StyleSchemas.slideStyleSchema.safeParse({ - 'horizontalRuleDecoration': { - 'color': '#CCCCCC', - 'borderRadius': 2.0, - }, + 'horizontalRuleDecoration': {'color': '#CCCCCC', 'borderRadius': 2.0}, }); expect(result.isOk, isTrue); }); @@ -1095,7 +1034,9 @@ void main() { test('accepts hyphenated names', () { final result = StyleSchemas.namedStyleSchema.safeParse({ 'name': 'code-heavy', - 'code': {'textStyle': {'fontSize': 14.0}}, + 'code': { + 'textStyle': {'fontSize': 14.0}, + }, }); expect(result.isOk, isTrue); }); @@ -1103,7 +1044,9 @@ void main() { test('accepts underscore names', () { final result = StyleSchemas.namedStyleSchema.safeParse({ 'name': 'code_heavy', - 'code': {'textStyle': {'fontSize': 14.0}}, + 'code': { + 'textStyle': {'fontSize': 14.0}, + }, }); expect(result.isOk, isTrue); }); @@ -1173,8 +1116,14 @@ void main() { test('rejects duplicate style names', () { final result = StyleSchemas.styleConfigSchema.safeParse({ 'styles': [ - {'name': 'title', 'h1': {'fontSize': 96.0}}, - {'name': 'title', 'h1': {'fontSize': 120.0}}, // duplicate + { + 'name': 'title', + 'h1': {'fontSize': 96.0}, + }, + { + 'name': 'title', + 'h1': {'fontSize': 120.0}, + }, // duplicate ], }); expect(result.isFail, isTrue); @@ -1183,7 +1132,9 @@ void main() { test('allows unknown top-level keys for forward compatibility', () { final result = StyleSchemas.styleConfigSchema.safeParse({ 'version': 2, // unknown key - should pass through - 'base': {'h1': {'fontSize': 96.0}}, + 'base': { + 'h1': {'fontSize': 96.0}, + }, }); expect(result.isOk, isTrue); }); @@ -1193,7 +1144,9 @@ void main() { 'version': 2, 'schema': 'v1', 'metadata': {'author': 'Test'}, - 'base': {'h1': {'fontSize': 96.0}}, + 'base': { + 'h1': {'fontSize': 96.0}, + }, }); expect(result.isOk, isTrue); }); @@ -1216,10 +1169,7 @@ void main() { 'color': '#FF0000', 'paddingBottom': 16.0, }, - 'link': { - 'color': '#0000FF', - 'decoration': 'underline', - }, + 'link': {'color': '#0000FF', 'decoration': 'underline'}, }, }); expect(result.isOk, isTrue); @@ -1233,16 +1183,10 @@ void main() { final result = StyleSchemas.styleConfigSchema.safeParse({ 'base': { 'code': { - 'textStyle': { - 'fontFamily': 'JetBrains Mono', - 'fontSize': 18.0, - }, + 'textStyle': {'fontFamily': 'JetBrains Mono', 'fontSize': 18.0}, 'container': { 'padding': 32.0, - 'decoration': { - 'color': '#000000', - 'borderRadius': 10.0, - }, + 'decoration': {'color': '#000000', 'borderRadius': 10.0}, }, }, }, @@ -1400,17 +1344,21 @@ void main() { test('validates color format strictly', () { // Test various invalid color formats final invalidColors = [ - 'FF0000', // missing # - '#FFF', // too short - '#FFFFFFF', // wrong length - '#GGGGGG', // invalid hex - 'red', // named colors not supported + 'FF0000', // missing # + '#FFF', // too short + '#FFFFFFF', // wrong length + '#GGGGGG', // invalid hex + 'red', // named colors not supported 'rgb(255,0,0)', // rgb format not supported ]; for (final color in invalidColors) { final result = StyleSchemas.colorSchema.safeParse(color); - expect(result.isFail, isTrue, reason: 'Expected $color to be invalid'); + expect( + result.isFail, + isTrue, + reason: 'Expected $color to be invalid', + ); } }); @@ -1422,9 +1370,7 @@ void main() { }); test('handles empty style list', () { - final result = StyleSchemas.styleConfigSchema.safeParse({ - 'styles': [], - }); + final result = StyleSchemas.styleConfigSchema.safeParse({'styles': []}); expect(result.isOk, isTrue); final config = result.getOrThrow()!; expect(config.styles, isEmpty); @@ -1463,10 +1409,7 @@ void main() { 'color': '#FFFFFF', 'paddingBottom': 12.0, }, - 'link': { - 'color': '#425260', - 'decoration': 'none', - }, + 'link': {'color': '#425260', 'decoration': 'none'}, 'code': { 'textStyle': { 'fontFamily': 'JetBrains Mono', @@ -1476,57 +1419,26 @@ void main() { }, 'container': { 'padding': 32.0, - 'decoration': { - 'color': '#000000', - 'borderRadius': 10.0, - }, + 'decoration': {'color': '#000000', 'borderRadius': 10.0}, }, }, 'blockquote': { - 'textStyle': { - 'fontSize': 32.0, - 'color': '#CCCCCC', - }, - 'padding': { - 'left': 30.0, - 'bottom': 12.0, - }, - 'decoration': { - 'color': '#888888', - }, + 'textStyle': {'fontSize': 32.0, 'color': '#CCCCCC'}, + 'padding': {'left': 30.0, 'bottom': 12.0}, + 'decoration': {'color': '#888888'}, }, 'list': { - 'bullet': { - 'fontSize': 24.0, - 'color': '#FFFFFF', - }, - 'text': { - 'fontSize': 24.0, - 'height': 1.6, - 'paddingBottom': 8.0, - }, + 'bullet': {'fontSize': 24.0, 'color': '#FFFFFF'}, + 'text': {'fontSize': 24.0, 'height': 1.6, 'paddingBottom': 8.0}, }, 'alert': { 'note': { - 'heading': { - 'fontSize': 24.0, - 'fontWeight': 'bold', - }, - 'description': { - 'fontSize': 24.0, - }, + 'heading': {'fontSize': 24.0, 'fontWeight': 'bold'}, + 'description': {'fontSize': 24.0}, 'container': { - 'padding': { - 'horizontal': 24.0, - 'vertical': 8.0, - }, - 'margin': { - 'vertical': 12.0, - }, - 'decoration': { - 'color': '#0D47A1', - 'borderRadius': 4.0, - }, + 'padding': {'horizontal': 24.0, 'vertical': 8.0}, + 'margin': {'vertical': 12.0}, + 'decoration': {'color': '#0D47A1', 'borderRadius': 4.0}, }, }, }, @@ -1534,16 +1446,12 @@ void main() { 'styles': [ { 'name': 'title-slide', - 'h1': { - 'fontSize': 120.0, - }, + 'h1': {'fontSize': 120.0}, }, { 'name': 'code-heavy', 'code': { - 'textStyle': { - 'fontSize': 16.0, - }, + 'textStyle': {'fontSize': 16.0}, }, }, ], @@ -1623,7 +1531,9 @@ void main() { test('styleConfigSchema transforms to record type', () { final result = StyleSchemas.styleConfigSchema.safeParse({ - 'base': {'h1': {'fontSize': 96.0}}, + 'base': { + 'h1': {'fontSize': 96.0}, + }, }); final config = result.getOrThrow()!; expect(config.baseStyle, isNotNull); @@ -1647,7 +1557,9 @@ void main() { test('top-level schema allows unknown keys (permissive)', () { final result = StyleSchemas.styleConfigSchema.safeParse({ 'unknownKey': 'value', - 'base': {'h1': {'fontSize': 96.0}}, + 'base': { + 'h1': {'fontSize': 96.0}, + }, }); expect(result.isOk, isTrue); }); @@ -1698,10 +1610,7 @@ void main() { }); test('can have null baseStyle', () { - final config = ( - baseStyle: null, - styles: {}, - ); + final config = (baseStyle: null, styles: {}); expect(config.baseStyle, isNull); expect(config.styles, isEmpty); diff --git a/packages/superdeck/test/testing_utils.dart b/packages/superdeck/test/testing_utils.dart index b469986c..5bd9aff0 100644 --- a/packages/superdeck/test/testing_utils.dart +++ b/packages/superdeck/test/testing_utils.dart @@ -45,7 +45,8 @@ SlideConfiguration createTestSlide({ /// Creates a test deck with the given slides Deck createTestDeck({List? slides, DeckConfiguration? config}) { - final testSlides = slides ?? + final testSlides = + slides ?? List.generate( 3, (index) => Slide( diff --git a/packages/superdeck/test/ui/panels_test.dart b/packages/superdeck/test/ui/panels_test.dart index 1bc7af18..3f12a789 100644 --- a/packages/superdeck/test/ui/panels_test.dart +++ b/packages/superdeck/test/ui/panels_test.dart @@ -3,7 +3,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mix/mix.dart'; import 'package:superdeck/src/ui/panels/comments_panel.dart'; - void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -12,9 +11,7 @@ void main() { await tester.pumpWidget( MixScope( child: MaterialApp( - home: Scaffold( - body: CommentsPanel(comments: const []), - ), + home: Scaffold(body: CommentsPanel(comments: const [])), ), ), ); @@ -62,9 +59,7 @@ void main() { await tester.pumpWidget( MixScope( child: MaterialApp( - home: Scaffold( - body: CommentsPanel(comments: [longText]), - ), + home: Scaffold(body: CommentsPanel(comments: [longText])), ), ), ); diff --git a/packages/superdeck/test/utils/converters_test.dart b/packages/superdeck/test/utils/converters_test.dart index 6eaca197..19582d55 100644 --- a/packages/superdeck/test/utils/converters_test.dart +++ b/packages/superdeck/test/utils/converters_test.dart @@ -411,9 +411,7 @@ void main() { test('decoration with border adds border dimensions', () { final spec = BoxSpec( padding: const EdgeInsets.all(10), - decoration: BoxDecoration( - border: Border.all(width: 2), - ), + decoration: BoxDecoration(border: Border.all(width: 2)), ); final offset = ConverterHelper.calculateBlockOffset(spec); expect(offset.dx, 24.0); // 20 padding + 4 border (2 left + 2 right) @@ -431,18 +429,14 @@ void main() { }); test('symmetric horizontal padding', () { - final spec = BoxSpec( - padding: const EdgeInsets.symmetric(horizontal: 15), - ); + final spec = BoxSpec(padding: const EdgeInsets.symmetric(horizontal: 15)); final offset = ConverterHelper.calculateBlockOffset(spec); expect(offset.dx, 30.0); expect(offset.dy, 0.0); }); test('symmetric vertical margin', () { - final spec = BoxSpec( - margin: const EdgeInsets.symmetric(vertical: 8), - ); + final spec = BoxSpec(margin: const EdgeInsets.symmetric(vertical: 8)); final offset = ConverterHelper.calculateBlockOffset(spec); expect(offset.dx, 0.0); expect(offset.dy, 16.0); diff --git a/packages/superdeck/test/utils/uri_validator_test.dart b/packages/superdeck/test/utils/uri_validator_test.dart index e77b5dfb..e53072c9 100644 --- a/packages/superdeck/test/utils/uri_validator_test.dart +++ b/packages/superdeck/test/utils/uri_validator_test.dart @@ -193,7 +193,9 @@ void main() { test('throws on backslash traversal in file URI', () { expect( - () => UriValidator.validate(r'file:///C:\Users\..\..\..\Windows\System32'), + () => UriValidator.validate( + r'file:///C:\Users\..\..\..\Windows\System32', + ), throwsA( isA().having( (e) => e.message, @@ -323,13 +325,17 @@ void main() { // HTTP URLs are exempt from traversal checks because: // 1. The server controls what path resolves to // 2. Browsers already handle this safely - final uri = UriValidator.validate('http://example.com/../../../etc/passwd'); + final uri = UriValidator.validate( + 'http://example.com/../../../etc/passwd', + ); expect(uri, isNotNull); expect(uri!.scheme, 'http'); }); test('allows .. in HTTPS URL path (intentionally permissive)', () { - final uri = UriValidator.validate('https://example.com/../../../etc/passwd'); + final uri = UriValidator.validate( + 'https://example.com/../../../etc/passwd', + ); expect(uri, isNotNull); expect(uri!.scheme, 'https'); });