diff --git a/.github/.cspell/dart_dictionary.txt b/.github/.cspell/dart_dictionary.txt index 6426ea12..a2bb11e2 100644 --- a/.github/.cspell/dart_dictionary.txt +++ b/.github/.cspell/dart_dictionary.txt @@ -3,6 +3,8 @@ audioplayers # A Flutter plugin to play multiple simultaneously audio files. cupertino # Flutter module containing iOS-style widgets. endtemplate # Dart doc macro used to end a reusable piece of documentation. fluttium # User flow testing tool for Flutter. +lerp # Linear interpolation, commonly used in Flutter for animating between values. +widgetbook # A Flutter package for cataloging widgets. LTWH # From Flutter, abbreviation left, top, width and height. macos # MacOs, apple's operating system for Mac. mockingjay # A Flutter package for mocking navigation. diff --git a/.github/.cspell/words_dictionary.txt b/.github/.cspell/words_dictionary.txt index 70292778..5ea562a6 100644 --- a/.github/.cspell/words_dictionary.txt +++ b/.github/.cspell/words_dictionary.txt @@ -28,6 +28,7 @@ nicorn # "unicorn", but without the "u". unmutes # To allow to produce sound again. verygood # Very Good, but without a space. xcode # Apple's integrated development environment. +xxlg # Abbreviation for extra extra large, used in spacing scales. xcworkspace # Abbreviation for Xcode workspace. xcassets # Abbreviation for Xcode assets. hola # part of translations example diff --git a/.github/workflows/very_good_app_ui.yaml b/.github/workflows/very_good_app_ui.yaml new file mode 100644 index 00000000..ff51c43e --- /dev/null +++ b/.github/workflows/very_good_app_ui.yaml @@ -0,0 +1,59 @@ +name: very_good_app_ui + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +on: + push: + paths: + - .github/workflows/very_good_app_ui.yaml + - "very_good_app_ui/**" + branches: + - main + pull_request: + paths: + - .github/workflows/very_good_app_ui.yaml + - "very_good_app_ui/**" + branches: + - main + +jobs: + brick: + runs-on: ubuntu-latest + + steps: + - name: ๐Ÿ“š Git Checkout + uses: actions/checkout@v6 + + - name: ๐Ÿฆ Setup Flutter + uses: subosito/flutter-action@v2 + with: + flutter-version: "3.41.x" + + - name: ๐Ÿงฑ Mason Make + run: | + dart pub global activate mason_cli + mason get + mason make very_good_app_ui -c very_good_app_ui/config.json --on-conflict overwrite -o very_good_app_ui_output + + - name: ๐Ÿ“ฆ Install Dependencies + run: | + dart pub global activate very_good_cli + very_good packages get --recursive very_good_app_ui_output + + - name: โœจ Check Formatting + run: dart format --set-exit-if-changed very_good_app_ui_output + + - name: ๐Ÿ•ต๏ธ Analyze + run: dart analyze --fatal-infos --fatal-warnings very_good_app_ui_output + + - name: ๐Ÿงช Run Tests + run: | + cd very_good_app_ui_output + very_good test -j 4 --recursive --optimization --coverage --test-randomize-ordering-seed random + + - name: ๐Ÿ“Š Check Code Coverage + uses: VeryGoodOpenSource/very_good_coverage@v3 + with: + path: very_good_app_ui_output/coverage/lcov.info diff --git a/.release-please-config.json b/.release-please-config.json index b62b9e2e..5a067200 100644 --- a/.release-please-config.json +++ b/.release-please-config.json @@ -67,6 +67,14 @@ "extra-files": [ "brick.yaml" ] + }, + "very_good_app_ui": { + "release-type": "simple", + "version-file": "version.txt", + "component": "very_good_app_ui", + "extra-files": [ + "brick.yaml" + ] } } } \ No newline at end of file diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 189e3341..b893c814 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -5,6 +5,7 @@ "very_good_docs_site": "1.0.3", "very_good_flame_game": "1.3.0", "very_good_flutter_package": "1.3.0", - "very_good_flutter_plugin": "1.3.0" + "very_good_flutter_plugin": "1.3.0", + "very_good_app_ui": "0.1.0" } \ No newline at end of file diff --git a/mason.yaml b/mason.yaml index 37f2b45f..15ec2c28 100644 --- a/mason.yaml +++ b/mason.yaml @@ -12,4 +12,6 @@ bricks: very_good_flame_game: path: very_good_flame_game very_good_flutter_plugin: - path: very_good_flutter_plugin \ No newline at end of file + path: very_good_flutter_plugin + very_good_app_ui: + path: very_good_app_ui \ No newline at end of file diff --git a/very_good_app_ui/.gitignore b/very_good_app_ui/.gitignore new file mode 100644 index 00000000..e451bd44 --- /dev/null +++ b/very_good_app_ui/.gitignore @@ -0,0 +1,17 @@ +.DS_Store +.atom/ +.idea/* +.vscode/* + +# Files and directories created by pub +.dart_tool/ +.packages +pubspec.lock + +# Files and directories created by mason +.mason/ +mason-lock.json +output/ + +# Files and directories created by MacOS +.DS_Store diff --git a/very_good_app_ui/CHANGELOG.md b/very_good_app_ui/CHANGELOG.md new file mode 100644 index 00000000..3acfdcee --- /dev/null +++ b/very_good_app_ui/CHANGELOG.md @@ -0,0 +1,3 @@ +# 0.1.0 + +- feat: initial release diff --git a/very_good_app_ui/LICENSE b/very_good_app_ui/LICENSE new file mode 100644 index 00000000..04414865 --- /dev/null +++ b/very_good_app_ui/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Very Good Ventures + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/very_good_app_ui/README.md b/very_good_app_ui/README.md new file mode 100644 index 00000000..bdf259a5 --- /dev/null +++ b/very_good_app_ui/README.md @@ -0,0 +1,108 @@ +# Very Good App UI + +[![Very Good Ventures][logo_white]][very_good_ventures_link_dark] + +Developed with ๐Ÿ’™ by [Very Good Ventures][very_good_ventures_link] ๐Ÿฆ„ + +[![License: MIT][license_badge]][license_link] +[![Powered by Mason](https://img.shields.io/endpoint?url=https%3A%2F%2Ftinyurl.com%2Fmason-badge)](https://github.com/felangel/mason) + +A Very Good Flutter app UI package created by Very Good Ventures ๐Ÿฆ„. + +## What's Included โœจ + +- โœ… GitHub Workflow powered by [Very Good Workflows][very_good_workflows_link] +- โœ… Pull Request Template +- โœ… Issue Templates +- โœ… Dependabot Integration +- โœ… Strict lint rules powered by [Very Good Analysis][very_good_analysis_link] +- โœ… 100% Test Coverage +- โœ… Fully Documented Public API +- โœ… MIT License +- โœ… Changelog +- โœ… `ThemeExtension`-based theming with light and dark variants +- โœ… Custom color and spacing tokens +- โœ… Example `AppButton` widget composing Material widgets +- โœ… `BuildContext` extensions for easy theme access +- โœ… Widget test helpers +- โœ… Widgetbook catalog for browsing widgets in isolation + +## Output ๐Ÿ“ฆ + +```sh +โ”œโ”€โ”€ .github +โ”‚ โ”œโ”€โ”€ ISSUE_TEMPLATE +โ”‚ โ”‚ โ”œโ”€โ”€ bug_report.md +โ”‚ โ”‚ โ”œโ”€โ”€ build.md +โ”‚ โ”‚ โ”œโ”€โ”€ chore.md +โ”‚ โ”‚ โ”œโ”€โ”€ ci.md +โ”‚ โ”‚ โ”œโ”€โ”€ config.yml +โ”‚ โ”‚ โ”œโ”€โ”€ documentation.md +โ”‚ โ”‚ โ”œโ”€โ”€ feature_request.md +โ”‚ โ”‚ โ”œโ”€โ”€ performance.md +โ”‚ โ”‚ โ”œโ”€โ”€ refactor.md +โ”‚ โ”‚ โ”œโ”€โ”€ revert.md +โ”‚ โ”‚ โ”œโ”€โ”€ style.md +โ”‚ โ”‚ โ””โ”€โ”€ test.md +โ”‚ โ”œโ”€โ”€ PULL_REQUEST_TEMPLATE.md +โ”‚ โ”œโ”€โ”€ dependabot.yaml +โ”‚ โ””โ”€โ”€ workflows +โ”‚ โ””โ”€โ”€ main.yaml +โ”œโ”€โ”€ .gitignore +โ”œโ”€โ”€ CHANGELOG.md +โ”œโ”€โ”€ LICENSE +โ”œโ”€โ”€ README.md +โ”œโ”€โ”€ analysis_options.yaml +โ”œโ”€โ”€ coverage_badge.svg +โ”œโ”€โ”€ lib +โ”‚ โ”œโ”€โ”€ src +โ”‚ โ”‚ โ”œโ”€โ”€ extensions +โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ build_context_extensions.dart +โ”‚ โ”‚ โ”œโ”€โ”€ theme +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ app_colors.dart +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ app_spacing.dart +โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ app_theme.dart +โ”‚ โ”‚ โ””โ”€โ”€ widgets +โ”‚ โ”‚ โ””โ”€โ”€ app_button.dart +โ”‚ โ””โ”€โ”€ my_app_ui.dart +โ”œโ”€โ”€ pubspec.yaml +โ”œโ”€โ”€ test +โ”‚ โ”œโ”€โ”€ helpers +โ”‚ โ”‚ โ”œโ”€โ”€ helpers.dart +โ”‚ โ”‚ โ””โ”€โ”€ pump_app.dart +โ”‚ โ””โ”€โ”€ src +โ”‚ โ”œโ”€โ”€ theme +โ”‚ โ”‚ โ”œโ”€โ”€ app_colors_test.dart +โ”‚ โ”‚ โ”œโ”€โ”€ app_spacing_test.dart +โ”‚ โ”‚ โ””โ”€โ”€ app_theme_test.dart +โ”‚ โ””โ”€โ”€ widgets +โ”‚ โ””โ”€โ”€ app_button_test.dart +โ””โ”€โ”€ widgetbook + โ”œโ”€โ”€ .gitignore + โ”œโ”€โ”€ analysis_options.yaml + โ”œโ”€โ”€ lib + โ”‚ โ”œโ”€โ”€ main.dart + โ”‚ โ””โ”€โ”€ widgetbook + โ”‚ โ”œโ”€โ”€ use_cases + โ”‚ โ”‚ โ””โ”€โ”€ app_button.dart + โ”‚ โ”œโ”€โ”€ widgetbook.dart + โ”‚ โ””โ”€โ”€ widgets + โ”‚ โ”œโ”€โ”€ use_case_decorator.dart + โ”‚ โ””โ”€โ”€ widgets.dart + โ””โ”€โ”€ pubspec.yaml +``` + +By default `mason make` will generate the output in the current working directory but a custom output directory can be specified via the [-o option][mason_output_dir]: + +```sh +mason make very_good_app_ui -o ./output_folder +``` + +[mason_output_dir]: https://docs.brickhub.dev/mason-make#-custom-output-directory +[license_badge]: https://img.shields.io/badge/license-MIT-blue.svg +[license_link]: https://opensource.org/licenses/MIT +[logo_white]: https://raw.githubusercontent.com/VGVentures/very_good_brand/main/styles/README/vgv_logo_white.png#gh-dark-mode-only +[very_good_analysis_link]: https://pub.dev/packages/very_good_analysis +[very_good_ventures_link_dark]: https://verygood.ventures#gh-dark-mode-only +[very_good_ventures_link]: https://verygood.ventures +[very_good_workflows_link]: https://github.com/VeryGoodOpenSource/very_good_workflows diff --git a/very_good_app_ui/__brick__/.github/ISSUE_TEMPLATE/bug_report.md b/very_good_app_ui/__brick__/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..50a4c7b8 --- /dev/null +++ b/very_good_app_ui/__brick__/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,29 @@ +--- +name: Bug Report +about: Create a report to help us improve +title: "fix: " +labels: bug +--- + +**Description** + +A clear and concise description of what the bug is. + +**Steps To Reproduce** + +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected Behavior** + +A clear and concise description of what you expected to happen. + +**Screenshots** + +If applicable, add screenshots to help explain your problem. + +**Additional Context** + +Add any other context about the problem here. diff --git a/very_good_app_ui/__brick__/.github/ISSUE_TEMPLATE/build.md b/very_good_app_ui/__brick__/.github/ISSUE_TEMPLATE/build.md new file mode 100644 index 00000000..0cf8e62c --- /dev/null +++ b/very_good_app_ui/__brick__/.github/ISSUE_TEMPLATE/build.md @@ -0,0 +1,14 @@ +--- +name: Build System +about: Changes that affect the build system or external dependencies +title: "build: " +labels: build +--- + +**Description** + +Describe what changes need to be done to the build system and why. + +**Requirements** + +- [ ] The build system is passing diff --git a/very_good_app_ui/__brick__/.github/ISSUE_TEMPLATE/chore.md b/very_good_app_ui/__brick__/.github/ISSUE_TEMPLATE/chore.md new file mode 100644 index 00000000..498ebfd8 --- /dev/null +++ b/very_good_app_ui/__brick__/.github/ISSUE_TEMPLATE/chore.md @@ -0,0 +1,14 @@ +--- +name: Chore +about: Other changes that don't modify src or test files +title: "chore: " +labels: chore +--- + +**Description** + +Clearly describe what change is needed and why. If this changes code then please use another issue type. + +**Requirements** + +- [ ] No functional changes to the code diff --git a/very_good_app_ui/__brick__/.github/ISSUE_TEMPLATE/ci.md b/very_good_app_ui/__brick__/.github/ISSUE_TEMPLATE/ci.md new file mode 100644 index 00000000..fa2dd9e2 --- /dev/null +++ b/very_good_app_ui/__brick__/.github/ISSUE_TEMPLATE/ci.md @@ -0,0 +1,14 @@ +--- +name: Continuous Integration +about: Changes to the CI configuration files and scripts +title: "ci: " +labels: ci +--- + +**Description** + +Describe what changes need to be done to the ci/cd system and why. + +**Requirements** + +- [ ] The ci system is passing diff --git a/very_good_app_ui/__brick__/.github/ISSUE_TEMPLATE/config.yml b/very_good_app_ui/__brick__/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..3ba13e0c --- /dev/null +++ b/very_good_app_ui/__brick__/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1 @@ +blank_issues_enabled: false diff --git a/very_good_app_ui/__brick__/.github/ISSUE_TEMPLATE/documentation.md b/very_good_app_ui/__brick__/.github/ISSUE_TEMPLATE/documentation.md new file mode 100644 index 00000000..f494a4d9 --- /dev/null +++ b/very_good_app_ui/__brick__/.github/ISSUE_TEMPLATE/documentation.md @@ -0,0 +1,14 @@ +--- +name: Documentation +about: Improve the documentation so all collaborators have a common understanding +title: "docs: " +labels: documentation +--- + +**Description** + +Clearly describe what documentation you are looking to add or improve. + +**Requirements** + +- [ ] Requirements go here diff --git a/very_good_app_ui/__brick__/.github/ISSUE_TEMPLATE/feature_request.md b/very_good_app_ui/__brick__/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..ddd2fcca --- /dev/null +++ b/very_good_app_ui/__brick__/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,18 @@ +--- +name: Feature Request +about: A new feature to be added to the project +title: "feat: " +labels: feature +--- + +**Description** + +Clearly describe what you are looking to add. The more context the better. + +**Requirements** + +- [ ] Checklist of requirements to be fulfilled + +**Additional Context** + +Add any other context or screenshots about the feature request go here. diff --git a/very_good_app_ui/__brick__/.github/ISSUE_TEMPLATE/performance.md b/very_good_app_ui/__brick__/.github/ISSUE_TEMPLATE/performance.md new file mode 100644 index 00000000..699b8d45 --- /dev/null +++ b/very_good_app_ui/__brick__/.github/ISSUE_TEMPLATE/performance.md @@ -0,0 +1,14 @@ +--- +name: Performance Update +about: A code change that improves performance +title: "perf: " +labels: performance +--- + +**Description** + +Clearly describe what code needs to be changed and what the performance impact is going to be. Bonus point's if you can tie this directly to user experience. + +**Requirements** + +- [ ] There is no drop in test coverage. diff --git a/very_good_app_ui/__brick__/.github/ISSUE_TEMPLATE/refactor.md b/very_good_app_ui/__brick__/.github/ISSUE_TEMPLATE/refactor.md new file mode 100644 index 00000000..1626c570 --- /dev/null +++ b/very_good_app_ui/__brick__/.github/ISSUE_TEMPLATE/refactor.md @@ -0,0 +1,14 @@ +--- +name: Refactor +about: A code change that neither fixes a bug nor adds a feature +title: "refactor: " +labels: refactor +--- + +**Description** + +Clearly describe what needs to be refactored and why. Please provide links to related issues (bugs or upcoming features) in order to help prioritize. + +**Requirements** + +- [ ] There is no drop in test coverage. diff --git a/very_good_app_ui/__brick__/.github/ISSUE_TEMPLATE/revert.md b/very_good_app_ui/__brick__/.github/ISSUE_TEMPLATE/revert.md new file mode 100644 index 00000000..9d121dc5 --- /dev/null +++ b/very_good_app_ui/__brick__/.github/ISSUE_TEMPLATE/revert.md @@ -0,0 +1,16 @@ +--- +name: Revert Commit +about: Reverts a previous commit +title: "revert: " +labels: revert +--- + +**Description** + +Provide a link to a PR/Commit that you are looking to revert and why. + +**Requirements** + +- [ ] Change has been reverted +- [ ] No change in test coverage has happened +- [ ] A new ticket is created for any follow on work that needs to happen diff --git a/very_good_app_ui/__brick__/.github/ISSUE_TEMPLATE/style.md b/very_good_app_ui/__brick__/.github/ISSUE_TEMPLATE/style.md new file mode 100644 index 00000000..02244a7b --- /dev/null +++ b/very_good_app_ui/__brick__/.github/ISSUE_TEMPLATE/style.md @@ -0,0 +1,14 @@ +--- +name: Style Changes +about: Changes that do not affect the meaning of the code (white space, formatting, missing semi-colons, etc) +title: "style: " +labels: style +--- + +**Description** + +Clearly describe what you are looking to change and why. + +**Requirements** + +- [ ] There is no drop in test coverage. diff --git a/very_good_app_ui/__brick__/.github/ISSUE_TEMPLATE/test.md b/very_good_app_ui/__brick__/.github/ISSUE_TEMPLATE/test.md new file mode 100644 index 00000000..431a7ea7 --- /dev/null +++ b/very_good_app_ui/__brick__/.github/ISSUE_TEMPLATE/test.md @@ -0,0 +1,14 @@ +--- +name: Test +about: Adding missing tests or correcting existing tests +title: "test: " +labels: test +--- + +**Description** + +List out the tests that need to be added or changed. Please also include any information as to why this was not covered in the past. + +**Requirements** + +- [ ] There is no drop in test coverage. diff --git a/very_good_app_ui/__brick__/.github/PULL_REQUEST_TEMPLATE.md b/very_good_app_ui/__brick__/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..b05836d3 --- /dev/null +++ b/very_good_app_ui/__brick__/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,28 @@ + + +## Status + +**READY/IN DEVELOPMENT/HOLD** + +## Description + + + +## Type of Change + + + +- [ ] โœจ New feature (non-breaking change which adds functionality) +- [ ] ๐Ÿ› ๏ธ Bug fix (non-breaking change which fixes an issue) +- [ ] โŒ Breaking change (fix or feature that would cause existing functionality to change) +- [ ] ๐Ÿงน Code refactor +- [ ] โœ… Build configuration change +- [ ] ๐Ÿ“ Documentation +- [ ] ๐Ÿ—‘๏ธ Chore +- [ ] ๐Ÿงช Test diff --git a/very_good_app_ui/__brick__/.github/cspell.json b/very_good_app_ui/__brick__/.github/cspell.json new file mode 100644 index 00000000..a099fe2b --- /dev/null +++ b/very_good_app_ui/__brick__/.github/cspell.json @@ -0,0 +1,25 @@ +{ + "version": "0.2", + "$schema": "https://raw.githubusercontent.com/streetsidesoftware/cspell/main/cspell.schema.json", + "dictionaries": ["vgv_allowed", "vgv_forbidden"], + "dictionaryDefinitions": [ + { + "name": "vgv_allowed", + "path": "https://raw.githubusercontent.com/verygoodopensource/very_good_dictionaries/main/allowed.txt", + "description": "Allowed VGV Spellings" + }, + { + "name": "vgv_forbidden", + "path": "https://raw.githubusercontent.com/verygoodopensource/very_good_dictionaries/main/forbidden.txt", + "description": "Forbidden VGV Spellings" + } + ], + "useGitignore": true, + "words": [ + "{{project_name.snakeCase()}}", + "lerp", + "xxlg", + "widgetbook", + "Widgetbook" + ] +} diff --git a/very_good_app_ui/__brick__/.github/dependabot.yaml b/very_good_app_ui/__brick__/.github/dependabot.yaml new file mode 100644 index 00000000..63b035cd --- /dev/null +++ b/very_good_app_ui/__brick__/.github/dependabot.yaml @@ -0,0 +1,11 @@ +version: 2 +enable-beta-ecosystems: true +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" + - package-ecosystem: "pub" + directory: "/" + schedule: + interval: "daily" diff --git a/very_good_app_ui/__brick__/.github/workflows/license_check.yaml b/very_good_app_ui/__brick__/.github/workflows/license_check.yaml new file mode 100644 index 00000000..923e7d94 --- /dev/null +++ b/very_good_app_ui/__brick__/.github/workflows/license_check.yaml @@ -0,0 +1,25 @@ +name: license_check + +concurrency: + group: ${{#mustacheCase}}github.workflow{{/mustacheCase}}-${{#mustacheCase}}github.ref{{/mustacheCase}} + cancel-in-progress: true + +on: + pull_request: + branches: + - main + paths: + - "pubspec.yaml" + - ".github/workflows/license_check.yaml" + push: + branches: + - main + paths: + - "pubspec.yaml" + - ".github/workflows/license_check.yaml" + +jobs: + license_check: + uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/license_check.yml@v1 + with: + allowed: "MIT,BSD-3-Clause,BSD-2-Clause,Apache-2.0" diff --git a/very_good_app_ui/__brick__/.github/workflows/main.yaml b/very_good_app_ui/__brick__/.github/workflows/main.yaml new file mode 100644 index 00000000..b18538da --- /dev/null +++ b/very_good_app_ui/__brick__/.github/workflows/main.yaml @@ -0,0 +1,25 @@ +name: ci + +concurrency: + group: ${{#mustacheCase}}github.workflow{{/mustacheCase}}-${{#mustacheCase}}github.ref{{/mustacheCase}} + cancel-in-progress: true + +on: + pull_request: + branches: + - main + +jobs: + semantic_pull_request: + uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/semantic_pull_request.yml@v1 + + spell-check: + uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/spell_check.yml@v1 + with: + includes: "**/*.md" + modified_files_only: false + + build: + uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 + with: + flutter_version: "3.41.x" diff --git a/very_good_app_ui/__brick__/.gitignore b/very_good_app_ui/__brick__/.gitignore new file mode 100644 index 00000000..2c0a96a3 --- /dev/null +++ b/very_good_app_ui/__brick__/.gitignore @@ -0,0 +1,44 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# VSCode related +.vscode/* + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ +pubspec.lock + +# Web related +lib/generated_plugin_registrant.dart + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Test related +coverage diff --git a/very_good_app_ui/__brick__/CHANGELOG.md b/very_good_app_ui/__brick__/CHANGELOG.md new file mode 100644 index 00000000..bec7d4c8 --- /dev/null +++ b/very_good_app_ui/__brick__/CHANGELOG.md @@ -0,0 +1,3 @@ +# 0.1.0+1 + +- Initial release. diff --git a/very_good_app_ui/__brick__/LICENSE b/very_good_app_ui/__brick__/LICENSE new file mode 100644 index 00000000..a0079717 --- /dev/null +++ b/very_good_app_ui/__brick__/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) {{current_year}} {{org_name}} + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/very_good_app_ui/__brick__/README.md b/very_good_app_ui/__brick__/README.md new file mode 100644 index 00000000..ed2cecdd --- /dev/null +++ b/very_good_app_ui/__brick__/README.md @@ -0,0 +1,112 @@ +# {{project_name.titleCase()}} + +[![style: very good analysis][very_good_analysis_badge]][very_good_analysis_link] +[![Powered by Mason](https://img.shields.io/endpoint?url=https%3A%2F%2Ftinyurl.com%2Fmason-badge)](https://github.com/felangel/mason) +[![License: MIT][license_badge]][license_link] + +{{{description}}} + +## Installation ๐Ÿ’ป + +**โ— In order to start using {{project_name.titleCase()}} you must have the [Flutter SDK][flutter_install_link] installed on your machine.** + +Install via `flutter pub add`: + +```sh +dart pub add {{project_name.snakeCase()}} +``` + +--- + +## Features โœจ + +- **ThemeExtension-based theming** โ€” light and dark theme variants with custom color and spacing tokens via `ThemeExtension` +- **Custom color tokens** โ€” semantic colors for success, warning, and info states via `AppColors` +- **Spacing scale** โ€” consistent spacing tokens from xxs to xxlg via `AppSpacing` +- **BuildContext extensions** โ€” shorthand `context.appColors` and `context.appSpacing` +- **Example widget** โ€” `AppButton` composing Material's `FilledButton` and `OutlinedButton` with app-specific sizing + +## Usage ๐Ÿš€ + +Wrap your app with the theme: + +```dart +import 'package:{{project_name.snakeCase()}}/{{project_name.snakeCase()}}.dart'; + +class MyApp extends StatelessWidget { + @override + Widget build(BuildContext context) { + return MaterialApp( + theme: AppTheme.light, + darkTheme: AppTheme.dark, + home: const MyHomePage(), + ); + } +} +``` + +Use widgets and tokens in your app: + +```dart +AppButton( + onPressed: () {}, + child: const Text('Click me'), +); +``` + +Access custom tokens via context extensions: + +```dart +final colors = context.appColors; +final spacing = context.appSpacing; +``` + +--- + +## Continuous Integration ๐Ÿค– + +{{project_name.titleCase()}} comes with a built-in [GitHub Actions workflow][github_actions_link] powered by [Very Good Workflows][very_good_workflows_link] but you can also add your preferred CI/CD solution. + +Out of the box, on each pull request and push, the CI `formats`, `lints`, and `tests` the code. This ensures the code remains consistent and behaves correctly as you add functionality or make changes. The project uses [Very Good Analysis][very_good_analysis_link] for a strict set of analysis options used by our team. Code coverage is enforced using the [Very Good Workflows][very_good_coverage_link]. + +--- + +## Running Tests ๐Ÿงช + +For first time users, install the [very_good_cli][very_good_cli_link]: + +```sh +dart pub global activate very_good_cli +``` + +To run all unit tests: + +```sh +very_good test --coverage +``` + +To view the generated coverage report you can use [lcov](https://github.com/linux-test-project/lcov). + +```sh +# Generate Coverage Report +genhtml coverage/lcov.info -o coverage/ + +# Open Coverage Report +open coverage/index.html +``` + +[flutter_install_link]: https://docs.flutter.dev/get-started/install +[github_actions_link]: https://docs.github.com/en/actions/learn-github-actions +[license_badge]: https://img.shields.io/badge/license-MIT-blue.svg +[license_link]: https://opensource.org/licenses/MIT +[logo_black]: https://raw.githubusercontent.com/VGVentures/very_good_brand/main/styles/README/vgv_logo_black.png#gh-light-mode-only +[logo_white]: https://raw.githubusercontent.com/VGVentures/very_good_brand/main/styles/README/vgv_logo_white.png#gh-dark-mode-only +[mason_link]: https://github.com/felangel/mason +[very_good_analysis_badge]: https://img.shields.io/badge/style-very_good_analysis-B22C89.svg +[very_good_analysis_link]: https://pub.dev/packages/very_good_analysis +[very_good_cli_link]: https://pub.dev/packages/very_good_cli +[very_good_coverage_link]: https://github.com/marketplace/actions/very-good-coverage +[very_good_ventures_link]: https://verygood.ventures +[very_good_ventures_link_light]: https://verygood.ventures#gh-light-mode-only +[very_good_ventures_link_dark]: https://verygood.ventures#gh-dark-mode-only +[very_good_workflows_link]: https://github.com/VeryGoodOpenSource/very_good_workflows diff --git a/very_good_app_ui/__brick__/analysis_options.yaml b/very_good_app_ui/__brick__/analysis_options.yaml new file mode 100644 index 00000000..8cf0c27c --- /dev/null +++ b/very_good_app_ui/__brick__/analysis_options.yaml @@ -0,0 +1,5 @@ +include: package:very_good_analysis/analysis_options.yaml + +analyzer: + exclude: + - "widgetbook/**" diff --git a/very_good_app_ui/__brick__/coverage_badge.svg b/very_good_app_ui/__brick__/coverage_badge.svg new file mode 100644 index 00000000..499e98ce --- /dev/null +++ b/very_good_app_ui/__brick__/coverage_badge.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + coverage + coverage + 100% + 100% + + diff --git a/very_good_app_ui/__brick__/lib/src/extensions/build_context_extensions.dart b/very_good_app_ui/__brick__/lib/src/extensions/build_context_extensions.dart new file mode 100644 index 00000000..e13db63e --- /dev/null +++ b/very_good_app_ui/__brick__/lib/src/extensions/build_context_extensions.dart @@ -0,0 +1,10 @@ +import 'package:{{project_name.snakeCase()}}/{{project_name.snakeCase()}}.dart'; + +/// Extension on [BuildContext] for easy access to custom theme tokens. +extension AppThemeBuildContext on BuildContext { + /// Returns the [AppColors] from the current theme. + AppColors get appColors => Theme.of(this).extension()!; + + /// Returns the [AppSpacing] from the current theme. + AppSpacing get appSpacing => Theme.of(this).extension()!; +} diff --git a/very_good_app_ui/__brick__/lib/src/theme/app_colors.dart b/very_good_app_ui/__brick__/lib/src/theme/app_colors.dart new file mode 100644 index 00000000..accd943a --- /dev/null +++ b/very_good_app_ui/__brick__/lib/src/theme/app_colors.dart @@ -0,0 +1,69 @@ +import 'package:{{project_name.snakeCase()}}/{{project_name.snakeCase()}}.dart'; + +/// {@template app_colors} +/// Custom color tokens beyond Material's [ColorScheme]. +/// +/// Provides semantic colors for success, warning, and info states +/// along with their on-color variants. +/// {@endtemplate} +class AppColors extends ThemeExtension { + /// {@macro app_colors} + const AppColors({ + required this.success, + required this.onSuccess, + required this.warning, + required this.onWarning, + required this.info, + required this.onInfo, + }); + + /// The color used for success states. + final Color success; + + /// The color used for content on top of [success]. + final Color onSuccess; + + /// The color used for warning states. + final Color warning; + + /// The color used for content on top of [warning]. + final Color onWarning; + + /// The color used for informational states. + final Color info; + + /// The color used for content on top of [info]. + final Color onInfo; + + @override + AppColors copyWith({ + Color? success, + Color? onSuccess, + Color? warning, + Color? onWarning, + Color? info, + Color? onInfo, + }) { + return AppColors( + success: success ?? this.success, + onSuccess: onSuccess ?? this.onSuccess, + warning: warning ?? this.warning, + onWarning: onWarning ?? this.onWarning, + info: info ?? this.info, + onInfo: onInfo ?? this.onInfo, + ); + } + + @override + AppColors lerp(AppColors? other, double t) { + if (other is! AppColors) return this; + return AppColors( + success: Color.lerp(success, other.success, t)!, + onSuccess: Color.lerp(onSuccess, other.onSuccess, t)!, + warning: Color.lerp(warning, other.warning, t)!, + onWarning: Color.lerp(onWarning, other.onWarning, t)!, + info: Color.lerp(info, other.info, t)!, + onInfo: Color.lerp(onInfo, other.onInfo, t)!, + ); + } +} diff --git a/very_good_app_ui/__brick__/lib/src/theme/app_spacing.dart b/very_good_app_ui/__brick__/lib/src/theme/app_spacing.dart new file mode 100644 index 00000000..55d89bf9 --- /dev/null +++ b/very_good_app_ui/__brick__/lib/src/theme/app_spacing.dart @@ -0,0 +1,75 @@ +import 'dart:ui'; + +import 'package:{{project_name.snakeCase()}}/{{project_name.snakeCase()}}.dart'; + +/// {@template app_spacing} +/// Spacing scale tokens for consistent layout throughout the app. +/// {@endtemplate} +class AppSpacing extends ThemeExtension { + /// {@macro app_spacing} + const AppSpacing({ + this.xxs = 4, + this.xs = 8, + this.sm = 12, + this.md = 16, + this.lg = 24, + this.xlg = 32, + this.xxlg = 48, + }); + + /// Extra extra small spacing: 4px. + final double xxs; + + /// Extra small spacing: 8px. + final double xs; + + /// Small spacing: 12px. + final double sm; + + /// Medium spacing: 16px. + final double md; + + /// Large spacing: 24px. + final double lg; + + /// Extra large spacing: 32px. + final double xlg; + + /// Extra extra large spacing: 48px. + final double xxlg; + + @override + AppSpacing copyWith({ + double? xxs, + double? xs, + double? sm, + double? md, + double? lg, + double? xlg, + double? xxlg, + }) { + return AppSpacing( + xxs: xxs ?? this.xxs, + xs: xs ?? this.xs, + sm: sm ?? this.sm, + md: md ?? this.md, + lg: lg ?? this.lg, + xlg: xlg ?? this.xlg, + xxlg: xxlg ?? this.xxlg, + ); + } + + @override + AppSpacing lerp(AppSpacing? other, double t) { + if (other is! AppSpacing) return this; + return AppSpacing( + xxs: lerpDouble(xxs, other.xxs, t)!, + xs: lerpDouble(xs, other.xs, t)!, + sm: lerpDouble(sm, other.sm, t)!, + md: lerpDouble(md, other.md, t)!, + lg: lerpDouble(lg, other.lg, t)!, + xlg: lerpDouble(xlg, other.xlg, t)!, + xxlg: lerpDouble(xxlg, other.xxlg, t)!, + ); + } +} diff --git a/very_good_app_ui/__brick__/lib/src/theme/app_theme.dart b/very_good_app_ui/__brick__/lib/src/theme/app_theme.dart new file mode 100644 index 00000000..9ad6928f --- /dev/null +++ b/very_good_app_ui/__brick__/lib/src/theme/app_theme.dart @@ -0,0 +1,44 @@ +import 'package:{{project_name.snakeCase()}}/{{project_name.snakeCase()}}.dart'; + +/// {@template app_theme} +/// Composes [ThemeData] with [ColorScheme.fromSeed] and custom +/// [ThemeExtension]s for light and dark variants. +/// {@endtemplate} +class AppTheme { + /// The light [ThemeData]. + static ThemeData get light { + const appColors = AppColors( + success: Color(0xFF16A34A), + onSuccess: Color(0xFFFFFFFF), + warning: Color(0xFFCA8A04), + onWarning: Color(0xFFFFFFFF), + info: Color(0xFF2563EB), + onInfo: Color(0xFFFFFFFF), + ); + + return ThemeData( + colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF4F46E5)), + extensions: const [appColors, AppSpacing()], + ); + } + + /// The dark [ThemeData]. + static ThemeData get dark { + const appColors = AppColors( + success: Color(0xFF4ADE80), + onSuccess: Color(0xFF1C1B1F), + warning: Color(0xFFFACC15), + onWarning: Color(0xFF1C1B1F), + info: Color(0xFF60A5FA), + onInfo: Color(0xFF1C1B1F), + ); + + return ThemeData( + colorScheme: ColorScheme.fromSeed( + seedColor: const Color(0xFF4F46E5), + brightness: Brightness.dark, + ), + extensions: const [appColors, AppSpacing()], + ); + } +} diff --git a/very_good_app_ui/__brick__/lib/src/widgets/app_button.dart b/very_good_app_ui/__brick__/lib/src/widgets/app_button.dart new file mode 100644 index 00000000..1465ba60 --- /dev/null +++ b/very_good_app_ui/__brick__/lib/src/widgets/app_button.dart @@ -0,0 +1,92 @@ +import 'package:{{project_name.snakeCase()}}/{{project_name.snakeCase()}}.dart'; + +/// Visual variants for [AppButton]. +enum AppButtonVariant { + /// A filled button with the primary color. + primary, + + /// A tonal filled button with a secondary color. + secondary, + + /// An outlined button. + outline, +} + +/// Size variants for [AppButton]. +enum AppButtonSize { + /// A small button. + small, + + /// A medium button. + medium, + + /// A large button. + large, +} + +/// {@template app_button} +/// A styled button that composes Material's [FilledButton] and +/// [OutlinedButton] with app-specific sizing and theming. +/// {@endtemplate} +class AppButton extends StatelessWidget { + /// {@macro app_button} + const AppButton({ + required this.onPressed, + required this.child, + this.variant = AppButtonVariant.primary, + this.size = AppButtonSize.medium, + super.key, + }); + + /// Called when the button is tapped. + final VoidCallback? onPressed; + + /// The button's content, typically a [Text] widget. + final Widget child; + + /// The visual variant of the button. + final AppButtonVariant variant; + + /// The size of the button. + final AppButtonSize size; + + @override + Widget build(BuildContext context) { + final spacing = context.appSpacing; + + final padding = switch (size) { + AppButtonSize.small => EdgeInsets.symmetric( + horizontal: spacing.sm, + vertical: spacing.xxs, + ), + AppButtonSize.medium => EdgeInsets.symmetric( + horizontal: spacing.md, + vertical: spacing.xs, + ), + AppButtonSize.large => EdgeInsets.symmetric( + horizontal: spacing.lg, + vertical: spacing.sm, + ), + }; + + final style = ButtonStyle(padding: WidgetStatePropertyAll(padding)); + + return switch (variant) { + AppButtonVariant.primary => FilledButton( + onPressed: onPressed, + style: style, + child: child, + ), + AppButtonVariant.secondary => FilledButton.tonal( + onPressed: onPressed, + style: style, + child: child, + ), + AppButtonVariant.outline => OutlinedButton( + onPressed: onPressed, + style: style, + child: child, + ), + }; + } +} diff --git a/very_good_app_ui/__brick__/lib/{{project_name.snakeCase()}}.dart b/very_good_app_ui/__brick__/lib/{{project_name.snakeCase()}}.dart new file mode 100644 index 00000000..b4f98370 --- /dev/null +++ b/very_good_app_ui/__brick__/lib/{{project_name.snakeCase()}}.dart @@ -0,0 +1,10 @@ +/// {{{description}}} +library; + +export 'package:flutter/material.dart'; + +export 'src/extensions/build_context_extensions.dart'; +export 'src/theme/app_colors.dart'; +export 'src/theme/app_spacing.dart'; +export 'src/theme/app_theme.dart'; +export 'src/widgets/app_button.dart'; diff --git a/very_good_app_ui/__brick__/pubspec.yaml b/very_good_app_ui/__brick__/pubspec.yaml new file mode 100644 index 00000000..bbd21604 --- /dev/null +++ b/very_good_app_ui/__brick__/pubspec.yaml @@ -0,0 +1,18 @@ +name: {{project_name.snakeCase()}} +description: {{{description}}} +version: 0.1.0+1 +{{^publishable}}publish_to: none{{/publishable}} + +environment: + sdk: ^3.11.0 + flutter: ^3.41.0 + +dependencies: + flutter: + sdk: flutter + +dev_dependencies: + flutter_test: + sdk: flutter + mocktail: ^1.0.4 + very_good_analysis: ^10.2.0 diff --git a/very_good_app_ui/__brick__/test/helpers/helpers.dart b/very_good_app_ui/__brick__/test/helpers/helpers.dart new file mode 100644 index 00000000..b15fe650 --- /dev/null +++ b/very_good_app_ui/__brick__/test/helpers/helpers.dart @@ -0,0 +1 @@ +export 'pump_app.dart'; diff --git a/very_good_app_ui/__brick__/test/helpers/pump_app.dart b/very_good_app_ui/__brick__/test/helpers/pump_app.dart new file mode 100644 index 00000000..f49d0092 --- /dev/null +++ b/very_good_app_ui/__brick__/test/helpers/pump_app.dart @@ -0,0 +1,16 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:{{project_name.snakeCase()}}/{{project_name.snakeCase()}}.dart'; + +/// Extension on [WidgetTester] to pump a widget wrapped in [MaterialApp] +/// with the full app theme. +extension PumpApp on WidgetTester { + /// Pumps [widget] wrapped in a [MaterialApp] with [AppTheme.light]. + Future pumpApp(Widget widget, {ThemeData? theme}) { + return pumpWidget( + MaterialApp( + theme: theme ?? AppTheme.light, + home: Scaffold(body: widget), + ), + ); + } +} diff --git a/very_good_app_ui/__brick__/test/src/extensions/build_context_extensions_test.dart b/very_good_app_ui/__brick__/test/src/extensions/build_context_extensions_test.dart new file mode 100644 index 00000000..c0454255 --- /dev/null +++ b/very_good_app_ui/__brick__/test/src/extensions/build_context_extensions_test.dart @@ -0,0 +1,36 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:{{project_name.snakeCase()}}/{{project_name.snakeCase()}}.dart'; + +import '../../helpers/helpers.dart'; + +void main() { + group('AppThemeBuildContext', () { + testWidgets('appColors returns AppColors from theme', (tester) async { + late AppColors colors; + await tester.pumpApp( + Builder( + builder: (context) { + colors = context.appColors; + return const SizedBox(); + }, + ), + ); + + expect(colors, isA()); + }); + + testWidgets('appSpacing returns AppSpacing from theme', (tester) async { + late AppSpacing spacing; + await tester.pumpApp( + Builder( + builder: (context) { + spacing = context.appSpacing; + return const SizedBox(); + }, + ), + ); + + expect(spacing, isA()); + }); + }); +} diff --git a/very_good_app_ui/__brick__/test/src/theme/app_colors_test.dart b/very_good_app_ui/__brick__/test/src/theme/app_colors_test.dart new file mode 100644 index 00000000..6d96bf4e --- /dev/null +++ b/very_good_app_ui/__brick__/test/src/theme/app_colors_test.dart @@ -0,0 +1,59 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:{{project_name.snakeCase()}}/{{project_name.snakeCase()}}.dart'; + +void main() { + group('AppColors', () { + const colors = AppColors( + success: Color(0xFF16A34A), + onSuccess: Color(0xFFFFFFFF), + warning: Color(0xFFCA8A04), + onWarning: Color(0xFFFFFFFF), + info: Color(0xFF2563EB), + onInfo: Color(0xFFFFFFFF), + ); + + test('copyWith returns a new instance with updated values', () { + final updated = colors.copyWith(success: const Color(0xFF000000)); + expect(updated.success, const Color(0xFF000000)); + expect(updated.onSuccess, colors.onSuccess); + expect(updated.warning, colors.warning); + expect(updated.onWarning, colors.onWarning); + expect(updated.info, colors.info); + expect(updated.onInfo, colors.onInfo); + }); + + test('copyWith returns identical instance when no values are provided', () { + final copy = colors.copyWith(); + expect(copy.success, colors.success); + expect(copy.onSuccess, colors.onSuccess); + expect(copy.warning, colors.warning); + expect(copy.onWarning, colors.onWarning); + expect(copy.info, colors.info); + expect(copy.onInfo, colors.onInfo); + }); + + test('lerp returns this when other is not AppColors', () { + final result = colors.lerp(null, 0.5); + expect(result, colors); + }); + + test('lerp interpolates between two AppColors', () { + const other = AppColors( + success: Color(0xFF000000), + onSuccess: Color(0xFF000000), + warning: Color(0xFF000000), + onWarning: Color(0xFF000000), + info: Color(0xFF000000), + onInfo: Color(0xFF000000), + ); + + final result = colors.lerp(other, 0.5); + expect(result.success, isNotNull); + expect(result.onSuccess, isNotNull); + expect(result.warning, isNotNull); + expect(result.onWarning, isNotNull); + expect(result.info, isNotNull); + expect(result.onInfo, isNotNull); + }); + }); +} diff --git a/very_good_app_ui/__brick__/test/src/theme/app_spacing_test.dart b/very_good_app_ui/__brick__/test/src/theme/app_spacing_test.dart new file mode 100644 index 00000000..fd99ee10 --- /dev/null +++ b/very_good_app_ui/__brick__/test/src/theme/app_spacing_test.dart @@ -0,0 +1,66 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:{{project_name.snakeCase()}}/{{project_name.snakeCase()}}.dart'; + +void main() { + group('AppSpacing', () { + const spacing = AppSpacing(); + + test('has correct default values', () { + expect(spacing.xxs, 4); + expect(spacing.xs, 8); + expect(spacing.sm, 12); + expect(spacing.md, 16); + expect(spacing.lg, 24); + expect(spacing.xlg, 32); + expect(spacing.xxlg, 48); + }); + + test('copyWith returns a new instance with updated values', () { + final updated = spacing.copyWith(xxs: 2, md: 20); + expect(updated.xxs, 2); + expect(updated.xs, spacing.xs); + expect(updated.sm, spacing.sm); + expect(updated.md, 20); + expect(updated.lg, spacing.lg); + expect(updated.xlg, spacing.xlg); + expect(updated.xxlg, spacing.xxlg); + }); + + test('copyWith returns identical instance when no values are provided', () { + final copy = spacing.copyWith(); + expect(copy.xxs, spacing.xxs); + expect(copy.xs, spacing.xs); + expect(copy.sm, spacing.sm); + expect(copy.md, spacing.md); + expect(copy.lg, spacing.lg); + expect(copy.xlg, spacing.xlg); + expect(copy.xxlg, spacing.xxlg); + }); + + test('lerp returns this when other is not AppSpacing', () { + final result = spacing.lerp(null, 0.5); + expect(result, spacing); + }); + + test('lerp interpolates between two AppSpacing instances', () { + const other = AppSpacing( + xxs: 8, + xs: 16, + sm: 24, + md: 32, + lg: 48, + xlg: 64, + xxlg: 96, + ); + + final result = spacing.lerp(other, 0.5); + expect(result.xxs, 6); + expect(result.xs, 12); + expect(result.sm, 18); + expect(result.md, 24); + expect(result.lg, 36); + expect(result.xlg, 48); + expect(result.xxlg, 72); + }); + }); +} diff --git a/very_good_app_ui/__brick__/test/src/theme/app_theme_test.dart b/very_good_app_ui/__brick__/test/src/theme/app_theme_test.dart new file mode 100644 index 00000000..7fd3b84d --- /dev/null +++ b/very_good_app_ui/__brick__/test/src/theme/app_theme_test.dart @@ -0,0 +1,42 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:{{project_name.snakeCase()}}/{{project_name.snakeCase()}}.dart'; + +void main() { + group('AppTheme', () { + group('light', () { + test('returns a ThemeData', () { + expect(AppTheme.light, isA()); + }); + + test('has AppColors extension', () { + expect(AppTheme.light.extension(), isNotNull); + }); + + test('has AppSpacing extension', () { + expect(AppTheme.light.extension(), isNotNull); + }); + + test('has light brightness', () { + expect(AppTheme.light.brightness, Brightness.light); + }); + }); + + group('dark', () { + test('returns a ThemeData', () { + expect(AppTheme.dark, isA()); + }); + + test('has AppColors extension', () { + expect(AppTheme.dark.extension(), isNotNull); + }); + + test('has AppSpacing extension', () { + expect(AppTheme.dark.extension(), isNotNull); + }); + + test('has dark brightness', () { + expect(AppTheme.dark.brightness, Brightness.dark); + }); + }); + }); +} diff --git a/very_good_app_ui/__brick__/test/src/widgets/app_button_test.dart b/very_good_app_ui/__brick__/test/src/widgets/app_button_test.dart new file mode 100644 index 00000000..0c345937 --- /dev/null +++ b/very_good_app_ui/__brick__/test/src/widgets/app_button_test.dart @@ -0,0 +1,89 @@ +// Not required for test files. +// ignore_for_file: prefer_const_constructors + +import 'package:flutter_test/flutter_test.dart'; +import 'package:{{project_name.snakeCase()}}/{{project_name.snakeCase()}}.dart'; + +import '../../helpers/helpers.dart'; + +void main() { + group('AppButton', () { + testWidgets('renders child', (tester) async { + await tester.pumpApp(AppButton(onPressed: () {}, child: Text('Tap me'))); + + expect(find.text('Tap me'), findsOneWidget); + }); + + testWidgets('calls onPressed when tapped', (tester) async { + var tapped = false; + await tester.pumpApp( + AppButton(onPressed: () => tapped = true, child: Text('Tap me')), + ); + + await tester.tap(find.byType(AppButton)); + expect(tapped, isTrue); + }); + + testWidgets('does not call onPressed when disabled', (tester) async { + await tester.pumpApp(AppButton(onPressed: null, child: Text('Disabled'))); + + await tester.tap(find.byType(AppButton)); + }); + + testWidgets('renders FilledButton for primary variant', (tester) async { + await tester.pumpApp(AppButton(onPressed: () {}, child: Text('Primary'))); + + expect(find.byType(FilledButton), findsOneWidget); + }); + + testWidgets('renders FilledButton.tonal for secondary variant', ( + tester, + ) async { + await tester.pumpApp( + AppButton( + onPressed: () {}, + variant: AppButtonVariant.secondary, + child: Text('Secondary'), + ), + ); + + expect(find.byType(FilledButton), findsOneWidget); + }); + + testWidgets('renders OutlinedButton for outline variant', (tester) async { + await tester.pumpApp( + AppButton( + onPressed: () {}, + variant: AppButtonVariant.outline, + child: Text('Outline'), + ), + ); + + expect(find.byType(OutlinedButton), findsOneWidget); + }); + + testWidgets('renders with small size', (tester) async { + await tester.pumpApp( + AppButton( + onPressed: () {}, + size: AppButtonSize.small, + child: Text('Small'), + ), + ); + + expect(find.text('Small'), findsOneWidget); + }); + + testWidgets('renders with large size', (tester) async { + await tester.pumpApp( + AppButton( + onPressed: () {}, + size: AppButtonSize.large, + child: Text('Large'), + ), + ); + + expect(find.text('Large'), findsOneWidget); + }); + }); +} diff --git a/very_good_app_ui/__brick__/widgetbook/.gitignore b/very_good_app_ui/__brick__/widgetbook/.gitignore new file mode 100644 index 00000000..3c5430ae --- /dev/null +++ b/very_good_app_ui/__brick__/widgetbook/.gitignore @@ -0,0 +1,34 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# VSCode related +.vscode/* + +# Flutter/Dart/Pub related +**/doc/api/ +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ +pubspec.lock + +# Test related +coverage diff --git a/very_good_app_ui/__brick__/widgetbook/analysis_options.yaml b/very_good_app_ui/__brick__/widgetbook/analysis_options.yaml new file mode 100644 index 00000000..cfe6f914 --- /dev/null +++ b/very_good_app_ui/__brick__/widgetbook/analysis_options.yaml @@ -0,0 +1,5 @@ +include: package:very_good_analysis/analysis_options.yaml + +analyzer: + exclude: + - "**/*.g.dart" diff --git a/very_good_app_ui/__brick__/widgetbook/lib/main.dart b/very_good_app_ui/__brick__/widgetbook/lib/main.dart new file mode 100644 index 00000000..ccfce2fd --- /dev/null +++ b/very_good_app_ui/__brick__/widgetbook/lib/main.dart @@ -0,0 +1,7 @@ +import 'package:flutter/material.dart'; +import 'package:widgetbook_catalog/widgetbook/widgetbook.dart'; + +void main() { + WidgetsFlutterBinding.ensureInitialized(); + runApp(const WidgetbookApp()); +} diff --git a/very_good_app_ui/__brick__/widgetbook/lib/widgetbook/use_cases/app_button.dart b/very_good_app_ui/__brick__/widgetbook/lib/widgetbook/use_cases/app_button.dart new file mode 100644 index 00000000..2879b2e9 --- /dev/null +++ b/very_good_app_ui/__brick__/widgetbook/lib/widgetbook/use_cases/app_button.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; +import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook; +import 'package:{{project_name.snakeCase()}}/{{project_name.snakeCase()}}.dart'; + +/// Primary [AppButton] use case. +@widgetbook.UseCase(name: 'primary', type: AppButton) +Widget primary(BuildContext context) => Center( + child: AppButton(onPressed: () {}, child: const Text('Primary')), +); + +/// Secondary [AppButton] use case. +@widgetbook.UseCase(name: 'secondary', type: AppButton) +Widget secondary(BuildContext context) => Center( + child: AppButton( + onPressed: () {}, + variant: AppButtonVariant.secondary, + child: const Text('Secondary'), + ), +); + +/// Outline [AppButton] use case. +@widgetbook.UseCase(name: 'outline', type: AppButton) +Widget outline(BuildContext context) => Center( + child: AppButton( + onPressed: () {}, + variant: AppButtonVariant.outline, + child: const Text('Outline'), + ), +); + +/// Disabled [AppButton] use case. +@widgetbook.UseCase(name: 'disabled', type: AppButton) +Widget disabled(BuildContext context) => + const Center(child: AppButton(onPressed: null, child: Text('Disabled'))); + +/// All sizes [AppButton] use case. +@widgetbook.UseCase(name: 'all sizes', type: AppButton) +Widget allSizes(BuildContext context) => Center( + child: Column( + mainAxisSize: MainAxisSize.min, + spacing: 8, + children: [ + for (final size in AppButtonSize.values) + AppButton(onPressed: () {}, size: size, child: Text(size.name)), + ], + ), +); diff --git a/very_good_app_ui/__brick__/widgetbook/lib/widgetbook/widgetbook.dart b/very_good_app_ui/__brick__/widgetbook/lib/widgetbook/widgetbook.dart new file mode 100644 index 00000000..85df4693 --- /dev/null +++ b/very_good_app_ui/__brick__/widgetbook/lib/widgetbook/widgetbook.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; +import 'package:widgetbook/widgetbook.dart'; +import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook; +import 'package:widgetbook_catalog/widgetbook/widgetbook.directories.g.dart'; +import 'package:widgetbook_catalog/widgetbook/widgets/widgets.dart'; +import 'package:{{project_name.snakeCase()}}/{{project_name.snakeCase()}}.dart'; + +/// The Widgetbook catalog app. +@widgetbook.App() +class WidgetbookApp extends StatelessWidget { + /// Creates a [WidgetbookApp]. + const WidgetbookApp({super.key}); + + @override + Widget build(BuildContext context) { + return Widgetbook.material( + directories: directories, + addons: [ + BuilderAddon( + name: 'Decorator', + builder: (context, child) { + return UseCaseDecorator(child: child); + }, + ), + ThemeAddon( + themes: [ + WidgetbookTheme(name: 'Light', data: AppTheme.light), + WidgetbookTheme(name: 'Dark', data: AppTheme.dark), + ], + themeBuilder: (context, theme, child) { + return Theme( + data: theme, + child: DefaultTextStyle( + style: theme.textTheme.bodyMedium ?? const TextStyle(), + child: child, + ), + ); + }, + ), + ], + ); + } +} diff --git a/very_good_app_ui/__brick__/widgetbook/lib/widgetbook/widgetbook.directories.g.dart b/very_good_app_ui/__brick__/widgetbook/lib/widgetbook/widgetbook.directories.g.dart new file mode 100644 index 00000000..e6d098e8 --- /dev/null +++ b/very_good_app_ui/__brick__/widgetbook/lib/widgetbook/widgetbook.directories.g.dart @@ -0,0 +1,53 @@ +// dart format width=80 +// coverage:ignore-file +// ignore_for_file: type=lint +// ignore_for_file: unused_import, prefer_relative_imports, directives_ordering + +// GENERATED CODE - DO NOT MODIFY BY HAND + +// ************************************************************************** +// AppGenerator +// ************************************************************************** + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'package:widgetbook/widgetbook.dart' as _widgetbook; +import 'package:widgetbook_catalog/widgetbook/use_cases/app_button.dart' + as _widgetbook_catalog_widgetbook_use_cases_app_button; + +final directories = <_widgetbook.WidgetbookNode>[ + _widgetbook.WidgetbookFolder( + name: 'widgets', + children: [ + _widgetbook.WidgetbookComponent( + name: 'AppButton', + useCases: [ + _widgetbook.WidgetbookUseCase( + name: 'all sizes', + builder: + _widgetbook_catalog_widgetbook_use_cases_app_button.allSizes, + ), + _widgetbook.WidgetbookUseCase( + name: 'disabled', + builder: + _widgetbook_catalog_widgetbook_use_cases_app_button.disabled, + ), + _widgetbook.WidgetbookUseCase( + name: 'outline', + builder: + _widgetbook_catalog_widgetbook_use_cases_app_button.outline, + ), + _widgetbook.WidgetbookUseCase( + name: 'primary', + builder: + _widgetbook_catalog_widgetbook_use_cases_app_button.primary, + ), + _widgetbook.WidgetbookUseCase( + name: 'secondary', + builder: + _widgetbook_catalog_widgetbook_use_cases_app_button.secondary, + ), + ], + ), + ], + ), +]; diff --git a/very_good_app_ui/__brick__/widgetbook/lib/widgetbook/widgets/use_case_decorator.dart b/very_good_app_ui/__brick__/widgetbook/lib/widgetbook/widgets/use_case_decorator.dart new file mode 100644 index 00000000..f854aca1 --- /dev/null +++ b/very_good_app_ui/__brick__/widgetbook/lib/widgetbook/widgets/use_case_decorator.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; + +/// A decorator that wraps every use case with a consistent background. +class UseCaseDecorator extends StatelessWidget { + /// Creates a [UseCaseDecorator]. + const UseCaseDecorator({required this.child, super.key}); + + /// The use case widget to wrap. + final Widget child; + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + return ColoredBox( + color: colorScheme.surfaceContainerHighest, + child: SizedBox.expand(child: Material(child: child)), + ); + } +} diff --git a/very_good_app_ui/__brick__/widgetbook/lib/widgetbook/widgets/widgets.dart b/very_good_app_ui/__brick__/widgetbook/lib/widgetbook/widgets/widgets.dart new file mode 100644 index 00000000..a4827d8c --- /dev/null +++ b/very_good_app_ui/__brick__/widgetbook/lib/widgetbook/widgets/widgets.dart @@ -0,0 +1 @@ +export 'use_case_decorator.dart'; diff --git a/very_good_app_ui/__brick__/widgetbook/pubspec.yaml b/very_good_app_ui/__brick__/widgetbook/pubspec.yaml new file mode 100644 index 00000000..11c9b066 --- /dev/null +++ b/very_good_app_ui/__brick__/widgetbook/pubspec.yaml @@ -0,0 +1,26 @@ +name: widgetbook_catalog +description: "Widgetbook catalog for {{project_name.titleCase()}}" +publish_to: none +version: 1.0.0+1 + +environment: + sdk: ^3.11.0 + flutter: ^3.41.0 + +dependencies: + flutter: + sdk: flutter + {{project_name.snakeCase()}}: + path: .. + widgetbook: ^3.10.0 + widgetbook_annotation: ^3.2.0 + +dev_dependencies: + build_runner: ^2.4.14 + flutter_test: + sdk: flutter + very_good_analysis: ^10.2.0 + widgetbook_generator: ^3.10.0 + +flutter: + uses-material-design: true diff --git a/very_good_app_ui/analysis_options.yaml b/very_good_app_ui/analysis_options.yaml new file mode 100644 index 00000000..41ffce1e --- /dev/null +++ b/very_good_app_ui/analysis_options.yaml @@ -0,0 +1,3 @@ +analyzer: + exclude: + - __brick__/** diff --git a/very_good_app_ui/brick.yaml b/very_good_app_ui/brick.yaml new file mode 100644 index 00000000..0dd26890 --- /dev/null +++ b/very_good_app_ui/brick.yaml @@ -0,0 +1,23 @@ +name: very_good_app_ui +description: A Very Good Flutter app UI package created by Very Good Ventures. +repository: >- + https://github.com/VeryGoodOpenSource/very_good_templates/tree/main/very_good_app_ui +version: 0.1.0 +environment: + mason: ^0.1.0 +vars: + project_name: + type: string + description: The package name + default: my_app_ui + prompt: What is the project name? + description: + type: string + description: The package description + default: A Very Good App UI package + prompt: What is the project description? + publishable: + type: boolean + description: Whether the generated package is intended to be published. + default: false + prompt: Will the package be published? diff --git a/very_good_app_ui/config.json b/very_good_app_ui/config.json new file mode 100644 index 00000000..6d6b1f63 --- /dev/null +++ b/very_good_app_ui/config.json @@ -0,0 +1,5 @@ +{ + "project_name": "very_good_app_ui_output", + "description": "A generated Very Good App UI package.", + "publishable": false +} diff --git a/very_good_app_ui/tool/release_ready.sh b/very_good_app_ui/tool/release_ready.sh new file mode 100755 index 00000000..4d6174e2 --- /dev/null +++ b/very_good_app_ui/tool/release_ready.sh @@ -0,0 +1,78 @@ +#!/bin/bash + +# Ensures that the brick is ready for a release. +# +# Will update the brick.yaml file and update the CHANGELOG.md. +# +# Set it up for a new version: +# `./release_ready.sh + +# Check if current directory is usable for this script, if so we assume it is correctly set up. +if [ ! -f "brick.yaml" ]; then + echo "$(pwd) is not a valid brick." + exit 1 +fi + +currentBranch=$(git symbolic-ref --short -q HEAD) +if [[ ! $currentBranch == "main" ]]; then + echo "Releasing is only supported on the main branch." + exit 1 +fi + +# Get information +old_version="" +if [ -f "brick.yaml" ]; then + old_version=$(cat brick.yaml | pcregrep 'version: (.*?)' | tr " " "\n" | tail -1) +fi + +if [ -z "$old_version" ]; then + echo "Current version was not resolved." + exit 1 +fi + +# Get new version +new_version="$1"; + +if [[ "$new_version" == "" ]]; then + echo "No new version supplied, please provide one" + exit 1 +fi + +if [[ "$new_version" == "$old_version" ]]; then + echo "Current version is $old_version, can't update." + exit 1 +fi + +# Retrieving all the commits in the current directory since the last tag. +previousTag="very_good_app_ui-v${old_version}" +raw_commits="$(git log --pretty=format:"%s" --no-merges --reverse $previousTag..HEAD -- .)" +markdown_commits=$(echo "$raw_commits" | sed -En "s/\(#([0-9]+)\)/([#\1](https:\/\/github.com\/VeryGoodOpenSource\/very_good_templates\/pull\/\1))/p") + +if [[ "$markdown_commits" == "" ]]; then + echo "No commits since last tag, can't update." + exit 0 +fi +commits=$(echo "$markdown_commits" | sed -En "s/^/- /p") + +echo "Updating version to $new_version" +if [ -f "brick.yaml" ]; then + sed -i '' "s/version: $old_version/version: $new_version/g" brick.yaml +fi + +if grep -q v$new_version "CHANGELOG.md"; then + echo "CHANGELOG already contains version $new_version." + exit 1 +fi + +# Add a new version entry with the found commits to the CHANGELOG.md. +echo "# ${new_version} \n\n${commits}\n\n$(cat CHANGELOG.md)" > CHANGELOG.md +echo "CHANGELOG generated, validate entries here: $(pwd)/CHANGELOG.md" + +echo "Creating git branch for very_good_app_ui@$new_version" +git checkout -b "chore/very_good_app_ui-v$new_version" > /dev/null + +git add brick.yaml CHANGELOG.md + +echo "" +echo "Run the following command if you wish to commit the changes:" +echo "git commit -m \"chore(very_good_app_ui): v$new_version\"" diff --git a/very_good_app_ui/version.txt b/very_good_app_ui/version.txt new file mode 100644 index 00000000..6e8bf73a --- /dev/null +++ b/very_good_app_ui/version.txt @@ -0,0 +1 @@ +0.1.0