From 0a011d120086041e092f2d0265cea84010f8248f Mon Sep 17 00:00:00 2001 From: Leo Farias Date: Fri, 4 Sep 2026 13:15:50 -0400 Subject: [PATCH 01/12] refactor(remix_fortal): align source with preset layout --- docs/components/accordion.mdx | 2 +- docs/components/avatar.mdx | 2 +- docs/components/badge.mdx | 2 +- docs/components/button.mdx | 2 +- docs/components/callout.mdx | 2 +- docs/components/card.mdx | 2 +- docs/components/checkbox.mdx | 2 +- docs/components/checkbox_group.mdx | 2 +- docs/components/dialog.mdx | 2 +- docs/components/divider.mdx | 2 +- docs/components/icon_button.mdx | 2 +- docs/components/link.mdx | 2 +- docs/components/menu.mdx | 2 +- docs/components/progress.mdx | 2 +- docs/components/radio.mdx | 2 +- docs/components/select.mdx | 2 +- docs/components/slider.mdx | 2 +- docs/components/spinner.mdx | 2 +- docs/components/switch.mdx | 2 +- docs/components/tabs.mdx | 2 +- docs/components/textfield.mdx | 2 +- docs/components/toggle.mdx | 2 +- docs/components/toggle_group.mdx | 2 +- docs/components/tooltip.mdx | 2 +- docs/fortal/catalog.mdx | 68 +- packages/remix_fortal/lib/remix_fortal.dart | 82 +- .../{recipes => components}/accordion.dart | 2 +- .../{recipes => components}/accordion.g.dart | 0 .../src/{recipes => components}/avatar.dart | 2 +- .../src/{recipes => components}/avatar.g.dart | 0 .../src/{recipes => components}/badge.dart | 2 +- .../src/{recipes => components}/badge.g.dart | 0 .../base_button.dart} | 251 +- .../src/{recipes => components}/button.dart | 4 +- .../src/{recipes => components}/button.g.dart | 0 .../src/{recipes => components}/callout.dart | 2 +- .../{recipes => components}/callout.g.dart | 0 .../lib/src/{recipes => components}/card.dart | 2 +- .../src/{recipes => components}/card.g.dart | 0 .../src/{recipes => components}/chart.dart | 4 +- .../src/{recipes => components}/chart.g.dart | 0 .../src/{recipes => components}/checkbox.dart | 2 +- .../{recipes => components}/checkbox.g.dart | 0 .../lib/src/{recipes => components}/code.dart | 4 +- .../{recipes => components}/data_list.dart | 2 +- .../{recipes => components}/data_list.g.dart | 0 .../{recipes => components}/data_table.dart | 2 +- .../{recipes => components}/data_table.g.dart | 0 .../src/{recipes => components}/dialog.dart | 2 +- .../src/{recipes => components}/dialog.g.dart | 0 .../{recipes => components}/disclosure.dart | 2 +- .../{recipes => components}/disclosure.g.dart | 0 .../src/{recipes => components}/divider.dart | 2 +- .../{recipes => components}/divider.g.dart | 0 .../src/{recipes => components}/heading.dart | 4 +- .../{recipes => components}/icon_button.dart | 4 +- .../icon_button.g.dart | 0 .../lib/src/{recipes => components}/kbd.dart | 4 +- .../lib/src/{recipes => components}/link.dart | 4 +- .../lib/src/{recipes => components}/menu.dart | 2 +- .../src/{recipes => components}/menu.g.dart | 0 .../src/{recipes => components}/popover.dart | 2 +- .../{recipes => components}/popover.g.dart | 0 .../src/{recipes => components}/progress.dart | 2 +- .../{recipes => components}/progress.g.dart | 0 .../src/{recipes => components}/radio.dart | 2 +- .../src/{recipes => components}/radio.g.dart | 0 .../segmented_control.dart | 2 +- .../segmented_control.g.dart | 0 .../src/{recipes => components}/select.dart | 2 +- .../src/{recipes => components}/select.g.dart | 0 .../src/{recipes => components}/sidebar.dart | 2 +- .../{recipes => components}/sidebar.g.dart | 0 .../src/{recipes => components}/skeleton.dart | 2 +- .../{recipes => components}/skeleton.g.dart | 0 .../src/{recipes => components}/slider.dart | 2 +- .../src/{recipes => components}/slider.g.dart | 0 .../src/{recipes => components}/spinner.dart | 2 +- .../{recipes => components}/spinner.g.dart | 0 .../src/{recipes => components}/switch.dart | 2 +- .../src/{recipes => components}/switch.g.dart | 0 .../lib/src/{recipes => components}/tabs.dart | 2 +- .../src/{recipes => components}/tabs.g.dart | 0 .../lib/src/{recipes => components}/text.dart | 4 +- .../src/{recipes => components}/text.g.dart | 0 .../{recipes => components}/textfield.dart | 2 +- .../{recipes => components}/textfield.g.dart | 0 .../src/{recipes => components}/toggle.dart | 2 +- .../src/{recipes => components}/toggle.g.dart | 0 .../{recipes => components}/toggle_group.dart | 2 +- .../toggle_group.g.dart | 0 .../src/{recipes => components}/tooltip.dart | 2 +- .../{recipes => components}/tooltip.g.dart | 0 .../typography.dart} | 2 +- .../lib/src/fortal/base_button_recipe.dart | 326 --- .../remix_fortal/lib/src/fortal/fortal.dart | 14 - .../lib/src/fortal/fortal_theme.dart | 2217 ----------------- .../lib/src/{radix => }/icons.dart | 0 .../lib/src/radix/colors/colors.dart | 92 - .../lib/src/{fortal => theme}/computed.dart | 5 +- .../lib/src/theme/control_styles.dart | 84 + .../radix_colors.dart} | 91 +- .../src/{fortal => theme}/surface_frame.dart | 0 .../remix_fortal/lib/src/theme/theme.dart | 14 + .../lib/src/theme/theme_data.dart | 1091 ++++++++ .../lib/src/theme/theme_scope.dart | 188 ++ .../remix_fortal/lib/src/theme/tokens.dart | 943 +++++++ .../reference/radix_themes_3_3_0/README.md | 2 +- packages/remix_fortal/scripts/README.md | 4 +- .../scripts/generate_radix_colors.dart | 97 +- .../accordion/accordion_widget_test.dart | 2 +- .../tool/fortal_parity/check.dart | 14 +- tool/generate_fortal_catalog.dart | 2 +- 113 files changed, 2914 insertions(+), 2809 deletions(-) rename packages/remix_fortal/lib/src/{recipes => components}/accordion.dart (99%) rename packages/remix_fortal/lib/src/{recipes => components}/accordion.g.dart (100%) rename packages/remix_fortal/lib/src/{recipes => components}/avatar.dart (99%) rename packages/remix_fortal/lib/src/{recipes => components}/avatar.g.dart (100%) rename packages/remix_fortal/lib/src/{recipes => components}/badge.dart (99%) rename packages/remix_fortal/lib/src/{recipes => components}/badge.g.dart (100%) rename packages/remix_fortal/lib/src/{fortal/base_button_state_styles.dart => components/base_button.dart} (55%) rename packages/remix_fortal/lib/src/{recipes => components}/button.dart (98%) rename packages/remix_fortal/lib/src/{recipes => components}/button.g.dart (100%) rename packages/remix_fortal/lib/src/{recipes => components}/callout.dart (98%) rename packages/remix_fortal/lib/src/{recipes => components}/callout.g.dart (100%) rename packages/remix_fortal/lib/src/{recipes => components}/card.dart (99%) rename packages/remix_fortal/lib/src/{recipes => components}/card.g.dart (100%) rename packages/remix_fortal/lib/src/{recipes => components}/chart.dart (98%) rename packages/remix_fortal/lib/src/{recipes => components}/chart.g.dart (100%) rename packages/remix_fortal/lib/src/{recipes => components}/checkbox.dart (99%) rename packages/remix_fortal/lib/src/{recipes => components}/checkbox.g.dart (100%) rename packages/remix_fortal/lib/src/{recipes => components}/code.dart (99%) rename packages/remix_fortal/lib/src/{recipes => components}/data_list.dart (97%) rename packages/remix_fortal/lib/src/{recipes => components}/data_list.g.dart (100%) rename packages/remix_fortal/lib/src/{recipes => components}/data_table.dart (99%) rename packages/remix_fortal/lib/src/{recipes => components}/data_table.g.dart (100%) rename packages/remix_fortal/lib/src/{recipes => components}/dialog.dart (98%) rename packages/remix_fortal/lib/src/{recipes => components}/dialog.g.dart (100%) rename packages/remix_fortal/lib/src/{recipes => components}/disclosure.dart (99%) rename packages/remix_fortal/lib/src/{recipes => components}/disclosure.g.dart (100%) rename packages/remix_fortal/lib/src/{recipes => components}/divider.dart (97%) rename packages/remix_fortal/lib/src/{recipes => components}/divider.g.dart (100%) rename packages/remix_fortal/lib/src/{recipes => components}/heading.dart (98%) rename packages/remix_fortal/lib/src/{recipes => components}/icon_button.dart (98%) rename packages/remix_fortal/lib/src/{recipes => components}/icon_button.g.dart (100%) rename packages/remix_fortal/lib/src/{recipes => components}/kbd.dart (98%) rename packages/remix_fortal/lib/src/{recipes => components}/link.dart (99%) rename packages/remix_fortal/lib/src/{recipes => components}/menu.dart (99%) rename packages/remix_fortal/lib/src/{recipes => components}/menu.g.dart (100%) rename packages/remix_fortal/lib/src/{recipes => components}/popover.dart (97%) rename packages/remix_fortal/lib/src/{recipes => components}/popover.g.dart (100%) rename packages/remix_fortal/lib/src/{recipes => components}/progress.dart (99%) rename packages/remix_fortal/lib/src/{recipes => components}/progress.g.dart (100%) rename packages/remix_fortal/lib/src/{recipes => components}/radio.dart (99%) rename packages/remix_fortal/lib/src/{recipes => components}/radio.g.dart (100%) rename packages/remix_fortal/lib/src/{recipes => components}/segmented_control.dart (99%) rename packages/remix_fortal/lib/src/{recipes => components}/segmented_control.g.dart (100%) rename packages/remix_fortal/lib/src/{recipes => components}/select.dart (99%) rename packages/remix_fortal/lib/src/{recipes => components}/select.g.dart (100%) rename packages/remix_fortal/lib/src/{recipes => components}/sidebar.dart (98%) rename packages/remix_fortal/lib/src/{recipes => components}/sidebar.g.dart (100%) rename packages/remix_fortal/lib/src/{recipes => components}/skeleton.dart (95%) rename packages/remix_fortal/lib/src/{recipes => components}/skeleton.g.dart (100%) rename packages/remix_fortal/lib/src/{recipes => components}/slider.dart (99%) rename packages/remix_fortal/lib/src/{recipes => components}/slider.g.dart (100%) rename packages/remix_fortal/lib/src/{recipes => components}/spinner.dart (96%) rename packages/remix_fortal/lib/src/{recipes => components}/spinner.g.dart (100%) rename packages/remix_fortal/lib/src/{recipes => components}/switch.dart (99%) rename packages/remix_fortal/lib/src/{recipes => components}/switch.g.dart (100%) rename packages/remix_fortal/lib/src/{recipes => components}/tabs.dart (99%) rename packages/remix_fortal/lib/src/{recipes => components}/tabs.g.dart (100%) rename packages/remix_fortal/lib/src/{recipes => components}/text.dart (95%) rename packages/remix_fortal/lib/src/{recipes => components}/text.g.dart (100%) rename packages/remix_fortal/lib/src/{recipes => components}/textfield.dart (99%) rename packages/remix_fortal/lib/src/{recipes => components}/textfield.g.dart (100%) rename packages/remix_fortal/lib/src/{recipes => components}/toggle.dart (99%) rename packages/remix_fortal/lib/src/{recipes => components}/toggle.g.dart (100%) rename packages/remix_fortal/lib/src/{recipes => components}/toggle_group.dart (99%) rename packages/remix_fortal/lib/src/{recipes => components}/toggle_group.g.dart (100%) rename packages/remix_fortal/lib/src/{recipes => components}/tooltip.dart (95%) rename packages/remix_fortal/lib/src/{recipes => components}/tooltip.g.dart (100%) rename packages/remix_fortal/lib/src/{recipes/typography_shared.dart => components/typography.dart} (98%) delete mode 100644 packages/remix_fortal/lib/src/fortal/base_button_recipe.dart delete mode 100644 packages/remix_fortal/lib/src/fortal/fortal.dart delete mode 100644 packages/remix_fortal/lib/src/fortal/fortal_theme.dart rename packages/remix_fortal/lib/src/{radix => }/icons.dart (100%) delete mode 100644 packages/remix_fortal/lib/src/radix/colors/colors.dart rename packages/remix_fortal/lib/src/{fortal => theme}/computed.dart (99%) create mode 100644 packages/remix_fortal/lib/src/theme/control_styles.dart rename packages/remix_fortal/lib/src/{radix/colors/colors_generated.dart => theme/radix_colors.dart} (96%) rename packages/remix_fortal/lib/src/{fortal => theme}/surface_frame.dart (100%) create mode 100644 packages/remix_fortal/lib/src/theme/theme.dart create mode 100644 packages/remix_fortal/lib/src/theme/theme_data.dart create mode 100644 packages/remix_fortal/lib/src/theme/theme_scope.dart create mode 100644 packages/remix_fortal/lib/src/theme/tokens.dart diff --git a/docs/components/accordion.mdx b/docs/components/accordion.mdx index e3ca9bb6a..67f622921 100644 --- a/docs/components/accordion.mdx +++ b/docs/components/accordion.mdx @@ -200,7 +200,7 @@ class _FortalAccordionExampleState extends State { - See the [fortalAccordionStyle source code](https://github.com/btwld/remix/blob/main/packages/remix_fortal/lib/src/recipes/accordion.dart) for all available options. + See the [fortalAccordionStyle source code](https://github.com/btwld/remix/blob/main/packages/remix_fortal/lib/src/components/accordion.dart) for all available options. ## Constructor diff --git a/docs/components/avatar.mdx b/docs/components/avatar.mdx index a62bcfabb..fc6c42272 100644 --- a/docs/components/avatar.mdx +++ b/docs/components/avatar.mdx @@ -111,7 +111,7 @@ class FortalAvatarExample extends StatelessWidget { Set `fallbackLength: 2` for two-character labels so the generated widget uses - the pinned two-initial typography. See the [fortalAvatarStyle source code](https://github.com/btwld/remix/blob/main/packages/remix_fortal/lib/src/recipes/avatar.dart) for all available options. + the pinned two-initial typography. See the [fortalAvatarStyle source code](https://github.com/btwld/remix/blob/main/packages/remix_fortal/lib/src/components/avatar.dart) for all available options. ## Constructor diff --git a/docs/components/badge.mdx b/docs/components/badge.mdx index 65b81be51..aaa8c1eb0 100644 --- a/docs/components/badge.mdx +++ b/docs/components/badge.mdx @@ -111,7 +111,7 @@ class FortalBadgeExample extends StatelessWidget { - See the [fortalBadgeStyle source code](https://github.com/btwld/remix/blob/main/packages/remix_fortal/lib/src/recipes/badge.dart) for all available options. + See the [fortalBadgeStyle source code](https://github.com/btwld/remix/blob/main/packages/remix_fortal/lib/src/components/badge.dart) for all available options. diff --git a/docs/components/button.mdx b/docs/components/button.mdx index d8a1248f9..a8371679e 100644 --- a/docs/components/button.mdx +++ b/docs/components/button.mdx @@ -149,7 +149,7 @@ class FortalButtonExample extends StatelessWidget { - See the [fortalButtonStyle source code](https://github.com/btwld/remix/blob/main/packages/remix_fortal/lib/src/recipes/button.dart) for all available options. + See the [fortalButtonStyle source code](https://github.com/btwld/remix/blob/main/packages/remix_fortal/lib/src/components/button.dart) for all available options. ## Constructor diff --git a/docs/components/callout.mdx b/docs/components/callout.mdx index cd74b219d..3fe1c51e9 100644 --- a/docs/components/callout.mdx +++ b/docs/components/callout.mdx @@ -94,7 +94,7 @@ class FortalCalloutExample extends StatelessWidget { - See the [fortalCalloutStyle source code](https://github.com/btwld/remix/blob/main/packages/remix_fortal/lib/src/recipes/callout.dart) for all available options. + See the [fortalCalloutStyle source code](https://github.com/btwld/remix/blob/main/packages/remix_fortal/lib/src/components/callout.dart) for all available options. ## Constructor diff --git a/docs/components/card.mdx b/docs/components/card.mdx index 11ae9bc3f..f21efcc3b 100644 --- a/docs/components/card.mdx +++ b/docs/components/card.mdx @@ -74,7 +74,7 @@ class FortalCardExample extends StatelessWidget { - See the [fortalCardStyle source code](https://github.com/btwld/remix/blob/main/packages/remix_fortal/lib/src/recipes/card.dart) for all available options. + See the [fortalCardStyle source code](https://github.com/btwld/remix/blob/main/packages/remix_fortal/lib/src/components/card.dart) for all available options. ## Constructor diff --git a/docs/components/checkbox.mdx b/docs/components/checkbox.mdx index 13f7109e1..e7569ba71 100644 --- a/docs/components/checkbox.mdx +++ b/docs/components/checkbox.mdx @@ -102,7 +102,7 @@ class _FortalCheckboxExampleState extends State { - See the [fortalCheckboxStyle source code](https://github.com/btwld/remix/blob/main/packages/remix_fortal/lib/src/recipes/checkbox.dart) for all available options. + See the [fortalCheckboxStyle source code](https://github.com/btwld/remix/blob/main/packages/remix_fortal/lib/src/components/checkbox.dart) for all available options. ## Constructor diff --git a/docs/components/checkbox_group.mdx b/docs/components/checkbox_group.mdx index e6c5cf2f6..f2c3b4f31 100644 --- a/docs/components/checkbox_group.mdx +++ b/docs/components/checkbox_group.mdx @@ -305,7 +305,7 @@ class _FortalCheckboxGroupExampleState - See the [fortalCheckboxGroupItemStyle source code](https://github.com/btwld/remix/blob/main/packages/remix_fortal/lib/src/recipes/checkbox.dart) for all available options. + See the [fortalCheckboxGroupItemStyle source code](https://github.com/btwld/remix/blob/main/packages/remix_fortal/lib/src/components/checkbox.dart) for all available options. ## Controlled values diff --git a/docs/components/dialog.mdx b/docs/components/dialog.mdx index 3a784e2bc..d02b543c8 100644 --- a/docs/components/dialog.mdx +++ b/docs/components/dialog.mdx @@ -131,7 +131,7 @@ class FortalDialogExample extends StatelessWidget { - See the [fortalDialogStyle source code](https://github.com/btwld/remix/blob/main/packages/remix_fortal/lib/src/recipes/dialog.dart) for all available options. + See the [fortalDialogStyle source code](https://github.com/btwld/remix/blob/main/packages/remix_fortal/lib/src/components/dialog.dart) for all available options. ## Show function diff --git a/docs/components/divider.mdx b/docs/components/divider.mdx index d223108c3..8279ca9cc 100644 --- a/docs/components/divider.mdx +++ b/docs/components/divider.mdx @@ -67,7 +67,7 @@ class FortalDividerExample extends StatelessWidget { - See the [fortalDividerStyle source code](https://github.com/btwld/remix/blob/main/packages/remix_fortal/lib/src/recipes/divider.dart) for all available options. + See the [fortalDividerStyle source code](https://github.com/btwld/remix/blob/main/packages/remix_fortal/lib/src/components/divider.dart) for all available options. ## Constructor diff --git a/docs/components/icon_button.mdx b/docs/components/icon_button.mdx index 02f3dc198..d3ecf8b24 100644 --- a/docs/components/icon_button.mdx +++ b/docs/components/icon_button.mdx @@ -125,7 +125,7 @@ class FortalIconButtonExample extends StatelessWidget { - See the [fortalIconButtonStyle source code](https://github.com/btwld/remix/blob/main/packages/remix_fortal/lib/src/recipes/icon_button.dart) for all available options. + See the [fortalIconButtonStyle source code](https://github.com/btwld/remix/blob/main/packages/remix_fortal/lib/src/components/icon_button.dart) for all available options. ## Constructor diff --git a/docs/components/link.mdx b/docs/components/link.mdx index 74fb88a8d..d1b413ed5 100644 --- a/docs/components/link.mdx +++ b/docs/components/link.mdx @@ -141,7 +141,7 @@ class FortalLinkExample extends StatelessWidget { - See the [fortalLinkStyle source code](https://github.com/btwld/remix/blob/main/packages/remix_fortal/lib/src/recipes/link.dart) for all available options. + See the [fortalLinkStyle source code](https://github.com/btwld/remix/blob/main/packages/remix_fortal/lib/src/components/link.dart) for all available options. ## Constructor diff --git a/docs/components/menu.mdx b/docs/components/menu.mdx index d9c1b49e1..b41d2de7e 100644 --- a/docs/components/menu.mdx +++ b/docs/components/menu.mdx @@ -398,7 +398,7 @@ customization, pass `fortalMenuStyle(...).merge(customStyle)` to - See the [fortalMenuStyle source code](https://github.com/btwld/remix/blob/main/packages/remix_fortal/lib/src/recipes/menu.dart) for all available options. + See the [fortalMenuStyle source code](https://github.com/btwld/remix/blob/main/packages/remix_fortal/lib/src/components/menu.dart) for all available options. ## Constructor diff --git a/docs/components/progress.mdx b/docs/components/progress.mdx index 15111fb04..3fbd229f9 100644 --- a/docs/components/progress.mdx +++ b/docs/components/progress.mdx @@ -74,7 +74,7 @@ class FortalProgressExample extends StatelessWidget { - See the [fortalProgressStyle source code](https://github.com/btwld/remix/blob/main/packages/remix_fortal/lib/src/recipes/progress.dart) for all available options. + See the [fortalProgressStyle source code](https://github.com/btwld/remix/blob/main/packages/remix_fortal/lib/src/components/progress.dart) for all available options. ## Constructor diff --git a/docs/components/radio.mdx b/docs/components/radio.mdx index 5fe9555c4..c00b1cbc1 100644 --- a/docs/components/radio.mdx +++ b/docs/components/radio.mdx @@ -166,7 +166,7 @@ class _FortalRadioExampleState extends State { - See the [fortalRadioStyle source code](https://github.com/btwld/remix/blob/main/packages/remix_fortal/lib/src/recipes/radio.dart) for all available options. + See the [fortalRadioStyle source code](https://github.com/btwld/remix/blob/main/packages/remix_fortal/lib/src/components/radio.dart) for all available options. ## Constructor diff --git a/docs/components/select.mdx b/docs/components/select.mdx index e2600db69..3e215666a 100644 --- a/docs/components/select.mdx +++ b/docs/components/select.mdx @@ -184,7 +184,7 @@ class _FortalSelectExampleState extends State { `FortalSelect` applies matching trigger, menu container, and default item styles. Set `RemixSelectItem.style` only for a row-level override. - See the [fortalSelectStyle source code](https://github.com/btwld/remix/blob/main/packages/remix_fortal/lib/src/recipes/select.dart) for all available options. + See the [fortalSelectStyle source code](https://github.com/btwld/remix/blob/main/packages/remix_fortal/lib/src/components/select.dart) for all available options. ## Constructor diff --git a/docs/components/slider.mdx b/docs/components/slider.mdx index 0e3b4779f..0ab63746d 100644 --- a/docs/components/slider.mdx +++ b/docs/components/slider.mdx @@ -112,7 +112,7 @@ class _FortalSliderExampleState extends State { - See the [fortalSliderStyle source code](https://github.com/btwld/remix/blob/main/packages/remix_fortal/lib/src/recipes/slider.dart) for all available options. + See the [fortalSliderStyle source code](https://github.com/btwld/remix/blob/main/packages/remix_fortal/lib/src/components/slider.dart) for all available options. ## Constructor diff --git a/docs/components/spinner.mdx b/docs/components/spinner.mdx index 7998ca0e7..a102ffa96 100644 --- a/docs/components/spinner.mdx +++ b/docs/components/spinner.mdx @@ -83,7 +83,7 @@ class FortalSpinnerExample extends StatelessWidget { - See the [fortalSpinnerStyle source code](https://github.com/btwld/remix/blob/main/packages/remix_fortal/lib/src/recipes/spinner.dart) for all available options. + See the [fortalSpinnerStyle source code](https://github.com/btwld/remix/blob/main/packages/remix_fortal/lib/src/components/spinner.dart) for all available options. ## Constructor diff --git a/docs/components/switch.mdx b/docs/components/switch.mdx index a32a2a524..158c05149 100644 --- a/docs/components/switch.mdx +++ b/docs/components/switch.mdx @@ -124,7 +124,7 @@ class _FortalSwitchExampleState extends State { - See the [fortalSwitchStyle source code](https://github.com/btwld/remix/blob/main/packages/remix_fortal/lib/src/recipes/switch.dart) for all available options. + See the [fortalSwitchStyle source code](https://github.com/btwld/remix/blob/main/packages/remix_fortal/lib/src/components/switch.dart) for all available options. ## Constructor diff --git a/docs/components/tabs.mdx b/docs/components/tabs.mdx index 291621552..3f74e08b2 100644 --- a/docs/components/tabs.mdx +++ b/docs/components/tabs.mdx @@ -169,7 +169,7 @@ class _FortalTabsExampleState extends State { - See the [fortalTabStyle source code](https://github.com/btwld/remix/blob/main/packages/remix_fortal/lib/src/recipes/tabs.dart) for all available options. + See the [fortalTabStyle source code](https://github.com/btwld/remix/blob/main/packages/remix_fortal/lib/src/components/tabs.dart) for all available options. ## Constructor diff --git a/docs/components/textfield.mdx b/docs/components/textfield.mdx index a9b6f39a7..4824f2824 100644 --- a/docs/components/textfield.mdx +++ b/docs/components/textfield.mdx @@ -129,7 +129,7 @@ class _FortalTextFieldExampleState extends State { - See the [fortalTextFieldStyle source code](https://github.com/btwld/remix/blob/main/packages/remix_fortal/lib/src/recipes/textfield.dart) for all available options. + See the [fortalTextFieldStyle source code](https://github.com/btwld/remix/blob/main/packages/remix_fortal/lib/src/components/textfield.dart) for all available options. ## Constructor diff --git a/docs/components/toggle.mdx b/docs/components/toggle.mdx index e2f6e39f2..d3fe3e176 100644 --- a/docs/components/toggle.mdx +++ b/docs/components/toggle.mdx @@ -104,7 +104,7 @@ class _FortalToggleExampleState extends State { - See the [fortalToggleStyle source code](https://github.com/btwld/remix/blob/main/packages/remix_fortal/lib/src/recipes/toggle.dart) for all available options. + See the [fortalToggleStyle source code](https://github.com/btwld/remix/blob/main/packages/remix_fortal/lib/src/components/toggle.dart) for all available options. ## Constructor diff --git a/docs/components/toggle_group.mdx b/docs/components/toggle_group.mdx index 802e0b0d1..e7bb9c0ac 100644 --- a/docs/components/toggle_group.mdx +++ b/docs/components/toggle_group.mdx @@ -186,5 +186,5 @@ The recipe uses edge-to-edge items, a bordered surface container, selected accent colors, a visible keyboard focus ring, and disabled state styling. - See the [fortalToggleGroupStyle source code](https://github.com/btwld/remix/blob/main/packages/remix_fortal/lib/src/recipes/toggle_group.dart) for all available options. + See the [fortalToggleGroupStyle source code](https://github.com/btwld/remix/blob/main/packages/remix_fortal/lib/src/components/toggle_group.dart) for all available options. diff --git a/docs/components/tooltip.mdx b/docs/components/tooltip.mdx index e5540b91d..68bb22498 100644 --- a/docs/components/tooltip.mdx +++ b/docs/components/tooltip.mdx @@ -123,7 +123,7 @@ class FortalTooltipExample extends StatelessWidget { - See the [fortalTooltipStyle source code](https://github.com/btwld/remix/blob/main/packages/remix_fortal/lib/src/recipes/tooltip.dart) for all available options. + See the [fortalTooltipStyle source code](https://github.com/btwld/remix/blob/main/packages/remix_fortal/lib/src/components/tooltip.dart) for all available options. ## Constructor diff --git a/docs/fortal/catalog.mdx b/docs/fortal/catalog.mdx index db2351b69..582fedce9 100644 --- a/docs/fortal/catalog.mdx +++ b/docs/fortal/catalog.mdx @@ -56,7 +56,7 @@ A `—` value means the widget does not expose that axis. When values are listed ### FortalAccordion -Fortal extension (no Radix counterpart) · recipe `packages/remix_fortal/lib/src/recipes/accordion.dart` +Fortal extension (no Radix counterpart) · recipe `packages/remix_fortal/lib/src/components/accordion.dart` | Parameter | Values | Default | | --- | --- | --- | @@ -78,7 +78,7 @@ Fortal extension (no Radix counterpart) · recipe `packages/remix_fortal/lib/src ### FortalAvatar -Radix `Avatar` · recipe `packages/remix_fortal/lib/src/recipes/avatar.dart` +Radix `Avatar` · recipe `packages/remix_fortal/lib/src/components/avatar.dart` | Parameter | Values | Default | | --- | --- | --- | @@ -99,7 +99,7 @@ Radix `Avatar` · recipe `packages/remix_fortal/lib/src/recipes/avatar.dart` ### FortalBadge -Radix `Badge` · recipe `packages/remix_fortal/lib/src/recipes/badge.dart` +Radix `Badge` · recipe `packages/remix_fortal/lib/src/components/badge.dart` | Parameter | Values | Default | | --- | --- | --- | @@ -120,7 +120,7 @@ Radix `Badge` · recipe `packages/remix_fortal/lib/src/recipes/badge.dart` ### FortalButton -Radix `Button` · recipe `packages/remix_fortal/lib/src/recipes/button.dart` +Radix `Button` · recipe `packages/remix_fortal/lib/src/components/button.dart` | Parameter | Values | Default | | --- | --- | --- | @@ -145,7 +145,7 @@ Radix `Button` · recipe `packages/remix_fortal/lib/src/recipes/button.dart` ### FortalCallout -Radix `Callout.Root` · recipe `packages/remix_fortal/lib/src/recipes/callout.dart` +Radix `Callout.Root` · recipe `packages/remix_fortal/lib/src/components/callout.dart` | Parameter | Values | Default | | --- | --- | --- | @@ -166,7 +166,7 @@ Radix `Callout.Root` · recipe `packages/remix_fortal/lib/src/recipes/callout.da ### FortalCard -Radix `Card` · recipe `packages/remix_fortal/lib/src/recipes/card.dart` +Radix `Card` · recipe `packages/remix_fortal/lib/src/components/card.dart` | Parameter | Values | Default | | --- | --- | --- | @@ -189,7 +189,7 @@ Radix `Card` · recipe `packages/remix_fortal/lib/src/recipes/card.dart` ### FortalCheckbox -Radix `Checkbox` · recipe `packages/remix_fortal/lib/src/recipes/checkbox.dart` +Radix `Checkbox` · recipe `packages/remix_fortal/lib/src/components/checkbox.dart` | Parameter | Values | Default | | --- | --- | --- | @@ -210,7 +210,7 @@ Radix `Checkbox` · recipe `packages/remix_fortal/lib/src/recipes/checkbox.dart` ### FortalCode -Radix `Code` · recipe `packages/remix_fortal/lib/src/recipes/code.dart` +Radix `Code` · recipe `packages/remix_fortal/lib/src/components/code.dart` | Parameter | Values | Default | | --- | --- | --- | @@ -247,7 +247,7 @@ Radix `Code` · recipe `packages/remix_fortal/lib/src/recipes/code.dart` ### FortalDataList -Radix `DataList.Root` · recipe `packages/remix_fortal/lib/src/recipes/data_list.dart` +Radix `DataList.Root` · recipe `packages/remix_fortal/lib/src/components/data_list.dart` | Parameter | Values | Default | | --- | --- | --- | @@ -282,7 +282,7 @@ Radix `DataList.Root` · recipe `packages/remix_fortal/lib/src/recipes/data_list ### FortalDataTable -Radix `Table` · recipe `packages/remix_fortal/lib/src/recipes/data_table.dart` +Radix `Table` · recipe `packages/remix_fortal/lib/src/components/data_table.dart` | Parameter | Values | Default | | --- | --- | --- | @@ -311,7 +311,7 @@ Radix `Table` · recipe `packages/remix_fortal/lib/src/recipes/data_table.dart` ### FortalDialog -Radix `Dialog.Content` · recipe `packages/remix_fortal/lib/src/recipes/dialog.dart` +Radix `Dialog.Content` · recipe `packages/remix_fortal/lib/src/components/dialog.dart` | Parameter | Values | Default | | --- | --- | --- | @@ -332,7 +332,7 @@ Radix `Dialog.Content` · recipe `packages/remix_fortal/lib/src/recipes/dialog.d ### FortalDisclosure -Radix `Collapsible` · recipe `packages/remix_fortal/lib/src/recipes/disclosure.dart` +Radix `Collapsible` · recipe `packages/remix_fortal/lib/src/components/disclosure.dart` | Parameter | Values | Default | | --- | --- | --- | @@ -358,7 +358,7 @@ Radix `Collapsible` · recipe `packages/remix_fortal/lib/src/recipes/disclosure. ### FortalDivider -Radix `Separator` · recipe `packages/remix_fortal/lib/src/recipes/divider.dart` +Radix `Separator` · recipe `packages/remix_fortal/lib/src/components/divider.dart` | Parameter | Values | Default | | --- | --- | --- | @@ -377,7 +377,7 @@ Radix `Separator` · recipe `packages/remix_fortal/lib/src/recipes/divider.dart` ### FortalHeading -Radix `Heading` · recipe `packages/remix_fortal/lib/src/recipes/heading.dart` +Radix `Heading` · recipe `packages/remix_fortal/lib/src/components/heading.dart` | Parameter | Values | Default | | --- | --- | --- | @@ -408,7 +408,7 @@ Radix `Heading` · recipe `packages/remix_fortal/lib/src/recipes/heading.dart` ### FortalIconButton -Radix `IconButton` · recipe `packages/remix_fortal/lib/src/recipes/icon_button.dart` +Radix `IconButton` · recipe `packages/remix_fortal/lib/src/components/icon_button.dart` | Parameter | Values | Default | | --- | --- | --- | @@ -429,7 +429,7 @@ Radix `IconButton` · recipe `packages/remix_fortal/lib/src/recipes/icon_button. ### FortalKbd -Radix `Kbd` · recipe `packages/remix_fortal/lib/src/recipes/kbd.dart` +Radix `Kbd` · recipe `packages/remix_fortal/lib/src/components/kbd.dart` | Parameter | Values | Default | | --- | --- | --- | @@ -465,7 +465,7 @@ Radix `Kbd` · recipe `packages/remix_fortal/lib/src/recipes/kbd.dart` ### FortalLink -Radix `Link` · recipe `packages/remix_fortal/lib/src/recipes/link.dart` +Radix `Link` · recipe `packages/remix_fortal/lib/src/components/link.dart` | Parameter | Values | Default | | --- | --- | --- | @@ -511,7 +511,7 @@ Radix `Link` · recipe `packages/remix_fortal/lib/src/recipes/link.dart` ### FortalMenu -Radix `DropdownMenu` · recipe `packages/remix_fortal/lib/src/recipes/menu.dart` +Radix `DropdownMenu` · recipe `packages/remix_fortal/lib/src/components/menu.dart` | Parameter | Values | Default | | --- | --- | --- | @@ -540,7 +540,7 @@ Radix `DropdownMenu` · recipe `packages/remix_fortal/lib/src/recipes/menu.dart` ### FortalPopover -Radix `Popover.Content` · recipe `packages/remix_fortal/lib/src/recipes/popover.dart` +Radix `Popover.Content` · recipe `packages/remix_fortal/lib/src/components/popover.dart` | Parameter | Values | Default | | --- | --- | --- | @@ -556,7 +556,7 @@ Radix `Popover.Content` · recipe `packages/remix_fortal/lib/src/recipes/popover ### FortalProgress -Radix `Progress` · recipe `packages/remix_fortal/lib/src/recipes/progress.dart` +Radix `Progress` · recipe `packages/remix_fortal/lib/src/components/progress.dart` | Parameter | Values | Default | | --- | --- | --- | @@ -583,7 +583,7 @@ Radix `Progress` · recipe `packages/remix_fortal/lib/src/recipes/progress.dart` ### FortalRadio -Radix `RadioGroup.Item` · recipe `packages/remix_fortal/lib/src/recipes/radio.dart` +Radix `RadioGroup.Item` · recipe `packages/remix_fortal/lib/src/components/radio.dart` | Parameter | Values | Default | | --- | --- | --- | @@ -604,7 +604,7 @@ Radix `RadioGroup.Item` · recipe `packages/remix_fortal/lib/src/recipes/radio.d ### FortalSegmentedControl -Radix `SegmentedControl.Root` · recipe `packages/remix_fortal/lib/src/recipes/segmented_control.dart` +Radix `SegmentedControl.Root` · recipe `packages/remix_fortal/lib/src/components/segmented_control.dart` | Parameter | Values | Default | | --- | --- | --- | @@ -641,7 +641,7 @@ Radix `SegmentedControl.Root` · recipe `packages/remix_fortal/lib/src/recipes/s ### FortalSelect -Radix `Select` · recipe `packages/remix_fortal/lib/src/recipes/select.dart` +Radix `Select` · recipe `packages/remix_fortal/lib/src/components/select.dart` | Parameter | Values | Default | | --- | --- | --- | @@ -667,7 +667,7 @@ Radix `Select` · recipe `packages/remix_fortal/lib/src/recipes/select.dart` ### FortalSkeleton -Radix `Skeleton` · recipe `packages/remix_fortal/lib/src/recipes/skeleton.dart` +Radix `Skeleton` · recipe `packages/remix_fortal/lib/src/components/skeleton.dart` **Other defaults:** `loading: true` @@ -688,7 +688,7 @@ Radix `Skeleton` · recipe `packages/remix_fortal/lib/src/recipes/skeleton.dart` ### FortalSlider -Radix `Slider` · recipe `packages/remix_fortal/lib/src/recipes/slider.dart` +Radix `Slider` · recipe `packages/remix_fortal/lib/src/components/slider.dart` | Parameter | Values | Default | | --- | --- | --- | @@ -716,7 +716,7 @@ Radix `Slider` · recipe `packages/remix_fortal/lib/src/recipes/slider.dart` ### FortalSpinner -Radix `Spinner` · recipe `packages/remix_fortal/lib/src/recipes/spinner.dart` +Radix `Spinner` · recipe `packages/remix_fortal/lib/src/components/spinner.dart` | Parameter | Values | Default | | --- | --- | --- | @@ -737,7 +737,7 @@ Radix `Spinner` · recipe `packages/remix_fortal/lib/src/recipes/spinner.dart` ### FortalSwitch -Radix `Switch` · recipe `packages/remix_fortal/lib/src/recipes/switch.dart` +Radix `Switch` · recipe `packages/remix_fortal/lib/src/components/switch.dart` | Parameter | Values | Default | | --- | --- | --- | @@ -758,7 +758,7 @@ Radix `Switch` · recipe `packages/remix_fortal/lib/src/recipes/switch.dart` ### FortalTabBar -Radix `Tabs` · recipe `packages/remix_fortal/lib/src/recipes/tabs.dart` +Radix `Tabs` · recipe `packages/remix_fortal/lib/src/components/tabs.dart` | Parameter | Values | Default | | --- | --- | --- | @@ -783,7 +783,7 @@ Radix `Tabs` · recipe `packages/remix_fortal/lib/src/recipes/tabs.dart` ### FortalText -Radix `Text` · recipe `packages/remix_fortal/lib/src/recipes/text.dart` +Radix `Text` · recipe `packages/remix_fortal/lib/src/components/text.dart` | Parameter | Values | Default | | --- | --- | --- | @@ -814,7 +814,7 @@ Radix `Text` · recipe `packages/remix_fortal/lib/src/recipes/text.dart` ### FortalTextArea -Radix `TextArea` · recipe `packages/remix_fortal/lib/src/recipes/textfield.dart` +Radix `TextArea` · recipe `packages/remix_fortal/lib/src/components/textfield.dart` | Parameter | Values | Default | | --- | --- | --- | @@ -848,7 +848,7 @@ Radix `TextArea` · recipe `packages/remix_fortal/lib/src/recipes/textfield.dart ### FortalTextField -Radix `TextField.Root` · recipe `packages/remix_fortal/lib/src/recipes/textfield.dart` +Radix `TextField.Root` · recipe `packages/remix_fortal/lib/src/components/textfield.dart` | Parameter | Values | Default | | --- | --- | --- | @@ -869,7 +869,7 @@ Radix `TextField.Root` · recipe `packages/remix_fortal/lib/src/recipes/textfiel ### FortalToggle -Fortal extension (no Radix counterpart) · recipe `packages/remix_fortal/lib/src/recipes/toggle.dart` +Fortal extension (no Radix counterpart) · recipe `packages/remix_fortal/lib/src/components/toggle.dart` | Parameter | Values | Default | | --- | --- | --- | @@ -890,7 +890,7 @@ Fortal extension (no Radix counterpart) · recipe `packages/remix_fortal/lib/src ### FortalToggleGroup -Fortal extension (no Radix counterpart) · recipe `packages/remix_fortal/lib/src/recipes/toggle_group.dart` +Fortal extension (no Radix counterpart) · recipe `packages/remix_fortal/lib/src/components/toggle_group.dart` | Parameter | Values | Default | | --- | --- | --- | @@ -912,7 +912,7 @@ Fortal extension (no Radix counterpart) · recipe `packages/remix_fortal/lib/src ### FortalTooltip -Radix `Tooltip` · recipe `packages/remix_fortal/lib/src/recipes/tooltip.dart` +Radix `Tooltip` · recipe `packages/remix_fortal/lib/src/components/tooltip.dart` **Other defaults:** `arrow: true`, `collisionPadding: 10`, `delayMilliseconds: 200`, `disableHoverableContent: false`, `maxWidth: 360`, `sideOffset: 4` diff --git a/packages/remix_fortal/lib/remix_fortal.dart b/packages/remix_fortal/lib/remix_fortal.dart index 424af7c67..421cc711c 100644 --- a/packages/remix_fortal/lib/remix_fortal.dart +++ b/packages/remix_fortal/lib/remix_fortal.dart @@ -8,45 +8,51 @@ library remix_fortal; /// ICONS -export 'src/radix/icons.dart'; +export 'src/icons.dart'; /// THEME -export 'src/fortal/fortal.dart'; +export 'src/theme/radix_colors.dart'; +export 'src/theme/theme.dart'; +export 'src/components/base_button.dart' + hide + FortalBaseButtonStateStyle, + FortalBaseButtonStateStyles, + fortalBaseButtonStateStyles; /// RECIPES -export 'src/recipes/accordion.dart'; -export 'src/recipes/avatar.dart'; -export 'src/recipes/badge.dart'; -export 'src/recipes/button.dart'; -export 'src/recipes/callout.dart'; -export 'src/recipes/card.dart'; -export 'src/recipes/chart.dart'; -export 'src/recipes/checkbox.dart'; -export 'src/recipes/code.dart'; -export 'src/recipes/data_list.dart'; -export 'src/recipes/data_table.dart'; -export 'src/recipes/dialog.dart'; -export 'src/recipes/disclosure.dart'; -export 'src/recipes/divider.dart'; -export 'src/recipes/heading.dart'; -export 'src/recipes/icon_button.dart'; -export 'src/recipes/kbd.dart'; -export 'src/recipes/link.dart'; -export 'src/recipes/menu.dart'; -export 'src/recipes/popover.dart'; -export 'src/recipes/progress.dart'; -export 'src/recipes/radio.dart'; -export 'src/recipes/segmented_control.dart'; -export 'src/recipes/select.dart'; -export 'src/recipes/sidebar.dart'; -export 'src/recipes/skeleton.dart'; -export 'src/recipes/slider.dart'; -export 'src/recipes/spinner.dart'; -export 'src/recipes/switch.dart'; -export 'src/recipes/tabs.dart'; -export 'src/recipes/text.dart'; -export 'src/recipes/textfield.dart'; -export 'src/recipes/toggle.dart'; -export 'src/recipes/toggle_group.dart'; -export 'src/recipes/tooltip.dart'; -export 'src/recipes/typography_shared.dart'; +export 'src/components/accordion.dart'; +export 'src/components/avatar.dart'; +export 'src/components/badge.dart'; +export 'src/components/button.dart'; +export 'src/components/callout.dart'; +export 'src/components/card.dart'; +export 'src/components/chart.dart'; +export 'src/components/checkbox.dart'; +export 'src/components/code.dart'; +export 'src/components/data_list.dart'; +export 'src/components/data_table.dart'; +export 'src/components/dialog.dart'; +export 'src/components/disclosure.dart'; +export 'src/components/divider.dart'; +export 'src/components/heading.dart'; +export 'src/components/icon_button.dart'; +export 'src/components/kbd.dart'; +export 'src/components/link.dart'; +export 'src/components/menu.dart'; +export 'src/components/popover.dart'; +export 'src/components/progress.dart'; +export 'src/components/radio.dart'; +export 'src/components/segmented_control.dart'; +export 'src/components/select.dart'; +export 'src/components/sidebar.dart'; +export 'src/components/skeleton.dart'; +export 'src/components/slider.dart'; +export 'src/components/spinner.dart'; +export 'src/components/switch.dart'; +export 'src/components/tabs.dart'; +export 'src/components/text.dart'; +export 'src/components/textfield.dart'; +export 'src/components/toggle.dart'; +export 'src/components/toggle_group.dart'; +export 'src/components/tooltip.dart'; +export 'src/components/typography.dart'; diff --git a/packages/remix_fortal/lib/src/recipes/accordion.dart b/packages/remix_fortal/lib/src/components/accordion.dart similarity index 99% rename from packages/remix_fortal/lib/src/recipes/accordion.dart rename to packages/remix_fortal/lib/src/components/accordion.dart index 7441903af..e2c1d8634 100644 --- a/packages/remix_fortal/lib/src/recipes/accordion.dart +++ b/packages/remix_fortal/lib/src/components/accordion.dart @@ -3,7 +3,7 @@ import 'package:mix_annotations/mix_annotations.dart'; import 'package:naked_ui/naked_ui.dart'; import 'package:remix/remix.dart'; -import '../fortal/fortal.dart'; +import '../theme/theme.dart'; part 'accordion.g.dart'; diff --git a/packages/remix_fortal/lib/src/recipes/accordion.g.dart b/packages/remix_fortal/lib/src/components/accordion.g.dart similarity index 100% rename from packages/remix_fortal/lib/src/recipes/accordion.g.dart rename to packages/remix_fortal/lib/src/components/accordion.g.dart diff --git a/packages/remix_fortal/lib/src/recipes/avatar.dart b/packages/remix_fortal/lib/src/components/avatar.dart similarity index 99% rename from packages/remix_fortal/lib/src/recipes/avatar.dart rename to packages/remix_fortal/lib/src/components/avatar.dart index 30e72ec71..b0765210e 100644 --- a/packages/remix_fortal/lib/src/recipes/avatar.dart +++ b/packages/remix_fortal/lib/src/components/avatar.dart @@ -2,7 +2,7 @@ import 'package:flutter/widgets.dart'; import 'package:mix_annotations/mix_annotations.dart'; import 'package:remix/remix.dart'; -import '../fortal/fortal.dart'; +import '../theme/theme.dart'; part 'avatar.g.dart'; diff --git a/packages/remix_fortal/lib/src/recipes/avatar.g.dart b/packages/remix_fortal/lib/src/components/avatar.g.dart similarity index 100% rename from packages/remix_fortal/lib/src/recipes/avatar.g.dart rename to packages/remix_fortal/lib/src/components/avatar.g.dart diff --git a/packages/remix_fortal/lib/src/recipes/badge.dart b/packages/remix_fortal/lib/src/components/badge.dart similarity index 99% rename from packages/remix_fortal/lib/src/recipes/badge.dart rename to packages/remix_fortal/lib/src/components/badge.dart index c5d616b5a..15c1e018f 100644 --- a/packages/remix_fortal/lib/src/recipes/badge.dart +++ b/packages/remix_fortal/lib/src/components/badge.dart @@ -2,7 +2,7 @@ import 'package:flutter/widgets.dart'; import 'package:mix_annotations/mix_annotations.dart'; import 'package:remix/remix.dart'; -import '../fortal/fortal.dart'; +import '../theme/theme.dart'; part 'badge.g.dart'; diff --git a/packages/remix_fortal/lib/src/recipes/badge.g.dart b/packages/remix_fortal/lib/src/components/badge.g.dart similarity index 100% rename from packages/remix_fortal/lib/src/recipes/badge.g.dart rename to packages/remix_fortal/lib/src/components/badge.g.dart diff --git a/packages/remix_fortal/lib/src/fortal/base_button_state_styles.dart b/packages/remix_fortal/lib/src/components/base_button.dart similarity index 55% rename from packages/remix_fortal/lib/src/fortal/base_button_state_styles.dart rename to packages/remix_fortal/lib/src/components/base_button.dart index 087f398db..60bce668b 100644 --- a/packages/remix_fortal/lib/src/fortal/base_button_state_styles.dart +++ b/packages/remix_fortal/lib/src/components/base_button.dart @@ -1,8 +1,253 @@ import 'package:flutter/widgets.dart'; -import 'package:remix/remix.dart'; +import 'package:mix/mix.dart'; +import 'package:remix/remix.dart' + show + RemixBoxEffectLayerMix, + RemixBoxEffectsMix, + RemixCssColorFilterOperation, + RemixLinearGradientMix, + RemixOrderedColorFilterModifier; -import 'base_button_recipe.dart'; -import 'fortal_theme.dart'; +import '../theme/theme.dart'; + +/// The Radix BaseButton size scale shared by Button and IconButton. +/// +/// Deliberate: this mirrors [FortalBaseButtonVariant]. Button and IconButton +/// each own a size enum, so the shared metrics need a common type — passing the +/// raw `size.index + 1` instead would drop every switch below to a wildcard and +/// defer an unknown size to a runtime throw. +enum FortalBaseButtonSize { size1, size2, size3, size4 } + +/// Shared Radix BaseButton metrics used by Button and IconButton recipes. +({ + double height, + double paddingX, + double gap, + Radius radius, + TextStyleToken text, + double spinnerSize, +}) +fortalBaseButtonMetrics(FortalBaseButtonSize size) => switch (size) { + .size1 => ( + height: FortalTokens.space5(), + paddingX: FortalTokens.space2(), + gap: FortalTokens.space1(), + radius: FortalTokens.radius1OrFull(), + text: FortalTokens.text1, + spinnerSize: FortalTokens.space3(), + ), + .size2 => ( + height: FortalTokens.space6(), + paddingX: FortalTokens.space3(), + gap: FortalTokens.space2(), + radius: FortalTokens.radius2OrFull(), + text: FortalTokens.text2, + spinnerSize: FortalTokens.space4(), + ), + .size3 => ( + height: FortalTokens.space7(), + paddingX: FortalTokens.space4(), + gap: FortalTokens.space3(), + radius: FortalTokens.radius3OrFull(), + text: FortalTokens.text3, + spinnerSize: FortalTokens.space4(), + ), + .size4 => ( + height: FortalTokens.space8(), + paddingX: FortalTokens.space5(), + gap: FortalTokens.space3(), + radius: FortalTokens.radius4OrFull(), + text: FortalTokens.text4, + spinnerSize: FortalTokens.spinnerSize3(), + ), +}; + +/// Content-box metrics for the ghost BaseButton variant. +({double paddingX, double paddingY, double marginX, double marginY, double gap}) +fortalBaseButtonGhostMetrics(FortalBaseButtonSize size) => switch (size) { + .size1 => ( + paddingX: FortalTokens.space2(), + paddingY: FortalTokens.space1(), + marginX: FortalTokens.baseButtonGhostMarginX12(), + marginY: FortalTokens.baseButtonGhostMarginY12(), + gap: FortalTokens.space1(), + ), + .size2 => ( + paddingX: FortalTokens.space2(), + paddingY: FortalTokens.space1(), + marginX: FortalTokens.baseButtonGhostMarginX12(), + marginY: FortalTokens.baseButtonGhostMarginY12(), + gap: FortalTokens.space1(), + ), + .size3 => ( + paddingX: FortalTokens.space3(), + paddingY: FortalTokens.baseButtonGhostPaddingY3(), + marginX: FortalTokens.baseButtonGhostMarginX3(), + marginY: FortalTokens.baseButtonGhostMarginY3(), + gap: FortalTokens.space2(), + ), + .size4 => ( + paddingX: FortalTokens.space4(), + paddingY: FortalTokens.space2(), + marginX: FortalTokens.baseButtonGhostMarginX4(), + marginY: FortalTokens.baseButtonGhostMarginY4(), + gap: FortalTokens.space2(), + ), +}; + +/// Content-box metrics for the ghost IconButton variant. +({double padding, double margin}) fortalIconButtonGhostMetrics( + FortalBaseButtonSize size, +) => switch (size) { + .size1 => ( + padding: FortalTokens.space1(), + margin: FortalTokens.iconButtonGhostMargin1(), + ), + .size2 => ( + padding: FortalTokens.iconButtonGhostPadding2(), + margin: FortalTokens.iconButtonGhostMargin2(), + ), + .size3 => ( + padding: FortalTokens.space2(), + margin: FortalTokens.iconButtonGhostMargin3(), + ), + .size4 => ( + padding: FortalTokens.space3(), + margin: FortalTokens.iconButtonGhostMargin4(), + ), +}; + +/// Resolves a mode-aware ordered CSS filter at the component build context. +WidgetModifierConfig fortalModeAwareFilter({ + required List light, + required List dark, +}) => WidgetModifierConfig.modifier( + _FortalModeAwareFilterMix(light: light, dark: dark), +); + +/// Explicit identity filter used to clear a higher-priority state filter. +WidgetModifierConfig fortalClearFilter() => + fortalModeAwareFilter(light: const [], dark: const []); + +/// Exact classic BaseButton surface for a visual state. +RemixBoxEffectLayerMix fortalClassicBaseButtonSurface({ + required bool highContrast, + bool hovered = false, + bool pressed = false, + bool disabled = false, +}) { + final inset = FortalTokens.baseButtonClassicAfterInset(); + if (disabled) { + return RemixBoxEffectLayerMix( + gradients: [ + RemixLinearGradientMix( + colors: [ + FortalTokens.blackA1(), + const Color(0x00000000), + FortalTokens.whiteA1(), + ], + stops: const [-0.2, 0.4, 1], + ), + RemixLinearGradientMix( + colors: [FortalTokens.grayA2(), FortalTokens.grayA2()], + ), + ], + gradientInsets: [inset, inset], + shadowToken: FortalTokens.baseButtonClassicDisabledShadows, + ); + } + + final baseColor = highContrast + ? FortalTokens.accent12() + : FortalTokens.accent9(); + final afterColor = hovered && !highContrast + ? FortalTokens.accent10() + : baseColor; + final pseudoGradient = RemixLinearGradientMix( + colors: [ + highContrast + ? hovered || pressed + ? FortalTokens.blackA5() + : FortalTokens.blackA3() + : hovered + ? FortalTokens.blackA2() + : pressed + ? FortalTokens.blackA2() + : FortalTokens.blackA1(), + const Color(0x00000000), + highContrast + ? pressed + ? FortalTokens.whiteA3() + : FortalTokens.whiteA2() + : hovered || pressed + ? FortalTokens.whiteA3() + : FortalTokens.whiteA2(), + ], + stops: hovered && !highContrast + ? const [-0.15, 0.425, 1] + : const [0, 0.5, 1], + ); + final gradients = [ + pseudoGradient, + RemixLinearGradientMix(colors: [afterColor, afterColor]), + if (pressed) + RemixLinearGradientMix( + colors: [FortalTokens.blackA1(), const Color(0x00000000)], + ) + else ...[ + RemixLinearGradientMix( + colors: [ + const Color(0x00000000), + const Color(0x00000000), + FortalTokens.grayA4(), + FortalTokens.grayA4(), + ], + stops: const [0, 0.5, 0.5, 1], + ), + RemixLinearGradientMix( + colors: [ + const Color(0x00000000), + const Color(0x00000000), + baseColor, + baseColor, + ], + stops: const [0, 0.5, 0.8, 1], + ), + ], + ]; + return RemixBoxEffectLayerMix( + gradients: gradients, + gradientInsets: [inset, inset, ...List.filled(gradients.length - 2, 0)], + shadowToken: pressed + ? highContrast + ? FortalTokens.baseButtonClassicActiveHighContrastShadows + : FortalTokens.baseButtonClassicActiveShadows + : highContrast + ? FortalTokens.baseButtonClassicHighContrastShadows + : FortalTokens.baseButtonClassicShadows, + ); +} + +final class _FortalModeAwareFilterMix + extends ModifierMix { + const _FortalModeAwareFilterMix({required this.light, required this.dark}); + + final List light; + final List dark; + + @override + RemixOrderedColorFilterModifier resolve(BuildContext context) => + RemixOrderedColorFilterModifier( + FortalTheme.of(context).isDark ? dark : light, + ); + + @override + _FortalModeAwareFilterMix merge(_FortalModeAwareFilterMix? other) => + other ?? this; + + @override + List get props => [light, dark]; +} /// Shared Radix BaseButton variants implemented by Button and IconButton. enum FortalBaseButtonVariant { classic, solid, soft, surface, outline, ghost } diff --git a/packages/remix_fortal/lib/src/recipes/button.dart b/packages/remix_fortal/lib/src/components/button.dart similarity index 98% rename from packages/remix_fortal/lib/src/recipes/button.dart rename to packages/remix_fortal/lib/src/components/button.dart index e1f9adcd2..960038bc3 100644 --- a/packages/remix_fortal/lib/src/recipes/button.dart +++ b/packages/remix_fortal/lib/src/components/button.dart @@ -2,8 +2,8 @@ import 'package:flutter/widgets.dart'; import 'package:mix_annotations/mix_annotations.dart'; import 'package:remix/remix.dart'; -import '../fortal/base_button_state_styles.dart'; -import '../fortal/fortal.dart'; +import 'base_button.dart'; +import '../theme/theme.dart'; part 'button.g.dart'; diff --git a/packages/remix_fortal/lib/src/recipes/button.g.dart b/packages/remix_fortal/lib/src/components/button.g.dart similarity index 100% rename from packages/remix_fortal/lib/src/recipes/button.g.dart rename to packages/remix_fortal/lib/src/components/button.g.dart diff --git a/packages/remix_fortal/lib/src/recipes/callout.dart b/packages/remix_fortal/lib/src/components/callout.dart similarity index 98% rename from packages/remix_fortal/lib/src/recipes/callout.dart rename to packages/remix_fortal/lib/src/components/callout.dart index 231a9f789..86a2afe15 100644 --- a/packages/remix_fortal/lib/src/recipes/callout.dart +++ b/packages/remix_fortal/lib/src/components/callout.dart @@ -2,7 +2,7 @@ import 'package:flutter/widgets.dart'; import 'package:mix_annotations/mix_annotations.dart'; import 'package:remix/remix.dart'; -import '../fortal/fortal.dart'; +import '../theme/theme.dart'; part 'callout.g.dart'; diff --git a/packages/remix_fortal/lib/src/recipes/callout.g.dart b/packages/remix_fortal/lib/src/components/callout.g.dart similarity index 100% rename from packages/remix_fortal/lib/src/recipes/callout.g.dart rename to packages/remix_fortal/lib/src/components/callout.g.dart diff --git a/packages/remix_fortal/lib/src/recipes/card.dart b/packages/remix_fortal/lib/src/components/card.dart similarity index 99% rename from packages/remix_fortal/lib/src/recipes/card.dart rename to packages/remix_fortal/lib/src/components/card.dart index 8ab102320..f63a48719 100644 --- a/packages/remix_fortal/lib/src/recipes/card.dart +++ b/packages/remix_fortal/lib/src/components/card.dart @@ -2,7 +2,7 @@ import 'package:flutter/widgets.dart'; import 'package:mix_annotations/mix_annotations.dart'; import 'package:remix/remix.dart'; -import '../fortal/fortal.dart'; +import '../theme/theme.dart'; part 'card.g.dart'; diff --git a/packages/remix_fortal/lib/src/recipes/card.g.dart b/packages/remix_fortal/lib/src/components/card.g.dart similarity index 100% rename from packages/remix_fortal/lib/src/recipes/card.g.dart rename to packages/remix_fortal/lib/src/components/card.g.dart diff --git a/packages/remix_fortal/lib/src/recipes/chart.dart b/packages/remix_fortal/lib/src/components/chart.dart similarity index 98% rename from packages/remix_fortal/lib/src/recipes/chart.dart rename to packages/remix_fortal/lib/src/components/chart.dart index ec2c9b762..e160d910a 100644 --- a/packages/remix_fortal/lib/src/recipes/chart.dart +++ b/packages/remix_fortal/lib/src/components/chart.dart @@ -4,7 +4,9 @@ import 'package:mix_annotations/mix_annotations.dart'; import 'package:mix_chart/mix_chart.dart'; import 'package:remix/remix.dart'; -import '../fortal/fortal.dart'; +import '../theme/radix_colors.dart' + show amber, blue, cyan, green, orange, ruby, violet; +import '../theme/theme.dart'; part 'chart.g.dart'; diff --git a/packages/remix_fortal/lib/src/recipes/chart.g.dart b/packages/remix_fortal/lib/src/components/chart.g.dart similarity index 100% rename from packages/remix_fortal/lib/src/recipes/chart.g.dart rename to packages/remix_fortal/lib/src/components/chart.g.dart diff --git a/packages/remix_fortal/lib/src/recipes/checkbox.dart b/packages/remix_fortal/lib/src/components/checkbox.dart similarity index 99% rename from packages/remix_fortal/lib/src/recipes/checkbox.dart rename to packages/remix_fortal/lib/src/components/checkbox.dart index fc6252127..ad439911b 100644 --- a/packages/remix_fortal/lib/src/recipes/checkbox.dart +++ b/packages/remix_fortal/lib/src/components/checkbox.dart @@ -2,7 +2,7 @@ import 'package:flutter/widgets.dart'; import 'package:mix_annotations/mix_annotations.dart'; import 'package:remix/remix.dart'; -import '../fortal/fortal.dart'; +import '../theme/theme.dart'; part 'checkbox.g.dart'; diff --git a/packages/remix_fortal/lib/src/recipes/checkbox.g.dart b/packages/remix_fortal/lib/src/components/checkbox.g.dart similarity index 100% rename from packages/remix_fortal/lib/src/recipes/checkbox.g.dart rename to packages/remix_fortal/lib/src/components/checkbox.g.dart diff --git a/packages/remix_fortal/lib/src/recipes/code.dart b/packages/remix_fortal/lib/src/components/code.dart similarity index 99% rename from packages/remix_fortal/lib/src/recipes/code.dart rename to packages/remix_fortal/lib/src/components/code.dart index 9385c582f..950caef3a 100644 --- a/packages/remix_fortal/lib/src/recipes/code.dart +++ b/packages/remix_fortal/lib/src/components/code.dart @@ -3,8 +3,8 @@ import 'dart:math' as math; import 'package:flutter/widgets.dart'; import 'package:remix/remix.dart'; -import '../fortal/fortal.dart'; -import 'typography_shared.dart'; +import '../theme/theme.dart'; +import 'typography.dart'; /// Radix Themes Code variants. enum FortalCodeVariant { solid, soft, outline, ghost } diff --git a/packages/remix_fortal/lib/src/recipes/data_list.dart b/packages/remix_fortal/lib/src/components/data_list.dart similarity index 97% rename from packages/remix_fortal/lib/src/recipes/data_list.dart rename to packages/remix_fortal/lib/src/components/data_list.dart index 166c71eb7..5c08bbfe0 100644 --- a/packages/remix_fortal/lib/src/recipes/data_list.dart +++ b/packages/remix_fortal/lib/src/components/data_list.dart @@ -2,7 +2,7 @@ import 'package:flutter/widgets.dart'; import 'package:mix_annotations/mix_annotations.dart'; import 'package:remix/remix.dart'; -import '../fortal/fortal.dart'; +import '../theme/theme.dart'; part 'data_list.g.dart'; diff --git a/packages/remix_fortal/lib/src/recipes/data_list.g.dart b/packages/remix_fortal/lib/src/components/data_list.g.dart similarity index 100% rename from packages/remix_fortal/lib/src/recipes/data_list.g.dart rename to packages/remix_fortal/lib/src/components/data_list.g.dart diff --git a/packages/remix_fortal/lib/src/recipes/data_table.dart b/packages/remix_fortal/lib/src/components/data_table.dart similarity index 99% rename from packages/remix_fortal/lib/src/recipes/data_table.dart rename to packages/remix_fortal/lib/src/components/data_table.dart index 0d6c6debd..d6064d520 100644 --- a/packages/remix_fortal/lib/src/recipes/data_table.dart +++ b/packages/remix_fortal/lib/src/components/data_table.dart @@ -2,7 +2,7 @@ import 'package:flutter/widgets.dart'; import 'package:mix_annotations/mix_annotations.dart'; import 'package:remix/remix.dart'; -import '../fortal/fortal.dart'; +import '../theme/theme.dart'; import 'checkbox.dart'; import 'icon_button.dart'; import 'select.dart'; diff --git a/packages/remix_fortal/lib/src/recipes/data_table.g.dart b/packages/remix_fortal/lib/src/components/data_table.g.dart similarity index 100% rename from packages/remix_fortal/lib/src/recipes/data_table.g.dart rename to packages/remix_fortal/lib/src/components/data_table.g.dart diff --git a/packages/remix_fortal/lib/src/recipes/dialog.dart b/packages/remix_fortal/lib/src/components/dialog.dart similarity index 98% rename from packages/remix_fortal/lib/src/recipes/dialog.dart rename to packages/remix_fortal/lib/src/components/dialog.dart index ba72bb8f4..ef4ee1aeb 100644 --- a/packages/remix_fortal/lib/src/recipes/dialog.dart +++ b/packages/remix_fortal/lib/src/components/dialog.dart @@ -4,7 +4,7 @@ import 'package:flutter/widgets.dart'; import 'package:mix_annotations/mix_annotations.dart'; import 'package:remix/remix.dart'; -import '../fortal/fortal.dart'; +import '../theme/theme.dart'; part 'dialog.g.dart'; diff --git a/packages/remix_fortal/lib/src/recipes/dialog.g.dart b/packages/remix_fortal/lib/src/components/dialog.g.dart similarity index 100% rename from packages/remix_fortal/lib/src/recipes/dialog.g.dart rename to packages/remix_fortal/lib/src/components/dialog.g.dart diff --git a/packages/remix_fortal/lib/src/recipes/disclosure.dart b/packages/remix_fortal/lib/src/components/disclosure.dart similarity index 99% rename from packages/remix_fortal/lib/src/recipes/disclosure.dart rename to packages/remix_fortal/lib/src/components/disclosure.dart index 9fb5accce..b14c8d392 100644 --- a/packages/remix_fortal/lib/src/recipes/disclosure.dart +++ b/packages/remix_fortal/lib/src/components/disclosure.dart @@ -3,7 +3,7 @@ import 'package:mix_annotations/mix_annotations.dart'; import 'package:naked_ui/naked_ui.dart'; import 'package:remix/remix.dart'; -import '../fortal/fortal.dart'; +import '../theme/theme.dart'; part 'disclosure.g.dart'; diff --git a/packages/remix_fortal/lib/src/recipes/disclosure.g.dart b/packages/remix_fortal/lib/src/components/disclosure.g.dart similarity index 100% rename from packages/remix_fortal/lib/src/recipes/disclosure.g.dart rename to packages/remix_fortal/lib/src/components/disclosure.g.dart diff --git a/packages/remix_fortal/lib/src/recipes/divider.dart b/packages/remix_fortal/lib/src/components/divider.dart similarity index 97% rename from packages/remix_fortal/lib/src/recipes/divider.dart rename to packages/remix_fortal/lib/src/components/divider.dart index c5ad1edbb..d647c490b 100644 --- a/packages/remix_fortal/lib/src/recipes/divider.dart +++ b/packages/remix_fortal/lib/src/components/divider.dart @@ -2,7 +2,7 @@ import 'package:flutter/widgets.dart'; import 'package:mix_annotations/mix_annotations.dart'; import 'package:remix/remix.dart'; -import '../fortal/fortal.dart'; +import '../theme/theme.dart'; part 'divider.g.dart'; diff --git a/packages/remix_fortal/lib/src/recipes/divider.g.dart b/packages/remix_fortal/lib/src/components/divider.g.dart similarity index 100% rename from packages/remix_fortal/lib/src/recipes/divider.g.dart rename to packages/remix_fortal/lib/src/components/divider.g.dart diff --git a/packages/remix_fortal/lib/src/recipes/heading.dart b/packages/remix_fortal/lib/src/components/heading.dart similarity index 98% rename from packages/remix_fortal/lib/src/recipes/heading.dart rename to packages/remix_fortal/lib/src/components/heading.dart index 84bc2ee93..982fadcb3 100644 --- a/packages/remix_fortal/lib/src/recipes/heading.dart +++ b/packages/remix_fortal/lib/src/components/heading.dart @@ -1,8 +1,8 @@ import 'package:flutter/widgets.dart'; import 'package:remix/remix.dart'; -import '../fortal/fortal.dart'; -import 'typography_shared.dart'; +import '../theme/theme.dart'; +import 'typography.dart'; /// Fortal-themed heading style on the Radix nine-step scale. /// diff --git a/packages/remix_fortal/lib/src/recipes/icon_button.dart b/packages/remix_fortal/lib/src/components/icon_button.dart similarity index 98% rename from packages/remix_fortal/lib/src/recipes/icon_button.dart rename to packages/remix_fortal/lib/src/components/icon_button.dart index 074b8eb4f..0ae769738 100644 --- a/packages/remix_fortal/lib/src/recipes/icon_button.dart +++ b/packages/remix_fortal/lib/src/components/icon_button.dart @@ -2,8 +2,8 @@ import 'package:flutter/widgets.dart'; import 'package:mix_annotations/mix_annotations.dart'; import 'package:remix/remix.dart'; -import '../fortal/base_button_state_styles.dart'; -import '../fortal/fortal.dart'; +import 'base_button.dart'; +import '../theme/theme.dart'; part 'icon_button.g.dart'; diff --git a/packages/remix_fortal/lib/src/recipes/icon_button.g.dart b/packages/remix_fortal/lib/src/components/icon_button.g.dart similarity index 100% rename from packages/remix_fortal/lib/src/recipes/icon_button.g.dart rename to packages/remix_fortal/lib/src/components/icon_button.g.dart diff --git a/packages/remix_fortal/lib/src/recipes/kbd.dart b/packages/remix_fortal/lib/src/components/kbd.dart similarity index 98% rename from packages/remix_fortal/lib/src/recipes/kbd.dart rename to packages/remix_fortal/lib/src/components/kbd.dart index 15b974e43..a93563a86 100644 --- a/packages/remix_fortal/lib/src/recipes/kbd.dart +++ b/packages/remix_fortal/lib/src/components/kbd.dart @@ -1,8 +1,8 @@ import 'package:flutter/widgets.dart'; import 'package:remix/remix.dart'; -import '../fortal/fortal.dart'; -import 'typography_shared.dart'; +import '../theme/theme.dart'; +import 'typography.dart'; /// Radix Themes Kbd variants. enum FortalKbdVariant { classic, soft } diff --git a/packages/remix_fortal/lib/src/recipes/link.dart b/packages/remix_fortal/lib/src/components/link.dart similarity index 99% rename from packages/remix_fortal/lib/src/recipes/link.dart rename to packages/remix_fortal/lib/src/components/link.dart index 9e95bac2a..04f707492 100644 --- a/packages/remix_fortal/lib/src/recipes/link.dart +++ b/packages/remix_fortal/lib/src/components/link.dart @@ -3,8 +3,8 @@ import 'dart:math' as math; import 'package:flutter/widgets.dart'; import 'package:remix/remix.dart'; -import '../fortal/fortal.dart'; -import 'typography_shared.dart'; +import '../theme/theme.dart'; +import 'typography.dart'; /// Underline visibility for [FortalLink]. enum FortalLinkUnderline { auto, always, hover, none } diff --git a/packages/remix_fortal/lib/src/recipes/menu.dart b/packages/remix_fortal/lib/src/components/menu.dart similarity index 99% rename from packages/remix_fortal/lib/src/recipes/menu.dart rename to packages/remix_fortal/lib/src/components/menu.dart index 55aa7a97a..ee4f50f09 100644 --- a/packages/remix_fortal/lib/src/recipes/menu.dart +++ b/packages/remix_fortal/lib/src/components/menu.dart @@ -2,7 +2,7 @@ import 'package:flutter/widgets.dart'; import 'package:mix_annotations/mix_annotations.dart'; import 'package:remix/remix.dart'; -import '../fortal/fortal.dart'; +import '../theme/theme.dart'; part 'menu.g.dart'; diff --git a/packages/remix_fortal/lib/src/recipes/menu.g.dart b/packages/remix_fortal/lib/src/components/menu.g.dart similarity index 100% rename from packages/remix_fortal/lib/src/recipes/menu.g.dart rename to packages/remix_fortal/lib/src/components/menu.g.dart diff --git a/packages/remix_fortal/lib/src/recipes/popover.dart b/packages/remix_fortal/lib/src/components/popover.dart similarity index 97% rename from packages/remix_fortal/lib/src/recipes/popover.dart rename to packages/remix_fortal/lib/src/components/popover.dart index 4e5e4ebf2..d93968659 100644 --- a/packages/remix_fortal/lib/src/recipes/popover.dart +++ b/packages/remix_fortal/lib/src/components/popover.dart @@ -2,7 +2,7 @@ import 'package:flutter/widgets.dart'; import 'package:mix_annotations/mix_annotations.dart'; import 'package:remix/remix.dart'; -import '../fortal/fortal.dart'; +import '../theme/theme.dart'; part 'popover.g.dart'; diff --git a/packages/remix_fortal/lib/src/recipes/popover.g.dart b/packages/remix_fortal/lib/src/components/popover.g.dart similarity index 100% rename from packages/remix_fortal/lib/src/recipes/popover.g.dart rename to packages/remix_fortal/lib/src/components/popover.g.dart diff --git a/packages/remix_fortal/lib/src/recipes/progress.dart b/packages/remix_fortal/lib/src/components/progress.dart similarity index 99% rename from packages/remix_fortal/lib/src/recipes/progress.dart rename to packages/remix_fortal/lib/src/components/progress.dart index 82d848612..88efd7007 100644 --- a/packages/remix_fortal/lib/src/recipes/progress.dart +++ b/packages/remix_fortal/lib/src/components/progress.dart @@ -2,7 +2,7 @@ import 'package:flutter/widgets.dart'; import 'package:mix_annotations/mix_annotations.dart'; import 'package:remix/remix.dart'; -import '../fortal/fortal.dart'; +import '../theme/theme.dart'; part 'progress.g.dart'; diff --git a/packages/remix_fortal/lib/src/recipes/progress.g.dart b/packages/remix_fortal/lib/src/components/progress.g.dart similarity index 100% rename from packages/remix_fortal/lib/src/recipes/progress.g.dart rename to packages/remix_fortal/lib/src/components/progress.g.dart diff --git a/packages/remix_fortal/lib/src/recipes/radio.dart b/packages/remix_fortal/lib/src/components/radio.dart similarity index 99% rename from packages/remix_fortal/lib/src/recipes/radio.dart rename to packages/remix_fortal/lib/src/components/radio.dart index b48ab813f..bdeb6d9a3 100644 --- a/packages/remix_fortal/lib/src/recipes/radio.dart +++ b/packages/remix_fortal/lib/src/components/radio.dart @@ -2,7 +2,7 @@ import 'package:flutter/widgets.dart'; import 'package:mix_annotations/mix_annotations.dart'; import 'package:remix/remix.dart'; -import '../fortal/fortal.dart'; +import '../theme/theme.dart'; part 'radio.g.dart'; diff --git a/packages/remix_fortal/lib/src/recipes/radio.g.dart b/packages/remix_fortal/lib/src/components/radio.g.dart similarity index 100% rename from packages/remix_fortal/lib/src/recipes/radio.g.dart rename to packages/remix_fortal/lib/src/components/radio.g.dart diff --git a/packages/remix_fortal/lib/src/recipes/segmented_control.dart b/packages/remix_fortal/lib/src/components/segmented_control.dart similarity index 99% rename from packages/remix_fortal/lib/src/recipes/segmented_control.dart rename to packages/remix_fortal/lib/src/components/segmented_control.dart index ca0bd65d1..009535227 100644 --- a/packages/remix_fortal/lib/src/recipes/segmented_control.dart +++ b/packages/remix_fortal/lib/src/components/segmented_control.dart @@ -2,7 +2,7 @@ import 'package:flutter/widgets.dart'; import 'package:mix_annotations/mix_annotations.dart'; import 'package:remix/remix.dart'; -import '../fortal/fortal.dart'; +import '../theme/theme.dart'; part 'segmented_control.g.dart'; diff --git a/packages/remix_fortal/lib/src/recipes/segmented_control.g.dart b/packages/remix_fortal/lib/src/components/segmented_control.g.dart similarity index 100% rename from packages/remix_fortal/lib/src/recipes/segmented_control.g.dart rename to packages/remix_fortal/lib/src/components/segmented_control.g.dart diff --git a/packages/remix_fortal/lib/src/recipes/select.dart b/packages/remix_fortal/lib/src/components/select.dart similarity index 99% rename from packages/remix_fortal/lib/src/recipes/select.dart rename to packages/remix_fortal/lib/src/components/select.dart index e44cd667b..847f0e58e 100644 --- a/packages/remix_fortal/lib/src/recipes/select.dart +++ b/packages/remix_fortal/lib/src/components/select.dart @@ -2,7 +2,7 @@ import 'package:flutter/widgets.dart'; import 'package:mix_annotations/mix_annotations.dart'; import 'package:remix/remix.dart'; -import '../fortal/fortal.dart'; +import '../theme/theme.dart'; part 'select.g.dart'; diff --git a/packages/remix_fortal/lib/src/recipes/select.g.dart b/packages/remix_fortal/lib/src/components/select.g.dart similarity index 100% rename from packages/remix_fortal/lib/src/recipes/select.g.dart rename to packages/remix_fortal/lib/src/components/select.g.dart diff --git a/packages/remix_fortal/lib/src/recipes/sidebar.dart b/packages/remix_fortal/lib/src/components/sidebar.dart similarity index 98% rename from packages/remix_fortal/lib/src/recipes/sidebar.dart rename to packages/remix_fortal/lib/src/components/sidebar.dart index 9f3aa4277..728616d77 100644 --- a/packages/remix_fortal/lib/src/recipes/sidebar.dart +++ b/packages/remix_fortal/lib/src/components/sidebar.dart @@ -2,7 +2,7 @@ import 'package:flutter/widgets.dart'; import 'package:mix_annotations/mix_annotations.dart'; import 'package:remix/remix.dart'; -import '../fortal/fortal.dart'; +import '../theme/theme.dart'; import 'text.dart'; import 'toggle.dart'; diff --git a/packages/remix_fortal/lib/src/recipes/sidebar.g.dart b/packages/remix_fortal/lib/src/components/sidebar.g.dart similarity index 100% rename from packages/remix_fortal/lib/src/recipes/sidebar.g.dart rename to packages/remix_fortal/lib/src/components/sidebar.g.dart diff --git a/packages/remix_fortal/lib/src/recipes/skeleton.dart b/packages/remix_fortal/lib/src/components/skeleton.dart similarity index 95% rename from packages/remix_fortal/lib/src/recipes/skeleton.dart rename to packages/remix_fortal/lib/src/components/skeleton.dart index f4b41a5bf..e49748a8c 100644 --- a/packages/remix_fortal/lib/src/recipes/skeleton.dart +++ b/packages/remix_fortal/lib/src/components/skeleton.dart @@ -2,7 +2,7 @@ import 'package:flutter/widgets.dart'; import 'package:mix_annotations/mix_annotations.dart'; import 'package:remix/remix.dart'; -import '../fortal/fortal.dart'; +import '../theme/theme.dart'; part 'skeleton.g.dart'; diff --git a/packages/remix_fortal/lib/src/recipes/skeleton.g.dart b/packages/remix_fortal/lib/src/components/skeleton.g.dart similarity index 100% rename from packages/remix_fortal/lib/src/recipes/skeleton.g.dart rename to packages/remix_fortal/lib/src/components/skeleton.g.dart diff --git a/packages/remix_fortal/lib/src/recipes/slider.dart b/packages/remix_fortal/lib/src/components/slider.dart similarity index 99% rename from packages/remix_fortal/lib/src/recipes/slider.dart rename to packages/remix_fortal/lib/src/components/slider.dart index 3db8e0703..9ea4af785 100644 --- a/packages/remix_fortal/lib/src/recipes/slider.dart +++ b/packages/remix_fortal/lib/src/components/slider.dart @@ -3,7 +3,7 @@ import 'package:mix_annotations/mix_annotations.dart'; import 'package:naked_ui/naked_ui.dart'; import 'package:remix/remix.dart'; -import '../fortal/fortal.dart'; +import '../theme/theme.dart'; part 'slider.g.dart'; diff --git a/packages/remix_fortal/lib/src/recipes/slider.g.dart b/packages/remix_fortal/lib/src/components/slider.g.dart similarity index 100% rename from packages/remix_fortal/lib/src/recipes/slider.g.dart rename to packages/remix_fortal/lib/src/components/slider.g.dart diff --git a/packages/remix_fortal/lib/src/recipes/spinner.dart b/packages/remix_fortal/lib/src/components/spinner.dart similarity index 96% rename from packages/remix_fortal/lib/src/recipes/spinner.dart rename to packages/remix_fortal/lib/src/components/spinner.dart index c3a8fa52f..60b3f9435 100644 --- a/packages/remix_fortal/lib/src/recipes/spinner.dart +++ b/packages/remix_fortal/lib/src/components/spinner.dart @@ -2,7 +2,7 @@ import 'package:flutter/widgets.dart'; import 'package:mix_annotations/mix_annotations.dart'; import 'package:remix/remix.dart'; -import '../fortal/fortal.dart'; +import '../theme/theme.dart'; part 'spinner.g.dart'; diff --git a/packages/remix_fortal/lib/src/recipes/spinner.g.dart b/packages/remix_fortal/lib/src/components/spinner.g.dart similarity index 100% rename from packages/remix_fortal/lib/src/recipes/spinner.g.dart rename to packages/remix_fortal/lib/src/components/spinner.g.dart diff --git a/packages/remix_fortal/lib/src/recipes/switch.dart b/packages/remix_fortal/lib/src/components/switch.dart similarity index 99% rename from packages/remix_fortal/lib/src/recipes/switch.dart rename to packages/remix_fortal/lib/src/components/switch.dart index d91e863b7..929facf74 100644 --- a/packages/remix_fortal/lib/src/recipes/switch.dart +++ b/packages/remix_fortal/lib/src/components/switch.dart @@ -2,7 +2,7 @@ import 'package:flutter/widgets.dart'; import 'package:mix_annotations/mix_annotations.dart'; import 'package:remix/remix.dart'; -import '../fortal/fortal.dart'; +import '../theme/theme.dart'; part 'switch.g.dart'; diff --git a/packages/remix_fortal/lib/src/recipes/switch.g.dart b/packages/remix_fortal/lib/src/components/switch.g.dart similarity index 100% rename from packages/remix_fortal/lib/src/recipes/switch.g.dart rename to packages/remix_fortal/lib/src/components/switch.g.dart diff --git a/packages/remix_fortal/lib/src/recipes/tabs.dart b/packages/remix_fortal/lib/src/components/tabs.dart similarity index 99% rename from packages/remix_fortal/lib/src/recipes/tabs.dart rename to packages/remix_fortal/lib/src/components/tabs.dart index 54fd56644..632053f8b 100644 --- a/packages/remix_fortal/lib/src/recipes/tabs.dart +++ b/packages/remix_fortal/lib/src/components/tabs.dart @@ -3,7 +3,7 @@ import 'package:mix_annotations/mix_annotations.dart'; import 'package:naked_ui/naked_ui.dart'; import 'package:remix/remix.dart'; -import '../fortal/fortal.dart'; +import '../theme/theme.dart'; part 'tabs.g.dart'; diff --git a/packages/remix_fortal/lib/src/recipes/tabs.g.dart b/packages/remix_fortal/lib/src/components/tabs.g.dart similarity index 100% rename from packages/remix_fortal/lib/src/recipes/tabs.g.dart rename to packages/remix_fortal/lib/src/components/tabs.g.dart diff --git a/packages/remix_fortal/lib/src/recipes/text.dart b/packages/remix_fortal/lib/src/components/text.dart similarity index 95% rename from packages/remix_fortal/lib/src/recipes/text.dart rename to packages/remix_fortal/lib/src/components/text.dart index 8bdf0d97f..1c1d95c01 100644 --- a/packages/remix_fortal/lib/src/recipes/text.dart +++ b/packages/remix_fortal/lib/src/components/text.dart @@ -2,8 +2,8 @@ import 'package:flutter/widgets.dart'; import 'package:mix_annotations/mix_annotations.dart'; import 'package:remix/remix.dart'; -import '../fortal/fortal.dart'; -import 'typography_shared.dart'; +import '../theme/theme.dart'; +import 'typography.dart'; part 'text.g.dart'; diff --git a/packages/remix_fortal/lib/src/recipes/text.g.dart b/packages/remix_fortal/lib/src/components/text.g.dart similarity index 100% rename from packages/remix_fortal/lib/src/recipes/text.g.dart rename to packages/remix_fortal/lib/src/components/text.g.dart diff --git a/packages/remix_fortal/lib/src/recipes/textfield.dart b/packages/remix_fortal/lib/src/components/textfield.dart similarity index 99% rename from packages/remix_fortal/lib/src/recipes/textfield.dart rename to packages/remix_fortal/lib/src/components/textfield.dart index f1a35db77..c13f7984a 100644 --- a/packages/remix_fortal/lib/src/recipes/textfield.dart +++ b/packages/remix_fortal/lib/src/components/textfield.dart @@ -7,7 +7,7 @@ import 'package:flutter/services.dart'; import 'package:mix_annotations/mix_annotations.dart'; import 'package:remix/remix.dart'; -import '../fortal/fortal.dart'; +import '../theme/theme.dart'; part 'textfield.g.dart'; diff --git a/packages/remix_fortal/lib/src/recipes/textfield.g.dart b/packages/remix_fortal/lib/src/components/textfield.g.dart similarity index 100% rename from packages/remix_fortal/lib/src/recipes/textfield.g.dart rename to packages/remix_fortal/lib/src/components/textfield.g.dart diff --git a/packages/remix_fortal/lib/src/recipes/toggle.dart b/packages/remix_fortal/lib/src/components/toggle.dart similarity index 99% rename from packages/remix_fortal/lib/src/recipes/toggle.dart rename to packages/remix_fortal/lib/src/components/toggle.dart index 59dfad4bf..8127e52ac 100644 --- a/packages/remix_fortal/lib/src/recipes/toggle.dart +++ b/packages/remix_fortal/lib/src/components/toggle.dart @@ -2,7 +2,7 @@ import 'package:flutter/widgets.dart'; import 'package:mix_annotations/mix_annotations.dart'; import 'package:remix/remix.dart'; -import '../fortal/fortal.dart'; +import '../theme/theme.dart'; part 'toggle.g.dart'; diff --git a/packages/remix_fortal/lib/src/recipes/toggle.g.dart b/packages/remix_fortal/lib/src/components/toggle.g.dart similarity index 100% rename from packages/remix_fortal/lib/src/recipes/toggle.g.dart rename to packages/remix_fortal/lib/src/components/toggle.g.dart diff --git a/packages/remix_fortal/lib/src/recipes/toggle_group.dart b/packages/remix_fortal/lib/src/components/toggle_group.dart similarity index 99% rename from packages/remix_fortal/lib/src/recipes/toggle_group.dart rename to packages/remix_fortal/lib/src/components/toggle_group.dart index 96418b061..e59646d02 100644 --- a/packages/remix_fortal/lib/src/recipes/toggle_group.dart +++ b/packages/remix_fortal/lib/src/components/toggle_group.dart @@ -2,7 +2,7 @@ import 'package:flutter/widgets.dart'; import 'package:mix_annotations/mix_annotations.dart'; import 'package:remix/remix.dart'; -import '../fortal/fortal.dart'; +import '../theme/theme.dart'; part 'toggle_group.g.dart'; diff --git a/packages/remix_fortal/lib/src/recipes/toggle_group.g.dart b/packages/remix_fortal/lib/src/components/toggle_group.g.dart similarity index 100% rename from packages/remix_fortal/lib/src/recipes/toggle_group.g.dart rename to packages/remix_fortal/lib/src/components/toggle_group.g.dart diff --git a/packages/remix_fortal/lib/src/recipes/tooltip.dart b/packages/remix_fortal/lib/src/components/tooltip.dart similarity index 95% rename from packages/remix_fortal/lib/src/recipes/tooltip.dart rename to packages/remix_fortal/lib/src/components/tooltip.dart index 2a2ab6461..6652fa000 100644 --- a/packages/remix_fortal/lib/src/recipes/tooltip.dart +++ b/packages/remix_fortal/lib/src/components/tooltip.dart @@ -2,7 +2,7 @@ import 'package:flutter/widgets.dart'; import 'package:mix_annotations/mix_annotations.dart'; import 'package:remix/remix.dart'; -import '../fortal/fortal.dart'; +import '../theme/theme.dart'; part 'tooltip.g.dart'; diff --git a/packages/remix_fortal/lib/src/recipes/tooltip.g.dart b/packages/remix_fortal/lib/src/components/tooltip.g.dart similarity index 100% rename from packages/remix_fortal/lib/src/recipes/tooltip.g.dart rename to packages/remix_fortal/lib/src/components/tooltip.g.dart diff --git a/packages/remix_fortal/lib/src/recipes/typography_shared.dart b/packages/remix_fortal/lib/src/components/typography.dart similarity index 98% rename from packages/remix_fortal/lib/src/recipes/typography_shared.dart rename to packages/remix_fortal/lib/src/components/typography.dart index eb7c15419..9a1843bd1 100644 --- a/packages/remix_fortal/lib/src/recipes/typography_shared.dart +++ b/packages/remix_fortal/lib/src/components/typography.dart @@ -1,7 +1,7 @@ import 'package:flutter/widgets.dart'; import 'package:remix/remix.dart'; -import '../fortal/fortal.dart'; +import '../theme/theme.dart'; /// The nine-step Radix Themes text scale supplied by [FortalTokens]. enum FortalTextSize { diff --git a/packages/remix_fortal/lib/src/fortal/base_button_recipe.dart b/packages/remix_fortal/lib/src/fortal/base_button_recipe.dart deleted file mode 100644 index 31f8c17fa..000000000 --- a/packages/remix_fortal/lib/src/fortal/base_button_recipe.dart +++ /dev/null @@ -1,326 +0,0 @@ -import 'package:flutter/widgets.dart'; -import 'package:mix/mix.dart'; -import 'package:remix/remix.dart' - show - RemixBoxEffectLayerMix, - RemixBoxEffectsMix, - RemixBoxShadowKind, - RemixBoxShadowMix, - RemixBoxStylerAnchors, - RemixCssColorFilterOperation, - RemixLinearGradientMix, - RemixOrderedColorFilterModifier; - -import 'fortal_theme.dart'; - -/// The Radix BaseButton size scale shared by Button and IconButton. -/// -/// Deliberate: this mirrors [FortalBaseButtonVariant]. Button and IconButton -/// each own a size enum, so the shared metrics need a common type — passing the -/// raw `size.index + 1` instead would drop every switch below to a wildcard and -/// defer an unknown size to a runtime throw. -enum FortalBaseButtonSize { size1, size2, size3, size4 } - -/// Shared Radix BaseButton metrics used by Button and IconButton recipes. -({ - double height, - double paddingX, - double gap, - Radius radius, - TextStyleToken text, - double spinnerSize, -}) -fortalBaseButtonMetrics(FortalBaseButtonSize size) => switch (size) { - .size1 => ( - height: FortalTokens.space5(), - paddingX: FortalTokens.space2(), - gap: FortalTokens.space1(), - radius: FortalTokens.radius1OrFull(), - text: FortalTokens.text1, - spinnerSize: FortalTokens.space3(), - ), - .size2 => ( - height: FortalTokens.space6(), - paddingX: FortalTokens.space3(), - gap: FortalTokens.space2(), - radius: FortalTokens.radius2OrFull(), - text: FortalTokens.text2, - spinnerSize: FortalTokens.space4(), - ), - .size3 => ( - height: FortalTokens.space7(), - paddingX: FortalTokens.space4(), - gap: FortalTokens.space3(), - radius: FortalTokens.radius3OrFull(), - text: FortalTokens.text3, - spinnerSize: FortalTokens.space4(), - ), - .size4 => ( - height: FortalTokens.space8(), - paddingX: FortalTokens.space5(), - gap: FortalTokens.space3(), - radius: FortalTokens.radius4OrFull(), - text: FortalTokens.text4, - spinnerSize: FortalTokens.spinnerSize3(), - ), -}; - -/// Content-box metrics for the ghost BaseButton variant. -({double paddingX, double paddingY, double marginX, double marginY, double gap}) -fortalBaseButtonGhostMetrics(FortalBaseButtonSize size) => switch (size) { - .size1 => ( - paddingX: FortalTokens.space2(), - paddingY: FortalTokens.space1(), - marginX: FortalTokens.baseButtonGhostMarginX12(), - marginY: FortalTokens.baseButtonGhostMarginY12(), - gap: FortalTokens.space1(), - ), - .size2 => ( - paddingX: FortalTokens.space2(), - paddingY: FortalTokens.space1(), - marginX: FortalTokens.baseButtonGhostMarginX12(), - marginY: FortalTokens.baseButtonGhostMarginY12(), - gap: FortalTokens.space1(), - ), - .size3 => ( - paddingX: FortalTokens.space3(), - paddingY: FortalTokens.baseButtonGhostPaddingY3(), - marginX: FortalTokens.baseButtonGhostMarginX3(), - marginY: FortalTokens.baseButtonGhostMarginY3(), - gap: FortalTokens.space2(), - ), - .size4 => ( - paddingX: FortalTokens.space4(), - paddingY: FortalTokens.space2(), - marginX: FortalTokens.baseButtonGhostMarginX4(), - marginY: FortalTokens.baseButtonGhostMarginY4(), - gap: FortalTokens.space2(), - ), -}; - -/// Content-box metrics for the ghost IconButton variant. -({double padding, double margin}) fortalIconButtonGhostMetrics( - FortalBaseButtonSize size, -) => switch (size) { - .size1 => ( - padding: FortalTokens.space1(), - margin: FortalTokens.iconButtonGhostMargin1(), - ), - .size2 => ( - padding: FortalTokens.iconButtonGhostPadding2(), - margin: FortalTokens.iconButtonGhostMargin2(), - ), - .size3 => ( - padding: FortalTokens.space2(), - margin: FortalTokens.iconButtonGhostMargin3(), - ), - .size4 => ( - padding: FortalTokens.space3(), - margin: FortalTokens.iconButtonGhostMargin4(), - ), -}; - -/// A CSS outline that does not affect layout. -RemixBoxEffectsMix fortalFocusOutline(Color color, {required double offset}) => - RemixBoxEffectsMix( - outline: BorderSideMix( - color: color, - width: 2, - strokeAlign: BorderSide.strokeAlignInside, - ), - outlineOffset: offset, - ); - -/// The focus ring for stylers that can only express a border. -/// -/// [fortalFocusOutline] is the preferred form, but it returns a -/// [RemixBoxEffectsMix] and several stylers — `ToggleStyler`, -/// `ToggleGroupItemStyler`, `TabStyler` — expose no `containerEffects` slot to -/// put one in. Those recipes each re-derived the same border, so the width -/// token lives here instead of in four places. -extension FortalFocusRing> on RemixBoxStylerAnchors { - /// Applies the ring to this styler. - /// - /// [color] defaults to `focus-a8`. Tabs is the one caller that differs: it - /// passes the solid `focus-8`. Whether that is intentional is unresolved — - /// the pinned Chromium probes capture computed styles only, not - /// `:focus-visible`, so it cannot be settled from the reference fixtures. - /// Preserved as-is rather than unified on a guess. - /// - /// Tabs also leaves [strokeAlign] unset, but that is not a second - /// difference: `BorderSide` itself defaults to `strokeAlignInside`, so unset - /// and explicit resolve to the same -1.0. The parameter is nullable only so - /// tabs can keep expressing it as absent. - T fortalFocusRing({ - Color? color, - double? strokeAlign = BorderSide.strokeAlignInside, - }) => border( - .all( - BorderSideMix( - color: color ?? _focusRingColor(), - width: _focusRingWidth(), - strokeAlign: strokeAlign, - ), - ), - ); -} - -/// The same ring for a Mix [FlexBoxStyler], which sits outside Remix's -/// `RemixBoxStylerAnchors` interface — accordion rings its trigger, not itself. -/// Separate extension rather than a differently-named function so both read as -/// `.fortalFocusRing()`; the receiver type picks the right one. -extension FortalFocusRingFlexBox on FlexBoxStyler { - FlexBoxStyler fortalFocusRing() => border( - .color( - _focusRingColor(), - ).width(_focusRingWidth()).strokeAlign(BorderSide.strokeAlignInside), - ); -} - -Color _focusRingColor() => FortalTokens.focusA8(); -double _focusRingWidth() => FortalTokens.focusRingWidth(); - -/// A one-pixel inset stroke, optionally layered over a fill. -RemixBoxEffectLayerMix fortalInsetSurface({required List strokes}) => - RemixBoxEffectLayerMix( - shadows: [ - for (final stroke in strokes) - RemixBoxShadowMix( - kind: RemixBoxShadowKind.inset, - color: stroke, - spreadRadius: 1, - ), - ], - ); - -/// Resolves a mode-aware ordered CSS filter at the component build context. -WidgetModifierConfig fortalModeAwareFilter({ - required List light, - required List dark, -}) => WidgetModifierConfig.modifier( - _FortalModeAwareFilterMix(light: light, dark: dark), -); - -/// Explicit identity filter used to clear a higher-priority state filter. -WidgetModifierConfig fortalClearFilter() => - fortalModeAwareFilter(light: const [], dark: const []); - -/// Exact classic BaseButton surface for a visual state. -RemixBoxEffectLayerMix fortalClassicBaseButtonSurface({ - required bool highContrast, - bool hovered = false, - bool pressed = false, - bool disabled = false, -}) { - final inset = FortalTokens.baseButtonClassicAfterInset(); - if (disabled) { - return RemixBoxEffectLayerMix( - gradients: [ - RemixLinearGradientMix( - colors: [ - FortalTokens.blackA1(), - const Color(0x00000000), - FortalTokens.whiteA1(), - ], - stops: const [-0.2, 0.4, 1], - ), - RemixLinearGradientMix( - colors: [FortalTokens.grayA2(), FortalTokens.grayA2()], - ), - ], - gradientInsets: [inset, inset], - shadowToken: FortalTokens.baseButtonClassicDisabledShadows, - ); - } - - final baseColor = highContrast - ? FortalTokens.accent12() - : FortalTokens.accent9(); - final afterColor = hovered && !highContrast - ? FortalTokens.accent10() - : baseColor; - final pseudoGradient = RemixLinearGradientMix( - colors: [ - highContrast - ? hovered || pressed - ? FortalTokens.blackA5() - : FortalTokens.blackA3() - : hovered - ? FortalTokens.blackA2() - : pressed - ? FortalTokens.blackA2() - : FortalTokens.blackA1(), - const Color(0x00000000), - highContrast - ? pressed - ? FortalTokens.whiteA3() - : FortalTokens.whiteA2() - : hovered || pressed - ? FortalTokens.whiteA3() - : FortalTokens.whiteA2(), - ], - stops: hovered && !highContrast - ? const [-0.15, 0.425, 1] - : const [0, 0.5, 1], - ); - final gradients = [ - pseudoGradient, - RemixLinearGradientMix(colors: [afterColor, afterColor]), - if (pressed) - RemixLinearGradientMix( - colors: [FortalTokens.blackA1(), const Color(0x00000000)], - ) - else ...[ - RemixLinearGradientMix( - colors: [ - const Color(0x00000000), - const Color(0x00000000), - FortalTokens.grayA4(), - FortalTokens.grayA4(), - ], - stops: const [0, 0.5, 0.5, 1], - ), - RemixLinearGradientMix( - colors: [ - const Color(0x00000000), - const Color(0x00000000), - baseColor, - baseColor, - ], - stops: const [0, 0.5, 0.8, 1], - ), - ], - ]; - return RemixBoxEffectLayerMix( - gradients: gradients, - gradientInsets: [inset, inset, ...List.filled(gradients.length - 2, 0)], - shadowToken: pressed - ? highContrast - ? FortalTokens.baseButtonClassicActiveHighContrastShadows - : FortalTokens.baseButtonClassicActiveShadows - : highContrast - ? FortalTokens.baseButtonClassicHighContrastShadows - : FortalTokens.baseButtonClassicShadows, - ); -} - -final class _FortalModeAwareFilterMix - extends ModifierMix { - const _FortalModeAwareFilterMix({required this.light, required this.dark}); - - final List light; - final List dark; - - @override - RemixOrderedColorFilterModifier resolve(BuildContext context) => - RemixOrderedColorFilterModifier( - FortalTheme.of(context).isDark ? dark : light, - ); - - @override - _FortalModeAwareFilterMix merge(_FortalModeAwareFilterMix? other) => - other ?? this; - - @override - List get props => [light, dark]; -} diff --git a/packages/remix_fortal/lib/src/fortal/fortal.dart b/packages/remix_fortal/lib/src/fortal/fortal.dart deleted file mode 100644 index 0717f2758..000000000 --- a/packages/remix_fortal/lib/src/fortal/fortal.dart +++ /dev/null @@ -1,14 +0,0 @@ -// Internal entrypoint for the theme layer, imported by every recipe under -// `src/recipes/`. The package's public entrypoint is `lib/remix_fortal.dart`. - -// Color swatches (Radix colors retained) -export '../radix/colors/colors.dart'; -// Computed helpers and shared types -export 'computed.dart'; -export 'base_button_recipe.dart'; -export 'surface_frame.dart'; -// Theme tokens and scope builder (consolidated) -export 'fortal_theme.dart'; - -// Note: the general-purpose style mixins stayed in `remix` (`src/style/`), which -// is what lets that package remain theme-free. diff --git a/packages/remix_fortal/lib/src/fortal/fortal_theme.dart b/packages/remix_fortal/lib/src/fortal/fortal_theme.dart deleted file mode 100644 index 6946b50a7..000000000 --- a/packages/remix_fortal/lib/src/fortal/fortal_theme.dart +++ /dev/null @@ -1,2217 +0,0 @@ -// Fortal theme: tokens + MixScope builder in one place. - -import 'dart:math' as math; - -import 'package:flutter/widgets.dart'; -import 'package:mix/mix.dart'; -import 'package:remix/remix.dart' - show RemixBoxShadow, RemixBoxShadowKind, RemixBoxShadowListToken; - -import '../radix/colors/colors.dart' as radix; -import 'computed.dart'; - -// OKLab mixing lives in computed.dart; no direct dependency here. - -/// Design tokens for the Fortal UI system (powered by Radix Colors). -/// -/// Provides color scales (12-step accent/gray), spacing (9-step), radius (6-step), -/// shadows (6-level), typography (9-size), and functional colors. -/// -/// Example: -/// ```dart -/// Style( -/// $box.color.ref(FortalTokens.accent9), -/// $text.style.ref(FortalTokens.text3), -/// $box.padding.ref(FortalTokens.space4), -/// ) -/// ``` -/// -/// Must be used within [FortalScope] to resolve actual values. -class FortalTokens { - // ============================================================================ - // BACKGROUND AND SURFACE COLORS - // ============================================================================ - - /// Page background color selected for the active brightness. - static const colorBackground = ColorToken('fortal.color.background'); - - /// Neutral surface color for input fields and controls. - static const colorSurface = ColorToken('fortal.color.surface'); - - /// Selected SegmentedControl surface for the active brightness. - static const segmentedControlIndicatorBackground = ColorToken( - 'fortal.segmented-control.indicator-background', - ); - - /// Solid panel background selected for the active brightness. - static const colorPanelSolid = ColorToken('fortal.color.panel.solid'); - - /// Translucent panel background with alpha transparency. - static const colorPanelTranslucent = ColorToken( - 'fortal.color.panel.translucent', - ); - - /// Panel background selected by [FortalPanelBackground]. - static const colorPanel = ColorToken('fortal.color.panel'); - - /// Backdrop blur applied to translucent floating panels. - static const panelBlur = DoubleToken('fortal.panel.blur'); - - /// Dark overlay for modals and dialogs. - static const colorOverlay = ColorToken('fortal.color.overlay'); - - // ============================================================================ - // FUNCTIONAL ACCENT COLORS - // ============================================================================ - - /// Subtle accent surface for soft button variants and chips. - static const accentSurface = ColorToken('fortal.accent.surface'); - - /// Active indicator color for progress bars and sliders. - static const accentIndicator = ColorToken('fortal.accent.indicator'); - - /// Track/rail background color for sliders and progress bars. - static const accentTrack = ColorToken('fortal.accent.track'); - - /// Mode-aware overlay used by high-contrast slider ranges. - static const sliderHighContrastOverlay = ColorToken( - 'fortal.slider.high-contrast-overlay', - ); - - /// High contrast foreground for solid accent backgrounds. - static const accentContrast = ColorToken('fortal.accent.contrast'); - - // ============================================================================ - // FOCUS AND INTERACTION STATES - // ============================================================================ - - /// Solid focus ring color (accent step 8). - static const focus8 = ColorToken('fortal.focus.8'); - - /// Translucent text-selection color (accent alpha step 5). - static const focusA5 = ColorToken('fortal.focus.a5'); - - /// Translucent focus ring color with alpha transparency. - static const focusA8 = ColorToken('fortal.focus.a8'); - - /// Mode-aware red roles used by documented validation extensions. - static const error3 = ColorToken('fortal.error.3'); - static const error7 = ColorToken('fortal.error.7'); - static const error8 = ColorToken('fortal.error.8'); - static const error9 = ColorToken('fortal.error.9'); - static const error11 = ColorToken('fortal.error.11'); - static const error12 = ColorToken('fortal.error.12'); - static const errorA7 = ColorToken('fortal.error.a7'); - - // ============================================================================ - // ACCENT COLOR SCALE (12 STEPS) - // ============================================================================ - // - // Fortal uses a 12-step color scale (inherited from Radix Themes) that provides semantic meaning: - // - // Steps 1-2: App backgrounds (subtle → more visible) - // Steps 3-5: Component backgrounds (rest → hover → active) - // Steps 6-8: Borders (subtle → component → hover) - // Steps 9-10: Solid backgrounds (default → hover) - // Steps 11-12: Text (low contrast → high contrast) - // - - /// Accent step 1 - App background, most subtle. - static const accent1 = ColorToken('fortal.accent.1'); - - /// Accent step 2 - Subtle background. - static const accent2 = ColorToken('fortal.accent.2'); - - /// Accent step 3 - Component background at rest. - static const accent3 = ColorToken('fortal.accent.3'); - - /// Accent step 4 - Component background on hover. - static const accent4 = ColorToken('fortal.accent.4'); - - /// Accent step 5 - Component background when active/pressed. - static const accent5 = ColorToken('fortal.accent.5'); - - /// Accent step 6 - Subtle borders and separators. - static const accent6 = ColorToken('fortal.accent.6'); - - /// Accent step 7 - Component borders at rest. - static const accent7 = ColorToken('fortal.accent.7'); - - /// Accent step 8 - Component borders on hover and focus. - static const accent8 = ColorToken('fortal.accent.8'); - - /// Accent step 9 - Primary solid background. - static const accent9 = ColorToken('fortal.accent.9'); - - /// Accent step 10 - Solid background on hover. - static const accent10 = ColorToken('fortal.accent.10'); - - /// Accent step 11 - Low contrast text. - static const accent11 = ColorToken('fortal.accent.11'); - - /// Accent step 12 - High contrast text. - static const accent12 = ColorToken('fortal.accent.12'); - - // ============================================================================ - // GRAY COLOR SCALE (12 STEPS) - // ============================================================================ - // - // The gray scale follows the same 12-step semantic structure as accent colors, - // but provides neutral colors for text, borders, and backgrounds. - // The specific gray variant (slate, mauve, sage, etc.) is chosen in the theme. - // - - /// Gray step 1 - Page background. - static const gray1 = ColorToken('fortal.gray.1'); - - /// Gray step 2 - Panel and card backgrounds. - static const gray2 = ColorToken('fortal.gray.2'); - - /// Gray step 3 - Input backgrounds and pressed states. - static const gray3 = ColorToken('fortal.gray.3'); - - /// Gray step 4 - Input backgrounds on hover. - static const gray4 = ColorToken('fortal.gray.4'); - - /// Gray step 5 - Active states and disabled backgrounds. - static const gray5 = ColorToken('fortal.gray.5'); - - /// Gray step 6 - Subtle borders and dividers. - static const gray6 = ColorToken('fortal.gray.6'); - - /// Gray step 7 - Standard borders and outlines. - /// - /// Primary border color for form inputs, cards, - /// and component boundaries. - static const gray7 = ColorToken('fortal.gray.7'); - - /// Gray step 8 - Borders on hover and focus. - /// - /// Interactive border states and stronger separators - /// that need more visual weight. - static const gray8 = ColorToken('fortal.gray.8'); - - /// Gray step 9 - Solid neutral backgrounds. - /// - /// For neutral buttons, badges, and other elements - /// that need a solid background without accent color. - static const gray9 = ColorToken('fortal.gray.9'); - - /// Gray step 10 - Solid neutral backgrounds on hover. - /// - /// Hover state for neutral solid backgrounds, - /// providing interactive feedback. - static const gray10 = ColorToken('fortal.gray.10'); - - /// Gray step 11 - Low contrast text and secondary content. - /// - /// For secondary text, placeholders, and content that should - /// be readable but not prominent. - static const gray11 = ColorToken('fortal.gray.11'); - - /// Gray step 12 - High contrast text and primary content. - /// - /// Primary text color for body content, headings, and any text - /// that needs maximum readability and prominence. - static const gray12 = ColorToken('fortal.gray.12'); - - // ============================================================================ - // GRAY ROLE TOKENS (parity with generated JSON roles) - // ============================================================================ - /// Neutral surface baseline for the selected gray scale (matches JSON surface) - static const graySurface = ColorToken('fortal.gray.surface'); - - /// Neutral indicator color (typically gray step 9) - static const grayIndicator = ColorToken('fortal.gray.indicator'); - - /// Neutral track color (typically gray step 9) - static const grayTrack = ColorToken('fortal.gray.track'); - - /// Contrast color for content over neutral solid backgrounds (white) - static const grayContrast = ColorToken('fortal.gray.contrast'); - - // ============================================================================ - // ALPHA VARIANTS (FULL 12-STEP FOR ACCENT AND GRAY) - // ============================================================================ - - // Accent alpha steps a1..a12 - static const accentA1 = ColorToken('fortal.accent.a1'); - static const accentA2 = ColorToken('fortal.accent.a2'); - static const accentA3 = ColorToken('fortal.accent.a3'); - static const accentA4 = ColorToken('fortal.accent.a4'); - static const accentA5 = ColorToken('fortal.accent.a5'); - static const accentA6 = ColorToken('fortal.accent.a6'); - static const accentA7 = ColorToken('fortal.accent.a7'); - static const accentA8 = ColorToken('fortal.accent.a8'); - static const accentA9 = ColorToken('fortal.accent.a9'); - static const accentA10 = ColorToken('fortal.accent.a10'); - static const accentA11 = ColorToken('fortal.accent.a11'); - static const accentA12 = ColorToken('fortal.accent.a12'); - - // Gray alpha steps a1..a12 - static const grayA1 = ColorToken('fortal.gray.a1'); - static const grayA2 = ColorToken('fortal.gray.a2'); - static const grayA3 = ColorToken('fortal.gray.a3'); - static const grayA4 = ColorToken('fortal.gray.a4'); - static const grayA5 = ColorToken('fortal.gray.a5'); - static const grayA6 = ColorToken('fortal.gray.a6'); - static const grayA7 = ColorToken('fortal.gray.a7'); - static const grayA8 = ColorToken('fortal.gray.a8'); - static const grayA9 = ColorToken('fortal.gray.a9'); - static const grayA10 = ColorToken('fortal.gray.a10'); - static const grayA11 = ColorToken('fortal.gray.a11'); - static const grayA12 = ColorToken('fortal.gray.a12'); - - // ============================================================================ - // NEUTRALS FOR SHADOWS (HELPER TOKENS) - // ============================================================================ - /// Gray alpha steps are declared above (grayA1..grayA12). - - /// Black alpha steps used in layered shadows. - static const blackA1 = ColorToken('fortal.black.a1'); - static const blackA2 = ColorToken('fortal.black.a2'); - static const blackA3 = ColorToken('fortal.black.a3'); - static const blackA4 = ColorToken('fortal.black.a4'); - static const blackA5 = ColorToken('fortal.black.a5'); - static const blackA6 = ColorToken('fortal.black.a6'); - static const blackA7 = ColorToken('fortal.black.a7'); - static const blackA8 = ColorToken('fortal.black.a8'); - static const blackA9 = ColorToken('fortal.black.a9'); - static const blackA10 = ColorToken('fortal.black.a10'); - static const blackA11 = ColorToken('fortal.black.a11'); - static const blackA12 = ColorToken('fortal.black.a12'); - - /// White alpha steps used by layered classic-control recipes. - static const whiteA1 = ColorToken('fortal.white.a1'); - static const whiteA2 = ColorToken('fortal.white.a2'); - static const whiteA3 = ColorToken('fortal.white.a3'); - static const whiteA4 = ColorToken('fortal.white.a4'); - static const whiteA5 = ColorToken('fortal.white.a5'); - static const whiteA6 = ColorToken('fortal.white.a6'); - static const whiteA7 = ColorToken('fortal.white.a7'); - static const whiteA8 = ColorToken('fortal.white.a8'); - static const whiteA9 = ColorToken('fortal.white.a9'); - static const whiteA10 = ColorToken('fortal.white.a10'); - static const whiteA11 = ColorToken('fortal.white.a11'); - static const whiteA12 = ColorToken('fortal.white.a12'); - - /// Mode-aware mixed shadow stroke. - static const shadowStroke = ColorToken('fortal.shadow.stroke'); - - /// Premultiplied OKLab mixes used by Radix neutral one-pixel strokes. - static const grayStroke3 = ColorToken('fortal.gray.stroke.3'); - static const grayStroke4 = ColorToken('fortal.gray.stroke.4'); - static const grayStroke5 = ColorToken('fortal.gray.stroke.5'); - static const grayStroke6 = ColorToken('fortal.gray.stroke.6'); - static const grayStroke7 = ColorToken('fortal.gray.stroke.7'); - - // ============================================================================ - // SPACING SCALE (9 STEPS) - // ============================================================================ - // - // A consistent spacing scale based on 4px increments. - // - - /// Space step 1 - 4px. - /// - /// Smallest spacing for tight layouts, borders, - /// and fine-grained adjustments. - static const space1 = SpaceToken('fortal.space.1'); - - /// Space step 2 - 8px. - /// - /// Small spacing for component padding and margins. - /// Good for button padding and form element spacing. - static const space2 = SpaceToken('fortal.space.2'); - - /// Space step 3 - 12px. - /// - /// Medium-small spacing for comfortable padding - /// and moderate element separation. - static const space3 = SpaceToken('fortal.space.3'); - - /// Space step 4 - 16px. - /// - /// Standard spacing for most layouts. Good default - /// for card padding and section margins. - static const space4 = SpaceToken('fortal.space.4'); - - /// Space step 5 - 24px. - /// - /// Medium spacing for generous padding and - /// comfortable separation between sections. - static const space5 = SpaceToken('fortal.space.5'); - - /// Space step 6 - 32px. - /// - /// Large spacing for significant visual separation - /// and generous component padding. - static const space6 = SpaceToken('fortal.space.6'); - - /// Space step 7 - 40px. - /// - /// Extra large spacing for major layout sections - /// and prominent visual separation. - static const space7 = SpaceToken('fortal.space.7'); - - /// Space step 8 - 48px. - /// - /// Very large spacing for significant page sections - /// and major layout boundaries. - static const space8 = SpaceToken('fortal.space.8'); - - /// Space step 9 - 64px. - /// - /// Maximum spacing for major page sections - /// and substantial layout separation. - static const space9 = SpaceToken('fortal.space.9'); - - /// Spinner size 3 - 20px at 100% scaling. - /// - /// Radix defines this as 1.25 times space 4, so it needs its own resolved - /// token rather than arithmetic on an unresolved token reference. - static const spinnerSize3 = DoubleToken('fortal.spinner.size.3'); - - /// Compact gap shared by size-1 toggle extensions (2px at 100% scaling). - static const toggleGap1 = DoubleToken('fortal.toggle.gap.1'); - - /// Comfortable gap shared by size-3 toggle extensions (6px at 100%). - static const toggleGap3 = DoubleToken('fortal.toggle.gap.3'); - - /// Avatar sizes expressed as scaled pixels rather than spacing steps. - static const avatarSize6 = DoubleToken('fortal.avatar.size.6'); - static const avatarSize7 = DoubleToken('fortal.avatar.size.7'); - static const avatarSize8 = DoubleToken('fortal.avatar.size.8'); - static const avatarSize9 = DoubleToken('fortal.avatar.size.9'); - - /// Avatar icon sizes are half of each resolved avatar dimension. - /// - /// These values need dedicated tokens because arithmetic on an unresolved - /// token reference would destroy its identity before Mix can resolve it. - static const avatarIconSize1 = DoubleToken('fortal.avatar.icon-size.1'); - static const avatarIconSize2 = DoubleToken('fortal.avatar.icon-size.2'); - static const avatarIconSize3 = DoubleToken('fortal.avatar.icon-size.3'); - static const avatarIconSize4 = DoubleToken('fortal.avatar.icon-size.4'); - static const avatarIconSize5 = DoubleToken('fortal.avatar.icon-size.5'); - static const avatarIconSize6 = DoubleToken('fortal.avatar.icon-size.6'); - static const avatarIconSize7 = DoubleToken('fortal.avatar.icon-size.7'); - static const avatarIconSize8 = DoubleToken('fortal.avatar.icon-size.8'); - static const avatarIconSize9 = DoubleToken('fortal.avatar.icon-size.9'); - - /// Badge measurements that are fractional spacing expressions upstream. - static const badgePaddingX1 = DoubleToken('fortal.badge.padding-x.1'); - static const badgePaddingY1 = DoubleToken('fortal.badge.padding-y.1'); - static const badgePaddingX3 = DoubleToken('fortal.badge.padding-x.3'); - - /// Checkbox dimensions expressed as scaled pixels by Radix Themes. - static const checkboxSize1 = DoubleToken('fortal.checkbox.size.1'); - static const checkboxSize3 = DoubleToken('fortal.checkbox.size.3'); - static const checkboxIndicatorSize1 = DoubleToken( - 'fortal.checkbox.indicator-size.1', - ); - static const checkboxIndicatorSize2 = DoubleToken( - 'fortal.checkbox.indicator-size.2', - ); - static const checkboxIndicatorSize3 = DoubleToken( - 'fortal.checkbox.indicator-size.3', - ); - - /// Checkbox-group label gaps derived from Radix's size-linked `0.5em`. - static const checkboxGroupItemGap1 = DoubleToken( - 'fortal.checkbox-group.item-gap.1', - ); - static const checkboxGroupItemGap2 = DoubleToken( - 'fortal.checkbox-group.item-gap.2', - ); - static const checkboxGroupItemGap3 = DoubleToken( - 'fortal.checkbox-group.item-gap.3', - ); - - /// Radio indicators are 40% of their control size in Radix Themes. - /// - /// These values need dedicated tokens because arithmetic on an unresolved - /// token reference would destroy its identity before Mix can resolve it. - static const radioIndicatorSize1 = DoubleToken( - 'fortal.radio.indicator-size.1', - ); - static const radioIndicatorSize2 = DoubleToken( - 'fortal.radio.indicator-size.2', - ); - static const radioIndicatorSize3 = DoubleToken( - 'fortal.radio.indicator-size.3', - ); - - /// Checkbox radii derived from fractional radius-step expressions. - static const checkboxRadius1 = RadiusToken('fortal.checkbox.radius.1'); - static const checkboxRadius3 = RadiusToken('fortal.checkbox.radius.3'); - - /// Switch geometry that cannot be derived from unresolved token references. - static const switchHeight2 = DoubleToken('fortal.switch.height.2'); - static const switchWidth1 = DoubleToken('fortal.switch.width.1'); - static const switchWidth2 = DoubleToken('fortal.switch.width.2'); - static const switchWidth3 = DoubleToken('fortal.switch.width.3'); - static const switchThumbSize1 = DoubleToken('fortal.switch.thumb-size.1'); - static const switchThumbSize2 = DoubleToken('fortal.switch.thumb-size.2'); - static const switchThumbSize3 = DoubleToken('fortal.switch.thumb-size.3'); - - /// Progress geometry derived from scaled fractional upstream expressions. - static const progressHeight2 = DoubleToken('fortal.progress.height.2'); - static const progressRadius1 = RadiusToken('fortal.progress.radius.1'); - static const progressRadius2 = RadiusToken('fortal.progress.radius.2'); - static const progressRadius3 = RadiusToken('fortal.progress.radius.3'); - - /// Slider geometry expressed as scaled Radix component dimensions. - static const sliderTrackSize1 = DoubleToken('fortal.slider.track-size.1'); - static const sliderTrackSize2 = DoubleToken('fortal.slider.track-size.2'); - static const sliderTrackSize3 = DoubleToken('fortal.slider.track-size.3'); - static const sliderThumbSize1 = DoubleToken('fortal.slider.thumb-size.1'); - static const sliderThumbSize2 = DoubleToken('fortal.slider.thumb-size.2'); - static const sliderThumbSize3 = DoubleToken('fortal.slider.thumb-size.3'); - static const sliderTrackRadius1 = RadiusToken('fortal.slider.track-radius.1'); - static const sliderTrackRadius2 = RadiusToken('fortal.slider.track-radius.2'); - static const sliderTrackRadius3 = RadiusToken('fortal.slider.track-radius.3'); - - /// TextField content insets after its fixed one-pixel border. - static const textFieldPadding1 = DoubleToken('fortal.text-field.padding.1'); - static const textFieldPadding2 = DoubleToken('fortal.text-field.padding.2'); - static const textFieldPadding3 = DoubleToken('fortal.text-field.padding.3'); - - /// TextArea metrics that cannot be expressed by existing spacing tokens. - static const textAreaMinHeight3 = DoubleToken( - 'fortal.text-area.min-height.3', - ); - - /// DataList metrics that cannot be expressed by existing spacing tokens. - static const dataListRowGap3 = DoubleToken('fortal.data-list.row-gap.3'); - static const dataListLabelMinWidth = DoubleToken( - 'fortal.data-list.label-min-width', - ); - - /// Exact uppercase fallback typography for each Avatar size. - static const avatarFallback1One = TextStyleToken( - 'fortal.avatar.fallback.1.one', - ); - static const avatarFallback1Two = TextStyleToken( - 'fortal.avatar.fallback.1.two', - ); - static const avatarFallback2One = TextStyleToken( - 'fortal.avatar.fallback.2.one', - ); - static const avatarFallback2Two = TextStyleToken( - 'fortal.avatar.fallback.2.two', - ); - static const avatarFallback3One = TextStyleToken( - 'fortal.avatar.fallback.3.one', - ); - static const avatarFallback3Two = TextStyleToken( - 'fortal.avatar.fallback.3.two', - ); - static const avatarFallback4One = TextStyleToken( - 'fortal.avatar.fallback.4.one', - ); - static const avatarFallback4Two = TextStyleToken( - 'fortal.avatar.fallback.4.two', - ); - static const avatarFallback5 = TextStyleToken('fortal.avatar.fallback.5'); - static const avatarFallback6 = TextStyleToken('fortal.avatar.fallback.6'); - static const avatarFallback7 = TextStyleToken('fortal.avatar.fallback.7'); - static const avatarFallback8 = TextStyleToken('fortal.avatar.fallback.8'); - static const avatarFallback9 = TextStyleToken('fortal.avatar.fallback.9'); - - /// Tabs size 1 inner vertical padding - 2px at 100% scaling. - static const tabInnerPaddingY1 = DoubleToken('fortal.tabs.inner-padding-y.1'); - - /// Tabs size 1 active tracking - -0.12px at 100% scaling. - static const tabActiveLetterSpacing1 = DoubleToken( - 'fortal.tabs.active-letter-spacing.1', - ); - - /// Tabs size 2 active tracking - -0.14px at 100% scaling. - static const tabActiveLetterSpacing2 = DoubleToken( - 'fortal.tabs.active-letter-spacing.2', - ); - - /// Table size-1 minimum cell height (36px at 100% scaling). - /// - /// Radix writes `calc(36px * var(--scaling))` literally, so no existing - /// spacing step expresses it. - static const dataTableRowHeight1 = DoubleToken('fortal.data-table.height.1'); - - /// Table size-2 minimum cell height (44px at 100% scaling). - static const dataTableRowHeight2 = DoubleToken('fortal.data-table.height.2'); - - /// Table surface border - `color-mix(in oklab, gray-a5, gray-6)`. - /// - /// The existing `grayStroke*` tokens blend an alpha step with the *same* - /// numbered solid step at 25%; Table blends step 5 with step 6 at 50%. - static const dataTableBorder = ColorToken('fortal.data-table.border'); - - /// Select's 1.5 × space-1 measurement (6px at 100% scaling). - static const selectSpace1Half = DoubleToken('fortal.select.space.1-half'); - - /// Select size-1 indicator column width (20px at 100% scaling). - static const selectIndicatorWidth1 = DoubleToken( - 'fortal.select.indicator-width.1', - ); - - /// Select size-1 check size (8px at 100% scaling). - static const selectIndicatorSize1 = DoubleToken( - 'fortal.select.indicator-size.1', - ); - - /// Select size-2/3 check size (10px at 100% scaling). - static const selectIndicatorSize2 = DoubleToken( - 'fortal.select.indicator-size.2', - ); - - /// Negative margins that cancel Select ghost-trigger padding. - static const selectGhostMarginX12 = DoubleToken( - 'fortal.select.ghost-margin-x.1-2', - ); - static const selectGhostMarginY12 = DoubleToken( - 'fortal.select.ghost-margin-y.1-2', - ); - static const selectGhostMarginX3 = DoubleToken( - 'fortal.select.ghost-margin-x.3', - ); - static const selectGhostMarginY3 = DoubleToken( - 'fortal.select.ghost-margin-y.3', - ); - - // ============================================================================ - // BORDER RADIUS SCALE (6 STEPS + FULL) - // ============================================================================ - - /// Radius step 1 - 3px. - /// - /// Subtle rounding for small elements like buttons - /// and form inputs. Provides gentle softening of corners. - static const radius1 = RadiusToken('fortal.radius.1'); - - /// Radius step 2 - 4px. - /// - /// Small radius for compact components and minor rounding. - /// Good for small badges and tight layouts. - static const radius2 = RadiusToken('fortal.radius.2'); - - /// Radius step 3 - 6px. - /// - /// Medium radius for standard components like buttons - /// and cards. Balances modern look with usability. - static const radius3 = RadiusToken('fortal.radius.3'); - - /// Radius step 4 - 8px. - /// - /// Large radius for prominent components and generous rounding. - /// Good for larger buttons and feature cards. - static const radius4 = RadiusToken('fortal.radius.4'); - - /// Radius step 5 - 12px. - /// - /// Extra large radius for major components and modern aesthetics. - /// Suitable for large cards and prominent interface elements. - static const radius5 = RadiusToken('fortal.radius.5'); - - /// Radius step 6 - 16px. - /// - /// Very large radius for distinctive styling and major components. - /// Creates a soft, friendly appearance for large interface elements. - static const radius6 = RadiusToken('fortal.radius.6'); - - /// Theme-level full radius, enabled only by [FortalRadius.full]. - static const radiusFull = RadiusToken('fortal.radius.full'); - - /// Radius used by control thumbs. - static const radiusThumb = RadiusToken('fortal.radius.thumb'); - - /// Fixed circle radius for shapes that stay circular across theme presets. - static const radiusCircle = RadiusToken('fortal.radius.circle'); - - /// Radius step 1 promoted to a pill when the theme radius is full. - static const radius1OrFull = RadiusToken('fortal.radius.1-or-full'); - - /// Radius step 2 promoted to a pill when the theme radius is full. - static const radius2OrFull = RadiusToken('fortal.radius.2-or-full'); - - /// Radius step 3 promoted to a pill when the theme radius is full. - static const radius3OrFull = RadiusToken('fortal.radius.3-or-full'); - - /// Radius step 4 promoted to a pill when the theme radius is full. - static const radius4OrFull = RadiusToken('fortal.radius.4-or-full'); - - /// Radius step 5 promoted to a pill when the theme radius is full. - static const radius5OrFull = RadiusToken('fortal.radius.5-or-full'); - - /// Radius step 6 promoted to a pill when the theme radius is full. - static const radius6OrFull = RadiusToken('fortal.radius.6-or-full'); - - /// Radius step 1 promoted to the control-thumb radius when larger. - static const radius1OrThumb = RadiusToken('fortal.radius.1-or-thumb'); - - /// Radius step 2 promoted to the control-thumb radius when larger. - static const radius2OrThumb = RadiusToken('fortal.radius.2-or-thumb'); - - // ============================================================================ - // ELEVATION SHADOWS (6 LEVELS) - // ============================================================================ - - /// Shadow level 1 - Subtle elevation. - /// - /// Minimal shadow for slight elevation effects. - /// Good for cards and buttons in their resting state. - static const shadow1 = BoxShadowToken('fortal.shadow.1'); - - /// Exact layered shadow level 1, including inset layers. - /// - /// This additive token powers Fortal's Radix-compatible rendering while - /// [shadow1] retains the original Remix public token type. - static const shadow1Layers = RemixBoxShadowListToken( - 'fortal.shadow.1.layers', - ); - - /// Half-opacity shadow-1 layers used by a disabled classic slider track. - static const sliderClassicDisabledTrackShadows = RemixBoxShadowListToken( - 'fortal.slider.classic.disabled-track-shadows', - ); - - /// Shadow level 2 - Low elevation. - /// - /// Light shadow for gentle elevation and hover states. - /// Suitable for interactive elements and small modals. - static const shadow2 = BoxShadowToken('fortal.shadow.2'); - - /// Shadow-2 painted on SegmentedControl's fixed one-pixel inset shape. - static const segmentedControlClassicIndicatorShadows = - RemixBoxShadowListToken( - 'fortal.segmented-control.classic.indicator-shadows', - ); - - /// Shadow level 3 - Medium elevation. - /// - /// Moderate shadow for clear visual separation. - /// Good for dropdowns, tooltips, and floating elements. - static const shadow3 = BoxShadowToken('fortal.shadow.3'); - - /// Shadow level 4 - High elevation. - /// - /// Prominent shadow for important floating content. - /// Suitable for modal dialogs and important overlays. - static const shadow4 = BoxShadowToken('fortal.shadow.4'); - - /// Shadow level 5 - Very high elevation. - /// - /// Strong shadow for primary modals and major overlays. - /// Creates clear hierarchy and focus on important content. - static const shadow5 = BoxShadowToken('fortal.shadow.5'); - - /// Shadow level 6 - Maximum elevation. - /// - /// Maximum shadow depth for critical dialogs and notifications. - /// Ensures content appears above all other interface elements. - static const shadow6 = BoxShadowToken('fortal.shadow.6'); - - /// Card classic outer and inset-pseudo-element shadow lists. - static const cardClassicOuterShadows = RemixBoxShadowListToken( - 'fortal.card.classic.outer-shadows', - ); - static const cardClassicInnerShadows = RemixBoxShadowListToken( - 'fortal.card.classic.inner-shadows', - ); - static const cardClassicHoverOuterShadows = RemixBoxShadowListToken( - 'fortal.card.classic.hover.outer-shadows', - ); - static const cardClassicHoverInnerShadows = RemixBoxShadowListToken( - 'fortal.card.classic.hover.inner-shadows', - ); - static const cardClassicActiveOuterShadows = RemixBoxShadowListToken( - 'fortal.card.classic.active.outer-shadows', - ); - static const cardClassicActiveInnerShadows = RemixBoxShadowListToken( - 'fortal.card.classic.active.inner-shadows', - ); - - /// Mode-aware inset layers for a classic Select trigger. - static const selectTriggerClassicShadows = RemixBoxShadowListToken( - 'fortal.select.trigger.classic.shadows', - ); - - /// Mode-aware open/hover layers for a classic Select trigger. - static const selectTriggerClassicHoverShadows = RemixBoxShadowListToken( - 'fortal.select.trigger.classic.hover-shadows', - ); - - /// Mode-aware disabled layers shared by classic button-shaped controls. - static const baseButtonClassicDisabledShadows = RemixBoxShadowListToken( - 'fortal.base-button.classic.disabled.shadows', - ); - - /// Mode-aware classic Button/IconButton layers. - static const baseButtonClassicShadows = RemixBoxShadowListToken( - 'fortal.base-button.classic.shadows', - ); - static const baseButtonClassicHighContrastShadows = RemixBoxShadowListToken( - 'fortal.base-button.classic.high-contrast.shadows', - ); - static const baseButtonClassicActiveShadows = RemixBoxShadowListToken( - 'fortal.base-button.classic.active.shadows', - ); - static const baseButtonClassicActiveHighContrastShadows = - RemixBoxShadowListToken( - 'fortal.base-button.classic.active.high-contrast.shadows', - ); - static const baseButtonClassicAfterInset = DoubleToken( - 'fortal.base-button.classic.after-inset', - ); - static const baseButtonGhostPaddingY3 = DoubleToken( - 'fortal.base-button.ghost.padding-y.3', - ); - static const baseButtonGhostMarginX12 = DoubleToken( - 'fortal.base-button.ghost.margin-x.1-2', - ); - static const baseButtonGhostMarginY12 = DoubleToken( - 'fortal.base-button.ghost.margin-y.1-2', - ); - static const baseButtonGhostMarginX3 = DoubleToken( - 'fortal.base-button.ghost.margin-x.3', - ); - static const baseButtonGhostMarginY3 = DoubleToken( - 'fortal.base-button.ghost.margin-y.3', - ); - static const baseButtonGhostMarginX4 = DoubleToken( - 'fortal.base-button.ghost.margin-x.4', - ); - static const baseButtonGhostMarginY4 = DoubleToken( - 'fortal.base-button.ghost.margin-y.4', - ); - static const iconButtonGhostPadding2 = DoubleToken( - 'fortal.icon-button.ghost.padding.2', - ); - static const iconButtonGhostMargin1 = DoubleToken( - 'fortal.icon-button.ghost.margin.1', - ); - static const iconButtonGhostMargin2 = DoubleToken( - 'fortal.icon-button.ghost.margin.2', - ); - static const iconButtonGhostMargin3 = DoubleToken( - 'fortal.icon-button.ghost.margin.3', - ); - static const iconButtonGhostMargin4 = DoubleToken( - 'fortal.icon-button.ghost.margin.4', - ); - static const cardGhostMargin1 = DoubleToken('fortal.card.ghost.margin.1'); - static const cardGhostMargin2 = DoubleToken('fortal.card.ghost.margin.2'); - static const cardGhostMargin3 = DoubleToken('fortal.card.ghost.margin.3'); - static const cardGhostMargin4 = DoubleToken('fortal.card.ghost.margin.4'); - static const cardGhostMargin5 = DoubleToken('fortal.card.ghost.margin.5'); - - // ============================================================================ - // BORDER AND STROKE WIDTHS - // ============================================================================ - - /// Standard border width (1px). - /// - /// Default border thickness for most components like inputs, - /// cards, and dividers. Provides clear boundaries without visual weight. - static const borderWidth1 = SpaceToken('fortal.border.width.1'); - - /// Thick border width (2px). - /// - /// Heavier border for emphasis, selected states, and components - /// that need stronger visual definition. - static const borderWidth2 = SpaceToken('fortal.border.width.2'); - - /// Focus ring border width (2px). - /// - /// Standard width for focus outlines to ensure accessibility - /// compliance and clear keyboard navigation feedback. - static const focusRingWidth = SpaceToken('fortal.focus.ring.width'); - - /// Focus ring offset distance from element edge. - /// - /// Space between the component border and focus ring, - /// ensuring the focus indicator doesn't interfere with the element. - static const focusRingOffset = SpaceToken('fortal.focus.ring.offset'); - - // ============================================================================ - // TYPOGRAPHY SCALE (9 LEVELS) - // ============================================================================ - // - // Text sizes with carefully tuned line heights and letter spacing - // for optimal readability across all scales. - // - - /// Text size 1 - 12px (Small labels and metadata). - /// - /// Smallest readable text for labels, captions, and secondary metadata. - /// Includes tight letter spacing for improved legibility at small sizes. - static const text1 = TextStyleToken('fortal.text.1'); - - /// Text size 2 - 14px (Standard UI text). - /// - /// Default size for most interface text including buttons, - /// form labels, and secondary content. - static const text2 = TextStyleToken('fortal.text.2'); - - /// Text size 3 - 16px (Body text and primary content). - /// - /// Ideal for body text and primary content. Provides excellent - /// readability for extended reading on all device types. - static const text3 = TextStyleToken('fortal.text.3'); - - /// Accordion size-2 text (15px with a 20px line box at 100% scaling). - /// - /// Accordion is a Fortal extension, so this intermediate size is kept - /// separate from the upstream Radix typography scale. - static const accordionText2 = TextStyleToken('fortal.accordion.text.2'); - - /// Text size 4 - 18px (Prominent body text). - /// - /// For important content that needs more visual weight than - /// standard body text but isn't quite a heading. - static const text4 = TextStyleToken('fortal.text.4'); - - /// Text size 5 - 20px (Small headings). - /// - /// For minor headings, subheadings, and content that needs - /// to stand out from body text. - static const text5 = TextStyleToken('fortal.text.5'); - - /// Text size 6 - 24px (Medium headings). - /// - /// Standard heading size for section titles and important content. - /// Good balance between prominence and page economy. - static const text6 = TextStyleToken('fortal.text.6'); - - /// Text size 7 - 28px (Large headings). - /// - /// For major page headings and important announcements. - /// Creates strong visual hierarchy and draws attention. - static const text7 = TextStyleToken('fortal.text.7'); - - /// Text size 8 - 35px (Extra large headings). - /// - /// For hero text, page titles, and major content sections. - /// Strong negative letter spacing improves appearance at large sizes. - static const text8 = TextStyleToken('fortal.text.8'); - - /// Text size 9 - 60px (Display text). - /// - /// Maximum text size for hero sections and display typography. - /// Includes significant negative letter spacing and tight line height. - static const text9 = TextStyleToken('fortal.text.9'); - - // ============================================================================ - // FONT WEIGHT TOKENS - // ============================================================================ - - /// Light font weight (300). - /// - /// Optional lighter weight occasionally used in display typography or - /// subdued text. Provided for parity with Radix token set. - static const fontWeightLight = FontWeightToken('fortal.font.weight.light'); - - /// Regular font weight (400). - /// - /// Standard weight for body text and most interface elements. - /// Provides good readability without visual strain. - static const fontWeightRegular = FontWeightToken( - 'fortal.font.weight.regular', - ); - - /// Medium font weight (500). - /// - /// Slightly heavier than regular for UI elements that need - /// more visual weight, like active states and button text. - static const fontWeightMedium = FontWeightToken('fortal.font.weight.medium'); - - /// Bold font weight (700). - /// - /// For headings and content that needs strong emphasis. - /// Provides clear hierarchy without being too heavy. - static const fontWeightBold = FontWeightToken('fortal.font.weight.bold'); - - // ============================================================================ - // ANIMATION DURATIONS - // ============================================================================ - - /// Fast animation duration (100ms). - /// - /// For quick micro-interactions like hover states and button presses. - /// Provides immediate feedback without feeling sluggish. - static const transitionFast = DurationToken('fortal.transition.fast'); - - /// Slow animation duration (300ms). - /// - /// For more substantial transitions like modal appearances, - /// page transitions, and complex state changes. - static const transitionSlow = DurationToken('fortal.transition.slow'); - - /// One leg of the Radix Skeleton pulse. - static const skeletonPulseDuration = DurationToken( - 'fortal.skeleton.pulse-duration', - ); -} - -/// Builds the token map for a Fortal scope. Used by [FortalScope]. -Map _buildFortalScopeTokens(FortalThemeData theme) { - final tokens = resolveFortalTokens(theme); - final scaling = theme.scaling.factor; - final shadows = buildFortalShadows(isDark: theme.isDark, colors: tokens); - - final colorTokens = { - // Role and functional tokens - FortalTokens.colorBackground: tokens.colorBackground, - FortalTokens.colorSurface: tokens.colorSurface, - FortalTokens.segmentedControlIndicatorBackground: theme.isDark - ? tokens.gray.scale.alphaStep(3) - : tokens.colorBackground, - FortalTokens.colorPanelSolid: tokens.colorPanelSolid, - FortalTokens.colorPanelTranslucent: tokens.colorPanelTranslucent, - FortalTokens.colorPanel: theme.panelBackground == .solid - ? tokens.colorPanelSolid - : tokens.colorPanelTranslucent, - FortalTokens.colorOverlay: tokens.colorOverlay, - FortalTokens.sliderHighContrastOverlay: theme.isDark - ? const Color(0x00000000) - : tokens.blackAlpha[8]!, - FortalTokens.error3: (theme.isDark ? radix.red.dark : radix.red.light).scale - .step(3), - FortalTokens.error7: (theme.isDark ? radix.red.dark : radix.red.light).scale - .step(7), - FortalTokens.error8: (theme.isDark ? radix.red.dark : radix.red.light).scale - .step(8), - FortalTokens.error9: (theme.isDark ? radix.red.dark : radix.red.light).scale - .step(9), - FortalTokens.error11: (theme.isDark ? radix.red.dark : radix.red.light) - .scale - .step(11), - FortalTokens.error12: (theme.isDark ? radix.red.dark : radix.red.light) - .scale - .step(12), - FortalTokens.errorA7: (theme.isDark ? radix.red.dark : radix.red.light) - .scale - .alphaStep(7), - ..._accentColorTokens(tokens), - // Gray steps - FortalTokens.gray1: tokens.gray.scale.step(1), - FortalTokens.gray2: tokens.gray.scale.step(2), - FortalTokens.gray3: tokens.gray.scale.step(3), - FortalTokens.gray4: tokens.gray.scale.step(4), - FortalTokens.gray5: tokens.gray.scale.step(5), - FortalTokens.gray6: tokens.gray.scale.step(6), - FortalTokens.gray7: tokens.gray.scale.step(7), - FortalTokens.gray8: tokens.gray.scale.step(8), - FortalTokens.gray9: tokens.gray.scale.step(9), - FortalTokens.gray10: tokens.gray.scale.step(10), - FortalTokens.gray11: tokens.gray.scale.step(11), - FortalTokens.gray12: tokens.gray.scale.step(12), - // Gray role tokens (from resolved colors) - FortalTokens.graySurface: tokens.gray.surface, - FortalTokens.grayIndicator: tokens.gray.indicator, - FortalTokens.grayTrack: tokens.gray.track, - FortalTokens.grayContrast: tokens.gray.contrast, - // Gray alpha a1..a12 - FortalTokens.grayA1: tokens.gray.scale.alphaStep(1), - FortalTokens.grayA2: tokens.gray.scale.alphaStep(2), - FortalTokens.grayA3: tokens.gray.scale.alphaStep(3), - FortalTokens.grayA4: tokens.gray.scale.alphaStep(4), - FortalTokens.grayA5: tokens.gray.scale.alphaStep(5), - FortalTokens.grayA6: tokens.gray.scale.alphaStep(6), - FortalTokens.grayA7: tokens.gray.scale.alphaStep(7), - FortalTokens.grayA8: tokens.gray.scale.alphaStep(8), - FortalTokens.grayA9: tokens.gray.scale.alphaStep(9), - FortalTokens.grayA10: tokens.gray.scale.alphaStep(10), - FortalTokens.grayA11: tokens.gray.scale.alphaStep(11), - FortalTokens.grayA12: tokens.gray.scale.alphaStep(12), - // Neutral helpers derived from primitives - FortalTokens.blackA1: tokens.blackAlpha[1]!, - FortalTokens.blackA2: tokens.blackAlpha[2]!, - FortalTokens.blackA3: tokens.blackAlpha[3]!, - FortalTokens.blackA4: tokens.blackAlpha[4]!, - FortalTokens.blackA5: tokens.blackAlpha[5]!, - FortalTokens.blackA6: tokens.blackAlpha[6]!, - FortalTokens.blackA7: tokens.blackAlpha[7]!, - FortalTokens.blackA8: tokens.blackAlpha[8]!, - FortalTokens.blackA9: tokens.blackAlpha[9]!, - FortalTokens.blackA10: tokens.blackAlpha[10]!, - FortalTokens.blackA11: tokens.blackAlpha[11]!, - FortalTokens.blackA12: tokens.blackAlpha[12]!, - FortalTokens.whiteA1: tokens.whiteAlpha[1]!, - FortalTokens.whiteA2: tokens.whiteAlpha[2]!, - FortalTokens.whiteA3: tokens.whiteAlpha[3]!, - FortalTokens.whiteA4: tokens.whiteAlpha[4]!, - FortalTokens.whiteA5: tokens.whiteAlpha[5]!, - FortalTokens.whiteA6: tokens.whiteAlpha[6]!, - FortalTokens.whiteA7: tokens.whiteAlpha[7]!, - FortalTokens.whiteA8: tokens.whiteAlpha[8]!, - FortalTokens.whiteA9: tokens.whiteAlpha[9]!, - FortalTokens.whiteA10: tokens.whiteAlpha[10]!, - FortalTokens.whiteA11: tokens.whiteAlpha[11]!, - FortalTokens.whiteA12: tokens.whiteAlpha[12]!, - FortalTokens.shadowStroke: tokens.shadowStroke, - FortalTokens.grayStroke3: mixOklabPremultiplied( - tokens.gray.scale.alphaStep(3), - tokens.gray.scale.step(3), - 0.25, - ), - FortalTokens.grayStroke4: mixOklabPremultiplied( - tokens.gray.scale.alphaStep(4), - tokens.gray.scale.step(4), - 0.25, - ), - FortalTokens.grayStroke5: mixOklabPremultiplied( - tokens.gray.scale.alphaStep(5), - tokens.gray.scale.step(5), - 0.25, - ), - FortalTokens.grayStroke6: mixOklabPremultiplied( - tokens.gray.scale.alphaStep(6), - tokens.gray.scale.step(6), - 0.25, - ), - FortalTokens.grayStroke7: mixOklabPremultiplied( - tokens.gray.scale.alphaStep(7), - tokens.gray.scale.step(7), - 0.25, - ), - FortalTokens.dataTableBorder: mixOklabPremultiplied( - tokens.gray.scale.alphaStep(5), - tokens.gray.scale.step(6), - 0.5, - ), - }; - - // Build base tokens map - final allTokens = { - ...colorTokens, - // Defaults (may be overridden by JSON tokens below) - FortalTokens.panelBlur: - theme.panelBackground == FortalPanelBackground.translucent ? 64.0 : 0.0, - FortalTokens.space1: 4.0 * scaling, - FortalTokens.space2: 8.0 * scaling, - FortalTokens.space3: 12.0 * scaling, - FortalTokens.space4: 16.0 * scaling, - FortalTokens.space5: 24.0 * scaling, - FortalTokens.space6: 32.0 * scaling, - FortalTokens.space7: 40.0 * scaling, - FortalTokens.space8: 48.0 * scaling, - FortalTokens.space9: 64.0 * scaling, - FortalTokens.spinnerSize3: 20.0 * scaling, - FortalTokens.dataTableRowHeight1: 36.0 * scaling, - FortalTokens.dataTableRowHeight2: 44.0 * scaling, - FortalTokens.toggleGap1: 2.0 * scaling, - FortalTokens.toggleGap3: 6.0 * scaling, - FortalTokens.avatarSize6: 80.0 * scaling, - FortalTokens.avatarSize7: 96.0 * scaling, - FortalTokens.avatarSize8: 128.0 * scaling, - FortalTokens.avatarSize9: 160.0 * scaling, - FortalTokens.avatarIconSize1: 12.0 * scaling, - FortalTokens.avatarIconSize2: 16.0 * scaling, - FortalTokens.avatarIconSize3: 20.0 * scaling, - FortalTokens.avatarIconSize4: 24.0 * scaling, - FortalTokens.avatarIconSize5: 32.0 * scaling, - FortalTokens.avatarIconSize6: 40.0 * scaling, - FortalTokens.avatarIconSize7: 48.0 * scaling, - FortalTokens.avatarIconSize8: 64.0 * scaling, - FortalTokens.avatarIconSize9: 80.0 * scaling, - FortalTokens.badgePaddingX1: 6.0 * scaling, - FortalTokens.badgePaddingY1: 2.0 * scaling, - FortalTokens.badgePaddingX3: 10.0 * scaling, - FortalTokens.checkboxSize1: 14.0 * scaling, - FortalTokens.checkboxSize3: 20.0 * scaling, - FortalTokens.checkboxIndicatorSize1: 9.0 * scaling, - FortalTokens.checkboxIndicatorSize2: 10.0 * scaling, - FortalTokens.checkboxIndicatorSize3: 12.0 * scaling, - FortalTokens.checkboxGroupItemGap1: 6.0 * scaling, - FortalTokens.checkboxGroupItemGap2: 7.0 * scaling, - FortalTokens.checkboxGroupItemGap3: 8.0 * scaling, - FortalTokens.radioIndicatorSize1: 5.6 * scaling, - FortalTokens.radioIndicatorSize2: 6.4 * scaling, - FortalTokens.radioIndicatorSize3: 8.0 * scaling, - FortalTokens.checkboxRadius1: _scaledRadiusToken( - theme.radius, - scaling, - 3.0 * 0.875, - ), - FortalTokens.checkboxRadius3: _scaledRadiusToken( - theme.radius, - scaling, - 3.0 * 1.25, - ), - FortalTokens.switchHeight2: 20.0 * scaling, - FortalTokens.switchWidth1: 28.0 * scaling, - FortalTokens.switchWidth2: 35.0 * scaling, - FortalTokens.switchWidth3: 42.0 * scaling, - FortalTokens.switchThumbSize1: 16.0 * scaling - 2.0, - FortalTokens.switchThumbSize2: 20.0 * scaling - 2.0, - FortalTokens.switchThumbSize3: 24.0 * scaling - 2.0, - FortalTokens.progressHeight2: 6.0 * scaling, - FortalTokens.sliderTrackSize1: 6.0 * scaling, - FortalTokens.sliderTrackSize2: 8.0 * scaling, - FortalTokens.sliderTrackSize3: 10.0 * scaling, - FortalTokens.sliderThumbSize1: 13.0 * scaling, - FortalTokens.sliderThumbSize2: 16.0 * scaling, - FortalTokens.sliderThumbSize3: 19.0 * scaling, - FortalTokens.textFieldPadding1: 6.0 * scaling, - FortalTokens.textFieldPadding2: 8.0 * scaling, - FortalTokens.textFieldPadding3: 12.0 * scaling, - FortalTokens.textAreaMinHeight3: 80.0, - FortalTokens.dataListRowGap3: 20.0 * scaling, - FortalTokens.dataListLabelMinWidth: 120.0, - FortalTokens.tabInnerPaddingY1: 2.0 * scaling, - FortalTokens.tabActiveLetterSpacing1: -0.12 * scaling, - FortalTokens.tabActiveLetterSpacing2: -0.14 * scaling, - FortalTokens.selectSpace1Half: 6.0 * scaling, - FortalTokens.selectIndicatorWidth1: 20.0 * scaling, - FortalTokens.selectIndicatorSize1: 8.0 * scaling, - FortalTokens.selectIndicatorSize2: 10.0 * scaling, - FortalTokens.selectGhostMarginX12: -8.0 * scaling, - FortalTokens.selectGhostMarginY12: -4.0 * scaling, - FortalTokens.selectGhostMarginX3: -12.0 * scaling, - FortalTokens.selectGhostMarginY3: -6.0 * scaling, - ..._radiusTokensFor(theme.radius, scaling), - - // Exact layered Radix shadow tokens, resolved for the active color scales. - ...shadows, - FortalTokens.sliderClassicDisabledTrackShadows: _scaleShadowOpacity( - shadows[FortalTokens.shadow1Layers]! as List, - 0.5, - ), - FortalTokens.cardClassicOuterShadows: _cardClassicShadows( - tokens, - isDark: theme.isDark, - layer: .outer, - state: .idle, - ), - FortalTokens.cardClassicInnerShadows: _cardClassicShadows( - tokens, - isDark: theme.isDark, - layer: .inner, - state: .idle, - ), - FortalTokens.cardClassicHoverOuterShadows: _cardClassicShadows( - tokens, - isDark: theme.isDark, - layer: .outer, - state: .hovered, - ), - FortalTokens.cardClassicHoverInnerShadows: _cardClassicShadows( - tokens, - isDark: theme.isDark, - layer: .inner, - state: .hovered, - ), - FortalTokens.cardClassicActiveOuterShadows: _cardClassicShadows( - tokens, - isDark: theme.isDark, - layer: .outer, - state: .active, - ), - FortalTokens.cardClassicActiveInnerShadows: _cardClassicShadows( - tokens, - isDark: theme.isDark, - layer: .inner, - state: .active, - ), - FortalTokens.selectTriggerClassicShadows: _selectClassicShadows( - tokens, - isDark: theme.isDark, - ), - FortalTokens.selectTriggerClassicHoverShadows: [ - _insetShadow(tokens.gray.scale.alphaStep(3), spread: 1), - ..._selectClassicShadows(tokens, isDark: theme.isDark), - ], - FortalTokens.baseButtonClassicDisabledShadows: - _baseButtonClassicDisabledShadows(tokens, isDark: theme.isDark), - FortalTokens.baseButtonClassicShadows: _baseButtonClassicShadows( - tokens, - isDark: theme.isDark, - highContrast: false, - ), - FortalTokens.baseButtonClassicHighContrastShadows: - _baseButtonClassicShadows( - tokens, - isDark: theme.isDark, - highContrast: true, - ), - FortalTokens.baseButtonClassicActiveShadows: - _baseButtonClassicActiveShadows(tokens, highContrast: false), - FortalTokens.baseButtonClassicActiveHighContrastShadows: - _baseButtonClassicActiveShadows(tokens, highContrast: true), - FortalTokens.baseButtonClassicAfterInset: theme.isDark ? 1.0 : 2.0, - FortalTokens.baseButtonGhostPaddingY3: 6.0 * scaling, - FortalTokens.baseButtonGhostMarginX12: -8.0 * scaling, - FortalTokens.baseButtonGhostMarginY12: -4.0 * scaling, - FortalTokens.baseButtonGhostMarginX3: -12.0 * scaling, - FortalTokens.baseButtonGhostMarginY3: -6.0 * scaling, - FortalTokens.baseButtonGhostMarginX4: -16.0 * scaling, - FortalTokens.baseButtonGhostMarginY4: -8.0 * scaling, - FortalTokens.iconButtonGhostPadding2: 6.0 * scaling, - FortalTokens.iconButtonGhostMargin1: -4.0 * scaling, - FortalTokens.iconButtonGhostMargin2: -6.0 * scaling, - FortalTokens.iconButtonGhostMargin3: -8.0 * scaling, - FortalTokens.iconButtonGhostMargin4: -12.0 * scaling, - FortalTokens.cardGhostMargin1: -12.0 * scaling, - FortalTokens.cardGhostMargin2: -16.0 * scaling, - FortalTokens.cardGhostMargin3: -24.0 * scaling, - FortalTokens.cardGhostMargin4: -32.0 * scaling, - FortalTokens.cardGhostMargin5: -48.0 * scaling, - FortalTokens.borderWidth1: 1.0, - FortalTokens.borderWidth2: 2.0, - FortalTokens.focusRingWidth: 2.0, - FortalTokens.focusRingOffset: 2.0, - FortalTokens.text1: TextStyle( - fontSize: 12.0 * scaling, - letterSpacing: 0.0025 * 12.0 * scaling, - height: 16.0 / 12.0, - ), - FortalTokens.text2: TextStyle( - fontSize: 14.0 * scaling, - letterSpacing: 0.0, - height: 20.0 / 14.0, - ), - FortalTokens.text3: TextStyle( - fontSize: 16.0 * scaling, - letterSpacing: 0.0, - height: 24.0 / 16.0, - ), - FortalTokens.accordionText2: TextStyle( - fontSize: 15.0 * scaling, - letterSpacing: 0.0, - height: 20.0 / 15.0, - ), - FortalTokens.text4: TextStyle( - fontSize: 18.0 * scaling, - letterSpacing: -0.0025 * 18.0 * scaling, - height: 26.0 / 18.0, - ), - FortalTokens.text5: TextStyle( - fontSize: 20.0 * scaling, - letterSpacing: -0.005 * 20.0 * scaling, - height: 28.0 / 20.0, - ), - FortalTokens.text6: TextStyle( - fontSize: 24.0 * scaling, - letterSpacing: -0.00625 * 24.0 * scaling, - height: 30.0 / 24.0, - ), - FortalTokens.text7: TextStyle( - fontSize: 28.0 * scaling, - letterSpacing: -0.0075 * 28.0 * scaling, - height: 36.0 / 28.0, - ), - FortalTokens.text8: TextStyle( - fontSize: 35.0 * scaling, - letterSpacing: -0.01 * 35.0 * scaling, - height: 40.0 / 35.0, - ), - FortalTokens.text9: TextStyle( - fontSize: 60.0 * scaling, - letterSpacing: -0.025 * 60.0 * scaling, - height: 1.0, - ), - FortalTokens.avatarFallback1One: _avatarFallbackText( - fontSize: 14, - letterSpacing: 0.0025 * 12, - scaling: scaling, - ), - FortalTokens.avatarFallback1Two: _avatarFallbackText( - fontSize: 12, - letterSpacing: 0.0025 * 12, - scaling: scaling, - ), - FortalTokens.avatarFallback2One: _avatarFallbackText( - fontSize: 16, - letterSpacing: 0, - scaling: scaling, - ), - FortalTokens.avatarFallback2Two: _avatarFallbackText( - fontSize: 14, - letterSpacing: 0, - scaling: scaling, - ), - FortalTokens.avatarFallback3One: _avatarFallbackText( - fontSize: 18, - letterSpacing: 0, - scaling: scaling, - ), - FortalTokens.avatarFallback3Two: _avatarFallbackText( - fontSize: 16, - letterSpacing: 0, - scaling: scaling, - ), - FortalTokens.avatarFallback4One: _avatarFallbackText( - fontSize: 20, - letterSpacing: -0.0025 * 18, - scaling: scaling, - ), - FortalTokens.avatarFallback4Two: _avatarFallbackText( - fontSize: 18, - letterSpacing: -0.0025 * 18, - scaling: scaling, - ), - FortalTokens.avatarFallback5: _avatarFallbackText( - fontSize: 24, - letterSpacing: -0.00625 * 24, - scaling: scaling, - ), - FortalTokens.avatarFallback6: _avatarFallbackText( - fontSize: 28, - letterSpacing: -0.0075 * 28, - scaling: scaling, - ), - FortalTokens.avatarFallback7: _avatarFallbackText( - fontSize: 28, - letterSpacing: -0.0075 * 28, - scaling: scaling, - ), - FortalTokens.avatarFallback8: _avatarFallbackText( - fontSize: 35, - letterSpacing: -0.01 * 35, - scaling: scaling, - ), - FortalTokens.avatarFallback9: _avatarFallbackText( - fontSize: 60, - letterSpacing: -0.025 * 60, - scaling: scaling, - ), - - // Font weights (token values) - FortalTokens.fontWeightLight: FontWeight.w300, - FortalTokens.fontWeightRegular: FontWeight.w400, - FortalTokens.fontWeightMedium: FontWeight.w500, - // Match Radix Themes font weights (bold = 700) - FortalTokens.fontWeightBold: FontWeight.w700, - - // Durations (token values) - FortalTokens.transitionFast: Duration(milliseconds: 100), - FortalTokens.transitionSlow: Duration(milliseconds: 300), - FortalTokens.skeletonPulseDuration: Duration(milliseconds: 1000), - }; - - return allTokens; -} - -/// Establishes a courtesy default text run for bare [Text] descendants. -/// -/// `.radix-themes` is not only a token carrier upstream: `color.css` sets -/// `color: var(--gray-12)` in the same rule as the `data-has-background` fill, -/// and `typography.css` pins the root to `--default-font-size` -/// (`--font-size-3`), `--default-line-height`, `--default-letter-spacing`, and -/// `--default-font-weight`. Those resolve to exactly [FortalTokens.text3] plus -/// [FortalTokens.gray12] at regular weight. -/// -/// Fortal text recipes resolve and pin their own runs. This fallback keeps -/// deliberately bare [Text] descendants aligned with Radix's root typography -/// and neutral foreground. A nearer descendant `DefaultTextStyle` still wins -/// through Flutter's normal inheritance. -/// -/// Only the outermost [FortalScope] installs this. A nested scope re-scopes -/// tokens for its subtree and nothing more: upstream, `.radix-themes` inside -/// another `.radix-themes` still inherits `color` and the font properties from -/// its parent chain, and a nested scope that reinstalled the root run here -/// would silently replace whatever `DefaultTextStyle` the subtree sits in. -/// -/// The font family is deliberately left unset. Radix's `--default-font-family` -/// is the platform system stack, and a null family is Flutter's equivalent; -/// naming a concrete family here would pin every consumer to one typeface. -Widget _fortalRootTextStyle({ - required Map tokens, - required Widget child, -}) { - final root = tokens[FortalTokens.text3]! as TextStyle; - - return DefaultTextStyle( - style: root.copyWith( - color: tokens[FortalTokens.gray12]! as Color, - fontWeight: tokens[FortalTokens.fontWeightRegular]! as FontWeight, - ), - child: child, - ); -} - -TextStyle _avatarFallbackText({ - required double fontSize, - required double letterSpacing, - required double scaling, -}) => TextStyle( - fontSize: fontSize * scaling, - letterSpacing: letterSpacing * scaling, - height: 1, -); - -enum _CardShadowLayer { outer, inner } - -enum _CardShadowState { idle, hovered, active } - -List _cardClassicShadows( - FortalThemeColors colors, { - required bool isDark, - required _CardShadowLayer layer, - required _CardShadowState state, -}) { - final inner = layer == _CardShadowLayer.inner; - final shapeInset = inner ? 1.0 : 0.0; - final border = switch ((isDark, state)) { - (true, _) => mixOklabPremultiplied( - colors.gray.scale.alphaStep(6), - colors.gray.scale.step(6), - 0.25, - ), - (false, _CardShadowState.hovered) => mixOklabPremultiplied( - colors.gray.scale.alphaStep(4), - colors.gray.scale.step(4), - 0.25, - ), - (false, _) => mixOklabPremultiplied( - colors.gray.scale.alphaStep(3), - colors.gray.scale.step(3), - 0.25, - ), - }; - - RemixBoxShadow shadow( - Color color, { - Offset offset = Offset.zero, - double blur = 0, - required double spread, - }) => RemixBoxShadow( - color: color, - offset: offset, - blurRadius: blur, - spreadRadius: spread, - shapeInset: shapeInset, - ); - - if (state == _CardShadowState.hovered) { - if (isDark) { - return [ - shadow(border, spread: inner ? 1 : 0), - shadow(colors.gray.scale.alphaStep(4), blur: 1, spread: inner ? 1 : 0), - shadow( - colors.gray.scale.alphaStep(4), - blur: 1, - spread: inner ? -1 : -2, - ), - shadow( - colors.gray.scale.alphaStep(3), - blur: 3, - spread: inner ? -2 : -3, - ), - shadow( - colors.gray.scale.alphaStep(3), - blur: 12, - spread: inner ? -2 : -3, - ), - shadow( - colors.gray.scale.alphaStep(7), - blur: 16, - spread: inner ? -8 : -9, - ), - ]; - } - return [ - shadow(border, spread: inner ? 1 : 0), - shadow( - colors.blackAlpha[1]!, - offset: const Offset(0, 1), - blur: 1, - spread: inner ? 1 : 0, - ), - shadow( - colors.gray.scale.alphaStep(3), - offset: const Offset(0, 2), - blur: 1, - spread: inner ? -1 : -2, - ), - shadow( - colors.blackAlpha[1]!, - offset: const Offset(0, 2), - blur: 3, - spread: inner ? -2 : -3, - ), - shadow( - colors.gray.scale.alphaStep(3), - offset: const Offset(0, 3), - blur: 12, - spread: inner ? -4 : -5, - ), - shadow( - colors.blackAlpha[1]!, - offset: const Offset(0, 4), - blur: 16, - spread: inner ? -8 : -9, - ), - ]; - } - - final active = state == _CardShadowState.active; - final subtle = isDark ? colors.blackAlpha[3]! : colors.blackAlpha[1]!; - final middle = isDark - ? colors.blackAlpha[6]! - : colors.gray.scale.alphaStep(active ? 4 : 2); - final bottom = isDark ? colors.blackAlpha[5]! : colors.blackAlpha[1]!; - return [ - shadow(border, spread: inner ? 1 : 0), - shadow(const Color(0x00000000), spread: inner ? 1 : 0), - shadow(subtle, spread: inner ? 0.5 : 0), - shadow(middle, offset: const Offset(0, 1), blur: 1, spread: inner ? 0 : -1), - shadow( - isDark ? colors.blackAlpha[6]! : colors.blackAlpha[1]!, - offset: const Offset(0, 2), - blur: 1, - spread: inner ? -1 : -2, - ), - shadow(bottom, offset: const Offset(0, 1), blur: 3, spread: inner ? 0 : -1), - ]; -} - -RemixBoxShadow _insetShadow( - Color color, { - Offset offset = Offset.zero, - double blur = 0, - double spread = 0, - double shapeInset = 0, -}) => RemixBoxShadow( - kind: RemixBoxShadowKind.inset, - color: color, - offset: offset, - blurRadius: blur, - spreadRadius: spread, - shapeInset: shapeInset, -); - -List _selectClassicShadows( - FortalThemeColors colors, { - required bool isDark, -}) { - if (isDark) { - return [ - _insetShadow(colors.whiteAlpha[4]!, spread: 1), - _insetShadow(colors.whiteAlpha[4]!, offset: const Offset(0, 1), blur: 1), - _insetShadow(colors.blackAlpha[9]!, offset: const Offset(0, -1), blur: 1), - ]; - } - return [ - _insetShadow(colors.gray.scale.alphaStep(5), spread: 1), - _insetShadow(colors.whiteAlpha[11]!, offset: const Offset(0, 2), blur: 1), - _insetShadow( - colors.gray.scale.alphaStep(4), - offset: const Offset(0, -2), - blur: 1, - ), - ]; -} - -List _baseButtonClassicDisabledShadows( - FortalThemeColors colors, { - required bool isDark, -}) { - if (isDark) { - return [ - _insetShadow(colors.gray.scale.alphaStep(5), spread: 1), - _insetShadow( - colors.gray.scale.alphaStep(2), - offset: const Offset(0, 4), - blur: 2, - spread: -2, - ), - _insetShadow( - colors.gray.scale.alphaStep(5), - offset: const Offset(0, 1), - blur: 1, - ), - _insetShadow(colors.blackAlpha[3]!, offset: const Offset(0, -1), blur: 1), - _insetShadow(colors.gray.scale.alphaStep(2), spread: 1), - ]; - } - return [ - _insetShadow(colors.gray.scale.alphaStep(4), spread: 1), - _insetShadow( - colors.gray.scale.alphaStep(3), - offset: const Offset(0, -2), - blur: 1, - ), - _insetShadow( - colors.whiteAlpha[9]!, - offset: const Offset(0, 4), - blur: 2, - spread: -2, - ), - _insetShadow( - colors.whiteAlpha[9]!, - offset: const Offset(0, 2), - blur: 1, - spread: -1, - ), - ]; -} - -List _baseButtonClassicShadows( - FortalThemeColors colors, { - required bool isDark, - required bool highContrast, -}) { - final accent = highContrast - ? colors.accent.scale.step(12) - : colors.accent.scale.step(9); - if (isDark) { - return [ - _insetShadow( - colors.whiteAlpha[4]!, - offset: const Offset(0, 2), - blur: 3, - spread: -1, - shapeInset: 1, - ), - _insetShadow(colors.whiteAlpha[2]!, spread: 1), - _insetShadow( - colors.whiteAlpha[3]!, - offset: const Offset(0, 4), - blur: 2, - spread: -2, - ), - _insetShadow(colors.whiteAlpha[6]!, offset: const Offset(0, 1), blur: 1), - _insetShadow(colors.blackAlpha[6]!, offset: const Offset(0, -1), blur: 1), - _insetShadow(accent, spread: 1), - ]; - } - return [ - _insetShadow( - colors.whiteAlpha[4]!, - offset: const Offset(0, 2), - blur: 3, - spread: -1, - shapeInset: 2, - ), - _insetShadow(colors.gray.scale.alphaStep(4), spread: 1), - _insetShadow( - colors.gray.scale.alphaStep(3), - offset: const Offset(0, -2), - blur: 1, - ), - _insetShadow(accent, spread: 1), - _insetShadow( - colors.whiteAlpha[9]!, - offset: const Offset(0, 4), - blur: 2, - spread: -2, - ), - _insetShadow( - colors.whiteAlpha[9]!, - offset: const Offset(0, 2), - blur: 1, - spread: -1, - ), - ]; -} - -List _baseButtonClassicActiveShadows( - FortalThemeColors colors, { - required bool highContrast, -}) { - final accent = highContrast - ? colors.accent.scale.step(12) - : colors.accent.scale.step(9); - return [ - _insetShadow( - colors.gray.scale.alphaStep(4), - offset: const Offset(0, 4), - blur: 2, - spread: -2, - ), - _insetShadow( - colors.gray.scale.alphaStep(7), - offset: const Offset(0, 1), - blur: 1, - ), - _insetShadow(colors.gray.scale.alphaStep(5), spread: 1), - _insetShadow(accent, spread: 1), - _insetShadow( - colors.gray.scale.alphaStep(3), - offset: const Offset(0, 3), - blur: 2, - ), - _insetShadow(colors.whiteAlpha[7]!, spread: 1), - _insetShadow(colors.whiteAlpha[5]!, offset: const Offset(0, -2), blur: 1), - ]; -} - -Map _radiusTokensFor(FortalRadius radius, double scaling) { - final factor = _radiusFactor(radius); - final thumb = switch (radius) { - .none || .small => const Radius.circular(0.5), - .medium || .large || .full => const Radius.circular(9999.0), - }; - Radius scaled(double base) => Radius.circular(base * factor * scaling); - Radius larger(Radius first, Radius second) => Radius.elliptical( - first.x > second.x ? first.x : second.x, - first.y > second.y ? first.y : second.y, - ); - final radius1 = scaled(3.0); - final radius2 = scaled(4.0); - final radius3 = scaled(6.0); - final radius4 = scaled(8.0); - final radius5 = scaled(12.0); - final radius6 = scaled(16.0); - final full = radius == .full ? const Radius.circular(9999.0) : Radius.zero; - Radius progressRadius(double height) { - final thumbBase = switch (radius) { - .none || .small => 0.5, - .medium || .large || .full => 9999.0, - }; - return Radius.circular(math.max(factor * height / 3, factor * thumbBase)); - } - - return { - FortalTokens.radius1: radius1, - FortalTokens.radius2: radius2, - FortalTokens.radius3: radius3, - FortalTokens.radius4: radius4, - FortalTokens.radius5: radius5, - FortalTokens.radius6: radius6, - FortalTokens.radiusFull: full, - FortalTokens.radiusThumb: thumb, - FortalTokens.radiusCircle: const Radius.circular(9999.0), - FortalTokens.radius1OrFull: larger(radius1, full), - FortalTokens.radius2OrFull: larger(radius2, full), - FortalTokens.radius3OrFull: larger(radius3, full), - FortalTokens.radius4OrFull: larger(radius4, full), - FortalTokens.radius5OrFull: larger(radius5, full), - FortalTokens.radius6OrFull: larger(radius6, full), - FortalTokens.radius1OrThumb: larger(radius1, thumb), - FortalTokens.radius2OrThumb: larger(radius2, thumb), - FortalTokens.progressRadius1: progressRadius(4.0 * scaling), - FortalTokens.progressRadius2: progressRadius(6.0 * scaling), - FortalTokens.progressRadius3: progressRadius(8.0 * scaling), - FortalTokens.sliderTrackRadius1: progressRadius(6.0 * scaling), - FortalTokens.sliderTrackRadius2: progressRadius(8.0 * scaling), - FortalTokens.sliderTrackRadius3: progressRadius(10.0 * scaling), - }; -} - -List _scaleShadowOpacity( - List shadows, - double factor, -) => [ - for (final shadow in shadows) - RemixBoxShadow( - kind: shadow.kind, - color: shadow.color.withValues(alpha: shadow.color.a * factor), - offset: shadow.offset, - blurRadius: shadow.blurRadius, - spreadRadius: shadow.spreadRadius, - shapeInset: shadow.shapeInset, - ), -]; - -double _radiusFactor(FortalRadius radius) => switch (radius) { - .none => 0.0, - .small => 0.75, - .medium => 1.0, - .large || .full => 1.5, -}; - -Radius _scaledRadiusToken(FortalRadius radius, double scaling, double base) => - Radius.circular(base * scaling * _radiusFactor(radius)); - -Map _accentColorTokens(FortalThemeColors tokens) { - final scale = tokens.accent.scale; - - return { - FortalTokens.accentSurface: tokens.accent.surface, - FortalTokens.accentIndicator: tokens.accent.indicator, - FortalTokens.accentTrack: tokens.accent.track, - FortalTokens.accentContrast: tokens.accent.contrast, - FortalTokens.focus8: tokens.focus8, - FortalTokens.focusA5: tokens.focusA5, - FortalTokens.focusA8: tokens.focusA8, - FortalTokens.accent1: scale.step(1), - FortalTokens.accent2: scale.step(2), - FortalTokens.accent3: scale.step(3), - FortalTokens.accent4: scale.step(4), - FortalTokens.accent5: scale.step(5), - FortalTokens.accent6: scale.step(6), - FortalTokens.accent7: scale.step(7), - FortalTokens.accent8: scale.step(8), - FortalTokens.accent9: scale.step(9), - FortalTokens.accent10: scale.step(10), - FortalTokens.accent11: scale.step(11), - FortalTokens.accent12: scale.step(12), - FortalTokens.accentA1: scale.alphaStep(1), - FortalTokens.accentA2: scale.alphaStep(2), - FortalTokens.accentA3: scale.alphaStep(3), - FortalTokens.accentA4: scale.alphaStep(4), - FortalTokens.accentA5: scale.alphaStep(5), - FortalTokens.accentA6: scale.alphaStep(6), - FortalTokens.accentA7: scale.alphaStep(7), - FortalTokens.accentA8: scale.alphaStep(8), - FortalTokens.accentA9: scale.alphaStep(9), - FortalTokens.accentA10: scale.alphaStep(10), - FortalTokens.accentA11: scale.alphaStep(11), - FortalTokens.accentA12: scale.alphaStep(12), - }; -} - -/// Widget that provides Fortal design tokens to its subtree via [MixScope]. -/// -/// Use [FortalScope] at the root of your app (or around any subtree that uses -/// Fortal styles) so that [FortalTokens] resolve to actual values. -/// -/// ```dart -/// FortalScope( -/// child: WidgetsApp(...), -/// ) -/// ``` -class FortalScope extends StatelessWidget { - const FortalScope({ - super.key, - this.accent, - this.gray, - this.brightness, - this.panelBackground, - this.radius, - this.scaling, - this.hasBackground, - this.orderOfModifiers, - required this.child, - }); - - final FortalAccentColor? accent; - final FortalGrayColor? gray; - final Brightness? brightness; - final FortalPanelBackground? panelBackground; - final FortalRadius? radius; - final FortalScaling? scaling; - final bool? hasBackground; - final List? orderOfModifiers; - final Widget child; - - @override - Widget build(BuildContext context) { - final config = FortalThemeConfig( - accent: accent, - gray: gray, - brightness: brightness, - panelBackground: panelBackground, - radius: radius, - scaling: scaling, - hasBackground: hasBackground, - ); - final parent = FortalTheme.maybeOf(context); - final data = _resolveFortalTheme(config, parent: parent); - final tokens = _buildFortalScopeTokens(data); - Widget result = MixScope( - tokens: tokens, - orderOfModifiers: orderOfModifiers, - // Theme-root identity, not `hasBackground`, decides who owns the text - // run: a scope nested for its accent or scaling must leave the current - // run alone, while a root scope with `hasBackground: false` still - // establishes it. - child: parent == null - ? _fortalRootTextStyle(tokens: tokens, child: child) - : child, - ); - if (data.hasBackground) { - result = ColoredBox( - color: tokens[FortalTokens.colorBackground]! as Color, - child: result, - ); - } - - return FortalTheme( - data: data, - orderOfModifiers: orderOfModifiers, - child: result, - ); - } -} - -/// Available accent colors matching Radix Themes names. -enum FortalAccentColor { - gray, - mauve, - slate, - sage, - olive, - sand, - gold, - bronze, - brown, - yellow, - amber, - orange, - tomato, - red, - ruby, - crimson, - pink, - plum, - purple, - violet, - iris, - indigo, - blue, - cyan, - teal, - jade, - green, - grass, - lime, - mint, - sky, -} - -/// Available neutral gray families matching Radix Themes names. -enum FortalGrayColor { gray, mauve, slate, sage, olive, sand } - -/// Theme-level radius multipliers matching the Radix Themes presets. -enum FortalRadius { none, small, medium, large, full } - -/// Background treatment used by floating panels. -enum FortalPanelBackground { solid, translucent } - -/// Discrete theme scaling values supported by Radix Themes. -enum FortalScaling { - percent90(0.9), - percent95(0.95), - percent100(1.0), - percent105(1.05), - percent110(1.1); - - const FortalScaling(this.factor); - - /// Numeric multiplier represented by this preset. - final double factor; -} - -/// Partial theme values applied by a [FortalScope]. -@immutable -class FortalThemeConfig { - const FortalThemeConfig({ - this.accent, - this.gray, - this.brightness, - this.panelBackground, - this.radius, - this.scaling, - this.hasBackground, - }); - - final FortalAccentColor? accent; - final FortalGrayColor? gray; - final Brightness? brightness; - final FortalPanelBackground? panelBackground; - final FortalRadius? radius; - final FortalScaling? scaling; - final bool? hasBackground; - - bool get isDark => brightness == .dark; - - FortalThemeConfig copyWith({ - FortalAccentColor? accent, - FortalGrayColor? gray, - Brightness? brightness, - FortalPanelBackground? panelBackground, - FortalRadius? radius, - FortalScaling? scaling, - bool? hasBackground, - }) => FortalThemeConfig( - accent: accent ?? this.accent, - gray: gray ?? this.gray, - brightness: brightness ?? this.brightness, - panelBackground: panelBackground ?? this.panelBackground, - radius: radius ?? this.radius, - scaling: scaling ?? this.scaling, - hasBackground: hasBackground ?? this.hasBackground, - ); - - @override - bool operator ==(Object other) => - identical(this, other) || - other is FortalThemeConfig && - accent == other.accent && - gray == other.gray && - brightness == other.brightness && - panelBackground == other.panelBackground && - radius == other.radius && - scaling == other.scaling && - hasBackground == other.hasBackground; - - @override - int get hashCode => Object.hash( - accent, - gray, - brightness, - panelBackground, - radius, - scaling, - hasBackground, - ); - - Widget createScope({List? orderOfModifiers, required Widget child}) => - FortalScope( - accent: accent, - gray: gray, - brightness: brightness, - panelBackground: panelBackground, - radius: radius, - scaling: scaling, - hasBackground: hasBackground, - orderOfModifiers: orderOfModifiers, - child: child, - ); -} - -/// Fully resolved theme values inherited by a Fortal subtree. -@immutable -class FortalThemeData extends FortalThemeConfig { - const FortalThemeData({ - required this.accent, - required this.gray, - required this.brightness, - required this.panelBackground, - required this.radius, - required this.scaling, - required this.hasBackground, - }) : super( - accent: accent, - gray: gray, - brightness: brightness, - panelBackground: panelBackground, - radius: radius, - scaling: scaling, - hasBackground: hasBackground, - ); - - @override - final FortalAccentColor accent; - @override - final FortalGrayColor gray; - @override - final Brightness brightness; - @override - final FortalPanelBackground panelBackground; - @override - final FortalRadius radius; - @override - final FortalScaling scaling; - @override - final bool hasBackground; - - @override - bool get isDark => brightness == .dark; - - @override - FortalThemeData copyWith({ - FortalAccentColor? accent, - FortalGrayColor? gray, - Brightness? brightness, - FortalPanelBackground? panelBackground, - FortalRadius? radius, - FortalScaling? scaling, - bool? hasBackground, - }) => FortalThemeData( - accent: accent ?? this.accent, - gray: gray ?? this.gray, - brightness: brightness ?? this.brightness, - panelBackground: panelBackground ?? this.panelBackground, - radius: radius ?? this.radius, - scaling: scaling ?? this.scaling, - hasBackground: hasBackground ?? this.hasBackground, - ); - - @override - bool operator ==(Object other) => - identical(this, other) || - other is FortalThemeData && - accent == other.accent && - gray == other.gray && - brightness == other.brightness && - panelBackground == other.panelBackground && - radius == other.radius && - scaling == other.scaling && - hasBackground == other.hasBackground; - - @override - int get hashCode => Object.hash( - accent, - gray, - brightness, - panelBackground, - radius, - scaling, - hasBackground, - ); -} - -FortalThemeData _resolveFortalTheme( - FortalThemeConfig config, { - FortalThemeData? parent, -}) { - final accent = config.accent ?? parent?.accent ?? FortalAccentColor.indigo; - - return FortalThemeData( - accent: accent, - gray: config.gray ?? parent?.gray ?? FortalGrayColor.slate, - brightness: config.brightness ?? parent?.brightness ?? Brightness.light, - panelBackground: - config.panelBackground ?? - parent?.panelBackground ?? - FortalPanelBackground.translucent, - radius: config.radius ?? parent?.radius ?? FortalRadius.medium, - scaling: config.scaling ?? parent?.scaling ?? FortalScaling.percent100, - hasBackground: config.hasBackground ?? parent == null, - ); -} - -/// Makes the active [FortalThemeData] available to descendants. -class FortalTheme extends InheritedTheme { - const FortalTheme({ - super.key, - required this.data, - this.orderOfModifiers, - required super.child, - }); - - final FortalThemeData data; - final List? orderOfModifiers; - - /// Returns the closest resolved Fortal theme. - static FortalThemeData of(BuildContext context) { - final data = maybeOf(context); - if (data != null) return data; - throw FlutterError.fromParts([ - ErrorSummary('No FortalTheme found.'), - ErrorDescription( - '${context.widget.runtimeType} tried to read the Fortal theme, but no FortalScope was found above it.', - ), - context.describeElement('The context used was'), - ]); - } - - /// Returns the closest resolved Fortal theme, if one is available. - static FortalThemeData? maybeOf(BuildContext context) => - context.dependOnInheritedWidgetOfExactType()?.data; - - /// Rebuilds only the theme and its Mix tokens. - /// - /// The captured subtree's text run is *not* synthesized here. - /// `DefaultTextStyle` is itself an [InheritedTheme], so - /// `InheritedTheme.capture` already carries the actual nearest ambient run - /// across to the new route; installing the Radix root run alongside it would - /// overwrite that capture with a value the source context never had. - @override - Widget wrap(BuildContext context, Widget child) => FortalTheme( - data: data, - orderOfModifiers: orderOfModifiers, - child: MixScope( - tokens: _buildFortalScopeTokens(data), - orderOfModifiers: orderOfModifiers, - child: child, - ), - ); - - @override - bool updateShouldNotify(FortalTheme oldWidget) => data != oldWidget.data; -} diff --git a/packages/remix_fortal/lib/src/radix/icons.dart b/packages/remix_fortal/lib/src/icons.dart similarity index 100% rename from packages/remix_fortal/lib/src/radix/icons.dart rename to packages/remix_fortal/lib/src/icons.dart diff --git a/packages/remix_fortal/lib/src/radix/colors/colors.dart b/packages/remix_fortal/lib/src/radix/colors/colors.dart deleted file mode 100644 index 23fd97151..000000000 --- a/packages/remix_fortal/lib/src/radix/colors/colors.dart +++ /dev/null @@ -1,92 +0,0 @@ -library radix_colors; // moved to radix/colors/ - -import 'package:flutter/painting.dart'; - -part 'colors_generated.dart'; - -class RadixColor { - final RadixColorScale scale; - final Color surface; - final Color indicator; - final Color track; - final Color contrast; - - const RadixColor( - this.scale, - this.surface, - this.indicator, - this.track, - this.contrast, - ); -} - -class RadixColorTheme { - final RadixColor light; - final RadixColor dark; - - const RadixColorTheme(this.light, this.dark); -} - -class RadixColorScale { - final ColorSwatch solid; - final ColorSwatch alpha; - - const RadixColorScale(this.solid, this.alpha); - - // Semantic color accessors for better code readability - - /// The most subtle background color (step 1). - Color get appBackground => step(1); - - /// Subtle background with slightly more presence (step 2). - Color get subtleBackground => step(2); - - /// Default background for interactive components (step 3). - Color get componentBackground => step(3); - - /// Background color for components on hover (step 4). - Color get componentBackgroundHover => step(4); - - /// Background color for active/pressed components (step 5). - Color get componentBackgroundActive => step(5); - - /// Subtle border color for gentle separation (step 6). - Color get subtleBorder => step(6); - - /// Standard border color for components (step 7). - Color get componentBorder => step(7); - - /// Border color for hover and focus states (step 8). - Color get componentBorderHover => step(8); - - /// Primary solid background color (step 9). - Color get solidBackground => step(9); - - /// Solid background color on hover (step 10). - Color get solidBackgroundHover => step(10); - - /// Low contrast text color (step 11). - Color get lowContrastText => step(11); - - /// High contrast text color (step 12). - Color get highContrastText => step(12); - - /// Gets a solid color from the 12-step scale. - /// - /// Steps must be between 1 and 12. Falls back to step 9 if unavailable. - Color step(int n) { - assert(n >= 1 && n <= 12, 'Step must be between 1 and 12'); - - return solid[n] ?? solid[9]!; - } - - /// Gets a translucent color from the 12-step alpha scale. - /// - /// Alpha variants maintain saturation when composited. - /// Falls back to alpha step 9 if unavailable. - Color alphaStep(int n) { - assert(n >= 1 && n <= 12, 'Step must be between 1 and 12'); - - return alpha[n] ?? alpha[9]!; - } -} diff --git a/packages/remix_fortal/lib/src/fortal/computed.dart b/packages/remix_fortal/lib/src/theme/computed.dart similarity index 99% rename from packages/remix_fortal/lib/src/fortal/computed.dart rename to packages/remix_fortal/lib/src/theme/computed.dart index 2b7ab873f..e18126370 100644 --- a/packages/remix_fortal/lib/src/fortal/computed.dart +++ b/packages/remix_fortal/lib/src/theme/computed.dart @@ -15,8 +15,9 @@ import 'package:flutter/painting.dart' show BoxShadow, ColorSwatch; import 'package:mix/mix.dart' show MixToken; import 'package:remix/remix.dart' show RemixBoxShadow, RemixBoxShadowKind; -import '../radix/colors/colors.dart'; -import 'fortal_theme.dart'; +import 'radix_colors.dart'; +import 'theme_data.dart'; +import 'tokens.dart'; // ============================================================================ // FUNCTIONAL / COMPUTED IMPLEMENTATIONS diff --git a/packages/remix_fortal/lib/src/theme/control_styles.dart b/packages/remix_fortal/lib/src/theme/control_styles.dart new file mode 100644 index 000000000..8311333b6 --- /dev/null +++ b/packages/remix_fortal/lib/src/theme/control_styles.dart @@ -0,0 +1,84 @@ +import 'package:flutter/widgets.dart'; +import 'package:mix/mix.dart'; +import 'package:remix/remix.dart' + show + RemixBoxEffectLayerMix, + RemixBoxEffectsMix, + RemixBoxShadowKind, + RemixBoxShadowMix, + RemixBoxStylerAnchors; + +import 'tokens.dart'; + +/// A CSS outline that does not affect layout. +RemixBoxEffectsMix fortalFocusOutline(Color color, {required double offset}) => + RemixBoxEffectsMix( + outline: BorderSideMix( + color: color, + width: 2, + strokeAlign: BorderSide.strokeAlignInside, + ), + outlineOffset: offset, + ); + +/// The focus ring for stylers that can only express a border. +/// +/// [fortalFocusOutline] is the preferred form, but it returns a +/// [RemixBoxEffectsMix] and several stylers — `ToggleStyler`, +/// `ToggleGroupItemStyler`, `TabStyler` — expose no `containerEffects` slot to +/// put one in. Those recipes each re-derived the same border, so the width +/// token lives here instead of in four places. +extension FortalFocusRing> on RemixBoxStylerAnchors { + /// Applies the ring to this styler. + /// + /// [color] defaults to `focus-a8`. Tabs is the one caller that differs: it + /// passes the solid `focus-8`. Whether that is intentional is unresolved — + /// the pinned Chromium probes capture computed styles only, not + /// `:focus-visible`, so it cannot be settled from the reference fixtures. + /// Preserved as-is rather than unified on a guess. + /// + /// Tabs also leaves [strokeAlign] unset, but that is not a second + /// difference: `BorderSide` itself defaults to `strokeAlignInside`, so unset + /// and explicit resolve to the same -1.0. The parameter is nullable only so + /// tabs can keep expressing it as absent. + T fortalFocusRing({ + Color? color, + double? strokeAlign = BorderSide.strokeAlignInside, + }) => border( + .all( + BorderSideMix( + color: color ?? _focusRingColor(), + width: _focusRingWidth(), + strokeAlign: strokeAlign, + ), + ), + ); +} + +/// The same ring for a Mix [FlexBoxStyler], which sits outside Remix's +/// `RemixBoxStylerAnchors` interface — accordion rings its trigger, not itself. +/// Separate extension rather than a differently-named function so both read as +/// `.fortalFocusRing()`; the receiver type picks the right one. +extension FortalFocusRingFlexBox on FlexBoxStyler { + FlexBoxStyler fortalFocusRing() => border( + .color( + _focusRingColor(), + ).width(_focusRingWidth()).strokeAlign(BorderSide.strokeAlignInside), + ); +} + +Color _focusRingColor() => FortalTokens.focusA8(); +double _focusRingWidth() => FortalTokens.focusRingWidth(); + +/// A one-pixel inset stroke, optionally layered over a fill. +RemixBoxEffectLayerMix fortalInsetSurface({required List strokes}) => + RemixBoxEffectLayerMix( + shadows: [ + for (final stroke in strokes) + RemixBoxShadowMix( + kind: RemixBoxShadowKind.inset, + color: stroke, + spreadRadius: 1, + ), + ], + ); diff --git a/packages/remix_fortal/lib/src/radix/colors/colors_generated.dart b/packages/remix_fortal/lib/src/theme/radix_colors.dart similarity index 96% rename from packages/remix_fortal/lib/src/radix/colors/colors_generated.dart rename to packages/remix_fortal/lib/src/theme/radix_colors.dart index ca8dbe6df..2c85bdd4e 100644 --- a/packages/remix_fortal/lib/src/radix/colors/colors_generated.dart +++ b/packages/remix_fortal/lib/src/theme/radix_colors.dart @@ -4,7 +4,96 @@ // Radix Colors version: bundled with Radix Themes 3.3.0 // Source integrity: sha512-I0/h2CRNTpYNB7Mi3xFIvSsQq5a108d7kK8dTO5zp5b9HR5QJXKag6B8tjpz2ITkVYkFdkGk45doNkSr7OxwNw== -part of 'colors.dart'; +library radix_colors; + +import 'package:flutter/painting.dart'; + +class RadixColor { + final RadixColorScale scale; + final Color surface; + final Color indicator; + final Color track; + final Color contrast; + + const RadixColor( + this.scale, + this.surface, + this.indicator, + this.track, + this.contrast, + ); +} + +class RadixColorTheme { + final RadixColor light; + final RadixColor dark; + + const RadixColorTheme(this.light, this.dark); +} + +class RadixColorScale { + final ColorSwatch solid; + final ColorSwatch alpha; + + const RadixColorScale(this.solid, this.alpha); + + // Semantic color accessors for better code readability + + /// The most subtle background color (step 1). + Color get appBackground => step(1); + + /// Subtle background with slightly more presence (step 2). + Color get subtleBackground => step(2); + + /// Default background for interactive components (step 3). + Color get componentBackground => step(3); + + /// Background color for components on hover (step 4). + Color get componentBackgroundHover => step(4); + + /// Background color for active/pressed components (step 5). + Color get componentBackgroundActive => step(5); + + /// Subtle border color for gentle separation (step 6). + Color get subtleBorder => step(6); + + /// Standard border color for components (step 7). + Color get componentBorder => step(7); + + /// Border color for hover and focus states (step 8). + Color get componentBorderHover => step(8); + + /// Primary solid background color (step 9). + Color get solidBackground => step(9); + + /// Solid background color on hover (step 10). + Color get solidBackgroundHover => step(10); + + /// Low contrast text color (step 11). + Color get lowContrastText => step(11); + + /// High contrast text color (step 12). + Color get highContrastText => step(12); + + /// Gets a solid color from the 12-step scale. + /// + /// Steps must be between 1 and 12. Falls back to step 9 if unavailable. + Color step(int n) { + assert(n >= 1 && n <= 12, 'Step must be between 1 and 12'); + + return solid[n] ?? solid[9]!; + } + + /// Gets a translucent color from the 12-step alpha scale. + /// + /// Alpha variants maintain saturation when composited. + /// Falls back to alpha step 9 if unavailable. + Color alphaStep(int n) { + assert(n >= 1 && n <= 12, 'Step must be between 1 and 12'); + + return alpha[n] ?? alpha[9]!; + } +} // gray color scale const _grayLight = RadixColor( diff --git a/packages/remix_fortal/lib/src/fortal/surface_frame.dart b/packages/remix_fortal/lib/src/theme/surface_frame.dart similarity index 100% rename from packages/remix_fortal/lib/src/fortal/surface_frame.dart rename to packages/remix_fortal/lib/src/theme/surface_frame.dart diff --git a/packages/remix_fortal/lib/src/theme/theme.dart b/packages/remix_fortal/lib/src/theme/theme.dart new file mode 100644 index 000000000..17c1983f0 --- /dev/null +++ b/packages/remix_fortal/lib/src/theme/theme.dart @@ -0,0 +1,14 @@ +// Internal entrypoint for the theme layer, imported by every component under +// `src/components/`. The package's public entrypoint is `lib/remix_fortal.dart`. + +export 'computed.dart'; +export 'control_styles.dart'; +export 'surface_frame.dart'; +export 'theme_data.dart' hide buildFortalScopeTokens; +export 'theme_scope.dart'; +export 'tokens.dart'; + +// `radix_colors.dart` deliberately stays out of this barrel because it exposes +// bare color names such as `gray` and `red`. The public package barrel exports +// it explicitly for compatibility; installed applications import it only from +// `theme_data.dart`. diff --git a/packages/remix_fortal/lib/src/theme/theme_data.dart b/packages/remix_fortal/lib/src/theme/theme_data.dart new file mode 100644 index 000000000..190cf7432 --- /dev/null +++ b/packages/remix_fortal/lib/src/theme/theme_data.dart @@ -0,0 +1,1091 @@ +import 'dart:math' as math; + +import 'package:flutter/widgets.dart'; +import 'package:mix/mix.dart'; +import 'package:remix/remix.dart' show RemixBoxShadow, RemixBoxShadowKind; + +import 'computed.dart'; +import 'radix_colors.dart' as radix; +import 'theme_scope.dart' show FortalScope; +import 'tokens.dart'; + +/// Available accent colors matching Radix Themes names. +enum FortalAccentColor { + gray, + mauve, + slate, + sage, + olive, + sand, + gold, + bronze, + brown, + yellow, + amber, + orange, + tomato, + red, + ruby, + crimson, + pink, + plum, + purple, + violet, + iris, + indigo, + blue, + cyan, + teal, + jade, + green, + grass, + lime, + mint, + sky, +} + +/// Available neutral gray families matching Radix Themes names. +enum FortalGrayColor { gray, mauve, slate, sage, olive, sand } + +/// Theme-level radius multipliers matching the Radix Themes presets. +enum FortalRadius { none, small, medium, large, full } + +/// Background treatment used by floating panels. +enum FortalPanelBackground { solid, translucent } + +/// Discrete theme scaling values supported by Radix Themes. +enum FortalScaling { + percent90(0.9), + percent95(0.95), + percent100(1.0), + percent105(1.05), + percent110(1.1); + + const FortalScaling(this.factor); + + /// Numeric multiplier represented by this preset. + final double factor; +} + +/// Partial theme values applied by a [FortalScope]. +@immutable +class FortalThemeConfig { + const FortalThemeConfig({ + this.accent, + this.gray, + this.brightness, + this.panelBackground, + this.radius, + this.scaling, + this.hasBackground, + }); + + final FortalAccentColor? accent; + final FortalGrayColor? gray; + final Brightness? brightness; + final FortalPanelBackground? panelBackground; + final FortalRadius? radius; + final FortalScaling? scaling; + final bool? hasBackground; + + bool get isDark => brightness == .dark; + + FortalThemeConfig copyWith({ + FortalAccentColor? accent, + FortalGrayColor? gray, + Brightness? brightness, + FortalPanelBackground? panelBackground, + FortalRadius? radius, + FortalScaling? scaling, + bool? hasBackground, + }) => FortalThemeConfig( + accent: accent ?? this.accent, + gray: gray ?? this.gray, + brightness: brightness ?? this.brightness, + panelBackground: panelBackground ?? this.panelBackground, + radius: radius ?? this.radius, + scaling: scaling ?? this.scaling, + hasBackground: hasBackground ?? this.hasBackground, + ); + + @override + bool operator ==(Object other) => + identical(this, other) || + other is FortalThemeConfig && + accent == other.accent && + gray == other.gray && + brightness == other.brightness && + panelBackground == other.panelBackground && + radius == other.radius && + scaling == other.scaling && + hasBackground == other.hasBackground; + + @override + int get hashCode => Object.hash( + accent, + gray, + brightness, + panelBackground, + radius, + scaling, + hasBackground, + ); + + Widget createScope({List? orderOfModifiers, required Widget child}) => + FortalScope( + accent: accent, + gray: gray, + brightness: brightness, + panelBackground: panelBackground, + radius: radius, + scaling: scaling, + hasBackground: hasBackground, + orderOfModifiers: orderOfModifiers, + child: child, + ); +} + +/// Fully resolved theme values inherited by a Fortal subtree. +@immutable +class FortalThemeData extends FortalThemeConfig { + const FortalThemeData({ + required this.accent, + required this.gray, + required this.brightness, + required this.panelBackground, + required this.radius, + required this.scaling, + required this.hasBackground, + }) : super( + accent: accent, + gray: gray, + brightness: brightness, + panelBackground: panelBackground, + radius: radius, + scaling: scaling, + hasBackground: hasBackground, + ); + + @override + final FortalAccentColor accent; + @override + final FortalGrayColor gray; + @override + final Brightness brightness; + @override + final FortalPanelBackground panelBackground; + @override + final FortalRadius radius; + @override + final FortalScaling scaling; + @override + final bool hasBackground; + + @override + bool get isDark => brightness == .dark; + + @override + FortalThemeData copyWith({ + FortalAccentColor? accent, + FortalGrayColor? gray, + Brightness? brightness, + FortalPanelBackground? panelBackground, + FortalRadius? radius, + FortalScaling? scaling, + bool? hasBackground, + }) => FortalThemeData( + accent: accent ?? this.accent, + gray: gray ?? this.gray, + brightness: brightness ?? this.brightness, + panelBackground: panelBackground ?? this.panelBackground, + radius: radius ?? this.radius, + scaling: scaling ?? this.scaling, + hasBackground: hasBackground ?? this.hasBackground, + ); + + @override + bool operator ==(Object other) => + identical(this, other) || + other is FortalThemeData && + accent == other.accent && + gray == other.gray && + brightness == other.brightness && + panelBackground == other.panelBackground && + radius == other.radius && + scaling == other.scaling && + hasBackground == other.hasBackground; + + @override + int get hashCode => Object.hash( + accent, + gray, + brightness, + panelBackground, + radius, + scaling, + hasBackground, + ); +} + +/// Builds the token map for a Fortal scope. Used by [FortalScope]. +Map buildFortalScopeTokens(FortalThemeData theme) { + final tokens = resolveFortalTokens(theme); + final scaling = theme.scaling.factor; + final shadows = buildFortalShadows(isDark: theme.isDark, colors: tokens); + + final colorTokens = { + // Role and functional tokens + FortalTokens.colorBackground: tokens.colorBackground, + FortalTokens.colorSurface: tokens.colorSurface, + FortalTokens.segmentedControlIndicatorBackground: theme.isDark + ? tokens.gray.scale.alphaStep(3) + : tokens.colorBackground, + FortalTokens.colorPanelSolid: tokens.colorPanelSolid, + FortalTokens.colorPanelTranslucent: tokens.colorPanelTranslucent, + FortalTokens.colorPanel: theme.panelBackground == .solid + ? tokens.colorPanelSolid + : tokens.colorPanelTranslucent, + FortalTokens.colorOverlay: tokens.colorOverlay, + FortalTokens.sliderHighContrastOverlay: theme.isDark + ? const Color(0x00000000) + : tokens.blackAlpha[8]!, + FortalTokens.error3: (theme.isDark ? radix.red.dark : radix.red.light).scale + .step(3), + FortalTokens.error7: (theme.isDark ? radix.red.dark : radix.red.light).scale + .step(7), + FortalTokens.error8: (theme.isDark ? radix.red.dark : radix.red.light).scale + .step(8), + FortalTokens.error9: (theme.isDark ? radix.red.dark : radix.red.light).scale + .step(9), + FortalTokens.error11: (theme.isDark ? radix.red.dark : radix.red.light) + .scale + .step(11), + FortalTokens.error12: (theme.isDark ? radix.red.dark : radix.red.light) + .scale + .step(12), + FortalTokens.errorA7: (theme.isDark ? radix.red.dark : radix.red.light) + .scale + .alphaStep(7), + ..._accentColorTokens(tokens), + // Gray steps + FortalTokens.gray1: tokens.gray.scale.step(1), + FortalTokens.gray2: tokens.gray.scale.step(2), + FortalTokens.gray3: tokens.gray.scale.step(3), + FortalTokens.gray4: tokens.gray.scale.step(4), + FortalTokens.gray5: tokens.gray.scale.step(5), + FortalTokens.gray6: tokens.gray.scale.step(6), + FortalTokens.gray7: tokens.gray.scale.step(7), + FortalTokens.gray8: tokens.gray.scale.step(8), + FortalTokens.gray9: tokens.gray.scale.step(9), + FortalTokens.gray10: tokens.gray.scale.step(10), + FortalTokens.gray11: tokens.gray.scale.step(11), + FortalTokens.gray12: tokens.gray.scale.step(12), + // Gray role tokens (from resolved colors) + FortalTokens.graySurface: tokens.gray.surface, + FortalTokens.grayIndicator: tokens.gray.indicator, + FortalTokens.grayTrack: tokens.gray.track, + FortalTokens.grayContrast: tokens.gray.contrast, + // Gray alpha a1..a12 + FortalTokens.grayA1: tokens.gray.scale.alphaStep(1), + FortalTokens.grayA2: tokens.gray.scale.alphaStep(2), + FortalTokens.grayA3: tokens.gray.scale.alphaStep(3), + FortalTokens.grayA4: tokens.gray.scale.alphaStep(4), + FortalTokens.grayA5: tokens.gray.scale.alphaStep(5), + FortalTokens.grayA6: tokens.gray.scale.alphaStep(6), + FortalTokens.grayA7: tokens.gray.scale.alphaStep(7), + FortalTokens.grayA8: tokens.gray.scale.alphaStep(8), + FortalTokens.grayA9: tokens.gray.scale.alphaStep(9), + FortalTokens.grayA10: tokens.gray.scale.alphaStep(10), + FortalTokens.grayA11: tokens.gray.scale.alphaStep(11), + FortalTokens.grayA12: tokens.gray.scale.alphaStep(12), + // Neutral helpers derived from primitives + FortalTokens.blackA1: tokens.blackAlpha[1]!, + FortalTokens.blackA2: tokens.blackAlpha[2]!, + FortalTokens.blackA3: tokens.blackAlpha[3]!, + FortalTokens.blackA4: tokens.blackAlpha[4]!, + FortalTokens.blackA5: tokens.blackAlpha[5]!, + FortalTokens.blackA6: tokens.blackAlpha[6]!, + FortalTokens.blackA7: tokens.blackAlpha[7]!, + FortalTokens.blackA8: tokens.blackAlpha[8]!, + FortalTokens.blackA9: tokens.blackAlpha[9]!, + FortalTokens.blackA10: tokens.blackAlpha[10]!, + FortalTokens.blackA11: tokens.blackAlpha[11]!, + FortalTokens.blackA12: tokens.blackAlpha[12]!, + FortalTokens.whiteA1: tokens.whiteAlpha[1]!, + FortalTokens.whiteA2: tokens.whiteAlpha[2]!, + FortalTokens.whiteA3: tokens.whiteAlpha[3]!, + FortalTokens.whiteA4: tokens.whiteAlpha[4]!, + FortalTokens.whiteA5: tokens.whiteAlpha[5]!, + FortalTokens.whiteA6: tokens.whiteAlpha[6]!, + FortalTokens.whiteA7: tokens.whiteAlpha[7]!, + FortalTokens.whiteA8: tokens.whiteAlpha[8]!, + FortalTokens.whiteA9: tokens.whiteAlpha[9]!, + FortalTokens.whiteA10: tokens.whiteAlpha[10]!, + FortalTokens.whiteA11: tokens.whiteAlpha[11]!, + FortalTokens.whiteA12: tokens.whiteAlpha[12]!, + FortalTokens.shadowStroke: tokens.shadowStroke, + FortalTokens.grayStroke3: mixOklabPremultiplied( + tokens.gray.scale.alphaStep(3), + tokens.gray.scale.step(3), + 0.25, + ), + FortalTokens.grayStroke4: mixOklabPremultiplied( + tokens.gray.scale.alphaStep(4), + tokens.gray.scale.step(4), + 0.25, + ), + FortalTokens.grayStroke5: mixOklabPremultiplied( + tokens.gray.scale.alphaStep(5), + tokens.gray.scale.step(5), + 0.25, + ), + FortalTokens.grayStroke6: mixOklabPremultiplied( + tokens.gray.scale.alphaStep(6), + tokens.gray.scale.step(6), + 0.25, + ), + FortalTokens.grayStroke7: mixOklabPremultiplied( + tokens.gray.scale.alphaStep(7), + tokens.gray.scale.step(7), + 0.25, + ), + FortalTokens.dataTableBorder: mixOklabPremultiplied( + tokens.gray.scale.alphaStep(5), + tokens.gray.scale.step(6), + 0.5, + ), + }; + + // Build base tokens map + final allTokens = { + ...colorTokens, + // Defaults (may be overridden by JSON tokens below) + FortalTokens.panelBlur: + theme.panelBackground == FortalPanelBackground.translucent ? 64.0 : 0.0, + FortalTokens.space1: 4.0 * scaling, + FortalTokens.space2: 8.0 * scaling, + FortalTokens.space3: 12.0 * scaling, + FortalTokens.space4: 16.0 * scaling, + FortalTokens.space5: 24.0 * scaling, + FortalTokens.space6: 32.0 * scaling, + FortalTokens.space7: 40.0 * scaling, + FortalTokens.space8: 48.0 * scaling, + FortalTokens.space9: 64.0 * scaling, + FortalTokens.spinnerSize3: 20.0 * scaling, + FortalTokens.dataTableRowHeight1: 36.0 * scaling, + FortalTokens.dataTableRowHeight2: 44.0 * scaling, + FortalTokens.toggleGap1: 2.0 * scaling, + FortalTokens.toggleGap3: 6.0 * scaling, + FortalTokens.avatarSize6: 80.0 * scaling, + FortalTokens.avatarSize7: 96.0 * scaling, + FortalTokens.avatarSize8: 128.0 * scaling, + FortalTokens.avatarSize9: 160.0 * scaling, + FortalTokens.avatarIconSize1: 12.0 * scaling, + FortalTokens.avatarIconSize2: 16.0 * scaling, + FortalTokens.avatarIconSize3: 20.0 * scaling, + FortalTokens.avatarIconSize4: 24.0 * scaling, + FortalTokens.avatarIconSize5: 32.0 * scaling, + FortalTokens.avatarIconSize6: 40.0 * scaling, + FortalTokens.avatarIconSize7: 48.0 * scaling, + FortalTokens.avatarIconSize8: 64.0 * scaling, + FortalTokens.avatarIconSize9: 80.0 * scaling, + FortalTokens.badgePaddingX1: 6.0 * scaling, + FortalTokens.badgePaddingY1: 2.0 * scaling, + FortalTokens.badgePaddingX3: 10.0 * scaling, + FortalTokens.checkboxSize1: 14.0 * scaling, + FortalTokens.checkboxSize3: 20.0 * scaling, + FortalTokens.checkboxIndicatorSize1: 9.0 * scaling, + FortalTokens.checkboxIndicatorSize2: 10.0 * scaling, + FortalTokens.checkboxIndicatorSize3: 12.0 * scaling, + FortalTokens.checkboxGroupItemGap1: 6.0 * scaling, + FortalTokens.checkboxGroupItemGap2: 7.0 * scaling, + FortalTokens.checkboxGroupItemGap3: 8.0 * scaling, + FortalTokens.radioIndicatorSize1: 5.6 * scaling, + FortalTokens.radioIndicatorSize2: 6.4 * scaling, + FortalTokens.radioIndicatorSize3: 8.0 * scaling, + FortalTokens.checkboxRadius1: _scaledRadiusToken( + theme.radius, + scaling, + 3.0 * 0.875, + ), + FortalTokens.checkboxRadius3: _scaledRadiusToken( + theme.radius, + scaling, + 3.0 * 1.25, + ), + FortalTokens.switchHeight2: 20.0 * scaling, + FortalTokens.switchWidth1: 28.0 * scaling, + FortalTokens.switchWidth2: 35.0 * scaling, + FortalTokens.switchWidth3: 42.0 * scaling, + FortalTokens.switchThumbSize1: 16.0 * scaling - 2.0, + FortalTokens.switchThumbSize2: 20.0 * scaling - 2.0, + FortalTokens.switchThumbSize3: 24.0 * scaling - 2.0, + FortalTokens.progressHeight2: 6.0 * scaling, + FortalTokens.sliderTrackSize1: 6.0 * scaling, + FortalTokens.sliderTrackSize2: 8.0 * scaling, + FortalTokens.sliderTrackSize3: 10.0 * scaling, + FortalTokens.sliderThumbSize1: 13.0 * scaling, + FortalTokens.sliderThumbSize2: 16.0 * scaling, + FortalTokens.sliderThumbSize3: 19.0 * scaling, + FortalTokens.textFieldPadding1: 6.0 * scaling, + FortalTokens.textFieldPadding2: 8.0 * scaling, + FortalTokens.textFieldPadding3: 12.0 * scaling, + FortalTokens.textAreaMinHeight3: 80.0, + FortalTokens.dataListRowGap3: 20.0 * scaling, + FortalTokens.dataListLabelMinWidth: 120.0, + FortalTokens.tabInnerPaddingY1: 2.0 * scaling, + FortalTokens.tabActiveLetterSpacing1: -0.12 * scaling, + FortalTokens.tabActiveLetterSpacing2: -0.14 * scaling, + FortalTokens.selectSpace1Half: 6.0 * scaling, + FortalTokens.selectIndicatorWidth1: 20.0 * scaling, + FortalTokens.selectIndicatorSize1: 8.0 * scaling, + FortalTokens.selectIndicatorSize2: 10.0 * scaling, + FortalTokens.selectGhostMarginX12: -8.0 * scaling, + FortalTokens.selectGhostMarginY12: -4.0 * scaling, + FortalTokens.selectGhostMarginX3: -12.0 * scaling, + FortalTokens.selectGhostMarginY3: -6.0 * scaling, + ..._radiusTokensFor(theme.radius, scaling), + + // Exact layered Radix shadow tokens, resolved for the active color scales. + ...shadows, + FortalTokens.sliderClassicDisabledTrackShadows: _scaleShadowOpacity( + shadows[FortalTokens.shadow1Layers]! as List, + 0.5, + ), + FortalTokens.cardClassicOuterShadows: _cardClassicShadows( + tokens, + isDark: theme.isDark, + layer: .outer, + state: .idle, + ), + FortalTokens.cardClassicInnerShadows: _cardClassicShadows( + tokens, + isDark: theme.isDark, + layer: .inner, + state: .idle, + ), + FortalTokens.cardClassicHoverOuterShadows: _cardClassicShadows( + tokens, + isDark: theme.isDark, + layer: .outer, + state: .hovered, + ), + FortalTokens.cardClassicHoverInnerShadows: _cardClassicShadows( + tokens, + isDark: theme.isDark, + layer: .inner, + state: .hovered, + ), + FortalTokens.cardClassicActiveOuterShadows: _cardClassicShadows( + tokens, + isDark: theme.isDark, + layer: .outer, + state: .active, + ), + FortalTokens.cardClassicActiveInnerShadows: _cardClassicShadows( + tokens, + isDark: theme.isDark, + layer: .inner, + state: .active, + ), + FortalTokens.selectTriggerClassicShadows: _selectClassicShadows( + tokens, + isDark: theme.isDark, + ), + FortalTokens.selectTriggerClassicHoverShadows: [ + _insetShadow(tokens.gray.scale.alphaStep(3), spread: 1), + ..._selectClassicShadows(tokens, isDark: theme.isDark), + ], + FortalTokens.baseButtonClassicDisabledShadows: + _baseButtonClassicDisabledShadows(tokens, isDark: theme.isDark), + FortalTokens.baseButtonClassicShadows: _baseButtonClassicShadows( + tokens, + isDark: theme.isDark, + highContrast: false, + ), + FortalTokens.baseButtonClassicHighContrastShadows: + _baseButtonClassicShadows( + tokens, + isDark: theme.isDark, + highContrast: true, + ), + FortalTokens.baseButtonClassicActiveShadows: + _baseButtonClassicActiveShadows(tokens, highContrast: false), + FortalTokens.baseButtonClassicActiveHighContrastShadows: + _baseButtonClassicActiveShadows(tokens, highContrast: true), + FortalTokens.baseButtonClassicAfterInset: theme.isDark ? 1.0 : 2.0, + FortalTokens.baseButtonGhostPaddingY3: 6.0 * scaling, + FortalTokens.baseButtonGhostMarginX12: -8.0 * scaling, + FortalTokens.baseButtonGhostMarginY12: -4.0 * scaling, + FortalTokens.baseButtonGhostMarginX3: -12.0 * scaling, + FortalTokens.baseButtonGhostMarginY3: -6.0 * scaling, + FortalTokens.baseButtonGhostMarginX4: -16.0 * scaling, + FortalTokens.baseButtonGhostMarginY4: -8.0 * scaling, + FortalTokens.iconButtonGhostPadding2: 6.0 * scaling, + FortalTokens.iconButtonGhostMargin1: -4.0 * scaling, + FortalTokens.iconButtonGhostMargin2: -6.0 * scaling, + FortalTokens.iconButtonGhostMargin3: -8.0 * scaling, + FortalTokens.iconButtonGhostMargin4: -12.0 * scaling, + FortalTokens.cardGhostMargin1: -12.0 * scaling, + FortalTokens.cardGhostMargin2: -16.0 * scaling, + FortalTokens.cardGhostMargin3: -24.0 * scaling, + FortalTokens.cardGhostMargin4: -32.0 * scaling, + FortalTokens.cardGhostMargin5: -48.0 * scaling, + FortalTokens.borderWidth1: 1.0, + FortalTokens.borderWidth2: 2.0, + FortalTokens.focusRingWidth: 2.0, + FortalTokens.focusRingOffset: 2.0, + FortalTokens.text1: TextStyle( + fontSize: 12.0 * scaling, + letterSpacing: 0.0025 * 12.0 * scaling, + height: 16.0 / 12.0, + ), + FortalTokens.text2: TextStyle( + fontSize: 14.0 * scaling, + letterSpacing: 0.0, + height: 20.0 / 14.0, + ), + FortalTokens.text3: TextStyle( + fontSize: 16.0 * scaling, + letterSpacing: 0.0, + height: 24.0 / 16.0, + ), + FortalTokens.accordionText2: TextStyle( + fontSize: 15.0 * scaling, + letterSpacing: 0.0, + height: 20.0 / 15.0, + ), + FortalTokens.text4: TextStyle( + fontSize: 18.0 * scaling, + letterSpacing: -0.0025 * 18.0 * scaling, + height: 26.0 / 18.0, + ), + FortalTokens.text5: TextStyle( + fontSize: 20.0 * scaling, + letterSpacing: -0.005 * 20.0 * scaling, + height: 28.0 / 20.0, + ), + FortalTokens.text6: TextStyle( + fontSize: 24.0 * scaling, + letterSpacing: -0.00625 * 24.0 * scaling, + height: 30.0 / 24.0, + ), + FortalTokens.text7: TextStyle( + fontSize: 28.0 * scaling, + letterSpacing: -0.0075 * 28.0 * scaling, + height: 36.0 / 28.0, + ), + FortalTokens.text8: TextStyle( + fontSize: 35.0 * scaling, + letterSpacing: -0.01 * 35.0 * scaling, + height: 40.0 / 35.0, + ), + FortalTokens.text9: TextStyle( + fontSize: 60.0 * scaling, + letterSpacing: -0.025 * 60.0 * scaling, + height: 1.0, + ), + FortalTokens.avatarFallback1One: _avatarFallbackText( + fontSize: 14, + letterSpacing: 0.0025 * 12, + scaling: scaling, + ), + FortalTokens.avatarFallback1Two: _avatarFallbackText( + fontSize: 12, + letterSpacing: 0.0025 * 12, + scaling: scaling, + ), + FortalTokens.avatarFallback2One: _avatarFallbackText( + fontSize: 16, + letterSpacing: 0, + scaling: scaling, + ), + FortalTokens.avatarFallback2Two: _avatarFallbackText( + fontSize: 14, + letterSpacing: 0, + scaling: scaling, + ), + FortalTokens.avatarFallback3One: _avatarFallbackText( + fontSize: 18, + letterSpacing: 0, + scaling: scaling, + ), + FortalTokens.avatarFallback3Two: _avatarFallbackText( + fontSize: 16, + letterSpacing: 0, + scaling: scaling, + ), + FortalTokens.avatarFallback4One: _avatarFallbackText( + fontSize: 20, + letterSpacing: -0.0025 * 18, + scaling: scaling, + ), + FortalTokens.avatarFallback4Two: _avatarFallbackText( + fontSize: 18, + letterSpacing: -0.0025 * 18, + scaling: scaling, + ), + FortalTokens.avatarFallback5: _avatarFallbackText( + fontSize: 24, + letterSpacing: -0.00625 * 24, + scaling: scaling, + ), + FortalTokens.avatarFallback6: _avatarFallbackText( + fontSize: 28, + letterSpacing: -0.0075 * 28, + scaling: scaling, + ), + FortalTokens.avatarFallback7: _avatarFallbackText( + fontSize: 28, + letterSpacing: -0.0075 * 28, + scaling: scaling, + ), + FortalTokens.avatarFallback8: _avatarFallbackText( + fontSize: 35, + letterSpacing: -0.01 * 35, + scaling: scaling, + ), + FortalTokens.avatarFallback9: _avatarFallbackText( + fontSize: 60, + letterSpacing: -0.025 * 60, + scaling: scaling, + ), + + // Font weights (token values) + FortalTokens.fontWeightLight: FontWeight.w300, + FortalTokens.fontWeightRegular: FontWeight.w400, + FortalTokens.fontWeightMedium: FontWeight.w500, + // Match Radix Themes font weights (bold = 700) + FortalTokens.fontWeightBold: FontWeight.w700, + + // Durations (token values) + FortalTokens.transitionFast: Duration(milliseconds: 100), + FortalTokens.transitionSlow: Duration(milliseconds: 300), + FortalTokens.skeletonPulseDuration: Duration(milliseconds: 1000), + }; + + return allTokens; +} + +TextStyle _avatarFallbackText({ + required double fontSize, + required double letterSpacing, + required double scaling, +}) => TextStyle( + fontSize: fontSize * scaling, + letterSpacing: letterSpacing * scaling, + height: 1, +); + +enum _CardShadowLayer { outer, inner } + +enum _CardShadowState { idle, hovered, active } + +List _cardClassicShadows( + FortalThemeColors colors, { + required bool isDark, + required _CardShadowLayer layer, + required _CardShadowState state, +}) { + final inner = layer == _CardShadowLayer.inner; + final shapeInset = inner ? 1.0 : 0.0; + final border = switch ((isDark, state)) { + (true, _) => mixOklabPremultiplied( + colors.gray.scale.alphaStep(6), + colors.gray.scale.step(6), + 0.25, + ), + (false, _CardShadowState.hovered) => mixOklabPremultiplied( + colors.gray.scale.alphaStep(4), + colors.gray.scale.step(4), + 0.25, + ), + (false, _) => mixOklabPremultiplied( + colors.gray.scale.alphaStep(3), + colors.gray.scale.step(3), + 0.25, + ), + }; + + RemixBoxShadow shadow( + Color color, { + Offset offset = Offset.zero, + double blur = 0, + required double spread, + }) => RemixBoxShadow( + color: color, + offset: offset, + blurRadius: blur, + spreadRadius: spread, + shapeInset: shapeInset, + ); + + if (state == _CardShadowState.hovered) { + if (isDark) { + return [ + shadow(border, spread: inner ? 1 : 0), + shadow(colors.gray.scale.alphaStep(4), blur: 1, spread: inner ? 1 : 0), + shadow( + colors.gray.scale.alphaStep(4), + blur: 1, + spread: inner ? -1 : -2, + ), + shadow( + colors.gray.scale.alphaStep(3), + blur: 3, + spread: inner ? -2 : -3, + ), + shadow( + colors.gray.scale.alphaStep(3), + blur: 12, + spread: inner ? -2 : -3, + ), + shadow( + colors.gray.scale.alphaStep(7), + blur: 16, + spread: inner ? -8 : -9, + ), + ]; + } + return [ + shadow(border, spread: inner ? 1 : 0), + shadow( + colors.blackAlpha[1]!, + offset: const Offset(0, 1), + blur: 1, + spread: inner ? 1 : 0, + ), + shadow( + colors.gray.scale.alphaStep(3), + offset: const Offset(0, 2), + blur: 1, + spread: inner ? -1 : -2, + ), + shadow( + colors.blackAlpha[1]!, + offset: const Offset(0, 2), + blur: 3, + spread: inner ? -2 : -3, + ), + shadow( + colors.gray.scale.alphaStep(3), + offset: const Offset(0, 3), + blur: 12, + spread: inner ? -4 : -5, + ), + shadow( + colors.blackAlpha[1]!, + offset: const Offset(0, 4), + blur: 16, + spread: inner ? -8 : -9, + ), + ]; + } + + final active = state == _CardShadowState.active; + final subtle = isDark ? colors.blackAlpha[3]! : colors.blackAlpha[1]!; + final middle = isDark + ? colors.blackAlpha[6]! + : colors.gray.scale.alphaStep(active ? 4 : 2); + final bottom = isDark ? colors.blackAlpha[5]! : colors.blackAlpha[1]!; + return [ + shadow(border, spread: inner ? 1 : 0), + shadow(const Color(0x00000000), spread: inner ? 1 : 0), + shadow(subtle, spread: inner ? 0.5 : 0), + shadow(middle, offset: const Offset(0, 1), blur: 1, spread: inner ? 0 : -1), + shadow( + isDark ? colors.blackAlpha[6]! : colors.blackAlpha[1]!, + offset: const Offset(0, 2), + blur: 1, + spread: inner ? -1 : -2, + ), + shadow(bottom, offset: const Offset(0, 1), blur: 3, spread: inner ? 0 : -1), + ]; +} + +RemixBoxShadow _insetShadow( + Color color, { + Offset offset = Offset.zero, + double blur = 0, + double spread = 0, + double shapeInset = 0, +}) => RemixBoxShadow( + kind: RemixBoxShadowKind.inset, + color: color, + offset: offset, + blurRadius: blur, + spreadRadius: spread, + shapeInset: shapeInset, +); + +List _selectClassicShadows( + FortalThemeColors colors, { + required bool isDark, +}) { + if (isDark) { + return [ + _insetShadow(colors.whiteAlpha[4]!, spread: 1), + _insetShadow(colors.whiteAlpha[4]!, offset: const Offset(0, 1), blur: 1), + _insetShadow(colors.blackAlpha[9]!, offset: const Offset(0, -1), blur: 1), + ]; + } + return [ + _insetShadow(colors.gray.scale.alphaStep(5), spread: 1), + _insetShadow(colors.whiteAlpha[11]!, offset: const Offset(0, 2), blur: 1), + _insetShadow( + colors.gray.scale.alphaStep(4), + offset: const Offset(0, -2), + blur: 1, + ), + ]; +} + +List _baseButtonClassicDisabledShadows( + FortalThemeColors colors, { + required bool isDark, +}) { + if (isDark) { + return [ + _insetShadow(colors.gray.scale.alphaStep(5), spread: 1), + _insetShadow( + colors.gray.scale.alphaStep(2), + offset: const Offset(0, 4), + blur: 2, + spread: -2, + ), + _insetShadow( + colors.gray.scale.alphaStep(5), + offset: const Offset(0, 1), + blur: 1, + ), + _insetShadow(colors.blackAlpha[3]!, offset: const Offset(0, -1), blur: 1), + _insetShadow(colors.gray.scale.alphaStep(2), spread: 1), + ]; + } + return [ + _insetShadow(colors.gray.scale.alphaStep(4), spread: 1), + _insetShadow( + colors.gray.scale.alphaStep(3), + offset: const Offset(0, -2), + blur: 1, + ), + _insetShadow( + colors.whiteAlpha[9]!, + offset: const Offset(0, 4), + blur: 2, + spread: -2, + ), + _insetShadow( + colors.whiteAlpha[9]!, + offset: const Offset(0, 2), + blur: 1, + spread: -1, + ), + ]; +} + +List _baseButtonClassicShadows( + FortalThemeColors colors, { + required bool isDark, + required bool highContrast, +}) { + final accent = highContrast + ? colors.accent.scale.step(12) + : colors.accent.scale.step(9); + if (isDark) { + return [ + _insetShadow( + colors.whiteAlpha[4]!, + offset: const Offset(0, 2), + blur: 3, + spread: -1, + shapeInset: 1, + ), + _insetShadow(colors.whiteAlpha[2]!, spread: 1), + _insetShadow( + colors.whiteAlpha[3]!, + offset: const Offset(0, 4), + blur: 2, + spread: -2, + ), + _insetShadow(colors.whiteAlpha[6]!, offset: const Offset(0, 1), blur: 1), + _insetShadow(colors.blackAlpha[6]!, offset: const Offset(0, -1), blur: 1), + _insetShadow(accent, spread: 1), + ]; + } + return [ + _insetShadow( + colors.whiteAlpha[4]!, + offset: const Offset(0, 2), + blur: 3, + spread: -1, + shapeInset: 2, + ), + _insetShadow(colors.gray.scale.alphaStep(4), spread: 1), + _insetShadow( + colors.gray.scale.alphaStep(3), + offset: const Offset(0, -2), + blur: 1, + ), + _insetShadow(accent, spread: 1), + _insetShadow( + colors.whiteAlpha[9]!, + offset: const Offset(0, 4), + blur: 2, + spread: -2, + ), + _insetShadow( + colors.whiteAlpha[9]!, + offset: const Offset(0, 2), + blur: 1, + spread: -1, + ), + ]; +} + +List _baseButtonClassicActiveShadows( + FortalThemeColors colors, { + required bool highContrast, +}) { + final accent = highContrast + ? colors.accent.scale.step(12) + : colors.accent.scale.step(9); + return [ + _insetShadow( + colors.gray.scale.alphaStep(4), + offset: const Offset(0, 4), + blur: 2, + spread: -2, + ), + _insetShadow( + colors.gray.scale.alphaStep(7), + offset: const Offset(0, 1), + blur: 1, + ), + _insetShadow(colors.gray.scale.alphaStep(5), spread: 1), + _insetShadow(accent, spread: 1), + _insetShadow( + colors.gray.scale.alphaStep(3), + offset: const Offset(0, 3), + blur: 2, + ), + _insetShadow(colors.whiteAlpha[7]!, spread: 1), + _insetShadow(colors.whiteAlpha[5]!, offset: const Offset(0, -2), blur: 1), + ]; +} + +Map _radiusTokensFor(FortalRadius radius, double scaling) { + final factor = _radiusFactor(radius); + final thumb = switch (radius) { + .none || .small => const Radius.circular(0.5), + .medium || .large || .full => const Radius.circular(9999.0), + }; + Radius scaled(double base) => Radius.circular(base * factor * scaling); + Radius larger(Radius first, Radius second) => Radius.elliptical( + first.x > second.x ? first.x : second.x, + first.y > second.y ? first.y : second.y, + ); + final radius1 = scaled(3.0); + final radius2 = scaled(4.0); + final radius3 = scaled(6.0); + final radius4 = scaled(8.0); + final radius5 = scaled(12.0); + final radius6 = scaled(16.0); + final full = radius == .full ? const Radius.circular(9999.0) : Radius.zero; + Radius progressRadius(double height) { + final thumbBase = switch (radius) { + .none || .small => 0.5, + .medium || .large || .full => 9999.0, + }; + return Radius.circular(math.max(factor * height / 3, factor * thumbBase)); + } + + return { + FortalTokens.radius1: radius1, + FortalTokens.radius2: radius2, + FortalTokens.radius3: radius3, + FortalTokens.radius4: radius4, + FortalTokens.radius5: radius5, + FortalTokens.radius6: radius6, + FortalTokens.radiusFull: full, + FortalTokens.radiusThumb: thumb, + FortalTokens.radiusCircle: const Radius.circular(9999.0), + FortalTokens.radius1OrFull: larger(radius1, full), + FortalTokens.radius2OrFull: larger(radius2, full), + FortalTokens.radius3OrFull: larger(radius3, full), + FortalTokens.radius4OrFull: larger(radius4, full), + FortalTokens.radius5OrFull: larger(radius5, full), + FortalTokens.radius6OrFull: larger(radius6, full), + FortalTokens.radius1OrThumb: larger(radius1, thumb), + FortalTokens.radius2OrThumb: larger(radius2, thumb), + FortalTokens.progressRadius1: progressRadius(4.0 * scaling), + FortalTokens.progressRadius2: progressRadius(6.0 * scaling), + FortalTokens.progressRadius3: progressRadius(8.0 * scaling), + FortalTokens.sliderTrackRadius1: progressRadius(6.0 * scaling), + FortalTokens.sliderTrackRadius2: progressRadius(8.0 * scaling), + FortalTokens.sliderTrackRadius3: progressRadius(10.0 * scaling), + }; +} + +List _scaleShadowOpacity( + List shadows, + double factor, +) => [ + for (final shadow in shadows) + RemixBoxShadow( + kind: shadow.kind, + color: shadow.color.withValues(alpha: shadow.color.a * factor), + offset: shadow.offset, + blurRadius: shadow.blurRadius, + spreadRadius: shadow.spreadRadius, + shapeInset: shadow.shapeInset, + ), +]; + +double _radiusFactor(FortalRadius radius) => switch (radius) { + .none => 0.0, + .small => 0.75, + .medium => 1.0, + .large || .full => 1.5, +}; + +Radius _scaledRadiusToken(FortalRadius radius, double scaling, double base) => + Radius.circular(base * scaling * _radiusFactor(radius)); + +Map _accentColorTokens(FortalThemeColors tokens) { + final scale = tokens.accent.scale; + + return { + FortalTokens.accentSurface: tokens.accent.surface, + FortalTokens.accentIndicator: tokens.accent.indicator, + FortalTokens.accentTrack: tokens.accent.track, + FortalTokens.accentContrast: tokens.accent.contrast, + FortalTokens.focus8: tokens.focus8, + FortalTokens.focusA5: tokens.focusA5, + FortalTokens.focusA8: tokens.focusA8, + FortalTokens.accent1: scale.step(1), + FortalTokens.accent2: scale.step(2), + FortalTokens.accent3: scale.step(3), + FortalTokens.accent4: scale.step(4), + FortalTokens.accent5: scale.step(5), + FortalTokens.accent6: scale.step(6), + FortalTokens.accent7: scale.step(7), + FortalTokens.accent8: scale.step(8), + FortalTokens.accent9: scale.step(9), + FortalTokens.accent10: scale.step(10), + FortalTokens.accent11: scale.step(11), + FortalTokens.accent12: scale.step(12), + FortalTokens.accentA1: scale.alphaStep(1), + FortalTokens.accentA2: scale.alphaStep(2), + FortalTokens.accentA3: scale.alphaStep(3), + FortalTokens.accentA4: scale.alphaStep(4), + FortalTokens.accentA5: scale.alphaStep(5), + FortalTokens.accentA6: scale.alphaStep(6), + FortalTokens.accentA7: scale.alphaStep(7), + FortalTokens.accentA8: scale.alphaStep(8), + FortalTokens.accentA9: scale.alphaStep(9), + FortalTokens.accentA10: scale.alphaStep(10), + FortalTokens.accentA11: scale.alphaStep(11), + FortalTokens.accentA12: scale.alphaStep(12), + }; +} diff --git a/packages/remix_fortal/lib/src/theme/theme_scope.dart b/packages/remix_fortal/lib/src/theme/theme_scope.dart new file mode 100644 index 000000000..934e3ee84 --- /dev/null +++ b/packages/remix_fortal/lib/src/theme/theme_scope.dart @@ -0,0 +1,188 @@ +import 'package:flutter/widgets.dart'; +import 'package:mix/mix.dart'; + +import 'theme_data.dart'; +import 'tokens.dart'; + +/// Establishes a courtesy default text run for bare [Text] descendants. +/// +/// `.radix-themes` is not only a token carrier upstream: `color.css` sets +/// `color: var(--gray-12)` in the same rule as the `data-has-background` fill, +/// and `typography.css` pins the root to `--default-font-size` +/// (`--font-size-3`), `--default-line-height`, `--default-letter-spacing`, and +/// `--default-font-weight`. Those resolve to exactly [FortalTokens.text3] plus +/// [FortalTokens.gray12] at regular weight. +/// +/// Fortal text recipes resolve and pin their own runs. This fallback keeps +/// deliberately bare [Text] descendants aligned with Radix's root typography +/// and neutral foreground. A nearer descendant `DefaultTextStyle` still wins +/// through Flutter's normal inheritance. +/// +/// Only the outermost [FortalScope] installs this. A nested scope re-scopes +/// tokens for its subtree and nothing more: upstream, `.radix-themes` inside +/// another `.radix-themes` still inherits `color` and the font properties from +/// its parent chain, and a nested scope that reinstalled the root run here +/// would silently replace whatever `DefaultTextStyle` the subtree sits in. +/// +/// The font family is deliberately left unset. Radix's `--default-font-family` +/// is the platform system stack, and a null family is Flutter's equivalent; +/// naming a concrete family here would pin every consumer to one typeface. +Widget _fortalRootTextStyle({ + required Map tokens, + required Widget child, +}) { + final root = tokens[FortalTokens.text3]! as TextStyle; + + return DefaultTextStyle( + style: root.copyWith( + color: tokens[FortalTokens.gray12]! as Color, + fontWeight: tokens[FortalTokens.fontWeightRegular]! as FontWeight, + ), + child: child, + ); +} + +/// Widget that provides Fortal design tokens to its subtree via [MixScope]. +/// +/// Use [FortalScope] at the root of your app (or around any subtree that uses +/// Fortal styles) so that [FortalTokens] resolve to actual values. +/// +/// ```dart +/// FortalScope( +/// child: WidgetsApp(...), +/// ) +/// ``` +class FortalScope extends StatelessWidget { + const FortalScope({ + super.key, + this.accent, + this.gray, + this.brightness, + this.panelBackground, + this.radius, + this.scaling, + this.hasBackground, + this.orderOfModifiers, + required this.child, + }); + + final FortalAccentColor? accent; + final FortalGrayColor? gray; + final Brightness? brightness; + final FortalPanelBackground? panelBackground; + final FortalRadius? radius; + final FortalScaling? scaling; + final bool? hasBackground; + final List? orderOfModifiers; + final Widget child; + + @override + Widget build(BuildContext context) { + final config = FortalThemeConfig( + accent: accent, + gray: gray, + brightness: brightness, + panelBackground: panelBackground, + radius: radius, + scaling: scaling, + hasBackground: hasBackground, + ); + final parent = FortalTheme.maybeOf(context); + final data = _resolveFortalTheme(config, parent: parent); + final tokens = buildFortalScopeTokens(data); + Widget result = MixScope( + tokens: tokens, + orderOfModifiers: orderOfModifiers, + // Theme-root identity, not `hasBackground`, decides who owns the text + // run: a scope nested for its accent or scaling must leave the current + // run alone, while a root scope with `hasBackground: false` still + // establishes it. + child: parent == null + ? _fortalRootTextStyle(tokens: tokens, child: child) + : child, + ); + if (data.hasBackground) { + result = ColoredBox( + color: tokens[FortalTokens.colorBackground]! as Color, + child: result, + ); + } + + return FortalTheme( + data: data, + orderOfModifiers: orderOfModifiers, + child: result, + ); + } +} + +FortalThemeData _resolveFortalTheme( + FortalThemeConfig config, { + FortalThemeData? parent, +}) { + final accent = config.accent ?? parent?.accent ?? FortalAccentColor.indigo; + + return FortalThemeData( + accent: accent, + gray: config.gray ?? parent?.gray ?? FortalGrayColor.slate, + brightness: config.brightness ?? parent?.brightness ?? Brightness.light, + panelBackground: + config.panelBackground ?? + parent?.panelBackground ?? + FortalPanelBackground.translucent, + radius: config.radius ?? parent?.radius ?? FortalRadius.medium, + scaling: config.scaling ?? parent?.scaling ?? FortalScaling.percent100, + hasBackground: config.hasBackground ?? parent == null, + ); +} + +/// Makes the active [FortalThemeData] available to descendants. +class FortalTheme extends InheritedTheme { + const FortalTheme({ + super.key, + required this.data, + this.orderOfModifiers, + required super.child, + }); + + final FortalThemeData data; + final List? orderOfModifiers; + + /// Returns the closest resolved Fortal theme. + static FortalThemeData of(BuildContext context) { + final data = maybeOf(context); + if (data != null) return data; + throw FlutterError.fromParts([ + ErrorSummary('No FortalTheme found.'), + ErrorDescription( + '${context.widget.runtimeType} tried to read the Fortal theme, but no FortalScope was found above it.', + ), + context.describeElement('The context used was'), + ]); + } + + /// Returns the closest resolved Fortal theme, if one is available. + static FortalThemeData? maybeOf(BuildContext context) => + context.dependOnInheritedWidgetOfExactType()?.data; + + /// Rebuilds only the theme and its Mix tokens. + /// + /// The captured subtree's text run is *not* synthesized here. + /// `DefaultTextStyle` is itself an [InheritedTheme], so + /// `InheritedTheme.capture` already carries the actual nearest ambient run + /// across to the new route; installing the Radix root run alongside it would + /// overwrite that capture with a value the source context never had. + @override + Widget wrap(BuildContext context, Widget child) => FortalTheme( + data: data, + orderOfModifiers: orderOfModifiers, + child: MixScope( + tokens: buildFortalScopeTokens(data), + orderOfModifiers: orderOfModifiers, + child: child, + ), + ); + + @override + bool updateShouldNotify(FortalTheme oldWidget) => data != oldWidget.data; +} diff --git a/packages/remix_fortal/lib/src/theme/tokens.dart b/packages/remix_fortal/lib/src/theme/tokens.dart new file mode 100644 index 000000000..f01b10c21 --- /dev/null +++ b/packages/remix_fortal/lib/src/theme/tokens.dart @@ -0,0 +1,943 @@ +import 'package:remix/remix.dart'; + +import 'theme_scope.dart' show FortalScope; + +/// Design tokens for the Fortal UI system (powered by Radix Colors). +/// +/// Provides color scales (12-step accent/gray), spacing (9-step), radius (6-step), +/// shadows (6-level), typography (9-size), and functional colors. +/// +/// Example: +/// ```dart +/// Style( +/// $box.color.ref(FortalTokens.accent9), +/// $text.style.ref(FortalTokens.text3), +/// $box.padding.ref(FortalTokens.space4), +/// ) +/// ``` +/// +/// Must be used within [FortalScope] to resolve actual values. +class FortalTokens { + // ============================================================================ + // BACKGROUND AND SURFACE COLORS + // ============================================================================ + + /// Page background color selected for the active brightness. + static const colorBackground = ColorToken('fortal.color.background'); + + /// Neutral surface color for input fields and controls. + static const colorSurface = ColorToken('fortal.color.surface'); + + /// Selected SegmentedControl surface for the active brightness. + static const segmentedControlIndicatorBackground = ColorToken( + 'fortal.segmented-control.indicator-background', + ); + + /// Solid panel background selected for the active brightness. + static const colorPanelSolid = ColorToken('fortal.color.panel.solid'); + + /// Translucent panel background with alpha transparency. + static const colorPanelTranslucent = ColorToken( + 'fortal.color.panel.translucent', + ); + + /// Panel background selected by [FortalPanelBackground]. + static const colorPanel = ColorToken('fortal.color.panel'); + + /// Backdrop blur applied to translucent floating panels. + static const panelBlur = DoubleToken('fortal.panel.blur'); + + /// Dark overlay for modals and dialogs. + static const colorOverlay = ColorToken('fortal.color.overlay'); + + // ============================================================================ + // FUNCTIONAL ACCENT COLORS + // ============================================================================ + + /// Subtle accent surface for soft button variants and chips. + static const accentSurface = ColorToken('fortal.accent.surface'); + + /// Active indicator color for progress bars and sliders. + static const accentIndicator = ColorToken('fortal.accent.indicator'); + + /// Track/rail background color for sliders and progress bars. + static const accentTrack = ColorToken('fortal.accent.track'); + + /// Mode-aware overlay used by high-contrast slider ranges. + static const sliderHighContrastOverlay = ColorToken( + 'fortal.slider.high-contrast-overlay', + ); + + /// High contrast foreground for solid accent backgrounds. + static const accentContrast = ColorToken('fortal.accent.contrast'); + + // ============================================================================ + // FOCUS AND INTERACTION STATES + // ============================================================================ + + /// Solid focus ring color (accent step 8). + static const focus8 = ColorToken('fortal.focus.8'); + + /// Translucent text-selection color (accent alpha step 5). + static const focusA5 = ColorToken('fortal.focus.a5'); + + /// Translucent focus ring color with alpha transparency. + static const focusA8 = ColorToken('fortal.focus.a8'); + + /// Mode-aware red roles used by documented validation extensions. + static const error3 = ColorToken('fortal.error.3'); + static const error7 = ColorToken('fortal.error.7'); + static const error8 = ColorToken('fortal.error.8'); + static const error9 = ColorToken('fortal.error.9'); + static const error11 = ColorToken('fortal.error.11'); + static const error12 = ColorToken('fortal.error.12'); + static const errorA7 = ColorToken('fortal.error.a7'); + + // ============================================================================ + // ACCENT COLOR SCALE (12 STEPS) + // ============================================================================ + // + // Fortal uses a 12-step color scale (inherited from Radix Themes) that provides semantic meaning: + // + // Steps 1-2: App backgrounds (subtle → more visible) + // Steps 3-5: Component backgrounds (rest → hover → active) + // Steps 6-8: Borders (subtle → component → hover) + // Steps 9-10: Solid backgrounds (default → hover) + // Steps 11-12: Text (low contrast → high contrast) + // + + /// Accent step 1 - App background, most subtle. + static const accent1 = ColorToken('fortal.accent.1'); + + /// Accent step 2 - Subtle background. + static const accent2 = ColorToken('fortal.accent.2'); + + /// Accent step 3 - Component background at rest. + static const accent3 = ColorToken('fortal.accent.3'); + + /// Accent step 4 - Component background on hover. + static const accent4 = ColorToken('fortal.accent.4'); + + /// Accent step 5 - Component background when active/pressed. + static const accent5 = ColorToken('fortal.accent.5'); + + /// Accent step 6 - Subtle borders and separators. + static const accent6 = ColorToken('fortal.accent.6'); + + /// Accent step 7 - Component borders at rest. + static const accent7 = ColorToken('fortal.accent.7'); + + /// Accent step 8 - Component borders on hover and focus. + static const accent8 = ColorToken('fortal.accent.8'); + + /// Accent step 9 - Primary solid background. + static const accent9 = ColorToken('fortal.accent.9'); + + /// Accent step 10 - Solid background on hover. + static const accent10 = ColorToken('fortal.accent.10'); + + /// Accent step 11 - Low contrast text. + static const accent11 = ColorToken('fortal.accent.11'); + + /// Accent step 12 - High contrast text. + static const accent12 = ColorToken('fortal.accent.12'); + + // ============================================================================ + // GRAY COLOR SCALE (12 STEPS) + // ============================================================================ + // + // The gray scale follows the same 12-step semantic structure as accent colors, + // but provides neutral colors for text, borders, and backgrounds. + // The specific gray variant (slate, mauve, sage, etc.) is chosen in the theme. + // + + /// Gray step 1 - Page background. + static const gray1 = ColorToken('fortal.gray.1'); + + /// Gray step 2 - Panel and card backgrounds. + static const gray2 = ColorToken('fortal.gray.2'); + + /// Gray step 3 - Input backgrounds and pressed states. + static const gray3 = ColorToken('fortal.gray.3'); + + /// Gray step 4 - Input backgrounds on hover. + static const gray4 = ColorToken('fortal.gray.4'); + + /// Gray step 5 - Active states and disabled backgrounds. + static const gray5 = ColorToken('fortal.gray.5'); + + /// Gray step 6 - Subtle borders and dividers. + static const gray6 = ColorToken('fortal.gray.6'); + + /// Gray step 7 - Standard borders and outlines. + /// + /// Primary border color for form inputs, cards, + /// and component boundaries. + static const gray7 = ColorToken('fortal.gray.7'); + + /// Gray step 8 - Borders on hover and focus. + /// + /// Interactive border states and stronger separators + /// that need more visual weight. + static const gray8 = ColorToken('fortal.gray.8'); + + /// Gray step 9 - Solid neutral backgrounds. + /// + /// For neutral buttons, badges, and other elements + /// that need a solid background without accent color. + static const gray9 = ColorToken('fortal.gray.9'); + + /// Gray step 10 - Solid neutral backgrounds on hover. + /// + /// Hover state for neutral solid backgrounds, + /// providing interactive feedback. + static const gray10 = ColorToken('fortal.gray.10'); + + /// Gray step 11 - Low contrast text and secondary content. + /// + /// For secondary text, placeholders, and content that should + /// be readable but not prominent. + static const gray11 = ColorToken('fortal.gray.11'); + + /// Gray step 12 - High contrast text and primary content. + /// + /// Primary text color for body content, headings, and any text + /// that needs maximum readability and prominence. + static const gray12 = ColorToken('fortal.gray.12'); + + // ============================================================================ + // GRAY ROLE TOKENS (parity with generated JSON roles) + // ============================================================================ + /// Neutral surface baseline for the selected gray scale (matches JSON surface) + static const graySurface = ColorToken('fortal.gray.surface'); + + /// Neutral indicator color (typically gray step 9) + static const grayIndicator = ColorToken('fortal.gray.indicator'); + + /// Neutral track color (typically gray step 9) + static const grayTrack = ColorToken('fortal.gray.track'); + + /// Contrast color for content over neutral solid backgrounds (white) + static const grayContrast = ColorToken('fortal.gray.contrast'); + + // ============================================================================ + // ALPHA VARIANTS (FULL 12-STEP FOR ACCENT AND GRAY) + // ============================================================================ + + // Accent alpha steps a1..a12 + static const accentA1 = ColorToken('fortal.accent.a1'); + static const accentA2 = ColorToken('fortal.accent.a2'); + static const accentA3 = ColorToken('fortal.accent.a3'); + static const accentA4 = ColorToken('fortal.accent.a4'); + static const accentA5 = ColorToken('fortal.accent.a5'); + static const accentA6 = ColorToken('fortal.accent.a6'); + static const accentA7 = ColorToken('fortal.accent.a7'); + static const accentA8 = ColorToken('fortal.accent.a8'); + static const accentA9 = ColorToken('fortal.accent.a9'); + static const accentA10 = ColorToken('fortal.accent.a10'); + static const accentA11 = ColorToken('fortal.accent.a11'); + static const accentA12 = ColorToken('fortal.accent.a12'); + + // Gray alpha steps a1..a12 + static const grayA1 = ColorToken('fortal.gray.a1'); + static const grayA2 = ColorToken('fortal.gray.a2'); + static const grayA3 = ColorToken('fortal.gray.a3'); + static const grayA4 = ColorToken('fortal.gray.a4'); + static const grayA5 = ColorToken('fortal.gray.a5'); + static const grayA6 = ColorToken('fortal.gray.a6'); + static const grayA7 = ColorToken('fortal.gray.a7'); + static const grayA8 = ColorToken('fortal.gray.a8'); + static const grayA9 = ColorToken('fortal.gray.a9'); + static const grayA10 = ColorToken('fortal.gray.a10'); + static const grayA11 = ColorToken('fortal.gray.a11'); + static const grayA12 = ColorToken('fortal.gray.a12'); + + // ============================================================================ + // NEUTRALS FOR SHADOWS (HELPER TOKENS) + // ============================================================================ + /// Gray alpha steps are declared above (grayA1..grayA12). + + /// Black alpha steps used in layered shadows. + static const blackA1 = ColorToken('fortal.black.a1'); + static const blackA2 = ColorToken('fortal.black.a2'); + static const blackA3 = ColorToken('fortal.black.a3'); + static const blackA4 = ColorToken('fortal.black.a4'); + static const blackA5 = ColorToken('fortal.black.a5'); + static const blackA6 = ColorToken('fortal.black.a6'); + static const blackA7 = ColorToken('fortal.black.a7'); + static const blackA8 = ColorToken('fortal.black.a8'); + static const blackA9 = ColorToken('fortal.black.a9'); + static const blackA10 = ColorToken('fortal.black.a10'); + static const blackA11 = ColorToken('fortal.black.a11'); + static const blackA12 = ColorToken('fortal.black.a12'); + + /// White alpha steps used by layered classic-control recipes. + static const whiteA1 = ColorToken('fortal.white.a1'); + static const whiteA2 = ColorToken('fortal.white.a2'); + static const whiteA3 = ColorToken('fortal.white.a3'); + static const whiteA4 = ColorToken('fortal.white.a4'); + static const whiteA5 = ColorToken('fortal.white.a5'); + static const whiteA6 = ColorToken('fortal.white.a6'); + static const whiteA7 = ColorToken('fortal.white.a7'); + static const whiteA8 = ColorToken('fortal.white.a8'); + static const whiteA9 = ColorToken('fortal.white.a9'); + static const whiteA10 = ColorToken('fortal.white.a10'); + static const whiteA11 = ColorToken('fortal.white.a11'); + static const whiteA12 = ColorToken('fortal.white.a12'); + + /// Mode-aware mixed shadow stroke. + static const shadowStroke = ColorToken('fortal.shadow.stroke'); + + /// Premultiplied OKLab mixes used by Radix neutral one-pixel strokes. + static const grayStroke3 = ColorToken('fortal.gray.stroke.3'); + static const grayStroke4 = ColorToken('fortal.gray.stroke.4'); + static const grayStroke5 = ColorToken('fortal.gray.stroke.5'); + static const grayStroke6 = ColorToken('fortal.gray.stroke.6'); + static const grayStroke7 = ColorToken('fortal.gray.stroke.7'); + + // ============================================================================ + // SPACING SCALE (9 STEPS) + // ============================================================================ + // + // A consistent spacing scale based on 4px increments. + // + + /// Space step 1 - 4px. + /// + /// Smallest spacing for tight layouts, borders, + /// and fine-grained adjustments. + static const space1 = SpaceToken('fortal.space.1'); + + /// Space step 2 - 8px. + /// + /// Small spacing for component padding and margins. + /// Good for button padding and form element spacing. + static const space2 = SpaceToken('fortal.space.2'); + + /// Space step 3 - 12px. + /// + /// Medium-small spacing for comfortable padding + /// and moderate element separation. + static const space3 = SpaceToken('fortal.space.3'); + + /// Space step 4 - 16px. + /// + /// Standard spacing for most layouts. Good default + /// for card padding and section margins. + static const space4 = SpaceToken('fortal.space.4'); + + /// Space step 5 - 24px. + /// + /// Medium spacing for generous padding and + /// comfortable separation between sections. + static const space5 = SpaceToken('fortal.space.5'); + + /// Space step 6 - 32px. + /// + /// Large spacing for significant visual separation + /// and generous component padding. + static const space6 = SpaceToken('fortal.space.6'); + + /// Space step 7 - 40px. + /// + /// Extra large spacing for major layout sections + /// and prominent visual separation. + static const space7 = SpaceToken('fortal.space.7'); + + /// Space step 8 - 48px. + /// + /// Very large spacing for significant page sections + /// and major layout boundaries. + static const space8 = SpaceToken('fortal.space.8'); + + /// Space step 9 - 64px. + /// + /// Maximum spacing for major page sections + /// and substantial layout separation. + static const space9 = SpaceToken('fortal.space.9'); + + /// Spinner size 3 - 20px at 100% scaling. + /// + /// Radix defines this as 1.25 times space 4, so it needs its own resolved + /// token rather than arithmetic on an unresolved token reference. + static const spinnerSize3 = DoubleToken('fortal.spinner.size.3'); + + /// Compact gap shared by size-1 toggle extensions (2px at 100% scaling). + static const toggleGap1 = DoubleToken('fortal.toggle.gap.1'); + + /// Comfortable gap shared by size-3 toggle extensions (6px at 100%). + static const toggleGap3 = DoubleToken('fortal.toggle.gap.3'); + + /// Avatar sizes expressed as scaled pixels rather than spacing steps. + static const avatarSize6 = DoubleToken('fortal.avatar.size.6'); + static const avatarSize7 = DoubleToken('fortal.avatar.size.7'); + static const avatarSize8 = DoubleToken('fortal.avatar.size.8'); + static const avatarSize9 = DoubleToken('fortal.avatar.size.9'); + + /// Avatar icon sizes are half of each resolved avatar dimension. + /// + /// These values need dedicated tokens because arithmetic on an unresolved + /// token reference would destroy its identity before Mix can resolve it. + static const avatarIconSize1 = DoubleToken('fortal.avatar.icon-size.1'); + static const avatarIconSize2 = DoubleToken('fortal.avatar.icon-size.2'); + static const avatarIconSize3 = DoubleToken('fortal.avatar.icon-size.3'); + static const avatarIconSize4 = DoubleToken('fortal.avatar.icon-size.4'); + static const avatarIconSize5 = DoubleToken('fortal.avatar.icon-size.5'); + static const avatarIconSize6 = DoubleToken('fortal.avatar.icon-size.6'); + static const avatarIconSize7 = DoubleToken('fortal.avatar.icon-size.7'); + static const avatarIconSize8 = DoubleToken('fortal.avatar.icon-size.8'); + static const avatarIconSize9 = DoubleToken('fortal.avatar.icon-size.9'); + + /// Badge measurements that are fractional spacing expressions upstream. + static const badgePaddingX1 = DoubleToken('fortal.badge.padding-x.1'); + static const badgePaddingY1 = DoubleToken('fortal.badge.padding-y.1'); + static const badgePaddingX3 = DoubleToken('fortal.badge.padding-x.3'); + + /// Checkbox dimensions expressed as scaled pixels by Radix Themes. + static const checkboxSize1 = DoubleToken('fortal.checkbox.size.1'); + static const checkboxSize3 = DoubleToken('fortal.checkbox.size.3'); + static const checkboxIndicatorSize1 = DoubleToken( + 'fortal.checkbox.indicator-size.1', + ); + static const checkboxIndicatorSize2 = DoubleToken( + 'fortal.checkbox.indicator-size.2', + ); + static const checkboxIndicatorSize3 = DoubleToken( + 'fortal.checkbox.indicator-size.3', + ); + + /// Checkbox-group label gaps derived from Radix's size-linked `0.5em`. + static const checkboxGroupItemGap1 = DoubleToken( + 'fortal.checkbox-group.item-gap.1', + ); + static const checkboxGroupItemGap2 = DoubleToken( + 'fortal.checkbox-group.item-gap.2', + ); + static const checkboxGroupItemGap3 = DoubleToken( + 'fortal.checkbox-group.item-gap.3', + ); + + /// Radio indicators are 40% of their control size in Radix Themes. + /// + /// These values need dedicated tokens because arithmetic on an unresolved + /// token reference would destroy its identity before Mix can resolve it. + static const radioIndicatorSize1 = DoubleToken( + 'fortal.radio.indicator-size.1', + ); + static const radioIndicatorSize2 = DoubleToken( + 'fortal.radio.indicator-size.2', + ); + static const radioIndicatorSize3 = DoubleToken( + 'fortal.radio.indicator-size.3', + ); + + /// Checkbox radii derived from fractional radius-step expressions. + static const checkboxRadius1 = RadiusToken('fortal.checkbox.radius.1'); + static const checkboxRadius3 = RadiusToken('fortal.checkbox.radius.3'); + + /// Switch geometry that cannot be derived from unresolved token references. + static const switchHeight2 = DoubleToken('fortal.switch.height.2'); + static const switchWidth1 = DoubleToken('fortal.switch.width.1'); + static const switchWidth2 = DoubleToken('fortal.switch.width.2'); + static const switchWidth3 = DoubleToken('fortal.switch.width.3'); + static const switchThumbSize1 = DoubleToken('fortal.switch.thumb-size.1'); + static const switchThumbSize2 = DoubleToken('fortal.switch.thumb-size.2'); + static const switchThumbSize3 = DoubleToken('fortal.switch.thumb-size.3'); + + /// Progress geometry derived from scaled fractional upstream expressions. + static const progressHeight2 = DoubleToken('fortal.progress.height.2'); + static const progressRadius1 = RadiusToken('fortal.progress.radius.1'); + static const progressRadius2 = RadiusToken('fortal.progress.radius.2'); + static const progressRadius3 = RadiusToken('fortal.progress.radius.3'); + + /// Slider geometry expressed as scaled Radix component dimensions. + static const sliderTrackSize1 = DoubleToken('fortal.slider.track-size.1'); + static const sliderTrackSize2 = DoubleToken('fortal.slider.track-size.2'); + static const sliderTrackSize3 = DoubleToken('fortal.slider.track-size.3'); + static const sliderThumbSize1 = DoubleToken('fortal.slider.thumb-size.1'); + static const sliderThumbSize2 = DoubleToken('fortal.slider.thumb-size.2'); + static const sliderThumbSize3 = DoubleToken('fortal.slider.thumb-size.3'); + static const sliderTrackRadius1 = RadiusToken('fortal.slider.track-radius.1'); + static const sliderTrackRadius2 = RadiusToken('fortal.slider.track-radius.2'); + static const sliderTrackRadius3 = RadiusToken('fortal.slider.track-radius.3'); + + /// TextField content insets after its fixed one-pixel border. + static const textFieldPadding1 = DoubleToken('fortal.text-field.padding.1'); + static const textFieldPadding2 = DoubleToken('fortal.text-field.padding.2'); + static const textFieldPadding3 = DoubleToken('fortal.text-field.padding.3'); + + /// TextArea metrics that cannot be expressed by existing spacing tokens. + static const textAreaMinHeight3 = DoubleToken( + 'fortal.text-area.min-height.3', + ); + + /// DataList metrics that cannot be expressed by existing spacing tokens. + static const dataListRowGap3 = DoubleToken('fortal.data-list.row-gap.3'); + static const dataListLabelMinWidth = DoubleToken( + 'fortal.data-list.label-min-width', + ); + + /// Exact uppercase fallback typography for each Avatar size. + static const avatarFallback1One = TextStyleToken( + 'fortal.avatar.fallback.1.one', + ); + static const avatarFallback1Two = TextStyleToken( + 'fortal.avatar.fallback.1.two', + ); + static const avatarFallback2One = TextStyleToken( + 'fortal.avatar.fallback.2.one', + ); + static const avatarFallback2Two = TextStyleToken( + 'fortal.avatar.fallback.2.two', + ); + static const avatarFallback3One = TextStyleToken( + 'fortal.avatar.fallback.3.one', + ); + static const avatarFallback3Two = TextStyleToken( + 'fortal.avatar.fallback.3.two', + ); + static const avatarFallback4One = TextStyleToken( + 'fortal.avatar.fallback.4.one', + ); + static const avatarFallback4Two = TextStyleToken( + 'fortal.avatar.fallback.4.two', + ); + static const avatarFallback5 = TextStyleToken('fortal.avatar.fallback.5'); + static const avatarFallback6 = TextStyleToken('fortal.avatar.fallback.6'); + static const avatarFallback7 = TextStyleToken('fortal.avatar.fallback.7'); + static const avatarFallback8 = TextStyleToken('fortal.avatar.fallback.8'); + static const avatarFallback9 = TextStyleToken('fortal.avatar.fallback.9'); + + /// Tabs size 1 inner vertical padding - 2px at 100% scaling. + static const tabInnerPaddingY1 = DoubleToken('fortal.tabs.inner-padding-y.1'); + + /// Tabs size 1 active tracking - -0.12px at 100% scaling. + static const tabActiveLetterSpacing1 = DoubleToken( + 'fortal.tabs.active-letter-spacing.1', + ); + + /// Tabs size 2 active tracking - -0.14px at 100% scaling. + static const tabActiveLetterSpacing2 = DoubleToken( + 'fortal.tabs.active-letter-spacing.2', + ); + + /// Table size-1 minimum cell height (36px at 100% scaling). + /// + /// Radix writes `calc(36px * var(--scaling))` literally, so no existing + /// spacing step expresses it. + static const dataTableRowHeight1 = DoubleToken('fortal.data-table.height.1'); + + /// Table size-2 minimum cell height (44px at 100% scaling). + static const dataTableRowHeight2 = DoubleToken('fortal.data-table.height.2'); + + /// Table surface border - `color-mix(in oklab, gray-a5, gray-6)`. + /// + /// The existing `grayStroke*` tokens blend an alpha step with the *same* + /// numbered solid step at 25%; Table blends step 5 with step 6 at 50%. + static const dataTableBorder = ColorToken('fortal.data-table.border'); + + /// Select's 1.5 × space-1 measurement (6px at 100% scaling). + static const selectSpace1Half = DoubleToken('fortal.select.space.1-half'); + + /// Select size-1 indicator column width (20px at 100% scaling). + static const selectIndicatorWidth1 = DoubleToken( + 'fortal.select.indicator-width.1', + ); + + /// Select size-1 check size (8px at 100% scaling). + static const selectIndicatorSize1 = DoubleToken( + 'fortal.select.indicator-size.1', + ); + + /// Select size-2/3 check size (10px at 100% scaling). + static const selectIndicatorSize2 = DoubleToken( + 'fortal.select.indicator-size.2', + ); + + /// Negative margins that cancel Select ghost-trigger padding. + static const selectGhostMarginX12 = DoubleToken( + 'fortal.select.ghost-margin-x.1-2', + ); + static const selectGhostMarginY12 = DoubleToken( + 'fortal.select.ghost-margin-y.1-2', + ); + static const selectGhostMarginX3 = DoubleToken( + 'fortal.select.ghost-margin-x.3', + ); + static const selectGhostMarginY3 = DoubleToken( + 'fortal.select.ghost-margin-y.3', + ); + + // ============================================================================ + // BORDER RADIUS SCALE (6 STEPS + FULL) + // ============================================================================ + + /// Radius step 1 - 3px. + /// + /// Subtle rounding for small elements like buttons + /// and form inputs. Provides gentle softening of corners. + static const radius1 = RadiusToken('fortal.radius.1'); + + /// Radius step 2 - 4px. + /// + /// Small radius for compact components and minor rounding. + /// Good for small badges and tight layouts. + static const radius2 = RadiusToken('fortal.radius.2'); + + /// Radius step 3 - 6px. + /// + /// Medium radius for standard components like buttons + /// and cards. Balances modern look with usability. + static const radius3 = RadiusToken('fortal.radius.3'); + + /// Radius step 4 - 8px. + /// + /// Large radius for prominent components and generous rounding. + /// Good for larger buttons and feature cards. + static const radius4 = RadiusToken('fortal.radius.4'); + + /// Radius step 5 - 12px. + /// + /// Extra large radius for major components and modern aesthetics. + /// Suitable for large cards and prominent interface elements. + static const radius5 = RadiusToken('fortal.radius.5'); + + /// Radius step 6 - 16px. + /// + /// Very large radius for distinctive styling and major components. + /// Creates a soft, friendly appearance for large interface elements. + static const radius6 = RadiusToken('fortal.radius.6'); + + /// Theme-level full radius, enabled only by [FortalRadius.full]. + static const radiusFull = RadiusToken('fortal.radius.full'); + + /// Radius used by control thumbs. + static const radiusThumb = RadiusToken('fortal.radius.thumb'); + + /// Fixed circle radius for shapes that stay circular across theme presets. + static const radiusCircle = RadiusToken('fortal.radius.circle'); + + /// Radius step 1 promoted to a pill when the theme radius is full. + static const radius1OrFull = RadiusToken('fortal.radius.1-or-full'); + + /// Radius step 2 promoted to a pill when the theme radius is full. + static const radius2OrFull = RadiusToken('fortal.radius.2-or-full'); + + /// Radius step 3 promoted to a pill when the theme radius is full. + static const radius3OrFull = RadiusToken('fortal.radius.3-or-full'); + + /// Radius step 4 promoted to a pill when the theme radius is full. + static const radius4OrFull = RadiusToken('fortal.radius.4-or-full'); + + /// Radius step 5 promoted to a pill when the theme radius is full. + static const radius5OrFull = RadiusToken('fortal.radius.5-or-full'); + + /// Radius step 6 promoted to a pill when the theme radius is full. + static const radius6OrFull = RadiusToken('fortal.radius.6-or-full'); + + /// Radius step 1 promoted to the control-thumb radius when larger. + static const radius1OrThumb = RadiusToken('fortal.radius.1-or-thumb'); + + /// Radius step 2 promoted to the control-thumb radius when larger. + static const radius2OrThumb = RadiusToken('fortal.radius.2-or-thumb'); + + // ============================================================================ + // ELEVATION SHADOWS (6 LEVELS) + // ============================================================================ + + /// Shadow level 1 - Subtle elevation. + /// + /// Minimal shadow for slight elevation effects. + /// Good for cards and buttons in their resting state. + static const shadow1 = BoxShadowToken('fortal.shadow.1'); + + /// Exact layered shadow level 1, including inset layers. + /// + /// This additive token powers Fortal's Radix-compatible rendering while + /// [shadow1] retains the original Remix public token type. + static const shadow1Layers = RemixBoxShadowListToken( + 'fortal.shadow.1.layers', + ); + + /// Half-opacity shadow-1 layers used by a disabled classic slider track. + static const sliderClassicDisabledTrackShadows = RemixBoxShadowListToken( + 'fortal.slider.classic.disabled-track-shadows', + ); + + /// Shadow level 2 - Low elevation. + /// + /// Light shadow for gentle elevation and hover states. + /// Suitable for interactive elements and small modals. + static const shadow2 = BoxShadowToken('fortal.shadow.2'); + + /// Shadow-2 painted on SegmentedControl's fixed one-pixel inset shape. + static const segmentedControlClassicIndicatorShadows = + RemixBoxShadowListToken( + 'fortal.segmented-control.classic.indicator-shadows', + ); + + /// Shadow level 3 - Medium elevation. + /// + /// Moderate shadow for clear visual separation. + /// Good for dropdowns, tooltips, and floating elements. + static const shadow3 = BoxShadowToken('fortal.shadow.3'); + + /// Shadow level 4 - High elevation. + /// + /// Prominent shadow for important floating content. + /// Suitable for modal dialogs and important overlays. + static const shadow4 = BoxShadowToken('fortal.shadow.4'); + + /// Shadow level 5 - Very high elevation. + /// + /// Strong shadow for primary modals and major overlays. + /// Creates clear hierarchy and focus on important content. + static const shadow5 = BoxShadowToken('fortal.shadow.5'); + + /// Shadow level 6 - Maximum elevation. + /// + /// Maximum shadow depth for critical dialogs and notifications. + /// Ensures content appears above all other interface elements. + static const shadow6 = BoxShadowToken('fortal.shadow.6'); + + /// Card classic outer and inset-pseudo-element shadow lists. + static const cardClassicOuterShadows = RemixBoxShadowListToken( + 'fortal.card.classic.outer-shadows', + ); + static const cardClassicInnerShadows = RemixBoxShadowListToken( + 'fortal.card.classic.inner-shadows', + ); + static const cardClassicHoverOuterShadows = RemixBoxShadowListToken( + 'fortal.card.classic.hover.outer-shadows', + ); + static const cardClassicHoverInnerShadows = RemixBoxShadowListToken( + 'fortal.card.classic.hover.inner-shadows', + ); + static const cardClassicActiveOuterShadows = RemixBoxShadowListToken( + 'fortal.card.classic.active.outer-shadows', + ); + static const cardClassicActiveInnerShadows = RemixBoxShadowListToken( + 'fortal.card.classic.active.inner-shadows', + ); + + /// Mode-aware inset layers for a classic Select trigger. + static const selectTriggerClassicShadows = RemixBoxShadowListToken( + 'fortal.select.trigger.classic.shadows', + ); + + /// Mode-aware open/hover layers for a classic Select trigger. + static const selectTriggerClassicHoverShadows = RemixBoxShadowListToken( + 'fortal.select.trigger.classic.hover-shadows', + ); + + /// Mode-aware disabled layers shared by classic button-shaped controls. + static const baseButtonClassicDisabledShadows = RemixBoxShadowListToken( + 'fortal.base-button.classic.disabled.shadows', + ); + + /// Mode-aware classic Button/IconButton layers. + static const baseButtonClassicShadows = RemixBoxShadowListToken( + 'fortal.base-button.classic.shadows', + ); + static const baseButtonClassicHighContrastShadows = RemixBoxShadowListToken( + 'fortal.base-button.classic.high-contrast.shadows', + ); + static const baseButtonClassicActiveShadows = RemixBoxShadowListToken( + 'fortal.base-button.classic.active.shadows', + ); + static const baseButtonClassicActiveHighContrastShadows = + RemixBoxShadowListToken( + 'fortal.base-button.classic.active.high-contrast.shadows', + ); + static const baseButtonClassicAfterInset = DoubleToken( + 'fortal.base-button.classic.after-inset', + ); + static const baseButtonGhostPaddingY3 = DoubleToken( + 'fortal.base-button.ghost.padding-y.3', + ); + static const baseButtonGhostMarginX12 = DoubleToken( + 'fortal.base-button.ghost.margin-x.1-2', + ); + static const baseButtonGhostMarginY12 = DoubleToken( + 'fortal.base-button.ghost.margin-y.1-2', + ); + static const baseButtonGhostMarginX3 = DoubleToken( + 'fortal.base-button.ghost.margin-x.3', + ); + static const baseButtonGhostMarginY3 = DoubleToken( + 'fortal.base-button.ghost.margin-y.3', + ); + static const baseButtonGhostMarginX4 = DoubleToken( + 'fortal.base-button.ghost.margin-x.4', + ); + static const baseButtonGhostMarginY4 = DoubleToken( + 'fortal.base-button.ghost.margin-y.4', + ); + static const iconButtonGhostPadding2 = DoubleToken( + 'fortal.icon-button.ghost.padding.2', + ); + static const iconButtonGhostMargin1 = DoubleToken( + 'fortal.icon-button.ghost.margin.1', + ); + static const iconButtonGhostMargin2 = DoubleToken( + 'fortal.icon-button.ghost.margin.2', + ); + static const iconButtonGhostMargin3 = DoubleToken( + 'fortal.icon-button.ghost.margin.3', + ); + static const iconButtonGhostMargin4 = DoubleToken( + 'fortal.icon-button.ghost.margin.4', + ); + static const cardGhostMargin1 = DoubleToken('fortal.card.ghost.margin.1'); + static const cardGhostMargin2 = DoubleToken('fortal.card.ghost.margin.2'); + static const cardGhostMargin3 = DoubleToken('fortal.card.ghost.margin.3'); + static const cardGhostMargin4 = DoubleToken('fortal.card.ghost.margin.4'); + static const cardGhostMargin5 = DoubleToken('fortal.card.ghost.margin.5'); + + // ============================================================================ + // BORDER AND STROKE WIDTHS + // ============================================================================ + + /// Standard border width (1px). + /// + /// Default border thickness for most components like inputs, + /// cards, and dividers. Provides clear boundaries without visual weight. + static const borderWidth1 = SpaceToken('fortal.border.width.1'); + + /// Thick border width (2px). + /// + /// Heavier border for emphasis, selected states, and components + /// that need stronger visual definition. + static const borderWidth2 = SpaceToken('fortal.border.width.2'); + + /// Focus ring border width (2px). + /// + /// Standard width for focus outlines to ensure accessibility + /// compliance and clear keyboard navigation feedback. + static const focusRingWidth = SpaceToken('fortal.focus.ring.width'); + + /// Focus ring offset distance from element edge. + /// + /// Space between the component border and focus ring, + /// ensuring the focus indicator doesn't interfere with the element. + static const focusRingOffset = SpaceToken('fortal.focus.ring.offset'); + + // ============================================================================ + // TYPOGRAPHY SCALE (9 LEVELS) + // ============================================================================ + // + // Text sizes with carefully tuned line heights and letter spacing + // for optimal readability across all scales. + // + + /// Text size 1 - 12px (Small labels and metadata). + /// + /// Smallest readable text for labels, captions, and secondary metadata. + /// Includes tight letter spacing for improved legibility at small sizes. + static const text1 = TextStyleToken('fortal.text.1'); + + /// Text size 2 - 14px (Standard UI text). + /// + /// Default size for most interface text including buttons, + /// form labels, and secondary content. + static const text2 = TextStyleToken('fortal.text.2'); + + /// Text size 3 - 16px (Body text and primary content). + /// + /// Ideal for body text and primary content. Provides excellent + /// readability for extended reading on all device types. + static const text3 = TextStyleToken('fortal.text.3'); + + /// Accordion size-2 text (15px with a 20px line box at 100% scaling). + /// + /// Accordion is a Fortal extension, so this intermediate size is kept + /// separate from the upstream Radix typography scale. + static const accordionText2 = TextStyleToken('fortal.accordion.text.2'); + + /// Text size 4 - 18px (Prominent body text). + /// + /// For important content that needs more visual weight than + /// standard body text but isn't quite a heading. + static const text4 = TextStyleToken('fortal.text.4'); + + /// Text size 5 - 20px (Small headings). + /// + /// For minor headings, subheadings, and content that needs + /// to stand out from body text. + static const text5 = TextStyleToken('fortal.text.5'); + + /// Text size 6 - 24px (Medium headings). + /// + /// Standard heading size for section titles and important content. + /// Good balance between prominence and page economy. + static const text6 = TextStyleToken('fortal.text.6'); + + /// Text size 7 - 28px (Large headings). + /// + /// For major page headings and important announcements. + /// Creates strong visual hierarchy and draws attention. + static const text7 = TextStyleToken('fortal.text.7'); + + /// Text size 8 - 35px (Extra large headings). + /// + /// For hero text, page titles, and major content sections. + /// Strong negative letter spacing improves appearance at large sizes. + static const text8 = TextStyleToken('fortal.text.8'); + + /// Text size 9 - 60px (Display text). + /// + /// Maximum text size for hero sections and display typography. + /// Includes significant negative letter spacing and tight line height. + static const text9 = TextStyleToken('fortal.text.9'); + + // ============================================================================ + // FONT WEIGHT TOKENS + // ============================================================================ + + /// Light font weight (300). + /// + /// Optional lighter weight occasionally used in display typography or + /// subdued text. Provided for parity with Radix token set. + static const fontWeightLight = FontWeightToken('fortal.font.weight.light'); + + /// Regular font weight (400). + /// + /// Standard weight for body text and most interface elements. + /// Provides good readability without visual strain. + static const fontWeightRegular = FontWeightToken( + 'fortal.font.weight.regular', + ); + + /// Medium font weight (500). + /// + /// Slightly heavier than regular for UI elements that need + /// more visual weight, like active states and button text. + static const fontWeightMedium = FontWeightToken('fortal.font.weight.medium'); + + /// Bold font weight (700). + /// + /// For headings and content that needs strong emphasis. + /// Provides clear hierarchy without being too heavy. + static const fontWeightBold = FontWeightToken('fortal.font.weight.bold'); + + // ============================================================================ + // ANIMATION DURATIONS + // ============================================================================ + + /// Fast animation duration (100ms). + /// + /// For quick micro-interactions like hover states and button presses. + /// Provides immediate feedback without feeling sluggish. + static const transitionFast = DurationToken('fortal.transition.fast'); + + /// Slow animation duration (300ms). + /// + /// For more substantial transitions like modal appearances, + /// page transitions, and complex state changes. + static const transitionSlow = DurationToken('fortal.transition.slow'); + + /// One leg of the Radix Skeleton pulse. + static const skeletonPulseDuration = DurationToken( + 'fortal.skeleton.pulse-duration', + ); +} diff --git a/packages/remix_fortal/reference/radix_themes_3_3_0/README.md b/packages/remix_fortal/reference/radix_themes_3_3_0/README.md index f934a58e7..6e25efb09 100644 --- a/packages/remix_fortal/reference/radix_themes_3_3_0/README.md +++ b/packages/remix_fortal/reference/radix_themes_3_3_0/README.md @@ -16,7 +16,7 @@ description, such as The contract tracks 30 mapped Radix families and three Fortal extensions. Text, Heading, Code, Kbd, and Link publish one shared nine-step `FortalTextSize`, so their `size` enum resolves against -`lib/src/recipes/typography_shared.dart` rather than five parallel per-family +`lib/src/components/typography.dart` rather than five parallel per-family enums. `upstreamInventory.families` must name exactly the tracked family set, so a new family fails the gate until both the Fortal mapping and the family-independent upstream record are written. diff --git a/packages/remix_fortal/scripts/README.md b/packages/remix_fortal/scripts/README.md index 2698ae115..e1e63c32e 100644 --- a/packages/remix_fortal/scripts/README.md +++ b/packages/remix_fortal/scripts/README.md @@ -20,7 +20,7 @@ radix_colors.generated.json # checked in; excluded from the published a │ │ generate_radix_colors.dart ▼ -lib/src/radix/colors/colors_generated.dart # checked in; shipped +lib/src/theme/radix_colors.dart # checked in; shipped ``` ### `extract_radix_tokens.dart` @@ -44,7 +44,7 @@ The tarball URL and its integrity hash are recorded in ### `generate_radix_colors.dart` Renders `radix_colors.generated.json` into the Dart swatch table at -`lib/src/radix/colors/colors_generated.dart`. +`lib/src/theme/radix_colors.dart`. ```sh fvm dart run scripts/generate_radix_colors.dart diff --git a/packages/remix_fortal/scripts/generate_radix_colors.dart b/packages/remix_fortal/scripts/generate_radix_colors.dart index 53652d5d9..5df389a60 100644 --- a/packages/remix_fortal/scripts/generate_radix_colors.dart +++ b/packages/remix_fortal/scripts/generate_radix_colors.dart @@ -8,7 +8,7 @@ void main() async { class RadixColorGenerator { static const String jsonPath = 'radix_colors.generated.json'; - static const String outputPath = 'lib/src/radix/colors/colors_generated.dart'; + static const String outputPath = 'lib/src/theme/radix_colors.dart'; const RadixColorGenerator(); void _generateColorInstance( StringBuffer buffer, @@ -203,7 +203,7 @@ class RadixColorGenerator { buffer.writeln('// Radix Colors version: ${meta['radix_colors_version']}'); buffer.writeln('// Source integrity: ${meta['source_integrity']}'); buffer.writeln(); - buffer.writeln('part of \'colors.dart\';'); + buffer.write(_radixColorApi); buffer.writeln(); // Generate color scale instances @@ -252,3 +252,96 @@ class RadixColorGenerator { print(' - ${neutrals.length} neutral colors'); } } + +const _radixColorApi = ''' +library radix_colors; + +import 'package:flutter/painting.dart'; + +class RadixColor { + final RadixColorScale scale; + final Color surface; + final Color indicator; + final Color track; + final Color contrast; + + const RadixColor( + this.scale, + this.surface, + this.indicator, + this.track, + this.contrast, + ); +} + +class RadixColorTheme { + final RadixColor light; + final RadixColor dark; + + const RadixColorTheme(this.light, this.dark); +} + +class RadixColorScale { + final ColorSwatch solid; + final ColorSwatch alpha; + + const RadixColorScale(this.solid, this.alpha); + + // Semantic color accessors for better code readability + + /// The most subtle background color (step 1). + Color get appBackground => step(1); + + /// Subtle background with slightly more presence (step 2). + Color get subtleBackground => step(2); + + /// Default background for interactive components (step 3). + Color get componentBackground => step(3); + + /// Background color for components on hover (step 4). + Color get componentBackgroundHover => step(4); + + /// Background color for active/pressed components (step 5). + Color get componentBackgroundActive => step(5); + + /// Subtle border color for gentle separation (step 6). + Color get subtleBorder => step(6); + + /// Standard border color for components (step 7). + Color get componentBorder => step(7); + + /// Border color for hover and focus states (step 8). + Color get componentBorderHover => step(8); + + /// Primary solid background color (step 9). + Color get solidBackground => step(9); + + /// Solid background color on hover (step 10). + Color get solidBackgroundHover => step(10); + + /// Low contrast text color (step 11). + Color get lowContrastText => step(11); + + /// High contrast text color (step 12). + Color get highContrastText => step(12); + + /// Gets a solid color from the 12-step scale. + /// + /// Steps must be between 1 and 12. Falls back to step 9 if unavailable. + Color step(int n) { + assert(n >= 1 && n <= 12, 'Step must be between 1 and 12'); + + return solid[n] ?? solid[9]!; + } + + /// Gets a translucent color from the 12-step alpha scale. + /// + /// Alpha variants maintain saturation when composited. + /// Falls back to alpha step 9 if unavailable. + Color alphaStep(int n) { + assert(n >= 1 && n <= 12, 'Step must be between 1 and 12'); + + return alpha[n] ?? alpha[9]!; + } +} +'''; diff --git a/packages/remix_fortal/test/components/accordion/accordion_widget_test.dart b/packages/remix_fortal/test/components/accordion/accordion_widget_test.dart index cafbeb5d2..8c6927254 100644 --- a/packages/remix_fortal/test/components/accordion/accordion_widget_test.dart +++ b/packages/remix_fortal/test/components/accordion/accordion_widget_test.dart @@ -6,7 +6,7 @@ import 'package:remix_fortal/remix_fortal.dart'; import '../../helpers/test_helpers.dart'; // Panel anatomy derives from the mapped Table family (see -// packages/remix_fortal/lib/src/recipes/data_table.dart): the container owns +// packages/remix_fortal/lib/src/components/data_table.dart): the container owns // radius, foreground frame, fill, and clipping so the trigger and content crop into // one rounded shape instead of each rounding their own corners. These tests // pin that contract directly, plus the content font size that keeps a diff --git a/packages/remix_fortal/tool/fortal_parity/check.dart b/packages/remix_fortal/tool/fortal_parity/check.dart index 7b39b6643..f9bfbfcdf 100644 --- a/packages/remix_fortal/tool/fortal_parity/check.dart +++ b/packages/remix_fortal/tool/fortal_parity/check.dart @@ -483,13 +483,13 @@ String? _readFortalStylesSource( String id, List failures, ) { - // Recipes are flat under lib/src/recipes/, and the TextField and TextArea - // recipes share one file because TextArea reuses TextField's private helpers. + // Components are flat under lib/src/components/, and TextField and TextArea + // share one file because TextArea reuses TextField's private helpers. final recipeName = switch (id) { 'text_field' || 'text_area' => 'textfield', _ => id, }; - final file = File('${packageRoot.path}/lib/src/recipes/$recipeName.dart'); + final file = File('${packageRoot.path}/lib/src/components/$recipeName.dart'); if (!file.existsSync()) { failures.add('$id is missing Fortal recipe source ${file.path}.'); return null; @@ -499,9 +499,7 @@ String? _readFortalStylesSource( // The shared scale, weights, and flow helpers live beside the five // typography recipes rather than inside any one of them. - final shared = File( - '${packageRoot.path}/lib/src/recipes/typography_shared.dart', - ); + final shared = File('${packageRoot.path}/lib/src/components/typography.dart'); if (!shared.existsSync()) { failures.add('$id is missing shared typography source ${shared.path}.'); return source; @@ -1258,14 +1256,14 @@ Set _dartEnumValues(String body) { } void _checkVariantConstructors(Directory packageRoot, List failures) { - final recipeRoot = Directory('${packageRoot.path}/lib/src/recipes'); + final recipeRoot = Directory('${packageRoot.path}/lib/src/components'); for (final entity in recipeRoot.listSync()) { if (entity is! File || !entity.path.endsWith('.dart') || entity.path.endsWith('.g.dart')) { continue; } - // Recipes are flat, so pair each one with its same-stem `.g.dart` rather + // Components are flat, so pair each one with its same-stem `.g.dart` rather // than concatenating every generated file in the directory. final generated = File( entity.path.replaceFirst(RegExp(r'\.dart$'), '.g.dart'), diff --git a/tool/generate_fortal_catalog.dart b/tool/generate_fortal_catalog.dart index 10b6fc176..14fa8e440 100644 --- a/tool/generate_fortal_catalog.dart +++ b/tool/generate_fortal_catalog.dart @@ -311,7 +311,7 @@ String _recipePath(String id) { 'text_field' || 'text_area' => 'textfield', _ => id, }; - return 'lib/src/recipes/$recipeName.dart'; + return 'lib/src/components/$recipeName.dart'; } String _otherDefaults(Map family, Map enums) { From 4958efb8d7f63b844cb9e7b809968b17f241053f Mon Sep 17 00:00:00 2001 From: Leo Farias Date: Fri, 4 Sep 2026 13:29:53 -0400 Subject: [PATCH 02/12] feat(remix_fortal): add instance style overrides --- packages/remix/lib/remix.dart | 6 + .../lib/src/components/accordion.dart | 6 +- .../lib/src/components/accordion.g.dart | 11 +- .../lib/src/components/avatar.dart | 5 +- .../lib/src/components/avatar.g.dart | 6 + .../lib/src/components/badge.dart | 5 +- .../lib/src/components/badge.g.dart | 8 ++ .../lib/src/components/base_button.dart | 9 +- .../lib/src/components/button.dart | 3 +- .../lib/src/components/button.g.dart | 10 ++ .../lib/src/components/callout.dart | 5 +- .../lib/src/components/callout.g.dart | 7 ++ .../remix_fortal/lib/src/components/card.dart | 5 +- .../lib/src/components/card.g.dart | 33 +++-- .../lib/src/components/chart.dart | 46 ++++--- .../lib/src/components/chart.g.dart | 12 ++ .../lib/src/components/checkbox.dart | 10 +- .../lib/src/components/checkbox.g.dart | 14 +++ .../remix_fortal/lib/src/components/code.dart | 18 ++- .../lib/src/components/data_list.dart | 4 +- .../lib/src/components/data_list.g.dart | 4 + .../lib/src/components/data_table.dart | 5 +- .../lib/src/components/data_table.g.dart | 11 +- .../lib/src/components/dialog.dart | 4 +- .../lib/src/components/dialog.g.dart | 9 +- .../lib/src/components/disclosure.dart | 6 +- .../lib/src/components/disclosure.g.dart | 11 +- .../lib/src/components/divider.dart | 4 +- .../lib/src/components/divider.g.dart | 9 +- .../lib/src/components/heading.dart | 18 +-- .../lib/src/components/icon_button.dart | 3 +- .../lib/src/components/icon_button.g.dart | 10 ++ .../remix_fortal/lib/src/components/kbd.dart | 20 ++- .../remix_fortal/lib/src/components/link.dart | 9 +- .../remix_fortal/lib/src/components/menu.dart | 3 +- .../lib/src/components/menu.g.dart | 6 + .../lib/src/components/popover.dart | 4 +- .../lib/src/components/popover.g.dart | 5 +- .../lib/src/components/progress.dart | 5 +- .../lib/src/components/progress.g.dart | 7 ++ .../lib/src/components/radio.dart | 5 +- .../lib/src/components/radio.g.dart | 7 ++ .../lib/src/components/segmented_control.dart | 4 +- .../src/components/segmented_control.g.dart | 6 + .../lib/src/components/select.dart | 4 +- .../lib/src/components/select.g.dart | 7 ++ .../lib/src/components/sidebar.dart | 3 +- .../lib/src/components/sidebar.g.dart | 4 + .../lib/src/components/skeleton.dart | 7 +- .../lib/src/components/skeleton.g.dart | 11 +- .../lib/src/components/slider.dart | 5 +- .../lib/src/components/slider.g.dart | 7 ++ .../lib/src/components/spinner.dart | 7 +- .../lib/src/components/spinner.g.dart | 5 +- .../lib/src/components/switch.dart | 5 +- .../lib/src/components/switch.g.dart | 7 ++ .../remix_fortal/lib/src/components/tabs.dart | 23 ++-- .../lib/src/components/tabs.g.dart | 24 +++- .../remix_fortal/lib/src/components/text.dart | 17 +-- .../lib/src/components/text.g.dart | 4 + .../lib/src/components/textfield.dart | 20 +-- .../lib/src/components/textfield.g.dart | 24 +++- .../lib/src/components/toggle.dart | 5 +- .../lib/src/components/toggle.g.dart | 6 + .../lib/src/components/toggle_group.dart | 3 +- .../lib/src/components/toggle_group.g.dart | 6 + .../lib/src/components/tooltip.dart | 7 +- .../lib/src/components/tooltip.g.dart | 5 +- .../remix_fortal/lib/src/theme/computed.dart | 4 +- .../lib/src/theme/control_styles.dart | 9 +- .../lib/src/theme/theme_data.dart | 3 +- .../lib/src/theme/theme_scope.dart | 2 +- .../fortal/fortal_style_override_test.dart | 118 ++++++++++++++++++ 73 files changed, 595 insertions(+), 155 deletions(-) create mode 100644 packages/remix_fortal/test/fortal/fortal_style_override_test.dart diff --git a/packages/remix/lib/remix.dart b/packages/remix/lib/remix.dart index bcbf5f777..34dc3db0e 100644 --- a/packages/remix/lib/remix.dart +++ b/packages/remix/lib/remix.dart @@ -40,6 +40,12 @@ export 'src/components/tooltip/tooltip.dart'; export 'package:mix/mix.dart'; export 'package:naked_ui/naked_ui.dart' show + NakedAccordionTriggerBuilder, + NakedAccordionItemState, + NakedDisclosureState, + NakedDisclosureTransitionBuilder, + NakedSliderSemanticFormatterCallback, + NakedTabState, NakedMenuState, OverlayAlignment, OverlayPlacement, diff --git a/packages/remix_fortal/lib/src/components/accordion.dart b/packages/remix_fortal/lib/src/components/accordion.dart index e2c1d8634..03dc9763b 100644 --- a/packages/remix_fortal/lib/src/components/accordion.dart +++ b/packages/remix_fortal/lib/src/components/accordion.dart @@ -1,6 +1,5 @@ import 'package:flutter/widgets.dart'; import 'package:mix_annotations/mix_annotations.dart'; -import 'package:naked_ui/naked_ui.dart'; import 'package:remix/remix.dart'; import '../theme/theme.dart'; @@ -18,11 +17,12 @@ enum FortalAccordionVariant { surface, soft } AccordionStyler fortalAccordionStyle({ FortalAccordionVariant variant = .surface, FortalAccordionSize size = .size2, + AccordionStyler style = const AccordionStyler.create(), }) { - return switch (variant) { + return (switch (variant) { .surface => _fortalAccordionSurfaceStyler(size), .soft => _fortalAccordionSoftStyler(size), - }; + }).merge(style); } // Panel anatomy follows the mapped Table family (see data_table.dart): diff --git a/packages/remix_fortal/lib/src/components/accordion.g.dart b/packages/remix_fortal/lib/src/components/accordion.g.dart index 0fe51a447..c7fd70b7f 100644 --- a/packages/remix_fortal/lib/src/components/accordion.g.dart +++ b/packages/remix_fortal/lib/src/components/accordion.g.dart @@ -12,6 +12,7 @@ class FortalAccordion extends StatelessWidget { super.key, this.variant = .surface, this.size = .size2, + this.style = const AccordionStyler.create(), required this.value, required this.child, this.title, @@ -33,6 +34,7 @@ class FortalAccordion extends StatelessWidget { const FortalAccordion.surface({ super.key, this.size = .size2, + this.style = const AccordionStyler.create(), required this.value, required this.child, this.title, @@ -54,6 +56,7 @@ class FortalAccordion extends StatelessWidget { const FortalAccordion.soft({ super.key, this.size = .size2, + this.style = const AccordionStyler.create(), required this.value, required this.child, this.title, @@ -76,6 +79,8 @@ class FortalAccordion extends StatelessWidget { final FortalAccordionSize size; + final AccordionStyler style; + final T value; final Widget child; @@ -112,7 +117,11 @@ class FortalAccordion extends StatelessWidget { Widget build(BuildContext context) { return RemixAccordion( key: this.key, - style: fortalAccordionStyle(variant: this.variant, size: this.size), + style: fortalAccordionStyle( + variant: this.variant, + size: this.size, + style: this.style, + ), value: this.value, child: this.child, title: this.title, diff --git a/packages/remix_fortal/lib/src/components/avatar.dart b/packages/remix_fortal/lib/src/components/avatar.dart index b0765210e..5982f36fc 100644 --- a/packages/remix_fortal/lib/src/components/avatar.dart +++ b/packages/remix_fortal/lib/src/components/avatar.dart @@ -32,6 +32,7 @@ AvatarStyler fortalAvatarStyle({ FortalAvatarSize size = .size3, bool highContrast = false, int fallbackLength = 1, + AvatarStyler style = const AvatarStyler.create(), }) { final base = _fortalAvatarBaseStyler(size, fallbackLength: fallbackLength); final softContent = highContrast @@ -40,7 +41,7 @@ AvatarStyler fortalAvatarStyle({ final solidContent = highContrast ? FortalTokens.accent1() : FortalTokens.accentContrast(); - return switch (variant) { + return (switch (variant) { .soft => base .color(FortalTokens.accentA3()) @@ -53,7 +54,7 @@ AvatarStyler fortalAvatarStyle({ ) .labelColor(solidContent) .iconColor(solidContent), - }; + }).merge(style); } AvatarStyler _fortalAvatarBaseStyler( diff --git a/packages/remix_fortal/lib/src/components/avatar.g.dart b/packages/remix_fortal/lib/src/components/avatar.g.dart index 91fca750a..e6057d0e3 100644 --- a/packages/remix_fortal/lib/src/components/avatar.g.dart +++ b/packages/remix_fortal/lib/src/components/avatar.g.dart @@ -17,6 +17,7 @@ class FortalAvatar extends StatelessWidget { this.size = .size3, this.highContrast = false, this.fallbackLength = 1, + this.style = const AvatarStyler.create(), this.backgroundImage, this.foregroundImage, this.onBackgroundImageError, @@ -33,6 +34,7 @@ class FortalAvatar extends StatelessWidget { this.size = .size3, this.highContrast = false, this.fallbackLength = 1, + this.style = const AvatarStyler.create(), this.backgroundImage, this.foregroundImage, this.onBackgroundImageError, @@ -49,6 +51,7 @@ class FortalAvatar extends StatelessWidget { this.size = .size3, this.highContrast = false, this.fallbackLength = 1, + this.style = const AvatarStyler.create(), this.backgroundImage, this.foregroundImage, this.onBackgroundImageError, @@ -68,6 +71,8 @@ class FortalAvatar extends StatelessWidget { final int fallbackLength; + final AvatarStyler style; + final ImageProvider? backgroundImage; final ImageProvider? foregroundImage; @@ -95,6 +100,7 @@ class FortalAvatar extends StatelessWidget { size: this.size, highContrast: this.highContrast, fallbackLength: this.fallbackLength, + style: this.style, ), backgroundImage: this.backgroundImage, foregroundImage: this.foregroundImage, diff --git a/packages/remix_fortal/lib/src/components/badge.dart b/packages/remix_fortal/lib/src/components/badge.dart index 15c1e018f..329dcae79 100644 --- a/packages/remix_fortal/lib/src/components/badge.dart +++ b/packages/remix_fortal/lib/src/components/badge.dart @@ -18,9 +18,10 @@ BadgeStyler fortalBadgeStyle({ FortalBadgeVariant variant = .soft, FortalBadgeSize size = .size1, bool highContrast = false, + BadgeStyler style = const BadgeStyler.create(), }) { final base = _fortalBadgeBaseStyler(size); - return switch (variant) { + return (switch (variant) { .solid => base .color( @@ -67,7 +68,7 @@ BadgeStyler fortalBadgeStyle({ .labelColor( highContrast ? FortalTokens.accent12() : FortalTokens.accentA11(), ), - }; + }).merge(style); } BadgeStyler _fortalBadgeBaseStyler(FortalBadgeSize size) { diff --git a/packages/remix_fortal/lib/src/components/badge.g.dart b/packages/remix_fortal/lib/src/components/badge.g.dart index eef3328eb..08d466b21 100644 --- a/packages/remix_fortal/lib/src/components/badge.g.dart +++ b/packages/remix_fortal/lib/src/components/badge.g.dart @@ -13,6 +13,7 @@ class FortalBadge extends StatelessWidget { this.variant = .soft, this.size = .size1, this.highContrast = false, + this.style = const BadgeStyler.create(), this.label, this.child, this.labelBuilder, @@ -22,6 +23,7 @@ class FortalBadge extends StatelessWidget { super.key, this.size = .size1, this.highContrast = false, + this.style = const BadgeStyler.create(), this.label, this.child, this.labelBuilder, @@ -31,6 +33,7 @@ class FortalBadge extends StatelessWidget { super.key, this.size = .size1, this.highContrast = false, + this.style = const BadgeStyler.create(), this.label, this.child, this.labelBuilder, @@ -40,6 +43,7 @@ class FortalBadge extends StatelessWidget { super.key, this.size = .size1, this.highContrast = false, + this.style = const BadgeStyler.create(), this.label, this.child, this.labelBuilder, @@ -49,6 +53,7 @@ class FortalBadge extends StatelessWidget { super.key, this.size = .size1, this.highContrast = false, + this.style = const BadgeStyler.create(), this.label, this.child, this.labelBuilder, @@ -60,6 +65,8 @@ class FortalBadge extends StatelessWidget { final bool highContrast; + final BadgeStyler style; + final String? label; final Widget? child; @@ -74,6 +81,7 @@ class FortalBadge extends StatelessWidget { variant: this.variant, size: this.size, highContrast: this.highContrast, + style: this.style, ), label: this.label, child: this.child, diff --git a/packages/remix_fortal/lib/src/components/base_button.dart b/packages/remix_fortal/lib/src/components/base_button.dart index 60bce668b..29d17eeeb 100644 --- a/packages/remix_fortal/lib/src/components/base_button.dart +++ b/packages/remix_fortal/lib/src/components/base_button.dart @@ -1,12 +1,5 @@ import 'package:flutter/widgets.dart'; -import 'package:mix/mix.dart'; -import 'package:remix/remix.dart' - show - RemixBoxEffectLayerMix, - RemixBoxEffectsMix, - RemixCssColorFilterOperation, - RemixLinearGradientMix, - RemixOrderedColorFilterModifier; +import 'package:remix/remix.dart'; import '../theme/theme.dart'; diff --git a/packages/remix_fortal/lib/src/components/button.dart b/packages/remix_fortal/lib/src/components/button.dart index 960038bc3..829b8611d 100644 --- a/packages/remix_fortal/lib/src/components/button.dart +++ b/packages/remix_fortal/lib/src/components/button.dart @@ -19,6 +19,7 @@ ButtonStyler fortalButtonStyle({ FortalButtonVariant variant = .solid, FortalButtonSize size = .size2, bool highContrast = false, + ButtonStyler style = const ButtonStyler.create(), }) { final base = _fortalButtonBaseStyler(variant, _fortalBaseButtonSize(size)); final stateStyles = fortalBaseButtonStateStyles( @@ -30,7 +31,7 @@ ButtonStyler fortalButtonStyle({ base, stateStyles, pressedPaddingTop: variant == .classic ? (size == .size1 ? 1 : 2) : null, - ); + ).merge(style); } ButtonStyler _fortalButtonBaseStyler( diff --git a/packages/remix_fortal/lib/src/components/button.g.dart b/packages/remix_fortal/lib/src/components/button.g.dart index 21e86af41..b4353bcec 100644 --- a/packages/remix_fortal/lib/src/components/button.g.dart +++ b/packages/remix_fortal/lib/src/components/button.g.dart @@ -13,6 +13,7 @@ class FortalButton extends StatelessWidget { this.variant = .solid, this.size = .size2, this.highContrast = false, + this.style = const ButtonStyler.create(), required this.label, this.leadingIcon, this.trailingIcon, @@ -37,6 +38,7 @@ class FortalButton extends StatelessWidget { super.key, this.size = .size2, this.highContrast = false, + this.style = const ButtonStyler.create(), required this.label, this.leadingIcon, this.trailingIcon, @@ -61,6 +63,7 @@ class FortalButton extends StatelessWidget { super.key, this.size = .size2, this.highContrast = false, + this.style = const ButtonStyler.create(), required this.label, this.leadingIcon, this.trailingIcon, @@ -85,6 +88,7 @@ class FortalButton extends StatelessWidget { super.key, this.size = .size2, this.highContrast = false, + this.style = const ButtonStyler.create(), required this.label, this.leadingIcon, this.trailingIcon, @@ -109,6 +113,7 @@ class FortalButton extends StatelessWidget { super.key, this.size = .size2, this.highContrast = false, + this.style = const ButtonStyler.create(), required this.label, this.leadingIcon, this.trailingIcon, @@ -133,6 +138,7 @@ class FortalButton extends StatelessWidget { super.key, this.size = .size2, this.highContrast = false, + this.style = const ButtonStyler.create(), required this.label, this.leadingIcon, this.trailingIcon, @@ -157,6 +163,7 @@ class FortalButton extends StatelessWidget { super.key, this.size = .size2, this.highContrast = false, + this.style = const ButtonStyler.create(), required this.label, this.leadingIcon, this.trailingIcon, @@ -183,6 +190,8 @@ class FortalButton extends StatelessWidget { final bool highContrast; + final ButtonStyler style; + final String label; final IconData? leadingIcon; @@ -227,6 +236,7 @@ class FortalButton extends StatelessWidget { variant: this.variant, size: this.size, highContrast: this.highContrast, + style: this.style, ), label: this.label, leadingIcon: this.leadingIcon, diff --git a/packages/remix_fortal/lib/src/components/callout.dart b/packages/remix_fortal/lib/src/components/callout.dart index 86a2afe15..dee4a40e5 100644 --- a/packages/remix_fortal/lib/src/components/callout.dart +++ b/packages/remix_fortal/lib/src/components/callout.dart @@ -18,6 +18,7 @@ CalloutStyler fortalCalloutStyle({ FortalCalloutVariant variant = .soft, FortalCalloutSize size = .size2, bool highContrast = false, + CalloutStyler style = const CalloutStyler.create(), }) { final contentColor = highContrast ? FortalTokens.accent12() @@ -25,7 +26,7 @@ CalloutStyler fortalCalloutStyle({ final base = _fortalCalloutBaseStyler( size, ).iconColor(contentColor).textColor(contentColor); - return switch (variant) { + return (switch (variant) { .soft => base.color(FortalTokens.accentA3()), .surface => base @@ -40,7 +41,7 @@ CalloutStyler fortalCalloutStyle({ fortalInsetSurface(strokes: [FortalTokens.accentA7()]), ), ), - }; + }).merge(style); } CalloutStyler _fortalCalloutBaseStyler(FortalCalloutSize size) { diff --git a/packages/remix_fortal/lib/src/components/callout.g.dart b/packages/remix_fortal/lib/src/components/callout.g.dart index 9873d365d..b3583c443 100644 --- a/packages/remix_fortal/lib/src/components/callout.g.dart +++ b/packages/remix_fortal/lib/src/components/callout.g.dart @@ -13,6 +13,7 @@ class FortalCallout extends StatelessWidget { this.variant = .soft, this.size = .size2, this.highContrast = false, + this.style = const CalloutStyler.create(), this.text, this.icon, this.child, @@ -22,6 +23,7 @@ class FortalCallout extends StatelessWidget { super.key, this.size = .size2, this.highContrast = false, + this.style = const CalloutStyler.create(), this.text, this.icon, this.child, @@ -31,6 +33,7 @@ class FortalCallout extends StatelessWidget { super.key, this.size = .size2, this.highContrast = false, + this.style = const CalloutStyler.create(), this.text, this.icon, this.child, @@ -40,6 +43,7 @@ class FortalCallout extends StatelessWidget { super.key, this.size = .size2, this.highContrast = false, + this.style = const CalloutStyler.create(), this.text, this.icon, this.child, @@ -51,6 +55,8 @@ class FortalCallout extends StatelessWidget { final bool highContrast; + final CalloutStyler style; + final String? text; final IconData? icon; @@ -65,6 +71,7 @@ class FortalCallout extends StatelessWidget { variant: this.variant, size: this.size, highContrast: this.highContrast, + style: this.style, ), text: this.text, icon: this.icon, diff --git a/packages/remix_fortal/lib/src/components/card.dart b/packages/remix_fortal/lib/src/components/card.dart index f63a48719..8f691bc50 100644 --- a/packages/remix_fortal/lib/src/components/card.dart +++ b/packages/remix_fortal/lib/src/components/card.dart @@ -17,6 +17,7 @@ enum FortalCardVariant { surface, classic, ghost } CardStyler fortalCardStyle({ FortalCardVariant variant = .surface, FortalCardSize size = .size1, + CardStyler style = const CardStyler.create(), }) { final metrics = _fortalCardMetrics(size); final base = CardStyler() @@ -36,11 +37,11 @@ CardStyler fortalCardStyle({ ), ); - return switch (variant) { + return (switch (variant) { .surface => _fortalCardSurface(base), .classic => _fortalCardClassic(base), .ghost => _fortalCardGhost(base, metrics.ghostMargin), - }; + }).merge(style); } ({double padding, double ghostMargin, Radius radius}) _fortalCardMetrics( diff --git a/packages/remix_fortal/lib/src/components/card.g.dart b/packages/remix_fortal/lib/src/components/card.g.dart index e010a5e78..cc14c3ee7 100644 --- a/packages/remix_fortal/lib/src/components/card.g.dart +++ b/packages/remix_fortal/lib/src/components/card.g.dart @@ -12,29 +12,48 @@ class FortalCard extends StatelessWidget { super.key, this.variant = .surface, this.size = .size1, + this.style = const CardStyler.create(), this.child, }); - const FortalCard.surface({super.key, this.size = .size1, this.child}) - : variant = FortalCardVariant.surface; + const FortalCard.surface({ + super.key, + this.size = .size1, + this.style = const CardStyler.create(), + this.child, + }) : variant = FortalCardVariant.surface; - const FortalCard.classic({super.key, this.size = .size1, this.child}) - : variant = FortalCardVariant.classic; + const FortalCard.classic({ + super.key, + this.size = .size1, + this.style = const CardStyler.create(), + this.child, + }) : variant = FortalCardVariant.classic; - const FortalCard.ghost({super.key, this.size = .size1, this.child}) - : variant = FortalCardVariant.ghost; + const FortalCard.ghost({ + super.key, + this.size = .size1, + this.style = const CardStyler.create(), + this.child, + }) : variant = FortalCardVariant.ghost; final FortalCardVariant variant; final FortalCardSize size; + final CardStyler style; + final Widget? child; @override Widget build(BuildContext context) { return RemixCard( key: this.key, - style: fortalCardStyle(variant: this.variant, size: this.size), + style: fortalCardStyle( + variant: this.variant, + size: this.size, + style: this.style, + ), child: this.child, ); } diff --git a/packages/remix_fortal/lib/src/components/chart.dart b/packages/remix_fortal/lib/src/components/chart.dart index e160d910a..2686304c7 100644 --- a/packages/remix_fortal/lib/src/components/chart.dart +++ b/packages/remix_fortal/lib/src/components/chart.dart @@ -1,5 +1,4 @@ import 'package:flutter/widgets.dart'; -import 'package:mix/mix.dart'; import 'package:mix_annotations/mix_annotations.dart'; import 'package:mix_chart/mix_chart.dart'; import 'package:remix/remix.dart'; @@ -55,8 +54,9 @@ LineChartStyler fortalLineChartStyle({ bool highContrast = false, bool showMarkers = false, List? palette, + LineChartStyler style = const LineChartStyler.create(), }) { - final style = LineChartStyler() + final recipe = LineChartStyler() .frame(_fortalFrameStyle()) .axis(_fortalAxisStyle()) .topAxis(_hiddenAxisStyle()) @@ -86,11 +86,13 @@ LineChartStyler fortalLineChartStyle({ ) .tooltip(_fortalTooltipStyle()); - return style.merge( - LineChartStyler.create( - palette: _paletteProp(highContrast: highContrast, palette: palette), - ), - ); + return recipe + .merge( + LineChartStyler.create( + palette: _paletteProp(highContrast: highContrast, palette: palette), + ), + ) + .merge(style); } /// Fortal presentation for a Mix grouped, stacked, or floating bar chart. @@ -100,11 +102,12 @@ LineChartStyler fortalLineChartStyle({ BarChartStyler fortalBarChartStyle({ bool highContrast = false, List? palette, + BarChartStyler style = const BarChartStyler.create(), }) { final bar = BarStyler.create( borderRadius: Prop.token(_barRadiusToken), ).width(FortalTokens.space4()); - final style = BarChartStyler() + final recipe = BarChartStyler() .frame(_fortalFrameStyle()) .axis(_fortalAxisStyle()) .topAxis(_hiddenAxisStyle()) @@ -115,11 +118,13 @@ BarChartStyler fortalBarChartStyle({ .barSpacing(FortalTokens.space2()) .tooltip(_fortalTooltipStyle()); - return style.merge( - BarChartStyler.create( - palette: _paletteProp(highContrast: highContrast, palette: palette), - ), - ); + return recipe + .merge( + BarChartStyler.create( + palette: _paletteProp(highContrast: highContrast, palette: palette), + ), + ) + .merge(style); } /// Fortal presentation for a Mix pie or donut chart. @@ -135,8 +140,9 @@ PieChartStyler fortalPieChartStyle({ double centerRadius = 0, bool showLabels = false, List? palette, + PieChartStyler style = const PieChartStyler.create(), }) { - final style = PieChartStyler() + final recipe = PieChartStyler() .frame(_fortalFrameStyle()) .centerRadius(centerRadius) .centerColor(FortalTokens.colorPanel()) @@ -155,11 +161,13 @@ PieChartStyler fortalPieChartStyle({ ) .tooltip(_fortalTooltipStyle()); - return style.merge( - PieChartStyler.create( - palette: _paletteProp(highContrast: highContrast, palette: palette), - ), - ); + return recipe + .merge( + PieChartStyler.create( + palette: _paletteProp(highContrast: highContrast, palette: palette), + ), + ) + .merge(style); } Prop> _paletteProp({ diff --git a/packages/remix_fortal/lib/src/components/chart.g.dart b/packages/remix_fortal/lib/src/components/chart.g.dart index d91db52e9..9b36c0469 100644 --- a/packages/remix_fortal/lib/src/components/chart.g.dart +++ b/packages/remix_fortal/lib/src/components/chart.g.dart @@ -16,6 +16,7 @@ class FortalLineChart extends StatelessWidget { this.highContrast = false, this.showMarkers = false, this.palette, + this.style = const LineChartStyler.create(), required this.series, this.xAxis, this.yAxis, @@ -41,6 +42,8 @@ class FortalLineChart extends StatelessWidget { final List? palette; + final LineChartStyler style; + final List series; final ChartAxis? xAxis; @@ -83,6 +86,7 @@ class FortalLineChart extends StatelessWidget { highContrast: this.highContrast, showMarkers: this.showMarkers, palette: this.palette, + style: this.style, ), series: this.series, xAxis: this.xAxis, @@ -113,6 +117,7 @@ class FortalBarChart extends StatelessWidget { super.key, this.highContrast = false, this.palette, + this.style = const BarChartStyler.create(), required this.groups, this.xAxis, this.yAxis, @@ -136,6 +141,8 @@ class FortalBarChart extends StatelessWidget { final List? palette; + final BarChartStyler style; + final List groups; final ChartAxis? xAxis; @@ -177,6 +184,7 @@ class FortalBarChart extends StatelessWidget { style: fortalBarChartStyle( highContrast: this.highContrast, palette: this.palette, + style: this.style, ), groups: this.groups, xAxis: this.xAxis, @@ -213,6 +221,7 @@ class FortalPieChart extends StatelessWidget { this.centerRadius = 0, this.showLabels = false, this.palette, + this.style = const PieChartStyler.create(), required this.slices, this.dataTransition = ChartDataTransition.none, this.selectedSliceIds = const {}, @@ -235,6 +244,8 @@ class FortalPieChart extends StatelessWidget { final List? palette; + final PieChartStyler style; + final List slices; final ChartDataTransition dataTransition; @@ -268,6 +279,7 @@ class FortalPieChart extends StatelessWidget { centerRadius: this.centerRadius, showLabels: this.showLabels, palette: this.palette, + style: this.style, ), slices: this.slices, dataTransition: this.dataTransition, diff --git a/packages/remix_fortal/lib/src/components/checkbox.dart b/packages/remix_fortal/lib/src/components/checkbox.dart index ad439911b..ec1f834f0 100644 --- a/packages/remix_fortal/lib/src/components/checkbox.dart +++ b/packages/remix_fortal/lib/src/components/checkbox.dart @@ -18,6 +18,7 @@ CheckboxStyler fortalCheckboxStyle({ FortalCheckboxVariant variant = .surface, FortalCheckboxSize size = .size2, bool highContrast = false, + CheckboxStyler style = const CheckboxStyler.create(), }) { final metrics = _fortalCheckboxMetrics(size); final base = @@ -44,11 +45,11 @@ CheckboxStyler fortalCheckboxStyle({ ), ); - return switch (variant) { + return (switch (variant) { .classic => _fortalCheckboxClassic(base, highContrast), .surface => _fortalCheckboxSurface(base, highContrast), .soft => _fortalCheckboxSoft(base, highContrast), - }; + }).merge(style); } /// Fortal recipe for [RemixCheckboxGroupItem]. @@ -67,6 +68,7 @@ CheckboxStyler fortalCheckboxGroupItemStyle({ FortalCheckboxVariant variant = .surface, FortalCheckboxSize size = .size2, bool highContrast = false, + CheckboxStyler style = const CheckboxStyler.create(), }) { final checkbox = fortalCheckboxStyle( variant: variant, @@ -74,7 +76,7 @@ CheckboxStyler fortalCheckboxGroupItemStyle({ highContrast: highContrast, ); - return switch (size) { + return (switch (size) { .size1 => checkbox .label(.style(FortalTokens.text1.mix())) @@ -87,7 +89,7 @@ CheckboxStyler fortalCheckboxGroupItemStyle({ checkbox .label(.style(FortalTokens.text3.mix())) .labelSpacing(FortalTokens.checkboxGroupItemGap3()), - }; + }).merge(style); } ({double size, double indicatorSize, Radius radius}) _fortalCheckboxMetrics( diff --git a/packages/remix_fortal/lib/src/components/checkbox.g.dart b/packages/remix_fortal/lib/src/components/checkbox.g.dart index 53e535f74..dd60aa9f9 100644 --- a/packages/remix_fortal/lib/src/components/checkbox.g.dart +++ b/packages/remix_fortal/lib/src/components/checkbox.g.dart @@ -13,6 +13,7 @@ class FortalCheckbox extends StatelessWidget { this.variant = .surface, this.size = .size2, this.highContrast = false, + this.style = const CheckboxStyler.create(), required this.selected, this.onChanged, this.enabled = true, @@ -33,6 +34,7 @@ class FortalCheckbox extends StatelessWidget { super.key, this.size = .size2, this.highContrast = false, + this.style = const CheckboxStyler.create(), required this.selected, this.onChanged, this.enabled = true, @@ -53,6 +55,7 @@ class FortalCheckbox extends StatelessWidget { super.key, this.size = .size2, this.highContrast = false, + this.style = const CheckboxStyler.create(), required this.selected, this.onChanged, this.enabled = true, @@ -73,6 +76,7 @@ class FortalCheckbox extends StatelessWidget { super.key, this.size = .size2, this.highContrast = false, + this.style = const CheckboxStyler.create(), required this.selected, this.onChanged, this.enabled = true, @@ -95,6 +99,8 @@ class FortalCheckbox extends StatelessWidget { final bool highContrast; + final CheckboxStyler style; + final bool? selected; final ValueChanged? onChanged; @@ -131,6 +137,7 @@ class FortalCheckbox extends StatelessWidget { variant: this.variant, size: this.size, highContrast: this.highContrast, + style: this.style, ), selected: this.selected, onChanged: this.onChanged, @@ -167,6 +174,7 @@ class FortalCheckboxGroupItem extends StatelessWidget { this.variant = .surface, this.size = .size2, this.highContrast = false, + this.style = const CheckboxStyler.create(), required this.value, required this.label, this.semanticLabel, @@ -184,6 +192,7 @@ class FortalCheckboxGroupItem extends StatelessWidget { super.key, this.size = .size2, this.highContrast = false, + this.style = const CheckboxStyler.create(), required this.value, required this.label, this.semanticLabel, @@ -201,6 +210,7 @@ class FortalCheckboxGroupItem extends StatelessWidget { super.key, this.size = .size2, this.highContrast = false, + this.style = const CheckboxStyler.create(), required this.value, required this.label, this.semanticLabel, @@ -218,6 +228,7 @@ class FortalCheckboxGroupItem extends StatelessWidget { super.key, this.size = .size2, this.highContrast = false, + this.style = const CheckboxStyler.create(), required this.value, required this.label, this.semanticLabel, @@ -237,6 +248,8 @@ class FortalCheckboxGroupItem extends StatelessWidget { final bool highContrast; + final CheckboxStyler style; + final T value; final String label; @@ -267,6 +280,7 @@ class FortalCheckboxGroupItem extends StatelessWidget { variant: this.variant, size: this.size, highContrast: this.highContrast, + style: this.style, ), value: this.value, label: this.label, diff --git a/packages/remix_fortal/lib/src/components/code.dart b/packages/remix_fortal/lib/src/components/code.dart index 950caef3a..88b7db1c3 100644 --- a/packages/remix_fortal/lib/src/components/code.dart +++ b/packages/remix_fortal/lib/src/components/code.dart @@ -24,6 +24,7 @@ BadgeStyler fortalCodeStyle( bool truncate = false, bool accent = false, bool highContrast = false, + BadgeStyler style = const BadgeStyler.create(), }) { final base = fortalResolveTextToken(context, size ?? FortalTextSize.size3); final baseFontSize = base.fontSize!; @@ -103,7 +104,7 @@ BadgeStyler fortalCodeStyle( } if (foreground != null) textStyle = textStyle.color(foreground); - var style = BadgeStyler() + var recipe = BadgeStyler() .label(textStyle) .borderRadius( BorderRadiusGeometryMix.circular( @@ -111,18 +112,18 @@ BadgeStyler fortalCodeStyle( ), ); if (decorated) { - style = style.padding( + recipe = recipe.padding( EdgeInsetsGeometryMix.symmetric( horizontal: 0.25 * fontSize, vertical: 0.10 * fontSize, ), ); } - if (fill != null) style = style.color(fill); + if (fill != null) recipe = recipe.color(fill); if (variant == .outline) { final ringWidth = math.max(1.0, 0.033 * fontSize); - style = style.containerEffects( + recipe = recipe.containerEffects( RemixBoxEffectsMix.behindContent( RemixBoxEffectLayerMix( shadows: [ @@ -146,7 +147,7 @@ BadgeStyler fortalCodeStyle( ); } - return style; + return recipe.merge(style); } final class _AmbientCodeForegroundDirective extends Directive { @@ -219,6 +220,7 @@ class FortalCode extends StatelessWidget { this.truncate = false, this.accent = false, this.highContrast = false, + this.style = const BadgeStyler.create(), }) : assert(text != ''); const FortalCode.solid( @@ -230,6 +232,7 @@ class FortalCode extends StatelessWidget { this.truncate = false, this.accent = false, this.highContrast = false, + this.style = const BadgeStyler.create(), }) : variant = FortalCodeVariant.solid, assert(text != ''); @@ -242,6 +245,7 @@ class FortalCode extends StatelessWidget { this.truncate = false, this.accent = false, this.highContrast = false, + this.style = const BadgeStyler.create(), }) : variant = FortalCodeVariant.soft, assert(text != ''); @@ -254,6 +258,7 @@ class FortalCode extends StatelessWidget { this.truncate = false, this.accent = false, this.highContrast = false, + this.style = const BadgeStyler.create(), }) : variant = FortalCodeVariant.outline, assert(text != ''); @@ -266,6 +271,7 @@ class FortalCode extends StatelessWidget { this.truncate = false, this.accent = false, this.highContrast = false, + this.style = const BadgeStyler.create(), }) : variant = FortalCodeVariant.ghost, assert(text != ''); @@ -277,6 +283,7 @@ class FortalCode extends StatelessWidget { final bool truncate; final bool accent; final bool highContrast; + final BadgeStyler style; @override Widget build(BuildContext context) => fortalCodeStyle( @@ -288,5 +295,6 @@ class FortalCode extends StatelessWidget { truncate: truncate, accent: accent, highContrast: highContrast, + style: style, )(label: text); } diff --git a/packages/remix_fortal/lib/src/components/data_list.dart b/packages/remix_fortal/lib/src/components/data_list.dart index 5c08bbfe0..e90eaf2a8 100644 --- a/packages/remix_fortal/lib/src/components/data_list.dart +++ b/packages/remix_fortal/lib/src/components/data_list.dart @@ -14,6 +14,7 @@ enum FortalDataListSize { size1, size2, size3 } DataListStyler fortalDataListStyle({ FortalDataListSize size = .size2, bool highContrast = false, + DataListStyler style = const DataListStyler.create(), }) { final metrics = _fortalDataListMetrics(size); @@ -35,7 +36,8 @@ DataListStyler fortalDataListStyle({ .rowSpacing(metrics.rowSpacing) .columnSpacing(metrics.rowSpacing) .labelValueSpacing(FortalTokens.space1()) - .minLabelWidth(FortalTokens.dataListLabelMinWidth()); + .minLabelWidth(FortalTokens.dataListLabelMinWidth()) + .merge(style); } ({TextStyleToken text, double rowSpacing}) _fortalDataListMetrics( diff --git a/packages/remix_fortal/lib/src/components/data_list.g.dart b/packages/remix_fortal/lib/src/components/data_list.g.dart index f5180770a..5788311d6 100644 --- a/packages/remix_fortal/lib/src/components/data_list.g.dart +++ b/packages/remix_fortal/lib/src/components/data_list.g.dart @@ -12,6 +12,7 @@ class FortalDataList extends StatelessWidget { super.key, this.size = .size2, this.highContrast = false, + this.style = const DataListStyler.create(), required this.items, this.orientation = Axis.horizontal, this.semanticLabel, @@ -22,6 +23,8 @@ class FortalDataList extends StatelessWidget { final bool highContrast; + final DataListStyler style; + final List items; final Axis orientation; @@ -37,6 +40,7 @@ class FortalDataList extends StatelessWidget { style: fortalDataListStyle( size: this.size, highContrast: this.highContrast, + style: this.style, ), items: this.items, orientation: this.orientation, diff --git a/packages/remix_fortal/lib/src/components/data_table.dart b/packages/remix_fortal/lib/src/components/data_table.dart index d6064d520..793fceb31 100644 --- a/packages/remix_fortal/lib/src/components/data_table.dart +++ b/packages/remix_fortal/lib/src/components/data_table.dart @@ -41,6 +41,7 @@ typedef _FortalDataTableMetrics = ({ DataTableStyler fortalDataTableStyle({ FortalDataTableSize size = .size2, FortalDataTableVariant variant = .ghost, + DataTableStyler style = const DataTableStyler.create(), }) { final metrics = _fortalDataTableMetrics(size); final base = DataTableStyler() @@ -89,10 +90,10 @@ DataTableStyler fortalDataTableStyle({ .pageButton(fortalIconButtonStyle(variant: .ghost, size: .size1)) .pageSizeSelect(fortalSelectStyle(variant: .ghost, size: .size1)); - return switch (variant) { + return (switch (variant) { .surface => _fortalDataTableSurface(base, metrics.radius), .ghost => base.color(const Color(0x00000000)), - }; + }).merge(style); } _FortalDataTableMetrics _fortalDataTableMetrics(FortalDataTableSize size) => diff --git a/packages/remix_fortal/lib/src/components/data_table.g.dart b/packages/remix_fortal/lib/src/components/data_table.g.dart index 054d5f5e3..15063563a 100644 --- a/packages/remix_fortal/lib/src/components/data_table.g.dart +++ b/packages/remix_fortal/lib/src/components/data_table.g.dart @@ -23,6 +23,7 @@ class FortalDataTable extends StatelessWidget { super.key, this.size = .size2, this.variant = .ghost, + this.style = const DataTableStyler.create(), required this.rows, required this.columns, this.semanticLabel, @@ -51,6 +52,7 @@ class FortalDataTable extends StatelessWidget { const FortalDataTable.surface({ super.key, this.size = .size2, + this.style = const DataTableStyler.create(), required this.rows, required this.columns, this.semanticLabel, @@ -79,6 +81,7 @@ class FortalDataTable extends StatelessWidget { const FortalDataTable.ghost({ super.key, this.size = .size2, + this.style = const DataTableStyler.create(), required this.rows, required this.columns, this.semanticLabel, @@ -108,6 +111,8 @@ class FortalDataTable extends StatelessWidget { final FortalDataTableVariant variant; + final DataTableStyler style; + final List rows; final List> columns; @@ -158,7 +163,11 @@ class FortalDataTable extends StatelessWidget { Widget build(BuildContext context) { return RemixDataTable( key: this.key, - style: fortalDataTableStyle(size: this.size, variant: this.variant), + style: fortalDataTableStyle( + size: this.size, + variant: this.variant, + style: this.style, + ), rows: this.rows, columns: this.columns, semanticLabel: this.semanticLabel, diff --git a/packages/remix_fortal/lib/src/components/dialog.dart b/packages/remix_fortal/lib/src/components/dialog.dart index ef4ee1aeb..36dd3699c 100644 --- a/packages/remix_fortal/lib/src/components/dialog.dart +++ b/packages/remix_fortal/lib/src/components/dialog.dart @@ -37,6 +37,7 @@ final _dialogViewportInsets = ContextToken((context) { DialogStyler fortalDialogStyle({ FortalDialogSize size = FortalDialogSize.size3, FortalDialogAlign align = FortalDialogAlign.center, + DialogStyler style = const DialogStyler.create(), }) { final radius = switch (size) { FortalDialogSize.size1 || FortalDialogSize.size2 => FortalTokens.radius4(), @@ -90,5 +91,6 @@ DialogStyler fortalDialogStyle({ ) .containerEffects( RemixBoxEffectsMix.backdropBlur(FortalTokens.panelBlur()), - ); + ) + .merge(style); } diff --git a/packages/remix_fortal/lib/src/components/dialog.g.dart b/packages/remix_fortal/lib/src/components/dialog.g.dart index 57480d3e0..16d3dcefd 100644 --- a/packages/remix_fortal/lib/src/components/dialog.g.dart +++ b/packages/remix_fortal/lib/src/components/dialog.g.dart @@ -16,6 +16,7 @@ class FortalDialog extends StatelessWidget { super.key, this.size = FortalDialogSize.size3, this.align = FortalDialogAlign.center, + this.style = const DialogStyler.create(), this.child, this.title, this.description, @@ -29,6 +30,8 @@ class FortalDialog extends StatelessWidget { final FortalDialogAlign align; + final DialogStyler style; + final Widget? child; final String? title; @@ -47,7 +50,11 @@ class FortalDialog extends StatelessWidget { Widget build(BuildContext context) { return RemixDialog( key: this.key, - style: fortalDialogStyle(size: this.size, align: this.align), + style: fortalDialogStyle( + size: this.size, + align: this.align, + style: this.style, + ), child: this.child, title: this.title, description: this.description, diff --git a/packages/remix_fortal/lib/src/components/disclosure.dart b/packages/remix_fortal/lib/src/components/disclosure.dart index b14c8d392..7b30df01d 100644 --- a/packages/remix_fortal/lib/src/components/disclosure.dart +++ b/packages/remix_fortal/lib/src/components/disclosure.dart @@ -1,6 +1,5 @@ import 'package:flutter/widgets.dart'; import 'package:mix_annotations/mix_annotations.dart'; -import 'package:naked_ui/naked_ui.dart'; import 'package:remix/remix.dart'; import '../theme/theme.dart'; @@ -18,11 +17,12 @@ enum FortalDisclosureVariant { surface, soft } DisclosureStyler fortalDisclosureStyle({ FortalDisclosureVariant variant = .surface, FortalDisclosureSize size = .size2, + DisclosureStyler style = const DisclosureStyler.create(), }) { - return switch (variant) { + return (switch (variant) { .surface => _fortalDisclosureSurfaceStyler(size), .soft => _fortalDisclosureSoftStyler(size), - }; + }).merge(style); } // Panel anatomy follows the mapped Table family (see data_table.dart): diff --git a/packages/remix_fortal/lib/src/components/disclosure.g.dart b/packages/remix_fortal/lib/src/components/disclosure.g.dart index 848f1f7a8..331df50a4 100644 --- a/packages/remix_fortal/lib/src/components/disclosure.g.dart +++ b/packages/remix_fortal/lib/src/components/disclosure.g.dart @@ -12,6 +12,7 @@ class FortalDisclosure extends StatelessWidget { super.key, this.variant = .surface, this.size = .size2, + this.style = const DisclosureStyler.create(), required this.trigger, required this.content, this.triggerBuilder, @@ -40,6 +41,7 @@ class FortalDisclosure extends StatelessWidget { const FortalDisclosure.surface({ super.key, this.size = .size2, + this.style = const DisclosureStyler.create(), required this.trigger, required this.content, this.triggerBuilder, @@ -68,6 +70,7 @@ class FortalDisclosure extends StatelessWidget { const FortalDisclosure.soft({ super.key, this.size = .size2, + this.style = const DisclosureStyler.create(), required this.trigger, required this.content, this.triggerBuilder, @@ -97,6 +100,8 @@ class FortalDisclosure extends StatelessWidget { final FortalDisclosureSize size; + final DisclosureStyler style; + final Widget trigger; final Widget content; @@ -139,7 +144,11 @@ class FortalDisclosure extends StatelessWidget { Widget build(BuildContext context) { return RemixDisclosure( key: this.key, - style: fortalDisclosureStyle(variant: this.variant, size: this.size), + style: fortalDisclosureStyle( + variant: this.variant, + size: this.size, + style: this.style, + ), trigger: this.trigger, content: this.content, triggerBuilder: this.triggerBuilder, diff --git a/packages/remix_fortal/lib/src/components/divider.dart b/packages/remix_fortal/lib/src/components/divider.dart index d647c490b..7223874c6 100644 --- a/packages/remix_fortal/lib/src/components/divider.dart +++ b/packages/remix_fortal/lib/src/components/divider.dart @@ -14,10 +14,12 @@ enum FortalDividerSize { size1, size2, size3, size4 } DividerStyler fortalDividerStyle({ FortalDividerSize size = .size1, Axis orientation = Axis.horizontal, + DividerStyler style = const DividerStyler.create(), }) { return DividerStyler() .color(FortalTokens.gray6()) - .merge(_fortalDividerSizeStyler(size, orientation)); + .merge(_fortalDividerSizeStyler(size, orientation)) + .merge(style); } DividerStyler _fortalDividerSizeStyler( diff --git a/packages/remix_fortal/lib/src/components/divider.g.dart b/packages/remix_fortal/lib/src/components/divider.g.dart index 02a958dca..6b22ed351 100644 --- a/packages/remix_fortal/lib/src/components/divider.g.dart +++ b/packages/remix_fortal/lib/src/components/divider.g.dart @@ -12,17 +12,24 @@ class FortalDivider extends StatelessWidget { super.key, this.size = .size1, this.orientation = Axis.horizontal, + this.style = const DividerStyler.create(), }); final FortalDividerSize size; final Axis orientation; + final DividerStyler style; + @override Widget build(BuildContext context) { return RemixDivider( key: this.key, - style: fortalDividerStyle(size: this.size, orientation: this.orientation), + style: fortalDividerStyle( + size: this.size, + orientation: this.orientation, + style: this.style, + ), ); } } diff --git a/packages/remix_fortal/lib/src/components/heading.dart b/packages/remix_fortal/lib/src/components/heading.dart index 982fadcb3..606288e67 100644 --- a/packages/remix_fortal/lib/src/components/heading.dart +++ b/packages/remix_fortal/lib/src/components/heading.dart @@ -22,6 +22,7 @@ TextStyler fortalHeadingStyle({ bool truncate = false, bool accent = false, bool highContrast = false, + TextStyler style = const TextStyler.create(), }) { final lineHeight = switch (size) { .size1 => 16.0 / 12.0, @@ -35,22 +36,22 @@ TextStyler fortalHeadingStyle({ .size9 => 1.0, }; - var style = TextStyler( + var recipe = TextStyler( style: fortalTextSizeToken(size).mix(), ).height(lineHeight).fontWeight(fortalTextWeightToken(weight)()); // Neutral headings pin `gray12` from the tokens rather than inheriting the // ambient foreground, matching fortalTextStyle's token-default contract. - style = accent - ? fortalAccentForeground(style, highContrast: highContrast) - : style.color(FortalTokens.gray12()); - style = style.inherit(false); + recipe = accent + ? fortalAccentForeground(recipe, highContrast: highContrast) + : recipe.color(FortalTokens.gray12()); + recipe = recipe.inherit(false); return fortalApplyTextFlow( - style, + recipe, align: align, softWrap: softWrap, truncate: truncate, - ); + ).merge(style); } /// Token-backed visual heading with an independent native heading level. @@ -71,6 +72,7 @@ class FortalHeading extends StatelessWidget { this.highContrast = false, this.semanticLabel, this.excludeSemantics = false, + this.style = const TextStyler.create(), }) : assert(text != ''), assert(headingLevel >= 1 && headingLevel <= 6), assert(semanticLabel == null || semanticLabel != ''); @@ -86,6 +88,7 @@ class FortalHeading extends StatelessWidget { final bool highContrast; final String? semanticLabel; final bool excludeSemantics; + final TextStyler style; @override Widget build(BuildContext context) { @@ -97,6 +100,7 @@ class FortalHeading extends StatelessWidget { truncate: truncate, accent: accent, highContrast: highContrast, + style: style, )(text); if (excludeSemantics) return ExcludeSemantics(child: content); diff --git a/packages/remix_fortal/lib/src/components/icon_button.dart b/packages/remix_fortal/lib/src/components/icon_button.dart index 0ae769738..c3fe508af 100644 --- a/packages/remix_fortal/lib/src/components/icon_button.dart +++ b/packages/remix_fortal/lib/src/components/icon_button.dart @@ -19,6 +19,7 @@ IconButtonStyler fortalIconButtonStyle({ FortalIconButtonVariant variant = .solid, FortalIconButtonSize size = .size2, bool highContrast = false, + IconButtonStyler style = const IconButtonStyler.create(), }) { final base = _fortalIconButtonBaseStyler( variant, @@ -33,7 +34,7 @@ IconButtonStyler fortalIconButtonStyle({ base, stateStyles, pressedPaddingTop: variant == .classic ? (size == .size1 ? 1 : 2) : null, - ); + ).merge(style); } IconButtonStyler _fortalIconButtonBaseStyler( diff --git a/packages/remix_fortal/lib/src/components/icon_button.g.dart b/packages/remix_fortal/lib/src/components/icon_button.g.dart index 63ccadf24..530f5292c 100644 --- a/packages/remix_fortal/lib/src/components/icon_button.g.dart +++ b/packages/remix_fortal/lib/src/components/icon_button.g.dart @@ -13,6 +13,7 @@ class FortalIconButton extends StatelessWidget { this.variant = .solid, this.size = .size2, this.highContrast = false, + this.style = const IconButtonStyler.create(), required this.icon, required this.semanticLabel, this.iconBuilder, @@ -33,6 +34,7 @@ class FortalIconButton extends StatelessWidget { super.key, this.size = .size2, this.highContrast = false, + this.style = const IconButtonStyler.create(), required this.icon, required this.semanticLabel, this.iconBuilder, @@ -53,6 +55,7 @@ class FortalIconButton extends StatelessWidget { super.key, this.size = .size2, this.highContrast = false, + this.style = const IconButtonStyler.create(), required this.icon, required this.semanticLabel, this.iconBuilder, @@ -73,6 +76,7 @@ class FortalIconButton extends StatelessWidget { super.key, this.size = .size2, this.highContrast = false, + this.style = const IconButtonStyler.create(), required this.icon, required this.semanticLabel, this.iconBuilder, @@ -93,6 +97,7 @@ class FortalIconButton extends StatelessWidget { super.key, this.size = .size2, this.highContrast = false, + this.style = const IconButtonStyler.create(), required this.icon, required this.semanticLabel, this.iconBuilder, @@ -113,6 +118,7 @@ class FortalIconButton extends StatelessWidget { super.key, this.size = .size2, this.highContrast = false, + this.style = const IconButtonStyler.create(), required this.icon, required this.semanticLabel, this.iconBuilder, @@ -133,6 +139,7 @@ class FortalIconButton extends StatelessWidget { super.key, this.size = .size2, this.highContrast = false, + this.style = const IconButtonStyler.create(), required this.icon, required this.semanticLabel, this.iconBuilder, @@ -155,6 +162,8 @@ class FortalIconButton extends StatelessWidget { final bool highContrast; + final IconButtonStyler style; + final IconData? icon; final String semanticLabel; @@ -191,6 +200,7 @@ class FortalIconButton extends StatelessWidget { variant: this.variant, size: this.size, highContrast: this.highContrast, + style: this.style, ), icon: this.icon, semanticLabel: this.semanticLabel, diff --git a/packages/remix_fortal/lib/src/components/kbd.dart b/packages/remix_fortal/lib/src/components/kbd.dart index a93563a86..f18520455 100644 --- a/packages/remix_fortal/lib/src/components/kbd.dart +++ b/packages/remix_fortal/lib/src/components/kbd.dart @@ -19,6 +19,7 @@ BadgeStyler fortalKbdStyle( BuildContext context, { FortalTextSize? size, FortalKbdVariant variant = .classic, + BadgeStyler style = const BadgeStyler.create(), }) { final base = fortalResolveTextToken(context, size ?? FortalTextSize.size3); final fontSize = base.fontSize! * (size == null ? 0.75 : 0.8); @@ -42,7 +43,7 @@ BadgeStyler fortalKbdStyle( .color(FortalTokens.gray12()) .inherit(false); - var style = BadgeStyler() + var recipe = BadgeStyler() .label(textStyle) .minWidth(1.75 * fontSize) .padding( @@ -63,14 +64,14 @@ BadgeStyler fortalKbdStyle( }); if (variant == .classic) { - style = style.containerEffects( + recipe = recipe.containerEffects( RemixBoxEffectsMix.behindContent( RemixBoxEffectLayerMix(shadows: _fortalKbdShadows(context, fontSize)), ), ); } - return style; + return recipe.merge(style); } /// The pinned six-layer classic key-cap stack, in upstream paint order. @@ -144,6 +145,7 @@ class FortalKbd extends StatelessWidget { this.variant = FortalKbdVariant.classic, this.semanticLabel, this.excludeSemantics = false, + this.style = const BadgeStyler.create(), }) : assert(text != ''), assert(semanticLabel == null || semanticLabel != ''); @@ -153,6 +155,7 @@ class FortalKbd extends StatelessWidget { this.size, this.semanticLabel, this.excludeSemantics = false, + this.style = const BadgeStyler.create(), }) : variant = FortalKbdVariant.classic, assert(text != ''), assert(semanticLabel == null || semanticLabel != ''); @@ -163,6 +166,7 @@ class FortalKbd extends StatelessWidget { this.size, this.semanticLabel, this.excludeSemantics = false, + this.style = const BadgeStyler.create(), }) : variant = FortalKbdVariant.soft, assert(text != ''), assert(semanticLabel == null || semanticLabel != ''); @@ -172,12 +176,16 @@ class FortalKbd extends StatelessWidget { final FortalKbdVariant variant; final String? semanticLabel; final bool excludeSemantics; + final BadgeStyler style; @override Widget build(BuildContext context) { - final content = fortalKbdStyle(context, size: size, variant: variant)( - label: text, - ); + final content = fortalKbdStyle( + context, + size: size, + variant: variant, + style: style, + )(label: text); if (excludeSemantics) return ExcludeSemantics(child: content); diff --git a/packages/remix_fortal/lib/src/components/link.dart b/packages/remix_fortal/lib/src/components/link.dart index 04f707492..ebd29e432 100644 --- a/packages/remix_fortal/lib/src/components/link.dart +++ b/packages/remix_fortal/lib/src/components/link.dart @@ -27,6 +27,7 @@ LinkStyler fortalLinkStyle( bool truncate = false, bool highContrast = false, required bool actionable, + LinkStyler style = const LinkStyler.create(), }) { LinkStyler styleFor({bool hovered = false, bool focused = false}) => _fortalLinkStateStyle( @@ -42,7 +43,7 @@ LinkStyler fortalLinkStyle( focused: focused, ); - if (!actionable) return styleFor(); + if (!actionable) return styleFor().merge(style); // The focus-visible snapshot already drops the underline via `focused`; the // explicit `none` also clears any decoration inherited through the merge. @@ -52,7 +53,8 @@ LinkStyler fortalLinkStyle( return styleFor() .onHovered(styleFor(hovered: true)) - .onFocusVisible(focusVisible); + .onFocusVisible(focusVisible) + .merge(style); } /// Resolves one point in the link's state space. @@ -187,6 +189,7 @@ class FortalLink extends StatelessWidget { this.semanticLabel, this.semanticHint, this.excludeSemantics = false, + this.style = const LinkStyler.create(), }) : assert(text != ''), assert(semanticLabel == null || semanticLabel != ''), assert(semanticHint == null || semanticHint != ''), @@ -209,6 +212,7 @@ class FortalLink extends StatelessWidget { final String? semanticLabel; final String? semanticHint; final bool excludeSemantics; + final LinkStyler style; @override Widget build(BuildContext context) { @@ -233,6 +237,7 @@ class FortalLink extends StatelessWidget { truncate: truncate, highContrast: highContrast, actionable: enabled && onPressed != null, + style: style, ), ); } diff --git a/packages/remix_fortal/lib/src/components/menu.dart b/packages/remix_fortal/lib/src/components/menu.dart index ee4f50f09..a8d38754e 100644 --- a/packages/remix_fortal/lib/src/components/menu.dart +++ b/packages/remix_fortal/lib/src/components/menu.dart @@ -18,6 +18,7 @@ MenuStyler fortalMenuStyle({ FortalMenuVariant variant = .solid, FortalMenuSize size = .size2, bool highContrast = false, + MenuStyler style = const MenuStyler.create(), }) { final metrics = _fortalMenuMetrics(size); final base = MenuStyler() @@ -44,7 +45,7 @@ MenuStyler fortalMenuStyle({ ) .divider(_fortalMenuDividerStyler(metrics)); - return base; + return base.merge(style); } /// Fortal item recipe for per-item style overrides. diff --git a/packages/remix_fortal/lib/src/components/menu.g.dart b/packages/remix_fortal/lib/src/components/menu.g.dart index 3dd054911..b4aefe785 100644 --- a/packages/remix_fortal/lib/src/components/menu.g.dart +++ b/packages/remix_fortal/lib/src/components/menu.g.dart @@ -13,6 +13,7 @@ class FortalMenu extends StatelessWidget { this.variant = .solid, this.size = .size2, this.highContrast = false, + this.style = const MenuStyler.create(), required this.trigger, required this.items, this.controller, @@ -35,6 +36,7 @@ class FortalMenu extends StatelessWidget { super.key, this.size = .size2, this.highContrast = false, + this.style = const MenuStyler.create(), required this.trigger, required this.items, this.controller, @@ -57,6 +59,7 @@ class FortalMenu extends StatelessWidget { super.key, this.size = .size2, this.highContrast = false, + this.style = const MenuStyler.create(), required this.trigger, required this.items, this.controller, @@ -81,6 +84,8 @@ class FortalMenu extends StatelessWidget { final bool highContrast; + final MenuStyler style; + final RemixMenuTrigger trigger; final List> items; @@ -121,6 +126,7 @@ class FortalMenu extends StatelessWidget { variant: this.variant, size: this.size, highContrast: this.highContrast, + style: this.style, ), trigger: this.trigger, items: this.items, diff --git a/packages/remix_fortal/lib/src/components/popover.dart b/packages/remix_fortal/lib/src/components/popover.dart index d93968659..779751323 100644 --- a/packages/remix_fortal/lib/src/components/popover.dart +++ b/packages/remix_fortal/lib/src/components/popover.dart @@ -16,6 +16,7 @@ enum FortalPopoverSize { size1, size2, size3, size4 } @MixWidget(target: RemixPopover.new) PopoverStyler fortalPopoverStyle({ FortalPopoverSize size = FortalPopoverSize.size2, + PopoverStyler style = const PopoverStyler.create(), }) { final radius = switch (size) { FortalPopoverSize.size1 || @@ -40,5 +41,6 @@ PopoverStyler fortalPopoverStyle({ ) .containerEffects( RemixBoxEffectsMix.backdropBlur(FortalTokens.panelBlur()), - ); + ) + .merge(style); } diff --git a/packages/remix_fortal/lib/src/components/popover.g.dart b/packages/remix_fortal/lib/src/components/popover.g.dart index 8891e2611..3e0892c32 100644 --- a/packages/remix_fortal/lib/src/components/popover.g.dart +++ b/packages/remix_fortal/lib/src/components/popover.g.dart @@ -14,6 +14,7 @@ class FortalPopover extends StatelessWidget { const FortalPopover({ super.key, this.size = FortalPopoverSize.size2, + this.style = const PopoverStyler.create(), required this.popoverChild, required this.child, this.positioning = const OverlayPositionConfig(), @@ -32,6 +33,8 @@ class FortalPopover extends StatelessWidget { final FortalPopoverSize size; + final PopoverStyler style; + final Widget popoverChild; final Widget child; @@ -64,7 +67,7 @@ class FortalPopover extends StatelessWidget { Widget build(BuildContext context) { return RemixPopover( key: this.key, - style: fortalPopoverStyle(size: this.size), + style: fortalPopoverStyle(size: this.size, style: this.style), popoverChild: this.popoverChild, child: this.child, positioning: this.positioning, diff --git a/packages/remix_fortal/lib/src/components/progress.dart b/packages/remix_fortal/lib/src/components/progress.dart index 88efd7007..0df978afb 100644 --- a/packages/remix_fortal/lib/src/components/progress.dart +++ b/packages/remix_fortal/lib/src/components/progress.dart @@ -18,12 +18,13 @@ ProgressStyler fortalProgressStyle({ FortalProgressVariant variant = .surface, FortalProgressSize size = .size2, bool highContrast = false, + ProgressStyler style = const ProgressStyler.create(), }) { - return switch (variant) { + return (switch (variant) { .classic => _fortalProgressClassicStyler(size, highContrast: highContrast), .surface => _fortalProgressSurfaceStyler(size, highContrast: highContrast), .soft => _fortalProgressSoftStyler(size, highContrast: highContrast), - }; + }).merge(style); } ProgressStyler _fortalProgressBaseStyler(FortalProgressSize size) { diff --git a/packages/remix_fortal/lib/src/components/progress.g.dart b/packages/remix_fortal/lib/src/components/progress.g.dart index 34fdd56aa..db166505f 100644 --- a/packages/remix_fortal/lib/src/components/progress.g.dart +++ b/packages/remix_fortal/lib/src/components/progress.g.dart @@ -13,6 +13,7 @@ class FortalProgress extends StatelessWidget { this.variant = .surface, this.size = .size2, this.highContrast = false, + this.style = const ProgressStyler.create(), required this.value, this.semanticsLabel, this.semanticsValue, @@ -22,6 +23,7 @@ class FortalProgress extends StatelessWidget { super.key, this.size = .size2, this.highContrast = false, + this.style = const ProgressStyler.create(), required this.value, this.semanticsLabel, this.semanticsValue, @@ -31,6 +33,7 @@ class FortalProgress extends StatelessWidget { super.key, this.size = .size2, this.highContrast = false, + this.style = const ProgressStyler.create(), required this.value, this.semanticsLabel, this.semanticsValue, @@ -40,6 +43,7 @@ class FortalProgress extends StatelessWidget { super.key, this.size = .size2, this.highContrast = false, + this.style = const ProgressStyler.create(), required this.value, this.semanticsLabel, this.semanticsValue, @@ -51,6 +55,8 @@ class FortalProgress extends StatelessWidget { final bool highContrast; + final ProgressStyler style; + final double value; final String? semanticsLabel; @@ -65,6 +71,7 @@ class FortalProgress extends StatelessWidget { variant: this.variant, size: this.size, highContrast: this.highContrast, + style: this.style, ), value: this.value, semanticsLabel: this.semanticsLabel, diff --git a/packages/remix_fortal/lib/src/components/radio.dart b/packages/remix_fortal/lib/src/components/radio.dart index bdeb6d9a3..a0068da96 100644 --- a/packages/remix_fortal/lib/src/components/radio.dart +++ b/packages/remix_fortal/lib/src/components/radio.dart @@ -36,12 +36,13 @@ RadioStyler fortalRadioStyle({ FortalRadioVariant variant = .surface, FortalRadioSize size = .size2, bool highContrast = false, + RadioStyler style = const RadioStyler.create(), }) { - return switch (variant) { + return (switch (variant) { .classic => _fortalRadioClassicStyler(size, highContrast: highContrast), .surface => _fortalRadioSurfaceStyler(size, highContrast: highContrast), .soft => _fortalRadioSoftStyler(size, highContrast: highContrast), - }; + }).merge(style); } RadioStyler _fortalRadioBaseStyler(FortalRadioSize size) { diff --git a/packages/remix_fortal/lib/src/components/radio.g.dart b/packages/remix_fortal/lib/src/components/radio.g.dart index de5abd930..12e86835b 100644 --- a/packages/remix_fortal/lib/src/components/radio.g.dart +++ b/packages/remix_fortal/lib/src/components/radio.g.dart @@ -13,6 +13,7 @@ class FortalRadio extends StatelessWidget { this.variant = .surface, this.size = .size2, this.highContrast = false, + this.style = const RadioStyler.create(), required this.value, required this.semanticLabel, this.enabled = true, @@ -28,6 +29,7 @@ class FortalRadio extends StatelessWidget { super.key, this.size = .size2, this.highContrast = false, + this.style = const RadioStyler.create(), required this.value, required this.semanticLabel, this.enabled = true, @@ -43,6 +45,7 @@ class FortalRadio extends StatelessWidget { super.key, this.size = .size2, this.highContrast = false, + this.style = const RadioStyler.create(), required this.value, required this.semanticLabel, this.enabled = true, @@ -58,6 +61,7 @@ class FortalRadio extends StatelessWidget { super.key, this.size = .size2, this.highContrast = false, + this.style = const RadioStyler.create(), required this.value, required this.semanticLabel, this.enabled = true, @@ -74,6 +78,8 @@ class FortalRadio extends StatelessWidget { final bool highContrast; + final RadioStyler style; + final T value; final String semanticLabel; @@ -98,6 +104,7 @@ class FortalRadio extends StatelessWidget { variant: this.variant, size: this.size, highContrast: this.highContrast, + style: this.style, ), value: this.value, semanticLabel: this.semanticLabel, diff --git a/packages/remix_fortal/lib/src/components/segmented_control.dart b/packages/remix_fortal/lib/src/components/segmented_control.dart index 009535227..be78c4bdb 100644 --- a/packages/remix_fortal/lib/src/components/segmented_control.dart +++ b/packages/remix_fortal/lib/src/components/segmented_control.dart @@ -57,6 +57,7 @@ enum FortalSegmentedControlVariant { surface, classic } SegmentedControlStyler fortalSegmentedControlStyle({ FortalSegmentedControlVariant variant = .surface, FortalSegmentedControlSize size = .size2, + SegmentedControlStyler style = const SegmentedControlStyler.create(), }) { final metrics = _fortalSegmentedControlMetrics(size); final item = _fortalSegmentedControlItemStyle(variant, metrics); @@ -72,7 +73,8 @@ SegmentedControlStyler fortalSegmentedControlStyle({ SegmentedControlStyler().color( _segmentedControlDisabledTrackBackground(), ), - ); + ) + .merge(style); } SegmentedControlItemStyler _fortalSegmentedControlItemStyle( diff --git a/packages/remix_fortal/lib/src/components/segmented_control.g.dart b/packages/remix_fortal/lib/src/components/segmented_control.g.dart index c47497560..e4506c936 100644 --- a/packages/remix_fortal/lib/src/components/segmented_control.g.dart +++ b/packages/remix_fortal/lib/src/components/segmented_control.g.dart @@ -17,6 +17,7 @@ class FortalSegmentedControl extends StatelessWidget { super.key, this.variant = .surface, this.size = .size2, + this.style = const SegmentedControlStyler.create(), required this.items, required this.selectedValue, this.onChanged, @@ -30,6 +31,7 @@ class FortalSegmentedControl extends StatelessWidget { const FortalSegmentedControl.surface({ super.key, this.size = .size2, + this.style = const SegmentedControlStyler.create(), required this.items, required this.selectedValue, this.onChanged, @@ -43,6 +45,7 @@ class FortalSegmentedControl extends StatelessWidget { const FortalSegmentedControl.classic({ super.key, this.size = .size2, + this.style = const SegmentedControlStyler.create(), required this.items, required this.selectedValue, this.onChanged, @@ -57,6 +60,8 @@ class FortalSegmentedControl extends StatelessWidget { final FortalSegmentedControlSize size; + final SegmentedControlStyler style; + final List> items; final T? selectedValue; @@ -80,6 +85,7 @@ class FortalSegmentedControl extends StatelessWidget { style: fortalSegmentedControlStyle( variant: this.variant, size: this.size, + style: this.style, ), items: this.items, selectedValue: this.selectedValue, diff --git a/packages/remix_fortal/lib/src/components/select.dart b/packages/remix_fortal/lib/src/components/select.dart index 847f0e58e..17fa5ec2d 100644 --- a/packages/remix_fortal/lib/src/components/select.dart +++ b/packages/remix_fortal/lib/src/components/select.dart @@ -18,11 +18,13 @@ SelectStyler fortalSelectStyle({ FortalSelectVariant variant = .surface, FortalSelectSize size = .size2, bool highContrast = false, + SelectStyler style = const SelectStyler.create(), }) { return SelectStyler() .trigger(_fortalSelectTriggerStyler(variant, size)) .content(_fortalSelectContentStyler(size)) - .item(_fortalSelectItemStyler(variant, size, highContrast: highContrast)); + .item(_fortalSelectItemStyler(variant, size, highContrast: highContrast)) + .merge(style); } /// Creates the established combined-variant Select item recipe. diff --git a/packages/remix_fortal/lib/src/components/select.g.dart b/packages/remix_fortal/lib/src/components/select.g.dart index 8fe07c70e..a63934260 100644 --- a/packages/remix_fortal/lib/src/components/select.g.dart +++ b/packages/remix_fortal/lib/src/components/select.g.dart @@ -13,6 +13,7 @@ class FortalSelect extends StatelessWidget { this.variant = .surface, this.size = .size2, this.highContrast = false, + this.style = const SelectStyler.create(), required this.trigger, required this.items, this.selectedValue, @@ -34,6 +35,7 @@ class FortalSelect extends StatelessWidget { super.key, this.size = .size2, this.highContrast = false, + this.style = const SelectStyler.create(), required this.trigger, required this.items, this.selectedValue, @@ -55,6 +57,7 @@ class FortalSelect extends StatelessWidget { super.key, this.size = .size2, this.highContrast = false, + this.style = const SelectStyler.create(), required this.trigger, required this.items, this.selectedValue, @@ -76,6 +79,7 @@ class FortalSelect extends StatelessWidget { super.key, this.size = .size2, this.highContrast = false, + this.style = const SelectStyler.create(), required this.trigger, required this.items, this.selectedValue, @@ -99,6 +103,8 @@ class FortalSelect extends StatelessWidget { final bool highContrast; + final SelectStyler style; + final RemixSelectTrigger trigger; final List> items; @@ -131,6 +137,7 @@ class FortalSelect extends StatelessWidget { variant: this.variant, size: this.size, highContrast: this.highContrast, + style: this.style, ), trigger: this.trigger, items: this.items, diff --git a/packages/remix_fortal/lib/src/components/sidebar.dart b/packages/remix_fortal/lib/src/components/sidebar.dart index 728616d77..ce7d6fa79 100644 --- a/packages/remix_fortal/lib/src/components/sidebar.dart +++ b/packages/remix_fortal/lib/src/components/sidebar.dart @@ -32,6 +32,7 @@ const _minimumDestinationTargetHeight = 48.0; SidebarStyler fortalSidebarStyle({ bool highContrast = false, EdgeInsetsGeometry? panelPadding, + SidebarStyler style = const SidebarStyler.create(), }) { return SidebarStyler( container: @@ -76,5 +77,5 @@ SidebarStyler fortalSidebarStyle({ ) .minHeight(_minimumDestinationTargetHeight) .container(.mainAxisSize(.max).mainAxisAlignment(.start)), - ); + ).merge(style); } diff --git a/packages/remix_fortal/lib/src/components/sidebar.g.dart b/packages/remix_fortal/lib/src/components/sidebar.g.dart index 5dfc37005..43e2cabdc 100644 --- a/packages/remix_fortal/lib/src/components/sidebar.g.dart +++ b/packages/remix_fortal/lib/src/components/sidebar.g.dart @@ -25,6 +25,7 @@ class FortalSidebar extends StatelessWidget { super.key, this.highContrast = false, this.panelPadding, + this.style = const SidebarStyler.create(), this.header, required this.sections, required this.selectedValue, @@ -39,6 +40,8 @@ class FortalSidebar extends StatelessWidget { final EdgeInsetsGeometry? panelPadding; + final SidebarStyler style; + final Widget? header; final List> sections; @@ -62,6 +65,7 @@ class FortalSidebar extends StatelessWidget { style: fortalSidebarStyle( highContrast: this.highContrast, panelPadding: this.panelPadding, + style: this.style, ), header: this.header, sections: this.sections, diff --git a/packages/remix_fortal/lib/src/components/skeleton.dart b/packages/remix_fortal/lib/src/components/skeleton.dart index e49748a8c..1a24561f7 100644 --- a/packages/remix_fortal/lib/src/components/skeleton.dart +++ b/packages/remix_fortal/lib/src/components/skeleton.dart @@ -11,7 +11,9 @@ part 'skeleton.g.dart'; /// The pulse starts and rests on `grayA3` before moving toward `grayA4`; /// Radix's CSS `alternate-reverse` phase starts from `grayA4`. @MixWidget(target: RemixSkeleton.new) -SkeletonStyler fortalSkeletonStyle() { +SkeletonStyler fortalSkeletonStyle({ + SkeletonStyler style = const SkeletonStyler.create(), +}) { return SkeletonStyler() .container( BoxStyler() @@ -20,5 +22,6 @@ SkeletonStyler fortalSkeletonStyle() { .borderRadius(.all(FortalTokens.radius1())), ) .pulseColor(FortalTokens.grayA4()) - .duration(FortalTokens.skeletonPulseDuration()); + .duration(FortalTokens.skeletonPulseDuration()) + .merge(style); } diff --git a/packages/remix_fortal/lib/src/components/skeleton.g.dart b/packages/remix_fortal/lib/src/components/skeleton.g.dart index 68f999131..c081b0105 100644 --- a/packages/remix_fortal/lib/src/components/skeleton.g.dart +++ b/packages/remix_fortal/lib/src/components/skeleton.g.dart @@ -11,7 +11,14 @@ part of 'skeleton.dart'; /// The pulse starts and rests on `grayA3` before moving toward `grayA4`; /// Radix's CSS `alternate-reverse` phase starts from `grayA4`. class FortalSkeleton extends StatelessWidget { - const FortalSkeleton({super.key, this.child, this.loading = true}); + const FortalSkeleton({ + super.key, + this.style = const SkeletonStyler.create(), + this.child, + this.loading = true, + }); + + final SkeletonStyler style; final Widget? child; @@ -21,7 +28,7 @@ class FortalSkeleton extends StatelessWidget { Widget build(BuildContext context) { return RemixSkeleton( key: this.key, - style: fortalSkeletonStyle(), + style: fortalSkeletonStyle(style: this.style), child: this.child, loading: this.loading, ); diff --git a/packages/remix_fortal/lib/src/components/slider.dart b/packages/remix_fortal/lib/src/components/slider.dart index 9ea4af785..1df8e6238 100644 --- a/packages/remix_fortal/lib/src/components/slider.dart +++ b/packages/remix_fortal/lib/src/components/slider.dart @@ -1,6 +1,5 @@ import 'package:flutter/widgets.dart'; import 'package:mix_annotations/mix_annotations.dart'; -import 'package:naked_ui/naked_ui.dart'; import 'package:remix/remix.dart'; import '../theme/theme.dart'; @@ -19,6 +18,7 @@ SliderStyler fortalSliderStyle({ FortalSliderVariant variant = .surface, FortalSliderSize size = .size2, bool highContrast = false, + SliderStyler style = const SliderStyler.create(), }) { final metrics = _fortalSliderMetrics(size); final radius = BorderRadiusMix.all(metrics.trackRadius); @@ -75,7 +75,8 @@ SliderStyler fortalSliderStyle({ (context) => FortalTheme.of(context).isDark, ), SliderStyler().onDisabled(.blendMode(BlendMode.screen)), - ); + ) + .merge(style); } SliderStyler _fortalSliderSurface( diff --git a/packages/remix_fortal/lib/src/components/slider.g.dart b/packages/remix_fortal/lib/src/components/slider.g.dart index 81d5f5d9d..758109958 100644 --- a/packages/remix_fortal/lib/src/components/slider.g.dart +++ b/packages/remix_fortal/lib/src/components/slider.g.dart @@ -13,6 +13,7 @@ class FortalSlider extends StatelessWidget { this.variant = .surface, this.size = .size2, this.highContrast = false, + this.style = const SliderStyler.create(), required this.value, this.onChanged, this.onChangeStart, @@ -33,6 +34,7 @@ class FortalSlider extends StatelessWidget { super.key, this.size = .size2, this.highContrast = false, + this.style = const SliderStyler.create(), required this.value, this.onChanged, this.onChangeStart, @@ -53,6 +55,7 @@ class FortalSlider extends StatelessWidget { super.key, this.size = .size2, this.highContrast = false, + this.style = const SliderStyler.create(), required this.value, this.onChanged, this.onChangeStart, @@ -73,6 +76,7 @@ class FortalSlider extends StatelessWidget { super.key, this.size = .size2, this.highContrast = false, + this.style = const SliderStyler.create(), required this.value, this.onChanged, this.onChangeStart, @@ -95,6 +99,8 @@ class FortalSlider extends StatelessWidget { final bool highContrast; + final SliderStyler style; + final double value; final ValueChanged? onChanged; @@ -131,6 +137,7 @@ class FortalSlider extends StatelessWidget { variant: this.variant, size: this.size, highContrast: this.highContrast, + style: this.style, ), value: this.value, onChanged: this.onChanged, diff --git a/packages/remix_fortal/lib/src/components/spinner.dart b/packages/remix_fortal/lib/src/components/spinner.dart index 60b3f9435..96b7f4716 100644 --- a/packages/remix_fortal/lib/src/components/spinner.dart +++ b/packages/remix_fortal/lib/src/components/spinner.dart @@ -11,12 +11,15 @@ enum FortalSpinnerSize { size1, size2, size3 } /// Fortal-themed preset for [RemixSpinner] using the inherited foreground color. @MixWidget(target: RemixSpinner.new) -SpinnerStyler fortalSpinnerStyle({FortalSpinnerSize size = .size2}) { +SpinnerStyler fortalSpinnerStyle({ + FortalSpinnerSize size = .size2, + SpinnerStyler style = const SpinnerStyler.create(), +}) { return SpinnerStyler( opacity: 0.65, leafRadius: FortalTokens.radius1(), duration: const Duration(milliseconds: 800), - ).merge(_fortalSpinnerSizeStyler(size)); + ).merge(_fortalSpinnerSizeStyler(size)).merge(style); } SpinnerStyler _fortalSpinnerSizeStyler(FortalSpinnerSize size) { diff --git a/packages/remix_fortal/lib/src/components/spinner.g.dart b/packages/remix_fortal/lib/src/components/spinner.g.dart index 7308489f2..c28494ea1 100644 --- a/packages/remix_fortal/lib/src/components/spinner.g.dart +++ b/packages/remix_fortal/lib/src/components/spinner.g.dart @@ -11,12 +11,15 @@ class FortalSpinner extends StatelessWidget { const FortalSpinner({ super.key, this.size = .size2, + this.style = const SpinnerStyler.create(), this.semanticsLabel, this.semanticsValue, }); final FortalSpinnerSize size; + final SpinnerStyler style; + final String? semanticsLabel; final String? semanticsValue; @@ -25,7 +28,7 @@ class FortalSpinner extends StatelessWidget { Widget build(BuildContext context) { return RemixSpinner( key: this.key, - style: fortalSpinnerStyle(size: this.size), + style: fortalSpinnerStyle(size: this.size, style: this.style), semanticsLabel: this.semanticsLabel, semanticsValue: this.semanticsValue, ); diff --git a/packages/remix_fortal/lib/src/components/switch.dart b/packages/remix_fortal/lib/src/components/switch.dart index 929facf74..115f21138 100644 --- a/packages/remix_fortal/lib/src/components/switch.dart +++ b/packages/remix_fortal/lib/src/components/switch.dart @@ -36,12 +36,13 @@ SwitchStyler fortalSwitchStyle({ FortalSwitchVariant variant = .surface, FortalSwitchSize size = .size2, bool highContrast = false, + SwitchStyler style = const SwitchStyler.create(), }) { - return switch (variant) { + return (switch (variant) { .classic => _fortalSwitchClassicStyler(size, highContrast: highContrast), .surface => _fortalSwitchSurfaceStyler(size, highContrast: highContrast), .soft => _fortalSwitchSoftStyler(size, highContrast: highContrast), - }; + }).merge(style); } SwitchStyler _fortalSwitchBaseStyler(FortalSwitchSize size) { diff --git a/packages/remix_fortal/lib/src/components/switch.g.dart b/packages/remix_fortal/lib/src/components/switch.g.dart index 5ddfc5e97..1c7aa4c0c 100644 --- a/packages/remix_fortal/lib/src/components/switch.g.dart +++ b/packages/remix_fortal/lib/src/components/switch.g.dart @@ -13,6 +13,7 @@ class FortalSwitch extends StatelessWidget { this.variant = .surface, this.size = .size2, this.highContrast = false, + this.style = const SwitchStyler.create(), required this.selected, required this.semanticLabel, this.onChanged, @@ -29,6 +30,7 @@ class FortalSwitch extends StatelessWidget { super.key, this.size = .size2, this.highContrast = false, + this.style = const SwitchStyler.create(), required this.selected, required this.semanticLabel, this.onChanged, @@ -45,6 +47,7 @@ class FortalSwitch extends StatelessWidget { super.key, this.size = .size2, this.highContrast = false, + this.style = const SwitchStyler.create(), required this.selected, required this.semanticLabel, this.onChanged, @@ -61,6 +64,7 @@ class FortalSwitch extends StatelessWidget { super.key, this.size = .size2, this.highContrast = false, + this.style = const SwitchStyler.create(), required this.selected, required this.semanticLabel, this.onChanged, @@ -78,6 +82,8 @@ class FortalSwitch extends StatelessWidget { final bool highContrast; + final SwitchStyler style; + final bool selected; final String semanticLabel; @@ -104,6 +110,7 @@ class FortalSwitch extends StatelessWidget { variant: this.variant, size: this.size, highContrast: this.highContrast, + style: this.style, ), selected: this.selected, semanticLabel: this.semanticLabel, diff --git a/packages/remix_fortal/lib/src/components/tabs.dart b/packages/remix_fortal/lib/src/components/tabs.dart index 632053f8b..4a6f4497a 100644 --- a/packages/remix_fortal/lib/src/components/tabs.dart +++ b/packages/remix_fortal/lib/src/components/tabs.dart @@ -1,6 +1,5 @@ import 'package:flutter/widgets.dart'; import 'package:mix_annotations/mix_annotations.dart'; -import 'package:naked_ui/naked_ui.dart'; import 'package:remix/remix.dart'; import '../theme/theme.dart'; @@ -15,21 +14,30 @@ enum FortalTabsSize { size1, size2 } /// The tab-list bottom border is a single hairline at every Radix size, so this /// preset takes no `size` — unlike [fortalTabStyle], whose per-tab metrics vary. @MixWidget(target: RemixTabBar.new) -TabBarStyler fortalTabBarStyle() { - return TabBarStyler().border( - .bottom(.color(FortalTokens.grayA5()).width(FortalTokens.borderWidth1())), - ); +TabBarStyler fortalTabBarStyle({ + TabBarStyler style = const TabBarStyler.create(), +}) { + return TabBarStyler() + .border( + .bottom( + .color(FortalTokens.grayA5()).width(FortalTokens.borderWidth1()), + ), + ) + .merge(style); } /// Fortal-themed preset for [RemixTabView]. @MixWidget(target: RemixTabView.new) -TabViewStyler fortalTabViewStyle() => TabViewStyler(); +TabViewStyler fortalTabViewStyle({ + TabViewStyler style = const TabViewStyler.create(), +}) => TabViewStyler().merge(style); /// Fortal-themed preset for [RemixTab]. @MixWidget(target: RemixTab.new) TabStyler fortalTabStyle({ FortalTabsSize size = FortalTabsSize.size2, bool highContrast = false, + TabStyler style = const TabStyler.create(), }) { final metrics = switch (size) { FortalTabsSize.size1 => ( @@ -114,5 +122,6 @@ TabStyler fortalTabStyle({ ), ), ), - ); + ) + .merge(style); } diff --git a/packages/remix_fortal/lib/src/components/tabs.g.dart b/packages/remix_fortal/lib/src/components/tabs.g.dart index 4b9fc9416..b7fb0d44a 100644 --- a/packages/remix_fortal/lib/src/components/tabs.g.dart +++ b/packages/remix_fortal/lib/src/components/tabs.g.dart @@ -11,7 +11,13 @@ part of 'tabs.dart'; /// The tab-list bottom border is a single hairline at every Radix size, so this /// preset takes no `size` — unlike [fortalTabStyle], whose per-tab metrics vary. class FortalTabBar extends StatelessWidget { - const FortalTabBar({super.key, required this.child}); + const FortalTabBar({ + super.key, + this.style = const TabBarStyler.create(), + required this.child, + }); + + final TabBarStyler style; final Widget child; @@ -19,7 +25,7 @@ class FortalTabBar extends StatelessWidget { Widget build(BuildContext context) { return RemixTabBar( key: this.key, - style: fortalTabBarStyle(), + style: fortalTabBarStyle(style: this.style), child: this.child, ); } @@ -29,11 +35,14 @@ class FortalTabBar extends StatelessWidget { class FortalTabView extends StatelessWidget { const FortalTabView({ super.key, + this.style = const TabViewStyler.create(), required this.tabId, required this.child, this.maintainState = true, }); + final TabViewStyler style; + final String tabId; final Widget child; @@ -44,7 +53,7 @@ class FortalTabView extends StatelessWidget { Widget build(BuildContext context) { return RemixTabView( key: this.key, - style: fortalTabViewStyle(), + style: fortalTabViewStyle(style: this.style), tabId: this.tabId, child: this.child, maintainState: this.maintainState, @@ -58,6 +67,7 @@ class FortalTab extends StatelessWidget { super.key, this.size = FortalTabsSize.size2, this.highContrast = false, + this.style = const TabStyler.create(), required this.tabId, this.child, this.label, @@ -78,6 +88,8 @@ class FortalTab extends StatelessWidget { final bool highContrast; + final TabStyler style; + final String tabId; final Widget? child; @@ -110,7 +122,11 @@ class FortalTab extends StatelessWidget { Widget build(BuildContext context) { return RemixTab( key: this.key, - style: fortalTabStyle(size: this.size, highContrast: this.highContrast), + style: fortalTabStyle( + size: this.size, + highContrast: this.highContrast, + style: this.style, + ), tabId: this.tabId, child: this.child, label: this.label, diff --git a/packages/remix_fortal/lib/src/components/text.dart b/packages/remix_fortal/lib/src/components/text.dart index 1c1d95c01..ba498eba4 100644 --- a/packages/remix_fortal/lib/src/components/text.dart +++ b/packages/remix_fortal/lib/src/components/text.dart @@ -26,22 +26,23 @@ TextStyler fortalTextStyle({ bool truncate = false, bool accent = false, bool highContrast = false, + TextStyler style = const TextStyler.create(), }) { - var style = TextStyler().style( + var recipe = TextStyler().style( fortalTextSizeToken(size ?? FortalTextSize.size3).mix(), ); - style = style.fontWeight( + recipe = recipe.fontWeight( fortalTextWeightToken(weight ?? FortalTextWeight.regular)(), ); - style = accent - ? fortalAccentForeground(style, highContrast: highContrast) - : style.color(FortalTokens.gray12()); - style = style.inherit(false); + recipe = accent + ? fortalAccentForeground(recipe, highContrast: highContrast) + : recipe.color(FortalTokens.gray12()); + recipe = recipe.inherit(false); return fortalApplyTextFlow( - style, + recipe, align: align, softWrap: softWrap, truncate: truncate, - ); + ).merge(style); } diff --git a/packages/remix_fortal/lib/src/components/text.g.dart b/packages/remix_fortal/lib/src/components/text.g.dart index 623ff7f18..53e87f208 100644 --- a/packages/remix_fortal/lib/src/components/text.g.dart +++ b/packages/remix_fortal/lib/src/components/text.g.dart @@ -27,6 +27,7 @@ class FortalText extends StatelessWidget { this.truncate = false, this.accent = false, this.highContrast = false, + this.style = const TextStyler.create(), }); final FortalTextSize? size; @@ -43,6 +44,8 @@ class FortalText extends StatelessWidget { final bool highContrast; + final TextStyler style; + final String text; @override @@ -55,6 +58,7 @@ class FortalText extends StatelessWidget { truncate: this.truncate, accent: this.accent, highContrast: this.highContrast, + style: this.style, ).call(this.text, key: this.key); } } diff --git a/packages/remix_fortal/lib/src/components/textfield.dart b/packages/remix_fortal/lib/src/components/textfield.dart index c13f7984a..27ef91b9b 100644 --- a/packages/remix_fortal/lib/src/components/textfield.dart +++ b/packages/remix_fortal/lib/src/components/textfield.dart @@ -49,6 +49,7 @@ const _neutralTextInputPlaceholder = ContextToken( TextFieldStyler fortalTextFieldStyle({ FortalTextFieldVariant variant = .surface, FortalTextFieldSize size = .size2, + TextFieldStyler style = const TextFieldStyler.create(), }) { final metrics = _fortalTextFieldMetrics(size, bordered: variant != .soft); final base = _fortalTextInputBaseStyle( @@ -66,16 +67,15 @@ TextFieldStyler fortalTextFieldStyle({ }, ); - final style = switch (variant) { + final recipe = switch (variant) { .classic => _fortalApplyClassicTextInput(base), .surface => _fortalApplySurfaceTextInput(base), .soft => _fortalApplySoftTextInput(base, placeholderOpacity: 0.60), }; - return style.variant( - ContextVariant.widgetState(.error), - _fortalTextInputErrorStyle(), - ); + return recipe + .variant(ContextVariant.widgetState(.error), _fortalTextInputErrorStyle()) + .merge(style); } TextFieldStyler _fortalTextInputBaseStyle({ @@ -321,6 +321,7 @@ enum FortalTextAreaVariant { classic, surface, soft } TextFieldStyler fortalTextAreaStyle({ FortalTextAreaVariant variant = .surface, FortalTextAreaSize size = .size2, + TextFieldStyler style = const TextFieldStyler.create(), }) { final metrics = _fortalTextAreaMetrics(size); final base = _fortalTextInputBaseStyle( @@ -340,16 +341,15 @@ TextFieldStyler fortalTextAreaStyle({ }, ); - final style = switch (variant) { + final recipe = switch (variant) { .classic => _fortalApplyClassicTextInput(base), .surface => _fortalApplySurfaceTextInput(base), .soft => _fortalApplySoftTextInput(base, placeholderOpacity: 0.65), }; - return style.variant( - ContextVariant.widgetState(.error), - _fortalTextInputErrorStyle(), - ); + return recipe + .variant(ContextVariant.widgetState(.error), _fortalTextInputErrorStyle()) + .merge(style); } ({ diff --git a/packages/remix_fortal/lib/src/components/textfield.g.dart b/packages/remix_fortal/lib/src/components/textfield.g.dart index 19b0c2042..07ac28a2c 100644 --- a/packages/remix_fortal/lib/src/components/textfield.g.dart +++ b/packages/remix_fortal/lib/src/components/textfield.g.dart @@ -12,6 +12,7 @@ class FortalTextField extends StatelessWidget { super.key, this.variant = .surface, this.size = .size2, + this.style = const TextFieldStyler.create(), this.controller, this.focusNode, this.label, @@ -75,6 +76,7 @@ class FortalTextField extends StatelessWidget { const FortalTextField.classic({ super.key, this.size = .size2, + this.style = const TextFieldStyler.create(), this.controller, this.focusNode, this.label, @@ -138,6 +140,7 @@ class FortalTextField extends StatelessWidget { const FortalTextField.surface({ super.key, this.size = .size2, + this.style = const TextFieldStyler.create(), this.controller, this.focusNode, this.label, @@ -201,6 +204,7 @@ class FortalTextField extends StatelessWidget { const FortalTextField.soft({ super.key, this.size = .size2, + this.style = const TextFieldStyler.create(), this.controller, this.focusNode, this.label, @@ -264,6 +268,8 @@ class FortalTextField extends StatelessWidget { final FortalTextFieldSize size; + final TextFieldStyler style; + final TextEditingController? controller; final FocusNode? focusNode; @@ -382,7 +388,11 @@ class FortalTextField extends StatelessWidget { Widget build(BuildContext context) { return RemixTextField( key: this.key, - style: fortalTextFieldStyle(variant: this.variant, size: this.size), + style: fortalTextFieldStyle( + variant: this.variant, + size: this.size, + style: this.style, + ), controller: this.controller, focusNode: this.focusNode, label: this.label, @@ -453,6 +463,7 @@ class FortalTextArea extends StatelessWidget { super.key, this.variant = .surface, this.size = .size2, + this.style = const TextFieldStyler.create(), this.controller, this.focusNode, this.label, @@ -512,6 +523,7 @@ class FortalTextArea extends StatelessWidget { const FortalTextArea.classic({ super.key, this.size = .size2, + this.style = const TextFieldStyler.create(), this.controller, this.focusNode, this.label, @@ -571,6 +583,7 @@ class FortalTextArea extends StatelessWidget { const FortalTextArea.surface({ super.key, this.size = .size2, + this.style = const TextFieldStyler.create(), this.controller, this.focusNode, this.label, @@ -630,6 +643,7 @@ class FortalTextArea extends StatelessWidget { const FortalTextArea.soft({ super.key, this.size = .size2, + this.style = const TextFieldStyler.create(), this.controller, this.focusNode, this.label, @@ -690,6 +704,8 @@ class FortalTextArea extends StatelessWidget { final FortalTextAreaSize size; + final TextFieldStyler style; + final TextEditingController? controller; final FocusNode? focusNode; @@ -802,7 +818,11 @@ class FortalTextArea extends StatelessWidget { Widget build(BuildContext context) { return RemixTextArea( key: this.key, - style: fortalTextAreaStyle(variant: this.variant, size: this.size), + style: fortalTextAreaStyle( + variant: this.variant, + size: this.size, + style: this.style, + ), controller: this.controller, focusNode: this.focusNode, label: this.label, diff --git a/packages/remix_fortal/lib/src/components/toggle.dart b/packages/remix_fortal/lib/src/components/toggle.dart index 8127e52ac..ff13e97c0 100644 --- a/packages/remix_fortal/lib/src/components/toggle.dart +++ b/packages/remix_fortal/lib/src/components/toggle.dart @@ -18,11 +18,12 @@ ToggleStyler fortalToggleStyle({ FortalToggleVariant variant = .ghost, FortalToggleSize size = .size2, bool highContrast = false, + ToggleStyler style = const ToggleStyler.create(), }) { - return switch (variant) { + return (switch (variant) { .ghost => _fortalToggleGhostStyler(size, highContrast: highContrast), .outline => _fortalToggleOutlineStyler(size, highContrast: highContrast), - }; + }).merge(style); } ToggleStyler _fortalToggleBaseStyler(FortalToggleSize size) { diff --git a/packages/remix_fortal/lib/src/components/toggle.g.dart b/packages/remix_fortal/lib/src/components/toggle.g.dart index 97a8674f4..3ad9a2303 100644 --- a/packages/remix_fortal/lib/src/components/toggle.g.dart +++ b/packages/remix_fortal/lib/src/components/toggle.g.dart @@ -13,6 +13,7 @@ class FortalToggle extends StatelessWidget { this.variant = .ghost, this.size = .size2, this.highContrast = false, + this.style = const ToggleStyler.create(), required this.selected, this.onChanged, this.enabled = true, @@ -30,6 +31,7 @@ class FortalToggle extends StatelessWidget { super.key, this.size = .size2, this.highContrast = false, + this.style = const ToggleStyler.create(), required this.selected, this.onChanged, this.enabled = true, @@ -47,6 +49,7 @@ class FortalToggle extends StatelessWidget { super.key, this.size = .size2, this.highContrast = false, + this.style = const ToggleStyler.create(), required this.selected, this.onChanged, this.enabled = true, @@ -66,6 +69,8 @@ class FortalToggle extends StatelessWidget { final bool highContrast; + final ToggleStyler style; + final bool selected; final ValueChanged? onChanged; @@ -96,6 +101,7 @@ class FortalToggle extends StatelessWidget { variant: this.variant, size: this.size, highContrast: this.highContrast, + style: this.style, ), selected: this.selected, onChanged: this.onChanged, diff --git a/packages/remix_fortal/lib/src/components/toggle_group.dart b/packages/remix_fortal/lib/src/components/toggle_group.dart index e59646d02..66978ea69 100644 --- a/packages/remix_fortal/lib/src/components/toggle_group.dart +++ b/packages/remix_fortal/lib/src/components/toggle_group.dart @@ -18,6 +18,7 @@ ToggleGroupStyler fortalToggleGroupStyle({ FortalToggleGroupVariant variant = .soft, FortalToggleGroupSize size = .size2, bool highContrast = false, + ToggleGroupStyler style = const ToggleGroupStyler.create(), }) { final ( selectedColor, @@ -75,7 +76,7 @@ ToggleGroupStyler fortalToggleGroupStyle({ .labelColor(FortalTokens.gray8()) .iconColor(FortalTokens.gray8()), ), - ).merge(_fortalToggleGroupSizeStyler(size)); + ).merge(_fortalToggleGroupSizeStyler(size)).merge(style); } ToggleGroupStyler _fortalToggleGroupSizeStyler(FortalToggleGroupSize size) { diff --git a/packages/remix_fortal/lib/src/components/toggle_group.g.dart b/packages/remix_fortal/lib/src/components/toggle_group.g.dart index 0112aebbc..7e8a23350 100644 --- a/packages/remix_fortal/lib/src/components/toggle_group.g.dart +++ b/packages/remix_fortal/lib/src/components/toggle_group.g.dart @@ -13,6 +13,7 @@ class FortalToggleGroup extends StatelessWidget { this.variant = .soft, this.size = .size2, this.highContrast = false, + this.style = const ToggleGroupStyler.create(), required this.items, required this.selectedValue, this.onChanged, @@ -27,6 +28,7 @@ class FortalToggleGroup extends StatelessWidget { super.key, this.size = .size2, this.highContrast = false, + this.style = const ToggleGroupStyler.create(), required this.items, required this.selectedValue, this.onChanged, @@ -41,6 +43,7 @@ class FortalToggleGroup extends StatelessWidget { super.key, this.size = .size2, this.highContrast = false, + this.style = const ToggleGroupStyler.create(), required this.items, required this.selectedValue, this.onChanged, @@ -57,6 +60,8 @@ class FortalToggleGroup extends StatelessWidget { final bool highContrast; + final ToggleGroupStyler style; + final List> items; final T? selectedValue; @@ -81,6 +86,7 @@ class FortalToggleGroup extends StatelessWidget { variant: this.variant, size: this.size, highContrast: this.highContrast, + style: this.style, ), items: this.items, selectedValue: this.selectedValue, diff --git a/packages/remix_fortal/lib/src/components/tooltip.dart b/packages/remix_fortal/lib/src/components/tooltip.dart index 6652fa000..2ac0c1023 100644 --- a/packages/remix_fortal/lib/src/components/tooltip.dart +++ b/packages/remix_fortal/lib/src/components/tooltip.dart @@ -8,7 +8,9 @@ part 'tooltip.g.dart'; /// Fortal-themed preset for [RemixTooltip]. @MixWidget(target: RemixTooltip.new) -TooltipStyler fortalTooltipStyle() { +TooltipStyler fortalTooltipStyle({ + TooltipStyler style = const TooltipStyler.create(), +}) { return TooltipStyler( label: .style(FortalTokens.text1.mix()), waitDuration: const Duration(milliseconds: 200), @@ -17,5 +19,6 @@ TooltipStyler fortalTooltipStyle() { .padding(.vertical(FortalTokens.space1())) .padding(.horizontal(FortalTokens.space2())) .label(.color(FortalTokens.gray1())) - .color(FortalTokens.gray12()); + .color(FortalTokens.gray12()) + .merge(style); } diff --git a/packages/remix_fortal/lib/src/components/tooltip.g.dart b/packages/remix_fortal/lib/src/components/tooltip.g.dart index 08040ac5d..09cb69889 100644 --- a/packages/remix_fortal/lib/src/components/tooltip.g.dart +++ b/packages/remix_fortal/lib/src/components/tooltip.g.dart @@ -10,12 +10,15 @@ part of 'tooltip.dart'; class FortalTooltip extends StatelessWidget { const FortalTooltip({ super.key, + this.style = const TooltipStyler.create(), required this.tooltipChild, required this.child, this.tooltipSemantics, this.positioning = const OverlayPositionConfig(), }); + final TooltipStyler style; + final Widget tooltipChild; final Widget child; @@ -28,7 +31,7 @@ class FortalTooltip extends StatelessWidget { Widget build(BuildContext context) { return RemixTooltip( key: this.key, - style: fortalTooltipStyle(), + style: fortalTooltipStyle(style: this.style), tooltipChild: this.tooltipChild, child: this.child, tooltipSemantics: this.tooltipSemantics, diff --git a/packages/remix_fortal/lib/src/theme/computed.dart b/packages/remix_fortal/lib/src/theme/computed.dart index e18126370..fdcf0c954 100644 --- a/packages/remix_fortal/lib/src/theme/computed.dart +++ b/packages/remix_fortal/lib/src/theme/computed.dart @@ -12,8 +12,8 @@ import 'dart:math' as math; import 'dart:ui' show Color, Offset; import 'package:flutter/painting.dart' show BoxShadow, ColorSwatch; -import 'package:mix/mix.dart' show MixToken; -import 'package:remix/remix.dart' show RemixBoxShadow, RemixBoxShadowKind; +import 'package:remix/remix.dart' + show MixToken, RemixBoxShadow, RemixBoxShadowKind; import 'radix_colors.dart'; import 'theme_data.dart'; diff --git a/packages/remix_fortal/lib/src/theme/control_styles.dart b/packages/remix_fortal/lib/src/theme/control_styles.dart index 8311333b6..fbf55d261 100644 --- a/packages/remix_fortal/lib/src/theme/control_styles.dart +++ b/packages/remix_fortal/lib/src/theme/control_styles.dart @@ -1,12 +1,5 @@ import 'package:flutter/widgets.dart'; -import 'package:mix/mix.dart'; -import 'package:remix/remix.dart' - show - RemixBoxEffectLayerMix, - RemixBoxEffectsMix, - RemixBoxShadowKind, - RemixBoxShadowMix, - RemixBoxStylerAnchors; +import 'package:remix/remix.dart'; import 'tokens.dart'; diff --git a/packages/remix_fortal/lib/src/theme/theme_data.dart b/packages/remix_fortal/lib/src/theme/theme_data.dart index 190cf7432..c5e24bcc8 100644 --- a/packages/remix_fortal/lib/src/theme/theme_data.dart +++ b/packages/remix_fortal/lib/src/theme/theme_data.dart @@ -1,8 +1,7 @@ import 'dart:math' as math; import 'package:flutter/widgets.dart'; -import 'package:mix/mix.dart'; -import 'package:remix/remix.dart' show RemixBoxShadow, RemixBoxShadowKind; +import 'package:remix/remix.dart'; import 'computed.dart'; import 'radix_colors.dart' as radix; diff --git a/packages/remix_fortal/lib/src/theme/theme_scope.dart b/packages/remix_fortal/lib/src/theme/theme_scope.dart index 934e3ee84..9cbb8177d 100644 --- a/packages/remix_fortal/lib/src/theme/theme_scope.dart +++ b/packages/remix_fortal/lib/src/theme/theme_scope.dart @@ -1,5 +1,5 @@ import 'package:flutter/widgets.dart'; -import 'package:mix/mix.dart'; +import 'package:remix/remix.dart'; import 'theme_data.dart'; import 'tokens.dart'; diff --git a/packages/remix_fortal/test/fortal/fortal_style_override_test.dart b/packages/remix_fortal/test/fortal/fortal_style_override_test.dart new file mode 100644 index 000000000..2f24c0d57 --- /dev/null +++ b/packages/remix_fortal/test/fortal/fortal_style_override_test.dart @@ -0,0 +1,118 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:remix/remix.dart'; +import 'package:remix_fortal/remix_fortal.dart'; + +void main() { + const idleOverride = Color(0xFF123456); + const hoverOverride = Color(0xFFABCDEF); + + group('Fortal style overrides', () { + testWidgets('Button merges idle and hovered overrides last', ( + tester, + ) async { + final idle = await _resolve( + tester, + (context) => fortalButtonStyle( + style: ButtonStyler() + .color(idleOverride) + .onHovered(.color(hoverOverride)), + ).build(context).spec, + ); + final hovered = await _resolve( + tester, + (context) => fortalButtonStyle( + style: ButtonStyler() + .color(idleOverride) + .onHovered(.color(hoverOverride)), + ).build(context).spec, + states: const {WidgetState.hovered}, + ); + + expect(_buttonColor(idle), idleOverride); + expect(_buttonColor(hovered), hoverOverride); + }); + + testWidgets('Checkbox merges idle and hovered overrides last', ( + tester, + ) async { + final idle = await _resolve( + tester, + (context) => fortalCheckboxStyle( + variant: .soft, + style: CheckboxStyler() + .color(idleOverride) + .onHovered(.color(hoverOverride)), + ).build(context).spec, + ); + final hovered = await _resolve( + tester, + (context) => fortalCheckboxStyle( + variant: .soft, + style: CheckboxStyler() + .color(idleOverride) + .onHovered(.color(hoverOverride)), + ).build(context).spec, + states: const {WidgetState.hovered}, + ); + + expect(_boxColor(idle.container), idleOverride); + expect(_boxColor(hovered.container), hoverOverride); + }); + + testWidgets('TextField merges idle and hovered overrides last', ( + tester, + ) async { + final idle = await _resolve( + tester, + (context) => fortalTextFieldStyle( + style: TextFieldStyler() + .color(idleOverride) + .onHovered(.color(hoverOverride)), + ).build(context).spec, + ); + final hovered = await _resolve( + tester, + (context) => fortalTextFieldStyle( + style: TextFieldStyler() + .color(idleOverride) + .onHovered(.color(hoverOverride)), + ).build(context).spec, + states: const {WidgetState.hovered}, + ); + + expect(_boxColor(idle.container), idleOverride); + expect(_boxColor(hovered.container), hoverOverride); + }); + }); +} + +Future _resolve( + WidgetTester tester, + T Function(BuildContext context) resolve, { + Set states = const {}, +}) async { + late T value; + await tester.pumpWidget( + FortalScope( + child: MaterialApp( + home: WidgetStateProvider( + states: states, + child: Builder( + builder: (context) { + value = resolve(context); + return const SizedBox.shrink(); + }, + ), + ), + ), + ), + ); + return value; +} + +Color? _buttonColor(ButtonSpec spec) => + (spec.container.spec.box?.spec.decoration as BoxDecoration?)?.color; + +Color? _boxColor(StyleSpec style) => + (style.spec.decoration as BoxDecoration?)?.color; From fe482b8a7563f2121bdc69724075a764995da052 Mon Sep 17 00:00:00 2001 From: Leo Farias Date: Fri, 4 Sep 2026 13:34:59 -0400 Subject: [PATCH 03/12] feat(remix_cli): add preset axis --- docs/open-code.mdx | 23 ++++--- packages/remix_cli/CHANGELOG.md | 3 + packages/remix_cli/README.md | 5 +- packages/remix_cli/lib/src/cli.dart | 9 ++- packages/remix_cli/lib/src/installer.dart | 14 ++-- .../remix_cli/lib/src/project_config.dart | 48 ++++++++++--- packages/remix_cli/lib/src/registry.dart | 22 +++++- .../src/registry/{ => default}/registry.yaml | 0 .../templates/accordion/accordion.dart.tmpl | 0 .../templates/avatar/avatar.dart.tmpl | 0 .../templates/badge/badge.dart.tmpl | 0 .../templates/button/button.dart.tmpl | 0 .../templates/callout/callout.dart.tmpl | 0 .../templates/card/card.dart.tmpl | 0 .../templates/chart/chart.dart.tmpl | 0 .../templates/checkbox/checkbox.dart.tmpl | 0 .../templates/data_list/data_list.dart.tmpl | 0 .../templates/data_table/data_table.dart.tmpl | 0 .../templates/dialog/dialog.dart.tmpl | 0 .../templates/disclosure/disclosure.dart.tmpl | 0 .../templates/divider/divider.dart.tmpl | 0 .../icon_button/icon_button.dart.tmpl | 0 .../templates/icons/icons.dart.tmpl | 0 .../templates/link/link.dart.tmpl | 0 .../templates/menu/menu.dart.tmpl | 0 .../templates/popover/popover.dart.tmpl | 0 .../templates/progress/progress.dart.tmpl | 0 .../templates/radio/radio.dart.tmpl | 0 .../segmented_control.dart.tmpl | 0 .../templates/select/select.dart.tmpl | 0 .../templates/skeleton/skeleton.dart.tmpl | 0 .../templates/slider/slider.dart.tmpl | 0 .../templates/spinner/spinner.dart.tmpl | 0 .../templates/switch/switch.dart.tmpl | 0 .../templates/tabs/tabs.dart.tmpl | 0 .../templates/textfield/textfield.dart.tmpl | 0 .../templates/theme/theme_data.dart.tmpl | 0 .../templates/theme/theme_scope.dart.tmpl | 0 .../templates/theme/tokens.dart.tmpl | 0 .../templates/toggle/toggle.dart.tmpl | 0 .../toggle_group/toggle_group.dart.tmpl | 0 .../templates/tooltip/tooltip.dart.tmpl | 0 packages/remix_cli/test/add_command_test.dart | 4 +- packages/remix_cli/test/cli_test.dart | 12 +++- .../remix_cli/test/init_command_test.dart | 60 ++++++++++++---- packages/remix_cli/test/installer_test.dart | 69 +++++++++++-------- .../remix_cli/test/project_config_test.dart | 39 ++++++++++- packages/remix_cli/test/registry_test.dart | 51 ++++++++++++-- packages/remix_cli/test/test_support.dart | 4 +- test/tool/check_open_code_test.dart | 2 +- tool/check_open_code.dart | 4 +- tool/check_open_code_dogfood.dart | 2 +- tool/check_version_alignment.dart | 3 +- tool/sync_registry_remix.dart | 2 +- tool/validate_docs.dart | 2 +- 55 files changed, 289 insertions(+), 89 deletions(-) rename packages/remix_cli/lib/src/registry/{ => default}/registry.yaml (100%) rename packages/remix_cli/lib/src/registry/{ => default}/templates/accordion/accordion.dart.tmpl (100%) rename packages/remix_cli/lib/src/registry/{ => default}/templates/avatar/avatar.dart.tmpl (100%) rename packages/remix_cli/lib/src/registry/{ => default}/templates/badge/badge.dart.tmpl (100%) rename packages/remix_cli/lib/src/registry/{ => default}/templates/button/button.dart.tmpl (100%) rename packages/remix_cli/lib/src/registry/{ => default}/templates/callout/callout.dart.tmpl (100%) rename packages/remix_cli/lib/src/registry/{ => default}/templates/card/card.dart.tmpl (100%) rename packages/remix_cli/lib/src/registry/{ => default}/templates/chart/chart.dart.tmpl (100%) rename packages/remix_cli/lib/src/registry/{ => default}/templates/checkbox/checkbox.dart.tmpl (100%) rename packages/remix_cli/lib/src/registry/{ => default}/templates/data_list/data_list.dart.tmpl (100%) rename packages/remix_cli/lib/src/registry/{ => default}/templates/data_table/data_table.dart.tmpl (100%) rename packages/remix_cli/lib/src/registry/{ => default}/templates/dialog/dialog.dart.tmpl (100%) rename packages/remix_cli/lib/src/registry/{ => default}/templates/disclosure/disclosure.dart.tmpl (100%) rename packages/remix_cli/lib/src/registry/{ => default}/templates/divider/divider.dart.tmpl (100%) rename packages/remix_cli/lib/src/registry/{ => default}/templates/icon_button/icon_button.dart.tmpl (100%) rename packages/remix_cli/lib/src/registry/{ => default}/templates/icons/icons.dart.tmpl (100%) rename packages/remix_cli/lib/src/registry/{ => default}/templates/link/link.dart.tmpl (100%) rename packages/remix_cli/lib/src/registry/{ => default}/templates/menu/menu.dart.tmpl (100%) rename packages/remix_cli/lib/src/registry/{ => default}/templates/popover/popover.dart.tmpl (100%) rename packages/remix_cli/lib/src/registry/{ => default}/templates/progress/progress.dart.tmpl (100%) rename packages/remix_cli/lib/src/registry/{ => default}/templates/radio/radio.dart.tmpl (100%) rename packages/remix_cli/lib/src/registry/{ => default}/templates/segmented_control/segmented_control.dart.tmpl (100%) rename packages/remix_cli/lib/src/registry/{ => default}/templates/select/select.dart.tmpl (100%) rename packages/remix_cli/lib/src/registry/{ => default}/templates/skeleton/skeleton.dart.tmpl (100%) rename packages/remix_cli/lib/src/registry/{ => default}/templates/slider/slider.dart.tmpl (100%) rename packages/remix_cli/lib/src/registry/{ => default}/templates/spinner/spinner.dart.tmpl (100%) rename packages/remix_cli/lib/src/registry/{ => default}/templates/switch/switch.dart.tmpl (100%) rename packages/remix_cli/lib/src/registry/{ => default}/templates/tabs/tabs.dart.tmpl (100%) rename packages/remix_cli/lib/src/registry/{ => default}/templates/textfield/textfield.dart.tmpl (100%) rename packages/remix_cli/lib/src/registry/{ => default}/templates/theme/theme_data.dart.tmpl (100%) rename packages/remix_cli/lib/src/registry/{ => default}/templates/theme/theme_scope.dart.tmpl (100%) rename packages/remix_cli/lib/src/registry/{ => default}/templates/theme/tokens.dart.tmpl (100%) rename packages/remix_cli/lib/src/registry/{ => default}/templates/toggle/toggle.dart.tmpl (100%) rename packages/remix_cli/lib/src/registry/{ => default}/templates/toggle_group/toggle_group.dart.tmpl (100%) rename packages/remix_cli/lib/src/registry/{ => default}/templates/tooltip/tooltip.dart.tmpl (100%) diff --git a/docs/open-code.mdx b/docs/open-code.mdx index 9f0f906c6..719d1b4dc 100644 --- a/docs/open-code.mdx +++ b/docs/open-code.mdx @@ -88,13 +88,14 @@ remix init This writes two files and nothing else: -- `remix.yaml` — your prefix and UI path +- `remix.yaml` — your prefix, preset, and UI path - `lib/ui/ui.dart` — a managed barrel with an export marker block ```yaml # remix.yaml -schema: 1 +schema: 2 prefix: Ui +preset: default paths: ui: lib/ui ``` @@ -103,7 +104,7 @@ Choose the naming and location up front, because the prefix is baked into every identifier the CLI renders: ```bash -remix init --prefix Acme --ui-path lib/design_system +remix init --prefix Acme --preset default --ui-path lib/design_system ``` `--prefix Acme` produces `AcmeButton`, `AcmeButtonVariant`, `AcmeThemeData`, @@ -188,20 +189,22 @@ class SignUpButton extends StatelessWidget { ## Configuration -`remix.yaml` is the entire configuration surface — two settings, no plugins, no -presets, no per-component overrides: +`remix.yaml` is the entire configuration surface — three settings, no plugins +and no per-component overrides: ```yaml -schema: 1 +schema: 2 prefix: Ui # type prefix for every generated identifier +preset: default # bundled recipe tree selected at init paths: ui: lib/ui # where installed source lives ``` | Key | Default | Rules | |---|---|---| -| `schema` | `1` | Must be exactly `1`. Anything else is rejected. | +| `schema` | `2` | New files use schema 2. Schema 1 remains readable as `preset: default`; anything else is rejected. | | `prefix` | `Ui` | ASCII UpperCamel Dart identifier. Its lowercased form must not be a Dart reserved word. `Remix` and `Mix` are reserved for runtime dependencies. | +| `preset` | `default` | Bundled registry selected for every later `add`. It must be chosen at `init`; this release bundles `default`. | | `paths.ui` | `lib/ui` | Project-relative, normalized, no traversal, must be under `lib/`. Symlinks that escape the package root are rejected. | Unknown keys are an error rather than a warning, so a typo fails loudly instead @@ -216,14 +219,14 @@ The prefix drives two identifier casings at once. With `prefix: Acme` you get the type `AcmeButton` and the function `acmeButtonStyle`. -Choose `prefix` and `paths.ui` before your first `add`. The CLI will not rename -or move source it has already installed. +Choose `prefix`, `preset`, and `paths.ui` before your first `add`. The CLI will +not rename, restyle, or move source it has already installed. Running `init` with different values against an existing `remix.yaml` is a hard error, not a migration: ``` -FormatException: Existing remix.yaml does not match the requested prefix and UI path. +FormatException: Existing remix.yaml does not match the requested prefix, preset, and UI path. ``` Editing `remix.yaml` by hand afterwards is worse, because installed state is diff --git a/packages/remix_cli/CHANGELOG.md b/packages/remix_cli/CHANGELOG.md index 34dbb689d..4acafc1a6 100644 --- a/packages/remix_cli/CHANGELOG.md +++ b/packages/remix_cli/CHANGELOG.md @@ -1,5 +1,8 @@ ## 0.1.0 +- Adds a preset axis to `remix init`. New configurations use schema 2 and + record `preset: default`; schema 1 configurations remain compatible and + read as the default preset. - Initial project-local CLI for installing editable Remix UI source. - Uses the Flutter SDK and its Dart executable on Windows. CI verifies installation with the real SDK. - Supports explicit, glob, and nested workspace members through Pub's resolved package configuration. diff --git a/packages/remix_cli/README.md b/packages/remix_cli/README.md index 152771f66..83f4dceaa 100644 --- a/packages/remix_cli/README.md +++ b/packages/remix_cli/README.md @@ -58,8 +58,9 @@ dart run remix_cli:remix init The default configuration is: ```yaml -schema: 1 +schema: 2 prefix: Ui +preset: default paths: ui: lib/ui ``` @@ -67,7 +68,7 @@ paths: Customize it only at initialization: ```shell -dart run remix_cli:remix init --prefix Acme --ui-path lib/design_system +dart run remix_cli:remix init --prefix Acme --preset default --ui-path lib/design_system ``` `init` validates the Flutter project, writes `remix.yaml`, and creates a barrel diff --git a/packages/remix_cli/lib/src/cli.dart b/packages/remix_cli/lib/src/cli.dart index 0aabc822c..9c8c42258 100644 --- a/packages/remix_cli/lib/src/cli.dart +++ b/packages/remix_cli/lib/src/cli.dart @@ -17,9 +17,14 @@ typedef InitHandler = Future Function(InitOptions options); typedef AddHandler = Future Function(AddOptions options); final class InitOptions { - const InitOptions({required this.prefix, required this.uiPath}); + const InitOptions({ + required this.prefix, + required this.preset, + required this.uiPath, + }); final String prefix; + final String preset; final String uiPath; } @@ -122,6 +127,7 @@ final class _InitCommand extends Command { _InitCommand(this._handler) { argParser ..addOption('prefix', defaultsTo: 'Ui') + ..addOption('preset', defaultsTo: 'default') ..addOption('ui-path', defaultsTo: 'lib/ui'); } @@ -145,6 +151,7 @@ final class _InitCommand extends Command { await handler( InitOptions( prefix: argResults!.option('prefix')!, + preset: argResults!.option('preset')!, uiPath: argResults!.option('ui-path')!, ), ); diff --git a/packages/remix_cli/lib/src/installer.dart b/packages/remix_cli/lib/src/installer.dart index 7a3ec0bc8..35afe4ec8 100644 --- a/packages/remix_cli/lib/src/installer.dart +++ b/packages/remix_cli/lib/src/installer.dart @@ -21,7 +21,10 @@ $managedExportsStart $managedExportsEnd '''; -typedef RegistryLoader = Future Function(); +typedef RegistryLoader = Future Function(String preset); + +Future _loadBundledRegistry(String preset) => + RegistryCatalog.loadBundled(preset: preset); abstract interface class ProjectFileWriter { void write(File target, String contents); @@ -54,7 +57,7 @@ final class Installer { required LineWriter writeOut, ProcessRunner processRunner = const SystemProcessRunner(), ProjectFileWriter fileWriter = const AtomicProjectFileWriter(), - RegistryLoader registryLoader = RegistryCatalog.loadBundled, + RegistryLoader registryLoader = _loadBundledRegistry, }) : projectRoot = projectRoot.absolute, _writeOut = writeOut, _processRunner = processRunner, @@ -73,6 +76,7 @@ final class Installer { final requested = ProjectConfig.create( packageRoot: root, prefix: options.prefix, + preset: options.preset, uiPath: options.uiPath, ); final configFile = File(p.join(root.path, projectConfigFileName)); @@ -88,9 +92,11 @@ final class Installer { packageRoot: root, ); if (existing.prefix != requested.prefix || + existing.preset != requested.preset || existing.uiPath != requested.uiPath) { throw const FormatException( - 'Existing remix.yaml does not match the requested prefix and UI path.', + 'Existing remix.yaml does not match the requested prefix, preset, ' + 'and UI path.', ); } writeConfig = false; @@ -137,7 +143,7 @@ final class Installer { final currentBarrel = barrel.readAsStringSync(); validateManagedBarrel(currentBarrel); - final catalog = await _registryLoader(); + final catalog = await _registryLoader(config.preset); final items = catalog.resolve(options.item); final requested = items.last; final rendered = {}; diff --git a/packages/remix_cli/lib/src/project_config.dart b/packages/remix_cli/lib/src/project_config.dart index 2f29f13ff..452c24315 100644 --- a/packages/remix_cli/lib/src/project_config.dart +++ b/packages/remix_cli/lib/src/project_config.dart @@ -4,20 +4,28 @@ import 'dart:io'; import 'package:path/path.dart' as p; import 'package:yaml/yaml.dart'; +import 'registry.dart'; + const projectConfigFileName = 'remix.yaml'; -const supportedProjectSchema = 1; +const supportedProjectSchema = 2; final class ProjectConfig { - ProjectConfig._({required this.prefix, required this.uiPath}); + ProjectConfig._({ + required this.prefix, + required this.preset, + required this.uiPath, + }); factory ProjectConfig.create({ required Directory packageRoot, required String prefix, + required String preset, required String uiPath, }) { _validatePrefix(prefix); + _validatePreset(preset); _validateUiPath(packageRoot, uiPath); - return ProjectConfig._(prefix: prefix, uiPath: uiPath); + return ProjectConfig._(prefix: prefix, preset: preset, uiPath: uiPath); } factory ProjectConfig.parse(String source, {required Directory packageRoot}) { @@ -30,18 +38,28 @@ final class ProjectConfig { if (document is! YamlMap) { throw const FormatException('$projectConfigFileName must contain a map.'); } - _requireExactKeys(document, {'schema', 'prefix', 'paths'}, 'configuration'); - final schema = document['schema']; - if (schema != supportedProjectSchema) { + if (schema != 1 && schema != supportedProjectSchema) { throw FormatException( - 'Unsupported remix.yaml schema $schema; remix_cli supports schema 1.', + 'Unsupported remix.yaml schema $schema; ' + 'remix_cli supports schemas 1 and $supportedProjectSchema.', ); } + _requireExactKeys( + document, + schema == 1 + ? {'schema', 'prefix', 'paths'} + : {'schema', 'prefix', 'preset', 'paths'}, + 'configuration', + ); final prefix = document['prefix']; if (prefix is! String) { throw const FormatException('remix.yaml prefix must be a string.'); } + final preset = schema == 1 ? 'default' : document['preset']; + if (preset is! String) { + throw const FormatException('remix.yaml preset must be a string.'); + } final paths = document['paths']; if (paths is! YamlMap) { throw const FormatException('remix.yaml paths must be a map.'); @@ -54,19 +72,22 @@ final class ProjectConfig { return ProjectConfig.create( packageRoot: packageRoot, prefix: prefix, + preset: preset, uiPath: uiPath, ); } final String prefix; + final String preset; final String uiPath; String get valuePrefix => '${prefix.substring(0, 1).toLowerCase()}${prefix.substring(1)}'; String encode() => - '''schema: 1 + '''schema: 2 prefix: $prefix +preset: $preset paths: ui: ${_encodeYamlPath(uiPath)} '''; @@ -197,6 +218,17 @@ void _validatePrefix(String prefix) { } } +void _validatePreset(String preset) { + if (!RegExp(r'^[a-z][a-z0-9_]*$').hasMatch(preset)) { + throw const FormatException('preset must be a lowercase ASCII identifier.'); + } + if (!bundledPresets.contains(preset)) { + throw FormatException( + 'Unknown preset $preset. Bundled presets: ${bundledPresets.join(', ')}.', + ); + } +} + void _validateUiPath(Directory packageRoot, String uiPath) { _validateProjectPath(packageRoot, uiPath, _ProjectPathKind.directory); if (!uiPath.startsWith('lib/')) { diff --git a/packages/remix_cli/lib/src/registry.dart b/packages/remix_cli/lib/src/registry.dart index 39052ed75..73d6525a6 100644 --- a/packages/remix_cli/lib/src/registry.dart +++ b/packages/remix_cli/lib/src/registry.dart @@ -5,7 +5,7 @@ import 'package:path/path.dart' as p; import 'package:pub_semver/pub_semver.dart'; import 'package:yaml/yaml.dart'; -const bundledRegistryUri = 'package:remix_cli/src/registry/registry.yaml'; +const bundledPresets = {'default'}; abstract interface class RegistryAssetLoader { Future read(Uri uri); @@ -29,6 +29,7 @@ final class PackageRegistryAssetLoader implements RegistryAssetLoader { final class RegistryCatalog { RegistryCatalog._({ + required this.preset, required this.items, required Uri rootUri, required RegistryAssetLoader loader, @@ -37,6 +38,7 @@ final class RegistryCatalog { factory RegistryCatalog.parse( String source, { + required String preset, required Uri rootUri, RegistryAssetLoader loader = const PackageRegistryAssetLoader(), }) { @@ -63,6 +65,7 @@ final class RegistryCatalog { items[name] = _parseItem(name, entry.value.value); } final catalog = RegistryCatalog._( + preset: preset, items: Map.unmodifiable(items), rootUri: rootUri, loader: loader, @@ -72,24 +75,37 @@ final class RegistryCatalog { } static Future loadBundled({ + required String preset, RegistryAssetLoader loader = const PackageRegistryAssetLoader(), }) async { - final uri = Uri.parse(bundledRegistryUri); + if (!bundledPresets.contains(preset)) { + throw FormatException( + 'Unknown preset $preset. Bundled presets: ' + '${bundledPresets.join(', ')}.', + ); + } + final uri = Uri.parse( + 'package:remix_cli/src/registry/$preset/registry.yaml', + ); final source = await loader.read(uri); return RegistryCatalog.parse( source, + preset: preset, rootUri: uri.resolve('.'), loader: loader, ); } + final String preset; final Map items; final Uri _rootUri; final RegistryAssetLoader _loader; List resolve(String requested) { if (!items.containsKey(requested)) { - throw FormatException('Unknown registry item $requested.'); + throw FormatException( + 'Unknown registry item $requested in the $preset preset.', + ); } final ordered = []; final visited = {}; diff --git a/packages/remix_cli/lib/src/registry/registry.yaml b/packages/remix_cli/lib/src/registry/default/registry.yaml similarity index 100% rename from packages/remix_cli/lib/src/registry/registry.yaml rename to packages/remix_cli/lib/src/registry/default/registry.yaml diff --git a/packages/remix_cli/lib/src/registry/templates/accordion/accordion.dart.tmpl b/packages/remix_cli/lib/src/registry/default/templates/accordion/accordion.dart.tmpl similarity index 100% rename from packages/remix_cli/lib/src/registry/templates/accordion/accordion.dart.tmpl rename to packages/remix_cli/lib/src/registry/default/templates/accordion/accordion.dart.tmpl diff --git a/packages/remix_cli/lib/src/registry/templates/avatar/avatar.dart.tmpl b/packages/remix_cli/lib/src/registry/default/templates/avatar/avatar.dart.tmpl similarity index 100% rename from packages/remix_cli/lib/src/registry/templates/avatar/avatar.dart.tmpl rename to packages/remix_cli/lib/src/registry/default/templates/avatar/avatar.dart.tmpl diff --git a/packages/remix_cli/lib/src/registry/templates/badge/badge.dart.tmpl b/packages/remix_cli/lib/src/registry/default/templates/badge/badge.dart.tmpl similarity index 100% rename from packages/remix_cli/lib/src/registry/templates/badge/badge.dart.tmpl rename to packages/remix_cli/lib/src/registry/default/templates/badge/badge.dart.tmpl diff --git a/packages/remix_cli/lib/src/registry/templates/button/button.dart.tmpl b/packages/remix_cli/lib/src/registry/default/templates/button/button.dart.tmpl similarity index 100% rename from packages/remix_cli/lib/src/registry/templates/button/button.dart.tmpl rename to packages/remix_cli/lib/src/registry/default/templates/button/button.dart.tmpl diff --git a/packages/remix_cli/lib/src/registry/templates/callout/callout.dart.tmpl b/packages/remix_cli/lib/src/registry/default/templates/callout/callout.dart.tmpl similarity index 100% rename from packages/remix_cli/lib/src/registry/templates/callout/callout.dart.tmpl rename to packages/remix_cli/lib/src/registry/default/templates/callout/callout.dart.tmpl diff --git a/packages/remix_cli/lib/src/registry/templates/card/card.dart.tmpl b/packages/remix_cli/lib/src/registry/default/templates/card/card.dart.tmpl similarity index 100% rename from packages/remix_cli/lib/src/registry/templates/card/card.dart.tmpl rename to packages/remix_cli/lib/src/registry/default/templates/card/card.dart.tmpl diff --git a/packages/remix_cli/lib/src/registry/templates/chart/chart.dart.tmpl b/packages/remix_cli/lib/src/registry/default/templates/chart/chart.dart.tmpl similarity index 100% rename from packages/remix_cli/lib/src/registry/templates/chart/chart.dart.tmpl rename to packages/remix_cli/lib/src/registry/default/templates/chart/chart.dart.tmpl diff --git a/packages/remix_cli/lib/src/registry/templates/checkbox/checkbox.dart.tmpl b/packages/remix_cli/lib/src/registry/default/templates/checkbox/checkbox.dart.tmpl similarity index 100% rename from packages/remix_cli/lib/src/registry/templates/checkbox/checkbox.dart.tmpl rename to packages/remix_cli/lib/src/registry/default/templates/checkbox/checkbox.dart.tmpl diff --git a/packages/remix_cli/lib/src/registry/templates/data_list/data_list.dart.tmpl b/packages/remix_cli/lib/src/registry/default/templates/data_list/data_list.dart.tmpl similarity index 100% rename from packages/remix_cli/lib/src/registry/templates/data_list/data_list.dart.tmpl rename to packages/remix_cli/lib/src/registry/default/templates/data_list/data_list.dart.tmpl diff --git a/packages/remix_cli/lib/src/registry/templates/data_table/data_table.dart.tmpl b/packages/remix_cli/lib/src/registry/default/templates/data_table/data_table.dart.tmpl similarity index 100% rename from packages/remix_cli/lib/src/registry/templates/data_table/data_table.dart.tmpl rename to packages/remix_cli/lib/src/registry/default/templates/data_table/data_table.dart.tmpl diff --git a/packages/remix_cli/lib/src/registry/templates/dialog/dialog.dart.tmpl b/packages/remix_cli/lib/src/registry/default/templates/dialog/dialog.dart.tmpl similarity index 100% rename from packages/remix_cli/lib/src/registry/templates/dialog/dialog.dart.tmpl rename to packages/remix_cli/lib/src/registry/default/templates/dialog/dialog.dart.tmpl diff --git a/packages/remix_cli/lib/src/registry/templates/disclosure/disclosure.dart.tmpl b/packages/remix_cli/lib/src/registry/default/templates/disclosure/disclosure.dart.tmpl similarity index 100% rename from packages/remix_cli/lib/src/registry/templates/disclosure/disclosure.dart.tmpl rename to packages/remix_cli/lib/src/registry/default/templates/disclosure/disclosure.dart.tmpl diff --git a/packages/remix_cli/lib/src/registry/templates/divider/divider.dart.tmpl b/packages/remix_cli/lib/src/registry/default/templates/divider/divider.dart.tmpl similarity index 100% rename from packages/remix_cli/lib/src/registry/templates/divider/divider.dart.tmpl rename to packages/remix_cli/lib/src/registry/default/templates/divider/divider.dart.tmpl diff --git a/packages/remix_cli/lib/src/registry/templates/icon_button/icon_button.dart.tmpl b/packages/remix_cli/lib/src/registry/default/templates/icon_button/icon_button.dart.tmpl similarity index 100% rename from packages/remix_cli/lib/src/registry/templates/icon_button/icon_button.dart.tmpl rename to packages/remix_cli/lib/src/registry/default/templates/icon_button/icon_button.dart.tmpl diff --git a/packages/remix_cli/lib/src/registry/templates/icons/icons.dart.tmpl b/packages/remix_cli/lib/src/registry/default/templates/icons/icons.dart.tmpl similarity index 100% rename from packages/remix_cli/lib/src/registry/templates/icons/icons.dart.tmpl rename to packages/remix_cli/lib/src/registry/default/templates/icons/icons.dart.tmpl diff --git a/packages/remix_cli/lib/src/registry/templates/link/link.dart.tmpl b/packages/remix_cli/lib/src/registry/default/templates/link/link.dart.tmpl similarity index 100% rename from packages/remix_cli/lib/src/registry/templates/link/link.dart.tmpl rename to packages/remix_cli/lib/src/registry/default/templates/link/link.dart.tmpl diff --git a/packages/remix_cli/lib/src/registry/templates/menu/menu.dart.tmpl b/packages/remix_cli/lib/src/registry/default/templates/menu/menu.dart.tmpl similarity index 100% rename from packages/remix_cli/lib/src/registry/templates/menu/menu.dart.tmpl rename to packages/remix_cli/lib/src/registry/default/templates/menu/menu.dart.tmpl diff --git a/packages/remix_cli/lib/src/registry/templates/popover/popover.dart.tmpl b/packages/remix_cli/lib/src/registry/default/templates/popover/popover.dart.tmpl similarity index 100% rename from packages/remix_cli/lib/src/registry/templates/popover/popover.dart.tmpl rename to packages/remix_cli/lib/src/registry/default/templates/popover/popover.dart.tmpl diff --git a/packages/remix_cli/lib/src/registry/templates/progress/progress.dart.tmpl b/packages/remix_cli/lib/src/registry/default/templates/progress/progress.dart.tmpl similarity index 100% rename from packages/remix_cli/lib/src/registry/templates/progress/progress.dart.tmpl rename to packages/remix_cli/lib/src/registry/default/templates/progress/progress.dart.tmpl diff --git a/packages/remix_cli/lib/src/registry/templates/radio/radio.dart.tmpl b/packages/remix_cli/lib/src/registry/default/templates/radio/radio.dart.tmpl similarity index 100% rename from packages/remix_cli/lib/src/registry/templates/radio/radio.dart.tmpl rename to packages/remix_cli/lib/src/registry/default/templates/radio/radio.dart.tmpl diff --git a/packages/remix_cli/lib/src/registry/templates/segmented_control/segmented_control.dart.tmpl b/packages/remix_cli/lib/src/registry/default/templates/segmented_control/segmented_control.dart.tmpl similarity index 100% rename from packages/remix_cli/lib/src/registry/templates/segmented_control/segmented_control.dart.tmpl rename to packages/remix_cli/lib/src/registry/default/templates/segmented_control/segmented_control.dart.tmpl diff --git a/packages/remix_cli/lib/src/registry/templates/select/select.dart.tmpl b/packages/remix_cli/lib/src/registry/default/templates/select/select.dart.tmpl similarity index 100% rename from packages/remix_cli/lib/src/registry/templates/select/select.dart.tmpl rename to packages/remix_cli/lib/src/registry/default/templates/select/select.dart.tmpl diff --git a/packages/remix_cli/lib/src/registry/templates/skeleton/skeleton.dart.tmpl b/packages/remix_cli/lib/src/registry/default/templates/skeleton/skeleton.dart.tmpl similarity index 100% rename from packages/remix_cli/lib/src/registry/templates/skeleton/skeleton.dart.tmpl rename to packages/remix_cli/lib/src/registry/default/templates/skeleton/skeleton.dart.tmpl diff --git a/packages/remix_cli/lib/src/registry/templates/slider/slider.dart.tmpl b/packages/remix_cli/lib/src/registry/default/templates/slider/slider.dart.tmpl similarity index 100% rename from packages/remix_cli/lib/src/registry/templates/slider/slider.dart.tmpl rename to packages/remix_cli/lib/src/registry/default/templates/slider/slider.dart.tmpl diff --git a/packages/remix_cli/lib/src/registry/templates/spinner/spinner.dart.tmpl b/packages/remix_cli/lib/src/registry/default/templates/spinner/spinner.dart.tmpl similarity index 100% rename from packages/remix_cli/lib/src/registry/templates/spinner/spinner.dart.tmpl rename to packages/remix_cli/lib/src/registry/default/templates/spinner/spinner.dart.tmpl diff --git a/packages/remix_cli/lib/src/registry/templates/switch/switch.dart.tmpl b/packages/remix_cli/lib/src/registry/default/templates/switch/switch.dart.tmpl similarity index 100% rename from packages/remix_cli/lib/src/registry/templates/switch/switch.dart.tmpl rename to packages/remix_cli/lib/src/registry/default/templates/switch/switch.dart.tmpl diff --git a/packages/remix_cli/lib/src/registry/templates/tabs/tabs.dart.tmpl b/packages/remix_cli/lib/src/registry/default/templates/tabs/tabs.dart.tmpl similarity index 100% rename from packages/remix_cli/lib/src/registry/templates/tabs/tabs.dart.tmpl rename to packages/remix_cli/lib/src/registry/default/templates/tabs/tabs.dart.tmpl diff --git a/packages/remix_cli/lib/src/registry/templates/textfield/textfield.dart.tmpl b/packages/remix_cli/lib/src/registry/default/templates/textfield/textfield.dart.tmpl similarity index 100% rename from packages/remix_cli/lib/src/registry/templates/textfield/textfield.dart.tmpl rename to packages/remix_cli/lib/src/registry/default/templates/textfield/textfield.dart.tmpl diff --git a/packages/remix_cli/lib/src/registry/templates/theme/theme_data.dart.tmpl b/packages/remix_cli/lib/src/registry/default/templates/theme/theme_data.dart.tmpl similarity index 100% rename from packages/remix_cli/lib/src/registry/templates/theme/theme_data.dart.tmpl rename to packages/remix_cli/lib/src/registry/default/templates/theme/theme_data.dart.tmpl diff --git a/packages/remix_cli/lib/src/registry/templates/theme/theme_scope.dart.tmpl b/packages/remix_cli/lib/src/registry/default/templates/theme/theme_scope.dart.tmpl similarity index 100% rename from packages/remix_cli/lib/src/registry/templates/theme/theme_scope.dart.tmpl rename to packages/remix_cli/lib/src/registry/default/templates/theme/theme_scope.dart.tmpl diff --git a/packages/remix_cli/lib/src/registry/templates/theme/tokens.dart.tmpl b/packages/remix_cli/lib/src/registry/default/templates/theme/tokens.dart.tmpl similarity index 100% rename from packages/remix_cli/lib/src/registry/templates/theme/tokens.dart.tmpl rename to packages/remix_cli/lib/src/registry/default/templates/theme/tokens.dart.tmpl diff --git a/packages/remix_cli/lib/src/registry/templates/toggle/toggle.dart.tmpl b/packages/remix_cli/lib/src/registry/default/templates/toggle/toggle.dart.tmpl similarity index 100% rename from packages/remix_cli/lib/src/registry/templates/toggle/toggle.dart.tmpl rename to packages/remix_cli/lib/src/registry/default/templates/toggle/toggle.dart.tmpl diff --git a/packages/remix_cli/lib/src/registry/templates/toggle_group/toggle_group.dart.tmpl b/packages/remix_cli/lib/src/registry/default/templates/toggle_group/toggle_group.dart.tmpl similarity index 100% rename from packages/remix_cli/lib/src/registry/templates/toggle_group/toggle_group.dart.tmpl rename to packages/remix_cli/lib/src/registry/default/templates/toggle_group/toggle_group.dart.tmpl diff --git a/packages/remix_cli/lib/src/registry/templates/tooltip/tooltip.dart.tmpl b/packages/remix_cli/lib/src/registry/default/templates/tooltip/tooltip.dart.tmpl similarity index 100% rename from packages/remix_cli/lib/src/registry/templates/tooltip/tooltip.dart.tmpl rename to packages/remix_cli/lib/src/registry/default/templates/tooltip/tooltip.dart.tmpl diff --git a/packages/remix_cli/test/add_command_test.dart b/packages/remix_cli/test/add_command_test.dart index 2f3c761bd..8400e3a1a 100644 --- a/packages/remix_cli/test/add_command_test.dart +++ b/packages/remix_cli/test/add_command_test.dart @@ -20,7 +20,7 @@ void main() { processRunner: runner, ); await installer.initialize( - const InitOptions(prefix: 'Ui', uiPath: 'lib/ui'), + const InitOptions(prefix: 'Ui', preset: 'default', uiPath: 'lib/ui'), ); final code = await runRemixCli( @@ -48,7 +48,7 @@ void main() { processRunner: runner, ); await installer.initialize( - const InitOptions(prefix: 'Ui', uiPath: 'lib/ui'), + const InitOptions(prefix: 'Ui', preset: 'default', uiPath: 'lib/ui'), ); final before = snapshotFiles(root); final errors = []; diff --git a/packages/remix_cli/test/cli_test.dart b/packages/remix_cli/test/cli_test.dart index e5295c272..1738e08be 100644 --- a/packages/remix_cli/test/cli_test.dart +++ b/packages/remix_cli/test/cli_test.dart @@ -113,15 +113,25 @@ void main() { successExitCode, ); expect(received!.prefix, 'Ui'); + expect(received!.preset, 'default'); expect(received!.uiPath, 'lib/ui'); await runRemixCli( - ['init', '--prefix', 'Acme', '--ui-path', 'lib/design_system'], + [ + 'init', + '--prefix', + 'Acme', + '--preset', + 'default', + '--ui-path', + 'lib/design_system', + ], writeOut: fail, writeError: fail, onInit: onInit, ); expect(received!.prefix, 'Acme'); + expect(received!.preset, 'default'); expect(received!.uiPath, 'lib/design_system'); }); diff --git a/packages/remix_cli/test/init_command_test.dart b/packages/remix_cli/test/init_command_test.dart index 2b2ac89fd..90e84f1be 100644 --- a/packages/remix_cli/test/init_command_test.dart +++ b/packages/remix_cli/test/init_command_test.dart @@ -21,12 +21,13 @@ void main() { test('initializes defaults and an identical second run is a no-op', () async { await installer.initialize( - const InitOptions(prefix: 'Ui', uiPath: 'lib/ui'), + const InitOptions(prefix: 'Ui', preset: 'default', uiPath: 'lib/ui'), ); expect( File(p.join(root.path, 'remix.yaml')).readAsStringSync(), - '''schema: 1 + '''schema: 2 prefix: Ui +preset: default paths: ui: lib/ui ''', @@ -39,7 +40,7 @@ paths: final first = snapshotFiles(root); await installer.initialize( - const InitOptions(prefix: 'Ui', uiPath: 'lib/ui'), + const InitOptions(prefix: 'Ui', preset: 'default', uiPath: 'lib/ui'), ); expect(snapshotFiles(root), first); @@ -50,13 +51,13 @@ paths: 'repairs only a missing config and names the barrel preserved', () async { await installer.initialize( - const InitOptions(prefix: 'Ui', uiPath: 'lib/ui'), + const InitOptions(prefix: 'Ui', preset: 'default', uiPath: 'lib/ui'), ); File(p.join(root.path, 'remix.yaml')).deleteSync(); output.clear(); await installer.initialize( - const InitOptions(prefix: 'Ui', uiPath: 'lib/ui'), + const InitOptions(prefix: 'Ui', preset: 'default', uiPath: 'lib/ui'), ); expect(output, ['Created remix.yaml; preserved lib/ui/ui.dart.']); @@ -67,13 +68,13 @@ paths: 'repairs only a missing barrel and names the config preserved', () async { await installer.initialize( - const InitOptions(prefix: 'Ui', uiPath: 'lib/ui'), + const InitOptions(prefix: 'Ui', preset: 'default', uiPath: 'lib/ui'), ); File(p.join(root.path, 'lib', 'ui', 'ui.dart')).deleteSync(); output.clear(); await installer.initialize( - const InitOptions(prefix: 'Ui', uiPath: 'lib/ui'), + const InitOptions(prefix: 'Ui', preset: 'default', uiPath: 'lib/ui'), ); expect(output, ['Created lib/ui/ui.dart; preserved remix.yaml.']); @@ -82,7 +83,11 @@ paths: test('initializes a custom prefix and path', () async { await installer.initialize( - const InitOptions(prefix: 'Acme', uiPath: 'lib/design_system'), + const InitOptions( + prefix: 'Acme', + preset: 'default', + uiPath: 'lib/design_system', + ), ); expect( @@ -95,6 +100,19 @@ paths: ); }); + test('rejects an unknown preset without partial initialization', () async { + final before = snapshotFiles(root); + + await expectLater( + installer.initialize( + const InitOptions(prefix: 'Ui', preset: 'missing', uiPath: 'lib/ui'), + ), + throwsFormatException, + ); + + expect(snapshotFiles(root), before); + }); + test( 'rejects runtime prefixes before initialization writes source', () async { @@ -125,7 +143,11 @@ paths: await expectLater( installer.initialize( - const InitOptions(prefix: 'Acme', uiPath: 'lib/design_system'), + const InitOptions( + prefix: 'Acme', + preset: 'default', + uiPath: 'lib/design_system', + ), ), throwsFormatException, ); @@ -141,7 +163,9 @@ paths: final before = snapshotFiles(root); await expectLater( - installer.initialize(const InitOptions(prefix: 'Ui', uiPath: 'lib/ui')), + installer.initialize( + const InitOptions(prefix: 'Ui', preset: 'default', uiPath: 'lib/ui'), + ), throwsFormatException, ); @@ -158,7 +182,9 @@ paths: final before = snapshotFiles(root); await expectLater( - installer.initialize(const InitOptions(prefix: 'Ui', uiPath: 'lib/ui')), + installer.initialize( + const InitOptions(prefix: 'Ui', preset: 'default', uiPath: 'lib/ui'), + ), throwsFormatException, ); @@ -179,7 +205,9 @@ paths: final before = snapshotFiles(root); await expectLater( - installer.initialize(const InitOptions(prefix: 'Ui', uiPath: 'lib/ui')), + installer.initialize( + const InitOptions(prefix: 'Ui', preset: 'default', uiPath: 'lib/ui'), + ), throwsFormatException, reason: source, ); @@ -192,13 +220,17 @@ paths: test('rejects non-Flutter and missing package roots', () async { File(p.join(root.path, 'pubspec.yaml')).writeAsStringSync('name: plain\n'); await expectLater( - installer.initialize(const InitOptions(prefix: 'Ui', uiPath: 'lib/ui')), + installer.initialize( + const InitOptions(prefix: 'Ui', preset: 'default', uiPath: 'lib/ui'), + ), throwsFormatException, ); File(p.join(root.path, 'pubspec.yaml')).deleteSync(); await expectLater( - installer.initialize(const InitOptions(prefix: 'Ui', uiPath: 'lib/ui')), + installer.initialize( + const InitOptions(prefix: 'Ui', preset: 'default', uiPath: 'lib/ui'), + ), throwsFormatException, ); }); diff --git a/packages/remix_cli/test/installer_test.dart b/packages/remix_cli/test/installer_test.dart index b7e2a7695..b6ae59a31 100644 --- a/packages/remix_cli/test/installer_test.dart +++ b/packages/remix_cli/test/installer_test.dart @@ -51,12 +51,27 @@ void main() { setUp(() async { root = createFlutterPackage(); - await Installer( + await Installer(projectRoot: root, writeOut: (_) {}).initialize( + const InitOptions(prefix: 'Ui', preset: 'default', uiPath: 'lib/ui'), + ); + }); + tearDown(() => root.deleteSync(recursive: true)); + + test('add loads the registry selected by project configuration', () async { + String? loadedPreset; + final installer = Installer( projectRoot: root, writeOut: (_) {}, - ).initialize(const InitOptions(prefix: 'Ui', uiPath: 'lib/ui')); + registryLoader: (preset) async { + loadedPreset = preset; + return RegistryCatalog.loadBundled(preset: 'default'); + }, + ); + + await installer.add(const AddOptions(item: 'button', mode: AddMode.dryRun)); + + expect(loadedPreset, 'default'); }); - tearDown(() => root.deleteSync(recursive: true)); test( 'new add installs Theme before Button and runs exact processes', @@ -295,10 +310,9 @@ packages: ] } '''); - await Installer( - projectRoot: member, - writeOut: (_) {}, - ).initialize(const InitOptions(prefix: 'Ui', uiPath: 'lib/ui')); + await Installer(projectRoot: member, writeOut: (_) {}).initialize( + const InitOptions(prefix: 'Ui', preset: 'default', uiPath: 'lib/ui'), + ); final installer = Installer( projectRoot: member, @@ -500,17 +514,20 @@ packages: // the formatter would then rewrite the file the CLI just wrote — which // shows up for a consumer as an `add --diff` that never comes back // clean. - final catalog = await RegistryCatalog.loadBundled(); + final catalog = await RegistryCatalog.loadBundled(preset: 'default'); final items = catalog.items.keys.where((name) => name != 'theme'); expect(items, isNotEmpty); for (final item in items) { final caseRoot = createFlutterPackage(); addTearDown(() => caseRoot.deleteSync(recursive: true)); - await Installer( - projectRoot: caseRoot, - writeOut: (_) {}, - ).initialize(const InitOptions(prefix: 'Playground', uiPath: 'lib/ui')); + await Installer(projectRoot: caseRoot, writeOut: (_) {}).initialize( + const InitOptions( + prefix: 'Playground', + preset: 'default', + uiPath: 'lib/ui', + ), + ); final remixUiIcons = item == 'icons' ? '0.1.0' : null; final mixChart = item == 'chart' ? '0.0.1-beta.1' : null; writeRequiredPubspec( @@ -607,10 +624,9 @@ packages: for (final stage in ['build', 'analyze']) { final caseRoot = createFlutterPackage(); addTearDown(() => caseRoot.deleteSync(recursive: true)); - await Installer( - projectRoot: caseRoot, - writeOut: (_) {}, - ).initialize(const InitOptions(prefix: 'Ui', uiPath: 'lib/ui')); + await Installer(projectRoot: caseRoot, writeOut: (_) {}).initialize( + const InitOptions(prefix: 'Ui', preset: 'default', uiPath: 'lib/ui'), + ); writeRequiredPubspec(caseRoot); writeRequiredLock(caseRoot); @@ -681,10 +697,9 @@ packages: for (final declaration in declarations) { final caseRoot = createFlutterPackage(); addTearDown(() => caseRoot.deleteSync(recursive: true)); - await Installer( - projectRoot: caseRoot, - writeOut: (_) {}, - ).initialize(const InitOptions(prefix: 'Ui', uiPath: 'lib/ui')); + await Installer(projectRoot: caseRoot, writeOut: (_) {}).initialize( + const InitOptions(prefix: 'Ui', preset: 'default', uiPath: 'lib/ui'), + ); final pubspec = File(p.join(caseRoot.path, 'pubspec.yaml')) ..writeAsStringSync('''name: consumer environment: @@ -802,10 +817,9 @@ dev_dependencies: for (final runtime in ['remix', 'mix_annotations']) { final caseRoot = createFlutterPackage(); addTearDown(() => caseRoot.deleteSync(recursive: true)); - await Installer( - projectRoot: caseRoot, - writeOut: (_) {}, - ).initialize(const InitOptions(prefix: 'Ui', uiPath: 'lib/ui')); + await Installer(projectRoot: caseRoot, writeOut: (_) {}).initialize( + const InitOptions(prefix: 'Ui', preset: 'default', uiPath: 'lib/ui'), + ); File(p.join(caseRoot.path, 'pubspec.yaml')).writeAsStringSync( runtime == 'remix' ? '''name: consumer @@ -872,10 +886,9 @@ dev_dependencies: () async { final caseRoot = createFlutterPackage(); addTearDown(() => caseRoot.deleteSync(recursive: true)); - await Installer( - projectRoot: caseRoot, - writeOut: (_) {}, - ).initialize(const InitOptions(prefix: 'Ui', uiPath: 'lib/ui')); + await Installer(projectRoot: caseRoot, writeOut: (_) {}).initialize( + const InitOptions(prefix: 'Ui', preset: 'default', uiPath: 'lib/ui'), + ); File(p.join(caseRoot.path, 'pubspec.yaml')).writeAsStringSync(''' name: consumer environment: diff --git a/packages/remix_cli/test/project_config_test.dart b/packages/remix_cli/test/project_config_test.dart index 477e4f0eb..a6f653075 100644 --- a/packages/remix_cli/test/project_config_test.dart +++ b/packages/remix_cli/test/project_config_test.dart @@ -12,7 +12,7 @@ void main() { setUp(() => root = createFlutterPackage()); tearDown(() => root.deleteSync(recursive: true)); - test('parses and encodes the exact schema', () { + test('reads schema 1 as default and encodes schema 2', () { final config = ProjectConfig.parse('''schema: 1 prefix: Acme paths: @@ -21,20 +21,36 @@ paths: expect(config.prefix, 'Acme'); expect(config.valuePrefix, 'acme'); + expect(config.preset, 'default'); expect(config.uiPath, 'lib/design_system'); - expect(config.encode(), '''schema: 1 + expect(config.encode(), '''schema: 2 prefix: Acme +preset: default paths: ui: lib/design_system '''); }); + test('parses the exact schema 2 shape', () { + final config = ProjectConfig.parse('''schema: 2 +prefix: Acme +preset: default +paths: + ui: lib/design_system +''', packageRoot: root); + + expect(config.prefix, 'Acme'); + expect(config.preset, 'default'); + expect(config.uiPath, 'lib/design_system'); + }); + test('rejects invalid and reserved prefixes', () { for (final prefix in ['', 'ui', '_Ui', 'Ui-name', 'Üi', 'Class', 'Is']) { expect( () => ProjectConfig.create( packageRoot: root, prefix: prefix, + preset: 'default', uiPath: 'lib/ui', ), throwsFormatException, @@ -45,6 +61,7 @@ paths: test('rejects unknown, missing, and unsupported config fields', () { for (final source in [ + 'schema: 3\nprefix: Ui\npreset: default\npaths:\n ui: lib/ui\n', 'schema: 2\nprefix: Ui\npaths:\n ui: lib/ui\n', 'schema: 1\npaths:\n ui: lib/ui\n', 'schema: 1\nprefix: Ui\npaths:\n ui: lib/ui\nstyle: new\n', @@ -58,6 +75,21 @@ paths: } }); + test('rejects invalid and unbundled presets', () { + for (final preset in ['', 'Default', 'default-name', 'fortal']) { + expect( + () => ProjectConfig.create( + packageRoot: root, + prefix: 'Ui', + preset: preset, + uiPath: 'lib/ui', + ), + throwsFormatException, + reason: preset, + ); + } + }); + test('rejects absolute, unnormalized, traversing, and non-lib UI paths', () { for (final uiPath in [ '/lib/ui', @@ -72,6 +104,7 @@ paths: () => ProjectConfig.create( packageRoot: root, prefix: 'Ui', + preset: 'default', uiPath: uiPath, ), throwsFormatException, @@ -89,6 +122,7 @@ paths: () => ProjectConfig.create( packageRoot: root, prefix: 'Ui', + preset: 'default', uiPath: 'lib/ui', ), throwsFormatException, @@ -100,6 +134,7 @@ paths: final encoded = ProjectConfig.create( packageRoot: root, prefix: 'Acme', + preset: 'default', uiPath: uiPath, ).encode(); diff --git a/packages/remix_cli/test/registry_test.dart b/packages/remix_cli/test/registry_test.dart index c59dc48c4..710b7d10d 100644 --- a/packages/remix_cli/test/registry_test.dart +++ b/packages/remix_cli/test/registry_test.dart @@ -7,8 +7,9 @@ void main() { test( 'bundled registry resolves theme before button and loads assets', () async { - final catalog = await RegistryCatalog.loadBundled(); + final catalog = await RegistryCatalog.loadBundled(preset: 'default'); + expect(catalog.preset, 'default'); expect(catalog.resolve('button').map((item) => item.name), [ 'theme', 'button', @@ -21,10 +22,47 @@ void main() { }, ); + test('unknown preset fails before reading an asset and lists bundles', () { + expect( + RegistryCatalog.loadBundled( + preset: 'missing', + loader: const _NoopLoader(), + ), + throwsA( + isA().having( + (error) => error.message, + 'message', + allOf(contains('missing'), contains('default')), + ), + ), + ); + }); + + test('unknown item error identifies its preset', () { + final catalog = parse('''schema: 1 +items: + button: + files: + - source: templates/button.dart.tmpl + target: "@ui/button.dart" +'''); + + expect( + () => catalog.resolve('missing'), + throwsA( + isA().having( + (error) => error.message, + 'message', + contains('test preset'), + ), + ), + ); + }); + test( 'chart is a theme-only extension backed directly by mix_chart', () async { - final catalog = await RegistryCatalog.loadBundled(); + final catalog = await RegistryCatalog.loadBundled(preset: 'default'); final chart = catalog.items['chart']!; final source = await catalog.readTemplate(chart.files.single); @@ -202,7 +240,7 @@ items: ); test('every item resolves theme first and owns its expected files', () async { - final catalog = await RegistryCatalog.loadBundled(); + final catalog = await RegistryCatalog.loadBundled(preset: 'default'); // Both directions. Checking only that each listed name exists would let a // new registry item ship with no surface pinned and no rendering asserted. @@ -238,7 +276,7 @@ items: }); test('both prefixes render every configured public surface', () async { - final catalog = await RegistryCatalog.loadBundled(); + final catalog = await RegistryCatalog.loadBundled(preset: 'default'); for (final entry in _componentSurfaces.entries) { final source = await catalog.readTemplate( @@ -290,7 +328,7 @@ items: }); test('bundled templates stay inside the allowed import boundary', () async { - final catalog = await RegistryCatalog.loadBundled(); + final catalog = await RegistryCatalog.loadBundled(preset: 'default'); const allowedPackages = { 'flutter', 'remix', @@ -381,7 +419,8 @@ const _componentSurfaces = RegistryCatalog parse(String source) => RegistryCatalog.parse( source, - rootUri: Uri.parse('package:remix_cli/src/registry/'), + preset: 'test', + rootUri: Uri.parse('package:remix_cli/src/registry/test/'), loader: const _NoopLoader(), ); diff --git a/packages/remix_cli/test/test_support.dart b/packages/remix_cli/test/test_support.dart index 2e3bf9ad2..64d4b903b 100644 --- a/packages/remix_cli/test/test_support.dart +++ b/packages/remix_cli/test/test_support.dart @@ -25,7 +25,9 @@ final Version registryRemixFloor = String _readRegistryRemixConstraint() { // `dart test` runs with the package root as the current directory. final document = loadYaml( - File(p.join('lib', 'src', 'registry', 'registry.yaml')).readAsStringSync(), + File( + p.join('lib', 'src', 'registry', 'default', 'registry.yaml'), + ).readAsStringSync(), ); final items = (document as YamlMap)['items'] as YamlMap; for (final item in items.values) { diff --git a/test/tool/check_open_code_test.dart b/test/tool/check_open_code_test.dart index c7df0d245..42a053e88 100644 --- a/test/tool/check_open_code_test.dart +++ b/test/tool/check_open_code_test.dart @@ -29,7 +29,7 @@ void main() { test('an item the checker never installs is reported', () { final root = Directory('${sandbox.path}/repo'); final registry = File( - '${root.path}/packages/remix_cli/lib/src/registry/registry.yaml', + '${root.path}/packages/remix_cli/lib/src/registry/default/registry.yaml', ); registry.parent.createSync(recursive: true); registry.writeAsStringSync(''' diff --git a/tool/check_open_code.dart b/tool/check_open_code.dart index 2edb63381..3cbb65ac3 100644 --- a/tool/check_open_code.dart +++ b/tool/check_open_code.dart @@ -523,7 +523,7 @@ Future _resolveToolchain(Directory root, String pinned) async { /// checked — and every suite still passes. _Failure? _verifyRegistryCoverage(Directory repositoryRoot) { final file = File( - '${repositoryRoot.path}/packages/remix_cli/lib/src/registry/registry.yaml', + '${repositoryRoot.path}/packages/remix_cli/lib/src/registry/default/registry.yaml', ); if (!file.existsSync()) { return _Failure('packages/remix_cli is missing its registry.yaml.'); @@ -568,7 +568,7 @@ _Failure? _verifyResolvedRemixFloor({ required Map hostedPackages, }) { final registry = File( - '${repositoryRoot.path}/packages/remix_cli/lib/src/registry/registry.yaml', + '${repositoryRoot.path}/packages/remix_cli/lib/src/registry/default/registry.yaml', ); final document = loadYaml(registry.readAsStringSync()); final items = document is YamlMap ? document['items'] : null; diff --git a/tool/check_open_code_dogfood.dart b/tool/check_open_code_dogfood.dart index b67414fdd..b4433e8ff 100644 --- a/tool/check_open_code_dogfood.dart +++ b/tool/check_open_code_dogfood.dart @@ -64,7 +64,7 @@ Future main(List arguments) async { Future _run(Directory root) async { final registry = File( - '${root.path}/packages/remix_cli/lib/src/registry/registry.yaml', + '${root.path}/packages/remix_cli/lib/src/registry/default/registry.yaml', ); final playground = Directory('${root.path}/apps/playground'); if (!registry.existsSync()) return 'the bundled registry is missing.'; diff --git a/tool/check_version_alignment.dart b/tool/check_version_alignment.dart index ddafab8c7..8011013d6 100644 --- a/tool/check_version_alignment.dart +++ b/tool/check_version_alignment.dart @@ -18,7 +18,8 @@ const _lockstepPackages = ['remix', 'remix_fortal']; /// The registry file whose `remix` constraint must floor at the released /// `remix` version. -const _registryPath = 'packages/remix_cli/lib/src/registry/registry.yaml'; +const _registryPath = + 'packages/remix_cli/lib/src/registry/default/registry.yaml'; void main() { final workspaceRoot = Directory.current.absolute; diff --git a/tool/sync_registry_remix.dart b/tool/sync_registry_remix.dart index 55054b9e0..819375a50 100644 --- a/tool/sync_registry_remix.dart +++ b/tool/sync_registry_remix.dart @@ -52,7 +52,7 @@ void main(List arguments) { } final registry = File( - '${workspaceRoot.path}/packages/remix_cli/lib/src/registry/registry.yaml', + '${workspaceRoot.path}/packages/remix_cli/lib/src/registry/default/registry.yaml', ); if (!registry.existsSync()) { stderr.writeln('Missing ${registry.path}.'); diff --git a/tool/validate_docs.dart b/tool/validate_docs.dart index ee9a25731..3a8539a34 100644 --- a/tool/validate_docs.dart +++ b/tool/validate_docs.dart @@ -222,7 +222,7 @@ const _openCodeCatalogDocuments = [ /// wrote down. void _checkOpenCodeCatalog(Directory workspaceRoot, List failures) { final registry = File( - '${workspaceRoot.path}/packages/remix_cli/lib/src/registry/registry.yaml', + '${workspaceRoot.path}/packages/remix_cli/lib/src/registry/default/registry.yaml', ); if (!registry.existsSync()) { failures.add('packages/remix_cli is missing its registry.yaml.'); From 3d712fd6351251ee96121a6990ef9ae2b442241f Mon Sep 17 00:00:00 2001 From: Leo Farias Date: Fri, 4 Sep 2026 14:00:58 -0400 Subject: [PATCH 04/12] feat(remix_cli): add Fortal preset --- .github/workflows/version.yml | 5 + analysis_options.yaml | 15 +- docs/open-code.mdx | 100 +- open_code/CLEAN_SHEET.md | 8 +- open_code/PRESETS.md | 393 +++ open_code/README.md | 39 +- .../fortal_fixture/analysis_options.yaml | 21 + open_code/fortal_fixture/lib/main.dart | 35 + open_code/fortal_fixture/pubspec.yaml | 20 + .../fortal_fixture/test/open_code_test.dart | 317 +++ .../components/slider/slider_widget_test.dart | 1 - packages/remix_cli/CHANGELOG.md | 4 + packages/remix_cli/README.md | 30 +- packages/remix_cli/lib/src/registry.dart | 2 +- .../lib/src/registry/fortal/registry.yaml | 599 ++++ .../templates/accordion/accordion.dart.tmpl | 166 ++ .../fortal/templates/avatar/avatar.dart.tmpl | 127 + .../fortal/templates/badge/badge.dart.tmpl | 108 + .../base_button/base_button.dart.tmpl | 547 ++++ .../fortal/templates/button/button.dart.tmpl | 137 + .../templates/callout/callout.dart.tmpl | 86 + .../fortal/templates/card/card.dart.tmpl | 223 ++ .../fortal/templates/chart/chart.dart.tmpl | 255 ++ .../templates/checkbox/checkbox.dart.tmpl | 258 ++ .../fortal/templates/code/code.dart.tmpl | 300 ++ .../templates/data_list/data_list.dart.tmpl | 52 + .../templates/data_table/data_table.dart.tmpl | 173 ++ .../fortal/templates/dialog/dialog.dart.tmpl | 96 + .../templates/disclosure/disclosure.dart.tmpl | 194 ++ .../templates/divider/divider.dart.tmpl | 49 + .../templates/heading/heading.dart.tmpl | 116 + .../icon_button/icon_button.dart.tmpl | 148 + .../fortal/templates/icons/icons.dart.tmpl | 17 + .../fortal/templates/kbd/kbd.dart.tmpl | 199 ++ .../fortal/templates/link/link.dart.tmpl | 244 ++ .../fortal/templates/menu/menu.dart.tmpl | 224 ++ .../templates/popover/popover.dart.tmpl | 46 + .../templates/progress/progress.dart.tmpl | 131 + .../fortal/templates/radio/radio.dart.tmpl | 261 ++ .../segmented_control.dart.tmpl | 236 ++ .../fortal/templates/select/select.dart.tmpl | 319 +++ .../templates/sidebar/sidebar.dart.tmpl | 81 + .../templates/skeleton/skeleton.dart.tmpl | 27 + .../fortal/templates/slider/slider.dart.tmpl | 350 +++ .../templates/spinner/spinner.dart.tmpl | 31 + .../fortal/templates/switch/switch.dart.tmpl | 354 +++ .../fortal/templates/tabs/tabs.dart.tmpl | 127 + .../fortal/templates/text/text.dart.tmpl | 48 + .../templates/textfield/textfield.dart.tmpl | 388 +++ .../fortal/templates/theme/computed.dart.tmpl | 562 ++++ .../templates/theme/control_styles.dart.tmpl | 77 + .../templates/theme/radix_colors.dart.tmpl | 2490 +++++++++++++++++ .../templates/theme/surface_frame.dart.tmpl | 31 + .../fortal/templates/theme/theme.dart.tmpl | 14 + .../templates/theme/theme_data.dart.tmpl | 1090 ++++++++ .../templates/theme/theme_scope.dart.tmpl | 188 ++ .../fortal/templates/theme/tokens.dart.tmpl | 943 +++++++ .../fortal/templates/toggle/toggle.dart.tmpl | 136 + .../toggle_group/toggle_group.dart.tmpl | 121 + .../templates/tooltip/tooltip.dart.tmpl | 24 + .../templates/typography/typography.dart.tmpl | 82 + .../remix_cli/test/init_command_test.dart | 30 + packages/remix_cli/test/installer_test.dart | 8 +- .../remix_cli/test/project_config_test.dart | 2 +- packages/remix_cli/test/registry_test.dart | 34 +- .../lib/src/components/chart.dart | 16 +- .../remix_fortal/lib/src/theme/computed.dart | 8 +- .../remix_fortal/lib/src/theme/theme.dart | 2 +- .../lib/src/theme/theme_data.dart | 10 +- .../lib/src/theme/theme_scope.dart | 2 +- pubspec.yaml | 11 + test/tool/build_fortal_preset_test.dart | 242 ++ test/tool/check_open_code_test.dart | 15 + tool/build_fortal_preset.dart | 544 ++++ tool/check_open_code.dart | 337 ++- tool/validate_docs.dart | 39 +- 76 files changed, 14608 insertions(+), 157 deletions(-) create mode 100644 open_code/PRESETS.md create mode 100644 open_code/fortal_fixture/analysis_options.yaml create mode 100644 open_code/fortal_fixture/lib/main.dart create mode 100644 open_code/fortal_fixture/pubspec.yaml create mode 100644 open_code/fortal_fixture/test/open_code_test.dart create mode 100644 packages/remix_cli/lib/src/registry/fortal/registry.yaml create mode 100644 packages/remix_cli/lib/src/registry/fortal/templates/accordion/accordion.dart.tmpl create mode 100644 packages/remix_cli/lib/src/registry/fortal/templates/avatar/avatar.dart.tmpl create mode 100644 packages/remix_cli/lib/src/registry/fortal/templates/badge/badge.dart.tmpl create mode 100644 packages/remix_cli/lib/src/registry/fortal/templates/base_button/base_button.dart.tmpl create mode 100644 packages/remix_cli/lib/src/registry/fortal/templates/button/button.dart.tmpl create mode 100644 packages/remix_cli/lib/src/registry/fortal/templates/callout/callout.dart.tmpl create mode 100644 packages/remix_cli/lib/src/registry/fortal/templates/card/card.dart.tmpl create mode 100644 packages/remix_cli/lib/src/registry/fortal/templates/chart/chart.dart.tmpl create mode 100644 packages/remix_cli/lib/src/registry/fortal/templates/checkbox/checkbox.dart.tmpl create mode 100644 packages/remix_cli/lib/src/registry/fortal/templates/code/code.dart.tmpl create mode 100644 packages/remix_cli/lib/src/registry/fortal/templates/data_list/data_list.dart.tmpl create mode 100644 packages/remix_cli/lib/src/registry/fortal/templates/data_table/data_table.dart.tmpl create mode 100644 packages/remix_cli/lib/src/registry/fortal/templates/dialog/dialog.dart.tmpl create mode 100644 packages/remix_cli/lib/src/registry/fortal/templates/disclosure/disclosure.dart.tmpl create mode 100644 packages/remix_cli/lib/src/registry/fortal/templates/divider/divider.dart.tmpl create mode 100644 packages/remix_cli/lib/src/registry/fortal/templates/heading/heading.dart.tmpl create mode 100644 packages/remix_cli/lib/src/registry/fortal/templates/icon_button/icon_button.dart.tmpl create mode 100644 packages/remix_cli/lib/src/registry/fortal/templates/icons/icons.dart.tmpl create mode 100644 packages/remix_cli/lib/src/registry/fortal/templates/kbd/kbd.dart.tmpl create mode 100644 packages/remix_cli/lib/src/registry/fortal/templates/link/link.dart.tmpl create mode 100644 packages/remix_cli/lib/src/registry/fortal/templates/menu/menu.dart.tmpl create mode 100644 packages/remix_cli/lib/src/registry/fortal/templates/popover/popover.dart.tmpl create mode 100644 packages/remix_cli/lib/src/registry/fortal/templates/progress/progress.dart.tmpl create mode 100644 packages/remix_cli/lib/src/registry/fortal/templates/radio/radio.dart.tmpl create mode 100644 packages/remix_cli/lib/src/registry/fortal/templates/segmented_control/segmented_control.dart.tmpl create mode 100644 packages/remix_cli/lib/src/registry/fortal/templates/select/select.dart.tmpl create mode 100644 packages/remix_cli/lib/src/registry/fortal/templates/sidebar/sidebar.dart.tmpl create mode 100644 packages/remix_cli/lib/src/registry/fortal/templates/skeleton/skeleton.dart.tmpl create mode 100644 packages/remix_cli/lib/src/registry/fortal/templates/slider/slider.dart.tmpl create mode 100644 packages/remix_cli/lib/src/registry/fortal/templates/spinner/spinner.dart.tmpl create mode 100644 packages/remix_cli/lib/src/registry/fortal/templates/switch/switch.dart.tmpl create mode 100644 packages/remix_cli/lib/src/registry/fortal/templates/tabs/tabs.dart.tmpl create mode 100644 packages/remix_cli/lib/src/registry/fortal/templates/text/text.dart.tmpl create mode 100644 packages/remix_cli/lib/src/registry/fortal/templates/textfield/textfield.dart.tmpl create mode 100644 packages/remix_cli/lib/src/registry/fortal/templates/theme/computed.dart.tmpl create mode 100644 packages/remix_cli/lib/src/registry/fortal/templates/theme/control_styles.dart.tmpl create mode 100644 packages/remix_cli/lib/src/registry/fortal/templates/theme/radix_colors.dart.tmpl create mode 100644 packages/remix_cli/lib/src/registry/fortal/templates/theme/surface_frame.dart.tmpl create mode 100644 packages/remix_cli/lib/src/registry/fortal/templates/theme/theme.dart.tmpl create mode 100644 packages/remix_cli/lib/src/registry/fortal/templates/theme/theme_data.dart.tmpl create mode 100644 packages/remix_cli/lib/src/registry/fortal/templates/theme/theme_scope.dart.tmpl create mode 100644 packages/remix_cli/lib/src/registry/fortal/templates/theme/tokens.dart.tmpl create mode 100644 packages/remix_cli/lib/src/registry/fortal/templates/toggle/toggle.dart.tmpl create mode 100644 packages/remix_cli/lib/src/registry/fortal/templates/toggle_group/toggle_group.dart.tmpl create mode 100644 packages/remix_cli/lib/src/registry/fortal/templates/tooltip/tooltip.dart.tmpl create mode 100644 packages/remix_cli/lib/src/registry/fortal/templates/typography/typography.dart.tmpl create mode 100644 test/tool/build_fortal_preset_test.dart create mode 100644 tool/build_fortal_preset.dart diff --git a/.github/workflows/version.yml b/.github/workflows/version.yml index 9cc80d73a..7cf9a7f24 100644 --- a/.github/workflows/version.yml +++ b/.github/workflows/version.yml @@ -113,6 +113,11 @@ jobs: # step is part of versioning, not a cleanup after it. dart run tool/sync_registry_remix.dart + # The Fortal preset copies its dependency floors from the default + # registry. Rebuild it immediately after the default floor moves so + # the two committed registry trees cannot disagree. + dart run tool/build_fortal_preset.dart + # The pair must land aligned; verify rather than trust the inputs. # This also fails when the sync step above did not run. dart run tool/check_version_alignment.dart diff --git a/analysis_options.yaml b/analysis_options.yaml index 2a999a25a..445671177 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -4,13 +4,13 @@ # `analysis_options.yaml`, and this file must not shadow them. It exists for # one reason. # -# `open_code/fixture/` is a standalone consumer application, not a workspace -# member (see open_code/README.md). It resolves its dependencies from pub.dev -# inside the throwaway app that `tool/check_open_code.dart` assembles, so it -# has no `.dart_tool/package_config.json` here and its `package:` imports -# cannot resolve in this checkout. Its own pubspec makes it a nested analysis -# context, so a root-level `dart analyze` would descend into it and report -# every import as missing. +# The `open_code/*fixture/` directories are standalone consumer applications, +# not workspace members (see open_code/README.md). They resolve dependencies +# from pub.dev inside the throwaway apps that `tool/check_open_code.dart` +# assembles, so they have no `.dart_tool/package_config.json` here and their +# `package:` imports cannot resolve in this checkout. Their own pubspecs make +# them nested analysis contexts, so a root-level `dart analyze` would descend +# into them and report every import as missing. # # Excluding it does not weaken coverage: the checker runs `flutter analyze` # over that exact source in the assembled app, where its dependencies are real. @@ -20,3 +20,4 @@ analyzer: exclude: - open_code/fixture/** + - open_code/fortal_fixture/** diff --git a/docs/open-code.mdx b/docs/open-code.mdx index 719d1b4dc..70bb6e411 100644 --- a/docs/open-code.mdx +++ b/docs/open-code.mdx @@ -3,11 +3,11 @@ title: Open Code description: Install editable Remix component source into your app with the remix CLI --- -Fortal gives you a themed catalog you *depend on*. **Open code** gives you the -opposite trade: `remix_cli` copies a small, readable theme and one file per -component **into your application**, and from that moment your team owns those -files. No wrapper, no subclass, no theme-override escape hatch — you edit the -source. +**Open code** copies a theme and component recipes **into your application**. +From that moment your team owns those files: no wrapper, no subclass, and no +package-level theme override standing between you and the source. Choose the +small `default` preset or the full Radix Themes-inspired `fortal` preset when +you initialize the project. Remix keeps everything that is genuinely hard: rendering, pointer and keyboard handling, focus management, accessibility semantics, and the loading/disabled @@ -17,19 +17,22 @@ interaction rules. The installed file only decides what things look like. `remix_cli` has not been published. The hosted installation commands below apply after its first release. Until then, use the checkout installation described in the CLI README. The CLI has its own version and release schedule. +Fortal preset does not add a `remix_fortal` dependency. -## When to choose open code +## Choose a preset -| | Fortal | Open code | +| | `default` | `fortal` | |---|---|---| -| You get | A dependency | Source files in `lib/` | -| Restyling | Token and style overrides | Edit the recipe | -| Upgrades | `flutter pub upgrade` | You own it; rerun `add --diff` to compare | -| Best for | Ship fast on a Radix-like system | A house design system you intend to diverge | +| Design language | Compact neutral starting point | Radix Themes 3.3.0 parity surface | +| Theme | 15 semantic tokens | 277 tokens, Radix color scales, scaling, radius, and panel modes | +| Component axes | Small default vocabulary | Fortal's `classic`, `solid`, `soft`, `surface`, `outline`, and `ghost` vocabulary where applicable | +| Best for | A house system you intend to shape | Starting from a complete Radix-like system | -Pick open code when the design system is *yours* and you expect to keep -changing it. Pick Fortal when you want a good catalog and no maintenance. +Both choices install local Dart under `lib/`, use Remix for behavior, and use +the same `add --diff` / `add --overwrite` review workflow. They cannot be mixed +inside one initialized source tree because their tokens and variant +vocabularies are different. ## Install @@ -83,7 +86,7 @@ Run "remix help " for more information about a command. ## 1. Initialize ```bash -remix init +remix init --preset default ``` This writes two files and nothing else: @@ -104,13 +107,14 @@ Choose the naming and location up front, because the prefix is baked into every identifier the CLI renders: ```bash -remix init --prefix Acme --preset default --ui-path lib/design_system +remix init --prefix Acme --preset fortal --ui-path lib/design_system ``` -`--prefix Acme` produces `AcmeButton`, `AcmeButtonVariant`, `AcmeThemeData`, -and `acmeButtonStyle`. The prefix must be an UpperCamel Dart identifier whose +`--prefix Acme` produces identifiers such as `AcmeButton` and +`acmeButtonStyle`. The prefix must be an UpperCamel Dart identifier whose lowercased form is not a reserved word, and the UI path must live under `lib/`. The prefixes `Remix` and `Mix` are reserved for runtime dependencies. +The preset defaults to `default` when omitted. `init` is also a repair command. Run it again and it recreates whichever of the two files is missing, telling you exactly which one it touched: @@ -204,7 +208,7 @@ paths: |---|---|---| | `schema` | `2` | New files use schema 2. Schema 1 remains readable as `preset: default`; anything else is rejected. | | `prefix` | `Ui` | ASCII UpperCamel Dart identifier. Its lowercased form must not be a Dart reserved word. `Remix` and `Mix` are reserved for runtime dependencies. | -| `preset` | `default` | Bundled registry selected for every later `add`. It must be chosen at `init`; this release bundles `default`. | +| `preset` | `default` | Bundled registry selected for every later `add`. Choose `default` or `fortal` at `init`. | | `paths.ui` | `lib/ui` | Project-relative, normalized, no traversal, must be under `lib/`. Symlinks that escape the package root are rejected. | Unknown keys are an error rather than a warning, so a typo fails loudly instead @@ -298,6 +302,51 @@ ButtonStyler _variantStyle(UiButtonVariant variant) => switch (variant) { constructor per variant (`UiButton.primary`, `.secondary`, `.outline`, `.ghost`, `.destructive`). +## The Fortal preset + +Initialize Fortal once, then use the same item commands and ownership workflow: + +```bash +remix init --prefix Acme --preset fortal +remix add button +``` + +The application receives `AcmeScope`, `AcmeTokens`, `AcmeButton`, and +`acmeButtonStyle`; no installed identifier is named Fortal. Button brings its +shared `base_button` recipe, while the theme arrives as a complete local layer: + +```text +lib/ui/ +├── ui.dart +├── theme/ +│ ├── theme.dart +│ ├── radix_colors.dart +│ ├── computed.dart +│ ├── control_styles.dart +│ ├── tokens.dart +│ ├── theme_data.dart +│ ├── theme_scope.dart +│ └── surface_frame.dart +└── components/ + ├── base_button.dart + ├── button.dart + └── button.g.dart +``` + +These are application files, not forwarding wrappers around +`package:remix_fortal`. The authored recipes and pinned Radix Themes 3.3.0 +color data are copied from the same source used by the repository's parity +suite. `remix` remains the behavior and styling-engine boundary; +`mix_annotations`, `build_runner`, and `mix_generator` support adapter +generation. `icons` additionally installs `remix_ui_icons`, and `chart` +installs `mix_chart`. The preset adds no direct `mix`, `naked_ui`, or +`remix_fortal` dependency. + +The source is deliberately editable. Change the token map, add an accent or +variant, or merge a one-instance styler last. Future CLI versions never merge +over those decisions automatically: inspect `remix add --diff`, then +use `--overwrite` only when you want the new authored file wholesale. + ## The three levels of customization **Edit the recipe** — change it for the whole app, permanently. It is your file. @@ -454,7 +503,7 @@ No source was written. Declaring the same package in both sections is rejected the same way. Fix your manifest and rerun — the CLI will not edit it for you. -## Catalog +## Default preset catalog The bundled registry is deliberately small, and every item after `theme` depends on it: @@ -502,6 +551,13 @@ and page-size control *are* those components — adding it installs all four. | `toggle_group` | `UiToggleGroup` | variant, size | | `tooltip` | `UiTooltip` | — | +The `fortal` catalog contains every item above and adds `base_button`, `code`, +`heading`, `kbd`, `sidebar`, `text`, and `typography`. `base_button` and +`typography` are shared authored recipes that arrive transitively with the +components that use them. Fortal's component names use the configured prefix, +so an Acme installation exposes `AcmeSidebar`, `AcmeHeading`, and +`AcmeText` alongside its generated controls. + One `add` installs one item. The names above use the default `Ui` prefix; `--prefix Acme` gives you `AcmeButton` and friends. An axis is an enum the recipe declares, and a recipe whose axis is named `variant` also gets one @@ -512,10 +568,10 @@ A parameter is not an axis. `UiDivider` takes an `orientation`, but it is a plain Flutter `Axis` the recipe accepts rather than an enum of its own, so it generates no named constructors. -### Charts outside Fortal +### Charts in the default preset -`chart` is an optional extension, not a Fortal preset and not a wrapper around -Fortal: +The default preset's `chart` is an optional extension with its own compact +recipe: ```bash remix add chart diff --git a/open_code/CLEAN_SHEET.md b/open_code/CLEAN_SHEET.md index 36a18c78c..45d752373 100644 --- a/open_code/CLEAN_SHEET.md +++ b/open_code/CLEAN_SHEET.md @@ -127,9 +127,11 @@ The recurring costs are visible: ## What remains outside this MVP -Fortal stays unchanged. There is no general component marketplace, remote -registry, automatic migration, source lockfile, or public-release automation -for `remix_cli`. +**Since 2026-09-04:** Fortal is available as a second application-owned preset. +Its separate source tree, derivation contract, and publication transition are +recorded in [Presets: Fortal as application-owned source](PRESETS.md). There is +still no general component marketplace, remote registry, automatic migration, +source lockfile, or public-release automation for `remix_cli`. The first public release has a separate gate: confirm the pub.dev package and publisher, add an isolated tag contract, verify bundled assets from a staged diff --git a/open_code/PRESETS.md b/open_code/PRESETS.md new file mode 100644 index 000000000..6bbe1dd56 --- /dev/null +++ b/open_code/PRESETS.md @@ -0,0 +1,393 @@ +# Presets: Fortal as application-owned source + +**Status:** Implemented +**Base branch:** `feat/open-code` + +## The problem + +`remix_cli` installs one registry: a 15-token `theme` item and one authored +recipe per component. Fortal is a second, complete design language on Remix, +with 277 tokens, Radix color data, 35 recipes, and a parity contract against +Radix Themes 3.3.0. Today an application can adopt Fortal only as a hosted +package. It cannot own the source the way it owns the default recipes. + +This document designs the change that lets an application run +`remix init --preset fortal` and then `remix add button`, and receive editable +Fortal source under its own prefix. It is organized as a stack of pull +requests against `feat/open-code`. Each pull request is green on its own. + +## Decisions + +These were reached in review before this document. Each one is stated once, +with the reason, so a reviewer does not reopen it by accident. + +1. **A preset is a registry tree, chosen once at `init`.** A Fortal button + resolves Fortal tokens and a default button resolves default tokens, so the + two cannot mix in one application. The choice is recorded in `remix.yaml`, + next to the prefix, and follows the same rule as the prefix: it cannot + change after `init`. This is also how shadcn treats `style` in + `components.json`. +2. **One recipe tree per preset.** shadcn v4 shares one component source across + styles because every style uses one variant vocabulary. Fortal's vocabulary + (`classic`, `solid`, `soft`, `surface`, `outline`, `ghost`) is not the + default's (`primary`, `secondary`, `outline`, `ghost`, `destructive`), so + a shared recipe with swappable data is not possible between these two. +3. **Fortal is authored once, as Dart, in `packages/remix_fortal`.** The + `remix_cli` templates are derived from that source and committed. A check + fails when they drift. The alternative, hand-authored `.tmpl` files, means + editing 36 files without an analyzer, and running the 50 test files and + the parity checker through a temporary-app render on every change. shadcn + moved from hand-authored per-style trees to authored source plus a build + for the same reason. +4. **The Radix color table installs as source.** It is one generated file + pinned to Radix Themes 3.3.0 that regenerates only on a reviewed pin move. + A package for data that never changes buys nothing, and the pub.dev name + `radix_colors` is already taken. Installing it lets an application add a + custom 12-step scale, which the closed accent enum forbids today. +5. **Nothing installed is named Fortal.** The word appears in the registry + directory name and in `remix.yaml`. The application gets `AcmeScope`, + `AcmeTokens`, `AcmeButton`, and token ids such as `acme.accent.9`. +6. **`mix_chart` stays a hosted dependency of the `chart` item.** It is the + chart engine the recipe styles, as `remix` is the button engine. The + default `chart` item already declares it. +7. **`remix_fortal` leaves pub.dev once the preset works; the directory + stays.** Hosted Fortal has no consumer base to protect: 0 likes and 438 + downloads at `1.0.0-beta.7`. pub.dev cannot delete a package, so the + package is marked discontinued with `remix init --preset fortal` named as + the replacement, after PR 4 proves that path. The directory + `packages/remix_fortal` is not deleted. It is the authored source the + templates derive from, the target of 50 test files and the parity + checker, and a dependency of `apps/dashboard`, `apps/demo`, and + `apps/playground`. Drift between it and the derived templates is not a + risk: `--check` fails CI on any byte of difference, the same way + `docs/fortal/catalog.mdx` and the playground dogfood are checked today. + +## What the application gets + +```shell +dart run remix_cli:remix init --prefix Acme --preset fortal +dart run remix_cli:remix add button +``` + +```text +lib/ui/ + ui.dart + theme/ + theme.dart barrel of the files below, minus radix_colors; exported from ui.dart + radix_colors.dart generated Radix 3.3.0 scales; not exported from ui.dart + computed.dart role math, shadows, token resolution + control_styles.dart shared focus and inset-surface helpers + tokens.dart AcmeTokens + theme_data.dart AcmeThemeConfig, AcmeThemeData, the token map + theme_scope.dart AcmeScope, AcmeTheme + surface_frame.dart + components/ + base_button.dart shared Button and IconButton metrics and states + button.dart authored; edit this + button.g.dart generated; AcmeButton, AcmeButton.solid, ... +``` + +Hosted dependencies after install: `remix` and `mix_annotations`, with +`build_runner` and `mix_generator` for generation. `remix_ui_icons` arrives +only with `icons`. `mix_chart` arrives only with `chart`. No Fortal package. + +The application then edits the same three places the default preset offers: +theme values in `theme_data.dart`, a recipe in `components/`, and a per-widget +`style` argument that merges last. + +## The registry after this stack + +```text +packages/remix_cli/lib/src/registry/ + default/ + registry.yaml moved, unchanged + templates//... moved, unchanged + fortal/ + registry.yaml derived + templates/theme/*.dart.tmpl derived + templates//.dart.tmpl +``` + +`remix.yaml` gains one key at schema 2: + +```yaml +schema: 2 +prefix: Acme +preset: fortal +paths: + ui: lib/ui +``` + +A schema 1 file has no `preset` key and reads as `preset: default`, so +`apps/playground/remix.yaml` and every existing consumer keep working. + +## The Fortal source after this stack + +The package layout mirrors the installed layout. This is a hard requirement, +not a nicety: derivation replaces every `Fortal` and `fortal` in a file, so a +relative import such as `'../fortal/fortal.dart'` would break. After the move, +no path segment under `lib/src` contains the word. + +| Today | After | +|---|---| +| `src/fortal/fortal_theme.dart` (2217 lines) | `src/theme/tokens.dart`, `src/theme/theme_data.dart`, `src/theme/theme_scope.dart`, `src/theme/control_styles.dart` | +| `src/fortal/computed.dart` | `src/theme/computed.dart` | +| `src/fortal/surface_frame.dart` | `src/theme/surface_frame.dart` | +| `src/radix/colors/colors.dart` + `colors_generated.dart` | `src/theme/radix_colors.dart`, one file, written by `scripts/generate_radix_colors.dart` | +| `src/fortal/base_button_recipe.dart` + `base_button_state_styles.dart` | `src/components/base_button.dart` | +| `src/recipes/typography_shared.dart` | `src/components/typography.dart` | +| `src/recipes/.dart` | `src/components/.dart` | +| `src/radix/icons.dart` | `src/icons.dart` | +| `src/fortal/fortal.dart` (internal barrel) | `src/theme/theme.dart`; exports every theme file except `radix_colors.dart` | + +The internal barrel survives on purpose. All 36 files under `recipes/` +import it today, so the move is one path change per file instead of a +36-file audit of which theme file each one uses. No recipe references a Radix color type, so the barrel +does not export `radix_colors.dart`; `computed.dart` and `theme_data.dart` +import that file directly. + +The public barrel `lib/remix_fortal.dart` keeps every export, so the public +API does not change. `test/public_api_test.dart` proves it. Remix's own public +barrel also re-exports the six Naked constructor/state types referenced by +generated adapters, keeping installed recipes on the `package:remix/remix.dart` +boundary. + +## Derivation + +`tool/build_fortal_preset.dart` at the workspace root, next to +`check_open_code.dart`, does the following. + +**Input.** Every `.dart` file under `packages/remix_fortal/lib/src` except +`*.g.dart`. The application generates its own parts. + +**Refusals, checked first.** Any path segment containing `fortal`. Any file +containing `{{`, because the renderer treats that as a template token. Any +`package:remix_fortal`, `package:mix`, or `package:naked_ui` import, because +installed source must import the public `remix` surface. Each refusal names +the file. + +**Substitution.** Plain `replaceAll` of `Fortal` with `{{typePrefix}}` and +`fortal` with `{{valuePrefix}}`. This is the exact inverse of +`TemplateRenderer.render`, so rendering with `Fortal` and `fortal` reproduces +the source byte for byte. That round trip is the drift test. Token id strings +follow the same rule and become `acme.accent.9` in the application. `Radix*` +identifiers do not contain the word and pass through. + +**Placement.** `theme/*.dart` goes to `templates/theme/`. `components/x.dart` +goes to `templates/x/x.dart.tmpl`. `icons.dart` is skipped: no Fortal recipe +references an icon, and an icon alias set is not part of a design language, +so the Fortal `icons` item is a copy of the default `icons` template. Both +presets install the same file. + +**Registry.** The tool also writes `registry/fortal/registry.yaml`, so the +item graph is correct by construction: + +- `registryDependencies` come from relative imports. Any `'../theme/...'` + import means `theme`, so the theme item is one node no matter which theme + file a recipe names. A sibling import such as `'checkbox.dart'` means + `checkbox`. Today those sibling imports are exactly: `data_table` to + `checkbox`, `icon_button`, and `select` (and transitively `base_button` + through `icon_button`); `sidebar` to `text` and `toggle`; + `text`, `code`, `heading`, `kbd`, and `link` to `typography`; `button` and + `icon_button` to `base_button`. +- `generated` is present when the file has a `part 'x.g.dart';` line. +- `dependencies` and `devDependencies` follow three rules, with every + constraint read from the default registry so the version floors live in + one place. `theme` declares `remix`. An item with a generated part declares + `mix_annotations`, `build_runner`, and `mix_generator`, as the default + `button` does. An item that imports `package:mix_chart` or + `package:remix_ui_icons` declares that package too. `base_button` and + `typography` have no part and no `@MixWidget`, so they declare nothing. +- `exports` lists `theme/theme.dart` for the theme item and the recipe file + for every other item. `theme/radix_colors.dart` is never exported: it + declares bare top-level names such as `gray` and `red` that must not enter + `ui.dart`. `theme/computed.dart` is exported through the barrel; its names + are prefixed, and `chart.dart` calls `resolveFortalTokens` from it. + +**Modes.** No flag writes. `--check` derives in memory, compares with the +committed files, and fails on any difference. `melos run open-code:fortal:check` +runs it in CI, the way `docs:check` runs `generate_fortal_catalog.dart`. + +## The stack + +Five pull requests. The first two touch only `remix_fortal`. The third +touches only `remix_cli` and the repository tools, and can be reviewed in +parallel with the first two. The fourth depends on all three. The fifth is a +product step with an external side effect, so it stays separate and last. + +Two earlier drafts were folded in. The fresh-app proof is part of the PR +that ships the Fortal preset, because a preset without its proof is not +done. Docs land with the PR they describe, because `validate_docs.dart` +already checks them and a flag without docs is not done either. + +### PR 1: `refactor(remix_fortal)`: lay the source out like an installed preset + +A pure move. Git sees renames, reviewers see paths, and no behavior changes. +This goes first so every later content diff lands on the final paths. + +- Apply the file table above. Split `fortal_theme.dart` into three files: + the `FortalTokens` class; the token-map builder with `FortalThemeConfig` + and `FortalThemeData`; and `FortalScope` with `FortalTheme`. The + declarations are not contiguous today, `FortalScope` sits between the + builder and the data classes, so this is an extraction by declaration, not + a slice by line range. Dart does not care about declaration order. +- Merge the color pair into `theme/radix_colors.dart`. Point + `scripts/generate_radix_colors.dart` at the new path. Keep the file header + and the integrity hash. +- Rename the internal barrel to `theme/theme.dart` and repoint every one of + the 36 files under `recipes/` that import it. Drop `radix_colors.dart` from + its exports. +- `tool/generate_fortal_catalog.dart` hardcodes `lib/src/recipes/.dart` + in `_recipePath` and opens those files to render `docs/fortal/catalog.mdx`. + Point it at `components/`. This tool feeds `docs:check`, which the `ci` + aggregate runs, so leaving it breaks CI on the move. +- The component docs link to recipe source on GitHub by path, for example + `docs/components/tooltip.mdx:126`. Twenty-five doc files carry such a + link. Rewrite every `lib/src/recipes/` link + to `lib/src/components/`. `validate_docs.dart` does not check these links, + so grep for the old path before merging. +- Update the paths in `scripts/README.md`. +- Proof: `test/public_api_test.dart` and `public_api_compatibility_test.dart` + unchanged and green; `melos run fortal:parity:check` green; `melos run + docs:check` green; the color fixture test still reads every scale; no + file in the repository outside `.context/` contains `src/recipes/` or + `src/fortal/`. + +### PR 2: `feat(remix_fortal)`: instance style override, and only `remix` imports + +Content edits on the final paths. Every default component template takes a +styler parameter merged last. No Fortal recipe does. + +- Add `Styler style = const Styler.create()` as the last + parameter of each of the 35 recipes, and `.merge(style)` as the last call. + Mirror `packages/remix_cli/lib/src/registry/templates/button/button.dart.tmpl`. +- Twelve recipes already declare a local named `style`: `button`, `chart`, + `code`, `divider`, `heading`, `icon_button`, `kbd`, `link`, `select`, + `text`, `textfield`, `toggle`. Rename those locals first. +- Remove the four unused `naked_ui` imports (`accordion`, `disclosure`, + `slider`, `tabs`; verified unused with the analyzer) and the four direct + `mix` imports (`chart`, `base_button`, `computed`, and the theme files). + `remix` re-exports `mix` in full. This is a precondition for PR 4, whose + tool refuses those imports. +- Regenerate the `.g.dart` parts. The generator adds `style` to every named + constructor. +- Test: for Button, Checkbox, and TextField, a `style` that sets one idle + property wins over the recipe, and a hover fragment in `style` wins over + the recipe's hover fragment. Extend `fortal_control_matrix_test.dart` or add + a sibling. +- Proof: `fortal:parity:check` and the package test suite. + +### PR 3: `feat(remix_cli)`: preset axis + +The CLI learns that more than one registry exists. Only `default` ships in +this PR, so behavior for every existing project is unchanged. This is +independent of PR 1 and PR 2. + +- Move `lib/src/registry/registry.yaml` and `templates/` to + `lib/src/registry/default/`. +- `registry.dart`: a `bundledPresets` constant listing `default`; + `loadBundled({required String preset})` resolves + `package:remix_cli/src/registry//registry.yaml`; an unknown preset + fails before any file read with a message that lists the bundled presets; + `RegistryCatalog.preset` for messages; the unknown-item error names the + preset. +- `project_config.dart`: `preset` field; schema 2 requires the key; schema 1 + reads as `default`; `encode()` writes schema 2; preset must match + `^[a-z][a-z0-9_]*$` and be in `bundledPresets`. +- `cli.dart`: `init --preset `, default `default`. +- `installer.dart`: `RegistryLoader` becomes + `Future Function(String preset)`; `add` passes + `config.preset`; the `init` mismatch error covers prefix, preset, and UI + path. +- Repository tools that hardcode the old path: `tool/check_open_code.dart`, + `tool/check_open_code_dogfood.dart`, `tool/sync_registry_remix.dart`, + `tool/check_version_alignment.dart`, `tool/validate_docs.dart`, and + `test/tool/check_open_code_test.dart`. Point them at `registry/default/`. + `validate_docs.dart` matters most: it feeds `docs:check`, which the `ci` + aggregate runs, so missing it breaks CI on the move. +- Tests: schema 1 parses as default; schema 2 without `preset` fails; bad + preset name fails; `init --preset default` writes schema 2; `init` with a + different preset on an existing project fails; `loadBundled` with an + unknown preset fails with the list; the installer passes the configured + preset to the loader; `--diff` still compares against the default tree. +- Docs: the configuration section of `docs/open-code.mdx` shows the schema 2 + file and the `--preset` flag, and drops the words "no presets". + `packages/remix_cli/CHANGELOG.md`. + +### PR 4: `feat(remix_cli)`: the Fortal preset, with its proof + +Depends on PR 1, 2, and 3. The diff is mostly derived content; review the +tool, the registry rules, and the check. + +- `tool/build_fortal_preset.dart` as specified above, and + `test/tool/build_fortal_preset_test.dart` covering the refusals, the round + trip, the dependency inference, and `--check` on a planted drift. +- The committed output: `registry/fortal/registry.yaml` and every template. +- `bundledPresets` gains `fortal`. The `remix_cli` unit tests load the + Fortal catalog, resolve `button` to `theme`, `base_button`, `button`, + resolve `data_table` to its four dependencies, and rely on + `_validateGraphAndTargets` for cycles and double-owned targets. +- Melos scripts `open-code:fortal:build` and `open-code:fortal:check`, named + like the existing `open-code:check`. The check joins the `ci` aggregate + next to `docs:check`. `.github/workflows/version.yml` runs + `sync_registry_remix.dart` as a shell step after `melos version`; add the + build right after it, because the Fortal registry copies its floors from + the default registry. +- `tool/check_open_code.dart --preset fortal`: install every Fortal item + into a guarded temporary Flutter app with prefix `Acme`, generate without a + consumer `build.yaml`, analyze clean, and run a smoke test from + `open_code/fortal_fixture/` that pumps every generated widget under + `AcmeScope` and asserts a few resolved token values against known Radix + 3.3.0 hex values. No committed expected `.g.dart`: the package already + commits the same adapter under the `Fortal` prefix. No equivalence test + against `FortalButton`: the byte round trip already proves the recipe is + the same code, the same generator produces both adapters, and a temporary + app cannot path-depend on a `resolution: workspace` package cleanly. +- Docs: a Fortal preset section in `docs/open-code.mdx` with the install and + the ownership boundary; `open_code/README.md`; a dated "Since" note in + `open_code/CLEAN_SHEET.md` under "What remains outside this MVP" linking + here; `packages/remix_cli/CHANGELOG.md`. + +### PR 5: `chore(remix_fortal)`: stop publishing + +- `publish_to: none` in the pubspec. Remove the pub.dev publish notes from + the pubspec comments and the package from any release automation that + versions it. +- Delete the hosted-pin check in `tool/fortal_parity/check.dart` and drop + `naked_ui` from the pubspec, which that check was the last reason to keep. +- `docs/fortal/*` install section shows `remix init --preset fortal` and + drops `flutter pub add remix_fortal`. +- After merge: mark `remix_fortal` discontinued on pub.dev with the preset + named as the replacement. + +## Risks and how each PR bounds them + +- **A `style` local collides with the new parameter.** Twelve recipes. PR 2 + renames them; the analyzer catches any miss. +- **A file or path still contains `fortal` after PR 1.** The derivation tool + refuses, and PR 4's test plants one to prove the refusal. +- **Formatter drift.** The installer formats rendered output, and wrapping + depends on prefix length, as `check_open_code_dogfood.dart` documents. The + drift check therefore compares the template to the source, never a rendered + file to a template. +- **Version floors diverge between presets.** The Fortal registry copies its + constraints from the default registry at build time, and `--check` fails + when they differ. +- **`remix.yaml` schema 2 reaches a consumer with an older CLI.** That CLI + rejects it with its existing "unsupported schema" error. The project-local + dev dependency pins the CLI, which is why the docs recommend it. + +## Out of this stack + +- `init --preset --reinstall`, a batched `add --overwrite` for switching + presets in place. shadcn does this. It is a later feature. +- Preset knobs at `init`, such as `--accent indigo --radius medium` written + into the installed theme. +- Authoring the default preset as Dart with the same derivation. The default + has no parity contract, so the pressure is lower. Reconsider after PR 4 + shows the tool's cost. +- Deleting `packages/remix_fortal` from the repository. That would make the + `.tmpl` files the only form of Fortal, move the parity suite behind a + temporary-app render, and force `apps/dashboard`, `apps/demo`, + `apps/playground`, and 39 doc pages to migrate to installed source. + Reconsider only if the apps migrate to the preset on their own. diff --git a/open_code/README.md b/open_code/README.md index 5f21cd318..824392cfc 100644 --- a/open_code/README.md +++ b/open_code/README.md @@ -4,10 +4,12 @@ This example is installed by `remix_cli`. It gives an application editable theme and component source while Remix continues to own rendering, interaction, focus, loading, disabled behavior, and accessibility. -The registry source lives in `packages/remix_cli/lib/src/registry/`. There is -no second copy under `open_code/`. The files in `open_code/fixture/` are a -fresh-app gallery, behavioral test suite, and expected generated adapter used -by `tool/check_open_code.dart`. +The two registry trees live in +`packages/remix_cli/lib/src/registry/default/` and +`packages/remix_cli/lib/src/registry/fortal/`. There is no second registry copy +under `open_code/`. `open_code/fixture/` proves the default preset, while +`open_code/fortal_fixture/` proves Fortal with every generated widget and +known Radix Themes 3.3.0 color values. ## Install the CLI in a project @@ -19,7 +21,7 @@ the CLI version and its bundled templates: ```shell flutter pub add dev:remix_cli -dart run remix_cli:remix init --prefix Acme +dart run remix_cli:remix init --prefix Acme --preset default dart run remix_cli:remix add button ``` @@ -30,6 +32,9 @@ the optional `chart` extension, plus `accordion`, `avatar`, `badge`, `button`, `radio`, `segmented_control`, `select`, `skeleton`, `slider`, `spinner`, `switch`, `tabs`, `textfield`, `toggle`, `toggle_group`, and `tooltip`. +The `fortal` preset contains that complete surface plus `base_button`, `code`, +`heading`, `kbd`, `sidebar`, `text`, and `typography`. + `chart` is the one optional extension outside the core Remix component surface. It builds directly on `mix_chart`, installs no Fortal code, and generates `AcmeLineChart`, `AcmeBarChart`, and `AcmePieChart` from one editable @@ -54,7 +59,28 @@ installed adapters inside them. `init` creates `remix.yaml` and a managed barrel at `lib/ui/ui.dart`. The default prefix is `Ui`; `--prefix Acme` produces names such as `AcmeThemeScope`, `AcmeButton`, and `acmeButtonStyle`. A different source root -can be chosen once with `--ui-path`. +can be chosen once with `--ui-path`. The preset also is chosen once: existing +projects default to `default`, and changing a configured preset is refused. + +## Fortal as application-owned source + +```shell +dart run remix_cli:remix init --prefix Acme --preset fortal +dart run remix_cli:remix add button +``` + +This installs the full Fortal theme and recipe as prefixed local source: +`AcmeScope`, `AcmeTokens`, `AcmeButton`, and `acmeButtonStyle`. It also installs +the shared `base_button.dart` implementation used by Button and IconButton. +No installed identifier is named Fortal, and the consumer does not declare +direct `mix`, `naked_ui`, or `remix_fortal` dependencies. `remix` owns behavior; +the application owns the copied Radix color table, 277-token theme, component +recipes, and generated adapters. + +The Fortal templates are derived from analyzed Dart in +`packages/remix_fortal/lib/src/`; do not edit the committed `.tmpl` files by +hand. `tool/build_fortal_preset.dart --check` makes source/template drift a CI +failure. The prefixes `Remix` and `Mix` are reserved for runtime dependencies. Use an application prefix such as `Ui` or `Acme`. @@ -216,6 +242,7 @@ root: ```shell fvm dart test test/tool/check_open_code_test.dart fvm dart run tool/check_open_code.dart +fvm dart run tool/check_open_code.dart --preset fortal ``` The full check creates a guarded temporary Flutter app, installs the checkout diff --git a/open_code/fortal_fixture/analysis_options.yaml b/open_code/fortal_fixture/analysis_options.yaml new file mode 100644 index 000000000..4ab697645 --- /dev/null +++ b/open_code/fortal_fixture/analysis_options.yaml @@ -0,0 +1,21 @@ +# Keep the consumer fixture dependency-free while treating copied-source drift +# as an error. Generated Mix adapters are covered by compilation and tests. +analyzer: + language: + strict-casts: true + strict-raw-types: true + errors: + unused_import: error + unused_element: error + exclude: + - build/** + - lib/ui/**/*.g.dart + +linter: + rules: + - always_declare_return_types + - avoid_print + - prefer_const_constructors + - prefer_const_declarations + - prefer_final_locals + - unnecessary_parenthesis diff --git a/open_code/fortal_fixture/lib/main.dart b/open_code/fortal_fixture/lib/main.dart new file mode 100644 index 000000000..6700bd045 --- /dev/null +++ b/open_code/fortal_fixture/lib/main.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; + +import 'ui/ui.dart'; + +/// Runs a small application composed entirely from installed Fortal source. +void main() => runApp(const AcmeApp()); + +/// Minimal consumer shell for the Fortal preset. +class AcmeApp extends StatelessWidget { + /// Creates the example application. + const AcmeApp({super.key}); + + @override + Widget build(BuildContext context) { + return const MaterialApp( + debugShowCheckedModeBanner: false, + home: AcmeScope( + child: Scaffold( + body: Center( + child: AcmeCard( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + AcmeHeading('Fortal, now yours'), + AcmeText('Radix Themes 3.3.0 expressed as local Dart.'), + AcmeButton(label: 'Continue'), + ], + ), + ), + ), + ), + ), + ); + } +} diff --git a/open_code/fortal_fixture/pubspec.yaml b/open_code/fortal_fixture/pubspec.yaml new file mode 100644 index 000000000..17aa9dc52 --- /dev/null +++ b/open_code/fortal_fixture/pubspec.yaml @@ -0,0 +1,20 @@ +name: open_code_fixture +description: A fresh application that proves the Fortal preset installs as owned source. +publish_to: "none" +version: 0.1.0 + +environment: + sdk: ">=3.12.0 <4.0.0" + flutter: ">=3.44.0" + +# Deliberately pre-install: remix_cli must add the complete dependency surface. +dependencies: + flutter: + sdk: flutter + +dev_dependencies: + flutter_test: + sdk: flutter + +flutter: + uses-material-design: true diff --git a/open_code/fortal_fixture/test/open_code_test.dart b/open_code/fortal_fixture/test/open_code_test.dart new file mode 100644 index 000000000..76c57f79a --- /dev/null +++ b/open_code/fortal_fixture/test/open_code_test.dart @@ -0,0 +1,317 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mix_chart/mix_chart.dart'; +import 'package:open_code_fixture/main.dart'; +import 'package:open_code_fixture/ui/ui.dart'; +import 'package:remix/remix.dart'; + +void main() { + test('the default palette retains its Radix Themes 3.3.0 values', () { + final colors = resolveAcmeTokens(const AcmeThemeConfig()); + + expect(colors.colorBackground, const Color(0xFFFFFFFF)); + expect(colors.accent.scale.step(9), const Color(0xFF3E63DD)); + expect(colors.gray.scale.step(12), const Color(0xFF1C2024)); + }); + + testWidgets('the example application renders installed source', ( + tester, + ) async { + await tester.pumpWidget(const AcmeApp()); + + expect(find.byType(AcmeCard), findsOneWidget); + expect(find.byType(AcmeButton), findsOneWidget); + expect(find.text('Fortal, now yours'), findsOneWidget); + expect(tester.takeException(), isNull); + }); + + testWidgets('every generated Fortal widget builds below AcmeScope', ( + tester, + ) async { + final cases = _generatedWidgetCases(); + final seen = {}; + + for (final widgetCase in cases) { + await tester.pumpWidget(_host(widgetCase.child)); + await tester.pump(); + + for (final type in widgetCase.generatedTypes) { + expect( + find.byType(type), + findsOneWidget, + reason: '${widgetCase.name} must build $type', + ); + seen.add(type); + } + expect( + tester.takeException(), + isNull, + reason: '${widgetCase.name} must build without framework errors', + ); + } + + expect(seen, _allGeneratedWidgetTypes); + await tester.pumpWidget(const SizedBox.shrink()); + }); +} + +Widget _host(Widget child) { + return MaterialApp( + home: AcmeScope( + child: Scaffold( + body: Center(child: SizedBox(width: 640, height: 480, child: child)), + ), + ), + ); +} + +List<_WidgetCase> _generatedWidgetCases() => [ + _WidgetCase( + 'accordion', + RemixAccordionGroup( + controller: RemixAccordionController(), + child: const AcmeAccordion( + value: 'item', + title: 'Item', + child: Text('Content'), + ), + ), + {AcmeAccordion}, + ), + const _WidgetCase('avatar', AcmeAvatar(label: 'AC'), {AcmeAvatar}), + const _WidgetCase('badge', AcmeBadge(label: 'New'), {AcmeBadge}), + const _WidgetCase('button', AcmeButton(label: 'Save'), {AcmeButton}), + const _WidgetCase('callout', AcmeCallout(text: 'Notice'), {AcmeCallout}), + const _WidgetCase('card', AcmeCard(child: Text('Card')), {AcmeCard}), + _WidgetCase( + 'line chart', + AcmeLineChart( + series: [ + LineSeries( + id: 'revenue', + label: 'Revenue', + points: [ChartPoint(id: 'monday', x: 0, y: 18)], + ), + ], + ), + const {AcmeLineChart}, + ), + _WidgetCase( + 'bar chart', + AcmeBarChart( + groups: [ + BarGroup( + id: 'q1', + label: 'Q1', + bars: [BarValue(id: 'actual', label: 'Actual', toY: 42)], + ), + ], + ), + const {AcmeBarChart}, + ), + _WidgetCase( + 'pie chart', + AcmePieChart( + slices: [PieSlice(id: 'direct', label: 'Direct', value: 64)], + ), + const {AcmePieChart}, + ), + const _WidgetCase( + 'checkbox', + AcmeCheckbox(selected: false, label: 'Choice'), + {AcmeCheckbox}, + ), + const _WidgetCase( + 'checkbox group item', + RemixCheckboxGroup( + values: {}, + child: AcmeCheckboxGroupItem(value: 'a', label: 'Choice A'), + ), + {AcmeCheckboxGroupItem}, + ), + const _WidgetCase( + 'data list', + AcmeDataList( + items: [RemixDataListItem(label: 'Name', value: 'Ada')], + ), + {AcmeDataList}, + ), + const _WidgetCase( + 'data table', + AcmeDataTable( + rows: [], + columns: [ + RemixDataTableColumn( + id: 'value', + label: 'Value', + cellBuilder: _stringCell, + ), + ], + ), + {AcmeDataTable}, + ), + const _WidgetCase('dialog', AcmeDialog(title: 'Dialog'), {AcmeDialog}), + const _WidgetCase( + 'disclosure', + AcmeDisclosure(trigger: Text('More'), content: Text('Details')), + {AcmeDisclosure}, + ), + const _WidgetCase('divider', AcmeDivider(), {AcmeDivider}), + const _WidgetCase( + 'icon button', + AcmeIconButton(icon: Icons.add, semanticLabel: 'Add'), + {AcmeIconButton}, + ), + const _WidgetCase( + 'menu', + AcmeMenu( + trigger: RemixMenuTrigger(label: 'Menu'), + items: [RemixMenuItem(value: 'a', label: 'A')], + ), + {AcmeMenu}, + ), + const _WidgetCase( + 'popover', + AcmePopover(popoverChild: Text('Content'), child: Text('Open')), + {AcmePopover}, + ), + const _WidgetCase('progress', AcmeProgress(value: 0.5), {AcmeProgress}), + const _WidgetCase( + 'radio', + RemixRadioGroup( + groupValue: 'a', + child: AcmeRadio(value: 'a', semanticLabel: 'Choice A'), + ), + {AcmeRadio}, + ), + const _WidgetCase( + 'segmented control', + AcmeSegmentedControl( + items: [RemixSegmentedControlItem(value: 'a', label: 'A')], + selectedValue: 'a', + ), + {AcmeSegmentedControl}, + ), + const _WidgetCase( + 'select', + AcmeSelect( + trigger: RemixSelectTrigger(placeholder: 'Pick'), + items: [RemixSelectItem(value: 'a', label: 'A')], + ), + {AcmeSelect}, + ), + const _WidgetCase( + 'sidebar', + AcmeSidebar( + sections: [ + RemixSidebarSection( + destinations: [ + RemixSidebarDestination(value: 'overview', label: 'Overview'), + ], + ), + ], + selectedValue: 'overview', + ), + {AcmeSidebar}, + ), + const _WidgetCase( + 'skeleton', + AcmeSkeleton(child: SizedBox(width: 80, height: 20)), + {AcmeSkeleton}, + ), + const _WidgetCase('slider', AcmeSlider(value: 0.5), {AcmeSlider}), + const _WidgetCase('spinner', AcmeSpinner(), {AcmeSpinner}), + const _WidgetCase( + 'switch', + AcmeSwitch(selected: false, semanticLabel: 'Notifications'), + {AcmeSwitch}, + ), + const _WidgetCase( + 'tabs', + RemixTabs( + selectedTabId: 'a', + child: Column( + children: [ + AcmeTabBar( + child: Row( + children: [AcmeTab(tabId: 'a', label: 'A')], + ), + ), + AcmeTabView(tabId: 'a', child: Text('A view')), + ], + ), + ), + {AcmeTabBar, AcmeTab, AcmeTabView}, + ), + const _WidgetCase('text', AcmeText('Body'), {AcmeText}), + const _WidgetCase('text field', AcmeTextField(label: 'Name'), { + AcmeTextField, + }), + const _WidgetCase('text area', AcmeTextArea(label: 'Notes'), {AcmeTextArea}), + const _WidgetCase('toggle', AcmeToggle(selected: false, label: 'Bold'), { + AcmeToggle, + }), + const _WidgetCase( + 'toggle group', + AcmeToggleGroup( + items: [RemixToggleGroupItem(value: 'a', label: 'A')], + selectedValue: 'a', + ), + {AcmeToggleGroup}, + ), + const _WidgetCase( + 'tooltip', + AcmeTooltip(tooltipChild: Text('Help'), child: Text('Hover')), + {AcmeTooltip}, + ), +]; + +const _allGeneratedWidgetTypes = { + AcmeAccordion, + AcmeAvatar, + AcmeBadge, + AcmeButton, + AcmeCallout, + AcmeCard, + AcmeLineChart, + AcmeBarChart, + AcmePieChart, + AcmeCheckbox, + AcmeCheckboxGroupItem, + AcmeDataList, + AcmeDataTable, + AcmeDialog, + AcmeDisclosure, + AcmeDivider, + AcmeIconButton, + AcmeMenu, + AcmePopover, + AcmeProgress, + AcmeRadio, + AcmeSegmentedControl, + AcmeSelect, + AcmeSidebar, + AcmeSkeleton, + AcmeSlider, + AcmeSpinner, + AcmeSwitch, + AcmeTabBar, + AcmeTab, + AcmeTabView, + AcmeText, + AcmeTextField, + AcmeTextArea, + AcmeToggle, + AcmeToggleGroup, + AcmeTooltip, +}; + +final class _WidgetCase { + const _WidgetCase(this.name, this.child, this.generatedTypes); + + final String name; + final Widget child; + final Set generatedTypes; +} + +Widget _stringCell(BuildContext context, String value) => Text(value); diff --git a/packages/remix/test/components/slider/slider_widget_test.dart b/packages/remix/test/components/slider/slider_widget_test.dart index bf9bd335f..4057792a2 100644 --- a/packages/remix/test/components/slider/slider_widget_test.dart +++ b/packages/remix/test/components/slider/slider_widget_test.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:naked_ui/naked_ui.dart'; import 'package:remix/remix.dart'; import 'package:remix/src/rendering/remix_box_effects.dart' show RemixBoxAdapter; diff --git a/packages/remix_cli/CHANGELOG.md b/packages/remix_cli/CHANGELOG.md index 4acafc1a6..d2b79d453 100644 --- a/packages/remix_cli/CHANGELOG.md +++ b/packages/remix_cli/CHANGELOG.md @@ -1,5 +1,9 @@ ## 0.1.0 +- Adds the `fortal` preset as application-owned source derived from the + repository's analyzed Radix Themes 3.3.0 implementation. It installs a + prefixed 277-token theme and full component catalog without a + `remix_fortal`, direct `mix`, or direct `naked_ui` dependency. - Adds a preset axis to `remix init`. New configurations use schema 2 and record `preset: default`; schema 1 configurations remain compatible and read as the default preset. diff --git a/packages/remix_cli/README.md b/packages/remix_cli/README.md index 83f4dceaa..ef6347870 100644 --- a/packages/remix_cli/README.md +++ b/packages/remix_cli/README.md @@ -1,8 +1,10 @@ # remix_cli `remix_cli` installs editable component source into a Flutter application. -Remix remains the behavior dependency; the application owns its tokens, theme -values, component recipes, and generated adapters. +Choose the compact `default` preset or the Radix Themes-inspired `fortal` +preset at initialization. Remix remains the behavior dependency; the +application owns its tokens, theme values, component recipes, and generated +adapters. The 0.1.0 catalog includes an opt-in `icons` seam and covers the core Remix component surface: `accordion`, `avatar`, `badge`, `button`, `callout`, `card`, @@ -10,7 +12,9 @@ component surface: `accordion`, `avatar`, `badge`, `button`, `callout`, `card`, `icon_button`, `link`, `menu`, `popover`, `progress`, `radio`, `segmented_control`, `select`, `skeleton`, `slider`, `spinner`, `switch`, `tabs`, `textfield`, `toggle`, `toggle_group`, and `tooltip`, each depending on -`theme`. `data_table` also depends on `checkbox`, `icon_button`, and `select`, +`theme`. The Fortal preset contains the same surface plus `base_button`, +`code`, `heading`, `kbd`, `sidebar`, `text`, and `typography`. `data_table` +also depends on `checkbox`, `icon_button`, and `select`, because its selection column, pager, and page-size control are those components. The catalog also offers `chart` as an optional extension over `mix_chart`; it does not depend on `remix_fortal`. There is no remote registry, @@ -26,7 +30,7 @@ pins the CLI version and its bundled templates: ```shell flutter pub add dev:remix_cli -dart run remix_cli:remix init --prefix Acme +dart run remix_cli:remix init --prefix Acme --preset fortal dart run remix_cli:remix add button ``` @@ -43,8 +47,8 @@ template version per project: dart pub global activate remix_cli ``` -`remix_cli` is versioned independently of `remix` and `remix_fortal`, so its -version never matches theirs. Its registry is authored against one `remix` +`remix_cli` is versioned independently of `remix`, so its version does not +match Remix's. Its registry is authored against one `remix` version; `add` reports when your resolved `remix` is newer than that one. ## Initialize a project @@ -68,12 +72,24 @@ paths: Customize it only at initialization: ```shell -dart run remix_cli:remix init --prefix Acme --preset default --ui-path lib/design_system +dart run remix_cli:remix init --prefix Acme --preset fortal --ui-path lib/design_system ``` `init` validates the Flutter project, writes `remix.yaml`, and creates a barrel with a managed export block. Repeating the same command is safe. A different configuration is refused instead of silently rewriting an existing project. +The preset defaults to `default` when omitted and cannot be changed after +initialization. + +### Fortal as owned source + +With `preset: fortal`, `add button` installs the full local theme layer, +`base_button.dart`, `button.dart`, and the generated adapter. The configured +prefix replaces Fortal throughout: `--prefix Acme` creates `AcmeScope`, +`AcmeTokens`, `AcmeButton`, and `acmeButtonStyle`. The installed application +does not depend directly on `mix`, `naked_ui`, or `remix_fortal`; it can edit +the Radix color data, tokens, recipes, and instance overrides as ordinary app +source. The prefixes `Remix` and `Mix` are reserved for runtime dependencies. Use an application prefix such as `Ui` or `Acme`. diff --git a/packages/remix_cli/lib/src/registry.dart b/packages/remix_cli/lib/src/registry.dart index 73d6525a6..946421a96 100644 --- a/packages/remix_cli/lib/src/registry.dart +++ b/packages/remix_cli/lib/src/registry.dart @@ -5,7 +5,7 @@ import 'package:path/path.dart' as p; import 'package:pub_semver/pub_semver.dart'; import 'package:yaml/yaml.dart'; -const bundledPresets = {'default'}; +const bundledPresets = {'default', 'fortal'}; abstract interface class RegistryAssetLoader { Future read(Uri uri); diff --git a/packages/remix_cli/lib/src/registry/fortal/registry.yaml b/packages/remix_cli/lib/src/registry/fortal/registry.yaml new file mode 100644 index 000000000..d385ab148 --- /dev/null +++ b/packages/remix_cli/lib/src/registry/fortal/registry.yaml @@ -0,0 +1,599 @@ +# Generated by tool/build_fortal_preset.dart. Do not edit. +schema: 1 +items: + theme: + dependencies: + remix: ^1.0.0-beta.7 + files: + - source: templates/theme/computed.dart.tmpl + target: "@ui/theme/computed.dart" + - source: templates/theme/control_styles.dart.tmpl + target: "@ui/theme/control_styles.dart" + - source: templates/theme/radix_colors.dart.tmpl + target: "@ui/theme/radix_colors.dart" + - source: templates/theme/surface_frame.dart.tmpl + target: "@ui/theme/surface_frame.dart" + - source: templates/theme/theme.dart.tmpl + target: "@ui/theme/theme.dart" + - source: templates/theme/theme_data.dart.tmpl + target: "@ui/theme/theme_data.dart" + - source: templates/theme/theme_scope.dart.tmpl + target: "@ui/theme/theme_scope.dart" + - source: templates/theme/tokens.dart.tmpl + target: "@ui/theme/tokens.dart" + exports: + - theme/theme.dart + + icons: + registryDependencies: + - theme + dependencies: + remix_ui_icons: ^0.1.0 + files: + - source: templates/icons/icons.dart.tmpl + target: "@ui/icons.dart" + exports: + - icons.dart + + accordion: + registryDependencies: + - theme + dependencies: + mix_annotations: ^2.2.0-beta.1 + devDependencies: + build_runner: ^2.10.1 + mix_generator: ^2.2.0-beta.3 + files: + - source: templates/accordion/accordion.dart.tmpl + target: "@ui/components/accordion.dart" + generated: + - "@ui/components/accordion.g.dart" + exports: + - components/accordion.dart + + avatar: + registryDependencies: + - theme + dependencies: + mix_annotations: ^2.2.0-beta.1 + devDependencies: + build_runner: ^2.10.1 + mix_generator: ^2.2.0-beta.3 + files: + - source: templates/avatar/avatar.dart.tmpl + target: "@ui/components/avatar.dart" + generated: + - "@ui/components/avatar.g.dart" + exports: + - components/avatar.dart + + badge: + registryDependencies: + - theme + dependencies: + mix_annotations: ^2.2.0-beta.1 + devDependencies: + build_runner: ^2.10.1 + mix_generator: ^2.2.0-beta.3 + files: + - source: templates/badge/badge.dart.tmpl + target: "@ui/components/badge.dart" + generated: + - "@ui/components/badge.g.dart" + exports: + - components/badge.dart + + base_button: + registryDependencies: + - theme + files: + - source: templates/base_button/base_button.dart.tmpl + target: "@ui/components/base_button.dart" + exports: + - components/base_button.dart + + button: + registryDependencies: + - theme + - base_button + dependencies: + mix_annotations: ^2.2.0-beta.1 + devDependencies: + build_runner: ^2.10.1 + mix_generator: ^2.2.0-beta.3 + files: + - source: templates/button/button.dart.tmpl + target: "@ui/components/button.dart" + generated: + - "@ui/components/button.g.dart" + exports: + - components/button.dart + + callout: + registryDependencies: + - theme + dependencies: + mix_annotations: ^2.2.0-beta.1 + devDependencies: + build_runner: ^2.10.1 + mix_generator: ^2.2.0-beta.3 + files: + - source: templates/callout/callout.dart.tmpl + target: "@ui/components/callout.dart" + generated: + - "@ui/components/callout.g.dart" + exports: + - components/callout.dart + + card: + registryDependencies: + - theme + dependencies: + mix_annotations: ^2.2.0-beta.1 + devDependencies: + build_runner: ^2.10.1 + mix_generator: ^2.2.0-beta.3 + files: + - source: templates/card/card.dart.tmpl + target: "@ui/components/card.dart" + generated: + - "@ui/components/card.g.dart" + exports: + - components/card.dart + + chart: + registryDependencies: + - theme + dependencies: + mix_annotations: ^2.2.0-beta.1 + mix_chart: ^0.0.1-beta.1 + devDependencies: + build_runner: ^2.10.1 + mix_generator: ^2.2.0-beta.3 + files: + - source: templates/chart/chart.dart.tmpl + target: "@ui/components/chart.dart" + generated: + - "@ui/components/chart.g.dart" + exports: + - components/chart.dart + + checkbox: + registryDependencies: + - theme + dependencies: + mix_annotations: ^2.2.0-beta.1 + devDependencies: + build_runner: ^2.10.1 + mix_generator: ^2.2.0-beta.3 + files: + - source: templates/checkbox/checkbox.dart.tmpl + target: "@ui/components/checkbox.dart" + generated: + - "@ui/components/checkbox.g.dart" + exports: + - components/checkbox.dart + + code: + registryDependencies: + - theme + - typography + files: + - source: templates/code/code.dart.tmpl + target: "@ui/components/code.dart" + exports: + - components/code.dart + + data_list: + registryDependencies: + - theme + dependencies: + mix_annotations: ^2.2.0-beta.1 + devDependencies: + build_runner: ^2.10.1 + mix_generator: ^2.2.0-beta.3 + files: + - source: templates/data_list/data_list.dart.tmpl + target: "@ui/components/data_list.dart" + generated: + - "@ui/components/data_list.g.dart" + exports: + - components/data_list.dart + + data_table: + registryDependencies: + - theme + - checkbox + - icon_button + - select + dependencies: + mix_annotations: ^2.2.0-beta.1 + devDependencies: + build_runner: ^2.10.1 + mix_generator: ^2.2.0-beta.3 + files: + - source: templates/data_table/data_table.dart.tmpl + target: "@ui/components/data_table.dart" + generated: + - "@ui/components/data_table.g.dart" + exports: + - components/data_table.dart + + dialog: + registryDependencies: + - theme + dependencies: + mix_annotations: ^2.2.0-beta.1 + devDependencies: + build_runner: ^2.10.1 + mix_generator: ^2.2.0-beta.3 + files: + - source: templates/dialog/dialog.dart.tmpl + target: "@ui/components/dialog.dart" + generated: + - "@ui/components/dialog.g.dart" + exports: + - components/dialog.dart + + disclosure: + registryDependencies: + - theme + dependencies: + mix_annotations: ^2.2.0-beta.1 + devDependencies: + build_runner: ^2.10.1 + mix_generator: ^2.2.0-beta.3 + files: + - source: templates/disclosure/disclosure.dart.tmpl + target: "@ui/components/disclosure.dart" + generated: + - "@ui/components/disclosure.g.dart" + exports: + - components/disclosure.dart + + divider: + registryDependencies: + - theme + dependencies: + mix_annotations: ^2.2.0-beta.1 + devDependencies: + build_runner: ^2.10.1 + mix_generator: ^2.2.0-beta.3 + files: + - source: templates/divider/divider.dart.tmpl + target: "@ui/components/divider.dart" + generated: + - "@ui/components/divider.g.dart" + exports: + - components/divider.dart + + heading: + registryDependencies: + - theme + - typography + files: + - source: templates/heading/heading.dart.tmpl + target: "@ui/components/heading.dart" + exports: + - components/heading.dart + + icon_button: + registryDependencies: + - theme + - base_button + dependencies: + mix_annotations: ^2.2.0-beta.1 + devDependencies: + build_runner: ^2.10.1 + mix_generator: ^2.2.0-beta.3 + files: + - source: templates/icon_button/icon_button.dart.tmpl + target: "@ui/components/icon_button.dart" + generated: + - "@ui/components/icon_button.g.dart" + exports: + - components/icon_button.dart + + kbd: + registryDependencies: + - theme + - typography + files: + - source: templates/kbd/kbd.dart.tmpl + target: "@ui/components/kbd.dart" + exports: + - components/kbd.dart + + link: + registryDependencies: + - theme + - typography + files: + - source: templates/link/link.dart.tmpl + target: "@ui/components/link.dart" + exports: + - components/link.dart + + menu: + registryDependencies: + - theme + dependencies: + mix_annotations: ^2.2.0-beta.1 + devDependencies: + build_runner: ^2.10.1 + mix_generator: ^2.2.0-beta.3 + files: + - source: templates/menu/menu.dart.tmpl + target: "@ui/components/menu.dart" + generated: + - "@ui/components/menu.g.dart" + exports: + - components/menu.dart + + popover: + registryDependencies: + - theme + dependencies: + mix_annotations: ^2.2.0-beta.1 + devDependencies: + build_runner: ^2.10.1 + mix_generator: ^2.2.0-beta.3 + files: + - source: templates/popover/popover.dart.tmpl + target: "@ui/components/popover.dart" + generated: + - "@ui/components/popover.g.dart" + exports: + - components/popover.dart + + progress: + registryDependencies: + - theme + dependencies: + mix_annotations: ^2.2.0-beta.1 + devDependencies: + build_runner: ^2.10.1 + mix_generator: ^2.2.0-beta.3 + files: + - source: templates/progress/progress.dart.tmpl + target: "@ui/components/progress.dart" + generated: + - "@ui/components/progress.g.dart" + exports: + - components/progress.dart + + radio: + registryDependencies: + - theme + dependencies: + mix_annotations: ^2.2.0-beta.1 + devDependencies: + build_runner: ^2.10.1 + mix_generator: ^2.2.0-beta.3 + files: + - source: templates/radio/radio.dart.tmpl + target: "@ui/components/radio.dart" + generated: + - "@ui/components/radio.g.dart" + exports: + - components/radio.dart + + segmented_control: + registryDependencies: + - theme + dependencies: + mix_annotations: ^2.2.0-beta.1 + devDependencies: + build_runner: ^2.10.1 + mix_generator: ^2.2.0-beta.3 + files: + - source: templates/segmented_control/segmented_control.dart.tmpl + target: "@ui/components/segmented_control.dart" + generated: + - "@ui/components/segmented_control.g.dart" + exports: + - components/segmented_control.dart + + select: + registryDependencies: + - theme + dependencies: + mix_annotations: ^2.2.0-beta.1 + devDependencies: + build_runner: ^2.10.1 + mix_generator: ^2.2.0-beta.3 + files: + - source: templates/select/select.dart.tmpl + target: "@ui/components/select.dart" + generated: + - "@ui/components/select.g.dart" + exports: + - components/select.dart + + sidebar: + registryDependencies: + - theme + - text + - toggle + dependencies: + mix_annotations: ^2.2.0-beta.1 + devDependencies: + build_runner: ^2.10.1 + mix_generator: ^2.2.0-beta.3 + files: + - source: templates/sidebar/sidebar.dart.tmpl + target: "@ui/components/sidebar.dart" + generated: + - "@ui/components/sidebar.g.dart" + exports: + - components/sidebar.dart + + skeleton: + registryDependencies: + - theme + dependencies: + mix_annotations: ^2.2.0-beta.1 + devDependencies: + build_runner: ^2.10.1 + mix_generator: ^2.2.0-beta.3 + files: + - source: templates/skeleton/skeleton.dart.tmpl + target: "@ui/components/skeleton.dart" + generated: + - "@ui/components/skeleton.g.dart" + exports: + - components/skeleton.dart + + slider: + registryDependencies: + - theme + dependencies: + mix_annotations: ^2.2.0-beta.1 + devDependencies: + build_runner: ^2.10.1 + mix_generator: ^2.2.0-beta.3 + files: + - source: templates/slider/slider.dart.tmpl + target: "@ui/components/slider.dart" + generated: + - "@ui/components/slider.g.dart" + exports: + - components/slider.dart + + spinner: + registryDependencies: + - theme + dependencies: + mix_annotations: ^2.2.0-beta.1 + devDependencies: + build_runner: ^2.10.1 + mix_generator: ^2.2.0-beta.3 + files: + - source: templates/spinner/spinner.dart.tmpl + target: "@ui/components/spinner.dart" + generated: + - "@ui/components/spinner.g.dart" + exports: + - components/spinner.dart + + switch: + registryDependencies: + - theme + dependencies: + mix_annotations: ^2.2.0-beta.1 + devDependencies: + build_runner: ^2.10.1 + mix_generator: ^2.2.0-beta.3 + files: + - source: templates/switch/switch.dart.tmpl + target: "@ui/components/switch.dart" + generated: + - "@ui/components/switch.g.dart" + exports: + - components/switch.dart + + tabs: + registryDependencies: + - theme + dependencies: + mix_annotations: ^2.2.0-beta.1 + devDependencies: + build_runner: ^2.10.1 + mix_generator: ^2.2.0-beta.3 + files: + - source: templates/tabs/tabs.dart.tmpl + target: "@ui/components/tabs.dart" + generated: + - "@ui/components/tabs.g.dart" + exports: + - components/tabs.dart + + text: + registryDependencies: + - theme + - typography + dependencies: + mix_annotations: ^2.2.0-beta.1 + devDependencies: + build_runner: ^2.10.1 + mix_generator: ^2.2.0-beta.3 + files: + - source: templates/text/text.dart.tmpl + target: "@ui/components/text.dart" + generated: + - "@ui/components/text.g.dart" + exports: + - components/text.dart + + textfield: + registryDependencies: + - theme + dependencies: + mix_annotations: ^2.2.0-beta.1 + devDependencies: + build_runner: ^2.10.1 + mix_generator: ^2.2.0-beta.3 + files: + - source: templates/textfield/textfield.dart.tmpl + target: "@ui/components/textfield.dart" + generated: + - "@ui/components/textfield.g.dart" + exports: + - components/textfield.dart + + toggle: + registryDependencies: + - theme + dependencies: + mix_annotations: ^2.2.0-beta.1 + devDependencies: + build_runner: ^2.10.1 + mix_generator: ^2.2.0-beta.3 + files: + - source: templates/toggle/toggle.dart.tmpl + target: "@ui/components/toggle.dart" + generated: + - "@ui/components/toggle.g.dart" + exports: + - components/toggle.dart + + toggle_group: + registryDependencies: + - theme + dependencies: + mix_annotations: ^2.2.0-beta.1 + devDependencies: + build_runner: ^2.10.1 + mix_generator: ^2.2.0-beta.3 + files: + - source: templates/toggle_group/toggle_group.dart.tmpl + target: "@ui/components/toggle_group.dart" + generated: + - "@ui/components/toggle_group.g.dart" + exports: + - components/toggle_group.dart + + tooltip: + registryDependencies: + - theme + dependencies: + mix_annotations: ^2.2.0-beta.1 + devDependencies: + build_runner: ^2.10.1 + mix_generator: ^2.2.0-beta.3 + files: + - source: templates/tooltip/tooltip.dart.tmpl + target: "@ui/components/tooltip.dart" + generated: + - "@ui/components/tooltip.g.dart" + exports: + - components/tooltip.dart + + typography: + registryDependencies: + - theme + files: + - source: templates/typography/typography.dart.tmpl + target: "@ui/components/typography.dart" + exports: + - components/typography.dart diff --git a/packages/remix_cli/lib/src/registry/fortal/templates/accordion/accordion.dart.tmpl b/packages/remix_cli/lib/src/registry/fortal/templates/accordion/accordion.dart.tmpl new file mode 100644 index 000000000..5597f9f7c --- /dev/null +++ b/packages/remix_cli/lib/src/registry/fortal/templates/accordion/accordion.dart.tmpl @@ -0,0 +1,166 @@ +import 'package:flutter/widgets.dart'; +import 'package:mix_annotations/mix_annotations.dart'; +import 'package:remix/remix.dart'; + +import '../theme/theme.dart'; + +part 'accordion.g.dart'; + +/// {{typePrefix}} accordion size presets. +enum {{typePrefix}}AccordionSize { size1, size2, size3 } + +/// {{typePrefix}} accordion color variants. +enum {{typePrefix}}AccordionVariant { surface, soft } + +/// {{typePrefix}}-themed preset for [RemixAccordion]. +@MixWidget(target: RemixAccordion.new) +AccordionStyler {{valuePrefix}}AccordionStyle({ + {{typePrefix}}AccordionVariant variant = .surface, + {{typePrefix}}AccordionSize size = .size2, + AccordionStyler style = const AccordionStyler.create(), +}) { + return (switch (variant) { + .surface => _{{valuePrefix}}AccordionSurfaceStyler(size), + .soft => _{{valuePrefix}}AccordionSoftStyler(size), + }).merge(style); +} + +// Panel anatomy follows the mapped Table family (see data_table.dart): +// `container` alone owns radius, frame, fill, and clipping, while trigger and +// content stay flat rectangles that simply get cropped to its rounded shape. +// The frame and divider are foreground borders so edge-to-edge child fills +// cannot partially cover their antialiased edges. +AccordionStyler _{{valuePrefix}}AccordionBaseStyler({{typePrefix}}AccordionSize size) { + return AccordionStyler() + .trigger(.direction(.horizontal)) + .leadingIcon(.color({{typePrefix}}Tokens.gray11())) + .title( + .fontWeight( + {{typePrefix}}Tokens.fontWeightMedium(), + ).color({{typePrefix}}Tokens.gray12()), + ) + .trailingIcon(.color({{typePrefix}}Tokens.gray11())) + .content(.width(.infinity)) + .merge(_{{valuePrefix}}AccordionSizeStyler(size)); +} + +AccordionStyler _{{valuePrefix}}AccordionFocusStyler() { + return AccordionStyler().trigger(FlexBoxStyler().{{valuePrefix}}FocusRing()); +} + +AccordionStyler _{{valuePrefix}}AccordionDisabledStyler() { + return AccordionStyler() + .trigger(.color({{typePrefix}}Tokens.grayA3())) + .leadingIcon(.color({{typePrefix}}Tokens.gray8())) + .title(.color({{typePrefix}}Tokens.gray8())) + .trailingIcon(.color({{typePrefix}}Tokens.gray8())); +} + +AccordionStyler _{{valuePrefix}}AccordionSurfaceStyler([ + {{typePrefix}}AccordionSize size = .size2, +]) { + return _{{valuePrefix}}AccordionBaseStyler(size) + .container( + {{valuePrefix}}SurfaceFrame( + fillColor: {{typePrefix}}Tokens.gray2(), + borderColor: {{typePrefix}}Tokens.gray6(), + borderWidth: {{typePrefix}}Tokens.borderWidth1(), + radius: _{{valuePrefix}}AccordionRadius(size), + ), + ) + .trigger(.color({{typePrefix}}Tokens.gray1())) + .content( + BoxStyler() + .foregroundDecoration( + BoxDecorationMix( + border: BoxBorderMix.top( + _{{valuePrefix}}AccordionBorderSide({{typePrefix}}Tokens.gray6()), + ), + ), + ) + .wrap(_{{valuePrefix}}AccordionContentTypography({{typePrefix}}Tokens.gray12())), + ) + .onHovered(.trigger(.color({{typePrefix}}Tokens.gray2()))) + .onPressed(.trigger(.color({{typePrefix}}Tokens.gray3()))) + .onFocusVisible(_{{valuePrefix}}AccordionFocusStyler()) + .onDisabled(_{{valuePrefix}}AccordionDisabledStyler()); +} + +AccordionStyler _{{valuePrefix}}AccordionSoftStyler([ + {{typePrefix}}AccordionSize size = .size2, +]) { + return _{{valuePrefix}}AccordionBaseStyler(size) + .container( + {{valuePrefix}}SurfaceFrame( + fillColor: {{typePrefix}}Tokens.accent2(), + borderColor: {{typePrefix}}Tokens.accent6(), + borderWidth: {{typePrefix}}Tokens.borderWidth1(), + radius: _{{valuePrefix}}AccordionRadius(size), + ), + ) + .trigger(.color({{typePrefix}}Tokens.accent2())) + .title(.color({{typePrefix}}Tokens.accent12())) + .trailingIcon(.color({{typePrefix}}Tokens.accent11())) + .content( + BoxStyler() + .foregroundDecoration( + BoxDecorationMix( + border: BoxBorderMix.top( + _{{valuePrefix}}AccordionBorderSide({{typePrefix}}Tokens.accent6()), + ), + ), + ) + .wrap(_{{valuePrefix}}AccordionContentTypography({{typePrefix}}Tokens.accent12())), + ) + .onHovered(.trigger(.color({{typePrefix}}Tokens.accent3()))) + .onPressed(.trigger(.color({{typePrefix}}Tokens.accent4()))) + .onFocusVisible(_{{valuePrefix}}AccordionFocusStyler()) + .onDisabled(_{{valuePrefix}}AccordionDisabledStyler()); +} + +/// The 1px edge shared by the panel's outer border and the trigger/content +/// divider, so the seam reads as a continuation of the frame rather than an +/// unrelated line. +BorderSideMix _{{valuePrefix}}AccordionBorderSide(Color color) => + BorderSideMix(color: color, width: {{typePrefix}}Tokens.borderWidth1()); + +/// Pins bare [Text] accordion content to the 14px type-scale step (`text2`) +/// regardless of accordion size, so content never renders larger than its own +/// trigger's title (measured 14/15/16px at size1/size2/size3). {{typePrefix}} text +/// children pin their own run. [color] supplies the variant's own content tint. +WidgetModifierConfig _{{valuePrefix}}AccordionContentTypography(Color color) => + WidgetModifierConfig.defaultTextStyle( + style: {{typePrefix}}Tokens.text2.mix(), + ).defaultTextStyle(style: TextStyleMix().color(color)); + +AccordionStyler _{{valuePrefix}}AccordionSizeStyler({{typePrefix}}AccordionSize size) { + return switch (size) { + .size1 => AccordionStyler( + trigger: FlexBoxStyler().padding(.all({{typePrefix}}Tokens.space2())), + leadingIcon: .size({{typePrefix}}Tokens.space4()), + title: .style({{typePrefix}}Tokens.text2.mix()), + trailingIcon: .size({{typePrefix}}Tokens.space4()), + content: .padding(.all({{typePrefix}}Tokens.space2())), + ), + .size2 => AccordionStyler( + trigger: FlexBoxStyler().padding(.all({{typePrefix}}Tokens.space3())), + leadingIcon: .size({{typePrefix}}Tokens.spinnerSize3()), + title: .style({{typePrefix}}Tokens.accordionText2.mix()), + trailingIcon: .size({{typePrefix}}Tokens.spinnerSize3()), + content: .padding(.all({{typePrefix}}Tokens.space3())), + ), + .size3 => AccordionStyler( + trigger: FlexBoxStyler().padding(.all({{typePrefix}}Tokens.space4())), + leadingIcon: .size({{typePrefix}}Tokens.space5()), + title: .style({{typePrefix}}Tokens.text3.mix()), + trailingIcon: .size({{typePrefix}}Tokens.space5()), + content: .padding(.all({{typePrefix}}Tokens.space4())), + ), + }; +} + +Radius _{{valuePrefix}}AccordionRadius({{typePrefix}}AccordionSize size) => switch (size) { + .size1 => {{typePrefix}}Tokens.radius3(), + .size2 => {{typePrefix}}Tokens.radius4(), + .size3 => {{typePrefix}}Tokens.radius5(), +}; diff --git a/packages/remix_cli/lib/src/registry/fortal/templates/avatar/avatar.dart.tmpl b/packages/remix_cli/lib/src/registry/fortal/templates/avatar/avatar.dart.tmpl new file mode 100644 index 000000000..db22521b0 --- /dev/null +++ b/packages/remix_cli/lib/src/registry/fortal/templates/avatar/avatar.dart.tmpl @@ -0,0 +1,127 @@ +import 'package:flutter/widgets.dart'; +import 'package:mix_annotations/mix_annotations.dart'; +import 'package:remix/remix.dart'; + +import '../theme/theme.dart'; + +part 'avatar.g.dart'; + +/// Radix Themes Avatar size presets. +enum {{typePrefix}}AvatarSize { + size1, + size2, + size3, + size4, + size5, + size6, + size7, + size8, + size9, +} + +/// Radix Themes Avatar variants. +enum {{typePrefix}}AvatarVariant { soft, solid } + +/// {{typePrefix}}-themed Avatar with the Radix size, variant, and override contract. +/// +/// [fallbackLength] selects the pinned one- or two-character fallback +/// typography. Pass `2` when [RemixAvatar.label] contains two initials. +@MixWidget(target: RemixAvatar.new) +AvatarStyler {{valuePrefix}}AvatarStyle({ + {{typePrefix}}AvatarVariant variant = .soft, + {{typePrefix}}AvatarSize size = .size3, + bool highContrast = false, + int fallbackLength = 1, + AvatarStyler style = const AvatarStyler.create(), +}) { + final base = _{{valuePrefix}}AvatarBaseStyler(size, fallbackLength: fallbackLength); + final softContent = highContrast + ? {{typePrefix}}Tokens.accent12() + : {{typePrefix}}Tokens.accentA11(); + final solidContent = highContrast + ? {{typePrefix}}Tokens.accent1() + : {{typePrefix}}Tokens.accentContrast(); + return (switch (variant) { + .soft => + base + .color({{typePrefix}}Tokens.accentA3()) + .labelColor(softContent) + .iconColor(softContent), + .solid => + base + .color( + highContrast ? {{typePrefix}}Tokens.accent12() : {{typePrefix}}Tokens.accent9(), + ) + .labelColor(solidContent) + .iconColor(solidContent), + }).merge(style); +} + +AvatarStyler _{{valuePrefix}}AvatarBaseStyler( + {{typePrefix}}AvatarSize size, { + required int fallbackLength, +}) { + final fallbackText = _{{valuePrefix}}AvatarFallbackText(size, fallbackLength); + final dimension = _{{valuePrefix}}AvatarDimension(size); + return AvatarStyler() + .clipBehavior(.hardEdge) + .label( + TextStyler( + style: fallbackText.mix(), + ).fontWeight({{typePrefix}}Tokens.fontWeightMedium()), + ) + .icon(.size(_{{valuePrefix}}AvatarIconSize(size)).color({{typePrefix}}Tokens.accentA11())) + .size(dimension, dimension) + .borderRadius(.all(_{{valuePrefix}}AvatarRadius(size))); +} + +double _{{valuePrefix}}AvatarDimension({{typePrefix}}AvatarSize size) => switch (size) { + .size1 => {{typePrefix}}Tokens.space5(), + .size2 => {{typePrefix}}Tokens.space6(), + .size3 => {{typePrefix}}Tokens.space7(), + .size4 => {{typePrefix}}Tokens.space8(), + .size5 => {{typePrefix}}Tokens.space9(), + .size6 => {{typePrefix}}Tokens.avatarSize6(), + .size7 => {{typePrefix}}Tokens.avatarSize7(), + .size8 => {{typePrefix}}Tokens.avatarSize8(), + .size9 => {{typePrefix}}Tokens.avatarSize9(), +}; + +double _{{valuePrefix}}AvatarIconSize({{typePrefix}}AvatarSize size) => switch (size) { + .size1 => {{typePrefix}}Tokens.avatarIconSize1(), + .size2 => {{typePrefix}}Tokens.avatarIconSize2(), + .size3 => {{typePrefix}}Tokens.avatarIconSize3(), + .size4 => {{typePrefix}}Tokens.avatarIconSize4(), + .size5 => {{typePrefix}}Tokens.avatarIconSize5(), + .size6 => {{typePrefix}}Tokens.avatarIconSize6(), + .size7 => {{typePrefix}}Tokens.avatarIconSize7(), + .size8 => {{typePrefix}}Tokens.avatarIconSize8(), + .size9 => {{typePrefix}}Tokens.avatarIconSize9(), +}; + +Radius _{{valuePrefix}}AvatarRadius({{typePrefix}}AvatarSize size) => switch (size) { + .size1 || .size2 => {{typePrefix}}Tokens.radius2OrFull(), + .size3 || .size4 => {{typePrefix}}Tokens.radius3OrFull(), + .size5 => {{typePrefix}}Tokens.radius4OrFull(), + .size6 || .size7 => {{typePrefix}}Tokens.radius5OrFull(), + .size8 || .size9 => {{typePrefix}}Tokens.radius6OrFull(), +}; + +TextStyleToken _{{valuePrefix}}AvatarFallbackText( + {{typePrefix}}AvatarSize size, + int fallbackLength, +) => switch ((size, fallbackLength == 2)) { + (.size1, false) => {{typePrefix}}Tokens.avatarFallback1One, + (.size1, true) => {{typePrefix}}Tokens.avatarFallback1Two, + (.size2, false) => {{typePrefix}}Tokens.avatarFallback2One, + (.size2, true) => {{typePrefix}}Tokens.avatarFallback2Two, + (.size3, false) => {{typePrefix}}Tokens.avatarFallback3One, + (.size3, true) => {{typePrefix}}Tokens.avatarFallback3Two, + (.size4, false) => {{typePrefix}}Tokens.avatarFallback4One, + (.size4, true) => {{typePrefix}}Tokens.avatarFallback4Two, + (.size5, _) => {{typePrefix}}Tokens.avatarFallback5, + (.size6, _) => {{typePrefix}}Tokens.avatarFallback6, + (.size7, _) => {{typePrefix}}Tokens.avatarFallback7, + (.size8, _) => {{typePrefix}}Tokens.avatarFallback8, + (.size9, _) => {{typePrefix}}Tokens.avatarFallback9, +}; diff --git a/packages/remix_cli/lib/src/registry/fortal/templates/badge/badge.dart.tmpl b/packages/remix_cli/lib/src/registry/fortal/templates/badge/badge.dart.tmpl new file mode 100644 index 000000000..1ee04fc76 --- /dev/null +++ b/packages/remix_cli/lib/src/registry/fortal/templates/badge/badge.dart.tmpl @@ -0,0 +1,108 @@ +import 'package:flutter/widgets.dart'; +import 'package:mix_annotations/mix_annotations.dart'; +import 'package:remix/remix.dart'; + +import '../theme/theme.dart'; + +part 'badge.g.dart'; + +/// Radix Themes Badge size presets. +enum {{typePrefix}}BadgeSize { size1, size2, size3 } + +/// Radix Themes Badge variants. +enum {{typePrefix}}BadgeVariant { solid, soft, surface, outline } + +/// {{typePrefix}}-themed Badge with the Radix size, variant, and override contract. +@MixWidget(target: RemixBadge.new) +BadgeStyler {{valuePrefix}}BadgeStyle({ + {{typePrefix}}BadgeVariant variant = .soft, + {{typePrefix}}BadgeSize size = .size1, + bool highContrast = false, + BadgeStyler style = const BadgeStyler.create(), +}) { + final base = _{{valuePrefix}}BadgeBaseStyler(size); + return (switch (variant) { + .solid => + base + .color( + highContrast ? {{typePrefix}}Tokens.accent12() : {{typePrefix}}Tokens.accent9(), + ) + .labelColor( + highContrast + ? {{typePrefix}}Tokens.accent1() + : {{typePrefix}}Tokens.accentContrast(), + ), + .soft => + base + .color({{typePrefix}}Tokens.accentA3()) + .labelColor( + // Step 11 is Radix low-contrast text, not WCAG AA 4.5:1 on + // accentA3 over colorPanelSolid. highContrast promotes accent12. + highContrast ? {{typePrefix}}Tokens.accent12() : {{typePrefix}}Tokens.accentA11(), + ), + .surface => + base + .color({{typePrefix}}Tokens.accentSurface()) + .containerEffects( + RemixBoxEffectsMix.behindContent( + {{valuePrefix}}InsetSurface(strokes: [{{typePrefix}}Tokens.accentA6()]), + ), + ) + .labelColor( + highContrast ? {{typePrefix}}Tokens.accent12() : {{typePrefix}}Tokens.accentA11(), + ), + .outline => + base + .containerEffects( + RemixBoxEffectsMix.behindContent( + {{valuePrefix}}InsetSurface( + strokes: [ + highContrast + ? {{typePrefix}}Tokens.accentA7() + : {{typePrefix}}Tokens.accentA8(), + if (highContrast) {{typePrefix}}Tokens.grayA11(), + ], + ), + ), + ) + .labelColor( + highContrast ? {{typePrefix}}Tokens.accent12() : {{typePrefix}}Tokens.accentA11(), + ), + }).merge(style); +} + +BadgeStyler _{{valuePrefix}}BadgeBaseStyler({{typePrefix}}BadgeSize size) { + final radius = _{{valuePrefix}}BadgeRadius(size); + return BadgeStyler( + container: .padding(_{{valuePrefix}}BadgePadding(size)), + label: .style( + _{{valuePrefix}}BadgeText(size).mix(), + ).fontWeight({{typePrefix}}Tokens.fontWeightMedium()), + ).borderRadius(.all(radius)); +} + +TextStyleToken _{{valuePrefix}}BadgeText({{typePrefix}}BadgeSize size) => switch (size) { + .size1 || .size2 => {{typePrefix}}Tokens.text1, + .size3 => {{typePrefix}}Tokens.text2, +}; + +EdgeInsetsGeometryMix _{{valuePrefix}}BadgePadding({{typePrefix}}BadgeSize size) => + switch (size) { + .size1 => EdgeInsetsGeometryMix.symmetric( + horizontal: {{typePrefix}}Tokens.badgePaddingX1(), + vertical: {{typePrefix}}Tokens.badgePaddingY1(), + ), + .size2 => EdgeInsetsGeometryMix.symmetric( + horizontal: {{typePrefix}}Tokens.space2(), + vertical: {{typePrefix}}Tokens.space1(), + ), + .size3 => EdgeInsetsGeometryMix.symmetric( + horizontal: {{typePrefix}}Tokens.badgePaddingX3(), + vertical: {{typePrefix}}Tokens.space1(), + ), + }; + +Radius _{{valuePrefix}}BadgeRadius({{typePrefix}}BadgeSize size) => switch (size) { + .size1 => {{typePrefix}}Tokens.radius1OrFull(), + .size2 || .size3 => {{typePrefix}}Tokens.radius2OrFull(), +}; diff --git a/packages/remix_cli/lib/src/registry/fortal/templates/base_button/base_button.dart.tmpl b/packages/remix_cli/lib/src/registry/fortal/templates/base_button/base_button.dart.tmpl new file mode 100644 index 000000000..d79b330d8 --- /dev/null +++ b/packages/remix_cli/lib/src/registry/fortal/templates/base_button/base_button.dart.tmpl @@ -0,0 +1,547 @@ +import 'package:flutter/widgets.dart'; +import 'package:remix/remix.dart'; + +import '../theme/theme.dart'; + +/// The Radix BaseButton size scale shared by Button and IconButton. +/// +/// Deliberate: this mirrors [{{typePrefix}}BaseButtonVariant]. Button and IconButton +/// each own a size enum, so the shared metrics need a common type — passing the +/// raw `size.index + 1` instead would drop every switch below to a wildcard and +/// defer an unknown size to a runtime throw. +enum {{typePrefix}}BaseButtonSize { size1, size2, size3, size4 } + +/// Shared Radix BaseButton metrics used by Button and IconButton recipes. +({ + double height, + double paddingX, + double gap, + Radius radius, + TextStyleToken text, + double spinnerSize, +}) +{{valuePrefix}}BaseButtonMetrics({{typePrefix}}BaseButtonSize size) => switch (size) { + .size1 => ( + height: {{typePrefix}}Tokens.space5(), + paddingX: {{typePrefix}}Tokens.space2(), + gap: {{typePrefix}}Tokens.space1(), + radius: {{typePrefix}}Tokens.radius1OrFull(), + text: {{typePrefix}}Tokens.text1, + spinnerSize: {{typePrefix}}Tokens.space3(), + ), + .size2 => ( + height: {{typePrefix}}Tokens.space6(), + paddingX: {{typePrefix}}Tokens.space3(), + gap: {{typePrefix}}Tokens.space2(), + radius: {{typePrefix}}Tokens.radius2OrFull(), + text: {{typePrefix}}Tokens.text2, + spinnerSize: {{typePrefix}}Tokens.space4(), + ), + .size3 => ( + height: {{typePrefix}}Tokens.space7(), + paddingX: {{typePrefix}}Tokens.space4(), + gap: {{typePrefix}}Tokens.space3(), + radius: {{typePrefix}}Tokens.radius3OrFull(), + text: {{typePrefix}}Tokens.text3, + spinnerSize: {{typePrefix}}Tokens.space4(), + ), + .size4 => ( + height: {{typePrefix}}Tokens.space8(), + paddingX: {{typePrefix}}Tokens.space5(), + gap: {{typePrefix}}Tokens.space3(), + radius: {{typePrefix}}Tokens.radius4OrFull(), + text: {{typePrefix}}Tokens.text4, + spinnerSize: {{typePrefix}}Tokens.spinnerSize3(), + ), +}; + +/// Content-box metrics for the ghost BaseButton variant. +({double paddingX, double paddingY, double marginX, double marginY, double gap}) +{{valuePrefix}}BaseButtonGhostMetrics({{typePrefix}}BaseButtonSize size) => switch (size) { + .size1 => ( + paddingX: {{typePrefix}}Tokens.space2(), + paddingY: {{typePrefix}}Tokens.space1(), + marginX: {{typePrefix}}Tokens.baseButtonGhostMarginX12(), + marginY: {{typePrefix}}Tokens.baseButtonGhostMarginY12(), + gap: {{typePrefix}}Tokens.space1(), + ), + .size2 => ( + paddingX: {{typePrefix}}Tokens.space2(), + paddingY: {{typePrefix}}Tokens.space1(), + marginX: {{typePrefix}}Tokens.baseButtonGhostMarginX12(), + marginY: {{typePrefix}}Tokens.baseButtonGhostMarginY12(), + gap: {{typePrefix}}Tokens.space1(), + ), + .size3 => ( + paddingX: {{typePrefix}}Tokens.space3(), + paddingY: {{typePrefix}}Tokens.baseButtonGhostPaddingY3(), + marginX: {{typePrefix}}Tokens.baseButtonGhostMarginX3(), + marginY: {{typePrefix}}Tokens.baseButtonGhostMarginY3(), + gap: {{typePrefix}}Tokens.space2(), + ), + .size4 => ( + paddingX: {{typePrefix}}Tokens.space4(), + paddingY: {{typePrefix}}Tokens.space2(), + marginX: {{typePrefix}}Tokens.baseButtonGhostMarginX4(), + marginY: {{typePrefix}}Tokens.baseButtonGhostMarginY4(), + gap: {{typePrefix}}Tokens.space2(), + ), +}; + +/// Content-box metrics for the ghost IconButton variant. +({double padding, double margin}) {{valuePrefix}}IconButtonGhostMetrics( + {{typePrefix}}BaseButtonSize size, +) => switch (size) { + .size1 => ( + padding: {{typePrefix}}Tokens.space1(), + margin: {{typePrefix}}Tokens.iconButtonGhostMargin1(), + ), + .size2 => ( + padding: {{typePrefix}}Tokens.iconButtonGhostPadding2(), + margin: {{typePrefix}}Tokens.iconButtonGhostMargin2(), + ), + .size3 => ( + padding: {{typePrefix}}Tokens.space2(), + margin: {{typePrefix}}Tokens.iconButtonGhostMargin3(), + ), + .size4 => ( + padding: {{typePrefix}}Tokens.space3(), + margin: {{typePrefix}}Tokens.iconButtonGhostMargin4(), + ), +}; + +/// Resolves a mode-aware ordered CSS filter at the component build context. +WidgetModifierConfig {{valuePrefix}}ModeAwareFilter({ + required List light, + required List dark, +}) => WidgetModifierConfig.modifier( + _{{typePrefix}}ModeAwareFilterMix(light: light, dark: dark), +); + +/// Explicit identity filter used to clear a higher-priority state filter. +WidgetModifierConfig {{valuePrefix}}ClearFilter() => + {{valuePrefix}}ModeAwareFilter(light: const [], dark: const []); + +/// Exact classic BaseButton surface for a visual state. +RemixBoxEffectLayerMix {{valuePrefix}}ClassicBaseButtonSurface({ + required bool highContrast, + bool hovered = false, + bool pressed = false, + bool disabled = false, +}) { + final inset = {{typePrefix}}Tokens.baseButtonClassicAfterInset(); + if (disabled) { + return RemixBoxEffectLayerMix( + gradients: [ + RemixLinearGradientMix( + colors: [ + {{typePrefix}}Tokens.blackA1(), + const Color(0x00000000), + {{typePrefix}}Tokens.whiteA1(), + ], + stops: const [-0.2, 0.4, 1], + ), + RemixLinearGradientMix( + colors: [{{typePrefix}}Tokens.grayA2(), {{typePrefix}}Tokens.grayA2()], + ), + ], + gradientInsets: [inset, inset], + shadowToken: {{typePrefix}}Tokens.baseButtonClassicDisabledShadows, + ); + } + + final baseColor = highContrast + ? {{typePrefix}}Tokens.accent12() + : {{typePrefix}}Tokens.accent9(); + final afterColor = hovered && !highContrast + ? {{typePrefix}}Tokens.accent10() + : baseColor; + final pseudoGradient = RemixLinearGradientMix( + colors: [ + highContrast + ? hovered || pressed + ? {{typePrefix}}Tokens.blackA5() + : {{typePrefix}}Tokens.blackA3() + : hovered + ? {{typePrefix}}Tokens.blackA2() + : pressed + ? {{typePrefix}}Tokens.blackA2() + : {{typePrefix}}Tokens.blackA1(), + const Color(0x00000000), + highContrast + ? pressed + ? {{typePrefix}}Tokens.whiteA3() + : {{typePrefix}}Tokens.whiteA2() + : hovered || pressed + ? {{typePrefix}}Tokens.whiteA3() + : {{typePrefix}}Tokens.whiteA2(), + ], + stops: hovered && !highContrast + ? const [-0.15, 0.425, 1] + : const [0, 0.5, 1], + ); + final gradients = [ + pseudoGradient, + RemixLinearGradientMix(colors: [afterColor, afterColor]), + if (pressed) + RemixLinearGradientMix( + colors: [{{typePrefix}}Tokens.blackA1(), const Color(0x00000000)], + ) + else ...[ + RemixLinearGradientMix( + colors: [ + const Color(0x00000000), + const Color(0x00000000), + {{typePrefix}}Tokens.grayA4(), + {{typePrefix}}Tokens.grayA4(), + ], + stops: const [0, 0.5, 0.5, 1], + ), + RemixLinearGradientMix( + colors: [ + const Color(0x00000000), + const Color(0x00000000), + baseColor, + baseColor, + ], + stops: const [0, 0.5, 0.8, 1], + ), + ], + ]; + return RemixBoxEffectLayerMix( + gradients: gradients, + gradientInsets: [inset, inset, ...List.filled(gradients.length - 2, 0)], + shadowToken: pressed + ? highContrast + ? {{typePrefix}}Tokens.baseButtonClassicActiveHighContrastShadows + : {{typePrefix}}Tokens.baseButtonClassicActiveShadows + : highContrast + ? {{typePrefix}}Tokens.baseButtonClassicHighContrastShadows + : {{typePrefix}}Tokens.baseButtonClassicShadows, + ); +} + +final class _{{typePrefix}}ModeAwareFilterMix + extends ModifierMix { + const _{{typePrefix}}ModeAwareFilterMix({required this.light, required this.dark}); + + final List light; + final List dark; + + @override + RemixOrderedColorFilterModifier resolve(BuildContext context) => + RemixOrderedColorFilterModifier( + {{typePrefix}}Theme.of(context).isDark ? dark : light, + ); + + @override + _{{typePrefix}}ModeAwareFilterMix merge(_{{typePrefix}}ModeAwareFilterMix? other) => + other ?? this; + + @override + List get props => [light, dark]; +} + +/// Shared Radix BaseButton variants implemented by Button and IconButton. +enum {{typePrefix}}BaseButtonVariant { classic, solid, soft, surface, outline, ghost } + +/// One visual-state style fragment from the shared BaseButton recipe. +final class {{typePrefix}}BaseButtonStateStyle { + const {{typePrefix}}BaseButtonStateStyle({ + this.foreground, + this.background, + this.effects, + this.modifier, + this.spinnerOpacity, + }); + + final Color? foreground; + final Color? background; + final RemixBoxEffectsMix? effects; + final WidgetModifierConfig? modifier; + final double? spinnerOpacity; +} + +/// Visual state styles shared by the concrete Button and IconButton stylers. +final class {{typePrefix}}BaseButtonStateStyles { + const {{typePrefix}}BaseButtonStateStyles({ + required this.idle, + required this.hovered, + required this.pressed, + required this.disabled, + required this.focusVisible, + required this.disabledFocus, + }); + + final {{typePrefix}}BaseButtonStateStyle idle; + final {{typePrefix}}BaseButtonStateStyle hovered; + final {{typePrefix}}BaseButtonStateStyle pressed; + final {{typePrefix}}BaseButtonStateStyle disabled; + final {{typePrefix}}BaseButtonStateStyle focusVisible; + final {{typePrefix}}BaseButtonStateStyle disabledFocus; +} + +{{typePrefix}}BaseButtonStateStyles {{valuePrefix}}BaseButtonStateStyles({ + required {{typePrefix}}BaseButtonVariant variant, + required bool highContrast, +}) { + final states = switch (variant) { + .classic => _classicStateStyles(highContrast: highContrast), + .solid => _solidStateStyles(highContrast: highContrast), + .soft => _softStateStyles(highContrast: highContrast), + .surface => _surfaceStateStyles(highContrast: highContrast), + .outline => _outlineStateStyles(highContrast: highContrast), + .ghost => _ghostStateStyles(highContrast: highContrast), + }; + final focusColor = switch (variant) { + .soft => {{typePrefix}}Tokens.accent8(), + .classic || + .solid || + .surface || + .outline || + .ghost => {{typePrefix}}Tokens.focus8(), + }; + final focusOffset = switch (variant) { + .classic || .solid => 2.0, + .soft || .surface || .outline || .ghost => -1.0, + }; + + return {{typePrefix}}BaseButtonStateStyles( + idle: states.idle, + hovered: states.hovered, + pressed: states.pressed, + disabled: states.disabled, + focusVisible: {{typePrefix}}BaseButtonStateStyle( + effects: {{valuePrefix}}FocusOutline(focusColor, offset: focusOffset), + ), + disabledFocus: {{typePrefix}}BaseButtonStateStyle( + effects: RemixBoxEffectsMix.outline( + BorderSideMix(style: BorderStyle.none), + ), + ), + ); +} + +typedef _InteractionStateStyles = ({ + {{typePrefix}}BaseButtonStateStyle idle, + {{typePrefix}}BaseButtonStateStyle hovered, + {{typePrefix}}BaseButtonStateStyle pressed, + {{typePrefix}}BaseButtonStateStyle disabled, +}); + +_InteractionStateStyles _classicStateStyles({required bool highContrast}) { + final foreground = highContrast + ? {{typePrefix}}Tokens.gray1() + : {{typePrefix}}Tokens.accentContrast(); + + return ( + idle: {{typePrefix}}BaseButtonStateStyle( + foreground: foreground, + background: highContrast + ? {{typePrefix}}Tokens.accent12() + : {{typePrefix}}Tokens.accent9(), + effects: RemixBoxEffectsMix.behindContent( + {{valuePrefix}}ClassicBaseButtonSurface(highContrast: highContrast), + ), + ), + hovered: {{typePrefix}}BaseButtonStateStyle( + effects: RemixBoxEffectsMix.behindContent( + {{valuePrefix}}ClassicBaseButtonSurface( + highContrast: highContrast, + hovered: true, + ), + ), + modifier: _hoverFilter(highContrast, classic: true), + ), + pressed: {{typePrefix}}BaseButtonStateStyle( + effects: RemixBoxEffectsMix.behindContent( + {{valuePrefix}}ClassicBaseButtonSurface( + highContrast: highContrast, + pressed: true, + ), + ), + modifier: _pressedFilter(highContrast), + ), + disabled: {{typePrefix}}BaseButtonStateStyle( + foreground: {{typePrefix}}Tokens.grayA8(), + background: {{typePrefix}}Tokens.gray2(), + effects: RemixBoxEffectsMix.behindContent( + {{valuePrefix}}ClassicBaseButtonSurface(highContrast: false, disabled: true), + ), + spinnerOpacity: 1, + modifier: {{valuePrefix}}ClearFilter(), + ), + ); +} + +_InteractionStateStyles _solidStateStyles({required bool highContrast}) { + final foreground = highContrast + ? {{typePrefix}}Tokens.gray1() + : {{typePrefix}}Tokens.accentContrast(); + + return ( + idle: {{typePrefix}}BaseButtonStateStyle( + foreground: foreground, + background: highContrast + ? {{typePrefix}}Tokens.accent12() + : {{typePrefix}}Tokens.accent9(), + ), + hovered: {{typePrefix}}BaseButtonStateStyle( + background: highContrast + ? {{typePrefix}}Tokens.accent12() + : {{typePrefix}}Tokens.accent10(), + modifier: _hoverFilter(highContrast, classic: false), + ), + pressed: {{typePrefix}}BaseButtonStateStyle( + background: highContrast + ? {{typePrefix}}Tokens.accent12() + : {{typePrefix}}Tokens.accent10(), + modifier: _pressedFilter(highContrast), + ), + disabled: {{typePrefix}}BaseButtonStateStyle( + foreground: {{typePrefix}}Tokens.grayA8(), + background: {{typePrefix}}Tokens.grayA3(), + spinnerOpacity: 1, + modifier: {{valuePrefix}}ClearFilter(), + ), + ); +} + +_InteractionStateStyles _softStateStyles({required bool highContrast}) => ( + idle: {{typePrefix}}BaseButtonStateStyle( + foreground: highContrast + ? {{typePrefix}}Tokens.accent12() + : {{typePrefix}}Tokens.accentA11(), + background: {{typePrefix}}Tokens.accentA3(), + ), + hovered: {{typePrefix}}BaseButtonStateStyle(background: {{typePrefix}}Tokens.accentA4()), + pressed: {{typePrefix}}BaseButtonStateStyle(background: {{typePrefix}}Tokens.accentA5()), + disabled: {{typePrefix}}BaseButtonStateStyle( + foreground: {{typePrefix}}Tokens.grayA8(), + background: {{typePrefix}}Tokens.grayA3(), + spinnerOpacity: 1, + ), +); + +_InteractionStateStyles _surfaceStateStyles({required bool highContrast}) => ( + idle: {{typePrefix}}BaseButtonStateStyle( + foreground: highContrast + ? {{typePrefix}}Tokens.accent12() + : {{typePrefix}}Tokens.accentA11(), + background: {{typePrefix}}Tokens.accentSurface(), + effects: RemixBoxEffectsMix.behindContent( + {{valuePrefix}}InsetSurface(strokes: [{{typePrefix}}Tokens.accentA7()]), + ), + ), + hovered: {{typePrefix}}BaseButtonStateStyle( + background: {{typePrefix}}Tokens.accentSurface(), + effects: RemixBoxEffectsMix.behindContent( + {{valuePrefix}}InsetSurface(strokes: [{{typePrefix}}Tokens.accentA8()]), + ), + ), + pressed: {{typePrefix}}BaseButtonStateStyle( + background: {{typePrefix}}Tokens.accentA3(), + effects: RemixBoxEffectsMix.behindContent( + {{valuePrefix}}InsetSurface(strokes: [{{typePrefix}}Tokens.accentA8()]), + ), + ), + disabled: {{typePrefix}}BaseButtonStateStyle( + foreground: {{typePrefix}}Tokens.grayA8(), + background: {{typePrefix}}Tokens.grayA2(), + effects: RemixBoxEffectsMix.behindContent( + {{valuePrefix}}InsetSurface(strokes: [{{typePrefix}}Tokens.grayA6()]), + ), + spinnerOpacity: 1, + ), +); + +_InteractionStateStyles _outlineStateStyles({required bool highContrast}) { + final strokes = highContrast + ? [{{typePrefix}}Tokens.accentA7(), {{typePrefix}}Tokens.grayA11()] + : [{{typePrefix}}Tokens.accentA8()]; + final effects = RemixBoxEffectsMix.behindContent( + {{valuePrefix}}InsetSurface(strokes: strokes), + ); + + return ( + idle: {{typePrefix}}BaseButtonStateStyle( + foreground: highContrast + ? {{typePrefix}}Tokens.accent12() + : {{typePrefix}}Tokens.accentA11(), + effects: effects, + ), + hovered: {{typePrefix}}BaseButtonStateStyle( + background: {{typePrefix}}Tokens.accentA2(), + effects: effects, + ), + pressed: {{typePrefix}}BaseButtonStateStyle( + background: {{typePrefix}}Tokens.accentA3(), + effects: effects, + ), + disabled: {{typePrefix}}BaseButtonStateStyle( + foreground: {{typePrefix}}Tokens.grayA8(), + background: const Color(0x00000000), + effects: RemixBoxEffectsMix.behindContent( + {{valuePrefix}}InsetSurface(strokes: [{{typePrefix}}Tokens.grayA7()]), + ), + spinnerOpacity: 1, + ), + ); +} + +_InteractionStateStyles _ghostStateStyles({required bool highContrast}) => ( + idle: {{typePrefix}}BaseButtonStateStyle( + foreground: highContrast + ? {{typePrefix}}Tokens.accent12() + : {{typePrefix}}Tokens.accentA11(), + background: const Color(0x00000000), + ), + hovered: {{typePrefix}}BaseButtonStateStyle(background: {{typePrefix}}Tokens.accentA3()), + pressed: {{typePrefix}}BaseButtonStateStyle(background: {{typePrefix}}Tokens.accentA4()), + disabled: {{typePrefix}}BaseButtonStateStyle( + foreground: {{typePrefix}}Tokens.grayA8(), + background: const Color(0x00000000), + spinnerOpacity: 1, + ), +); + +WidgetModifierConfig _hoverFilter(bool highContrast, {required bool classic}) { + if (!highContrast) return {{valuePrefix}}ClearFilter(); + + return {{valuePrefix}}ModeAwareFilter( + light: const [ + RemixCssColorFilterOperation.contrast(0.88), + RemixCssColorFilterOperation.saturate(1.1), + RemixCssColorFilterOperation.brightness(1.1), + ], + dark: [ + const RemixCssColorFilterOperation.contrast(0.88), + const RemixCssColorFilterOperation.saturate(1.3), + RemixCssColorFilterOperation.brightness(classic ? 1.14 : 1.18), + ], + ); +} + +WidgetModifierConfig _pressedFilter(bool highContrast) { + if (highContrast) { + return {{valuePrefix}}ModeAwareFilter( + light: const [ + RemixCssColorFilterOperation.contrast(0.82), + RemixCssColorFilterOperation.saturate(1.2), + RemixCssColorFilterOperation.brightness(1.16), + ], + dark: const [ + RemixCssColorFilterOperation.brightness(0.95), + RemixCssColorFilterOperation.saturate(1.2), + ], + ); + } + + return {{valuePrefix}}ModeAwareFilter( + light: const [ + RemixCssColorFilterOperation.brightness(0.92), + RemixCssColorFilterOperation.saturate(1.1), + ], + dark: const [RemixCssColorFilterOperation.brightness(1.08)], + ); +} diff --git a/packages/remix_cli/lib/src/registry/fortal/templates/button/button.dart.tmpl b/packages/remix_cli/lib/src/registry/fortal/templates/button/button.dart.tmpl new file mode 100644 index 000000000..64123b8ef --- /dev/null +++ b/packages/remix_cli/lib/src/registry/fortal/templates/button/button.dart.tmpl @@ -0,0 +1,137 @@ +import 'package:flutter/widgets.dart'; +import 'package:mix_annotations/mix_annotations.dart'; +import 'package:remix/remix.dart'; + +import 'base_button.dart'; +import '../theme/theme.dart'; + +part 'button.g.dart'; + +/// Radix Themes Button size presets. +enum {{typePrefix}}ButtonSize { size1, size2, size3, size4 } + +/// Radix Themes Button variants. +enum {{typePrefix}}ButtonVariant { classic, solid, soft, surface, outline, ghost } + +/// {{typePrefix}}-themed Button with the Radix size, variant, and override contract. +@MixWidget(target: RemixButton.new) +ButtonStyler {{valuePrefix}}ButtonStyle({ + {{typePrefix}}ButtonVariant variant = .solid, + {{typePrefix}}ButtonSize size = .size2, + bool highContrast = false, + ButtonStyler style = const ButtonStyler.create(), +}) { + final base = _{{valuePrefix}}ButtonBaseStyler(variant, _{{valuePrefix}}BaseButtonSize(size)); + final stateStyles = {{valuePrefix}}BaseButtonStateStyles( + variant: _{{valuePrefix}}BaseButtonVariant(variant), + highContrast: highContrast, + ); + + return _apply{{typePrefix}}ButtonStateStyles( + base, + stateStyles, + pressedPaddingTop: variant == .classic ? (size == .size1 ? 1 : 2) : null, + ).merge(style); +} + +ButtonStyler _{{valuePrefix}}ButtonBaseStyler( + {{typePrefix}}ButtonVariant variant, + {{typePrefix}}BaseButtonSize size, +) { + final metrics = {{valuePrefix}}BaseButtonMetrics(size); + var style = ButtonStyler( + container: .direction(.horizontal).mainAxisSize(.min).spacing(metrics.gap), + label: .style(metrics.text.mix()).fontWeight( + variant == .ghost + ? {{typePrefix}}Tokens.fontWeightRegular() + : {{typePrefix}}Tokens.fontWeightMedium(), + ), + spinner: .size(metrics.spinnerSize) + .opacity(0.65) + .leafRadius({{typePrefix}}Tokens.radius1()) + .duration(const Duration(milliseconds: 800)), + ).borderRadius(.all(metrics.radius)); + + if (variant == .ghost) { + final ghost = {{valuePrefix}}BaseButtonGhostMetrics(size); + style = style + .spacing(ghost.gap) + .padding( + .symmetric(horizontal: ghost.paddingX, vertical: ghost.paddingY), + ) + .margin(.symmetric(horizontal: ghost.marginX, vertical: ghost.marginY)); + } else { + style = style + .minHeight(metrics.height) + .padding(.horizontal(metrics.paddingX)) + .icon(.opacity(0.9)); + } + return style; +} + +{{typePrefix}}BaseButtonVariant _{{valuePrefix}}BaseButtonVariant({{typePrefix}}ButtonVariant variant) => + switch (variant) { + .classic => .classic, + .solid => .solid, + .soft => .soft, + .surface => .surface, + .outline => .outline, + .ghost => .ghost, + }; + +{{typePrefix}}BaseButtonSize _{{valuePrefix}}BaseButtonSize({{typePrefix}}ButtonSize size) => + switch (size) { + .size1 => .size1, + .size2 => .size2, + .size3 => .size3, + .size4 => .size4, + }; + +ButtonStyler _apply{{typePrefix}}ButtonStateStyles( + ButtonStyler base, + {{typePrefix}}BaseButtonStateStyles stateStyles, { + required double? pressedPaddingTop, +}) { + var pressed = _apply{{typePrefix}}ButtonState(ButtonStyler(), stateStyles.pressed); + if (pressedPaddingTop != null) { + pressed = pressed.padding(.top(pressedPaddingTop)); + } + + return _apply{{typePrefix}}ButtonState(base, stateStyles.idle) + .onHovered(_apply{{typePrefix}}ButtonState(ButtonStyler(), stateStyles.hovered)) + .onPressed(pressed) + .onDisabled(_apply{{typePrefix}}ButtonState(ButtonStyler(), stateStyles.disabled)) + .onFocusVisible( + _apply{{typePrefix}}ButtonState(ButtonStyler(), stateStyles.focusVisible), + ) + .onDisabled( + _apply{{typePrefix}}ButtonState(ButtonStyler(), stateStyles.disabledFocus), + ); +} + +ButtonStyler _apply{{typePrefix}}ButtonState( + ButtonStyler style, + {{typePrefix}}BaseButtonStateStyle state, +) { + var result = style; + final foreground = state.foreground; + if (foreground != null) { + result = result + .label(.color(foreground)) + .icon(.color(foreground)) + .spinner(.color(foreground)); + } + if (state.background != null) { + result = result.color(state.background!); + } + if (state.effects != null) { + result = result.containerEffects(state.effects!); + } + if (state.spinnerOpacity != null) { + result = result.spinner(.opacity(state.spinnerOpacity!)); + } + if (state.modifier != null) { + result = result.wrap(state.modifier!); + } + return result; +} diff --git a/packages/remix_cli/lib/src/registry/fortal/templates/callout/callout.dart.tmpl b/packages/remix_cli/lib/src/registry/fortal/templates/callout/callout.dart.tmpl new file mode 100644 index 000000000..023e65fd9 --- /dev/null +++ b/packages/remix_cli/lib/src/registry/fortal/templates/callout/callout.dart.tmpl @@ -0,0 +1,86 @@ +import 'package:flutter/widgets.dart'; +import 'package:mix_annotations/mix_annotations.dart'; +import 'package:remix/remix.dart'; + +import '../theme/theme.dart'; + +part 'callout.g.dart'; + +/// Radix Themes Callout size presets. +enum {{typePrefix}}CalloutSize { size1, size2, size3 } + +/// Radix Themes Callout variants. +enum {{typePrefix}}CalloutVariant { soft, surface, outline } + +/// {{typePrefix}}-themed Callout with the Radix size, variant, and override contract. +@MixWidget(target: RemixCallout.new) +CalloutStyler {{valuePrefix}}CalloutStyle({ + {{typePrefix}}CalloutVariant variant = .soft, + {{typePrefix}}CalloutSize size = .size2, + bool highContrast = false, + CalloutStyler style = const CalloutStyler.create(), +}) { + final contentColor = highContrast + ? {{typePrefix}}Tokens.accent12() + : {{typePrefix}}Tokens.accentA11(); + final base = _{{valuePrefix}}CalloutBaseStyler( + size, + ).iconColor(contentColor).textColor(contentColor); + return (switch (variant) { + .soft => base.color({{typePrefix}}Tokens.accentA3()), + .surface => + base + .color({{typePrefix}}Tokens.accentA2()) + .containerEffects( + RemixBoxEffectsMix.behindContent( + {{valuePrefix}}InsetSurface(strokes: [{{typePrefix}}Tokens.accentA6()]), + ), + ), + .outline => base.containerEffects( + RemixBoxEffectsMix.behindContent( + {{valuePrefix}}InsetSurface(strokes: [{{typePrefix}}Tokens.accentA7()]), + ), + ), + }).merge(style); +} + +CalloutStyler _{{valuePrefix}}CalloutBaseStyler({{typePrefix}}CalloutSize size) { + final radius = _{{valuePrefix}}CalloutRadius(size); + return CalloutStyler( + container: .direction(.horizontal) + .mainAxisSize(.min) + .crossAxisAlignment(.start) + .spacing(_{{valuePrefix}}CalloutGap(size)) + .padding(EdgeInsetsGeometryMix.all(_{{valuePrefix}}CalloutPadding(size))), + text: .style(_{{valuePrefix}}CalloutText(size).mix()), + icon: .size(_{{valuePrefix}}CalloutIconSize(size)), + ).borderRadius(.all(radius)); +} + +double _{{valuePrefix}}CalloutPadding({{typePrefix}}CalloutSize size) => switch (size) { + .size1 => {{typePrefix}}Tokens.space3(), + .size2 => {{typePrefix}}Tokens.space4(), + .size3 => {{typePrefix}}Tokens.space5(), +}; + +double _{{valuePrefix}}CalloutGap({{typePrefix}}CalloutSize size) => switch (size) { + .size1 => {{typePrefix}}Tokens.space2(), + .size2 => {{typePrefix}}Tokens.space3(), + .size3 => {{typePrefix}}Tokens.space4(), +}; + +TextStyleToken _{{valuePrefix}}CalloutText({{typePrefix}}CalloutSize size) => switch (size) { + .size1 || .size2 => {{typePrefix}}Tokens.text2, + .size3 => {{typePrefix}}Tokens.text3, +}; + +double _{{valuePrefix}}CalloutIconSize({{typePrefix}}CalloutSize size) => switch (size) { + .size1 || .size2 => {{typePrefix}}Tokens.space4(), + .size3 => {{typePrefix}}Tokens.spinnerSize3(), +}; + +Radius _{{valuePrefix}}CalloutRadius({{typePrefix}}CalloutSize size) => switch (size) { + .size1 => {{typePrefix}}Tokens.radius3(), + .size2 => {{typePrefix}}Tokens.radius4(), + .size3 => {{typePrefix}}Tokens.radius5(), +}; diff --git a/packages/remix_cli/lib/src/registry/fortal/templates/card/card.dart.tmpl b/packages/remix_cli/lib/src/registry/fortal/templates/card/card.dart.tmpl new file mode 100644 index 000000000..98b9d4c95 --- /dev/null +++ b/packages/remix_cli/lib/src/registry/fortal/templates/card/card.dart.tmpl @@ -0,0 +1,223 @@ +import 'package:flutter/widgets.dart'; +import 'package:mix_annotations/mix_annotations.dart'; +import 'package:remix/remix.dart'; + +import '../theme/theme.dart'; + +part 'card.g.dart'; + +/// Radix Themes Card size presets. +enum {{typePrefix}}CardSize { size1, size2, size3, size4, size5 } + +/// Radix Themes Card variants. +enum {{typePrefix}}CardVariant { surface, classic, ghost } + +/// {{typePrefix}}-themed Card with the Radix size and variant contract. +@MixWidget(target: RemixCard.new) +CardStyler {{valuePrefix}}CardStyle({ + {{typePrefix}}CardVariant variant = .surface, + {{typePrefix}}CardSize size = .size1, + CardStyler style = const CardStyler.create(), +}) { + final metrics = _{{valuePrefix}}CardMetrics(size); + final base = CardStyler() + .padding(.all(metrics.padding)) + .borderRadius(.all(metrics.radius)) + .clipBehavior(Clip.antiAlias) + .onFocusVisible( + .containerEffects( + RemixBoxEffectsMix( + outline: BorderSideMix( + color: {{typePrefix}}Tokens.focus8(), + width: 2, + strokeAlign: BorderSide.strokeAlignInside, + ), + outlineOffset: -1, + ), + ), + ); + + return (switch (variant) { + .surface => _{{valuePrefix}}CardSurface(base), + .classic => _{{valuePrefix}}CardClassic(base), + .ghost => _{{valuePrefix}}CardGhost(base, metrics.ghostMargin), + }).merge(style); +} + +({double padding, double ghostMargin, Radius radius}) _{{valuePrefix}}CardMetrics( + {{typePrefix}}CardSize size, +) => switch (size) { + .size1 => ( + padding: {{typePrefix}}Tokens.space3(), + ghostMargin: {{typePrefix}}Tokens.cardGhostMargin1(), + radius: {{typePrefix}}Tokens.radius4(), + ), + .size2 => ( + padding: {{typePrefix}}Tokens.space4(), + ghostMargin: {{typePrefix}}Tokens.cardGhostMargin2(), + radius: {{typePrefix}}Tokens.radius4(), + ), + .size3 => ( + padding: {{typePrefix}}Tokens.space5(), + ghostMargin: {{typePrefix}}Tokens.cardGhostMargin3(), + radius: {{typePrefix}}Tokens.radius5(), + ), + .size4 => ( + padding: {{typePrefix}}Tokens.space6(), + ghostMargin: {{typePrefix}}Tokens.cardGhostMargin4(), + radius: {{typePrefix}}Tokens.radius5(), + ), + .size5 => ( + padding: {{typePrefix}}Tokens.space8(), + ghostMargin: {{typePrefix}}Tokens.cardGhostMargin5(), + radius: {{typePrefix}}Tokens.radius6(), + ), +}; + +CardStyler _{{valuePrefix}}CardSurface(CardStyler base) { + base = base.containerEffects( + RemixBoxEffectsMix.backdropBlur({{typePrefix}}Tokens.panelBlur()), + ); + final open = CardStyler() + .containerEffects(RemixBoxEffectsMix.behindContent(_{{valuePrefix}}CardPanel())) + .containerEffects( + RemixBoxEffectsMix.overContent( + _{{valuePrefix}}CardSurfaceStroke({{typePrefix}}Tokens.grayStroke7()), + ), + ); + final activeFocus = CardStyler() + .containerEffects( + RemixBoxEffectsMix.behindContent(_{{valuePrefix}}CardActiveFocus()), + ) + .onSelected(open); + final pressed = CardStyler() + .containerEffects( + RemixBoxEffectsMix.overContent( + _{{valuePrefix}}CardSurfaceStroke({{typePrefix}}Tokens.grayStroke6()), + ), + ) + .onFocusVisible(activeFocus) + .onSelected(open); + + return base + .containerEffects(RemixBoxEffectsMix.behindContent(_{{valuePrefix}}CardPanel())) + .containerEffects( + RemixBoxEffectsMix.overContent( + _{{valuePrefix}}CardSurfaceStroke({{typePrefix}}Tokens.grayStroke5()), + ), + ) + .onHovered(open) + .onPressed(pressed) + .onSelected(open.onPressed(open)); +} + +CardStyler _{{valuePrefix}}CardClassic(CardStyler base) { + base = base.containerEffects( + RemixBoxEffectsMix.backdropBlur({{typePrefix}}Tokens.panelBlur()), + ); + final open = CardStyler() + .animate(AnimationConfig.ease(const Duration(milliseconds: 40))) + .containerEffects( + RemixBoxEffectsMix.behindContent( + _{{valuePrefix}}CardPanel( + shadowToken: {{typePrefix}}Tokens.cardClassicHoverOuterShadows, + ), + ), + ) + .containerEffects( + RemixBoxEffectsMix.overContent( + RemixBoxEffectLayerMix( + shadowToken: {{typePrefix}}Tokens.cardClassicHoverInnerShadows, + ), + ), + ); + final pressed = CardStyler() + .animate(AnimationConfig.ease(const Duration(milliseconds: 40))) + .containerEffects( + RemixBoxEffectsMix.behindContent( + RemixBoxEffectLayerMix( + shadowToken: {{typePrefix}}Tokens.cardClassicActiveOuterShadows, + ), + ), + ) + .containerEffects( + RemixBoxEffectsMix.overContent( + RemixBoxEffectLayerMix( + shadowToken: {{typePrefix}}Tokens.cardClassicActiveInnerShadows, + ), + ), + ) + .onFocusVisible( + .containerEffects( + RemixBoxEffectsMix.behindContent(_{{valuePrefix}}CardActiveFocus()), + ).onSelected(open), + ) + .onSelected(open); + + return base + .animate(AnimationConfig.ease(const Duration(milliseconds: 120))) + .containerEffects( + RemixBoxEffectsMix.behindContent( + _{{valuePrefix}}CardPanel(shadowToken: {{typePrefix}}Tokens.cardClassicOuterShadows), + ), + ) + .containerEffects( + RemixBoxEffectsMix.overContent( + RemixBoxEffectLayerMix( + shadowToken: {{typePrefix}}Tokens.cardClassicInnerShadows, + ), + ), + ) + .onHovered(open) + .onPressed(pressed) + .onSelected(open.onPressed(open)); +} + +CardStyler _{{valuePrefix}}CardGhost(CardStyler base, double ghostMargin) { + final focused = CardStyler().color({{typePrefix}}Tokens.accentA2()); + final open = CardStyler() + .color({{typePrefix}}Tokens.grayA3()) + .onFocusVisible(focused); + final pressed = CardStyler() + .color({{typePrefix}}Tokens.grayA4()) + .onFocusVisible(focused) + .onSelected(open); + + return base + .margin(.all(ghostMargin)) + .color(const Color(0x00000000)) + .onHovered(open) + .onPressed(pressed) + .onSelected(open.onPressed(open)); +} + +RemixBoxEffectLayerMix _{{valuePrefix}}CardPanel({ + RemixBoxShadowListToken? shadowToken, +}) => RemixBoxEffectLayerMix( + gradients: [ + RemixLinearGradientMix( + colors: [{{typePrefix}}Tokens.colorPanel(), {{typePrefix}}Tokens.colorPanel()], + ), + ], + gradientInsets: const [1], + shadowToken: shadowToken, +); + +RemixBoxEffectLayerMix _{{valuePrefix}}CardActiveFocus() => RemixBoxEffectLayerMix( + gradients: [ + RemixLinearGradientMix( + colors: [{{typePrefix}}Tokens.accentA2(), {{typePrefix}}Tokens.accentA2()], + ), + RemixLinearGradientMix( + colors: [{{typePrefix}}Tokens.colorPanel(), {{typePrefix}}Tokens.colorPanel()], + ), + ], + gradientInsets: const [1, 1], +); + +RemixBoxEffectLayerMix _{{valuePrefix}}CardSurfaceStroke(Color color) => + RemixBoxEffectLayerMix( + shadows: [ + RemixBoxShadowMix(color: color, spreadRadius: 1, shapeInset: 1), + ], + ); diff --git a/packages/remix_cli/lib/src/registry/fortal/templates/chart/chart.dart.tmpl b/packages/remix_cli/lib/src/registry/fortal/templates/chart/chart.dart.tmpl new file mode 100644 index 000000000..f9b5d29c8 --- /dev/null +++ b/packages/remix_cli/lib/src/registry/fortal/templates/chart/chart.dart.tmpl @@ -0,0 +1,255 @@ +import 'package:flutter/widgets.dart'; +import 'package:mix_annotations/mix_annotations.dart'; +import 'package:mix_chart/mix_chart.dart'; +import 'package:remix/remix.dart'; + +import '../theme/radix_colors.dart' + show amber, blue, cyan, green, orange, ruby, violet; +import '../theme/theme.dart'; + +part 'chart.g.dart'; + +const _standardPaletteToken = ContextToken>( + _resolveStandardPalette, +); +const _highContrastPaletteToken = ContextToken>( + _resolveHighContrastPalette, +); +const _tooltipBorderToken = ContextToken(_resolveTooltipBorder); +const _tooltipRadiusToken = ContextToken(_resolveTooltipRadius); +const _tooltipPaddingToken = ContextToken(_resolveTooltipPadding); +const _barRadiusToken = ContextToken(_resolveBarRadius); +const _lineWidthToken = ContextToken(_resolveLineWidth); +const _highContrastLineWidthToken = ContextToken( + _resolveHighContrastLineWidth, +); + +/// Returns the categorical palette used by {{typePrefix}} charts in the current scope. +/// +/// The first entry follows the configured {{typePrefix}} accent. Remaining entries use +/// Radix color families selected for clear categorical separation. Set +/// [highContrast] to use step 12 instead of the standard solid-color step 9. +List resolve{{typePrefix}}ChartPalette( + BuildContext context, { + bool highContrast = false, +}) { + final theme = {{typePrefix}}Theme.of(context); + final colors = resolve{{typePrefix}}Tokens(theme); + final step = highContrast ? 12 : 9; + final candidates = [ + colors.accent.scale.step(step), + for (final family in [cyan, orange, ruby, green, violet, amber, blue]) + (theme.isDark ? family.dark : family.light).scale.step(step), + ]; + + return List.unmodifiable(candidates.toSet()); +} + +/// {{typePrefix}} presentation for a Mix line or area chart. +/// +/// Generates [{{typePrefix}}LineChart] through `mix_generator`. The plot remains +/// transparent so callers can compose it inside any {{typePrefix}} surface. +@MixWidget(target: LineChart.new) +LineChartStyler {{valuePrefix}}LineChartStyle({ + bool highContrast = false, + bool showMarkers = false, + List? palette, + LineChartStyler style = const LineChartStyler.create(), +}) { + final recipe = LineChartStyler() + .frame(_{{valuePrefix}}FrameStyle()) + .axis(_{{valuePrefix}}AxisStyle()) + .topAxis(_hiddenAxisStyle()) + .rightAxis(_hiddenAxisStyle()) + .grid(_{{valuePrefix}}GridStyle()) + .series( + LineSeriesStyler() + .curve(.curved) + .smoothness(0.18) + .preventCurveOvershooting(true) + .roundStrokeCap(true) + .roundStrokeJoin(true) + .stroke( + ChartStrokeStyler().width( + highContrast + ? _highContrastLineWidthToken() + : _lineWidthToken(), + ), + ) + .marker( + ChartMarkerStyler() + .show(showMarkers) + .radius({{typePrefix}}Tokens.space1()) + .borderColor({{typePrefix}}Tokens.colorPanel()) + .borderWidth({{typePrefix}}Tokens.borderWidth2()), + ), + ) + .tooltip(_{{valuePrefix}}TooltipStyle()); + + return recipe + .merge( + LineChartStyler.create( + palette: _paletteProp(highContrast: highContrast, palette: palette), + ), + ) + .merge(style); +} + +/// {{typePrefix}} presentation for a Mix grouped, stacked, or floating bar chart. +/// +/// Generates [{{typePrefix}}BarChart] through `mix_generator`. +@MixWidget(target: BarChart.new) +BarChartStyler {{valuePrefix}}BarChartStyle({ + bool highContrast = false, + List? palette, + BarChartStyler style = const BarChartStyler.create(), +}) { + final bar = BarStyler.create( + borderRadius: Prop.token(_barRadiusToken), + ).width({{typePrefix}}Tokens.space4()); + final recipe = BarChartStyler() + .frame(_{{valuePrefix}}FrameStyle()) + .axis(_{{valuePrefix}}AxisStyle()) + .topAxis(_hiddenAxisStyle()) + .rightAxis(_hiddenAxisStyle()) + .grid(_{{valuePrefix}}GridStyle()) + .bar(bar) + .groupSpacing({{typePrefix}}Tokens.space4()) + .barSpacing({{typePrefix}}Tokens.space2()) + .tooltip(_{{valuePrefix}}TooltipStyle()); + + return recipe + .merge( + BarChartStyler.create( + palette: _paletteProp(highContrast: highContrast, palette: palette), + ), + ) + .merge(style); +} + +/// {{typePrefix}} presentation for a Mix pie or donut chart. +/// +/// A positive [centerRadius] renders a donut. Labels are hidden by default so +/// category names can be presented in a caller-owned legend without forcing +/// low-contrast text onto arbitrary categorical colors. Generates +/// [{{typePrefix}}PieChart] through `mix_generator`. For advanced chart-level geometry, +/// pass this recipe directly to [PieChart.style] and merge a [PieSliceStyler]. +@MixWidget(target: PieChart.new) +PieChartStyler {{valuePrefix}}PieChartStyle({ + bool highContrast = false, + double centerRadius = 0, + bool showLabels = false, + List? palette, + PieChartStyler style = const PieChartStyler.create(), +}) { + final recipe = PieChartStyler() + .frame(_{{valuePrefix}}FrameStyle()) + .centerRadius(centerRadius) + .centerColor({{typePrefix}}Tokens.colorPanel()) + .sliceSpacing({{typePrefix}}Tokens.borderWidth2()) + .selectedSliceRadiusOffset({{typePrefix}}Tokens.space2()) + .slice( + PieSliceStyler() + .showLabel(showLabels) + .cornerRadius({{typePrefix}}Tokens.borderWidth2()) + .label( + TextStyler() + .style({{typePrefix}}Tokens.text1.mix()) + .fontWeight(.w600) + .color({{typePrefix}}Tokens.accentContrast()), + ), + ) + .tooltip(_{{valuePrefix}}TooltipStyle()); + + return recipe + .merge( + PieChartStyler.create( + palette: _paletteProp(highContrast: highContrast, palette: palette), + ), + ) + .merge(style); +} + +Prop> _paletteProp({ + required bool highContrast, + required List? palette, +}) { + if (palette != null) return Prop.value(List.unmodifiable(palette)); + + return Prop.token( + highContrast ? _highContrastPaletteToken : _standardPaletteToken, + ); +} + +ChartFrameStyler _{{valuePrefix}}FrameStyle() => ChartFrameStyler() + .backgroundColor(MixColors.transparent) + .showBorder(false) + .clip(true); + +ChartAxisStyler _{{valuePrefix}}AxisStyle() => ChartAxisStyler() + .showLabels(true) + .label( + TextStyler().style({{typePrefix}}Tokens.text1.mix()).color({{typePrefix}}Tokens.gray11()), + ) + .labelSpace({{typePrefix}}Tokens.space2()) + .fitInside(true) + .fitInsideDistance({{typePrefix}}Tokens.space1()) + .drawBelowEverything(true); + +ChartAxisStyler _hiddenAxisStyle() => ChartAxisStyler().showLabels(false); + +ChartGridStyler _{{valuePrefix}}GridStyle() => ChartGridStyler() + .show(true) + .showHorizontal(true) + .showVertical(false) + .stroke( + ChartStrokeStyler() + .color({{typePrefix}}Tokens.grayA5()) + .width({{typePrefix}}Tokens.borderWidth1()), + ); + +ChartTooltipStyler _{{valuePrefix}}TooltipStyle() => + ChartTooltipStyler.create( + border: Prop.token(_tooltipBorderToken), + borderRadius: Prop.token(_tooltipRadiusToken), + padding: Prop.token(_tooltipPaddingToken), + ) + .backgroundColor({{typePrefix}}Tokens.colorPanel()) + .margin({{typePrefix}}Tokens.space2()) + .maxWidth(280) + .fitHorizontally(true) + .fitVertically(true) + .text( + TextStyler() + .style({{typePrefix}}Tokens.text1.mix()) + .fontWeight(.w500) + .color({{typePrefix}}Tokens.gray12()), + ); + +List _resolveStandardPalette(BuildContext context) => + resolve{{typePrefix}}ChartPalette(context); + +List _resolveHighContrastPalette(BuildContext context) => + resolve{{typePrefix}}ChartPalette(context, highContrast: true); + +BorderSide _resolveTooltipBorder(BuildContext context) => BorderSide( + color: {{typePrefix}}Tokens.grayStroke6.resolve(context), + width: {{typePrefix}}Tokens.borderWidth1.resolve(context), +); + +BorderRadius _resolveTooltipRadius(BuildContext context) => + BorderRadius.all({{typePrefix}}Tokens.radius3.resolve(context)); + +EdgeInsets _resolveTooltipPadding(BuildContext context) => EdgeInsets.symmetric( + horizontal: {{typePrefix}}Tokens.space3.resolve(context), + vertical: {{typePrefix}}Tokens.space2.resolve(context), +); + +BorderRadius _resolveBarRadius(BuildContext context) => + BorderRadius.all({{typePrefix}}Tokens.radius2.resolve(context)); + +double _resolveLineWidth(BuildContext context) => + 2 * {{typePrefix}}Theme.of(context).scaling.factor; + +double _resolveHighContrastLineWidth(BuildContext context) => + 3 * {{typePrefix}}Theme.of(context).scaling.factor; diff --git a/packages/remix_cli/lib/src/registry/fortal/templates/checkbox/checkbox.dart.tmpl b/packages/remix_cli/lib/src/registry/fortal/templates/checkbox/checkbox.dart.tmpl new file mode 100644 index 000000000..f86e69909 --- /dev/null +++ b/packages/remix_cli/lib/src/registry/fortal/templates/checkbox/checkbox.dart.tmpl @@ -0,0 +1,258 @@ +import 'package:flutter/widgets.dart'; +import 'package:mix_annotations/mix_annotations.dart'; +import 'package:remix/remix.dart'; + +import '../theme/theme.dart'; + +part 'checkbox.g.dart'; + +/// Radix Themes Checkbox size presets. +enum {{typePrefix}}CheckboxSize { size1, size2, size3 } + +/// Radix Themes Checkbox variants. +enum {{typePrefix}}CheckboxVariant { classic, surface, soft } + +/// {{typePrefix}} recipe for [RemixCheckbox]. +@MixWidget(target: RemixCheckbox.new) +CheckboxStyler {{valuePrefix}}CheckboxStyle({ + {{typePrefix}}CheckboxVariant variant = .surface, + {{typePrefix}}CheckboxSize size = .size2, + bool highContrast = false, + CheckboxStyler style = const CheckboxStyler.create(), +}) { + final metrics = _{{valuePrefix}}CheckboxMetrics(size); + final base = + CheckboxStyler( + container: .size( + metrics.size, + metrics.size, + ).alignment(.center).borderRadius(.all(metrics.radius)), + indicator: .size(metrics.indicatorSize), + containerEffects: RemixBoxEffectsMix( + behindContent: RemixBoxEffectLayerMix(), + overContent: RemixBoxEffectLayerMix(), + ), + ).onFocusVisible( + .containerEffects( + RemixBoxEffectsMix( + outline: BorderSideMix( + color: {{typePrefix}}Tokens.focus8(), + width: 2, + strokeAlign: BorderSide.strokeAlignInside, + ), + outlineOffset: 2, + ), + ), + ); + + return (switch (variant) { + .classic => _{{valuePrefix}}CheckboxClassic(base, highContrast), + .surface => _{{valuePrefix}}CheckboxSurface(base, highContrast), + .soft => _{{valuePrefix}}CheckboxSoft(base, highContrast), + }).merge(style); +} + +/// {{typePrefix}} recipe for [RemixCheckboxGroupItem]. +/// +/// Combines the mapped checkbox recipe with Radix's size-linked item label +/// typography and `0.5em` label gap. The behavioral group remains layout +/// transparent, so callers continue to own root direction and spacing. +/// +/// It exists because `RemixCheckboxGroup` is behavioral and carries no styler, +/// so unlike every other Remix item (menu, select, segmented control, toggle +/// group) there is no parent recipe to push item styling down. Without this, +/// callers hand-attach a styler to each item and a missed one in a loop renders +/// unstyled beside its styled siblings. +@MixWidget(target: RemixCheckboxGroupItem.new) +CheckboxStyler {{valuePrefix}}CheckboxGroupItemStyle({ + {{typePrefix}}CheckboxVariant variant = .surface, + {{typePrefix}}CheckboxSize size = .size2, + bool highContrast = false, + CheckboxStyler style = const CheckboxStyler.create(), +}) { + final checkbox = {{valuePrefix}}CheckboxStyle( + variant: variant, + size: size, + highContrast: highContrast, + ); + + return (switch (size) { + .size1 => + checkbox + .label(.style({{typePrefix}}Tokens.text1.mix())) + .labelSpacing({{typePrefix}}Tokens.checkboxGroupItemGap1()), + .size2 => + checkbox + .label(.style({{typePrefix}}Tokens.text2.mix())) + .labelSpacing({{typePrefix}}Tokens.checkboxGroupItemGap2()), + .size3 => + checkbox + .label(.style({{typePrefix}}Tokens.text3.mix())) + .labelSpacing({{typePrefix}}Tokens.checkboxGroupItemGap3()), + }).merge(style); +} + +({double size, double indicatorSize, Radius radius}) _{{valuePrefix}}CheckboxMetrics( + {{typePrefix}}CheckboxSize size, +) => switch (size) { + .size1 => ( + size: {{typePrefix}}Tokens.checkboxSize1(), + indicatorSize: {{typePrefix}}Tokens.checkboxIndicatorSize1(), + radius: {{typePrefix}}Tokens.checkboxRadius1(), + ), + .size2 => ( + size: {{typePrefix}}Tokens.space4(), + indicatorSize: {{typePrefix}}Tokens.checkboxIndicatorSize2(), + radius: {{typePrefix}}Tokens.radius1(), + ), + .size3 => ( + size: {{typePrefix}}Tokens.checkboxSize3(), + indicatorSize: {{typePrefix}}Tokens.checkboxIndicatorSize3(), + radius: {{typePrefix}}Tokens.checkboxRadius3(), + ), +}; + +CheckboxStyler _{{valuePrefix}}CheckboxSurface(CheckboxStyler base, bool highContrast) { + final selected = CheckboxStyler() + .color( + highContrast ? {{typePrefix}}Tokens.accent12() : {{typePrefix}}Tokens.accentIndicator(), + ) + .containerEffects( + RemixBoxEffectsMix.behindContent(RemixBoxEffectLayerMix()), + ) + .containerEffects( + RemixBoxEffectsMix.overContent( + RemixBoxEffectLayerMix(shadows: const []), + ), + ) + .indicatorColor( + highContrast ? {{typePrefix}}Tokens.accent1() : {{typePrefix}}Tokens.accentContrast(), + ); + + return base + .color({{typePrefix}}Tokens.colorSurface()) + .containerEffects( + RemixBoxEffectsMix.behindContent(RemixBoxEffectLayerMix()), + ) + .containerEffects( + RemixBoxEffectsMix.overContent( + _{{valuePrefix}}CheckboxInsetRing({{typePrefix}}Tokens.grayA7()), + ), + ) + .onSelected(selected) + .onIndeterminate(selected) + .onDisabled( + .color({{typePrefix}}Tokens.grayA3()) + .containerEffects( + RemixBoxEffectsMix.behindContent(RemixBoxEffectLayerMix()), + ) + .containerEffects( + RemixBoxEffectsMix.overContent( + _{{valuePrefix}}CheckboxInsetRing({{typePrefix}}Tokens.grayA6()), + ), + ) + .indicatorColor({{typePrefix}}Tokens.grayA8()), + ); +} + +CheckboxStyler _{{valuePrefix}}CheckboxClassic(CheckboxStyler base, bool highContrast) { + final selected = CheckboxStyler() + .color( + highContrast ? {{typePrefix}}Tokens.accent12() : {{typePrefix}}Tokens.accentIndicator(), + ) + .containerEffects( + RemixBoxEffectsMix.behindContent( + RemixBoxEffectLayerMix( + gradients: [ + RemixLinearGradientMix( + colors: [ + {{typePrefix}}Tokens.whiteA3(), + const Color(0x00000000), + {{typePrefix}}Tokens.blackA1(), + ], + ), + ], + shadows: [ + RemixBoxShadowMix( + kind: .inset, + color: {{typePrefix}}Tokens.whiteA4(), + offset: const Offset(0, 0.5), + blurRadius: 0.5, + ), + RemixBoxShadowMix( + kind: .inset, + color: {{typePrefix}}Tokens.blackA4(), + offset: const Offset(0, -0.5), + blurRadius: 0.5, + ), + ], + ), + ), + ) + .containerEffects( + RemixBoxEffectsMix.overContent( + RemixBoxEffectLayerMix(shadows: const []), + ), + ) + .indicatorColor( + highContrast ? {{typePrefix}}Tokens.accent1() : {{typePrefix}}Tokens.accentContrast(), + ); + + return base + .color({{typePrefix}}Tokens.colorSurface()) + .containerEffects( + RemixBoxEffectsMix.behindContent( + RemixBoxEffectLayerMix(shadowToken: {{typePrefix}}Tokens.shadow1Layers), + ), + ) + .containerEffects( + RemixBoxEffectsMix.overContent( + _{{valuePrefix}}CheckboxInsetRing({{typePrefix}}Tokens.grayA3()), + ), + ) + .onSelected(selected) + .onIndeterminate(selected) + .onDisabled( + .color({{typePrefix}}Tokens.grayA3()) + .containerEffects( + RemixBoxEffectsMix.behindContent( + RemixBoxEffectLayerMix( + gradients: const [], + shadowToken: {{typePrefix}}Tokens.shadow1Layers, + ), + ), + ) + .containerEffects( + RemixBoxEffectsMix.overContent( + RemixBoxEffectLayerMix(shadows: const []), + ), + ) + .indicatorColor({{typePrefix}}Tokens.grayA8()), + ); +} + +CheckboxStyler _{{valuePrefix}}CheckboxSoft(CheckboxStyler base, bool highContrast) { + final selected = CheckboxStyler().indicatorColor( + highContrast ? {{typePrefix}}Tokens.accent12() : {{typePrefix}}Tokens.accentA11(), + ); + + return base + .color({{typePrefix}}Tokens.accentA5()) + .containerEffects( + RemixBoxEffectsMix.behindContent(RemixBoxEffectLayerMix()), + ) + .onSelected(selected) + .onIndeterminate(selected) + .onDisabled( + .color({{typePrefix}}Tokens.grayA3()) + .containerEffects( + RemixBoxEffectsMix.behindContent(RemixBoxEffectLayerMix()), + ) + .indicatorColor({{typePrefix}}Tokens.grayA8()), + ); +} + +RemixBoxEffectLayerMix _{{valuePrefix}}CheckboxInsetRing(Color color) => + RemixBoxEffectLayerMix( + shadows: [RemixBoxShadowMix(kind: .inset, color: color, spreadRadius: 1)], + ); diff --git a/packages/remix_cli/lib/src/registry/fortal/templates/code/code.dart.tmpl b/packages/remix_cli/lib/src/registry/fortal/templates/code/code.dart.tmpl new file mode 100644 index 000000000..c8b832226 --- /dev/null +++ b/packages/remix_cli/lib/src/registry/fortal/templates/code/code.dart.tmpl @@ -0,0 +1,300 @@ +import 'dart:math' as math; + +import 'package:flutter/widgets.dart'; +import 'package:remix/remix.dart'; + +import '../theme/theme.dart'; +import 'typography.dart'; + +/// Radix Themes Code variants. +enum {{typePrefix}}CodeVariant { solid, soft, outline, ghost } + +/// {{typePrefix}}-themed inline code on the Radix nine-step scale. +/// +/// Geometry is em-relative to the resolved font size, so this recipe takes a +/// [context]. An omitted [size] anchors to the root `text3` token — not the +/// ambient `DefaultTextStyle` — while keeping upstream's separate unsized +/// factors, so a host text run cannot change Code's geometry. +BadgeStyler {{valuePrefix}}CodeStyle( + BuildContext context, { + {{typePrefix}}TextSize? size, + {{typePrefix}}CodeVariant variant = .soft, + {{typePrefix}}TextWeight? weight, + bool softWrap = true, + bool truncate = false, + bool accent = false, + bool highContrast = false, + BadgeStyler style = const BadgeStyler.create(), +}) { + final base = {{valuePrefix}}ResolveTextToken(context, size ?? {{typePrefix}}TextSize.size3); + final baseFontSize = base.fontSize!; + + // Radix nests two adjustments: --code-font-size-adjust is 0.95, and + // --code-variant-font-size-adjust multiplies it by 0.95 again for every + // variant except ghost, which keeps the outer value. + final decorated = variant != .ghost; + final fontSize = baseFontSize * (decorated ? 0.95 * 0.95 : 0.95); + // An explicit size keeps its token's absolute line box; the unsized path + // uses the pinned unitless 1.25. + final lineHeight = size == null + ? 1.25 + : (baseFontSize * (base.height ?? 1)) / fontSize; + final letterSpacing = (base.letterSpacing ?? 0) - (0.007 * fontSize); + + var textStyle = TextStyler() + .fontFamily('Menlo') + .fontFamilyFallback(const [ + 'Consolas', + 'Bitstream Vera Sans Mono', + 'monospace', + 'Apple Color Emoji', + 'Segoe UI Emoji', + ]) + .fontSize(fontSize) + .height(lineHeight) + .letterSpacing(letterSpacing) + .inherit(false); + if (weight != null) { + textStyle = textStyle.fontWeight({{valuePrefix}}TextWeightToken(weight)()); + } + textStyle = {{valuePrefix}}ApplyTextFlow( + textStyle, + softWrap: softWrap, + truncate: truncate, + ); + + Color? fill; + Color? foreground; + final accent1 = {{valuePrefix}}ResolveColor(context, {{typePrefix}}Tokens.accent1); + final accent12 = {{valuePrefix}}ResolveColor(context, {{typePrefix}}Tokens.accent12); + final accentA3 = {{valuePrefix}}ResolveColor(context, {{typePrefix}}Tokens.accentA3); + final accentA9 = {{valuePrefix}}ResolveColor(context, {{typePrefix}}Tokens.accentA9); + final accentA11 = {{valuePrefix}}ResolveColor(context, {{typePrefix}}Tokens.accentA11); + final accentContrast = {{valuePrefix}}ResolveColor( + context, + {{typePrefix}}Tokens.accentContrast, + ); + switch (variant) { + case .solid: + fill = highContrast ? accent12 : accentA9; + foreground = highContrast ? accent1 : accentContrast; + case .soft: + fill = accentA3; + foreground = highContrast ? accent12 : accentA11; + case .outline: + foreground = highContrast ? accent12 : accentA11; + case .ghost: + // Ghost is transparent and inherits the ambient colour unless the caller + // opts into the local accent. Apply only that intended ambient field + // after Mix composition so an explicit recipe colour or foreground can + // override it without creating an invalid Flutter TextStyle. + if (accent) { + foreground = highContrast ? accent12 : accentA11; + } else { + textStyle = textStyle.merge( + TextStyler.create( + style: Prop.directives([ + _AmbientCodeForegroundDirective( + DefaultTextStyle.of(context).style, + ), + ]), + ), + ); + } + } + if (foreground != null) textStyle = textStyle.color(foreground); + + var recipe = BadgeStyler() + .label(textStyle) + .borderRadius( + BorderRadiusGeometryMix.circular( + (0.5 + 0.2 * fontSize) * {{valuePrefix}}RadiusFactor(context), + ), + ); + if (decorated) { + recipe = recipe.padding( + EdgeInsetsGeometryMix.symmetric( + horizontal: 0.25 * fontSize, + vertical: 0.10 * fontSize, + ), + ); + } + if (fill != null) recipe = recipe.color(fill); + + if (variant == .outline) { + final ringWidth = math.max(1.0, 0.033 * fontSize); + recipe = recipe.containerEffects( + RemixBoxEffectsMix.behindContent( + RemixBoxEffectLayerMix( + shadows: [ + RemixBoxShadowMix( + kind: .inset, + color: {{valuePrefix}}ResolveColor( + context, + highContrast ? {{typePrefix}}Tokens.accentA7 : {{typePrefix}}Tokens.accentA8, + ), + spreadRadius: ringWidth, + ), + if (highContrast) + RemixBoxShadowMix( + kind: .inset, + color: {{valuePrefix}}ResolveColor(context, {{typePrefix}}Tokens.grayA11), + spreadRadius: ringWidth, + ), + ], + ), + ), + ); + } + + return recipe.merge(style); +} + +final class _AmbientCodeForegroundDirective extends Directive { + _AmbientCodeForegroundDirective(TextStyle ambient) + : color = ambient.color, + foreground = ambient.foreground; + + final Color? color; + final Paint? foreground; + + @override + String get key => '{{valuePrefix}}_code_ambient_foreground'; + + @override + TextStyle apply(TextStyle style) { + final hasAmbientFallback = _ambientCodeForegroundFallbacks[style] ?? false; + if (!hasAmbientFallback && + (style.color != null || style.foreground != null)) { + return style; + } + if (color == null && foreground == null) return style; + + late final TextStyle result; + if (foreground case final paint?) { + result = style.copyWith(foreground: paint); + } else if (style.foreground != null) { + // Mix concatenates directives when recipes merge. If an earlier fallback + // supplied a Paint, copyWith cannot clear it in favour of a Color. Keep + // the equivalent Paint representation so the later recipe still wins + // without producing an invalid TextStyle(color:, foreground:). + result = style.copyWith(foreground: Paint()..color = color!); + } else { + result = style.copyWith(color: color); + } + + // Expando keeps provenance out of TextStyle's visual and diagnostic + // fields, works with assertions disabled, and does not retain resolved + // styles after Mix finishes applying the directive list. + _ambientCodeForegroundFallbacks[result] = true; + return result; + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is _AmbientCodeForegroundDirective && + other.color == color && + other.foreground == foreground; + + @override + int get hashCode => Object.hash(color, foreground); +} + +final _ambientCodeForegroundFallbacks = Expando( + '{{valuePrefix}}_code_ambient_foreground', +); + +/// Token-backed standalone code text with the Radix Code variants. +/// +/// Code carries no accessibility role: Flutter has no code semantics, and +/// inventing one would misreport the content. +class {{typePrefix}}Code extends StatelessWidget { + const {{typePrefix}}Code( + this.text, { + super.key, + this.size, + this.variant = {{typePrefix}}CodeVariant.soft, + this.weight, + this.softWrap = true, + this.truncate = false, + this.accent = false, + this.highContrast = false, + this.style = const BadgeStyler.create(), + }) : assert(text != ''); + + const {{typePrefix}}Code.solid( + this.text, { + super.key, + this.size, + this.weight, + this.softWrap = true, + this.truncate = false, + this.accent = false, + this.highContrast = false, + this.style = const BadgeStyler.create(), + }) : variant = {{typePrefix}}CodeVariant.solid, + assert(text != ''); + + const {{typePrefix}}Code.soft( + this.text, { + super.key, + this.size, + this.weight, + this.softWrap = true, + this.truncate = false, + this.accent = false, + this.highContrast = false, + this.style = const BadgeStyler.create(), + }) : variant = {{typePrefix}}CodeVariant.soft, + assert(text != ''); + + const {{typePrefix}}Code.outline( + this.text, { + super.key, + this.size, + this.weight, + this.softWrap = true, + this.truncate = false, + this.accent = false, + this.highContrast = false, + this.style = const BadgeStyler.create(), + }) : variant = {{typePrefix}}CodeVariant.outline, + assert(text != ''); + + const {{typePrefix}}Code.ghost( + this.text, { + super.key, + this.size, + this.weight, + this.softWrap = true, + this.truncate = false, + this.accent = false, + this.highContrast = false, + this.style = const BadgeStyler.create(), + }) : variant = {{typePrefix}}CodeVariant.ghost, + assert(text != ''); + + final String text; + final {{typePrefix}}TextSize? size; + final {{typePrefix}}CodeVariant variant; + final {{typePrefix}}TextWeight? weight; + final bool softWrap; + final bool truncate; + final bool accent; + final bool highContrast; + final BadgeStyler style; + + @override + Widget build(BuildContext context) => {{valuePrefix}}CodeStyle( + context, + size: size, + variant: variant, + weight: weight, + softWrap: softWrap, + truncate: truncate, + accent: accent, + highContrast: highContrast, + style: style, + )(label: text); +} diff --git a/packages/remix_cli/lib/src/registry/fortal/templates/data_list/data_list.dart.tmpl b/packages/remix_cli/lib/src/registry/fortal/templates/data_list/data_list.dart.tmpl new file mode 100644 index 000000000..75277fc4e --- /dev/null +++ b/packages/remix_cli/lib/src/registry/fortal/templates/data_list/data_list.dart.tmpl @@ -0,0 +1,52 @@ +import 'package:flutter/widgets.dart'; +import 'package:mix_annotations/mix_annotations.dart'; +import 'package:remix/remix.dart'; + +import '../theme/theme.dart'; + +part 'data_list.g.dart'; + +/// Radix Themes DataList size presets. +enum {{typePrefix}}DataListSize { size1, size2, size3 } + +/// {{typePrefix}} recipe for [RemixDataList]. +@MixWidget(target: RemixDataList.new) +DataListStyler {{valuePrefix}}DataListStyle({ + {{typePrefix}}DataListSize size = .size2, + bool highContrast = false, + DataListStyler style = const DataListStyler.create(), +}) { + final metrics = _{{valuePrefix}}DataListMetrics(size); + + return DataListStyler() + .label( + TextStyler() + .style(metrics.text.mix()) + .fontWeight({{typePrefix}}Tokens.fontWeightRegular()) + .color( + highContrast ? {{typePrefix}}Tokens.gray12() : {{typePrefix}}Tokens.grayA11(), + ), + ) + .value( + TextStyler() + .style(metrics.text.mix()) + .fontWeight({{typePrefix}}Tokens.fontWeightRegular()) + .color({{typePrefix}}Tokens.gray12()), + ) + .rowSpacing(metrics.rowSpacing) + .columnSpacing(metrics.rowSpacing) + .labelValueSpacing({{typePrefix}}Tokens.space1()) + .minLabelWidth({{typePrefix}}Tokens.dataListLabelMinWidth()) + .merge(style); +} + +({TextStyleToken text, double rowSpacing}) _{{valuePrefix}}DataListMetrics( + {{typePrefix}}DataListSize size, +) => switch (size) { + .size1 => (text: {{typePrefix}}Tokens.text1, rowSpacing: {{typePrefix}}Tokens.space3()), + .size2 => (text: {{typePrefix}}Tokens.text2, rowSpacing: {{typePrefix}}Tokens.space4()), + .size3 => ( + text: {{typePrefix}}Tokens.text3, + rowSpacing: {{typePrefix}}Tokens.dataListRowGap3(), + ), +}; diff --git a/packages/remix_cli/lib/src/registry/fortal/templates/data_table/data_table.dart.tmpl b/packages/remix_cli/lib/src/registry/fortal/templates/data_table/data_table.dart.tmpl new file mode 100644 index 000000000..1302d7cdb --- /dev/null +++ b/packages/remix_cli/lib/src/registry/fortal/templates/data_table/data_table.dart.tmpl @@ -0,0 +1,173 @@ +import 'package:flutter/widgets.dart'; +import 'package:mix_annotations/mix_annotations.dart'; +import 'package:remix/remix.dart'; + +import '../theme/theme.dart'; +import 'checkbox.dart'; +import 'icon_button.dart'; +import 'select.dart'; + +part 'data_table.g.dart'; + +/// Radix Themes Table size presets. +enum {{typePrefix}}DataTableSize { size1, size2, size3 } + +/// Radix Themes Table variants. +enum {{typePrefix}}DataTableVariant { surface, ghost } + +/// Resolved Radix `table.css` metrics for one size step. +typedef _{{typePrefix}}DataTableMetrics = ({ + double paddingX, + double paddingY, + double minHeight, + double sortIconSize, + Radius radius, + TextStyleToken text, +}); + +/// {{typePrefix}} recipe for [RemixDataTable]. +/// +/// Sizes and variants map `@radix-ui/themes@3.3.0` `table.css` exactly: cell +/// padding, minimum cell height, typography, radius, the `gray-a5` row +/// divider, bold column headers, the surface panel/border, the `gray-a2` +/// header background, and the suppressed divider under a surface table's last +/// row. +/// +/// Sorting, selection, pagination, and row hover have no Radix counterpart — +/// Radix's Table is a passive layout. They are {{typePrefix}} extensions built from +/// existing accent/gray control tokens and are recorded as extensions in the +/// parity manifest. +@MixWidget(target: RemixDataTable.new) +DataTableStyler {{valuePrefix}}DataTableStyle({ + {{typePrefix}}DataTableSize size = .size2, + {{typePrefix}}DataTableVariant variant = .ghost, + DataTableStyler style = const DataTableStyler.create(), +}) { + final metrics = _{{valuePrefix}}DataTableMetrics(size); + final base = DataTableStyler() + .cellText( + TextStyler(style: metrics.text.mix()).color({{typePrefix}}Tokens.gray12()), + ) + .headerLabel( + TextStyler(style: metrics.text.mix()) + .fontWeight({{typePrefix}}Tokens.fontWeightBold()) + .color({{typePrefix}}Tokens.gray12()), + ) + .footerLabel( + TextStyler(style: {{typePrefix}}Tokens.text1.mix()) + .fontWeight({{typePrefix}}Tokens.fontWeightRegular()) + .color({{typePrefix}}Tokens.gray11()), + ) + .headerCell(_{{valuePrefix}}DataTableCell(metrics)) + .bodyCell(_{{valuePrefix}}DataTableCell(metrics)) + // The selection column is a {{typePrefix}} extension with no Radix counterpart. + // It carries no padding of its own, so the composed checkbox's + // interaction target — sized to this cell — spans the whole column and + // the full row height instead of being inset from both. + .selectionCell(BoxStyler().alignment(Alignment.center)) + .headerMinHeight(metrics.minHeight) + .rowMinHeight(metrics.minHeight) + .selectionColumnWidth({{typePrefix}}Tokens.space8()) + .sortIconSpacing({{typePrefix}}Tokens.space1()) + .sortIcon( + IconStyler(color: {{typePrefix}}Tokens.gray11(), size: metrics.sortIconSize), + ) + .headerRow(_{{valuePrefix}}DataTableRowDivider()) + .bodyRow( + _{{valuePrefix}}DataTableRowDivider() + .color(const Color(0x00000000)) + // Hover and selection are {{typePrefix}} extensions. Both are pure color + // layers, so a row never changes geometry when either applies. + .onHovered(.color({{typePrefix}}Tokens.grayA3())) + .onSelected( + .color( + {{typePrefix}}Tokens.accentA3(), + ).onHovered(.color({{typePrefix}}Tokens.accentA4())), + ), + ) + .footer(_{{valuePrefix}}DataTableFooter()) + .selectionCheckbox({{valuePrefix}}CheckboxStyle(size: .size1)) + .pageButton({{valuePrefix}}IconButtonStyle(variant: .ghost, size: .size1)) + .pageSizeSelect({{valuePrefix}}SelectStyle(variant: .ghost, size: .size1)); + + return (switch (variant) { + .surface => _{{valuePrefix}}DataTableSurface(base, metrics.radius), + .ghost => base.color(const Color(0x00000000)), + }).merge(style); +} + +_{{typePrefix}}DataTableMetrics _{{valuePrefix}}DataTableMetrics({{typePrefix}}DataTableSize size) => + switch (size) { + .size1 => ( + paddingX: {{typePrefix}}Tokens.space2(), + paddingY: {{typePrefix}}Tokens.space2(), + minHeight: {{typePrefix}}Tokens.dataTableRowHeight1(), + sortIconSize: 14.0, + radius: {{typePrefix}}Tokens.radius3(), + text: {{typePrefix}}Tokens.text2, + ), + .size2 => ( + paddingX: {{typePrefix}}Tokens.space3(), + paddingY: {{typePrefix}}Tokens.space3(), + minHeight: {{typePrefix}}Tokens.dataTableRowHeight2(), + sortIconSize: 16.0, + radius: {{typePrefix}}Tokens.radius4(), + text: {{typePrefix}}Tokens.text2, + ), + .size3 => ( + paddingX: {{typePrefix}}Tokens.space4(), + paddingY: {{typePrefix}}Tokens.space3(), + minHeight: {{typePrefix}}Tokens.space8(), + sortIconSize: 18.0, + radius: {{typePrefix}}Tokens.radius4(), + text: {{typePrefix}}Tokens.text3, + ), + }; + +BoxStyler _{{valuePrefix}}DataTableCell(_{{typePrefix}}DataTableMetrics metrics) => BoxStyler() + .padding(.horizontal(metrics.paddingX)) + .padding(.vertical(metrics.paddingY)); + +/// Radix draws the row divider as `inset 0 -1px var(--gray-a5)`, which paints +/// over the cell without reserving layout space. A foreground border is the +/// Flutter equivalent; a regular border would inset the cell content by 1px. +BoxStyler _{{valuePrefix}}DataTableRowDivider() => BoxStyler().foregroundDecoration( + BoxDecorationMix(border: BoxBorderMix.bottom(_{{valuePrefix}}DataTableDividerSide())), +); + +/// The `gray-a5` 1px edge shared by the row divider and the footer's top +/// border, so the footer reads as a continuation of the last row's divider. +BorderSideMix _{{valuePrefix}}DataTableDividerSide() => + BorderSideMix(color: {{typePrefix}}Tokens.grayA5(), width: 1); + +FlexBoxStyler _{{valuePrefix}}DataTableFooter() => FlexBoxStyler() + .direction(.horizontal) + .spacing({{typePrefix}}Tokens.space2()) + .padding(.horizontal({{typePrefix}}Tokens.space4())) + .padding(.vertical({{typePrefix}}Tokens.space2())) + .foregroundDecoration( + BoxDecorationMix(border: BoxBorderMix.top(_{{valuePrefix}}DataTableDividerSide())), + ); + +DataTableStyler _{{valuePrefix}}DataTableSurface(DataTableStyler base, Radius radius) { + return base + .container( + {{valuePrefix}}SurfaceFrame( + fillColor: {{typePrefix}}Tokens.colorPanel(), + borderColor: {{typePrefix}}Tokens.dataTableBorder(), + borderWidth: {{typePrefix}}Tokens.borderWidth1(), + radius: radius, + ), + ) + .containerEffects( + RemixBoxEffectsMix.backdropBlur({{typePrefix}}Tokens.panelBlur()), + ) + .headerRow(.color({{typePrefix}}Tokens.grayA2())) + // Radix clears `--table-row-box-shadow` on the surface variant's last + // row so its divider never doubles up with the panel border. + .lastBodyRow( + BoxStyler().foregroundDecoration( + BoxDecorationMix(border: BoxBorderMix.bottom(BorderSideMix.none)), + ), + ); +} diff --git a/packages/remix_cli/lib/src/registry/fortal/templates/dialog/dialog.dart.tmpl b/packages/remix_cli/lib/src/registry/fortal/templates/dialog/dialog.dart.tmpl new file mode 100644 index 000000000..9f1dc8bb7 --- /dev/null +++ b/packages/remix_cli/lib/src/registry/fortal/templates/dialog/dialog.dart.tmpl @@ -0,0 +1,96 @@ +import 'dart:math' as math; + +import 'package:flutter/widgets.dart'; +import 'package:mix_annotations/mix_annotations.dart'; +import 'package:remix/remix.dart'; + +import '../theme/theme.dart'; + +part 'dialog.g.dart'; + +/// {{typePrefix}} dialog size presets matching Radix Themes 3.3.0. +enum {{typePrefix}}DialogSize { size1, size2, size3, size4 } + +/// {{typePrefix}} dialog vertical alignment matching Radix Themes 3.3.0. +enum {{typePrefix}}DialogAlign { start, center } + +final _dialogViewportInsets = ContextToken((context) { + final safeArea = MediaQuery.paddingOf(context); + final viewportHeight = MediaQuery.sizeOf(context).height; + final horizontal = {{typePrefix}}Tokens.space4.resolve(context); + final vertical = {{typePrefix}}Tokens.space6.resolve(context); + + return EdgeInsets.fromLTRB( + math.max(safeArea.left, horizontal), + math.max(safeArea.top, vertical), + math.max(safeArea.right, horizontal), + math.max(safeArea.bottom, math.max(vertical, viewportHeight * 0.06)), + ); +}); + +/// {{typePrefix}}-themed preset for [RemixDialog]. +/// +/// The generated [{{typePrefix}}Dialog] defaults to [{{typePrefix}}DialogSize.size3], +/// [{{typePrefix}}DialogAlign.center], fills up to 600 logical pixels, preserves safe +/// viewport insets, and is modal. +@MixWidget(target: RemixDialog.new) +DialogStyler {{valuePrefix}}DialogStyle({ + {{typePrefix}}DialogSize size = {{typePrefix}}DialogSize.size3, + {{typePrefix}}DialogAlign align = {{typePrefix}}DialogAlign.center, + DialogStyler style = const DialogStyler.create(), +}) { + final radius = switch (size) { + {{typePrefix}}DialogSize.size1 || {{typePrefix}}DialogSize.size2 => {{typePrefix}}Tokens.radius4(), + {{typePrefix}}DialogSize.size3 || {{typePrefix}}DialogSize.size4 => {{typePrefix}}Tokens.radius5(), + }; + final padding = switch (size) { + {{typePrefix}}DialogSize.size1 => {{typePrefix}}Tokens.space3(), + {{typePrefix}}DialogSize.size2 => {{typePrefix}}Tokens.space4(), + {{typePrefix}}DialogSize.size3 => {{typePrefix}}Tokens.space5(), + {{typePrefix}}DialogSize.size4 => {{typePrefix}}Tokens.space6(), + }; + final alignment = switch (align) { + {{typePrefix}}DialogAlign.start => Alignment.topCenter, + {{typePrefix}}DialogAlign.center => Alignment.center, + }; + + return DialogStyler() + .wrap( + .modifier( + PaddingModifierMix.create(padding: Prop.token(_dialogViewportInsets)), + ).align(alignment: alignment).orderOfModifiers([ + PaddingModifier, + AlignModifier, + ]), + ) + .title( + .style({{typePrefix}}Tokens.text5.mix()) + .fontWeight({{typePrefix}}Tokens.fontWeightBold()) + .color({{typePrefix}}Tokens.gray12()) + .wrap( + .padding(EdgeInsetsMix.fromLTRB(0, 0, 0, {{typePrefix}}Tokens.space3())), + ), + ) + .description( + TextStyler( + style: {{typePrefix}}Tokens.text3.mix(), + ).color({{typePrefix}}Tokens.gray12()), + ) + .actions( + FlexBoxStyler() + .mainAxisAlignment(.end) + .spacing({{typePrefix}}Tokens.space3()) + .margin(.top({{typePrefix}}Tokens.space5())), + ) + .width(600) + .padding(.all(padding)) + .borderRadius(.all(radius)) + .color({{typePrefix}}Tokens.colorPanel()) + .decoration( + BoxDecorationMix.create(boxShadow: {{typePrefix}}Tokens.shadow6.mix()), + ) + .containerEffects( + RemixBoxEffectsMix.backdropBlur({{typePrefix}}Tokens.panelBlur()), + ) + .merge(style); +} diff --git a/packages/remix_cli/lib/src/registry/fortal/templates/disclosure/disclosure.dart.tmpl b/packages/remix_cli/lib/src/registry/fortal/templates/disclosure/disclosure.dart.tmpl new file mode 100644 index 000000000..a81eb2406 --- /dev/null +++ b/packages/remix_cli/lib/src/registry/fortal/templates/disclosure/disclosure.dart.tmpl @@ -0,0 +1,194 @@ +import 'package:flutter/widgets.dart'; +import 'package:mix_annotations/mix_annotations.dart'; +import 'package:remix/remix.dart'; + +import '../theme/theme.dart'; + +part 'disclosure.g.dart'; + +/// {{typePrefix}} disclosure size presets. +enum {{typePrefix}}DisclosureSize { size1, size2, size3 } + +/// {{typePrefix}} disclosure color variants. +enum {{typePrefix}}DisclosureVariant { surface, soft } + +/// {{typePrefix}}-themed preset for [RemixDisclosure]. +@MixWidget(target: RemixDisclosure.new) +DisclosureStyler {{valuePrefix}}DisclosureStyle({ + {{typePrefix}}DisclosureVariant variant = .surface, + {{typePrefix}}DisclosureSize size = .size2, + DisclosureStyler style = const DisclosureStyler.create(), +}) { + return (switch (variant) { + .surface => _{{valuePrefix}}DisclosureSurfaceStyler(size), + .soft => _{{valuePrefix}}DisclosureSoftStyler(size), + }).merge(style); +} + +// Panel anatomy follows the mapped Table family (see data_table.dart): +// `container` alone owns radius, frame, fill, and clipping, while trigger and +// content stay flat rectangles that simply get cropped to its rounded shape. +// The frame and divider are foreground borders so edge-to-edge child fills +// cannot partially cover their antialiased edges. +DisclosureStyler _{{valuePrefix}}DisclosureBaseStyler({{typePrefix}}DisclosureSize size) { + final metrics = _{{valuePrefix}}DisclosureMetrics(size); + + return DisclosureStyler() + .trigger( + BoxStyler() + .width(.infinity) + .alignment(.centerLeft) + .padding(.all(metrics.padding)) + .wrap( + _{{valuePrefix}}DisclosureTypography( + style: metrics.triggerText, + color: {{typePrefix}}Tokens.gray12(), + iconColor: {{typePrefix}}Tokens.gray11(), + iconSize: metrics.iconSize, + ), + ), + ) + .content( + BoxStyler() + .width(.infinity) + .padding(.all(metrics.padding)) + .wrap( + _{{valuePrefix}}DisclosureTypography( + style: {{typePrefix}}Tokens.text2, + color: {{typePrefix}}Tokens.gray12(), + iconColor: {{typePrefix}}Tokens.gray11(), + iconSize: metrics.iconSize, + ), + ), + ); +} + +DisclosureStyler _{{valuePrefix}}DisclosureSurfaceStyler({{typePrefix}}DisclosureSize size) { + final metrics = _{{valuePrefix}}DisclosureMetrics(size); + return _{{valuePrefix}}DisclosureBaseStyler(size) + .container( + {{valuePrefix}}SurfaceFrame( + fillColor: {{typePrefix}}Tokens.gray2(), + borderColor: {{typePrefix}}Tokens.gray6(), + borderWidth: {{typePrefix}}Tokens.borderWidth1(), + radius: metrics.radius, + ), + ) + .trigger(.color({{typePrefix}}Tokens.gray1())) + .content( + .foregroundDecoration( + BoxDecorationMix( + border: BoxBorderMix.top( + _{{valuePrefix}}DisclosureBorderSide({{typePrefix}}Tokens.gray6()), + ), + ), + ), + ) + .onHovered(.trigger(.color({{typePrefix}}Tokens.gray2()))) + .onPressed(.trigger(.color({{typePrefix}}Tokens.gray3()))) + .onFocusVisible(DisclosureStyler().{{valuePrefix}}FocusRing()) + .onDisabled(_{{valuePrefix}}DisclosureDisabledStyler()); +} + +DisclosureStyler _{{valuePrefix}}DisclosureSoftStyler({{typePrefix}}DisclosureSize size) { + final metrics = _{{valuePrefix}}DisclosureMetrics(size); + return _{{valuePrefix}}DisclosureBaseStyler(size) + .container( + {{valuePrefix}}SurfaceFrame( + fillColor: {{typePrefix}}Tokens.accent2(), + borderColor: {{typePrefix}}Tokens.accent6(), + borderWidth: {{typePrefix}}Tokens.borderWidth1(), + radius: metrics.radius, + ), + ) + .trigger( + BoxStyler() + .color({{typePrefix}}Tokens.accent2()) + .wrap( + _{{valuePrefix}}DisclosureForeground( + color: {{typePrefix}}Tokens.accent12(), + iconColor: {{typePrefix}}Tokens.accent11(), + ), + ), + ) + .content( + BoxStyler() + .foregroundDecoration( + BoxDecorationMix( + border: BoxBorderMix.top( + _{{valuePrefix}}DisclosureBorderSide({{typePrefix}}Tokens.accent6()), + ), + ), + ) + .wrap( + _{{valuePrefix}}DisclosureForeground( + color: {{typePrefix}}Tokens.accent12(), + iconColor: {{typePrefix}}Tokens.accent11(), + ), + ), + ) + .onHovered(.trigger(.color({{typePrefix}}Tokens.accent3()))) + .onPressed(.trigger(.color({{typePrefix}}Tokens.accent4()))) + .onFocusVisible(DisclosureStyler().{{valuePrefix}}FocusRing()) + .onDisabled(_{{valuePrefix}}DisclosureDisabledStyler()); +} + +DisclosureStyler _{{valuePrefix}}DisclosureDisabledStyler() { + return DisclosureStyler().trigger( + BoxStyler() + .color({{typePrefix}}Tokens.grayA3()) + .wrap( + _{{valuePrefix}}DisclosureForeground( + color: {{typePrefix}}Tokens.gray8(), + iconColor: {{typePrefix}}Tokens.gray8(), + ), + ), + ); +} + +BorderSideMix _{{valuePrefix}}DisclosureBorderSide(Color color) => + BorderSideMix(color: color, width: {{typePrefix}}Tokens.borderWidth1()); + +WidgetModifierConfig _{{valuePrefix}}DisclosureTypography({ + required TextStyleToken style, + required Color color, + required Color iconColor, + required double iconSize, +}) { + return WidgetModifierConfig.defaultTextStyle(style: style.mix()) + .defaultTextStyle(style: TextStyleMix().color(color)) + .merge(WidgetModifierConfig.iconTheme(color: iconColor, size: iconSize)); +} + +WidgetModifierConfig _{{valuePrefix}}DisclosureForeground({ + required Color color, + required Color iconColor, +}) { + return WidgetModifierConfig.defaultTextStyle( + style: TextStyleMix().color(color), + ).merge(WidgetModifierConfig.iconTheme(color: iconColor)); +} + +({double padding, Radius radius, TextStyleToken triggerText, double iconSize}) +_{{valuePrefix}}DisclosureMetrics({{typePrefix}}DisclosureSize size) { + return switch (size) { + .size1 => ( + padding: {{typePrefix}}Tokens.space2(), + radius: {{typePrefix}}Tokens.radius3(), + triggerText: {{typePrefix}}Tokens.text2, + iconSize: {{typePrefix}}Tokens.space4(), + ), + .size2 => ( + padding: {{typePrefix}}Tokens.space3(), + radius: {{typePrefix}}Tokens.radius4(), + triggerText: {{typePrefix}}Tokens.accordionText2, + iconSize: {{typePrefix}}Tokens.spinnerSize3(), + ), + .size3 => ( + padding: {{typePrefix}}Tokens.space4(), + radius: {{typePrefix}}Tokens.radius5(), + triggerText: {{typePrefix}}Tokens.text3, + iconSize: {{typePrefix}}Tokens.space5(), + ), + }; +} diff --git a/packages/remix_cli/lib/src/registry/fortal/templates/divider/divider.dart.tmpl b/packages/remix_cli/lib/src/registry/fortal/templates/divider/divider.dart.tmpl new file mode 100644 index 000000000..d6d374029 --- /dev/null +++ b/packages/remix_cli/lib/src/registry/fortal/templates/divider/divider.dart.tmpl @@ -0,0 +1,49 @@ +import 'package:flutter/widgets.dart'; +import 'package:mix_annotations/mix_annotations.dart'; +import 'package:remix/remix.dart'; + +import '../theme/theme.dart'; + +part 'divider.g.dart'; + +/// {{typePrefix}} divider length presets: 16, 32, 64, or the available axis extent. +enum {{typePrefix}}DividerSize { size1, size2, size3, size4 } + +/// {{typePrefix}}-themed preset for [RemixDivider]. +@MixWidget(target: RemixDivider.new) +DividerStyler {{valuePrefix}}DividerStyle({ + {{typePrefix}}DividerSize size = .size1, + Axis orientation = Axis.horizontal, + DividerStyler style = const DividerStyler.create(), +}) { + return DividerStyler() + .color({{typePrefix}}Tokens.gray6()) + .merge(_{{valuePrefix}}DividerSizeStyler(size, orientation)) + .merge(style); +} + +DividerStyler _{{valuePrefix}}DividerSizeStyler( + {{typePrefix}}DividerSize size, + Axis orientation, +) { + final length = switch (size) { + .size1 => {{typePrefix}}Tokens.space4(), + .size2 => {{typePrefix}}Tokens.space6(), + .size3 => {{typePrefix}}Tokens.space9(), + .size4 => null, + }; + if (orientation == Axis.horizontal) { + final style = DividerStyler().height({{typePrefix}}Tokens.borderWidth1()); + return length == null + ? style.wrap( + WidgetModifierConfig.fractionallySizedBox(widthFactor: 1).align(), + ) + : style.width(length); + } + final style = DividerStyler().width({{typePrefix}}Tokens.borderWidth1()); + return length == null + ? style.wrap( + WidgetModifierConfig.fractionallySizedBox(heightFactor: 1).align(), + ) + : style.height(length); +} diff --git a/packages/remix_cli/lib/src/registry/fortal/templates/heading/heading.dart.tmpl b/packages/remix_cli/lib/src/registry/fortal/templates/heading/heading.dart.tmpl new file mode 100644 index 000000000..2b62cab71 --- /dev/null +++ b/packages/remix_cli/lib/src/registry/fortal/templates/heading/heading.dart.tmpl @@ -0,0 +1,116 @@ +import 'package:flutter/widgets.dart'; +import 'package:remix/remix.dart'; + +import '../theme/theme.dart'; +import 'typography.dart'; + +/// {{typePrefix}}-themed heading style on the Radix nine-step scale. +/// +/// Radix's `--heading-font-size-adjust` is `1`, so headings use the raw token +/// size; only the line box differs from body text. Each ratio below is the +/// pinned Radix heading line height over its font size, so both scale together +/// and the ratio stays constant across theme scaling. +/// +/// This is a plain recipe rather than a `@MixWidget`: a generated widget only +/// renders the styler, and [{{typePrefix}}Heading] must additionally publish a native +/// heading node that generation cannot supply. +TextStyler {{valuePrefix}}HeadingStyle({ + {{typePrefix}}TextSize size = .size6, + {{typePrefix}}TextWeight weight = .bold, + TextAlign? align, + bool softWrap = true, + bool truncate = false, + bool accent = false, + bool highContrast = false, + TextStyler style = const TextStyler.create(), +}) { + final lineHeight = switch (size) { + .size1 => 16.0 / 12.0, + .size2 => 18.0 / 14.0, + .size3 => 22.0 / 16.0, + .size4 => 24.0 / 18.0, + .size5 => 26.0 / 20.0, + .size6 => 30.0 / 24.0, + .size7 => 36.0 / 28.0, + .size8 => 40.0 / 35.0, + .size9 => 1.0, + }; + + var recipe = TextStyler( + style: {{valuePrefix}}TextSizeToken(size).mix(), + ).height(lineHeight).fontWeight({{valuePrefix}}TextWeightToken(weight)()); + // Neutral headings pin `gray12` from the tokens rather than inheriting the + // ambient foreground, matching {{valuePrefix}}TextStyle's token-default contract. + recipe = accent + ? {{valuePrefix}}AccentForeground(recipe, highContrast: highContrast) + : recipe.color({{typePrefix}}Tokens.gray12()); + recipe = recipe.inherit(false); + + return {{valuePrefix}}ApplyTextFlow( + recipe, + align: align, + softWrap: softWrap, + truncate: truncate, + ).merge(style); +} + +/// Token-backed visual heading with an independent native heading level. +/// +/// [headingLevel] drives the accessibility level only; changing it never +/// changes the visual [size], matching Radix. +class {{typePrefix}}Heading extends StatelessWidget { + const {{typePrefix}}Heading( + this.text, { + super.key, + this.headingLevel = 1, + this.size = {{typePrefix}}TextSize.size6, + this.weight = {{typePrefix}}TextWeight.bold, + this.align, + this.softWrap = true, + this.truncate = false, + this.accent = false, + this.highContrast = false, + this.semanticLabel, + this.excludeSemantics = false, + this.style = const TextStyler.create(), + }) : assert(text != ''), + assert(headingLevel >= 1 && headingLevel <= 6), + assert(semanticLabel == null || semanticLabel != ''); + + final String text; + final int headingLevel; + final {{typePrefix}}TextSize size; + final {{typePrefix}}TextWeight weight; + final TextAlign? align; + final bool softWrap; + final bool truncate; + final bool accent; + final bool highContrast; + final String? semanticLabel; + final bool excludeSemantics; + final TextStyler style; + + @override + Widget build(BuildContext context) { + final content = {{valuePrefix}}HeadingStyle( + size: size, + weight: weight, + align: align, + softWrap: softWrap, + truncate: truncate, + accent: accent, + highContrast: highContrast, + style: style, + )(text); + + if (excludeSemantics) return ExcludeSemantics(child: content); + + return Semantics( + header: true, + headingLevel: headingLevel, + label: semanticLabel ?? text, + excludeSemantics: true, + child: content, + ); + } +} diff --git a/packages/remix_cli/lib/src/registry/fortal/templates/icon_button/icon_button.dart.tmpl b/packages/remix_cli/lib/src/registry/fortal/templates/icon_button/icon_button.dart.tmpl new file mode 100644 index 000000000..0ef728253 --- /dev/null +++ b/packages/remix_cli/lib/src/registry/fortal/templates/icon_button/icon_button.dart.tmpl @@ -0,0 +1,148 @@ +import 'package:flutter/widgets.dart'; +import 'package:mix_annotations/mix_annotations.dart'; +import 'package:remix/remix.dart'; + +import 'base_button.dart'; +import '../theme/theme.dart'; + +part 'icon_button.g.dart'; + +/// Radix Themes IconButton size presets. +enum {{typePrefix}}IconButtonSize { size1, size2, size3, size4 } + +/// Radix Themes IconButton variants. +enum {{typePrefix}}IconButtonVariant { classic, solid, soft, surface, outline, ghost } + +/// {{typePrefix}}-themed IconButton with the Radix size, variant, and override contract. +@MixWidget(target: RemixIconButton.new) +IconButtonStyler {{valuePrefix}}IconButtonStyle({ + {{typePrefix}}IconButtonVariant variant = .solid, + {{typePrefix}}IconButtonSize size = .size2, + bool highContrast = false, + IconButtonStyler style = const IconButtonStyler.create(), +}) { + final base = _{{valuePrefix}}IconButtonBaseStyler( + variant, + _{{valuePrefix}}BaseButtonSize(size), + ); + final stateStyles = {{valuePrefix}}BaseButtonStateStyles( + variant: _{{valuePrefix}}BaseButtonVariant(variant), + highContrast: highContrast, + ); + + return _apply{{typePrefix}}IconButtonStateStyles( + base, + stateStyles, + pressedPaddingTop: variant == .classic ? (size == .size1 ? 1 : 2) : null, + ).merge(style); +} + +IconButtonStyler _{{valuePrefix}}IconButtonBaseStyler( + {{typePrefix}}IconButtonVariant variant, + {{typePrefix}}BaseButtonSize size, +) { + final metrics = {{valuePrefix}}BaseButtonMetrics(size); + var style = IconButtonStyler( + icon: .size(_{{valuePrefix}}IconButtonIconSize(size)), + spinner: .size(metrics.spinnerSize) + .opacity(0.65) + .leafRadius({{typePrefix}}Tokens.radius1()) + .duration(const Duration(milliseconds: 800)), + ).borderRadius(.all(metrics.radius)); + + if (variant == .ghost) { + final ghost = {{valuePrefix}}IconButtonGhostMetrics(size); + style = style.padding(.all(ghost.padding)).margin(.all(ghost.margin)); + } else { + style = style + .container(.alignment(.center)) + .width(metrics.height) + .height(metrics.height); + } + return style; +} + +double _{{valuePrefix}}IconButtonIconSize({{typePrefix}}BaseButtonSize size) => switch (size) { + .size1 => {{typePrefix}}Tokens.space3(), + .size2 => {{typePrefix}}Tokens.space4(), + .size3 => {{typePrefix}}Tokens.spinnerSize3(), + .size4 => {{typePrefix}}Tokens.space5(), +}; + +{{typePrefix}}BaseButtonVariant _{{valuePrefix}}BaseButtonVariant( + {{typePrefix}}IconButtonVariant variant, +) => switch (variant) { + .classic => .classic, + .solid => .solid, + .soft => .soft, + .surface => .surface, + .outline => .outline, + .ghost => .ghost, +}; + +{{typePrefix}}BaseButtonSize _{{valuePrefix}}BaseButtonSize({{typePrefix}}IconButtonSize size) => + switch (size) { + .size1 => .size1, + .size2 => .size2, + .size3 => .size3, + .size4 => .size4, + }; + +IconButtonStyler _apply{{typePrefix}}IconButtonStateStyles( + IconButtonStyler base, + {{typePrefix}}BaseButtonStateStyles stateStyles, { + required double? pressedPaddingTop, +}) { + var pressed = _apply{{typePrefix}}IconButtonState( + IconButtonStyler(), + stateStyles.pressed, + ); + if (pressedPaddingTop != null) { + pressed = pressed.padding(.top(pressedPaddingTop)); + } + + return _apply{{typePrefix}}IconButtonState(base, stateStyles.idle) + .onHovered( + _apply{{typePrefix}}IconButtonState(IconButtonStyler(), stateStyles.hovered), + ) + .onPressed(pressed) + .onDisabled( + _apply{{typePrefix}}IconButtonState(IconButtonStyler(), stateStyles.disabled), + ) + .onFocusVisible( + _apply{{typePrefix}}IconButtonState( + IconButtonStyler(), + stateStyles.focusVisible, + ), + ) + .onDisabled( + _apply{{typePrefix}}IconButtonState( + IconButtonStyler(), + stateStyles.disabledFocus, + ), + ); +} + +IconButtonStyler _apply{{typePrefix}}IconButtonState( + IconButtonStyler style, + {{typePrefix}}BaseButtonStateStyle state, +) { + var result = style; + final foreground = state.foreground; + if (foreground != null) { + result = result.icon(.color(foreground)).spinner(.color(foreground)); + } + if (state.background != null) { + result = result.color(state.background!); + } + if (state.effects != null) { + result = result.containerEffects(state.effects!); + } + if (state.spinnerOpacity != null) { + result = result.spinner(.opacity(state.spinnerOpacity!)); + } + if (state.modifier != null) { + result = result.wrap(state.modifier!); + } + return result; +} diff --git a/packages/remix_cli/lib/src/registry/fortal/templates/icons/icons.dart.tmpl b/packages/remix_cli/lib/src/registry/fortal/templates/icons/icons.dart.tmpl new file mode 100644 index 000000000..f8cc80778 --- /dev/null +++ b/packages/remix_cli/lib/src/registry/fortal/templates/icons/icons.dart.tmpl @@ -0,0 +1,17 @@ +import 'package:flutter/widgets.dart'; +import 'package:remix_ui_icons/remix_ui_icons.dart'; + +/// Application-owned aliases for the icons used by this UI layer. +/// +/// The complete 318-glyph catalog remains available through [RemixIcons]. +/// Add, rename, or remove aliases here as the application vocabulary evolves. +abstract final class {{typePrefix}}Icons { + /// Confirms a successful or selected action. + static const IconData check = RemixIcons.check; + + /// Dismisses, clears, or marks a failed action. + static const IconData cross = RemixIcons.cross2; + + /// Opens content positioned below the current control. + static const IconData chevronDown = RemixIcons.chevronDown; +} diff --git a/packages/remix_cli/lib/src/registry/fortal/templates/kbd/kbd.dart.tmpl b/packages/remix_cli/lib/src/registry/fortal/templates/kbd/kbd.dart.tmpl new file mode 100644 index 000000000..d80634e59 --- /dev/null +++ b/packages/remix_cli/lib/src/registry/fortal/templates/kbd/kbd.dart.tmpl @@ -0,0 +1,199 @@ +import 'package:flutter/widgets.dart'; +import 'package:remix/remix.dart'; + +import '../theme/theme.dart'; +import 'typography.dart'; + +/// Radix Themes Kbd variants. +enum {{typePrefix}}KbdVariant { classic, soft } + +/// {{typePrefix}}-themed keyboard key. +/// +/// Like Code, the geometry is em-relative to the resolved font size, so this +/// recipe takes a [context]. Radix uses two different type-scale factors: an +/// explicit size multiplies its token by `0.8`, while an omitted one keeps +/// upstream's unsized `0.75em` — anchored to the root `text3` token rather +/// than the ambient `DefaultTextStyle`, so a host text run cannot resize the +/// key cap. +BadgeStyler {{valuePrefix}}KbdStyle( + BuildContext context, { + {{typePrefix}}TextSize? size, + {{typePrefix}}KbdVariant variant = .classic, + BadgeStyler style = const BadgeStyler.create(), +}) { + final base = {{valuePrefix}}ResolveTextToken(context, size ?? {{typePrefix}}TextSize.size3); + final fontSize = base.fontSize! * (size == null ? 0.75 : 0.8); + // Upstream `--letter-spacing-N` is em-relative, so an explicit size resolves + // it against Kbd's own `0.8em` rather than the token's own font size. The + // unsized path keeps the token's letter spacing unscaled, matching the + // resolved value upstream's `0.75em` run would carry. + final letterSpacing = (base.letterSpacing ?? 0) * (size == null ? 1 : 0.8); + + // Kbd pins its own weight and line box regardless of the surrounding style, + // so it stays a key cap rather than following surrounding copy. + final textStyle = TextStyler() + .fontSize(fontSize) + .fontWeight({{typePrefix}}Tokens.fontWeightRegular()) + .height(1.7) + .letterSpacing(letterSpacing) + .wordSpacing(-0.1 * fontSize) + .textAlign(TextAlign.center) + .softWrap(false) + .maxLines(1) + .color({{typePrefix}}Tokens.gray12()) + .inherit(false); + + var recipe = BadgeStyler() + .label(textStyle) + .minWidth(1.75 * fontSize) + .padding( + EdgeInsetsGeometryMix.only( + left: 0.5 * fontSize, + right: 0.5 * fontSize, + bottom: 0.05 * fontSize, + ), + ) + .borderRadius( + BorderRadiusGeometryMix.circular( + 0.35 * fontSize * {{valuePrefix}}RadiusFactor(context), + ), + ) + .color(switch (variant) { + .classic => {{valuePrefix}}ResolveColor(context, {{typePrefix}}Tokens.gray1), + .soft => {{valuePrefix}}ResolveColor(context, {{typePrefix}}Tokens.grayA3), + }); + + if (variant == .classic) { + recipe = recipe.containerEffects( + RemixBoxEffectsMix.behindContent( + RemixBoxEffectLayerMix(shadows: _{{valuePrefix}}KbdShadows(context, fontSize)), + ), + ); + } + + return recipe.merge(style); +} + +/// The pinned six-layer classic key-cap stack, in upstream paint order. +/// +/// Radix's `-0.03em` visual top nudge is deliberately skipped; a transform +/// wrapper for a sub-pixel baseline tweak is recorded as a measured visual +/// approximation instead. +List _{{valuePrefix}}KbdShadows(BuildContext context, double em) { + final isDark = {{typePrefix}}Theme.of(context).isDark; + + return [ + RemixBoxShadowMix( + kind: .inset, + color: {{valuePrefix}}ResolveColor( + context, + isDark ? {{typePrefix}}Tokens.grayA3 : {{typePrefix}}Tokens.grayA2, + ), + offset: Offset(0, -0.05 * em), + blurRadius: 0.5 * em, + ), + RemixBoxShadowMix( + kind: .inset, + color: {{valuePrefix}}ResolveColor( + context, + isDark ? {{typePrefix}}Tokens.grayA11 : {{typePrefix}}Tokens.whiteA12, + ), + offset: Offset(0, 0.05 * em), + ), + RemixBoxShadowMix( + kind: .inset, + color: {{valuePrefix}}ResolveColor(context, {{typePrefix}}Tokens.grayA2), + offset: Offset(0, 0.25 * em), + blurRadius: 0.5 * em, + ), + RemixBoxShadowMix( + kind: .inset, + color: {{valuePrefix}}ResolveColor( + context, + isDark ? {{typePrefix}}Tokens.blackA11 : {{typePrefix}}Tokens.grayA6, + ), + offset: Offset(0, (isDark ? -0.1 : -0.05) * em), + ), + RemixBoxShadowMix( + color: {{valuePrefix}}ResolveColor( + context, + isDark ? {{typePrefix}}Tokens.grayA7 : {{typePrefix}}Tokens.grayA5, + ), + spreadRadius: (isDark ? 0.075 : 0.05) * em, + ), + RemixBoxShadowMix( + color: {{valuePrefix}}ResolveColor( + context, + isDark ? {{typePrefix}}Tokens.blackA12 : {{typePrefix}}Tokens.grayA7, + ), + offset: Offset(0, 0.08 * em), + blurRadius: 0.17 * em, + ), + ]; +} + +/// Token-backed representation of one keyboard key or shortcut. +/// +/// Publishes a single native `keyboardKey` node and no tap action; Kbd is inert +/// upstream, so the hover/pressed selectors that apply only when it is nested +/// in an actionable element are deliberately absent. +class {{typePrefix}}Kbd extends StatelessWidget { + const {{typePrefix}}Kbd( + this.text, { + super.key, + this.size, + this.variant = {{typePrefix}}KbdVariant.classic, + this.semanticLabel, + this.excludeSemantics = false, + this.style = const BadgeStyler.create(), + }) : assert(text != ''), + assert(semanticLabel == null || semanticLabel != ''); + + const {{typePrefix}}Kbd.classic( + this.text, { + super.key, + this.size, + this.semanticLabel, + this.excludeSemantics = false, + this.style = const BadgeStyler.create(), + }) : variant = {{typePrefix}}KbdVariant.classic, + assert(text != ''), + assert(semanticLabel == null || semanticLabel != ''); + + const {{typePrefix}}Kbd.soft( + this.text, { + super.key, + this.size, + this.semanticLabel, + this.excludeSemantics = false, + this.style = const BadgeStyler.create(), + }) : variant = {{typePrefix}}KbdVariant.soft, + assert(text != ''), + assert(semanticLabel == null || semanticLabel != ''); + + final String text; + final {{typePrefix}}TextSize? size; + final {{typePrefix}}KbdVariant variant; + final String? semanticLabel; + final bool excludeSemantics; + final BadgeStyler style; + + @override + Widget build(BuildContext context) { + final content = {{valuePrefix}}KbdStyle( + context, + size: size, + variant: variant, + style: style, + )(label: text); + + if (excludeSemantics) return ExcludeSemantics(child: content); + + return Semantics( + keyboardKey: true, + label: semanticLabel ?? text, + excludeSemantics: true, + child: content, + ); + } +} diff --git a/packages/remix_cli/lib/src/registry/fortal/templates/link/link.dart.tmpl b/packages/remix_cli/lib/src/registry/fortal/templates/link/link.dart.tmpl new file mode 100644 index 000000000..b0de2e5e2 --- /dev/null +++ b/packages/remix_cli/lib/src/registry/fortal/templates/link/link.dart.tmpl @@ -0,0 +1,244 @@ +import 'dart:math' as math; + +import 'package:flutter/widgets.dart'; +import 'package:remix/remix.dart'; + +import '../theme/theme.dart'; +import 'typography.dart'; + +/// Underline visibility for [{{typePrefix}}Link]. +enum {{typePrefix}}LinkUnderline { auto, always, hover, none } + +/// {{typePrefix}}-themed link style. +/// +/// Takes a [context] because the focus outline's radius is em-relative to the +/// resolved font size. +/// +/// [actionable] gates every state-dependent rule, matching upstream's +/// `:where(:any-link, button)`. A non-actionable link carries no hover or +/// focus-visible variant at all, so it stays plain accent text no matter what +/// widget states are resolved around it. +LinkStyler {{valuePrefix}}LinkStyle( + BuildContext context, { + {{typePrefix}}TextSize? size, + {{typePrefix}}TextWeight? weight, + {{typePrefix}}LinkUnderline underline = .auto, + bool softWrap = true, + bool truncate = false, + bool highContrast = false, + required bool actionable, + LinkStyler style = const LinkStyler.create(), +}) { + LinkStyler styleFor({bool hovered = false, bool focused = false}) => + _{{valuePrefix}}LinkStateStyle( + context, + size: size, + weight: weight, + underline: underline, + softWrap: softWrap, + truncate: truncate, + highContrast: highContrast, + actionable: actionable, + hovered: hovered, + focused: focused, + ); + + if (!actionable) return styleFor().merge(style); + + // The focus-visible snapshot already drops the underline via `focused`; the + // explicit `none` also clears any decoration inherited through the merge. + final focusVisible = styleFor( + focused: true, + ).label(.decoration(TextDecoration.none)); + + return styleFor() + .onHovered(styleFor(hovered: true)) + .onFocusVisible(focusVisible) + .merge(style); +} + +/// Resolves one point in the link's state space. +/// +/// Separate from [{{valuePrefix}}LinkStyle] because the public recipe returns a style +/// carrying Mix variants, and building those variants needs the flat snapshots +/// they are built from. +LinkStyler _{{valuePrefix}}LinkStateStyle( + BuildContext context, { + required {{typePrefix}}TextSize? size, + required {{typePrefix}}TextWeight? weight, + required {{typePrefix}}LinkUnderline underline, + required bool softWrap, + required bool truncate, + required bool highContrast, + required bool actionable, + required bool hovered, + required bool focused, +}) { + var textStyle = {{valuePrefix}}AccentForeground( + TextStyler(), + highContrast: highContrast, + ); + // An omitted size anchors to the root `text3` token rather than the ambient + // `DefaultTextStyle`, so a host text run cannot change the link's metrics or + // its em-relative underline geometry. + textStyle = textStyle.style( + {{valuePrefix}}TextSizeToken(size ?? {{typePrefix}}TextSize.size3).mix(), + ); + if (weight != null) { + textStyle = textStyle.fontWeight({{valuePrefix}}TextWeightToken(weight)()); + } + textStyle = textStyle.inherit(false); + + final effectiveText = {{valuePrefix}}ResolveTextToken( + context, + size ?? {{typePrefix}}TextSize.size3, + ); + final fontSize = effectiveText.fontSize!; + + // Every upstream underline rule is gated behind `:where(:any-link, button)`, + // so a link with no callback stays plain accent-coloured text. A focus-visible + // outline replaces the underline rather than stacking both. + final underlined = + actionable && + !focused && + switch (underline) { + .always => true, + .hover => hovered, + .auto => highContrast || hovered, + .none => false, + }; + if (underlined) { + // Radix declares the decoration colour twice for this selector and the + // later rule wins: + // text-decoration-color: color-mix(in oklab, var(--accent-aN), var(--gray-a6)) + // Using the accent alpha alone leaves the underline noticeably fainter, so + // blend it. Color.lerp is an sRGB approximation of the oklab mix. + final accentStep = underline == {{typePrefix}}LinkUnderline.auto && highContrast + ? {{typePrefix}}Tokens.accentA6 + : {{typePrefix}}Tokens.accentA5; + final decorationColor = Color.lerp( + {{valuePrefix}}ResolveColor(context, accentStep), + {{valuePrefix}}ResolveColor(context, {{typePrefix}}Tokens.grayA6), + 0.5, + )!; + textStyle = textStyle + .decoration(TextDecoration.underline) + .decorationStyle(TextDecorationStyle.solid) + .decorationColor(decorationColor) + // Upstream is `min(2px, max(1px, 0.05em))`. Flutter reads + // decorationThickness as a multiple of the font's own underline + // thickness rather than a length, so the pinned 1–2 range lands as a + // 1×–2× stroke instead of exact pixels; the em breakpoints still fall + // where Radix puts them. + .decorationThickness(math.min(2, math.max(1, 0.05 * fontSize))); + } + textStyle = {{valuePrefix}}ApplyTextFlow( + textStyle, + softWrap: softWrap, + truncate: truncate, + ); + + var style = LinkStyler() + .label(textStyle) + .borderRadius( + BorderRadiusGeometryMix.circular( + 0.07 * fontSize * {{valuePrefix}}RadiusFactor(context), + ), + ); + if (focused) { + style = style.containerEffects( + {{valuePrefix}}FocusOutline( + {{valuePrefix}}ResolveColor(context, {{typePrefix}}Tokens.focus8), + offset: 2, + ), + ); + } + + return style; +} + +/// Token-backed text that becomes an accessible link only when actionable. +/// +/// A null [onPressed] disables the link just as [enabled] `false` does: accent +/// text with no focus stop, link role, or activation, and never underlined. +/// Reach for `{{typePrefix}}Text(accent: true)` when the text was never meant to +/// navigate. +/// +/// `linkUrl` is assistive metadata only and is never launched; navigation stays +/// the caller's responsibility in [onPressed]. +/// +/// An actionable link activates on pointer and Enter. Space belongs to the +/// Button role and is deliberately left unclaimed. +class {{typePrefix}}Link extends StatelessWidget { + const {{typePrefix}}Link( + this.text, { + super.key, + this.size, + this.weight, + this.underline = {{typePrefix}}LinkUnderline.auto, + this.softWrap = true, + this.truncate = false, + this.highContrast = false, + this.onPressed, + this.enabled = true, + this.linkUrl, + this.focusNode, + this.autofocus = false, + this.enableFeedback = true, + this.mouseCursor = SystemMouseCursors.click, + this.semanticLabel, + this.semanticHint, + this.excludeSemantics = false, + this.style = const LinkStyler.create(), + }) : assert(text != ''), + assert(semanticLabel == null || semanticLabel != ''), + assert(semanticHint == null || semanticHint != ''), + assert(linkUrl == null || onPressed != null); + + final String text; + final {{typePrefix}}TextSize? size; + final {{typePrefix}}TextWeight? weight; + final {{typePrefix}}LinkUnderline underline; + final bool softWrap; + final bool truncate; + final bool highContrast; + final VoidCallback? onPressed; + final bool enabled; + final Uri? linkUrl; + final FocusNode? focusNode; + final bool autofocus; + final bool enableFeedback; + final MouseCursor mouseCursor; + final String? semanticLabel; + final String? semanticHint; + final bool excludeSemantics; + final LinkStyler style; + + @override + Widget build(BuildContext context) { + return RemixLink( + label: text, + onPressed: onPressed, + enabled: enabled, + linkUrl: linkUrl, + focusNode: focusNode, + autofocus: autofocus, + enableFeedback: enableFeedback, + mouseCursor: mouseCursor, + semanticLabel: semanticLabel, + semanticHint: semanticHint, + excludeSemantics: excludeSemantics, + style: {{valuePrefix}}LinkStyle( + context, + size: size, + weight: weight, + underline: underline, + softWrap: softWrap, + truncate: truncate, + highContrast: highContrast, + actionable: enabled && onPressed != null, + style: style, + ), + ); + } +} diff --git a/packages/remix_cli/lib/src/registry/fortal/templates/menu/menu.dart.tmpl b/packages/remix_cli/lib/src/registry/fortal/templates/menu/menu.dart.tmpl new file mode 100644 index 000000000..318b55b1d --- /dev/null +++ b/packages/remix_cli/lib/src/registry/fortal/templates/menu/menu.dart.tmpl @@ -0,0 +1,224 @@ +import 'package:flutter/widgets.dart'; +import 'package:mix_annotations/mix_annotations.dart'; +import 'package:remix/remix.dart'; + +import '../theme/theme.dart'; + +part 'menu.g.dart'; + +/// Radix Themes menu content sizes. +enum {{typePrefix}}MenuSize { size1, size2 } + +/// Radix Themes menu content variants. +enum {{typePrefix}}MenuVariant { solid, soft } + +/// {{typePrefix}} menu content with Radix-owned size, variant, and contrast behavior. +@MixWidget(target: RemixMenu.new) +MenuStyler {{valuePrefix}}MenuStyle({ + {{typePrefix}}MenuVariant variant = .solid, + {{typePrefix}}MenuSize size = .size2, + bool highContrast = false, + MenuStyler style = const MenuStyler.create(), +}) { + final metrics = _{{valuePrefix}}MenuMetrics(size); + final base = MenuStyler() + .trigger(_{{valuePrefix}}MenuTriggerStyler(metrics)) + .overlay( + FlexBoxStyler() + .padding(.all(metrics.contentPadding)) + .borderRadius(.all(metrics.contentRadius)) + // Radix pins menus to the solid panel with no backdrop blur, + // even when the theme panel background is translucent. + .color({{typePrefix}}Tokens.colorPanelSolid()) + .decoration( + BoxDecorationMix.create(boxShadow: {{typePrefix}}Tokens.shadow5.mix()), + ) + .clipBehavior(Clip.antiAlias), + ) + .item(_{{valuePrefix}}MenuItemStyler(variant, metrics, highContrast: highContrast)) + .submenuItem( + _{{valuePrefix}}MenuSubmenuItemStyler( + variant, + metrics, + highContrast: highContrast, + ), + ) + .divider(_{{valuePrefix}}MenuDividerStyler(metrics)); + + return base.merge(style); +} + +/// {{typePrefix}} item recipe for per-item style overrides. +MenuItemStyler {{valuePrefix}}MenuItemStyle({ + {{typePrefix}}MenuVariant variant = .solid, + {{typePrefix}}MenuSize size = .size2, + bool highContrast = false, +}) => _{{valuePrefix}}MenuItemStyler( + variant, + _{{valuePrefix}}MenuMetrics(size), + highContrast: highContrast, +); + +/// Radix has no menu-owned trigger; this mirrors the base Radix button +/// content treatment (gap, text token, icon) without button chrome. +MenuTriggerStyler _{{valuePrefix}}MenuTriggerStyler(_{{typePrefix}}MenuMetrics metrics) => + MenuTriggerStyler() + .spacing(metrics.triggerGap) + .label(.style(metrics.text.mix()).color({{typePrefix}}Tokens.gray12())) + .icon(.color({{typePrefix}}Tokens.gray12()).size(metrics.contentIconSize)); + +MenuItemStyler _{{valuePrefix}}MenuItemStyler( + {{typePrefix}}MenuVariant variant, + _{{typePrefix}}MenuMetrics metrics, { + required bool highContrast, +}) { + final base = MenuItemStyler() + .direction(.horizontal) + .spacing({{typePrefix}}Tokens.space2()) + .height(metrics.itemHeight) + .padding(.horizontal(metrics.leadingInset)) + .borderRadius(.all(metrics.itemRadius)) + .label(.style(metrics.text.mix()).color({{typePrefix}}Tokens.gray12())) + // Radix pins only indicator/subtrigger icons (8/10px); content icons + // follow the repo-wide text-matched sizes used by tabs and toggles. + .leadingIcon(.color({{typePrefix}}Tokens.gray12()).size(metrics.contentIconSize)) + .trailingIcon( + .color({{typePrefix}}Tokens.grayA11()).size(metrics.contentIconSize), + ) + .indicator(.color({{typePrefix}}Tokens.gray12()).size(metrics.indicatorSize)); + final highlighted = _{{valuePrefix}}MenuHighlightedItemStyler( + variant, + highContrast: highContrast, + ); + final disabled = MenuItemStyler() + .color(const Color(0x00000000)) + .label(.color({{typePrefix}}Tokens.grayA8())) + .leadingIcon(.color({{typePrefix}}Tokens.grayA8())) + .trailingIcon(.color({{typePrefix}}Tokens.grayA8())) + .indicator(.color({{typePrefix}}Tokens.grayA8())); + + // Naked's focused item is Radix's roving `data-highlighted` item, not a + // CSS focus ring, so this intentionally follows raw focus. + return base + .onHovered(highlighted) + .onFocused(highlighted) + .onPressed(highlighted) + .onDisabled(disabled); +} + +MenuItemStyler _{{valuePrefix}}MenuHighlightedItemStyler( + {{typePrefix}}MenuVariant variant, { + required bool highContrast, +}) { + final solidForeground = highContrast + ? {{typePrefix}}Tokens.accent1() + : {{typePrefix}}Tokens.accentContrast(); + + return switch (variant) { + .solid => + MenuItemStyler() + .color( + highContrast ? {{typePrefix}}Tokens.accent12() : {{typePrefix}}Tokens.accent9(), + ) + .label(.color(solidForeground)) + .leadingIcon(.color(solidForeground)) + .trailingIcon(.color(solidForeground)) + .indicator(.color(solidForeground)), + .soft => + MenuItemStyler() + .color({{typePrefix}}Tokens.accentA4()) + .trailingIcon(.color({{typePrefix}}Tokens.gray12())), + }; +} + +MenuItemStyler _{{valuePrefix}}MenuSubmenuItemStyler( + {{typePrefix}}MenuVariant variant, + _{{typePrefix}}MenuMetrics metrics, { + required bool highContrast, +}) { + final highlighted = _{{valuePrefix}}MenuHighlightedItemStyler( + variant, + highContrast: highContrast, + ); + final submenuOpen = MenuItemStyler() + .color(switch (variant) { + .solid => {{typePrefix}}Tokens.grayA3(), + .soft => {{typePrefix}}Tokens.accentA3(), + }) + .onHovered(highlighted) + // Roving `data-highlighted` state, not a focus-visible ring. + .onFocused(highlighted) + .onPressed(highlighted); + + // The chevron keeps the exact Radix subtrigger icon size even though + // content trailing icons are text-matched. + return MenuItemStyler() + .trailingIcon(.color({{typePrefix}}Tokens.gray12()).size(metrics.indicatorSize)) + .onSelected(submenuOpen); +} + +DividerStyler _{{valuePrefix}}MenuDividerStyler(_{{typePrefix}}MenuMetrics metrics) => + DividerStyler() + .height(1) + .margin( + .only( + left: metrics.leadingInset, + right: metrics.trailingInset, + top: {{typePrefix}}Tokens.space2(), + bottom: {{typePrefix}}Tokens.space2(), + ), + ) + .color({{typePrefix}}Tokens.grayA6()); + +class _{{typePrefix}}MenuMetrics { + const _{{typePrefix}}MenuMetrics({ + required this.contentPadding, + required this.contentRadius, + required this.itemHeight, + required this.itemRadius, + required this.leadingInset, + required this.trailingInset, + required this.indicatorSize, + required this.contentIconSize, + required this.triggerGap, + required this.text, + }); + + final double contentPadding; + final Radius contentRadius; + final double itemHeight; + final Radius itemRadius; + final double leadingInset; + final double trailingInset; + final double indicatorSize; + final double contentIconSize; + final double triggerGap; + final TextStyleToken text; +} + +_{{typePrefix}}MenuMetrics _{{valuePrefix}}MenuMetrics({{typePrefix}}MenuSize size) => switch (size) { + .size1 => _{{typePrefix}}MenuMetrics( + contentPadding: {{typePrefix}}Tokens.space1(), + contentRadius: {{typePrefix}}Tokens.radius3(), + itemHeight: {{typePrefix}}Tokens.space5(), + itemRadius: {{typePrefix}}Tokens.radius1(), + leadingInset: {{typePrefix}}Tokens.space2(), + trailingInset: {{typePrefix}}Tokens.space2(), + indicatorSize: {{typePrefix}}Tokens.selectIndicatorSize1(), + contentIconSize: {{typePrefix}}Tokens.space3(), + triggerGap: {{typePrefix}}Tokens.space1(), + text: {{typePrefix}}Tokens.text1, + ), + .size2 => _{{typePrefix}}MenuMetrics( + contentPadding: {{typePrefix}}Tokens.space2(), + contentRadius: {{typePrefix}}Tokens.radius4(), + itemHeight: {{typePrefix}}Tokens.space6(), + itemRadius: {{typePrefix}}Tokens.radius2(), + leadingInset: {{typePrefix}}Tokens.space3(), + trailingInset: {{typePrefix}}Tokens.space3(), + indicatorSize: {{typePrefix}}Tokens.selectIndicatorSize2(), + contentIconSize: {{typePrefix}}Tokens.space4(), + triggerGap: {{typePrefix}}Tokens.space2(), + text: {{typePrefix}}Tokens.text2, + ), +}; diff --git a/packages/remix_cli/lib/src/registry/fortal/templates/popover/popover.dart.tmpl b/packages/remix_cli/lib/src/registry/fortal/templates/popover/popover.dart.tmpl new file mode 100644 index 000000000..65510c6c5 --- /dev/null +++ b/packages/remix_cli/lib/src/registry/fortal/templates/popover/popover.dart.tmpl @@ -0,0 +1,46 @@ +import 'package:flutter/widgets.dart'; +import 'package:mix_annotations/mix_annotations.dart'; +import 'package:remix/remix.dart'; + +import '../theme/theme.dart'; + +part 'popover.g.dart'; + +/// {{typePrefix}} popover size presets matching Radix Themes 3.3.0. +enum {{typePrefix}}PopoverSize { size1, size2, size3, size4 } + +/// {{typePrefix}}-themed preset for [RemixPopover]. +/// +/// The generated [{{typePrefix}}Popover] defaults to [{{typePrefix}}PopoverSize.size2], a +/// 480-pixel maximum width, and no arrow. +@MixWidget(target: RemixPopover.new) +PopoverStyler {{valuePrefix}}PopoverStyle({ + {{typePrefix}}PopoverSize size = {{typePrefix}}PopoverSize.size2, + PopoverStyler style = const PopoverStyler.create(), +}) { + final radius = switch (size) { + {{typePrefix}}PopoverSize.size1 || + {{typePrefix}}PopoverSize.size2 => {{typePrefix}}Tokens.radius4(), + {{typePrefix}}PopoverSize.size3 || + {{typePrefix}}PopoverSize.size4 => {{typePrefix}}Tokens.radius5(), + }; + final padding = switch (size) { + {{typePrefix}}PopoverSize.size1 => {{typePrefix}}Tokens.space3(), + {{typePrefix}}PopoverSize.size2 => {{typePrefix}}Tokens.space4(), + {{typePrefix}}PopoverSize.size3 => {{typePrefix}}Tokens.space5(), + {{typePrefix}}PopoverSize.size4 => {{typePrefix}}Tokens.space6(), + }; + + return PopoverStyler() + .maxWidth(480) + .padding(.all(padding)) + .borderRadius(.all(radius)) + .color({{typePrefix}}Tokens.colorPanel()) + .decoration( + BoxDecorationMix.create(boxShadow: {{typePrefix}}Tokens.shadow5.mix()), + ) + .containerEffects( + RemixBoxEffectsMix.backdropBlur({{typePrefix}}Tokens.panelBlur()), + ) + .merge(style); +} diff --git a/packages/remix_cli/lib/src/registry/fortal/templates/progress/progress.dart.tmpl b/packages/remix_cli/lib/src/registry/fortal/templates/progress/progress.dart.tmpl new file mode 100644 index 000000000..6430b77d0 --- /dev/null +++ b/packages/remix_cli/lib/src/registry/fortal/templates/progress/progress.dart.tmpl @@ -0,0 +1,131 @@ +import 'package:flutter/widgets.dart'; +import 'package:mix_annotations/mix_annotations.dart'; +import 'package:remix/remix.dart'; + +import '../theme/theme.dart'; + +part 'progress.g.dart'; + +/// {{typePrefix}} progress size presets. +enum {{typePrefix}}ProgressSize { size1, size2, size3 } + +/// {{typePrefix}} progress color variants. +enum {{typePrefix}}ProgressVariant { classic, surface, soft } + +/// {{typePrefix}}-themed preset for [RemixProgress]. +@MixWidget(target: RemixProgress.new) +ProgressStyler {{valuePrefix}}ProgressStyle({ + {{typePrefix}}ProgressVariant variant = .surface, + {{typePrefix}}ProgressSize size = .size2, + bool highContrast = false, + ProgressStyler style = const ProgressStyler.create(), +}) { + return (switch (variant) { + .classic => _{{valuePrefix}}ProgressClassicStyler(size, highContrast: highContrast), + .surface => _{{valuePrefix}}ProgressSurfaceStyler(size, highContrast: highContrast), + .soft => _{{valuePrefix}}ProgressSoftStyler(size, highContrast: highContrast), + }).merge(style); +} + +ProgressStyler _{{valuePrefix}}ProgressBaseStyler({{typePrefix}}ProgressSize size) { + final metrics = _{{valuePrefix}}ProgressMetrics(size); + return ProgressStyler( + container: .width(.infinity) + .height(metrics.height) + .borderRadius(.all(metrics.radius)) + .clipBehavior(.antiAlias), + track: .width(.infinity).height(metrics.height), + indicator: .height(metrics.height).borderRadius(.all(metrics.radius)), + trackEffects: RemixBoxEffectsMix( + behindContent: _{{valuePrefix}}ProgressLayer(), + overContent: _{{valuePrefix}}ProgressLayer(), + ), + indicatorEffects: RemixBoxEffectsMix( + behindContent: _{{valuePrefix}}ProgressLayer(), + overContent: _{{valuePrefix}}ProgressLayer(), + ), + ); +} + +ProgressStyler _{{valuePrefix}}ProgressClassicStyler( + {{typePrefix}}ProgressSize size, { + required bool highContrast, +}) { + return _{{valuePrefix}}ProgressBaseStyler(size) + .trackColor({{typePrefix}}Tokens.grayA3()) + .trackEffects( + RemixBoxEffectsMix.overContent( + _{{valuePrefix}}ProgressLayer(shadowToken: {{typePrefix}}Tokens.shadow1Layers), + ), + ) + .indicatorColor( + highContrast ? {{typePrefix}}Tokens.accent12() : {{typePrefix}}Tokens.accentTrack(), + ); +} + +ProgressStyler _{{valuePrefix}}ProgressSurfaceStyler( + {{typePrefix}}ProgressSize size, { + required bool highContrast, +}) { + return _{{valuePrefix}}ProgressBaseStyler(size) + .trackColor({{typePrefix}}Tokens.grayA3()) + .trackEffects( + RemixBoxEffectsMix.overContent( + _{{valuePrefix}}ProgressLayer( + shadows: [ + RemixBoxShadowMix( + kind: .inset, + color: {{typePrefix}}Tokens.grayA4(), + spreadRadius: 1, + ), + ], + ), + ), + ) + .indicatorColor( + highContrast ? {{typePrefix}}Tokens.accent12() : {{typePrefix}}Tokens.accentTrack(), + ); +} + +ProgressStyler _{{valuePrefix}}ProgressSoftStyler( + {{typePrefix}}ProgressSize size, { + required bool highContrast, +}) { + return _{{valuePrefix}}ProgressBaseStyler(size) + .trackColor({{typePrefix}}Tokens.grayA4()) + .track( + .foregroundDecoration(BoxDecorationMix(color: {{typePrefix}}Tokens.whiteA1())), + ) + .indicatorColor( + highContrast ? {{typePrefix}}Tokens.accent12() : {{typePrefix}}Tokens.accent8(), + ) + .indicator( + .foregroundDecoration( + BoxDecorationMix( + color: highContrast ? null : {{typePrefix}}Tokens.accentA5(), + ), + ), + ); +} + +({double height, Radius radius}) _{{valuePrefix}}ProgressMetrics( + {{typePrefix}}ProgressSize size, +) => switch (size) { + .size1 => ( + height: {{typePrefix}}Tokens.space1(), + radius: {{typePrefix}}Tokens.progressRadius1(), + ), + .size2 => ( + height: {{typePrefix}}Tokens.progressHeight2(), + radius: {{typePrefix}}Tokens.progressRadius2(), + ), + .size3 => ( + height: {{typePrefix}}Tokens.space2(), + radius: {{typePrefix}}Tokens.progressRadius3(), + ), +}; + +RemixBoxEffectLayerMix _{{valuePrefix}}ProgressLayer({ + List? shadows, + RemixBoxShadowListToken? shadowToken, +}) => RemixBoxEffectLayerMix(shadows: shadows, shadowToken: shadowToken); diff --git a/packages/remix_cli/lib/src/registry/fortal/templates/radio/radio.dart.tmpl b/packages/remix_cli/lib/src/registry/fortal/templates/radio/radio.dart.tmpl new file mode 100644 index 000000000..dd9c39874 --- /dev/null +++ b/packages/remix_cli/lib/src/registry/fortal/templates/radio/radio.dart.tmpl @@ -0,0 +1,261 @@ +import 'package:flutter/widgets.dart'; +import 'package:mix_annotations/mix_annotations.dart'; +import 'package:remix/remix.dart'; + +import '../theme/theme.dart'; + +part 'radio.g.dart'; + +/// {{typePrefix}} radio size presets. +enum {{typePrefix}}RadioSize { + /// Compact radio. + size1, + + /// Default radio. + size2, + + /// Large radio. + size3, +} + +/// {{typePrefix}} radio color variants. +enum {{typePrefix}}RadioVariant { + /// Raised treatment with Radix's classic shadow and gradient layers. + classic, + + /// Surface treatment with neutral border. + surface, + + /// Soft accent treatment. + soft, +} + +/// {{typePrefix}}-themed preset for [RemixRadio]. +@MixWidget(target: RemixRadio.new) +RadioStyler {{valuePrefix}}RadioStyle({ + {{typePrefix}}RadioVariant variant = .surface, + {{typePrefix}}RadioSize size = .size2, + bool highContrast = false, + RadioStyler style = const RadioStyler.create(), +}) { + return (switch (variant) { + .classic => _{{valuePrefix}}RadioClassicStyler(size, highContrast: highContrast), + .surface => _{{valuePrefix}}RadioSurfaceStyler(size, highContrast: highContrast), + .soft => _{{valuePrefix}}RadioSoftStyler(size, highContrast: highContrast), + }).merge(style); +} + +RadioStyler _{{valuePrefix}}RadioBaseStyler({{typePrefix}}RadioSize size) { + final metrics = _{{valuePrefix}}RadioMetrics(size); + return RadioStyler( + container: .size( + metrics.size, + metrics.size, + ).alignment(.center).borderRadius(.all({{typePrefix}}Tokens.radiusCircle())), + indicator: .size( + metrics.indicatorSize, + metrics.indicatorSize, + ).borderRadius(.all({{typePrefix}}Tokens.radiusCircle())), + containerEffects: RemixBoxEffectsMix( + behindContent: RemixBoxEffectLayerMix(), + overContent: RemixBoxEffectLayerMix(), + ), + ).onFocusVisible( + .containerEffects( + RemixBoxEffectsMix( + outline: BorderSideMix( + color: {{typePrefix}}Tokens.focus8(), + width: 2, + strokeAlign: BorderSide.strokeAlignInside, + ), + outlineOffset: 2, + ), + ), + ); +} + +RadioStyler _{{valuePrefix}}RadioClassicStyler( + {{typePrefix}}RadioSize size, { + required bool highContrast, +}) { + final selectedColor = highContrast + ? {{typePrefix}}Tokens.accent12() + : {{typePrefix}}Tokens.accentIndicator(); + return _{{valuePrefix}}RadioBaseStyler(size) + .color({{typePrefix}}Tokens.colorSurface()) + .containerEffects( + RemixBoxEffectsMix.behindContent( + RemixBoxEffectLayerMix(shadowToken: {{typePrefix}}Tokens.shadow1Layers), + ), + ) + .containerEffects( + RemixBoxEffectsMix.overContent( + _{{valuePrefix}}RadioInsetRing({{typePrefix}}Tokens.gray7()), + ), + ) + .indicatorColor( + highContrast ? {{typePrefix}}Tokens.accent1() : {{typePrefix}}Tokens.accentContrast(), + ) + .onSelected( + .color(selectedColor) + .containerEffects( + RemixBoxEffectsMix.behindContent( + RemixBoxEffectLayerMix( + gradients: [ + RemixLinearGradientMix( + colors: [ + {{typePrefix}}Tokens.whiteA3(), + const Color(0x00000000), + {{typePrefix}}Tokens.blackA3(), + ], + ), + ], + shadows: [ + RemixBoxShadowMix( + kind: .inset, + color: {{typePrefix}}Tokens.whiteA4(), + offset: const Offset(0, 0.5), + blurRadius: 0.5, + ), + RemixBoxShadowMix( + kind: .inset, + color: {{typePrefix}}Tokens.blackA4(), + offset: const Offset(0, -0.5), + blurRadius: 0.5, + ), + ], + ), + ), + ) + .indicatorColor( + highContrast + ? {{typePrefix}}Tokens.accent1() + : {{typePrefix}}Tokens.accentContrast(), + ), + ) + .onDisabled( + .color({{typePrefix}}Tokens.grayA3()) + .containerEffects( + RemixBoxEffectsMix.behindContent( + RemixBoxEffectLayerMix(shadowToken: {{typePrefix}}Tokens.shadow1Layers), + ), + ) + .containerEffects( + RemixBoxEffectsMix.overContent( + RemixBoxEffectLayerMix(shadows: const []), + ), + ) + .indicatorColor({{typePrefix}}Tokens.grayA8()), + ); +} + +RadioStyler _{{valuePrefix}}RadioSurfaceStyler( + {{typePrefix}}RadioSize size, { + required bool highContrast, +}) { + return _{{valuePrefix}}RadioBaseStyler(size) + .color({{typePrefix}}Tokens.colorSurface()) + .containerEffects( + RemixBoxEffectsMix.behindContent(RemixBoxEffectLayerMix()), + ) + .containerEffects( + RemixBoxEffectsMix.overContent( + _{{valuePrefix}}RadioInsetRing({{typePrefix}}Tokens.grayA7()), + ), + ) + .indicator( + .color( + {{typePrefix}}Tokens.accent9(), + ).borderRadius(.all({{typePrefix}}Tokens.radiusCircle())), + ) + .onSelected( + .color( + highContrast + ? {{typePrefix}}Tokens.accent12() + : {{typePrefix}}Tokens.accentIndicator(), + ) + .containerEffects( + RemixBoxEffectsMix.behindContent(RemixBoxEffectLayerMix()), + ) + .containerEffects( + RemixBoxEffectsMix.overContent( + RemixBoxEffectLayerMix(shadows: const []), + ), + ) + .indicatorColor( + highContrast + ? {{typePrefix}}Tokens.accent1() + : {{typePrefix}}Tokens.accentContrast(), + ), + ) + .onDisabled( + .color({{typePrefix}}Tokens.grayA3()) + .containerEffects( + RemixBoxEffectsMix.behindContent(RemixBoxEffectLayerMix()), + ) + .containerEffects( + RemixBoxEffectsMix.overContent( + _{{valuePrefix}}RadioInsetRing({{typePrefix}}Tokens.grayA6()), + ), + ) + .indicatorColor({{typePrefix}}Tokens.grayA8()), + ); +} + +RadioStyler _{{valuePrefix}}RadioSoftStyler( + {{typePrefix}}RadioSize size, { + required bool highContrast, +}) { + return _{{valuePrefix}}RadioBaseStyler(size) + .color({{typePrefix}}Tokens.accentA4()) + .containerEffects( + RemixBoxEffectsMix.behindContent(RemixBoxEffectLayerMix()), + ) + .indicator( + .color( + highContrast ? {{typePrefix}}Tokens.accent12() : {{typePrefix}}Tokens.accent11(), + ).borderRadius(.all({{typePrefix}}Tokens.radiusCircle())), + ) + .onSelected( + .color({{typePrefix}}Tokens.accentA4()) + .containerEffects( + RemixBoxEffectsMix.behindContent(RemixBoxEffectLayerMix()), + ) + .indicator( + .color( + highContrast + ? {{typePrefix}}Tokens.accent12() + : {{typePrefix}}Tokens.accent11(), + ), + ), + ) + .onDisabled( + .color({{typePrefix}}Tokens.grayA3()) + .containerEffects( + RemixBoxEffectsMix.behindContent(RemixBoxEffectLayerMix()), + ) + .indicatorColor({{typePrefix}}Tokens.grayA8()), + ); +} + +({double size, double indicatorSize}) _{{valuePrefix}}RadioMetrics( + {{typePrefix}}RadioSize size, +) => switch (size) { + .size1 => ( + size: {{typePrefix}}Tokens.checkboxSize1(), + indicatorSize: {{typePrefix}}Tokens.radioIndicatorSize1(), + ), + .size2 => ( + size: {{typePrefix}}Tokens.space4(), + indicatorSize: {{typePrefix}}Tokens.radioIndicatorSize2(), + ), + .size3 => ( + size: {{typePrefix}}Tokens.checkboxSize3(), + indicatorSize: {{typePrefix}}Tokens.radioIndicatorSize3(), + ), +}; + +RemixBoxEffectLayerMix _{{valuePrefix}}RadioInsetRing(Color color) => + RemixBoxEffectLayerMix( + shadows: [RemixBoxShadowMix(kind: .inset, color: color, spreadRadius: 1)], + ); diff --git a/packages/remix_cli/lib/src/registry/fortal/templates/segmented_control/segmented_control.dart.tmpl b/packages/remix_cli/lib/src/registry/fortal/templates/segmented_control/segmented_control.dart.tmpl new file mode 100644 index 000000000..5e6cca339 --- /dev/null +++ b/packages/remix_cli/lib/src/registry/fortal/templates/segmented_control/segmented_control.dart.tmpl @@ -0,0 +1,236 @@ +import 'package:flutter/widgets.dart'; +import 'package:mix_annotations/mix_annotations.dart'; +import 'package:remix/remix.dart'; + +import '../theme/theme.dart'; + +part 'segmented_control.g.dart'; + +double _resolveSegmentedControlActiveLetterSpacing3(BuildContext context) { + final fontSize = {{typePrefix}}Tokens.text3.resolve(context).fontSize!; + return -0.01 * fontSize; +} + +const _segmentedControlActiveLetterSpacing3 = ContextToken( + _resolveSegmentedControlActiveLetterSpacing3, +); + +/// Radix layers the track as `color-surface` under a `gray-a3` +/// background-image. One BoxDecoration cannot stack two background fills, so +/// the recipe pre-blends the pair; a foreground overlay would instead paint +/// over the selected indicator fill, breaking the source z-order. +Color _resolveSegmentedControlTrackBackground(BuildContext context) => + Color.alphaBlend( + {{typePrefix}}Tokens.grayA3.resolve(context), + {{typePrefix}}Tokens.colorSurface.resolve(context), + ); + +const _segmentedControlTrackBackground = ContextToken( + _resolveSegmentedControlTrackBackground, +); + +/// The disabled root swaps only `background-color` to `gray-3`; the `gray-a3` +/// background-image layer persists in the source, so it stays in the blend. +Color _resolveSegmentedControlDisabledTrackBackground(BuildContext context) => + Color.alphaBlend( + {{typePrefix}}Tokens.grayA3.resolve(context), + {{typePrefix}}Tokens.gray3.resolve(context), + ); + +const _segmentedControlDisabledTrackBackground = ContextToken( + _resolveSegmentedControlDisabledTrackBackground, +); + +/// Radix Themes SegmentedControl size presets. +enum {{typePrefix}}SegmentedControlSize { size1, size2, size3 } + +/// Radix Themes SegmentedControl variants. +enum {{typePrefix}}SegmentedControlVariant { surface, classic } + +/// {{typePrefix}} recipe for [RemixSegmentedControl]. +/// +/// Paints the selected item in place. It does not reproduce Radix's sliding +/// indicator, duplicate-label crossfade, inactive separators, or max-content +/// overflow. Changing an item's label with the selection can therefore cause a +/// small intrinsic-width shift. +@MixWidget(target: RemixSegmentedControl.new) +SegmentedControlStyler {{valuePrefix}}SegmentedControlStyle({ + {{typePrefix}}SegmentedControlVariant variant = .surface, + {{typePrefix}}SegmentedControlSize size = .size2, + SegmentedControlStyler style = const SegmentedControlStyler.create(), +}) { + final metrics = _{{valuePrefix}}SegmentedControlMetrics(size); + final item = _{{valuePrefix}}SegmentedControlItemStyle(variant, metrics); + + return SegmentedControlStyler() + .mainAxisSize(.min) + .minHeight(metrics.height) + .borderRadius(.all(metrics.radius)) + .color(_segmentedControlTrackBackground()) + .clipBehavior(.antiAlias) + .item(item) + .onDisabled( + SegmentedControlStyler().color( + _segmentedControlDisabledTrackBackground(), + ), + ) + .merge(style); +} + +SegmentedControlItemStyler _{{valuePrefix}}SegmentedControlItemStyle( + {{typePrefix}}SegmentedControlVariant variant, + _{{typePrefix}}SegmentedControlMetrics metrics, +) { + final base = SegmentedControlItemStyler() + .minHeight(metrics.height) + .padding(.horizontal(metrics.paddingX)) + .spacing(metrics.itemGap) + .label( + TextStyler() + .style(metrics.text.mix()) + .color({{typePrefix}}Tokens.gray12()) + .fontWeight({{typePrefix}}Tokens.fontWeightRegular()) + .letterSpacing(0) + .wordSpacing(0) + // Radix keeps `min-width: max-content` on the track, so a label + // never wraps and the track overflows a narrow parent instead. + // The equal-segment layout shrinks to fit, so pin one line and + // ellipsize to preserve the same single-line behavior. + .maxLines(1) + .overflow(TextOverflow.ellipsis), + ) + .icon(IconStyler().color({{typePrefix}}Tokens.gray12())) + .containerEffects( + RemixBoxEffectsMix( + behindContent: RemixBoxEffectLayerMix(), + overContent: RemixBoxEffectLayerMix(), + ), + ); + final selected = _{{valuePrefix}}SegmentedControlSelectedItem(variant, metrics); + final disabled = SegmentedControlItemStyler() + .label(TextStyler().color({{typePrefix}}Tokens.grayA8())) + .icon(IconStyler().color({{typePrefix}}Tokens.grayA8())); + final disabledSelected = disabled + .color(const Color(0x00000000)) + .borderRadius(.all(metrics.radius)) + .containerEffects( + RemixBoxEffectsMix( + behindContent: _{{valuePrefix}}SegmentedControlFill({{typePrefix}}Tokens.grayA3()), + overContent: RemixBoxEffectLayerMix(shadows: const []), + ), + ); + + return base + .onHovered(.color({{typePrefix}}Tokens.grayA2())) + .onSelected( + selected + .onHovered(.color(const Color(0x00000000))) + .onDisabled(disabledSelected), + ) + .onFocusVisible( + SegmentedControlItemStyler() + .borderRadius(.all(metrics.radius)) + .containerEffects( + {{valuePrefix}}FocusOutline({{typePrefix}}Tokens.focus8(), offset: -1), + ), + ) + .onDisabled(disabled.onSelected(disabledSelected)); +} + +SegmentedControlItemStyler _{{valuePrefix}}SegmentedControlSelectedItem( + {{typePrefix}}SegmentedControlVariant variant, + _{{typePrefix}}SegmentedControlMetrics metrics, +) { + final overContent = switch (variant) { + .surface => RemixBoxEffectLayerMix( + shadows: [ + RemixBoxShadowMix( + color: {{typePrefix}}Tokens.grayA4(), + spreadRadius: 1, + shapeInset: 1, + ), + ], + ), + .classic => RemixBoxEffectLayerMix( + shadowToken: {{typePrefix}}Tokens.segmentedControlClassicIndicatorShadows, + ), + }; + + return SegmentedControlItemStyler() + .color(const Color(0x00000000)) + .borderRadius(.all(metrics.radius)) + .label( + TextStyler() + .fontWeight({{typePrefix}}Tokens.fontWeightMedium()) + .letterSpacing(metrics.activeLetterSpacing) + .wordSpacing(0), + ) + .containerEffects( + RemixBoxEffectsMix( + behindContent: _{{valuePrefix}}SegmentedControlFill( + {{typePrefix}}Tokens.segmentedControlIndicatorBackground(), + inset: 1, + ), + overContent: overContent, + ), + ); +} + +RemixBoxEffectLayerMix _{{valuePrefix}}SegmentedControlFill( + Color color, { + double? inset, +}) => RemixBoxEffectLayerMix( + gradients: [ + RemixLinearGradientMix(colors: [color, color]), + ], + gradientInsets: inset == null ? const [] : [inset], +); + +class _{{typePrefix}}SegmentedControlMetrics { + const _{{typePrefix}}SegmentedControlMetrics({ + required this.height, + required this.paddingX, + required this.itemGap, + required this.radius, + required this.text, + required this.activeLetterSpacing, + }); + + final double height; + final double paddingX; + final double itemGap; + final Radius radius; + final TextStyleToken text; + final double activeLetterSpacing; +} + +_{{typePrefix}}SegmentedControlMetrics _{{valuePrefix}}SegmentedControlMetrics( + {{typePrefix}}SegmentedControlSize size, +) => switch (size) { + .size1 => _{{typePrefix}}SegmentedControlMetrics( + height: {{typePrefix}}Tokens.space5(), + paddingX: {{typePrefix}}Tokens.space3(), + itemGap: {{typePrefix}}Tokens.space1(), + radius: {{typePrefix}}Tokens.radius2OrFull(), + text: {{typePrefix}}Tokens.text1, + activeLetterSpacing: {{typePrefix}}Tokens.tabActiveLetterSpacing1(), + ), + .size2 => _{{typePrefix}}SegmentedControlMetrics( + height: {{typePrefix}}Tokens.space6(), + paddingX: {{typePrefix}}Tokens.space4(), + itemGap: {{typePrefix}}Tokens.space2(), + radius: {{typePrefix}}Tokens.radius2OrFull(), + text: {{typePrefix}}Tokens.text2, + activeLetterSpacing: {{typePrefix}}Tokens.tabActiveLetterSpacing2(), + ), + .size3 => _{{typePrefix}}SegmentedControlMetrics( + height: {{typePrefix}}Tokens.space7(), + paddingX: {{typePrefix}}Tokens.space4(), + itemGap: {{typePrefix}}Tokens.space3(), + radius: {{typePrefix}}Tokens.radius3OrFull(), + text: {{typePrefix}}Tokens.text3, + // The pinned `-0.01em` is derived from the resolved size-3 text token so + // it remains exact at every {{typePrefix}} scaling without adding an eighth token. + activeLetterSpacing: _segmentedControlActiveLetterSpacing3(), + ), +}; diff --git a/packages/remix_cli/lib/src/registry/fortal/templates/select/select.dart.tmpl b/packages/remix_cli/lib/src/registry/fortal/templates/select/select.dart.tmpl new file mode 100644 index 000000000..369410ab9 --- /dev/null +++ b/packages/remix_cli/lib/src/registry/fortal/templates/select/select.dart.tmpl @@ -0,0 +1,319 @@ +import 'package:flutter/widgets.dart'; +import 'package:mix_annotations/mix_annotations.dart'; +import 'package:remix/remix.dart'; + +import '../theme/theme.dart'; + +part 'select.g.dart'; + +/// Radix Themes Select root size presets. +enum {{typePrefix}}SelectSize { size1, size2, size3 } + +/// Radix Themes Select variants. +enum {{typePrefix}}SelectVariant { surface, soft, ghost } + +/// {{typePrefix}}-themed Select with Radix-owned trigger and content configuration. +@MixWidget(target: RemixSelect.new) +SelectStyler {{valuePrefix}}SelectStyle({ + {{typePrefix}}SelectVariant variant = .surface, + {{typePrefix}}SelectSize size = .size2, + bool highContrast = false, + SelectStyler style = const SelectStyler.create(), +}) { + return SelectStyler() + .trigger(_{{valuePrefix}}SelectTriggerStyler(variant, size)) + .content(_{{valuePrefix}}SelectContentStyler(size)) + .item(_{{valuePrefix}}SelectItemStyler(variant, size, highContrast: highContrast)) + .merge(style); +} + +/// Creates the established combined-variant Select item recipe. +SelectMenuItemStyler {{valuePrefix}}SelectMenuItemStyle({ + {{typePrefix}}SelectVariant variant = .surface, + {{typePrefix}}SelectSize size = .size2, + bool highContrast = false, +}) => _{{valuePrefix}}SelectItemStyler(variant, size, highContrast: highContrast); + +SelectTriggerStyler _{{valuePrefix}}SelectTriggerStyler( + {{typePrefix}}SelectVariant variant, + {{typePrefix}}SelectSize size, +) { + final radius = _{{valuePrefix}}SelectTriggerRadius(size); + final base = SelectTriggerStyler() + .direction(.horizontal) + .mainAxisAlignment(.spaceBetween) + .borderRadius(.all(radius)) + .label(_{{valuePrefix}}SelectTriggerText(size, color: {{typePrefix}}Tokens.gray12())) + .placeholder( + _{{valuePrefix}}SelectTriggerText(size, color: {{typePrefix}}Tokens.grayA10()), + ) + .icon(.color({{typePrefix}}Tokens.gray12())) + .indicator(.color({{typePrefix}}Tokens.gray12()).size(size == .size3 ? 11 : 9)) + .onFocusVisible( + .containerEffects( + RemixBoxEffectsMix.overContent(_{{valuePrefix}}SelectFocusRing()), + ), + ) + .merge(_{{valuePrefix}}SelectTriggerSizeStyler(variant, size)); + + return switch (variant) { + .surface => _{{valuePrefix}}SelectSurfaceTrigger(base), + .soft => _{{valuePrefix}}SelectSoftTrigger(base), + .ghost => _{{valuePrefix}}SelectGhostTrigger(base), + }; +} + +TextStyler _{{valuePrefix}}SelectTriggerText({{typePrefix}}SelectSize size, {Color? color}) { + final token = switch (size) { + .size1 => {{typePrefix}}Tokens.text1, + .size2 => {{typePrefix}}Tokens.text2, + .size3 => {{typePrefix}}Tokens.text3, + }; + return TextStyler(style: token.mix()) + .fontWeight({{typePrefix}}Tokens.fontWeightRegular()) + .color(color ?? {{typePrefix}}Tokens.gray12()); +} + +Radius _{{valuePrefix}}SelectTriggerRadius({{typePrefix}}SelectSize size) => switch (size) { + .size1 => {{typePrefix}}Tokens.radius1OrFull(), + .size2 => {{typePrefix}}Tokens.radius2OrFull(), + .size3 => {{typePrefix}}Tokens.radius3OrFull(), +}; + +SelectTriggerStyler _{{valuePrefix}}SelectTriggerSizeStyler( + {{typePrefix}}SelectVariant variant, + {{typePrefix}}SelectSize size, +) { + final style = SelectTriggerStyler().spacing(switch (size) { + .size1 => {{typePrefix}}Tokens.space1(), + .size2 => {{typePrefix}}Tokens.selectSpace1Half(), + .size3 => {{typePrefix}}Tokens.space2(), + }); + return switch (variant) { + .ghost => switch (size) { + .size1 || .size2 => + style + .padding(.horizontal({{typePrefix}}Tokens.space2())) + .padding(.vertical({{typePrefix}}Tokens.space1())) + .margin(.horizontal({{typePrefix}}Tokens.selectGhostMarginX12())) + .margin(.vertical({{typePrefix}}Tokens.selectGhostMarginY12())), + .size3 => + style + .padding(.horizontal({{typePrefix}}Tokens.space3())) + .padding(.vertical({{typePrefix}}Tokens.selectSpace1Half())) + .margin(.horizontal({{typePrefix}}Tokens.selectGhostMarginX3())) + .margin(.vertical({{typePrefix}}Tokens.selectGhostMarginY3())), + }, + .surface || .soft => switch (size) { + .size1 => + style + .height({{typePrefix}}Tokens.space5()) + .padding(.horizontal({{typePrefix}}Tokens.space2())), + .size2 => + style + .height({{typePrefix}}Tokens.space6()) + .padding(.horizontal({{typePrefix}}Tokens.space3())), + .size3 => + style + .height({{typePrefix}}Tokens.space7()) + .padding(.horizontal({{typePrefix}}Tokens.space4())), + }, + }; +} + +RemixBoxEffectLayerMix _{{valuePrefix}}SelectFocusRing() { + return RemixBoxEffectLayerMix( + shadows: [ + RemixBoxShadowMix(color: {{typePrefix}}Tokens.focus8(), spreadRadius: 1), + RemixBoxShadowMix( + kind: RemixBoxShadowKind.inset, + color: {{typePrefix}}Tokens.focus8(), + spreadRadius: 1, + ), + ], + ); +} + +SelectTriggerStyler _{{valuePrefix}}SelectSurfaceTrigger(SelectTriggerStyler base) { + return base + .indicatorOpacity(0.9) + .color({{typePrefix}}Tokens.colorSurface()) + .containerEffects( + RemixBoxEffectsMix.behindContent( + {{valuePrefix}}InsetSurface(strokes: [{{typePrefix}}Tokens.grayA7()]), + ), + ) + .onHovered( + .containerEffects( + RemixBoxEffectsMix.behindContent( + {{valuePrefix}}InsetSurface(strokes: [{{typePrefix}}Tokens.grayA8()]), + ), + ), + ) + .onSelected( + .containerEffects( + RemixBoxEffectsMix.behindContent( + {{valuePrefix}}InsetSurface(strokes: [{{typePrefix}}Tokens.grayA8()]), + ), + ), + ) + .onDisabled( + .color({{typePrefix}}Tokens.grayA2()) + .label(.color({{typePrefix}}Tokens.grayA11())) + .icon(.color({{typePrefix}}Tokens.grayA9())) + .indicator(.color({{typePrefix}}Tokens.grayA9())) + .containerEffects( + RemixBoxEffectsMix.behindContent( + {{valuePrefix}}InsetSurface(strokes: [{{typePrefix}}Tokens.grayA6()]), + ), + ), + ); +} + +SelectTriggerStyler _{{valuePrefix}}SelectSoftTrigger(SelectTriggerStyler base) { + return base + .label(.color({{typePrefix}}Tokens.accent12())) + .placeholder(.color({{typePrefix}}Tokens.accent12())) + .placeholderOpacity(0.6) + .icon(.color({{typePrefix}}Tokens.accent12())) + .indicator(.color({{typePrefix}}Tokens.accent12())) + .color({{typePrefix}}Tokens.accentA3()) + .onHovered(.color({{typePrefix}}Tokens.accentA4())) + .onSelected(.color({{typePrefix}}Tokens.accentA4())) + .onDisabled( + .label(.color({{typePrefix}}Tokens.grayA11())) + .icon(.color({{typePrefix}}Tokens.grayA9())) + .indicator(.color({{typePrefix}}Tokens.grayA9())) + .color({{typePrefix}}Tokens.grayA3()), + ); +} + +SelectTriggerStyler _{{valuePrefix}}SelectGhostTrigger(SelectTriggerStyler base) { + return base + .label(.color({{typePrefix}}Tokens.accent12())) + .placeholder(.color({{typePrefix}}Tokens.accent12())) + .placeholderOpacity(0.6) + .icon(.color({{typePrefix}}Tokens.accent12())) + .indicator(.color({{typePrefix}}Tokens.accent12())) + .color(const Color(0x00000000)) + .onHovered(.color({{typePrefix}}Tokens.accentA3())) + .onSelected(.color({{typePrefix}}Tokens.accentA3())) + .onDisabled( + .label(.color({{typePrefix}}Tokens.grayA11())) + .icon(.color({{typePrefix}}Tokens.grayA9())) + .indicator(.color({{typePrefix}}Tokens.grayA9())) + .color(const Color(0x00000000)), + ); +} + +SelectContentStyler _{{valuePrefix}}SelectContentStyler({{typePrefix}}SelectSize size) { + final radius = switch (size) { + .size1 => {{typePrefix}}Tokens.radius3(), + .size2 || .size3 => {{typePrefix}}Tokens.radius4(), + }; + return SelectContentStyler() + .padding( + .all(switch (size) { + .size1 => {{typePrefix}}Tokens.space1(), + .size2 || .size3 => {{typePrefix}}Tokens.space2(), + }), + ) + .borderRadius(.all(radius)) + .color({{typePrefix}}Tokens.colorPanel()) + .decoration( + BoxDecorationMix.create(boxShadow: {{typePrefix}}Tokens.shadow5.mix()), + ) + .clipBehavior(Clip.antiAlias) + .containerEffects( + RemixBoxEffectsMix.backdropBlur({{typePrefix}}Tokens.panelBlur()), + ); +} + +SelectMenuItemStyler _{{valuePrefix}}SelectItemStyler( + {{typePrefix}}SelectVariant variant, + {{typePrefix}}SelectSize size, { + bool highContrast = false, +}) { + final metrics = _{{valuePrefix}}SelectContentMetrics(size); + final base = SelectMenuItemStyler() + .direction(.horizontal) + .height(metrics.itemHeight) + .padding(.horizontal(metrics.indicatorWidth)) + .borderRadius(.all(metrics.itemRadius)) + .text( + TextStyler(style: metrics.itemText.mix()).color({{typePrefix}}Tokens.gray12()), + ) + .indicator( + BoxStyler( + alignment: .center, + constraints: BoxConstraintsMix.width(metrics.indicatorWidth), + ), + ) + .icon( + IconStyler(color: {{typePrefix}}Tokens.gray12(), size: metrics.indicatorSize), + ); + + final highlighted = switch (variant) { + .surface || .ghost => + SelectMenuItemStyler() + .color( + highContrast ? {{typePrefix}}Tokens.accent12() : {{typePrefix}}Tokens.accent9(), + ) + .text( + TextStyler().color( + highContrast + ? {{typePrefix}}Tokens.accent1() + : {{typePrefix}}Tokens.accentContrast(), + ), + ) + .iconColor( + highContrast + ? {{typePrefix}}Tokens.accent1() + : {{typePrefix}}Tokens.accentContrast(), + ), + .soft => SelectMenuItemStyler().color({{typePrefix}}Tokens.accentA4()), + }; + + // Naked's focused option is Radix's roving `data-highlighted` item, not a + // CSS focus ring, so this intentionally follows raw focus. + return base + .onHovered(highlighted) + .onFocused(highlighted) + .onPressed(highlighted) + .onDisabled( + .color( + const Color(0x00000000), + ).text(.color({{typePrefix}}Tokens.grayA8())).iconColor({{typePrefix}}Tokens.grayA8()), + ); +} + +({ + double itemHeight, + double indicatorWidth, + double indicatorSize, + Radius itemRadius, + TextStyleToken itemText, +}) +_{{valuePrefix}}SelectContentMetrics({{typePrefix}}SelectSize size) => switch (size) { + .size1 => ( + itemHeight: {{typePrefix}}Tokens.space5(), + indicatorWidth: {{typePrefix}}Tokens.selectIndicatorWidth1(), + indicatorSize: {{typePrefix}}Tokens.selectIndicatorSize1(), + itemRadius: {{typePrefix}}Tokens.radius1(), + itemText: {{typePrefix}}Tokens.text1, + ), + .size2 => ( + itemHeight: {{typePrefix}}Tokens.space6(), + indicatorWidth: {{typePrefix}}Tokens.space5(), + indicatorSize: {{typePrefix}}Tokens.selectIndicatorSize2(), + itemRadius: {{typePrefix}}Tokens.radius2(), + itemText: {{typePrefix}}Tokens.text2, + ), + .size3 => ( + itemHeight: {{typePrefix}}Tokens.space6(), + indicatorWidth: {{typePrefix}}Tokens.space5(), + indicatorSize: {{typePrefix}}Tokens.selectIndicatorSize2(), + itemRadius: {{typePrefix}}Tokens.radius2(), + itemText: {{typePrefix}}Tokens.text3, + ), +}; diff --git a/packages/remix_cli/lib/src/registry/fortal/templates/sidebar/sidebar.dart.tmpl b/packages/remix_cli/lib/src/registry/fortal/templates/sidebar/sidebar.dart.tmpl new file mode 100644 index 000000000..2ee310d3f --- /dev/null +++ b/packages/remix_cli/lib/src/registry/fortal/templates/sidebar/sidebar.dart.tmpl @@ -0,0 +1,81 @@ +import 'package:flutter/widgets.dart'; +import 'package:mix_annotations/mix_annotations.dart'; +import 'package:remix/remix.dart'; + +import '../theme/theme.dart'; +import 'text.dart'; +import 'toggle.dart'; + +part 'sidebar.g.dart'; + +const _sectionLabelHorizontalPadding = 10.0; +const _sectionLabelVerticalPadding = 6.0; +const _sectionLabelLetterSpacing = 0.7; +const _destinationSpacing = 2.0; +const _minimumDestinationTargetHeight = 48.0; + +/// {{typePrefix}}-themed preset for [RemixSidebar]. +/// +/// The recipe paints the solid panel surface with a trailing edge border, +/// pads the scrolling destination region, keeps section labels compact and +/// muted, separates sections with {{typePrefix}}'s `space3` token, and reuses the +/// ghost `size2` toggle treatment inside full-width destinations with a +/// 48-logical-pixel minimum height. The footer carries the divider that +/// separates account content from navigation. [highContrast] strengthens +/// section and selected destination content without changing layout. +/// [panelPadding] applies host-owned insets inside the painted panel surface. +/// +/// The recipe sets no panel width and no header padding. Width belongs to the +/// host, which must also size any drawer that presents the same panel, and +/// header metrics usually have to match an application top bar. +@MixWidget(target: RemixSidebar.new) +SidebarStyler {{valuePrefix}}SidebarStyle({ + bool highContrast = false, + EdgeInsetsGeometry? panelPadding, + SidebarStyler style = const SidebarStyler.create(), +}) { + return SidebarStyler( + container: + FlexBoxStyler(padding: EdgeInsetsGeometryMix.maybeValue(panelPadding)) + .color({{typePrefix}}Tokens.colorPanelSolid()) + .border( + .end( + .color( + {{typePrefix}}Tokens.grayA5(), + ).width({{typePrefix}}Tokens.borderWidth1()), + ), + ), + content: FlexBoxStyler() + .spacing({{typePrefix}}Tokens.space3()) + .padding( + .symmetric( + horizontal: {{typePrefix}}Tokens.space3(), + vertical: {{typePrefix}}Tokens.space4(), + ), + ), + footer: BoxStyler().border( + .top(.color({{typePrefix}}Tokens.gray6()).width({{typePrefix}}Tokens.borderWidth1())), + ), + sectionLabel: {{valuePrefix}}TextStyle(size: .size1, weight: .medium) + .color(highContrast ? {{typePrefix}}Tokens.gray12() : {{typePrefix}}Tokens.gray11()) + .uppercase() + .letterSpacing(_sectionLabelLetterSpacing) + .wrap( + .padding( + .symmetric( + horizontal: _sectionLabelHorizontalPadding, + vertical: _sectionLabelVerticalPadding, + ), + ), + ), + destinations: FlexBoxStyler().spacing(_destinationSpacing), + destination: + {{valuePrefix}}ToggleStyle( + variant: .ghost, + size: .size2, + highContrast: highContrast, + ) + .minHeight(_minimumDestinationTargetHeight) + .container(.mainAxisSize(.max).mainAxisAlignment(.start)), + ).merge(style); +} diff --git a/packages/remix_cli/lib/src/registry/fortal/templates/skeleton/skeleton.dart.tmpl b/packages/remix_cli/lib/src/registry/fortal/templates/skeleton/skeleton.dart.tmpl new file mode 100644 index 000000000..904f61895 --- /dev/null +++ b/packages/remix_cli/lib/src/registry/fortal/templates/skeleton/skeleton.dart.tmpl @@ -0,0 +1,27 @@ +import 'package:flutter/widgets.dart'; +import 'package:mix_annotations/mix_annotations.dart'; +import 'package:remix/remix.dart'; + +import '../theme/theme.dart'; + +part 'skeleton.g.dart'; + +/// {{typePrefix}} recipe for [RemixSkeleton]. +/// +/// The pulse starts and rests on `grayA3` before moving toward `grayA4`; +/// Radix's CSS `alternate-reverse` phase starts from `grayA4`. +@MixWidget(target: RemixSkeleton.new) +SkeletonStyler {{valuePrefix}}SkeletonStyle({ + SkeletonStyler style = const SkeletonStyler.create(), +}) { + return SkeletonStyler() + .container( + BoxStyler() + .minHeight({{typePrefix}}Tokens.space3()) + .color({{typePrefix}}Tokens.grayA3()) + .borderRadius(.all({{typePrefix}}Tokens.radius1())), + ) + .pulseColor({{typePrefix}}Tokens.grayA4()) + .duration({{typePrefix}}Tokens.skeletonPulseDuration()) + .merge(style); +} diff --git a/packages/remix_cli/lib/src/registry/fortal/templates/slider/slider.dart.tmpl b/packages/remix_cli/lib/src/registry/fortal/templates/slider/slider.dart.tmpl new file mode 100644 index 000000000..05f390cf6 --- /dev/null +++ b/packages/remix_cli/lib/src/registry/fortal/templates/slider/slider.dart.tmpl @@ -0,0 +1,350 @@ +import 'package:flutter/widgets.dart'; +import 'package:mix_annotations/mix_annotations.dart'; +import 'package:remix/remix.dart'; + +import '../theme/theme.dart'; + +part 'slider.g.dart'; + +/// Radix Themes slider sizes. +enum {{typePrefix}}SliderSize { size1, size2, size3 } + +/// Radix Themes slider variants. +enum {{typePrefix}}SliderVariant { classic, surface, soft } + +/// {{typePrefix}} slider with Radix-owned size, variant, and component overrides. +@MixWidget(target: RemixSlider.new) +SliderStyler {{valuePrefix}}SliderStyle({ + {{typePrefix}}SliderVariant variant = .surface, + {{typePrefix}}SliderSize size = .size2, + bool highContrast = false, + SliderStyler style = const SliderStyler.create(), +}) { + final metrics = _{{valuePrefix}}SliderMetrics(size); + final radius = BorderRadiusMix.all(metrics.trackRadius); + final thumbRadius = BorderRadiusMix.all({{typePrefix}}Tokens.radius1OrThumb()); + final base = SliderStyler() + .track(.borderRadius(radius)) + .range(.borderRadius(radius)) + .thumb( + .size(metrics.thumbSize, metrics.thumbSize).borderRadius(thumbRadius), + ) + .thickness(metrics.trackSize) + .thumbFocusEffects( + RemixBoxEffectsMix.overContent( + RemixBoxEffectLayerMix( + shadows: [ + RemixBoxShadowMix(color: {{typePrefix}}Tokens.accent3(), spreadRadius: 3), + RemixBoxShadowMix(color: {{typePrefix}}Tokens.focus8(), spreadRadius: 5), + ], + ), + ), + ); + + final styled = switch (variant) { + .classic => _{{valuePrefix}}SliderClassic( + base, + trackRadius: radius, + thumbRadius: thumbRadius, + highContrast: highContrast, + ), + .surface => _{{valuePrefix}}SliderSurface( + base, + trackRadius: radius, + thumbRadius: thumbRadius, + highContrast: highContrast, + ), + .soft => _{{valuePrefix}}SliderSoft( + base, + trackRadius: radius, + thumbRadius: thumbRadius, + highContrast: highContrast, + ), + }; + return styled + .onDisabled( + _{{valuePrefix}}SliderDisabled( + variant, + trackRadius: radius, + thumbRadius: thumbRadius, + ), + ) + .variant( + ContextVariant( + '{{valuePrefix}}SliderDisabledDarkBlend', + (context) => {{typePrefix}}Theme.of(context).isDark, + ), + SliderStyler().onDisabled(.blendMode(BlendMode.screen)), + ) + .merge(style); +} + +SliderStyler _{{valuePrefix}}SliderSurface( + SliderStyler base, { + required BorderRadiusMix trackRadius, + required BorderRadiusMix thumbRadius, + required bool highContrast, +}) => base + .track(.color({{typePrefix}}Tokens.grayA3())) + .range(.color({{typePrefix}}Tokens.accentTrack())) + .thumbColor(const Color(0xFFFFFFFF)) + .trackEffects( + RemixBoxEffectsMix.behindContent( + {{valuePrefix}}InsetSurface(strokes: [{{typePrefix}}Tokens.grayA5()]), + ), + ) + .rangeEffects( + RemixBoxEffectsMix.behindContent( + {{valuePrefix}}InsetSurface(strokes: [{{typePrefix}}Tokens.grayA5()]).merge( + RemixBoxEffectLayerMix( + gradients: _{{valuePrefix}}SliderHighContrastGradients(highContrast), + ), + ), + ), + ) + .thumb( + BoxStyler().decoration( + .boxShadow([ + BoxShadowMix(color: {{typePrefix}}Tokens.blackA4(), spreadRadius: 1), + ]), + ), + ); + +SliderStyler _{{valuePrefix}}SliderClassic( + SliderStyler base, { + required BorderRadiusMix trackRadius, + required BorderRadiusMix thumbRadius, + required bool highContrast, +}) => base + .track(.color({{typePrefix}}Tokens.grayA3())) + .range(.color({{typePrefix}}Tokens.accentTrack())) + .thumbColor(const Color(0xFFFFFFFF)) + .trackEffects( + RemixBoxEffectsMix.overContent( + RemixBoxEffectLayerMix(shadowToken: {{typePrefix}}Tokens.shadow1Layers), + ), + ) + .rangeEffects( + RemixBoxEffectsMix.behindContent( + RemixBoxEffectLayerMix( + gradients: _{{valuePrefix}}SliderHighContrastGradients(highContrast), + shadows: highContrast + ? [ + _{{valuePrefix}}SliderInset({{typePrefix}}Tokens.grayA3()), + _{{valuePrefix}}SliderInset({{typePrefix}}Tokens.blackA2()), + _{{valuePrefix}}SliderInset( + {{typePrefix}}Tokens.blackA2(), + offset: const Offset(0, 1.5), + blurRadius: 2, + spreadRadius: 0, + ), + ] + : [ + _{{valuePrefix}}SliderInset({{typePrefix}}Tokens.grayA3()), + _{{valuePrefix}}SliderInset({{typePrefix}}Tokens.accentA4()), + _{{valuePrefix}}SliderInset({{typePrefix}}Tokens.blackA1()), + _{{valuePrefix}}SliderInset( + {{typePrefix}}Tokens.blackA2(), + offset: const Offset(0, 1.5), + blurRadius: 2, + spreadRadius: 0, + ), + ], + ), + ), + ) + .thumb( + BoxStyler().decoration( + .boxShadow([ + BoxShadowMix(color: {{typePrefix}}Tokens.blackA3(), spreadRadius: 1), + BoxShadowMix( + color: {{typePrefix}}Tokens.blackA1(), + offset: const Offset(0, 1), + blurRadius: 3, + ), + BoxShadowMix( + color: {{typePrefix}}Tokens.blackA1(), + offset: const Offset(0, 2), + blurRadius: 4, + spreadRadius: -1, + ), + ]), + ), + ); + +SliderStyler _{{valuePrefix}}SliderSoft( + SliderStyler base, { + required BorderRadiusMix trackRadius, + required BorderRadiusMix thumbRadius, + required bool highContrast, +}) => base + .track(.color({{typePrefix}}Tokens.grayA4())) + .range(.color({{typePrefix}}Tokens.accent6())) + .thumbColor(const Color(0xFFFFFFFF)) + .trackEffects( + RemixBoxEffectsMix.behindContent( + RemixBoxEffectLayerMix( + gradients: [ + RemixLinearGradientMix( + colors: [{{typePrefix}}Tokens.whiteA1(), {{typePrefix}}Tokens.whiteA1()], + ), + ], + ), + ), + ) + .rangeEffects( + RemixBoxEffectsMix.behindContent( + RemixBoxEffectLayerMix( + gradients: [ + RemixLinearGradientMix( + colors: [{{typePrefix}}Tokens.accentA5(), {{typePrefix}}Tokens.accentA5()], + ), + ..._{{valuePrefix}}SliderHighContrastGradients(highContrast), + ], + ), + ), + ) + .thumb( + BoxStyler().decoration( + .boxShadow([ + BoxShadowMix(color: {{typePrefix}}Tokens.blackA3(), spreadRadius: 1), + BoxShadowMix(color: {{typePrefix}}Tokens.grayA2(), spreadRadius: 1), + BoxShadowMix(color: {{typePrefix}}Tokens.accentA2(), spreadRadius: 1), + BoxShadowMix( + color: {{typePrefix}}Tokens.grayA4(), + offset: const Offset(0, 1), + blurRadius: 2, + ), + BoxShadowMix( + color: {{typePrefix}}Tokens.grayA3(), + offset: const Offset(0, 1), + blurRadius: 3, + spreadRadius: -0.5, + ), + ]), + ), + ); + +SliderStyler _{{valuePrefix}}SliderDisabled( + {{typePrefix}}SliderVariant variant, { + required BorderRadiusMix trackRadius, + required BorderRadiusMix thumbRadius, +}) { + final track = switch (variant) { + .surface => + SliderStyler() + .track(.color({{typePrefix}}Tokens.grayA3())) + .trackEffects( + RemixBoxEffectsMix.behindContent( + {{valuePrefix}}InsetSurface(strokes: [{{typePrefix}}Tokens.grayA4()]), + ), + ), + .classic => + SliderStyler() + .track(.color({{typePrefix}}Tokens.grayA3())) + .trackEffects( + RemixBoxEffectsMix.overContent( + RemixBoxEffectLayerMix( + shadowToken: {{typePrefix}}Tokens.sliderClassicDisabledTrackShadows, + ), + ), + ), + .soft => + SliderStyler() + .track(.color({{typePrefix}}Tokens.grayA4())) + .trackEffects( + RemixBoxEffectsMix.behindContent( + RemixBoxEffectLayerMix(gradients: const []), + ), + ), + }; + return track + .range(.color(const Color(0x00000000))) + .thumbColor({{typePrefix}}Tokens.gray1()) + .rangeEffects( + RemixBoxEffectsMix.behindContent( + RemixBoxEffectLayerMix(gradients: const [], shadows: const []), + ), + ) + .rangeEffects( + RemixBoxEffectsMix.overContent( + RemixBoxEffectLayerMix(gradients: const [], shadows: const []), + ), + ) + .thumb( + BoxStyler().decoration( + .boxShadow([ + BoxShadowMix( + color: switch (variant) { + .soft => {{typePrefix}}Tokens.gray5(), + .classic || .surface => {{typePrefix}}Tokens.gray6(), + }, + spreadRadius: 1, + ), + ]), + ), + ) + .thumbFocusEffects( + RemixBoxEffectsMix.overContent( + RemixBoxEffectLayerMix(shadows: const []), + ), + ) + .blendMode(BlendMode.multiply); +} + +RemixBoxShadowMix _{{valuePrefix}}SliderInset( + Color color, { + Offset offset = Offset.zero, + double blurRadius = 0, + double spreadRadius = 1, +}) => RemixBoxShadowMix( + kind: RemixBoxShadowKind.inset, + color: color, + offset: offset, + blurRadius: blurRadius, + spreadRadius: spreadRadius, +); + +List _{{valuePrefix}}SliderHighContrastGradients( + bool highContrast, +) => highContrast + ? [ + RemixLinearGradientMix( + colors: [ + {{typePrefix}}Tokens.sliderHighContrastOverlay(), + {{typePrefix}}Tokens.sliderHighContrastOverlay(), + ], + ), + ] + : const []; + +class _{{typePrefix}}SliderMetrics { + const _{{typePrefix}}SliderMetrics({ + required this.trackSize, + required this.thumbSize, + required this.trackRadius, + }); + + final double trackSize; + final double thumbSize; + final Radius trackRadius; +} + +_{{typePrefix}}SliderMetrics _{{valuePrefix}}SliderMetrics({{typePrefix}}SliderSize size) => + switch (size) { + .size1 => _{{typePrefix}}SliderMetrics( + trackSize: {{typePrefix}}Tokens.sliderTrackSize1(), + thumbSize: {{typePrefix}}Tokens.sliderThumbSize1(), + trackRadius: {{typePrefix}}Tokens.sliderTrackRadius1(), + ), + .size2 => _{{typePrefix}}SliderMetrics( + trackSize: {{typePrefix}}Tokens.sliderTrackSize2(), + thumbSize: {{typePrefix}}Tokens.sliderThumbSize2(), + trackRadius: {{typePrefix}}Tokens.sliderTrackRadius2(), + ), + .size3 => _{{typePrefix}}SliderMetrics( + trackSize: {{typePrefix}}Tokens.sliderTrackSize3(), + thumbSize: {{typePrefix}}Tokens.sliderThumbSize3(), + trackRadius: {{typePrefix}}Tokens.sliderTrackRadius3(), + ), + }; diff --git a/packages/remix_cli/lib/src/registry/fortal/templates/spinner/spinner.dart.tmpl b/packages/remix_cli/lib/src/registry/fortal/templates/spinner/spinner.dart.tmpl new file mode 100644 index 000000000..33fa2bf87 --- /dev/null +++ b/packages/remix_cli/lib/src/registry/fortal/templates/spinner/spinner.dart.tmpl @@ -0,0 +1,31 @@ +import 'package:flutter/widgets.dart'; +import 'package:mix_annotations/mix_annotations.dart'; +import 'package:remix/remix.dart'; + +import '../theme/theme.dart'; + +part 'spinner.g.dart'; + +/// {{typePrefix}} spinner size presets. +enum {{typePrefix}}SpinnerSize { size1, size2, size3 } + +/// {{typePrefix}}-themed preset for [RemixSpinner] using the inherited foreground color. +@MixWidget(target: RemixSpinner.new) +SpinnerStyler {{valuePrefix}}SpinnerStyle({ + {{typePrefix}}SpinnerSize size = .size2, + SpinnerStyler style = const SpinnerStyler.create(), +}) { + return SpinnerStyler( + opacity: 0.65, + leafRadius: {{typePrefix}}Tokens.radius1(), + duration: const Duration(milliseconds: 800), + ).merge(_{{valuePrefix}}SpinnerSizeStyler(size)).merge(style); +} + +SpinnerStyler _{{valuePrefix}}SpinnerSizeStyler({{typePrefix}}SpinnerSize size) { + return switch (size) { + .size1 => SpinnerStyler(size: {{typePrefix}}Tokens.space3()), + .size2 => SpinnerStyler(size: {{typePrefix}}Tokens.space4()), + .size3 => SpinnerStyler(size: {{typePrefix}}Tokens.spinnerSize3()), + }; +} diff --git a/packages/remix_cli/lib/src/registry/fortal/templates/switch/switch.dart.tmpl b/packages/remix_cli/lib/src/registry/fortal/templates/switch/switch.dart.tmpl new file mode 100644 index 000000000..58a85469f --- /dev/null +++ b/packages/remix_cli/lib/src/registry/fortal/templates/switch/switch.dart.tmpl @@ -0,0 +1,354 @@ +import 'package:flutter/widgets.dart'; +import 'package:mix_annotations/mix_annotations.dart'; +import 'package:remix/remix.dart'; + +import '../theme/theme.dart'; + +part 'switch.g.dart'; + +/// {{typePrefix}} switch size presets. +enum {{typePrefix}}SwitchSize { + /// Compact switch. + size1, + + /// Default switch. + size2, + + /// Large switch. + size3, +} + +/// {{typePrefix}} switch color variants. +enum {{typePrefix}}SwitchVariant { + /// Raised treatment with Radix's classic shadows. + classic, + + /// Surface treatment with a visible border. + surface, + + /// Softer accent treatment. + soft, +} + +/// {{typePrefix}}-themed preset for [RemixSwitch]. +@MixWidget(target: RemixSwitch.new) +SwitchStyler {{valuePrefix}}SwitchStyle({ + {{typePrefix}}SwitchVariant variant = .surface, + {{typePrefix}}SwitchSize size = .size2, + bool highContrast = false, + SwitchStyler style = const SwitchStyler.create(), +}) { + return (switch (variant) { + .classic => _{{valuePrefix}}SwitchClassicStyler(size, highContrast: highContrast), + .surface => _{{valuePrefix}}SwitchSurfaceStyler(size, highContrast: highContrast), + .soft => _{{valuePrefix}}SwitchSoftStyler(size, highContrast: highContrast), + }).merge(style); +} + +SwitchStyler _{{valuePrefix}}SwitchBaseStyler({{typePrefix}}SwitchSize size) { + final metrics = _{{valuePrefix}}SwitchMetrics(size); + return SwitchStyler( + container: .size( + metrics.width, + metrics.height, + ).padding(.all(1)).borderRadius(.all(metrics.radius)), + thumb: .size( + metrics.thumbSize, + metrics.thumbSize, + ).borderRadius(.all(metrics.radius)), + trackEffects: RemixBoxEffectsMix( + behindContent: _{{valuePrefix}}SwitchLayer(), + overContent: _{{valuePrefix}}SwitchLayer(), + ), + ) + .thumbColor(const Color(0xFFFFFFFF)) + .onFocusVisible( + .trackEffects( + RemixBoxEffectsMix( + outline: BorderSideMix( + color: {{typePrefix}}Tokens.focus8(), + width: 2, + strokeAlign: BorderSide.strokeAlignInside, + ), + outlineOffset: 2, + ), + ), + ); +} + +SwitchStyler _{{valuePrefix}}SwitchClassicStyler( + {{typePrefix}}SwitchSize size, { + required bool highContrast, +}) { + return _{{valuePrefix}}SwitchBaseStyler(size) + .trackColor({{typePrefix}}Tokens.grayA4()) + .trackEffects( + RemixBoxEffectsMix.behindContent( + _{{valuePrefix}}SwitchLayer(shadowToken: {{typePrefix}}Tokens.shadow1Layers), + ), + ) + .thumb( + _{{valuePrefix}}SwitchThumbStyler(selected: false, highContrast: highContrast), + ) + .onSelected( + SwitchStyler() + .trackColor( + highContrast + ? {{typePrefix}}Tokens.accent12() + : {{typePrefix}}Tokens.accentTrack(), + ) + .trackEffects( + RemixBoxEffectsMix.behindContent( + _{{valuePrefix}}SwitchLayer( + shadows: [ + RemixBoxShadowMix( + kind: .inset, + color: {{typePrefix}}Tokens.grayA3(), + spreadRadius: 1, + ), + RemixBoxShadowMix( + kind: .inset, + color: highContrast + ? {{typePrefix}}Tokens.blackA2() + : {{typePrefix}}Tokens.accentA4(), + spreadRadius: 1, + ), + RemixBoxShadowMix( + kind: .inset, + color: {{typePrefix}}Tokens.blackA2(), + offset: const Offset(0, 1.5), + blurRadius: 2, + ), + ], + ), + ), + ) + .thumb( + _{{valuePrefix}}SwitchThumbStyler( + selected: true, + highContrast: highContrast, + ), + ), + ) + .onPressed( + SwitchStyler() + .trackColor({{typePrefix}}Tokens.grayA5()) + .trackEffects( + RemixBoxEffectsMix.behindContent(_{{valuePrefix}}SwitchLayer()), + ), + ) + .onDisabled(_{{valuePrefix}}SwitchDisabledStyler(classic: true)); +} + +SwitchStyler _{{valuePrefix}}SwitchSurfaceStyler( + {{typePrefix}}SwitchSize size, { + required bool highContrast, +}) { + return _{{valuePrefix}}SwitchBaseStyler(size) + .trackColor({{typePrefix}}Tokens.grayA3()) + .trackEffects(RemixBoxEffectsMix.behindContent(_{{valuePrefix}}SwitchLayer())) + .trackEffects( + RemixBoxEffectsMix.overContent( + _{{valuePrefix}}SwitchInsetRing({{typePrefix}}Tokens.grayA5()), + ), + ) + .thumb( + _{{valuePrefix}}SwitchThumbStyler(selected: false, highContrast: highContrast), + ) + .onSelected( + SwitchStyler() + .trackColor( + highContrast + ? {{typePrefix}}Tokens.accent12() + : {{typePrefix}}Tokens.accentTrack(), + ) + .trackEffects( + RemixBoxEffectsMix.behindContent(_{{valuePrefix}}SwitchLayer()), + ) + .thumb( + _{{valuePrefix}}SwitchThumbStyler( + selected: true, + highContrast: highContrast, + ), + ), + ) + .onPressed( + SwitchStyler() + .trackColor({{typePrefix}}Tokens.grayA4()) + .trackEffects( + RemixBoxEffectsMix.behindContent(_{{valuePrefix}}SwitchLayer()), + ), + ) + .onDisabled(_{{valuePrefix}}SwitchDisabledStyler()); +} + +SwitchStyler _{{valuePrefix}}SwitchSoftStyler( + {{typePrefix}}SwitchSize size, { + required bool highContrast, +}) { + return _{{valuePrefix}}SwitchBaseStyler(size) + .trackColor({{typePrefix}}Tokens.grayA3()) + .trackEffects(RemixBoxEffectsMix.behindContent(_{{valuePrefix}}SwitchLayer())) + .thumb(_{{valuePrefix}}SwitchSoftThumbStyler(false)) + .onSelected( + SwitchStyler() + .trackColor( + highContrast ? {{typePrefix}}Tokens.accentA6() : {{typePrefix}}Tokens.accentA4(), + ) + .trackEffects( + RemixBoxEffectsMix.behindContent(_{{valuePrefix}}SwitchLayer()), + ) + .thumb(_{{valuePrefix}}SwitchSoftThumbStyler(true)), + ) + .onPressed( + SwitchStyler() + .trackColor({{typePrefix}}Tokens.grayA4()) + .trackEffects( + RemixBoxEffectsMix.behindContent(_{{valuePrefix}}SwitchLayer()), + ), + ) + .onDisabled(_{{valuePrefix}}SwitchDisabledStyler(soft: true)); +} + +({double width, double height, double thumbSize, Radius radius}) +_{{valuePrefix}}SwitchMetrics({{typePrefix}}SwitchSize size) { + final height = switch (size) { + .size1 => {{typePrefix}}Tokens.space4(), + .size2 => {{typePrefix}}Tokens.switchHeight2(), + .size3 => {{typePrefix}}Tokens.space5(), + }; + final width = switch (size) { + .size1 => {{typePrefix}}Tokens.switchWidth1(), + .size2 => {{typePrefix}}Tokens.switchWidth2(), + .size3 => {{typePrefix}}Tokens.switchWidth3(), + }; + final thumbSize = switch (size) { + .size1 => {{typePrefix}}Tokens.switchThumbSize1(), + .size2 => {{typePrefix}}Tokens.switchThumbSize2(), + .size3 => {{typePrefix}}Tokens.switchThumbSize3(), + }; + final radius = switch (size) { + .size1 => {{typePrefix}}Tokens.radius1OrThumb(), + .size2 || .size3 => {{typePrefix}}Tokens.radius2OrThumb(), + }; + return (width: width, height: height, thumbSize: thumbSize, radius: radius); +} + +SwitchStyler _{{valuePrefix}}SwitchDisabledStyler({ + bool classic = false, + bool soft = false, +}) { + final trackColor = switch ((classic, soft)) { + (true, _) => {{typePrefix}}Tokens.grayA5(), + (_, true) => {{typePrefix}}Tokens.grayA4(), + _ => {{typePrefix}}Tokens.grayA3(), + }; + return SwitchStyler() + .trackColor(trackColor) + .trackEffects( + RemixBoxEffectsMix.behindContent( + _{{valuePrefix}}SwitchLayer( + shadowToken: classic ? {{typePrefix}}Tokens.shadow1Layers : null, + ), + ), + ) + .trackEffects( + RemixBoxEffectsMix.overContent( + classic || soft + ? _{{valuePrefix}}SwitchLayer(shadows: const []) + : _{{valuePrefix}}SwitchInsetRing({{typePrefix}}Tokens.grayA3()), + ), + ) + .thumb( + BoxStyler().decoration( + .boxShadow([ + BoxShadowMix(color: {{typePrefix}}Tokens.grayA2(), spreadRadius: 1), + BoxShadowMix( + color: {{typePrefix}}Tokens.blackA1(), + offset: const Offset(0, 1), + blurRadius: 3, + ), + ]), + ), + ) + .thumbColor({{typePrefix}}Tokens.gray2()); +} + +BoxStyler _{{valuePrefix}}SwitchThumbStyler({ + required bool selected, + required bool highContrast, +}) => BoxStyler().decoration( + .boxShadow( + selected + ? [ + BoxShadowMix( + color: {{typePrefix}}Tokens.blackA2(), + offset: const Offset(0, 1), + blurRadius: 3, + ), + BoxShadowMix( + color: {{typePrefix}}Tokens.blackA1(), + offset: const Offset(0, 2), + blurRadius: 4, + spreadRadius: -1, + ), + BoxShadowMix( + color: highContrast + ? {{typePrefix}}Tokens.blackA2() + : {{typePrefix}}Tokens.accentA4(), + spreadRadius: 1, + ), + BoxShadowMix( + color: {{typePrefix}}Tokens.blackA2(), + offset: const Offset(-1, 0), + blurRadius: 1, + ), + ] + : [ + BoxShadowMix(color: {{typePrefix}}Tokens.blackA2(), spreadRadius: 1), + BoxShadowMix( + color: {{typePrefix}}Tokens.blackA1(), + offset: const Offset(0, 1), + blurRadius: 3, + ), + BoxShadowMix( + color: {{typePrefix}}Tokens.blackA1(), + offset: const Offset(0, 2), + blurRadius: 4, + spreadRadius: -1, + ), + ], + ), +); + +BoxStyler _{{valuePrefix}}SwitchSoftThumbStyler(bool selected) => BoxStyler().decoration( + .boxShadow([ + BoxShadowMix(color: {{typePrefix}}Tokens.blackA1(), spreadRadius: 1), + BoxShadowMix( + color: selected ? {{typePrefix}}Tokens.blackA2() : {{typePrefix}}Tokens.blackA1(), + offset: const Offset(0, 1), + blurRadius: 3, + ), + BoxShadowMix( + color: selected ? {{typePrefix}}Tokens.accentA3() : {{typePrefix}}Tokens.blackA1(), + offset: const Offset(0, 1), + blurRadius: 3, + ), + BoxShadowMix( + color: selected ? {{typePrefix}}Tokens.accentA3() : {{typePrefix}}Tokens.blackA1(), + offset: const Offset(0, 2), + blurRadius: 4, + spreadRadius: -1, + ), + ]), +); + +RemixBoxEffectLayerMix _{{valuePrefix}}SwitchInsetRing(Color color) => + _{{valuePrefix}}SwitchLayer( + shadows: [RemixBoxShadowMix(kind: .inset, color: color, spreadRadius: 1)], + ); + +RemixBoxEffectLayerMix _{{valuePrefix}}SwitchLayer({ + List? shadows, + RemixBoxShadowListToken? shadowToken, +}) => RemixBoxEffectLayerMix(shadows: shadows, shadowToken: shadowToken); diff --git a/packages/remix_cli/lib/src/registry/fortal/templates/tabs/tabs.dart.tmpl b/packages/remix_cli/lib/src/registry/fortal/templates/tabs/tabs.dart.tmpl new file mode 100644 index 000000000..2a6c996b1 --- /dev/null +++ b/packages/remix_cli/lib/src/registry/fortal/templates/tabs/tabs.dart.tmpl @@ -0,0 +1,127 @@ +import 'package:flutter/widgets.dart'; +import 'package:mix_annotations/mix_annotations.dart'; +import 'package:remix/remix.dart'; + +import '../theme/theme.dart'; + +part 'tabs.g.dart'; + +/// {{typePrefix}} tab-list size presets matching Radix Themes 3.3.0. +enum {{typePrefix}}TabsSize { size1, size2 } + +/// {{typePrefix}}-themed preset for [RemixTabBar]. +/// +/// The tab-list bottom border is a single hairline at every Radix size, so this +/// preset takes no `size` — unlike [{{valuePrefix}}TabStyle], whose per-tab metrics vary. +@MixWidget(target: RemixTabBar.new) +TabBarStyler {{valuePrefix}}TabBarStyle({ + TabBarStyler style = const TabBarStyler.create(), +}) { + return TabBarStyler() + .border( + .bottom( + .color({{typePrefix}}Tokens.grayA5()).width({{typePrefix}}Tokens.borderWidth1()), + ), + ) + .merge(style); +} + +/// {{typePrefix}}-themed preset for [RemixTabView]. +@MixWidget(target: RemixTabView.new) +TabViewStyler {{valuePrefix}}TabViewStyle({ + TabViewStyler style = const TabViewStyler.create(), +}) => TabViewStyler().merge(style); + +/// {{typePrefix}}-themed preset for [RemixTab]. +@MixWidget(target: RemixTab.new) +TabStyler {{valuePrefix}}TabStyle({ + {{typePrefix}}TabsSize size = {{typePrefix}}TabsSize.size2, + bool highContrast = false, + TabStyler style = const TabStyler.create(), +}) { + final metrics = switch (size) { + {{typePrefix}}TabsSize.size1 => ( + height: {{typePrefix}}Tokens.space6(), + outerPaddingX: {{typePrefix}}Tokens.space1(), + innerPaddingX: {{typePrefix}}Tokens.space1(), + innerPaddingY: {{typePrefix}}Tokens.tabInnerPaddingY1(), + radius: {{typePrefix}}Tokens.radius1(), + text: {{typePrefix}}Tokens.text1.mix(), + activeLetterSpacing: {{typePrefix}}Tokens.tabActiveLetterSpacing1(), + ), + {{typePrefix}}TabsSize.size2 => ( + height: {{typePrefix}}Tokens.space7(), + outerPaddingX: {{typePrefix}}Tokens.space2(), + innerPaddingX: {{typePrefix}}Tokens.space2(), + innerPaddingY: {{typePrefix}}Tokens.space1(), + radius: {{typePrefix}}Tokens.radius2(), + text: {{typePrefix}}Tokens.text2.mix(), + activeLetterSpacing: {{typePrefix}}Tokens.tabActiveLetterSpacing2(), + ), + }; + + return TabStyler() + .label( + .style(metrics.text).letterSpacing(0.0).color({{typePrefix}}Tokens.grayA11()), + ) + .icon(.color({{typePrefix}}Tokens.grayA11()).size({{typePrefix}}Tokens.space4())) + .wrap( + .box( + BoxStyler() + .height(metrics.height) + .padding(.horizontal(metrics.outerPaddingX)) + .alignment(.center) + .border( + .bottom( + .color( + const Color(0x00000000), + ).width({{typePrefix}}Tokens.borderWidth2()), + ), + ), + ), + ) + .container( + .direction(.horizontal) + .padding(.horizontal(metrics.innerPaddingX)) + .padding(.vertical(metrics.innerPaddingY)) + .borderRadius(.all(metrics.radius)) + .mainAxisAlignment(.center) + .spacing({{typePrefix}}Tokens.space2()), + ) + .onHovered( + .label(.color({{typePrefix}}Tokens.gray12())) + .icon(.color({{typePrefix}}Tokens.gray12())) + .color({{typePrefix}}Tokens.grayA3()) + .onFocusVisible(.color({{typePrefix}}Tokens.accentA3())), + ) + .onFocusVisible( + // Solid `focus-8` where the other three rings use alpha `focus-a8`. + // See {{valuePrefix}}FocusRing: unresolved whether that is intentional. + TabStyler().{{valuePrefix}}FocusRing( + color: {{typePrefix}}Tokens.focus8(), + strokeAlign: null, + ), + ) + .onSelected( + .label( + .color({{typePrefix}}Tokens.gray12()) + .fontWeight({{typePrefix}}Tokens.fontWeightMedium()) + .letterSpacing(metrics.activeLetterSpacing), + ) + .icon(.color({{typePrefix}}Tokens.gray12())) + .wrap( + .box( + BoxStyler().border( + .bottom( + .color( + highContrast + ? {{typePrefix}}Tokens.accent12() + : {{typePrefix}}Tokens.accentIndicator(), + ).width({{typePrefix}}Tokens.borderWidth2()), + ), + ), + ), + ), + ) + .merge(style); +} diff --git a/packages/remix_cli/lib/src/registry/fortal/templates/text/text.dart.tmpl b/packages/remix_cli/lib/src/registry/fortal/templates/text/text.dart.tmpl new file mode 100644 index 000000000..17b3b3186 --- /dev/null +++ b/packages/remix_cli/lib/src/registry/fortal/templates/text/text.dart.tmpl @@ -0,0 +1,48 @@ +import 'package:flutter/widgets.dart'; +import 'package:mix_annotations/mix_annotations.dart'; +import 'package:remix/remix.dart'; + +import '../theme/theme.dart'; +import 'typography.dart'; + +part 'text.g.dart'; + +/// {{typePrefix}}-themed body text on the Radix nine-step scale. +/// +/// Omitted [size] and [weight] resolve to the Radix root run (`text3`, +/// regular) from the active [{{typePrefix}}Scope]'s tokens rather than the ambient +/// `DefaultTextStyle`. This deliberately deviates from Radix's CSS `1em` +/// inheritance: a token default cannot be silently replaced by a host-installed +/// text run (a `Material` surface, or a host with no run at all), which keeps +/// {{typePrefix}} text a function of the theme alone. Set [accent] to take the +/// surrounding [{{typePrefix}}Scope]'s accent colour; leaving it false uses the +/// neutral `gray12` foreground. +@MixWidget() +TextStyler {{valuePrefix}}TextStyle({ + {{typePrefix}}TextSize? size, + {{typePrefix}}TextWeight? weight, + TextAlign? align, + bool softWrap = true, + bool truncate = false, + bool accent = false, + bool highContrast = false, + TextStyler style = const TextStyler.create(), +}) { + var recipe = TextStyler().style( + {{valuePrefix}}TextSizeToken(size ?? {{typePrefix}}TextSize.size3).mix(), + ); + recipe = recipe.fontWeight( + {{valuePrefix}}TextWeightToken(weight ?? {{typePrefix}}TextWeight.regular)(), + ); + recipe = accent + ? {{valuePrefix}}AccentForeground(recipe, highContrast: highContrast) + : recipe.color({{typePrefix}}Tokens.gray12()); + recipe = recipe.inherit(false); + + return {{valuePrefix}}ApplyTextFlow( + recipe, + align: align, + softWrap: softWrap, + truncate: truncate, + ).merge(style); +} diff --git a/packages/remix_cli/lib/src/registry/fortal/templates/textfield/textfield.dart.tmpl b/packages/remix_cli/lib/src/registry/fortal/templates/textfield/textfield.dart.tmpl new file mode 100644 index 000000000..25b9f525b --- /dev/null +++ b/packages/remix_cli/lib/src/registry/fortal/templates/textfield/textfield.dart.tmpl @@ -0,0 +1,388 @@ +// `DragStartBehavior` and `MaxLengthEnforcement` appear in the generated +// {{typePrefix}}TextField/{{typePrefix}}TextArea constructors, so they must be visible from +// this library even though nothing here references them directly. +import 'package:flutter/gestures.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter/services.dart'; +import 'package:mix_annotations/mix_annotations.dart'; +import 'package:remix/remix.dart'; + +import '../theme/theme.dart'; + +part 'textfield.g.dart'; + +/// {{typePrefix}} text field size presets. +enum {{typePrefix}}TextFieldSize { + /// Compact text field. + size1, + + /// Default text field. + size2, + + /// Large text field. + size3, +} + +/// {{typePrefix}} text field color variants. +enum {{typePrefix}}TextFieldVariant { + /// Raised treatment with Radix's level-one shadow. + classic, + + /// Surface treatment with neutral border and text colors. + surface, + + /// Soft accent treatment. + soft, +} + +Color _resolveNeutralTextInputPlaceholder(BuildContext context) { + final color = {{typePrefix}}Tokens.grayA10.resolve(context); + return color.withValues(alpha: color.a * 0.5); +} + +const _neutralTextInputPlaceholder = ContextToken( + _resolveNeutralTextInputPlaceholder, +); + +/// {{typePrefix}}-themed preset for [RemixTextField]. +@MixWidget(target: RemixTextField.new) +TextFieldStyler {{valuePrefix}}TextFieldStyle({ + {{typePrefix}}TextFieldVariant variant = .surface, + {{typePrefix}}TextFieldSize size = .size2, + TextFieldStyler style = const TextFieldStyler.create(), +}) { + final metrics = _{{valuePrefix}}TextFieldMetrics(size, bordered: variant != .soft); + final base = _{{valuePrefix}}TextInputBaseStyle( + container: BoxStyler() + .height(metrics.height) + .padding(.horizontal(metrics.paddingX)) + .borderRadius(.all(metrics.radius)) + .clipBehavior(.antiAlias), + spacing: metrics.spacing, + crossAxisAlignment: .center, + text: metrics.text, + focusColor: switch (variant) { + .soft => {{typePrefix}}Tokens.accent8(), + .classic || .surface => {{typePrefix}}Tokens.focus8(), + }, + ); + + final recipe = switch (variant) { + .classic => _{{valuePrefix}}ApplyClassicTextInput(base), + .surface => _{{valuePrefix}}ApplySurfaceTextInput(base), + .soft => _{{valuePrefix}}ApplySoftTextInput(base, placeholderOpacity: 0.60), + }; + + return recipe + .variant(ContextVariant.widgetState(.error), _{{valuePrefix}}TextInputErrorStyle()) + .merge(style); +} + +TextFieldStyler _{{valuePrefix}}TextInputBaseStyle({ + required BoxStyler container, + required double spacing, + required CrossAxisAlignment crossAxisAlignment, + required TextStyleToken text, + required Color focusColor, +}) => + TextFieldStyler( + container: container, + spacing: spacing, + crossAxisAlignment: crossAxisAlignment, + text: .style(text.mix()), + hintText: .style(text.mix()).textHeightBehavior( + TextHeightBehaviorMix() + .applyHeightToFirstAscent(false) + .applyHeightToLastDescent(true), + ), + helperText: .style({{typePrefix}}Tokens.text1.mix()), + label: .style({{typePrefix}}Tokens.text2.mix()), + cursorWidth: 1.5, + containerEffects: RemixBoxEffectsMix( + behindContent: RemixBoxEffectLayerMix(), + overContent: RemixBoxEffectLayerMix(), + ), + ) + .wrap(.iconTheme(color: {{typePrefix}}Tokens.gray11(), size: 16.0)) + // Radix keys text-input rings from :focus/:focus-within, so unlike + // control focus rings this intentionally follows raw focus. + .onFocused( + .containerEffects({{valuePrefix}}FocusOutline(focusColor, offset: -1)), + ); + +TextFieldStyler _{{valuePrefix}}ApplyClassicTextInput(TextFieldStyler base) => + _{{valuePrefix}}ApplyNeutralTextInput(base) + .color({{typePrefix}}Tokens.colorSurface()) + .containerEffects( + RemixBoxEffectsMix.behindContent( + RemixBoxEffectLayerMix(shadowToken: {{typePrefix}}Tokens.shadow1Layers), + ), + ) + .onDisabled( + _{{valuePrefix}}NeutralTextInputDisabledStyle() + .color({{typePrefix}}Tokens.colorSurface()) + .containerEffects( + RemixBoxEffectsMix.behindContent( + RemixBoxEffectLayerMix( + gradients: [ + RemixLinearGradientMix( + colors: [{{typePrefix}}Tokens.grayA2(), {{typePrefix}}Tokens.grayA2()], + ), + ], + shadowToken: {{typePrefix}}Tokens.shadow1Layers, + ), + ), + ), + ); + +TextFieldStyler _{{valuePrefix}}ApplySurfaceTextInput(TextFieldStyler base) => + _{{valuePrefix}}ApplyNeutralTextInput(base) + .color({{typePrefix}}Tokens.colorSurface()) + .containerEffects( + RemixBoxEffectsMix.behindContent(RemixBoxEffectLayerMix()), + ) + .containerEffects( + RemixBoxEffectsMix.overContent( + _{{valuePrefix}}TextInputInsetRing({{typePrefix}}Tokens.grayA7()), + ), + ) + .onDisabled( + _{{valuePrefix}}NeutralTextInputDisabledStyle() + .color({{typePrefix}}Tokens.colorSurface()) + .containerEffects( + RemixBoxEffectsMix.behindContent( + RemixBoxEffectLayerMix( + gradients: [ + RemixLinearGradientMix( + colors: [{{typePrefix}}Tokens.grayA2(), {{typePrefix}}Tokens.grayA2()], + ), + ], + ), + ), + ) + .containerEffects( + RemixBoxEffectsMix.overContent( + _{{valuePrefix}}TextInputInsetRing({{typePrefix}}Tokens.grayA6()), + ), + ), + ); + +TextFieldStyler _{{valuePrefix}}ApplySoftTextInput( + TextFieldStyler base, { + required double placeholderOpacity, +}) => base + .merge( + TextFieldStyler( + text: .fontWeight({{typePrefix}}Tokens.fontWeightRegular()), + hintText: .fontWeight({{typePrefix}}Tokens.fontWeightRegular()), + cursorColor: {{typePrefix}}Tokens.accent12(), + helperText: .color( + {{typePrefix}}Tokens.gray11(), + ).fontWeight({{typePrefix}}Tokens.fontWeightRegular()), + label: .color( + {{typePrefix}}Tokens.gray12(), + ).fontWeight({{typePrefix}}Tokens.fontWeightMedium()), + ), + ) + .textColor({{typePrefix}}Tokens.accent12()) + .text(.selectionColor({{typePrefix}}Tokens.accentA5())) + .onEnabled( + .hintText( + .color({{typePrefix}}Tokens.accent12().withValues(alpha: placeholderOpacity)), + ), + ) + .wrap(.iconTheme(color: {{typePrefix}}Tokens.accent10())) + .color({{typePrefix}}Tokens.accentA3()) + .containerEffects( + RemixBoxEffectsMix.behindContent(RemixBoxEffectLayerMix()), + ) + .onDisabled( + _{{valuePrefix}}SoftTextInputDisabledStyle() + .color({{typePrefix}}Tokens.grayA3()) + .containerEffects( + RemixBoxEffectsMix.behindContent(RemixBoxEffectLayerMix()), + ), + ); + +TextFieldStyler _{{valuePrefix}}ApplyNeutralTextInput(TextFieldStyler base) => + base.merge( + TextFieldStyler( + text: .color( + {{typePrefix}}Tokens.gray12(), + ).selectionColor({{typePrefix}}Tokens.focusA5()), + hintText: .color(_neutralTextInputPlaceholder()), + cursorColor: {{typePrefix}}Tokens.gray12(), + helperText: .color({{typePrefix}}Tokens.gray11()), + label: .color( + {{typePrefix}}Tokens.gray12(), + ).fontWeight({{typePrefix}}Tokens.fontWeightMedium()), + ), + ); + +// Keep the disabled-color branch on raw focus for the same :focus-within +// contract as the enabled text input. +TextFieldStyler _{{valuePrefix}}TextInputDisabledBaseStyle() => + TextFieldStyler( + text: .color( + {{typePrefix}}Tokens.grayA11(), + ).selectionColor({{typePrefix}}Tokens.grayA5()), + cursorColor: {{typePrefix}}Tokens.grayA11(), + ).onFocused( + .containerEffects({{valuePrefix}}FocusOutline({{typePrefix}}Tokens.gray8(), offset: -1)), + ); + +TextFieldStyler _{{valuePrefix}}NeutralTextInputDisabledStyle() => + _{{valuePrefix}}TextInputDisabledBaseStyle().hintText( + .color(_neutralTextInputPlaceholder()), + ); + +TextFieldStyler _{{valuePrefix}}SoftTextInputDisabledStyle() => + _{{valuePrefix}}TextInputDisabledBaseStyle().hintText( + .color({{typePrefix}}Tokens.accent12().withValues(alpha: 0.5)), + ); + +TextFieldStyler _{{valuePrefix}}TextInputErrorStyle() => TextFieldStyler( + helperText: .color({{typePrefix}}Tokens.error11()), + label: .color({{typePrefix}}Tokens.error11()), + cursorColor: {{typePrefix}}Tokens.error9(), + containerEffects: RemixBoxEffectsMix( + overContent: RemixBoxEffectLayerMix( + shadows: [ + RemixBoxShadowMix( + kind: .inset, + color: {{typePrefix}}Tokens.errorA7(), + spreadRadius: 1, + ), + ], + ), + outline: BorderSideMix( + color: {{typePrefix}}Tokens.error8(), + width: 2, + strokeAlign: BorderSide.strokeAlignInside, + ), + outlineOffset: -1, + ), +); + +({ + double height, + double paddingX, + double spacing, + Radius radius, + TextStyleToken text, +}) +_{{valuePrefix}}TextFieldMetrics({{typePrefix}}TextFieldSize size, {required bool bordered}) => + switch (size) { + .size1 => ( + height: {{typePrefix}}Tokens.space5(), + paddingX: bordered + ? {{typePrefix}}Tokens.textFieldPadding1() + : {{typePrefix}}Tokens.selectSpace1Half(), + spacing: {{typePrefix}}Tokens.space2(), + radius: {{typePrefix}}Tokens.radius2OrFull(), + text: {{typePrefix}}Tokens.text1, + ), + .size2 => ( + height: {{typePrefix}}Tokens.space6(), + paddingX: bordered + ? {{typePrefix}}Tokens.textFieldPadding2() + : {{typePrefix}}Tokens.space2(), + spacing: {{typePrefix}}Tokens.space2(), + radius: {{typePrefix}}Tokens.radius2OrFull(), + text: {{typePrefix}}Tokens.text2, + ), + .size3 => ( + height: {{typePrefix}}Tokens.space7(), + paddingX: bordered + ? {{typePrefix}}Tokens.textFieldPadding3() + : {{typePrefix}}Tokens.space3(), + spacing: {{typePrefix}}Tokens.space3(), + radius: {{typePrefix}}Tokens.radius3OrFull(), + text: {{typePrefix}}Tokens.text3, + ), + }; + +RemixBoxEffectLayerMix _{{valuePrefix}}TextInputInsetRing(Color color) => + RemixBoxEffectLayerMix( + shadows: [RemixBoxShadowMix(kind: .inset, color: color, spreadRadius: 1)], + ); + +/// Radix Themes TextArea size presets. +enum {{typePrefix}}TextAreaSize { size1, size2, size3 } + +/// Radix Themes TextArea variants. +enum {{typePrefix}}TextAreaVariant { classic, surface, soft } + +/// {{typePrefix}} recipe for [RemixTextArea]. +/// +/// Scrolling follows the host platform; this recipe does not reproduce Radix's +/// themed browser scrollbar or resize handle. +@MixWidget(target: RemixTextArea.new) +TextFieldStyler {{valuePrefix}}TextAreaStyle({ + {{typePrefix}}TextAreaVariant variant = .surface, + {{typePrefix}}TextAreaSize size = .size2, + TextFieldStyler style = const TextFieldStyler.create(), +}) { + final metrics = _{{valuePrefix}}TextAreaMetrics(size); + final base = _{{valuePrefix}}TextInputBaseStyle( + container: BoxStyler() + .minHeight(metrics.minHeight) + .padding( + .symmetric(horizontal: metrics.paddingX, vertical: metrics.paddingY), + ) + .borderRadius(.all(metrics.radius)) + .clipBehavior(.antiAlias), + spacing: metrics.spacing, + crossAxisAlignment: .start, + text: metrics.text, + focusColor: switch (variant) { + .soft => {{typePrefix}}Tokens.accent8(), + .classic || .surface => {{typePrefix}}Tokens.focus8(), + }, + ); + + final recipe = switch (variant) { + .classic => _{{valuePrefix}}ApplyClassicTextInput(base), + .surface => _{{valuePrefix}}ApplySurfaceTextInput(base), + .soft => _{{valuePrefix}}ApplySoftTextInput(base, placeholderOpacity: 0.65), + }; + + return recipe + .variant(ContextVariant.widgetState(.error), _{{valuePrefix}}TextInputErrorStyle()) + .merge(style); +} + +({ + double minHeight, + double paddingX, + double paddingY, + double spacing, + Radius radius, + TextStyleToken text, +}) +_{{valuePrefix}}TextAreaMetrics({{typePrefix}}TextAreaSize size) => switch (size) { + .size1 => ( + minHeight: {{typePrefix}}Tokens.space8(), + paddingX: {{typePrefix}}Tokens.selectSpace1Half(), + paddingY: {{typePrefix}}Tokens.space1(), + spacing: {{typePrefix}}Tokens.space2(), + radius: {{typePrefix}}Tokens.radius2(), + text: {{typePrefix}}Tokens.text1, + ), + .size2 => ( + minHeight: {{typePrefix}}Tokens.space9(), + paddingX: {{typePrefix}}Tokens.space2(), + paddingY: {{typePrefix}}Tokens.selectSpace1Half(), + spacing: {{typePrefix}}Tokens.space2(), + radius: {{typePrefix}}Tokens.radius2(), + text: {{typePrefix}}Tokens.text2, + ), + .size3 => ( + minHeight: {{typePrefix}}Tokens.textAreaMinHeight3(), + paddingX: {{typePrefix}}Tokens.space3(), + paddingY: {{typePrefix}}Tokens.space2(), + spacing: {{typePrefix}}Tokens.space3(), + radius: {{typePrefix}}Tokens.radius3(), + text: {{typePrefix}}Tokens.text3, + ), +}; diff --git a/packages/remix_cli/lib/src/registry/fortal/templates/theme/computed.dart.tmpl b/packages/remix_cli/lib/src/registry/fortal/templates/theme/computed.dart.tmpl new file mode 100644 index 000000000..5e2197e85 --- /dev/null +++ b/packages/remix_cli/lib/src/registry/fortal/templates/theme/computed.dart.tmpl @@ -0,0 +1,562 @@ +/// {{typePrefix}} computed tokens and functional color utilities. +/// +/// Implements computed role tokens (accent-contrast, accent-track, etc.) and +/// background/overlay colors that mirror Radix Themes behavior while keeping the +/// original Radix Colors data. +/// +/// Components should use these functional roles rather than raw color steps. + +// Documentation for all public APIs is provided below + +import 'dart:math' as math; +import 'dart:ui' show Color, Offset; + +import 'package:flutter/painting.dart' show BoxShadow, ColorSwatch; +import 'package:remix/remix.dart' + show MixToken, RemixBoxShadow, RemixBoxShadowKind; + +import 'radix_colors.dart'; +import 'theme_data.dart'; +import 'tokens.dart'; + +// ============================================================================ +// FUNCTIONAL / COMPUTED IMPLEMENTATIONS +// ============================================================================ + +// Computes contrast foreground color for solid accent backgrounds. +// + +/// Computes solid focus ring color (accent step 8). +Color computeFocus8(RadixColorScale accent) => accent.step(8); + +/// Computes translucent text selection color (accent alpha step 5). +Color computeFocusA5(RadixColorScale accent) => accent.alphaStep(5); + +/// Computes translucent focus ring color (accent alpha step 8). +Color computeFocusA8(RadixColorScale accent) => accent.alphaStep(8); + +// ============================================================================ +// BACKGROUND / PANEL / OVERLAY +// ============================================================================ + +/// Computes primary page background color. +/// +/// Radix Themes uses white in light mode and gray step 1 in dark mode. +Color computeColorBackground(RadixColorScale gray, {required bool isDark}) => + isDark ? gray.step(1) : const Color(0xFFFFFFFF); + +/// Computes solid background for panels and input surfaces. +/// +/// Radix Themes uses white in light mode and gray step 2 in dark mode. +Color computeColorPanelSolid(RadixColorScale gray, {required bool isDark}) => + isDark ? gray.step(2) : const Color(0xFFFFFFFF); + +/// Computes translucent background for floating panels. +/// +/// Radix Themes uses 70% white in light mode and gray alpha step 2 in dark +/// mode. +Color computeColorPanelTranslucent( + RadixColorScale gray, { + required bool isDark, +}) => isDark ? gray.alphaStep(2) : const Color(0xB3FFFFFF); + +/// Computes the neutral control surface color. +/// +/// Radix Themes uses 85% white in light mode and 25% black in dark mode. +Color computeColorSurface({required bool isDark}) => + isDark ? const Color(0x40000000) : const Color(0xD9FFFFFF); + +/// Computes modal backdrop overlay color. +/// +/// Uses black alpha step 6 (light mode) or step 8 (dark mode). +Color computeColorOverlay({required bool isDark}) => + isDark ? blackAlpha[8]! : blackAlpha[6]!; + +/// Computes the mode-aware stroke used by elevation shadows. +Color computeShadowStroke(RadixColorScale gray, {required bool isDark}) => + mixOklabPremultiplied( + gray.alphaStep(isDark ? 6 : 3), + gray.step(isDark ? 6 : 3), + 0.25, + ); + +/// Mixes two sRGB colors in OKLab after premultiplying their channels by +/// alpha, matching CSS `color-mix(in oklab, ...)` for translucent colors. +Color mixOklabPremultiplied(Color first, Color second, double amount) { + if (!amount.isFinite || amount < 0 || amount > 1) { + throw ArgumentError.value( + amount, + 'amount', + 'Expected a value from 0 to 1.', + ); + } + final firstWeight = 1 - amount; + final alpha = first.a * firstWeight + second.a * amount; + if (alpha == 0) return const Color(0x00000000); + + final firstLab = _srgbToOklab(first); + final secondLab = _srgbToOklab(second); + final mixedLab = ( + lightness: + (firstLab.lightness * first.a * firstWeight + + secondLab.lightness * second.a * amount) / + alpha, + a: + (firstLab.a * first.a * firstWeight + secondLab.a * second.a * amount) / + alpha, + b: + (firstLab.b * first.a * firstWeight + secondLab.b * second.a * amount) / + alpha, + ); + final rgb = _oklabToSrgb(mixedLab); + + return Color.from( + alpha: alpha, + red: rgb.red.clamp(0, 1), + green: rgb.green.clamp(0, 1), + blue: rgb.blue.clamp(0, 1), + ); +} + +({double lightness, double a, double b}) _srgbToOklab(Color color) { + final red = _linearizeSrgb(color.r); + final green = _linearizeSrgb(color.g); + final blue = _linearizeSrgb(color.b); + final l = math + .pow( + 0.4122214708 * red + 0.5363325363 * green + 0.0514459929 * blue, + 1 / 3, + ) + .toDouble(); + final m = math + .pow( + 0.2119034982 * red + 0.6806995451 * green + 0.1073969566 * blue, + 1 / 3, + ) + .toDouble(); + final s = math + .pow( + 0.0883024619 * red + 0.2817188376 * green + 0.6299787005 * blue, + 1 / 3, + ) + .toDouble(); + + return ( + lightness: 0.2104542553 * l + 0.7936177850 * m - 0.0040720468 * s, + a: 1.9779984951 * l - 2.4285922050 * m + 0.4505937099 * s, + b: 0.0259040371 * l + 0.7827717662 * m - 0.8086757660 * s, + ); +} + +({double red, double green, double blue}) _oklabToSrgb( + ({double lightness, double a, double b}) color, +) { + final l = math + .pow(color.lightness + 0.3963377774 * color.a + 0.2158037573 * color.b, 3) + .toDouble(); + final m = math + .pow(color.lightness - 0.1055613458 * color.a - 0.0638541728 * color.b, 3) + .toDouble(); + final s = math + .pow(color.lightness - 0.0894841775 * color.a - 1.2914855480 * color.b, 3) + .toDouble(); + + return ( + red: _encodeSrgb(4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s), + green: _encodeSrgb(-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s), + blue: _encodeSrgb(-0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s), + ); +} + +double _linearizeSrgb(double channel) => channel <= 0.04045 + ? channel / 12.92 + : math.pow((channel + 0.055) / 1.055, 2.4).toDouble(); + +double _encodeSrgb(double channel) => channel <= 0.0031308 + ? 12.92 * channel + : 1.055 * math.pow(channel, 1 / 2.4).toDouble() - 0.055; + +/// Builds Radix Themes elevation shadows for the active brightness. +Map, Object> build{{typePrefix}}Shadows({ + required bool isDark, + required {{typePrefix}}ThemeColors colors, +}) { + if (isDark) { + final shadows = >{ + 'shadow1': [ + _shadow( + colors.gray.scale.alphaStep(3), + kind: .inset, + offset: const Offset(0, -1), + blur: 1, + ), + _shadow(colors.gray.scale.alphaStep(3), kind: .inset, spread: 1), + _shadow( + colors.blackAlpha[5]!, + kind: .inset, + offset: const Offset(0, 3), + blur: 4, + ), + _shadow(colors.gray.scale.alphaStep(4), kind: .inset, spread: 1), + ], + 'shadow2': [ + _shadow(colors.shadowStroke, spread: 1), + _shadow(colors.blackAlpha[3]!, blur: 0.5), + _shadow(colors.blackAlpha[6]!, offset: const Offset(0, 1), blur: 1), + _shadow( + colors.blackAlpha[6]!, + offset: const Offset(0, 2), + blur: 1, + spread: -1, + ), + _shadow(colors.blackAlpha[5]!, offset: const Offset(0, 1), blur: 3), + ], + 'shadow3': [ + _shadow(colors.shadowStroke, spread: 1), + _shadow( + colors.blackAlpha[3]!, + offset: const Offset(0, 2), + blur: 3, + spread: -2, + ), + _shadow( + colors.blackAlpha[6]!, + offset: const Offset(0, 3), + blur: 8, + spread: -2, + ), + _shadow( + colors.blackAlpha[7]!, + offset: const Offset(0, 4), + blur: 12, + spread: -4, + ), + ], + 'shadow4': [ + _shadow(colors.shadowStroke, spread: 1), + _shadow(colors.blackAlpha[3]!, offset: const Offset(0, 8), blur: 40), + _shadow( + colors.blackAlpha[5]!, + offset: const Offset(0, 12), + blur: 32, + spread: -16, + ), + ], + 'shadow5': [ + _shadow(colors.shadowStroke, spread: 1), + _shadow(colors.blackAlpha[5]!, offset: const Offset(0, 12), blur: 60), + _shadow( + colors.blackAlpha[7]!, + offset: const Offset(0, 12), + blur: 32, + spread: -16, + ), + ], + 'shadow6': [ + _shadow(colors.shadowStroke, spread: 1), + _shadow(colors.blackAlpha[4]!, offset: const Offset(0, 12), blur: 60), + _shadow(colors.blackAlpha[6]!, offset: const Offset(0, 16), blur: 64), + _shadow( + colors.blackAlpha[11]!, + offset: const Offset(0, 16), + blur: 36, + spread: -20, + ), + ], + }; + return _{{valuePrefix}}ShadowTokens(shadows); + } + + final shadows = >{ + 'shadow1': [ + _shadow(colors.gray.scale.alphaStep(5), kind: .inset, spread: 1), + _shadow( + colors.gray.scale.alphaStep(2), + kind: .inset, + offset: const Offset(0, 1.5), + blur: 2, + ), + _shadow( + colors.blackAlpha[2]!, + kind: .inset, + offset: const Offset(0, 1.5), + blur: 2, + ), + ], + 'shadow2': [ + _shadow(colors.shadowStroke, spread: 1), + _shadow(colors.blackAlpha[1]!, blur: 0.5), + _shadow( + colors.gray.scale.alphaStep(2), + offset: const Offset(0, 1), + blur: 1, + ), + _shadow( + colors.blackAlpha[1]!, + offset: const Offset(0, 2), + blur: 1, + spread: -1, + ), + _shadow(colors.blackAlpha[1]!, offset: const Offset(0, 1), blur: 3), + ], + 'shadow3': [ + _shadow(colors.shadowStroke, spread: 1), + _shadow( + colors.gray.scale.alphaStep(3), + offset: const Offset(0, 2), + blur: 3, + spread: -2, + ), + _shadow( + colors.blackAlpha[2]!, + offset: const Offset(0, 3), + blur: 12, + spread: -4, + ), + _shadow( + colors.blackAlpha[2]!, + offset: const Offset(0, 4), + blur: 16, + spread: -8, + ), + ], + 'shadow4': [ + _shadow(colors.shadowStroke, spread: 1), + _shadow(colors.blackAlpha[1]!, offset: const Offset(0, 8), blur: 40), + _shadow( + colors.gray.scale.alphaStep(3), + offset: const Offset(0, 12), + blur: 32, + spread: -16, + ), + ], + 'shadow5': [ + _shadow(colors.shadowStroke, spread: 1), + _shadow(colors.blackAlpha[3]!, offset: const Offset(0, 12), blur: 60), + _shadow( + colors.gray.scale.alphaStep(5), + offset: const Offset(0, 12), + blur: 32, + spread: -16, + ), + ], + 'shadow6': [ + _shadow(colors.shadowStroke, spread: 1), + _shadow(colors.blackAlpha[3]!, offset: const Offset(0, 12), blur: 60), + _shadow( + colors.gray.scale.alphaStep(2), + offset: const Offset(0, 16), + blur: 64, + ), + _shadow( + colors.gray.scale.alphaStep(7), + offset: const Offset(0, 16), + blur: 36, + spread: -20, + ), + ], + }; + return _{{valuePrefix}}ShadowTokens(shadows); +} + +Map, Object> _{{valuePrefix}}ShadowTokens( + Map> shadows, +) { + final shadow1 = shadows['shadow1']!; + final shadow2 = shadows['shadow2']!; + return { + {{typePrefix}}Tokens.shadow1: _ordinaryShadows(shadow1), + {{typePrefix}}Tokens.shadow1Layers: shadow1, + {{typePrefix}}Tokens.shadow2: _ordinaryShadows(shadow2), + {{typePrefix}}Tokens.segmentedControlClassicIndicatorShadows: [ + for (final shadow in shadow2) + RemixBoxShadow( + kind: shadow.kind, + color: shadow.color, + offset: shadow.offset, + blurRadius: shadow.blurRadius, + spreadRadius: shadow.spreadRadius, + shapeInset: 1, + ), + ], + {{typePrefix}}Tokens.shadow3: _ordinaryShadows(shadows['shadow3']!), + {{typePrefix}}Tokens.shadow4: _ordinaryShadows(shadows['shadow4']!), + {{typePrefix}}Tokens.shadow5: _ordinaryShadows(shadows['shadow5']!), + {{typePrefix}}Tokens.shadow6: _ordinaryShadows(shadows['shadow6']!), + }; +} + +List _ordinaryShadows(List shadows) => [ + for (final shadow in shadows) + BoxShadow( + color: shadow.color, + offset: shadow.offset, + blurRadius: shadow.blurRadius, + spreadRadius: shadow.spreadRadius, + ), +]; + +RemixBoxShadow _shadow( + Color color, { + RemixBoxShadowKind kind = RemixBoxShadowKind.outer, + Offset offset = Offset.zero, + double blur = 0, + double spread = 0, +}) => RemixBoxShadow( + kind: kind, + color: color, + offset: offset, + blurRadius: blur, + spreadRadius: spread, +); + +// ============================================================================ +// RESOLVER (merged from resolver.dart) +// ============================================================================ + +/// Container for all computed {{typePrefix}} theme colors and scales. +/// +/// Holds resolved color system for a specific theme configuration. +/// Created by [resolve{{typePrefix}}Tokens] for internal use by the token system. +class {{typePrefix}}ThemeColors { + final RadixColor accent; + final RadixColor gray; + final ColorSwatch blackAlpha; + final ColorSwatch whiteAlpha; + + // Functional colors + final Color colorBackground; + final Color colorSurface; + final Color colorPanelSolid; + final Color colorPanelTranslucent; + final Color colorOverlay; + final Color shadowStroke; + + // Focus + final Color focus8; + final Color focusA5; + final Color focusA8; + + const {{typePrefix}}ThemeColors({ + required this.accent, + required this.gray, + required this.blackAlpha, + required this.whiteAlpha, + required this.colorBackground, + required this.colorSurface, + required this.colorPanelSolid, + required this.colorPanelTranslucent, + required this.colorOverlay, + required this.shadowStroke, + required this.focus8, + required this.focusA5, + required this.focusA8, + }); +} + +// Map by enum .name to generated RadixColorTheme instances (light/dark contained). +const Map _accentThemesByName = { + 'gray': gray, + 'mauve': mauve, + 'slate': slate, + 'sage': sage, + 'olive': olive, + 'sand': sand, + 'amber': amber, + 'blue': blue, + 'bronze': bronze, + 'brown': brown, + 'crimson': crimson, + 'cyan': cyan, + 'gold': gold, + 'grass': grass, + 'green': green, + 'indigo': indigo, + 'iris': iris, + 'jade': jade, + 'lime': lime, + 'mint': mint, + 'orange': orange, + 'pink': pink, + 'plum': plum, + 'purple': purple, + 'red': red, + 'ruby': ruby, + 'sky': sky, + 'teal': teal, + 'tomato': tomato, + 'violet': violet, + 'yellow': yellow, +}; + +const Map _grayThemesByName = { + 'gray': gray, + 'mauve': mauve, + 'slate': slate, + 'sage': sage, + 'olive': olive, + 'sand': sand, +}; + +/// Resolves all computed tokens for a theme configuration. +{{typePrefix}}ThemeColors resolve{{typePrefix}}Tokens({{typePrefix}}ThemeConfig theme) { + // Pick light/dark RadixColor for accent and neutral using enum .name keys + final accentColor = theme.accent ?? {{typePrefix}}AccentColor.indigo; + final grayColor = theme.gray ?? {{typePrefix}}GrayColor.slate; + final String accentName = accentColor.name; + final String grayName = grayColor.name; + final RadixColorTheme grayTheme = _grayThemesByName[grayName]!; + final RadixColorTheme accentTheme = accentColor == .gray + ? grayTheme + : _accentThemesByName[accentName]!; + final RadixColor accentRC = theme.isDark + ? accentTheme.dark + : accentTheme.light; + final RadixColor grayRC = theme.isDark ? grayTheme.dark : grayTheme.light; + + // Extract scales + final RadixColorScale accent = accentRC.scale; + final RadixColorScale gray = grayRC.scale; + + // Neutral alpha swatches + const ColorSwatch blackA = blackAlpha; + const ColorSwatch whiteA = whiteAlpha; + + // Backgrounds/panels/overlay + final Color colorBackground = computeColorBackground( + gray, + isDark: theme.isDark, + ); + final Color colorPanelSolid = computeColorPanelSolid( + gray, + isDark: theme.isDark, + ); + final Color colorPanelTranslucent = computeColorPanelTranslucent( + gray, + isDark: theme.isDark, + ); + final Color colorSurface = computeColorSurface(isDark: theme.isDark); + final Color colorOverlay = computeColorOverlay(isDark: theme.isDark); + final Color shadowStroke = computeShadowStroke(gray, isDark: theme.isDark); + + // Focus + final Color focus8 = computeFocus8(accent); + final Color focusA5 = computeFocusA5(accent); + final Color focusA8 = computeFocusA8(accent); + + return {{typePrefix}}ThemeColors( + accent: accentRC, + gray: grayRC, + blackAlpha: blackA, + whiteAlpha: whiteA, + colorBackground: colorBackground, + colorSurface: colorSurface, + colorPanelSolid: colorPanelSolid, + colorPanelTranslucent: colorPanelTranslucent, + colorOverlay: colorOverlay, + shadowStroke: shadowStroke, + focus8: focus8, + focusA5: focusA5, + focusA8: focusA8, + ); +} diff --git a/packages/remix_cli/lib/src/registry/fortal/templates/theme/control_styles.dart.tmpl b/packages/remix_cli/lib/src/registry/fortal/templates/theme/control_styles.dart.tmpl new file mode 100644 index 000000000..09d90625a --- /dev/null +++ b/packages/remix_cli/lib/src/registry/fortal/templates/theme/control_styles.dart.tmpl @@ -0,0 +1,77 @@ +import 'package:flutter/widgets.dart'; +import 'package:remix/remix.dart'; + +import 'tokens.dart'; + +/// A CSS outline that does not affect layout. +RemixBoxEffectsMix {{valuePrefix}}FocusOutline(Color color, {required double offset}) => + RemixBoxEffectsMix( + outline: BorderSideMix( + color: color, + width: 2, + strokeAlign: BorderSide.strokeAlignInside, + ), + outlineOffset: offset, + ); + +/// The focus ring for stylers that can only express a border. +/// +/// [{{valuePrefix}}FocusOutline] is the preferred form, but it returns a +/// [RemixBoxEffectsMix] and several stylers — `ToggleStyler`, +/// `ToggleGroupItemStyler`, `TabStyler` — expose no `containerEffects` slot to +/// put one in. Those recipes each re-derived the same border, so the width +/// token lives here instead of in four places. +extension {{typePrefix}}FocusRing> on RemixBoxStylerAnchors { + /// Applies the ring to this styler. + /// + /// [color] defaults to `focus-a8`. Tabs is the one caller that differs: it + /// passes the solid `focus-8`. Whether that is intentional is unresolved — + /// the pinned Chromium probes capture computed styles only, not + /// `:focus-visible`, so it cannot be settled from the reference fixtures. + /// Preserved as-is rather than unified on a guess. + /// + /// Tabs also leaves [strokeAlign] unset, but that is not a second + /// difference: `BorderSide` itself defaults to `strokeAlignInside`, so unset + /// and explicit resolve to the same -1.0. The parameter is nullable only so + /// tabs can keep expressing it as absent. + T {{valuePrefix}}FocusRing({ + Color? color, + double? strokeAlign = BorderSide.strokeAlignInside, + }) => border( + .all( + BorderSideMix( + color: color ?? _focusRingColor(), + width: _focusRingWidth(), + strokeAlign: strokeAlign, + ), + ), + ); +} + +/// The same ring for a Mix [FlexBoxStyler], which sits outside Remix's +/// `RemixBoxStylerAnchors` interface — accordion rings its trigger, not itself. +/// Separate extension rather than a differently-named function so both read as +/// `.{{valuePrefix}}FocusRing()`; the receiver type picks the right one. +extension {{typePrefix}}FocusRingFlexBox on FlexBoxStyler { + FlexBoxStyler {{valuePrefix}}FocusRing() => border( + .color( + _focusRingColor(), + ).width(_focusRingWidth()).strokeAlign(BorderSide.strokeAlignInside), + ); +} + +Color _focusRingColor() => {{typePrefix}}Tokens.focusA8(); +double _focusRingWidth() => {{typePrefix}}Tokens.focusRingWidth(); + +/// A one-pixel inset stroke, optionally layered over a fill. +RemixBoxEffectLayerMix {{valuePrefix}}InsetSurface({required List strokes}) => + RemixBoxEffectLayerMix( + shadows: [ + for (final stroke in strokes) + RemixBoxShadowMix( + kind: RemixBoxShadowKind.inset, + color: stroke, + spreadRadius: 1, + ), + ], + ); diff --git a/packages/remix_cli/lib/src/registry/fortal/templates/theme/radix_colors.dart.tmpl b/packages/remix_cli/lib/src/registry/fortal/templates/theme/radix_colors.dart.tmpl new file mode 100644 index 000000000..2c85bdd4e --- /dev/null +++ b/packages/remix_cli/lib/src/registry/fortal/templates/theme/radix_colors.dart.tmpl @@ -0,0 +1,2490 @@ +// GENERATED CODE - DO NOT EDIT +// Generated from: sRGB fallback tokens extracted from the pinned @radix-ui/themes npm artifact +// Radix Themes version: 3.3.0 +// Radix Colors version: bundled with Radix Themes 3.3.0 +// Source integrity: sha512-I0/h2CRNTpYNB7Mi3xFIvSsQq5a108d7kK8dTO5zp5b9HR5QJXKag6B8tjpz2ITkVYkFdkGk45doNkSr7OxwNw== + +library radix_colors; + +import 'package:flutter/painting.dart'; + +class RadixColor { + final RadixColorScale scale; + final Color surface; + final Color indicator; + final Color track; + final Color contrast; + + const RadixColor( + this.scale, + this.surface, + this.indicator, + this.track, + this.contrast, + ); +} + +class RadixColorTheme { + final RadixColor light; + final RadixColor dark; + + const RadixColorTheme(this.light, this.dark); +} + +class RadixColorScale { + final ColorSwatch solid; + final ColorSwatch alpha; + + const RadixColorScale(this.solid, this.alpha); + + // Semantic color accessors for better code readability + + /// The most subtle background color (step 1). + Color get appBackground => step(1); + + /// Subtle background with slightly more presence (step 2). + Color get subtleBackground => step(2); + + /// Default background for interactive components (step 3). + Color get componentBackground => step(3); + + /// Background color for components on hover (step 4). + Color get componentBackgroundHover => step(4); + + /// Background color for active/pressed components (step 5). + Color get componentBackgroundActive => step(5); + + /// Subtle border color for gentle separation (step 6). + Color get subtleBorder => step(6); + + /// Standard border color for components (step 7). + Color get componentBorder => step(7); + + /// Border color for hover and focus states (step 8). + Color get componentBorderHover => step(8); + + /// Primary solid background color (step 9). + Color get solidBackground => step(9); + + /// Solid background color on hover (step 10). + Color get solidBackgroundHover => step(10); + + /// Low contrast text color (step 11). + Color get lowContrastText => step(11); + + /// High contrast text color (step 12). + Color get highContrastText => step(12); + + /// Gets a solid color from the 12-step scale. + /// + /// Steps must be between 1 and 12. Falls back to step 9 if unavailable. + Color step(int n) { + assert(n >= 1 && n <= 12, 'Step must be between 1 and 12'); + + return solid[n] ?? solid[9]!; + } + + /// Gets a translucent color from the 12-step alpha scale. + /// + /// Alpha variants maintain saturation when composited. + /// Falls back to alpha step 9 if unavailable. + Color alphaStep(int n) { + assert(n >= 1 && n <= 12, 'Step must be between 1 and 12'); + + return alpha[n] ?? alpha[9]!; + } +} + +// gray color scale +const _grayLight = RadixColor( + RadixColorScale( + ColorSwatch(0xff8d8d8d, { + 1: Color(0xfffcfcfc), + 2: Color(0xfff9f9f9), + 3: Color(0xfff0f0f0), + 4: Color(0xffe8e8e8), + 5: Color(0xffe0e0e0), + 6: Color(0xffd9d9d9), + 7: Color(0xffcecece), + 8: Color(0xffbbbbbb), + 9: Color(0xff8d8d8d), + 10: Color(0xff838383), + 11: Color(0xff646464), + 12: Color(0xff202020), + }), + ColorSwatch(0x72000000, { + 1: Color(0x03000000), + 2: Color(0x06000000), + 3: Color(0x0f000000), + 4: Color(0x17000000), + 5: Color(0x1f000000), + 6: Color(0x26000000), + 7: Color(0x31000000), + 8: Color(0x44000000), + 9: Color(0x72000000), + 10: Color(0x7c000000), + 11: Color(0x9b000000), + 12: Color(0xdf000000), + }), + ), + Color(0xccffffff), + Color(0xff8d8d8d), + Color(0xff8d8d8d), + Color(0xffffffff), +); + +const _grayDark = RadixColor( + RadixColorScale( + ColorSwatch(0xff6e6e6e, { + 1: Color(0xff111111), + 2: Color(0xff191919), + 3: Color(0xff222222), + 4: Color(0xff2a2a2a), + 5: Color(0xff313131), + 6: Color(0xff3a3a3a), + 7: Color(0xff484848), + 8: Color(0xff606060), + 9: Color(0xff6e6e6e), + 10: Color(0xff7b7b7b), + 11: Color(0xffb4b4b4), + 12: Color(0xffeeeeee), + }), + ColorSwatch(0x64ffffff, { + 1: Color(0x00000000), + 2: Color(0x09ffffff), + 3: Color(0x12ffffff), + 4: Color(0x1bffffff), + 5: Color(0x22ffffff), + 6: Color(0x2cffffff), + 7: Color(0x3bffffff), + 8: Color(0x55ffffff), + 9: Color(0x64ffffff), + 10: Color(0x72ffffff), + 11: Color(0xafffffff), + 12: Color(0xedffffff), + }), + ), + Color(0x80212121), + Color(0xff6e6e6e), + Color(0xff6e6e6e), + Color(0xffffffff), +); + +// mauve color scale +const _mauveLight = RadixColor( + RadixColorScale( + ColorSwatch(0xff8e8c99, { + 1: Color(0xfffdfcfd), + 2: Color(0xfffaf9fb), + 3: Color(0xfff2eff3), + 4: Color(0xffeae7ec), + 5: Color(0xffe3dfe6), + 6: Color(0xffdbd8e0), + 7: Color(0xffd0cdd7), + 8: Color(0xffbcbac7), + 9: Color(0xff8e8c99), + 10: Color(0xff84828e), + 11: Color(0xff65636d), + 12: Color(0xff211f26), + }), + ColorSwatch(0x7305001d, { + 1: Color(0x03550055), + 2: Color(0x062b0055), + 3: Color(0x10300040), + 4: Color(0x18200036), + 5: Color(0x20200038), + 6: Color(0x27140035), + 7: Color(0x32100033), + 8: Color(0x45080031), + 9: Color(0x7305001d), + 10: Color(0x7d050019), + 11: Color(0x9c040011), + 12: Color(0xe0020008), + }), + ), + Color(0xccffffff), + Color(0xff8e8c99), + Color(0xff8e8c99), + Color(0xffffffff), +); + +const _mauveDark = RadixColor( + RadixColorScale( + ColorSwatch(0xff6f6d78, { + 1: Color(0xff121113), + 2: Color(0xff1a191b), + 3: Color(0xff232225), + 4: Color(0xff2b292d), + 5: Color(0xff323035), + 6: Color(0xff3c393f), + 7: Color(0xff49474e), + 8: Color(0xff625f69), + 9: Color(0xff6f6d78), + 10: Color(0xff7c7a85), + 11: Color(0xffb5b2bc), + 12: Color(0xffeeeef0), + }), + ColorSwatch(0x6eeae6fd, { + 1: Color(0x00000000), + 2: Color(0x09f5f4f6), + 3: Color(0x14ebeaf8), + 4: Color(0x1deee5f8), + 5: Color(0x25efe6fe), + 6: Color(0x30f1e6fd), + 7: Color(0x40eee9ff), + 8: Color(0x5deee7ff), + 9: Color(0x6eeae6fd), + 10: Color(0x7cece9fd), + 11: Color(0xb7f5f1ff), + 12: Color(0xeffdfdff), + }), + ), + Color(0x80222123), + Color(0xff6f6d78), + Color(0xff6f6d78), + Color(0xffffffff), +); + +// slate color scale +const _slateLight = RadixColor( + RadixColorScale( + ColorSwatch(0xff8b8d98, { + 1: Color(0xfffcfcfd), + 2: Color(0xfff9f9fb), + 3: Color(0xfff0f0f3), + 4: Color(0xffe8e8ec), + 5: Color(0xffe0e1e6), + 6: Color(0xffd9d9e0), + 7: Color(0xffcdced6), + 8: Color(0xffb9bbc6), + 9: Color(0xff8b8d98), + 10: Color(0xff80838d), + 11: Color(0xff60646c), + 12: Color(0xff1c2024), + }), + ColorSwatch(0x7400051d, { + 1: Color(0x03000055), + 2: Color(0x06000055), + 3: Color(0x0f000033), + 4: Color(0x1700002d), + 5: Color(0x1f000932), + 6: Color(0x2600002f), + 7: Color(0x3200062e), + 8: Color(0x46000830), + 9: Color(0x7400051d), + 10: Color(0x7f00071b), + 11: Color(0x9f000714), + 12: Color(0xe3000509), + }), + ), + Color(0xccffffff), + Color(0xff8b8d98), + Color(0xff8b8d98), + Color(0xffffffff), +); + +const _slateDark = RadixColor( + RadixColorScale( + ColorSwatch(0xff696e77, { + 1: Color(0xff111113), + 2: Color(0xff18191b), + 3: Color(0xff212225), + 4: Color(0xff272a2d), + 5: Color(0xff2e3135), + 6: Color(0xff363a3f), + 7: Color(0xff43484e), + 8: Color(0xff5a6169), + 9: Color(0xff696e77), + 10: Color(0xff777b84), + 11: Color(0xffb0b4ba), + 12: Color(0xffedeef0), + }), + ColorSwatch(0x6ddfebfd, { + 1: Color(0x00000000), + 2: Color(0x09d8f4f6), + 3: Color(0x14ddeaf8), + 4: Color(0x1dd3edf8), + 5: Color(0x25d9edfe), + 6: Color(0x30d6ebfd), + 7: Color(0x40d9edff), + 8: Color(0x5dd9edff), + 9: Color(0x6ddfebfd), + 10: Color(0x7be5edfd), + 11: Color(0xb5f1f7fe), + 12: Color(0xeffcfdff), + }), + ), + Color(0x801f2123), + Color(0xff696e77), + Color(0xff696e77), + Color(0xffffffff), +); + +// sage color scale +const _sageLight = RadixColor( + RadixColorScale( + ColorSwatch(0xff868e8b, { + 1: Color(0xfffbfdfc), + 2: Color(0xfff7f9f8), + 3: Color(0xffeef1f0), + 4: Color(0xffe6e9e8), + 5: Color(0xffdfe2e0), + 6: Color(0xffd7dad9), + 7: Color(0xffcbcfcd), + 8: Color(0xffb8bcba), + 9: Color(0xff868e8b), + 10: Color(0xff7c8481), + 11: Color(0xff5f6563), + 12: Color(0xff1a211e), + }), + ColorSwatch(0x7900110b, { + 1: Color(0x04008040), + 2: Color(0x08004020), + 3: Color(0x11002d1e), + 4: Color(0x19001f15), + 5: Color(0x20001808), + 6: Color(0x2800140d), + 7: Color(0x3400140a), + 8: Color(0x47000f08), + 9: Color(0x7900110b), + 10: Color(0x8300100a), + 11: Color(0xa0000a07), + 12: Color(0xe5000805), + }), + ), + Color(0xccffffff), + Color(0xff868e8b), + Color(0xff868e8b), + Color(0xffffffff), +); + +const _sageDark = RadixColor( + RadixColorScale( + ColorSwatch(0xff63706b, { + 1: Color(0xff101211), + 2: Color(0xff171918), + 3: Color(0xff202221), + 4: Color(0xff272a29), + 5: Color(0xff2e3130), + 6: Color(0xff373b39), + 7: Color(0xff444947), + 8: Color(0xff5b625f), + 9: Color(0xff63706b), + 10: Color(0xff717d79), + 11: Color(0xffadb5b2), + 12: Color(0xffeceeed), + }), + ColorSwatch(0x66dffdf2, { + 1: Color(0x00000000), + 2: Color(0x08f0f2f1), + 3: Color(0x12f3f5f4), + 4: Color(0x1af2fefd), + 5: Color(0x22f1fbfa), + 6: Color(0x2dedfbf4), + 7: Color(0x3cedfcf7), + 8: Color(0x57ebfdf6), + 9: Color(0x66dffdf2), + 10: Color(0x74e5fdf6), + 11: Color(0xb0f4fefb), + 12: Color(0xedfdfffe), + }), + ), + Color(0x801e201f), + Color(0xff63706b), + Color(0xff63706b), + Color(0xffffffff), +); + +// olive color scale +const _oliveLight = RadixColor( + RadixColorScale( + ColorSwatch(0xff898e87, { + 1: Color(0xfffcfdfc), + 2: Color(0xfff8faf8), + 3: Color(0xffeff1ef), + 4: Color(0xffe7e9e7), + 5: Color(0xffdfe2df), + 6: Color(0xffd7dad7), + 7: Color(0xffcccfcc), + 8: Color(0xffb9bcb8), + 9: Color(0xff898e87), + 10: Color(0xff7f847d), + 11: Color(0xff60655f), + 12: Color(0xff1d211c), + }), + ColorSwatch(0x78050f00, { + 1: Color(0x03005500), + 2: Color(0x07004900), + 3: Color(0x10002000), + 4: Color(0x18001600), + 5: Color(0x20001800), + 6: Color(0x28001400), + 7: Color(0x33000f00), + 8: Color(0x47040f00), + 9: Color(0x78050f00), + 10: Color(0x82040e00), + 11: Color(0xa0020a00), + 12: Color(0xe3010600), + }), + ), + Color(0xccffffff), + Color(0xff898e87), + Color(0xff898e87), + Color(0xffffffff), +); + +const _oliveDark = RadixColor( + RadixColorScale( + ColorSwatch(0xff687066, { + 1: Color(0xff111210), + 2: Color(0xff181917), + 3: Color(0xff212220), + 4: Color(0xff282a27), + 5: Color(0xff2f312e), + 6: Color(0xff383a36), + 7: Color(0xff454843), + 8: Color(0xff5c625b), + 9: Color(0xff687066), + 10: Color(0xff767d74), + 11: Color(0xffafb5ad), + 12: Color(0xffeceeec), + }), + ColorSwatch(0x66ebfde7, { + 1: Color(0x00000000), + 2: Color(0x08f1f2f0), + 3: Color(0x12f4f5f3), + 4: Color(0x1af3fef2), + 5: Color(0x22f2fbf1), + 6: Color(0x2cf4faed), + 7: Color(0x3bf2fced), + 8: Color(0x57edfdeb), + 9: Color(0x66ebfde7), + 10: Color(0x74f0fdec), + 11: Color(0xb0f6fef4), + 12: Color(0xedfdfffd), + }), + ), + Color(0x801f201e), + Color(0xff687066), + Color(0xff687066), + Color(0xffffffff), +); + +// sand color scale +const _sandLight = RadixColor( + RadixColorScale( + ColorSwatch(0xff8d8d86, { + 1: Color(0xfffdfdfc), + 2: Color(0xfff9f9f8), + 3: Color(0xfff1f0ef), + 4: Color(0xffe9e8e6), + 5: Color(0xffe2e1de), + 6: Color(0xffdad9d6), + 7: Color(0xffcfceca), + 8: Color(0xffbcbbb5), + 9: Color(0xff8d8d86), + 10: Color(0xff82827c), + 11: Color(0xff63635e), + 12: Color(0xff21201c), + }), + ColorSwatch(0x790f0f00, { + 1: Color(0x03555500), + 2: Color(0x07252500), + 3: Color(0x10201000), + 4: Color(0x191f1500), + 5: Color(0x211f1800), + 6: Color(0x29191300), + 7: Color(0x35191400), + 8: Color(0x4a191501), + 9: Color(0x790f0f00), + 10: Color(0x830c0c00), + 11: Color(0xa1080800), + 12: Color(0xe3060500), + }), + ), + Color(0xccffffff), + Color(0xff8d8d86), + Color(0xff8d8d86), + Color(0xffffffff), +); + +const _sandDark = RadixColor( + RadixColorScale( + ColorSwatch(0xff6f6d66, { + 1: Color(0xff111110), + 2: Color(0xff191918), + 3: Color(0xff222221), + 4: Color(0xff2a2a28), + 5: Color(0xff31312e), + 6: Color(0xff3b3a37), + 7: Color(0xff494844), + 8: Color(0xff62605b), + 9: Color(0xff6f6d66), + 10: Color(0xff7c7b74), + 11: Color(0xffb5b3ad), + 12: Color(0xffeeeeec), + }), + ColorSwatch(0x65fffae9, { + 1: Color(0x00000000), + 2: Color(0x09f4f4f3), + 3: Color(0x13f6f6f5), + 4: Color(0x1bfefef3), + 5: Color(0x23fbfbeb), + 6: Color(0x2dfffaed), + 7: Color(0x3cfffbed), + 8: Color(0x57fff9eb), + 9: Color(0x65fffae9), + 10: Color(0x73fffdee), + 11: Color(0xb0fffcf4), + 12: Color(0xedfffffd), + }), + ), + Color(0x80212120), + Color(0xff6f6d66), + Color(0xff6f6d66), + Color(0xffffffff), +); + +// tomato color scale +const _tomatoLight = RadixColor( + RadixColorScale( + ColorSwatch(0xffe54d2e, { + 1: Color(0xfffffcfc), + 2: Color(0xfffff8f7), + 3: Color(0xfffeebe7), + 4: Color(0xffffdcd3), + 5: Color(0xffffcdc2), + 6: Color(0xfffdbdaf), + 7: Color(0xfff5a898), + 8: Color(0xffec8e7b), + 9: Color(0xffe54d2e), + 10: Color(0xffdd4425), + 11: Color(0xffd13415), + 12: Color(0xff5c271f), + }), + ColorSwatch(0xd1df2600, { + 1: Color(0x03ff0000), + 2: Color(0x08ff2000), + 3: Color(0x18f52b00), + 4: Color(0x2cff3500), + 5: Color(0x3dff2e00), + 6: Color(0x50f92d00), + 7: Color(0x67e72800), + 8: Color(0x84db2500), + 9: Color(0xd1df2600), + 10: Color(0xdad72400), + 11: Color(0xeacd2200), + 12: Color(0xe0460900), + }), + ), + Color(0xccfff6f5), + Color(0xffe54d2e), + Color(0xffe54d2e), + Color(0xffffffff), +); + +const _tomatoDark = RadixColor( + RadixColorScale( + ColorSwatch(0xffe54d2e, { + 1: Color(0xff181111), + 2: Color(0xff1f1513), + 3: Color(0xff391714), + 4: Color(0xff4e1511), + 5: Color(0xff5e1c16), + 6: Color(0xff6e2920), + 7: Color(0xff853a2d), + 8: Color(0xffac4d39), + 9: Color(0xffe54d2e), + 10: Color(0xffec6142), + 11: Color(0xffff977d), + 12: Color(0xfffbd3cb), + }), + ColorSwatch(0xe4fe5431, { + 1: Color(0x08f11212), + 2: Color(0x0fff5533), + 3: Color(0x2bff3523), + 4: Color(0x42fd2011), + 5: Color(0x53fe3321), + 6: Color(0x64ff4f38), + 7: Color(0x7dfd644a), + 8: Color(0xa7fe6d4e), + 9: Color(0xe4fe5431), + 10: Color(0xebff6847), + 11: Color(0xffff977d), + 12: Color(0xfbffd6ce), + }), + ), + Color(0x802d1915), + Color(0xffe54d2e), + Color(0xffe54d2e), + Color(0xffffffff), +); + +// red color scale +const _redLight = RadixColor( + RadixColorScale( + ColorSwatch(0xffe5484d, { + 1: Color(0xfffffcfc), + 2: Color(0xfffff7f7), + 3: Color(0xfffeebec), + 4: Color(0xffffdbdc), + 5: Color(0xffffcdce), + 6: Color(0xfffdbdbe), + 7: Color(0xfff4a9aa), + 8: Color(0xffeb8e90), + 9: Color(0xffe5484d), + 10: Color(0xffdc3e42), + 11: Color(0xffce2c31), + 12: Color(0xff641723), + }), + ColorSwatch(0xb7db0007, { + 1: Color(0x03ff0000), + 2: Color(0x08ff0000), + 3: Color(0x14f3000d), + 4: Color(0x24ff0008), + 5: Color(0x32ff0006), + 6: Color(0x42f80004), + 7: Color(0x56df0003), + 8: Color(0x71d20005), + 9: Color(0xb7db0007), + 10: Color(0xc1d10005), + 11: Color(0xd3c40006), + 12: Color(0xe855000d), + }), + ), + Color(0xccfff5f5), + Color(0xffe5484d), + Color(0xffe5484d), + Color(0xffffffff), +); + +const _redDark = RadixColor( + RadixColorScale( + ColorSwatch(0xffe5484d, { + 1: Color(0xff191111), + 2: Color(0xff201314), + 3: Color(0xff3b1219), + 4: Color(0xff500f1c), + 5: Color(0xff611623), + 6: Color(0xff72232d), + 7: Color(0xff8c333a), + 8: Color(0xffb54548), + 9: Color(0xffe5484d), + 10: Color(0xffec5d5e), + 11: Color(0xffff9592), + 12: Color(0xffffd1d9), + }), + ColorSwatch(0xe4fe4e54, { + 1: Color(0x09f41212), + 2: Color(0x11f22f3e), + 3: Color(0x2dff173f), + 4: Color(0x44fe0a3b), + 5: Color(0x56ff2047), + 6: Color(0x68ff3e56), + 7: Color(0x84ff5361), + 8: Color(0xb0ff5d61), + 9: Color(0xe4fe4e54), + 10: Color(0xebff6465), + 11: Color(0xffff9592), + 12: Color(0xffffd1d9), + }), + ), + Color(0x802f1517), + Color(0xffe5484d), + Color(0xffe5484d), + Color(0xffffffff), +); + +// ruby color scale +const _rubyLight = RadixColor( + RadixColorScale( + ColorSwatch(0xffe54666, { + 1: Color(0xfffffcfd), + 2: Color(0xfffff7f8), + 3: Color(0xfffeeaed), + 4: Color(0xffffdce1), + 5: Color(0xffffced6), + 6: Color(0xfff8bfc8), + 7: Color(0xffefacb8), + 8: Color(0xffe592a3), + 9: Color(0xffe54666), + 10: Color(0xffdc3b5d), + 11: Color(0xffca244d), + 12: Color(0xff64172b), + }), + ColorSwatch(0xb9db002c, { + 1: Color(0x03ff0055), + 2: Color(0x08ff0020), + 3: Color(0x15f30025), + 4: Color(0x23ff0025), + 5: Color(0x31ff002a), + 6: Color(0x40e40024), + 7: Color(0x53ce0025), + 8: Color(0x6dc30028), + 9: Color(0xb9db002c), + 10: Color(0xc4d2002c), + 11: Color(0xdbc10030), + 12: Color(0xe8550016), + }), + ), + Color(0xccfff5f6), + Color(0xffe54666), + Color(0xffe54666), + Color(0xffffffff), +); + +const _rubyDark = RadixColor( + RadixColorScale( + ColorSwatch(0xffe54666, { + 1: Color(0xff191113), + 2: Color(0xff1e1517), + 3: Color(0xff3a141e), + 4: Color(0xff4e1325), + 5: Color(0xff5e1a2e), + 6: Color(0xff6f2539), + 7: Color(0xff883447), + 8: Color(0xffb3445a), + 9: Color(0xffe54666), + 10: Color(0xffec5a72), + 11: Color(0xffff949d), + 12: Color(0xfffed2e1), + }), + ColorSwatch(0xe4fe4c70, { + 1: Color(0x09f4124a), + 2: Color(0x0efe5a7f), + 3: Color(0x2cff235d), + 4: Color(0x42fd195e), + 5: Color(0x53fe2d6b), + 6: Color(0x65ff4476), + 7: Color(0x80ff577d), + 8: Color(0xaeff5c7c), + 9: Color(0xe4fe4c70), + 10: Color(0xebff617b), + 11: Color(0xffff949d), + 12: Color(0xfeffd3e2), + }), + ), + Color(0x802b191d), + Color(0xffe54666), + Color(0xffe54666), + Color(0xffffffff), +); + +// crimson color scale +const _crimsonLight = RadixColor( + RadixColorScale( + ColorSwatch(0xffe93d82, { + 1: Color(0xfffffcfd), + 2: Color(0xfffef7f9), + 3: Color(0xffffe9f0), + 4: Color(0xfffedce7), + 5: Color(0xfffacedd), + 6: Color(0xfff3bed1), + 7: Color(0xffeaacc3), + 8: Color(0xffe093b2), + 9: Color(0xffe93d82), + 10: Color(0xffdf3478), + 11: Color(0xffcb1d63), + 12: Color(0xff621639), + }), + ColorSwatch(0xc2e2005b, { + 1: Color(0x03ff0055), + 2: Color(0x08e00040), + 3: Color(0x16ff0052), + 4: Color(0x23f80051), + 5: Color(0x31e5004f), + 6: Color(0x41d0004b), + 7: Color(0x53bf0047), + 8: Color(0x6cb6004a), + 9: Color(0xc2e2005b), + 10: Color(0xcbd70056), + 11: Color(0xe2c4004f), + 12: Color(0xe9530026), + }), + ), + Color(0xccfef5f8), + Color(0xffe93d82), + Color(0xffe93d82), + Color(0xffffffff), +); + +const _crimsonDark = RadixColor( + RadixColorScale( + ColorSwatch(0xffe93d82, { + 1: Color(0xff191114), + 2: Color(0xff201318), + 3: Color(0xff381525), + 4: Color(0xff4d122f), + 5: Color(0xff5c1839), + 6: Color(0xff6d2545), + 7: Color(0xff873356), + 8: Color(0xffb0436e), + 9: Color(0xffe93d82), + 10: Color(0xffee518a), + 11: Color(0xffff92ad), + 12: Color(0xfffdd3e8), + }), + ColorSwatch(0xe8fe418d, { + 1: Color(0x09f41267), + 2: Color(0x11f22f7a), + 3: Color(0x2afe2a8b), + 4: Color(0x41fd1587), + 5: Color(0x51fd278f), + 6: Color(0x63fe4597), + 7: Color(0x7ffd559b), + 8: Color(0xabfe5b9b), + 9: Color(0xe8fe418d), + 10: Color(0xedff5693), + 11: Color(0xffff92ad), + 12: Color(0xfdffd5ea), + }), + ), + Color(0x802f151f), + Color(0xffe93d82), + Color(0xffe93d82), + Color(0xffffffff), +); + +// pink color scale +const _pinkLight = RadixColor( + RadixColorScale( + ColorSwatch(0xffd6409f, { + 1: Color(0xfffffcfe), + 2: Color(0xfffef7fb), + 3: Color(0xfffee9f5), + 4: Color(0xfffbdcef), + 5: Color(0xfff6cee7), + 6: Color(0xffefbfdd), + 7: Color(0xffe7acd0), + 8: Color(0xffdd93c2), + 9: Color(0xffd6409f), + 10: Color(0xffcf3897), + 11: Color(0xffc2298a), + 12: Color(0xff651249), + }), + ColorSwatch(0xbfc8007f, { + 1: Color(0x03ff00aa), + 2: Color(0x08e00080), + 3: Color(0x16f4008c), + 4: Color(0x23e2008b), + 5: Color(0x31d10083), + 6: Color(0x40c00078), + 7: Color(0x53b6006f), + 8: Color(0x6caf006f), + 9: Color(0xbfc8007f), + 10: Color(0xc7c2007a), + 11: Color(0xd6b60074), + 12: Color(0xed59003b), + }), + ), + Color(0xccfef5fa), + Color(0xffd6409f), + Color(0xffd6409f), + Color(0xffffffff), +); + +const _pinkDark = RadixColor( + RadixColorScale( + ColorSwatch(0xffd6409f, { + 1: Color(0xff191117), + 2: Color(0xff21121d), + 3: Color(0xff37172f), + 4: Color(0xff4b143d), + 5: Color(0xff591c47), + 6: Color(0xff692955), + 7: Color(0xff833869), + 8: Color(0xffa84885), + 9: Color(0xffd6409f), + 10: Color(0xffde51a8), + 11: Color(0xffff8dcc), + 12: Color(0xfffdd1ea), + }), + ColorSwatch(0xd4fe49bc, { + 1: Color(0x09f412bc), + 2: Color(0x12f420bb), + 3: Color(0x29fe37cc), + 4: Color(0x3ffc1ec4), + 5: Color(0x4efd35c2), + 6: Color(0x5ffd51c7), + 7: Color(0x7bfd62c8), + 8: Color(0xa2ff68c8), + 9: Color(0xd4fe49bc), + 10: Color(0xdcff5cc0), + 11: Color(0xffff8dcc), + 12: Color(0xfdffd3ec), + }), + ), + Color(0x80311329), + Color(0xffd6409f), + Color(0xffd6409f), + Color(0xffffffff), +); + +// plum color scale +const _plumLight = RadixColor( + RadixColorScale( + ColorSwatch(0xffab4aba, { + 1: Color(0xfffefcff), + 2: Color(0xfffdf7fd), + 3: Color(0xfffbebfb), + 4: Color(0xfff7def8), + 5: Color(0xfff2d1f3), + 6: Color(0xffe9c2ec), + 7: Color(0xffdeade3), + 8: Color(0xffcf91d8), + 9: Color(0xffab4aba), + 10: Color(0xffa144af), + 11: Color(0xff953ea3), + 12: Color(0xff53195d), + }), + ColorSwatch(0xb589009e, { + 1: Color(0x03aa00ff), + 2: Color(0x08c000c0), + 3: Color(0x14cc00cc), + 4: Color(0x21c200c9), + 5: Color(0x2eb700bd), + 6: Color(0x3da400b0), + 7: Color(0x529900a8), + 8: Color(0x6e9000a5), + 9: Color(0xb589009e), + 10: Color(0xbb7f0092), + 11: Color(0xc1730086), + 12: Color(0xe640004b), + }), + ), + Color(0xccfdf5fd), + Color(0xffab4aba), + Color(0xffab4aba), + Color(0xffffffff), +); + +const _plumDark = RadixColor( + RadixColorScale( + ColorSwatch(0xffab4aba, { + 1: Color(0xff181118), + 2: Color(0xff201320), + 3: Color(0xff351a35), + 4: Color(0xff451d47), + 5: Color(0xff512454), + 6: Color(0xff5e3061), + 7: Color(0xff734079), + 8: Color(0xff92549c), + 9: Color(0xffab4aba), + 10: Color(0xffb658c4), + 11: Color(0xffe796f3), + 12: Color(0xfff4d4f4), + }), + ColorSwatch(0xb6e961fe, { + 1: Color(0x08f112f1), + 2: Color(0x11f22ff2), + 3: Color(0x27fd4cfd), + 4: Color(0x3af646ff), + 5: Color(0x48f455ff), + 6: Color(0x56f66dff), + 7: Color(0x70f07cfd), + 8: Color(0x95ee84ff), + 9: Color(0xb6e961fe), + 10: Color(0xc0ed70ff), + 11: Color(0xf3f19cfe), + 12: Color(0xf4feddfe), + }), + ), + Color(0x802f152f), + Color(0xffab4aba), + Color(0xffab4aba), + Color(0xffffffff), +); + +// purple color scale +const _purpleLight = RadixColor( + RadixColorScale( + ColorSwatch(0xff8e4ec6, { + 1: Color(0xfffefcfe), + 2: Color(0xfffbf7fe), + 3: Color(0xfff7edfe), + 4: Color(0xfff2e2fc), + 5: Color(0xffead5f9), + 6: Color(0xffe0c4f4), + 7: Color(0xffd1afec), + 8: Color(0xffbe93e4), + 9: Color(0xff8e4ec6), + 10: Color(0xff8347b9), + 11: Color(0xff8145b5), + 12: Color(0xff402060), + }), + ColorSwatch(0xb15c00ad, { + 1: Color(0x03aa00aa), + 2: Color(0x088000e0), + 3: Color(0x128e00f1), + 4: Color(0x1d8d00e5), + 5: Color(0x2a8000db), + 6: Color(0x3b7a01d0), + 7: Color(0x506d00c3), + 8: Color(0x6c6600c0), + 9: Color(0xb15c00ad), + 10: Color(0xb853009e), + 11: Color(0xba52009a), + 12: Color(0xdf250049), + }), + ), + Color(0xccfaf5fe), + Color(0xff8e4ec6), + Color(0xff8e4ec6), + Color(0xffffffff), +); + +const _purpleDark = RadixColor( + RadixColorScale( + ColorSwatch(0xff8e4ec6, { + 1: Color(0xff18111b), + 2: Color(0xff1e1523), + 3: Color(0xff301c3b), + 4: Color(0xff3d224e), + 5: Color(0xff48295c), + 6: Color(0xff54346b), + 7: Color(0xff664282), + 8: Color(0xff8457aa), + 9: Color(0xff8e4ec6), + 10: Color(0xff9a5cd0), + 11: Color(0xffd19dff), + 12: Color(0xffecd9fa), + }), + ColorSwatch(0xc2b661ff, { + 1: Color(0x0bb412f9), + 2: Color(0x14b744f7), + 3: Color(0x2dc150ff), + 4: Color(0x42bb53fd), + 5: Color(0x51be5cfd), + 6: Color(0x61c16dfd), + 7: Color(0x7ac378fd), + 8: Color(0xa4c47eff), + 9: Color(0xc2b661ff), + 10: Color(0xcdbc6fff), + 11: Color(0xffd19dff), + 12: Color(0xfaf1ddff), + }), + ), + Color(0x802b1735), + Color(0xff8e4ec6), + Color(0xff8e4ec6), + Color(0xffffffff), +); + +// violet color scale +const _violetLight = RadixColor( + RadixColorScale( + ColorSwatch(0xff6e56cf, { + 1: Color(0xfffdfcfe), + 2: Color(0xfffaf8ff), + 3: Color(0xfff4f0fe), + 4: Color(0xffebe4ff), + 5: Color(0xffe1d9ff), + 6: Color(0xffd4cafe), + 7: Color(0xffc2b5f5), + 8: Color(0xffaa99ec), + 9: Color(0xff6e56cf), + 10: Color(0xff654dc4), + 11: Color(0xff6550b9), + 12: Color(0xff2f265f), + }), + ColorSwatch(0xa92400b7, { + 1: Color(0x035500aa), + 2: Color(0x074900ff), + 3: Color(0x0f4400ee), + 4: Color(0x1b4300ff), + 5: Color(0x263600ff), + 6: Color(0x353100fb), + 7: Color(0x4a2d01dd), + 8: Color(0x662b00d0), + 9: Color(0xa92400b7), + 10: Color(0xb22300ab), + 11: Color(0xaf1f0099), + 12: Color(0xd90b0043), + }), + ), + Color(0xccf9f6ff), + Color(0xff6e56cf), + Color(0xff6e56cf), + Color(0xffffffff), +); + +const _violetDark = RadixColor( + RadixColorScale( + ColorSwatch(0xff6e56cf, { + 1: Color(0xff14121f), + 2: Color(0xff1b1525), + 3: Color(0xff291f43), + 4: Color(0xff33255b), + 5: Color(0xff3c2e69), + 6: Color(0xff473876), + 7: Color(0xff56468b), + 8: Color(0xff6958ad), + 9: Color(0xff6e56cf), + 10: Color(0xff7d66d9), + 11: Color(0xffbaa7ff), + 12: Color(0xffe2ddfe), + }), + ColorSwatch(0xcc8668ff, { + 1: Color(0x0f4422ff), + 2: Color(0x16853ff9), + 3: Color(0x368354fe), + 4: Color(0x507d51fd), + 5: Color(0x5f845ffd), + 6: Color(0x6d8f6cfd), + 7: Color(0x839879ff), + 8: Color(0xa8977dfe), + 9: Color(0xcc8668ff), + 10: Color(0xd79176fe), + 11: Color(0xffbaa7ff), + 12: Color(0xfee3deff), + }), + ), + Color(0x80251939), + Color(0xff6e56cf), + Color(0xff6e56cf), + Color(0xffffffff), +); + +// iris color scale +const _irisLight = RadixColor( + RadixColorScale( + ColorSwatch(0xff5b5bd6, { + 1: Color(0xfffdfdff), + 2: Color(0xfff8f8ff), + 3: Color(0xfff0f1fe), + 4: Color(0xffe6e7ff), + 5: Color(0xffdadcff), + 6: Color(0xffcbcdff), + 7: Color(0xffb8baf8), + 8: Color(0xff9b9ef0), + 9: Color(0xff5b5bd6), + 10: Color(0xff5151cd), + 11: Color(0xff5753c6), + 12: Color(0xff272962), + }), + ColorSwatch(0xa40000c0, { + 1: Color(0x020000ff), + 2: Color(0x070000ff), + 3: Color(0x0f0011ee), + 4: Color(0x19000bff), + 5: Color(0x25000eff), + 6: Color(0x34000aff), + 7: Color(0x470008e6), + 8: Color(0x640008d9), + 9: Color(0xa40000c0), + 10: Color(0xae0000b6), + 11: Color(0xac0600ab), + 12: Color(0xd8000246), + }), + ), + Color(0xccf6f6ff), + Color(0xff5b5bd6), + Color(0xff5b5bd6), + Color(0xffffffff), +); + +const _irisDark = RadixColor( + RadixColorScale( + ColorSwatch(0xff5b5bd6, { + 1: Color(0xff13131e), + 2: Color(0xff171625), + 3: Color(0xff202248), + 4: Color(0xff262a65), + 5: Color(0xff303374), + 6: Color(0xff3d3e82), + 7: Color(0xff4a4a95), + 8: Color(0xff5958b1), + 9: Color(0xff5b5bd6), + 10: Color(0xff6e6ade), + 11: Color(0xffb1a9ff), + 12: Color(0xffe0dffe), + }), + ColorSwatch(0xd46a6afe, { + 1: Color(0x0e3636fe), + 2: Color(0x16564bf9), + 3: Color(0x3b525bff), + 4: Color(0x5a4d58ff), + 5: Color(0x6b5b62fd), + 6: Color(0x7a6d6ffd), + 7: Color(0x8e7777fe), + 8: Color(0xac7b7afe), + 9: Color(0xd46a6afe), + 10: Color(0xdc7d79ff), + 11: Color(0xffb1a9ff), + 12: Color(0xfee1e0ff), + }), + ), + Color(0x801d1b39), + Color(0xff5b5bd6), + Color(0xff5b5bd6), + Color(0xffffffff), +); + +// indigo color scale +const _indigoLight = RadixColor( + RadixColorScale( + ColorSwatch(0xff3e63dd, { + 1: Color(0xfffdfdfe), + 2: Color(0xfff7f9ff), + 3: Color(0xffedf2fe), + 4: Color(0xffe1e9ff), + 5: Color(0xffd2deff), + 6: Color(0xffc1d0ff), + 7: Color(0xffabbdf9), + 8: Color(0xff8da4ef), + 9: Color(0xff3e63dd), + 10: Color(0xff3358d4), + 11: Color(0xff3a5bc7), + 12: Color(0xff1f2d5c), + }), + ColorSwatch(0xc10031d2, { + 1: Color(0x02000080), + 2: Color(0x080040ff), + 3: Color(0x120047f1), + 4: Color(0x1e0044ff), + 5: Color(0x2d0044ff), + 6: Color(0x3e003eff), + 7: Color(0x540037ed), + 8: Color(0x720034dc), + 9: Color(0xc10031d2), + 10: Color(0xcc002ec9), + 11: Color(0xc5002bb7), + 12: Color(0xe0001046), + }), + ), + Color(0xccf5f8ff), + Color(0xff3e63dd), + Color(0xff3e63dd), + Color(0xffffffff), +); + +const _indigoDark = RadixColor( + RadixColorScale( + ColorSwatch(0xff3e63dd, { + 1: Color(0xff11131f), + 2: Color(0xff141726), + 3: Color(0xff182449), + 4: Color(0xff1d2e62), + 5: Color(0xff253974), + 6: Color(0xff304384), + 7: Color(0xff3a4f97), + 8: Color(0xff435db1), + 9: Color(0xff3e63dd), + 10: Color(0xff5472e4), + 11: Color(0xff9eb1ff), + 12: Color(0xffd6e1ff), + }), + ColorSwatch(0xdb4671ff, { + 1: Color(0x0f1133ff), + 2: Color(0x173354fa), + 3: Color(0x3c2f62ff), + 4: Color(0x573566ff), + 5: Color(0x6b4171fd), + 6: Color(0x7c5178fd), + 7: Color(0x905a7fff), + 8: Color(0xac5b81fe), + 9: Color(0xdb4671ff), + 10: Color(0xe35c7efe), + 11: Color(0xff9eb1ff), + 12: Color(0xffd6e1ff), + }), + ), + Color(0x80171d3b), + Color(0xff3e63dd), + Color(0xff3e63dd), + Color(0xffffffff), +); + +// blue color scale +const _blueLight = RadixColor( + RadixColorScale( + ColorSwatch(0xff0090ff, { + 1: Color(0xfffbfdff), + 2: Color(0xfff4faff), + 3: Color(0xffe6f4fe), + 4: Color(0xffd5efff), + 5: Color(0xffc2e5ff), + 6: Color(0xffacd8fc), + 7: Color(0xff8ec8f6), + 8: Color(0xff5eb1ef), + 9: Color(0xff0090ff), + 10: Color(0xff0588f0), + 11: Color(0xff0d74ce), + 12: Color(0xff113264), + }), + ColorSwatch(0xff0090ff, { + 1: Color(0x040080ff), + 2: Color(0x0b008cff), + 3: Color(0x19008ff5), + 4: Color(0x2a009eff), + 5: Color(0x3d0093ff), + 6: Color(0x530088f6), + 7: Color(0x710083eb), + 8: Color(0xa10084e6), + 9: Color(0xff0090ff), + 10: Color(0xfa0086f0), + 11: Color(0xf2006dcb), + 12: Color(0xee002359), + }), + ), + Color(0xccf1f9ff), + Color(0xff0090ff), + Color(0xff0090ff), + Color(0xffffffff), +); + +const _blueDark = RadixColor( + RadixColorScale( + ColorSwatch(0xff0090ff, { + 1: Color(0xff0d1520), + 2: Color(0xff111927), + 3: Color(0xff0d2847), + 4: Color(0xff003362), + 5: Color(0xff004074), + 6: Color(0xff104d87), + 7: Color(0xff205d9e), + 8: Color(0xff2870bd), + 9: Color(0xff0090ff), + 10: Color(0xff3b9eff), + 11: Color(0xff70b8ff), + 12: Color(0xffc2e6ff), + }), + ColorSwatch(0xff0090ff, { + 1: Color(0x11004df2), + 2: Color(0x181166fb), + 3: Color(0x3a0077ff), + 4: Color(0x570075ff), + 5: Color(0x6b0081fd), + 6: Color(0x7f0f89fd), + 7: Color(0x982a91fe), + 8: Color(0xb93094fe), + 9: Color(0xff0090ff), + 10: Color(0xff3b9eff), + 11: Color(0xff70b8ff), + 12: Color(0xffc2e6ff), + }), + ), + Color(0x8011213d), + Color(0xff0090ff), + Color(0xff0090ff), + Color(0xffffffff), +); + +// cyan color scale +const _cyanLight = RadixColor( + RadixColorScale( + ColorSwatch(0xff00a2c7, { + 1: Color(0xfffafdfe), + 2: Color(0xfff2fafb), + 3: Color(0xffdef7f9), + 4: Color(0xffcaf1f6), + 5: Color(0xffb5e9f0), + 6: Color(0xff9ddde7), + 7: Color(0xff7dcedc), + 8: Color(0xff3db9cf), + 9: Color(0xff00a2c7), + 10: Color(0xff0797b9), + 11: Color(0xff107d98), + 12: Color(0xff0d3c48), + }), + ColorSwatch(0xff00a2c7, { + 1: Color(0x050099cc), + 2: Color(0x0d009db1), + 3: Color(0x2100c2d1), + 4: Color(0x3500bcd4), + 5: Color(0x4a01b4cc), + 6: Color(0x6200a7c1), + 7: Color(0x82009fbb), + 8: Color(0xc200a3c0), + 9: Color(0xff00a2c7), + 10: Color(0xf80094b7), + 11: Color(0xef007491), + 12: Color(0xf200323e), + }), + ), + Color(0xcceff9fa), + Color(0xff00a2c7), + Color(0xff00a2c7), + Color(0xffffffff), +); + +const _cyanDark = RadixColor( + RadixColorScale( + ColorSwatch(0xff00a2c7, { + 1: Color(0xff0b161a), + 2: Color(0xff101b20), + 3: Color(0xff082c36), + 4: Color(0xff003848), + 5: Color(0xff004558), + 6: Color(0xff045468), + 7: Color(0xff12677e), + 8: Color(0xff11809c), + 9: Color(0xff00a2c7), + 10: Color(0xff23afd0), + 11: Color(0xff4ccce6), + 12: Color(0xffb6ecf7), + }), + ColorSwatch(0xc300cfff, { + 1: Color(0x0a0091f7), + 2: Color(0x1102a7f2), + 3: Color(0x2800befd), + 4: Color(0x3b00baff), + 5: Color(0x4d00befd), + 6: Color(0x5e00c7fd), + 7: Color(0x7514cdff), + 8: Color(0x9511cfff), + 9: Color(0xc300cfff), + 10: Color(0xcd28d6ff), + 11: Color(0xe552e1fe), + 12: Color(0xf7bbf3fe), + }), + ), + Color(0x8011252d), + Color(0xff00a2c7), + Color(0xff00a2c7), + Color(0xffffffff), +); + +// teal color scale +const _tealLight = RadixColor( + RadixColorScale( + ColorSwatch(0xff12a594, { + 1: Color(0xfffafefd), + 2: Color(0xfff3fbf9), + 3: Color(0xffe0f8f3), + 4: Color(0xffccf3ea), + 5: Color(0xffb8eae0), + 6: Color(0xffa1ded2), + 7: Color(0xff83cdc1), + 8: Color(0xff53b9ab), + 9: Color(0xff12a594), + 10: Color(0xff0d9b8a), + 11: Color(0xff008573), + 12: Color(0xff0d3d38), + }), + ColorSwatch(0xed009e8c, { + 1: Color(0x0500cc99), + 2: Color(0x0c00aa80), + 3: Color(0x1f00c69d), + 4: Color(0x3300c396), + 5: Color(0x4700b490), + 6: Color(0x5e00a685), + 7: Color(0x7c009980), + 8: Color(0xac009783), + 9: Color(0xed009e8c), + 10: Color(0xf2009684), + 11: Color(0xff008573), + 12: Color(0xf200332d), + }), + ), + Color(0xccf0faf8), + Color(0xff12a594), + Color(0xff12a594), + Color(0xffffffff), +); + +const _tealDark = RadixColor( + RadixColorScale( + ColorSwatch(0xff12a594, { + 1: Color(0xff0d1514), + 2: Color(0xff111c1b), + 3: Color(0xff0d2d2a), + 4: Color(0xff023b37), + 5: Color(0xff084843), + 6: Color(0xff145750), + 7: Color(0xff1c6961), + 8: Color(0xff207e73), + 9: Color(0xff12a594), + 10: Color(0xff0eb39e), + 11: Color(0xff0bd8b6), + 12: Color(0xffadf0dd), + }), + ColorSwatch(0x9f13ffe4, { + 1: Color(0x0500deab), + 2: Color(0x0c12fbe6), + 3: Color(0x1e00ffe6), + 4: Color(0x2d00ffe9), + 5: Color(0x3b00ffea), + 6: Color(0x4b1cffe8), + 7: Color(0x5f2efde8), + 8: Color(0x7532ffe7), + 9: Color(0x9f13ffe4), + 10: Color(0xae0dffe0), + 11: Color(0xd60afed5), + 12: Color(0xefb8ffeb), + }), + ), + Color(0x80132725), + Color(0xff12a594), + Color(0xff12a594), + Color(0xffffffff), +); + +// jade color scale +const _jadeLight = RadixColor( + RadixColorScale( + ColorSwatch(0xff29a383, { + 1: Color(0xfffbfefd), + 2: Color(0xfff4fbf7), + 3: Color(0xffe6f7ed), + 4: Color(0xffd6f1e3), + 5: Color(0xffc3e9d7), + 6: Color(0xffacdec8), + 7: Color(0xff8bceb6), + 8: Color(0xff56ba9f), + 9: Color(0xff29a383), + 10: Color(0xff26997b), + 11: Color(0xff208368), + 12: Color(0xff1d3b31), + }), + ColorSwatch(0xd600916b, { + 1: Color(0x0400c080), + 2: Color(0x0b00a346), + 3: Color(0x1900ae48), + 4: Color(0x2900a851), + 5: Color(0x3c00a255), + 6: Color(0x53009a57), + 7: Color(0x7400945f), + 8: Color(0xa900976e), + 9: Color(0xd600916b), + 10: Color(0xd9008764), + 11: Color(0xdf007152), + 12: Color(0xe2002217), + }), + ), + Color(0xccf1faf5), + Color(0xff29a383), + Color(0xff29a383), + Color(0xffffffff), +); + +const _jadeDark = RadixColor( + RadixColorScale( + ColorSwatch(0xff29a383, { + 1: Color(0xff0d1512), + 2: Color(0xff121c18), + 3: Color(0xff0f2e22), + 4: Color(0xff0b3b2c), + 5: Color(0xff114837), + 6: Color(0xff1b5745), + 7: Color(0xff246854), + 8: Color(0xff2a7e68), + 9: Color(0xff29a383), + 10: Color(0xff27b08b), + 11: Color(0xff1fd8a4), + 12: Color(0xffadf0d4), + }), + ColorSwatch(0x9d38feca, { + 1: Color(0x0500de45), + 2: Color(0x0c27fba6), + 3: Color(0x2002f999), + 4: Color(0x2d00ffaa), + 5: Color(0x3b11ffb6), + 6: Color(0x4b34ffc2), + 7: Color(0x5e45fdc7), + 8: Color(0x7548ffcf), + 9: Color(0x9d38feca), + 10: Color(0xab31fec7), + 11: Color(0xd621fec0), + 12: Color(0xefb8ffe1), + }), + ), + Color(0x8013271f), + Color(0xff29a383), + Color(0xff29a383), + Color(0xffffffff), +); + +// green color scale +const _greenLight = RadixColor( + RadixColorScale( + ColorSwatch(0xff30a46c, { + 1: Color(0xfffbfefc), + 2: Color(0xfff4fbf6), + 3: Color(0xffe6f6eb), + 4: Color(0xffd6f1df), + 5: Color(0xffc4e8d1), + 6: Color(0xffadddc0), + 7: Color(0xff8eceaa), + 8: Color(0xff5bb98b), + 9: Color(0xff30a46c), + 10: Color(0xff2b9a66), + 11: Color(0xff218358), + 12: Color(0xff193b2d), + }), + ColorSwatch(0xcf008f4a, { + 1: Color(0x0400c040), + 2: Color(0x0b00a32f), + 3: Color(0x1900a433), + 4: Color(0x2900a838), + 5: Color(0x3b019c39), + 6: Color(0x5200963c), + 7: Color(0x71009140), + 8: Color(0xa400924b), + 9: Color(0xcf008f4a), + 10: Color(0xd4008647), + 11: Color(0xde00713f), + 12: Color(0xe6002616), + }), + ), + Color(0xccf1faf4), + Color(0xff30a46c), + Color(0xff30a46c), + Color(0xffffffff), +); + +const _greenDark = RadixColor( + RadixColorScale( + ColorSwatch(0xff30a46c, { + 1: Color(0xff0e1512), + 2: Color(0xff121b17), + 3: Color(0xff132d21), + 4: Color(0xff113b29), + 5: Color(0xff174933), + 6: Color(0xff20573e), + 7: Color(0xff28684a), + 8: Color(0xff2f7c57), + 9: Color(0xff30a46c), + 10: Color(0xff33b074), + 11: Color(0xff3dd68c), + 12: Color(0xffb1f1cb), + }), + ColorSwatch(0x9e44ffa4, { + 1: Color(0x0500de45), + 2: Color(0x0b29f99d), + 3: Color(0x1e22ff99), + 4: Color(0x2d11ff99), + 5: Color(0x3c2bffa2), + 6: Color(0x4b44ffaa), + 7: Color(0x5e50fdac), + 8: Color(0x7354ffad), + 9: Color(0x9e44ffa4), + 10: Color(0xab43fea4), + 11: Color(0xd446fea5), + 12: Color(0xf0bbffd7), + }), + ), + Color(0x8015251d), + Color(0xff30a46c), + Color(0xff30a46c), + Color(0xffffffff), +); + +// grass color scale +const _grassLight = RadixColor( + RadixColorScale( + ColorSwatch(0xff46a758, { + 1: Color(0xfffbfefb), + 2: Color(0xfff5fbf5), + 3: Color(0xffe9f6e9), + 4: Color(0xffdaf1db), + 5: Color(0xffc9e8ca), + 6: Color(0xffb2ddb5), + 7: Color(0xff94ce9a), + 8: Color(0xff65ba74), + 9: Color(0xff46a758), + 10: Color(0xff3e9b4f), + 11: Color(0xff2a7e3b), + 12: Color(0xff203c25), + }), + ColorSwatch(0xb9008619, { + 1: Color(0x0400c000), + 2: Color(0x0a009900), + 3: Color(0x16009700), + 4: Color(0x25009f07), + 5: Color(0x36009305), + 6: Color(0x4d008f0a), + 7: Color(0x6b018b0f), + 8: Color(0x9a008d19), + 9: Color(0xb9008619), + 10: Color(0xc1007b17), + 11: Color(0xd5006514), + 12: Color(0xdf002006), + }), + ), + Color(0xccf3faf3), + Color(0xff46a758), + Color(0xff46a758), + Color(0xffffffff), +); + +const _grassDark = RadixColor( + RadixColorScale( + ColorSwatch(0xff46a758, { + 1: Color(0xff0e1511), + 2: Color(0xff141a15), + 3: Color(0xff1b2a1e), + 4: Color(0xff1d3a24), + 5: Color(0xff25482d), + 6: Color(0xff2d5736), + 7: Color(0xff366740), + 8: Color(0xff3e7949), + 9: Color(0xff46a758), + 10: Color(0xff53b365), + 11: Color(0xff71d083), + 12: Color(0xffc2f0c2), + }), + ColorSwatch(0xa165ff82, { + 1: Color(0x0500de12), + 2: Color(0x0a5ef778), + 3: Color(0x1b70fe8c), + 4: Color(0x2c57ff80), + 5: Color(0x3b68ff8b), + 6: Color(0x4b71ff8f), + 7: Color(0x5d77fd92), + 8: Color(0x7077fd90), + 9: Color(0xa165ff82), + 10: Color(0xae72ff8d), + 11: Color(0xcd89ff9f), + 12: Color(0xefceffce), + }), + ), + Color(0x8019231b), + Color(0xff46a758), + Color(0xff46a758), + Color(0xffffffff), +); + +// bronze color scale +const _bronzeLight = RadixColor( + RadixColorScale( + ColorSwatch(0xffa18072, { + 1: Color(0xfffdfcfc), + 2: Color(0xfffdf7f5), + 3: Color(0xfff6edea), + 4: Color(0xffefe4df), + 5: Color(0xffe7d9d3), + 6: Color(0xffdfcdc5), + 7: Color(0xffd3bcb3), + 8: Color(0xffc2a499), + 9: Color(0xffa18072), + 10: Color(0xff957468), + 11: Color(0xff7d5e54), + 12: Color(0xff43302b), + }), + ColorSwatch(0x8d551a00, { + 1: Color(0x03550000), + 2: Color(0x0acc3300), + 3: Color(0x15922500), + 4: Color(0x20802800), + 5: Color(0x2c742300), + 6: Color(0x3a732400), + 7: Color(0x4c6c1f00), + 8: Color(0x66671c00), + 9: Color(0x8d551a00), + 10: Color(0x974c1500), + 11: Color(0xab3d0f00), + 12: Color(0xd41d0600), + }), + ), + Color(0xccfdf5f3), + Color(0xffa18072), + Color(0xffa18072), + Color(0xffffffff), +); + +const _bronzeDark = RadixColor( + RadixColorScale( + ColorSwatch(0xffa18072, { + 1: Color(0xff141110), + 2: Color(0xff1c1917), + 3: Color(0xff262220), + 4: Color(0xff302a27), + 5: Color(0xff3b3330), + 6: Color(0xff493e3a), + 7: Color(0xff5a4c47), + 8: Color(0xff6f5f58), + 9: Color(0xffa18072), + 10: Color(0xffae8c7e), + 11: Color(0xffd4b3a5), + 12: Color(0xffede0d9), + }), + ColorSwatch(0x9bfec7b0, { + 1: Color(0x04d11100), + 2: Color(0x0cfbbc91), + 3: Color(0x17faceb8), + 4: Color(0x22facdb6), + 5: Color(0x2dffd2c1), + 6: Color(0x3cffd1c0), + 7: Color(0x4ffdd0c0), + 8: Color(0x65ffd6c5), + 9: Color(0x9bfec7b0), + 10: Color(0xa9fecab5), + 11: Color(0xd1ffd7c6), + 12: Color(0xecfff1e9), + }), + ), + Color(0x8027211d), + Color(0xffa18072), + Color(0xffa18072), + Color(0xffffffff), +); + +// gold color scale +const _goldLight = RadixColor( + RadixColorScale( + ColorSwatch(0xff978365, { + 1: Color(0xfffdfdfc), + 2: Color(0xfffaf9f2), + 3: Color(0xfff2f0e7), + 4: Color(0xffeae6db), + 5: Color(0xffe1dccf), + 6: Color(0xffd8d0bf), + 7: Color(0xffcbc0aa), + 8: Color(0xffb9a88d), + 9: Color(0xff978365), + 10: Color(0xff8c7a5e), + 11: Color(0xff71624b), + 12: Color(0xff3b352b), + }), + ColorSwatch(0x9a533200, { + 1: Color(0x03555500), + 2: Color(0x0d9d8a00), + 3: Color(0x18756000), + 4: Color(0x246b4e00), + 5: Color(0x30604600), + 6: Color(0x40644400), + 7: Color(0x55634200), + 8: Color(0x72633d00), + 9: Color(0x9a533200), + 10: Color(0xa1492d00), + 11: Color(0xb4362100), + 12: Color(0xd4130c00), + }), + ), + Color(0xccf9f8ef), + Color(0xff978365), + Color(0xff978365), + Color(0xffffffff), +); + +const _goldDark = RadixColor( + RadixColorScale( + ColorSwatch(0xff978365, { + 1: Color(0xff121211), + 2: Color(0xff1b1a17), + 3: Color(0xff24231f), + 4: Color(0xff2d2b26), + 5: Color(0xff38352e), + 6: Color(0xff444039), + 7: Color(0xff544f46), + 8: Color(0xff696256), + 9: Color(0xff978365), + 10: Color(0xffa39073), + 11: Color(0xffcbb99f), + 12: Color(0xffe8e2d9), + }), + ColorSwatch(0x90ffdba6, { + 1: Color(0x02919111), + 2: Color(0x0bf9e29d), + 3: Color(0x15f8ecbb), + 4: Color(0x1effeec4), + 5: Color(0x2afeecc2), + 6: Color(0x37feebcb), + 7: Color(0x48ffedcd), + 8: Color(0x5ffdeaca), + 9: Color(0x90ffdba6), + 10: Color(0x9dfedfb0), + 11: Color(0xc8fee7c6), + 12: Color(0xe7fef7ed), + }), + ), + Color(0x8025231d), + Color(0xff978365), + Color(0xff978365), + Color(0xffffffff), +); + +// brown color scale +const _brownLight = RadixColor( + RadixColorScale( + ColorSwatch(0xffad7f58, { + 1: Color(0xfffefdfc), + 2: Color(0xfffcf9f6), + 3: Color(0xfff6eee7), + 4: Color(0xfff0e4d9), + 5: Color(0xffebdaca), + 6: Color(0xffe4cdb7), + 7: Color(0xffdcbc9f), + 8: Color(0xffcea37e), + 9: Color(0xffad7f58), + 10: Color(0xffa07553), + 11: Color(0xff815e46), + 12: Color(0xff3e332e), + }), + ColorSwatch(0xa7823c00, { + 1: Color(0x03aa5500), + 2: Color(0x09aa5500), + 3: Color(0x18a04b00), + 4: Color(0x269b4a00), + 5: Color(0x359f4d00), + 6: Color(0x48a04e00), + 7: Color(0x60a34e00), + 8: Color(0x819f4a00), + 9: Color(0xa7823c00), + 10: Color(0xac723300), + 11: Color(0xb9522100), + 12: Color(0xd1140600), + }), + ), + Color(0xccfbf8f4), + Color(0xffad7f58), + Color(0xffad7f58), + Color(0xffffffff), +); + +const _brownDark = RadixColor( + RadixColorScale( + ColorSwatch(0xffad7f58, { + 1: Color(0xff12110f), + 2: Color(0xff1c1816), + 3: Color(0xff28211d), + 4: Color(0xff322922), + 5: Color(0xff3e3128), + 6: Color(0xff4d3c2f), + 7: Color(0xff614a39), + 8: Color(0xff7c5f46), + 9: Color(0xffad7f58), + 10: Color(0xffb88c67), + 11: Color(0xffdbb594), + 12: Color(0xfff2e1ca), + }), + ColorSwatch(0xa8feb87d, { + 1: Color(0x02911100), + 2: Color(0x0cfba67c), + 3: Color(0x19fcb58c), + 4: Color(0x24fbbb8a), + 5: Color(0x31fcb889), + 6: Color(0x41fdba87), + 7: Color(0x56ffbb88), + 8: Color(0x73ffbe87), + 9: Color(0xa8feb87d), + 10: Color(0xb3ffc18c), + 11: Color(0xd9fed1aa), + 12: Color(0xf2feecd4), + }), + ), + Color(0x80271f1b), + Color(0xffad7f58), + Color(0xffad7f58), + Color(0xffffffff), +); + +// orange color scale +const _orangeLight = RadixColor( + RadixColorScale( + ColorSwatch(0xfff76b15, { + 1: Color(0xfffefcfb), + 2: Color(0xfffff7ed), + 3: Color(0xffffefd6), + 4: Color(0xffffdfb5), + 5: Color(0xffffd19a), + 6: Color(0xffffc182), + 7: Color(0xfff5ae73), + 8: Color(0xffec9455), + 9: Color(0xfff76b15), + 10: Color(0xffef5f00), + 11: Color(0xffcc4e00), + 12: Color(0xff582d1d), + }), + ColorSwatch(0xeaf65e00, { + 1: Color(0x04c04000), + 2: Color(0x12ff8e00), + 3: Color(0x29ff9c00), + 4: Color(0x4aff9101), + 5: Color(0x65ff8b00), + 6: Color(0x7dff8100), + 7: Color(0x8ced6c00), + 8: Color(0xaae35f00), + 9: Color(0xeaf65e00), + 10: Color(0xffef5f00), + 11: Color(0xffcc4e00), + 12: Color(0xe2431200), + }), + ), + Color(0xccfff5e9), + Color(0xfff76b15), + Color(0xfff76b15), + Color(0xffffffff), +); + +const _orangeDark = RadixColor( + RadixColorScale( + ColorSwatch(0xfff76b15, { + 1: Color(0xff17120e), + 2: Color(0xff1e160f), + 3: Color(0xff331e0b), + 4: Color(0xff462100), + 5: Color(0xff562800), + 6: Color(0xff66350c), + 7: Color(0xff7e451d), + 8: Color(0xffa35829), + 9: Color(0xfff76b15), + 10: Color(0xffff801f), + 11: Color(0xffffa057), + 12: Color(0xffffe0c2), + }), + ColorSwatch(0xf7fe6d15, { + 1: Color(0x07ec3600), + 2: Color(0x0efe6d00), + 3: Color(0x25fb6a00), + 4: Color(0x39ff5900), + 5: Color(0x4aff6100), + 6: Color(0x5cfd7504), + 7: Color(0x75ff832c), + 8: Color(0x9dfe8438), + 9: Color(0xf7fe6d15), + 10: Color(0xffff801f), + 11: Color(0xffffa057), + 12: Color(0xffffe0c2), + }), + ), + Color(0x80271d13), + Color(0xfff76b15), + Color(0xfff76b15), + Color(0xffffffff), +); + +// amber color scale +const _amberLight = RadixColor( + RadixColorScale( + ColorSwatch(0xffffc53d, { + 1: Color(0xfffefdfb), + 2: Color(0xfffefbe9), + 3: Color(0xfffff7c2), + 4: Color(0xffffee9c), + 5: Color(0xfffbe577), + 6: Color(0xfff3d673), + 7: Color(0xffe9c162), + 8: Color(0xffe2a336), + 9: Color(0xffffc53d), + 10: Color(0xffffba18), + 11: Color(0xffab6400), + 12: Color(0xff4f3422), + }), + ColorSwatch(0xc2ffb300, { + 1: Color(0x04c08000), + 2: Color(0x16f4d100), + 3: Color(0x3dffde00), + 4: Color(0x63ffd400), + 5: Color(0x88f8cf00), + 6: Color(0x8ceab500), + 7: Color(0x9ddc9b00), + 8: Color(0xc9da8a00), + 9: Color(0xc2ffb300), + 10: Color(0xe7ffb300), + 11: Color(0xffab6400), + 12: Color(0xdd341500), + }), + ), + Color(0xccfefae4), + Color(0xffffc53d), + Color(0xffffc53d), + Color(0xff21201c), +); + +const _amberDark = RadixColor( + RadixColorScale( + ColorSwatch(0xffffc53d, { + 1: Color(0xff16120c), + 2: Color(0xff1d180f), + 3: Color(0xff302008), + 4: Color(0xff3f2700), + 5: Color(0xff4d3000), + 6: Color(0xff5c3d05), + 7: Color(0xff714f19), + 8: Color(0xff8f6424), + 9: Color(0xffffc53d), + 10: Color(0xffffd60a), + 11: Color(0xffffca16), + 12: Color(0xffffe7b3), + }), + ColorSwatch(0xffffc53d, { + 1: Color(0x06e63c00), + 2: Color(0x0dfd9b00), + 3: Color(0x22fa8200), + 4: Color(0x32fc8200), + 5: Color(0x41fd8b00), + 6: Color(0x51fd9b00), + 7: Color(0x67ffab25), + 8: Color(0x87ffae35), + 9: Color(0xffffc53d), + 10: Color(0xffffd60a), + 11: Color(0xffffca16), + 12: Color(0xffffe7b3), + }), + ), + Color(0x80271f13), + Color(0xffffc53d), + Color(0xffe2ac37), + Color(0xff21201c), +); + +// yellow color scale +const _yellowLight = RadixColor( + RadixColorScale( + ColorSwatch(0xffffe629, { + 1: Color(0xfffdfdf9), + 2: Color(0xfffefce9), + 3: Color(0xfffffab8), + 4: Color(0xfffff394), + 5: Color(0xffffe770), + 6: Color(0xfff3d768), + 7: Color(0xffe4c767), + 8: Color(0xffd5ae39), + 9: Color(0xffffe629), + 10: Color(0xffffdc00), + 11: Color(0xff9e6c00), + 12: Color(0xff473b1f), + }), + ColorSwatch(0xd6ffe100, { + 1: Color(0x06aaaa00), + 2: Color(0x16f4dd00), + 3: Color(0x47ffee00), + 4: Color(0x6bffe301), + 5: Color(0x8fffd500), + 6: Color(0x97ebbc00), + 7: Color(0x98d2a100), + 8: Color(0xc6c99700), + 9: Color(0xd6ffe100), + 10: Color(0xffffdc00), + 11: Color(0xff9e6c00), + 12: Color(0xe02e2000), + }), + ), + Color(0xccfefbe4), + Color(0xffffe629), + Color(0xffffe629), + Color(0xff21201c), +); + +const _yellowDark = RadixColor( + RadixColorScale( + ColorSwatch(0xffffe629, { + 1: Color(0xff14120b), + 2: Color(0xff1b180f), + 3: Color(0xff2d2305), + 4: Color(0xff362b00), + 5: Color(0xff433500), + 6: Color(0xff524202), + 7: Color(0xff665417), + 8: Color(0xff836a21), + 9: Color(0xffffe629), + 10: Color(0xffffff57), + 11: Color(0xfff5e147), + 12: Color(0xfff6eeb4), + }), + ColorSwatch(0xffffe629, { + 1: Color(0x04d15100), + 2: Color(0x0bf9b400), + 3: Color(0x1effaa00), + 4: Color(0x28fdb700), + 5: Color(0x36febb00), + 6: Color(0x46fec400), + 7: Color(0x5cfdcb22), + 8: Color(0x7bfdca32), + 9: Color(0xffffe629), + 10: Color(0xffffff57), + 11: Color(0xf5fee949), + 12: Color(0xf6fef6ba), + }), + ), + Color(0x80231f13), + Color(0xffffe629), + Color(0xffd2b929), + Color(0xff21201c), +); + +// lime color scale +const _limeLight = RadixColor( + RadixColorScale( + ColorSwatch(0xffbdee63, { + 1: Color(0xfffcfdfa), + 2: Color(0xfff8faf3), + 3: Color(0xffeef6d6), + 4: Color(0xffe2f0bd), + 5: Color(0xffd3e7a6), + 6: Color(0xffc2da91), + 7: Color(0xffabc978), + 8: Color(0xff8db654), + 9: Color(0xffbdee63), + 10: Color(0xffb0e64c), + 11: Color(0xff5c7c2f), + 12: Color(0xff37401c), + }), + ColorSwatch(0x9c93e400, { + 1: Color(0x05669900), + 2: Color(0x0c6b9500), + 3: Color(0x2996c800), + 4: Color(0x428fc600), + 5: Color(0x5981bb00), + 6: Color(0x6e72aa00), + 7: Color(0x87619900), + 8: Color(0xab559200), + 9: Color(0x9c93e400), + 10: Color(0xb38fdc00), + 11: Color(0xd0375f00), + 12: Color(0xe31e2900), + }), + ), + Color(0xccf6f9f0), + Color(0xffbdee63), + Color(0xffbdee63), + Color(0xff1d211c), +); + +const _limeDark = RadixColor( + RadixColorScale( + ColorSwatch(0xffbdee63, { + 1: Color(0xff11130c), + 2: Color(0xff151a10), + 3: Color(0xff1f2917), + 4: Color(0xff29371d), + 5: Color(0xff334423), + 6: Color(0xff3d522a), + 7: Color(0xff496231), + 8: Color(0xff577538), + 9: Color(0xffbdee63), + 10: Color(0xffd4ff70), + 11: Color(0xffbde56c), + 12: Color(0xffe3f7ba), + }), + ColorSwatch(0xedcaff69, { + 1: Color(0x0311bb00), + 2: Color(0x0a78f700), + 3: Color(0x1a9bfd4c), + 4: Color(0x29a7fe5c), + 5: Color(0x37affe65), + 6: Color(0x46b2fe6d), + 7: Color(0x57b6ff6f), + 8: Color(0x6cb6fd6d), + 9: Color(0xedcaff69), + 10: Color(0xffd4ff70), + 11: Color(0xe4d1fe77), + 12: Color(0xf7e9febf), + }), + ), + Color(0x801b2115), + Color(0xffbdee63), + Color(0xff98c254), + Color(0xff1d211c), +); + +// mint color scale +const _mintLight = RadixColor( + RadixColorScale( + ColorSwatch(0xff86ead4, { + 1: Color(0xfff9fefd), + 2: Color(0xfff2fbf9), + 3: Color(0xffddf9f2), + 4: Color(0xffc8f4e9), + 5: Color(0xffb3ecde), + 6: Color(0xff9ce0d0), + 7: Color(0xff7ecfbd), + 8: Color(0xff4cbba5), + 9: Color(0xff86ead4), + 10: Color(0xff7de0cb), + 11: Color(0xff027864), + 12: Color(0xff16433c), + }), + ColorSwatch(0x7900d3a5, { + 1: Color(0x0600d5aa), + 2: Color(0x0d00b18a), + 3: Color(0x2200d29e), + 4: Color(0x3700cc99), + 5: Color(0x4c00c091), + 6: Color(0x6300b086), + 7: Color(0x8100a17d), + 8: Color(0xb3009e7f), + 9: Color(0x7900d3a5), + 10: Color(0x8200c399), + 11: Color(0xfd007763), + 12: Color(0xe900312a), + }), + ), + Color(0xcceffaf8), + Color(0xff86ead4), + Color(0xff86ead4), + Color(0xff1a211e), +); + +const _mintDark = RadixColor( + RadixColorScale( + ColorSwatch(0xff86ead4, { + 1: Color(0xff0e1515), + 2: Color(0xff0f1b1b), + 3: Color(0xff092c2b), + 4: Color(0xff003a38), + 5: Color(0xff004744), + 6: Color(0xff105650), + 7: Color(0xff1e685f), + 8: Color(0xff277f70), + 9: Color(0xff86ead4), + 10: Color(0xffa8f5e5), + 11: Color(0xff58d5ba), + 12: Color(0xffc4f5e1), + }), + ColorSwatch(0xe992ffe7, { + 1: Color(0x0500dede), + 2: Color(0x0b00f9f9), + 3: Color(0x1d00fff6), + 4: Color(0x2c00fff4), + 5: Color(0x3a00fff2), + 6: Color(0x4a0effeb), + 7: Color(0x5e34fde5), + 8: Color(0x7641ffdf), + 9: Color(0xe992ffe7), + 10: Color(0xf5aefeed), + 11: Color(0xd267ffde), + 12: Color(0xf5cbfee9), + }), + ), + Color(0x80152727), + Color(0xff86ead4), + Color(0xff65c3b0), + Color(0xff1a211e), +); + +// sky color scale +const _skyLight = RadixColor( + RadixColorScale( + ColorSwatch(0xff7ce2fe, { + 1: Color(0xfff9feff), + 2: Color(0xfff1fafd), + 3: Color(0xffe1f6fd), + 4: Color(0xffd1f0fa), + 5: Color(0xffbee7f5), + 6: Color(0xffa9daed), + 7: Color(0xff8dcae3), + 8: Color(0xff60b3d7), + 9: Color(0xff7ce2fe), + 10: Color(0xff74daf8), + 11: Color(0xff00749e), + 12: Color(0xff1d3e56), + }), + ColorSwatch(0x8300c7fe, { + 1: Color(0x0600d5ff), + 2: Color(0x0e00a4db), + 3: Color(0x1e00b3ee), + 4: Color(0x2e00ace4), + 5: Color(0x4100a1d8), + 6: Color(0x560092ca), + 7: Color(0x720089c1), + 8: Color(0x9f0085bf), + 9: Color(0x8300c7fe), + 10: Color(0x8b00bcf3), + 11: Color(0xff00749e), + 12: Color(0xe2002540), + }), + ), + Color(0xcceef9fd), + Color(0xff7ce2fe), + Color(0xff7ce2fe), + Color(0xff1c2024), +); + +const _skyDark = RadixColor( + RadixColorScale( + ColorSwatch(0xff7ce2fe, { + 1: Color(0xff0d141f), + 2: Color(0xff111a27), + 3: Color(0xff112840), + 4: Color(0xff113555), + 5: Color(0xff154467), + 6: Color(0xff1b537b), + 7: Color(0xff1f6692), + 8: Color(0xff197cae), + 9: Color(0xff7ce2fe), + 10: Color(0xffa8eeff), + 11: Color(0xff75c7f0), + 12: Color(0xffc2f3ff), + }), + ColorSwatch(0xfe7ce3ff, { + 1: Color(0x0f0044ff), + 2: Color(0x181171fb), + 3: Color(0x331184fc), + 4: Color(0x49128fff), + 5: Color(0x5d1c9dfd), + 6: Color(0x7228a5ff), + 7: Color(0x8b2badfe), + 8: Color(0xa91db2fe), + 9: Color(0xfe7ce3ff), + 10: Color(0xffa8eeff), + 11: Color(0xef7cd3ff), + 12: Color(0xffc2f3ff), + }), + ), + Color(0x8013233b), + Color(0xff7ce2fe), + Color(0xff5bbde2), + Color(0xff1c2024), +); + +// blackA neutral +const _blackAlphaAlpha = ColorSwatch(0xb3000000, { + 1: Color(0x0d000000), + 2: Color(0x1a000000), + 3: Color(0x26000000), + 4: Color(0x33000000), + 5: Color(0x4d000000), + 6: Color(0x66000000), + 7: Color(0x80000000), + 8: Color(0x99000000), + 9: Color(0xb3000000), + 10: Color(0xcc000000), + 11: Color(0xe6000000), + 12: Color(0xf2000000), +}); + +// whiteA neutral +const _whiteAlphaAlpha = ColorSwatch(0xb3ffffff, { + 1: Color(0x0dffffff), + 2: Color(0x1affffff), + 3: Color(0x26ffffff), + 4: Color(0x33ffffff), + 5: Color(0x4dffffff), + 6: Color(0x66ffffff), + 7: Color(0x80ffffff), + 8: Color(0x99ffffff), + 9: Color(0xb3ffffff), + 10: Color(0xccffffff), + 11: Color(0xe6ffffff), + 12: Color(0xf2ffffff), +}); + +// Color theme instances +const gray = RadixColorTheme(_grayLight, _grayDark); +const mauve = RadixColorTheme(_mauveLight, _mauveDark); +const slate = RadixColorTheme(_slateLight, _slateDark); +const sage = RadixColorTheme(_sageLight, _sageDark); +const olive = RadixColorTheme(_oliveLight, _oliveDark); +const sand = RadixColorTheme(_sandLight, _sandDark); +const tomato = RadixColorTheme(_tomatoLight, _tomatoDark); +const red = RadixColorTheme(_redLight, _redDark); +const ruby = RadixColorTheme(_rubyLight, _rubyDark); +const crimson = RadixColorTheme(_crimsonLight, _crimsonDark); +const pink = RadixColorTheme(_pinkLight, _pinkDark); +const plum = RadixColorTheme(_plumLight, _plumDark); +const purple = RadixColorTheme(_purpleLight, _purpleDark); +const violet = RadixColorTheme(_violetLight, _violetDark); +const iris = RadixColorTheme(_irisLight, _irisDark); +const indigo = RadixColorTheme(_indigoLight, _indigoDark); +const blue = RadixColorTheme(_blueLight, _blueDark); +const cyan = RadixColorTheme(_cyanLight, _cyanDark); +const teal = RadixColorTheme(_tealLight, _tealDark); +const jade = RadixColorTheme(_jadeLight, _jadeDark); +const green = RadixColorTheme(_greenLight, _greenDark); +const grass = RadixColorTheme(_grassLight, _grassDark); +const bronze = RadixColorTheme(_bronzeLight, _bronzeDark); +const gold = RadixColorTheme(_goldLight, _goldDark); +const brown = RadixColorTheme(_brownLight, _brownDark); +const orange = RadixColorTheme(_orangeLight, _orangeDark); +const amber = RadixColorTheme(_amberLight, _amberDark); +const yellow = RadixColorTheme(_yellowLight, _yellowDark); +const lime = RadixColorTheme(_limeLight, _limeDark); +const mint = RadixColorTheme(_mintLight, _mintDark); +const sky = RadixColorTheme(_skyLight, _skyDark); + +// Neutral instances +const blackAlpha = _blackAlphaAlpha; +const whiteAlpha = _whiteAlphaAlpha; diff --git a/packages/remix_cli/lib/src/registry/fortal/templates/theme/surface_frame.dart.tmpl b/packages/remix_cli/lib/src/registry/fortal/templates/theme/surface_frame.dart.tmpl new file mode 100644 index 000000000..6a3862868 --- /dev/null +++ b/packages/remix_cli/lib/src/registry/fortal/templates/theme/surface_frame.dart.tmpl @@ -0,0 +1,31 @@ +import 'package:flutter/widgets.dart'; +import 'package:remix/remix.dart'; + +/// Builds a rounded panel whose visible frame paints above its children. +/// +/// Composite surfaces often contain edge-to-edge child backgrounds. A regular +/// [BoxDecoration] border paints behind those children, so their antialiased +/// rounded edges can partially cover the frame. This keeps the fill and clip on +/// the background decoration, reserves the same inset explicitly, and paints +/// the frame through the container's foreground decoration. +BoxStyler {{valuePrefix}}SurfaceFrame({ + required Color fillColor, + required Color borderColor, + required double borderWidth, + required Radius radius, +}) { + final borderRadius = BorderRadiusMix.all(radius); + return BoxStyler() + .color(fillColor) + .padding(.all(borderWidth)) + .borderRadius(borderRadius) + .clipBehavior(.antiAlias) + .foregroundDecoration( + BoxDecorationMix( + border: BoxBorderMix.all( + BorderSideMix(color: borderColor, width: borderWidth), + ), + borderRadius: borderRadius, + ), + ); +} diff --git a/packages/remix_cli/lib/src/registry/fortal/templates/theme/theme.dart.tmpl b/packages/remix_cli/lib/src/registry/fortal/templates/theme/theme.dart.tmpl new file mode 100644 index 000000000..7795f9dfd --- /dev/null +++ b/packages/remix_cli/lib/src/registry/fortal/templates/theme/theme.dart.tmpl @@ -0,0 +1,14 @@ +// Internal entrypoint for the theme layer, imported by every component under +// `src/components/`. The public entrypoint exports this barrel. + +export 'computed.dart'; +export 'control_styles.dart'; +export 'surface_frame.dart'; +export 'theme_data.dart' hide build{{typePrefix}}ScopeTokens; +export 'theme_scope.dart'; +export 'tokens.dart'; + +// `radix_colors.dart` deliberately stays out of this barrel because it exposes +// bare color names such as `gray` and `red`. The public package barrel exports +// it explicitly for compatibility; installed applications import it only from +// `theme_data.dart`. diff --git a/packages/remix_cli/lib/src/registry/fortal/templates/theme/theme_data.dart.tmpl b/packages/remix_cli/lib/src/registry/fortal/templates/theme/theme_data.dart.tmpl new file mode 100644 index 000000000..c84e2fbe3 --- /dev/null +++ b/packages/remix_cli/lib/src/registry/fortal/templates/theme/theme_data.dart.tmpl @@ -0,0 +1,1090 @@ +import 'dart:math' as math; + +import 'package:flutter/widgets.dart'; +import 'package:remix/remix.dart'; + +import 'computed.dart'; +import 'radix_colors.dart' as radix; +import 'theme_scope.dart' show {{typePrefix}}Scope; +import 'tokens.dart'; + +/// Available accent colors matching Radix Themes names. +enum {{typePrefix}}AccentColor { + gray, + mauve, + slate, + sage, + olive, + sand, + gold, + bronze, + brown, + yellow, + amber, + orange, + tomato, + red, + ruby, + crimson, + pink, + plum, + purple, + violet, + iris, + indigo, + blue, + cyan, + teal, + jade, + green, + grass, + lime, + mint, + sky, +} + +/// Available neutral gray families matching Radix Themes names. +enum {{typePrefix}}GrayColor { gray, mauve, slate, sage, olive, sand } + +/// Theme-level radius multipliers matching the Radix Themes presets. +enum {{typePrefix}}Radius { none, small, medium, large, full } + +/// Background treatment used by floating panels. +enum {{typePrefix}}PanelBackground { solid, translucent } + +/// Discrete theme scaling values supported by Radix Themes. +enum {{typePrefix}}Scaling { + percent90(0.9), + percent95(0.95), + percent100(1.0), + percent105(1.05), + percent110(1.1); + + const {{typePrefix}}Scaling(this.factor); + + /// Numeric multiplier represented by this preset. + final double factor; +} + +/// Partial theme values applied by a [{{typePrefix}}Scope]. +@immutable +class {{typePrefix}}ThemeConfig { + const {{typePrefix}}ThemeConfig({ + this.accent, + this.gray, + this.brightness, + this.panelBackground, + this.radius, + this.scaling, + this.hasBackground, + }); + + final {{typePrefix}}AccentColor? accent; + final {{typePrefix}}GrayColor? gray; + final Brightness? brightness; + final {{typePrefix}}PanelBackground? panelBackground; + final {{typePrefix}}Radius? radius; + final {{typePrefix}}Scaling? scaling; + final bool? hasBackground; + + bool get isDark => brightness == .dark; + + {{typePrefix}}ThemeConfig copyWith({ + {{typePrefix}}AccentColor? accent, + {{typePrefix}}GrayColor? gray, + Brightness? brightness, + {{typePrefix}}PanelBackground? panelBackground, + {{typePrefix}}Radius? radius, + {{typePrefix}}Scaling? scaling, + bool? hasBackground, + }) => {{typePrefix}}ThemeConfig( + accent: accent ?? this.accent, + gray: gray ?? this.gray, + brightness: brightness ?? this.brightness, + panelBackground: panelBackground ?? this.panelBackground, + radius: radius ?? this.radius, + scaling: scaling ?? this.scaling, + hasBackground: hasBackground ?? this.hasBackground, + ); + + @override + bool operator ==(Object other) => + identical(this, other) || + other is {{typePrefix}}ThemeConfig && + accent == other.accent && + gray == other.gray && + brightness == other.brightness && + panelBackground == other.panelBackground && + radius == other.radius && + scaling == other.scaling && + hasBackground == other.hasBackground; + + @override + int get hashCode => Object.hash( + accent, + gray, + brightness, + panelBackground, + radius, + scaling, + hasBackground, + ); + + Widget createScope({List? orderOfModifiers, required Widget child}) => + {{typePrefix}}Scope( + accent: accent, + gray: gray, + brightness: brightness, + panelBackground: panelBackground, + radius: radius, + scaling: scaling, + hasBackground: hasBackground, + orderOfModifiers: orderOfModifiers, + child: child, + ); +} + +/// Fully resolved theme values inherited by a {{typePrefix}} subtree. +@immutable +class {{typePrefix}}ThemeData extends {{typePrefix}}ThemeConfig { + const {{typePrefix}}ThemeData({ + required this.accent, + required this.gray, + required this.brightness, + required this.panelBackground, + required this.radius, + required this.scaling, + required this.hasBackground, + }) : super( + accent: accent, + gray: gray, + brightness: brightness, + panelBackground: panelBackground, + radius: radius, + scaling: scaling, + hasBackground: hasBackground, + ); + + @override + final {{typePrefix}}AccentColor accent; + @override + final {{typePrefix}}GrayColor gray; + @override + final Brightness brightness; + @override + final {{typePrefix}}PanelBackground panelBackground; + @override + final {{typePrefix}}Radius radius; + @override + final {{typePrefix}}Scaling scaling; + @override + final bool hasBackground; + + @override + bool get isDark => brightness == .dark; + + @override + {{typePrefix}}ThemeData copyWith({ + {{typePrefix}}AccentColor? accent, + {{typePrefix}}GrayColor? gray, + Brightness? brightness, + {{typePrefix}}PanelBackground? panelBackground, + {{typePrefix}}Radius? radius, + {{typePrefix}}Scaling? scaling, + bool? hasBackground, + }) => {{typePrefix}}ThemeData( + accent: accent ?? this.accent, + gray: gray ?? this.gray, + brightness: brightness ?? this.brightness, + panelBackground: panelBackground ?? this.panelBackground, + radius: radius ?? this.radius, + scaling: scaling ?? this.scaling, + hasBackground: hasBackground ?? this.hasBackground, + ); + + @override + bool operator ==(Object other) => + identical(this, other) || + other is {{typePrefix}}ThemeData && + accent == other.accent && + gray == other.gray && + brightness == other.brightness && + panelBackground == other.panelBackground && + radius == other.radius && + scaling == other.scaling && + hasBackground == other.hasBackground; + + @override + int get hashCode => Object.hash( + accent, + gray, + brightness, + panelBackground, + radius, + scaling, + hasBackground, + ); +} + +/// Builds the token map for a {{typePrefix}} scope. Used by [{{typePrefix}}Scope]. +Map, Object> build{{typePrefix}}ScopeTokens({{typePrefix}}ThemeData theme) { + final tokens = resolve{{typePrefix}}Tokens(theme); + final scaling = theme.scaling.factor; + final shadows = build{{typePrefix}}Shadows(isDark: theme.isDark, colors: tokens); + + final colorTokens = { + // Role and functional tokens + {{typePrefix}}Tokens.colorBackground: tokens.colorBackground, + {{typePrefix}}Tokens.colorSurface: tokens.colorSurface, + {{typePrefix}}Tokens.segmentedControlIndicatorBackground: theme.isDark + ? tokens.gray.scale.alphaStep(3) + : tokens.colorBackground, + {{typePrefix}}Tokens.colorPanelSolid: tokens.colorPanelSolid, + {{typePrefix}}Tokens.colorPanelTranslucent: tokens.colorPanelTranslucent, + {{typePrefix}}Tokens.colorPanel: theme.panelBackground == .solid + ? tokens.colorPanelSolid + : tokens.colorPanelTranslucent, + {{typePrefix}}Tokens.colorOverlay: tokens.colorOverlay, + {{typePrefix}}Tokens.sliderHighContrastOverlay: theme.isDark + ? const Color(0x00000000) + : tokens.blackAlpha[8]!, + {{typePrefix}}Tokens.error3: (theme.isDark ? radix.red.dark : radix.red.light).scale + .step(3), + {{typePrefix}}Tokens.error7: (theme.isDark ? radix.red.dark : radix.red.light).scale + .step(7), + {{typePrefix}}Tokens.error8: (theme.isDark ? radix.red.dark : radix.red.light).scale + .step(8), + {{typePrefix}}Tokens.error9: (theme.isDark ? radix.red.dark : radix.red.light).scale + .step(9), + {{typePrefix}}Tokens.error11: (theme.isDark ? radix.red.dark : radix.red.light) + .scale + .step(11), + {{typePrefix}}Tokens.error12: (theme.isDark ? radix.red.dark : radix.red.light) + .scale + .step(12), + {{typePrefix}}Tokens.errorA7: (theme.isDark ? radix.red.dark : radix.red.light) + .scale + .alphaStep(7), + ..._accentColorTokens(tokens), + // Gray steps + {{typePrefix}}Tokens.gray1: tokens.gray.scale.step(1), + {{typePrefix}}Tokens.gray2: tokens.gray.scale.step(2), + {{typePrefix}}Tokens.gray3: tokens.gray.scale.step(3), + {{typePrefix}}Tokens.gray4: tokens.gray.scale.step(4), + {{typePrefix}}Tokens.gray5: tokens.gray.scale.step(5), + {{typePrefix}}Tokens.gray6: tokens.gray.scale.step(6), + {{typePrefix}}Tokens.gray7: tokens.gray.scale.step(7), + {{typePrefix}}Tokens.gray8: tokens.gray.scale.step(8), + {{typePrefix}}Tokens.gray9: tokens.gray.scale.step(9), + {{typePrefix}}Tokens.gray10: tokens.gray.scale.step(10), + {{typePrefix}}Tokens.gray11: tokens.gray.scale.step(11), + {{typePrefix}}Tokens.gray12: tokens.gray.scale.step(12), + // Gray role tokens (from resolved colors) + {{typePrefix}}Tokens.graySurface: tokens.gray.surface, + {{typePrefix}}Tokens.grayIndicator: tokens.gray.indicator, + {{typePrefix}}Tokens.grayTrack: tokens.gray.track, + {{typePrefix}}Tokens.grayContrast: tokens.gray.contrast, + // Gray alpha a1..a12 + {{typePrefix}}Tokens.grayA1: tokens.gray.scale.alphaStep(1), + {{typePrefix}}Tokens.grayA2: tokens.gray.scale.alphaStep(2), + {{typePrefix}}Tokens.grayA3: tokens.gray.scale.alphaStep(3), + {{typePrefix}}Tokens.grayA4: tokens.gray.scale.alphaStep(4), + {{typePrefix}}Tokens.grayA5: tokens.gray.scale.alphaStep(5), + {{typePrefix}}Tokens.grayA6: tokens.gray.scale.alphaStep(6), + {{typePrefix}}Tokens.grayA7: tokens.gray.scale.alphaStep(7), + {{typePrefix}}Tokens.grayA8: tokens.gray.scale.alphaStep(8), + {{typePrefix}}Tokens.grayA9: tokens.gray.scale.alphaStep(9), + {{typePrefix}}Tokens.grayA10: tokens.gray.scale.alphaStep(10), + {{typePrefix}}Tokens.grayA11: tokens.gray.scale.alphaStep(11), + {{typePrefix}}Tokens.grayA12: tokens.gray.scale.alphaStep(12), + // Neutral helpers derived from primitives + {{typePrefix}}Tokens.blackA1: tokens.blackAlpha[1]!, + {{typePrefix}}Tokens.blackA2: tokens.blackAlpha[2]!, + {{typePrefix}}Tokens.blackA3: tokens.blackAlpha[3]!, + {{typePrefix}}Tokens.blackA4: tokens.blackAlpha[4]!, + {{typePrefix}}Tokens.blackA5: tokens.blackAlpha[5]!, + {{typePrefix}}Tokens.blackA6: tokens.blackAlpha[6]!, + {{typePrefix}}Tokens.blackA7: tokens.blackAlpha[7]!, + {{typePrefix}}Tokens.blackA8: tokens.blackAlpha[8]!, + {{typePrefix}}Tokens.blackA9: tokens.blackAlpha[9]!, + {{typePrefix}}Tokens.blackA10: tokens.blackAlpha[10]!, + {{typePrefix}}Tokens.blackA11: tokens.blackAlpha[11]!, + {{typePrefix}}Tokens.blackA12: tokens.blackAlpha[12]!, + {{typePrefix}}Tokens.whiteA1: tokens.whiteAlpha[1]!, + {{typePrefix}}Tokens.whiteA2: tokens.whiteAlpha[2]!, + {{typePrefix}}Tokens.whiteA3: tokens.whiteAlpha[3]!, + {{typePrefix}}Tokens.whiteA4: tokens.whiteAlpha[4]!, + {{typePrefix}}Tokens.whiteA5: tokens.whiteAlpha[5]!, + {{typePrefix}}Tokens.whiteA6: tokens.whiteAlpha[6]!, + {{typePrefix}}Tokens.whiteA7: tokens.whiteAlpha[7]!, + {{typePrefix}}Tokens.whiteA8: tokens.whiteAlpha[8]!, + {{typePrefix}}Tokens.whiteA9: tokens.whiteAlpha[9]!, + {{typePrefix}}Tokens.whiteA10: tokens.whiteAlpha[10]!, + {{typePrefix}}Tokens.whiteA11: tokens.whiteAlpha[11]!, + {{typePrefix}}Tokens.whiteA12: tokens.whiteAlpha[12]!, + {{typePrefix}}Tokens.shadowStroke: tokens.shadowStroke, + {{typePrefix}}Tokens.grayStroke3: mixOklabPremultiplied( + tokens.gray.scale.alphaStep(3), + tokens.gray.scale.step(3), + 0.25, + ), + {{typePrefix}}Tokens.grayStroke4: mixOklabPremultiplied( + tokens.gray.scale.alphaStep(4), + tokens.gray.scale.step(4), + 0.25, + ), + {{typePrefix}}Tokens.grayStroke5: mixOklabPremultiplied( + tokens.gray.scale.alphaStep(5), + tokens.gray.scale.step(5), + 0.25, + ), + {{typePrefix}}Tokens.grayStroke6: mixOklabPremultiplied( + tokens.gray.scale.alphaStep(6), + tokens.gray.scale.step(6), + 0.25, + ), + {{typePrefix}}Tokens.grayStroke7: mixOklabPremultiplied( + tokens.gray.scale.alphaStep(7), + tokens.gray.scale.step(7), + 0.25, + ), + {{typePrefix}}Tokens.dataTableBorder: mixOklabPremultiplied( + tokens.gray.scale.alphaStep(5), + tokens.gray.scale.step(6), + 0.5, + ), + }; + + // Build base tokens map + final allTokens = , Object>{ + ...colorTokens, + // Defaults (may be overridden by JSON tokens below) + {{typePrefix}}Tokens.panelBlur: + theme.panelBackground == {{typePrefix}}PanelBackground.translucent ? 64.0 : 0.0, + {{typePrefix}}Tokens.space1: 4.0 * scaling, + {{typePrefix}}Tokens.space2: 8.0 * scaling, + {{typePrefix}}Tokens.space3: 12.0 * scaling, + {{typePrefix}}Tokens.space4: 16.0 * scaling, + {{typePrefix}}Tokens.space5: 24.0 * scaling, + {{typePrefix}}Tokens.space6: 32.0 * scaling, + {{typePrefix}}Tokens.space7: 40.0 * scaling, + {{typePrefix}}Tokens.space8: 48.0 * scaling, + {{typePrefix}}Tokens.space9: 64.0 * scaling, + {{typePrefix}}Tokens.spinnerSize3: 20.0 * scaling, + {{typePrefix}}Tokens.dataTableRowHeight1: 36.0 * scaling, + {{typePrefix}}Tokens.dataTableRowHeight2: 44.0 * scaling, + {{typePrefix}}Tokens.toggleGap1: 2.0 * scaling, + {{typePrefix}}Tokens.toggleGap3: 6.0 * scaling, + {{typePrefix}}Tokens.avatarSize6: 80.0 * scaling, + {{typePrefix}}Tokens.avatarSize7: 96.0 * scaling, + {{typePrefix}}Tokens.avatarSize8: 128.0 * scaling, + {{typePrefix}}Tokens.avatarSize9: 160.0 * scaling, + {{typePrefix}}Tokens.avatarIconSize1: 12.0 * scaling, + {{typePrefix}}Tokens.avatarIconSize2: 16.0 * scaling, + {{typePrefix}}Tokens.avatarIconSize3: 20.0 * scaling, + {{typePrefix}}Tokens.avatarIconSize4: 24.0 * scaling, + {{typePrefix}}Tokens.avatarIconSize5: 32.0 * scaling, + {{typePrefix}}Tokens.avatarIconSize6: 40.0 * scaling, + {{typePrefix}}Tokens.avatarIconSize7: 48.0 * scaling, + {{typePrefix}}Tokens.avatarIconSize8: 64.0 * scaling, + {{typePrefix}}Tokens.avatarIconSize9: 80.0 * scaling, + {{typePrefix}}Tokens.badgePaddingX1: 6.0 * scaling, + {{typePrefix}}Tokens.badgePaddingY1: 2.0 * scaling, + {{typePrefix}}Tokens.badgePaddingX3: 10.0 * scaling, + {{typePrefix}}Tokens.checkboxSize1: 14.0 * scaling, + {{typePrefix}}Tokens.checkboxSize3: 20.0 * scaling, + {{typePrefix}}Tokens.checkboxIndicatorSize1: 9.0 * scaling, + {{typePrefix}}Tokens.checkboxIndicatorSize2: 10.0 * scaling, + {{typePrefix}}Tokens.checkboxIndicatorSize3: 12.0 * scaling, + {{typePrefix}}Tokens.checkboxGroupItemGap1: 6.0 * scaling, + {{typePrefix}}Tokens.checkboxGroupItemGap2: 7.0 * scaling, + {{typePrefix}}Tokens.checkboxGroupItemGap3: 8.0 * scaling, + {{typePrefix}}Tokens.radioIndicatorSize1: 5.6 * scaling, + {{typePrefix}}Tokens.radioIndicatorSize2: 6.4 * scaling, + {{typePrefix}}Tokens.radioIndicatorSize3: 8.0 * scaling, + {{typePrefix}}Tokens.checkboxRadius1: _scaledRadiusToken( + theme.radius, + scaling, + 3.0 * 0.875, + ), + {{typePrefix}}Tokens.checkboxRadius3: _scaledRadiusToken( + theme.radius, + scaling, + 3.0 * 1.25, + ), + {{typePrefix}}Tokens.switchHeight2: 20.0 * scaling, + {{typePrefix}}Tokens.switchWidth1: 28.0 * scaling, + {{typePrefix}}Tokens.switchWidth2: 35.0 * scaling, + {{typePrefix}}Tokens.switchWidth3: 42.0 * scaling, + {{typePrefix}}Tokens.switchThumbSize1: 16.0 * scaling - 2.0, + {{typePrefix}}Tokens.switchThumbSize2: 20.0 * scaling - 2.0, + {{typePrefix}}Tokens.switchThumbSize3: 24.0 * scaling - 2.0, + {{typePrefix}}Tokens.progressHeight2: 6.0 * scaling, + {{typePrefix}}Tokens.sliderTrackSize1: 6.0 * scaling, + {{typePrefix}}Tokens.sliderTrackSize2: 8.0 * scaling, + {{typePrefix}}Tokens.sliderTrackSize3: 10.0 * scaling, + {{typePrefix}}Tokens.sliderThumbSize1: 13.0 * scaling, + {{typePrefix}}Tokens.sliderThumbSize2: 16.0 * scaling, + {{typePrefix}}Tokens.sliderThumbSize3: 19.0 * scaling, + {{typePrefix}}Tokens.textFieldPadding1: 6.0 * scaling, + {{typePrefix}}Tokens.textFieldPadding2: 8.0 * scaling, + {{typePrefix}}Tokens.textFieldPadding3: 12.0 * scaling, + {{typePrefix}}Tokens.textAreaMinHeight3: 80.0, + {{typePrefix}}Tokens.dataListRowGap3: 20.0 * scaling, + {{typePrefix}}Tokens.dataListLabelMinWidth: 120.0, + {{typePrefix}}Tokens.tabInnerPaddingY1: 2.0 * scaling, + {{typePrefix}}Tokens.tabActiveLetterSpacing1: -0.12 * scaling, + {{typePrefix}}Tokens.tabActiveLetterSpacing2: -0.14 * scaling, + {{typePrefix}}Tokens.selectSpace1Half: 6.0 * scaling, + {{typePrefix}}Tokens.selectIndicatorWidth1: 20.0 * scaling, + {{typePrefix}}Tokens.selectIndicatorSize1: 8.0 * scaling, + {{typePrefix}}Tokens.selectIndicatorSize2: 10.0 * scaling, + {{typePrefix}}Tokens.selectGhostMarginX12: -8.0 * scaling, + {{typePrefix}}Tokens.selectGhostMarginY12: -4.0 * scaling, + {{typePrefix}}Tokens.selectGhostMarginX3: -12.0 * scaling, + {{typePrefix}}Tokens.selectGhostMarginY3: -6.0 * scaling, + ..._radiusTokensFor(theme.radius, scaling), + + // Exact layered Radix shadow tokens, resolved for the active color scales. + ...shadows, + {{typePrefix}}Tokens.sliderClassicDisabledTrackShadows: _scaleShadowOpacity( + shadows[{{typePrefix}}Tokens.shadow1Layers]! as List, + 0.5, + ), + {{typePrefix}}Tokens.cardClassicOuterShadows: _cardClassicShadows( + tokens, + isDark: theme.isDark, + layer: .outer, + state: .idle, + ), + {{typePrefix}}Tokens.cardClassicInnerShadows: _cardClassicShadows( + tokens, + isDark: theme.isDark, + layer: .inner, + state: .idle, + ), + {{typePrefix}}Tokens.cardClassicHoverOuterShadows: _cardClassicShadows( + tokens, + isDark: theme.isDark, + layer: .outer, + state: .hovered, + ), + {{typePrefix}}Tokens.cardClassicHoverInnerShadows: _cardClassicShadows( + tokens, + isDark: theme.isDark, + layer: .inner, + state: .hovered, + ), + {{typePrefix}}Tokens.cardClassicActiveOuterShadows: _cardClassicShadows( + tokens, + isDark: theme.isDark, + layer: .outer, + state: .active, + ), + {{typePrefix}}Tokens.cardClassicActiveInnerShadows: _cardClassicShadows( + tokens, + isDark: theme.isDark, + layer: .inner, + state: .active, + ), + {{typePrefix}}Tokens.selectTriggerClassicShadows: _selectClassicShadows( + tokens, + isDark: theme.isDark, + ), + {{typePrefix}}Tokens.selectTriggerClassicHoverShadows: [ + _insetShadow(tokens.gray.scale.alphaStep(3), spread: 1), + ..._selectClassicShadows(tokens, isDark: theme.isDark), + ], + {{typePrefix}}Tokens.baseButtonClassicDisabledShadows: + _baseButtonClassicDisabledShadows(tokens, isDark: theme.isDark), + {{typePrefix}}Tokens.baseButtonClassicShadows: _baseButtonClassicShadows( + tokens, + isDark: theme.isDark, + highContrast: false, + ), + {{typePrefix}}Tokens.baseButtonClassicHighContrastShadows: + _baseButtonClassicShadows( + tokens, + isDark: theme.isDark, + highContrast: true, + ), + {{typePrefix}}Tokens.baseButtonClassicActiveShadows: + _baseButtonClassicActiveShadows(tokens, highContrast: false), + {{typePrefix}}Tokens.baseButtonClassicActiveHighContrastShadows: + _baseButtonClassicActiveShadows(tokens, highContrast: true), + {{typePrefix}}Tokens.baseButtonClassicAfterInset: theme.isDark ? 1.0 : 2.0, + {{typePrefix}}Tokens.baseButtonGhostPaddingY3: 6.0 * scaling, + {{typePrefix}}Tokens.baseButtonGhostMarginX12: -8.0 * scaling, + {{typePrefix}}Tokens.baseButtonGhostMarginY12: -4.0 * scaling, + {{typePrefix}}Tokens.baseButtonGhostMarginX3: -12.0 * scaling, + {{typePrefix}}Tokens.baseButtonGhostMarginY3: -6.0 * scaling, + {{typePrefix}}Tokens.baseButtonGhostMarginX4: -16.0 * scaling, + {{typePrefix}}Tokens.baseButtonGhostMarginY4: -8.0 * scaling, + {{typePrefix}}Tokens.iconButtonGhostPadding2: 6.0 * scaling, + {{typePrefix}}Tokens.iconButtonGhostMargin1: -4.0 * scaling, + {{typePrefix}}Tokens.iconButtonGhostMargin2: -6.0 * scaling, + {{typePrefix}}Tokens.iconButtonGhostMargin3: -8.0 * scaling, + {{typePrefix}}Tokens.iconButtonGhostMargin4: -12.0 * scaling, + {{typePrefix}}Tokens.cardGhostMargin1: -12.0 * scaling, + {{typePrefix}}Tokens.cardGhostMargin2: -16.0 * scaling, + {{typePrefix}}Tokens.cardGhostMargin3: -24.0 * scaling, + {{typePrefix}}Tokens.cardGhostMargin4: -32.0 * scaling, + {{typePrefix}}Tokens.cardGhostMargin5: -48.0 * scaling, + {{typePrefix}}Tokens.borderWidth1: 1.0, + {{typePrefix}}Tokens.borderWidth2: 2.0, + {{typePrefix}}Tokens.focusRingWidth: 2.0, + {{typePrefix}}Tokens.focusRingOffset: 2.0, + {{typePrefix}}Tokens.text1: TextStyle( + fontSize: 12.0 * scaling, + letterSpacing: 0.0025 * 12.0 * scaling, + height: 16.0 / 12.0, + ), + {{typePrefix}}Tokens.text2: TextStyle( + fontSize: 14.0 * scaling, + letterSpacing: 0.0, + height: 20.0 / 14.0, + ), + {{typePrefix}}Tokens.text3: TextStyle( + fontSize: 16.0 * scaling, + letterSpacing: 0.0, + height: 24.0 / 16.0, + ), + {{typePrefix}}Tokens.accordionText2: TextStyle( + fontSize: 15.0 * scaling, + letterSpacing: 0.0, + height: 20.0 / 15.0, + ), + {{typePrefix}}Tokens.text4: TextStyle( + fontSize: 18.0 * scaling, + letterSpacing: -0.0025 * 18.0 * scaling, + height: 26.0 / 18.0, + ), + {{typePrefix}}Tokens.text5: TextStyle( + fontSize: 20.0 * scaling, + letterSpacing: -0.005 * 20.0 * scaling, + height: 28.0 / 20.0, + ), + {{typePrefix}}Tokens.text6: TextStyle( + fontSize: 24.0 * scaling, + letterSpacing: -0.00625 * 24.0 * scaling, + height: 30.0 / 24.0, + ), + {{typePrefix}}Tokens.text7: TextStyle( + fontSize: 28.0 * scaling, + letterSpacing: -0.0075 * 28.0 * scaling, + height: 36.0 / 28.0, + ), + {{typePrefix}}Tokens.text8: TextStyle( + fontSize: 35.0 * scaling, + letterSpacing: -0.01 * 35.0 * scaling, + height: 40.0 / 35.0, + ), + {{typePrefix}}Tokens.text9: TextStyle( + fontSize: 60.0 * scaling, + letterSpacing: -0.025 * 60.0 * scaling, + height: 1.0, + ), + {{typePrefix}}Tokens.avatarFallback1One: _avatarFallbackText( + fontSize: 14, + letterSpacing: 0.0025 * 12, + scaling: scaling, + ), + {{typePrefix}}Tokens.avatarFallback1Two: _avatarFallbackText( + fontSize: 12, + letterSpacing: 0.0025 * 12, + scaling: scaling, + ), + {{typePrefix}}Tokens.avatarFallback2One: _avatarFallbackText( + fontSize: 16, + letterSpacing: 0, + scaling: scaling, + ), + {{typePrefix}}Tokens.avatarFallback2Two: _avatarFallbackText( + fontSize: 14, + letterSpacing: 0, + scaling: scaling, + ), + {{typePrefix}}Tokens.avatarFallback3One: _avatarFallbackText( + fontSize: 18, + letterSpacing: 0, + scaling: scaling, + ), + {{typePrefix}}Tokens.avatarFallback3Two: _avatarFallbackText( + fontSize: 16, + letterSpacing: 0, + scaling: scaling, + ), + {{typePrefix}}Tokens.avatarFallback4One: _avatarFallbackText( + fontSize: 20, + letterSpacing: -0.0025 * 18, + scaling: scaling, + ), + {{typePrefix}}Tokens.avatarFallback4Two: _avatarFallbackText( + fontSize: 18, + letterSpacing: -0.0025 * 18, + scaling: scaling, + ), + {{typePrefix}}Tokens.avatarFallback5: _avatarFallbackText( + fontSize: 24, + letterSpacing: -0.00625 * 24, + scaling: scaling, + ), + {{typePrefix}}Tokens.avatarFallback6: _avatarFallbackText( + fontSize: 28, + letterSpacing: -0.0075 * 28, + scaling: scaling, + ), + {{typePrefix}}Tokens.avatarFallback7: _avatarFallbackText( + fontSize: 28, + letterSpacing: -0.0075 * 28, + scaling: scaling, + ), + {{typePrefix}}Tokens.avatarFallback8: _avatarFallbackText( + fontSize: 35, + letterSpacing: -0.01 * 35, + scaling: scaling, + ), + {{typePrefix}}Tokens.avatarFallback9: _avatarFallbackText( + fontSize: 60, + letterSpacing: -0.025 * 60, + scaling: scaling, + ), + + // Font weights (token values) + {{typePrefix}}Tokens.fontWeightLight: FontWeight.w300, + {{typePrefix}}Tokens.fontWeightRegular: FontWeight.w400, + {{typePrefix}}Tokens.fontWeightMedium: FontWeight.w500, + // Match Radix Themes font weights (bold = 700) + {{typePrefix}}Tokens.fontWeightBold: FontWeight.w700, + + // Durations (token values) + {{typePrefix}}Tokens.transitionFast: const Duration(milliseconds: 100), + {{typePrefix}}Tokens.transitionSlow: const Duration(milliseconds: 300), + {{typePrefix}}Tokens.skeletonPulseDuration: const Duration(milliseconds: 1000), + }; + + return allTokens; +} + +TextStyle _avatarFallbackText({ + required double fontSize, + required double letterSpacing, + required double scaling, +}) => TextStyle( + fontSize: fontSize * scaling, + letterSpacing: letterSpacing * scaling, + height: 1, +); + +enum _CardShadowLayer { outer, inner } + +enum _CardShadowState { idle, hovered, active } + +List _cardClassicShadows( + {{typePrefix}}ThemeColors colors, { + required bool isDark, + required _CardShadowLayer layer, + required _CardShadowState state, +}) { + final inner = layer == _CardShadowLayer.inner; + final shapeInset = inner ? 1.0 : 0.0; + final border = switch ((isDark, state)) { + (true, _) => mixOklabPremultiplied( + colors.gray.scale.alphaStep(6), + colors.gray.scale.step(6), + 0.25, + ), + (false, _CardShadowState.hovered) => mixOklabPremultiplied( + colors.gray.scale.alphaStep(4), + colors.gray.scale.step(4), + 0.25, + ), + (false, _) => mixOklabPremultiplied( + colors.gray.scale.alphaStep(3), + colors.gray.scale.step(3), + 0.25, + ), + }; + + RemixBoxShadow shadow( + Color color, { + Offset offset = Offset.zero, + double blur = 0, + required double spread, + }) => RemixBoxShadow( + color: color, + offset: offset, + blurRadius: blur, + spreadRadius: spread, + shapeInset: shapeInset, + ); + + if (state == _CardShadowState.hovered) { + if (isDark) { + return [ + shadow(border, spread: inner ? 1 : 0), + shadow(colors.gray.scale.alphaStep(4), blur: 1, spread: inner ? 1 : 0), + shadow( + colors.gray.scale.alphaStep(4), + blur: 1, + spread: inner ? -1 : -2, + ), + shadow( + colors.gray.scale.alphaStep(3), + blur: 3, + spread: inner ? -2 : -3, + ), + shadow( + colors.gray.scale.alphaStep(3), + blur: 12, + spread: inner ? -2 : -3, + ), + shadow( + colors.gray.scale.alphaStep(7), + blur: 16, + spread: inner ? -8 : -9, + ), + ]; + } + return [ + shadow(border, spread: inner ? 1 : 0), + shadow( + colors.blackAlpha[1]!, + offset: const Offset(0, 1), + blur: 1, + spread: inner ? 1 : 0, + ), + shadow( + colors.gray.scale.alphaStep(3), + offset: const Offset(0, 2), + blur: 1, + spread: inner ? -1 : -2, + ), + shadow( + colors.blackAlpha[1]!, + offset: const Offset(0, 2), + blur: 3, + spread: inner ? -2 : -3, + ), + shadow( + colors.gray.scale.alphaStep(3), + offset: const Offset(0, 3), + blur: 12, + spread: inner ? -4 : -5, + ), + shadow( + colors.blackAlpha[1]!, + offset: const Offset(0, 4), + blur: 16, + spread: inner ? -8 : -9, + ), + ]; + } + + final active = state == _CardShadowState.active; + final subtle = isDark ? colors.blackAlpha[3]! : colors.blackAlpha[1]!; + final middle = isDark + ? colors.blackAlpha[6]! + : colors.gray.scale.alphaStep(active ? 4 : 2); + final bottom = isDark ? colors.blackAlpha[5]! : colors.blackAlpha[1]!; + return [ + shadow(border, spread: inner ? 1 : 0), + shadow(const Color(0x00000000), spread: inner ? 1 : 0), + shadow(subtle, spread: inner ? 0.5 : 0), + shadow(middle, offset: const Offset(0, 1), blur: 1, spread: inner ? 0 : -1), + shadow( + isDark ? colors.blackAlpha[6]! : colors.blackAlpha[1]!, + offset: const Offset(0, 2), + blur: 1, + spread: inner ? -1 : -2, + ), + shadow(bottom, offset: const Offset(0, 1), blur: 3, spread: inner ? 0 : -1), + ]; +} + +RemixBoxShadow _insetShadow( + Color color, { + Offset offset = Offset.zero, + double blur = 0, + double spread = 0, + double shapeInset = 0, +}) => RemixBoxShadow( + kind: RemixBoxShadowKind.inset, + color: color, + offset: offset, + blurRadius: blur, + spreadRadius: spread, + shapeInset: shapeInset, +); + +List _selectClassicShadows( + {{typePrefix}}ThemeColors colors, { + required bool isDark, +}) { + if (isDark) { + return [ + _insetShadow(colors.whiteAlpha[4]!, spread: 1), + _insetShadow(colors.whiteAlpha[4]!, offset: const Offset(0, 1), blur: 1), + _insetShadow(colors.blackAlpha[9]!, offset: const Offset(0, -1), blur: 1), + ]; + } + return [ + _insetShadow(colors.gray.scale.alphaStep(5), spread: 1), + _insetShadow(colors.whiteAlpha[11]!, offset: const Offset(0, 2), blur: 1), + _insetShadow( + colors.gray.scale.alphaStep(4), + offset: const Offset(0, -2), + blur: 1, + ), + ]; +} + +List _baseButtonClassicDisabledShadows( + {{typePrefix}}ThemeColors colors, { + required bool isDark, +}) { + if (isDark) { + return [ + _insetShadow(colors.gray.scale.alphaStep(5), spread: 1), + _insetShadow( + colors.gray.scale.alphaStep(2), + offset: const Offset(0, 4), + blur: 2, + spread: -2, + ), + _insetShadow( + colors.gray.scale.alphaStep(5), + offset: const Offset(0, 1), + blur: 1, + ), + _insetShadow(colors.blackAlpha[3]!, offset: const Offset(0, -1), blur: 1), + _insetShadow(colors.gray.scale.alphaStep(2), spread: 1), + ]; + } + return [ + _insetShadow(colors.gray.scale.alphaStep(4), spread: 1), + _insetShadow( + colors.gray.scale.alphaStep(3), + offset: const Offset(0, -2), + blur: 1, + ), + _insetShadow( + colors.whiteAlpha[9]!, + offset: const Offset(0, 4), + blur: 2, + spread: -2, + ), + _insetShadow( + colors.whiteAlpha[9]!, + offset: const Offset(0, 2), + blur: 1, + spread: -1, + ), + ]; +} + +List _baseButtonClassicShadows( + {{typePrefix}}ThemeColors colors, { + required bool isDark, + required bool highContrast, +}) { + final accent = highContrast + ? colors.accent.scale.step(12) + : colors.accent.scale.step(9); + if (isDark) { + return [ + _insetShadow( + colors.whiteAlpha[4]!, + offset: const Offset(0, 2), + blur: 3, + spread: -1, + shapeInset: 1, + ), + _insetShadow(colors.whiteAlpha[2]!, spread: 1), + _insetShadow( + colors.whiteAlpha[3]!, + offset: const Offset(0, 4), + blur: 2, + spread: -2, + ), + _insetShadow(colors.whiteAlpha[6]!, offset: const Offset(0, 1), blur: 1), + _insetShadow(colors.blackAlpha[6]!, offset: const Offset(0, -1), blur: 1), + _insetShadow(accent, spread: 1), + ]; + } + return [ + _insetShadow( + colors.whiteAlpha[4]!, + offset: const Offset(0, 2), + blur: 3, + spread: -1, + shapeInset: 2, + ), + _insetShadow(colors.gray.scale.alphaStep(4), spread: 1), + _insetShadow( + colors.gray.scale.alphaStep(3), + offset: const Offset(0, -2), + blur: 1, + ), + _insetShadow(accent, spread: 1), + _insetShadow( + colors.whiteAlpha[9]!, + offset: const Offset(0, 4), + blur: 2, + spread: -2, + ), + _insetShadow( + colors.whiteAlpha[9]!, + offset: const Offset(0, 2), + blur: 1, + spread: -1, + ), + ]; +} + +List _baseButtonClassicActiveShadows( + {{typePrefix}}ThemeColors colors, { + required bool highContrast, +}) { + final accent = highContrast + ? colors.accent.scale.step(12) + : colors.accent.scale.step(9); + return [ + _insetShadow( + colors.gray.scale.alphaStep(4), + offset: const Offset(0, 4), + blur: 2, + spread: -2, + ), + _insetShadow( + colors.gray.scale.alphaStep(7), + offset: const Offset(0, 1), + blur: 1, + ), + _insetShadow(colors.gray.scale.alphaStep(5), spread: 1), + _insetShadow(accent, spread: 1), + _insetShadow( + colors.gray.scale.alphaStep(3), + offset: const Offset(0, 3), + blur: 2, + ), + _insetShadow(colors.whiteAlpha[7]!, spread: 1), + _insetShadow(colors.whiteAlpha[5]!, offset: const Offset(0, -2), blur: 1), + ]; +} + +Map _radiusTokensFor({{typePrefix}}Radius radius, double scaling) { + final factor = _radiusFactor(radius); + final thumb = switch (radius) { + .none || .small => const Radius.circular(0.5), + .medium || .large || .full => const Radius.circular(9999.0), + }; + Radius scaled(double base) => Radius.circular(base * factor * scaling); + Radius larger(Radius first, Radius second) => Radius.elliptical( + first.x > second.x ? first.x : second.x, + first.y > second.y ? first.y : second.y, + ); + final radius1 = scaled(3.0); + final radius2 = scaled(4.0); + final radius3 = scaled(6.0); + final radius4 = scaled(8.0); + final radius5 = scaled(12.0); + final radius6 = scaled(16.0); + final full = radius == .full ? const Radius.circular(9999.0) : Radius.zero; + Radius progressRadius(double height) { + final thumbBase = switch (radius) { + .none || .small => 0.5, + .medium || .large || .full => 9999.0, + }; + return Radius.circular(math.max(factor * height / 3, factor * thumbBase)); + } + + return { + {{typePrefix}}Tokens.radius1: radius1, + {{typePrefix}}Tokens.radius2: radius2, + {{typePrefix}}Tokens.radius3: radius3, + {{typePrefix}}Tokens.radius4: radius4, + {{typePrefix}}Tokens.radius5: radius5, + {{typePrefix}}Tokens.radius6: radius6, + {{typePrefix}}Tokens.radiusFull: full, + {{typePrefix}}Tokens.radiusThumb: thumb, + {{typePrefix}}Tokens.radiusCircle: const Radius.circular(9999.0), + {{typePrefix}}Tokens.radius1OrFull: larger(radius1, full), + {{typePrefix}}Tokens.radius2OrFull: larger(radius2, full), + {{typePrefix}}Tokens.radius3OrFull: larger(radius3, full), + {{typePrefix}}Tokens.radius4OrFull: larger(radius4, full), + {{typePrefix}}Tokens.radius5OrFull: larger(radius5, full), + {{typePrefix}}Tokens.radius6OrFull: larger(radius6, full), + {{typePrefix}}Tokens.radius1OrThumb: larger(radius1, thumb), + {{typePrefix}}Tokens.radius2OrThumb: larger(radius2, thumb), + {{typePrefix}}Tokens.progressRadius1: progressRadius(4.0 * scaling), + {{typePrefix}}Tokens.progressRadius2: progressRadius(6.0 * scaling), + {{typePrefix}}Tokens.progressRadius3: progressRadius(8.0 * scaling), + {{typePrefix}}Tokens.sliderTrackRadius1: progressRadius(6.0 * scaling), + {{typePrefix}}Tokens.sliderTrackRadius2: progressRadius(8.0 * scaling), + {{typePrefix}}Tokens.sliderTrackRadius3: progressRadius(10.0 * scaling), + }; +} + +List _scaleShadowOpacity( + List shadows, + double factor, +) => [ + for (final shadow in shadows) + RemixBoxShadow( + kind: shadow.kind, + color: shadow.color.withValues(alpha: shadow.color.a * factor), + offset: shadow.offset, + blurRadius: shadow.blurRadius, + spreadRadius: shadow.spreadRadius, + shapeInset: shadow.shapeInset, + ), +]; + +double _radiusFactor({{typePrefix}}Radius radius) => switch (radius) { + .none => 0.0, + .small => 0.75, + .medium => 1.0, + .large || .full => 1.5, +}; + +Radius _scaledRadiusToken({{typePrefix}}Radius radius, double scaling, double base) => + Radius.circular(base * scaling * _radiusFactor(radius)); + +Map _accentColorTokens({{typePrefix}}ThemeColors tokens) { + final scale = tokens.accent.scale; + + return { + {{typePrefix}}Tokens.accentSurface: tokens.accent.surface, + {{typePrefix}}Tokens.accentIndicator: tokens.accent.indicator, + {{typePrefix}}Tokens.accentTrack: tokens.accent.track, + {{typePrefix}}Tokens.accentContrast: tokens.accent.contrast, + {{typePrefix}}Tokens.focus8: tokens.focus8, + {{typePrefix}}Tokens.focusA5: tokens.focusA5, + {{typePrefix}}Tokens.focusA8: tokens.focusA8, + {{typePrefix}}Tokens.accent1: scale.step(1), + {{typePrefix}}Tokens.accent2: scale.step(2), + {{typePrefix}}Tokens.accent3: scale.step(3), + {{typePrefix}}Tokens.accent4: scale.step(4), + {{typePrefix}}Tokens.accent5: scale.step(5), + {{typePrefix}}Tokens.accent6: scale.step(6), + {{typePrefix}}Tokens.accent7: scale.step(7), + {{typePrefix}}Tokens.accent8: scale.step(8), + {{typePrefix}}Tokens.accent9: scale.step(9), + {{typePrefix}}Tokens.accent10: scale.step(10), + {{typePrefix}}Tokens.accent11: scale.step(11), + {{typePrefix}}Tokens.accent12: scale.step(12), + {{typePrefix}}Tokens.accentA1: scale.alphaStep(1), + {{typePrefix}}Tokens.accentA2: scale.alphaStep(2), + {{typePrefix}}Tokens.accentA3: scale.alphaStep(3), + {{typePrefix}}Tokens.accentA4: scale.alphaStep(4), + {{typePrefix}}Tokens.accentA5: scale.alphaStep(5), + {{typePrefix}}Tokens.accentA6: scale.alphaStep(6), + {{typePrefix}}Tokens.accentA7: scale.alphaStep(7), + {{typePrefix}}Tokens.accentA8: scale.alphaStep(8), + {{typePrefix}}Tokens.accentA9: scale.alphaStep(9), + {{typePrefix}}Tokens.accentA10: scale.alphaStep(10), + {{typePrefix}}Tokens.accentA11: scale.alphaStep(11), + {{typePrefix}}Tokens.accentA12: scale.alphaStep(12), + }; +} diff --git a/packages/remix_cli/lib/src/registry/fortal/templates/theme/theme_scope.dart.tmpl b/packages/remix_cli/lib/src/registry/fortal/templates/theme/theme_scope.dart.tmpl new file mode 100644 index 000000000..6b6834a0c --- /dev/null +++ b/packages/remix_cli/lib/src/registry/fortal/templates/theme/theme_scope.dart.tmpl @@ -0,0 +1,188 @@ +import 'package:flutter/widgets.dart'; +import 'package:remix/remix.dart'; + +import 'theme_data.dart'; +import 'tokens.dart'; + +/// Establishes a courtesy default text run for bare [Text] descendants. +/// +/// `.radix-themes` is not only a token carrier upstream: `color.css` sets +/// `color: var(--gray-12)` in the same rule as the `data-has-background` fill, +/// and `typography.css` pins the root to `--default-font-size` +/// (`--font-size-3`), `--default-line-height`, `--default-letter-spacing`, and +/// `--default-font-weight`. Those resolve to exactly [{{typePrefix}}Tokens.text3] plus +/// [{{typePrefix}}Tokens.gray12] at regular weight. +/// +/// {{typePrefix}} text recipes resolve and pin their own runs. This fallback keeps +/// deliberately bare [Text] descendants aligned with Radix's root typography +/// and neutral foreground. A nearer descendant `DefaultTextStyle` still wins +/// through Flutter's normal inheritance. +/// +/// Only the outermost [{{typePrefix}}Scope] installs this. A nested scope re-scopes +/// tokens for its subtree and nothing more: upstream, `.radix-themes` inside +/// another `.radix-themes` still inherits `color` and the font properties from +/// its parent chain, and a nested scope that reinstalled the root run here +/// would silently replace whatever `DefaultTextStyle` the subtree sits in. +/// +/// The font family is deliberately left unset. Radix's `--default-font-family` +/// is the platform system stack, and a null family is Flutter's equivalent; +/// naming a concrete family here would pin every consumer to one typeface. +Widget _{{valuePrefix}}RootTextStyle({ + required Map, Object> tokens, + required Widget child, +}) { + final root = tokens[{{typePrefix}}Tokens.text3]! as TextStyle; + + return DefaultTextStyle( + style: root.copyWith( + color: tokens[{{typePrefix}}Tokens.gray12]! as Color, + fontWeight: tokens[{{typePrefix}}Tokens.fontWeightRegular]! as FontWeight, + ), + child: child, + ); +} + +/// Widget that provides {{typePrefix}} design tokens to its subtree via [MixScope]. +/// +/// Use [{{typePrefix}}Scope] at the root of your app (or around any subtree that uses +/// {{typePrefix}} styles) so that [{{typePrefix}}Tokens] resolve to actual values. +/// +/// ```dart +/// {{typePrefix}}Scope( +/// child: WidgetsApp(...), +/// ) +/// ``` +class {{typePrefix}}Scope extends StatelessWidget { + const {{typePrefix}}Scope({ + super.key, + this.accent, + this.gray, + this.brightness, + this.panelBackground, + this.radius, + this.scaling, + this.hasBackground, + this.orderOfModifiers, + required this.child, + }); + + final {{typePrefix}}AccentColor? accent; + final {{typePrefix}}GrayColor? gray; + final Brightness? brightness; + final {{typePrefix}}PanelBackground? panelBackground; + final {{typePrefix}}Radius? radius; + final {{typePrefix}}Scaling? scaling; + final bool? hasBackground; + final List? orderOfModifiers; + final Widget child; + + @override + Widget build(BuildContext context) { + final config = {{typePrefix}}ThemeConfig( + accent: accent, + gray: gray, + brightness: brightness, + panelBackground: panelBackground, + radius: radius, + scaling: scaling, + hasBackground: hasBackground, + ); + final parent = {{typePrefix}}Theme.maybeOf(context); + final data = _resolve{{typePrefix}}Theme(config, parent: parent); + final tokens = build{{typePrefix}}ScopeTokens(data); + Widget result = MixScope( + tokens: tokens, + orderOfModifiers: orderOfModifiers, + // Theme-root identity, not `hasBackground`, decides who owns the text + // run: a scope nested for its accent or scaling must leave the current + // run alone, while a root scope with `hasBackground: false` still + // establishes it. + child: parent == null + ? _{{valuePrefix}}RootTextStyle(tokens: tokens, child: child) + : child, + ); + if (data.hasBackground) { + result = ColoredBox( + color: tokens[{{typePrefix}}Tokens.colorBackground]! as Color, + child: result, + ); + } + + return {{typePrefix}}Theme( + data: data, + orderOfModifiers: orderOfModifiers, + child: result, + ); + } +} + +{{typePrefix}}ThemeData _resolve{{typePrefix}}Theme( + {{typePrefix}}ThemeConfig config, { + {{typePrefix}}ThemeData? parent, +}) { + final accent = config.accent ?? parent?.accent ?? {{typePrefix}}AccentColor.indigo; + + return {{typePrefix}}ThemeData( + accent: accent, + gray: config.gray ?? parent?.gray ?? {{typePrefix}}GrayColor.slate, + brightness: config.brightness ?? parent?.brightness ?? Brightness.light, + panelBackground: + config.panelBackground ?? + parent?.panelBackground ?? + {{typePrefix}}PanelBackground.translucent, + radius: config.radius ?? parent?.radius ?? {{typePrefix}}Radius.medium, + scaling: config.scaling ?? parent?.scaling ?? {{typePrefix}}Scaling.percent100, + hasBackground: config.hasBackground ?? parent == null, + ); +} + +/// Makes the active [{{typePrefix}}ThemeData] available to descendants. +class {{typePrefix}}Theme extends InheritedTheme { + const {{typePrefix}}Theme({ + super.key, + required this.data, + this.orderOfModifiers, + required super.child, + }); + + final {{typePrefix}}ThemeData data; + final List? orderOfModifiers; + + /// Returns the closest resolved {{typePrefix}} theme. + static {{typePrefix}}ThemeData of(BuildContext context) { + final data = maybeOf(context); + if (data != null) return data; + throw FlutterError.fromParts([ + ErrorSummary('No {{typePrefix}}Theme found.'), + ErrorDescription( + '${context.widget.runtimeType} tried to read the {{typePrefix}} theme, but no {{typePrefix}}Scope was found above it.', + ), + context.describeElement('The context used was'), + ]); + } + + /// Returns the closest resolved {{typePrefix}} theme, if one is available. + static {{typePrefix}}ThemeData? maybeOf(BuildContext context) => + context.dependOnInheritedWidgetOfExactType<{{typePrefix}}Theme>()?.data; + + /// Rebuilds only the theme and its Mix tokens. + /// + /// The captured subtree's text run is *not* synthesized here. + /// `DefaultTextStyle` is itself an [InheritedTheme], so + /// `InheritedTheme.capture` already carries the actual nearest ambient run + /// across to the new route; installing the Radix root run alongside it would + /// overwrite that capture with a value the source context never had. + @override + Widget wrap(BuildContext context, Widget child) => {{typePrefix}}Theme( + data: data, + orderOfModifiers: orderOfModifiers, + child: MixScope( + tokens: build{{typePrefix}}ScopeTokens(data), + orderOfModifiers: orderOfModifiers, + child: child, + ), + ); + + @override + bool updateShouldNotify({{typePrefix}}Theme oldWidget) => data != oldWidget.data; +} diff --git a/packages/remix_cli/lib/src/registry/fortal/templates/theme/tokens.dart.tmpl b/packages/remix_cli/lib/src/registry/fortal/templates/theme/tokens.dart.tmpl new file mode 100644 index 000000000..1a2278c0b --- /dev/null +++ b/packages/remix_cli/lib/src/registry/fortal/templates/theme/tokens.dart.tmpl @@ -0,0 +1,943 @@ +import 'package:remix/remix.dart'; + +import 'theme_scope.dart' show {{typePrefix}}Scope; + +/// Design tokens for the {{typePrefix}} UI system (powered by Radix Colors). +/// +/// Provides color scales (12-step accent/gray), spacing (9-step), radius (6-step), +/// shadows (6-level), typography (9-size), and functional colors. +/// +/// Example: +/// ```dart +/// Style( +/// $box.color.ref({{typePrefix}}Tokens.accent9), +/// $text.style.ref({{typePrefix}}Tokens.text3), +/// $box.padding.ref({{typePrefix}}Tokens.space4), +/// ) +/// ``` +/// +/// Must be used within [{{typePrefix}}Scope] to resolve actual values. +class {{typePrefix}}Tokens { + // ============================================================================ + // BACKGROUND AND SURFACE COLORS + // ============================================================================ + + /// Page background color selected for the active brightness. + static const colorBackground = ColorToken('{{valuePrefix}}.color.background'); + + /// Neutral surface color for input fields and controls. + static const colorSurface = ColorToken('{{valuePrefix}}.color.surface'); + + /// Selected SegmentedControl surface for the active brightness. + static const segmentedControlIndicatorBackground = ColorToken( + '{{valuePrefix}}.segmented-control.indicator-background', + ); + + /// Solid panel background selected for the active brightness. + static const colorPanelSolid = ColorToken('{{valuePrefix}}.color.panel.solid'); + + /// Translucent panel background with alpha transparency. + static const colorPanelTranslucent = ColorToken( + '{{valuePrefix}}.color.panel.translucent', + ); + + /// Panel background selected by [{{typePrefix}}PanelBackground]. + static const colorPanel = ColorToken('{{valuePrefix}}.color.panel'); + + /// Backdrop blur applied to translucent floating panels. + static const panelBlur = DoubleToken('{{valuePrefix}}.panel.blur'); + + /// Dark overlay for modals and dialogs. + static const colorOverlay = ColorToken('{{valuePrefix}}.color.overlay'); + + // ============================================================================ + // FUNCTIONAL ACCENT COLORS + // ============================================================================ + + /// Subtle accent surface for soft button variants and chips. + static const accentSurface = ColorToken('{{valuePrefix}}.accent.surface'); + + /// Active indicator color for progress bars and sliders. + static const accentIndicator = ColorToken('{{valuePrefix}}.accent.indicator'); + + /// Track/rail background color for sliders and progress bars. + static const accentTrack = ColorToken('{{valuePrefix}}.accent.track'); + + /// Mode-aware overlay used by high-contrast slider ranges. + static const sliderHighContrastOverlay = ColorToken( + '{{valuePrefix}}.slider.high-contrast-overlay', + ); + + /// High contrast foreground for solid accent backgrounds. + static const accentContrast = ColorToken('{{valuePrefix}}.accent.contrast'); + + // ============================================================================ + // FOCUS AND INTERACTION STATES + // ============================================================================ + + /// Solid focus ring color (accent step 8). + static const focus8 = ColorToken('{{valuePrefix}}.focus.8'); + + /// Translucent text-selection color (accent alpha step 5). + static const focusA5 = ColorToken('{{valuePrefix}}.focus.a5'); + + /// Translucent focus ring color with alpha transparency. + static const focusA8 = ColorToken('{{valuePrefix}}.focus.a8'); + + /// Mode-aware red roles used by documented validation extensions. + static const error3 = ColorToken('{{valuePrefix}}.error.3'); + static const error7 = ColorToken('{{valuePrefix}}.error.7'); + static const error8 = ColorToken('{{valuePrefix}}.error.8'); + static const error9 = ColorToken('{{valuePrefix}}.error.9'); + static const error11 = ColorToken('{{valuePrefix}}.error.11'); + static const error12 = ColorToken('{{valuePrefix}}.error.12'); + static const errorA7 = ColorToken('{{valuePrefix}}.error.a7'); + + // ============================================================================ + // ACCENT COLOR SCALE (12 STEPS) + // ============================================================================ + // + // {{typePrefix}} uses a 12-step color scale (inherited from Radix Themes) that provides semantic meaning: + // + // Steps 1-2: App backgrounds (subtle → more visible) + // Steps 3-5: Component backgrounds (rest → hover → active) + // Steps 6-8: Borders (subtle → component → hover) + // Steps 9-10: Solid backgrounds (default → hover) + // Steps 11-12: Text (low contrast → high contrast) + // + + /// Accent step 1 - App background, most subtle. + static const accent1 = ColorToken('{{valuePrefix}}.accent.1'); + + /// Accent step 2 - Subtle background. + static const accent2 = ColorToken('{{valuePrefix}}.accent.2'); + + /// Accent step 3 - Component background at rest. + static const accent3 = ColorToken('{{valuePrefix}}.accent.3'); + + /// Accent step 4 - Component background on hover. + static const accent4 = ColorToken('{{valuePrefix}}.accent.4'); + + /// Accent step 5 - Component background when active/pressed. + static const accent5 = ColorToken('{{valuePrefix}}.accent.5'); + + /// Accent step 6 - Subtle borders and separators. + static const accent6 = ColorToken('{{valuePrefix}}.accent.6'); + + /// Accent step 7 - Component borders at rest. + static const accent7 = ColorToken('{{valuePrefix}}.accent.7'); + + /// Accent step 8 - Component borders on hover and focus. + static const accent8 = ColorToken('{{valuePrefix}}.accent.8'); + + /// Accent step 9 - Primary solid background. + static const accent9 = ColorToken('{{valuePrefix}}.accent.9'); + + /// Accent step 10 - Solid background on hover. + static const accent10 = ColorToken('{{valuePrefix}}.accent.10'); + + /// Accent step 11 - Low contrast text. + static const accent11 = ColorToken('{{valuePrefix}}.accent.11'); + + /// Accent step 12 - High contrast text. + static const accent12 = ColorToken('{{valuePrefix}}.accent.12'); + + // ============================================================================ + // GRAY COLOR SCALE (12 STEPS) + // ============================================================================ + // + // The gray scale follows the same 12-step semantic structure as accent colors, + // but provides neutral colors for text, borders, and backgrounds. + // The specific gray variant (slate, mauve, sage, etc.) is chosen in the theme. + // + + /// Gray step 1 - Page background. + static const gray1 = ColorToken('{{valuePrefix}}.gray.1'); + + /// Gray step 2 - Panel and card backgrounds. + static const gray2 = ColorToken('{{valuePrefix}}.gray.2'); + + /// Gray step 3 - Input backgrounds and pressed states. + static const gray3 = ColorToken('{{valuePrefix}}.gray.3'); + + /// Gray step 4 - Input backgrounds on hover. + static const gray4 = ColorToken('{{valuePrefix}}.gray.4'); + + /// Gray step 5 - Active states and disabled backgrounds. + static const gray5 = ColorToken('{{valuePrefix}}.gray.5'); + + /// Gray step 6 - Subtle borders and dividers. + static const gray6 = ColorToken('{{valuePrefix}}.gray.6'); + + /// Gray step 7 - Standard borders and outlines. + /// + /// Primary border color for form inputs, cards, + /// and component boundaries. + static const gray7 = ColorToken('{{valuePrefix}}.gray.7'); + + /// Gray step 8 - Borders on hover and focus. + /// + /// Interactive border states and stronger separators + /// that need more visual weight. + static const gray8 = ColorToken('{{valuePrefix}}.gray.8'); + + /// Gray step 9 - Solid neutral backgrounds. + /// + /// For neutral buttons, badges, and other elements + /// that need a solid background without accent color. + static const gray9 = ColorToken('{{valuePrefix}}.gray.9'); + + /// Gray step 10 - Solid neutral backgrounds on hover. + /// + /// Hover state for neutral solid backgrounds, + /// providing interactive feedback. + static const gray10 = ColorToken('{{valuePrefix}}.gray.10'); + + /// Gray step 11 - Low contrast text and secondary content. + /// + /// For secondary text, placeholders, and content that should + /// be readable but not prominent. + static const gray11 = ColorToken('{{valuePrefix}}.gray.11'); + + /// Gray step 12 - High contrast text and primary content. + /// + /// Primary text color for body content, headings, and any text + /// that needs maximum readability and prominence. + static const gray12 = ColorToken('{{valuePrefix}}.gray.12'); + + // ============================================================================ + // GRAY ROLE TOKENS (parity with generated JSON roles) + // ============================================================================ + /// Neutral surface baseline for the selected gray scale (matches JSON surface) + static const graySurface = ColorToken('{{valuePrefix}}.gray.surface'); + + /// Neutral indicator color (typically gray step 9) + static const grayIndicator = ColorToken('{{valuePrefix}}.gray.indicator'); + + /// Neutral track color (typically gray step 9) + static const grayTrack = ColorToken('{{valuePrefix}}.gray.track'); + + /// Contrast color for content over neutral solid backgrounds (white) + static const grayContrast = ColorToken('{{valuePrefix}}.gray.contrast'); + + // ============================================================================ + // ALPHA VARIANTS (FULL 12-STEP FOR ACCENT AND GRAY) + // ============================================================================ + + // Accent alpha steps a1..a12 + static const accentA1 = ColorToken('{{valuePrefix}}.accent.a1'); + static const accentA2 = ColorToken('{{valuePrefix}}.accent.a2'); + static const accentA3 = ColorToken('{{valuePrefix}}.accent.a3'); + static const accentA4 = ColorToken('{{valuePrefix}}.accent.a4'); + static const accentA5 = ColorToken('{{valuePrefix}}.accent.a5'); + static const accentA6 = ColorToken('{{valuePrefix}}.accent.a6'); + static const accentA7 = ColorToken('{{valuePrefix}}.accent.a7'); + static const accentA8 = ColorToken('{{valuePrefix}}.accent.a8'); + static const accentA9 = ColorToken('{{valuePrefix}}.accent.a9'); + static const accentA10 = ColorToken('{{valuePrefix}}.accent.a10'); + static const accentA11 = ColorToken('{{valuePrefix}}.accent.a11'); + static const accentA12 = ColorToken('{{valuePrefix}}.accent.a12'); + + // Gray alpha steps a1..a12 + static const grayA1 = ColorToken('{{valuePrefix}}.gray.a1'); + static const grayA2 = ColorToken('{{valuePrefix}}.gray.a2'); + static const grayA3 = ColorToken('{{valuePrefix}}.gray.a3'); + static const grayA4 = ColorToken('{{valuePrefix}}.gray.a4'); + static const grayA5 = ColorToken('{{valuePrefix}}.gray.a5'); + static const grayA6 = ColorToken('{{valuePrefix}}.gray.a6'); + static const grayA7 = ColorToken('{{valuePrefix}}.gray.a7'); + static const grayA8 = ColorToken('{{valuePrefix}}.gray.a8'); + static const grayA9 = ColorToken('{{valuePrefix}}.gray.a9'); + static const grayA10 = ColorToken('{{valuePrefix}}.gray.a10'); + static const grayA11 = ColorToken('{{valuePrefix}}.gray.a11'); + static const grayA12 = ColorToken('{{valuePrefix}}.gray.a12'); + + // ============================================================================ + // NEUTRALS FOR SHADOWS (HELPER TOKENS) + // ============================================================================ + /// Gray alpha steps are declared above (grayA1..grayA12). + + /// Black alpha steps used in layered shadows. + static const blackA1 = ColorToken('{{valuePrefix}}.black.a1'); + static const blackA2 = ColorToken('{{valuePrefix}}.black.a2'); + static const blackA3 = ColorToken('{{valuePrefix}}.black.a3'); + static const blackA4 = ColorToken('{{valuePrefix}}.black.a4'); + static const blackA5 = ColorToken('{{valuePrefix}}.black.a5'); + static const blackA6 = ColorToken('{{valuePrefix}}.black.a6'); + static const blackA7 = ColorToken('{{valuePrefix}}.black.a7'); + static const blackA8 = ColorToken('{{valuePrefix}}.black.a8'); + static const blackA9 = ColorToken('{{valuePrefix}}.black.a9'); + static const blackA10 = ColorToken('{{valuePrefix}}.black.a10'); + static const blackA11 = ColorToken('{{valuePrefix}}.black.a11'); + static const blackA12 = ColorToken('{{valuePrefix}}.black.a12'); + + /// White alpha steps used by layered classic-control recipes. + static const whiteA1 = ColorToken('{{valuePrefix}}.white.a1'); + static const whiteA2 = ColorToken('{{valuePrefix}}.white.a2'); + static const whiteA3 = ColorToken('{{valuePrefix}}.white.a3'); + static const whiteA4 = ColorToken('{{valuePrefix}}.white.a4'); + static const whiteA5 = ColorToken('{{valuePrefix}}.white.a5'); + static const whiteA6 = ColorToken('{{valuePrefix}}.white.a6'); + static const whiteA7 = ColorToken('{{valuePrefix}}.white.a7'); + static const whiteA8 = ColorToken('{{valuePrefix}}.white.a8'); + static const whiteA9 = ColorToken('{{valuePrefix}}.white.a9'); + static const whiteA10 = ColorToken('{{valuePrefix}}.white.a10'); + static const whiteA11 = ColorToken('{{valuePrefix}}.white.a11'); + static const whiteA12 = ColorToken('{{valuePrefix}}.white.a12'); + + /// Mode-aware mixed shadow stroke. + static const shadowStroke = ColorToken('{{valuePrefix}}.shadow.stroke'); + + /// Premultiplied OKLab mixes used by Radix neutral one-pixel strokes. + static const grayStroke3 = ColorToken('{{valuePrefix}}.gray.stroke.3'); + static const grayStroke4 = ColorToken('{{valuePrefix}}.gray.stroke.4'); + static const grayStroke5 = ColorToken('{{valuePrefix}}.gray.stroke.5'); + static const grayStroke6 = ColorToken('{{valuePrefix}}.gray.stroke.6'); + static const grayStroke7 = ColorToken('{{valuePrefix}}.gray.stroke.7'); + + // ============================================================================ + // SPACING SCALE (9 STEPS) + // ============================================================================ + // + // A consistent spacing scale based on 4px increments. + // + + /// Space step 1 - 4px. + /// + /// Smallest spacing for tight layouts, borders, + /// and fine-grained adjustments. + static const space1 = SpaceToken('{{valuePrefix}}.space.1'); + + /// Space step 2 - 8px. + /// + /// Small spacing for component padding and margins. + /// Good for button padding and form element spacing. + static const space2 = SpaceToken('{{valuePrefix}}.space.2'); + + /// Space step 3 - 12px. + /// + /// Medium-small spacing for comfortable padding + /// and moderate element separation. + static const space3 = SpaceToken('{{valuePrefix}}.space.3'); + + /// Space step 4 - 16px. + /// + /// Standard spacing for most layouts. Good default + /// for card padding and section margins. + static const space4 = SpaceToken('{{valuePrefix}}.space.4'); + + /// Space step 5 - 24px. + /// + /// Medium spacing for generous padding and + /// comfortable separation between sections. + static const space5 = SpaceToken('{{valuePrefix}}.space.5'); + + /// Space step 6 - 32px. + /// + /// Large spacing for significant visual separation + /// and generous component padding. + static const space6 = SpaceToken('{{valuePrefix}}.space.6'); + + /// Space step 7 - 40px. + /// + /// Extra large spacing for major layout sections + /// and prominent visual separation. + static const space7 = SpaceToken('{{valuePrefix}}.space.7'); + + /// Space step 8 - 48px. + /// + /// Very large spacing for significant page sections + /// and major layout boundaries. + static const space8 = SpaceToken('{{valuePrefix}}.space.8'); + + /// Space step 9 - 64px. + /// + /// Maximum spacing for major page sections + /// and substantial layout separation. + static const space9 = SpaceToken('{{valuePrefix}}.space.9'); + + /// Spinner size 3 - 20px at 100% scaling. + /// + /// Radix defines this as 1.25 times space 4, so it needs its own resolved + /// token rather than arithmetic on an unresolved token reference. + static const spinnerSize3 = DoubleToken('{{valuePrefix}}.spinner.size.3'); + + /// Compact gap shared by size-1 toggle extensions (2px at 100% scaling). + static const toggleGap1 = DoubleToken('{{valuePrefix}}.toggle.gap.1'); + + /// Comfortable gap shared by size-3 toggle extensions (6px at 100%). + static const toggleGap3 = DoubleToken('{{valuePrefix}}.toggle.gap.3'); + + /// Avatar sizes expressed as scaled pixels rather than spacing steps. + static const avatarSize6 = DoubleToken('{{valuePrefix}}.avatar.size.6'); + static const avatarSize7 = DoubleToken('{{valuePrefix}}.avatar.size.7'); + static const avatarSize8 = DoubleToken('{{valuePrefix}}.avatar.size.8'); + static const avatarSize9 = DoubleToken('{{valuePrefix}}.avatar.size.9'); + + /// Avatar icon sizes are half of each resolved avatar dimension. + /// + /// These values need dedicated tokens because arithmetic on an unresolved + /// token reference would destroy its identity before Mix can resolve it. + static const avatarIconSize1 = DoubleToken('{{valuePrefix}}.avatar.icon-size.1'); + static const avatarIconSize2 = DoubleToken('{{valuePrefix}}.avatar.icon-size.2'); + static const avatarIconSize3 = DoubleToken('{{valuePrefix}}.avatar.icon-size.3'); + static const avatarIconSize4 = DoubleToken('{{valuePrefix}}.avatar.icon-size.4'); + static const avatarIconSize5 = DoubleToken('{{valuePrefix}}.avatar.icon-size.5'); + static const avatarIconSize6 = DoubleToken('{{valuePrefix}}.avatar.icon-size.6'); + static const avatarIconSize7 = DoubleToken('{{valuePrefix}}.avatar.icon-size.7'); + static const avatarIconSize8 = DoubleToken('{{valuePrefix}}.avatar.icon-size.8'); + static const avatarIconSize9 = DoubleToken('{{valuePrefix}}.avatar.icon-size.9'); + + /// Badge measurements that are fractional spacing expressions upstream. + static const badgePaddingX1 = DoubleToken('{{valuePrefix}}.badge.padding-x.1'); + static const badgePaddingY1 = DoubleToken('{{valuePrefix}}.badge.padding-y.1'); + static const badgePaddingX3 = DoubleToken('{{valuePrefix}}.badge.padding-x.3'); + + /// Checkbox dimensions expressed as scaled pixels by Radix Themes. + static const checkboxSize1 = DoubleToken('{{valuePrefix}}.checkbox.size.1'); + static const checkboxSize3 = DoubleToken('{{valuePrefix}}.checkbox.size.3'); + static const checkboxIndicatorSize1 = DoubleToken( + '{{valuePrefix}}.checkbox.indicator-size.1', + ); + static const checkboxIndicatorSize2 = DoubleToken( + '{{valuePrefix}}.checkbox.indicator-size.2', + ); + static const checkboxIndicatorSize3 = DoubleToken( + '{{valuePrefix}}.checkbox.indicator-size.3', + ); + + /// Checkbox-group label gaps derived from Radix's size-linked `0.5em`. + static const checkboxGroupItemGap1 = DoubleToken( + '{{valuePrefix}}.checkbox-group.item-gap.1', + ); + static const checkboxGroupItemGap2 = DoubleToken( + '{{valuePrefix}}.checkbox-group.item-gap.2', + ); + static const checkboxGroupItemGap3 = DoubleToken( + '{{valuePrefix}}.checkbox-group.item-gap.3', + ); + + /// Radio indicators are 40% of their control size in Radix Themes. + /// + /// These values need dedicated tokens because arithmetic on an unresolved + /// token reference would destroy its identity before Mix can resolve it. + static const radioIndicatorSize1 = DoubleToken( + '{{valuePrefix}}.radio.indicator-size.1', + ); + static const radioIndicatorSize2 = DoubleToken( + '{{valuePrefix}}.radio.indicator-size.2', + ); + static const radioIndicatorSize3 = DoubleToken( + '{{valuePrefix}}.radio.indicator-size.3', + ); + + /// Checkbox radii derived from fractional radius-step expressions. + static const checkboxRadius1 = RadiusToken('{{valuePrefix}}.checkbox.radius.1'); + static const checkboxRadius3 = RadiusToken('{{valuePrefix}}.checkbox.radius.3'); + + /// Switch geometry that cannot be derived from unresolved token references. + static const switchHeight2 = DoubleToken('{{valuePrefix}}.switch.height.2'); + static const switchWidth1 = DoubleToken('{{valuePrefix}}.switch.width.1'); + static const switchWidth2 = DoubleToken('{{valuePrefix}}.switch.width.2'); + static const switchWidth3 = DoubleToken('{{valuePrefix}}.switch.width.3'); + static const switchThumbSize1 = DoubleToken('{{valuePrefix}}.switch.thumb-size.1'); + static const switchThumbSize2 = DoubleToken('{{valuePrefix}}.switch.thumb-size.2'); + static const switchThumbSize3 = DoubleToken('{{valuePrefix}}.switch.thumb-size.3'); + + /// Progress geometry derived from scaled fractional upstream expressions. + static const progressHeight2 = DoubleToken('{{valuePrefix}}.progress.height.2'); + static const progressRadius1 = RadiusToken('{{valuePrefix}}.progress.radius.1'); + static const progressRadius2 = RadiusToken('{{valuePrefix}}.progress.radius.2'); + static const progressRadius3 = RadiusToken('{{valuePrefix}}.progress.radius.3'); + + /// Slider geometry expressed as scaled Radix component dimensions. + static const sliderTrackSize1 = DoubleToken('{{valuePrefix}}.slider.track-size.1'); + static const sliderTrackSize2 = DoubleToken('{{valuePrefix}}.slider.track-size.2'); + static const sliderTrackSize3 = DoubleToken('{{valuePrefix}}.slider.track-size.3'); + static const sliderThumbSize1 = DoubleToken('{{valuePrefix}}.slider.thumb-size.1'); + static const sliderThumbSize2 = DoubleToken('{{valuePrefix}}.slider.thumb-size.2'); + static const sliderThumbSize3 = DoubleToken('{{valuePrefix}}.slider.thumb-size.3'); + static const sliderTrackRadius1 = RadiusToken('{{valuePrefix}}.slider.track-radius.1'); + static const sliderTrackRadius2 = RadiusToken('{{valuePrefix}}.slider.track-radius.2'); + static const sliderTrackRadius3 = RadiusToken('{{valuePrefix}}.slider.track-radius.3'); + + /// TextField content insets after its fixed one-pixel border. + static const textFieldPadding1 = DoubleToken('{{valuePrefix}}.text-field.padding.1'); + static const textFieldPadding2 = DoubleToken('{{valuePrefix}}.text-field.padding.2'); + static const textFieldPadding3 = DoubleToken('{{valuePrefix}}.text-field.padding.3'); + + /// TextArea metrics that cannot be expressed by existing spacing tokens. + static const textAreaMinHeight3 = DoubleToken( + '{{valuePrefix}}.text-area.min-height.3', + ); + + /// DataList metrics that cannot be expressed by existing spacing tokens. + static const dataListRowGap3 = DoubleToken('{{valuePrefix}}.data-list.row-gap.3'); + static const dataListLabelMinWidth = DoubleToken( + '{{valuePrefix}}.data-list.label-min-width', + ); + + /// Exact uppercase fallback typography for each Avatar size. + static const avatarFallback1One = TextStyleToken( + '{{valuePrefix}}.avatar.fallback.1.one', + ); + static const avatarFallback1Two = TextStyleToken( + '{{valuePrefix}}.avatar.fallback.1.two', + ); + static const avatarFallback2One = TextStyleToken( + '{{valuePrefix}}.avatar.fallback.2.one', + ); + static const avatarFallback2Two = TextStyleToken( + '{{valuePrefix}}.avatar.fallback.2.two', + ); + static const avatarFallback3One = TextStyleToken( + '{{valuePrefix}}.avatar.fallback.3.one', + ); + static const avatarFallback3Two = TextStyleToken( + '{{valuePrefix}}.avatar.fallback.3.two', + ); + static const avatarFallback4One = TextStyleToken( + '{{valuePrefix}}.avatar.fallback.4.one', + ); + static const avatarFallback4Two = TextStyleToken( + '{{valuePrefix}}.avatar.fallback.4.two', + ); + static const avatarFallback5 = TextStyleToken('{{valuePrefix}}.avatar.fallback.5'); + static const avatarFallback6 = TextStyleToken('{{valuePrefix}}.avatar.fallback.6'); + static const avatarFallback7 = TextStyleToken('{{valuePrefix}}.avatar.fallback.7'); + static const avatarFallback8 = TextStyleToken('{{valuePrefix}}.avatar.fallback.8'); + static const avatarFallback9 = TextStyleToken('{{valuePrefix}}.avatar.fallback.9'); + + /// Tabs size 1 inner vertical padding - 2px at 100% scaling. + static const tabInnerPaddingY1 = DoubleToken('{{valuePrefix}}.tabs.inner-padding-y.1'); + + /// Tabs size 1 active tracking - -0.12px at 100% scaling. + static const tabActiveLetterSpacing1 = DoubleToken( + '{{valuePrefix}}.tabs.active-letter-spacing.1', + ); + + /// Tabs size 2 active tracking - -0.14px at 100% scaling. + static const tabActiveLetterSpacing2 = DoubleToken( + '{{valuePrefix}}.tabs.active-letter-spacing.2', + ); + + /// Table size-1 minimum cell height (36px at 100% scaling). + /// + /// Radix writes `calc(36px * var(--scaling))` literally, so no existing + /// spacing step expresses it. + static const dataTableRowHeight1 = DoubleToken('{{valuePrefix}}.data-table.height.1'); + + /// Table size-2 minimum cell height (44px at 100% scaling). + static const dataTableRowHeight2 = DoubleToken('{{valuePrefix}}.data-table.height.2'); + + /// Table surface border - `color-mix(in oklab, gray-a5, gray-6)`. + /// + /// The existing `grayStroke*` tokens blend an alpha step with the *same* + /// numbered solid step at 25%; Table blends step 5 with step 6 at 50%. + static const dataTableBorder = ColorToken('{{valuePrefix}}.data-table.border'); + + /// Select's 1.5 × space-1 measurement (6px at 100% scaling). + static const selectSpace1Half = DoubleToken('{{valuePrefix}}.select.space.1-half'); + + /// Select size-1 indicator column width (20px at 100% scaling). + static const selectIndicatorWidth1 = DoubleToken( + '{{valuePrefix}}.select.indicator-width.1', + ); + + /// Select size-1 check size (8px at 100% scaling). + static const selectIndicatorSize1 = DoubleToken( + '{{valuePrefix}}.select.indicator-size.1', + ); + + /// Select size-2/3 check size (10px at 100% scaling). + static const selectIndicatorSize2 = DoubleToken( + '{{valuePrefix}}.select.indicator-size.2', + ); + + /// Negative margins that cancel Select ghost-trigger padding. + static const selectGhostMarginX12 = DoubleToken( + '{{valuePrefix}}.select.ghost-margin-x.1-2', + ); + static const selectGhostMarginY12 = DoubleToken( + '{{valuePrefix}}.select.ghost-margin-y.1-2', + ); + static const selectGhostMarginX3 = DoubleToken( + '{{valuePrefix}}.select.ghost-margin-x.3', + ); + static const selectGhostMarginY3 = DoubleToken( + '{{valuePrefix}}.select.ghost-margin-y.3', + ); + + // ============================================================================ + // BORDER RADIUS SCALE (6 STEPS + FULL) + // ============================================================================ + + /// Radius step 1 - 3px. + /// + /// Subtle rounding for small elements like buttons + /// and form inputs. Provides gentle softening of corners. + static const radius1 = RadiusToken('{{valuePrefix}}.radius.1'); + + /// Radius step 2 - 4px. + /// + /// Small radius for compact components and minor rounding. + /// Good for small badges and tight layouts. + static const radius2 = RadiusToken('{{valuePrefix}}.radius.2'); + + /// Radius step 3 - 6px. + /// + /// Medium radius for standard components like buttons + /// and cards. Balances modern look with usability. + static const radius3 = RadiusToken('{{valuePrefix}}.radius.3'); + + /// Radius step 4 - 8px. + /// + /// Large radius for prominent components and generous rounding. + /// Good for larger buttons and feature cards. + static const radius4 = RadiusToken('{{valuePrefix}}.radius.4'); + + /// Radius step 5 - 12px. + /// + /// Extra large radius for major components and modern aesthetics. + /// Suitable for large cards and prominent interface elements. + static const radius5 = RadiusToken('{{valuePrefix}}.radius.5'); + + /// Radius step 6 - 16px. + /// + /// Very large radius for distinctive styling and major components. + /// Creates a soft, friendly appearance for large interface elements. + static const radius6 = RadiusToken('{{valuePrefix}}.radius.6'); + + /// Theme-level full radius, enabled only by [{{typePrefix}}Radius.full]. + static const radiusFull = RadiusToken('{{valuePrefix}}.radius.full'); + + /// Radius used by control thumbs. + static const radiusThumb = RadiusToken('{{valuePrefix}}.radius.thumb'); + + /// Fixed circle radius for shapes that stay circular across theme presets. + static const radiusCircle = RadiusToken('{{valuePrefix}}.radius.circle'); + + /// Radius step 1 promoted to a pill when the theme radius is full. + static const radius1OrFull = RadiusToken('{{valuePrefix}}.radius.1-or-full'); + + /// Radius step 2 promoted to a pill when the theme radius is full. + static const radius2OrFull = RadiusToken('{{valuePrefix}}.radius.2-or-full'); + + /// Radius step 3 promoted to a pill when the theme radius is full. + static const radius3OrFull = RadiusToken('{{valuePrefix}}.radius.3-or-full'); + + /// Radius step 4 promoted to a pill when the theme radius is full. + static const radius4OrFull = RadiusToken('{{valuePrefix}}.radius.4-or-full'); + + /// Radius step 5 promoted to a pill when the theme radius is full. + static const radius5OrFull = RadiusToken('{{valuePrefix}}.radius.5-or-full'); + + /// Radius step 6 promoted to a pill when the theme radius is full. + static const radius6OrFull = RadiusToken('{{valuePrefix}}.radius.6-or-full'); + + /// Radius step 1 promoted to the control-thumb radius when larger. + static const radius1OrThumb = RadiusToken('{{valuePrefix}}.radius.1-or-thumb'); + + /// Radius step 2 promoted to the control-thumb radius when larger. + static const radius2OrThumb = RadiusToken('{{valuePrefix}}.radius.2-or-thumb'); + + // ============================================================================ + // ELEVATION SHADOWS (6 LEVELS) + // ============================================================================ + + /// Shadow level 1 - Subtle elevation. + /// + /// Minimal shadow for slight elevation effects. + /// Good for cards and buttons in their resting state. + static const shadow1 = BoxShadowToken('{{valuePrefix}}.shadow.1'); + + /// Exact layered shadow level 1, including inset layers. + /// + /// This additive token powers {{typePrefix}}'s Radix-compatible rendering while + /// [shadow1] retains the original Remix public token type. + static const shadow1Layers = RemixBoxShadowListToken( + '{{valuePrefix}}.shadow.1.layers', + ); + + /// Half-opacity shadow-1 layers used by a disabled classic slider track. + static const sliderClassicDisabledTrackShadows = RemixBoxShadowListToken( + '{{valuePrefix}}.slider.classic.disabled-track-shadows', + ); + + /// Shadow level 2 - Low elevation. + /// + /// Light shadow for gentle elevation and hover states. + /// Suitable for interactive elements and small modals. + static const shadow2 = BoxShadowToken('{{valuePrefix}}.shadow.2'); + + /// Shadow-2 painted on SegmentedControl's fixed one-pixel inset shape. + static const segmentedControlClassicIndicatorShadows = + RemixBoxShadowListToken( + '{{valuePrefix}}.segmented-control.classic.indicator-shadows', + ); + + /// Shadow level 3 - Medium elevation. + /// + /// Moderate shadow for clear visual separation. + /// Good for dropdowns, tooltips, and floating elements. + static const shadow3 = BoxShadowToken('{{valuePrefix}}.shadow.3'); + + /// Shadow level 4 - High elevation. + /// + /// Prominent shadow for important floating content. + /// Suitable for modal dialogs and important overlays. + static const shadow4 = BoxShadowToken('{{valuePrefix}}.shadow.4'); + + /// Shadow level 5 - Very high elevation. + /// + /// Strong shadow for primary modals and major overlays. + /// Creates clear hierarchy and focus on important content. + static const shadow5 = BoxShadowToken('{{valuePrefix}}.shadow.5'); + + /// Shadow level 6 - Maximum elevation. + /// + /// Maximum shadow depth for critical dialogs and notifications. + /// Ensures content appears above all other interface elements. + static const shadow6 = BoxShadowToken('{{valuePrefix}}.shadow.6'); + + /// Card classic outer and inset-pseudo-element shadow lists. + static const cardClassicOuterShadows = RemixBoxShadowListToken( + '{{valuePrefix}}.card.classic.outer-shadows', + ); + static const cardClassicInnerShadows = RemixBoxShadowListToken( + '{{valuePrefix}}.card.classic.inner-shadows', + ); + static const cardClassicHoverOuterShadows = RemixBoxShadowListToken( + '{{valuePrefix}}.card.classic.hover.outer-shadows', + ); + static const cardClassicHoverInnerShadows = RemixBoxShadowListToken( + '{{valuePrefix}}.card.classic.hover.inner-shadows', + ); + static const cardClassicActiveOuterShadows = RemixBoxShadowListToken( + '{{valuePrefix}}.card.classic.active.outer-shadows', + ); + static const cardClassicActiveInnerShadows = RemixBoxShadowListToken( + '{{valuePrefix}}.card.classic.active.inner-shadows', + ); + + /// Mode-aware inset layers for a classic Select trigger. + static const selectTriggerClassicShadows = RemixBoxShadowListToken( + '{{valuePrefix}}.select.trigger.classic.shadows', + ); + + /// Mode-aware open/hover layers for a classic Select trigger. + static const selectTriggerClassicHoverShadows = RemixBoxShadowListToken( + '{{valuePrefix}}.select.trigger.classic.hover-shadows', + ); + + /// Mode-aware disabled layers shared by classic button-shaped controls. + static const baseButtonClassicDisabledShadows = RemixBoxShadowListToken( + '{{valuePrefix}}.base-button.classic.disabled.shadows', + ); + + /// Mode-aware classic Button/IconButton layers. + static const baseButtonClassicShadows = RemixBoxShadowListToken( + '{{valuePrefix}}.base-button.classic.shadows', + ); + static const baseButtonClassicHighContrastShadows = RemixBoxShadowListToken( + '{{valuePrefix}}.base-button.classic.high-contrast.shadows', + ); + static const baseButtonClassicActiveShadows = RemixBoxShadowListToken( + '{{valuePrefix}}.base-button.classic.active.shadows', + ); + static const baseButtonClassicActiveHighContrastShadows = + RemixBoxShadowListToken( + '{{valuePrefix}}.base-button.classic.active.high-contrast.shadows', + ); + static const baseButtonClassicAfterInset = DoubleToken( + '{{valuePrefix}}.base-button.classic.after-inset', + ); + static const baseButtonGhostPaddingY3 = DoubleToken( + '{{valuePrefix}}.base-button.ghost.padding-y.3', + ); + static const baseButtonGhostMarginX12 = DoubleToken( + '{{valuePrefix}}.base-button.ghost.margin-x.1-2', + ); + static const baseButtonGhostMarginY12 = DoubleToken( + '{{valuePrefix}}.base-button.ghost.margin-y.1-2', + ); + static const baseButtonGhostMarginX3 = DoubleToken( + '{{valuePrefix}}.base-button.ghost.margin-x.3', + ); + static const baseButtonGhostMarginY3 = DoubleToken( + '{{valuePrefix}}.base-button.ghost.margin-y.3', + ); + static const baseButtonGhostMarginX4 = DoubleToken( + '{{valuePrefix}}.base-button.ghost.margin-x.4', + ); + static const baseButtonGhostMarginY4 = DoubleToken( + '{{valuePrefix}}.base-button.ghost.margin-y.4', + ); + static const iconButtonGhostPadding2 = DoubleToken( + '{{valuePrefix}}.icon-button.ghost.padding.2', + ); + static const iconButtonGhostMargin1 = DoubleToken( + '{{valuePrefix}}.icon-button.ghost.margin.1', + ); + static const iconButtonGhostMargin2 = DoubleToken( + '{{valuePrefix}}.icon-button.ghost.margin.2', + ); + static const iconButtonGhostMargin3 = DoubleToken( + '{{valuePrefix}}.icon-button.ghost.margin.3', + ); + static const iconButtonGhostMargin4 = DoubleToken( + '{{valuePrefix}}.icon-button.ghost.margin.4', + ); + static const cardGhostMargin1 = DoubleToken('{{valuePrefix}}.card.ghost.margin.1'); + static const cardGhostMargin2 = DoubleToken('{{valuePrefix}}.card.ghost.margin.2'); + static const cardGhostMargin3 = DoubleToken('{{valuePrefix}}.card.ghost.margin.3'); + static const cardGhostMargin4 = DoubleToken('{{valuePrefix}}.card.ghost.margin.4'); + static const cardGhostMargin5 = DoubleToken('{{valuePrefix}}.card.ghost.margin.5'); + + // ============================================================================ + // BORDER AND STROKE WIDTHS + // ============================================================================ + + /// Standard border width (1px). + /// + /// Default border thickness for most components like inputs, + /// cards, and dividers. Provides clear boundaries without visual weight. + static const borderWidth1 = SpaceToken('{{valuePrefix}}.border.width.1'); + + /// Thick border width (2px). + /// + /// Heavier border for emphasis, selected states, and components + /// that need stronger visual definition. + static const borderWidth2 = SpaceToken('{{valuePrefix}}.border.width.2'); + + /// Focus ring border width (2px). + /// + /// Standard width for focus outlines to ensure accessibility + /// compliance and clear keyboard navigation feedback. + static const focusRingWidth = SpaceToken('{{valuePrefix}}.focus.ring.width'); + + /// Focus ring offset distance from element edge. + /// + /// Space between the component border and focus ring, + /// ensuring the focus indicator doesn't interfere with the element. + static const focusRingOffset = SpaceToken('{{valuePrefix}}.focus.ring.offset'); + + // ============================================================================ + // TYPOGRAPHY SCALE (9 LEVELS) + // ============================================================================ + // + // Text sizes with carefully tuned line heights and letter spacing + // for optimal readability across all scales. + // + + /// Text size 1 - 12px (Small labels and metadata). + /// + /// Smallest readable text for labels, captions, and secondary metadata. + /// Includes tight letter spacing for improved legibility at small sizes. + static const text1 = TextStyleToken('{{valuePrefix}}.text.1'); + + /// Text size 2 - 14px (Standard UI text). + /// + /// Default size for most interface text including buttons, + /// form labels, and secondary content. + static const text2 = TextStyleToken('{{valuePrefix}}.text.2'); + + /// Text size 3 - 16px (Body text and primary content). + /// + /// Ideal for body text and primary content. Provides excellent + /// readability for extended reading on all device types. + static const text3 = TextStyleToken('{{valuePrefix}}.text.3'); + + /// Accordion size-2 text (15px with a 20px line box at 100% scaling). + /// + /// Accordion is a {{typePrefix}} extension, so this intermediate size is kept + /// separate from the upstream Radix typography scale. + static const accordionText2 = TextStyleToken('{{valuePrefix}}.accordion.text.2'); + + /// Text size 4 - 18px (Prominent body text). + /// + /// For important content that needs more visual weight than + /// standard body text but isn't quite a heading. + static const text4 = TextStyleToken('{{valuePrefix}}.text.4'); + + /// Text size 5 - 20px (Small headings). + /// + /// For minor headings, subheadings, and content that needs + /// to stand out from body text. + static const text5 = TextStyleToken('{{valuePrefix}}.text.5'); + + /// Text size 6 - 24px (Medium headings). + /// + /// Standard heading size for section titles and important content. + /// Good balance between prominence and page economy. + static const text6 = TextStyleToken('{{valuePrefix}}.text.6'); + + /// Text size 7 - 28px (Large headings). + /// + /// For major page headings and important announcements. + /// Creates strong visual hierarchy and draws attention. + static const text7 = TextStyleToken('{{valuePrefix}}.text.7'); + + /// Text size 8 - 35px (Extra large headings). + /// + /// For hero text, page titles, and major content sections. + /// Strong negative letter spacing improves appearance at large sizes. + static const text8 = TextStyleToken('{{valuePrefix}}.text.8'); + + /// Text size 9 - 60px (Display text). + /// + /// Maximum text size for hero sections and display typography. + /// Includes significant negative letter spacing and tight line height. + static const text9 = TextStyleToken('{{valuePrefix}}.text.9'); + + // ============================================================================ + // FONT WEIGHT TOKENS + // ============================================================================ + + /// Light font weight (300). + /// + /// Optional lighter weight occasionally used in display typography or + /// subdued text. Provided for parity with Radix token set. + static const fontWeightLight = FontWeightToken('{{valuePrefix}}.font.weight.light'); + + /// Regular font weight (400). + /// + /// Standard weight for body text and most interface elements. + /// Provides good readability without visual strain. + static const fontWeightRegular = FontWeightToken( + '{{valuePrefix}}.font.weight.regular', + ); + + /// Medium font weight (500). + /// + /// Slightly heavier than regular for UI elements that need + /// more visual weight, like active states and button text. + static const fontWeightMedium = FontWeightToken('{{valuePrefix}}.font.weight.medium'); + + /// Bold font weight (700). + /// + /// For headings and content that needs strong emphasis. + /// Provides clear hierarchy without being too heavy. + static const fontWeightBold = FontWeightToken('{{valuePrefix}}.font.weight.bold'); + + // ============================================================================ + // ANIMATION DURATIONS + // ============================================================================ + + /// Fast animation duration (100ms). + /// + /// For quick micro-interactions like hover states and button presses. + /// Provides immediate feedback without feeling sluggish. + static const transitionFast = DurationToken('{{valuePrefix}}.transition.fast'); + + /// Slow animation duration (300ms). + /// + /// For more substantial transitions like modal appearances, + /// page transitions, and complex state changes. + static const transitionSlow = DurationToken('{{valuePrefix}}.transition.slow'); + + /// One leg of the Radix Skeleton pulse. + static const skeletonPulseDuration = DurationToken( + '{{valuePrefix}}.skeleton.pulse-duration', + ); +} diff --git a/packages/remix_cli/lib/src/registry/fortal/templates/toggle/toggle.dart.tmpl b/packages/remix_cli/lib/src/registry/fortal/templates/toggle/toggle.dart.tmpl new file mode 100644 index 000000000..0df3bcd67 --- /dev/null +++ b/packages/remix_cli/lib/src/registry/fortal/templates/toggle/toggle.dart.tmpl @@ -0,0 +1,136 @@ +import 'package:flutter/widgets.dart'; +import 'package:mix_annotations/mix_annotations.dart'; +import 'package:remix/remix.dart'; + +import '../theme/theme.dart'; + +part 'toggle.g.dart'; + +/// {{typePrefix}} toggle size presets. +enum {{typePrefix}}ToggleSize { size1, size2, size3 } + +/// {{typePrefix}} toggle color and border variants. +enum {{typePrefix}}ToggleVariant { ghost, outline } + +/// {{typePrefix}}-themed preset for [RemixToggle]. +@MixWidget(target: RemixToggle.new) +ToggleStyler {{valuePrefix}}ToggleStyle({ + {{typePrefix}}ToggleVariant variant = .ghost, + {{typePrefix}}ToggleSize size = .size2, + bool highContrast = false, + ToggleStyler style = const ToggleStyler.create(), +}) { + return (switch (variant) { + .ghost => _{{valuePrefix}}ToggleGhostStyler(size, highContrast: highContrast), + .outline => _{{valuePrefix}}ToggleOutlineStyler(size, highContrast: highContrast), + }).merge(style); +} + +ToggleStyler _{{valuePrefix}}ToggleBaseStyler({{typePrefix}}ToggleSize size) { + return ToggleStyler() + .container(.mainAxisSize(.min)) + .labelColor({{typePrefix}}Tokens.gray12()) + .iconColor({{typePrefix}}Tokens.gray12()) + .labelFontWeight({{typePrefix}}Tokens.fontWeightMedium()) + .merge(_{{valuePrefix}}ToggleSizeStyler(size)); +} + +ToggleStyler _{{valuePrefix}}ToggleFocusStyler() => ToggleStyler().{{valuePrefix}}FocusRing(); + +ToggleStyler _{{valuePrefix}}ToggleDisabledStyler({bool outlined = false}) { + final style = ToggleStyler() + .color({{typePrefix}}Tokens.grayA3()) + .labelColor({{typePrefix}}Tokens.gray8()) + .iconColor({{typePrefix}}Tokens.gray8()); + return outlined + ? style.border( + .color({{typePrefix}}Tokens.grayA6()) + .width({{typePrefix}}Tokens.borderWidth1()) + .strokeAlign(BorderSide.strokeAlignInside), + ) + : style; +} + +ToggleStyler _{{valuePrefix}}ToggleGhostStyler( + {{typePrefix}}ToggleSize size, { + required bool highContrast, +}) { + final selectedContent = highContrast + ? {{typePrefix}}Tokens.accent12() + : {{typePrefix}}Tokens.accent11(); + return _{{valuePrefix}}ToggleBaseStyler(size) + .color(const Color(0x00000000)) + .onHovered(ToggleStyler().color({{typePrefix}}Tokens.grayA3())) + .onPressed(ToggleStyler().color({{typePrefix}}Tokens.grayA4())) + .onSelected( + ToggleStyler() + .color({{typePrefix}}Tokens.accent3()) + .labelColor(selectedContent) + .iconColor(selectedContent) + .onHovered(ToggleStyler().color({{typePrefix}}Tokens.accent4())) + .onPressed(ToggleStyler().color({{typePrefix}}Tokens.accent5())), + ) + .onFocusVisible(_{{valuePrefix}}ToggleFocusStyler()) + .onDisabled(_{{valuePrefix}}ToggleDisabledStyler()); +} + +ToggleStyler _{{valuePrefix}}ToggleOutlineStyler( + {{typePrefix}}ToggleSize size, { + required bool highContrast, +}) { + final selectedContent = highContrast + ? {{typePrefix}}Tokens.accent12() + : {{typePrefix}}Tokens.accent11(); + return _{{valuePrefix}}ToggleBaseStyler(size) + .color(const Color(0x00000000)) + .border( + .color({{typePrefix}}Tokens.gray7()) + .width({{typePrefix}}Tokens.borderWidth1()) + .strokeAlign(BorderSide.strokeAlignInside), + ) + .onHovered(ToggleStyler().color({{typePrefix}}Tokens.grayA3())) + .onPressed(ToggleStyler().color({{typePrefix}}Tokens.grayA4())) + .onSelected( + ToggleStyler() + .color({{typePrefix}}Tokens.accentA3()) + .labelColor(selectedContent) + .iconColor(selectedContent) + .border(.color({{typePrefix}}Tokens.accentA5())) + .onHovered(ToggleStyler().color({{typePrefix}}Tokens.accentA4())) + .onPressed(ToggleStyler().color({{typePrefix}}Tokens.accentA5())), + ) + .onFocusVisible(_{{valuePrefix}}ToggleFocusStyler()) + .onDisabled(_{{valuePrefix}}ToggleDisabledStyler(outlined: true)); +} + +ToggleStyler _{{valuePrefix}}ToggleSizeStyler({{typePrefix}}ToggleSize size) { + return switch (size) { + .size1 => ToggleStyler( + container: FlexBoxStyler() + .padding(.horizontal({{typePrefix}}Tokens.space2())) + .padding(.vertical({{typePrefix}}Tokens.space1())) + .borderRadius(.all({{typePrefix}}Tokens.radius2())) + .spacing({{typePrefix}}Tokens.toggleGap1()), + label: .style({{typePrefix}}Tokens.text1.mix()), + icon: .size({{typePrefix}}Tokens.space3()), + ), + .size2 => ToggleStyler( + container: FlexBoxStyler() + .padding(.horizontal({{typePrefix}}Tokens.space3())) + .padding(.vertical({{typePrefix}}Tokens.space2())) + .borderRadius(.all({{typePrefix}}Tokens.radius2())) + .spacing({{typePrefix}}Tokens.space1()), + label: .style({{typePrefix}}Tokens.text2.mix()), + icon: .size({{typePrefix}}Tokens.space4()), + ), + .size3 => ToggleStyler( + container: FlexBoxStyler() + .padding(.horizontal({{typePrefix}}Tokens.space4())) + .padding(.vertical({{typePrefix}}Tokens.space2())) + .borderRadius(.all({{typePrefix}}Tokens.radius3())) + .spacing({{typePrefix}}Tokens.toggleGap3()), + label: .style({{typePrefix}}Tokens.text3.mix()), + icon: .size({{typePrefix}}Tokens.spinnerSize3()), + ), + }; +} diff --git a/packages/remix_cli/lib/src/registry/fortal/templates/toggle_group/toggle_group.dart.tmpl b/packages/remix_cli/lib/src/registry/fortal/templates/toggle_group/toggle_group.dart.tmpl new file mode 100644 index 000000000..165d0a6cf --- /dev/null +++ b/packages/remix_cli/lib/src/registry/fortal/templates/toggle_group/toggle_group.dart.tmpl @@ -0,0 +1,121 @@ +import 'package:flutter/widgets.dart'; +import 'package:mix_annotations/mix_annotations.dart'; +import 'package:remix/remix.dart'; + +import '../theme/theme.dart'; + +part 'toggle_group.g.dart'; + +/// {{typePrefix}} toggle-group size presets. +enum {{typePrefix}}ToggleGroupSize { size1, size2, size3 } + +/// {{typePrefix}} toggle-group color treatments. +enum {{typePrefix}}ToggleGroupVariant { soft, surface } + +/// {{typePrefix}}-themed segmented-control preset for [RemixToggleGroup]. +@MixWidget(target: RemixToggleGroup.new) +ToggleGroupStyler {{valuePrefix}}ToggleGroupStyle({ + {{typePrefix}}ToggleGroupVariant variant = .soft, + {{typePrefix}}ToggleGroupSize size = .size2, + bool highContrast = false, + ToggleGroupStyler style = const ToggleGroupStyler.create(), +}) { + final ( + selectedColor, + selectedHoverColor, + selectedPressedColor, + ) = switch (variant) { + .soft => ( + {{typePrefix}}Tokens.accent3(), + {{typePrefix}}Tokens.accent4(), + {{typePrefix}}Tokens.accent5(), + ), + .surface => ( + {{typePrefix}}Tokens.accentSurface(), + {{typePrefix}}Tokens.accentA4(), + {{typePrefix}}Tokens.accentA5(), + ), + }; + final selectedForeground = highContrast + ? {{typePrefix}}Tokens.accent12() + : {{typePrefix}}Tokens.accent11(); + + return ToggleGroupStyler( + container: FlexBoxStyler( + decoration: BoxDecorationMix( + border: BorderMix.all( + BorderSideMix( + color: {{typePrefix}}Tokens.gray7(), + width: {{typePrefix}}Tokens.borderWidth1(), + ), + ), + color: {{typePrefix}}Tokens.colorSurface(), + ), + clipBehavior: .hardEdge, + mainAxisSize: .min, + spacing: 0, + ), + item: .alignment(.center) + .labelColor({{typePrefix}}Tokens.gray11()) + .iconColor({{typePrefix}}Tokens.gray11()) + .labelFontWeight({{typePrefix}}Tokens.fontWeightMedium()) + .onHovered(ToggleGroupItemStyler().color({{typePrefix}}Tokens.grayA3())) + .onPressed(ToggleGroupItemStyler().color({{typePrefix}}Tokens.grayA4())) + .onSelected( + ToggleGroupItemStyler() + .color(selectedColor) + .labelColor(selectedForeground) + .iconColor(selectedForeground) + .onHovered(ToggleGroupItemStyler().color(selectedHoverColor)) + .onPressed(ToggleGroupItemStyler().color(selectedPressedColor)), + ) + .onFocusVisible(ToggleGroupItemStyler().{{valuePrefix}}FocusRing()) + .onDisabled( + ToggleGroupItemStyler() + .color({{typePrefix}}Tokens.grayA3()) + .labelColor({{typePrefix}}Tokens.gray8()) + .iconColor({{typePrefix}}Tokens.gray8()), + ), + ).merge(_{{valuePrefix}}ToggleGroupSizeStyler(size)).merge(style); +} + +ToggleGroupStyler _{{valuePrefix}}ToggleGroupSizeStyler({{typePrefix}}ToggleGroupSize size) { + return switch (size) { + .size1 => ToggleGroupStyler( + container: FlexBoxStyler().borderRadius(.all({{typePrefix}}Tokens.radius2())), + item: + .container( + FlexBoxStyler() + .padding(.horizontal({{typePrefix}}Tokens.space2())) + .padding(.vertical({{typePrefix}}Tokens.space1())) + .spacing({{typePrefix}}Tokens.toggleGap1()), + ) + .label(.style({{typePrefix}}Tokens.text1.mix())) + .icon(.size({{typePrefix}}Tokens.space3())), + ), + .size2 => ToggleGroupStyler( + container: FlexBoxStyler().borderRadius(.all({{typePrefix}}Tokens.radius2())), + item: + .container( + FlexBoxStyler() + .padding(.horizontal({{typePrefix}}Tokens.space3())) + .padding(.vertical({{typePrefix}}Tokens.space2())) + .spacing({{typePrefix}}Tokens.space1()), + ) + .label(.style({{typePrefix}}Tokens.text2.mix())) + .icon(.size({{typePrefix}}Tokens.space4())), + ), + .size3 => ToggleGroupStyler( + container: FlexBoxStyler().borderRadius(.all({{typePrefix}}Tokens.radius3())), + item: + .container( + FlexBoxStyler() + .padding(.horizontal({{typePrefix}}Tokens.space4())) + .padding(.vertical({{typePrefix}}Tokens.space2())) + .spacing({{typePrefix}}Tokens.toggleGap3()), + ) + .label(.style({{typePrefix}}Tokens.text3.mix())) + .icon(.size({{typePrefix}}Tokens.spinnerSize3())), + ), + }; +} diff --git a/packages/remix_cli/lib/src/registry/fortal/templates/tooltip/tooltip.dart.tmpl b/packages/remix_cli/lib/src/registry/fortal/templates/tooltip/tooltip.dart.tmpl new file mode 100644 index 000000000..4b9d757f4 --- /dev/null +++ b/packages/remix_cli/lib/src/registry/fortal/templates/tooltip/tooltip.dart.tmpl @@ -0,0 +1,24 @@ +import 'package:flutter/widgets.dart'; +import 'package:mix_annotations/mix_annotations.dart'; +import 'package:remix/remix.dart'; + +import '../theme/theme.dart'; + +part 'tooltip.g.dart'; + +/// {{typePrefix}}-themed preset for [RemixTooltip]. +@MixWidget(target: RemixTooltip.new) +TooltipStyler {{valuePrefix}}TooltipStyle({ + TooltipStyler style = const TooltipStyler.create(), +}) { + return TooltipStyler( + label: .style({{typePrefix}}Tokens.text1.mix()), + waitDuration: const Duration(milliseconds: 200), + ) + .borderRadius(.all({{typePrefix}}Tokens.radius2())) + .padding(.vertical({{typePrefix}}Tokens.space1())) + .padding(.horizontal({{typePrefix}}Tokens.space2())) + .label(.color({{typePrefix}}Tokens.gray1())) + .color({{typePrefix}}Tokens.gray12()) + .merge(style); +} diff --git a/packages/remix_cli/lib/src/registry/fortal/templates/typography/typography.dart.tmpl b/packages/remix_cli/lib/src/registry/fortal/templates/typography/typography.dart.tmpl new file mode 100644 index 000000000..4c381f49d --- /dev/null +++ b/packages/remix_cli/lib/src/registry/fortal/templates/typography/typography.dart.tmpl @@ -0,0 +1,82 @@ +import 'package:flutter/widgets.dart'; +import 'package:remix/remix.dart'; + +import '../theme/theme.dart'; + +/// The nine-step Radix Themes text scale supplied by [{{typePrefix}}Tokens]. +enum {{typePrefix}}TextSize { + size1, + size2, + size3, + size4, + size5, + size6, + size7, + size8, + size9, +} + +/// Font weights supported by the {{typePrefix}} typography scale. +/// +/// Closed rather than Flutter's [FontWeight], which is an open class accepting +/// any value from 1 to 1000; Radix ships exactly these four. +enum {{typePrefix}}TextWeight { light, regular, medium, bold } + +TextStyleToken {{valuePrefix}}TextSizeToken({{typePrefix}}TextSize size) => switch (size) { + .size1 => {{typePrefix}}Tokens.text1, + .size2 => {{typePrefix}}Tokens.text2, + .size3 => {{typePrefix}}Tokens.text3, + .size4 => {{typePrefix}}Tokens.text4, + .size5 => {{typePrefix}}Tokens.text5, + .size6 => {{typePrefix}}Tokens.text6, + .size7 => {{typePrefix}}Tokens.text7, + .size8 => {{typePrefix}}Tokens.text8, + .size9 => {{typePrefix}}Tokens.text9, +}; + +FontWeightToken {{valuePrefix}}TextWeightToken({{typePrefix}}TextWeight weight) => + switch (weight) { + .light => {{typePrefix}}Tokens.fontWeightLight, + .regular => {{typePrefix}}Tokens.fontWeightRegular, + .medium => {{typePrefix}}Tokens.fontWeightMedium, + .bold => {{typePrefix}}Tokens.fontWeightBold, + }; + +/// [truncate] deliberately wins over [softWrap], forcing one ellipsized line. +TextStyler {{valuePrefix}}ApplyTextFlow( + TextStyler style, { + TextAlign? align, + required bool softWrap, + required bool truncate, +}) { + if (align != null) style = style.textAlign(align); + if (truncate) { + return style.maxLines(1).softWrap(false).overflow(TextOverflow.ellipsis); + } + + return style.softWrap(softWrap); +} + +TextStyler {{valuePrefix}}AccentForeground( + TextStyler style, { + required bool highContrast, +}) => style.color( + highContrast ? {{typePrefix}}Tokens.accent12() : {{typePrefix}}Tokens.accentA11(), +); + +/// Code, Kbd, and Link derive em-relative geometry from the resolved font size, +/// so unlike the other recipes they cannot stay context-free. +TextStyle {{valuePrefix}}ResolveTextToken(BuildContext context, {{typePrefix}}TextSize size) => + MixScope.tokenOf({{valuePrefix}}TextSizeToken(size), context); + +Color {{valuePrefix}}ResolveColor(BuildContext context, ColorToken token) => + MixScope.tokenOf(token, context); + +/// Derived from the resolved `radius1` rather than duplicating the {{typePrefix}} +/// radius enum table, so theme radius and scaling changes flow through. +double {{valuePrefix}}RadiusFactor(BuildContext context) { + final scaling = {{typePrefix}}Theme.of(context).scaling.factor; + final radius = MixScope.tokenOf({{typePrefix}}Tokens.radius1, context); + + return radius.x / (3 * scaling); +} diff --git a/packages/remix_cli/test/init_command_test.dart b/packages/remix_cli/test/init_command_test.dart index 90e84f1be..1f4568738 100644 --- a/packages/remix_cli/test/init_command_test.dart +++ b/packages/remix_cli/test/init_command_test.dart @@ -192,6 +192,36 @@ paths: expect(snapshotFiles(root), before); }); + test('rejects changing the preset of an initialized project', () async { + await installer.initialize( + const InitOptions( + prefix: 'Ui', + preset: 'default', + uiPath: 'lib/ui', + ), + ); + final before = snapshotFiles(root); + + await expectLater( + installer.initialize( + const InitOptions( + prefix: 'Ui', + preset: 'fortal', + uiPath: 'lib/ui', + ), + ), + throwsA( + isA().having( + (error) => error.message, + 'message', + contains('prefix, preset, and UI path'), + ), + ), + ); + + expect(snapshotFiles(root), before); + }); + test('rejects unrelated and malformed barrels without mutation', () async { for (final source in [ 'library;\n', diff --git a/packages/remix_cli/test/installer_test.dart b/packages/remix_cli/test/installer_test.dart index b6ae59a31..56eefa484 100644 --- a/packages/remix_cli/test/installer_test.dart +++ b/packages/remix_cli/test/installer_test.dart @@ -58,6 +58,12 @@ void main() { tearDown(() => root.deleteSync(recursive: true)); test('add loads the registry selected by project configuration', () async { + File(p.join(root.path, 'remix.yaml')).writeAsStringSync('''schema: 2 +prefix: Ui +preset: fortal +paths: + ui: lib/ui +'''); String? loadedPreset; final installer = Installer( projectRoot: root, @@ -70,7 +76,7 @@ void main() { await installer.add(const AddOptions(item: 'button', mode: AddMode.dryRun)); - expect(loadedPreset, 'default'); + expect(loadedPreset, 'fortal'); }); test( diff --git a/packages/remix_cli/test/project_config_test.dart b/packages/remix_cli/test/project_config_test.dart index a6f653075..6743e75fd 100644 --- a/packages/remix_cli/test/project_config_test.dart +++ b/packages/remix_cli/test/project_config_test.dart @@ -76,7 +76,7 @@ paths: }); test('rejects invalid and unbundled presets', () { - for (final preset in ['', 'Default', 'default-name', 'fortal']) { + for (final preset in ['', 'Default', 'default-name', 'missing']) { expect( () => ProjectConfig.create( packageRoot: root, diff --git a/packages/remix_cli/test/registry_test.dart b/packages/remix_cli/test/registry_test.dart index 710b7d10d..8cb7c2ac8 100644 --- a/packages/remix_cli/test/registry_test.dart +++ b/packages/remix_cli/test/registry_test.dart @@ -22,6 +22,38 @@ void main() { }, ); + test('Fortal registry loads its inferred dependency graph', () async { + final catalog = await RegistryCatalog.loadBundled(preset: 'fortal'); + + expect(catalog.preset, 'fortal'); + expect(catalog.resolve('button').map((item) => item.name), [ + 'theme', + 'base_button', + 'button', + ]); + expect(catalog.items['data_table']!.registryDependencies, [ + 'theme', + 'checkbox', + 'icon_button', + 'select', + ]); + expect(catalog.resolve('data_table').map((item) => item.name), [ + 'theme', + 'checkbox', + 'base_button', + 'icon_button', + 'select', + 'data_table', + ]); + expect(catalog.items['theme']!.files, hasLength(8)); + expect(catalog.items['theme']!.exports, ['theme/theme.dart']); + + final button = catalog.items['button']!; + final source = await catalog.readTemplate(button.files.single); + expect(source, contains('{{typePrefix}}Button')); + expect(source, isNot(contains('remix_fortal'))); + }); + test('unknown preset fails before reading an asset and lists bundles', () { expect( RegistryCatalog.loadBundled( @@ -32,7 +64,7 @@ void main() { isA().having( (error) => error.message, 'message', - allOf(contains('missing'), contains('default')), + allOf(contains('missing'), contains('default'), contains('fortal')), ), ), ); diff --git a/packages/remix_fortal/lib/src/components/chart.dart b/packages/remix_fortal/lib/src/components/chart.dart index 2686304c7..2f5ada379 100644 --- a/packages/remix_fortal/lib/src/components/chart.dart +++ b/packages/remix_fortal/lib/src/components/chart.dart @@ -9,18 +9,18 @@ import '../theme/theme.dart'; part 'chart.g.dart'; -final _standardPaletteToken = ContextToken>( +const _standardPaletteToken = ContextToken>( _resolveStandardPalette, ); -final _highContrastPaletteToken = ContextToken>( +const _highContrastPaletteToken = ContextToken>( _resolveHighContrastPalette, ); -final _tooltipBorderToken = ContextToken(_resolveTooltipBorder); -final _tooltipRadiusToken = ContextToken(_resolveTooltipRadius); -final _tooltipPaddingToken = ContextToken(_resolveTooltipPadding); -final _barRadiusToken = ContextToken(_resolveBarRadius); -final _lineWidthToken = ContextToken(_resolveLineWidth); -final _highContrastLineWidthToken = ContextToken( +const _tooltipBorderToken = ContextToken(_resolveTooltipBorder); +const _tooltipRadiusToken = ContextToken(_resolveTooltipRadius); +const _tooltipPaddingToken = ContextToken(_resolveTooltipPadding); +const _barRadiusToken = ContextToken(_resolveBarRadius); +const _lineWidthToken = ContextToken(_resolveLineWidth); +const _highContrastLineWidthToken = ContextToken( _resolveHighContrastLineWidth, ); diff --git a/packages/remix_fortal/lib/src/theme/computed.dart b/packages/remix_fortal/lib/src/theme/computed.dart index fdcf0c954..fc4e3ee0b 100644 --- a/packages/remix_fortal/lib/src/theme/computed.dart +++ b/packages/remix_fortal/lib/src/theme/computed.dart @@ -177,7 +177,7 @@ double _encodeSrgb(double channel) => channel <= 0.0031308 : 1.055 * math.pow(channel, 1 / 2.4).toDouble() - 0.055; /// Builds Radix Themes elevation shadows for the active brightness. -Map buildFortalShadows({ +Map, Object> buildFortalShadows({ required bool isDark, required FortalThemeColors colors, }) { @@ -359,7 +359,7 @@ Map buildFortalShadows({ return _fortalShadowTokens(shadows); } -Map _fortalShadowTokens( +Map, Object> _fortalShadowTokens( Map> shadows, ) { final shadow1 = shadows['shadow1']!; @@ -519,8 +519,8 @@ FortalThemeColors resolveFortalTokens(FortalThemeConfig theme) { final RadixColorScale gray = grayRC.scale; // Neutral alpha swatches - final ColorSwatch blackA = blackAlpha; - final ColorSwatch whiteA = whiteAlpha; + const ColorSwatch blackA = blackAlpha; + const ColorSwatch whiteA = whiteAlpha; // Backgrounds/panels/overlay final Color colorBackground = computeColorBackground( diff --git a/packages/remix_fortal/lib/src/theme/theme.dart b/packages/remix_fortal/lib/src/theme/theme.dart index 17c1983f0..353b67858 100644 --- a/packages/remix_fortal/lib/src/theme/theme.dart +++ b/packages/remix_fortal/lib/src/theme/theme.dart @@ -1,5 +1,5 @@ // Internal entrypoint for the theme layer, imported by every component under -// `src/components/`. The package's public entrypoint is `lib/remix_fortal.dart`. +// `src/components/`. The public entrypoint exports this barrel. export 'computed.dart'; export 'control_styles.dart'; diff --git a/packages/remix_fortal/lib/src/theme/theme_data.dart b/packages/remix_fortal/lib/src/theme/theme_data.dart index c5e24bcc8..9f537833a 100644 --- a/packages/remix_fortal/lib/src/theme/theme_data.dart +++ b/packages/remix_fortal/lib/src/theme/theme_data.dart @@ -227,7 +227,7 @@ class FortalThemeData extends FortalThemeConfig { } /// Builds the token map for a Fortal scope. Used by [FortalScope]. -Map buildFortalScopeTokens(FortalThemeData theme) { +Map, Object> buildFortalScopeTokens(FortalThemeData theme) { final tokens = resolveFortalTokens(theme); final scaling = theme.scaling.factor; final shadows = buildFortalShadows(isDark: theme.isDark, colors: tokens); @@ -356,7 +356,7 @@ Map buildFortalScopeTokens(FortalThemeData theme) { }; // Build base tokens map - final allTokens = { + final allTokens = , Object>{ ...colorTokens, // Defaults (may be overridden by JSON tokens below) FortalTokens.panelBlur: @@ -658,9 +658,9 @@ Map buildFortalScopeTokens(FortalThemeData theme) { FortalTokens.fontWeightBold: FontWeight.w700, // Durations (token values) - FortalTokens.transitionFast: Duration(milliseconds: 100), - FortalTokens.transitionSlow: Duration(milliseconds: 300), - FortalTokens.skeletonPulseDuration: Duration(milliseconds: 1000), + FortalTokens.transitionFast: const Duration(milliseconds: 100), + FortalTokens.transitionSlow: const Duration(milliseconds: 300), + FortalTokens.skeletonPulseDuration: const Duration(milliseconds: 1000), }; return allTokens; diff --git a/packages/remix_fortal/lib/src/theme/theme_scope.dart b/packages/remix_fortal/lib/src/theme/theme_scope.dart index 9cbb8177d..68aed417c 100644 --- a/packages/remix_fortal/lib/src/theme/theme_scope.dart +++ b/packages/remix_fortal/lib/src/theme/theme_scope.dart @@ -28,7 +28,7 @@ import 'tokens.dart'; /// is the platform system stack, and a null family is Flutter's equivalent; /// naming a concrete family here would pin every consumer to one typeface. Widget _fortalRootTextStyle({ - required Map tokens, + required Map, Object> tokens, required Widget child, }) { final root = tokens[FortalTokens.text3]! as TextStyle; diff --git a/pubspec.yaml b/pubspec.yaml index 91c51cc85..3b0e9fd07 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -131,6 +131,16 @@ melos: - dart run tool/check_open_code.dart description: Test the checker and install every registry item in a fresh app + open-code:fortal:build: + run: dart run tool/build_fortal_preset.dart + description: Derive the Fortal registry from analyzer-checked package source + + open-code:fortal:check: + steps: + - dart test test/tool/build_fortal_preset_test.dart + - dart run tool/build_fortal_preset.dart --check + description: Test Fortal derivation and fail on committed preset drift + open-code:dogfood:check: run: dart run tool/check_open_code_dogfood.dart description: Verify the playground's installed source still mirrors the registry @@ -155,6 +165,7 @@ melos: - mix:consumer:check - material:check - generate:check + - open-code:fortal:check - docs:check - fortal:parity:check - open-code:dogfood:check diff --git a/test/tool/build_fortal_preset_test.dart b/test/tool/build_fortal_preset_test.dart new file mode 100644 index 000000000..8ce5ecc34 --- /dev/null +++ b/test/tool/build_fortal_preset_test.dart @@ -0,0 +1,242 @@ +import 'dart:io'; + +import 'package:path/path.dart' as p; +import 'package:test/test.dart'; +import 'package:yaml/yaml.dart'; + +import '../../tool/build_fortal_preset.dart'; + +void main() { + late Directory sandbox; + + setUp(() { + sandbox = Directory.systemTemp.createTempSync('fortal_preset_test_'); + }); + + tearDown(() { + if (sandbox.existsSync()) sandbox.deleteSync(recursive: true); + }); + + test('every authored template round-trips and removes Fortal names', () { + final output = FortalPresetBuilder.forRepository( + Directory.current, + ).derive(); + + expect(output.sourceByTemplate, isNotEmpty); + for (final entry in output.sourceByTemplate.entries) { + final template = output.files[entry.key]!; + final rendered = template + .replaceAll('{{typePrefix}}', 'Fortal') + .replaceAll('{{valuePrefix}}', 'fortal'); + + expect(rendered, entry.value, reason: entry.key); + expect(template, isNot(contains('Fortal')), reason: entry.key); + expect(template, isNot(contains('fortal')), reason: entry.key); + } + }); + + test('registry dependencies and package floors are inferred', () { + final output = FortalPresetBuilder.forRepository( + Directory.current, + ).derive(); + final document = loadYaml(output.files['registry.yaml']!) as YamlMap; + final items = document['items'] as YamlMap; + + expect(_strings((items['button'] as YamlMap)['registryDependencies']), [ + 'theme', + 'base_button', + ]); + expect(_strings((items['data_table'] as YamlMap)['registryDependencies']), [ + 'theme', + 'checkbox', + 'icon_button', + 'select', + ]); + expect(_strings((items['sidebar'] as YamlMap)['registryDependencies']), [ + 'theme', + 'text', + 'toggle', + ]); + expect( + (items['base_button'] as YamlMap).containsKey('dependencies'), + isFalse, + ); + expect( + (items['typography'] as YamlMap).containsKey('devDependencies'), + isFalse, + ); + expect( + ((items['chart'] as YamlMap)['dependencies'] as YamlMap).keys, + containsAll(['mix_annotations', 'mix_chart']), + ); + expect(_strings((items['button'] as YamlMap)['generated']), [ + '@ui/components/button.g.dart', + ]); + }); + + test('refuses Fortal path segments before reading registry metadata', () { + final builder = _emptyBuilder(sandbox); + _write( + builder.sourceRoot, + 'components/fortal_button.dart', + 'void recipe() {}\n', + ); + + expect( + builder.derive, + throwsA( + isA().having( + (error) => error.message, + 'message', + allOf(contains('components/fortal_button.dart'), contains('segment')), + ), + ), + ); + }); + + test('refuses reserved template tokens before substitution', () { + final builder = _emptyBuilder(sandbox); + _write( + builder.sourceRoot, + 'components/button.dart', + '// {{reserved}}\nvoid recipe() {}\n', + ); + + expect( + builder.derive, + throwsA( + isA().having( + (error) => error.message, + 'message', + allOf(contains('components/button.dart'), contains('"{{"')), + ), + ), + ); + }); + + test('refuses every package import outside the installed boundary', () { + for (final package in ['remix_fortal', 'mix', 'naked_ui']) { + final root = Directory(p.join(sandbox.path, package)); + final builder = _emptyBuilder(root); + _write( + builder.sourceRoot, + 'components/button.dart', + "import 'package:$package/example.dart';\n", + ); + + expect( + builder.derive, + throwsA( + isA().having( + (error) => error.message, + 'message', + allOf( + contains('components/button.dart'), + contains('package:$package/'), + ), + ), + ), + reason: package, + ); + } + }); + + test('check mode reports planted changed and stale output', () { + final builder = _fixtureBuilder(sandbox); + final output = builder.derive(); + builder.write(output); + expect(builder.drift(output), isEmpty); + + File( + p.join( + builder.outputRoot.path, + 'templates', + 'button', + 'button.dart.tmpl', + ), + ).writeAsStringSync('// planted drift\n'); + _write(builder.outputRoot, 'templates/stale.dart.tmpl', '// stale\n'); + + expect(builder.drift(output), [ + 'changed templates/button/button.dart.tmpl', + 'stale templates/stale.dart.tmpl', + ]); + }); +} + +FortalPresetBuilder _emptyBuilder(Directory root) => FortalPresetBuilder( + sourceRoot: Directory(p.join(root.path, 'source')), + defaultRegistryRoot: Directory(p.join(root.path, 'default')), + outputRoot: Directory(p.join(root.path, 'output')), +); + +FortalPresetBuilder _fixtureBuilder(Directory root) { + final builder = _emptyBuilder(root); + _write(builder.sourceRoot, 'theme/theme.dart', "export 'tokens.dart';\n"); + _write( + builder.sourceRoot, + 'theme/tokens.dart', + "import 'package:remix/remix.dart';\nabstract class FortalTokens {}\n", + ); + _write( + builder.sourceRoot, + 'components/button.dart', + """import 'package:mix_annotations/mix_annotations.dart'; +import 'package:remix/remix.dart'; + +import '../theme/theme.dart'; + +part 'button.g.dart'; + +void fortalButtonStyle() {} +""", + ); + _writeDefaultRegistry(builder.defaultRegistryRoot); + return builder; +} + +void _writeDefaultRegistry(Directory root) { + _write(root, 'registry.yaml', '''schema: 1 +items: + theme: + dependencies: + remix: ^1.0.0 + files: + - source: templates/theme/theme.dart.tmpl + target: "@ui/theme/theme.dart" + button: + dependencies: + mix_annotations: ^2.0.0 + devDependencies: + build_runner: ^2.0.0 + mix_generator: ^2.0.0 + files: + - source: templates/button/button.dart.tmpl + target: "@ui/components/button.dart" + chart: + dependencies: + mix_chart: ^1.0.0 + files: + - source: templates/chart/chart.dart.tmpl + target: "@ui/components/chart.dart" + icons: + dependencies: + remix_ui_icons: ^1.0.0 + files: + - source: templates/icons/icons.dart.tmpl + target: "@ui/icons.dart" +'''); + _write( + root, + 'templates/icons/icons.dart.tmpl', + 'abstract final class {{typePrefix}}Icons {}\n', + ); +} + +void _write(Directory root, String relativePath, String source) { + final file = File(p.joinAll([root.path, ...p.posix.split(relativePath)])); + file.parent.createSync(recursive: true); + file.writeAsStringSync(source); +} + +List _strings(Object? value) => (value as YamlList).cast(); diff --git a/test/tool/check_open_code_test.dart b/test/tool/check_open_code_test.dart index 42a053e88..c2ff1edfe 100644 --- a/test/tool/check_open_code_test.dart +++ b/test/tool/check_open_code_test.dart @@ -21,11 +21,26 @@ void main() { expect(checker.fixtureContractProblem(fixture), isNull); }); + test('the Fortal fixture is also an isolated pre-install contract', () { + final fixture = Directory( + '${Directory.current.path}/open_code/fortal_fixture', + ); + + expect(checker.fixtureContractProblem(fixture, preset: 'fortal'), isNull); + }); + group('registry coverage', () { test('the checkout catalog and the checker agree', () { expect(checker.registryCoverageProblem(Directory.current), isNull); }); + test('the Fortal catalog and the checker agree', () { + expect( + checker.registryCoverageProblem(Directory.current, preset: 'fortal'), + isNull, + ); + }); + test('an item the checker never installs is reported', () { final root = Directory('${sandbox.path}/repo'); final registry = File( diff --git a/tool/build_fortal_preset.dart b/tool/build_fortal_preset.dart new file mode 100644 index 000000000..6f5ed7cae --- /dev/null +++ b/tool/build_fortal_preset.dart @@ -0,0 +1,544 @@ +import 'dart:io'; + +import 'package:path/path.dart' as p; +import 'package:yaml/yaml.dart'; + +/// Derives the application-owned Fortal registry from `remix_fortal` source. +/// +/// Run without arguments to synchronize committed output. Pass `--check` to +/// compare in memory and fail on drift without writing. +void main(List arguments) { + final check = arguments.contains('--check'); + final unknown = arguments.where((argument) => argument != '--check').toList(); + if (unknown.isNotEmpty || arguments.length != (check ? 1 : 0)) { + stderr.writeln('Usage: dart run tool/build_fortal_preset.dart [--check]'); + exitCode = 64; + return; + } + + final repositoryRoot = Directory.current.absolute; + final pubspec = File(p.join(repositoryRoot.path, 'pubspec.yaml')); + if (!pubspec.existsSync() || + !RegExp( + r'^name:\s*remix_workspace\s*$', + multiLine: true, + ).hasMatch(pubspec.readAsStringSync())) { + stderr.writeln('Run this tool from the Remix workspace root.'); + exitCode = 64; + return; + } + + try { + final builder = FortalPresetBuilder.forRepository(repositoryRoot); + final output = builder.derive(); + if (!check) { + builder.write(output); + stdout.writeln( + 'Wrote the Fortal preset: ${output.files.length - 1} templates and ' + 'registry.yaml.', + ); + return; + } + + final drift = builder.drift(output); + if (drift.isNotEmpty) { + stderr + ..writeln('The committed Fortal preset is stale:') + ..writeln(drift.map((entry) => ' - $entry').join('\n')) + ..writeln( + 'Run `dart run tool/build_fortal_preset.dart` and commit the result.', + ); + exitCode = 1; + return; + } + stdout.writeln('The committed Fortal preset matches authored source.'); + } on Object catch (error) { + stderr.writeln(error); + exitCode = 1; + } +} + +/// A deterministic snapshot of every file owned by the Fortal preset. +final class FortalPresetOutput { + const FortalPresetOutput({ + required this.files, + required this.sourceByTemplate, + }); + + /// Output paths relative to the preset root, including `registry.yaml`. + final Map files; + + /// Original authored source keyed by its derived template output path. + /// + /// This makes the substitution round trip directly testable without + /// exposing filesystem implementation details. + final Map sourceByTemplate; +} + +/// Builds one bundled registry tree from analyzer-checked Fortal Dart source. +final class FortalPresetBuilder { + const FortalPresetBuilder({ + required this.sourceRoot, + required this.defaultRegistryRoot, + required this.outputRoot, + }); + + factory FortalPresetBuilder.forRepository(Directory repositoryRoot) { + final registryRoot = Directory( + p.join( + repositoryRoot.path, + 'packages', + 'remix_cli', + 'lib', + 'src', + 'registry', + ), + ); + return FortalPresetBuilder( + sourceRoot: Directory( + p.join(repositoryRoot.path, 'packages', 'remix_fortal', 'lib', 'src'), + ), + defaultRegistryRoot: Directory(p.join(registryRoot.path, 'default')), + outputRoot: Directory(p.join(registryRoot.path, 'fortal')), + ); + } + + final Directory sourceRoot; + final Directory defaultRegistryRoot; + final Directory outputRoot; + + FortalPresetOutput derive() { + if (!sourceRoot.existsSync()) { + throw FormatException( + 'Fortal source root is missing: ${sourceRoot.path}', + ); + } + + final sources = _readSources(); + _validateSources(sources); + + final floors = _readDefaultFloors(); + final output = {}; + final sourceByTemplate = {}; + final items = {}; + + final themeSources = sources.entries + .where((entry) => entry.key.startsWith('theme/')) + .toList(); + if (themeSources.isEmpty || + !themeSources.any((entry) => entry.key == 'theme/theme.dart')) { + throw const FormatException( + 'Fortal source must contain theme/theme.dart and its theme files.', + ); + } + + final themeFiles = <_RegistryFileDraft>[]; + for (final entry in themeSources) { + final name = p.posix.basename(entry.key); + final templatePath = 'templates/theme/$name.tmpl'; + output[templatePath] = _templateFor(entry.key, entry.value); + sourceByTemplate[templatePath] = entry.value; + themeFiles.add( + _RegistryFileDraft(source: templatePath, target: '@ui/theme/$name'), + ); + } + items['theme'] = _RegistryItemDraft( + name: 'theme', + dependencies: {'remix': floors['remix']!}, + files: themeFiles, + exports: const ['theme/theme.dart'], + ); + + final defaultIcons = File( + p.join(defaultRegistryRoot.path, 'templates', 'icons', 'icons.dart.tmpl'), + ); + if (!defaultIcons.existsSync()) { + throw FormatException( + 'Default icons template is missing: ${defaultIcons.path}', + ); + } + const iconsTemplate = 'templates/icons/icons.dart.tmpl'; + output[iconsTemplate] = defaultIcons.readAsStringSync(); + items['icons'] = _RegistryItemDraft( + name: 'icons', + registryDependencies: const ['theme'], + dependencies: {'remix_ui_icons': floors['remix_ui_icons']!}, + files: const [ + _RegistryFileDraft(source: iconsTemplate, target: '@ui/icons.dart'), + ], + exports: const ['icons.dart'], + ); + + final componentNames = { + for (final path in sources.keys) + if (path.startsWith('components/')) + p.posix.basenameWithoutExtension(path), + }; + for (final entry in sources.entries.where( + (entry) => entry.key.startsWith('components/'), + )) { + final name = p.posix.basenameWithoutExtension(entry.key); + final templatePath = 'templates/$name/$name.dart.tmpl'; + final generated = _generatedPart(entry.value); + final imports = _imports(entry.value); + final registryDependencies = _registryDependencies( + sourcePath: entry.key, + imports: imports, + componentNames: componentNames, + ); + final dependencies = {}; + final devDependencies = {}; + if (generated != null) { + dependencies['mix_annotations'] = floors['mix_annotations']!; + devDependencies + ..['build_runner'] = floors['build_runner']! + ..['mix_generator'] = floors['mix_generator']!; + } + for (final package in const ['mix_chart', 'remix_ui_icons']) { + if (imports.any((uri) => uri.startsWith('package:$package/'))) { + dependencies[package] = floors[package]!; + } + } + + output[templatePath] = _templateFor(entry.key, entry.value); + sourceByTemplate[templatePath] = entry.value; + items[name] = _RegistryItemDraft( + name: name, + registryDependencies: registryDependencies, + dependencies: dependencies, + devDependencies: devDependencies, + files: [ + _RegistryFileDraft( + source: templatePath, + target: '@ui/components/$name.dart', + ), + ], + generated: generated == null ? const [] : ['@ui/components/$generated'], + exports: ['components/$name.dart'], + ); + } + + if (items.length != componentNames.length + 2) { + throw StateError('Fortal registry item names collided.'); + } + + output['registry.yaml'] = _renderRegistry(items); + return FortalPresetOutput( + files: Map.unmodifiable(_sortedMap(output)), + sourceByTemplate: Map.unmodifiable(_sortedMap(sourceByTemplate)), + ); + } + + /// Synchronizes only the files owned beneath [outputRoot]. + void write(FortalPresetOutput output) { + outputRoot.createSync(recursive: true); + final expected = output.files.keys.toSet(); + for (final file in _outputFiles()) { + final relative = _relative(file, outputRoot); + if (!expected.contains(relative)) file.deleteSync(); + } + for (final entry in output.files.entries) { + final file = File( + p.joinAll([outputRoot.path, ...p.posix.split(entry.key)]), + ); + if (file.existsSync() && file.readAsStringSync() == entry.value) continue; + file.parent.createSync(recursive: true); + file.writeAsStringSync(entry.value); + } + } + + /// Returns stable, human-readable differences without mutating output. + List drift(FortalPresetOutput output) { + final differences = []; + final actual = { + for (final file in _outputFiles()) _relative(file, outputRoot): file, + }; + for (final entry in output.files.entries) { + final file = actual.remove(entry.key); + if (file == null) { + differences.add('missing ${entry.key}'); + } else if (file.readAsStringSync() != entry.value) { + differences.add('changed ${entry.key}'); + } + } + for (final stale in actual.keys) { + differences.add('stale $stale'); + } + differences.sort(); + return differences; + } + + Map _readSources() { + final files = + sourceRoot + .listSync(recursive: true, followLinks: false) + .whereType() + .where( + (file) => + file.path.endsWith('.dart') && !file.path.endsWith('.g.dart'), + ) + .toList() + ..sort((left, right) => left.path.compareTo(right.path)); + return { + for (final file in files) + _relative(file, sourceRoot): file.readAsStringSync(), + }; + } + + void _validateSources(Map sources) { + final failures = []; + for (final entry in sources.entries) { + final path = entry.key; + final content = entry.value; + if (p.posix + .split(path) + .any((segment) => segment.toLowerCase().contains('fortal'))) { + failures.add('$path: a path segment contains "fortal"'); + } + if (content.contains('{{')) { + failures.add('$path: source contains the reserved template token "{{"'); + } + for (final match in _importPattern.allMatches(content)) { + final uri = match.group(1)!; + if (_forbiddenImportPrefixes.any(uri.startsWith)) { + failures.add('$path: forbidden installed-source import $uri'); + } + } + if (path != 'icons.dart' && + !path.startsWith('theme/') && + !path.startsWith('components/')) { + failures.add('$path: unsupported Fortal source placement'); + } + } + if (failures.isNotEmpty) { + failures.sort(); + throw FormatException( + 'Cannot derive the Fortal preset:\n${failures.map((failure) => ' - $failure').join('\n')}', + ); + } + } + + Map _readDefaultFloors() { + final registry = File(p.join(defaultRegistryRoot.path, 'registry.yaml')); + if (!registry.existsSync()) { + throw FormatException('Default registry is missing: ${registry.path}'); + } + final document = loadYaml(registry.readAsStringSync()); + if (document is! YamlMap || document['items'] is! YamlMap) { + throw FormatException('${registry.path} is not a registry map.'); + } + final constraints = >{}; + for (final item in (document['items'] as YamlMap).values) { + if (item is! YamlMap) continue; + for (final sectionName in const ['dependencies', 'devDependencies']) { + final section = item[sectionName]; + if (section is! YamlMap) continue; + for (final entry in section.entries) { + if (entry.key is String && entry.value is String) { + constraints + .putIfAbsent(entry.key as String, () => {}) + .add(entry.value as String); + } + } + } + } + + const required = { + 'remix', + 'mix_annotations', + 'build_runner', + 'mix_generator', + 'mix_chart', + 'remix_ui_icons', + }; + final floors = {}; + for (final package in required) { + final values = constraints[package]; + if (values == null || values.length != 1) { + throw FormatException( + 'Default registry must declare one $package constraint; found ' + '${values?.join(', ') ?? 'none'}.', + ); + } + floors[package] = values.single; + } + return floors; + } + + List _outputFiles() { + if (!outputRoot.existsSync()) return const []; + final files = + outputRoot + .listSync(recursive: true, followLinks: false) + .whereType() + .toList() + ..sort((left, right) => left.path.compareTo(right.path)); + return files; + } +} + +final class _RegistryItemDraft { + const _RegistryItemDraft({ + required this.name, + this.registryDependencies = const [], + this.dependencies = const {}, + this.devDependencies = const {}, + required this.files, + this.generated = const [], + required this.exports, + }); + + final String name; + final List registryDependencies; + final Map dependencies; + final Map devDependencies; + final List<_RegistryFileDraft> files; + final List generated; + final List exports; +} + +final class _RegistryFileDraft { + const _RegistryFileDraft({required this.source, required this.target}); + + final String source; + final String target; +} + +String _templateFor(String path, String source) { + final template = source + .replaceAll('Fortal', '{{typePrefix}}') + .replaceAll('fortal', '{{valuePrefix}}'); + final roundTrip = template + .replaceAll('{{typePrefix}}', 'Fortal') + .replaceAll('{{valuePrefix}}', 'fortal'); + if (roundTrip != source) { + throw StateError('$path did not survive the template round trip.'); + } + return template; +} + +List _imports(String source) => [ + for (final match in _importPattern.allMatches(source)) match.group(1)!, +]; + +List _registryDependencies({ + required String sourcePath, + required List imports, + required Set componentNames, +}) { + final dependencies = {}; + for (final uri in imports) { + if (uri.startsWith('package:') || uri.startsWith('dart:')) continue; + final resolved = p.posix.normalize( + p.posix.join(p.posix.dirname(sourcePath), uri), + ); + if (resolved.startsWith('theme/')) { + dependencies.add('theme'); + continue; + } + if (resolved.startsWith('components/')) { + final component = p.posix.basenameWithoutExtension(resolved); + if (component != p.posix.basenameWithoutExtension(sourcePath)) { + if (!componentNames.contains(component)) { + throw FormatException( + '$sourcePath imports missing component source $uri.', + ); + } + dependencies.add(component); + } + } + } + return [ + if (dependencies.remove('theme')) 'theme', + ...(dependencies.toList()..sort()), + ]; +} + +String? _generatedPart(String source) { + final matches = RegExp( + r'''^\s*part\s+['"]([^'"]+\.g\.dart)['"]\s*;''', + multiLine: true, + ).allMatches(source).toList(); + if (matches.length > 1) { + throw const FormatException( + 'A component declares multiple generated parts.', + ); + } + return matches.singleOrNull?.group(1); +} + +String _renderRegistry(Map items) { + final ordered = <_RegistryItemDraft>[ + items['theme']!, + items['icons']!, + ...items.entries + .where((entry) => entry.key != 'theme' && entry.key != 'icons') + .map((entry) => entry.value) + .toList() + ..sort((left, right) => left.name.compareTo(right.name)), + ]; + final buffer = StringBuffer() + ..writeln('# Generated by tool/build_fortal_preset.dart. Do not edit.') + ..writeln('schema: 1') + ..writeln('items:'); + for (final item in ordered) { + buffer.writeln(' ${item.name}:'); + _writeStringList(buffer, 'registryDependencies', item.registryDependencies); + _writeConstraintMap(buffer, 'dependencies', item.dependencies); + _writeConstraintMap(buffer, 'devDependencies', item.devDependencies); + buffer.writeln(' files:'); + for (final file in item.files) { + buffer + ..writeln(' - source: ${file.source}') + ..writeln(' target: "${file.target}"'); + } + _writeStringList(buffer, 'generated', item.generated, quote: true); + _writeStringList(buffer, 'exports', item.exports); + buffer.writeln(); + } + return '${buffer.toString().trimRight()}\n'; +} + +void _writeStringList( + StringBuffer buffer, + String name, + List values, { + bool quote = false, +}) { + if (values.isEmpty) return; + buffer.writeln(' $name:'); + for (final value in values) { + buffer.writeln(' - ${quote ? '"$value"' : value}'); + } +} + +void _writeConstraintMap( + StringBuffer buffer, + String name, + Map values, +) { + if (values.isEmpty) return; + buffer.writeln(' $name:'); + for (final key in values.keys.toList()..sort()) { + buffer.writeln(' $key: ${values[key]}'); + } +} + +Map _sortedMap(Map source) { + final keys = source.keys.toList()..sort(); + return {for (final key in keys) key: source[key]!}; +} + +String _relative(File file, Directory root) => + p.posix.joinAll(p.split(p.relative(file.path, from: root.path))); + +final _importPattern = RegExp( + r'''^\s*import\s+['"]([^'"]+)['"]''', + multiLine: true, +); + +const _forbiddenImportPrefixes = [ + 'package:remix_fortal/', + 'package:mix/', + 'package:naked_ui/', +]; diff --git a/tool/check_open_code.dart b/tool/check_open_code.dart index 3cbb65ac3..de3f0e2d4 100644 --- a/tool/check_open_code.dart +++ b/tool/check_open_code.dart @@ -2,7 +2,7 @@ /// Flutter application. /// /// ```shell -/// dart run tool/check_open_code.dart [--keep] +/// dart run tool/check_open_code.dart [--preset default|fortal] [--keep] /// ``` /// /// The checker writes only to a guarded system-temporary directory. It installs @@ -27,7 +27,7 @@ const _fixtureAppFiles = [ /// /// Each is added by its own `remix add`, which is the only supported call /// shape. Theme arrives as the first item's registry dependency. -const _registryItems = [ +const _defaultRegistryItems = [ 'icons', 'accordion', 'avatar', @@ -79,26 +79,78 @@ const _generatedSnapshots = { 'lib/ui/components/tabs.g.dart': 'expected/acme_tabs.g.dart', }; -/// Every file the CLI is expected to leave under `lib/ui/`, and nothing else. -/// -/// Derived from [_registryItems] rather than restated: an item that installed -/// a file this list does not name would be a boundary violation, and a list -/// maintained by hand would drift into agreeing with whatever the CLI did. -final _installedUiFiles = [ - 'ui.dart', - 'theme/tokens.dart', - 'theme/theme_data.dart', - 'theme/theme_scope.dart', - for (final item in _registryItems) - ...(item == 'icons' - ? const ['icons.dart'] - : ['components/$item.dart', 'components/$item.g.dart']), +const _fortalRegistryItems = [ + 'icons', + 'accordion', + 'avatar', + 'badge', + 'base_button', + 'button', + 'callout', + 'card', + 'chart', + 'checkbox', + 'code', + 'data_list', + 'data_table', + 'dialog', + 'disclosure', + 'divider', + 'heading', + 'icon_button', + 'kbd', + 'link', + 'menu', + 'popover', + 'progress', + 'radio', + 'segmented_control', + 'select', + 'sidebar', + 'skeleton', + 'slider', + 'spinner', + 'switch', + 'tabs', + 'text', + 'textfield', + 'toggle', + 'toggle_group', + 'tooltip', + 'typography', ]; -final _generatedAppFiles = [ - for (final item in _registryItems) - if (item != 'icons') 'lib/ui/components/$item.g.dart', -]; +const _defaultPreset = _PresetContract( + name: 'default', + fixtureDirectory: 'fixture', + registryItems: _defaultRegistryItems, + themeFiles: ['tokens.dart', 'theme_data.dart', 'theme_scope.dart'], + generatedSnapshots: _generatedSnapshots, +); + +const _fortalPreset = _PresetContract( + name: 'fortal', + fixtureDirectory: 'fortal_fixture', + registryItems: _fortalRegistryItems, + themeFiles: [ + 'computed.dart', + 'control_styles.dart', + 'radix_colors.dart', + 'surface_frame.dart', + 'theme.dart', + 'theme_data.dart', + 'theme_scope.dart', + 'tokens.dart', + ], + nonGeneratedItems: { + 'base_button', + 'code', + 'heading', + 'kbd', + 'link', + 'typography', + }, +); const _requiredRuntimeDependencies = [ 'remix', @@ -116,25 +168,107 @@ const _allowedImportPackages = [ 'remix_ui_icons', ]; +final class _PresetContract { + const _PresetContract({ + required this.name, + required this.fixtureDirectory, + required this.registryItems, + required this.themeFiles, + this.nonGeneratedItems = const {}, + this.generatedSnapshots = const {}, + }); + + final String name; + final String fixtureDirectory; + final List registryItems; + final List themeFiles; + final Set nonGeneratedItems; + final Map generatedSnapshots; + + List get installedUiFiles => [ + 'ui.dart', + for (final file in themeFiles) 'theme/$file', + for (final item in registryItems) + ...(item == 'icons' + ? const ['icons.dart'] + : [ + 'components/$item.dart', + if (!nonGeneratedItems.contains(item)) 'components/$item.g.dart', + ]), + ]; + + List get generatedAppFiles => [ + for (final item in registryItems) + if (item != 'icons' && !nonGeneratedItems.contains(item)) + 'lib/ui/components/$item.g.dart', + ]; +} + +final class _ParsedArguments { + const _ParsedArguments({required this.keep, required this.preset}); + + final bool keep; + final _PresetContract preset; +} + +_ParsedArguments? _parseArguments(List arguments) { + var keep = false; + var sawPreset = false; + var preset = _defaultPreset; + for (var index = 0; index < arguments.length; index += 1) { + final argument = arguments[index]; + if (argument == '--keep') { + if (keep) return null; + keep = true; + continue; + } + if (argument == '--preset') { + if (sawPreset || index + 1 >= arguments.length) return null; + sawPreset = true; + final name = arguments[++index]; + switch (name) { + case 'default': + preset = _defaultPreset; + case 'fortal': + preset = _fortalPreset; + default: + return null; + } + continue; + } + return null; + } + return _ParsedArguments(keep: keep, preset: preset); +} + Future main(List arguments) async { - final keep = arguments.contains('--keep'); - final unknown = arguments.where((argument) => argument != '--keep').toList(); - if (unknown.isNotEmpty) { - stderr.writeln('Unknown arguments: ${unknown.join(' ')}'); - stderr.writeln('Usage: dart run tool/check_open_code.dart [--keep]'); + final parsed = _parseArguments(arguments); + if (parsed == null) { + stderr.writeln( + 'Usage: dart run tool/check_open_code.dart ' + '[--preset default|fortal] [--keep]', + ); exitCode = 64; return; } final repositoryRoot = Directory.current.absolute; - final failure = await _run(repositoryRoot, keep: keep); + final failure = await _run( + repositoryRoot, + keep: parsed.keep, + preset: parsed.preset, + ); if (failure != null) { stderr.writeln('open-code check failed: ${failure.message}'); exitCode = failure.exitCode; } } -Future<_Failure?> _run(Directory repositoryRoot, {required bool keep}) async { +Future<_Failure?> _run( + Directory repositoryRoot, { + required bool keep, + required _PresetContract preset, +}) async { final rootFailure = _verifyRepositoryRoot(repositoryRoot); if (rootFailure != null) return rootFailure; @@ -148,12 +282,14 @@ Future<_Failure?> _run(Directory repositoryRoot, {required bool keep}) async { final sdk = resolved as _Toolchain; _step('Flutter ${sdk.version} from ${sdk.root}'); - final coverageFailure = _verifyRegistryCoverage(repositoryRoot); + final coverageFailure = _verifyRegistryCoverage(repositoryRoot, preset); if (coverageFailure != null) return coverageFailure; _step('Every bundled registry item is installed by this check.'); - final fixtureRoot = Directory('${repositoryRoot.path}/open_code/fixture'); - final fixtureFailure = _verifyFixtureContract(fixtureRoot); + final fixtureRoot = Directory( + '${repositoryRoot.path}/open_code/${preset.fixtureDirectory}', + ); + final fixtureFailure = _verifyFixtureContract(fixtureRoot, preset); if (fixtureFailure != null) return fixtureFailure; _step('Fixture is a minimal pre-install app with no consumer build.yaml.'); @@ -169,6 +305,7 @@ Future<_Failure?> _run(Directory repositoryRoot, {required bool keep}) async { repositoryRoot: repositoryRoot, fixtureRoot: fixtureRoot, app: app, + preset: preset, ); if (checkFailure != null) { return _retainedFailure(parent, checkFailure); @@ -200,7 +337,7 @@ Future<_Failure?> _run(Directory repositoryRoot, {required bool keep}) async { ..writeln('Review local source against the registry:') ..writeln( ' cd ${app.path} && ${sdk.dart} run remix_cli:remix add ' - '${_registryItems.first} --diff', + '${preset.registryItems.first} --diff', ); } @@ -213,6 +350,7 @@ Future<_Failure?> _checkInTemporaryApp({ required Directory repositoryRoot, required Directory fixtureRoot, required Directory app, + required _PresetContract preset, }) async { final environment = _toolchainEnvironment(sdk); @@ -259,13 +397,21 @@ Future<_Failure?> _checkInTemporaryApp({ final init = await _runProcess( sdk.dart, - ['run', 'remix_cli:remix', 'init', '--prefix', 'Acme'], + [ + 'run', + 'remix_cli:remix', + 'init', + '--prefix', + 'Acme', + '--preset', + preset.name, + ], workingDirectory: app.path, environment: environment, ); if (init != null) return _Failure('remix init failed in the fresh app'); - for (final item in _registryItems) { + for (final item in preset.registryItems) { final add = await _runProcess( sdk.dart, ['run', 'remix_cli:remix', 'add', item], @@ -276,7 +422,7 @@ Future<_Failure?> _checkInTemporaryApp({ } _step('CLI installed Theme, every item, dependencies, and generated output.'); - final installedFailure = _verifyInstalledUi(app); + final installedFailure = _verifyInstalledUi(app, preset); if (installedFailure != null) return installedFailure; _step('Installed UI inventory and import boundary are exact.'); @@ -302,6 +448,7 @@ Future<_Failure?> _checkInTemporaryApp({ final floorFailure = _verifyResolvedRemixFloor( repositoryRoot: repositoryRoot, hostedPackages: hostedPackages, + preset: preset, ); if (floorFailure != null) return floorFailure; _step('Hosted remix resolved to the registry floor.'); @@ -314,12 +461,15 @@ Future<_Failure?> _checkInTemporaryApp({ if (hostedCheckoutFailure != null) return hostedCheckoutFailure; _step('remix_cli is the only checkout package in hosted-consumer mode.'); - final generatedFailure = _verifyGeneratedFixture( - app: app, - fixtureRoot: fixtureRoot, - ); - if (generatedFailure != null) return generatedFailure; - _step('Hosted generation matches the committed Acme adapter.'); + if (preset.generatedSnapshots.isNotEmpty) { + final generatedFailure = _verifyGeneratedFixture( + app: app, + fixtureRoot: fixtureRoot, + preset: preset, + ); + if (generatedFailure != null) return generatedFailure; + _step('Hosted generation matches the committed Acme adapter.'); + } final hostedVerification = await _analyzeAndTest( sdk: sdk, @@ -361,7 +511,7 @@ dependency_overrides: if (currentCheckoutFailure != null) return currentCheckoutFailure; _step('Current-source override resolves only Remix and remix_cli locally.'); - for (final relative in _generatedAppFiles) { + for (final relative in preset.generatedAppFiles) { final generated = File('${app.path}/$relative'); if (!generated.existsSync()) { return _Failure('CLI produced no $relative before regeneration.'); @@ -374,7 +524,8 @@ dependency_overrides: 'run', 'build_runner', 'build', - for (final relative in _generatedAppFiles) '--build-filter=$relative', + for (final relative in preset.generatedAppFiles) + '--build-filter=$relative', ], workingDirectory: app.path, environment: environment, @@ -386,12 +537,15 @@ dependency_overrides: return _Failure('generation created an app-level build.yaml'); } - final regeneratedFailure = _verifyGeneratedFixture( - app: app, - fixtureRoot: fixtureRoot, - ); - if (regeneratedFailure != null) return regeneratedFailure; - _step('Current-source regeneration matches the committed Acme adapter.'); + if (preset.generatedSnapshots.isNotEmpty) { + final regeneratedFailure = _verifyGeneratedFixture( + app: app, + fixtureRoot: fixtureRoot, + preset: preset, + ); + if (regeneratedFailure != null) return regeneratedFailure; + _step('Current-source regeneration matches the committed Acme adapter.'); + } return _analyzeAndTest( sdk: sdk, @@ -515,15 +669,19 @@ Future _resolveToolchain(Directory root, String pinned) async { ); } -/// Fails when [_registryItems] and the bundled registry have drifted apart. +/// Fails when the checked item list and bundled registry have drifted apart. /// /// This check exists because the failure it prevents is silent: an item added -/// to `registry.yaml` but not to [_registryItems] is never installed, never +/// to `registry.yaml` but not to the preset contract is never installed, never /// rendered into the fixture, never behavior-tested, and never boundary /// checked — and every suite still passes. -_Failure? _verifyRegistryCoverage(Directory repositoryRoot) { +_Failure? _verifyRegistryCoverage( + Directory repositoryRoot, + _PresetContract preset, +) { final file = File( - '${repositoryRoot.path}/packages/remix_cli/lib/src/registry/default/registry.yaml', + '${repositoryRoot.path}/packages/remix_cli/lib/src/registry/' + '${preset.name}/registry.yaml', ); if (!file.existsSync()) { return _Failure('packages/remix_cli is missing its registry.yaml.'); @@ -540,7 +698,7 @@ _Failure? _verifyRegistryCoverage(Directory repositoryRoot) { for (final key in (document['items'] as YamlMap).keys) if (key is String && key != 'theme') key, }; - final installed = _registryItems.toSet(); + final installed = preset.registryItems.toSet(); final problems = [ for (final item in bundled.difference(installed)) 'registry.yaml has $item, which this check never installs', @@ -566,9 +724,11 @@ _Failure? _verifyRegistryCoverage(Directory repositoryRoot) { _Failure? _verifyResolvedRemixFloor({ required Directory repositoryRoot, required Map hostedPackages, + required _PresetContract preset, }) { final registry = File( - '${repositoryRoot.path}/packages/remix_cli/lib/src/registry/default/registry.yaml', + '${repositoryRoot.path}/packages/remix_cli/lib/src/registry/' + '${preset.name}/registry.yaml', ); final document = loadYaml(registry.readAsStringSync()); final items = document is YamlMap ? document['items'] : null; @@ -611,11 +771,17 @@ _Failure? _verifyResolvedRemixFloor({ ); } -_Failure? _verifyFixtureContract(Directory fixtureRoot) { +_Failure? _verifyFixtureContract( + Directory fixtureRoot, + _PresetContract preset, +) { final problems = []; - for (final relative in [..._fixtureAppFiles, ..._generatedSnapshots.values]) { + for (final relative in [ + ..._fixtureAppFiles, + ...preset.generatedSnapshots.values, + ]) { if (!File('${fixtureRoot.path}/$relative').existsSync()) { - problems.add('open_code/fixture/$relative is missing'); + problems.add('open_code/${preset.fixtureDirectory}/$relative is missing'); } } if (File('${fixtureRoot.path}/build.yaml').existsSync()) { @@ -643,7 +809,8 @@ _Failure? _verifyFixtureContract(Directory fixtureRoot) { if (problems.isEmpty) return null; return _Failure( - 'open_code/fixture is not the minimal CLI consumer contract:\n' + 'open_code/${preset.fixtureDirectory} is not the minimal CLI consumer ' + 'contract:\n' '${problems.map((problem) => ' - $problem').join('\n')}', ); } @@ -714,7 +881,7 @@ void _expectExactKeys( bool _isFlutterSdkDeclaration(Object? declaration) => declaration is YamlMap && declaration['sdk'] == 'flutter'; -_Failure? _verifyInstalledUi(Directory app) { +_Failure? _verifyInstalledUi(Directory app, _PresetContract preset) { if (File('${app.path}/build.yaml').existsSync()) { return _Failure('remix_cli created a consumer build.yaml'); } @@ -730,7 +897,7 @@ _Failure? _verifyInstalledUi(Directory app) { .map((file) => _relativePath(uiRoot, file)) .toList() ..sort(); - final expected = [..._installedUiFiles]..sort(); + final expected = [...preset.installedUiFiles]..sort(); final problems = []; for (final relative in expected) { if (!found.contains(relative)) problems.add('installed UI lacks $relative'); @@ -740,7 +907,9 @@ _Failure? _verifyInstalledUi(Directory app) { problems.add('installed UI has an unexpected file: $relative'); } } - problems.addAll(_installedReferenceProblems(uiRoot)); + problems.addAll( + _installedReferenceProblems(uiRoot, preset.installedUiFiles.toSet()), + ); if (problems.isEmpty) return null; return _Failure( @@ -749,7 +918,10 @@ _Failure? _verifyInstalledUi(Directory app) { ); } -List _installedReferenceProblems(Directory uiRoot) { +List _installedReferenceProblems( + Directory uiRoot, + Set installedUiFiles, +) { final directive = RegExp( r'''^\s*(?:import|export|part(?:\s+of)?)\s+([^;]+);''', multiLine: true, @@ -766,7 +938,11 @@ List _installedReferenceProblems(Directory uiRoot) { for (final match in directive.allMatches(file.readAsStringSync())) { for (final uriMatch in quotedUri.allMatches(match.group(1)!)) { final uri = uriMatch.group(1) ?? uriMatch.group(2)!; - final problem = _referenceProblem(uri, from: relative); + final problem = _referenceProblem( + uri, + from: relative, + installedUiFiles: installedUiFiles, + ); if (problem != null) problems.add('$relative: $problem'); } } @@ -774,7 +950,11 @@ List _installedReferenceProblems(Directory uiRoot) { return problems; } -String? _referenceProblem(String uri, {required String from}) { +String? _referenceProblem( + String uri, { + required String from, + required Set installedUiFiles, +}) { if (uri.startsWith('dart:')) return null; if (uri.startsWith('package:')) { final package = uri.substring('package:'.length).split('/').first; @@ -788,7 +968,7 @@ String? _referenceProblem(String uri, {required String from}) { if (resolved.startsWith('..') || resolved.startsWith('/')) { return '`$uri` escapes lib/ui'; } - if (!_installedUiFiles.contains(resolved)) { + if (!installedUiFiles.contains(resolved)) { return '`$uri` resolves to $resolved, which is not installed'; } return null; @@ -797,8 +977,9 @@ String? _referenceProblem(String uri, {required String from}) { _Failure? _verifyGeneratedFixture({ required Directory app, required Directory fixtureRoot, + required _PresetContract preset, }) { - for (final entry in _generatedSnapshots.entries) { + for (final entry in preset.generatedSnapshots.entries) { final generated = File('${app.path}/${entry.key}'); if (!generated.existsSync()) { return _Failure('generation produced no ${entry.key}'); @@ -806,7 +987,8 @@ _Failure? _verifyGeneratedFixture({ final expected = File('${fixtureRoot.path}/${entry.value}'); if (!_sameBytes(expected.readAsBytesSync(), generated.readAsBytesSync())) { return _Failure( - '${entry.key} differs from open_code/fixture/${entry.value}. ' + '${entry.key} differs from open_code/${preset.fixtureDirectory}/' + '${entry.value}. ' 'Regenerate with --keep and review the diff; do not hand-edit ' 'generated source.', ); @@ -1001,16 +1183,27 @@ bool _sameBytes(List left, List right) { return true; } +_PresetContract _presetByName(String name) => switch (name) { + 'default' => _defaultPreset, + 'fortal' => _fortalPreset, + _ => throw ArgumentError.value(name, 'preset', 'must be default or fortal'), +}; + /// Returns the fixture contract error for focused regression tests. -String? fixtureContractProblem(Directory fixtureRoot) => - _verifyFixtureContract(fixtureRoot)?.message; +String? fixtureContractProblem( + Directory fixtureRoot, { + String preset = 'default', +}) => _verifyFixtureContract(fixtureRoot, _presetByName(preset))?.message; /// Returns the catalog-drift error for focused regression tests. -String? registryCoverageProblem(Directory repositoryRoot) => - _verifyRegistryCoverage(repositoryRoot)?.message; +String? registryCoverageProblem( + Directory repositoryRoot, { + String preset = 'default', +}) => _verifyRegistryCoverage(repositoryRoot, _presetByName(preset))?.message; /// Returns installed inventory/import errors for focused regression tests. -String? installedUiProblem(Directory app) => _verifyInstalledUi(app)?.message; +String? installedUiProblem(Directory app, {String preset = 'default'}) => + _verifyInstalledUi(app, _presetByName(preset))?.message; /// Formats a post-creation failure so the retained directory is always named. String retainedFailureMessage( diff --git a/tool/validate_docs.dart b/tool/validate_docs.dart index 3a8539a34..67cf7318a 100644 --- a/tool/validate_docs.dart +++ b/tool/validate_docs.dart @@ -214,29 +214,35 @@ const _openCodeCatalogDocuments = [ 'open_code/README.md', ]; -/// Fails when a bundled registry item is missing from a catalog document. +/// Fails when an item in either bundled preset is missing from a catalog +/// document. /// /// Deliberately one-directional: a document may mention `theme` or discuss an /// item in prose without listing it, and matching that exactly would make the /// check fight the writing. What it will not allow is shipping an item nobody /// wrote down. void _checkOpenCodeCatalog(Directory workspaceRoot, List failures) { - final registry = File( - '${workspaceRoot.path}/packages/remix_cli/lib/src/registry/default/registry.yaml', - ); - if (!registry.existsSync()) { - failures.add('packages/remix_cli is missing its registry.yaml.'); - return; - } + final items = {}; + for (final preset in ['default', 'fortal']) { + final registry = File( + '${workspaceRoot.path}/packages/remix_cli/lib/src/registry/' + '$preset/registry.yaml', + ); + if (!registry.existsSync()) { + failures.add('packages/remix_cli is missing its $preset registry.yaml.'); + continue; + } - // Item keys are the only two-space-indented `name:` lines in the file. - final items = RegExp(r'^ ([a-z][a-z0-9_]*):$', multiLine: true) - .allMatches(registry.readAsStringSync()) - .map((match) => match.group(1)!) - .where((name) => name != 'theme') - .toList(); + // Item keys are the only two-space-indented `name:` lines in the file. + items.addAll( + RegExp(r'^ ([a-z][a-z0-9_]*):$', multiLine: true) + .allMatches(registry.readAsStringSync()) + .map((match) => match.group(1)!) + .where((name) => name != 'theme'), + ); + } if (items.isEmpty) { - failures.add('registry.yaml declared no component items.'); + failures.add('bundled registries declared no component items.'); return; } @@ -247,7 +253,8 @@ void _checkOpenCodeCatalog(Directory workspaceRoot, List failures) { continue; } final source = file.readAsStringSync(); - final missing = items.where((item) => !source.contains('`$item`')).toList(); + final missing = items.where((item) => !source.contains('`$item`')).toList() + ..sort(); if (missing.isNotEmpty) { failures.add( '$relativePath does not list registry ${missing.join(', ')}.', From f0911471a045ec1ba9a1fac67b9a644067d306d4 Mon Sep 17 00:00:00 2001 From: Leo Farias Date: Fri, 4 Sep 2026 14:21:04 -0400 Subject: [PATCH 05/12] chore(remix_fortal): stop publishing --- .github/workflows/publish.yml | 39 ++------ .github/workflows/version.yml | 77 +++++++-------- README.md | 17 +++- docs/components/accordion.mdx | 4 +- docs/components/avatar.mdx | 4 +- docs/components/badge.mdx | 4 +- docs/components/button.mdx | 4 +- docs/components/callout.mdx | 4 +- docs/components/card.mdx | 4 +- docs/components/checkbox.mdx | 4 +- docs/components/checkbox_group.mdx | 2 +- docs/components/data_list.mdx | 2 +- docs/components/data_table.mdx | 10 +- docs/components/dialog.mdx | 8 +- docs/components/disclosure.mdx | 2 +- docs/components/divider.mdx | 4 +- docs/components/icon_button.mdx | 4 +- docs/components/link.mdx | 4 +- docs/components/menu.mdx | 8 +- docs/components/popover.mdx | 2 +- docs/components/progress.mdx | 4 +- docs/components/radio.mdx | 4 +- docs/components/segmented_control.mdx | 2 +- docs/components/select.mdx | 4 +- docs/components/sidebar.mdx | 2 +- docs/components/skeleton.mdx | 2 +- docs/components/slider.mdx | 4 +- docs/components/spinner.mdx | 4 +- docs/components/switch.mdx | 4 +- docs/components/tabs.mdx | 4 +- docs/components/textarea.mdx | 2 +- docs/components/textfield.mdx | 4 +- docs/components/toggle.mdx | 4 +- docs/components/toggle_group.mdx | 2 +- docs/components/tooltip.mdx | 4 +- docs/fortal.mdx | 55 +++++++---- docs/fortal/typography.mdx | 23 ++--- docs/index.mdx | 17 +++- docs/remix-cli-guide.html | 26 ++--- docs/styler-api.mdx | 4 +- packages/remix/README.md | 17 +++- packages/remix/example/main.dart | 3 +- packages/remix/lib/remix.dart | 4 +- packages/remix_fortal/README.md | 63 ++++++------ packages/remix_fortal/pubspec.yaml | 30 +----- .../tool/fortal_parity/check.dart | 96 +++---------------- pubspec.yaml | 42 ++++---- skills/using-remix/SKILL.md | 47 +++++---- skills/using-remix/evals/evals.json | 32 +++---- skills/using-remix/references/actions.md | 3 +- skills/using-remix/references/components.md | 4 +- skills/using-remix/references/data-display.md | 3 +- skills/using-remix/references/forms.md | 3 +- skills/using-remix/references/fortal.md | 33 ++++--- skills/using-remix/references/navigation.md | 4 +- skills/using-remix/references/overlays.md | 4 +- tool/check_consumer_mix.dart | 32 +------ tool/check_material_independence.dart | 11 ++- tool/check_open_code.dart | 4 +- tool/check_toolchain.dart | 31 +++--- tool/check_version_alignment.dart | 63 +++--------- tool/sync_registry_remix.dart | 10 +- tool/validate_docs.dart | 32 ++++++- 63 files changed, 431 insertions(+), 522 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index dd0e72e51..92a2a55ef 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,16 +1,13 @@ name: Publish to pub.dev -# Four tag-gated jobs rather than one job listing all packages: the reusable -# publish workflow turns `packages:` into a parallel matrix with no ordering -# knob, and `remix_fortal` depends on hosted `remix` and `remix_ui_icons` -# versions, so a single job would race. +# Three tag-gated jobs keep each package aligned with its configured pub.dev +# automated-publishing tag pattern. # -# THE FOUR PACKAGES PUBLISH FROM DIFFERENT TAG PATTERNS. This is not a style +# THE THREE PACKAGES PUBLISH FROM DIFFERENT TAG PATTERNS. This is not a style # choice — it is each package's "Automated publishing" tag pattern on pub.dev, # and the OIDC token is rejected if the pushed tag does not match: # # remix -> v (bare, e.g. v1.0.0-beta.5) -# remix_fortal -> remix_fortal-v # remix_ui_icons -> remix_ui_icons-v # remix_cli -> remix_cli-v # @@ -25,30 +22,20 @@ name: Publish to pub.dev # publishes, `remix-v` is what melos reads. Only the first triggers # this workflow. # -# The split does not order the two publishes: each tag push starts its own run, -# so `git push --tags` still races — and it can also fire this workflow from -# whatever stale `v*` tags a clone happens to hold. Push the tags one at a -# time, remix first, and wait for pub.dev to serve it before pushing the -# remix_fortal tag. pub.dev does not verify that a dependency version exists, -# so the wrong order ships an uninstallable remix_fortal rather than failing -# the job. -# -# remix_cli is versioned independently of the remix/remix_fortal pair, so its -# tag is pushed on its own schedule and orders against nothing. It depends on -# no other package here — it ships a CLI and a registry of templates — so it -# cannot race the jobs above. +# remix_cli and remix_ui_icons are versioned independently from Remix, so their +# tags are pushed on their own schedules. remix_fortal is not publishable; its +# source remains in the repository as the Fortal preset's analyzed authoring +# and parity surface. on: push: tags: - 'remix_cli-v[0-9]+.[0-9]+.[0-9]+*' - - 'remix_fortal-v[0-9]+.[0-9]+.[0-9]+*' - 'remix_ui_icons-v[0-9]+.[0-9]+.[0-9]+*' - 'v[0-9]+.[0-9]+.[0-9]+*' jobs: publish-remix: - # Bare `v*` only. `refs/tags/remix_fortal-v...` does not start with - # `refs/tags/v`, so the two conditions stay mutually exclusive. + # Bare `v*` only; package-prefixed tags do not start with `refs/tags/v`. if: startsWith(github.ref, 'refs/tags/v') uses: conceptadev/dart-actions/.github/workflows/publish.yml@9075ce1232ec77b8747953f2ff4a349190e5a805 with: @@ -58,16 +45,6 @@ jobs: id-token: write contents: read - publish-remix-fortal: - if: startsWith(github.ref, 'refs/tags/remix_fortal-v') - uses: conceptadev/dart-actions/.github/workflows/publish.yml@9075ce1232ec77b8747953f2ff4a349190e5a805 - with: - packages_folder_path: "packages" - packages: "remix_fortal" - permissions: - id-token: write - contents: read - publish-remix-ui-icons: if: startsWith(github.ref, 'refs/tags/remix_ui_icons-v') uses: conceptadev/dart-actions/.github/workflows/publish.yml@9075ce1232ec77b8747953f2ff4a349190e5a805 diff --git a/.github/workflows/version.yml b/.github/workflows/version.yml index 7cf9a7f24..bff02c187 100644 --- a/.github/workflows/version.yml +++ b/.github/workflows/version.yml @@ -1,13 +1,10 @@ name: Prepare Version Bump -# Pushes a branch that sets both published packages to one explicit version and -# writes their changelogs from conventional commits. It never tags and never -# publishes: `publish.yml` fires on a tag push, so the tag is pushed by hand -# after this PR is reviewed and merged. -# -# The version is a single input rather than one per package. remix and -# remix_fortal ship as a pair and must carry the identical version, so there is -# no per-package value to disagree about. +# Pushes a branch that sets Remix to one explicit version and writes its +# changelog from conventional commits. It never tags and never publishes: +# `publish.yml` fires on a tag push, so the tag is pushed by hand after this PR +# is reviewed and merged. remix_fortal is an unpublished authoring package and +# is deliberately outside this workflow. # # It pushes a branch and prints its compare link rather than opening the pull # request itself. Opening one from a workflow needs "Allow GitHub Actions to @@ -18,7 +15,7 @@ on: workflow_dispatch: inputs: version: - description: "Exact version for BOTH remix and remix_fortal (e.g. 1.0.0-beta.5)." + description: "Exact version for remix (e.g. 1.0.0-beta.8)." required: true permissions: @@ -26,13 +23,13 @@ permissions: jobs: version: - name: Bump versions and changelogs + name: Bump Remix version and changelog runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: - # melos derives each package's last release from its `-v*` - # tag, so the full history and all tags must be present. A shallow + # melos derives Remix's last release from its `remix-v*` tag, so the + # full history and all tags must be present. A shallow # clone makes it walk every commit and rewrite the whole changelog. fetch-depth: 0 @@ -56,15 +53,6 @@ jobs: version='${{ inputs.version }}' - # One input applied to both packages, rather than a version per - # package that a human has to keep in step. remix and remix_fortal - # release in lockstep (tool/check_version_alignment.dart enforces it - # in CI), and melos has no lockstep mode of its own — left to derive - # versions from commits it would bump each package independently and - # split the pair apart. Passing an explicit `-V` for both is what - # keeps them identical. - overrides="-V remix:$version -V remix_fortal:$version" - # No `dart pub global activate melos`. melos runs whichever version # the workspace pins as a dev dependency, so a global install cannot # decide the version and only prints a reassuring, wrong one into the @@ -95,20 +83,24 @@ jobs: # is not what makes this work: melos re-executes itself and the # child does not inherit the redirect, so a redirected stdin alone # still reached the prompt and threw StdinException. - # Scope is explicit so a conventional commit touching remix_cli - # cannot pull that independently versioned package into the lockstep - # Remix/Fortal release branch. + # `-V` makes the workflow input authoritative rather than asking + # melos to infer a version from commits. Scope is explicit so + # independently versioned remix_cli cannot enter the release branch. + # Disable dependent constraint and version propagation too: melos + # otherwise rewrites and can version workspace dependents even when + # the initial package filter selects only Remix, which would pull the + # unpublished remix_fortal authoring package back into releases. dart run melos version \ --scope=remix \ - --scope=remix_fortal \ + --no-dependent-constraints \ + --no-dependent-versions \ --no-git-commit-version \ --yes \ - $overrides < /dev/null + -V "remix:$version" < /dev/null - # melos rewrites the `remix` constraint of every pubspec dependent, - # which is how remix_fortal's pin follows the bump. remix_cli's - # registry.yaml is data, not a pubspec, so melos never reaches it and - # its `remix: ^` would keep pointing at the previous release. + # remix_cli's registry.yaml is data, not a pubspec, so melos never + # reaches it and its `remix: ^` would keep pointing at the + # previous release. # That floor is what the CLI reports as the tested version, so this # step is part of versioning, not a cleanup after it. dart run tool/sync_registry_remix.dart @@ -118,12 +110,12 @@ jobs: # the two committed registry trees cannot disagree. dart run tool/build_fortal_preset.dart - # The pair must land aligned; verify rather than trust the inputs. - # This also fails when the sync step above did not run. + # Verify the released Remix version and registry floor agree. This + # also fails when the sync step above did not run. dart run tool/check_version_alignment.dart if git diff --quiet && git diff --cached --quiet; then - echo "No packages required versioning." + echo "Remix did not require versioning." echo "versioned=false" >> "$GITHUB_OUTPUT" exit 0 fi @@ -133,30 +125,29 @@ jobs: git checkout -b release/version-packages git add -A - git commit -m "chore(release): version packages" + git commit -m "chore(release): version Remix" git push --set-upstream origin release/version-packages --force - name: Summarise if: steps.bump.outputs.versioned == 'true' run: | url="${{ github.server_url }}/${{ github.repository }}/compare/main...release/version-packages?expand=1" + # GitHub expands these workflow expressions before the shell sees + # the intentionally single-quoted Markdown commands below. + # shellcheck disable=SC2016 { - echo "### Versions bumped" + echo "### Remix version bumped" echo echo "[Open the pull request]($url)" echo - echo "Both packages are on \`${{ inputs.version }}\`." + echo "Remix is on \`${{ inputs.version }}\`." echo - echo "After merging, push these tags ONE AT A TIME, in this order." + echo "After merging, push these Remix tags one at a time." echo "Do not \`git push --tags\`: stale \`v*\` tags in this repo" echo "would start publish runs from the wrong commits." echo - echo '1. `v${{ inputs.version }}` — publishes **remix**. Wait until' - echo ' pub.dev actually serves it before step 2; pub.dev does not' - echo ' verify a dependency exists, so publishing remix_fortal' - echo ' early ships an uninstallable package rather than failing.' - echo '2. `remix_fortal-v${{ inputs.version }}` — publishes **remix_fortal**.' - echo '3. `remix-v${{ inputs.version }}` — publishes nothing. melos' + echo '1. `v${{ inputs.version }}` — publishes **remix**.' + echo '2. `remix-v${{ inputs.version }}` — publishes nothing. melos' echo ' reads it to derive the next release changelog.' echo echo "### CI on this branch" diff --git a/README.md b/README.md index 2aeedbe9a..48f449fd8 100644 --- a/README.md +++ b/README.md @@ -217,10 +217,17 @@ final destructiveButton = baseButtonStyle ## Theming with Fortal Remix ships no theme of its own. If you want a polished, Radix Themes-inspired -starting point instead of authoring every style yourself, add the companion -[`remix_fortal`](https://pub.dev/packages/remix_fortal) package — it provides -`FortalScope`, a token system, the `fortal*Style()` recipes, and a matching -catalog of ready-made `Fortal*` widgets. +starting point instead of authoring every style yourself, initialize the +application-owned Fortal preset: + +```bash +flutter pub add dev:remix_cli +dart run remix_cli:remix init --prefix Fortal --preset fortal +dart run remix_cli:remix add button +``` + +It copies `FortalScope`, the token system, `fortal*Style()` recipes, and the +components you add into your application. See the [Fortal documentation](https://docs.page/btwld/remix/fortal) to get started. @@ -233,7 +240,7 @@ Remix composes inside your existing Flutter host. It does not require a | UI | Caller provides | Compatible hosts | |----|-----------------|------------------| | Ordinary `Remix*` widgets | The inherited Flutter services used by the widget subtree | Material, Cupertino, Widgets, and router-based hosts | -| Widgets styled by a theme package such as Fortal | That package's token scope, in addition to the widget's normal Flutter services | Any Flutter host | +| Widgets styled by the Fortal preset | The preset's token scope, in addition to the widget's normal Flutter services | Any Flutter host | | Menu, select, popover, and tooltip | An `Overlay` | Any host exposing an overlay; use `Overlay.wrap` when no `Navigator` is needed | | `showRemixDialog` and `showRemixAlertDialog` | A `Navigator` | Any host with a caller-owned navigator | diff --git a/docs/components/accordion.mdx b/docs/components/accordion.mdx index 67f622921..c649b4ea4 100644 --- a/docs/components/accordion.mdx +++ b/docs/components/accordion.mdx @@ -146,13 +146,13 @@ class _AccordionExampleState extends State { ## Fortal widgets -Remix includes a Fortal-themed widget for this component: +The application-owned Fortal preset provides a themed widget for this component: ```dart import 'package:flutter/material.dart'; import 'package:remix/remix.dart'; -import 'package:remix_fortal/remix_fortal.dart'; +import 'ui/ui.dart'; class FortalAccordionExample extends StatefulWidget { const FortalAccordionExample({super.key}); diff --git a/docs/components/avatar.mdx b/docs/components/avatar.mdx index fc6c42272..8aa3658d3 100644 --- a/docs/components/avatar.mdx +++ b/docs/components/avatar.mdx @@ -77,12 +77,12 @@ class AvatarExample extends StatelessWidget { ## Fortal widgets -Remix includes a Fortal-themed widget for this component: +The application-owned Fortal preset provides a themed widget for this component: ```dart import 'package:flutter/material.dart'; -import 'package:remix_fortal/remix_fortal.dart'; +import 'ui/ui.dart'; class FortalAvatarExample extends StatelessWidget { const FortalAvatarExample({super.key}); diff --git a/docs/components/badge.mdx b/docs/components/badge.mdx index aaa8c1eb0..6068c4580 100644 --- a/docs/components/badge.mdx +++ b/docs/components/badge.mdx @@ -76,12 +76,12 @@ class BadgeExample extends StatelessWidget { ## Fortal widgets -Remix includes a Fortal-themed widget for this component: +The application-owned Fortal preset provides a themed widget for this component: ```dart import 'package:flutter/material.dart'; -import 'package:remix_fortal/remix_fortal.dart'; +import 'ui/ui.dart'; class FortalBadgeExample extends StatelessWidget { const FortalBadgeExample({super.key}); diff --git a/docs/components/button.mdx b/docs/components/button.mdx index a8371679e..62c9f0d39 100644 --- a/docs/components/button.mdx +++ b/docs/components/button.mdx @@ -101,12 +101,12 @@ class ButtonExample extends StatelessWidget { ## Fortal widgets -Remix includes a Fortal-themed widget for this component: +The application-owned Fortal preset provides a themed widget for this component: ```dart import 'package:flutter/material.dart'; -import 'package:remix_fortal/remix_fortal.dart'; +import 'ui/ui.dart'; class FortalButtonExample extends StatelessWidget { const FortalButtonExample({super.key}); diff --git a/docs/components/callout.mdx b/docs/components/callout.mdx index 3fe1c51e9..3a47617db 100644 --- a/docs/components/callout.mdx +++ b/docs/components/callout.mdx @@ -59,12 +59,12 @@ class CalloutExample extends StatelessWidget { ## Fortal widgets -Remix includes a Fortal-themed widget for this component: +The application-owned Fortal preset provides a themed widget for this component: ```dart import 'package:flutter/material.dart'; -import 'package:remix_fortal/remix_fortal.dart'; +import 'ui/ui.dart'; class FortalCalloutExample extends StatelessWidget { const FortalCalloutExample({super.key}); diff --git a/docs/components/card.mdx b/docs/components/card.mdx index f21efcc3b..13881344e 100644 --- a/docs/components/card.mdx +++ b/docs/components/card.mdx @@ -48,12 +48,12 @@ class CardExample extends StatelessWidget { ## Fortal widgets -Remix includes a Fortal-themed widget for this component: +The application-owned Fortal preset provides a themed widget for this component: ```dart import 'package:flutter/material.dart'; -import 'package:remix_fortal/remix_fortal.dart'; +import 'ui/ui.dart'; class FortalCardExample extends StatelessWidget { const FortalCardExample({super.key}); diff --git a/docs/components/checkbox.mdx b/docs/components/checkbox.mdx index e7569ba71..39b6a2ea0 100644 --- a/docs/components/checkbox.mdx +++ b/docs/components/checkbox.mdx @@ -62,12 +62,12 @@ class _CheckboxExampleState extends State { ## Fortal widgets -Remix includes a Fortal-themed widget for this component: +The application-owned Fortal preset provides a themed widget for this component: ```dart import 'package:flutter/material.dart'; -import 'package:remix_fortal/remix_fortal.dart'; +import 'ui/ui.dart'; class FortalCheckboxExample extends StatefulWidget { const FortalCheckboxExample({super.key}); diff --git a/docs/components/checkbox_group.mdx b/docs/components/checkbox_group.mdx index f2c3b4f31..6a0f9e661 100644 --- a/docs/components/checkbox_group.mdx +++ b/docs/components/checkbox_group.mdx @@ -263,7 +263,7 @@ separately as intentionally unmapped at the root: caller-owned ```dart import 'package:flutter/material.dart'; import 'package:remix/remix.dart'; -import 'package:remix_fortal/remix_fortal.dart'; +import 'ui/ui.dart'; class FortalCheckboxGroupExample extends StatefulWidget { const FortalCheckboxGroupExample({super.key}); diff --git a/docs/components/data_list.mdx b/docs/components/data_list.mdx index b5b403b0b..ceee5051c 100644 --- a/docs/components/data_list.mdx +++ b/docs/components/data_list.mdx @@ -59,7 +59,7 @@ minimum, and normal/high-contrast label roles. ```dart import 'package:remix/remix.dart'; -import 'package:remix_fortal/remix_fortal.dart'; +import 'ui/ui.dart'; const accountDetails = FortalDataList( size: FortalDataListSize.size2, diff --git a/docs/components/data_table.mdx b/docs/components/data_table.mdx index 3b40c6b12..7e8a94d93 100644 --- a/docs/components/data_table.mdx +++ b/docs/components/data_table.mdx @@ -37,7 +37,7 @@ and cell editing are out of scope. ```dart import 'package:flutter/material.dart'; import 'package:remix/remix.dart'; -import 'package:remix_fortal/remix_fortal.dart'; +import 'ui/ui.dart'; class Person { const Person(this.id, this.name, this.role); @@ -90,7 +90,7 @@ reorders the rows it was given. ```dart import 'package:flutter/material.dart'; import 'package:remix/remix.dart'; -import 'package:remix_fortal/remix_fortal.dart'; +import 'ui/ui.dart'; class SortableTableExample extends StatefulWidget { const SortableTableExample({super.key, required this.names}); @@ -146,7 +146,7 @@ scoped to the visible page and preserves selections made on other pages. ```dart import 'package:flutter/material.dart'; import 'package:remix/remix.dart'; -import 'package:remix_fortal/remix_fortal.dart'; +import 'ui/ui.dart'; class PagedTableExample extends StatefulWidget { const PagedTableExample({super.key, required this.allRows}); @@ -204,7 +204,7 @@ body rows while the column header and pagination footer stay in place. ```dart import 'package:flutter/material.dart'; import 'package:remix/remix.dart'; -import 'package:remix_fortal/remix_fortal.dart'; +import 'ui/ui.dart'; class ActionCellExample extends StatelessWidget { const ActionCellExample({super.key, required this.rows}); @@ -256,7 +256,7 @@ range without a Material host. `start` and `end` alignments follow ```dart import 'package:flutter/material.dart'; import 'package:remix/remix.dart'; -import 'package:remix_fortal/remix_fortal.dart'; +import 'ui/ui.dart'; class LocalizedTableExample extends StatelessWidget { const LocalizedTableExample({super.key, required this.rows}); diff --git a/docs/components/dialog.mdx b/docs/components/dialog.mdx index d02b543c8..0d5c482ec 100644 --- a/docs/components/dialog.mdx +++ b/docs/components/dialog.mdx @@ -19,7 +19,7 @@ A modal dialog component for focused content and actions. ```dart import 'package:flutter/material.dart'; import 'package:remix/remix.dart'; -import 'package:remix_fortal/remix_fortal.dart'; +import 'ui/ui.dart'; class DialogExample extends StatelessWidget { const DialogExample({super.key}); @@ -66,7 +66,7 @@ configuration is needed. ```dart import 'package:flutter/material.dart'; import 'package:remix/remix.dart'; -import 'package:remix_fortal/remix_fortal.dart'; +import 'ui/ui.dart'; Future showDeleteDialog(BuildContext context) async { await showRemixAlertDialog( @@ -93,7 +93,7 @@ Future showDeleteDialog(BuildContext context) async { ## Fortal widgets -Remix includes a Fortal-themed widget for this component: +The application-owned Fortal preset provides a themed widget for this component: `FortalDialog` defaults to `FortalDialogSize.size3`, `FortalDialogAlign.center`, fills the available width up to 600 logical pixels, @@ -103,7 +103,7 @@ preserves safe viewport insets, and uses `modal: true`. Set ```dart import 'package:flutter/material.dart'; -import 'package:remix_fortal/remix_fortal.dart'; +import 'ui/ui.dart'; class FortalDialogExample extends StatelessWidget { const FortalDialogExample({super.key}); diff --git a/docs/components/disclosure.mdx b/docs/components/disclosure.mdx index 0e06bebae..f772c658d 100644 --- a/docs/components/disclosure.mdx +++ b/docs/components/disclosure.mdx @@ -62,7 +62,7 @@ Omit `expanded` for internal state. `defaultExpanded` only applies then. ```dart import 'package:flutter/material.dart'; import 'package:naked_ui/naked_ui.dart'; -import 'package:remix_fortal/remix_fortal.dart'; +import 'ui/ui.dart'; final disclosure = SizedBox( width: 320, diff --git a/docs/components/divider.mdx b/docs/components/divider.mdx index 8279ca9cc..ed3e1c747 100644 --- a/docs/components/divider.mdx +++ b/docs/components/divider.mdx @@ -42,12 +42,12 @@ class DividerExample extends StatelessWidget { ## Fortal widgets -Remix includes a Fortal-themed widget for this component: +The application-owned Fortal preset provides a themed widget for this component: ```dart import 'package:flutter/material.dart'; -import 'package:remix_fortal/remix_fortal.dart'; +import 'ui/ui.dart'; class FortalDividerExample extends StatelessWidget { const FortalDividerExample({super.key}); diff --git a/docs/components/icon_button.mdx b/docs/components/icon_button.mdx index d3ecf8b24..52d54fbe2 100644 --- a/docs/components/icon_button.mdx +++ b/docs/components/icon_button.mdx @@ -76,12 +76,12 @@ class IconButtonExample extends StatelessWidget { ## Fortal widgets -Remix includes a Fortal-themed widget for this component: +The application-owned Fortal preset provides a themed widget for this component: ```dart import 'package:flutter/material.dart'; -import 'package:remix_fortal/remix_fortal.dart'; +import 'ui/ui.dart'; class FortalIconButtonExample extends StatelessWidget { const FortalIconButtonExample({super.key}); diff --git a/docs/components/link.mdx b/docs/components/link.mdx index d1b413ed5..0b45655c8 100644 --- a/docs/components/link.mdx +++ b/docs/components/link.mdx @@ -102,12 +102,12 @@ link with no callback. ## Fortal widgets -Remix includes a Fortal-themed widget for this component: +The application-owned Fortal preset provides a themed widget for this component: ```dart import 'package:flutter/material.dart'; -import 'package:remix_fortal/remix_fortal.dart'; +import 'ui/ui.dart'; class FortalLinkExample extends StatelessWidget { const FortalLinkExample({super.key}); diff --git a/docs/components/menu.mdx b/docs/components/menu.mdx index b41d2de7e..e514f2aae 100644 --- a/docs/components/menu.mdx +++ b/docs/components/menu.mdx @@ -151,7 +151,7 @@ replace it. ```dart import 'package:flutter/material.dart'; import 'package:remix/remix.dart'; -import 'package:remix_fortal/remix_fortal.dart'; +import 'ui/ui.dart'; class AccountMenuExample extends StatelessWidget { const AccountMenuExample({super.key}); @@ -187,7 +187,7 @@ menu item data, including another submenu. ```dart import 'package:flutter/material.dart'; import 'package:remix/remix.dart'; -import 'package:remix_fortal/remix_fortal.dart'; +import 'ui/ui.dart'; class CompoundMenuExample extends StatefulWidget { const CompoundMenuExample({super.key}); @@ -320,13 +320,13 @@ directional submenu chevron. ## Fortal widgets -Remix includes a Fortal-themed widget for this component: +The application-owned Fortal preset provides a themed widget for this component: ```dart import 'package:flutter/material.dart'; import 'package:remix/remix.dart'; -import 'package:remix_fortal/remix_fortal.dart'; +import 'ui/ui.dart'; class FortalMenuExample extends StatelessWidget { const FortalMenuExample({super.key}); diff --git a/docs/components/popover.mdx b/docs/components/popover.mdx index b49d878a4..142a44201 100644 --- a/docs/components/popover.mdx +++ b/docs/components/popover.mdx @@ -16,7 +16,7 @@ user must address modal content before continuing. ```dart import 'package:flutter/material.dart'; import 'package:remix/remix.dart'; -import 'package:remix_fortal/remix_fortal.dart'; +import 'ui/ui.dart'; class AccountPopover extends StatelessWidget { const AccountPopover({super.key}); diff --git a/docs/components/progress.mdx b/docs/components/progress.mdx index 3fbd229f9..0b52ede49 100644 --- a/docs/components/progress.mdx +++ b/docs/components/progress.mdx @@ -45,12 +45,12 @@ class ProgressExample extends StatelessWidget { ## Fortal widgets -Remix includes a Fortal-themed widget for this component: +The application-owned Fortal preset provides a themed widget for this component: ```dart import 'package:flutter/material.dart'; -import 'package:remix_fortal/remix_fortal.dart'; +import 'ui/ui.dart'; class FortalProgressExample extends StatelessWidget { const FortalProgressExample({super.key}); diff --git a/docs/components/radio.mdx b/docs/components/radio.mdx index c00b1cbc1..cc6230da6 100644 --- a/docs/components/radio.mdx +++ b/docs/components/radio.mdx @@ -112,13 +112,13 @@ class _RadioExampleState extends State { ## Fortal widgets -Remix includes a Fortal-themed widget for this component: +The application-owned Fortal preset provides a themed widget for this component: ```dart import 'package:flutter/material.dart'; import 'package:remix/remix.dart'; -import 'package:remix_fortal/remix_fortal.dart'; +import 'ui/ui.dart'; class FortalRadioExample extends StatefulWidget { const FortalRadioExample({super.key}); diff --git a/docs/components/segmented_control.mdx b/docs/components/segmented_control.mdx index 09f579c47..5a625b056 100644 --- a/docs/components/segmented_control.mdx +++ b/docs/components/segmented_control.mdx @@ -26,7 +26,7 @@ null, and `onChanged` only emits non-null item values. ```dart import 'package:flutter/foundation.dart'; import 'package:remix/remix.dart'; -import 'package:remix_fortal/remix_fortal.dart'; +import 'ui/ui.dart'; FortalSegmentedControl reportingPeriod({ required String value, diff --git a/docs/components/select.mdx b/docs/components/select.mdx index 3e215666a..672cd3627 100644 --- a/docs/components/select.mdx +++ b/docs/components/select.mdx @@ -127,13 +127,13 @@ class _SelectExampleState extends State { ## Fortal widgets -Remix includes a Fortal-themed widget for this component: +The application-owned Fortal preset provides a themed widget for this component: ```dart import 'package:flutter/material.dart'; import 'package:remix/remix.dart'; -import 'package:remix_fortal/remix_fortal.dart'; +import 'ui/ui.dart'; class FortalSelectExample extends StatefulWidget { const FortalSelectExample({super.key}); diff --git a/docs/components/sidebar.mdx b/docs/components/sidebar.mdx index 05c68a6b4..51f0fb0fe 100644 --- a/docs/components/sidebar.mdx +++ b/docs/components/sidebar.mdx @@ -249,7 +249,7 @@ intentionally absent from the generated Radix parity catalog. ```dart import 'package:flutter/foundation.dart'; import 'package:remix/remix.dart'; -import 'package:remix_fortal/remix_fortal.dart'; +import 'ui/ui.dart'; FortalSidebar buildNavigation({ required ValueChanged onSelected, diff --git a/docs/components/skeleton.mdx b/docs/components/skeleton.mdx index 57e8723e8..8e42e4e37 100644 --- a/docs/components/skeleton.mdx +++ b/docs/components/skeleton.mdx @@ -240,7 +240,7 @@ or parent constraints. ```dart import 'package:flutter/material.dart'; import 'package:remix/remix.dart'; -import 'package:remix_fortal/remix_fortal.dart'; +import 'ui/ui.dart'; Widget profileSkeleton(bool loading) { return FortalSkeleton( diff --git a/docs/components/slider.mdx b/docs/components/slider.mdx index 0ab63746d..2b4e5f509 100644 --- a/docs/components/slider.mdx +++ b/docs/components/slider.mdx @@ -74,12 +74,12 @@ class _SliderExampleState extends State { ## Fortal widgets -Remix includes a Fortal-themed widget for this component: +The application-owned Fortal preset provides a themed widget for this component: ```dart import 'package:flutter/material.dart'; -import 'package:remix_fortal/remix_fortal.dart'; +import 'ui/ui.dart'; class FortalSliderExample extends StatefulWidget { const FortalSliderExample({super.key}); diff --git a/docs/components/spinner.mdx b/docs/components/spinner.mdx index a102ffa96..75c0c71bf 100644 --- a/docs/components/spinner.mdx +++ b/docs/components/spinner.mdx @@ -58,12 +58,12 @@ class SpinnerExample extends StatelessWidget { ## Fortal widgets -Remix includes a Fortal-themed widget for this component: +The application-owned Fortal preset provides a themed widget for this component: ```dart import 'package:flutter/material.dart'; -import 'package:remix_fortal/remix_fortal.dart'; +import 'ui/ui.dart'; class FortalSpinnerExample extends StatelessWidget { const FortalSpinnerExample({super.key}); diff --git a/docs/components/switch.mdx b/docs/components/switch.mdx index 158c05149..1d1af1f05 100644 --- a/docs/components/switch.mdx +++ b/docs/components/switch.mdx @@ -84,12 +84,12 @@ class _SwitchExampleState extends State { ## Fortal widgets -Remix includes a Fortal-themed widget for this component: +The application-owned Fortal preset provides a themed widget for this component: ```dart import 'package:flutter/material.dart'; -import 'package:remix_fortal/remix_fortal.dart'; +import 'ui/ui.dart'; class FortalSwitchExample extends StatefulWidget { const FortalSwitchExample({super.key}); diff --git a/docs/components/tabs.mdx b/docs/components/tabs.mdx index 3f74e08b2..4a4929046 100644 --- a/docs/components/tabs.mdx +++ b/docs/components/tabs.mdx @@ -119,13 +119,13 @@ class _TabsExampleState extends State { ## Fortal widgets -Remix includes a Fortal-themed widget for this component: +The application-owned Fortal preset provides a themed widget for this component: ```dart import 'package:flutter/material.dart'; import 'package:remix/remix.dart'; -import 'package:remix_fortal/remix_fortal.dart'; +import 'ui/ui.dart'; class FortalTabsExample extends StatefulWidget { const FortalTabsExample({super.key}); diff --git a/docs/components/textarea.mdx b/docs/components/textarea.mdx index 069040344..13aa0e0b5 100644 --- a/docs/components/textarea.mdx +++ b/docs/components/textarea.mdx @@ -145,7 +145,7 @@ content can still grow within parent constraints. ```dart import 'package:flutter/material.dart'; -import 'package:remix_fortal/remix_fortal.dart'; +import 'ui/ui.dart'; class FortalTextAreaExample extends StatelessWidget { const FortalTextAreaExample({super.key}); diff --git a/docs/components/textfield.mdx b/docs/components/textfield.mdx index 4824f2824..2cc27bcb0 100644 --- a/docs/components/textfield.mdx +++ b/docs/components/textfield.mdx @@ -89,12 +89,12 @@ class _TextfieldExampleState extends State { ## Fortal widgets -Remix includes a Fortal-themed widget for this component: +The application-owned Fortal preset provides a themed widget for this component: ```dart import 'package:flutter/material.dart'; -import 'package:remix_fortal/remix_fortal.dart'; +import 'ui/ui.dart'; class FortalTextFieldExample extends StatefulWidget { const FortalTextFieldExample({super.key}); diff --git a/docs/components/toggle.mdx b/docs/components/toggle.mdx index d3fe3e176..ff2696342 100644 --- a/docs/components/toggle.mdx +++ b/docs/components/toggle.mdx @@ -64,12 +64,12 @@ class _ToggleExampleState extends State { ## Fortal widgets -Remix includes a Fortal-themed widget for this component: +The application-owned Fortal preset provides a themed widget for this component: ```dart import 'package:flutter/material.dart'; -import 'package:remix_fortal/remix_fortal.dart'; +import 'ui/ui.dart'; class FortalToggleExample extends StatefulWidget { const FortalToggleExample({super.key}); diff --git a/docs/components/toggle_group.mdx b/docs/components/toggle_group.mdx index e7bb9c0ac..4a017cfac 100644 --- a/docs/components/toggle_group.mdx +++ b/docs/components/toggle_group.mdx @@ -20,7 +20,7 @@ item is in the Tab order; arrow keys move focus without changing selection. ```dart import 'package:flutter/material.dart'; import 'package:remix/remix.dart'; -import 'package:remix_fortal/remix_fortal.dart'; +import 'ui/ui.dart'; class ViewToggle extends StatefulWidget { const ViewToggle({super.key}); diff --git a/docs/components/tooltip.mdx b/docs/components/tooltip.mdx index 68bb22498..0c3379e4f 100644 --- a/docs/components/tooltip.mdx +++ b/docs/components/tooltip.mdx @@ -92,12 +92,12 @@ class _TriggerButton extends StatelessWidget { ## Fortal widgets -Remix includes a Fortal-themed widget for this component: +The application-owned Fortal preset provides a themed widget for this component: ```dart import 'package:flutter/material.dart'; -import 'package:remix_fortal/remix_fortal.dart'; +import 'ui/ui.dart'; class FortalTooltipExample extends StatelessWidget { const FortalTooltipExample({super.key}); diff --git a/docs/fortal.mdx b/docs/fortal.mdx index ec723c335..d94240b0b 100644 --- a/docs/fortal.mdx +++ b/docs/fortal.mdx @@ -9,23 +9,29 @@ comprehensive set of prebuilt styles based on [Radix Themes 3.3.0](https://www.radix-ui.com/themes), providing a polished, modern UI out of the box while remaining fully customizable. -## Why a separate package +## Why a separate preset -Fortal lives in its own package, `remix_fortal`, so that `remix` stays a +Fortal lives in its own application-owned preset so that `remix` stays a genuinely theme-free component library. If you are building your own design -system on Remix, you pay nothing for a theme you never use — no token tables, no -Radix color data, no parity contract. If you want Fortal, you add one dependency -and get the whole catalog. +system on Remix, you pay nothing for a theme you never use. If you choose +Fortal, the CLI copies its token tables, Radix color data, and recipes into your +application, where they become your source. -`remix_fortal` depends on `remix` and does not re-export it. Import both when you -need base Remix widgets alongside Fortal ones. +The repository keeps `packages/remix_fortal` as the analyzed authoring and +Radix parity surface. Consumer applications do not depend on that package. ## Installation ```bash -flutter pub add remix_fortal +flutter pub add dev:remix_cli +dart run remix_cli:remix init --prefix Fortal --preset fortal +dart run remix_cli:remix add button ``` +The `Fortal` prefix keeps the API names shown throughout this guide. Add each +additional item when you use it, and import your installed `lib/ui/ui.dart` +barrel from application code. + ## Quick Start Wrap your app with `FortalScope` to provide the design tokens, then use the @@ -34,7 +40,7 @@ constructor with `variant:` when the choice is dynamic: ```dart import 'package:flutter/material.dart'; -import 'package:remix_fortal/remix_fortal.dart'; +import 'ui/ui.dart'; class MyApp extends StatelessWidget { @override @@ -82,7 +88,7 @@ Where the outermost scope goes therefore depends on your host: ```dart import 'package:flutter/material.dart'; -import 'package:remix_fortal/remix_fortal.dart'; +import 'ui/ui.dart'; class MyMaterialApp extends StatelessWidget { const MyMaterialApp({super.key}); @@ -125,7 +131,7 @@ recipes directly when you need a custom Remix widget composition: ```dart import 'package:flutter/material.dart'; -import 'package:remix_fortal/remix_fortal.dart'; +import 'ui/ui.dart'; final style = fortalButtonStyle(variant: FortalButtonVariant.solid) .borderRadius(.all(const Radius.circular(8))) @@ -148,7 +154,7 @@ You can use these tokens directly in your custom styles: ```dart import 'package:remix/remix.dart'; -import 'package:remix_fortal/remix_fortal.dart'; +import 'ui/ui.dart'; final style = ButtonStyler() .color(FortalTokens.accent9()) @@ -167,17 +173,16 @@ recipe customization, and the documented parity boundaries. ## Charts -Fortal provides themed line, bar, and pie recipes backed by `mix_chart`. Add -both packages because `remix_fortal` deliberately does not re-export its -dependencies: +Fortal provides themed line, bar, and pie recipes backed by `mix_chart`. Install +the owned chart recipe through the same configured preset: ```bash -flutter pub add mix_chart remix_fortal +dart run remix_cli:remix add chart ``` ```dart import 'package:mix_chart/mix_chart.dart'; -import 'package:remix_fortal/remix_fortal.dart'; +import 'ui/ui.dart'; final chart = FortalLineChart( semanticsLabel: 'Weekly revenue', @@ -200,10 +205,18 @@ widgets. Their matching `fortal*ChartStyle()` recipes can style the underlying ## Icons -Fortal depends on and re-exports the complete 318-glyph Radix Icons 1.3.2 -catalog from `remix_ui_icons`. `FortalIcons` remains a compatibility alias for -`RemixIcons`, so existing references continue to compile and Flutter can -subset the font. There is no name-to-icon map on that class. +Install the optional application-owned icon aliases through the configured +preset: + +```bash +dart run remix_cli:remix add icons +``` + +That item adds `remix_ui_icons` and creates `FortalIcons` with the small alias +set used by your UI layer. Add, rename, or remove those aliases as the +application vocabulary evolves. Import +`package:remix_ui_icons/remix_ui_icons.dart` and use `RemixIcons` directly when +you need the complete 318-glyph Radix Icons 1.3.2 catalog. Catalogs, galleries, and drift tests that must enumerate every glyph can import the opt-in index. An application that never imports it keeps full font diff --git a/docs/fortal/typography.mdx b/docs/fortal/typography.mdx index d926c8fc8..7358d9f3f 100644 --- a/docs/fortal/typography.mdx +++ b/docs/fortal/typography.mdx @@ -10,9 +10,10 @@ Fortal ships five typography families — `FortalText`, `FortalHeading`, size scale (`FortalTextSize`) and one weight enum (`FortalTextWeight`) instead of declaring five parallel copies. -These families live only in `remix_fortal`. There is no `RemixText` or -`RemixHeading`: base Remix stays theme-free, and a `TextStyler` plus your own -tokens is the equivalent there. +These families live only in the application-owned Fortal preset. There is no +`RemixText` or `RemixHeading`: base Remix stays theme-free, and a `TextStyler` +plus your own tokens is the equivalent there. The examples assume the preset +was initialized with `--prefix Fortal` as shown in the [Fortal overview](/fortal). The five Fortal typography families in light mode @@ -60,7 +61,7 @@ scaling tokens, while ordinary Flutter text keeps its nearest inherited style. ```dart import 'package:flutter/material.dart'; -import 'package:remix_fortal/remix_fortal.dart'; +import 'ui/ui.dart'; class TypographyExample extends StatelessWidget { const TypographyExample({super.key}); @@ -125,7 +126,7 @@ the neutral color unchanged — the same gate Radix puts behind ```dart import 'package:flutter/material.dart'; -import 'package:remix_fortal/remix_fortal.dart'; +import 'ui/ui.dart'; class TypographyVariantsExample extends StatelessWidget { const TypographyVariantsExample({super.key}); @@ -163,7 +164,7 @@ can be level 1 at size 6 while an empty-state title stays level 2 at size 3. ```dart import 'package:flutter/material.dart'; -import 'package:remix_fortal/remix_fortal.dart'; +import 'ui/ui.dart'; class HeadingLevelExample extends StatelessWidget { const HeadingLevelExample({super.key}); @@ -222,7 +223,7 @@ idle treatment. ```dart import 'package:flutter/material.dart'; -import 'package:remix_fortal/remix_fortal.dart'; +import 'ui/ui.dart'; class LinkExample extends StatelessWidget { const LinkExample({super.key, required this.onOpenDocs}); @@ -250,9 +251,9 @@ class LinkExample extends StatelessWidget { `linkUrl` is assistive metadata only and is **never launched**. Navigation stays -your responsibility inside `onPressed`, which keeps `remix_fortal` free of a URL -launcher dependency. Passing `linkUrl` without `onPressed` asserts, because an -announced destination with no activation is a broken promise to assistive +your responsibility inside `onPressed`, which keeps the owned recipe free of a +URL launcher dependency. Passing `linkUrl` without `onPressed` asserts, because +an announced destination with no activation is a broken promise to assistive technology. `enabled: false` keeps the callback and still refuses focus and activation, so @@ -269,7 +270,7 @@ Each widget calls a `fortal*Style` recipe you can use directly. `FortalText` and ```dart import 'package:flutter/material.dart'; import 'package:remix/remix.dart'; -import 'package:remix_fortal/remix_fortal.dart'; +import 'ui/ui.dart'; /// Body text on the Fortal scale with one project-specific neutral tone. TextStyler mutedBody() => fortalTextStyle( diff --git a/docs/index.mdx b/docs/index.mdx index 547f634b2..e54ad43b2 100644 --- a/docs/index.mdx +++ b/docs/index.mdx @@ -171,10 +171,17 @@ final destructiveButton = baseButtonStyle ## Theming with Fortal Remix ships no theme of its own. If you want a polished, Radix Themes-inspired -starting point instead of authoring every style yourself, add the companion -[`remix_fortal`](https://pub.dev/packages/remix_fortal) package — it provides -`FortalScope`, a token system, the `fortal*Style()` recipes, and a matching -catalog of ready-made `Fortal*` widgets. +starting point instead of authoring every style yourself, initialize the +application-owned Fortal preset: + +```bash +flutter pub add dev:remix_cli +dart run remix_cli:remix init --prefix Fortal --preset fortal +dart run remix_cli:remix add button +``` + +It copies `FortalScope`, the token system, `fortal*Style()` recipes, and the +components you add into your application. See the [Fortal overview](/fortal) to get started. @@ -186,7 +193,7 @@ Remix composes inside your existing Flutter host. It does not require a | UI | Caller provides | Compatible hosts | |----|-----------------|------------------| | Ordinary `Remix*` widgets | The inherited Flutter services used by the widget subtree | Material, Cupertino, Widgets, and router-based hosts | -| Widgets styled by a theme package such as [Fortal](/fortal) | That package's token scope, in addition to the widget's normal Flutter services | Any Flutter host | +| Widgets styled by the [Fortal preset](/fortal) | The preset's token scope, in addition to the widget's normal Flutter services | Any Flutter host | | Menu, select, popover, and tooltip | An `Overlay` | Any host exposing an overlay; use `Overlay.wrap` when no `Navigator` is needed | | `showRemixDialog` and `showRemixAlertDialog` | A `Navigator` | Any host with a caller-owned navigator | diff --git a/docs/remix-cli-guide.html b/docs/remix-cli-guide.html index a45edc3de..e04bbf443 100644 --- a/docs/remix-cli-guide.html +++ b/docs/remix-cli-guide.html @@ -407,7 +407,7 @@
  • The model
  • What you own
  • The ownership seam
  • -
  • Fortal or open code
  • +
  • Choose a preset
  • The path
  • The six steps
  • 1Install
  • @@ -580,26 +580,26 @@

    The ownership seam

    -

    Fortal or open code

    -

    One decision, made before you install. Both sit on the same Remix widgets.

    +

    Choose a preset

    +

    One application-owned source tree, with the visual language chosen at initialization.

    -

    Fortal is a Radix Themes-inspired preset theme and widget catalog, shipped as the - remix_fortal package. Open code is this CLI.

    +

    Open code includes the minimal default preset and the Radix Themes-inspired Fortal + preset. Both copy editable source into your application and build on the same Remix widgets.

    - + - - - - - + + + + +
    FortalOpen code
    DefaultFortal
    Appearance lives inA package dependencySource files in your lib/
    Normal customizationToken and style overridesEdit the recipe
    Behavioral foundationRemixRemix
    Updatesflutter pub upgradeReview source with --diff
    Best fitA ready-made system you do not intend to changeA design system the application owns
    Initializeremix init --prefix Acmeremix init --prefix Acme --preset fortal
    Visual directionA compact foundation for your own systemA complete Radix-inspired token and recipe system
    Appearance lives inSource files in your lib/Source files in your lib/
    Behavioral foundationRemixRemix
    UpdatesReview source with --diffReview source with --diff
    -

    One app can use both. They are Remix widgets either way, so a Fortal screen and an open-code - screen can sit beside each other.

    +

    The preset is recorded in remix.yaml and cannot change after initialization. + Recipes from different presets do not mix because their tokens and variant vocabularies differ.

    diff --git a/docs/styler-api.mdx b/docs/styler-api.mdx index 873fa502c..0dfa8494e 100644 --- a/docs/styler-api.mdx +++ b/docs/styler-api.mdx @@ -142,8 +142,8 @@ operation without ambiguous overloads. Component stylers are generated from `@MixableSpec`. Preset wrapper classes are generated from `@MixWidget`, which is not Remix-internal: any package that depends on `remix` can annotate a recipe function with it, and the generator -emits a widget calling the target Remix constructor. The -[`remix_fortal`](/fortal) package does exactly this to produce its `Fortal*` +emits a widget calling the target Remix constructor. The application-owned +[`fortal` preset](/fortal) uses exactly this mechanism to produce its prefixed catalog. The annotations identify the target Remix constructor and which recipe parameters become wrapper fields. Generated files remain deterministic build artifacts and should never be edited directly. diff --git a/packages/remix/README.md b/packages/remix/README.md index 82b9714c6..615f8b4b9 100644 --- a/packages/remix/README.md +++ b/packages/remix/README.md @@ -163,10 +163,17 @@ final destructiveButton = baseButtonStyle ## Theming with Fortal Remix ships no theme of its own. If you want a polished, Radix Themes-inspired -starting point instead of authoring every style yourself, add the companion -[`remix_fortal`](https://pub.dev/packages/remix_fortal) package — it provides -`FortalScope`, a token system, the `fortal*Style()` recipes, and a matching -catalog of ready-made `Fortal*` widgets. +starting point instead of authoring every style yourself, initialize the +application-owned Fortal preset: + +```bash +flutter pub add dev:remix_cli +dart run remix_cli:remix init --prefix Fortal --preset fortal +dart run remix_cli:remix add button +``` + +It copies `FortalScope`, the token system, `fortal*Style()` recipes, and the +components you add into your application. See the [Fortal documentation](https://docs.page/btwld/remix/fortal) to get started. @@ -179,7 +186,7 @@ Remix composes inside your existing Flutter host. It does not require a | UI | Caller provides | Compatible hosts | |----|-----------------|------------------| | Ordinary `Remix*` widgets | The inherited Flutter services used by the widget subtree | Material, Cupertino, Widgets, and router-based hosts | -| Widgets styled by a theme package such as Fortal | That package's token scope, in addition to the widget's normal Flutter services | Any Flutter host | +| Widgets styled by the Fortal preset | The preset's token scope, in addition to the widget's normal Flutter services | Any Flutter host | | Menu, select, popover, and tooltip | An `Overlay` | Any host exposing an overlay; use `Overlay.wrap` when no `Navigator` is needed | | `showRemixDialog` and `showRemixAlertDialog` | A `Navigator` | Any host with a caller-owned navigator | diff --git a/packages/remix/example/main.dart b/packages/remix/example/main.dart index a9b1f90f0..329385f9c 100644 --- a/packages/remix/example/main.dart +++ b/packages/remix/example/main.dart @@ -8,7 +8,8 @@ void main() { /// Remix ships no theme: every visual decision below is a styler you author. /// /// If you would rather start from a ready-made Radix Themes-inspired palette, -/// add the companion `remix_fortal` package and wrap your app in `FortalScope`. +/// initialize the `fortal` preset with `remix_cli` and wrap your app in the +/// installed scope. class RemixExampleApp extends StatelessWidget { const RemixExampleApp({super.key}); diff --git a/packages/remix/lib/remix.dart b/packages/remix/lib/remix.dart index 34dc3db0e..13ce84c77 100644 --- a/packages/remix/lib/remix.dart +++ b/packages/remix/lib/remix.dart @@ -1,7 +1,7 @@ /// Remix design system widgets and styles. /// -/// Remix ships no theme. For a ready-made Radix Themes-inspired preset, add the -/// companion `remix_fortal` package. +/// Remix ships no theme. For a ready-made Radix Themes-inspired starting point, +/// initialize the application-owned `fortal` preset with `remix_cli`. library remix; /// COMPONENTS diff --git a/packages/remix_fortal/README.md b/packages/remix_fortal/README.md index 457febaa4..5ac5323e2 100644 --- a/packages/remix_fortal/README.md +++ b/packages/remix_fortal/README.md @@ -1,4 +1,4 @@ -# Fortal +# Fortal authoring package A Radix Themes-inspired preset theme and widget catalog for [Remix](https://pub.dev/packages/remix). @@ -9,18 +9,23 @@ set of prebuilt styles based on [Radix Themes 3.3.0](https://www.radix-ui.com/themes), providing a polished, modern UI out of the box while remaining fully customizable. -Fortal lives in its own package so that `remix` stays genuinely theme-free. If -you are building your own design system on Remix, you pay nothing for a theme you -never use — no token tables, no Radix color data, no parity contract. +This workspace package is no longer released for consumers. It is the +analyzer-checked source of truth for the application-owned `fortal` preset +bundled with `remix_cli`, plus the target of Fortal's tests and Radix parity +checks. Consumer applications copy the source into their own project instead +of depending on `remix_fortal`. ## Installation ```bash -flutter pub add remix_fortal +flutter pub add dev:remix_cli +dart run remix_cli:remix init --prefix Fortal --preset fortal +dart run remix_cli:remix add button ``` -`remix_fortal` depends on `remix` and does not re-export it. Import both when you -need base Remix widgets alongside Fortal ones. +The `Fortal` prefix preserves the API names in this guide. Add each additional +item when you use it, then import the generated `lib/ui/ui.dart` barrel from +application code. ## Quick Start @@ -30,7 +35,7 @@ constructor with `variant:` when the choice is dynamic: ```dart import 'package:flutter/widgets.dart'; -import 'package:remix_fortal/remix_fortal.dart'; +import 'ui/ui.dart'; class MyApp extends StatelessWidget { @override @@ -66,7 +71,7 @@ own root text style below anything wrapping the app. There, put the scope in ```dart import 'package:flutter/material.dart'; -import 'package:remix_fortal/remix_fortal.dart'; +import 'ui/ui.dart'; final app = MaterialApp( builder: (context, child) => FortalScope(child: child!), @@ -90,7 +95,7 @@ recipes directly when you need a custom Remix widget composition: ```dart import 'package:flutter/widgets.dart'; import 'package:remix/remix.dart'; -import 'package:remix_fortal/remix_fortal.dart'; +import 'ui/ui.dart'; final style = fortalButtonStyle(variant: FortalButtonVariant.solid) .borderRadius(.all(const Radius.circular(8))) @@ -100,22 +105,25 @@ final style = fortalButtonStyle(variant: FortalButtonVariant.solid) ## Icons -Fortal depends on and re-exports the complete 318-glyph Radix Icons 1.3.2 -catalog from `remix_ui_icons`. `FortalIcons` remains a compatibility alias for -`RemixIcons`, so existing references continue to compile and Flutter release -builds can subset the font to referenced glyphs: +Install the optional application-owned icon aliases through the configured +preset: + +```bash +dart run remix_cli:remix add icons +``` + +That item adds `remix_ui_icons` and creates a small `FortalIcons` alias set you +can edit as the application's vocabulary evolves: ```dart const Icon(FortalIcons.check) ``` -There is deliberately no runtime name-to-icon map on `RemixIcons` itself -because dynamic lookup would keep the full catalog reachable. Pass -`FortalIcons` constants to any widget that accepts `IconData`; Fortal controls -otherwise use Remix's inline Radix-shaped vector defaults. The font is a -declared `remix_ui_icons` package asset, so applications should measure their -release artifact rather than assume that referencing no catalog constants -removes the asset entirely. +Import `package:remix_ui_icons/remix_ui_icons.dart` and use `RemixIcons` +directly for the complete 318-glyph Radix Icons 1.3.2 catalog. There is +deliberately no runtime name-to-icon map on `RemixIcons` itself because dynamic +lookup would keep the full catalog reachable. Fortal controls otherwise use +Remix's inline Radix-shaped vector defaults. Catalogs, galleries, and drift tests that must enumerate every glyph can import the opt-in index. An application that never imports it keeps full font @@ -144,16 +152,17 @@ light-mode soft labels must meet WCAG AA. ## Charts Fortal includes themed line, bar, and pie chart recipes backed by -[`mix_chart`](https://pub.dev/packages/mix_chart). Add `mix_chart` directly when -constructing chart data; Fortal intentionally does not re-export dependencies. +[`mix_chart`](https://pub.dev/packages/mix_chart). The chart item adds that +dependency automatically; import it directly when constructing chart data +because the application-owned barrel does not re-export it. ```bash -flutter pub add mix_chart remix_fortal +dart run remix_cli:remix add chart ``` ```dart import 'package:mix_chart/mix_chart.dart'; -import 'package:remix_fortal/remix_fortal.dart'; +import 'ui/ui.dart'; final chart = FortalLineChart( semanticsLabel: 'Weekly revenue', @@ -202,7 +211,7 @@ You can use these tokens directly in your own styles: ```dart import 'package:remix/remix.dart'; -import 'package:remix_fortal/remix_fortal.dart'; +import 'ui/ui.dart'; final style = ButtonStyler() .color(FortalTokens.accent9()) @@ -270,7 +279,7 @@ wrapper that calls the corresponding `Remix*` constructor with a `fortal*Style() recipe. Anything you can do with Remix, you can do with Fortal — and you can drop down to plain Remix stylers at any point. -- [Remix on pub.dev](https://pub.dev/packages/remix) +- [Remix CLI on pub.dev](https://pub.dev/packages/remix_cli) - [Documentation](https://docs.page/btwld/remix/fortal) - [GitHub](https://github.com/btwld/remix) diff --git a/packages/remix_fortal/pubspec.yaml b/packages/remix_fortal/pubspec.yaml index c265d2e74..287807507 100644 --- a/packages/remix_fortal/pubspec.yaml +++ b/packages/remix_fortal/pubspec.yaml @@ -1,5 +1,6 @@ name: remix_fortal description: Fortal is a Radix Themes-inspired preset theme and widget catalog built on Remix. +publish_to: none repository: https://github.com/btwld/remix documentation: https://docs.page/btwld/remix/fortal issue_tracker: https://github.com/btwld/remix/issues @@ -12,12 +13,10 @@ topics: - styling - mix -# Pre-release because every dependency below is itself a pre-release; pub warns -# when a stable package is built on beta foundations. Graduate to 0.1.0 when -# remix reaches 1.0.0 stable. +# Retained for workspace package resolution; releases now ship through the +# application-owned Fortal registry in remix_cli. version: 1.0.0-beta.8 -# Consumer floor, matching `remix` and `mix`. See packages/remix/pubspec.yaml. environment: sdk: ">=3.11.0 <4.0.0" flutter: ">=3.41.0" @@ -25,31 +24,12 @@ environment: dependencies: flutter: sdk: flutter - # Tracks the sibling `remix` version automatically: release-please rewrites the - # annotated line below whenever it bumps `remix` (see the `extra-files` entry - # for packages/remix in release-please-config.json). During development pub - # resolves this to the local sibling anyway via `resolution: workspace`. - # - # The floor must name a `remix` that no longer exports Fortal. Releases up to - # and including 1.0.0-beta.1 still bundled FortalScope/FortalTokens/Fortal*, so - # `remix` has to cut its Fortal-free release before `remix_fortal` is published - # — otherwise pub could resolve a `remix` whose symbols collide with this - # package. `tool/fortal_parity/check.dart` (_checkRemixHostedPin) fails if the - # two ever fall out of sync. `melos version` rewrites this line when `remix` - # bumps, because remix_fortal depends on it. + # The workspace resolves this to the sibling source package. The derived + # registry owns the hosted Remix floor installed into consumer applications. remix: ^1.0.0-beta.8 mix: ^2.2.0-beta.5 mix_annotations: ^2.2.0-beta.1 mix_chart: ^0.0.1-beta.1 - # Ranged, not pinned: pub.dev rejects an exact hosted constraint with - # "your dependency should allow more than one version" and the publish - # exits 65. The floor is the release the Fortal parity contract was - # validated against; the exact tested resolution is enforced on the - # workspace lockfile by tool/fortal_parity/check.dart, which also requires - # both published packages to carry this identical constraint. - naked_ui: ^1.0.0-beta.14 - # Publish remix_ui_icons before the next remix_fortal release so this remains a - # valid hosted dependency outside the workspace. remix_ui_icons: ^0.1.0 dev_dependencies: diff --git a/packages/remix_fortal/tool/fortal_parity/check.dart b/packages/remix_fortal/tool/fortal_parity/check.dart index f9bfbfcdf..36fe7cd8d 100644 --- a/packages/remix_fortal/tool/fortal_parity/check.dart +++ b/packages/remix_fortal/tool/fortal_parity/check.dart @@ -6,15 +6,9 @@ const _expectedIntegrity = 'sha512-I0/h2CRNTpYNB7Mi3xFIvSsQq5a108d7kK8dTO5zp5b9HR5QJXKag6B8tjpz2ITkVYkFdkGk45doNkSr7OxwNw=='; const _expectedNakedUiVersion = '1.0.0-beta.14'; -/// A range, not the exact version, because these are two different guarantees -/// enforced in two different places. The parity contract is validated against -/// exactly [_expectedNakedUiVersion], and the *lockfile* check below is what -/// pins that: the workspace must resolve it hosted, byte-exact. The pubspec -/// constraint is the consumer surface, and pub.dev refuses to publish an exact -/// hosted constraint ("your dependency should allow more than one version", -/// exit 65) — an exact pin here made the v1.0.0-beta.7 publish impossible. -/// The floor still names the tested release; both packages must carry this -/// identical string. +/// The parity contract validates one exact hosted Naked UI resolution through +/// Remix. The range is Remix's consumer constraint; the workspace lockfile is +/// the byte-exact tested resolution. const _expectedNakedUiConstraint = '^1.0.0-beta.14'; const _expectedMappedFamilies = { 'avatar', @@ -127,14 +121,12 @@ void main() { final remixPubspec = File( '${workspaceRoot.path}/packages/remix/pubspec.yaml', ); - _checkNakedPin( - pubspec, + _checkNakedResolution( remixPubspec, File('${workspaceRoot.path}/pubspec.yaml'), File('${workspaceRoot.path}/pubspec.lock'), failures, ); - _checkRemixHostedPin(pubspec, remixPubspec, failures); _checkVariantConstructors(packageRoot, failures); _checkApproximations(manifest, failures); _finish(failures); @@ -993,30 +985,24 @@ void _checkFixtures( ); } -void _checkNakedPin( - File packagePubspec, +void _checkNakedResolution( File remixPubspec, File workspacePubspec, File workspaceLock, List failures, ) { - // Both publishable packages must carry the identical hosted constraint, - // whose floor is the release the parity contract was validated against. - // The exact tested resolution is enforced on the lockfile below, not here. + // Remix owns the Naked UI behavior boundary. Its hosted constraint must + // admit the release used by this parity contract, and the workspace lockfile + // below pins that resolution exactly. final pinned = RegExp( '^ naked_ui: ${RegExp.escape(_expectedNakedUiConstraint)}\\s*\$', multiLine: true, ); - for (final entry in { - 'remix_fortal': packagePubspec, - 'remix': remixPubspec, - }.entries) { - _expect( - pinned.hasMatch(entry.value.readAsStringSync()), - '${entry.key} must constrain naked_ui to $_expectedNakedUiConstraint.', - failures, - ); - } + _expect( + pinned.hasMatch(remixPubspec.readAsStringSync()), + 'remix must constrain naked_ui to $_expectedNakedUiConstraint.', + failures, + ); final workspaceSource = workspacePubspec.readAsStringSync(); _expect( !_hasDependencyOverride(workspaceSource, 'naked_ui'), @@ -1042,62 +1028,6 @@ void _checkNakedPin( ); } -/// Guards the publish-order hazard: `remix_fortal` must depend on a *hosted* -/// `remix` whose floor is the version `remix` currently declares. -/// -/// The invariant behind the equality is that the floor must name a `remix` that -/// does **not** export Fortal. `remix` releases up to and including 1.0.0-beta.1 -/// bundled `FortalScope`, `FortalTokens`, and the `Fortal*` widgets, so a floor -/// that admits one of those lets pub resolve a `remix` whose symbols collide -/// with this package's. Keeping the floor pinned to whatever `remix` currently -/// declares means the version bump that removes Fortal also forces this floor -/// forward. -/// -/// `dart pub publish --dry-run` cannot catch this — it validates the pubspec -/// text and packs a tarball without re-resolving against the live registry, so -/// a stale or path-shaped constraint would only fail after `remix_fortal` is -/// already published. -void _checkRemixHostedPin( - File packagePubspec, - File remixPubspec, - List failures, -) { - final source = packagePubspec.readAsStringSync(); - // `melos version` rewrites this floor when `remix` bumps, because - // remix_fortal depends on it. A trailing comment is still tolerated so the - // line can carry a note without failing the contract. - final declared = RegExp( - r'^ remix:\s*\^?([^\s#]+)\s*(?:#.*)?$', - multiLine: true, - ).firstMatch(source); - _expect( - declared != null, - 'remix_fortal must declare remix as a hosted constraint on one line ' - '(no path: or git: entry).', - failures, - ); - if (declared == null) return; - - final remixVersion = RegExp( - r'^version:\s*(\S+)\s*$', - multiLine: true, - ).firstMatch(remixPubspec.readAsStringSync())?.group(1); - if (remixVersion == null) { - failures.add('packages/remix/pubspec.yaml must declare a version.'); - - return; - } - - _expect( - declared.group(1) == remixVersion, - 'remix_fortal must constrain remix to ^$remixVersion to match ' - 'packages/remix/pubspec.yaml (found ^${declared.group(1)}). The floor has ' - 'to name a remix release that no longer exports Fortal, or pub can resolve ' - 'a remix whose Fortal symbols collide with this package.', - failures, - ); -} - bool _hasDependencyOverride(String source, String dependency) { var inDependencyOverrides = false; for (final line in const LineSplitter().convert(source)) { diff --git a/pubspec.yaml b/pubspec.yaml index 3b0e9fd07..454554f15 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -27,10 +27,11 @@ melos: # No `command.bootstrap.environment` here on purpose. It rewrites one # `environment:` into every package it manages, and this workspace has two - # legitimate floors: the published libraries ship code that runs on Mix's - # Flutter 3.41, while the dev toolchain (analyzer, via build_runner) needs - # meta 1.18 and therefore Flutter 3.44. Melos offers no per-package - # exemption, so a single managed value could only be wrong for one group. + # legitimate floors: published libraries and application-owned Fortal source + # run on Mix's Flutter 3.41, while the dev toolchain (analyzer, via + # build_runner) needs meta 1.18 and therefore Flutter 3.44. Melos offers no + # per-package exemption, so a single managed value could only be wrong for + # one group. # tool/check_toolchain.dart owns both floors and enforces them instead. command: @@ -40,16 +41,17 @@ melos: workspaceChangelog: false bootstrap: - # Mix floors are managed here so `remix` and `remix_fortal` cannot drift - # apart. Unlike `environment` above, this is safe to manage centrally: - # melos skips any package that does not already declare the dependency - # (see `_updateDependencies` in melos), so the apps keep resolving Mix - # transitively and never gain a direct dependency. + # Mix floors are managed here so the Fortal authoring source is analyzed + # against the same runtime as Remix. Unlike `environment` above, this is + # safe to manage centrally: melos skips any package that does not already + # declare the dependency (see `_updateDependencies` in melos), so the + # apps keep resolving Mix transitively and never gain a direct dependency. # # Keep these at the versions actually exercised by CI. Declaring an older # floor than we test would claim support for a Mix we never build against. - # tool/check_consumer_mix.dart enforces the same equality for anyone who - # edits a pubspec without bootstrapping. + # tool/check_consumer_mix.dart verifies the hosted Remix consumer + # contract; Fortal registry generation copies its floors from the default + # registry rather than publishing this internal manifest. dependencies: # beta.5 is the floor, not a preference: mix_generator 2.2.0-beta.3 # emits `implements StylerFieldMetadata`, and that symbol first ships @@ -61,14 +63,10 @@ melos: mix_generator: ^2.2.0-beta.3 scripts: - # No `release:*` versioning scripts here on purpose. They used to wrap - # `melos version --prerelease` / `--graduate`, both of which derive a - # version per package from that package's own commits — which is exactly - # the drift `version:align:check` now rejects. Run against this workspace - # they produce `remix 1.0.0-beta.5` and `remix_fortal 0.1.0-beta.4`. - # - # `.github/workflows/version.yml` is the only supported path: it takes one - # version and applies it to both packages with `-V`. + # No `release:*` versioning scripts here on purpose. The version workflow + # is the supported path for the published Remix package; remix_fortal is an + # internal authoring and parity package whose source derives the bundled + # Fortal preset, so release automation must not version it. generate: description: Regenerate Remix sources # remix_fortal's generator resolves package:remix, so it must not build @@ -109,9 +107,7 @@ melos: version:align:check: run: dart run tool/check_version_alignment.dart - description: >- - Verify remix and remix_fortal carry the identical version and the - remix_cli registry floors at it + description: Verify the remix_cli registry floors at the Remix version mix:consumer:check: run: dart run tool/check_consumer_mix.dart @@ -123,7 +119,7 @@ melos: material:check: run: dart run tool/check_material_independence.dart - description: Verify published Remix libraries contain no direct Material usage + description: Verify Remix and Fortal consumer sources contain no direct Material usage open-code:check: steps: diff --git a/skills/using-remix/SKILL.md b/skills/using-remix/SKILL.md index 57f9082cb..a3a5f1e2a 100644 --- a/skills/using-remix/SKILL.md +++ b/skills/using-remix/SKILL.md @@ -2,14 +2,14 @@ name: using-remix description: >- Use when building Flutter UI with the Remix component library or its - companion Fortal theme package (`remix_fortal`): selecting Remix or Fortal, - adding dependencies, placing `FortalScope`, composing overlays or routes, + application-owned Fortal preset: selecting Remix or Fortal, setting up + `remix_cli`, placing the configured theme scope, composing overlays or routes, choosing Remix/Fortal components, or styling `Remix*` widgets with stylers, states, variants, recipes, and tokens. Also trigger when building or auditing a Remix/Fortal reference showcase or component gallery, for Remix/Fortal - widget names, or for a UI request in a project that already depends on - `remix` or `remix_fortal`. Do not trigger for generic Flutter UI work when - neither package is present or requested. + widget names, or for a UI request in a project that already contains + `remix`, `remix.yaml`, or the legacy `remix_fortal` package. Do not trigger + for generic Flutter UI work when none of those are present or requested. --- # Using Remix @@ -18,19 +18,23 @@ Build accessible Flutter interfaces with Remix behavior and either custom styles or Fortal's ready-made Radix-inspired theme. For a new standalone design-system package built on Remix, use `building-remix-design-system`. -## Choose the package layer +## Choose the source layer -Inspect the project's `pubspec.yaml` before assuming Remix is in use. +Inspect the project's `pubspec.yaml` and `remix.yaml` before assuming how Remix +is configured. -| Need | Package and API | +| Need | Source and API | | --- | --- | | Accessible component behavior with a custom visual system | `remix`; use `Remix*` widgets and `*Styler`s | -| Ready-made Radix-inspired visuals | `remix_fortal`; use `FortalScope` and `Fortal*` widgets | -| Fortal tokens with a customized composition | both packages; apply `fortal*Style()` to a `Remix*` widget | -| A visual system unrelated to Fortal | base Remix styling; do not add `remix_fortal` | +| Ready-made Radix-inspired visuals | `remix_cli` with `preset: fortal`; use the configured scope and prefixed widgets | +| Fortal tokens with a customized composition | installed Fortal source plus `remix`; apply the prefixed recipe to a `Remix*` widget | +| A visual system unrelated to Fortal | base Remix styling; do not initialize the Fortal preset | -Remix ships no theme. Fortal is optional, lives in a separate package, depends -on Remix, and does not re-export Remix APIs. +Remix ships no theme. Fortal is optional application-owned source installed by +the CLI. The repository's `remix_fortal` workspace package is its analyzed +authoring and parity surface, not a dependency for new consumer applications. +Preserve an existing legacy `remix_fortal` dependency unless the user asks to +migrate it; do not add that dependency to a new consumer. ## Set up dependencies and imports @@ -45,19 +49,24 @@ import 'package:flutter/widgets.dart'; import 'package:remix/remix.dart'; ``` -For Fortal, add its separate dependency: +For a new Fortal consumer, initialize the preset and add the needed items: ```bash -flutter pub add remix_fortal +flutter pub add dev:remix_cli +dart run remix_cli:remix init --prefix Fortal --preset fortal +dart run remix_cli:remix add button ``` ```dart import 'package:flutter/widgets.dart'; -import 'package:remix_fortal/remix_fortal.dart'; +import 'ui/ui.dart'; ``` -Also import `package:remix/remix.dart` when the file uses `Remix*` widgets, -`*Styler` types, or Remix data classes alongside Fortal. +The examples in this skill use `--prefix Fortal`; honor the prefix already +recorded in `remix.yaml` for an initialized project. Add each recipe before +using it. Also import `package:remix/remix.dart` when the file uses `Remix*` +widgets, `*Styler` types, or Remix data classes that the owned barrel does not +export. ## Place FortalScope correctly @@ -234,7 +243,7 @@ Read only the references needed for the task: ## Verify the result -- Keep imports aligned with the selected package layer. +- Keep imports aligned with the selected source layer and configured prefix. - Confirm Fortal content and route/overlay builders are below `FortalScope`. - Confirm overlays and dialogs have the required caller-owned host capability. - For showcases, verify product examples and exhaustive coverage use their diff --git a/skills/using-remix/evals/evals.json b/skills/using-remix/evals/evals.json index cc69ded4a..8d090dddd 100644 --- a/skills/using-remix/evals/evals.json +++ b/skills/using-remix/evals/evals.json @@ -4,10 +4,10 @@ { "id": 1, "prompt": "Build a settings page with a form that has text fields for name and email, a select dropdown for language preference, some checkboxes for notification options, and a submit button. Use Fortal theming with indigo accent. Make it look polished.", - "expected_output": "A complete Flutter widget importing package:remix_fortal/remix_fortal.dart (plus package:remix/remix.dart only if base Remix types are used), wrapped in FortalScope(accent: .indigo), using named Fortal variant constructors where the variant is fixed, including FortalSelect.surface without an unnecessary explicit generic type or redundant per-item preset styling, plus FortalTextField, FortalCheckbox, and FortalButton preset widgets.", + "expected_output": "A complete Flutter widget using the application-owned Fortal preset with the Fortal prefix, importing its ui/ui.dart barrel (plus package:remix/remix.dart only if base Remix types are used), wrapped in FortalScope(accent: .indigo), using named Fortal variant constructors where the variant is fixed, including FortalSelect.surface without an unnecessary explicit generic type or redundant per-item preset styling, plus FortalTextField, FortalCheckbox, and FortalButton preset widgets. Setup uses remix_cli init --prefix Fortal --preset fortal and adds the required items.", "files": [], "expectations": [ - "Imports remix_fortal and imports remix only if base Remix types are used.", + "Uses the Fortal preset and its owned ui/ui.dart barrel, importing remix only if base Remix types are used.", "Places the UI below FortalScope with the indigo accent.", "Uses FortalTextField, FortalCheckbox, FortalButton, and a named FortalSelect variant without redundant item styling." ] @@ -18,7 +18,7 @@ "expected_output": "A RemixButton with a fully custom ButtonStyler using the fluent API — gradient, borderRadius(.circular(...)), .onPressed with scale, .onHovered with shadow/glow, and .animate(AnimationConfig...) for transitions. Imports only package:remix/remix.dart, since no theme package is needed. Uses ButtonStyler (not the removed RemixButtonStyle) throughout.", "files": [], "expectations": [ - "Imports remix without adding remix_fortal or FortalScope.", + "Imports remix without initializing the Fortal preset or adding FortalScope.", "Uses ButtonStyler gradient, borderRadius(.circular(...)), pressed scale, hovered shadow, and animation APIs.", "Does not use removed RemixButtonStyle or borderRounded APIs." ] @@ -26,10 +26,10 @@ { "id": 3, "prompt": "I'm using FortalIconButton but I need a long-press action and a semantic label on it. How do I do that?", - "expected_output": "Explains that FortalIconButton (from the separate remix_fortal package) forwards the complete RemixIconButton behavior surface, and shows a named variant constructor using both onLongPress and semanticLabel directly, with import 'package:remix_fortal/remix_fortal.dart'; present.", + "expected_output": "Explains that the application-owned FortalIconButton forwards the complete RemixIconButton behavior surface, and shows a named variant constructor using both onLongPress and semanticLabel directly, imported from the installed ui/ui.dart barrel.", "files": [], "expectations": [ - "Imports remix_fortal and uses a named FortalIconButton variant constructor.", + "Imports the owned Fortal barrel and uses a named FortalIconButton variant constructor.", "Passes both onLongPress and semanticLabel directly to FortalIconButton.", "Explains that the Fortal wrapper preserves the RemixIconButton behavior surface." ] @@ -48,12 +48,12 @@ { "id": 5, "prompt": "Can I call showRemixDialog from a subtree that does not have MixScope? I only need a plain Remix dialog, not Fortal tokens.", - "expected_output": "Explains that showRemixDialog works without MixScope and only clones tokens plus modifier order when a current scope exists. Distinguishes the scope-optional route helper from Fortal-styled content, which additionally needs the remix_fortal package and an ambient FortalScope so FortalTokens can resolve.", + "expected_output": "Explains that showRemixDialog works without MixScope and only clones tokens plus modifier order when a current scope exists. Distinguishes the scope-optional route helper from Fortal-styled content, which additionally needs the installed Fortal source and an ambient FortalScope so FortalTokens can resolve.", "files": [], "expectations": [ "States that showRemixDialog works without MixScope.", "States that token and modifier-order cloning occurs only when a current MixScope exists.", - "Distinguishes plain Remix dialog hosting from Fortal content, which needs remix_fortal and FortalScope." + "Distinguishes plain Remix dialog hosting from Fortal content, which needs the installed Fortal preset and FortalScope." ] }, { @@ -70,12 +70,12 @@ { "id": 7, "prompt": "This Flutter app already depends on remix. I want to switch one screen to FortalButton and FortalTextField. Which dependency and imports do I need?", - "expected_output": "Adds remix_fortal as a separate dependency, imports package:remix_fortal/remix_fortal.dart, retains an explicit package:remix/remix.dart import only when the file also uses base Remix widgets, stylers, or data classes, and does not claim that either package re-exports the other.", + "expected_output": "Initializes the application-owned Fortal preset with remix_cli when remix.yaml is absent, adds button and textfield, imports the installed ui/ui.dart barrel, and retains an explicit package:remix/remix.dart import only when the file also uses base Remix widgets, stylers, or data classes. It does not add remix_fortal as a new dependency.", "files": [], "expectations": [ - "Adds remix_fortal as a separate dependency and imports its library.", + "Initializes the Fortal preset, adds the needed items, and imports the owned UI barrel.", "Keeps a remix import only when base Remix APIs are used in the file.", - "Does not claim that remix or remix_fortal re-exports the other package." + "Does not add remix_fortal as a new consumer dependency." ] }, { @@ -103,33 +103,33 @@ { "id": 10, "prompt": "Create Fortal-styled tabs for Overview and Billing. I expected a FortalTabs widget; show the correct composition.", - "expected_output": "Uses RemixTabs as the behavioral root with FortalTabBar, FortalTab, and FortalTabView children, imports both remix and remix_fortal, and explicitly states that no FortalTabs wrapper exists.", + "expected_output": "Uses RemixTabs as the behavioral root with FortalTabBar, FortalTab, and FortalTabView children, imports remix plus the application-owned UI barrel, and explicitly states that no FortalTabs wrapper exists.", "files": [], "expectations": [ "Uses RemixTabs as the behavioral root.", "Uses FortalTabBar, FortalTab, and FortalTabView as styled children.", - "Imports both packages and states that FortalTabs does not exist." + "Imports remix plus the owned Fortal barrel and states that FortalTabs does not exist." ] }, { "id": 11, "prompt": "Our app uses Remix for behavior, but the design is fully custom and must not use Radix-like colors or Fortal tokens. Style a primary button from scratch.", - "expected_output": "Chooses base Remix rather than adding remix_fortal, imports package:remix/remix.dart, creates a custom ButtonStyler with interaction states, and uses RemixButton without FortalScope or Fortal tokens.", + "expected_output": "Chooses base Remix rather than initializing the Fortal preset, imports package:remix/remix.dart, creates a custom ButtonStyler with interaction states, and uses RemixButton without FortalScope or Fortal tokens.", "files": [], "expectations": [ "Uses RemixButton with a custom ButtonStyler and interaction states.", - "Imports remix without adding remix_fortal.", + "Imports remix without initializing the Fortal preset.", "Does not add FortalScope, Fortal tokens, or Radix-like defaults." ] }, { "id": 12, "prompt": "In a plain Flutter app with no Remix dependencies, write a CustomPainter that draws a sparkline.", - "expected_output": "Does not trigger or invoke using-remix, does not add remix or remix_fortal, and answers with ordinary Flutter CustomPainter guidance.", + "expected_output": "Does not trigger or invoke using-remix, does not add Remix or initialize a Fortal preset, and answers with ordinary Flutter CustomPainter guidance.", "files": [], "expectations": [ "Does not invoke using-remix for an unrelated Flutter painting task.", - "Does not add remix or remix_fortal dependencies or imports.", + "Does not add Remix dependencies, remix_cli, or Fortal source.", "Answers with ordinary Flutter CustomPainter guidance." ] }, diff --git a/skills/using-remix/references/actions.md b/skills/using-remix/references/actions.md index 501f6ed0a..ea23c4707 100644 --- a/skills/using-remix/references/actions.md +++ b/skills/using-remix/references/actions.md @@ -1,6 +1,7 @@ # Action Components -Constructor details for Remix buttons and toggles. Fortal widgets mentioned here live in the separate `remix_fortal` package; see [Fortal](fortal.md). +Constructor details for Remix buttons and toggles. Fortal widgets mentioned +here come from the application-owned Fortal preset; see [Fortal](fortal.md). ## Table of Contents diff --git a/skills/using-remix/references/components.md b/skills/using-remix/references/components.md index ed0909d21..74ba64b5e 100644 --- a/skills/using-remix/references/components.md +++ b/skills/using-remix/references/components.md @@ -16,5 +16,5 @@ Every styled leaf `Remix*` widget accepts `style` (a `*Styler`) and such as `RemixTabs`, `RemixRadioGroup`, `RemixCheckboxGroup`, and `RemixAccordionGroup` do not have a styler. -Fortal cross-references name APIs from the separate `remix_fortal` package. -Read [fortal.md](fortal.md) for setup, presets, variants, sizes, and tokens. +Fortal cross-references name APIs from the application-owned Fortal preset. +Read [fortal.md](fortal.md) for setup, variants, sizes, and tokens. diff --git a/skills/using-remix/references/data-display.md b/skills/using-remix/references/data-display.md index 8b48544f1..19e018e6d 100644 --- a/skills/using-remix/references/data-display.md +++ b/skills/using-remix/references/data-display.md @@ -1,6 +1,7 @@ # Data Display Components -Constructor details for Remix data-display widgets. Fortal widgets mentioned here live in the separate `remix_fortal` package; see [Fortal](fortal.md). +Constructor details for Remix data-display widgets. Fortal widgets mentioned +here come from the application-owned Fortal preset; see [Fortal](fortal.md). ## Table of Contents diff --git a/skills/using-remix/references/forms.md b/skills/using-remix/references/forms.md index 0564fdec2..fe5b248a1 100644 --- a/skills/using-remix/references/forms.md +++ b/skills/using-remix/references/forms.md @@ -1,6 +1,7 @@ # Form Components -Constructor details for Remix form controls. Fortal widgets mentioned here live in the separate `remix_fortal` package; see [Fortal](fortal.md). +Constructor details for Remix form controls. Fortal widgets mentioned here come +from the application-owned Fortal preset; see [Fortal](fortal.md). ## Table of Contents diff --git a/skills/using-remix/references/fortal.md b/skills/using-remix/references/fortal.md index bee74d188..dbcc67eae 100644 --- a/skills/using-remix/references/fortal.md +++ b/skills/using-remix/references/fortal.md @@ -1,7 +1,7 @@ # Fortal Theme Reference -Complete reference for Fortal — the Radix-inspired design system that ships as a -separate package on top of Remix: preset widgets, variants, sizes, and tokens. +Complete reference for Fortal — the Radix-inspired, application-owned Remix +preset: widgets, variants, sizes, and tokens. ## Table of Contents @@ -21,22 +21,26 @@ Place `FortalScope` above every subtree that renders Fortal styles. Put it above ## Install and import -Everything on this page lives in `remix_fortal`, not `remix`: +Initialize the Fortal registry and add the items the application uses: ```bash -flutter pub add remix_fortal +flutter pub add dev:remix_cli +dart run remix_cli:remix init --prefix Fortal --preset fortal +dart run remix_cli:remix add button ``` ```dart -import 'package:remix_fortal/remix_fortal.dart'; +import 'ui/ui.dart'; ``` -`remix_fortal` does not re-export `remix`. Import both when a file also uses base -`Remix*` widgets or `*Styler` types. +The examples use the `Fortal` prefix; use the prefix already recorded in +`remix.yaml` for an initialized project. Add every recipe before using it. The +owned barrel does not re-export `remix`, so import `package:remix/remix.dart` +when a file also uses base `Remix*` widgets or `*Styler` types. -Charts also use `mix_chart` data models. Add and import `mix_chart` directly; -Fortal provides the themed recipes and generated wrappers without re-exporting -the chart package. +`dart run remix_cli:remix add chart` installs the chart recipe and its +`mix_chart` dependency. Import `mix_chart` directly for its data models because +the owned barrel does not re-export it. ## Presets and recipes @@ -132,10 +136,11 @@ Notes: - There is no `FortalTabs` — use `RemixTabs` as the behavioral root. - `FortalIconButton` forwards the complete `RemixIconButton` behavior surface, including builders, long press, focus, semantics, and cursor options. -- Generated `FortalButton` does not accept a style override. For Fortal visuals - with custom one-icon placement, use - `RemixButton(style: fortalButtonStyle(...).iconAlignment(.end), ...)`. - With two icons, leading → label → trailing order remains stable. +- Every installed preset widget accepts its component styler through `style`, + merged after the recipe. For custom one-icon placement, use + `FortalButton.solid(style: ButtonStyler().iconAlignment(.end), ...)` or + compose a `RemixButton` from `fortalButtonStyle(...)`. With two icons, + leading → label → trailing order remains stable. - `FortalSelect` and `FortalMenu` both include matching default item styles. Set an individual item's `style` only when that row needs an override. diff --git a/skills/using-remix/references/navigation.md b/skills/using-remix/references/navigation.md index 0a6ccd5fa..fc5a42239 100644 --- a/skills/using-remix/references/navigation.md +++ b/skills/using-remix/references/navigation.md @@ -1,6 +1,8 @@ # Navigation Components -Constructor details for Remix tabs, accordions, and disclosures. Fortal widgets mentioned here live in the separate `remix_fortal` package; see [Fortal](fortal.md). +Constructor details for Remix tabs, accordions, and disclosures. Fortal widgets +mentioned here come from the application-owned Fortal preset; see +[Fortal](fortal.md). ## Components diff --git a/skills/using-remix/references/overlays.md b/skills/using-remix/references/overlays.md index e75b0bb53..7134f0175 100644 --- a/skills/using-remix/references/overlays.md +++ b/skills/using-remix/references/overlays.md @@ -1,6 +1,8 @@ # Overlay Components -Constructor details and host requirements for Remix dialogs, tooltips, and menus. Fortal widgets mentioned here live in the separate `remix_fortal` package; see [Fortal](fortal.md). +Constructor details and host requirements for Remix dialogs, tooltips, and +menus. Fortal widgets mentioned here come from the application-owned Fortal +preset; see [Fortal](fortal.md). ## Table of Contents diff --git a/tool/check_consumer_mix.dart b/tool/check_consumer_mix.dart index d9298facd..659bdf84e 100644 --- a/tool/check_consumer_mix.dart +++ b/tool/check_consumer_mix.dart @@ -3,12 +3,12 @@ import 'dart:io'; import 'package:pub_semver/pub_semver.dart'; import 'package:yaml/yaml.dart'; -/// Publishable packages whose runtime Mix constraints are a consumer contract. +/// Published packages whose runtime Mix constraints are a consumer contract. /// -/// Both are checked because the workspace resolves a single Mix version for -/// everything; a constraint bumped in one package and forgotten in the other -/// still resolves locally and only fails once a consumer installs them. -const _publishedPackages = ['remix', 'remix_fortal']; +/// Fortal is installed as application-owned source and inherits these floors +/// from the Remix registry entry, so only the hosted Remix package belongs in +/// this check. +const _publishedPackages = ['remix']; /// Runtime Mix dependencies a consumer resolves alongside those packages. /// @@ -29,7 +29,6 @@ void main() { final lock = loadYaml(lockfile.readAsStringSync()) as YamlMap; final lockedPackages = lock['packages'] as YamlMap; final failures = []; - final declarations = >{}; for (final package in _publishedPackages) { final pubspecFile = File( @@ -42,16 +41,12 @@ void main() { final pubspec = loadYaml(pubspecFile.readAsStringSync()) as YamlMap; final dependencies = pubspec['dependencies'] as YamlMap; - declarations[package] = {}; - for (final dependency in _runtimeDependencies) { final declared = dependencies[dependency]; if (declared is! String) { failures.add('$package does not declare a $dependency constraint'); continue; } - declarations[package]![dependency] = declared; - final resolved = lockedPackages[dependency]; if (resolved is! YamlMap) { failures.add('$dependency is absent from pubspec.lock'); @@ -82,23 +77,6 @@ void main() { } } - // The published packages ship together, so a floor raised in one and not the - // other is a publishing hazard the single workspace lockfile cannot surface. - for (final dependency in _runtimeDependencies) { - final declared = { - for (final package in _publishedPackages) - if (declarations[package]?[dependency] case final constraint?) - package: constraint, - }; - final distinct = declared.values.toSet(); - if (distinct.length > 1) { - failures.add( - '$dependency constraints diverge across published packages: ' - '${declared.entries.map((e) => '${e.key} ${e.value}').join(', ')}', - ); - } - } - if (failures.isEmpty) return; stderr.writeln('Mix consumer validation failed:'); diff --git a/tool/check_material_independence.dart b/tool/check_material_independence.dart index ea4e5e4b9..96f36e594 100644 --- a/tool/check_material_independence.dart +++ b/tool/check_material_independence.dart @@ -1,6 +1,8 @@ import 'dart:io'; -const _publishedPackages = ['remix', 'remix_fortal']; +// Fortal stays here because these sources are copied into consumer apps even +// though the authoring package itself is no longer published. +const _consumerSourcePackages = ['remix', 'remix_fortal']; final _forbiddenLibraryDirective = RegExp( r'''^\s*(?:import|export)\s+['"]package:(?:flutter/(?:material\.dart|src/material/[^'"]+)|material_ui/[^'"]+)['"]''', @@ -21,7 +23,7 @@ void main() { final workspace = Directory.current.absolute; final failures = []; - for (final package in _publishedPackages) { + for (final package in _consumerSourcePackages) { final packageDirectory = Directory('${workspace.path}/packages/$package'); final libraryDirectory = Directory('${packageDirectory.path}/lib'); final pubspec = File('${packageDirectory.path}/pubspec.yaml'); @@ -55,7 +57,10 @@ void main() { } if (failures.isEmpty) { - stdout.writeln('Published Remix libraries have no direct Material usage.'); + stdout.writeln( + 'Remix and application-owned Fortal sources have no direct Material ' + 'usage.', + ); return; } diff --git a/tool/check_open_code.dart b/tool/check_open_code.dart index de3f0e2d4..34f71dc3a 100644 --- a/tool/check_open_code.dart +++ b/tool/check_open_code.dart @@ -383,8 +383,8 @@ Future<_Failure?> _checkInTemporaryApp({ // at. So between the version pull request merging and its `v` tag // being served, this phase cannot resolve the new floor and fails on that // commit. That window is expected and self-heals once pub.dev serves the - // tag; `remix_fortal`'s hosted pin lives with the identical one. The fix is - // to push the tag, never to teach this checker to probe pub.dev. + // tag. The fix is to push the tag, never to teach this checker to probe + // pub.dev. final addCli = await _runProcess( sdk.dart, ['pub', 'add', 'dev:remix_cli@{path: ${cliRoot.path}}'], diff --git a/tool/check_toolchain.dart b/tool/check_toolchain.dart index 1dc22d4b4..db4269cc3 100644 --- a/tool/check_toolchain.dart +++ b/tool/check_toolchain.dart @@ -2,21 +2,20 @@ import 'dart:io'; import 'package:yaml/yaml.dart'; -/// The floor the published libraries declare to consumers. +/// The floor required by code that reaches consumer applications. /// /// Matches `mix` and `naked_ui` so the three can be installed together on the -/// oldest Flutter any of them supports. It is the true minimum of the *shipped* -/// code: `packages/remix` and `packages/remix_fortal` analyze clean against it, -/// and their runtime dependencies all allow it. -const _publishedFloor = {'sdk': '>=3.11.0 <4.0.0', 'flutter': '>=3.41.0'}; +/// oldest Flutter any of them supports. It is the true minimum of the shipped +/// package code and the Fortal source copied by the CLI. +const _consumerFloor = {'sdk': '>=3.11.0 <4.0.0', 'flutter': '>=3.41.0'}; /// The floor everything workspace-only declares. /// -/// Higher than [_publishedFloor] because the dev toolchain, not the shipped +/// Higher than [_consumerFloor] because the dev toolchain, not the shipped /// code, is what needs a newer SDK: `build_runner` pulls `analyzer`, which /// requires `meta ^1.18.0`, and Flutter ships meta 1.18 only from 3.44. Raising -/// the published packages to match would exclude consumers for a constraint -/// that never reaches them, since dev dependencies are not published. +/// consumer-facing code to match would exclude applications for a constraint +/// that never reaches them. const _workspaceFloor = {'sdk': '>=3.12.0 <4.0.0', 'flutter': '>=3.44.0'}; /// The floor for pure-Dart workspace tools. @@ -25,9 +24,9 @@ const _workspaceFloor = {'sdk': '>=3.12.0 <4.0.0', 'flutter': '>=3.44.0'}; /// consumer constraint merely because they live beside Flutter packages. const _pureDartFloor = {'sdk': '>=3.12.0 <4.0.0'}; -/// Packages whose `environment` is the consumer contract rather than a -/// development detail. Every other workspace member gets [_workspaceFloor]. -const _publishedPackages = { +/// Packages whose sources reach consumers, either through pub.dev or the +/// application-owned Fortal preset. Every other member gets [_workspaceFloor]. +const _consumerFloorPackages = { 'packages/remix', 'packages/remix_fortal', 'packages/remix_ui_icons', @@ -62,7 +61,7 @@ void main() { return; } - final unknown = _publishedPackages.difference(members.toSet()); + final unknown = _consumerFloorPackages.difference(members.toSet()); final unknownPureDart = _pureDartPackages.difference(members.toSet()); if (unknown.isNotEmpty || unknownPureDart.isNotEmpty) { stderr.writeln( @@ -134,18 +133,18 @@ void main() { } stdout.writeln( - 'Toolchain floors are consistent: ${_publishedPackages.length} published ' - 'packages on Flutter ${_publishedFloor['flutter']}, ' + 'Toolchain floors are consistent: ${_consumerFloorPackages.length} ' + 'consumer-source packages on Flutter ${_consumerFloor['flutter']}, ' '${_pureDartPackages.length} pure-Dart tool on Dart ' '${_pureDartFloor['sdk']}, and ' - '${members.length + 1 - _publishedPackages.length - _pureDartPackages.length} ' + '${members.length + 1 - _consumerFloorPackages.length - _pureDartPackages.length} ' 'workspace-only packages ' 'on Flutter ${_workspaceFloor['flutter']}.', ); } Map _expectedFloor(String relativePath) { - if (_publishedPackages.contains(relativePath)) return _publishedFloor; + if (_consumerFloorPackages.contains(relativePath)) return _consumerFloor; if (_pureDartPackages.contains(relativePath)) return _pureDartFloor; return _workspaceFloor; } diff --git a/tool/check_version_alignment.dart b/tool/check_version_alignment.dart index 8011013d6..63ed8593b 100644 --- a/tool/check_version_alignment.dart +++ b/tool/check_version_alignment.dart @@ -3,18 +3,8 @@ import 'dart:io'; import 'package:pub_semver/pub_semver.dart'; import 'package:yaml/yaml.dart'; -/// Published packages that must always carry the identical version. -/// -/// `remix_fortal` is a theme layer over `remix` and the two are only ever -/// tested against each other, so a released pair is the unit consumers -/// install. Sharing one version makes that pairing legible from the version -/// alone instead of requiring a changelog lookup. -/// -/// Melos has no lockstep mode: it versions each package from its own commits, -/// and the propagation is one-way (a `remix` bump reaches `remix_fortal` as a -/// dependent, never the reverse). So a `remix_fortal`-only release would drift -/// the pair apart with nothing to stop it. This checker is that stop. -const _lockstepPackages = ['remix', 'remix_fortal']; +/// Published Remix manifest whose version owns the registry floor. +const _remixPubspecPath = 'packages/remix/pubspec.yaml'; /// The registry file whose `remix` constraint must floor at the released /// `remix` version. @@ -23,50 +13,25 @@ const _registryPath = void main() { final workspaceRoot = Directory.current.absolute; - final versions = {}; final failures = []; - - for (final package in _lockstepPackages) { - final pubspecFile = File( - '${workspaceRoot.path}/packages/$package/pubspec.yaml', - ); - if (!pubspecFile.existsSync()) { - failures.add('packages/$package/pubspec.yaml is missing'); - continue; - } - - final pubspec = loadYaml(pubspecFile.readAsStringSync()) as YamlMap; + final remixPubspec = File('${workspaceRoot.path}/$_remixPubspecPath'); + Version? remixVersion; + if (!remixPubspec.existsSync()) { + failures.add('$_remixPubspecPath is missing'); + } else { + final pubspec = loadYaml(remixPubspec.readAsStringSync()) as YamlMap; final declared = pubspec['version']; if (declared is! String) { - failures.add('$package does not declare a version'); - continue; - } - - try { - versions[package] = Version.parse(declared); - } on FormatException { - failures.add('$package declares an unparseable version "$declared"'); - } - } - - if (versions.length == _lockstepPackages.length) { - final distinct = versions.values.toSet(); - if (distinct.length > 1) { - failures.add( - 'lockstep packages disagree: ' - '${versions.entries.map((e) => '${e.key} ${e.value}').join(', ')}. ' - 'Release them on one version — pass the same value to both ' - '`remix_version` and `remix_fortal_version` in the version workflow.', - ); + failures.add('remix does not declare a version'); } else { - stdout.writeln( - 'Lockstep versions aligned: ' - '${_lockstepPackages.join(', ')} all on ${distinct.single}.', - ); + try { + remixVersion = Version.parse(declared); + } on FormatException { + failures.add('remix declares an unparseable version "$declared"'); + } } } - final remixVersion = versions['remix']; if (remixVersion != null) { _checkRegistryFloor(workspaceRoot, remixVersion, failures); } diff --git a/tool/sync_registry_remix.dart b/tool/sync_registry_remix.dart index 819375a50..74aa0ab76 100644 --- a/tool/sync_registry_remix.dart +++ b/tool/sync_registry_remix.dart @@ -9,12 +9,10 @@ import 'package:yaml/yaml.dart'; /// dart run tool/sync_registry_remix.dart /// ``` /// -/// `melos version` rewrites the `remix` constraint of every pubspec dependent, -/// which is how `remix_fortal`'s parity pin stays in step. The registry is -/// data, not a pubspec, so melos never reaches it — this tool is the step that -/// does. `tool/check_version_alignment.dart` is the checker that fails when it -/// has not been run; the two are deliberately separate, because no `check_*` -/// tool in this workspace writes. +/// The registry is data, not a pubspec, so `melos version` never reaches it. +/// This tool is the explicit write step; `tool/check_version_alignment.dart` +/// is the checker that fails when it has not been run. The two stay separate +/// because no `check_*` tool in this workspace writes. void main(List arguments) { if (arguments.isNotEmpty) { stderr.writeln('Unsupported arguments: ${arguments.join(', ')}'); diff --git a/tool/validate_docs.dart b/tool/validate_docs.dart index 67cf7318a..911cdf858 100644 --- a/tool/validate_docs.dart +++ b/tool/validate_docs.dart @@ -132,7 +132,11 @@ final _iconButtonInvocation = RegExp( final _remixImport = RegExp( r'''import\s+['"]package:(?:remix|remix_fortal)/(?:remix|remix_fortal)\.dart['"]\s*;''', ); +final _applicationOwnedFortalImport = RegExp( + r'''import\s+['"]ui/ui\.dart['"]\s*;''', +); final _remixApiReference = RegExp(r'\b(?:Remix|Fortal)[A-Z]\w*'); +final _fortalApiReference = RegExp(r'\bFortal[A-Z]\w*'); const _exampleSourceDirectories = [ 'apps/dashboard/lib', @@ -355,9 +359,17 @@ Future main() async { try { for (final (index, snippet) in snippets.indexed) { final file = File('${tempRoot.path}/snippet_$index.dart'); + // Fortal docs show the barrel an initialized application owns. The + // temporary validation directory has no application package, so map only + // that import to the analyzer-checked authoring package. The Fortal + // derivation round trip separately proves that the prefixed APIs match. + final validationSource = snippet.source.replaceAll( + _applicationOwnedFortalImport, + "import 'package:remix_fortal/remix_fortal.dart';", + ); file.writeAsStringSync( '// Generated temporarily by tool/validate_docs.dart.\n' - '${snippet.source}\n', + '$validationSource\n', ); } @@ -819,14 +831,26 @@ _extractAnalyzableSnippets( final source = file.readAsStringSync(); for (final (index, match) in fence.allMatches(source).indexed) { final snippet = match.group(1)!; - final importsRemix = _remixImport.hasMatch(snippet); + final importsApplicationUi = _applicationOwnedFortalImport.hasMatch( + snippet, + ); + // Default-preset snippets use the same application-relative barrel but + // have no package source to analyze against here. Fortal snippets use a + // known prefix and can be mapped to the authoring package byte-for-byte. + if (importsApplicationUi && !_fortalApiReference.hasMatch(snippet)) { + skipped += 1; + continue; + } + final importsRemix = + _remixImport.hasMatch(snippet) || importsApplicationUi; if (!importsRemix) { skipped += 1; if (_remixApiReference.hasMatch(snippet)) { failures.add( '$relativePath Dart example ${index + 1} uses Remix or Fortal APIs ' - 'but imports neither package:remix/remix.dart nor ' - 'package:remix_fortal/remix_fortal.dart.', + 'but imports neither package:remix/remix.dart, ' + 'package:remix_fortal/remix_fortal.dart, nor the application-owned ' + 'ui/ui.dart barrel.', ); } continue; From 0c743bee63f00ae58f047ad86c72aed567ac2027 Mon Sep 17 00:00:00 2001 From: Leo Farias Date: Fri, 4 Sep 2026 15:26:44 -0400 Subject: [PATCH 06/12] fix(remix_cli): validate preset consumers before release --- .github/workflows/publish.yml | 22 +- .github/workflows/version.yml | 10 +- docs/fortal.mdx | 3 + docs/open-code.mdx | 18 ++ docs/remix-cli-guide.html | 6 +- open_code/CLEAN_SHEET.md | 11 +- open_code/PRESETS.md | 74 ++++--- open_code/README.md | 29 ++- open_code/RELEASING.md | 95 +++++++++ packages/remix/CHANGELOG.md | 2 + packages/remix_cli/CHANGELOG.md | 5 +- packages/remix_cli/README.md | 12 ++ .../lib/src/registry/fortal/registry.yaml | 2 +- packages/remix_cli/test/installer_test.dart | 29 +++ pubspec.yaml | 16 +- skills/using-remix/SKILL.md | 4 + test/tool/check_open_code_test.dart | 57 +++++ tool/check_open_code.dart | 196 ++++++++++++------ 18 files changed, 473 insertions(+), 118 deletions(-) create mode 100644 open_code/RELEASING.md diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 92a2a55ef..fdf9a50d3 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -22,8 +22,9 @@ name: Publish to pub.dev # publishes, `remix-v` is what melos reads. Only the first triggers # this workflow. # -# remix_cli and remix_ui_icons are versioned independently from Remix, so their -# tags are pushed on their own schedules. remix_fortal is not publishable; its +# remix_cli and remix_ui_icons are versioned independently from Remix. +# The CLI consumer check requires its bundled Remix version to exist before +# the CLI can publish. remix_fortal is not publishable; its # source remains in the repository as the Fortal preset's analyzed authoring # and parity surface. on: @@ -34,6 +35,22 @@ on: - 'v[0-9]+.[0-9]+.[0-9]+*' jobs: + verify-cli-consumers: + if: startsWith(github.ref, 'refs/tags/remix_cli-v') + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + - uses: subosito/flutter-action@v2 + with: + flutter-version: '3.44.0' + cache: true + - name: Resolve the workspace + run: flutter pub get + - name: Verify both presets with hosted Remix + run: dart run melos run open-code:release:check + publish-remix: # Bare `v*` only; package-prefixed tags do not start with `refs/tags/v`. if: startsWith(github.ref, 'refs/tags/v') @@ -57,6 +74,7 @@ jobs: publish-remix-cli: if: startsWith(github.ref, 'refs/tags/remix_cli-v') + needs: verify-cli-consumers uses: conceptadev/dart-actions/.github/workflows/publish.yml@9075ce1232ec77b8747953f2ff4a349190e5a805 with: packages_folder_path: "packages" diff --git a/.github/workflows/version.yml b/.github/workflows/version.yml index bff02c187..3b3c49842 100644 --- a/.github/workflows/version.yml +++ b/.github/workflows/version.yml @@ -152,11 +152,11 @@ jobs: echo echo "### CI on this branch" echo - echo 'This branch also moves the `remix` floor in the' - echo '`remix_cli` registry. `open-code:check` resolves that floor' - echo 'from pub.dev, so it fails until step 1 above is served.' - echo 'Expect a red run before the tag and a green one after; do' - echo 'not lower the floor to make it pass.' + echo 'CI installs both presets against checkout Remix.' + echo 'After pub.dev serves step 1, run' + echo '`dart run melos run open-code:release:check`.' + echo 'The CLI publish workflow requires this hosted check.' + echo 'Do not lower the registry floor to bypass a missing release.' echo echo "### remix_cli" echo diff --git a/docs/fortal.mdx b/docs/fortal.mdx index d94240b0b..3281d0eec 100644 --- a/docs/fortal.mdx +++ b/docs/fortal.mdx @@ -22,6 +22,9 @@ Radix parity surface. Consumer applications do not depend on that package. ## Installation +The commands below apply after the first CLI release and Remix beta.8. +For checkout use, follow the [Open Code installation instructions](/open-code#install). + ```bash flutter pub add dev:remix_cli dart run remix_cli:remix init --prefix Fortal --preset fortal diff --git a/docs/open-code.mdx b/docs/open-code.mdx index 70bb6e411..39c2c939d 100644 --- a/docs/open-code.mdx +++ b/docs/open-code.mdx @@ -36,6 +36,24 @@ vocabularies are different. ## Install +Before the first release, add the CLI from a checkout: + +```bash +dart pub add "dev:remix_cli@{path: /path/to/remix/packages/remix_cli}" +``` + +Until Remix beta.8 is published, add this temporary `pubspec_overrides.yaml` +to the application. Replace the path with your checkout: + +```yaml +dependency_overrides: + remix: + path: /path/to/remix/packages/remix +``` + +After both releases are available, remove this override and use the hosted +installation below. Both presets require Remix `^1.0.0-beta.8`. + Add the CLI as a project-local dev dependency so its version is pinned in your lockfile, not in a machine-global install: diff --git a/docs/remix-cli-guide.html b/docs/remix-cli-guide.html index e04bbf443..f8f0842d6 100644 --- a/docs/remix-cli-guide.html +++ b/docs/remix-cli-guide.html @@ -638,7 +638,11 @@

    Install the CLI

    This walkthrough uses one command name all the way through: remix.

    -

    Install the exact CLI version this guide was written against. If Dart prints a +

    The CLI has not been published. The hosted commands below apply after its + first release and Remix beta.8. For now, follow the + checkout installation instructions.

    + +

    After publication, install the exact CLI version this guide was written against. If Dart prints a PATH instruction, follow it and reopen the shell before you continue.

    diff --git a/open_code/CLEAN_SHEET.md b/open_code/CLEAN_SHEET.md index 45d752373..444577d7f 100644 --- a/open_code/CLEAN_SHEET.md +++ b/open_code/CLEAN_SHEET.md @@ -131,10 +131,9 @@ The recurring costs are visible: Its separate source tree, derivation contract, and publication transition are recorded in [Presets: Fortal as application-owned source](PRESETS.md). There is still no general component marketplace, remote registry, automatic migration, -source lockfile, or public-release automation for `remix_cli`. +or source lockfile. The CLI now has a tag-based publish workflow with a +hosted consumer check. -The first public release has a separate gate: confirm the pub.dev package and -publisher, add an isolated tag contract, verify bundled assets from a staged -archive in a clean Pub cache, publish a prerelease, and repeat the flow from the -hosted package. Until then, project-local checkout or staged-package use is the -honest supported path. +The first public release still requires package bootstrap and hosted CLI +verification. Until then, use a checkout or staged package with checkout Remix. +The [release instructions](RELEASING.md) define the order and required checks. diff --git a/open_code/PRESETS.md b/open_code/PRESETS.md index 6bbe1dd56..b806af196 100644 --- a/open_code/PRESETS.md +++ b/open_code/PRESETS.md @@ -1,6 +1,6 @@ # Presets: Fortal as application-owned source -**Status:** Implemented +**Status:** Implemented; publication pending **Base branch:** `feat/open-code` ## The problem @@ -13,8 +13,8 @@ package. It cannot own the source the way it owns the default recipes. This document designs the change that lets an application run `remix init --preset fortal` and then `remix add button`, and receive editable -Fortal source under its own prefix. It is organized as a stack of pull -requests against `feat/open-code`. Each pull request is green on its own. +Fortal source under its own prefix. The final PR stack is Open Code, then Fortal, then the existing Sidebar PR. +The Fortal PR preserves the five implementation commits described below. ## Decisions @@ -50,16 +50,15 @@ with the reason, so a reviewer does not reopen it by accident. 6. **`mix_chart` stays a hosted dependency of the `chart` item.** It is the chart engine the recipe styles, as `remix` is the button engine. The default `chart` item already declares it. -7. **`remix_fortal` leaves pub.dev once the preset works; the directory +7. **`remix_fortal` leaves pub.dev after the published replacement works; the directory stays.** Hosted Fortal has no consumer base to protect: 0 likes and 438 downloads at `1.0.0-beta.7`. pub.dev cannot delete a package, so the package is marked discontinued with `remix init --preset fortal` named as - the replacement, after PR 4 proves that path. The directory + the replacement, after Commit 4 proves that path. The directory `packages/remix_fortal` is not deleted. It is the authored source the templates derive from, the target of 50 test files and the parity checker, and a dependency of `apps/dashboard`, `apps/demo`, and - `apps/playground`. Drift between it and the derived templates is not a - risk: `--check` fails CI on any byte of difference, the same way + `apps/playground`. The source and template check detects byte drift: `--check` fails CI on any byte of difference, the same way `docs/fortal/catalog.mdx` and the playground dogfood are checked today. ## What the application gets @@ -208,19 +207,29 @@ item graph is correct by construction: committed files, and fails on any difference. `melos run open-code:fortal:check` runs it in CI, the way `docs:check` runs `generate_fortal_catalog.dart`. -## The stack +## The PR stack -Five pull requests. The first two touch only `remix_fortal`. The third -touches only `remix_cli` and the repository tools, and can be reviewed in -parallel with the first two. The fourth depends on all three. The fifth is a -product step with an external side effect, so it stays separate and last. +1. `feat/open-code` targets `main` in PR #177. +2. `feat/fortal-presets` targets `feat/open-code` and contains the Fortal work. +3. `feat/open-code-sidebar` targets `feat/fortal-presets` in PR #178. + +Merge in that order. The original plan proposed five Fortal PRs. The final +stack keeps those implementation boundaries as five commits in one Fortal PR. +The Sidebar PR remains separate because it adds a default recipe. + +## Implementation commits + +The first two commits prepare Fortal source. The third adds preset selection. +The fourth adds the derived preset and its consumer fixture. The fifth removes +Fortal publication from repository automation. Package discontinuation remains +a later release action. Two earlier drafts were folded in. The fresh-app proof is part of the PR that ships the Fortal preset, because a preset without its proof is not done. Docs land with the PR they describe, because `validate_docs.dart` already checks them and a flag without docs is not done either. -### PR 1: `refactor(remix_fortal)`: lay the source out like an installed preset +### Commit 1: `refactor(remix_fortal)`: lay the source out like an installed preset A pure move. Git sees renames, reviewers see paths, and no behavior changes. This goes first so every later content diff lands on the final paths. @@ -253,7 +262,7 @@ This goes first so every later content diff lands on the final paths. file in the repository outside `.context/` contains `src/recipes/` or `src/fortal/`. -### PR 2: `feat(remix_fortal)`: instance style override, and only `remix` imports +### Commit 2: `feat(remix_fortal)`: instance style override, and only `remix` imports Content edits on the final paths. Every default component template takes a styler parameter merged last. No Fortal recipe does. @@ -267,7 +276,7 @@ styler parameter merged last. No Fortal recipe does. - Remove the four unused `naked_ui` imports (`accordion`, `disclosure`, `slider`, `tabs`; verified unused with the analyzer) and the four direct `mix` imports (`chart`, `base_button`, `computed`, and the theme files). - `remix` re-exports `mix` in full. This is a precondition for PR 4, whose + `remix` re-exports `mix` in full. This is a precondition for Commit 4, whose tool refuses those imports. - Regenerate the `.g.dart` parts. The generator adds `style` to every named constructor. @@ -277,11 +286,11 @@ styler parameter merged last. No Fortal recipe does. a sibling. - Proof: `fortal:parity:check` and the package test suite. -### PR 3: `feat(remix_cli)`: preset axis +### Commit 3: `feat(remix_cli)`: preset axis The CLI learns that more than one registry exists. Only `default` ships in this PR, so behavior for every existing project is unchanged. This is -independent of PR 1 and PR 2. +independent of Commit 1 and Commit 2. - Move `lib/src/registry/registry.yaml` and `templates/` to `lib/src/registry/default/`. @@ -314,9 +323,9 @@ independent of PR 1 and PR 2. file and the `--preset` flag, and drops the words "no presets". `packages/remix_cli/CHANGELOG.md`. -### PR 4: `feat(remix_cli)`: the Fortal preset, with its proof +### Commit 4: `feat(remix_cli)`: the Fortal preset, with its proof -Depends on PR 1, 2, and 3. The diff is mostly derived content; review the +Depends on Commit 1, 2, and 3. The diff is mostly derived content; review the tool, the registry rules, and the check. - `tool/build_fortal_preset.dart` as specified above, and @@ -348,7 +357,7 @@ tool, the registry rules, and the check. `open_code/CLEAN_SHEET.md` under "What remains outside this MVP" linking here; `packages/remix_cli/CHANGELOG.md`. -### PR 5: `chore(remix_fortal)`: stop publishing +### Commit 5: `chore(remix_fortal)`: stop publishing - `publish_to: none` in the pubspec. Remove the pub.dev publish notes from the pubspec comments and the package from any release automation that @@ -360,12 +369,12 @@ tool, the registry rules, and the check. - After merge: mark `remix_fortal` discontinued on pub.dev with the preset named as the replacement. -## Risks and how each PR bounds them +## Risks and how each commit bounds them -- **A `style` local collides with the new parameter.** Twelve recipes. PR 2 +- **A `style` local collides with the new parameter.** Twelve recipes. Commit 2 renames them; the analyzer catches any miss. -- **A file or path still contains `fortal` after PR 1.** The derivation tool - refuses, and PR 4's test plants one to prove the refusal. +- **A file or path still contains `fortal` after Commit 1.** The derivation tool + refuses, and Commit 4's test plants one to prove the refusal. - **Formatter drift.** The installer formats rendered output, and wrapping depends on prefix length, as `check_open_code_dogfood.dart` documents. The drift check therefore compares the template to the source, never a rendered @@ -377,6 +386,21 @@ tool, the registry rules, and the check. rejects it with its existing "unsupported schema" error. The project-local dev dependency pins the CLI, which is why the docs recommend it. +## Consumer and release validation + +Both registries require Remix `^1.0.0-beta.8`. Beta.7 lacks the Naked UI types +needed by generated Fortal adapters. The repository prepares beta.8 with these +exports and the Sidebar panel. + +`melos run open-code:check` installs every item from both presets against the +checkout. `melos run open-code:release:check` requires hosted Remix and never +falls back to the checkout. The CLI publish job depends on the hosted check. +A source/template round trip does not replace either consumer check. + +The first public release also requires the CLI bootstrap and a hosted CLI +installation check. Follow [the release instructions](RELEASING.md). Keep +`remix_fortal` available until the published replacement passes those checks. + ## Out of this stack - `init --preset --reinstall`, a batched `add --overwrite` for switching @@ -384,7 +408,7 @@ tool, the registry rules, and the check. - Preset knobs at `init`, such as `--accent indigo --radius medium` written into the installed theme. - Authoring the default preset as Dart with the same derivation. The default - has no parity contract, so the pressure is lower. Reconsider after PR 4 + has no parity contract, so the pressure is lower. Reconsider after Commit 4 shows the tool's cost. - Deleting `packages/remix_fortal` from the repository. That would make the `.tmpl` files the only form of Fortal, move the parity suite behind a diff --git a/open_code/README.md b/open_code/README.md index 824392cfc..d1dc8ed45 100644 --- a/open_code/README.md +++ b/open_code/README.md @@ -47,6 +47,18 @@ at a checkout or staged package: dart pub add "dev:remix_cli@{path: /path/to/remix/packages/remix_cli}" ``` +Both presets require Remix `^1.0.0-beta.8`. Until that release is available, +add a temporary `pubspec_overrides.yaml` to the application: + +```yaml +dependency_overrides: + remix: + path: /path/to/remix/packages/remix +``` + +Replace the path with your checkout. The [release instructions](RELEASING.md) +cover hosted validation, CLI bootstrap, and the Fortal transition. + Two host notes travel with the catalog. An installed text field needs an `Overlay` ancestor once it takes focus, for its selection handles — `MaterialApp`, `CupertinoApp`, and any `WidgetsApp` with routes already provide @@ -241,12 +253,15 @@ root: ```shell fvm dart test test/tool/check_open_code_test.dart -fvm dart run tool/check_open_code.dart -fvm dart run tool/check_open_code.dart --preset fortal +fvm dart run melos run open-code:check ``` -The full check creates a guarded temporary Flutter app, installs the checkout -CLI with prefix `Acme`, adds every registry item, and proves generation, -analysis, and all behavior tests against hosted Remix. It then overrides Remix to the current checkout, -regenerates without a consumer `build.yaml`, and repeats the checks. Pass -`--keep` to retain the generated gallery for inspection. +The CI check creates a fresh application for each preset. It installs the +checkout CLI with prefix `Acme`, adds every item, and verifies generation, +analysis, and consumer tests against checkout Remix. + +After Remix is published, run `fvm dart run melos run open-code:release:check` +to verify both presets with hosted Remix. The direct checker defaults to both +sources. Select `--source checkout` or `--source hosted` for one source. +Use `--hosted-cli --source hosted` after CLI publication to verify its hosted +assets. Pass `--keep` to retain a generated application for inspection. diff --git a/open_code/RELEASING.md b/open_code/RELEASING.md new file mode 100644 index 000000000..6486312f9 --- /dev/null +++ b/open_code/RELEASING.md @@ -0,0 +1,95 @@ +# Release the Open Code presets + +## Current state + +The stack prepares Remix `1.0.0-beta.8` and `remix_cli` `0.1.0`. +Neither version has been published from this stack. The existing +`remix_fortal` release remains available until the replacement passes validation. + +Merge PR #177, the Fortal PR, and PR #178 in that order. Retarget each child +PR to `main` after its parent merges. Preserve the parent commits when merging, +or rebase the child after a squash merge. Require passing checks after each +retarget or rebase. + +## Validate the release candidate + +Run these commands from the final checkout with Flutter 3.44.0: + +```shell +fvm flutter pub get +fvm dart run melos run ci +fvm dart analyze +``` + +CI installs every item from both presets in fresh applications. It uses +checkout Remix and hosted supporting packages. The source and template checks +also verify generated files, the Radix contract, and the catalog. + +Run `fvm dart pub publish --dry-run` inside `packages/remix` and +`packages/remix_cli`. Confirm that the CLI archive includes both registry trees +and their templates. Resolve every publication error before continuing. + +The version and changelog for beta.8 are already prepared in this stack. +Future releases use `.github/workflows/version.yml` to update Remix and both +registry minimum versions together. + +## Publish Remix first + +After release approval, tag the validated merge commit as `v1.0.0-beta.8` and +push that tag alone. The existing publish workflow publishes Remix. +After success, record `remix-v1.0.0-beta.8` on the same commit for Melos history. + +Wait until pub.dev serves beta.8. Then run: + +```shell +fvm dart run melos run open-code:release:check +``` + +This command installs both presets with hosted Remix and the checkout CLI. +It must pass before CLI publication. It never substitutes checkout Remix for +a missing hosted release. Keep the registry minimum at beta.8 if resolution fails. + +## Bootstrap the CLI package + +The first CLI publication requires an authorized uploader to run +`fvm dart pub publish` inside `packages/remix_cli` after the hosted checks pass. +Pub.dev requires a manual first publication before automated publishing can +be configured. See the [Dart publication instructions](https://dart.dev/tools/pub/automated-publishing). + +Confirm the intended publisher. Then configure automated publishing in the +package Admin tab with repository `conceptadev/remix` and tag pattern +`remix_cli-v{{version}}`. + +Do not push `remix_cli-v0.1.0` to publish the same version again. Future CLI +releases update the package version, `lib/src/version.dart`, and the changelog. +Their tag starts the hosted consumer checks before the publish job. + +## Verify the published CLI + +After pub.dev serves CLI `0.1.0`, run: + +```shell +fvm dart run tool/check_open_code.dart --source hosted --hosted-cli +fvm dart run tool/check_open_code.dart --preset fortal --source hosted --hosted-cli +``` + +These checks install the exact CLI version declared in the checkout from +pub.dev. They require hosted runtime packages and reject checkout package +dependencies. They exercise the published registry assets, generation, analyzer, +and consumer tests. + +Remove the pending-publication notices from the installation guides after +these checks pass. + +## Discontinue the old Fortal package + +After the published CLI checks pass, mark `remix_fortal` discontinued in its +pub.dev Admin tab. Name `remix_cli` as the replacement package. The migration +instructions use `remix init --preset fortal` and application-owned imports. + +Keep `packages/remix_fortal` in the repository as the analyzed authoring source. +Keep `publish_to: none` and its tests. Existing hosted installations remain +available; discontinuation does not delete their package versions. + +If either published consumer check fails, keep `remix_fortal` active and fix +the replacement before completing the transition. diff --git a/packages/remix/CHANGELOG.md b/packages/remix/CHANGELOG.md index c2af8f144..e1ed9af75 100644 --- a/packages/remix/CHANGELOG.md +++ b/packages/remix/CHANGELOG.md @@ -1,5 +1,7 @@ ## 1.0.0-beta.8 +- Export the Naked UI constructor and state types required by generated + application-owned Accordion, Disclosure, Slider, and Tabs recipes. - **FEAT**(sidebar): publish a Remix sidebar panel (#168). ## 1.0.0-beta.7 diff --git a/packages/remix_cli/CHANGELOG.md b/packages/remix_cli/CHANGELOG.md index d2b79d453..89a5b3769 100644 --- a/packages/remix_cli/CHANGELOG.md +++ b/packages/remix_cli/CHANGELOG.md @@ -1,5 +1,8 @@ ## 0.1.0 +- Require Remix `^1.0.0-beta.8` for the generated Fortal adapters. CI checks + both presets with checkout Remix. Publication requires the hosted check. + - Adds the `fortal` preset as application-owned source derived from the repository's analyzed Radix Themes 3.3.0 implementation. It installs a prefixed 277-token theme and full component catalog without a @@ -13,7 +16,7 @@ - Rejects reserved runtime prefixes before writing source and prints command help once. - Preserves spaces and special characters in generation filters. Each filter selects one generated file. - Bundles theme plus a registry item for every styled component surface in - `remix` 1.0.0-beta.7: accordion, avatar, badge, button, callout, card, + `remix`: accordion, avatar, badge, button, callout, card, checkbox, data_list, data_table, dialog, disclosure, divider, icon_button, link, menu, popover, progress, radio, segmented_control, select, skeleton, slider, spinner, diff --git a/packages/remix_cli/README.md b/packages/remix_cli/README.md index ef6347870..0ad9153b9 100644 --- a/packages/remix_cli/README.md +++ b/packages/remix_cli/README.md @@ -40,6 +40,18 @@ To run an unreleased build, point at a checkout or staged package path: dart pub add "dev:remix_cli@{path: /path/to/remix/packages/remix_cli}" ``` +Both presets require Remix `^1.0.0-beta.8`. Before that release is available, +add this `pubspec_overrides.yaml` to the application: + +```yaml +dependency_overrides: + remix: + path: /path/to/remix/packages/remix +``` + +Replace the path with your checkout. Remove the override after beta.8 is +published and run `flutter pub get` to use hosted Remix. + Global activation is convenient for experiments, but it does not pin the template version per project: diff --git a/packages/remix_cli/lib/src/registry/fortal/registry.yaml b/packages/remix_cli/lib/src/registry/fortal/registry.yaml index d385ab148..f53d6284b 100644 --- a/packages/remix_cli/lib/src/registry/fortal/registry.yaml +++ b/packages/remix_cli/lib/src/registry/fortal/registry.yaml @@ -3,7 +3,7 @@ schema: 1 items: theme: dependencies: - remix: ^1.0.0-beta.7 + remix: ^1.0.0-beta.8 files: - source: templates/theme/computed.dart.tmpl target: "@ui/theme/computed.dart" diff --git a/packages/remix_cli/test/installer_test.dart b/packages/remix_cli/test/installer_test.dart index 56eefa484..8cb29f794 100644 --- a/packages/remix_cli/test/installer_test.dart +++ b/packages/remix_cli/test/installer_test.dart @@ -778,6 +778,35 @@ dev_dependencies: }, ); + test('both presets reject resolved beta.7 before writing source', () async { + for (final preset in ['default', 'fortal']) { + final caseRoot = createFlutterPackage(); + addTearDown(() => caseRoot.deleteSync(recursive: true)); + await Installer( + projectRoot: caseRoot, + writeOut: (_) {}, + ).initialize(InitOptions(prefix: 'Ui', preset: preset, uiPath: 'lib/ui')); + writeRequiredPubspec(caseRoot, remix: '^1.0.0-beta.7'); + writeRequiredLock(caseRoot, remix: '1.0.0-beta.7'); + final before = snapshotFiles(caseRoot); + final writer = RecordingFileWriter(caseRoot); + + await expectLater( + Installer( + projectRoot: caseRoot, + writeOut: (_) {}, + processRunner: happyRunner(caseRoot, writeLockOnPubGet: false), + fileWriter: writer, + ).add(const AddOptions(item: 'accordion', mode: AddMode.write)), + throwsStateError, + reason: preset, + ); + + expect(writer.paths, isEmpty, reason: preset); + expect(snapshotFiles(caseRoot), before, reason: preset); + } + }); + test('a resolved remix above the snapshot floor prints a notice', () async { // The registry constraint is a caret, so a consumer reaches an untested // remix without ever failing lock verification. This line is the signal. diff --git a/pubspec.yaml b/pubspec.yaml index 454554f15..9867eb384 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -124,8 +124,15 @@ melos: open-code:check: steps: - dart test test/tool/check_open_code_test.dart - - dart run tool/check_open_code.dart - description: Test the checker and install every registry item in a fresh app + - dart run tool/check_open_code.dart --source checkout + - dart run tool/check_open_code.dart --preset fortal --source checkout + description: Install every item from both presets against the Remix checkout + + open-code:release:check: + steps: + - dart run tool/check_open_code.dart --source hosted + - dart run tool/check_open_code.dart --preset fortal --source hosted + description: Verify both presets against the required published Remix release open-code:fortal:build: run: dart run tool/build_fortal_preset.dart @@ -167,9 +174,8 @@ melos: - open-code:dogfood:check - test:cli - test:flutter - # Last because it is the only step that leaves the workspace: it builds - # a throwaway app in the system temp directory and resolves from - # pub.dev before retesting against this checkout. + # Fresh consumers use checkout Remix and hosted supporting packages. + # The release check separately requires the published Remix version. - open-code:check description: >- Verify generated sources, docs, Fortal parity, tests, and the diff --git a/skills/using-remix/SKILL.md b/skills/using-remix/SKILL.md index a3a5f1e2a..13d13581b 100644 --- a/skills/using-remix/SKILL.md +++ b/skills/using-remix/SKILL.md @@ -51,6 +51,10 @@ import 'package:remix/remix.dart'; For a new Fortal consumer, initialize the preset and add the needed items: +This checkout prepares the first CLI release and requires Remix beta.8. +Until both releases are available, use the checkout setup in +`packages/remix_cli/README.md`. Use the hosted commands below after publication. + ```bash flutter pub add dev:remix_cli dart run remix_cli:remix init --prefix Fortal --preset fortal diff --git a/test/tool/check_open_code_test.dart b/test/tool/check_open_code_test.dart index c2ff1edfe..9bea512e8 100644 --- a/test/tool/check_open_code_test.dart +++ b/test/tool/check_open_code_test.dart @@ -15,6 +15,63 @@ void main() { if (sandbox.existsSync()) sandbox.deleteSync(recursive: true); }); + group('consumer source selection', () { + test('the default retains hosted and checkout validation', () { + final options = checker.parseConsumerCheckOptions([])!; + + expect(options.source, checker.RemixSource.both); + expect(options.preset, 'default'); + expect(options.keep, isFalse); + expect(options.hostedCli, isFalse); + }); + + test('either preset can select an explicit source', () { + for (final preset in ['default', 'fortal']) { + for (final source in checker.RemixSource.values) { + final options = checker.parseConsumerCheckOptions([ + '--preset', + preset, + '--source', + source.name, + '--keep', + ])!; + + expect(options.source, source); + expect(options.preset, preset); + expect(options.keep, isTrue); + } + } + }); + + test('invalid or repeated source choices fail instead of falling back', () { + for (final arguments in [ + ['--source'], + ['--source', 'missing'], + ['--source', 'hosted', '--source', 'checkout'], + ['--preset', 'fortal', '--source', 'missing'], + ['--hosted-cli'], + ['--source', 'checkout', '--hosted-cli'], + ['--source', 'hosted', '--hosted-cli', '--hosted-cli'], + ]) { + expect(checker.parseConsumerCheckOptions(arguments), isNull); + } + }); + + test('hosted CLI validation requires an explicit hosted Remix source', () { + final options = checker.parseConsumerCheckOptions([ + '--hosted-cli', + '--source', + 'hosted', + '--preset', + 'fortal', + ])!; + + expect(options.hostedCli, isTrue); + expect(options.source, checker.RemixSource.hosted); + expect(options.preset, 'fortal'); + }); + }); + test('the committed fixture is the minimal pre-install contract', () { final fixture = Directory('${Directory.current.path}/open_code/fixture'); diff --git a/tool/check_open_code.dart b/tool/check_open_code.dart index 34f71dc3a..90bff9635 100644 --- a/tool/check_open_code.dart +++ b/tool/check_open_code.dart @@ -2,13 +2,15 @@ /// Flutter application. /// /// ```shell -/// dart run tool/check_open_code.dart [--preset default|fortal] [--keep] +/// dart run tool/check_open_code.dart [--preset default|fortal] +/// [--source both|hosted|checkout] [--hosted-cli] [--keep] /// ``` /// /// The checker writes only to a guarded system-temporary directory. It installs /// this checkout's `remix_cli`, lets the CLI install every registry item, then -/// tests the installed source first with hosted Remix and again with a temporary -/// override to the current Remix checkout. +/// tests the installed source with the selected Remix source. The default runs +/// hosted Remix first, then regenerates against the checkout. CI selects the +/// checkout explicitly so an unpublished release can be validated before tagging. library; import 'dart:convert'; @@ -204,19 +206,36 @@ final class _PresetContract { ]; } -final class _ParsedArguments { - const _ParsedArguments({required this.keep, required this.preset}); +enum RemixSource { both, hosted, checkout } + +final class ConsumerCheckOptions { + const ConsumerCheckOptions({ + required this.keep, + required this.preset, + required this.source, + required this.hostedCli, + }); final bool keep; - final _PresetContract preset; + final String preset; + final RemixSource source; + final bool hostedCli; } -_ParsedArguments? _parseArguments(List arguments) { +ConsumerCheckOptions? parseConsumerCheckOptions(List arguments) { var keep = false; var sawPreset = false; + var sawSource = false; var preset = _defaultPreset; + var source = RemixSource.both; + var hostedCli = false; for (var index = 0; index < arguments.length; index += 1) { final argument = arguments[index]; + if (argument == '--hosted-cli') { + if (hostedCli) return null; + hostedCli = true; + continue; + } if (argument == '--keep') { if (keep) return null; keep = true; @@ -236,17 +255,33 @@ _ParsedArguments? _parseArguments(List arguments) { } continue; } + if (argument == '--source') { + if (sawSource || index + 1 >= arguments.length) return null; + sawSource = true; + final name = arguments[++index]; + final selected = RemixSource.values.where((value) => value.name == name); + if (selected.isEmpty) return null; + source = selected.single; + continue; + } return null; } - return _ParsedArguments(keep: keep, preset: preset); + if (hostedCli && source != RemixSource.hosted) return null; + return ConsumerCheckOptions( + keep: keep, + preset: preset.name, + source: source, + hostedCli: hostedCli, + ); } Future main(List arguments) async { - final parsed = _parseArguments(arguments); + final parsed = parseConsumerCheckOptions(arguments); if (parsed == null) { stderr.writeln( 'Usage: dart run tool/check_open_code.dart ' - '[--preset default|fortal] [--keep]', + '[--preset default|fortal] [--source both|hosted|checkout] ' + '[--hosted-cli (requires --source hosted)] [--keep]', ); exitCode = 64; return; @@ -256,7 +291,9 @@ Future main(List arguments) async { final failure = await _run( repositoryRoot, keep: parsed.keep, - preset: parsed.preset, + preset: _presetByName(parsed.preset), + source: parsed.source, + hostedCli: parsed.hostedCli, ); if (failure != null) { stderr.writeln('open-code check failed: ${failure.message}'); @@ -268,6 +305,8 @@ Future<_Failure?> _run( Directory repositoryRoot, { required bool keep, required _PresetContract preset, + required RemixSource source, + required bool hostedCli, }) async { final rootFailure = _verifyRepositoryRoot(repositoryRoot); if (rootFailure != null) return rootFailure; @@ -306,6 +345,8 @@ Future<_Failure?> _run( fixtureRoot: fixtureRoot, app: app, preset: preset, + source: source, + hostedCli: hostedCli, ); if (checkFailure != null) { return _retainedFailure(parent, checkFailure); @@ -326,11 +367,15 @@ Future<_Failure?> _run( ..writeln('') ..writeln('Kept the generated application at:') ..writeln(' ${app.path}') - ..writeln('') - ..writeln('It has a temporary checkout override for Remix. Delete') - ..writeln('pubspec_overrides.yaml and run `${sdk.flutter} pub get` to') - ..writeln('return to hosted Remix.') - ..writeln('') + ..writeln(''); + if (source != RemixSource.hosted) { + stdout + ..writeln('It has a temporary checkout override for Remix. Delete') + ..writeln('pubspec_overrides.yaml and run `${sdk.flutter} pub get` to') + ..writeln('return to hosted Remix after its required release exists.') + ..writeln(''); + } + stdout ..writeln('Run the gallery:') ..writeln(' cd ${app.path} && ${sdk.flutter} run -d chrome') ..writeln('') @@ -341,7 +386,7 @@ Future<_Failure?> _run( ); } - stdout.writeln('open-code check passed.'); + stdout.writeln('open-code check passed (${preset.name}, ${source.name}).'); return null; } @@ -351,6 +396,8 @@ Future<_Failure?> _checkInTemporaryApp({ required Directory fixtureRoot, required Directory app, required _PresetContract preset, + required RemixSource source, + required bool hostedCli, }) async { final environment = _toolchainEnvironment(sdk); @@ -378,21 +425,30 @@ Future<_Failure?> _checkInTemporaryApp({ if (!cliRoot.existsSync()) { return _Failure('packages/remix_cli is missing from this checkout.'); } - // The CLI comes from this checkout, but everything it then installs is - // resolved from pub.dev — including the `remix` version the registry floors - // at. So between the version pull request merging and its `v` tag - // being served, this phase cannot resolve the new floor and fails on that - // commit. That window is expected and self-heals once pub.dev serves the - // tag. The fix is to push the tag, never to teach this checker to probe - // pub.dev. + final remixSource = Directory('${repositoryRoot.path}/packages/remix'); + if (!remixSource.existsSync()) { + return _Failure('packages/remix is missing from this checkout.'); + } + final startsWithCheckout = source == RemixSource.checkout; + if (startsWithCheckout) _writeCheckoutOverride(app, remixSource); + + // Hosted mode never falls back to the checkout. A missing release must fail + // the release check, even when checkout validation already passed. final addCli = await _runProcess( sdk.dart, - ['pub', 'add', 'dev:remix_cli@{path: ${cliRoot.path}}'], + [ + 'pub', + 'add', + if (hostedCli) + 'dev:remix_cli:${(loadYaml(File('${cliRoot.path}/pubspec.yaml').readAsStringSync()) as YamlMap)['version']}' + else + 'dev:remix_cli@{path: ${jsonEncode(cliRoot.path)}}', + ], workingDirectory: app.path, environment: environment, ); if (addCli != null) { - return _Failure('could not add the checkout CLI to the temporary app'); + return _Failure('could not add the selected CLI to the temporary app'); } final init = await _runProcess( @@ -426,17 +482,22 @@ Future<_Failure?> _checkInTemporaryApp({ if (installedFailure != null) return installedFailure; _step('Installed UI inventory and import boundary are exact.'); - final dependencyFailure = _verifyInstalledDependencies(app); + final dependencyFailure = _verifyInstalledDependencies( + app, + hostedCli: hostedCli, + ); if (dependencyFailure != null) return dependencyFailure; - final hostedConfig = _readPackageConfig(app); - if (hostedConfig is _Failure) return hostedConfig; - final hostedPackages = hostedConfig as Map; + final installedConfig = _readPackageConfig(app); + if (installedConfig is _Failure) return installedConfig; + final installedPackages = installedConfig as Map; for (final package in [ ..._requiredRuntimeDependencies, ..._requiredDevDependencies, + if (hostedCli) 'remix_cli', ]) { - final root = hostedPackages[package]; + if (startsWithCheckout && package == 'remix') continue; + final root = installedPackages[package]; if (root == null || !_isHostedCachePath(root)) { return _Failure( '$package resolved to ${root ?? 'nothing'}, expected a hosted cache.', @@ -447,19 +508,22 @@ Future<_Failure?> _checkInTemporaryApp({ final floorFailure = _verifyResolvedRemixFloor( repositoryRoot: repositoryRoot, - hostedPackages: hostedPackages, + resolvedPackages: installedPackages, preset: preset, ); if (floorFailure != null) return floorFailure; - _step('Hosted remix resolved to the registry floor.'); + _step('Resolved Remix matches the registry floor.'); - final hostedCheckoutFailure = _verifyCheckoutPackages( - packages: hostedPackages, + final installedCheckoutFailure = _verifyCheckoutPackages( + packages: installedPackages, repositoryRoot: repositoryRoot, - expected: {'remix_cli': cliRoot}, + expected: { + if (!hostedCli) 'remix_cli': cliRoot, + if (startsWithCheckout) 'remix': remixSource, + }, ); - if (hostedCheckoutFailure != null) return hostedCheckoutFailure; - _step('remix_cli is the only checkout package in hosted-consumer mode.'); + if (installedCheckoutFailure != null) return installedCheckoutFailure; + _step('Installed packages match the selected source boundary.'); if (preset.generatedSnapshots.isNotEmpty) { final generatedFailure = _verifyGeneratedFixture( @@ -468,27 +532,19 @@ Future<_Failure?> _checkInTemporaryApp({ preset: preset, ); if (generatedFailure != null) return generatedFailure; - _step('Hosted generation matches the committed Acme adapter.'); + _step('Generation matches the committed Acme adapter.'); } - final hostedVerification = await _analyzeAndTest( + final installedVerification = await _analyzeAndTest( sdk: sdk, app: app, - dependencySource: 'hosted Remix', + dependencySource: startsWithCheckout ? 'current checkout' : 'hosted Remix', environment: environment, ); - if (hostedVerification != null) return hostedVerification; + if (installedVerification != null) return installedVerification; + if (source != RemixSource.both) return null; - final remixSource = Directory('${repositoryRoot.path}/packages/remix'); - if (!remixSource.existsSync()) { - return _Failure('packages/remix is missing from this checkout.'); - } - File('${app.path}/pubspec_overrides.yaml').writeAsStringSync(''' -# Created in a guarded temporary app by tool/check_open_code.dart. -dependency_overrides: - remix: - path: ${remixSource.path} -'''); + _writeCheckoutOverride(app, remixSource); final overrideGet = await _runProcess( sdk.flutter, @@ -555,6 +611,15 @@ dependency_overrides: ); } +void _writeCheckoutOverride(Directory app, Directory remixSource) { + File('${app.path}/pubspec_overrides.yaml').writeAsStringSync(''' +# Created in a guarded temporary app by tool/check_open_code.dart. +dependency_overrides: + remix: + path: ${jsonEncode(remixSource.path)} +'''); +} + Future<_Failure?> _analyzeAndTest({ required _Toolchain sdk, required Directory app, @@ -713,7 +778,7 @@ _Failure? _verifyRegistryCoverage( ); } -/// Fails when the hosted phase resolved a `remix` other than the one the +/// Fails when the selected source resolved a `remix` other than the one the /// registry floors at. /// /// The registry constraint is a caret, so every later beta resolves and every @@ -723,7 +788,7 @@ _Failure? _verifyRegistryCoverage( /// consumer actually gets is the same one. _Failure? _verifyResolvedRemixFloor({ required Directory repositoryRoot, - required Map hostedPackages, + required Map resolvedPackages, required _PresetContract preset, }) { final registry = File( @@ -750,20 +815,16 @@ _Failure? _verifyResolvedRemixFloor({ ); } - final root = hostedPackages['remix']; + final root = resolvedPackages['remix']; if (root == null) { return _Failure('remix is missing from package_config.json.'); } final floor = constraint.substring(1); - final expected = 'remix-$floor'; - // pub resolves the cache directory URI with a trailing separator. - final actual = _basename( - root.endsWith(Platform.pathSeparator) - ? root.substring(0, root.length - 1) - : root, - ); - if (actual == expected) return null; + final manifest = File('$root/pubspec.yaml'); + if (!manifest.existsSync()) return _Failure('Resolved Remix has no pubspec.'); + final actual = (loadYaml(manifest.readAsStringSync()) as YamlMap)['version']; + if (actual == floor) return null; return _Failure( 'the fresh app resolved $actual, but the registry floors at $floor. ' 'If packages/remix was just bumped, push its `v$floor` tag and wait for ' @@ -815,7 +876,10 @@ _Failure? _verifyFixtureContract( ); } -_Failure? _verifyInstalledDependencies(Directory app) { +_Failure? _verifyInstalledDependencies( + Directory app, { + required bool hostedCli, +}) { final sections = _dependencySections( File('${app.path}/pubspec.yaml').readAsStringSync(), ); @@ -850,7 +914,9 @@ _Failure? _verifyInstalledDependencies(Directory app) { } } final cli = development['remix_cli']; - if (cli is! YamlMap || !cli.containsKey('path')) { + if (hostedCli) { + if (cli is! String) problems.add('remix_cli is not a hosted constraint'); + } else if (cli is! YamlMap || !cli.containsKey('path')) { problems.add('remix_cli is not a temporary path development dependency'); } if (sections.containsKey('dependency_overrides')) { From 3298332fddc90b55968656218b6a99a8a74ea9c4 Mon Sep 17 00:00:00 2001 From: Leo Farias Date: Fri, 4 Sep 2026 15:34:59 -0400 Subject: [PATCH 07/12] docs(open-code): describe native stack release steps --- open_code/PRESETS.md | 8 +++++--- open_code/RELEASING.md | 14 ++++++++++---- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/open_code/PRESETS.md b/open_code/PRESETS.md index b806af196..bb63989b0 100644 --- a/open_code/PRESETS.md +++ b/open_code/PRESETS.md @@ -210,11 +210,13 @@ runs it in CI, the way `docs:check` runs `generate_fortal_catalog.dart`. ## The PR stack 1. `feat/open-code` targets `main` in PR #177. -2. `feat/fortal-presets` targets `feat/open-code` and contains the Fortal work. +2. `feat/fortal-presets` targets `feat/open-code` in PR #180. 3. `feat/open-code-sidebar` targets `feat/fortal-presets` in PR #178. -Merge in that order. The original plan proposed five Fortal PRs. The final -stack keeps those implementation boundaries as five commits in one Fortal PR. +GitHub tracks these PRs as a native stack. Follow the merge and publication +steps in [RELEASING.md](RELEASING.md). +The original plan proposed five Fortal PRs. The final stack keeps those +implementation boundaries as five commits in one Fortal PR. The Sidebar PR remains separate because it adds a default recipe. ## Implementation commits diff --git a/open_code/RELEASING.md b/open_code/RELEASING.md index 6486312f9..68a45b54c 100644 --- a/open_code/RELEASING.md +++ b/open_code/RELEASING.md @@ -6,10 +6,16 @@ The stack prepares Remix `1.0.0-beta.8` and `remix_cli` `0.1.0`. Neither version has been published from this stack. The existing `remix_fortal` release remains available until the replacement passes validation. -Merge PR #177, the Fortal PR, and PR #178 in that order. Retarget each child -PR to `main` after its parent merges. Preserve the parent commits when merging, -or rebase the child after a squash merge. Require passing checks after each -retarget or rebase. +The native GitHub stack contains [#177](https://github.com/conceptadev/remix/pull/177), +[#180](https://github.com/conceptadev/remix/pull/180), and +[#178](https://github.com/conceptadev/remix/pull/178), from bottom to top. +Review each PR against its parent. Require passing checks for every PR. + +After merge approval, merge the stack through the top PR, #178. +GitHub merges all three PRs into `main`. For a partial merge, start with #177. +GitHub automatically rebases the remaining branches and updates their targets. +Require passing checks after any rebase. See the +[GitHub stack instructions](https://docs.github.com/en/pull-requests/get-started/about-stacked-prs#merging). ## Validate the release candidate From a97050e176e61ff1a6d8b11aa655eb9aac541693 Mon Sep 17 00:00:00 2001 From: Leo Farias Date: Mon, 7 Sep 2026 10:38:41 -0400 Subject: [PATCH 08/12] fix(remix_cli): require beta.9 for the Fortal exports --- .github/workflows/version.yml | 2 +- docs/fortal.mdx | 2 +- docs/open-code.mdx | 8 +-- docs/remix-cli-guide.html | 10 ++-- open_code/PRESETS.md | 6 +- open_code/README.md | 2 +- open_code/RELEASING.md | 15 +++-- packages/remix/CHANGELOG.md | 5 +- packages/remix/pubspec.yaml | 2 +- packages/remix_cli/CHANGELOG.md | 2 +- packages/remix_cli/README.md | 4 +- .../lib/src/registry/default/registry.yaml | 2 +- .../lib/src/registry/fortal/registry.yaml | 2 +- packages/remix_cli/test/installer_test.dart | 56 ++++++++++--------- .../evals/evals.json | 2 +- .../references/component-playbook.md | 2 +- skills/using-remix/SKILL.md | 2 +- 17 files changed, 67 insertions(+), 57 deletions(-) diff --git a/.github/workflows/version.yml b/.github/workflows/version.yml index 3b3c49842..48fcd9c24 100644 --- a/.github/workflows/version.yml +++ b/.github/workflows/version.yml @@ -15,7 +15,7 @@ on: workflow_dispatch: inputs: version: - description: "Exact version for remix (e.g. 1.0.0-beta.8)." + description: "Exact version for remix (e.g. 1.0.0-beta.9)." required: true permissions: diff --git a/docs/fortal.mdx b/docs/fortal.mdx index 3281d0eec..bd0465919 100644 --- a/docs/fortal.mdx +++ b/docs/fortal.mdx @@ -22,7 +22,7 @@ Radix parity surface. Consumer applications do not depend on that package. ## Installation -The commands below apply after the first CLI release and Remix beta.8. +The commands below apply after the first CLI release and Remix beta.9. For checkout use, follow the [Open Code installation instructions](/open-code#install). ```bash diff --git a/docs/open-code.mdx b/docs/open-code.mdx index 39c2c939d..2204418eb 100644 --- a/docs/open-code.mdx +++ b/docs/open-code.mdx @@ -42,7 +42,7 @@ Before the first release, add the CLI from a checkout: dart pub add "dev:remix_cli@{path: /path/to/remix/packages/remix_cli}" ``` -Until Remix beta.8 is published, add this temporary `pubspec_overrides.yaml` +Until Remix beta.9 is published, add this temporary `pubspec_overrides.yaml` to the application. Replace the path with your checkout: ```yaml @@ -52,7 +52,7 @@ dependency_overrides: ``` After both releases are available, remove this override and use the hosted -installation below. Both presets require Remix `^1.0.0-beta.8`. +installation below. Both presets require Remix `^1.0.0-beta.9`. Add the CLI as a project-local dev dependency so its version is pinned in your lockfile, not in a machine-global install: @@ -153,7 +153,7 @@ remix add button --dry-run ``` Items: theme -> button -Dependencies: remix@^1.0.0-beta.8, mix_annotations@^2.2.0-beta.1, dev:build_runner@^2.10.1, dev:mix_generator@^2.2.0-beta.3 +Dependencies: remix@^1.0.0-beta.9, mix_annotations@^2.2.0-beta.1, dev:build_runner@^2.10.1, dev:mix_generator@^2.2.0-beta.3 theme: missing; lib/ui/theme/tokens.dart, lib/ui/theme/theme_data.dart, lib/ui/theme/theme_scope.dart button: missing; lib/ui/components/button.dart Exports: theme/tokens.dart, theme/theme_data.dart, theme/theme_scope.dart, components/button.dart @@ -458,7 +458,7 @@ a later `remix` that these templates were never tested against. `add` reports that after it resolves your lockfile: ``` -Resolved remix 1.0.0; this remix_cli registry was authored against 1.0.0-beta.8. Run flutter pub upgrade remix_cli, then review with --diff. +Resolved remix 1.0.0; this remix_cli registry was authored against 1.0.0-beta.9. Run flutter pub upgrade remix_cli, then review with --diff. ``` It is a notice, not a failure — the install completes. A `remix` *below* the diff --git a/docs/remix-cli-guide.html b/docs/remix-cli-guide.html index f8f0842d6..6377e7209 100644 --- a/docs/remix-cli-guide.html +++ b/docs/remix-cli-guide.html @@ -392,7 +392,7 @@ remix remix_cli 0.1.0 - remix 1.0.0-beta.8 + remix 1.0.0-beta.9 @@ -639,7 +639,7 @@

    Install the CLI

    The CLI has not been published. The hosted commands below apply after its - first release and Remix beta.8. For now, follow the + first release and Remix beta.9. For now, follow the checkout installation instructions.

    After publication, install the exact CLI version this guide was written against. If Dart prints a @@ -767,7 +767,7 @@

    --dry-run shows the plan

    Preview the install order and file state
    $ remix add button --dry-run
     Items: theme -> button
    -Dependencies: remix@^1.0.0-beta.8, mix_annotations@^2.2.0-beta.1, dev:build_runner@^2.10.1, dev:mix_generator@^2.2.0-beta.3
    +Dependencies: remix@^1.0.0-beta.9, mix_annotations@^2.2.0-beta.1, dev:build_runner@^2.10.1, dev:mix_generator@^2.2.0-beta.3
     theme: missing; lib/ui/theme/tokens.dart, lib/ui/theme/theme_data.dart, lib/ui/theme/theme_scope.dart
     button: missing; lib/ui/components/button.dart
     Exports: theme/tokens.dart, theme/theme_data.dart, theme/theme_scope.dart, components/button.dart
    @@ -2075,7 +2075,7 @@ 

    Provenance

    remix_cli0.1.0 Flutter3.44.0 stable, framework 559ffa3f75 Dart3.12.0 stable - remix1.0.0-beta.8, the registry snapshot version + remix1.0.0-beta.9, the registry snapshot version mix2.2.0-beta.5 mix_annotations2.2.0-beta.1 mix_generator2.2.0-beta.3 @@ -2094,7 +2094,7 @@

    Provenance

    A guide to remix_cli 0.1.0. The registry is authored against - remix 1.0.0-beta.8. See Provenance for the complete + remix 1.0.0-beta.9. See Provenance for the complete toolchain and for how each example was produced.

    diff --git a/open_code/PRESETS.md b/open_code/PRESETS.md index bb63989b0..757d3fa95 100644 --- a/open_code/PRESETS.md +++ b/open_code/PRESETS.md @@ -390,9 +390,9 @@ tool, the registry rules, and the check. ## Consumer and release validation -Both registries require Remix `^1.0.0-beta.8`. Beta.7 lacks the Naked UI types -needed by generated Fortal adapters. The repository prepares beta.8 with these -exports and the Sidebar panel. +Both registries require Remix `^1.0.0-beta.9`. Published versions through beta.8 +lack the Naked UI types needed by generated Fortal adapters. The repository +prepares beta.9 with these exports. Beta.8 already provides the Sidebar panel. `melos run open-code:check` installs every item from both presets against the checkout. `melos run open-code:release:check` requires hosted Remix and never diff --git a/open_code/README.md b/open_code/README.md index d1dc8ed45..825940e10 100644 --- a/open_code/README.md +++ b/open_code/README.md @@ -47,7 +47,7 @@ at a checkout or staged package: dart pub add "dev:remix_cli@{path: /path/to/remix/packages/remix_cli}" ``` -Both presets require Remix `^1.0.0-beta.8`. Until that release is available, +Both presets require Remix `^1.0.0-beta.9`. Until that release is available, add a temporary `pubspec_overrides.yaml` to the application: ```yaml diff --git a/open_code/RELEASING.md b/open_code/RELEASING.md index 68a45b54c..5acffa4c1 100644 --- a/open_code/RELEASING.md +++ b/open_code/RELEASING.md @@ -2,10 +2,13 @@ ## Current state -The stack prepares Remix `1.0.0-beta.8` and `remix_cli` `0.1.0`. +The stack prepares Remix `1.0.0-beta.9` and `remix_cli` `0.1.0`. Neither version has been published from this stack. The existing `remix_fortal` release remains available until the replacement passes validation. +Remix beta.8 was published from PR #182 on September 5, 2026. It does not +include the exports required by the Fortal preset. This stack requires a new release. + The native GitHub stack contains [#177](https://github.com/conceptadev/remix/pull/177), [#180](https://github.com/conceptadev/remix/pull/180), and [#178](https://github.com/conceptadev/remix/pull/178), from bottom to top. @@ -35,17 +38,17 @@ Run `fvm dart pub publish --dry-run` inside `packages/remix` and `packages/remix_cli`. Confirm that the CLI archive includes both registry trees and their templates. Resolve every publication error before continuing. -The version and changelog for beta.8 are already prepared in this stack. +The version and changelog for beta.9 are already prepared in this stack. Future releases use `.github/workflows/version.yml` to update Remix and both registry minimum versions together. ## Publish Remix first -After release approval, tag the validated merge commit as `v1.0.0-beta.8` and +After release approval, tag the validated merge commit as `v1.0.0-beta.9` and push that tag alone. The existing publish workflow publishes Remix. -After success, record `remix-v1.0.0-beta.8` on the same commit for Melos history. +After success, record `remix-v1.0.0-beta.9` on the same commit for Melos history. -Wait until pub.dev serves beta.8. Then run: +Wait until pub.dev serves beta.9. Then run: ```shell fvm dart run melos run open-code:release:check @@ -53,7 +56,7 @@ fvm dart run melos run open-code:release:check This command installs both presets with hosted Remix and the checkout CLI. It must pass before CLI publication. It never substitutes checkout Remix for -a missing hosted release. Keep the registry minimum at beta.8 if resolution fails. +a missing hosted release. Keep the registry minimum at beta.9 if resolution fails. ## Bootstrap the CLI package diff --git a/packages/remix/CHANGELOG.md b/packages/remix/CHANGELOG.md index e1ed9af75..b5171a1f7 100644 --- a/packages/remix/CHANGELOG.md +++ b/packages/remix/CHANGELOG.md @@ -1,7 +1,10 @@ -## 1.0.0-beta.8 +## 1.0.0-beta.9 - Export the Naked UI constructor and state types required by generated application-owned Accordion, Disclosure, Slider, and Tabs recipes. + +## 1.0.0-beta.8 + - **FEAT**(sidebar): publish a Remix sidebar panel (#168). ## 1.0.0-beta.7 diff --git a/packages/remix/pubspec.yaml b/packages/remix/pubspec.yaml index 87fe1b36a..a7c62a3c6 100644 --- a/packages/remix/pubspec.yaml +++ b/packages/remix/pubspec.yaml @@ -12,7 +12,7 @@ topics: - styling - mix -version: 1.0.0-beta.8 +version: 1.0.0-beta.9 # Consumer floor, matching `mix` and `naked_ui`. Intentionally lower than the # workspace's own toolchain floor: dev tooling needs a newer SDK than the diff --git a/packages/remix_cli/CHANGELOG.md b/packages/remix_cli/CHANGELOG.md index 89a5b3769..68ef1ca22 100644 --- a/packages/remix_cli/CHANGELOG.md +++ b/packages/remix_cli/CHANGELOG.md @@ -1,6 +1,6 @@ ## 0.1.0 -- Require Remix `^1.0.0-beta.8` for the generated Fortal adapters. CI checks +- Require Remix `^1.0.0-beta.9` for the generated Fortal adapters. CI checks both presets with checkout Remix. Publication requires the hosted check. - Adds the `fortal` preset as application-owned source derived from the diff --git a/packages/remix_cli/README.md b/packages/remix_cli/README.md index 0ad9153b9..3d60649cc 100644 --- a/packages/remix_cli/README.md +++ b/packages/remix_cli/README.md @@ -40,7 +40,7 @@ To run an unreleased build, point at a checkout or staged package path: dart pub add "dev:remix_cli@{path: /path/to/remix/packages/remix_cli}" ``` -Both presets require Remix `^1.0.0-beta.8`. Before that release is available, +Both presets require Remix `^1.0.0-beta.9`. Before that release is available, add this `pubspec_overrides.yaml` to the application: ```yaml @@ -49,7 +49,7 @@ dependency_overrides: path: /path/to/remix/packages/remix ``` -Replace the path with your checkout. Remove the override after beta.8 is +Replace the path with your checkout. Remove the override after beta.9 is published and run `flutter pub get` to use hosted Remix. Global activation is convenient for experiments, but it does not pin the diff --git a/packages/remix_cli/lib/src/registry/default/registry.yaml b/packages/remix_cli/lib/src/registry/default/registry.yaml index 8576679a8..ba575c453 100644 --- a/packages/remix_cli/lib/src/registry/default/registry.yaml +++ b/packages/remix_cli/lib/src/registry/default/registry.yaml @@ -2,7 +2,7 @@ schema: 1 items: theme: dependencies: - remix: ^1.0.0-beta.8 + remix: ^1.0.0-beta.9 files: - source: templates/theme/tokens.dart.tmpl target: "@ui/theme/tokens.dart" diff --git a/packages/remix_cli/lib/src/registry/fortal/registry.yaml b/packages/remix_cli/lib/src/registry/fortal/registry.yaml index f53d6284b..3319d1bcf 100644 --- a/packages/remix_cli/lib/src/registry/fortal/registry.yaml +++ b/packages/remix_cli/lib/src/registry/fortal/registry.yaml @@ -3,7 +3,7 @@ schema: 1 items: theme: dependencies: - remix: ^1.0.0-beta.8 + remix: ^1.0.0-beta.9 files: - source: templates/theme/computed.dart.tmpl target: "@ui/theme/computed.dart" diff --git a/packages/remix_cli/test/installer_test.dart b/packages/remix_cli/test/installer_test.dart index 8cb29f794..c0ddbfc8e 100644 --- a/packages/remix_cli/test/installer_test.dart +++ b/packages/remix_cli/test/installer_test.dart @@ -778,34 +778,38 @@ dev_dependencies: }, ); - test('both presets reject resolved beta.7 before writing source', () async { - for (final preset in ['default', 'fortal']) { - final caseRoot = createFlutterPackage(); - addTearDown(() => caseRoot.deleteSync(recursive: true)); - await Installer( - projectRoot: caseRoot, - writeOut: (_) {}, - ).initialize(InitOptions(prefix: 'Ui', preset: preset, uiPath: 'lib/ui')); - writeRequiredPubspec(caseRoot, remix: '^1.0.0-beta.7'); - writeRequiredLock(caseRoot, remix: '1.0.0-beta.7'); - final before = snapshotFiles(caseRoot); - final writer = RecordingFileWriter(caseRoot); + for (final version in ['1.0.0-beta.7', '1.0.0-beta.8']) { + test( + 'both presets reject resolved $version before writing source', + () async { + for (final preset in ['default', 'fortal']) { + final caseRoot = createFlutterPackage(); + addTearDown(() => caseRoot.deleteSync(recursive: true)); + await Installer(projectRoot: caseRoot, writeOut: (_) {}).initialize( + InitOptions(prefix: 'Ui', preset: preset, uiPath: 'lib/ui'), + ); + writeRequiredPubspec(caseRoot, remix: '^$version'); + writeRequiredLock(caseRoot, remix: version); + final before = snapshotFiles(caseRoot); + final writer = RecordingFileWriter(caseRoot); - await expectLater( - Installer( - projectRoot: caseRoot, - writeOut: (_) {}, - processRunner: happyRunner(caseRoot, writeLockOnPubGet: false), - fileWriter: writer, - ).add(const AddOptions(item: 'accordion', mode: AddMode.write)), - throwsStateError, - reason: preset, - ); + await expectLater( + Installer( + projectRoot: caseRoot, + writeOut: (_) {}, + processRunner: happyRunner(caseRoot, writeLockOnPubGet: false), + fileWriter: writer, + ).add(const AddOptions(item: 'accordion', mode: AddMode.write)), + throwsStateError, + reason: preset, + ); - expect(writer.paths, isEmpty, reason: preset); - expect(snapshotFiles(caseRoot), before, reason: preset); - } - }); + expect(writer.paths, isEmpty, reason: preset); + expect(snapshotFiles(caseRoot), before, reason: preset); + } + }, + ); + } test('a resolved remix above the snapshot floor prints a notice', () async { // The registry constraint is a caret, so a consumer reaches an untested diff --git a/skills/building-remix-design-system/evals/evals.json b/skills/building-remix-design-system/evals/evals.json index 936b72178..e49a2afa5 100644 --- a/skills/building-remix-design-system/evals/evals.json +++ b/skills/building-remix-design-system/evals/evals.json @@ -72,7 +72,7 @@ { "id": 7, "prompt": "Our RemixSelect-based component has a generic call() and three variants. With this workspace's Mix codegen versions, can the generator create the public AcmeSelect wrapper and named constructors, or do we still need to hand-write it? Show the recipe shape and explain the boundary.", - "expected_output": "Reads the current root managed dependencies and packages/remix/pubspec.yaml instead of assuming an old version floor, and identifies the tested set as remix ^1.0.0-beta.8, mix ^2.2.0-beta.5, mix_annotations ^2.2.0-beta.1, build_runner ^2.10.1, and mix_generator ^2.2.0-beta.3. Explains that genericity comes from the styler call() and is copied to the generated widget, while the annotated top-level recipe function itself must be non-generic. Shows @MixWidget() on a non-generic acmeSelectStyle function whose Style suffix infers AcmeSelect, with a named, non-nullable enum parameter named exactly variant; this generates AcmeSelect. constructors and lets T infer from values/items/callbacks. Checks that generated call parameters do not leak Remix-only public types; if they do, chooses a hand-written target-vocabulary facade. Mentions that nullable, required-positional, differently named, or non-enum variant parameters do not generate named constructors, while optional positional factory parameters are rejected. Requires a package-local spike and a second build_runner run that leaves generated output unchanged before committing to the API.", + "expected_output": "Reads the current root managed dependencies and packages/remix/pubspec.yaml instead of assuming an old version floor, and identifies the tested set as remix ^1.0.0-beta.9, mix ^2.2.0-beta.5, mix_annotations ^2.2.0-beta.1, build_runner ^2.10.1, and mix_generator ^2.2.0-beta.3. Explains that genericity comes from the styler call() and is copied to the generated widget, while the annotated top-level recipe function itself must be non-generic. Shows @MixWidget() on a non-generic acmeSelectStyle function whose Style suffix infers AcmeSelect, with a named, non-nullable enum parameter named exactly variant; this generates AcmeSelect. constructors and lets T infer from values/items/callbacks. Checks that generated call parameters do not leak Remix-only public types; if they do, chooses a hand-written target-vocabulary facade. Mentions that nullable, required-positional, differently named, or non-enum variant parameters do not generate named constructors, while optional positional factory parameters are rejected. Requires a package-local spike and a second build_runner run that leaves generated output unchanged before committing to the API.", "files": [], "expectations": [ "Reads and reports the workspace-tested remix, mix, mix_annotations, and mix_generator constraints.", diff --git a/skills/building-remix-design-system/references/component-playbook.md b/skills/building-remix-design-system/references/component-playbook.md index 24d62c16a..fc3b8b349 100644 --- a/skills/building-remix-design-system/references/component-playbook.md +++ b/skills/building-remix-design-system/references/component-playbook.md @@ -109,7 +109,7 @@ below is validated against them and should move with them: ```yaml dependencies: - remix: ^1.0.0-beta.8 + remix: ^1.0.0-beta.9 mix: ^2.2.0-beta.5 mix_annotations: ^2.2.0-beta.1 diff --git a/skills/using-remix/SKILL.md b/skills/using-remix/SKILL.md index 13d13581b..a3d044f81 100644 --- a/skills/using-remix/SKILL.md +++ b/skills/using-remix/SKILL.md @@ -51,7 +51,7 @@ import 'package:remix/remix.dart'; For a new Fortal consumer, initialize the preset and add the needed items: -This checkout prepares the first CLI release and requires Remix beta.8. +This checkout prepares the first CLI release and requires Remix beta.9. Until both releases are available, use the checkout setup in `packages/remix_cli/README.md`. Use the hosted commands below after publication. From 39dcb0aa1b12e69b31f96591109bf4e17837ee3e Mon Sep 17 00:00:00 2001 From: Leo Farias Date: Mon, 7 Sep 2026 12:01:15 -0400 Subject: [PATCH 09/12] fix(remix_fortal): keep copied themes clean under Flutter lints --- .../fortal_fixture/analysis_options.yaml | 3 ++ .../fortal/templates/theme/computed.dart.tmpl | 1 + .../templates/theme/radix_colors.dart.tmpl | 2 +- .../templates/theme/theme_data.dart.tmpl | 38 ++++++++----------- .../remix_fortal/lib/src/theme/computed.dart | 1 + .../lib/src/theme/radix_colors.dart | 2 +- .../lib/src/theme/theme_data.dart | 38 ++++++++----------- .../scripts/generate_radix_colors.dart | 2 +- 8 files changed, 38 insertions(+), 49 deletions(-) diff --git a/open_code/fortal_fixture/analysis_options.yaml b/open_code/fortal_fixture/analysis_options.yaml index 4ab697645..f20a583c8 100644 --- a/open_code/fortal_fixture/analysis_options.yaml +++ b/open_code/fortal_fixture/analysis_options.yaml @@ -15,7 +15,10 @@ linter: rules: - always_declare_return_types - avoid_print + - dangling_library_doc_comments + - overridden_fields - prefer_const_constructors - prefer_const_declarations - prefer_final_locals + - unnecessary_library_name - unnecessary_parenthesis diff --git a/packages/remix_cli/lib/src/registry/fortal/templates/theme/computed.dart.tmpl b/packages/remix_cli/lib/src/registry/fortal/templates/theme/computed.dart.tmpl index 5e2197e85..cbec44d7b 100644 --- a/packages/remix_cli/lib/src/registry/fortal/templates/theme/computed.dart.tmpl +++ b/packages/remix_cli/lib/src/registry/fortal/templates/theme/computed.dart.tmpl @@ -5,6 +5,7 @@ /// original Radix Colors data. /// /// Components should use these functional roles rather than raw color steps. +library; // Documentation for all public APIs is provided below diff --git a/packages/remix_cli/lib/src/registry/fortal/templates/theme/radix_colors.dart.tmpl b/packages/remix_cli/lib/src/registry/fortal/templates/theme/radix_colors.dart.tmpl index 2c85bdd4e..adaf2f318 100644 --- a/packages/remix_cli/lib/src/registry/fortal/templates/theme/radix_colors.dart.tmpl +++ b/packages/remix_cli/lib/src/registry/fortal/templates/theme/radix_colors.dart.tmpl @@ -4,7 +4,7 @@ // Radix Colors version: bundled with Radix Themes 3.3.0 // Source integrity: sha512-I0/h2CRNTpYNB7Mi3xFIvSsQq5a108d7kK8dTO5zp5b9HR5QJXKag6B8tjpz2ITkVYkFdkGk45doNkSr7OxwNw== -library radix_colors; +library; import 'package:flutter/painting.dart'; diff --git a/packages/remix_cli/lib/src/registry/fortal/templates/theme/theme_data.dart.tmpl b/packages/remix_cli/lib/src/registry/fortal/templates/theme/theme_data.dart.tmpl index c84e2fbe3..1767ad3d8 100644 --- a/packages/remix_cli/lib/src/registry/fortal/templates/theme/theme_data.dart.tmpl +++ b/packages/remix_cli/lib/src/registry/fortal/templates/theme/theme_data.dart.tmpl @@ -148,37 +148,29 @@ class {{typePrefix}}ThemeConfig { @immutable class {{typePrefix}}ThemeData extends {{typePrefix}}ThemeConfig { const {{typePrefix}}ThemeData({ - required this.accent, - required this.gray, - required this.brightness, - required this.panelBackground, - required this.radius, - required this.scaling, - required this.hasBackground, - }) : super( - accent: accent, - gray: gray, - brightness: brightness, - panelBackground: panelBackground, - radius: radius, - scaling: scaling, - hasBackground: hasBackground, - ); + required {{typePrefix}}AccentColor super.accent, + required {{typePrefix}}GrayColor super.gray, + required Brightness super.brightness, + required {{typePrefix}}PanelBackground super.panelBackground, + required {{typePrefix}}Radius super.radius, + required {{typePrefix}}Scaling super.scaling, + required bool super.hasBackground, + }); @override - final {{typePrefix}}AccentColor accent; + {{typePrefix}}AccentColor get accent => super.accent!; @override - final {{typePrefix}}GrayColor gray; + {{typePrefix}}GrayColor get gray => super.gray!; @override - final Brightness brightness; + Brightness get brightness => super.brightness!; @override - final {{typePrefix}}PanelBackground panelBackground; + {{typePrefix}}PanelBackground get panelBackground => super.panelBackground!; @override - final {{typePrefix}}Radius radius; + {{typePrefix}}Radius get radius => super.radius!; @override - final {{typePrefix}}Scaling scaling; + {{typePrefix}}Scaling get scaling => super.scaling!; @override - final bool hasBackground; + bool get hasBackground => super.hasBackground!; @override bool get isDark => brightness == .dark; diff --git a/packages/remix_fortal/lib/src/theme/computed.dart b/packages/remix_fortal/lib/src/theme/computed.dart index fc4e3ee0b..cc1510a9b 100644 --- a/packages/remix_fortal/lib/src/theme/computed.dart +++ b/packages/remix_fortal/lib/src/theme/computed.dart @@ -5,6 +5,7 @@ /// original Radix Colors data. /// /// Components should use these functional roles rather than raw color steps. +library; // Documentation for all public APIs is provided below diff --git a/packages/remix_fortal/lib/src/theme/radix_colors.dart b/packages/remix_fortal/lib/src/theme/radix_colors.dart index 2c85bdd4e..adaf2f318 100644 --- a/packages/remix_fortal/lib/src/theme/radix_colors.dart +++ b/packages/remix_fortal/lib/src/theme/radix_colors.dart @@ -4,7 +4,7 @@ // Radix Colors version: bundled with Radix Themes 3.3.0 // Source integrity: sha512-I0/h2CRNTpYNB7Mi3xFIvSsQq5a108d7kK8dTO5zp5b9HR5QJXKag6B8tjpz2ITkVYkFdkGk45doNkSr7OxwNw== -library radix_colors; +library; import 'package:flutter/painting.dart'; diff --git a/packages/remix_fortal/lib/src/theme/theme_data.dart b/packages/remix_fortal/lib/src/theme/theme_data.dart index 9f537833a..cbcaa4579 100644 --- a/packages/remix_fortal/lib/src/theme/theme_data.dart +++ b/packages/remix_fortal/lib/src/theme/theme_data.dart @@ -148,37 +148,29 @@ class FortalThemeConfig { @immutable class FortalThemeData extends FortalThemeConfig { const FortalThemeData({ - required this.accent, - required this.gray, - required this.brightness, - required this.panelBackground, - required this.radius, - required this.scaling, - required this.hasBackground, - }) : super( - accent: accent, - gray: gray, - brightness: brightness, - panelBackground: panelBackground, - radius: radius, - scaling: scaling, - hasBackground: hasBackground, - ); + required FortalAccentColor super.accent, + required FortalGrayColor super.gray, + required Brightness super.brightness, + required FortalPanelBackground super.panelBackground, + required FortalRadius super.radius, + required FortalScaling super.scaling, + required bool super.hasBackground, + }); @override - final FortalAccentColor accent; + FortalAccentColor get accent => super.accent!; @override - final FortalGrayColor gray; + FortalGrayColor get gray => super.gray!; @override - final Brightness brightness; + Brightness get brightness => super.brightness!; @override - final FortalPanelBackground panelBackground; + FortalPanelBackground get panelBackground => super.panelBackground!; @override - final FortalRadius radius; + FortalRadius get radius => super.radius!; @override - final FortalScaling scaling; + FortalScaling get scaling => super.scaling!; @override - final bool hasBackground; + bool get hasBackground => super.hasBackground!; @override bool get isDark => brightness == .dark; diff --git a/packages/remix_fortal/scripts/generate_radix_colors.dart b/packages/remix_fortal/scripts/generate_radix_colors.dart index 5df389a60..1a2cd7a03 100644 --- a/packages/remix_fortal/scripts/generate_radix_colors.dart +++ b/packages/remix_fortal/scripts/generate_radix_colors.dart @@ -254,7 +254,7 @@ class RadixColorGenerator { } const _radixColorApi = ''' -library radix_colors; +library; import 'package:flutter/painting.dart'; From da056eaa9bcc6cbee05c8f82b8aaa510b3226f35 Mon Sep 17 00:00:00 2001 From: Leo Farias Date: Mon, 7 Sep 2026 12:01:29 -0400 Subject: [PATCH 10/12] docs(remix_cli): correct the preset guide and checkout workflow --- docs/remix-cli-guide.html | 72 +++++++++++++++++++++++++----------- open_code/README.md | 12 ++++-- packages/remix_cli/README.md | 17 +++++---- 3 files changed, 69 insertions(+), 32 deletions(-) diff --git a/docs/remix-cli-guide.html b/docs/remix-cli-guide.html index 6377e7209..f0700d000 100644 --- a/docs/remix-cli-guide.html +++ b/docs/remix-cli-guide.html @@ -600,6 +600,9 @@

    Choose a preset

    The preset is recorded in remix.yaml and cannot change after initialization. Recipes from different presets do not mix because their tokens and variant vocabularies differ.

    +

    The following walkthrough, file trees, and catalog use the default preset with the Acme prefix. + Fortal uses AcmeScope and constructors such as AcmeButton.solid. + Its theme files and component axes differ.

    @@ -638,9 +641,26 @@

    Install the CLI

    This walkthrough uses one command name all the way through: remix.

    -

    The CLI has not been published. The hosted commands below apply after its - first release and Remix beta.9. For now, follow the - checkout installation instructions.

    +

    The CLI has not been published. The hosted commands below require its first release and Remix beta.9. + Use a checkout until both packages are available.

    + +
    + Use a checkout before publication +

    Create pubspec_overrides.yaml in your Flutter application. Replace both checkout paths with your local repository path.

    +
    +
    pubspec_overrides.yaml
    +
    dependency_overrides:
    +  remix:
    +    path: /path/to/remix/packages/remix
    +
    +
    +
    Install the checkout CLI in the application
    +
    dart pub add "dev:remix_cli@{path: /path/to/remix/packages/remix_cli}"
    +dart run remix_cli:remix --version
    +
    +

    For each following command, replace remix with dart run remix_cli:remix. + After publication, remove the Remix override and run flutter pub get.

    +

    After publication, install the exact CLI version this guide was written against. If Dart prints a PATH instruction, follow it and reopen the shell before you continue.

    @@ -653,11 +673,11 @@

    Install the CLI

    RESULT

    The command is ready. - Every command below now works exactly as written. Your Flutter app has not changed yet.

    + Global activation leaves your application unchanged. Project-local installation updates its development dependencies and lockfile.

    Requirements

      -
    • Flutter 3.44 or newer. The CLI runs flutter --version --machine before it touches a file.
    • +
    • Flutter 3.44 or newer on PATH. The CLI inspects that SDK before installation or --diff.
    • A Flutter package root. Its pubspec.yaml must list the Flutter SDK under dependencies.
    • Git on PATH, for --diff only.
    @@ -701,8 +721,9 @@

    Initialize

    remix.yaml
    -
    schema: 1
    +  
    schema: 2
     prefix: Acme
    +preset: default
     paths:
       ui: lib/ui
    @@ -958,6 +979,10 @@

    The first render

    RESULT

    This is the first visual step. Step 4 supplied the source. Your widget tree decides where that source appears.

    +

    A fresh Flutter project can report generated-code lints. Apply the + generated adapter exclusion before running flutter analyze. + Keep authored recipes under analysis.

    +

    AcmeButton.primary(…) is a named constructor. The generator emits one for each value of the variant enum, so AcmeButton.destructive(label: 'Delete') works the same way.

    @@ -1203,9 +1228,10 @@

    The whole theme WIDEST

    -

    Add a token here and to AcmeThemeData, and the scope map exposes it. Rename one, - and the analyzer finds every recipe that used it. Every recipe reads its corner radius from one - token, so one value turns every control into a pill.

    +

    Add the token to tokens.dart, its value to AcmeThemeData, and its mapping to AcmeThemeData.tokens. + Include the value in constructors, copyWith, equality, and hashCode. + The scope reads this explicit map. A token does not enter the map automatically.

    +

    Recipes that read AcmeTokens.radius share that radius value. Controls with fixed or circular geometry keep their own shape.

    lib/main.dart — one value changed between two builds
    @@ -1220,7 +1246,7 @@

    The whole theme WIDEST

    The same brand theme buttons with a radius token of 20, which makes pill buttons.
    After: radius token 20
    -
    EFFECT

    Every recipe changes. +

    EFFECT

    Recipes that use the radius token change. The recipe files were untouched between these two builds. Only the theme value moved.

    Light and dark are the same mechanism. Switching at runtime changes only the value passed to @@ -1375,8 +1401,8 @@

    What the recipe can generate next

    This section is a design preview
    -

    Every other section on this page is verified against a real installation. - This one is not built yet. It describes the intended direction. The two +

    The other sections describe implemented CLI features. Hosted commands require package publication. + This section is not built yet. It describes the intended direction. The two contracts it targets already exist, so the shapes below are real; the generator that would emit them is not.

    @@ -1411,8 +1437,7 @@

    The Muse A2UI catalog schema

    the catalog entry a generator would emit for AcmeButton
    {
       "component": { "const": "AcmeButton" },
    -  "description": "Labeled control that dispatches an allowed action.
    -                  Use AcmeIconButton when a compact icon-only control is clearer.",
    +  "description": "A labeled control that dispatches an allowed action. Use AcmeIconButton for a compact control without visible text.",
       "properties": {
         "label":   { "type": "string",
                      "description": "Visible action label." },
    @@ -1600,8 +1625,9 @@ 

    Reference

    Files it writes

    -

    Everything below lib/ui/ is yours. The CLI touches nothing else in your app except - the dependency lists in pubspec.yaml.

    +

    The application owns the installed source below lib/ui/. + The CLI also writes remix.yaml and updates dependency declarations in pubspec.yaml. + Pub updates the lockfile and package metadata. The generator writes adapters and its build cache.

    @@ -1758,8 +1784,9 @@

    remix.yaml keys

    FileWhat it holdsGenerated?
    - + +
    KeyDefaultRule
    schema1Must be exactly 1.
    schema2New files use schema 2. Legacy schema 1 remains readable and selects the default preset.
    prefixUiASCII UpperCamel. Its lowercase form cannot be a Dart reserved word. Remix and Mix are reserved for runtime dependencies.
    presetdefaultSchema 2 requires default or fortal. Schema 1 has no preset key. Choose the preset at initialization.
    paths.uilib/uiNormalized, project-relative, and inside lib/.
    @@ -2052,9 +2079,9 @@

    Provenance

    What produced the commands, the source, and the images on this page.

    -

    Every command and status line on this page was run against a fresh Flutter application created - for this guide. Long planning output is shortened to the lines that affect your next decision, and - every shortened block marks the elision.

    +

    The command examples describe CLI 0.1.0. Hosted installation requires publication; the checkout workflow works before publication. + The September 7, 2026 review repeated initialization and component installation in fresh applications for both presets. + Long planning output is shortened, and each shortened block marks the elision.

    One part is different. What the recipe can generate next is a design preview, and it says so at @@ -2069,13 +2096,16 @@

    Provenance

    host-neutral gallery fixture that tool/check_open_code.dart installs. Both use the same installed lib/ui/ layer.

    +

    The embedded renders predate the beta.9 release target. They show the default recipes, not a hosted beta.9 installation. + The table below describes the current checkout verification toolchain.

    +
    - + diff --git a/open_code/README.md b/open_code/README.md index 825940e10..545b9477f 100644 --- a/open_code/README.md +++ b/open_code/README.md @@ -97,9 +97,9 @@ failure. The prefixes `Remix` and `Mix` are reserved for runtime dependencies. Use an application prefix such as `Ui` or `Acme`. -`add button` resolves the Theme dependency first, adds compatible hosted -package constraints when they are missing, writes the authored source, updates -the managed barrel, and generates only the declared adapter: +The default preset installs Theme before Button. The command adds missing +dependency constraints, writes source, updates the barrel, and generates the +adapter: ```text lib/ui/ @@ -113,11 +113,15 @@ lib/ui/ button.g.dart generated; do not hand-edit ``` -Every later component `add` drops one more pair into `components/` and extends +Each later default component `add` adds another pair to `components/` and extends the barrel's managed block. Existing authored source stays untouched. The focused build also includes every installed generated adapter, so a dependency change cannot remove an earlier generated part. +Fortal also installs shared source items without generated parts, such as +`base_button` and `typography`. Use `add --dry-run` to inspect each +preset's files and dependencies. + `dart run remix_cli:remix add icons` instead adds `lib/ui/icons.dart`, declares `remix_ui_icons`, and exposes a small, application-owned `UiIcons` alias set without a generated adapter. Add or rename aliases there as the application diff --git a/packages/remix_cli/README.md b/packages/remix_cli/README.md index 3d60649cc..66e831baa 100644 --- a/packages/remix_cli/README.md +++ b/packages/remix_cli/README.md @@ -117,7 +117,7 @@ dependencies, formats the authored files, runs generation only for the declared `button.g.dart`, and analyzes the installed UI path. It does not create or modify `build.yaml`. -With the default path, the application receives: +With the default preset and path, the application receives: ```text lib/ui/ @@ -131,11 +131,13 @@ lib/ui/ button.g.dart ``` -One `add` installs one item. Repeat it for each component you want; every -component item drops one authored file and one generated part into -`components/` and extends the barrel. Existing authored source stays -untouched. The focused build also includes every installed generated adapter, -so a dependency change cannot remove an earlier generated part. +One `add` installs one item and its registry dependencies. Repeat it for each +component you need. Default component items add an authored file and a +generated part to `components/`. Fortal also has shared source items without +generated parts, such as `base_button` and `typography`. +Existing authored source stays untouched. When generation runs, the build +includes every installed adapter. This protects earlier generated parts when +dependencies change. `dart run remix_cli:remix add icons` is the deliberate exception. It adds `lib/ui/icons.dart`, declares `remix_ui_icons`, and exposes a small, @@ -143,7 +145,8 @@ application-owned `UiIcons` alias set with no generated adapter. Add or rename aliases there as your interface evolves. The complete 318-icon catalog remains one direct `package:remix_ui_icons/remix_ui_icons.dart` import away. -Most items generate one widget. Four do not: `checkbox` also generates +Most default component items generate one widget. Four generate several: +`checkbox` also generates `UiCheckboxGroupItem`, `textfield` generates `UiTextField` and `UiTextArea`, `tabs` generates `UiTabBar`, `UiTab`, and `UiTabView`, and `chart` generates `UiLineChart`, `UiBarChart`, and `UiPieChart`. From d22de58156a0f6fcc1e571f249a79eacb05d4cbc Mon Sep 17 00:00:00 2001 From: Leo Farias Date: Mon, 7 Sep 2026 12:14:39 -0400 Subject: [PATCH 11/12] docs(remix_cli): place theme scopes below the application host --- docs/open-code.mdx | 5 +++++ docs/remix-cli-guide.html | 4 ++++ packages/remix_cli/README.md | 5 +++++ .../fortal/templates/theme/theme_scope.dart.tmpl | 10 ++++++---- packages/remix_fortal/lib/src/theme/theme_scope.dart | 10 ++++++---- 5 files changed, 26 insertions(+), 8 deletions(-) diff --git a/docs/open-code.mdx b/docs/open-code.mdx index 2204418eb..a5a83cccf 100644 --- a/docs/open-code.mdx +++ b/docs/open-code.mdx @@ -351,6 +351,11 @@ lib/ui/ └── button.g.dart ``` +Place `AcmeScope` below the application host. For a routed Material app, wrap +the child of `MaterialApp.builder` with `AcmeScope`. Routes and dialogs then +inherit its tokens and text defaults. The scope example lives in +`lib/ui/theme/theme_scope.dart`. + These are application files, not forwarding wrappers around `package:remix_fortal`. The authored recipes and pinned Radix Themes 3.3.0 color data are copied from the same source used by the repository's parity diff --git a/docs/remix-cli-guide.html b/docs/remix-cli-guide.html index f0700d000..6b9a85345 100644 --- a/docs/remix-cli-guide.html +++ b/docs/remix-cli-guide.html @@ -603,6 +603,10 @@

    Choose a preset

    The following walkthrough, file trees, and catalog use the default preset with the Acme prefix. Fortal uses AcmeScope and constructors such as AcmeButton.solid. Its theme files and component axes differ.

    +

    Place AcmeScope below the application host. + In a routed Material app, wrap the child of MaterialApp.builder with AcmeScope. + Routes and dialogs then inherit its tokens and text defaults. + See the example in lib/ui/theme/theme_scope.dart.

    diff --git a/packages/remix_cli/README.md b/packages/remix_cli/README.md index 66e831baa..d3ed2bd0e 100644 --- a/packages/remix_cli/README.md +++ b/packages/remix_cli/README.md @@ -103,6 +103,11 @@ does not depend directly on `mix`, `naked_ui`, or `remix_fortal`; it can edit the Radix color data, tokens, recipes, and instance overrides as ordinary app source. +Place `AcmeScope` below the application host. For a routed Material app, wrap +the child of `MaterialApp.builder` with `AcmeScope`. Routes and dialogs then +inherit its tokens and text defaults. See `lib/ui/theme/theme_scope.dart` for +the scope example. + The prefixes `Remix` and `Mix` are reserved for runtime dependencies. Use an application prefix such as `Ui` or `Acme`. diff --git a/packages/remix_cli/lib/src/registry/fortal/templates/theme/theme_scope.dart.tmpl b/packages/remix_cli/lib/src/registry/fortal/templates/theme/theme_scope.dart.tmpl index 6b6834a0c..87eda0c0d 100644 --- a/packages/remix_cli/lib/src/registry/fortal/templates/theme/theme_scope.dart.tmpl +++ b/packages/remix_cli/lib/src/registry/fortal/templates/theme/theme_scope.dart.tmpl @@ -44,12 +44,14 @@ Widget _{{valuePrefix}}RootTextStyle({ /// Widget that provides {{typePrefix}} design tokens to its subtree via [MixScope]. /// -/// Use [{{typePrefix}}Scope] at the root of your app (or around any subtree that uses -/// {{typePrefix}} styles) so that [{{typePrefix}}Tokens] resolve to actual values. +/// Place [{{typePrefix}}Scope] below the application host so its text defaults apply. +/// For a routed app, wrap the navigator in the host's builder. This also keeps +/// [{{typePrefix}}Tokens] available to routes and dialogs. /// /// ```dart -/// {{typePrefix}}Scope( -/// child: WidgetsApp(...), +/// MaterialApp( +/// builder: (_, child) => {{typePrefix}}Scope(child: child!), +/// home: const HomePage(), /// ) /// ``` class {{typePrefix}}Scope extends StatelessWidget { diff --git a/packages/remix_fortal/lib/src/theme/theme_scope.dart b/packages/remix_fortal/lib/src/theme/theme_scope.dart index 68aed417c..34652d771 100644 --- a/packages/remix_fortal/lib/src/theme/theme_scope.dart +++ b/packages/remix_fortal/lib/src/theme/theme_scope.dart @@ -44,12 +44,14 @@ Widget _fortalRootTextStyle({ /// Widget that provides Fortal design tokens to its subtree via [MixScope]. /// -/// Use [FortalScope] at the root of your app (or around any subtree that uses -/// Fortal styles) so that [FortalTokens] resolve to actual values. +/// Place [FortalScope] below the application host so its text defaults apply. +/// For a routed app, wrap the navigator in the host's builder. This also keeps +/// [FortalTokens] available to routes and dialogs. /// /// ```dart -/// FortalScope( -/// child: WidgetsApp(...), +/// MaterialApp( +/// builder: (_, child) => FortalScope(child: child!), +/// home: const HomePage(), /// ) /// ``` class FortalScope extends StatelessWidget { From 68c78e2f2cae6b647a249dafda3fb53e1e9fc267 Mon Sep 17 00:00:00 2001 From: Leo Farias Date: Mon, 7 Sep 2026 12:27:54 -0400 Subject: [PATCH 12/12] docs(remix_cli): clarify dialog host requirements --- docs/remix-cli-guide.html | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/docs/remix-cli-guide.html b/docs/remix-cli-guide.html index 6b9a85345..12e5d4c3a 100644 --- a/docs/remix-cli-guide.html +++ b/docs/remix-cli-guide.html @@ -1711,7 +1711,7 @@

    Dependency rules

    An incompatible constraint also fails preflight. The CLI never widens a constraint you chose.

    When something fails

    -

    Three problems meet a fresh app first.

    +

    These checks cover common problems in a fresh app.

    Your linter flags the generated adapters. A default Flutter app includes flutter_lints, and the current generator emits explicit this. qualifiers. @@ -1729,9 +1729,17 @@

    When something fails

    With that exclude, flutter analyze lib reports No issues found!.

    Text field selection handles fail. An installed text field needs an - Overlay ancestor once it takes focus. Material, Cupertino, and any routed + Overlay ancestor once it takes focus. MaterialApp, CupertinoApp, and any routed WidgetsApp already provide one; a bare WidgetsApp(builder: …) does not.

    +

    A default dialog fills the viewport. The default recipe styles the panel. + Wrap AcmeDialog in Center inside the showRemixDialog builder. + This lets the panel's maximum width apply. The Fortal recipe supplies its own viewport alignment.

    + +

    Dialog text has unexpected decoration. The dialog captures the invoking context's inherited themes. + Use a context below your intended DefaultTextStyle. + For Fortal, place AcmeScope in the application builder, below the host.

    +

    “Run remix init before remix add.” The current directory has no remix.yaml. Run the CLI from the package that owns pubspec.yaml. In a workspace, use the member package, not the workspace root.

    ComponentVersion
    remix_cli0.1.0
    Flutter3.44.0 stable, framework 559ffa3f75
    Dart3.12.0 stable
    remix1.0.0-beta.9, the registry snapshot version
    remix1.0.0-beta.9, the current registry target; verified from a checkout before publication
    mix2.2.0-beta.5
    mix_annotations2.2.0-beta.1
    mix_generator2.2.0-beta.3