From e9bfd8f5ac73c9e9052e95bb271654f30a453760 Mon Sep 17 00:00:00 2001 From: Viktor Lidholt Date: Sun, 25 Oct 2026 10:33:06 +0100 Subject: [PATCH 1/3] chore: upgrade Serverpod to 4.1.0-rc.1 - Bump every Serverpod package, the CI `VERSION`, CONTRIBUTING and the board's contribute page from 4.1.0-beta.3 to 4.1.0-rc.1. - Refresh the agent skills with `skills get` (Claude, Cursor and `.agents/skills`); adds shad-overview and the flutter_scene skills. - AGENTS.md: agents now start the stack with `serverpod runner start` (per the 4.1.0-rc.1 template), read `tail_runner_logs` when the server is down, and stop the runner only when asked. - Migration for the auth module's new `serverpod_auth_idp_email_account_otp_login_request` table. Co-Authored-By: Claude Opus 5.5 --- .../flutter-add-widget-preview/SKILL.md | 101 +- .agents/skills/flutter_scene-idioms/SKILL.md | 137 + .../references/architecture.md | 315 + .../references/depth-and-layering.md | 64 + .../flutter_scene-idioms/references/traps.md | 632 + .../references/what-exists.md | 550 + .agents/skills/flutter_scene-kit/SKILL.md | 157 + .agents/skills/flutter_scene-looks/SKILL.md | 166 + .../flutter_scene-looks/references/looks.md | 290 + .../skills/flutter_scene-performance/SKILL.md | 63 + .../references/performance.md | 128 + .../skills/flutter_scene-procedural/SKILL.md | 138 + .../references/procedural.md | 512 + .../flutter_scene-verification-loop/SKILL.md | 71 + .../references/loop.md | 161 + .../skills/serverpod-auth/references/setup.md | 12 + .../skills/serverpod-configuration/SKILL.md | 8 +- .agents/skills/serverpod-endpoints/SKILL.md | 2 +- .agents/skills/serverpod-migrations/SKILL.md | 20 +- .agents/skills/serverpod-models/SKILL.md | 2 +- .agents/skills/serverpod-overview/SKILL.md | 37 +- .agents/skills/serverpod-upgrading/SKILL.md | 4 +- .agents/skills/shad-overview/SKILL.md | 155 + .../shad-overview/components/accordion.md | 91 + .../skills/shad-overview/components/alert.md | 78 + .../skills/shad-overview/components/avatar.md | 33 + .../skills/shad-overview/components/badge.md | 29 + .../shad-overview/components/breadcrumb.md | 64 + .../skills/shad-overview/components/button.md | 145 + .../shad-overview/components/calendar.md | 51 + .../skills/shad-overview/components/card.md | 66 + .../shad-overview/components/checkbox.md | 55 + .../shad-overview/components/collapsible.md | 57 + .../shad-overview/components/command.md | 113 + .../shad-overview/components/context-menu.md | 57 + .../shad-overview/components/data-table.md | 149 + .../shad-overview/components/date-picker.md | 36 + .../skills/shad-overview/components/dialog.md | 114 + .../skills/shad-overview/components/empty.md | 39 + .../shad-overview/components/icon-button.md | 45 + .../shad-overview/components/input-otp.md | 40 + .../skills/shad-overview/components/input.md | 96 + .../skills/shad-overview/components/kbd.md | 35 + .../components/keyboard-toolbar.md | 29 + .../shad-overview/components/menubar.md | 70 + .../shad-overview/components/pagination.md | 126 + .../shad-overview/components/popover.md | 69 + .../shad-overview/components/progress.md | 32 + .../shad-overview/components/radio-group.md | 37 + .../shad-overview/components/resizable.md | 64 + .../skills/shad-overview/components/select.md | 106 + .../shad-overview/components/separator.md | 54 + .../skills/shad-overview/components/sheet.md | 53 + .../shad-overview/components/sidebar.md | 209 + .../shad-overview/components/skeleton.md | 35 + .../skills/shad-overview/components/slider.md | 106 + .../skills/shad-overview/components/sonner.md | 39 + .../shad-overview/components/spinner.md | 28 + .../skills/shad-overview/components/switch.md | 31 + .../skills/shad-overview/components/table.md | 104 + .../skills/shad-overview/components/tabs.md | 81 + .../shad-overview/components/textarea.md | 26 + .../shad-overview/components/time-picker.md | 44 + .../skills/shad-overview/components/toast.md | 61 + .../shad-overview/components/toggle-group.md | 142 + .../skills/shad-overview/components/toggle.md | 88 + .../shad-overview/components/tooltip.md | 268 + .agents/skills/shad-overview/guides/form.md | 139 + .../shad-overview/guides/installation.md | 82 + .agents/skills/shad-overview/guides/layout.md | 130 + .agents/skills/shad-overview/guides/portal.md | 99 + .../skills/shad-overview/guides/responsive.md | 50 + .agents/skills/shad-overview/guides/styles.md | 56 + .../skills/shad-overview/guides/theming.md | 149 + .../skills/shad-overview/guides/typography.md | 98 + .../flutter-add-widget-preview/SKILL.md | 101 +- .claude/skills/flutter_scene-idioms/SKILL.md | 138 + .../references/architecture.md | 315 + .../references/depth-and-layering.md | 64 + .../flutter_scene-idioms/references/traps.md | 632 + .../references/what-exists.md | 550 + .claude/skills/flutter_scene-kit/SKILL.md | 158 + .claude/skills/flutter_scene-looks/SKILL.md | 167 + .../flutter_scene-looks/references/looks.md | 290 + .../skills/flutter_scene-performance/SKILL.md | 64 + .../references/performance.md | 128 + .../skills/flutter_scene-procedural/SKILL.md | 139 + .../references/procedural.md | 512 + .../flutter_scene-verification-loop/SKILL.md | 72 + .../references/loop.md | 161 + .../skills/serverpod-auth/references/setup.md | 12 + .../skills/serverpod-configuration/SKILL.md | 8 +- .claude/skills/serverpod-endpoints/SKILL.md | 2 +- .claude/skills/serverpod-migrations/SKILL.md | 20 +- .claude/skills/serverpod-models/SKILL.md | 2 +- .claude/skills/serverpod-overview/SKILL.md | 37 +- .claude/skills/serverpod-upgrading/SKILL.md | 4 +- .claude/skills/shad-overview/SKILL.md | 156 + .../shad-overview/components/accordion.md | 91 + .../skills/shad-overview/components/alert.md | 78 + .../skills/shad-overview/components/avatar.md | 33 + .../skills/shad-overview/components/badge.md | 29 + .../shad-overview/components/breadcrumb.md | 64 + .../skills/shad-overview/components/button.md | 145 + .../shad-overview/components/calendar.md | 51 + .../skills/shad-overview/components/card.md | 66 + .../shad-overview/components/checkbox.md | 55 + .../shad-overview/components/collapsible.md | 57 + .../shad-overview/components/command.md | 113 + .../shad-overview/components/context-menu.md | 57 + .../shad-overview/components/data-table.md | 149 + .../shad-overview/components/date-picker.md | 36 + .../skills/shad-overview/components/dialog.md | 114 + .../skills/shad-overview/components/empty.md | 39 + .../shad-overview/components/icon-button.md | 45 + .../shad-overview/components/input-otp.md | 40 + .../skills/shad-overview/components/input.md | 96 + .../skills/shad-overview/components/kbd.md | 35 + .../components/keyboard-toolbar.md | 29 + .../shad-overview/components/menubar.md | 70 + .../shad-overview/components/pagination.md | 126 + .../shad-overview/components/popover.md | 69 + .../shad-overview/components/progress.md | 32 + .../shad-overview/components/radio-group.md | 37 + .../shad-overview/components/resizable.md | 64 + .../skills/shad-overview/components/select.md | 106 + .../shad-overview/components/separator.md | 54 + .../skills/shad-overview/components/sheet.md | 53 + .../shad-overview/components/sidebar.md | 209 + .../shad-overview/components/skeleton.md | 35 + .../skills/shad-overview/components/slider.md | 106 + .../skills/shad-overview/components/sonner.md | 39 + .../shad-overview/components/spinner.md | 28 + .../skills/shad-overview/components/switch.md | 31 + .../skills/shad-overview/components/table.md | 104 + .../skills/shad-overview/components/tabs.md | 81 + .../shad-overview/components/textarea.md | 26 + .../shad-overview/components/time-picker.md | 44 + .../skills/shad-overview/components/toast.md | 61 + .../shad-overview/components/toggle-group.md | 142 + .../skills/shad-overview/components/toggle.md | 88 + .../shad-overview/components/tooltip.md | 268 + .claude/skills/shad-overview/guides/form.md | 139 + .../shad-overview/guides/installation.md | 82 + .claude/skills/shad-overview/guides/layout.md | 130 + .claude/skills/shad-overview/guides/portal.md | 99 + .../skills/shad-overview/guides/responsive.md | 50 + .claude/skills/shad-overview/guides/styles.md | 56 + .../skills/shad-overview/guides/theming.md | 149 + .../skills/shad-overview/guides/typography.md | 98 + .../flutter-add-widget-preview/SKILL.md | 101 +- .cursor/skills/flutter_scene-idioms/SKILL.md | 137 + .../references/architecture.md | 315 + .../references/depth-and-layering.md | 64 + .../flutter_scene-idioms/references/traps.md | 632 + .../references/what-exists.md | 550 + .cursor/skills/flutter_scene-kit/SKILL.md | 157 + .cursor/skills/flutter_scene-looks/SKILL.md | 166 + .../flutter_scene-looks/references/looks.md | 290 + .../skills/flutter_scene-performance/SKILL.md | 63 + .../references/performance.md | 128 + .../skills/flutter_scene-procedural/SKILL.md | 138 + .../references/procedural.md | 512 + .../flutter_scene-verification-loop/SKILL.md | 71 + .../references/loop.md | 161 + .../skills/serverpod-auth/references/setup.md | 12 + .../skills/serverpod-configuration/SKILL.md | 8 +- .cursor/skills/serverpod-endpoints/SKILL.md | 2 +- .cursor/skills/serverpod-migrations/SKILL.md | 20 +- .cursor/skills/serverpod-models/SKILL.md | 2 +- .cursor/skills/serverpod-overview/SKILL.md | 37 +- .cursor/skills/serverpod-upgrading/SKILL.md | 4 +- .cursor/skills/shad-overview/SKILL.md | 155 + .../shad-overview/components/accordion.md | 91 + .../skills/shad-overview/components/alert.md | 78 + .../skills/shad-overview/components/avatar.md | 33 + .../skills/shad-overview/components/badge.md | 29 + .../shad-overview/components/breadcrumb.md | 64 + .../skills/shad-overview/components/button.md | 145 + .../shad-overview/components/calendar.md | 51 + .../skills/shad-overview/components/card.md | 66 + .../shad-overview/components/checkbox.md | 55 + .../shad-overview/components/collapsible.md | 57 + .../shad-overview/components/command.md | 113 + .../shad-overview/components/context-menu.md | 57 + .../shad-overview/components/data-table.md | 149 + .../shad-overview/components/date-picker.md | 36 + .../skills/shad-overview/components/dialog.md | 114 + .../skills/shad-overview/components/empty.md | 39 + .../shad-overview/components/icon-button.md | 45 + .../shad-overview/components/input-otp.md | 40 + .../skills/shad-overview/components/input.md | 96 + .../skills/shad-overview/components/kbd.md | 35 + .../components/keyboard-toolbar.md | 29 + .../shad-overview/components/menubar.md | 70 + .../shad-overview/components/pagination.md | 126 + .../shad-overview/components/popover.md | 69 + .../shad-overview/components/progress.md | 32 + .../shad-overview/components/radio-group.md | 37 + .../shad-overview/components/resizable.md | 64 + .../skills/shad-overview/components/select.md | 106 + .../shad-overview/components/separator.md | 54 + .../skills/shad-overview/components/sheet.md | 53 + .../shad-overview/components/sidebar.md | 209 + .../shad-overview/components/skeleton.md | 35 + .../skills/shad-overview/components/slider.md | 106 + .../skills/shad-overview/components/sonner.md | 39 + .../shad-overview/components/spinner.md | 28 + .../skills/shad-overview/components/switch.md | 31 + .../skills/shad-overview/components/table.md | 104 + .../skills/shad-overview/components/tabs.md | 81 + .../shad-overview/components/textarea.md | 26 + .../shad-overview/components/time-picker.md | 44 + .../skills/shad-overview/components/toast.md | 61 + .../shad-overview/components/toggle-group.md | 142 + .../skills/shad-overview/components/toggle.md | 88 + .../shad-overview/components/tooltip.md | 268 + .cursor/skills/shad-overview/guides/form.md | 139 + .../shad-overview/guides/installation.md | 82 + .cursor/skills/shad-overview/guides/layout.md | 130 + .cursor/skills/shad-overview/guides/portal.md | 99 + .../skills/shad-overview/guides/responsive.md | 50 + .cursor/skills/shad-overview/guides/styles.md | 56 + .../skills/shad-overview/guides/theming.md | 149 + .../skills/shad-overview/guides/typography.md | 98 + .github/workflows/analyze.yml | 2 +- .github/workflows/deploy.yml | 2 +- .github/workflows/e2e.yml | 2 +- .github/workflows/nightly.yml | 2 +- .github/workflows/tests.yml | 2 +- AGENTS.md | 13 +- CONTRIBUTING.md | 4 +- .../lib/src/board/pages/contribute_page.dart | 2 +- packages/wonky_app/pubspec.yaml | 6 +- .../test/board/pages/task_page_test.dart | 7 +- pubspec.lock | 72 +- wonky_client/pubspec.yaml | 6 +- .../definition.json | 20250 ++++++++++++++++ .../definition.sql | 4384 ++++ .../definition_project.json | 16945 +++++++++++++ .../migration.json | 109 + .../migration.sql | 96 + .../migrations/migration_registry.txt | 1 + wonky_server/pubspec.yaml | 8 +- wonky_write/pubspec.yaml | 2 +- 245 files changed, 65175 insertions(+), 216 deletions(-) create mode 100644 .agents/skills/flutter_scene-idioms/SKILL.md create mode 100644 .agents/skills/flutter_scene-idioms/references/architecture.md create mode 100644 .agents/skills/flutter_scene-idioms/references/depth-and-layering.md create mode 100644 .agents/skills/flutter_scene-idioms/references/traps.md create mode 100644 .agents/skills/flutter_scene-idioms/references/what-exists.md create mode 100644 .agents/skills/flutter_scene-kit/SKILL.md create mode 100644 .agents/skills/flutter_scene-looks/SKILL.md create mode 100644 .agents/skills/flutter_scene-looks/references/looks.md create mode 100644 .agents/skills/flutter_scene-performance/SKILL.md create mode 100644 .agents/skills/flutter_scene-performance/references/performance.md create mode 100644 .agents/skills/flutter_scene-procedural/SKILL.md create mode 100644 .agents/skills/flutter_scene-procedural/references/procedural.md create mode 100644 .agents/skills/flutter_scene-verification-loop/SKILL.md create mode 100644 .agents/skills/flutter_scene-verification-loop/references/loop.md create mode 100644 .agents/skills/shad-overview/SKILL.md create mode 100644 .agents/skills/shad-overview/components/accordion.md create mode 100644 .agents/skills/shad-overview/components/alert.md create mode 100644 .agents/skills/shad-overview/components/avatar.md create mode 100644 .agents/skills/shad-overview/components/badge.md create mode 100644 .agents/skills/shad-overview/components/breadcrumb.md create mode 100644 .agents/skills/shad-overview/components/button.md create mode 100644 .agents/skills/shad-overview/components/calendar.md create mode 100644 .agents/skills/shad-overview/components/card.md create mode 100644 .agents/skills/shad-overview/components/checkbox.md create mode 100644 .agents/skills/shad-overview/components/collapsible.md create mode 100644 .agents/skills/shad-overview/components/command.md create mode 100644 .agents/skills/shad-overview/components/context-menu.md create mode 100644 .agents/skills/shad-overview/components/data-table.md create mode 100644 .agents/skills/shad-overview/components/date-picker.md create mode 100644 .agents/skills/shad-overview/components/dialog.md create mode 100644 .agents/skills/shad-overview/components/empty.md create mode 100644 .agents/skills/shad-overview/components/icon-button.md create mode 100644 .agents/skills/shad-overview/components/input-otp.md create mode 100644 .agents/skills/shad-overview/components/input.md create mode 100644 .agents/skills/shad-overview/components/kbd.md create mode 100644 .agents/skills/shad-overview/components/keyboard-toolbar.md create mode 100644 .agents/skills/shad-overview/components/menubar.md create mode 100644 .agents/skills/shad-overview/components/pagination.md create mode 100644 .agents/skills/shad-overview/components/popover.md create mode 100644 .agents/skills/shad-overview/components/progress.md create mode 100644 .agents/skills/shad-overview/components/radio-group.md create mode 100644 .agents/skills/shad-overview/components/resizable.md create mode 100644 .agents/skills/shad-overview/components/select.md create mode 100644 .agents/skills/shad-overview/components/separator.md create mode 100644 .agents/skills/shad-overview/components/sheet.md create mode 100644 .agents/skills/shad-overview/components/sidebar.md create mode 100644 .agents/skills/shad-overview/components/skeleton.md create mode 100644 .agents/skills/shad-overview/components/slider.md create mode 100644 .agents/skills/shad-overview/components/sonner.md create mode 100644 .agents/skills/shad-overview/components/spinner.md create mode 100644 .agents/skills/shad-overview/components/switch.md create mode 100644 .agents/skills/shad-overview/components/table.md create mode 100644 .agents/skills/shad-overview/components/tabs.md create mode 100644 .agents/skills/shad-overview/components/textarea.md create mode 100644 .agents/skills/shad-overview/components/time-picker.md create mode 100644 .agents/skills/shad-overview/components/toast.md create mode 100644 .agents/skills/shad-overview/components/toggle-group.md create mode 100644 .agents/skills/shad-overview/components/toggle.md create mode 100644 .agents/skills/shad-overview/components/tooltip.md create mode 100644 .agents/skills/shad-overview/guides/form.md create mode 100644 .agents/skills/shad-overview/guides/installation.md create mode 100644 .agents/skills/shad-overview/guides/layout.md create mode 100644 .agents/skills/shad-overview/guides/portal.md create mode 100644 .agents/skills/shad-overview/guides/responsive.md create mode 100644 .agents/skills/shad-overview/guides/styles.md create mode 100644 .agents/skills/shad-overview/guides/theming.md create mode 100644 .agents/skills/shad-overview/guides/typography.md create mode 100644 .claude/skills/flutter_scene-idioms/SKILL.md create mode 100644 .claude/skills/flutter_scene-idioms/references/architecture.md create mode 100644 .claude/skills/flutter_scene-idioms/references/depth-and-layering.md create mode 100644 .claude/skills/flutter_scene-idioms/references/traps.md create mode 100644 .claude/skills/flutter_scene-idioms/references/what-exists.md create mode 100644 .claude/skills/flutter_scene-kit/SKILL.md create mode 100644 .claude/skills/flutter_scene-looks/SKILL.md create mode 100644 .claude/skills/flutter_scene-looks/references/looks.md create mode 100644 .claude/skills/flutter_scene-performance/SKILL.md create mode 100644 .claude/skills/flutter_scene-performance/references/performance.md create mode 100644 .claude/skills/flutter_scene-procedural/SKILL.md create mode 100644 .claude/skills/flutter_scene-procedural/references/procedural.md create mode 100644 .claude/skills/flutter_scene-verification-loop/SKILL.md create mode 100644 .claude/skills/flutter_scene-verification-loop/references/loop.md create mode 100644 .claude/skills/shad-overview/SKILL.md create mode 100644 .claude/skills/shad-overview/components/accordion.md create mode 100644 .claude/skills/shad-overview/components/alert.md create mode 100644 .claude/skills/shad-overview/components/avatar.md create mode 100644 .claude/skills/shad-overview/components/badge.md create mode 100644 .claude/skills/shad-overview/components/breadcrumb.md create mode 100644 .claude/skills/shad-overview/components/button.md create mode 100644 .claude/skills/shad-overview/components/calendar.md create mode 100644 .claude/skills/shad-overview/components/card.md create mode 100644 .claude/skills/shad-overview/components/checkbox.md create mode 100644 .claude/skills/shad-overview/components/collapsible.md create mode 100644 .claude/skills/shad-overview/components/command.md create mode 100644 .claude/skills/shad-overview/components/context-menu.md create mode 100644 .claude/skills/shad-overview/components/data-table.md create mode 100644 .claude/skills/shad-overview/components/date-picker.md create mode 100644 .claude/skills/shad-overview/components/dialog.md create mode 100644 .claude/skills/shad-overview/components/empty.md create mode 100644 .claude/skills/shad-overview/components/icon-button.md create mode 100644 .claude/skills/shad-overview/components/input-otp.md create mode 100644 .claude/skills/shad-overview/components/input.md create mode 100644 .claude/skills/shad-overview/components/kbd.md create mode 100644 .claude/skills/shad-overview/components/keyboard-toolbar.md create mode 100644 .claude/skills/shad-overview/components/menubar.md create mode 100644 .claude/skills/shad-overview/components/pagination.md create mode 100644 .claude/skills/shad-overview/components/popover.md create mode 100644 .claude/skills/shad-overview/components/progress.md create mode 100644 .claude/skills/shad-overview/components/radio-group.md create mode 100644 .claude/skills/shad-overview/components/resizable.md create mode 100644 .claude/skills/shad-overview/components/select.md create mode 100644 .claude/skills/shad-overview/components/separator.md create mode 100644 .claude/skills/shad-overview/components/sheet.md create mode 100644 .claude/skills/shad-overview/components/sidebar.md create mode 100644 .claude/skills/shad-overview/components/skeleton.md create mode 100644 .claude/skills/shad-overview/components/slider.md create mode 100644 .claude/skills/shad-overview/components/sonner.md create mode 100644 .claude/skills/shad-overview/components/spinner.md create mode 100644 .claude/skills/shad-overview/components/switch.md create mode 100644 .claude/skills/shad-overview/components/table.md create mode 100644 .claude/skills/shad-overview/components/tabs.md create mode 100644 .claude/skills/shad-overview/components/textarea.md create mode 100644 .claude/skills/shad-overview/components/time-picker.md create mode 100644 .claude/skills/shad-overview/components/toast.md create mode 100644 .claude/skills/shad-overview/components/toggle-group.md create mode 100644 .claude/skills/shad-overview/components/toggle.md create mode 100644 .claude/skills/shad-overview/components/tooltip.md create mode 100644 .claude/skills/shad-overview/guides/form.md create mode 100644 .claude/skills/shad-overview/guides/installation.md create mode 100644 .claude/skills/shad-overview/guides/layout.md create mode 100644 .claude/skills/shad-overview/guides/portal.md create mode 100644 .claude/skills/shad-overview/guides/responsive.md create mode 100644 .claude/skills/shad-overview/guides/styles.md create mode 100644 .claude/skills/shad-overview/guides/theming.md create mode 100644 .claude/skills/shad-overview/guides/typography.md create mode 100644 .cursor/skills/flutter_scene-idioms/SKILL.md create mode 100644 .cursor/skills/flutter_scene-idioms/references/architecture.md create mode 100644 .cursor/skills/flutter_scene-idioms/references/depth-and-layering.md create mode 100644 .cursor/skills/flutter_scene-idioms/references/traps.md create mode 100644 .cursor/skills/flutter_scene-idioms/references/what-exists.md create mode 100644 .cursor/skills/flutter_scene-kit/SKILL.md create mode 100644 .cursor/skills/flutter_scene-looks/SKILL.md create mode 100644 .cursor/skills/flutter_scene-looks/references/looks.md create mode 100644 .cursor/skills/flutter_scene-performance/SKILL.md create mode 100644 .cursor/skills/flutter_scene-performance/references/performance.md create mode 100644 .cursor/skills/flutter_scene-procedural/SKILL.md create mode 100644 .cursor/skills/flutter_scene-procedural/references/procedural.md create mode 100644 .cursor/skills/flutter_scene-verification-loop/SKILL.md create mode 100644 .cursor/skills/flutter_scene-verification-loop/references/loop.md create mode 100644 .cursor/skills/shad-overview/SKILL.md create mode 100644 .cursor/skills/shad-overview/components/accordion.md create mode 100644 .cursor/skills/shad-overview/components/alert.md create mode 100644 .cursor/skills/shad-overview/components/avatar.md create mode 100644 .cursor/skills/shad-overview/components/badge.md create mode 100644 .cursor/skills/shad-overview/components/breadcrumb.md create mode 100644 .cursor/skills/shad-overview/components/button.md create mode 100644 .cursor/skills/shad-overview/components/calendar.md create mode 100644 .cursor/skills/shad-overview/components/card.md create mode 100644 .cursor/skills/shad-overview/components/checkbox.md create mode 100644 .cursor/skills/shad-overview/components/collapsible.md create mode 100644 .cursor/skills/shad-overview/components/command.md create mode 100644 .cursor/skills/shad-overview/components/context-menu.md create mode 100644 .cursor/skills/shad-overview/components/data-table.md create mode 100644 .cursor/skills/shad-overview/components/date-picker.md create mode 100644 .cursor/skills/shad-overview/components/dialog.md create mode 100644 .cursor/skills/shad-overview/components/empty.md create mode 100644 .cursor/skills/shad-overview/components/icon-button.md create mode 100644 .cursor/skills/shad-overview/components/input-otp.md create mode 100644 .cursor/skills/shad-overview/components/input.md create mode 100644 .cursor/skills/shad-overview/components/kbd.md create mode 100644 .cursor/skills/shad-overview/components/keyboard-toolbar.md create mode 100644 .cursor/skills/shad-overview/components/menubar.md create mode 100644 .cursor/skills/shad-overview/components/pagination.md create mode 100644 .cursor/skills/shad-overview/components/popover.md create mode 100644 .cursor/skills/shad-overview/components/progress.md create mode 100644 .cursor/skills/shad-overview/components/radio-group.md create mode 100644 .cursor/skills/shad-overview/components/resizable.md create mode 100644 .cursor/skills/shad-overview/components/select.md create mode 100644 .cursor/skills/shad-overview/components/separator.md create mode 100644 .cursor/skills/shad-overview/components/sheet.md create mode 100644 .cursor/skills/shad-overview/components/sidebar.md create mode 100644 .cursor/skills/shad-overview/components/skeleton.md create mode 100644 .cursor/skills/shad-overview/components/slider.md create mode 100644 .cursor/skills/shad-overview/components/sonner.md create mode 100644 .cursor/skills/shad-overview/components/spinner.md create mode 100644 .cursor/skills/shad-overview/components/switch.md create mode 100644 .cursor/skills/shad-overview/components/table.md create mode 100644 .cursor/skills/shad-overview/components/tabs.md create mode 100644 .cursor/skills/shad-overview/components/textarea.md create mode 100644 .cursor/skills/shad-overview/components/time-picker.md create mode 100644 .cursor/skills/shad-overview/components/toast.md create mode 100644 .cursor/skills/shad-overview/components/toggle-group.md create mode 100644 .cursor/skills/shad-overview/components/toggle.md create mode 100644 .cursor/skills/shad-overview/components/tooltip.md create mode 100644 .cursor/skills/shad-overview/guides/form.md create mode 100644 .cursor/skills/shad-overview/guides/installation.md create mode 100644 .cursor/skills/shad-overview/guides/layout.md create mode 100644 .cursor/skills/shad-overview/guides/portal.md create mode 100644 .cursor/skills/shad-overview/guides/responsive.md create mode 100644 .cursor/skills/shad-overview/guides/styles.md create mode 100644 .cursor/skills/shad-overview/guides/theming.md create mode 100644 .cursor/skills/shad-overview/guides/typography.md create mode 100644 wonky_server/migrations/20261025085716775-serverpod-4-1-rc1/definition.json create mode 100644 wonky_server/migrations/20261025085716775-serverpod-4-1-rc1/definition.sql create mode 100644 wonky_server/migrations/20261025085716775-serverpod-4-1-rc1/definition_project.json create mode 100644 wonky_server/migrations/20261025085716775-serverpod-4-1-rc1/migration.json create mode 100644 wonky_server/migrations/20261025085716775-serverpod-4-1-rc1/migration.sql diff --git a/.agents/skills/flutter-add-widget-preview/SKILL.md b/.agents/skills/flutter-add-widget-preview/SKILL.md index 6ba68942..89db1321 100644 --- a/.agents/skills/flutter-add-widget-preview/SKILL.md +++ b/.agents/skills/flutter-add-widget-preview/SKILL.md @@ -9,28 +9,43 @@ metadata: ## Contents - [Preview Guidelines](#preview-guidelines) +- [Preview Parameters](#preview-parameters) - [Handling Limitations](#handling-limitations) - [Workflows](#workflows) - [Examples](#examples) ## Preview Guidelines -Use the Flutter Widget Previewer to render widgets in real-time, isolated from the full application context. +Use the Flutter Widget Previewer to render widgets in real-time, isolated from the full application context. - **Target Elements:** Apply the `@Preview` annotation to top-level functions, static methods within a class, or public widget constructors/factories that have no required arguments and return a `Widget` or `WidgetBuilder`. - **Imports:** Always import `package:flutter/widget_previews.dart` to access the preview annotations. -- **Custom Annotations:** Extend the `Preview` class to create custom annotations that inject common properties (e.g., themes, wrappers) across multiple widgets. +- **Custom Annotations:** Extend the `Preview` class to create custom annotations that inject common properties (e.g., custom themes, wrappers, sizes) across multiple widgets. - **Multiple Configurations:** Apply multiple `@Preview` annotations to a single target to generate multiple preview instances. Alternatively, extend `MultiPreview` to encapsulate common multi-preview configurations. -- **Runtime Transformations:** Override the `transform()` method in custom `Preview` or `MultiPreview` classes to modify preview configurations dynamically at runtime (e.g., generating names based on dynamic values, which is impossible in a `const` context). +- **Runtime Transformations:** Override the `transform()` method in custom `Preview` or `MultiPreview` classes to modify preview configurations dynamically at runtime (e.g., building names dynamically based on parameters, which is impossible in a `const` constructor). + +## Preview Parameters + +The `@Preview` annotation supports the following parameters for tailoring preview environments: + +- `name`: A descriptive name for the preview instance. +- `group`: A name used to group related previews together in the previewer. +- `size`: Artificial constraints using a `Size` object. +- `textScaleFactor`: Custom font scale factor. +- `wrapper`: A function that wraps the previewed widget in a specific widget tree (e.g., to inject state or inherited widgets). +- `theme`: A function returning a `PreviewThemeData` subclass instance to provide custom theming with support for sequential theme layering. +- `brightness`: The initial theme `Brightness` (`Brightness.light` or `Brightness.dark`). +- `localizations`: A function applying a localization configuration. ## Handling Limitations Adhere to the following constraints when authoring previewable widgets, as the Widget Previewer runs in a web environment: -- **No Native APIs:** Do not use native plugins or APIs from `dart:io` or `dart:ffi`. Widgets with transitive dependencies on `dart:io` or `dart:ffi` will throw exceptions upon invocation. Use conditional imports to mock or bypass these in preview mode. +- **No Native APIs:** Do not use native plugins or APIs from `dart:io` or `dart:ffi`. Widgets with transitive dependencies on `dart:io` or `dart:ffi` will throw exceptions upon invocation. Use conditional imports or wrappers to mock or bypass these in preview mode. - **Asset Paths:** Use package-based paths for assets loaded via `dart:ui` `fromAsset` APIs (e.g., `packages/my_package_name/assets/my_image.png` instead of `assets/my_image.png`). - **Public Callbacks:** Ensure all callback arguments provided to preview annotations are public and constant to satisfy code generation requirements. -- **Constraints:** Apply explicit constraints using the `size` parameter in the `@Preview` annotation if your widget is unconstrained, as the previewer defaults to constraining them to approximately half the viewport. +- **Constraints:** Apply explicit constraints using the `size` parameter in the `@Preview` annotation if your widget is unconstrained, as unconstrained widgets default to roughly half the viewport. +- **Build Caching:** The previewer caches builds in a `.widget_preview/` folder in the project root. ## Workflows @@ -40,13 +55,13 @@ Copy and track this checklist when implementing a new widget preview: - [ ] Import `package:flutter/widget_previews.dart`. - [ ] Identify a valid target (top-level function, static method, or parameter-less public constructor). - [ ] Apply the `@Preview` annotation to the target. -- [ ] Configure preview parameters (`name`, `group`, `size`, `theme`, `brightness`, etc.) as needed. -- [ ] If applying the same configuration to multiple widgets, extract the configuration into a custom class extending `Preview`. +- [ ] Configure preview parameters (`name`, `group`, `size`, `textScaleFactor`, `wrapper`, `theme`, `brightness`, `localizations`) as needed. +- [ ] If applying the same configuration to multiple widgets, extract the configuration into a custom class extending `Preview` or `MultiPreview`. ### Interacting with Previews Follow the appropriate conditional workflow to launch and interact with the Widget Previewer: -**If using a supported IDE (Android Studio, IntelliJ, VS Code with Flutter 3.38+):** +**If using a supported IDE (Android Studio, IntelliJ, VS Code with Flutter 3.47+):** 1. Launch the IDE. The Widget Previewer starts automatically. 2. Open the "Flutter Widget Preview" tab in the sidebar. 3. Toggle "Filter previews by selected file" at the bottom left if you want to view previews outside the currently active file. @@ -70,45 +85,72 @@ Follow the appropriate conditional workflow to launch and interact with the Widg import 'package:flutter/widget_previews.dart'; import 'package:flutter/material.dart'; -@Preview(name: 'My Sample Text', group: 'Typography') -Widget mySampleText() { - return const Text('Hello, World!'); -} +@Preview(name: 'Submit Button', group: 'Form Controls') +Widget submitButtonPreview() => const ElevatedButton( + onPressed: null, + child: Text('Submit'), + ); ``` -### Custom Preview with Runtime Transformation +### Custom Preview with PreviewThemeData and Transformation ```dart import 'package:flutter/widget_previews.dart'; import 'package:flutter/material.dart'; -final class TransformativePreview extends Preview { - const TransformativePreview({ +final class ThemedPreview extends Preview { + const ThemedPreview({ super.name, super.group, + super.brightness, }); - PreviewThemeData _themeBuilder() { - return PreviewThemeData( - materialLight: ThemeData.light(), - materialDark: ThemeData.dark(), - ); - } + PreviewThemeData _themeBuilder() => + CustomThemeData(brightness: brightness); @override Preview transform() { final originalPreview = super.transform(); + final themeVariant = switch (brightness) { + null => 'Responsive', + final b => b.name, + }; + final builder = originalPreview.toBuilder(); - + final baseName = originalPreview.name ?? 'Preview'; builder - ..name = 'Transformed - ${originalPreview.name}' + ..name = '$baseName [$themeVariant]' ..theme = _themeBuilder; - return builder.toPreview(); + return builder.build(); } } -@TransformativePreview(name: 'Custom Themed Button') -Widget myButton() => const ElevatedButton(onPressed: null, child: Text('Click')); +final class CustomThemeData extends PreviewThemeData { + const CustomThemeData({this.brightness}); + + final Brightness? brightness; + + @override + Widget apply(BuildContext context, Widget child) { + return Theme( + data: ThemeData.from( + colorScheme: ColorScheme.fromSeed( + seedColor: Colors.teal, + brightness: brightness ?? + MediaQuery.maybePlatformBrightnessOf(context) ?? + Brightness.light, + ), + ), + child: child, + ); + } +} + +@ThemedPreview(name: 'Themed Action Button', brightness: Brightness.dark) +Widget themedButtonPreview() => const ElevatedButton( + onPressed: null, + child: Text('Action'), + ); ``` ### MultiPreview Implementation @@ -141,5 +183,10 @@ final class MultiBrightnessPreview extends MultiPreview { } @MultiBrightnessPreview(name: 'Primary Card') -Widget cardPreview() => const Card(child: Padding(padding: EdgeInsets.all(8.0), child: Text('Content'))); +Widget cardPreview() => const Card( + child: Padding( + padding: EdgeInsets.all(8.0), + child: Text('Content'), + ), + ); ``` diff --git a/.agents/skills/flutter_scene-idioms/SKILL.md b/.agents/skills/flutter_scene-idioms/SKILL.md new file mode 100644 index 00000000..491b5cb9 --- /dev/null +++ b/.agents/skills/flutter_scene-idioms/SKILL.md @@ -0,0 +1,137 @@ +--- +name: flutter_scene-idioms +version: 16 +description: Write correct flutter_scene code. Use this whenever building 3D with the flutter_scene Dart/Flutter engine (rendering a scene, geometry, materials, lighting, loading a .glb model, animation, custom shaders). It corrects the wrong assumptions models carry from three.js, Godot, and Unity, and names the APIs and traps that are specific to this engine. +--- + +# Building with flutter_scene + +flutter_scene is a realtime 3D engine for Flutter, built on Flutter GPU. It has a retained scene graph (`Scene` holds `Node`s, nodes carry a `Mesh`), physically based materials, image-based lighting, and a deep post-processing stack. + +**The one thing to internalize: this is not three.js, Godot, or Unity, and the API diverges from all three in specific ways.** Most first-attempt failures come from reaching for another engine's spelling. The corrections below are the highest-value part of this skill; read them before writing code. + +## Do not reach for these (they do not exist or will break the build) + +- **Not the master channel.** flutter_scene runs on **Flutter 3.47 stable or newer**. Do not run `flutter channel master`; it resolves worse, not better. +- **Not `--enable-impeller`, not `--enable-experiment=native-assets`.** The run flag is **`--enable-flutter-gpu`** and nothing else. `--enable-experiment=native-assets` actively breaks the build on Dart 3.10+. +- **Not `package:vector_math/vector_math_64.dart`.** flutter_scene uses **`package:vector_math/vector_math.dart`**. The `_64` types are a different, incompatible `Vector3`. +- **Not `Node.fromAsset(...)`, not `loadModel(...)`.** Load a preprocessed model with **`loadScene('assets/x.glb')`** (returns `Future`), or a runtime glTF with `Node.fromGlbAsset` / `Node.fromGlbBytes`. +- **Not a hand-rolled `CustomPainter` + `Ticker`.** Display a scene with the **`SceneView`** widget; it drives the per-frame loop for you. +- **Not `node.position.set(x, y, z)`.** See transforms below. +- **Not `forward.cross(up)` for a camera or node right vector.** View space is left-handed (`+Z` forward into the screen, `+Y` up, `+X` right), while `vector_math`'s `cross` is right-handed, so `forward.cross(up)` points **screen-left** and horizontally inverts `A`/`D` strafing and drag panning. Read `camera.right`, `camera.horizontalRight`, or `node.right`, or compute `up.cross(forward).normalized()`. +- **Not `.model` files or `buildModels`.** The offline format is `.fsceneb`, produced by the `flutter_scene:init` build hook; you load it by source path with `loadScene`. +- **Not the removed `Environment` class.** Environment lighting is `EnvironmentMap` on `Scene.environment`. + +## Setup + +```sh +flutter pub add flutter_scene +dart run flutter_scene:init # installs the build hook, sets up assets +flutter run --enable-flutter-gpu # native; add -d chrome for web +``` + +`flutter_scene:init` is required setup, not optional. Rendering is gated on `Scene.initializeStaticResources()`; until it completes the engine prints "Flutter Scene is not ready to render. Skipping frame." + +## Minimal scene (this compiles as-is) + +```dart +import 'package:flutter/material.dart'; +import 'package:flutter_scene/scene.dart'; +import 'package:vector_math/vector_math.dart' as vm; + +void main() => runApp(const MaterialApp(home: CubeView())); + +class CubeView extends StatefulWidget { + const CubeView({super.key}); + @override + State createState() => _CubeViewState(); +} + +class _CubeViewState extends State { + final Scene scene = Scene(); + bool ready = false; + + @override + void initState() { + super.initState(); + // Geometry and materials touch the shader bundle, so build them only + // after the engine's static resources are up. + Scene.initializeStaticResources().then((_) { + scene.add(Node( + mesh: Mesh(CuboidGeometry(vm.Vector3(1, 1, 1)), PhysicallyBasedMaterial()), + )); + if (mounted) setState(() => ready = true); + }); + } + + @override + Widget build(BuildContext context) { + if (!ready) return const SizedBox.expand(); + return SceneView(scene, camera: PerspectiveCamera(position: vm.Vector3(2, 2, -4))); + } +} +``` + +An unset `Scene.environment` still gives image-based lighting (a default studio map is resolved at render), so a bare `PhysicallyBasedMaterial()` is lit without any light setup. + +## Choosing declarative or imperative + +flutter_scene has two ways to build a scene, and picking the wrong one is a structural decision that is expensive to undo later. Choose up front by what the app does, not by which reads nicer. + +**Declarative (inline widgets).** Describe the scene as Flutter widgets under `SceneView.declarative(children: [...])`, using `SceneMesh`, `SceneNode`, and `SceneModel`. Flutter's own rebuild diffing keeps the rendered scene in sync with your widget state, the same way it keeps the UI in sync. Reach for this when app state maps cleanly onto a fixed set of objects on screen and nothing is simulation-like, for example a product configurator where a few `SceneMesh`es track some `setState` values. + +**Imperative (retained scene graph).** Own a `Scene`, add `Node`s, attach `Component`s, and display it with `SceneView(scene, camera: ..., onTick: ...)`. Track state the way you would in another game engine. The cleanest shape is a plain Dart `Game` class that owns the `Scene` and holds the game state, with the scene build and per-frame tick routed into it, and behavior living in custom `Component`s attached to nodes that the engine runs through the component lifecycle hooks. Reach for this for anything with real simulation. + +**Go imperative when** the scene has complex physics, network replication, a character walking around, or procedural generation. Any one of these means imperative. +**Stay declarative when** state maps directly to a fixed set of shown objects and nothing ticks or simulates. + +The two interoperate. A mostly-declarative scene can drop to an imperative node where it needs one, and an imperative scene can mount declarative subtrees. See `references/architecture.md` for the `Game`-class pattern, component-driven nodes, and the hybrid seam. + +## The API shape (where it diverges from what you expect) + +**Transforms.** `Node` has `position`, `rotation` (a `Quaternion`), and `scale`, but they are whole-value get/set, not the mutable spelling other engines use. Assign the whole vector (`node.position = vm.Vector3(0, 1, 0)` or `node.position += ...`). The getters return copies, so `node.position.x = 5` does nothing and throws in debug. For a raw matrix edit use `node.localTransform = matrix` or `node.mutateLocalTransform((m) => m.translateByVector3(...))`; a bare in-place edit of `node.localTransform` never moves the node, because the cache is not told. `node.forward`, `node.right`, and `node.up` return the node's unit world-space basis axes (`+Z`, `+X`, `+Y`), and `node.lookAt(target)` or `node.lookAtFrom(eye, target)` aims local `+Z` at `target`. + +**Geometry.** Ten built-in primitives (`CuboidGeometry`, `SphereGeometry`, `IcosphereGeometry`, `CylinderGeometry` with separate top/bottom radii so cones are free, `CapsuleGeometry`, `TorusGeometry`, `PlaneGeometry`, `DiscGeometry`, `RingGeometry`, `WedgeGeometry`), plus swept geometry (`ExtrudeGeometry`, `TubeGeometry`, `RibbonGeometry`), lines (`PolylineGeometry`), and `GeometryBuilder`/`MeshData` for custom meshes. Do not hand-pack a `ByteData` vertex buffer before checking these. + +**Materials.** `PhysicallyBasedMaterial` (base color, metallic, roughness, normal, emissive, plus clearcoat/sheen/transmission/etc.), `UnlitMaterial`, `ShaderMaterial` for custom shaders. Texture slots take a `TextureSource` (from `loadTexture(path)`), not a raw `gpu.Texture`. + +**Camera and coordinate basis.** `PerspectiveCamera(position: ..., target: ...)`, or `OrthographicCamera(position: ..., target: ..., projection: OrthographicProjection(size: OrthographicSize.height(12)))` for isometric, top-down, and pixel-art views. Every depth effect works under both. Prefer attaching `OrbitCameraController`, `FollowCameraController`, or `FlyCameraController` to a camera node (driven with the `CameraControls` widget) over hand-rolling spherical camera math. When driving a camera yourself, remember that view space is **left-handed (`+Z` forward into the screen, `+Y` up, `+X` right)**. +- Default camera placement sits on the `-Z` side (`(0, 0, -5)`) looking along `+Z` at the origin, where world `+X` points screen-right. If you instead place an eye on the `+Z` side (`eye = target + vm.Vector3(d * sin(a), y, d * cos(a))`) looking back along `-Z`, world `+X` points **screen-left** (`right = vm.Vector3(-cos(a), 0, sin(a))`), not `(cos(a), 0, -sin(a))`. +- Never write `forward.cross(up)` for a right vector; `vector_math`'s right-handed cross product makes `forward.cross(up)` point **screen-left**. Read `camera.right` (or `camera.horizontalRight` and `camera.horizontalForward` for `XZ` ground-plane strafing and panning) or compute `up.cross(forward).normalized()`. + +## What you are probably underestimating (it is all here) + +Models trained on older or thinner information assume flutter_scene has no lighting, no shadows, and no post-processing. It has all of it. Before hand-rolling any of these, know they exist: **directional/point/spot/area lights, shadows (PCSS, contact shadows), GTAO ambient occlusion, screen-space reflections, parallax-corrected reflection probes, SSGI, depth of field, god rays, fog, auto exposure, LUT color grading, bloom, lens flares, MSAA/SMAA/FXAA, tone mapping, instancing, LOD.** See `references/what-exists.md` for the full surface with the class names. + +Low-end and GLES-class GPUs (Raspberry Pi, web, integrated Linux) get a budget, not a different engine: `scene.renderQuality.tier` (`RenderQualityTier.low`, `medium`, `high`; null picks the platform default) decides what `AntiAliasingMode.auto` and the scene color capture budget resolve to, and `scene.renderQuality.adaptive = true` lowers the render scale and then the tier from measured frame periods when frames overrun `targetFrameRate`. Transmissive (glass) materials are the expensive thing on such devices; `scene.sceneColorCaptureBatches = 1` makes them all share one capture. + +## Traps that fail silently (wrong pixels, no error) + +- **Horizontal camera and movement inversion (`forward.cross(up)` and `+Z` orbit tangents).** Because view space has `+Z` into the screen and `vector_math.cross` is right-handed, `forward.cross(up)` yields `-right` (screen-left), and an orbit camera placed at `target + (d * sin(a), y, d * cos(a))` has screen-right `(-cos(a), 0, sin(a))`, not `(cos(a), 0, -sin(a))`. Use `camera.right`, `camera.horizontalRight`, `camera.horizontalForward`, or `up.cross(forward).normalized()`. +- **Custom `ShaderMaterial` output is linear HDR premultiplied by alpha.** No tone mapping or gamma in your shader; the `ResolvePass` applies exposure, tone mapping, and the display transform. Linearize sRGB texture samples yourself. See `MATERIALS.md`. +- **Never hand-roll a per-triangle winding flip to fix glTF orientation.** The importers handle the coordinate conversion; a manual flip leaves normals and IBL wrong. +- **Do not emit a vertex buffer at the wrong stride.** Unskinned is 72 bytes/vertex, skinned is 104; the attribute order is fixed. Use `GeometryBuilder`, do not guess the layout. +- **Surfaces that share a plane flicker as the camera moves (z-fighting), and one screenshot can look fine.** + - Never let two different-looking faces overlap in one plane. Repeated pieces abut (a piece's length equals its spacing), perpendicular runs meet at a corner post, and flat patches at one height never overlap. No depth setting fixes an exact overlap. + - An overlay that lies on a surface (a sign or screen on a wall, road paint, a decal quad, a rug) gets `material.depthLayer = 1` and wins at any distance; an overlay on that overlay gets 2. Place it flush or a few millimetres off, not at a guessed gap. + - Leave the camera's near plane at its default unless geometry clips. The engine fits the plane it rasterizes with to visible content every frame. + - Prove it in motion. `await scene.probeDepthConflicts()` names every pair of nodes that trades pixels from a camera; drive its `conflicts` to empty. `scene.debug.overlays.add(DebugOverlay.depthConflicts)` marks fights live in magenta, and debug builds print overlapping coplanar faces once the scene holds still. +- **When pixels look wrong, look at the surface before guessing.** `scene.debug.view = const DebugView(channel: SurfaceDebugChannel.roughness)` (or `uv0`, `worldNormal`, `baseColor`, `validation`, any `SurfaceDebugChannel`) replaces the lit result with that value on every material, `scene.debug.split = 0.5` compares it against the lit half, `scene.debug.overlays.add(DebugOverlay.wireframe)` traces the mesh, and `node.debugView = DebugView.none` excludes a subtree. The material channels need the views compiled in, which costs shader build time, so they are off by default: set `hooks: user_defines: flutter_scene: debug_views: true` in the app's pubspec (the workspace root's in a pub workspace), plus `flutter_scene_debug_views: true` under the app's own name if its hook calls `buildMaterials` or `buildEngineAssets`, and rebuild. Without it only the geometry and identity channels draw, and the app prints how to turn them on. A `.fmat` shows any intermediate through `material.debug` and the `custom` channel. + +## More depth + +- `references/architecture.md` for the declarative-vs-imperative choice in depth, the `Game`-class pattern, component-driven nodes, and hybrid interop. +- `references/what-exists.md` for the full API surface (the false-absence fix). +- `references/traps.md` for the complete silent-failure list. +- `references/depth-and-layering.md` for why surfaces fight, a catalog of the overlaps generated content makes, and how to find and fix them. +- The repo-root `MATERIALS.md` for the custom-shader contract. + +## Keeping this skill current + +This skill ships inside the flutter_scene package, so upgrading flutter_scene can carry a newer revision of it than the copy installed in the project. To check, run: + +```sh +dart run flutter_scene:skills --check +``` + +It reports the installed and bundled skill versions and exits non-zero when an update is available. If the installed flutter_scene ships a newer skill than what is installed, tell the user, since they are working against out-of-date guidance, and offer to update it with `dart run flutter_scene:skills` (which touches only the skill, not their build hook or pubspec). Worth a check when you start substantial flutter_scene work or when the user mentions upgrading the package. \ No newline at end of file diff --git a/.agents/skills/flutter_scene-idioms/references/architecture.md b/.agents/skills/flutter_scene-idioms/references/architecture.md new file mode 100644 index 00000000..4bb72229 --- /dev/null +++ b/.agents/skills/flutter_scene-idioms/references/architecture.md @@ -0,0 +1,315 @@ +# Structuring a flutter_scene app + +Which API to build the scene with, and how to combine them. The short version is in SKILL.md; +this is the depth, with patterns that compile against 0.22.0. + +flutter_scene exposes the same scene graph two ways. The declarative widgets describe it as Flutter +widgets that rebuild-diff into the graph; the imperative API hands you the retained `Scene`/`Node`/ +`Component` graph directly. They are not competing renderers, they drive the same engine. The choice +is about how your app tracks state, and it is worth making deliberately because reworking a large +scene from one to the other is a rewrite. + +--- + +## The decision + +Go **declarative** when app state maps directly onto a fixed set of shown objects and nothing +simulates. A product configurator, a data-driven diagram, a few models whose transforms follow some +`setState` values. Flutter already owns the state, and the widgets keep the scene tracking it for +free. + +Go **imperative** when the scene simulates. Complex physics, network replication, a character +walking around under input, procedural generation. Any one of these means imperative. Here the +scene's state is the app's state, it changes every frame, and you want to own the loop rather than +express each frame as a widget rebuild. + +If you are unsure, ask whether anything in the scene changes on its own between user actions. If yes, +imperative. If the scene only changes when the user changes a value, declarative. + +--- + +## Declarative + +`SceneView.declarative` owns an internal `Scene`; its `children` are the whole scene description. + +```dart +class Configurator extends StatefulWidget { + const Configurator({super.key}); + @override + State createState() => _ConfiguratorState(); +} + +class _ConfiguratorState extends State { + // Build engine objects once, not per rebuild. Constructing GPU resources + // every build is the main performance hazard of the declarative layer. + final Geometry _geometry = CuboidGeometry(vm.Vector3(1, 1, 1)); + final PhysicallyBasedMaterial _material = PhysicallyBasedMaterial(); + double _spin = 0; + + @override + Widget build(BuildContext context) { + return Column(children: [ + Expanded( + child: SceneView.declarative( + camera: PerspectiveCamera(position: vm.Vector3(2, 2, -4)), + children: [ + SceneMesh( + geometry: _geometry, + material: _material, + rotation: vm.Quaternion.axisAngle(vm.Vector3(0, 1, 0), _spin), + ), + ], + ), + ), + Slider( + value: _spin, + max: 6.28, + onChanged: (v) => setState(() => _spin = v), + ), + ]); + } +} +``` + +The scene tracks widget state through the normal rebuild path. Note two things the example shows: +engine objects (`geometry`, `material`) are created once and held as fields (they are diffed by +identity, and rebuilding them every frame is the classic mistake), while cheap value props +(`rotation`) are fine to pass fresh each build. + +Declarative building blocks (all under `SceneView.declarative` or a `SceneView` with `children`): + +- `SceneMesh(geometry:, material:, ...)` a node with a mesh. +- `SceneNode(...)` a bare transform node, for grouping children. +- `SceneModel('assets/x.glb', animations: [...])` a loaded model (runtime glTF path). +- `SceneSubtree(parent:, children:)` mounts children under a given imperative `Node`. +- Every node widget takes `position`/`rotation`/`scale` (or a full `transform`), `visible`, + `components:` (attach imperative `Component`s), `controller:` (a `SceneNodeController` handle), and + `children:`. + +--- + +## Imperative + +Own the `Scene`, add `Node`s, attach `Component`s, display with `SceneView(scene, camera:, onTick:)`. +For anything beyond a demo, do not scatter this across a `StatefulWidget`. Put it in a plain Dart +class that owns the scene and the game state, and keep the widget thin. + +```dart +// Pure Dart, no Flutter import. Owns the scene and the game state. +class Game { + final Scene scene = Scene(); + late final Node player; + + Future load() async { + await Scene.initializeStaticResources(); + + // The camera lives in the scene as a node, not on the widget. A camera + // node's transform is its view: the translation is the eye, local +Z is + // the look direction, +Y is up. lookAtFrom sets both at once, so there is + // no view-matrix math to hand-roll. + final cameraNode = Node() + ..addComponent(CameraComponent(activateOnMount: true)) + ..lookAtFrom(vm.Vector3(0, 3, -8), vm.Vector3.zero()); + scene.add(cameraNode); + // activateOnMount makes this the scene's primary camera when the node + // mounts, so SceneView needs no `camera:` argument. (The first mounted + // camera auto-promotes anyway; this states the intent explicitly, and is + // how you pick one when several cameras exist.) + + player = Node(mesh: Mesh(CuboidGeometry(vm.Vector3(1, 1, 1)), + PhysicallyBasedMaterial())); + player.addComponent(PlayerController()); + scene.add(player); + } + + // Per-frame app logic that is not tied to one node. Component updates run + // on their own (see below), so this is for whole-game concerns. + void tick(double dt) { + // advance timers, spawn waves, read input, etc. + } +} +``` + +```dart +// Thin widget: builds the game, forwards ticks, renders the scene. +class GameView extends StatefulWidget { + const GameView({super.key}); + @override + State createState() => _GameViewState(); +} + +class _GameViewState extends State { + final Game game = Game(); + bool _ready = false; + + @override + void initState() { + super.initState(); + game.load().then((_) { + if (mounted) setState(() => _ready = true); + }); + } + + @override + Widget build(BuildContext context) { + if (!_ready) return const SizedBox.expand(); + // No `camera:` here: the view resolves the scene's active camera, which is + // the CameraComponent added in Game.load. Resolution order is the explicit + // `camera:` (absent), then `cameraBuilder`, then `scene.camera` (the active + // CameraComponent), then a default camera. + return SceneView( + game.scene, + onTick: (elapsed, dt) => game.tick(dt), + ); + } +} +``` + +### The active camera + +The scene owns which camera is active, and there are three levers: + +- **`CameraComponent(activateOnMount: true)`** (above) selects this camera when its node mounts. +- **`cameraComponent.makeActive()`** switches to it at runtime, for example a chase-cam to a + cutscene camera. Before its node mounts the choice is deferred and applied on mount. +- **`scene.camera = someCamera`** sets any `Camera` as the override directly, and `scene.camera` + reads the active one back. + +With no camera set at all, the first mounted `CameraComponent` auto-promotes, and a scene with none +still renders through a default camera. Move or rotate a `CameraComponent`'s node to move the view; +the `NodeCamera` reads the node's world transform live each frame. Aim it with `node.lookAt(target)` +(rotate toward a world point) or `node.lookAtFrom(eye, target)` (position and aim in one call); +Z is +the forward axis, so the same helpers aim lights and imported models. A follow-cam is then a one-line +component that calls `node.lookAtFrom(...)` in `update` each frame. + +### Camera controllers (interactive cameras) + +For a user-controlled camera, do not hand-roll the drag/scroll/key math: attach a camera controller +component to the camera node. `OrbitCameraController` (turntable around a target, drag rotates, scroll +dollies), `FlyCameraController` (WASD + drag free flight, `moveVertical: false` gives grounded +first-person), and `FollowCameraController` (third-person that eases behind a target node). Each holds +the camera state, eases toward it with frame-rate-independent smoothing, clamps pitch so the view +never flips, and writes the node via `lookAtFrom`. + +Wire input with the `CameraControls` widget wrapping the view; it forwards Flutter gestures and keys +to the controller. `SceneView` itself has no camera-input knobs, so nothing camera-specific leaks into +it. + +```dart +final camera = Node() + ..addComponent(CameraComponent(activateOnMount: true)) + ..addComponent(OrbitCameraController(target: vm.Vector3.zero(), distance: 8)); +scene.add(camera); + +// In build: +return CameraControls( + controller: camera.getComponent()!, + child: SceneView(scene), +); +``` + +The controllers also expose intent methods (`orbitBy`, `dollyBy`, `panBy`, `look`), so an app with its +own input handling can drive them without the widget. + +### Game input (flutter_scene_input) + +For games (rebindable controls, gamepads, pointer lock, pause menus), do not hand-roll held-key sets or +raw `gamepads` parsing: add the separate `flutter_scene_input` package. Declare typed const actions +(`VectorAction('move')`, `ButtonAction('jump')`, `DeltaAction('look')`), bind them in an `ActionSet` +with named slots (`'keyboard'`, `'gamepad'`), push the set on `player.contexts`, and call +`scene.attachInput()` so read windows advance with the scene. Read `player.button(jump).justPressed` +and `player.vector(move)` in `update`, and through `player.fixed` in `fixedUpdate`. Drivers +(`CharacterInputDriver`, `FlyCameraInputDriver`, `OrbitCameraInputDriver`) feed the bundled +controllers, and `DefaultActions` has ready-made sets. + +Traps. 2D values are +Y up everywhere, mouse movement included, so convert before passing a drag +`Offset` to `look`. A stick feeding a `DeltaAction` needs a `PerSecond` processor. Wrap the game view (not +the HUD) in `InputListener` for mouse input. Lock the cursor with `PointerLock.instance.lock()` from a +pointer or tap handler and pause when a listener sees `isLocked` go false; never add the `pointer_lock` +package. Rebinding goes through `player.overrides` and `listenForBinding`, and profiles persist with +`overrides.toJsonString()`. + +### Behavior lives in components, not in the tick + +The bulk of per-object logic should be custom `Component`s, not a giant `onTick`. A component is +attached to a node and the engine runs it through the lifecycle. Crucially, component ticks are +driven automatically by the render path, so you do not call them yourself, and `onTick` is only for +game-wide concerns that do not belong to a single node. + +```dart +class PlayerController extends Component { + vm.Vector3 velocity = vm.Vector3.zero(); + + @override + void onMount() { + // node is available here; wire up input, cache references. + } + + @override + void update(double deltaSeconds) { + // `node` is the node this component is attached to. + node.mutateLocalTransform( + (m) => m.translateByVector3(velocity * deltaSeconds), + ); + } +} +``` + +Component lifecycle hooks (subclass `Component`, override what you need): + +- `onAttach()` added to a node, before it is in a live scene. +- `Future onLoad()` async setup (await assets); mount waits for it. +- `onMount()` the node entered a live scene; `node` is usable. +- `update(double deltaSeconds)` per rendered frame. +- `fixedUpdate(double fixedDt)` fixed-step, driven by the physics accumulator when a `PhysicsWorld` + is present. Put physics-coupled logic here, not in `update`. +- `onUnmount()` / `onDetach()` teardown. + +This is the structure that scales. A character is a node with a controller component; an enemy is a +node with an AI component; a pickup is a node with a trigger component. The `Game` class holds what +is genuinely global (score, wave state, the input map), and everything spatial is a component on a +node. + +--- + +## Hybrid interop + +The two APIs share one graph, so you can mix them at the seam that suits the app. + +**Declarative shell, imperative pockets.** A declarative node accepts `components:`, so an otherwise +declarative scene can attach imperative behavior to any node without leaving the widget tree. + +```dart +SceneMesh( + geometry: _geometry, + material: _material, + components: [Spinner()], // a custom Component, ticked by the engine +) +``` + +**Imperative scene, declarative subtrees.** `SceneView(scene, children: [...])` mounts declarative +widgets over an app-owned scene. Use `SceneSubtree(parent: someNode, children: [...])` to attach a +declarative subtree under a specific imperative node, for example UI-like markers that follow a +game object. + +```dart +SceneView( + game.scene, + camera: PerspectiveCamera(position: vm.Vector3(0, 3, -8)), + children: [ + SceneSubtree( + parent: game.player, + children: [SceneModel('assets/hat.glb')], + ), + ], +) +``` + +**Reaching an imperative node from a declarative widget.** Pass a `SceneNodeController` as +`controller:` and read `controller.node` for the managed `Node` (null while unmounted). This is the +escape hatch when a declarative node needs an imperative handle for a one-off operation. + +The rule of thumb: pick the mode that matches how the *majority* of the scene is driven, then use the +seam above for the exceptions. Do not build a whole simulation out of declarative widgets to avoid +the imperative API, and do not hand-roll a diffing layer over the imperative graph to avoid the +declarative one. diff --git a/.agents/skills/flutter_scene-idioms/references/depth-and-layering.md b/.agents/skills/flutter_scene-idioms/references/depth-and-layering.md new file mode 100644 index 00000000..c2aa0994 --- /dev/null +++ b/.agents/skills/flutter_scene-idioms/references/depth-and-layering.md @@ -0,0 +1,64 @@ +# Depth and layering + +Two surfaces that cover the same pixels at nearly the same depth flicker against each other as the camera moves (z-fighting). One screenshot rarely shows it, because each frame picks a winner and a different frame picks the other one. This page covers why it happens, the overlaps generated scenes make most, how to fix each, and how to prove a scene is clean. + +## Why surfaces fight + +**Exact overlaps.** Two faces in one plane built from different vertices each compute their depth from their own rounded vertex positions and from the rasterizer's sub-pixel snapping. The difference is noise whose sign changes across the surface and with any camera motion. On a surface seen at a grazing angle that noise is large, a fraction of a pixel's worth of the surface's own depth slope. No near plane, depth format, or precision fixes an exact overlap; only an order does (a layer, a content change). + +**Near overlaps.** Two parallel faces a few millimetres or centimetres apart hold their order up close and fight past the distance where the depth buffer can no longer separate them. flutter_scene spends depth precision where content is: it fits the near plane it rasterizes with to visible geometry every frame, and on Metal and most browsers it stores reversed float depth, which keeps precision nearly even with distance. The `depthGap` debug view (below) shows how far apart two surfaces must be at each pixel. + +## The overlaps generated scenes make, and the fix for each + +| Pattern | Why it fights | Do instead | +| --- | --- | --- | +| A fence, barrier, curb, or rail of repeated pieces longer than their spacing (8.4 m pieces every 8 m) | every joint holds two faces in one plane, and alternating colors make it visible | make each piece exactly as long as the spacing, or put a slightly larger post at each joint | +| Perpendicular runs that both extend into a corner | the corner holds overlapping faces | stop one run at the other's face, or cap the corner with a post | +| Ground patches, tiles, or rugs at one height that overlap each other | same-height faces of different colors | place them without overlap (`PoissonDiscSampler` in the kit), or give each overlapping kind its own `depthLayer` | +| A screen, sign, window, poster, or road marking flush with (or a few millimetres off) its surface | exact or near overlap | `material.depthLayer = 1` on the overlay; 2 for an overlay on that overlay | +| A box wrapped by a slightly larger box (window bands, trim, a frame) | parallel faces a few centimetres apart, seen from far away | keep the wrap a clear distance proud, put wraps on separate height slots so two never overlap, or bake the detail into the material | +| The same mesh added twice (a duplicated node, a model loaded into two parents) | bit-identical faces | remove the duplicate | +| `doubleSided` forced on materials whose meshes have back-to-back faces | culling no longer hides the coincident faces | leave `doubleSided` off unless the mesh needs it | +| An object placed at a guessed surface height | it hovers or sinks a few centimetres, or sits exactly in the surface | measure the surface with `scene.raycast` or the asset's bounds before placing | + +## Mechanisms, in order of preference + +1. **Fix the content.** Abut, space, or merge the pieces. Nothing fights when nothing overlaps. +2. **`Material.depthLayer`.** For an overlay that lies on a surface. A higher layer draws over coplanar surfaces with a lower one at any distance and on every backend. Each layer moves the surface a quarter pixel of its own depth slope plus a few depth steps toward the camera, in the vertex stage, so it costs nothing per fragment and moves nothing on screen by more than a fraction of a pixel. Negative layers let a ground yield to everything on it. Two different materials at the same layer that overlap still fight. +3. **`DecalNode`.** Projects a mark onto uneven surfaces without creating coplanar geometry. +4. **Bake detail into the material.** A grid of lit windows as a pattern in a `.fmat` instead of thousands of boxes on a facade. +5. **`Material.depthBias`.** A world-space nudge toward the camera. It buys fewer depth steps the farther away it is seen, so it holds up close and fails far away. Prefer `depthLayer`. +6. **`Scene.coplanarTieBreak`.** An experimental safety net for generated content. It gives every unlayered material and instance an arbitrary rank so exact overlaps resolve to a stable winner. The rank is arbitrary, so an overlay can end up consistently hidden, and surfaces a sliver apart far from the camera can swap. Fix the overlap or set `depthLayer` instead where you can. + +## The camera + +Leave `near` at its default unless geometry clips. The engine rasterizes with the largest near plane that clips nothing visible and keeps the authored one as a floor, so a chase or aerial camera gets the precision of a much larger near for free (`Scene.fitNearPlane`, on by default). The fit falls back to the authored near when the camera sits inside an item's bounds (a sky sphere, one merged city mesh) or an item has no bounds; debug builds print which node blocked it. Draw a sky with `scene.skybox` rather than a giant sphere around the camera. + +`Scene.reversedDepth` (on by default) stores depth reversed, which keeps float depth precise at every distance. Turn it off only for a custom `ShaderMaterial` vertex shader that writes clip-space depth assuming the standard mapping. + +## Proving a scene is clean + +Check in motion, with numbers, not with a screenshot. + +- **`await scene.probeDepthConflicts()`** renders the view's object ids several times (reversed draw order, changed depth rounding, sub-pixel camera offsets, and surfaces nudged a sliver apart) and reports every pair of nodes whose pixels change hands. `report.conflicts` is empty when nothing fights; `report.describe()` is a short log with node paths, pixel counts, and distances. Probe from the cameras the app actually uses, after the scene has rendered a frame. Surfaces sharing a material still count, since vertex colors or texture coordinates can tell them apart. Blended surfaces and a `.fmat` cut by its own `Surface()` are listed in `report.untested` rather than tested. + + ```dart + final report = await scene.probeDepthConflicts(camera: camera); + if (report.conflicts.isNotEmpty) debugPrint(report.describe()); + ``` + +- **`scene.findCoplanarOverlaps()`** checks the geometry without rendering and returns every pair of faces that overlap in one plane (or sit closer than the depth buffer can separate from the camera), with the overlap area, where it is, and for repeated pieces the length that fixes it. Unlike the probe, it also catches instances of one `InstancedMesh` whose colors differ overlapping each other. Debug builds run it once the scene holds still and print a summary; set `scene.debugCheckCoplanarOverlaps = false` to silence it. +- **`scene.debug.overlays.add(DebugOverlay.depthConflicts)`** marks fighting pixels live with a crawling magenta checkerboard. +- **`scene.debug.view = const DebugView(channel: SurfaceDebugChannel.depthGap)`** colors each pixel by the gap two surfaces need there to keep their order, blue for a hundredth of a millimetre through red for a metre. An overlay closer to its surface than that gap needs a layer. +- **`SurfaceDebugChannel.objectColor`** shows two node colors interleaving where they fight. + +The tells of a fight without tools: a material edit that changes nothing on screen (the other surface is winning there), a surface that looks right from one camera only, stripes or speckles that crawl as the camera moves, and flicker that only appears at grazing angles or far away. + +## Habits that do not work + +- **`Node.renderOrder` does not order opaque coplanar faces.** Depth decides between them, not draw order. +- **Turning off the depth test** draws the overlay through everything in front of it. +- **Forcing `doubleSided` broadly** exposes coincident back faces. +- **Shrinking the far plane** barely changes precision; the near plane dominates, and the engine already fits it. +- **A tiny fixed world offset** for distant content fails past a distance; use `depthLayer`. +- **Polygon offset factors** have no API here; `depthLayer` is the slope-aware offset. diff --git a/.agents/skills/flutter_scene-idioms/references/traps.md b/.agents/skills/flutter_scene-idioms/references/traps.md new file mode 100644 index 00000000..d36e9fb2 --- /dev/null +++ b/.agents/skills/flutter_scene-idioms/references/traps.md @@ -0,0 +1,632 @@ +# flutter_scene silent-failure traps + +Mistakes that produce wrong pixels with no exception and no console message. Each entry gives the +mistake, what you see, and what to do instead. Sorted worst-first (most likely to hit, hardest to +diagnose from the symptom). + +Some of these are now caught by the engine in version 0.22.0. Those are tagged **[0.22.0 catches +this]** with what the engine does, so if you see that error you know what it means. The rest are +still silent, so you have to recognize them yourself. + +--- + +## 1. Editing a transform in place instead of assigning it + +**Mistake.** `node.localTransform.setTranslation(v)`, `node.localTransform..rotateY(t)`, +`node.position.x = 5`, or any edit of the matrix/vector a getter returns. This is the natural +`vector_math` style and the first thing most people reach for. + +**Symptom.** The node does not move. Not "moves wrong", nothing happens, forever, including its +children and bounds. Reading `node.localTransform`/`node.position` back shows the value you wrote, so +the state looks correct while the render disagrees. + +**Do instead.** Assign a fresh value (`node.position = ...`, `node.localTransform = node.localTransform.clone()..translateByVector3(v)`), +use the component setters `node.position`/`node.rotation`/`node.scale`, or edit the raw matrix +through `node.mutateLocalTransform((m) => m.translateByVector3(v))`, which dirties the cache for you. + +**[0.22.0 catches this]** Debug builds throw a `StateError` naming the node and the fix, both for an +in-place `localTransform` edit and for editing a copy returned by `position`/`rotation`/`scale`. + +--- + +## 2. Faces of different surfaces overlapping in one plane + +**Mistake.** Repeated pieces longer than their spacing (an 8.4 m barrier every 8 m), runs that both +extend into a corner, patches of different colors overlapping at one height, a sign, screen, or +road marking laid flush on its surface, or the same mesh added twice. + +**Symptom.** The overlap flickers between the two surfaces as the camera moves (z-fighting): stripes, +speckles, or triangles of the wrong color that crawl. Any one screenshot can look right, and no near +plane or depth setting fixes an exact overlap. + +**Do instead.** Make repeated pieces abut (length equals spacing) and cap joints with a post, keep +same-height patches apart, and give an overlay that lies on a surface `material.depthLayer = 1`. +`await scene.probeDepthConflicts()` names every pair that fights from a camera, and +`scene.findCoplanarOverlaps()` lists the overlapping faces with the length that fixes a repeated +piece. See `depth-and-layering.md`. + +**[Caught in debug]** Debug builds print the overlapping coplanar faces once the scene holds still, +naming the nodes and the fix. + +--- + +## 3. Passing a normal or metallic-roughness map as `TextureContent.color` + +**Mistake.** `material.normalTexture = await Texture2D.fromAsset('brick_normal.png')` without +`content: TextureContent.normal`. The `content` parameter defaults to `color`. + +**Symptom.** The base mip is fine, so it looks right up close and progressively wrong with distance: +normals flatten and skew, roughness reads too smooth at range, specular shimmers. A +distance-dependent symptom is nearly the worst case for screenshot-driven iteration. + +**Do instead.** Build non-color maps with the right content: `Texture2D.fromAsset(path, content: +TextureContent.normal)` for normal maps, `TextureContent.data` for metallic-roughness, AO, and other +linear data. Still silent, so this is on you. + +--- + +## 4. Non-uniform scale on a lit mesh + +**Mistake.** Any non-uniform scale on the node or an ancestor, e.g. `node.scale = Vector3(1, 3, 1)`. + +**Symptom.** Lighting, specular, and reflections are wrong across the whole mesh. It reads as a +shading or material bug, so it sends you into the materials, never the transform. + +**Do instead.** Use a uniform scale, or bake the non-uniform scale into the geometry with +`MeshData.transformed(matrix)` and build a fresh `MeshGeometry` from it. Still silent. + +--- + +## 5. Moving a skinned mesh node + +**Mistake.** `skinnedNode.localTransform = Matrix4.translation(v)` on a node that carries a `Skin`. + +**Symptom.** The mesh does not move. Worse, it *does* move when the node you transformed happens to +be an ancestor of the skeleton's joints, so it looks intermittent across models. + +**Do instead.** glTF requires a skinned mesh node's own transform to be ignored, so the engine passes +identity. Move the skeleton root (the common ancestor of `skin.joints`) instead, or parent both the +mesh node and the skeleton under a shared node and move that. Still silent. + +--- + +## 6. Replacing the transform of a runtime-imported model root + +**Mistake.** +```dart +final model = await Node.fromGlbAsset('assets/ship.glb'); +model.localTransform = Matrix4.translation(v); // wipes the handedness flip +``` + +**Symptom.** The model renders mirrored through Z (asymmetric geometry reversed, text backwards) with +normals and IBL wrong for the mirrored orientation. It still draws and is not obviously broken. + +**Do instead.** The runtime glTF importer synthesizes a root carrying a `scale(1, 1, -1)` handedness +flip. Do not overwrite it. Parent that node under a new `Node` and transform the parent, or +pre-multiply your transform by the existing `localTransform`. (The offline `.fscene`/`loadScene` path +bakes handedness into the vertices, so its roots are identity and do not have this trap.) Still +silent. + +--- + +## 7. `EnvironmentMap.fromGpuTextures` with a raw panorama + +**Mistake.** +```dart +final tex = await gpuTextureFromAsset('assets/panorama.png'); +scene.environment = EnvironmentMap.fromGpuTextures(prefilteredRadiance: tex); +``` + +**Symptom.** Every reflective surface (and the sky) shows the top 1/8 of the panorama stretched 8x +vertically, cross-fading between slices as roughness varies. Diffuse is fully black. + +**Do instead.** `fromGpuTextures` expects an already-prefiltered radiance atlas, not a plain image. +Run the source through `prefilterEquirectRadiance()` first, or just use +`EnvironmentMap.fromEquirectImageAsset(assetPath: ...)`/`fromUIImages`, which prefilter and project +SH for you. Still silent. + +--- + +## 8. `ShaderMaterial(cullingMode: none)` for a double-sided custom material + +**Mistake.** `ShaderMaterial(cullingMode: gpu.CullMode.none)`, or setting `doubleSided = true` on a +`ShaderMaterial`. The two do not agree. + +**Symptom.** `cullingMode.none` draws back faces in color but leaves them out of the depth prepass, so +SSAO, SSR, and contact shadows sample the wrong surface exactly where a back face shows: dark halos, +wrong reflections, occlusion bleeding through a leaf card. `doubleSided = true` does the opposite: the +color pass still culls while the prepass draws the extra faces. + +**Do instead.** For a truly double-sided `ShaderMaterial`, understand that the color cull and the +prepass cull are driven separately today; keep the mesh single-sided where the depth-based effects +need to match, or split it. Still silent. + +--- + +## 9. Caller-supplied `bounds` that do not cover the geometry + +**Mistake.** `MeshGeometry.fromArrays(positions: p, bounds: someAabb)` where the AABB does not contain +every position, or `setLocalBounds` with a guessed or stale box. + +**Symptom.** The mesh pops out of existence at some camera angles and reappears at others; part of a +large mesh vanishes; shadows disappear before the caster does. Intermittent and view-dependent, so a +single screenshot can look fine. + +**Do instead.** Widen the bounds to cover every vertex, or just omit the `bounds` argument and let the +constructor scan the positions. An absent bounds is safe (it means always-visible); only a wrong one +is dangerous. Still silent. + +--- + +## 10. Binding a mipless texture to a material + +**Mistake.** `material.baseColorTexture = GpuTextureSource(await gpuTextureFromAsset('brick.png'))`. +The helper's own doc even recommends this. + +**Symptom.** Severe minification aliasing: crawling and shimmering on any surface at an angle or +distance, sparkling on a metallic-roughness map. `Texture2D.fromAsset` on the same file looks fine, so +two seemingly equivalent APIs disagree visually. + +**Do instead.** Use `Texture2D.fromAsset`/`fromImage`/`fromPixels`, which generate a mip chain, or +supply a texture you mipped yourself. Reserve `gpuTextureFromAsset` for non-material uses. Still +silent. + +--- + +## 11. `RenderView.layerMask`/`Node.layers` mismatch, or a zero mask + +**Mistake.** Putting a node on a non-default layer and forgetting the view side (or vice versa), or +`layerMask: 0`, or `Node.layers = 2` meaning to select "layer 2" (which is actually `1 << 2 == 4`). + +**Symptom.** A node, a group, or the whole scene is simply absent, with no hint a mask is involved. A +`layerMask` of 0 renders nothing. + +**Do instead.** `Node.layers` is a bitmask and is NOT inherited by children, so set it on each node +you want the view to see. Use `kRenderLayerAll` to see everything, or a bitmask like `(1 << 2)`. +Match the view's `layerMask` to the nodes' `layers`. Still silent (but see #24 for the +draws-nothing diagnostic). + +--- + +## 12. Mutating a `TextureTransform` in place + +**Mistake.** `material.baseColorTextureTransform.offset.x = 0.5` instead of assigning a fresh +`TextureTransform`. Same shape as trap #1, for materials. + +**Symptom.** UV scroll/rotation animation freezes at the first value, but ONLY for materials on the +physical-variant path (any material with clearcoat/sheen/transmission/etc). The identical code works +on a plain PBR material, so it reads as a shader bug. + +**Do instead.** Assign a new transform each frame: `material.baseColorTextureTransform = +TextureTransform(offset: ...)`. Still silent. + +--- + +## 13. Environment image that is not 2:1 equirectangular + +**Mistake.** Passing a cube cross, a 1:1 angular light probe, or a cropped panorama to any environment +entry point. HDRI downloads are not reliably 2:1. + +**Symptom.** The scene is lit from wildly wrong directions, reflections show mirrored or duplicated +content, the sky is smeared. + +**Do instead.** Re-project the source to a 2:1 latitude-longitude panorama before loading. Cube +crosses and angular probes are not supported. Still silent. + +--- + +## 14. Hand-built triangles wound clockwise + +**Mistake.** Generating triangles with clockwise winding instead of counter-clockwise (CCW) winding around the outward normal when feeding `MeshGeometry.fromArrays` or `GeometryBuilder`. + +**Symptom.** The mesh is invisible from outside and visible from inside; a closed shape looks hollow or inside-out; lighting is inverted where it shows. ("See-through faces.") + +**Do instead.** flutter_scene's front faces wind COUNTER-CLOCKWISE (CCW) in model space around the outward face normal, matching glTF. Ensure triangle indices wind CCW when viewed from outside the surface, or omit `normals` and let `GeometryBuilder` derive them from your winding. Still silent. + +--- + +## 15. Out-of-range indices in `fromArrays` + +**Mistake.** `MeshGeometry.fromArrays(positions: p /* 100 verts */, indices: [0, 1, 100])`, e.g. from +an off-by-one or an index list built against a different vertex array. + +**Symptom.** Stray triangles stretching to the origin or infinity, holes, flicker. On some backends +the fetch is clamped and on others it reads adjacent memory, so the symptom differs per backend. + +**Do instead.** Keep every index in `0 .. vertexCount - 1`. (`GeometryBuilder.addTriangle` range-checks +for you and throws; the `fromArrays` index path does not.) Still silent on the `fromArrays` path. + +--- + +## 16. A `vertexCount` that does not match the buffer in `setVertices` + +**Mistake.** `geometry.setVertices(bufferView, vertexCount)` where `vertexCount` is a byte count, a +float count, or a triangle count rather than a vertex count. + +**Symptom.** Too small: part of the mesh is missing. Too large: the draw reads past the buffer, giving +stray geometry or a dropped draw depending on backend. The buffer is fine, so the investigation goes +to the packing code. + +**Do instead.** `vertexCount` is a count of vertices. Prefer `uploadVertexData` (which validates the +stride, see #18) or `fromArrays` over the caller-managed `setVertices` path unless you really own the +GPU buffer. Still silent. + +--- + +## 17. Oversized texture on a low-end device + +**Mistake.** `EnvironmentMap.fromEquirectImageAsset(assetPath: 'pano_16k.hdr', maxWidth: 16384)` or +`EnvironmentMap.radianceCubeSize = 4096` on a device whose max texture size is lower. + +**Symptom.** A completely black environment: no IBL, no reflections, black sky. Works on the dev +machine, black on a phone. + +**Do instead.** Keep environment and texture sizes within the device limit; lower `maxWidth` or +`radianceCubeSize`. Test on the lowest-end target you support. Still silent. + +--- + +## 18. Hand-packing vertex bytes at the wrong stride + +**Mistake.** `SkinnedGeometry()..uploadVertexData(bytes, vertexCount, indices)` with the wrong stride +(a common one is 96 bytes having forgotten UV1, or the legacy 80-byte layout). + +**Symptom.** Washed-out colors, see-through faces, geometry smeared toward the origin. + +**Do instead.** Unskinned vertices are 72 bytes (position 3, normal 3, tex_coords 2, tex_coords_1 2, +color 4, tangent 4, all float32), skinned are 104 (+ joints 4, weights 4). Better, do not hand-pack: +use `MeshGeometry.fromArrays`, `fromMeshData`, or `GeometryBuilder`. + +**[0.22.0 catches this]** `uploadVertexData` on both `SkinnedGeometry` and `UnskinnedGeometry` now +throws an `ArgumentError` when the byte length does not match `vertexCount * stride`, naming the +expected layout. + +--- + +## 19. Custom attribute length not matching the vertex count + +**Mistake.** `geometry.setCustomAttribute('a_wind', data, components: 3)` where `data` has the wrong +length, or set before uploading vertices, or not re-set after a `rebuild` changed the count. + +**Symptom.** The attribute is read at the wrong stride, so every vertex gets a neighbor's value: a +displacement shader shears the mesh, a color attribute smears. Nearly right, so hard to spot. + +**Do instead.** `data.length` must equal `vertexCount * components`. Set the attribute after uploading +vertices, and re-set it after any rebuild. Depth and shadow passes run the material's full vertex +variant, so an attribute-driven displacement casts its shadow. The mesh and material sides need not +match: a declared attribute the mesh lacks reads zero, and a mesh's extra attributes are ignored by +materials that do not declare them. + +**[0.22.0 catches this]** `setCustomAttribute` now throws an `ArgumentError` on a length mismatch +(once the vertex count is known). + +--- + +## 20. `UnlitMaterial` with `AlphaMode.mask` + +**Mistake.** `UnlitMaterial(colorTexture: foliage)..alphaMode = AlphaMode.mask` for cutout foliage. + +**Symptom.** No alpha test. Cutout edges render soft and blended, the material goes through the +translucent pass, writes no depth, sorts badly against itself, and casts no cutout shadow. + +**Do instead.** `UnlitMaterial` does not implement `mask` (it behaves as `blend`). Use +`PhysicallyBasedMaterial` for cutouts, or a `.fmat` unlit material that discards below your cutoff. +Still silent. + +--- + +## 21. `vertexColorWeight` on a material that took a physical variant + +**Mistake.** +```dart +final m = PhysicallyBasedMaterial()..vertexColorWeight = 0.0; +m.clearcoat = 1.0; // or sheen/transmission/anisotropy/ior != 1.5/any extension texture +``` + +**Symptom.** Vertex colors snap back to full strength the moment an unrelated extension is enabled. +On a vertex-colored import, an abrupt tint change with no plausible cause. (The same gap silently +drops `specularAntiAliasingVariance` and `specularAntiAliasingThreshold` on the variant path.) + +**Do instead.** Leave `vertexColorWeight` at 1.0 when using any advanced PBR feature, or drop the +extension. Still silent. + +--- + +## 22. Vertex-stage binding on a `ShaderMaterial` with no vertex shader + +**Mistake.** +```dart +final m = ShaderMaterial(fragmentShader: frag); +m.setUniformBlock('WaveInfo', bytes, stage: ShaderStage.vertex); // never set a vertex shader +``` + +**Symptom.** The vertex-stage parameter has no effect; geometry stays undisplaced while the fragment +stage looks right. Reads as "my vertex shader is not running." + +**Do instead.** Pass a `vertexShader` (and `skinnedVertexShader`/`depthVertexShader` for those mesh +kinds) to the constructor before binding vertex-stage blocks, or bind the block on +`ShaderStage.fragment`. Still silent. + +--- + +## 23. A `ShaderMaterial` vertex shader on line/trail/polyline geometry + +**Mistake.** Attaching a `ShaderMaterial` that supplies a vertex shader to a `LineSegmentsGeometry`, a +trail, or a polyline. + +**Symptom.** Lines vanish or explode into garbage. The unskinned vertex shader is paired with the +line-segments instanced layout and never does the ribbon expansion. + +**Do instead.** These geometries do their vertex expansion in the engine's own shader; a material +vertex shader cannot be used with them. Drop the vertex shader for line/trail/polyline geometry, or +use a mesh geometry. Still silent. + +--- + +## 24. Four different causes of a blank frame + +**Mistake.** Any of: a degenerate camera (target equals position, or `up` parallel to the view +direction, e.g. a top-down camera left at the default `up`), a field of view passed in degrees +(`fovRadiansY: 60`), an inverted or zero frustum, `layerMask: 0`, a zero-area draw region, or +rendering before `Scene.isReadyToRender`. + +**Symptom.** The entire scene is empty. Every one of these looks identical, so it is easy to "fix" +lighting, materials, and geometry for many iterations before suspecting the camera or the mask. + +**Do instead.** For a top-down/bottom-up camera set `up` to `Vector3(0, 0, 1)` or `Vector3(0, 0, +-1)`, not the default `(0, 1, 0)`. Pass FOV in radians (`60 * degrees2Radians`). Keep `near > 0` and +`far > near`. Give the view a non-zero `layerMask` and a non-empty draw region. + +**[0.22.0 catches most of this]** Degenerate cameras (zero view direction, parallel `up`, degrees-valued +FOV, degenerate near/far) assert in debug. And a frame that issues zero draw calls now prints once in +debug naming the likely cause (not ready, empty region, no views, no visible meshes, or a layer mask +matching nothing). + +--- + +## 25. Missing bounds after swapping a primitive's geometry + +**Mistake.** `mesh.primitives[0].geometry = newGeometry` for hand LOD, a rebuilt procedural mesh, or a +variant swap. + +**Symptom.** The new geometry is culled against the old geometry's bounds; if it is larger or +displaced, it pops in and out exactly like trap #9. + +**Do instead.** Nothing extra is needed anymore. + +**[0.22.0 catches this]** A `Mesh` now recomputes its bounds on its own when a primitive's geometry +identity changes, so the manual `markLocalBoundsDirty()` is no longer required. + +--- + +## 26. A `.fmat` material that overruns the 15-sampler budget + +**Mistake.** A `lit` or `physical` `.fmat` declaring several `sampler2d` parameters plus +`engine_inputs: [scene_color, scene_depth]`, on top of the lit framework's own textures. + +**Symptom.** Geometry disappears on a mid-range Android device while everything is correct on Metal, +with no build-time signal. The draw is rejected on GLES drivers reporting the 16-unit minimum. + +**Do instead.** The lit fragment shader budgets 15 fragment samplers. Pack channels into one texture +(an ORM-style atlas), drop an `engine_input`, or make the material unlit. Still silent (fails at +runtime on the device, not at build). + +--- + +## 27. `RenderView.viewport` with a `target` set + +**Mistake.** `RenderView(camera: cam, target: myRenderTexture, viewport: Rect.fromLTWH(0, 0, 0.5, 1))` +expecting a half-width render into the texture. + +**Symptom.** The view fills the entire render texture; the passed `viewport` is ignored. Reads as +"my viewport math is off." + +**Do instead.** `viewport` is ignored when `target` is set. Size the `RenderTexture` to the region you +want, or drop the target to render a sub-rect of the screen. Still silent. + +--- + +## 28. Scaled or mirrored camera node + +**Mistake.** Attaching a `CameraComponent` to a scaled node, or parenting a camera node under a scaled +one. + +**Symptom.** A uniform scale rescales the world in view; a negative scale mirrors the view, so every +surface goes back-facing and the scene renders inside out. The camera's reported `forward`/`up` look +correct, which makes it hard. + +**Do instead.** A camera node must carry only rotation and translation, and no ancestor may be scaled. +Still silent. + +--- + +## 29. Hand-built `Skin` with mismatched joints and inverse-bind matrices + +**Mistake.** `skin.joints.add(n)` without a matching `skin.inverseBindMatrices.add(...)` (both are +plain mutable lists). + +**Symptom.** Extra inverse bind matrices are silently ignored and the mesh deforms wrongly. (Too few +throws a `RangeError`, so only the extra-matrices direction is silent.) + +**Do instead.** Keep the two lists parallel: one inverse bind matrix per joint (`Matrix4.identity()` +if the joint's rest pose is the mesh's model space). Imported skins are validated; hand-built ones are +not. Still silent. + +--- + +## 30. Cloning a mesh node whose skeleton is a sibling + +**Mistake.** `meshNode.clone()` when the skeleton lives outside the cloned subtree. + +**Symptom.** The clone renders collapsed or in bind-pose garbage. There is a `debugPrint`, but it says +only "Index path formation failed" and names neither the skin nor the consequence. + +**Do instead.** Clone the common ancestor of the mesh node and its skeleton, not the mesh node alone. +Still effectively silent. + +--- + +## 31. `updateInstanceTransforms(recomputeWinding: false)` with a mirroring edit + +**Mistake.** Editing an instance transform to a negative determinant while asking the engine to skip +the parity refresh. + +**Symptom.** Those instances render inside out (front faces culled, back faces lit). + +**Do instead.** Drop `recomputeWinding: false`, or keep every instance edit orientation-preserving +(no negative/mirrored scale). Still silent. + +--- + +## 32. Flipbook frame count vs atlas grid mismatch + +**Mistake.** A `FlipbookModule(frameCount: 16)` without `emitter.flipbookColumns = 4; +emitter.flipbookRows = 4`. + +**Symptom.** Particles sample the wrong atlas cells, or only the first cell; the effect animates but +shows the wrong art. + +**Do instead.** Set `flipbookColumns * flipbookRows` equal to the module's `frameCount`. Still silent. + +--- + +## 33. `LodComponent` blend bands overlapping + +**Mistake.** A `blendRange` larger than the gap between adjacent LOD thresholds. + +**Symptom.** An object sits permanently in the wrong cross-fade pair, dither-blending two levels that +should not blend, or skipping a level. + +**Do instead.** Keep `blendRange` smaller than the smallest gap between adjacent `screenSize` +thresholds. Still silent. + +--- + +## 34. `TextureAtlas` grid not matching its texture + +**Mistake.** `TextureAtlas(columns: 16, rows: 16, tileSize: 32, padding: 2, baseColor: eightBySix)` +where the grid does not match the image, or an out-of-range tile `index`. + +**Symptom.** Every UV points at the wrong tile. With the default `repeat` addressing, an out-of-range +index in release wraps to a different valid-looking tile rather than failing. + +**Do instead.** Make the grid parameters produce exactly the texture's dimensions +(`columns * (tileSize + 2*padding)` etc), keep tile indices in range, and set +`TextureSampling.maxMipmapLevels` so tiles do not bleed across the padding gutter at high mips. Still +silent. + +--- + +## 35. `useEnvironment` sky with no cube-radiance variant + +**Mistake.** `ShaderSkySource(fragmentShader: myShader, useEnvironment: true)` with +`radianceCubeFragmentShader` left null. + +**Symptom.** The sky contributes no image-based specular on any backend that builds the cube layout +(the default nearly everywhere), so the scene loses its reflections. + +**Do instead.** Supply `radianceCubeFragmentShader`, the entry built with +`FLUTTER_SCENE_RADIANCE_CUBE`. Debug builds warn about this at bind; release builds are silent, so do +not rely on the warning. + +--- + +## 36. `radianceCubeFragmentShader` that is not the cube build + +**Mistake.** `ShaderMaterial(fragmentShader: f, radianceCubeFragmentShader: f)` (the same shader +twice), or naming the non-cube entry as the cube twin. + +**Symptom.** The engine binds a cubemap into a shader whose sampler is a `sampler2D`: nothing on some +backends, garbage specular on others. + +**Do instead.** The cube variant must be the entry compiled with `FLUTTER_SCENE_RADIANCE_CUBE`, whose +`prefiltered_radiance` sampler is a `samplerCube`. Pass the distinct `...Cube` entry from your bundle. +(The engine can only catch the identical-shader case, and only in debug.) Still effectively silent. + +--- + +## 37. Reading `int`/`bool`/`uint` shader members through `setUniformBlockFromFloats` + +**Mistake.** `setUniformBlockFromFloats('FragInfo', [1.0, 0.5])` where the shader declares `int mode; +float amount;`. + +**Symptom.** The shader reads `mode` as the float bit pattern of `1.0` (a huge integer), so every +`if (mode == 1)` branch misses and the material takes its fallback path. The block size is correct, so +nothing complains. + +**Do instead.** Pack integer members with `ByteData.setInt32` at the member's offset, or use a `.fmat` +material whose `MaterialParameters` type-checks every assignment. Unenforceable at runtime. + +--- + +## 38. A custom fragment shader that tone-maps or writes straight alpha + +**Mistake.** Ending a `ShaderMaterial`/`ShaderSkySource`/`beforeTonemap` `PostEffect` fragment +shader with `frag_color = vec4(color, alpha)` (straight alpha) or `pow(color, vec3(1.0/2.2))` +(gamma-encoded). + +**Symptom.** Straight alpha gives edge halos and over-bright overlaps. sRGB output is tone-mapped and +EOTF-encoded a second time by the resolve pass, giving washed-out low-contrast color that looks like a +bad exposure. + +**Do instead.** Output linear HDR premultiplied by alpha. Exposure, tone mapping, and the display +encode are applied later by the full-screen resolve pass. When sampling an sRGB texture, linearize +first. `.fmat` materials get the premultiply for free. Unenforceable at runtime. + +--- + +## 39. `MaterialParameters.copyStateFrom` across a changed layout + +**Mistake.** Applying a re-realized material onto a live instance whose shader layout changed (an +editor hot reload where the `.fmat` gained or lost a parameter). + +**Symptom.** Every parameter reverts to its sidecar default while `assignedValues` still reports your +overrides, so the inspector shows the right numbers and the render shows the wrong ones. + +**Do instead.** `copyStateFrom` needs both sides to come from the same compiled shader entry; remap by +name through `updateFromMetadata` across a layout change instead. Still silent. + +--- + +## 40. Environment or widget textures with sub-255 alpha + +**Mistake.** Passing an equirect image carrying alpha below 255 (an unfilled sky dome, a masked +panorama) to `fromUIImages`/`fromEquirectImageAsset`. Or, for `WidgetTexture`/`WidgetComponent`, +simply using a widget with anti-aliased or translucent edges. + +**Symptom.** For environments, diffuse ambient comes out darker than the specular reflections of the +same environment, so objects look lit by two environments. For widget textures, dark halos around +anti-aliased text and rounded corners on the zero-copy path (correct on the web readback path, so it +reads as a platform quirk). + +**Do instead.** Use opaque (alpha 255) environment sources. The widget-alpha double-multiply is a +backend difference you cannot fully control from the API; keep widget content opaque where you can. +Still silent. + +--- + +## 41. Computing a camera or node right vector with `forward.cross(up)` or `+Z` orbit tangents + +**Mistake.** Writing `final right = forward.cross(up).normalized()`, or placing an orbit camera at `target + Vector3(d * sin(azimuth), y, d * cos(azimuth))` and using `Vector3(cos(azimuth), 0, -sin(azimuth))` as its horizontal right vector. + +**Symptom.** Horizontal camera strafing (`A`/`D`) and click-drag panning move in the exact opposite horizontal direction from expected, while vertical movement (`W`/`S` and vertical drag) works normally. + +**Do instead.** `flutter_scene`'s view space is left-handed (`+Z` forward into the screen, `+Y` up, `+X` right), while `package:vector_math`'s `cross()` is right-handed, so `forward.cross(up)` yields `-right` (screen-left). Likewise, when a camera sits on the `+Z` side of the origin looking along `-Z`, world `+X` is on the left of the screen (`right = Vector3(-cos(azimuth), 0, sin(azimuth))`). Read `camera.right`, `camera.horizontalRight`, `camera.horizontalForward`, or `node.right` directly, or compute `up.cross(forward).normalized()`. Prefer `OrbitCameraController`, `FollowCameraController`, or `FlyCameraController` over hand-rolled spherical camera math. + +--- + +## Now caught by the engine, in one place + +For quick reference, these traps became loud in 0.22.0. If you hit one you get an error, not silent +wrong pixels: + +- In-place edit of `localTransform`/`position`/`rotation`/`scale` -> throws in debug (#1). +- Degenerate camera and a frame that draws nothing -> asserts/prints once in debug (#24). +- `uploadVertexData` and `setCustomAttribute` length mismatches -> throw always (#18, #19). +- A `Mesh` whose primitive geometry is swapped -> recomputes bounds itself (#25). +- Faces of different surfaces overlapping in one plane -> printed once in debug with the fix (#2). +- An `AnimationClip` binding zero of its channels -> asserts in debug naming the wanted nodes. +- A web-backend bind to a shader uniform/texture name the shader does not declare -> throws (matches + native), instead of silently sampling whatever was bound last. +- Also fixed outright: `Node.clone()` sharing the original's matrix, the skinning joints texture being + too narrow for small joint counts, and `ParticleSystem.reset()` not restarting its random stream. diff --git a/.agents/skills/flutter_scene-idioms/references/what-exists.md b/.agents/skills/flutter_scene-idioms/references/what-exists.md new file mode 100644 index 00000000..520428f2 --- /dev/null +++ b/.agents/skills/flutter_scene-idioms/references/what-exists.md @@ -0,0 +1,550 @@ +# What exists in flutter_scene + +Complete public API inventory (package version 0.22.0). flutter_scene has lights, shadows, PBR +materials, instancing, LOD, skeletal animation, and a full post-processing stack. If you think a +feature is missing, it is almost certainly here under the name below. Look before you hand-roll. + +The public surface is the explicit `show` lists in `lib/scene.dart` (plus the separate barrels +`gpu.dart`, `fscene.dart`, `build_hooks.dart`, `physics.dart`, `audio.dart`). Nothing under +`lib/src` is public unless a barrel shows it. + +Import: +```dart +import 'package:flutter_scene/scene.dart'; +import 'package:vector_math/vector_math.dart' as vm; // NOT vector_math_64 +``` + +--- + +## Node and scene graph + +`Node` (`base class Node implements SceneGraph`). Construct `Node({String name = '', Matrix4? +localTransform, Mesh? mesh})`. A non-null `mesh` is wrapped in a `MeshComponent`. + +Transform API (0.22.0 added the component properties; older docs say only `localTransform` exists): + +| Member | Type | Notes | +| --- | --- | --- | +| `position` | `Vector3` get/set | Getter returns a copy; editing the copy in place throws in debug. Assign to move. | +| `rotation` | `Quaternion` get/set | Same copy rule. | +| `scale` | `Vector3` get/set | Same copy rule. | +| `localTransform` | `Matrix4` get/set | Getter returns the LIVE matrix; in-place edit throws in debug on next read. Assign a fresh matrix. | +| `mutateLocalTransform(void Function(Matrix4) edit)` | method | Edits in place AND dirties the cache. Correct raw-matrix path. | +| `globalTransform` | `Matrix4` get/set | Cached world transform; setter solves for the needed local. | +| `forward` | `Vector3` get | Unit world-space direction of the node's local `+Z` axis (the axis `lookAt` aims). | +| `right` | `Vector3` get | Unit world-space direction of the node's local `+X` axis (screen-right when viewed along `forward`). | +| `up` | `Vector3` get | Unit world-space direction of the node's local `+Y` axis. | +| `lookAt(target, {up})` | method | Orients the node's forward axis (local +Z) at a world-space target; preserves world position and scale. | +| `lookAtFrom(eye, target, {up})` | method | Positions at `eye` and aims +Z at `target` in one call (the imperative camera one-liner). | +| `Node.lookAtTransform(eye, target, {up})` | static -> `Matrix4` | The `lookAt` basis as a local transform, for `Node(localTransform:)` and declarative `transform:`. | + ++Z is the forward axis engine-wide (cameras, directional/spot lights, imported models), so the +lookAt helpers aim any of them. Compose a plain matrix with `vm.Matrix4.translation(v)`, +`vm.Matrix4.rotationY(a)`, `vm.Matrix4.compose(t, q, s)`. There is no `translate`/`rotateX` on Node. + +Hierarchy (`SceneGraph` is a mixin): `add`, `addAll`, `addMesh`, `remove`, `removeAll`. `add` throws +if the child already has a parent. `parent`, `children`, `detach()`, `getRoot()`, `getDepth()`. + +Lookup: `getChildByName(name, {excludeAnimationPlayers})`, `getChildByNamePath`, +`getChildByIndexPath`, static `getNamePath`/`getIndexPath`, `meshNodes`, `clone({recursive = true})`. + +Per-node flags: `visible` (true), `frustumCulled` (true), `layers` (`kRenderLayerDefault`, a 32-bit +mask, NOT inherited), `castsShadows` (true, not inherited), `shadowStatic` (false), `raycastable` +(true), `highlightColor` (`Vector4?`), `skin` (`Skin?`, set by importers). + +Bounds: `combinedLocalBounds`, `combinedWorldBounds`, `markBoundsDirty()`, `isVisibleTo(camera, +size)`. A `null` bounds means always-visible. + +Loading models (see Assets): `Node.fromGlbAsset`, `Node.fromGlbBytes`, `Node.fromGltfBytes`. + +Geometry readback: `extractMeshData({Matrix4? transform})` flattens the subtree to one `MeshData`. +Throws on instanced meshes, non-triangle primitives, caller-managed geometry, or an empty subtree. + +`Scene` (`base class Scene implements SceneGraph`, cannot be subclassed). See the render/lighting/ +post sections. `Mesh(geometry, material)`/`Mesh.primitives({primitives})`; `MeshPrimitive(geometry, +material)`; `Mesh.clone()` (shallow, shares geometry+material); `Mesh.localBounds`, +`Mesh.markLocalBoundsDirty()`. + +### Camera + +- `PerspectiveCamera({double fovRadiansY = 45 * degrees2Radians, Vector3? position /*(0,0,-5)*/, + Vector3? target /*(0,0,0)*/, Vector3? up /*(0,1,0)*/, double fovNear = 0.1, double fovFar = + 1000.0})`. Field names are `fovNear`/`fovFar`, NOT `near`/`far`. +- `PerspectiveCamera.framing(Aabb3 bounds, {direction, fovRadiansY, up, margin = 1.1})`. +- `PerspectiveProjection({fovRadiansY, near = 0.1, far = 1000.0})` and abstract `CameraProjection`, + `Camera`. Camera basis getters (`position`, `forward`, `up`, `right`, `horizontalForward`, `horizontalRight`) and helpers (`screenPointToRay`, `worldToScreen`, `getViewMatrix`, `getFrustum`). `right` and `horizontalRight` use `up.cross(forward)` so they always point toward the right edge of the view in the engine's left-handed view space. +- `OrthographicCamera({OrthographicProjection? projection, Vector3? position, target, up})` and + `OrthographicCamera.framing(bounds, {direction, up, margin})`. +- `OrthographicProjection({OrthographicSize size = OrthographicSize.height(10), zoom = 1.0, + Vector2? offset, near = 0.0, far = 1000.0})`, plus `.bounds(left:, right:, bottom:, top:)` and + `.matchingPerspective(fovRadiansY:, distance:)`. Sizes are FULL world extents, never half: + `OrthographicSize.height(h)`, `.width(w)`, `.contain(w, h)`, `.cover(w, h)`, `.stretch(w, h)`, + or `.pixelsPerUnit(ppu)` (logical pixels per world unit, so the scale holds as the view resizes). + Frame with `zoom`/`size`, not by moving the eye. `near` may be negative (isometric scenes that + extend behind the eye). +- Custom projections: implement `CameraProjection`. The renderer reads perspective versus + orthographic from the matrix, so effects follow. +- Node-driven: `CameraComponent({CameraProjection? projection, activateOnMount = false})` -> + `toCamera()` gives a `NodeCamera`. Camera node must not be scaled. +- Interactive cameras: `CameraController` components attached to the camera node. `OrbitCameraController` + (turntable around `target`; `orbitBy`/`dollyBy`/`panBy`/`frame`), `FlyCameraController` (WASD + drag + free flight; `moveVertical: false` = grounded first-person; `look`, `setMoveInput`, `forward`, `right`), `FollowCameraController` + (third-person easing behind `followTarget` node; `orbitBy`/`dollyBy`, `forward`, `right`). All ease with frame-rate + independent `smoothing` (settle seconds), clamp pitch short of vertical, and write the node via + `lookAtFrom`. Wire input with the `CameraControls({required controller, enabled, autofocus, child})` + widget (Focus + gestures + wheel); `SceneView` has no camera-input params by design. + +--- + +## Components + +`abstract class Component`. Lifecycle hooks (exact names): `onAttach`, `onLoad` (async), `onMount`, +`update(double deltaSeconds)` (NOT `onUpdate`), `fixedUpdate(double)`, `onUnmount`, `onDetach`, +`cloneFor(Node)`. Node side: `addComponent`, `removeComponent`, `getComponent()`, +`getComponents()`. +Game input lives in the separate `flutter_scene_input` package: `InputSystem`, `PlayerInput` +(`button`, `axis`, `vector`, `delta`, `fixed`, `contexts`, `overrides`), `ActionSet`, typed actions, +binding nodes (`ButtonBinding`, `DpadBinding`, `StickBinding`, `DeltaBinding`, `ChordBinding`, +`GatedBinding`), processors (`Deadzone`, `Scale`, `Invert`, `PerSecond`), `scene.attachInput`, +`InputListener`, `PointerLock`, `DefaultActions`, drivers, `listenForBinding`, `bindingDisplay`. + +Components tick root-first in tree order. For code that must run before every component each frame +(sampling input, applying network state), subclass `SceneTickListener` (`beforeTick(dt)`, +`beforeFixedStep(fixedDt)`, both optional) and register it with `scene.addTickListener`. + +| Component | Constructor/notes | +| --- | --- | +| `MeshComponent` | `MeshComponent(mesh)`; `mesh` get/set, `refreshMaterials()` | +| `InstancedMeshComponent` | `InstancedMeshComponent(instancedMesh)` | +| `LodComponent` | `LodComponent(List, {lodBias = 1.0, hysteresis = 0.1, blendRange = 0.0})`; extends MeshComponent | +| `CameraComponent`, `NodeCamera` | see Camera | +| `DirectionalLightComponent` | `(light)` aims down node local +Z; `.aimed(light, localDir)`; `.fromLightDirection(light)` | +| `PointLightComponent` | `(light)`; `worldPosition` | +| `SpotLightComponent` | `(light)`; `worldPosition`, `worldDirection` | +| `RectAreaLightComponent` | `(light)`; `worldPosition`, `worldRight`, `worldUp` | +| `EnvironmentVolumeComponent` | `({required settings, shape = box, extents, radius = 5.0, blendDistance = 1.0, priority = 0.0, weight = 1.0})` | +| `ReflectionProbeComponent` | `({extents = Vector3.all(5), blendDistance = 1.0, priority = 10.0, weight = 1.0, faceResolution = 128, captureOnActivate = true})`; parallax-corrected local reflections in the box; `requestCapture()` re-captures | +| `MaterialsVariantsComponent` | No public ctor. `MaterialsVariantsComponent.of(root)`/`.allOf(root)`, then `select(name)`, `variants`, `selected` | +| `SemanticsComponent` | `({label, value, hint, button, onTap, ... boundsOverride, properties})` | +| `WidgetComponent` | `({required Widget child, required Size size, pixelRatio = 1.0, worldHeight = 1.0, update = everyFrame, input = automatic, ...})`; `.bindOnly(...)` | +| `SplatComponent` | `SplatComponent(GaussianSplats)`; `opacity`, `splatScale`, `tint`, `shDegree`, `cropBox`, `cropMode` | +| `ParticleEmitterComponent` | `({required system, SpriteMaterial? material})`; `facing`, `flipbookColumns/Rows/Blend`, `paused` | +| `MeshParticleEmitterComponent` | `({required system, required List geometries, required material, facing = tumble})` | +| `TrailComponent` | `({width = 0.25, lifetime = 0.6, minVertexDistance = 0.05, maxPoints = 48, ...})`; `emitting`, `clear()` | + +`SemanticsComponent`, `SplatComponent`, particle emitters, `TrailComponent`, `WidgetInput`, +`MeshParticleFacing`, `LodLevel`, `EnvironmentVolumeShape` are all exported. + +--- + +## Geometry + +### Primitives (`primitives.dart`, all factory constructors, all `extends MeshGeometry`) + +| Class | Constructor | Facing/notes | +| --- | --- | --- | +| `CuboidGeometry` | `CuboidGeometry(Vector3 extents, {debugColors = false})` | positional extents; box `-extents/2..+extents/2`; debugColors off | +| `WedgeGeometry` | `WedgeGeometry(Vector3 size)` | triangular prism; base on `y=0`, not Y-centered | +| `PlaneGeometry` | `({width = 1.0, depth = 1.0, segmentsX = 1, segmentsZ = 1})` | XZ plane, faces +Y; no collisionShape | +| `SphereGeometry` | `({radius = 0.5, segments = 32, rings = 16})` | UV sphere | +| `CylinderGeometry` | `({bottomRadius = 0.5, topRadius = 0.5, height = 1.0, radialSegments = 32, heightSegments = 1, bottomCap = true, topCap = true})` | topRadius 0 = cone | +| `CapsuleGeometry` | `({radius = 0.5, height = 1.0, radialSegments = 32, capRings = 8})` | `height` is the mid-section; total Y = height + 2*radius | +| `TorusGeometry` | `({radius = 0.5, tubeRadius = 0.2, radialSegments = 32, tubularSegments = 16})` | XZ plane | +| `DiscGeometry` | `({radius = 0.5, segments = 32})` | XZ, faces +Y | +| `RingGeometry` | `({innerRadius = 0.25, outerRadius = 0.5, segments = 32})` | annulus, XZ, +Y | +| `IcosphereGeometry` | `({radius = 0.5, subdivisions = 2})` | subdivided icosahedron | + +Every primitive except `PlaneGeometry` has a `Shape get collisionShape`. + +### Swept/procedural (sweep a `ScenePath`; also `BezierPath`, `CatmullRomPath`, `PolylinePath`) + +- `RibbonGeometry(path, {width = 1.0, stations = 64, alignment = RibbonAlignment.ground, up, storage = fixed})`; `updatePath(path)`. `RibbonAlignment` = `ground` | `path`. +- `TubeGeometry(path, {radius = 0.5, radialSegments = 12, stations = 64, caps = true, storage})`. +- `ExtrudeGeometry(path, {required List profile, stations = 64, caps = true, storage})`. +- `PolylineGeometry(List points, {width = 8.0, widthMode = screenPixels, cap = butt, dash, perVertexWidth, perVertexColor})`. INERT until `updateForCamera(camera, viewportSize)` is called every frame. `PolylineWidthMode` = `screenPixels` | `worldUnits`; `PolylineCap` = `butt` | `round`; `DashPattern({dashLength, gapLength, cap})`. +- `LineSegmentsGeometry(LineSegmentData segments, {width = 0.01, normalOffset = 0.0})`. `extends Geometry`, GPU-expanded, no per-frame CPU work. For large independent-segment sets. +- `BillboardGeometry({capacity = 256})`. `floatsPerInstance = 14`; `BillboardFacing` = `spherical` | `axisLocked` | `velocityStretched`. + +### MeshGeometry, GeometryBuilder, MeshData + +`MeshGeometry.fromArrays({required Float32List positions, Float32List? normals, texCoords, +texCoords1, colors, tangents, List? indices, primitiveType = triangle, Aabb3? bounds, storage = +fixed, GeometryBufferArena? bufferArena, retainCpuData = true})`. Components per vertex: positions 3, +normals 3, texCoords/texCoords1 2, colors/tangents 4. Omitted normals on a triangle list are +generated. Omitted indices need a vertex count divisible by 3. `bounds` skips the position scan (it +must actually cover every vertex). + +`MeshGeometry.fromMeshData(MeshData data, {storage, bufferArena, retainCpuData})`. + +In-place update (require `GeometryStorage.updatable`, all take `{dirtyStart, dirtyCount}`): +`updatePositions`, `updateNormals`, `updateTexCoords`, `updateTexCoords1`, `updateColors`, +`updateTangents`. `rebuild({positions, normals, ...})` may change the vertex/index count. +`applyMeshData(data)`. `GeometryStorage` = `fixed` | `updatable`. + +`GeometryBuilder({deduplicate = true})`: `normal(v)`, `texCoord(v)`, `texCoord1(v)`, `color(v)`, +`tangent(v)`, `addVertex(Vector3) -> int`, `addTriangle(a, b, c)` (throws RangeError on bad index), +`packVertices()`, `build({storage, bufferArena, retainCpuData})`. Attribute setters are STICKY. +Calling `normal()` once disables generated normals for the whole mesh. + +`MeshData` (isolate-transferable, pure): `MeshData({required positions, required vertexCount, +normals, ..., customAttributes})`, `MeshData.build({required positions, ...})` (derives vertexCount, +generates normals). Derivations: `triangleCount`, `triangles`, `transformed(Matrix4)` (inverse +transpose for normals; a mirror flips winding), `toTriMeshShape()` (hollow static collider), +`toConvexHullShape()` (dynamic body), `unweld({attributes})`, `extractEdges({creaseAngleDegrees})`, +static `merge(parts)`. `MeshAttributeData(data, {components})`; `UnweldAttribute` = `centroid` | +`seed` | `triangleIndex` | `barycentric`; `LineSegmentData({positions, normals})`. + +`Geometry` base: `primitiveType`, `localBounds`, `localBoundingSphere`, `setLocalBounds(aabb, +sphere)`, `setVertices(BufferView, vertexCount)`, `setIndices(BufferView, indexType)`, +`setCustomAttribute(name, Float32List, {required components})` (1..4; depth and shadow passes read +it too, so attribute-driven displacement casts its shadow; materials that do not declare it ignore +it, and one that declares an attribute the mesh lacks reads zero), `uploadVertexData(ByteData, vertexCount, ByteData? indices, {indexType = +int16})`, `isReadable`, `extractMeshData()`, `setVertexShader`/`setVertexShaderName`, +`setVertexLayout(descriptor, {bindsModelTransform = true})`, `draw(pass, {instanceCount = 1})`. +`SkinnedGeometry`/`UnskinnedGeometry` subclasses. `GeometryBufferArena({blockSizeInBytes = 16MB})`. + +Vertex layout: unskinned 72 bytes/18 floats = position(3) normal(3) texture_coords(2) +texture_coords_1(2) color(4) tangent(4). Skinned 104 bytes/26 floats = + joints(4) weights(4). Do +not hand-pack; use `fromArrays`/`fromMeshData`/`GeometryBuilder`. + +### Instancing and LOD + +`InstancedMesh({required geometry, required material, cullInstances = false, +sortTransparentInstances = true})`: `instanceCount`, `addInstance(Matrix4, {Vector4? color}) -> int` +(clones the matrix), `setInstanceTransform(i, m)`, `updateInstanceTransforms(update, +{recomputeWinding = true})`, `setInstanceColor(i, color)`, `removeInstanceAt(i)`, `clearInstances()`. +Attach via `InstancedMeshComponent`. + +`LodLevel({required geometry, required material, required double screenSize})` (screenSize = projected +bounding-sphere diameter as a fraction of viewport height, descending, last is the cull floor). +Attach via `LodComponent`. Shadow/depth passes always draw level 0. + +--- + +## Materials and textures + +`Material` (abstract): `name`, `doubleSided` (false), `depthLayer` (0), `depthBias` (0.0), +`setFragmentShader`, `setFragmentShaderName(name, {cubeName})`, `setRadianceCubeFragmentShader`, +`isOpaque()`. `depthLayer` (-8..8) orders coplanar surfaces: a higher layer draws over a lower one +in the same plane at any distance (an overlay on a wall or road gets 1). `depthBias` is a world-unit +nudge toward the camera that buys fewer depth steps with distance; prefer `depthLayer`. `.fmat` +spells the layer `depth_layer:`. + +### UnlitMaterial + +`UnlitMaterial({TextureSource? colorTexture})`. `baseColorTexture` (field name differs from the ctor +arg), `baseColorTextureTransform`, `baseColorTextureTexCoord` (0), `alphaMode` (`opaque`; `mask` not +implemented, behaves as blend), `baseColorFactor` (white), `vertexColorWeight` (1.0). Fog applies. + +### PhysicallyBasedMaterial + +`PhysicallyBasedMaterial({baseColorTexture, metallicRoughnessTexture, normalTexture, emissiveTexture, +occlusionTexture, EnvironmentMap? environment})`. Every texture slot is a `TextureSource?` with a +`TextureTransform` and `TextureTexCoord`. + +Core: `baseColorFactor` (white), `vertexColorWeight` (1.0), `metallicFactor` (1.0), `roughnessFactor` +(1.0), `normalScale` (1.0), `emissiveFactor` (`Vector4.zero()`), `emissiveStrength` (1.0), +`occlusionStrength` (1.0), `environment` (null, falls back to `Scene.environment`), `alphaMode` +(opaque), `alphaCutoff` (0.5), `specularAntiAliasingVariance` (0.15), `specularAntiAliasingThreshold` +(0.2). + +Advanced KHR_materials_* (setting any flips onto an internal physical-variant shader; each has a +`Texture`): `specular` (1.0), `specularColor`, `ior` (1.5), `clearcoat` (0.0), +`clearcoatRoughness` (0.0), `clearcoatNormalScale`, `sheenColor` (zero), `sheenRoughness` (0.0), +`transmission` (0.0), `diffuseTransmission`, `diffuseTransmissionColor`, `thickness` (0.0), +`attenuationDistance` (inf), `attenuationColor`, `dispersion` (0.0), `iridescence` (0.0), +`iridescenceIor` (1.3), `iridescenceThicknessMinimum` (100.0), `iridescenceThicknessMaximum` (400.0), +`anisotropy` (0.0), `anisotropyRotation` (0.0). + +`isOpaque()` is false when `transmission > 0`, `alphaMode == blend`, or `baseColorFactor.a < 1.0`. + +`AlphaMode` = `opaque` | `mask` | `blend`. `TextureTransform({offset, scale, rotation})` (glTF +KHR_texture_transform order). + +### SpriteMaterial + +`SpriteMaterial({TextureSource? colorTexture})`: `colorTexture`, `tint` (white), `blendMode` +(`SpriteBlendMode.alpha` | `additive`), `softDepthFade` (0.0), `cameraNearFade` (0.0), `sampler`. +Always non-opaque, always cull none. + +### ShaderMaterial (raw GLSL escape hatch) + +`ShaderMaterial({gpu.Shader? fragmentShader, radianceCubeFragmentShader, vertexShader, +skinnedVertexShader, depthVertexShader, useEnvironment = false, cullingMode = backFace, windingOrder = +counterClockwise, isOpaqueOverride = true})`. `setVertexShader(shader, {variant = unskinned})`, +`vertexShaderFor(variant)`, `setUniformBlock(name, ByteData?, {stage = fragment})`, +`setUniformBlockFromFloats(name, List, {stage})`, `getUniformBlock`, `uniformBlockNames`, +`setTexture(name, texture, {sampler, stage})` (accepts `gpu.Texture`/`Texture2D`/`RenderTexture`), +`getTexture`, `textureNames`. `ShaderStage` = `vertex` | `fragment`; `MeshVariant` = `unskinned` | +`skinned` | `depth`. + +Fragment shaders MUST output linear HDR premultiplied by alpha (exposure, tone mapping, and the +display encode are applied later by the resolve pass). Same contract for `ShaderSkySource` and +`PostInsertion.beforeTonemap` effects. std140 packing is by hand. + +### .fmat (declarative, recommended over ShaderMaterial) + +`loadFmatMaterial(sourcePath) -> PreprocessedMaterial`, `loadFmatSky(...) -> PreprocessedSky`. +`PreprocessedMaterial`: `parameters` (`MaterialParameters`), `shadingModel`, `environment`. +`MaterialParameters` (typed, reflection-backed, throws on wrong type/name): `setFloat`, `setInt`, +`setVec2/3/4`, `setMat4`, `setColor(name, Color)`, `setTexture(name, gpu.Texture, {sampler})`, +`operator []=`, `parameterNames`, `samplerNames`, `hasUniformBlock`. + +### Textures + +`TextureSource` (interface): implementers are `Texture2D`, `RenderTexture`, `GpuTextureSource`. Every +built-in material slot takes a `TextureSource`, not a raw `gpu.Texture`. + +`Texture2D` (factories, generates a mip chain): `Texture2D.fromPixels(Uint8List, w, h, {content = +color, sampling})`, `fromImage(ui.Image, {...})`, `fromAsset(String, {content = color, sampling, +bundle})`. `TextureContent` = `color` (sRGB) | `data` (linear, e.g. metallic-roughness/AO) | `normal` +(vector-averaged). `TextureSampling({mipmaps = true, maxMipmapLevels, minFilter = linear, magFilter = +linear, mipFilter = linear, maxAnisotropy = 8, addressMode = repeat})`. + +`GpuTextureSource(gpu.Texture, {sampler})` adapts a raw texture. Barrel helpers: +`gpuTextureFromImage`, `gpuTextureFromAsset` (mipless, aliases on materials), `imageFromAsset`, +`imageFromBytes`. Cooked `.fstex`: `loadTexture(sourcePath, {package, bundle, sampling}) -> +TextureSource`, `releaseTexture`, `clearTextureCache`. + +Custom-shader GPU barrel (`package:flutter_scene/gpu.dart`): `Shader`, `ShaderLibrary`, +`loadShaderLibraryAsync` (use this, not `ShaderLibrary.fromAsset` which throws on web), +`resolveShaderBundleKey`, `Texture`, `SamplerOptions`, `MinMagFilter`, `MipFilter`, +`SamplerAddressMode`, `IndexType`, `VertexFormat`, `VertexStepMode`. + +--- + +## Lighting and environment + +Lights (all in `light.dart`, all fields mutable): + +- `DirectionalLight({direction /*(-0.3,-1,-0.2)*/, color, intensity = 3.0, priority = 0, castsShadow + = false, cacheStaticShadows = true, shadowFadeRange = 2.0, shadowSoftness = 0.08, shadowCascadeCount + = 4, shadowMaxDistance = 150.0, shadowCascadeSplitLambda = 0.6, shadowMapResolution = 1024, + shadowDepthBias = 0.02, shadowNormalBias = 0.02, shadowAmbientStrength = 0.0, shadowFilter = + rotatedPoisson, shadowCasterFaces = front, contactShadows = false, contactShadowDistance = 0.3, + angularRadius = 0.005})`. +- `PointLight({color, intensity = 1.0, range = 0.0, falloffExponent = 2.0, castsShadow = false, + shadowMapResolution = 512, shadowNear = 0.1, shadowDepthBias = 0.0, shadowNormalBias = 0.1, + shadowSoftness = 1.0, shadowCasterFaces = front})`. Shadows render six cube faces into the shared + atlas (a limited number of point casters per frame; the rest shade unshadowed). +- `SpotLight({color, intensity = 1.0, range = 0.0, falloffExponent = 2.0, direction /*(0,-1,0)*/, + innerConeAngle = 0.0, outerConeAngle = pi/4, castsShadow = false, ...})`. +- `RectAreaLight({color, intensity = 1.0, width = 1.0, height = 1.0, range = 0.0})`. Local XY plane, + emits along +Z, no shadows. +- `SunLight(SunSky source, {castsShadow = true, ...})` drives `Scene.directionalLight` from a sky. + +`Node.shadowCastingMode` (`ShadowCastingMode` = `on` (default) | `off` | `doubleSided` | +`shadowsOnly`) selects how a node's meshes cast; `Node.castsShadows` is a deprecated two-state +view of it. Every light also has `shadowCasterChannelMask` (8-bit), tested against +`Node.lightChannelMask`, to drop casters from one light's map. + +`ShadowCasterFaces` = `front` | `back` | `both`. `DirectionalShadowFilter` = `rotatedPoisson` | +`fixedPcf` | `pcss`. `ShadowCascade`, `Lighting` (per-draw state) are exported. + +Scene lighting: `Scene.directionalLight` (`DirectionalLight?`, null = IBL only; honors `direction`; +highest-priority one gets cascaded shadows), `Scene.sunLight`, `Scene.environment` (`EnvironmentMap?`, +null falls back to `EnvironmentMap.studio()`; for genuinely no IBL use `EnvironmentMap.empty()`), +`Scene.environmentIntensity` (1.0), `Scene.environmentTransform` (`Matrix3.identity()`), +`Scene.skybox` (`Skybox?`, null = transparent), `Scene.skyEnvironment`. + +### EnvironmentMap + +Carries a prefiltered specular radiance atlas AND SH-9 diffuse coefficients (one texture path, no +separate radiance/irradiance). Factories: `.empty()`, `.constantDiffuse(ambientRadiance)`, +`.fromGpuTextures({required prefilteredRadiance, diffuseSphericalHarmonics, diffuseShTexture})` (the +texture must already be prefiltered), `.fromUIImages({required radianceImage, ...})`, +`.fromEquirectHdr({required Float32List linearPixels, w, h, ...})`, +`.fromEquirectImageAsset({required assetPath, maxWidth = 4096, ...})` (auto-detects .hdr/.exr/LDR), +`.fromEquirectImageBytes(...)`, `.fromSky(SkySource, {...})`, `.studio()` (zero-config default). +Deprecated: `.fromAssets` (use `.fromEquirectImageAsset`). Env images must be equirect 2:1. +`prefilterEquirectRadiance` is exported. `Scene.loadEnvironment(assetPath, {showSkybox = true, +skyBlur = 0.0, intensity, exposure, rotationY, maxWidth = 4096, bundle})` is one-call setup. + +### Skybox/sky sources + +`Skybox(SkySource source, {intensity = 1.0})`. `SkySource` implementers: `EnvironmentSkySource({blurriness += 0.0})`, `ShaderSkySource({fragmentShader, fragmentShaderName, radianceCubeFragmentShader, +useEnvironment = false})`, `GradientSkySource({zenithColor, horizonColor, groundColor, sunDirection, +sunColor, sunSharpness = 400.0})`, `PhysicalSkySource({sunDirection, sunAngularRadius = 0.0175, +rayleighCoefficient = 2.0, mieCoefficient = 0.005, turbidity = 10.0, energy = 1.0, ...})`. +`SkyEnvironment(ShaderSkySource, {refresh = manual, interval, faceResolution = 128, equirectWidth = +512})`; `SkyEnvironmentRefresh` = `manual` | `interval` | `everyFrame`. + +### Exposure and tone mapping + +`Scene.exposure` (1.0; not 2.0), `Scene.toneMapping` (`ToneMappingMode.pbrNeutral`; also `aces`, +`reinhard`, `linear`, `agx`), `Scene.agxWhite` (16.29), `Scene.agxContrast` (1.25). Static +`Scene.physicalCameraExposure({required aperture, shutterSpeed, iso})` returns a multiplier to assign +to `exposure`. + +--- + +## Post-processing + +Every effect is a settings object on `Scene`, off by default, turned on with `enabled`. Environment +looks blend via `EnvironmentSettings` (snapshot/lerp of the whole look) and `EnvironmentVolume` / +`EnvironmentVolumeComponent` (spatial). + +| Scene field | Type | Key fields (default) | Requires | +| --- | --- | --- | --- | +| `ambientOcclusion` | `AmbientOcclusionSettings` | `method` (obscurance/`groundTruth`), `radius` (0.33), `intensity` (1.0), `power` (1.5), `bentNormals` (false), `halfResolution` (true), `indirectLight` (0.0 = SSGI), `specularMode` | perspective camera | +| `screenSpaceReflections` | `ScreenSpaceReflectionsSettings` | `intensity` (1.0), `maxDistance` (24.4), `thickness` (0.46), `stride` (9.0), `maxSteps` (90), `blur` (0.3), `debugView` | perspective camera | +| `fog` | `Fog` | `mode` (`FogMode.exponential`; also none/linear/exponentialSquared), `color`, `density` (0.02), `start`/`end`, needs both `enabled` AND non-none `mode` | any camera | +| `godRays` | `GodRaysSettings` | `intensity` (1.0), `density` (0.5), `anisotropy` (0.7), `stepCount` (24), `maxDistance` (200), `color` | shadow-casting DirectionalLight + perspective camera | +| `depthOfField` | `DepthOfField` | `focusDistance` (10.0), `fStop` (2.8), `focalLength`, `sensorHeight` (0.024), `bladeCount`, `quality` (low/medium/high) | perspective camera | +| `autoExposure` | `AutoExposureSettings` | `strength` (0.55), `compensation`, `minEv` (-4), `maxEv` (4), `speedUp` (3.0), `speedDown` (1.0); multiplies on top of `exposure` | none | +| `postProcess` | `PostProcessSettings` | see below | none | + +`AmbientOcclusionMethod` = `obscurance` (McGuire SAO) | `groundTruth` (GTAO). `SpecularAmbientOcclusionMode` += `none` | `simple` | `bentCone`. `SsrDebugView` = composite/reflectedUv/hitMask/normal/confidence/depth. + +`PostProcessSettings` (all sub-settings off by default; mutate the nested objects): +- `colorGrading` (`ColorGradingSettings`): `brightness` (1.0), `contrast` (1.0), `saturation` (1.0), + `temperature`, `tint`, `lift`/`gamma`/`gain`, `lut` (`ColorLut?`, applies after tone mapping, + independent of `enabled`), `lutBlend` (1.0). +- `chromaticAberration` (`intensity` 0.2), `vignette` (`intensity` 0.5, `radius` 0.75, `smoothness` + 0.5), `filmGrain` (`intensity` 0.3), `bloom` (`threshold` 1.0, `intensity` 0.15, `scatter` 0.7, + and `lensFlare`: `enabled` false, `intensity` 1.0, `ghostCount` 4, `ghostSpacing` 0.3, `haloRadius` + 0.35, `haloIntensity` 1.0, `chromaticAberration` 0.005; rides the bloom, needs bloom enabled). +- `customEffects` (`List`). + +`ColorLut.fromCubeString`/`.fromCubeAsset` (Adobe `.cube`, edge 2..64). + +Custom post: `PostEffect({gpu.Shader? fragmentShader, insertion = beforeTonemap, enabled = true, +useFrameInfo = false})`, added to `scene.postProcess.customEffects`. Engine binds `uniform sampler2D +input_color` at `in vec2 v_uv`. `PostInsertion` = `beforeTonemap` (linear HDR premultiplied) | +`afterTonemap` (display-referred). + +--- + +## Scene, render, and widgets + +`Scene()` (no args; calls `initializeStaticResources()`, needs a live Flutter GPU context). Methods: +`add`, `addAll`, `addMesh`, `remove`, `removeAll`, `update(dt)` (optional), `render(camera, canvas, +{viewport, pixelRatio})`, `renderViews(views, canvas, {region, pixelRatio})`, `warmUp(views, +{includeOffscreen})`, `raycast(ray, {maxDistance, layerMask, where, includeInvisible})`, +`raycastAll(...)`, `addRenderPass`/`removeRenderPass`, `captureRenderGraph({viewIndex, request, +timeout})`, `captureEnvironment({required position, faceResolution = 128, equirectWidth = 512, +layerMask})` -> `EnvironmentMap` (one-shot static capture; use `ReflectionProbeComponent` for a +node-anchored, parallax-corrected, auto-blended probe). Statics: `Scene.initializeStaticResources()`, +`Scene.preload({physicalMaterials = true, smaa = false})` (loads the shaders behind PBR extensions +and `ShadowCatcherMaterial`, and the SMAA tables, which otherwise load on first use), +`Scene.isReadyToRender`, `Scene.physicalCameraExposure`, `Scene.isAntiAliasingModeSupported`, +`Scene.effectiveAntiAliasingMode`. + +`Scene.antiAliasingMode` (`AntiAliasingMode.auto` -> msaa or fxaa; also `none`, `msaa`, `fxaa`, +`smaa`, `taa`), `Scene.smaa` (`SmaaSettings`: threshold, search steps, corner rounding), +`Scene.temporalAntiAliasing` (`TemporalAntiAliasingSettings`), +`Scene.renderScale` (1.0), `Scene.filterQuality` (`FilterQuality.medium`), `Scene.views` +(`List` for RenderTexture targets). + +`RenderView({required Camera camera, RenderTexture? target, Rect? viewport /*normalized 0..1, +ignored when target set*/, int layerMask = kRenderLayerAll, order = 0, AntiAliasingMode? +antiAliasingMode, double? renderScale, FilterQuality? filterQuality, List cullingPlanes})`. +`kRenderLayerDefault = 1`, `kRenderLayerAll = 0xFFFFFFFF`. + +`RenderTexture`, `RenderTextureSampling`, `RenderTextureUpdate`, `RenderTextureView(renderTexture, +{fit = contain, filterQuality = medium, followLayout = false})`. + +Widgets: +- `SceneView(Scene scene, {Camera? camera, SceneCameraBuilder? cameraBuilder, SceneViewsBuilder? + viewsBuilder, autoTick = true, pixelRatio, onTick, loading, loadingBuilder, revealMinDuration, + warmUp = false, children})`. App-owned scene; does not write scene properties. `camera`, + `cameraBuilder`, `viewsBuilder` are mutually exclusive. +- `SceneView.declarative({environment, environmentIntensity = 1.0, exposure = 1.0, toneMapping = + pbrNeutral, camera, cameraBuilder, viewsBuilder, children, ...})`. View-owned scene. +- `SceneViewsBuilder` is exported as of 0.22.0 (older docs list it as a trap; it is public now). +- Declarative widgets: `SceneNode`, `SceneMesh`, `SceneModel`, `SceneSubtree`, `SceneNodeHost`, + `SceneNodeController`, `SceneModelSource`, `AssetModelSource`, `MemoryModelSource`, + `SceneAnimationSpec`. `WidgetTexture`, `WidgetTextureController`, `WidgetUpdatePolicy`. `SceneScope`. +- Camera resolution precedence: `camera` -> `cameraBuilder(elapsed)` -> `scene.camera` (or first + mounted `CameraComponent`) -> default `PerspectiveCamera()`. + +`CustomRenderPass`, `RenderInput`, `RenderPassContext`, `RenderStage`, `TransientWriter`, +`NodeFilter`, `HighlightStyle`, render-graph capture types (`CapturedPass`, `CapturedResource`, +`CapturedDraw`, `CapturedSkip`, `RenderGraphCaptureRequest`, `RenderGraphCaptureResult`) are all +exported. + +Debugging and profiling (all exported, all on every backend): +- `scene.renderStats.latest` (`RenderFrameStats`): per-frame counters (draws, instances, vertices, + culled, batches, pipeline binds and builds) per view and per pass with CPU micros, plus + `history`. Always on; GPU times are null until the engine exposes timestamp queries. Each pass + also emits a `dart:developer` timeline event, so DevTools shows the frame. +- `Scene.debugAllowRenderGraphCapture = true` then `scene.captureRenderGraph()` captures one frame: + passes with data flow and thumbnails, and every draw (`pass.draws`) with node path, material, + shader names, pipeline id, counts, batch size, `batchBreak` reason, and the uniform blocks bound + for it (`block.decode(draw)` names the members once `ShaderReflection.loadAll()` has run). + `result.toJson()` / `RenderGraphCaptureResult.fromJson` round-trip a capture as JSON. +- `ShaderReflection.loadAll()` parses every loaded shader bundle; `ShaderReflection.infoFor(shader)` + gives inputs, uniform block layouts, texture bindings, and `sourceOf(shader)` the compiled MSL, + GLSL, or SPIR-V for any backend the bundle holds. `FmatCompileException.diagnostics` parses + compiler errors with line numbers; `shaderSourceWindow` renders the marked source around one. + +--- + +## Assets and animation + +Setup: `flutter pub add flutter_scene` then `dart run flutter_scene:init`. Enable Flutter GPU with +`flutter run --enable-flutter-gpu` (native only; nothing for web). Requires Flutter 3.47 stable+, NOT +master. Impeller is default; do not pass `--enable-impeller`. Never pass +`--enable-experiment=native-assets` (breaks the build on Dart 3.10+). + +Two model-loading paths, do not conflate: + +- Pipeline (preferred, needs the `buildScenes` hook): `loadScene(sourcePath, {package, bundle, + registry, onReload, applyStageTo}) -> Future`. `sourcePath` is the SOURCE path relative to + the package root (e.g. `'assets/level.glb'`), NOT a generated name. Companions: + `loadSceneSubtree`, `releaseScene`, `clearSceneTemplateCache`. +- Runtime glTF (no hook, parses every load): `Node.fromGlbAsset(assetPath)`, + `Node.fromGlbBytes(bytes)`, `Node.fromGltfBytes(gltfJson, {required resolveUri})`. Each synthesizes + a root node. + +Sibling loaders: `loadTexture` (`.fstex`), `loadFmatMaterial`/`loadFmatSky` (`.fmat`). + +Build hooks (`package:flutter_scene/build_hooks.dart`): `buildScenes({buildInput, buildOutput, +inputFilePaths, discoveryRoot = 'assets/', assetMode = generatedTree, compressTextures = false})`, +`buildMaterials({...})`, `buildTextures({..., required textures, contents})`, `buildEngineAssets`, +`buildTargetShaderBundleJson`. Outputs land in `flutter_scene_generated/` (never commit +`.fsceneb`/`.shaderbundle`/`.fmat.json`/`.fstex`). Removed 0.21.0: `legacyOnly`, +`dataAssetsIfAvailable`, `outputDirectory`. + +Animation (`Animation`, `AnimationClip`, `AnimationPlayer` exported): + +- Off a loaded model: `node.parsedAnimations`, `node.findAnimationByName(name)`, + `node.createAnimationClip(animation)`, `node.removeAnimationClip(clip)`. Clips start paused at t=0; + call `play()`. +- `AnimationClip`: `playbackTime` (assignment is seek), `playbackTimeScale` (1; negative reverses), + `weight` (0..1), `playing`, `loop`. `play()`, `pause()`, `stop()`, `replay()`, `gotoAndPlay(t)`, + `seek(t)`, `advance(dt)`, `rebind(newTarget, {animation})`. Channels bind by node NAME; channels + whose node is absent from the subtree are dropped (0.22.0 asserts in debug when ALL channels drop). +- `AnimationPlayer`: `createAnimationClip(animation, bindTarget)` (a second call with the same + `Animation.name` replaces), `getClipByName`, `rebind`, `update(dt)` (auto-driven per frame). +- `Animation({name, channels})`, `AnimationChannel`, `BindKey({required nodeName, property = + translation})`, `AnimationProperty` = `translation` | `rotation` | `scale`. +- Declarative: `SceneModel(assetPath, animations: [SceneAnimationSpec(name, {playing = true, loop = + true, weight = 1.0, speed = 1.0})])`. Note `SceneModel` loads via the runtime glTF path. + +The engine-agnostic scene-document core is a separate package `scene` (0.2.0), re-exported through +`package:flutter_scene/fscene.dart`. `flutter_scene_importer` and `flutter_gpu_shim` no longer exist +(folded in). Physics and audio are separate barrels (`physics.dart`, `audio.dart`). + +## Depth precision and z-fighting + +Defaults that keep depth precise: `Scene.reversedDepth` (true; reversed float depth on Metal and +browsers with clip control) and `Scene.fitNearPlane` (true; the near plane the passes rasterize with +is fitted to visible content each frame, the authored near is the floor, and the public projection is +untouched). `Scene.coplanarTieBreak` (false, experimental) gives unlayered materials and instances a +stable order where they overlap exactly. See `depth-and-layering.md`. + +Finding fights: `scene.probeDepthConflicts({camera, width = 960, height = 540, layerMask, minPixels += 4})` -> `Future` (`conflicts` of `DepthConflict` with `nodeA`, `nodeB`, +`pixelCount`, `bounds`, `distance`; `conflictPixelCount`; `describe()`), rendered from the camera. +`scene.findCoplanarOverlaps({camera})` -> `List` (`nodeA`, `nodeB`, `instanceA`, +`instanceB`, `area`, `separation`, `center`, `normal`, `hint`, `exact`, `describe()`), from geometry +with no rendering; debug builds run it once the scene settles and print a summary +(`scene.debugCheckCoplanarOverlaps = false` silences it). `DebugOverlay.depthConflicts` marks fights +live, and `SurfaceDebugChannel.depthGap` shows the gap two surfaces need at each pixel. + +## Debugging the surface + +`Scene.debug` (a `SceneDebugSettings`): `view` is a `DebugView` over a `SurfaceDebugChannel` (geometry attributes, resolved surface channels, physical fields, object and material identity colors, validation flags, a `custom` channel fed by `material.debug` in a `.fmat`), with `gain`, a scalar range, and a `DebugRangePolicy`; `split` compares the view against the lit image; `overlays` holds `DebugOverlay.wireframe` and `DebugOverlay.depthConflicts`. `Node.debugView` overrides or excludes a subtree. The material channels are compiled in only with flutter_scene's `debug_views` hook user-define (`hooks: user_defines: flutter_scene: debug_views: true`, plus `flutter_scene_debug_views: true` under the app's name when its hook builds materials or engine assets); without it every material draws through the fallback, which serves the geometry and identity channels. `DebugViewRegistry` lists every view by id for tools, and `Scene.debugViewId` selects one by id. Raw `ShaderMaterial`s opt in with `debugViews: true` after defining `FLUTTER_SCENE_DEBUG_VIEWS` and including `material_debug.glsl`; those that do not are drawn by a fallback that stripes the material channels. diff --git a/.agents/skills/flutter_scene-kit/SKILL.md b/.agents/skills/flutter_scene-kit/SKILL.md new file mode 100644 index 00000000..a931dfd1 --- /dev/null +++ b/.agents/skills/flutter_scene-kit/SKILL.md @@ -0,0 +1,157 @@ +--- +name: flutter_scene-kit +version: 8 +description: Build interactive 3D gameplay, character controllers, camera rigs, dynamic day/night cycles, water surfaces, audio, pooling, and debug overlays in flutter_scene. Use when creating game mechanics, camera controls, NPC behaviors, atmospheric environments, or diagnostic HUDs. +--- + +# Gameplay, camera, and atmosphere kit in flutter_scene + +Flutter Scene provides high-level gameplay components and ergonomic building blocks in `package:flutter_scene/kit.dart` so games and interactive experiences do not need to re-implement standard mechanics from scratch. + +When choosing components, consider existing engine alternatives: +- For physics-driven character navigation with collider capsules, wall sliding, and autostep, use `KinematicCharacterController` from `package:flutter_scene/physics.dart`. +- For interactive mouse/touch orbit cameras with inertia, use `OrbitCameraController` or `FollowCameraController`. +- For framing a standalone `PerspectiveCamera`, use `PerspectiveCamera.framing`. Use `BoundsFraming` when computing a transform for a `NodeCamera` mounted in the scene graph. +- When driving custom camera-relative movement or ground panning (WASD strafing, RTS/strategy camera drag), read `camera.horizontalRight` and `camera.horizontalForward` (or `camera.right` and `node.right`). Never write `forward.cross(up)`, which points screen-left in `flutter_scene`'s left-handed (`+Z` forward) view space and horizontally inverts `A`/`D` and drag controls. + +## Imports + +```dart +import 'package:flutter_scene/scene.dart'; +import 'package:flutter_scene/kit.dart'; +import 'package:vector_math/vector_math.dart' as vm; +``` + +## Camera rigs and smoothing + +### SpringArmComponent + +`SpringArmComponent` attaches to a target character node and mounts a camera node at the arm's socket. It casts rays against the scene hierarchy to prevent geometry clipping, smoothly pulling the camera inward when colliding with walls. + +Note on offsets: `targetOffset` is applied in world space from the character node's origin, and `socketOffset` acts in the camera socket's local plane along X (right) and Y (up). + +Leave the mounted camera's near plane at its default. Every pass rasterizes with a near plane fitted to visible content each frame, so a chase or follow camera gets the depth precision of a much larger near without clipping the character when the arm pulls in. Raising `near` by hand clips the character at close range and gains nothing; the fit falls back to the authored value only when the camera sits inside an item's bounds (a sky sphere), so draw skies with `scene.skybox`. + +```dart +final characterNode = Node(); +final cameraNode = Node(); +final cameraArm = SpringArmComponent( + targetLength: 5.0, + targetOffset: vm.Vector3(0, 1.6, 0), // Eye height + socketOffset: vm.Vector3(0.5, 0, 0), // Over-the-shoulder + enablePositionLag: true, + positionLagSpeed: 8.0, + cameraNode: cameraNode, +); + +characterNode.addComponent(cameraArm); +scene.root.add(characterNode); +scene.root.add(cameraNode); +``` + +### CameraShake + +`CameraShake` implements a trauma-decay model driven by deterministic simplex noise for organic multi-axis camera shake (explosions, footsteps, hits). + +```dart +final shake = CameraShake(decayRate: 1.2, frequency: 25.0); + +// Add trauma on hit +shake.addTrauma(0.6); + +// Inside game loop +final offset = shake.update(deltaSeconds); +cameraNode.localTransform = baseTransform * offset.toMatrix4(); +``` + +## Character movement and steering + +### ThirdPersonControllerComponent + +`ThirdPersonControllerComponent` handles kinematic movement, sprint multipliers, turn smoothing, ground snapping with raycasts, slope sliding, coyote time, and buffered jumps. Input expects `+Y` as forward in 3D. + +```dart +final playerNode = Node(); +final controller = ThirdPersonControllerComponent( + walkSpeed: 4.5, + runMultiplier: 1.8, + jumpVelocity: 7.0, + groundPlaneHeight: 0.0, // Optional fallback floor +); +playerNode.addComponent(controller); + +// Pass rotatesToMovement: false to move the node without turning it; read +// controller.yaw to drive a child mesh or animation from the heading instead. +// When using VirtualJoystick (where up is -Y in screen space), invert Y: +// controller.setMoveInput(vm.Vector2(joystickDir.x, -joystickDir.y), isRunning: isSprinting); +if (jumpPressed) controller.jump(); +``` + +### Autonomous Steering Behaviors + +`Steering` provides math helpers for NPC navigation, flocking, and crowd dynamics. + +```dart +// Seek target +final seekForce = Steering.seek(npcPos, npcVel, targetPos, maxSpeed: 4.0); + +// Arrive smoothly +final arriveForce = Steering.arrive(npcPos, npcVel, targetPos, slowingRadius: 3.0); + +// Flocking separation +final sepForce = Steering.separation(npcPos, npcVel, neighborPositions, desiredDistance: 1.5); +``` + +## Dynamic environments and atmosphere + +### DayNightCycleComponent + +`DayNightCycleComponent` moves the sun along a realistic solar arc given latitude and time of day, evaluating sun colors, intensities, and ambient lighting transitions. + +```dart +final sunLight = DirectionalLight(); +final sunNode = Node()..addComponent(DirectionalLightComponent(sunLight)); +scene.root.add(sunNode); + +final skyCycle = DayNightCycleComponent( + timeOfDay: 14.5, // 2:30 PM + timeSpeed: 0.1, // Progress 0.1 hours per second + latitude: 34.0, + sunLightNode: sunNode, +); +scene.root.addComponent(skyCycle); +``` + +### WaterSurfaceComponent + +`WaterSurfaceComponent` evaluates multi-harmonic Gerstner trochoidal waves for water surfaces and floating buoyancy queries. + +```dart +final water = WaterSurfaceComponent(); +final surface = water.evaluateAt(vm.Vector2(playerPos.x, playerPos.z)); +final waterHeight = surface.displacement.y; +final waterNormal = surface.normal; +``` + +## Immediate-mode debug visualization + +`DebugDraw` provides static immediate-mode line, ray, box, sphere, axis, and physics collider drawing utilities for physics debugging and AI visualizers. + +```dart +DebugDraw.line(startPos, endPos, color: vm.Vector4(1, 0, 0, 1)); +DebugDraw.box(aabb, color: vm.Vector4(0, 1, 0, 1)); +DebugDraw.sphere(center, 1.0, color: vm.Vector4(0, 0, 1, 1)); +DebugDraw.axes(node.globalTransform, size: 2.0); + +// Every collider under a node, posed the way the simulation sees it. +// Triggers draw in triggerColor. DebugDraw.shape draws a single posed Shape. +DebugDraw.colliders(scene.root); + +// Render debug lines: one updatable geometry, rebuilt in place each frame. +final debugGeometry = DebugDraw.createGeometry(); +debugNode.mesh = Mesh(debugGeometry, UnlitMaterial()); +// Per frame, after the DebugDraw calls: +DebugDraw.flushInto(debugGeometry); +``` + +`DebugDraw.flushMesh()` builds a new geometry per call; it suits a one-off capture, not a per-frame loop. diff --git a/.agents/skills/flutter_scene-looks/SKILL.md b/.agents/skills/flutter_scene-looks/SKILL.md new file mode 100644 index 00000000..bbce45d7 --- /dev/null +++ b/.agents/skills/flutter_scene-looks/SKILL.md @@ -0,0 +1,166 @@ +--- +name: flutter_scene-looks +version: 5 +description: Give a flutter_scene render a deliberate, polished look. Use this whenever a scene looks flat, dull, or washed out, or whenever the ask is to make it look good, because a good look is lighting plus post-processing, not geometry. Ships copy-paste EnvironmentSettings presets that configure the whole stack coherently. +--- + +# Making flutter_scene look good + +The single most common reason a flutter_scene render looks amateur is that the post-processing stack was left at defaults. flutter_scene has a deep lit-and-post pipeline (image-based lighting, tone mapping, bloom, lens flares, ambient occlusion, screen-space reflections, fog, god rays, depth of field, color grading, vignette, film grain). Out of the box almost all of it is off, so a bare scene is technically correct and visually flat. + +**The insight: a polished look is lighting and post-processing, not geometry.** Better meshes will not fix a flat render. Do not spend effort on modeling detail when the scene reads dull; spend it on the look. And a look is a *coherent* set of choices, not twelve knobs turned independently. Turning bloom, AO, SSR, fog, grain, and grading up one at a time, each by feel, lands in muddy incoherent territory. Pick one deliberate preset and paste it whole. + +## Apply a look in one line + +Every scene-wide look field lives on one aggregate value, `EnvironmentSettings`, assigned through a single setter: + +```dart +scene.environmentSettings = EnvironmentSettings(/* fields below */); +``` + +That one assignment configures tone mapping, exposure, image-based-lighting intensity, and the entire post stack, **including** fog, god rays, depth of field, and auto exposure. You do not need to touch `scene.fog`, `scene.godRays`, `scene.depthOfField`, or `scene.autoExposure` separately; those are the live per-effect objects, but `EnvironmentSettings` carries all of their fields and applies them for you. Every effect is off by default, so a preset only names the fields it turns on. + +`EnvironmentSettings` is also a blendable snapshot. Read the current look with `scene.environmentSettings`, and cross-fade two looks with `EnvironmentSettings.lerp(a, b, t)` driven from an animation. That is how you transition day to night or ramp an effect in. + +## Lights are separate, and still matter + +`EnvironmentSettings` covers image-based lighting (the `environment` map) and the whole post stack, but **direct lights are not part of it.** Shadows, god rays, and any strong key light come from the scene's lights, set separately: + +```dart +scene.directionalLight = DirectionalLight( + direction: vm.Vector3(-0.4, -1.0, -0.3), + intensity: 4.0, + castsShadow: true, // shadows are off until you ask +); +``` + +An unset `scene.environment` still resolves to a default studio IBL, so a `PhysicallyBasedMaterial` is always lit. But a scene with no direct light is soft and shadowless. God rays require a shadow-casting `DirectionalLight`; shadows require `castsShadow: true` on the light. + +## The four looks + +Paste one whole. Each is a real `EnvironmentSettings` literal; import `package:vector_math/vector_math.dart as vm` for the `Vector3` color fields. AO, SSR, god rays, and depth of field work with perspective and orthographic cameras. + +### showcase + +Clean, bright product-viz beauty. Punchy tone mapping, soft bloom on highlights, grounded contact occlusion, and real reflections. The default reach-for-it look. + +```dart +scene.environmentSettings = EnvironmentSettings( + toneMapping: ToneMappingMode.aces, + exposure: 1.0, + bloomEnabled: true, + bloomThreshold: 1.1, + bloomIntensity: 0.2, + bloomScatter: 0.7, + ambientOcclusionEnabled: true, + ambientOcclusionMethod: AmbientOcclusionMethod.groundTruth, + ambientOcclusionBentNormals: true, + ambientOcclusionSpecularMode: SpecularAmbientOcclusionMode.bentCone, + ambientOcclusionIntensity: 1.0, + screenSpaceReflectionsEnabled: true, + screenSpaceReflectionsIntensity: 1.0, + vignetteEnabled: true, + vignetteIntensity: 0.25, +); +``` + +### stylized + +Vivid and graphic. Saturated, slightly warm, glowing, flatter shading (no heavy occlusion or reflections). For playful or illustrative scenes. + +```dart +scene.environmentSettings = EnvironmentSettings( + toneMapping: ToneMappingMode.aces, + colorGradingEnabled: true, + saturation: 1.25, + contrast: 1.1, + brightness: 1.05, + temperature: 0.1, + bloomEnabled: true, + bloomThreshold: 0.9, + bloomIntensity: 0.28, + bloomScatter: 0.8, + vignetteEnabled: true, + vignetteIntensity: 0.2, +); +``` + +### moody + +Dark, cinematic, atmospheric. Lower exposure, cool graded, foggy, heavy vignette, subtle grain and aberration, deep occlusion. God rays if the scene has a shadow-casting sun. Use a cool horizon-colored fog. + +```dart +scene.environmentSettings = EnvironmentSettings( + toneMapping: ToneMappingMode.aces, + exposure: 0.8, + colorGradingEnabled: true, + contrast: 1.15, + saturation: 0.9, + temperature: -0.1, + fogEnabled: true, + fogMode: FogMode.exponential, + fogColor: vm.Vector3(0.05, 0.06, 0.09), + fogDensity: 0.03, + ambientOcclusionEnabled: true, + ambientOcclusionMethod: AmbientOcclusionMethod.groundTruth, + ambientOcclusionIntensity: 1.2, + ambientOcclusionPower: 1.8, + vignetteEnabled: true, + vignetteIntensity: 0.6, + vignetteRadius: 0.6, + filmGrainEnabled: true, + filmGrainIntensity: 0.25, + chromaticAberrationEnabled: true, + chromaticAberrationIntensity: 0.15, + godRaysEnabled: true, // needs scene.directionalLight with castsShadow: true + godRaysIntensity: 1.0, + godRaysDensity: 0.6, + godRaysColor: vm.Vector3(1.0, 0.95, 0.85), +); +``` + +### clean + +Neutral and honest. Minimal post, no grading, no bloom, no vignette, just correct tone mapping and gentle grounding occlusion. The right look for an editor, an inspector, a UI-embedded viewer, or anywhere you want an accurate read of the actual material. + +```dart +scene.environmentSettings = EnvironmentSettings( + toneMapping: ToneMappingMode.pbrNeutral, // the engine default + exposure: 1.0, + ambientOcclusionEnabled: true, + ambientOcclusionIntensity: 0.8, + ambientOcclusionHalfResolution: true, +); +``` + +## Tuning from a preset + +Start from the nearest look, then move one field at a time. + +- **Too dim or too bright overall.** Change `exposure` (default 1.0), not per-light intensity. Or turn on `autoExposureEnabled: true` to let the scene meter itself. +- **Highlights not glowing.** Lower `bloomThreshold` toward 1.0 or below, or raise `bloomIntensity`. `bloomScatter` widens the glow. +- **Want a lens flare off a bright source.** Turn on `lensFlareEnabled` (needs `bloomEnabled`). Keep `lensFlareIntensity` modest and drop `lensFlareHaloIntensity` first if the flare washes the frame; the halo is the broad wash, the ghosts are the crisp chain. +- **Reads flat and ungrounded.** `ambientOcclusionEnabled: true`. Use `AmbientOcclusionMethod.groundTruth` for quality, keep `ambientOcclusionHalfResolution: true` for cost. +- **Colors feel wrong.** Turn on `colorGradingEnabled` and reach for `saturation`, `contrast`, `temperature`, `tint` before anything else. +- **Wrong tone-map feel.** `ToneMappingMode.aces` is contrasty and filmic, `pbrNeutral` (default) preserves hue and saturation, `agx` is the most neutral highlight rolloff. Do not reintroduce an `exposure: 2.0` hack; that was an artifact of an older renderer. + +## Micro-surface metrics and anti-waxiness tuning + +When tuning procedural materials or custom shaders, use surface metrics and exposure discipline to eliminate waxiness, plastic reads, or harsh aliasing: + +- **Surface reads like smooth plastic.** Increase high-frequency micro-scale variation. Low 1-pixel luminance gradients `(|dL/dx| + |dL/dy|) / 2` indicate untextured or overly smooth surfaces. +- **Blotchy macro clouds.** Balance high-frequency and low-frequency energy ratios (`hf/lf`). High variance with low fine detail indicates large-scale noise patches without adequate surface grain. +- **Harsh normal-map glitter or torn noise.** Check terminator crossing behavior under low grazing light angles (sun low on the horizon). Over-amplified normal maps flip adjacent pixels between full light and full shadow. +- **Colors look washed out in bright areas.** Tone mapping curves compress channel differences near the shoulder. A surface reading high brightness with low saturation is often over-exposed rather than under-pigmented; reduce exposure to restore natural material saturation. + +## Look tools that are not on EnvironmentSettings + +Anti-aliasing, reflection probes, and planar reflectors affect the look but are set outside `EnvironmentSettings`, so a preset does not turn them on. + +- **Anti-aliasing** is a `Scene` field. The default `AntiAliasingMode.auto` already picks `msaa` where the backend supports it and `fxaa` otherwise, so edges are handled. Reach for `scene.antiAliasingMode = AntiAliasingMode.smaa` when `fxaa` looks mushy (it blurs texture detail) and `msaa` is unavailable; SMAA keeps edges clean without the blur, at ~3x fxaa cost. +- **Reflection probes** capture true local reflections that SSR cannot, because SSR only reflects what is on screen. Attach a `ReflectionProbeComponent` to a node placed at the reflective spot (a room, a mirror ball); it captures the surroundings into a parallax-corrected box and blends with the environment. The capture renders the scene six times, so it happens once on activate (or on an explicit `requestCapture()`), never per frame. +- **Planar reflectors** render a true per-frame mirror for one flat surface (a mirror, a glossy floor), which neither SSR nor a probe can produce. Attach a `PlanarReflectorComponent` to the mirror node and give the surface a `.fmat` material declaring `engine_inputs: [ planar_reflection ]`; the component renders the scene once more per frame from the reflected camera and the material samples it with `GetPlanarReflection()`. That extra scene render is the cost, so bound it with `resolutionScale` (default 0.5) and `layerMask`. See `references/looks.md` for all three. + +## Cost + +The post stack is not free. AO, SSR, depth of field, and god rays each add screen-space passes, and mobile and web are the budget. Keep `ambientOcclusionHalfResolution: true`, prefer `DepthOfFieldQuality.low` on mobile, and do not stack SSR plus god rays plus depth of field on a low-end target without profiling. The `clean` look is nearly free; `moody` is the heaviest. See `references/looks.md` for the full per-effect knob reference, defaults, and cost notes. diff --git a/.agents/skills/flutter_scene-looks/references/looks.md b/.agents/skills/flutter_scene-looks/references/looks.md new file mode 100644 index 00000000..86823c8d --- /dev/null +++ b/.agents/skills/flutter_scene-looks/references/looks.md @@ -0,0 +1,290 @@ +# The look stack, knob by knob + +Every field here is a constructor argument on `EnvironmentSettings` (`lib/src/environment_settings.dart`). Assigning `scene.environmentSettings = EnvironmentSettings(...)` applies all of them at once. Defaults are the constructor defaults; a preset only names what it changes. Direct lights (`scene.directionalLight` and friends) are separate and not covered by `EnvironmentSettings`. + +Color fields are `vm.Vector3` (import `package:vector_math/vector_math.dart as vm`). + +--- + +## Base look + +| Field | Default | What it does | +| --- | --- | --- | +| `toneMapping` | `ToneMappingMode.pbrNeutral` | HDR to display operator. `pbrNeutral` preserves hue/saturation, `aces` is contrasty and filmic, `agx` has the gentlest highlight rolloff, `reinhard`/`linear` are simpler references. | +| `exposure` | `1.0` | Linear scene exposure multiplier. The one knob for overall brightness. Do not use `2.0`; that was an old-renderer hack. | +| `environmentIntensity` | `1.0` | Scales image-based lighting (the environment map) contribution. | +| `agxWhite` | `16.29` | AgX white point, only meaningful with `ToneMappingMode.agx`. | +| `agxContrast` | `1.25` | AgX contrast, only with `agx`. | + +`environment` (an `EnvironmentMap?`) and the sky fields are also on `EnvironmentSettings`, but building environment maps is the domain of the idioms skill; a null environment resolves to a default studio IBL. + +## Auto exposure (`autoExposure*`) + +Meters the frame and multiplies on top of `exposure`. Off by default. + +| Field | Default | What it does | +| --- | --- | --- | +| `autoExposureEnabled` | `false` | Turn metering on. | +| `autoExposureStrength` | `0.55` | How fully it drives toward the metered target (0 = none, 1 = full). | +| `autoExposureCompensation` | `0.0` | EV bias applied after metering. | +| `autoExposureMinEv` / `autoExposureMaxEv` | `-4.0` / `4.0` | Clamp range for the adaptation. | +| `autoExposureSpeedUp` / `autoExposureSpeedDown` | `3.0` / `1.0` | Adaptation rate brightening vs darkening. | + +## Bloom (`bloom*`) + +Blooms bright pixels. Off by default. The cheapest way to make a scene feel lit rather than rendered. + +| Field | Default | What it does | +| --- | --- | --- | +| `bloomEnabled` | `false` | Turn bloom on. | +| `bloomThreshold` | `1.0` | Brightness above which a pixel blooms. Lower for more glow. | +| `bloomIntensity` | `0.15` | Strength of the added glow. | +| `bloomScatter` | `0.7` | Spread of the glow, wider values feel dreamier. | + +### Lens flares (`lensFlare*`) + +Ghost chains and a halo ring off the bloom pyramid, for bright emissive sources and sun disks. Rides the bloom chain, so `bloomEnabled` must be on and the flare scales with `bloomIntensity`. Off by default. A little goes a long way; a strong source with high intensity/halo washes the frame. + +| Field | Default | What it does | +| --- | --- | --- | +| `lensFlareEnabled` | `false` | Turn flares on. Needs `bloomEnabled`. | +| `lensFlareIntensity` | `1.0` | Strength of the flare features relative to the bloom. | +| `lensFlareGhostCount` | `4` | Internal-reflection ghosts along the line through the screen center (clamped to 8 at render). | +| `lensFlareGhostSpacing` | `0.3` | Spacing between ghosts, as a fraction of the distance to the center. | +| `lensFlareHaloRadius` | `0.35` | Halo ring radius in screen UV units. | +| `lensFlareHaloIntensity` | `1.0` | Halo strength relative to the ghosts. `0` disables the halo. The halo is what washes the whole frame, so tame it first. | +| `lensFlareChromaticAberration` | `0.005` | Radial color dispersion of the flare features. | + +## Color grading (`colorGrading*`) + +Post-tone-map color shaping. Off by default. Reach here to change the *mood* of the color rather than the exposure. + +| Field | Default | What it does | +| --- | --- | --- | +| `colorGradingEnabled` | `false` | Turn grading on. | +| `brightness` | `1.0` | Multiplicative brightness. | +| `contrast` | `1.0` | Contrast around mid-gray. | +| `saturation` | `1.0` | Color saturation. Above 1 is vivid, below 1 desaturates toward gray. | +| `temperature` | `0.0` | Warm (positive) to cool (negative) white balance. | +| `tint` | `0.0` | Green to magenta balance. | +| `lift` / `gamma` / `gain` | `Vector3(0)` / `Vector3(1)` / `Vector3(1)` | Per-channel shadow/mid/highlight color control (lift-gamma-gain). | +| `colorGradingLut` | `null` | A `ColorLut` (from a `.cube` file) applied after tone mapping. Independent of `colorGradingEnabled`. | +| `colorGradingLutBlend` | `1.0` | LUT mix amount. | + +## Vignette (`vignette*`) + +Darkens the frame edges. Off by default. Small amounts read as cinematic; large amounts as a peephole. + +| Field | Default | What it does | +| --- | --- | --- | +| `vignetteEnabled` | `false` | Turn vignette on. | +| `vignetteIntensity` | `0.5` | Darkening strength at the edge. | +| `vignetteRadius` | `0.75` | How far in the darkening starts (smaller = tighter, more closed-in). | +| `vignetteSmoothness` | `0.5` | Falloff softness of the edge. | + +## Chromatic aberration (`chromaticAberration*`) + +Splits color channels toward the edges. Off by default. A touch adds a lens feel; too much looks broken. + +| Field | Default | What it does | +| --- | --- | --- | +| `chromaticAberrationEnabled` | `false` | Turn it on. | +| `chromaticAberrationIntensity` | `0.2` | Channel-separation strength. | + +## Film grain (`filmGrain*`) + +Adds animated grain. Off by default. Sells a moody or analog look and hides banding in dark gradients. + +| Field | Default | What it does | +| --- | --- | --- | +| `filmGrainEnabled` | `false` | Turn it on. | +| `filmGrainIntensity` | `0.3` | Grain strength. | + +## Ambient occlusion (`ambientOcclusion*`) + +Screen-space contact darkening. Off by default. Requires a `PerspectiveCamera`. The single biggest upgrade for "grounded" vs "floating". Half-resolution by default to stay affordable. + +| Field | Default | What it does | +| --- | --- | --- | +| `ambientOcclusionEnabled` | `false` | Turn AO on. | +| `ambientOcclusionMethod` | `AmbientOcclusionMethod.obscurance` | `obscurance` is the cheap default; `groundTruth` (GTAO) is higher quality and needed for bent normals. | +| `ambientOcclusionIntensity` | `1.0` | Darkening strength. | +| `ambientOcclusionRadius` | `0.33` | World-space sampling radius. | +| `ambientOcclusionPower` | `1.5` | Contrast of the occlusion curve. | +| `ambientOcclusionBias` | `0.07` | Self-occlusion rejection. | +| `ambientOcclusionBentNormals` | `false` | Compute bent normals (needs `groundTruth`); improves indirect lighting direction and enables `bentCone` specular AO. | +| `ambientOcclusionSpecularMode` | `SpecularAmbientOcclusionMode.none` | `simple` occludes reflections cheaply; `bentCone` is directional and needs bent normals. | +| `ambientOcclusionHalfResolution` | `true` | Compute at half res. Keep on unless AO edges look too coarse. | +| `ambientOcclusionIndirectLight` | `0.0` | Above 0 turns on screen-space global illumination (SSGI) bounce; expensive. Its radiance history reprojects, so the bounce stays put under camera motion (object motion still lags). | +| `ambientOcclusionMultiBounce` | `0.0` | Approximate multi-bounce darkening recovery. | +| `ambientOcclusionSampleCount` | `16` | Samples for the `obscurance` method. | +| `ambientOcclusionSliceCount` / `ambientOcclusionStepsPerSlice` | `3` / `3` | GTAO slice sampling. | +| `ambientOcclusionDetail` | `0.5` | Fine-detail term weight (obscurance). | +| `ambientOcclusionHorizonAngle` | `0.06` | Horizon rejection angle. | +| `ambientOcclusionThickness` / `ambientOcclusionThicknessHeuristic` | `0.5` / `0.004` | Depth thickness assumptions for occlusion. | +| `ambientOcclusionDirectLightAffect` | `0.0` | How much AO also dims direct light. | +| `ambientOcclusionVisibilityBitmask` | `false` | Bitmask visibility estimator. | +| `ambientOcclusionDepthMipChain` | `false` | Build a depth mip chain for wide-radius sampling. | + +## Screen-space reflections (`screenSpaceReflections*`) + +Reflects on-screen geometry. Off by default. Requires a `PerspectiveCamera`. Adds realism to floors, water, and glossy surfaces, but only reflects what is on screen. + +| Field | Default | What it does | +| --- | --- | --- | +| `screenSpaceReflectionsEnabled` | `false` | Turn SSR on. | +| `screenSpaceReflectionsIntensity` | `1.0` | Reflection strength. | +| `screenSpaceReflectionsMaxDistance` | `24.4` | Max world-space ray distance. | +| `screenSpaceReflectionsThickness` | `0.46` | Assumed surface thickness for hit tests. | +| `screenSpaceReflectionsStride` | `9.0` | March step size (larger = faster, coarser). | +| `screenSpaceReflectionsMaxSteps` | `90` | Ray-march step budget. | +| `screenSpaceReflectionsBlur` | `0.3` | Roughness-based blur of the reflection. | +| `screenSpaceReflectionsDistanceFadeStart` | `0.0` | Where reflections start fading with distance. | +| `screenSpaceReflectionsResolutionScale` | `1.0` | Compute resolution scale; drop below 1 to save cost. | + +## Fog (`fog*`) + +Distance and height fog, evaluated in linear HDR before tone mapping. Off by default. Needs both `fogEnabled` and a non-`none` `fogMode`. Applies to lit and unlit materials; the skybox is left unfogged, so set `fogColor` to your horizon color for distant blending. + +| Field | Default | What it does | +| --- | --- | --- | +| `fogEnabled` | `false` | Turn fog on. | +| `fogMode` | `FogMode.exponential` | `none`, `linear`, `exponential`, `exponentialSquared`. Must be non-`none` to render. | +| `fogColor` | `Vector3(0.6, 0.7, 0.8)` | Fog tint. Match your sky/horizon. | +| `fogDensity` | `0.02` | Density for the exponential modes. | +| `fogStart` / `fogEnd` | `0.0` / `200.0` | Near/far bounds for `linear` mode. | +| `fogSkyColorInfluence` | `0.0` | Blend fog color toward the sky color. | +| `fogMaxOpacity` | `1.0` | Cap on how opaque fog gets. | +| `fogHeight` / `fogHeightFalloff` | `0.0` / `0.0` | Height-fog band and falloff. | +| `fogSunInScatter` / `fogSunInScatterExponent` | `0.0` / `8.0` | Sun in-scatter glow through the fog. | +| `fogCutoffDistance` | `0.0` | Distance beyond which fog stops accumulating. | + +## God rays (`godRays*`) + +Volumetric light shafts. Off by default. Requires a shadow-casting `DirectionalLight` and a `PerspectiveCamera`; without a shadow-casting sun there is nothing to shaft. + +| Field | Default | What it does | +| --- | --- | --- | +| `godRaysEnabled` | `false` | Turn shafts on. | +| `godRaysIntensity` | `1.0` | Shaft strength. | +| `godRaysDensity` | `0.5` | Medium density the light scatters through. | +| `godRaysAnisotropy` | `0.7` | Forward-scatter bias (higher = tighter shafts toward the sun). | +| `godRaysStepCount` | `24` | March steps; higher is smoother and costlier. | +| `godRaysMaxDistance` | `200.0` | Max shaft distance. | +| `godRaysJitter` | `1.0` | Dither to hide banding. | +| `godRaysColor` | `Vector3(1)` | Shaft tint. | + +## Depth of field (`depthOfField*`) + +Physically parameterized lens blur. Off by default. Requires a `PerspectiveCamera`. Great for a hero shot, wasteful for a full interactive scene. + +| Field | Default | What it does | +| --- | --- | --- | +| `depthOfFieldEnabled` | `false` | Turn DoF on. | +| `depthOfFieldFocusDistance` | `10.0` | World distance in sharp focus. | +| `depthOfFieldFStop` | `2.8` | Aperture; lower = shallower focus, more blur. | +| `depthOfFieldFocalLength` | `0.0` | Lens focal length (0 derives from FOV). | +| `depthOfFieldSensorHeight` | `0.024` | Sensor height in meters (35mm-ish). | +| `depthOfFieldBlurScale` | `1.0` | Overall blur multiplier. | +| `depthOfFieldMaxForegroundBlur` / `depthOfFieldMaxBackgroundBlur` | `24.0` / `32.0` | Blur radius caps. | +| `depthOfFieldBladeCount` | `0` | Aperture blades for bokeh shape (0 = round). | +| `depthOfFieldBladeRotation` / `depthOfFieldBladeCurvature` | `0.0` / `0.0` | Bokeh blade shaping. | +| `depthOfFieldQuality` | `DepthOfFieldQuality.medium` | `low` (16 taps, mobile/web), `medium` (32 taps + postfilter), `high` (48 taps). | + +--- + +## Not on `EnvironmentSettings` + +Two look-affecting settings live outside the `EnvironmentSettings` snapshot: anti-aliasing is a `Scene` field, and reflection probes are a scene-graph component. Set them directly. + +### Anti-aliasing (`Scene.antiAliasingMode`) + +Edge anti-aliasing. `AntiAliasingMode.auto` is the default: it picks `msaa` where the backend supports it and `fxaa` otherwise. Set it directly, not through `EnvironmentSettings`. + +```dart +scene.antiAliasingMode = AntiAliasingMode.smaa; +``` + +| Mode | What it does | +| --- | --- | +| `none` | No anti-aliasing; native-resolution edges. | +| `msaa` | 4x MSAA on the scene pass, the best geometry-edge quality and cheap on mobile GPUs, but not supported on every Flutter GPU backend (falls back to `fxaa`). Check `Scene.isAntiAliasingModeSupported` and read `Scene.effectiveAntiAliasingMode`. | +| `fxaa` | One post pass over the tone-mapped image, supported everywhere, but softens all high-contrast edges including texture detail. | +| `smaa` | SMAA 1x, three post passes, supported everywhere. Reconstructs edge shapes so edges are cleaner than `fxaa` with far less texture blurring, at roughly 3x the `fxaa` cost. Reach for it when `fxaa` looks mushy and `msaa` is unavailable. | +| `auto` | `msaa` where supported, else `fxaa`. | + +### Reflection probes (`ReflectionProbeComponent`) + +SSR only reflects what is currently on screen. A reflection probe captures the surroundings from a point into a local, parallax-corrected environment, so off-screen geometry reflects correctly inside a bounded box (a mirror ball, a glossy floor in a room). It is a `Component` attached to a `Node`, not an `EnvironmentSettings` field. + +```dart +final probe = Node() + ..localTransform = vm.Matrix4.translation(vm.Vector3(0, 1, 0)); // reflective spot +probe.addComponent(ReflectionProbeComponent( + extents: vm.Vector3(4, 3, 4), // box half-extents (the influence + parallax volume) +)); +scene.add(probe); +``` + +| Constructor arg | Default | What it does | +| --- | --- | --- | +| `extents` | `Vector3.all(5.0)` | Half-extents of the world-axis-aligned box that is both the influence volume and the parallax proxy. | +| `blendDistance` | `1.0` | Distance over which the probe cross-fades with the environment at the box edge. | +| `priority` | `10.0` | Which probe wins where several overlap. | +| `weight` | `1.0` | Contribution scale in the blend. | +| `faceResolution` | `128` | Cubemap face resolution of the capture. | +| `captureOnActivate` | `true` | Capture once when the probe joins the scene. Call `requestCapture()` to re-capture after the scene changes; the capture is a static snapshot otherwise. | + +For a one-shot environment capture with no node or parallax (e.g. to hand a captured `EnvironmentMap` to another material), `Scene.captureEnvironment(position: ...)` returns an `EnvironmentMap` directly. + +### Planar reflectors (`PlanarReflectorComponent`) + +A true mirror for one flat surface, re-rendered every frame the surface is visible: the engine renders the scene from the view camera reflected across the surface's plane (near plane clamped to the mirror, so nothing behind it leaks in) and hands the capture to the surface's material. Use it for mirrors and glossy floors where SSR's on-screen-only reflections or a probe's static capture are not enough. + +Two pieces pair up. The component goes on the mirror node (the plane is the node's local `+Y` through its transform, or an explicit `localNormal`): + +```dart +final mirror = Node(mesh: Mesh(PlaneGeometry(width: 10, depth: 10), mirrorMaterial)) + ..addComponent(PlanarReflectorComponent()); +scene.add(mirror); +``` + +And the surface's material is a `.fmat` that declares the `planar_reflection` engine input and samples `GetPlanarReflection()` (mirrored scene color in rgb, `a` 1 while a capture is bound; fall back to the environment reflection at `a == 0`). A worked mirror lives at `examples/flutter_app/assets/planar_mirror.fmat`. + +| Constructor arg | Default | What it does | +| --- | --- | --- | +| `resolutionScale` | `0.5` | Capture resolution relative to the view (clamped `0.1..1.0`). The fragment-cost lever. | +| `layerMask` | all layers | What renders into the capture. The draw-cost lever. | +| `reflectionGroupId` | `-1` | Co-planar surfaces sharing a non-negative id share one capture per frame; `-1` means an own capture. | +| `clipBias` | `1e-3` | World-space offset of the clip plane in front of the mirror, keeping the surface itself out of the capture. | +| `localNormal` | local `+Y` | The mirror plane's facing direction in node space. | + +The capture is a second scene submission per reflection group per frame: its CPU and draw-call cost scales with scene complexity, not just resolution. It reuses the frame's shadow atlas and runs without screen-space post; reflectors seen inside a capture draw their base look, so captures never recurse. + +--- + +## Cost and budget + +Post effects are screen-space passes; they cost per output pixel, not per triangle. Rough order, cheapest first: + +- **Nearly free.** Tone mapping, exposure, color grading, vignette, chromatic aberration, film grain, bloom (with lens flares). The `clean` and `stylized` looks live here. +- **Moderate.** Ambient occlusion (keep `ambientOcclusionHalfResolution: true`), fog, `fxaa`, `smaa` (~3x `fxaa`). `msaa` is nearly free on mobile GPUs but costs more elsewhere. +- **Expensive.** SSR, god rays, depth of field, and especially SSGI (`ambientOcclusionIndirectLight > 0`). Each adds ray-marching or gather passes. A reflection probe's capture renders the scene six times, so capture on activate or an occasional `requestCapture()`, never per frame. + +Budget guidance: + +- **Mobile and web are the ceiling.** A look that is smooth on desktop can tank a phone. Profile the target, do not assume. +- **Keep AO at half resolution** unless the coarse edges actually show. Full-res AO rarely earns its cost. +- **Do not stack the expensive effects blindly.** SSR plus god rays plus depth of field together on a low-end device needs profiling; drop one or lower its resolution/step budget (`screenSpaceReflectionsResolutionScale`, `godRaysStepCount`, `DepthOfFieldQuality.low`). +- **Depth of field is a hero-shot tool.** It reads as intentional on a framed still and as a smear on a free-moving interactive camera. Prefer it where the camera is controlled. +- **Bloom and grading buy the most look per cost.** When you need "flashier" cheaply, reach for these before the screen-space passes. + +## Why each look is built the way it is + +**showcase** aims for a clean, believable, flattering render, the default for showing a model off. `aces` tone mapping gives contrast and pop; soft bloom (threshold just above 1.0) lights the highlights without haze; GTAO with bent normals and `bentCone` specular AO grounds the object and tightens reflections in its cavities; SSR adds real floor and surface reflections; a light vignette focuses the eye. Every choice supports "look at this object", nothing calls attention to itself. + +**stylized** trades realism for graphic punch. It leans on color (saturation up, a warm push, contrast up) and glow (a lower bloom threshold so more of the frame blooms) and deliberately *omits* AO and SSR, because heavy occlusion and reflection read as realism and fight the flatter, poppier intent. Cheap to run, since it is all near-free passes. + +**moody** is the atmospheric, cinematic end. Lower exposure and cool grading set a somber base; exponential fog with a dark horizon color adds depth and hides the far plane; strong AO deepens the shadows; a tight heavy vignette closes the frame; film grain and a touch of chromatic aberration add texture and a lens feel; god rays (given a shadow-casting sun) add drama. It is the most expensive look; on a tight budget, drop god rays first, then SSR if present. + +**clean** is the honest look, for when the render must show the *actual* material and lighting without editorializing: an editor viewport, an inspector, a UI-embedded preview. Default `pbrNeutral` tone mapping preserves hue and saturation, and the only effect on is gentle half-res AO for grounding. No bloom, grading, or vignette, because each of those changes what the color and brightness actually are, which is exactly what an accurate preview must not do. Also the cheapest look by far. diff --git a/.agents/skills/flutter_scene-performance/SKILL.md b/.agents/skills/flutter_scene-performance/SKILL.md new file mode 100644 index 00000000..4b71ee0b --- /dev/null +++ b/.agents/skills/flutter_scene-performance/SKILL.md @@ -0,0 +1,63 @@ +--- +name: flutter_scene-performance +version: 3 +description: Make a flutter_scene app hit frame budget. Use whenever a scene janks, stutters, or drops frames, or when the ask is to make it faster or run on mobile or web, because code-driven scenes are reliably slow and the fix depends on which thread is over budget, not on a guessed poly count. +--- + +# Making flutter_scene fast + +A code-driven flutter_scene scene is reliably slow, and the usual reason is that nothing told you the budget or what to fix first, so optimization starts as guesswork. Guessing wastes iterations and often makes the wrong thread slower. This skill replaces the guessing with a budget, a way to measure it, and a fixed order to apply fixes in. + +**The one thing to internalize: measure the actual frame, find which of the two threads is over budget, then fix that thread. Do not target a triangle or draw-call number from memory.** There is no built-in poly budget, and the same scene can be fast on desktop and jank on a phone. The number that matters is milliseconds per frame on the real target. + +## The budget + +A frame has a fixed wall-clock budget set by the refresh rate. + +- **60 fps is 16.6 ms per frame. 120 fps is 8.3 ms.** Miss it and the frame janks. +- That budget is split across **two threads**, and either one blowing it drops the frame: + - **UI thread** runs your Dart. flutter_scene walks the scene graph, culls, updates components, and builds the render here. + - **Raster thread** is where Impeller draws the built frame on the GPU. The whole post-processing stack (ambient occlusion, reflections, depth of field, god rays, bloom) lands here. +- **Mobile and web are the real constraint.** Desktop GPUs hide a lot; a scene that runs smooth on a laptop can miss budget badly on a phone or in a browser. Profile on the lowest target you must support. + +## Measure first (do not skip this) + +There is no runtime stats API (`scene.frameTime` does not exist), but the renderer has compile-time profiling. Measurement is those defines plus Flutter's own tooling. + +1. **Run in profile mode.** `flutter run --profile --enable-flutter-gpu`. Debug-mode timings are meaningless for performance (assertions, no JIT-to-AOT optimization, extra checks), so never judge speed in debug. +2. **Engine pass timing.** Add `--dart-define=FLUTTER_SCENE_PROFILE=true` and the renderer prints sampled per-pass CPU means and maxes to stdout (`ScenePass`, `DepthPrepass`, `ShadowPass`, `BloomPass`, ...) plus encoder stats (`FLUTTER_SCENE_PROFILE_ENCODER`, draw and instance counts, sort/encode/instance-pack times). A healthy mean next to a huge max is a one-off stall, not steady-state cost; find the stall (see the pipeline section below) instead of shrinking the steady state. +3. **Read the frame chart.** Open DevTools, go to the Performance view, and read **per-frame UI time vs raster time**. The jank frames are flagged. This single view tells you which thread is over budget, which decides everything below. +4. **Or use the performance overlay** for a quick in-app read of the two thread graphs without DevTools. +5. **Stopwatch as a coarse fallback.** A `Stopwatch` around the per-frame work gives a rough UI-thread number when you cannot open DevTools. It sees nothing on the raster thread. + +Which thread is over budget names the fix. UI over budget means too much graph/CPU work (steps a, b, c below). Raster over budget means too much GPU work (steps d, e). See `references/performance.md` for the symptom to thread diagnosis. + +## The fixed remediation order + +Apply top-down. Each step lists the real API. Do the measured-over-budget thread's steps first, but the order within is deliberate, the earlier fixes are the bigger wins. + +1. **Instancing** (UI thread). Many copies of one mesh collapse into one draw and one cull test. Build an `InstancedMesh(geometry:, material:)`, add a transform per copy with `addInstance(matrix, {color})`, and mount it with `InstancedMeshComponent`. The single biggest win for repeated geometry (foliage, crowds, tiles, debris). Per-instance frustum culling is off by default (`cullInstances: false`), so the batch is one cull test as a unit. See the `flutter_scene-procedural` skill for the full scatter pattern. + +2. **Level of detail** (both threads). `LodComponent(List)` swaps cheaper meshes as an object shrinks on screen. Each `LodLevel(geometry:, material:, screenSize:)` gives a threshold (projected size as a fraction of viewport height, highest detail first, last is the cull floor). Fewer triangles for distant objects on the GPU, and nothing drawn below the floor. Note the shadow and depth passes always draw the highest-detail level, so LOD does not lighten shadow cost. + +3. **Culling** (UI thread). `Node.frustumCulled` (default true) skips off-screen subtrees; leave it on. Set it `false` only where the cached bound is known-stale or unbounded (procedural terrain you regenerate). For whole sets, `RenderView.cullingPlanes` adds extra clip planes, and `node.layers` (default `kRenderLayerDefault`) against `RenderView.layerMask` (default `kRenderLayerAll`) skips entire layers a view should not draw. + +4. **Shrink the post stack** (raster thread). This is usually where raster time goes. Turn off effects you do not need via the `EnvironmentSettings` `*Enabled` flags, keep `ambientOcclusionHalfResolution: true`, lower `depthOfFieldQuality` toward `DepthOfFieldQuality.low`, drop `RenderView.renderScale` (or `Scene.renderScale`) below `1.0` to render fewer pixels, and step `Scene.antiAliasingMode` down (`msaa` to `fxaa` to `none`). See the `flutter_scene-looks` skill for what each knob does to the look. + +5. **Texture and material consolidation** (raster thread). Fewer distinct textures and materials means fewer state changes and binds per frame. `TextureAtlas` (with `generateSolidColorAtlasPixels` for placeholders) packs many tiles into one texture so one material covers them all; share a single `Material` instance across many nodes instead of constructing one per node; use `MaterialsVariantsComponent` to switch a model between named material sets rather than duplicating materials. + +6. **Static shadows** (raster thread). `Node.shadowStatic = true` promises a caster's geometry, material coverage, and world transform will not change while mounted, so the engine renders it into cached shadow-map tiles reused across frames instead of re-encoding every caster every frame. A large static world becomes dramatically cheaper to shadow. Flag only genuinely static content; a static node that moves shows stale shadows until its render item re-registers. + +## First-draw pipeline stalls (the other kind of slow) + +Percentiles clean but the **worst** frame is tens to hundreds of milliseconds, at boot, on the first placement, the first effect, the first hover? That is not steady-state cost. Each (vertex shader, fragment shader, vertex layout) triple compiles a backend pipeline synchronously on its first draw, and a heavy custom material can cost 100ms+ on that one frame. + +- **Debug builds name the offenders.** Any pipeline build of 8ms or more prints `flutter_scene: pipeline build took ...` with the material and geometry. Play through the moments that hitch and collect the list; fix the list, not a guess. +- **The fix is pre-warming, not optimization.** The compile happens exactly once; move it where nobody sees it. Draw each named material once behind a load screen or during an intro, at tiny scale inside the frustum (a frustum-culled or `visible: false` node never draws, so it warms nothing). +- **Warm through the real builders.** The pipeline key includes the vertex layout, so a placeholder sphere warms a different pipeline than the real mesh. Build the actual tower/effect/overlay through its normal code path. +- **Keep warmup draws brief.** One frame compiles the pipeline. A material that samples scene depth also forces the full-resolution depth prepass while it is live, so a lingering warmup shows up as raster cost. +- **Do not trust repeat measurements.** GPU drivers cache compiled shaders across runs, so the second profiling session hits caches the first did not. First-run cost is what a fresh install pays. + +## More depth + +`references/performance.md` expands each step with the full API and when it helps, the UI-vs-raster symptom map (what a wrong frame time points to), and the honest note on what measurement tooling actually exists. diff --git a/.agents/skills/flutter_scene-performance/references/performance.md b/.agents/skills/flutter_scene-performance/references/performance.md new file mode 100644 index 00000000..51cd8598 --- /dev/null +++ b/.agents/skills/flutter_scene-performance/references/performance.md @@ -0,0 +1,128 @@ +# flutter_scene performance reference + +Companion to the `flutter_scene-performance` skill. The skill states the budget, the measure-first rule, and the fixed remediation order. This file expands each step with the real API, when it helps and when it does not, the diagnosis that maps a wrong frame time to a thread, and an honest account of the measurement tooling. + +Verify any symbol here against `lib/src` before relying on it; the inventory in the `flutter_scene-idioms` skill (`references/what-exists.md`) is the fuller API map. + +## The two threads, concretely + +A frame is built on the UI thread and drawn on the raster thread, and they overlap across frames (frame N rasters while frame N+1 builds). Either thread over budget drops the frame. + +- **UI thread work** is Dart. flutter_scene walks the scene graph, computes world transforms, runs frustum culling, ticks every component's `update`, and encodes the draw list. Cost scales with node count, component count, and how much per-frame Dart you run in `onTick` or component `update`. +- **Raster thread work** is the GPU. Impeller executes the encoded passes, the shadow pass, the main color pass, and every enabled screen-space post-processing pass. Cost scales with pixels drawn, overdraw, shadow-map resolution, and how many post passes are on. + +## Diagnosis, symptom to thread + +Read the two thread graphs in DevTools (or the performance overlay) and match the over-budget one to a cause. + +| Observation | Over-budget thread | Likely cause | Go to step | +| --- | --- | --- | --- | +| UI time high, raster fine | UI | Too many nodes/draws or heavy per-frame Dart | 1 instancing, 3 culling | +| UI time scales with object count | UI | Thousands of separate nodes for one repeated mesh | 1 instancing | +| UI time high with a huge static world | UI | No culling; whole graph walked every frame | 3 culling | +| Raster time high, UI fine | Raster | Too many pixels or post passes | 4 post stack, 5 consolidation | +| Raster time high, and it tracks resolution | Raster | Fill-bound; too many pixels | 4 `renderScale`, AA | +| Raster spikes only when shadows are on | Raster | Every caster re-encoded per frame | 6 static shadows | +| Raster time tracks the number of distinct materials | Raster | State-change churn from per-node materials/textures | 5 consolidation | +| Jank only on phone or web, smooth on desktop | Whichever is over budget there | Desktop GPU was hiding it | measure on the real target | + +If both threads are near budget, fix the UI thread first (steps 1 to 3); a lighter draw list also lightens the raster thread. + +## Measurement tooling, honestly + +There is **no built-in frame-stats API in flutter_scene**. No `scene.frameTime`, no draw-call count, no visible-triangle count. Do not invent one or claim one exists. + +- **Profile mode is mandatory.** `flutter run --profile --enable-flutter-gpu`. Debug builds carry assertions and skip AOT optimization, so their timings do not reflect a release build. A number taken in debug mode is not a performance number. +- **DevTools Performance view** is the primary tool. It shows per-frame UI time and raster time as two tracks, flags janky frames, and lets you expand a frame's timeline. This is what tells you which thread is over budget. +- **The performance overlay** gives the same two thread graphs in-app for a quick read without attaching DevTools. +- **A `Stopwatch`** around the per-frame work (the `onTick` body, or a component `update`) is a coarse UI-thread fallback. It cannot see the raster thread at all, so a good stopwatch number does not clear a raster-bound jank. +- **Editor MCP.** `get_app_state` reports lifecycle only (launching/running), not timing. The one place per-pass GPU timings surface is a render-graph capture (`Scene.captureRenderGraph`, or the editor MCP capture tool), whose result carries per-pass timing and lets you see which post pass is expensive. That is per-pass GPU detail, not a whole-frame counter, and the editor MCP is not connected in every project. When it is not, the DevTools loop above is fully sufficient. + +## Step 1, instancing + +`InstancedMesh` holds one `geometry`/`material` pair and one transform per copy. The whole set encodes as a single draw and, by default, a single frustum cull test. + +```dart +final mesh = InstancedMesh( + geometry: someGeometry, + material: sharedMaterial, // one material for the whole batch +); +for (final placement in placements) { + mesh.addInstance(placement.transform, color: placement.tint); // matrix is cloned +} +scene.add(Node()..addComponent(InstancedMeshComponent(mesh))); +``` + +- `addInstance(Matrix4, {Vector4? color})` returns an index; the matrix is cloned, so mutating your copy afterward is safe. Per-instance `color` is a linear RGBA multiplier. +- Edit later with `setInstanceTransform(i, m)`, `setInstanceColor(i, color)`, `removeInstanceAt(i)`, `clearInstances()`, or move the whole batch in one pass with `updateInstanceTransforms((list) { ... })`. +- **Culling default.** `cullInstances` defaults to `false`, so the batch is culled as one unit against its combined bounds, not per instance. Turn `cullInstances: true` on only for a batch spread across a large area where many instances are off-screen, since per-instance culling adds CPU work. +- **When it helps.** Repeated geometry, foliage, crowds, tiles, debris, particles-as-meshes. It is the largest UI-thread win available, because N separate nodes become one. It does nothing for a scene of distinct meshes. +- **Winding trap.** A mirrored (negative-determinant) instance edited with `updateInstanceTransforms(recomputeWinding: false)` renders inside-out. Keep instance edits orientation-preserving, or let winding recompute. + +See the `flutter_scene-procedural` skill for the full scatter-on-terrain pattern. + +## Step 2, level of detail + +`LodComponent(List)` draws one of several mesh variants per frame, chosen from how large the object appears on screen. + +```dart +node.addComponent(LodComponent([ + LodLevel(geometry: high, material: mat, screenSize: 0.4), + LodLevel(geometry: mid, material: mat, screenSize: 0.15), + LodLevel(geometry: low, material: mat, screenSize: 0.04), // set 0.0 to never cull +])); +``` + +- `screenSize` is the projected bounding-sphere diameter as a fraction of viewport height. Levels are highest detail first, strictly descending. The engine draws the highest-detail level whose threshold the object still meets, and draws nothing below the last threshold (the cull floor). +- Selection is screen-size based, so it is field-of-view aware and resolution independent, and it is per view (a split-screen frame can pick different levels per view). +- `LodComponent(levels, {lodBias = 1.0, hysteresis = 0.1, blendRange = 0.0})`. `lodBias` above `1` keeps detail farther away; `hysteresis` is a dead-band so an object on a boundary does not flip-flop; `blendRange` above `0` dither-cross-fades adjacent levels to remove the pop (honored by the built-in lit and unlit materials). +- **Limitation that matters for shadows.** The shadow and depth-prepass passes always draw the highest-detail level and ignore the LOD cull. So LOD lightens the color pass, not shadow or depth cost. A shadow-heavy scene needs step 6, not LOD. +- **Not for instanced draws.** A `LodComponent` draws a single mesh and picks one level for the whole node; it does not combine with hardware instancing. + +## Step 3, culling + +Skip work for things the camera cannot see. + +- **`Node.frustumCulled`** (default `true`) skips a subtree whose `combinedLocalBounds` do not intersect the camera frustum. Leave it on. Set it `false` only where the cached bound is known-stale or misleading (procedural geometry you regenerate, large terrain pieces). A subtree that reports no bound (skinned content, geometry without a computable bound) is treated as always visible regardless of the flag. +- **`RenderView.cullingPlanes`** (`List`, default empty) adds extra clip planes beyond the frustum, for portal or region culling. +- **Layers.** `node.layers` (a 32-bit mask, default `kRenderLayerDefault` which is layer 0, not inherited by children) against `RenderView.layerMask` (default `kRenderLayerAll`) decides whether a view draws a node at all, when `node.layers & view.layerMask != 0`. Put editor gizmos, an inset viewport's contents, or a minimap's set on their own layer and give each view the mask it needs, so a view skips whole sets cheaply. +- **When it helps.** Large worlds where much of the graph is off-screen each frame. Culling is a UI-thread win (fewer nodes encoded) that also lightens the raster thread (fewer draws). + +## Step 4, shrink the post stack + +Every screen-space effect is a raster-thread pass. Turning off what you do not need is the most direct raster win. All the scene-wide look fields live on `EnvironmentSettings` (see the `flutter_scene-looks` skill); each effect has an `*Enabled` flag, off by default. + +- **Turn effects off.** `ambientOcclusionEnabled`, `screenSpaceReflectionsEnabled`, `godRaysEnabled`, `depthOfFieldEnabled`, `bloomEnabled`, and the rest default `false`. A preset the app copied may have turned several on; drop the ones the scene does not visibly need. Ambient occlusion, screen-space reflections, god rays, and depth of field are the heavy ones. +- **Half-resolution AO.** `ambientOcclusionHalfResolution` defaults `true`; keep it. Full-resolution AO roughly doubles that pass's cost for little visible gain on most content. +- **Cheaper depth of field.** `depthOfFieldQuality` (`DepthOfFieldQuality.low`/`medium`/`high`, default `medium`) trades gather taps and cleanup passes for time. Step it down to `low` on mobile. +- **Render fewer pixels.** `Scene.renderScale` (default `1.0`), or per-view `RenderView.renderScale`, renders the scene at a fraction of resolution and upscales. Dropping to `0.75` cuts fill cost by nearly half and is often barely visible after anti-aliasing. This is the biggest lever for a fill-bound (resolution-tracking) raster time. +- **Step anti-aliasing down.** `Scene.antiAliasingMode`. `AntiAliasingMode.auto` picks `msaa` where supported else `fxaa`. `msaa` is cheap on mobile tilers and highest quality; `fxaa` is a single post pass on every backend; `smaa` is cleaner than `fxaa` but three post passes (~3x its cost), so step it down to `fxaa` or `none` on a raster-bound target; `none` is free. Read what actually runs with `Scene.effectiveAntiAliasingMode`. +- **Do not re-capture reflection probes per frame.** A `ReflectionProbeComponent` capture (or `Scene.captureEnvironment`) renders the scene six times, a large one-frame spike. Let it capture once on activate and only call `requestCapture()` when the scene visibly changes, never every frame. +- **When it helps.** Any raster-bound scene. The `clean` look in the looks skill is nearly free; a full `moody` stack (AO plus SSR plus god rays plus DoF plus grain) is the heaviest. Do not stack all of those on a low-end target without profiling. + +## Step 5, texture and material consolidation + +Every distinct material and texture is a potential state change and bind on the raster thread. Fewer of them means a shorter, cheaper draw list. + +- **`TextureAtlas`** packs many equally sized tiles (voxel faces, sprite sheets, terrain tiles) into one texture, so a single material and draw call cover every tile. Resolve a tile's UV box with `tileBounds(index)` or map a within-tile coordinate with `tileUv(index, u, v)`, write those into the mesh's texture coordinates, and build the bound material with `toMaterial()`. `generateSolidColorAtlasPixels(tileColors:, columns:, tileSize:, padding:)` builds placeholder pixels to bring the atlas path up before real art exists. +- **Share one `Material` instance** across many nodes rather than constructing a new one per node. Identical materials that are separate objects still churn binds; the same object does not. Build the material once and reuse the reference. +- **`MaterialsVariantsComponent`** switches an imported model between its named `KHR_materials_variants` sets in place (`MaterialsVariantsComponent.of(model)?.select('name')`), instead of duplicating a model per look. Read `variants` for the declared names; `select(null)` restores defaults. +- **When it helps.** Scenes whose raster time tracks the count of distinct materials or textures, tile-based worlds, and models shown in several finishes. + +## Step 6, static shadows + +Shadow casting re-encodes every caster into the shadow map each frame by default. `Node.shadowStatic = true` promises a caster will not change and lets the engine cache its shadow-map tiles across frames. + +```dart +staticWorldNode.shadowStatic = true; // set per mesh-bearing node; not inherited +``` + +- **The contract.** The node's geometry, material coverage, and world transform must not change while mounted. In return, the engine renders it into cached shadow-map tiles reused across frames instead of re-encoding it every frame. Dynamic nodes (the default) still cast per-frame shadows on top of the cache, so a moving character over a static world works. +- **Not inherited.** Set it on each mesh-bearing node, not once on a root. +- **Stale-shadow caveat.** A static node that does change (moves, remeshes, edits material coverage) shows stale shadows until its render item re-registers. Flag only genuinely static content. +- **Displacement caveat.** A material with a `vertex { }` displacement stage should stay dynamic, since its cached shadow would not follow a camera-dependent displacement. +- **When it helps.** Large static worlds with a shadow-casting `DirectionalLight`. The win scales with how many static casters you have; a mostly static level with a few moving actors is the ideal case. + +## Order and stopping + +Fix the measured over-budget thread first, top-down within it, and re-measure after each change so cause and effect stay legible. Stop when the frame chart clears budget on the real target; there is no reason to keep optimizing a thread that is already under budget while the other one janks. The whole point of measuring first is to avoid spending a step's effort on the thread that was never the problem. diff --git a/.agents/skills/flutter_scene-procedural/SKILL.md b/.agents/skills/flutter_scene-procedural/SKILL.md new file mode 100644 index 00000000..11eaa1d7 --- /dev/null +++ b/.agents/skills/flutter_scene-procedural/SKILL.md @@ -0,0 +1,138 @@ +--- +name: flutter_scene-procedural +version: 4 +description: Build flutter_scene content from code instead of asset files. Use when generating terrain, scattering vegetation or crowds, building oceans and Gerstner waves, setting up procedural skies and trees, assembling modular kits, or driving a scene from noise and instancing rather than loading a .glb. +--- + +# Procedural content in flutter_scene + +A lot of 3D work does not need an artist's `.glb` at all. Terrain, scattered foliage, debris fields, crowds, and modular buildings are cheaper and more flexible built from code, and flutter_scene has the whole path in the box. Reach for it before wiring up an asset pipeline. + +**The insight: for a code-driven scene, generate geometry and draw it instanced. That path is more reliable than loading external assets, because it has no import step, no coordinate-conversion traps, no missing-file failure modes, and one draw call for thousands of copies.** Three pieces cover almost everything: + +- **`GeometryBuilder`** (and the built-in primitives and swept paths) build custom meshes without a model file. +- **`FastNoiseLite`** drives heightmaps, placement, and displacement deterministically. +- **`InstancedMesh`** draws thousands of copies of one mesh as a single render item. + +Do not hand-pack a `ByteData` vertex buffer. The vertex layout is fixed (72 bytes unskinned, a specific attribute order) and a wrong stride fails silently with washed-out or see-through geometry. `GeometryBuilder` and `MeshGeometry.fromArrays` interleave the layout for you. + +## Imports + +Geometry and instancing live in the main barrel. **Noise is a separate barrel** and is easy to forget: + +```dart +import 'package:flutter_scene/scene.dart'; // GeometryBuilder, MeshGeometry, InstancedMesh, ... +import 'package:flutter_scene/noise.dart'; // FastNoiseLite, bakeNoiseTexture, noiseCurl3 +import 'package:vector_math/vector_math.dart' as vm; // NOT vector_math_64 +``` + +## Terrain from a noise heightmap + +Sample `FastNoiseLite` on a grid, add each vertex, and wind the two triangles per cell so the lit surface faces up. Omitting normals lets the builder derive them from the actual face slopes, which is what you want for terrain. + +```dart +MeshGeometry buildTerrain({int cols = 128, int rows = 128, double spacing = 0.5}) { + final noise = FastNoiseLite(seed: 1337) + ..noiseType = NoiseType.openSimplex2 + ..fractalType = FractalType.fbm // stack octaves for natural detail + ..octaves = 5 + ..frequency = 0.02; // world units are multiplied by this + + final builder = GeometryBuilder(); + + // One vertex per grid point. getNoise2 returns roughly -1..1. + for (var r = 0; r < rows; r++) { + for (var c = 0; c < cols; c++) { + final x = c * spacing; + final z = r * spacing; + final height = noise.getNoise2(x, z) * 6.0; + builder.addVertex(vm.Vector3(x, height, z)); + } + } + + // Two triangles per cell, wound Counter-Clockwise (CCW) so the front face points +Y (up). + for (var r = 0; r < rows - 1; r++) { + for (var c = 0; c < cols - 1; c++) { + final v00 = r * cols + c; + final v10 = v00 + 1; + final v01 = v00 + cols; + final v11 = v01 + 1; + builder + ..addTriangle(v00, v01, v10) + ..addTriangle(v10, v01, v11); + } + } + + return builder.build(); +} +``` + +Attach it like any mesh: + +```dart +final terrain = Node(mesh: Mesh(buildTerrain(), PhysicallyBasedMaterial()..roughnessFactor = 1.0)); +scene.add(terrain); +``` + +If a hand-built surface renders inside-out (visible only from below, dark where lit), reverse each triangle's index order. flutter_scene's front faces wind Counter-Clockwise (CCW) in model space, matching glTF and standard conventions; never fix orientation with a per-triangle flip on an imported model, but for geometry you author yourself the winding is yours to set. + +## Natural formations and landscape recipes + +To achieve documentary realism rather than generic procedural lumps: + +1. **Footpaths are scoured trenches, not flat stripes.** A real trail is the lowest line across terrain because water and foot traffic erode it downwards. When generating heightfields, cut the trail path profile down into the terrain with banks rising away on both sides. +2. **Ridged noise for valley walls and cliffs.** Standard `FractalType.fbm` makes rolling mounds. Use `FractalType.ridged` for valley walls, mountain spurs, and cliffs to produce sharp erosion creases. +3. **Free-end Worley rock cracks.** Standard Worley noise (`F2 - F1`) creates closed polygonal loops like bathroom tile. To produce weathered rock fractures with natural free ends, multiply the cell border by a low-frequency region mask and a high-frequency grain breaker. +4. **Noise-modulated pitting.** A constant threshold radius across Worley cells places a pit in every cell, producing an artificial grid lattice. Modulate the threshold radius with an underlying Perlin field so pores vary in size and only appear in exposed weathering pockets. +5. **Macro massing for scattered gravel.** Soil wears in 0.5m to 2m zones. Modulate multi-scale pebble instances with a low-frequency massing field so gravel clusters into realistic water scour lines rather than uniform sandpaper noise. +6. **Sunk block settling and ground contact staining.** Place boulders and masonry courses 1/3 to 2/3 submerged into the sampled ground height. Use vertex colors or shader ground distance to stain the bottom 20cm of rock near the soil boundary, creating a smooth moisture transition. +7. **Oceans and Gerstner waves.** Sum 4 to 8 directional Gerstner trochoidal waves that pull vertices horizontally toward crests, producing sharp peaks and wide flat troughs. Use Beer-Lambert depth absorption (exp(-sigma_a * d)) via scene depth for turquoise to deep navy transitions, Jacobian folding for peak foam, and darken/smooth tidal sand within the shoreline wash. +8. **Trees and foliage translucency.** Extrude branch splines using `TubeGeometry` or `ExtrudeGeometry`, conserving cross-sectional area across splits (d_parent^2 = sum d_child^2). Set `Material.doubleSided = true` and add diffuse transmission in custom leaf shaders so backlit canopies glow. Apply quadratic cantilever displacement (delta_p proportional to h^2) for organic wind sway. +9. **Procedural skies and IBL synchronization.** Use `PhysicalSkySource` (`lib/src/sky_sources.dart`) with analytic Rayleigh and Mie scattering. Assign `SkyEnvironment` to `Scene.skyEnvironment` or call `EnvironmentMap.fromSky` to bake prefiltered radiance and SH-9 diffuse coefficients into the scene's IBL automatically, and assign the source to `Scene.skybox` for matching background visuals. +10. **Islands and coastal erosion.** Multiply radial distance falloff with domain-warped FBM to form organic bays, sandbars, and lagoons. Use analytical surface slopes to strip topsoil on steep cliffs while depositing golden sand and reef shoals on shallow coastal planes. + +## Scattering thousands of copies + +`InstancedMesh` holds one geometry/material pair and a transform per copy. The whole set is one pipeline and one cull test. Place instances by sampling the same terrain height so they sit on the ground. + +```dart +final rng = math.Random(7); +final scatter = InstancedMesh( + geometry: CylinderGeometry(bottomRadius: 0.0, topRadius: 0.15, height: 1.2), // a cone + material: PhysicallyBasedMaterial()..baseColorFactor = vm.Vector4(0.2, 0.5, 0.15, 1), +); + +for (var i = 0; i < 4000; i++) { + final x = rng.nextDouble() * 64; + final z = rng.nextDouble() * 64; + final y = noise.getNoise2(x, z) * 6.0; // same field as the terrain + final transform = vm.Matrix4.translation(vm.Vector3(x, y, z)) + ..rotateY(rng.nextDouble() * math.pi * 2); + scatter.addInstance(transform); // the matrix is cloned; mutating it later is safe +} + +// InstancedMesh rides on a component, not Node(mesh:). +final node = Node()..addComponent(InstancedMeshComponent(scatter)); +scene.add(node); +``` + +`addInstance(matrix, {color})` returns an index; edit later with `setInstanceTransform(i, m)` or move the whole batch at once through `updateInstanceTransforms((list) { ... })`. Per-instance `color` is a linear RGBA multiplier. Keep instance edits orientation-preserving, a mirrored (negative-determinant) instance edited with `updateInstanceTransforms(recomputeWinding: false)` renders inside-out. + +## Repeated pieces must not overlap + +A fence, barrier, curb, or row of tiles repeated along a line flickers at every joint when each piece is longer than its spacing (8.4 m pieces every 8 m), because neighbors share faces in one plane and fight as the camera moves (z-fighting). Alternating colors make it obvious. Size each piece to exactly its spacing, stop perpendicular runs at a corner post instead of overlapping them, and scatter flat patches at one height without overlap. An overlay that lies on a surface (paint, a sign, a decal quad) gets `material.depthLayer = 1`. `scene.findCoplanarOverlaps()` lists any overlap left, with the length that fixes a repeated piece. + +## The web noise trap + +The Dart `FastNoiseLite` relies on 32-bit integer math. On the web (dart2js) a Dart `int` is a JavaScript double, exact only to 53 bits, so the hash loses its low bits and 3D noise can overflow, producing wrong values. This is silent, you get a plausible-looking but incorrect field, and only on web. + +For web targets: + +- Prefer the **GLSL side** (`#include ` in a `.fmat` block), which is correct on every backend including WebGL2 and matches the Dart algorithms table-for-table. +- Or **bake** the field once with `bakeNoiseTexture(noise, width: ..., height: ...)` at build time or in a native isolate, then sample the texture. `bakeNoisePixels` is pure CPU with no engine imports, so it runs in a build hook or background isolate. + +Native platforms are unaffected. `noiseHash2`/`noiseHash3` are the bit-exact CPU/GPU-agreeing integer path for decisions that must never disagree (world generation, placement), but they carry the same web-overflow caveat, so make the decision once and share it rather than re-deriving it on both sides. + +## More depth + +`references/procedural.md` has the full `GeometryBuilder` and `MeshData` API (including off-isolate meshing), the complete `FastNoiseLite` config reference, natural rock, ocean, tree, sky, and island formation recipes, the instancing API in full, modular-kit assembly from the built-in primitives, and the web-noise caveat expanded. diff --git a/.agents/skills/flutter_scene-procedural/references/procedural.md b/.agents/skills/flutter_scene-procedural/references/procedural.md new file mode 100644 index 00000000..a66fe69a --- /dev/null +++ b/.agents/skills/flutter_scene-procedural/references/procedural.md @@ -0,0 +1,512 @@ +# Procedural content, the full API + +Everything for building flutter_scene content from code, custom meshes, noise, instancing, and modular kits. All symbols verified against the package source. Import geometry and instancing from the main barrel, noise from its own barrel: + +```dart +import 'package:flutter_scene/scene.dart'; +import 'package:flutter_scene/noise.dart'; +import 'package:vector_math/vector_math.dart' as vm; // NOT vector_math_64 +``` + +--- + +## GeometryBuilder + +The incremental way to build a custom triangle mesh. You add vertices one at a time, each carrying whatever attributes are currently set, then reference them by returned index to form triangles. + +```dart +class GeometryBuilder { + GeometryBuilder({bool deduplicate = true}); + + // Sticky attribute setters (return `this`, so cascade or chain them). + GeometryBuilder normal(vm.Vector3 value); + GeometryBuilder texCoord(vm.Vector2 value); + GeometryBuilder texCoord1(vm.Vector2 value); // secondary UV set + GeometryBuilder color(vm.Vector4 value); // linear RGBA + GeometryBuilder tangent(vm.Vector4 value); // xyz + handedness in w + + int addVertex(vm.Vector3 position); // returns the vertex index + GeometryBuilder addTriangle(int a, int b, int c); // throws RangeError on a bad index + + int get vertexCount; + int get triangleCount; + + Uint8List packVertices(); // pure, no GPU context needed + MeshGeometry build({ + GeometryStorage storage = GeometryStorage.fixed, + GeometryBufferArena? bufferArena, + bool retainCpuData = true, + }); +} +``` + +### The sticky-attribute model + +The attribute setters do not apply to one vertex, they set state that every following `addVertex` inherits until you change it. This makes flat-shaded faces and per-region colors natural: + +```dart +final geometry = (GeometryBuilder() + ..color(vm.Vector4(1, 0, 0, 1)) // every vertex below is red... + ..addVertex(vm.Vector3(0, 0, 0)) + ..addVertex(vm.Vector3(1, 0, 0)) + ..color(vm.Vector4(0, 1, 0, 1)) // ...until this changes it to green + ..addVertex(vm.Vector3(0, 1, 0)) + ..addTriangle(0, 1, 2)) + .build(); +``` + +### Normals, generated or authored + +If you never call `normal()`, the builder generates area-weighted vertex normals from the faces you wound. That is the right default for most procedural geometry, terrain, extrusions, anything where the surface shape defines the normal. + +**Calling `normal()` even once opts the whole mesh out of generated normals.** After that, any vertex you add without an explicit normal keeps the default `(0, 0, 1)`, which is almost never what you want. So either author a normal for every vertex, or author none and let generation run. Do not mix. + +### Deduplication + +With `deduplicate: true` (the default), `addVertex` merges a vertex equal to one already added and returns the existing index, so a shared grid corner is stored once. Pass `deduplicate: false` when you want every call to produce a distinct vertex (flat shading with per-face normals, or per-vertex data that must not collapse). + +### Winding + +flutter_scene's front faces wind **counter-clockwise in model space**, matching glTF and standard conventions. For a surface that should face +Y (a heightmap, a floor), match the built-in plane's winding: for a cell with corners `v00`(x,z), `v10`(x+1,z), `v01`(x,z+1), `v11`(x+1,z+1), emit `addTriangle(v00, v01, v10)` and `addTriangle(v10, v01, v11)`. + +If a mesh renders inside-out (invisible from the front, visible and inverted-lit from behind), reverse each triangle's index order. Because generated normals follow the winding, fixing the winding fixes the normals too. This freedom is only for geometry you author. Never apply a per-triangle winding flip to an imported model to correct its orientation, that leaves its normals and image-based lighting wrong. + +### From builder to scene + +`build()` returns a `MeshGeometry`, which is a `Geometry`. Wrap it in a `Mesh` with a material, hang the mesh on a `Node`, add the node: + +```dart +final node = Node(mesh: Mesh(geometry, PhysicallyBasedMaterial())); +scene.add(node); +``` + +--- + +## MeshGeometry.fromArrays, the bulk path + +When you already have attributes as flat arrays (a generator that fills typed lists), skip the per-vertex calls and hand `MeshGeometry.fromArrays` structure-of-arrays data directly. Same result, less overhead for large meshes. + +```dart +MeshGeometry.fromArrays({ + required Float32List positions, // 3 floats/vertex, required + Float32List? normals, // 3/vertex; omitted -> generated for triangle lists + Float32List? texCoords, // 2/vertex; omitted -> (0, 0) + Float32List? texCoords1, // 2/vertex + Float32List? colors, // 4/vertex; omitted -> opaque white + Float32List? tangents, // 4/vertex + List? indices, // omitted -> vertex count must be a multiple of 3 + gpu.PrimitiveType primitiveType = gpu.PrimitiveType.triangle, + Aabb3? bounds, // skips the position scan; MUST cover every vertex + GeometryStorage storage = GeometryStorage.fixed, + GeometryBufferArena? bufferArena, + bool retainCpuData = true, +}); +``` + +Notes that bite: + +- Every supplied optional array must match the vertex count implied by `positions`. +- Out-of-range `indices` are **not** validated here (unlike `GeometryBuilder.addTriangle`), and produce stray triangles or holes silently. Keep every index in `0..vertexCount-1`. +- A `bounds` you pass that does not enclose every vertex makes the mesh over-cull and pop out of view at some angles. Omit it (the constructor scans positions) unless you computed it correctly off-thread. +- `retainCpuData: false` drops the CPU copy after upload, saving memory, but then the mesh cannot be raycast or read back with `extractMeshData`. + +### Updatable geometry (animated meshes) + +Pass `storage: GeometryStorage.updatable` to get a mesh you can mutate in place each frame without reallocating. The in-place updaters replace one attribute when the vertex count is unchanged: + +```dart +final water = MeshGeometry.fromArrays(positions: p, storage: GeometryStorage.updatable); +// later, per frame: +water.updatePositions(newPositions); // also updateNormals/TexCoords/Colors/Tangents +// or replace everything (may change the count): +water.rebuild(positions: p2, indices: i2); +``` + +An updatable mesh fixes its indexed-or-not state at construction, if you built it with `indices`, `rebuild` requires them thereafter, and vice versa. To start empty and fill later, pass a zero-length `positions` with `updatable`. Updatable geometry must retain CPU data and cannot use a buffer arena. + +`GeometryBufferArena({int blockSizeInBytes = 16 * 1024 * 1024})` lets many fixed meshes share immutable GPU buffer blocks, worth it when you build a large number of small static meshes. It never reclaims space (an arena only grows for as long as it is alive), so give it the lifetime of the batch it serves and never feed it geometry rebuilt every frame; that is what `updatable` is for. + +--- + +## MeshData, meshing off the render isolate + +Heavy generation (remeshing a voxel chunk, a large marching-cubes surface) should not block the render isolate. `MeshData` is a pure, isolate-transferable snapshot, build it on a background isolate with `compute`, send it back, upload it there. + +```dart +factory MeshData.build({ + required Float32List positions, + Float32List? normals, // omitted -> generated for triangle lists (the win here) + Float32List? texCoords, + Float32List? texCoords1, + Float32List? colors, + Float32List? tangents, + List? indices, + gpu.PrimitiveType primitiveType = gpu.PrimitiveType.triangle, + Map customAttributes = const {}, +}); +``` + +Recipe: + +```dart +// Top-level or static, runs on the background isolate. +MeshData buildChunk(ChunkInput input) { + final positions = /* your generator */; + final indices = /* ... */; + return MeshData.build(positions: positions, indices: indices); +} + +// On the render isolate: +final data = await compute(buildChunk, input); +final geometry = MeshGeometry.fromMeshData(data); +// or, to feed an existing updatable mesh in place: +existing.applyMeshData(data); +``` + +The normal generation is the expensive part, and running it inside `MeshData.build` is exactly the work you moved off the render isolate. + +Pure derivations on a `MeshData` (all off-isolate safe): `transformed(Matrix4)` (moves positions, carries normals by the inverse transpose so a non-uniform scale stays correct, reverses winding on a mirror), `unweld({attributes})`, `extractEdges({creaseAngleDegrees})`, static `MeshData.merge(parts)`, plus `triangleCount`/`triangles`. `Geometry.extractMeshData()` reads a loaded mesh back into one. + +--- + +## FastNoiseLite + +One configurable object evaluating several noise algorithms, sampled with `getNoise2`/`getNoise3`. Output is roughly in `[-1, 1]`. + +```dart +final noise = FastNoiseLite(seed: 1337) + ..frequency = 0.01 // coords are multiplied by this before eval + ..noiseType = NoiseType.openSimplex2 + ..fractalType = FractalType.fbm + ..octaves = 5; + +final h = noise.getNoise2(x, z); // 2D +final d = noise.getNoise3(x, y, z); // 3D +``` + +### Config reference + +| Field | Default | Meaning | +| --- | --- | --- | +| `seed` | 1337 | Seed for every noise type. | +| `frequency` | 0.01 | Input coordinates are scaled by this. Bigger = finer features. | +| `noiseType` | `openSimplex2` | Base algorithm (see below). | +| `fractalType` | `none` | How octaves layer (see below). | +| `octaves` | 3 | Number of fractal layers. More detail, more cost. | +| `lacunarity` | 2.0 | Frequency multiplier between octaves. | +| `gain` | 0.5 | Amplitude multiplier between octaves. | +| `weightedStrength` | 0.0 | Biases octave amplitude toward stronger detail. | +| `pingPongStrength` | 2.0 | Warp strength for `FractalType.pingPong`. | +| `cellularDistanceFunction` | `euclideanSq` | Distance metric for `NoiseType.cellular`. | +| `cellularReturnType` | `distance` | What cellular returns. | +| `cellularJitterModifier` | 1.0 | Cell-point jitter; above 1 causes artifacts. | +| `domainWarpType` | `openSimplex2` | Warp algorithm for `domainWarp2`/`domainWarp3`. | +| `domainWarpAmp` | 1.0 | Max warp distance. | +| `domainWarpFractalType` | `none` | Octave layering for domain warp. | + +Enums: + +- `NoiseType` = `openSimplex2` | `openSimplex2S` | `cellular` | `perlin` | `value`. +- `FractalType` = `none` | `fbm` (classic layered fractal, the usual terrain choice) | `ridged` (sharp ridges, mountains) | `pingPong`. +- `CellularDistanceFunction` = `euclidean` | `euclideanSq` | `manhattan` | `hybrid`. +- `CellularReturnType` = `cellValue` | `distance` | `distance2` | `distance2Add` | `distance2Sub` | `distance2Mul` | `distance2Div`. +- `DomainWarpType` = `openSimplex2` | `openSimplex2Reduced` | `basicGrid`. +- `DomainWarpFractalType` = `none` | `progressive` | `independent`. + +### Domain warp + +`domainWarp2`/`domainWarp3` distort the input coordinates before sampling, breaking up the regular look of raw fractal noise. The reference version mutates in place, this port returns the warped position for you to feed back in: + +```dart +final w = noise.domainWarp2(x, z); // ({double x, double y}) +final v = noise.getNoise2(w.x, w.y); +``` + +### Curl noise + +`noiseCurl3(x, y, z, {int seed = 1337, double epsilon = 0.25})` returns a divergence-free 3D vector `({x, y, z})` from a seeded potential field, for advecting particles so they swirl without clumping. Coordinates are taken pre-scaled (no frequency parameter), matching the GLSL `NoiseCurl3`. Advect by adding `curl * speed * dt`. A smaller `epsilon` sharpens the field and amplifies CPU/GPU divergence. + +### Baking noise to a texture + +Sampling many octaves per fragment is expensive. When the field is static, bake it once and sample the texture instead: + +```dart +Texture2D bakeNoiseTexture( + FastNoiseLite noise, { + required int width, + required int height, + double originX = 0.0, + double originY = 0.0, + double cellSize = 1.0, + TextureSampling sampling = const TextureSampling(), +}); +``` + +It bakes `getNoise2` over a `width` x `height` grid into a grayscale `Texture2D` (content is linear `data`, so mipmaps average cleanly) ready to bind as a material sampler. It must run where GPU resources are created (the raster thread). The CPU half, `bakeNoisePixels(noise, {width, height, originX, originY, cellSize})`, returns `Uint8List` RGBA and has no engine imports, so it runs in a build hook or a background isolate, then `Texture2D.fromPixels` uploads the result. + +--- + +### Natural formations: rocks, cliffs, trails, and scatter recipes + +Composing raw noise into realistic natural terrain and geology requires specific math patterns to avoid telltale procedural artifacts. + +### 1. Free-end Worley rock cracks (avoiding closed cell loops) + +Standard cellular Worley distance (`F2 - F1`) creates a continuous polygon network like bathroom tile or dry mud. To create natural weathering cracks with free ends, mask the cell borders with a low-frequency macro patch and a high-frequency grain breaker: + +```glsl +// GLSL shader bake or .fmat surface +// Cellular Worley noise returning F2 - F1 distance +float cwl = NoiseCellular2(p * 3.0, 1337, kNoiseCellularEuclidean, kNoiseCellularDistance2Sub, 1.0); +float net = smoothstep(0.08, -0.80, cwl); +float region = smoothstep(-0.2, 0.4, NoiseFbm2(p * 1.0, 1338, 3, 2.0, 0.5)); +float breaker = smoothstep(-0.4, 0.2, NoiseFbm2(p * 6.0, 1339, 2, 2.0, 0.5)); +float crack = net * region * breaker; // produces isolated segments with natural start/end points +``` + +### 2. Noise-modulated pitting (avoiding regular dot lattices) + +Thresholding Worley noise at a constant radius places a pit in every single cell, creating an artificial grid lattice. Modulate the threshold radius with an underlying Perlin field so pores vary in size and only appear in exposed weathering pockets: + +```glsl +float sizeVar = (NoiseFbm2(p * 2.5, 1337, 3, 2.0, 0.5) + 1.0) * 0.5; +float pw = NoiseCellular2(p * 5.0, 1338, kNoiseCellularEuclidean, kNoiseCellularDistance, 1.0); +float pit = smoothstep(0.05 + 0.24 * sizeVar * sizeVar, 0.005, pw) + * smoothstep(-0.1, 0.5, NoiseFbm2(p * 1.5, 1339, 3, 2.0, 0.5)); +``` + +### 3. Incised trail heightfields (scours vs flat stripes) + +Footpaths are formed by water and foot traffic compressing and eroding soil downwards. Sample the path polyline once (`trail.sample(n, evenlySpaced: true)`) and compute the minimum point-to-segment distance to cut the path profile into the terrain heightfield with raised spoil banks: + +```dart +double computeTerrainHeight(double x, double z, FastNoiseLite noise, List trailPoints) { + final baseHeight = noise.getNoise2(x, z) * 8.0; + + // Find minimum distance from (x, z) to the sampled 2D path segments + var minDist = double.infinity; + final p = vm.Vector2(x, z); + for (var i = 0; i < trailPoints.length - 1; i++) { + final a = vm.Vector2(trailPoints[i].x, trailPoints[i].z); + final b = vm.Vector2(trailPoints[i + 1].x, trailPoints[i + 1].z); + final ab = b - a; + final t = ((p - a).dot(ab) / ab.length2).clamp(0.0, 1.0); + final dist = (p - (a + ab * t)).length; + if (dist < minDist) minDist = dist; + } + + const pathWidth = 1.8; + const pathDepth = 0.45; + const bermHeight = 0.25; + + // Carve central path trough + final trench = (1.0 - (minDist / pathWidth).clamp(0.0, 1.0)) * pathDepth; + // Build gentle spoil berm along the verge + final verge = ((minDist - pathWidth * 0.8) / (pathWidth * 0.8)).clamp(0.0, 1.0); + final berm = math.sin(verge * math.pi) * bermHeight; + + return baseHeight - trench + berm; +} +``` + +### 4. Macro-massed pebble scatter (avoiding uniform sandpaper noise) + +Gravel and pebbles cluster into water-washed scour lines rather than spreading evenly over an entire level. Gate multi-scale pebble instances with a low-frequency macro massing field and key the hash off integer cell coordinates: + +```dart +void scatterPebbles(InstancedMesh finePebbles, InstancedMesh largeStones, FastNoiseLite terrainNoise) { + final macroNoise = FastNoiseLite(seed: 42)..frequency = 0.05; + const step = 0.8; + const cells = 80; // 64m / 0.8m + for (var ix = 0; ix < cells; ix++) { + for (var iz = 0; iz < cells; iz++) { + final x = ix * step; + final z = iz * step; + // Deterministic coordinate jitter keyed off integer cell indices + final h = noiseHash2(1337, ix, iz); + final jx = x + ((h & 0xFF) / 255.0 - 0.5) * 0.6; + final jz = z + (((h >> 8) & 0xFF) / 255.0 - 0.5) * 0.6; + + final mass = (macroNoise.getNoise2(jx, jz) + 1.0) * 0.5; + if (mass > 0.65) { + final y = terrainNoise.getNoise2(jx, jz) * 8.0; + final matrix = vm.Matrix4.translation(vm.Vector3(jx, y, jz)); + if (((h >> 16) & 0xFF) > 180) { + largeStones.addInstance(matrix); + } else { + finePebbles.addInstance(matrix); + } + } + } + } +} +``` + +### 5. Oceans and Gerstner waves + +Trochoidal Gerstner waves pull vertices horizontally toward wave peaks, creating sharp crests and wide flat troughs. Sum multiple directional waves and compute normals analytically: + +```glsl +// GLSL Gerstner wave displacement +struct Wave { vec2 dir; float amp; float freq; float speed; float steepness; }; + +// Caller seeds accumulators with tangent = vec3(1.0, 0.0, 0.0) and binormal = vec3(0.0, 0.0, 1.0). +vec3 evaluateGerstner(vec2 pos, float time, Wave w, float numWaves, inout vec3 tangent, inout vec3 binormal) { + vec2 d = normalize(w.dir); + float phase = dot(d, pos) * w.freq + time * w.speed; + float c = cos(phase); + float s = sin(phase); + float q = w.steepness / (w.amp * w.freq * numWaves); + + tangent += vec3(-q * d.x * d.x * w.amp * w.freq * s, + d.x * w.amp * w.freq * c, + -q * d.x * d.y * w.amp * w.freq * s); + binormal += vec3(-q * d.x * d.y * w.amp * w.freq * s, + d.y * w.amp * w.freq * c, + -q * d.y * d.y * w.amp * w.freq * s); + + return vec3(q * w.amp * d.x * c, + w.amp * s, + q * w.amp * d.y * c); +} +``` + +For shallow water transitions and shorelines: +- **Beer-Lambert Depth Extinction**: Declare `engine_inputs: [ depth ]` in the `.fmat` to sample linear opaque scene depth (`RenderInput.depth`). Compute water depth `d = sceneDepth - surfaceDepth` and attenuate color with `C = C_deep + (C_shallow - C_deep) * exp(-sigma_a * d)`. +- **Tidal Wet Sand**: Reduce sand roughness to 0.15 and multiply albedo by 0.6 within the wave wash zone to produce glistening wet shorelines. + +### 6. Trees, branching splines, and backlit foliage + +Trunk and branch structures follow Leonardo da Vinci's rule: total cross-sectional area is conserved across splits (d_parent^2 = sum d_child^2). Extrude branches along swept spline tubes using `TubeGeometry` (sweeping a round cross-section along a `ScenePath`) or `ExtrudeGeometry`: + +- **Backlit Leaf Translucency**: Set `Material.doubleSided = true` for two-sided rendering. In a custom leaf shader, add a diffuse transmission term so backlit foliage glows rather than rendering as a dark silhouette: +```glsl +// In custom leaf shader +float NdotL = dot(normal, lightDir); +float backLight = max(0.0, -NdotL) * leafTransmissionFactor; +vec3 litColor = albedo * (max(0.0, NdotL) + backLight * leafTranslucentColor); +``` +- **Quadratic Cantilever Wind**: Displace leaf and branch vertices in world space proportional to height squared (delta_p = windVec * (h / h_max)^2 * sin(omega * t - k * p)) so tips sway vigorously while roots remain anchored. + +### 7. Procedural skies and runtime IBL synchronization + +Use `PhysicalSkySource` (`lib/src/sky_sources.dart`) with analytic Rayleigh and Mie scattering. Assign `SkyEnvironment` to `Scene.skyEnvironment` or call `EnvironmentMap.fromSky` to bake prefiltered radiance and SH-9 diffuse coefficients into the scene's IBL automatically, and assign the source to `Scene.skybox` for matching background visuals. + +### 8. Islands, coastal bays, and sand dunes + +To form natural island topographies: +- **Domain-Warped Island Mask**: Multiply a radial distance falloff (1.0 - (r / R)^2) with domain-warped FBM to form organic bays, sandbars, and peninsulas rather than symmetrical circular cones. +- **Slope-Based Sediment Stripping**: Compute heightfield slope sqrt((dh/dx)^2 + (dh/dz)^2). Steep cliffs strip topsoil to expose rock strata, while gentle coastal planes accumulate golden beach sand. +- **Anisotropic Wind Dune Ripples**: Layer 8:1 anisotropically stretched noise perpendicular to the prevailing wind direction to generate fine ripple crests across sand surfaces. + +--- + +## The web noise caveat, expanded + +The Dart `FastNoiseLite` port relies on 32-bit integer arithmetic. On native platforms this is exact. On the web (dart2js), a Dart `int` is a JavaScript double, exact only to 53 bits, so the integer hash loses its low bits and 3D noise can overflow. The result is a plausible-looking but wrong field, silent, and web-only. A web-safe integer multiply for the Dart side is a planned follow-up. + +The GLSL half of the module is unaffected, it is correct on every backend including WebGL2, and implements the same algorithms with the same tables and seeds, so a field sampled on the CPU (native) and evaluated in a shader agree. The agreement has two tiers: + +- **Bit-exact**: `noiseHash2`/`noiseHash3` (and GLSL `NoiseHash2`/`NoiseHash3`) are pure integer math and match bit for bit across backends. Use them for decisions that must never disagree between machines (world generation, deterministic placement). +- **Float-close**: the float noise functions match within a small tolerance (float32 rounding differs per GPU), imperceptible visually. Do not re-derive a hard threshold from float noise on both the CPU and GPU sides, make the decision once and share the result. + +Both tiers carry the web-overflow caveat on the Dart side. Strategy by target: + +- **Native only**: use the Dart `FastNoiseLite` freely, on the render isolate or a background one. +- **Web, per-fragment noise**: move it to the GLSL side (`#include ` in a `.fmat` block). +- **Web, a static field**: bake it with `bakeNoiseTexture` (or `bakeNoisePixels` in a build hook / native isolate) and sample the texture. This sidesteps the overflow because the baking happens where `int` is 64-bit. + +--- + +## InstancedMesh, thousands of copies for one draw + +One geometry/material pair drawn many times, each placed by its own model transform. The whole set is one render item, one pipeline, one cull test. This is how you scatter foliage, crowds, debris, or a grid of the same prop without a node per copy. + +```dart +class InstancedMesh { + InstancedMesh({ + required Geometry geometry, + required Material material, + bool cullInstances = false, // per-instance cull after the aggregate pass + bool sortTransparentInstances = true, + }); + + int get instanceCount; + + int addInstance(vm.Matrix4 transform, {vm.Vector4? color}); // matrix is CLONED; returns index + void setInstanceTransform(int index, vm.Matrix4 transform); + void updateInstanceTransforms( + void Function(List transforms) update, { + bool recomputeWinding = true, + }); + void setInstanceColor(int index, vm.Vector4 color); // linear RGBA multiplier + void removeInstanceAt(int index); // shifts later indices down + void clearInstances(); +} +``` + +Attach it to a node with an `InstancedMeshComponent` (it does not go on `Node(mesh:)`): + +```dart +final mesh = InstancedMesh(geometry: geo, material: mat); +for (final placement in placements) { + mesh.addInstance(placement); // a Matrix4 in the instanced mesh's local space +} +final node = Node()..addComponent(InstancedMeshComponent(mesh)); +scene.add(node); +``` + +Practical notes: + +- `addInstance` clones the matrix, so reusing one scratch `Matrix4` across the loop is fine. +- The node the component is on transforms the entire batch. Instance transforms compose under it. +- To animate all instances cheaply, use `updateInstanceTransforms`, which invalidates the batch once instead of per call. Mutate the matrices in the callback list; do not add, remove, or replace entries. +- `updateInstanceTransforms(recomputeWinding: false)` skips the parity refresh. Only pass it when no edit changes a transform's winding. A mirrored (negative-determinant) edit under it renders those instances inside-out. +- `cullInstances: true` pays for per-instance culling, worth it for a large spatial spread whose instances enter view at different times; leave it off for a small compact clump that the single aggregate cull already handles. +- Set `cullInstances` per instanced mesh based on that trade; it is not a global. + +--- + +## Modular kits from the built-in primitives + +Before authoring a mesh, remember the ten primitives assemble a surprising amount by composition, no builder needed. Each is a `Geometry`, so each goes on its own `Node`, and a parent node groups a kit piece you can clone and place. + +| Class | Constructor | Notes | +| --- | --- | --- | +| `CuboidGeometry` | `CuboidGeometry(vm.Vector3 extents)` | Box from `-extents/2` to `+extents/2`. Positional. | +| `SphereGeometry` | `SphereGeometry({radius = 0.5, segments = 32, rings = 16})` | UV sphere. | +| `IcosphereGeometry` | `IcosphereGeometry({radius = 0.5, subdivisions = 2})` | Even triangle distribution. | +| `CylinderGeometry` | `CylinderGeometry({bottomRadius = 0.5, topRadius = 0.5, height = 1.0, ...})` | `topRadius: 0` makes a cone; different radii make a frustum. | +| `CapsuleGeometry` | `CapsuleGeometry({radius = 0.5, height = 1.0, ...})` | `height` is the mid-section; total Y is `height + 2*radius`. | +| `TorusGeometry` | `TorusGeometry({radius = 0.5, tubeRadius = 0.2, ...})` | Lies in XZ. | +| `PlaneGeometry` | `PlaneGeometry({width = 1.0, depth = 1.0, segmentsX = 1, segmentsZ = 1})` | XZ plane, faces +Y. | +| `DiscGeometry` | `DiscGeometry({radius = 0.5, segments = 32})` | Filled circle, XZ, faces +Y. | +| `RingGeometry` | `RingGeometry({innerRadius = 0.25, outerRadius = 0.5, segments = 32})` | Annulus, XZ, +Y. | +| `WedgeGeometry` | `WedgeGeometry(vm.Vector3 size)` | Triangular prism; base on `y = 0` (not Y-centered). | + +Because a cone is just `CylinderGeometry(topRadius: 0)`, a tree is a green cone on a brown cylinder, a fence is repeated thin cuboids, a table is a plane on four cylinders. Assemble each piece as a parented `Node` subtree, then `clone()` and place it, or feed the placements to an `InstancedMesh` when the same piece repeats many times. + +### Kit pieces must abut, never overlap + +Two faces of different pieces in one plane flicker as the camera moves (z-fighting), at any distance and on any device, so a kit that overlaps its pieces looks broken in motion even when a still looks right. + +- **Size a repeated piece to its spacing.** Fence rails, barriers, and curbs placed every `s` meters are `s` meters long. A piece 0.4 m longer than its spacing puts 0.4 m of coplanar faces at every joint. +- **Cap joints and corners with a post** a centimeter or two larger than the pieces on every face, and stop perpendicular runs at the post instead of running both into the corner. +- **Make stripes from color, not overlap.** Alternate per-instance `color` on abutting pieces, or paint stripes with vertex colors, rather than laying a strip of one color over another. +- **Scatter flat pieces without overlap.** Patches, tiles, and rugs at one height must not overlap each other; reject a candidate whose footprint overlaps one already placed, or give each kind its own `Material.depthLayer`. +- **Wrap with a clear gap or not at all.** A band or trim box wrapped around a larger box sits a clear distance proud (tens of centimeters for content seen from hundreds of meters), and two wraps on one piece use separate height slots. Facade detail seen from far away (windows on a skyline) belongs in the material rather than in thousands of proud boxes. +- **Overlays get a layer.** Paint, signs, and decal quads that lie on a surface get `material.depthLayer = 1`. + +`scene.findCoplanarOverlaps()` lists every overlap left, with the length that fixes a repeated piece, and debug builds print a summary once the scene holds still. + +Every primitive except `PlaneGeometry` exposes a `Shape get collisionShape` for the physics package, so a code-built kit gets colliders for free. + +### Swept geometry for shapes primitives cannot make + +For paths, tubes, and profiles, sweep a `ScenePath` (`BezierPath`, `CatmullRomPath`, `PolylinePath`): + +- `TubeGeometry(path, {radius = 0.5, radialSegments = 12, stations = 64, caps = true})` for pipes, cables, vines. +- `ExtrudeGeometry(path, {required List profile, stations = 64, caps = true})` sweeps a 2D profile along the path (railings, moldings, extruded logos). +- `RibbonGeometry(path, {width = 1.0, stations = 64, alignment = RibbonAlignment.ground})` for flat strips (roads, trails). + +These build detailed shapes from a curve and a few parameters, often replacing an imported model outright. diff --git a/.agents/skills/flutter_scene-verification-loop/SKILL.md b/.agents/skills/flutter_scene-verification-loop/SKILL.md new file mode 100644 index 00000000..e21261ca --- /dev/null +++ b/.agents/skills/flutter_scene-verification-loop/SKILL.md @@ -0,0 +1,71 @@ +--- +name: flutter_scene-verification-loop +version: 4 +description: Close the visual-iteration loop when building or debugging a flutter_scene 3D app so you see your own output and self-correct. Use whenever a change affects what renders (geometry, materials, lighting, shaders, post-processing) or a frame looks wrong (black, washed-out, see-through, missing geometry). +--- + +# Verifying flutter_scene visually + +flutter_scene renders 3D. A rendering change you cannot see is a guess, and guessing at pixels is the single biggest waste of iterations. The highest-value habit is a closed visual loop plus judgment that does not drift. This skill is that loop. + +**The one thing to internalize: run it, let it settle, look at the frame AND the console, localize before you edit, repeat.** Do not change code off a hypothesis you did not confirm from the actual output. + +## The loop + +1. **Run.** Launch the app with `flutter run --enable-flutter-gpu` (native; the flag is mandatory and it is the only run flag, see the idioms skill). Where the editor MCP is connected, `run_project` launches the managed session instead. +2. **Settle.** A live frame is a moving target. Auto-exposure is still ramping, particles have a random phase, an animation is mid-clip, IBL re-bakes after the first present. Let the scene reach steady state (a few frames, or until the image stops changing) before you trust a capture. Do not screenshot the first frame and reason from it. +3. **Capture the frame AND read the console.** A screenshot alone hides errors that print; the console alone hides wrong pixels. Take both every time. Baseline path is a screenshot plus the run log; with the editor MCP it is `screenshot_viewport` plus `get_console`. +4. **Localize before editing.** When something is wrong, find where it goes wrong before you touch code. Read an intermediate buffer, read a single pixel's exact value, or scan for non-finite values. A NaN or Inf propagates silently into black or garbage downstream, so the first pass that produced it is the culprit, not the pass where you see the black. `references/loop.md` has the tool table and a symptom to action map. +5. **Correct, repeat.** Make one change, run the loop again. One change per iteration keeps cause and effect legible. + +## Flicker needs motion, not a screenshot + +Two surfaces that overlap in one plane trade pixels as the camera moves (z-fighting), and any single frame can look right. Wherever surfaces meet or lie on each other (paint on a road, a screen on a wall, barriers along a track, overlapping tiles), prove the scene clean with a number: `await scene.probeDepthConflicts(camera: camera)` from each camera the app uses, driving `report.conflicts` to empty (each entry names the two nodes, the pixels they trade, and the distance). `scene.debug.overlays.add(DebugOverlay.depthConflicts)` shows the same live in magenta, and the editor MCP's `scan_for_depth_conflicts` runs the probe for you. Without either, compare two frames a few centimetres of camera motion apart. + +## The readiness gate (do not debug through it) + +Rendering is gated on `Scene.initializeStaticResources()`. Until that Future completes, every frame is skipped and the engine prints exactly: + +``` +Flutter Scene is not ready to render. Skipping frame. +``` + +If you see that line, the scene is not broken, it is not ready. Wait for readiness (build geometry and materials inside `initializeStaticResources().then(...)`, gate the widget on `Scene.isReadyToRender`) before you diagnose anything else. A black frame while that line prints is the gate, not your code. + +A second, quieter gate: the shaders behind `PhysicallyBasedMaterial` extensions (clearcoat, sheen, transmission, and the rest) and `ShadowCatcherMaterial` load on first use, and the scene holds its previous frame (or draws nothing, before its first) until they land. Await `Scene.preload()` before a capture so it shows the scene as it now is. + +## Judge blind, never self-score (the load-bearing rule) + +When deciding whether a change improved the look, **do not assign the frame a quality score.** Self-assigned scores drift upward, because the model is grading its own trajectory and wants to have made progress. That drift is how a session convinces itself a regression is an improvement. + +Instead, **compare two frames and return a binary pick.** Put the new frame next to a reference (a known-good target) or the previous frame, and answer only "which of these two is better", A or B. No number, no "8/10", no "looks pretty good now". A blind pairwise pick does not inflate the way a solo score does. This applies to every visual review, including the ones that feel obvious. + +If you have no reference at all, say so and describe the concrete difference between the two frames (this one is brighter here, that one has an artifact there) rather than inventing a score. + +## Empirical verification rules + +These rules prevent false diagnoses, hollow passes, and measurement traps during visual iteration: + +1. **Liveness before ablation.** A negative result is evidence only if the removed or modified term was actually live in the draw pass. Diff pixels for liveness before trusting an ablation. +2. **Tools must fail loud on empty measurements.** A tool that measures an empty population, zero pixels, or non-finite data must fail loud rather than returning a default passing number. +3. **Population discipline.** Always quote the population window, crop rectangle, brightness threshold, and rendering resolution beside any color or lighting figure. +4. **Baselines expire quickly.** Two captures taken hours apart in a changing tree reflect multiple edits; isolate paired A/B captures with temporary snapshots or git worktrees. +5. **Look at the raw frame before quoting numbers.** Inspect the actual captured frame before taking numbers off it; metrics can yield valid-looking numbers on corrupt frames. +6. **Negative control requirement.** A metric that returns the same score for positive and negative control populations cannot serve as evidence for either. +7. **Explanations are hypotheses, not evidence.** Before adopting an explanatory mechanism, identify the specific numerical observation that would differ if the mechanism were false. +8. **Attribution must reach the triangle.** When diagnosing geometry defects, trace down to the specific triangle indices and edge lengths rather than stopping at the mesh component. +9. **Symmetric domain clamping.** Clamping parametric lookup domains must be verified at both boundaries, and derived slope or heading accessors must clamp both sample points. +10. **Multi-band octave tables for tiling.** Two-band ratios like `hf/lf` are blind to regular mid-frequency patterns; inspect multi-octave energy tables to detect periodic tiling. +11. **Physical discrimination over naming.** A class-discriminating physical observation (such as missing shadow terminators) outranks code comments or variable names. +12. **Grazing light terminator crossing.** Under low-angle grazing lighting, prioritize terminator-crossing fractions over slope RMS to detect normal map over-amplification. +13. **Resolution scaling.** High-frequency energy (`hf/lf`) and relative contrast scale with resolution; compare them only at equal pixel resolutions. +14. **Paired capture isolation.** Snapshot source files or freeze environment state when capturing before/after pairs so background modifications cannot corrupt the comparison. + +## Be honest about tooling + +The richest observation tooling lives behind the editor MCP (`flutter_scene_mcp`): screenshots, console, NaN scans, render-graph capture, per-pass and per-pixel readback, viewport debug modes. A general-purpose observation server for an arbitrary running game is still being built, so do not assume those tools exist for every project. When the MCP is not connected, the baseline loop is still fully usable: `flutter run --enable-flutter-gpu`, read the console, take a screenshot. Do not claim a capability you cannot reach in the current project. + +## More depth + +- `references/loop.md` for the full editor-MCP tool table, the symptom to action map (black frame, washed-out, see-through, missing geometry), and the settle details. +- The `flutter_scene-idioms` skill (`references/traps.md`) for the underlying mistakes each symptom points back to (wrong vertex layout, hand-rolled winding flip, transform-in-place, the blank-frame causes). diff --git a/.agents/skills/flutter_scene-verification-loop/references/loop.md b/.agents/skills/flutter_scene-verification-loop/references/loop.md new file mode 100644 index 00000000..0ed9ae6d --- /dev/null +++ b/.agents/skills/flutter_scene-verification-loop/references/loop.md @@ -0,0 +1,161 @@ +# The verification loop in detail + +The core loop, the readiness gate, and the blind-judgment rule are in `SKILL.md`. This file has the +tool table (what exists where), the settle details, and the symptom to action map. + +--- + +## Two tooling tiers + +### Baseline (any project, no MCP) + +This always works and needs nothing installed beyond the package setup. + +- Launch: `flutter run --enable-flutter-gpu` (native; add `-d chrome` for web). The flag is mandatory. +- Console: read the run log. The readiness line, `debugPrint` output, asserts, and the 0.22.0 + blank-frame diagnostic all land here. +- Frame: take a screenshot of the running app after it settles. + +That is the whole loop when there is no editor. Run, settle, screenshot, read the log, correct. + +### Editor MCP (`flutter_scene_mcp`, when connected) + +The editor exposes richer observation. Tool names below are exact. Do not assume they exist unless +the MCP is actually connected for the current project. + +| Tool | What it does | Reach for it when | +| --- | --- | --- | +| `run_project` | Launch the editor-managed Play session (a managed `flutter run`). | Starting a session under the editor. | +| `build_project` | Start the selected build config; output streams to the console. | You want a build without launching. | +| `stop_project` | Stop the running session. | Ending or restarting cleanly. | +| `hot_reload` | Hot reload the running debug session. | A Dart-only change, fastest turnaround. | +| `hot_restart` | Hot restart the session. | State or startup changed, or reload did not take. | +| `get_console` | The build/run console tail plus building/running flags. | EVERY iteration, paired with a screenshot. | +| `screenshot_viewport` | The viewport as a PNG, what the user sees. | EVERY iteration, paired with the console. | +| `describe_scene` | The scene-graph tree (ids, paths, names, component types). | Confirming a node/mesh is actually in the scene. | +| `scan_for_nans` | Capture a frame and scan every float render target for NaN/Inf in pass order. | A black or garbage frame with no error. Find where non-finite values start. | +| `scan_for_depth_conflicts` | Probe the viewport camera for surfaces that trade pixels as the camera moves (z-fighting) and list the node pairs, pixel counts, and distances. | Stripes, speckles, or a surface that looks right only from some angles. | +| `capture_render_graph` | Capture the next frame's graph with thumbnails. | You need to see intermediate buffers. | +| `list_render_passes` | The executed passes in order with CPU timings and the buffer keys each read/wrote, plus target formats and sizes. No images. | Learning which pass owns which buffer, and the key names to read. | +| `get_pass_output` | Render one captured buffer (a key like `scene_color`, `linear_depth`) as a PNG. NaN paints magenta, Inf yellow, negative blue. | Eyeballing an intermediate buffer to see which stage broke. | +| `read_pass_pixel` | One pixel's exact float RGBA from a captured buffer, with NaN/Inf flags. | Confirming an exact value (is this really 0, or NaN, or negative). | +| `list_viewport_debug_modes` | The available debug outputs (final, HDR color, linear depth, normals, AO, shadow atlas, ...) and which is active. | Seeing what debug views exist. | +| `set_viewport_debug_mode` | Render one debug output full-viewport. Set `final` to restore. | Inspecting depth/normals/AO live, paired with `screenshot_viewport`. | + +Render-graph capture (`capture_render_graph`, `list_render_passes`, `get_pass_output`, +`read_pass_pixel`, `scan_for_nans`) is gated on `Scene.debugAllowRenderGraphCapture`. It is a debug +opt-in, so a release build or a scene that never armed it returns nothing. The editor arms it for you; +outside the editor, set `Scene.debugAllowRenderGraphCapture = true` and call +`Scene.captureRenderGraph(...)` directly. + +--- + +## Settle, do not seed + +Frames differ from run to run for benign reasons. That is normal, not a bug to eliminate. + +- **Auto-exposure** (`Scene.autoExposure`) ramps toward the target over `speedUp`/`speedDown` seconds, + so the first second is darker or brighter than the settled image. +- **Particles and trails** carry a random phase, so a `ParticleSystem` looks different every launch. +- **Animations** are mid-clip unless you seek them, so a screenshot lands on an arbitrary frame. +- **Image-based lighting** re-bakes after the first present on some paths, so reflections dim in for + a frame before they are correct. + +So let the scene settle before you trust a capture. Watch until the image stops changing, or advance +a fixed few frames, then screenshot. Judge the settled frame, not the first one. + +**Seeding is a different job.** Strict determinism (a fixed random seed, a pinned animation time, a +frozen exposure) is what you set up for pixel-exact regression comparison, where two runs must be +byte-identical. You do not need it for ordinary observation. For "does this change look right", settle +and look. Reserve the seeding work for when you are building a golden or diffing two runs at the pixel +level. + +--- + +## Symptom to action map + +Localize before editing. Each row says what to capture first and the mistakes it usually points back +to. The mistakes are detailed in the `flutter_scene-idioms` skill's `references/traps.md`; this map +routes a symptom to the right one. + +### Entirely black frame + +1. Read the console FIRST. If `Flutter Scene is not ready to render. Skipping frame.` is printing, it + is the readiness gate, not your scene. Wait for `Scene.initializeStaticResources()`. Stop here. +2. In 0.22.0 a frame that issues zero draws prints once in debug naming the likely cause (not ready, + empty region, no views, no visible meshes, or a layer mask matching nothing). Read that line. +3. If draws are happening but the image is black, `scan_for_nans`. A NaN or Inf anywhere upstream + collapses the final image to black, and the scan names the first offending pass. Then + `get_pass_output` on that pass's buffer (NaN shows magenta) to confirm. +4. Common non-NaN causes: a degenerate camera (target equals position, `up` parallel to the view + direction on a top-down camera, FOV passed in degrees not radians), `layerMask: 0`, an oversized + environment texture that failed to allocate on the device. See traps #24 and #17. + +### Washed-out, low-contrast, or too-bright color + +1. `screenshot_viewport` after settling, and check whether auto-exposure has finished ramping (a + too-bright first second is just the ramp). +2. If it persists, suspect a shader-output contract break. A custom `ShaderMaterial`/`PostEffect`/sky + shader must output linear HDR premultiplied by alpha. Tone-mapping or gamma-encoding in the shader + gets applied a second time by the resolve pass, giving exactly this washed-out look. See traps #38 + and the root `MATERIALS.md`. +3. Also check for a non-color texture bound as color (a normal or metallic-roughness map without the + right `TextureContent`), which reads wrong and distance-dependent. Trap #3. +4. Hand-packed vertex data at the wrong stride also washes out color (the color attribute lands at the + wrong offset). `describe_scene` plus trap #18. + +### See-through or inside-out faces + +1. `set_viewport_debug_mode` to normals (or `get_pass_output` on the normals buffer) and look at the + orientation. Inverted normals confirm a winding problem. +2. Cause is almost always clockwise hand-built triangles. flutter_scene front faces wind + COUNTER-CLOCKWISE (CCW) in model space, matching glTF and standard conventions. Ensure triangle + indices wind CCW around the outward face normal, or omit normals and let the constructor derive + them. NEVER fix orientation with a per-triangle winding flip on an imported model; that leaves + normals and IBL wrong. Traps #14 and #18. +3. For an imported model rendered mirrored, check you did not overwrite the runtime importer's + `scale(1, 1, -1)` handedness root. Trap #6. + +### Missing or popping geometry + +1. `describe_scene` to confirm the node is actually in the graph. If it is absent, it is a scene-build + bug, not a render bug. +2. If it is present but invisible, check the layer mask (`Node.layers` is a bitmask, NOT inherited, + and must match the view's `layerMask`; `layers = 2` means `1 << 1`, not "layer 2"). Trap #11. +3. If it appears and disappears with camera angle, the bounds do not cover the geometry (a + caller-supplied `bounds` or `setLocalBounds` that is too small, or a swapped primitive geometry on + an older version). Widen or omit the bounds. Traps #9 and #25. +4. A moved skinned mesh that will not move is the skinned-node transform being ignored by design; move + the skeleton root instead. Trap #5. + +### Flickering stripes or speckles where surfaces meet + +1. Do not judge this from one frame. Run `scan_for_depth_conflicts` (editor MCP) or `await + scene.probeDepthConflicts(camera: camera)` in the app; each conflict names two nodes, the pixels + they trade, and the distance. `scene.debug.overlays.add(DebugOverlay.depthConflicts)` marks them + live in magenta. +2. Two faces overlapping in one plane (repeated pieces longer than their spacing, overlapping patches + at one height, a duplicated node) fight at any distance; fix the content. `scene.findCoplanarOverlaps()` + lists the overlapping faces with the length that fixes a repeated piece. +3. An overlay meant to lie on a surface (a screen, a sign, road paint) gets `material.depthLayer = 1`. +4. Faces a few centimetres apart that fight only far away need a larger gap or a layer. The + `depthGap` debug view shows the gap each pixel needs. See the idioms skill's + `references/depth-and-layering.md`. +5. Re-probe until the conflict list is empty. + +### A value looks numerically wrong (not visually) + +Use `read_pass_pixel` on the relevant buffer to read the exact float RGBA at a coordinate, with NaN/Inf +flags. This settles "is this pixel actually 0.5, or is it NaN, or negative" without eyeballing a PNG +that the display remap has already clamped. + +--- + +## Judgment, restated + +The blind-pairwise rule from `SKILL.md` is the part most likely to be skipped, so it bears repeating +here. When you have a before and an after, put them side by side and pick the better one as a binary +A-or-B choice against a reference or the previous frame. Do not narrate a score. A solo score climbs +on its own because you are grading your own progress; a blind pick between two concrete frames does +not. Every visual review runs through a pairwise pick, including the ones that feel too obvious to +bother with. diff --git a/.agents/skills/serverpod-auth/references/setup.md b/.agents/skills/serverpod-auth/references/setup.md index afd721af..bf6d5c8e 100644 --- a/.agents/skills/serverpod-auth/references/setup.md +++ b/.agents/skills/serverpod-auth/references/setup.md @@ -45,6 +45,18 @@ client = Client(serverUrl) client.auth.initialize(); ``` +## Passwordless email + +To sign in with a one-time password sent by email, extend `EmailOtpIdpBaseEndpoint` instead of `EmailIdpBaseEndpoint` on the server. `SignInWidget` then shows `EmailOtpSignInWidget`, which can also be used on its own. + +The codes are sent through `EmailIdpConfig.sendOtpLoginVerificationCode`, falling back to `sendRegistrationVerificationCode` when it is not set. Set the OTP callback if the registration emails use `accountRequestId`, e.g. in links. + +The code length is not sent to the client, so configure it on both sides. The server generates 6 digit codes by default (`EmailIdpConfig.otpLoginVerificationCodeGenerator`), and the widget's code input defaults to 6 digits too (`EmailOtpSignInWidget(otpVerificationCodeConfig: VerificationCodeConfig(length: ...))`). Change both together. + +Customize the texts with `SignInLocalizationProvider(emailWithOtp: EmailWithOtpSignInTexts.defaults.copyWith(...))`. + +Customize the resend-code text shared by both email flows with `SignInLocalizationProvider(verificationCodeTexts: VerificationCodeTexts.defaults.copyWith(resendCode: ...))`. + ## Social sign-ins Each provider needs the user to configure it outside the project (e.g. the GCP console or the Apple developer portal). Follow the official setup guide: diff --git a/.agents/skills/serverpod-configuration/SKILL.md b/.agents/skills/serverpod-configuration/SKILL.md index ce7f24b5..e71bb72e 100644 --- a/.agents/skills/serverpod-configuration/SKILL.md +++ b/.agents/skills/serverpod-configuration/SKILL.md @@ -21,7 +21,9 @@ apiServer: publicScheme: http ``` -`publicHost`/`publicPort`/`publicScheme` are used for URLs returned to clients. +`port` is the port the listener binds. `publicHost`/`publicPort`/`publicScheme` are used for URLs returned to clients; a `publicPort` of 0 follows the bound port. + +In development the Serverpod runner keeps the configured ports unless another runner on the machine holds them, in which case it binds ephemeral ports (through `SERVERPOD_API_SERVER_PORT`, its siblings and `SERVERPOD_DATABASE_PORT`; the `_PUBLIC_PORT` variables of the api, insights and web listeners carry the same value, so URLs returned to clients use the bound port) and publishes the real addresses in `serverpod runner status`. See [Several servers at once](../serverpod-overview/SKILL.md#several-servers-at-once) in the overview skill. ## Database @@ -40,9 +42,9 @@ database: Password in `passwords.yaml` under run mode or `SERVERPOD_PASSWORD_database`. Optional: `searchPaths`, `maxConnectionCount`, `requireSsl`, `isUnixSocket`. -**Embedded PostgreSQL:** with `dataPath` set, Serverpod launches and manages a PostgreSQL instance in that directory, so nothing has to be installed or run separately. New projects use it for development and testing. `serverpod database start [--mode ] [--port ]` boots the same database on its own, without the server. +**Embedded PostgreSQL:** with `dataPath` set, Serverpod launches and manages a PostgreSQL instance in that directory, so nothing has to be installed or run separately. New projects use it for development and testing. It listens on its Unix socket, plus loopback TCP on `port` when the run mode has a database password. In development the runner moves that port aside with the server ports when another Serverpod runner holds it, so several checkouts can run their own at once. `serverpod database start [--mode ] [--port ]` boots the same database on its own, without the server, or joins the one the server already runs. -Without `dataPath` the database is external, and `serverpod start` starts Docker Compose services when the project has a compose file and PostgreSQL is on localhost (`--docker` / `--no-docker` override this). +Without `dataPath` the database is external, and the runner (`serverpod start`) starts Docker Compose services when the project has a compose file and PostgreSQL is on localhost (`--docker` / `--no-docker` override this). ### SQLite diff --git a/.agents/skills/serverpod-endpoints/SKILL.md b/.agents/skills/serverpod-endpoints/SKILL.md index 3055cebb..f5566edb 100644 --- a/.agents/skills/serverpod-endpoints/SKILL.md +++ b/.agents/skills/serverpod-endpoints/SKILL.md @@ -5,7 +5,7 @@ description: Define Serverpod endpoints, use Session, pass parameters, and call # Serverpod Endpoints -Extend `Endpoint` with instance methods; first parameter is `Session`, return `Future` (or `Stream` for real-time data streaming). Place anywhere under server `lib/`. If `serverpod start` is not running with hot-reload, run `serverpod generate` to update the client. +Extend `Endpoint` with instance methods; first parameter is `Session`, return `Future` (or `Stream` for real-time data streaming). Place anywhere under server `lib/`. The Serverpod runner regenerates the client on save; without one, run `serverpod generate`. ## Defining an endpoint diff --git a/.agents/skills/serverpod-migrations/SKILL.md b/.agents/skills/serverpod-migrations/SKILL.md index 3308ce0e..75440742 100644 --- a/.agents/skills/serverpod-migrations/SKILL.md +++ b/.agents/skills/serverpod-migrations/SKILL.md @@ -7,7 +7,7 @@ description: Serverpod database migrations — when and how to create/apply/repa Serverpod has a migration system that generates SQL for changes to models with `table` in `.spy.yaml`. The migrations are applied: -- When the `apply_migrations` tool is called via the `serverpod` MCP. Typically during development with a running `serverpod start`. +- When the `apply_migrations` tool is called via the `serverpod` MCP. Typically during development with the Serverpod runner running (`serverpod start` or `serverpod runner start`). - When the server is started with `dart run bin/main.dart --apply-migrations` flag. Typically when running the server in production. ## When migrations are needed @@ -20,20 +20,20 @@ Migrations are not needed when the project has no database configured (e.g. `con ## Standard flow -The standard flow for creating and applying migrations is simplified when a `serverpod start` is running. +The standard flow for creating and applying migrations is simplified when the runner is running. -### With a running `serverpod start` and MCP server +### With a running runner and MCP server -When the server is running from `serverpod start` use the `serverpod` MCP to: +When the server is running under the runner use the `serverpod` MCP to: 1. Create a migration using the `create_migration` tool. 2. Apply the migration using the `apply_migrations` tool. -ALWAYS use the MCP server if it is available. +ALWAYS use the MCP server if it is available. When it reports that the server is not running, call `tail_runner_logs`, then run `serverpod runner start` in the server package when no runner is running, then call the tools again. -### ONLY if MCP server fails to connect +### ONLY if the MCP is not available or the runner cannot bring the server up -When the server is not running from `serverpod start` use the CLI commands to: +When the MCP is not available or `serverpod runner start` fails, use the CLI commands to: 1. Ensure the code is generated by running `serverpod generate`. 2. Create a migration using the `serverpod create-migration` command. @@ -60,14 +60,14 @@ On the client side (models with `database: client` or `database: all`), the same If the database is in an inconsistent state, a repair migration brings it back to a consistent state. It is created by reading the live schema and diffing it against a target migration version, so the database must be reachable. -### With a running `serverpod start` and MCP server +### With a running runner and MCP server 1. Create the repair migration using the `create_repair_migration` tool. Optional arguments: `version` (target migration version, defaults to the latest), `tag`, and `force` (required for destructive changes, or when no drift is detected). 2. Apply it using the `apply_migrations` tool, which applies both pending and repair migrations without restarting the server. -ALWAYS use the MCP server if it is available. +ALWAYS use the MCP server if it is available. When it reports that the server is not running, call `tail_runner_logs`, then run `serverpod runner start` when no runner is running, then call the tools again. -### ONLY if MCP server fails to connect +### ONLY if the MCP is not available or the runner cannot bring the server up ```bash # Use the `--mode` flag to specify the run mode diff --git a/.agents/skills/serverpod-models/SKILL.md b/.agents/skills/serverpod-models/SKILL.md index 37f3409f..5489e98e 100644 --- a/.agents/skills/serverpod-models/SKILL.md +++ b/.agents/skills/serverpod-models/SKILL.md @@ -7,7 +7,7 @@ description: Define Serverpod data models in YAML (.spy.yaml) — fields, defaul Models are defined in `.spy.yaml` files anywhere under server `lib/`. They generate Dart classes for server and client, and optionally database tables. -After each change to models, ensure that the code is generated (automatically when a `serverpod start` is running or manually with `serverpod generate`). If models with `table` have changed, the database schema must be updated following the [migration workflow](../serverpod-migrations/SKILL.md). +After each change to models, ensure that the code is generated (automatically when the Serverpod runner is running or manually with `serverpod generate`). If models with `table` have changed, the database schema must be updated following the [migration workflow](../serverpod-migrations/SKILL.md). ## Basic class diff --git a/.agents/skills/serverpod-overview/SKILL.md b/.agents/skills/serverpod-overview/SKILL.md index 9059df89..a08451bf 100644 --- a/.agents/skills/serverpod-overview/SKILL.md +++ b/.agents/skills/serverpod-overview/SKILL.md @@ -23,27 +23,52 @@ Each of these feature areas has its own `serverpod-*` skill; use the one that ma ## Running the server -The user runs the server with `serverpod start`, which watches for file changes to run incremental code generation and hot reload the server (and the Flutter app, when the project has one). +The development stack is run by the Serverpod runner, one background process per server package. It generates code, compiles the server with hot reload, launches the companion Flutter apps and serves the `serverpod` MCP. While it runs it watches for file changes to run incremental code generation and hot reload the server (and the Flutter app, when the project has one). -Do not check whether the server is running before acting: make the changes and call the `serverpod` MCP tools. When nothing is running, the MCP answers with an explicit "the server is not running" error. Only then, stop and ask the user to start it. NEVER start the server yourself. +- `serverpod start` ensures a runner is running, spawning one when none is, and attaches to it: the interactive terminal UI on a terminal, a plain log stream otherwise or with `--no-tui`. It blocks until the user leaves. Leaving a `start` that spawned the runner stops it; leaving one that attached to a runner already running keeps it running. This is what the user runs. `serverpod start --no-attach` behaves as `serverpod runner start`. +- `serverpod runner start` ensures a runner is running the same way, prints its addresses and returns, leaving it running in the background. This is how you start it. A cold start compiles the whole project and can take minutes. Companion Flutter apps declared with `auto_launch` launch once a UI attaches; launch any app before that with the MCP tool `spawn_flutter_app`. +- `serverpod runner attach` attaches a UI to a runner already running, the same way `serverpod start` does, and never spawns one. Leaving it keeps the runner running. +- `serverpod runner status` prints whether the runner is starting, running, stopping or up with a project that failed to build, the API, Insights and web URLs and the log path. +- `serverpod runner stop` stops the runner. -ALWAYS use the MCP server instead of the command line. A running `serverpod start` exposes: +All of them resolve the server package from the current directory, or take `--directory ` (`-d`). + +Do not check whether the server is running before acting: make the changes and call the `serverpod` MCP tools. When nothing is running, the MCP answers with an explicit "the server is not running" error. Then: + +1. Call `tail_runner_logs` to learn whether a runner is still starting or why it stopped. +2. If none is running, run `serverpod runner start` in the server package. It returns zero once the server is up. It returns non-zero when the project does not build (the runner stays up and, with watch on, the default, starts the server once a change makes it build), when a configured port is held by something other than a Serverpod runner, or when Docker refused. Fix the cause instead of retrying. The one exception is a `serverpod runner start` your shell killed on a timeout before it returned: the runner keeps starting in the background, and running the command again waits for it. +3. Call the MCP tool again. The MCP reconnects by itself. + +Leave the runner running when you are done; the user attaches to it with `serverpod start`. Run `serverpod runner stop` only when asked. Starting the stack with `--watch`, `--target`, `--flutter`, `--docker` or server arguments that differ from the running runner's fails and names the difference; stop that runner first. + +ALWAYS use the MCP server instead of the command line for the tasks below. The `serverpod runner` commands are the exception: they manage the stack itself. A running runner exposes: - `create_migration` and `apply_migrations` for the database (after you change data models). - `create_repair_migration` when the database has drifted out of sync with the migrations. It only writes the repair file; follow up with `apply_migrations`. - `tail_server_logs` to read logs from the server. +- `tail_runner_logs` to read the output of the runner itself (code generation, compilation, Docker, raw server output). The only tool that also works while the server is starting or after it stopped. - `tail_flutter_logs` to read raw stdout/stderr from a Flutter app. -- `hot_reload` to reload the server and the Flutter app while keeping in-memory state. Only needed with `--no-watch`, since `serverpod start` reloads on file changes. +- `hot_reload` to reload the server and the Flutter app while keeping in-memory state. Only needed with `--no-watch`, since the runner reloads on file changes. - `hot_restart` to restart the server and the Flutter app, dropping in-memory state. ALWAYS call it after doing changes in the Flutter app that may not work with normal hot reload (which is automatically applied). - `spawn_flutter_app` to start a Flutter app declared under `serverpod: flutter_apps:` in the server `pubspec.yaml`. - `get_flutter_app_dtd` to get the Dart Tooling Daemon URI of a running Flutter app. Pass it to the `dart` MCP to drive the app. Tools that target a Flutter app take an optional `appId`, which is the map key under `serverpod: flutter_apps:`. It is required only when the project declares more than one app. +## Several servers at once + +Each server package has exactly one runner, keyed by the package directory: its manifest, lock, log and sockets live in `/.dart_tool/serverpod/`, and every runner registers itself under `~/.serverpod/runners/`. So a workspace with several server packages, and several checkouts or worktrees of the same project, each run their own runner. Start each with `serverpod runner start --directory `. An MCP bridge serves one server package, so the agent configuration needs one `serverpod mcp-server --server-dir ` entry per server. The bridge serves the package its `--server-dir` names, not the directory you are in, and says which in its instructions and errors. Check that before applying migrations or restarting from another checkout or worktree. + +The packages usually share a `config/development.yaml` with the same ports (8080, 8081, 8082). The first runner binds them. A runner that finds them claimed by another runner moves all of its listeners to ephemeral ports together, and keeps those ports across its own restarts while they stay free. Never assume `localhost:8080`: read the addresses from `serverpod runner status` or from what `serverpod runner start` printed. Flutter apps the runner launches receive the right URL through `--dart-define=SERVER_URL=...`; an app started by hand, a browser tab or `curl` must use the address from `status`. + +A configured port held by anything other than a Serverpod runner, such as a server started by hand with `dart run bin/main.dart`, is an error: stop that process or change the port in `config/development.yaml`. + +The embedded PostgreSQL (`database.dataPath`) is per checkout. It listens on its Unix socket, and on loopback TCP at `database.port` when the run mode has a database password. That port is a fourth listener: it moves aside with the server ports, and `status` prints the database address. A database this checkout already runs, started by `serverpod database start`, is joined rather than moved. Only a project that uses the Docker Compose database shares host ports, and the compose project is named after the server directory, so two checkouts of such a project would share one set of containers. Start a checkout that uses this shared Compose stack only when no other checkout is using it. Otherwise, leave the existing stack running and do not start this checkout's runner unless the user explicitly asks to switch checkouts. + ## Working on the project with no running instance -- NEVER use the CLI unless you have already attempted to use the MCP. -- ONLY if you cannot connect to the MCP server, the code can be generated by calling `serverpod generate`. +- NEVER run `serverpod generate` or `serverpod create-migration` while the MCP is available: the runner generates code on save and the MCP creates migrations. The `serverpod runner` commands manage the stack itself and are always allowed. +- ONLY when the MCP is not available or `serverpod runner start` could not bring the server up, generate the code by calling `serverpod generate`. - NEVER edit the generated code, as it will be overwritten by the next generation. This does not cover the SQL of a created migration, which may be edited — see the [Serverpod Migrations](../serverpod-migrations/SKILL.md) skill. After generating the code, database migrations can be created by calling `serverpod create-migration`. Use ONLY if you cannot use the MCP. diff --git a/.agents/skills/serverpod-upgrading/SKILL.md b/.agents/skills/serverpod-upgrading/SKILL.md index 10a3e967..9211cdc7 100644 --- a/.agents/skills/serverpod-upgrading/SKILL.md +++ b/.agents/skills/serverpod-upgrading/SKILL.md @@ -7,7 +7,7 @@ description: Upgrade Serverpod — minor/patch updates, major upgrades (2.x to 3 Requirements for this Serverpod version: Dart 3.12.2+, Flutter 3.44.4+. Check the release notes for the version being installed. -Use the same pinned Serverpod version across all packages. Use the CLI to do the upgrade. Ask the user to start the server with `serverpod start` after the upgrade. NEVER update the CLI tooling, instead STOP and ask the user to do it. +Use the same pinned Serverpod version across all packages. Use the CLI to do the upgrade. Restart the runner after the upgrade: `serverpod runner stop`, then `serverpod runner start`. NEVER update the CLI tooling, instead STOP and ask the user to do it. 1. Check the latest version of Serverpod: https://pub.dev/packages/serverpod (unless the user has requested a specific version). 2. Run `serverpod version` to verify that the tooling has the correct version. If not, STOP and ask the user to install the correct version (`dart install serverpod_cli` for latest or `dart install serverpod_cli 3.x.x` for specific version). @@ -17,7 +17,7 @@ Use the same pinned Serverpod version across all packages. Use the CLI to do the 6. Run `serverpod create-migration`. 7. Run `dart analyze` in the root of the project and address any issues. 8. Ensure that the Dockerfile uses at least `FROM dart:3.12.2 AS build`. -9. Inform the user that the upgrade is complete and they should start the server with `serverpod start`. +9. Restart the runner with `serverpod runner stop` and `serverpod runner start` in the server package, and inform the user that the upgrade is complete. ## Major upgrade: Serverpod 3.x to 4.0 diff --git a/.agents/skills/shad-overview/SKILL.md b/.agents/skills/shad-overview/SKILL.md new file mode 100644 index 00000000..4c8516dd --- /dev/null +++ b/.agents/skills/shad-overview/SKILL.md @@ -0,0 +1,155 @@ +--- +name: shad-overview +description: A Flutter UI library that ports shadcn/ui's components, styles, and theming. Provides accessible, fully customizable widgets (Button, Card, Form, Sidebar, and more) plus theming, layout, and typography guides. Use this skill when building a Flutter UI, implementing a design system, or looking up how to use a specific `shad` component. +--- + +# Shad: shadcn/ui for Flutter + +`shad` ports shadcn/ui's components and design system to Flutter. Every widget is a plain, fully customizable Flutter widget, themed by one `ShadThemeData`. + +Import everything through one file: + +```dart +import 'package:shad/shad.dart'; +``` + +The least obvious part of the API: use `ShadPadding` instead of Flutter's `Padding` inside a `shad`-themed page. It has no plain constructor, only `ShadPadding.all`, `.symmetric`, `.only`, and `.directional`, and their arguments are steps on the theme's spacing scale, not logical pixels — `ShadPadding.symmetric(horizontal: 6, vertical: 4)` is shadcn's `px-6 py-4`, not 6px/4px. See [layout.md](guides/layout.md#padding) for a full example. + +## Guides + +- [Installation](guides/installation.md): add the package, set up `ShadApp`, use a component. +- [Theming](guides/theming.md): color schemes, component theme overrides, radius, focus rings, Material/Cupertino interop. +- [Styles](guides/styles.md): the eight shadcn geometry presets (`vega`, `nova`, `maia`, `lyra`, `mira`, `luma`, `sera`, `rhea`). +- [Typography](guides/typography.md): the text role scale and how to change the font. +- [Layout](guides/layout.md): `ShadRow`, `ShadColumn`, `ShadGap`, `ShadPadding`. +- [Form](guides/form.md): `ShadForm` and the `*FormField` widgets. +- [Responsive](guides/responsive.md): the breakpoint scale, `ShadResponsiveBuilder`, `context.breakpoint`. +- [Portal](guides/portal.md): the overlay primitive behind popovers, selects, and menus. + +## Components + +| Name | Description | Reference | +| :--- | :--- | :--- | +| Accordion | A vertically stacked set of interactive headings that each reveal a section of content. | [accordion.md](components/accordion.md) | +| Alert | Displays a callout for user attention. | [alert.md](components/alert.md) | +| Avatar | An image element with a fallback for representing the user. | [avatar.md](components/avatar.md) | +| Badge | Displays a badge or a component that looks like a badge. | [badge.md](components/badge.md) | +| Breadcrumb | Displays the path to the current resource using a hierarchy of links. | [breadcrumb.md](components/breadcrumb.md) | +| Button | Displays a button or a component that looks like a button. Six variants share one API. | [button.md](components/button.md) | +| Calendar | A component that allows users to select dates: single, multiple, or a range. | [calendar.md](components/calendar.md) | +| Card | Displays a card with title, description, content, and footer. | [card.md](components/card.md) | +| Checkbox | A control that allows the user to toggle between checked and not checked. | [checkbox.md](components/checkbox.md) | +| Collapsible | An interactive component that expands and collapses a panel. | [collapsible.md](components/collapsible.md) | +| Command | A composable command menu with filtering and full keyboard navigation. | [command.md](components/command.md) | +| Context Menu | Displays a menu at the pointer, triggered by a right click or a long press. | [context-menu.md](components/context-menu.md) | +| Data Table | A table with sorting, selection, filtering, and pagination, driven by a controller. | [data-table.md](components/data-table.md) | +| Date Picker | A date picker with the calendar in a popover. | [date-picker.md](components/date-picker.md) | +| Dialog | A modal window overlaid on the page, rendering the content underneath inert. | [dialog.md](components/dialog.md) | +| Empty | A placeholder for an empty state: icon, title, description, and actions. | [empty.md](components/empty.md) | +| Icon Button | A square button holding a single icon. | [icon-button.md](components/icon-button.md) | +| Input | Displays a form input field. | [input.md](components/input.md) | +| Input OTP | A one-time password input with copy-paste support. | [input-otp.md](components/input-otp.md) | +| Kbd | Displays a keyboard key or chord. | [kbd.md](components/kbd.md) | +| Keyboard Toolbar | A toolbar shown above the software keyboard on mobile, with focus navigation and a done button. | [keyboard-toolbar.md](components/keyboard-toolbar.md) | +| Menubar | A visually persistent menu, common in desktop applications. | [menubar.md](components/menubar.md) | +| Pagination | Page navigation with previous and next links, plus a compact variant for toolbars and table footers. | [pagination.md](components/pagination.md) | +| Popover | Displays rich content in a portal, triggered by a button. | [popover.md](components/popover.md) | +| Progress | An indicator that shows how much of a task is complete. | [progress.md](components/progress.md) | +| Radio Group | A set of checkable buttons where only one can be checked at a time. | [radio-group.md](components/radio-group.md) | +| Resizable | Resizable panel groups and layouts, with nesting and drag handles. | [resizable.md](components/resizable.md) | +| Select | Displays a list of options for the user to pick from, triggered by a button. | [select.md](components/select.md) | +| Separator | Visually or semantically separates content. | [separator.md](components/separator.md) | +| Sheet | Extends the dialog to display content that complements the main content of the screen, sliding in from any edge. | [sheet.md](components/sheet.md) | +| Sidebar | A composable, collapsible side navigation panel: icon rail, off-canvas, floating, or inset, with a mobile sheet fallback. | [sidebar.md](components/sidebar.md) | +| Skeleton | A placeholder to show while content is loading. | [skeleton.md](components/skeleton.md) | +| Slider | An input where the user selects a value from within a given range. | [slider.md](components/slider.md) | +| Sonner | An opinionated toast stack: notifications collect and expand on hover. | [sonner.md](components/sonner.md) | +| Spinner | An animated loading indicator. | [spinner.md](components/spinner.md) | +| Switch | A control that allows the user to toggle between checked and not checked. | [switch.md](components/switch.md) | +| Table | A responsive table component with header and footer rows. | [table.md](components/table.md) | +| Tabs | A set of layered sections of content displayed one at a time. | [tabs.md](components/tabs.md) | +| Textarea | A multi-line text input, optionally user-resizable. | [textarea.md](components/textarea.md) | +| Time Picker | A field for entering a time of day. | [time-picker.md](components/time-picker.md) | +| Toast | A succinct message that is displayed temporarily. | [toast.md](components/toast.md) | +| Toggle | A two-state button that can be either on or off. | [toggle.md](components/toggle.md) | +| Toggle Group | A set of two-state buttons that can be toggled on or off. | [toggle-group.md](components/toggle-group.md) | +| Tooltip | A popup that displays information related to an element on hover or focus. | [tooltip.md](components/tooltip.md) | + +Each component page shows the actual example source bundled with the example app, so it cannot drift from the code that runs. + +## Basic setup + +A complete counter app with light and dark theme support: + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return ShadApp( + theme: ShadThemeData( + brightness: Brightness.light, + colorScheme: const ShadZincColorScheme.light(), + ), + darkTheme: ShadThemeData( + brightness: Brightness.dark, + colorScheme: const ShadZincColorScheme.dark(), + ), + themeMode: ThemeMode.system, + home: const CounterPage(), + ); + } +} + +class CounterPage extends StatefulWidget { + const CounterPage({super.key}); + + @override + State createState() => _CounterPageState(); +} + +class _CounterPageState extends State { + int _counter = 0; + + @override + Widget build(BuildContext context) { + final theme = ShadTheme.of(context); + return Scaffold( + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'You have pushed the button this many times:', + style: theme.textTheme.muted, + ), + Text('$_counter', style: theme.textTheme.h1), + const SizedBox(height: 16), + ShadButton( + onPressed: () => setState(() => _counter++), + child: const Icon(LucideIcons.plus), + ), + ], + ), + ), + ); + } +} +``` + +## Packages re-exported by shad + +`shad`'s public API (`package:shad/shad.dart`) re-exports the parts of a few libraries that its own component signatures depend on, so callers do not need a second dependency just to pass an argument `shad` asks for. + +- [flutter_animate](https://pub.dev/packages/flutter_animate): the `effects:` parameter on `ShadPopover`, `ShadSelect`, `ShadAccordion`, `ShadMenubar`, and similar components takes a `List` (aliased as `AnimateEffect`). +- [lucide_icons_flutter](https://pub.dev/packages/lucide_icons_flutter): the icon set used throughout the library and its examples, exposed as `LucideIcons.*`. Browse the icons [here](https://lucide.dev/icons/). +- [two_dimensional_scrollables](https://pub.dev/packages/two_dimensional_scrollables): `ShadTable` builds on `TableView`, so `ShadTableCell` extends its `TableViewCell`. +- [intl](https://pub.dev/packages/intl): `DateFormat` and `NumberFormat` are used by the calendar, date picker, and pagination widgets. diff --git a/.agents/skills/shad-overview/components/accordion.md b/.agents/skills/shad-overview/components/accordion.md new file mode 100644 index 00000000..69d835ca --- /dev/null +++ b/.agents/skills/shad-overview/components/accordion.md @@ -0,0 +1,91 @@ +# Accordion + +A vertically stacked set of interactive headings that each reveal a section of content. + +## Single + +Opening a section closes the previous one. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +const _items = [ + ( + title: 'Is it accessible?', + content: 'Yes. It adheres to the WAI-ARIA design pattern.', + ), + ( + title: 'Is it styled?', + content: + 'Yes. It comes with default styles that match the other ' + "components' aesthetic.", + ), + ( + title: 'Is it animated?', + content: + "Yes. It's animated by default, but you can disable it if " + 'you prefer.', + ), +]; + +class AccordionSingleExample extends StatelessWidget { + const AccordionSingleExample({super.key}); + + @override + Widget build(BuildContext context) { + return ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 600), + child: ShadAccordion<({String content, String title})>( + children: [ + for (final item in _items) + ShadAccordionItem( + value: item, + title: Text(item.title), + child: Text(item.content), + ), + ], + ), + ); + } +} +``` + +## Multiple + +Sections open and close independently. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +const _items = [ + (title: 'First section', content: 'Multiple sections can be open at once.'), + ( + title: 'Second section', + content: 'Opening this one does not close the first.', + ), +]; + +class AccordionMultipleExample extends StatelessWidget { + const AccordionMultipleExample({super.key}); + + @override + Widget build(BuildContext context) { + return ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 600), + child: ShadAccordion<({String content, String title})>.multiple( + children: [ + for (final item in _items) + ShadAccordionItem( + value: item, + title: Text(item.title), + child: Text(item.content), + ), + ], + ), + ); + } +} +``` + diff --git a/.agents/skills/shad-overview/components/alert.md b/.agents/skills/shad-overview/components/alert.md new file mode 100644 index 00000000..0593e303 --- /dev/null +++ b/.agents/skills/shad-overview/components/alert.md @@ -0,0 +1,78 @@ +# Alert + +Displays a callout for user attention. + +## Default + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class AlertDefaultExample extends StatelessWidget { + const AlertDefaultExample({super.key}); + + @override + Widget build(BuildContext context) { + return ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 576), + child: const Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + spacing: 16, + children: [ + ShadAlert( + icon: Icon(LucideIcons.circleCheck), + title: Text('Success! Your changes have been saved'), + description: Text( + 'This is an alert with icon, title and description.', + ), + ), + ShadAlert( + icon: Icon(LucideIcons.popcorn), + title: Text('This Alert has a title and an icon. No description.'), + ), + ShadAlert.destructive( + icon: Icon(LucideIcons.circleAlert), + title: Text('Unable to process your payment.'), + description: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 4, + children: [ + Text('Please verify your billing information and try again.'), + Text('• Check your card details'), + Text('• Ensure sufficient funds'), + Text('• Verify billing address'), + ], + ), + ), + ], + ), + ); + } +} +``` + +## Destructive + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class AlertDestructiveExample extends StatelessWidget { + const AlertDestructiveExample({super.key}); + + @override + Widget build(BuildContext context) { + return ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 576), + child: const ShadAlert.destructive( + icon: Icon(LucideIcons.circleAlert), + title: Text('Error'), + description: Text('Your session has expired. Please log in again.'), + ), + ); + } +} +``` + diff --git a/.agents/skills/shad-overview/components/avatar.md b/.agents/skills/shad-overview/components/avatar.md new file mode 100644 index 00000000..41b22137 --- /dev/null +++ b/.agents/skills/shad-overview/components/avatar.md @@ -0,0 +1,33 @@ +# Avatar + +An image element with a fallback for representing the user. + +## Default + +The placeholder shows while the image loads, or when it fails. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class AvatarDefaultExample extends StatelessWidget { + const AvatarDefaultExample({super.key}); + + @override + Widget build(BuildContext context) { + return const Row( + mainAxisSize: MainAxisSize.min, + spacing: 8, + children: [ + ShadAvatar( + 'https://avatars.githubusercontent.com/u/124599?v=4', + placeholder: Text('CN'), + ), + // The placeholder shows while loading, or if the image fails. + ShadAvatar('', placeholder: Text('AB')), + ], + ); + } +} +``` + diff --git a/.agents/skills/shad-overview/components/badge.md b/.agents/skills/shad-overview/components/badge.md new file mode 100644 index 00000000..5be6961f --- /dev/null +++ b/.agents/skills/shad-overview/components/badge.md @@ -0,0 +1,29 @@ +# Badge + +Displays a badge or a component that looks like a badge. + +## Variants + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class BadgeVariantsExample extends StatelessWidget { + const BadgeVariantsExample({super.key}); + + @override + Widget build(BuildContext context) { + return const Wrap( + spacing: 8, + runSpacing: 8, + children: [ + ShadBadge(child: Text('Primary')), + ShadBadge.secondary(child: Text('Secondary')), + ShadBadge.destructive(child: Text('Destructive')), + ShadBadge.outline(child: Text('Outline')), + ], + ); + } +} +``` + diff --git a/.agents/skills/shad-overview/components/breadcrumb.md b/.agents/skills/shad-overview/components/breadcrumb.md new file mode 100644 index 00000000..5bbd7ead --- /dev/null +++ b/.agents/skills/shad-overview/components/breadcrumb.md @@ -0,0 +1,64 @@ +# Breadcrumb + +Displays the path to the current resource using a hierarchy of links. + +## Default + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class BreadcrumbBasicExample extends StatelessWidget { + const BreadcrumbBasicExample({super.key}); + + @override + Widget build(BuildContext context) { + return ShadBreadcrumb( + children: [ + ShadBreadcrumbLink( + onPressed: () {}, + child: const Text('Home'), + ), + ShadBreadcrumbLink( + onPressed: () {}, + child: const Text('Components'), + ), + const Text('Breadcrumb'), + ], + ); + } +} +``` + +## Collapsed + +An ellipsis stands in for the middle of a deep hierarchy. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class BreadcrumbEllipsisExample extends StatelessWidget { + const BreadcrumbEllipsisExample({super.key}); + + @override + Widget build(BuildContext context) { + return ShadBreadcrumb( + children: [ + ShadBreadcrumbLink( + onPressed: () {}, + child: const Text('Home'), + ), + // Collapses the middle of a deep hierarchy. + const ShadBreadcrumbEllipsis(), + ShadBreadcrumbLink( + onPressed: () {}, + child: const Text('Components'), + ), + const Text('Breadcrumb'), + ], + ); + } +} +``` + diff --git a/.agents/skills/shad-overview/components/button.md b/.agents/skills/shad-overview/components/button.md new file mode 100644 index 00000000..1ab1feb6 --- /dev/null +++ b/.agents/skills/shad-overview/components/button.md @@ -0,0 +1,145 @@ +# Button + +Displays a button or a component that looks like a button. Six variants share one API: pick the one matching the action's weight. + +## Variants + +Primary for the main action, secondary and outline for everything else, destructive for irreversible actions, ghost for toolbars, and link for inline navigation. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class ButtonVariantsExample extends StatelessWidget { + const ButtonVariantsExample({super.key}); + + @override + Widget build(BuildContext context) { + return Wrap( + spacing: 8, + runSpacing: 8, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + ShadButton( + onPressed: () {}, + child: const Text('Primary'), + ), + ShadButton.secondary( + onPressed: () {}, + child: const Text('Secondary'), + ), + ShadButton.destructive( + onPressed: () {}, + child: const Text('Destructive'), + ), + ShadButton.outline( + onPressed: () {}, + child: const Text('Outline'), + ), + ShadButton.ghost( + onPressed: () {}, + child: const Text('Ghost'), + ), + ShadButton.link( + onPressed: () {}, + child: const Text('Link'), + ), + ], + ); + } +} +``` + +## Sizes + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class ButtonSizesExample extends StatelessWidget { + const ButtonSizesExample({super.key}); + + @override + Widget build(BuildContext context) { + return Wrap( + spacing: 8, + runSpacing: 8, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + ShadButton.outline( + size: ShadButtonSize.sm, + onPressed: () {}, + child: const Text('Small'), + ), + ShadButton.outline( + onPressed: () {}, + child: const Text('Regular'), + ), + ShadButton.outline( + size: ShadButtonSize.lg, + onPressed: () {}, + child: const Text('Large'), + ), + ], + ); + } +} +``` + +## With icon + +Use leading and trailing to place icons around the label. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class ButtonIconExample extends StatelessWidget { + const ButtonIconExample({super.key}); + + @override + Widget build(BuildContext context) { + return Wrap( + spacing: 8, + runSpacing: 8, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + ShadButton( + onPressed: () {}, + leading: const Icon(LucideIcons.mail), + child: const Text('Login with Email'), + ), + ShadButton.outline( + onPressed: () {}, + trailing: const Icon(LucideIcons.chevronRight), + child: const Text('Next'), + ), + ], + ); + } +} +``` + +## Loading + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class ButtonLoadingExample extends StatelessWidget { + const ButtonLoadingExample({super.key}); + + @override + Widget build(BuildContext context) { + return ShadButton( + enabled: false, + leading: const SizedBox.square( + dimension: 16, + child: ShadSpinner(), + ), + child: const Text('Please wait'), + ); + } +} +``` + diff --git a/.agents/skills/shad-overview/components/calendar.md b/.agents/skills/shad-overview/components/calendar.md new file mode 100644 index 00000000..61d64fae --- /dev/null +++ b/.agents/skills/shad-overview/components/calendar.md @@ -0,0 +1,51 @@ +# Calendar + +A component that allows users to select dates: single, multiple, or a range. + +## Single + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class CalendarSingleExample extends StatefulWidget { + const CalendarSingleExample({super.key}); + + @override + State createState() => _CalendarSingleExampleState(); +} + +class _CalendarSingleExampleState extends State { + DateTime? selected = DateTime.now(); + + @override + Widget build(BuildContext context) { + return ShadCalendar( + selected: selected, + onChanged: (date) => setState(() => selected = date), + ); + } +} +``` + +## Range + +Constrain the selection with min and max lengths. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class CalendarRangeExample extends StatelessWidget { + const CalendarRangeExample({super.key}); + + @override + Widget build(BuildContext context) { + return ShadCalendar.range( + min: 2, + max: 7, + ); + } +} +``` + diff --git a/.agents/skills/shad-overview/components/card.md b/.agents/skills/shad-overview/components/card.md new file mode 100644 index 00000000..7177c0f7 --- /dev/null +++ b/.agents/skills/shad-overview/components/card.md @@ -0,0 +1,66 @@ +# Card + +Displays a card with title, description, content, and footer. + +## With a form + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +const _frameworks = { + 'next': 'Next.js', + 'react': 'React', + 'astro': 'Astro', + 'nuxt': 'Nuxt.js', +}; + +class CardProjectExample extends StatelessWidget { + const CardProjectExample({super.key}); + + @override + Widget build(BuildContext context) { + return ShadCard( + width: 350, + title: const Text('Create project'), + description: const Text('Deploy your new project in one-click.'), + footer: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + ShadButton.outline( + onPressed: () {}, + child: const Text('Cancel'), + ), + ShadButton( + onPressed: () {}, + child: const Text('Deploy'), + ), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const Text('Name'), + const SizedBox(height: 6), + const ShadInput(placeholder: Text('Name of your project')), + const SizedBox(height: 16), + const Text('Framework'), + const SizedBox(height: 6), + ShadSelect( + placeholder: const Text('Select'), + options: [ + for (final entry in _frameworks.entries) + ShadOption(value: entry.key, child: Text(entry.value)), + ], + selectedOptionBuilder: (context, value) => + Text(_frameworks[value]!), + onChanged: (value) {}, + ), + ], + ), + ); + } +} +``` + diff --git a/.agents/skills/shad-overview/components/checkbox.md b/.agents/skills/shad-overview/components/checkbox.md new file mode 100644 index 00000000..c00151a0 --- /dev/null +++ b/.agents/skills/shad-overview/components/checkbox.md @@ -0,0 +1,55 @@ +# Checkbox + +A control that allows the user to toggle between checked and not checked. + +## With label + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class CheckboxBasicExample extends StatefulWidget { + const CheckboxBasicExample({super.key}); + + @override + State createState() => _CheckboxBasicExampleState(); +} + +class _CheckboxBasicExampleState extends State { + bool value = false; + + @override + Widget build(BuildContext context) { + return ShadCheckbox( + value: value, + onChanged: (v) => setState(() => value = v), + label: const Text('Accept terms and conditions'), + sublabel: const Text( + 'You agree to our Terms of Service and Privacy Policy.', + ), + ); + } +} +``` + +## Disabled + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class CheckboxDisabledExample extends StatelessWidget { + const CheckboxDisabledExample({super.key}); + + @override + Widget build(BuildContext context) { + return ShadCheckbox( + value: true, + enabled: false, + onChanged: (v) {}, + label: const Text('Accept terms and conditions'), + ); + } +} +``` + diff --git a/.agents/skills/shad-overview/components/collapsible.md b/.agents/skills/shad-overview/components/collapsible.md new file mode 100644 index 00000000..8bab379f --- /dev/null +++ b/.agents/skills/shad-overview/components/collapsible.md @@ -0,0 +1,57 @@ +# Collapsible + +An interactive component that expands and collapses a panel. + +## Default + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class CollapsibleBasicExample extends StatelessWidget { + const CollapsibleBasicExample({super.key}); + + @override + Widget build(BuildContext context) { + final theme = ShadTheme.of(context); + return SizedBox( + width: 350, + child: ShadCollapsible( + trigger: (context, open, toggle) => Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Text( + '@peduarte starred 3 repositories', + style: theme.textTheme.small, + overflow: TextOverflow.ellipsis, + ), + ), + ShadIconButton.ghost( + icon: Icon( + open ? LucideIcons.chevronsDownUp : LucideIcons.chevronsUpDown, + ), + semanticLabel: open ? 'Collapse' : 'Expand', + onPressed: toggle, + ), + ], + ), + child: const Padding( + padding: EdgeInsets.only(top: 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + ShadCard(child: Text('@radix-ui/primitives')), + SizedBox(height: 8), + ShadCard(child: Text('@radix-ui/colors')), + SizedBox(height: 8), + ShadCard(child: Text('@stitches/react')), + ], + ), + ), + ), + ); + } +} +``` + diff --git a/.agents/skills/shad-overview/components/command.md b/.agents/skills/shad-overview/components/command.md new file mode 100644 index 00000000..72838d8f --- /dev/null +++ b/.agents/skills/shad-overview/components/command.md @@ -0,0 +1,113 @@ +# Command + +A composable command menu with filtering and full keyboard navigation. + +## Inline + +Arrow keys move the highlight, Enter selects. Keywords let items match terms their label does not contain. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class CommandInlineExample extends StatelessWidget { + const CommandInlineExample({super.key}); + + @override + Widget build(BuildContext context) { + return ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 450), + child: ShadCommand( + autofocus: false, + decoration: const ShadDecoration(shadows: Shadows.md), + placeholder: const Text('Type a command or search…'), + groups: [ + ShadCommandGroup( + heading: 'Suggestions', + items: [ + ShadCommandItem( + label: 'Calendar', + value: 'calendar', + leading: const Icon(LucideIcons.calendar), + onSelected: () {}, + ), + ShadCommandItem( + label: 'Search Emoji', + value: 'emoji', + keywords: const ['smiley', 'face'], + leading: const Icon(LucideIcons.smile), + onSelected: () {}, + ), + ], + ), + ShadCommandGroup( + heading: 'Settings', + items: [ + ShadCommandItem( + label: 'Profile', + value: 'profile', + leading: const Icon(LucideIcons.user), + trailing: const ShadKbd.group(['⌘', 'P']), + onSelected: () {}, + ), + ShadCommandItem( + label: 'Billing', + value: 'billing', + leading: const Icon(LucideIcons.creditCard), + trailing: const ShadKbd.group(['⌘', 'B']), + onSelected: () {}, + ), + ], + ), + ], + ), + ); + } +} +``` + +## Dialog + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class CommandDialogExample extends StatelessWidget { + const CommandDialogExample({super.key}); + + @override + Widget build(BuildContext context) { + return ShadButton.outline( + trailing: const ShadKbd.group(['⌘', 'K']), + child: const Text('Open command palette'), + onPressed: () { + showShadCommandDialog( + context: context, + placeholder: const Text('Type a command or search…'), + groups: [ + ShadCommandGroup( + heading: 'Suggestions', + items: [ + ShadCommandItem( + label: 'Calendar', + value: 'calendar', + leading: const Icon(LucideIcons.calendar), + onSelected: () {}, + ), + ShadCommandItem( + label: 'Sign out', + value: 'signout', + keywords: const ['logout', 'exit'], + leading: const Icon(LucideIcons.logOut), + onSelected: () {}, + ), + ], + ), + ], + ); + }, + ); + } +} +``` + diff --git a/.agents/skills/shad-overview/components/context-menu.md b/.agents/skills/shad-overview/components/context-menu.md new file mode 100644 index 00000000..a70b248f --- /dev/null +++ b/.agents/skills/shad-overview/components/context-menu.md @@ -0,0 +1,57 @@ +# Context Menu + +Displays a menu at the pointer, triggered by a right click or a long press. + +## Default + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class ContextMenuBasicExample extends StatelessWidget { + const ContextMenuBasicExample({super.key}); + + @override + Widget build(BuildContext context) { + final theme = ShadTheme.of(context); + const divider = ShadSeparator.horizontal( + margin: EdgeInsets.symmetric(vertical: 4), + ); + return ShadContextMenuRegion( + constraints: const BoxConstraints(minWidth: 300), + items: const [ + ShadContextMenuItem.inset(child: Text('Back')), + ShadContextMenuItem.inset(enabled: false, child: Text('Forward')), + ShadContextMenuItem.inset(child: Text('Reload')), + ShadContextMenuItem.inset( + trailing: Icon(LucideIcons.chevronRight), + items: [ + ShadContextMenuItem(child: Text('Save Page As...')), + ShadContextMenuItem(child: Text('Create Shortcut...')), + divider, + ShadContextMenuItem(child: Text('Developer Tools')), + ], + child: Text('More Tools'), + ), + divider, + ShadContextMenuItem( + leading: Icon(LucideIcons.check), + child: Text('Show Bookmarks Bar'), + ), + ShadContextMenuItem.inset(child: Text('Show Full URLs')), + ], + child: Container( + width: 300, + height: 160, + alignment: Alignment.center, + decoration: BoxDecoration( + border: Border.all(color: theme.colorScheme.border), + borderRadius: theme.radii.md, + ), + child: const Text('Right click here'), + ), + ); + } +} +``` + diff --git a/.agents/skills/shad-overview/components/data-table.md b/.agents/skills/shad-overview/components/data-table.md new file mode 100644 index 00000000..56a562d8 --- /dev/null +++ b/.agents/skills/shad-overview/components/data-table.md @@ -0,0 +1,149 @@ +# Data Table + +A table with sorting, selection, filtering, and pagination, driven by a controller. + +## Payments + +Tap a column header to sort. A third tap clears the sort. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class Payment { + const Payment(this.id, this.status, this.email, this.amount); + + final String id; + final String status; + final String email; + final double amount; +} + +const _payments = [ + Payment('m5gr84i9', 'success', 'ken99@example.com', 316), + Payment('3u1reuv4', 'success', 'abe45@example.com', 242), + Payment('derv1ws0', 'processing', 'monserrat44@example.com', 837), + Payment('5kma53ae', 'success', 'silas22@example.com', 874), + Payment('bhqecj4p', 'failed', 'carmella@example.com', 721), + Payment('a1b2c3d4', 'processing', 'jason@example.com', 128), + Payment('e5f6g7h8', 'failed', 'nina@example.com', 455), +]; + +class DataTablePaymentsExample extends StatefulWidget { + const DataTablePaymentsExample({super.key}); + + @override + State createState() => + _DataTablePaymentsExampleState(); +} + +class _DataTablePaymentsExampleState extends State { + final controller = ShadDataTableController( + rows: _payments, + pageSize: 5, + ); + + @override + void initState() { + super.initState(); + controller.addListener(_onControllerChanged); + } + + @override + void dispose() { + controller.removeListener(_onControllerChanged); + controller.dispose(); + super.dispose(); + } + + void _onControllerChanged() { + if (mounted) setState(() {}); + } + + @override + Widget build(BuildContext context) { + final theme = ShadTheme.of(context); + final total = controller.filteredRows.length; + final rowHeight = theme.tableTheme.cellHeight ?? 48; + final visibleRowCount = controller.visibleRows.length; + // TableView does not shrink-wrap, so height tracks the pinned header + // plus the rows on the current page. + final tableHeight = visibleRowCount == 0 + ? rowHeight * 3 + : rowHeight * (visibleRowCount + 1); + + return ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 700), + child: SizedBox( + width: double.infinity, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, + children: [ + ShadCard( + padding: EdgeInsets.zero, + clipBehavior: Clip.antiAlias, + child: ShadDataTable( + controller: controller, + selectable: true, + keyOf: (payment) => payment.id, + height: tableHeight, + showPagination: false, + showSelectionCount: false, + columns: [ + ShadDataTableColumn( + id: 'status', + header: 'Status', + extent: const FixedTableSpanExtent(140), + compare: (a, b) => a.status.compareTo(b.status), + cellBuilder: (context, payment) => ShadBadge.raw( + variant: switch (payment.status) { + 'success' => ShadBadgeVariant.primary, + 'failed' => ShadBadgeVariant.destructive, + _ => ShadBadgeVariant.secondary, + }, + child: Text(payment.status), + ), + ), + ShadDataTableColumn( + id: 'email', + header: 'Email', + compare: (a, b) => a.email.compareTo(b.email), + cellBuilder: (context, payment) => + Text(payment.email, overflow: TextOverflow.ellipsis), + ), + ShadDataTableColumn( + id: 'amount', + header: 'Amount', + extent: const FixedTableSpanExtent(120), + alignment: Alignment.centerRight, + compare: (a, b) => a.amount.compareTo(b.amount), + cellBuilder: (context, payment) => + Text('\$${payment.amount.toStringAsFixed(2)}'), + ), + ], + ), + ), + const SizedBox(height: 12), + Row( + children: [ + Text( + '${controller.selectedKeys.length} of $total row(s) selected.', + style: theme.textTheme.muted, + ), + const Spacer(), + ShadPaginationCompact( + page: controller.page, + pageCount: controller.pageCount, + onPageChanged: (page) => controller.page = page, + ), + ], + ), + ], + ), + ), + ); + } +} +``` + diff --git a/.agents/skills/shad-overview/components/date-picker.md b/.agents/skills/shad-overview/components/date-picker.md new file mode 100644 index 00000000..ecf00268 --- /dev/null +++ b/.agents/skills/shad-overview/components/date-picker.md @@ -0,0 +1,36 @@ +# Date Picker + +A date picker with the calendar in a popover. + +## Single + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class DatePickerSingleExample extends StatelessWidget { + const DatePickerSingleExample({super.key}); + + @override + Widget build(BuildContext context) { + return const ShadDatePicker(); + } +} +``` + +## Range + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class DatePickerRangeExample extends StatelessWidget { + const DatePickerRangeExample({super.key}); + + @override + Widget build(BuildContext context) { + return const ShadDatePicker.range(); + } +} +``` + diff --git a/.agents/skills/shad-overview/components/dialog.md b/.agents/skills/shad-overview/components/dialog.md new file mode 100644 index 00000000..1b3e246f --- /dev/null +++ b/.agents/skills/shad-overview/components/dialog.md @@ -0,0 +1,114 @@ +# Dialog + +A modal window overlaid on the page, rendering the content underneath inert. + +## With a form + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +const _profile = [ + (title: 'Name', value: 'Alexandru'), + (title: 'Username', value: 'nank1ro'), +]; + +class DialogFormExample extends StatelessWidget { + const DialogFormExample({super.key}); + + @override + Widget build(BuildContext context) { + final theme = ShadTheme.of(context); + return ShadButton.outline( + child: const Text('Edit Profile'), + onPressed: () { + showShadDialog( + context: context, + builder: (context) => ShadDialog( + title: const Text('Edit Profile'), + description: const Text( + "Make changes to your profile here. Click save when you're " + 'done.', + ), + actions: const [ShadButton(child: Text('Save changes'))], + crossAxisAlignment: CrossAxisAlignment.stretch, + child: Container( + width: 375, + padding: const EdgeInsets.symmetric(vertical: 20), + child: Column( + mainAxisSize: MainAxisSize.min, + spacing: 16, + children: [ + for (final field in _profile) + Row( + children: [ + Expanded( + child: Text( + field.title, + textAlign: TextAlign.end, + style: theme.textTheme.small, + ), + ), + const SizedBox(width: 16), + Expanded( + flex: 3, + child: ShadInput(initialValue: field.value), + ), + ], + ), + ], + ), + ), + ), + ); + }, + ); + } +} +``` + +## Alert dialog + +Interrupts the user with an important question and explicit actions. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class DialogAlertExample extends StatelessWidget { + const DialogAlertExample({super.key}); + + @override + Widget build(BuildContext context) { + return ShadButton.outline( + child: const Text('Delete account'), + onPressed: () { + showShadDialog( + context: context, + builder: (context) => ShadDialog.alert( + title: const Text('Are you absolutely sure?'), + description: const Padding( + padding: EdgeInsets.only(bottom: 8), + child: Text( + 'This action cannot be undone. This will permanently delete ' + 'your account and remove your data from our servers.', + ), + ), + actions: [ + ShadButton.outline( + child: const Text('Cancel'), + onPressed: () => Navigator.of(context).pop(false), + ), + ShadButton( + child: const Text('Continue'), + onPressed: () => Navigator.of(context).pop(true), + ), + ], + ), + ); + }, + ); + } +} +``` + diff --git a/.agents/skills/shad-overview/components/empty.md b/.agents/skills/shad-overview/components/empty.md new file mode 100644 index 00000000..42ea7df2 --- /dev/null +++ b/.agents/skills/shad-overview/components/empty.md @@ -0,0 +1,39 @@ +# Empty + +A placeholder for an empty state: icon, title, description, and actions. + +## Default + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class EmptyBasicExample extends StatelessWidget { + const EmptyBasicExample({super.key}); + + @override + Widget build(BuildContext context) { + return ShadCard( + width: 400, + height: 320, + columnMainAxisSize: MainAxisSize.max, + child: ShadEmpty( + icon: const Icon(LucideIcons.inbox), + title: const Text('No messages'), + description: const Text('Messages you receive will show up here.'), + actions: [ + ShadButton.outline( + onPressed: () {}, + child: const Text('Refresh'), + ), + ShadButton( + onPressed: () {}, + child: const Text('Compose'), + ), + ], + ), + ); + } +} +``` + diff --git a/.agents/skills/shad-overview/components/icon-button.md b/.agents/skills/shad-overview/components/icon-button.md new file mode 100644 index 00000000..bb53ba73 --- /dev/null +++ b/.agents/skills/shad-overview/components/icon-button.md @@ -0,0 +1,45 @@ +# Icon Button + +A square button holding a single icon. + +## Variants + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class IconButtonVariantsExample extends StatelessWidget { + const IconButtonVariantsExample({super.key}); + + @override + Widget build(BuildContext context) { + return Wrap( + spacing: 8, + runSpacing: 8, + children: [ + ShadIconButton( + onPressed: () {}, + icon: const Icon(LucideIcons.rocket), + ), + ShadIconButton.secondary( + onPressed: () {}, + icon: const Icon(LucideIcons.rocket), + ), + ShadIconButton.destructive( + onPressed: () {}, + icon: const Icon(LucideIcons.rocket), + ), + ShadIconButton.outline( + onPressed: () {}, + icon: const Icon(LucideIcons.rocket), + ), + ShadIconButton.ghost( + onPressed: () {}, + icon: const Icon(LucideIcons.rocket), + ), + ], + ); + } +} +``` + diff --git a/.agents/skills/shad-overview/components/input-otp.md b/.agents/skills/shad-overview/components/input-otp.md new file mode 100644 index 00000000..7cc9ea40 --- /dev/null +++ b/.agents/skills/shad-overview/components/input-otp.md @@ -0,0 +1,40 @@ +# Input OTP + +A one-time password input with copy-paste support. + +## Default + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class InputOtpBasicExample extends StatelessWidget { + const InputOtpBasicExample({super.key}); + + @override + Widget build(BuildContext context) { + return ShadInputOTP( + maxLength: 6, + onChanged: (value) {}, + children: const [ + ShadInputOTPGroup( + children: [ + ShadInputOTPSlot(), + ShadInputOTPSlot(), + ShadInputOTPSlot(), + ], + ), + Icon(LucideIcons.dot), + ShadInputOTPGroup( + children: [ + ShadInputOTPSlot(), + ShadInputOTPSlot(), + ShadInputOTPSlot(), + ], + ), + ], + ); + } +} +``` + diff --git a/.agents/skills/shad-overview/components/input.md b/.agents/skills/shad-overview/components/input.md new file mode 100644 index 00000000..b3676688 --- /dev/null +++ b/.agents/skills/shad-overview/components/input.md @@ -0,0 +1,96 @@ +# Input + +Displays a form input field. + +## Default + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class InputBasicExample extends StatelessWidget { + const InputBasicExample({super.key}); + + @override + Widget build(BuildContext context) { + return ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 320), + child: const ShadInput( + placeholder: Text('Email'), + keyboardType: TextInputType.emailAddress, + ), + ); + } +} +``` + +## Password + +Leading and trailing slots hold icons and actions. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class InputPasswordExample extends StatefulWidget { + const InputPasswordExample({super.key}); + + @override + State createState() => _InputPasswordExampleState(); +} + +class _InputPasswordExampleState extends State { + bool obscure = true; + + @override + Widget build(BuildContext context) { + final theme = ShadTheme.of(context); + + return ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 320), + child: ShadInput( + placeholder: const Text('Password'), + obscureText: obscure, + leading: Icon( + LucideIcons.lock, + size: 16, + color: theme.colorScheme.mutedForeground, + ), + trailing: ShadGestureDetector( + cursor: SystemMouseCursors.click, + behavior: HitTestBehavior.opaque, + onTap: () => setState(() => obscure = !obscure), + child: Icon( + obscure ? LucideIcons.eyeOff : LucideIcons.eye, + size: 16, + color: theme.colorScheme.mutedForeground, + ), + ), + ), + ); + } +} +``` + +## Disabled + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class InputDisabledExample extends StatelessWidget { + const InputDisabledExample({super.key}); + + @override + Widget build(BuildContext context) { + return ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 320), + child: const ShadInput( + enabled: false, + placeholder: Text('Email'), + ), + ); + } +} +``` + diff --git a/.agents/skills/shad-overview/components/kbd.md b/.agents/skills/shad-overview/components/kbd.md new file mode 100644 index 00000000..b20da38c --- /dev/null +++ b/.agents/skills/shad-overview/components/kbd.md @@ -0,0 +1,35 @@ +# Kbd + +Displays a keyboard key or chord. + +## Default + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class KbdBasicExample extends StatelessWidget { + const KbdBasicExample({super.key}); + + @override + Widget build(BuildContext context) { + return const Column( + mainAxisSize: MainAxisSize.min, + spacing: 16, + children: [ + ShadKbd('K'), + ShadKbd.group(['⌘', 'K']), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text('Press '), + ShadKbd.group(['Ctrl', 'Shift', 'P']), + Text(' to open the palette'), + ], + ), + ], + ); + } +} +``` + diff --git a/.agents/skills/shad-overview/components/keyboard-toolbar.md b/.agents/skills/shad-overview/components/keyboard-toolbar.md new file mode 100644 index 00000000..6990bfda --- /dev/null +++ b/.agents/skills/shad-overview/components/keyboard-toolbar.md @@ -0,0 +1,29 @@ +# Keyboard Toolbar + +A toolbar shown above the software keyboard on mobile, with focus navigation and a done button. + +## Default + +Run on a mobile device to see the toolbar. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class KeyboardToolbarBasicExample extends StatelessWidget { + const KeyboardToolbarBasicExample({super.key}); + + @override + Widget build(BuildContext context) { + return ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 400), + child: ShadInput( + placeholder: const Text('Focus on mobile to show the toolbar'), + keyboardType: TextInputType.emailAddress, + keyboardToolbarBuilder: (context) => const ShadDefaultKeyboardToolbar(), + ), + ); + } +} +``` + diff --git a/.agents/skills/shad-overview/components/menubar.md b/.agents/skills/shad-overview/components/menubar.md new file mode 100644 index 00000000..6eca8db2 --- /dev/null +++ b/.agents/skills/shad-overview/components/menubar.md @@ -0,0 +1,70 @@ +# Menubar + +A visually persistent menu, common in desktop applications. + +## Default + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class MenubarBasicExample extends StatelessWidget { + const MenubarBasicExample({super.key}); + + @override + Widget build(BuildContext context) { + const divider = ShadSeparator.horizontal( + margin: EdgeInsets.symmetric(vertical: 4), + ); + return ShadMenubar( + items: const [ + ShadMenubarItem( + items: [ + ShadContextMenuItem(child: Text('New Tab')), + ShadContextMenuItem(child: Text('New Window')), + ShadContextMenuItem( + enabled: false, + child: Text('New Incognito Window'), + ), + divider, + ShadContextMenuItem( + trailing: Icon(LucideIcons.chevronRight), + items: [ + ShadContextMenuItem(child: Text('Email Link')), + ShadContextMenuItem(child: Text('Messages')), + ], + child: Text('Share'), + ), + divider, + ShadContextMenuItem(child: Text('Print...')), + ], + child: Text('File'), + ), + ShadMenubarItem( + items: [ + ShadContextMenuItem(child: Text('Undo')), + ShadContextMenuItem(child: Text('Redo')), + divider, + ShadContextMenuItem(child: Text('Cut')), + ShadContextMenuItem(child: Text('Copy')), + ShadContextMenuItem(child: Text('Paste')), + ], + child: Text('Edit'), + ), + ShadMenubarItem( + items: [ + ShadContextMenuItem( + leading: Icon(LucideIcons.check), + child: Text('Always Show Full URLs'), + ), + ShadContextMenuItem.inset(child: Text('Reload')), + ShadContextMenuItem.inset(child: Text('Toggle Full Screen')), + ], + child: Text('View'), + ), + ], + ); + } +} +``` + diff --git a/.agents/skills/shad-overview/components/pagination.md b/.agents/skills/shad-overview/components/pagination.md new file mode 100644 index 00000000..b8b0d1f7 --- /dev/null +++ b/.agents/skills/shad-overview/components/pagination.md @@ -0,0 +1,126 @@ +# Pagination + +Page navigation with previous and next links. [ShadPagination] shows a windowed run of page numbers; [ShadPaginationCompact] fits toolbars and table footers. + +## Default + +Previous and next labels with a windowed run of page numbers. An ellipsis marks pages that were skipped. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class PaginationBasicExample extends StatefulWidget { + const PaginationBasicExample({super.key}); + + @override + State createState() => _PaginationBasicExampleState(); +} + +class _PaginationBasicExampleState extends State { + int page = 5; + + @override + Widget build(BuildContext context) { + return ShadPagination( + page: page, + pageCount: 20, + onPageChanged: (value) => setState(() => page = value), + ); + } +} +``` + +## Simple + +Page numbers only — set `showEdges: false` when every page fits without an ellipsis. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class PaginationSimpleExample extends StatefulWidget { + const PaginationSimpleExample({super.key}); + + @override + State createState() => + _PaginationSimpleExampleState(); +} + +class _PaginationSimpleExampleState extends State { + int page = 2; + + @override + Widget build(BuildContext context) { + return ShadPagination( + page: page, + pageCount: 5, + showEdges: false, + onPageChanged: (value) => setState(() => page = value), + ); + } +} +``` + +## Compact + +Previous and next icon buttons with a `Page n / m` counter, useful beneath a data table. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class PaginationCompactExample extends StatefulWidget { + const PaginationCompactExample({super.key}); + + @override + State createState() => + _PaginationCompactExampleState(); +} + +class _PaginationCompactExampleState extends State { + int page = 3; + + @override + Widget build(BuildContext context) { + return ShadPaginationCompact( + page: page, + pageCount: 10, + onPageChanged: (value) => setState(() => page = value), + ); + } +} +``` + +## Icons only + +Drop the counter with `labelBuilder` when space is tight, for example beside a rows-per-page control. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class PaginationIconsOnlyExample extends StatefulWidget { + const PaginationIconsOnlyExample({super.key}); + + @override + State createState() => + _PaginationIconsOnlyExampleState(); +} + +class _PaginationIconsOnlyExampleState + extends State { + int page = 4; + + @override + Widget build(BuildContext context) { + return ShadPaginationCompact( + page: page, + pageCount: 12, + onPageChanged: (value) => setState(() => page = value), + labelBuilder: (_, _, _) => const SizedBox.shrink(), + ); + } +} +``` + diff --git a/.agents/skills/shad-overview/components/popover.md b/.agents/skills/shad-overview/components/popover.md new file mode 100644 index 00000000..cb481e94 --- /dev/null +++ b/.agents/skills/shad-overview/components/popover.md @@ -0,0 +1,69 @@ +# Popover + +Displays rich content in a portal, triggered by a button. + +## Default + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +const _layer = [ + (name: 'Width', initialValue: '100%'), + (name: 'Max. width', initialValue: '300px'), + (name: 'Height', initialValue: '25px'), + (name: 'Max. height', initialValue: 'none'), +]; + +class PopoverBasicExample extends StatefulWidget { + const PopoverBasicExample({super.key}); + + @override + State createState() => _PopoverBasicExampleState(); +} + +class _PopoverBasicExampleState extends State { + final controller = ShadPopoverController(); + + @override + void dispose() { + controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final textTheme = ShadTheme.of(context).textTheme; + return ShadPopover( + controller: controller, + popover: (_) => SizedBox( + width: 288, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + spacing: 8, + children: [ + Text('Dimensions', style: textTheme.h4), + Text('Set the dimensions for the layer.', style: textTheme.p), + for (final field in _layer) + Row( + children: [ + Expanded(child: Text(field.name)), + Expanded( + flex: 2, + child: ShadInput(initialValue: field.initialValue), + ), + ], + ), + ], + ), + ), + child: ShadButton.outline( + onPressed: controller.toggle, + child: const Text('Open popover'), + ), + ); + } +} +``` + diff --git a/.agents/skills/shad-overview/components/progress.md b/.agents/skills/shad-overview/components/progress.md new file mode 100644 index 00000000..93ad6e7f --- /dev/null +++ b/.agents/skills/shad-overview/components/progress.md @@ -0,0 +1,32 @@ +# Progress + +An indicator that shows how much of a task is complete. + +## Determinate and indeterminate + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class ProgressBasicExample extends StatelessWidget { + const ProgressBasicExample({super.key}); + + @override + Widget build(BuildContext context) { + return ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 400), + child: Column( + mainAxisSize: MainAxisSize.min, + spacing: 24, + children: [ + // Determinate: pass a value between 0 and 1. + ShadProgress(value: .5), + // Indeterminate: leave the value out. + const ShadProgress(), + ], + ), + ); + } +} +``` + diff --git a/.agents/skills/shad-overview/components/radio-group.md b/.agents/skills/shad-overview/components/radio-group.md new file mode 100644 index 00000000..c1cf9f8a --- /dev/null +++ b/.agents/skills/shad-overview/components/radio-group.md @@ -0,0 +1,37 @@ +# Radio Group + +A set of checkable buttons where only one can be checked at a time. + +## Default + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +enum NotifyAbout { + all('All new messages'), + mentions('Direct messages and mentions'), + nothing('Nothing'); + + const NotifyAbout(this.message); + + final String message; +} + +class RadioGroupBasicExample extends StatelessWidget { + const RadioGroupBasicExample({super.key}); + + @override + Widget build(BuildContext context) { + return ShadRadioGroup( + initialValue: NotifyAbout.mentions, + onChanged: (value) {}, + items: [ + for (final option in NotifyAbout.values) + ShadRadio(value: option, label: Text(option.message)), + ], + ); + } +} +``` + diff --git a/.agents/skills/shad-overview/components/resizable.md b/.agents/skills/shad-overview/components/resizable.md new file mode 100644 index 00000000..7047cda4 --- /dev/null +++ b/.agents/skills/shad-overview/components/resizable.md @@ -0,0 +1,64 @@ +# Resizable + +Resizable panel groups and layouts, with nesting and drag handles. + +## Default + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class ResizableBasicExample extends StatelessWidget { + const ResizableBasicExample({super.key}); + + @override + Widget build(BuildContext context) { + final theme = ShadTheme.of(context); + return SizedBox( + width: 300, + height: 200, + child: DecoratedBox( + decoration: BoxDecoration( + borderRadius: theme.radius, + border: Border.all(color: theme.colorScheme.border), + ), + child: ClipRRect( + borderRadius: theme.radius, + child: ShadResizablePanelGroup( + showHandle: true, + children: [ + ShadResizablePanel( + id: 0, + defaultSize: .5, + minSize: .2, + child: const Center(child: Text('One')), + ), + ShadResizablePanel( + id: 1, + defaultSize: .5, + child: ShadResizablePanelGroup( + axis: Axis.vertical, + showHandle: true, + children: [ + ShadResizablePanel( + id: 0, + defaultSize: .5, + child: const Center(child: Text('Two')), + ), + ShadResizablePanel( + id: 1, + defaultSize: .5, + child: const Center(child: Text('Three')), + ), + ], + ), + ), + ], + ), + ), + ), + ); + } +} +``` + diff --git a/.agents/skills/shad-overview/components/select.md b/.agents/skills/shad-overview/components/select.md new file mode 100644 index 00000000..381721f1 --- /dev/null +++ b/.agents/skills/shad-overview/components/select.md @@ -0,0 +1,106 @@ +# Select + +Displays a list of options for the user to pick from, triggered by a button. + +## Default + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +const _fruits = { + 'apple': 'Apple', + 'banana': 'Banana', + 'blueberry': 'Blueberry', + 'grapes': 'Grapes', + 'pineapple': 'Pineapple', +}; + +class SelectBasicExample extends StatelessWidget { + const SelectBasicExample({super.key}); + + @override + Widget build(BuildContext context) { + return ConstrainedBox( + constraints: const BoxConstraints(minWidth: 180), + child: ShadSelect( + placeholder: const Text('Select a fruit'), + options: [ + for (final fruit in _fruits.entries) + ShadOption(value: fruit.key, child: Text(fruit.value)), + ], + selectedOptionBuilder: (context, value) => Text(_fruits[value]!), + onChanged: (value) {}, + ), + ); + } +} +``` + +## With search + +Filter a long list of Dart frameworks and tools — Serverpod, Relic, Flutter, and more. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +const _dartFrameworks = { + 'serverpod': 'Serverpod', + 'relic': 'Relic', + 'flutter': 'Flutter', + 'flame': 'Flame', + 'jaspr': 'Jaspr', + 'drift': 'Drift', + 'riverpod': 'Riverpod', + 'melos': 'Melos', + 'mason': 'Mason', + 'very_good_cli': 'Very Good CLI', +}; + +class SelectSearchExample extends StatefulWidget { + const SelectSearchExample({super.key}); + + @override + State createState() => _SelectSearchExampleState(); +} + +class _SelectSearchExampleState extends State { + var searchValue = ''; + + Map get filtered => { + for (final entry in _dartFrameworks.entries) + if (entry.value.toLowerCase().contains(searchValue.toLowerCase())) + entry.key: entry.value, + }; + + @override + Widget build(BuildContext context) { + return ConstrainedBox( + constraints: const BoxConstraints(minWidth: 180), + child: ShadSelect.withSearch( + minWidth: 180, + placeholder: const Text('Select framework...'), + onSearchChanged: (value) => setState(() => searchValue = value), + searchPlaceholder: const Text('Search framework'), + options: [ + if (filtered.isEmpty) + const Padding( + padding: EdgeInsets.symmetric(vertical: 24), + child: Text('No framework found'), + ), + for (final entry in _dartFrameworks.entries) + Offstage( + offstage: !filtered.containsKey(entry.key), + child: ShadOption(value: entry.key, child: Text(entry.value)), + ), + ], + selectedOptionBuilder: (context, value) => + Text(_dartFrameworks[value]!), + onChanged: (value) {}, + ), + ); + } +} +``` + diff --git a/.agents/skills/shad-overview/components/separator.md b/.agents/skills/shad-overview/components/separator.md new file mode 100644 index 00000000..2520fb8e --- /dev/null +++ b/.agents/skills/shad-overview/components/separator.md @@ -0,0 +1,54 @@ +# Separator + +Visually or semantically separates content. + +## Default + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class SeparatorBasicExample extends StatelessWidget { + const SeparatorBasicExample({super.key}); + + @override + Widget build(BuildContext context) { + final theme = ShadTheme.of(context); + return SizedBox( + width: 300, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Radix Primitives', style: theme.textTheme.small), + const SizedBox(height: 4), + Text( + 'An open-source UI component library.', + style: theme.textTheme.muted, + ), + const ShadSeparator.horizontal( + margin: EdgeInsets.symmetric(vertical: 16), + ), + const SizedBox( + height: 20, + child: Row( + children: [ + Text('Blog'), + ShadSeparator.vertical( + margin: EdgeInsets.symmetric(horizontal: 16), + ), + Text('Docs'), + ShadSeparator.vertical( + margin: EdgeInsets.symmetric(horizontal: 16), + ), + Text('Source'), + ], + ), + ), + ], + ), + ); + } +} +``` + diff --git a/.agents/skills/shad-overview/components/sheet.md b/.agents/skills/shad-overview/components/sheet.md new file mode 100644 index 00000000..28912fd7 --- /dev/null +++ b/.agents/skills/shad-overview/components/sheet.md @@ -0,0 +1,53 @@ +# Sheet + +Extends the dialog to display content that complements the main content of the screen, sliding in from any edge. + +## Sides + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class SheetSidesExample extends StatelessWidget { + const SheetSidesExample({super.key}); + + void _open(BuildContext context, ShadSheetSide side) { + showShadSheet( + context: context, + side: side, + builder: (context) => ShadSheet( + title: const Text('Edit Profile'), + description: const Text( + "Make changes to your profile here. Click save when you're done.", + ), + actions: [ + ShadButton( + child: const Text('Save changes'), + onPressed: () => Navigator.of(context).pop(), + ), + ], + child: const Padding( + padding: EdgeInsets.symmetric(vertical: 20), + child: ShadInput(placeholder: Text('Name')), + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + return Wrap( + spacing: 8, + runSpacing: 8, + children: [ + for (final side in ShadSheetSide.values) + ShadButton.outline( + onPressed: () => _open(context, side), + child: Text(side.name), + ), + ], + ); + } +} +``` + diff --git a/.agents/skills/shad-overview/components/sidebar.md b/.agents/skills/shad-overview/components/sidebar.md new file mode 100644 index 00000000..849494c4 --- /dev/null +++ b/.agents/skills/shad-overview/components/sidebar.md @@ -0,0 +1,209 @@ +# Sidebar + +A composable, collapsible side navigation panel. Collapses to an icon rail or off-canvas, floats or insets, and becomes a modal sheet under the mobile breakpoint. ⌘B toggles it. + +## Icon rail + +With collapsible: icon the sidebar shrinks to a rail of icons. Menu buttons show their label as a tooltip, and with rail: true the inner edge becomes a grab rail — click it to toggle. This docs app itself runs on ShadSidebarScaffold. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class SidebarBasicExample extends StatefulWidget { + const SidebarBasicExample({super.key}); + + @override + State createState() => _SidebarBasicExampleState(); +} + +class _SidebarBasicExampleState extends State { + String selected = 'Home'; + + ShadSidebarMenuButton item(String label, IconData icon) { + return ShadSidebarMenuButton( + leading: Icon(icon), + isActive: selected == label, + tooltip: label, + onPressed: () => setState(() => selected = label), + child: Text(label), + ); + } + + @override + Widget build(BuildContext context) { + final theme = ShadTheme.of(context); + return SizedBox( + width: 700, + height: 420, + child: DecoratedBox( + decoration: BoxDecoration( + border: Border.all(color: theme.colorScheme.border), + borderRadius: theme.radii.lg, + ), + child: ShadSidebarScaffold( + borderRadius: theme.radii.lg, + sidebar: ShadSidebar( + collapsible: ShadSidebarCollapsible.icon, + // The inner edge highlights under the pointer and toggles the + // sidebar on click. + rail: true, + header: Row( + children: [ + Container( + width: 32, + height: 32, + decoration: BoxDecoration( + color: theme.colorScheme.primary, + borderRadius: theme.radii.md, + ), + child: Icon( + LucideIcons.galleryVerticalEnd, + size: 16, + color: theme.colorScheme.primaryForeground, + ), + ), + const SizedBox(width: 8), + Expanded( + child: Text( + 'Acme Inc', + maxLines: 1, + softWrap: false, + overflow: TextOverflow.clip, + style: theme.textTheme.small, + ), + ), + ], + ), + footer: ShadSidebarMenuButton( + leading: const Icon(LucideIcons.circleUser), + trailing: const Icon(LucideIcons.chevronsUpDown), + tooltip: 'Account', + onPressed: () {}, + child: const Text('shadcn'), + ), + children: [ + ShadSidebarGroup( + label: const Text('Platform'), + children: [ + ShadSidebarMenu( + children: [ + item('Home', LucideIcons.house), + item('Inbox', LucideIcons.inbox), + item('Calendar', LucideIcons.calendar), + ], + ), + ], + ), + ShadSidebarGroup( + label: const Text('Settings'), + children: [ + ShadSidebarMenu( + children: [ + item('Profile', LucideIcons.user), + item('Billing', LucideIcons.creditCard), + ], + ), + ], + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(8), + child: Row( + children: [ + const ShadSidebarTrigger(), + const SizedBox(width: 8), + Text(selected, style: theme.textTheme.small), + ], + ), + ), + const Expanded( + child: Center(child: Text('Content area')), + ), + ], + ), + ), + ), + ); + } +} +``` + +## Floating, with sub-menus + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class SidebarSubmenuExample extends StatefulWidget { + const SidebarSubmenuExample({super.key}); + + @override + State createState() => _SidebarSubmenuExampleState(); +} + +class _SidebarSubmenuExampleState extends State { + String selected = 'Installation'; + + static const _sections = { + 'Getting Started': ['Installation', 'Project Structure'], + 'Building Your Application': ['Routing', 'Data Fetching', 'Rendering'], + }; + + @override + Widget build(BuildContext context) { + final theme = ShadTheme.of(context); + return SizedBox( + width: 700, + height: 420, + child: DecoratedBox( + decoration: BoxDecoration( + border: Border.all(color: theme.colorScheme.border), + borderRadius: theme.radii.lg, + ), + child: ShadSidebarScaffold( + borderRadius: theme.radii.lg, + sidebar: ShadSidebar( + variant: ShadSidebarVariant.floating, + children: [ + ShadSidebarGroup( + label: const Text('Documentation'), + children: [ + ShadSidebarMenu( + children: [ + for (final entry in _sections.entries) ...[ + ShadSidebarMenuButton( + leading: const Icon(LucideIcons.bookOpen), + onPressed: () {}, + child: Text(entry.key), + ), + ShadSidebarMenuSub( + children: [ + for (final page in entry.value) + ShadSidebarMenuSubButton( + isActive: selected == page, + onPressed: () => + setState(() => selected = page), + child: Text(page), + ), + ], + ), + ], + ], + ), + ], + ), + ], + ), + child: Center(child: Text(selected)), + ), + ), + ); + } +} +``` + diff --git a/.agents/skills/shad-overview/components/skeleton.md b/.agents/skills/shad-overview/components/skeleton.md new file mode 100644 index 00000000..0874a63a --- /dev/null +++ b/.agents/skills/shad-overview/components/skeleton.md @@ -0,0 +1,35 @@ +# Skeleton + +A placeholder to show while content is loading. + +## Default + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class SkeletonCardExample extends StatelessWidget { + const SkeletonCardExample({super.key}); + + @override + Widget build(BuildContext context) { + return const Row( + mainAxisSize: MainAxisSize.min, + children: [ + ShadSkeleton.circle(size: 48), + SizedBox(width: 12), + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ShadSkeleton(width: 250, height: 16), + SizedBox(height: 8), + ShadSkeleton(width: 200, height: 16), + ], + ), + ], + ); + } +} +``` + diff --git a/.agents/skills/shad-overview/components/slider.md b/.agents/skills/shad-overview/components/slider.md new file mode 100644 index 00000000..29686f50 --- /dev/null +++ b/.agents/skills/shad-overview/components/slider.md @@ -0,0 +1,106 @@ +# Slider + +An input where the user selects a value from within a given range. + +## Default + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class SliderBasicExample extends StatelessWidget { + const SliderBasicExample({super.key}); + + @override + Widget build(BuildContext context) { + return const SizedBox( + width: 300, + child: ShadSlider( + initialValue: 33, + max: 100, + ), + ); + } +} +``` + +## Range + +ShadRangeSlider takes one value per thumb. The values stay in ascending order — a thumb stops where its neighbour is rather than crossing it. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class SliderRangeExample extends StatefulWidget { + const SliderRangeExample({super.key}); + + @override + State createState() => _SliderRangeExampleState(); +} + +class _SliderRangeExampleState extends State { + List values = const [25, 75]; + + @override + Widget build(BuildContext context) { + final theme = ShadTheme.of(context); + return SizedBox( + width: 300, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + spacing: 12, + children: [ + Text( + // The values stay ascending, so the ends of the range are always + // the first and last entry. + '\$${values.first.round()} – \$${values.last.round()}', + style: theme.textTheme.small, + ), + ShadRangeSlider( + initialValues: values, + max: 100, + onChanged: (next) => setState(() => values = next), + ), + ], + ), + ); + } +} +``` + +## Steps + +With divisions the slider snaps; set showDivisionMarks to false to snap without drawing the ticks. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class SliderStepsExample extends StatelessWidget { + const SliderStepsExample({super.key}); + + @override + Widget build(BuildContext context) { + return const SizedBox( + width: 300, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + spacing: 24, + children: [ + ShadSlider(initialValue: 50, max: 100, divisions: 10), + ShadSlider( + initialValue: 50, + max: 100, + divisions: 10, + showDivisionMarks: false, + ), + ], + ), + ); + } +} +``` + diff --git a/.agents/skills/shad-overview/components/sonner.md b/.agents/skills/shad-overview/components/sonner.md new file mode 100644 index 00000000..e8b29722 --- /dev/null +++ b/.agents/skills/shad-overview/components/sonner.md @@ -0,0 +1,39 @@ +# Sonner + +An opinionated toast stack: notifications collect and expand on hover. + +## Default + +```dart +import 'dart:math'; + +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class SonnerBasicExample extends StatelessWidget { + const SonnerBasicExample({super.key}); + + @override + Widget build(BuildContext context) { + return ShadButton.outline( + child: const Text('Show Toast'), + onPressed: () { + final sonner = ShadSonner.of(context); + final id = Random().nextInt(1000); + sonner.show( + ShadToast( + id: id, + title: const Text('Event has been created'), + description: Text('${DateTime.now()}'), + action: ShadButton( + child: const Text('Undo'), + onPressed: () => sonner.hide(id), + ), + ), + ); + }, + ); + } +} +``` + diff --git a/.agents/skills/shad-overview/components/spinner.md b/.agents/skills/shad-overview/components/spinner.md new file mode 100644 index 00000000..c592875f --- /dev/null +++ b/.agents/skills/shad-overview/components/spinner.md @@ -0,0 +1,28 @@ +# Spinner + +An animated loading indicator. + +## Sizes + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class SpinnerBasicExample extends StatelessWidget { + const SpinnerBasicExample({super.key}); + + @override + Widget build(BuildContext context) { + return const Row( + mainAxisSize: MainAxisSize.min, + spacing: 24, + children: [ + ShadSpinner(size: 16), + ShadSpinner(), + ShadSpinner(size: 32), + ], + ); + } +} +``` + diff --git a/.agents/skills/shad-overview/components/switch.md b/.agents/skills/shad-overview/components/switch.md new file mode 100644 index 00000000..bb6e5025 --- /dev/null +++ b/.agents/skills/shad-overview/components/switch.md @@ -0,0 +1,31 @@ +# Switch + +A control that allows the user to toggle between checked and not checked. + +## Default + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class SwitchBasicExample extends StatefulWidget { + const SwitchBasicExample({super.key}); + + @override + State createState() => _SwitchBasicExampleState(); +} + +class _SwitchBasicExampleState extends State { + bool value = false; + + @override + Widget build(BuildContext context) { + return ShadSwitch( + value: value, + onChanged: (v) => setState(() => value = v), + label: const Text('Airplane Mode'), + ); + } +} +``` + diff --git a/.agents/skills/shad-overview/components/table.md b/.agents/skills/shad-overview/components/table.md new file mode 100644 index 00000000..0ad3d197 --- /dev/null +++ b/.agents/skills/shad-overview/components/table.md @@ -0,0 +1,104 @@ +# Table + +A responsive table component with header and footer rows. + +## Invoices + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +const _invoices = [ + ( + invoice: 'INV001', + status: 'Paid', + method: 'Credit Card', + amount: r'$250.00', + ), + (invoice: 'INV002', status: 'Pending', method: 'PayPal', amount: r'$150.00'), + ( + invoice: 'INV003', + status: 'Unpaid', + method: 'Bank Transfer', + amount: r'$350.00', + ), + ( + invoice: 'INV004', + status: 'Paid', + method: 'Credit Card', + amount: r'$450.00', + ), + (invoice: 'INV005', status: 'Paid', method: 'PayPal', amount: r'$550.00'), +]; + +class TableInvoicesExample extends StatelessWidget { + const TableInvoicesExample({super.key}); + + @override + Widget build(BuildContext context) { + final theme = ShadTheme.of(context); + final rowHeight = theme.tableTheme.cellHeight ?? 48; + // Header row + one row per invoice + footer row, so the card hugs the + // table exactly instead of leaving space below the last row. + final tableHeight = rowHeight * (_invoices.length + 2); + + return ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 600), + child: SizedBox( + width: double.infinity, + child: ShadCard( + padding: EdgeInsets.zero, + clipBehavior: Clip.antiAlias, + child: SizedBox( + height: tableHeight, + child: ShadTable.list( + // The Amount column absorbs any leftover width so the table + // spans the full card instead of leaving a gap on the right. + columnSpanExtent: (index) => switch (index) { + 0 => const FixedTableSpanExtent(140), + 1 => const FixedTableSpanExtent(110), + 2 => const FixedTableSpanExtent(140), + _ => const MaxTableSpanExtent( + FixedTableSpanExtent(120), + RemainingTableSpanExtent(), + ), + }, + header: const [ + ShadTableCell.header(child: Text('Invoice')), + ShadTableCell.header(child: Text('Status')), + ShadTableCell.header(child: Text('Method')), + ShadTableCell.header( + alignment: Alignment.centerRight, + child: Text('Amount'), + ), + ], + footer: const [ + ShadTableCell.footer(child: Text('Total')), + ShadTableCell.footer(child: Text('')), + ShadTableCell.footer(child: Text('')), + ShadTableCell.footer( + alignment: Alignment.centerRight, + child: Text(r'$1750.00'), + ), + ], + children: [ + for (final invoice in _invoices) + [ + ShadTableCell(child: Text(invoice.invoice)), + ShadTableCell(child: Text(invoice.status)), + ShadTableCell(child: Text(invoice.method)), + ShadTableCell( + alignment: Alignment.centerRight, + child: Text(invoice.amount), + ), + ], + ], + ), + ), + ), + ), + ); + } +} +``` + diff --git a/.agents/skills/shad-overview/components/tabs.md b/.agents/skills/shad-overview/components/tabs.md new file mode 100644 index 00000000..9c3f1591 --- /dev/null +++ b/.agents/skills/shad-overview/components/tabs.md @@ -0,0 +1,81 @@ +# Tabs + +A set of layered sections of content displayed one at a time. + +## Default + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class TabsBasicExample extends StatelessWidget { + const TabsBasicExample({super.key}); + + @override + Widget build(BuildContext context) { + return ShadTabs( + value: 'account', + tabBarConstraints: const BoxConstraints(maxWidth: 400), + contentConstraints: const BoxConstraints(maxWidth: 400), + tabs: [ + ShadTab( + value: 'account', + content: ShadCard( + title: const Text('Account'), + description: const Text( + "Make changes to your account here. Click save when you're " + 'done.', + ), + footer: const ShadButton(child: Text('Save changes')), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(height: 16), + ShadInputFormField( + label: const Text('Name'), + initialValue: 'Ale', + ), + const SizedBox(height: 8), + ShadInputFormField( + label: const Text('Username'), + initialValue: 'nank1ro', + ), + const SizedBox(height: 16), + ], + ), + ), + child: const Text('Account'), + ), + ShadTab( + value: 'password', + content: ShadCard( + title: const Text('Password'), + description: const Text( + "Change your password here. After saving, you'll be logged " + 'out.', + ), + footer: const ShadButton(child: Text('Save password')), + child: Column( + children: [ + const SizedBox(height: 16), + ShadInputFormField( + label: const Text('Current password'), + obscureText: true, + ), + const SizedBox(height: 8), + ShadInputFormField( + label: const Text('New password'), + obscureText: true, + ), + const SizedBox(height: 16), + ], + ), + ), + child: const Text('Password'), + ), + ], + ); + } +} +``` + diff --git a/.agents/skills/shad-overview/components/textarea.md b/.agents/skills/shad-overview/components/textarea.md new file mode 100644 index 00000000..ac0d0936 --- /dev/null +++ b/.agents/skills/shad-overview/components/textarea.md @@ -0,0 +1,26 @@ +# Textarea + +A multi-line text input, optionally user-resizable. + +## Default + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class TextareaBasicExample extends StatelessWidget { + const TextareaBasicExample({super.key}); + + @override + Widget build(BuildContext context) { + return ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 400), + child: ShadTextarea( + placeholder: const Text('Type your message here...'), + onChanged: (value) {}, + ), + ); + } +} +``` + diff --git a/.agents/skills/shad-overview/components/time-picker.md b/.agents/skills/shad-overview/components/time-picker.md new file mode 100644 index 00000000..006095ab --- /dev/null +++ b/.agents/skills/shad-overview/components/time-picker.md @@ -0,0 +1,44 @@ +# Time Picker + +A field for entering a time of day. + +## 24-hour + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class TimePickerBasicExample extends StatelessWidget { + const TimePickerBasicExample({super.key}); + + @override + Widget build(BuildContext context) { + return ShadTimePicker( + trailing: const Padding( + padding: EdgeInsets.only(left: 8, top: 14), + child: Icon(LucideIcons.clock4), + ), + onChanged: (time) {}, + ); + } +} +``` + +## With AM/PM period + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class TimePickerPeriodExample extends StatelessWidget { + const TimePickerPeriodExample({super.key}); + + @override + Widget build(BuildContext context) { + return ShadTimePicker.period( + onChanged: (time) {}, + ); + } +} +``` + diff --git a/.agents/skills/shad-overview/components/toast.md b/.agents/skills/shad-overview/components/toast.md new file mode 100644 index 00000000..75bd4499 --- /dev/null +++ b/.agents/skills/shad-overview/components/toast.md @@ -0,0 +1,61 @@ +# Toast + +A succinct message that is displayed temporarily. + +## Default + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class ToastBasicExample extends StatelessWidget { + const ToastBasicExample({super.key}); + + @override + Widget build(BuildContext context) { + return ShadButton.outline( + child: const Text('Add to calendar'), + onPressed: () { + final toaster = ShadToaster.of(context); + toaster.show( + ShadToast( + title: const Text('Scheduled: Catch up'), + description: const Text('Friday, February 10, 2023 at 5:57 PM'), + action: ShadButton.outline( + child: const Text('Undo'), + onPressed: () => toaster.hide(), + ), + ), + ); + }, + ); + } +} +``` + +## Destructive + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class ToastDestructiveExample extends StatelessWidget { + const ToastDestructiveExample({super.key}); + + @override + Widget build(BuildContext context) { + return ShadButton.outline( + child: const Text('Show destructive toast'), + onPressed: () { + ShadToaster.of(context).show( + const ShadToast.destructive( + title: Text('Uh oh! Something went wrong'), + description: Text('There was a problem with your request'), + ), + ); + }, + ); + } +} +``` + diff --git a/.agents/skills/shad-overview/components/toggle-group.md b/.agents/skills/shad-overview/components/toggle-group.md new file mode 100644 index 00000000..6ad8cda7 --- /dev/null +++ b/.agents/skills/shad-overview/components/toggle-group.md @@ -0,0 +1,142 @@ +# Toggle Group + +A set of two-state buttons that can be toggled on or off. + +## Single + +Selecting one value clears the others. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class ToggleGroupBasicExample extends StatefulWidget { + const ToggleGroupBasicExample({super.key}); + + @override + State createState() => + _ToggleGroupBasicExampleState(); +} + +class _ToggleGroupBasicExampleState extends State { + Set alignment = {'left'}; + + @override + Widget build(BuildContext context) { + return ShadToggleGroup( + values: alignment, + onChanged: (values) => setState(() => alignment = values), + children: const [ + ShadToggleGroupItem( + value: 'left', + semanticLabel: 'Align left', + child: Icon(LucideIcons.alignLeft), + ), + ShadToggleGroupItem( + value: 'center', + semanticLabel: 'Align center', + child: Icon(LucideIcons.alignCenter), + ), + ShadToggleGroupItem( + value: 'right', + semanticLabel: 'Align right', + child: Icon(LucideIcons.alignRight), + ), + ], + ); + } +} +``` + +## Multiple + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class ToggleGroupMultipleExample extends StatefulWidget { + const ToggleGroupMultipleExample({super.key}); + + @override + State createState() => + _ToggleGroupMultipleExampleState(); +} + +class _ToggleGroupMultipleExampleState + extends State { + Set formatting = {'bold'}; + + @override + Widget build(BuildContext context) { + return ShadToggleGroup.multiple( + values: formatting, + onChanged: (values) => setState(() => formatting = values), + children: const [ + ShadToggleGroupItem( + value: 'bold', + semanticLabel: 'Bold', + child: Icon(LucideIcons.bold), + ), + ShadToggleGroupItem( + value: 'italic', + semanticLabel: 'Italic', + child: Icon(LucideIcons.italic), + ), + ShadToggleGroupItem( + value: 'underline', + semanticLabel: 'Underline', + child: Icon(LucideIcons.underline), + ), + ], + ); + } +} +``` + +## Outline + +Every item uses the outline toggle style. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class ToggleGroupOutlineExample extends StatefulWidget { + const ToggleGroupOutlineExample({super.key}); + + @override + State createState() => + _ToggleGroupOutlineExampleState(); +} + +class _ToggleGroupOutlineExampleState extends State { + Set values = const {'bold'}; + + @override + Widget build(BuildContext context) { + return ShadToggleGroup( + values: values, + toggleVariant: ShadToggleVariant.outline, + onChanged: (next) => setState(() => values = next), + children: const [ + ShadToggleGroupItem( + value: 'bold', + semanticLabel: 'Bold', + child: Icon(LucideIcons.bold), + ), + ShadToggleGroupItem( + value: 'italic', + semanticLabel: 'Italic', + child: Icon(LucideIcons.italic), + ), + ShadToggleGroupItem( + value: 'underline', + semanticLabel: 'Underline', + child: Icon(LucideIcons.underline), + ), + ], + ); + } +} +``` + diff --git a/.agents/skills/shad-overview/components/toggle.md b/.agents/skills/shad-overview/components/toggle.md new file mode 100644 index 00000000..8cae7cf4 --- /dev/null +++ b/.agents/skills/shad-overview/components/toggle.md @@ -0,0 +1,88 @@ +# Toggle + +A two-state button that can be either on or off. + +## Default + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class ToggleBasicExample extends StatefulWidget { + const ToggleBasicExample({super.key}); + + @override + State createState() => _ToggleBasicExampleState(); +} + +class _ToggleBasicExampleState extends State { + bool bold = false; + bool underline = false; + + @override + Widget build(BuildContext context) { + return Row( + mainAxisSize: MainAxisSize.min, + spacing: 8, + children: [ + ShadToggle( + value: bold, + semanticLabel: 'Bold', + onChanged: (value) => setState(() => bold = value), + child: const Icon(LucideIcons.bold), + ), + ShadToggle( + value: underline, + leading: const Icon(LucideIcons.underline), + onChanged: (value) => setState(() => underline = value), + child: const Text('Underline'), + ), + ], + ); + } +} +``` + +## Outline + +A bordered toggle with a subtle shadow. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class ToggleOutlineExample extends StatefulWidget { + const ToggleOutlineExample({super.key}); + + @override + State createState() => _ToggleOutlineExampleState(); +} + +class _ToggleOutlineExampleState extends State { + bool bold = false; + bool italic = false; + + @override + Widget build(BuildContext context) { + return Row( + mainAxisSize: MainAxisSize.min, + spacing: 8, + children: [ + ShadToggle.outline( + value: bold, + semanticLabel: 'Bold', + onChanged: (value) => setState(() => bold = value), + child: const Icon(LucideIcons.bold), + ), + ShadToggle.outline( + value: italic, + semanticLabel: 'Italic', + onChanged: (value) => setState(() => italic = value), + child: const Icon(LucideIcons.italic), + ), + ], + ); + } +} +``` + diff --git a/.agents/skills/shad-overview/components/tooltip.md b/.agents/skills/shad-overview/components/tooltip.md new file mode 100644 index 00000000..ce6e4abd --- /dev/null +++ b/.agents/skills/shad-overview/components/tooltip.md @@ -0,0 +1,268 @@ +# Tooltip + +A popup that displays information related to an element on hover or focus. + +## Basic + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class TooltipBasicExample extends StatelessWidget { + const TooltipBasicExample({super.key}); + + @override + Widget build(BuildContext context) { + return ShadTooltip( + builder: (context) => const Text('Add to library'), + child: ShadButton.outline( + onPressed: () {}, + child: const Text('Show Tooltip'), + ), + ); + } +} +``` + +## Sides + +An anchor places the tooltip on any side of its trigger; the arrow direction follows it. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class TooltipSidesExample extends StatelessWidget { + const TooltipSidesExample({super.key}); + + @override + Widget build(BuildContext context) { + return const Wrap( + spacing: 8, + runSpacing: 8, + children: [ + _SideTooltip(label: 'Left', direction: AxisDirection.left), + _SideTooltip(label: 'Top', direction: AxisDirection.up), + _SideTooltip(label: 'Bottom', direction: AxisDirection.down), + _SideTooltip(label: 'Right', direction: AxisDirection.right), + ], + ); + } +} + +class _SideTooltip extends StatelessWidget { + const _SideTooltip({required this.label, required this.direction}); + + final String label; + + /// The side of the trigger the tooltip appears on. + final AxisDirection direction; + + @override + Widget build(BuildContext context) { + final anchor = switch (direction) { + AxisDirection.up => const ShadAnchor( + childAlignment: Alignment.topCenter, + overlayAlignment: Alignment.bottomCenter, + offset: Offset(0, -4), + ), + AxisDirection.down => const ShadAnchor( + childAlignment: Alignment.bottomCenter, + overlayAlignment: Alignment.topCenter, + offset: Offset(0, 4), + ), + AxisDirection.left => const ShadAnchor( + childAlignment: Alignment.centerLeft, + overlayAlignment: Alignment.centerRight, + offset: Offset(-4, 0), + ), + AxisDirection.right => const ShadAnchor( + childAlignment: Alignment.centerRight, + overlayAlignment: Alignment.centerLeft, + offset: Offset(4, 0), + ), + }; + + return ShadTooltip( + anchor: anchor, + // The arrow points back at the trigger, opposite the placement side. + arrowDirection: switch (direction) { + AxisDirection.up => AxisDirection.down, + AxisDirection.down => AxisDirection.up, + AxisDirection.left => AxisDirection.right, + AxisDirection.right => AxisDirection.left, + }, + builder: (context) => const Text('Add to library'), + child: ShadButton.outline(onPressed: () {}, child: Text(label)), + ); + } +} +``` + +## With icon + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class TooltipIconExample extends StatelessWidget { + const TooltipIconExample({super.key}); + + @override + Widget build(BuildContext context) { + return ShadTooltip( + builder: (context) => const Text('Additional information'), + child: ShadIconButton.ghost( + icon: const Icon(LucideIcons.info), + onPressed: () {}, + ), + ); + } +} +``` + +## Long content + +Long content wraps at the tooltip’s max width. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class TooltipLongContentExample extends StatelessWidget { + const TooltipLongContentExample({super.key}); + + @override + Widget build(BuildContext context) { + return ShadTooltip( + // Long content wraps at the tooltip's max width. + builder: (context) => const Text( + 'To learn more about how this works, check out the docs. If you ' + 'have any questions, please reach out to us.', + ), + child: ShadButton.outline( + onPressed: () {}, + child: const Text('Show Tooltip'), + ), + ); + } +} +``` + +## Disabled + +A disabled control absorbs pointer events, so a gesture wrapper reports the hover on its behalf. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class TooltipDisabledExample extends StatelessWidget { + const TooltipDisabledExample({super.key}); + + @override + Widget build(BuildContext context) { + // A disabled button absorbs pointer events, so a wrapper reports the + // hover on its behalf — the reference wraps its trigger in a span for + // the same reason. + return ShadTooltip( + builder: (context) => const Text('This feature is currently unavailable'), + child: ShadGestureDetector( + child: ShadButton.outline( + enabled: false, + onPressed: () {}, + child: const Text('Disabled'), + ), + ), + ); + } +} +``` + +## With keyboard shortcut + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class TooltipKeyboardExample extends StatelessWidget { + const TooltipKeyboardExample({super.key}); + + @override + Widget build(BuildContext context) { + return ShadTooltip( + builder: (context) => const Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text('Save Changes'), + SizedBox(width: 6), + ShadKbd('S'), + ], + ), + child: ShadIconButton.outline( + icon: const Icon(LucideIcons.save), + onPressed: () {}, + ), + ); + } +} +``` + +## On link + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class TooltipLinkExample extends StatelessWidget { + const TooltipLinkExample({super.key}); + + @override + Widget build(BuildContext context) { + return ShadTooltip( + builder: (context) => const Text('Click to read the documentation'), + child: ShadButton.link( + onPressed: () {}, + child: const Text('Learn more'), + ), + ); + } +} +``` + +## Formatted content + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class TooltipFormattedExample extends StatelessWidget { + const TooltipFormattedExample({super.key}); + + @override + Widget build(BuildContext context) { + return ShadTooltip( + builder: (context) { + final style = DefaultTextStyle.of(context).style; + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Active', + style: style.copyWith(fontWeight: FontWeight.w600), + ), + const SizedBox(height: 4), + Opacity( + opacity: .8, + child: Text('Last updated 2 hours ago', style: style), + ), + ], + ); + }, + child: ShadButton.outline(onPressed: () {}, child: const Text('Status')), + ); + } +} +``` + diff --git a/.agents/skills/shad-overview/guides/form.md b/.agents/skills/shad-overview/guides/form.md new file mode 100644 index 00000000..ed70eb04 --- /dev/null +++ b/.agents/skills/shad-overview/guides/form.md @@ -0,0 +1,139 @@ +# Form + +Building forms with `ShadForm`: field values collect under their ids, and validation runs per field. + +## Validation and submit + +`saveAndValidate` runs every validator and returns whether the form is valid. Values collect in `formKey.currentState!.value`. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class FormProfileExample extends StatefulWidget { + const FormProfileExample({super.key}); + + @override + State createState() => _FormProfileExampleState(); +} + +class _FormProfileExampleState extends State { + final formKey = GlobalKey(); + + @override + Widget build(BuildContext context) { + return ShadForm( + key: formKey, + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 350), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 16, + children: [ + ShadInputFormField( + id: 'username', + label: const Text('Username'), + placeholder: const Text('Enter your username'), + description: const Text('This is your public display name.'), + validator: (v) { + if (v.length < 2) { + return 'Username must be at least 2 characters.'; + } + return null; + }, + ), + ShadCheckboxFormField( + id: 'terms', + initialValue: false, + inputLabel: const Text('I accept the terms and conditions'), + validator: (v) { + if (!v) { + return 'You must accept the terms and conditions'; + } + return null; + }, + ), + ShadButton( + child: const Text('Submit'), + onPressed: () { + if (formKey.currentState!.saveAndValidate()) { + ShadToaster.of(context).show( + ShadToast( + title: const Text('Form submitted'), + description: Text('${formKey.currentState!.value}'), + ), + ); + } + }, + ), + ], + ), + ), + ); + } +} +``` + +## Every field type + +Each input component has a `*FormField` counterpart that plugs into `ShadForm` the same way: `ShadInputFormField`, `ShadSelectFormField`, `ShadTextareaFormField`, `ShadRadioGroupFormField`, `ShadDatePickerFormField`, `ShadDateRangePickerFormField`, `ShadTimePickerFormField`, `ShadCheckboxFormField`, `ShadSwitchFormField`, and `ShadInputOTPFormField`. See `example/lib/docs/examples/form/form_fields.dart` for one of each wired into a single form. + +## OTP verification + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class FormOtpExample extends StatefulWidget { + const FormOtpExample({super.key}); + + @override + State createState() => _FormOtpExampleState(); +} + +class _FormOtpExampleState extends State { + final formKey = GlobalKey(); + + @override + Widget build(BuildContext context) { + return ShadForm( + key: formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 16, + children: [ + ShadInputOTPFormField( + id: 'otp', + maxLength: 6, + label: const Text('OTP'), + description: const Text('Enter your one-time password.'), + validator: (v) => v.contains(' ') ? 'Fill the whole code' : null, + children: const [ + ShadInputOTPGroup( + children: [ + ShadInputOTPSlot(), + ShadInputOTPSlot(), + ShadInputOTPSlot(), + ], + ), + Icon(LucideIcons.dot), + ShadInputOTPGroup( + children: [ + ShadInputOTPSlot(), + ShadInputOTPSlot(), + ShadInputOTPSlot(), + ], + ), + ], + ), + ShadButton( + child: const Text('Verify'), + onPressed: () => formKey.currentState!.saveAndValidate(), + ), + ], + ), + ); + } +} +``` diff --git a/.agents/skills/shad-overview/guides/installation.md b/.agents/skills/shad-overview/guides/installation.md new file mode 100644 index 00000000..70a78c27 --- /dev/null +++ b/.agents/skills/shad-overview/guides/installation.md @@ -0,0 +1,82 @@ +# Installation + +Add `shad` to your project from the command line: + +```bash +flutter pub add shad +``` + +Everything ships through one import: components, theme, Lucide icons, and animation effects. + +```dart +import 'package:shad/shad.dart'; +``` + +## Set up ShadApp + +`ShadApp` takes the place of `MaterialApp`. It installs the theme, localizations, navigation, and the overlay layer the floating components use. With no arguments it uses the slate color scheme and the nova style. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return const ShadApp( + home: HomePage(), + ); + } +} +``` + +Apps that use the `Router` API use `ShadApp.router` with the usual `routerConfig` or delegate parameters. + +## Dark mode + +Pass a `theme` and a `darkTheme`, and pick between them with `themeMode`. Follow the platform, or bind it to your own setting. + +```dart +ShadApp( + themeMode: ThemeMode.system, + theme: ShadThemeData(), + darkTheme: ShadThemeData( + brightness: Brightness.dark, + ), + home: const HomePage(), +) +``` + +## Use the components + +Components are plain widgets. Icons come from the bundled Lucide set: + +```dart +ShadButton( + leading: const Icon(LucideIcons.mail), + onPressed: () {}, + child: const Text('Login with Email'), +) +``` + +## Padding + +Use `ShadPadding` instead of Flutter's `Padding` to keep insets on the theme's spacing scale. It has no plain constructor, only `ShadPadding.all`, `.symmetric`, `.only`, and `.directional`, and their arguments are steps, not logical pixels: + +```dart +ShadPadding.symmetric( + horizontal: 6, // px-6 + vertical: 4, // py-4 + child: const Text('Inset by the theme spacing scale'), +) +``` + +See [layout.md](layout.md#padding) for a full example. + +From here, [theming.md](theming.md) and [styles.md](styles.md) cover how to make it yours, and each page under `components/` pairs a description with the actual example source that ships in the example app. diff --git a/.agents/skills/shad-overview/guides/layout.md b/.agents/skills/shad-overview/guides/layout.md new file mode 100644 index 00000000..250fa10a --- /dev/null +++ b/.agents/skills/shad-overview/guides/layout.md @@ -0,0 +1,130 @@ +# Layout + +Layout widgets measure gaps and padding in theme spacing steps: `ShadRow`, `ShadColumn`, `ShadGap`, and `ShadPadding`. They read `ShadThemeData.spacing` the same way components do (see [styles.md](styles.md#spacing)), so a layout built from them scales with the rest of the theme. + +## Row and column spacing + +`ShadRow` and `ShadColumn` accept a `spacing` argument in theme steps. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class LayoutSpacingExample extends StatelessWidget { + const LayoutSpacingExample({super.key}); + + @override + Widget build(BuildContext context) { + return ShadColumn( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + spacing: 4, + children: [ + ShadCard( + child: ShadRow( + mainAxisSize: MainAxisSize.min, + spacing: 2, + children: [ + for (final label in ['One', 'Two', 'Three']) + ShadBadge.secondary(child: Text(label)), + ], + ), + ), + ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 320), + child: ShadCard( + child: ShadColumn( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, + spacing: 3, + children: [ + const ShadInput(placeholder: Text('Email')), + const ShadInput(placeholder: Text('Password')), + ShadButton(onPressed: () {}, child: const Text('Sign in')), + ], + ), + ), + ), + ], + ); + } +} +``` + +## Gaps + +Use `ShadGap` in a plain `Row` or `Column` when only some children need space between them. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class LayoutGapExample extends StatelessWidget { + const LayoutGapExample({super.key}); + + @override + Widget build(BuildContext context) { + final theme = ShadTheme.of(context); + + return ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 320), + child: ShadCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, + children: [ + Text('Account', style: theme.textTheme.h4), + const ShadGap(1), + Text( + 'Manage your profile and security settings.', + style: theme.textTheme.muted, + ), + const ShadGap(4), + ShadButton.outline( + onPressed: () {}, + child: const Text('Open settings'), + ), + ], + ), + ), + ); + } +} +``` + +## Padding + +`ShadPadding` mirrors Tailwind's step-based inset utilities. `symmetric(horizontal: 6, vertical: 4)` matches `px-6 py-4`. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class LayoutPaddingExample extends StatelessWidget { + const LayoutPaddingExample({super.key}); + + @override + Widget build(BuildContext context) { + final theme = ShadTheme.of(context); + final spacing = theme.spacing; + + return ColoredBox( + color: theme.colorScheme.muted, + child: ShadPadding.symmetric( + horizontal: 6, + vertical: 4, + child: ShadCard( + child: Text( + 'Inset by ${spacing(6)} x ${spacing(4)}', + style: theme.textTheme.small, + ), + ), + ), + ); + } +} +``` + +## Card sections and form actions + +`ShadColumn` with `crossAxisAlignment: stretch` lines up full-width children inside a card, with `ShadSeparator` between groups. The same pattern stacks fields and buttons in a form: put the primary action above an outline cancel. See `example/lib/docs/examples/layout/layout_card_sections.dart` and `layout_form_actions.dart` for the full examples. diff --git a/.agents/skills/shad-overview/guides/portal.md b/.agents/skills/shad-overview/guides/portal.md new file mode 100644 index 00000000..9b07d0bc --- /dev/null +++ b/.agents/skills/shad-overview/guides/portal.md @@ -0,0 +1,99 @@ +# Portal + +The raw overlay primitive underneath popover, select, tooltip, and menus: it renders a follower widget anchored to its child, above everything else. + +`ShadPortal` is a building block rather than an end-user component. Reach for `ShadPopover`, `ShadTooltip`, or the menu components first. Use the portal directly only when you build a new floating surface. + +## Anchoring modes + +`ShadAnchorAuto` flips and shifts the follower to stay on screen, the same anchor mode every built-in floating component uses. + +```dart +import 'dart:async'; + +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +/// Cycles `ShadAnchorAuto` follower anchors on a hover-triggered portal. +class PortalAnchoringExample extends StatefulWidget { + const PortalAnchoringExample({super.key}); + + @override + State createState() => _PortalAnchoringExampleState(); +} + +class _PortalAnchoringExampleState extends State { + var visible = false; + late final Timer timer; + var alignmentIndex = 0; + + static const _alignments = [ + AlignmentGeometry.topStart, + AlignmentGeometry.topCenter, + AlignmentGeometry.topEnd, + AlignmentGeometry.centerStart, + AlignmentGeometry.center, + AlignmentGeometry.centerEnd, + AlignmentGeometry.bottomStart, + AlignmentGeometry.bottomCenter, + AlignmentGeometry.bottomEnd, + ]; + + @override + void initState() { + super.initState(); + timer = Timer.periodic(const Duration(seconds: 1), (_) { + setState(() { + alignmentIndex = (alignmentIndex + 1) % _alignments.length; + }); + }); + } + + @override + void dispose() { + timer.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final theme = ShadTheme.of(context); + final alignment = _alignments[alignmentIndex]; + + return Align( + child: ShadPortal( + anchor: ShadAnchorAuto(followerAnchor: alignment), + visible: visible, + portalBuilder: (context) { + return ShadMouseArea( + groupId: 'portal-demo', + child: ColoredBox( + color: theme.colorScheme.primary, + child: const SizedBox( + width: 200, + height: 200, + child: Center(child: Text('200x200')), + ), + ), + ); + }, + child: ShadMouseArea( + groupId: 'portal-demo', + onEnter: (_) => setState(() => visible = true), + onExit: (_) => setState(() => visible = false), + child: ColoredBox( + color: theme.colorScheme.destructive, + child: const SizedBox( + width: 100, + height: 100, + child: Center(child: Text('100x100')), + ), + ), + ), + ), + ); + } +} +``` + +The two `ShadMouseArea` widgets share a `groupId` so hovering the portal's own content counts as still hovering the trigger, which is what keeps a popover open while the pointer moves from the trigger onto the popover surface. diff --git a/.agents/skills/shad-overview/guides/responsive.md b/.agents/skills/shad-overview/guides/responsive.md new file mode 100644 index 00000000..06b95833 --- /dev/null +++ b/.agents/skills/shad-overview/guides/responsive.md @@ -0,0 +1,50 @@ +# Responsive + +Tailwind's breakpoint scale, carried on the theme: a builder widget and a context extension. + +## Breakpoints + +The theme carries six breakpoints matching Tailwind's scale. Override any of them on `ShadThemeData`: + +```dart +ShadThemeData( + breakpoints: ShadBreakpoints( + tn: 0, // tiny + sm: 640, + md: 768, + lg: 1024, + xl: 1280, + xxl: 1536, + ), +) +``` + +## Reading the current breakpoint + +`ShadResponsiveBuilder` rebuilds with the breakpoint for the current width. `context.breakpoint` reads the same value inline. + +```dart +ShadResponsiveBuilder( + builder: (context, breakpoint) { + final sm = breakpoint >= ShadTheme.of(context).breakpoints.sm; + return sm ? const WideLayout() : const NarrowLayout(); + }, +) +``` + +Comparisons follow Tailwind's semantics. `sm` means "small and up", so test with `>=`. Use `==` only to target exactly one band. + +## Switching on the band + +Each band is its own type on a sealed class, so a `switch` covers the scale exhaustively: + +```dart +return switch (context.breakpoint) { + ShadBreakpointTN() => const Text('Tiny'), + ShadBreakpointSM() => const Text('Small'), + ShadBreakpointMD() => const Text('Medium'), + ShadBreakpointLG() => const Text('Large'), + ShadBreakpointXL() => const Text('Extra large'), + ShadBreakpointXXL() => const Text('Extra extra large'), +}; +``` diff --git a/.agents/skills/shad-overview/guides/styles.md b/.agents/skills/shad-overview/guides/styles.md new file mode 100644 index 00000000..e127ce07 --- /dev/null +++ b/.agents/skills/shad-overview/guides/styles.md @@ -0,0 +1,56 @@ +# Styles + +The eight shadcn styles are complete presets for geometry, typography, and shadows, switchable with one line. + +shadcn/ui ships eight named styles, and they are not color variations. Each one is a full geometry set. A `mira` button is 28px tall with 8px of padding where a `sera` button is 40px with 24px, and inputs, menu rows, cards, sliders, and switches move with them. Styles also retype the UI: each one redefines the title, label, body, caption, and field roles, and reshapes it with its own radii and shadows. Every value was read from the shadcn/ui reference CSS. + +## Choosing a style + +```dart +ShadThemeData( + style: ShadStyleTokens.maia, +) +``` + +- `vega`: the classic. 36px controls, moderate radii, subtle shadows. +- `nova`: the default. Vega, condensed to 32px controls and tighter surfaces. +- `maia`: pill-shaped and roomy, with fully rounded controls. +- `lyra`: square. No radii, flat shadows, 12px body text. +- `mira`: the densest. 28px controls on a relaxed 12px line. +- `luma`: soft. Filled fields, large radii, weighty shadows. +- `sera`: editorial. 40px controls, underlined fields, uppercase labels, generous space. +- `rhea`: gently rounded mid-size controls with soft hairlines. + +Open the example app's theme editor and switch styles to see them live. The whole app follows. + +## Picking at runtime + +The presets are values on `ShadStyleTokens`, so a style switcher is a list and a lookup: + +```dart +ShadStyleTokens.all; // the eight presets +ShadStyleTokens.fromName('sera'); // one, by its shadcn name +``` + +## Custom styles + +A style is data, a set of measured tokens. Derive your own from the preset closest to what you want: + +```dart +ShadThemeData( + style: ShadStyleTokens.nova.copyWith( + buttonRadius: ShadRadiusToken.full, + buttonHeight: 40, + ), +) +``` + +## Spacing + +Metrics the reference writes in Tailwind's spacing units follow the theme's `ShadSpacing` step: 4px, like `0.25rem`. Raising it loosens every unit-derived padding and gap in one move: + +```dart +ShadThemeData( + spacing: const ShadSpacing(step: 5), +) +``` diff --git a/.agents/skills/shad-overview/guides/theming.md b/.agents/skills/shad-overview/guides/theming.md new file mode 100644 index 00000000..aca4a6ec --- /dev/null +++ b/.agents/skills/shad-overview/guides/theming.md @@ -0,0 +1,149 @@ +# Theming + +One `ShadThemeData` drives every component: color scheme, radius, typography, and per-component overrides. + +## Reading the theme + +Every component resolves its appearance from the nearest `ShadTheme`. Your own widgets read it the same way: + +```dart +final theme = ShadTheme.of(context); + +theme.colorScheme.primary; // the palette +theme.textTheme.p; // the type scale +theme.radius; // the base corner radius +theme.style; // the active style preset +``` + +## Color schemes + +Sixteen schemes ship in light and dark: blue, gray, green, mauve, mist, neutral, olive, orange, red, rose, slate (the default), stone, taupe, violet, yellow, and zinc. + +```dart +ShadApp( + theme: ShadThemeData( + colorScheme: const ShadZincColorScheme.light(), + ), + darkTheme: ShadThemeData( + brightness: Brightness.dark, + colorScheme: const ShadZincColorScheme.dark(), + ), +) +``` + +To let users pick a palette at runtime, resolve one by name with `ShadColorScheme.fromName`: + +```dart +final scheme = ShadColorScheme.fromName( + 'rose', + brightness: Brightness.dark, +); +``` + +## Customizing a scheme + +A scheme is a value. Override individual slots where you construct it, and carry your own colors in the `custom` map so they lerp and switch with the rest of the theme. + +```dart +ShadThemeData( + colorScheme: const ShadSlateColorScheme.light( + primary: Color(0xFF16A34A), + custom: { + 'highlight': Color(0xFFFACC15), + }, + ), +) + +// Read it back anywhere: +ShadTheme.of(context).colorScheme.custom['highlight']; +``` + +An extension keeps custom colors as convenient as the built-in ones: + +```dart +extension AppColors on ShadColorScheme { + Color get highlight => custom['highlight']!; +} +``` + +## Component themes + +Every component has a theme slot on `ShadThemeData`: `primaryButtonTheme`, `cardTheme`, `inputTheme`, and so on. Values you pass are merged over what the style derives, so an override touches exactly the property you name: + +```dart +ShadThemeData( + primaryButtonTheme: const ShadButtonTheme( + backgroundColor: Color(0xFF16A34A), + ), +) +``` + +## Radius + +The theme carries one base radius, the `md` step of the scale. Everything else derives from it: at the default 8 the scale runs sm 6, md 8, lg 10, xl 14, 2xl 16, 3xl 24, 4xl 32, and each component picks the step its style prescribes. + +```dart +ShadThemeData( + radius: const BorderRadius.all(Radius.circular(10)), +) +``` + +## Menu appearance + +The shadcn theme editor's menu options are theme settings here too. `menuColorScheme` renders menu surfaces on another palette (pass the dark counterpart for the "Inverted" look), `menuTranslucent` gives them the blurred 70% finish, and `menuAccent` picks how strongly rows highlight. + +```dart +ShadThemeData( + menuColorScheme: const ShadSlateColorScheme.dark(), + menuTranslucent: true, + menuAccent: ShadMenuAccent.bold, +) +``` + +## Focus rings + +Focus is shown as an outward ring, like shadcn/ui. Set `disableSecondaryBorder: true` to switch to the alternative treatment: a 2px border inside the control, which keeps focus visible where an outward ring would be clipped. + +```dart +ShadThemeData( + disableSecondaryBorder: true, +) +``` + +## Interop with Material and Cupertino + +`ShadApp.custom` renders shadcn components inside a `MaterialApp` or `CupertinoApp`, so both widget sets can share one page. Add the localizations delegate for each set you use and wrap `builder` in a `ShadAppBuilder`: + +```dart +import 'package:flutter/material.dart'; +import 'package:shad/shad.dart'; + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return ShadApp.custom( + themeMode: ThemeMode.dark, + darkTheme: ShadThemeData( + brightness: Brightness.dark, + colorScheme: const ShadSlateColorScheme.dark(), + ), + appBuilder: (context) { + return MaterialApp( + theme: Theme.of(context), + localizationsDelegates: const [ + GlobalShadLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ], + builder: (context, child) => ShadAppBuilder(child: child!), + ); + }, + ); + } +} +``` + +`Theme.of(context)` inside `appBuilder` already carries a `ThemeData` derived from the `ShadThemeData` (color scheme, typography, and common widget themes), so Material widgets on the same page pick up matching colors without extra work. Use `CupertinoApp` and `CupertinoTheme.of(context)` the same way for Cupertino. If the app needs the `Router` API, use `MaterialApp.router` or `CupertinoApp.router` inside `appBuilder`. diff --git a/.agents/skills/shad-overview/guides/typography.md b/.agents/skills/shad-overview/guides/typography.md new file mode 100644 index 00000000..aa8909e1 --- /dev/null +++ b/.agents/skills/shad-overview/guides/typography.md @@ -0,0 +1,98 @@ +# Typography + +`ShadThemeData.textTheme` carries the prose scale. Read a role and hand it to a `Text` widget: + +```dart +Text( + 'Taxing Laughter: The Joke Tax Chronicles', + style: ShadTheme.of(context).textTheme.h1, +) +``` + +## Roles + +| Role | Use for | +| --- | --- | +| `h1Large` | The largest heading, for hero-style titles. | +| `h1` | Page titles. | +| `h2` | Section titles. | +| `h3` | Sub-section titles. | +| `h4` | Card and dialog titles. | +| `p` | Body paragraphs. | +| `blockquote` | Quoted text, indented with a left border. | +| `table` | Table cell text. | +| `list` | List item text. | +| `lead` | The introductory paragraph under a page title. | +| `large` | Emphasized body text, for example a dialog's main question. | +| `small` | Field labels and secondary UI text. | +| `muted` | De-emphasized helper text. | + +Every component that shows text (buttons, badges, cards, alerts, menus, and so on) reads its own style from a component theme instead of these roles directly. Those styles derive from the active style preset (see [styles.md](styles.md)) but can be overridden the same way as any other component theme, see [theming.md](theming.md). + +## Custom font family + +By default `shad` uses [Geist](https://vercel.com/font) as the default font family. To change it, add the font to your project, for example under a `/fonts` directory, then declare it in `pubspec.yaml`: + +```yaml +flutter: + fonts: + - family: UbuntuMono + fonts: + - asset: fonts/UbuntuMono-Regular.ttf + - asset: fonts/UbuntuMono-Italic.ttf + style: italic + - asset: fonts/UbuntuMono-Bold.ttf + weight: 700 + - asset: fonts/UbuntuMono-BoldItalic.ttf + weight: 700 + style: italic +``` + +Then pass a `ShadTextTheme` with the new family to `ShadThemeData`: + +```dart +ShadThemeData( + colorScheme: const ShadZincColorScheme.light(), + textTheme: ShadTextTheme( + colorScheme: const ShadZincColorScheme.light(), + family: 'UbuntuMono', + ), +) +``` + +## Google Fonts + +Add the [google_fonts](https://pub.dev/packages/google_fonts) package, then build the text theme from one of its fonts: + +```dart +ShadThemeData( + colorScheme: const ShadZincColorScheme.light(), + textTheme: ShadTextTheme.fromGoogleFont(GoogleFonts.poppins), +) +``` + +## Custom styles + +Add your own named styles with the `custom` parameter, the same pattern `ShadColorScheme` uses for custom colors: + +```dart +ShadThemeData( + textTheme: ShadTextTheme( + custom: { + 'myCustomStyle': const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w400, + color: Colors.blue, + ), + }, + ), +) +``` + +Read it back with `ShadTheme.of(context).textTheme.custom['myCustomStyle']!`, or add an extension to make it a getter: + +```dart +extension CustomStyleExtension on ShadTextTheme { + TextStyle get myCustomStyle => custom['myCustomStyle']!; +} +``` diff --git a/.claude/skills/flutter-add-widget-preview/SKILL.md b/.claude/skills/flutter-add-widget-preview/SKILL.md index a9235f58..4b38568d 100644 --- a/.claude/skills/flutter-add-widget-preview/SKILL.md +++ b/.claude/skills/flutter-add-widget-preview/SKILL.md @@ -10,28 +10,43 @@ user-invocable: false ## Contents - [Preview Guidelines](#preview-guidelines) +- [Preview Parameters](#preview-parameters) - [Handling Limitations](#handling-limitations) - [Workflows](#workflows) - [Examples](#examples) ## Preview Guidelines -Use the Flutter Widget Previewer to render widgets in real-time, isolated from the full application context. +Use the Flutter Widget Previewer to render widgets in real-time, isolated from the full application context. - **Target Elements:** Apply the `@Preview` annotation to top-level functions, static methods within a class, or public widget constructors/factories that have no required arguments and return a `Widget` or `WidgetBuilder`. - **Imports:** Always import `package:flutter/widget_previews.dart` to access the preview annotations. -- **Custom Annotations:** Extend the `Preview` class to create custom annotations that inject common properties (e.g., themes, wrappers) across multiple widgets. +- **Custom Annotations:** Extend the `Preview` class to create custom annotations that inject common properties (e.g., custom themes, wrappers, sizes) across multiple widgets. - **Multiple Configurations:** Apply multiple `@Preview` annotations to a single target to generate multiple preview instances. Alternatively, extend `MultiPreview` to encapsulate common multi-preview configurations. -- **Runtime Transformations:** Override the `transform()` method in custom `Preview` or `MultiPreview` classes to modify preview configurations dynamically at runtime (e.g., generating names based on dynamic values, which is impossible in a `const` context). +- **Runtime Transformations:** Override the `transform()` method in custom `Preview` or `MultiPreview` classes to modify preview configurations dynamically at runtime (e.g., building names dynamically based on parameters, which is impossible in a `const` constructor). + +## Preview Parameters + +The `@Preview` annotation supports the following parameters for tailoring preview environments: + +- `name`: A descriptive name for the preview instance. +- `group`: A name used to group related previews together in the previewer. +- `size`: Artificial constraints using a `Size` object. +- `textScaleFactor`: Custom font scale factor. +- `wrapper`: A function that wraps the previewed widget in a specific widget tree (e.g., to inject state or inherited widgets). +- `theme`: A function returning a `PreviewThemeData` subclass instance to provide custom theming with support for sequential theme layering. +- `brightness`: The initial theme `Brightness` (`Brightness.light` or `Brightness.dark`). +- `localizations`: A function applying a localization configuration. ## Handling Limitations Adhere to the following constraints when authoring previewable widgets, as the Widget Previewer runs in a web environment: -- **No Native APIs:** Do not use native plugins or APIs from `dart:io` or `dart:ffi`. Widgets with transitive dependencies on `dart:io` or `dart:ffi` will throw exceptions upon invocation. Use conditional imports to mock or bypass these in preview mode. +- **No Native APIs:** Do not use native plugins or APIs from `dart:io` or `dart:ffi`. Widgets with transitive dependencies on `dart:io` or `dart:ffi` will throw exceptions upon invocation. Use conditional imports or wrappers to mock or bypass these in preview mode. - **Asset Paths:** Use package-based paths for assets loaded via `dart:ui` `fromAsset` APIs (e.g., `packages/my_package_name/assets/my_image.png` instead of `assets/my_image.png`). - **Public Callbacks:** Ensure all callback arguments provided to preview annotations are public and constant to satisfy code generation requirements. -- **Constraints:** Apply explicit constraints using the `size` parameter in the `@Preview` annotation if your widget is unconstrained, as the previewer defaults to constraining them to approximately half the viewport. +- **Constraints:** Apply explicit constraints using the `size` parameter in the `@Preview` annotation if your widget is unconstrained, as unconstrained widgets default to roughly half the viewport. +- **Build Caching:** The previewer caches builds in a `.widget_preview/` folder in the project root. ## Workflows @@ -41,13 +56,13 @@ Copy and track this checklist when implementing a new widget preview: - [ ] Import `package:flutter/widget_previews.dart`. - [ ] Identify a valid target (top-level function, static method, or parameter-less public constructor). - [ ] Apply the `@Preview` annotation to the target. -- [ ] Configure preview parameters (`name`, `group`, `size`, `theme`, `brightness`, etc.) as needed. -- [ ] If applying the same configuration to multiple widgets, extract the configuration into a custom class extending `Preview`. +- [ ] Configure preview parameters (`name`, `group`, `size`, `textScaleFactor`, `wrapper`, `theme`, `brightness`, `localizations`) as needed. +- [ ] If applying the same configuration to multiple widgets, extract the configuration into a custom class extending `Preview` or `MultiPreview`. ### Interacting with Previews Follow the appropriate conditional workflow to launch and interact with the Widget Previewer: -**If using a supported IDE (Android Studio, IntelliJ, VS Code with Flutter 3.38+):** +**If using a supported IDE (Android Studio, IntelliJ, VS Code with Flutter 3.47+):** 1. Launch the IDE. The Widget Previewer starts automatically. 2. Open the "Flutter Widget Preview" tab in the sidebar. 3. Toggle "Filter previews by selected file" at the bottom left if you want to view previews outside the currently active file. @@ -71,45 +86,72 @@ Follow the appropriate conditional workflow to launch and interact with the Widg import 'package:flutter/widget_previews.dart'; import 'package:flutter/material.dart'; -@Preview(name: 'My Sample Text', group: 'Typography') -Widget mySampleText() { - return const Text('Hello, World!'); -} +@Preview(name: 'Submit Button', group: 'Form Controls') +Widget submitButtonPreview() => const ElevatedButton( + onPressed: null, + child: Text('Submit'), + ); ``` -### Custom Preview with Runtime Transformation +### Custom Preview with PreviewThemeData and Transformation ```dart import 'package:flutter/widget_previews.dart'; import 'package:flutter/material.dart'; -final class TransformativePreview extends Preview { - const TransformativePreview({ +final class ThemedPreview extends Preview { + const ThemedPreview({ super.name, super.group, + super.brightness, }); - PreviewThemeData _themeBuilder() { - return PreviewThemeData( - materialLight: ThemeData.light(), - materialDark: ThemeData.dark(), - ); - } + PreviewThemeData _themeBuilder() => + CustomThemeData(brightness: brightness); @override Preview transform() { final originalPreview = super.transform(); + final themeVariant = switch (brightness) { + null => 'Responsive', + final b => b.name, + }; + final builder = originalPreview.toBuilder(); - + final baseName = originalPreview.name ?? 'Preview'; builder - ..name = 'Transformed - ${originalPreview.name}' + ..name = '$baseName [$themeVariant]' ..theme = _themeBuilder; - return builder.toPreview(); + return builder.build(); } } -@TransformativePreview(name: 'Custom Themed Button') -Widget myButton() => const ElevatedButton(onPressed: null, child: Text('Click')); +final class CustomThemeData extends PreviewThemeData { + const CustomThemeData({this.brightness}); + + final Brightness? brightness; + + @override + Widget apply(BuildContext context, Widget child) { + return Theme( + data: ThemeData.from( + colorScheme: ColorScheme.fromSeed( + seedColor: Colors.teal, + brightness: brightness ?? + MediaQuery.maybePlatformBrightnessOf(context) ?? + Brightness.light, + ), + ), + child: child, + ); + } +} + +@ThemedPreview(name: 'Themed Action Button', brightness: Brightness.dark) +Widget themedButtonPreview() => const ElevatedButton( + onPressed: null, + child: Text('Action'), + ); ``` ### MultiPreview Implementation @@ -142,5 +184,10 @@ final class MultiBrightnessPreview extends MultiPreview { } @MultiBrightnessPreview(name: 'Primary Card') -Widget cardPreview() => const Card(child: Padding(padding: EdgeInsets.all(8.0), child: Text('Content'))); +Widget cardPreview() => const Card( + child: Padding( + padding: EdgeInsets.all(8.0), + child: Text('Content'), + ), + ); ``` diff --git a/.claude/skills/flutter_scene-idioms/SKILL.md b/.claude/skills/flutter_scene-idioms/SKILL.md new file mode 100644 index 00000000..5459ff30 --- /dev/null +++ b/.claude/skills/flutter_scene-idioms/SKILL.md @@ -0,0 +1,138 @@ +--- +name: flutter_scene-idioms +version: 16 +description: Write correct flutter_scene code. Use this whenever building 3D with the flutter_scene Dart/Flutter engine (rendering a scene, geometry, materials, lighting, loading a .glb model, animation, custom shaders). It corrects the wrong assumptions models carry from three.js, Godot, and Unity, and names the APIs and traps that are specific to this engine. +user-invocable: false +--- + +# Building with flutter_scene + +flutter_scene is a realtime 3D engine for Flutter, built on Flutter GPU. It has a retained scene graph (`Scene` holds `Node`s, nodes carry a `Mesh`), physically based materials, image-based lighting, and a deep post-processing stack. + +**The one thing to internalize: this is not three.js, Godot, or Unity, and the API diverges from all three in specific ways.** Most first-attempt failures come from reaching for another engine's spelling. The corrections below are the highest-value part of this skill; read them before writing code. + +## Do not reach for these (they do not exist or will break the build) + +- **Not the master channel.** flutter_scene runs on **Flutter 3.47 stable or newer**. Do not run `flutter channel master`; it resolves worse, not better. +- **Not `--enable-impeller`, not `--enable-experiment=native-assets`.** The run flag is **`--enable-flutter-gpu`** and nothing else. `--enable-experiment=native-assets` actively breaks the build on Dart 3.10+. +- **Not `package:vector_math/vector_math_64.dart`.** flutter_scene uses **`package:vector_math/vector_math.dart`**. The `_64` types are a different, incompatible `Vector3`. +- **Not `Node.fromAsset(...)`, not `loadModel(...)`.** Load a preprocessed model with **`loadScene('assets/x.glb')`** (returns `Future`), or a runtime glTF with `Node.fromGlbAsset` / `Node.fromGlbBytes`. +- **Not a hand-rolled `CustomPainter` + `Ticker`.** Display a scene with the **`SceneView`** widget; it drives the per-frame loop for you. +- **Not `node.position.set(x, y, z)`.** See transforms below. +- **Not `forward.cross(up)` for a camera or node right vector.** View space is left-handed (`+Z` forward into the screen, `+Y` up, `+X` right), while `vector_math`'s `cross` is right-handed, so `forward.cross(up)` points **screen-left** and horizontally inverts `A`/`D` strafing and drag panning. Read `camera.right`, `camera.horizontalRight`, or `node.right`, or compute `up.cross(forward).normalized()`. +- **Not `.model` files or `buildModels`.** The offline format is `.fsceneb`, produced by the `flutter_scene:init` build hook; you load it by source path with `loadScene`. +- **Not the removed `Environment` class.** Environment lighting is `EnvironmentMap` on `Scene.environment`. + +## Setup + +```sh +flutter pub add flutter_scene +dart run flutter_scene:init # installs the build hook, sets up assets +flutter run --enable-flutter-gpu # native; add -d chrome for web +``` + +`flutter_scene:init` is required setup, not optional. Rendering is gated on `Scene.initializeStaticResources()`; until it completes the engine prints "Flutter Scene is not ready to render. Skipping frame." + +## Minimal scene (this compiles as-is) + +```dart +import 'package:flutter/material.dart'; +import 'package:flutter_scene/scene.dart'; +import 'package:vector_math/vector_math.dart' as vm; + +void main() => runApp(const MaterialApp(home: CubeView())); + +class CubeView extends StatefulWidget { + const CubeView({super.key}); + @override + State createState() => _CubeViewState(); +} + +class _CubeViewState extends State { + final Scene scene = Scene(); + bool ready = false; + + @override + void initState() { + super.initState(); + // Geometry and materials touch the shader bundle, so build them only + // after the engine's static resources are up. + Scene.initializeStaticResources().then((_) { + scene.add(Node( + mesh: Mesh(CuboidGeometry(vm.Vector3(1, 1, 1)), PhysicallyBasedMaterial()), + )); + if (mounted) setState(() => ready = true); + }); + } + + @override + Widget build(BuildContext context) { + if (!ready) return const SizedBox.expand(); + return SceneView(scene, camera: PerspectiveCamera(position: vm.Vector3(2, 2, -4))); + } +} +``` + +An unset `Scene.environment` still gives image-based lighting (a default studio map is resolved at render), so a bare `PhysicallyBasedMaterial()` is lit without any light setup. + +## Choosing declarative or imperative + +flutter_scene has two ways to build a scene, and picking the wrong one is a structural decision that is expensive to undo later. Choose up front by what the app does, not by which reads nicer. + +**Declarative (inline widgets).** Describe the scene as Flutter widgets under `SceneView.declarative(children: [...])`, using `SceneMesh`, `SceneNode`, and `SceneModel`. Flutter's own rebuild diffing keeps the rendered scene in sync with your widget state, the same way it keeps the UI in sync. Reach for this when app state maps cleanly onto a fixed set of objects on screen and nothing is simulation-like, for example a product configurator where a few `SceneMesh`es track some `setState` values. + +**Imperative (retained scene graph).** Own a `Scene`, add `Node`s, attach `Component`s, and display it with `SceneView(scene, camera: ..., onTick: ...)`. Track state the way you would in another game engine. The cleanest shape is a plain Dart `Game` class that owns the `Scene` and holds the game state, with the scene build and per-frame tick routed into it, and behavior living in custom `Component`s attached to nodes that the engine runs through the component lifecycle hooks. Reach for this for anything with real simulation. + +**Go imperative when** the scene has complex physics, network replication, a character walking around, or procedural generation. Any one of these means imperative. +**Stay declarative when** state maps directly to a fixed set of shown objects and nothing ticks or simulates. + +The two interoperate. A mostly-declarative scene can drop to an imperative node where it needs one, and an imperative scene can mount declarative subtrees. See `references/architecture.md` for the `Game`-class pattern, component-driven nodes, and the hybrid seam. + +## The API shape (where it diverges from what you expect) + +**Transforms.** `Node` has `position`, `rotation` (a `Quaternion`), and `scale`, but they are whole-value get/set, not the mutable spelling other engines use. Assign the whole vector (`node.position = vm.Vector3(0, 1, 0)` or `node.position += ...`). The getters return copies, so `node.position.x = 5` does nothing and throws in debug. For a raw matrix edit use `node.localTransform = matrix` or `node.mutateLocalTransform((m) => m.translateByVector3(...))`; a bare in-place edit of `node.localTransform` never moves the node, because the cache is not told. `node.forward`, `node.right`, and `node.up` return the node's unit world-space basis axes (`+Z`, `+X`, `+Y`), and `node.lookAt(target)` or `node.lookAtFrom(eye, target)` aims local `+Z` at `target`. + +**Geometry.** Ten built-in primitives (`CuboidGeometry`, `SphereGeometry`, `IcosphereGeometry`, `CylinderGeometry` with separate top/bottom radii so cones are free, `CapsuleGeometry`, `TorusGeometry`, `PlaneGeometry`, `DiscGeometry`, `RingGeometry`, `WedgeGeometry`), plus swept geometry (`ExtrudeGeometry`, `TubeGeometry`, `RibbonGeometry`), lines (`PolylineGeometry`), and `GeometryBuilder`/`MeshData` for custom meshes. Do not hand-pack a `ByteData` vertex buffer before checking these. + +**Materials.** `PhysicallyBasedMaterial` (base color, metallic, roughness, normal, emissive, plus clearcoat/sheen/transmission/etc.), `UnlitMaterial`, `ShaderMaterial` for custom shaders. Texture slots take a `TextureSource` (from `loadTexture(path)`), not a raw `gpu.Texture`. + +**Camera and coordinate basis.** `PerspectiveCamera(position: ..., target: ...)`, or `OrthographicCamera(position: ..., target: ..., projection: OrthographicProjection(size: OrthographicSize.height(12)))` for isometric, top-down, and pixel-art views. Every depth effect works under both. Prefer attaching `OrbitCameraController`, `FollowCameraController`, or `FlyCameraController` to a camera node (driven with the `CameraControls` widget) over hand-rolling spherical camera math. When driving a camera yourself, remember that view space is **left-handed (`+Z` forward into the screen, `+Y` up, `+X` right)**. +- Default camera placement sits on the `-Z` side (`(0, 0, -5)`) looking along `+Z` at the origin, where world `+X` points screen-right. If you instead place an eye on the `+Z` side (`eye = target + vm.Vector3(d * sin(a), y, d * cos(a))`) looking back along `-Z`, world `+X` points **screen-left** (`right = vm.Vector3(-cos(a), 0, sin(a))`), not `(cos(a), 0, -sin(a))`. +- Never write `forward.cross(up)` for a right vector; `vector_math`'s right-handed cross product makes `forward.cross(up)` point **screen-left**. Read `camera.right` (or `camera.horizontalRight` and `camera.horizontalForward` for `XZ` ground-plane strafing and panning) or compute `up.cross(forward).normalized()`. + +## What you are probably underestimating (it is all here) + +Models trained on older or thinner information assume flutter_scene has no lighting, no shadows, and no post-processing. It has all of it. Before hand-rolling any of these, know they exist: **directional/point/spot/area lights, shadows (PCSS, contact shadows), GTAO ambient occlusion, screen-space reflections, parallax-corrected reflection probes, SSGI, depth of field, god rays, fog, auto exposure, LUT color grading, bloom, lens flares, MSAA/SMAA/FXAA, tone mapping, instancing, LOD.** See `references/what-exists.md` for the full surface with the class names. + +Low-end and GLES-class GPUs (Raspberry Pi, web, integrated Linux) get a budget, not a different engine: `scene.renderQuality.tier` (`RenderQualityTier.low`, `medium`, `high`; null picks the platform default) decides what `AntiAliasingMode.auto` and the scene color capture budget resolve to, and `scene.renderQuality.adaptive = true` lowers the render scale and then the tier from measured frame periods when frames overrun `targetFrameRate`. Transmissive (glass) materials are the expensive thing on such devices; `scene.sceneColorCaptureBatches = 1` makes them all share one capture. + +## Traps that fail silently (wrong pixels, no error) + +- **Horizontal camera and movement inversion (`forward.cross(up)` and `+Z` orbit tangents).** Because view space has `+Z` into the screen and `vector_math.cross` is right-handed, `forward.cross(up)` yields `-right` (screen-left), and an orbit camera placed at `target + (d * sin(a), y, d * cos(a))` has screen-right `(-cos(a), 0, sin(a))`, not `(cos(a), 0, -sin(a))`. Use `camera.right`, `camera.horizontalRight`, `camera.horizontalForward`, or `up.cross(forward).normalized()`. +- **Custom `ShaderMaterial` output is linear HDR premultiplied by alpha.** No tone mapping or gamma in your shader; the `ResolvePass` applies exposure, tone mapping, and the display transform. Linearize sRGB texture samples yourself. See `MATERIALS.md`. +- **Never hand-roll a per-triangle winding flip to fix glTF orientation.** The importers handle the coordinate conversion; a manual flip leaves normals and IBL wrong. +- **Do not emit a vertex buffer at the wrong stride.** Unskinned is 72 bytes/vertex, skinned is 104; the attribute order is fixed. Use `GeometryBuilder`, do not guess the layout. +- **Surfaces that share a plane flicker as the camera moves (z-fighting), and one screenshot can look fine.** + - Never let two different-looking faces overlap in one plane. Repeated pieces abut (a piece's length equals its spacing), perpendicular runs meet at a corner post, and flat patches at one height never overlap. No depth setting fixes an exact overlap. + - An overlay that lies on a surface (a sign or screen on a wall, road paint, a decal quad, a rug) gets `material.depthLayer = 1` and wins at any distance; an overlay on that overlay gets 2. Place it flush or a few millimetres off, not at a guessed gap. + - Leave the camera's near plane at its default unless geometry clips. The engine fits the plane it rasterizes with to visible content every frame. + - Prove it in motion. `await scene.probeDepthConflicts()` names every pair of nodes that trades pixels from a camera; drive its `conflicts` to empty. `scene.debug.overlays.add(DebugOverlay.depthConflicts)` marks fights live in magenta, and debug builds print overlapping coplanar faces once the scene holds still. +- **When pixels look wrong, look at the surface before guessing.** `scene.debug.view = const DebugView(channel: SurfaceDebugChannel.roughness)` (or `uv0`, `worldNormal`, `baseColor`, `validation`, any `SurfaceDebugChannel`) replaces the lit result with that value on every material, `scene.debug.split = 0.5` compares it against the lit half, `scene.debug.overlays.add(DebugOverlay.wireframe)` traces the mesh, and `node.debugView = DebugView.none` excludes a subtree. The material channels need the views compiled in, which costs shader build time, so they are off by default: set `hooks: user_defines: flutter_scene: debug_views: true` in the app's pubspec (the workspace root's in a pub workspace), plus `flutter_scene_debug_views: true` under the app's own name if its hook calls `buildMaterials` or `buildEngineAssets`, and rebuild. Without it only the geometry and identity channels draw, and the app prints how to turn them on. A `.fmat` shows any intermediate through `material.debug` and the `custom` channel. + +## More depth + +- `references/architecture.md` for the declarative-vs-imperative choice in depth, the `Game`-class pattern, component-driven nodes, and hybrid interop. +- `references/what-exists.md` for the full API surface (the false-absence fix). +- `references/traps.md` for the complete silent-failure list. +- `references/depth-and-layering.md` for why surfaces fight, a catalog of the overlaps generated content makes, and how to find and fix them. +- The repo-root `MATERIALS.md` for the custom-shader contract. + +## Keeping this skill current + +This skill ships inside the flutter_scene package, so upgrading flutter_scene can carry a newer revision of it than the copy installed in the project. To check, run: + +```sh +dart run flutter_scene:skills --check +``` + +It reports the installed and bundled skill versions and exits non-zero when an update is available. If the installed flutter_scene ships a newer skill than what is installed, tell the user, since they are working against out-of-date guidance, and offer to update it with `dart run flutter_scene:skills` (which touches only the skill, not their build hook or pubspec). Worth a check when you start substantial flutter_scene work or when the user mentions upgrading the package. \ No newline at end of file diff --git a/.claude/skills/flutter_scene-idioms/references/architecture.md b/.claude/skills/flutter_scene-idioms/references/architecture.md new file mode 100644 index 00000000..4bb72229 --- /dev/null +++ b/.claude/skills/flutter_scene-idioms/references/architecture.md @@ -0,0 +1,315 @@ +# Structuring a flutter_scene app + +Which API to build the scene with, and how to combine them. The short version is in SKILL.md; +this is the depth, with patterns that compile against 0.22.0. + +flutter_scene exposes the same scene graph two ways. The declarative widgets describe it as Flutter +widgets that rebuild-diff into the graph; the imperative API hands you the retained `Scene`/`Node`/ +`Component` graph directly. They are not competing renderers, they drive the same engine. The choice +is about how your app tracks state, and it is worth making deliberately because reworking a large +scene from one to the other is a rewrite. + +--- + +## The decision + +Go **declarative** when app state maps directly onto a fixed set of shown objects and nothing +simulates. A product configurator, a data-driven diagram, a few models whose transforms follow some +`setState` values. Flutter already owns the state, and the widgets keep the scene tracking it for +free. + +Go **imperative** when the scene simulates. Complex physics, network replication, a character +walking around under input, procedural generation. Any one of these means imperative. Here the +scene's state is the app's state, it changes every frame, and you want to own the loop rather than +express each frame as a widget rebuild. + +If you are unsure, ask whether anything in the scene changes on its own between user actions. If yes, +imperative. If the scene only changes when the user changes a value, declarative. + +--- + +## Declarative + +`SceneView.declarative` owns an internal `Scene`; its `children` are the whole scene description. + +```dart +class Configurator extends StatefulWidget { + const Configurator({super.key}); + @override + State createState() => _ConfiguratorState(); +} + +class _ConfiguratorState extends State { + // Build engine objects once, not per rebuild. Constructing GPU resources + // every build is the main performance hazard of the declarative layer. + final Geometry _geometry = CuboidGeometry(vm.Vector3(1, 1, 1)); + final PhysicallyBasedMaterial _material = PhysicallyBasedMaterial(); + double _spin = 0; + + @override + Widget build(BuildContext context) { + return Column(children: [ + Expanded( + child: SceneView.declarative( + camera: PerspectiveCamera(position: vm.Vector3(2, 2, -4)), + children: [ + SceneMesh( + geometry: _geometry, + material: _material, + rotation: vm.Quaternion.axisAngle(vm.Vector3(0, 1, 0), _spin), + ), + ], + ), + ), + Slider( + value: _spin, + max: 6.28, + onChanged: (v) => setState(() => _spin = v), + ), + ]); + } +} +``` + +The scene tracks widget state through the normal rebuild path. Note two things the example shows: +engine objects (`geometry`, `material`) are created once and held as fields (they are diffed by +identity, and rebuilding them every frame is the classic mistake), while cheap value props +(`rotation`) are fine to pass fresh each build. + +Declarative building blocks (all under `SceneView.declarative` or a `SceneView` with `children`): + +- `SceneMesh(geometry:, material:, ...)` a node with a mesh. +- `SceneNode(...)` a bare transform node, for grouping children. +- `SceneModel('assets/x.glb', animations: [...])` a loaded model (runtime glTF path). +- `SceneSubtree(parent:, children:)` mounts children under a given imperative `Node`. +- Every node widget takes `position`/`rotation`/`scale` (or a full `transform`), `visible`, + `components:` (attach imperative `Component`s), `controller:` (a `SceneNodeController` handle), and + `children:`. + +--- + +## Imperative + +Own the `Scene`, add `Node`s, attach `Component`s, display with `SceneView(scene, camera:, onTick:)`. +For anything beyond a demo, do not scatter this across a `StatefulWidget`. Put it in a plain Dart +class that owns the scene and the game state, and keep the widget thin. + +```dart +// Pure Dart, no Flutter import. Owns the scene and the game state. +class Game { + final Scene scene = Scene(); + late final Node player; + + Future load() async { + await Scene.initializeStaticResources(); + + // The camera lives in the scene as a node, not on the widget. A camera + // node's transform is its view: the translation is the eye, local +Z is + // the look direction, +Y is up. lookAtFrom sets both at once, so there is + // no view-matrix math to hand-roll. + final cameraNode = Node() + ..addComponent(CameraComponent(activateOnMount: true)) + ..lookAtFrom(vm.Vector3(0, 3, -8), vm.Vector3.zero()); + scene.add(cameraNode); + // activateOnMount makes this the scene's primary camera when the node + // mounts, so SceneView needs no `camera:` argument. (The first mounted + // camera auto-promotes anyway; this states the intent explicitly, and is + // how you pick one when several cameras exist.) + + player = Node(mesh: Mesh(CuboidGeometry(vm.Vector3(1, 1, 1)), + PhysicallyBasedMaterial())); + player.addComponent(PlayerController()); + scene.add(player); + } + + // Per-frame app logic that is not tied to one node. Component updates run + // on their own (see below), so this is for whole-game concerns. + void tick(double dt) { + // advance timers, spawn waves, read input, etc. + } +} +``` + +```dart +// Thin widget: builds the game, forwards ticks, renders the scene. +class GameView extends StatefulWidget { + const GameView({super.key}); + @override + State createState() => _GameViewState(); +} + +class _GameViewState extends State { + final Game game = Game(); + bool _ready = false; + + @override + void initState() { + super.initState(); + game.load().then((_) { + if (mounted) setState(() => _ready = true); + }); + } + + @override + Widget build(BuildContext context) { + if (!_ready) return const SizedBox.expand(); + // No `camera:` here: the view resolves the scene's active camera, which is + // the CameraComponent added in Game.load. Resolution order is the explicit + // `camera:` (absent), then `cameraBuilder`, then `scene.camera` (the active + // CameraComponent), then a default camera. + return SceneView( + game.scene, + onTick: (elapsed, dt) => game.tick(dt), + ); + } +} +``` + +### The active camera + +The scene owns which camera is active, and there are three levers: + +- **`CameraComponent(activateOnMount: true)`** (above) selects this camera when its node mounts. +- **`cameraComponent.makeActive()`** switches to it at runtime, for example a chase-cam to a + cutscene camera. Before its node mounts the choice is deferred and applied on mount. +- **`scene.camera = someCamera`** sets any `Camera` as the override directly, and `scene.camera` + reads the active one back. + +With no camera set at all, the first mounted `CameraComponent` auto-promotes, and a scene with none +still renders through a default camera. Move or rotate a `CameraComponent`'s node to move the view; +the `NodeCamera` reads the node's world transform live each frame. Aim it with `node.lookAt(target)` +(rotate toward a world point) or `node.lookAtFrom(eye, target)` (position and aim in one call); +Z is +the forward axis, so the same helpers aim lights and imported models. A follow-cam is then a one-line +component that calls `node.lookAtFrom(...)` in `update` each frame. + +### Camera controllers (interactive cameras) + +For a user-controlled camera, do not hand-roll the drag/scroll/key math: attach a camera controller +component to the camera node. `OrbitCameraController` (turntable around a target, drag rotates, scroll +dollies), `FlyCameraController` (WASD + drag free flight, `moveVertical: false` gives grounded +first-person), and `FollowCameraController` (third-person that eases behind a target node). Each holds +the camera state, eases toward it with frame-rate-independent smoothing, clamps pitch so the view +never flips, and writes the node via `lookAtFrom`. + +Wire input with the `CameraControls` widget wrapping the view; it forwards Flutter gestures and keys +to the controller. `SceneView` itself has no camera-input knobs, so nothing camera-specific leaks into +it. + +```dart +final camera = Node() + ..addComponent(CameraComponent(activateOnMount: true)) + ..addComponent(OrbitCameraController(target: vm.Vector3.zero(), distance: 8)); +scene.add(camera); + +// In build: +return CameraControls( + controller: camera.getComponent()!, + child: SceneView(scene), +); +``` + +The controllers also expose intent methods (`orbitBy`, `dollyBy`, `panBy`, `look`), so an app with its +own input handling can drive them without the widget. + +### Game input (flutter_scene_input) + +For games (rebindable controls, gamepads, pointer lock, pause menus), do not hand-roll held-key sets or +raw `gamepads` parsing: add the separate `flutter_scene_input` package. Declare typed const actions +(`VectorAction('move')`, `ButtonAction('jump')`, `DeltaAction('look')`), bind them in an `ActionSet` +with named slots (`'keyboard'`, `'gamepad'`), push the set on `player.contexts`, and call +`scene.attachInput()` so read windows advance with the scene. Read `player.button(jump).justPressed` +and `player.vector(move)` in `update`, and through `player.fixed` in `fixedUpdate`. Drivers +(`CharacterInputDriver`, `FlyCameraInputDriver`, `OrbitCameraInputDriver`) feed the bundled +controllers, and `DefaultActions` has ready-made sets. + +Traps. 2D values are +Y up everywhere, mouse movement included, so convert before passing a drag +`Offset` to `look`. A stick feeding a `DeltaAction` needs a `PerSecond` processor. Wrap the game view (not +the HUD) in `InputListener` for mouse input. Lock the cursor with `PointerLock.instance.lock()` from a +pointer or tap handler and pause when a listener sees `isLocked` go false; never add the `pointer_lock` +package. Rebinding goes through `player.overrides` and `listenForBinding`, and profiles persist with +`overrides.toJsonString()`. + +### Behavior lives in components, not in the tick + +The bulk of per-object logic should be custom `Component`s, not a giant `onTick`. A component is +attached to a node and the engine runs it through the lifecycle. Crucially, component ticks are +driven automatically by the render path, so you do not call them yourself, and `onTick` is only for +game-wide concerns that do not belong to a single node. + +```dart +class PlayerController extends Component { + vm.Vector3 velocity = vm.Vector3.zero(); + + @override + void onMount() { + // node is available here; wire up input, cache references. + } + + @override + void update(double deltaSeconds) { + // `node` is the node this component is attached to. + node.mutateLocalTransform( + (m) => m.translateByVector3(velocity * deltaSeconds), + ); + } +} +``` + +Component lifecycle hooks (subclass `Component`, override what you need): + +- `onAttach()` added to a node, before it is in a live scene. +- `Future onLoad()` async setup (await assets); mount waits for it. +- `onMount()` the node entered a live scene; `node` is usable. +- `update(double deltaSeconds)` per rendered frame. +- `fixedUpdate(double fixedDt)` fixed-step, driven by the physics accumulator when a `PhysicsWorld` + is present. Put physics-coupled logic here, not in `update`. +- `onUnmount()` / `onDetach()` teardown. + +This is the structure that scales. A character is a node with a controller component; an enemy is a +node with an AI component; a pickup is a node with a trigger component. The `Game` class holds what +is genuinely global (score, wave state, the input map), and everything spatial is a component on a +node. + +--- + +## Hybrid interop + +The two APIs share one graph, so you can mix them at the seam that suits the app. + +**Declarative shell, imperative pockets.** A declarative node accepts `components:`, so an otherwise +declarative scene can attach imperative behavior to any node without leaving the widget tree. + +```dart +SceneMesh( + geometry: _geometry, + material: _material, + components: [Spinner()], // a custom Component, ticked by the engine +) +``` + +**Imperative scene, declarative subtrees.** `SceneView(scene, children: [...])` mounts declarative +widgets over an app-owned scene. Use `SceneSubtree(parent: someNode, children: [...])` to attach a +declarative subtree under a specific imperative node, for example UI-like markers that follow a +game object. + +```dart +SceneView( + game.scene, + camera: PerspectiveCamera(position: vm.Vector3(0, 3, -8)), + children: [ + SceneSubtree( + parent: game.player, + children: [SceneModel('assets/hat.glb')], + ), + ], +) +``` + +**Reaching an imperative node from a declarative widget.** Pass a `SceneNodeController` as +`controller:` and read `controller.node` for the managed `Node` (null while unmounted). This is the +escape hatch when a declarative node needs an imperative handle for a one-off operation. + +The rule of thumb: pick the mode that matches how the *majority* of the scene is driven, then use the +seam above for the exceptions. Do not build a whole simulation out of declarative widgets to avoid +the imperative API, and do not hand-roll a diffing layer over the imperative graph to avoid the +declarative one. diff --git a/.claude/skills/flutter_scene-idioms/references/depth-and-layering.md b/.claude/skills/flutter_scene-idioms/references/depth-and-layering.md new file mode 100644 index 00000000..c2aa0994 --- /dev/null +++ b/.claude/skills/flutter_scene-idioms/references/depth-and-layering.md @@ -0,0 +1,64 @@ +# Depth and layering + +Two surfaces that cover the same pixels at nearly the same depth flicker against each other as the camera moves (z-fighting). One screenshot rarely shows it, because each frame picks a winner and a different frame picks the other one. This page covers why it happens, the overlaps generated scenes make most, how to fix each, and how to prove a scene is clean. + +## Why surfaces fight + +**Exact overlaps.** Two faces in one plane built from different vertices each compute their depth from their own rounded vertex positions and from the rasterizer's sub-pixel snapping. The difference is noise whose sign changes across the surface and with any camera motion. On a surface seen at a grazing angle that noise is large, a fraction of a pixel's worth of the surface's own depth slope. No near plane, depth format, or precision fixes an exact overlap; only an order does (a layer, a content change). + +**Near overlaps.** Two parallel faces a few millimetres or centimetres apart hold their order up close and fight past the distance where the depth buffer can no longer separate them. flutter_scene spends depth precision where content is: it fits the near plane it rasterizes with to visible geometry every frame, and on Metal and most browsers it stores reversed float depth, which keeps precision nearly even with distance. The `depthGap` debug view (below) shows how far apart two surfaces must be at each pixel. + +## The overlaps generated scenes make, and the fix for each + +| Pattern | Why it fights | Do instead | +| --- | --- | --- | +| A fence, barrier, curb, or rail of repeated pieces longer than their spacing (8.4 m pieces every 8 m) | every joint holds two faces in one plane, and alternating colors make it visible | make each piece exactly as long as the spacing, or put a slightly larger post at each joint | +| Perpendicular runs that both extend into a corner | the corner holds overlapping faces | stop one run at the other's face, or cap the corner with a post | +| Ground patches, tiles, or rugs at one height that overlap each other | same-height faces of different colors | place them without overlap (`PoissonDiscSampler` in the kit), or give each overlapping kind its own `depthLayer` | +| A screen, sign, window, poster, or road marking flush with (or a few millimetres off) its surface | exact or near overlap | `material.depthLayer = 1` on the overlay; 2 for an overlay on that overlay | +| A box wrapped by a slightly larger box (window bands, trim, a frame) | parallel faces a few centimetres apart, seen from far away | keep the wrap a clear distance proud, put wraps on separate height slots so two never overlap, or bake the detail into the material | +| The same mesh added twice (a duplicated node, a model loaded into two parents) | bit-identical faces | remove the duplicate | +| `doubleSided` forced on materials whose meshes have back-to-back faces | culling no longer hides the coincident faces | leave `doubleSided` off unless the mesh needs it | +| An object placed at a guessed surface height | it hovers or sinks a few centimetres, or sits exactly in the surface | measure the surface with `scene.raycast` or the asset's bounds before placing | + +## Mechanisms, in order of preference + +1. **Fix the content.** Abut, space, or merge the pieces. Nothing fights when nothing overlaps. +2. **`Material.depthLayer`.** For an overlay that lies on a surface. A higher layer draws over coplanar surfaces with a lower one at any distance and on every backend. Each layer moves the surface a quarter pixel of its own depth slope plus a few depth steps toward the camera, in the vertex stage, so it costs nothing per fragment and moves nothing on screen by more than a fraction of a pixel. Negative layers let a ground yield to everything on it. Two different materials at the same layer that overlap still fight. +3. **`DecalNode`.** Projects a mark onto uneven surfaces without creating coplanar geometry. +4. **Bake detail into the material.** A grid of lit windows as a pattern in a `.fmat` instead of thousands of boxes on a facade. +5. **`Material.depthBias`.** A world-space nudge toward the camera. It buys fewer depth steps the farther away it is seen, so it holds up close and fails far away. Prefer `depthLayer`. +6. **`Scene.coplanarTieBreak`.** An experimental safety net for generated content. It gives every unlayered material and instance an arbitrary rank so exact overlaps resolve to a stable winner. The rank is arbitrary, so an overlay can end up consistently hidden, and surfaces a sliver apart far from the camera can swap. Fix the overlap or set `depthLayer` instead where you can. + +## The camera + +Leave `near` at its default unless geometry clips. The engine rasterizes with the largest near plane that clips nothing visible and keeps the authored one as a floor, so a chase or aerial camera gets the precision of a much larger near for free (`Scene.fitNearPlane`, on by default). The fit falls back to the authored near when the camera sits inside an item's bounds (a sky sphere, one merged city mesh) or an item has no bounds; debug builds print which node blocked it. Draw a sky with `scene.skybox` rather than a giant sphere around the camera. + +`Scene.reversedDepth` (on by default) stores depth reversed, which keeps float depth precise at every distance. Turn it off only for a custom `ShaderMaterial` vertex shader that writes clip-space depth assuming the standard mapping. + +## Proving a scene is clean + +Check in motion, with numbers, not with a screenshot. + +- **`await scene.probeDepthConflicts()`** renders the view's object ids several times (reversed draw order, changed depth rounding, sub-pixel camera offsets, and surfaces nudged a sliver apart) and reports every pair of nodes whose pixels change hands. `report.conflicts` is empty when nothing fights; `report.describe()` is a short log with node paths, pixel counts, and distances. Probe from the cameras the app actually uses, after the scene has rendered a frame. Surfaces sharing a material still count, since vertex colors or texture coordinates can tell them apart. Blended surfaces and a `.fmat` cut by its own `Surface()` are listed in `report.untested` rather than tested. + + ```dart + final report = await scene.probeDepthConflicts(camera: camera); + if (report.conflicts.isNotEmpty) debugPrint(report.describe()); + ``` + +- **`scene.findCoplanarOverlaps()`** checks the geometry without rendering and returns every pair of faces that overlap in one plane (or sit closer than the depth buffer can separate from the camera), with the overlap area, where it is, and for repeated pieces the length that fixes it. Unlike the probe, it also catches instances of one `InstancedMesh` whose colors differ overlapping each other. Debug builds run it once the scene holds still and print a summary; set `scene.debugCheckCoplanarOverlaps = false` to silence it. +- **`scene.debug.overlays.add(DebugOverlay.depthConflicts)`** marks fighting pixels live with a crawling magenta checkerboard. +- **`scene.debug.view = const DebugView(channel: SurfaceDebugChannel.depthGap)`** colors each pixel by the gap two surfaces need there to keep their order, blue for a hundredth of a millimetre through red for a metre. An overlay closer to its surface than that gap needs a layer. +- **`SurfaceDebugChannel.objectColor`** shows two node colors interleaving where they fight. + +The tells of a fight without tools: a material edit that changes nothing on screen (the other surface is winning there), a surface that looks right from one camera only, stripes or speckles that crawl as the camera moves, and flicker that only appears at grazing angles or far away. + +## Habits that do not work + +- **`Node.renderOrder` does not order opaque coplanar faces.** Depth decides between them, not draw order. +- **Turning off the depth test** draws the overlay through everything in front of it. +- **Forcing `doubleSided` broadly** exposes coincident back faces. +- **Shrinking the far plane** barely changes precision; the near plane dominates, and the engine already fits it. +- **A tiny fixed world offset** for distant content fails past a distance; use `depthLayer`. +- **Polygon offset factors** have no API here; `depthLayer` is the slope-aware offset. diff --git a/.claude/skills/flutter_scene-idioms/references/traps.md b/.claude/skills/flutter_scene-idioms/references/traps.md new file mode 100644 index 00000000..d36e9fb2 --- /dev/null +++ b/.claude/skills/flutter_scene-idioms/references/traps.md @@ -0,0 +1,632 @@ +# flutter_scene silent-failure traps + +Mistakes that produce wrong pixels with no exception and no console message. Each entry gives the +mistake, what you see, and what to do instead. Sorted worst-first (most likely to hit, hardest to +diagnose from the symptom). + +Some of these are now caught by the engine in version 0.22.0. Those are tagged **[0.22.0 catches +this]** with what the engine does, so if you see that error you know what it means. The rest are +still silent, so you have to recognize them yourself. + +--- + +## 1. Editing a transform in place instead of assigning it + +**Mistake.** `node.localTransform.setTranslation(v)`, `node.localTransform..rotateY(t)`, +`node.position.x = 5`, or any edit of the matrix/vector a getter returns. This is the natural +`vector_math` style and the first thing most people reach for. + +**Symptom.** The node does not move. Not "moves wrong", nothing happens, forever, including its +children and bounds. Reading `node.localTransform`/`node.position` back shows the value you wrote, so +the state looks correct while the render disagrees. + +**Do instead.** Assign a fresh value (`node.position = ...`, `node.localTransform = node.localTransform.clone()..translateByVector3(v)`), +use the component setters `node.position`/`node.rotation`/`node.scale`, or edit the raw matrix +through `node.mutateLocalTransform((m) => m.translateByVector3(v))`, which dirties the cache for you. + +**[0.22.0 catches this]** Debug builds throw a `StateError` naming the node and the fix, both for an +in-place `localTransform` edit and for editing a copy returned by `position`/`rotation`/`scale`. + +--- + +## 2. Faces of different surfaces overlapping in one plane + +**Mistake.** Repeated pieces longer than their spacing (an 8.4 m barrier every 8 m), runs that both +extend into a corner, patches of different colors overlapping at one height, a sign, screen, or +road marking laid flush on its surface, or the same mesh added twice. + +**Symptom.** The overlap flickers between the two surfaces as the camera moves (z-fighting): stripes, +speckles, or triangles of the wrong color that crawl. Any one screenshot can look right, and no near +plane or depth setting fixes an exact overlap. + +**Do instead.** Make repeated pieces abut (length equals spacing) and cap joints with a post, keep +same-height patches apart, and give an overlay that lies on a surface `material.depthLayer = 1`. +`await scene.probeDepthConflicts()` names every pair that fights from a camera, and +`scene.findCoplanarOverlaps()` lists the overlapping faces with the length that fixes a repeated +piece. See `depth-and-layering.md`. + +**[Caught in debug]** Debug builds print the overlapping coplanar faces once the scene holds still, +naming the nodes and the fix. + +--- + +## 3. Passing a normal or metallic-roughness map as `TextureContent.color` + +**Mistake.** `material.normalTexture = await Texture2D.fromAsset('brick_normal.png')` without +`content: TextureContent.normal`. The `content` parameter defaults to `color`. + +**Symptom.** The base mip is fine, so it looks right up close and progressively wrong with distance: +normals flatten and skew, roughness reads too smooth at range, specular shimmers. A +distance-dependent symptom is nearly the worst case for screenshot-driven iteration. + +**Do instead.** Build non-color maps with the right content: `Texture2D.fromAsset(path, content: +TextureContent.normal)` for normal maps, `TextureContent.data` for metallic-roughness, AO, and other +linear data. Still silent, so this is on you. + +--- + +## 4. Non-uniform scale on a lit mesh + +**Mistake.** Any non-uniform scale on the node or an ancestor, e.g. `node.scale = Vector3(1, 3, 1)`. + +**Symptom.** Lighting, specular, and reflections are wrong across the whole mesh. It reads as a +shading or material bug, so it sends you into the materials, never the transform. + +**Do instead.** Use a uniform scale, or bake the non-uniform scale into the geometry with +`MeshData.transformed(matrix)` and build a fresh `MeshGeometry` from it. Still silent. + +--- + +## 5. Moving a skinned mesh node + +**Mistake.** `skinnedNode.localTransform = Matrix4.translation(v)` on a node that carries a `Skin`. + +**Symptom.** The mesh does not move. Worse, it *does* move when the node you transformed happens to +be an ancestor of the skeleton's joints, so it looks intermittent across models. + +**Do instead.** glTF requires a skinned mesh node's own transform to be ignored, so the engine passes +identity. Move the skeleton root (the common ancestor of `skin.joints`) instead, or parent both the +mesh node and the skeleton under a shared node and move that. Still silent. + +--- + +## 6. Replacing the transform of a runtime-imported model root + +**Mistake.** +```dart +final model = await Node.fromGlbAsset('assets/ship.glb'); +model.localTransform = Matrix4.translation(v); // wipes the handedness flip +``` + +**Symptom.** The model renders mirrored through Z (asymmetric geometry reversed, text backwards) with +normals and IBL wrong for the mirrored orientation. It still draws and is not obviously broken. + +**Do instead.** The runtime glTF importer synthesizes a root carrying a `scale(1, 1, -1)` handedness +flip. Do not overwrite it. Parent that node under a new `Node` and transform the parent, or +pre-multiply your transform by the existing `localTransform`. (The offline `.fscene`/`loadScene` path +bakes handedness into the vertices, so its roots are identity and do not have this trap.) Still +silent. + +--- + +## 7. `EnvironmentMap.fromGpuTextures` with a raw panorama + +**Mistake.** +```dart +final tex = await gpuTextureFromAsset('assets/panorama.png'); +scene.environment = EnvironmentMap.fromGpuTextures(prefilteredRadiance: tex); +``` + +**Symptom.** Every reflective surface (and the sky) shows the top 1/8 of the panorama stretched 8x +vertically, cross-fading between slices as roughness varies. Diffuse is fully black. + +**Do instead.** `fromGpuTextures` expects an already-prefiltered radiance atlas, not a plain image. +Run the source through `prefilterEquirectRadiance()` first, or just use +`EnvironmentMap.fromEquirectImageAsset(assetPath: ...)`/`fromUIImages`, which prefilter and project +SH for you. Still silent. + +--- + +## 8. `ShaderMaterial(cullingMode: none)` for a double-sided custom material + +**Mistake.** `ShaderMaterial(cullingMode: gpu.CullMode.none)`, or setting `doubleSided = true` on a +`ShaderMaterial`. The two do not agree. + +**Symptom.** `cullingMode.none` draws back faces in color but leaves them out of the depth prepass, so +SSAO, SSR, and contact shadows sample the wrong surface exactly where a back face shows: dark halos, +wrong reflections, occlusion bleeding through a leaf card. `doubleSided = true` does the opposite: the +color pass still culls while the prepass draws the extra faces. + +**Do instead.** For a truly double-sided `ShaderMaterial`, understand that the color cull and the +prepass cull are driven separately today; keep the mesh single-sided where the depth-based effects +need to match, or split it. Still silent. + +--- + +## 9. Caller-supplied `bounds` that do not cover the geometry + +**Mistake.** `MeshGeometry.fromArrays(positions: p, bounds: someAabb)` where the AABB does not contain +every position, or `setLocalBounds` with a guessed or stale box. + +**Symptom.** The mesh pops out of existence at some camera angles and reappears at others; part of a +large mesh vanishes; shadows disappear before the caster does. Intermittent and view-dependent, so a +single screenshot can look fine. + +**Do instead.** Widen the bounds to cover every vertex, or just omit the `bounds` argument and let the +constructor scan the positions. An absent bounds is safe (it means always-visible); only a wrong one +is dangerous. Still silent. + +--- + +## 10. Binding a mipless texture to a material + +**Mistake.** `material.baseColorTexture = GpuTextureSource(await gpuTextureFromAsset('brick.png'))`. +The helper's own doc even recommends this. + +**Symptom.** Severe minification aliasing: crawling and shimmering on any surface at an angle or +distance, sparkling on a metallic-roughness map. `Texture2D.fromAsset` on the same file looks fine, so +two seemingly equivalent APIs disagree visually. + +**Do instead.** Use `Texture2D.fromAsset`/`fromImage`/`fromPixels`, which generate a mip chain, or +supply a texture you mipped yourself. Reserve `gpuTextureFromAsset` for non-material uses. Still +silent. + +--- + +## 11. `RenderView.layerMask`/`Node.layers` mismatch, or a zero mask + +**Mistake.** Putting a node on a non-default layer and forgetting the view side (or vice versa), or +`layerMask: 0`, or `Node.layers = 2` meaning to select "layer 2" (which is actually `1 << 2 == 4`). + +**Symptom.** A node, a group, or the whole scene is simply absent, with no hint a mask is involved. A +`layerMask` of 0 renders nothing. + +**Do instead.** `Node.layers` is a bitmask and is NOT inherited by children, so set it on each node +you want the view to see. Use `kRenderLayerAll` to see everything, or a bitmask like `(1 << 2)`. +Match the view's `layerMask` to the nodes' `layers`. Still silent (but see #24 for the +draws-nothing diagnostic). + +--- + +## 12. Mutating a `TextureTransform` in place + +**Mistake.** `material.baseColorTextureTransform.offset.x = 0.5` instead of assigning a fresh +`TextureTransform`. Same shape as trap #1, for materials. + +**Symptom.** UV scroll/rotation animation freezes at the first value, but ONLY for materials on the +physical-variant path (any material with clearcoat/sheen/transmission/etc). The identical code works +on a plain PBR material, so it reads as a shader bug. + +**Do instead.** Assign a new transform each frame: `material.baseColorTextureTransform = +TextureTransform(offset: ...)`. Still silent. + +--- + +## 13. Environment image that is not 2:1 equirectangular + +**Mistake.** Passing a cube cross, a 1:1 angular light probe, or a cropped panorama to any environment +entry point. HDRI downloads are not reliably 2:1. + +**Symptom.** The scene is lit from wildly wrong directions, reflections show mirrored or duplicated +content, the sky is smeared. + +**Do instead.** Re-project the source to a 2:1 latitude-longitude panorama before loading. Cube +crosses and angular probes are not supported. Still silent. + +--- + +## 14. Hand-built triangles wound clockwise + +**Mistake.** Generating triangles with clockwise winding instead of counter-clockwise (CCW) winding around the outward normal when feeding `MeshGeometry.fromArrays` or `GeometryBuilder`. + +**Symptom.** The mesh is invisible from outside and visible from inside; a closed shape looks hollow or inside-out; lighting is inverted where it shows. ("See-through faces.") + +**Do instead.** flutter_scene's front faces wind COUNTER-CLOCKWISE (CCW) in model space around the outward face normal, matching glTF. Ensure triangle indices wind CCW when viewed from outside the surface, or omit `normals` and let `GeometryBuilder` derive them from your winding. Still silent. + +--- + +## 15. Out-of-range indices in `fromArrays` + +**Mistake.** `MeshGeometry.fromArrays(positions: p /* 100 verts */, indices: [0, 1, 100])`, e.g. from +an off-by-one or an index list built against a different vertex array. + +**Symptom.** Stray triangles stretching to the origin or infinity, holes, flicker. On some backends +the fetch is clamped and on others it reads adjacent memory, so the symptom differs per backend. + +**Do instead.** Keep every index in `0 .. vertexCount - 1`. (`GeometryBuilder.addTriangle` range-checks +for you and throws; the `fromArrays` index path does not.) Still silent on the `fromArrays` path. + +--- + +## 16. A `vertexCount` that does not match the buffer in `setVertices` + +**Mistake.** `geometry.setVertices(bufferView, vertexCount)` where `vertexCount` is a byte count, a +float count, or a triangle count rather than a vertex count. + +**Symptom.** Too small: part of the mesh is missing. Too large: the draw reads past the buffer, giving +stray geometry or a dropped draw depending on backend. The buffer is fine, so the investigation goes +to the packing code. + +**Do instead.** `vertexCount` is a count of vertices. Prefer `uploadVertexData` (which validates the +stride, see #18) or `fromArrays` over the caller-managed `setVertices` path unless you really own the +GPU buffer. Still silent. + +--- + +## 17. Oversized texture on a low-end device + +**Mistake.** `EnvironmentMap.fromEquirectImageAsset(assetPath: 'pano_16k.hdr', maxWidth: 16384)` or +`EnvironmentMap.radianceCubeSize = 4096` on a device whose max texture size is lower. + +**Symptom.** A completely black environment: no IBL, no reflections, black sky. Works on the dev +machine, black on a phone. + +**Do instead.** Keep environment and texture sizes within the device limit; lower `maxWidth` or +`radianceCubeSize`. Test on the lowest-end target you support. Still silent. + +--- + +## 18. Hand-packing vertex bytes at the wrong stride + +**Mistake.** `SkinnedGeometry()..uploadVertexData(bytes, vertexCount, indices)` with the wrong stride +(a common one is 96 bytes having forgotten UV1, or the legacy 80-byte layout). + +**Symptom.** Washed-out colors, see-through faces, geometry smeared toward the origin. + +**Do instead.** Unskinned vertices are 72 bytes (position 3, normal 3, tex_coords 2, tex_coords_1 2, +color 4, tangent 4, all float32), skinned are 104 (+ joints 4, weights 4). Better, do not hand-pack: +use `MeshGeometry.fromArrays`, `fromMeshData`, or `GeometryBuilder`. + +**[0.22.0 catches this]** `uploadVertexData` on both `SkinnedGeometry` and `UnskinnedGeometry` now +throws an `ArgumentError` when the byte length does not match `vertexCount * stride`, naming the +expected layout. + +--- + +## 19. Custom attribute length not matching the vertex count + +**Mistake.** `geometry.setCustomAttribute('a_wind', data, components: 3)` where `data` has the wrong +length, or set before uploading vertices, or not re-set after a `rebuild` changed the count. + +**Symptom.** The attribute is read at the wrong stride, so every vertex gets a neighbor's value: a +displacement shader shears the mesh, a color attribute smears. Nearly right, so hard to spot. + +**Do instead.** `data.length` must equal `vertexCount * components`. Set the attribute after uploading +vertices, and re-set it after any rebuild. Depth and shadow passes run the material's full vertex +variant, so an attribute-driven displacement casts its shadow. The mesh and material sides need not +match: a declared attribute the mesh lacks reads zero, and a mesh's extra attributes are ignored by +materials that do not declare them. + +**[0.22.0 catches this]** `setCustomAttribute` now throws an `ArgumentError` on a length mismatch +(once the vertex count is known). + +--- + +## 20. `UnlitMaterial` with `AlphaMode.mask` + +**Mistake.** `UnlitMaterial(colorTexture: foliage)..alphaMode = AlphaMode.mask` for cutout foliage. + +**Symptom.** No alpha test. Cutout edges render soft and blended, the material goes through the +translucent pass, writes no depth, sorts badly against itself, and casts no cutout shadow. + +**Do instead.** `UnlitMaterial` does not implement `mask` (it behaves as `blend`). Use +`PhysicallyBasedMaterial` for cutouts, or a `.fmat` unlit material that discards below your cutoff. +Still silent. + +--- + +## 21. `vertexColorWeight` on a material that took a physical variant + +**Mistake.** +```dart +final m = PhysicallyBasedMaterial()..vertexColorWeight = 0.0; +m.clearcoat = 1.0; // or sheen/transmission/anisotropy/ior != 1.5/any extension texture +``` + +**Symptom.** Vertex colors snap back to full strength the moment an unrelated extension is enabled. +On a vertex-colored import, an abrupt tint change with no plausible cause. (The same gap silently +drops `specularAntiAliasingVariance` and `specularAntiAliasingThreshold` on the variant path.) + +**Do instead.** Leave `vertexColorWeight` at 1.0 when using any advanced PBR feature, or drop the +extension. Still silent. + +--- + +## 22. Vertex-stage binding on a `ShaderMaterial` with no vertex shader + +**Mistake.** +```dart +final m = ShaderMaterial(fragmentShader: frag); +m.setUniformBlock('WaveInfo', bytes, stage: ShaderStage.vertex); // never set a vertex shader +``` + +**Symptom.** The vertex-stage parameter has no effect; geometry stays undisplaced while the fragment +stage looks right. Reads as "my vertex shader is not running." + +**Do instead.** Pass a `vertexShader` (and `skinnedVertexShader`/`depthVertexShader` for those mesh +kinds) to the constructor before binding vertex-stage blocks, or bind the block on +`ShaderStage.fragment`. Still silent. + +--- + +## 23. A `ShaderMaterial` vertex shader on line/trail/polyline geometry + +**Mistake.** Attaching a `ShaderMaterial` that supplies a vertex shader to a `LineSegmentsGeometry`, a +trail, or a polyline. + +**Symptom.** Lines vanish or explode into garbage. The unskinned vertex shader is paired with the +line-segments instanced layout and never does the ribbon expansion. + +**Do instead.** These geometries do their vertex expansion in the engine's own shader; a material +vertex shader cannot be used with them. Drop the vertex shader for line/trail/polyline geometry, or +use a mesh geometry. Still silent. + +--- + +## 24. Four different causes of a blank frame + +**Mistake.** Any of: a degenerate camera (target equals position, or `up` parallel to the view +direction, e.g. a top-down camera left at the default `up`), a field of view passed in degrees +(`fovRadiansY: 60`), an inverted or zero frustum, `layerMask: 0`, a zero-area draw region, or +rendering before `Scene.isReadyToRender`. + +**Symptom.** The entire scene is empty. Every one of these looks identical, so it is easy to "fix" +lighting, materials, and geometry for many iterations before suspecting the camera or the mask. + +**Do instead.** For a top-down/bottom-up camera set `up` to `Vector3(0, 0, 1)` or `Vector3(0, 0, +-1)`, not the default `(0, 1, 0)`. Pass FOV in radians (`60 * degrees2Radians`). Keep `near > 0` and +`far > near`. Give the view a non-zero `layerMask` and a non-empty draw region. + +**[0.22.0 catches most of this]** Degenerate cameras (zero view direction, parallel `up`, degrees-valued +FOV, degenerate near/far) assert in debug. And a frame that issues zero draw calls now prints once in +debug naming the likely cause (not ready, empty region, no views, no visible meshes, or a layer mask +matching nothing). + +--- + +## 25. Missing bounds after swapping a primitive's geometry + +**Mistake.** `mesh.primitives[0].geometry = newGeometry` for hand LOD, a rebuilt procedural mesh, or a +variant swap. + +**Symptom.** The new geometry is culled against the old geometry's bounds; if it is larger or +displaced, it pops in and out exactly like trap #9. + +**Do instead.** Nothing extra is needed anymore. + +**[0.22.0 catches this]** A `Mesh` now recomputes its bounds on its own when a primitive's geometry +identity changes, so the manual `markLocalBoundsDirty()` is no longer required. + +--- + +## 26. A `.fmat` material that overruns the 15-sampler budget + +**Mistake.** A `lit` or `physical` `.fmat` declaring several `sampler2d` parameters plus +`engine_inputs: [scene_color, scene_depth]`, on top of the lit framework's own textures. + +**Symptom.** Geometry disappears on a mid-range Android device while everything is correct on Metal, +with no build-time signal. The draw is rejected on GLES drivers reporting the 16-unit minimum. + +**Do instead.** The lit fragment shader budgets 15 fragment samplers. Pack channels into one texture +(an ORM-style atlas), drop an `engine_input`, or make the material unlit. Still silent (fails at +runtime on the device, not at build). + +--- + +## 27. `RenderView.viewport` with a `target` set + +**Mistake.** `RenderView(camera: cam, target: myRenderTexture, viewport: Rect.fromLTWH(0, 0, 0.5, 1))` +expecting a half-width render into the texture. + +**Symptom.** The view fills the entire render texture; the passed `viewport` is ignored. Reads as +"my viewport math is off." + +**Do instead.** `viewport` is ignored when `target` is set. Size the `RenderTexture` to the region you +want, or drop the target to render a sub-rect of the screen. Still silent. + +--- + +## 28. Scaled or mirrored camera node + +**Mistake.** Attaching a `CameraComponent` to a scaled node, or parenting a camera node under a scaled +one. + +**Symptom.** A uniform scale rescales the world in view; a negative scale mirrors the view, so every +surface goes back-facing and the scene renders inside out. The camera's reported `forward`/`up` look +correct, which makes it hard. + +**Do instead.** A camera node must carry only rotation and translation, and no ancestor may be scaled. +Still silent. + +--- + +## 29. Hand-built `Skin` with mismatched joints and inverse-bind matrices + +**Mistake.** `skin.joints.add(n)` without a matching `skin.inverseBindMatrices.add(...)` (both are +plain mutable lists). + +**Symptom.** Extra inverse bind matrices are silently ignored and the mesh deforms wrongly. (Too few +throws a `RangeError`, so only the extra-matrices direction is silent.) + +**Do instead.** Keep the two lists parallel: one inverse bind matrix per joint (`Matrix4.identity()` +if the joint's rest pose is the mesh's model space). Imported skins are validated; hand-built ones are +not. Still silent. + +--- + +## 30. Cloning a mesh node whose skeleton is a sibling + +**Mistake.** `meshNode.clone()` when the skeleton lives outside the cloned subtree. + +**Symptom.** The clone renders collapsed or in bind-pose garbage. There is a `debugPrint`, but it says +only "Index path formation failed" and names neither the skin nor the consequence. + +**Do instead.** Clone the common ancestor of the mesh node and its skeleton, not the mesh node alone. +Still effectively silent. + +--- + +## 31. `updateInstanceTransforms(recomputeWinding: false)` with a mirroring edit + +**Mistake.** Editing an instance transform to a negative determinant while asking the engine to skip +the parity refresh. + +**Symptom.** Those instances render inside out (front faces culled, back faces lit). + +**Do instead.** Drop `recomputeWinding: false`, or keep every instance edit orientation-preserving +(no negative/mirrored scale). Still silent. + +--- + +## 32. Flipbook frame count vs atlas grid mismatch + +**Mistake.** A `FlipbookModule(frameCount: 16)` without `emitter.flipbookColumns = 4; +emitter.flipbookRows = 4`. + +**Symptom.** Particles sample the wrong atlas cells, or only the first cell; the effect animates but +shows the wrong art. + +**Do instead.** Set `flipbookColumns * flipbookRows` equal to the module's `frameCount`. Still silent. + +--- + +## 33. `LodComponent` blend bands overlapping + +**Mistake.** A `blendRange` larger than the gap between adjacent LOD thresholds. + +**Symptom.** An object sits permanently in the wrong cross-fade pair, dither-blending two levels that +should not blend, or skipping a level. + +**Do instead.** Keep `blendRange` smaller than the smallest gap between adjacent `screenSize` +thresholds. Still silent. + +--- + +## 34. `TextureAtlas` grid not matching its texture + +**Mistake.** `TextureAtlas(columns: 16, rows: 16, tileSize: 32, padding: 2, baseColor: eightBySix)` +where the grid does not match the image, or an out-of-range tile `index`. + +**Symptom.** Every UV points at the wrong tile. With the default `repeat` addressing, an out-of-range +index in release wraps to a different valid-looking tile rather than failing. + +**Do instead.** Make the grid parameters produce exactly the texture's dimensions +(`columns * (tileSize + 2*padding)` etc), keep tile indices in range, and set +`TextureSampling.maxMipmapLevels` so tiles do not bleed across the padding gutter at high mips. Still +silent. + +--- + +## 35. `useEnvironment` sky with no cube-radiance variant + +**Mistake.** `ShaderSkySource(fragmentShader: myShader, useEnvironment: true)` with +`radianceCubeFragmentShader` left null. + +**Symptom.** The sky contributes no image-based specular on any backend that builds the cube layout +(the default nearly everywhere), so the scene loses its reflections. + +**Do instead.** Supply `radianceCubeFragmentShader`, the entry built with +`FLUTTER_SCENE_RADIANCE_CUBE`. Debug builds warn about this at bind; release builds are silent, so do +not rely on the warning. + +--- + +## 36. `radianceCubeFragmentShader` that is not the cube build + +**Mistake.** `ShaderMaterial(fragmentShader: f, radianceCubeFragmentShader: f)` (the same shader +twice), or naming the non-cube entry as the cube twin. + +**Symptom.** The engine binds a cubemap into a shader whose sampler is a `sampler2D`: nothing on some +backends, garbage specular on others. + +**Do instead.** The cube variant must be the entry compiled with `FLUTTER_SCENE_RADIANCE_CUBE`, whose +`prefiltered_radiance` sampler is a `samplerCube`. Pass the distinct `...Cube` entry from your bundle. +(The engine can only catch the identical-shader case, and only in debug.) Still effectively silent. + +--- + +## 37. Reading `int`/`bool`/`uint` shader members through `setUniformBlockFromFloats` + +**Mistake.** `setUniformBlockFromFloats('FragInfo', [1.0, 0.5])` where the shader declares `int mode; +float amount;`. + +**Symptom.** The shader reads `mode` as the float bit pattern of `1.0` (a huge integer), so every +`if (mode == 1)` branch misses and the material takes its fallback path. The block size is correct, so +nothing complains. + +**Do instead.** Pack integer members with `ByteData.setInt32` at the member's offset, or use a `.fmat` +material whose `MaterialParameters` type-checks every assignment. Unenforceable at runtime. + +--- + +## 38. A custom fragment shader that tone-maps or writes straight alpha + +**Mistake.** Ending a `ShaderMaterial`/`ShaderSkySource`/`beforeTonemap` `PostEffect` fragment +shader with `frag_color = vec4(color, alpha)` (straight alpha) or `pow(color, vec3(1.0/2.2))` +(gamma-encoded). + +**Symptom.** Straight alpha gives edge halos and over-bright overlaps. sRGB output is tone-mapped and +EOTF-encoded a second time by the resolve pass, giving washed-out low-contrast color that looks like a +bad exposure. + +**Do instead.** Output linear HDR premultiplied by alpha. Exposure, tone mapping, and the display +encode are applied later by the full-screen resolve pass. When sampling an sRGB texture, linearize +first. `.fmat` materials get the premultiply for free. Unenforceable at runtime. + +--- + +## 39. `MaterialParameters.copyStateFrom` across a changed layout + +**Mistake.** Applying a re-realized material onto a live instance whose shader layout changed (an +editor hot reload where the `.fmat` gained or lost a parameter). + +**Symptom.** Every parameter reverts to its sidecar default while `assignedValues` still reports your +overrides, so the inspector shows the right numbers and the render shows the wrong ones. + +**Do instead.** `copyStateFrom` needs both sides to come from the same compiled shader entry; remap by +name through `updateFromMetadata` across a layout change instead. Still silent. + +--- + +## 40. Environment or widget textures with sub-255 alpha + +**Mistake.** Passing an equirect image carrying alpha below 255 (an unfilled sky dome, a masked +panorama) to `fromUIImages`/`fromEquirectImageAsset`. Or, for `WidgetTexture`/`WidgetComponent`, +simply using a widget with anti-aliased or translucent edges. + +**Symptom.** For environments, diffuse ambient comes out darker than the specular reflections of the +same environment, so objects look lit by two environments. For widget textures, dark halos around +anti-aliased text and rounded corners on the zero-copy path (correct on the web readback path, so it +reads as a platform quirk). + +**Do instead.** Use opaque (alpha 255) environment sources. The widget-alpha double-multiply is a +backend difference you cannot fully control from the API; keep widget content opaque where you can. +Still silent. + +--- + +## 41. Computing a camera or node right vector with `forward.cross(up)` or `+Z` orbit tangents + +**Mistake.** Writing `final right = forward.cross(up).normalized()`, or placing an orbit camera at `target + Vector3(d * sin(azimuth), y, d * cos(azimuth))` and using `Vector3(cos(azimuth), 0, -sin(azimuth))` as its horizontal right vector. + +**Symptom.** Horizontal camera strafing (`A`/`D`) and click-drag panning move in the exact opposite horizontal direction from expected, while vertical movement (`W`/`S` and vertical drag) works normally. + +**Do instead.** `flutter_scene`'s view space is left-handed (`+Z` forward into the screen, `+Y` up, `+X` right), while `package:vector_math`'s `cross()` is right-handed, so `forward.cross(up)` yields `-right` (screen-left). Likewise, when a camera sits on the `+Z` side of the origin looking along `-Z`, world `+X` is on the left of the screen (`right = Vector3(-cos(azimuth), 0, sin(azimuth))`). Read `camera.right`, `camera.horizontalRight`, `camera.horizontalForward`, or `node.right` directly, or compute `up.cross(forward).normalized()`. Prefer `OrbitCameraController`, `FollowCameraController`, or `FlyCameraController` over hand-rolled spherical camera math. + +--- + +## Now caught by the engine, in one place + +For quick reference, these traps became loud in 0.22.0. If you hit one you get an error, not silent +wrong pixels: + +- In-place edit of `localTransform`/`position`/`rotation`/`scale` -> throws in debug (#1). +- Degenerate camera and a frame that draws nothing -> asserts/prints once in debug (#24). +- `uploadVertexData` and `setCustomAttribute` length mismatches -> throw always (#18, #19). +- A `Mesh` whose primitive geometry is swapped -> recomputes bounds itself (#25). +- Faces of different surfaces overlapping in one plane -> printed once in debug with the fix (#2). +- An `AnimationClip` binding zero of its channels -> asserts in debug naming the wanted nodes. +- A web-backend bind to a shader uniform/texture name the shader does not declare -> throws (matches + native), instead of silently sampling whatever was bound last. +- Also fixed outright: `Node.clone()` sharing the original's matrix, the skinning joints texture being + too narrow for small joint counts, and `ParticleSystem.reset()` not restarting its random stream. diff --git a/.claude/skills/flutter_scene-idioms/references/what-exists.md b/.claude/skills/flutter_scene-idioms/references/what-exists.md new file mode 100644 index 00000000..520428f2 --- /dev/null +++ b/.claude/skills/flutter_scene-idioms/references/what-exists.md @@ -0,0 +1,550 @@ +# What exists in flutter_scene + +Complete public API inventory (package version 0.22.0). flutter_scene has lights, shadows, PBR +materials, instancing, LOD, skeletal animation, and a full post-processing stack. If you think a +feature is missing, it is almost certainly here under the name below. Look before you hand-roll. + +The public surface is the explicit `show` lists in `lib/scene.dart` (plus the separate barrels +`gpu.dart`, `fscene.dart`, `build_hooks.dart`, `physics.dart`, `audio.dart`). Nothing under +`lib/src` is public unless a barrel shows it. + +Import: +```dart +import 'package:flutter_scene/scene.dart'; +import 'package:vector_math/vector_math.dart' as vm; // NOT vector_math_64 +``` + +--- + +## Node and scene graph + +`Node` (`base class Node implements SceneGraph`). Construct `Node({String name = '', Matrix4? +localTransform, Mesh? mesh})`. A non-null `mesh` is wrapped in a `MeshComponent`. + +Transform API (0.22.0 added the component properties; older docs say only `localTransform` exists): + +| Member | Type | Notes | +| --- | --- | --- | +| `position` | `Vector3` get/set | Getter returns a copy; editing the copy in place throws in debug. Assign to move. | +| `rotation` | `Quaternion` get/set | Same copy rule. | +| `scale` | `Vector3` get/set | Same copy rule. | +| `localTransform` | `Matrix4` get/set | Getter returns the LIVE matrix; in-place edit throws in debug on next read. Assign a fresh matrix. | +| `mutateLocalTransform(void Function(Matrix4) edit)` | method | Edits in place AND dirties the cache. Correct raw-matrix path. | +| `globalTransform` | `Matrix4` get/set | Cached world transform; setter solves for the needed local. | +| `forward` | `Vector3` get | Unit world-space direction of the node's local `+Z` axis (the axis `lookAt` aims). | +| `right` | `Vector3` get | Unit world-space direction of the node's local `+X` axis (screen-right when viewed along `forward`). | +| `up` | `Vector3` get | Unit world-space direction of the node's local `+Y` axis. | +| `lookAt(target, {up})` | method | Orients the node's forward axis (local +Z) at a world-space target; preserves world position and scale. | +| `lookAtFrom(eye, target, {up})` | method | Positions at `eye` and aims +Z at `target` in one call (the imperative camera one-liner). | +| `Node.lookAtTransform(eye, target, {up})` | static -> `Matrix4` | The `lookAt` basis as a local transform, for `Node(localTransform:)` and declarative `transform:`. | + ++Z is the forward axis engine-wide (cameras, directional/spot lights, imported models), so the +lookAt helpers aim any of them. Compose a plain matrix with `vm.Matrix4.translation(v)`, +`vm.Matrix4.rotationY(a)`, `vm.Matrix4.compose(t, q, s)`. There is no `translate`/`rotateX` on Node. + +Hierarchy (`SceneGraph` is a mixin): `add`, `addAll`, `addMesh`, `remove`, `removeAll`. `add` throws +if the child already has a parent. `parent`, `children`, `detach()`, `getRoot()`, `getDepth()`. + +Lookup: `getChildByName(name, {excludeAnimationPlayers})`, `getChildByNamePath`, +`getChildByIndexPath`, static `getNamePath`/`getIndexPath`, `meshNodes`, `clone({recursive = true})`. + +Per-node flags: `visible` (true), `frustumCulled` (true), `layers` (`kRenderLayerDefault`, a 32-bit +mask, NOT inherited), `castsShadows` (true, not inherited), `shadowStatic` (false), `raycastable` +(true), `highlightColor` (`Vector4?`), `skin` (`Skin?`, set by importers). + +Bounds: `combinedLocalBounds`, `combinedWorldBounds`, `markBoundsDirty()`, `isVisibleTo(camera, +size)`. A `null` bounds means always-visible. + +Loading models (see Assets): `Node.fromGlbAsset`, `Node.fromGlbBytes`, `Node.fromGltfBytes`. + +Geometry readback: `extractMeshData({Matrix4? transform})` flattens the subtree to one `MeshData`. +Throws on instanced meshes, non-triangle primitives, caller-managed geometry, or an empty subtree. + +`Scene` (`base class Scene implements SceneGraph`, cannot be subclassed). See the render/lighting/ +post sections. `Mesh(geometry, material)`/`Mesh.primitives({primitives})`; `MeshPrimitive(geometry, +material)`; `Mesh.clone()` (shallow, shares geometry+material); `Mesh.localBounds`, +`Mesh.markLocalBoundsDirty()`. + +### Camera + +- `PerspectiveCamera({double fovRadiansY = 45 * degrees2Radians, Vector3? position /*(0,0,-5)*/, + Vector3? target /*(0,0,0)*/, Vector3? up /*(0,1,0)*/, double fovNear = 0.1, double fovFar = + 1000.0})`. Field names are `fovNear`/`fovFar`, NOT `near`/`far`. +- `PerspectiveCamera.framing(Aabb3 bounds, {direction, fovRadiansY, up, margin = 1.1})`. +- `PerspectiveProjection({fovRadiansY, near = 0.1, far = 1000.0})` and abstract `CameraProjection`, + `Camera`. Camera basis getters (`position`, `forward`, `up`, `right`, `horizontalForward`, `horizontalRight`) and helpers (`screenPointToRay`, `worldToScreen`, `getViewMatrix`, `getFrustum`). `right` and `horizontalRight` use `up.cross(forward)` so they always point toward the right edge of the view in the engine's left-handed view space. +- `OrthographicCamera({OrthographicProjection? projection, Vector3? position, target, up})` and + `OrthographicCamera.framing(bounds, {direction, up, margin})`. +- `OrthographicProjection({OrthographicSize size = OrthographicSize.height(10), zoom = 1.0, + Vector2? offset, near = 0.0, far = 1000.0})`, plus `.bounds(left:, right:, bottom:, top:)` and + `.matchingPerspective(fovRadiansY:, distance:)`. Sizes are FULL world extents, never half: + `OrthographicSize.height(h)`, `.width(w)`, `.contain(w, h)`, `.cover(w, h)`, `.stretch(w, h)`, + or `.pixelsPerUnit(ppu)` (logical pixels per world unit, so the scale holds as the view resizes). + Frame with `zoom`/`size`, not by moving the eye. `near` may be negative (isometric scenes that + extend behind the eye). +- Custom projections: implement `CameraProjection`. The renderer reads perspective versus + orthographic from the matrix, so effects follow. +- Node-driven: `CameraComponent({CameraProjection? projection, activateOnMount = false})` -> + `toCamera()` gives a `NodeCamera`. Camera node must not be scaled. +- Interactive cameras: `CameraController` components attached to the camera node. `OrbitCameraController` + (turntable around `target`; `orbitBy`/`dollyBy`/`panBy`/`frame`), `FlyCameraController` (WASD + drag + free flight; `moveVertical: false` = grounded first-person; `look`, `setMoveInput`, `forward`, `right`), `FollowCameraController` + (third-person easing behind `followTarget` node; `orbitBy`/`dollyBy`, `forward`, `right`). All ease with frame-rate + independent `smoothing` (settle seconds), clamp pitch short of vertical, and write the node via + `lookAtFrom`. Wire input with the `CameraControls({required controller, enabled, autofocus, child})` + widget (Focus + gestures + wheel); `SceneView` has no camera-input params by design. + +--- + +## Components + +`abstract class Component`. Lifecycle hooks (exact names): `onAttach`, `onLoad` (async), `onMount`, +`update(double deltaSeconds)` (NOT `onUpdate`), `fixedUpdate(double)`, `onUnmount`, `onDetach`, +`cloneFor(Node)`. Node side: `addComponent`, `removeComponent`, `getComponent()`, +`getComponents()`. +Game input lives in the separate `flutter_scene_input` package: `InputSystem`, `PlayerInput` +(`button`, `axis`, `vector`, `delta`, `fixed`, `contexts`, `overrides`), `ActionSet`, typed actions, +binding nodes (`ButtonBinding`, `DpadBinding`, `StickBinding`, `DeltaBinding`, `ChordBinding`, +`GatedBinding`), processors (`Deadzone`, `Scale`, `Invert`, `PerSecond`), `scene.attachInput`, +`InputListener`, `PointerLock`, `DefaultActions`, drivers, `listenForBinding`, `bindingDisplay`. + +Components tick root-first in tree order. For code that must run before every component each frame +(sampling input, applying network state), subclass `SceneTickListener` (`beforeTick(dt)`, +`beforeFixedStep(fixedDt)`, both optional) and register it with `scene.addTickListener`. + +| Component | Constructor/notes | +| --- | --- | +| `MeshComponent` | `MeshComponent(mesh)`; `mesh` get/set, `refreshMaterials()` | +| `InstancedMeshComponent` | `InstancedMeshComponent(instancedMesh)` | +| `LodComponent` | `LodComponent(List, {lodBias = 1.0, hysteresis = 0.1, blendRange = 0.0})`; extends MeshComponent | +| `CameraComponent`, `NodeCamera` | see Camera | +| `DirectionalLightComponent` | `(light)` aims down node local +Z; `.aimed(light, localDir)`; `.fromLightDirection(light)` | +| `PointLightComponent` | `(light)`; `worldPosition` | +| `SpotLightComponent` | `(light)`; `worldPosition`, `worldDirection` | +| `RectAreaLightComponent` | `(light)`; `worldPosition`, `worldRight`, `worldUp` | +| `EnvironmentVolumeComponent` | `({required settings, shape = box, extents, radius = 5.0, blendDistance = 1.0, priority = 0.0, weight = 1.0})` | +| `ReflectionProbeComponent` | `({extents = Vector3.all(5), blendDistance = 1.0, priority = 10.0, weight = 1.0, faceResolution = 128, captureOnActivate = true})`; parallax-corrected local reflections in the box; `requestCapture()` re-captures | +| `MaterialsVariantsComponent` | No public ctor. `MaterialsVariantsComponent.of(root)`/`.allOf(root)`, then `select(name)`, `variants`, `selected` | +| `SemanticsComponent` | `({label, value, hint, button, onTap, ... boundsOverride, properties})` | +| `WidgetComponent` | `({required Widget child, required Size size, pixelRatio = 1.0, worldHeight = 1.0, update = everyFrame, input = automatic, ...})`; `.bindOnly(...)` | +| `SplatComponent` | `SplatComponent(GaussianSplats)`; `opacity`, `splatScale`, `tint`, `shDegree`, `cropBox`, `cropMode` | +| `ParticleEmitterComponent` | `({required system, SpriteMaterial? material})`; `facing`, `flipbookColumns/Rows/Blend`, `paused` | +| `MeshParticleEmitterComponent` | `({required system, required List geometries, required material, facing = tumble})` | +| `TrailComponent` | `({width = 0.25, lifetime = 0.6, minVertexDistance = 0.05, maxPoints = 48, ...})`; `emitting`, `clear()` | + +`SemanticsComponent`, `SplatComponent`, particle emitters, `TrailComponent`, `WidgetInput`, +`MeshParticleFacing`, `LodLevel`, `EnvironmentVolumeShape` are all exported. + +--- + +## Geometry + +### Primitives (`primitives.dart`, all factory constructors, all `extends MeshGeometry`) + +| Class | Constructor | Facing/notes | +| --- | --- | --- | +| `CuboidGeometry` | `CuboidGeometry(Vector3 extents, {debugColors = false})` | positional extents; box `-extents/2..+extents/2`; debugColors off | +| `WedgeGeometry` | `WedgeGeometry(Vector3 size)` | triangular prism; base on `y=0`, not Y-centered | +| `PlaneGeometry` | `({width = 1.0, depth = 1.0, segmentsX = 1, segmentsZ = 1})` | XZ plane, faces +Y; no collisionShape | +| `SphereGeometry` | `({radius = 0.5, segments = 32, rings = 16})` | UV sphere | +| `CylinderGeometry` | `({bottomRadius = 0.5, topRadius = 0.5, height = 1.0, radialSegments = 32, heightSegments = 1, bottomCap = true, topCap = true})` | topRadius 0 = cone | +| `CapsuleGeometry` | `({radius = 0.5, height = 1.0, radialSegments = 32, capRings = 8})` | `height` is the mid-section; total Y = height + 2*radius | +| `TorusGeometry` | `({radius = 0.5, tubeRadius = 0.2, radialSegments = 32, tubularSegments = 16})` | XZ plane | +| `DiscGeometry` | `({radius = 0.5, segments = 32})` | XZ, faces +Y | +| `RingGeometry` | `({innerRadius = 0.25, outerRadius = 0.5, segments = 32})` | annulus, XZ, +Y | +| `IcosphereGeometry` | `({radius = 0.5, subdivisions = 2})` | subdivided icosahedron | + +Every primitive except `PlaneGeometry` has a `Shape get collisionShape`. + +### Swept/procedural (sweep a `ScenePath`; also `BezierPath`, `CatmullRomPath`, `PolylinePath`) + +- `RibbonGeometry(path, {width = 1.0, stations = 64, alignment = RibbonAlignment.ground, up, storage = fixed})`; `updatePath(path)`. `RibbonAlignment` = `ground` | `path`. +- `TubeGeometry(path, {radius = 0.5, radialSegments = 12, stations = 64, caps = true, storage})`. +- `ExtrudeGeometry(path, {required List profile, stations = 64, caps = true, storage})`. +- `PolylineGeometry(List points, {width = 8.0, widthMode = screenPixels, cap = butt, dash, perVertexWidth, perVertexColor})`. INERT until `updateForCamera(camera, viewportSize)` is called every frame. `PolylineWidthMode` = `screenPixels` | `worldUnits`; `PolylineCap` = `butt` | `round`; `DashPattern({dashLength, gapLength, cap})`. +- `LineSegmentsGeometry(LineSegmentData segments, {width = 0.01, normalOffset = 0.0})`. `extends Geometry`, GPU-expanded, no per-frame CPU work. For large independent-segment sets. +- `BillboardGeometry({capacity = 256})`. `floatsPerInstance = 14`; `BillboardFacing` = `spherical` | `axisLocked` | `velocityStretched`. + +### MeshGeometry, GeometryBuilder, MeshData + +`MeshGeometry.fromArrays({required Float32List positions, Float32List? normals, texCoords, +texCoords1, colors, tangents, List? indices, primitiveType = triangle, Aabb3? bounds, storage = +fixed, GeometryBufferArena? bufferArena, retainCpuData = true})`. Components per vertex: positions 3, +normals 3, texCoords/texCoords1 2, colors/tangents 4. Omitted normals on a triangle list are +generated. Omitted indices need a vertex count divisible by 3. `bounds` skips the position scan (it +must actually cover every vertex). + +`MeshGeometry.fromMeshData(MeshData data, {storage, bufferArena, retainCpuData})`. + +In-place update (require `GeometryStorage.updatable`, all take `{dirtyStart, dirtyCount}`): +`updatePositions`, `updateNormals`, `updateTexCoords`, `updateTexCoords1`, `updateColors`, +`updateTangents`. `rebuild({positions, normals, ...})` may change the vertex/index count. +`applyMeshData(data)`. `GeometryStorage` = `fixed` | `updatable`. + +`GeometryBuilder({deduplicate = true})`: `normal(v)`, `texCoord(v)`, `texCoord1(v)`, `color(v)`, +`tangent(v)`, `addVertex(Vector3) -> int`, `addTriangle(a, b, c)` (throws RangeError on bad index), +`packVertices()`, `build({storage, bufferArena, retainCpuData})`. Attribute setters are STICKY. +Calling `normal()` once disables generated normals for the whole mesh. + +`MeshData` (isolate-transferable, pure): `MeshData({required positions, required vertexCount, +normals, ..., customAttributes})`, `MeshData.build({required positions, ...})` (derives vertexCount, +generates normals). Derivations: `triangleCount`, `triangles`, `transformed(Matrix4)` (inverse +transpose for normals; a mirror flips winding), `toTriMeshShape()` (hollow static collider), +`toConvexHullShape()` (dynamic body), `unweld({attributes})`, `extractEdges({creaseAngleDegrees})`, +static `merge(parts)`. `MeshAttributeData(data, {components})`; `UnweldAttribute` = `centroid` | +`seed` | `triangleIndex` | `barycentric`; `LineSegmentData({positions, normals})`. + +`Geometry` base: `primitiveType`, `localBounds`, `localBoundingSphere`, `setLocalBounds(aabb, +sphere)`, `setVertices(BufferView, vertexCount)`, `setIndices(BufferView, indexType)`, +`setCustomAttribute(name, Float32List, {required components})` (1..4; depth and shadow passes read +it too, so attribute-driven displacement casts its shadow; materials that do not declare it ignore +it, and one that declares an attribute the mesh lacks reads zero), `uploadVertexData(ByteData, vertexCount, ByteData? indices, {indexType = +int16})`, `isReadable`, `extractMeshData()`, `setVertexShader`/`setVertexShaderName`, +`setVertexLayout(descriptor, {bindsModelTransform = true})`, `draw(pass, {instanceCount = 1})`. +`SkinnedGeometry`/`UnskinnedGeometry` subclasses. `GeometryBufferArena({blockSizeInBytes = 16MB})`. + +Vertex layout: unskinned 72 bytes/18 floats = position(3) normal(3) texture_coords(2) +texture_coords_1(2) color(4) tangent(4). Skinned 104 bytes/26 floats = + joints(4) weights(4). Do +not hand-pack; use `fromArrays`/`fromMeshData`/`GeometryBuilder`. + +### Instancing and LOD + +`InstancedMesh({required geometry, required material, cullInstances = false, +sortTransparentInstances = true})`: `instanceCount`, `addInstance(Matrix4, {Vector4? color}) -> int` +(clones the matrix), `setInstanceTransform(i, m)`, `updateInstanceTransforms(update, +{recomputeWinding = true})`, `setInstanceColor(i, color)`, `removeInstanceAt(i)`, `clearInstances()`. +Attach via `InstancedMeshComponent`. + +`LodLevel({required geometry, required material, required double screenSize})` (screenSize = projected +bounding-sphere diameter as a fraction of viewport height, descending, last is the cull floor). +Attach via `LodComponent`. Shadow/depth passes always draw level 0. + +--- + +## Materials and textures + +`Material` (abstract): `name`, `doubleSided` (false), `depthLayer` (0), `depthBias` (0.0), +`setFragmentShader`, `setFragmentShaderName(name, {cubeName})`, `setRadianceCubeFragmentShader`, +`isOpaque()`. `depthLayer` (-8..8) orders coplanar surfaces: a higher layer draws over a lower one +in the same plane at any distance (an overlay on a wall or road gets 1). `depthBias` is a world-unit +nudge toward the camera that buys fewer depth steps with distance; prefer `depthLayer`. `.fmat` +spells the layer `depth_layer:`. + +### UnlitMaterial + +`UnlitMaterial({TextureSource? colorTexture})`. `baseColorTexture` (field name differs from the ctor +arg), `baseColorTextureTransform`, `baseColorTextureTexCoord` (0), `alphaMode` (`opaque`; `mask` not +implemented, behaves as blend), `baseColorFactor` (white), `vertexColorWeight` (1.0). Fog applies. + +### PhysicallyBasedMaterial + +`PhysicallyBasedMaterial({baseColorTexture, metallicRoughnessTexture, normalTexture, emissiveTexture, +occlusionTexture, EnvironmentMap? environment})`. Every texture slot is a `TextureSource?` with a +`TextureTransform` and `TextureTexCoord`. + +Core: `baseColorFactor` (white), `vertexColorWeight` (1.0), `metallicFactor` (1.0), `roughnessFactor` +(1.0), `normalScale` (1.0), `emissiveFactor` (`Vector4.zero()`), `emissiveStrength` (1.0), +`occlusionStrength` (1.0), `environment` (null, falls back to `Scene.environment`), `alphaMode` +(opaque), `alphaCutoff` (0.5), `specularAntiAliasingVariance` (0.15), `specularAntiAliasingThreshold` +(0.2). + +Advanced KHR_materials_* (setting any flips onto an internal physical-variant shader; each has a +`Texture`): `specular` (1.0), `specularColor`, `ior` (1.5), `clearcoat` (0.0), +`clearcoatRoughness` (0.0), `clearcoatNormalScale`, `sheenColor` (zero), `sheenRoughness` (0.0), +`transmission` (0.0), `diffuseTransmission`, `diffuseTransmissionColor`, `thickness` (0.0), +`attenuationDistance` (inf), `attenuationColor`, `dispersion` (0.0), `iridescence` (0.0), +`iridescenceIor` (1.3), `iridescenceThicknessMinimum` (100.0), `iridescenceThicknessMaximum` (400.0), +`anisotropy` (0.0), `anisotropyRotation` (0.0). + +`isOpaque()` is false when `transmission > 0`, `alphaMode == blend`, or `baseColorFactor.a < 1.0`. + +`AlphaMode` = `opaque` | `mask` | `blend`. `TextureTransform({offset, scale, rotation})` (glTF +KHR_texture_transform order). + +### SpriteMaterial + +`SpriteMaterial({TextureSource? colorTexture})`: `colorTexture`, `tint` (white), `blendMode` +(`SpriteBlendMode.alpha` | `additive`), `softDepthFade` (0.0), `cameraNearFade` (0.0), `sampler`. +Always non-opaque, always cull none. + +### ShaderMaterial (raw GLSL escape hatch) + +`ShaderMaterial({gpu.Shader? fragmentShader, radianceCubeFragmentShader, vertexShader, +skinnedVertexShader, depthVertexShader, useEnvironment = false, cullingMode = backFace, windingOrder = +counterClockwise, isOpaqueOverride = true})`. `setVertexShader(shader, {variant = unskinned})`, +`vertexShaderFor(variant)`, `setUniformBlock(name, ByteData?, {stage = fragment})`, +`setUniformBlockFromFloats(name, List, {stage})`, `getUniformBlock`, `uniformBlockNames`, +`setTexture(name, texture, {sampler, stage})` (accepts `gpu.Texture`/`Texture2D`/`RenderTexture`), +`getTexture`, `textureNames`. `ShaderStage` = `vertex` | `fragment`; `MeshVariant` = `unskinned` | +`skinned` | `depth`. + +Fragment shaders MUST output linear HDR premultiplied by alpha (exposure, tone mapping, and the +display encode are applied later by the resolve pass). Same contract for `ShaderSkySource` and +`PostInsertion.beforeTonemap` effects. std140 packing is by hand. + +### .fmat (declarative, recommended over ShaderMaterial) + +`loadFmatMaterial(sourcePath) -> PreprocessedMaterial`, `loadFmatSky(...) -> PreprocessedSky`. +`PreprocessedMaterial`: `parameters` (`MaterialParameters`), `shadingModel`, `environment`. +`MaterialParameters` (typed, reflection-backed, throws on wrong type/name): `setFloat`, `setInt`, +`setVec2/3/4`, `setMat4`, `setColor(name, Color)`, `setTexture(name, gpu.Texture, {sampler})`, +`operator []=`, `parameterNames`, `samplerNames`, `hasUniformBlock`. + +### Textures + +`TextureSource` (interface): implementers are `Texture2D`, `RenderTexture`, `GpuTextureSource`. Every +built-in material slot takes a `TextureSource`, not a raw `gpu.Texture`. + +`Texture2D` (factories, generates a mip chain): `Texture2D.fromPixels(Uint8List, w, h, {content = +color, sampling})`, `fromImage(ui.Image, {...})`, `fromAsset(String, {content = color, sampling, +bundle})`. `TextureContent` = `color` (sRGB) | `data` (linear, e.g. metallic-roughness/AO) | `normal` +(vector-averaged). `TextureSampling({mipmaps = true, maxMipmapLevels, minFilter = linear, magFilter = +linear, mipFilter = linear, maxAnisotropy = 8, addressMode = repeat})`. + +`GpuTextureSource(gpu.Texture, {sampler})` adapts a raw texture. Barrel helpers: +`gpuTextureFromImage`, `gpuTextureFromAsset` (mipless, aliases on materials), `imageFromAsset`, +`imageFromBytes`. Cooked `.fstex`: `loadTexture(sourcePath, {package, bundle, sampling}) -> +TextureSource`, `releaseTexture`, `clearTextureCache`. + +Custom-shader GPU barrel (`package:flutter_scene/gpu.dart`): `Shader`, `ShaderLibrary`, +`loadShaderLibraryAsync` (use this, not `ShaderLibrary.fromAsset` which throws on web), +`resolveShaderBundleKey`, `Texture`, `SamplerOptions`, `MinMagFilter`, `MipFilter`, +`SamplerAddressMode`, `IndexType`, `VertexFormat`, `VertexStepMode`. + +--- + +## Lighting and environment + +Lights (all in `light.dart`, all fields mutable): + +- `DirectionalLight({direction /*(-0.3,-1,-0.2)*/, color, intensity = 3.0, priority = 0, castsShadow + = false, cacheStaticShadows = true, shadowFadeRange = 2.0, shadowSoftness = 0.08, shadowCascadeCount + = 4, shadowMaxDistance = 150.0, shadowCascadeSplitLambda = 0.6, shadowMapResolution = 1024, + shadowDepthBias = 0.02, shadowNormalBias = 0.02, shadowAmbientStrength = 0.0, shadowFilter = + rotatedPoisson, shadowCasterFaces = front, contactShadows = false, contactShadowDistance = 0.3, + angularRadius = 0.005})`. +- `PointLight({color, intensity = 1.0, range = 0.0, falloffExponent = 2.0, castsShadow = false, + shadowMapResolution = 512, shadowNear = 0.1, shadowDepthBias = 0.0, shadowNormalBias = 0.1, + shadowSoftness = 1.0, shadowCasterFaces = front})`. Shadows render six cube faces into the shared + atlas (a limited number of point casters per frame; the rest shade unshadowed). +- `SpotLight({color, intensity = 1.0, range = 0.0, falloffExponent = 2.0, direction /*(0,-1,0)*/, + innerConeAngle = 0.0, outerConeAngle = pi/4, castsShadow = false, ...})`. +- `RectAreaLight({color, intensity = 1.0, width = 1.0, height = 1.0, range = 0.0})`. Local XY plane, + emits along +Z, no shadows. +- `SunLight(SunSky source, {castsShadow = true, ...})` drives `Scene.directionalLight` from a sky. + +`Node.shadowCastingMode` (`ShadowCastingMode` = `on` (default) | `off` | `doubleSided` | +`shadowsOnly`) selects how a node's meshes cast; `Node.castsShadows` is a deprecated two-state +view of it. Every light also has `shadowCasterChannelMask` (8-bit), tested against +`Node.lightChannelMask`, to drop casters from one light's map. + +`ShadowCasterFaces` = `front` | `back` | `both`. `DirectionalShadowFilter` = `rotatedPoisson` | +`fixedPcf` | `pcss`. `ShadowCascade`, `Lighting` (per-draw state) are exported. + +Scene lighting: `Scene.directionalLight` (`DirectionalLight?`, null = IBL only; honors `direction`; +highest-priority one gets cascaded shadows), `Scene.sunLight`, `Scene.environment` (`EnvironmentMap?`, +null falls back to `EnvironmentMap.studio()`; for genuinely no IBL use `EnvironmentMap.empty()`), +`Scene.environmentIntensity` (1.0), `Scene.environmentTransform` (`Matrix3.identity()`), +`Scene.skybox` (`Skybox?`, null = transparent), `Scene.skyEnvironment`. + +### EnvironmentMap + +Carries a prefiltered specular radiance atlas AND SH-9 diffuse coefficients (one texture path, no +separate radiance/irradiance). Factories: `.empty()`, `.constantDiffuse(ambientRadiance)`, +`.fromGpuTextures({required prefilteredRadiance, diffuseSphericalHarmonics, diffuseShTexture})` (the +texture must already be prefiltered), `.fromUIImages({required radianceImage, ...})`, +`.fromEquirectHdr({required Float32List linearPixels, w, h, ...})`, +`.fromEquirectImageAsset({required assetPath, maxWidth = 4096, ...})` (auto-detects .hdr/.exr/LDR), +`.fromEquirectImageBytes(...)`, `.fromSky(SkySource, {...})`, `.studio()` (zero-config default). +Deprecated: `.fromAssets` (use `.fromEquirectImageAsset`). Env images must be equirect 2:1. +`prefilterEquirectRadiance` is exported. `Scene.loadEnvironment(assetPath, {showSkybox = true, +skyBlur = 0.0, intensity, exposure, rotationY, maxWidth = 4096, bundle})` is one-call setup. + +### Skybox/sky sources + +`Skybox(SkySource source, {intensity = 1.0})`. `SkySource` implementers: `EnvironmentSkySource({blurriness += 0.0})`, `ShaderSkySource({fragmentShader, fragmentShaderName, radianceCubeFragmentShader, +useEnvironment = false})`, `GradientSkySource({zenithColor, horizonColor, groundColor, sunDirection, +sunColor, sunSharpness = 400.0})`, `PhysicalSkySource({sunDirection, sunAngularRadius = 0.0175, +rayleighCoefficient = 2.0, mieCoefficient = 0.005, turbidity = 10.0, energy = 1.0, ...})`. +`SkyEnvironment(ShaderSkySource, {refresh = manual, interval, faceResolution = 128, equirectWidth = +512})`; `SkyEnvironmentRefresh` = `manual` | `interval` | `everyFrame`. + +### Exposure and tone mapping + +`Scene.exposure` (1.0; not 2.0), `Scene.toneMapping` (`ToneMappingMode.pbrNeutral`; also `aces`, +`reinhard`, `linear`, `agx`), `Scene.agxWhite` (16.29), `Scene.agxContrast` (1.25). Static +`Scene.physicalCameraExposure({required aperture, shutterSpeed, iso})` returns a multiplier to assign +to `exposure`. + +--- + +## Post-processing + +Every effect is a settings object on `Scene`, off by default, turned on with `enabled`. Environment +looks blend via `EnvironmentSettings` (snapshot/lerp of the whole look) and `EnvironmentVolume` / +`EnvironmentVolumeComponent` (spatial). + +| Scene field | Type | Key fields (default) | Requires | +| --- | --- | --- | --- | +| `ambientOcclusion` | `AmbientOcclusionSettings` | `method` (obscurance/`groundTruth`), `radius` (0.33), `intensity` (1.0), `power` (1.5), `bentNormals` (false), `halfResolution` (true), `indirectLight` (0.0 = SSGI), `specularMode` | perspective camera | +| `screenSpaceReflections` | `ScreenSpaceReflectionsSettings` | `intensity` (1.0), `maxDistance` (24.4), `thickness` (0.46), `stride` (9.0), `maxSteps` (90), `blur` (0.3), `debugView` | perspective camera | +| `fog` | `Fog` | `mode` (`FogMode.exponential`; also none/linear/exponentialSquared), `color`, `density` (0.02), `start`/`end`, needs both `enabled` AND non-none `mode` | any camera | +| `godRays` | `GodRaysSettings` | `intensity` (1.0), `density` (0.5), `anisotropy` (0.7), `stepCount` (24), `maxDistance` (200), `color` | shadow-casting DirectionalLight + perspective camera | +| `depthOfField` | `DepthOfField` | `focusDistance` (10.0), `fStop` (2.8), `focalLength`, `sensorHeight` (0.024), `bladeCount`, `quality` (low/medium/high) | perspective camera | +| `autoExposure` | `AutoExposureSettings` | `strength` (0.55), `compensation`, `minEv` (-4), `maxEv` (4), `speedUp` (3.0), `speedDown` (1.0); multiplies on top of `exposure` | none | +| `postProcess` | `PostProcessSettings` | see below | none | + +`AmbientOcclusionMethod` = `obscurance` (McGuire SAO) | `groundTruth` (GTAO). `SpecularAmbientOcclusionMode` += `none` | `simple` | `bentCone`. `SsrDebugView` = composite/reflectedUv/hitMask/normal/confidence/depth. + +`PostProcessSettings` (all sub-settings off by default; mutate the nested objects): +- `colorGrading` (`ColorGradingSettings`): `brightness` (1.0), `contrast` (1.0), `saturation` (1.0), + `temperature`, `tint`, `lift`/`gamma`/`gain`, `lut` (`ColorLut?`, applies after tone mapping, + independent of `enabled`), `lutBlend` (1.0). +- `chromaticAberration` (`intensity` 0.2), `vignette` (`intensity` 0.5, `radius` 0.75, `smoothness` + 0.5), `filmGrain` (`intensity` 0.3), `bloom` (`threshold` 1.0, `intensity` 0.15, `scatter` 0.7, + and `lensFlare`: `enabled` false, `intensity` 1.0, `ghostCount` 4, `ghostSpacing` 0.3, `haloRadius` + 0.35, `haloIntensity` 1.0, `chromaticAberration` 0.005; rides the bloom, needs bloom enabled). +- `customEffects` (`List`). + +`ColorLut.fromCubeString`/`.fromCubeAsset` (Adobe `.cube`, edge 2..64). + +Custom post: `PostEffect({gpu.Shader? fragmentShader, insertion = beforeTonemap, enabled = true, +useFrameInfo = false})`, added to `scene.postProcess.customEffects`. Engine binds `uniform sampler2D +input_color` at `in vec2 v_uv`. `PostInsertion` = `beforeTonemap` (linear HDR premultiplied) | +`afterTonemap` (display-referred). + +--- + +## Scene, render, and widgets + +`Scene()` (no args; calls `initializeStaticResources()`, needs a live Flutter GPU context). Methods: +`add`, `addAll`, `addMesh`, `remove`, `removeAll`, `update(dt)` (optional), `render(camera, canvas, +{viewport, pixelRatio})`, `renderViews(views, canvas, {region, pixelRatio})`, `warmUp(views, +{includeOffscreen})`, `raycast(ray, {maxDistance, layerMask, where, includeInvisible})`, +`raycastAll(...)`, `addRenderPass`/`removeRenderPass`, `captureRenderGraph({viewIndex, request, +timeout})`, `captureEnvironment({required position, faceResolution = 128, equirectWidth = 512, +layerMask})` -> `EnvironmentMap` (one-shot static capture; use `ReflectionProbeComponent` for a +node-anchored, parallax-corrected, auto-blended probe). Statics: `Scene.initializeStaticResources()`, +`Scene.preload({physicalMaterials = true, smaa = false})` (loads the shaders behind PBR extensions +and `ShadowCatcherMaterial`, and the SMAA tables, which otherwise load on first use), +`Scene.isReadyToRender`, `Scene.physicalCameraExposure`, `Scene.isAntiAliasingModeSupported`, +`Scene.effectiveAntiAliasingMode`. + +`Scene.antiAliasingMode` (`AntiAliasingMode.auto` -> msaa or fxaa; also `none`, `msaa`, `fxaa`, +`smaa`, `taa`), `Scene.smaa` (`SmaaSettings`: threshold, search steps, corner rounding), +`Scene.temporalAntiAliasing` (`TemporalAntiAliasingSettings`), +`Scene.renderScale` (1.0), `Scene.filterQuality` (`FilterQuality.medium`), `Scene.views` +(`List` for RenderTexture targets). + +`RenderView({required Camera camera, RenderTexture? target, Rect? viewport /*normalized 0..1, +ignored when target set*/, int layerMask = kRenderLayerAll, order = 0, AntiAliasingMode? +antiAliasingMode, double? renderScale, FilterQuality? filterQuality, List cullingPlanes})`. +`kRenderLayerDefault = 1`, `kRenderLayerAll = 0xFFFFFFFF`. + +`RenderTexture`, `RenderTextureSampling`, `RenderTextureUpdate`, `RenderTextureView(renderTexture, +{fit = contain, filterQuality = medium, followLayout = false})`. + +Widgets: +- `SceneView(Scene scene, {Camera? camera, SceneCameraBuilder? cameraBuilder, SceneViewsBuilder? + viewsBuilder, autoTick = true, pixelRatio, onTick, loading, loadingBuilder, revealMinDuration, + warmUp = false, children})`. App-owned scene; does not write scene properties. `camera`, + `cameraBuilder`, `viewsBuilder` are mutually exclusive. +- `SceneView.declarative({environment, environmentIntensity = 1.0, exposure = 1.0, toneMapping = + pbrNeutral, camera, cameraBuilder, viewsBuilder, children, ...})`. View-owned scene. +- `SceneViewsBuilder` is exported as of 0.22.0 (older docs list it as a trap; it is public now). +- Declarative widgets: `SceneNode`, `SceneMesh`, `SceneModel`, `SceneSubtree`, `SceneNodeHost`, + `SceneNodeController`, `SceneModelSource`, `AssetModelSource`, `MemoryModelSource`, + `SceneAnimationSpec`. `WidgetTexture`, `WidgetTextureController`, `WidgetUpdatePolicy`. `SceneScope`. +- Camera resolution precedence: `camera` -> `cameraBuilder(elapsed)` -> `scene.camera` (or first + mounted `CameraComponent`) -> default `PerspectiveCamera()`. + +`CustomRenderPass`, `RenderInput`, `RenderPassContext`, `RenderStage`, `TransientWriter`, +`NodeFilter`, `HighlightStyle`, render-graph capture types (`CapturedPass`, `CapturedResource`, +`CapturedDraw`, `CapturedSkip`, `RenderGraphCaptureRequest`, `RenderGraphCaptureResult`) are all +exported. + +Debugging and profiling (all exported, all on every backend): +- `scene.renderStats.latest` (`RenderFrameStats`): per-frame counters (draws, instances, vertices, + culled, batches, pipeline binds and builds) per view and per pass with CPU micros, plus + `history`. Always on; GPU times are null until the engine exposes timestamp queries. Each pass + also emits a `dart:developer` timeline event, so DevTools shows the frame. +- `Scene.debugAllowRenderGraphCapture = true` then `scene.captureRenderGraph()` captures one frame: + passes with data flow and thumbnails, and every draw (`pass.draws`) with node path, material, + shader names, pipeline id, counts, batch size, `batchBreak` reason, and the uniform blocks bound + for it (`block.decode(draw)` names the members once `ShaderReflection.loadAll()` has run). + `result.toJson()` / `RenderGraphCaptureResult.fromJson` round-trip a capture as JSON. +- `ShaderReflection.loadAll()` parses every loaded shader bundle; `ShaderReflection.infoFor(shader)` + gives inputs, uniform block layouts, texture bindings, and `sourceOf(shader)` the compiled MSL, + GLSL, or SPIR-V for any backend the bundle holds. `FmatCompileException.diagnostics` parses + compiler errors with line numbers; `shaderSourceWindow` renders the marked source around one. + +--- + +## Assets and animation + +Setup: `flutter pub add flutter_scene` then `dart run flutter_scene:init`. Enable Flutter GPU with +`flutter run --enable-flutter-gpu` (native only; nothing for web). Requires Flutter 3.47 stable+, NOT +master. Impeller is default; do not pass `--enable-impeller`. Never pass +`--enable-experiment=native-assets` (breaks the build on Dart 3.10+). + +Two model-loading paths, do not conflate: + +- Pipeline (preferred, needs the `buildScenes` hook): `loadScene(sourcePath, {package, bundle, + registry, onReload, applyStageTo}) -> Future`. `sourcePath` is the SOURCE path relative to + the package root (e.g. `'assets/level.glb'`), NOT a generated name. Companions: + `loadSceneSubtree`, `releaseScene`, `clearSceneTemplateCache`. +- Runtime glTF (no hook, parses every load): `Node.fromGlbAsset(assetPath)`, + `Node.fromGlbBytes(bytes)`, `Node.fromGltfBytes(gltfJson, {required resolveUri})`. Each synthesizes + a root node. + +Sibling loaders: `loadTexture` (`.fstex`), `loadFmatMaterial`/`loadFmatSky` (`.fmat`). + +Build hooks (`package:flutter_scene/build_hooks.dart`): `buildScenes({buildInput, buildOutput, +inputFilePaths, discoveryRoot = 'assets/', assetMode = generatedTree, compressTextures = false})`, +`buildMaterials({...})`, `buildTextures({..., required textures, contents})`, `buildEngineAssets`, +`buildTargetShaderBundleJson`. Outputs land in `flutter_scene_generated/` (never commit +`.fsceneb`/`.shaderbundle`/`.fmat.json`/`.fstex`). Removed 0.21.0: `legacyOnly`, +`dataAssetsIfAvailable`, `outputDirectory`. + +Animation (`Animation`, `AnimationClip`, `AnimationPlayer` exported): + +- Off a loaded model: `node.parsedAnimations`, `node.findAnimationByName(name)`, + `node.createAnimationClip(animation)`, `node.removeAnimationClip(clip)`. Clips start paused at t=0; + call `play()`. +- `AnimationClip`: `playbackTime` (assignment is seek), `playbackTimeScale` (1; negative reverses), + `weight` (0..1), `playing`, `loop`. `play()`, `pause()`, `stop()`, `replay()`, `gotoAndPlay(t)`, + `seek(t)`, `advance(dt)`, `rebind(newTarget, {animation})`. Channels bind by node NAME; channels + whose node is absent from the subtree are dropped (0.22.0 asserts in debug when ALL channels drop). +- `AnimationPlayer`: `createAnimationClip(animation, bindTarget)` (a second call with the same + `Animation.name` replaces), `getClipByName`, `rebind`, `update(dt)` (auto-driven per frame). +- `Animation({name, channels})`, `AnimationChannel`, `BindKey({required nodeName, property = + translation})`, `AnimationProperty` = `translation` | `rotation` | `scale`. +- Declarative: `SceneModel(assetPath, animations: [SceneAnimationSpec(name, {playing = true, loop = + true, weight = 1.0, speed = 1.0})])`. Note `SceneModel` loads via the runtime glTF path. + +The engine-agnostic scene-document core is a separate package `scene` (0.2.0), re-exported through +`package:flutter_scene/fscene.dart`. `flutter_scene_importer` and `flutter_gpu_shim` no longer exist +(folded in). Physics and audio are separate barrels (`physics.dart`, `audio.dart`). + +## Depth precision and z-fighting + +Defaults that keep depth precise: `Scene.reversedDepth` (true; reversed float depth on Metal and +browsers with clip control) and `Scene.fitNearPlane` (true; the near plane the passes rasterize with +is fitted to visible content each frame, the authored near is the floor, and the public projection is +untouched). `Scene.coplanarTieBreak` (false, experimental) gives unlayered materials and instances a +stable order where they overlap exactly. See `depth-and-layering.md`. + +Finding fights: `scene.probeDepthConflicts({camera, width = 960, height = 540, layerMask, minPixels += 4})` -> `Future` (`conflicts` of `DepthConflict` with `nodeA`, `nodeB`, +`pixelCount`, `bounds`, `distance`; `conflictPixelCount`; `describe()`), rendered from the camera. +`scene.findCoplanarOverlaps({camera})` -> `List` (`nodeA`, `nodeB`, `instanceA`, +`instanceB`, `area`, `separation`, `center`, `normal`, `hint`, `exact`, `describe()`), from geometry +with no rendering; debug builds run it once the scene settles and print a summary +(`scene.debugCheckCoplanarOverlaps = false` silences it). `DebugOverlay.depthConflicts` marks fights +live, and `SurfaceDebugChannel.depthGap` shows the gap two surfaces need at each pixel. + +## Debugging the surface + +`Scene.debug` (a `SceneDebugSettings`): `view` is a `DebugView` over a `SurfaceDebugChannel` (geometry attributes, resolved surface channels, physical fields, object and material identity colors, validation flags, a `custom` channel fed by `material.debug` in a `.fmat`), with `gain`, a scalar range, and a `DebugRangePolicy`; `split` compares the view against the lit image; `overlays` holds `DebugOverlay.wireframe` and `DebugOverlay.depthConflicts`. `Node.debugView` overrides or excludes a subtree. The material channels are compiled in only with flutter_scene's `debug_views` hook user-define (`hooks: user_defines: flutter_scene: debug_views: true`, plus `flutter_scene_debug_views: true` under the app's name when its hook builds materials or engine assets); without it every material draws through the fallback, which serves the geometry and identity channels. `DebugViewRegistry` lists every view by id for tools, and `Scene.debugViewId` selects one by id. Raw `ShaderMaterial`s opt in with `debugViews: true` after defining `FLUTTER_SCENE_DEBUG_VIEWS` and including `material_debug.glsl`; those that do not are drawn by a fallback that stripes the material channels. diff --git a/.claude/skills/flutter_scene-kit/SKILL.md b/.claude/skills/flutter_scene-kit/SKILL.md new file mode 100644 index 00000000..5feef6b7 --- /dev/null +++ b/.claude/skills/flutter_scene-kit/SKILL.md @@ -0,0 +1,158 @@ +--- +name: flutter_scene-kit +version: 8 +description: Build interactive 3D gameplay, character controllers, camera rigs, dynamic day/night cycles, water surfaces, audio, pooling, and debug overlays in flutter_scene. Use when creating game mechanics, camera controls, NPC behaviors, atmospheric environments, or diagnostic HUDs. +user-invocable: false +--- + +# Gameplay, camera, and atmosphere kit in flutter_scene + +Flutter Scene provides high-level gameplay components and ergonomic building blocks in `package:flutter_scene/kit.dart` so games and interactive experiences do not need to re-implement standard mechanics from scratch. + +When choosing components, consider existing engine alternatives: +- For physics-driven character navigation with collider capsules, wall sliding, and autostep, use `KinematicCharacterController` from `package:flutter_scene/physics.dart`. +- For interactive mouse/touch orbit cameras with inertia, use `OrbitCameraController` or `FollowCameraController`. +- For framing a standalone `PerspectiveCamera`, use `PerspectiveCamera.framing`. Use `BoundsFraming` when computing a transform for a `NodeCamera` mounted in the scene graph. +- When driving custom camera-relative movement or ground panning (WASD strafing, RTS/strategy camera drag), read `camera.horizontalRight` and `camera.horizontalForward` (or `camera.right` and `node.right`). Never write `forward.cross(up)`, which points screen-left in `flutter_scene`'s left-handed (`+Z` forward) view space and horizontally inverts `A`/`D` and drag controls. + +## Imports + +```dart +import 'package:flutter_scene/scene.dart'; +import 'package:flutter_scene/kit.dart'; +import 'package:vector_math/vector_math.dart' as vm; +``` + +## Camera rigs and smoothing + +### SpringArmComponent + +`SpringArmComponent` attaches to a target character node and mounts a camera node at the arm's socket. It casts rays against the scene hierarchy to prevent geometry clipping, smoothly pulling the camera inward when colliding with walls. + +Note on offsets: `targetOffset` is applied in world space from the character node's origin, and `socketOffset` acts in the camera socket's local plane along X (right) and Y (up). + +Leave the mounted camera's near plane at its default. Every pass rasterizes with a near plane fitted to visible content each frame, so a chase or follow camera gets the depth precision of a much larger near without clipping the character when the arm pulls in. Raising `near` by hand clips the character at close range and gains nothing; the fit falls back to the authored value only when the camera sits inside an item's bounds (a sky sphere), so draw skies with `scene.skybox`. + +```dart +final characterNode = Node(); +final cameraNode = Node(); +final cameraArm = SpringArmComponent( + targetLength: 5.0, + targetOffset: vm.Vector3(0, 1.6, 0), // Eye height + socketOffset: vm.Vector3(0.5, 0, 0), // Over-the-shoulder + enablePositionLag: true, + positionLagSpeed: 8.0, + cameraNode: cameraNode, +); + +characterNode.addComponent(cameraArm); +scene.root.add(characterNode); +scene.root.add(cameraNode); +``` + +### CameraShake + +`CameraShake` implements a trauma-decay model driven by deterministic simplex noise for organic multi-axis camera shake (explosions, footsteps, hits). + +```dart +final shake = CameraShake(decayRate: 1.2, frequency: 25.0); + +// Add trauma on hit +shake.addTrauma(0.6); + +// Inside game loop +final offset = shake.update(deltaSeconds); +cameraNode.localTransform = baseTransform * offset.toMatrix4(); +``` + +## Character movement and steering + +### ThirdPersonControllerComponent + +`ThirdPersonControllerComponent` handles kinematic movement, sprint multipliers, turn smoothing, ground snapping with raycasts, slope sliding, coyote time, and buffered jumps. Input expects `+Y` as forward in 3D. + +```dart +final playerNode = Node(); +final controller = ThirdPersonControllerComponent( + walkSpeed: 4.5, + runMultiplier: 1.8, + jumpVelocity: 7.0, + groundPlaneHeight: 0.0, // Optional fallback floor +); +playerNode.addComponent(controller); + +// Pass rotatesToMovement: false to move the node without turning it; read +// controller.yaw to drive a child mesh or animation from the heading instead. +// When using VirtualJoystick (where up is -Y in screen space), invert Y: +// controller.setMoveInput(vm.Vector2(joystickDir.x, -joystickDir.y), isRunning: isSprinting); +if (jumpPressed) controller.jump(); +``` + +### Autonomous Steering Behaviors + +`Steering` provides math helpers for NPC navigation, flocking, and crowd dynamics. + +```dart +// Seek target +final seekForce = Steering.seek(npcPos, npcVel, targetPos, maxSpeed: 4.0); + +// Arrive smoothly +final arriveForce = Steering.arrive(npcPos, npcVel, targetPos, slowingRadius: 3.0); + +// Flocking separation +final sepForce = Steering.separation(npcPos, npcVel, neighborPositions, desiredDistance: 1.5); +``` + +## Dynamic environments and atmosphere + +### DayNightCycleComponent + +`DayNightCycleComponent` moves the sun along a realistic solar arc given latitude and time of day, evaluating sun colors, intensities, and ambient lighting transitions. + +```dart +final sunLight = DirectionalLight(); +final sunNode = Node()..addComponent(DirectionalLightComponent(sunLight)); +scene.root.add(sunNode); + +final skyCycle = DayNightCycleComponent( + timeOfDay: 14.5, // 2:30 PM + timeSpeed: 0.1, // Progress 0.1 hours per second + latitude: 34.0, + sunLightNode: sunNode, +); +scene.root.addComponent(skyCycle); +``` + +### WaterSurfaceComponent + +`WaterSurfaceComponent` evaluates multi-harmonic Gerstner trochoidal waves for water surfaces and floating buoyancy queries. + +```dart +final water = WaterSurfaceComponent(); +final surface = water.evaluateAt(vm.Vector2(playerPos.x, playerPos.z)); +final waterHeight = surface.displacement.y; +final waterNormal = surface.normal; +``` + +## Immediate-mode debug visualization + +`DebugDraw` provides static immediate-mode line, ray, box, sphere, axis, and physics collider drawing utilities for physics debugging and AI visualizers. + +```dart +DebugDraw.line(startPos, endPos, color: vm.Vector4(1, 0, 0, 1)); +DebugDraw.box(aabb, color: vm.Vector4(0, 1, 0, 1)); +DebugDraw.sphere(center, 1.0, color: vm.Vector4(0, 0, 1, 1)); +DebugDraw.axes(node.globalTransform, size: 2.0); + +// Every collider under a node, posed the way the simulation sees it. +// Triggers draw in triggerColor. DebugDraw.shape draws a single posed Shape. +DebugDraw.colliders(scene.root); + +// Render debug lines: one updatable geometry, rebuilt in place each frame. +final debugGeometry = DebugDraw.createGeometry(); +debugNode.mesh = Mesh(debugGeometry, UnlitMaterial()); +// Per frame, after the DebugDraw calls: +DebugDraw.flushInto(debugGeometry); +``` + +`DebugDraw.flushMesh()` builds a new geometry per call; it suits a one-off capture, not a per-frame loop. diff --git a/.claude/skills/flutter_scene-looks/SKILL.md b/.claude/skills/flutter_scene-looks/SKILL.md new file mode 100644 index 00000000..64573ce9 --- /dev/null +++ b/.claude/skills/flutter_scene-looks/SKILL.md @@ -0,0 +1,167 @@ +--- +name: flutter_scene-looks +version: 5 +description: Give a flutter_scene render a deliberate, polished look. Use this whenever a scene looks flat, dull, or washed out, or whenever the ask is to make it look good, because a good look is lighting plus post-processing, not geometry. Ships copy-paste EnvironmentSettings presets that configure the whole stack coherently. +user-invocable: false +--- + +# Making flutter_scene look good + +The single most common reason a flutter_scene render looks amateur is that the post-processing stack was left at defaults. flutter_scene has a deep lit-and-post pipeline (image-based lighting, tone mapping, bloom, lens flares, ambient occlusion, screen-space reflections, fog, god rays, depth of field, color grading, vignette, film grain). Out of the box almost all of it is off, so a bare scene is technically correct and visually flat. + +**The insight: a polished look is lighting and post-processing, not geometry.** Better meshes will not fix a flat render. Do not spend effort on modeling detail when the scene reads dull; spend it on the look. And a look is a *coherent* set of choices, not twelve knobs turned independently. Turning bloom, AO, SSR, fog, grain, and grading up one at a time, each by feel, lands in muddy incoherent territory. Pick one deliberate preset and paste it whole. + +## Apply a look in one line + +Every scene-wide look field lives on one aggregate value, `EnvironmentSettings`, assigned through a single setter: + +```dart +scene.environmentSettings = EnvironmentSettings(/* fields below */); +``` + +That one assignment configures tone mapping, exposure, image-based-lighting intensity, and the entire post stack, **including** fog, god rays, depth of field, and auto exposure. You do not need to touch `scene.fog`, `scene.godRays`, `scene.depthOfField`, or `scene.autoExposure` separately; those are the live per-effect objects, but `EnvironmentSettings` carries all of their fields and applies them for you. Every effect is off by default, so a preset only names the fields it turns on. + +`EnvironmentSettings` is also a blendable snapshot. Read the current look with `scene.environmentSettings`, and cross-fade two looks with `EnvironmentSettings.lerp(a, b, t)` driven from an animation. That is how you transition day to night or ramp an effect in. + +## Lights are separate, and still matter + +`EnvironmentSettings` covers image-based lighting (the `environment` map) and the whole post stack, but **direct lights are not part of it.** Shadows, god rays, and any strong key light come from the scene's lights, set separately: + +```dart +scene.directionalLight = DirectionalLight( + direction: vm.Vector3(-0.4, -1.0, -0.3), + intensity: 4.0, + castsShadow: true, // shadows are off until you ask +); +``` + +An unset `scene.environment` still resolves to a default studio IBL, so a `PhysicallyBasedMaterial` is always lit. But a scene with no direct light is soft and shadowless. God rays require a shadow-casting `DirectionalLight`; shadows require `castsShadow: true` on the light. + +## The four looks + +Paste one whole. Each is a real `EnvironmentSettings` literal; import `package:vector_math/vector_math.dart as vm` for the `Vector3` color fields. AO, SSR, god rays, and depth of field work with perspective and orthographic cameras. + +### showcase + +Clean, bright product-viz beauty. Punchy tone mapping, soft bloom on highlights, grounded contact occlusion, and real reflections. The default reach-for-it look. + +```dart +scene.environmentSettings = EnvironmentSettings( + toneMapping: ToneMappingMode.aces, + exposure: 1.0, + bloomEnabled: true, + bloomThreshold: 1.1, + bloomIntensity: 0.2, + bloomScatter: 0.7, + ambientOcclusionEnabled: true, + ambientOcclusionMethod: AmbientOcclusionMethod.groundTruth, + ambientOcclusionBentNormals: true, + ambientOcclusionSpecularMode: SpecularAmbientOcclusionMode.bentCone, + ambientOcclusionIntensity: 1.0, + screenSpaceReflectionsEnabled: true, + screenSpaceReflectionsIntensity: 1.0, + vignetteEnabled: true, + vignetteIntensity: 0.25, +); +``` + +### stylized + +Vivid and graphic. Saturated, slightly warm, glowing, flatter shading (no heavy occlusion or reflections). For playful or illustrative scenes. + +```dart +scene.environmentSettings = EnvironmentSettings( + toneMapping: ToneMappingMode.aces, + colorGradingEnabled: true, + saturation: 1.25, + contrast: 1.1, + brightness: 1.05, + temperature: 0.1, + bloomEnabled: true, + bloomThreshold: 0.9, + bloomIntensity: 0.28, + bloomScatter: 0.8, + vignetteEnabled: true, + vignetteIntensity: 0.2, +); +``` + +### moody + +Dark, cinematic, atmospheric. Lower exposure, cool graded, foggy, heavy vignette, subtle grain and aberration, deep occlusion. God rays if the scene has a shadow-casting sun. Use a cool horizon-colored fog. + +```dart +scene.environmentSettings = EnvironmentSettings( + toneMapping: ToneMappingMode.aces, + exposure: 0.8, + colorGradingEnabled: true, + contrast: 1.15, + saturation: 0.9, + temperature: -0.1, + fogEnabled: true, + fogMode: FogMode.exponential, + fogColor: vm.Vector3(0.05, 0.06, 0.09), + fogDensity: 0.03, + ambientOcclusionEnabled: true, + ambientOcclusionMethod: AmbientOcclusionMethod.groundTruth, + ambientOcclusionIntensity: 1.2, + ambientOcclusionPower: 1.8, + vignetteEnabled: true, + vignetteIntensity: 0.6, + vignetteRadius: 0.6, + filmGrainEnabled: true, + filmGrainIntensity: 0.25, + chromaticAberrationEnabled: true, + chromaticAberrationIntensity: 0.15, + godRaysEnabled: true, // needs scene.directionalLight with castsShadow: true + godRaysIntensity: 1.0, + godRaysDensity: 0.6, + godRaysColor: vm.Vector3(1.0, 0.95, 0.85), +); +``` + +### clean + +Neutral and honest. Minimal post, no grading, no bloom, no vignette, just correct tone mapping and gentle grounding occlusion. The right look for an editor, an inspector, a UI-embedded viewer, or anywhere you want an accurate read of the actual material. + +```dart +scene.environmentSettings = EnvironmentSettings( + toneMapping: ToneMappingMode.pbrNeutral, // the engine default + exposure: 1.0, + ambientOcclusionEnabled: true, + ambientOcclusionIntensity: 0.8, + ambientOcclusionHalfResolution: true, +); +``` + +## Tuning from a preset + +Start from the nearest look, then move one field at a time. + +- **Too dim or too bright overall.** Change `exposure` (default 1.0), not per-light intensity. Or turn on `autoExposureEnabled: true` to let the scene meter itself. +- **Highlights not glowing.** Lower `bloomThreshold` toward 1.0 or below, or raise `bloomIntensity`. `bloomScatter` widens the glow. +- **Want a lens flare off a bright source.** Turn on `lensFlareEnabled` (needs `bloomEnabled`). Keep `lensFlareIntensity` modest and drop `lensFlareHaloIntensity` first if the flare washes the frame; the halo is the broad wash, the ghosts are the crisp chain. +- **Reads flat and ungrounded.** `ambientOcclusionEnabled: true`. Use `AmbientOcclusionMethod.groundTruth` for quality, keep `ambientOcclusionHalfResolution: true` for cost. +- **Colors feel wrong.** Turn on `colorGradingEnabled` and reach for `saturation`, `contrast`, `temperature`, `tint` before anything else. +- **Wrong tone-map feel.** `ToneMappingMode.aces` is contrasty and filmic, `pbrNeutral` (default) preserves hue and saturation, `agx` is the most neutral highlight rolloff. Do not reintroduce an `exposure: 2.0` hack; that was an artifact of an older renderer. + +## Micro-surface metrics and anti-waxiness tuning + +When tuning procedural materials or custom shaders, use surface metrics and exposure discipline to eliminate waxiness, plastic reads, or harsh aliasing: + +- **Surface reads like smooth plastic.** Increase high-frequency micro-scale variation. Low 1-pixel luminance gradients `(|dL/dx| + |dL/dy|) / 2` indicate untextured or overly smooth surfaces. +- **Blotchy macro clouds.** Balance high-frequency and low-frequency energy ratios (`hf/lf`). High variance with low fine detail indicates large-scale noise patches without adequate surface grain. +- **Harsh normal-map glitter or torn noise.** Check terminator crossing behavior under low grazing light angles (sun low on the horizon). Over-amplified normal maps flip adjacent pixels between full light and full shadow. +- **Colors look washed out in bright areas.** Tone mapping curves compress channel differences near the shoulder. A surface reading high brightness with low saturation is often over-exposed rather than under-pigmented; reduce exposure to restore natural material saturation. + +## Look tools that are not on EnvironmentSettings + +Anti-aliasing, reflection probes, and planar reflectors affect the look but are set outside `EnvironmentSettings`, so a preset does not turn them on. + +- **Anti-aliasing** is a `Scene` field. The default `AntiAliasingMode.auto` already picks `msaa` where the backend supports it and `fxaa` otherwise, so edges are handled. Reach for `scene.antiAliasingMode = AntiAliasingMode.smaa` when `fxaa` looks mushy (it blurs texture detail) and `msaa` is unavailable; SMAA keeps edges clean without the blur, at ~3x fxaa cost. +- **Reflection probes** capture true local reflections that SSR cannot, because SSR only reflects what is on screen. Attach a `ReflectionProbeComponent` to a node placed at the reflective spot (a room, a mirror ball); it captures the surroundings into a parallax-corrected box and blends with the environment. The capture renders the scene six times, so it happens once on activate (or on an explicit `requestCapture()`), never per frame. +- **Planar reflectors** render a true per-frame mirror for one flat surface (a mirror, a glossy floor), which neither SSR nor a probe can produce. Attach a `PlanarReflectorComponent` to the mirror node and give the surface a `.fmat` material declaring `engine_inputs: [ planar_reflection ]`; the component renders the scene once more per frame from the reflected camera and the material samples it with `GetPlanarReflection()`. That extra scene render is the cost, so bound it with `resolutionScale` (default 0.5) and `layerMask`. See `references/looks.md` for all three. + +## Cost + +The post stack is not free. AO, SSR, depth of field, and god rays each add screen-space passes, and mobile and web are the budget. Keep `ambientOcclusionHalfResolution: true`, prefer `DepthOfFieldQuality.low` on mobile, and do not stack SSR plus god rays plus depth of field on a low-end target without profiling. The `clean` look is nearly free; `moody` is the heaviest. See `references/looks.md` for the full per-effect knob reference, defaults, and cost notes. diff --git a/.claude/skills/flutter_scene-looks/references/looks.md b/.claude/skills/flutter_scene-looks/references/looks.md new file mode 100644 index 00000000..86823c8d --- /dev/null +++ b/.claude/skills/flutter_scene-looks/references/looks.md @@ -0,0 +1,290 @@ +# The look stack, knob by knob + +Every field here is a constructor argument on `EnvironmentSettings` (`lib/src/environment_settings.dart`). Assigning `scene.environmentSettings = EnvironmentSettings(...)` applies all of them at once. Defaults are the constructor defaults; a preset only names what it changes. Direct lights (`scene.directionalLight` and friends) are separate and not covered by `EnvironmentSettings`. + +Color fields are `vm.Vector3` (import `package:vector_math/vector_math.dart as vm`). + +--- + +## Base look + +| Field | Default | What it does | +| --- | --- | --- | +| `toneMapping` | `ToneMappingMode.pbrNeutral` | HDR to display operator. `pbrNeutral` preserves hue/saturation, `aces` is contrasty and filmic, `agx` has the gentlest highlight rolloff, `reinhard`/`linear` are simpler references. | +| `exposure` | `1.0` | Linear scene exposure multiplier. The one knob for overall brightness. Do not use `2.0`; that was an old-renderer hack. | +| `environmentIntensity` | `1.0` | Scales image-based lighting (the environment map) contribution. | +| `agxWhite` | `16.29` | AgX white point, only meaningful with `ToneMappingMode.agx`. | +| `agxContrast` | `1.25` | AgX contrast, only with `agx`. | + +`environment` (an `EnvironmentMap?`) and the sky fields are also on `EnvironmentSettings`, but building environment maps is the domain of the idioms skill; a null environment resolves to a default studio IBL. + +## Auto exposure (`autoExposure*`) + +Meters the frame and multiplies on top of `exposure`. Off by default. + +| Field | Default | What it does | +| --- | --- | --- | +| `autoExposureEnabled` | `false` | Turn metering on. | +| `autoExposureStrength` | `0.55` | How fully it drives toward the metered target (0 = none, 1 = full). | +| `autoExposureCompensation` | `0.0` | EV bias applied after metering. | +| `autoExposureMinEv` / `autoExposureMaxEv` | `-4.0` / `4.0` | Clamp range for the adaptation. | +| `autoExposureSpeedUp` / `autoExposureSpeedDown` | `3.0` / `1.0` | Adaptation rate brightening vs darkening. | + +## Bloom (`bloom*`) + +Blooms bright pixels. Off by default. The cheapest way to make a scene feel lit rather than rendered. + +| Field | Default | What it does | +| --- | --- | --- | +| `bloomEnabled` | `false` | Turn bloom on. | +| `bloomThreshold` | `1.0` | Brightness above which a pixel blooms. Lower for more glow. | +| `bloomIntensity` | `0.15` | Strength of the added glow. | +| `bloomScatter` | `0.7` | Spread of the glow, wider values feel dreamier. | + +### Lens flares (`lensFlare*`) + +Ghost chains and a halo ring off the bloom pyramid, for bright emissive sources and sun disks. Rides the bloom chain, so `bloomEnabled` must be on and the flare scales with `bloomIntensity`. Off by default. A little goes a long way; a strong source with high intensity/halo washes the frame. + +| Field | Default | What it does | +| --- | --- | --- | +| `lensFlareEnabled` | `false` | Turn flares on. Needs `bloomEnabled`. | +| `lensFlareIntensity` | `1.0` | Strength of the flare features relative to the bloom. | +| `lensFlareGhostCount` | `4` | Internal-reflection ghosts along the line through the screen center (clamped to 8 at render). | +| `lensFlareGhostSpacing` | `0.3` | Spacing between ghosts, as a fraction of the distance to the center. | +| `lensFlareHaloRadius` | `0.35` | Halo ring radius in screen UV units. | +| `lensFlareHaloIntensity` | `1.0` | Halo strength relative to the ghosts. `0` disables the halo. The halo is what washes the whole frame, so tame it first. | +| `lensFlareChromaticAberration` | `0.005` | Radial color dispersion of the flare features. | + +## Color grading (`colorGrading*`) + +Post-tone-map color shaping. Off by default. Reach here to change the *mood* of the color rather than the exposure. + +| Field | Default | What it does | +| --- | --- | --- | +| `colorGradingEnabled` | `false` | Turn grading on. | +| `brightness` | `1.0` | Multiplicative brightness. | +| `contrast` | `1.0` | Contrast around mid-gray. | +| `saturation` | `1.0` | Color saturation. Above 1 is vivid, below 1 desaturates toward gray. | +| `temperature` | `0.0` | Warm (positive) to cool (negative) white balance. | +| `tint` | `0.0` | Green to magenta balance. | +| `lift` / `gamma` / `gain` | `Vector3(0)` / `Vector3(1)` / `Vector3(1)` | Per-channel shadow/mid/highlight color control (lift-gamma-gain). | +| `colorGradingLut` | `null` | A `ColorLut` (from a `.cube` file) applied after tone mapping. Independent of `colorGradingEnabled`. | +| `colorGradingLutBlend` | `1.0` | LUT mix amount. | + +## Vignette (`vignette*`) + +Darkens the frame edges. Off by default. Small amounts read as cinematic; large amounts as a peephole. + +| Field | Default | What it does | +| --- | --- | --- | +| `vignetteEnabled` | `false` | Turn vignette on. | +| `vignetteIntensity` | `0.5` | Darkening strength at the edge. | +| `vignetteRadius` | `0.75` | How far in the darkening starts (smaller = tighter, more closed-in). | +| `vignetteSmoothness` | `0.5` | Falloff softness of the edge. | + +## Chromatic aberration (`chromaticAberration*`) + +Splits color channels toward the edges. Off by default. A touch adds a lens feel; too much looks broken. + +| Field | Default | What it does | +| --- | --- | --- | +| `chromaticAberrationEnabled` | `false` | Turn it on. | +| `chromaticAberrationIntensity` | `0.2` | Channel-separation strength. | + +## Film grain (`filmGrain*`) + +Adds animated grain. Off by default. Sells a moody or analog look and hides banding in dark gradients. + +| Field | Default | What it does | +| --- | --- | --- | +| `filmGrainEnabled` | `false` | Turn it on. | +| `filmGrainIntensity` | `0.3` | Grain strength. | + +## Ambient occlusion (`ambientOcclusion*`) + +Screen-space contact darkening. Off by default. Requires a `PerspectiveCamera`. The single biggest upgrade for "grounded" vs "floating". Half-resolution by default to stay affordable. + +| Field | Default | What it does | +| --- | --- | --- | +| `ambientOcclusionEnabled` | `false` | Turn AO on. | +| `ambientOcclusionMethod` | `AmbientOcclusionMethod.obscurance` | `obscurance` is the cheap default; `groundTruth` (GTAO) is higher quality and needed for bent normals. | +| `ambientOcclusionIntensity` | `1.0` | Darkening strength. | +| `ambientOcclusionRadius` | `0.33` | World-space sampling radius. | +| `ambientOcclusionPower` | `1.5` | Contrast of the occlusion curve. | +| `ambientOcclusionBias` | `0.07` | Self-occlusion rejection. | +| `ambientOcclusionBentNormals` | `false` | Compute bent normals (needs `groundTruth`); improves indirect lighting direction and enables `bentCone` specular AO. | +| `ambientOcclusionSpecularMode` | `SpecularAmbientOcclusionMode.none` | `simple` occludes reflections cheaply; `bentCone` is directional and needs bent normals. | +| `ambientOcclusionHalfResolution` | `true` | Compute at half res. Keep on unless AO edges look too coarse. | +| `ambientOcclusionIndirectLight` | `0.0` | Above 0 turns on screen-space global illumination (SSGI) bounce; expensive. Its radiance history reprojects, so the bounce stays put under camera motion (object motion still lags). | +| `ambientOcclusionMultiBounce` | `0.0` | Approximate multi-bounce darkening recovery. | +| `ambientOcclusionSampleCount` | `16` | Samples for the `obscurance` method. | +| `ambientOcclusionSliceCount` / `ambientOcclusionStepsPerSlice` | `3` / `3` | GTAO slice sampling. | +| `ambientOcclusionDetail` | `0.5` | Fine-detail term weight (obscurance). | +| `ambientOcclusionHorizonAngle` | `0.06` | Horizon rejection angle. | +| `ambientOcclusionThickness` / `ambientOcclusionThicknessHeuristic` | `0.5` / `0.004` | Depth thickness assumptions for occlusion. | +| `ambientOcclusionDirectLightAffect` | `0.0` | How much AO also dims direct light. | +| `ambientOcclusionVisibilityBitmask` | `false` | Bitmask visibility estimator. | +| `ambientOcclusionDepthMipChain` | `false` | Build a depth mip chain for wide-radius sampling. | + +## Screen-space reflections (`screenSpaceReflections*`) + +Reflects on-screen geometry. Off by default. Requires a `PerspectiveCamera`. Adds realism to floors, water, and glossy surfaces, but only reflects what is on screen. + +| Field | Default | What it does | +| --- | --- | --- | +| `screenSpaceReflectionsEnabled` | `false` | Turn SSR on. | +| `screenSpaceReflectionsIntensity` | `1.0` | Reflection strength. | +| `screenSpaceReflectionsMaxDistance` | `24.4` | Max world-space ray distance. | +| `screenSpaceReflectionsThickness` | `0.46` | Assumed surface thickness for hit tests. | +| `screenSpaceReflectionsStride` | `9.0` | March step size (larger = faster, coarser). | +| `screenSpaceReflectionsMaxSteps` | `90` | Ray-march step budget. | +| `screenSpaceReflectionsBlur` | `0.3` | Roughness-based blur of the reflection. | +| `screenSpaceReflectionsDistanceFadeStart` | `0.0` | Where reflections start fading with distance. | +| `screenSpaceReflectionsResolutionScale` | `1.0` | Compute resolution scale; drop below 1 to save cost. | + +## Fog (`fog*`) + +Distance and height fog, evaluated in linear HDR before tone mapping. Off by default. Needs both `fogEnabled` and a non-`none` `fogMode`. Applies to lit and unlit materials; the skybox is left unfogged, so set `fogColor` to your horizon color for distant blending. + +| Field | Default | What it does | +| --- | --- | --- | +| `fogEnabled` | `false` | Turn fog on. | +| `fogMode` | `FogMode.exponential` | `none`, `linear`, `exponential`, `exponentialSquared`. Must be non-`none` to render. | +| `fogColor` | `Vector3(0.6, 0.7, 0.8)` | Fog tint. Match your sky/horizon. | +| `fogDensity` | `0.02` | Density for the exponential modes. | +| `fogStart` / `fogEnd` | `0.0` / `200.0` | Near/far bounds for `linear` mode. | +| `fogSkyColorInfluence` | `0.0` | Blend fog color toward the sky color. | +| `fogMaxOpacity` | `1.0` | Cap on how opaque fog gets. | +| `fogHeight` / `fogHeightFalloff` | `0.0` / `0.0` | Height-fog band and falloff. | +| `fogSunInScatter` / `fogSunInScatterExponent` | `0.0` / `8.0` | Sun in-scatter glow through the fog. | +| `fogCutoffDistance` | `0.0` | Distance beyond which fog stops accumulating. | + +## God rays (`godRays*`) + +Volumetric light shafts. Off by default. Requires a shadow-casting `DirectionalLight` and a `PerspectiveCamera`; without a shadow-casting sun there is nothing to shaft. + +| Field | Default | What it does | +| --- | --- | --- | +| `godRaysEnabled` | `false` | Turn shafts on. | +| `godRaysIntensity` | `1.0` | Shaft strength. | +| `godRaysDensity` | `0.5` | Medium density the light scatters through. | +| `godRaysAnisotropy` | `0.7` | Forward-scatter bias (higher = tighter shafts toward the sun). | +| `godRaysStepCount` | `24` | March steps; higher is smoother and costlier. | +| `godRaysMaxDistance` | `200.0` | Max shaft distance. | +| `godRaysJitter` | `1.0` | Dither to hide banding. | +| `godRaysColor` | `Vector3(1)` | Shaft tint. | + +## Depth of field (`depthOfField*`) + +Physically parameterized lens blur. Off by default. Requires a `PerspectiveCamera`. Great for a hero shot, wasteful for a full interactive scene. + +| Field | Default | What it does | +| --- | --- | --- | +| `depthOfFieldEnabled` | `false` | Turn DoF on. | +| `depthOfFieldFocusDistance` | `10.0` | World distance in sharp focus. | +| `depthOfFieldFStop` | `2.8` | Aperture; lower = shallower focus, more blur. | +| `depthOfFieldFocalLength` | `0.0` | Lens focal length (0 derives from FOV). | +| `depthOfFieldSensorHeight` | `0.024` | Sensor height in meters (35mm-ish). | +| `depthOfFieldBlurScale` | `1.0` | Overall blur multiplier. | +| `depthOfFieldMaxForegroundBlur` / `depthOfFieldMaxBackgroundBlur` | `24.0` / `32.0` | Blur radius caps. | +| `depthOfFieldBladeCount` | `0` | Aperture blades for bokeh shape (0 = round). | +| `depthOfFieldBladeRotation` / `depthOfFieldBladeCurvature` | `0.0` / `0.0` | Bokeh blade shaping. | +| `depthOfFieldQuality` | `DepthOfFieldQuality.medium` | `low` (16 taps, mobile/web), `medium` (32 taps + postfilter), `high` (48 taps). | + +--- + +## Not on `EnvironmentSettings` + +Two look-affecting settings live outside the `EnvironmentSettings` snapshot: anti-aliasing is a `Scene` field, and reflection probes are a scene-graph component. Set them directly. + +### Anti-aliasing (`Scene.antiAliasingMode`) + +Edge anti-aliasing. `AntiAliasingMode.auto` is the default: it picks `msaa` where the backend supports it and `fxaa` otherwise. Set it directly, not through `EnvironmentSettings`. + +```dart +scene.antiAliasingMode = AntiAliasingMode.smaa; +``` + +| Mode | What it does | +| --- | --- | +| `none` | No anti-aliasing; native-resolution edges. | +| `msaa` | 4x MSAA on the scene pass, the best geometry-edge quality and cheap on mobile GPUs, but not supported on every Flutter GPU backend (falls back to `fxaa`). Check `Scene.isAntiAliasingModeSupported` and read `Scene.effectiveAntiAliasingMode`. | +| `fxaa` | One post pass over the tone-mapped image, supported everywhere, but softens all high-contrast edges including texture detail. | +| `smaa` | SMAA 1x, three post passes, supported everywhere. Reconstructs edge shapes so edges are cleaner than `fxaa` with far less texture blurring, at roughly 3x the `fxaa` cost. Reach for it when `fxaa` looks mushy and `msaa` is unavailable. | +| `auto` | `msaa` where supported, else `fxaa`. | + +### Reflection probes (`ReflectionProbeComponent`) + +SSR only reflects what is currently on screen. A reflection probe captures the surroundings from a point into a local, parallax-corrected environment, so off-screen geometry reflects correctly inside a bounded box (a mirror ball, a glossy floor in a room). It is a `Component` attached to a `Node`, not an `EnvironmentSettings` field. + +```dart +final probe = Node() + ..localTransform = vm.Matrix4.translation(vm.Vector3(0, 1, 0)); // reflective spot +probe.addComponent(ReflectionProbeComponent( + extents: vm.Vector3(4, 3, 4), // box half-extents (the influence + parallax volume) +)); +scene.add(probe); +``` + +| Constructor arg | Default | What it does | +| --- | --- | --- | +| `extents` | `Vector3.all(5.0)` | Half-extents of the world-axis-aligned box that is both the influence volume and the parallax proxy. | +| `blendDistance` | `1.0` | Distance over which the probe cross-fades with the environment at the box edge. | +| `priority` | `10.0` | Which probe wins where several overlap. | +| `weight` | `1.0` | Contribution scale in the blend. | +| `faceResolution` | `128` | Cubemap face resolution of the capture. | +| `captureOnActivate` | `true` | Capture once when the probe joins the scene. Call `requestCapture()` to re-capture after the scene changes; the capture is a static snapshot otherwise. | + +For a one-shot environment capture with no node or parallax (e.g. to hand a captured `EnvironmentMap` to another material), `Scene.captureEnvironment(position: ...)` returns an `EnvironmentMap` directly. + +### Planar reflectors (`PlanarReflectorComponent`) + +A true mirror for one flat surface, re-rendered every frame the surface is visible: the engine renders the scene from the view camera reflected across the surface's plane (near plane clamped to the mirror, so nothing behind it leaks in) and hands the capture to the surface's material. Use it for mirrors and glossy floors where SSR's on-screen-only reflections or a probe's static capture are not enough. + +Two pieces pair up. The component goes on the mirror node (the plane is the node's local `+Y` through its transform, or an explicit `localNormal`): + +```dart +final mirror = Node(mesh: Mesh(PlaneGeometry(width: 10, depth: 10), mirrorMaterial)) + ..addComponent(PlanarReflectorComponent()); +scene.add(mirror); +``` + +And the surface's material is a `.fmat` that declares the `planar_reflection` engine input and samples `GetPlanarReflection()` (mirrored scene color in rgb, `a` 1 while a capture is bound; fall back to the environment reflection at `a == 0`). A worked mirror lives at `examples/flutter_app/assets/planar_mirror.fmat`. + +| Constructor arg | Default | What it does | +| --- | --- | --- | +| `resolutionScale` | `0.5` | Capture resolution relative to the view (clamped `0.1..1.0`). The fragment-cost lever. | +| `layerMask` | all layers | What renders into the capture. The draw-cost lever. | +| `reflectionGroupId` | `-1` | Co-planar surfaces sharing a non-negative id share one capture per frame; `-1` means an own capture. | +| `clipBias` | `1e-3` | World-space offset of the clip plane in front of the mirror, keeping the surface itself out of the capture. | +| `localNormal` | local `+Y` | The mirror plane's facing direction in node space. | + +The capture is a second scene submission per reflection group per frame: its CPU and draw-call cost scales with scene complexity, not just resolution. It reuses the frame's shadow atlas and runs without screen-space post; reflectors seen inside a capture draw their base look, so captures never recurse. + +--- + +## Cost and budget + +Post effects are screen-space passes; they cost per output pixel, not per triangle. Rough order, cheapest first: + +- **Nearly free.** Tone mapping, exposure, color grading, vignette, chromatic aberration, film grain, bloom (with lens flares). The `clean` and `stylized` looks live here. +- **Moderate.** Ambient occlusion (keep `ambientOcclusionHalfResolution: true`), fog, `fxaa`, `smaa` (~3x `fxaa`). `msaa` is nearly free on mobile GPUs but costs more elsewhere. +- **Expensive.** SSR, god rays, depth of field, and especially SSGI (`ambientOcclusionIndirectLight > 0`). Each adds ray-marching or gather passes. A reflection probe's capture renders the scene six times, so capture on activate or an occasional `requestCapture()`, never per frame. + +Budget guidance: + +- **Mobile and web are the ceiling.** A look that is smooth on desktop can tank a phone. Profile the target, do not assume. +- **Keep AO at half resolution** unless the coarse edges actually show. Full-res AO rarely earns its cost. +- **Do not stack the expensive effects blindly.** SSR plus god rays plus depth of field together on a low-end device needs profiling; drop one or lower its resolution/step budget (`screenSpaceReflectionsResolutionScale`, `godRaysStepCount`, `DepthOfFieldQuality.low`). +- **Depth of field is a hero-shot tool.** It reads as intentional on a framed still and as a smear on a free-moving interactive camera. Prefer it where the camera is controlled. +- **Bloom and grading buy the most look per cost.** When you need "flashier" cheaply, reach for these before the screen-space passes. + +## Why each look is built the way it is + +**showcase** aims for a clean, believable, flattering render, the default for showing a model off. `aces` tone mapping gives contrast and pop; soft bloom (threshold just above 1.0) lights the highlights without haze; GTAO with bent normals and `bentCone` specular AO grounds the object and tightens reflections in its cavities; SSR adds real floor and surface reflections; a light vignette focuses the eye. Every choice supports "look at this object", nothing calls attention to itself. + +**stylized** trades realism for graphic punch. It leans on color (saturation up, a warm push, contrast up) and glow (a lower bloom threshold so more of the frame blooms) and deliberately *omits* AO and SSR, because heavy occlusion and reflection read as realism and fight the flatter, poppier intent. Cheap to run, since it is all near-free passes. + +**moody** is the atmospheric, cinematic end. Lower exposure and cool grading set a somber base; exponential fog with a dark horizon color adds depth and hides the far plane; strong AO deepens the shadows; a tight heavy vignette closes the frame; film grain and a touch of chromatic aberration add texture and a lens feel; god rays (given a shadow-casting sun) add drama. It is the most expensive look; on a tight budget, drop god rays first, then SSR if present. + +**clean** is the honest look, for when the render must show the *actual* material and lighting without editorializing: an editor viewport, an inspector, a UI-embedded preview. Default `pbrNeutral` tone mapping preserves hue and saturation, and the only effect on is gentle half-res AO for grounding. No bloom, grading, or vignette, because each of those changes what the color and brightness actually are, which is exactly what an accurate preview must not do. Also the cheapest look by far. diff --git a/.claude/skills/flutter_scene-performance/SKILL.md b/.claude/skills/flutter_scene-performance/SKILL.md new file mode 100644 index 00000000..0ae8eee0 --- /dev/null +++ b/.claude/skills/flutter_scene-performance/SKILL.md @@ -0,0 +1,64 @@ +--- +name: flutter_scene-performance +version: 3 +description: Make a flutter_scene app hit frame budget. Use whenever a scene janks, stutters, or drops frames, or when the ask is to make it faster or run on mobile or web, because code-driven scenes are reliably slow and the fix depends on which thread is over budget, not on a guessed poly count. +user-invocable: false +--- + +# Making flutter_scene fast + +A code-driven flutter_scene scene is reliably slow, and the usual reason is that nothing told you the budget or what to fix first, so optimization starts as guesswork. Guessing wastes iterations and often makes the wrong thread slower. This skill replaces the guessing with a budget, a way to measure it, and a fixed order to apply fixes in. + +**The one thing to internalize: measure the actual frame, find which of the two threads is over budget, then fix that thread. Do not target a triangle or draw-call number from memory.** There is no built-in poly budget, and the same scene can be fast on desktop and jank on a phone. The number that matters is milliseconds per frame on the real target. + +## The budget + +A frame has a fixed wall-clock budget set by the refresh rate. + +- **60 fps is 16.6 ms per frame. 120 fps is 8.3 ms.** Miss it and the frame janks. +- That budget is split across **two threads**, and either one blowing it drops the frame: + - **UI thread** runs your Dart. flutter_scene walks the scene graph, culls, updates components, and builds the render here. + - **Raster thread** is where Impeller draws the built frame on the GPU. The whole post-processing stack (ambient occlusion, reflections, depth of field, god rays, bloom) lands here. +- **Mobile and web are the real constraint.** Desktop GPUs hide a lot; a scene that runs smooth on a laptop can miss budget badly on a phone or in a browser. Profile on the lowest target you must support. + +## Measure first (do not skip this) + +There is no runtime stats API (`scene.frameTime` does not exist), but the renderer has compile-time profiling. Measurement is those defines plus Flutter's own tooling. + +1. **Run in profile mode.** `flutter run --profile --enable-flutter-gpu`. Debug-mode timings are meaningless for performance (assertions, no JIT-to-AOT optimization, extra checks), so never judge speed in debug. +2. **Engine pass timing.** Add `--dart-define=FLUTTER_SCENE_PROFILE=true` and the renderer prints sampled per-pass CPU means and maxes to stdout (`ScenePass`, `DepthPrepass`, `ShadowPass`, `BloomPass`, ...) plus encoder stats (`FLUTTER_SCENE_PROFILE_ENCODER`, draw and instance counts, sort/encode/instance-pack times). A healthy mean next to a huge max is a one-off stall, not steady-state cost; find the stall (see the pipeline section below) instead of shrinking the steady state. +3. **Read the frame chart.** Open DevTools, go to the Performance view, and read **per-frame UI time vs raster time**. The jank frames are flagged. This single view tells you which thread is over budget, which decides everything below. +4. **Or use the performance overlay** for a quick in-app read of the two thread graphs without DevTools. +5. **Stopwatch as a coarse fallback.** A `Stopwatch` around the per-frame work gives a rough UI-thread number when you cannot open DevTools. It sees nothing on the raster thread. + +Which thread is over budget names the fix. UI over budget means too much graph/CPU work (steps a, b, c below). Raster over budget means too much GPU work (steps d, e). See `references/performance.md` for the symptom to thread diagnosis. + +## The fixed remediation order + +Apply top-down. Each step lists the real API. Do the measured-over-budget thread's steps first, but the order within is deliberate, the earlier fixes are the bigger wins. + +1. **Instancing** (UI thread). Many copies of one mesh collapse into one draw and one cull test. Build an `InstancedMesh(geometry:, material:)`, add a transform per copy with `addInstance(matrix, {color})`, and mount it with `InstancedMeshComponent`. The single biggest win for repeated geometry (foliage, crowds, tiles, debris). Per-instance frustum culling is off by default (`cullInstances: false`), so the batch is one cull test as a unit. See the `flutter_scene-procedural` skill for the full scatter pattern. + +2. **Level of detail** (both threads). `LodComponent(List)` swaps cheaper meshes as an object shrinks on screen. Each `LodLevel(geometry:, material:, screenSize:)` gives a threshold (projected size as a fraction of viewport height, highest detail first, last is the cull floor). Fewer triangles for distant objects on the GPU, and nothing drawn below the floor. Note the shadow and depth passes always draw the highest-detail level, so LOD does not lighten shadow cost. + +3. **Culling** (UI thread). `Node.frustumCulled` (default true) skips off-screen subtrees; leave it on. Set it `false` only where the cached bound is known-stale or unbounded (procedural terrain you regenerate). For whole sets, `RenderView.cullingPlanes` adds extra clip planes, and `node.layers` (default `kRenderLayerDefault`) against `RenderView.layerMask` (default `kRenderLayerAll`) skips entire layers a view should not draw. + +4. **Shrink the post stack** (raster thread). This is usually where raster time goes. Turn off effects you do not need via the `EnvironmentSettings` `*Enabled` flags, keep `ambientOcclusionHalfResolution: true`, lower `depthOfFieldQuality` toward `DepthOfFieldQuality.low`, drop `RenderView.renderScale` (or `Scene.renderScale`) below `1.0` to render fewer pixels, and step `Scene.antiAliasingMode` down (`msaa` to `fxaa` to `none`). See the `flutter_scene-looks` skill for what each knob does to the look. + +5. **Texture and material consolidation** (raster thread). Fewer distinct textures and materials means fewer state changes and binds per frame. `TextureAtlas` (with `generateSolidColorAtlasPixels` for placeholders) packs many tiles into one texture so one material covers them all; share a single `Material` instance across many nodes instead of constructing one per node; use `MaterialsVariantsComponent` to switch a model between named material sets rather than duplicating materials. + +6. **Static shadows** (raster thread). `Node.shadowStatic = true` promises a caster's geometry, material coverage, and world transform will not change while mounted, so the engine renders it into cached shadow-map tiles reused across frames instead of re-encoding every caster every frame. A large static world becomes dramatically cheaper to shadow. Flag only genuinely static content; a static node that moves shows stale shadows until its render item re-registers. + +## First-draw pipeline stalls (the other kind of slow) + +Percentiles clean but the **worst** frame is tens to hundreds of milliseconds, at boot, on the first placement, the first effect, the first hover? That is not steady-state cost. Each (vertex shader, fragment shader, vertex layout) triple compiles a backend pipeline synchronously on its first draw, and a heavy custom material can cost 100ms+ on that one frame. + +- **Debug builds name the offenders.** Any pipeline build of 8ms or more prints `flutter_scene: pipeline build took ...` with the material and geometry. Play through the moments that hitch and collect the list; fix the list, not a guess. +- **The fix is pre-warming, not optimization.** The compile happens exactly once; move it where nobody sees it. Draw each named material once behind a load screen or during an intro, at tiny scale inside the frustum (a frustum-culled or `visible: false` node never draws, so it warms nothing). +- **Warm through the real builders.** The pipeline key includes the vertex layout, so a placeholder sphere warms a different pipeline than the real mesh. Build the actual tower/effect/overlay through its normal code path. +- **Keep warmup draws brief.** One frame compiles the pipeline. A material that samples scene depth also forces the full-resolution depth prepass while it is live, so a lingering warmup shows up as raster cost. +- **Do not trust repeat measurements.** GPU drivers cache compiled shaders across runs, so the second profiling session hits caches the first did not. First-run cost is what a fresh install pays. + +## More depth + +`references/performance.md` expands each step with the full API and when it helps, the UI-vs-raster symptom map (what a wrong frame time points to), and the honest note on what measurement tooling actually exists. diff --git a/.claude/skills/flutter_scene-performance/references/performance.md b/.claude/skills/flutter_scene-performance/references/performance.md new file mode 100644 index 00000000..51cd8598 --- /dev/null +++ b/.claude/skills/flutter_scene-performance/references/performance.md @@ -0,0 +1,128 @@ +# flutter_scene performance reference + +Companion to the `flutter_scene-performance` skill. The skill states the budget, the measure-first rule, and the fixed remediation order. This file expands each step with the real API, when it helps and when it does not, the diagnosis that maps a wrong frame time to a thread, and an honest account of the measurement tooling. + +Verify any symbol here against `lib/src` before relying on it; the inventory in the `flutter_scene-idioms` skill (`references/what-exists.md`) is the fuller API map. + +## The two threads, concretely + +A frame is built on the UI thread and drawn on the raster thread, and they overlap across frames (frame N rasters while frame N+1 builds). Either thread over budget drops the frame. + +- **UI thread work** is Dart. flutter_scene walks the scene graph, computes world transforms, runs frustum culling, ticks every component's `update`, and encodes the draw list. Cost scales with node count, component count, and how much per-frame Dart you run in `onTick` or component `update`. +- **Raster thread work** is the GPU. Impeller executes the encoded passes, the shadow pass, the main color pass, and every enabled screen-space post-processing pass. Cost scales with pixels drawn, overdraw, shadow-map resolution, and how many post passes are on. + +## Diagnosis, symptom to thread + +Read the two thread graphs in DevTools (or the performance overlay) and match the over-budget one to a cause. + +| Observation | Over-budget thread | Likely cause | Go to step | +| --- | --- | --- | --- | +| UI time high, raster fine | UI | Too many nodes/draws or heavy per-frame Dart | 1 instancing, 3 culling | +| UI time scales with object count | UI | Thousands of separate nodes for one repeated mesh | 1 instancing | +| UI time high with a huge static world | UI | No culling; whole graph walked every frame | 3 culling | +| Raster time high, UI fine | Raster | Too many pixels or post passes | 4 post stack, 5 consolidation | +| Raster time high, and it tracks resolution | Raster | Fill-bound; too many pixels | 4 `renderScale`, AA | +| Raster spikes only when shadows are on | Raster | Every caster re-encoded per frame | 6 static shadows | +| Raster time tracks the number of distinct materials | Raster | State-change churn from per-node materials/textures | 5 consolidation | +| Jank only on phone or web, smooth on desktop | Whichever is over budget there | Desktop GPU was hiding it | measure on the real target | + +If both threads are near budget, fix the UI thread first (steps 1 to 3); a lighter draw list also lightens the raster thread. + +## Measurement tooling, honestly + +There is **no built-in frame-stats API in flutter_scene**. No `scene.frameTime`, no draw-call count, no visible-triangle count. Do not invent one or claim one exists. + +- **Profile mode is mandatory.** `flutter run --profile --enable-flutter-gpu`. Debug builds carry assertions and skip AOT optimization, so their timings do not reflect a release build. A number taken in debug mode is not a performance number. +- **DevTools Performance view** is the primary tool. It shows per-frame UI time and raster time as two tracks, flags janky frames, and lets you expand a frame's timeline. This is what tells you which thread is over budget. +- **The performance overlay** gives the same two thread graphs in-app for a quick read without attaching DevTools. +- **A `Stopwatch`** around the per-frame work (the `onTick` body, or a component `update`) is a coarse UI-thread fallback. It cannot see the raster thread at all, so a good stopwatch number does not clear a raster-bound jank. +- **Editor MCP.** `get_app_state` reports lifecycle only (launching/running), not timing. The one place per-pass GPU timings surface is a render-graph capture (`Scene.captureRenderGraph`, or the editor MCP capture tool), whose result carries per-pass timing and lets you see which post pass is expensive. That is per-pass GPU detail, not a whole-frame counter, and the editor MCP is not connected in every project. When it is not, the DevTools loop above is fully sufficient. + +## Step 1, instancing + +`InstancedMesh` holds one `geometry`/`material` pair and one transform per copy. The whole set encodes as a single draw and, by default, a single frustum cull test. + +```dart +final mesh = InstancedMesh( + geometry: someGeometry, + material: sharedMaterial, // one material for the whole batch +); +for (final placement in placements) { + mesh.addInstance(placement.transform, color: placement.tint); // matrix is cloned +} +scene.add(Node()..addComponent(InstancedMeshComponent(mesh))); +``` + +- `addInstance(Matrix4, {Vector4? color})` returns an index; the matrix is cloned, so mutating your copy afterward is safe. Per-instance `color` is a linear RGBA multiplier. +- Edit later with `setInstanceTransform(i, m)`, `setInstanceColor(i, color)`, `removeInstanceAt(i)`, `clearInstances()`, or move the whole batch in one pass with `updateInstanceTransforms((list) { ... })`. +- **Culling default.** `cullInstances` defaults to `false`, so the batch is culled as one unit against its combined bounds, not per instance. Turn `cullInstances: true` on only for a batch spread across a large area where many instances are off-screen, since per-instance culling adds CPU work. +- **When it helps.** Repeated geometry, foliage, crowds, tiles, debris, particles-as-meshes. It is the largest UI-thread win available, because N separate nodes become one. It does nothing for a scene of distinct meshes. +- **Winding trap.** A mirrored (negative-determinant) instance edited with `updateInstanceTransforms(recomputeWinding: false)` renders inside-out. Keep instance edits orientation-preserving, or let winding recompute. + +See the `flutter_scene-procedural` skill for the full scatter-on-terrain pattern. + +## Step 2, level of detail + +`LodComponent(List)` draws one of several mesh variants per frame, chosen from how large the object appears on screen. + +```dart +node.addComponent(LodComponent([ + LodLevel(geometry: high, material: mat, screenSize: 0.4), + LodLevel(geometry: mid, material: mat, screenSize: 0.15), + LodLevel(geometry: low, material: mat, screenSize: 0.04), // set 0.0 to never cull +])); +``` + +- `screenSize` is the projected bounding-sphere diameter as a fraction of viewport height. Levels are highest detail first, strictly descending. The engine draws the highest-detail level whose threshold the object still meets, and draws nothing below the last threshold (the cull floor). +- Selection is screen-size based, so it is field-of-view aware and resolution independent, and it is per view (a split-screen frame can pick different levels per view). +- `LodComponent(levels, {lodBias = 1.0, hysteresis = 0.1, blendRange = 0.0})`. `lodBias` above `1` keeps detail farther away; `hysteresis` is a dead-band so an object on a boundary does not flip-flop; `blendRange` above `0` dither-cross-fades adjacent levels to remove the pop (honored by the built-in lit and unlit materials). +- **Limitation that matters for shadows.** The shadow and depth-prepass passes always draw the highest-detail level and ignore the LOD cull. So LOD lightens the color pass, not shadow or depth cost. A shadow-heavy scene needs step 6, not LOD. +- **Not for instanced draws.** A `LodComponent` draws a single mesh and picks one level for the whole node; it does not combine with hardware instancing. + +## Step 3, culling + +Skip work for things the camera cannot see. + +- **`Node.frustumCulled`** (default `true`) skips a subtree whose `combinedLocalBounds` do not intersect the camera frustum. Leave it on. Set it `false` only where the cached bound is known-stale or misleading (procedural geometry you regenerate, large terrain pieces). A subtree that reports no bound (skinned content, geometry without a computable bound) is treated as always visible regardless of the flag. +- **`RenderView.cullingPlanes`** (`List`, default empty) adds extra clip planes beyond the frustum, for portal or region culling. +- **Layers.** `node.layers` (a 32-bit mask, default `kRenderLayerDefault` which is layer 0, not inherited by children) against `RenderView.layerMask` (default `kRenderLayerAll`) decides whether a view draws a node at all, when `node.layers & view.layerMask != 0`. Put editor gizmos, an inset viewport's contents, or a minimap's set on their own layer and give each view the mask it needs, so a view skips whole sets cheaply. +- **When it helps.** Large worlds where much of the graph is off-screen each frame. Culling is a UI-thread win (fewer nodes encoded) that also lightens the raster thread (fewer draws). + +## Step 4, shrink the post stack + +Every screen-space effect is a raster-thread pass. Turning off what you do not need is the most direct raster win. All the scene-wide look fields live on `EnvironmentSettings` (see the `flutter_scene-looks` skill); each effect has an `*Enabled` flag, off by default. + +- **Turn effects off.** `ambientOcclusionEnabled`, `screenSpaceReflectionsEnabled`, `godRaysEnabled`, `depthOfFieldEnabled`, `bloomEnabled`, and the rest default `false`. A preset the app copied may have turned several on; drop the ones the scene does not visibly need. Ambient occlusion, screen-space reflections, god rays, and depth of field are the heavy ones. +- **Half-resolution AO.** `ambientOcclusionHalfResolution` defaults `true`; keep it. Full-resolution AO roughly doubles that pass's cost for little visible gain on most content. +- **Cheaper depth of field.** `depthOfFieldQuality` (`DepthOfFieldQuality.low`/`medium`/`high`, default `medium`) trades gather taps and cleanup passes for time. Step it down to `low` on mobile. +- **Render fewer pixels.** `Scene.renderScale` (default `1.0`), or per-view `RenderView.renderScale`, renders the scene at a fraction of resolution and upscales. Dropping to `0.75` cuts fill cost by nearly half and is often barely visible after anti-aliasing. This is the biggest lever for a fill-bound (resolution-tracking) raster time. +- **Step anti-aliasing down.** `Scene.antiAliasingMode`. `AntiAliasingMode.auto` picks `msaa` where supported else `fxaa`. `msaa` is cheap on mobile tilers and highest quality; `fxaa` is a single post pass on every backend; `smaa` is cleaner than `fxaa` but three post passes (~3x its cost), so step it down to `fxaa` or `none` on a raster-bound target; `none` is free. Read what actually runs with `Scene.effectiveAntiAliasingMode`. +- **Do not re-capture reflection probes per frame.** A `ReflectionProbeComponent` capture (or `Scene.captureEnvironment`) renders the scene six times, a large one-frame spike. Let it capture once on activate and only call `requestCapture()` when the scene visibly changes, never every frame. +- **When it helps.** Any raster-bound scene. The `clean` look in the looks skill is nearly free; a full `moody` stack (AO plus SSR plus god rays plus DoF plus grain) is the heaviest. Do not stack all of those on a low-end target without profiling. + +## Step 5, texture and material consolidation + +Every distinct material and texture is a potential state change and bind on the raster thread. Fewer of them means a shorter, cheaper draw list. + +- **`TextureAtlas`** packs many equally sized tiles (voxel faces, sprite sheets, terrain tiles) into one texture, so a single material and draw call cover every tile. Resolve a tile's UV box with `tileBounds(index)` or map a within-tile coordinate with `tileUv(index, u, v)`, write those into the mesh's texture coordinates, and build the bound material with `toMaterial()`. `generateSolidColorAtlasPixels(tileColors:, columns:, tileSize:, padding:)` builds placeholder pixels to bring the atlas path up before real art exists. +- **Share one `Material` instance** across many nodes rather than constructing a new one per node. Identical materials that are separate objects still churn binds; the same object does not. Build the material once and reuse the reference. +- **`MaterialsVariantsComponent`** switches an imported model between its named `KHR_materials_variants` sets in place (`MaterialsVariantsComponent.of(model)?.select('name')`), instead of duplicating a model per look. Read `variants` for the declared names; `select(null)` restores defaults. +- **When it helps.** Scenes whose raster time tracks the count of distinct materials or textures, tile-based worlds, and models shown in several finishes. + +## Step 6, static shadows + +Shadow casting re-encodes every caster into the shadow map each frame by default. `Node.shadowStatic = true` promises a caster will not change and lets the engine cache its shadow-map tiles across frames. + +```dart +staticWorldNode.shadowStatic = true; // set per mesh-bearing node; not inherited +``` + +- **The contract.** The node's geometry, material coverage, and world transform must not change while mounted. In return, the engine renders it into cached shadow-map tiles reused across frames instead of re-encoding it every frame. Dynamic nodes (the default) still cast per-frame shadows on top of the cache, so a moving character over a static world works. +- **Not inherited.** Set it on each mesh-bearing node, not once on a root. +- **Stale-shadow caveat.** A static node that does change (moves, remeshes, edits material coverage) shows stale shadows until its render item re-registers. Flag only genuinely static content. +- **Displacement caveat.** A material with a `vertex { }` displacement stage should stay dynamic, since its cached shadow would not follow a camera-dependent displacement. +- **When it helps.** Large static worlds with a shadow-casting `DirectionalLight`. The win scales with how many static casters you have; a mostly static level with a few moving actors is the ideal case. + +## Order and stopping + +Fix the measured over-budget thread first, top-down within it, and re-measure after each change so cause and effect stay legible. Stop when the frame chart clears budget on the real target; there is no reason to keep optimizing a thread that is already under budget while the other one janks. The whole point of measuring first is to avoid spending a step's effort on the thread that was never the problem. diff --git a/.claude/skills/flutter_scene-procedural/SKILL.md b/.claude/skills/flutter_scene-procedural/SKILL.md new file mode 100644 index 00000000..eba7ee9d --- /dev/null +++ b/.claude/skills/flutter_scene-procedural/SKILL.md @@ -0,0 +1,139 @@ +--- +name: flutter_scene-procedural +version: 4 +description: Build flutter_scene content from code instead of asset files. Use when generating terrain, scattering vegetation or crowds, building oceans and Gerstner waves, setting up procedural skies and trees, assembling modular kits, or driving a scene from noise and instancing rather than loading a .glb. +user-invocable: false +--- + +# Procedural content in flutter_scene + +A lot of 3D work does not need an artist's `.glb` at all. Terrain, scattered foliage, debris fields, crowds, and modular buildings are cheaper and more flexible built from code, and flutter_scene has the whole path in the box. Reach for it before wiring up an asset pipeline. + +**The insight: for a code-driven scene, generate geometry and draw it instanced. That path is more reliable than loading external assets, because it has no import step, no coordinate-conversion traps, no missing-file failure modes, and one draw call for thousands of copies.** Three pieces cover almost everything: + +- **`GeometryBuilder`** (and the built-in primitives and swept paths) build custom meshes without a model file. +- **`FastNoiseLite`** drives heightmaps, placement, and displacement deterministically. +- **`InstancedMesh`** draws thousands of copies of one mesh as a single render item. + +Do not hand-pack a `ByteData` vertex buffer. The vertex layout is fixed (72 bytes unskinned, a specific attribute order) and a wrong stride fails silently with washed-out or see-through geometry. `GeometryBuilder` and `MeshGeometry.fromArrays` interleave the layout for you. + +## Imports + +Geometry and instancing live in the main barrel. **Noise is a separate barrel** and is easy to forget: + +```dart +import 'package:flutter_scene/scene.dart'; // GeometryBuilder, MeshGeometry, InstancedMesh, ... +import 'package:flutter_scene/noise.dart'; // FastNoiseLite, bakeNoiseTexture, noiseCurl3 +import 'package:vector_math/vector_math.dart' as vm; // NOT vector_math_64 +``` + +## Terrain from a noise heightmap + +Sample `FastNoiseLite` on a grid, add each vertex, and wind the two triangles per cell so the lit surface faces up. Omitting normals lets the builder derive them from the actual face slopes, which is what you want for terrain. + +```dart +MeshGeometry buildTerrain({int cols = 128, int rows = 128, double spacing = 0.5}) { + final noise = FastNoiseLite(seed: 1337) + ..noiseType = NoiseType.openSimplex2 + ..fractalType = FractalType.fbm // stack octaves for natural detail + ..octaves = 5 + ..frequency = 0.02; // world units are multiplied by this + + final builder = GeometryBuilder(); + + // One vertex per grid point. getNoise2 returns roughly -1..1. + for (var r = 0; r < rows; r++) { + for (var c = 0; c < cols; c++) { + final x = c * spacing; + final z = r * spacing; + final height = noise.getNoise2(x, z) * 6.0; + builder.addVertex(vm.Vector3(x, height, z)); + } + } + + // Two triangles per cell, wound Counter-Clockwise (CCW) so the front face points +Y (up). + for (var r = 0; r < rows - 1; r++) { + for (var c = 0; c < cols - 1; c++) { + final v00 = r * cols + c; + final v10 = v00 + 1; + final v01 = v00 + cols; + final v11 = v01 + 1; + builder + ..addTriangle(v00, v01, v10) + ..addTriangle(v10, v01, v11); + } + } + + return builder.build(); +} +``` + +Attach it like any mesh: + +```dart +final terrain = Node(mesh: Mesh(buildTerrain(), PhysicallyBasedMaterial()..roughnessFactor = 1.0)); +scene.add(terrain); +``` + +If a hand-built surface renders inside-out (visible only from below, dark where lit), reverse each triangle's index order. flutter_scene's front faces wind Counter-Clockwise (CCW) in model space, matching glTF and standard conventions; never fix orientation with a per-triangle flip on an imported model, but for geometry you author yourself the winding is yours to set. + +## Natural formations and landscape recipes + +To achieve documentary realism rather than generic procedural lumps: + +1. **Footpaths are scoured trenches, not flat stripes.** A real trail is the lowest line across terrain because water and foot traffic erode it downwards. When generating heightfields, cut the trail path profile down into the terrain with banks rising away on both sides. +2. **Ridged noise for valley walls and cliffs.** Standard `FractalType.fbm` makes rolling mounds. Use `FractalType.ridged` for valley walls, mountain spurs, and cliffs to produce sharp erosion creases. +3. **Free-end Worley rock cracks.** Standard Worley noise (`F2 - F1`) creates closed polygonal loops like bathroom tile. To produce weathered rock fractures with natural free ends, multiply the cell border by a low-frequency region mask and a high-frequency grain breaker. +4. **Noise-modulated pitting.** A constant threshold radius across Worley cells places a pit in every cell, producing an artificial grid lattice. Modulate the threshold radius with an underlying Perlin field so pores vary in size and only appear in exposed weathering pockets. +5. **Macro massing for scattered gravel.** Soil wears in 0.5m to 2m zones. Modulate multi-scale pebble instances with a low-frequency massing field so gravel clusters into realistic water scour lines rather than uniform sandpaper noise. +6. **Sunk block settling and ground contact staining.** Place boulders and masonry courses 1/3 to 2/3 submerged into the sampled ground height. Use vertex colors or shader ground distance to stain the bottom 20cm of rock near the soil boundary, creating a smooth moisture transition. +7. **Oceans and Gerstner waves.** Sum 4 to 8 directional Gerstner trochoidal waves that pull vertices horizontally toward crests, producing sharp peaks and wide flat troughs. Use Beer-Lambert depth absorption (exp(-sigma_a * d)) via scene depth for turquoise to deep navy transitions, Jacobian folding for peak foam, and darken/smooth tidal sand within the shoreline wash. +8. **Trees and foliage translucency.** Extrude branch splines using `TubeGeometry` or `ExtrudeGeometry`, conserving cross-sectional area across splits (d_parent^2 = sum d_child^2). Set `Material.doubleSided = true` and add diffuse transmission in custom leaf shaders so backlit canopies glow. Apply quadratic cantilever displacement (delta_p proportional to h^2) for organic wind sway. +9. **Procedural skies and IBL synchronization.** Use `PhysicalSkySource` (`lib/src/sky_sources.dart`) with analytic Rayleigh and Mie scattering. Assign `SkyEnvironment` to `Scene.skyEnvironment` or call `EnvironmentMap.fromSky` to bake prefiltered radiance and SH-9 diffuse coefficients into the scene's IBL automatically, and assign the source to `Scene.skybox` for matching background visuals. +10. **Islands and coastal erosion.** Multiply radial distance falloff with domain-warped FBM to form organic bays, sandbars, and lagoons. Use analytical surface slopes to strip topsoil on steep cliffs while depositing golden sand and reef shoals on shallow coastal planes. + +## Scattering thousands of copies + +`InstancedMesh` holds one geometry/material pair and a transform per copy. The whole set is one pipeline and one cull test. Place instances by sampling the same terrain height so they sit on the ground. + +```dart +final rng = math.Random(7); +final scatter = InstancedMesh( + geometry: CylinderGeometry(bottomRadius: 0.0, topRadius: 0.15, height: 1.2), // a cone + material: PhysicallyBasedMaterial()..baseColorFactor = vm.Vector4(0.2, 0.5, 0.15, 1), +); + +for (var i = 0; i < 4000; i++) { + final x = rng.nextDouble() * 64; + final z = rng.nextDouble() * 64; + final y = noise.getNoise2(x, z) * 6.0; // same field as the terrain + final transform = vm.Matrix4.translation(vm.Vector3(x, y, z)) + ..rotateY(rng.nextDouble() * math.pi * 2); + scatter.addInstance(transform); // the matrix is cloned; mutating it later is safe +} + +// InstancedMesh rides on a component, not Node(mesh:). +final node = Node()..addComponent(InstancedMeshComponent(scatter)); +scene.add(node); +``` + +`addInstance(matrix, {color})` returns an index; edit later with `setInstanceTransform(i, m)` or move the whole batch at once through `updateInstanceTransforms((list) { ... })`. Per-instance `color` is a linear RGBA multiplier. Keep instance edits orientation-preserving, a mirrored (negative-determinant) instance edited with `updateInstanceTransforms(recomputeWinding: false)` renders inside-out. + +## Repeated pieces must not overlap + +A fence, barrier, curb, or row of tiles repeated along a line flickers at every joint when each piece is longer than its spacing (8.4 m pieces every 8 m), because neighbors share faces in one plane and fight as the camera moves (z-fighting). Alternating colors make it obvious. Size each piece to exactly its spacing, stop perpendicular runs at a corner post instead of overlapping them, and scatter flat patches at one height without overlap. An overlay that lies on a surface (paint, a sign, a decal quad) gets `material.depthLayer = 1`. `scene.findCoplanarOverlaps()` lists any overlap left, with the length that fixes a repeated piece. + +## The web noise trap + +The Dart `FastNoiseLite` relies on 32-bit integer math. On the web (dart2js) a Dart `int` is a JavaScript double, exact only to 53 bits, so the hash loses its low bits and 3D noise can overflow, producing wrong values. This is silent, you get a plausible-looking but incorrect field, and only on web. + +For web targets: + +- Prefer the **GLSL side** (`#include ` in a `.fmat` block), which is correct on every backend including WebGL2 and matches the Dart algorithms table-for-table. +- Or **bake** the field once with `bakeNoiseTexture(noise, width: ..., height: ...)` at build time or in a native isolate, then sample the texture. `bakeNoisePixels` is pure CPU with no engine imports, so it runs in a build hook or background isolate. + +Native platforms are unaffected. `noiseHash2`/`noiseHash3` are the bit-exact CPU/GPU-agreeing integer path for decisions that must never disagree (world generation, placement), but they carry the same web-overflow caveat, so make the decision once and share it rather than re-deriving it on both sides. + +## More depth + +`references/procedural.md` has the full `GeometryBuilder` and `MeshData` API (including off-isolate meshing), the complete `FastNoiseLite` config reference, natural rock, ocean, tree, sky, and island formation recipes, the instancing API in full, modular-kit assembly from the built-in primitives, and the web-noise caveat expanded. diff --git a/.claude/skills/flutter_scene-procedural/references/procedural.md b/.claude/skills/flutter_scene-procedural/references/procedural.md new file mode 100644 index 00000000..a66fe69a --- /dev/null +++ b/.claude/skills/flutter_scene-procedural/references/procedural.md @@ -0,0 +1,512 @@ +# Procedural content, the full API + +Everything for building flutter_scene content from code, custom meshes, noise, instancing, and modular kits. All symbols verified against the package source. Import geometry and instancing from the main barrel, noise from its own barrel: + +```dart +import 'package:flutter_scene/scene.dart'; +import 'package:flutter_scene/noise.dart'; +import 'package:vector_math/vector_math.dart' as vm; // NOT vector_math_64 +``` + +--- + +## GeometryBuilder + +The incremental way to build a custom triangle mesh. You add vertices one at a time, each carrying whatever attributes are currently set, then reference them by returned index to form triangles. + +```dart +class GeometryBuilder { + GeometryBuilder({bool deduplicate = true}); + + // Sticky attribute setters (return `this`, so cascade or chain them). + GeometryBuilder normal(vm.Vector3 value); + GeometryBuilder texCoord(vm.Vector2 value); + GeometryBuilder texCoord1(vm.Vector2 value); // secondary UV set + GeometryBuilder color(vm.Vector4 value); // linear RGBA + GeometryBuilder tangent(vm.Vector4 value); // xyz + handedness in w + + int addVertex(vm.Vector3 position); // returns the vertex index + GeometryBuilder addTriangle(int a, int b, int c); // throws RangeError on a bad index + + int get vertexCount; + int get triangleCount; + + Uint8List packVertices(); // pure, no GPU context needed + MeshGeometry build({ + GeometryStorage storage = GeometryStorage.fixed, + GeometryBufferArena? bufferArena, + bool retainCpuData = true, + }); +} +``` + +### The sticky-attribute model + +The attribute setters do not apply to one vertex, they set state that every following `addVertex` inherits until you change it. This makes flat-shaded faces and per-region colors natural: + +```dart +final geometry = (GeometryBuilder() + ..color(vm.Vector4(1, 0, 0, 1)) // every vertex below is red... + ..addVertex(vm.Vector3(0, 0, 0)) + ..addVertex(vm.Vector3(1, 0, 0)) + ..color(vm.Vector4(0, 1, 0, 1)) // ...until this changes it to green + ..addVertex(vm.Vector3(0, 1, 0)) + ..addTriangle(0, 1, 2)) + .build(); +``` + +### Normals, generated or authored + +If you never call `normal()`, the builder generates area-weighted vertex normals from the faces you wound. That is the right default for most procedural geometry, terrain, extrusions, anything where the surface shape defines the normal. + +**Calling `normal()` even once opts the whole mesh out of generated normals.** After that, any vertex you add without an explicit normal keeps the default `(0, 0, 1)`, which is almost never what you want. So either author a normal for every vertex, or author none and let generation run. Do not mix. + +### Deduplication + +With `deduplicate: true` (the default), `addVertex` merges a vertex equal to one already added and returns the existing index, so a shared grid corner is stored once. Pass `deduplicate: false` when you want every call to produce a distinct vertex (flat shading with per-face normals, or per-vertex data that must not collapse). + +### Winding + +flutter_scene's front faces wind **counter-clockwise in model space**, matching glTF and standard conventions. For a surface that should face +Y (a heightmap, a floor), match the built-in plane's winding: for a cell with corners `v00`(x,z), `v10`(x+1,z), `v01`(x,z+1), `v11`(x+1,z+1), emit `addTriangle(v00, v01, v10)` and `addTriangle(v10, v01, v11)`. + +If a mesh renders inside-out (invisible from the front, visible and inverted-lit from behind), reverse each triangle's index order. Because generated normals follow the winding, fixing the winding fixes the normals too. This freedom is only for geometry you author. Never apply a per-triangle winding flip to an imported model to correct its orientation, that leaves its normals and image-based lighting wrong. + +### From builder to scene + +`build()` returns a `MeshGeometry`, which is a `Geometry`. Wrap it in a `Mesh` with a material, hang the mesh on a `Node`, add the node: + +```dart +final node = Node(mesh: Mesh(geometry, PhysicallyBasedMaterial())); +scene.add(node); +``` + +--- + +## MeshGeometry.fromArrays, the bulk path + +When you already have attributes as flat arrays (a generator that fills typed lists), skip the per-vertex calls and hand `MeshGeometry.fromArrays` structure-of-arrays data directly. Same result, less overhead for large meshes. + +```dart +MeshGeometry.fromArrays({ + required Float32List positions, // 3 floats/vertex, required + Float32List? normals, // 3/vertex; omitted -> generated for triangle lists + Float32List? texCoords, // 2/vertex; omitted -> (0, 0) + Float32List? texCoords1, // 2/vertex + Float32List? colors, // 4/vertex; omitted -> opaque white + Float32List? tangents, // 4/vertex + List? indices, // omitted -> vertex count must be a multiple of 3 + gpu.PrimitiveType primitiveType = gpu.PrimitiveType.triangle, + Aabb3? bounds, // skips the position scan; MUST cover every vertex + GeometryStorage storage = GeometryStorage.fixed, + GeometryBufferArena? bufferArena, + bool retainCpuData = true, +}); +``` + +Notes that bite: + +- Every supplied optional array must match the vertex count implied by `positions`. +- Out-of-range `indices` are **not** validated here (unlike `GeometryBuilder.addTriangle`), and produce stray triangles or holes silently. Keep every index in `0..vertexCount-1`. +- A `bounds` you pass that does not enclose every vertex makes the mesh over-cull and pop out of view at some angles. Omit it (the constructor scans positions) unless you computed it correctly off-thread. +- `retainCpuData: false` drops the CPU copy after upload, saving memory, but then the mesh cannot be raycast or read back with `extractMeshData`. + +### Updatable geometry (animated meshes) + +Pass `storage: GeometryStorage.updatable` to get a mesh you can mutate in place each frame without reallocating. The in-place updaters replace one attribute when the vertex count is unchanged: + +```dart +final water = MeshGeometry.fromArrays(positions: p, storage: GeometryStorage.updatable); +// later, per frame: +water.updatePositions(newPositions); // also updateNormals/TexCoords/Colors/Tangents +// or replace everything (may change the count): +water.rebuild(positions: p2, indices: i2); +``` + +An updatable mesh fixes its indexed-or-not state at construction, if you built it with `indices`, `rebuild` requires them thereafter, and vice versa. To start empty and fill later, pass a zero-length `positions` with `updatable`. Updatable geometry must retain CPU data and cannot use a buffer arena. + +`GeometryBufferArena({int blockSizeInBytes = 16 * 1024 * 1024})` lets many fixed meshes share immutable GPU buffer blocks, worth it when you build a large number of small static meshes. It never reclaims space (an arena only grows for as long as it is alive), so give it the lifetime of the batch it serves and never feed it geometry rebuilt every frame; that is what `updatable` is for. + +--- + +## MeshData, meshing off the render isolate + +Heavy generation (remeshing a voxel chunk, a large marching-cubes surface) should not block the render isolate. `MeshData` is a pure, isolate-transferable snapshot, build it on a background isolate with `compute`, send it back, upload it there. + +```dart +factory MeshData.build({ + required Float32List positions, + Float32List? normals, // omitted -> generated for triangle lists (the win here) + Float32List? texCoords, + Float32List? texCoords1, + Float32List? colors, + Float32List? tangents, + List? indices, + gpu.PrimitiveType primitiveType = gpu.PrimitiveType.triangle, + Map customAttributes = const {}, +}); +``` + +Recipe: + +```dart +// Top-level or static, runs on the background isolate. +MeshData buildChunk(ChunkInput input) { + final positions = /* your generator */; + final indices = /* ... */; + return MeshData.build(positions: positions, indices: indices); +} + +// On the render isolate: +final data = await compute(buildChunk, input); +final geometry = MeshGeometry.fromMeshData(data); +// or, to feed an existing updatable mesh in place: +existing.applyMeshData(data); +``` + +The normal generation is the expensive part, and running it inside `MeshData.build` is exactly the work you moved off the render isolate. + +Pure derivations on a `MeshData` (all off-isolate safe): `transformed(Matrix4)` (moves positions, carries normals by the inverse transpose so a non-uniform scale stays correct, reverses winding on a mirror), `unweld({attributes})`, `extractEdges({creaseAngleDegrees})`, static `MeshData.merge(parts)`, plus `triangleCount`/`triangles`. `Geometry.extractMeshData()` reads a loaded mesh back into one. + +--- + +## FastNoiseLite + +One configurable object evaluating several noise algorithms, sampled with `getNoise2`/`getNoise3`. Output is roughly in `[-1, 1]`. + +```dart +final noise = FastNoiseLite(seed: 1337) + ..frequency = 0.01 // coords are multiplied by this before eval + ..noiseType = NoiseType.openSimplex2 + ..fractalType = FractalType.fbm + ..octaves = 5; + +final h = noise.getNoise2(x, z); // 2D +final d = noise.getNoise3(x, y, z); // 3D +``` + +### Config reference + +| Field | Default | Meaning | +| --- | --- | --- | +| `seed` | 1337 | Seed for every noise type. | +| `frequency` | 0.01 | Input coordinates are scaled by this. Bigger = finer features. | +| `noiseType` | `openSimplex2` | Base algorithm (see below). | +| `fractalType` | `none` | How octaves layer (see below). | +| `octaves` | 3 | Number of fractal layers. More detail, more cost. | +| `lacunarity` | 2.0 | Frequency multiplier between octaves. | +| `gain` | 0.5 | Amplitude multiplier between octaves. | +| `weightedStrength` | 0.0 | Biases octave amplitude toward stronger detail. | +| `pingPongStrength` | 2.0 | Warp strength for `FractalType.pingPong`. | +| `cellularDistanceFunction` | `euclideanSq` | Distance metric for `NoiseType.cellular`. | +| `cellularReturnType` | `distance` | What cellular returns. | +| `cellularJitterModifier` | 1.0 | Cell-point jitter; above 1 causes artifacts. | +| `domainWarpType` | `openSimplex2` | Warp algorithm for `domainWarp2`/`domainWarp3`. | +| `domainWarpAmp` | 1.0 | Max warp distance. | +| `domainWarpFractalType` | `none` | Octave layering for domain warp. | + +Enums: + +- `NoiseType` = `openSimplex2` | `openSimplex2S` | `cellular` | `perlin` | `value`. +- `FractalType` = `none` | `fbm` (classic layered fractal, the usual terrain choice) | `ridged` (sharp ridges, mountains) | `pingPong`. +- `CellularDistanceFunction` = `euclidean` | `euclideanSq` | `manhattan` | `hybrid`. +- `CellularReturnType` = `cellValue` | `distance` | `distance2` | `distance2Add` | `distance2Sub` | `distance2Mul` | `distance2Div`. +- `DomainWarpType` = `openSimplex2` | `openSimplex2Reduced` | `basicGrid`. +- `DomainWarpFractalType` = `none` | `progressive` | `independent`. + +### Domain warp + +`domainWarp2`/`domainWarp3` distort the input coordinates before sampling, breaking up the regular look of raw fractal noise. The reference version mutates in place, this port returns the warped position for you to feed back in: + +```dart +final w = noise.domainWarp2(x, z); // ({double x, double y}) +final v = noise.getNoise2(w.x, w.y); +``` + +### Curl noise + +`noiseCurl3(x, y, z, {int seed = 1337, double epsilon = 0.25})` returns a divergence-free 3D vector `({x, y, z})` from a seeded potential field, for advecting particles so they swirl without clumping. Coordinates are taken pre-scaled (no frequency parameter), matching the GLSL `NoiseCurl3`. Advect by adding `curl * speed * dt`. A smaller `epsilon` sharpens the field and amplifies CPU/GPU divergence. + +### Baking noise to a texture + +Sampling many octaves per fragment is expensive. When the field is static, bake it once and sample the texture instead: + +```dart +Texture2D bakeNoiseTexture( + FastNoiseLite noise, { + required int width, + required int height, + double originX = 0.0, + double originY = 0.0, + double cellSize = 1.0, + TextureSampling sampling = const TextureSampling(), +}); +``` + +It bakes `getNoise2` over a `width` x `height` grid into a grayscale `Texture2D` (content is linear `data`, so mipmaps average cleanly) ready to bind as a material sampler. It must run where GPU resources are created (the raster thread). The CPU half, `bakeNoisePixels(noise, {width, height, originX, originY, cellSize})`, returns `Uint8List` RGBA and has no engine imports, so it runs in a build hook or a background isolate, then `Texture2D.fromPixels` uploads the result. + +--- + +### Natural formations: rocks, cliffs, trails, and scatter recipes + +Composing raw noise into realistic natural terrain and geology requires specific math patterns to avoid telltale procedural artifacts. + +### 1. Free-end Worley rock cracks (avoiding closed cell loops) + +Standard cellular Worley distance (`F2 - F1`) creates a continuous polygon network like bathroom tile or dry mud. To create natural weathering cracks with free ends, mask the cell borders with a low-frequency macro patch and a high-frequency grain breaker: + +```glsl +// GLSL shader bake or .fmat surface +// Cellular Worley noise returning F2 - F1 distance +float cwl = NoiseCellular2(p * 3.0, 1337, kNoiseCellularEuclidean, kNoiseCellularDistance2Sub, 1.0); +float net = smoothstep(0.08, -0.80, cwl); +float region = smoothstep(-0.2, 0.4, NoiseFbm2(p * 1.0, 1338, 3, 2.0, 0.5)); +float breaker = smoothstep(-0.4, 0.2, NoiseFbm2(p * 6.0, 1339, 2, 2.0, 0.5)); +float crack = net * region * breaker; // produces isolated segments with natural start/end points +``` + +### 2. Noise-modulated pitting (avoiding regular dot lattices) + +Thresholding Worley noise at a constant radius places a pit in every single cell, creating an artificial grid lattice. Modulate the threshold radius with an underlying Perlin field so pores vary in size and only appear in exposed weathering pockets: + +```glsl +float sizeVar = (NoiseFbm2(p * 2.5, 1337, 3, 2.0, 0.5) + 1.0) * 0.5; +float pw = NoiseCellular2(p * 5.0, 1338, kNoiseCellularEuclidean, kNoiseCellularDistance, 1.0); +float pit = smoothstep(0.05 + 0.24 * sizeVar * sizeVar, 0.005, pw) + * smoothstep(-0.1, 0.5, NoiseFbm2(p * 1.5, 1339, 3, 2.0, 0.5)); +``` + +### 3. Incised trail heightfields (scours vs flat stripes) + +Footpaths are formed by water and foot traffic compressing and eroding soil downwards. Sample the path polyline once (`trail.sample(n, evenlySpaced: true)`) and compute the minimum point-to-segment distance to cut the path profile into the terrain heightfield with raised spoil banks: + +```dart +double computeTerrainHeight(double x, double z, FastNoiseLite noise, List trailPoints) { + final baseHeight = noise.getNoise2(x, z) * 8.0; + + // Find minimum distance from (x, z) to the sampled 2D path segments + var minDist = double.infinity; + final p = vm.Vector2(x, z); + for (var i = 0; i < trailPoints.length - 1; i++) { + final a = vm.Vector2(trailPoints[i].x, trailPoints[i].z); + final b = vm.Vector2(trailPoints[i + 1].x, trailPoints[i + 1].z); + final ab = b - a; + final t = ((p - a).dot(ab) / ab.length2).clamp(0.0, 1.0); + final dist = (p - (a + ab * t)).length; + if (dist < minDist) minDist = dist; + } + + const pathWidth = 1.8; + const pathDepth = 0.45; + const bermHeight = 0.25; + + // Carve central path trough + final trench = (1.0 - (minDist / pathWidth).clamp(0.0, 1.0)) * pathDepth; + // Build gentle spoil berm along the verge + final verge = ((minDist - pathWidth * 0.8) / (pathWidth * 0.8)).clamp(0.0, 1.0); + final berm = math.sin(verge * math.pi) * bermHeight; + + return baseHeight - trench + berm; +} +``` + +### 4. Macro-massed pebble scatter (avoiding uniform sandpaper noise) + +Gravel and pebbles cluster into water-washed scour lines rather than spreading evenly over an entire level. Gate multi-scale pebble instances with a low-frequency macro massing field and key the hash off integer cell coordinates: + +```dart +void scatterPebbles(InstancedMesh finePebbles, InstancedMesh largeStones, FastNoiseLite terrainNoise) { + final macroNoise = FastNoiseLite(seed: 42)..frequency = 0.05; + const step = 0.8; + const cells = 80; // 64m / 0.8m + for (var ix = 0; ix < cells; ix++) { + for (var iz = 0; iz < cells; iz++) { + final x = ix * step; + final z = iz * step; + // Deterministic coordinate jitter keyed off integer cell indices + final h = noiseHash2(1337, ix, iz); + final jx = x + ((h & 0xFF) / 255.0 - 0.5) * 0.6; + final jz = z + (((h >> 8) & 0xFF) / 255.0 - 0.5) * 0.6; + + final mass = (macroNoise.getNoise2(jx, jz) + 1.0) * 0.5; + if (mass > 0.65) { + final y = terrainNoise.getNoise2(jx, jz) * 8.0; + final matrix = vm.Matrix4.translation(vm.Vector3(jx, y, jz)); + if (((h >> 16) & 0xFF) > 180) { + largeStones.addInstance(matrix); + } else { + finePebbles.addInstance(matrix); + } + } + } + } +} +``` + +### 5. Oceans and Gerstner waves + +Trochoidal Gerstner waves pull vertices horizontally toward wave peaks, creating sharp crests and wide flat troughs. Sum multiple directional waves and compute normals analytically: + +```glsl +// GLSL Gerstner wave displacement +struct Wave { vec2 dir; float amp; float freq; float speed; float steepness; }; + +// Caller seeds accumulators with tangent = vec3(1.0, 0.0, 0.0) and binormal = vec3(0.0, 0.0, 1.0). +vec3 evaluateGerstner(vec2 pos, float time, Wave w, float numWaves, inout vec3 tangent, inout vec3 binormal) { + vec2 d = normalize(w.dir); + float phase = dot(d, pos) * w.freq + time * w.speed; + float c = cos(phase); + float s = sin(phase); + float q = w.steepness / (w.amp * w.freq * numWaves); + + tangent += vec3(-q * d.x * d.x * w.amp * w.freq * s, + d.x * w.amp * w.freq * c, + -q * d.x * d.y * w.amp * w.freq * s); + binormal += vec3(-q * d.x * d.y * w.amp * w.freq * s, + d.y * w.amp * w.freq * c, + -q * d.y * d.y * w.amp * w.freq * s); + + return vec3(q * w.amp * d.x * c, + w.amp * s, + q * w.amp * d.y * c); +} +``` + +For shallow water transitions and shorelines: +- **Beer-Lambert Depth Extinction**: Declare `engine_inputs: [ depth ]` in the `.fmat` to sample linear opaque scene depth (`RenderInput.depth`). Compute water depth `d = sceneDepth - surfaceDepth` and attenuate color with `C = C_deep + (C_shallow - C_deep) * exp(-sigma_a * d)`. +- **Tidal Wet Sand**: Reduce sand roughness to 0.15 and multiply albedo by 0.6 within the wave wash zone to produce glistening wet shorelines. + +### 6. Trees, branching splines, and backlit foliage + +Trunk and branch structures follow Leonardo da Vinci's rule: total cross-sectional area is conserved across splits (d_parent^2 = sum d_child^2). Extrude branches along swept spline tubes using `TubeGeometry` (sweeping a round cross-section along a `ScenePath`) or `ExtrudeGeometry`: + +- **Backlit Leaf Translucency**: Set `Material.doubleSided = true` for two-sided rendering. In a custom leaf shader, add a diffuse transmission term so backlit foliage glows rather than rendering as a dark silhouette: +```glsl +// In custom leaf shader +float NdotL = dot(normal, lightDir); +float backLight = max(0.0, -NdotL) * leafTransmissionFactor; +vec3 litColor = albedo * (max(0.0, NdotL) + backLight * leafTranslucentColor); +``` +- **Quadratic Cantilever Wind**: Displace leaf and branch vertices in world space proportional to height squared (delta_p = windVec * (h / h_max)^2 * sin(omega * t - k * p)) so tips sway vigorously while roots remain anchored. + +### 7. Procedural skies and runtime IBL synchronization + +Use `PhysicalSkySource` (`lib/src/sky_sources.dart`) with analytic Rayleigh and Mie scattering. Assign `SkyEnvironment` to `Scene.skyEnvironment` or call `EnvironmentMap.fromSky` to bake prefiltered radiance and SH-9 diffuse coefficients into the scene's IBL automatically, and assign the source to `Scene.skybox` for matching background visuals. + +### 8. Islands, coastal bays, and sand dunes + +To form natural island topographies: +- **Domain-Warped Island Mask**: Multiply a radial distance falloff (1.0 - (r / R)^2) with domain-warped FBM to form organic bays, sandbars, and peninsulas rather than symmetrical circular cones. +- **Slope-Based Sediment Stripping**: Compute heightfield slope sqrt((dh/dx)^2 + (dh/dz)^2). Steep cliffs strip topsoil to expose rock strata, while gentle coastal planes accumulate golden beach sand. +- **Anisotropic Wind Dune Ripples**: Layer 8:1 anisotropically stretched noise perpendicular to the prevailing wind direction to generate fine ripple crests across sand surfaces. + +--- + +## The web noise caveat, expanded + +The Dart `FastNoiseLite` port relies on 32-bit integer arithmetic. On native platforms this is exact. On the web (dart2js), a Dart `int` is a JavaScript double, exact only to 53 bits, so the integer hash loses its low bits and 3D noise can overflow. The result is a plausible-looking but wrong field, silent, and web-only. A web-safe integer multiply for the Dart side is a planned follow-up. + +The GLSL half of the module is unaffected, it is correct on every backend including WebGL2, and implements the same algorithms with the same tables and seeds, so a field sampled on the CPU (native) and evaluated in a shader agree. The agreement has two tiers: + +- **Bit-exact**: `noiseHash2`/`noiseHash3` (and GLSL `NoiseHash2`/`NoiseHash3`) are pure integer math and match bit for bit across backends. Use them for decisions that must never disagree between machines (world generation, deterministic placement). +- **Float-close**: the float noise functions match within a small tolerance (float32 rounding differs per GPU), imperceptible visually. Do not re-derive a hard threshold from float noise on both the CPU and GPU sides, make the decision once and share the result. + +Both tiers carry the web-overflow caveat on the Dart side. Strategy by target: + +- **Native only**: use the Dart `FastNoiseLite` freely, on the render isolate or a background one. +- **Web, per-fragment noise**: move it to the GLSL side (`#include ` in a `.fmat` block). +- **Web, a static field**: bake it with `bakeNoiseTexture` (or `bakeNoisePixels` in a build hook / native isolate) and sample the texture. This sidesteps the overflow because the baking happens where `int` is 64-bit. + +--- + +## InstancedMesh, thousands of copies for one draw + +One geometry/material pair drawn many times, each placed by its own model transform. The whole set is one render item, one pipeline, one cull test. This is how you scatter foliage, crowds, debris, or a grid of the same prop without a node per copy. + +```dart +class InstancedMesh { + InstancedMesh({ + required Geometry geometry, + required Material material, + bool cullInstances = false, // per-instance cull after the aggregate pass + bool sortTransparentInstances = true, + }); + + int get instanceCount; + + int addInstance(vm.Matrix4 transform, {vm.Vector4? color}); // matrix is CLONED; returns index + void setInstanceTransform(int index, vm.Matrix4 transform); + void updateInstanceTransforms( + void Function(List transforms) update, { + bool recomputeWinding = true, + }); + void setInstanceColor(int index, vm.Vector4 color); // linear RGBA multiplier + void removeInstanceAt(int index); // shifts later indices down + void clearInstances(); +} +``` + +Attach it to a node with an `InstancedMeshComponent` (it does not go on `Node(mesh:)`): + +```dart +final mesh = InstancedMesh(geometry: geo, material: mat); +for (final placement in placements) { + mesh.addInstance(placement); // a Matrix4 in the instanced mesh's local space +} +final node = Node()..addComponent(InstancedMeshComponent(mesh)); +scene.add(node); +``` + +Practical notes: + +- `addInstance` clones the matrix, so reusing one scratch `Matrix4` across the loop is fine. +- The node the component is on transforms the entire batch. Instance transforms compose under it. +- To animate all instances cheaply, use `updateInstanceTransforms`, which invalidates the batch once instead of per call. Mutate the matrices in the callback list; do not add, remove, or replace entries. +- `updateInstanceTransforms(recomputeWinding: false)` skips the parity refresh. Only pass it when no edit changes a transform's winding. A mirrored (negative-determinant) edit under it renders those instances inside-out. +- `cullInstances: true` pays for per-instance culling, worth it for a large spatial spread whose instances enter view at different times; leave it off for a small compact clump that the single aggregate cull already handles. +- Set `cullInstances` per instanced mesh based on that trade; it is not a global. + +--- + +## Modular kits from the built-in primitives + +Before authoring a mesh, remember the ten primitives assemble a surprising amount by composition, no builder needed. Each is a `Geometry`, so each goes on its own `Node`, and a parent node groups a kit piece you can clone and place. + +| Class | Constructor | Notes | +| --- | --- | --- | +| `CuboidGeometry` | `CuboidGeometry(vm.Vector3 extents)` | Box from `-extents/2` to `+extents/2`. Positional. | +| `SphereGeometry` | `SphereGeometry({radius = 0.5, segments = 32, rings = 16})` | UV sphere. | +| `IcosphereGeometry` | `IcosphereGeometry({radius = 0.5, subdivisions = 2})` | Even triangle distribution. | +| `CylinderGeometry` | `CylinderGeometry({bottomRadius = 0.5, topRadius = 0.5, height = 1.0, ...})` | `topRadius: 0` makes a cone; different radii make a frustum. | +| `CapsuleGeometry` | `CapsuleGeometry({radius = 0.5, height = 1.0, ...})` | `height` is the mid-section; total Y is `height + 2*radius`. | +| `TorusGeometry` | `TorusGeometry({radius = 0.5, tubeRadius = 0.2, ...})` | Lies in XZ. | +| `PlaneGeometry` | `PlaneGeometry({width = 1.0, depth = 1.0, segmentsX = 1, segmentsZ = 1})` | XZ plane, faces +Y. | +| `DiscGeometry` | `DiscGeometry({radius = 0.5, segments = 32})` | Filled circle, XZ, faces +Y. | +| `RingGeometry` | `RingGeometry({innerRadius = 0.25, outerRadius = 0.5, segments = 32})` | Annulus, XZ, +Y. | +| `WedgeGeometry` | `WedgeGeometry(vm.Vector3 size)` | Triangular prism; base on `y = 0` (not Y-centered). | + +Because a cone is just `CylinderGeometry(topRadius: 0)`, a tree is a green cone on a brown cylinder, a fence is repeated thin cuboids, a table is a plane on four cylinders. Assemble each piece as a parented `Node` subtree, then `clone()` and place it, or feed the placements to an `InstancedMesh` when the same piece repeats many times. + +### Kit pieces must abut, never overlap + +Two faces of different pieces in one plane flicker as the camera moves (z-fighting), at any distance and on any device, so a kit that overlaps its pieces looks broken in motion even when a still looks right. + +- **Size a repeated piece to its spacing.** Fence rails, barriers, and curbs placed every `s` meters are `s` meters long. A piece 0.4 m longer than its spacing puts 0.4 m of coplanar faces at every joint. +- **Cap joints and corners with a post** a centimeter or two larger than the pieces on every face, and stop perpendicular runs at the post instead of running both into the corner. +- **Make stripes from color, not overlap.** Alternate per-instance `color` on abutting pieces, or paint stripes with vertex colors, rather than laying a strip of one color over another. +- **Scatter flat pieces without overlap.** Patches, tiles, and rugs at one height must not overlap each other; reject a candidate whose footprint overlaps one already placed, or give each kind its own `Material.depthLayer`. +- **Wrap with a clear gap or not at all.** A band or trim box wrapped around a larger box sits a clear distance proud (tens of centimeters for content seen from hundreds of meters), and two wraps on one piece use separate height slots. Facade detail seen from far away (windows on a skyline) belongs in the material rather than in thousands of proud boxes. +- **Overlays get a layer.** Paint, signs, and decal quads that lie on a surface get `material.depthLayer = 1`. + +`scene.findCoplanarOverlaps()` lists every overlap left, with the length that fixes a repeated piece, and debug builds print a summary once the scene holds still. + +Every primitive except `PlaneGeometry` exposes a `Shape get collisionShape` for the physics package, so a code-built kit gets colliders for free. + +### Swept geometry for shapes primitives cannot make + +For paths, tubes, and profiles, sweep a `ScenePath` (`BezierPath`, `CatmullRomPath`, `PolylinePath`): + +- `TubeGeometry(path, {radius = 0.5, radialSegments = 12, stations = 64, caps = true})` for pipes, cables, vines. +- `ExtrudeGeometry(path, {required List profile, stations = 64, caps = true})` sweeps a 2D profile along the path (railings, moldings, extruded logos). +- `RibbonGeometry(path, {width = 1.0, stations = 64, alignment = RibbonAlignment.ground})` for flat strips (roads, trails). + +These build detailed shapes from a curve and a few parameters, often replacing an imported model outright. diff --git a/.claude/skills/flutter_scene-verification-loop/SKILL.md b/.claude/skills/flutter_scene-verification-loop/SKILL.md new file mode 100644 index 00000000..7c640d8c --- /dev/null +++ b/.claude/skills/flutter_scene-verification-loop/SKILL.md @@ -0,0 +1,72 @@ +--- +name: flutter_scene-verification-loop +version: 4 +description: Close the visual-iteration loop when building or debugging a flutter_scene 3D app so you see your own output and self-correct. Use whenever a change affects what renders (geometry, materials, lighting, shaders, post-processing) or a frame looks wrong (black, washed-out, see-through, missing geometry). +user-invocable: false +--- + +# Verifying flutter_scene visually + +flutter_scene renders 3D. A rendering change you cannot see is a guess, and guessing at pixels is the single biggest waste of iterations. The highest-value habit is a closed visual loop plus judgment that does not drift. This skill is that loop. + +**The one thing to internalize: run it, let it settle, look at the frame AND the console, localize before you edit, repeat.** Do not change code off a hypothesis you did not confirm from the actual output. + +## The loop + +1. **Run.** Launch the app with `flutter run --enable-flutter-gpu` (native; the flag is mandatory and it is the only run flag, see the idioms skill). Where the editor MCP is connected, `run_project` launches the managed session instead. +2. **Settle.** A live frame is a moving target. Auto-exposure is still ramping, particles have a random phase, an animation is mid-clip, IBL re-bakes after the first present. Let the scene reach steady state (a few frames, or until the image stops changing) before you trust a capture. Do not screenshot the first frame and reason from it. +3. **Capture the frame AND read the console.** A screenshot alone hides errors that print; the console alone hides wrong pixels. Take both every time. Baseline path is a screenshot plus the run log; with the editor MCP it is `screenshot_viewport` plus `get_console`. +4. **Localize before editing.** When something is wrong, find where it goes wrong before you touch code. Read an intermediate buffer, read a single pixel's exact value, or scan for non-finite values. A NaN or Inf propagates silently into black or garbage downstream, so the first pass that produced it is the culprit, not the pass where you see the black. `references/loop.md` has the tool table and a symptom to action map. +5. **Correct, repeat.** Make one change, run the loop again. One change per iteration keeps cause and effect legible. + +## Flicker needs motion, not a screenshot + +Two surfaces that overlap in one plane trade pixels as the camera moves (z-fighting), and any single frame can look right. Wherever surfaces meet or lie on each other (paint on a road, a screen on a wall, barriers along a track, overlapping tiles), prove the scene clean with a number: `await scene.probeDepthConflicts(camera: camera)` from each camera the app uses, driving `report.conflicts` to empty (each entry names the two nodes, the pixels they trade, and the distance). `scene.debug.overlays.add(DebugOverlay.depthConflicts)` shows the same live in magenta, and the editor MCP's `scan_for_depth_conflicts` runs the probe for you. Without either, compare two frames a few centimetres of camera motion apart. + +## The readiness gate (do not debug through it) + +Rendering is gated on `Scene.initializeStaticResources()`. Until that Future completes, every frame is skipped and the engine prints exactly: + +``` +Flutter Scene is not ready to render. Skipping frame. +``` + +If you see that line, the scene is not broken, it is not ready. Wait for readiness (build geometry and materials inside `initializeStaticResources().then(...)`, gate the widget on `Scene.isReadyToRender`) before you diagnose anything else. A black frame while that line prints is the gate, not your code. + +A second, quieter gate: the shaders behind `PhysicallyBasedMaterial` extensions (clearcoat, sheen, transmission, and the rest) and `ShadowCatcherMaterial` load on first use, and the scene holds its previous frame (or draws nothing, before its first) until they land. Await `Scene.preload()` before a capture so it shows the scene as it now is. + +## Judge blind, never self-score (the load-bearing rule) + +When deciding whether a change improved the look, **do not assign the frame a quality score.** Self-assigned scores drift upward, because the model is grading its own trajectory and wants to have made progress. That drift is how a session convinces itself a regression is an improvement. + +Instead, **compare two frames and return a binary pick.** Put the new frame next to a reference (a known-good target) or the previous frame, and answer only "which of these two is better", A or B. No number, no "8/10", no "looks pretty good now". A blind pairwise pick does not inflate the way a solo score does. This applies to every visual review, including the ones that feel obvious. + +If you have no reference at all, say so and describe the concrete difference between the two frames (this one is brighter here, that one has an artifact there) rather than inventing a score. + +## Empirical verification rules + +These rules prevent false diagnoses, hollow passes, and measurement traps during visual iteration: + +1. **Liveness before ablation.** A negative result is evidence only if the removed or modified term was actually live in the draw pass. Diff pixels for liveness before trusting an ablation. +2. **Tools must fail loud on empty measurements.** A tool that measures an empty population, zero pixels, or non-finite data must fail loud rather than returning a default passing number. +3. **Population discipline.** Always quote the population window, crop rectangle, brightness threshold, and rendering resolution beside any color or lighting figure. +4. **Baselines expire quickly.** Two captures taken hours apart in a changing tree reflect multiple edits; isolate paired A/B captures with temporary snapshots or git worktrees. +5. **Look at the raw frame before quoting numbers.** Inspect the actual captured frame before taking numbers off it; metrics can yield valid-looking numbers on corrupt frames. +6. **Negative control requirement.** A metric that returns the same score for positive and negative control populations cannot serve as evidence for either. +7. **Explanations are hypotheses, not evidence.** Before adopting an explanatory mechanism, identify the specific numerical observation that would differ if the mechanism were false. +8. **Attribution must reach the triangle.** When diagnosing geometry defects, trace down to the specific triangle indices and edge lengths rather than stopping at the mesh component. +9. **Symmetric domain clamping.** Clamping parametric lookup domains must be verified at both boundaries, and derived slope or heading accessors must clamp both sample points. +10. **Multi-band octave tables for tiling.** Two-band ratios like `hf/lf` are blind to regular mid-frequency patterns; inspect multi-octave energy tables to detect periodic tiling. +11. **Physical discrimination over naming.** A class-discriminating physical observation (such as missing shadow terminators) outranks code comments or variable names. +12. **Grazing light terminator crossing.** Under low-angle grazing lighting, prioritize terminator-crossing fractions over slope RMS to detect normal map over-amplification. +13. **Resolution scaling.** High-frequency energy (`hf/lf`) and relative contrast scale with resolution; compare them only at equal pixel resolutions. +14. **Paired capture isolation.** Snapshot source files or freeze environment state when capturing before/after pairs so background modifications cannot corrupt the comparison. + +## Be honest about tooling + +The richest observation tooling lives behind the editor MCP (`flutter_scene_mcp`): screenshots, console, NaN scans, render-graph capture, per-pass and per-pixel readback, viewport debug modes. A general-purpose observation server for an arbitrary running game is still being built, so do not assume those tools exist for every project. When the MCP is not connected, the baseline loop is still fully usable: `flutter run --enable-flutter-gpu`, read the console, take a screenshot. Do not claim a capability you cannot reach in the current project. + +## More depth + +- `references/loop.md` for the full editor-MCP tool table, the symptom to action map (black frame, washed-out, see-through, missing geometry), and the settle details. +- The `flutter_scene-idioms` skill (`references/traps.md`) for the underlying mistakes each symptom points back to (wrong vertex layout, hand-rolled winding flip, transform-in-place, the blank-frame causes). diff --git a/.claude/skills/flutter_scene-verification-loop/references/loop.md b/.claude/skills/flutter_scene-verification-loop/references/loop.md new file mode 100644 index 00000000..0ed9ae6d --- /dev/null +++ b/.claude/skills/flutter_scene-verification-loop/references/loop.md @@ -0,0 +1,161 @@ +# The verification loop in detail + +The core loop, the readiness gate, and the blind-judgment rule are in `SKILL.md`. This file has the +tool table (what exists where), the settle details, and the symptom to action map. + +--- + +## Two tooling tiers + +### Baseline (any project, no MCP) + +This always works and needs nothing installed beyond the package setup. + +- Launch: `flutter run --enable-flutter-gpu` (native; add `-d chrome` for web). The flag is mandatory. +- Console: read the run log. The readiness line, `debugPrint` output, asserts, and the 0.22.0 + blank-frame diagnostic all land here. +- Frame: take a screenshot of the running app after it settles. + +That is the whole loop when there is no editor. Run, settle, screenshot, read the log, correct. + +### Editor MCP (`flutter_scene_mcp`, when connected) + +The editor exposes richer observation. Tool names below are exact. Do not assume they exist unless +the MCP is actually connected for the current project. + +| Tool | What it does | Reach for it when | +| --- | --- | --- | +| `run_project` | Launch the editor-managed Play session (a managed `flutter run`). | Starting a session under the editor. | +| `build_project` | Start the selected build config; output streams to the console. | You want a build without launching. | +| `stop_project` | Stop the running session. | Ending or restarting cleanly. | +| `hot_reload` | Hot reload the running debug session. | A Dart-only change, fastest turnaround. | +| `hot_restart` | Hot restart the session. | State or startup changed, or reload did not take. | +| `get_console` | The build/run console tail plus building/running flags. | EVERY iteration, paired with a screenshot. | +| `screenshot_viewport` | The viewport as a PNG, what the user sees. | EVERY iteration, paired with the console. | +| `describe_scene` | The scene-graph tree (ids, paths, names, component types). | Confirming a node/mesh is actually in the scene. | +| `scan_for_nans` | Capture a frame and scan every float render target for NaN/Inf in pass order. | A black or garbage frame with no error. Find where non-finite values start. | +| `scan_for_depth_conflicts` | Probe the viewport camera for surfaces that trade pixels as the camera moves (z-fighting) and list the node pairs, pixel counts, and distances. | Stripes, speckles, or a surface that looks right only from some angles. | +| `capture_render_graph` | Capture the next frame's graph with thumbnails. | You need to see intermediate buffers. | +| `list_render_passes` | The executed passes in order with CPU timings and the buffer keys each read/wrote, plus target formats and sizes. No images. | Learning which pass owns which buffer, and the key names to read. | +| `get_pass_output` | Render one captured buffer (a key like `scene_color`, `linear_depth`) as a PNG. NaN paints magenta, Inf yellow, negative blue. | Eyeballing an intermediate buffer to see which stage broke. | +| `read_pass_pixel` | One pixel's exact float RGBA from a captured buffer, with NaN/Inf flags. | Confirming an exact value (is this really 0, or NaN, or negative). | +| `list_viewport_debug_modes` | The available debug outputs (final, HDR color, linear depth, normals, AO, shadow atlas, ...) and which is active. | Seeing what debug views exist. | +| `set_viewport_debug_mode` | Render one debug output full-viewport. Set `final` to restore. | Inspecting depth/normals/AO live, paired with `screenshot_viewport`. | + +Render-graph capture (`capture_render_graph`, `list_render_passes`, `get_pass_output`, +`read_pass_pixel`, `scan_for_nans`) is gated on `Scene.debugAllowRenderGraphCapture`. It is a debug +opt-in, so a release build or a scene that never armed it returns nothing. The editor arms it for you; +outside the editor, set `Scene.debugAllowRenderGraphCapture = true` and call +`Scene.captureRenderGraph(...)` directly. + +--- + +## Settle, do not seed + +Frames differ from run to run for benign reasons. That is normal, not a bug to eliminate. + +- **Auto-exposure** (`Scene.autoExposure`) ramps toward the target over `speedUp`/`speedDown` seconds, + so the first second is darker or brighter than the settled image. +- **Particles and trails** carry a random phase, so a `ParticleSystem` looks different every launch. +- **Animations** are mid-clip unless you seek them, so a screenshot lands on an arbitrary frame. +- **Image-based lighting** re-bakes after the first present on some paths, so reflections dim in for + a frame before they are correct. + +So let the scene settle before you trust a capture. Watch until the image stops changing, or advance +a fixed few frames, then screenshot. Judge the settled frame, not the first one. + +**Seeding is a different job.** Strict determinism (a fixed random seed, a pinned animation time, a +frozen exposure) is what you set up for pixel-exact regression comparison, where two runs must be +byte-identical. You do not need it for ordinary observation. For "does this change look right", settle +and look. Reserve the seeding work for when you are building a golden or diffing two runs at the pixel +level. + +--- + +## Symptom to action map + +Localize before editing. Each row says what to capture first and the mistakes it usually points back +to. The mistakes are detailed in the `flutter_scene-idioms` skill's `references/traps.md`; this map +routes a symptom to the right one. + +### Entirely black frame + +1. Read the console FIRST. If `Flutter Scene is not ready to render. Skipping frame.` is printing, it + is the readiness gate, not your scene. Wait for `Scene.initializeStaticResources()`. Stop here. +2. In 0.22.0 a frame that issues zero draws prints once in debug naming the likely cause (not ready, + empty region, no views, no visible meshes, or a layer mask matching nothing). Read that line. +3. If draws are happening but the image is black, `scan_for_nans`. A NaN or Inf anywhere upstream + collapses the final image to black, and the scan names the first offending pass. Then + `get_pass_output` on that pass's buffer (NaN shows magenta) to confirm. +4. Common non-NaN causes: a degenerate camera (target equals position, `up` parallel to the view + direction on a top-down camera, FOV passed in degrees not radians), `layerMask: 0`, an oversized + environment texture that failed to allocate on the device. See traps #24 and #17. + +### Washed-out, low-contrast, or too-bright color + +1. `screenshot_viewport` after settling, and check whether auto-exposure has finished ramping (a + too-bright first second is just the ramp). +2. If it persists, suspect a shader-output contract break. A custom `ShaderMaterial`/`PostEffect`/sky + shader must output linear HDR premultiplied by alpha. Tone-mapping or gamma-encoding in the shader + gets applied a second time by the resolve pass, giving exactly this washed-out look. See traps #38 + and the root `MATERIALS.md`. +3. Also check for a non-color texture bound as color (a normal or metallic-roughness map without the + right `TextureContent`), which reads wrong and distance-dependent. Trap #3. +4. Hand-packed vertex data at the wrong stride also washes out color (the color attribute lands at the + wrong offset). `describe_scene` plus trap #18. + +### See-through or inside-out faces + +1. `set_viewport_debug_mode` to normals (or `get_pass_output` on the normals buffer) and look at the + orientation. Inverted normals confirm a winding problem. +2. Cause is almost always clockwise hand-built triangles. flutter_scene front faces wind + COUNTER-CLOCKWISE (CCW) in model space, matching glTF and standard conventions. Ensure triangle + indices wind CCW around the outward face normal, or omit normals and let the constructor derive + them. NEVER fix orientation with a per-triangle winding flip on an imported model; that leaves + normals and IBL wrong. Traps #14 and #18. +3. For an imported model rendered mirrored, check you did not overwrite the runtime importer's + `scale(1, 1, -1)` handedness root. Trap #6. + +### Missing or popping geometry + +1. `describe_scene` to confirm the node is actually in the graph. If it is absent, it is a scene-build + bug, not a render bug. +2. If it is present but invisible, check the layer mask (`Node.layers` is a bitmask, NOT inherited, + and must match the view's `layerMask`; `layers = 2` means `1 << 1`, not "layer 2"). Trap #11. +3. If it appears and disappears with camera angle, the bounds do not cover the geometry (a + caller-supplied `bounds` or `setLocalBounds` that is too small, or a swapped primitive geometry on + an older version). Widen or omit the bounds. Traps #9 and #25. +4. A moved skinned mesh that will not move is the skinned-node transform being ignored by design; move + the skeleton root instead. Trap #5. + +### Flickering stripes or speckles where surfaces meet + +1. Do not judge this from one frame. Run `scan_for_depth_conflicts` (editor MCP) or `await + scene.probeDepthConflicts(camera: camera)` in the app; each conflict names two nodes, the pixels + they trade, and the distance. `scene.debug.overlays.add(DebugOverlay.depthConflicts)` marks them + live in magenta. +2. Two faces overlapping in one plane (repeated pieces longer than their spacing, overlapping patches + at one height, a duplicated node) fight at any distance; fix the content. `scene.findCoplanarOverlaps()` + lists the overlapping faces with the length that fixes a repeated piece. +3. An overlay meant to lie on a surface (a screen, a sign, road paint) gets `material.depthLayer = 1`. +4. Faces a few centimetres apart that fight only far away need a larger gap or a layer. The + `depthGap` debug view shows the gap each pixel needs. See the idioms skill's + `references/depth-and-layering.md`. +5. Re-probe until the conflict list is empty. + +### A value looks numerically wrong (not visually) + +Use `read_pass_pixel` on the relevant buffer to read the exact float RGBA at a coordinate, with NaN/Inf +flags. This settles "is this pixel actually 0.5, or is it NaN, or negative" without eyeballing a PNG +that the display remap has already clamped. + +--- + +## Judgment, restated + +The blind-pairwise rule from `SKILL.md` is the part most likely to be skipped, so it bears repeating +here. When you have a before and an after, put them side by side and pick the better one as a binary +A-or-B choice against a reference or the previous frame. Do not narrate a score. A solo score climbs +on its own because you are grading your own progress; a blind pick between two concrete frames does +not. Every visual review runs through a pairwise pick, including the ones that feel too obvious to +bother with. diff --git a/.claude/skills/serverpod-auth/references/setup.md b/.claude/skills/serverpod-auth/references/setup.md index afd721af..bf6d5c8e 100644 --- a/.claude/skills/serverpod-auth/references/setup.md +++ b/.claude/skills/serverpod-auth/references/setup.md @@ -45,6 +45,18 @@ client = Client(serverUrl) client.auth.initialize(); ``` +## Passwordless email + +To sign in with a one-time password sent by email, extend `EmailOtpIdpBaseEndpoint` instead of `EmailIdpBaseEndpoint` on the server. `SignInWidget` then shows `EmailOtpSignInWidget`, which can also be used on its own. + +The codes are sent through `EmailIdpConfig.sendOtpLoginVerificationCode`, falling back to `sendRegistrationVerificationCode` when it is not set. Set the OTP callback if the registration emails use `accountRequestId`, e.g. in links. + +The code length is not sent to the client, so configure it on both sides. The server generates 6 digit codes by default (`EmailIdpConfig.otpLoginVerificationCodeGenerator`), and the widget's code input defaults to 6 digits too (`EmailOtpSignInWidget(otpVerificationCodeConfig: VerificationCodeConfig(length: ...))`). Change both together. + +Customize the texts with `SignInLocalizationProvider(emailWithOtp: EmailWithOtpSignInTexts.defaults.copyWith(...))`. + +Customize the resend-code text shared by both email flows with `SignInLocalizationProvider(verificationCodeTexts: VerificationCodeTexts.defaults.copyWith(resendCode: ...))`. + ## Social sign-ins Each provider needs the user to configure it outside the project (e.g. the GCP console or the Apple developer portal). Follow the official setup guide: diff --git a/.claude/skills/serverpod-configuration/SKILL.md b/.claude/skills/serverpod-configuration/SKILL.md index ef09b870..0c72f313 100644 --- a/.claude/skills/serverpod-configuration/SKILL.md +++ b/.claude/skills/serverpod-configuration/SKILL.md @@ -22,7 +22,9 @@ apiServer: publicScheme: http ``` -`publicHost`/`publicPort`/`publicScheme` are used for URLs returned to clients. +`port` is the port the listener binds. `publicHost`/`publicPort`/`publicScheme` are used for URLs returned to clients; a `publicPort` of 0 follows the bound port. + +In development the Serverpod runner keeps the configured ports unless another runner on the machine holds them, in which case it binds ephemeral ports (through `SERVERPOD_API_SERVER_PORT`, its siblings and `SERVERPOD_DATABASE_PORT`; the `_PUBLIC_PORT` variables of the api, insights and web listeners carry the same value, so URLs returned to clients use the bound port) and publishes the real addresses in `serverpod runner status`. See [Several servers at once](../serverpod-overview/SKILL.md#several-servers-at-once) in the overview skill. ## Database @@ -41,9 +43,9 @@ database: Password in `passwords.yaml` under run mode or `SERVERPOD_PASSWORD_database`. Optional: `searchPaths`, `maxConnectionCount`, `requireSsl`, `isUnixSocket`. -**Embedded PostgreSQL:** with `dataPath` set, Serverpod launches and manages a PostgreSQL instance in that directory, so nothing has to be installed or run separately. New projects use it for development and testing. `serverpod database start [--mode ] [--port ]` boots the same database on its own, without the server. +**Embedded PostgreSQL:** with `dataPath` set, Serverpod launches and manages a PostgreSQL instance in that directory, so nothing has to be installed or run separately. New projects use it for development and testing. It listens on its Unix socket, plus loopback TCP on `port` when the run mode has a database password. In development the runner moves that port aside with the server ports when another Serverpod runner holds it, so several checkouts can run their own at once. `serverpod database start [--mode ] [--port ]` boots the same database on its own, without the server, or joins the one the server already runs. -Without `dataPath` the database is external, and `serverpod start` starts Docker Compose services when the project has a compose file and PostgreSQL is on localhost (`--docker` / `--no-docker` override this). +Without `dataPath` the database is external, and the runner (`serverpod start`) starts Docker Compose services when the project has a compose file and PostgreSQL is on localhost (`--docker` / `--no-docker` override this). ### SQLite diff --git a/.claude/skills/serverpod-endpoints/SKILL.md b/.claude/skills/serverpod-endpoints/SKILL.md index db2168f5..b66c38a1 100644 --- a/.claude/skills/serverpod-endpoints/SKILL.md +++ b/.claude/skills/serverpod-endpoints/SKILL.md @@ -6,7 +6,7 @@ user-invocable: false # Serverpod Endpoints -Extend `Endpoint` with instance methods; first parameter is `Session`, return `Future` (or `Stream` for real-time data streaming). Place anywhere under server `lib/`. If `serverpod start` is not running with hot-reload, run `serverpod generate` to update the client. +Extend `Endpoint` with instance methods; first parameter is `Session`, return `Future` (or `Stream` for real-time data streaming). Place anywhere under server `lib/`. The Serverpod runner regenerates the client on save; without one, run `serverpod generate`. ## Defining an endpoint diff --git a/.claude/skills/serverpod-migrations/SKILL.md b/.claude/skills/serverpod-migrations/SKILL.md index 6301922f..ee4fdcbc 100644 --- a/.claude/skills/serverpod-migrations/SKILL.md +++ b/.claude/skills/serverpod-migrations/SKILL.md @@ -8,7 +8,7 @@ user-invocable: false Serverpod has a migration system that generates SQL for changes to models with `table` in `.spy.yaml`. The migrations are applied: -- When the `apply_migrations` tool is called via the `serverpod` MCP. Typically during development with a running `serverpod start`. +- When the `apply_migrations` tool is called via the `serverpod` MCP. Typically during development with the Serverpod runner running (`serverpod start` or `serverpod runner start`). - When the server is started with `dart run bin/main.dart --apply-migrations` flag. Typically when running the server in production. ## When migrations are needed @@ -21,20 +21,20 @@ Migrations are not needed when the project has no database configured (e.g. `con ## Standard flow -The standard flow for creating and applying migrations is simplified when a `serverpod start` is running. +The standard flow for creating and applying migrations is simplified when the runner is running. -### With a running `serverpod start` and MCP server +### With a running runner and MCP server -When the server is running from `serverpod start` use the `serverpod` MCP to: +When the server is running under the runner use the `serverpod` MCP to: 1. Create a migration using the `create_migration` tool. 2. Apply the migration using the `apply_migrations` tool. -ALWAYS use the MCP server if it is available. +ALWAYS use the MCP server if it is available. When it reports that the server is not running, call `tail_runner_logs`, then run `serverpod runner start` in the server package when no runner is running, then call the tools again. -### ONLY if MCP server fails to connect +### ONLY if the MCP is not available or the runner cannot bring the server up -When the server is not running from `serverpod start` use the CLI commands to: +When the MCP is not available or `serverpod runner start` fails, use the CLI commands to: 1. Ensure the code is generated by running `serverpod generate`. 2. Create a migration using the `serverpod create-migration` command. @@ -61,14 +61,14 @@ On the client side (models with `database: client` or `database: all`), the same If the database is in an inconsistent state, a repair migration brings it back to a consistent state. It is created by reading the live schema and diffing it against a target migration version, so the database must be reachable. -### With a running `serverpod start` and MCP server +### With a running runner and MCP server 1. Create the repair migration using the `create_repair_migration` tool. Optional arguments: `version` (target migration version, defaults to the latest), `tag`, and `force` (required for destructive changes, or when no drift is detected). 2. Apply it using the `apply_migrations` tool, which applies both pending and repair migrations without restarting the server. -ALWAYS use the MCP server if it is available. +ALWAYS use the MCP server if it is available. When it reports that the server is not running, call `tail_runner_logs`, then run `serverpod runner start` when no runner is running, then call the tools again. -### ONLY if MCP server fails to connect +### ONLY if the MCP is not available or the runner cannot bring the server up ```bash # Use the `--mode` flag to specify the run mode diff --git a/.claude/skills/serverpod-models/SKILL.md b/.claude/skills/serverpod-models/SKILL.md index b111ad2a..792131c8 100644 --- a/.claude/skills/serverpod-models/SKILL.md +++ b/.claude/skills/serverpod-models/SKILL.md @@ -8,7 +8,7 @@ user-invocable: false Models are defined in `.spy.yaml` files anywhere under server `lib/`. They generate Dart classes for server and client, and optionally database tables. -After each change to models, ensure that the code is generated (automatically when a `serverpod start` is running or manually with `serverpod generate`). If models with `table` have changed, the database schema must be updated following the [migration workflow](../serverpod-migrations/SKILL.md). +After each change to models, ensure that the code is generated (automatically when the Serverpod runner is running or manually with `serverpod generate`). If models with `table` have changed, the database schema must be updated following the [migration workflow](../serverpod-migrations/SKILL.md). ## Basic class diff --git a/.claude/skills/serverpod-overview/SKILL.md b/.claude/skills/serverpod-overview/SKILL.md index d3096515..e2516d27 100644 --- a/.claude/skills/serverpod-overview/SKILL.md +++ b/.claude/skills/serverpod-overview/SKILL.md @@ -24,27 +24,52 @@ Each of these feature areas has its own `serverpod-*` skill; use the one that ma ## Running the server -The user runs the server with `serverpod start`, which watches for file changes to run incremental code generation and hot reload the server (and the Flutter app, when the project has one). +The development stack is run by the Serverpod runner, one background process per server package. It generates code, compiles the server with hot reload, launches the companion Flutter apps and serves the `serverpod` MCP. While it runs it watches for file changes to run incremental code generation and hot reload the server (and the Flutter app, when the project has one). -Do not check whether the server is running before acting: make the changes and call the `serverpod` MCP tools. When nothing is running, the MCP answers with an explicit "the server is not running" error. Only then, stop and ask the user to start it. NEVER start the server yourself. +- `serverpod start` ensures a runner is running, spawning one when none is, and attaches to it: the interactive terminal UI on a terminal, a plain log stream otherwise or with `--no-tui`. It blocks until the user leaves. Leaving a `start` that spawned the runner stops it; leaving one that attached to a runner already running keeps it running. This is what the user runs. `serverpod start --no-attach` behaves as `serverpod runner start`. +- `serverpod runner start` ensures a runner is running the same way, prints its addresses and returns, leaving it running in the background. This is how you start it. A cold start compiles the whole project and can take minutes. Companion Flutter apps declared with `auto_launch` launch once a UI attaches; launch any app before that with the MCP tool `spawn_flutter_app`. +- `serverpod runner attach` attaches a UI to a runner already running, the same way `serverpod start` does, and never spawns one. Leaving it keeps the runner running. +- `serverpod runner status` prints whether the runner is starting, running, stopping or up with a project that failed to build, the API, Insights and web URLs and the log path. +- `serverpod runner stop` stops the runner. -ALWAYS use the MCP server instead of the command line. A running `serverpod start` exposes: +All of them resolve the server package from the current directory, or take `--directory ` (`-d`). + +Do not check whether the server is running before acting: make the changes and call the `serverpod` MCP tools. When nothing is running, the MCP answers with an explicit "the server is not running" error. Then: + +1. Call `tail_runner_logs` to learn whether a runner is still starting or why it stopped. +2. If none is running, run `serverpod runner start` in the server package. It returns zero once the server is up. It returns non-zero when the project does not build (the runner stays up and, with watch on, the default, starts the server once a change makes it build), when a configured port is held by something other than a Serverpod runner, or when Docker refused. Fix the cause instead of retrying. The one exception is a `serverpod runner start` your shell killed on a timeout before it returned: the runner keeps starting in the background, and running the command again waits for it. +3. Call the MCP tool again. The MCP reconnects by itself. + +Leave the runner running when you are done; the user attaches to it with `serverpod start`. Run `serverpod runner stop` only when asked. Starting the stack with `--watch`, `--target`, `--flutter`, `--docker` or server arguments that differ from the running runner's fails and names the difference; stop that runner first. + +ALWAYS use the MCP server instead of the command line for the tasks below. The `serverpod runner` commands are the exception: they manage the stack itself. A running runner exposes: - `create_migration` and `apply_migrations` for the database (after you change data models). - `create_repair_migration` when the database has drifted out of sync with the migrations. It only writes the repair file; follow up with `apply_migrations`. - `tail_server_logs` to read logs from the server. +- `tail_runner_logs` to read the output of the runner itself (code generation, compilation, Docker, raw server output). The only tool that also works while the server is starting or after it stopped. - `tail_flutter_logs` to read raw stdout/stderr from a Flutter app. -- `hot_reload` to reload the server and the Flutter app while keeping in-memory state. Only needed with `--no-watch`, since `serverpod start` reloads on file changes. +- `hot_reload` to reload the server and the Flutter app while keeping in-memory state. Only needed with `--no-watch`, since the runner reloads on file changes. - `hot_restart` to restart the server and the Flutter app, dropping in-memory state. ALWAYS call it after doing changes in the Flutter app that may not work with normal hot reload (which is automatically applied). - `spawn_flutter_app` to start a Flutter app declared under `serverpod: flutter_apps:` in the server `pubspec.yaml`. - `get_flutter_app_dtd` to get the Dart Tooling Daemon URI of a running Flutter app. Pass it to the `dart` MCP to drive the app. Tools that target a Flutter app take an optional `appId`, which is the map key under `serverpod: flutter_apps:`. It is required only when the project declares more than one app. +## Several servers at once + +Each server package has exactly one runner, keyed by the package directory: its manifest, lock, log and sockets live in `/.dart_tool/serverpod/`, and every runner registers itself under `~/.serverpod/runners/`. So a workspace with several server packages, and several checkouts or worktrees of the same project, each run their own runner. Start each with `serverpod runner start --directory `. An MCP bridge serves one server package, so the agent configuration needs one `serverpod mcp-server --server-dir ` entry per server. The bridge serves the package its `--server-dir` names, not the directory you are in, and says which in its instructions and errors. Check that before applying migrations or restarting from another checkout or worktree. + +The packages usually share a `config/development.yaml` with the same ports (8080, 8081, 8082). The first runner binds them. A runner that finds them claimed by another runner moves all of its listeners to ephemeral ports together, and keeps those ports across its own restarts while they stay free. Never assume `localhost:8080`: read the addresses from `serverpod runner status` or from what `serverpod runner start` printed. Flutter apps the runner launches receive the right URL through `--dart-define=SERVER_URL=...`; an app started by hand, a browser tab or `curl` must use the address from `status`. + +A configured port held by anything other than a Serverpod runner, such as a server started by hand with `dart run bin/main.dart`, is an error: stop that process or change the port in `config/development.yaml`. + +The embedded PostgreSQL (`database.dataPath`) is per checkout. It listens on its Unix socket, and on loopback TCP at `database.port` when the run mode has a database password. That port is a fourth listener: it moves aside with the server ports, and `status` prints the database address. A database this checkout already runs, started by `serverpod database start`, is joined rather than moved. Only a project that uses the Docker Compose database shares host ports, and the compose project is named after the server directory, so two checkouts of such a project would share one set of containers. Start a checkout that uses this shared Compose stack only when no other checkout is using it. Otherwise, leave the existing stack running and do not start this checkout's runner unless the user explicitly asks to switch checkouts. + ## Working on the project with no running instance -- NEVER use the CLI unless you have already attempted to use the MCP. -- ONLY if you cannot connect to the MCP server, the code can be generated by calling `serverpod generate`. +- NEVER run `serverpod generate` or `serverpod create-migration` while the MCP is available: the runner generates code on save and the MCP creates migrations. The `serverpod runner` commands manage the stack itself and are always allowed. +- ONLY when the MCP is not available or `serverpod runner start` could not bring the server up, generate the code by calling `serverpod generate`. - NEVER edit the generated code, as it will be overwritten by the next generation. This does not cover the SQL of a created migration, which may be edited — see the [Serverpod Migrations](../serverpod-migrations/SKILL.md) skill. After generating the code, database migrations can be created by calling `serverpod create-migration`. Use ONLY if you cannot use the MCP. diff --git a/.claude/skills/serverpod-upgrading/SKILL.md b/.claude/skills/serverpod-upgrading/SKILL.md index 8bf70176..e2da687e 100644 --- a/.claude/skills/serverpod-upgrading/SKILL.md +++ b/.claude/skills/serverpod-upgrading/SKILL.md @@ -8,7 +8,7 @@ user-invocable: false Requirements for this Serverpod version: Dart 3.12.2+, Flutter 3.44.4+. Check the release notes for the version being installed. -Use the same pinned Serverpod version across all packages. Use the CLI to do the upgrade. Ask the user to start the server with `serverpod start` after the upgrade. NEVER update the CLI tooling, instead STOP and ask the user to do it. +Use the same pinned Serverpod version across all packages. Use the CLI to do the upgrade. Restart the runner after the upgrade: `serverpod runner stop`, then `serverpod runner start`. NEVER update the CLI tooling, instead STOP and ask the user to do it. 1. Check the latest version of Serverpod: https://pub.dev/packages/serverpod (unless the user has requested a specific version). 2. Run `serverpod version` to verify that the tooling has the correct version. If not, STOP and ask the user to install the correct version (`dart install serverpod_cli` for latest or `dart install serverpod_cli 3.x.x` for specific version). @@ -18,7 +18,7 @@ Use the same pinned Serverpod version across all packages. Use the CLI to do the 6. Run `serverpod create-migration`. 7. Run `dart analyze` in the root of the project and address any issues. 8. Ensure that the Dockerfile uses at least `FROM dart:3.12.2 AS build`. -9. Inform the user that the upgrade is complete and they should start the server with `serverpod start`. +9. Restart the runner with `serverpod runner stop` and `serverpod runner start` in the server package, and inform the user that the upgrade is complete. ## Major upgrade: Serverpod 3.x to 4.0 diff --git a/.claude/skills/shad-overview/SKILL.md b/.claude/skills/shad-overview/SKILL.md new file mode 100644 index 00000000..8aa4d36e --- /dev/null +++ b/.claude/skills/shad-overview/SKILL.md @@ -0,0 +1,156 @@ +--- +name: shad-overview +description: A Flutter UI library that ports shadcn/ui's components, styles, and theming. Provides accessible, fully customizable widgets (Button, Card, Form, Sidebar, and more) plus theming, layout, and typography guides. Use this skill when building a Flutter UI, implementing a design system, or looking up how to use a specific `shad` component. +user-invocable: false +--- + +# Shad: shadcn/ui for Flutter + +`shad` ports shadcn/ui's components and design system to Flutter. Every widget is a plain, fully customizable Flutter widget, themed by one `ShadThemeData`. + +Import everything through one file: + +```dart +import 'package:shad/shad.dart'; +``` + +The least obvious part of the API: use `ShadPadding` instead of Flutter's `Padding` inside a `shad`-themed page. It has no plain constructor, only `ShadPadding.all`, `.symmetric`, `.only`, and `.directional`, and their arguments are steps on the theme's spacing scale, not logical pixels — `ShadPadding.symmetric(horizontal: 6, vertical: 4)` is shadcn's `px-6 py-4`, not 6px/4px. See [layout.md](guides/layout.md#padding) for a full example. + +## Guides + +- [Installation](guides/installation.md): add the package, set up `ShadApp`, use a component. +- [Theming](guides/theming.md): color schemes, component theme overrides, radius, focus rings, Material/Cupertino interop. +- [Styles](guides/styles.md): the eight shadcn geometry presets (`vega`, `nova`, `maia`, `lyra`, `mira`, `luma`, `sera`, `rhea`). +- [Typography](guides/typography.md): the text role scale and how to change the font. +- [Layout](guides/layout.md): `ShadRow`, `ShadColumn`, `ShadGap`, `ShadPadding`. +- [Form](guides/form.md): `ShadForm` and the `*FormField` widgets. +- [Responsive](guides/responsive.md): the breakpoint scale, `ShadResponsiveBuilder`, `context.breakpoint`. +- [Portal](guides/portal.md): the overlay primitive behind popovers, selects, and menus. + +## Components + +| Name | Description | Reference | +| :--- | :--- | :--- | +| Accordion | A vertically stacked set of interactive headings that each reveal a section of content. | [accordion.md](components/accordion.md) | +| Alert | Displays a callout for user attention. | [alert.md](components/alert.md) | +| Avatar | An image element with a fallback for representing the user. | [avatar.md](components/avatar.md) | +| Badge | Displays a badge or a component that looks like a badge. | [badge.md](components/badge.md) | +| Breadcrumb | Displays the path to the current resource using a hierarchy of links. | [breadcrumb.md](components/breadcrumb.md) | +| Button | Displays a button or a component that looks like a button. Six variants share one API. | [button.md](components/button.md) | +| Calendar | A component that allows users to select dates: single, multiple, or a range. | [calendar.md](components/calendar.md) | +| Card | Displays a card with title, description, content, and footer. | [card.md](components/card.md) | +| Checkbox | A control that allows the user to toggle between checked and not checked. | [checkbox.md](components/checkbox.md) | +| Collapsible | An interactive component that expands and collapses a panel. | [collapsible.md](components/collapsible.md) | +| Command | A composable command menu with filtering and full keyboard navigation. | [command.md](components/command.md) | +| Context Menu | Displays a menu at the pointer, triggered by a right click or a long press. | [context-menu.md](components/context-menu.md) | +| Data Table | A table with sorting, selection, filtering, and pagination, driven by a controller. | [data-table.md](components/data-table.md) | +| Date Picker | A date picker with the calendar in a popover. | [date-picker.md](components/date-picker.md) | +| Dialog | A modal window overlaid on the page, rendering the content underneath inert. | [dialog.md](components/dialog.md) | +| Empty | A placeholder for an empty state: icon, title, description, and actions. | [empty.md](components/empty.md) | +| Icon Button | A square button holding a single icon. | [icon-button.md](components/icon-button.md) | +| Input | Displays a form input field. | [input.md](components/input.md) | +| Input OTP | A one-time password input with copy-paste support. | [input-otp.md](components/input-otp.md) | +| Kbd | Displays a keyboard key or chord. | [kbd.md](components/kbd.md) | +| Keyboard Toolbar | A toolbar shown above the software keyboard on mobile, with focus navigation and a done button. | [keyboard-toolbar.md](components/keyboard-toolbar.md) | +| Menubar | A visually persistent menu, common in desktop applications. | [menubar.md](components/menubar.md) | +| Pagination | Page navigation with previous and next links, plus a compact variant for toolbars and table footers. | [pagination.md](components/pagination.md) | +| Popover | Displays rich content in a portal, triggered by a button. | [popover.md](components/popover.md) | +| Progress | An indicator that shows how much of a task is complete. | [progress.md](components/progress.md) | +| Radio Group | A set of checkable buttons where only one can be checked at a time. | [radio-group.md](components/radio-group.md) | +| Resizable | Resizable panel groups and layouts, with nesting and drag handles. | [resizable.md](components/resizable.md) | +| Select | Displays a list of options for the user to pick from, triggered by a button. | [select.md](components/select.md) | +| Separator | Visually or semantically separates content. | [separator.md](components/separator.md) | +| Sheet | Extends the dialog to display content that complements the main content of the screen, sliding in from any edge. | [sheet.md](components/sheet.md) | +| Sidebar | A composable, collapsible side navigation panel: icon rail, off-canvas, floating, or inset, with a mobile sheet fallback. | [sidebar.md](components/sidebar.md) | +| Skeleton | A placeholder to show while content is loading. | [skeleton.md](components/skeleton.md) | +| Slider | An input where the user selects a value from within a given range. | [slider.md](components/slider.md) | +| Sonner | An opinionated toast stack: notifications collect and expand on hover. | [sonner.md](components/sonner.md) | +| Spinner | An animated loading indicator. | [spinner.md](components/spinner.md) | +| Switch | A control that allows the user to toggle between checked and not checked. | [switch.md](components/switch.md) | +| Table | A responsive table component with header and footer rows. | [table.md](components/table.md) | +| Tabs | A set of layered sections of content displayed one at a time. | [tabs.md](components/tabs.md) | +| Textarea | A multi-line text input, optionally user-resizable. | [textarea.md](components/textarea.md) | +| Time Picker | A field for entering a time of day. | [time-picker.md](components/time-picker.md) | +| Toast | A succinct message that is displayed temporarily. | [toast.md](components/toast.md) | +| Toggle | A two-state button that can be either on or off. | [toggle.md](components/toggle.md) | +| Toggle Group | A set of two-state buttons that can be toggled on or off. | [toggle-group.md](components/toggle-group.md) | +| Tooltip | A popup that displays information related to an element on hover or focus. | [tooltip.md](components/tooltip.md) | + +Each component page shows the actual example source bundled with the example app, so it cannot drift from the code that runs. + +## Basic setup + +A complete counter app with light and dark theme support: + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return ShadApp( + theme: ShadThemeData( + brightness: Brightness.light, + colorScheme: const ShadZincColorScheme.light(), + ), + darkTheme: ShadThemeData( + brightness: Brightness.dark, + colorScheme: const ShadZincColorScheme.dark(), + ), + themeMode: ThemeMode.system, + home: const CounterPage(), + ); + } +} + +class CounterPage extends StatefulWidget { + const CounterPage({super.key}); + + @override + State createState() => _CounterPageState(); +} + +class _CounterPageState extends State { + int _counter = 0; + + @override + Widget build(BuildContext context) { + final theme = ShadTheme.of(context); + return Scaffold( + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'You have pushed the button this many times:', + style: theme.textTheme.muted, + ), + Text('$_counter', style: theme.textTheme.h1), + const SizedBox(height: 16), + ShadButton( + onPressed: () => setState(() => _counter++), + child: const Icon(LucideIcons.plus), + ), + ], + ), + ), + ); + } +} +``` + +## Packages re-exported by shad + +`shad`'s public API (`package:shad/shad.dart`) re-exports the parts of a few libraries that its own component signatures depend on, so callers do not need a second dependency just to pass an argument `shad` asks for. + +- [flutter_animate](https://pub.dev/packages/flutter_animate): the `effects:` parameter on `ShadPopover`, `ShadSelect`, `ShadAccordion`, `ShadMenubar`, and similar components takes a `List` (aliased as `AnimateEffect`). +- [lucide_icons_flutter](https://pub.dev/packages/lucide_icons_flutter): the icon set used throughout the library and its examples, exposed as `LucideIcons.*`. Browse the icons [here](https://lucide.dev/icons/). +- [two_dimensional_scrollables](https://pub.dev/packages/two_dimensional_scrollables): `ShadTable` builds on `TableView`, so `ShadTableCell` extends its `TableViewCell`. +- [intl](https://pub.dev/packages/intl): `DateFormat` and `NumberFormat` are used by the calendar, date picker, and pagination widgets. diff --git a/.claude/skills/shad-overview/components/accordion.md b/.claude/skills/shad-overview/components/accordion.md new file mode 100644 index 00000000..69d835ca --- /dev/null +++ b/.claude/skills/shad-overview/components/accordion.md @@ -0,0 +1,91 @@ +# Accordion + +A vertically stacked set of interactive headings that each reveal a section of content. + +## Single + +Opening a section closes the previous one. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +const _items = [ + ( + title: 'Is it accessible?', + content: 'Yes. It adheres to the WAI-ARIA design pattern.', + ), + ( + title: 'Is it styled?', + content: + 'Yes. It comes with default styles that match the other ' + "components' aesthetic.", + ), + ( + title: 'Is it animated?', + content: + "Yes. It's animated by default, but you can disable it if " + 'you prefer.', + ), +]; + +class AccordionSingleExample extends StatelessWidget { + const AccordionSingleExample({super.key}); + + @override + Widget build(BuildContext context) { + return ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 600), + child: ShadAccordion<({String content, String title})>( + children: [ + for (final item in _items) + ShadAccordionItem( + value: item, + title: Text(item.title), + child: Text(item.content), + ), + ], + ), + ); + } +} +``` + +## Multiple + +Sections open and close independently. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +const _items = [ + (title: 'First section', content: 'Multiple sections can be open at once.'), + ( + title: 'Second section', + content: 'Opening this one does not close the first.', + ), +]; + +class AccordionMultipleExample extends StatelessWidget { + const AccordionMultipleExample({super.key}); + + @override + Widget build(BuildContext context) { + return ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 600), + child: ShadAccordion<({String content, String title})>.multiple( + children: [ + for (final item in _items) + ShadAccordionItem( + value: item, + title: Text(item.title), + child: Text(item.content), + ), + ], + ), + ); + } +} +``` + diff --git a/.claude/skills/shad-overview/components/alert.md b/.claude/skills/shad-overview/components/alert.md new file mode 100644 index 00000000..0593e303 --- /dev/null +++ b/.claude/skills/shad-overview/components/alert.md @@ -0,0 +1,78 @@ +# Alert + +Displays a callout for user attention. + +## Default + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class AlertDefaultExample extends StatelessWidget { + const AlertDefaultExample({super.key}); + + @override + Widget build(BuildContext context) { + return ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 576), + child: const Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + spacing: 16, + children: [ + ShadAlert( + icon: Icon(LucideIcons.circleCheck), + title: Text('Success! Your changes have been saved'), + description: Text( + 'This is an alert with icon, title and description.', + ), + ), + ShadAlert( + icon: Icon(LucideIcons.popcorn), + title: Text('This Alert has a title and an icon. No description.'), + ), + ShadAlert.destructive( + icon: Icon(LucideIcons.circleAlert), + title: Text('Unable to process your payment.'), + description: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 4, + children: [ + Text('Please verify your billing information and try again.'), + Text('• Check your card details'), + Text('• Ensure sufficient funds'), + Text('• Verify billing address'), + ], + ), + ), + ], + ), + ); + } +} +``` + +## Destructive + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class AlertDestructiveExample extends StatelessWidget { + const AlertDestructiveExample({super.key}); + + @override + Widget build(BuildContext context) { + return ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 576), + child: const ShadAlert.destructive( + icon: Icon(LucideIcons.circleAlert), + title: Text('Error'), + description: Text('Your session has expired. Please log in again.'), + ), + ); + } +} +``` + diff --git a/.claude/skills/shad-overview/components/avatar.md b/.claude/skills/shad-overview/components/avatar.md new file mode 100644 index 00000000..41b22137 --- /dev/null +++ b/.claude/skills/shad-overview/components/avatar.md @@ -0,0 +1,33 @@ +# Avatar + +An image element with a fallback for representing the user. + +## Default + +The placeholder shows while the image loads, or when it fails. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class AvatarDefaultExample extends StatelessWidget { + const AvatarDefaultExample({super.key}); + + @override + Widget build(BuildContext context) { + return const Row( + mainAxisSize: MainAxisSize.min, + spacing: 8, + children: [ + ShadAvatar( + 'https://avatars.githubusercontent.com/u/124599?v=4', + placeholder: Text('CN'), + ), + // The placeholder shows while loading, or if the image fails. + ShadAvatar('', placeholder: Text('AB')), + ], + ); + } +} +``` + diff --git a/.claude/skills/shad-overview/components/badge.md b/.claude/skills/shad-overview/components/badge.md new file mode 100644 index 00000000..5be6961f --- /dev/null +++ b/.claude/skills/shad-overview/components/badge.md @@ -0,0 +1,29 @@ +# Badge + +Displays a badge or a component that looks like a badge. + +## Variants + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class BadgeVariantsExample extends StatelessWidget { + const BadgeVariantsExample({super.key}); + + @override + Widget build(BuildContext context) { + return const Wrap( + spacing: 8, + runSpacing: 8, + children: [ + ShadBadge(child: Text('Primary')), + ShadBadge.secondary(child: Text('Secondary')), + ShadBadge.destructive(child: Text('Destructive')), + ShadBadge.outline(child: Text('Outline')), + ], + ); + } +} +``` + diff --git a/.claude/skills/shad-overview/components/breadcrumb.md b/.claude/skills/shad-overview/components/breadcrumb.md new file mode 100644 index 00000000..5bbd7ead --- /dev/null +++ b/.claude/skills/shad-overview/components/breadcrumb.md @@ -0,0 +1,64 @@ +# Breadcrumb + +Displays the path to the current resource using a hierarchy of links. + +## Default + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class BreadcrumbBasicExample extends StatelessWidget { + const BreadcrumbBasicExample({super.key}); + + @override + Widget build(BuildContext context) { + return ShadBreadcrumb( + children: [ + ShadBreadcrumbLink( + onPressed: () {}, + child: const Text('Home'), + ), + ShadBreadcrumbLink( + onPressed: () {}, + child: const Text('Components'), + ), + const Text('Breadcrumb'), + ], + ); + } +} +``` + +## Collapsed + +An ellipsis stands in for the middle of a deep hierarchy. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class BreadcrumbEllipsisExample extends StatelessWidget { + const BreadcrumbEllipsisExample({super.key}); + + @override + Widget build(BuildContext context) { + return ShadBreadcrumb( + children: [ + ShadBreadcrumbLink( + onPressed: () {}, + child: const Text('Home'), + ), + // Collapses the middle of a deep hierarchy. + const ShadBreadcrumbEllipsis(), + ShadBreadcrumbLink( + onPressed: () {}, + child: const Text('Components'), + ), + const Text('Breadcrumb'), + ], + ); + } +} +``` + diff --git a/.claude/skills/shad-overview/components/button.md b/.claude/skills/shad-overview/components/button.md new file mode 100644 index 00000000..1ab1feb6 --- /dev/null +++ b/.claude/skills/shad-overview/components/button.md @@ -0,0 +1,145 @@ +# Button + +Displays a button or a component that looks like a button. Six variants share one API: pick the one matching the action's weight. + +## Variants + +Primary for the main action, secondary and outline for everything else, destructive for irreversible actions, ghost for toolbars, and link for inline navigation. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class ButtonVariantsExample extends StatelessWidget { + const ButtonVariantsExample({super.key}); + + @override + Widget build(BuildContext context) { + return Wrap( + spacing: 8, + runSpacing: 8, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + ShadButton( + onPressed: () {}, + child: const Text('Primary'), + ), + ShadButton.secondary( + onPressed: () {}, + child: const Text('Secondary'), + ), + ShadButton.destructive( + onPressed: () {}, + child: const Text('Destructive'), + ), + ShadButton.outline( + onPressed: () {}, + child: const Text('Outline'), + ), + ShadButton.ghost( + onPressed: () {}, + child: const Text('Ghost'), + ), + ShadButton.link( + onPressed: () {}, + child: const Text('Link'), + ), + ], + ); + } +} +``` + +## Sizes + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class ButtonSizesExample extends StatelessWidget { + const ButtonSizesExample({super.key}); + + @override + Widget build(BuildContext context) { + return Wrap( + spacing: 8, + runSpacing: 8, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + ShadButton.outline( + size: ShadButtonSize.sm, + onPressed: () {}, + child: const Text('Small'), + ), + ShadButton.outline( + onPressed: () {}, + child: const Text('Regular'), + ), + ShadButton.outline( + size: ShadButtonSize.lg, + onPressed: () {}, + child: const Text('Large'), + ), + ], + ); + } +} +``` + +## With icon + +Use leading and trailing to place icons around the label. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class ButtonIconExample extends StatelessWidget { + const ButtonIconExample({super.key}); + + @override + Widget build(BuildContext context) { + return Wrap( + spacing: 8, + runSpacing: 8, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + ShadButton( + onPressed: () {}, + leading: const Icon(LucideIcons.mail), + child: const Text('Login with Email'), + ), + ShadButton.outline( + onPressed: () {}, + trailing: const Icon(LucideIcons.chevronRight), + child: const Text('Next'), + ), + ], + ); + } +} +``` + +## Loading + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class ButtonLoadingExample extends StatelessWidget { + const ButtonLoadingExample({super.key}); + + @override + Widget build(BuildContext context) { + return ShadButton( + enabled: false, + leading: const SizedBox.square( + dimension: 16, + child: ShadSpinner(), + ), + child: const Text('Please wait'), + ); + } +} +``` + diff --git a/.claude/skills/shad-overview/components/calendar.md b/.claude/skills/shad-overview/components/calendar.md new file mode 100644 index 00000000..61d64fae --- /dev/null +++ b/.claude/skills/shad-overview/components/calendar.md @@ -0,0 +1,51 @@ +# Calendar + +A component that allows users to select dates: single, multiple, or a range. + +## Single + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class CalendarSingleExample extends StatefulWidget { + const CalendarSingleExample({super.key}); + + @override + State createState() => _CalendarSingleExampleState(); +} + +class _CalendarSingleExampleState extends State { + DateTime? selected = DateTime.now(); + + @override + Widget build(BuildContext context) { + return ShadCalendar( + selected: selected, + onChanged: (date) => setState(() => selected = date), + ); + } +} +``` + +## Range + +Constrain the selection with min and max lengths. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class CalendarRangeExample extends StatelessWidget { + const CalendarRangeExample({super.key}); + + @override + Widget build(BuildContext context) { + return ShadCalendar.range( + min: 2, + max: 7, + ); + } +} +``` + diff --git a/.claude/skills/shad-overview/components/card.md b/.claude/skills/shad-overview/components/card.md new file mode 100644 index 00000000..7177c0f7 --- /dev/null +++ b/.claude/skills/shad-overview/components/card.md @@ -0,0 +1,66 @@ +# Card + +Displays a card with title, description, content, and footer. + +## With a form + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +const _frameworks = { + 'next': 'Next.js', + 'react': 'React', + 'astro': 'Astro', + 'nuxt': 'Nuxt.js', +}; + +class CardProjectExample extends StatelessWidget { + const CardProjectExample({super.key}); + + @override + Widget build(BuildContext context) { + return ShadCard( + width: 350, + title: const Text('Create project'), + description: const Text('Deploy your new project in one-click.'), + footer: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + ShadButton.outline( + onPressed: () {}, + child: const Text('Cancel'), + ), + ShadButton( + onPressed: () {}, + child: const Text('Deploy'), + ), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const Text('Name'), + const SizedBox(height: 6), + const ShadInput(placeholder: Text('Name of your project')), + const SizedBox(height: 16), + const Text('Framework'), + const SizedBox(height: 6), + ShadSelect( + placeholder: const Text('Select'), + options: [ + for (final entry in _frameworks.entries) + ShadOption(value: entry.key, child: Text(entry.value)), + ], + selectedOptionBuilder: (context, value) => + Text(_frameworks[value]!), + onChanged: (value) {}, + ), + ], + ), + ); + } +} +``` + diff --git a/.claude/skills/shad-overview/components/checkbox.md b/.claude/skills/shad-overview/components/checkbox.md new file mode 100644 index 00000000..c00151a0 --- /dev/null +++ b/.claude/skills/shad-overview/components/checkbox.md @@ -0,0 +1,55 @@ +# Checkbox + +A control that allows the user to toggle between checked and not checked. + +## With label + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class CheckboxBasicExample extends StatefulWidget { + const CheckboxBasicExample({super.key}); + + @override + State createState() => _CheckboxBasicExampleState(); +} + +class _CheckboxBasicExampleState extends State { + bool value = false; + + @override + Widget build(BuildContext context) { + return ShadCheckbox( + value: value, + onChanged: (v) => setState(() => value = v), + label: const Text('Accept terms and conditions'), + sublabel: const Text( + 'You agree to our Terms of Service and Privacy Policy.', + ), + ); + } +} +``` + +## Disabled + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class CheckboxDisabledExample extends StatelessWidget { + const CheckboxDisabledExample({super.key}); + + @override + Widget build(BuildContext context) { + return ShadCheckbox( + value: true, + enabled: false, + onChanged: (v) {}, + label: const Text('Accept terms and conditions'), + ); + } +} +``` + diff --git a/.claude/skills/shad-overview/components/collapsible.md b/.claude/skills/shad-overview/components/collapsible.md new file mode 100644 index 00000000..8bab379f --- /dev/null +++ b/.claude/skills/shad-overview/components/collapsible.md @@ -0,0 +1,57 @@ +# Collapsible + +An interactive component that expands and collapses a panel. + +## Default + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class CollapsibleBasicExample extends StatelessWidget { + const CollapsibleBasicExample({super.key}); + + @override + Widget build(BuildContext context) { + final theme = ShadTheme.of(context); + return SizedBox( + width: 350, + child: ShadCollapsible( + trigger: (context, open, toggle) => Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Text( + '@peduarte starred 3 repositories', + style: theme.textTheme.small, + overflow: TextOverflow.ellipsis, + ), + ), + ShadIconButton.ghost( + icon: Icon( + open ? LucideIcons.chevronsDownUp : LucideIcons.chevronsUpDown, + ), + semanticLabel: open ? 'Collapse' : 'Expand', + onPressed: toggle, + ), + ], + ), + child: const Padding( + padding: EdgeInsets.only(top: 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + ShadCard(child: Text('@radix-ui/primitives')), + SizedBox(height: 8), + ShadCard(child: Text('@radix-ui/colors')), + SizedBox(height: 8), + ShadCard(child: Text('@stitches/react')), + ], + ), + ), + ), + ); + } +} +``` + diff --git a/.claude/skills/shad-overview/components/command.md b/.claude/skills/shad-overview/components/command.md new file mode 100644 index 00000000..72838d8f --- /dev/null +++ b/.claude/skills/shad-overview/components/command.md @@ -0,0 +1,113 @@ +# Command + +A composable command menu with filtering and full keyboard navigation. + +## Inline + +Arrow keys move the highlight, Enter selects. Keywords let items match terms their label does not contain. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class CommandInlineExample extends StatelessWidget { + const CommandInlineExample({super.key}); + + @override + Widget build(BuildContext context) { + return ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 450), + child: ShadCommand( + autofocus: false, + decoration: const ShadDecoration(shadows: Shadows.md), + placeholder: const Text('Type a command or search…'), + groups: [ + ShadCommandGroup( + heading: 'Suggestions', + items: [ + ShadCommandItem( + label: 'Calendar', + value: 'calendar', + leading: const Icon(LucideIcons.calendar), + onSelected: () {}, + ), + ShadCommandItem( + label: 'Search Emoji', + value: 'emoji', + keywords: const ['smiley', 'face'], + leading: const Icon(LucideIcons.smile), + onSelected: () {}, + ), + ], + ), + ShadCommandGroup( + heading: 'Settings', + items: [ + ShadCommandItem( + label: 'Profile', + value: 'profile', + leading: const Icon(LucideIcons.user), + trailing: const ShadKbd.group(['⌘', 'P']), + onSelected: () {}, + ), + ShadCommandItem( + label: 'Billing', + value: 'billing', + leading: const Icon(LucideIcons.creditCard), + trailing: const ShadKbd.group(['⌘', 'B']), + onSelected: () {}, + ), + ], + ), + ], + ), + ); + } +} +``` + +## Dialog + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class CommandDialogExample extends StatelessWidget { + const CommandDialogExample({super.key}); + + @override + Widget build(BuildContext context) { + return ShadButton.outline( + trailing: const ShadKbd.group(['⌘', 'K']), + child: const Text('Open command palette'), + onPressed: () { + showShadCommandDialog( + context: context, + placeholder: const Text('Type a command or search…'), + groups: [ + ShadCommandGroup( + heading: 'Suggestions', + items: [ + ShadCommandItem( + label: 'Calendar', + value: 'calendar', + leading: const Icon(LucideIcons.calendar), + onSelected: () {}, + ), + ShadCommandItem( + label: 'Sign out', + value: 'signout', + keywords: const ['logout', 'exit'], + leading: const Icon(LucideIcons.logOut), + onSelected: () {}, + ), + ], + ), + ], + ); + }, + ); + } +} +``` + diff --git a/.claude/skills/shad-overview/components/context-menu.md b/.claude/skills/shad-overview/components/context-menu.md new file mode 100644 index 00000000..a70b248f --- /dev/null +++ b/.claude/skills/shad-overview/components/context-menu.md @@ -0,0 +1,57 @@ +# Context Menu + +Displays a menu at the pointer, triggered by a right click or a long press. + +## Default + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class ContextMenuBasicExample extends StatelessWidget { + const ContextMenuBasicExample({super.key}); + + @override + Widget build(BuildContext context) { + final theme = ShadTheme.of(context); + const divider = ShadSeparator.horizontal( + margin: EdgeInsets.symmetric(vertical: 4), + ); + return ShadContextMenuRegion( + constraints: const BoxConstraints(minWidth: 300), + items: const [ + ShadContextMenuItem.inset(child: Text('Back')), + ShadContextMenuItem.inset(enabled: false, child: Text('Forward')), + ShadContextMenuItem.inset(child: Text('Reload')), + ShadContextMenuItem.inset( + trailing: Icon(LucideIcons.chevronRight), + items: [ + ShadContextMenuItem(child: Text('Save Page As...')), + ShadContextMenuItem(child: Text('Create Shortcut...')), + divider, + ShadContextMenuItem(child: Text('Developer Tools')), + ], + child: Text('More Tools'), + ), + divider, + ShadContextMenuItem( + leading: Icon(LucideIcons.check), + child: Text('Show Bookmarks Bar'), + ), + ShadContextMenuItem.inset(child: Text('Show Full URLs')), + ], + child: Container( + width: 300, + height: 160, + alignment: Alignment.center, + decoration: BoxDecoration( + border: Border.all(color: theme.colorScheme.border), + borderRadius: theme.radii.md, + ), + child: const Text('Right click here'), + ), + ); + } +} +``` + diff --git a/.claude/skills/shad-overview/components/data-table.md b/.claude/skills/shad-overview/components/data-table.md new file mode 100644 index 00000000..56a562d8 --- /dev/null +++ b/.claude/skills/shad-overview/components/data-table.md @@ -0,0 +1,149 @@ +# Data Table + +A table with sorting, selection, filtering, and pagination, driven by a controller. + +## Payments + +Tap a column header to sort. A third tap clears the sort. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class Payment { + const Payment(this.id, this.status, this.email, this.amount); + + final String id; + final String status; + final String email; + final double amount; +} + +const _payments = [ + Payment('m5gr84i9', 'success', 'ken99@example.com', 316), + Payment('3u1reuv4', 'success', 'abe45@example.com', 242), + Payment('derv1ws0', 'processing', 'monserrat44@example.com', 837), + Payment('5kma53ae', 'success', 'silas22@example.com', 874), + Payment('bhqecj4p', 'failed', 'carmella@example.com', 721), + Payment('a1b2c3d4', 'processing', 'jason@example.com', 128), + Payment('e5f6g7h8', 'failed', 'nina@example.com', 455), +]; + +class DataTablePaymentsExample extends StatefulWidget { + const DataTablePaymentsExample({super.key}); + + @override + State createState() => + _DataTablePaymentsExampleState(); +} + +class _DataTablePaymentsExampleState extends State { + final controller = ShadDataTableController( + rows: _payments, + pageSize: 5, + ); + + @override + void initState() { + super.initState(); + controller.addListener(_onControllerChanged); + } + + @override + void dispose() { + controller.removeListener(_onControllerChanged); + controller.dispose(); + super.dispose(); + } + + void _onControllerChanged() { + if (mounted) setState(() {}); + } + + @override + Widget build(BuildContext context) { + final theme = ShadTheme.of(context); + final total = controller.filteredRows.length; + final rowHeight = theme.tableTheme.cellHeight ?? 48; + final visibleRowCount = controller.visibleRows.length; + // TableView does not shrink-wrap, so height tracks the pinned header + // plus the rows on the current page. + final tableHeight = visibleRowCount == 0 + ? rowHeight * 3 + : rowHeight * (visibleRowCount + 1); + + return ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 700), + child: SizedBox( + width: double.infinity, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, + children: [ + ShadCard( + padding: EdgeInsets.zero, + clipBehavior: Clip.antiAlias, + child: ShadDataTable( + controller: controller, + selectable: true, + keyOf: (payment) => payment.id, + height: tableHeight, + showPagination: false, + showSelectionCount: false, + columns: [ + ShadDataTableColumn( + id: 'status', + header: 'Status', + extent: const FixedTableSpanExtent(140), + compare: (a, b) => a.status.compareTo(b.status), + cellBuilder: (context, payment) => ShadBadge.raw( + variant: switch (payment.status) { + 'success' => ShadBadgeVariant.primary, + 'failed' => ShadBadgeVariant.destructive, + _ => ShadBadgeVariant.secondary, + }, + child: Text(payment.status), + ), + ), + ShadDataTableColumn( + id: 'email', + header: 'Email', + compare: (a, b) => a.email.compareTo(b.email), + cellBuilder: (context, payment) => + Text(payment.email, overflow: TextOverflow.ellipsis), + ), + ShadDataTableColumn( + id: 'amount', + header: 'Amount', + extent: const FixedTableSpanExtent(120), + alignment: Alignment.centerRight, + compare: (a, b) => a.amount.compareTo(b.amount), + cellBuilder: (context, payment) => + Text('\$${payment.amount.toStringAsFixed(2)}'), + ), + ], + ), + ), + const SizedBox(height: 12), + Row( + children: [ + Text( + '${controller.selectedKeys.length} of $total row(s) selected.', + style: theme.textTheme.muted, + ), + const Spacer(), + ShadPaginationCompact( + page: controller.page, + pageCount: controller.pageCount, + onPageChanged: (page) => controller.page = page, + ), + ], + ), + ], + ), + ), + ); + } +} +``` + diff --git a/.claude/skills/shad-overview/components/date-picker.md b/.claude/skills/shad-overview/components/date-picker.md new file mode 100644 index 00000000..ecf00268 --- /dev/null +++ b/.claude/skills/shad-overview/components/date-picker.md @@ -0,0 +1,36 @@ +# Date Picker + +A date picker with the calendar in a popover. + +## Single + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class DatePickerSingleExample extends StatelessWidget { + const DatePickerSingleExample({super.key}); + + @override + Widget build(BuildContext context) { + return const ShadDatePicker(); + } +} +``` + +## Range + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class DatePickerRangeExample extends StatelessWidget { + const DatePickerRangeExample({super.key}); + + @override + Widget build(BuildContext context) { + return const ShadDatePicker.range(); + } +} +``` + diff --git a/.claude/skills/shad-overview/components/dialog.md b/.claude/skills/shad-overview/components/dialog.md new file mode 100644 index 00000000..1b3e246f --- /dev/null +++ b/.claude/skills/shad-overview/components/dialog.md @@ -0,0 +1,114 @@ +# Dialog + +A modal window overlaid on the page, rendering the content underneath inert. + +## With a form + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +const _profile = [ + (title: 'Name', value: 'Alexandru'), + (title: 'Username', value: 'nank1ro'), +]; + +class DialogFormExample extends StatelessWidget { + const DialogFormExample({super.key}); + + @override + Widget build(BuildContext context) { + final theme = ShadTheme.of(context); + return ShadButton.outline( + child: const Text('Edit Profile'), + onPressed: () { + showShadDialog( + context: context, + builder: (context) => ShadDialog( + title: const Text('Edit Profile'), + description: const Text( + "Make changes to your profile here. Click save when you're " + 'done.', + ), + actions: const [ShadButton(child: Text('Save changes'))], + crossAxisAlignment: CrossAxisAlignment.stretch, + child: Container( + width: 375, + padding: const EdgeInsets.symmetric(vertical: 20), + child: Column( + mainAxisSize: MainAxisSize.min, + spacing: 16, + children: [ + for (final field in _profile) + Row( + children: [ + Expanded( + child: Text( + field.title, + textAlign: TextAlign.end, + style: theme.textTheme.small, + ), + ), + const SizedBox(width: 16), + Expanded( + flex: 3, + child: ShadInput(initialValue: field.value), + ), + ], + ), + ], + ), + ), + ), + ); + }, + ); + } +} +``` + +## Alert dialog + +Interrupts the user with an important question and explicit actions. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class DialogAlertExample extends StatelessWidget { + const DialogAlertExample({super.key}); + + @override + Widget build(BuildContext context) { + return ShadButton.outline( + child: const Text('Delete account'), + onPressed: () { + showShadDialog( + context: context, + builder: (context) => ShadDialog.alert( + title: const Text('Are you absolutely sure?'), + description: const Padding( + padding: EdgeInsets.only(bottom: 8), + child: Text( + 'This action cannot be undone. This will permanently delete ' + 'your account and remove your data from our servers.', + ), + ), + actions: [ + ShadButton.outline( + child: const Text('Cancel'), + onPressed: () => Navigator.of(context).pop(false), + ), + ShadButton( + child: const Text('Continue'), + onPressed: () => Navigator.of(context).pop(true), + ), + ], + ), + ); + }, + ); + } +} +``` + diff --git a/.claude/skills/shad-overview/components/empty.md b/.claude/skills/shad-overview/components/empty.md new file mode 100644 index 00000000..42ea7df2 --- /dev/null +++ b/.claude/skills/shad-overview/components/empty.md @@ -0,0 +1,39 @@ +# Empty + +A placeholder for an empty state: icon, title, description, and actions. + +## Default + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class EmptyBasicExample extends StatelessWidget { + const EmptyBasicExample({super.key}); + + @override + Widget build(BuildContext context) { + return ShadCard( + width: 400, + height: 320, + columnMainAxisSize: MainAxisSize.max, + child: ShadEmpty( + icon: const Icon(LucideIcons.inbox), + title: const Text('No messages'), + description: const Text('Messages you receive will show up here.'), + actions: [ + ShadButton.outline( + onPressed: () {}, + child: const Text('Refresh'), + ), + ShadButton( + onPressed: () {}, + child: const Text('Compose'), + ), + ], + ), + ); + } +} +``` + diff --git a/.claude/skills/shad-overview/components/icon-button.md b/.claude/skills/shad-overview/components/icon-button.md new file mode 100644 index 00000000..bb53ba73 --- /dev/null +++ b/.claude/skills/shad-overview/components/icon-button.md @@ -0,0 +1,45 @@ +# Icon Button + +A square button holding a single icon. + +## Variants + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class IconButtonVariantsExample extends StatelessWidget { + const IconButtonVariantsExample({super.key}); + + @override + Widget build(BuildContext context) { + return Wrap( + spacing: 8, + runSpacing: 8, + children: [ + ShadIconButton( + onPressed: () {}, + icon: const Icon(LucideIcons.rocket), + ), + ShadIconButton.secondary( + onPressed: () {}, + icon: const Icon(LucideIcons.rocket), + ), + ShadIconButton.destructive( + onPressed: () {}, + icon: const Icon(LucideIcons.rocket), + ), + ShadIconButton.outline( + onPressed: () {}, + icon: const Icon(LucideIcons.rocket), + ), + ShadIconButton.ghost( + onPressed: () {}, + icon: const Icon(LucideIcons.rocket), + ), + ], + ); + } +} +``` + diff --git a/.claude/skills/shad-overview/components/input-otp.md b/.claude/skills/shad-overview/components/input-otp.md new file mode 100644 index 00000000..7cc9ea40 --- /dev/null +++ b/.claude/skills/shad-overview/components/input-otp.md @@ -0,0 +1,40 @@ +# Input OTP + +A one-time password input with copy-paste support. + +## Default + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class InputOtpBasicExample extends StatelessWidget { + const InputOtpBasicExample({super.key}); + + @override + Widget build(BuildContext context) { + return ShadInputOTP( + maxLength: 6, + onChanged: (value) {}, + children: const [ + ShadInputOTPGroup( + children: [ + ShadInputOTPSlot(), + ShadInputOTPSlot(), + ShadInputOTPSlot(), + ], + ), + Icon(LucideIcons.dot), + ShadInputOTPGroup( + children: [ + ShadInputOTPSlot(), + ShadInputOTPSlot(), + ShadInputOTPSlot(), + ], + ), + ], + ); + } +} +``` + diff --git a/.claude/skills/shad-overview/components/input.md b/.claude/skills/shad-overview/components/input.md new file mode 100644 index 00000000..b3676688 --- /dev/null +++ b/.claude/skills/shad-overview/components/input.md @@ -0,0 +1,96 @@ +# Input + +Displays a form input field. + +## Default + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class InputBasicExample extends StatelessWidget { + const InputBasicExample({super.key}); + + @override + Widget build(BuildContext context) { + return ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 320), + child: const ShadInput( + placeholder: Text('Email'), + keyboardType: TextInputType.emailAddress, + ), + ); + } +} +``` + +## Password + +Leading and trailing slots hold icons and actions. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class InputPasswordExample extends StatefulWidget { + const InputPasswordExample({super.key}); + + @override + State createState() => _InputPasswordExampleState(); +} + +class _InputPasswordExampleState extends State { + bool obscure = true; + + @override + Widget build(BuildContext context) { + final theme = ShadTheme.of(context); + + return ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 320), + child: ShadInput( + placeholder: const Text('Password'), + obscureText: obscure, + leading: Icon( + LucideIcons.lock, + size: 16, + color: theme.colorScheme.mutedForeground, + ), + trailing: ShadGestureDetector( + cursor: SystemMouseCursors.click, + behavior: HitTestBehavior.opaque, + onTap: () => setState(() => obscure = !obscure), + child: Icon( + obscure ? LucideIcons.eyeOff : LucideIcons.eye, + size: 16, + color: theme.colorScheme.mutedForeground, + ), + ), + ), + ); + } +} +``` + +## Disabled + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class InputDisabledExample extends StatelessWidget { + const InputDisabledExample({super.key}); + + @override + Widget build(BuildContext context) { + return ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 320), + child: const ShadInput( + enabled: false, + placeholder: Text('Email'), + ), + ); + } +} +``` + diff --git a/.claude/skills/shad-overview/components/kbd.md b/.claude/skills/shad-overview/components/kbd.md new file mode 100644 index 00000000..b20da38c --- /dev/null +++ b/.claude/skills/shad-overview/components/kbd.md @@ -0,0 +1,35 @@ +# Kbd + +Displays a keyboard key or chord. + +## Default + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class KbdBasicExample extends StatelessWidget { + const KbdBasicExample({super.key}); + + @override + Widget build(BuildContext context) { + return const Column( + mainAxisSize: MainAxisSize.min, + spacing: 16, + children: [ + ShadKbd('K'), + ShadKbd.group(['⌘', 'K']), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text('Press '), + ShadKbd.group(['Ctrl', 'Shift', 'P']), + Text(' to open the palette'), + ], + ), + ], + ); + } +} +``` + diff --git a/.claude/skills/shad-overview/components/keyboard-toolbar.md b/.claude/skills/shad-overview/components/keyboard-toolbar.md new file mode 100644 index 00000000..6990bfda --- /dev/null +++ b/.claude/skills/shad-overview/components/keyboard-toolbar.md @@ -0,0 +1,29 @@ +# Keyboard Toolbar + +A toolbar shown above the software keyboard on mobile, with focus navigation and a done button. + +## Default + +Run on a mobile device to see the toolbar. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class KeyboardToolbarBasicExample extends StatelessWidget { + const KeyboardToolbarBasicExample({super.key}); + + @override + Widget build(BuildContext context) { + return ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 400), + child: ShadInput( + placeholder: const Text('Focus on mobile to show the toolbar'), + keyboardType: TextInputType.emailAddress, + keyboardToolbarBuilder: (context) => const ShadDefaultKeyboardToolbar(), + ), + ); + } +} +``` + diff --git a/.claude/skills/shad-overview/components/menubar.md b/.claude/skills/shad-overview/components/menubar.md new file mode 100644 index 00000000..6eca8db2 --- /dev/null +++ b/.claude/skills/shad-overview/components/menubar.md @@ -0,0 +1,70 @@ +# Menubar + +A visually persistent menu, common in desktop applications. + +## Default + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class MenubarBasicExample extends StatelessWidget { + const MenubarBasicExample({super.key}); + + @override + Widget build(BuildContext context) { + const divider = ShadSeparator.horizontal( + margin: EdgeInsets.symmetric(vertical: 4), + ); + return ShadMenubar( + items: const [ + ShadMenubarItem( + items: [ + ShadContextMenuItem(child: Text('New Tab')), + ShadContextMenuItem(child: Text('New Window')), + ShadContextMenuItem( + enabled: false, + child: Text('New Incognito Window'), + ), + divider, + ShadContextMenuItem( + trailing: Icon(LucideIcons.chevronRight), + items: [ + ShadContextMenuItem(child: Text('Email Link')), + ShadContextMenuItem(child: Text('Messages')), + ], + child: Text('Share'), + ), + divider, + ShadContextMenuItem(child: Text('Print...')), + ], + child: Text('File'), + ), + ShadMenubarItem( + items: [ + ShadContextMenuItem(child: Text('Undo')), + ShadContextMenuItem(child: Text('Redo')), + divider, + ShadContextMenuItem(child: Text('Cut')), + ShadContextMenuItem(child: Text('Copy')), + ShadContextMenuItem(child: Text('Paste')), + ], + child: Text('Edit'), + ), + ShadMenubarItem( + items: [ + ShadContextMenuItem( + leading: Icon(LucideIcons.check), + child: Text('Always Show Full URLs'), + ), + ShadContextMenuItem.inset(child: Text('Reload')), + ShadContextMenuItem.inset(child: Text('Toggle Full Screen')), + ], + child: Text('View'), + ), + ], + ); + } +} +``` + diff --git a/.claude/skills/shad-overview/components/pagination.md b/.claude/skills/shad-overview/components/pagination.md new file mode 100644 index 00000000..b8b0d1f7 --- /dev/null +++ b/.claude/skills/shad-overview/components/pagination.md @@ -0,0 +1,126 @@ +# Pagination + +Page navigation with previous and next links. [ShadPagination] shows a windowed run of page numbers; [ShadPaginationCompact] fits toolbars and table footers. + +## Default + +Previous and next labels with a windowed run of page numbers. An ellipsis marks pages that were skipped. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class PaginationBasicExample extends StatefulWidget { + const PaginationBasicExample({super.key}); + + @override + State createState() => _PaginationBasicExampleState(); +} + +class _PaginationBasicExampleState extends State { + int page = 5; + + @override + Widget build(BuildContext context) { + return ShadPagination( + page: page, + pageCount: 20, + onPageChanged: (value) => setState(() => page = value), + ); + } +} +``` + +## Simple + +Page numbers only — set `showEdges: false` when every page fits without an ellipsis. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class PaginationSimpleExample extends StatefulWidget { + const PaginationSimpleExample({super.key}); + + @override + State createState() => + _PaginationSimpleExampleState(); +} + +class _PaginationSimpleExampleState extends State { + int page = 2; + + @override + Widget build(BuildContext context) { + return ShadPagination( + page: page, + pageCount: 5, + showEdges: false, + onPageChanged: (value) => setState(() => page = value), + ); + } +} +``` + +## Compact + +Previous and next icon buttons with a `Page n / m` counter, useful beneath a data table. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class PaginationCompactExample extends StatefulWidget { + const PaginationCompactExample({super.key}); + + @override + State createState() => + _PaginationCompactExampleState(); +} + +class _PaginationCompactExampleState extends State { + int page = 3; + + @override + Widget build(BuildContext context) { + return ShadPaginationCompact( + page: page, + pageCount: 10, + onPageChanged: (value) => setState(() => page = value), + ); + } +} +``` + +## Icons only + +Drop the counter with `labelBuilder` when space is tight, for example beside a rows-per-page control. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class PaginationIconsOnlyExample extends StatefulWidget { + const PaginationIconsOnlyExample({super.key}); + + @override + State createState() => + _PaginationIconsOnlyExampleState(); +} + +class _PaginationIconsOnlyExampleState + extends State { + int page = 4; + + @override + Widget build(BuildContext context) { + return ShadPaginationCompact( + page: page, + pageCount: 12, + onPageChanged: (value) => setState(() => page = value), + labelBuilder: (_, _, _) => const SizedBox.shrink(), + ); + } +} +``` + diff --git a/.claude/skills/shad-overview/components/popover.md b/.claude/skills/shad-overview/components/popover.md new file mode 100644 index 00000000..cb481e94 --- /dev/null +++ b/.claude/skills/shad-overview/components/popover.md @@ -0,0 +1,69 @@ +# Popover + +Displays rich content in a portal, triggered by a button. + +## Default + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +const _layer = [ + (name: 'Width', initialValue: '100%'), + (name: 'Max. width', initialValue: '300px'), + (name: 'Height', initialValue: '25px'), + (name: 'Max. height', initialValue: 'none'), +]; + +class PopoverBasicExample extends StatefulWidget { + const PopoverBasicExample({super.key}); + + @override + State createState() => _PopoverBasicExampleState(); +} + +class _PopoverBasicExampleState extends State { + final controller = ShadPopoverController(); + + @override + void dispose() { + controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final textTheme = ShadTheme.of(context).textTheme; + return ShadPopover( + controller: controller, + popover: (_) => SizedBox( + width: 288, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + spacing: 8, + children: [ + Text('Dimensions', style: textTheme.h4), + Text('Set the dimensions for the layer.', style: textTheme.p), + for (final field in _layer) + Row( + children: [ + Expanded(child: Text(field.name)), + Expanded( + flex: 2, + child: ShadInput(initialValue: field.initialValue), + ), + ], + ), + ], + ), + ), + child: ShadButton.outline( + onPressed: controller.toggle, + child: const Text('Open popover'), + ), + ); + } +} +``` + diff --git a/.claude/skills/shad-overview/components/progress.md b/.claude/skills/shad-overview/components/progress.md new file mode 100644 index 00000000..93ad6e7f --- /dev/null +++ b/.claude/skills/shad-overview/components/progress.md @@ -0,0 +1,32 @@ +# Progress + +An indicator that shows how much of a task is complete. + +## Determinate and indeterminate + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class ProgressBasicExample extends StatelessWidget { + const ProgressBasicExample({super.key}); + + @override + Widget build(BuildContext context) { + return ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 400), + child: Column( + mainAxisSize: MainAxisSize.min, + spacing: 24, + children: [ + // Determinate: pass a value between 0 and 1. + ShadProgress(value: .5), + // Indeterminate: leave the value out. + const ShadProgress(), + ], + ), + ); + } +} +``` + diff --git a/.claude/skills/shad-overview/components/radio-group.md b/.claude/skills/shad-overview/components/radio-group.md new file mode 100644 index 00000000..c1cf9f8a --- /dev/null +++ b/.claude/skills/shad-overview/components/radio-group.md @@ -0,0 +1,37 @@ +# Radio Group + +A set of checkable buttons where only one can be checked at a time. + +## Default + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +enum NotifyAbout { + all('All new messages'), + mentions('Direct messages and mentions'), + nothing('Nothing'); + + const NotifyAbout(this.message); + + final String message; +} + +class RadioGroupBasicExample extends StatelessWidget { + const RadioGroupBasicExample({super.key}); + + @override + Widget build(BuildContext context) { + return ShadRadioGroup( + initialValue: NotifyAbout.mentions, + onChanged: (value) {}, + items: [ + for (final option in NotifyAbout.values) + ShadRadio(value: option, label: Text(option.message)), + ], + ); + } +} +``` + diff --git a/.claude/skills/shad-overview/components/resizable.md b/.claude/skills/shad-overview/components/resizable.md new file mode 100644 index 00000000..7047cda4 --- /dev/null +++ b/.claude/skills/shad-overview/components/resizable.md @@ -0,0 +1,64 @@ +# Resizable + +Resizable panel groups and layouts, with nesting and drag handles. + +## Default + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class ResizableBasicExample extends StatelessWidget { + const ResizableBasicExample({super.key}); + + @override + Widget build(BuildContext context) { + final theme = ShadTheme.of(context); + return SizedBox( + width: 300, + height: 200, + child: DecoratedBox( + decoration: BoxDecoration( + borderRadius: theme.radius, + border: Border.all(color: theme.colorScheme.border), + ), + child: ClipRRect( + borderRadius: theme.radius, + child: ShadResizablePanelGroup( + showHandle: true, + children: [ + ShadResizablePanel( + id: 0, + defaultSize: .5, + minSize: .2, + child: const Center(child: Text('One')), + ), + ShadResizablePanel( + id: 1, + defaultSize: .5, + child: ShadResizablePanelGroup( + axis: Axis.vertical, + showHandle: true, + children: [ + ShadResizablePanel( + id: 0, + defaultSize: .5, + child: const Center(child: Text('Two')), + ), + ShadResizablePanel( + id: 1, + defaultSize: .5, + child: const Center(child: Text('Three')), + ), + ], + ), + ), + ], + ), + ), + ), + ); + } +} +``` + diff --git a/.claude/skills/shad-overview/components/select.md b/.claude/skills/shad-overview/components/select.md new file mode 100644 index 00000000..381721f1 --- /dev/null +++ b/.claude/skills/shad-overview/components/select.md @@ -0,0 +1,106 @@ +# Select + +Displays a list of options for the user to pick from, triggered by a button. + +## Default + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +const _fruits = { + 'apple': 'Apple', + 'banana': 'Banana', + 'blueberry': 'Blueberry', + 'grapes': 'Grapes', + 'pineapple': 'Pineapple', +}; + +class SelectBasicExample extends StatelessWidget { + const SelectBasicExample({super.key}); + + @override + Widget build(BuildContext context) { + return ConstrainedBox( + constraints: const BoxConstraints(minWidth: 180), + child: ShadSelect( + placeholder: const Text('Select a fruit'), + options: [ + for (final fruit in _fruits.entries) + ShadOption(value: fruit.key, child: Text(fruit.value)), + ], + selectedOptionBuilder: (context, value) => Text(_fruits[value]!), + onChanged: (value) {}, + ), + ); + } +} +``` + +## With search + +Filter a long list of Dart frameworks and tools — Serverpod, Relic, Flutter, and more. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +const _dartFrameworks = { + 'serverpod': 'Serverpod', + 'relic': 'Relic', + 'flutter': 'Flutter', + 'flame': 'Flame', + 'jaspr': 'Jaspr', + 'drift': 'Drift', + 'riverpod': 'Riverpod', + 'melos': 'Melos', + 'mason': 'Mason', + 'very_good_cli': 'Very Good CLI', +}; + +class SelectSearchExample extends StatefulWidget { + const SelectSearchExample({super.key}); + + @override + State createState() => _SelectSearchExampleState(); +} + +class _SelectSearchExampleState extends State { + var searchValue = ''; + + Map get filtered => { + for (final entry in _dartFrameworks.entries) + if (entry.value.toLowerCase().contains(searchValue.toLowerCase())) + entry.key: entry.value, + }; + + @override + Widget build(BuildContext context) { + return ConstrainedBox( + constraints: const BoxConstraints(minWidth: 180), + child: ShadSelect.withSearch( + minWidth: 180, + placeholder: const Text('Select framework...'), + onSearchChanged: (value) => setState(() => searchValue = value), + searchPlaceholder: const Text('Search framework'), + options: [ + if (filtered.isEmpty) + const Padding( + padding: EdgeInsets.symmetric(vertical: 24), + child: Text('No framework found'), + ), + for (final entry in _dartFrameworks.entries) + Offstage( + offstage: !filtered.containsKey(entry.key), + child: ShadOption(value: entry.key, child: Text(entry.value)), + ), + ], + selectedOptionBuilder: (context, value) => + Text(_dartFrameworks[value]!), + onChanged: (value) {}, + ), + ); + } +} +``` + diff --git a/.claude/skills/shad-overview/components/separator.md b/.claude/skills/shad-overview/components/separator.md new file mode 100644 index 00000000..2520fb8e --- /dev/null +++ b/.claude/skills/shad-overview/components/separator.md @@ -0,0 +1,54 @@ +# Separator + +Visually or semantically separates content. + +## Default + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class SeparatorBasicExample extends StatelessWidget { + const SeparatorBasicExample({super.key}); + + @override + Widget build(BuildContext context) { + final theme = ShadTheme.of(context); + return SizedBox( + width: 300, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Radix Primitives', style: theme.textTheme.small), + const SizedBox(height: 4), + Text( + 'An open-source UI component library.', + style: theme.textTheme.muted, + ), + const ShadSeparator.horizontal( + margin: EdgeInsets.symmetric(vertical: 16), + ), + const SizedBox( + height: 20, + child: Row( + children: [ + Text('Blog'), + ShadSeparator.vertical( + margin: EdgeInsets.symmetric(horizontal: 16), + ), + Text('Docs'), + ShadSeparator.vertical( + margin: EdgeInsets.symmetric(horizontal: 16), + ), + Text('Source'), + ], + ), + ), + ], + ), + ); + } +} +``` + diff --git a/.claude/skills/shad-overview/components/sheet.md b/.claude/skills/shad-overview/components/sheet.md new file mode 100644 index 00000000..28912fd7 --- /dev/null +++ b/.claude/skills/shad-overview/components/sheet.md @@ -0,0 +1,53 @@ +# Sheet + +Extends the dialog to display content that complements the main content of the screen, sliding in from any edge. + +## Sides + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class SheetSidesExample extends StatelessWidget { + const SheetSidesExample({super.key}); + + void _open(BuildContext context, ShadSheetSide side) { + showShadSheet( + context: context, + side: side, + builder: (context) => ShadSheet( + title: const Text('Edit Profile'), + description: const Text( + "Make changes to your profile here. Click save when you're done.", + ), + actions: [ + ShadButton( + child: const Text('Save changes'), + onPressed: () => Navigator.of(context).pop(), + ), + ], + child: const Padding( + padding: EdgeInsets.symmetric(vertical: 20), + child: ShadInput(placeholder: Text('Name')), + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + return Wrap( + spacing: 8, + runSpacing: 8, + children: [ + for (final side in ShadSheetSide.values) + ShadButton.outline( + onPressed: () => _open(context, side), + child: Text(side.name), + ), + ], + ); + } +} +``` + diff --git a/.claude/skills/shad-overview/components/sidebar.md b/.claude/skills/shad-overview/components/sidebar.md new file mode 100644 index 00000000..849494c4 --- /dev/null +++ b/.claude/skills/shad-overview/components/sidebar.md @@ -0,0 +1,209 @@ +# Sidebar + +A composable, collapsible side navigation panel. Collapses to an icon rail or off-canvas, floats or insets, and becomes a modal sheet under the mobile breakpoint. ⌘B toggles it. + +## Icon rail + +With collapsible: icon the sidebar shrinks to a rail of icons. Menu buttons show their label as a tooltip, and with rail: true the inner edge becomes a grab rail — click it to toggle. This docs app itself runs on ShadSidebarScaffold. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class SidebarBasicExample extends StatefulWidget { + const SidebarBasicExample({super.key}); + + @override + State createState() => _SidebarBasicExampleState(); +} + +class _SidebarBasicExampleState extends State { + String selected = 'Home'; + + ShadSidebarMenuButton item(String label, IconData icon) { + return ShadSidebarMenuButton( + leading: Icon(icon), + isActive: selected == label, + tooltip: label, + onPressed: () => setState(() => selected = label), + child: Text(label), + ); + } + + @override + Widget build(BuildContext context) { + final theme = ShadTheme.of(context); + return SizedBox( + width: 700, + height: 420, + child: DecoratedBox( + decoration: BoxDecoration( + border: Border.all(color: theme.colorScheme.border), + borderRadius: theme.radii.lg, + ), + child: ShadSidebarScaffold( + borderRadius: theme.radii.lg, + sidebar: ShadSidebar( + collapsible: ShadSidebarCollapsible.icon, + // The inner edge highlights under the pointer and toggles the + // sidebar on click. + rail: true, + header: Row( + children: [ + Container( + width: 32, + height: 32, + decoration: BoxDecoration( + color: theme.colorScheme.primary, + borderRadius: theme.radii.md, + ), + child: Icon( + LucideIcons.galleryVerticalEnd, + size: 16, + color: theme.colorScheme.primaryForeground, + ), + ), + const SizedBox(width: 8), + Expanded( + child: Text( + 'Acme Inc', + maxLines: 1, + softWrap: false, + overflow: TextOverflow.clip, + style: theme.textTheme.small, + ), + ), + ], + ), + footer: ShadSidebarMenuButton( + leading: const Icon(LucideIcons.circleUser), + trailing: const Icon(LucideIcons.chevronsUpDown), + tooltip: 'Account', + onPressed: () {}, + child: const Text('shadcn'), + ), + children: [ + ShadSidebarGroup( + label: const Text('Platform'), + children: [ + ShadSidebarMenu( + children: [ + item('Home', LucideIcons.house), + item('Inbox', LucideIcons.inbox), + item('Calendar', LucideIcons.calendar), + ], + ), + ], + ), + ShadSidebarGroup( + label: const Text('Settings'), + children: [ + ShadSidebarMenu( + children: [ + item('Profile', LucideIcons.user), + item('Billing', LucideIcons.creditCard), + ], + ), + ], + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(8), + child: Row( + children: [ + const ShadSidebarTrigger(), + const SizedBox(width: 8), + Text(selected, style: theme.textTheme.small), + ], + ), + ), + const Expanded( + child: Center(child: Text('Content area')), + ), + ], + ), + ), + ), + ); + } +} +``` + +## Floating, with sub-menus + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class SidebarSubmenuExample extends StatefulWidget { + const SidebarSubmenuExample({super.key}); + + @override + State createState() => _SidebarSubmenuExampleState(); +} + +class _SidebarSubmenuExampleState extends State { + String selected = 'Installation'; + + static const _sections = { + 'Getting Started': ['Installation', 'Project Structure'], + 'Building Your Application': ['Routing', 'Data Fetching', 'Rendering'], + }; + + @override + Widget build(BuildContext context) { + final theme = ShadTheme.of(context); + return SizedBox( + width: 700, + height: 420, + child: DecoratedBox( + decoration: BoxDecoration( + border: Border.all(color: theme.colorScheme.border), + borderRadius: theme.radii.lg, + ), + child: ShadSidebarScaffold( + borderRadius: theme.radii.lg, + sidebar: ShadSidebar( + variant: ShadSidebarVariant.floating, + children: [ + ShadSidebarGroup( + label: const Text('Documentation'), + children: [ + ShadSidebarMenu( + children: [ + for (final entry in _sections.entries) ...[ + ShadSidebarMenuButton( + leading: const Icon(LucideIcons.bookOpen), + onPressed: () {}, + child: Text(entry.key), + ), + ShadSidebarMenuSub( + children: [ + for (final page in entry.value) + ShadSidebarMenuSubButton( + isActive: selected == page, + onPressed: () => + setState(() => selected = page), + child: Text(page), + ), + ], + ), + ], + ], + ), + ], + ), + ], + ), + child: Center(child: Text(selected)), + ), + ), + ); + } +} +``` + diff --git a/.claude/skills/shad-overview/components/skeleton.md b/.claude/skills/shad-overview/components/skeleton.md new file mode 100644 index 00000000..0874a63a --- /dev/null +++ b/.claude/skills/shad-overview/components/skeleton.md @@ -0,0 +1,35 @@ +# Skeleton + +A placeholder to show while content is loading. + +## Default + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class SkeletonCardExample extends StatelessWidget { + const SkeletonCardExample({super.key}); + + @override + Widget build(BuildContext context) { + return const Row( + mainAxisSize: MainAxisSize.min, + children: [ + ShadSkeleton.circle(size: 48), + SizedBox(width: 12), + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ShadSkeleton(width: 250, height: 16), + SizedBox(height: 8), + ShadSkeleton(width: 200, height: 16), + ], + ), + ], + ); + } +} +``` + diff --git a/.claude/skills/shad-overview/components/slider.md b/.claude/skills/shad-overview/components/slider.md new file mode 100644 index 00000000..29686f50 --- /dev/null +++ b/.claude/skills/shad-overview/components/slider.md @@ -0,0 +1,106 @@ +# Slider + +An input where the user selects a value from within a given range. + +## Default + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class SliderBasicExample extends StatelessWidget { + const SliderBasicExample({super.key}); + + @override + Widget build(BuildContext context) { + return const SizedBox( + width: 300, + child: ShadSlider( + initialValue: 33, + max: 100, + ), + ); + } +} +``` + +## Range + +ShadRangeSlider takes one value per thumb. The values stay in ascending order — a thumb stops where its neighbour is rather than crossing it. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class SliderRangeExample extends StatefulWidget { + const SliderRangeExample({super.key}); + + @override + State createState() => _SliderRangeExampleState(); +} + +class _SliderRangeExampleState extends State { + List values = const [25, 75]; + + @override + Widget build(BuildContext context) { + final theme = ShadTheme.of(context); + return SizedBox( + width: 300, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + spacing: 12, + children: [ + Text( + // The values stay ascending, so the ends of the range are always + // the first and last entry. + '\$${values.first.round()} – \$${values.last.round()}', + style: theme.textTheme.small, + ), + ShadRangeSlider( + initialValues: values, + max: 100, + onChanged: (next) => setState(() => values = next), + ), + ], + ), + ); + } +} +``` + +## Steps + +With divisions the slider snaps; set showDivisionMarks to false to snap without drawing the ticks. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class SliderStepsExample extends StatelessWidget { + const SliderStepsExample({super.key}); + + @override + Widget build(BuildContext context) { + return const SizedBox( + width: 300, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + spacing: 24, + children: [ + ShadSlider(initialValue: 50, max: 100, divisions: 10), + ShadSlider( + initialValue: 50, + max: 100, + divisions: 10, + showDivisionMarks: false, + ), + ], + ), + ); + } +} +``` + diff --git a/.claude/skills/shad-overview/components/sonner.md b/.claude/skills/shad-overview/components/sonner.md new file mode 100644 index 00000000..e8b29722 --- /dev/null +++ b/.claude/skills/shad-overview/components/sonner.md @@ -0,0 +1,39 @@ +# Sonner + +An opinionated toast stack: notifications collect and expand on hover. + +## Default + +```dart +import 'dart:math'; + +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class SonnerBasicExample extends StatelessWidget { + const SonnerBasicExample({super.key}); + + @override + Widget build(BuildContext context) { + return ShadButton.outline( + child: const Text('Show Toast'), + onPressed: () { + final sonner = ShadSonner.of(context); + final id = Random().nextInt(1000); + sonner.show( + ShadToast( + id: id, + title: const Text('Event has been created'), + description: Text('${DateTime.now()}'), + action: ShadButton( + child: const Text('Undo'), + onPressed: () => sonner.hide(id), + ), + ), + ); + }, + ); + } +} +``` + diff --git a/.claude/skills/shad-overview/components/spinner.md b/.claude/skills/shad-overview/components/spinner.md new file mode 100644 index 00000000..c592875f --- /dev/null +++ b/.claude/skills/shad-overview/components/spinner.md @@ -0,0 +1,28 @@ +# Spinner + +An animated loading indicator. + +## Sizes + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class SpinnerBasicExample extends StatelessWidget { + const SpinnerBasicExample({super.key}); + + @override + Widget build(BuildContext context) { + return const Row( + mainAxisSize: MainAxisSize.min, + spacing: 24, + children: [ + ShadSpinner(size: 16), + ShadSpinner(), + ShadSpinner(size: 32), + ], + ); + } +} +``` + diff --git a/.claude/skills/shad-overview/components/switch.md b/.claude/skills/shad-overview/components/switch.md new file mode 100644 index 00000000..bb6e5025 --- /dev/null +++ b/.claude/skills/shad-overview/components/switch.md @@ -0,0 +1,31 @@ +# Switch + +A control that allows the user to toggle between checked and not checked. + +## Default + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class SwitchBasicExample extends StatefulWidget { + const SwitchBasicExample({super.key}); + + @override + State createState() => _SwitchBasicExampleState(); +} + +class _SwitchBasicExampleState extends State { + bool value = false; + + @override + Widget build(BuildContext context) { + return ShadSwitch( + value: value, + onChanged: (v) => setState(() => value = v), + label: const Text('Airplane Mode'), + ); + } +} +``` + diff --git a/.claude/skills/shad-overview/components/table.md b/.claude/skills/shad-overview/components/table.md new file mode 100644 index 00000000..0ad3d197 --- /dev/null +++ b/.claude/skills/shad-overview/components/table.md @@ -0,0 +1,104 @@ +# Table + +A responsive table component with header and footer rows. + +## Invoices + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +const _invoices = [ + ( + invoice: 'INV001', + status: 'Paid', + method: 'Credit Card', + amount: r'$250.00', + ), + (invoice: 'INV002', status: 'Pending', method: 'PayPal', amount: r'$150.00'), + ( + invoice: 'INV003', + status: 'Unpaid', + method: 'Bank Transfer', + amount: r'$350.00', + ), + ( + invoice: 'INV004', + status: 'Paid', + method: 'Credit Card', + amount: r'$450.00', + ), + (invoice: 'INV005', status: 'Paid', method: 'PayPal', amount: r'$550.00'), +]; + +class TableInvoicesExample extends StatelessWidget { + const TableInvoicesExample({super.key}); + + @override + Widget build(BuildContext context) { + final theme = ShadTheme.of(context); + final rowHeight = theme.tableTheme.cellHeight ?? 48; + // Header row + one row per invoice + footer row, so the card hugs the + // table exactly instead of leaving space below the last row. + final tableHeight = rowHeight * (_invoices.length + 2); + + return ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 600), + child: SizedBox( + width: double.infinity, + child: ShadCard( + padding: EdgeInsets.zero, + clipBehavior: Clip.antiAlias, + child: SizedBox( + height: tableHeight, + child: ShadTable.list( + // The Amount column absorbs any leftover width so the table + // spans the full card instead of leaving a gap on the right. + columnSpanExtent: (index) => switch (index) { + 0 => const FixedTableSpanExtent(140), + 1 => const FixedTableSpanExtent(110), + 2 => const FixedTableSpanExtent(140), + _ => const MaxTableSpanExtent( + FixedTableSpanExtent(120), + RemainingTableSpanExtent(), + ), + }, + header: const [ + ShadTableCell.header(child: Text('Invoice')), + ShadTableCell.header(child: Text('Status')), + ShadTableCell.header(child: Text('Method')), + ShadTableCell.header( + alignment: Alignment.centerRight, + child: Text('Amount'), + ), + ], + footer: const [ + ShadTableCell.footer(child: Text('Total')), + ShadTableCell.footer(child: Text('')), + ShadTableCell.footer(child: Text('')), + ShadTableCell.footer( + alignment: Alignment.centerRight, + child: Text(r'$1750.00'), + ), + ], + children: [ + for (final invoice in _invoices) + [ + ShadTableCell(child: Text(invoice.invoice)), + ShadTableCell(child: Text(invoice.status)), + ShadTableCell(child: Text(invoice.method)), + ShadTableCell( + alignment: Alignment.centerRight, + child: Text(invoice.amount), + ), + ], + ], + ), + ), + ), + ), + ); + } +} +``` + diff --git a/.claude/skills/shad-overview/components/tabs.md b/.claude/skills/shad-overview/components/tabs.md new file mode 100644 index 00000000..9c3f1591 --- /dev/null +++ b/.claude/skills/shad-overview/components/tabs.md @@ -0,0 +1,81 @@ +# Tabs + +A set of layered sections of content displayed one at a time. + +## Default + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class TabsBasicExample extends StatelessWidget { + const TabsBasicExample({super.key}); + + @override + Widget build(BuildContext context) { + return ShadTabs( + value: 'account', + tabBarConstraints: const BoxConstraints(maxWidth: 400), + contentConstraints: const BoxConstraints(maxWidth: 400), + tabs: [ + ShadTab( + value: 'account', + content: ShadCard( + title: const Text('Account'), + description: const Text( + "Make changes to your account here. Click save when you're " + 'done.', + ), + footer: const ShadButton(child: Text('Save changes')), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(height: 16), + ShadInputFormField( + label: const Text('Name'), + initialValue: 'Ale', + ), + const SizedBox(height: 8), + ShadInputFormField( + label: const Text('Username'), + initialValue: 'nank1ro', + ), + const SizedBox(height: 16), + ], + ), + ), + child: const Text('Account'), + ), + ShadTab( + value: 'password', + content: ShadCard( + title: const Text('Password'), + description: const Text( + "Change your password here. After saving, you'll be logged " + 'out.', + ), + footer: const ShadButton(child: Text('Save password')), + child: Column( + children: [ + const SizedBox(height: 16), + ShadInputFormField( + label: const Text('Current password'), + obscureText: true, + ), + const SizedBox(height: 8), + ShadInputFormField( + label: const Text('New password'), + obscureText: true, + ), + const SizedBox(height: 16), + ], + ), + ), + child: const Text('Password'), + ), + ], + ); + } +} +``` + diff --git a/.claude/skills/shad-overview/components/textarea.md b/.claude/skills/shad-overview/components/textarea.md new file mode 100644 index 00000000..ac0d0936 --- /dev/null +++ b/.claude/skills/shad-overview/components/textarea.md @@ -0,0 +1,26 @@ +# Textarea + +A multi-line text input, optionally user-resizable. + +## Default + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class TextareaBasicExample extends StatelessWidget { + const TextareaBasicExample({super.key}); + + @override + Widget build(BuildContext context) { + return ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 400), + child: ShadTextarea( + placeholder: const Text('Type your message here...'), + onChanged: (value) {}, + ), + ); + } +} +``` + diff --git a/.claude/skills/shad-overview/components/time-picker.md b/.claude/skills/shad-overview/components/time-picker.md new file mode 100644 index 00000000..006095ab --- /dev/null +++ b/.claude/skills/shad-overview/components/time-picker.md @@ -0,0 +1,44 @@ +# Time Picker + +A field for entering a time of day. + +## 24-hour + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class TimePickerBasicExample extends StatelessWidget { + const TimePickerBasicExample({super.key}); + + @override + Widget build(BuildContext context) { + return ShadTimePicker( + trailing: const Padding( + padding: EdgeInsets.only(left: 8, top: 14), + child: Icon(LucideIcons.clock4), + ), + onChanged: (time) {}, + ); + } +} +``` + +## With AM/PM period + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class TimePickerPeriodExample extends StatelessWidget { + const TimePickerPeriodExample({super.key}); + + @override + Widget build(BuildContext context) { + return ShadTimePicker.period( + onChanged: (time) {}, + ); + } +} +``` + diff --git a/.claude/skills/shad-overview/components/toast.md b/.claude/skills/shad-overview/components/toast.md new file mode 100644 index 00000000..75bd4499 --- /dev/null +++ b/.claude/skills/shad-overview/components/toast.md @@ -0,0 +1,61 @@ +# Toast + +A succinct message that is displayed temporarily. + +## Default + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class ToastBasicExample extends StatelessWidget { + const ToastBasicExample({super.key}); + + @override + Widget build(BuildContext context) { + return ShadButton.outline( + child: const Text('Add to calendar'), + onPressed: () { + final toaster = ShadToaster.of(context); + toaster.show( + ShadToast( + title: const Text('Scheduled: Catch up'), + description: const Text('Friday, February 10, 2023 at 5:57 PM'), + action: ShadButton.outline( + child: const Text('Undo'), + onPressed: () => toaster.hide(), + ), + ), + ); + }, + ); + } +} +``` + +## Destructive + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class ToastDestructiveExample extends StatelessWidget { + const ToastDestructiveExample({super.key}); + + @override + Widget build(BuildContext context) { + return ShadButton.outline( + child: const Text('Show destructive toast'), + onPressed: () { + ShadToaster.of(context).show( + const ShadToast.destructive( + title: Text('Uh oh! Something went wrong'), + description: Text('There was a problem with your request'), + ), + ); + }, + ); + } +} +``` + diff --git a/.claude/skills/shad-overview/components/toggle-group.md b/.claude/skills/shad-overview/components/toggle-group.md new file mode 100644 index 00000000..6ad8cda7 --- /dev/null +++ b/.claude/skills/shad-overview/components/toggle-group.md @@ -0,0 +1,142 @@ +# Toggle Group + +A set of two-state buttons that can be toggled on or off. + +## Single + +Selecting one value clears the others. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class ToggleGroupBasicExample extends StatefulWidget { + const ToggleGroupBasicExample({super.key}); + + @override + State createState() => + _ToggleGroupBasicExampleState(); +} + +class _ToggleGroupBasicExampleState extends State { + Set alignment = {'left'}; + + @override + Widget build(BuildContext context) { + return ShadToggleGroup( + values: alignment, + onChanged: (values) => setState(() => alignment = values), + children: const [ + ShadToggleGroupItem( + value: 'left', + semanticLabel: 'Align left', + child: Icon(LucideIcons.alignLeft), + ), + ShadToggleGroupItem( + value: 'center', + semanticLabel: 'Align center', + child: Icon(LucideIcons.alignCenter), + ), + ShadToggleGroupItem( + value: 'right', + semanticLabel: 'Align right', + child: Icon(LucideIcons.alignRight), + ), + ], + ); + } +} +``` + +## Multiple + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class ToggleGroupMultipleExample extends StatefulWidget { + const ToggleGroupMultipleExample({super.key}); + + @override + State createState() => + _ToggleGroupMultipleExampleState(); +} + +class _ToggleGroupMultipleExampleState + extends State { + Set formatting = {'bold'}; + + @override + Widget build(BuildContext context) { + return ShadToggleGroup.multiple( + values: formatting, + onChanged: (values) => setState(() => formatting = values), + children: const [ + ShadToggleGroupItem( + value: 'bold', + semanticLabel: 'Bold', + child: Icon(LucideIcons.bold), + ), + ShadToggleGroupItem( + value: 'italic', + semanticLabel: 'Italic', + child: Icon(LucideIcons.italic), + ), + ShadToggleGroupItem( + value: 'underline', + semanticLabel: 'Underline', + child: Icon(LucideIcons.underline), + ), + ], + ); + } +} +``` + +## Outline + +Every item uses the outline toggle style. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class ToggleGroupOutlineExample extends StatefulWidget { + const ToggleGroupOutlineExample({super.key}); + + @override + State createState() => + _ToggleGroupOutlineExampleState(); +} + +class _ToggleGroupOutlineExampleState extends State { + Set values = const {'bold'}; + + @override + Widget build(BuildContext context) { + return ShadToggleGroup( + values: values, + toggleVariant: ShadToggleVariant.outline, + onChanged: (next) => setState(() => values = next), + children: const [ + ShadToggleGroupItem( + value: 'bold', + semanticLabel: 'Bold', + child: Icon(LucideIcons.bold), + ), + ShadToggleGroupItem( + value: 'italic', + semanticLabel: 'Italic', + child: Icon(LucideIcons.italic), + ), + ShadToggleGroupItem( + value: 'underline', + semanticLabel: 'Underline', + child: Icon(LucideIcons.underline), + ), + ], + ); + } +} +``` + diff --git a/.claude/skills/shad-overview/components/toggle.md b/.claude/skills/shad-overview/components/toggle.md new file mode 100644 index 00000000..8cae7cf4 --- /dev/null +++ b/.claude/skills/shad-overview/components/toggle.md @@ -0,0 +1,88 @@ +# Toggle + +A two-state button that can be either on or off. + +## Default + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class ToggleBasicExample extends StatefulWidget { + const ToggleBasicExample({super.key}); + + @override + State createState() => _ToggleBasicExampleState(); +} + +class _ToggleBasicExampleState extends State { + bool bold = false; + bool underline = false; + + @override + Widget build(BuildContext context) { + return Row( + mainAxisSize: MainAxisSize.min, + spacing: 8, + children: [ + ShadToggle( + value: bold, + semanticLabel: 'Bold', + onChanged: (value) => setState(() => bold = value), + child: const Icon(LucideIcons.bold), + ), + ShadToggle( + value: underline, + leading: const Icon(LucideIcons.underline), + onChanged: (value) => setState(() => underline = value), + child: const Text('Underline'), + ), + ], + ); + } +} +``` + +## Outline + +A bordered toggle with a subtle shadow. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class ToggleOutlineExample extends StatefulWidget { + const ToggleOutlineExample({super.key}); + + @override + State createState() => _ToggleOutlineExampleState(); +} + +class _ToggleOutlineExampleState extends State { + bool bold = false; + bool italic = false; + + @override + Widget build(BuildContext context) { + return Row( + mainAxisSize: MainAxisSize.min, + spacing: 8, + children: [ + ShadToggle.outline( + value: bold, + semanticLabel: 'Bold', + onChanged: (value) => setState(() => bold = value), + child: const Icon(LucideIcons.bold), + ), + ShadToggle.outline( + value: italic, + semanticLabel: 'Italic', + onChanged: (value) => setState(() => italic = value), + child: const Icon(LucideIcons.italic), + ), + ], + ); + } +} +``` + diff --git a/.claude/skills/shad-overview/components/tooltip.md b/.claude/skills/shad-overview/components/tooltip.md new file mode 100644 index 00000000..ce6e4abd --- /dev/null +++ b/.claude/skills/shad-overview/components/tooltip.md @@ -0,0 +1,268 @@ +# Tooltip + +A popup that displays information related to an element on hover or focus. + +## Basic + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class TooltipBasicExample extends StatelessWidget { + const TooltipBasicExample({super.key}); + + @override + Widget build(BuildContext context) { + return ShadTooltip( + builder: (context) => const Text('Add to library'), + child: ShadButton.outline( + onPressed: () {}, + child: const Text('Show Tooltip'), + ), + ); + } +} +``` + +## Sides + +An anchor places the tooltip on any side of its trigger; the arrow direction follows it. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class TooltipSidesExample extends StatelessWidget { + const TooltipSidesExample({super.key}); + + @override + Widget build(BuildContext context) { + return const Wrap( + spacing: 8, + runSpacing: 8, + children: [ + _SideTooltip(label: 'Left', direction: AxisDirection.left), + _SideTooltip(label: 'Top', direction: AxisDirection.up), + _SideTooltip(label: 'Bottom', direction: AxisDirection.down), + _SideTooltip(label: 'Right', direction: AxisDirection.right), + ], + ); + } +} + +class _SideTooltip extends StatelessWidget { + const _SideTooltip({required this.label, required this.direction}); + + final String label; + + /// The side of the trigger the tooltip appears on. + final AxisDirection direction; + + @override + Widget build(BuildContext context) { + final anchor = switch (direction) { + AxisDirection.up => const ShadAnchor( + childAlignment: Alignment.topCenter, + overlayAlignment: Alignment.bottomCenter, + offset: Offset(0, -4), + ), + AxisDirection.down => const ShadAnchor( + childAlignment: Alignment.bottomCenter, + overlayAlignment: Alignment.topCenter, + offset: Offset(0, 4), + ), + AxisDirection.left => const ShadAnchor( + childAlignment: Alignment.centerLeft, + overlayAlignment: Alignment.centerRight, + offset: Offset(-4, 0), + ), + AxisDirection.right => const ShadAnchor( + childAlignment: Alignment.centerRight, + overlayAlignment: Alignment.centerLeft, + offset: Offset(4, 0), + ), + }; + + return ShadTooltip( + anchor: anchor, + // The arrow points back at the trigger, opposite the placement side. + arrowDirection: switch (direction) { + AxisDirection.up => AxisDirection.down, + AxisDirection.down => AxisDirection.up, + AxisDirection.left => AxisDirection.right, + AxisDirection.right => AxisDirection.left, + }, + builder: (context) => const Text('Add to library'), + child: ShadButton.outline(onPressed: () {}, child: Text(label)), + ); + } +} +``` + +## With icon + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class TooltipIconExample extends StatelessWidget { + const TooltipIconExample({super.key}); + + @override + Widget build(BuildContext context) { + return ShadTooltip( + builder: (context) => const Text('Additional information'), + child: ShadIconButton.ghost( + icon: const Icon(LucideIcons.info), + onPressed: () {}, + ), + ); + } +} +``` + +## Long content + +Long content wraps at the tooltip’s max width. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class TooltipLongContentExample extends StatelessWidget { + const TooltipLongContentExample({super.key}); + + @override + Widget build(BuildContext context) { + return ShadTooltip( + // Long content wraps at the tooltip's max width. + builder: (context) => const Text( + 'To learn more about how this works, check out the docs. If you ' + 'have any questions, please reach out to us.', + ), + child: ShadButton.outline( + onPressed: () {}, + child: const Text('Show Tooltip'), + ), + ); + } +} +``` + +## Disabled + +A disabled control absorbs pointer events, so a gesture wrapper reports the hover on its behalf. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class TooltipDisabledExample extends StatelessWidget { + const TooltipDisabledExample({super.key}); + + @override + Widget build(BuildContext context) { + // A disabled button absorbs pointer events, so a wrapper reports the + // hover on its behalf — the reference wraps its trigger in a span for + // the same reason. + return ShadTooltip( + builder: (context) => const Text('This feature is currently unavailable'), + child: ShadGestureDetector( + child: ShadButton.outline( + enabled: false, + onPressed: () {}, + child: const Text('Disabled'), + ), + ), + ); + } +} +``` + +## With keyboard shortcut + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class TooltipKeyboardExample extends StatelessWidget { + const TooltipKeyboardExample({super.key}); + + @override + Widget build(BuildContext context) { + return ShadTooltip( + builder: (context) => const Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text('Save Changes'), + SizedBox(width: 6), + ShadKbd('S'), + ], + ), + child: ShadIconButton.outline( + icon: const Icon(LucideIcons.save), + onPressed: () {}, + ), + ); + } +} +``` + +## On link + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class TooltipLinkExample extends StatelessWidget { + const TooltipLinkExample({super.key}); + + @override + Widget build(BuildContext context) { + return ShadTooltip( + builder: (context) => const Text('Click to read the documentation'), + child: ShadButton.link( + onPressed: () {}, + child: const Text('Learn more'), + ), + ); + } +} +``` + +## Formatted content + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class TooltipFormattedExample extends StatelessWidget { + const TooltipFormattedExample({super.key}); + + @override + Widget build(BuildContext context) { + return ShadTooltip( + builder: (context) { + final style = DefaultTextStyle.of(context).style; + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Active', + style: style.copyWith(fontWeight: FontWeight.w600), + ), + const SizedBox(height: 4), + Opacity( + opacity: .8, + child: Text('Last updated 2 hours ago', style: style), + ), + ], + ); + }, + child: ShadButton.outline(onPressed: () {}, child: const Text('Status')), + ); + } +} +``` + diff --git a/.claude/skills/shad-overview/guides/form.md b/.claude/skills/shad-overview/guides/form.md new file mode 100644 index 00000000..ed70eb04 --- /dev/null +++ b/.claude/skills/shad-overview/guides/form.md @@ -0,0 +1,139 @@ +# Form + +Building forms with `ShadForm`: field values collect under their ids, and validation runs per field. + +## Validation and submit + +`saveAndValidate` runs every validator and returns whether the form is valid. Values collect in `formKey.currentState!.value`. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class FormProfileExample extends StatefulWidget { + const FormProfileExample({super.key}); + + @override + State createState() => _FormProfileExampleState(); +} + +class _FormProfileExampleState extends State { + final formKey = GlobalKey(); + + @override + Widget build(BuildContext context) { + return ShadForm( + key: formKey, + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 350), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 16, + children: [ + ShadInputFormField( + id: 'username', + label: const Text('Username'), + placeholder: const Text('Enter your username'), + description: const Text('This is your public display name.'), + validator: (v) { + if (v.length < 2) { + return 'Username must be at least 2 characters.'; + } + return null; + }, + ), + ShadCheckboxFormField( + id: 'terms', + initialValue: false, + inputLabel: const Text('I accept the terms and conditions'), + validator: (v) { + if (!v) { + return 'You must accept the terms and conditions'; + } + return null; + }, + ), + ShadButton( + child: const Text('Submit'), + onPressed: () { + if (formKey.currentState!.saveAndValidate()) { + ShadToaster.of(context).show( + ShadToast( + title: const Text('Form submitted'), + description: Text('${formKey.currentState!.value}'), + ), + ); + } + }, + ), + ], + ), + ), + ); + } +} +``` + +## Every field type + +Each input component has a `*FormField` counterpart that plugs into `ShadForm` the same way: `ShadInputFormField`, `ShadSelectFormField`, `ShadTextareaFormField`, `ShadRadioGroupFormField`, `ShadDatePickerFormField`, `ShadDateRangePickerFormField`, `ShadTimePickerFormField`, `ShadCheckboxFormField`, `ShadSwitchFormField`, and `ShadInputOTPFormField`. See `example/lib/docs/examples/form/form_fields.dart` for one of each wired into a single form. + +## OTP verification + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class FormOtpExample extends StatefulWidget { + const FormOtpExample({super.key}); + + @override + State createState() => _FormOtpExampleState(); +} + +class _FormOtpExampleState extends State { + final formKey = GlobalKey(); + + @override + Widget build(BuildContext context) { + return ShadForm( + key: formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 16, + children: [ + ShadInputOTPFormField( + id: 'otp', + maxLength: 6, + label: const Text('OTP'), + description: const Text('Enter your one-time password.'), + validator: (v) => v.contains(' ') ? 'Fill the whole code' : null, + children: const [ + ShadInputOTPGroup( + children: [ + ShadInputOTPSlot(), + ShadInputOTPSlot(), + ShadInputOTPSlot(), + ], + ), + Icon(LucideIcons.dot), + ShadInputOTPGroup( + children: [ + ShadInputOTPSlot(), + ShadInputOTPSlot(), + ShadInputOTPSlot(), + ], + ), + ], + ), + ShadButton( + child: const Text('Verify'), + onPressed: () => formKey.currentState!.saveAndValidate(), + ), + ], + ), + ); + } +} +``` diff --git a/.claude/skills/shad-overview/guides/installation.md b/.claude/skills/shad-overview/guides/installation.md new file mode 100644 index 00000000..70a78c27 --- /dev/null +++ b/.claude/skills/shad-overview/guides/installation.md @@ -0,0 +1,82 @@ +# Installation + +Add `shad` to your project from the command line: + +```bash +flutter pub add shad +``` + +Everything ships through one import: components, theme, Lucide icons, and animation effects. + +```dart +import 'package:shad/shad.dart'; +``` + +## Set up ShadApp + +`ShadApp` takes the place of `MaterialApp`. It installs the theme, localizations, navigation, and the overlay layer the floating components use. With no arguments it uses the slate color scheme and the nova style. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return const ShadApp( + home: HomePage(), + ); + } +} +``` + +Apps that use the `Router` API use `ShadApp.router` with the usual `routerConfig` or delegate parameters. + +## Dark mode + +Pass a `theme` and a `darkTheme`, and pick between them with `themeMode`. Follow the platform, or bind it to your own setting. + +```dart +ShadApp( + themeMode: ThemeMode.system, + theme: ShadThemeData(), + darkTheme: ShadThemeData( + brightness: Brightness.dark, + ), + home: const HomePage(), +) +``` + +## Use the components + +Components are plain widgets. Icons come from the bundled Lucide set: + +```dart +ShadButton( + leading: const Icon(LucideIcons.mail), + onPressed: () {}, + child: const Text('Login with Email'), +) +``` + +## Padding + +Use `ShadPadding` instead of Flutter's `Padding` to keep insets on the theme's spacing scale. It has no plain constructor, only `ShadPadding.all`, `.symmetric`, `.only`, and `.directional`, and their arguments are steps, not logical pixels: + +```dart +ShadPadding.symmetric( + horizontal: 6, // px-6 + vertical: 4, // py-4 + child: const Text('Inset by the theme spacing scale'), +) +``` + +See [layout.md](layout.md#padding) for a full example. + +From here, [theming.md](theming.md) and [styles.md](styles.md) cover how to make it yours, and each page under `components/` pairs a description with the actual example source that ships in the example app. diff --git a/.claude/skills/shad-overview/guides/layout.md b/.claude/skills/shad-overview/guides/layout.md new file mode 100644 index 00000000..250fa10a --- /dev/null +++ b/.claude/skills/shad-overview/guides/layout.md @@ -0,0 +1,130 @@ +# Layout + +Layout widgets measure gaps and padding in theme spacing steps: `ShadRow`, `ShadColumn`, `ShadGap`, and `ShadPadding`. They read `ShadThemeData.spacing` the same way components do (see [styles.md](styles.md#spacing)), so a layout built from them scales with the rest of the theme. + +## Row and column spacing + +`ShadRow` and `ShadColumn` accept a `spacing` argument in theme steps. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class LayoutSpacingExample extends StatelessWidget { + const LayoutSpacingExample({super.key}); + + @override + Widget build(BuildContext context) { + return ShadColumn( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + spacing: 4, + children: [ + ShadCard( + child: ShadRow( + mainAxisSize: MainAxisSize.min, + spacing: 2, + children: [ + for (final label in ['One', 'Two', 'Three']) + ShadBadge.secondary(child: Text(label)), + ], + ), + ), + ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 320), + child: ShadCard( + child: ShadColumn( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, + spacing: 3, + children: [ + const ShadInput(placeholder: Text('Email')), + const ShadInput(placeholder: Text('Password')), + ShadButton(onPressed: () {}, child: const Text('Sign in')), + ], + ), + ), + ), + ], + ); + } +} +``` + +## Gaps + +Use `ShadGap` in a plain `Row` or `Column` when only some children need space between them. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class LayoutGapExample extends StatelessWidget { + const LayoutGapExample({super.key}); + + @override + Widget build(BuildContext context) { + final theme = ShadTheme.of(context); + + return ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 320), + child: ShadCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, + children: [ + Text('Account', style: theme.textTheme.h4), + const ShadGap(1), + Text( + 'Manage your profile and security settings.', + style: theme.textTheme.muted, + ), + const ShadGap(4), + ShadButton.outline( + onPressed: () {}, + child: const Text('Open settings'), + ), + ], + ), + ), + ); + } +} +``` + +## Padding + +`ShadPadding` mirrors Tailwind's step-based inset utilities. `symmetric(horizontal: 6, vertical: 4)` matches `px-6 py-4`. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class LayoutPaddingExample extends StatelessWidget { + const LayoutPaddingExample({super.key}); + + @override + Widget build(BuildContext context) { + final theme = ShadTheme.of(context); + final spacing = theme.spacing; + + return ColoredBox( + color: theme.colorScheme.muted, + child: ShadPadding.symmetric( + horizontal: 6, + vertical: 4, + child: ShadCard( + child: Text( + 'Inset by ${spacing(6)} x ${spacing(4)}', + style: theme.textTheme.small, + ), + ), + ), + ); + } +} +``` + +## Card sections and form actions + +`ShadColumn` with `crossAxisAlignment: stretch` lines up full-width children inside a card, with `ShadSeparator` between groups. The same pattern stacks fields and buttons in a form: put the primary action above an outline cancel. See `example/lib/docs/examples/layout/layout_card_sections.dart` and `layout_form_actions.dart` for the full examples. diff --git a/.claude/skills/shad-overview/guides/portal.md b/.claude/skills/shad-overview/guides/portal.md new file mode 100644 index 00000000..9b07d0bc --- /dev/null +++ b/.claude/skills/shad-overview/guides/portal.md @@ -0,0 +1,99 @@ +# Portal + +The raw overlay primitive underneath popover, select, tooltip, and menus: it renders a follower widget anchored to its child, above everything else. + +`ShadPortal` is a building block rather than an end-user component. Reach for `ShadPopover`, `ShadTooltip`, or the menu components first. Use the portal directly only when you build a new floating surface. + +## Anchoring modes + +`ShadAnchorAuto` flips and shifts the follower to stay on screen, the same anchor mode every built-in floating component uses. + +```dart +import 'dart:async'; + +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +/// Cycles `ShadAnchorAuto` follower anchors on a hover-triggered portal. +class PortalAnchoringExample extends StatefulWidget { + const PortalAnchoringExample({super.key}); + + @override + State createState() => _PortalAnchoringExampleState(); +} + +class _PortalAnchoringExampleState extends State { + var visible = false; + late final Timer timer; + var alignmentIndex = 0; + + static const _alignments = [ + AlignmentGeometry.topStart, + AlignmentGeometry.topCenter, + AlignmentGeometry.topEnd, + AlignmentGeometry.centerStart, + AlignmentGeometry.center, + AlignmentGeometry.centerEnd, + AlignmentGeometry.bottomStart, + AlignmentGeometry.bottomCenter, + AlignmentGeometry.bottomEnd, + ]; + + @override + void initState() { + super.initState(); + timer = Timer.periodic(const Duration(seconds: 1), (_) { + setState(() { + alignmentIndex = (alignmentIndex + 1) % _alignments.length; + }); + }); + } + + @override + void dispose() { + timer.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final theme = ShadTheme.of(context); + final alignment = _alignments[alignmentIndex]; + + return Align( + child: ShadPortal( + anchor: ShadAnchorAuto(followerAnchor: alignment), + visible: visible, + portalBuilder: (context) { + return ShadMouseArea( + groupId: 'portal-demo', + child: ColoredBox( + color: theme.colorScheme.primary, + child: const SizedBox( + width: 200, + height: 200, + child: Center(child: Text('200x200')), + ), + ), + ); + }, + child: ShadMouseArea( + groupId: 'portal-demo', + onEnter: (_) => setState(() => visible = true), + onExit: (_) => setState(() => visible = false), + child: ColoredBox( + color: theme.colorScheme.destructive, + child: const SizedBox( + width: 100, + height: 100, + child: Center(child: Text('100x100')), + ), + ), + ), + ), + ); + } +} +``` + +The two `ShadMouseArea` widgets share a `groupId` so hovering the portal's own content counts as still hovering the trigger, which is what keeps a popover open while the pointer moves from the trigger onto the popover surface. diff --git a/.claude/skills/shad-overview/guides/responsive.md b/.claude/skills/shad-overview/guides/responsive.md new file mode 100644 index 00000000..06b95833 --- /dev/null +++ b/.claude/skills/shad-overview/guides/responsive.md @@ -0,0 +1,50 @@ +# Responsive + +Tailwind's breakpoint scale, carried on the theme: a builder widget and a context extension. + +## Breakpoints + +The theme carries six breakpoints matching Tailwind's scale. Override any of them on `ShadThemeData`: + +```dart +ShadThemeData( + breakpoints: ShadBreakpoints( + tn: 0, // tiny + sm: 640, + md: 768, + lg: 1024, + xl: 1280, + xxl: 1536, + ), +) +``` + +## Reading the current breakpoint + +`ShadResponsiveBuilder` rebuilds with the breakpoint for the current width. `context.breakpoint` reads the same value inline. + +```dart +ShadResponsiveBuilder( + builder: (context, breakpoint) { + final sm = breakpoint >= ShadTheme.of(context).breakpoints.sm; + return sm ? const WideLayout() : const NarrowLayout(); + }, +) +``` + +Comparisons follow Tailwind's semantics. `sm` means "small and up", so test with `>=`. Use `==` only to target exactly one band. + +## Switching on the band + +Each band is its own type on a sealed class, so a `switch` covers the scale exhaustively: + +```dart +return switch (context.breakpoint) { + ShadBreakpointTN() => const Text('Tiny'), + ShadBreakpointSM() => const Text('Small'), + ShadBreakpointMD() => const Text('Medium'), + ShadBreakpointLG() => const Text('Large'), + ShadBreakpointXL() => const Text('Extra large'), + ShadBreakpointXXL() => const Text('Extra extra large'), +}; +``` diff --git a/.claude/skills/shad-overview/guides/styles.md b/.claude/skills/shad-overview/guides/styles.md new file mode 100644 index 00000000..e127ce07 --- /dev/null +++ b/.claude/skills/shad-overview/guides/styles.md @@ -0,0 +1,56 @@ +# Styles + +The eight shadcn styles are complete presets for geometry, typography, and shadows, switchable with one line. + +shadcn/ui ships eight named styles, and they are not color variations. Each one is a full geometry set. A `mira` button is 28px tall with 8px of padding where a `sera` button is 40px with 24px, and inputs, menu rows, cards, sliders, and switches move with them. Styles also retype the UI: each one redefines the title, label, body, caption, and field roles, and reshapes it with its own radii and shadows. Every value was read from the shadcn/ui reference CSS. + +## Choosing a style + +```dart +ShadThemeData( + style: ShadStyleTokens.maia, +) +``` + +- `vega`: the classic. 36px controls, moderate radii, subtle shadows. +- `nova`: the default. Vega, condensed to 32px controls and tighter surfaces. +- `maia`: pill-shaped and roomy, with fully rounded controls. +- `lyra`: square. No radii, flat shadows, 12px body text. +- `mira`: the densest. 28px controls on a relaxed 12px line. +- `luma`: soft. Filled fields, large radii, weighty shadows. +- `sera`: editorial. 40px controls, underlined fields, uppercase labels, generous space. +- `rhea`: gently rounded mid-size controls with soft hairlines. + +Open the example app's theme editor and switch styles to see them live. The whole app follows. + +## Picking at runtime + +The presets are values on `ShadStyleTokens`, so a style switcher is a list and a lookup: + +```dart +ShadStyleTokens.all; // the eight presets +ShadStyleTokens.fromName('sera'); // one, by its shadcn name +``` + +## Custom styles + +A style is data, a set of measured tokens. Derive your own from the preset closest to what you want: + +```dart +ShadThemeData( + style: ShadStyleTokens.nova.copyWith( + buttonRadius: ShadRadiusToken.full, + buttonHeight: 40, + ), +) +``` + +## Spacing + +Metrics the reference writes in Tailwind's spacing units follow the theme's `ShadSpacing` step: 4px, like `0.25rem`. Raising it loosens every unit-derived padding and gap in one move: + +```dart +ShadThemeData( + spacing: const ShadSpacing(step: 5), +) +``` diff --git a/.claude/skills/shad-overview/guides/theming.md b/.claude/skills/shad-overview/guides/theming.md new file mode 100644 index 00000000..aca4a6ec --- /dev/null +++ b/.claude/skills/shad-overview/guides/theming.md @@ -0,0 +1,149 @@ +# Theming + +One `ShadThemeData` drives every component: color scheme, radius, typography, and per-component overrides. + +## Reading the theme + +Every component resolves its appearance from the nearest `ShadTheme`. Your own widgets read it the same way: + +```dart +final theme = ShadTheme.of(context); + +theme.colorScheme.primary; // the palette +theme.textTheme.p; // the type scale +theme.radius; // the base corner radius +theme.style; // the active style preset +``` + +## Color schemes + +Sixteen schemes ship in light and dark: blue, gray, green, mauve, mist, neutral, olive, orange, red, rose, slate (the default), stone, taupe, violet, yellow, and zinc. + +```dart +ShadApp( + theme: ShadThemeData( + colorScheme: const ShadZincColorScheme.light(), + ), + darkTheme: ShadThemeData( + brightness: Brightness.dark, + colorScheme: const ShadZincColorScheme.dark(), + ), +) +``` + +To let users pick a palette at runtime, resolve one by name with `ShadColorScheme.fromName`: + +```dart +final scheme = ShadColorScheme.fromName( + 'rose', + brightness: Brightness.dark, +); +``` + +## Customizing a scheme + +A scheme is a value. Override individual slots where you construct it, and carry your own colors in the `custom` map so they lerp and switch with the rest of the theme. + +```dart +ShadThemeData( + colorScheme: const ShadSlateColorScheme.light( + primary: Color(0xFF16A34A), + custom: { + 'highlight': Color(0xFFFACC15), + }, + ), +) + +// Read it back anywhere: +ShadTheme.of(context).colorScheme.custom['highlight']; +``` + +An extension keeps custom colors as convenient as the built-in ones: + +```dart +extension AppColors on ShadColorScheme { + Color get highlight => custom['highlight']!; +} +``` + +## Component themes + +Every component has a theme slot on `ShadThemeData`: `primaryButtonTheme`, `cardTheme`, `inputTheme`, and so on. Values you pass are merged over what the style derives, so an override touches exactly the property you name: + +```dart +ShadThemeData( + primaryButtonTheme: const ShadButtonTheme( + backgroundColor: Color(0xFF16A34A), + ), +) +``` + +## Radius + +The theme carries one base radius, the `md` step of the scale. Everything else derives from it: at the default 8 the scale runs sm 6, md 8, lg 10, xl 14, 2xl 16, 3xl 24, 4xl 32, and each component picks the step its style prescribes. + +```dart +ShadThemeData( + radius: const BorderRadius.all(Radius.circular(10)), +) +``` + +## Menu appearance + +The shadcn theme editor's menu options are theme settings here too. `menuColorScheme` renders menu surfaces on another palette (pass the dark counterpart for the "Inverted" look), `menuTranslucent` gives them the blurred 70% finish, and `menuAccent` picks how strongly rows highlight. + +```dart +ShadThemeData( + menuColorScheme: const ShadSlateColorScheme.dark(), + menuTranslucent: true, + menuAccent: ShadMenuAccent.bold, +) +``` + +## Focus rings + +Focus is shown as an outward ring, like shadcn/ui. Set `disableSecondaryBorder: true` to switch to the alternative treatment: a 2px border inside the control, which keeps focus visible where an outward ring would be clipped. + +```dart +ShadThemeData( + disableSecondaryBorder: true, +) +``` + +## Interop with Material and Cupertino + +`ShadApp.custom` renders shadcn components inside a `MaterialApp` or `CupertinoApp`, so both widget sets can share one page. Add the localizations delegate for each set you use and wrap `builder` in a `ShadAppBuilder`: + +```dart +import 'package:flutter/material.dart'; +import 'package:shad/shad.dart'; + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return ShadApp.custom( + themeMode: ThemeMode.dark, + darkTheme: ShadThemeData( + brightness: Brightness.dark, + colorScheme: const ShadSlateColorScheme.dark(), + ), + appBuilder: (context) { + return MaterialApp( + theme: Theme.of(context), + localizationsDelegates: const [ + GlobalShadLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ], + builder: (context, child) => ShadAppBuilder(child: child!), + ); + }, + ); + } +} +``` + +`Theme.of(context)` inside `appBuilder` already carries a `ThemeData` derived from the `ShadThemeData` (color scheme, typography, and common widget themes), so Material widgets on the same page pick up matching colors without extra work. Use `CupertinoApp` and `CupertinoTheme.of(context)` the same way for Cupertino. If the app needs the `Router` API, use `MaterialApp.router` or `CupertinoApp.router` inside `appBuilder`. diff --git a/.claude/skills/shad-overview/guides/typography.md b/.claude/skills/shad-overview/guides/typography.md new file mode 100644 index 00000000..aa8909e1 --- /dev/null +++ b/.claude/skills/shad-overview/guides/typography.md @@ -0,0 +1,98 @@ +# Typography + +`ShadThemeData.textTheme` carries the prose scale. Read a role and hand it to a `Text` widget: + +```dart +Text( + 'Taxing Laughter: The Joke Tax Chronicles', + style: ShadTheme.of(context).textTheme.h1, +) +``` + +## Roles + +| Role | Use for | +| --- | --- | +| `h1Large` | The largest heading, for hero-style titles. | +| `h1` | Page titles. | +| `h2` | Section titles. | +| `h3` | Sub-section titles. | +| `h4` | Card and dialog titles. | +| `p` | Body paragraphs. | +| `blockquote` | Quoted text, indented with a left border. | +| `table` | Table cell text. | +| `list` | List item text. | +| `lead` | The introductory paragraph under a page title. | +| `large` | Emphasized body text, for example a dialog's main question. | +| `small` | Field labels and secondary UI text. | +| `muted` | De-emphasized helper text. | + +Every component that shows text (buttons, badges, cards, alerts, menus, and so on) reads its own style from a component theme instead of these roles directly. Those styles derive from the active style preset (see [styles.md](styles.md)) but can be overridden the same way as any other component theme, see [theming.md](theming.md). + +## Custom font family + +By default `shad` uses [Geist](https://vercel.com/font) as the default font family. To change it, add the font to your project, for example under a `/fonts` directory, then declare it in `pubspec.yaml`: + +```yaml +flutter: + fonts: + - family: UbuntuMono + fonts: + - asset: fonts/UbuntuMono-Regular.ttf + - asset: fonts/UbuntuMono-Italic.ttf + style: italic + - asset: fonts/UbuntuMono-Bold.ttf + weight: 700 + - asset: fonts/UbuntuMono-BoldItalic.ttf + weight: 700 + style: italic +``` + +Then pass a `ShadTextTheme` with the new family to `ShadThemeData`: + +```dart +ShadThemeData( + colorScheme: const ShadZincColorScheme.light(), + textTheme: ShadTextTheme( + colorScheme: const ShadZincColorScheme.light(), + family: 'UbuntuMono', + ), +) +``` + +## Google Fonts + +Add the [google_fonts](https://pub.dev/packages/google_fonts) package, then build the text theme from one of its fonts: + +```dart +ShadThemeData( + colorScheme: const ShadZincColorScheme.light(), + textTheme: ShadTextTheme.fromGoogleFont(GoogleFonts.poppins), +) +``` + +## Custom styles + +Add your own named styles with the `custom` parameter, the same pattern `ShadColorScheme` uses for custom colors: + +```dart +ShadThemeData( + textTheme: ShadTextTheme( + custom: { + 'myCustomStyle': const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w400, + color: Colors.blue, + ), + }, + ), +) +``` + +Read it back with `ShadTheme.of(context).textTheme.custom['myCustomStyle']!`, or add an extension to make it a getter: + +```dart +extension CustomStyleExtension on ShadTextTheme { + TextStyle get myCustomStyle => custom['myCustomStyle']!; +} +``` diff --git a/.cursor/skills/flutter-add-widget-preview/SKILL.md b/.cursor/skills/flutter-add-widget-preview/SKILL.md index 6ba68942..89db1321 100644 --- a/.cursor/skills/flutter-add-widget-preview/SKILL.md +++ b/.cursor/skills/flutter-add-widget-preview/SKILL.md @@ -9,28 +9,43 @@ metadata: ## Contents - [Preview Guidelines](#preview-guidelines) +- [Preview Parameters](#preview-parameters) - [Handling Limitations](#handling-limitations) - [Workflows](#workflows) - [Examples](#examples) ## Preview Guidelines -Use the Flutter Widget Previewer to render widgets in real-time, isolated from the full application context. +Use the Flutter Widget Previewer to render widgets in real-time, isolated from the full application context. - **Target Elements:** Apply the `@Preview` annotation to top-level functions, static methods within a class, or public widget constructors/factories that have no required arguments and return a `Widget` or `WidgetBuilder`. - **Imports:** Always import `package:flutter/widget_previews.dart` to access the preview annotations. -- **Custom Annotations:** Extend the `Preview` class to create custom annotations that inject common properties (e.g., themes, wrappers) across multiple widgets. +- **Custom Annotations:** Extend the `Preview` class to create custom annotations that inject common properties (e.g., custom themes, wrappers, sizes) across multiple widgets. - **Multiple Configurations:** Apply multiple `@Preview` annotations to a single target to generate multiple preview instances. Alternatively, extend `MultiPreview` to encapsulate common multi-preview configurations. -- **Runtime Transformations:** Override the `transform()` method in custom `Preview` or `MultiPreview` classes to modify preview configurations dynamically at runtime (e.g., generating names based on dynamic values, which is impossible in a `const` context). +- **Runtime Transformations:** Override the `transform()` method in custom `Preview` or `MultiPreview` classes to modify preview configurations dynamically at runtime (e.g., building names dynamically based on parameters, which is impossible in a `const` constructor). + +## Preview Parameters + +The `@Preview` annotation supports the following parameters for tailoring preview environments: + +- `name`: A descriptive name for the preview instance. +- `group`: A name used to group related previews together in the previewer. +- `size`: Artificial constraints using a `Size` object. +- `textScaleFactor`: Custom font scale factor. +- `wrapper`: A function that wraps the previewed widget in a specific widget tree (e.g., to inject state or inherited widgets). +- `theme`: A function returning a `PreviewThemeData` subclass instance to provide custom theming with support for sequential theme layering. +- `brightness`: The initial theme `Brightness` (`Brightness.light` or `Brightness.dark`). +- `localizations`: A function applying a localization configuration. ## Handling Limitations Adhere to the following constraints when authoring previewable widgets, as the Widget Previewer runs in a web environment: -- **No Native APIs:** Do not use native plugins or APIs from `dart:io` or `dart:ffi`. Widgets with transitive dependencies on `dart:io` or `dart:ffi` will throw exceptions upon invocation. Use conditional imports to mock or bypass these in preview mode. +- **No Native APIs:** Do not use native plugins or APIs from `dart:io` or `dart:ffi`. Widgets with transitive dependencies on `dart:io` or `dart:ffi` will throw exceptions upon invocation. Use conditional imports or wrappers to mock or bypass these in preview mode. - **Asset Paths:** Use package-based paths for assets loaded via `dart:ui` `fromAsset` APIs (e.g., `packages/my_package_name/assets/my_image.png` instead of `assets/my_image.png`). - **Public Callbacks:** Ensure all callback arguments provided to preview annotations are public and constant to satisfy code generation requirements. -- **Constraints:** Apply explicit constraints using the `size` parameter in the `@Preview` annotation if your widget is unconstrained, as the previewer defaults to constraining them to approximately half the viewport. +- **Constraints:** Apply explicit constraints using the `size` parameter in the `@Preview` annotation if your widget is unconstrained, as unconstrained widgets default to roughly half the viewport. +- **Build Caching:** The previewer caches builds in a `.widget_preview/` folder in the project root. ## Workflows @@ -40,13 +55,13 @@ Copy and track this checklist when implementing a new widget preview: - [ ] Import `package:flutter/widget_previews.dart`. - [ ] Identify a valid target (top-level function, static method, or parameter-less public constructor). - [ ] Apply the `@Preview` annotation to the target. -- [ ] Configure preview parameters (`name`, `group`, `size`, `theme`, `brightness`, etc.) as needed. -- [ ] If applying the same configuration to multiple widgets, extract the configuration into a custom class extending `Preview`. +- [ ] Configure preview parameters (`name`, `group`, `size`, `textScaleFactor`, `wrapper`, `theme`, `brightness`, `localizations`) as needed. +- [ ] If applying the same configuration to multiple widgets, extract the configuration into a custom class extending `Preview` or `MultiPreview`. ### Interacting with Previews Follow the appropriate conditional workflow to launch and interact with the Widget Previewer: -**If using a supported IDE (Android Studio, IntelliJ, VS Code with Flutter 3.38+):** +**If using a supported IDE (Android Studio, IntelliJ, VS Code with Flutter 3.47+):** 1. Launch the IDE. The Widget Previewer starts automatically. 2. Open the "Flutter Widget Preview" tab in the sidebar. 3. Toggle "Filter previews by selected file" at the bottom left if you want to view previews outside the currently active file. @@ -70,45 +85,72 @@ Follow the appropriate conditional workflow to launch and interact with the Widg import 'package:flutter/widget_previews.dart'; import 'package:flutter/material.dart'; -@Preview(name: 'My Sample Text', group: 'Typography') -Widget mySampleText() { - return const Text('Hello, World!'); -} +@Preview(name: 'Submit Button', group: 'Form Controls') +Widget submitButtonPreview() => const ElevatedButton( + onPressed: null, + child: Text('Submit'), + ); ``` -### Custom Preview with Runtime Transformation +### Custom Preview with PreviewThemeData and Transformation ```dart import 'package:flutter/widget_previews.dart'; import 'package:flutter/material.dart'; -final class TransformativePreview extends Preview { - const TransformativePreview({ +final class ThemedPreview extends Preview { + const ThemedPreview({ super.name, super.group, + super.brightness, }); - PreviewThemeData _themeBuilder() { - return PreviewThemeData( - materialLight: ThemeData.light(), - materialDark: ThemeData.dark(), - ); - } + PreviewThemeData _themeBuilder() => + CustomThemeData(brightness: brightness); @override Preview transform() { final originalPreview = super.transform(); + final themeVariant = switch (brightness) { + null => 'Responsive', + final b => b.name, + }; + final builder = originalPreview.toBuilder(); - + final baseName = originalPreview.name ?? 'Preview'; builder - ..name = 'Transformed - ${originalPreview.name}' + ..name = '$baseName [$themeVariant]' ..theme = _themeBuilder; - return builder.toPreview(); + return builder.build(); } } -@TransformativePreview(name: 'Custom Themed Button') -Widget myButton() => const ElevatedButton(onPressed: null, child: Text('Click')); +final class CustomThemeData extends PreviewThemeData { + const CustomThemeData({this.brightness}); + + final Brightness? brightness; + + @override + Widget apply(BuildContext context, Widget child) { + return Theme( + data: ThemeData.from( + colorScheme: ColorScheme.fromSeed( + seedColor: Colors.teal, + brightness: brightness ?? + MediaQuery.maybePlatformBrightnessOf(context) ?? + Brightness.light, + ), + ), + child: child, + ); + } +} + +@ThemedPreview(name: 'Themed Action Button', brightness: Brightness.dark) +Widget themedButtonPreview() => const ElevatedButton( + onPressed: null, + child: Text('Action'), + ); ``` ### MultiPreview Implementation @@ -141,5 +183,10 @@ final class MultiBrightnessPreview extends MultiPreview { } @MultiBrightnessPreview(name: 'Primary Card') -Widget cardPreview() => const Card(child: Padding(padding: EdgeInsets.all(8.0), child: Text('Content'))); +Widget cardPreview() => const Card( + child: Padding( + padding: EdgeInsets.all(8.0), + child: Text('Content'), + ), + ); ``` diff --git a/.cursor/skills/flutter_scene-idioms/SKILL.md b/.cursor/skills/flutter_scene-idioms/SKILL.md new file mode 100644 index 00000000..491b5cb9 --- /dev/null +++ b/.cursor/skills/flutter_scene-idioms/SKILL.md @@ -0,0 +1,137 @@ +--- +name: flutter_scene-idioms +version: 16 +description: Write correct flutter_scene code. Use this whenever building 3D with the flutter_scene Dart/Flutter engine (rendering a scene, geometry, materials, lighting, loading a .glb model, animation, custom shaders). It corrects the wrong assumptions models carry from three.js, Godot, and Unity, and names the APIs and traps that are specific to this engine. +--- + +# Building with flutter_scene + +flutter_scene is a realtime 3D engine for Flutter, built on Flutter GPU. It has a retained scene graph (`Scene` holds `Node`s, nodes carry a `Mesh`), physically based materials, image-based lighting, and a deep post-processing stack. + +**The one thing to internalize: this is not three.js, Godot, or Unity, and the API diverges from all three in specific ways.** Most first-attempt failures come from reaching for another engine's spelling. The corrections below are the highest-value part of this skill; read them before writing code. + +## Do not reach for these (they do not exist or will break the build) + +- **Not the master channel.** flutter_scene runs on **Flutter 3.47 stable or newer**. Do not run `flutter channel master`; it resolves worse, not better. +- **Not `--enable-impeller`, not `--enable-experiment=native-assets`.** The run flag is **`--enable-flutter-gpu`** and nothing else. `--enable-experiment=native-assets` actively breaks the build on Dart 3.10+. +- **Not `package:vector_math/vector_math_64.dart`.** flutter_scene uses **`package:vector_math/vector_math.dart`**. The `_64` types are a different, incompatible `Vector3`. +- **Not `Node.fromAsset(...)`, not `loadModel(...)`.** Load a preprocessed model with **`loadScene('assets/x.glb')`** (returns `Future`), or a runtime glTF with `Node.fromGlbAsset` / `Node.fromGlbBytes`. +- **Not a hand-rolled `CustomPainter` + `Ticker`.** Display a scene with the **`SceneView`** widget; it drives the per-frame loop for you. +- **Not `node.position.set(x, y, z)`.** See transforms below. +- **Not `forward.cross(up)` for a camera or node right vector.** View space is left-handed (`+Z` forward into the screen, `+Y` up, `+X` right), while `vector_math`'s `cross` is right-handed, so `forward.cross(up)` points **screen-left** and horizontally inverts `A`/`D` strafing and drag panning. Read `camera.right`, `camera.horizontalRight`, or `node.right`, or compute `up.cross(forward).normalized()`. +- **Not `.model` files or `buildModels`.** The offline format is `.fsceneb`, produced by the `flutter_scene:init` build hook; you load it by source path with `loadScene`. +- **Not the removed `Environment` class.** Environment lighting is `EnvironmentMap` on `Scene.environment`. + +## Setup + +```sh +flutter pub add flutter_scene +dart run flutter_scene:init # installs the build hook, sets up assets +flutter run --enable-flutter-gpu # native; add -d chrome for web +``` + +`flutter_scene:init` is required setup, not optional. Rendering is gated on `Scene.initializeStaticResources()`; until it completes the engine prints "Flutter Scene is not ready to render. Skipping frame." + +## Minimal scene (this compiles as-is) + +```dart +import 'package:flutter/material.dart'; +import 'package:flutter_scene/scene.dart'; +import 'package:vector_math/vector_math.dart' as vm; + +void main() => runApp(const MaterialApp(home: CubeView())); + +class CubeView extends StatefulWidget { + const CubeView({super.key}); + @override + State createState() => _CubeViewState(); +} + +class _CubeViewState extends State { + final Scene scene = Scene(); + bool ready = false; + + @override + void initState() { + super.initState(); + // Geometry and materials touch the shader bundle, so build them only + // after the engine's static resources are up. + Scene.initializeStaticResources().then((_) { + scene.add(Node( + mesh: Mesh(CuboidGeometry(vm.Vector3(1, 1, 1)), PhysicallyBasedMaterial()), + )); + if (mounted) setState(() => ready = true); + }); + } + + @override + Widget build(BuildContext context) { + if (!ready) return const SizedBox.expand(); + return SceneView(scene, camera: PerspectiveCamera(position: vm.Vector3(2, 2, -4))); + } +} +``` + +An unset `Scene.environment` still gives image-based lighting (a default studio map is resolved at render), so a bare `PhysicallyBasedMaterial()` is lit without any light setup. + +## Choosing declarative or imperative + +flutter_scene has two ways to build a scene, and picking the wrong one is a structural decision that is expensive to undo later. Choose up front by what the app does, not by which reads nicer. + +**Declarative (inline widgets).** Describe the scene as Flutter widgets under `SceneView.declarative(children: [...])`, using `SceneMesh`, `SceneNode`, and `SceneModel`. Flutter's own rebuild diffing keeps the rendered scene in sync with your widget state, the same way it keeps the UI in sync. Reach for this when app state maps cleanly onto a fixed set of objects on screen and nothing is simulation-like, for example a product configurator where a few `SceneMesh`es track some `setState` values. + +**Imperative (retained scene graph).** Own a `Scene`, add `Node`s, attach `Component`s, and display it with `SceneView(scene, camera: ..., onTick: ...)`. Track state the way you would in another game engine. The cleanest shape is a plain Dart `Game` class that owns the `Scene` and holds the game state, with the scene build and per-frame tick routed into it, and behavior living in custom `Component`s attached to nodes that the engine runs through the component lifecycle hooks. Reach for this for anything with real simulation. + +**Go imperative when** the scene has complex physics, network replication, a character walking around, or procedural generation. Any one of these means imperative. +**Stay declarative when** state maps directly to a fixed set of shown objects and nothing ticks or simulates. + +The two interoperate. A mostly-declarative scene can drop to an imperative node where it needs one, and an imperative scene can mount declarative subtrees. See `references/architecture.md` for the `Game`-class pattern, component-driven nodes, and the hybrid seam. + +## The API shape (where it diverges from what you expect) + +**Transforms.** `Node` has `position`, `rotation` (a `Quaternion`), and `scale`, but they are whole-value get/set, not the mutable spelling other engines use. Assign the whole vector (`node.position = vm.Vector3(0, 1, 0)` or `node.position += ...`). The getters return copies, so `node.position.x = 5` does nothing and throws in debug. For a raw matrix edit use `node.localTransform = matrix` or `node.mutateLocalTransform((m) => m.translateByVector3(...))`; a bare in-place edit of `node.localTransform` never moves the node, because the cache is not told. `node.forward`, `node.right`, and `node.up` return the node's unit world-space basis axes (`+Z`, `+X`, `+Y`), and `node.lookAt(target)` or `node.lookAtFrom(eye, target)` aims local `+Z` at `target`. + +**Geometry.** Ten built-in primitives (`CuboidGeometry`, `SphereGeometry`, `IcosphereGeometry`, `CylinderGeometry` with separate top/bottom radii so cones are free, `CapsuleGeometry`, `TorusGeometry`, `PlaneGeometry`, `DiscGeometry`, `RingGeometry`, `WedgeGeometry`), plus swept geometry (`ExtrudeGeometry`, `TubeGeometry`, `RibbonGeometry`), lines (`PolylineGeometry`), and `GeometryBuilder`/`MeshData` for custom meshes. Do not hand-pack a `ByteData` vertex buffer before checking these. + +**Materials.** `PhysicallyBasedMaterial` (base color, metallic, roughness, normal, emissive, plus clearcoat/sheen/transmission/etc.), `UnlitMaterial`, `ShaderMaterial` for custom shaders. Texture slots take a `TextureSource` (from `loadTexture(path)`), not a raw `gpu.Texture`. + +**Camera and coordinate basis.** `PerspectiveCamera(position: ..., target: ...)`, or `OrthographicCamera(position: ..., target: ..., projection: OrthographicProjection(size: OrthographicSize.height(12)))` for isometric, top-down, and pixel-art views. Every depth effect works under both. Prefer attaching `OrbitCameraController`, `FollowCameraController`, or `FlyCameraController` to a camera node (driven with the `CameraControls` widget) over hand-rolling spherical camera math. When driving a camera yourself, remember that view space is **left-handed (`+Z` forward into the screen, `+Y` up, `+X` right)**. +- Default camera placement sits on the `-Z` side (`(0, 0, -5)`) looking along `+Z` at the origin, where world `+X` points screen-right. If you instead place an eye on the `+Z` side (`eye = target + vm.Vector3(d * sin(a), y, d * cos(a))`) looking back along `-Z`, world `+X` points **screen-left** (`right = vm.Vector3(-cos(a), 0, sin(a))`), not `(cos(a), 0, -sin(a))`. +- Never write `forward.cross(up)` for a right vector; `vector_math`'s right-handed cross product makes `forward.cross(up)` point **screen-left**. Read `camera.right` (or `camera.horizontalRight` and `camera.horizontalForward` for `XZ` ground-plane strafing and panning) or compute `up.cross(forward).normalized()`. + +## What you are probably underestimating (it is all here) + +Models trained on older or thinner information assume flutter_scene has no lighting, no shadows, and no post-processing. It has all of it. Before hand-rolling any of these, know they exist: **directional/point/spot/area lights, shadows (PCSS, contact shadows), GTAO ambient occlusion, screen-space reflections, parallax-corrected reflection probes, SSGI, depth of field, god rays, fog, auto exposure, LUT color grading, bloom, lens flares, MSAA/SMAA/FXAA, tone mapping, instancing, LOD.** See `references/what-exists.md` for the full surface with the class names. + +Low-end and GLES-class GPUs (Raspberry Pi, web, integrated Linux) get a budget, not a different engine: `scene.renderQuality.tier` (`RenderQualityTier.low`, `medium`, `high`; null picks the platform default) decides what `AntiAliasingMode.auto` and the scene color capture budget resolve to, and `scene.renderQuality.adaptive = true` lowers the render scale and then the tier from measured frame periods when frames overrun `targetFrameRate`. Transmissive (glass) materials are the expensive thing on such devices; `scene.sceneColorCaptureBatches = 1` makes them all share one capture. + +## Traps that fail silently (wrong pixels, no error) + +- **Horizontal camera and movement inversion (`forward.cross(up)` and `+Z` orbit tangents).** Because view space has `+Z` into the screen and `vector_math.cross` is right-handed, `forward.cross(up)` yields `-right` (screen-left), and an orbit camera placed at `target + (d * sin(a), y, d * cos(a))` has screen-right `(-cos(a), 0, sin(a))`, not `(cos(a), 0, -sin(a))`. Use `camera.right`, `camera.horizontalRight`, `camera.horizontalForward`, or `up.cross(forward).normalized()`. +- **Custom `ShaderMaterial` output is linear HDR premultiplied by alpha.** No tone mapping or gamma in your shader; the `ResolvePass` applies exposure, tone mapping, and the display transform. Linearize sRGB texture samples yourself. See `MATERIALS.md`. +- **Never hand-roll a per-triangle winding flip to fix glTF orientation.** The importers handle the coordinate conversion; a manual flip leaves normals and IBL wrong. +- **Do not emit a vertex buffer at the wrong stride.** Unskinned is 72 bytes/vertex, skinned is 104; the attribute order is fixed. Use `GeometryBuilder`, do not guess the layout. +- **Surfaces that share a plane flicker as the camera moves (z-fighting), and one screenshot can look fine.** + - Never let two different-looking faces overlap in one plane. Repeated pieces abut (a piece's length equals its spacing), perpendicular runs meet at a corner post, and flat patches at one height never overlap. No depth setting fixes an exact overlap. + - An overlay that lies on a surface (a sign or screen on a wall, road paint, a decal quad, a rug) gets `material.depthLayer = 1` and wins at any distance; an overlay on that overlay gets 2. Place it flush or a few millimetres off, not at a guessed gap. + - Leave the camera's near plane at its default unless geometry clips. The engine fits the plane it rasterizes with to visible content every frame. + - Prove it in motion. `await scene.probeDepthConflicts()` names every pair of nodes that trades pixels from a camera; drive its `conflicts` to empty. `scene.debug.overlays.add(DebugOverlay.depthConflicts)` marks fights live in magenta, and debug builds print overlapping coplanar faces once the scene holds still. +- **When pixels look wrong, look at the surface before guessing.** `scene.debug.view = const DebugView(channel: SurfaceDebugChannel.roughness)` (or `uv0`, `worldNormal`, `baseColor`, `validation`, any `SurfaceDebugChannel`) replaces the lit result with that value on every material, `scene.debug.split = 0.5` compares it against the lit half, `scene.debug.overlays.add(DebugOverlay.wireframe)` traces the mesh, and `node.debugView = DebugView.none` excludes a subtree. The material channels need the views compiled in, which costs shader build time, so they are off by default: set `hooks: user_defines: flutter_scene: debug_views: true` in the app's pubspec (the workspace root's in a pub workspace), plus `flutter_scene_debug_views: true` under the app's own name if its hook calls `buildMaterials` or `buildEngineAssets`, and rebuild. Without it only the geometry and identity channels draw, and the app prints how to turn them on. A `.fmat` shows any intermediate through `material.debug` and the `custom` channel. + +## More depth + +- `references/architecture.md` for the declarative-vs-imperative choice in depth, the `Game`-class pattern, component-driven nodes, and hybrid interop. +- `references/what-exists.md` for the full API surface (the false-absence fix). +- `references/traps.md` for the complete silent-failure list. +- `references/depth-and-layering.md` for why surfaces fight, a catalog of the overlaps generated content makes, and how to find and fix them. +- The repo-root `MATERIALS.md` for the custom-shader contract. + +## Keeping this skill current + +This skill ships inside the flutter_scene package, so upgrading flutter_scene can carry a newer revision of it than the copy installed in the project. To check, run: + +```sh +dart run flutter_scene:skills --check +``` + +It reports the installed and bundled skill versions and exits non-zero when an update is available. If the installed flutter_scene ships a newer skill than what is installed, tell the user, since they are working against out-of-date guidance, and offer to update it with `dart run flutter_scene:skills` (which touches only the skill, not their build hook or pubspec). Worth a check when you start substantial flutter_scene work or when the user mentions upgrading the package. \ No newline at end of file diff --git a/.cursor/skills/flutter_scene-idioms/references/architecture.md b/.cursor/skills/flutter_scene-idioms/references/architecture.md new file mode 100644 index 00000000..4bb72229 --- /dev/null +++ b/.cursor/skills/flutter_scene-idioms/references/architecture.md @@ -0,0 +1,315 @@ +# Structuring a flutter_scene app + +Which API to build the scene with, and how to combine them. The short version is in SKILL.md; +this is the depth, with patterns that compile against 0.22.0. + +flutter_scene exposes the same scene graph two ways. The declarative widgets describe it as Flutter +widgets that rebuild-diff into the graph; the imperative API hands you the retained `Scene`/`Node`/ +`Component` graph directly. They are not competing renderers, they drive the same engine. The choice +is about how your app tracks state, and it is worth making deliberately because reworking a large +scene from one to the other is a rewrite. + +--- + +## The decision + +Go **declarative** when app state maps directly onto a fixed set of shown objects and nothing +simulates. A product configurator, a data-driven diagram, a few models whose transforms follow some +`setState` values. Flutter already owns the state, and the widgets keep the scene tracking it for +free. + +Go **imperative** when the scene simulates. Complex physics, network replication, a character +walking around under input, procedural generation. Any one of these means imperative. Here the +scene's state is the app's state, it changes every frame, and you want to own the loop rather than +express each frame as a widget rebuild. + +If you are unsure, ask whether anything in the scene changes on its own between user actions. If yes, +imperative. If the scene only changes when the user changes a value, declarative. + +--- + +## Declarative + +`SceneView.declarative` owns an internal `Scene`; its `children` are the whole scene description. + +```dart +class Configurator extends StatefulWidget { + const Configurator({super.key}); + @override + State createState() => _ConfiguratorState(); +} + +class _ConfiguratorState extends State { + // Build engine objects once, not per rebuild. Constructing GPU resources + // every build is the main performance hazard of the declarative layer. + final Geometry _geometry = CuboidGeometry(vm.Vector3(1, 1, 1)); + final PhysicallyBasedMaterial _material = PhysicallyBasedMaterial(); + double _spin = 0; + + @override + Widget build(BuildContext context) { + return Column(children: [ + Expanded( + child: SceneView.declarative( + camera: PerspectiveCamera(position: vm.Vector3(2, 2, -4)), + children: [ + SceneMesh( + geometry: _geometry, + material: _material, + rotation: vm.Quaternion.axisAngle(vm.Vector3(0, 1, 0), _spin), + ), + ], + ), + ), + Slider( + value: _spin, + max: 6.28, + onChanged: (v) => setState(() => _spin = v), + ), + ]); + } +} +``` + +The scene tracks widget state through the normal rebuild path. Note two things the example shows: +engine objects (`geometry`, `material`) are created once and held as fields (they are diffed by +identity, and rebuilding them every frame is the classic mistake), while cheap value props +(`rotation`) are fine to pass fresh each build. + +Declarative building blocks (all under `SceneView.declarative` or a `SceneView` with `children`): + +- `SceneMesh(geometry:, material:, ...)` a node with a mesh. +- `SceneNode(...)` a bare transform node, for grouping children. +- `SceneModel('assets/x.glb', animations: [...])` a loaded model (runtime glTF path). +- `SceneSubtree(parent:, children:)` mounts children under a given imperative `Node`. +- Every node widget takes `position`/`rotation`/`scale` (or a full `transform`), `visible`, + `components:` (attach imperative `Component`s), `controller:` (a `SceneNodeController` handle), and + `children:`. + +--- + +## Imperative + +Own the `Scene`, add `Node`s, attach `Component`s, display with `SceneView(scene, camera:, onTick:)`. +For anything beyond a demo, do not scatter this across a `StatefulWidget`. Put it in a plain Dart +class that owns the scene and the game state, and keep the widget thin. + +```dart +// Pure Dart, no Flutter import. Owns the scene and the game state. +class Game { + final Scene scene = Scene(); + late final Node player; + + Future load() async { + await Scene.initializeStaticResources(); + + // The camera lives in the scene as a node, not on the widget. A camera + // node's transform is its view: the translation is the eye, local +Z is + // the look direction, +Y is up. lookAtFrom sets both at once, so there is + // no view-matrix math to hand-roll. + final cameraNode = Node() + ..addComponent(CameraComponent(activateOnMount: true)) + ..lookAtFrom(vm.Vector3(0, 3, -8), vm.Vector3.zero()); + scene.add(cameraNode); + // activateOnMount makes this the scene's primary camera when the node + // mounts, so SceneView needs no `camera:` argument. (The first mounted + // camera auto-promotes anyway; this states the intent explicitly, and is + // how you pick one when several cameras exist.) + + player = Node(mesh: Mesh(CuboidGeometry(vm.Vector3(1, 1, 1)), + PhysicallyBasedMaterial())); + player.addComponent(PlayerController()); + scene.add(player); + } + + // Per-frame app logic that is not tied to one node. Component updates run + // on their own (see below), so this is for whole-game concerns. + void tick(double dt) { + // advance timers, spawn waves, read input, etc. + } +} +``` + +```dart +// Thin widget: builds the game, forwards ticks, renders the scene. +class GameView extends StatefulWidget { + const GameView({super.key}); + @override + State createState() => _GameViewState(); +} + +class _GameViewState extends State { + final Game game = Game(); + bool _ready = false; + + @override + void initState() { + super.initState(); + game.load().then((_) { + if (mounted) setState(() => _ready = true); + }); + } + + @override + Widget build(BuildContext context) { + if (!_ready) return const SizedBox.expand(); + // No `camera:` here: the view resolves the scene's active camera, which is + // the CameraComponent added in Game.load. Resolution order is the explicit + // `camera:` (absent), then `cameraBuilder`, then `scene.camera` (the active + // CameraComponent), then a default camera. + return SceneView( + game.scene, + onTick: (elapsed, dt) => game.tick(dt), + ); + } +} +``` + +### The active camera + +The scene owns which camera is active, and there are three levers: + +- **`CameraComponent(activateOnMount: true)`** (above) selects this camera when its node mounts. +- **`cameraComponent.makeActive()`** switches to it at runtime, for example a chase-cam to a + cutscene camera. Before its node mounts the choice is deferred and applied on mount. +- **`scene.camera = someCamera`** sets any `Camera` as the override directly, and `scene.camera` + reads the active one back. + +With no camera set at all, the first mounted `CameraComponent` auto-promotes, and a scene with none +still renders through a default camera. Move or rotate a `CameraComponent`'s node to move the view; +the `NodeCamera` reads the node's world transform live each frame. Aim it with `node.lookAt(target)` +(rotate toward a world point) or `node.lookAtFrom(eye, target)` (position and aim in one call); +Z is +the forward axis, so the same helpers aim lights and imported models. A follow-cam is then a one-line +component that calls `node.lookAtFrom(...)` in `update` each frame. + +### Camera controllers (interactive cameras) + +For a user-controlled camera, do not hand-roll the drag/scroll/key math: attach a camera controller +component to the camera node. `OrbitCameraController` (turntable around a target, drag rotates, scroll +dollies), `FlyCameraController` (WASD + drag free flight, `moveVertical: false` gives grounded +first-person), and `FollowCameraController` (third-person that eases behind a target node). Each holds +the camera state, eases toward it with frame-rate-independent smoothing, clamps pitch so the view +never flips, and writes the node via `lookAtFrom`. + +Wire input with the `CameraControls` widget wrapping the view; it forwards Flutter gestures and keys +to the controller. `SceneView` itself has no camera-input knobs, so nothing camera-specific leaks into +it. + +```dart +final camera = Node() + ..addComponent(CameraComponent(activateOnMount: true)) + ..addComponent(OrbitCameraController(target: vm.Vector3.zero(), distance: 8)); +scene.add(camera); + +// In build: +return CameraControls( + controller: camera.getComponent()!, + child: SceneView(scene), +); +``` + +The controllers also expose intent methods (`orbitBy`, `dollyBy`, `panBy`, `look`), so an app with its +own input handling can drive them without the widget. + +### Game input (flutter_scene_input) + +For games (rebindable controls, gamepads, pointer lock, pause menus), do not hand-roll held-key sets or +raw `gamepads` parsing: add the separate `flutter_scene_input` package. Declare typed const actions +(`VectorAction('move')`, `ButtonAction('jump')`, `DeltaAction('look')`), bind them in an `ActionSet` +with named slots (`'keyboard'`, `'gamepad'`), push the set on `player.contexts`, and call +`scene.attachInput()` so read windows advance with the scene. Read `player.button(jump).justPressed` +and `player.vector(move)` in `update`, and through `player.fixed` in `fixedUpdate`. Drivers +(`CharacterInputDriver`, `FlyCameraInputDriver`, `OrbitCameraInputDriver`) feed the bundled +controllers, and `DefaultActions` has ready-made sets. + +Traps. 2D values are +Y up everywhere, mouse movement included, so convert before passing a drag +`Offset` to `look`. A stick feeding a `DeltaAction` needs a `PerSecond` processor. Wrap the game view (not +the HUD) in `InputListener` for mouse input. Lock the cursor with `PointerLock.instance.lock()` from a +pointer or tap handler and pause when a listener sees `isLocked` go false; never add the `pointer_lock` +package. Rebinding goes through `player.overrides` and `listenForBinding`, and profiles persist with +`overrides.toJsonString()`. + +### Behavior lives in components, not in the tick + +The bulk of per-object logic should be custom `Component`s, not a giant `onTick`. A component is +attached to a node and the engine runs it through the lifecycle. Crucially, component ticks are +driven automatically by the render path, so you do not call them yourself, and `onTick` is only for +game-wide concerns that do not belong to a single node. + +```dart +class PlayerController extends Component { + vm.Vector3 velocity = vm.Vector3.zero(); + + @override + void onMount() { + // node is available here; wire up input, cache references. + } + + @override + void update(double deltaSeconds) { + // `node` is the node this component is attached to. + node.mutateLocalTransform( + (m) => m.translateByVector3(velocity * deltaSeconds), + ); + } +} +``` + +Component lifecycle hooks (subclass `Component`, override what you need): + +- `onAttach()` added to a node, before it is in a live scene. +- `Future onLoad()` async setup (await assets); mount waits for it. +- `onMount()` the node entered a live scene; `node` is usable. +- `update(double deltaSeconds)` per rendered frame. +- `fixedUpdate(double fixedDt)` fixed-step, driven by the physics accumulator when a `PhysicsWorld` + is present. Put physics-coupled logic here, not in `update`. +- `onUnmount()` / `onDetach()` teardown. + +This is the structure that scales. A character is a node with a controller component; an enemy is a +node with an AI component; a pickup is a node with a trigger component. The `Game` class holds what +is genuinely global (score, wave state, the input map), and everything spatial is a component on a +node. + +--- + +## Hybrid interop + +The two APIs share one graph, so you can mix them at the seam that suits the app. + +**Declarative shell, imperative pockets.** A declarative node accepts `components:`, so an otherwise +declarative scene can attach imperative behavior to any node without leaving the widget tree. + +```dart +SceneMesh( + geometry: _geometry, + material: _material, + components: [Spinner()], // a custom Component, ticked by the engine +) +``` + +**Imperative scene, declarative subtrees.** `SceneView(scene, children: [...])` mounts declarative +widgets over an app-owned scene. Use `SceneSubtree(parent: someNode, children: [...])` to attach a +declarative subtree under a specific imperative node, for example UI-like markers that follow a +game object. + +```dart +SceneView( + game.scene, + camera: PerspectiveCamera(position: vm.Vector3(0, 3, -8)), + children: [ + SceneSubtree( + parent: game.player, + children: [SceneModel('assets/hat.glb')], + ), + ], +) +``` + +**Reaching an imperative node from a declarative widget.** Pass a `SceneNodeController` as +`controller:` and read `controller.node` for the managed `Node` (null while unmounted). This is the +escape hatch when a declarative node needs an imperative handle for a one-off operation. + +The rule of thumb: pick the mode that matches how the *majority* of the scene is driven, then use the +seam above for the exceptions. Do not build a whole simulation out of declarative widgets to avoid +the imperative API, and do not hand-roll a diffing layer over the imperative graph to avoid the +declarative one. diff --git a/.cursor/skills/flutter_scene-idioms/references/depth-and-layering.md b/.cursor/skills/flutter_scene-idioms/references/depth-and-layering.md new file mode 100644 index 00000000..c2aa0994 --- /dev/null +++ b/.cursor/skills/flutter_scene-idioms/references/depth-and-layering.md @@ -0,0 +1,64 @@ +# Depth and layering + +Two surfaces that cover the same pixels at nearly the same depth flicker against each other as the camera moves (z-fighting). One screenshot rarely shows it, because each frame picks a winner and a different frame picks the other one. This page covers why it happens, the overlaps generated scenes make most, how to fix each, and how to prove a scene is clean. + +## Why surfaces fight + +**Exact overlaps.** Two faces in one plane built from different vertices each compute their depth from their own rounded vertex positions and from the rasterizer's sub-pixel snapping. The difference is noise whose sign changes across the surface and with any camera motion. On a surface seen at a grazing angle that noise is large, a fraction of a pixel's worth of the surface's own depth slope. No near plane, depth format, or precision fixes an exact overlap; only an order does (a layer, a content change). + +**Near overlaps.** Two parallel faces a few millimetres or centimetres apart hold their order up close and fight past the distance where the depth buffer can no longer separate them. flutter_scene spends depth precision where content is: it fits the near plane it rasterizes with to visible geometry every frame, and on Metal and most browsers it stores reversed float depth, which keeps precision nearly even with distance. The `depthGap` debug view (below) shows how far apart two surfaces must be at each pixel. + +## The overlaps generated scenes make, and the fix for each + +| Pattern | Why it fights | Do instead | +| --- | --- | --- | +| A fence, barrier, curb, or rail of repeated pieces longer than their spacing (8.4 m pieces every 8 m) | every joint holds two faces in one plane, and alternating colors make it visible | make each piece exactly as long as the spacing, or put a slightly larger post at each joint | +| Perpendicular runs that both extend into a corner | the corner holds overlapping faces | stop one run at the other's face, or cap the corner with a post | +| Ground patches, tiles, or rugs at one height that overlap each other | same-height faces of different colors | place them without overlap (`PoissonDiscSampler` in the kit), or give each overlapping kind its own `depthLayer` | +| A screen, sign, window, poster, or road marking flush with (or a few millimetres off) its surface | exact or near overlap | `material.depthLayer = 1` on the overlay; 2 for an overlay on that overlay | +| A box wrapped by a slightly larger box (window bands, trim, a frame) | parallel faces a few centimetres apart, seen from far away | keep the wrap a clear distance proud, put wraps on separate height slots so two never overlap, or bake the detail into the material | +| The same mesh added twice (a duplicated node, a model loaded into two parents) | bit-identical faces | remove the duplicate | +| `doubleSided` forced on materials whose meshes have back-to-back faces | culling no longer hides the coincident faces | leave `doubleSided` off unless the mesh needs it | +| An object placed at a guessed surface height | it hovers or sinks a few centimetres, or sits exactly in the surface | measure the surface with `scene.raycast` or the asset's bounds before placing | + +## Mechanisms, in order of preference + +1. **Fix the content.** Abut, space, or merge the pieces. Nothing fights when nothing overlaps. +2. **`Material.depthLayer`.** For an overlay that lies on a surface. A higher layer draws over coplanar surfaces with a lower one at any distance and on every backend. Each layer moves the surface a quarter pixel of its own depth slope plus a few depth steps toward the camera, in the vertex stage, so it costs nothing per fragment and moves nothing on screen by more than a fraction of a pixel. Negative layers let a ground yield to everything on it. Two different materials at the same layer that overlap still fight. +3. **`DecalNode`.** Projects a mark onto uneven surfaces without creating coplanar geometry. +4. **Bake detail into the material.** A grid of lit windows as a pattern in a `.fmat` instead of thousands of boxes on a facade. +5. **`Material.depthBias`.** A world-space nudge toward the camera. It buys fewer depth steps the farther away it is seen, so it holds up close and fails far away. Prefer `depthLayer`. +6. **`Scene.coplanarTieBreak`.** An experimental safety net for generated content. It gives every unlayered material and instance an arbitrary rank so exact overlaps resolve to a stable winner. The rank is arbitrary, so an overlay can end up consistently hidden, and surfaces a sliver apart far from the camera can swap. Fix the overlap or set `depthLayer` instead where you can. + +## The camera + +Leave `near` at its default unless geometry clips. The engine rasterizes with the largest near plane that clips nothing visible and keeps the authored one as a floor, so a chase or aerial camera gets the precision of a much larger near for free (`Scene.fitNearPlane`, on by default). The fit falls back to the authored near when the camera sits inside an item's bounds (a sky sphere, one merged city mesh) or an item has no bounds; debug builds print which node blocked it. Draw a sky with `scene.skybox` rather than a giant sphere around the camera. + +`Scene.reversedDepth` (on by default) stores depth reversed, which keeps float depth precise at every distance. Turn it off only for a custom `ShaderMaterial` vertex shader that writes clip-space depth assuming the standard mapping. + +## Proving a scene is clean + +Check in motion, with numbers, not with a screenshot. + +- **`await scene.probeDepthConflicts()`** renders the view's object ids several times (reversed draw order, changed depth rounding, sub-pixel camera offsets, and surfaces nudged a sliver apart) and reports every pair of nodes whose pixels change hands. `report.conflicts` is empty when nothing fights; `report.describe()` is a short log with node paths, pixel counts, and distances. Probe from the cameras the app actually uses, after the scene has rendered a frame. Surfaces sharing a material still count, since vertex colors or texture coordinates can tell them apart. Blended surfaces and a `.fmat` cut by its own `Surface()` are listed in `report.untested` rather than tested. + + ```dart + final report = await scene.probeDepthConflicts(camera: camera); + if (report.conflicts.isNotEmpty) debugPrint(report.describe()); + ``` + +- **`scene.findCoplanarOverlaps()`** checks the geometry without rendering and returns every pair of faces that overlap in one plane (or sit closer than the depth buffer can separate from the camera), with the overlap area, where it is, and for repeated pieces the length that fixes it. Unlike the probe, it also catches instances of one `InstancedMesh` whose colors differ overlapping each other. Debug builds run it once the scene holds still and print a summary; set `scene.debugCheckCoplanarOverlaps = false` to silence it. +- **`scene.debug.overlays.add(DebugOverlay.depthConflicts)`** marks fighting pixels live with a crawling magenta checkerboard. +- **`scene.debug.view = const DebugView(channel: SurfaceDebugChannel.depthGap)`** colors each pixel by the gap two surfaces need there to keep their order, blue for a hundredth of a millimetre through red for a metre. An overlay closer to its surface than that gap needs a layer. +- **`SurfaceDebugChannel.objectColor`** shows two node colors interleaving where they fight. + +The tells of a fight without tools: a material edit that changes nothing on screen (the other surface is winning there), a surface that looks right from one camera only, stripes or speckles that crawl as the camera moves, and flicker that only appears at grazing angles or far away. + +## Habits that do not work + +- **`Node.renderOrder` does not order opaque coplanar faces.** Depth decides between them, not draw order. +- **Turning off the depth test** draws the overlay through everything in front of it. +- **Forcing `doubleSided` broadly** exposes coincident back faces. +- **Shrinking the far plane** barely changes precision; the near plane dominates, and the engine already fits it. +- **A tiny fixed world offset** for distant content fails past a distance; use `depthLayer`. +- **Polygon offset factors** have no API here; `depthLayer` is the slope-aware offset. diff --git a/.cursor/skills/flutter_scene-idioms/references/traps.md b/.cursor/skills/flutter_scene-idioms/references/traps.md new file mode 100644 index 00000000..d36e9fb2 --- /dev/null +++ b/.cursor/skills/flutter_scene-idioms/references/traps.md @@ -0,0 +1,632 @@ +# flutter_scene silent-failure traps + +Mistakes that produce wrong pixels with no exception and no console message. Each entry gives the +mistake, what you see, and what to do instead. Sorted worst-first (most likely to hit, hardest to +diagnose from the symptom). + +Some of these are now caught by the engine in version 0.22.0. Those are tagged **[0.22.0 catches +this]** with what the engine does, so if you see that error you know what it means. The rest are +still silent, so you have to recognize them yourself. + +--- + +## 1. Editing a transform in place instead of assigning it + +**Mistake.** `node.localTransform.setTranslation(v)`, `node.localTransform..rotateY(t)`, +`node.position.x = 5`, or any edit of the matrix/vector a getter returns. This is the natural +`vector_math` style and the first thing most people reach for. + +**Symptom.** The node does not move. Not "moves wrong", nothing happens, forever, including its +children and bounds. Reading `node.localTransform`/`node.position` back shows the value you wrote, so +the state looks correct while the render disagrees. + +**Do instead.** Assign a fresh value (`node.position = ...`, `node.localTransform = node.localTransform.clone()..translateByVector3(v)`), +use the component setters `node.position`/`node.rotation`/`node.scale`, or edit the raw matrix +through `node.mutateLocalTransform((m) => m.translateByVector3(v))`, which dirties the cache for you. + +**[0.22.0 catches this]** Debug builds throw a `StateError` naming the node and the fix, both for an +in-place `localTransform` edit and for editing a copy returned by `position`/`rotation`/`scale`. + +--- + +## 2. Faces of different surfaces overlapping in one plane + +**Mistake.** Repeated pieces longer than their spacing (an 8.4 m barrier every 8 m), runs that both +extend into a corner, patches of different colors overlapping at one height, a sign, screen, or +road marking laid flush on its surface, or the same mesh added twice. + +**Symptom.** The overlap flickers between the two surfaces as the camera moves (z-fighting): stripes, +speckles, or triangles of the wrong color that crawl. Any one screenshot can look right, and no near +plane or depth setting fixes an exact overlap. + +**Do instead.** Make repeated pieces abut (length equals spacing) and cap joints with a post, keep +same-height patches apart, and give an overlay that lies on a surface `material.depthLayer = 1`. +`await scene.probeDepthConflicts()` names every pair that fights from a camera, and +`scene.findCoplanarOverlaps()` lists the overlapping faces with the length that fixes a repeated +piece. See `depth-and-layering.md`. + +**[Caught in debug]** Debug builds print the overlapping coplanar faces once the scene holds still, +naming the nodes and the fix. + +--- + +## 3. Passing a normal or metallic-roughness map as `TextureContent.color` + +**Mistake.** `material.normalTexture = await Texture2D.fromAsset('brick_normal.png')` without +`content: TextureContent.normal`. The `content` parameter defaults to `color`. + +**Symptom.** The base mip is fine, so it looks right up close and progressively wrong with distance: +normals flatten and skew, roughness reads too smooth at range, specular shimmers. A +distance-dependent symptom is nearly the worst case for screenshot-driven iteration. + +**Do instead.** Build non-color maps with the right content: `Texture2D.fromAsset(path, content: +TextureContent.normal)` for normal maps, `TextureContent.data` for metallic-roughness, AO, and other +linear data. Still silent, so this is on you. + +--- + +## 4. Non-uniform scale on a lit mesh + +**Mistake.** Any non-uniform scale on the node or an ancestor, e.g. `node.scale = Vector3(1, 3, 1)`. + +**Symptom.** Lighting, specular, and reflections are wrong across the whole mesh. It reads as a +shading or material bug, so it sends you into the materials, never the transform. + +**Do instead.** Use a uniform scale, or bake the non-uniform scale into the geometry with +`MeshData.transformed(matrix)` and build a fresh `MeshGeometry` from it. Still silent. + +--- + +## 5. Moving a skinned mesh node + +**Mistake.** `skinnedNode.localTransform = Matrix4.translation(v)` on a node that carries a `Skin`. + +**Symptom.** The mesh does not move. Worse, it *does* move when the node you transformed happens to +be an ancestor of the skeleton's joints, so it looks intermittent across models. + +**Do instead.** glTF requires a skinned mesh node's own transform to be ignored, so the engine passes +identity. Move the skeleton root (the common ancestor of `skin.joints`) instead, or parent both the +mesh node and the skeleton under a shared node and move that. Still silent. + +--- + +## 6. Replacing the transform of a runtime-imported model root + +**Mistake.** +```dart +final model = await Node.fromGlbAsset('assets/ship.glb'); +model.localTransform = Matrix4.translation(v); // wipes the handedness flip +``` + +**Symptom.** The model renders mirrored through Z (asymmetric geometry reversed, text backwards) with +normals and IBL wrong for the mirrored orientation. It still draws and is not obviously broken. + +**Do instead.** The runtime glTF importer synthesizes a root carrying a `scale(1, 1, -1)` handedness +flip. Do not overwrite it. Parent that node under a new `Node` and transform the parent, or +pre-multiply your transform by the existing `localTransform`. (The offline `.fscene`/`loadScene` path +bakes handedness into the vertices, so its roots are identity and do not have this trap.) Still +silent. + +--- + +## 7. `EnvironmentMap.fromGpuTextures` with a raw panorama + +**Mistake.** +```dart +final tex = await gpuTextureFromAsset('assets/panorama.png'); +scene.environment = EnvironmentMap.fromGpuTextures(prefilteredRadiance: tex); +``` + +**Symptom.** Every reflective surface (and the sky) shows the top 1/8 of the panorama stretched 8x +vertically, cross-fading between slices as roughness varies. Diffuse is fully black. + +**Do instead.** `fromGpuTextures` expects an already-prefiltered radiance atlas, not a plain image. +Run the source through `prefilterEquirectRadiance()` first, or just use +`EnvironmentMap.fromEquirectImageAsset(assetPath: ...)`/`fromUIImages`, which prefilter and project +SH for you. Still silent. + +--- + +## 8. `ShaderMaterial(cullingMode: none)` for a double-sided custom material + +**Mistake.** `ShaderMaterial(cullingMode: gpu.CullMode.none)`, or setting `doubleSided = true` on a +`ShaderMaterial`. The two do not agree. + +**Symptom.** `cullingMode.none` draws back faces in color but leaves them out of the depth prepass, so +SSAO, SSR, and contact shadows sample the wrong surface exactly where a back face shows: dark halos, +wrong reflections, occlusion bleeding through a leaf card. `doubleSided = true` does the opposite: the +color pass still culls while the prepass draws the extra faces. + +**Do instead.** For a truly double-sided `ShaderMaterial`, understand that the color cull and the +prepass cull are driven separately today; keep the mesh single-sided where the depth-based effects +need to match, or split it. Still silent. + +--- + +## 9. Caller-supplied `bounds` that do not cover the geometry + +**Mistake.** `MeshGeometry.fromArrays(positions: p, bounds: someAabb)` where the AABB does not contain +every position, or `setLocalBounds` with a guessed or stale box. + +**Symptom.** The mesh pops out of existence at some camera angles and reappears at others; part of a +large mesh vanishes; shadows disappear before the caster does. Intermittent and view-dependent, so a +single screenshot can look fine. + +**Do instead.** Widen the bounds to cover every vertex, or just omit the `bounds` argument and let the +constructor scan the positions. An absent bounds is safe (it means always-visible); only a wrong one +is dangerous. Still silent. + +--- + +## 10. Binding a mipless texture to a material + +**Mistake.** `material.baseColorTexture = GpuTextureSource(await gpuTextureFromAsset('brick.png'))`. +The helper's own doc even recommends this. + +**Symptom.** Severe minification aliasing: crawling and shimmering on any surface at an angle or +distance, sparkling on a metallic-roughness map. `Texture2D.fromAsset` on the same file looks fine, so +two seemingly equivalent APIs disagree visually. + +**Do instead.** Use `Texture2D.fromAsset`/`fromImage`/`fromPixels`, which generate a mip chain, or +supply a texture you mipped yourself. Reserve `gpuTextureFromAsset` for non-material uses. Still +silent. + +--- + +## 11. `RenderView.layerMask`/`Node.layers` mismatch, or a zero mask + +**Mistake.** Putting a node on a non-default layer and forgetting the view side (or vice versa), or +`layerMask: 0`, or `Node.layers = 2` meaning to select "layer 2" (which is actually `1 << 2 == 4`). + +**Symptom.** A node, a group, or the whole scene is simply absent, with no hint a mask is involved. A +`layerMask` of 0 renders nothing. + +**Do instead.** `Node.layers` is a bitmask and is NOT inherited by children, so set it on each node +you want the view to see. Use `kRenderLayerAll` to see everything, or a bitmask like `(1 << 2)`. +Match the view's `layerMask` to the nodes' `layers`. Still silent (but see #24 for the +draws-nothing diagnostic). + +--- + +## 12. Mutating a `TextureTransform` in place + +**Mistake.** `material.baseColorTextureTransform.offset.x = 0.5` instead of assigning a fresh +`TextureTransform`. Same shape as trap #1, for materials. + +**Symptom.** UV scroll/rotation animation freezes at the first value, but ONLY for materials on the +physical-variant path (any material with clearcoat/sheen/transmission/etc). The identical code works +on a plain PBR material, so it reads as a shader bug. + +**Do instead.** Assign a new transform each frame: `material.baseColorTextureTransform = +TextureTransform(offset: ...)`. Still silent. + +--- + +## 13. Environment image that is not 2:1 equirectangular + +**Mistake.** Passing a cube cross, a 1:1 angular light probe, or a cropped panorama to any environment +entry point. HDRI downloads are not reliably 2:1. + +**Symptom.** The scene is lit from wildly wrong directions, reflections show mirrored or duplicated +content, the sky is smeared. + +**Do instead.** Re-project the source to a 2:1 latitude-longitude panorama before loading. Cube +crosses and angular probes are not supported. Still silent. + +--- + +## 14. Hand-built triangles wound clockwise + +**Mistake.** Generating triangles with clockwise winding instead of counter-clockwise (CCW) winding around the outward normal when feeding `MeshGeometry.fromArrays` or `GeometryBuilder`. + +**Symptom.** The mesh is invisible from outside and visible from inside; a closed shape looks hollow or inside-out; lighting is inverted where it shows. ("See-through faces.") + +**Do instead.** flutter_scene's front faces wind COUNTER-CLOCKWISE (CCW) in model space around the outward face normal, matching glTF. Ensure triangle indices wind CCW when viewed from outside the surface, or omit `normals` and let `GeometryBuilder` derive them from your winding. Still silent. + +--- + +## 15. Out-of-range indices in `fromArrays` + +**Mistake.** `MeshGeometry.fromArrays(positions: p /* 100 verts */, indices: [0, 1, 100])`, e.g. from +an off-by-one or an index list built against a different vertex array. + +**Symptom.** Stray triangles stretching to the origin or infinity, holes, flicker. On some backends +the fetch is clamped and on others it reads adjacent memory, so the symptom differs per backend. + +**Do instead.** Keep every index in `0 .. vertexCount - 1`. (`GeometryBuilder.addTriangle` range-checks +for you and throws; the `fromArrays` index path does not.) Still silent on the `fromArrays` path. + +--- + +## 16. A `vertexCount` that does not match the buffer in `setVertices` + +**Mistake.** `geometry.setVertices(bufferView, vertexCount)` where `vertexCount` is a byte count, a +float count, or a triangle count rather than a vertex count. + +**Symptom.** Too small: part of the mesh is missing. Too large: the draw reads past the buffer, giving +stray geometry or a dropped draw depending on backend. The buffer is fine, so the investigation goes +to the packing code. + +**Do instead.** `vertexCount` is a count of vertices. Prefer `uploadVertexData` (which validates the +stride, see #18) or `fromArrays` over the caller-managed `setVertices` path unless you really own the +GPU buffer. Still silent. + +--- + +## 17. Oversized texture on a low-end device + +**Mistake.** `EnvironmentMap.fromEquirectImageAsset(assetPath: 'pano_16k.hdr', maxWidth: 16384)` or +`EnvironmentMap.radianceCubeSize = 4096` on a device whose max texture size is lower. + +**Symptom.** A completely black environment: no IBL, no reflections, black sky. Works on the dev +machine, black on a phone. + +**Do instead.** Keep environment and texture sizes within the device limit; lower `maxWidth` or +`radianceCubeSize`. Test on the lowest-end target you support. Still silent. + +--- + +## 18. Hand-packing vertex bytes at the wrong stride + +**Mistake.** `SkinnedGeometry()..uploadVertexData(bytes, vertexCount, indices)` with the wrong stride +(a common one is 96 bytes having forgotten UV1, or the legacy 80-byte layout). + +**Symptom.** Washed-out colors, see-through faces, geometry smeared toward the origin. + +**Do instead.** Unskinned vertices are 72 bytes (position 3, normal 3, tex_coords 2, tex_coords_1 2, +color 4, tangent 4, all float32), skinned are 104 (+ joints 4, weights 4). Better, do not hand-pack: +use `MeshGeometry.fromArrays`, `fromMeshData`, or `GeometryBuilder`. + +**[0.22.0 catches this]** `uploadVertexData` on both `SkinnedGeometry` and `UnskinnedGeometry` now +throws an `ArgumentError` when the byte length does not match `vertexCount * stride`, naming the +expected layout. + +--- + +## 19. Custom attribute length not matching the vertex count + +**Mistake.** `geometry.setCustomAttribute('a_wind', data, components: 3)` where `data` has the wrong +length, or set before uploading vertices, or not re-set after a `rebuild` changed the count. + +**Symptom.** The attribute is read at the wrong stride, so every vertex gets a neighbor's value: a +displacement shader shears the mesh, a color attribute smears. Nearly right, so hard to spot. + +**Do instead.** `data.length` must equal `vertexCount * components`. Set the attribute after uploading +vertices, and re-set it after any rebuild. Depth and shadow passes run the material's full vertex +variant, so an attribute-driven displacement casts its shadow. The mesh and material sides need not +match: a declared attribute the mesh lacks reads zero, and a mesh's extra attributes are ignored by +materials that do not declare them. + +**[0.22.0 catches this]** `setCustomAttribute` now throws an `ArgumentError` on a length mismatch +(once the vertex count is known). + +--- + +## 20. `UnlitMaterial` with `AlphaMode.mask` + +**Mistake.** `UnlitMaterial(colorTexture: foliage)..alphaMode = AlphaMode.mask` for cutout foliage. + +**Symptom.** No alpha test. Cutout edges render soft and blended, the material goes through the +translucent pass, writes no depth, sorts badly against itself, and casts no cutout shadow. + +**Do instead.** `UnlitMaterial` does not implement `mask` (it behaves as `blend`). Use +`PhysicallyBasedMaterial` for cutouts, or a `.fmat` unlit material that discards below your cutoff. +Still silent. + +--- + +## 21. `vertexColorWeight` on a material that took a physical variant + +**Mistake.** +```dart +final m = PhysicallyBasedMaterial()..vertexColorWeight = 0.0; +m.clearcoat = 1.0; // or sheen/transmission/anisotropy/ior != 1.5/any extension texture +``` + +**Symptom.** Vertex colors snap back to full strength the moment an unrelated extension is enabled. +On a vertex-colored import, an abrupt tint change with no plausible cause. (The same gap silently +drops `specularAntiAliasingVariance` and `specularAntiAliasingThreshold` on the variant path.) + +**Do instead.** Leave `vertexColorWeight` at 1.0 when using any advanced PBR feature, or drop the +extension. Still silent. + +--- + +## 22. Vertex-stage binding on a `ShaderMaterial` with no vertex shader + +**Mistake.** +```dart +final m = ShaderMaterial(fragmentShader: frag); +m.setUniformBlock('WaveInfo', bytes, stage: ShaderStage.vertex); // never set a vertex shader +``` + +**Symptom.** The vertex-stage parameter has no effect; geometry stays undisplaced while the fragment +stage looks right. Reads as "my vertex shader is not running." + +**Do instead.** Pass a `vertexShader` (and `skinnedVertexShader`/`depthVertexShader` for those mesh +kinds) to the constructor before binding vertex-stage blocks, or bind the block on +`ShaderStage.fragment`. Still silent. + +--- + +## 23. A `ShaderMaterial` vertex shader on line/trail/polyline geometry + +**Mistake.** Attaching a `ShaderMaterial` that supplies a vertex shader to a `LineSegmentsGeometry`, a +trail, or a polyline. + +**Symptom.** Lines vanish or explode into garbage. The unskinned vertex shader is paired with the +line-segments instanced layout and never does the ribbon expansion. + +**Do instead.** These geometries do their vertex expansion in the engine's own shader; a material +vertex shader cannot be used with them. Drop the vertex shader for line/trail/polyline geometry, or +use a mesh geometry. Still silent. + +--- + +## 24. Four different causes of a blank frame + +**Mistake.** Any of: a degenerate camera (target equals position, or `up` parallel to the view +direction, e.g. a top-down camera left at the default `up`), a field of view passed in degrees +(`fovRadiansY: 60`), an inverted or zero frustum, `layerMask: 0`, a zero-area draw region, or +rendering before `Scene.isReadyToRender`. + +**Symptom.** The entire scene is empty. Every one of these looks identical, so it is easy to "fix" +lighting, materials, and geometry for many iterations before suspecting the camera or the mask. + +**Do instead.** For a top-down/bottom-up camera set `up` to `Vector3(0, 0, 1)` or `Vector3(0, 0, +-1)`, not the default `(0, 1, 0)`. Pass FOV in radians (`60 * degrees2Radians`). Keep `near > 0` and +`far > near`. Give the view a non-zero `layerMask` and a non-empty draw region. + +**[0.22.0 catches most of this]** Degenerate cameras (zero view direction, parallel `up`, degrees-valued +FOV, degenerate near/far) assert in debug. And a frame that issues zero draw calls now prints once in +debug naming the likely cause (not ready, empty region, no views, no visible meshes, or a layer mask +matching nothing). + +--- + +## 25. Missing bounds after swapping a primitive's geometry + +**Mistake.** `mesh.primitives[0].geometry = newGeometry` for hand LOD, a rebuilt procedural mesh, or a +variant swap. + +**Symptom.** The new geometry is culled against the old geometry's bounds; if it is larger or +displaced, it pops in and out exactly like trap #9. + +**Do instead.** Nothing extra is needed anymore. + +**[0.22.0 catches this]** A `Mesh` now recomputes its bounds on its own when a primitive's geometry +identity changes, so the manual `markLocalBoundsDirty()` is no longer required. + +--- + +## 26. A `.fmat` material that overruns the 15-sampler budget + +**Mistake.** A `lit` or `physical` `.fmat` declaring several `sampler2d` parameters plus +`engine_inputs: [scene_color, scene_depth]`, on top of the lit framework's own textures. + +**Symptom.** Geometry disappears on a mid-range Android device while everything is correct on Metal, +with no build-time signal. The draw is rejected on GLES drivers reporting the 16-unit minimum. + +**Do instead.** The lit fragment shader budgets 15 fragment samplers. Pack channels into one texture +(an ORM-style atlas), drop an `engine_input`, or make the material unlit. Still silent (fails at +runtime on the device, not at build). + +--- + +## 27. `RenderView.viewport` with a `target` set + +**Mistake.** `RenderView(camera: cam, target: myRenderTexture, viewport: Rect.fromLTWH(0, 0, 0.5, 1))` +expecting a half-width render into the texture. + +**Symptom.** The view fills the entire render texture; the passed `viewport` is ignored. Reads as +"my viewport math is off." + +**Do instead.** `viewport` is ignored when `target` is set. Size the `RenderTexture` to the region you +want, or drop the target to render a sub-rect of the screen. Still silent. + +--- + +## 28. Scaled or mirrored camera node + +**Mistake.** Attaching a `CameraComponent` to a scaled node, or parenting a camera node under a scaled +one. + +**Symptom.** A uniform scale rescales the world in view; a negative scale mirrors the view, so every +surface goes back-facing and the scene renders inside out. The camera's reported `forward`/`up` look +correct, which makes it hard. + +**Do instead.** A camera node must carry only rotation and translation, and no ancestor may be scaled. +Still silent. + +--- + +## 29. Hand-built `Skin` with mismatched joints and inverse-bind matrices + +**Mistake.** `skin.joints.add(n)` without a matching `skin.inverseBindMatrices.add(...)` (both are +plain mutable lists). + +**Symptom.** Extra inverse bind matrices are silently ignored and the mesh deforms wrongly. (Too few +throws a `RangeError`, so only the extra-matrices direction is silent.) + +**Do instead.** Keep the two lists parallel: one inverse bind matrix per joint (`Matrix4.identity()` +if the joint's rest pose is the mesh's model space). Imported skins are validated; hand-built ones are +not. Still silent. + +--- + +## 30. Cloning a mesh node whose skeleton is a sibling + +**Mistake.** `meshNode.clone()` when the skeleton lives outside the cloned subtree. + +**Symptom.** The clone renders collapsed or in bind-pose garbage. There is a `debugPrint`, but it says +only "Index path formation failed" and names neither the skin nor the consequence. + +**Do instead.** Clone the common ancestor of the mesh node and its skeleton, not the mesh node alone. +Still effectively silent. + +--- + +## 31. `updateInstanceTransforms(recomputeWinding: false)` with a mirroring edit + +**Mistake.** Editing an instance transform to a negative determinant while asking the engine to skip +the parity refresh. + +**Symptom.** Those instances render inside out (front faces culled, back faces lit). + +**Do instead.** Drop `recomputeWinding: false`, or keep every instance edit orientation-preserving +(no negative/mirrored scale). Still silent. + +--- + +## 32. Flipbook frame count vs atlas grid mismatch + +**Mistake.** A `FlipbookModule(frameCount: 16)` without `emitter.flipbookColumns = 4; +emitter.flipbookRows = 4`. + +**Symptom.** Particles sample the wrong atlas cells, or only the first cell; the effect animates but +shows the wrong art. + +**Do instead.** Set `flipbookColumns * flipbookRows` equal to the module's `frameCount`. Still silent. + +--- + +## 33. `LodComponent` blend bands overlapping + +**Mistake.** A `blendRange` larger than the gap between adjacent LOD thresholds. + +**Symptom.** An object sits permanently in the wrong cross-fade pair, dither-blending two levels that +should not blend, or skipping a level. + +**Do instead.** Keep `blendRange` smaller than the smallest gap between adjacent `screenSize` +thresholds. Still silent. + +--- + +## 34. `TextureAtlas` grid not matching its texture + +**Mistake.** `TextureAtlas(columns: 16, rows: 16, tileSize: 32, padding: 2, baseColor: eightBySix)` +where the grid does not match the image, or an out-of-range tile `index`. + +**Symptom.** Every UV points at the wrong tile. With the default `repeat` addressing, an out-of-range +index in release wraps to a different valid-looking tile rather than failing. + +**Do instead.** Make the grid parameters produce exactly the texture's dimensions +(`columns * (tileSize + 2*padding)` etc), keep tile indices in range, and set +`TextureSampling.maxMipmapLevels` so tiles do not bleed across the padding gutter at high mips. Still +silent. + +--- + +## 35. `useEnvironment` sky with no cube-radiance variant + +**Mistake.** `ShaderSkySource(fragmentShader: myShader, useEnvironment: true)` with +`radianceCubeFragmentShader` left null. + +**Symptom.** The sky contributes no image-based specular on any backend that builds the cube layout +(the default nearly everywhere), so the scene loses its reflections. + +**Do instead.** Supply `radianceCubeFragmentShader`, the entry built with +`FLUTTER_SCENE_RADIANCE_CUBE`. Debug builds warn about this at bind; release builds are silent, so do +not rely on the warning. + +--- + +## 36. `radianceCubeFragmentShader` that is not the cube build + +**Mistake.** `ShaderMaterial(fragmentShader: f, radianceCubeFragmentShader: f)` (the same shader +twice), or naming the non-cube entry as the cube twin. + +**Symptom.** The engine binds a cubemap into a shader whose sampler is a `sampler2D`: nothing on some +backends, garbage specular on others. + +**Do instead.** The cube variant must be the entry compiled with `FLUTTER_SCENE_RADIANCE_CUBE`, whose +`prefiltered_radiance` sampler is a `samplerCube`. Pass the distinct `...Cube` entry from your bundle. +(The engine can only catch the identical-shader case, and only in debug.) Still effectively silent. + +--- + +## 37. Reading `int`/`bool`/`uint` shader members through `setUniformBlockFromFloats` + +**Mistake.** `setUniformBlockFromFloats('FragInfo', [1.0, 0.5])` where the shader declares `int mode; +float amount;`. + +**Symptom.** The shader reads `mode` as the float bit pattern of `1.0` (a huge integer), so every +`if (mode == 1)` branch misses and the material takes its fallback path. The block size is correct, so +nothing complains. + +**Do instead.** Pack integer members with `ByteData.setInt32` at the member's offset, or use a `.fmat` +material whose `MaterialParameters` type-checks every assignment. Unenforceable at runtime. + +--- + +## 38. A custom fragment shader that tone-maps or writes straight alpha + +**Mistake.** Ending a `ShaderMaterial`/`ShaderSkySource`/`beforeTonemap` `PostEffect` fragment +shader with `frag_color = vec4(color, alpha)` (straight alpha) or `pow(color, vec3(1.0/2.2))` +(gamma-encoded). + +**Symptom.** Straight alpha gives edge halos and over-bright overlaps. sRGB output is tone-mapped and +EOTF-encoded a second time by the resolve pass, giving washed-out low-contrast color that looks like a +bad exposure. + +**Do instead.** Output linear HDR premultiplied by alpha. Exposure, tone mapping, and the display +encode are applied later by the full-screen resolve pass. When sampling an sRGB texture, linearize +first. `.fmat` materials get the premultiply for free. Unenforceable at runtime. + +--- + +## 39. `MaterialParameters.copyStateFrom` across a changed layout + +**Mistake.** Applying a re-realized material onto a live instance whose shader layout changed (an +editor hot reload where the `.fmat` gained or lost a parameter). + +**Symptom.** Every parameter reverts to its sidecar default while `assignedValues` still reports your +overrides, so the inspector shows the right numbers and the render shows the wrong ones. + +**Do instead.** `copyStateFrom` needs both sides to come from the same compiled shader entry; remap by +name through `updateFromMetadata` across a layout change instead. Still silent. + +--- + +## 40. Environment or widget textures with sub-255 alpha + +**Mistake.** Passing an equirect image carrying alpha below 255 (an unfilled sky dome, a masked +panorama) to `fromUIImages`/`fromEquirectImageAsset`. Or, for `WidgetTexture`/`WidgetComponent`, +simply using a widget with anti-aliased or translucent edges. + +**Symptom.** For environments, diffuse ambient comes out darker than the specular reflections of the +same environment, so objects look lit by two environments. For widget textures, dark halos around +anti-aliased text and rounded corners on the zero-copy path (correct on the web readback path, so it +reads as a platform quirk). + +**Do instead.** Use opaque (alpha 255) environment sources. The widget-alpha double-multiply is a +backend difference you cannot fully control from the API; keep widget content opaque where you can. +Still silent. + +--- + +## 41. Computing a camera or node right vector with `forward.cross(up)` or `+Z` orbit tangents + +**Mistake.** Writing `final right = forward.cross(up).normalized()`, or placing an orbit camera at `target + Vector3(d * sin(azimuth), y, d * cos(azimuth))` and using `Vector3(cos(azimuth), 0, -sin(azimuth))` as its horizontal right vector. + +**Symptom.** Horizontal camera strafing (`A`/`D`) and click-drag panning move in the exact opposite horizontal direction from expected, while vertical movement (`W`/`S` and vertical drag) works normally. + +**Do instead.** `flutter_scene`'s view space is left-handed (`+Z` forward into the screen, `+Y` up, `+X` right), while `package:vector_math`'s `cross()` is right-handed, so `forward.cross(up)` yields `-right` (screen-left). Likewise, when a camera sits on the `+Z` side of the origin looking along `-Z`, world `+X` is on the left of the screen (`right = Vector3(-cos(azimuth), 0, sin(azimuth))`). Read `camera.right`, `camera.horizontalRight`, `camera.horizontalForward`, or `node.right` directly, or compute `up.cross(forward).normalized()`. Prefer `OrbitCameraController`, `FollowCameraController`, or `FlyCameraController` over hand-rolled spherical camera math. + +--- + +## Now caught by the engine, in one place + +For quick reference, these traps became loud in 0.22.0. If you hit one you get an error, not silent +wrong pixels: + +- In-place edit of `localTransform`/`position`/`rotation`/`scale` -> throws in debug (#1). +- Degenerate camera and a frame that draws nothing -> asserts/prints once in debug (#24). +- `uploadVertexData` and `setCustomAttribute` length mismatches -> throw always (#18, #19). +- A `Mesh` whose primitive geometry is swapped -> recomputes bounds itself (#25). +- Faces of different surfaces overlapping in one plane -> printed once in debug with the fix (#2). +- An `AnimationClip` binding zero of its channels -> asserts in debug naming the wanted nodes. +- A web-backend bind to a shader uniform/texture name the shader does not declare -> throws (matches + native), instead of silently sampling whatever was bound last. +- Also fixed outright: `Node.clone()` sharing the original's matrix, the skinning joints texture being + too narrow for small joint counts, and `ParticleSystem.reset()` not restarting its random stream. diff --git a/.cursor/skills/flutter_scene-idioms/references/what-exists.md b/.cursor/skills/flutter_scene-idioms/references/what-exists.md new file mode 100644 index 00000000..520428f2 --- /dev/null +++ b/.cursor/skills/flutter_scene-idioms/references/what-exists.md @@ -0,0 +1,550 @@ +# What exists in flutter_scene + +Complete public API inventory (package version 0.22.0). flutter_scene has lights, shadows, PBR +materials, instancing, LOD, skeletal animation, and a full post-processing stack. If you think a +feature is missing, it is almost certainly here under the name below. Look before you hand-roll. + +The public surface is the explicit `show` lists in `lib/scene.dart` (plus the separate barrels +`gpu.dart`, `fscene.dart`, `build_hooks.dart`, `physics.dart`, `audio.dart`). Nothing under +`lib/src` is public unless a barrel shows it. + +Import: +```dart +import 'package:flutter_scene/scene.dart'; +import 'package:vector_math/vector_math.dart' as vm; // NOT vector_math_64 +``` + +--- + +## Node and scene graph + +`Node` (`base class Node implements SceneGraph`). Construct `Node({String name = '', Matrix4? +localTransform, Mesh? mesh})`. A non-null `mesh` is wrapped in a `MeshComponent`. + +Transform API (0.22.0 added the component properties; older docs say only `localTransform` exists): + +| Member | Type | Notes | +| --- | --- | --- | +| `position` | `Vector3` get/set | Getter returns a copy; editing the copy in place throws in debug. Assign to move. | +| `rotation` | `Quaternion` get/set | Same copy rule. | +| `scale` | `Vector3` get/set | Same copy rule. | +| `localTransform` | `Matrix4` get/set | Getter returns the LIVE matrix; in-place edit throws in debug on next read. Assign a fresh matrix. | +| `mutateLocalTransform(void Function(Matrix4) edit)` | method | Edits in place AND dirties the cache. Correct raw-matrix path. | +| `globalTransform` | `Matrix4` get/set | Cached world transform; setter solves for the needed local. | +| `forward` | `Vector3` get | Unit world-space direction of the node's local `+Z` axis (the axis `lookAt` aims). | +| `right` | `Vector3` get | Unit world-space direction of the node's local `+X` axis (screen-right when viewed along `forward`). | +| `up` | `Vector3` get | Unit world-space direction of the node's local `+Y` axis. | +| `lookAt(target, {up})` | method | Orients the node's forward axis (local +Z) at a world-space target; preserves world position and scale. | +| `lookAtFrom(eye, target, {up})` | method | Positions at `eye` and aims +Z at `target` in one call (the imperative camera one-liner). | +| `Node.lookAtTransform(eye, target, {up})` | static -> `Matrix4` | The `lookAt` basis as a local transform, for `Node(localTransform:)` and declarative `transform:`. | + ++Z is the forward axis engine-wide (cameras, directional/spot lights, imported models), so the +lookAt helpers aim any of them. Compose a plain matrix with `vm.Matrix4.translation(v)`, +`vm.Matrix4.rotationY(a)`, `vm.Matrix4.compose(t, q, s)`. There is no `translate`/`rotateX` on Node. + +Hierarchy (`SceneGraph` is a mixin): `add`, `addAll`, `addMesh`, `remove`, `removeAll`. `add` throws +if the child already has a parent. `parent`, `children`, `detach()`, `getRoot()`, `getDepth()`. + +Lookup: `getChildByName(name, {excludeAnimationPlayers})`, `getChildByNamePath`, +`getChildByIndexPath`, static `getNamePath`/`getIndexPath`, `meshNodes`, `clone({recursive = true})`. + +Per-node flags: `visible` (true), `frustumCulled` (true), `layers` (`kRenderLayerDefault`, a 32-bit +mask, NOT inherited), `castsShadows` (true, not inherited), `shadowStatic` (false), `raycastable` +(true), `highlightColor` (`Vector4?`), `skin` (`Skin?`, set by importers). + +Bounds: `combinedLocalBounds`, `combinedWorldBounds`, `markBoundsDirty()`, `isVisibleTo(camera, +size)`. A `null` bounds means always-visible. + +Loading models (see Assets): `Node.fromGlbAsset`, `Node.fromGlbBytes`, `Node.fromGltfBytes`. + +Geometry readback: `extractMeshData({Matrix4? transform})` flattens the subtree to one `MeshData`. +Throws on instanced meshes, non-triangle primitives, caller-managed geometry, or an empty subtree. + +`Scene` (`base class Scene implements SceneGraph`, cannot be subclassed). See the render/lighting/ +post sections. `Mesh(geometry, material)`/`Mesh.primitives({primitives})`; `MeshPrimitive(geometry, +material)`; `Mesh.clone()` (shallow, shares geometry+material); `Mesh.localBounds`, +`Mesh.markLocalBoundsDirty()`. + +### Camera + +- `PerspectiveCamera({double fovRadiansY = 45 * degrees2Radians, Vector3? position /*(0,0,-5)*/, + Vector3? target /*(0,0,0)*/, Vector3? up /*(0,1,0)*/, double fovNear = 0.1, double fovFar = + 1000.0})`. Field names are `fovNear`/`fovFar`, NOT `near`/`far`. +- `PerspectiveCamera.framing(Aabb3 bounds, {direction, fovRadiansY, up, margin = 1.1})`. +- `PerspectiveProjection({fovRadiansY, near = 0.1, far = 1000.0})` and abstract `CameraProjection`, + `Camera`. Camera basis getters (`position`, `forward`, `up`, `right`, `horizontalForward`, `horizontalRight`) and helpers (`screenPointToRay`, `worldToScreen`, `getViewMatrix`, `getFrustum`). `right` and `horizontalRight` use `up.cross(forward)` so they always point toward the right edge of the view in the engine's left-handed view space. +- `OrthographicCamera({OrthographicProjection? projection, Vector3? position, target, up})` and + `OrthographicCamera.framing(bounds, {direction, up, margin})`. +- `OrthographicProjection({OrthographicSize size = OrthographicSize.height(10), zoom = 1.0, + Vector2? offset, near = 0.0, far = 1000.0})`, plus `.bounds(left:, right:, bottom:, top:)` and + `.matchingPerspective(fovRadiansY:, distance:)`. Sizes are FULL world extents, never half: + `OrthographicSize.height(h)`, `.width(w)`, `.contain(w, h)`, `.cover(w, h)`, `.stretch(w, h)`, + or `.pixelsPerUnit(ppu)` (logical pixels per world unit, so the scale holds as the view resizes). + Frame with `zoom`/`size`, not by moving the eye. `near` may be negative (isometric scenes that + extend behind the eye). +- Custom projections: implement `CameraProjection`. The renderer reads perspective versus + orthographic from the matrix, so effects follow. +- Node-driven: `CameraComponent({CameraProjection? projection, activateOnMount = false})` -> + `toCamera()` gives a `NodeCamera`. Camera node must not be scaled. +- Interactive cameras: `CameraController` components attached to the camera node. `OrbitCameraController` + (turntable around `target`; `orbitBy`/`dollyBy`/`panBy`/`frame`), `FlyCameraController` (WASD + drag + free flight; `moveVertical: false` = grounded first-person; `look`, `setMoveInput`, `forward`, `right`), `FollowCameraController` + (third-person easing behind `followTarget` node; `orbitBy`/`dollyBy`, `forward`, `right`). All ease with frame-rate + independent `smoothing` (settle seconds), clamp pitch short of vertical, and write the node via + `lookAtFrom`. Wire input with the `CameraControls({required controller, enabled, autofocus, child})` + widget (Focus + gestures + wheel); `SceneView` has no camera-input params by design. + +--- + +## Components + +`abstract class Component`. Lifecycle hooks (exact names): `onAttach`, `onLoad` (async), `onMount`, +`update(double deltaSeconds)` (NOT `onUpdate`), `fixedUpdate(double)`, `onUnmount`, `onDetach`, +`cloneFor(Node)`. Node side: `addComponent`, `removeComponent`, `getComponent()`, +`getComponents()`. +Game input lives in the separate `flutter_scene_input` package: `InputSystem`, `PlayerInput` +(`button`, `axis`, `vector`, `delta`, `fixed`, `contexts`, `overrides`), `ActionSet`, typed actions, +binding nodes (`ButtonBinding`, `DpadBinding`, `StickBinding`, `DeltaBinding`, `ChordBinding`, +`GatedBinding`), processors (`Deadzone`, `Scale`, `Invert`, `PerSecond`), `scene.attachInput`, +`InputListener`, `PointerLock`, `DefaultActions`, drivers, `listenForBinding`, `bindingDisplay`. + +Components tick root-first in tree order. For code that must run before every component each frame +(sampling input, applying network state), subclass `SceneTickListener` (`beforeTick(dt)`, +`beforeFixedStep(fixedDt)`, both optional) and register it with `scene.addTickListener`. + +| Component | Constructor/notes | +| --- | --- | +| `MeshComponent` | `MeshComponent(mesh)`; `mesh` get/set, `refreshMaterials()` | +| `InstancedMeshComponent` | `InstancedMeshComponent(instancedMesh)` | +| `LodComponent` | `LodComponent(List, {lodBias = 1.0, hysteresis = 0.1, blendRange = 0.0})`; extends MeshComponent | +| `CameraComponent`, `NodeCamera` | see Camera | +| `DirectionalLightComponent` | `(light)` aims down node local +Z; `.aimed(light, localDir)`; `.fromLightDirection(light)` | +| `PointLightComponent` | `(light)`; `worldPosition` | +| `SpotLightComponent` | `(light)`; `worldPosition`, `worldDirection` | +| `RectAreaLightComponent` | `(light)`; `worldPosition`, `worldRight`, `worldUp` | +| `EnvironmentVolumeComponent` | `({required settings, shape = box, extents, radius = 5.0, blendDistance = 1.0, priority = 0.0, weight = 1.0})` | +| `ReflectionProbeComponent` | `({extents = Vector3.all(5), blendDistance = 1.0, priority = 10.0, weight = 1.0, faceResolution = 128, captureOnActivate = true})`; parallax-corrected local reflections in the box; `requestCapture()` re-captures | +| `MaterialsVariantsComponent` | No public ctor. `MaterialsVariantsComponent.of(root)`/`.allOf(root)`, then `select(name)`, `variants`, `selected` | +| `SemanticsComponent` | `({label, value, hint, button, onTap, ... boundsOverride, properties})` | +| `WidgetComponent` | `({required Widget child, required Size size, pixelRatio = 1.0, worldHeight = 1.0, update = everyFrame, input = automatic, ...})`; `.bindOnly(...)` | +| `SplatComponent` | `SplatComponent(GaussianSplats)`; `opacity`, `splatScale`, `tint`, `shDegree`, `cropBox`, `cropMode` | +| `ParticleEmitterComponent` | `({required system, SpriteMaterial? material})`; `facing`, `flipbookColumns/Rows/Blend`, `paused` | +| `MeshParticleEmitterComponent` | `({required system, required List geometries, required material, facing = tumble})` | +| `TrailComponent` | `({width = 0.25, lifetime = 0.6, minVertexDistance = 0.05, maxPoints = 48, ...})`; `emitting`, `clear()` | + +`SemanticsComponent`, `SplatComponent`, particle emitters, `TrailComponent`, `WidgetInput`, +`MeshParticleFacing`, `LodLevel`, `EnvironmentVolumeShape` are all exported. + +--- + +## Geometry + +### Primitives (`primitives.dart`, all factory constructors, all `extends MeshGeometry`) + +| Class | Constructor | Facing/notes | +| --- | --- | --- | +| `CuboidGeometry` | `CuboidGeometry(Vector3 extents, {debugColors = false})` | positional extents; box `-extents/2..+extents/2`; debugColors off | +| `WedgeGeometry` | `WedgeGeometry(Vector3 size)` | triangular prism; base on `y=0`, not Y-centered | +| `PlaneGeometry` | `({width = 1.0, depth = 1.0, segmentsX = 1, segmentsZ = 1})` | XZ plane, faces +Y; no collisionShape | +| `SphereGeometry` | `({radius = 0.5, segments = 32, rings = 16})` | UV sphere | +| `CylinderGeometry` | `({bottomRadius = 0.5, topRadius = 0.5, height = 1.0, radialSegments = 32, heightSegments = 1, bottomCap = true, topCap = true})` | topRadius 0 = cone | +| `CapsuleGeometry` | `({radius = 0.5, height = 1.0, radialSegments = 32, capRings = 8})` | `height` is the mid-section; total Y = height + 2*radius | +| `TorusGeometry` | `({radius = 0.5, tubeRadius = 0.2, radialSegments = 32, tubularSegments = 16})` | XZ plane | +| `DiscGeometry` | `({radius = 0.5, segments = 32})` | XZ, faces +Y | +| `RingGeometry` | `({innerRadius = 0.25, outerRadius = 0.5, segments = 32})` | annulus, XZ, +Y | +| `IcosphereGeometry` | `({radius = 0.5, subdivisions = 2})` | subdivided icosahedron | + +Every primitive except `PlaneGeometry` has a `Shape get collisionShape`. + +### Swept/procedural (sweep a `ScenePath`; also `BezierPath`, `CatmullRomPath`, `PolylinePath`) + +- `RibbonGeometry(path, {width = 1.0, stations = 64, alignment = RibbonAlignment.ground, up, storage = fixed})`; `updatePath(path)`. `RibbonAlignment` = `ground` | `path`. +- `TubeGeometry(path, {radius = 0.5, radialSegments = 12, stations = 64, caps = true, storage})`. +- `ExtrudeGeometry(path, {required List profile, stations = 64, caps = true, storage})`. +- `PolylineGeometry(List points, {width = 8.0, widthMode = screenPixels, cap = butt, dash, perVertexWidth, perVertexColor})`. INERT until `updateForCamera(camera, viewportSize)` is called every frame. `PolylineWidthMode` = `screenPixels` | `worldUnits`; `PolylineCap` = `butt` | `round`; `DashPattern({dashLength, gapLength, cap})`. +- `LineSegmentsGeometry(LineSegmentData segments, {width = 0.01, normalOffset = 0.0})`. `extends Geometry`, GPU-expanded, no per-frame CPU work. For large independent-segment sets. +- `BillboardGeometry({capacity = 256})`. `floatsPerInstance = 14`; `BillboardFacing` = `spherical` | `axisLocked` | `velocityStretched`. + +### MeshGeometry, GeometryBuilder, MeshData + +`MeshGeometry.fromArrays({required Float32List positions, Float32List? normals, texCoords, +texCoords1, colors, tangents, List? indices, primitiveType = triangle, Aabb3? bounds, storage = +fixed, GeometryBufferArena? bufferArena, retainCpuData = true})`. Components per vertex: positions 3, +normals 3, texCoords/texCoords1 2, colors/tangents 4. Omitted normals on a triangle list are +generated. Omitted indices need a vertex count divisible by 3. `bounds` skips the position scan (it +must actually cover every vertex). + +`MeshGeometry.fromMeshData(MeshData data, {storage, bufferArena, retainCpuData})`. + +In-place update (require `GeometryStorage.updatable`, all take `{dirtyStart, dirtyCount}`): +`updatePositions`, `updateNormals`, `updateTexCoords`, `updateTexCoords1`, `updateColors`, +`updateTangents`. `rebuild({positions, normals, ...})` may change the vertex/index count. +`applyMeshData(data)`. `GeometryStorage` = `fixed` | `updatable`. + +`GeometryBuilder({deduplicate = true})`: `normal(v)`, `texCoord(v)`, `texCoord1(v)`, `color(v)`, +`tangent(v)`, `addVertex(Vector3) -> int`, `addTriangle(a, b, c)` (throws RangeError on bad index), +`packVertices()`, `build({storage, bufferArena, retainCpuData})`. Attribute setters are STICKY. +Calling `normal()` once disables generated normals for the whole mesh. + +`MeshData` (isolate-transferable, pure): `MeshData({required positions, required vertexCount, +normals, ..., customAttributes})`, `MeshData.build({required positions, ...})` (derives vertexCount, +generates normals). Derivations: `triangleCount`, `triangles`, `transformed(Matrix4)` (inverse +transpose for normals; a mirror flips winding), `toTriMeshShape()` (hollow static collider), +`toConvexHullShape()` (dynamic body), `unweld({attributes})`, `extractEdges({creaseAngleDegrees})`, +static `merge(parts)`. `MeshAttributeData(data, {components})`; `UnweldAttribute` = `centroid` | +`seed` | `triangleIndex` | `barycentric`; `LineSegmentData({positions, normals})`. + +`Geometry` base: `primitiveType`, `localBounds`, `localBoundingSphere`, `setLocalBounds(aabb, +sphere)`, `setVertices(BufferView, vertexCount)`, `setIndices(BufferView, indexType)`, +`setCustomAttribute(name, Float32List, {required components})` (1..4; depth and shadow passes read +it too, so attribute-driven displacement casts its shadow; materials that do not declare it ignore +it, and one that declares an attribute the mesh lacks reads zero), `uploadVertexData(ByteData, vertexCount, ByteData? indices, {indexType = +int16})`, `isReadable`, `extractMeshData()`, `setVertexShader`/`setVertexShaderName`, +`setVertexLayout(descriptor, {bindsModelTransform = true})`, `draw(pass, {instanceCount = 1})`. +`SkinnedGeometry`/`UnskinnedGeometry` subclasses. `GeometryBufferArena({blockSizeInBytes = 16MB})`. + +Vertex layout: unskinned 72 bytes/18 floats = position(3) normal(3) texture_coords(2) +texture_coords_1(2) color(4) tangent(4). Skinned 104 bytes/26 floats = + joints(4) weights(4). Do +not hand-pack; use `fromArrays`/`fromMeshData`/`GeometryBuilder`. + +### Instancing and LOD + +`InstancedMesh({required geometry, required material, cullInstances = false, +sortTransparentInstances = true})`: `instanceCount`, `addInstance(Matrix4, {Vector4? color}) -> int` +(clones the matrix), `setInstanceTransform(i, m)`, `updateInstanceTransforms(update, +{recomputeWinding = true})`, `setInstanceColor(i, color)`, `removeInstanceAt(i)`, `clearInstances()`. +Attach via `InstancedMeshComponent`. + +`LodLevel({required geometry, required material, required double screenSize})` (screenSize = projected +bounding-sphere diameter as a fraction of viewport height, descending, last is the cull floor). +Attach via `LodComponent`. Shadow/depth passes always draw level 0. + +--- + +## Materials and textures + +`Material` (abstract): `name`, `doubleSided` (false), `depthLayer` (0), `depthBias` (0.0), +`setFragmentShader`, `setFragmentShaderName(name, {cubeName})`, `setRadianceCubeFragmentShader`, +`isOpaque()`. `depthLayer` (-8..8) orders coplanar surfaces: a higher layer draws over a lower one +in the same plane at any distance (an overlay on a wall or road gets 1). `depthBias` is a world-unit +nudge toward the camera that buys fewer depth steps with distance; prefer `depthLayer`. `.fmat` +spells the layer `depth_layer:`. + +### UnlitMaterial + +`UnlitMaterial({TextureSource? colorTexture})`. `baseColorTexture` (field name differs from the ctor +arg), `baseColorTextureTransform`, `baseColorTextureTexCoord` (0), `alphaMode` (`opaque`; `mask` not +implemented, behaves as blend), `baseColorFactor` (white), `vertexColorWeight` (1.0). Fog applies. + +### PhysicallyBasedMaterial + +`PhysicallyBasedMaterial({baseColorTexture, metallicRoughnessTexture, normalTexture, emissiveTexture, +occlusionTexture, EnvironmentMap? environment})`. Every texture slot is a `TextureSource?` with a +`TextureTransform` and `TextureTexCoord`. + +Core: `baseColorFactor` (white), `vertexColorWeight` (1.0), `metallicFactor` (1.0), `roughnessFactor` +(1.0), `normalScale` (1.0), `emissiveFactor` (`Vector4.zero()`), `emissiveStrength` (1.0), +`occlusionStrength` (1.0), `environment` (null, falls back to `Scene.environment`), `alphaMode` +(opaque), `alphaCutoff` (0.5), `specularAntiAliasingVariance` (0.15), `specularAntiAliasingThreshold` +(0.2). + +Advanced KHR_materials_* (setting any flips onto an internal physical-variant shader; each has a +`Texture`): `specular` (1.0), `specularColor`, `ior` (1.5), `clearcoat` (0.0), +`clearcoatRoughness` (0.0), `clearcoatNormalScale`, `sheenColor` (zero), `sheenRoughness` (0.0), +`transmission` (0.0), `diffuseTransmission`, `diffuseTransmissionColor`, `thickness` (0.0), +`attenuationDistance` (inf), `attenuationColor`, `dispersion` (0.0), `iridescence` (0.0), +`iridescenceIor` (1.3), `iridescenceThicknessMinimum` (100.0), `iridescenceThicknessMaximum` (400.0), +`anisotropy` (0.0), `anisotropyRotation` (0.0). + +`isOpaque()` is false when `transmission > 0`, `alphaMode == blend`, or `baseColorFactor.a < 1.0`. + +`AlphaMode` = `opaque` | `mask` | `blend`. `TextureTransform({offset, scale, rotation})` (glTF +KHR_texture_transform order). + +### SpriteMaterial + +`SpriteMaterial({TextureSource? colorTexture})`: `colorTexture`, `tint` (white), `blendMode` +(`SpriteBlendMode.alpha` | `additive`), `softDepthFade` (0.0), `cameraNearFade` (0.0), `sampler`. +Always non-opaque, always cull none. + +### ShaderMaterial (raw GLSL escape hatch) + +`ShaderMaterial({gpu.Shader? fragmentShader, radianceCubeFragmentShader, vertexShader, +skinnedVertexShader, depthVertexShader, useEnvironment = false, cullingMode = backFace, windingOrder = +counterClockwise, isOpaqueOverride = true})`. `setVertexShader(shader, {variant = unskinned})`, +`vertexShaderFor(variant)`, `setUniformBlock(name, ByteData?, {stage = fragment})`, +`setUniformBlockFromFloats(name, List, {stage})`, `getUniformBlock`, `uniformBlockNames`, +`setTexture(name, texture, {sampler, stage})` (accepts `gpu.Texture`/`Texture2D`/`RenderTexture`), +`getTexture`, `textureNames`. `ShaderStage` = `vertex` | `fragment`; `MeshVariant` = `unskinned` | +`skinned` | `depth`. + +Fragment shaders MUST output linear HDR premultiplied by alpha (exposure, tone mapping, and the +display encode are applied later by the resolve pass). Same contract for `ShaderSkySource` and +`PostInsertion.beforeTonemap` effects. std140 packing is by hand. + +### .fmat (declarative, recommended over ShaderMaterial) + +`loadFmatMaterial(sourcePath) -> PreprocessedMaterial`, `loadFmatSky(...) -> PreprocessedSky`. +`PreprocessedMaterial`: `parameters` (`MaterialParameters`), `shadingModel`, `environment`. +`MaterialParameters` (typed, reflection-backed, throws on wrong type/name): `setFloat`, `setInt`, +`setVec2/3/4`, `setMat4`, `setColor(name, Color)`, `setTexture(name, gpu.Texture, {sampler})`, +`operator []=`, `parameterNames`, `samplerNames`, `hasUniformBlock`. + +### Textures + +`TextureSource` (interface): implementers are `Texture2D`, `RenderTexture`, `GpuTextureSource`. Every +built-in material slot takes a `TextureSource`, not a raw `gpu.Texture`. + +`Texture2D` (factories, generates a mip chain): `Texture2D.fromPixels(Uint8List, w, h, {content = +color, sampling})`, `fromImage(ui.Image, {...})`, `fromAsset(String, {content = color, sampling, +bundle})`. `TextureContent` = `color` (sRGB) | `data` (linear, e.g. metallic-roughness/AO) | `normal` +(vector-averaged). `TextureSampling({mipmaps = true, maxMipmapLevels, minFilter = linear, magFilter = +linear, mipFilter = linear, maxAnisotropy = 8, addressMode = repeat})`. + +`GpuTextureSource(gpu.Texture, {sampler})` adapts a raw texture. Barrel helpers: +`gpuTextureFromImage`, `gpuTextureFromAsset` (mipless, aliases on materials), `imageFromAsset`, +`imageFromBytes`. Cooked `.fstex`: `loadTexture(sourcePath, {package, bundle, sampling}) -> +TextureSource`, `releaseTexture`, `clearTextureCache`. + +Custom-shader GPU barrel (`package:flutter_scene/gpu.dart`): `Shader`, `ShaderLibrary`, +`loadShaderLibraryAsync` (use this, not `ShaderLibrary.fromAsset` which throws on web), +`resolveShaderBundleKey`, `Texture`, `SamplerOptions`, `MinMagFilter`, `MipFilter`, +`SamplerAddressMode`, `IndexType`, `VertexFormat`, `VertexStepMode`. + +--- + +## Lighting and environment + +Lights (all in `light.dart`, all fields mutable): + +- `DirectionalLight({direction /*(-0.3,-1,-0.2)*/, color, intensity = 3.0, priority = 0, castsShadow + = false, cacheStaticShadows = true, shadowFadeRange = 2.0, shadowSoftness = 0.08, shadowCascadeCount + = 4, shadowMaxDistance = 150.0, shadowCascadeSplitLambda = 0.6, shadowMapResolution = 1024, + shadowDepthBias = 0.02, shadowNormalBias = 0.02, shadowAmbientStrength = 0.0, shadowFilter = + rotatedPoisson, shadowCasterFaces = front, contactShadows = false, contactShadowDistance = 0.3, + angularRadius = 0.005})`. +- `PointLight({color, intensity = 1.0, range = 0.0, falloffExponent = 2.0, castsShadow = false, + shadowMapResolution = 512, shadowNear = 0.1, shadowDepthBias = 0.0, shadowNormalBias = 0.1, + shadowSoftness = 1.0, shadowCasterFaces = front})`. Shadows render six cube faces into the shared + atlas (a limited number of point casters per frame; the rest shade unshadowed). +- `SpotLight({color, intensity = 1.0, range = 0.0, falloffExponent = 2.0, direction /*(0,-1,0)*/, + innerConeAngle = 0.0, outerConeAngle = pi/4, castsShadow = false, ...})`. +- `RectAreaLight({color, intensity = 1.0, width = 1.0, height = 1.0, range = 0.0})`. Local XY plane, + emits along +Z, no shadows. +- `SunLight(SunSky source, {castsShadow = true, ...})` drives `Scene.directionalLight` from a sky. + +`Node.shadowCastingMode` (`ShadowCastingMode` = `on` (default) | `off` | `doubleSided` | +`shadowsOnly`) selects how a node's meshes cast; `Node.castsShadows` is a deprecated two-state +view of it. Every light also has `shadowCasterChannelMask` (8-bit), tested against +`Node.lightChannelMask`, to drop casters from one light's map. + +`ShadowCasterFaces` = `front` | `back` | `both`. `DirectionalShadowFilter` = `rotatedPoisson` | +`fixedPcf` | `pcss`. `ShadowCascade`, `Lighting` (per-draw state) are exported. + +Scene lighting: `Scene.directionalLight` (`DirectionalLight?`, null = IBL only; honors `direction`; +highest-priority one gets cascaded shadows), `Scene.sunLight`, `Scene.environment` (`EnvironmentMap?`, +null falls back to `EnvironmentMap.studio()`; for genuinely no IBL use `EnvironmentMap.empty()`), +`Scene.environmentIntensity` (1.0), `Scene.environmentTransform` (`Matrix3.identity()`), +`Scene.skybox` (`Skybox?`, null = transparent), `Scene.skyEnvironment`. + +### EnvironmentMap + +Carries a prefiltered specular radiance atlas AND SH-9 diffuse coefficients (one texture path, no +separate radiance/irradiance). Factories: `.empty()`, `.constantDiffuse(ambientRadiance)`, +`.fromGpuTextures({required prefilteredRadiance, diffuseSphericalHarmonics, diffuseShTexture})` (the +texture must already be prefiltered), `.fromUIImages({required radianceImage, ...})`, +`.fromEquirectHdr({required Float32List linearPixels, w, h, ...})`, +`.fromEquirectImageAsset({required assetPath, maxWidth = 4096, ...})` (auto-detects .hdr/.exr/LDR), +`.fromEquirectImageBytes(...)`, `.fromSky(SkySource, {...})`, `.studio()` (zero-config default). +Deprecated: `.fromAssets` (use `.fromEquirectImageAsset`). Env images must be equirect 2:1. +`prefilterEquirectRadiance` is exported. `Scene.loadEnvironment(assetPath, {showSkybox = true, +skyBlur = 0.0, intensity, exposure, rotationY, maxWidth = 4096, bundle})` is one-call setup. + +### Skybox/sky sources + +`Skybox(SkySource source, {intensity = 1.0})`. `SkySource` implementers: `EnvironmentSkySource({blurriness += 0.0})`, `ShaderSkySource({fragmentShader, fragmentShaderName, radianceCubeFragmentShader, +useEnvironment = false})`, `GradientSkySource({zenithColor, horizonColor, groundColor, sunDirection, +sunColor, sunSharpness = 400.0})`, `PhysicalSkySource({sunDirection, sunAngularRadius = 0.0175, +rayleighCoefficient = 2.0, mieCoefficient = 0.005, turbidity = 10.0, energy = 1.0, ...})`. +`SkyEnvironment(ShaderSkySource, {refresh = manual, interval, faceResolution = 128, equirectWidth = +512})`; `SkyEnvironmentRefresh` = `manual` | `interval` | `everyFrame`. + +### Exposure and tone mapping + +`Scene.exposure` (1.0; not 2.0), `Scene.toneMapping` (`ToneMappingMode.pbrNeutral`; also `aces`, +`reinhard`, `linear`, `agx`), `Scene.agxWhite` (16.29), `Scene.agxContrast` (1.25). Static +`Scene.physicalCameraExposure({required aperture, shutterSpeed, iso})` returns a multiplier to assign +to `exposure`. + +--- + +## Post-processing + +Every effect is a settings object on `Scene`, off by default, turned on with `enabled`. Environment +looks blend via `EnvironmentSettings` (snapshot/lerp of the whole look) and `EnvironmentVolume` / +`EnvironmentVolumeComponent` (spatial). + +| Scene field | Type | Key fields (default) | Requires | +| --- | --- | --- | --- | +| `ambientOcclusion` | `AmbientOcclusionSettings` | `method` (obscurance/`groundTruth`), `radius` (0.33), `intensity` (1.0), `power` (1.5), `bentNormals` (false), `halfResolution` (true), `indirectLight` (0.0 = SSGI), `specularMode` | perspective camera | +| `screenSpaceReflections` | `ScreenSpaceReflectionsSettings` | `intensity` (1.0), `maxDistance` (24.4), `thickness` (0.46), `stride` (9.0), `maxSteps` (90), `blur` (0.3), `debugView` | perspective camera | +| `fog` | `Fog` | `mode` (`FogMode.exponential`; also none/linear/exponentialSquared), `color`, `density` (0.02), `start`/`end`, needs both `enabled` AND non-none `mode` | any camera | +| `godRays` | `GodRaysSettings` | `intensity` (1.0), `density` (0.5), `anisotropy` (0.7), `stepCount` (24), `maxDistance` (200), `color` | shadow-casting DirectionalLight + perspective camera | +| `depthOfField` | `DepthOfField` | `focusDistance` (10.0), `fStop` (2.8), `focalLength`, `sensorHeight` (0.024), `bladeCount`, `quality` (low/medium/high) | perspective camera | +| `autoExposure` | `AutoExposureSettings` | `strength` (0.55), `compensation`, `minEv` (-4), `maxEv` (4), `speedUp` (3.0), `speedDown` (1.0); multiplies on top of `exposure` | none | +| `postProcess` | `PostProcessSettings` | see below | none | + +`AmbientOcclusionMethod` = `obscurance` (McGuire SAO) | `groundTruth` (GTAO). `SpecularAmbientOcclusionMode` += `none` | `simple` | `bentCone`. `SsrDebugView` = composite/reflectedUv/hitMask/normal/confidence/depth. + +`PostProcessSettings` (all sub-settings off by default; mutate the nested objects): +- `colorGrading` (`ColorGradingSettings`): `brightness` (1.0), `contrast` (1.0), `saturation` (1.0), + `temperature`, `tint`, `lift`/`gamma`/`gain`, `lut` (`ColorLut?`, applies after tone mapping, + independent of `enabled`), `lutBlend` (1.0). +- `chromaticAberration` (`intensity` 0.2), `vignette` (`intensity` 0.5, `radius` 0.75, `smoothness` + 0.5), `filmGrain` (`intensity` 0.3), `bloom` (`threshold` 1.0, `intensity` 0.15, `scatter` 0.7, + and `lensFlare`: `enabled` false, `intensity` 1.0, `ghostCount` 4, `ghostSpacing` 0.3, `haloRadius` + 0.35, `haloIntensity` 1.0, `chromaticAberration` 0.005; rides the bloom, needs bloom enabled). +- `customEffects` (`List`). + +`ColorLut.fromCubeString`/`.fromCubeAsset` (Adobe `.cube`, edge 2..64). + +Custom post: `PostEffect({gpu.Shader? fragmentShader, insertion = beforeTonemap, enabled = true, +useFrameInfo = false})`, added to `scene.postProcess.customEffects`. Engine binds `uniform sampler2D +input_color` at `in vec2 v_uv`. `PostInsertion` = `beforeTonemap` (linear HDR premultiplied) | +`afterTonemap` (display-referred). + +--- + +## Scene, render, and widgets + +`Scene()` (no args; calls `initializeStaticResources()`, needs a live Flutter GPU context). Methods: +`add`, `addAll`, `addMesh`, `remove`, `removeAll`, `update(dt)` (optional), `render(camera, canvas, +{viewport, pixelRatio})`, `renderViews(views, canvas, {region, pixelRatio})`, `warmUp(views, +{includeOffscreen})`, `raycast(ray, {maxDistance, layerMask, where, includeInvisible})`, +`raycastAll(...)`, `addRenderPass`/`removeRenderPass`, `captureRenderGraph({viewIndex, request, +timeout})`, `captureEnvironment({required position, faceResolution = 128, equirectWidth = 512, +layerMask})` -> `EnvironmentMap` (one-shot static capture; use `ReflectionProbeComponent` for a +node-anchored, parallax-corrected, auto-blended probe). Statics: `Scene.initializeStaticResources()`, +`Scene.preload({physicalMaterials = true, smaa = false})` (loads the shaders behind PBR extensions +and `ShadowCatcherMaterial`, and the SMAA tables, which otherwise load on first use), +`Scene.isReadyToRender`, `Scene.physicalCameraExposure`, `Scene.isAntiAliasingModeSupported`, +`Scene.effectiveAntiAliasingMode`. + +`Scene.antiAliasingMode` (`AntiAliasingMode.auto` -> msaa or fxaa; also `none`, `msaa`, `fxaa`, +`smaa`, `taa`), `Scene.smaa` (`SmaaSettings`: threshold, search steps, corner rounding), +`Scene.temporalAntiAliasing` (`TemporalAntiAliasingSettings`), +`Scene.renderScale` (1.0), `Scene.filterQuality` (`FilterQuality.medium`), `Scene.views` +(`List` for RenderTexture targets). + +`RenderView({required Camera camera, RenderTexture? target, Rect? viewport /*normalized 0..1, +ignored when target set*/, int layerMask = kRenderLayerAll, order = 0, AntiAliasingMode? +antiAliasingMode, double? renderScale, FilterQuality? filterQuality, List cullingPlanes})`. +`kRenderLayerDefault = 1`, `kRenderLayerAll = 0xFFFFFFFF`. + +`RenderTexture`, `RenderTextureSampling`, `RenderTextureUpdate`, `RenderTextureView(renderTexture, +{fit = contain, filterQuality = medium, followLayout = false})`. + +Widgets: +- `SceneView(Scene scene, {Camera? camera, SceneCameraBuilder? cameraBuilder, SceneViewsBuilder? + viewsBuilder, autoTick = true, pixelRatio, onTick, loading, loadingBuilder, revealMinDuration, + warmUp = false, children})`. App-owned scene; does not write scene properties. `camera`, + `cameraBuilder`, `viewsBuilder` are mutually exclusive. +- `SceneView.declarative({environment, environmentIntensity = 1.0, exposure = 1.0, toneMapping = + pbrNeutral, camera, cameraBuilder, viewsBuilder, children, ...})`. View-owned scene. +- `SceneViewsBuilder` is exported as of 0.22.0 (older docs list it as a trap; it is public now). +- Declarative widgets: `SceneNode`, `SceneMesh`, `SceneModel`, `SceneSubtree`, `SceneNodeHost`, + `SceneNodeController`, `SceneModelSource`, `AssetModelSource`, `MemoryModelSource`, + `SceneAnimationSpec`. `WidgetTexture`, `WidgetTextureController`, `WidgetUpdatePolicy`. `SceneScope`. +- Camera resolution precedence: `camera` -> `cameraBuilder(elapsed)` -> `scene.camera` (or first + mounted `CameraComponent`) -> default `PerspectiveCamera()`. + +`CustomRenderPass`, `RenderInput`, `RenderPassContext`, `RenderStage`, `TransientWriter`, +`NodeFilter`, `HighlightStyle`, render-graph capture types (`CapturedPass`, `CapturedResource`, +`CapturedDraw`, `CapturedSkip`, `RenderGraphCaptureRequest`, `RenderGraphCaptureResult`) are all +exported. + +Debugging and profiling (all exported, all on every backend): +- `scene.renderStats.latest` (`RenderFrameStats`): per-frame counters (draws, instances, vertices, + culled, batches, pipeline binds and builds) per view and per pass with CPU micros, plus + `history`. Always on; GPU times are null until the engine exposes timestamp queries. Each pass + also emits a `dart:developer` timeline event, so DevTools shows the frame. +- `Scene.debugAllowRenderGraphCapture = true` then `scene.captureRenderGraph()` captures one frame: + passes with data flow and thumbnails, and every draw (`pass.draws`) with node path, material, + shader names, pipeline id, counts, batch size, `batchBreak` reason, and the uniform blocks bound + for it (`block.decode(draw)` names the members once `ShaderReflection.loadAll()` has run). + `result.toJson()` / `RenderGraphCaptureResult.fromJson` round-trip a capture as JSON. +- `ShaderReflection.loadAll()` parses every loaded shader bundle; `ShaderReflection.infoFor(shader)` + gives inputs, uniform block layouts, texture bindings, and `sourceOf(shader)` the compiled MSL, + GLSL, or SPIR-V for any backend the bundle holds. `FmatCompileException.diagnostics` parses + compiler errors with line numbers; `shaderSourceWindow` renders the marked source around one. + +--- + +## Assets and animation + +Setup: `flutter pub add flutter_scene` then `dart run flutter_scene:init`. Enable Flutter GPU with +`flutter run --enable-flutter-gpu` (native only; nothing for web). Requires Flutter 3.47 stable+, NOT +master. Impeller is default; do not pass `--enable-impeller`. Never pass +`--enable-experiment=native-assets` (breaks the build on Dart 3.10+). + +Two model-loading paths, do not conflate: + +- Pipeline (preferred, needs the `buildScenes` hook): `loadScene(sourcePath, {package, bundle, + registry, onReload, applyStageTo}) -> Future`. `sourcePath` is the SOURCE path relative to + the package root (e.g. `'assets/level.glb'`), NOT a generated name. Companions: + `loadSceneSubtree`, `releaseScene`, `clearSceneTemplateCache`. +- Runtime glTF (no hook, parses every load): `Node.fromGlbAsset(assetPath)`, + `Node.fromGlbBytes(bytes)`, `Node.fromGltfBytes(gltfJson, {required resolveUri})`. Each synthesizes + a root node. + +Sibling loaders: `loadTexture` (`.fstex`), `loadFmatMaterial`/`loadFmatSky` (`.fmat`). + +Build hooks (`package:flutter_scene/build_hooks.dart`): `buildScenes({buildInput, buildOutput, +inputFilePaths, discoveryRoot = 'assets/', assetMode = generatedTree, compressTextures = false})`, +`buildMaterials({...})`, `buildTextures({..., required textures, contents})`, `buildEngineAssets`, +`buildTargetShaderBundleJson`. Outputs land in `flutter_scene_generated/` (never commit +`.fsceneb`/`.shaderbundle`/`.fmat.json`/`.fstex`). Removed 0.21.0: `legacyOnly`, +`dataAssetsIfAvailable`, `outputDirectory`. + +Animation (`Animation`, `AnimationClip`, `AnimationPlayer` exported): + +- Off a loaded model: `node.parsedAnimations`, `node.findAnimationByName(name)`, + `node.createAnimationClip(animation)`, `node.removeAnimationClip(clip)`. Clips start paused at t=0; + call `play()`. +- `AnimationClip`: `playbackTime` (assignment is seek), `playbackTimeScale` (1; negative reverses), + `weight` (0..1), `playing`, `loop`. `play()`, `pause()`, `stop()`, `replay()`, `gotoAndPlay(t)`, + `seek(t)`, `advance(dt)`, `rebind(newTarget, {animation})`. Channels bind by node NAME; channels + whose node is absent from the subtree are dropped (0.22.0 asserts in debug when ALL channels drop). +- `AnimationPlayer`: `createAnimationClip(animation, bindTarget)` (a second call with the same + `Animation.name` replaces), `getClipByName`, `rebind`, `update(dt)` (auto-driven per frame). +- `Animation({name, channels})`, `AnimationChannel`, `BindKey({required nodeName, property = + translation})`, `AnimationProperty` = `translation` | `rotation` | `scale`. +- Declarative: `SceneModel(assetPath, animations: [SceneAnimationSpec(name, {playing = true, loop = + true, weight = 1.0, speed = 1.0})])`. Note `SceneModel` loads via the runtime glTF path. + +The engine-agnostic scene-document core is a separate package `scene` (0.2.0), re-exported through +`package:flutter_scene/fscene.dart`. `flutter_scene_importer` and `flutter_gpu_shim` no longer exist +(folded in). Physics and audio are separate barrels (`physics.dart`, `audio.dart`). + +## Depth precision and z-fighting + +Defaults that keep depth precise: `Scene.reversedDepth` (true; reversed float depth on Metal and +browsers with clip control) and `Scene.fitNearPlane` (true; the near plane the passes rasterize with +is fitted to visible content each frame, the authored near is the floor, and the public projection is +untouched). `Scene.coplanarTieBreak` (false, experimental) gives unlayered materials and instances a +stable order where they overlap exactly. See `depth-and-layering.md`. + +Finding fights: `scene.probeDepthConflicts({camera, width = 960, height = 540, layerMask, minPixels += 4})` -> `Future` (`conflicts` of `DepthConflict` with `nodeA`, `nodeB`, +`pixelCount`, `bounds`, `distance`; `conflictPixelCount`; `describe()`), rendered from the camera. +`scene.findCoplanarOverlaps({camera})` -> `List` (`nodeA`, `nodeB`, `instanceA`, +`instanceB`, `area`, `separation`, `center`, `normal`, `hint`, `exact`, `describe()`), from geometry +with no rendering; debug builds run it once the scene settles and print a summary +(`scene.debugCheckCoplanarOverlaps = false` silences it). `DebugOverlay.depthConflicts` marks fights +live, and `SurfaceDebugChannel.depthGap` shows the gap two surfaces need at each pixel. + +## Debugging the surface + +`Scene.debug` (a `SceneDebugSettings`): `view` is a `DebugView` over a `SurfaceDebugChannel` (geometry attributes, resolved surface channels, physical fields, object and material identity colors, validation flags, a `custom` channel fed by `material.debug` in a `.fmat`), with `gain`, a scalar range, and a `DebugRangePolicy`; `split` compares the view against the lit image; `overlays` holds `DebugOverlay.wireframe` and `DebugOverlay.depthConflicts`. `Node.debugView` overrides or excludes a subtree. The material channels are compiled in only with flutter_scene's `debug_views` hook user-define (`hooks: user_defines: flutter_scene: debug_views: true`, plus `flutter_scene_debug_views: true` under the app's name when its hook builds materials or engine assets); without it every material draws through the fallback, which serves the geometry and identity channels. `DebugViewRegistry` lists every view by id for tools, and `Scene.debugViewId` selects one by id. Raw `ShaderMaterial`s opt in with `debugViews: true` after defining `FLUTTER_SCENE_DEBUG_VIEWS` and including `material_debug.glsl`; those that do not are drawn by a fallback that stripes the material channels. diff --git a/.cursor/skills/flutter_scene-kit/SKILL.md b/.cursor/skills/flutter_scene-kit/SKILL.md new file mode 100644 index 00000000..a931dfd1 --- /dev/null +++ b/.cursor/skills/flutter_scene-kit/SKILL.md @@ -0,0 +1,157 @@ +--- +name: flutter_scene-kit +version: 8 +description: Build interactive 3D gameplay, character controllers, camera rigs, dynamic day/night cycles, water surfaces, audio, pooling, and debug overlays in flutter_scene. Use when creating game mechanics, camera controls, NPC behaviors, atmospheric environments, or diagnostic HUDs. +--- + +# Gameplay, camera, and atmosphere kit in flutter_scene + +Flutter Scene provides high-level gameplay components and ergonomic building blocks in `package:flutter_scene/kit.dart` so games and interactive experiences do not need to re-implement standard mechanics from scratch. + +When choosing components, consider existing engine alternatives: +- For physics-driven character navigation with collider capsules, wall sliding, and autostep, use `KinematicCharacterController` from `package:flutter_scene/physics.dart`. +- For interactive mouse/touch orbit cameras with inertia, use `OrbitCameraController` or `FollowCameraController`. +- For framing a standalone `PerspectiveCamera`, use `PerspectiveCamera.framing`. Use `BoundsFraming` when computing a transform for a `NodeCamera` mounted in the scene graph. +- When driving custom camera-relative movement or ground panning (WASD strafing, RTS/strategy camera drag), read `camera.horizontalRight` and `camera.horizontalForward` (or `camera.right` and `node.right`). Never write `forward.cross(up)`, which points screen-left in `flutter_scene`'s left-handed (`+Z` forward) view space and horizontally inverts `A`/`D` and drag controls. + +## Imports + +```dart +import 'package:flutter_scene/scene.dart'; +import 'package:flutter_scene/kit.dart'; +import 'package:vector_math/vector_math.dart' as vm; +``` + +## Camera rigs and smoothing + +### SpringArmComponent + +`SpringArmComponent` attaches to a target character node and mounts a camera node at the arm's socket. It casts rays against the scene hierarchy to prevent geometry clipping, smoothly pulling the camera inward when colliding with walls. + +Note on offsets: `targetOffset` is applied in world space from the character node's origin, and `socketOffset` acts in the camera socket's local plane along X (right) and Y (up). + +Leave the mounted camera's near plane at its default. Every pass rasterizes with a near plane fitted to visible content each frame, so a chase or follow camera gets the depth precision of a much larger near without clipping the character when the arm pulls in. Raising `near` by hand clips the character at close range and gains nothing; the fit falls back to the authored value only when the camera sits inside an item's bounds (a sky sphere), so draw skies with `scene.skybox`. + +```dart +final characterNode = Node(); +final cameraNode = Node(); +final cameraArm = SpringArmComponent( + targetLength: 5.0, + targetOffset: vm.Vector3(0, 1.6, 0), // Eye height + socketOffset: vm.Vector3(0.5, 0, 0), // Over-the-shoulder + enablePositionLag: true, + positionLagSpeed: 8.0, + cameraNode: cameraNode, +); + +characterNode.addComponent(cameraArm); +scene.root.add(characterNode); +scene.root.add(cameraNode); +``` + +### CameraShake + +`CameraShake` implements a trauma-decay model driven by deterministic simplex noise for organic multi-axis camera shake (explosions, footsteps, hits). + +```dart +final shake = CameraShake(decayRate: 1.2, frequency: 25.0); + +// Add trauma on hit +shake.addTrauma(0.6); + +// Inside game loop +final offset = shake.update(deltaSeconds); +cameraNode.localTransform = baseTransform * offset.toMatrix4(); +``` + +## Character movement and steering + +### ThirdPersonControllerComponent + +`ThirdPersonControllerComponent` handles kinematic movement, sprint multipliers, turn smoothing, ground snapping with raycasts, slope sliding, coyote time, and buffered jumps. Input expects `+Y` as forward in 3D. + +```dart +final playerNode = Node(); +final controller = ThirdPersonControllerComponent( + walkSpeed: 4.5, + runMultiplier: 1.8, + jumpVelocity: 7.0, + groundPlaneHeight: 0.0, // Optional fallback floor +); +playerNode.addComponent(controller); + +// Pass rotatesToMovement: false to move the node without turning it; read +// controller.yaw to drive a child mesh or animation from the heading instead. +// When using VirtualJoystick (where up is -Y in screen space), invert Y: +// controller.setMoveInput(vm.Vector2(joystickDir.x, -joystickDir.y), isRunning: isSprinting); +if (jumpPressed) controller.jump(); +``` + +### Autonomous Steering Behaviors + +`Steering` provides math helpers for NPC navigation, flocking, and crowd dynamics. + +```dart +// Seek target +final seekForce = Steering.seek(npcPos, npcVel, targetPos, maxSpeed: 4.0); + +// Arrive smoothly +final arriveForce = Steering.arrive(npcPos, npcVel, targetPos, slowingRadius: 3.0); + +// Flocking separation +final sepForce = Steering.separation(npcPos, npcVel, neighborPositions, desiredDistance: 1.5); +``` + +## Dynamic environments and atmosphere + +### DayNightCycleComponent + +`DayNightCycleComponent` moves the sun along a realistic solar arc given latitude and time of day, evaluating sun colors, intensities, and ambient lighting transitions. + +```dart +final sunLight = DirectionalLight(); +final sunNode = Node()..addComponent(DirectionalLightComponent(sunLight)); +scene.root.add(sunNode); + +final skyCycle = DayNightCycleComponent( + timeOfDay: 14.5, // 2:30 PM + timeSpeed: 0.1, // Progress 0.1 hours per second + latitude: 34.0, + sunLightNode: sunNode, +); +scene.root.addComponent(skyCycle); +``` + +### WaterSurfaceComponent + +`WaterSurfaceComponent` evaluates multi-harmonic Gerstner trochoidal waves for water surfaces and floating buoyancy queries. + +```dart +final water = WaterSurfaceComponent(); +final surface = water.evaluateAt(vm.Vector2(playerPos.x, playerPos.z)); +final waterHeight = surface.displacement.y; +final waterNormal = surface.normal; +``` + +## Immediate-mode debug visualization + +`DebugDraw` provides static immediate-mode line, ray, box, sphere, axis, and physics collider drawing utilities for physics debugging and AI visualizers. + +```dart +DebugDraw.line(startPos, endPos, color: vm.Vector4(1, 0, 0, 1)); +DebugDraw.box(aabb, color: vm.Vector4(0, 1, 0, 1)); +DebugDraw.sphere(center, 1.0, color: vm.Vector4(0, 0, 1, 1)); +DebugDraw.axes(node.globalTransform, size: 2.0); + +// Every collider under a node, posed the way the simulation sees it. +// Triggers draw in triggerColor. DebugDraw.shape draws a single posed Shape. +DebugDraw.colliders(scene.root); + +// Render debug lines: one updatable geometry, rebuilt in place each frame. +final debugGeometry = DebugDraw.createGeometry(); +debugNode.mesh = Mesh(debugGeometry, UnlitMaterial()); +// Per frame, after the DebugDraw calls: +DebugDraw.flushInto(debugGeometry); +``` + +`DebugDraw.flushMesh()` builds a new geometry per call; it suits a one-off capture, not a per-frame loop. diff --git a/.cursor/skills/flutter_scene-looks/SKILL.md b/.cursor/skills/flutter_scene-looks/SKILL.md new file mode 100644 index 00000000..bbce45d7 --- /dev/null +++ b/.cursor/skills/flutter_scene-looks/SKILL.md @@ -0,0 +1,166 @@ +--- +name: flutter_scene-looks +version: 5 +description: Give a flutter_scene render a deliberate, polished look. Use this whenever a scene looks flat, dull, or washed out, or whenever the ask is to make it look good, because a good look is lighting plus post-processing, not geometry. Ships copy-paste EnvironmentSettings presets that configure the whole stack coherently. +--- + +# Making flutter_scene look good + +The single most common reason a flutter_scene render looks amateur is that the post-processing stack was left at defaults. flutter_scene has a deep lit-and-post pipeline (image-based lighting, tone mapping, bloom, lens flares, ambient occlusion, screen-space reflections, fog, god rays, depth of field, color grading, vignette, film grain). Out of the box almost all of it is off, so a bare scene is technically correct and visually flat. + +**The insight: a polished look is lighting and post-processing, not geometry.** Better meshes will not fix a flat render. Do not spend effort on modeling detail when the scene reads dull; spend it on the look. And a look is a *coherent* set of choices, not twelve knobs turned independently. Turning bloom, AO, SSR, fog, grain, and grading up one at a time, each by feel, lands in muddy incoherent territory. Pick one deliberate preset and paste it whole. + +## Apply a look in one line + +Every scene-wide look field lives on one aggregate value, `EnvironmentSettings`, assigned through a single setter: + +```dart +scene.environmentSettings = EnvironmentSettings(/* fields below */); +``` + +That one assignment configures tone mapping, exposure, image-based-lighting intensity, and the entire post stack, **including** fog, god rays, depth of field, and auto exposure. You do not need to touch `scene.fog`, `scene.godRays`, `scene.depthOfField`, or `scene.autoExposure` separately; those are the live per-effect objects, but `EnvironmentSettings` carries all of their fields and applies them for you. Every effect is off by default, so a preset only names the fields it turns on. + +`EnvironmentSettings` is also a blendable snapshot. Read the current look with `scene.environmentSettings`, and cross-fade two looks with `EnvironmentSettings.lerp(a, b, t)` driven from an animation. That is how you transition day to night or ramp an effect in. + +## Lights are separate, and still matter + +`EnvironmentSettings` covers image-based lighting (the `environment` map) and the whole post stack, but **direct lights are not part of it.** Shadows, god rays, and any strong key light come from the scene's lights, set separately: + +```dart +scene.directionalLight = DirectionalLight( + direction: vm.Vector3(-0.4, -1.0, -0.3), + intensity: 4.0, + castsShadow: true, // shadows are off until you ask +); +``` + +An unset `scene.environment` still resolves to a default studio IBL, so a `PhysicallyBasedMaterial` is always lit. But a scene with no direct light is soft and shadowless. God rays require a shadow-casting `DirectionalLight`; shadows require `castsShadow: true` on the light. + +## The four looks + +Paste one whole. Each is a real `EnvironmentSettings` literal; import `package:vector_math/vector_math.dart as vm` for the `Vector3` color fields. AO, SSR, god rays, and depth of field work with perspective and orthographic cameras. + +### showcase + +Clean, bright product-viz beauty. Punchy tone mapping, soft bloom on highlights, grounded contact occlusion, and real reflections. The default reach-for-it look. + +```dart +scene.environmentSettings = EnvironmentSettings( + toneMapping: ToneMappingMode.aces, + exposure: 1.0, + bloomEnabled: true, + bloomThreshold: 1.1, + bloomIntensity: 0.2, + bloomScatter: 0.7, + ambientOcclusionEnabled: true, + ambientOcclusionMethod: AmbientOcclusionMethod.groundTruth, + ambientOcclusionBentNormals: true, + ambientOcclusionSpecularMode: SpecularAmbientOcclusionMode.bentCone, + ambientOcclusionIntensity: 1.0, + screenSpaceReflectionsEnabled: true, + screenSpaceReflectionsIntensity: 1.0, + vignetteEnabled: true, + vignetteIntensity: 0.25, +); +``` + +### stylized + +Vivid and graphic. Saturated, slightly warm, glowing, flatter shading (no heavy occlusion or reflections). For playful or illustrative scenes. + +```dart +scene.environmentSettings = EnvironmentSettings( + toneMapping: ToneMappingMode.aces, + colorGradingEnabled: true, + saturation: 1.25, + contrast: 1.1, + brightness: 1.05, + temperature: 0.1, + bloomEnabled: true, + bloomThreshold: 0.9, + bloomIntensity: 0.28, + bloomScatter: 0.8, + vignetteEnabled: true, + vignetteIntensity: 0.2, +); +``` + +### moody + +Dark, cinematic, atmospheric. Lower exposure, cool graded, foggy, heavy vignette, subtle grain and aberration, deep occlusion. God rays if the scene has a shadow-casting sun. Use a cool horizon-colored fog. + +```dart +scene.environmentSettings = EnvironmentSettings( + toneMapping: ToneMappingMode.aces, + exposure: 0.8, + colorGradingEnabled: true, + contrast: 1.15, + saturation: 0.9, + temperature: -0.1, + fogEnabled: true, + fogMode: FogMode.exponential, + fogColor: vm.Vector3(0.05, 0.06, 0.09), + fogDensity: 0.03, + ambientOcclusionEnabled: true, + ambientOcclusionMethod: AmbientOcclusionMethod.groundTruth, + ambientOcclusionIntensity: 1.2, + ambientOcclusionPower: 1.8, + vignetteEnabled: true, + vignetteIntensity: 0.6, + vignetteRadius: 0.6, + filmGrainEnabled: true, + filmGrainIntensity: 0.25, + chromaticAberrationEnabled: true, + chromaticAberrationIntensity: 0.15, + godRaysEnabled: true, // needs scene.directionalLight with castsShadow: true + godRaysIntensity: 1.0, + godRaysDensity: 0.6, + godRaysColor: vm.Vector3(1.0, 0.95, 0.85), +); +``` + +### clean + +Neutral and honest. Minimal post, no grading, no bloom, no vignette, just correct tone mapping and gentle grounding occlusion. The right look for an editor, an inspector, a UI-embedded viewer, or anywhere you want an accurate read of the actual material. + +```dart +scene.environmentSettings = EnvironmentSettings( + toneMapping: ToneMappingMode.pbrNeutral, // the engine default + exposure: 1.0, + ambientOcclusionEnabled: true, + ambientOcclusionIntensity: 0.8, + ambientOcclusionHalfResolution: true, +); +``` + +## Tuning from a preset + +Start from the nearest look, then move one field at a time. + +- **Too dim or too bright overall.** Change `exposure` (default 1.0), not per-light intensity. Or turn on `autoExposureEnabled: true` to let the scene meter itself. +- **Highlights not glowing.** Lower `bloomThreshold` toward 1.0 or below, or raise `bloomIntensity`. `bloomScatter` widens the glow. +- **Want a lens flare off a bright source.** Turn on `lensFlareEnabled` (needs `bloomEnabled`). Keep `lensFlareIntensity` modest and drop `lensFlareHaloIntensity` first if the flare washes the frame; the halo is the broad wash, the ghosts are the crisp chain. +- **Reads flat and ungrounded.** `ambientOcclusionEnabled: true`. Use `AmbientOcclusionMethod.groundTruth` for quality, keep `ambientOcclusionHalfResolution: true` for cost. +- **Colors feel wrong.** Turn on `colorGradingEnabled` and reach for `saturation`, `contrast`, `temperature`, `tint` before anything else. +- **Wrong tone-map feel.** `ToneMappingMode.aces` is contrasty and filmic, `pbrNeutral` (default) preserves hue and saturation, `agx` is the most neutral highlight rolloff. Do not reintroduce an `exposure: 2.0` hack; that was an artifact of an older renderer. + +## Micro-surface metrics and anti-waxiness tuning + +When tuning procedural materials or custom shaders, use surface metrics and exposure discipline to eliminate waxiness, plastic reads, or harsh aliasing: + +- **Surface reads like smooth plastic.** Increase high-frequency micro-scale variation. Low 1-pixel luminance gradients `(|dL/dx| + |dL/dy|) / 2` indicate untextured or overly smooth surfaces. +- **Blotchy macro clouds.** Balance high-frequency and low-frequency energy ratios (`hf/lf`). High variance with low fine detail indicates large-scale noise patches without adequate surface grain. +- **Harsh normal-map glitter or torn noise.** Check terminator crossing behavior under low grazing light angles (sun low on the horizon). Over-amplified normal maps flip adjacent pixels between full light and full shadow. +- **Colors look washed out in bright areas.** Tone mapping curves compress channel differences near the shoulder. A surface reading high brightness with low saturation is often over-exposed rather than under-pigmented; reduce exposure to restore natural material saturation. + +## Look tools that are not on EnvironmentSettings + +Anti-aliasing, reflection probes, and planar reflectors affect the look but are set outside `EnvironmentSettings`, so a preset does not turn them on. + +- **Anti-aliasing** is a `Scene` field. The default `AntiAliasingMode.auto` already picks `msaa` where the backend supports it and `fxaa` otherwise, so edges are handled. Reach for `scene.antiAliasingMode = AntiAliasingMode.smaa` when `fxaa` looks mushy (it blurs texture detail) and `msaa` is unavailable; SMAA keeps edges clean without the blur, at ~3x fxaa cost. +- **Reflection probes** capture true local reflections that SSR cannot, because SSR only reflects what is on screen. Attach a `ReflectionProbeComponent` to a node placed at the reflective spot (a room, a mirror ball); it captures the surroundings into a parallax-corrected box and blends with the environment. The capture renders the scene six times, so it happens once on activate (or on an explicit `requestCapture()`), never per frame. +- **Planar reflectors** render a true per-frame mirror for one flat surface (a mirror, a glossy floor), which neither SSR nor a probe can produce. Attach a `PlanarReflectorComponent` to the mirror node and give the surface a `.fmat` material declaring `engine_inputs: [ planar_reflection ]`; the component renders the scene once more per frame from the reflected camera and the material samples it with `GetPlanarReflection()`. That extra scene render is the cost, so bound it with `resolutionScale` (default 0.5) and `layerMask`. See `references/looks.md` for all three. + +## Cost + +The post stack is not free. AO, SSR, depth of field, and god rays each add screen-space passes, and mobile and web are the budget. Keep `ambientOcclusionHalfResolution: true`, prefer `DepthOfFieldQuality.low` on mobile, and do not stack SSR plus god rays plus depth of field on a low-end target without profiling. The `clean` look is nearly free; `moody` is the heaviest. See `references/looks.md` for the full per-effect knob reference, defaults, and cost notes. diff --git a/.cursor/skills/flutter_scene-looks/references/looks.md b/.cursor/skills/flutter_scene-looks/references/looks.md new file mode 100644 index 00000000..86823c8d --- /dev/null +++ b/.cursor/skills/flutter_scene-looks/references/looks.md @@ -0,0 +1,290 @@ +# The look stack, knob by knob + +Every field here is a constructor argument on `EnvironmentSettings` (`lib/src/environment_settings.dart`). Assigning `scene.environmentSettings = EnvironmentSettings(...)` applies all of them at once. Defaults are the constructor defaults; a preset only names what it changes. Direct lights (`scene.directionalLight` and friends) are separate and not covered by `EnvironmentSettings`. + +Color fields are `vm.Vector3` (import `package:vector_math/vector_math.dart as vm`). + +--- + +## Base look + +| Field | Default | What it does | +| --- | --- | --- | +| `toneMapping` | `ToneMappingMode.pbrNeutral` | HDR to display operator. `pbrNeutral` preserves hue/saturation, `aces` is contrasty and filmic, `agx` has the gentlest highlight rolloff, `reinhard`/`linear` are simpler references. | +| `exposure` | `1.0` | Linear scene exposure multiplier. The one knob for overall brightness. Do not use `2.0`; that was an old-renderer hack. | +| `environmentIntensity` | `1.0` | Scales image-based lighting (the environment map) contribution. | +| `agxWhite` | `16.29` | AgX white point, only meaningful with `ToneMappingMode.agx`. | +| `agxContrast` | `1.25` | AgX contrast, only with `agx`. | + +`environment` (an `EnvironmentMap?`) and the sky fields are also on `EnvironmentSettings`, but building environment maps is the domain of the idioms skill; a null environment resolves to a default studio IBL. + +## Auto exposure (`autoExposure*`) + +Meters the frame and multiplies on top of `exposure`. Off by default. + +| Field | Default | What it does | +| --- | --- | --- | +| `autoExposureEnabled` | `false` | Turn metering on. | +| `autoExposureStrength` | `0.55` | How fully it drives toward the metered target (0 = none, 1 = full). | +| `autoExposureCompensation` | `0.0` | EV bias applied after metering. | +| `autoExposureMinEv` / `autoExposureMaxEv` | `-4.0` / `4.0` | Clamp range for the adaptation. | +| `autoExposureSpeedUp` / `autoExposureSpeedDown` | `3.0` / `1.0` | Adaptation rate brightening vs darkening. | + +## Bloom (`bloom*`) + +Blooms bright pixels. Off by default. The cheapest way to make a scene feel lit rather than rendered. + +| Field | Default | What it does | +| --- | --- | --- | +| `bloomEnabled` | `false` | Turn bloom on. | +| `bloomThreshold` | `1.0` | Brightness above which a pixel blooms. Lower for more glow. | +| `bloomIntensity` | `0.15` | Strength of the added glow. | +| `bloomScatter` | `0.7` | Spread of the glow, wider values feel dreamier. | + +### Lens flares (`lensFlare*`) + +Ghost chains and a halo ring off the bloom pyramid, for bright emissive sources and sun disks. Rides the bloom chain, so `bloomEnabled` must be on and the flare scales with `bloomIntensity`. Off by default. A little goes a long way; a strong source with high intensity/halo washes the frame. + +| Field | Default | What it does | +| --- | --- | --- | +| `lensFlareEnabled` | `false` | Turn flares on. Needs `bloomEnabled`. | +| `lensFlareIntensity` | `1.0` | Strength of the flare features relative to the bloom. | +| `lensFlareGhostCount` | `4` | Internal-reflection ghosts along the line through the screen center (clamped to 8 at render). | +| `lensFlareGhostSpacing` | `0.3` | Spacing between ghosts, as a fraction of the distance to the center. | +| `lensFlareHaloRadius` | `0.35` | Halo ring radius in screen UV units. | +| `lensFlareHaloIntensity` | `1.0` | Halo strength relative to the ghosts. `0` disables the halo. The halo is what washes the whole frame, so tame it first. | +| `lensFlareChromaticAberration` | `0.005` | Radial color dispersion of the flare features. | + +## Color grading (`colorGrading*`) + +Post-tone-map color shaping. Off by default. Reach here to change the *mood* of the color rather than the exposure. + +| Field | Default | What it does | +| --- | --- | --- | +| `colorGradingEnabled` | `false` | Turn grading on. | +| `brightness` | `1.0` | Multiplicative brightness. | +| `contrast` | `1.0` | Contrast around mid-gray. | +| `saturation` | `1.0` | Color saturation. Above 1 is vivid, below 1 desaturates toward gray. | +| `temperature` | `0.0` | Warm (positive) to cool (negative) white balance. | +| `tint` | `0.0` | Green to magenta balance. | +| `lift` / `gamma` / `gain` | `Vector3(0)` / `Vector3(1)` / `Vector3(1)` | Per-channel shadow/mid/highlight color control (lift-gamma-gain). | +| `colorGradingLut` | `null` | A `ColorLut` (from a `.cube` file) applied after tone mapping. Independent of `colorGradingEnabled`. | +| `colorGradingLutBlend` | `1.0` | LUT mix amount. | + +## Vignette (`vignette*`) + +Darkens the frame edges. Off by default. Small amounts read as cinematic; large amounts as a peephole. + +| Field | Default | What it does | +| --- | --- | --- | +| `vignetteEnabled` | `false` | Turn vignette on. | +| `vignetteIntensity` | `0.5` | Darkening strength at the edge. | +| `vignetteRadius` | `0.75` | How far in the darkening starts (smaller = tighter, more closed-in). | +| `vignetteSmoothness` | `0.5` | Falloff softness of the edge. | + +## Chromatic aberration (`chromaticAberration*`) + +Splits color channels toward the edges. Off by default. A touch adds a lens feel; too much looks broken. + +| Field | Default | What it does | +| --- | --- | --- | +| `chromaticAberrationEnabled` | `false` | Turn it on. | +| `chromaticAberrationIntensity` | `0.2` | Channel-separation strength. | + +## Film grain (`filmGrain*`) + +Adds animated grain. Off by default. Sells a moody or analog look and hides banding in dark gradients. + +| Field | Default | What it does | +| --- | --- | --- | +| `filmGrainEnabled` | `false` | Turn it on. | +| `filmGrainIntensity` | `0.3` | Grain strength. | + +## Ambient occlusion (`ambientOcclusion*`) + +Screen-space contact darkening. Off by default. Requires a `PerspectiveCamera`. The single biggest upgrade for "grounded" vs "floating". Half-resolution by default to stay affordable. + +| Field | Default | What it does | +| --- | --- | --- | +| `ambientOcclusionEnabled` | `false` | Turn AO on. | +| `ambientOcclusionMethod` | `AmbientOcclusionMethod.obscurance` | `obscurance` is the cheap default; `groundTruth` (GTAO) is higher quality and needed for bent normals. | +| `ambientOcclusionIntensity` | `1.0` | Darkening strength. | +| `ambientOcclusionRadius` | `0.33` | World-space sampling radius. | +| `ambientOcclusionPower` | `1.5` | Contrast of the occlusion curve. | +| `ambientOcclusionBias` | `0.07` | Self-occlusion rejection. | +| `ambientOcclusionBentNormals` | `false` | Compute bent normals (needs `groundTruth`); improves indirect lighting direction and enables `bentCone` specular AO. | +| `ambientOcclusionSpecularMode` | `SpecularAmbientOcclusionMode.none` | `simple` occludes reflections cheaply; `bentCone` is directional and needs bent normals. | +| `ambientOcclusionHalfResolution` | `true` | Compute at half res. Keep on unless AO edges look too coarse. | +| `ambientOcclusionIndirectLight` | `0.0` | Above 0 turns on screen-space global illumination (SSGI) bounce; expensive. Its radiance history reprojects, so the bounce stays put under camera motion (object motion still lags). | +| `ambientOcclusionMultiBounce` | `0.0` | Approximate multi-bounce darkening recovery. | +| `ambientOcclusionSampleCount` | `16` | Samples for the `obscurance` method. | +| `ambientOcclusionSliceCount` / `ambientOcclusionStepsPerSlice` | `3` / `3` | GTAO slice sampling. | +| `ambientOcclusionDetail` | `0.5` | Fine-detail term weight (obscurance). | +| `ambientOcclusionHorizonAngle` | `0.06` | Horizon rejection angle. | +| `ambientOcclusionThickness` / `ambientOcclusionThicknessHeuristic` | `0.5` / `0.004` | Depth thickness assumptions for occlusion. | +| `ambientOcclusionDirectLightAffect` | `0.0` | How much AO also dims direct light. | +| `ambientOcclusionVisibilityBitmask` | `false` | Bitmask visibility estimator. | +| `ambientOcclusionDepthMipChain` | `false` | Build a depth mip chain for wide-radius sampling. | + +## Screen-space reflections (`screenSpaceReflections*`) + +Reflects on-screen geometry. Off by default. Requires a `PerspectiveCamera`. Adds realism to floors, water, and glossy surfaces, but only reflects what is on screen. + +| Field | Default | What it does | +| --- | --- | --- | +| `screenSpaceReflectionsEnabled` | `false` | Turn SSR on. | +| `screenSpaceReflectionsIntensity` | `1.0` | Reflection strength. | +| `screenSpaceReflectionsMaxDistance` | `24.4` | Max world-space ray distance. | +| `screenSpaceReflectionsThickness` | `0.46` | Assumed surface thickness for hit tests. | +| `screenSpaceReflectionsStride` | `9.0` | March step size (larger = faster, coarser). | +| `screenSpaceReflectionsMaxSteps` | `90` | Ray-march step budget. | +| `screenSpaceReflectionsBlur` | `0.3` | Roughness-based blur of the reflection. | +| `screenSpaceReflectionsDistanceFadeStart` | `0.0` | Where reflections start fading with distance. | +| `screenSpaceReflectionsResolutionScale` | `1.0` | Compute resolution scale; drop below 1 to save cost. | + +## Fog (`fog*`) + +Distance and height fog, evaluated in linear HDR before tone mapping. Off by default. Needs both `fogEnabled` and a non-`none` `fogMode`. Applies to lit and unlit materials; the skybox is left unfogged, so set `fogColor` to your horizon color for distant blending. + +| Field | Default | What it does | +| --- | --- | --- | +| `fogEnabled` | `false` | Turn fog on. | +| `fogMode` | `FogMode.exponential` | `none`, `linear`, `exponential`, `exponentialSquared`. Must be non-`none` to render. | +| `fogColor` | `Vector3(0.6, 0.7, 0.8)` | Fog tint. Match your sky/horizon. | +| `fogDensity` | `0.02` | Density for the exponential modes. | +| `fogStart` / `fogEnd` | `0.0` / `200.0` | Near/far bounds for `linear` mode. | +| `fogSkyColorInfluence` | `0.0` | Blend fog color toward the sky color. | +| `fogMaxOpacity` | `1.0` | Cap on how opaque fog gets. | +| `fogHeight` / `fogHeightFalloff` | `0.0` / `0.0` | Height-fog band and falloff. | +| `fogSunInScatter` / `fogSunInScatterExponent` | `0.0` / `8.0` | Sun in-scatter glow through the fog. | +| `fogCutoffDistance` | `0.0` | Distance beyond which fog stops accumulating. | + +## God rays (`godRays*`) + +Volumetric light shafts. Off by default. Requires a shadow-casting `DirectionalLight` and a `PerspectiveCamera`; without a shadow-casting sun there is nothing to shaft. + +| Field | Default | What it does | +| --- | --- | --- | +| `godRaysEnabled` | `false` | Turn shafts on. | +| `godRaysIntensity` | `1.0` | Shaft strength. | +| `godRaysDensity` | `0.5` | Medium density the light scatters through. | +| `godRaysAnisotropy` | `0.7` | Forward-scatter bias (higher = tighter shafts toward the sun). | +| `godRaysStepCount` | `24` | March steps; higher is smoother and costlier. | +| `godRaysMaxDistance` | `200.0` | Max shaft distance. | +| `godRaysJitter` | `1.0` | Dither to hide banding. | +| `godRaysColor` | `Vector3(1)` | Shaft tint. | + +## Depth of field (`depthOfField*`) + +Physically parameterized lens blur. Off by default. Requires a `PerspectiveCamera`. Great for a hero shot, wasteful for a full interactive scene. + +| Field | Default | What it does | +| --- | --- | --- | +| `depthOfFieldEnabled` | `false` | Turn DoF on. | +| `depthOfFieldFocusDistance` | `10.0` | World distance in sharp focus. | +| `depthOfFieldFStop` | `2.8` | Aperture; lower = shallower focus, more blur. | +| `depthOfFieldFocalLength` | `0.0` | Lens focal length (0 derives from FOV). | +| `depthOfFieldSensorHeight` | `0.024` | Sensor height in meters (35mm-ish). | +| `depthOfFieldBlurScale` | `1.0` | Overall blur multiplier. | +| `depthOfFieldMaxForegroundBlur` / `depthOfFieldMaxBackgroundBlur` | `24.0` / `32.0` | Blur radius caps. | +| `depthOfFieldBladeCount` | `0` | Aperture blades for bokeh shape (0 = round). | +| `depthOfFieldBladeRotation` / `depthOfFieldBladeCurvature` | `0.0` / `0.0` | Bokeh blade shaping. | +| `depthOfFieldQuality` | `DepthOfFieldQuality.medium` | `low` (16 taps, mobile/web), `medium` (32 taps + postfilter), `high` (48 taps). | + +--- + +## Not on `EnvironmentSettings` + +Two look-affecting settings live outside the `EnvironmentSettings` snapshot: anti-aliasing is a `Scene` field, and reflection probes are a scene-graph component. Set them directly. + +### Anti-aliasing (`Scene.antiAliasingMode`) + +Edge anti-aliasing. `AntiAliasingMode.auto` is the default: it picks `msaa` where the backend supports it and `fxaa` otherwise. Set it directly, not through `EnvironmentSettings`. + +```dart +scene.antiAliasingMode = AntiAliasingMode.smaa; +``` + +| Mode | What it does | +| --- | --- | +| `none` | No anti-aliasing; native-resolution edges. | +| `msaa` | 4x MSAA on the scene pass, the best geometry-edge quality and cheap on mobile GPUs, but not supported on every Flutter GPU backend (falls back to `fxaa`). Check `Scene.isAntiAliasingModeSupported` and read `Scene.effectiveAntiAliasingMode`. | +| `fxaa` | One post pass over the tone-mapped image, supported everywhere, but softens all high-contrast edges including texture detail. | +| `smaa` | SMAA 1x, three post passes, supported everywhere. Reconstructs edge shapes so edges are cleaner than `fxaa` with far less texture blurring, at roughly 3x the `fxaa` cost. Reach for it when `fxaa` looks mushy and `msaa` is unavailable. | +| `auto` | `msaa` where supported, else `fxaa`. | + +### Reflection probes (`ReflectionProbeComponent`) + +SSR only reflects what is currently on screen. A reflection probe captures the surroundings from a point into a local, parallax-corrected environment, so off-screen geometry reflects correctly inside a bounded box (a mirror ball, a glossy floor in a room). It is a `Component` attached to a `Node`, not an `EnvironmentSettings` field. + +```dart +final probe = Node() + ..localTransform = vm.Matrix4.translation(vm.Vector3(0, 1, 0)); // reflective spot +probe.addComponent(ReflectionProbeComponent( + extents: vm.Vector3(4, 3, 4), // box half-extents (the influence + parallax volume) +)); +scene.add(probe); +``` + +| Constructor arg | Default | What it does | +| --- | --- | --- | +| `extents` | `Vector3.all(5.0)` | Half-extents of the world-axis-aligned box that is both the influence volume and the parallax proxy. | +| `blendDistance` | `1.0` | Distance over which the probe cross-fades with the environment at the box edge. | +| `priority` | `10.0` | Which probe wins where several overlap. | +| `weight` | `1.0` | Contribution scale in the blend. | +| `faceResolution` | `128` | Cubemap face resolution of the capture. | +| `captureOnActivate` | `true` | Capture once when the probe joins the scene. Call `requestCapture()` to re-capture after the scene changes; the capture is a static snapshot otherwise. | + +For a one-shot environment capture with no node or parallax (e.g. to hand a captured `EnvironmentMap` to another material), `Scene.captureEnvironment(position: ...)` returns an `EnvironmentMap` directly. + +### Planar reflectors (`PlanarReflectorComponent`) + +A true mirror for one flat surface, re-rendered every frame the surface is visible: the engine renders the scene from the view camera reflected across the surface's plane (near plane clamped to the mirror, so nothing behind it leaks in) and hands the capture to the surface's material. Use it for mirrors and glossy floors where SSR's on-screen-only reflections or a probe's static capture are not enough. + +Two pieces pair up. The component goes on the mirror node (the plane is the node's local `+Y` through its transform, or an explicit `localNormal`): + +```dart +final mirror = Node(mesh: Mesh(PlaneGeometry(width: 10, depth: 10), mirrorMaterial)) + ..addComponent(PlanarReflectorComponent()); +scene.add(mirror); +``` + +And the surface's material is a `.fmat` that declares the `planar_reflection` engine input and samples `GetPlanarReflection()` (mirrored scene color in rgb, `a` 1 while a capture is bound; fall back to the environment reflection at `a == 0`). A worked mirror lives at `examples/flutter_app/assets/planar_mirror.fmat`. + +| Constructor arg | Default | What it does | +| --- | --- | --- | +| `resolutionScale` | `0.5` | Capture resolution relative to the view (clamped `0.1..1.0`). The fragment-cost lever. | +| `layerMask` | all layers | What renders into the capture. The draw-cost lever. | +| `reflectionGroupId` | `-1` | Co-planar surfaces sharing a non-negative id share one capture per frame; `-1` means an own capture. | +| `clipBias` | `1e-3` | World-space offset of the clip plane in front of the mirror, keeping the surface itself out of the capture. | +| `localNormal` | local `+Y` | The mirror plane's facing direction in node space. | + +The capture is a second scene submission per reflection group per frame: its CPU and draw-call cost scales with scene complexity, not just resolution. It reuses the frame's shadow atlas and runs without screen-space post; reflectors seen inside a capture draw their base look, so captures never recurse. + +--- + +## Cost and budget + +Post effects are screen-space passes; they cost per output pixel, not per triangle. Rough order, cheapest first: + +- **Nearly free.** Tone mapping, exposure, color grading, vignette, chromatic aberration, film grain, bloom (with lens flares). The `clean` and `stylized` looks live here. +- **Moderate.** Ambient occlusion (keep `ambientOcclusionHalfResolution: true`), fog, `fxaa`, `smaa` (~3x `fxaa`). `msaa` is nearly free on mobile GPUs but costs more elsewhere. +- **Expensive.** SSR, god rays, depth of field, and especially SSGI (`ambientOcclusionIndirectLight > 0`). Each adds ray-marching or gather passes. A reflection probe's capture renders the scene six times, so capture on activate or an occasional `requestCapture()`, never per frame. + +Budget guidance: + +- **Mobile and web are the ceiling.** A look that is smooth on desktop can tank a phone. Profile the target, do not assume. +- **Keep AO at half resolution** unless the coarse edges actually show. Full-res AO rarely earns its cost. +- **Do not stack the expensive effects blindly.** SSR plus god rays plus depth of field together on a low-end device needs profiling; drop one or lower its resolution/step budget (`screenSpaceReflectionsResolutionScale`, `godRaysStepCount`, `DepthOfFieldQuality.low`). +- **Depth of field is a hero-shot tool.** It reads as intentional on a framed still and as a smear on a free-moving interactive camera. Prefer it where the camera is controlled. +- **Bloom and grading buy the most look per cost.** When you need "flashier" cheaply, reach for these before the screen-space passes. + +## Why each look is built the way it is + +**showcase** aims for a clean, believable, flattering render, the default for showing a model off. `aces` tone mapping gives contrast and pop; soft bloom (threshold just above 1.0) lights the highlights without haze; GTAO with bent normals and `bentCone` specular AO grounds the object and tightens reflections in its cavities; SSR adds real floor and surface reflections; a light vignette focuses the eye. Every choice supports "look at this object", nothing calls attention to itself. + +**stylized** trades realism for graphic punch. It leans on color (saturation up, a warm push, contrast up) and glow (a lower bloom threshold so more of the frame blooms) and deliberately *omits* AO and SSR, because heavy occlusion and reflection read as realism and fight the flatter, poppier intent. Cheap to run, since it is all near-free passes. + +**moody** is the atmospheric, cinematic end. Lower exposure and cool grading set a somber base; exponential fog with a dark horizon color adds depth and hides the far plane; strong AO deepens the shadows; a tight heavy vignette closes the frame; film grain and a touch of chromatic aberration add texture and a lens feel; god rays (given a shadow-casting sun) add drama. It is the most expensive look; on a tight budget, drop god rays first, then SSR if present. + +**clean** is the honest look, for when the render must show the *actual* material and lighting without editorializing: an editor viewport, an inspector, a UI-embedded preview. Default `pbrNeutral` tone mapping preserves hue and saturation, and the only effect on is gentle half-res AO for grounding. No bloom, grading, or vignette, because each of those changes what the color and brightness actually are, which is exactly what an accurate preview must not do. Also the cheapest look by far. diff --git a/.cursor/skills/flutter_scene-performance/SKILL.md b/.cursor/skills/flutter_scene-performance/SKILL.md new file mode 100644 index 00000000..4b71ee0b --- /dev/null +++ b/.cursor/skills/flutter_scene-performance/SKILL.md @@ -0,0 +1,63 @@ +--- +name: flutter_scene-performance +version: 3 +description: Make a flutter_scene app hit frame budget. Use whenever a scene janks, stutters, or drops frames, or when the ask is to make it faster or run on mobile or web, because code-driven scenes are reliably slow and the fix depends on which thread is over budget, not on a guessed poly count. +--- + +# Making flutter_scene fast + +A code-driven flutter_scene scene is reliably slow, and the usual reason is that nothing told you the budget or what to fix first, so optimization starts as guesswork. Guessing wastes iterations and often makes the wrong thread slower. This skill replaces the guessing with a budget, a way to measure it, and a fixed order to apply fixes in. + +**The one thing to internalize: measure the actual frame, find which of the two threads is over budget, then fix that thread. Do not target a triangle or draw-call number from memory.** There is no built-in poly budget, and the same scene can be fast on desktop and jank on a phone. The number that matters is milliseconds per frame on the real target. + +## The budget + +A frame has a fixed wall-clock budget set by the refresh rate. + +- **60 fps is 16.6 ms per frame. 120 fps is 8.3 ms.** Miss it and the frame janks. +- That budget is split across **two threads**, and either one blowing it drops the frame: + - **UI thread** runs your Dart. flutter_scene walks the scene graph, culls, updates components, and builds the render here. + - **Raster thread** is where Impeller draws the built frame on the GPU. The whole post-processing stack (ambient occlusion, reflections, depth of field, god rays, bloom) lands here. +- **Mobile and web are the real constraint.** Desktop GPUs hide a lot; a scene that runs smooth on a laptop can miss budget badly on a phone or in a browser. Profile on the lowest target you must support. + +## Measure first (do not skip this) + +There is no runtime stats API (`scene.frameTime` does not exist), but the renderer has compile-time profiling. Measurement is those defines plus Flutter's own tooling. + +1. **Run in profile mode.** `flutter run --profile --enable-flutter-gpu`. Debug-mode timings are meaningless for performance (assertions, no JIT-to-AOT optimization, extra checks), so never judge speed in debug. +2. **Engine pass timing.** Add `--dart-define=FLUTTER_SCENE_PROFILE=true` and the renderer prints sampled per-pass CPU means and maxes to stdout (`ScenePass`, `DepthPrepass`, `ShadowPass`, `BloomPass`, ...) plus encoder stats (`FLUTTER_SCENE_PROFILE_ENCODER`, draw and instance counts, sort/encode/instance-pack times). A healthy mean next to a huge max is a one-off stall, not steady-state cost; find the stall (see the pipeline section below) instead of shrinking the steady state. +3. **Read the frame chart.** Open DevTools, go to the Performance view, and read **per-frame UI time vs raster time**. The jank frames are flagged. This single view tells you which thread is over budget, which decides everything below. +4. **Or use the performance overlay** for a quick in-app read of the two thread graphs without DevTools. +5. **Stopwatch as a coarse fallback.** A `Stopwatch` around the per-frame work gives a rough UI-thread number when you cannot open DevTools. It sees nothing on the raster thread. + +Which thread is over budget names the fix. UI over budget means too much graph/CPU work (steps a, b, c below). Raster over budget means too much GPU work (steps d, e). See `references/performance.md` for the symptom to thread diagnosis. + +## The fixed remediation order + +Apply top-down. Each step lists the real API. Do the measured-over-budget thread's steps first, but the order within is deliberate, the earlier fixes are the bigger wins. + +1. **Instancing** (UI thread). Many copies of one mesh collapse into one draw and one cull test. Build an `InstancedMesh(geometry:, material:)`, add a transform per copy with `addInstance(matrix, {color})`, and mount it with `InstancedMeshComponent`. The single biggest win for repeated geometry (foliage, crowds, tiles, debris). Per-instance frustum culling is off by default (`cullInstances: false`), so the batch is one cull test as a unit. See the `flutter_scene-procedural` skill for the full scatter pattern. + +2. **Level of detail** (both threads). `LodComponent(List)` swaps cheaper meshes as an object shrinks on screen. Each `LodLevel(geometry:, material:, screenSize:)` gives a threshold (projected size as a fraction of viewport height, highest detail first, last is the cull floor). Fewer triangles for distant objects on the GPU, and nothing drawn below the floor. Note the shadow and depth passes always draw the highest-detail level, so LOD does not lighten shadow cost. + +3. **Culling** (UI thread). `Node.frustumCulled` (default true) skips off-screen subtrees; leave it on. Set it `false` only where the cached bound is known-stale or unbounded (procedural terrain you regenerate). For whole sets, `RenderView.cullingPlanes` adds extra clip planes, and `node.layers` (default `kRenderLayerDefault`) against `RenderView.layerMask` (default `kRenderLayerAll`) skips entire layers a view should not draw. + +4. **Shrink the post stack** (raster thread). This is usually where raster time goes. Turn off effects you do not need via the `EnvironmentSettings` `*Enabled` flags, keep `ambientOcclusionHalfResolution: true`, lower `depthOfFieldQuality` toward `DepthOfFieldQuality.low`, drop `RenderView.renderScale` (or `Scene.renderScale`) below `1.0` to render fewer pixels, and step `Scene.antiAliasingMode` down (`msaa` to `fxaa` to `none`). See the `flutter_scene-looks` skill for what each knob does to the look. + +5. **Texture and material consolidation** (raster thread). Fewer distinct textures and materials means fewer state changes and binds per frame. `TextureAtlas` (with `generateSolidColorAtlasPixels` for placeholders) packs many tiles into one texture so one material covers them all; share a single `Material` instance across many nodes instead of constructing one per node; use `MaterialsVariantsComponent` to switch a model between named material sets rather than duplicating materials. + +6. **Static shadows** (raster thread). `Node.shadowStatic = true` promises a caster's geometry, material coverage, and world transform will not change while mounted, so the engine renders it into cached shadow-map tiles reused across frames instead of re-encoding every caster every frame. A large static world becomes dramatically cheaper to shadow. Flag only genuinely static content; a static node that moves shows stale shadows until its render item re-registers. + +## First-draw pipeline stalls (the other kind of slow) + +Percentiles clean but the **worst** frame is tens to hundreds of milliseconds, at boot, on the first placement, the first effect, the first hover? That is not steady-state cost. Each (vertex shader, fragment shader, vertex layout) triple compiles a backend pipeline synchronously on its first draw, and a heavy custom material can cost 100ms+ on that one frame. + +- **Debug builds name the offenders.** Any pipeline build of 8ms or more prints `flutter_scene: pipeline build took ...` with the material and geometry. Play through the moments that hitch and collect the list; fix the list, not a guess. +- **The fix is pre-warming, not optimization.** The compile happens exactly once; move it where nobody sees it. Draw each named material once behind a load screen or during an intro, at tiny scale inside the frustum (a frustum-culled or `visible: false` node never draws, so it warms nothing). +- **Warm through the real builders.** The pipeline key includes the vertex layout, so a placeholder sphere warms a different pipeline than the real mesh. Build the actual tower/effect/overlay through its normal code path. +- **Keep warmup draws brief.** One frame compiles the pipeline. A material that samples scene depth also forces the full-resolution depth prepass while it is live, so a lingering warmup shows up as raster cost. +- **Do not trust repeat measurements.** GPU drivers cache compiled shaders across runs, so the second profiling session hits caches the first did not. First-run cost is what a fresh install pays. + +## More depth + +`references/performance.md` expands each step with the full API and when it helps, the UI-vs-raster symptom map (what a wrong frame time points to), and the honest note on what measurement tooling actually exists. diff --git a/.cursor/skills/flutter_scene-performance/references/performance.md b/.cursor/skills/flutter_scene-performance/references/performance.md new file mode 100644 index 00000000..51cd8598 --- /dev/null +++ b/.cursor/skills/flutter_scene-performance/references/performance.md @@ -0,0 +1,128 @@ +# flutter_scene performance reference + +Companion to the `flutter_scene-performance` skill. The skill states the budget, the measure-first rule, and the fixed remediation order. This file expands each step with the real API, when it helps and when it does not, the diagnosis that maps a wrong frame time to a thread, and an honest account of the measurement tooling. + +Verify any symbol here against `lib/src` before relying on it; the inventory in the `flutter_scene-idioms` skill (`references/what-exists.md`) is the fuller API map. + +## The two threads, concretely + +A frame is built on the UI thread and drawn on the raster thread, and they overlap across frames (frame N rasters while frame N+1 builds). Either thread over budget drops the frame. + +- **UI thread work** is Dart. flutter_scene walks the scene graph, computes world transforms, runs frustum culling, ticks every component's `update`, and encodes the draw list. Cost scales with node count, component count, and how much per-frame Dart you run in `onTick` or component `update`. +- **Raster thread work** is the GPU. Impeller executes the encoded passes, the shadow pass, the main color pass, and every enabled screen-space post-processing pass. Cost scales with pixels drawn, overdraw, shadow-map resolution, and how many post passes are on. + +## Diagnosis, symptom to thread + +Read the two thread graphs in DevTools (or the performance overlay) and match the over-budget one to a cause. + +| Observation | Over-budget thread | Likely cause | Go to step | +| --- | --- | --- | --- | +| UI time high, raster fine | UI | Too many nodes/draws or heavy per-frame Dart | 1 instancing, 3 culling | +| UI time scales with object count | UI | Thousands of separate nodes for one repeated mesh | 1 instancing | +| UI time high with a huge static world | UI | No culling; whole graph walked every frame | 3 culling | +| Raster time high, UI fine | Raster | Too many pixels or post passes | 4 post stack, 5 consolidation | +| Raster time high, and it tracks resolution | Raster | Fill-bound; too many pixels | 4 `renderScale`, AA | +| Raster spikes only when shadows are on | Raster | Every caster re-encoded per frame | 6 static shadows | +| Raster time tracks the number of distinct materials | Raster | State-change churn from per-node materials/textures | 5 consolidation | +| Jank only on phone or web, smooth on desktop | Whichever is over budget there | Desktop GPU was hiding it | measure on the real target | + +If both threads are near budget, fix the UI thread first (steps 1 to 3); a lighter draw list also lightens the raster thread. + +## Measurement tooling, honestly + +There is **no built-in frame-stats API in flutter_scene**. No `scene.frameTime`, no draw-call count, no visible-triangle count. Do not invent one or claim one exists. + +- **Profile mode is mandatory.** `flutter run --profile --enable-flutter-gpu`. Debug builds carry assertions and skip AOT optimization, so their timings do not reflect a release build. A number taken in debug mode is not a performance number. +- **DevTools Performance view** is the primary tool. It shows per-frame UI time and raster time as two tracks, flags janky frames, and lets you expand a frame's timeline. This is what tells you which thread is over budget. +- **The performance overlay** gives the same two thread graphs in-app for a quick read without attaching DevTools. +- **A `Stopwatch`** around the per-frame work (the `onTick` body, or a component `update`) is a coarse UI-thread fallback. It cannot see the raster thread at all, so a good stopwatch number does not clear a raster-bound jank. +- **Editor MCP.** `get_app_state` reports lifecycle only (launching/running), not timing. The one place per-pass GPU timings surface is a render-graph capture (`Scene.captureRenderGraph`, or the editor MCP capture tool), whose result carries per-pass timing and lets you see which post pass is expensive. That is per-pass GPU detail, not a whole-frame counter, and the editor MCP is not connected in every project. When it is not, the DevTools loop above is fully sufficient. + +## Step 1, instancing + +`InstancedMesh` holds one `geometry`/`material` pair and one transform per copy. The whole set encodes as a single draw and, by default, a single frustum cull test. + +```dart +final mesh = InstancedMesh( + geometry: someGeometry, + material: sharedMaterial, // one material for the whole batch +); +for (final placement in placements) { + mesh.addInstance(placement.transform, color: placement.tint); // matrix is cloned +} +scene.add(Node()..addComponent(InstancedMeshComponent(mesh))); +``` + +- `addInstance(Matrix4, {Vector4? color})` returns an index; the matrix is cloned, so mutating your copy afterward is safe. Per-instance `color` is a linear RGBA multiplier. +- Edit later with `setInstanceTransform(i, m)`, `setInstanceColor(i, color)`, `removeInstanceAt(i)`, `clearInstances()`, or move the whole batch in one pass with `updateInstanceTransforms((list) { ... })`. +- **Culling default.** `cullInstances` defaults to `false`, so the batch is culled as one unit against its combined bounds, not per instance. Turn `cullInstances: true` on only for a batch spread across a large area where many instances are off-screen, since per-instance culling adds CPU work. +- **When it helps.** Repeated geometry, foliage, crowds, tiles, debris, particles-as-meshes. It is the largest UI-thread win available, because N separate nodes become one. It does nothing for a scene of distinct meshes. +- **Winding trap.** A mirrored (negative-determinant) instance edited with `updateInstanceTransforms(recomputeWinding: false)` renders inside-out. Keep instance edits orientation-preserving, or let winding recompute. + +See the `flutter_scene-procedural` skill for the full scatter-on-terrain pattern. + +## Step 2, level of detail + +`LodComponent(List)` draws one of several mesh variants per frame, chosen from how large the object appears on screen. + +```dart +node.addComponent(LodComponent([ + LodLevel(geometry: high, material: mat, screenSize: 0.4), + LodLevel(geometry: mid, material: mat, screenSize: 0.15), + LodLevel(geometry: low, material: mat, screenSize: 0.04), // set 0.0 to never cull +])); +``` + +- `screenSize` is the projected bounding-sphere diameter as a fraction of viewport height. Levels are highest detail first, strictly descending. The engine draws the highest-detail level whose threshold the object still meets, and draws nothing below the last threshold (the cull floor). +- Selection is screen-size based, so it is field-of-view aware and resolution independent, and it is per view (a split-screen frame can pick different levels per view). +- `LodComponent(levels, {lodBias = 1.0, hysteresis = 0.1, blendRange = 0.0})`. `lodBias` above `1` keeps detail farther away; `hysteresis` is a dead-band so an object on a boundary does not flip-flop; `blendRange` above `0` dither-cross-fades adjacent levels to remove the pop (honored by the built-in lit and unlit materials). +- **Limitation that matters for shadows.** The shadow and depth-prepass passes always draw the highest-detail level and ignore the LOD cull. So LOD lightens the color pass, not shadow or depth cost. A shadow-heavy scene needs step 6, not LOD. +- **Not for instanced draws.** A `LodComponent` draws a single mesh and picks one level for the whole node; it does not combine with hardware instancing. + +## Step 3, culling + +Skip work for things the camera cannot see. + +- **`Node.frustumCulled`** (default `true`) skips a subtree whose `combinedLocalBounds` do not intersect the camera frustum. Leave it on. Set it `false` only where the cached bound is known-stale or misleading (procedural geometry you regenerate, large terrain pieces). A subtree that reports no bound (skinned content, geometry without a computable bound) is treated as always visible regardless of the flag. +- **`RenderView.cullingPlanes`** (`List`, default empty) adds extra clip planes beyond the frustum, for portal or region culling. +- **Layers.** `node.layers` (a 32-bit mask, default `kRenderLayerDefault` which is layer 0, not inherited by children) against `RenderView.layerMask` (default `kRenderLayerAll`) decides whether a view draws a node at all, when `node.layers & view.layerMask != 0`. Put editor gizmos, an inset viewport's contents, or a minimap's set on their own layer and give each view the mask it needs, so a view skips whole sets cheaply. +- **When it helps.** Large worlds where much of the graph is off-screen each frame. Culling is a UI-thread win (fewer nodes encoded) that also lightens the raster thread (fewer draws). + +## Step 4, shrink the post stack + +Every screen-space effect is a raster-thread pass. Turning off what you do not need is the most direct raster win. All the scene-wide look fields live on `EnvironmentSettings` (see the `flutter_scene-looks` skill); each effect has an `*Enabled` flag, off by default. + +- **Turn effects off.** `ambientOcclusionEnabled`, `screenSpaceReflectionsEnabled`, `godRaysEnabled`, `depthOfFieldEnabled`, `bloomEnabled`, and the rest default `false`. A preset the app copied may have turned several on; drop the ones the scene does not visibly need. Ambient occlusion, screen-space reflections, god rays, and depth of field are the heavy ones. +- **Half-resolution AO.** `ambientOcclusionHalfResolution` defaults `true`; keep it. Full-resolution AO roughly doubles that pass's cost for little visible gain on most content. +- **Cheaper depth of field.** `depthOfFieldQuality` (`DepthOfFieldQuality.low`/`medium`/`high`, default `medium`) trades gather taps and cleanup passes for time. Step it down to `low` on mobile. +- **Render fewer pixels.** `Scene.renderScale` (default `1.0`), or per-view `RenderView.renderScale`, renders the scene at a fraction of resolution and upscales. Dropping to `0.75` cuts fill cost by nearly half and is often barely visible after anti-aliasing. This is the biggest lever for a fill-bound (resolution-tracking) raster time. +- **Step anti-aliasing down.** `Scene.antiAliasingMode`. `AntiAliasingMode.auto` picks `msaa` where supported else `fxaa`. `msaa` is cheap on mobile tilers and highest quality; `fxaa` is a single post pass on every backend; `smaa` is cleaner than `fxaa` but three post passes (~3x its cost), so step it down to `fxaa` or `none` on a raster-bound target; `none` is free. Read what actually runs with `Scene.effectiveAntiAliasingMode`. +- **Do not re-capture reflection probes per frame.** A `ReflectionProbeComponent` capture (or `Scene.captureEnvironment`) renders the scene six times, a large one-frame spike. Let it capture once on activate and only call `requestCapture()` when the scene visibly changes, never every frame. +- **When it helps.** Any raster-bound scene. The `clean` look in the looks skill is nearly free; a full `moody` stack (AO plus SSR plus god rays plus DoF plus grain) is the heaviest. Do not stack all of those on a low-end target without profiling. + +## Step 5, texture and material consolidation + +Every distinct material and texture is a potential state change and bind on the raster thread. Fewer of them means a shorter, cheaper draw list. + +- **`TextureAtlas`** packs many equally sized tiles (voxel faces, sprite sheets, terrain tiles) into one texture, so a single material and draw call cover every tile. Resolve a tile's UV box with `tileBounds(index)` or map a within-tile coordinate with `tileUv(index, u, v)`, write those into the mesh's texture coordinates, and build the bound material with `toMaterial()`. `generateSolidColorAtlasPixels(tileColors:, columns:, tileSize:, padding:)` builds placeholder pixels to bring the atlas path up before real art exists. +- **Share one `Material` instance** across many nodes rather than constructing a new one per node. Identical materials that are separate objects still churn binds; the same object does not. Build the material once and reuse the reference. +- **`MaterialsVariantsComponent`** switches an imported model between its named `KHR_materials_variants` sets in place (`MaterialsVariantsComponent.of(model)?.select('name')`), instead of duplicating a model per look. Read `variants` for the declared names; `select(null)` restores defaults. +- **When it helps.** Scenes whose raster time tracks the count of distinct materials or textures, tile-based worlds, and models shown in several finishes. + +## Step 6, static shadows + +Shadow casting re-encodes every caster into the shadow map each frame by default. `Node.shadowStatic = true` promises a caster will not change and lets the engine cache its shadow-map tiles across frames. + +```dart +staticWorldNode.shadowStatic = true; // set per mesh-bearing node; not inherited +``` + +- **The contract.** The node's geometry, material coverage, and world transform must not change while mounted. In return, the engine renders it into cached shadow-map tiles reused across frames instead of re-encoding it every frame. Dynamic nodes (the default) still cast per-frame shadows on top of the cache, so a moving character over a static world works. +- **Not inherited.** Set it on each mesh-bearing node, not once on a root. +- **Stale-shadow caveat.** A static node that does change (moves, remeshes, edits material coverage) shows stale shadows until its render item re-registers. Flag only genuinely static content. +- **Displacement caveat.** A material with a `vertex { }` displacement stage should stay dynamic, since its cached shadow would not follow a camera-dependent displacement. +- **When it helps.** Large static worlds with a shadow-casting `DirectionalLight`. The win scales with how many static casters you have; a mostly static level with a few moving actors is the ideal case. + +## Order and stopping + +Fix the measured over-budget thread first, top-down within it, and re-measure after each change so cause and effect stay legible. Stop when the frame chart clears budget on the real target; there is no reason to keep optimizing a thread that is already under budget while the other one janks. The whole point of measuring first is to avoid spending a step's effort on the thread that was never the problem. diff --git a/.cursor/skills/flutter_scene-procedural/SKILL.md b/.cursor/skills/flutter_scene-procedural/SKILL.md new file mode 100644 index 00000000..11eaa1d7 --- /dev/null +++ b/.cursor/skills/flutter_scene-procedural/SKILL.md @@ -0,0 +1,138 @@ +--- +name: flutter_scene-procedural +version: 4 +description: Build flutter_scene content from code instead of asset files. Use when generating terrain, scattering vegetation or crowds, building oceans and Gerstner waves, setting up procedural skies and trees, assembling modular kits, or driving a scene from noise and instancing rather than loading a .glb. +--- + +# Procedural content in flutter_scene + +A lot of 3D work does not need an artist's `.glb` at all. Terrain, scattered foliage, debris fields, crowds, and modular buildings are cheaper and more flexible built from code, and flutter_scene has the whole path in the box. Reach for it before wiring up an asset pipeline. + +**The insight: for a code-driven scene, generate geometry and draw it instanced. That path is more reliable than loading external assets, because it has no import step, no coordinate-conversion traps, no missing-file failure modes, and one draw call for thousands of copies.** Three pieces cover almost everything: + +- **`GeometryBuilder`** (and the built-in primitives and swept paths) build custom meshes without a model file. +- **`FastNoiseLite`** drives heightmaps, placement, and displacement deterministically. +- **`InstancedMesh`** draws thousands of copies of one mesh as a single render item. + +Do not hand-pack a `ByteData` vertex buffer. The vertex layout is fixed (72 bytes unskinned, a specific attribute order) and a wrong stride fails silently with washed-out or see-through geometry. `GeometryBuilder` and `MeshGeometry.fromArrays` interleave the layout for you. + +## Imports + +Geometry and instancing live in the main barrel. **Noise is a separate barrel** and is easy to forget: + +```dart +import 'package:flutter_scene/scene.dart'; // GeometryBuilder, MeshGeometry, InstancedMesh, ... +import 'package:flutter_scene/noise.dart'; // FastNoiseLite, bakeNoiseTexture, noiseCurl3 +import 'package:vector_math/vector_math.dart' as vm; // NOT vector_math_64 +``` + +## Terrain from a noise heightmap + +Sample `FastNoiseLite` on a grid, add each vertex, and wind the two triangles per cell so the lit surface faces up. Omitting normals lets the builder derive them from the actual face slopes, which is what you want for terrain. + +```dart +MeshGeometry buildTerrain({int cols = 128, int rows = 128, double spacing = 0.5}) { + final noise = FastNoiseLite(seed: 1337) + ..noiseType = NoiseType.openSimplex2 + ..fractalType = FractalType.fbm // stack octaves for natural detail + ..octaves = 5 + ..frequency = 0.02; // world units are multiplied by this + + final builder = GeometryBuilder(); + + // One vertex per grid point. getNoise2 returns roughly -1..1. + for (var r = 0; r < rows; r++) { + for (var c = 0; c < cols; c++) { + final x = c * spacing; + final z = r * spacing; + final height = noise.getNoise2(x, z) * 6.0; + builder.addVertex(vm.Vector3(x, height, z)); + } + } + + // Two triangles per cell, wound Counter-Clockwise (CCW) so the front face points +Y (up). + for (var r = 0; r < rows - 1; r++) { + for (var c = 0; c < cols - 1; c++) { + final v00 = r * cols + c; + final v10 = v00 + 1; + final v01 = v00 + cols; + final v11 = v01 + 1; + builder + ..addTriangle(v00, v01, v10) + ..addTriangle(v10, v01, v11); + } + } + + return builder.build(); +} +``` + +Attach it like any mesh: + +```dart +final terrain = Node(mesh: Mesh(buildTerrain(), PhysicallyBasedMaterial()..roughnessFactor = 1.0)); +scene.add(terrain); +``` + +If a hand-built surface renders inside-out (visible only from below, dark where lit), reverse each triangle's index order. flutter_scene's front faces wind Counter-Clockwise (CCW) in model space, matching glTF and standard conventions; never fix orientation with a per-triangle flip on an imported model, but for geometry you author yourself the winding is yours to set. + +## Natural formations and landscape recipes + +To achieve documentary realism rather than generic procedural lumps: + +1. **Footpaths are scoured trenches, not flat stripes.** A real trail is the lowest line across terrain because water and foot traffic erode it downwards. When generating heightfields, cut the trail path profile down into the terrain with banks rising away on both sides. +2. **Ridged noise for valley walls and cliffs.** Standard `FractalType.fbm` makes rolling mounds. Use `FractalType.ridged` for valley walls, mountain spurs, and cliffs to produce sharp erosion creases. +3. **Free-end Worley rock cracks.** Standard Worley noise (`F2 - F1`) creates closed polygonal loops like bathroom tile. To produce weathered rock fractures with natural free ends, multiply the cell border by a low-frequency region mask and a high-frequency grain breaker. +4. **Noise-modulated pitting.** A constant threshold radius across Worley cells places a pit in every cell, producing an artificial grid lattice. Modulate the threshold radius with an underlying Perlin field so pores vary in size and only appear in exposed weathering pockets. +5. **Macro massing for scattered gravel.** Soil wears in 0.5m to 2m zones. Modulate multi-scale pebble instances with a low-frequency massing field so gravel clusters into realistic water scour lines rather than uniform sandpaper noise. +6. **Sunk block settling and ground contact staining.** Place boulders and masonry courses 1/3 to 2/3 submerged into the sampled ground height. Use vertex colors or shader ground distance to stain the bottom 20cm of rock near the soil boundary, creating a smooth moisture transition. +7. **Oceans and Gerstner waves.** Sum 4 to 8 directional Gerstner trochoidal waves that pull vertices horizontally toward crests, producing sharp peaks and wide flat troughs. Use Beer-Lambert depth absorption (exp(-sigma_a * d)) via scene depth for turquoise to deep navy transitions, Jacobian folding for peak foam, and darken/smooth tidal sand within the shoreline wash. +8. **Trees and foliage translucency.** Extrude branch splines using `TubeGeometry` or `ExtrudeGeometry`, conserving cross-sectional area across splits (d_parent^2 = sum d_child^2). Set `Material.doubleSided = true` and add diffuse transmission in custom leaf shaders so backlit canopies glow. Apply quadratic cantilever displacement (delta_p proportional to h^2) for organic wind sway. +9. **Procedural skies and IBL synchronization.** Use `PhysicalSkySource` (`lib/src/sky_sources.dart`) with analytic Rayleigh and Mie scattering. Assign `SkyEnvironment` to `Scene.skyEnvironment` or call `EnvironmentMap.fromSky` to bake prefiltered radiance and SH-9 diffuse coefficients into the scene's IBL automatically, and assign the source to `Scene.skybox` for matching background visuals. +10. **Islands and coastal erosion.** Multiply radial distance falloff with domain-warped FBM to form organic bays, sandbars, and lagoons. Use analytical surface slopes to strip topsoil on steep cliffs while depositing golden sand and reef shoals on shallow coastal planes. + +## Scattering thousands of copies + +`InstancedMesh` holds one geometry/material pair and a transform per copy. The whole set is one pipeline and one cull test. Place instances by sampling the same terrain height so they sit on the ground. + +```dart +final rng = math.Random(7); +final scatter = InstancedMesh( + geometry: CylinderGeometry(bottomRadius: 0.0, topRadius: 0.15, height: 1.2), // a cone + material: PhysicallyBasedMaterial()..baseColorFactor = vm.Vector4(0.2, 0.5, 0.15, 1), +); + +for (var i = 0; i < 4000; i++) { + final x = rng.nextDouble() * 64; + final z = rng.nextDouble() * 64; + final y = noise.getNoise2(x, z) * 6.0; // same field as the terrain + final transform = vm.Matrix4.translation(vm.Vector3(x, y, z)) + ..rotateY(rng.nextDouble() * math.pi * 2); + scatter.addInstance(transform); // the matrix is cloned; mutating it later is safe +} + +// InstancedMesh rides on a component, not Node(mesh:). +final node = Node()..addComponent(InstancedMeshComponent(scatter)); +scene.add(node); +``` + +`addInstance(matrix, {color})` returns an index; edit later with `setInstanceTransform(i, m)` or move the whole batch at once through `updateInstanceTransforms((list) { ... })`. Per-instance `color` is a linear RGBA multiplier. Keep instance edits orientation-preserving, a mirrored (negative-determinant) instance edited with `updateInstanceTransforms(recomputeWinding: false)` renders inside-out. + +## Repeated pieces must not overlap + +A fence, barrier, curb, or row of tiles repeated along a line flickers at every joint when each piece is longer than its spacing (8.4 m pieces every 8 m), because neighbors share faces in one plane and fight as the camera moves (z-fighting). Alternating colors make it obvious. Size each piece to exactly its spacing, stop perpendicular runs at a corner post instead of overlapping them, and scatter flat patches at one height without overlap. An overlay that lies on a surface (paint, a sign, a decal quad) gets `material.depthLayer = 1`. `scene.findCoplanarOverlaps()` lists any overlap left, with the length that fixes a repeated piece. + +## The web noise trap + +The Dart `FastNoiseLite` relies on 32-bit integer math. On the web (dart2js) a Dart `int` is a JavaScript double, exact only to 53 bits, so the hash loses its low bits and 3D noise can overflow, producing wrong values. This is silent, you get a plausible-looking but incorrect field, and only on web. + +For web targets: + +- Prefer the **GLSL side** (`#include ` in a `.fmat` block), which is correct on every backend including WebGL2 and matches the Dart algorithms table-for-table. +- Or **bake** the field once with `bakeNoiseTexture(noise, width: ..., height: ...)` at build time or in a native isolate, then sample the texture. `bakeNoisePixels` is pure CPU with no engine imports, so it runs in a build hook or background isolate. + +Native platforms are unaffected. `noiseHash2`/`noiseHash3` are the bit-exact CPU/GPU-agreeing integer path for decisions that must never disagree (world generation, placement), but they carry the same web-overflow caveat, so make the decision once and share it rather than re-deriving it on both sides. + +## More depth + +`references/procedural.md` has the full `GeometryBuilder` and `MeshData` API (including off-isolate meshing), the complete `FastNoiseLite` config reference, natural rock, ocean, tree, sky, and island formation recipes, the instancing API in full, modular-kit assembly from the built-in primitives, and the web-noise caveat expanded. diff --git a/.cursor/skills/flutter_scene-procedural/references/procedural.md b/.cursor/skills/flutter_scene-procedural/references/procedural.md new file mode 100644 index 00000000..a66fe69a --- /dev/null +++ b/.cursor/skills/flutter_scene-procedural/references/procedural.md @@ -0,0 +1,512 @@ +# Procedural content, the full API + +Everything for building flutter_scene content from code, custom meshes, noise, instancing, and modular kits. All symbols verified against the package source. Import geometry and instancing from the main barrel, noise from its own barrel: + +```dart +import 'package:flutter_scene/scene.dart'; +import 'package:flutter_scene/noise.dart'; +import 'package:vector_math/vector_math.dart' as vm; // NOT vector_math_64 +``` + +--- + +## GeometryBuilder + +The incremental way to build a custom triangle mesh. You add vertices one at a time, each carrying whatever attributes are currently set, then reference them by returned index to form triangles. + +```dart +class GeometryBuilder { + GeometryBuilder({bool deduplicate = true}); + + // Sticky attribute setters (return `this`, so cascade or chain them). + GeometryBuilder normal(vm.Vector3 value); + GeometryBuilder texCoord(vm.Vector2 value); + GeometryBuilder texCoord1(vm.Vector2 value); // secondary UV set + GeometryBuilder color(vm.Vector4 value); // linear RGBA + GeometryBuilder tangent(vm.Vector4 value); // xyz + handedness in w + + int addVertex(vm.Vector3 position); // returns the vertex index + GeometryBuilder addTriangle(int a, int b, int c); // throws RangeError on a bad index + + int get vertexCount; + int get triangleCount; + + Uint8List packVertices(); // pure, no GPU context needed + MeshGeometry build({ + GeometryStorage storage = GeometryStorage.fixed, + GeometryBufferArena? bufferArena, + bool retainCpuData = true, + }); +} +``` + +### The sticky-attribute model + +The attribute setters do not apply to one vertex, they set state that every following `addVertex` inherits until you change it. This makes flat-shaded faces and per-region colors natural: + +```dart +final geometry = (GeometryBuilder() + ..color(vm.Vector4(1, 0, 0, 1)) // every vertex below is red... + ..addVertex(vm.Vector3(0, 0, 0)) + ..addVertex(vm.Vector3(1, 0, 0)) + ..color(vm.Vector4(0, 1, 0, 1)) // ...until this changes it to green + ..addVertex(vm.Vector3(0, 1, 0)) + ..addTriangle(0, 1, 2)) + .build(); +``` + +### Normals, generated or authored + +If you never call `normal()`, the builder generates area-weighted vertex normals from the faces you wound. That is the right default for most procedural geometry, terrain, extrusions, anything where the surface shape defines the normal. + +**Calling `normal()` even once opts the whole mesh out of generated normals.** After that, any vertex you add without an explicit normal keeps the default `(0, 0, 1)`, which is almost never what you want. So either author a normal for every vertex, or author none and let generation run. Do not mix. + +### Deduplication + +With `deduplicate: true` (the default), `addVertex` merges a vertex equal to one already added and returns the existing index, so a shared grid corner is stored once. Pass `deduplicate: false` when you want every call to produce a distinct vertex (flat shading with per-face normals, or per-vertex data that must not collapse). + +### Winding + +flutter_scene's front faces wind **counter-clockwise in model space**, matching glTF and standard conventions. For a surface that should face +Y (a heightmap, a floor), match the built-in plane's winding: for a cell with corners `v00`(x,z), `v10`(x+1,z), `v01`(x,z+1), `v11`(x+1,z+1), emit `addTriangle(v00, v01, v10)` and `addTriangle(v10, v01, v11)`. + +If a mesh renders inside-out (invisible from the front, visible and inverted-lit from behind), reverse each triangle's index order. Because generated normals follow the winding, fixing the winding fixes the normals too. This freedom is only for geometry you author. Never apply a per-triangle winding flip to an imported model to correct its orientation, that leaves its normals and image-based lighting wrong. + +### From builder to scene + +`build()` returns a `MeshGeometry`, which is a `Geometry`. Wrap it in a `Mesh` with a material, hang the mesh on a `Node`, add the node: + +```dart +final node = Node(mesh: Mesh(geometry, PhysicallyBasedMaterial())); +scene.add(node); +``` + +--- + +## MeshGeometry.fromArrays, the bulk path + +When you already have attributes as flat arrays (a generator that fills typed lists), skip the per-vertex calls and hand `MeshGeometry.fromArrays` structure-of-arrays data directly. Same result, less overhead for large meshes. + +```dart +MeshGeometry.fromArrays({ + required Float32List positions, // 3 floats/vertex, required + Float32List? normals, // 3/vertex; omitted -> generated for triangle lists + Float32List? texCoords, // 2/vertex; omitted -> (0, 0) + Float32List? texCoords1, // 2/vertex + Float32List? colors, // 4/vertex; omitted -> opaque white + Float32List? tangents, // 4/vertex + List? indices, // omitted -> vertex count must be a multiple of 3 + gpu.PrimitiveType primitiveType = gpu.PrimitiveType.triangle, + Aabb3? bounds, // skips the position scan; MUST cover every vertex + GeometryStorage storage = GeometryStorage.fixed, + GeometryBufferArena? bufferArena, + bool retainCpuData = true, +}); +``` + +Notes that bite: + +- Every supplied optional array must match the vertex count implied by `positions`. +- Out-of-range `indices` are **not** validated here (unlike `GeometryBuilder.addTriangle`), and produce stray triangles or holes silently. Keep every index in `0..vertexCount-1`. +- A `bounds` you pass that does not enclose every vertex makes the mesh over-cull and pop out of view at some angles. Omit it (the constructor scans positions) unless you computed it correctly off-thread. +- `retainCpuData: false` drops the CPU copy after upload, saving memory, but then the mesh cannot be raycast or read back with `extractMeshData`. + +### Updatable geometry (animated meshes) + +Pass `storage: GeometryStorage.updatable` to get a mesh you can mutate in place each frame without reallocating. The in-place updaters replace one attribute when the vertex count is unchanged: + +```dart +final water = MeshGeometry.fromArrays(positions: p, storage: GeometryStorage.updatable); +// later, per frame: +water.updatePositions(newPositions); // also updateNormals/TexCoords/Colors/Tangents +// or replace everything (may change the count): +water.rebuild(positions: p2, indices: i2); +``` + +An updatable mesh fixes its indexed-or-not state at construction, if you built it with `indices`, `rebuild` requires them thereafter, and vice versa. To start empty and fill later, pass a zero-length `positions` with `updatable`. Updatable geometry must retain CPU data and cannot use a buffer arena. + +`GeometryBufferArena({int blockSizeInBytes = 16 * 1024 * 1024})` lets many fixed meshes share immutable GPU buffer blocks, worth it when you build a large number of small static meshes. It never reclaims space (an arena only grows for as long as it is alive), so give it the lifetime of the batch it serves and never feed it geometry rebuilt every frame; that is what `updatable` is for. + +--- + +## MeshData, meshing off the render isolate + +Heavy generation (remeshing a voxel chunk, a large marching-cubes surface) should not block the render isolate. `MeshData` is a pure, isolate-transferable snapshot, build it on a background isolate with `compute`, send it back, upload it there. + +```dart +factory MeshData.build({ + required Float32List positions, + Float32List? normals, // omitted -> generated for triangle lists (the win here) + Float32List? texCoords, + Float32List? texCoords1, + Float32List? colors, + Float32List? tangents, + List? indices, + gpu.PrimitiveType primitiveType = gpu.PrimitiveType.triangle, + Map customAttributes = const {}, +}); +``` + +Recipe: + +```dart +// Top-level or static, runs on the background isolate. +MeshData buildChunk(ChunkInput input) { + final positions = /* your generator */; + final indices = /* ... */; + return MeshData.build(positions: positions, indices: indices); +} + +// On the render isolate: +final data = await compute(buildChunk, input); +final geometry = MeshGeometry.fromMeshData(data); +// or, to feed an existing updatable mesh in place: +existing.applyMeshData(data); +``` + +The normal generation is the expensive part, and running it inside `MeshData.build` is exactly the work you moved off the render isolate. + +Pure derivations on a `MeshData` (all off-isolate safe): `transformed(Matrix4)` (moves positions, carries normals by the inverse transpose so a non-uniform scale stays correct, reverses winding on a mirror), `unweld({attributes})`, `extractEdges({creaseAngleDegrees})`, static `MeshData.merge(parts)`, plus `triangleCount`/`triangles`. `Geometry.extractMeshData()` reads a loaded mesh back into one. + +--- + +## FastNoiseLite + +One configurable object evaluating several noise algorithms, sampled with `getNoise2`/`getNoise3`. Output is roughly in `[-1, 1]`. + +```dart +final noise = FastNoiseLite(seed: 1337) + ..frequency = 0.01 // coords are multiplied by this before eval + ..noiseType = NoiseType.openSimplex2 + ..fractalType = FractalType.fbm + ..octaves = 5; + +final h = noise.getNoise2(x, z); // 2D +final d = noise.getNoise3(x, y, z); // 3D +``` + +### Config reference + +| Field | Default | Meaning | +| --- | --- | --- | +| `seed` | 1337 | Seed for every noise type. | +| `frequency` | 0.01 | Input coordinates are scaled by this. Bigger = finer features. | +| `noiseType` | `openSimplex2` | Base algorithm (see below). | +| `fractalType` | `none` | How octaves layer (see below). | +| `octaves` | 3 | Number of fractal layers. More detail, more cost. | +| `lacunarity` | 2.0 | Frequency multiplier between octaves. | +| `gain` | 0.5 | Amplitude multiplier between octaves. | +| `weightedStrength` | 0.0 | Biases octave amplitude toward stronger detail. | +| `pingPongStrength` | 2.0 | Warp strength for `FractalType.pingPong`. | +| `cellularDistanceFunction` | `euclideanSq` | Distance metric for `NoiseType.cellular`. | +| `cellularReturnType` | `distance` | What cellular returns. | +| `cellularJitterModifier` | 1.0 | Cell-point jitter; above 1 causes artifacts. | +| `domainWarpType` | `openSimplex2` | Warp algorithm for `domainWarp2`/`domainWarp3`. | +| `domainWarpAmp` | 1.0 | Max warp distance. | +| `domainWarpFractalType` | `none` | Octave layering for domain warp. | + +Enums: + +- `NoiseType` = `openSimplex2` | `openSimplex2S` | `cellular` | `perlin` | `value`. +- `FractalType` = `none` | `fbm` (classic layered fractal, the usual terrain choice) | `ridged` (sharp ridges, mountains) | `pingPong`. +- `CellularDistanceFunction` = `euclidean` | `euclideanSq` | `manhattan` | `hybrid`. +- `CellularReturnType` = `cellValue` | `distance` | `distance2` | `distance2Add` | `distance2Sub` | `distance2Mul` | `distance2Div`. +- `DomainWarpType` = `openSimplex2` | `openSimplex2Reduced` | `basicGrid`. +- `DomainWarpFractalType` = `none` | `progressive` | `independent`. + +### Domain warp + +`domainWarp2`/`domainWarp3` distort the input coordinates before sampling, breaking up the regular look of raw fractal noise. The reference version mutates in place, this port returns the warped position for you to feed back in: + +```dart +final w = noise.domainWarp2(x, z); // ({double x, double y}) +final v = noise.getNoise2(w.x, w.y); +``` + +### Curl noise + +`noiseCurl3(x, y, z, {int seed = 1337, double epsilon = 0.25})` returns a divergence-free 3D vector `({x, y, z})` from a seeded potential field, for advecting particles so they swirl without clumping. Coordinates are taken pre-scaled (no frequency parameter), matching the GLSL `NoiseCurl3`. Advect by adding `curl * speed * dt`. A smaller `epsilon` sharpens the field and amplifies CPU/GPU divergence. + +### Baking noise to a texture + +Sampling many octaves per fragment is expensive. When the field is static, bake it once and sample the texture instead: + +```dart +Texture2D bakeNoiseTexture( + FastNoiseLite noise, { + required int width, + required int height, + double originX = 0.0, + double originY = 0.0, + double cellSize = 1.0, + TextureSampling sampling = const TextureSampling(), +}); +``` + +It bakes `getNoise2` over a `width` x `height` grid into a grayscale `Texture2D` (content is linear `data`, so mipmaps average cleanly) ready to bind as a material sampler. It must run where GPU resources are created (the raster thread). The CPU half, `bakeNoisePixels(noise, {width, height, originX, originY, cellSize})`, returns `Uint8List` RGBA and has no engine imports, so it runs in a build hook or a background isolate, then `Texture2D.fromPixels` uploads the result. + +--- + +### Natural formations: rocks, cliffs, trails, and scatter recipes + +Composing raw noise into realistic natural terrain and geology requires specific math patterns to avoid telltale procedural artifacts. + +### 1. Free-end Worley rock cracks (avoiding closed cell loops) + +Standard cellular Worley distance (`F2 - F1`) creates a continuous polygon network like bathroom tile or dry mud. To create natural weathering cracks with free ends, mask the cell borders with a low-frequency macro patch and a high-frequency grain breaker: + +```glsl +// GLSL shader bake or .fmat surface +// Cellular Worley noise returning F2 - F1 distance +float cwl = NoiseCellular2(p * 3.0, 1337, kNoiseCellularEuclidean, kNoiseCellularDistance2Sub, 1.0); +float net = smoothstep(0.08, -0.80, cwl); +float region = smoothstep(-0.2, 0.4, NoiseFbm2(p * 1.0, 1338, 3, 2.0, 0.5)); +float breaker = smoothstep(-0.4, 0.2, NoiseFbm2(p * 6.0, 1339, 2, 2.0, 0.5)); +float crack = net * region * breaker; // produces isolated segments with natural start/end points +``` + +### 2. Noise-modulated pitting (avoiding regular dot lattices) + +Thresholding Worley noise at a constant radius places a pit in every single cell, creating an artificial grid lattice. Modulate the threshold radius with an underlying Perlin field so pores vary in size and only appear in exposed weathering pockets: + +```glsl +float sizeVar = (NoiseFbm2(p * 2.5, 1337, 3, 2.0, 0.5) + 1.0) * 0.5; +float pw = NoiseCellular2(p * 5.0, 1338, kNoiseCellularEuclidean, kNoiseCellularDistance, 1.0); +float pit = smoothstep(0.05 + 0.24 * sizeVar * sizeVar, 0.005, pw) + * smoothstep(-0.1, 0.5, NoiseFbm2(p * 1.5, 1339, 3, 2.0, 0.5)); +``` + +### 3. Incised trail heightfields (scours vs flat stripes) + +Footpaths are formed by water and foot traffic compressing and eroding soil downwards. Sample the path polyline once (`trail.sample(n, evenlySpaced: true)`) and compute the minimum point-to-segment distance to cut the path profile into the terrain heightfield with raised spoil banks: + +```dart +double computeTerrainHeight(double x, double z, FastNoiseLite noise, List trailPoints) { + final baseHeight = noise.getNoise2(x, z) * 8.0; + + // Find minimum distance from (x, z) to the sampled 2D path segments + var minDist = double.infinity; + final p = vm.Vector2(x, z); + for (var i = 0; i < trailPoints.length - 1; i++) { + final a = vm.Vector2(trailPoints[i].x, trailPoints[i].z); + final b = vm.Vector2(trailPoints[i + 1].x, trailPoints[i + 1].z); + final ab = b - a; + final t = ((p - a).dot(ab) / ab.length2).clamp(0.0, 1.0); + final dist = (p - (a + ab * t)).length; + if (dist < minDist) minDist = dist; + } + + const pathWidth = 1.8; + const pathDepth = 0.45; + const bermHeight = 0.25; + + // Carve central path trough + final trench = (1.0 - (minDist / pathWidth).clamp(0.0, 1.0)) * pathDepth; + // Build gentle spoil berm along the verge + final verge = ((minDist - pathWidth * 0.8) / (pathWidth * 0.8)).clamp(0.0, 1.0); + final berm = math.sin(verge * math.pi) * bermHeight; + + return baseHeight - trench + berm; +} +``` + +### 4. Macro-massed pebble scatter (avoiding uniform sandpaper noise) + +Gravel and pebbles cluster into water-washed scour lines rather than spreading evenly over an entire level. Gate multi-scale pebble instances with a low-frequency macro massing field and key the hash off integer cell coordinates: + +```dart +void scatterPebbles(InstancedMesh finePebbles, InstancedMesh largeStones, FastNoiseLite terrainNoise) { + final macroNoise = FastNoiseLite(seed: 42)..frequency = 0.05; + const step = 0.8; + const cells = 80; // 64m / 0.8m + for (var ix = 0; ix < cells; ix++) { + for (var iz = 0; iz < cells; iz++) { + final x = ix * step; + final z = iz * step; + // Deterministic coordinate jitter keyed off integer cell indices + final h = noiseHash2(1337, ix, iz); + final jx = x + ((h & 0xFF) / 255.0 - 0.5) * 0.6; + final jz = z + (((h >> 8) & 0xFF) / 255.0 - 0.5) * 0.6; + + final mass = (macroNoise.getNoise2(jx, jz) + 1.0) * 0.5; + if (mass > 0.65) { + final y = terrainNoise.getNoise2(jx, jz) * 8.0; + final matrix = vm.Matrix4.translation(vm.Vector3(jx, y, jz)); + if (((h >> 16) & 0xFF) > 180) { + largeStones.addInstance(matrix); + } else { + finePebbles.addInstance(matrix); + } + } + } + } +} +``` + +### 5. Oceans and Gerstner waves + +Trochoidal Gerstner waves pull vertices horizontally toward wave peaks, creating sharp crests and wide flat troughs. Sum multiple directional waves and compute normals analytically: + +```glsl +// GLSL Gerstner wave displacement +struct Wave { vec2 dir; float amp; float freq; float speed; float steepness; }; + +// Caller seeds accumulators with tangent = vec3(1.0, 0.0, 0.0) and binormal = vec3(0.0, 0.0, 1.0). +vec3 evaluateGerstner(vec2 pos, float time, Wave w, float numWaves, inout vec3 tangent, inout vec3 binormal) { + vec2 d = normalize(w.dir); + float phase = dot(d, pos) * w.freq + time * w.speed; + float c = cos(phase); + float s = sin(phase); + float q = w.steepness / (w.amp * w.freq * numWaves); + + tangent += vec3(-q * d.x * d.x * w.amp * w.freq * s, + d.x * w.amp * w.freq * c, + -q * d.x * d.y * w.amp * w.freq * s); + binormal += vec3(-q * d.x * d.y * w.amp * w.freq * s, + d.y * w.amp * w.freq * c, + -q * d.y * d.y * w.amp * w.freq * s); + + return vec3(q * w.amp * d.x * c, + w.amp * s, + q * w.amp * d.y * c); +} +``` + +For shallow water transitions and shorelines: +- **Beer-Lambert Depth Extinction**: Declare `engine_inputs: [ depth ]` in the `.fmat` to sample linear opaque scene depth (`RenderInput.depth`). Compute water depth `d = sceneDepth - surfaceDepth` and attenuate color with `C = C_deep + (C_shallow - C_deep) * exp(-sigma_a * d)`. +- **Tidal Wet Sand**: Reduce sand roughness to 0.15 and multiply albedo by 0.6 within the wave wash zone to produce glistening wet shorelines. + +### 6. Trees, branching splines, and backlit foliage + +Trunk and branch structures follow Leonardo da Vinci's rule: total cross-sectional area is conserved across splits (d_parent^2 = sum d_child^2). Extrude branches along swept spline tubes using `TubeGeometry` (sweeping a round cross-section along a `ScenePath`) or `ExtrudeGeometry`: + +- **Backlit Leaf Translucency**: Set `Material.doubleSided = true` for two-sided rendering. In a custom leaf shader, add a diffuse transmission term so backlit foliage glows rather than rendering as a dark silhouette: +```glsl +// In custom leaf shader +float NdotL = dot(normal, lightDir); +float backLight = max(0.0, -NdotL) * leafTransmissionFactor; +vec3 litColor = albedo * (max(0.0, NdotL) + backLight * leafTranslucentColor); +``` +- **Quadratic Cantilever Wind**: Displace leaf and branch vertices in world space proportional to height squared (delta_p = windVec * (h / h_max)^2 * sin(omega * t - k * p)) so tips sway vigorously while roots remain anchored. + +### 7. Procedural skies and runtime IBL synchronization + +Use `PhysicalSkySource` (`lib/src/sky_sources.dart`) with analytic Rayleigh and Mie scattering. Assign `SkyEnvironment` to `Scene.skyEnvironment` or call `EnvironmentMap.fromSky` to bake prefiltered radiance and SH-9 diffuse coefficients into the scene's IBL automatically, and assign the source to `Scene.skybox` for matching background visuals. + +### 8. Islands, coastal bays, and sand dunes + +To form natural island topographies: +- **Domain-Warped Island Mask**: Multiply a radial distance falloff (1.0 - (r / R)^2) with domain-warped FBM to form organic bays, sandbars, and peninsulas rather than symmetrical circular cones. +- **Slope-Based Sediment Stripping**: Compute heightfield slope sqrt((dh/dx)^2 + (dh/dz)^2). Steep cliffs strip topsoil to expose rock strata, while gentle coastal planes accumulate golden beach sand. +- **Anisotropic Wind Dune Ripples**: Layer 8:1 anisotropically stretched noise perpendicular to the prevailing wind direction to generate fine ripple crests across sand surfaces. + +--- + +## The web noise caveat, expanded + +The Dart `FastNoiseLite` port relies on 32-bit integer arithmetic. On native platforms this is exact. On the web (dart2js), a Dart `int` is a JavaScript double, exact only to 53 bits, so the integer hash loses its low bits and 3D noise can overflow. The result is a plausible-looking but wrong field, silent, and web-only. A web-safe integer multiply for the Dart side is a planned follow-up. + +The GLSL half of the module is unaffected, it is correct on every backend including WebGL2, and implements the same algorithms with the same tables and seeds, so a field sampled on the CPU (native) and evaluated in a shader agree. The agreement has two tiers: + +- **Bit-exact**: `noiseHash2`/`noiseHash3` (and GLSL `NoiseHash2`/`NoiseHash3`) are pure integer math and match bit for bit across backends. Use them for decisions that must never disagree between machines (world generation, deterministic placement). +- **Float-close**: the float noise functions match within a small tolerance (float32 rounding differs per GPU), imperceptible visually. Do not re-derive a hard threshold from float noise on both the CPU and GPU sides, make the decision once and share the result. + +Both tiers carry the web-overflow caveat on the Dart side. Strategy by target: + +- **Native only**: use the Dart `FastNoiseLite` freely, on the render isolate or a background one. +- **Web, per-fragment noise**: move it to the GLSL side (`#include ` in a `.fmat` block). +- **Web, a static field**: bake it with `bakeNoiseTexture` (or `bakeNoisePixels` in a build hook / native isolate) and sample the texture. This sidesteps the overflow because the baking happens where `int` is 64-bit. + +--- + +## InstancedMesh, thousands of copies for one draw + +One geometry/material pair drawn many times, each placed by its own model transform. The whole set is one render item, one pipeline, one cull test. This is how you scatter foliage, crowds, debris, or a grid of the same prop without a node per copy. + +```dart +class InstancedMesh { + InstancedMesh({ + required Geometry geometry, + required Material material, + bool cullInstances = false, // per-instance cull after the aggregate pass + bool sortTransparentInstances = true, + }); + + int get instanceCount; + + int addInstance(vm.Matrix4 transform, {vm.Vector4? color}); // matrix is CLONED; returns index + void setInstanceTransform(int index, vm.Matrix4 transform); + void updateInstanceTransforms( + void Function(List transforms) update, { + bool recomputeWinding = true, + }); + void setInstanceColor(int index, vm.Vector4 color); // linear RGBA multiplier + void removeInstanceAt(int index); // shifts later indices down + void clearInstances(); +} +``` + +Attach it to a node with an `InstancedMeshComponent` (it does not go on `Node(mesh:)`): + +```dart +final mesh = InstancedMesh(geometry: geo, material: mat); +for (final placement in placements) { + mesh.addInstance(placement); // a Matrix4 in the instanced mesh's local space +} +final node = Node()..addComponent(InstancedMeshComponent(mesh)); +scene.add(node); +``` + +Practical notes: + +- `addInstance` clones the matrix, so reusing one scratch `Matrix4` across the loop is fine. +- The node the component is on transforms the entire batch. Instance transforms compose under it. +- To animate all instances cheaply, use `updateInstanceTransforms`, which invalidates the batch once instead of per call. Mutate the matrices in the callback list; do not add, remove, or replace entries. +- `updateInstanceTransforms(recomputeWinding: false)` skips the parity refresh. Only pass it when no edit changes a transform's winding. A mirrored (negative-determinant) edit under it renders those instances inside-out. +- `cullInstances: true` pays for per-instance culling, worth it for a large spatial spread whose instances enter view at different times; leave it off for a small compact clump that the single aggregate cull already handles. +- Set `cullInstances` per instanced mesh based on that trade; it is not a global. + +--- + +## Modular kits from the built-in primitives + +Before authoring a mesh, remember the ten primitives assemble a surprising amount by composition, no builder needed. Each is a `Geometry`, so each goes on its own `Node`, and a parent node groups a kit piece you can clone and place. + +| Class | Constructor | Notes | +| --- | --- | --- | +| `CuboidGeometry` | `CuboidGeometry(vm.Vector3 extents)` | Box from `-extents/2` to `+extents/2`. Positional. | +| `SphereGeometry` | `SphereGeometry({radius = 0.5, segments = 32, rings = 16})` | UV sphere. | +| `IcosphereGeometry` | `IcosphereGeometry({radius = 0.5, subdivisions = 2})` | Even triangle distribution. | +| `CylinderGeometry` | `CylinderGeometry({bottomRadius = 0.5, topRadius = 0.5, height = 1.0, ...})` | `topRadius: 0` makes a cone; different radii make a frustum. | +| `CapsuleGeometry` | `CapsuleGeometry({radius = 0.5, height = 1.0, ...})` | `height` is the mid-section; total Y is `height + 2*radius`. | +| `TorusGeometry` | `TorusGeometry({radius = 0.5, tubeRadius = 0.2, ...})` | Lies in XZ. | +| `PlaneGeometry` | `PlaneGeometry({width = 1.0, depth = 1.0, segmentsX = 1, segmentsZ = 1})` | XZ plane, faces +Y. | +| `DiscGeometry` | `DiscGeometry({radius = 0.5, segments = 32})` | Filled circle, XZ, faces +Y. | +| `RingGeometry` | `RingGeometry({innerRadius = 0.25, outerRadius = 0.5, segments = 32})` | Annulus, XZ, +Y. | +| `WedgeGeometry` | `WedgeGeometry(vm.Vector3 size)` | Triangular prism; base on `y = 0` (not Y-centered). | + +Because a cone is just `CylinderGeometry(topRadius: 0)`, a tree is a green cone on a brown cylinder, a fence is repeated thin cuboids, a table is a plane on four cylinders. Assemble each piece as a parented `Node` subtree, then `clone()` and place it, or feed the placements to an `InstancedMesh` when the same piece repeats many times. + +### Kit pieces must abut, never overlap + +Two faces of different pieces in one plane flicker as the camera moves (z-fighting), at any distance and on any device, so a kit that overlaps its pieces looks broken in motion even when a still looks right. + +- **Size a repeated piece to its spacing.** Fence rails, barriers, and curbs placed every `s` meters are `s` meters long. A piece 0.4 m longer than its spacing puts 0.4 m of coplanar faces at every joint. +- **Cap joints and corners with a post** a centimeter or two larger than the pieces on every face, and stop perpendicular runs at the post instead of running both into the corner. +- **Make stripes from color, not overlap.** Alternate per-instance `color` on abutting pieces, or paint stripes with vertex colors, rather than laying a strip of one color over another. +- **Scatter flat pieces without overlap.** Patches, tiles, and rugs at one height must not overlap each other; reject a candidate whose footprint overlaps one already placed, or give each kind its own `Material.depthLayer`. +- **Wrap with a clear gap or not at all.** A band or trim box wrapped around a larger box sits a clear distance proud (tens of centimeters for content seen from hundreds of meters), and two wraps on one piece use separate height slots. Facade detail seen from far away (windows on a skyline) belongs in the material rather than in thousands of proud boxes. +- **Overlays get a layer.** Paint, signs, and decal quads that lie on a surface get `material.depthLayer = 1`. + +`scene.findCoplanarOverlaps()` lists every overlap left, with the length that fixes a repeated piece, and debug builds print a summary once the scene holds still. + +Every primitive except `PlaneGeometry` exposes a `Shape get collisionShape` for the physics package, so a code-built kit gets colliders for free. + +### Swept geometry for shapes primitives cannot make + +For paths, tubes, and profiles, sweep a `ScenePath` (`BezierPath`, `CatmullRomPath`, `PolylinePath`): + +- `TubeGeometry(path, {radius = 0.5, radialSegments = 12, stations = 64, caps = true})` for pipes, cables, vines. +- `ExtrudeGeometry(path, {required List profile, stations = 64, caps = true})` sweeps a 2D profile along the path (railings, moldings, extruded logos). +- `RibbonGeometry(path, {width = 1.0, stations = 64, alignment = RibbonAlignment.ground})` for flat strips (roads, trails). + +These build detailed shapes from a curve and a few parameters, often replacing an imported model outright. diff --git a/.cursor/skills/flutter_scene-verification-loop/SKILL.md b/.cursor/skills/flutter_scene-verification-loop/SKILL.md new file mode 100644 index 00000000..e21261ca --- /dev/null +++ b/.cursor/skills/flutter_scene-verification-loop/SKILL.md @@ -0,0 +1,71 @@ +--- +name: flutter_scene-verification-loop +version: 4 +description: Close the visual-iteration loop when building or debugging a flutter_scene 3D app so you see your own output and self-correct. Use whenever a change affects what renders (geometry, materials, lighting, shaders, post-processing) or a frame looks wrong (black, washed-out, see-through, missing geometry). +--- + +# Verifying flutter_scene visually + +flutter_scene renders 3D. A rendering change you cannot see is a guess, and guessing at pixels is the single biggest waste of iterations. The highest-value habit is a closed visual loop plus judgment that does not drift. This skill is that loop. + +**The one thing to internalize: run it, let it settle, look at the frame AND the console, localize before you edit, repeat.** Do not change code off a hypothesis you did not confirm from the actual output. + +## The loop + +1. **Run.** Launch the app with `flutter run --enable-flutter-gpu` (native; the flag is mandatory and it is the only run flag, see the idioms skill). Where the editor MCP is connected, `run_project` launches the managed session instead. +2. **Settle.** A live frame is a moving target. Auto-exposure is still ramping, particles have a random phase, an animation is mid-clip, IBL re-bakes after the first present. Let the scene reach steady state (a few frames, or until the image stops changing) before you trust a capture. Do not screenshot the first frame and reason from it. +3. **Capture the frame AND read the console.** A screenshot alone hides errors that print; the console alone hides wrong pixels. Take both every time. Baseline path is a screenshot plus the run log; with the editor MCP it is `screenshot_viewport` plus `get_console`. +4. **Localize before editing.** When something is wrong, find where it goes wrong before you touch code. Read an intermediate buffer, read a single pixel's exact value, or scan for non-finite values. A NaN or Inf propagates silently into black or garbage downstream, so the first pass that produced it is the culprit, not the pass where you see the black. `references/loop.md` has the tool table and a symptom to action map. +5. **Correct, repeat.** Make one change, run the loop again. One change per iteration keeps cause and effect legible. + +## Flicker needs motion, not a screenshot + +Two surfaces that overlap in one plane trade pixels as the camera moves (z-fighting), and any single frame can look right. Wherever surfaces meet or lie on each other (paint on a road, a screen on a wall, barriers along a track, overlapping tiles), prove the scene clean with a number: `await scene.probeDepthConflicts(camera: camera)` from each camera the app uses, driving `report.conflicts` to empty (each entry names the two nodes, the pixels they trade, and the distance). `scene.debug.overlays.add(DebugOverlay.depthConflicts)` shows the same live in magenta, and the editor MCP's `scan_for_depth_conflicts` runs the probe for you. Without either, compare two frames a few centimetres of camera motion apart. + +## The readiness gate (do not debug through it) + +Rendering is gated on `Scene.initializeStaticResources()`. Until that Future completes, every frame is skipped and the engine prints exactly: + +``` +Flutter Scene is not ready to render. Skipping frame. +``` + +If you see that line, the scene is not broken, it is not ready. Wait for readiness (build geometry and materials inside `initializeStaticResources().then(...)`, gate the widget on `Scene.isReadyToRender`) before you diagnose anything else. A black frame while that line prints is the gate, not your code. + +A second, quieter gate: the shaders behind `PhysicallyBasedMaterial` extensions (clearcoat, sheen, transmission, and the rest) and `ShadowCatcherMaterial` load on first use, and the scene holds its previous frame (or draws nothing, before its first) until they land. Await `Scene.preload()` before a capture so it shows the scene as it now is. + +## Judge blind, never self-score (the load-bearing rule) + +When deciding whether a change improved the look, **do not assign the frame a quality score.** Self-assigned scores drift upward, because the model is grading its own trajectory and wants to have made progress. That drift is how a session convinces itself a regression is an improvement. + +Instead, **compare two frames and return a binary pick.** Put the new frame next to a reference (a known-good target) or the previous frame, and answer only "which of these two is better", A or B. No number, no "8/10", no "looks pretty good now". A blind pairwise pick does not inflate the way a solo score does. This applies to every visual review, including the ones that feel obvious. + +If you have no reference at all, say so and describe the concrete difference between the two frames (this one is brighter here, that one has an artifact there) rather than inventing a score. + +## Empirical verification rules + +These rules prevent false diagnoses, hollow passes, and measurement traps during visual iteration: + +1. **Liveness before ablation.** A negative result is evidence only if the removed or modified term was actually live in the draw pass. Diff pixels for liveness before trusting an ablation. +2. **Tools must fail loud on empty measurements.** A tool that measures an empty population, zero pixels, or non-finite data must fail loud rather than returning a default passing number. +3. **Population discipline.** Always quote the population window, crop rectangle, brightness threshold, and rendering resolution beside any color or lighting figure. +4. **Baselines expire quickly.** Two captures taken hours apart in a changing tree reflect multiple edits; isolate paired A/B captures with temporary snapshots or git worktrees. +5. **Look at the raw frame before quoting numbers.** Inspect the actual captured frame before taking numbers off it; metrics can yield valid-looking numbers on corrupt frames. +6. **Negative control requirement.** A metric that returns the same score for positive and negative control populations cannot serve as evidence for either. +7. **Explanations are hypotheses, not evidence.** Before adopting an explanatory mechanism, identify the specific numerical observation that would differ if the mechanism were false. +8. **Attribution must reach the triangle.** When diagnosing geometry defects, trace down to the specific triangle indices and edge lengths rather than stopping at the mesh component. +9. **Symmetric domain clamping.** Clamping parametric lookup domains must be verified at both boundaries, and derived slope or heading accessors must clamp both sample points. +10. **Multi-band octave tables for tiling.** Two-band ratios like `hf/lf` are blind to regular mid-frequency patterns; inspect multi-octave energy tables to detect periodic tiling. +11. **Physical discrimination over naming.** A class-discriminating physical observation (such as missing shadow terminators) outranks code comments or variable names. +12. **Grazing light terminator crossing.** Under low-angle grazing lighting, prioritize terminator-crossing fractions over slope RMS to detect normal map over-amplification. +13. **Resolution scaling.** High-frequency energy (`hf/lf`) and relative contrast scale with resolution; compare them only at equal pixel resolutions. +14. **Paired capture isolation.** Snapshot source files or freeze environment state when capturing before/after pairs so background modifications cannot corrupt the comparison. + +## Be honest about tooling + +The richest observation tooling lives behind the editor MCP (`flutter_scene_mcp`): screenshots, console, NaN scans, render-graph capture, per-pass and per-pixel readback, viewport debug modes. A general-purpose observation server for an arbitrary running game is still being built, so do not assume those tools exist for every project. When the MCP is not connected, the baseline loop is still fully usable: `flutter run --enable-flutter-gpu`, read the console, take a screenshot. Do not claim a capability you cannot reach in the current project. + +## More depth + +- `references/loop.md` for the full editor-MCP tool table, the symptom to action map (black frame, washed-out, see-through, missing geometry), and the settle details. +- The `flutter_scene-idioms` skill (`references/traps.md`) for the underlying mistakes each symptom points back to (wrong vertex layout, hand-rolled winding flip, transform-in-place, the blank-frame causes). diff --git a/.cursor/skills/flutter_scene-verification-loop/references/loop.md b/.cursor/skills/flutter_scene-verification-loop/references/loop.md new file mode 100644 index 00000000..0ed9ae6d --- /dev/null +++ b/.cursor/skills/flutter_scene-verification-loop/references/loop.md @@ -0,0 +1,161 @@ +# The verification loop in detail + +The core loop, the readiness gate, and the blind-judgment rule are in `SKILL.md`. This file has the +tool table (what exists where), the settle details, and the symptom to action map. + +--- + +## Two tooling tiers + +### Baseline (any project, no MCP) + +This always works and needs nothing installed beyond the package setup. + +- Launch: `flutter run --enable-flutter-gpu` (native; add `-d chrome` for web). The flag is mandatory. +- Console: read the run log. The readiness line, `debugPrint` output, asserts, and the 0.22.0 + blank-frame diagnostic all land here. +- Frame: take a screenshot of the running app after it settles. + +That is the whole loop when there is no editor. Run, settle, screenshot, read the log, correct. + +### Editor MCP (`flutter_scene_mcp`, when connected) + +The editor exposes richer observation. Tool names below are exact. Do not assume they exist unless +the MCP is actually connected for the current project. + +| Tool | What it does | Reach for it when | +| --- | --- | --- | +| `run_project` | Launch the editor-managed Play session (a managed `flutter run`). | Starting a session under the editor. | +| `build_project` | Start the selected build config; output streams to the console. | You want a build without launching. | +| `stop_project` | Stop the running session. | Ending or restarting cleanly. | +| `hot_reload` | Hot reload the running debug session. | A Dart-only change, fastest turnaround. | +| `hot_restart` | Hot restart the session. | State or startup changed, or reload did not take. | +| `get_console` | The build/run console tail plus building/running flags. | EVERY iteration, paired with a screenshot. | +| `screenshot_viewport` | The viewport as a PNG, what the user sees. | EVERY iteration, paired with the console. | +| `describe_scene` | The scene-graph tree (ids, paths, names, component types). | Confirming a node/mesh is actually in the scene. | +| `scan_for_nans` | Capture a frame and scan every float render target for NaN/Inf in pass order. | A black or garbage frame with no error. Find where non-finite values start. | +| `scan_for_depth_conflicts` | Probe the viewport camera for surfaces that trade pixels as the camera moves (z-fighting) and list the node pairs, pixel counts, and distances. | Stripes, speckles, or a surface that looks right only from some angles. | +| `capture_render_graph` | Capture the next frame's graph with thumbnails. | You need to see intermediate buffers. | +| `list_render_passes` | The executed passes in order with CPU timings and the buffer keys each read/wrote, plus target formats and sizes. No images. | Learning which pass owns which buffer, and the key names to read. | +| `get_pass_output` | Render one captured buffer (a key like `scene_color`, `linear_depth`) as a PNG. NaN paints magenta, Inf yellow, negative blue. | Eyeballing an intermediate buffer to see which stage broke. | +| `read_pass_pixel` | One pixel's exact float RGBA from a captured buffer, with NaN/Inf flags. | Confirming an exact value (is this really 0, or NaN, or negative). | +| `list_viewport_debug_modes` | The available debug outputs (final, HDR color, linear depth, normals, AO, shadow atlas, ...) and which is active. | Seeing what debug views exist. | +| `set_viewport_debug_mode` | Render one debug output full-viewport. Set `final` to restore. | Inspecting depth/normals/AO live, paired with `screenshot_viewport`. | + +Render-graph capture (`capture_render_graph`, `list_render_passes`, `get_pass_output`, +`read_pass_pixel`, `scan_for_nans`) is gated on `Scene.debugAllowRenderGraphCapture`. It is a debug +opt-in, so a release build or a scene that never armed it returns nothing. The editor arms it for you; +outside the editor, set `Scene.debugAllowRenderGraphCapture = true` and call +`Scene.captureRenderGraph(...)` directly. + +--- + +## Settle, do not seed + +Frames differ from run to run for benign reasons. That is normal, not a bug to eliminate. + +- **Auto-exposure** (`Scene.autoExposure`) ramps toward the target over `speedUp`/`speedDown` seconds, + so the first second is darker or brighter than the settled image. +- **Particles and trails** carry a random phase, so a `ParticleSystem` looks different every launch. +- **Animations** are mid-clip unless you seek them, so a screenshot lands on an arbitrary frame. +- **Image-based lighting** re-bakes after the first present on some paths, so reflections dim in for + a frame before they are correct. + +So let the scene settle before you trust a capture. Watch until the image stops changing, or advance +a fixed few frames, then screenshot. Judge the settled frame, not the first one. + +**Seeding is a different job.** Strict determinism (a fixed random seed, a pinned animation time, a +frozen exposure) is what you set up for pixel-exact regression comparison, where two runs must be +byte-identical. You do not need it for ordinary observation. For "does this change look right", settle +and look. Reserve the seeding work for when you are building a golden or diffing two runs at the pixel +level. + +--- + +## Symptom to action map + +Localize before editing. Each row says what to capture first and the mistakes it usually points back +to. The mistakes are detailed in the `flutter_scene-idioms` skill's `references/traps.md`; this map +routes a symptom to the right one. + +### Entirely black frame + +1. Read the console FIRST. If `Flutter Scene is not ready to render. Skipping frame.` is printing, it + is the readiness gate, not your scene. Wait for `Scene.initializeStaticResources()`. Stop here. +2. In 0.22.0 a frame that issues zero draws prints once in debug naming the likely cause (not ready, + empty region, no views, no visible meshes, or a layer mask matching nothing). Read that line. +3. If draws are happening but the image is black, `scan_for_nans`. A NaN or Inf anywhere upstream + collapses the final image to black, and the scan names the first offending pass. Then + `get_pass_output` on that pass's buffer (NaN shows magenta) to confirm. +4. Common non-NaN causes: a degenerate camera (target equals position, `up` parallel to the view + direction on a top-down camera, FOV passed in degrees not radians), `layerMask: 0`, an oversized + environment texture that failed to allocate on the device. See traps #24 and #17. + +### Washed-out, low-contrast, or too-bright color + +1. `screenshot_viewport` after settling, and check whether auto-exposure has finished ramping (a + too-bright first second is just the ramp). +2. If it persists, suspect a shader-output contract break. A custom `ShaderMaterial`/`PostEffect`/sky + shader must output linear HDR premultiplied by alpha. Tone-mapping or gamma-encoding in the shader + gets applied a second time by the resolve pass, giving exactly this washed-out look. See traps #38 + and the root `MATERIALS.md`. +3. Also check for a non-color texture bound as color (a normal or metallic-roughness map without the + right `TextureContent`), which reads wrong and distance-dependent. Trap #3. +4. Hand-packed vertex data at the wrong stride also washes out color (the color attribute lands at the + wrong offset). `describe_scene` plus trap #18. + +### See-through or inside-out faces + +1. `set_viewport_debug_mode` to normals (or `get_pass_output` on the normals buffer) and look at the + orientation. Inverted normals confirm a winding problem. +2. Cause is almost always clockwise hand-built triangles. flutter_scene front faces wind + COUNTER-CLOCKWISE (CCW) in model space, matching glTF and standard conventions. Ensure triangle + indices wind CCW around the outward face normal, or omit normals and let the constructor derive + them. NEVER fix orientation with a per-triangle winding flip on an imported model; that leaves + normals and IBL wrong. Traps #14 and #18. +3. For an imported model rendered mirrored, check you did not overwrite the runtime importer's + `scale(1, 1, -1)` handedness root. Trap #6. + +### Missing or popping geometry + +1. `describe_scene` to confirm the node is actually in the graph. If it is absent, it is a scene-build + bug, not a render bug. +2. If it is present but invisible, check the layer mask (`Node.layers` is a bitmask, NOT inherited, + and must match the view's `layerMask`; `layers = 2` means `1 << 1`, not "layer 2"). Trap #11. +3. If it appears and disappears with camera angle, the bounds do not cover the geometry (a + caller-supplied `bounds` or `setLocalBounds` that is too small, or a swapped primitive geometry on + an older version). Widen or omit the bounds. Traps #9 and #25. +4. A moved skinned mesh that will not move is the skinned-node transform being ignored by design; move + the skeleton root instead. Trap #5. + +### Flickering stripes or speckles where surfaces meet + +1. Do not judge this from one frame. Run `scan_for_depth_conflicts` (editor MCP) or `await + scene.probeDepthConflicts(camera: camera)` in the app; each conflict names two nodes, the pixels + they trade, and the distance. `scene.debug.overlays.add(DebugOverlay.depthConflicts)` marks them + live in magenta. +2. Two faces overlapping in one plane (repeated pieces longer than their spacing, overlapping patches + at one height, a duplicated node) fight at any distance; fix the content. `scene.findCoplanarOverlaps()` + lists the overlapping faces with the length that fixes a repeated piece. +3. An overlay meant to lie on a surface (a screen, a sign, road paint) gets `material.depthLayer = 1`. +4. Faces a few centimetres apart that fight only far away need a larger gap or a layer. The + `depthGap` debug view shows the gap each pixel needs. See the idioms skill's + `references/depth-and-layering.md`. +5. Re-probe until the conflict list is empty. + +### A value looks numerically wrong (not visually) + +Use `read_pass_pixel` on the relevant buffer to read the exact float RGBA at a coordinate, with NaN/Inf +flags. This settles "is this pixel actually 0.5, or is it NaN, or negative" without eyeballing a PNG +that the display remap has already clamped. + +--- + +## Judgment, restated + +The blind-pairwise rule from `SKILL.md` is the part most likely to be skipped, so it bears repeating +here. When you have a before and an after, put them side by side and pick the better one as a binary +A-or-B choice against a reference or the previous frame. Do not narrate a score. A solo score climbs +on its own because you are grading your own progress; a blind pick between two concrete frames does +not. Every visual review runs through a pairwise pick, including the ones that feel too obvious to +bother with. diff --git a/.cursor/skills/serverpod-auth/references/setup.md b/.cursor/skills/serverpod-auth/references/setup.md index afd721af..bf6d5c8e 100644 --- a/.cursor/skills/serverpod-auth/references/setup.md +++ b/.cursor/skills/serverpod-auth/references/setup.md @@ -45,6 +45,18 @@ client = Client(serverUrl) client.auth.initialize(); ``` +## Passwordless email + +To sign in with a one-time password sent by email, extend `EmailOtpIdpBaseEndpoint` instead of `EmailIdpBaseEndpoint` on the server. `SignInWidget` then shows `EmailOtpSignInWidget`, which can also be used on its own. + +The codes are sent through `EmailIdpConfig.sendOtpLoginVerificationCode`, falling back to `sendRegistrationVerificationCode` when it is not set. Set the OTP callback if the registration emails use `accountRequestId`, e.g. in links. + +The code length is not sent to the client, so configure it on both sides. The server generates 6 digit codes by default (`EmailIdpConfig.otpLoginVerificationCodeGenerator`), and the widget's code input defaults to 6 digits too (`EmailOtpSignInWidget(otpVerificationCodeConfig: VerificationCodeConfig(length: ...))`). Change both together. + +Customize the texts with `SignInLocalizationProvider(emailWithOtp: EmailWithOtpSignInTexts.defaults.copyWith(...))`. + +Customize the resend-code text shared by both email flows with `SignInLocalizationProvider(verificationCodeTexts: VerificationCodeTexts.defaults.copyWith(resendCode: ...))`. + ## Social sign-ins Each provider needs the user to configure it outside the project (e.g. the GCP console or the Apple developer portal). Follow the official setup guide: diff --git a/.cursor/skills/serverpod-configuration/SKILL.md b/.cursor/skills/serverpod-configuration/SKILL.md index ce7f24b5..e71bb72e 100644 --- a/.cursor/skills/serverpod-configuration/SKILL.md +++ b/.cursor/skills/serverpod-configuration/SKILL.md @@ -21,7 +21,9 @@ apiServer: publicScheme: http ``` -`publicHost`/`publicPort`/`publicScheme` are used for URLs returned to clients. +`port` is the port the listener binds. `publicHost`/`publicPort`/`publicScheme` are used for URLs returned to clients; a `publicPort` of 0 follows the bound port. + +In development the Serverpod runner keeps the configured ports unless another runner on the machine holds them, in which case it binds ephemeral ports (through `SERVERPOD_API_SERVER_PORT`, its siblings and `SERVERPOD_DATABASE_PORT`; the `_PUBLIC_PORT` variables of the api, insights and web listeners carry the same value, so URLs returned to clients use the bound port) and publishes the real addresses in `serverpod runner status`. See [Several servers at once](../serverpod-overview/SKILL.md#several-servers-at-once) in the overview skill. ## Database @@ -40,9 +42,9 @@ database: Password in `passwords.yaml` under run mode or `SERVERPOD_PASSWORD_database`. Optional: `searchPaths`, `maxConnectionCount`, `requireSsl`, `isUnixSocket`. -**Embedded PostgreSQL:** with `dataPath` set, Serverpod launches and manages a PostgreSQL instance in that directory, so nothing has to be installed or run separately. New projects use it for development and testing. `serverpod database start [--mode ] [--port ]` boots the same database on its own, without the server. +**Embedded PostgreSQL:** with `dataPath` set, Serverpod launches and manages a PostgreSQL instance in that directory, so nothing has to be installed or run separately. New projects use it for development and testing. It listens on its Unix socket, plus loopback TCP on `port` when the run mode has a database password. In development the runner moves that port aside with the server ports when another Serverpod runner holds it, so several checkouts can run their own at once. `serverpod database start [--mode ] [--port ]` boots the same database on its own, without the server, or joins the one the server already runs. -Without `dataPath` the database is external, and `serverpod start` starts Docker Compose services when the project has a compose file and PostgreSQL is on localhost (`--docker` / `--no-docker` override this). +Without `dataPath` the database is external, and the runner (`serverpod start`) starts Docker Compose services when the project has a compose file and PostgreSQL is on localhost (`--docker` / `--no-docker` override this). ### SQLite diff --git a/.cursor/skills/serverpod-endpoints/SKILL.md b/.cursor/skills/serverpod-endpoints/SKILL.md index 3055cebb..f5566edb 100644 --- a/.cursor/skills/serverpod-endpoints/SKILL.md +++ b/.cursor/skills/serverpod-endpoints/SKILL.md @@ -5,7 +5,7 @@ description: Define Serverpod endpoints, use Session, pass parameters, and call # Serverpod Endpoints -Extend `Endpoint` with instance methods; first parameter is `Session`, return `Future` (or `Stream` for real-time data streaming). Place anywhere under server `lib/`. If `serverpod start` is not running with hot-reload, run `serverpod generate` to update the client. +Extend `Endpoint` with instance methods; first parameter is `Session`, return `Future` (or `Stream` for real-time data streaming). Place anywhere under server `lib/`. The Serverpod runner regenerates the client on save; without one, run `serverpod generate`. ## Defining an endpoint diff --git a/.cursor/skills/serverpod-migrations/SKILL.md b/.cursor/skills/serverpod-migrations/SKILL.md index 3308ce0e..75440742 100644 --- a/.cursor/skills/serverpod-migrations/SKILL.md +++ b/.cursor/skills/serverpod-migrations/SKILL.md @@ -7,7 +7,7 @@ description: Serverpod database migrations — when and how to create/apply/repa Serverpod has a migration system that generates SQL for changes to models with `table` in `.spy.yaml`. The migrations are applied: -- When the `apply_migrations` tool is called via the `serverpod` MCP. Typically during development with a running `serverpod start`. +- When the `apply_migrations` tool is called via the `serverpod` MCP. Typically during development with the Serverpod runner running (`serverpod start` or `serverpod runner start`). - When the server is started with `dart run bin/main.dart --apply-migrations` flag. Typically when running the server in production. ## When migrations are needed @@ -20,20 +20,20 @@ Migrations are not needed when the project has no database configured (e.g. `con ## Standard flow -The standard flow for creating and applying migrations is simplified when a `serverpod start` is running. +The standard flow for creating and applying migrations is simplified when the runner is running. -### With a running `serverpod start` and MCP server +### With a running runner and MCP server -When the server is running from `serverpod start` use the `serverpod` MCP to: +When the server is running under the runner use the `serverpod` MCP to: 1. Create a migration using the `create_migration` tool. 2. Apply the migration using the `apply_migrations` tool. -ALWAYS use the MCP server if it is available. +ALWAYS use the MCP server if it is available. When it reports that the server is not running, call `tail_runner_logs`, then run `serverpod runner start` in the server package when no runner is running, then call the tools again. -### ONLY if MCP server fails to connect +### ONLY if the MCP is not available or the runner cannot bring the server up -When the server is not running from `serverpod start` use the CLI commands to: +When the MCP is not available or `serverpod runner start` fails, use the CLI commands to: 1. Ensure the code is generated by running `serverpod generate`. 2. Create a migration using the `serverpod create-migration` command. @@ -60,14 +60,14 @@ On the client side (models with `database: client` or `database: all`), the same If the database is in an inconsistent state, a repair migration brings it back to a consistent state. It is created by reading the live schema and diffing it against a target migration version, so the database must be reachable. -### With a running `serverpod start` and MCP server +### With a running runner and MCP server 1. Create the repair migration using the `create_repair_migration` tool. Optional arguments: `version` (target migration version, defaults to the latest), `tag`, and `force` (required for destructive changes, or when no drift is detected). 2. Apply it using the `apply_migrations` tool, which applies both pending and repair migrations without restarting the server. -ALWAYS use the MCP server if it is available. +ALWAYS use the MCP server if it is available. When it reports that the server is not running, call `tail_runner_logs`, then run `serverpod runner start` when no runner is running, then call the tools again. -### ONLY if MCP server fails to connect +### ONLY if the MCP is not available or the runner cannot bring the server up ```bash # Use the `--mode` flag to specify the run mode diff --git a/.cursor/skills/serverpod-models/SKILL.md b/.cursor/skills/serverpod-models/SKILL.md index 37f3409f..5489e98e 100644 --- a/.cursor/skills/serverpod-models/SKILL.md +++ b/.cursor/skills/serverpod-models/SKILL.md @@ -7,7 +7,7 @@ description: Define Serverpod data models in YAML (.spy.yaml) — fields, defaul Models are defined in `.spy.yaml` files anywhere under server `lib/`. They generate Dart classes for server and client, and optionally database tables. -After each change to models, ensure that the code is generated (automatically when a `serverpod start` is running or manually with `serverpod generate`). If models with `table` have changed, the database schema must be updated following the [migration workflow](../serverpod-migrations/SKILL.md). +After each change to models, ensure that the code is generated (automatically when the Serverpod runner is running or manually with `serverpod generate`). If models with `table` have changed, the database schema must be updated following the [migration workflow](../serverpod-migrations/SKILL.md). ## Basic class diff --git a/.cursor/skills/serverpod-overview/SKILL.md b/.cursor/skills/serverpod-overview/SKILL.md index 9059df89..a08451bf 100644 --- a/.cursor/skills/serverpod-overview/SKILL.md +++ b/.cursor/skills/serverpod-overview/SKILL.md @@ -23,27 +23,52 @@ Each of these feature areas has its own `serverpod-*` skill; use the one that ma ## Running the server -The user runs the server with `serverpod start`, which watches for file changes to run incremental code generation and hot reload the server (and the Flutter app, when the project has one). +The development stack is run by the Serverpod runner, one background process per server package. It generates code, compiles the server with hot reload, launches the companion Flutter apps and serves the `serverpod` MCP. While it runs it watches for file changes to run incremental code generation and hot reload the server (and the Flutter app, when the project has one). -Do not check whether the server is running before acting: make the changes and call the `serverpod` MCP tools. When nothing is running, the MCP answers with an explicit "the server is not running" error. Only then, stop and ask the user to start it. NEVER start the server yourself. +- `serverpod start` ensures a runner is running, spawning one when none is, and attaches to it: the interactive terminal UI on a terminal, a plain log stream otherwise or with `--no-tui`. It blocks until the user leaves. Leaving a `start` that spawned the runner stops it; leaving one that attached to a runner already running keeps it running. This is what the user runs. `serverpod start --no-attach` behaves as `serverpod runner start`. +- `serverpod runner start` ensures a runner is running the same way, prints its addresses and returns, leaving it running in the background. This is how you start it. A cold start compiles the whole project and can take minutes. Companion Flutter apps declared with `auto_launch` launch once a UI attaches; launch any app before that with the MCP tool `spawn_flutter_app`. +- `serverpod runner attach` attaches a UI to a runner already running, the same way `serverpod start` does, and never spawns one. Leaving it keeps the runner running. +- `serverpod runner status` prints whether the runner is starting, running, stopping or up with a project that failed to build, the API, Insights and web URLs and the log path. +- `serverpod runner stop` stops the runner. -ALWAYS use the MCP server instead of the command line. A running `serverpod start` exposes: +All of them resolve the server package from the current directory, or take `--directory ` (`-d`). + +Do not check whether the server is running before acting: make the changes and call the `serverpod` MCP tools. When nothing is running, the MCP answers with an explicit "the server is not running" error. Then: + +1. Call `tail_runner_logs` to learn whether a runner is still starting or why it stopped. +2. If none is running, run `serverpod runner start` in the server package. It returns zero once the server is up. It returns non-zero when the project does not build (the runner stays up and, with watch on, the default, starts the server once a change makes it build), when a configured port is held by something other than a Serverpod runner, or when Docker refused. Fix the cause instead of retrying. The one exception is a `serverpod runner start` your shell killed on a timeout before it returned: the runner keeps starting in the background, and running the command again waits for it. +3. Call the MCP tool again. The MCP reconnects by itself. + +Leave the runner running when you are done; the user attaches to it with `serverpod start`. Run `serverpod runner stop` only when asked. Starting the stack with `--watch`, `--target`, `--flutter`, `--docker` or server arguments that differ from the running runner's fails and names the difference; stop that runner first. + +ALWAYS use the MCP server instead of the command line for the tasks below. The `serverpod runner` commands are the exception: they manage the stack itself. A running runner exposes: - `create_migration` and `apply_migrations` for the database (after you change data models). - `create_repair_migration` when the database has drifted out of sync with the migrations. It only writes the repair file; follow up with `apply_migrations`. - `tail_server_logs` to read logs from the server. +- `tail_runner_logs` to read the output of the runner itself (code generation, compilation, Docker, raw server output). The only tool that also works while the server is starting or after it stopped. - `tail_flutter_logs` to read raw stdout/stderr from a Flutter app. -- `hot_reload` to reload the server and the Flutter app while keeping in-memory state. Only needed with `--no-watch`, since `serverpod start` reloads on file changes. +- `hot_reload` to reload the server and the Flutter app while keeping in-memory state. Only needed with `--no-watch`, since the runner reloads on file changes. - `hot_restart` to restart the server and the Flutter app, dropping in-memory state. ALWAYS call it after doing changes in the Flutter app that may not work with normal hot reload (which is automatically applied). - `spawn_flutter_app` to start a Flutter app declared under `serverpod: flutter_apps:` in the server `pubspec.yaml`. - `get_flutter_app_dtd` to get the Dart Tooling Daemon URI of a running Flutter app. Pass it to the `dart` MCP to drive the app. Tools that target a Flutter app take an optional `appId`, which is the map key under `serverpod: flutter_apps:`. It is required only when the project declares more than one app. +## Several servers at once + +Each server package has exactly one runner, keyed by the package directory: its manifest, lock, log and sockets live in `/.dart_tool/serverpod/`, and every runner registers itself under `~/.serverpod/runners/`. So a workspace with several server packages, and several checkouts or worktrees of the same project, each run their own runner. Start each with `serverpod runner start --directory `. An MCP bridge serves one server package, so the agent configuration needs one `serverpod mcp-server --server-dir ` entry per server. The bridge serves the package its `--server-dir` names, not the directory you are in, and says which in its instructions and errors. Check that before applying migrations or restarting from another checkout or worktree. + +The packages usually share a `config/development.yaml` with the same ports (8080, 8081, 8082). The first runner binds them. A runner that finds them claimed by another runner moves all of its listeners to ephemeral ports together, and keeps those ports across its own restarts while they stay free. Never assume `localhost:8080`: read the addresses from `serverpod runner status` or from what `serverpod runner start` printed. Flutter apps the runner launches receive the right URL through `--dart-define=SERVER_URL=...`; an app started by hand, a browser tab or `curl` must use the address from `status`. + +A configured port held by anything other than a Serverpod runner, such as a server started by hand with `dart run bin/main.dart`, is an error: stop that process or change the port in `config/development.yaml`. + +The embedded PostgreSQL (`database.dataPath`) is per checkout. It listens on its Unix socket, and on loopback TCP at `database.port` when the run mode has a database password. That port is a fourth listener: it moves aside with the server ports, and `status` prints the database address. A database this checkout already runs, started by `serverpod database start`, is joined rather than moved. Only a project that uses the Docker Compose database shares host ports, and the compose project is named after the server directory, so two checkouts of such a project would share one set of containers. Start a checkout that uses this shared Compose stack only when no other checkout is using it. Otherwise, leave the existing stack running and do not start this checkout's runner unless the user explicitly asks to switch checkouts. + ## Working on the project with no running instance -- NEVER use the CLI unless you have already attempted to use the MCP. -- ONLY if you cannot connect to the MCP server, the code can be generated by calling `serverpod generate`. +- NEVER run `serverpod generate` or `serverpod create-migration` while the MCP is available: the runner generates code on save and the MCP creates migrations. The `serverpod runner` commands manage the stack itself and are always allowed. +- ONLY when the MCP is not available or `serverpod runner start` could not bring the server up, generate the code by calling `serverpod generate`. - NEVER edit the generated code, as it will be overwritten by the next generation. This does not cover the SQL of a created migration, which may be edited — see the [Serverpod Migrations](../serverpod-migrations/SKILL.md) skill. After generating the code, database migrations can be created by calling `serverpod create-migration`. Use ONLY if you cannot use the MCP. diff --git a/.cursor/skills/serverpod-upgrading/SKILL.md b/.cursor/skills/serverpod-upgrading/SKILL.md index 10a3e967..9211cdc7 100644 --- a/.cursor/skills/serverpod-upgrading/SKILL.md +++ b/.cursor/skills/serverpod-upgrading/SKILL.md @@ -7,7 +7,7 @@ description: Upgrade Serverpod — minor/patch updates, major upgrades (2.x to 3 Requirements for this Serverpod version: Dart 3.12.2+, Flutter 3.44.4+. Check the release notes for the version being installed. -Use the same pinned Serverpod version across all packages. Use the CLI to do the upgrade. Ask the user to start the server with `serverpod start` after the upgrade. NEVER update the CLI tooling, instead STOP and ask the user to do it. +Use the same pinned Serverpod version across all packages. Use the CLI to do the upgrade. Restart the runner after the upgrade: `serverpod runner stop`, then `serverpod runner start`. NEVER update the CLI tooling, instead STOP and ask the user to do it. 1. Check the latest version of Serverpod: https://pub.dev/packages/serverpod (unless the user has requested a specific version). 2. Run `serverpod version` to verify that the tooling has the correct version. If not, STOP and ask the user to install the correct version (`dart install serverpod_cli` for latest or `dart install serverpod_cli 3.x.x` for specific version). @@ -17,7 +17,7 @@ Use the same pinned Serverpod version across all packages. Use the CLI to do the 6. Run `serverpod create-migration`. 7. Run `dart analyze` in the root of the project and address any issues. 8. Ensure that the Dockerfile uses at least `FROM dart:3.12.2 AS build`. -9. Inform the user that the upgrade is complete and they should start the server with `serverpod start`. +9. Restart the runner with `serverpod runner stop` and `serverpod runner start` in the server package, and inform the user that the upgrade is complete. ## Major upgrade: Serverpod 3.x to 4.0 diff --git a/.cursor/skills/shad-overview/SKILL.md b/.cursor/skills/shad-overview/SKILL.md new file mode 100644 index 00000000..4c8516dd --- /dev/null +++ b/.cursor/skills/shad-overview/SKILL.md @@ -0,0 +1,155 @@ +--- +name: shad-overview +description: A Flutter UI library that ports shadcn/ui's components, styles, and theming. Provides accessible, fully customizable widgets (Button, Card, Form, Sidebar, and more) plus theming, layout, and typography guides. Use this skill when building a Flutter UI, implementing a design system, or looking up how to use a specific `shad` component. +--- + +# Shad: shadcn/ui for Flutter + +`shad` ports shadcn/ui's components and design system to Flutter. Every widget is a plain, fully customizable Flutter widget, themed by one `ShadThemeData`. + +Import everything through one file: + +```dart +import 'package:shad/shad.dart'; +``` + +The least obvious part of the API: use `ShadPadding` instead of Flutter's `Padding` inside a `shad`-themed page. It has no plain constructor, only `ShadPadding.all`, `.symmetric`, `.only`, and `.directional`, and their arguments are steps on the theme's spacing scale, not logical pixels — `ShadPadding.symmetric(horizontal: 6, vertical: 4)` is shadcn's `px-6 py-4`, not 6px/4px. See [layout.md](guides/layout.md#padding) for a full example. + +## Guides + +- [Installation](guides/installation.md): add the package, set up `ShadApp`, use a component. +- [Theming](guides/theming.md): color schemes, component theme overrides, radius, focus rings, Material/Cupertino interop. +- [Styles](guides/styles.md): the eight shadcn geometry presets (`vega`, `nova`, `maia`, `lyra`, `mira`, `luma`, `sera`, `rhea`). +- [Typography](guides/typography.md): the text role scale and how to change the font. +- [Layout](guides/layout.md): `ShadRow`, `ShadColumn`, `ShadGap`, `ShadPadding`. +- [Form](guides/form.md): `ShadForm` and the `*FormField` widgets. +- [Responsive](guides/responsive.md): the breakpoint scale, `ShadResponsiveBuilder`, `context.breakpoint`. +- [Portal](guides/portal.md): the overlay primitive behind popovers, selects, and menus. + +## Components + +| Name | Description | Reference | +| :--- | :--- | :--- | +| Accordion | A vertically stacked set of interactive headings that each reveal a section of content. | [accordion.md](components/accordion.md) | +| Alert | Displays a callout for user attention. | [alert.md](components/alert.md) | +| Avatar | An image element with a fallback for representing the user. | [avatar.md](components/avatar.md) | +| Badge | Displays a badge or a component that looks like a badge. | [badge.md](components/badge.md) | +| Breadcrumb | Displays the path to the current resource using a hierarchy of links. | [breadcrumb.md](components/breadcrumb.md) | +| Button | Displays a button or a component that looks like a button. Six variants share one API. | [button.md](components/button.md) | +| Calendar | A component that allows users to select dates: single, multiple, or a range. | [calendar.md](components/calendar.md) | +| Card | Displays a card with title, description, content, and footer. | [card.md](components/card.md) | +| Checkbox | A control that allows the user to toggle between checked and not checked. | [checkbox.md](components/checkbox.md) | +| Collapsible | An interactive component that expands and collapses a panel. | [collapsible.md](components/collapsible.md) | +| Command | A composable command menu with filtering and full keyboard navigation. | [command.md](components/command.md) | +| Context Menu | Displays a menu at the pointer, triggered by a right click or a long press. | [context-menu.md](components/context-menu.md) | +| Data Table | A table with sorting, selection, filtering, and pagination, driven by a controller. | [data-table.md](components/data-table.md) | +| Date Picker | A date picker with the calendar in a popover. | [date-picker.md](components/date-picker.md) | +| Dialog | A modal window overlaid on the page, rendering the content underneath inert. | [dialog.md](components/dialog.md) | +| Empty | A placeholder for an empty state: icon, title, description, and actions. | [empty.md](components/empty.md) | +| Icon Button | A square button holding a single icon. | [icon-button.md](components/icon-button.md) | +| Input | Displays a form input field. | [input.md](components/input.md) | +| Input OTP | A one-time password input with copy-paste support. | [input-otp.md](components/input-otp.md) | +| Kbd | Displays a keyboard key or chord. | [kbd.md](components/kbd.md) | +| Keyboard Toolbar | A toolbar shown above the software keyboard on mobile, with focus navigation and a done button. | [keyboard-toolbar.md](components/keyboard-toolbar.md) | +| Menubar | A visually persistent menu, common in desktop applications. | [menubar.md](components/menubar.md) | +| Pagination | Page navigation with previous and next links, plus a compact variant for toolbars and table footers. | [pagination.md](components/pagination.md) | +| Popover | Displays rich content in a portal, triggered by a button. | [popover.md](components/popover.md) | +| Progress | An indicator that shows how much of a task is complete. | [progress.md](components/progress.md) | +| Radio Group | A set of checkable buttons where only one can be checked at a time. | [radio-group.md](components/radio-group.md) | +| Resizable | Resizable panel groups and layouts, with nesting and drag handles. | [resizable.md](components/resizable.md) | +| Select | Displays a list of options for the user to pick from, triggered by a button. | [select.md](components/select.md) | +| Separator | Visually or semantically separates content. | [separator.md](components/separator.md) | +| Sheet | Extends the dialog to display content that complements the main content of the screen, sliding in from any edge. | [sheet.md](components/sheet.md) | +| Sidebar | A composable, collapsible side navigation panel: icon rail, off-canvas, floating, or inset, with a mobile sheet fallback. | [sidebar.md](components/sidebar.md) | +| Skeleton | A placeholder to show while content is loading. | [skeleton.md](components/skeleton.md) | +| Slider | An input where the user selects a value from within a given range. | [slider.md](components/slider.md) | +| Sonner | An opinionated toast stack: notifications collect and expand on hover. | [sonner.md](components/sonner.md) | +| Spinner | An animated loading indicator. | [spinner.md](components/spinner.md) | +| Switch | A control that allows the user to toggle between checked and not checked. | [switch.md](components/switch.md) | +| Table | A responsive table component with header and footer rows. | [table.md](components/table.md) | +| Tabs | A set of layered sections of content displayed one at a time. | [tabs.md](components/tabs.md) | +| Textarea | A multi-line text input, optionally user-resizable. | [textarea.md](components/textarea.md) | +| Time Picker | A field for entering a time of day. | [time-picker.md](components/time-picker.md) | +| Toast | A succinct message that is displayed temporarily. | [toast.md](components/toast.md) | +| Toggle | A two-state button that can be either on or off. | [toggle.md](components/toggle.md) | +| Toggle Group | A set of two-state buttons that can be toggled on or off. | [toggle-group.md](components/toggle-group.md) | +| Tooltip | A popup that displays information related to an element on hover or focus. | [tooltip.md](components/tooltip.md) | + +Each component page shows the actual example source bundled with the example app, so it cannot drift from the code that runs. + +## Basic setup + +A complete counter app with light and dark theme support: + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return ShadApp( + theme: ShadThemeData( + brightness: Brightness.light, + colorScheme: const ShadZincColorScheme.light(), + ), + darkTheme: ShadThemeData( + brightness: Brightness.dark, + colorScheme: const ShadZincColorScheme.dark(), + ), + themeMode: ThemeMode.system, + home: const CounterPage(), + ); + } +} + +class CounterPage extends StatefulWidget { + const CounterPage({super.key}); + + @override + State createState() => _CounterPageState(); +} + +class _CounterPageState extends State { + int _counter = 0; + + @override + Widget build(BuildContext context) { + final theme = ShadTheme.of(context); + return Scaffold( + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'You have pushed the button this many times:', + style: theme.textTheme.muted, + ), + Text('$_counter', style: theme.textTheme.h1), + const SizedBox(height: 16), + ShadButton( + onPressed: () => setState(() => _counter++), + child: const Icon(LucideIcons.plus), + ), + ], + ), + ), + ); + } +} +``` + +## Packages re-exported by shad + +`shad`'s public API (`package:shad/shad.dart`) re-exports the parts of a few libraries that its own component signatures depend on, so callers do not need a second dependency just to pass an argument `shad` asks for. + +- [flutter_animate](https://pub.dev/packages/flutter_animate): the `effects:` parameter on `ShadPopover`, `ShadSelect`, `ShadAccordion`, `ShadMenubar`, and similar components takes a `List` (aliased as `AnimateEffect`). +- [lucide_icons_flutter](https://pub.dev/packages/lucide_icons_flutter): the icon set used throughout the library and its examples, exposed as `LucideIcons.*`. Browse the icons [here](https://lucide.dev/icons/). +- [two_dimensional_scrollables](https://pub.dev/packages/two_dimensional_scrollables): `ShadTable` builds on `TableView`, so `ShadTableCell` extends its `TableViewCell`. +- [intl](https://pub.dev/packages/intl): `DateFormat` and `NumberFormat` are used by the calendar, date picker, and pagination widgets. diff --git a/.cursor/skills/shad-overview/components/accordion.md b/.cursor/skills/shad-overview/components/accordion.md new file mode 100644 index 00000000..69d835ca --- /dev/null +++ b/.cursor/skills/shad-overview/components/accordion.md @@ -0,0 +1,91 @@ +# Accordion + +A vertically stacked set of interactive headings that each reveal a section of content. + +## Single + +Opening a section closes the previous one. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +const _items = [ + ( + title: 'Is it accessible?', + content: 'Yes. It adheres to the WAI-ARIA design pattern.', + ), + ( + title: 'Is it styled?', + content: + 'Yes. It comes with default styles that match the other ' + "components' aesthetic.", + ), + ( + title: 'Is it animated?', + content: + "Yes. It's animated by default, but you can disable it if " + 'you prefer.', + ), +]; + +class AccordionSingleExample extends StatelessWidget { + const AccordionSingleExample({super.key}); + + @override + Widget build(BuildContext context) { + return ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 600), + child: ShadAccordion<({String content, String title})>( + children: [ + for (final item in _items) + ShadAccordionItem( + value: item, + title: Text(item.title), + child: Text(item.content), + ), + ], + ), + ); + } +} +``` + +## Multiple + +Sections open and close independently. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +const _items = [ + (title: 'First section', content: 'Multiple sections can be open at once.'), + ( + title: 'Second section', + content: 'Opening this one does not close the first.', + ), +]; + +class AccordionMultipleExample extends StatelessWidget { + const AccordionMultipleExample({super.key}); + + @override + Widget build(BuildContext context) { + return ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 600), + child: ShadAccordion<({String content, String title})>.multiple( + children: [ + for (final item in _items) + ShadAccordionItem( + value: item, + title: Text(item.title), + child: Text(item.content), + ), + ], + ), + ); + } +} +``` + diff --git a/.cursor/skills/shad-overview/components/alert.md b/.cursor/skills/shad-overview/components/alert.md new file mode 100644 index 00000000..0593e303 --- /dev/null +++ b/.cursor/skills/shad-overview/components/alert.md @@ -0,0 +1,78 @@ +# Alert + +Displays a callout for user attention. + +## Default + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class AlertDefaultExample extends StatelessWidget { + const AlertDefaultExample({super.key}); + + @override + Widget build(BuildContext context) { + return ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 576), + child: const Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + spacing: 16, + children: [ + ShadAlert( + icon: Icon(LucideIcons.circleCheck), + title: Text('Success! Your changes have been saved'), + description: Text( + 'This is an alert with icon, title and description.', + ), + ), + ShadAlert( + icon: Icon(LucideIcons.popcorn), + title: Text('This Alert has a title and an icon. No description.'), + ), + ShadAlert.destructive( + icon: Icon(LucideIcons.circleAlert), + title: Text('Unable to process your payment.'), + description: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 4, + children: [ + Text('Please verify your billing information and try again.'), + Text('• Check your card details'), + Text('• Ensure sufficient funds'), + Text('• Verify billing address'), + ], + ), + ), + ], + ), + ); + } +} +``` + +## Destructive + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class AlertDestructiveExample extends StatelessWidget { + const AlertDestructiveExample({super.key}); + + @override + Widget build(BuildContext context) { + return ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 576), + child: const ShadAlert.destructive( + icon: Icon(LucideIcons.circleAlert), + title: Text('Error'), + description: Text('Your session has expired. Please log in again.'), + ), + ); + } +} +``` + diff --git a/.cursor/skills/shad-overview/components/avatar.md b/.cursor/skills/shad-overview/components/avatar.md new file mode 100644 index 00000000..41b22137 --- /dev/null +++ b/.cursor/skills/shad-overview/components/avatar.md @@ -0,0 +1,33 @@ +# Avatar + +An image element with a fallback for representing the user. + +## Default + +The placeholder shows while the image loads, or when it fails. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class AvatarDefaultExample extends StatelessWidget { + const AvatarDefaultExample({super.key}); + + @override + Widget build(BuildContext context) { + return const Row( + mainAxisSize: MainAxisSize.min, + spacing: 8, + children: [ + ShadAvatar( + 'https://avatars.githubusercontent.com/u/124599?v=4', + placeholder: Text('CN'), + ), + // The placeholder shows while loading, or if the image fails. + ShadAvatar('', placeholder: Text('AB')), + ], + ); + } +} +``` + diff --git a/.cursor/skills/shad-overview/components/badge.md b/.cursor/skills/shad-overview/components/badge.md new file mode 100644 index 00000000..5be6961f --- /dev/null +++ b/.cursor/skills/shad-overview/components/badge.md @@ -0,0 +1,29 @@ +# Badge + +Displays a badge or a component that looks like a badge. + +## Variants + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class BadgeVariantsExample extends StatelessWidget { + const BadgeVariantsExample({super.key}); + + @override + Widget build(BuildContext context) { + return const Wrap( + spacing: 8, + runSpacing: 8, + children: [ + ShadBadge(child: Text('Primary')), + ShadBadge.secondary(child: Text('Secondary')), + ShadBadge.destructive(child: Text('Destructive')), + ShadBadge.outline(child: Text('Outline')), + ], + ); + } +} +``` + diff --git a/.cursor/skills/shad-overview/components/breadcrumb.md b/.cursor/skills/shad-overview/components/breadcrumb.md new file mode 100644 index 00000000..5bbd7ead --- /dev/null +++ b/.cursor/skills/shad-overview/components/breadcrumb.md @@ -0,0 +1,64 @@ +# Breadcrumb + +Displays the path to the current resource using a hierarchy of links. + +## Default + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class BreadcrumbBasicExample extends StatelessWidget { + const BreadcrumbBasicExample({super.key}); + + @override + Widget build(BuildContext context) { + return ShadBreadcrumb( + children: [ + ShadBreadcrumbLink( + onPressed: () {}, + child: const Text('Home'), + ), + ShadBreadcrumbLink( + onPressed: () {}, + child: const Text('Components'), + ), + const Text('Breadcrumb'), + ], + ); + } +} +``` + +## Collapsed + +An ellipsis stands in for the middle of a deep hierarchy. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class BreadcrumbEllipsisExample extends StatelessWidget { + const BreadcrumbEllipsisExample({super.key}); + + @override + Widget build(BuildContext context) { + return ShadBreadcrumb( + children: [ + ShadBreadcrumbLink( + onPressed: () {}, + child: const Text('Home'), + ), + // Collapses the middle of a deep hierarchy. + const ShadBreadcrumbEllipsis(), + ShadBreadcrumbLink( + onPressed: () {}, + child: const Text('Components'), + ), + const Text('Breadcrumb'), + ], + ); + } +} +``` + diff --git a/.cursor/skills/shad-overview/components/button.md b/.cursor/skills/shad-overview/components/button.md new file mode 100644 index 00000000..1ab1feb6 --- /dev/null +++ b/.cursor/skills/shad-overview/components/button.md @@ -0,0 +1,145 @@ +# Button + +Displays a button or a component that looks like a button. Six variants share one API: pick the one matching the action's weight. + +## Variants + +Primary for the main action, secondary and outline for everything else, destructive for irreversible actions, ghost for toolbars, and link for inline navigation. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class ButtonVariantsExample extends StatelessWidget { + const ButtonVariantsExample({super.key}); + + @override + Widget build(BuildContext context) { + return Wrap( + spacing: 8, + runSpacing: 8, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + ShadButton( + onPressed: () {}, + child: const Text('Primary'), + ), + ShadButton.secondary( + onPressed: () {}, + child: const Text('Secondary'), + ), + ShadButton.destructive( + onPressed: () {}, + child: const Text('Destructive'), + ), + ShadButton.outline( + onPressed: () {}, + child: const Text('Outline'), + ), + ShadButton.ghost( + onPressed: () {}, + child: const Text('Ghost'), + ), + ShadButton.link( + onPressed: () {}, + child: const Text('Link'), + ), + ], + ); + } +} +``` + +## Sizes + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class ButtonSizesExample extends StatelessWidget { + const ButtonSizesExample({super.key}); + + @override + Widget build(BuildContext context) { + return Wrap( + spacing: 8, + runSpacing: 8, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + ShadButton.outline( + size: ShadButtonSize.sm, + onPressed: () {}, + child: const Text('Small'), + ), + ShadButton.outline( + onPressed: () {}, + child: const Text('Regular'), + ), + ShadButton.outline( + size: ShadButtonSize.lg, + onPressed: () {}, + child: const Text('Large'), + ), + ], + ); + } +} +``` + +## With icon + +Use leading and trailing to place icons around the label. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class ButtonIconExample extends StatelessWidget { + const ButtonIconExample({super.key}); + + @override + Widget build(BuildContext context) { + return Wrap( + spacing: 8, + runSpacing: 8, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + ShadButton( + onPressed: () {}, + leading: const Icon(LucideIcons.mail), + child: const Text('Login with Email'), + ), + ShadButton.outline( + onPressed: () {}, + trailing: const Icon(LucideIcons.chevronRight), + child: const Text('Next'), + ), + ], + ); + } +} +``` + +## Loading + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class ButtonLoadingExample extends StatelessWidget { + const ButtonLoadingExample({super.key}); + + @override + Widget build(BuildContext context) { + return ShadButton( + enabled: false, + leading: const SizedBox.square( + dimension: 16, + child: ShadSpinner(), + ), + child: const Text('Please wait'), + ); + } +} +``` + diff --git a/.cursor/skills/shad-overview/components/calendar.md b/.cursor/skills/shad-overview/components/calendar.md new file mode 100644 index 00000000..61d64fae --- /dev/null +++ b/.cursor/skills/shad-overview/components/calendar.md @@ -0,0 +1,51 @@ +# Calendar + +A component that allows users to select dates: single, multiple, or a range. + +## Single + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class CalendarSingleExample extends StatefulWidget { + const CalendarSingleExample({super.key}); + + @override + State createState() => _CalendarSingleExampleState(); +} + +class _CalendarSingleExampleState extends State { + DateTime? selected = DateTime.now(); + + @override + Widget build(BuildContext context) { + return ShadCalendar( + selected: selected, + onChanged: (date) => setState(() => selected = date), + ); + } +} +``` + +## Range + +Constrain the selection with min and max lengths. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class CalendarRangeExample extends StatelessWidget { + const CalendarRangeExample({super.key}); + + @override + Widget build(BuildContext context) { + return ShadCalendar.range( + min: 2, + max: 7, + ); + } +} +``` + diff --git a/.cursor/skills/shad-overview/components/card.md b/.cursor/skills/shad-overview/components/card.md new file mode 100644 index 00000000..7177c0f7 --- /dev/null +++ b/.cursor/skills/shad-overview/components/card.md @@ -0,0 +1,66 @@ +# Card + +Displays a card with title, description, content, and footer. + +## With a form + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +const _frameworks = { + 'next': 'Next.js', + 'react': 'React', + 'astro': 'Astro', + 'nuxt': 'Nuxt.js', +}; + +class CardProjectExample extends StatelessWidget { + const CardProjectExample({super.key}); + + @override + Widget build(BuildContext context) { + return ShadCard( + width: 350, + title: const Text('Create project'), + description: const Text('Deploy your new project in one-click.'), + footer: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + ShadButton.outline( + onPressed: () {}, + child: const Text('Cancel'), + ), + ShadButton( + onPressed: () {}, + child: const Text('Deploy'), + ), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const Text('Name'), + const SizedBox(height: 6), + const ShadInput(placeholder: Text('Name of your project')), + const SizedBox(height: 16), + const Text('Framework'), + const SizedBox(height: 6), + ShadSelect( + placeholder: const Text('Select'), + options: [ + for (final entry in _frameworks.entries) + ShadOption(value: entry.key, child: Text(entry.value)), + ], + selectedOptionBuilder: (context, value) => + Text(_frameworks[value]!), + onChanged: (value) {}, + ), + ], + ), + ); + } +} +``` + diff --git a/.cursor/skills/shad-overview/components/checkbox.md b/.cursor/skills/shad-overview/components/checkbox.md new file mode 100644 index 00000000..c00151a0 --- /dev/null +++ b/.cursor/skills/shad-overview/components/checkbox.md @@ -0,0 +1,55 @@ +# Checkbox + +A control that allows the user to toggle between checked and not checked. + +## With label + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class CheckboxBasicExample extends StatefulWidget { + const CheckboxBasicExample({super.key}); + + @override + State createState() => _CheckboxBasicExampleState(); +} + +class _CheckboxBasicExampleState extends State { + bool value = false; + + @override + Widget build(BuildContext context) { + return ShadCheckbox( + value: value, + onChanged: (v) => setState(() => value = v), + label: const Text('Accept terms and conditions'), + sublabel: const Text( + 'You agree to our Terms of Service and Privacy Policy.', + ), + ); + } +} +``` + +## Disabled + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class CheckboxDisabledExample extends StatelessWidget { + const CheckboxDisabledExample({super.key}); + + @override + Widget build(BuildContext context) { + return ShadCheckbox( + value: true, + enabled: false, + onChanged: (v) {}, + label: const Text('Accept terms and conditions'), + ); + } +} +``` + diff --git a/.cursor/skills/shad-overview/components/collapsible.md b/.cursor/skills/shad-overview/components/collapsible.md new file mode 100644 index 00000000..8bab379f --- /dev/null +++ b/.cursor/skills/shad-overview/components/collapsible.md @@ -0,0 +1,57 @@ +# Collapsible + +An interactive component that expands and collapses a panel. + +## Default + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class CollapsibleBasicExample extends StatelessWidget { + const CollapsibleBasicExample({super.key}); + + @override + Widget build(BuildContext context) { + final theme = ShadTheme.of(context); + return SizedBox( + width: 350, + child: ShadCollapsible( + trigger: (context, open, toggle) => Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Text( + '@peduarte starred 3 repositories', + style: theme.textTheme.small, + overflow: TextOverflow.ellipsis, + ), + ), + ShadIconButton.ghost( + icon: Icon( + open ? LucideIcons.chevronsDownUp : LucideIcons.chevronsUpDown, + ), + semanticLabel: open ? 'Collapse' : 'Expand', + onPressed: toggle, + ), + ], + ), + child: const Padding( + padding: EdgeInsets.only(top: 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + ShadCard(child: Text('@radix-ui/primitives')), + SizedBox(height: 8), + ShadCard(child: Text('@radix-ui/colors')), + SizedBox(height: 8), + ShadCard(child: Text('@stitches/react')), + ], + ), + ), + ), + ); + } +} +``` + diff --git a/.cursor/skills/shad-overview/components/command.md b/.cursor/skills/shad-overview/components/command.md new file mode 100644 index 00000000..72838d8f --- /dev/null +++ b/.cursor/skills/shad-overview/components/command.md @@ -0,0 +1,113 @@ +# Command + +A composable command menu with filtering and full keyboard navigation. + +## Inline + +Arrow keys move the highlight, Enter selects. Keywords let items match terms their label does not contain. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class CommandInlineExample extends StatelessWidget { + const CommandInlineExample({super.key}); + + @override + Widget build(BuildContext context) { + return ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 450), + child: ShadCommand( + autofocus: false, + decoration: const ShadDecoration(shadows: Shadows.md), + placeholder: const Text('Type a command or search…'), + groups: [ + ShadCommandGroup( + heading: 'Suggestions', + items: [ + ShadCommandItem( + label: 'Calendar', + value: 'calendar', + leading: const Icon(LucideIcons.calendar), + onSelected: () {}, + ), + ShadCommandItem( + label: 'Search Emoji', + value: 'emoji', + keywords: const ['smiley', 'face'], + leading: const Icon(LucideIcons.smile), + onSelected: () {}, + ), + ], + ), + ShadCommandGroup( + heading: 'Settings', + items: [ + ShadCommandItem( + label: 'Profile', + value: 'profile', + leading: const Icon(LucideIcons.user), + trailing: const ShadKbd.group(['⌘', 'P']), + onSelected: () {}, + ), + ShadCommandItem( + label: 'Billing', + value: 'billing', + leading: const Icon(LucideIcons.creditCard), + trailing: const ShadKbd.group(['⌘', 'B']), + onSelected: () {}, + ), + ], + ), + ], + ), + ); + } +} +``` + +## Dialog + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class CommandDialogExample extends StatelessWidget { + const CommandDialogExample({super.key}); + + @override + Widget build(BuildContext context) { + return ShadButton.outline( + trailing: const ShadKbd.group(['⌘', 'K']), + child: const Text('Open command palette'), + onPressed: () { + showShadCommandDialog( + context: context, + placeholder: const Text('Type a command or search…'), + groups: [ + ShadCommandGroup( + heading: 'Suggestions', + items: [ + ShadCommandItem( + label: 'Calendar', + value: 'calendar', + leading: const Icon(LucideIcons.calendar), + onSelected: () {}, + ), + ShadCommandItem( + label: 'Sign out', + value: 'signout', + keywords: const ['logout', 'exit'], + leading: const Icon(LucideIcons.logOut), + onSelected: () {}, + ), + ], + ), + ], + ); + }, + ); + } +} +``` + diff --git a/.cursor/skills/shad-overview/components/context-menu.md b/.cursor/skills/shad-overview/components/context-menu.md new file mode 100644 index 00000000..a70b248f --- /dev/null +++ b/.cursor/skills/shad-overview/components/context-menu.md @@ -0,0 +1,57 @@ +# Context Menu + +Displays a menu at the pointer, triggered by a right click or a long press. + +## Default + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class ContextMenuBasicExample extends StatelessWidget { + const ContextMenuBasicExample({super.key}); + + @override + Widget build(BuildContext context) { + final theme = ShadTheme.of(context); + const divider = ShadSeparator.horizontal( + margin: EdgeInsets.symmetric(vertical: 4), + ); + return ShadContextMenuRegion( + constraints: const BoxConstraints(minWidth: 300), + items: const [ + ShadContextMenuItem.inset(child: Text('Back')), + ShadContextMenuItem.inset(enabled: false, child: Text('Forward')), + ShadContextMenuItem.inset(child: Text('Reload')), + ShadContextMenuItem.inset( + trailing: Icon(LucideIcons.chevronRight), + items: [ + ShadContextMenuItem(child: Text('Save Page As...')), + ShadContextMenuItem(child: Text('Create Shortcut...')), + divider, + ShadContextMenuItem(child: Text('Developer Tools')), + ], + child: Text('More Tools'), + ), + divider, + ShadContextMenuItem( + leading: Icon(LucideIcons.check), + child: Text('Show Bookmarks Bar'), + ), + ShadContextMenuItem.inset(child: Text('Show Full URLs')), + ], + child: Container( + width: 300, + height: 160, + alignment: Alignment.center, + decoration: BoxDecoration( + border: Border.all(color: theme.colorScheme.border), + borderRadius: theme.radii.md, + ), + child: const Text('Right click here'), + ), + ); + } +} +``` + diff --git a/.cursor/skills/shad-overview/components/data-table.md b/.cursor/skills/shad-overview/components/data-table.md new file mode 100644 index 00000000..56a562d8 --- /dev/null +++ b/.cursor/skills/shad-overview/components/data-table.md @@ -0,0 +1,149 @@ +# Data Table + +A table with sorting, selection, filtering, and pagination, driven by a controller. + +## Payments + +Tap a column header to sort. A third tap clears the sort. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class Payment { + const Payment(this.id, this.status, this.email, this.amount); + + final String id; + final String status; + final String email; + final double amount; +} + +const _payments = [ + Payment('m5gr84i9', 'success', 'ken99@example.com', 316), + Payment('3u1reuv4', 'success', 'abe45@example.com', 242), + Payment('derv1ws0', 'processing', 'monserrat44@example.com', 837), + Payment('5kma53ae', 'success', 'silas22@example.com', 874), + Payment('bhqecj4p', 'failed', 'carmella@example.com', 721), + Payment('a1b2c3d4', 'processing', 'jason@example.com', 128), + Payment('e5f6g7h8', 'failed', 'nina@example.com', 455), +]; + +class DataTablePaymentsExample extends StatefulWidget { + const DataTablePaymentsExample({super.key}); + + @override + State createState() => + _DataTablePaymentsExampleState(); +} + +class _DataTablePaymentsExampleState extends State { + final controller = ShadDataTableController( + rows: _payments, + pageSize: 5, + ); + + @override + void initState() { + super.initState(); + controller.addListener(_onControllerChanged); + } + + @override + void dispose() { + controller.removeListener(_onControllerChanged); + controller.dispose(); + super.dispose(); + } + + void _onControllerChanged() { + if (mounted) setState(() {}); + } + + @override + Widget build(BuildContext context) { + final theme = ShadTheme.of(context); + final total = controller.filteredRows.length; + final rowHeight = theme.tableTheme.cellHeight ?? 48; + final visibleRowCount = controller.visibleRows.length; + // TableView does not shrink-wrap, so height tracks the pinned header + // plus the rows on the current page. + final tableHeight = visibleRowCount == 0 + ? rowHeight * 3 + : rowHeight * (visibleRowCount + 1); + + return ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 700), + child: SizedBox( + width: double.infinity, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, + children: [ + ShadCard( + padding: EdgeInsets.zero, + clipBehavior: Clip.antiAlias, + child: ShadDataTable( + controller: controller, + selectable: true, + keyOf: (payment) => payment.id, + height: tableHeight, + showPagination: false, + showSelectionCount: false, + columns: [ + ShadDataTableColumn( + id: 'status', + header: 'Status', + extent: const FixedTableSpanExtent(140), + compare: (a, b) => a.status.compareTo(b.status), + cellBuilder: (context, payment) => ShadBadge.raw( + variant: switch (payment.status) { + 'success' => ShadBadgeVariant.primary, + 'failed' => ShadBadgeVariant.destructive, + _ => ShadBadgeVariant.secondary, + }, + child: Text(payment.status), + ), + ), + ShadDataTableColumn( + id: 'email', + header: 'Email', + compare: (a, b) => a.email.compareTo(b.email), + cellBuilder: (context, payment) => + Text(payment.email, overflow: TextOverflow.ellipsis), + ), + ShadDataTableColumn( + id: 'amount', + header: 'Amount', + extent: const FixedTableSpanExtent(120), + alignment: Alignment.centerRight, + compare: (a, b) => a.amount.compareTo(b.amount), + cellBuilder: (context, payment) => + Text('\$${payment.amount.toStringAsFixed(2)}'), + ), + ], + ), + ), + const SizedBox(height: 12), + Row( + children: [ + Text( + '${controller.selectedKeys.length} of $total row(s) selected.', + style: theme.textTheme.muted, + ), + const Spacer(), + ShadPaginationCompact( + page: controller.page, + pageCount: controller.pageCount, + onPageChanged: (page) => controller.page = page, + ), + ], + ), + ], + ), + ), + ); + } +} +``` + diff --git a/.cursor/skills/shad-overview/components/date-picker.md b/.cursor/skills/shad-overview/components/date-picker.md new file mode 100644 index 00000000..ecf00268 --- /dev/null +++ b/.cursor/skills/shad-overview/components/date-picker.md @@ -0,0 +1,36 @@ +# Date Picker + +A date picker with the calendar in a popover. + +## Single + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class DatePickerSingleExample extends StatelessWidget { + const DatePickerSingleExample({super.key}); + + @override + Widget build(BuildContext context) { + return const ShadDatePicker(); + } +} +``` + +## Range + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class DatePickerRangeExample extends StatelessWidget { + const DatePickerRangeExample({super.key}); + + @override + Widget build(BuildContext context) { + return const ShadDatePicker.range(); + } +} +``` + diff --git a/.cursor/skills/shad-overview/components/dialog.md b/.cursor/skills/shad-overview/components/dialog.md new file mode 100644 index 00000000..1b3e246f --- /dev/null +++ b/.cursor/skills/shad-overview/components/dialog.md @@ -0,0 +1,114 @@ +# Dialog + +A modal window overlaid on the page, rendering the content underneath inert. + +## With a form + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +const _profile = [ + (title: 'Name', value: 'Alexandru'), + (title: 'Username', value: 'nank1ro'), +]; + +class DialogFormExample extends StatelessWidget { + const DialogFormExample({super.key}); + + @override + Widget build(BuildContext context) { + final theme = ShadTheme.of(context); + return ShadButton.outline( + child: const Text('Edit Profile'), + onPressed: () { + showShadDialog( + context: context, + builder: (context) => ShadDialog( + title: const Text('Edit Profile'), + description: const Text( + "Make changes to your profile here. Click save when you're " + 'done.', + ), + actions: const [ShadButton(child: Text('Save changes'))], + crossAxisAlignment: CrossAxisAlignment.stretch, + child: Container( + width: 375, + padding: const EdgeInsets.symmetric(vertical: 20), + child: Column( + mainAxisSize: MainAxisSize.min, + spacing: 16, + children: [ + for (final field in _profile) + Row( + children: [ + Expanded( + child: Text( + field.title, + textAlign: TextAlign.end, + style: theme.textTheme.small, + ), + ), + const SizedBox(width: 16), + Expanded( + flex: 3, + child: ShadInput(initialValue: field.value), + ), + ], + ), + ], + ), + ), + ), + ); + }, + ); + } +} +``` + +## Alert dialog + +Interrupts the user with an important question and explicit actions. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class DialogAlertExample extends StatelessWidget { + const DialogAlertExample({super.key}); + + @override + Widget build(BuildContext context) { + return ShadButton.outline( + child: const Text('Delete account'), + onPressed: () { + showShadDialog( + context: context, + builder: (context) => ShadDialog.alert( + title: const Text('Are you absolutely sure?'), + description: const Padding( + padding: EdgeInsets.only(bottom: 8), + child: Text( + 'This action cannot be undone. This will permanently delete ' + 'your account and remove your data from our servers.', + ), + ), + actions: [ + ShadButton.outline( + child: const Text('Cancel'), + onPressed: () => Navigator.of(context).pop(false), + ), + ShadButton( + child: const Text('Continue'), + onPressed: () => Navigator.of(context).pop(true), + ), + ], + ), + ); + }, + ); + } +} +``` + diff --git a/.cursor/skills/shad-overview/components/empty.md b/.cursor/skills/shad-overview/components/empty.md new file mode 100644 index 00000000..42ea7df2 --- /dev/null +++ b/.cursor/skills/shad-overview/components/empty.md @@ -0,0 +1,39 @@ +# Empty + +A placeholder for an empty state: icon, title, description, and actions. + +## Default + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class EmptyBasicExample extends StatelessWidget { + const EmptyBasicExample({super.key}); + + @override + Widget build(BuildContext context) { + return ShadCard( + width: 400, + height: 320, + columnMainAxisSize: MainAxisSize.max, + child: ShadEmpty( + icon: const Icon(LucideIcons.inbox), + title: const Text('No messages'), + description: const Text('Messages you receive will show up here.'), + actions: [ + ShadButton.outline( + onPressed: () {}, + child: const Text('Refresh'), + ), + ShadButton( + onPressed: () {}, + child: const Text('Compose'), + ), + ], + ), + ); + } +} +``` + diff --git a/.cursor/skills/shad-overview/components/icon-button.md b/.cursor/skills/shad-overview/components/icon-button.md new file mode 100644 index 00000000..bb53ba73 --- /dev/null +++ b/.cursor/skills/shad-overview/components/icon-button.md @@ -0,0 +1,45 @@ +# Icon Button + +A square button holding a single icon. + +## Variants + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class IconButtonVariantsExample extends StatelessWidget { + const IconButtonVariantsExample({super.key}); + + @override + Widget build(BuildContext context) { + return Wrap( + spacing: 8, + runSpacing: 8, + children: [ + ShadIconButton( + onPressed: () {}, + icon: const Icon(LucideIcons.rocket), + ), + ShadIconButton.secondary( + onPressed: () {}, + icon: const Icon(LucideIcons.rocket), + ), + ShadIconButton.destructive( + onPressed: () {}, + icon: const Icon(LucideIcons.rocket), + ), + ShadIconButton.outline( + onPressed: () {}, + icon: const Icon(LucideIcons.rocket), + ), + ShadIconButton.ghost( + onPressed: () {}, + icon: const Icon(LucideIcons.rocket), + ), + ], + ); + } +} +``` + diff --git a/.cursor/skills/shad-overview/components/input-otp.md b/.cursor/skills/shad-overview/components/input-otp.md new file mode 100644 index 00000000..7cc9ea40 --- /dev/null +++ b/.cursor/skills/shad-overview/components/input-otp.md @@ -0,0 +1,40 @@ +# Input OTP + +A one-time password input with copy-paste support. + +## Default + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class InputOtpBasicExample extends StatelessWidget { + const InputOtpBasicExample({super.key}); + + @override + Widget build(BuildContext context) { + return ShadInputOTP( + maxLength: 6, + onChanged: (value) {}, + children: const [ + ShadInputOTPGroup( + children: [ + ShadInputOTPSlot(), + ShadInputOTPSlot(), + ShadInputOTPSlot(), + ], + ), + Icon(LucideIcons.dot), + ShadInputOTPGroup( + children: [ + ShadInputOTPSlot(), + ShadInputOTPSlot(), + ShadInputOTPSlot(), + ], + ), + ], + ); + } +} +``` + diff --git a/.cursor/skills/shad-overview/components/input.md b/.cursor/skills/shad-overview/components/input.md new file mode 100644 index 00000000..b3676688 --- /dev/null +++ b/.cursor/skills/shad-overview/components/input.md @@ -0,0 +1,96 @@ +# Input + +Displays a form input field. + +## Default + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class InputBasicExample extends StatelessWidget { + const InputBasicExample({super.key}); + + @override + Widget build(BuildContext context) { + return ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 320), + child: const ShadInput( + placeholder: Text('Email'), + keyboardType: TextInputType.emailAddress, + ), + ); + } +} +``` + +## Password + +Leading and trailing slots hold icons and actions. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class InputPasswordExample extends StatefulWidget { + const InputPasswordExample({super.key}); + + @override + State createState() => _InputPasswordExampleState(); +} + +class _InputPasswordExampleState extends State { + bool obscure = true; + + @override + Widget build(BuildContext context) { + final theme = ShadTheme.of(context); + + return ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 320), + child: ShadInput( + placeholder: const Text('Password'), + obscureText: obscure, + leading: Icon( + LucideIcons.lock, + size: 16, + color: theme.colorScheme.mutedForeground, + ), + trailing: ShadGestureDetector( + cursor: SystemMouseCursors.click, + behavior: HitTestBehavior.opaque, + onTap: () => setState(() => obscure = !obscure), + child: Icon( + obscure ? LucideIcons.eyeOff : LucideIcons.eye, + size: 16, + color: theme.colorScheme.mutedForeground, + ), + ), + ), + ); + } +} +``` + +## Disabled + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class InputDisabledExample extends StatelessWidget { + const InputDisabledExample({super.key}); + + @override + Widget build(BuildContext context) { + return ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 320), + child: const ShadInput( + enabled: false, + placeholder: Text('Email'), + ), + ); + } +} +``` + diff --git a/.cursor/skills/shad-overview/components/kbd.md b/.cursor/skills/shad-overview/components/kbd.md new file mode 100644 index 00000000..b20da38c --- /dev/null +++ b/.cursor/skills/shad-overview/components/kbd.md @@ -0,0 +1,35 @@ +# Kbd + +Displays a keyboard key or chord. + +## Default + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class KbdBasicExample extends StatelessWidget { + const KbdBasicExample({super.key}); + + @override + Widget build(BuildContext context) { + return const Column( + mainAxisSize: MainAxisSize.min, + spacing: 16, + children: [ + ShadKbd('K'), + ShadKbd.group(['⌘', 'K']), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text('Press '), + ShadKbd.group(['Ctrl', 'Shift', 'P']), + Text(' to open the palette'), + ], + ), + ], + ); + } +} +``` + diff --git a/.cursor/skills/shad-overview/components/keyboard-toolbar.md b/.cursor/skills/shad-overview/components/keyboard-toolbar.md new file mode 100644 index 00000000..6990bfda --- /dev/null +++ b/.cursor/skills/shad-overview/components/keyboard-toolbar.md @@ -0,0 +1,29 @@ +# Keyboard Toolbar + +A toolbar shown above the software keyboard on mobile, with focus navigation and a done button. + +## Default + +Run on a mobile device to see the toolbar. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class KeyboardToolbarBasicExample extends StatelessWidget { + const KeyboardToolbarBasicExample({super.key}); + + @override + Widget build(BuildContext context) { + return ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 400), + child: ShadInput( + placeholder: const Text('Focus on mobile to show the toolbar'), + keyboardType: TextInputType.emailAddress, + keyboardToolbarBuilder: (context) => const ShadDefaultKeyboardToolbar(), + ), + ); + } +} +``` + diff --git a/.cursor/skills/shad-overview/components/menubar.md b/.cursor/skills/shad-overview/components/menubar.md new file mode 100644 index 00000000..6eca8db2 --- /dev/null +++ b/.cursor/skills/shad-overview/components/menubar.md @@ -0,0 +1,70 @@ +# Menubar + +A visually persistent menu, common in desktop applications. + +## Default + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class MenubarBasicExample extends StatelessWidget { + const MenubarBasicExample({super.key}); + + @override + Widget build(BuildContext context) { + const divider = ShadSeparator.horizontal( + margin: EdgeInsets.symmetric(vertical: 4), + ); + return ShadMenubar( + items: const [ + ShadMenubarItem( + items: [ + ShadContextMenuItem(child: Text('New Tab')), + ShadContextMenuItem(child: Text('New Window')), + ShadContextMenuItem( + enabled: false, + child: Text('New Incognito Window'), + ), + divider, + ShadContextMenuItem( + trailing: Icon(LucideIcons.chevronRight), + items: [ + ShadContextMenuItem(child: Text('Email Link')), + ShadContextMenuItem(child: Text('Messages')), + ], + child: Text('Share'), + ), + divider, + ShadContextMenuItem(child: Text('Print...')), + ], + child: Text('File'), + ), + ShadMenubarItem( + items: [ + ShadContextMenuItem(child: Text('Undo')), + ShadContextMenuItem(child: Text('Redo')), + divider, + ShadContextMenuItem(child: Text('Cut')), + ShadContextMenuItem(child: Text('Copy')), + ShadContextMenuItem(child: Text('Paste')), + ], + child: Text('Edit'), + ), + ShadMenubarItem( + items: [ + ShadContextMenuItem( + leading: Icon(LucideIcons.check), + child: Text('Always Show Full URLs'), + ), + ShadContextMenuItem.inset(child: Text('Reload')), + ShadContextMenuItem.inset(child: Text('Toggle Full Screen')), + ], + child: Text('View'), + ), + ], + ); + } +} +``` + diff --git a/.cursor/skills/shad-overview/components/pagination.md b/.cursor/skills/shad-overview/components/pagination.md new file mode 100644 index 00000000..b8b0d1f7 --- /dev/null +++ b/.cursor/skills/shad-overview/components/pagination.md @@ -0,0 +1,126 @@ +# Pagination + +Page navigation with previous and next links. [ShadPagination] shows a windowed run of page numbers; [ShadPaginationCompact] fits toolbars and table footers. + +## Default + +Previous and next labels with a windowed run of page numbers. An ellipsis marks pages that were skipped. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class PaginationBasicExample extends StatefulWidget { + const PaginationBasicExample({super.key}); + + @override + State createState() => _PaginationBasicExampleState(); +} + +class _PaginationBasicExampleState extends State { + int page = 5; + + @override + Widget build(BuildContext context) { + return ShadPagination( + page: page, + pageCount: 20, + onPageChanged: (value) => setState(() => page = value), + ); + } +} +``` + +## Simple + +Page numbers only — set `showEdges: false` when every page fits without an ellipsis. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class PaginationSimpleExample extends StatefulWidget { + const PaginationSimpleExample({super.key}); + + @override + State createState() => + _PaginationSimpleExampleState(); +} + +class _PaginationSimpleExampleState extends State { + int page = 2; + + @override + Widget build(BuildContext context) { + return ShadPagination( + page: page, + pageCount: 5, + showEdges: false, + onPageChanged: (value) => setState(() => page = value), + ); + } +} +``` + +## Compact + +Previous and next icon buttons with a `Page n / m` counter, useful beneath a data table. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class PaginationCompactExample extends StatefulWidget { + const PaginationCompactExample({super.key}); + + @override + State createState() => + _PaginationCompactExampleState(); +} + +class _PaginationCompactExampleState extends State { + int page = 3; + + @override + Widget build(BuildContext context) { + return ShadPaginationCompact( + page: page, + pageCount: 10, + onPageChanged: (value) => setState(() => page = value), + ); + } +} +``` + +## Icons only + +Drop the counter with `labelBuilder` when space is tight, for example beside a rows-per-page control. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class PaginationIconsOnlyExample extends StatefulWidget { + const PaginationIconsOnlyExample({super.key}); + + @override + State createState() => + _PaginationIconsOnlyExampleState(); +} + +class _PaginationIconsOnlyExampleState + extends State { + int page = 4; + + @override + Widget build(BuildContext context) { + return ShadPaginationCompact( + page: page, + pageCount: 12, + onPageChanged: (value) => setState(() => page = value), + labelBuilder: (_, _, _) => const SizedBox.shrink(), + ); + } +} +``` + diff --git a/.cursor/skills/shad-overview/components/popover.md b/.cursor/skills/shad-overview/components/popover.md new file mode 100644 index 00000000..cb481e94 --- /dev/null +++ b/.cursor/skills/shad-overview/components/popover.md @@ -0,0 +1,69 @@ +# Popover + +Displays rich content in a portal, triggered by a button. + +## Default + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +const _layer = [ + (name: 'Width', initialValue: '100%'), + (name: 'Max. width', initialValue: '300px'), + (name: 'Height', initialValue: '25px'), + (name: 'Max. height', initialValue: 'none'), +]; + +class PopoverBasicExample extends StatefulWidget { + const PopoverBasicExample({super.key}); + + @override + State createState() => _PopoverBasicExampleState(); +} + +class _PopoverBasicExampleState extends State { + final controller = ShadPopoverController(); + + @override + void dispose() { + controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final textTheme = ShadTheme.of(context).textTheme; + return ShadPopover( + controller: controller, + popover: (_) => SizedBox( + width: 288, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + spacing: 8, + children: [ + Text('Dimensions', style: textTheme.h4), + Text('Set the dimensions for the layer.', style: textTheme.p), + for (final field in _layer) + Row( + children: [ + Expanded(child: Text(field.name)), + Expanded( + flex: 2, + child: ShadInput(initialValue: field.initialValue), + ), + ], + ), + ], + ), + ), + child: ShadButton.outline( + onPressed: controller.toggle, + child: const Text('Open popover'), + ), + ); + } +} +``` + diff --git a/.cursor/skills/shad-overview/components/progress.md b/.cursor/skills/shad-overview/components/progress.md new file mode 100644 index 00000000..93ad6e7f --- /dev/null +++ b/.cursor/skills/shad-overview/components/progress.md @@ -0,0 +1,32 @@ +# Progress + +An indicator that shows how much of a task is complete. + +## Determinate and indeterminate + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class ProgressBasicExample extends StatelessWidget { + const ProgressBasicExample({super.key}); + + @override + Widget build(BuildContext context) { + return ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 400), + child: Column( + mainAxisSize: MainAxisSize.min, + spacing: 24, + children: [ + // Determinate: pass a value between 0 and 1. + ShadProgress(value: .5), + // Indeterminate: leave the value out. + const ShadProgress(), + ], + ), + ); + } +} +``` + diff --git a/.cursor/skills/shad-overview/components/radio-group.md b/.cursor/skills/shad-overview/components/radio-group.md new file mode 100644 index 00000000..c1cf9f8a --- /dev/null +++ b/.cursor/skills/shad-overview/components/radio-group.md @@ -0,0 +1,37 @@ +# Radio Group + +A set of checkable buttons where only one can be checked at a time. + +## Default + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +enum NotifyAbout { + all('All new messages'), + mentions('Direct messages and mentions'), + nothing('Nothing'); + + const NotifyAbout(this.message); + + final String message; +} + +class RadioGroupBasicExample extends StatelessWidget { + const RadioGroupBasicExample({super.key}); + + @override + Widget build(BuildContext context) { + return ShadRadioGroup( + initialValue: NotifyAbout.mentions, + onChanged: (value) {}, + items: [ + for (final option in NotifyAbout.values) + ShadRadio(value: option, label: Text(option.message)), + ], + ); + } +} +``` + diff --git a/.cursor/skills/shad-overview/components/resizable.md b/.cursor/skills/shad-overview/components/resizable.md new file mode 100644 index 00000000..7047cda4 --- /dev/null +++ b/.cursor/skills/shad-overview/components/resizable.md @@ -0,0 +1,64 @@ +# Resizable + +Resizable panel groups and layouts, with nesting and drag handles. + +## Default + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class ResizableBasicExample extends StatelessWidget { + const ResizableBasicExample({super.key}); + + @override + Widget build(BuildContext context) { + final theme = ShadTheme.of(context); + return SizedBox( + width: 300, + height: 200, + child: DecoratedBox( + decoration: BoxDecoration( + borderRadius: theme.radius, + border: Border.all(color: theme.colorScheme.border), + ), + child: ClipRRect( + borderRadius: theme.radius, + child: ShadResizablePanelGroup( + showHandle: true, + children: [ + ShadResizablePanel( + id: 0, + defaultSize: .5, + minSize: .2, + child: const Center(child: Text('One')), + ), + ShadResizablePanel( + id: 1, + defaultSize: .5, + child: ShadResizablePanelGroup( + axis: Axis.vertical, + showHandle: true, + children: [ + ShadResizablePanel( + id: 0, + defaultSize: .5, + child: const Center(child: Text('Two')), + ), + ShadResizablePanel( + id: 1, + defaultSize: .5, + child: const Center(child: Text('Three')), + ), + ], + ), + ), + ], + ), + ), + ), + ); + } +} +``` + diff --git a/.cursor/skills/shad-overview/components/select.md b/.cursor/skills/shad-overview/components/select.md new file mode 100644 index 00000000..381721f1 --- /dev/null +++ b/.cursor/skills/shad-overview/components/select.md @@ -0,0 +1,106 @@ +# Select + +Displays a list of options for the user to pick from, triggered by a button. + +## Default + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +const _fruits = { + 'apple': 'Apple', + 'banana': 'Banana', + 'blueberry': 'Blueberry', + 'grapes': 'Grapes', + 'pineapple': 'Pineapple', +}; + +class SelectBasicExample extends StatelessWidget { + const SelectBasicExample({super.key}); + + @override + Widget build(BuildContext context) { + return ConstrainedBox( + constraints: const BoxConstraints(minWidth: 180), + child: ShadSelect( + placeholder: const Text('Select a fruit'), + options: [ + for (final fruit in _fruits.entries) + ShadOption(value: fruit.key, child: Text(fruit.value)), + ], + selectedOptionBuilder: (context, value) => Text(_fruits[value]!), + onChanged: (value) {}, + ), + ); + } +} +``` + +## With search + +Filter a long list of Dart frameworks and tools — Serverpod, Relic, Flutter, and more. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +const _dartFrameworks = { + 'serverpod': 'Serverpod', + 'relic': 'Relic', + 'flutter': 'Flutter', + 'flame': 'Flame', + 'jaspr': 'Jaspr', + 'drift': 'Drift', + 'riverpod': 'Riverpod', + 'melos': 'Melos', + 'mason': 'Mason', + 'very_good_cli': 'Very Good CLI', +}; + +class SelectSearchExample extends StatefulWidget { + const SelectSearchExample({super.key}); + + @override + State createState() => _SelectSearchExampleState(); +} + +class _SelectSearchExampleState extends State { + var searchValue = ''; + + Map get filtered => { + for (final entry in _dartFrameworks.entries) + if (entry.value.toLowerCase().contains(searchValue.toLowerCase())) + entry.key: entry.value, + }; + + @override + Widget build(BuildContext context) { + return ConstrainedBox( + constraints: const BoxConstraints(minWidth: 180), + child: ShadSelect.withSearch( + minWidth: 180, + placeholder: const Text('Select framework...'), + onSearchChanged: (value) => setState(() => searchValue = value), + searchPlaceholder: const Text('Search framework'), + options: [ + if (filtered.isEmpty) + const Padding( + padding: EdgeInsets.symmetric(vertical: 24), + child: Text('No framework found'), + ), + for (final entry in _dartFrameworks.entries) + Offstage( + offstage: !filtered.containsKey(entry.key), + child: ShadOption(value: entry.key, child: Text(entry.value)), + ), + ], + selectedOptionBuilder: (context, value) => + Text(_dartFrameworks[value]!), + onChanged: (value) {}, + ), + ); + } +} +``` + diff --git a/.cursor/skills/shad-overview/components/separator.md b/.cursor/skills/shad-overview/components/separator.md new file mode 100644 index 00000000..2520fb8e --- /dev/null +++ b/.cursor/skills/shad-overview/components/separator.md @@ -0,0 +1,54 @@ +# Separator + +Visually or semantically separates content. + +## Default + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class SeparatorBasicExample extends StatelessWidget { + const SeparatorBasicExample({super.key}); + + @override + Widget build(BuildContext context) { + final theme = ShadTheme.of(context); + return SizedBox( + width: 300, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Radix Primitives', style: theme.textTheme.small), + const SizedBox(height: 4), + Text( + 'An open-source UI component library.', + style: theme.textTheme.muted, + ), + const ShadSeparator.horizontal( + margin: EdgeInsets.symmetric(vertical: 16), + ), + const SizedBox( + height: 20, + child: Row( + children: [ + Text('Blog'), + ShadSeparator.vertical( + margin: EdgeInsets.symmetric(horizontal: 16), + ), + Text('Docs'), + ShadSeparator.vertical( + margin: EdgeInsets.symmetric(horizontal: 16), + ), + Text('Source'), + ], + ), + ), + ], + ), + ); + } +} +``` + diff --git a/.cursor/skills/shad-overview/components/sheet.md b/.cursor/skills/shad-overview/components/sheet.md new file mode 100644 index 00000000..28912fd7 --- /dev/null +++ b/.cursor/skills/shad-overview/components/sheet.md @@ -0,0 +1,53 @@ +# Sheet + +Extends the dialog to display content that complements the main content of the screen, sliding in from any edge. + +## Sides + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class SheetSidesExample extends StatelessWidget { + const SheetSidesExample({super.key}); + + void _open(BuildContext context, ShadSheetSide side) { + showShadSheet( + context: context, + side: side, + builder: (context) => ShadSheet( + title: const Text('Edit Profile'), + description: const Text( + "Make changes to your profile here. Click save when you're done.", + ), + actions: [ + ShadButton( + child: const Text('Save changes'), + onPressed: () => Navigator.of(context).pop(), + ), + ], + child: const Padding( + padding: EdgeInsets.symmetric(vertical: 20), + child: ShadInput(placeholder: Text('Name')), + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + return Wrap( + spacing: 8, + runSpacing: 8, + children: [ + for (final side in ShadSheetSide.values) + ShadButton.outline( + onPressed: () => _open(context, side), + child: Text(side.name), + ), + ], + ); + } +} +``` + diff --git a/.cursor/skills/shad-overview/components/sidebar.md b/.cursor/skills/shad-overview/components/sidebar.md new file mode 100644 index 00000000..849494c4 --- /dev/null +++ b/.cursor/skills/shad-overview/components/sidebar.md @@ -0,0 +1,209 @@ +# Sidebar + +A composable, collapsible side navigation panel. Collapses to an icon rail or off-canvas, floats or insets, and becomes a modal sheet under the mobile breakpoint. ⌘B toggles it. + +## Icon rail + +With collapsible: icon the sidebar shrinks to a rail of icons. Menu buttons show their label as a tooltip, and with rail: true the inner edge becomes a grab rail — click it to toggle. This docs app itself runs on ShadSidebarScaffold. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class SidebarBasicExample extends StatefulWidget { + const SidebarBasicExample({super.key}); + + @override + State createState() => _SidebarBasicExampleState(); +} + +class _SidebarBasicExampleState extends State { + String selected = 'Home'; + + ShadSidebarMenuButton item(String label, IconData icon) { + return ShadSidebarMenuButton( + leading: Icon(icon), + isActive: selected == label, + tooltip: label, + onPressed: () => setState(() => selected = label), + child: Text(label), + ); + } + + @override + Widget build(BuildContext context) { + final theme = ShadTheme.of(context); + return SizedBox( + width: 700, + height: 420, + child: DecoratedBox( + decoration: BoxDecoration( + border: Border.all(color: theme.colorScheme.border), + borderRadius: theme.radii.lg, + ), + child: ShadSidebarScaffold( + borderRadius: theme.radii.lg, + sidebar: ShadSidebar( + collapsible: ShadSidebarCollapsible.icon, + // The inner edge highlights under the pointer and toggles the + // sidebar on click. + rail: true, + header: Row( + children: [ + Container( + width: 32, + height: 32, + decoration: BoxDecoration( + color: theme.colorScheme.primary, + borderRadius: theme.radii.md, + ), + child: Icon( + LucideIcons.galleryVerticalEnd, + size: 16, + color: theme.colorScheme.primaryForeground, + ), + ), + const SizedBox(width: 8), + Expanded( + child: Text( + 'Acme Inc', + maxLines: 1, + softWrap: false, + overflow: TextOverflow.clip, + style: theme.textTheme.small, + ), + ), + ], + ), + footer: ShadSidebarMenuButton( + leading: const Icon(LucideIcons.circleUser), + trailing: const Icon(LucideIcons.chevronsUpDown), + tooltip: 'Account', + onPressed: () {}, + child: const Text('shadcn'), + ), + children: [ + ShadSidebarGroup( + label: const Text('Platform'), + children: [ + ShadSidebarMenu( + children: [ + item('Home', LucideIcons.house), + item('Inbox', LucideIcons.inbox), + item('Calendar', LucideIcons.calendar), + ], + ), + ], + ), + ShadSidebarGroup( + label: const Text('Settings'), + children: [ + ShadSidebarMenu( + children: [ + item('Profile', LucideIcons.user), + item('Billing', LucideIcons.creditCard), + ], + ), + ], + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(8), + child: Row( + children: [ + const ShadSidebarTrigger(), + const SizedBox(width: 8), + Text(selected, style: theme.textTheme.small), + ], + ), + ), + const Expanded( + child: Center(child: Text('Content area')), + ), + ], + ), + ), + ), + ); + } +} +``` + +## Floating, with sub-menus + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class SidebarSubmenuExample extends StatefulWidget { + const SidebarSubmenuExample({super.key}); + + @override + State createState() => _SidebarSubmenuExampleState(); +} + +class _SidebarSubmenuExampleState extends State { + String selected = 'Installation'; + + static const _sections = { + 'Getting Started': ['Installation', 'Project Structure'], + 'Building Your Application': ['Routing', 'Data Fetching', 'Rendering'], + }; + + @override + Widget build(BuildContext context) { + final theme = ShadTheme.of(context); + return SizedBox( + width: 700, + height: 420, + child: DecoratedBox( + decoration: BoxDecoration( + border: Border.all(color: theme.colorScheme.border), + borderRadius: theme.radii.lg, + ), + child: ShadSidebarScaffold( + borderRadius: theme.radii.lg, + sidebar: ShadSidebar( + variant: ShadSidebarVariant.floating, + children: [ + ShadSidebarGroup( + label: const Text('Documentation'), + children: [ + ShadSidebarMenu( + children: [ + for (final entry in _sections.entries) ...[ + ShadSidebarMenuButton( + leading: const Icon(LucideIcons.bookOpen), + onPressed: () {}, + child: Text(entry.key), + ), + ShadSidebarMenuSub( + children: [ + for (final page in entry.value) + ShadSidebarMenuSubButton( + isActive: selected == page, + onPressed: () => + setState(() => selected = page), + child: Text(page), + ), + ], + ), + ], + ], + ), + ], + ), + ], + ), + child: Center(child: Text(selected)), + ), + ), + ); + } +} +``` + diff --git a/.cursor/skills/shad-overview/components/skeleton.md b/.cursor/skills/shad-overview/components/skeleton.md new file mode 100644 index 00000000..0874a63a --- /dev/null +++ b/.cursor/skills/shad-overview/components/skeleton.md @@ -0,0 +1,35 @@ +# Skeleton + +A placeholder to show while content is loading. + +## Default + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class SkeletonCardExample extends StatelessWidget { + const SkeletonCardExample({super.key}); + + @override + Widget build(BuildContext context) { + return const Row( + mainAxisSize: MainAxisSize.min, + children: [ + ShadSkeleton.circle(size: 48), + SizedBox(width: 12), + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ShadSkeleton(width: 250, height: 16), + SizedBox(height: 8), + ShadSkeleton(width: 200, height: 16), + ], + ), + ], + ); + } +} +``` + diff --git a/.cursor/skills/shad-overview/components/slider.md b/.cursor/skills/shad-overview/components/slider.md new file mode 100644 index 00000000..29686f50 --- /dev/null +++ b/.cursor/skills/shad-overview/components/slider.md @@ -0,0 +1,106 @@ +# Slider + +An input where the user selects a value from within a given range. + +## Default + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class SliderBasicExample extends StatelessWidget { + const SliderBasicExample({super.key}); + + @override + Widget build(BuildContext context) { + return const SizedBox( + width: 300, + child: ShadSlider( + initialValue: 33, + max: 100, + ), + ); + } +} +``` + +## Range + +ShadRangeSlider takes one value per thumb. The values stay in ascending order — a thumb stops where its neighbour is rather than crossing it. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class SliderRangeExample extends StatefulWidget { + const SliderRangeExample({super.key}); + + @override + State createState() => _SliderRangeExampleState(); +} + +class _SliderRangeExampleState extends State { + List values = const [25, 75]; + + @override + Widget build(BuildContext context) { + final theme = ShadTheme.of(context); + return SizedBox( + width: 300, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + spacing: 12, + children: [ + Text( + // The values stay ascending, so the ends of the range are always + // the first and last entry. + '\$${values.first.round()} – \$${values.last.round()}', + style: theme.textTheme.small, + ), + ShadRangeSlider( + initialValues: values, + max: 100, + onChanged: (next) => setState(() => values = next), + ), + ], + ), + ); + } +} +``` + +## Steps + +With divisions the slider snaps; set showDivisionMarks to false to snap without drawing the ticks. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class SliderStepsExample extends StatelessWidget { + const SliderStepsExample({super.key}); + + @override + Widget build(BuildContext context) { + return const SizedBox( + width: 300, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + spacing: 24, + children: [ + ShadSlider(initialValue: 50, max: 100, divisions: 10), + ShadSlider( + initialValue: 50, + max: 100, + divisions: 10, + showDivisionMarks: false, + ), + ], + ), + ); + } +} +``` + diff --git a/.cursor/skills/shad-overview/components/sonner.md b/.cursor/skills/shad-overview/components/sonner.md new file mode 100644 index 00000000..e8b29722 --- /dev/null +++ b/.cursor/skills/shad-overview/components/sonner.md @@ -0,0 +1,39 @@ +# Sonner + +An opinionated toast stack: notifications collect and expand on hover. + +## Default + +```dart +import 'dart:math'; + +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class SonnerBasicExample extends StatelessWidget { + const SonnerBasicExample({super.key}); + + @override + Widget build(BuildContext context) { + return ShadButton.outline( + child: const Text('Show Toast'), + onPressed: () { + final sonner = ShadSonner.of(context); + final id = Random().nextInt(1000); + sonner.show( + ShadToast( + id: id, + title: const Text('Event has been created'), + description: Text('${DateTime.now()}'), + action: ShadButton( + child: const Text('Undo'), + onPressed: () => sonner.hide(id), + ), + ), + ); + }, + ); + } +} +``` + diff --git a/.cursor/skills/shad-overview/components/spinner.md b/.cursor/skills/shad-overview/components/spinner.md new file mode 100644 index 00000000..c592875f --- /dev/null +++ b/.cursor/skills/shad-overview/components/spinner.md @@ -0,0 +1,28 @@ +# Spinner + +An animated loading indicator. + +## Sizes + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class SpinnerBasicExample extends StatelessWidget { + const SpinnerBasicExample({super.key}); + + @override + Widget build(BuildContext context) { + return const Row( + mainAxisSize: MainAxisSize.min, + spacing: 24, + children: [ + ShadSpinner(size: 16), + ShadSpinner(), + ShadSpinner(size: 32), + ], + ); + } +} +``` + diff --git a/.cursor/skills/shad-overview/components/switch.md b/.cursor/skills/shad-overview/components/switch.md new file mode 100644 index 00000000..bb6e5025 --- /dev/null +++ b/.cursor/skills/shad-overview/components/switch.md @@ -0,0 +1,31 @@ +# Switch + +A control that allows the user to toggle between checked and not checked. + +## Default + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class SwitchBasicExample extends StatefulWidget { + const SwitchBasicExample({super.key}); + + @override + State createState() => _SwitchBasicExampleState(); +} + +class _SwitchBasicExampleState extends State { + bool value = false; + + @override + Widget build(BuildContext context) { + return ShadSwitch( + value: value, + onChanged: (v) => setState(() => value = v), + label: const Text('Airplane Mode'), + ); + } +} +``` + diff --git a/.cursor/skills/shad-overview/components/table.md b/.cursor/skills/shad-overview/components/table.md new file mode 100644 index 00000000..0ad3d197 --- /dev/null +++ b/.cursor/skills/shad-overview/components/table.md @@ -0,0 +1,104 @@ +# Table + +A responsive table component with header and footer rows. + +## Invoices + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +const _invoices = [ + ( + invoice: 'INV001', + status: 'Paid', + method: 'Credit Card', + amount: r'$250.00', + ), + (invoice: 'INV002', status: 'Pending', method: 'PayPal', amount: r'$150.00'), + ( + invoice: 'INV003', + status: 'Unpaid', + method: 'Bank Transfer', + amount: r'$350.00', + ), + ( + invoice: 'INV004', + status: 'Paid', + method: 'Credit Card', + amount: r'$450.00', + ), + (invoice: 'INV005', status: 'Paid', method: 'PayPal', amount: r'$550.00'), +]; + +class TableInvoicesExample extends StatelessWidget { + const TableInvoicesExample({super.key}); + + @override + Widget build(BuildContext context) { + final theme = ShadTheme.of(context); + final rowHeight = theme.tableTheme.cellHeight ?? 48; + // Header row + one row per invoice + footer row, so the card hugs the + // table exactly instead of leaving space below the last row. + final tableHeight = rowHeight * (_invoices.length + 2); + + return ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 600), + child: SizedBox( + width: double.infinity, + child: ShadCard( + padding: EdgeInsets.zero, + clipBehavior: Clip.antiAlias, + child: SizedBox( + height: tableHeight, + child: ShadTable.list( + // The Amount column absorbs any leftover width so the table + // spans the full card instead of leaving a gap on the right. + columnSpanExtent: (index) => switch (index) { + 0 => const FixedTableSpanExtent(140), + 1 => const FixedTableSpanExtent(110), + 2 => const FixedTableSpanExtent(140), + _ => const MaxTableSpanExtent( + FixedTableSpanExtent(120), + RemainingTableSpanExtent(), + ), + }, + header: const [ + ShadTableCell.header(child: Text('Invoice')), + ShadTableCell.header(child: Text('Status')), + ShadTableCell.header(child: Text('Method')), + ShadTableCell.header( + alignment: Alignment.centerRight, + child: Text('Amount'), + ), + ], + footer: const [ + ShadTableCell.footer(child: Text('Total')), + ShadTableCell.footer(child: Text('')), + ShadTableCell.footer(child: Text('')), + ShadTableCell.footer( + alignment: Alignment.centerRight, + child: Text(r'$1750.00'), + ), + ], + children: [ + for (final invoice in _invoices) + [ + ShadTableCell(child: Text(invoice.invoice)), + ShadTableCell(child: Text(invoice.status)), + ShadTableCell(child: Text(invoice.method)), + ShadTableCell( + alignment: Alignment.centerRight, + child: Text(invoice.amount), + ), + ], + ], + ), + ), + ), + ), + ); + } +} +``` + diff --git a/.cursor/skills/shad-overview/components/tabs.md b/.cursor/skills/shad-overview/components/tabs.md new file mode 100644 index 00000000..9c3f1591 --- /dev/null +++ b/.cursor/skills/shad-overview/components/tabs.md @@ -0,0 +1,81 @@ +# Tabs + +A set of layered sections of content displayed one at a time. + +## Default + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class TabsBasicExample extends StatelessWidget { + const TabsBasicExample({super.key}); + + @override + Widget build(BuildContext context) { + return ShadTabs( + value: 'account', + tabBarConstraints: const BoxConstraints(maxWidth: 400), + contentConstraints: const BoxConstraints(maxWidth: 400), + tabs: [ + ShadTab( + value: 'account', + content: ShadCard( + title: const Text('Account'), + description: const Text( + "Make changes to your account here. Click save when you're " + 'done.', + ), + footer: const ShadButton(child: Text('Save changes')), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(height: 16), + ShadInputFormField( + label: const Text('Name'), + initialValue: 'Ale', + ), + const SizedBox(height: 8), + ShadInputFormField( + label: const Text('Username'), + initialValue: 'nank1ro', + ), + const SizedBox(height: 16), + ], + ), + ), + child: const Text('Account'), + ), + ShadTab( + value: 'password', + content: ShadCard( + title: const Text('Password'), + description: const Text( + "Change your password here. After saving, you'll be logged " + 'out.', + ), + footer: const ShadButton(child: Text('Save password')), + child: Column( + children: [ + const SizedBox(height: 16), + ShadInputFormField( + label: const Text('Current password'), + obscureText: true, + ), + const SizedBox(height: 8), + ShadInputFormField( + label: const Text('New password'), + obscureText: true, + ), + const SizedBox(height: 16), + ], + ), + ), + child: const Text('Password'), + ), + ], + ); + } +} +``` + diff --git a/.cursor/skills/shad-overview/components/textarea.md b/.cursor/skills/shad-overview/components/textarea.md new file mode 100644 index 00000000..ac0d0936 --- /dev/null +++ b/.cursor/skills/shad-overview/components/textarea.md @@ -0,0 +1,26 @@ +# Textarea + +A multi-line text input, optionally user-resizable. + +## Default + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class TextareaBasicExample extends StatelessWidget { + const TextareaBasicExample({super.key}); + + @override + Widget build(BuildContext context) { + return ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 400), + child: ShadTextarea( + placeholder: const Text('Type your message here...'), + onChanged: (value) {}, + ), + ); + } +} +``` + diff --git a/.cursor/skills/shad-overview/components/time-picker.md b/.cursor/skills/shad-overview/components/time-picker.md new file mode 100644 index 00000000..006095ab --- /dev/null +++ b/.cursor/skills/shad-overview/components/time-picker.md @@ -0,0 +1,44 @@ +# Time Picker + +A field for entering a time of day. + +## 24-hour + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class TimePickerBasicExample extends StatelessWidget { + const TimePickerBasicExample({super.key}); + + @override + Widget build(BuildContext context) { + return ShadTimePicker( + trailing: const Padding( + padding: EdgeInsets.only(left: 8, top: 14), + child: Icon(LucideIcons.clock4), + ), + onChanged: (time) {}, + ); + } +} +``` + +## With AM/PM period + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class TimePickerPeriodExample extends StatelessWidget { + const TimePickerPeriodExample({super.key}); + + @override + Widget build(BuildContext context) { + return ShadTimePicker.period( + onChanged: (time) {}, + ); + } +} +``` + diff --git a/.cursor/skills/shad-overview/components/toast.md b/.cursor/skills/shad-overview/components/toast.md new file mode 100644 index 00000000..75bd4499 --- /dev/null +++ b/.cursor/skills/shad-overview/components/toast.md @@ -0,0 +1,61 @@ +# Toast + +A succinct message that is displayed temporarily. + +## Default + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class ToastBasicExample extends StatelessWidget { + const ToastBasicExample({super.key}); + + @override + Widget build(BuildContext context) { + return ShadButton.outline( + child: const Text('Add to calendar'), + onPressed: () { + final toaster = ShadToaster.of(context); + toaster.show( + ShadToast( + title: const Text('Scheduled: Catch up'), + description: const Text('Friday, February 10, 2023 at 5:57 PM'), + action: ShadButton.outline( + child: const Text('Undo'), + onPressed: () => toaster.hide(), + ), + ), + ); + }, + ); + } +} +``` + +## Destructive + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class ToastDestructiveExample extends StatelessWidget { + const ToastDestructiveExample({super.key}); + + @override + Widget build(BuildContext context) { + return ShadButton.outline( + child: const Text('Show destructive toast'), + onPressed: () { + ShadToaster.of(context).show( + const ShadToast.destructive( + title: Text('Uh oh! Something went wrong'), + description: Text('There was a problem with your request'), + ), + ); + }, + ); + } +} +``` + diff --git a/.cursor/skills/shad-overview/components/toggle-group.md b/.cursor/skills/shad-overview/components/toggle-group.md new file mode 100644 index 00000000..6ad8cda7 --- /dev/null +++ b/.cursor/skills/shad-overview/components/toggle-group.md @@ -0,0 +1,142 @@ +# Toggle Group + +A set of two-state buttons that can be toggled on or off. + +## Single + +Selecting one value clears the others. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class ToggleGroupBasicExample extends StatefulWidget { + const ToggleGroupBasicExample({super.key}); + + @override + State createState() => + _ToggleGroupBasicExampleState(); +} + +class _ToggleGroupBasicExampleState extends State { + Set alignment = {'left'}; + + @override + Widget build(BuildContext context) { + return ShadToggleGroup( + values: alignment, + onChanged: (values) => setState(() => alignment = values), + children: const [ + ShadToggleGroupItem( + value: 'left', + semanticLabel: 'Align left', + child: Icon(LucideIcons.alignLeft), + ), + ShadToggleGroupItem( + value: 'center', + semanticLabel: 'Align center', + child: Icon(LucideIcons.alignCenter), + ), + ShadToggleGroupItem( + value: 'right', + semanticLabel: 'Align right', + child: Icon(LucideIcons.alignRight), + ), + ], + ); + } +} +``` + +## Multiple + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class ToggleGroupMultipleExample extends StatefulWidget { + const ToggleGroupMultipleExample({super.key}); + + @override + State createState() => + _ToggleGroupMultipleExampleState(); +} + +class _ToggleGroupMultipleExampleState + extends State { + Set formatting = {'bold'}; + + @override + Widget build(BuildContext context) { + return ShadToggleGroup.multiple( + values: formatting, + onChanged: (values) => setState(() => formatting = values), + children: const [ + ShadToggleGroupItem( + value: 'bold', + semanticLabel: 'Bold', + child: Icon(LucideIcons.bold), + ), + ShadToggleGroupItem( + value: 'italic', + semanticLabel: 'Italic', + child: Icon(LucideIcons.italic), + ), + ShadToggleGroupItem( + value: 'underline', + semanticLabel: 'Underline', + child: Icon(LucideIcons.underline), + ), + ], + ); + } +} +``` + +## Outline + +Every item uses the outline toggle style. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class ToggleGroupOutlineExample extends StatefulWidget { + const ToggleGroupOutlineExample({super.key}); + + @override + State createState() => + _ToggleGroupOutlineExampleState(); +} + +class _ToggleGroupOutlineExampleState extends State { + Set values = const {'bold'}; + + @override + Widget build(BuildContext context) { + return ShadToggleGroup( + values: values, + toggleVariant: ShadToggleVariant.outline, + onChanged: (next) => setState(() => values = next), + children: const [ + ShadToggleGroupItem( + value: 'bold', + semanticLabel: 'Bold', + child: Icon(LucideIcons.bold), + ), + ShadToggleGroupItem( + value: 'italic', + semanticLabel: 'Italic', + child: Icon(LucideIcons.italic), + ), + ShadToggleGroupItem( + value: 'underline', + semanticLabel: 'Underline', + child: Icon(LucideIcons.underline), + ), + ], + ); + } +} +``` + diff --git a/.cursor/skills/shad-overview/components/toggle.md b/.cursor/skills/shad-overview/components/toggle.md new file mode 100644 index 00000000..8cae7cf4 --- /dev/null +++ b/.cursor/skills/shad-overview/components/toggle.md @@ -0,0 +1,88 @@ +# Toggle + +A two-state button that can be either on or off. + +## Default + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class ToggleBasicExample extends StatefulWidget { + const ToggleBasicExample({super.key}); + + @override + State createState() => _ToggleBasicExampleState(); +} + +class _ToggleBasicExampleState extends State { + bool bold = false; + bool underline = false; + + @override + Widget build(BuildContext context) { + return Row( + mainAxisSize: MainAxisSize.min, + spacing: 8, + children: [ + ShadToggle( + value: bold, + semanticLabel: 'Bold', + onChanged: (value) => setState(() => bold = value), + child: const Icon(LucideIcons.bold), + ), + ShadToggle( + value: underline, + leading: const Icon(LucideIcons.underline), + onChanged: (value) => setState(() => underline = value), + child: const Text('Underline'), + ), + ], + ); + } +} +``` + +## Outline + +A bordered toggle with a subtle shadow. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class ToggleOutlineExample extends StatefulWidget { + const ToggleOutlineExample({super.key}); + + @override + State createState() => _ToggleOutlineExampleState(); +} + +class _ToggleOutlineExampleState extends State { + bool bold = false; + bool italic = false; + + @override + Widget build(BuildContext context) { + return Row( + mainAxisSize: MainAxisSize.min, + spacing: 8, + children: [ + ShadToggle.outline( + value: bold, + semanticLabel: 'Bold', + onChanged: (value) => setState(() => bold = value), + child: const Icon(LucideIcons.bold), + ), + ShadToggle.outline( + value: italic, + semanticLabel: 'Italic', + onChanged: (value) => setState(() => italic = value), + child: const Icon(LucideIcons.italic), + ), + ], + ); + } +} +``` + diff --git a/.cursor/skills/shad-overview/components/tooltip.md b/.cursor/skills/shad-overview/components/tooltip.md new file mode 100644 index 00000000..ce6e4abd --- /dev/null +++ b/.cursor/skills/shad-overview/components/tooltip.md @@ -0,0 +1,268 @@ +# Tooltip + +A popup that displays information related to an element on hover or focus. + +## Basic + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class TooltipBasicExample extends StatelessWidget { + const TooltipBasicExample({super.key}); + + @override + Widget build(BuildContext context) { + return ShadTooltip( + builder: (context) => const Text('Add to library'), + child: ShadButton.outline( + onPressed: () {}, + child: const Text('Show Tooltip'), + ), + ); + } +} +``` + +## Sides + +An anchor places the tooltip on any side of its trigger; the arrow direction follows it. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class TooltipSidesExample extends StatelessWidget { + const TooltipSidesExample({super.key}); + + @override + Widget build(BuildContext context) { + return const Wrap( + spacing: 8, + runSpacing: 8, + children: [ + _SideTooltip(label: 'Left', direction: AxisDirection.left), + _SideTooltip(label: 'Top', direction: AxisDirection.up), + _SideTooltip(label: 'Bottom', direction: AxisDirection.down), + _SideTooltip(label: 'Right', direction: AxisDirection.right), + ], + ); + } +} + +class _SideTooltip extends StatelessWidget { + const _SideTooltip({required this.label, required this.direction}); + + final String label; + + /// The side of the trigger the tooltip appears on. + final AxisDirection direction; + + @override + Widget build(BuildContext context) { + final anchor = switch (direction) { + AxisDirection.up => const ShadAnchor( + childAlignment: Alignment.topCenter, + overlayAlignment: Alignment.bottomCenter, + offset: Offset(0, -4), + ), + AxisDirection.down => const ShadAnchor( + childAlignment: Alignment.bottomCenter, + overlayAlignment: Alignment.topCenter, + offset: Offset(0, 4), + ), + AxisDirection.left => const ShadAnchor( + childAlignment: Alignment.centerLeft, + overlayAlignment: Alignment.centerRight, + offset: Offset(-4, 0), + ), + AxisDirection.right => const ShadAnchor( + childAlignment: Alignment.centerRight, + overlayAlignment: Alignment.centerLeft, + offset: Offset(4, 0), + ), + }; + + return ShadTooltip( + anchor: anchor, + // The arrow points back at the trigger, opposite the placement side. + arrowDirection: switch (direction) { + AxisDirection.up => AxisDirection.down, + AxisDirection.down => AxisDirection.up, + AxisDirection.left => AxisDirection.right, + AxisDirection.right => AxisDirection.left, + }, + builder: (context) => const Text('Add to library'), + child: ShadButton.outline(onPressed: () {}, child: Text(label)), + ); + } +} +``` + +## With icon + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class TooltipIconExample extends StatelessWidget { + const TooltipIconExample({super.key}); + + @override + Widget build(BuildContext context) { + return ShadTooltip( + builder: (context) => const Text('Additional information'), + child: ShadIconButton.ghost( + icon: const Icon(LucideIcons.info), + onPressed: () {}, + ), + ); + } +} +``` + +## Long content + +Long content wraps at the tooltip’s max width. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class TooltipLongContentExample extends StatelessWidget { + const TooltipLongContentExample({super.key}); + + @override + Widget build(BuildContext context) { + return ShadTooltip( + // Long content wraps at the tooltip's max width. + builder: (context) => const Text( + 'To learn more about how this works, check out the docs. If you ' + 'have any questions, please reach out to us.', + ), + child: ShadButton.outline( + onPressed: () {}, + child: const Text('Show Tooltip'), + ), + ); + } +} +``` + +## Disabled + +A disabled control absorbs pointer events, so a gesture wrapper reports the hover on its behalf. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class TooltipDisabledExample extends StatelessWidget { + const TooltipDisabledExample({super.key}); + + @override + Widget build(BuildContext context) { + // A disabled button absorbs pointer events, so a wrapper reports the + // hover on its behalf — the reference wraps its trigger in a span for + // the same reason. + return ShadTooltip( + builder: (context) => const Text('This feature is currently unavailable'), + child: ShadGestureDetector( + child: ShadButton.outline( + enabled: false, + onPressed: () {}, + child: const Text('Disabled'), + ), + ), + ); + } +} +``` + +## With keyboard shortcut + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class TooltipKeyboardExample extends StatelessWidget { + const TooltipKeyboardExample({super.key}); + + @override + Widget build(BuildContext context) { + return ShadTooltip( + builder: (context) => const Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text('Save Changes'), + SizedBox(width: 6), + ShadKbd('S'), + ], + ), + child: ShadIconButton.outline( + icon: const Icon(LucideIcons.save), + onPressed: () {}, + ), + ); + } +} +``` + +## On link + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class TooltipLinkExample extends StatelessWidget { + const TooltipLinkExample({super.key}); + + @override + Widget build(BuildContext context) { + return ShadTooltip( + builder: (context) => const Text('Click to read the documentation'), + child: ShadButton.link( + onPressed: () {}, + child: const Text('Learn more'), + ), + ); + } +} +``` + +## Formatted content + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class TooltipFormattedExample extends StatelessWidget { + const TooltipFormattedExample({super.key}); + + @override + Widget build(BuildContext context) { + return ShadTooltip( + builder: (context) { + final style = DefaultTextStyle.of(context).style; + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Active', + style: style.copyWith(fontWeight: FontWeight.w600), + ), + const SizedBox(height: 4), + Opacity( + opacity: .8, + child: Text('Last updated 2 hours ago', style: style), + ), + ], + ); + }, + child: ShadButton.outline(onPressed: () {}, child: const Text('Status')), + ); + } +} +``` + diff --git a/.cursor/skills/shad-overview/guides/form.md b/.cursor/skills/shad-overview/guides/form.md new file mode 100644 index 00000000..ed70eb04 --- /dev/null +++ b/.cursor/skills/shad-overview/guides/form.md @@ -0,0 +1,139 @@ +# Form + +Building forms with `ShadForm`: field values collect under their ids, and validation runs per field. + +## Validation and submit + +`saveAndValidate` runs every validator and returns whether the form is valid. Values collect in `formKey.currentState!.value`. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class FormProfileExample extends StatefulWidget { + const FormProfileExample({super.key}); + + @override + State createState() => _FormProfileExampleState(); +} + +class _FormProfileExampleState extends State { + final formKey = GlobalKey(); + + @override + Widget build(BuildContext context) { + return ShadForm( + key: formKey, + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 350), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 16, + children: [ + ShadInputFormField( + id: 'username', + label: const Text('Username'), + placeholder: const Text('Enter your username'), + description: const Text('This is your public display name.'), + validator: (v) { + if (v.length < 2) { + return 'Username must be at least 2 characters.'; + } + return null; + }, + ), + ShadCheckboxFormField( + id: 'terms', + initialValue: false, + inputLabel: const Text('I accept the terms and conditions'), + validator: (v) { + if (!v) { + return 'You must accept the terms and conditions'; + } + return null; + }, + ), + ShadButton( + child: const Text('Submit'), + onPressed: () { + if (formKey.currentState!.saveAndValidate()) { + ShadToaster.of(context).show( + ShadToast( + title: const Text('Form submitted'), + description: Text('${formKey.currentState!.value}'), + ), + ); + } + }, + ), + ], + ), + ), + ); + } +} +``` + +## Every field type + +Each input component has a `*FormField` counterpart that plugs into `ShadForm` the same way: `ShadInputFormField`, `ShadSelectFormField`, `ShadTextareaFormField`, `ShadRadioGroupFormField`, `ShadDatePickerFormField`, `ShadDateRangePickerFormField`, `ShadTimePickerFormField`, `ShadCheckboxFormField`, `ShadSwitchFormField`, and `ShadInputOTPFormField`. See `example/lib/docs/examples/form/form_fields.dart` for one of each wired into a single form. + +## OTP verification + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class FormOtpExample extends StatefulWidget { + const FormOtpExample({super.key}); + + @override + State createState() => _FormOtpExampleState(); +} + +class _FormOtpExampleState extends State { + final formKey = GlobalKey(); + + @override + Widget build(BuildContext context) { + return ShadForm( + key: formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 16, + children: [ + ShadInputOTPFormField( + id: 'otp', + maxLength: 6, + label: const Text('OTP'), + description: const Text('Enter your one-time password.'), + validator: (v) => v.contains(' ') ? 'Fill the whole code' : null, + children: const [ + ShadInputOTPGroup( + children: [ + ShadInputOTPSlot(), + ShadInputOTPSlot(), + ShadInputOTPSlot(), + ], + ), + Icon(LucideIcons.dot), + ShadInputOTPGroup( + children: [ + ShadInputOTPSlot(), + ShadInputOTPSlot(), + ShadInputOTPSlot(), + ], + ), + ], + ), + ShadButton( + child: const Text('Verify'), + onPressed: () => formKey.currentState!.saveAndValidate(), + ), + ], + ), + ); + } +} +``` diff --git a/.cursor/skills/shad-overview/guides/installation.md b/.cursor/skills/shad-overview/guides/installation.md new file mode 100644 index 00000000..70a78c27 --- /dev/null +++ b/.cursor/skills/shad-overview/guides/installation.md @@ -0,0 +1,82 @@ +# Installation + +Add `shad` to your project from the command line: + +```bash +flutter pub add shad +``` + +Everything ships through one import: components, theme, Lucide icons, and animation effects. + +```dart +import 'package:shad/shad.dart'; +``` + +## Set up ShadApp + +`ShadApp` takes the place of `MaterialApp`. It installs the theme, localizations, navigation, and the overlay layer the floating components use. With no arguments it uses the slate color scheme and the nova style. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return const ShadApp( + home: HomePage(), + ); + } +} +``` + +Apps that use the `Router` API use `ShadApp.router` with the usual `routerConfig` or delegate parameters. + +## Dark mode + +Pass a `theme` and a `darkTheme`, and pick between them with `themeMode`. Follow the platform, or bind it to your own setting. + +```dart +ShadApp( + themeMode: ThemeMode.system, + theme: ShadThemeData(), + darkTheme: ShadThemeData( + brightness: Brightness.dark, + ), + home: const HomePage(), +) +``` + +## Use the components + +Components are plain widgets. Icons come from the bundled Lucide set: + +```dart +ShadButton( + leading: const Icon(LucideIcons.mail), + onPressed: () {}, + child: const Text('Login with Email'), +) +``` + +## Padding + +Use `ShadPadding` instead of Flutter's `Padding` to keep insets on the theme's spacing scale. It has no plain constructor, only `ShadPadding.all`, `.symmetric`, `.only`, and `.directional`, and their arguments are steps, not logical pixels: + +```dart +ShadPadding.symmetric( + horizontal: 6, // px-6 + vertical: 4, // py-4 + child: const Text('Inset by the theme spacing scale'), +) +``` + +See [layout.md](layout.md#padding) for a full example. + +From here, [theming.md](theming.md) and [styles.md](styles.md) cover how to make it yours, and each page under `components/` pairs a description with the actual example source that ships in the example app. diff --git a/.cursor/skills/shad-overview/guides/layout.md b/.cursor/skills/shad-overview/guides/layout.md new file mode 100644 index 00000000..250fa10a --- /dev/null +++ b/.cursor/skills/shad-overview/guides/layout.md @@ -0,0 +1,130 @@ +# Layout + +Layout widgets measure gaps and padding in theme spacing steps: `ShadRow`, `ShadColumn`, `ShadGap`, and `ShadPadding`. They read `ShadThemeData.spacing` the same way components do (see [styles.md](styles.md#spacing)), so a layout built from them scales with the rest of the theme. + +## Row and column spacing + +`ShadRow` and `ShadColumn` accept a `spacing` argument in theme steps. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class LayoutSpacingExample extends StatelessWidget { + const LayoutSpacingExample({super.key}); + + @override + Widget build(BuildContext context) { + return ShadColumn( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + spacing: 4, + children: [ + ShadCard( + child: ShadRow( + mainAxisSize: MainAxisSize.min, + spacing: 2, + children: [ + for (final label in ['One', 'Two', 'Three']) + ShadBadge.secondary(child: Text(label)), + ], + ), + ), + ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 320), + child: ShadCard( + child: ShadColumn( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, + spacing: 3, + children: [ + const ShadInput(placeholder: Text('Email')), + const ShadInput(placeholder: Text('Password')), + ShadButton(onPressed: () {}, child: const Text('Sign in')), + ], + ), + ), + ), + ], + ); + } +} +``` + +## Gaps + +Use `ShadGap` in a plain `Row` or `Column` when only some children need space between them. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class LayoutGapExample extends StatelessWidget { + const LayoutGapExample({super.key}); + + @override + Widget build(BuildContext context) { + final theme = ShadTheme.of(context); + + return ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 320), + child: ShadCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, + children: [ + Text('Account', style: theme.textTheme.h4), + const ShadGap(1), + Text( + 'Manage your profile and security settings.', + style: theme.textTheme.muted, + ), + const ShadGap(4), + ShadButton.outline( + onPressed: () {}, + child: const Text('Open settings'), + ), + ], + ), + ), + ); + } +} +``` + +## Padding + +`ShadPadding` mirrors Tailwind's step-based inset utilities. `symmetric(horizontal: 6, vertical: 4)` matches `px-6 py-4`. + +```dart +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +class LayoutPaddingExample extends StatelessWidget { + const LayoutPaddingExample({super.key}); + + @override + Widget build(BuildContext context) { + final theme = ShadTheme.of(context); + final spacing = theme.spacing; + + return ColoredBox( + color: theme.colorScheme.muted, + child: ShadPadding.symmetric( + horizontal: 6, + vertical: 4, + child: ShadCard( + child: Text( + 'Inset by ${spacing(6)} x ${spacing(4)}', + style: theme.textTheme.small, + ), + ), + ), + ); + } +} +``` + +## Card sections and form actions + +`ShadColumn` with `crossAxisAlignment: stretch` lines up full-width children inside a card, with `ShadSeparator` between groups. The same pattern stacks fields and buttons in a form: put the primary action above an outline cancel. See `example/lib/docs/examples/layout/layout_card_sections.dart` and `layout_form_actions.dart` for the full examples. diff --git a/.cursor/skills/shad-overview/guides/portal.md b/.cursor/skills/shad-overview/guides/portal.md new file mode 100644 index 00000000..9b07d0bc --- /dev/null +++ b/.cursor/skills/shad-overview/guides/portal.md @@ -0,0 +1,99 @@ +# Portal + +The raw overlay primitive underneath popover, select, tooltip, and menus: it renders a follower widget anchored to its child, above everything else. + +`ShadPortal` is a building block rather than an end-user component. Reach for `ShadPopover`, `ShadTooltip`, or the menu components first. Use the portal directly only when you build a new floating surface. + +## Anchoring modes + +`ShadAnchorAuto` flips and shifts the follower to stay on screen, the same anchor mode every built-in floating component uses. + +```dart +import 'dart:async'; + +import 'package:flutter/widgets.dart'; +import 'package:shad/shad.dart'; + +/// Cycles `ShadAnchorAuto` follower anchors on a hover-triggered portal. +class PortalAnchoringExample extends StatefulWidget { + const PortalAnchoringExample({super.key}); + + @override + State createState() => _PortalAnchoringExampleState(); +} + +class _PortalAnchoringExampleState extends State { + var visible = false; + late final Timer timer; + var alignmentIndex = 0; + + static const _alignments = [ + AlignmentGeometry.topStart, + AlignmentGeometry.topCenter, + AlignmentGeometry.topEnd, + AlignmentGeometry.centerStart, + AlignmentGeometry.center, + AlignmentGeometry.centerEnd, + AlignmentGeometry.bottomStart, + AlignmentGeometry.bottomCenter, + AlignmentGeometry.bottomEnd, + ]; + + @override + void initState() { + super.initState(); + timer = Timer.periodic(const Duration(seconds: 1), (_) { + setState(() { + alignmentIndex = (alignmentIndex + 1) % _alignments.length; + }); + }); + } + + @override + void dispose() { + timer.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final theme = ShadTheme.of(context); + final alignment = _alignments[alignmentIndex]; + + return Align( + child: ShadPortal( + anchor: ShadAnchorAuto(followerAnchor: alignment), + visible: visible, + portalBuilder: (context) { + return ShadMouseArea( + groupId: 'portal-demo', + child: ColoredBox( + color: theme.colorScheme.primary, + child: const SizedBox( + width: 200, + height: 200, + child: Center(child: Text('200x200')), + ), + ), + ); + }, + child: ShadMouseArea( + groupId: 'portal-demo', + onEnter: (_) => setState(() => visible = true), + onExit: (_) => setState(() => visible = false), + child: ColoredBox( + color: theme.colorScheme.destructive, + child: const SizedBox( + width: 100, + height: 100, + child: Center(child: Text('100x100')), + ), + ), + ), + ), + ); + } +} +``` + +The two `ShadMouseArea` widgets share a `groupId` so hovering the portal's own content counts as still hovering the trigger, which is what keeps a popover open while the pointer moves from the trigger onto the popover surface. diff --git a/.cursor/skills/shad-overview/guides/responsive.md b/.cursor/skills/shad-overview/guides/responsive.md new file mode 100644 index 00000000..06b95833 --- /dev/null +++ b/.cursor/skills/shad-overview/guides/responsive.md @@ -0,0 +1,50 @@ +# Responsive + +Tailwind's breakpoint scale, carried on the theme: a builder widget and a context extension. + +## Breakpoints + +The theme carries six breakpoints matching Tailwind's scale. Override any of them on `ShadThemeData`: + +```dart +ShadThemeData( + breakpoints: ShadBreakpoints( + tn: 0, // tiny + sm: 640, + md: 768, + lg: 1024, + xl: 1280, + xxl: 1536, + ), +) +``` + +## Reading the current breakpoint + +`ShadResponsiveBuilder` rebuilds with the breakpoint for the current width. `context.breakpoint` reads the same value inline. + +```dart +ShadResponsiveBuilder( + builder: (context, breakpoint) { + final sm = breakpoint >= ShadTheme.of(context).breakpoints.sm; + return sm ? const WideLayout() : const NarrowLayout(); + }, +) +``` + +Comparisons follow Tailwind's semantics. `sm` means "small and up", so test with `>=`. Use `==` only to target exactly one band. + +## Switching on the band + +Each band is its own type on a sealed class, so a `switch` covers the scale exhaustively: + +```dart +return switch (context.breakpoint) { + ShadBreakpointTN() => const Text('Tiny'), + ShadBreakpointSM() => const Text('Small'), + ShadBreakpointMD() => const Text('Medium'), + ShadBreakpointLG() => const Text('Large'), + ShadBreakpointXL() => const Text('Extra large'), + ShadBreakpointXXL() => const Text('Extra extra large'), +}; +``` diff --git a/.cursor/skills/shad-overview/guides/styles.md b/.cursor/skills/shad-overview/guides/styles.md new file mode 100644 index 00000000..e127ce07 --- /dev/null +++ b/.cursor/skills/shad-overview/guides/styles.md @@ -0,0 +1,56 @@ +# Styles + +The eight shadcn styles are complete presets for geometry, typography, and shadows, switchable with one line. + +shadcn/ui ships eight named styles, and they are not color variations. Each one is a full geometry set. A `mira` button is 28px tall with 8px of padding where a `sera` button is 40px with 24px, and inputs, menu rows, cards, sliders, and switches move with them. Styles also retype the UI: each one redefines the title, label, body, caption, and field roles, and reshapes it with its own radii and shadows. Every value was read from the shadcn/ui reference CSS. + +## Choosing a style + +```dart +ShadThemeData( + style: ShadStyleTokens.maia, +) +``` + +- `vega`: the classic. 36px controls, moderate radii, subtle shadows. +- `nova`: the default. Vega, condensed to 32px controls and tighter surfaces. +- `maia`: pill-shaped and roomy, with fully rounded controls. +- `lyra`: square. No radii, flat shadows, 12px body text. +- `mira`: the densest. 28px controls on a relaxed 12px line. +- `luma`: soft. Filled fields, large radii, weighty shadows. +- `sera`: editorial. 40px controls, underlined fields, uppercase labels, generous space. +- `rhea`: gently rounded mid-size controls with soft hairlines. + +Open the example app's theme editor and switch styles to see them live. The whole app follows. + +## Picking at runtime + +The presets are values on `ShadStyleTokens`, so a style switcher is a list and a lookup: + +```dart +ShadStyleTokens.all; // the eight presets +ShadStyleTokens.fromName('sera'); // one, by its shadcn name +``` + +## Custom styles + +A style is data, a set of measured tokens. Derive your own from the preset closest to what you want: + +```dart +ShadThemeData( + style: ShadStyleTokens.nova.copyWith( + buttonRadius: ShadRadiusToken.full, + buttonHeight: 40, + ), +) +``` + +## Spacing + +Metrics the reference writes in Tailwind's spacing units follow the theme's `ShadSpacing` step: 4px, like `0.25rem`. Raising it loosens every unit-derived padding and gap in one move: + +```dart +ShadThemeData( + spacing: const ShadSpacing(step: 5), +) +``` diff --git a/.cursor/skills/shad-overview/guides/theming.md b/.cursor/skills/shad-overview/guides/theming.md new file mode 100644 index 00000000..aca4a6ec --- /dev/null +++ b/.cursor/skills/shad-overview/guides/theming.md @@ -0,0 +1,149 @@ +# Theming + +One `ShadThemeData` drives every component: color scheme, radius, typography, and per-component overrides. + +## Reading the theme + +Every component resolves its appearance from the nearest `ShadTheme`. Your own widgets read it the same way: + +```dart +final theme = ShadTheme.of(context); + +theme.colorScheme.primary; // the palette +theme.textTheme.p; // the type scale +theme.radius; // the base corner radius +theme.style; // the active style preset +``` + +## Color schemes + +Sixteen schemes ship in light and dark: blue, gray, green, mauve, mist, neutral, olive, orange, red, rose, slate (the default), stone, taupe, violet, yellow, and zinc. + +```dart +ShadApp( + theme: ShadThemeData( + colorScheme: const ShadZincColorScheme.light(), + ), + darkTheme: ShadThemeData( + brightness: Brightness.dark, + colorScheme: const ShadZincColorScheme.dark(), + ), +) +``` + +To let users pick a palette at runtime, resolve one by name with `ShadColorScheme.fromName`: + +```dart +final scheme = ShadColorScheme.fromName( + 'rose', + brightness: Brightness.dark, +); +``` + +## Customizing a scheme + +A scheme is a value. Override individual slots where you construct it, and carry your own colors in the `custom` map so they lerp and switch with the rest of the theme. + +```dart +ShadThemeData( + colorScheme: const ShadSlateColorScheme.light( + primary: Color(0xFF16A34A), + custom: { + 'highlight': Color(0xFFFACC15), + }, + ), +) + +// Read it back anywhere: +ShadTheme.of(context).colorScheme.custom['highlight']; +``` + +An extension keeps custom colors as convenient as the built-in ones: + +```dart +extension AppColors on ShadColorScheme { + Color get highlight => custom['highlight']!; +} +``` + +## Component themes + +Every component has a theme slot on `ShadThemeData`: `primaryButtonTheme`, `cardTheme`, `inputTheme`, and so on. Values you pass are merged over what the style derives, so an override touches exactly the property you name: + +```dart +ShadThemeData( + primaryButtonTheme: const ShadButtonTheme( + backgroundColor: Color(0xFF16A34A), + ), +) +``` + +## Radius + +The theme carries one base radius, the `md` step of the scale. Everything else derives from it: at the default 8 the scale runs sm 6, md 8, lg 10, xl 14, 2xl 16, 3xl 24, 4xl 32, and each component picks the step its style prescribes. + +```dart +ShadThemeData( + radius: const BorderRadius.all(Radius.circular(10)), +) +``` + +## Menu appearance + +The shadcn theme editor's menu options are theme settings here too. `menuColorScheme` renders menu surfaces on another palette (pass the dark counterpart for the "Inverted" look), `menuTranslucent` gives them the blurred 70% finish, and `menuAccent` picks how strongly rows highlight. + +```dart +ShadThemeData( + menuColorScheme: const ShadSlateColorScheme.dark(), + menuTranslucent: true, + menuAccent: ShadMenuAccent.bold, +) +``` + +## Focus rings + +Focus is shown as an outward ring, like shadcn/ui. Set `disableSecondaryBorder: true` to switch to the alternative treatment: a 2px border inside the control, which keeps focus visible where an outward ring would be clipped. + +```dart +ShadThemeData( + disableSecondaryBorder: true, +) +``` + +## Interop with Material and Cupertino + +`ShadApp.custom` renders shadcn components inside a `MaterialApp` or `CupertinoApp`, so both widget sets can share one page. Add the localizations delegate for each set you use and wrap `builder` in a `ShadAppBuilder`: + +```dart +import 'package:flutter/material.dart'; +import 'package:shad/shad.dart'; + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return ShadApp.custom( + themeMode: ThemeMode.dark, + darkTheme: ShadThemeData( + brightness: Brightness.dark, + colorScheme: const ShadSlateColorScheme.dark(), + ), + appBuilder: (context) { + return MaterialApp( + theme: Theme.of(context), + localizationsDelegates: const [ + GlobalShadLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ], + builder: (context, child) => ShadAppBuilder(child: child!), + ); + }, + ); + } +} +``` + +`Theme.of(context)` inside `appBuilder` already carries a `ThemeData` derived from the `ShadThemeData` (color scheme, typography, and common widget themes), so Material widgets on the same page pick up matching colors without extra work. Use `CupertinoApp` and `CupertinoTheme.of(context)` the same way for Cupertino. If the app needs the `Router` API, use `MaterialApp.router` or `CupertinoApp.router` inside `appBuilder`. diff --git a/.cursor/skills/shad-overview/guides/typography.md b/.cursor/skills/shad-overview/guides/typography.md new file mode 100644 index 00000000..aa8909e1 --- /dev/null +++ b/.cursor/skills/shad-overview/guides/typography.md @@ -0,0 +1,98 @@ +# Typography + +`ShadThemeData.textTheme` carries the prose scale. Read a role and hand it to a `Text` widget: + +```dart +Text( + 'Taxing Laughter: The Joke Tax Chronicles', + style: ShadTheme.of(context).textTheme.h1, +) +``` + +## Roles + +| Role | Use for | +| --- | --- | +| `h1Large` | The largest heading, for hero-style titles. | +| `h1` | Page titles. | +| `h2` | Section titles. | +| `h3` | Sub-section titles. | +| `h4` | Card and dialog titles. | +| `p` | Body paragraphs. | +| `blockquote` | Quoted text, indented with a left border. | +| `table` | Table cell text. | +| `list` | List item text. | +| `lead` | The introductory paragraph under a page title. | +| `large` | Emphasized body text, for example a dialog's main question. | +| `small` | Field labels and secondary UI text. | +| `muted` | De-emphasized helper text. | + +Every component that shows text (buttons, badges, cards, alerts, menus, and so on) reads its own style from a component theme instead of these roles directly. Those styles derive from the active style preset (see [styles.md](styles.md)) but can be overridden the same way as any other component theme, see [theming.md](theming.md). + +## Custom font family + +By default `shad` uses [Geist](https://vercel.com/font) as the default font family. To change it, add the font to your project, for example under a `/fonts` directory, then declare it in `pubspec.yaml`: + +```yaml +flutter: + fonts: + - family: UbuntuMono + fonts: + - asset: fonts/UbuntuMono-Regular.ttf + - asset: fonts/UbuntuMono-Italic.ttf + style: italic + - asset: fonts/UbuntuMono-Bold.ttf + weight: 700 + - asset: fonts/UbuntuMono-BoldItalic.ttf + weight: 700 + style: italic +``` + +Then pass a `ShadTextTheme` with the new family to `ShadThemeData`: + +```dart +ShadThemeData( + colorScheme: const ShadZincColorScheme.light(), + textTheme: ShadTextTheme( + colorScheme: const ShadZincColorScheme.light(), + family: 'UbuntuMono', + ), +) +``` + +## Google Fonts + +Add the [google_fonts](https://pub.dev/packages/google_fonts) package, then build the text theme from one of its fonts: + +```dart +ShadThemeData( + colorScheme: const ShadZincColorScheme.light(), + textTheme: ShadTextTheme.fromGoogleFont(GoogleFonts.poppins), +) +``` + +## Custom styles + +Add your own named styles with the `custom` parameter, the same pattern `ShadColorScheme` uses for custom colors: + +```dart +ShadThemeData( + textTheme: ShadTextTheme( + custom: { + 'myCustomStyle': const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w400, + color: Colors.blue, + ), + }, + ), +) +``` + +Read it back with `ShadTheme.of(context).textTheme.custom['myCustomStyle']!`, or add an extension to make it a getter: + +```dart +extension CustomStyleExtension on ShadTextTheme { + TextStyle get myCustomStyle => custom['myCustomStyle']!; +} +``` diff --git a/.github/workflows/analyze.yml b/.github/workflows/analyze.yml index c33261e8..7b647d09 100644 --- a/.github/workflows/analyze.yml +++ b/.github/workflows/analyze.yml @@ -66,7 +66,7 @@ jobs: generated: runs-on: ubuntu-latest env: - VERSION: '4.1.0-beta.3' + VERSION: '4.1.0-rc.1' steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index c1c8fcf1..78d1137c 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -29,7 +29,7 @@ jobs: deploy: runs-on: ubuntu-latest env: - VERSION: '4.1.0-beta.3' + VERSION: '4.1.0-rc.1' steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 10a118f9..a1f367b6 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -22,7 +22,7 @@ concurrency: env: FLUTTER_VERSION: '3.47.5' - VERSION: '4.1.0-beta.3' + VERSION: '4.1.0-rc.1' jobs: write-macos: diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 2f39d2e8..e8d7f7a6 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -103,7 +103,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 60 env: - VERSION: '4.1.0-beta.3' + VERSION: '4.1.0-rc.1' # Non-secret: the test database is an embedded PostgreSQL that exists # only for the run (same values as tests.yml). SERVERPOD_PASSWORD_database: '8hI_nurnnhk2QTfnxCOHS_hqtFwFEvsG' diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 3b66cebc..9169d8fd 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -23,7 +23,7 @@ jobs: server: runs-on: ubuntu-latest env: - VERSION: '4.1.0-beta.3' + VERSION: '4.1.0-rc.1' # Non-secret: the test database is an embedded PostgreSQL that exists # only for the run (the test passwords of wonky_server/docker-compose.yaml). SERVERPOD_PASSWORD_database: '8hI_nurnnhk2QTfnxCOHS_hqtFwFEvsG' diff --git a/AGENTS.md b/AGENTS.md index b97a2157..1642f5d6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -22,21 +22,22 @@ Key rules from the specs: every user-visible action is a command and every state | `third_party/` | Vendored, locally patched third-party packages (`specs/01-architecture.md` §2.3) | | `tool/` | Repository scripts (dependency-rule check, web build + bundle sizes, generated-code check, goldens, pseudo-locale) | -Current state: the M0 restructure is done. **Wonky Write is complete for its first iteration** (sign-in, drive, sharing, real-time editor, comments, suggestions, history, themes and theme editor, import/export, offline cache, spellcheck, MCP, AI assistant). **Wonky Sheets is complete for its first iteration** (offline-first sync over RevLog, grid and editing, formulas, formatting and conditional formatting, data tools, validation and protection, pivots, charts, import/export/print, collaboration and history, MCP, AI assistant, scripting with its editor, macros and triggers). Deferred work is in `TODO.md`; the Sheets status per area is in `specs/wonky_sheets/STATUS.md`. **v2 (Drive, Slides, suite changes) is built**: the plans are `specs/wonky_drive/05-implementation-plan.md` and `specs/wonky_slides/12-implementation-plan.md`, the status `specs/wonky_drive/STATUS.md` and `specs/wonky_slides/STATUS.md` (M8 — Drive and the suite changes — is integrated and checked, D-18; the M8–M11 exit checklists are in `specs/14-roadmap.md` §2.1). **Wonky Slides is complete for its first iteration** (M9 SL-23, M10 SL-36, M11 SL-45): home and 10 templates, offline-first decks (RevLog sync), canvas with text, shapes, lines/connectors with route handles, icons, images with live adjustments, tables, element and slide groups, grid and outline views, speaker notes, themes, layouts, masters and the theme editor, clipboard within/across decks and apps, find & replace, builds and transitions incl. 3D and Magic Move, code blocks with highlight steps and type-in, linked Sheets charts incl. 3D, presenting with presenter view and audience/follower on another device, PDF (builds as pages) and PNG export, Layers, accessibility checker, presence, soft locks, live pointers and follow, comments on slides and elements, version history with diffs and restore, overwritten-text review, MCP `slides.*` and `ui.slides.*`, the assistant and AI commands, spelling, special characters and emoji. On hold (user decision): the phone remote, rehearse timings and the same-browser fast path (SL-41). Hidden by `SlidesFeatures` until built: native charts and *Import slides…* (P1), *Make a code walkthrough…*, editor preferences, notification settings, outline Markdown export and snap to grid (P2); the remaining work is in `TODO.md` ("Wonky Slides — later"). **Bugs & improvements (M12, `specs/16-bugs-and-improvements.md`) is built**: the public in-app board replacing *Report a bug* (report dialog with annotated screenshots, votes, Markdown comments, Jev/Gemini vetting on internal keys, admins by email domain, tasks with claims, the `board` MCP scope, `/improvements` routes), including the GitHub App integration (PR gate, AI review, merged/shipped lifecycle), which is enabled by config (`board.github.enabled`, default off); its open items are in spec 16 §17. `serverpod: flutter_apps:` declares `drive` (web-port 8100), `write` (8101), `sheets` (8102) and `slides` (8104), all with `auto_launch: false`: `serverpod start` starts only the server, and an app is started on demand (the user from the `serverpod start` UI, agents with the `spawn_flutter_app` MCP tool). The debug web apps load slowly (thousands of DDC modules); for browser checks build a release with `flutter build web --wasm --base-href /write/ --output ../wonky_server/web/write` in `wonky_write` and open http://localhost:8082/write/ (Sheets: `--base-href /sheets/ --output ../wonky_server/web/sheets` in `wonky_sheets`, http://localhost:8082/sheets/; Drive: `--base-href /drive/ --output ../wonky_server/web/drive` in `wonky_drive`, http://localhost:8082/drive/; Slides: `--base-href /slides/ --output ../wonky_server/web/slides` in `wonky_slides`, http://localhost:8082/slides/). `serverpod run flutter_build` (in `wonky_server`) builds every existing app one after another with `tool/build_web.dart` (it fails when any build fails and prints the bundle sizes; `dart run tool/build_web.dart write` builds one app). Browser checks of the release builds: the dev server injects a live-reload script that reloads the page when `/__dev/version` changes (every server reload, also from other agents' edits — for a long walkthrough, make `fetch('/__dev/version')` reject in the page's console after each load); the agent's Browser pane only paints while it is displayed (hidden: frames lag and animations can't be judged), has no PDF viewer and can't emulate `prefers-reduced-motion`; enabling Flutter semantics (click `flt-semantics-placeholder`) makes `find`/`read_page` usable. +Current state: the M0 restructure is done. **Wonky Write is complete for its first iteration** (sign-in, drive, sharing, real-time editor, comments, suggestions, history, themes and theme editor, import/export, offline cache, spellcheck, MCP, AI assistant). **Wonky Sheets is complete for its first iteration** (offline-first sync over RevLog, grid and editing, formulas, formatting and conditional formatting, data tools, validation and protection, pivots, charts, import/export/print, collaboration and history, MCP, AI assistant, scripting with its editor, macros and triggers). Deferred work is in `TODO.md`; the Sheets status per area is in `specs/wonky_sheets/STATUS.md`. **v2 (Drive, Slides, suite changes) is built**: the plans are `specs/wonky_drive/05-implementation-plan.md` and `specs/wonky_slides/12-implementation-plan.md`, the status `specs/wonky_drive/STATUS.md` and `specs/wonky_slides/STATUS.md` (M8 — Drive and the suite changes — is integrated and checked, D-18; the M8–M11 exit checklists are in `specs/14-roadmap.md` §2.1). **Wonky Slides is complete for its first iteration** (M9 SL-23, M10 SL-36, M11 SL-45): home and 10 templates, offline-first decks (RevLog sync), canvas with text, shapes, lines/connectors with route handles, icons, images with live adjustments, tables, element and slide groups, grid and outline views, speaker notes, themes, layouts, masters and the theme editor, clipboard within/across decks and apps, find & replace, builds and transitions incl. 3D and Magic Move, code blocks with highlight steps and type-in, linked Sheets charts incl. 3D, presenting with presenter view and audience/follower on another device, PDF (builds as pages) and PNG export, Layers, accessibility checker, presence, soft locks, live pointers and follow, comments on slides and elements, version history with diffs and restore, overwritten-text review, MCP `slides.*` and `ui.slides.*`, the assistant and AI commands, spelling, special characters and emoji. On hold (user decision): the phone remote, rehearse timings and the same-browser fast path (SL-41). Hidden by `SlidesFeatures` until built: native charts and *Import slides…* (P1), *Make a code walkthrough…*, editor preferences, notification settings, outline Markdown export and snap to grid (P2); the remaining work is in `TODO.md` ("Wonky Slides — later"). **Bugs & improvements (M12, `specs/16-bugs-and-improvements.md`) is built**: the public in-app board replacing *Report a bug* (report dialog with annotated screenshots, votes, Markdown comments, Jev/Gemini vetting on internal keys, admins by email domain, tasks with claims, the `board` MCP scope, `/improvements` routes), including the GitHub App integration (PR gate, AI review, merged/shipped lifecycle), which is enabled by config (`board.github.enabled`, default off); its open items are in spec 16 §17. `serverpod: flutter_apps:` declares `drive` (web-port 8100), `write` (8101), `sheets` (8102) and `slides` (8104), all with `auto_launch: false`: the Serverpod runner starts only the server, and an app is started on demand (the user from the `serverpod start` UI, agents with the `spawn_flutter_app` MCP tool). The debug web apps load slowly (thousands of DDC modules); for browser checks build a release with `flutter build web --wasm --base-href /write/ --output ../wonky_server/web/write` in `wonky_write` and open http://localhost:8082/write/ (Sheets: `--base-href /sheets/ --output ../wonky_server/web/sheets` in `wonky_sheets`, http://localhost:8082/sheets/; Drive: `--base-href /drive/ --output ../wonky_server/web/drive` in `wonky_drive`, http://localhost:8082/drive/; Slides: `--base-href /slides/ --output ../wonky_server/web/slides` in `wonky_slides`, http://localhost:8082/slides/). `serverpod run flutter_build` (in `wonky_server`) builds every existing app one after another with `tool/build_web.dart` (it fails when any build fails and prints the bundle sizes; `dart run tool/build_web.dart write` builds one app). Browser checks of the release builds: the dev server injects a live-reload script that reloads the page when `/__dev/version` changes (every server reload, also from other agents' edits — for a long walkthrough, make `fetch('/__dev/version')` reject in the page's console after each load); the agent's Browser pane only paints while it is displayed (hidden: frames lag and animations can't be judged), has no PDF viewer and can't emulate `prefers-reduced-motion`; enabling Flutter semantics (click `flt-semantics-placeholder`) makes `find`/`read_page` usable. ## Serverpod workflow -The user starts the server with `serverpod start` (the Flutter apps are not launched automatically). There is no need to check if the server is running: make the changes and call the `serverpod` MCP tools as needed. If the server is not running, an informative error message will be received from the MCP server. Then STOP and ask the user to start it. NEVER start the server yourself. Start the Flutter app you need with the MCP tool `spawn_flutter_app` (check first with `get_flutter_app_dtd` whether it is already running). +The server and the Flutter apps run under the Serverpod runner, a background process for `wonky_server`. The user attaches to it with `serverpod start`. There is no need to check if the server is running: make the changes and call the `serverpod` MCP tools as needed. If the server is not running, an informative error message will be received from the MCP server. Then call `tail_runner_logs` to learn whether it is still starting or why it stopped. If nothing is running, start it with `serverpod runner start` in `wonky_server` (or `serverpod runner start -d wonky_server`), which returns once the server is up and leaves it running for the user to attach to; a cold start compiles the whole project and can take minutes, so run it in the background. If it fails, fix the cause it reports (a build error, a port held by another process, Docker) instead of retrying; only a `serverpod runner start` your shell killed on a timeout is run again, since the runner keeps starting. Leave the runner running, and run `serverpod runner stop` only when asked. The Flutter apps are not launched automatically (`auto_launch: false`): start the one you need with the MCP tool `spawn_flutter_app` (check first with `get_flutter_app_dtd` whether it is already running). -While running, `serverpod start` watches for file changes to run incremental code generation and hot reload both the server and the Flutter apps. +While running, the runner watches for file changes to run incremental code generation and hot reload both the server and the Flutter apps. `serverpod runner status` prints its state and the server addresses, which are not always the configured ports when another Serverpod runner on the machine holds them. Calling `serverpod generate` directly is not needed, but might be useful to troubleshoot when an incremental generation fails. -ALWAYS use the MCP server instead of the command line. Use the MCP server to: +ALWAYS use the MCP server instead of the command line for the tasks below. The `serverpod runner` commands are the exception: they manage the stack itself. Use the MCP server to: - `create_migration` and `apply_migrations` for database (after you change data models). - `create_repair_migration` if the database has drifted out of sync with the migrations. - `tail_server_logs` to read logs from the server. +- `tail_runner_logs` to read the output of the runner itself (code generation, compilation, Docker, raw server output). The only tool that also works while the server is starting or after it stopped. - `tail_flutter_logs` to read the raw stdout/stderr of the Flutter app. - `hot_reload` / `hot_restart` to reload or restart the server and the Flutter app. ALWAYS call `hot_restart` after doing changes in the Flutter app that may not work with normal hot reload (which is automatically applied). - `spawn_flutter_app` to start a Flutter app declared under `serverpod: flutter_apps:` in the server `pubspec.yaml`. @@ -46,10 +47,10 @@ NEVER edit generated code. The server's `wonky_server/lib/src/generated/` direct Migrations are a narrow exception: the `migration.sql` of a generated migration MAY be edited by hand when the generated SQL would lose data — to add a data transformation, or to reach a destructive change through non-destructive steps. Never touch the other files in the migration directory, and keep the schema the SQL ends up with identical to `definition.sql` — new databases are created from that file and never run `migration.sql`. -Only when the server cannot be started at all, fall back to the CLI in the server package: +Only when the MCP is not available or `serverpod runner start` cannot bring the server up, fall back to the CLI in the server package: - `serverpod generate` to regenerate the client and the generated server code. -- `serverpod create-migration` after changing a model with a `table` (add `--force` for destructive changes). It only writes the migration; `serverpod start` applies pending migrations when it boots the server. +- `serverpod create-migration` after changing a model with a `table` (add `--force` for destructive changes). It only writes the migration; the runner applies pending migrations when it boots the server. How to write tests (layout, naming, helpers, determinism, tags) is in `specs/15-writing-tests.md`; read it before writing tests. Tests need no Docker. `config/test.yaml` sets `database.dataPath`, so Serverpod starts and manages the test database (an embedded PostgreSQL) itself, and the project's `docker-compose.yaml` is not used for it. Just run `dart test` in the server package. Pure-Dart packages run `dart test` and Flutter packages and apps run `flutter test` in their own folder. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fb386847..ab15fcf9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -36,10 +36,10 @@ Flutter, Dart and what Serverpod needs on your machine. You also need: ## 2. Install the Serverpod CLI ```bash -dart install serverpod_cli 4.1.0-beta.3 +dart install serverpod_cli 4.1.0-rc.1 ``` -This installs the CLI, or updates an existing install to the latest beta, which Wonky Work +This installs the CLI, or updates an existing install to the release candidate Wonky Work needs (the version is `VERSION` in `.github/workflows/tests.yml`). `dart install` puts the executable in Dart's install directory; add it to your `PATH` if `serverpod` is not found. diff --git a/packages/wonky_app/lib/src/board/pages/contribute_page.dart b/packages/wonky_app/lib/src/board/pages/contribute_page.dart index 6ab64401..75e0317f 100644 --- a/packages/wonky_app/lib/src/board/pages/contribute_page.dart +++ b/packages/wonky_app/lib/src/board/pages/contribute_page.dart @@ -17,7 +17,7 @@ String boardRepository(ShellServices s) => /// The Serverpod CLI version of `CONTRIBUTING.md` step 2 (the `VERSION` in /// `.github/workflows/tests.yml`). -const boardServerpodCliVersion = '4.1.0-beta.3'; +const boardServerpodCliVersion = '4.1.0-rc.1'; /// Serverpod's installation guide (Flutter, Dart and the Serverpod CLI): /// the first setup step of `CONTRIBUTING.md` (specs/16 §10.3). diff --git a/packages/wonky_app/pubspec.yaml b/packages/wonky_app/pubspec.yaml index bcf59b1c..7c22f587 100644 --- a/packages/wonky_app/pubspec.yaml +++ b/packages/wonky_app/pubspec.yaml @@ -26,9 +26,9 @@ dependencies: path: ^1.9.1 path_provider: ^2.1.6 provider: ^6.1.5+1 - serverpod_auth_idp_client: 4.1.0-beta.3 - serverpod_auth_idp_flutter: 4.1.0-beta.3 - serverpod_flutter: 4.1.0-beta.3 + serverpod_auth_idp_client: 4.1.0-rc.1 + serverpod_auth_idp_flutter: 4.1.0-rc.1 + serverpod_flutter: 4.1.0-rc.1 shad: ^0.99.4 shared_preferences: ^2.5.6 sqlite_async: ^0.14.5 diff --git a/packages/wonky_app/test/board/pages/task_page_test.dart b/packages/wonky_app/test/board/pages/task_page_test.dart index 4cd2040b..a7fe8b9d 100644 --- a/packages/wonky_app/test/board/pages/task_page_test.dart +++ b/packages/wonky_app/test/board/pages/task_page_test.dart @@ -196,7 +196,7 @@ void main() { expect(find.text('Set up your development environment'), findsOneWidget); for (final line in const [ - 'dart install serverpod_cli 4.1.0-beta.3', + 'dart install serverpod_cli 4.1.0-rc.1', 'serverpod start', ]) { expect( @@ -212,10 +212,7 @@ void main() { await tester.pumpAndSettle(); expect(t().location, '/improvements/contribute'); - expect( - find.text('dart install serverpod_cli 4.1.0-beta.3'), - findsOneWidget, - ); + expect(find.text('dart install serverpod_cli 4.1.0-rc.1'), findsOneWidget); expect(byKey('board.crumb.contribute'), findsOneWidget); await settleTooltips(tester); }); diff --git a/pubspec.lock b/pubspec.lock index 59115b14..12a56d88 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1516,10 +1516,10 @@ packages: dependency: transitive description: name: serverpod - sha256: b80a3afead911feeb5984101c377ceafa7be0dd4f8223fd989e2179f84ffb9cf + sha256: "93d3d33f495f0da3c0c3fc3008ec16faf165ed3dd7779e48cbebc8b04b6a5fa0" url: "https://pub.dev" source: hosted - version: "4.1.0-beta.3" + version: "4.1.0-rc.1" serverpod_argon2: dependency: transitive description: @@ -1532,114 +1532,114 @@ packages: dependency: transitive description: name: serverpod_auth_core_client - sha256: "7134c42b8ae0a36397d0e832f8e2679221eb2fe66c3b8435b3c1e9beb19440c6" + sha256: "4a7953c35e31eb5575464f0ed4be0ab279e4219d68f80e11f0d80f0331c0472a" url: "https://pub.dev" source: hosted - version: "4.1.0-beta.3" + version: "4.1.0-rc.1" serverpod_auth_core_flutter: dependency: transitive description: name: serverpod_auth_core_flutter - sha256: "106e500af363a9077842cf93cdf7ec644acccaeb1cc2b0bcfe54bd16b6dac5eb" + sha256: "47c444f82fb6285302ad2f311dc5558c3f86fe9feb4d8f672885cfe9ecfff42b" url: "https://pub.dev" source: hosted - version: "4.1.0-beta.3" + version: "4.1.0-rc.1" serverpod_auth_core_server: dependency: transitive description: name: serverpod_auth_core_server - sha256: "4d7a7c56e7349d319bf986e48393273a19a3d9ab85d3aeef2e3476df57fe06d8" + sha256: efdf619b1cff55185f541dbeb620e88cf80a863d8c5c81a765706cd536354043 url: "https://pub.dev" source: hosted - version: "4.1.0-beta.3" + version: "4.1.0-rc.1" serverpod_auth_idp_client: dependency: transitive description: name: serverpod_auth_idp_client - sha256: ddd77e09d92f647979f566aeb376fdeafc9618a9eb67f21dc4ef9b37eca3789a + sha256: "61e8a3838d03085d7d7d55f8e4aa0c756f991853f5bb8ed4a2822e3190be5599" url: "https://pub.dev" source: hosted - version: "4.1.0-beta.3" + version: "4.1.0-rc.1" serverpod_auth_idp_flutter: dependency: transitive description: name: serverpod_auth_idp_flutter - sha256: e2ffab542688ba9762c128ac871aa22a9c0f8ebbc1d6cef76b225b023cb392fa + sha256: "047130f725439ef7af0985a46c36c05f75e6ebc71f784cd1ec12126b8500c79c" url: "https://pub.dev" source: hosted - version: "4.1.0-beta.3" + version: "4.1.0-rc.1" serverpod_auth_idp_server: dependency: transitive description: name: serverpod_auth_idp_server - sha256: "44aed05da7051ecebd7a5899301d4e7e6886db632fc596010d1de753800eb460" + sha256: "487f157684abd9add141c629b49c139b206320f924d2516cd9321332c1f66959" url: "https://pub.dev" source: hosted - version: "4.1.0-beta.3" + version: "4.1.0-rc.1" serverpod_client: dependency: transitive description: name: serverpod_client - sha256: "7ef8695dc9af88e50dab305d6741ad2f7539fdb6fd1d87fb7a3b05617d84a175" + sha256: "1d3baf251a9dbe4e8de64eb14c07838a392c4d9459b695f3f7b3651d8a52f2f4" url: "https://pub.dev" source: hosted - version: "4.1.0-beta.3" + version: "4.1.0-rc.1" serverpod_cloud_storage: dependency: transitive description: name: serverpod_cloud_storage - sha256: b0cb2fda64e42130585dfa3cf15ca4842e763104bbc5f2cf6dfc8a82524beb62 + sha256: be0661ba295e6c1703e8114fe74774516f33279334252d73791e8e8d3895b8a4 url: "https://pub.dev" source: hosted - version: "4.1.0-beta.3" + version: "4.1.0-rc.1" serverpod_cloud_storage_gcp: dependency: transitive description: name: serverpod_cloud_storage_gcp - sha256: ef7bf019ed63691ffae134ad623702a8d6ffc79e389e4a0153ba4ba904fb60ee + sha256: "0d8be351e40cdc54e8a02be75b272044dfc83232529ffb94323a6cfaa2605b98" url: "https://pub.dev" source: hosted - version: "4.1.0-beta.3" + version: "4.1.0-rc.1" serverpod_cloud_storage_s3_compat: dependency: transitive description: name: serverpod_cloud_storage_s3_compat - sha256: afbeb1eb88c2269d4798d9ecfa573aa8a2939a0f31fa19be8c1f5122aafd6657 + sha256: e5be0ee265d2842ee2b5c58cf1a267c5a5096996e59507fe54e965b07929a60a url: "https://pub.dev" source: hosted - version: "4.1.0-beta.3" + version: "4.1.0-rc.1" serverpod_database: dependency: transitive description: name: serverpod_database - sha256: c69222ddbd4278618a4ee7139dc058c87327a79d93a02566c92113f6ed8b033c + sha256: a12bb6cc3b4d966da817d9c5e94267af6cd49201bba65be472085819736b4b26 url: "https://pub.dev" source: hosted - version: "4.1.0-beta.3" + version: "4.1.0-rc.1" serverpod_embedded_postgres: dependency: transitive description: name: serverpod_embedded_postgres - sha256: "7c2fce94d64d1fb2548198152484f43e17200a909fb7a0d58ec2a7ce784f76ee" + sha256: b6999c4617d6210f829ae86b97113fbb5072c863f2c9b8e295ba76afc1a342e3 url: "https://pub.dev" source: hosted - version: "4.1.0-beta.3" + version: "4.1.0-rc.1" serverpod_flutter: dependency: transitive description: name: serverpod_flutter - sha256: "185343041b538cf7b518c99e8d92736c991d6516a70e3ce0853a7f118001e651" + sha256: b071579ffaa968786e89318c5c7e1465ccd8c162231eb2382acf3641dddf6d26 url: "https://pub.dev" source: hosted - version: "4.1.0-beta.3" + version: "4.1.0-rc.1" serverpod_lints: dependency: transitive description: name: serverpod_lints - sha256: "255ae29c6bb16f42a7bbe46e02dd230fd55c75ba8bb9f5d24ef2246ccb925c62" + sha256: "7a580e2cc229f3ccf279b9dec7ff602a0c38a1b61590a671d527c27920d7b9d1" url: "https://pub.dev" source: hosted - version: "4.1.0-beta.3" + version: "4.1.0-rc.1" serverpod_logging: dependency: transitive description: @@ -1652,26 +1652,26 @@ packages: dependency: transitive description: name: serverpod_serialization - sha256: "0351adbe9cba0339c76f203bf834bc23fd45724efa9c1266a4d9bf0b872a6ad2" + sha256: "76ff94a2e82dab9edbd85af48cb98bee986d9c5cd1e524385afa68162fda71b8" url: "https://pub.dev" source: hosted - version: "4.1.0-beta.3" + version: "4.1.0-rc.1" serverpod_shared: dependency: transitive description: name: serverpod_shared - sha256: "230ad1f9a36bb4583a222a8c8035af2aa5a46d06955e042c95dd0cb00d581636" + sha256: "61a2bf16f6901f37b6324d0f008b1b8bb9fcf8102d4c8696f79b3bbecfe532f4" url: "https://pub.dev" source: hosted - version: "4.1.0-beta.3" + version: "4.1.0-rc.1" serverpod_test: dependency: transitive description: name: serverpod_test - sha256: "9967ab1e0aca491e487c14767ca79e5a75e9c88d085a87cbef6152d16985290c" + sha256: a37b416c22d95ba364c7943c9e32397dc9827b4d427c64b91c80d4506e867972 url: "https://pub.dev" source: hosted - version: "4.1.0-beta.3" + version: "4.1.0-rc.1" shad: dependency: transitive description: diff --git a/wonky_client/pubspec.yaml b/wonky_client/pubspec.yaml index f929c8f2..a2bbda16 100644 --- a/wonky_client/pubspec.yaml +++ b/wonky_client/pubspec.yaml @@ -7,6 +7,6 @@ environment: resolution: workspace dependencies: - serverpod_auth_idp_client: 4.1.0-beta.3 - serverpod_client: 4.1.0-beta.3 - serverpod_database: 4.1.0-beta.3 + serverpod_auth_idp_client: 4.1.0-rc.1 + serverpod_client: 4.1.0-rc.1 + serverpod_database: 4.1.0-rc.1 diff --git a/wonky_server/migrations/20261025085716775-serverpod-4-1-rc1/definition.json b/wonky_server/migrations/20261025085716775-serverpod-4-1-rc1/definition.json new file mode 100644 index 00000000..39d3b609 --- /dev/null +++ b/wonky_server/migrations/20261025085716775-serverpod-4-1-rc1/definition.json @@ -0,0 +1,20250 @@ +{ + "__className__": "serverpod.DatabaseDefinition", + "schemaVersion": 2, + "moduleName": "wonky", + "tables": [ + { + "__className__": "serverpod.TableDefinition", + "name": "access_request", + "dartName": "AccessRequest", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "itemId", + "fieldName": "itemId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "requesterId", + "fieldName": "requesterId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "requestedRole", + "fieldName": "requestedRole", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:AccessRole" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "message", + "fieldName": "message", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "handledAt", + "fieldName": "handledAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "handledById", + "fieldName": "handledById", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "outcome", + "fieldName": "outcome", + "columnType": 0, + "isNullable": true, + "dartType": "protocol:AccessRequestOutcome?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "access_request_fk_0", + "columns": [ + "itemId" + ], + "referenceTable": "drive_item", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "access_request_fk_1", + "columns": [ + "requesterId" + ], + "referenceTable": "user_profile", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "access_request_item_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "itemId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "handledAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "activity_event", + "dartName": "ActivityEvent", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "itemId", + "fieldName": "itemId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "actorId", + "fieldName": "actorId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "actorKind", + "fieldName": "actorKind", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:ActorKind" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "type", + "fieldName": "type", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:ActivityType" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "summary", + "fieldName": "summary", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "at", + "fieldName": "at", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "activity_event_fk_0", + "columns": [ + "itemId" + ], + "referenceTable": "drive_item", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "activity_item_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "itemId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "at" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "activity_type_at_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "type" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "at" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "ai_function_cache", + "dartName": "AiFunctionCache", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "key", + "fieldName": "key", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "spreadsheetUuid", + "fieldName": "spreadsheetUuid", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "model", + "fieldName": "model", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "value", + "fieldName": "value", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "error", + "fieldName": "error", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "hits", + "fieldName": "hits", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "ai_function_cache_key_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "key" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "ai_function_cache_sheet_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "spreadsheetUuid" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "ai_preview_record", + "dartName": "AiPreviewRecord", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "previewId", + "fieldName": "previewId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "fieldName": "userId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileUuid", + "fieldName": "fileUuid", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "previewJson", + "fieldName": "previewJson", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "changesJson", + "fieldName": "changesJson", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "expiresAt", + "fieldName": "expiresAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "ai_preview_id_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "previewId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "ai_preview_expires_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "expiresAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "ai_usage", + "dartName": "AiUsage", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "fieldName": "userId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "model", + "fieldName": "model", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "feature", + "fieldName": "feature", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "inputTokens", + "fieldName": "inputTokens", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "outputTokens", + "fieldName": "outputTokens", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "cacheReadTokens", + "fieldName": "cacheReadTokens", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "cacheWriteTokens", + "fieldName": "cacheWriteTokens", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "at", + "fieldName": "at", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "ai_usage_fk_0", + "columns": [ + "userId" + ], + "referenceTable": "user_profile", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "ai_usage_user_at_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "at" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "ai_usage_at_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "at" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "ai_user_key", + "dartName": "AiUserKey", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "fieldName": "userId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "provider", + "fieldName": "provider", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:AiProvider" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "apiKey", + "fieldName": "apiKey", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "updatedAt", + "fieldName": "updatedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "ai_user_key_fk_0", + "columns": [ + "userId" + ], + "referenceTable": "user_profile", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "ai_user_key_user_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "ai_user_setting", + "dartName": "AiUserSetting", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "fieldName": "userId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "quality", + "fieldName": "quality", + "columnType": 0, + "isNullable": false, + "columnDefault": "'best'", + "dartType": "protocol:AiQuality" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "updatedAt", + "fieldName": "updatedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "ai_user_setting_fk_0", + "columns": [ + "userId" + ], + "referenceTable": "user_profile", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "ai_user_setting_user_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "assistant_message", + "dartName": "AssistantMessage", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "threadId", + "fieldName": "threadId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "role", + "fieldName": "role", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "contentJson", + "fieldName": "contentJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "displayText", + "fieldName": "displayText", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "toolCallsJson", + "fieldName": "toolCallsJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "visible", + "fieldName": "visible", + "columnType": 1, + "isNullable": false, + "columnDefault": "true", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "replay", + "fieldName": "replay", + "columnType": 1, + "isNullable": false, + "columnDefault": "true", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "runId", + "fieldName": "runId", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "stopReason", + "fieldName": "stopReason", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "assistant_message_fk_0", + "columns": [ + "threadId" + ], + "referenceTable": "assistant_thread", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "assistant_message_thread_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "threadId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "assistant_thread", + "dartName": "AssistantThread", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "fieldName": "userId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "itemId", + "fieldName": "itemId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "title", + "fieldName": "title", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "contextRevision", + "fieldName": "contextRevision", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "contextMarkdown", + "fieldName": "contextMarkdown", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "updatedAt", + "fieldName": "updatedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "assistant_thread_fk_0", + "columns": [ + "userId" + ], + "referenceTable": "user_profile", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "assistant_thread_fk_1", + "columns": [ + "itemId" + ], + "referenceTable": "drive_item", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "assistant_thread_user_item_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "itemId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "updatedAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "banding", + "dartName": "Banding", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "random_v7", + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileId", + "fieldName": "fileId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revision", + "fieldName": "revision", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sheetId", + "fieldName": "sheetId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "rangeJson", + "fieldName": "rangeJson", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "styleJson", + "fieldName": "styleJson", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "banding_fk_0", + "columns": [ + "fileId" + ], + "referenceTable": "rev_file", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "banding_fk_1", + "columns": [ + "sheetId" + ], + "referenceTable": "sheet", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4, + "deferrable": "initiallyDeferred" + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "banding_file_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "fileId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "basic_filter", + "dartName": "BasicFilter", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "random_v7", + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileId", + "fieldName": "fileId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revision", + "fieldName": "revision", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sheetId", + "fieldName": "sheetId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "rangeJson", + "fieldName": "rangeJson", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "criteriaJson", + "fieldName": "criteriaJson", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sortJson", + "fieldName": "sortJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "basic_filter_fk_0", + "columns": [ + "fileId" + ], + "referenceTable": "rev_file", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "basic_filter_fk_1", + "columns": [ + "sheetId" + ], + "referenceTable": "sheet", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4, + "deferrable": "initiallyDeferred" + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "basic_filter_file_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "fileId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "board_ai_usage", + "dartName": "BoardAiUsage", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "day", + "fieldName": "day", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "provider", + "fieldName": "provider", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "model", + "fieldName": "model", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "feature", + "fieldName": "feature", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "requests", + "fieldName": "requests", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "inputTokens", + "fieldName": "inputTokens", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "outputTokens", + "fieldName": "outputTokens", + "columnType": 6, + "isNullable": false, + "dartType": "int" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "board_ai_usage_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "day" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "provider" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "model" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "feature" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "board_bundle_suggestion", + "dartName": "BoardBundleSuggestion", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "title", + "fieldName": "title", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "rationale", + "fieldName": "rationale", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "area", + "fieldName": "area", + "columnType": 0, + "isNullable": true, + "dartType": "protocol:BoardArea?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "postIds", + "fieldName": "postIds", + "columnType": 8, + "isNullable": false, + "dartType": "List" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "state", + "fieldName": "state", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [], + "indexes": [], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "board_comment", + "dartName": "BoardComment", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "uuid", + "fieldName": "uuid", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "postId", + "fieldName": "postId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "taskId", + "fieldName": "taskId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "authorId", + "fieldName": "authorId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "body", + "fieldName": "body", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "systemEvent", + "fieldName": "systemEvent", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "moderation", + "fieldName": "moderation", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:BoardModeration" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "moderationReason", + "fieldName": "moderationReason", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "needsCheck", + "fieldName": "needsCheck", + "columnType": 1, + "isNullable": false, + "columnDefault": "false", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "editedAt", + "fieldName": "editedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "deletedAt", + "fieldName": "deletedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "removedAt", + "fieldName": "removedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "board_comment_fk_0", + "columns": [ + "postId" + ], + "referenceTable": "board_post", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "board_comment_fk_1", + "columns": [ + "taskId" + ], + "referenceTable": "board_task", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "board_comment_fk_2", + "columns": [ + "authorId" + ], + "referenceTable": "user_profile", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 0 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "board_comment_uuid_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "uuid" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "board_comment_post_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "postId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "createdAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "board_comment_task_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "taskId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "createdAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "board_flag", + "dartName": "BoardFlag", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "postId", + "fieldName": "postId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "commentId", + "fieldName": "commentId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "fieldName": "userId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "reason", + "fieldName": "reason", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:BoardFlagReason" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "note", + "fieldName": "note", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "board_flag_fk_0", + "columns": [ + "postId" + ], + "referenceTable": "board_post", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "board_flag_fk_1", + "columns": [ + "commentId" + ], + "referenceTable": "board_comment", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "board_flag_fk_2", + "columns": [ + "userId" + ], + "referenceTable": "user_profile", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "board_flag_post_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "postId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "board_flag_comment_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "commentId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "board_github_delivery", + "dartName": "BoardGithubDelivery", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "deliveryId", + "fieldName": "deliveryId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "event", + "fieldName": "event", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "receivedAt", + "fieldName": "receivedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "payload", + "fieldName": "payload", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "processedAt", + "fieldName": "processedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "attempts", + "fieldName": "attempts", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "board_github_delivery_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "deliveryId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "board_moderation_log", + "dartName": "BoardModerationLog", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "adminId", + "fieldName": "adminId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "action", + "fieldName": "action", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "targetKind", + "fieldName": "targetKind", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:BoardTargetKind" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "targetId", + "fieldName": "targetId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "beforeJson", + "fieldName": "beforeJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "afterJson", + "fieldName": "afterJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "reason", + "fieldName": "reason", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "board_moderation_log_target_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "targetKind" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "targetId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "board_post", + "dartName": "BoardPost", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "authorId", + "fieldName": "authorId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "kind", + "fieldName": "kind", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:BoardPostKind" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "reportedKind", + "fieldName": "reportedKind", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:BoardPostKind" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "area", + "fieldName": "area", + "columnType": 0, + "isNullable": true, + "dartType": "protocol:BoardArea?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "size", + "fieldName": "size", + "columnType": 0, + "isNullable": true, + "dartType": "protocol:BoardSize?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "aiLabels", + "fieldName": "aiLabels", + "columnType": 1, + "isNullable": false, + "columnDefault": "true", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "title", + "fieldName": "title", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "body", + "fieldName": "body", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "contextJson", + "fieldName": "contextJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "appHint", + "fieldName": "appHint", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "screenshotFileId", + "fieldName": "screenshotFileId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "status", + "fieldName": "status", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:BoardPostStatus" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "moderation", + "fieldName": "moderation", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:BoardModeration" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "moderationReason", + "fieldName": "moderationReason", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "needsCheck", + "fieldName": "needsCheck", + "columnType": 1, + "isNullable": false, + "columnDefault": "false", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "editedByModerator", + "fieldName": "editedByModerator", + "columnType": 1, + "isNullable": false, + "columnDefault": "false", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "triageState", + "fieldName": "triageState", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:BoardTriageState" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "triageJson", + "fieldName": "triageJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "triageAttempts", + "fieldName": "triageAttempts", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "suggestedTitle", + "fieldName": "suggestedTitle", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "summary", + "fieldName": "summary", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "duplicateOfId", + "fieldName": "duplicateOfId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "possibleDuplicateOfId", + "fieldName": "possibleDuplicateOfId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "duplicateHintDismissed", + "fieldName": "duplicateHintDismissed", + "columnType": 1, + "isNullable": false, + "columnDefault": "false", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "declineReason", + "fieldName": "declineReason", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "taskId", + "fieldName": "taskId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "score", + "fieldName": "score", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "voteCount", + "fieldName": "voteCount", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "commentCount", + "fieldName": "commentCount", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "flagCount", + "fieldName": "flagCount", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "editedAt", + "fieldName": "editedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "deletedAt", + "fieldName": "deletedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "removedAt", + "fieldName": "removedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "board_post_fk_0", + "columns": [ + "authorId" + ], + "referenceTable": "user_profile", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 0 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "board_post_fk_1", + "columns": [ + "screenshotFileId" + ], + "referenceTable": "stored_file", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 0 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "board_post_fk_2", + "columns": [ + "taskId" + ], + "referenceTable": "board_task", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 0 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "board_post_list_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "moderation" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "kind" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "status" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "score" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "board_post_author_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "authorId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "board_post_task_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "taskId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "board_pull_request", + "dartName": "BoardPullRequest", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "prNumber", + "fieldName": "prNumber", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "taskId", + "fieldName": "taskId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "authorLogin", + "fieldName": "authorLogin", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "headSha", + "fieldName": "headSha", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "state", + "fieldName": "state", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "gateState", + "fieldName": "gateState", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "gateReason", + "fieldName": "gateReason", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "gateFirstFailedAt", + "fieldName": "gateFirstFailedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "gateCommentId", + "fieldName": "gateCommentId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "reviewCommentId", + "fieldName": "reviewCommentId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lastReviewSha", + "fieldName": "lastReviewSha", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lastReviewJson", + "fieldName": "lastReviewJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lastReviewAt", + "fieldName": "lastReviewAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "reviewDay", + "fieldName": "reviewDay", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "reviewCount", + "fieldName": "reviewCount", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "mergeCommitSha", + "fieldName": "mergeCommitSha", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "reviewPendingSha", + "fieldName": "reviewPendingSha", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "reviewDueAt", + "fieldName": "reviewDueAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "updatedAt", + "fieldName": "updatedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "board_pull_request_fk_0", + "columns": [ + "taskId" + ], + "referenceTable": "board_task", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 0 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "board_pull_request_number_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "prNumber" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "board_task", + "dartName": "BoardTask", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "title", + "fieldName": "title", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "area", + "fieldName": "area", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:BoardArea" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "size", + "fieldName": "size", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:BoardSize" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "status", + "fieldName": "status", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:BoardTaskStatus" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "promptBody", + "fieldName": "promptBody", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "promptGeneratedAt", + "fieldName": "promptGeneratedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "promptRequestedAt", + "fieldName": "promptRequestedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "promptNote", + "fieldName": "promptNote", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "promptExcludedCommentIds", + "fieldName": "promptExcludedCommentIds", + "columnType": 8, + "isNullable": true, + "dartType": "List?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "promptEditedById", + "fieldName": "promptEditedById", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "promptEditedAt", + "fieldName": "promptEditedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdById", + "fieldName": "createdById", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "publishedAt", + "fieldName": "publishedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "claimedById", + "fieldName": "claimedById", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "claimedAt", + "fieldName": "claimedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "claimExpiresAt", + "fieldName": "claimExpiresAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "claimWarnedAt", + "fieldName": "claimWarnedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "claimCode", + "fieldName": "claimCode", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "pullRequestUrl", + "fieldName": "pullRequestUrl", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "prNumber", + "fieldName": "prNumber", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "prAuthorLogin", + "fieldName": "prAuthorLogin", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "mergeCommitSha", + "fieldName": "mergeCommitSha", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "doneAt", + "fieldName": "doneAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "shippedDeploySha", + "fieldName": "shippedDeploySha", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "shippedAt", + "fieldName": "shippedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "board_task_fk_0", + "columns": [ + "claimedById" + ], + "referenceTable": "user_profile", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 0 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "board_task_status_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "status" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "board_task_claim_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "claimedById" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "board_user_profile", + "dartName": "BoardUserProfile", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "fieldName": "userId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "githubLogin", + "fieldName": "githubLogin", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "blockedAt", + "fieldName": "blockedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "blockedUntil", + "fieldName": "blockedUntil", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "blockedReason", + "fieldName": "blockedReason", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "blockedById", + "fieldName": "blockedById", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "board_user_profile_fk_0", + "columns": [ + "userId" + ], + "referenceTable": "user_profile", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "board_user_profile_user_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "board_vote", + "dartName": "BoardVote", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "postId", + "fieldName": "postId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "fieldName": "userId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "value", + "fieldName": "value", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "board_vote_fk_0", + "columns": [ + "postId" + ], + "referenceTable": "board_post", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "board_vote_fk_1", + "columns": [ + "userId" + ], + "referenceTable": "user_profile", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "board_vote_unique_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "postId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "board_vote_recent_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "postId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "createdAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "cell", + "dartName": "Cell", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "random_v7", + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileId", + "fieldName": "fileId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revision", + "fieldName": "revision", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sheetId", + "fieldName": "sheetId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "rowId", + "fieldName": "rowId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "columnId", + "fieldName": "columnId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "content", + "fieldName": "content", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "note", + "fieldName": "note", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "numberFormat", + "fieldName": "numberFormat", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fontFamily", + "fieldName": "fontFamily", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fontSize", + "fieldName": "fontSize", + "columnType": 3, + "isNullable": true, + "dartType": "double?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "bold", + "fieldName": "bold", + "columnType": 1, + "isNullable": true, + "dartType": "bool?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "italic", + "fieldName": "italic", + "columnType": 1, + "isNullable": true, + "dartType": "bool?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "underline", + "fieldName": "underline", + "columnType": 1, + "isNullable": true, + "dartType": "bool?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "strikethrough", + "fieldName": "strikethrough", + "columnType": 1, + "isNullable": true, + "dartType": "bool?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "textColor", + "fieldName": "textColor", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fillColor", + "fieldName": "fillColor", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "hAlign", + "fieldName": "hAlign", + "columnType": 0, + "isNullable": true, + "dartType": "protocol:HorizontalAlign?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "vAlign", + "fieldName": "vAlign", + "columnType": 0, + "isNullable": true, + "dartType": "protocol:VerticalAlign?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "wrap", + "fieldName": "wrap", + "columnType": 0, + "isNullable": true, + "dartType": "protocol:WrapStrategy?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "rotation", + "fieldName": "rotation", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "borderTop", + "fieldName": "borderTop", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "borderRight", + "fieldName": "borderRight", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "borderBottom", + "fieldName": "borderBottom", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "borderLeft", + "fieldName": "borderLeft", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "linkUrl", + "fieldName": "linkUrl", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "cell_fk_0", + "columns": [ + "sheetId" + ], + "referenceTable": "sheet", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4, + "deferrable": "initiallyDeferred" + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "cell_row_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "sheetId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "rowId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "cell_col_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "sheetId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "columnId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "cell_history", + "dartName": "CellHistory", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileUuid", + "fieldName": "fileUuid", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sheetId", + "fieldName": "sheetId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "rowId", + "fieldName": "rowId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "colId", + "fieldName": "colId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revision", + "fieldName": "revision", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userUuid", + "fieldName": "userUuid", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "actorKind", + "fieldName": "actorKind", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:ActorKind" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "label", + "fieldName": "label", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "at", + "fieldName": "at", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "before", + "fieldName": "before", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "after", + "fieldName": "after", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "cell_history_cell_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "fileUuid" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "sheetId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "rowId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "colId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "at" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "cell_history_change_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "fileUuid" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "revision" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "rowId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "colId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "chart_link", + "dartName": "ChartLink", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "random_v7", + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileId", + "fieldName": "fileId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revision", + "fieldName": "revision", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "elementId", + "fieldName": "elementId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "spreadsheetItemUuid", + "fieldName": "spreadsheetItemUuid", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "chartObjectId", + "fieldName": "chartObjectId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sheetId", + "fieldName": "sheetId", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sourceSpecJson", + "fieldName": "sourceSpecJson", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "dataSnapshotJson", + "fieldName": "dataSnapshotJson", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sourceVersion", + "fieldName": "sourceVersion", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "updatedAt", + "fieldName": "updatedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "updatedBy", + "fieldName": "updatedBy", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "autoUpdate", + "fieldName": "autoUpdate", + "columnType": 1, + "isNullable": false, + "columnDefault": "false", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "chart_link_fk_0", + "columns": [ + "fileId" + ], + "referenceTable": "rev_file", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "chart_link_fk_1", + "columns": [ + "elementId" + ], + "referenceTable": "slide_element", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4, + "deferrable": "initiallyDeferred" + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "chart_link_file_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "fileId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "chart_link_element_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "elementId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "code_state", + "dartName": "CodeState", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "random_v7", + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileId", + "fieldName": "fileId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revision", + "fieldName": "revision", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "elementId", + "fieldName": "elementId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sortKey", + "fieldName": "sortKey", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "label", + "fieldName": "label", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "code", + "fieldName": "code", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "language", + "fieldName": "language", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "title", + "fieldName": "title", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "highlight", + "fieldName": "highlight", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "annotationsJson", + "fieldName": "annotationsJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "transitionJson", + "fieldName": "transitionJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "code_state_fk_0", + "columns": [ + "fileId" + ], + "referenceTable": "rev_file", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "code_state_fk_1", + "columns": [ + "elementId" + ], + "referenceTable": "slide_element", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4, + "deferrable": "initiallyDeferred" + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "code_state_file_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "fileId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "code_state_element_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "elementId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "sortKey" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "comment", + "dartName": "Comment", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "threadId", + "fieldName": "threadId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "uuid", + "fieldName": "uuid", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "authorId", + "fieldName": "authorId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "authorKind", + "fieldName": "authorKind", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:ActorKind" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "body", + "fieldName": "body", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "mentions", + "fieldName": "mentions", + "columnType": 8, + "isNullable": false, + "dartType": "List" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "reactions", + "fieldName": "reactions", + "columnType": 0, + "isNullable": false, + "columnDefault": "'{}'", + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "editedAt", + "fieldName": "editedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "deletedAt", + "fieldName": "deletedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "systemEvent", + "fieldName": "systemEvent", + "columnType": 0, + "isNullable": true, + "dartType": "protocol:CommentSystemEvent?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "publicationId", + "fieldName": "publicationId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "comment_fk_0", + "columns": [ + "threadId" + ], + "referenceTable": "comment_thread", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "comment_fk_1", + "columns": [ + "publicationId" + ], + "referenceTable": "publication", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 0 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "comment_thread_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "threadId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "createdAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "comment_uuid_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "uuid" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "comment_created_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "createdAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "comment_thread", + "dartName": "CommentThread", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "uuid", + "fieldName": "uuid", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "itemId", + "fieldName": "itemId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "anchorKind", + "fieldName": "anchorKind", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:CommentAnchorKind" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "anchorJson", + "fieldName": "anchorJson", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "quotedText", + "fieldName": "quotedText", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "status", + "fieldName": "status", + "columnType": 0, + "isNullable": false, + "columnDefault": "'open'", + "dartType": "protocol:CommentStatus" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdById", + "fieldName": "createdById", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "updatedAt", + "fieldName": "updatedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "resolvedById", + "fieldName": "resolvedById", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "resolvedAt", + "fieldName": "resolvedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "assigneeId", + "fieldName": "assigneeId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "statusBeforeOrphan", + "fieldName": "statusBeforeOrphan", + "columnType": 0, + "isNullable": true, + "dartType": "protocol:CommentStatus?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "orphanReasonJson", + "fieldName": "orphanReasonJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "suggestionId", + "fieldName": "suggestionId", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "comment_thread_fk_0", + "columns": [ + "itemId" + ], + "referenceTable": "drive_item", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "comment_thread_item_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "itemId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "status" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "comment_thread_uuid_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "uuid" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "conditional_format_rule", + "dartName": "ConditionalFormatRule", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "random_v7", + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileId", + "fieldName": "fileId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revision", + "fieldName": "revision", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sheetId", + "fieldName": "sheetId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sortKey", + "fieldName": "sortKey", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "rangesJson", + "fieldName": "rangesJson", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "ruleJson", + "fieldName": "ruleJson", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "enabled", + "fieldName": "enabled", + "columnType": 1, + "isNullable": false, + "columnDefault": "true", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "conditional_format_rule_fk_0", + "columns": [ + "fileId" + ], + "referenceTable": "rev_file", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "conditional_format_rule_fk_1", + "columns": [ + "sheetId" + ], + "referenceTable": "sheet", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4, + "deferrable": "initiallyDeferred" + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "conditional_format_rule_file_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "fileId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "data_validation_rule", + "dartName": "DataValidationRule", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "random_v7", + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileId", + "fieldName": "fileId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revision", + "fieldName": "revision", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sheetId", + "fieldName": "sheetId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "rangesJson", + "fieldName": "rangesJson", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "criteriaJson", + "fieldName": "criteriaJson", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "strict", + "fieldName": "strict", + "columnType": 1, + "isNullable": false, + "columnDefault": "false", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "showDropdown", + "fieldName": "showDropdown", + "columnType": 1, + "isNullable": false, + "columnDefault": "true", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "displayStyle", + "fieldName": "displayStyle", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "helpText", + "fieldName": "helpText", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "data_validation_rule_fk_0", + "columns": [ + "fileId" + ], + "referenceTable": "rev_file", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "data_validation_rule_fk_1", + "columns": [ + "sheetId" + ], + "referenceTable": "sheet", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4, + "deferrable": "initiallyDeferred" + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "data_validation_rule_file_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "fileId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "deck", + "dartName": "Deck", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "random_v7", + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileId", + "fieldName": "fileId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revision", + "fieldName": "revision", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "title", + "fieldName": "title", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "width", + "fieldName": "width", + "columnType": 3, + "isNullable": false, + "columnDefault": "1920.0", + "dartType": "double" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "height", + "fieldName": "height", + "columnType": 3, + "isNullable": false, + "columnDefault": "1080.0", + "dartType": "double" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sizePreset", + "fieldName": "sizePreset", + "columnType": 0, + "isNullable": false, + "columnDefault": "'widescreen16x9'", + "dartType": "protocol:SlideSizePreset" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "themeId", + "fieldName": "themeId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "appearance", + "fieldName": "appearance", + "columnType": 0, + "isNullable": false, + "columnDefault": "'light'", + "dartType": "protocol:DeckAppearance" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "language", + "fieldName": "language", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "showSlideNumbers", + "fieldName": "showSlideNumbers", + "columnType": 1, + "isNullable": false, + "columnDefault": "false", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "hideNumberOnTitle", + "fieldName": "hideNumberOnTitle", + "columnType": 1, + "isNullable": false, + "columnDefault": "true", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "firstSlideNumber", + "fieldName": "firstSlideNumber", + "columnType": 6, + "isNullable": false, + "columnDefault": "1", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "footerText", + "fieldName": "footerText", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "defaultsJson", + "fieldName": "defaultsJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "guidesJson", + "fieldName": "guidesJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "presentJson", + "fieldName": "presentJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdBy", + "fieldName": "createdBy", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "deck_fk_0", + "columns": [ + "fileId" + ], + "referenceTable": "rev_file", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "deck_file_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "fileId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "deck_theme", + "dartName": "DeckTheme", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "random_v7", + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileId", + "fieldName": "fileId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revision", + "fieldName": "revision", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "deckId", + "fieldName": "deckId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "name", + "fieldName": "name", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sourceKey", + "fieldName": "sourceKey", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "colorsJson", + "fieldName": "colorsJson", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fontsJson", + "fieldName": "fontsJson", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "textStylesJson", + "fieldName": "textStylesJson", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "backgroundJson", + "fieldName": "backgroundJson", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "shapeStyleJson", + "fieldName": "shapeStyleJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "tableStyleJson", + "fieldName": "tableStyleJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "codeStyleJson", + "fieldName": "codeStyleJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "chartStyleJson", + "fieldName": "chartStyleJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sortKey", + "fieldName": "sortKey", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "deck_theme_fk_0", + "columns": [ + "fileId" + ], + "referenceTable": "rev_file", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "deck_theme_fk_1", + "columns": [ + "deckId" + ], + "referenceTable": "deck", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4, + "deferrable": "initiallyDeferred" + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "deck_theme_file_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "fileId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "deck_theme_deck_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "deckId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "dimension_group", + "dartName": "DimensionGroup", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "random_v7", + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileId", + "fieldName": "fileId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revision", + "fieldName": "revision", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sheetId", + "fieldName": "sheetId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "dimension", + "fieldName": "dimension", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:Dimension" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "startId", + "fieldName": "startId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "endId", + "fieldName": "endId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "depth", + "fieldName": "depth", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "collapsed", + "fieldName": "collapsed", + "columnType": 1, + "isNullable": false, + "columnDefault": "false", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "dimension_group_fk_0", + "columns": [ + "fileId" + ], + "referenceTable": "rev_file", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "dimension_group_fk_1", + "columns": [ + "sheetId" + ], + "referenceTable": "sheet", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4, + "deferrable": "initiallyDeferred" + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "dimension_group_file_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "fileId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "document", + "dartName": "Document", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "uuid", + "fieldName": "uuid", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "itemId", + "fieldName": "itemId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "headRevision", + "fieldName": "headRevision", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "latestSnapshotRevision", + "fieldName": "latestSnapshotRevision", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "themeId", + "fieldName": "themeId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "pageSetupJson", + "fieldName": "pageSetupJson", + "columnType": 0, + "isNullable": false, + "columnDefault": "'{}'", + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "language", + "fieldName": "language", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "frontMatter", + "fieldName": "frontMatter", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "charCount", + "fieldName": "charCount", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "wordCount", + "fieldName": "wordCount", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "updatedAt", + "fieldName": "updatedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "document_fk_0", + "columns": [ + "itemId" + ], + "referenceTable": "drive_item", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "document_uuid_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "uuid" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "document_item_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "itemId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "document_import_report", + "dartName": "DocumentImportReport", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "documentId", + "fieldName": "documentId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "fieldName": "userId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "warningsJson", + "fieldName": "warningsJson", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "seenAt", + "fieldName": "seenAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "document_import_report_fk_0", + "columns": [ + "documentId" + ], + "referenceTable": "document", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "document_import_report_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "documentId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "document_meta_change", + "dartName": "DocumentMetaChange", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "documentId", + "fieldName": "documentId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "field", + "fieldName": "field", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "clientOpId", + "fieldName": "clientOpId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "beforeJson", + "fieldName": "beforeJson", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "afterJson", + "fieldName": "afterJson", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "authorId", + "fieldName": "authorId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "actorKind", + "fieldName": "actorKind", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:ActorKind" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "document_meta_change_fk_0", + "columns": [ + "documentId" + ], + "referenceTable": "document", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "document_meta_change_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "documentId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "createdAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "document_meta_change_client_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "documentId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "clientOpId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "document_op", + "dartName": "DocumentOp", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "documentId", + "fieldName": "documentId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revision", + "fieldName": "revision", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "clientOpId", + "fieldName": "clientOpId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "authorId", + "fieldName": "authorId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "actorKind", + "fieldName": "actorKind", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:ActorKind" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sessionId", + "fieldName": "sessionId", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "kind", + "fieldName": "kind", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:DocumentOpKind" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "baseRevision", + "fieldName": "baseRevision", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "deltaJson", + "fieldName": "deltaJson", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "inverseJson", + "fieldName": "inverseJson", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "document_op_fk_0", + "columns": [ + "documentId" + ], + "referenceTable": "document", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "document_op_rev_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "documentId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "revision" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "document_op_client_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "documentId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "clientOpId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "document_op_created_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "createdAt" + } + ], + "type": "brin", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "document_snapshot", + "dartName": "DocumentSnapshot", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "documentId", + "fieldName": "documentId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revision", + "fieldName": "revision", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "deltaJson", + "fieldName": "deltaJson", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "compressed", + "fieldName": "compressed", + "columnType": 1, + "isNullable": false, + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "document_snapshot_fk_0", + "columns": [ + "documentId" + ], + "referenceTable": "document", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "document_snapshot_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "documentId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "revision" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "drive_item", + "dartName": "DriveItem", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "uuid", + "fieldName": "uuid", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "kind", + "fieldName": "kind", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:DriveItemKind" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "name", + "fieldName": "name", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "ownerId", + "fieldName": "ownerId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "parentId", + "fieldName": "parentId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "description", + "fieldName": "description", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "contentRef", + "fieldName": "contentRef", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "thumbnailFileId", + "fieldName": "thumbnailFileId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sizeBytes", + "fieldName": "sizeBytes", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "attachmentBytes", + "fieldName": "attachmentBytes", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "kindMask", + "fieldName": "kindMask", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "mimeType", + "fieldName": "mimeType", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileCategory", + "fieldName": "fileCategory", + "columnType": 0, + "isNullable": true, + "dartType": "protocol:FileCategory?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "uploadState", + "fieldName": "uploadState", + "columnType": 0, + "isNullable": true, + "dartType": "protocol:FileUploadState?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileMetaJson", + "fieldName": "fileMetaJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "previewFileId", + "fieldName": "previewFileId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdById", + "fieldName": "createdById", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "modifiedAt", + "fieldName": "modifiedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "modifiedById", + "fieldName": "modifiedById", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "trashedAt", + "fieldName": "trashedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "trashedById", + "fieldName": "trashedById", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "templateOf", + "fieldName": "templateOf", + "columnType": 0, + "isNullable": true, + "dartType": "protocol:TemplateCategory?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "templateKey", + "fieldName": "templateKey", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "editorsCanShare", + "fieldName": "editorsCanShare", + "columnType": 1, + "isNullable": false, + "columnDefault": "true", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "nonEditorsCanExport", + "fieldName": "nonEditorsCanExport", + "columnType": 1, + "isNullable": false, + "columnDefault": "true", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "viewersSeeComments", + "fieldName": "viewersSeeComments", + "columnType": 1, + "isNullable": false, + "columnDefault": "false", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "searchText", + "fieldName": "searchText", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "drive_item_fk_0", + "columns": [ + "ownerId" + ], + "referenceTable": "user_profile", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 2 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "drive_item_fk_1", + "columns": [ + "parentId" + ], + "referenceTable": "drive_item", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 0 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "drive_item_uuid_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "uuid" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "drive_item_parent_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "parentId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "drive_item_owner_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "ownerId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "trashedAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "drive_item_content_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "contentRef" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "drive_item_template_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "templateOf" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "drive_item_upload_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "uploadState" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "createdAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "drive_item_trashed_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "trashedAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "drive_item_permission", + "dartName": "DriveItemPermission", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "itemId", + "fieldName": "itemId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "fieldName": "userId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "invitedEmail", + "fieldName": "invitedEmail", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "role", + "fieldName": "role", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:AccessRole" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "inheritedFromId", + "fieldName": "inheritedFromId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "grantedById", + "fieldName": "grantedById", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "grantedAt", + "fieldName": "grantedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "expiresAt", + "fieldName": "expiresAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "viaPublicationId", + "fieldName": "viaPublicationId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "drive_item_permission_fk_0", + "columns": [ + "itemId" + ], + "referenceTable": "drive_item", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "drive_item_permission_fk_1", + "columns": [ + "userId" + ], + "referenceTable": "user_profile", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "drive_item_permission_fk_2", + "columns": [ + "viaPublicationId" + ], + "referenceTable": "publication", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "perm_item_user_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "itemId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "inheritedFromId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "invitedEmail" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false, + "nullsDistinct": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "perm_user_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "perm_email_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "invitedEmail" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "perm_expires_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "expiresAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "perm_granted_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "grantedAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "drive_item_user_state", + "dartName": "DriveItemUserState", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "itemId", + "fieldName": "itemId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "fieldName": "userId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "starred", + "fieldName": "starred", + "columnType": 1, + "isNullable": false, + "columnDefault": "false", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lastOpenedAt", + "fieldName": "lastOpenedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lastEditedAt", + "fieldName": "lastEditedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "pinnedOffline", + "fieldName": "pinnedOffline", + "columnType": 1, + "isNullable": false, + "columnDefault": "false", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "shortcutParentId", + "fieldName": "shortcutParentId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "viaLink", + "fieldName": "viaLink", + "columnType": 1, + "isNullable": false, + "columnDefault": "false", + "dartType": "bool" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "drive_item_user_state_fk_0", + "columns": [ + "itemId" + ], + "referenceTable": "drive_item", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "drive_item_user_state_fk_1", + "columns": [ + "userId" + ], + "referenceTable": "user_profile", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "drive_item_user_state_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "itemId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "drive_item_user_recent_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "lastOpenedAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "embedded_object", + "dartName": "EmbeddedObject", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "random_v7", + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileId", + "fieldName": "fileId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revision", + "fieldName": "revision", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sheetId", + "fieldName": "sheetId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "kind", + "fieldName": "kind", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:EmbeddedObjectKind" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "anchorJson", + "fieldName": "anchorJson", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "specJson", + "fieldName": "specJson", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "zIndex", + "fieldName": "zIndex", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "title", + "fieldName": "title", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "altText", + "fieldName": "altText", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "embedded_object_fk_0", + "columns": [ + "fileId" + ], + "referenceTable": "rev_file", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "embedded_object_fk_1", + "columns": [ + "sheetId" + ], + "referenceTable": "sheet", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4, + "deferrable": "initiallyDeferred" + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "embedded_object_file_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "fileId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "feedback_report", + "dartName": "FeedbackReport", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "fieldName": "userId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "kind", + "fieldName": "kind", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:FeedbackKind" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "message", + "fieldName": "message", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "app", + "fieldName": "app", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "appVersion", + "fieldName": "appVersion", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "platform", + "fieldName": "platform", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "route", + "fieldName": "route", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "stackTrace", + "fieldName": "stackTrace", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "contextJson", + "fieldName": "contextJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "feedback_report_created_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "createdAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "filter_view", + "dartName": "FilterView", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "random_v7", + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileId", + "fieldName": "fileId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revision", + "fieldName": "revision", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sheetId", + "fieldName": "sheetId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "name", + "fieldName": "name", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "rangeJson", + "fieldName": "rangeJson", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "criteriaJson", + "fieldName": "criteriaJson", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sortJson", + "fieldName": "sortJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdBy", + "fieldName": "createdBy", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "filter_view_fk_0", + "columns": [ + "fileId" + ], + "referenceTable": "rev_file", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "filter_view_fk_1", + "columns": [ + "sheetId" + ], + "referenceTable": "sheet", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4, + "deferrable": "initiallyDeferred" + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "filter_view_file_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "fileId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "headless_change", + "dartName": "HeadlessChange", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "changeId", + "fieldName": "changeId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "kind", + "fieldName": "kind", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "itemId", + "fieldName": "itemId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "documentId", + "fieldName": "documentId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revision", + "fieldName": "revision", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "inverseJson", + "fieldName": "inverseJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "metaField", + "fieldName": "metaField", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "metaBeforeJson", + "fieldName": "metaBeforeJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "metaAfterJson", + "fieldName": "metaAfterJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "fieldName": "userId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "actorKind", + "fieldName": "actorKind", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:ActorKind" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "label", + "fieldName": "label", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "toolName", + "fieldName": "toolName", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revertedAt", + "fieldName": "revertedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "headless_change_fk_0", + "columns": [ + "itemId" + ], + "referenceTable": "drive_item", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "headless_change_id_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "changeId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "headless_change_created_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "createdAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "item_version", + "dartName": "ItemVersion", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "itemId", + "fieldName": "itemId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "kind", + "fieldName": "kind", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:VersionKind" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "name", + "fieldName": "name", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdById", + "fieldName": "createdById", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "authors", + "fieldName": "authors", + "columnType": 8, + "isNullable": false, + "dartType": "List" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "storageKind", + "fieldName": "storageKind", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:VersionStorage" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revision", + "fieldName": "revision", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "snapshotFileId", + "fieldName": "snapshotFileId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sizeBytes", + "fieldName": "sizeBytes", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "item_version_fk_0", + "columns": [ + "itemId" + ], + "referenceTable": "drive_item", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "item_version_item_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "itemId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "createdAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "item_version_state", + "dartName": "ItemVersionState", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "itemId", + "fieldName": "itemId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lastEditAt", + "fieldName": "lastEditAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "dirtySince", + "fieldName": "dirtySince", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lastVersionAt", + "fieldName": "lastVersionAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "editorsSinceLastVersion", + "fieldName": "editorsSinceLastVersion", + "columnType": 8, + "isNullable": false, + "dartType": "List" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lastRevision", + "fieldName": "lastRevision", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "capturingSince", + "fieldName": "capturingSince", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "item_version_state_fk_0", + "columns": [ + "itemId" + ], + "referenceTable": "drive_item", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "item_version_state_item_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "itemId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "item_version_state_dirty_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "dirtySince" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "item_view_day", + "dartName": "ItemViewDay", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "itemId", + "fieldName": "itemId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "fieldName": "userId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "day", + "fieldName": "day", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "views", + "fieldName": "views", + "columnType": 6, + "isNullable": false, + "columnDefault": "1", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lastViewedAt", + "fieldName": "lastViewedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "item_view_day_fk_0", + "columns": [ + "itemId" + ], + "referenceTable": "drive_item", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "item_view_day_fk_1", + "columns": [ + "userId" + ], + "referenceTable": "user_profile", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "item_view_day_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "itemId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "day" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "item_view_day_item_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "itemId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "day" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "macro", + "dartName": "Macro", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "random_v7", + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileId", + "fieldName": "fileId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revision", + "fieldName": "revision", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "projectId", + "fieldName": "projectId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "name", + "fieldName": "name", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "functionName", + "fieldName": "functionName", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "shortcutSlot", + "fieldName": "shortcutSlot", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "macro_fk_0", + "columns": [ + "fileId" + ], + "referenceTable": "rev_file", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "macro_fk_1", + "columns": [ + "projectId" + ], + "referenceTable": "script_project", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4, + "deferrable": "initiallyDeferred" + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "macro_file_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "fileId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "merged_range", + "dartName": "MergedRange", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "random_v7", + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileId", + "fieldName": "fileId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revision", + "fieldName": "revision", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sheetId", + "fieldName": "sheetId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "rangeJson", + "fieldName": "rangeJson", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "merged_range_fk_0", + "columns": [ + "fileId" + ], + "referenceTable": "rev_file", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "merged_range_fk_1", + "columns": [ + "sheetId" + ], + "referenceTable": "sheet", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4, + "deferrable": "initiallyDeferred" + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "merged_range_file_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "fileId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "named_range", + "dartName": "NamedRange", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "random_v7", + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileId", + "fieldName": "fileId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revision", + "fieldName": "revision", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "spreadsheetId", + "fieldName": "spreadsheetId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "name", + "fieldName": "name", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "rangeJson", + "fieldName": "rangeJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "formula", + "fieldName": "formula", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "comment", + "fieldName": "comment", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "metaJson", + "fieldName": "metaJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "named_range_fk_0", + "columns": [ + "fileId" + ], + "referenceTable": "rev_file", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "named_range_fk_1", + "columns": [ + "spreadsheetId" + ], + "referenceTable": "spreadsheet", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4, + "deferrable": "initiallyDeferred" + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "named_range_file_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "fileId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "notification", + "dartName": "UserNotification", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "fieldName": "userId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "kind", + "fieldName": "kind", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:NotificationKind" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "itemId", + "fieldName": "itemId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "actorId", + "fieldName": "actorId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "payload", + "fieldName": "payload", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "readAt", + "fieldName": "readAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "emailedAt", + "fieldName": "emailedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "emailPending", + "fieldName": "emailPending", + "columnType": 1, + "isNullable": true, + "columnDefault": "false", + "dartType": "bool?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "notification_fk_0", + "columns": [ + "userId" + ], + "referenceTable": "user_profile", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "notification_user_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "createdAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "notification_email_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "emailPending" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "createdAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "notification_created_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "createdAt" + } + ], + "type": "brin", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "ownership_transfer", + "dartName": "OwnershipTransfer", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "itemId", + "fieldName": "itemId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fromUserId", + "fieldName": "fromUserId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "toUserId", + "fieldName": "toUserId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "handledAt", + "fieldName": "handledAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "accepted", + "fieldName": "accepted", + "columnType": 1, + "isNullable": true, + "dartType": "bool?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "ownership_transfer_fk_0", + "columns": [ + "itemId" + ], + "referenceTable": "drive_item", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "ownership_transfer_fk_1", + "columns": [ + "toUserId" + ], + "referenceTable": "user_profile", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "ownership_transfer_item_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "itemId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "handledAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "personal_access_token", + "dartName": "PersonalAccessToken", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "fieldName": "userId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "name", + "fieldName": "name", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "tokenHash", + "fieldName": "tokenHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "prefix", + "fieldName": "prefix", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "scopes", + "fieldName": "scopes", + "columnType": 8, + "isNullable": false, + "dartType": "List" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "expiresAt", + "fieldName": "expiresAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lastUsedAt", + "fieldName": "lastUsedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revokedAt", + "fieldName": "revokedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "personal_access_token_fk_0", + "columns": [ + "userId" + ], + "referenceTable": "user_profile", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "personal_access_token_user_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "personal_access_token_hash_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "tokenHash" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "pivot_table", + "dartName": "PivotTable", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "random_v7", + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileId", + "fieldName": "fileId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revision", + "fieldName": "revision", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sheetId", + "fieldName": "sheetId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "anchorRowId", + "fieldName": "anchorRowId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "anchorColId", + "fieldName": "anchorColId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sourceRangeJson", + "fieldName": "sourceRangeJson", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "specJson", + "fieldName": "specJson", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "pivot_table_fk_0", + "columns": [ + "fileId" + ], + "referenceTable": "rev_file", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "pivot_table_fk_1", + "columns": [ + "sheetId" + ], + "referenceTable": "sheet", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4, + "deferrable": "initiallyDeferred" + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "pivot_table_file_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "fileId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "protected_range", + "dartName": "ProtectedRange", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "random_v7", + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileId", + "fieldName": "fileId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revision", + "fieldName": "revision", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sheetId", + "fieldName": "sheetId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "rangeJson", + "fieldName": "rangeJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "exceptRangesJson", + "fieldName": "exceptRangesJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "description", + "fieldName": "description", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "warningOnly", + "fieldName": "warningOnly", + "columnType": 1, + "isNullable": false, + "columnDefault": "false", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "editorsJson", + "fieldName": "editorsJson", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdBy", + "fieldName": "createdBy", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "protected_range_fk_0", + "columns": [ + "fileId" + ], + "referenceTable": "rev_file", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "protected_range_fk_1", + "columns": [ + "sheetId" + ], + "referenceTable": "sheet", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4, + "deferrable": "initiallyDeferred" + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "protected_range_file_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "fileId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "publication", + "dartName": "Publication", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "publicId", + "fieldName": "publicId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "ownerId", + "fieldName": "ownerId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "name", + "fieldName": "name", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "siteUrl", + "fieldName": "siteUrl", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "webhookUrl", + "fieldName": "webhookUrl", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "verifiedHost", + "fieldName": "verifiedHost", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "verifiedAt", + "fieldName": "verifiedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "templateItemId", + "fieldName": "templateItemId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "termsUrl", + "fieldName": "termsUrl", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fields", + "fieldName": "fields", + "columnType": 8, + "isNullable": false, + "dartType": "protocol:PublicationFields" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "signingSecret", + "fieldName": "signingSecret", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "previousSigningSecret", + "fieldName": "previousSigningSecret", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "previousSecretUntil", + "fieldName": "previousSecretUntil", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "apiKeyHash", + "fieldName": "apiKeyHash", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "apiKeyLast4", + "fieldName": "apiKeyLast4", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "enabled", + "fieldName": "enabled", + "columnType": 1, + "isNullable": false, + "columnDefault": "true", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "updatedAt", + "fieldName": "updatedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "publication_fk_0", + "columns": [ + "ownerId" + ], + "referenceTable": "user_profile", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "publication_fk_1", + "columns": [ + "templateItemId" + ], + "referenceTable": "drive_item", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 0 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "publication_public_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "publicId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "publication_owner_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "ownerId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "publication_api_key_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "apiKeyHash" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "publication_block", + "dartName": "PublicationBlock", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "publicationId", + "fieldName": "publicationId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "fieldName": "userId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "at", + "fieldName": "at", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "publication_block_fk_0", + "columns": [ + "publicationId" + ], + "referenceTable": "publication", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "publication_block_fk_1", + "columns": [ + "userId" + ], + "referenceTable": "user_profile", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "publication_block_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "publicationId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "publication_consent", + "dartName": "PublicationConsent", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "publicationId", + "fieldName": "publicationId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "fieldName": "userId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "consentKey", + "fieldName": "consentKey", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "at", + "fieldName": "at", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "publication_consent_fk_0", + "columns": [ + "publicationId" + ], + "referenceTable": "publication", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "publication_consent_fk_1", + "columns": [ + "userId" + ], + "referenceTable": "user_profile", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "publication_consent_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "publicationId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "publication_delivery", + "dartName": "PublicationDelivery", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "publicationId", + "fieldName": "publicationId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "submissionId", + "fieldName": "submissionId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "webhookId", + "fieldName": "webhookId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "type", + "fieldName": "type", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sequence", + "fieldName": "sequence", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "payload", + "fieldName": "payload", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "attempts", + "fieldName": "attempts", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "nextAttemptAt", + "fieldName": "nextAttemptAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "leaseUntil", + "fieldName": "leaseUntil", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lastStatus", + "fieldName": "lastStatus", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lastError", + "fieldName": "lastError", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "deliveredAt", + "fieldName": "deliveredAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "failedAt", + "fieldName": "failedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "publication_delivery_fk_0", + "columns": [ + "publicationId" + ], + "referenceTable": "publication", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "publication_delivery_fk_1", + "columns": [ + "submissionId" + ], + "referenceTable": "submission", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 0 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "publication_delivery_webhook_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "webhookId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "publication_delivery_due_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "nextAttemptAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "publication_delivery_publication_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "publicationId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "createdAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "publication_moderator", + "dartName": "PublicationModerator", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "publicationId", + "fieldName": "publicationId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "fieldName": "userId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "email", + "fieldName": "email", + "columnType": 0, + "isNullable": false, + "dartType": "String" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "publication_moderator_fk_0", + "columns": [ + "publicationId" + ], + "referenceTable": "publication", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "publication_moderator_fk_1", + "columns": [ + "userId" + ], + "referenceTable": "user_profile", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "publication_moderator_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "publicationId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "email" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "publication_moderator_user_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "rev_file", + "dartName": "RevFile", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileUuid", + "fieldName": "fileUuid", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "kind", + "fieldName": "kind", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:RevFileKind" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "itemId", + "fieldName": "itemId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "headRevision", + "fieldName": "headRevision", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "prunedBelow", + "fieldName": "prunedBelow", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "initPending", + "fieldName": "initPending", + "columnType": 1, + "isNullable": false, + "columnDefault": "false", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "purging", + "fieldName": "purging", + "columnType": 1, + "isNullable": false, + "columnDefault": "false", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "schemaHash", + "fieldName": "schemaHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "stats", + "fieldName": "stats", + "columnType": 0, + "isNullable": false, + "columnDefault": "'{}'", + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "contentCount", + "fieldName": "contentCount", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "updatedAt", + "fieldName": "updatedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "rev_file_fk_0", + "columns": [ + "itemId" + ], + "referenceTable": "drive_item", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 2 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "rev_file_uuid_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "fileUuid" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "rev_file_item_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "itemId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "rev_hook_state", + "dartName": "RevHookState", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileId", + "fieldName": "fileId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "cursorsJson", + "fieldName": "cursorsJson", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "leaseOwner", + "fieldName": "leaseOwner", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "leaseUntil", + "fieldName": "leaseUntil", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "attempts", + "fieldName": "attempts", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lastError", + "fieldName": "lastError", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "nextAttemptAt", + "fieldName": "nextAttemptAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "updatedAt", + "fieldName": "updatedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "rev_hook_state_fk_0", + "columns": [ + "fileId" + ], + "referenceTable": "rev_file", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "rev_hook_state_file_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "fileId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "rev_hook_state_due_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "nextAttemptAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "rev_op", + "dartName": "RevOp", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileId", + "fieldName": "fileId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revision", + "fieldName": "revision", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "clientOpId", + "fieldName": "clientOpId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "baseRevision", + "fieldName": "baseRevision", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "authorId", + "fieldName": "authorId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "authorUuid", + "fieldName": "authorUuid", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sessionId", + "fieldName": "sessionId", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "onBehalfOfSessionId", + "fieldName": "onBehalfOfSessionId", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "actorKind", + "fieldName": "actorKind", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:ActorKind" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "label", + "fieldName": "label", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "groupId", + "fieldName": "groupId", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "groupIndex", + "fieldName": "groupIndex", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "groupLast", + "fieldName": "groupLast", + "columnType": 1, + "isNullable": false, + "columnDefault": "true", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "recordId", + "fieldName": "recordId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "offline", + "fieldName": "offline", + "columnType": 1, + "isNullable": false, + "columnDefault": "false", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "payload", + "fieldName": "payload", + "columnType": 5, + "isNullable": true, + "dartType": "dart:typed_data:ByteData?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "changeCount", + "fieldName": "changeCount", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "summaryJson", + "fieldName": "summaryJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "detailJson", + "fieldName": "detailJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "outcomeJson", + "fieldName": "outcomeJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "appliedEqualsSubmitted", + "fieldName": "appliedEqualsSubmitted", + "columnType": 1, + "isNullable": false, + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "clientCreatedAt", + "fieldName": "clientCreatedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "committedAt", + "fieldName": "committedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "rev_op_fk_0", + "columns": [ + "fileId" + ], + "referenceTable": "rev_file", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "rev_op_rev_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "fileId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "revision" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "rev_op_client_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "fileId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "clientOpId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "rev_op_record_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "recordId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "rev_op_committed_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "committedAt" + } + ], + "type": "brin", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "script_authorization", + "dartName": "ScriptAuthorization", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "fieldName": "userId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "spreadsheetUuid", + "fieldName": "spreadsheetUuid", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "scopes", + "fieldName": "scopes", + "columnType": 8, + "isNullable": false, + "dartType": "List" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "grantedAt", + "fieldName": "grantedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revokedAt", + "fieldName": "revokedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "script_auth_user_sheet_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "spreadsheetUuid" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "script_build", + "dartName": "ScriptBuild", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "random_v7", + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileId", + "fieldName": "fileId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revision", + "fieldName": "revision", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "projectId", + "fieldName": "projectId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "status", + "fieldName": "status", + "columnType": 0, + "isNullable": false, + "columnDefault": "'ok'", + "dartType": "protocol:ScriptBuildStatus" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sourceHash", + "fieldName": "sourceHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "bytecodeFileUuid", + "fieldName": "bytecodeFileUuid", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "diagnosticsJson", + "fieldName": "diagnosticsJson", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "exportsJson", + "fieldName": "exportsJson", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "apiVersion", + "fieldName": "apiVersion", + "columnType": 6, + "isNullable": false, + "columnDefault": "1", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "compilerVersion", + "fieldName": "compilerVersion", + "columnType": 0, + "isNullable": false, + "columnDefault": "''", + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "requestedBy", + "fieldName": "requestedBy", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lastGoodSourceHash", + "fieldName": "lastGoodSourceHash", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lastGoodBytecodeFileUuid", + "fieldName": "lastGoodBytecodeFileUuid", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lastGoodExportsJson", + "fieldName": "lastGoodExportsJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lastGoodBuiltAt", + "fieldName": "lastGoodBuiltAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "positionsFileUuid", + "fieldName": "positionsFileUuid", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "builtAt", + "fieldName": "builtAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "script_build_fk_0", + "columns": [ + "fileId" + ], + "referenceTable": "rev_file", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "script_build_fk_1", + "columns": [ + "projectId" + ], + "referenceTable": "script_project", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4, + "deferrable": "initiallyDeferred" + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "script_build_file_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "fileId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "script_execution", + "dartName": "ScriptExecution", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "uuid", + "fieldName": "uuid", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "spreadsheetUuid", + "fieldName": "spreadsheetUuid", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "projectId", + "fieldName": "projectId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "functionName", + "fieldName": "functionName", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "kind", + "fieldName": "kind", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:ScriptExecutionKind" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "triggerId", + "fieldName": "triggerId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "fieldName": "userId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "runAsUserId", + "fieldName": "runAsUserId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "runtime", + "fieldName": "runtime", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:ScriptRuntimeKind" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "status", + "fieldName": "status", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:ScriptExecutionStatus" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "buildSourceHash", + "fieldName": "buildSourceHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "startedAt", + "fieldName": "startedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "durationMs", + "fieldName": "durationMs", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "logs", + "fieldName": "logs", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "errorMessage", + "fieldName": "errorMessage", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "errorStackJson", + "fieldName": "errorStackJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "changeId", + "fieldName": "changeId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "cellsRead", + "fieldName": "cellsRead", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "cellsWritten", + "fieldName": "cellsWritten", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fetchCalls", + "fieldName": "fetchCalls", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "callCount", + "fieldName": "callCount", + "columnType": 6, + "isNullable": false, + "columnDefault": "1", + "dartType": "int" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "script_exec_uuid_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "uuid" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "script_exec_sheet_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "spreadsheetUuid" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "startedAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "script_exec_user_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "startedAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "script_file", + "dartName": "ScriptFile", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "random_v7", + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileId", + "fieldName": "fileId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revision", + "fieldName": "revision", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "projectId", + "fieldName": "projectId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "name", + "fieldName": "name", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sortKey", + "fieldName": "sortKey", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "source", + "fieldName": "source", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "contentHash", + "fieldName": "contentHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "updatedBy", + "fieldName": "updatedBy", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "updatedAt", + "fieldName": "updatedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "script_file_fk_0", + "columns": [ + "fileId" + ], + "referenceTable": "rev_file", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "script_file_fk_1", + "columns": [ + "projectId" + ], + "referenceTable": "script_project", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4, + "deferrable": "initiallyDeferred" + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "script_file_file_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "fileId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "script_job", + "dartName": "ScriptJob", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "kind", + "fieldName": "kind", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:ScriptExecutionKind" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "spreadsheetUuid", + "fieldName": "spreadsheetUuid", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "itemUuid", + "fieldName": "itemUuid", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "triggerId", + "fieldName": "triggerId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "runAsUserId", + "fieldName": "runAsUserId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "functionName", + "fieldName": "functionName", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "payloadJson", + "fieldName": "payloadJson", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "status", + "fieldName": "status", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:ScriptExecutionStatus" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "enqueuedAt", + "fieldName": "enqueuedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "leaseOwner", + "fieldName": "leaseOwner", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "leaseUntil", + "fieldName": "leaseUntil", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "attempts", + "fieldName": "attempts", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "coalesced", + "fieldName": "coalesced", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "resultJson", + "fieldName": "resultJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "finishedAt", + "fieldName": "finishedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "script_job_status_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "status" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "enqueuedAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "script_job_trigger_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "triggerId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "status" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "script_job_user_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "runAsUserId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "status" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "script_project", + "dartName": "ScriptProject", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "random_v7", + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileId", + "fieldName": "fileId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revision", + "fieldName": "revision", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "spreadsheetId", + "fieldName": "spreadsheetId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "name", + "fieldName": "name", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "settingsJson", + "fieldName": "settingsJson", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "script_project_fk_0", + "columns": [ + "fileId" + ], + "referenceTable": "rev_file", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "script_project_fk_1", + "columns": [ + "spreadsheetId" + ], + "referenceTable": "spreadsheet", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4, + "deferrable": "initiallyDeferred" + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "script_project_file_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "fileId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "script_property", + "dartName": "ScriptProperty", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "random_v7", + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileId", + "fieldName": "fileId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revision", + "fieldName": "revision", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "projectId", + "fieldName": "projectId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "key", + "fieldName": "key", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "value", + "fieldName": "value", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "script_property_fk_0", + "columns": [ + "fileId" + ], + "referenceTable": "rev_file", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "script_property_fk_1", + "columns": [ + "projectId" + ], + "referenceTable": "script_project", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4, + "deferrable": "initiallyDeferred" + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "script_property_file_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "fileId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "script_quota_usage", + "dartName": "ScriptQuotaUsage", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "fieldName": "userId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "day", + "fieldName": "day", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "serverRuntimeMs", + "fieldName": "serverRuntimeMs", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "clientRuntimeMs", + "fieldName": "clientRuntimeMs", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "urlFetchCalls", + "fieldName": "urlFetchCalls", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "urlFetchBytes", + "fieldName": "urlFetchBytes", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "triggerRuns", + "fieldName": "triggerRuns", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "script_quota_user_day_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "day" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "script_trigger", + "dartName": "ScriptTrigger", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "random_v7", + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileId", + "fieldName": "fileId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revision", + "fieldName": "revision", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "projectId", + "fieldName": "projectId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "kind", + "fieldName": "kind", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:TriggerKind" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "functionName", + "fieldName": "functionName", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "scheduleJson", + "fieldName": "scheduleJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "runAsUserId", + "fieldName": "runAsUserId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "enabled", + "fieldName": "enabled", + "columnType": 1, + "isNullable": false, + "columnDefault": "true", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "optionsJson", + "fieldName": "optionsJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "script_trigger_fk_0", + "columns": [ + "fileId" + ], + "referenceTable": "rev_file", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "script_trigger_fk_1", + "columns": [ + "projectId" + ], + "referenceTable": "script_project", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4, + "deferrable": "initiallyDeferred" + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "script_trigger_file_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "fileId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "script_trigger_state", + "dartName": "ScriptTriggerState", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "triggerId", + "fieldName": "triggerId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "spreadsheetUuid", + "fieldName": "spreadsheetUuid", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "scheduleJson", + "fieldName": "scheduleJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "nextRunAt", + "fieldName": "nextRunAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lastRunAt", + "fieldName": "lastRunAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lastStatus", + "fieldName": "lastStatus", + "columnType": 0, + "isNullable": true, + "dartType": "protocol:ScriptExecutionStatus?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "consecutiveFailures", + "fieldName": "consecutiveFailures", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "disabledReason", + "fieldName": "disabledReason", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "runningSince", + "fieldName": "runningSince", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lastNotifiedAt", + "fieldName": "lastNotifiedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "script_trigger_state_id_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "triggerId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "script_trigger_state_due_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "nextRunAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "script_user_property", + "dartName": "ScriptUserProperty", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "fieldName": "userId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileUuid", + "fieldName": "fileUuid", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "key", + "fieldName": "key", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "value", + "fieldName": "value", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "updatedAt", + "fieldName": "updatedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "script_user_property_fk_0", + "columns": [ + "userId" + ], + "referenceTable": "user_profile", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "script_user_property_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "fileUuid" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "key" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "server_change_record", + "dartName": "ServerChangeRecord", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "changeId", + "fieldName": "changeId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileUuid", + "fieldName": "fileUuid", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "fieldName": "userId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "actorKind", + "fieldName": "actorKind", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:ActorKind" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "label", + "fieldName": "label", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "inverseJson", + "fieldName": "inverseJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "inverseFileId", + "fieldName": "inverseFileId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "forwardFileId", + "fieldName": "forwardFileId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "recordId", + "fieldName": "recordId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "cellsAffected", + "fieldName": "cellsAffected", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "expiresAt", + "fieldName": "expiresAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revertedAt", + "fieldName": "revertedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revertChangeId", + "fieldName": "revertChangeId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "server_change_record_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "recordId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "server_change_file_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "fileUuid" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "createdAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "server_change_id_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "changeId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "share_link", + "dartName": "ShareLink", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "itemId", + "fieldName": "itemId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "token", + "fieldName": "token", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "role", + "fieldName": "role", + "columnType": 0, + "isNullable": true, + "dartType": "protocol:AccessRole?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdById", + "fieldName": "createdById", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "share_link_fk_0", + "columns": [ + "itemId" + ], + "referenceTable": "drive_item", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "share_link_item_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "itemId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "share_link_token_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "token" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "sheet", + "dartName": "Sheet", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "random_v7", + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileId", + "fieldName": "fileId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revision", + "fieldName": "revision", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "spreadsheetId", + "fieldName": "spreadsheetId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "name", + "fieldName": "name", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sortKey", + "fieldName": "sortKey", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "kind", + "fieldName": "kind", + "columnType": 0, + "isNullable": false, + "columnDefault": "'grid'", + "dartType": "protocol:SheetKind" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "hidden", + "fieldName": "hidden", + "columnType": 1, + "isNullable": false, + "columnDefault": "false", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "tabColor", + "fieldName": "tabColor", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "rightToLeft", + "fieldName": "rightToLeft", + "columnType": 1, + "isNullable": false, + "columnDefault": "false", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "showGridlines", + "fieldName": "showGridlines", + "columnType": 1, + "isNullable": false, + "columnDefault": "true", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "frozenRows", + "fieldName": "frozenRows", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "frozenColumns", + "fieldName": "frozenColumns", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "defaultRowHeight", + "fieldName": "defaultRowHeight", + "columnType": 3, + "isNullable": false, + "columnDefault": "21.0", + "dartType": "double" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "defaultColumnWidth", + "fieldName": "defaultColumnWidth", + "columnType": 3, + "isNullable": false, + "columnDefault": "100.0", + "dartType": "double" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "formatJson", + "fieldName": "formatJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "printSettingsJson", + "fieldName": "printSettingsJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "sheet_fk_0", + "columns": [ + "fileId" + ], + "referenceTable": "rev_file", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "sheet_fk_1", + "columns": [ + "spreadsheetId" + ], + "referenceTable": "spreadsheet", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4, + "deferrable": "initiallyDeferred" + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "sheet_file_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "fileId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "sheet_order_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "spreadsheetId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "sortKey" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "sheet_column", + "dartName": "SheetColumn", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "random_v7", + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileId", + "fieldName": "fileId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revision", + "fieldName": "revision", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sheetId", + "fieldName": "sheetId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sortKey", + "fieldName": "sortKey", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "width", + "fieldName": "width", + "columnType": 3, + "isNullable": true, + "dartType": "double?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "hidden", + "fieldName": "hidden", + "columnType": 1, + "isNullable": false, + "columnDefault": "false", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "formatJson", + "fieldName": "formatJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "sheet_column_fk_0", + "columns": [ + "fileId" + ], + "referenceTable": "rev_file", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "sheet_column_fk_1", + "columns": [ + "sheetId" + ], + "referenceTable": "sheet", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4, + "deferrable": "initiallyDeferred" + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "sheet_column_file_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "fileId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "sheet_column_order_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "sheetId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "sortKey" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "sheet_row", + "dartName": "SheetRow", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "random_v7", + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileId", + "fieldName": "fileId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revision", + "fieldName": "revision", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sheetId", + "fieldName": "sheetId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sortKey", + "fieldName": "sortKey", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "height", + "fieldName": "height", + "columnType": 3, + "isNullable": true, + "dartType": "double?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "hidden", + "fieldName": "hidden", + "columnType": 1, + "isNullable": false, + "columnDefault": "false", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "formatJson", + "fieldName": "formatJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "sheet_row_fk_0", + "columns": [ + "fileId" + ], + "referenceTable": "rev_file", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "sheet_row_fk_1", + "columns": [ + "sheetId" + ], + "referenceTable": "sheet", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4, + "deferrable": "initiallyDeferred" + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "sheet_row_file_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "fileId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "sheet_row_order_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "sheetId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "sortKey" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "sheets_import_job", + "dartName": "SheetsImportJob", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "uuid", + "fieldName": "uuid", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "fieldName": "userId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "authUserId", + "fieldName": "authUserId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "spreadsheetUuid", + "fieldName": "spreadsheetUuid", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "uploadFileId", + "fieldName": "uploadFileId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileName", + "fieldName": "fileName", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "format", + "fieldName": "format", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "optionsJson", + "fieldName": "optionsJson", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "activeSheetId", + "fieldName": "activeSheetId", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "status", + "fieldName": "status", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:SheetsImportStatus" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "progressDone", + "fieldName": "progressDone", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "progressTotal", + "fieldName": "progressTotal", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "reportJson", + "fieldName": "reportJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sheetIdsJson", + "fieldName": "sheetIdsJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "changeId", + "fieldName": "changeId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "errorCode", + "fieldName": "errorCode", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "error", + "fieldName": "error", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "cancelRequested", + "fieldName": "cancelRequested", + "columnType": 1, + "isNullable": false, + "columnDefault": "false", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "updatedAt", + "fieldName": "updatedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "leaseOwner", + "fieldName": "leaseOwner", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "leaseUntil", + "fieldName": "leaseUntil", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "sheets_import_job_uuid_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "uuid" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "sheets_import_job_user_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "status" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "sheets_snapshot_index", + "dartName": "SheetsSnapshotIndex", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "itemVersionId", + "fieldName": "itemVersionId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sheetsJson", + "fieldName": "sheetsJson", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "changedRangesJson", + "fieldName": "changedRangesJson", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "authorIds", + "fieldName": "authorIds", + "columnType": 8, + "isNullable": false, + "dartType": "List" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "diffFileId", + "fieldName": "diffFileId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "sheets_snapshot_index_fk_0", + "columns": [ + "itemVersionId" + ], + "referenceTable": "item_version", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "sheets_snapshot_index_version_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "itemVersionId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "slide", + "dartName": "Slide", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "random_v7", + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileId", + "fieldName": "fileId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revision", + "fieldName": "revision", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "deckId", + "fieldName": "deckId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sortKey", + "fieldName": "sortKey", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "groupId", + "fieldName": "groupId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "layoutId", + "fieldName": "layoutId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "backgroundJson", + "fieldName": "backgroundJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "skipped", + "fieldName": "skipped", + "columnType": 1, + "isNullable": false, + "columnDefault": "false", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "notes", + "fieldName": "notes", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "transitionJson", + "fieldName": "transitionJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "hideMasterElements", + "fieldName": "hideMasterElements", + "columnType": 1, + "isNullable": true, + "dartType": "bool?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "timingsJson", + "fieldName": "timingsJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "slide_fk_0", + "columns": [ + "fileId" + ], + "referenceTable": "rev_file", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "slide_fk_1", + "columns": [ + "deckId" + ], + "referenceTable": "deck", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4, + "deferrable": "initiallyDeferred" + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "slide_file_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "fileId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "slide_order_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "deckId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "sortKey" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "slide_build", + "dartName": "SlideBuild", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "random_v7", + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileId", + "fieldName": "fileId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revision", + "fieldName": "revision", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "slideId", + "fieldName": "slideId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "elementId", + "fieldName": "elementId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "kind", + "fieldName": "kind", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:BuildKind" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "effect", + "fieldName": "effect", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sortKey", + "fieldName": "sortKey", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "trigger", + "fieldName": "trigger", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:BuildTrigger" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "delayMs", + "fieldName": "delayMs", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "durationMs", + "fieldName": "durationMs", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "easing", + "fieldName": "easing", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "optionsJson", + "fieldName": "optionsJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "slide_build_fk_0", + "columns": [ + "fileId" + ], + "referenceTable": "rev_file", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "slide_build_fk_1", + "columns": [ + "slideId" + ], + "referenceTable": "slide", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4, + "deferrable": "initiallyDeferred" + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "slide_build_fk_2", + "columns": [ + "elementId" + ], + "referenceTable": "slide_element", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4, + "deferrable": "initiallyDeferred" + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "slide_build_file_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "fileId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "slide_build_slide_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "slideId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "sortKey" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "slide_build_element_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "elementId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "slide_element", + "dartName": "SlideElement", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "random_v7", + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileId", + "fieldName": "fileId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revision", + "fieldName": "revision", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "slideId", + "fieldName": "slideId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "layoutId", + "fieldName": "layoutId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "masterId", + "fieldName": "masterId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "parentId", + "fieldName": "parentId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "kind", + "fieldName": "kind", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:SlideElementKind" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "name", + "fieldName": "name", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "x", + "fieldName": "x", + "columnType": 3, + "isNullable": true, + "dartType": "double?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "y", + "fieldName": "y", + "columnType": 3, + "isNullable": true, + "dartType": "double?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "width", + "fieldName": "width", + "columnType": 3, + "isNullable": true, + "dartType": "double?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "height", + "fieldName": "height", + "columnType": 3, + "isNullable": true, + "dartType": "double?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "rotation", + "fieldName": "rotation", + "columnType": 3, + "isNullable": true, + "dartType": "double?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "flipH", + "fieldName": "flipH", + "columnType": 1, + "isNullable": false, + "columnDefault": "false", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "flipV", + "fieldName": "flipV", + "columnType": 1, + "isNullable": false, + "columnDefault": "false", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "zKey", + "fieldName": "zKey", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "readingKey", + "fieldName": "readingKey", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "locked", + "fieldName": "locked", + "columnType": 1, + "isNullable": false, + "columnDefault": "false", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "hidden", + "fieldName": "hidden", + "columnType": 1, + "isNullable": false, + "columnDefault": "false", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "placeholderType", + "fieldName": "placeholderType", + "columnType": 0, + "isNullable": true, + "dartType": "protocol:PlaceholderType?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "placeholderOf", + "fieldName": "placeholderOf", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "matchKey", + "fieldName": "matchKey", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "text", + "fieldName": "text", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "textStyleJson", + "fieldName": "textStyleJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fillJson", + "fieldName": "fillJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "strokeJson", + "fieldName": "strokeJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "effectsJson", + "fieldName": "effectsJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "opacity", + "fieldName": "opacity", + "columnType": 3, + "isNullable": true, + "dartType": "double?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "dataJson", + "fieldName": "dataJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "linkJson", + "fieldName": "linkJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "altText", + "fieldName": "altText", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "decorative", + "fieldName": "decorative", + "columnType": 1, + "isNullable": false, + "columnDefault": "false", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "slide_element_fk_0", + "columns": [ + "fileId" + ], + "referenceTable": "rev_file", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "slide_element_fk_1", + "columns": [ + "slideId" + ], + "referenceTable": "slide", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4, + "deferrable": "initiallyDeferred" + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "slide_element_fk_2", + "columns": [ + "layoutId" + ], + "referenceTable": "slide_layout", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4, + "deferrable": "initiallyDeferred" + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "slide_element_fk_3", + "columns": [ + "masterId" + ], + "referenceTable": "slide_master", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4, + "deferrable": "initiallyDeferred" + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "slide_element_fk_4", + "columns": [ + "parentId" + ], + "referenceTable": "slide_element", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4, + "deferrable": "initiallyDeferred" + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "slide_element_file_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "fileId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "slide_element_slide_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "slideId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "zKey" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "slide_element_layout_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "layoutId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "slide_element_master_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "masterId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "slide_element_parent_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "parentId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "slide_group", + "dartName": "SlideGroup", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "random_v7", + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileId", + "fieldName": "fileId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revision", + "fieldName": "revision", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "deckId", + "fieldName": "deckId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "title", + "fieldName": "title", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sortKey", + "fieldName": "sortKey", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "parentId", + "fieldName": "parentId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "color", + "fieldName": "color", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "skipped", + "fieldName": "skipped", + "columnType": 1, + "isNullable": false, + "columnDefault": "false", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "slide_group_fk_0", + "columns": [ + "fileId" + ], + "referenceTable": "rev_file", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "slide_group_fk_1", + "columns": [ + "deckId" + ], + "referenceTable": "deck", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4, + "deferrable": "initiallyDeferred" + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "slide_group_file_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "fileId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "slide_group_order_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "deckId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "sortKey" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "slide_layout", + "dartName": "SlideLayout", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "random_v7", + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileId", + "fieldName": "fileId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revision", + "fieldName": "revision", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "masterId", + "fieldName": "masterId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "name", + "fieldName": "name", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "key", + "fieldName": "key", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sortKey", + "fieldName": "sortKey", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "backgroundJson", + "fieldName": "backgroundJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "hideMasterElements", + "fieldName": "hideMasterElements", + "columnType": 1, + "isNullable": false, + "columnDefault": "false", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "hidden", + "fieldName": "hidden", + "columnType": 1, + "isNullable": false, + "columnDefault": "false", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "slide_layout_fk_0", + "columns": [ + "fileId" + ], + "referenceTable": "rev_file", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "slide_layout_fk_1", + "columns": [ + "masterId" + ], + "referenceTable": "slide_master", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4, + "deferrable": "initiallyDeferred" + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "slide_layout_file_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "fileId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "slide_layout_master_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "masterId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "slide_master", + "dartName": "SlideMaster", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "random_v7", + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileId", + "fieldName": "fileId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revision", + "fieldName": "revision", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "themeId", + "fieldName": "themeId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "name", + "fieldName": "name", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sortKey", + "fieldName": "sortKey", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "backgroundJson", + "fieldName": "backgroundJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "slide_master_fk_0", + "columns": [ + "fileId" + ], + "referenceTable": "rev_file", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "slide_master_fk_1", + "columns": [ + "themeId" + ], + "referenceTable": "deck_theme", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4, + "deferrable": "initiallyDeferred" + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "slide_master_file_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "fileId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "slide_master_theme_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "themeId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "slide_table_axis", + "dartName": "SlideTableAxis", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "random_v7", + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileId", + "fieldName": "fileId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revision", + "fieldName": "revision", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "elementId", + "fieldName": "elementId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "dimension", + "fieldName": "dimension", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:TableDimension" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sortKey", + "fieldName": "sortKey", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "size", + "fieldName": "size", + "columnType": 3, + "isNullable": true, + "dartType": "double?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "header", + "fieldName": "header", + "columnType": 1, + "isNullable": false, + "columnDefault": "false", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "slide_table_axis_fk_0", + "columns": [ + "fileId" + ], + "referenceTable": "rev_file", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "slide_table_axis_fk_1", + "columns": [ + "elementId" + ], + "referenceTable": "slide_element", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4, + "deferrable": "initiallyDeferred" + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "slide_table_axis_file_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "fileId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "slide_table_axis_element_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "elementId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "sortKey" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "slide_table_cell", + "dartName": "SlideTableCell", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "random_v7", + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileId", + "fieldName": "fileId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revision", + "fieldName": "revision", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "elementId", + "fieldName": "elementId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "rowId", + "fieldName": "rowId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "columnId", + "fieldName": "columnId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "text", + "fieldName": "text", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fill", + "fieldName": "fill", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "borderTop", + "fieldName": "borderTop", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "borderRight", + "fieldName": "borderRight", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "borderBottom", + "fieldName": "borderBottom", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "borderLeft", + "fieldName": "borderLeft", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "vAlign", + "fieldName": "vAlign", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "padding", + "fieldName": "padding", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "rowSpan", + "fieldName": "rowSpan", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "colSpan", + "fieldName": "colSpan", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "slide_table_cell_fk_0", + "columns": [ + "fileId" + ], + "referenceTable": "rev_file", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "slide_table_cell_fk_1", + "columns": [ + "elementId" + ], + "referenceTable": "slide_element", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4, + "deferrable": "initiallyDeferred" + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "slide_table_cell_fk_2", + "columns": [ + "rowId" + ], + "referenceTable": "slide_table_axis", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4, + "deferrable": "initiallyDeferred" + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "slide_table_cell_fk_3", + "columns": [ + "columnId" + ], + "referenceTable": "slide_table_axis", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4, + "deferrable": "initiallyDeferred" + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "slide_table_cell_file_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "fileId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "slide_table_cell_element_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "elementId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "slides_snapshot_index", + "dartName": "SlidesSnapshotIndex", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "itemVersionId", + "fieldName": "itemVersionId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "slidesJson", + "fieldName": "slidesJson", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "changesJson", + "fieldName": "changesJson", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "authorIds", + "fieldName": "authorIds", + "columnType": 8, + "isNullable": false, + "dartType": "List" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "slides_snapshot_index_fk_0", + "columns": [ + "itemVersionId" + ], + "referenceTable": "item_version", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "slides_snapshot_index_version_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "itemVersionId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "spreadsheet", + "dartName": "Spreadsheet", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "random_v7", + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileId", + "fieldName": "fileId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revision", + "fieldName": "revision", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "title", + "fieldName": "title", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "locale", + "fieldName": "locale", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "timeZone", + "fieldName": "timeZone", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "recalcMode", + "fieldName": "recalcMode", + "columnType": 0, + "isNullable": false, + "columnDefault": "'onChange'", + "dartType": "protocol:RecalcMode" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "iterativeCalc", + "fieldName": "iterativeCalc", + "columnType": 1, + "isNullable": false, + "columnDefault": "false", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "maxIterations", + "fieldName": "maxIterations", + "columnType": 6, + "isNullable": false, + "columnDefault": "50", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "convergenceThreshold", + "fieldName": "convergenceThreshold", + "columnType": 3, + "isNullable": false, + "columnDefault": "0.05", + "dartType": "double" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "defaultFontFamily", + "fieldName": "defaultFontFamily", + "columnType": 0, + "isNullable": false, + "columnDefault": "'Inter'", + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "defaultFontSize", + "fieldName": "defaultFontSize", + "columnType": 3, + "isNullable": false, + "columnDefault": "10.0", + "dartType": "double" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "themeJson", + "fieldName": "themeJson", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "displayLanguage", + "fieldName": "displayLanguage", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdBy", + "fieldName": "createdBy", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "spreadsheet_fk_0", + "columns": [ + "fileId" + ], + "referenceTable": "rev_file", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "spreadsheet_file_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "fileId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "storage_reservation", + "dartName": "StorageReservation", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "fieldName": "userId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileUuid", + "fieldName": "fileUuid", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "itemId", + "fieldName": "itemId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "bytes", + "fieldName": "bytes", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "expiresAt", + "fieldName": "expiresAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "storage_reservation_fk_0", + "columns": [ + "userId" + ], + "referenceTable": "user_profile", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "storage_reservation_fk_1", + "columns": [ + "itemId" + ], + "referenceTable": "drive_item", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "storage_reservation_file_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "fileUuid" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "storage_reservation_user_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "storage_reservation_expiry_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "expiresAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "stored_file", + "dartName": "StoredFile", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileUuid", + "fieldName": "fileUuid", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "ownerId", + "fieldName": "ownerId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "itemId", + "fieldName": "itemId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "kind", + "fieldName": "kind", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:StoredFileKind" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "storageId", + "fieldName": "storageId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "path", + "fieldName": "path", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileName", + "fieldName": "fileName", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "mimeType", + "fieldName": "mimeType", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sizeBytes", + "fieldName": "sizeBytes", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "status", + "fieldName": "status", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:StoredFileStatus" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "completedAt", + "fieldName": "completedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "expiresAt", + "fieldName": "expiresAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "stored_file_fk_0", + "columns": [ + "ownerId" + ], + "referenceTable": "user_profile", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 0 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "stored_file_fk_1", + "columns": [ + "itemId" + ], + "referenceTable": "drive_item", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "stored_file_uuid_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "fileUuid" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "stored_file_item_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "itemId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "stored_file_expiry_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "expiresAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "stored_file_owner_kind_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "ownerId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "kind" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "createdAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "stored_file_status_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "status" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "createdAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "submission", + "dartName": "Submission", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "uuid", + "fieldName": "uuid", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "publicationId", + "fieldName": "publicationId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "itemId", + "fieldName": "itemId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "ref", + "fieldName": "ref", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "status", + "fieldName": "status", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:SubmissionStatus" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "statusBeforeSubmit", + "fieldName": "statusBeforeSubmit", + "columnType": 0, + "isNullable": true, + "dartType": "protocol:SubmissionStatus?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "delivery", + "fieldName": "delivery", + "columnType": 0, + "isNullable": true, + "dartType": "protocol:SubmissionDeliveryState?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "values", + "fieldName": "values", + "columnType": 8, + "isNullable": false, + "dartType": "protocol:SubmissionValues" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "publicUrl", + "fieldName": "publicUrl", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "feedbackThreadId", + "fieldName": "feedbackThreadId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdById", + "fieldName": "createdById", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "updatedAt", + "fieldName": "updatedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "disconnectedAt", + "fieldName": "disconnectedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "submission_fk_0", + "columns": [ + "publicationId" + ], + "referenceTable": "publication", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "submission_fk_1", + "columns": [ + "itemId" + ], + "referenceTable": "drive_item", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "submission_uuid_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "uuid" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "submission_item_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "itemId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "submission_publication_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "publicationId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "status" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "submission_asset", + "dartName": "SubmissionAsset", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "submissionId", + "fieldName": "submissionId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileUuid", + "fieldName": "fileUuid", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "submission_asset_fk_0", + "columns": [ + "submissionId" + ], + "referenceTable": "submission", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "submission_asset_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "submissionId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "fileUuid" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "submission_asset_file_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "fileUuid" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "submission_revision", + "dartName": "SubmissionRevision", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "submissionId", + "fieldName": "submissionId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sequence", + "fieldName": "sequence", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revision", + "fieldName": "revision", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "versionId", + "fieldName": "versionId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "values", + "fieldName": "values", + "columnType": 8, + "isNullable": false, + "dartType": "protocol:SubmissionValues" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "payload", + "fieldName": "payload", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "submittedById", + "fieldName": "submittedById", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "submittedAt", + "fieldName": "submittedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "submission_revision_fk_0", + "columns": [ + "submissionId" + ], + "referenceTable": "submission", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "submission_revision_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "submissionId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "sequence" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "usage_counter_day", + "dartName": "UsageCounterDay", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "day", + "fieldName": "day", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "name", + "fieldName": "name", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "value", + "fieldName": "value", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "usage_counter_day_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "day" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "name" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "usage_day_stats", + "dartName": "UsageDayStats", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "day", + "fieldName": "day", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "includeAdmins", + "fieldName": "includeAdmins", + "columnType": 1, + "isNullable": false, + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "active", + "fieldName": "active", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "editors", + "fieldName": "editors", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "drive", + "fieldName": "drive", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "write", + "fieldName": "write", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sheets", + "fieldName": "sheets", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "slides", + "fieldName": "slides", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "mcp", + "fieldName": "mcp", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "weekly", + "fieldName": "weekly", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "monthly", + "fieldName": "monthly", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "computedAt", + "fieldName": "computedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "usage_day_stats_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "day" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "includeAdmins" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "user_activity_day", + "dartName": "UserActivityDay", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "fieldName": "userId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "day", + "fieldName": "day", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "app", + "fieldName": "app", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:UsageApp" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "edited", + "fieldName": "edited", + "columnType": 1, + "isNullable": false, + "columnDefault": "false", + "dartType": "bool" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "user_activity_day_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "day" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "app" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "user_activity_day_day_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "day" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "user_activity_settings", + "dartName": "UserActivitySettings", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "fieldName": "userId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "showViewHistory", + "fieldName": "showViewHistory", + "columnType": 1, + "isNullable": false, + "columnDefault": "true", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "updatedAt", + "fieldName": "updatedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "user_activity_settings_fk_0", + "columns": [ + "userId" + ], + "referenceTable": "user_profile", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "user_activity_settings_user_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "user_preferences", + "dartName": "UserPreferences", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "fieldName": "userId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "appTheme", + "fieldName": "appTheme", + "columnType": 0, + "isNullable": false, + "columnDefault": "'system'", + "dartType": "protocol:AppThemeMode" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "accentColor", + "fieldName": "accentColor", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "density", + "fieldName": "density", + "columnType": 0, + "isNullable": false, + "columnDefault": "'comfortable'", + "dartType": "protocol:UiDensity" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "reduceMotion", + "fieldName": "reduceMotion", + "columnType": 1, + "isNullable": false, + "columnDefault": "false", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "keymapOverrides", + "fieldName": "keymapOverrides", + "columnType": 0, + "isNullable": false, + "columnDefault": "'{}'", + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "writeDefaults", + "fieldName": "writeDefaults", + "columnType": 0, + "isNullable": false, + "columnDefault": "'{}'", + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sheetsDefaults", + "fieldName": "sheetsDefaults", + "columnType": 0, + "isNullable": false, + "columnDefault": "'{}'", + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "slidesDefaults", + "fieldName": "slidesDefaults", + "columnType": 0, + "isNullable": false, + "columnDefault": "'{}'", + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "slidesPresenter", + "fieldName": "slidesPresenter", + "columnType": 0, + "isNullable": false, + "columnDefault": "'{}'", + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "driveDefaults", + "fieldName": "driveDefaults", + "columnType": 0, + "isNullable": false, + "columnDefault": "'{}'", + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "notificationSettings", + "fieldName": "notificationSettings", + "columnType": 0, + "isNullable": false, + "columnDefault": "'{}'", + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "aiEnabled", + "fieldName": "aiEnabled", + "columnType": 1, + "isNullable": false, + "columnDefault": "true", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "updatedAt", + "fieldName": "updatedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "user_preferences_fk_0", + "columns": [ + "userId" + ], + "referenceTable": "user_profile", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "user_preferences_user_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "user_profile", + "dartName": "UserProfile", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "authUserId", + "fieldName": "authUserId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "displayName", + "fieldName": "displayName", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "email", + "fieldName": "email", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "avatarFileId", + "fieldName": "avatarFileId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "avatarColor", + "fieldName": "avatarColor", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "locale", + "fieldName": "locale", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "timeZone", + "fieldName": "timeZone", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lastSeenAt", + "fieldName": "lastSeenAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "welcomeContentAt", + "fieldName": "welcomeContentAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "user_profile_fk_0", + "columns": [ + "authUserId" + ], + "referenceTable": "serverpod_auth_core_user", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "user_profile_auth_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "authUserId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "user_profile_email_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "email" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "user_profile_created_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "createdAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "user_storage", + "dartName": "UserStorage", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "fieldName": "userId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "usedBytes", + "fieldName": "usedBytes", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "reservedBytes", + "fieldName": "reservedBytes", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "documentsBytes", + "fieldName": "documentsBytes", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "spreadsheetsBytes", + "fieldName": "spreadsheetsBytes", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "presentationsBytes", + "fieldName": "presentationsBytes", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "filesBytes", + "fieldName": "filesBytes", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "trashBytes", + "fieldName": "trashBytes", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "quotaBytesOverride", + "fieldName": "quotaBytesOverride", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "overQuotaSince", + "fieldName": "overQuotaSince", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "reconciledAt", + "fieldName": "reconciledAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "updatedAt", + "fieldName": "updatedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "user_storage_fk_0", + "columns": [ + "userId" + ], + "referenceTable": "user_profile", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "user_storage_user_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "write_diff_cache", + "dartName": "WriteDiffCache", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "documentId", + "fieldName": "documentId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fromRevision", + "fieldName": "fromRevision", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "toRevision", + "fieldName": "toRevision", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "json", + "fieldName": "json", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "write_diff_cache_fk_0", + "columns": [ + "documentId" + ], + "referenceTable": "document", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "write_diff_cache_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "documentId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "fromRevision" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "toRevision" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "write_diff_cache_created_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "createdAt" + } + ], + "type": "brin", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "write_theme", + "dartName": "WriteTheme", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "uuid", + "fieldName": "uuid", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "ownerId", + "fieldName": "ownerId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "isBuiltin", + "fieldName": "isBuiltin", + "columnType": 1, + "isNullable": false, + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "builtinKey", + "fieldName": "builtinKey", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "name", + "fieldName": "name", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "description", + "fieldName": "description", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "schemaVersion", + "fieldName": "schemaVersion", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "version", + "fieldName": "version", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "json", + "fieldName": "json", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "contentHash", + "fieldName": "contentHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "updatedAt", + "fieldName": "updatedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "deletedAt", + "fieldName": "deletedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "write_theme_fk_0", + "columns": [ + "ownerId" + ], + "referenceTable": "user_profile", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "write_theme_uuid_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "uuid" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "write_theme_owner_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "ownerId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "deletedAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "write_theme_builtin_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "builtinKey" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "write_theme_revision", + "dartName": "WriteThemeRevision", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "themeId", + "fieldName": "themeId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "version", + "fieldName": "version", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "json", + "fieldName": "json", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "savedById", + "fieldName": "savedById", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "actorKind", + "fieldName": "actorKind", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:ActorKind" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "write_theme_revision_fk_0", + "columns": [ + "themeId" + ], + "referenceTable": "write_theme", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "write_theme_rev_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "themeId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "version" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "serverpod_cloud_storage", + "dartName": "CloudStorageEntry", + "module": "serverpod", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "storageId", + "fieldName": "storageId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "path", + "fieldName": "path", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "addedTime", + "fieldName": "addedTime", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "expiration", + "fieldName": "expiration", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "byteData", + "fieldName": "byteData", + "columnType": 5, + "isNullable": false, + "dartType": "dart:typed_data:ByteData" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "verified", + "fieldName": "verified", + "columnType": 1, + "isNullable": false, + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "contentType", + "fieldName": "contentType", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "cacheControl", + "fieldName": "cacheControl", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "contentDisposition", + "fieldName": "contentDisposition", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "contentEncoding", + "fieldName": "contentEncoding", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "customMetadata", + "fieldName": "customMetadata", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_cloud_storage_path_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "storageId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "path" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_cloud_storage_expiration", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "expiration" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "serverpod_cloud_storage_direct_download", + "dartName": "CloudStorageDirectDownloadEntry", + "module": "serverpod", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "storageId", + "fieldName": "storageId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "path", + "fieldName": "path", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "expiration", + "fieldName": "expiration", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "authKey", + "fieldName": "authKey", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "downloadFileName", + "fieldName": "downloadFileName", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "contentType", + "fieldName": "contentType", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_cloud_storage_direct_download_auth_key", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "authKey" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_cloud_storage_direct_download_expiration", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "expiration" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "serverpod_cloud_storage_direct_upload", + "dartName": "CloudStorageDirectUploadEntry", + "module": "serverpod", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "storageId", + "fieldName": "storageId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "path", + "fieldName": "path", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "expiration", + "fieldName": "expiration", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "authKey", + "fieldName": "authKey", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "maxFileSize", + "fieldName": "maxFileSize", + "columnType": 6, + "isNullable": false, + "columnDefault": "10485760", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "contentLength", + "fieldName": "contentLength", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "preventOverwrite", + "fieldName": "preventOverwrite", + "columnType": 1, + "isNullable": false, + "columnDefault": "false", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "contentType", + "fieldName": "contentType", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "cacheControl", + "fieldName": "cacheControl", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "contentDisposition", + "fieldName": "contentDisposition", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "contentEncoding", + "fieldName": "contentEncoding", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "customMetadata", + "fieldName": "customMetadata", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_cloud_storage_direct_upload_storage_path", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "storageId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "path" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "serverpod_future_call", + "dartName": "FutureCallEntry", + "module": "serverpod", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "name", + "fieldName": "name", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "time", + "fieldName": "time", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "serializedObject", + "fieldName": "serializedObject", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "serverId", + "fieldName": "serverId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "identifier", + "fieldName": "identifier", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "scheduling", + "fieldName": "scheduling", + "columnType": 8, + "isNullable": true, + "dartType": "protocol:FutureCallScheduling?" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_future_call_time_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "time" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_future_call_serverId_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "serverId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_future_call_identifier_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "identifier" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "serverpod_future_call_claim", + "dartName": "FutureCallClaimEntry", + "module": "serverpod", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "futureCallId", + "fieldName": "futureCallId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lastHeartbeatTime", + "fieldName": "lastHeartbeatTime", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "serverpod_future_call_claim_fk_0", + "columns": [ + "futureCallId" + ], + "referenceTable": "serverpod_future_call", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "future_call_unique_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "futureCallId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "serverpod_health_connection_info", + "dartName": "ServerHealthConnectionInfo", + "module": "serverpod", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "serverId", + "fieldName": "serverId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "timestamp", + "fieldName": "timestamp", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "active", + "fieldName": "active", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "closing", + "fieldName": "closing", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "idle", + "fieldName": "idle", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "granularity", + "fieldName": "granularity", + "columnType": 6, + "isNullable": false, + "dartType": "int" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_health_connection_info_timestamp_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "timestamp" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "serverId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "granularity" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "serverpod_health_metric", + "dartName": "ServerHealthMetric", + "module": "serverpod", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "name", + "fieldName": "name", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "serverId", + "fieldName": "serverId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "timestamp", + "fieldName": "timestamp", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "isHealthy", + "fieldName": "isHealthy", + "columnType": 1, + "isNullable": false, + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "value", + "fieldName": "value", + "columnType": 3, + "isNullable": false, + "dartType": "double" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "granularity", + "fieldName": "granularity", + "columnType": 6, + "isNullable": false, + "dartType": "int" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_health_metric_timestamp_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "timestamp" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "serverId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "name" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "granularity" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "serverpod_log", + "dartName": "LogEntry", + "module": "serverpod", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sessionLogId", + "fieldName": "sessionLogId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "messageId", + "fieldName": "messageId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "reference", + "fieldName": "reference", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "serverId", + "fieldName": "serverId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "time", + "fieldName": "time", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "logLevel", + "fieldName": "logLevel", + "columnType": 6, + "isNullable": false, + "dartType": "protocol:LogLevel" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "message", + "fieldName": "message", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "error", + "fieldName": "error", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "stackTrace", + "fieldName": "stackTrace", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "order", + "fieldName": "order", + "columnType": 6, + "isNullable": false, + "dartType": "int" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "serverpod_log_fk_0", + "columns": [ + "sessionLogId" + ], + "referenceTable": "serverpod_session_log", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_log_sessionLogId_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "sessionLogId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "order" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "serverpod_message_log", + "dartName": "MessageLogEntry", + "module": "serverpod", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sessionLogId", + "fieldName": "sessionLogId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "serverId", + "fieldName": "serverId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "messageId", + "fieldName": "messageId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "endpoint", + "fieldName": "endpoint", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "messageName", + "fieldName": "messageName", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "duration", + "fieldName": "duration", + "columnType": 3, + "isNullable": false, + "dartType": "double" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "error", + "fieldName": "error", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "stackTrace", + "fieldName": "stackTrace", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "slow", + "fieldName": "slow", + "columnType": 1, + "isNullable": false, + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "order", + "fieldName": "order", + "columnType": 6, + "isNullable": false, + "dartType": "int" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "serverpod_message_log_fk_0", + "columns": [ + "sessionLogId" + ], + "referenceTable": "serverpod_session_log", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_message_log_sessionLogId_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "sessionLogId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "order" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "serverpod_method", + "dartName": "MethodInfo", + "module": "serverpod", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "endpoint", + "fieldName": "endpoint", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "method", + "fieldName": "method", + "columnType": 0, + "isNullable": false, + "dartType": "String" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_method_endpoint_method_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "endpoint" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "method" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "serverpod_migrations", + "dartName": "DatabaseMigrationVersion", + "module": "serverpod", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "module", + "fieldName": "module", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "version", + "fieldName": "version", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "timestamp", + "fieldName": "timestamp", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_migrations_ids", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "module" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "serverpod_query_log", + "dartName": "QueryLogEntry", + "module": "serverpod", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "serverId", + "fieldName": "serverId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sessionLogId", + "fieldName": "sessionLogId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "messageId", + "fieldName": "messageId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "query", + "fieldName": "query", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "duration", + "fieldName": "duration", + "columnType": 3, + "isNullable": false, + "dartType": "double" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "numRows", + "fieldName": "numRows", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "error", + "fieldName": "error", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "stackTrace", + "fieldName": "stackTrace", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "slow", + "fieldName": "slow", + "columnType": 1, + "isNullable": false, + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "order", + "fieldName": "order", + "columnType": 6, + "isNullable": false, + "dartType": "int" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "serverpod_query_log_fk_0", + "columns": [ + "sessionLogId" + ], + "referenceTable": "serverpod_session_log", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_query_log_sessionLogId_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "sessionLogId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "order" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "serverpod_readwrite_test", + "dartName": "ReadWriteTestEntry", + "module": "serverpod", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "number", + "fieldName": "number", + "columnType": 6, + "isNullable": false, + "dartType": "int" + } + ], + "foreignKeys": [], + "indexes": [], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "serverpod_runtime_settings", + "dartName": "RuntimeSettings", + "module": "serverpod", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "logSettings", + "fieldName": "logSettings", + "columnType": 8, + "isNullable": false, + "dartType": "protocol:LogSettings" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "logSettingsOverrides", + "fieldName": "logSettingsOverrides", + "columnType": 8, + "isNullable": false, + "dartType": "List" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "logServiceCalls", + "fieldName": "logServiceCalls", + "columnType": 1, + "isNullable": false, + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "logMalformedCalls", + "fieldName": "logMalformedCalls", + "columnType": 1, + "isNullable": false, + "dartType": "bool" + } + ], + "foreignKeys": [], + "indexes": [], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "serverpod_session_log", + "dartName": "SessionLogEntry", + "module": "serverpod", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "serverId", + "fieldName": "serverId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "time", + "fieldName": "time", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "module", + "fieldName": "module", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "endpoint", + "fieldName": "endpoint", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "method", + "fieldName": "method", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "duration", + "fieldName": "duration", + "columnType": 3, + "isNullable": true, + "dartType": "double?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "numQueries", + "fieldName": "numQueries", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "slow", + "fieldName": "slow", + "columnType": 1, + "isNullable": true, + "dartType": "bool?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "error", + "fieldName": "error", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "stackTrace", + "fieldName": "stackTrace", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "authenticatedUserId", + "fieldName": "authenticatedUserId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "fieldName": "userId", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "isOpen", + "fieldName": "isOpen", + "columnType": 1, + "isNullable": true, + "dartType": "bool?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "touched", + "fieldName": "touched", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_session_log_serverid_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "serverId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_session_log_time_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "time" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_session_log_touched_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "touched" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_session_log_isopen_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "isOpen" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "serverpod_auth_idp_anonymous_account", + "dartName": "AnonymousAccount", + "module": "serverpod_auth_idp", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "random_v7", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "authUserId", + "fieldName": "authUserId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "serverpod_auth_idp_anonymous_account_fk_0", + "columns": [ + "authUserId" + ], + "referenceTable": "serverpod_auth_core_user", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "serverpod_auth_idp_apple_account", + "dartName": "AppleAccount", + "module": "serverpod_auth_idp", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "random_v7", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userIdentifier", + "fieldName": "userIdentifier", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "refreshToken", + "fieldName": "refreshToken", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "refreshTokenRequestedWithBundleIdentifier", + "fieldName": "refreshTokenRequestedWithBundleIdentifier", + "columnType": 1, + "isNullable": false, + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lastRefreshedAt", + "fieldName": "lastRefreshedAt", + "columnType": 4, + "isNullable": false, + "columnDefault": "now", + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "authUserId", + "fieldName": "authUserId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "email", + "fieldName": "email", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "isEmailVerified", + "fieldName": "isEmailVerified", + "columnType": 1, + "isNullable": true, + "dartType": "bool?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "isPrivateEmail", + "fieldName": "isPrivateEmail", + "columnType": 1, + "isNullable": true, + "dartType": "bool?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "firstName", + "fieldName": "firstName", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lastName", + "fieldName": "lastName", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "serverpod_auth_idp_apple_account_fk_0", + "columns": [ + "authUserId" + ], + "referenceTable": "serverpod_auth_core_user", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_auth_apple_account_identifier", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userIdentifier" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "serverpod_auth_idp_email_account", + "dartName": "EmailAccount", + "module": "serverpod_auth_idp", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "random_v7", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "authUserId", + "fieldName": "authUserId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "email", + "fieldName": "email", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "passwordHash", + "fieldName": "passwordHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "serverpod_auth_idp_email_account_fk_0", + "columns": [ + "authUserId" + ], + "referenceTable": "serverpod_auth_core_user", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_auth_idp_email_account_email", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "email" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "serverpod_auth_idp_email_account_otp_login_request", + "dartName": "EmailAccountOtpLoginRequest", + "module": "serverpod_auth_idp", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "random_v7", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "columnDefault": "now", + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "email", + "fieldName": "email", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "challengeId", + "fieldName": "challengeId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "completeChallengeId", + "fieldName": "completeChallengeId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "serverpod_auth_idp_email_account_otp_login_request_fk_0", + "columns": [ + "challengeId" + ], + "referenceTable": "serverpod_auth_idp_secret_challenge", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "serverpod_auth_idp_email_account_otp_login_request_fk_1", + "columns": [ + "completeChallengeId" + ], + "referenceTable": "serverpod_auth_idp_secret_challenge", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_auth_idp_email_account_otp_login_request_email", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "email" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "serverpod_auth_idp_email_account_password_reset_request", + "dartName": "EmailAccountPasswordResetRequest", + "module": "serverpod_auth_idp", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "random_v7", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "emailAccountId", + "fieldName": "emailAccountId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "columnDefault": "now", + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "challengeId", + "fieldName": "challengeId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "setPasswordChallengeId", + "fieldName": "setPasswordChallengeId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "serverpod_auth_idp_email_account_password_reset_request_fk_0", + "columns": [ + "emailAccountId" + ], + "referenceTable": "serverpod_auth_idp_email_account", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "serverpod_auth_idp_email_account_password_reset_request_fk_1", + "columns": [ + "challengeId" + ], + "referenceTable": "serverpod_auth_idp_secret_challenge", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "serverpod_auth_idp_email_account_password_reset_request_fk_2", + "columns": [ + "setPasswordChallengeId" + ], + "referenceTable": "serverpod_auth_idp_secret_challenge", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "serverpod_auth_idp_email_account_request", + "dartName": "EmailAccountRequest", + "module": "serverpod_auth_idp", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "random_v7", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "columnDefault": "now", + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "email", + "fieldName": "email", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "challengeId", + "fieldName": "challengeId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createAccountChallengeId", + "fieldName": "createAccountChallengeId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "serverpod_auth_idp_email_account_request_fk_0", + "columns": [ + "challengeId" + ], + "referenceTable": "serverpod_auth_idp_secret_challenge", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "serverpod_auth_idp_email_account_request_fk_1", + "columns": [ + "createAccountChallengeId" + ], + "referenceTable": "serverpod_auth_idp_secret_challenge", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_auth_idp_email_account_request_email", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "email" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "serverpod_auth_idp_facebook_account", + "dartName": "FacebookAccount", + "module": "serverpod_auth_idp", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "random_v7", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "authUserId", + "fieldName": "authUserId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userIdentifier", + "fieldName": "userIdentifier", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "email", + "fieldName": "email", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fullName", + "fieldName": "fullName", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "firstName", + "fieldName": "firstName", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lastName", + "fieldName": "lastName", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "serverpod_auth_idp_facebook_account_fk_0", + "columns": [ + "authUserId" + ], + "referenceTable": "serverpod_auth_core_user", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_auth_facebook_account_user_identifier", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userIdentifier" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "serverpod_auth_idp_firebase_account", + "dartName": "FirebaseAccount", + "module": "serverpod_auth_idp", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "random_v7", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "authUserId", + "fieldName": "authUserId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "created", + "fieldName": "created", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "email", + "fieldName": "email", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "phone", + "fieldName": "phone", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userIdentifier", + "fieldName": "userIdentifier", + "columnType": 0, + "isNullable": false, + "dartType": "String" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "serverpod_auth_idp_firebase_account_fk_0", + "columns": [ + "authUserId" + ], + "referenceTable": "serverpod_auth_core_user", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_auth_firebase_account_user_identifier", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userIdentifier" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "serverpod_auth_idp_github_account", + "dartName": "GitHubAccount", + "module": "serverpod_auth_idp", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "random_v7", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "authUserId", + "fieldName": "authUserId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userIdentifier", + "fieldName": "userIdentifier", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "email", + "fieldName": "email", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "created", + "fieldName": "created", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "serverpod_auth_idp_github_account_fk_0", + "columns": [ + "authUserId" + ], + "referenceTable": "serverpod_auth_core_user", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_auth_github_account_user_identifier", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userIdentifier" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "serverpod_auth_idp_google_account", + "dartName": "GoogleAccount", + "module": "serverpod_auth_idp", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "random_v7", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "authUserId", + "fieldName": "authUserId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "created", + "fieldName": "created", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "email", + "fieldName": "email", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userIdentifier", + "fieldName": "userIdentifier", + "columnType": 0, + "isNullable": false, + "dartType": "String" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "serverpod_auth_idp_google_account_fk_0", + "columns": [ + "authUserId" + ], + "referenceTable": "serverpod_auth_core_user", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_auth_google_account_user_identifier", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userIdentifier" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "serverpod_auth_idp_microsoft_account", + "dartName": "MicrosoftAccount", + "module": "serverpod_auth_idp", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "random_v7", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "authUserId", + "fieldName": "authUserId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userIdentifier", + "fieldName": "userIdentifier", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "email", + "fieldName": "email", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "created", + "fieldName": "created", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "serverpod_auth_idp_microsoft_account_fk_0", + "columns": [ + "authUserId" + ], + "referenceTable": "serverpod_auth_core_user", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_auth_microsoft_account_user_identifier", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userIdentifier" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "serverpod_auth_idp_passkey_account", + "dartName": "PasskeyAccount", + "module": "serverpod_auth_idp", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "random_v7", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "authUserId", + "fieldName": "authUserId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "keyId", + "fieldName": "keyId", + "columnType": 5, + "isNullable": false, + "dartType": "dart:typed_data:ByteData" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "keyIdBase64", + "fieldName": "keyIdBase64", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "clientDataJSON", + "fieldName": "clientDataJSON", + "columnType": 5, + "isNullable": false, + "dartType": "dart:typed_data:ByteData" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "attestationObject", + "fieldName": "attestationObject", + "columnType": 5, + "isNullable": false, + "dartType": "dart:typed_data:ByteData" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "originalChallenge", + "fieldName": "originalChallenge", + "columnType": 5, + "isNullable": false, + "dartType": "dart:typed_data:ByteData" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "serverpod_auth_idp_passkey_account_fk_0", + "columns": [ + "authUserId" + ], + "referenceTable": "serverpod_auth_core_user", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_auth_idp_passkey_account_key_id_base64", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "keyIdBase64" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "serverpod_auth_idp_passkey_challenge", + "dartName": "PasskeyChallenge", + "module": "serverpod_auth_idp", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "random_v7", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "challenge", + "fieldName": "challenge", + "columnType": 5, + "isNullable": false, + "dartType": "dart:typed_data:ByteData" + } + ], + "foreignKeys": [], + "indexes": [], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "serverpod_auth_idp_rate_limited_request_attempt", + "dartName": "RateLimitedRequestAttempt", + "module": "serverpod_auth_idp", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "random_v7", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "domain", + "fieldName": "domain", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "source", + "fieldName": "source", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "key", + "fieldName": "key", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "ipAddress", + "fieldName": "ipAddress", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "attemptedAt", + "fieldName": "attemptedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "extraData", + "fieldName": "extraData", + "columnType": 8, + "isNullable": true, + "dartType": "Map?" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_auth_idp_rate_limited_request_attempt_composite", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "domain" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "source" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "key" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "attemptedAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "serverpod_auth_idp_secret_challenge", + "dartName": "SecretChallenge", + "module": "serverpod_auth_idp", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "random_v7", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "challengeCodeHash", + "fieldName": "challengeCodeHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + } + ], + "foreignKeys": [], + "indexes": [], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "serverpod_auth_core_jwt_refresh_token", + "dartName": "RefreshToken", + "module": "serverpod_auth_core", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "random_v7", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "authUserId", + "fieldName": "authUserId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "scopeNames", + "fieldName": "scopeNames", + "columnType": 8, + "isNullable": false, + "dartType": "Set" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "extraClaims", + "fieldName": "extraClaims", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "method", + "fieldName": "method", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fixedSecret", + "fieldName": "fixedSecret", + "columnType": 5, + "isNullable": false, + "dartType": "dart:typed_data:ByteData" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "rotatingSecretHash", + "fieldName": "rotatingSecretHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lastUpdatedAt", + "fieldName": "lastUpdatedAt", + "columnType": 4, + "isNullable": false, + "columnDefault": "now", + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "columnDefault": "now", + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "serverpod_auth_core_jwt_refresh_token_fk_0", + "columns": [ + "authUserId" + ], + "referenceTable": "serverpod_auth_core_user", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_auth_core_jwt_refresh_token_last_updated_at", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "lastUpdatedAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "serverpod_auth_core_profile", + "dartName": "UserProfile", + "module": "serverpod_auth_core", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "random_v7", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "authUserId", + "fieldName": "authUserId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userName", + "fieldName": "userName", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fullName", + "fieldName": "fullName", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "email", + "fieldName": "email", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "columnDefault": "now", + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "imageId", + "fieldName": "imageId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "serverpod_auth_core_profile_fk_0", + "columns": [ + "authUserId" + ], + "referenceTable": "serverpod_auth_core_user", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "serverpod_auth_core_profile_fk_1", + "columns": [ + "imageId" + ], + "referenceTable": "serverpod_auth_core_profile_image", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3, + "deferrable": "initiallyDeferred" + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_auth_profile_user_profile_email_auth_user_id", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "authUserId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "serverpod_auth_core_profile_image", + "dartName": "UserProfileImage", + "module": "serverpod_auth_core", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "random_v7", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userProfileId", + "fieldName": "userProfileId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "columnDefault": "now", + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "storageId", + "fieldName": "storageId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "path", + "fieldName": "path", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "url", + "fieldName": "url", + "columnType": 0, + "isNullable": false, + "dartType": "Uri" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "serverpod_auth_core_profile_image_fk_0", + "columns": [ + "userProfileId" + ], + "referenceTable": "serverpod_auth_core_profile", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "serverpod_auth_core_session", + "dartName": "ServerSideSession", + "module": "serverpod_auth_core", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "random_v7", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "authUserId", + "fieldName": "authUserId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "scopeNames", + "fieldName": "scopeNames", + "columnType": 8, + "isNullable": false, + "dartType": "Set" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "columnDefault": "now", + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lastUsedAt", + "fieldName": "lastUsedAt", + "columnType": 4, + "isNullable": false, + "columnDefault": "now", + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "expiresAt", + "fieldName": "expiresAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "expireAfterUnusedFor", + "fieldName": "expireAfterUnusedFor", + "columnType": 6, + "isNullable": true, + "dartType": "Duration?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sessionKeyHash", + "fieldName": "sessionKeyHash", + "columnType": 5, + "isNullable": false, + "dartType": "dart:typed_data:ByteData" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sessionKeySalt", + "fieldName": "sessionKeySalt", + "columnType": 5, + "isNullable": false, + "dartType": "dart:typed_data:ByteData" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "method", + "fieldName": "method", + "columnType": 0, + "isNullable": false, + "dartType": "String" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "serverpod_auth_core_session_fk_0", + "columns": [ + "authUserId" + ], + "referenceTable": "serverpod_auth_core_user", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "serverpod_auth_core_user", + "dartName": "AuthUser", + "module": "serverpod_auth_core", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "random_v7", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "scopeNames", + "fieldName": "scopeNames", + "columnType": 8, + "isNullable": false, + "dartType": "Set" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "blocked", + "fieldName": "blocked", + "columnType": 1, + "isNullable": false, + "dartType": "bool" + } + ], + "foreignKeys": [], + "indexes": [], + "managed": true + } + ], + "installedModules": [ + { + "__className__": "serverpod.DatabaseMigrationVersionModel", + "module": "wonky", + "version": "20261025085716775-serverpod-4-1-rc1" + }, + { + "__className__": "serverpod.DatabaseMigrationVersion", + "module": "serverpod", + "version": "20260824182259319" + }, + { + "__className__": "serverpod.DatabaseMigrationVersion", + "module": "serverpod_auth_idp", + "version": "20261015164015382" + }, + { + "__className__": "serverpod.DatabaseMigrationVersion", + "module": "serverpod_auth_core", + "version": "20260924105232991" + } + ], + "migrationApiVersion": 1 +} \ No newline at end of file diff --git a/wonky_server/migrations/20261025085716775-serverpod-4-1-rc1/definition.sql b/wonky_server/migrations/20261025085716775-serverpod-4-1-rc1/definition.sql new file mode 100644 index 00000000..5fbc6345 --- /dev/null +++ b/wonky_server/migrations/20261025085716775-serverpod-4-1-rc1/definition.sql @@ -0,0 +1,4384 @@ +BEGIN; + +-- +-- Function: gen_random_uuid_v7() +-- Source: https://gist.github.com/kjmph/5bd772b2c2df145aa645b837da7eca74 +-- License: MIT (copyright notice included on the generator source code). +-- +create or replace function gen_random_uuid_v7() +returns uuid +as $$ +begin + -- use random v4 uuid as starting point (which has the same variant we need) + -- then overlay timestamp + -- then set version 7 by flipping the 2 and 1 bit in the version 4 string + return encode( + set_bit( + set_bit( + overlay(uuid_send(gen_random_uuid()) + placing substring(int8send(floor(extract(epoch from clock_timestamp()) * 1000)::bigint) from 3) + from 1 for 6 + ), + 52, 1 + ), + 53, 1 + ), + 'hex')::uuid; +end +$$ +language plpgsql +volatile; + +-- +-- Class AccessRequest as table access_request +-- +CREATE TABLE "access_request" ( + "id" bigserial PRIMARY KEY, + "itemId" bigint NOT NULL, + "requesterId" bigint NOT NULL, + "requestedRole" text NOT NULL, + "message" text, + "createdAt" timestamp without time zone NOT NULL, + "handledAt" timestamp without time zone, + "handledById" bigint, + "outcome" text +); + +-- Indexes +CREATE INDEX "access_request_item_idx" ON "access_request" USING btree ("itemId", "handledAt"); + +-- +-- Class ActivityEvent as table activity_event +-- +CREATE TABLE "activity_event" ( + "id" bigserial PRIMARY KEY, + "itemId" bigint NOT NULL, + "actorId" bigint, + "actorKind" text NOT NULL, + "type" text NOT NULL, + "summary" text NOT NULL, + "at" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE INDEX "activity_item_idx" ON "activity_event" USING btree ("itemId", "at"); +CREATE INDEX "activity_type_at_idx" ON "activity_event" USING btree ("type", "at"); + +-- +-- Class AiFunctionCache as table ai_function_cache +-- +CREATE TABLE "ai_function_cache" ( + "id" bigserial PRIMARY KEY, + "key" text NOT NULL, + "spreadsheetUuid" uuid NOT NULL, + "model" text NOT NULL, + "value" text NOT NULL, + "error" text, + "createdAt" timestamp without time zone NOT NULL, + "hits" bigint NOT NULL DEFAULT 0 +); + +-- Indexes +CREATE UNIQUE INDEX "ai_function_cache_key_idx" ON "ai_function_cache" USING btree ("key"); +CREATE INDEX "ai_function_cache_sheet_idx" ON "ai_function_cache" USING btree ("spreadsheetUuid"); + +-- +-- Class AiPreviewRecord as table ai_preview_record +-- +CREATE TABLE "ai_preview_record" ( + "id" bigserial PRIMARY KEY, + "previewId" text NOT NULL, + "userId" bigint NOT NULL, + "fileUuid" uuid NOT NULL, + "previewJson" text NOT NULL, + "changesJson" text NOT NULL, + "createdAt" timestamp without time zone NOT NULL, + "expiresAt" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE UNIQUE INDEX "ai_preview_id_idx" ON "ai_preview_record" USING btree ("previewId"); +CREATE INDEX "ai_preview_expires_idx" ON "ai_preview_record" USING btree ("expiresAt"); + +-- +-- Class AiUsage as table ai_usage +-- +CREATE TABLE "ai_usage" ( + "id" bigserial PRIMARY KEY, + "userId" bigint NOT NULL, + "model" text NOT NULL, + "feature" text NOT NULL, + "inputTokens" bigint NOT NULL, + "outputTokens" bigint NOT NULL, + "cacheReadTokens" bigint NOT NULL, + "cacheWriteTokens" bigint NOT NULL, + "at" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE INDEX "ai_usage_user_at_idx" ON "ai_usage" USING btree ("userId", "at"); +CREATE INDEX "ai_usage_at_idx" ON "ai_usage" USING btree ("at"); + +-- +-- Class AiUserKey as table ai_user_key +-- +CREATE TABLE "ai_user_key" ( + "id" bigserial PRIMARY KEY, + "userId" bigint NOT NULL, + "provider" text NOT NULL, + "apiKey" text NOT NULL, + "createdAt" timestamp without time zone NOT NULL, + "updatedAt" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE UNIQUE INDEX "ai_user_key_user_idx" ON "ai_user_key" USING btree ("userId"); + +-- +-- Class AiUserSetting as table ai_user_setting +-- +CREATE TABLE "ai_user_setting" ( + "id" bigserial PRIMARY KEY, + "userId" bigint NOT NULL, + "quality" text NOT NULL DEFAULT 'best'::text, + "updatedAt" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE UNIQUE INDEX "ai_user_setting_user_idx" ON "ai_user_setting" USING btree ("userId"); + +-- +-- Class AssistantMessage as table assistant_message +-- +CREATE TABLE "assistant_message" ( + "id" bigserial PRIMARY KEY, + "threadId" bigint NOT NULL, + "role" text NOT NULL, + "contentJson" text, + "displayText" text NOT NULL, + "toolCallsJson" text, + "visible" boolean NOT NULL DEFAULT true, + "replay" boolean NOT NULL DEFAULT true, + "runId" text, + "stopReason" text, + "createdAt" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE INDEX "assistant_message_thread_idx" ON "assistant_message" USING btree ("threadId", "id"); + +-- +-- Class AssistantThread as table assistant_thread +-- +CREATE TABLE "assistant_thread" ( + "id" bigserial PRIMARY KEY, + "userId" bigint NOT NULL, + "itemId" bigint, + "title" text NOT NULL, + "contextRevision" bigint, + "contextMarkdown" text, + "createdAt" timestamp without time zone NOT NULL, + "updatedAt" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE INDEX "assistant_thread_user_item_idx" ON "assistant_thread" USING btree ("userId", "itemId", "updatedAt"); + +-- +-- Class Banding as table banding +-- +CREATE TABLE "banding" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid_v7(), + "fileId" bigint NOT NULL, + "revision" bigint NOT NULL DEFAULT 0, + "sheetId" uuid NOT NULL, + "rangeJson" text NOT NULL, + "styleJson" text NOT NULL, + "createdAt" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE INDEX "banding_file_idx" ON "banding" USING btree ("fileId", "id"); + +-- +-- Class BasicFilter as table basic_filter +-- +CREATE TABLE "basic_filter" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid_v7(), + "fileId" bigint NOT NULL, + "revision" bigint NOT NULL DEFAULT 0, + "sheetId" uuid NOT NULL, + "rangeJson" text NOT NULL, + "criteriaJson" text NOT NULL, + "sortJson" text, + "createdAt" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE INDEX "basic_filter_file_idx" ON "basic_filter" USING btree ("fileId", "id"); + +-- +-- Class BoardAiUsage as table board_ai_usage +-- +CREATE TABLE "board_ai_usage" ( + "id" bigserial PRIMARY KEY, + "day" text NOT NULL, + "provider" text NOT NULL, + "model" text NOT NULL, + "feature" text NOT NULL, + "requests" bigint NOT NULL, + "inputTokens" bigint NOT NULL, + "outputTokens" bigint NOT NULL +); + +-- Indexes +CREATE UNIQUE INDEX "board_ai_usage_idx" ON "board_ai_usage" USING btree ("day", "provider", "model", "feature"); + +-- +-- Class BoardBundleSuggestion as table board_bundle_suggestion +-- +CREATE TABLE "board_bundle_suggestion" ( + "id" bigserial PRIMARY KEY, + "title" text NOT NULL, + "rationale" text NOT NULL, + "area" text, + "postIds" json NOT NULL, + "state" text NOT NULL, + "createdAt" timestamp without time zone NOT NULL +); + +-- +-- Class BoardComment as table board_comment +-- +CREATE TABLE "board_comment" ( + "id" bigserial PRIMARY KEY, + "uuid" uuid NOT NULL, + "postId" bigint, + "taskId" bigint, + "authorId" bigint, + "body" text NOT NULL, + "systemEvent" text, + "moderation" text NOT NULL, + "moderationReason" text, + "needsCheck" boolean NOT NULL DEFAULT false, + "createdAt" timestamp without time zone NOT NULL, + "editedAt" timestamp without time zone, + "deletedAt" timestamp without time zone, + "removedAt" timestamp without time zone +); + +-- Indexes +CREATE UNIQUE INDEX "board_comment_uuid_idx" ON "board_comment" USING btree ("uuid"); +CREATE INDEX "board_comment_post_idx" ON "board_comment" USING btree ("postId", "createdAt"); +CREATE INDEX "board_comment_task_idx" ON "board_comment" USING btree ("taskId", "createdAt"); + +-- +-- Class BoardFlag as table board_flag +-- +CREATE TABLE "board_flag" ( + "id" bigserial PRIMARY KEY, + "postId" bigint, + "commentId" bigint, + "userId" bigint NOT NULL, + "reason" text NOT NULL, + "note" text, + "createdAt" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE INDEX "board_flag_post_idx" ON "board_flag" USING btree ("postId", "userId"); +CREATE INDEX "board_flag_comment_idx" ON "board_flag" USING btree ("commentId", "userId"); + +-- +-- Class BoardGithubDelivery as table board_github_delivery +-- +CREATE TABLE "board_github_delivery" ( + "id" bigserial PRIMARY KEY, + "deliveryId" text NOT NULL, + "event" text NOT NULL, + "receivedAt" timestamp without time zone NOT NULL, + "payload" text, + "processedAt" timestamp without time zone, + "attempts" bigint NOT NULL DEFAULT 0 +); + +-- Indexes +CREATE UNIQUE INDEX "board_github_delivery_idx" ON "board_github_delivery" USING btree ("deliveryId"); + +-- +-- Class BoardModerationLog as table board_moderation_log +-- +CREATE TABLE "board_moderation_log" ( + "id" bigserial PRIMARY KEY, + "adminId" bigint NOT NULL, + "action" text NOT NULL, + "targetKind" text NOT NULL, + "targetId" bigint NOT NULL, + "beforeJson" text, + "afterJson" text, + "reason" text, + "createdAt" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE INDEX "board_moderation_log_target_idx" ON "board_moderation_log" USING btree ("targetKind", "targetId"); + +-- +-- Class BoardPost as table board_post +-- +CREATE TABLE "board_post" ( + "id" bigserial PRIMARY KEY, + "authorId" bigint, + "kind" text NOT NULL, + "reportedKind" text NOT NULL, + "area" text, + "size" text, + "aiLabels" boolean NOT NULL DEFAULT true, + "title" text NOT NULL, + "body" text NOT NULL, + "contextJson" text, + "appHint" text NOT NULL, + "screenshotFileId" bigint, + "status" text NOT NULL, + "moderation" text NOT NULL, + "moderationReason" text, + "needsCheck" boolean NOT NULL DEFAULT false, + "editedByModerator" boolean NOT NULL DEFAULT false, + "triageState" text NOT NULL, + "triageJson" text, + "triageAttempts" bigint NOT NULL DEFAULT 0, + "suggestedTitle" text, + "summary" text, + "duplicateOfId" bigint, + "possibleDuplicateOfId" bigint, + "duplicateHintDismissed" boolean NOT NULL DEFAULT false, + "declineReason" text, + "taskId" bigint, + "score" bigint NOT NULL DEFAULT 0, + "voteCount" bigint NOT NULL DEFAULT 0, + "commentCount" bigint NOT NULL DEFAULT 0, + "flagCount" bigint NOT NULL DEFAULT 0, + "createdAt" timestamp without time zone NOT NULL, + "editedAt" timestamp without time zone, + "deletedAt" timestamp without time zone, + "removedAt" timestamp without time zone +); + +-- Indexes +CREATE INDEX "board_post_list_idx" ON "board_post" USING btree ("moderation", "kind", "status", "score"); +CREATE INDEX "board_post_author_idx" ON "board_post" USING btree ("authorId"); +CREATE INDEX "board_post_task_idx" ON "board_post" USING btree ("taskId"); + +-- +-- Class BoardPullRequest as table board_pull_request +-- +CREATE TABLE "board_pull_request" ( + "id" bigserial PRIMARY KEY, + "prNumber" bigint NOT NULL, + "taskId" bigint, + "authorLogin" text NOT NULL, + "headSha" text NOT NULL, + "state" text NOT NULL, + "gateState" text NOT NULL, + "gateReason" text, + "gateFirstFailedAt" timestamp without time zone, + "gateCommentId" bigint, + "reviewCommentId" bigint, + "lastReviewSha" text, + "lastReviewJson" text, + "lastReviewAt" timestamp without time zone, + "reviewDay" text, + "reviewCount" bigint NOT NULL DEFAULT 0, + "mergeCommitSha" text, + "reviewPendingSha" text, + "reviewDueAt" timestamp without time zone, + "createdAt" timestamp without time zone NOT NULL, + "updatedAt" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE UNIQUE INDEX "board_pull_request_number_idx" ON "board_pull_request" USING btree ("prNumber"); + +-- +-- Class BoardTask as table board_task +-- +CREATE TABLE "board_task" ( + "id" bigserial PRIMARY KEY, + "title" text NOT NULL, + "area" text NOT NULL, + "size" text NOT NULL, + "status" text NOT NULL, + "promptBody" text NOT NULL, + "promptGeneratedAt" timestamp without time zone, + "promptRequestedAt" timestamp without time zone, + "promptNote" text, + "promptExcludedCommentIds" json, + "promptEditedById" bigint, + "promptEditedAt" timestamp without time zone, + "createdById" bigint NOT NULL, + "publishedAt" timestamp without time zone, + "claimedById" bigint, + "claimedAt" timestamp without time zone, + "claimExpiresAt" timestamp without time zone, + "claimWarnedAt" timestamp without time zone, + "claimCode" text, + "pullRequestUrl" text, + "prNumber" bigint, + "prAuthorLogin" text, + "mergeCommitSha" text, + "doneAt" timestamp without time zone, + "shippedDeploySha" text, + "shippedAt" timestamp without time zone, + "createdAt" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE INDEX "board_task_status_idx" ON "board_task" USING btree ("status"); +CREATE INDEX "board_task_claim_idx" ON "board_task" USING btree ("claimedById"); + +-- +-- Class BoardUserProfile as table board_user_profile +-- +CREATE TABLE "board_user_profile" ( + "id" bigserial PRIMARY KEY, + "userId" bigint NOT NULL, + "githubLogin" text, + "blockedAt" timestamp without time zone, + "blockedUntil" timestamp without time zone, + "blockedReason" text, + "blockedById" bigint +); + +-- Indexes +CREATE UNIQUE INDEX "board_user_profile_user_idx" ON "board_user_profile" USING btree ("userId"); + +-- +-- Class BoardVote as table board_vote +-- +CREATE TABLE "board_vote" ( + "id" bigserial PRIMARY KEY, + "postId" bigint NOT NULL, + "userId" bigint NOT NULL, + "value" bigint NOT NULL, + "createdAt" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE UNIQUE INDEX "board_vote_unique_idx" ON "board_vote" USING btree ("postId", "userId"); +CREATE INDEX "board_vote_recent_idx" ON "board_vote" USING btree ("postId", "createdAt"); + +-- +-- Class Cell as table cell +-- +CREATE TABLE "cell" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid_v7(), + "fileId" bigint NOT NULL, + "revision" bigint NOT NULL DEFAULT 0, + "sheetId" uuid NOT NULL, + "rowId" uuid NOT NULL, + "columnId" uuid NOT NULL, + "content" text, + "note" text, + "numberFormat" text, + "fontFamily" text, + "fontSize" double precision, + "bold" boolean, + "italic" boolean, + "underline" boolean, + "strikethrough" boolean, + "textColor" text, + "fillColor" text, + "hAlign" text, + "vAlign" text, + "wrap" text, + "rotation" bigint, + "borderTop" text, + "borderRight" text, + "borderBottom" text, + "borderLeft" text, + "linkUrl" text, + "createdAt" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE INDEX "cell_row_idx" ON "cell" USING btree ("sheetId", "rowId"); +CREATE INDEX "cell_col_idx" ON "cell" USING btree ("sheetId", "columnId"); + +-- +-- Class CellHistory as table cell_history +-- +CREATE TABLE "cell_history" ( + "id" bigserial PRIMARY KEY, + "fileUuid" uuid NOT NULL, + "sheetId" uuid NOT NULL, + "rowId" uuid NOT NULL, + "colId" uuid NOT NULL, + "revision" bigint NOT NULL, + "userUuid" uuid NOT NULL, + "actorKind" text NOT NULL, + "label" text NOT NULL, + "at" timestamp without time zone NOT NULL, + "before" text, + "after" text +); + +-- Indexes +CREATE INDEX "cell_history_cell_idx" ON "cell_history" USING btree ("fileUuid", "sheetId", "rowId", "colId", "at"); +CREATE UNIQUE INDEX "cell_history_change_idx" ON "cell_history" USING btree ("fileUuid", "revision", "rowId", "colId"); + +-- +-- Class ChartLink as table chart_link +-- +CREATE TABLE "chart_link" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid_v7(), + "fileId" bigint NOT NULL, + "revision" bigint NOT NULL DEFAULT 0, + "elementId" uuid NOT NULL, + "spreadsheetItemUuid" uuid NOT NULL, + "chartObjectId" uuid NOT NULL, + "sheetId" text, + "sourceSpecJson" text NOT NULL, + "dataSnapshotJson" text NOT NULL, + "sourceVersion" text NOT NULL, + "updatedAt" timestamp without time zone NOT NULL, + "updatedBy" uuid NOT NULL, + "autoUpdate" boolean NOT NULL DEFAULT false, + "createdAt" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE INDEX "chart_link_file_idx" ON "chart_link" USING btree ("fileId", "id"); +CREATE INDEX "chart_link_element_idx" ON "chart_link" USING btree ("elementId"); + +-- +-- Class CodeState as table code_state +-- +CREATE TABLE "code_state" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid_v7(), + "fileId" bigint NOT NULL, + "revision" bigint NOT NULL DEFAULT 0, + "elementId" uuid NOT NULL, + "sortKey" text NOT NULL, + "label" text, + "code" text NOT NULL, + "language" text, + "title" text, + "highlight" text, + "annotationsJson" text, + "transitionJson" text, + "createdAt" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE INDEX "code_state_file_idx" ON "code_state" USING btree ("fileId", "id"); +CREATE INDEX "code_state_element_idx" ON "code_state" USING btree ("elementId", "sortKey"); + +-- +-- Class Comment as table comment +-- +CREATE TABLE "comment" ( + "id" bigserial PRIMARY KEY, + "threadId" bigint NOT NULL, + "uuid" uuid, + "authorId" bigint, + "authorKind" text NOT NULL, + "body" text NOT NULL, + "mentions" json NOT NULL, + "reactions" text NOT NULL DEFAULT '{}'::text, + "createdAt" timestamp without time zone NOT NULL, + "editedAt" timestamp without time zone, + "deletedAt" timestamp without time zone, + "systemEvent" text, + "publicationId" bigint +); + +-- Indexes +CREATE INDEX "comment_thread_idx" ON "comment" USING btree ("threadId", "createdAt"); +CREATE UNIQUE INDEX "comment_uuid_idx" ON "comment" USING btree ("uuid"); +CREATE INDEX "comment_created_idx" ON "comment" USING btree ("createdAt"); + +-- +-- Class CommentThread as table comment_thread +-- +CREATE TABLE "comment_thread" ( + "id" bigserial PRIMARY KEY, + "uuid" uuid NOT NULL, + "itemId" bigint NOT NULL, + "anchorKind" text NOT NULL, + "anchorJson" text NOT NULL, + "quotedText" text, + "status" text NOT NULL DEFAULT 'open'::text, + "createdById" bigint NOT NULL, + "createdAt" timestamp without time zone NOT NULL, + "updatedAt" timestamp without time zone, + "resolvedById" bigint, + "resolvedAt" timestamp without time zone, + "assigneeId" bigint, + "statusBeforeOrphan" text, + "orphanReasonJson" text, + "suggestionId" text +); + +-- Indexes +CREATE INDEX "comment_thread_item_idx" ON "comment_thread" USING btree ("itemId", "status"); +CREATE UNIQUE INDEX "comment_thread_uuid_idx" ON "comment_thread" USING btree ("uuid"); + +-- +-- Class ConditionalFormatRule as table conditional_format_rule +-- +CREATE TABLE "conditional_format_rule" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid_v7(), + "fileId" bigint NOT NULL, + "revision" bigint NOT NULL DEFAULT 0, + "sheetId" uuid NOT NULL, + "sortKey" text NOT NULL, + "rangesJson" text NOT NULL, + "ruleJson" text NOT NULL, + "enabled" boolean NOT NULL DEFAULT true, + "createdAt" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE INDEX "conditional_format_rule_file_idx" ON "conditional_format_rule" USING btree ("fileId", "id"); + +-- +-- Class DataValidationRule as table data_validation_rule +-- +CREATE TABLE "data_validation_rule" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid_v7(), + "fileId" bigint NOT NULL, + "revision" bigint NOT NULL DEFAULT 0, + "sheetId" uuid NOT NULL, + "rangesJson" text NOT NULL, + "criteriaJson" text NOT NULL, + "strict" boolean NOT NULL DEFAULT false, + "showDropdown" boolean NOT NULL DEFAULT true, + "displayStyle" text, + "helpText" text, + "createdAt" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE INDEX "data_validation_rule_file_idx" ON "data_validation_rule" USING btree ("fileId", "id"); + +-- +-- Class Deck as table deck +-- +CREATE TABLE "deck" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid_v7(), + "fileId" bigint NOT NULL, + "revision" bigint NOT NULL DEFAULT 0, + "title" text NOT NULL, + "width" double precision NOT NULL DEFAULT 1920.0, + "height" double precision NOT NULL DEFAULT 1080.0, + "sizePreset" text NOT NULL DEFAULT 'widescreen16x9'::text, + "themeId" uuid, + "appearance" text NOT NULL DEFAULT 'light'::text, + "language" text, + "showSlideNumbers" boolean NOT NULL DEFAULT false, + "hideNumberOnTitle" boolean NOT NULL DEFAULT true, + "firstSlideNumber" bigint NOT NULL DEFAULT 1, + "footerText" text, + "defaultsJson" text, + "guidesJson" text, + "presentJson" text, + "createdBy" uuid NOT NULL, + "createdAt" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE INDEX "deck_file_idx" ON "deck" USING btree ("fileId", "id"); + +-- +-- Class DeckTheme as table deck_theme +-- +CREATE TABLE "deck_theme" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid_v7(), + "fileId" bigint NOT NULL, + "revision" bigint NOT NULL DEFAULT 0, + "deckId" uuid NOT NULL, + "name" text NOT NULL, + "sourceKey" text, + "colorsJson" text NOT NULL, + "fontsJson" text NOT NULL, + "textStylesJson" text NOT NULL, + "backgroundJson" text NOT NULL, + "shapeStyleJson" text, + "tableStyleJson" text, + "codeStyleJson" text, + "chartStyleJson" text, + "sortKey" text NOT NULL, + "createdAt" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE INDEX "deck_theme_file_idx" ON "deck_theme" USING btree ("fileId", "id"); +CREATE INDEX "deck_theme_deck_idx" ON "deck_theme" USING btree ("deckId"); + +-- +-- Class DimensionGroup as table dimension_group +-- +CREATE TABLE "dimension_group" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid_v7(), + "fileId" bigint NOT NULL, + "revision" bigint NOT NULL DEFAULT 0, + "sheetId" uuid NOT NULL, + "dimension" text NOT NULL, + "startId" uuid NOT NULL, + "endId" uuid NOT NULL, + "depth" bigint NOT NULL, + "collapsed" boolean NOT NULL DEFAULT false, + "createdAt" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE INDEX "dimension_group_file_idx" ON "dimension_group" USING btree ("fileId", "id"); + +-- +-- Class Document as table document +-- +CREATE TABLE "document" ( + "id" bigserial PRIMARY KEY, + "uuid" uuid NOT NULL, + "itemId" bigint NOT NULL, + "headRevision" bigint NOT NULL DEFAULT 0, + "latestSnapshotRevision" bigint NOT NULL DEFAULT 0, + "themeId" uuid, + "pageSetupJson" text NOT NULL DEFAULT '{}'::text, + "language" text, + "frontMatter" text, + "charCount" bigint NOT NULL DEFAULT 0, + "wordCount" bigint NOT NULL DEFAULT 0, + "createdAt" timestamp without time zone NOT NULL, + "updatedAt" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE UNIQUE INDEX "document_uuid_idx" ON "document" USING btree ("uuid"); +CREATE UNIQUE INDEX "document_item_idx" ON "document" USING btree ("itemId"); + +-- +-- Class DocumentImportReport as table document_import_report +-- +CREATE TABLE "document_import_report" ( + "id" bigserial PRIMARY KEY, + "documentId" bigint NOT NULL, + "userId" bigint NOT NULL, + "warningsJson" text NOT NULL, + "createdAt" timestamp without time zone NOT NULL, + "seenAt" timestamp without time zone +); + +-- Indexes +CREATE INDEX "document_import_report_idx" ON "document_import_report" USING btree ("documentId", "userId"); + +-- +-- Class DocumentMetaChange as table document_meta_change +-- +CREATE TABLE "document_meta_change" ( + "id" bigserial PRIMARY KEY, + "documentId" bigint NOT NULL, + "field" text NOT NULL, + "clientOpId" uuid, + "beforeJson" text NOT NULL, + "afterJson" text NOT NULL, + "authorId" bigint, + "actorKind" text NOT NULL, + "createdAt" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE INDEX "document_meta_change_idx" ON "document_meta_change" USING btree ("documentId", "createdAt"); +CREATE UNIQUE INDEX "document_meta_change_client_idx" ON "document_meta_change" USING btree ("documentId", "clientOpId"); + +-- +-- Class DocumentOp as table document_op +-- +CREATE TABLE "document_op" ( + "id" bigserial PRIMARY KEY, + "documentId" bigint NOT NULL, + "revision" bigint NOT NULL, + "clientOpId" uuid NOT NULL, + "authorId" bigint, + "actorKind" text NOT NULL, + "sessionId" text, + "kind" text NOT NULL, + "baseRevision" bigint NOT NULL, + "deltaJson" text NOT NULL, + "inverseJson" text NOT NULL, + "createdAt" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE UNIQUE INDEX "document_op_rev_idx" ON "document_op" USING btree ("documentId", "revision"); +CREATE UNIQUE INDEX "document_op_client_idx" ON "document_op" USING btree ("documentId", "clientOpId"); +CREATE INDEX "document_op_created_idx" ON "document_op" USING brin ("createdAt"); + +-- +-- Class DocumentSnapshot as table document_snapshot +-- +CREATE TABLE "document_snapshot" ( + "id" bigserial PRIMARY KEY, + "documentId" bigint NOT NULL, + "revision" bigint NOT NULL, + "deltaJson" text NOT NULL, + "compressed" boolean NOT NULL, + "createdAt" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE UNIQUE INDEX "document_snapshot_idx" ON "document_snapshot" USING btree ("documentId", "revision"); + +-- +-- Class DriveItem as table drive_item +-- +CREATE TABLE "drive_item" ( + "id" bigserial PRIMARY KEY, + "uuid" uuid NOT NULL, + "kind" text NOT NULL, + "name" text NOT NULL, + "ownerId" bigint NOT NULL, + "parentId" bigint, + "description" text, + "contentRef" uuid, + "thumbnailFileId" uuid, + "sizeBytes" bigint NOT NULL DEFAULT 0, + "attachmentBytes" bigint NOT NULL DEFAULT 0, + "kindMask" bigint NOT NULL DEFAULT 0, + "mimeType" text, + "fileCategory" text, + "uploadState" text, + "fileMetaJson" text, + "previewFileId" uuid, + "createdAt" timestamp without time zone NOT NULL, + "createdById" bigint NOT NULL, + "modifiedAt" timestamp without time zone NOT NULL, + "modifiedById" bigint, + "trashedAt" timestamp without time zone, + "trashedById" bigint, + "templateOf" text, + "templateKey" text, + "editorsCanShare" boolean NOT NULL DEFAULT true, + "nonEditorsCanExport" boolean NOT NULL DEFAULT true, + "viewersSeeComments" boolean NOT NULL DEFAULT false, + "searchText" text +); + +-- Indexes +CREATE UNIQUE INDEX "drive_item_uuid_idx" ON "drive_item" USING btree ("uuid"); +CREATE INDEX "drive_item_parent_idx" ON "drive_item" USING btree ("parentId"); +CREATE INDEX "drive_item_owner_idx" ON "drive_item" USING btree ("ownerId", "trashedAt"); +CREATE INDEX "drive_item_content_idx" ON "drive_item" USING btree ("contentRef"); +CREATE INDEX "drive_item_template_idx" ON "drive_item" USING btree ("templateOf"); +CREATE INDEX "drive_item_upload_idx" ON "drive_item" USING btree ("uploadState", "createdAt"); +CREATE INDEX "drive_item_trashed_idx" ON "drive_item" USING btree ("trashedAt"); + +-- +-- Class DriveItemPermission as table drive_item_permission +-- +CREATE TABLE "drive_item_permission" ( + "id" bigserial PRIMARY KEY, + "itemId" bigint NOT NULL, + "userId" bigint, + "invitedEmail" text, + "role" text NOT NULL, + "inheritedFromId" bigint, + "grantedById" bigint NOT NULL, + "grantedAt" timestamp without time zone NOT NULL, + "expiresAt" timestamp without time zone, + "viaPublicationId" bigint +); + +-- Indexes +CREATE UNIQUE INDEX "perm_item_user_idx" ON "drive_item_permission" USING btree ("itemId", "userId", "inheritedFromId", "invitedEmail") NULLS NOT DISTINCT; +CREATE INDEX "perm_user_idx" ON "drive_item_permission" USING btree ("userId"); +CREATE INDEX "perm_email_idx" ON "drive_item_permission" USING btree ("invitedEmail"); +CREATE INDEX "perm_expires_idx" ON "drive_item_permission" USING btree ("expiresAt"); +CREATE INDEX "perm_granted_idx" ON "drive_item_permission" USING btree ("grantedAt"); + +-- +-- Class DriveItemUserState as table drive_item_user_state +-- +CREATE TABLE "drive_item_user_state" ( + "id" bigserial PRIMARY KEY, + "itemId" bigint NOT NULL, + "userId" bigint NOT NULL, + "starred" boolean NOT NULL DEFAULT false, + "lastOpenedAt" timestamp without time zone, + "lastEditedAt" timestamp without time zone, + "pinnedOffline" boolean NOT NULL DEFAULT false, + "shortcutParentId" bigint, + "viaLink" boolean NOT NULL DEFAULT false +); + +-- Indexes +CREATE UNIQUE INDEX "drive_item_user_state_idx" ON "drive_item_user_state" USING btree ("itemId", "userId"); +CREATE INDEX "drive_item_user_recent_idx" ON "drive_item_user_state" USING btree ("userId", "lastOpenedAt"); + +-- +-- Class EmbeddedObject as table embedded_object +-- +CREATE TABLE "embedded_object" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid_v7(), + "fileId" bigint NOT NULL, + "revision" bigint NOT NULL DEFAULT 0, + "sheetId" uuid NOT NULL, + "kind" text NOT NULL, + "anchorJson" text NOT NULL, + "specJson" text NOT NULL, + "zIndex" bigint NOT NULL DEFAULT 0, + "title" text, + "altText" text, + "createdAt" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE INDEX "embedded_object_file_idx" ON "embedded_object" USING btree ("fileId", "id"); + +-- +-- Class FeedbackReport as table feedback_report +-- +CREATE TABLE "feedback_report" ( + "id" bigserial PRIMARY KEY, + "userId" bigint, + "kind" text NOT NULL, + "message" text NOT NULL, + "app" text NOT NULL, + "appVersion" text NOT NULL, + "platform" text NOT NULL, + "route" text, + "stackTrace" text, + "contextJson" text, + "createdAt" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE INDEX "feedback_report_created_idx" ON "feedback_report" USING btree ("createdAt"); + +-- +-- Class FilterView as table filter_view +-- +CREATE TABLE "filter_view" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid_v7(), + "fileId" bigint NOT NULL, + "revision" bigint NOT NULL DEFAULT 0, + "sheetId" uuid NOT NULL, + "name" text NOT NULL, + "rangeJson" text NOT NULL, + "criteriaJson" text NOT NULL, + "sortJson" text, + "createdBy" uuid NOT NULL, + "createdAt" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE INDEX "filter_view_file_idx" ON "filter_view" USING btree ("fileId", "id"); + +-- +-- Class HeadlessChange as table headless_change +-- +CREATE TABLE "headless_change" ( + "id" bigserial PRIMARY KEY, + "changeId" uuid NOT NULL, + "kind" text NOT NULL, + "itemId" bigint NOT NULL, + "documentId" bigint, + "revision" bigint, + "inverseJson" text, + "metaField" text, + "metaBeforeJson" text, + "metaAfterJson" text, + "userId" bigint NOT NULL, + "actorKind" text NOT NULL, + "label" text, + "toolName" text NOT NULL, + "createdAt" timestamp without time zone NOT NULL, + "revertedAt" timestamp without time zone +); + +-- Indexes +CREATE UNIQUE INDEX "headless_change_id_idx" ON "headless_change" USING btree ("changeId"); +CREATE INDEX "headless_change_created_idx" ON "headless_change" USING btree ("createdAt"); + +-- +-- Class ItemVersion as table item_version +-- +CREATE TABLE "item_version" ( + "id" bigserial PRIMARY KEY, + "itemId" bigint NOT NULL, + "kind" text NOT NULL, + "name" text, + "createdAt" timestamp without time zone NOT NULL, + "createdById" bigint, + "authors" json NOT NULL, + "storageKind" text NOT NULL, + "revision" bigint, + "snapshotFileId" uuid, + "sizeBytes" bigint NOT NULL DEFAULT 0 +); + +-- Indexes +CREATE INDEX "item_version_item_idx" ON "item_version" USING btree ("itemId", "createdAt"); + +-- +-- Class ItemVersionState as table item_version_state +-- +CREATE TABLE "item_version_state" ( + "id" bigserial PRIMARY KEY, + "itemId" bigint NOT NULL, + "lastEditAt" timestamp without time zone NOT NULL, + "dirtySince" timestamp without time zone, + "lastVersionAt" timestamp without time zone, + "editorsSinceLastVersion" json NOT NULL, + "lastRevision" bigint, + "capturingSince" timestamp without time zone +); + +-- Indexes +CREATE UNIQUE INDEX "item_version_state_item_idx" ON "item_version_state" USING btree ("itemId"); +CREATE INDEX "item_version_state_dirty_idx" ON "item_version_state" USING btree ("dirtySince"); + +-- +-- Class ItemViewDay as table item_view_day +-- +CREATE TABLE "item_view_day" ( + "id" bigserial PRIMARY KEY, + "itemId" bigint NOT NULL, + "userId" bigint NOT NULL, + "day" timestamp without time zone NOT NULL, + "views" bigint NOT NULL DEFAULT 1, + "lastViewedAt" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE UNIQUE INDEX "item_view_day_idx" ON "item_view_day" USING btree ("itemId", "userId", "day"); +CREATE INDEX "item_view_day_item_idx" ON "item_view_day" USING btree ("itemId", "day"); + +-- +-- Class Macro as table macro +-- +CREATE TABLE "macro" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid_v7(), + "fileId" bigint NOT NULL, + "revision" bigint NOT NULL DEFAULT 0, + "projectId" uuid NOT NULL, + "name" text NOT NULL, + "functionName" text NOT NULL, + "shortcutSlot" bigint, + "createdAt" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE INDEX "macro_file_idx" ON "macro" USING btree ("fileId", "id"); + +-- +-- Class MergedRange as table merged_range +-- +CREATE TABLE "merged_range" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid_v7(), + "fileId" bigint NOT NULL, + "revision" bigint NOT NULL DEFAULT 0, + "sheetId" uuid NOT NULL, + "rangeJson" text NOT NULL, + "createdAt" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE INDEX "merged_range_file_idx" ON "merged_range" USING btree ("fileId", "id"); + +-- +-- Class NamedRange as table named_range +-- +CREATE TABLE "named_range" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid_v7(), + "fileId" bigint NOT NULL, + "revision" bigint NOT NULL DEFAULT 0, + "spreadsheetId" uuid NOT NULL, + "name" text NOT NULL, + "rangeJson" text, + "formula" text, + "comment" text, + "metaJson" text, + "createdAt" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE INDEX "named_range_file_idx" ON "named_range" USING btree ("fileId", "id"); + +-- +-- Class UserNotification as table notification +-- +CREATE TABLE "notification" ( + "id" bigserial PRIMARY KEY, + "userId" bigint NOT NULL, + "kind" text NOT NULL, + "itemId" bigint, + "actorId" bigint, + "payload" text NOT NULL, + "createdAt" timestamp without time zone NOT NULL, + "readAt" timestamp without time zone, + "emailedAt" timestamp without time zone, + "emailPending" boolean DEFAULT false +); + +-- Indexes +CREATE INDEX "notification_user_idx" ON "notification" USING btree ("userId", "createdAt"); +CREATE INDEX "notification_email_idx" ON "notification" USING btree ("emailPending", "createdAt"); +CREATE INDEX "notification_created_idx" ON "notification" USING brin ("createdAt"); + +-- +-- Class OwnershipTransfer as table ownership_transfer +-- +CREATE TABLE "ownership_transfer" ( + "id" bigserial PRIMARY KEY, + "itemId" bigint NOT NULL, + "fromUserId" bigint NOT NULL, + "toUserId" bigint NOT NULL, + "createdAt" timestamp without time zone NOT NULL, + "handledAt" timestamp without time zone, + "accepted" boolean +); + +-- Indexes +CREATE INDEX "ownership_transfer_item_idx" ON "ownership_transfer" USING btree ("itemId", "handledAt"); + +-- +-- Class PersonalAccessToken as table personal_access_token +-- +CREATE TABLE "personal_access_token" ( + "id" bigserial PRIMARY KEY, + "userId" bigint NOT NULL, + "name" text NOT NULL, + "tokenHash" text NOT NULL, + "prefix" text NOT NULL, + "scopes" json NOT NULL, + "expiresAt" timestamp without time zone, + "lastUsedAt" timestamp without time zone, + "createdAt" timestamp without time zone NOT NULL, + "revokedAt" timestamp without time zone +); + +-- Indexes +CREATE INDEX "personal_access_token_user_idx" ON "personal_access_token" USING btree ("userId"); +CREATE UNIQUE INDEX "personal_access_token_hash_idx" ON "personal_access_token" USING btree ("tokenHash"); + +-- +-- Class PivotTable as table pivot_table +-- +CREATE TABLE "pivot_table" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid_v7(), + "fileId" bigint NOT NULL, + "revision" bigint NOT NULL DEFAULT 0, + "sheetId" uuid NOT NULL, + "anchorRowId" uuid NOT NULL, + "anchorColId" uuid NOT NULL, + "sourceRangeJson" text NOT NULL, + "specJson" text NOT NULL, + "createdAt" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE INDEX "pivot_table_file_idx" ON "pivot_table" USING btree ("fileId", "id"); + +-- +-- Class ProtectedRange as table protected_range +-- +CREATE TABLE "protected_range" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid_v7(), + "fileId" bigint NOT NULL, + "revision" bigint NOT NULL DEFAULT 0, + "sheetId" uuid NOT NULL, + "rangeJson" text, + "exceptRangesJson" text, + "description" text, + "warningOnly" boolean NOT NULL DEFAULT false, + "editorsJson" text NOT NULL, + "createdBy" uuid NOT NULL, + "createdAt" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE INDEX "protected_range_file_idx" ON "protected_range" USING btree ("fileId", "id"); + +-- +-- Class Publication as table publication +-- +CREATE TABLE "publication" ( + "id" bigserial PRIMARY KEY, + "publicId" text NOT NULL, + "ownerId" bigint NOT NULL, + "name" text NOT NULL, + "siteUrl" text NOT NULL, + "webhookUrl" text NOT NULL, + "verifiedHost" text, + "verifiedAt" timestamp without time zone, + "templateItemId" bigint, + "termsUrl" text, + "fields" json NOT NULL, + "signingSecret" text NOT NULL, + "previousSigningSecret" text, + "previousSecretUntil" timestamp without time zone, + "apiKeyHash" text, + "apiKeyLast4" text, + "enabled" boolean NOT NULL DEFAULT true, + "createdAt" timestamp without time zone NOT NULL, + "updatedAt" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE UNIQUE INDEX "publication_public_idx" ON "publication" USING btree ("publicId"); +CREATE INDEX "publication_owner_idx" ON "publication" USING btree ("ownerId"); +CREATE INDEX "publication_api_key_idx" ON "publication" USING btree ("apiKeyHash"); + +-- +-- Class PublicationBlock as table publication_block +-- +CREATE TABLE "publication_block" ( + "id" bigserial PRIMARY KEY, + "publicationId" bigint NOT NULL, + "userId" bigint NOT NULL, + "at" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE UNIQUE INDEX "publication_block_idx" ON "publication_block" USING btree ("publicationId", "userId"); + +-- +-- Class PublicationConsent as table publication_consent +-- +CREATE TABLE "publication_consent" ( + "id" bigserial PRIMARY KEY, + "publicationId" bigint NOT NULL, + "userId" bigint NOT NULL, + "consentKey" text NOT NULL, + "at" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE UNIQUE INDEX "publication_consent_idx" ON "publication_consent" USING btree ("publicationId", "userId"); + +-- +-- Class PublicationDelivery as table publication_delivery +-- +CREATE TABLE "publication_delivery" ( + "id" bigserial PRIMARY KEY, + "publicationId" bigint NOT NULL, + "submissionId" bigint, + "webhookId" text NOT NULL, + "type" text NOT NULL, + "sequence" bigint, + "payload" text NOT NULL, + "attempts" bigint NOT NULL DEFAULT 0, + "nextAttemptAt" timestamp without time zone, + "leaseUntil" timestamp without time zone, + "lastStatus" bigint, + "lastError" text, + "deliveredAt" timestamp without time zone, + "failedAt" timestamp without time zone, + "createdAt" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE UNIQUE INDEX "publication_delivery_webhook_idx" ON "publication_delivery" USING btree ("webhookId"); +CREATE INDEX "publication_delivery_due_idx" ON "publication_delivery" USING btree ("nextAttemptAt"); +CREATE INDEX "publication_delivery_publication_idx" ON "publication_delivery" USING btree ("publicationId", "createdAt"); + +-- +-- Class PublicationModerator as table publication_moderator +-- +CREATE TABLE "publication_moderator" ( + "id" bigserial PRIMARY KEY, + "publicationId" bigint NOT NULL, + "userId" bigint, + "email" text NOT NULL +); + +-- Indexes +CREATE UNIQUE INDEX "publication_moderator_idx" ON "publication_moderator" USING btree ("publicationId", "email"); +CREATE INDEX "publication_moderator_user_idx" ON "publication_moderator" USING btree ("userId"); + +-- +-- Class RevFile as table rev_file +-- +CREATE TABLE "rev_file" ( + "id" bigserial PRIMARY KEY, + "fileUuid" uuid NOT NULL, + "kind" text NOT NULL, + "itemId" bigint NOT NULL, + "headRevision" bigint NOT NULL DEFAULT 0, + "prunedBelow" bigint NOT NULL DEFAULT 0, + "initPending" boolean NOT NULL DEFAULT false, + "purging" boolean NOT NULL DEFAULT false, + "schemaHash" text NOT NULL, + "stats" text NOT NULL DEFAULT '{}'::text, + "contentCount" bigint NOT NULL DEFAULT 0, + "createdAt" timestamp without time zone NOT NULL, + "updatedAt" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE UNIQUE INDEX "rev_file_uuid_idx" ON "rev_file" USING btree ("fileUuid"); +CREATE UNIQUE INDEX "rev_file_item_idx" ON "rev_file" USING btree ("itemId"); + +-- +-- Class RevHookState as table rev_hook_state +-- +CREATE TABLE "rev_hook_state" ( + "id" bigserial PRIMARY KEY, + "fileId" bigint NOT NULL, + "cursorsJson" text NOT NULL, + "leaseOwner" text, + "leaseUntil" timestamp without time zone, + "attempts" bigint NOT NULL DEFAULT 0, + "lastError" text, + "nextAttemptAt" timestamp without time zone, + "updatedAt" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE UNIQUE INDEX "rev_hook_state_file_idx" ON "rev_hook_state" USING btree ("fileId"); +CREATE INDEX "rev_hook_state_due_idx" ON "rev_hook_state" USING btree ("nextAttemptAt"); + +-- +-- Class RevOp as table rev_op +-- +CREATE TABLE "rev_op" ( + "id" bigserial PRIMARY KEY, + "fileId" bigint NOT NULL, + "revision" bigint NOT NULL, + "clientOpId" uuid NOT NULL, + "baseRevision" bigint NOT NULL, + "authorId" bigint, + "authorUuid" uuid, + "sessionId" text, + "onBehalfOfSessionId" text, + "actorKind" text NOT NULL, + "label" text NOT NULL, + "groupId" text, + "groupIndex" bigint NOT NULL DEFAULT 0, + "groupLast" boolean NOT NULL DEFAULT true, + "recordId" uuid, + "offline" boolean NOT NULL DEFAULT false, + "payload" bytea, + "changeCount" bigint NOT NULL, + "summaryJson" text, + "detailJson" text, + "outcomeJson" text, + "appliedEqualsSubmitted" boolean NOT NULL, + "clientCreatedAt" timestamp without time zone, + "committedAt" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE UNIQUE INDEX "rev_op_rev_idx" ON "rev_op" USING btree ("fileId", "revision"); +CREATE UNIQUE INDEX "rev_op_client_idx" ON "rev_op" USING btree ("fileId", "clientOpId"); +CREATE INDEX "rev_op_record_idx" ON "rev_op" USING btree ("recordId"); +CREATE INDEX "rev_op_committed_idx" ON "rev_op" USING brin ("committedAt"); + +-- +-- Class ScriptAuthorization as table script_authorization +-- +CREATE TABLE "script_authorization" ( + "id" bigserial PRIMARY KEY, + "userId" bigint NOT NULL, + "spreadsheetUuid" uuid NOT NULL, + "scopes" json NOT NULL, + "grantedAt" timestamp without time zone NOT NULL, + "revokedAt" timestamp without time zone +); + +-- Indexes +CREATE UNIQUE INDEX "script_auth_user_sheet_idx" ON "script_authorization" USING btree ("userId", "spreadsheetUuid"); + +-- +-- Class ScriptBuild as table script_build +-- +CREATE TABLE "script_build" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid_v7(), + "fileId" bigint NOT NULL, + "revision" bigint NOT NULL DEFAULT 0, + "projectId" uuid NOT NULL, + "status" text NOT NULL DEFAULT 'ok'::text, + "sourceHash" text NOT NULL, + "bytecodeFileUuid" uuid, + "diagnosticsJson" text NOT NULL, + "exportsJson" text NOT NULL, + "apiVersion" bigint NOT NULL DEFAULT 1, + "compilerVersion" text NOT NULL DEFAULT ''::text, + "requestedBy" uuid, + "lastGoodSourceHash" text, + "lastGoodBytecodeFileUuid" uuid, + "lastGoodExportsJson" text, + "lastGoodBuiltAt" timestamp without time zone, + "positionsFileUuid" uuid, + "builtAt" timestamp without time zone NOT NULL, + "createdAt" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE INDEX "script_build_file_idx" ON "script_build" USING btree ("fileId", "id"); + +-- +-- Class ScriptExecution as table script_execution +-- +CREATE TABLE "script_execution" ( + "id" bigserial PRIMARY KEY, + "uuid" uuid NOT NULL, + "spreadsheetUuid" uuid NOT NULL, + "projectId" uuid, + "functionName" text NOT NULL, + "kind" text NOT NULL, + "triggerId" uuid, + "userId" bigint NOT NULL, + "runAsUserId" bigint NOT NULL, + "runtime" text NOT NULL, + "status" text NOT NULL, + "buildSourceHash" text NOT NULL, + "startedAt" timestamp without time zone NOT NULL, + "durationMs" bigint NOT NULL, + "logs" text NOT NULL, + "errorMessage" text, + "errorStackJson" text, + "changeId" uuid, + "cellsRead" bigint NOT NULL, + "cellsWritten" bigint NOT NULL, + "fetchCalls" bigint NOT NULL, + "callCount" bigint NOT NULL DEFAULT 1 +); + +-- Indexes +CREATE UNIQUE INDEX "script_exec_uuid_idx" ON "script_execution" USING btree ("uuid"); +CREATE INDEX "script_exec_sheet_idx" ON "script_execution" USING btree ("spreadsheetUuid", "startedAt"); +CREATE INDEX "script_exec_user_idx" ON "script_execution" USING btree ("userId", "startedAt"); + +-- +-- Class ScriptFile as table script_file +-- +CREATE TABLE "script_file" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid_v7(), + "fileId" bigint NOT NULL, + "revision" bigint NOT NULL DEFAULT 0, + "projectId" uuid NOT NULL, + "name" text NOT NULL, + "sortKey" text NOT NULL, + "source" text NOT NULL, + "contentHash" text NOT NULL, + "updatedBy" uuid NOT NULL, + "updatedAt" timestamp without time zone NOT NULL, + "createdAt" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE INDEX "script_file_file_idx" ON "script_file" USING btree ("fileId", "id"); + +-- +-- Class ScriptJob as table script_job +-- +CREATE TABLE "script_job" ( + "id" bigserial PRIMARY KEY, + "kind" text NOT NULL, + "spreadsheetUuid" uuid NOT NULL, + "itemUuid" uuid NOT NULL, + "triggerId" uuid, + "runAsUserId" bigint NOT NULL, + "functionName" text NOT NULL, + "payloadJson" text NOT NULL, + "status" text NOT NULL, + "enqueuedAt" timestamp without time zone NOT NULL, + "leaseOwner" text, + "leaseUntil" timestamp without time zone, + "attempts" bigint NOT NULL DEFAULT 0, + "coalesced" bigint NOT NULL DEFAULT 0, + "resultJson" text, + "finishedAt" timestamp without time zone +); + +-- Indexes +CREATE INDEX "script_job_status_idx" ON "script_job" USING btree ("status", "enqueuedAt"); +CREATE INDEX "script_job_trigger_idx" ON "script_job" USING btree ("triggerId", "status"); +CREATE INDEX "script_job_user_idx" ON "script_job" USING btree ("runAsUserId", "status"); + +-- +-- Class ScriptProject as table script_project +-- +CREATE TABLE "script_project" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid_v7(), + "fileId" bigint NOT NULL, + "revision" bigint NOT NULL DEFAULT 0, + "spreadsheetId" uuid NOT NULL, + "name" text NOT NULL, + "settingsJson" text NOT NULL, + "createdAt" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE INDEX "script_project_file_idx" ON "script_project" USING btree ("fileId", "id"); + +-- +-- Class ScriptProperty as table script_property +-- +CREATE TABLE "script_property" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid_v7(), + "fileId" bigint NOT NULL, + "revision" bigint NOT NULL DEFAULT 0, + "projectId" uuid NOT NULL, + "key" text NOT NULL, + "value" text NOT NULL, + "createdAt" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE INDEX "script_property_file_idx" ON "script_property" USING btree ("fileId", "id"); + +-- +-- Class ScriptQuotaUsage as table script_quota_usage +-- +CREATE TABLE "script_quota_usage" ( + "id" bigserial PRIMARY KEY, + "userId" bigint NOT NULL, + "day" timestamp without time zone NOT NULL, + "serverRuntimeMs" bigint NOT NULL DEFAULT 0, + "clientRuntimeMs" bigint NOT NULL DEFAULT 0, + "urlFetchCalls" bigint NOT NULL DEFAULT 0, + "urlFetchBytes" bigint NOT NULL DEFAULT 0, + "triggerRuns" bigint NOT NULL DEFAULT 0 +); + +-- Indexes +CREATE UNIQUE INDEX "script_quota_user_day_idx" ON "script_quota_usage" USING btree ("userId", "day"); + +-- +-- Class ScriptTrigger as table script_trigger +-- +CREATE TABLE "script_trigger" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid_v7(), + "fileId" bigint NOT NULL, + "revision" bigint NOT NULL DEFAULT 0, + "projectId" uuid NOT NULL, + "kind" text NOT NULL, + "functionName" text NOT NULL, + "scheduleJson" text, + "runAsUserId" uuid NOT NULL, + "enabled" boolean NOT NULL DEFAULT true, + "optionsJson" text, + "createdAt" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE INDEX "script_trigger_file_idx" ON "script_trigger" USING btree ("fileId", "id"); + +-- +-- Class ScriptTriggerState as table script_trigger_state +-- +CREATE TABLE "script_trigger_state" ( + "id" bigserial PRIMARY KEY, + "triggerId" uuid NOT NULL, + "spreadsheetUuid" uuid NOT NULL, + "scheduleJson" text, + "nextRunAt" timestamp without time zone, + "lastRunAt" timestamp without time zone, + "lastStatus" text, + "consecutiveFailures" bigint NOT NULL DEFAULT 0, + "disabledReason" text, + "runningSince" timestamp without time zone, + "lastNotifiedAt" timestamp without time zone +); + +-- Indexes +CREATE UNIQUE INDEX "script_trigger_state_id_idx" ON "script_trigger_state" USING btree ("triggerId"); +CREATE INDEX "script_trigger_state_due_idx" ON "script_trigger_state" USING btree ("nextRunAt"); + +-- +-- Class ScriptUserProperty as table script_user_property +-- +CREATE TABLE "script_user_property" ( + "id" bigserial PRIMARY KEY, + "userId" bigint NOT NULL, + "fileUuid" uuid NOT NULL, + "key" text NOT NULL, + "value" text NOT NULL, + "updatedAt" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE UNIQUE INDEX "script_user_property_idx" ON "script_user_property" USING btree ("userId", "fileUuid", "key"); + +-- +-- Class ServerChangeRecord as table server_change_record +-- +CREATE TABLE "server_change_record" ( + "id" bigserial PRIMARY KEY, + "changeId" uuid NOT NULL, + "fileUuid" uuid NOT NULL, + "userId" bigint NOT NULL, + "actorKind" text NOT NULL, + "label" text NOT NULL, + "inverseJson" text, + "inverseFileId" uuid, + "forwardFileId" uuid, + "recordId" uuid, + "cellsAffected" bigint NOT NULL, + "createdAt" timestamp without time zone NOT NULL, + "expiresAt" timestamp without time zone NOT NULL, + "revertedAt" timestamp without time zone, + "revertChangeId" uuid +); + +-- Indexes +CREATE INDEX "server_change_record_idx" ON "server_change_record" USING btree ("recordId"); +CREATE INDEX "server_change_file_idx" ON "server_change_record" USING btree ("fileUuid", "createdAt"); +CREATE UNIQUE INDEX "server_change_id_idx" ON "server_change_record" USING btree ("changeId"); + +-- +-- Class ShareLink as table share_link +-- +CREATE TABLE "share_link" ( + "id" bigserial PRIMARY KEY, + "itemId" bigint NOT NULL, + "token" text NOT NULL, + "role" text, + "createdById" bigint NOT NULL, + "createdAt" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE UNIQUE INDEX "share_link_item_idx" ON "share_link" USING btree ("itemId"); +CREATE UNIQUE INDEX "share_link_token_idx" ON "share_link" USING btree ("token"); + +-- +-- Class Sheet as table sheet +-- +CREATE TABLE "sheet" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid_v7(), + "fileId" bigint NOT NULL, + "revision" bigint NOT NULL DEFAULT 0, + "spreadsheetId" uuid NOT NULL, + "name" text NOT NULL, + "sortKey" text NOT NULL, + "kind" text NOT NULL DEFAULT 'grid'::text, + "hidden" boolean NOT NULL DEFAULT false, + "tabColor" text, + "rightToLeft" boolean NOT NULL DEFAULT false, + "showGridlines" boolean NOT NULL DEFAULT true, + "frozenRows" bigint NOT NULL DEFAULT 0, + "frozenColumns" bigint NOT NULL DEFAULT 0, + "defaultRowHeight" double precision NOT NULL DEFAULT 21.0, + "defaultColumnWidth" double precision NOT NULL DEFAULT 100.0, + "formatJson" text, + "printSettingsJson" text, + "createdAt" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE INDEX "sheet_file_idx" ON "sheet" USING btree ("fileId", "id"); +CREATE INDEX "sheet_order_idx" ON "sheet" USING btree ("spreadsheetId", "sortKey"); + +-- +-- Class SheetColumn as table sheet_column +-- +CREATE TABLE "sheet_column" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid_v7(), + "fileId" bigint NOT NULL, + "revision" bigint NOT NULL DEFAULT 0, + "sheetId" uuid NOT NULL, + "sortKey" text NOT NULL, + "width" double precision, + "hidden" boolean NOT NULL DEFAULT false, + "formatJson" text, + "createdAt" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE INDEX "sheet_column_file_idx" ON "sheet_column" USING btree ("fileId", "id"); +CREATE INDEX "sheet_column_order_idx" ON "sheet_column" USING btree ("sheetId", "sortKey"); + +-- +-- Class SheetRow as table sheet_row +-- +CREATE TABLE "sheet_row" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid_v7(), + "fileId" bigint NOT NULL, + "revision" bigint NOT NULL DEFAULT 0, + "sheetId" uuid NOT NULL, + "sortKey" text NOT NULL, + "height" double precision, + "hidden" boolean NOT NULL DEFAULT false, + "formatJson" text, + "createdAt" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE INDEX "sheet_row_file_idx" ON "sheet_row" USING btree ("fileId", "id"); +CREATE INDEX "sheet_row_order_idx" ON "sheet_row" USING btree ("sheetId", "sortKey"); + +-- +-- Class SheetsImportJob as table sheets_import_job +-- +CREATE TABLE "sheets_import_job" ( + "id" bigserial PRIMARY KEY, + "uuid" uuid NOT NULL, + "userId" bigint NOT NULL, + "authUserId" uuid NOT NULL, + "spreadsheetUuid" uuid NOT NULL, + "uploadFileId" uuid NOT NULL, + "fileName" text NOT NULL, + "format" text NOT NULL, + "optionsJson" text NOT NULL, + "activeSheetId" text, + "status" text NOT NULL, + "progressDone" bigint NOT NULL, + "progressTotal" bigint NOT NULL, + "reportJson" text, + "sheetIdsJson" text, + "changeId" uuid, + "errorCode" text, + "error" text, + "cancelRequested" boolean NOT NULL DEFAULT false, + "createdAt" timestamp without time zone NOT NULL, + "updatedAt" timestamp without time zone NOT NULL, + "leaseOwner" text, + "leaseUntil" timestamp without time zone +); + +-- Indexes +CREATE UNIQUE INDEX "sheets_import_job_uuid_idx" ON "sheets_import_job" USING btree ("uuid"); +CREATE INDEX "sheets_import_job_user_idx" ON "sheets_import_job" USING btree ("userId", "status"); + +-- +-- Class SheetsSnapshotIndex as table sheets_snapshot_index +-- +CREATE TABLE "sheets_snapshot_index" ( + "id" bigserial PRIMARY KEY, + "itemVersionId" bigint NOT NULL, + "sheetsJson" text NOT NULL, + "changedRangesJson" text NOT NULL, + "authorIds" json NOT NULL, + "diffFileId" uuid +); + +-- Indexes +CREATE UNIQUE INDEX "sheets_snapshot_index_version_idx" ON "sheets_snapshot_index" USING btree ("itemVersionId"); + +-- +-- Class Slide as table slide +-- +CREATE TABLE "slide" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid_v7(), + "fileId" bigint NOT NULL, + "revision" bigint NOT NULL DEFAULT 0, + "deckId" uuid NOT NULL, + "sortKey" text NOT NULL, + "groupId" uuid, + "layoutId" uuid, + "backgroundJson" text, + "skipped" boolean NOT NULL DEFAULT false, + "notes" text, + "transitionJson" text, + "hideMasterElements" boolean, + "timingsJson" text, + "createdAt" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE INDEX "slide_file_idx" ON "slide" USING btree ("fileId", "id"); +CREATE INDEX "slide_order_idx" ON "slide" USING btree ("deckId", "sortKey"); + +-- +-- Class SlideBuild as table slide_build +-- +CREATE TABLE "slide_build" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid_v7(), + "fileId" bigint NOT NULL, + "revision" bigint NOT NULL DEFAULT 0, + "slideId" uuid NOT NULL, + "elementId" uuid NOT NULL, + "kind" text NOT NULL, + "effect" text NOT NULL, + "sortKey" text NOT NULL, + "trigger" text NOT NULL, + "delayMs" bigint NOT NULL DEFAULT 0, + "durationMs" bigint NOT NULL, + "easing" text NOT NULL, + "optionsJson" text, + "createdAt" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE INDEX "slide_build_file_idx" ON "slide_build" USING btree ("fileId", "id"); +CREATE INDEX "slide_build_slide_idx" ON "slide_build" USING btree ("slideId", "sortKey"); +CREATE INDEX "slide_build_element_idx" ON "slide_build" USING btree ("elementId"); + +-- +-- Class SlideElement as table slide_element +-- +CREATE TABLE "slide_element" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid_v7(), + "fileId" bigint NOT NULL, + "revision" bigint NOT NULL DEFAULT 0, + "slideId" uuid, + "layoutId" uuid, + "masterId" uuid, + "parentId" uuid, + "kind" text NOT NULL, + "name" text, + "x" double precision, + "y" double precision, + "width" double precision, + "height" double precision, + "rotation" double precision, + "flipH" boolean NOT NULL DEFAULT false, + "flipV" boolean NOT NULL DEFAULT false, + "zKey" text NOT NULL, + "readingKey" text, + "locked" boolean NOT NULL DEFAULT false, + "hidden" boolean NOT NULL DEFAULT false, + "placeholderType" text, + "placeholderOf" uuid, + "matchKey" text NOT NULL, + "text" text, + "textStyleJson" text, + "fillJson" text, + "strokeJson" text, + "effectsJson" text, + "opacity" double precision, + "dataJson" text, + "linkJson" text, + "altText" text, + "decorative" boolean NOT NULL DEFAULT false, + "createdAt" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE INDEX "slide_element_file_idx" ON "slide_element" USING btree ("fileId", "id"); +CREATE INDEX "slide_element_slide_idx" ON "slide_element" USING btree ("slideId", "zKey"); +CREATE INDEX "slide_element_layout_idx" ON "slide_element" USING btree ("layoutId"); +CREATE INDEX "slide_element_master_idx" ON "slide_element" USING btree ("masterId"); +CREATE INDEX "slide_element_parent_idx" ON "slide_element" USING btree ("parentId"); + +-- +-- Class SlideGroup as table slide_group +-- +CREATE TABLE "slide_group" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid_v7(), + "fileId" bigint NOT NULL, + "revision" bigint NOT NULL DEFAULT 0, + "deckId" uuid NOT NULL, + "title" text NOT NULL, + "sortKey" text NOT NULL, + "parentId" uuid, + "color" text, + "skipped" boolean NOT NULL DEFAULT false, + "createdAt" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE INDEX "slide_group_file_idx" ON "slide_group" USING btree ("fileId", "id"); +CREATE INDEX "slide_group_order_idx" ON "slide_group" USING btree ("deckId", "sortKey"); + +-- +-- Class SlideLayout as table slide_layout +-- +CREATE TABLE "slide_layout" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid_v7(), + "fileId" bigint NOT NULL, + "revision" bigint NOT NULL DEFAULT 0, + "masterId" uuid NOT NULL, + "name" text NOT NULL, + "key" text, + "sortKey" text NOT NULL, + "backgroundJson" text, + "hideMasterElements" boolean NOT NULL DEFAULT false, + "hidden" boolean NOT NULL DEFAULT false, + "createdAt" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE INDEX "slide_layout_file_idx" ON "slide_layout" USING btree ("fileId", "id"); +CREATE INDEX "slide_layout_master_idx" ON "slide_layout" USING btree ("masterId"); + +-- +-- Class SlideMaster as table slide_master +-- +CREATE TABLE "slide_master" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid_v7(), + "fileId" bigint NOT NULL, + "revision" bigint NOT NULL DEFAULT 0, + "themeId" uuid NOT NULL, + "name" text NOT NULL, + "sortKey" text NOT NULL, + "backgroundJson" text, + "createdAt" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE INDEX "slide_master_file_idx" ON "slide_master" USING btree ("fileId", "id"); +CREATE INDEX "slide_master_theme_idx" ON "slide_master" USING btree ("themeId"); + +-- +-- Class SlideTableAxis as table slide_table_axis +-- +CREATE TABLE "slide_table_axis" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid_v7(), + "fileId" bigint NOT NULL, + "revision" bigint NOT NULL DEFAULT 0, + "elementId" uuid NOT NULL, + "dimension" text NOT NULL, + "sortKey" text NOT NULL, + "size" double precision, + "header" boolean NOT NULL DEFAULT false, + "createdAt" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE INDEX "slide_table_axis_file_idx" ON "slide_table_axis" USING btree ("fileId", "id"); +CREATE INDEX "slide_table_axis_element_idx" ON "slide_table_axis" USING btree ("elementId", "sortKey"); + +-- +-- Class SlideTableCell as table slide_table_cell +-- +CREATE TABLE "slide_table_cell" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid_v7(), + "fileId" bigint NOT NULL, + "revision" bigint NOT NULL DEFAULT 0, + "elementId" uuid NOT NULL, + "rowId" uuid NOT NULL, + "columnId" uuid NOT NULL, + "text" text, + "fill" text, + "borderTop" text, + "borderRight" text, + "borderBottom" text, + "borderLeft" text, + "vAlign" text, + "padding" text, + "rowSpan" bigint, + "colSpan" bigint, + "createdAt" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE INDEX "slide_table_cell_file_idx" ON "slide_table_cell" USING btree ("fileId", "id"); +CREATE INDEX "slide_table_cell_element_idx" ON "slide_table_cell" USING btree ("elementId"); + +-- +-- Class SlidesSnapshotIndex as table slides_snapshot_index +-- +CREATE TABLE "slides_snapshot_index" ( + "id" bigserial PRIMARY KEY, + "itemVersionId" bigint NOT NULL, + "slidesJson" text NOT NULL, + "changesJson" text NOT NULL, + "authorIds" json NOT NULL +); + +-- Indexes +CREATE UNIQUE INDEX "slides_snapshot_index_version_idx" ON "slides_snapshot_index" USING btree ("itemVersionId"); + +-- +-- Class Spreadsheet as table spreadsheet +-- +CREATE TABLE "spreadsheet" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid_v7(), + "fileId" bigint NOT NULL, + "revision" bigint NOT NULL DEFAULT 0, + "title" text NOT NULL, + "locale" text NOT NULL, + "timeZone" text NOT NULL, + "recalcMode" text NOT NULL DEFAULT 'onChange'::text, + "iterativeCalc" boolean NOT NULL DEFAULT false, + "maxIterations" bigint NOT NULL DEFAULT 50, + "convergenceThreshold" double precision NOT NULL DEFAULT 0.05, + "defaultFontFamily" text NOT NULL DEFAULT 'Inter'::text, + "defaultFontSize" double precision NOT NULL DEFAULT 10.0, + "themeJson" text NOT NULL, + "displayLanguage" text, + "createdBy" uuid NOT NULL, + "createdAt" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE INDEX "spreadsheet_file_idx" ON "spreadsheet" USING btree ("fileId", "id"); + +-- +-- Class StorageReservation as table storage_reservation +-- +CREATE TABLE "storage_reservation" ( + "id" bigserial PRIMARY KEY, + "userId" bigint NOT NULL, + "fileUuid" uuid NOT NULL, + "itemId" bigint, + "bytes" bigint NOT NULL, + "createdAt" timestamp without time zone NOT NULL, + "expiresAt" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE UNIQUE INDEX "storage_reservation_file_idx" ON "storage_reservation" USING btree ("fileUuid"); +CREATE INDEX "storage_reservation_user_idx" ON "storage_reservation" USING btree ("userId"); +CREATE INDEX "storage_reservation_expiry_idx" ON "storage_reservation" USING btree ("expiresAt"); + +-- +-- Class StoredFile as table stored_file +-- +CREATE TABLE "stored_file" ( + "id" bigserial PRIMARY KEY, + "fileUuid" uuid NOT NULL, + "ownerId" bigint, + "itemId" bigint, + "kind" text NOT NULL, + "storageId" text NOT NULL, + "path" text NOT NULL, + "fileName" text NOT NULL, + "mimeType" text NOT NULL, + "sizeBytes" bigint NOT NULL, + "status" text NOT NULL, + "createdAt" timestamp without time zone NOT NULL, + "completedAt" timestamp without time zone, + "expiresAt" timestamp without time zone +); + +-- Indexes +CREATE UNIQUE INDEX "stored_file_uuid_idx" ON "stored_file" USING btree ("fileUuid"); +CREATE INDEX "stored_file_item_idx" ON "stored_file" USING btree ("itemId"); +CREATE INDEX "stored_file_expiry_idx" ON "stored_file" USING btree ("expiresAt"); +CREATE INDEX "stored_file_owner_kind_idx" ON "stored_file" USING btree ("ownerId", "kind", "createdAt"); +CREATE INDEX "stored_file_status_idx" ON "stored_file" USING btree ("status", "createdAt"); + +-- +-- Class Submission as table submission +-- +CREATE TABLE "submission" ( + "id" bigserial PRIMARY KEY, + "uuid" uuid NOT NULL, + "publicationId" bigint NOT NULL, + "itemId" bigint NOT NULL, + "ref" text, + "status" text NOT NULL, + "statusBeforeSubmit" text, + "delivery" text, + "values" json NOT NULL, + "publicUrl" text, + "feedbackThreadId" bigint, + "createdById" bigint NOT NULL, + "createdAt" timestamp without time zone NOT NULL, + "updatedAt" timestamp without time zone NOT NULL, + "disconnectedAt" timestamp without time zone +); + +-- Indexes +CREATE UNIQUE INDEX "submission_uuid_idx" ON "submission" USING btree ("uuid"); +CREATE UNIQUE INDEX "submission_item_idx" ON "submission" USING btree ("itemId"); +CREATE INDEX "submission_publication_idx" ON "submission" USING btree ("publicationId", "status"); + +-- +-- Class SubmissionAsset as table submission_asset +-- +CREATE TABLE "submission_asset" ( + "id" bigserial PRIMARY KEY, + "submissionId" bigint NOT NULL, + "fileUuid" uuid NOT NULL +); + +-- Indexes +CREATE UNIQUE INDEX "submission_asset_idx" ON "submission_asset" USING btree ("submissionId", "fileUuid"); +CREATE INDEX "submission_asset_file_idx" ON "submission_asset" USING btree ("fileUuid"); + +-- +-- Class SubmissionRevision as table submission_revision +-- +CREATE TABLE "submission_revision" ( + "id" bigserial PRIMARY KEY, + "submissionId" bigint NOT NULL, + "sequence" bigint NOT NULL, + "revision" bigint NOT NULL, + "versionId" bigint, + "values" json NOT NULL, + "payload" text NOT NULL, + "submittedById" bigint NOT NULL, + "submittedAt" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE UNIQUE INDEX "submission_revision_idx" ON "submission_revision" USING btree ("submissionId", "sequence"); + +-- +-- Class UsageCounterDay as table usage_counter_day +-- +CREATE TABLE "usage_counter_day" ( + "id" bigserial PRIMARY KEY, + "day" timestamp without time zone NOT NULL, + "name" text NOT NULL, + "value" bigint NOT NULL DEFAULT 0 +); + +-- Indexes +CREATE UNIQUE INDEX "usage_counter_day_idx" ON "usage_counter_day" USING btree ("day", "name"); + +-- +-- Class UsageDayStats as table usage_day_stats +-- +CREATE TABLE "usage_day_stats" ( + "id" bigserial PRIMARY KEY, + "day" timestamp without time zone NOT NULL, + "includeAdmins" boolean NOT NULL, + "active" bigint NOT NULL, + "editors" bigint NOT NULL, + "drive" bigint NOT NULL, + "write" bigint NOT NULL, + "sheets" bigint NOT NULL, + "slides" bigint NOT NULL, + "mcp" bigint NOT NULL, + "weekly" bigint NOT NULL, + "monthly" bigint NOT NULL, + "computedAt" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE UNIQUE INDEX "usage_day_stats_idx" ON "usage_day_stats" USING btree ("day", "includeAdmins"); + +-- +-- Class UserActivityDay as table user_activity_day +-- +CREATE TABLE "user_activity_day" ( + "id" bigserial PRIMARY KEY, + "userId" bigint NOT NULL, + "day" timestamp without time zone NOT NULL, + "app" text NOT NULL, + "edited" boolean NOT NULL DEFAULT false +); + +-- Indexes +CREATE UNIQUE INDEX "user_activity_day_idx" ON "user_activity_day" USING btree ("userId", "day", "app"); +CREATE INDEX "user_activity_day_day_idx" ON "user_activity_day" USING btree ("day"); + +-- +-- Class UserActivitySettings as table user_activity_settings +-- +CREATE TABLE "user_activity_settings" ( + "id" bigserial PRIMARY KEY, + "userId" bigint NOT NULL, + "showViewHistory" boolean NOT NULL DEFAULT true, + "updatedAt" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE UNIQUE INDEX "user_activity_settings_user_idx" ON "user_activity_settings" USING btree ("userId"); + +-- +-- Class UserPreferences as table user_preferences +-- +CREATE TABLE "user_preferences" ( + "id" bigserial PRIMARY KEY, + "userId" bigint NOT NULL, + "appTheme" text NOT NULL DEFAULT 'system'::text, + "accentColor" text, + "density" text NOT NULL DEFAULT 'comfortable'::text, + "reduceMotion" boolean NOT NULL DEFAULT false, + "keymapOverrides" text NOT NULL DEFAULT '{}'::text, + "writeDefaults" text NOT NULL DEFAULT '{}'::text, + "sheetsDefaults" text NOT NULL DEFAULT '{}'::text, + "slidesDefaults" text NOT NULL DEFAULT '{}'::text, + "slidesPresenter" text NOT NULL DEFAULT '{}'::text, + "driveDefaults" text NOT NULL DEFAULT '{}'::text, + "notificationSettings" text NOT NULL DEFAULT '{}'::text, + "aiEnabled" boolean NOT NULL DEFAULT true, + "updatedAt" timestamp without time zone +); + +-- Indexes +CREATE UNIQUE INDEX "user_preferences_user_idx" ON "user_preferences" USING btree ("userId"); + +-- +-- Class UserProfile as table user_profile +-- +CREATE TABLE "user_profile" ( + "id" bigserial PRIMARY KEY, + "authUserId" uuid NOT NULL, + "displayName" text NOT NULL, + "email" text NOT NULL, + "avatarFileId" uuid, + "avatarColor" text NOT NULL, + "locale" text, + "timeZone" text, + "createdAt" timestamp without time zone NOT NULL, + "lastSeenAt" timestamp without time zone, + "welcomeContentAt" timestamp without time zone +); + +-- Indexes +CREATE UNIQUE INDEX "user_profile_auth_idx" ON "user_profile" USING btree ("authUserId"); +CREATE UNIQUE INDEX "user_profile_email_idx" ON "user_profile" USING btree ("email"); +CREATE INDEX "user_profile_created_idx" ON "user_profile" USING btree ("createdAt"); + +-- +-- Class UserStorage as table user_storage +-- +CREATE TABLE "user_storage" ( + "id" bigserial PRIMARY KEY, + "userId" bigint NOT NULL, + "usedBytes" bigint NOT NULL DEFAULT 0, + "reservedBytes" bigint NOT NULL DEFAULT 0, + "documentsBytes" bigint NOT NULL DEFAULT 0, + "spreadsheetsBytes" bigint NOT NULL DEFAULT 0, + "presentationsBytes" bigint NOT NULL DEFAULT 0, + "filesBytes" bigint NOT NULL DEFAULT 0, + "trashBytes" bigint NOT NULL DEFAULT 0, + "quotaBytesOverride" bigint, + "overQuotaSince" timestamp without time zone, + "reconciledAt" timestamp without time zone, + "updatedAt" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE UNIQUE INDEX "user_storage_user_idx" ON "user_storage" USING btree ("userId"); + +-- +-- Class WriteDiffCache as table write_diff_cache +-- +CREATE TABLE "write_diff_cache" ( + "id" bigserial PRIMARY KEY, + "documentId" bigint NOT NULL, + "fromRevision" bigint NOT NULL, + "toRevision" bigint NOT NULL, + "json" text NOT NULL, + "createdAt" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE UNIQUE INDEX "write_diff_cache_idx" ON "write_diff_cache" USING btree ("documentId", "fromRevision", "toRevision"); +CREATE INDEX "write_diff_cache_created_idx" ON "write_diff_cache" USING brin ("createdAt"); + +-- +-- Class WriteTheme as table write_theme +-- +CREATE TABLE "write_theme" ( + "id" bigserial PRIMARY KEY, + "uuid" uuid NOT NULL, + "ownerId" bigint, + "isBuiltin" boolean NOT NULL, + "builtinKey" text, + "name" text NOT NULL, + "description" text, + "schemaVersion" bigint NOT NULL, + "version" bigint NOT NULL, + "json" text NOT NULL, + "contentHash" text NOT NULL, + "createdAt" timestamp without time zone NOT NULL, + "updatedAt" timestamp without time zone NOT NULL, + "deletedAt" timestamp without time zone +); + +-- Indexes +CREATE UNIQUE INDEX "write_theme_uuid_idx" ON "write_theme" USING btree ("uuid"); +CREATE INDEX "write_theme_owner_idx" ON "write_theme" USING btree ("ownerId", "deletedAt"); +CREATE UNIQUE INDEX "write_theme_builtin_idx" ON "write_theme" USING btree ("builtinKey"); + +-- +-- Class WriteThemeRevision as table write_theme_revision +-- +CREATE TABLE "write_theme_revision" ( + "id" bigserial PRIMARY KEY, + "themeId" bigint NOT NULL, + "version" bigint NOT NULL, + "json" text NOT NULL, + "savedById" bigint, + "actorKind" text NOT NULL, + "createdAt" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE UNIQUE INDEX "write_theme_rev_idx" ON "write_theme_revision" USING btree ("themeId", "version"); + +-- +-- Class CloudStorageEntry as table serverpod_cloud_storage +-- +CREATE TABLE "serverpod_cloud_storage" ( + "id" bigserial PRIMARY KEY, + "storageId" text NOT NULL, + "path" text NOT NULL, + "addedTime" timestamp without time zone NOT NULL, + "expiration" timestamp without time zone, + "byteData" bytea NOT NULL, + "verified" boolean NOT NULL, + "contentType" text, + "cacheControl" text, + "contentDisposition" text, + "contentEncoding" text, + "customMetadata" text +); + +-- Indexes +CREATE UNIQUE INDEX "serverpod_cloud_storage_path_idx" ON "serverpod_cloud_storage" USING btree ("storageId", "path"); +CREATE INDEX "serverpod_cloud_storage_expiration" ON "serverpod_cloud_storage" USING btree ("expiration"); + +-- +-- Class CloudStorageDirectDownloadEntry as table serverpod_cloud_storage_direct_download +-- +CREATE TABLE "serverpod_cloud_storage_direct_download" ( + "id" bigserial PRIMARY KEY, + "storageId" text NOT NULL, + "path" text NOT NULL, + "expiration" timestamp without time zone NOT NULL, + "authKey" text NOT NULL, + "downloadFileName" text, + "contentType" text +); + +-- Indexes +CREATE UNIQUE INDEX "serverpod_cloud_storage_direct_download_auth_key" ON "serverpod_cloud_storage_direct_download" USING btree ("authKey"); +CREATE INDEX "serverpod_cloud_storage_direct_download_expiration" ON "serverpod_cloud_storage_direct_download" USING btree ("expiration"); + +-- +-- Class CloudStorageDirectUploadEntry as table serverpod_cloud_storage_direct_upload +-- +CREATE TABLE "serverpod_cloud_storage_direct_upload" ( + "id" bigserial PRIMARY KEY, + "storageId" text NOT NULL, + "path" text NOT NULL, + "expiration" timestamp without time zone NOT NULL, + "authKey" text NOT NULL, + "maxFileSize" bigint NOT NULL DEFAULT 10485760, + "contentLength" bigint, + "preventOverwrite" boolean NOT NULL DEFAULT false, + "contentType" text, + "cacheControl" text, + "contentDisposition" text, + "contentEncoding" text, + "customMetadata" text +); + +-- Indexes +CREATE UNIQUE INDEX "serverpod_cloud_storage_direct_upload_storage_path" ON "serverpod_cloud_storage_direct_upload" USING btree ("storageId", "path"); + +-- +-- Class FutureCallEntry as table serverpod_future_call +-- +CREATE TABLE "serverpod_future_call" ( + "id" bigserial PRIMARY KEY, + "name" text NOT NULL, + "time" timestamp without time zone NOT NULL, + "serializedObject" text, + "serverId" text NOT NULL, + "identifier" text, + "scheduling" json +); + +-- Indexes +CREATE INDEX "serverpod_future_call_time_idx" ON "serverpod_future_call" USING btree ("time"); +CREATE INDEX "serverpod_future_call_serverId_idx" ON "serverpod_future_call" USING btree ("serverId"); +CREATE INDEX "serverpod_future_call_identifier_idx" ON "serverpod_future_call" USING btree ("identifier"); + +-- +-- Class FutureCallClaimEntry as table serverpod_future_call_claim +-- +CREATE TABLE "serverpod_future_call_claim" ( + "id" bigserial PRIMARY KEY, + "futureCallId" bigint, + "lastHeartbeatTime" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE UNIQUE INDEX "future_call_unique_idx" ON "serverpod_future_call_claim" USING btree ("futureCallId"); + +-- +-- Class ServerHealthConnectionInfo as table serverpod_health_connection_info +-- +CREATE TABLE "serverpod_health_connection_info" ( + "id" bigserial PRIMARY KEY, + "serverId" text NOT NULL, + "timestamp" timestamp without time zone NOT NULL, + "active" bigint NOT NULL, + "closing" bigint NOT NULL, + "idle" bigint NOT NULL, + "granularity" bigint NOT NULL +); + +-- Indexes +CREATE UNIQUE INDEX "serverpod_health_connection_info_timestamp_idx" ON "serverpod_health_connection_info" USING btree ("timestamp", "serverId", "granularity"); + +-- +-- Class ServerHealthMetric as table serverpod_health_metric +-- +CREATE TABLE "serverpod_health_metric" ( + "id" bigserial PRIMARY KEY, + "name" text NOT NULL, + "serverId" text NOT NULL, + "timestamp" timestamp without time zone NOT NULL, + "isHealthy" boolean NOT NULL, + "value" double precision NOT NULL, + "granularity" bigint NOT NULL +); + +-- Indexes +CREATE UNIQUE INDEX "serverpod_health_metric_timestamp_idx" ON "serverpod_health_metric" USING btree ("timestamp", "serverId", "name", "granularity"); + +-- +-- Class LogEntry as table serverpod_log +-- +CREATE TABLE "serverpod_log" ( + "id" bigserial PRIMARY KEY, + "sessionLogId" bigint NOT NULL, + "messageId" bigint, + "reference" text, + "serverId" text NOT NULL, + "time" timestamp without time zone NOT NULL, + "logLevel" bigint NOT NULL, + "message" text NOT NULL, + "error" text, + "stackTrace" text, + "order" bigint NOT NULL +); + +-- Indexes +CREATE INDEX "serverpod_log_sessionLogId_idx" ON "serverpod_log" USING btree ("sessionLogId", "order"); + +-- +-- Class MessageLogEntry as table serverpod_message_log +-- +CREATE TABLE "serverpod_message_log" ( + "id" bigserial PRIMARY KEY, + "sessionLogId" bigint NOT NULL, + "serverId" text NOT NULL, + "messageId" bigint NOT NULL, + "endpoint" text NOT NULL, + "messageName" text NOT NULL, + "duration" double precision NOT NULL, + "error" text, + "stackTrace" text, + "slow" boolean NOT NULL, + "order" bigint NOT NULL +); + +-- Indexes +CREATE INDEX "serverpod_message_log_sessionLogId_idx" ON "serverpod_message_log" USING btree ("sessionLogId", "order"); + +-- +-- Class MethodInfo as table serverpod_method +-- +CREATE TABLE "serverpod_method" ( + "id" bigserial PRIMARY KEY, + "endpoint" text NOT NULL, + "method" text NOT NULL +); + +-- Indexes +CREATE UNIQUE INDEX "serverpod_method_endpoint_method_idx" ON "serverpod_method" USING btree ("endpoint", "method"); + +-- +-- Class DatabaseMigrationVersion as table serverpod_migrations +-- +CREATE TABLE "serverpod_migrations" ( + "id" bigserial PRIMARY KEY, + "module" text NOT NULL, + "version" text NOT NULL, + "timestamp" timestamp without time zone +); + +-- Indexes +CREATE UNIQUE INDEX "serverpod_migrations_ids" ON "serverpod_migrations" USING btree ("module"); + +-- +-- Class QueryLogEntry as table serverpod_query_log +-- +CREATE TABLE "serverpod_query_log" ( + "id" bigserial PRIMARY KEY, + "serverId" text NOT NULL, + "sessionLogId" bigint NOT NULL, + "messageId" bigint, + "query" text NOT NULL, + "duration" double precision NOT NULL, + "numRows" bigint, + "error" text, + "stackTrace" text, + "slow" boolean NOT NULL, + "order" bigint NOT NULL +); + +-- Indexes +CREATE INDEX "serverpod_query_log_sessionLogId_idx" ON "serverpod_query_log" USING btree ("sessionLogId", "order"); + +-- +-- Class ReadWriteTestEntry as table serverpod_readwrite_test +-- +CREATE TABLE "serverpod_readwrite_test" ( + "id" bigserial PRIMARY KEY, + "number" bigint NOT NULL +); + +-- +-- Class RuntimeSettings as table serverpod_runtime_settings +-- +CREATE TABLE "serverpod_runtime_settings" ( + "id" bigserial PRIMARY KEY, + "logSettings" json NOT NULL, + "logSettingsOverrides" json NOT NULL, + "logServiceCalls" boolean NOT NULL, + "logMalformedCalls" boolean NOT NULL +); + +-- +-- Class SessionLogEntry as table serverpod_session_log +-- +CREATE TABLE "serverpod_session_log" ( + "id" bigserial PRIMARY KEY, + "serverId" text NOT NULL, + "time" timestamp without time zone NOT NULL, + "module" text, + "endpoint" text, + "method" text, + "duration" double precision, + "numQueries" bigint, + "slow" boolean, + "error" text, + "stackTrace" text, + "authenticatedUserId" bigint, + "userId" text, + "isOpen" boolean, + "touched" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE INDEX "serverpod_session_log_serverid_idx" ON "serverpod_session_log" USING btree ("serverId"); +CREATE INDEX "serverpod_session_log_time_idx" ON "serverpod_session_log" USING btree ("time"); +CREATE INDEX "serverpod_session_log_touched_idx" ON "serverpod_session_log" USING btree ("touched"); +CREATE INDEX "serverpod_session_log_isopen_idx" ON "serverpod_session_log" USING btree ("isOpen"); + +-- +-- Class AnonymousAccount as table serverpod_auth_idp_anonymous_account +-- +CREATE TABLE "serverpod_auth_idp_anonymous_account" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid_v7(), + "authUserId" uuid NOT NULL, + "createdAt" timestamp without time zone NOT NULL +); + +-- +-- Class AppleAccount as table serverpod_auth_idp_apple_account +-- +CREATE TABLE "serverpod_auth_idp_apple_account" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid_v7(), + "userIdentifier" text NOT NULL, + "refreshToken" text NOT NULL, + "refreshTokenRequestedWithBundleIdentifier" boolean NOT NULL, + "lastRefreshedAt" timestamp without time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, + "authUserId" uuid NOT NULL, + "createdAt" timestamp without time zone NOT NULL, + "email" text, + "isEmailVerified" boolean, + "isPrivateEmail" boolean, + "firstName" text, + "lastName" text +); + +-- Indexes +CREATE UNIQUE INDEX "serverpod_auth_apple_account_identifier" ON "serverpod_auth_idp_apple_account" USING btree ("userIdentifier"); + +-- +-- Class EmailAccount as table serverpod_auth_idp_email_account +-- +CREATE TABLE "serverpod_auth_idp_email_account" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid_v7(), + "authUserId" uuid NOT NULL, + "createdAt" timestamp without time zone NOT NULL, + "email" text NOT NULL, + "passwordHash" text NOT NULL +); + +-- Indexes +CREATE UNIQUE INDEX "serverpod_auth_idp_email_account_email" ON "serverpod_auth_idp_email_account" USING btree ("email"); + +-- +-- Class EmailAccountOtpLoginRequest as table serverpod_auth_idp_email_account_otp_login_request +-- +CREATE TABLE "serverpod_auth_idp_email_account_otp_login_request" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid_v7(), + "createdAt" timestamp without time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, + "email" text NOT NULL, + "challengeId" uuid NOT NULL, + "completeChallengeId" uuid +); + +-- Indexes +CREATE UNIQUE INDEX "serverpod_auth_idp_email_account_otp_login_request_email" ON "serverpod_auth_idp_email_account_otp_login_request" USING btree ("email"); + +-- +-- Class EmailAccountPasswordResetRequest as table serverpod_auth_idp_email_account_password_reset_request +-- +CREATE TABLE "serverpod_auth_idp_email_account_password_reset_request" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid_v7(), + "emailAccountId" uuid NOT NULL, + "createdAt" timestamp without time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, + "challengeId" uuid NOT NULL, + "setPasswordChallengeId" uuid +); + +-- +-- Class EmailAccountRequest as table serverpod_auth_idp_email_account_request +-- +CREATE TABLE "serverpod_auth_idp_email_account_request" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid_v7(), + "createdAt" timestamp without time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, + "email" text NOT NULL, + "challengeId" uuid NOT NULL, + "createAccountChallengeId" uuid +); + +-- Indexes +CREATE UNIQUE INDEX "serverpod_auth_idp_email_account_request_email" ON "serverpod_auth_idp_email_account_request" USING btree ("email"); + +-- +-- Class FacebookAccount as table serverpod_auth_idp_facebook_account +-- +CREATE TABLE "serverpod_auth_idp_facebook_account" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid_v7(), + "authUserId" uuid NOT NULL, + "createdAt" timestamp without time zone NOT NULL, + "userIdentifier" text NOT NULL, + "email" text, + "fullName" text, + "firstName" text, + "lastName" text +); + +-- Indexes +CREATE UNIQUE INDEX "serverpod_auth_facebook_account_user_identifier" ON "serverpod_auth_idp_facebook_account" USING btree ("userIdentifier"); + +-- +-- Class FirebaseAccount as table serverpod_auth_idp_firebase_account +-- +CREATE TABLE "serverpod_auth_idp_firebase_account" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid_v7(), + "authUserId" uuid NOT NULL, + "created" timestamp without time zone NOT NULL, + "email" text, + "phone" text, + "userIdentifier" text NOT NULL +); + +-- Indexes +CREATE UNIQUE INDEX "serverpod_auth_firebase_account_user_identifier" ON "serverpod_auth_idp_firebase_account" USING btree ("userIdentifier"); + +-- +-- Class GitHubAccount as table serverpod_auth_idp_github_account +-- +CREATE TABLE "serverpod_auth_idp_github_account" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid_v7(), + "authUserId" uuid NOT NULL, + "userIdentifier" text NOT NULL, + "email" text, + "created" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE UNIQUE INDEX "serverpod_auth_github_account_user_identifier" ON "serverpod_auth_idp_github_account" USING btree ("userIdentifier"); + +-- +-- Class GoogleAccount as table serverpod_auth_idp_google_account +-- +CREATE TABLE "serverpod_auth_idp_google_account" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid_v7(), + "authUserId" uuid NOT NULL, + "created" timestamp without time zone NOT NULL, + "email" text NOT NULL, + "userIdentifier" text NOT NULL +); + +-- Indexes +CREATE UNIQUE INDEX "serverpod_auth_google_account_user_identifier" ON "serverpod_auth_idp_google_account" USING btree ("userIdentifier"); + +-- +-- Class MicrosoftAccount as table serverpod_auth_idp_microsoft_account +-- +CREATE TABLE "serverpod_auth_idp_microsoft_account" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid_v7(), + "authUserId" uuid NOT NULL, + "userIdentifier" text NOT NULL, + "email" text, + "created" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE UNIQUE INDEX "serverpod_auth_microsoft_account_user_identifier" ON "serverpod_auth_idp_microsoft_account" USING btree ("userIdentifier"); + +-- +-- Class PasskeyAccount as table serverpod_auth_idp_passkey_account +-- +CREATE TABLE "serverpod_auth_idp_passkey_account" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid_v7(), + "authUserId" uuid NOT NULL, + "createdAt" timestamp without time zone NOT NULL, + "keyId" bytea NOT NULL, + "keyIdBase64" text NOT NULL, + "clientDataJSON" bytea NOT NULL, + "attestationObject" bytea NOT NULL, + "originalChallenge" bytea NOT NULL +); + +-- Indexes +CREATE UNIQUE INDEX "serverpod_auth_idp_passkey_account_key_id_base64" ON "serverpod_auth_idp_passkey_account" USING btree ("keyIdBase64"); + +-- +-- Class PasskeyChallenge as table serverpod_auth_idp_passkey_challenge +-- +CREATE TABLE "serverpod_auth_idp_passkey_challenge" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid_v7(), + "createdAt" timestamp without time zone NOT NULL, + "challenge" bytea NOT NULL +); + +-- +-- Class RateLimitedRequestAttempt as table serverpod_auth_idp_rate_limited_request_attempt +-- +CREATE TABLE "serverpod_auth_idp_rate_limited_request_attempt" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid_v7(), + "domain" text NOT NULL, + "source" text NOT NULL, + "key" text NOT NULL, + "ipAddress" text, + "attemptedAt" timestamp without time zone NOT NULL, + "extraData" json +); + +-- Indexes +CREATE INDEX "serverpod_auth_idp_rate_limited_request_attempt_composite" ON "serverpod_auth_idp_rate_limited_request_attempt" USING btree ("domain", "source", "key", "attemptedAt"); + +-- +-- Class SecretChallenge as table serverpod_auth_idp_secret_challenge +-- +CREATE TABLE "serverpod_auth_idp_secret_challenge" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid_v7(), + "challengeCodeHash" text NOT NULL +); + +-- +-- Class RefreshToken as table serverpod_auth_core_jwt_refresh_token +-- +CREATE TABLE "serverpod_auth_core_jwt_refresh_token" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid_v7(), + "authUserId" uuid NOT NULL, + "scopeNames" json NOT NULL, + "extraClaims" text, + "method" text NOT NULL, + "fixedSecret" bytea NOT NULL, + "rotatingSecretHash" text NOT NULL, + "lastUpdatedAt" timestamp without time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, + "createdAt" timestamp without time zone NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- Indexes +CREATE INDEX "serverpod_auth_core_jwt_refresh_token_last_updated_at" ON "serverpod_auth_core_jwt_refresh_token" USING btree ("lastUpdatedAt"); + +-- +-- Class UserProfile as table serverpod_auth_core_profile +-- +CREATE TABLE "serverpod_auth_core_profile" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid_v7(), + "authUserId" uuid NOT NULL, + "userName" text, + "fullName" text, + "email" text, + "createdAt" timestamp without time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, + "imageId" uuid +); + +-- Indexes +CREATE UNIQUE INDEX "serverpod_auth_profile_user_profile_email_auth_user_id" ON "serverpod_auth_core_profile" USING btree ("authUserId"); + +-- +-- Class UserProfileImage as table serverpod_auth_core_profile_image +-- +CREATE TABLE "serverpod_auth_core_profile_image" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid_v7(), + "userProfileId" uuid NOT NULL, + "createdAt" timestamp without time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, + "storageId" text NOT NULL, + "path" text NOT NULL, + "url" text NOT NULL +); + +-- +-- Class ServerSideSession as table serverpod_auth_core_session +-- +CREATE TABLE "serverpod_auth_core_session" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid_v7(), + "authUserId" uuid NOT NULL, + "scopeNames" json NOT NULL, + "createdAt" timestamp without time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, + "lastUsedAt" timestamp without time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, + "expiresAt" timestamp without time zone, + "expireAfterUnusedFor" bigint, + "sessionKeyHash" bytea NOT NULL, + "sessionKeySalt" bytea NOT NULL, + "method" text NOT NULL +); + +-- +-- Class AuthUser as table serverpod_auth_core_user +-- +CREATE TABLE "serverpod_auth_core_user" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid_v7(), + "createdAt" timestamp without time zone NOT NULL, + "scopeNames" json NOT NULL, + "blocked" boolean NOT NULL +); + +-- +-- Foreign relations for "access_request" table +-- +ALTER TABLE ONLY "access_request" + ADD CONSTRAINT "access_request_fk_0" + FOREIGN KEY("itemId") + REFERENCES "drive_item"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; +ALTER TABLE ONLY "access_request" + ADD CONSTRAINT "access_request_fk_1" + FOREIGN KEY("requesterId") + REFERENCES "user_profile"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "activity_event" table +-- +ALTER TABLE ONLY "activity_event" + ADD CONSTRAINT "activity_event_fk_0" + FOREIGN KEY("itemId") + REFERENCES "drive_item"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "ai_usage" table +-- +ALTER TABLE ONLY "ai_usage" + ADD CONSTRAINT "ai_usage_fk_0" + FOREIGN KEY("userId") + REFERENCES "user_profile"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "ai_user_key" table +-- +ALTER TABLE ONLY "ai_user_key" + ADD CONSTRAINT "ai_user_key_fk_0" + FOREIGN KEY("userId") + REFERENCES "user_profile"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "ai_user_setting" table +-- +ALTER TABLE ONLY "ai_user_setting" + ADD CONSTRAINT "ai_user_setting_fk_0" + FOREIGN KEY("userId") + REFERENCES "user_profile"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "assistant_message" table +-- +ALTER TABLE ONLY "assistant_message" + ADD CONSTRAINT "assistant_message_fk_0" + FOREIGN KEY("threadId") + REFERENCES "assistant_thread"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "assistant_thread" table +-- +ALTER TABLE ONLY "assistant_thread" + ADD CONSTRAINT "assistant_thread_fk_0" + FOREIGN KEY("userId") + REFERENCES "user_profile"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; +ALTER TABLE ONLY "assistant_thread" + ADD CONSTRAINT "assistant_thread_fk_1" + FOREIGN KEY("itemId") + REFERENCES "drive_item"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "banding" table +-- +ALTER TABLE ONLY "banding" + ADD CONSTRAINT "banding_fk_0" + FOREIGN KEY("fileId") + REFERENCES "rev_file"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; +ALTER TABLE ONLY "banding" + ADD CONSTRAINT "banding_fk_1" + FOREIGN KEY("sheetId") + REFERENCES "sheet"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION + DEFERRABLE INITIALLY DEFERRED; + +-- +-- Foreign relations for "basic_filter" table +-- +ALTER TABLE ONLY "basic_filter" + ADD CONSTRAINT "basic_filter_fk_0" + FOREIGN KEY("fileId") + REFERENCES "rev_file"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; +ALTER TABLE ONLY "basic_filter" + ADD CONSTRAINT "basic_filter_fk_1" + FOREIGN KEY("sheetId") + REFERENCES "sheet"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION + DEFERRABLE INITIALLY DEFERRED; + +-- +-- Foreign relations for "board_comment" table +-- +ALTER TABLE ONLY "board_comment" + ADD CONSTRAINT "board_comment_fk_0" + FOREIGN KEY("postId") + REFERENCES "board_post"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; +ALTER TABLE ONLY "board_comment" + ADD CONSTRAINT "board_comment_fk_1" + FOREIGN KEY("taskId") + REFERENCES "board_task"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; +ALTER TABLE ONLY "board_comment" + ADD CONSTRAINT "board_comment_fk_2" + FOREIGN KEY("authorId") + REFERENCES "user_profile"("id") + ON DELETE SET NULL + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "board_flag" table +-- +ALTER TABLE ONLY "board_flag" + ADD CONSTRAINT "board_flag_fk_0" + FOREIGN KEY("postId") + REFERENCES "board_post"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; +ALTER TABLE ONLY "board_flag" + ADD CONSTRAINT "board_flag_fk_1" + FOREIGN KEY("commentId") + REFERENCES "board_comment"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; +ALTER TABLE ONLY "board_flag" + ADD CONSTRAINT "board_flag_fk_2" + FOREIGN KEY("userId") + REFERENCES "user_profile"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "board_post" table +-- +ALTER TABLE ONLY "board_post" + ADD CONSTRAINT "board_post_fk_0" + FOREIGN KEY("authorId") + REFERENCES "user_profile"("id") + ON DELETE SET NULL + ON UPDATE NO ACTION; +ALTER TABLE ONLY "board_post" + ADD CONSTRAINT "board_post_fk_1" + FOREIGN KEY("screenshotFileId") + REFERENCES "stored_file"("id") + ON DELETE SET NULL + ON UPDATE NO ACTION; +ALTER TABLE ONLY "board_post" + ADD CONSTRAINT "board_post_fk_2" + FOREIGN KEY("taskId") + REFERENCES "board_task"("id") + ON DELETE SET NULL + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "board_pull_request" table +-- +ALTER TABLE ONLY "board_pull_request" + ADD CONSTRAINT "board_pull_request_fk_0" + FOREIGN KEY("taskId") + REFERENCES "board_task"("id") + ON DELETE SET NULL + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "board_task" table +-- +ALTER TABLE ONLY "board_task" + ADD CONSTRAINT "board_task_fk_0" + FOREIGN KEY("claimedById") + REFERENCES "user_profile"("id") + ON DELETE SET NULL + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "board_user_profile" table +-- +ALTER TABLE ONLY "board_user_profile" + ADD CONSTRAINT "board_user_profile_fk_0" + FOREIGN KEY("userId") + REFERENCES "user_profile"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "board_vote" table +-- +ALTER TABLE ONLY "board_vote" + ADD CONSTRAINT "board_vote_fk_0" + FOREIGN KEY("postId") + REFERENCES "board_post"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; +ALTER TABLE ONLY "board_vote" + ADD CONSTRAINT "board_vote_fk_1" + FOREIGN KEY("userId") + REFERENCES "user_profile"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "cell" table +-- +ALTER TABLE ONLY "cell" + ADD CONSTRAINT "cell_fk_0" + FOREIGN KEY("sheetId") + REFERENCES "sheet"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION + DEFERRABLE INITIALLY DEFERRED; + +-- +-- Foreign relations for "chart_link" table +-- +ALTER TABLE ONLY "chart_link" + ADD CONSTRAINT "chart_link_fk_0" + FOREIGN KEY("fileId") + REFERENCES "rev_file"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; +ALTER TABLE ONLY "chart_link" + ADD CONSTRAINT "chart_link_fk_1" + FOREIGN KEY("elementId") + REFERENCES "slide_element"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION + DEFERRABLE INITIALLY DEFERRED; + +-- +-- Foreign relations for "code_state" table +-- +ALTER TABLE ONLY "code_state" + ADD CONSTRAINT "code_state_fk_0" + FOREIGN KEY("fileId") + REFERENCES "rev_file"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; +ALTER TABLE ONLY "code_state" + ADD CONSTRAINT "code_state_fk_1" + FOREIGN KEY("elementId") + REFERENCES "slide_element"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION + DEFERRABLE INITIALLY DEFERRED; + +-- +-- Foreign relations for "comment" table +-- +ALTER TABLE ONLY "comment" + ADD CONSTRAINT "comment_fk_0" + FOREIGN KEY("threadId") + REFERENCES "comment_thread"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; +ALTER TABLE ONLY "comment" + ADD CONSTRAINT "comment_fk_1" + FOREIGN KEY("publicationId") + REFERENCES "publication"("id") + ON DELETE SET NULL + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "comment_thread" table +-- +ALTER TABLE ONLY "comment_thread" + ADD CONSTRAINT "comment_thread_fk_0" + FOREIGN KEY("itemId") + REFERENCES "drive_item"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "conditional_format_rule" table +-- +ALTER TABLE ONLY "conditional_format_rule" + ADD CONSTRAINT "conditional_format_rule_fk_0" + FOREIGN KEY("fileId") + REFERENCES "rev_file"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; +ALTER TABLE ONLY "conditional_format_rule" + ADD CONSTRAINT "conditional_format_rule_fk_1" + FOREIGN KEY("sheetId") + REFERENCES "sheet"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION + DEFERRABLE INITIALLY DEFERRED; + +-- +-- Foreign relations for "data_validation_rule" table +-- +ALTER TABLE ONLY "data_validation_rule" + ADD CONSTRAINT "data_validation_rule_fk_0" + FOREIGN KEY("fileId") + REFERENCES "rev_file"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; +ALTER TABLE ONLY "data_validation_rule" + ADD CONSTRAINT "data_validation_rule_fk_1" + FOREIGN KEY("sheetId") + REFERENCES "sheet"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION + DEFERRABLE INITIALLY DEFERRED; + +-- +-- Foreign relations for "deck" table +-- +ALTER TABLE ONLY "deck" + ADD CONSTRAINT "deck_fk_0" + FOREIGN KEY("fileId") + REFERENCES "rev_file"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "deck_theme" table +-- +ALTER TABLE ONLY "deck_theme" + ADD CONSTRAINT "deck_theme_fk_0" + FOREIGN KEY("fileId") + REFERENCES "rev_file"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; +ALTER TABLE ONLY "deck_theme" + ADD CONSTRAINT "deck_theme_fk_1" + FOREIGN KEY("deckId") + REFERENCES "deck"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION + DEFERRABLE INITIALLY DEFERRED; + +-- +-- Foreign relations for "dimension_group" table +-- +ALTER TABLE ONLY "dimension_group" + ADD CONSTRAINT "dimension_group_fk_0" + FOREIGN KEY("fileId") + REFERENCES "rev_file"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; +ALTER TABLE ONLY "dimension_group" + ADD CONSTRAINT "dimension_group_fk_1" + FOREIGN KEY("sheetId") + REFERENCES "sheet"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION + DEFERRABLE INITIALLY DEFERRED; + +-- +-- Foreign relations for "document" table +-- +ALTER TABLE ONLY "document" + ADD CONSTRAINT "document_fk_0" + FOREIGN KEY("itemId") + REFERENCES "drive_item"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "document_import_report" table +-- +ALTER TABLE ONLY "document_import_report" + ADD CONSTRAINT "document_import_report_fk_0" + FOREIGN KEY("documentId") + REFERENCES "document"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "document_meta_change" table +-- +ALTER TABLE ONLY "document_meta_change" + ADD CONSTRAINT "document_meta_change_fk_0" + FOREIGN KEY("documentId") + REFERENCES "document"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "document_op" table +-- +ALTER TABLE ONLY "document_op" + ADD CONSTRAINT "document_op_fk_0" + FOREIGN KEY("documentId") + REFERENCES "document"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "document_snapshot" table +-- +ALTER TABLE ONLY "document_snapshot" + ADD CONSTRAINT "document_snapshot_fk_0" + FOREIGN KEY("documentId") + REFERENCES "document"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "drive_item" table +-- +ALTER TABLE ONLY "drive_item" + ADD CONSTRAINT "drive_item_fk_0" + FOREIGN KEY("ownerId") + REFERENCES "user_profile"("id") + ON DELETE RESTRICT + ON UPDATE NO ACTION; +ALTER TABLE ONLY "drive_item" + ADD CONSTRAINT "drive_item_fk_1" + FOREIGN KEY("parentId") + REFERENCES "drive_item"("id") + ON DELETE SET NULL + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "drive_item_permission" table +-- +ALTER TABLE ONLY "drive_item_permission" + ADD CONSTRAINT "drive_item_permission_fk_0" + FOREIGN KEY("itemId") + REFERENCES "drive_item"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; +ALTER TABLE ONLY "drive_item_permission" + ADD CONSTRAINT "drive_item_permission_fk_1" + FOREIGN KEY("userId") + REFERENCES "user_profile"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; +ALTER TABLE ONLY "drive_item_permission" + ADD CONSTRAINT "drive_item_permission_fk_2" + FOREIGN KEY("viaPublicationId") + REFERENCES "publication"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "drive_item_user_state" table +-- +ALTER TABLE ONLY "drive_item_user_state" + ADD CONSTRAINT "drive_item_user_state_fk_0" + FOREIGN KEY("itemId") + REFERENCES "drive_item"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; +ALTER TABLE ONLY "drive_item_user_state" + ADD CONSTRAINT "drive_item_user_state_fk_1" + FOREIGN KEY("userId") + REFERENCES "user_profile"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "embedded_object" table +-- +ALTER TABLE ONLY "embedded_object" + ADD CONSTRAINT "embedded_object_fk_0" + FOREIGN KEY("fileId") + REFERENCES "rev_file"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; +ALTER TABLE ONLY "embedded_object" + ADD CONSTRAINT "embedded_object_fk_1" + FOREIGN KEY("sheetId") + REFERENCES "sheet"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION + DEFERRABLE INITIALLY DEFERRED; + +-- +-- Foreign relations for "filter_view" table +-- +ALTER TABLE ONLY "filter_view" + ADD CONSTRAINT "filter_view_fk_0" + FOREIGN KEY("fileId") + REFERENCES "rev_file"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; +ALTER TABLE ONLY "filter_view" + ADD CONSTRAINT "filter_view_fk_1" + FOREIGN KEY("sheetId") + REFERENCES "sheet"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION + DEFERRABLE INITIALLY DEFERRED; + +-- +-- Foreign relations for "headless_change" table +-- +ALTER TABLE ONLY "headless_change" + ADD CONSTRAINT "headless_change_fk_0" + FOREIGN KEY("itemId") + REFERENCES "drive_item"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "item_version" table +-- +ALTER TABLE ONLY "item_version" + ADD CONSTRAINT "item_version_fk_0" + FOREIGN KEY("itemId") + REFERENCES "drive_item"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "item_version_state" table +-- +ALTER TABLE ONLY "item_version_state" + ADD CONSTRAINT "item_version_state_fk_0" + FOREIGN KEY("itemId") + REFERENCES "drive_item"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "item_view_day" table +-- +ALTER TABLE ONLY "item_view_day" + ADD CONSTRAINT "item_view_day_fk_0" + FOREIGN KEY("itemId") + REFERENCES "drive_item"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; +ALTER TABLE ONLY "item_view_day" + ADD CONSTRAINT "item_view_day_fk_1" + FOREIGN KEY("userId") + REFERENCES "user_profile"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "macro" table +-- +ALTER TABLE ONLY "macro" + ADD CONSTRAINT "macro_fk_0" + FOREIGN KEY("fileId") + REFERENCES "rev_file"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; +ALTER TABLE ONLY "macro" + ADD CONSTRAINT "macro_fk_1" + FOREIGN KEY("projectId") + REFERENCES "script_project"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION + DEFERRABLE INITIALLY DEFERRED; + +-- +-- Foreign relations for "merged_range" table +-- +ALTER TABLE ONLY "merged_range" + ADD CONSTRAINT "merged_range_fk_0" + FOREIGN KEY("fileId") + REFERENCES "rev_file"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; +ALTER TABLE ONLY "merged_range" + ADD CONSTRAINT "merged_range_fk_1" + FOREIGN KEY("sheetId") + REFERENCES "sheet"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION + DEFERRABLE INITIALLY DEFERRED; + +-- +-- Foreign relations for "named_range" table +-- +ALTER TABLE ONLY "named_range" + ADD CONSTRAINT "named_range_fk_0" + FOREIGN KEY("fileId") + REFERENCES "rev_file"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; +ALTER TABLE ONLY "named_range" + ADD CONSTRAINT "named_range_fk_1" + FOREIGN KEY("spreadsheetId") + REFERENCES "spreadsheet"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION + DEFERRABLE INITIALLY DEFERRED; + +-- +-- Foreign relations for "notification" table +-- +ALTER TABLE ONLY "notification" + ADD CONSTRAINT "notification_fk_0" + FOREIGN KEY("userId") + REFERENCES "user_profile"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "ownership_transfer" table +-- +ALTER TABLE ONLY "ownership_transfer" + ADD CONSTRAINT "ownership_transfer_fk_0" + FOREIGN KEY("itemId") + REFERENCES "drive_item"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; +ALTER TABLE ONLY "ownership_transfer" + ADD CONSTRAINT "ownership_transfer_fk_1" + FOREIGN KEY("toUserId") + REFERENCES "user_profile"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "personal_access_token" table +-- +ALTER TABLE ONLY "personal_access_token" + ADD CONSTRAINT "personal_access_token_fk_0" + FOREIGN KEY("userId") + REFERENCES "user_profile"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "pivot_table" table +-- +ALTER TABLE ONLY "pivot_table" + ADD CONSTRAINT "pivot_table_fk_0" + FOREIGN KEY("fileId") + REFERENCES "rev_file"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; +ALTER TABLE ONLY "pivot_table" + ADD CONSTRAINT "pivot_table_fk_1" + FOREIGN KEY("sheetId") + REFERENCES "sheet"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION + DEFERRABLE INITIALLY DEFERRED; + +-- +-- Foreign relations for "protected_range" table +-- +ALTER TABLE ONLY "protected_range" + ADD CONSTRAINT "protected_range_fk_0" + FOREIGN KEY("fileId") + REFERENCES "rev_file"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; +ALTER TABLE ONLY "protected_range" + ADD CONSTRAINT "protected_range_fk_1" + FOREIGN KEY("sheetId") + REFERENCES "sheet"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION + DEFERRABLE INITIALLY DEFERRED; + +-- +-- Foreign relations for "publication" table +-- +ALTER TABLE ONLY "publication" + ADD CONSTRAINT "publication_fk_0" + FOREIGN KEY("ownerId") + REFERENCES "user_profile"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; +ALTER TABLE ONLY "publication" + ADD CONSTRAINT "publication_fk_1" + FOREIGN KEY("templateItemId") + REFERENCES "drive_item"("id") + ON DELETE SET NULL + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "publication_block" table +-- +ALTER TABLE ONLY "publication_block" + ADD CONSTRAINT "publication_block_fk_0" + FOREIGN KEY("publicationId") + REFERENCES "publication"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; +ALTER TABLE ONLY "publication_block" + ADD CONSTRAINT "publication_block_fk_1" + FOREIGN KEY("userId") + REFERENCES "user_profile"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "publication_consent" table +-- +ALTER TABLE ONLY "publication_consent" + ADD CONSTRAINT "publication_consent_fk_0" + FOREIGN KEY("publicationId") + REFERENCES "publication"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; +ALTER TABLE ONLY "publication_consent" + ADD CONSTRAINT "publication_consent_fk_1" + FOREIGN KEY("userId") + REFERENCES "user_profile"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "publication_delivery" table +-- +ALTER TABLE ONLY "publication_delivery" + ADD CONSTRAINT "publication_delivery_fk_0" + FOREIGN KEY("publicationId") + REFERENCES "publication"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; +ALTER TABLE ONLY "publication_delivery" + ADD CONSTRAINT "publication_delivery_fk_1" + FOREIGN KEY("submissionId") + REFERENCES "submission"("id") + ON DELETE SET NULL + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "publication_moderator" table +-- +ALTER TABLE ONLY "publication_moderator" + ADD CONSTRAINT "publication_moderator_fk_0" + FOREIGN KEY("publicationId") + REFERENCES "publication"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; +ALTER TABLE ONLY "publication_moderator" + ADD CONSTRAINT "publication_moderator_fk_1" + FOREIGN KEY("userId") + REFERENCES "user_profile"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "rev_file" table +-- +ALTER TABLE ONLY "rev_file" + ADD CONSTRAINT "rev_file_fk_0" + FOREIGN KEY("itemId") + REFERENCES "drive_item"("id") + ON DELETE RESTRICT + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "rev_hook_state" table +-- +ALTER TABLE ONLY "rev_hook_state" + ADD CONSTRAINT "rev_hook_state_fk_0" + FOREIGN KEY("fileId") + REFERENCES "rev_file"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "rev_op" table +-- +ALTER TABLE ONLY "rev_op" + ADD CONSTRAINT "rev_op_fk_0" + FOREIGN KEY("fileId") + REFERENCES "rev_file"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "script_build" table +-- +ALTER TABLE ONLY "script_build" + ADD CONSTRAINT "script_build_fk_0" + FOREIGN KEY("fileId") + REFERENCES "rev_file"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; +ALTER TABLE ONLY "script_build" + ADD CONSTRAINT "script_build_fk_1" + FOREIGN KEY("projectId") + REFERENCES "script_project"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION + DEFERRABLE INITIALLY DEFERRED; + +-- +-- Foreign relations for "script_file" table +-- +ALTER TABLE ONLY "script_file" + ADD CONSTRAINT "script_file_fk_0" + FOREIGN KEY("fileId") + REFERENCES "rev_file"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; +ALTER TABLE ONLY "script_file" + ADD CONSTRAINT "script_file_fk_1" + FOREIGN KEY("projectId") + REFERENCES "script_project"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION + DEFERRABLE INITIALLY DEFERRED; + +-- +-- Foreign relations for "script_project" table +-- +ALTER TABLE ONLY "script_project" + ADD CONSTRAINT "script_project_fk_0" + FOREIGN KEY("fileId") + REFERENCES "rev_file"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; +ALTER TABLE ONLY "script_project" + ADD CONSTRAINT "script_project_fk_1" + FOREIGN KEY("spreadsheetId") + REFERENCES "spreadsheet"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION + DEFERRABLE INITIALLY DEFERRED; + +-- +-- Foreign relations for "script_property" table +-- +ALTER TABLE ONLY "script_property" + ADD CONSTRAINT "script_property_fk_0" + FOREIGN KEY("fileId") + REFERENCES "rev_file"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; +ALTER TABLE ONLY "script_property" + ADD CONSTRAINT "script_property_fk_1" + FOREIGN KEY("projectId") + REFERENCES "script_project"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION + DEFERRABLE INITIALLY DEFERRED; + +-- +-- Foreign relations for "script_trigger" table +-- +ALTER TABLE ONLY "script_trigger" + ADD CONSTRAINT "script_trigger_fk_0" + FOREIGN KEY("fileId") + REFERENCES "rev_file"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; +ALTER TABLE ONLY "script_trigger" + ADD CONSTRAINT "script_trigger_fk_1" + FOREIGN KEY("projectId") + REFERENCES "script_project"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION + DEFERRABLE INITIALLY DEFERRED; + +-- +-- Foreign relations for "script_user_property" table +-- +ALTER TABLE ONLY "script_user_property" + ADD CONSTRAINT "script_user_property_fk_0" + FOREIGN KEY("userId") + REFERENCES "user_profile"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "share_link" table +-- +ALTER TABLE ONLY "share_link" + ADD CONSTRAINT "share_link_fk_0" + FOREIGN KEY("itemId") + REFERENCES "drive_item"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "sheet" table +-- +ALTER TABLE ONLY "sheet" + ADD CONSTRAINT "sheet_fk_0" + FOREIGN KEY("fileId") + REFERENCES "rev_file"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; +ALTER TABLE ONLY "sheet" + ADD CONSTRAINT "sheet_fk_1" + FOREIGN KEY("spreadsheetId") + REFERENCES "spreadsheet"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION + DEFERRABLE INITIALLY DEFERRED; + +-- +-- Foreign relations for "sheet_column" table +-- +ALTER TABLE ONLY "sheet_column" + ADD CONSTRAINT "sheet_column_fk_0" + FOREIGN KEY("fileId") + REFERENCES "rev_file"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; +ALTER TABLE ONLY "sheet_column" + ADD CONSTRAINT "sheet_column_fk_1" + FOREIGN KEY("sheetId") + REFERENCES "sheet"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION + DEFERRABLE INITIALLY DEFERRED; + +-- +-- Foreign relations for "sheet_row" table +-- +ALTER TABLE ONLY "sheet_row" + ADD CONSTRAINT "sheet_row_fk_0" + FOREIGN KEY("fileId") + REFERENCES "rev_file"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; +ALTER TABLE ONLY "sheet_row" + ADD CONSTRAINT "sheet_row_fk_1" + FOREIGN KEY("sheetId") + REFERENCES "sheet"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION + DEFERRABLE INITIALLY DEFERRED; + +-- +-- Foreign relations for "sheets_snapshot_index" table +-- +ALTER TABLE ONLY "sheets_snapshot_index" + ADD CONSTRAINT "sheets_snapshot_index_fk_0" + FOREIGN KEY("itemVersionId") + REFERENCES "item_version"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "slide" table +-- +ALTER TABLE ONLY "slide" + ADD CONSTRAINT "slide_fk_0" + FOREIGN KEY("fileId") + REFERENCES "rev_file"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; +ALTER TABLE ONLY "slide" + ADD CONSTRAINT "slide_fk_1" + FOREIGN KEY("deckId") + REFERENCES "deck"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION + DEFERRABLE INITIALLY DEFERRED; + +-- +-- Foreign relations for "slide_build" table +-- +ALTER TABLE ONLY "slide_build" + ADD CONSTRAINT "slide_build_fk_0" + FOREIGN KEY("fileId") + REFERENCES "rev_file"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; +ALTER TABLE ONLY "slide_build" + ADD CONSTRAINT "slide_build_fk_1" + FOREIGN KEY("slideId") + REFERENCES "slide"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION + DEFERRABLE INITIALLY DEFERRED; +ALTER TABLE ONLY "slide_build" + ADD CONSTRAINT "slide_build_fk_2" + FOREIGN KEY("elementId") + REFERENCES "slide_element"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION + DEFERRABLE INITIALLY DEFERRED; + +-- +-- Foreign relations for "slide_element" table +-- +ALTER TABLE ONLY "slide_element" + ADD CONSTRAINT "slide_element_fk_0" + FOREIGN KEY("fileId") + REFERENCES "rev_file"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; +ALTER TABLE ONLY "slide_element" + ADD CONSTRAINT "slide_element_fk_1" + FOREIGN KEY("slideId") + REFERENCES "slide"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION + DEFERRABLE INITIALLY DEFERRED; +ALTER TABLE ONLY "slide_element" + ADD CONSTRAINT "slide_element_fk_2" + FOREIGN KEY("layoutId") + REFERENCES "slide_layout"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION + DEFERRABLE INITIALLY DEFERRED; +ALTER TABLE ONLY "slide_element" + ADD CONSTRAINT "slide_element_fk_3" + FOREIGN KEY("masterId") + REFERENCES "slide_master"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION + DEFERRABLE INITIALLY DEFERRED; +ALTER TABLE ONLY "slide_element" + ADD CONSTRAINT "slide_element_fk_4" + FOREIGN KEY("parentId") + REFERENCES "slide_element"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION + DEFERRABLE INITIALLY DEFERRED; + +-- +-- Foreign relations for "slide_group" table +-- +ALTER TABLE ONLY "slide_group" + ADD CONSTRAINT "slide_group_fk_0" + FOREIGN KEY("fileId") + REFERENCES "rev_file"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; +ALTER TABLE ONLY "slide_group" + ADD CONSTRAINT "slide_group_fk_1" + FOREIGN KEY("deckId") + REFERENCES "deck"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION + DEFERRABLE INITIALLY DEFERRED; + +-- +-- Foreign relations for "slide_layout" table +-- +ALTER TABLE ONLY "slide_layout" + ADD CONSTRAINT "slide_layout_fk_0" + FOREIGN KEY("fileId") + REFERENCES "rev_file"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; +ALTER TABLE ONLY "slide_layout" + ADD CONSTRAINT "slide_layout_fk_1" + FOREIGN KEY("masterId") + REFERENCES "slide_master"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION + DEFERRABLE INITIALLY DEFERRED; + +-- +-- Foreign relations for "slide_master" table +-- +ALTER TABLE ONLY "slide_master" + ADD CONSTRAINT "slide_master_fk_0" + FOREIGN KEY("fileId") + REFERENCES "rev_file"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; +ALTER TABLE ONLY "slide_master" + ADD CONSTRAINT "slide_master_fk_1" + FOREIGN KEY("themeId") + REFERENCES "deck_theme"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION + DEFERRABLE INITIALLY DEFERRED; + +-- +-- Foreign relations for "slide_table_axis" table +-- +ALTER TABLE ONLY "slide_table_axis" + ADD CONSTRAINT "slide_table_axis_fk_0" + FOREIGN KEY("fileId") + REFERENCES "rev_file"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; +ALTER TABLE ONLY "slide_table_axis" + ADD CONSTRAINT "slide_table_axis_fk_1" + FOREIGN KEY("elementId") + REFERENCES "slide_element"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION + DEFERRABLE INITIALLY DEFERRED; + +-- +-- Foreign relations for "slide_table_cell" table +-- +ALTER TABLE ONLY "slide_table_cell" + ADD CONSTRAINT "slide_table_cell_fk_0" + FOREIGN KEY("fileId") + REFERENCES "rev_file"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; +ALTER TABLE ONLY "slide_table_cell" + ADD CONSTRAINT "slide_table_cell_fk_1" + FOREIGN KEY("elementId") + REFERENCES "slide_element"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION + DEFERRABLE INITIALLY DEFERRED; +ALTER TABLE ONLY "slide_table_cell" + ADD CONSTRAINT "slide_table_cell_fk_2" + FOREIGN KEY("rowId") + REFERENCES "slide_table_axis"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION + DEFERRABLE INITIALLY DEFERRED; +ALTER TABLE ONLY "slide_table_cell" + ADD CONSTRAINT "slide_table_cell_fk_3" + FOREIGN KEY("columnId") + REFERENCES "slide_table_axis"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION + DEFERRABLE INITIALLY DEFERRED; + +-- +-- Foreign relations for "slides_snapshot_index" table +-- +ALTER TABLE ONLY "slides_snapshot_index" + ADD CONSTRAINT "slides_snapshot_index_fk_0" + FOREIGN KEY("itemVersionId") + REFERENCES "item_version"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "spreadsheet" table +-- +ALTER TABLE ONLY "spreadsheet" + ADD CONSTRAINT "spreadsheet_fk_0" + FOREIGN KEY("fileId") + REFERENCES "rev_file"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "storage_reservation" table +-- +ALTER TABLE ONLY "storage_reservation" + ADD CONSTRAINT "storage_reservation_fk_0" + FOREIGN KEY("userId") + REFERENCES "user_profile"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; +ALTER TABLE ONLY "storage_reservation" + ADD CONSTRAINT "storage_reservation_fk_1" + FOREIGN KEY("itemId") + REFERENCES "drive_item"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "stored_file" table +-- +ALTER TABLE ONLY "stored_file" + ADD CONSTRAINT "stored_file_fk_0" + FOREIGN KEY("ownerId") + REFERENCES "user_profile"("id") + ON DELETE SET NULL + ON UPDATE NO ACTION; +ALTER TABLE ONLY "stored_file" + ADD CONSTRAINT "stored_file_fk_1" + FOREIGN KEY("itemId") + REFERENCES "drive_item"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "submission" table +-- +ALTER TABLE ONLY "submission" + ADD CONSTRAINT "submission_fk_0" + FOREIGN KEY("publicationId") + REFERENCES "publication"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; +ALTER TABLE ONLY "submission" + ADD CONSTRAINT "submission_fk_1" + FOREIGN KEY("itemId") + REFERENCES "drive_item"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "submission_asset" table +-- +ALTER TABLE ONLY "submission_asset" + ADD CONSTRAINT "submission_asset_fk_0" + FOREIGN KEY("submissionId") + REFERENCES "submission"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "submission_revision" table +-- +ALTER TABLE ONLY "submission_revision" + ADD CONSTRAINT "submission_revision_fk_0" + FOREIGN KEY("submissionId") + REFERENCES "submission"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "user_activity_settings" table +-- +ALTER TABLE ONLY "user_activity_settings" + ADD CONSTRAINT "user_activity_settings_fk_0" + FOREIGN KEY("userId") + REFERENCES "user_profile"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "user_preferences" table +-- +ALTER TABLE ONLY "user_preferences" + ADD CONSTRAINT "user_preferences_fk_0" + FOREIGN KEY("userId") + REFERENCES "user_profile"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "user_profile" table +-- +ALTER TABLE ONLY "user_profile" + ADD CONSTRAINT "user_profile_fk_0" + FOREIGN KEY("authUserId") + REFERENCES "serverpod_auth_core_user"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "user_storage" table +-- +ALTER TABLE ONLY "user_storage" + ADD CONSTRAINT "user_storage_fk_0" + FOREIGN KEY("userId") + REFERENCES "user_profile"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "write_diff_cache" table +-- +ALTER TABLE ONLY "write_diff_cache" + ADD CONSTRAINT "write_diff_cache_fk_0" + FOREIGN KEY("documentId") + REFERENCES "document"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "write_theme" table +-- +ALTER TABLE ONLY "write_theme" + ADD CONSTRAINT "write_theme_fk_0" + FOREIGN KEY("ownerId") + REFERENCES "user_profile"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "write_theme_revision" table +-- +ALTER TABLE ONLY "write_theme_revision" + ADD CONSTRAINT "write_theme_revision_fk_0" + FOREIGN KEY("themeId") + REFERENCES "write_theme"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "serverpod_future_call_claim" table +-- +ALTER TABLE ONLY "serverpod_future_call_claim" + ADD CONSTRAINT "serverpod_future_call_claim_fk_0" + FOREIGN KEY("futureCallId") + REFERENCES "serverpod_future_call"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "serverpod_log" table +-- +ALTER TABLE ONLY "serverpod_log" + ADD CONSTRAINT "serverpod_log_fk_0" + FOREIGN KEY("sessionLogId") + REFERENCES "serverpod_session_log"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "serverpod_message_log" table +-- +ALTER TABLE ONLY "serverpod_message_log" + ADD CONSTRAINT "serverpod_message_log_fk_0" + FOREIGN KEY("sessionLogId") + REFERENCES "serverpod_session_log"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "serverpod_query_log" table +-- +ALTER TABLE ONLY "serverpod_query_log" + ADD CONSTRAINT "serverpod_query_log_fk_0" + FOREIGN KEY("sessionLogId") + REFERENCES "serverpod_session_log"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "serverpod_auth_idp_anonymous_account" table +-- +ALTER TABLE ONLY "serverpod_auth_idp_anonymous_account" + ADD CONSTRAINT "serverpod_auth_idp_anonymous_account_fk_0" + FOREIGN KEY("authUserId") + REFERENCES "serverpod_auth_core_user"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "serverpod_auth_idp_apple_account" table +-- +ALTER TABLE ONLY "serverpod_auth_idp_apple_account" + ADD CONSTRAINT "serverpod_auth_idp_apple_account_fk_0" + FOREIGN KEY("authUserId") + REFERENCES "serverpod_auth_core_user"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "serverpod_auth_idp_email_account" table +-- +ALTER TABLE ONLY "serverpod_auth_idp_email_account" + ADD CONSTRAINT "serverpod_auth_idp_email_account_fk_0" + FOREIGN KEY("authUserId") + REFERENCES "serverpod_auth_core_user"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "serverpod_auth_idp_email_account_otp_login_request" table +-- +ALTER TABLE ONLY "serverpod_auth_idp_email_account_otp_login_request" + ADD CONSTRAINT "serverpod_auth_idp_email_account_otp_login_request_fk_0" + FOREIGN KEY("challengeId") + REFERENCES "serverpod_auth_idp_secret_challenge"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; +ALTER TABLE ONLY "serverpod_auth_idp_email_account_otp_login_request" + ADD CONSTRAINT "serverpod_auth_idp_email_account_otp_login_request_fk_1" + FOREIGN KEY("completeChallengeId") + REFERENCES "serverpod_auth_idp_secret_challenge"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "serverpod_auth_idp_email_account_password_reset_request" table +-- +ALTER TABLE ONLY "serverpod_auth_idp_email_account_password_reset_request" + ADD CONSTRAINT "serverpod_auth_idp_email_account_password_reset_request_fk_0" + FOREIGN KEY("emailAccountId") + REFERENCES "serverpod_auth_idp_email_account"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; +ALTER TABLE ONLY "serverpod_auth_idp_email_account_password_reset_request" + ADD CONSTRAINT "serverpod_auth_idp_email_account_password_reset_request_fk_1" + FOREIGN KEY("challengeId") + REFERENCES "serverpod_auth_idp_secret_challenge"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; +ALTER TABLE ONLY "serverpod_auth_idp_email_account_password_reset_request" + ADD CONSTRAINT "serverpod_auth_idp_email_account_password_reset_request_fk_2" + FOREIGN KEY("setPasswordChallengeId") + REFERENCES "serverpod_auth_idp_secret_challenge"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "serverpod_auth_idp_email_account_request" table +-- +ALTER TABLE ONLY "serverpod_auth_idp_email_account_request" + ADD CONSTRAINT "serverpod_auth_idp_email_account_request_fk_0" + FOREIGN KEY("challengeId") + REFERENCES "serverpod_auth_idp_secret_challenge"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; +ALTER TABLE ONLY "serverpod_auth_idp_email_account_request" + ADD CONSTRAINT "serverpod_auth_idp_email_account_request_fk_1" + FOREIGN KEY("createAccountChallengeId") + REFERENCES "serverpod_auth_idp_secret_challenge"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "serverpod_auth_idp_facebook_account" table +-- +ALTER TABLE ONLY "serverpod_auth_idp_facebook_account" + ADD CONSTRAINT "serverpod_auth_idp_facebook_account_fk_0" + FOREIGN KEY("authUserId") + REFERENCES "serverpod_auth_core_user"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "serverpod_auth_idp_firebase_account" table +-- +ALTER TABLE ONLY "serverpod_auth_idp_firebase_account" + ADD CONSTRAINT "serverpod_auth_idp_firebase_account_fk_0" + FOREIGN KEY("authUserId") + REFERENCES "serverpod_auth_core_user"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "serverpod_auth_idp_github_account" table +-- +ALTER TABLE ONLY "serverpod_auth_idp_github_account" + ADD CONSTRAINT "serverpod_auth_idp_github_account_fk_0" + FOREIGN KEY("authUserId") + REFERENCES "serverpod_auth_core_user"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "serverpod_auth_idp_google_account" table +-- +ALTER TABLE ONLY "serverpod_auth_idp_google_account" + ADD CONSTRAINT "serverpod_auth_idp_google_account_fk_0" + FOREIGN KEY("authUserId") + REFERENCES "serverpod_auth_core_user"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "serverpod_auth_idp_microsoft_account" table +-- +ALTER TABLE ONLY "serverpod_auth_idp_microsoft_account" + ADD CONSTRAINT "serverpod_auth_idp_microsoft_account_fk_0" + FOREIGN KEY("authUserId") + REFERENCES "serverpod_auth_core_user"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "serverpod_auth_idp_passkey_account" table +-- +ALTER TABLE ONLY "serverpod_auth_idp_passkey_account" + ADD CONSTRAINT "serverpod_auth_idp_passkey_account_fk_0" + FOREIGN KEY("authUserId") + REFERENCES "serverpod_auth_core_user"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "serverpod_auth_core_jwt_refresh_token" table +-- +ALTER TABLE ONLY "serverpod_auth_core_jwt_refresh_token" + ADD CONSTRAINT "serverpod_auth_core_jwt_refresh_token_fk_0" + FOREIGN KEY("authUserId") + REFERENCES "serverpod_auth_core_user"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "serverpod_auth_core_profile" table +-- +ALTER TABLE ONLY "serverpod_auth_core_profile" + ADD CONSTRAINT "serverpod_auth_core_profile_fk_0" + FOREIGN KEY("authUserId") + REFERENCES "serverpod_auth_core_user"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; +ALTER TABLE ONLY "serverpod_auth_core_profile" + ADD CONSTRAINT "serverpod_auth_core_profile_fk_1" + FOREIGN KEY("imageId") + REFERENCES "serverpod_auth_core_profile_image"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION + DEFERRABLE INITIALLY DEFERRED; + +-- +-- Foreign relations for "serverpod_auth_core_profile_image" table +-- +ALTER TABLE ONLY "serverpod_auth_core_profile_image" + ADD CONSTRAINT "serverpod_auth_core_profile_image_fk_0" + FOREIGN KEY("userProfileId") + REFERENCES "serverpod_auth_core_profile"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "serverpod_auth_core_session" table +-- +ALTER TABLE ONLY "serverpod_auth_core_session" + ADD CONSTRAINT "serverpod_auth_core_session_fk_0" + FOREIGN KEY("authUserId") + REFERENCES "serverpod_auth_core_user"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; + + +-- +-- MIGRATION VERSION FOR wonky +-- +INSERT INTO "serverpod_migrations" ("module", "version", "timestamp") + VALUES ('wonky', '20261025085716775-serverpod-4-1-rc1', now()) + ON CONFLICT ("module") + DO UPDATE SET "version" = '20261025085716775-serverpod-4-1-rc1', "timestamp" = now(); + +-- +-- MIGRATION VERSION FOR serverpod +-- +INSERT INTO "serverpod_migrations" ("module", "version", "timestamp") + VALUES ('serverpod', '20260824182259319', now()) + ON CONFLICT ("module") + DO UPDATE SET "version" = '20260824182259319', "timestamp" = now(); + +-- +-- MIGRATION VERSION FOR serverpod_auth_idp +-- +INSERT INTO "serverpod_migrations" ("module", "version", "timestamp") + VALUES ('serverpod_auth_idp', '20261015164015382', now()) + ON CONFLICT ("module") + DO UPDATE SET "version" = '20261015164015382', "timestamp" = now(); + +-- +-- MIGRATION VERSION FOR serverpod_auth_core +-- +INSERT INTO "serverpod_migrations" ("module", "version", "timestamp") + VALUES ('serverpod_auth_core', '20260924105232991', now()) + ON CONFLICT ("module") + DO UPDATE SET "version" = '20260924105232991', "timestamp" = now(); + + +COMMIT; diff --git a/wonky_server/migrations/20261025085716775-serverpod-4-1-rc1/definition_project.json b/wonky_server/migrations/20261025085716775-serverpod-4-1-rc1/definition_project.json new file mode 100644 index 00000000..83c2b567 --- /dev/null +++ b/wonky_server/migrations/20261025085716775-serverpod-4-1-rc1/definition_project.json @@ -0,0 +1,16945 @@ +{ + "__className__": "serverpod.DatabaseDefinition", + "schemaVersion": 2, + "moduleName": "wonky", + "tables": [ + { + "__className__": "serverpod.TableDefinition", + "name": "access_request", + "dartName": "AccessRequest", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "itemId", + "fieldName": "itemId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "requesterId", + "fieldName": "requesterId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "requestedRole", + "fieldName": "requestedRole", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:AccessRole" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "message", + "fieldName": "message", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "handledAt", + "fieldName": "handledAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "handledById", + "fieldName": "handledById", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "outcome", + "fieldName": "outcome", + "columnType": 0, + "isNullable": true, + "dartType": "protocol:AccessRequestOutcome?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "access_request_fk_0", + "columns": [ + "itemId" + ], + "referenceTable": "drive_item", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "access_request_fk_1", + "columns": [ + "requesterId" + ], + "referenceTable": "user_profile", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "access_request_item_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "itemId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "handledAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "activity_event", + "dartName": "ActivityEvent", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "itemId", + "fieldName": "itemId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "actorId", + "fieldName": "actorId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "actorKind", + "fieldName": "actorKind", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:ActorKind" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "type", + "fieldName": "type", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:ActivityType" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "summary", + "fieldName": "summary", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "at", + "fieldName": "at", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "activity_event_fk_0", + "columns": [ + "itemId" + ], + "referenceTable": "drive_item", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "activity_item_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "itemId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "at" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "activity_type_at_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "type" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "at" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "ai_function_cache", + "dartName": "AiFunctionCache", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "key", + "fieldName": "key", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "spreadsheetUuid", + "fieldName": "spreadsheetUuid", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "model", + "fieldName": "model", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "value", + "fieldName": "value", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "error", + "fieldName": "error", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "hits", + "fieldName": "hits", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "ai_function_cache_key_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "key" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "ai_function_cache_sheet_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "spreadsheetUuid" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "ai_preview_record", + "dartName": "AiPreviewRecord", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "previewId", + "fieldName": "previewId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "fieldName": "userId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileUuid", + "fieldName": "fileUuid", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "previewJson", + "fieldName": "previewJson", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "changesJson", + "fieldName": "changesJson", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "expiresAt", + "fieldName": "expiresAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "ai_preview_id_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "previewId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "ai_preview_expires_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "expiresAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "ai_usage", + "dartName": "AiUsage", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "fieldName": "userId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "model", + "fieldName": "model", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "feature", + "fieldName": "feature", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "inputTokens", + "fieldName": "inputTokens", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "outputTokens", + "fieldName": "outputTokens", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "cacheReadTokens", + "fieldName": "cacheReadTokens", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "cacheWriteTokens", + "fieldName": "cacheWriteTokens", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "at", + "fieldName": "at", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "ai_usage_fk_0", + "columns": [ + "userId" + ], + "referenceTable": "user_profile", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "ai_usage_user_at_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "at" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "ai_usage_at_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "at" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "ai_user_key", + "dartName": "AiUserKey", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "fieldName": "userId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "provider", + "fieldName": "provider", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:AiProvider" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "apiKey", + "fieldName": "apiKey", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "updatedAt", + "fieldName": "updatedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "ai_user_key_fk_0", + "columns": [ + "userId" + ], + "referenceTable": "user_profile", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "ai_user_key_user_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "ai_user_setting", + "dartName": "AiUserSetting", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "fieldName": "userId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "quality", + "fieldName": "quality", + "columnType": 0, + "isNullable": false, + "columnDefault": "'best'", + "dartType": "protocol:AiQuality" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "updatedAt", + "fieldName": "updatedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "ai_user_setting_fk_0", + "columns": [ + "userId" + ], + "referenceTable": "user_profile", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "ai_user_setting_user_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "assistant_message", + "dartName": "AssistantMessage", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "threadId", + "fieldName": "threadId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "role", + "fieldName": "role", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "contentJson", + "fieldName": "contentJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "displayText", + "fieldName": "displayText", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "toolCallsJson", + "fieldName": "toolCallsJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "visible", + "fieldName": "visible", + "columnType": 1, + "isNullable": false, + "columnDefault": "true", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "replay", + "fieldName": "replay", + "columnType": 1, + "isNullable": false, + "columnDefault": "true", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "runId", + "fieldName": "runId", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "stopReason", + "fieldName": "stopReason", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "assistant_message_fk_0", + "columns": [ + "threadId" + ], + "referenceTable": "assistant_thread", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "assistant_message_thread_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "threadId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "assistant_thread", + "dartName": "AssistantThread", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "fieldName": "userId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "itemId", + "fieldName": "itemId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "title", + "fieldName": "title", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "contextRevision", + "fieldName": "contextRevision", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "contextMarkdown", + "fieldName": "contextMarkdown", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "updatedAt", + "fieldName": "updatedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "assistant_thread_fk_0", + "columns": [ + "userId" + ], + "referenceTable": "user_profile", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "assistant_thread_fk_1", + "columns": [ + "itemId" + ], + "referenceTable": "drive_item", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "assistant_thread_user_item_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "itemId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "updatedAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "banding", + "dartName": "Banding", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "random_v7", + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileId", + "fieldName": "fileId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revision", + "fieldName": "revision", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sheetId", + "fieldName": "sheetId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "rangeJson", + "fieldName": "rangeJson", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "styleJson", + "fieldName": "styleJson", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "banding_fk_0", + "columns": [ + "fileId" + ], + "referenceTable": "rev_file", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "banding_fk_1", + "columns": [ + "sheetId" + ], + "referenceTable": "sheet", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4, + "deferrable": "initiallyDeferred" + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "banding_file_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "fileId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "basic_filter", + "dartName": "BasicFilter", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "random_v7", + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileId", + "fieldName": "fileId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revision", + "fieldName": "revision", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sheetId", + "fieldName": "sheetId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "rangeJson", + "fieldName": "rangeJson", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "criteriaJson", + "fieldName": "criteriaJson", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sortJson", + "fieldName": "sortJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "basic_filter_fk_0", + "columns": [ + "fileId" + ], + "referenceTable": "rev_file", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "basic_filter_fk_1", + "columns": [ + "sheetId" + ], + "referenceTable": "sheet", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4, + "deferrable": "initiallyDeferred" + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "basic_filter_file_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "fileId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "board_ai_usage", + "dartName": "BoardAiUsage", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "day", + "fieldName": "day", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "provider", + "fieldName": "provider", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "model", + "fieldName": "model", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "feature", + "fieldName": "feature", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "requests", + "fieldName": "requests", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "inputTokens", + "fieldName": "inputTokens", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "outputTokens", + "fieldName": "outputTokens", + "columnType": 6, + "isNullable": false, + "dartType": "int" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "board_ai_usage_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "day" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "provider" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "model" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "feature" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "board_bundle_suggestion", + "dartName": "BoardBundleSuggestion", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "title", + "fieldName": "title", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "rationale", + "fieldName": "rationale", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "area", + "fieldName": "area", + "columnType": 0, + "isNullable": true, + "dartType": "protocol:BoardArea?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "postIds", + "fieldName": "postIds", + "columnType": 8, + "isNullable": false, + "dartType": "List" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "state", + "fieldName": "state", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [], + "indexes": [], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "board_comment", + "dartName": "BoardComment", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "uuid", + "fieldName": "uuid", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "postId", + "fieldName": "postId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "taskId", + "fieldName": "taskId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "authorId", + "fieldName": "authorId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "body", + "fieldName": "body", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "systemEvent", + "fieldName": "systemEvent", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "moderation", + "fieldName": "moderation", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:BoardModeration" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "moderationReason", + "fieldName": "moderationReason", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "needsCheck", + "fieldName": "needsCheck", + "columnType": 1, + "isNullable": false, + "columnDefault": "false", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "editedAt", + "fieldName": "editedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "deletedAt", + "fieldName": "deletedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "removedAt", + "fieldName": "removedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "board_comment_fk_0", + "columns": [ + "postId" + ], + "referenceTable": "board_post", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "board_comment_fk_1", + "columns": [ + "taskId" + ], + "referenceTable": "board_task", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "board_comment_fk_2", + "columns": [ + "authorId" + ], + "referenceTable": "user_profile", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 0 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "board_comment_uuid_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "uuid" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "board_comment_post_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "postId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "createdAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "board_comment_task_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "taskId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "createdAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "board_flag", + "dartName": "BoardFlag", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "postId", + "fieldName": "postId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "commentId", + "fieldName": "commentId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "fieldName": "userId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "reason", + "fieldName": "reason", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:BoardFlagReason" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "note", + "fieldName": "note", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "board_flag_fk_0", + "columns": [ + "postId" + ], + "referenceTable": "board_post", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "board_flag_fk_1", + "columns": [ + "commentId" + ], + "referenceTable": "board_comment", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "board_flag_fk_2", + "columns": [ + "userId" + ], + "referenceTable": "user_profile", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "board_flag_post_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "postId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "board_flag_comment_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "commentId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "board_github_delivery", + "dartName": "BoardGithubDelivery", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "deliveryId", + "fieldName": "deliveryId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "event", + "fieldName": "event", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "receivedAt", + "fieldName": "receivedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "payload", + "fieldName": "payload", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "processedAt", + "fieldName": "processedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "attempts", + "fieldName": "attempts", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "board_github_delivery_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "deliveryId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "board_moderation_log", + "dartName": "BoardModerationLog", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "adminId", + "fieldName": "adminId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "action", + "fieldName": "action", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "targetKind", + "fieldName": "targetKind", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:BoardTargetKind" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "targetId", + "fieldName": "targetId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "beforeJson", + "fieldName": "beforeJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "afterJson", + "fieldName": "afterJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "reason", + "fieldName": "reason", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "board_moderation_log_target_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "targetKind" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "targetId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "board_post", + "dartName": "BoardPost", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "authorId", + "fieldName": "authorId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "kind", + "fieldName": "kind", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:BoardPostKind" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "reportedKind", + "fieldName": "reportedKind", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:BoardPostKind" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "area", + "fieldName": "area", + "columnType": 0, + "isNullable": true, + "dartType": "protocol:BoardArea?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "size", + "fieldName": "size", + "columnType": 0, + "isNullable": true, + "dartType": "protocol:BoardSize?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "aiLabels", + "fieldName": "aiLabels", + "columnType": 1, + "isNullable": false, + "columnDefault": "true", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "title", + "fieldName": "title", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "body", + "fieldName": "body", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "contextJson", + "fieldName": "contextJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "appHint", + "fieldName": "appHint", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "screenshotFileId", + "fieldName": "screenshotFileId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "status", + "fieldName": "status", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:BoardPostStatus" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "moderation", + "fieldName": "moderation", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:BoardModeration" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "moderationReason", + "fieldName": "moderationReason", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "needsCheck", + "fieldName": "needsCheck", + "columnType": 1, + "isNullable": false, + "columnDefault": "false", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "editedByModerator", + "fieldName": "editedByModerator", + "columnType": 1, + "isNullable": false, + "columnDefault": "false", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "triageState", + "fieldName": "triageState", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:BoardTriageState" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "triageJson", + "fieldName": "triageJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "triageAttempts", + "fieldName": "triageAttempts", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "suggestedTitle", + "fieldName": "suggestedTitle", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "summary", + "fieldName": "summary", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "duplicateOfId", + "fieldName": "duplicateOfId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "possibleDuplicateOfId", + "fieldName": "possibleDuplicateOfId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "duplicateHintDismissed", + "fieldName": "duplicateHintDismissed", + "columnType": 1, + "isNullable": false, + "columnDefault": "false", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "declineReason", + "fieldName": "declineReason", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "taskId", + "fieldName": "taskId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "score", + "fieldName": "score", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "voteCount", + "fieldName": "voteCount", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "commentCount", + "fieldName": "commentCount", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "flagCount", + "fieldName": "flagCount", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "editedAt", + "fieldName": "editedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "deletedAt", + "fieldName": "deletedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "removedAt", + "fieldName": "removedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "board_post_fk_0", + "columns": [ + "authorId" + ], + "referenceTable": "user_profile", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 0 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "board_post_fk_1", + "columns": [ + "screenshotFileId" + ], + "referenceTable": "stored_file", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 0 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "board_post_fk_2", + "columns": [ + "taskId" + ], + "referenceTable": "board_task", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 0 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "board_post_list_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "moderation" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "kind" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "status" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "score" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "board_post_author_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "authorId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "board_post_task_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "taskId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "board_pull_request", + "dartName": "BoardPullRequest", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "prNumber", + "fieldName": "prNumber", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "taskId", + "fieldName": "taskId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "authorLogin", + "fieldName": "authorLogin", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "headSha", + "fieldName": "headSha", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "state", + "fieldName": "state", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "gateState", + "fieldName": "gateState", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "gateReason", + "fieldName": "gateReason", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "gateFirstFailedAt", + "fieldName": "gateFirstFailedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "gateCommentId", + "fieldName": "gateCommentId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "reviewCommentId", + "fieldName": "reviewCommentId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lastReviewSha", + "fieldName": "lastReviewSha", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lastReviewJson", + "fieldName": "lastReviewJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lastReviewAt", + "fieldName": "lastReviewAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "reviewDay", + "fieldName": "reviewDay", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "reviewCount", + "fieldName": "reviewCount", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "mergeCommitSha", + "fieldName": "mergeCommitSha", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "reviewPendingSha", + "fieldName": "reviewPendingSha", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "reviewDueAt", + "fieldName": "reviewDueAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "updatedAt", + "fieldName": "updatedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "board_pull_request_fk_0", + "columns": [ + "taskId" + ], + "referenceTable": "board_task", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 0 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "board_pull_request_number_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "prNumber" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "board_task", + "dartName": "BoardTask", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "title", + "fieldName": "title", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "area", + "fieldName": "area", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:BoardArea" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "size", + "fieldName": "size", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:BoardSize" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "status", + "fieldName": "status", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:BoardTaskStatus" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "promptBody", + "fieldName": "promptBody", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "promptGeneratedAt", + "fieldName": "promptGeneratedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "promptRequestedAt", + "fieldName": "promptRequestedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "promptNote", + "fieldName": "promptNote", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "promptExcludedCommentIds", + "fieldName": "promptExcludedCommentIds", + "columnType": 8, + "isNullable": true, + "dartType": "List?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "promptEditedById", + "fieldName": "promptEditedById", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "promptEditedAt", + "fieldName": "promptEditedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdById", + "fieldName": "createdById", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "publishedAt", + "fieldName": "publishedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "claimedById", + "fieldName": "claimedById", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "claimedAt", + "fieldName": "claimedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "claimExpiresAt", + "fieldName": "claimExpiresAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "claimWarnedAt", + "fieldName": "claimWarnedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "claimCode", + "fieldName": "claimCode", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "pullRequestUrl", + "fieldName": "pullRequestUrl", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "prNumber", + "fieldName": "prNumber", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "prAuthorLogin", + "fieldName": "prAuthorLogin", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "mergeCommitSha", + "fieldName": "mergeCommitSha", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "doneAt", + "fieldName": "doneAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "shippedDeploySha", + "fieldName": "shippedDeploySha", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "shippedAt", + "fieldName": "shippedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "board_task_fk_0", + "columns": [ + "claimedById" + ], + "referenceTable": "user_profile", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 0 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "board_task_status_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "status" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "board_task_claim_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "claimedById" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "board_user_profile", + "dartName": "BoardUserProfile", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "fieldName": "userId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "githubLogin", + "fieldName": "githubLogin", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "blockedAt", + "fieldName": "blockedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "blockedUntil", + "fieldName": "blockedUntil", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "blockedReason", + "fieldName": "blockedReason", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "blockedById", + "fieldName": "blockedById", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "board_user_profile_fk_0", + "columns": [ + "userId" + ], + "referenceTable": "user_profile", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "board_user_profile_user_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "board_vote", + "dartName": "BoardVote", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "postId", + "fieldName": "postId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "fieldName": "userId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "value", + "fieldName": "value", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "board_vote_fk_0", + "columns": [ + "postId" + ], + "referenceTable": "board_post", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "board_vote_fk_1", + "columns": [ + "userId" + ], + "referenceTable": "user_profile", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "board_vote_unique_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "postId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "board_vote_recent_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "postId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "createdAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "cell", + "dartName": "Cell", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "random_v7", + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileId", + "fieldName": "fileId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revision", + "fieldName": "revision", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sheetId", + "fieldName": "sheetId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "rowId", + "fieldName": "rowId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "columnId", + "fieldName": "columnId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "content", + "fieldName": "content", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "note", + "fieldName": "note", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "numberFormat", + "fieldName": "numberFormat", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fontFamily", + "fieldName": "fontFamily", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fontSize", + "fieldName": "fontSize", + "columnType": 3, + "isNullable": true, + "dartType": "double?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "bold", + "fieldName": "bold", + "columnType": 1, + "isNullable": true, + "dartType": "bool?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "italic", + "fieldName": "italic", + "columnType": 1, + "isNullable": true, + "dartType": "bool?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "underline", + "fieldName": "underline", + "columnType": 1, + "isNullable": true, + "dartType": "bool?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "strikethrough", + "fieldName": "strikethrough", + "columnType": 1, + "isNullable": true, + "dartType": "bool?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "textColor", + "fieldName": "textColor", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fillColor", + "fieldName": "fillColor", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "hAlign", + "fieldName": "hAlign", + "columnType": 0, + "isNullable": true, + "dartType": "protocol:HorizontalAlign?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "vAlign", + "fieldName": "vAlign", + "columnType": 0, + "isNullable": true, + "dartType": "protocol:VerticalAlign?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "wrap", + "fieldName": "wrap", + "columnType": 0, + "isNullable": true, + "dartType": "protocol:WrapStrategy?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "rotation", + "fieldName": "rotation", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "borderTop", + "fieldName": "borderTop", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "borderRight", + "fieldName": "borderRight", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "borderBottom", + "fieldName": "borderBottom", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "borderLeft", + "fieldName": "borderLeft", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "linkUrl", + "fieldName": "linkUrl", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "cell_fk_0", + "columns": [ + "sheetId" + ], + "referenceTable": "sheet", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4, + "deferrable": "initiallyDeferred" + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "cell_row_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "sheetId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "rowId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "cell_col_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "sheetId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "columnId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "cell_history", + "dartName": "CellHistory", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileUuid", + "fieldName": "fileUuid", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sheetId", + "fieldName": "sheetId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "rowId", + "fieldName": "rowId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "colId", + "fieldName": "colId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revision", + "fieldName": "revision", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userUuid", + "fieldName": "userUuid", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "actorKind", + "fieldName": "actorKind", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:ActorKind" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "label", + "fieldName": "label", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "at", + "fieldName": "at", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "before", + "fieldName": "before", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "after", + "fieldName": "after", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "cell_history_cell_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "fileUuid" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "sheetId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "rowId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "colId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "at" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "cell_history_change_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "fileUuid" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "revision" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "rowId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "colId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "chart_link", + "dartName": "ChartLink", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "random_v7", + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileId", + "fieldName": "fileId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revision", + "fieldName": "revision", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "elementId", + "fieldName": "elementId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "spreadsheetItemUuid", + "fieldName": "spreadsheetItemUuid", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "chartObjectId", + "fieldName": "chartObjectId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sheetId", + "fieldName": "sheetId", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sourceSpecJson", + "fieldName": "sourceSpecJson", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "dataSnapshotJson", + "fieldName": "dataSnapshotJson", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sourceVersion", + "fieldName": "sourceVersion", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "updatedAt", + "fieldName": "updatedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "updatedBy", + "fieldName": "updatedBy", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "autoUpdate", + "fieldName": "autoUpdate", + "columnType": 1, + "isNullable": false, + "columnDefault": "false", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "chart_link_fk_0", + "columns": [ + "fileId" + ], + "referenceTable": "rev_file", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "chart_link_fk_1", + "columns": [ + "elementId" + ], + "referenceTable": "slide_element", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4, + "deferrable": "initiallyDeferred" + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "chart_link_file_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "fileId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "chart_link_element_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "elementId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "code_state", + "dartName": "CodeState", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "random_v7", + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileId", + "fieldName": "fileId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revision", + "fieldName": "revision", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "elementId", + "fieldName": "elementId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sortKey", + "fieldName": "sortKey", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "label", + "fieldName": "label", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "code", + "fieldName": "code", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "language", + "fieldName": "language", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "title", + "fieldName": "title", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "highlight", + "fieldName": "highlight", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "annotationsJson", + "fieldName": "annotationsJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "transitionJson", + "fieldName": "transitionJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "code_state_fk_0", + "columns": [ + "fileId" + ], + "referenceTable": "rev_file", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "code_state_fk_1", + "columns": [ + "elementId" + ], + "referenceTable": "slide_element", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4, + "deferrable": "initiallyDeferred" + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "code_state_file_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "fileId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "code_state_element_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "elementId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "sortKey" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "comment", + "dartName": "Comment", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "threadId", + "fieldName": "threadId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "uuid", + "fieldName": "uuid", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "authorId", + "fieldName": "authorId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "authorKind", + "fieldName": "authorKind", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:ActorKind" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "body", + "fieldName": "body", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "mentions", + "fieldName": "mentions", + "columnType": 8, + "isNullable": false, + "dartType": "List" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "reactions", + "fieldName": "reactions", + "columnType": 0, + "isNullable": false, + "columnDefault": "'{}'", + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "editedAt", + "fieldName": "editedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "deletedAt", + "fieldName": "deletedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "systemEvent", + "fieldName": "systemEvent", + "columnType": 0, + "isNullable": true, + "dartType": "protocol:CommentSystemEvent?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "publicationId", + "fieldName": "publicationId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "comment_fk_0", + "columns": [ + "threadId" + ], + "referenceTable": "comment_thread", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "comment_fk_1", + "columns": [ + "publicationId" + ], + "referenceTable": "publication", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 0 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "comment_thread_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "threadId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "createdAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "comment_uuid_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "uuid" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "comment_created_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "createdAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "comment_thread", + "dartName": "CommentThread", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "uuid", + "fieldName": "uuid", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "itemId", + "fieldName": "itemId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "anchorKind", + "fieldName": "anchorKind", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:CommentAnchorKind" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "anchorJson", + "fieldName": "anchorJson", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "quotedText", + "fieldName": "quotedText", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "status", + "fieldName": "status", + "columnType": 0, + "isNullable": false, + "columnDefault": "'open'", + "dartType": "protocol:CommentStatus" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdById", + "fieldName": "createdById", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "updatedAt", + "fieldName": "updatedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "resolvedById", + "fieldName": "resolvedById", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "resolvedAt", + "fieldName": "resolvedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "assigneeId", + "fieldName": "assigneeId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "statusBeforeOrphan", + "fieldName": "statusBeforeOrphan", + "columnType": 0, + "isNullable": true, + "dartType": "protocol:CommentStatus?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "orphanReasonJson", + "fieldName": "orphanReasonJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "suggestionId", + "fieldName": "suggestionId", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "comment_thread_fk_0", + "columns": [ + "itemId" + ], + "referenceTable": "drive_item", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "comment_thread_item_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "itemId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "status" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "comment_thread_uuid_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "uuid" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "conditional_format_rule", + "dartName": "ConditionalFormatRule", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "random_v7", + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileId", + "fieldName": "fileId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revision", + "fieldName": "revision", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sheetId", + "fieldName": "sheetId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sortKey", + "fieldName": "sortKey", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "rangesJson", + "fieldName": "rangesJson", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "ruleJson", + "fieldName": "ruleJson", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "enabled", + "fieldName": "enabled", + "columnType": 1, + "isNullable": false, + "columnDefault": "true", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "conditional_format_rule_fk_0", + "columns": [ + "fileId" + ], + "referenceTable": "rev_file", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "conditional_format_rule_fk_1", + "columns": [ + "sheetId" + ], + "referenceTable": "sheet", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4, + "deferrable": "initiallyDeferred" + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "conditional_format_rule_file_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "fileId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "data_validation_rule", + "dartName": "DataValidationRule", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "random_v7", + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileId", + "fieldName": "fileId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revision", + "fieldName": "revision", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sheetId", + "fieldName": "sheetId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "rangesJson", + "fieldName": "rangesJson", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "criteriaJson", + "fieldName": "criteriaJson", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "strict", + "fieldName": "strict", + "columnType": 1, + "isNullable": false, + "columnDefault": "false", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "showDropdown", + "fieldName": "showDropdown", + "columnType": 1, + "isNullable": false, + "columnDefault": "true", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "displayStyle", + "fieldName": "displayStyle", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "helpText", + "fieldName": "helpText", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "data_validation_rule_fk_0", + "columns": [ + "fileId" + ], + "referenceTable": "rev_file", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "data_validation_rule_fk_1", + "columns": [ + "sheetId" + ], + "referenceTable": "sheet", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4, + "deferrable": "initiallyDeferred" + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "data_validation_rule_file_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "fileId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "deck", + "dartName": "Deck", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "random_v7", + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileId", + "fieldName": "fileId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revision", + "fieldName": "revision", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "title", + "fieldName": "title", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "width", + "fieldName": "width", + "columnType": 3, + "isNullable": false, + "columnDefault": "1920.0", + "dartType": "double" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "height", + "fieldName": "height", + "columnType": 3, + "isNullable": false, + "columnDefault": "1080.0", + "dartType": "double" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sizePreset", + "fieldName": "sizePreset", + "columnType": 0, + "isNullable": false, + "columnDefault": "'widescreen16x9'", + "dartType": "protocol:SlideSizePreset" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "themeId", + "fieldName": "themeId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "appearance", + "fieldName": "appearance", + "columnType": 0, + "isNullable": false, + "columnDefault": "'light'", + "dartType": "protocol:DeckAppearance" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "language", + "fieldName": "language", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "showSlideNumbers", + "fieldName": "showSlideNumbers", + "columnType": 1, + "isNullable": false, + "columnDefault": "false", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "hideNumberOnTitle", + "fieldName": "hideNumberOnTitle", + "columnType": 1, + "isNullable": false, + "columnDefault": "true", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "firstSlideNumber", + "fieldName": "firstSlideNumber", + "columnType": 6, + "isNullable": false, + "columnDefault": "1", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "footerText", + "fieldName": "footerText", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "defaultsJson", + "fieldName": "defaultsJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "guidesJson", + "fieldName": "guidesJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "presentJson", + "fieldName": "presentJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdBy", + "fieldName": "createdBy", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "deck_fk_0", + "columns": [ + "fileId" + ], + "referenceTable": "rev_file", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "deck_file_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "fileId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "deck_theme", + "dartName": "DeckTheme", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "random_v7", + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileId", + "fieldName": "fileId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revision", + "fieldName": "revision", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "deckId", + "fieldName": "deckId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "name", + "fieldName": "name", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sourceKey", + "fieldName": "sourceKey", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "colorsJson", + "fieldName": "colorsJson", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fontsJson", + "fieldName": "fontsJson", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "textStylesJson", + "fieldName": "textStylesJson", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "backgroundJson", + "fieldName": "backgroundJson", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "shapeStyleJson", + "fieldName": "shapeStyleJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "tableStyleJson", + "fieldName": "tableStyleJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "codeStyleJson", + "fieldName": "codeStyleJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "chartStyleJson", + "fieldName": "chartStyleJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sortKey", + "fieldName": "sortKey", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "deck_theme_fk_0", + "columns": [ + "fileId" + ], + "referenceTable": "rev_file", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "deck_theme_fk_1", + "columns": [ + "deckId" + ], + "referenceTable": "deck", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4, + "deferrable": "initiallyDeferred" + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "deck_theme_file_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "fileId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "deck_theme_deck_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "deckId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "dimension_group", + "dartName": "DimensionGroup", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "random_v7", + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileId", + "fieldName": "fileId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revision", + "fieldName": "revision", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sheetId", + "fieldName": "sheetId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "dimension", + "fieldName": "dimension", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:Dimension" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "startId", + "fieldName": "startId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "endId", + "fieldName": "endId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "depth", + "fieldName": "depth", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "collapsed", + "fieldName": "collapsed", + "columnType": 1, + "isNullable": false, + "columnDefault": "false", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "dimension_group_fk_0", + "columns": [ + "fileId" + ], + "referenceTable": "rev_file", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "dimension_group_fk_1", + "columns": [ + "sheetId" + ], + "referenceTable": "sheet", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4, + "deferrable": "initiallyDeferred" + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "dimension_group_file_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "fileId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "document", + "dartName": "Document", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "uuid", + "fieldName": "uuid", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "itemId", + "fieldName": "itemId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "headRevision", + "fieldName": "headRevision", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "latestSnapshotRevision", + "fieldName": "latestSnapshotRevision", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "themeId", + "fieldName": "themeId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "pageSetupJson", + "fieldName": "pageSetupJson", + "columnType": 0, + "isNullable": false, + "columnDefault": "'{}'", + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "language", + "fieldName": "language", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "frontMatter", + "fieldName": "frontMatter", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "charCount", + "fieldName": "charCount", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "wordCount", + "fieldName": "wordCount", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "updatedAt", + "fieldName": "updatedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "document_fk_0", + "columns": [ + "itemId" + ], + "referenceTable": "drive_item", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "document_uuid_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "uuid" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "document_item_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "itemId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "document_import_report", + "dartName": "DocumentImportReport", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "documentId", + "fieldName": "documentId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "fieldName": "userId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "warningsJson", + "fieldName": "warningsJson", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "seenAt", + "fieldName": "seenAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "document_import_report_fk_0", + "columns": [ + "documentId" + ], + "referenceTable": "document", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "document_import_report_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "documentId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "document_meta_change", + "dartName": "DocumentMetaChange", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "documentId", + "fieldName": "documentId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "field", + "fieldName": "field", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "clientOpId", + "fieldName": "clientOpId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "beforeJson", + "fieldName": "beforeJson", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "afterJson", + "fieldName": "afterJson", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "authorId", + "fieldName": "authorId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "actorKind", + "fieldName": "actorKind", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:ActorKind" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "document_meta_change_fk_0", + "columns": [ + "documentId" + ], + "referenceTable": "document", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "document_meta_change_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "documentId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "createdAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "document_meta_change_client_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "documentId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "clientOpId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "document_op", + "dartName": "DocumentOp", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "documentId", + "fieldName": "documentId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revision", + "fieldName": "revision", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "clientOpId", + "fieldName": "clientOpId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "authorId", + "fieldName": "authorId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "actorKind", + "fieldName": "actorKind", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:ActorKind" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sessionId", + "fieldName": "sessionId", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "kind", + "fieldName": "kind", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:DocumentOpKind" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "baseRevision", + "fieldName": "baseRevision", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "deltaJson", + "fieldName": "deltaJson", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "inverseJson", + "fieldName": "inverseJson", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "document_op_fk_0", + "columns": [ + "documentId" + ], + "referenceTable": "document", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "document_op_rev_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "documentId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "revision" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "document_op_client_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "documentId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "clientOpId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "document_op_created_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "createdAt" + } + ], + "type": "brin", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "document_snapshot", + "dartName": "DocumentSnapshot", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "documentId", + "fieldName": "documentId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revision", + "fieldName": "revision", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "deltaJson", + "fieldName": "deltaJson", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "compressed", + "fieldName": "compressed", + "columnType": 1, + "isNullable": false, + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "document_snapshot_fk_0", + "columns": [ + "documentId" + ], + "referenceTable": "document", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "document_snapshot_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "documentId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "revision" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "drive_item", + "dartName": "DriveItem", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "uuid", + "fieldName": "uuid", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "kind", + "fieldName": "kind", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:DriveItemKind" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "name", + "fieldName": "name", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "ownerId", + "fieldName": "ownerId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "parentId", + "fieldName": "parentId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "description", + "fieldName": "description", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "contentRef", + "fieldName": "contentRef", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "thumbnailFileId", + "fieldName": "thumbnailFileId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sizeBytes", + "fieldName": "sizeBytes", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "attachmentBytes", + "fieldName": "attachmentBytes", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "kindMask", + "fieldName": "kindMask", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "mimeType", + "fieldName": "mimeType", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileCategory", + "fieldName": "fileCategory", + "columnType": 0, + "isNullable": true, + "dartType": "protocol:FileCategory?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "uploadState", + "fieldName": "uploadState", + "columnType": 0, + "isNullable": true, + "dartType": "protocol:FileUploadState?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileMetaJson", + "fieldName": "fileMetaJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "previewFileId", + "fieldName": "previewFileId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdById", + "fieldName": "createdById", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "modifiedAt", + "fieldName": "modifiedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "modifiedById", + "fieldName": "modifiedById", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "trashedAt", + "fieldName": "trashedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "trashedById", + "fieldName": "trashedById", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "templateOf", + "fieldName": "templateOf", + "columnType": 0, + "isNullable": true, + "dartType": "protocol:TemplateCategory?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "templateKey", + "fieldName": "templateKey", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "editorsCanShare", + "fieldName": "editorsCanShare", + "columnType": 1, + "isNullable": false, + "columnDefault": "true", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "nonEditorsCanExport", + "fieldName": "nonEditorsCanExport", + "columnType": 1, + "isNullable": false, + "columnDefault": "true", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "viewersSeeComments", + "fieldName": "viewersSeeComments", + "columnType": 1, + "isNullable": false, + "columnDefault": "false", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "searchText", + "fieldName": "searchText", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "drive_item_fk_0", + "columns": [ + "ownerId" + ], + "referenceTable": "user_profile", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 2 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "drive_item_fk_1", + "columns": [ + "parentId" + ], + "referenceTable": "drive_item", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 0 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "drive_item_uuid_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "uuid" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "drive_item_parent_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "parentId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "drive_item_owner_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "ownerId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "trashedAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "drive_item_content_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "contentRef" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "drive_item_template_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "templateOf" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "drive_item_upload_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "uploadState" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "createdAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "drive_item_trashed_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "trashedAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "drive_item_permission", + "dartName": "DriveItemPermission", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "itemId", + "fieldName": "itemId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "fieldName": "userId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "invitedEmail", + "fieldName": "invitedEmail", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "role", + "fieldName": "role", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:AccessRole" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "inheritedFromId", + "fieldName": "inheritedFromId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "grantedById", + "fieldName": "grantedById", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "grantedAt", + "fieldName": "grantedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "expiresAt", + "fieldName": "expiresAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "viaPublicationId", + "fieldName": "viaPublicationId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "drive_item_permission_fk_0", + "columns": [ + "itemId" + ], + "referenceTable": "drive_item", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "drive_item_permission_fk_1", + "columns": [ + "userId" + ], + "referenceTable": "user_profile", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "drive_item_permission_fk_2", + "columns": [ + "viaPublicationId" + ], + "referenceTable": "publication", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "perm_item_user_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "itemId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "inheritedFromId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "invitedEmail" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false, + "nullsDistinct": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "perm_user_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "perm_email_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "invitedEmail" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "perm_expires_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "expiresAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "perm_granted_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "grantedAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "drive_item_user_state", + "dartName": "DriveItemUserState", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "itemId", + "fieldName": "itemId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "fieldName": "userId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "starred", + "fieldName": "starred", + "columnType": 1, + "isNullable": false, + "columnDefault": "false", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lastOpenedAt", + "fieldName": "lastOpenedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lastEditedAt", + "fieldName": "lastEditedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "pinnedOffline", + "fieldName": "pinnedOffline", + "columnType": 1, + "isNullable": false, + "columnDefault": "false", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "shortcutParentId", + "fieldName": "shortcutParentId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "viaLink", + "fieldName": "viaLink", + "columnType": 1, + "isNullable": false, + "columnDefault": "false", + "dartType": "bool" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "drive_item_user_state_fk_0", + "columns": [ + "itemId" + ], + "referenceTable": "drive_item", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "drive_item_user_state_fk_1", + "columns": [ + "userId" + ], + "referenceTable": "user_profile", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "drive_item_user_state_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "itemId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "drive_item_user_recent_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "lastOpenedAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "embedded_object", + "dartName": "EmbeddedObject", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "random_v7", + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileId", + "fieldName": "fileId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revision", + "fieldName": "revision", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sheetId", + "fieldName": "sheetId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "kind", + "fieldName": "kind", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:EmbeddedObjectKind" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "anchorJson", + "fieldName": "anchorJson", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "specJson", + "fieldName": "specJson", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "zIndex", + "fieldName": "zIndex", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "title", + "fieldName": "title", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "altText", + "fieldName": "altText", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "embedded_object_fk_0", + "columns": [ + "fileId" + ], + "referenceTable": "rev_file", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "embedded_object_fk_1", + "columns": [ + "sheetId" + ], + "referenceTable": "sheet", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4, + "deferrable": "initiallyDeferred" + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "embedded_object_file_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "fileId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "feedback_report", + "dartName": "FeedbackReport", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "fieldName": "userId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "kind", + "fieldName": "kind", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:FeedbackKind" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "message", + "fieldName": "message", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "app", + "fieldName": "app", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "appVersion", + "fieldName": "appVersion", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "platform", + "fieldName": "platform", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "route", + "fieldName": "route", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "stackTrace", + "fieldName": "stackTrace", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "contextJson", + "fieldName": "contextJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "feedback_report_created_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "createdAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "filter_view", + "dartName": "FilterView", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "random_v7", + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileId", + "fieldName": "fileId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revision", + "fieldName": "revision", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sheetId", + "fieldName": "sheetId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "name", + "fieldName": "name", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "rangeJson", + "fieldName": "rangeJson", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "criteriaJson", + "fieldName": "criteriaJson", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sortJson", + "fieldName": "sortJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdBy", + "fieldName": "createdBy", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "filter_view_fk_0", + "columns": [ + "fileId" + ], + "referenceTable": "rev_file", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "filter_view_fk_1", + "columns": [ + "sheetId" + ], + "referenceTable": "sheet", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4, + "deferrable": "initiallyDeferred" + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "filter_view_file_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "fileId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "headless_change", + "dartName": "HeadlessChange", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "changeId", + "fieldName": "changeId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "kind", + "fieldName": "kind", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "itemId", + "fieldName": "itemId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "documentId", + "fieldName": "documentId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revision", + "fieldName": "revision", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "inverseJson", + "fieldName": "inverseJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "metaField", + "fieldName": "metaField", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "metaBeforeJson", + "fieldName": "metaBeforeJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "metaAfterJson", + "fieldName": "metaAfterJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "fieldName": "userId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "actorKind", + "fieldName": "actorKind", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:ActorKind" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "label", + "fieldName": "label", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "toolName", + "fieldName": "toolName", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revertedAt", + "fieldName": "revertedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "headless_change_fk_0", + "columns": [ + "itemId" + ], + "referenceTable": "drive_item", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "headless_change_id_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "changeId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "headless_change_created_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "createdAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "item_version", + "dartName": "ItemVersion", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "itemId", + "fieldName": "itemId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "kind", + "fieldName": "kind", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:VersionKind" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "name", + "fieldName": "name", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdById", + "fieldName": "createdById", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "authors", + "fieldName": "authors", + "columnType": 8, + "isNullable": false, + "dartType": "List" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "storageKind", + "fieldName": "storageKind", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:VersionStorage" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revision", + "fieldName": "revision", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "snapshotFileId", + "fieldName": "snapshotFileId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sizeBytes", + "fieldName": "sizeBytes", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "item_version_fk_0", + "columns": [ + "itemId" + ], + "referenceTable": "drive_item", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "item_version_item_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "itemId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "createdAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "item_version_state", + "dartName": "ItemVersionState", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "itemId", + "fieldName": "itemId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lastEditAt", + "fieldName": "lastEditAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "dirtySince", + "fieldName": "dirtySince", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lastVersionAt", + "fieldName": "lastVersionAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "editorsSinceLastVersion", + "fieldName": "editorsSinceLastVersion", + "columnType": 8, + "isNullable": false, + "dartType": "List" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lastRevision", + "fieldName": "lastRevision", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "capturingSince", + "fieldName": "capturingSince", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "item_version_state_fk_0", + "columns": [ + "itemId" + ], + "referenceTable": "drive_item", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "item_version_state_item_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "itemId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "item_version_state_dirty_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "dirtySince" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "item_view_day", + "dartName": "ItemViewDay", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "itemId", + "fieldName": "itemId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "fieldName": "userId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "day", + "fieldName": "day", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "views", + "fieldName": "views", + "columnType": 6, + "isNullable": false, + "columnDefault": "1", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lastViewedAt", + "fieldName": "lastViewedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "item_view_day_fk_0", + "columns": [ + "itemId" + ], + "referenceTable": "drive_item", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "item_view_day_fk_1", + "columns": [ + "userId" + ], + "referenceTable": "user_profile", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "item_view_day_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "itemId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "day" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "item_view_day_item_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "itemId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "day" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "macro", + "dartName": "Macro", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "random_v7", + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileId", + "fieldName": "fileId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revision", + "fieldName": "revision", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "projectId", + "fieldName": "projectId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "name", + "fieldName": "name", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "functionName", + "fieldName": "functionName", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "shortcutSlot", + "fieldName": "shortcutSlot", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "macro_fk_0", + "columns": [ + "fileId" + ], + "referenceTable": "rev_file", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "macro_fk_1", + "columns": [ + "projectId" + ], + "referenceTable": "script_project", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4, + "deferrable": "initiallyDeferred" + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "macro_file_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "fileId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "merged_range", + "dartName": "MergedRange", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "random_v7", + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileId", + "fieldName": "fileId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revision", + "fieldName": "revision", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sheetId", + "fieldName": "sheetId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "rangeJson", + "fieldName": "rangeJson", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "merged_range_fk_0", + "columns": [ + "fileId" + ], + "referenceTable": "rev_file", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "merged_range_fk_1", + "columns": [ + "sheetId" + ], + "referenceTable": "sheet", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4, + "deferrable": "initiallyDeferred" + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "merged_range_file_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "fileId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "named_range", + "dartName": "NamedRange", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "random_v7", + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileId", + "fieldName": "fileId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revision", + "fieldName": "revision", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "spreadsheetId", + "fieldName": "spreadsheetId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "name", + "fieldName": "name", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "rangeJson", + "fieldName": "rangeJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "formula", + "fieldName": "formula", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "comment", + "fieldName": "comment", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "metaJson", + "fieldName": "metaJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "named_range_fk_0", + "columns": [ + "fileId" + ], + "referenceTable": "rev_file", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "named_range_fk_1", + "columns": [ + "spreadsheetId" + ], + "referenceTable": "spreadsheet", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4, + "deferrable": "initiallyDeferred" + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "named_range_file_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "fileId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "notification", + "dartName": "UserNotification", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "fieldName": "userId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "kind", + "fieldName": "kind", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:NotificationKind" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "itemId", + "fieldName": "itemId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "actorId", + "fieldName": "actorId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "payload", + "fieldName": "payload", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "readAt", + "fieldName": "readAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "emailedAt", + "fieldName": "emailedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "emailPending", + "fieldName": "emailPending", + "columnType": 1, + "isNullable": true, + "columnDefault": "false", + "dartType": "bool?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "notification_fk_0", + "columns": [ + "userId" + ], + "referenceTable": "user_profile", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "notification_user_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "createdAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "notification_email_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "emailPending" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "createdAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "notification_created_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "createdAt" + } + ], + "type": "brin", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "ownership_transfer", + "dartName": "OwnershipTransfer", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "itemId", + "fieldName": "itemId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fromUserId", + "fieldName": "fromUserId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "toUserId", + "fieldName": "toUserId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "handledAt", + "fieldName": "handledAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "accepted", + "fieldName": "accepted", + "columnType": 1, + "isNullable": true, + "dartType": "bool?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "ownership_transfer_fk_0", + "columns": [ + "itemId" + ], + "referenceTable": "drive_item", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "ownership_transfer_fk_1", + "columns": [ + "toUserId" + ], + "referenceTable": "user_profile", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "ownership_transfer_item_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "itemId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "handledAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "personal_access_token", + "dartName": "PersonalAccessToken", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "fieldName": "userId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "name", + "fieldName": "name", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "tokenHash", + "fieldName": "tokenHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "prefix", + "fieldName": "prefix", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "scopes", + "fieldName": "scopes", + "columnType": 8, + "isNullable": false, + "dartType": "List" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "expiresAt", + "fieldName": "expiresAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lastUsedAt", + "fieldName": "lastUsedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revokedAt", + "fieldName": "revokedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "personal_access_token_fk_0", + "columns": [ + "userId" + ], + "referenceTable": "user_profile", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "personal_access_token_user_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "personal_access_token_hash_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "tokenHash" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "pivot_table", + "dartName": "PivotTable", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "random_v7", + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileId", + "fieldName": "fileId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revision", + "fieldName": "revision", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sheetId", + "fieldName": "sheetId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "anchorRowId", + "fieldName": "anchorRowId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "anchorColId", + "fieldName": "anchorColId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sourceRangeJson", + "fieldName": "sourceRangeJson", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "specJson", + "fieldName": "specJson", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "pivot_table_fk_0", + "columns": [ + "fileId" + ], + "referenceTable": "rev_file", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "pivot_table_fk_1", + "columns": [ + "sheetId" + ], + "referenceTable": "sheet", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4, + "deferrable": "initiallyDeferred" + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "pivot_table_file_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "fileId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "protected_range", + "dartName": "ProtectedRange", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "random_v7", + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileId", + "fieldName": "fileId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revision", + "fieldName": "revision", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sheetId", + "fieldName": "sheetId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "rangeJson", + "fieldName": "rangeJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "exceptRangesJson", + "fieldName": "exceptRangesJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "description", + "fieldName": "description", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "warningOnly", + "fieldName": "warningOnly", + "columnType": 1, + "isNullable": false, + "columnDefault": "false", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "editorsJson", + "fieldName": "editorsJson", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdBy", + "fieldName": "createdBy", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "protected_range_fk_0", + "columns": [ + "fileId" + ], + "referenceTable": "rev_file", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "protected_range_fk_1", + "columns": [ + "sheetId" + ], + "referenceTable": "sheet", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4, + "deferrable": "initiallyDeferred" + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "protected_range_file_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "fileId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "publication", + "dartName": "Publication", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "publicId", + "fieldName": "publicId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "ownerId", + "fieldName": "ownerId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "name", + "fieldName": "name", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "siteUrl", + "fieldName": "siteUrl", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "webhookUrl", + "fieldName": "webhookUrl", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "verifiedHost", + "fieldName": "verifiedHost", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "verifiedAt", + "fieldName": "verifiedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "templateItemId", + "fieldName": "templateItemId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "termsUrl", + "fieldName": "termsUrl", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fields", + "fieldName": "fields", + "columnType": 8, + "isNullable": false, + "dartType": "protocol:PublicationFields" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "signingSecret", + "fieldName": "signingSecret", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "previousSigningSecret", + "fieldName": "previousSigningSecret", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "previousSecretUntil", + "fieldName": "previousSecretUntil", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "apiKeyHash", + "fieldName": "apiKeyHash", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "apiKeyLast4", + "fieldName": "apiKeyLast4", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "enabled", + "fieldName": "enabled", + "columnType": 1, + "isNullable": false, + "columnDefault": "true", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "updatedAt", + "fieldName": "updatedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "publication_fk_0", + "columns": [ + "ownerId" + ], + "referenceTable": "user_profile", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "publication_fk_1", + "columns": [ + "templateItemId" + ], + "referenceTable": "drive_item", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 0 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "publication_public_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "publicId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "publication_owner_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "ownerId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "publication_api_key_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "apiKeyHash" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "publication_block", + "dartName": "PublicationBlock", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "publicationId", + "fieldName": "publicationId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "fieldName": "userId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "at", + "fieldName": "at", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "publication_block_fk_0", + "columns": [ + "publicationId" + ], + "referenceTable": "publication", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "publication_block_fk_1", + "columns": [ + "userId" + ], + "referenceTable": "user_profile", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "publication_block_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "publicationId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "publication_consent", + "dartName": "PublicationConsent", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "publicationId", + "fieldName": "publicationId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "fieldName": "userId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "consentKey", + "fieldName": "consentKey", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "at", + "fieldName": "at", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "publication_consent_fk_0", + "columns": [ + "publicationId" + ], + "referenceTable": "publication", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "publication_consent_fk_1", + "columns": [ + "userId" + ], + "referenceTable": "user_profile", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "publication_consent_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "publicationId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "publication_delivery", + "dartName": "PublicationDelivery", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "publicationId", + "fieldName": "publicationId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "submissionId", + "fieldName": "submissionId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "webhookId", + "fieldName": "webhookId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "type", + "fieldName": "type", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sequence", + "fieldName": "sequence", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "payload", + "fieldName": "payload", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "attempts", + "fieldName": "attempts", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "nextAttemptAt", + "fieldName": "nextAttemptAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "leaseUntil", + "fieldName": "leaseUntil", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lastStatus", + "fieldName": "lastStatus", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lastError", + "fieldName": "lastError", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "deliveredAt", + "fieldName": "deliveredAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "failedAt", + "fieldName": "failedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "publication_delivery_fk_0", + "columns": [ + "publicationId" + ], + "referenceTable": "publication", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "publication_delivery_fk_1", + "columns": [ + "submissionId" + ], + "referenceTable": "submission", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 0 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "publication_delivery_webhook_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "webhookId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "publication_delivery_due_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "nextAttemptAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "publication_delivery_publication_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "publicationId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "createdAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "publication_moderator", + "dartName": "PublicationModerator", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "publicationId", + "fieldName": "publicationId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "fieldName": "userId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "email", + "fieldName": "email", + "columnType": 0, + "isNullable": false, + "dartType": "String" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "publication_moderator_fk_0", + "columns": [ + "publicationId" + ], + "referenceTable": "publication", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "publication_moderator_fk_1", + "columns": [ + "userId" + ], + "referenceTable": "user_profile", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "publication_moderator_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "publicationId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "email" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "publication_moderator_user_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "rev_file", + "dartName": "RevFile", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileUuid", + "fieldName": "fileUuid", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "kind", + "fieldName": "kind", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:RevFileKind" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "itemId", + "fieldName": "itemId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "headRevision", + "fieldName": "headRevision", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "prunedBelow", + "fieldName": "prunedBelow", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "initPending", + "fieldName": "initPending", + "columnType": 1, + "isNullable": false, + "columnDefault": "false", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "purging", + "fieldName": "purging", + "columnType": 1, + "isNullable": false, + "columnDefault": "false", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "schemaHash", + "fieldName": "schemaHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "stats", + "fieldName": "stats", + "columnType": 0, + "isNullable": false, + "columnDefault": "'{}'", + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "contentCount", + "fieldName": "contentCount", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "updatedAt", + "fieldName": "updatedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "rev_file_fk_0", + "columns": [ + "itemId" + ], + "referenceTable": "drive_item", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 2 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "rev_file_uuid_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "fileUuid" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "rev_file_item_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "itemId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "rev_hook_state", + "dartName": "RevHookState", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileId", + "fieldName": "fileId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "cursorsJson", + "fieldName": "cursorsJson", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "leaseOwner", + "fieldName": "leaseOwner", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "leaseUntil", + "fieldName": "leaseUntil", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "attempts", + "fieldName": "attempts", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lastError", + "fieldName": "lastError", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "nextAttemptAt", + "fieldName": "nextAttemptAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "updatedAt", + "fieldName": "updatedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "rev_hook_state_fk_0", + "columns": [ + "fileId" + ], + "referenceTable": "rev_file", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "rev_hook_state_file_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "fileId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "rev_hook_state_due_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "nextAttemptAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "rev_op", + "dartName": "RevOp", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileId", + "fieldName": "fileId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revision", + "fieldName": "revision", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "clientOpId", + "fieldName": "clientOpId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "baseRevision", + "fieldName": "baseRevision", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "authorId", + "fieldName": "authorId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "authorUuid", + "fieldName": "authorUuid", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sessionId", + "fieldName": "sessionId", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "onBehalfOfSessionId", + "fieldName": "onBehalfOfSessionId", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "actorKind", + "fieldName": "actorKind", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:ActorKind" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "label", + "fieldName": "label", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "groupId", + "fieldName": "groupId", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "groupIndex", + "fieldName": "groupIndex", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "groupLast", + "fieldName": "groupLast", + "columnType": 1, + "isNullable": false, + "columnDefault": "true", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "recordId", + "fieldName": "recordId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "offline", + "fieldName": "offline", + "columnType": 1, + "isNullable": false, + "columnDefault": "false", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "payload", + "fieldName": "payload", + "columnType": 5, + "isNullable": true, + "dartType": "dart:typed_data:ByteData?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "changeCount", + "fieldName": "changeCount", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "summaryJson", + "fieldName": "summaryJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "detailJson", + "fieldName": "detailJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "outcomeJson", + "fieldName": "outcomeJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "appliedEqualsSubmitted", + "fieldName": "appliedEqualsSubmitted", + "columnType": 1, + "isNullable": false, + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "clientCreatedAt", + "fieldName": "clientCreatedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "committedAt", + "fieldName": "committedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "rev_op_fk_0", + "columns": [ + "fileId" + ], + "referenceTable": "rev_file", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "rev_op_rev_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "fileId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "revision" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "rev_op_client_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "fileId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "clientOpId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "rev_op_record_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "recordId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "rev_op_committed_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "committedAt" + } + ], + "type": "brin", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "script_authorization", + "dartName": "ScriptAuthorization", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "fieldName": "userId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "spreadsheetUuid", + "fieldName": "spreadsheetUuid", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "scopes", + "fieldName": "scopes", + "columnType": 8, + "isNullable": false, + "dartType": "List" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "grantedAt", + "fieldName": "grantedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revokedAt", + "fieldName": "revokedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "script_auth_user_sheet_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "spreadsheetUuid" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "script_build", + "dartName": "ScriptBuild", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "random_v7", + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileId", + "fieldName": "fileId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revision", + "fieldName": "revision", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "projectId", + "fieldName": "projectId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "status", + "fieldName": "status", + "columnType": 0, + "isNullable": false, + "columnDefault": "'ok'", + "dartType": "protocol:ScriptBuildStatus" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sourceHash", + "fieldName": "sourceHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "bytecodeFileUuid", + "fieldName": "bytecodeFileUuid", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "diagnosticsJson", + "fieldName": "diagnosticsJson", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "exportsJson", + "fieldName": "exportsJson", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "apiVersion", + "fieldName": "apiVersion", + "columnType": 6, + "isNullable": false, + "columnDefault": "1", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "compilerVersion", + "fieldName": "compilerVersion", + "columnType": 0, + "isNullable": false, + "columnDefault": "''", + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "requestedBy", + "fieldName": "requestedBy", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lastGoodSourceHash", + "fieldName": "lastGoodSourceHash", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lastGoodBytecodeFileUuid", + "fieldName": "lastGoodBytecodeFileUuid", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lastGoodExportsJson", + "fieldName": "lastGoodExportsJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lastGoodBuiltAt", + "fieldName": "lastGoodBuiltAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "positionsFileUuid", + "fieldName": "positionsFileUuid", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "builtAt", + "fieldName": "builtAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "script_build_fk_0", + "columns": [ + "fileId" + ], + "referenceTable": "rev_file", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "script_build_fk_1", + "columns": [ + "projectId" + ], + "referenceTable": "script_project", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4, + "deferrable": "initiallyDeferred" + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "script_build_file_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "fileId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "script_execution", + "dartName": "ScriptExecution", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "uuid", + "fieldName": "uuid", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "spreadsheetUuid", + "fieldName": "spreadsheetUuid", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "projectId", + "fieldName": "projectId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "functionName", + "fieldName": "functionName", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "kind", + "fieldName": "kind", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:ScriptExecutionKind" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "triggerId", + "fieldName": "triggerId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "fieldName": "userId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "runAsUserId", + "fieldName": "runAsUserId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "runtime", + "fieldName": "runtime", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:ScriptRuntimeKind" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "status", + "fieldName": "status", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:ScriptExecutionStatus" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "buildSourceHash", + "fieldName": "buildSourceHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "startedAt", + "fieldName": "startedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "durationMs", + "fieldName": "durationMs", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "logs", + "fieldName": "logs", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "errorMessage", + "fieldName": "errorMessage", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "errorStackJson", + "fieldName": "errorStackJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "changeId", + "fieldName": "changeId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "cellsRead", + "fieldName": "cellsRead", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "cellsWritten", + "fieldName": "cellsWritten", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fetchCalls", + "fieldName": "fetchCalls", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "callCount", + "fieldName": "callCount", + "columnType": 6, + "isNullable": false, + "columnDefault": "1", + "dartType": "int" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "script_exec_uuid_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "uuid" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "script_exec_sheet_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "spreadsheetUuid" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "startedAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "script_exec_user_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "startedAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "script_file", + "dartName": "ScriptFile", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "random_v7", + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileId", + "fieldName": "fileId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revision", + "fieldName": "revision", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "projectId", + "fieldName": "projectId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "name", + "fieldName": "name", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sortKey", + "fieldName": "sortKey", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "source", + "fieldName": "source", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "contentHash", + "fieldName": "contentHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "updatedBy", + "fieldName": "updatedBy", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "updatedAt", + "fieldName": "updatedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "script_file_fk_0", + "columns": [ + "fileId" + ], + "referenceTable": "rev_file", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "script_file_fk_1", + "columns": [ + "projectId" + ], + "referenceTable": "script_project", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4, + "deferrable": "initiallyDeferred" + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "script_file_file_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "fileId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "script_job", + "dartName": "ScriptJob", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "kind", + "fieldName": "kind", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:ScriptExecutionKind" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "spreadsheetUuid", + "fieldName": "spreadsheetUuid", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "itemUuid", + "fieldName": "itemUuid", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "triggerId", + "fieldName": "triggerId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "runAsUserId", + "fieldName": "runAsUserId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "functionName", + "fieldName": "functionName", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "payloadJson", + "fieldName": "payloadJson", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "status", + "fieldName": "status", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:ScriptExecutionStatus" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "enqueuedAt", + "fieldName": "enqueuedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "leaseOwner", + "fieldName": "leaseOwner", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "leaseUntil", + "fieldName": "leaseUntil", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "attempts", + "fieldName": "attempts", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "coalesced", + "fieldName": "coalesced", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "resultJson", + "fieldName": "resultJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "finishedAt", + "fieldName": "finishedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "script_job_status_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "status" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "enqueuedAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "script_job_trigger_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "triggerId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "status" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "script_job_user_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "runAsUserId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "status" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "script_project", + "dartName": "ScriptProject", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "random_v7", + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileId", + "fieldName": "fileId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revision", + "fieldName": "revision", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "spreadsheetId", + "fieldName": "spreadsheetId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "name", + "fieldName": "name", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "settingsJson", + "fieldName": "settingsJson", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "script_project_fk_0", + "columns": [ + "fileId" + ], + "referenceTable": "rev_file", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "script_project_fk_1", + "columns": [ + "spreadsheetId" + ], + "referenceTable": "spreadsheet", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4, + "deferrable": "initiallyDeferred" + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "script_project_file_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "fileId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "script_property", + "dartName": "ScriptProperty", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "random_v7", + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileId", + "fieldName": "fileId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revision", + "fieldName": "revision", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "projectId", + "fieldName": "projectId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "key", + "fieldName": "key", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "value", + "fieldName": "value", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "script_property_fk_0", + "columns": [ + "fileId" + ], + "referenceTable": "rev_file", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "script_property_fk_1", + "columns": [ + "projectId" + ], + "referenceTable": "script_project", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4, + "deferrable": "initiallyDeferred" + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "script_property_file_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "fileId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "script_quota_usage", + "dartName": "ScriptQuotaUsage", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "fieldName": "userId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "day", + "fieldName": "day", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "serverRuntimeMs", + "fieldName": "serverRuntimeMs", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "clientRuntimeMs", + "fieldName": "clientRuntimeMs", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "urlFetchCalls", + "fieldName": "urlFetchCalls", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "urlFetchBytes", + "fieldName": "urlFetchBytes", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "triggerRuns", + "fieldName": "triggerRuns", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "script_quota_user_day_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "day" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "script_trigger", + "dartName": "ScriptTrigger", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "random_v7", + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileId", + "fieldName": "fileId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revision", + "fieldName": "revision", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "projectId", + "fieldName": "projectId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "kind", + "fieldName": "kind", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:TriggerKind" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "functionName", + "fieldName": "functionName", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "scheduleJson", + "fieldName": "scheduleJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "runAsUserId", + "fieldName": "runAsUserId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "enabled", + "fieldName": "enabled", + "columnType": 1, + "isNullable": false, + "columnDefault": "true", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "optionsJson", + "fieldName": "optionsJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "script_trigger_fk_0", + "columns": [ + "fileId" + ], + "referenceTable": "rev_file", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "script_trigger_fk_1", + "columns": [ + "projectId" + ], + "referenceTable": "script_project", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4, + "deferrable": "initiallyDeferred" + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "script_trigger_file_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "fileId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "script_trigger_state", + "dartName": "ScriptTriggerState", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "triggerId", + "fieldName": "triggerId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "spreadsheetUuid", + "fieldName": "spreadsheetUuid", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "scheduleJson", + "fieldName": "scheduleJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "nextRunAt", + "fieldName": "nextRunAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lastRunAt", + "fieldName": "lastRunAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lastStatus", + "fieldName": "lastStatus", + "columnType": 0, + "isNullable": true, + "dartType": "protocol:ScriptExecutionStatus?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "consecutiveFailures", + "fieldName": "consecutiveFailures", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "disabledReason", + "fieldName": "disabledReason", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "runningSince", + "fieldName": "runningSince", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lastNotifiedAt", + "fieldName": "lastNotifiedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "script_trigger_state_id_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "triggerId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "script_trigger_state_due_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "nextRunAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "script_user_property", + "dartName": "ScriptUserProperty", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "fieldName": "userId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileUuid", + "fieldName": "fileUuid", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "key", + "fieldName": "key", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "value", + "fieldName": "value", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "updatedAt", + "fieldName": "updatedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "script_user_property_fk_0", + "columns": [ + "userId" + ], + "referenceTable": "user_profile", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "script_user_property_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "fileUuid" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "key" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "server_change_record", + "dartName": "ServerChangeRecord", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "changeId", + "fieldName": "changeId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileUuid", + "fieldName": "fileUuid", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "fieldName": "userId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "actorKind", + "fieldName": "actorKind", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:ActorKind" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "label", + "fieldName": "label", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "inverseJson", + "fieldName": "inverseJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "inverseFileId", + "fieldName": "inverseFileId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "forwardFileId", + "fieldName": "forwardFileId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "recordId", + "fieldName": "recordId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "cellsAffected", + "fieldName": "cellsAffected", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "expiresAt", + "fieldName": "expiresAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revertedAt", + "fieldName": "revertedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revertChangeId", + "fieldName": "revertChangeId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "server_change_record_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "recordId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "server_change_file_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "fileUuid" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "createdAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "server_change_id_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "changeId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "share_link", + "dartName": "ShareLink", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "itemId", + "fieldName": "itemId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "token", + "fieldName": "token", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "role", + "fieldName": "role", + "columnType": 0, + "isNullable": true, + "dartType": "protocol:AccessRole?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdById", + "fieldName": "createdById", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "share_link_fk_0", + "columns": [ + "itemId" + ], + "referenceTable": "drive_item", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "share_link_item_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "itemId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "share_link_token_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "token" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "sheet", + "dartName": "Sheet", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "random_v7", + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileId", + "fieldName": "fileId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revision", + "fieldName": "revision", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "spreadsheetId", + "fieldName": "spreadsheetId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "name", + "fieldName": "name", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sortKey", + "fieldName": "sortKey", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "kind", + "fieldName": "kind", + "columnType": 0, + "isNullable": false, + "columnDefault": "'grid'", + "dartType": "protocol:SheetKind" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "hidden", + "fieldName": "hidden", + "columnType": 1, + "isNullable": false, + "columnDefault": "false", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "tabColor", + "fieldName": "tabColor", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "rightToLeft", + "fieldName": "rightToLeft", + "columnType": 1, + "isNullable": false, + "columnDefault": "false", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "showGridlines", + "fieldName": "showGridlines", + "columnType": 1, + "isNullable": false, + "columnDefault": "true", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "frozenRows", + "fieldName": "frozenRows", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "frozenColumns", + "fieldName": "frozenColumns", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "defaultRowHeight", + "fieldName": "defaultRowHeight", + "columnType": 3, + "isNullable": false, + "columnDefault": "21.0", + "dartType": "double" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "defaultColumnWidth", + "fieldName": "defaultColumnWidth", + "columnType": 3, + "isNullable": false, + "columnDefault": "100.0", + "dartType": "double" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "formatJson", + "fieldName": "formatJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "printSettingsJson", + "fieldName": "printSettingsJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "sheet_fk_0", + "columns": [ + "fileId" + ], + "referenceTable": "rev_file", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "sheet_fk_1", + "columns": [ + "spreadsheetId" + ], + "referenceTable": "spreadsheet", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4, + "deferrable": "initiallyDeferred" + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "sheet_file_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "fileId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "sheet_order_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "spreadsheetId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "sortKey" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "sheet_column", + "dartName": "SheetColumn", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "random_v7", + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileId", + "fieldName": "fileId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revision", + "fieldName": "revision", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sheetId", + "fieldName": "sheetId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sortKey", + "fieldName": "sortKey", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "width", + "fieldName": "width", + "columnType": 3, + "isNullable": true, + "dartType": "double?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "hidden", + "fieldName": "hidden", + "columnType": 1, + "isNullable": false, + "columnDefault": "false", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "formatJson", + "fieldName": "formatJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "sheet_column_fk_0", + "columns": [ + "fileId" + ], + "referenceTable": "rev_file", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "sheet_column_fk_1", + "columns": [ + "sheetId" + ], + "referenceTable": "sheet", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4, + "deferrable": "initiallyDeferred" + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "sheet_column_file_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "fileId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "sheet_column_order_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "sheetId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "sortKey" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "sheet_row", + "dartName": "SheetRow", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "random_v7", + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileId", + "fieldName": "fileId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revision", + "fieldName": "revision", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sheetId", + "fieldName": "sheetId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sortKey", + "fieldName": "sortKey", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "height", + "fieldName": "height", + "columnType": 3, + "isNullable": true, + "dartType": "double?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "hidden", + "fieldName": "hidden", + "columnType": 1, + "isNullable": false, + "columnDefault": "false", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "formatJson", + "fieldName": "formatJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "sheet_row_fk_0", + "columns": [ + "fileId" + ], + "referenceTable": "rev_file", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "sheet_row_fk_1", + "columns": [ + "sheetId" + ], + "referenceTable": "sheet", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4, + "deferrable": "initiallyDeferred" + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "sheet_row_file_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "fileId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "sheet_row_order_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "sheetId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "sortKey" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "sheets_import_job", + "dartName": "SheetsImportJob", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "uuid", + "fieldName": "uuid", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "fieldName": "userId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "authUserId", + "fieldName": "authUserId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "spreadsheetUuid", + "fieldName": "spreadsheetUuid", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "uploadFileId", + "fieldName": "uploadFileId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileName", + "fieldName": "fileName", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "format", + "fieldName": "format", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "optionsJson", + "fieldName": "optionsJson", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "activeSheetId", + "fieldName": "activeSheetId", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "status", + "fieldName": "status", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:SheetsImportStatus" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "progressDone", + "fieldName": "progressDone", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "progressTotal", + "fieldName": "progressTotal", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "reportJson", + "fieldName": "reportJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sheetIdsJson", + "fieldName": "sheetIdsJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "changeId", + "fieldName": "changeId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "errorCode", + "fieldName": "errorCode", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "error", + "fieldName": "error", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "cancelRequested", + "fieldName": "cancelRequested", + "columnType": 1, + "isNullable": false, + "columnDefault": "false", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "updatedAt", + "fieldName": "updatedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "leaseOwner", + "fieldName": "leaseOwner", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "leaseUntil", + "fieldName": "leaseUntil", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "sheets_import_job_uuid_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "uuid" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "sheets_import_job_user_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "status" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "sheets_snapshot_index", + "dartName": "SheetsSnapshotIndex", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "itemVersionId", + "fieldName": "itemVersionId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sheetsJson", + "fieldName": "sheetsJson", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "changedRangesJson", + "fieldName": "changedRangesJson", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "authorIds", + "fieldName": "authorIds", + "columnType": 8, + "isNullable": false, + "dartType": "List" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "diffFileId", + "fieldName": "diffFileId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "sheets_snapshot_index_fk_0", + "columns": [ + "itemVersionId" + ], + "referenceTable": "item_version", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "sheets_snapshot_index_version_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "itemVersionId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "slide", + "dartName": "Slide", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "random_v7", + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileId", + "fieldName": "fileId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revision", + "fieldName": "revision", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "deckId", + "fieldName": "deckId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sortKey", + "fieldName": "sortKey", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "groupId", + "fieldName": "groupId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "layoutId", + "fieldName": "layoutId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "backgroundJson", + "fieldName": "backgroundJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "skipped", + "fieldName": "skipped", + "columnType": 1, + "isNullable": false, + "columnDefault": "false", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "notes", + "fieldName": "notes", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "transitionJson", + "fieldName": "transitionJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "hideMasterElements", + "fieldName": "hideMasterElements", + "columnType": 1, + "isNullable": true, + "dartType": "bool?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "timingsJson", + "fieldName": "timingsJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "slide_fk_0", + "columns": [ + "fileId" + ], + "referenceTable": "rev_file", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "slide_fk_1", + "columns": [ + "deckId" + ], + "referenceTable": "deck", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4, + "deferrable": "initiallyDeferred" + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "slide_file_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "fileId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "slide_order_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "deckId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "sortKey" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "slide_build", + "dartName": "SlideBuild", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "random_v7", + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileId", + "fieldName": "fileId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revision", + "fieldName": "revision", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "slideId", + "fieldName": "slideId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "elementId", + "fieldName": "elementId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "kind", + "fieldName": "kind", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:BuildKind" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "effect", + "fieldName": "effect", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sortKey", + "fieldName": "sortKey", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "trigger", + "fieldName": "trigger", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:BuildTrigger" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "delayMs", + "fieldName": "delayMs", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "durationMs", + "fieldName": "durationMs", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "easing", + "fieldName": "easing", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "optionsJson", + "fieldName": "optionsJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "slide_build_fk_0", + "columns": [ + "fileId" + ], + "referenceTable": "rev_file", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "slide_build_fk_1", + "columns": [ + "slideId" + ], + "referenceTable": "slide", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4, + "deferrable": "initiallyDeferred" + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "slide_build_fk_2", + "columns": [ + "elementId" + ], + "referenceTable": "slide_element", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4, + "deferrable": "initiallyDeferred" + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "slide_build_file_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "fileId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "slide_build_slide_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "slideId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "sortKey" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "slide_build_element_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "elementId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "slide_element", + "dartName": "SlideElement", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "random_v7", + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileId", + "fieldName": "fileId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revision", + "fieldName": "revision", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "slideId", + "fieldName": "slideId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "layoutId", + "fieldName": "layoutId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "masterId", + "fieldName": "masterId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "parentId", + "fieldName": "parentId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "kind", + "fieldName": "kind", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:SlideElementKind" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "name", + "fieldName": "name", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "x", + "fieldName": "x", + "columnType": 3, + "isNullable": true, + "dartType": "double?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "y", + "fieldName": "y", + "columnType": 3, + "isNullable": true, + "dartType": "double?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "width", + "fieldName": "width", + "columnType": 3, + "isNullable": true, + "dartType": "double?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "height", + "fieldName": "height", + "columnType": 3, + "isNullable": true, + "dartType": "double?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "rotation", + "fieldName": "rotation", + "columnType": 3, + "isNullable": true, + "dartType": "double?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "flipH", + "fieldName": "flipH", + "columnType": 1, + "isNullable": false, + "columnDefault": "false", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "flipV", + "fieldName": "flipV", + "columnType": 1, + "isNullable": false, + "columnDefault": "false", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "zKey", + "fieldName": "zKey", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "readingKey", + "fieldName": "readingKey", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "locked", + "fieldName": "locked", + "columnType": 1, + "isNullable": false, + "columnDefault": "false", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "hidden", + "fieldName": "hidden", + "columnType": 1, + "isNullable": false, + "columnDefault": "false", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "placeholderType", + "fieldName": "placeholderType", + "columnType": 0, + "isNullable": true, + "dartType": "protocol:PlaceholderType?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "placeholderOf", + "fieldName": "placeholderOf", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "matchKey", + "fieldName": "matchKey", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "text", + "fieldName": "text", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "textStyleJson", + "fieldName": "textStyleJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fillJson", + "fieldName": "fillJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "strokeJson", + "fieldName": "strokeJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "effectsJson", + "fieldName": "effectsJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "opacity", + "fieldName": "opacity", + "columnType": 3, + "isNullable": true, + "dartType": "double?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "dataJson", + "fieldName": "dataJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "linkJson", + "fieldName": "linkJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "altText", + "fieldName": "altText", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "decorative", + "fieldName": "decorative", + "columnType": 1, + "isNullable": false, + "columnDefault": "false", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "slide_element_fk_0", + "columns": [ + "fileId" + ], + "referenceTable": "rev_file", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "slide_element_fk_1", + "columns": [ + "slideId" + ], + "referenceTable": "slide", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4, + "deferrable": "initiallyDeferred" + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "slide_element_fk_2", + "columns": [ + "layoutId" + ], + "referenceTable": "slide_layout", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4, + "deferrable": "initiallyDeferred" + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "slide_element_fk_3", + "columns": [ + "masterId" + ], + "referenceTable": "slide_master", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4, + "deferrable": "initiallyDeferred" + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "slide_element_fk_4", + "columns": [ + "parentId" + ], + "referenceTable": "slide_element", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4, + "deferrable": "initiallyDeferred" + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "slide_element_file_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "fileId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "slide_element_slide_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "slideId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "zKey" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "slide_element_layout_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "layoutId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "slide_element_master_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "masterId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "slide_element_parent_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "parentId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "slide_group", + "dartName": "SlideGroup", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "random_v7", + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileId", + "fieldName": "fileId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revision", + "fieldName": "revision", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "deckId", + "fieldName": "deckId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "title", + "fieldName": "title", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sortKey", + "fieldName": "sortKey", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "parentId", + "fieldName": "parentId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "color", + "fieldName": "color", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "skipped", + "fieldName": "skipped", + "columnType": 1, + "isNullable": false, + "columnDefault": "false", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "slide_group_fk_0", + "columns": [ + "fileId" + ], + "referenceTable": "rev_file", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "slide_group_fk_1", + "columns": [ + "deckId" + ], + "referenceTable": "deck", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4, + "deferrable": "initiallyDeferred" + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "slide_group_file_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "fileId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "slide_group_order_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "deckId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "sortKey" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "slide_layout", + "dartName": "SlideLayout", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "random_v7", + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileId", + "fieldName": "fileId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revision", + "fieldName": "revision", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "masterId", + "fieldName": "masterId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "name", + "fieldName": "name", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "key", + "fieldName": "key", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sortKey", + "fieldName": "sortKey", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "backgroundJson", + "fieldName": "backgroundJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "hideMasterElements", + "fieldName": "hideMasterElements", + "columnType": 1, + "isNullable": false, + "columnDefault": "false", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "hidden", + "fieldName": "hidden", + "columnType": 1, + "isNullable": false, + "columnDefault": "false", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "slide_layout_fk_0", + "columns": [ + "fileId" + ], + "referenceTable": "rev_file", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "slide_layout_fk_1", + "columns": [ + "masterId" + ], + "referenceTable": "slide_master", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4, + "deferrable": "initiallyDeferred" + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "slide_layout_file_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "fileId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "slide_layout_master_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "masterId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "slide_master", + "dartName": "SlideMaster", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "random_v7", + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileId", + "fieldName": "fileId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revision", + "fieldName": "revision", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "themeId", + "fieldName": "themeId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "name", + "fieldName": "name", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sortKey", + "fieldName": "sortKey", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "backgroundJson", + "fieldName": "backgroundJson", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "slide_master_fk_0", + "columns": [ + "fileId" + ], + "referenceTable": "rev_file", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "slide_master_fk_1", + "columns": [ + "themeId" + ], + "referenceTable": "deck_theme", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4, + "deferrable": "initiallyDeferred" + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "slide_master_file_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "fileId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "slide_master_theme_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "themeId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "slide_table_axis", + "dartName": "SlideTableAxis", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "random_v7", + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileId", + "fieldName": "fileId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revision", + "fieldName": "revision", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "elementId", + "fieldName": "elementId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "dimension", + "fieldName": "dimension", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:TableDimension" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sortKey", + "fieldName": "sortKey", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "size", + "fieldName": "size", + "columnType": 3, + "isNullable": true, + "dartType": "double?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "header", + "fieldName": "header", + "columnType": 1, + "isNullable": false, + "columnDefault": "false", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "slide_table_axis_fk_0", + "columns": [ + "fileId" + ], + "referenceTable": "rev_file", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "slide_table_axis_fk_1", + "columns": [ + "elementId" + ], + "referenceTable": "slide_element", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4, + "deferrable": "initiallyDeferred" + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "slide_table_axis_file_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "fileId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "slide_table_axis_element_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "elementId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "sortKey" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "slide_table_cell", + "dartName": "SlideTableCell", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "random_v7", + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileId", + "fieldName": "fileId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revision", + "fieldName": "revision", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "elementId", + "fieldName": "elementId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "rowId", + "fieldName": "rowId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "columnId", + "fieldName": "columnId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "text", + "fieldName": "text", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fill", + "fieldName": "fill", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "borderTop", + "fieldName": "borderTop", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "borderRight", + "fieldName": "borderRight", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "borderBottom", + "fieldName": "borderBottom", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "borderLeft", + "fieldName": "borderLeft", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "vAlign", + "fieldName": "vAlign", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "padding", + "fieldName": "padding", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "rowSpan", + "fieldName": "rowSpan", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "colSpan", + "fieldName": "colSpan", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "slide_table_cell_fk_0", + "columns": [ + "fileId" + ], + "referenceTable": "rev_file", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "slide_table_cell_fk_1", + "columns": [ + "elementId" + ], + "referenceTable": "slide_element", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4, + "deferrable": "initiallyDeferred" + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "slide_table_cell_fk_2", + "columns": [ + "rowId" + ], + "referenceTable": "slide_table_axis", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4, + "deferrable": "initiallyDeferred" + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "slide_table_cell_fk_3", + "columns": [ + "columnId" + ], + "referenceTable": "slide_table_axis", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4, + "deferrable": "initiallyDeferred" + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "slide_table_cell_file_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "fileId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "slide_table_cell_element_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "elementId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "slides_snapshot_index", + "dartName": "SlidesSnapshotIndex", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "itemVersionId", + "fieldName": "itemVersionId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "slidesJson", + "fieldName": "slidesJson", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "changesJson", + "fieldName": "changesJson", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "authorIds", + "fieldName": "authorIds", + "columnType": 8, + "isNullable": false, + "dartType": "List" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "slides_snapshot_index_fk_0", + "columns": [ + "itemVersionId" + ], + "referenceTable": "item_version", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "slides_snapshot_index_version_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "itemVersionId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "spreadsheet", + "dartName": "Spreadsheet", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "random_v7", + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileId", + "fieldName": "fileId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revision", + "fieldName": "revision", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "title", + "fieldName": "title", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "locale", + "fieldName": "locale", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "timeZone", + "fieldName": "timeZone", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "recalcMode", + "fieldName": "recalcMode", + "columnType": 0, + "isNullable": false, + "columnDefault": "'onChange'", + "dartType": "protocol:RecalcMode" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "iterativeCalc", + "fieldName": "iterativeCalc", + "columnType": 1, + "isNullable": false, + "columnDefault": "false", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "maxIterations", + "fieldName": "maxIterations", + "columnType": 6, + "isNullable": false, + "columnDefault": "50", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "convergenceThreshold", + "fieldName": "convergenceThreshold", + "columnType": 3, + "isNullable": false, + "columnDefault": "0.05", + "dartType": "double" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "defaultFontFamily", + "fieldName": "defaultFontFamily", + "columnType": 0, + "isNullable": false, + "columnDefault": "'Inter'", + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "defaultFontSize", + "fieldName": "defaultFontSize", + "columnType": 3, + "isNullable": false, + "columnDefault": "10.0", + "dartType": "double" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "themeJson", + "fieldName": "themeJson", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "displayLanguage", + "fieldName": "displayLanguage", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdBy", + "fieldName": "createdBy", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "spreadsheet_fk_0", + "columns": [ + "fileId" + ], + "referenceTable": "rev_file", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "spreadsheet_file_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "fileId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "storage_reservation", + "dartName": "StorageReservation", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "fieldName": "userId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileUuid", + "fieldName": "fileUuid", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "itemId", + "fieldName": "itemId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "bytes", + "fieldName": "bytes", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "expiresAt", + "fieldName": "expiresAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "storage_reservation_fk_0", + "columns": [ + "userId" + ], + "referenceTable": "user_profile", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "storage_reservation_fk_1", + "columns": [ + "itemId" + ], + "referenceTable": "drive_item", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "storage_reservation_file_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "fileUuid" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "storage_reservation_user_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "storage_reservation_expiry_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "expiresAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "stored_file", + "dartName": "StoredFile", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileUuid", + "fieldName": "fileUuid", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "ownerId", + "fieldName": "ownerId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "itemId", + "fieldName": "itemId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "kind", + "fieldName": "kind", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:StoredFileKind" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "storageId", + "fieldName": "storageId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "path", + "fieldName": "path", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileName", + "fieldName": "fileName", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "mimeType", + "fieldName": "mimeType", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sizeBytes", + "fieldName": "sizeBytes", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "status", + "fieldName": "status", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:StoredFileStatus" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "completedAt", + "fieldName": "completedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "expiresAt", + "fieldName": "expiresAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "stored_file_fk_0", + "columns": [ + "ownerId" + ], + "referenceTable": "user_profile", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 0 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "stored_file_fk_1", + "columns": [ + "itemId" + ], + "referenceTable": "drive_item", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "stored_file_uuid_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "fileUuid" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "stored_file_item_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "itemId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "stored_file_expiry_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "expiresAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "stored_file_owner_kind_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "ownerId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "kind" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "createdAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "stored_file_status_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "status" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "createdAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "submission", + "dartName": "Submission", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "uuid", + "fieldName": "uuid", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "publicationId", + "fieldName": "publicationId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "itemId", + "fieldName": "itemId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "ref", + "fieldName": "ref", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "status", + "fieldName": "status", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:SubmissionStatus" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "statusBeforeSubmit", + "fieldName": "statusBeforeSubmit", + "columnType": 0, + "isNullable": true, + "dartType": "protocol:SubmissionStatus?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "delivery", + "fieldName": "delivery", + "columnType": 0, + "isNullable": true, + "dartType": "protocol:SubmissionDeliveryState?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "values", + "fieldName": "values", + "columnType": 8, + "isNullable": false, + "dartType": "protocol:SubmissionValues" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "publicUrl", + "fieldName": "publicUrl", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "feedbackThreadId", + "fieldName": "feedbackThreadId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdById", + "fieldName": "createdById", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "updatedAt", + "fieldName": "updatedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "disconnectedAt", + "fieldName": "disconnectedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "submission_fk_0", + "columns": [ + "publicationId" + ], + "referenceTable": "publication", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "submission_fk_1", + "columns": [ + "itemId" + ], + "referenceTable": "drive_item", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "submission_uuid_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "uuid" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "submission_item_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "itemId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "submission_publication_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "publicationId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "status" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "submission_asset", + "dartName": "SubmissionAsset", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "submissionId", + "fieldName": "submissionId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fileUuid", + "fieldName": "fileUuid", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "submission_asset_fk_0", + "columns": [ + "submissionId" + ], + "referenceTable": "submission", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "submission_asset_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "submissionId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "fileUuid" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "submission_asset_file_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "fileUuid" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "submission_revision", + "dartName": "SubmissionRevision", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "submissionId", + "fieldName": "submissionId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sequence", + "fieldName": "sequence", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revision", + "fieldName": "revision", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "versionId", + "fieldName": "versionId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "values", + "fieldName": "values", + "columnType": 8, + "isNullable": false, + "dartType": "protocol:SubmissionValues" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "payload", + "fieldName": "payload", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "submittedById", + "fieldName": "submittedById", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "submittedAt", + "fieldName": "submittedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "submission_revision_fk_0", + "columns": [ + "submissionId" + ], + "referenceTable": "submission", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "submission_revision_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "submissionId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "sequence" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "usage_counter_day", + "dartName": "UsageCounterDay", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "day", + "fieldName": "day", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "name", + "fieldName": "name", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "value", + "fieldName": "value", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "usage_counter_day_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "day" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "name" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "usage_day_stats", + "dartName": "UsageDayStats", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "day", + "fieldName": "day", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "includeAdmins", + "fieldName": "includeAdmins", + "columnType": 1, + "isNullable": false, + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "active", + "fieldName": "active", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "editors", + "fieldName": "editors", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "drive", + "fieldName": "drive", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "write", + "fieldName": "write", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sheets", + "fieldName": "sheets", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "slides", + "fieldName": "slides", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "mcp", + "fieldName": "mcp", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "weekly", + "fieldName": "weekly", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "monthly", + "fieldName": "monthly", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "computedAt", + "fieldName": "computedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "usage_day_stats_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "day" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "includeAdmins" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "user_activity_day", + "dartName": "UserActivityDay", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "fieldName": "userId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "day", + "fieldName": "day", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "app", + "fieldName": "app", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:UsageApp" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "edited", + "fieldName": "edited", + "columnType": 1, + "isNullable": false, + "columnDefault": "false", + "dartType": "bool" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "user_activity_day_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "day" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "app" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "user_activity_day_day_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "day" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "user_activity_settings", + "dartName": "UserActivitySettings", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "fieldName": "userId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "showViewHistory", + "fieldName": "showViewHistory", + "columnType": 1, + "isNullable": false, + "columnDefault": "true", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "updatedAt", + "fieldName": "updatedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "user_activity_settings_fk_0", + "columns": [ + "userId" + ], + "referenceTable": "user_profile", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "user_activity_settings_user_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "user_preferences", + "dartName": "UserPreferences", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "fieldName": "userId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "appTheme", + "fieldName": "appTheme", + "columnType": 0, + "isNullable": false, + "columnDefault": "'system'", + "dartType": "protocol:AppThemeMode" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "accentColor", + "fieldName": "accentColor", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "density", + "fieldName": "density", + "columnType": 0, + "isNullable": false, + "columnDefault": "'comfortable'", + "dartType": "protocol:UiDensity" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "reduceMotion", + "fieldName": "reduceMotion", + "columnType": 1, + "isNullable": false, + "columnDefault": "false", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "keymapOverrides", + "fieldName": "keymapOverrides", + "columnType": 0, + "isNullable": false, + "columnDefault": "'{}'", + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "writeDefaults", + "fieldName": "writeDefaults", + "columnType": 0, + "isNullable": false, + "columnDefault": "'{}'", + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sheetsDefaults", + "fieldName": "sheetsDefaults", + "columnType": 0, + "isNullable": false, + "columnDefault": "'{}'", + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "slidesDefaults", + "fieldName": "slidesDefaults", + "columnType": 0, + "isNullable": false, + "columnDefault": "'{}'", + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "slidesPresenter", + "fieldName": "slidesPresenter", + "columnType": 0, + "isNullable": false, + "columnDefault": "'{}'", + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "driveDefaults", + "fieldName": "driveDefaults", + "columnType": 0, + "isNullable": false, + "columnDefault": "'{}'", + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "notificationSettings", + "fieldName": "notificationSettings", + "columnType": 0, + "isNullable": false, + "columnDefault": "'{}'", + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "aiEnabled", + "fieldName": "aiEnabled", + "columnType": 1, + "isNullable": false, + "columnDefault": "true", + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "updatedAt", + "fieldName": "updatedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "user_preferences_fk_0", + "columns": [ + "userId" + ], + "referenceTable": "user_profile", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "user_preferences_user_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "user_profile", + "dartName": "UserProfile", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "authUserId", + "fieldName": "authUserId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "displayName", + "fieldName": "displayName", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "email", + "fieldName": "email", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "avatarFileId", + "fieldName": "avatarFileId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "avatarColor", + "fieldName": "avatarColor", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "locale", + "fieldName": "locale", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "timeZone", + "fieldName": "timeZone", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lastSeenAt", + "fieldName": "lastSeenAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "welcomeContentAt", + "fieldName": "welcomeContentAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "user_profile_fk_0", + "columns": [ + "authUserId" + ], + "referenceTable": "serverpod_auth_core_user", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "user_profile_auth_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "authUserId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "user_profile_email_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "email" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "user_profile_created_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "createdAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "user_storage", + "dartName": "UserStorage", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "fieldName": "userId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "usedBytes", + "fieldName": "usedBytes", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "reservedBytes", + "fieldName": "reservedBytes", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "documentsBytes", + "fieldName": "documentsBytes", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "spreadsheetsBytes", + "fieldName": "spreadsheetsBytes", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "presentationsBytes", + "fieldName": "presentationsBytes", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "filesBytes", + "fieldName": "filesBytes", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "trashBytes", + "fieldName": "trashBytes", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "quotaBytesOverride", + "fieldName": "quotaBytesOverride", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "overQuotaSince", + "fieldName": "overQuotaSince", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "reconciledAt", + "fieldName": "reconciledAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "updatedAt", + "fieldName": "updatedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "user_storage_fk_0", + "columns": [ + "userId" + ], + "referenceTable": "user_profile", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "user_storage_user_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "write_diff_cache", + "dartName": "WriteDiffCache", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "documentId", + "fieldName": "documentId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "fromRevision", + "fieldName": "fromRevision", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "toRevision", + "fieldName": "toRevision", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "json", + "fieldName": "json", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "write_diff_cache_fk_0", + "columns": [ + "documentId" + ], + "referenceTable": "document", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "write_diff_cache_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "documentId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "fromRevision" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "toRevision" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "write_diff_cache_created_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "createdAt" + } + ], + "type": "brin", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "write_theme", + "dartName": "WriteTheme", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "uuid", + "fieldName": "uuid", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "ownerId", + "fieldName": "ownerId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "isBuiltin", + "fieldName": "isBuiltin", + "columnType": 1, + "isNullable": false, + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "builtinKey", + "fieldName": "builtinKey", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "name", + "fieldName": "name", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "description", + "fieldName": "description", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "schemaVersion", + "fieldName": "schemaVersion", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "version", + "fieldName": "version", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "json", + "fieldName": "json", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "contentHash", + "fieldName": "contentHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "updatedAt", + "fieldName": "updatedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "deletedAt", + "fieldName": "deletedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "write_theme_fk_0", + "columns": [ + "ownerId" + ], + "referenceTable": "user_profile", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "write_theme_uuid_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "uuid" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "write_theme_owner_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "ownerId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "deletedAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "write_theme_builtin_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "builtinKey" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "write_theme_revision", + "dartName": "WriteThemeRevision", + "module": "wonky", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "serial", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "themeId", + "fieldName": "themeId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "version", + "fieldName": "version", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "json", + "fieldName": "json", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "savedById", + "fieldName": "savedById", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "actorKind", + "fieldName": "actorKind", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:ActorKind" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "write_theme_revision_fk_0", + "columns": [ + "themeId" + ], + "referenceTable": "write_theme", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "write_theme_rev_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "themeId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "version" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + } + ], + "installedModules": [ + { + "__className__": "serverpod.DatabaseMigrationVersion", + "module": "serverpod", + "version": "20260824182259319" + }, + { + "__className__": "serverpod.DatabaseMigrationVersion", + "module": "serverpod_auth_idp", + "version": "20261015164015382" + }, + { + "__className__": "serverpod.DatabaseMigrationVersion", + "module": "serverpod_auth_core", + "version": "20260924105232991" + } + ], + "migrationApiVersion": 1 +} \ No newline at end of file diff --git a/wonky_server/migrations/20261025085716775-serverpod-4-1-rc1/migration.json b/wonky_server/migrations/20261025085716775-serverpod-4-1-rc1/migration.json new file mode 100644 index 00000000..8d0aecf5 --- /dev/null +++ b/wonky_server/migrations/20261025085716775-serverpod-4-1-rc1/migration.json @@ -0,0 +1,109 @@ +{ + "__className__": "serverpod.DatabaseMigration", + "actions": [ + { + "__className__": "serverpod.DatabaseMigrationAction", + "type": "createTable", + "createTable": { + "__className__": "serverpod.TableDefinition", + "name": "serverpod_auth_idp_email_account_otp_login_request", + "dartName": "EmailAccountOtpLoginRequest", + "module": "serverpod_auth_idp", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "fieldName": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "random_v7", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "fieldName": "createdAt", + "columnType": 4, + "isNullable": false, + "columnDefault": "now", + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "email", + "fieldName": "email", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "challengeId", + "fieldName": "challengeId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "completeChallengeId", + "fieldName": "completeChallengeId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "serverpod_auth_idp_email_account_otp_login_request_fk_0", + "columns": [ + "challengeId" + ], + "referenceTable": "serverpod_auth_idp_secret_challenge", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "serverpod_auth_idp_email_account_otp_login_request_fk_1", + "columns": [ + "completeChallengeId" + ], + "referenceTable": "serverpod_auth_idp_secret_challenge", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_auth_idp_email_account_otp_login_request_email", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "email" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + } + } + ], + "warnings": [], + "migrationApiVersion": 1 +} \ No newline at end of file diff --git a/wonky_server/migrations/20261025085716775-serverpod-4-1-rc1/migration.sql b/wonky_server/migrations/20261025085716775-serverpod-4-1-rc1/migration.sql new file mode 100644 index 00000000..87562e92 --- /dev/null +++ b/wonky_server/migrations/20261025085716775-serverpod-4-1-rc1/migration.sql @@ -0,0 +1,96 @@ +BEGIN; + +-- +-- Function: gen_random_uuid_v7() +-- Source: https://gist.github.com/kjmph/5bd772b2c2df145aa645b837da7eca74 +-- License: MIT (copyright notice included on the generator source code). +-- +create or replace function gen_random_uuid_v7() +returns uuid +as $$ +begin + -- use random v4 uuid as starting point (which has the same variant we need) + -- then overlay timestamp + -- then set version 7 by flipping the 2 and 1 bit in the version 4 string + return encode( + set_bit( + set_bit( + overlay(uuid_send(gen_random_uuid()) + placing substring(int8send(floor(extract(epoch from clock_timestamp()) * 1000)::bigint) from 3) + from 1 for 6 + ), + 52, 1 + ), + 53, 1 + ), + 'hex')::uuid; +end +$$ +language plpgsql +volatile; + +-- +-- ACTION CREATE TABLE +-- +CREATE TABLE "serverpod_auth_idp_email_account_otp_login_request" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid_v7(), + "createdAt" timestamp without time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, + "email" text NOT NULL, + "challengeId" uuid NOT NULL, + "completeChallengeId" uuid +); + +-- Indexes +CREATE UNIQUE INDEX "serverpod_auth_idp_email_account_otp_login_request_email" ON "serverpod_auth_idp_email_account_otp_login_request" USING btree ("email"); + +-- +-- ACTION CREATE FOREIGN KEY +-- +ALTER TABLE ONLY "serverpod_auth_idp_email_account_otp_login_request" + ADD CONSTRAINT "serverpod_auth_idp_email_account_otp_login_request_fk_0" + FOREIGN KEY("challengeId") + REFERENCES "serverpod_auth_idp_secret_challenge"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; +ALTER TABLE ONLY "serverpod_auth_idp_email_account_otp_login_request" + ADD CONSTRAINT "serverpod_auth_idp_email_account_otp_login_request_fk_1" + FOREIGN KEY("completeChallengeId") + REFERENCES "serverpod_auth_idp_secret_challenge"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; + + +-- +-- MIGRATION VERSION FOR wonky +-- +INSERT INTO "serverpod_migrations" ("module", "version", "timestamp") + VALUES ('wonky', '20261025085716775-serverpod-4-1-rc1', now()) + ON CONFLICT ("module") + DO UPDATE SET "version" = '20261025085716775-serverpod-4-1-rc1', "timestamp" = now(); + +-- +-- MIGRATION VERSION FOR serverpod +-- +INSERT INTO "serverpod_migrations" ("module", "version", "timestamp") + VALUES ('serverpod', '20260824182259319', now()) + ON CONFLICT ("module") + DO UPDATE SET "version" = '20260824182259319', "timestamp" = now(); + +-- +-- MIGRATION VERSION FOR serverpod_auth_idp +-- +INSERT INTO "serverpod_migrations" ("module", "version", "timestamp") + VALUES ('serverpod_auth_idp', '20261015164015382', now()) + ON CONFLICT ("module") + DO UPDATE SET "version" = '20261015164015382', "timestamp" = now(); + +-- +-- MIGRATION VERSION FOR serverpod_auth_core +-- +INSERT INTO "serverpod_migrations" ("module", "version", "timestamp") + VALUES ('serverpod_auth_core', '20260924105232991', now()) + ON CONFLICT ("module") + DO UPDATE SET "version" = '20260924105232991', "timestamp" = now(); + + +COMMIT; diff --git a/wonky_server/migrations/migration_registry.txt b/wonky_server/migrations/migration_registry.txt index e9a3ae48..86e0f5ac 100644 --- a/wonky_server/migrations/migration_registry.txt +++ b/wonky_server/migrations/migration_registry.txt @@ -32,3 +32,4 @@ 20261020084059086-publications 20261020133251149-usage-analytics 20261020142413171-analytics-indexes +20261025085716775-serverpod-4-1-rc1 diff --git a/wonky_server/pubspec.yaml b/wonky_server/pubspec.yaml index bdc5f7a1..6a6fea2d 100644 --- a/wonky_server/pubspec.yaml +++ b/wonky_server/pubspec.yaml @@ -18,9 +18,9 @@ dependencies: image: ^4.12.0 mcp_dart: ^2.4.2 mime: ^2.0.0 - serverpod: 4.1.0-beta.3 - serverpod_auth_idp_server: 4.1.0-beta.3 - serverpod_cloud_storage: 4.1.0-beta.3 + serverpod: 4.1.0-rc.1 + serverpod_auth_idp_server: 4.1.0-rc.1 + serverpod_cloud_storage: 4.1.0-rc.1 wonky_core: path: ../packages/wonky_core wonky_sheets_core: @@ -43,7 +43,7 @@ dev_dependencies: path: ../packages/wonky_lints meta: ^1.19.0 pointycastle: ^4.0.0 - serverpod_test: 4.1.0-beta.3 + serverpod_test: 4.1.0-rc.1 test: ^1.25.5 wonky_mcp_stdio: path: ../packages/wonky_mcp_stdio diff --git a/wonky_write/pubspec.yaml b/wonky_write/pubspec.yaml index 0a16abe8..00c60078 100644 --- a/wonky_write/pubspec.yaml +++ b/wonky_write/pubspec.yaml @@ -67,7 +67,7 @@ dev_dependencies: flutter_test_robots: ^0.0.24 # CPU profiles of the perf suite (test/perf/support/cpu_profile.dart). vm_service: ^15.0.0 - serverpod_auth_core_client: 4.1.0-beta.3 + serverpod_auth_core_client: 4.1.0-rc.1 provider: ^6.1.5+1 integration_test: sdk: flutter From de81837217e1ff633a979545a532abc1f61ce72c Mon Sep 17 00:00:00 2001 From: Viktor Lidholt Date: Sun, 25 Oct 2026 10:38:18 +0100 Subject: [PATCH 2/3] docs: name the Serverpod CLI version neutrally in the contribution steps Co-Authored-By: Claude Opus 5.5 --- CONTRIBUTING.md | 2 +- specs/16-bugs-and-improvements.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ab15fcf9..f0a609c5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -39,7 +39,7 @@ Flutter, Dart and what Serverpod needs on your machine. You also need: dart install serverpod_cli 4.1.0-rc.1 ``` -This installs the CLI, or updates an existing install to the release candidate Wonky Work +This installs the CLI, or updates an existing install to the version Wonky Work needs (the version is `VERSION` in `.github/workflows/tests.yml`). `dart install` puts the executable in Dart's install directory; add it to your `PATH` if `serverpod` is not found. diff --git a/specs/16-bugs-and-improvements.md b/specs/16-bugs-and-improvements.md index f132578b..7e941f09 100644 --- a/specs/16-bugs-and-improvements.md +++ b/specs/16-bugs-and-improvements.md @@ -1437,7 +1437,7 @@ is opened for a change nobody has seen working. The frame is added when the task 1. Prerequisites: the [Serverpod installation guide](https://docs.serverpod.dev/installation) (Flutter, Dart and what Serverpod needs), Flutter at least the CI version, and an AI coding agent (Claude Code recommended, any agent that reads `AGENTS.md` works). - 2. Install or update the Serverpod CLI to the beta Wonky Work uses: + 2. Install or update the Serverpod CLI to the version Wonky Work uses: `dart install serverpod_cli ` (the version of the CI workflow). 3. Fork the repository on GitHub (the in-app step links to `https://github.com//fork`). 4. Clone the fork and run `flutter pub get`. From 80c3434b27dfd5eed49df0e761b51abda2a5610b Mon Sep 17 00:00:00 2001 From: Viktor Lidholt Date: Sun, 25 Oct 2026 12:58:07 +0100 Subject: [PATCH 3/3] fix(sheets): wait for the sync to open before running maintenance A file opened with a pending outbox turns ready inside RevFileSync.open (when the outbox is applied), which schedules maintenance. open() then awaits the store before creating its sync core, so on a slow machine the maintenance timer fired first and read `sync.core` before it was set (LateInitializationError, flaky offline_scenarios_test #3 on CI). Maintenance now reschedules until open() has returned. Co-Authored-By: Claude Opus 5.5 --- .../features/revlog/logged_workbook_session.dart | 12 ++++++++++-- wonky_sheets/test/revlog/maintenance_test.dart | 14 ++++++++++++++ wonky_sheets/test/revlog/support/rev_harness.dart | 3 ++- 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/wonky_sheets/lib/features/revlog/logged_workbook_session.dart b/wonky_sheets/lib/features/revlog/logged_workbook_session.dart index 45d03762..b677fdc2 100644 --- a/wonky_sheets/lib/features/revlog/logged_workbook_session.dart +++ b/wonky_sheets/lib/features/revlog/logged_workbook_session.dart @@ -75,6 +75,11 @@ class LoggedWorkbookSession extends WorkbookSessionBase { ViewportHint? viewportHint; late final RevFileSync sync; + + /// Whether [RevFileSync.open] returned: its sync core exists. The outbox + /// applied while opening can make the session ready (and schedule + /// maintenance) before that. + bool _syncOpen = false; late final _SheetsPlan _plan = _SheetsPlan(this); Future _opened = Future.value(); bool _disposed = false; @@ -107,6 +112,7 @@ class LoggedWorkbookSession extends WorkbookSessionBase { sync.rejections.listen(_onRejected); sync.history.listen(_onOp); final loaded = await sync.open(); + _syncOpen = true; if (_disposed) return; // A cache reopened viewport-first turns ready through the plan (small // files once every row is loaded). @@ -193,10 +199,12 @@ class LoggedWorkbookSession extends WorkbookSessionBase { /// Writes the `WorkbookMaintenance` repairs ("Repair spreadsheet", a /// `"repair": true` op that is not undoable) when the spreadsheet is /// settled: every row loaded, no remote group half applied and this - /// tab's outbox empty (06 §1.2). Returns the plan, or null. + /// tab's outbox empty (06 §1.2), and only once the sync has opened. + /// Returns the plan, or null. MutationPlan? runMaintenance() { if (_disposed || !_isReady || role.value < Role.editor) return null; - if (!rowLoading.complete || + if (!_syncOpen || + !rowLoading.complete || _remoteGroupOpen || sync.core.pendingChanges > 0) { _scheduleMaintenance(); diff --git a/wonky_sheets/test/revlog/maintenance_test.dart b/wonky_sheets/test/revlog/maintenance_test.dart index 6d33c04e..d1ce6d60 100644 --- a/wonky_sheets/test/revlog/maintenance_test.dart +++ b/wonky_sheets/test/revlog/maintenance_test.dart @@ -71,4 +71,18 @@ void main() { expect(s.repairs, 0); expect(hub.file(file.uuid).head, head + 1); }); + + test('Given a spreadsheet created offline, when it opens with its outbox ' + 'pending and maintenance due at once, then the check waits for the ' + 'sync to open instead of failing', () async { + final local = UuidValue.fromString('0190a000-0000-7000-8000-0000000000f4'); + final a = await SheetsDevice.start(hub, anna); + a.online = false; + + final s = await a.create(local, maintenanceDelay: Duration.zero); + await Future.delayed(const Duration(milliseconds: 50)); + + expect(s.pendingChanges, greaterThan(0)); + expect(s.repairs, 0); + }); } diff --git a/wonky_sheets/test/revlog/support/rev_harness.dart b/wonky_sheets/test/revlog/support/rev_harness.dart index 011dea8c..6bd4b2b1 100644 --- a/wonky_sheets/test/revlog/support/rev_harness.dart +++ b/wonky_sheets/test/revlog/support/rev_harness.dart @@ -115,6 +115,7 @@ final class SheetsDevice { String title = 'Test', int rows = 50, int columns = 10, + Duration maintenanceDelay = const Duration(milliseconds: 20), }) async { final plan = creationPlan( file, @@ -133,7 +134,7 @@ final class SheetsDevice { summary: plan.summary.toJson(), ), ); - return openFile(file, role: Role.owner); + return openFile(file, role: Role.owner, maintenanceDelay: maintenanceDelay); } /// Opens [file] and waits until it is ready.