diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 00000000..917aa9d7 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,57 @@ +{ + "permissions": { + "allow": [ + "Bash(lsof -ti:3000)", + "Bash(node capture.mjs)", + "Bash(node compare.mjs)", + "Bash(open /Users/john/projects/very_good_workflows/tools/visual-compare/screenshots/report.html)", + "Bash(node /private/tmp/check-h1.mjs)", + "Bash(node check-h1.mjs)", + "Bash(lsof -ti:4000)", + "Bash(npx serve build/jaspr -l 4000)", + "Bash(curl -s -o /dev/null -w \"%{http_code}\" http://localhost:4000/docs/overview/)", + "Bash(node capture.mjs --jaspr-only)", + "Bash(node inspect-dark.mjs)", + "Bash(node inspect-spr.mjs)", + "Bash(node inspect-width.mjs)", + "Bash(dart run jaspr_cli:jaspr build)", + "Bash(pkill -f build_daemon)", + "Bash(pkill -f dart.*build_runner)", + "Bash(curl -s http://localhost:4000/docs/overview/)", + "mcp__plugin_wingspan_dart__dart_fix", + "Bash(node capture.js --url=\"http://localhost:4000/docs/workflows\" --name=\"docs-workflows\" --width=1200)", + "Bash(node capture.mjs --url=\"http://localhost:3000/docs/workflows\" --name=\"docs-workflows\" --width=1200)", + "Bash(node capture.mjs --url=\"http://localhost:4000/docs/workflows\" --name=\"docs-workflows\" --width=1200)", + "mcp__plugin_wingspan_dart__hover", + "Bash(jaspr serve --port 4000)", + "Bash(curl -s -o /dev/null -w \"%{http_code}\" http://localhost:4000/docs/workflows)", + "Bash(node /tmp/measure-layout.mjs)", + "Bash(npm run capture:jaspr)", + "Bash(npm run compare)", + "Bash(node analyze-diffs.mjs)", + "Bash(ls /Users/john/projects/very_good_workflows/tools/visual-compare/screenshots/diff-*.png)", + "Bash(curl -s -o /dev/null -w \"%{http_code}\" http://localhost:4000/)", + "Bash(node measure.mjs)", + "Bash(node measure-home.mjs)", + "Bash(node measure-buttons.mjs)", + "Read(//Users/john/.pub-cache/hosted/pub.dev/jaspr_content-0.5.0/lib/components/**)", + "Bash(curl -s http://localhost:4000/)", + "Read(//Users/john/.pub-cache/hosted/pub.dev/jaspr_content-0.5.0/**)", + "Read(//Users/john/.pub-cache/hosted/pub.dev/jaspr-0.22.3/lib/src/dom/**)", + "Read(//Users/john/.pub-cache/hosted/pub.dev/jaspr-0.22.3/lib/**)", + "Bash(grep -n 'final class li\\\\|final class ul' /Users/john/.pub-cache/hosted/pub.dev/jaspr-0.22.3/lib/src/dom/html/*.dart)", + "Bash(while read hash msg)", + "Bash(do echo \"=== $msg ===\")", + "Bash(git diff-tree --no-commit-id --name-only -r \"$hash\")", + "Bash(done)", + "Bash(python3 -c \"import sys,json; h=json.load\\(sys.stdin\\); [print\\(f\"\"{e[''timestamp''][:16]} {e[''parityScore'']}%\"\"\\) for e in h]\")", + "Bash(node -e \":*)", + "Bash(xargs kill -9)", + "Bash(lsof -ti:5467)", + "Bash(pkill -9 -f 'dart.*build_daemon')", + "Bash(python3 -m json.tool)", + "Bash(ls /Users/john/projects/very_good_workflows/tools/visual-compare/screenshots/parity*)", + "Bash(ls /Users/john/projects/very_good_workflows/tools/visual-compare/parity*)" + ] + } +} diff --git a/.github/cspell.json b/.github/cspell.json index b011c3d7..82b89885 100644 --- a/.github/cspell.json +++ b/.github/cspell.json @@ -15,28 +15,46 @@ } ], "useGitignore": true, + "ignorePaths": [ + "**/*.tmLanguage.json" + ], "words": [ "amannn", "APPDATA", "brickhub", + "Consolas", "creatordate", "creds", "clsx", "Csvg", "Contador", "endtemplate", + "evenodd", + "flexbox", + "frontmatter", "Infima", + "jaspr", "linecap", + "linejoin", "localizable", + "Menlo", "mostrado", "noopener", + "noreferrer", + "Noto", + "Octicon", "pΓ‘gina", "peaceiris", + "Scrollspy", + "Segoe", "srealmoreno", "retag", "Texto", "typecheck", + "unhighlighted", + "viewports", "webfactory", - "xlink" + "xlink", + "zoomable" ] } diff --git a/JASPR_MIGRATION.md b/JASPR_MIGRATION.md new file mode 100644 index 00000000..fefdae64 --- /dev/null +++ b/JASPR_MIGRATION.md @@ -0,0 +1,70 @@ +# Migrating from Docusaurus to Jaspr + +## 1. Scaffold the Jaspr project + +```bash +dart pub global activate jaspr_cli +jaspr create --template=docs site_jaspr +``` + +This gives you a Jaspr static site pre-configured with `jaspr_content`, which includes a `DocsLayout` with sidebar, header, dark/light mode, markdown parsing, and syntax highlighting out of the box. + +## 2. Migrate your content + +Your current docs are 10 Markdown files in `site/docs/` with YAML frontmatter (`sidebar_position`, `title`, `description`). These can largely be copied over as-is -- `jaspr_content` uses a `FilesystemLoader` that reads `.md` files from a content directory and supports frontmatter. + +The current sidebar is auto-generated from the filesystem in Docusaurus. In Jaspr, the sidebar is defined explicitly in Dart code: + +```dart +sidebar: Sidebar(groups: [ + SidebarGroup(links: [ + SidebarLink(text: 'Overview', href: '/'), + ]), + SidebarGroup(title: 'Workflows', links: [ + SidebarLink(text: 'Dart Package', href: '/workflows/dart_package'), + SidebarLink(text: 'Flutter Package', href: '/workflows/flutter_package'), + // ... etc + ]), +]), +``` + +## 3. Recreate the custom homepage + +The current homepage (`site/src/pages/index.tsx`) is a custom React component with a hero banner, CTA button, and blog section. In Jaspr, you'd build this as a `StatelessComponent` using Jaspr's HTML element functions (`div`, `a`, `img`, etc.) with typed `Styles` for CSS. + +## 4. Migrate theming + +The custom CSS (`site/src/css/custom.css`) defines brand colors, Poppins font, and dark mode overrides. In Jaspr, theming is done via `ContentTheme`: + +```dart +theme: ContentTheme( + primary: ThemeColor(Color('#2a48df'), dark: Color('#66fbd1')), + background: ThemeColor(Colors.white, dark: Color('#0b0d0e')), +), +``` + +## 5. Update the deployment workflow + +Replace the Node/Docusaurus build steps in `.github/workflows/site_deploy.yaml` with Dart/Jaspr: + +```yaml +- uses: dart-lang/setup-dart@v1 +- run: dart pub global activate jaspr_cli +- run: jaspr build +# Output goes to build/jaspr/ instead of site/build/ +``` + +## Key differences to be aware of + +| Aspect | Docusaurus (current) | Jaspr | +|---|---|---| +| Sidebar | Auto-generated from filesystem | Manual definition in Dart | +| Custom pages | React/TSX components | Dart `StatelessComponent` | +| Styling | CSS/CSS Modules | Typed `Styles` in Dart + optional CSS | +| Markdown extensions | MDX (JSX in Markdown) | Custom components registered in Dart | +| Search | Algolia integration available | No built-in search | +| Docs versioning | Built-in | Not available | + +## Tradeoffs + +The main benefit is staying entirely within the Dart ecosystem. The main costs are losing Docusaurus's auto-generated sidebar, built-in search integration, and the broader plugin ecosystem. The site is relatively straightforward (10 doc pages, one custom homepage), so the migration scope is manageable. diff --git a/site_jaspr/.gitignore b/site_jaspr/.gitignore new file mode 100644 index 00000000..b7ea95fd --- /dev/null +++ b/site_jaspr/.gitignore @@ -0,0 +1,30 @@ +# Files and directories created by pub. +**/doc/api/ +.dart_tool/ +.packages + +# Conventional directory for build output. +/build/ + +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ diff --git a/site_jaspr/README.md b/site_jaspr/README.md new file mode 100644 index 00000000..eef2407a --- /dev/null +++ b/site_jaspr/README.md @@ -0,0 +1,15 @@ +# site_jaspr + +A documentation site built with Jaspr + +## Running the project + +Run your project using `jaspr serve`. + +The development server will be available on `http://localhost:8080`. + +## Building the project + +Build your project using `jaspr build`. + +The output will be located inside the `build/jaspr/` directory. diff --git a/site_jaspr/analysis_options.yaml b/site_jaspr/analysis_options.yaml new file mode 100644 index 00000000..1198b97b --- /dev/null +++ b/site_jaspr/analysis_options.yaml @@ -0,0 +1,46 @@ +# This file configures the static analysis results for your project (errors, +# warnings, and lints). +# +# This enables the 'recommended' set of lints from `package:lints`. +# This set helps identify many issues that may lead to problems when running +# or consuming Dart code, and enforces writing Dart using a single, idiomatic +# style and format. +# +# If you want a smaller set of lints you can change this to specify +# 'package:lints/core.yaml'. These are just the most critical lints +# (the recommended set includes the core lints). +# The core lints are also what is used by pub.dev for scoring packages. + +include: package:lints/recommended.yaml + +analyzer: +# exclude: +# - path/to/excluded/files/** + +# Jaspr has a custom analyzer plugin 'jaspr_lints', which is enabled here. +# +# You can toggle Jaspr specific lint rules in the 'diagnostics' section below. +plugins: + jaspr_lints: + version: ^0.6.0 + diagnostics: + prefer_html_components: true + sort_children_last: true + styles_ordering: true + +# Uncomment the following section to enable or disable additional rules. + +# linter: +# rules: +# camel_case_types: true + +# For more information about the core and recommended set of lints, see +# https://dart.dev/go/core-lints + +formatter: + # Change this to your preferred line length. + page_width: 120 + trailing_commas: preserve + +# For additional information about configuring this file, see +# https://dart.dev/guides/language/analysis-options diff --git a/site_jaspr/content/_data/links.yaml b/site_jaspr/content/_data/links.yaml new file mode 100644 index 00000000..221c43df --- /dev/null +++ b/site_jaspr/content/_data/links.yaml @@ -0,0 +1,3 @@ +website: https://verygood.ventures +devTools: https://developer.vgv.dev +github: https://github.com/VeryGoodOpenSource/very_good_workflows diff --git a/site_jaspr/content/_data/site.yaml b/site_jaspr/content/_data/site.yaml new file mode 100644 index 00000000..78ad681a --- /dev/null +++ b/site_jaspr/content/_data/site.yaml @@ -0,0 +1,3 @@ +# Site Configuration +titleBase: Very Good Workflows +favicon: favicon.ico diff --git a/site_jaspr/content/docs/category/workflows.md b/site_jaspr/content/docs/category/workflows.md new file mode 100644 index 00000000..4b839938 --- /dev/null +++ b/site_jaspr/content/docs/category/workflows.md @@ -0,0 +1,8 @@ +--- +title: "Workflows" +--- + + + + +If you are not redirected automatically, [click here](/docs/workflows). diff --git a/site_jaspr/content/docs/overview.md b/site_jaspr/content/docs/overview.md new file mode 100644 index 00000000..ec9c7a29 --- /dev/null +++ b/site_jaspr/content/docs/overview.md @@ -0,0 +1,45 @@ +--- +title: Overview +description: A collection of reusable GitHub workflows used at Very Good Ventures. +image: /images/meta/open-graph.png +--- + +# Overview + +[GitHub workflows][github_workflows_link] are configurable, automated processes that can run at various points during the development process. For example, a workflow can run when a pull request is created or updated to perform various code quality checks before allowing the changes to be merged. + +Very Good Workflows is a collection of workflows that we use at VGV to run automated checks in our CI pipelines. While built by VGV to be used internally, they can be used by anyone. + +## Quick Start πŸš€ + +To get started, add Very Good Workflows to an existing GitHub workflow: + +```yaml +# A reusable workflow for Dart packages +uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/dart_package.yml@v1 + +# A reusable workflow for Flutter packages +uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 + +# A reusable workflow for ensuring commits are semantic +uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/semantic_pull_request.yml@v1 + +# A reusable workflow for verifying package scores on pub.dev +uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/pana.yml@v1 + +# A reusable workflow for running a spell check +uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/spell_check.yml@v1 + +# A reusable workflow for publishing Flutter packages +uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_pub_publish.yml@v1 + +# A reusable workflow for publishing Dart packages +uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/dart_pub_publish.yml@v1 + +# A reusable workflow for publishing Mason bricks +uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/mason_publish.yml@v1 + +``` + +[github_workflows_link]: https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions +[very_good_ventures_link]: https://verygood.ventures diff --git a/site_jaspr/content/docs/workflows.md b/site_jaspr/content/docs/workflows.md new file mode 100644 index 00000000..ed7703fa --- /dev/null +++ b/site_jaspr/content/docs/workflows.md @@ -0,0 +1,47 @@ +--- +title: "Workflows" +description: "Learn about all of the workflows that Very Good Workflows supports." +--- + +# Workflows πŸ¦„ + +Learn about all of the workflows that Very Good Workflows supports. + +
+ +

πŸ“„ Dart Package

+

This workflow runs helpful checks on a Dart package according to the steps below. As with any workflow, it can be customized.

+
+ +

πŸ“„ Dart Pub Publish

+

We use this workflow to publish a Dart package to pub.dev.

+
+ +

πŸ“„ Flutter Package

+

This workflow runs helpful checks on a Flutter package according to the steps below. As with any workflow, it can be customized.

+
+ +

πŸ“„ Flutter Pub Publish

+

We use this workflow to publish a Flutter package to pub.dev.

+
+ +

πŸ“„ License Check

+

At VGV, we keep track of the rights and restrictions external dependencies might impose on Dart or Flutter projects.

+
+ +

πŸ“„ Mason Publish

+

We use this workflow to publish a brick to brickhub.dev.

+
+ +

πŸ“„ Pana

+

We use the Dart tool pana to validate that your pub score will be the max possible score before publishing a package to pub.dev.

+
+ +

πŸ“„ Semantic Pull Request

+

At VGV, we use conventional commits. This way, our commit history is clean and useful when we squash and merge β€” we can easily see the type and brief description of each PR.

+
+ +

πŸ“„ Spell Check

+

We use cspell for basic spell check on our projects.

+
+
diff --git a/site_jaspr/content/docs/workflows/dart_package.md b/site_jaspr/content/docs/workflows/dart_package.md new file mode 100644 index 00000000..d4145d3c --- /dev/null +++ b/site_jaspr/content/docs/workflows/dart_package.md @@ -0,0 +1,135 @@ +--- +title: Dart Package +description: A reusable workflow for running checks on Dart packages. +image: /images/meta/open-graph.png +--- + +# Dart Package + +This workflow runs helpful checks on a Dart package according to the steps below. As with any workflow, it can be customized. + +## Steps + +The Dart package workflow consists of the following steps: + +1. Install dependencies +2. Format +3. Analyze +4. Run tests +5. Check code coverage + +## Inputs + +### `concurrency` + +**Optional** The number of concurrent test suites run. + +**Default** `4` + +### `coverage_excludes` + +**Optional** List of paths to exclude from the coverage report, separated by an empty space. Supports `globs` to describe file patterns. + +**Default** `""` + +### `dart_sdk` + +**Optional** Which Dart SDK version to use. It can be a version (e.g. `3.5.0`) or a channel (e.g. `stable`): + +**Default** `"stable"` + +### `format_line_length` + +**Optional** The preferred line length preferred for running the `dart format` command. Be aware that this does not change the behavior of the analysis step and longer lines could still make the workflow fail if the rule `lines_longer_than_80_chars` is used. + +### `min_coverage` + +**Optional** The minimum coverage percentage allowed. + +**Default** 100 + +### `working_directory` + +**Optional** The path to the root of the Dart package. + +**Default** `"."` + +### `analyze_directories` + +**Optional** A space-separated list of folders that should be analyzed. + +**Default** `"lib test"` + +### `format_directories` + +**Optional** A space-separated list of folders that should be formatted. + +**Default** `"."` + +### `check_ignore` + +**Optional** Allows ignoring lines from [coverage](https://pub.dev/packages/coverage). + +**Default** `false` + +### `report_on` + +**Optional** A comma-separated list of folders that should be checked in code coverage. + +**Default** `"lib"` + +### `runs_on` + +**Optional** The operating system on which to run the workflow. + +**Default** `"ubuntu-latest"` + +### `setup` + +**Optional** A command that should be executed immediately after dependencies are installed. + +**Default** `""` + +### `platform` + +**Optional** A comma-separated list of platforms on which to run the tests. +`[vm (default), chrome, firefox, safari, node]` + +**Default** `"vm"` + +### `run_skipped` + +**Optional** Run skipped tests instead of skipping them. + +**Default** `false` + +### `no_example` + +**Optional** To avoid getting packages in `example/` when running `dart pub get` (if it exists). + +**Default** `false` + +## Secrets + +### `ssh_key` + +**Optional** An SSH key used to access private repositories when installing dependencies. + +## Example Usage + +```yaml +name: My Dart Workflow + +on: pull_request + +jobs: + build: + uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/dart_package.yml@v1 + with: + coverage_excludes: '*.g.dart' + dart_sdk: 'stable' + platform: 'chrome,vm' + working_directory: 'examples/my_dart_package' + secrets: + ssh_key: ${{secrets.EXAMPLE_KEY}} +``` diff --git a/site_jaspr/content/docs/workflows/dart_pub_publish.md b/site_jaspr/content/docs/workflows/dart_pub_publish.md new file mode 100644 index 00000000..8f6b21d0 --- /dev/null +++ b/site_jaspr/content/docs/workflows/dart_pub_publish.md @@ -0,0 +1,55 @@ +--- +title: Dart Pub Publish +description: A reusable workflow for publishing Dart packages to pub.dev. +image: /images/meta/open-graph.png +--- + +# Dart Pub Publish + +We use this workflow to publish a Dart package to [pub.dev](https://pub.dev). + +## Steps + +The Dart Pub Publish workflow consists of the following steps: + +1. Setup Dart, including pub.dev publish token +2. Install dependencies +3. Dry run +4. Publish + +This workflow uses the automated publishing of packages to pub.dev which is part of the [Dart documentation](https://dart.dev/tools/pub/automated-publishing). Before using this workflow ensure that you have configured your package on pub.dev correctly to allow the publish process to complete. + +## Inputs + +### `dart_sdk` + +**Optional** Which Dart SDK version to use. It can be a version (e.g. `3.5.0`) or a channel (e.g. `stable`): + +**Default** `"stable"` + +### `working_directory` + +**Optional** The path to the root of the Dart package. + +**Default** `"."` + +### `runs_on` + +**Optional** An optional operating system on which to run the workflow. + +**Default** `"ubuntu-latest"` + +## Example Usage + +```yaml +name: My Dart Pub Publish Workflow + +on: pull_request + +jobs: + build: + uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/dart_pub_publish.yml@v1 + with: + dart_sdk: 'stable' + working_directory: 'packages/my_dart_package' +``` diff --git a/site_jaspr/content/docs/workflows/flutter_package.md b/site_jaspr/content/docs/workflows/flutter_package.md new file mode 100644 index 00000000..394a0a0b --- /dev/null +++ b/site_jaspr/content/docs/workflows/flutter_package.md @@ -0,0 +1,131 @@ +--- +title: Flutter Package +description: A reusable workflow for running checks on Flutter packages. +image: /images/meta/open-graph.png +--- + +# Flutter Package + +This workflow runs helpful checks on a Flutter package according to the steps below. As with any workflow, it can be customized. + +## Steps + +The Flutter package workflow consists of the following steps: + +1. Install dependencies +2. Format +3. Analyze +4. Run tests +5. Check code coverage + +## Inputs + +### `analyze_directories` + +**Optional** A space-separated list of folders that should be analyzed. + +**Default** `"lib test"` + +### `format_directories` + +**Optional** A space-separated list of folders that should be formatted. + +**Default** `"lib test"` + +### `concurrency` + +**Optional** The number of concurrent test suites run. + +**Default** `4` + +### `coverage_excludes` + +**Optional** A space-separated list of paths to exclude from the coverage report. Supports `globs` to describe file patterns. + +**Default** `""` + +### `flutter_channel` + +**Optional** The Flutter release channel to use (e.g. `stable`). + +**Default** `"stable"` + +### `flutter_version` + +**Optional** The Flutter SDK version to use (e.g. `3.24.0`). + +**Default** `""` + +### `format_line_length` + +**Optional** The preferred line length preferred for running the `dart format` command. Be aware that this does not change the behavior of the analysis step and longer lines could still make the workflow fail if the rule `lines_longer_than_80_chars` is used. + +### `min_coverage` + +**Optional** The minimum coverage percentage allowed. + +**Default** 100 + +### `setup` + +**Optional** A command that should be executed immediately after dependencies are installed. + +**Default** `""` + +### `working_directory` + +**Optional** The path to the root of the Flutter package. + +**Default** `"."` + +### `test_optimization` + +**Optional** Enable the test optimization. + +**Default** `true` + +**Note**: Since the optimization process groups tests into a single file, golden tests will not work properly. Consider disabling optimizations if you are using golden tests. + +### `test_recursion` + +**Optional** Whether to recursively run tests in nested directories. + +**Default** `false` + +### `runs_on` + +**Optional** The operating system on which to run the workflow. + +**Default** `"ubuntu-latest"` + +### `package_get_excludes` + +**Optional** List of paths to exclude from `packages get`. Supports `globs` to describe file patterns. + +**Default** `"!*"` + +## Secrets + +### `ssh_key` + +**Optional** An SSH key used to access private repositories when installing dependencies. + +## Example Usage + +```yaml +name: My Flutter Workflow + +on: pull_request + +jobs: + build: + uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 + with: + coverage_excludes: '**/*.g.dart' + flutter_channel: 'stable' + flutter_version: '3.35.0' + working_directory: 'examples/my_flutter_package' + test_recursion: true + secrets: + ssh_key: ${{secrets.EXAMPLE_KEY}} +``` diff --git a/site_jaspr/content/docs/workflows/flutter_pub_publish.md b/site_jaspr/content/docs/workflows/flutter_pub_publish.md new file mode 100644 index 00000000..aaadaef4 --- /dev/null +++ b/site_jaspr/content/docs/workflows/flutter_pub_publish.md @@ -0,0 +1,67 @@ +--- +title: Flutter Pub Publish +description: A reusable workflow for publishing Flutter packages to pub.dev. +image: /images/meta/open-graph.png +--- + +# Flutter Pub Publish + +We use this workflow to publish a Flutter package to [pub.dev](https://pub.dev). + +## Steps + +The Flutter Pub Publish workflow consists of the following steps: + +1. Install dependencies +2. Setup Flutter & Dart, including pub.dev publish token +3. Dry run +4. Publish + +This workflow uses the automated publishing of packages to pub.dev which is part of the [Dart documentation](https://dart.dev/tools/pub/automated-publishing). Before using this workflow ensure that you have configured your package on pub.dev correctly to allow the publish process to complete. + +## Inputs + +### `flutter_channel` + +**Optional** The Flutter release channel to use (e.g. `stable`). + +**Default** `"stable"` + +### `flutter_version` + +**Optional** The Flutter SDK version to use (e.g. `3.24.0`). + +**Default** `""` + +### `working_directory` + +**Optional** The path to the root of the Flutter package. + +**Default** `"."` + +### `runs_on` + +**Optional** An optional operating system on which to run the workflow. + +**Default** `"ubuntu-latest"` + +## Example Usage + +We recommend using [GitHub Secrets](https://docs.github.com/en/actions/security-guides/encrypted-secrets) for safely storing and reading the credentials. + +```yaml +name: My Flutter Pub Publish Workflow + +on: + push: + tags: + - 'my_flutter_package-v*.*.*' + +jobs: + build: + uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_pub_publish.yml@v1 + with: + flutter_channel: 'stable' + flutter_version: '3.35.0' + working_directory: 'packages/my_flutter_package' +``` diff --git a/site_jaspr/content/docs/workflows/license_check.md b/site_jaspr/content/docs/workflows/license_check.md new file mode 100644 index 00000000..7706eb26 --- /dev/null +++ b/site_jaspr/content/docs/workflows/license_check.md @@ -0,0 +1,118 @@ +--- +title: License Check +description: A reusable workflow for checking dependency licenses. +image: /images/meta/open-graph.png +--- + +# License Check + +At VGV, we keep track of the rights and restrictions external dependencies might impose on Dart or Flutter projects. + + + The License Check functionality is powered by Very Good CLI's license checker, for a deeper understanding of some inputs refer to its documentation. + + +## Steps + +The License Check workflow consists of the following steps: + +1. Setup Dart +2. Set SSH Key (if provided) +3. Install project dependencies +4. Check licenses + +## Inputs + +### `working_directory` + +**Optional** The path to the root of the Dart or Flutter package. + +**Default** `"."` + +### `runs_on` + +**Optional** An optional operating system on which to run the workflow. + +**Default** `"ubuntu-latest"` + +### `dart_sdk` + +**Optional** Which Dart SDK version to use. It can be a version (e.g. `3.5.0`) or a channel (e.g. `stable`): + +**Default** `"stable"` + +### `allowed` + +**Optional** Only allow the use of certain licenses. The expected format is a comma-separated list. + +**Default** `"MIT,BSD-3-Clause,BSD-2-Clause,Apache-2.0"` + +### `forbidden` + +**Optional** Deny the use of certain licenses. The expected format is a comma-separated list. + +**Default** `""` + + +The allowed and forbidden options can't be used at the same time. If you want to use `forbidden` set `allowed` to an empty string. + + +### `skip_packages` + +**Optional** Skip packages from having their licenses checked. + +**Default** `""` + +### `dependency_type` + +**Optional** The type of dependencies to check licenses for. + +**Default** `"direct-main,transitive"` + +### `ignore_retrieval_failures` + +**Optional** Disregard licenses that failed to be retrieved. + +**Default** `false` + +## Secrets + +### `ssh_key` + +**Optional** An SSH key to use for setting up the credentials for fetching dependencies that are not publicly available. + +## Example Usage + +```yaml +name: license_check + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +on: + pull_request: + 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' +``` + +The example [workflow file](https://docs.github.com/en/actions/quickstart#creating-your-first-workflow) will [trigger](https://docs.github.com/en/actions/using-workflows/triggering-a-workflow) the `license_check` job on every push to the `main` branch and on every pull request that modifies the `pubspec.yaml` or the `license_check.yaml` workflow file. + +If you are [committing the `pubspec.lock`](https://dart.dev/guides/libraries/private-files#pubspec-lock) file for an application package you may consider adding it to the list of paths to trigger the workflow. + + +For repositories with multiple packages we recommend adding a workflow file per package to avoid triggering a license check for packages which dependencies haven't been modified. + diff --git a/site_jaspr/content/docs/workflows/mason_publish.md b/site_jaspr/content/docs/workflows/mason_publish.md new file mode 100644 index 00000000..507cc0c1 --- /dev/null +++ b/site_jaspr/content/docs/workflows/mason_publish.md @@ -0,0 +1,69 @@ +--- +title: Mason Publish +description: A reusable workflow for publishing Mason bricks to brickhub.dev. +image: /images/meta/open-graph.png +--- + +# Mason Publish + +We use this workflow to publish a brick to [brickhub.dev](https://brickhub.dev/). + +## Steps + +The Mason Publish workflow consists of the following steps: + +1. Install Mason +2. Setup Mason credentials +3. Dry run +4. Publish + +## Inputs + +### `mason_version` + +**Optional** Which Mason version to use (e.g. `0.1.0-dev.50`). + +**Default** `""` + +### `working_directory` + +**Optional** The path to the root of the Mason brick. + +**Default** `"."` + +### `runs_on` + +**Optional** An optional operating system on which to run the workflow. + +**Default** `"ubuntu-latest"` + +## Secrets + +### `mason_credentials` + +**Required** The mason credentials needed for publishing. This can be retrieved by reading out your `mason-credentials.json` on your system after you ran a `mason login`, the location of the file is different per operating system: + +| OS | Path | +| ------- | ----------------------------------------------------------------------------------------------- | +| Linux | `$XDG_CONFIG_HOME/mason/mason-credentials.json` or `$HOME/.config/mason/mason-credentials.json` | +| macOS | `~/Library/Application\ Support/mason/mason-credentials.json` | +| Windows | `%APPDATA%/mason/mason-credentials.json` | + +## Example Usage + +We recommend using [GitHub Secrets](https://docs.github.com/en/actions/security-guides/encrypted-secrets) for safely storing and reading the credentials. + +```yaml +name: My Mason Brick Publish Workflow + +on: pull_request + +jobs: + build: + uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/mason_publish.yml@v1 + with: + mason_version: '0.1.0-dev.50' + working_directory: 'packages/my_mason_brick' + secrets: + mason_credentials: ${{ secrets.MASON_CREDENTIALS }} +``` diff --git a/site_jaspr/content/docs/workflows/pana.md b/site_jaspr/content/docs/workflows/pana.md new file mode 100644 index 00000000..cb3a5214 --- /dev/null +++ b/site_jaspr/content/docs/workflows/pana.md @@ -0,0 +1,55 @@ +--- +title: Pana +description: A reusable workflow for verifying pub.dev package scores. +image: /images/meta/open-graph.png +--- + +# Pana + +We use the Dart tool [pana](https://pub.dev/packages/pana) to validate that your pub score will be the max possible score before publishing a package to [pub.dev](https://pub.dev). + +## Steps + +The pana workflow consists of the following steps: + +1. Install pana +2. Verify pana score + +## Inputs + +### `pana_version` + +**Optional** Which version of `package:pana` to use (See the available versions [here](https://pub.dev/packages/pana/changelog)). + +### `min_score` + +**Optional** The minimum score allowed. + +**Default** `120` + +### `working_directory` + +**Optional** The path to the root of the Dart package. + +**Default** `"."` + +### `runs_on` + +**Optional** The operating system on which to run the workflow. + +**Default** `"ubuntu-latest"` + +## Example Usage + +```yaml +name: My Workflow + +on: pull_request + +jobs: + build: + uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/pana.yml@v1 + with: + min_score: 95 + working_directory: 'examples/my_flutter_package' +``` diff --git a/site_jaspr/content/docs/workflows/semantic_pull_request.md b/site_jaspr/content/docs/workflows/semantic_pull_request.md new file mode 100644 index 00000000..10dc4a28 --- /dev/null +++ b/site_jaspr/content/docs/workflows/semantic_pull_request.md @@ -0,0 +1,64 @@ +--- +title: Semantic Pull Request +description: A reusable workflow for enforcing semantic commit conventions. +image: /images/meta/open-graph.png +--- + +# Semantic Pull Request + +At VGV, we use [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/). This way, our commit history is clean and useful when we squash and merge β€” we can easily see the type and brief description of each PR. Note that this workflow can be customized to reflect the pull request types you want to enforce (e.g. feat, fix, docs, chore). + +## Steps + +The semantic pull request package workflow consists of the following step: + +1. Ensure commit is semantic + +## Inputs + +### `types` + +**Optional** Configure which types are allowed (e.g. `"feat, fix, docs"`). + +**Note**: If not set, then the action uses the list of [Commitizen conventional commit types][commitizen]. + +### `scopes` + +**Optional** Configure which scopes are allowed (e.g. `"dart_package, flutter_package"`). + +## GitHub Repository Configuration + +### Squash Merging + +To verify that this workflow will properly evaluate your pull request messages, we recommend configuring your GitHub repository to [allow squash merging](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/configuring-pull-request-merges/configuring-commit-squashing-for-pull-requests) and setting the [default commit message](https://github.blog/changelog/2022-08-23-new-options-for-controlling-the-default-commit-message-when-merging-a-pull-request/) to "Pull request title." + +### GitHub Permissions + +When running this workflow, the `GITHUB_TOKEN` has to have the correct permissions to run successfully. On public repositories, the default settings grant the token enough permissions to run correctly. However, on private repositories, settings have to be updated. There are two ways of doing so: + +- **Repository wide update.** Inside your repository, go to _Settings > Actions > General_, scroll down to the _Workflow permissions_ section and update it to allow _Read and write permissions_. Don't forget to save the changes. +- **Workflow specific update.** In your workflow `yaml` file, you can modify the permissions for the `GITHUB_TOKEN`. For this workflow to work you have to enable write permissions for pull requests in your job as follows. + + ```yaml + jobs: + build: + permissions: + pull-requests: write + uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/semantic_pull_request.yml@v1 + ``` + + You can read more about this in the [github documentation](https://docs.github.com/en/actions/security-guides/automatic-token-authentication#modifying-the-permissions-for-the-github_token). + +## Example Usage + +```yaml +name: My Workflow + +on: pull_request + +jobs: + build: + uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/semantic_pull_request.yml@v1 +``` + +[commitizen]: https://github.com/commitizen/conventional-commit-types diff --git a/site_jaspr/content/docs/workflows/spell_check.md b/site_jaspr/content/docs/workflows/spell_check.md new file mode 100644 index 00000000..9ee9ab77 --- /dev/null +++ b/site_jaspr/content/docs/workflows/spell_check.md @@ -0,0 +1,111 @@ +--- +title: Spell Check +description: A reusable workflow for running spell checks on projects. +image: /images/meta/open-graph.png +--- + +# Spell Check + +We use [cspell](https://github.com/streetsidesoftware/cspell) for basic spell check on our projects. + +## Steps + +The spell check workflow consists of the following steps: + +1. Git checkout +2. Run spell check + +## Inputs + +### `config` + +**Optional** The location of the `cspell.json`. + +**Default** `".github/cspell.json"` + +### `includes` + +**Optional** The glob patterns to filter the files to be checked. Use a new line between patterns to define multiple patterns. + +**Default** `""` + +### `working_directory` + +**Optional** The path to the root of the Dart package. + +**Default** `"."` + +### `runs_on` + +**Optional** An optional operating system on which to run the workflow. + +**Default** `"ubuntu-latest"` + +### `verbose` + +**Optional** An optional boolean which determines whether to log verbose output. + +**Default** `false` + +### `modified_files_only` + +**Optional** An optional boolean which determines whether spell check is run on modified files. + +**Default** `true` + +## Example Usage + +```yaml +name: My Workflow + +on: pull_request + +jobs: + build: + uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/spell_check.yml@v1 + with: + includes: | + **/*.{dart,md,yaml} + !.dart_tool/**/*.{dart,yaml} + .*/**/*.yml + runs_on: macos-latest + modified_files_only: false + working_directory: examples/my_project +``` + +## CSpell File Example + +More information can be found in [cspell docs](https://cspell.org/configuration/). + +Our custom dictionaries are available [here](https://github.com/verygoodopensource/very_good_dictionaries/) for everyone to use. + +```json +{ + "$schema": "https://raw.githubusercontent.com/streetsidesoftware/cspell/main/cspell.schema.json", + "version": "0.2", + // List of the names of the dictionaries to use. + "dictionaries": ["vgv_allowed", "vgv_forbidden"], + // List of custom dictionary definitions. + "dictionaryDefinitions": [ + // Remote dictionary example. URLs will be retrieved via HTTP GET. + { + "name": "vgv_allowed", + "path": "https://raw.githubusercontent.com/verygoodopensource/very_good_dictionaries/main/allowed.txt", + "description": "Allowed VGV Spellings" + }, + // Local dictionary example. Relative paths are relative to the config file. + { + "name": "vgv_forbidden", + "path": "./vgv_forbidden.txt", + "addWords": true + } + ], + // Ignores files found in .gitignore. + "useGitignore": true, + // List of allowed words that are not part of dictionaries. + "words": ["Contador", "localizable", "mostrado", "pΓ‘gina", "Texto"], + // List of not-allowed words. + // For example "hte" should be "the". + "flagWords": ["hte"] +} +``` diff --git a/site_jaspr/content/index.md b/site_jaspr/content/index.md new file mode 100644 index 00000000..c1326527 --- /dev/null +++ b/site_jaspr/content/index.md @@ -0,0 +1,6 @@ +--- +title: Very Good Workflows +description: A collection of helpful, reusable GitHub workflows used by VGV. +image: /images/meta/open-graph.png +layout: homepage +--- diff --git a/site_jaspr/grammars/bash.tmLanguage.json b/site_jaspr/grammars/bash.tmLanguage.json new file mode 100644 index 00000000..a875b61c --- /dev/null +++ b/site_jaspr/grammars/bash.tmLanguage.json @@ -0,0 +1,2353 @@ +{ + "information_for_contributors": [ + "This file has been converted from https://github.com/jeff-hykin/better-shell-syntax/blob/master/autogenerated/shell.tmLanguage.json", + "If you want to provide a fix or improvement, please create a pull request against the original repository.", + "Once accepted there, we are happy to receive an update request." + ], + "version": "https://github.com/jeff-hykin/better-shell-syntax/commit/35020b0bd79a90d3b262b4c13a8bb0b33adc1f45", + "name": "Shell Script", + "scopeName": "source.shell", + "patterns": [ + { + "include": "#initial_context" + } + ], + "repository": { + "alias_statement": { + "begin": "(?:(?:[ \\t]*)(alias)(?:[ \\t]*)((?:(?:((?&;<>\\(\\)\\$`\\\\\"'<\\|]+)(?!>))", + "captures": { + "1": { + "name": "string.unquoted.argument.shell", + "patterns": [ + { + "match": "\\*", + "name": "variable.language.special.wildcard.shell" + }, + { + "include": "#variable" + }, + { + "include": "#numeric_literal" + }, + { + "match": "(?|#|\\n|$|;|[ \\t]))(?!nocorrect |nocorrect\t|nocorrect$|readonly |readonly\t|readonly$|function |function\t|function$|foreach |foreach\t|foreach$|coproc |coproc\t|coproc$|logout |logout\t|logout$|export |export\t|export$|select |select\t|select$|repeat |repeat\t|repeat$|pushd |pushd\t|pushd$|until |until\t|until$|while |while\t|while$|local |local\t|local$|case |case\t|case$|done |done\t|done$|elif |elif\t|elif$|else |else\t|else$|esac |esac\t|esac$|popd |popd\t|popd$|then |then\t|then$|time |time\t|time$|for |for\t|for$|end |end\t|end$|fi |fi\t|fi$|do |do\t|do$|in |in\t|in$|if |if\t|if$))(?:((?<=^|;|&|[ \\t])(?:readonly|declare|typeset|export|local)(?=[ \\t]|;|&|$))|((?!\"|'|\\\\\\n?$)(?:[^!'\"<> \\t\\n\\r]+?)))(?:(?= |\\t)|(?:(?=;|\\||&|\\n|\\)|\\`|\\{|\\}|[ \\t]*#|\\])(?]+))", + "captures": { + "1": { + "name": "entity.name.function.call.shell entity.name.command.shell" + } + } + }, + { + "begin": "(?:(?:\\G|(?|#|\\n|$|;|[ \\t]))(?!nocorrect |nocorrect\t|nocorrect$|readonly |readonly\t|readonly$|function |function\t|function$|foreach |foreach\t|foreach$|coproc |coproc\t|coproc$|logout |logout\t|logout$|export |export\t|export$|select |select\t|select$|repeat |repeat\t|repeat$|pushd |pushd\t|pushd$|until |until\t|until$|while |while\t|while$|local |local\t|local$|case |case\t|case$|done |done\t|done$|elif |elif\t|elif$|else |else\t|else$|esac |esac\t|esac$|popd |popd\t|popd$|then |then\t|then$|time |time\t|time$|for |for\t|for$|end |end\t|end$|fi |fi\t|fi$|do |do\t|do$|in |in\t|in$|if |if\t|if$)(?!\\\\\\n?$)))", + "end": "(?=;|\\||&|\\n|\\)|\\`|\\{|\\}|[ \\t]*#|\\])(?|&&|\\|\\|", + "name": "keyword.operator.logical.shell" + }, + { + "match": "(?[>=]?|==|!=|^|\\|{1,2}|&{1,2}|\\?|\\:|,|=|[*/%+\\-&^|]=|<<=|>>=", + "name": "keyword.operator.arithmetic.shell" + }, + { + "match": "0[xX][0-9A-Fa-f]+", + "name": "constant.numeric.hex.shell" + }, + { + "match": ";", + "name": "punctuation.separator.semicolon.range" + }, + { + "match": "0\\d+", + "name": "constant.numeric.octal.shell" + }, + { + "match": "\\d{1,2}#[0-9a-zA-Z@_]+", + "name": "constant.numeric.other.shell" + }, + { + "match": "\\d+", + "name": "constant.numeric.integer.shell" + }, + { + "match": "(?[>=]?|==|!=|^|\\|{1,2}|&{1,2}|\\?|\\:|,|=|[*/%+\\-&^|]=|<<=|>>=", + "name": "keyword.operator.arithmetic.shell" + }, + { + "match": "0[xX][0-9A-Fa-f]+", + "name": "constant.numeric.hex.shell" + }, + { + "match": "0\\d+", + "name": "constant.numeric.octal.shell" + }, + { + "match": "\\d{1,2}#[0-9a-zA-Z@_]+", + "name": "constant.numeric.other.shell" + }, + { + "match": "\\d+", + "name": "constant.numeric.integer.shell" + } + ] + }, + "misc_ranges": { + "patterns": [ + { + "include": "#logical_expression_single" + }, + { + "include": "#logical_expression_double" + }, + { + "include": "#subshell_dollar" + }, + { + "begin": "(?|#|\\n|$|;|[ \\t]))))", + "end": "(?:(?=[ \\t])|(?:(?=;|\\||&|\\n|\\)|\\`|\\{|\\}|[ \\t]*#|\\])(?>?)(?:[ \\t]*)([^ \t\n>&;<>\\(\\)\\$`\\\\\"'<\\|]+))", + "captures": { + "1": { + "name": "keyword.operator.redirect.shell" + }, + "2": { + "name": "string.unquoted.argument.shell" + } + } + }, + "redirect_number": { + "match": "(?<=[ \\t])(?:(?:(1)|(2)|(\\d+))(?=>))", + "captures": { + "1": { + "name": "keyword.operator.redirect.stdout.shell" + }, + "2": { + "name": "keyword.operator.redirect.stderr.shell" + }, + "3": { + "name": "keyword.operator.redirect.$3.shell" + } + } + }, + "redirection": { + "patterns": [ + { + "begin": "[><]\\(", + "beginCaptures": { + "0": { + "name": "punctuation.definition.string.begin.shell" + } + }, + "end": "\\)", + "endCaptures": { + "0": { + "name": "punctuation.definition.string.end.shell" + } + }, + "name": "string.interpolated.process-substitution.shell", + "patterns": [ + { + "include": "#initial_context" + } + ] + }, + { + "match": "(?])(&>|\\d*>&\\d*|\\d*(>>|>|<)|\\d*<&|\\d*<>)(?![<>])", + "name": "keyword.operator.redirect.shell" + } + ] + }, + "regex_comparison": { + "match": "\\=~", + "name": "keyword.operator.logical.regex.shell" + }, + "regexp": { + "patterns": [ + { + "match": "(?:.+)" + } + ] + }, + "simple_options": { + "match": "(?:(?:[ \\t]+)\\-(?:\\w+))*", + "captures": { + "0": { + "patterns": [ + { + "match": "(?:[ \\t]+)(\\-)(\\w+)", + "captures": { + "1": { + "name": "string.unquoted.argument.shell constant.other.option.dash.shell" + }, + "2": { + "name": "string.unquoted.argument.shell constant.other.option.shell" + } + } + } + ] + } + } + }, + "simple_unquoted": { + "match": "[^ \\t\\n>&;<>\\(\\)\\$`\\\\\"'<\\|]", + "name": "string.unquoted.shell" + }, + "special_expansion": { + "match": "!|:[-=?]?|\\*|@|##|#|%%|%|\\/", + "name": "keyword.operator.expansion.shell" + }, + "start_of_command": { + "match": "(?:(?:[ \\t]*)(?:(?!(?:!|&|\\||\\(|\\)|\\{|\\[|<|>|#|\\n|$|;|[ \\t]))(?!nocorrect |nocorrect\t|nocorrect$|readonly |readonly\t|readonly$|function |function\t|function$|foreach |foreach\t|foreach$|coproc |coproc\t|coproc$|logout |logout\t|logout$|export |export\t|export$|select |select\t|select$|repeat |repeat\t|repeat$|pushd |pushd\t|pushd$|until |until\t|until$|while |while\t|while$|local |local\t|local$|case |case\t|case$|done |done\t|done$|elif |elif\t|elif$|else |else\t|else$|esac |esac\t|esac$|popd |popd\t|popd$|then |then\t|then$|time |time\t|time$|for |for\t|for$|end |end\t|end$|fi |fi\t|fi$|do |do\t|do$|in |in\t|in$|if |if\t|if$)(?!\\\\\\n?$)))" + }, + "string": { + "patterns": [ + { + "match": "\\\\.", + "name": "constant.character.escape.shell" + }, + { + "begin": "'", + "end": "'", + "beginCaptures": { + "0": { + "name": "punctuation.definition.string.begin.shell" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.string.end.shell" + } + }, + "name": "string.quoted.single.shell" + }, + { + "begin": "\\$?\"", + "end": "\"", + "beginCaptures": { + "0": { + "name": "punctuation.definition.string.begin.shell" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.string.end.shell" + } + }, + "name": "string.quoted.double.shell", + "patterns": [ + { + "match": "\\\\[\\$\\n`\"\\\\]", + "name": "constant.character.escape.shell" + }, + { + "include": "#variable" + }, + { + "include": "#interpolation" + } + ] + }, + { + "begin": "\\$'", + "end": "'", + "beginCaptures": { + "0": { + "name": "punctuation.definition.string.begin.shell" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.string.end.shell" + } + }, + "name": "string.quoted.single.dollar.shell", + "patterns": [ + { + "match": "\\\\(?:a|b|e|f|n|r|t|v|\\\\|')", + "name": "constant.character.escape.ansi-c.shell" + }, + { + "match": "\\\\[0-9]{3}\"", + "name": "constant.character.escape.octal.shell" + }, + { + "match": "\\\\x[0-9a-fA-F]{2}\"", + "name": "constant.character.escape.hex.shell" + }, + { + "match": "\\\\c.\"", + "name": "constant.character.escape.control-char.shell" + } + ] + } + ] + }, + "subshell_dollar": { + "patterns": [ + { + "begin": "(?:\\$\\()", + "end": "\\)", + "beginCaptures": { + "0": { + "name": "punctuation.definition.subshell.single.shell" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.subshell.single.shell" + } + }, + "name": "meta.scope.subshell", + "patterns": [ + { + "include": "#parenthese" + }, + { + "include": "#initial_context" + } + ] + } + ] + }, + "support": { + "patterns": [ + { + "match": "(?<=^|;|&|\\s)(?::|\\.)(?=\\s|;|&|$)", + "name": "support.function.builtin.shell" + } + ] + }, + "typical_statements": { + "patterns": [ + { + "include": "#assignment_statement" + }, + { + "include": "#case_statement" + }, + { + "include": "#for_statement" + }, + { + "include": "#while_statement" + }, + { + "include": "#function_definition" + }, + { + "include": "#command_statement" + }, + { + "include": "#line_continuation" + }, + { + "include": "#arithmetic_double" + }, + { + "include": "#normal_context" + } + ] + }, + "variable": { + "patterns": [ + { + "match": "(?:(\\$)(\\@(?!\\w)))", + "captures": { + "1": { + "name": "punctuation.definition.variable.shell variable.parameter.positional.all.shell" + }, + "2": { + "name": "variable.parameter.positional.all.shell" + } + } + }, + { + "match": "(?:(\\$)([0-9](?!\\w)))", + "captures": { + "1": { + "name": "punctuation.definition.variable.shell variable.parameter.positional.shell" + }, + "2": { + "name": "variable.parameter.positional.shell" + } + } + }, + { + "match": "(?:(\\$)([-*#?$!0_](?!\\w)))", + "captures": { + "1": { + "name": "punctuation.definition.variable.shell variable.language.special.shell" + }, + "2": { + "name": "variable.language.special.shell" + } + } + }, + { + "begin": "(?:(\\$)(\\{)(?:[ \\t]*)(?=\\d))", + "end": "\\}", + "beginCaptures": { + "1": { + "name": "punctuation.definition.variable.shell variable.parameter.positional.shell" + }, + "2": { + "name": "punctuation.section.bracket.curly.variable.begin.shell punctuation.definition.variable.shell variable.parameter.positional.shell" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.section.bracket.curly.variable.end.shell punctuation.definition.variable.shell variable.parameter.positional.shell" + } + }, + "contentName": "meta.parameter-expansion", + "patterns": [ + { + "include": "#special_expansion" + }, + { + "include": "#array_access_inline" + }, + { + "match": "[0-9]+", + "name": "variable.parameter.positional.shell" + }, + { + "match": "(?])([1-9])?([+-])?\\s*(?:#.*)?$", + "beginCaptures": { + "1": { "name": "keyword.control.flow.block-scalar.yaml" }, + "2": { "name": "constant.numeric.indentation-indicator.yaml" }, + "3": { "name": "storage.modifier.chomping-indicator.yaml" } + }, + "end": "^(?=\\S)", + "contentName": "string.unquoted.block.yaml" + } + ] + }, + "flow-collection": { + "patterns": [ + { + "begin": "\\{", + "beginCaptures": { "0": { "name": "punctuation.definition.mapping.begin.yaml" } }, + "end": "\\}", + "endCaptures": { "0": { "name": "punctuation.definition.mapping.end.yaml" } }, + "name": "meta.flow-mapping.yaml", + "patterns": [ + { "include": "#flow-scalar" }, + { "include": "#comment" }, + { "match": ",", "name": "punctuation.separator.mapping.yaml" }, + { + "match": "([\\w./-]+)\\s*(:)(?=\\s|[,}])", + "captures": { + "1": { "name": "entity.name.tag.yaml" }, + "2": { "name": "punctuation.separator.key-value.mapping.yaml" } + } + } + ] + }, + { + "begin": "\\[", + "beginCaptures": { "0": { "name": "punctuation.definition.sequence.begin.yaml" } }, + "end": "\\]", + "endCaptures": { "0": { "name": "punctuation.definition.sequence.end.yaml" } }, + "name": "meta.flow-sequence.yaml", + "patterns": [ + { "include": "#flow-scalar" }, + { "include": "#comment" }, + { "match": ",", "name": "punctuation.separator.sequence.yaml" } + ] + } + ] + }, + "flow-scalar": { + "patterns": [ + { "include": "#double-quoted" }, + { "include": "#single-quoted" }, + { "include": "#implicit-type" } + ] + }, + "double-quoted": { + "begin": "\"", + "beginCaptures": { "0": { "name": "punctuation.definition.string.begin.yaml" } }, + "end": "\"", + "endCaptures": { "0": { "name": "punctuation.definition.string.end.yaml" } }, + "name": "string.quoted.double.yaml", + "patterns": [ + { "match": "\\\\[\"\\\\bfnrt/0aevNLP_ xuU]", "name": "constant.character.escape.yaml" } + ] + }, + "single-quoted": { + "begin": "'", + "beginCaptures": { "0": { "name": "punctuation.definition.string.begin.yaml" } }, + "end": "'(?!')", + "endCaptures": { "0": { "name": "punctuation.definition.string.end.yaml" } }, + "name": "string.quoted.single.yaml", + "patterns": [ + { "match": "''", "name": "constant.character.escape.yaml" } + ] + }, + "implicit-type": { + "patterns": [ + { + "match": "(?<=:\\s|^\\s*-\\s|,\\s)\\s*(true|false|True|False|TRUE|FALSE|yes|no|Yes|No|YES|NO|on|off|On|Off|ON|OFF)(?=\\s*$|\\s+#|\\s*[,\\]\\}])", + "captures": { + "1": { "name": "constant.language.boolean.yaml" } + } + }, + { + "match": "(?<=:\\s|^\\s*-\\s|,\\s)\\s*(null|Null|NULL|~)(?=\\s*$|\\s+#|\\s*[,\\]\\}])", + "captures": { + "1": { "name": "constant.language.null.yaml" } + } + }, + { + "match": "(?<=:\\s|^\\s*-\\s|,\\s)\\s*([-+]?(?:0x[0-9a-fA-F]+|0o[0-7]+|0b[01]+|[0-9]+(?:\\.[0-9]*)?(?:[eE][-+]?[0-9]+)?|\\.(?:inf|Inf|INF)|\\.(nan|NaN|NAN)))(?=\\s*$|\\s+#|\\s*[,\\]\\}])", + "captures": { + "1": { "name": "constant.numeric.yaml" } + } + } + ] + } + } +} diff --git a/site_jaspr/lib/components/breadcrumb.dart b/site_jaspr/lib/components/breadcrumb.dart new file mode 100644 index 00000000..22577aab --- /dev/null +++ b/site_jaspr/lib/components/breadcrumb.dart @@ -0,0 +1,89 @@ +import 'package:jaspr/dom.dart'; +import 'package:jaspr/jaspr.dart'; +import 'package:jaspr_content/jaspr_content.dart'; + +/// A breadcrumb navigation component for docs pages. +/// +/// Derives the breadcrumb path from the current page URL. +/// Only renders on docs pages (URLs starting with `/docs/`). +class Breadcrumb extends StatelessComponent { + const Breadcrumb({super.key}); + + @override + Component build(BuildContext context) { + if (kIsWeb) return Component.fragment([]); + + final page = context.page; + final url = page.url; + if (!url.startsWith('/docs/')) return Component.fragment([]); + + final segments = url.split('/').where((seg) => seg.isNotEmpty).toList(); + // e.g. ['docs', 'overview'] or ['docs', 'workflows', 'dart_package'] + + final title = page.data.page['title'] as String? ?? _formatSegment(segments.last); + + final items = []; + + // Home icon + items.add( + a(classes: 'breadcrumb-link', href: '/', [ + svg( + viewBox: '0 0 24 24', + attributes: {'width': '16', 'height': '16', 'fill': 'currentColor'}, + [path(d: 'M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z', [])], + ), + ]), + ); + + // Middle segments (skip 'docs' prefix and last segment) + for (var i = 1; i < segments.length - 1; i++) { + final href = '/${segments.sublist(0, i + 1).join('/')}'; + items.add(span(classes: 'breadcrumb-sep', [Component.text('>')])); + items.add( + a(classes: 'breadcrumb-link', href: href, [Component.text(_formatSegment(segments[i]))]), + ); + } + + // Current page + items.add(span(classes: 'breadcrumb-sep', [Component.text('>')])); + items.add(span(classes: 'breadcrumb-current', [Component.text(title)])); + + return nav(classes: 'breadcrumb', items); + } + + static String _formatSegment(String segment) { + return segment.split('_').map((w) => '${w[0].toUpperCase()}${w.substring(1)}').join(' '); + } + + @css + static List get styles => [ + css('.breadcrumb', [ + css('&').styles( + display: Display.flex, + padding: Padding.only(bottom: 1.rem), + alignItems: AlignItems.center, + gap: Gap(column: 0.5.rem), + fontSize: 0.875.rem, + ), + ]), + css('.breadcrumb-link', [ + css('&').styles( + color: Color('var(--secondary-text)'), + textDecoration: TextDecoration.none, + ), + css('&:hover').styles(color: Color('var(--primary)')), + ]), + css('.breadcrumb-sep').styles( + opacity: 0.5, + color: Color('var(--secondary-text)'), + fontSize: 0.75.rem, + ), + css('.breadcrumb-current').styles( + padding: Padding.symmetric(horizontal: 0.5.rem, vertical: 0.125.rem), + radius: BorderRadius.circular(12.px), + color: Color('var(--primary)'), + fontSize: 0.8.rem, + backgroundColor: Color('color-mix(in srgb, currentColor 8%, transparent)'), + ), + ]; +} diff --git a/site_jaspr/lib/components/collapsible_sidebar.dart b/site_jaspr/lib/components/collapsible_sidebar.dart new file mode 100644 index 00000000..0032b46c --- /dev/null +++ b/site_jaspr/lib/components/collapsible_sidebar.dart @@ -0,0 +1,483 @@ +import 'package:jaspr/dom.dart'; +import 'package:jaspr/jaspr.dart'; +import 'package:jaspr_content/components/sidebar.dart'; +import 'package:jaspr_content/jaspr_content.dart'; +import 'package:jaspr_content/theme.dart'; + +/// A sidebar entry that can optionally have collapsible children. +class SidebarEntry { + const SidebarEntry({ + required this.text, + required this.href, + this.children = const [], + }); + + final String text; + final String href; + final List children; + + bool get hasChildren => children.isNotEmpty; +} + +/// A sidebar replicating Docusaurus's collapsible sidebar exactly. +/// +/// Matches the Docusaurus `menu__list-item-collapsible` structure: +/// - Category items render a navigable [a] link alongside a separate +/// [button] caret that handles expand/collapse only. +/// - Leaf items render a single [a] link. +/// - The active item and expanded category are determined server-side from +/// the current page URL; client-side toggling uses an inline script. +/// +/// On mobile (< 1024 px) a header bar is rendered at the top of the panel +/// containing [mobileNavItems] (all the header nav items β€” links, icons, +/// ThemeToggle, etc.) and the close button. This mirrors Docusaurus's +/// `navbar-sidebar__brand` + `navbar-sidebar__items` structure. +/// +/// When [primaryNavItems] is provided, a two-panel mobile sidebar is rendered: +/// - Secondary panel (default): the doc links + "← Back to main menu" +/// - Primary panel: the [primaryNavItems] shown as a vertical list. +/// Tapping "← Back to main menu" switches to the primary panel. +/// +/// Styling follows Infima's sidebar tokens: +/// - Item padding: 0.375rem 0.75rem, border-radius: 0.25rem +/// - Active: var(--primary) color, w600, 10 % primary tint background +/// - Category headers: always w600 (semibold), whether expanded or not +/// - Hover: rgba(0,0,0,0.05) light / rgba(255,255,255,0.05) dark +class CollapsibleSidebar extends StatelessComponent { + const CollapsibleSidebar({ + required this.items, + this.mobileNavItems = const [], + this.primaryNavItems = const [], + super.key, + }); + + final List items; + + /// Nav items rendered in the mobile-only header bar (hidden on desktop). + /// + /// Pass the same items as the desktop header (NavLinks, IconLinks, + /// ThemeToggle, etc.) so they remain accessible on narrow viewports. + final List mobileNavItems; + + /// Nav items shown in the primary panel when "← Back to main menu" is tapped. + /// + /// Mirrors Docusaurus's two-panel mobile sidebar: the secondary panel shows + /// doc links, and the primary panel shows the global site nav items. + final List primaryNavItems; + + @override + Component build(BuildContext context) { + final currentRoute = context.page.url; + + return Component.fragment([ + Document.head( + children: [ + script(defer: true, content: _toggleScript), + ], + ), + nav(classes: 'sidebar', [ + // Mobile-only header: nav items (flex row) + close button. + // Mirrors Docusaurus's navbar-sidebar__brand / navbar-sidebar__items. + // Hidden on desktop (β‰₯ 1024 px) via CSS. + div(classes: 'sidebar-mobile-header', [ + div(classes: 'sidebar-mobile-nav', mobileNavItems), + // Close button β€” the SidebarToggleButton listens for .sidebar-close + // clicks to collapse the panel. + button( + classes: 'sidebar-close', + attributes: {'type': 'button', 'aria-label': 'Close menu'}, + [ + svg( + viewBox: '0 0 24 24', + attributes: { + 'width': '20', + 'height': '20', + 'fill': 'none', + 'stroke': 'currentColor', + 'stroke-width': '2', + }, + [ + line( + attributes: {'x1': '18', 'y1': '6', 'x2': '6', 'y2': '18'}, + [], + ), + line( + attributes: {'x1': '6', 'y1': '6', 'x2': '18', 'y2': '18'}, + [], + ), + ], + ), + ], + ), + ]), + // Mobile-only primary panel β€” shown when the nav has .show-primary. + // Contains the global nav items (Get Started, VGV Dev Tools, icons). + // Hidden on desktop (β‰₯ 1024 px) via CSS. + if (primaryNavItems.isNotEmpty) + div(classes: 'sidebar-primary', [ + div(classes: 'sidebar-primary-nav', primaryNavItems), + ]), + // Mobile-only "← Back to main menu" β€” mirrors Docusaurus's secondary + // panel back-link; switches to the primary panel when clicked. + // Hidden on desktop (β‰₯ 1024 px) via CSS. + // Hidden in primary mode (.show-primary on parent .sidebar) via CSS. + button( + classes: 'sidebar-back', + attributes: {'type': 'button'}, + [Component.text('← Back to main menu')], + ), + div(classes: 'sidebar-group', [ + ul([ + for (final item in items) _buildItem(item, currentRoute), + ]), + ]), + ]), + ]); + } + + Component _buildItem(SidebarEntry item, String currentRoute) { + final isActive = currentRoute == item.href; + + if (!item.hasChildren) { + return li([ + a( + href: item.href, + classes: 'sidebar-link${isActive ? ' active' : ''}', + [Component.text(item.text)], + ), + ]); + } + + // Expand the category if the current page is the category itself or a child. + final isChildActive = item.children.any((c) => currentRoute == c.href); + final isExpanded = isActive || isChildActive; + + return li( + classes: 'sidebar-collapsible${isExpanded ? ' expanded' : ''}', + [ + // Category header: navigable link + separate caret toggle button. + // Mirrors Docusaurus's div.menu__list-item-collapsible structure. + div(classes: 'sidebar-category-header', [ + a( + href: item.href, + // Full .active (color + bg tint) when the category page itself is + // current; color-only .parent-active when a child page is current. + classes: + 'sidebar-link sidebar-category-link' + '${isActive + ? ' active' + : isChildActive + ? ' parent-active' + : ''}', + [Component.text(item.text)], + ), + button( + classes: 'sidebar-caret', + attributes: {'type': 'button'}, + [ + svg( + viewBox: '0 0 24 24', + attributes: { + 'width': '16', + 'height': '16', + 'fill': 'none', + 'stroke': 'currentColor', + 'stroke-width': '2.5', + 'stroke-linecap': 'round', + 'stroke-linejoin': 'round', + }, + // Right-pointing chevron (>); rotates 90Β° to point down when expanded. + [polyline(points: '9 6 15 12 9 18', [])], + ), + ], + ), + ]), + // Wrapper div is required for the CSS grid expand animation: + // grid-template-rows: 0fr β†’ 1fr transitions on the actual content + // height, giving smooth expand AND collapse (unlike max-height tricks). + div(classes: 'sidebar-children', [ + ul([ + for (final child in item.children) + li([ + a( + href: child.href, + classes: 'sidebar-link${currentRoute == child.href ? ' active' : ''}', + [Component.text(child.text)], + ), + ]), + ]), + ]), + ], + ); + } + + /// Handles sidebar interactions: + /// - `.sidebar-caret` clicks toggle the `.expanded` class on the parent + /// collapsible item. + /// - `.sidebar-back` clicks switch to the primary panel by adding + /// `.show-primary` to the parent `.sidebar`. + /// - `.sidebar-close` / `.sidebar-barrier` clicks reset the panel state + /// back to secondary so the next open starts fresh. + static const _toggleScript = ''' +(function(){ + document.addEventListener('click', function(e){ + var caret = e.target.closest('.sidebar-caret'); + if (caret) { + caret.closest('.sidebar-collapsible').classList.toggle('expanded'); + return; + } + if (e.target.closest('.sidebar-back')) { + var s = e.target.closest('.sidebar'); + if (s) s.classList.add('show-primary'); + return; + } + if (e.target.closest('.sidebar-close') || e.target.closest('.sidebar-barrier')) { + var s = document.querySelector('.sidebar'); + if (s) s.classList.remove('show-primary'); + } + }); +})(); +'''; + + @css + static List get styles => [ + // ── Sidebar container ──────────────────────────────────────────────────── + css('.sidebar', [ + css('&').styles( + position: Position.relative(), + padding: Padding.only(bottom: 1.25.rem), + fontSize: 0.875.rem, + ), + + // ── Mobile header: nav items + close button ────────────────────────── + // Shown only on mobile (< 1024 px). Mirrors Docusaurus's + // .navbar-sidebar__brand / .navbar-sidebar__items structure. + css('.sidebar-mobile-header', [ + css('&').styles( + display: Display.flex, + height: 3.75.rem, + padding: Padding.symmetric(horizontal: 1.rem, vertical: 0.5.rem), + alignItems: AlignItems.center, + raw: {'flex-shrink': '0'}, + ), + css.media(MediaQuery.all(minWidth: 1024.px), [ + css('&').styles(display: Display.none), + ]), + ]), + + // Flex row inside the mobile header that holds the nav item components. + css('.sidebar-mobile-nav', [ + css('&').styles( + display: Display.flex, + alignItems: AlignItems.center, + gap: Gap.column(0.5.rem), + flex: Flex(grow: 1), + raw: {'flex-wrap': 'wrap'}, + ), + ]), + + // ── Close button (inside mobile header) ───────────────────────────── + // Styled as a square icon button; the SidebarToggleButton client + // component listens for .sidebar-close clicks to dismiss the panel. + css('.sidebar-close', [ + css('&').styles( + display: Display.flex, + width: 2.rem, + height: 2.rem, + radius: BorderRadius.circular(0.25.rem), + justifyContent: JustifyContent.center, + alignItems: AlignItems.center, + color: Color.inherit, + raw: { + 'flex-shrink': '0', + 'border': 'none', + 'background': 'none', + 'cursor': 'pointer', + 'opacity': '0.6', + 'transition': 'opacity 0.15s ease, background 0.15s ease', + }, + ), + css('&:hover').styles( + opacity: 0.9, + backgroundColor: Color('rgba(0, 0, 0, 0.05)'), + ), + ]), + + // ── Primary panel ──────────────────────────────────────────────────── + // Shown on mobile when the sidebar has .show-primary (set by JS). + // Contains the global nav items passed as primaryNavItems. + // Hidden on desktop (β‰₯ 1024 px) and by default on mobile. + css('.sidebar-primary', [ + css('&').styles(display: Display.none), + ]), + css('.sidebar-primary-nav', [ + css('&').styles( + display: Display.flex, + padding: Padding.symmetric(vertical: 0.5.rem), + flexDirection: FlexDirection.column, + gap: Gap.column(2.px), + ), + ]), + + // ── "← Back to main menu" button ──────────────────────────────────── + // Shown only on mobile. Mirrors Docusaurus's secondary-panel back link. + // Clicking switches to the primary panel (handled in _toggleScript). + css('.sidebar-back', [ + css('&').styles( + display: Display.block, + width: Unit.percent(100), + padding: Padding.symmetric(horizontal: 0.75.rem, vertical: 0.625.rem), + opacity: 0.7, + cursor: Cursor.pointer, + transition: Transition('all', duration: 150.ms, curve: Curve.easeInOut), + fontSize: 0.875.rem, + raw: { + 'text-align': 'left', + 'background': 'none', + 'border': 'none', + 'color': 'inherit', + }, + ), + css('&:hover').styles( + opacity: 1, + backgroundColor: Color('rgba(0, 0, 0, 0.05)'), + ), + css.media(MediaQuery.all(minWidth: 1024.px), [ + css('&').styles(display: Display.none), + ]), + ]), + + // ── Sidebar link group ─────────────────────────────────────────────── + css('.sidebar-group', [ + css('&').styles( + padding: Padding.only(top: 1.5.rem, right: 0.75.rem, left: 8.px), + ), + css('ul').styles( + padding: Padding.zero, + margin: Margin.zero, + listStyle: ListStyle.none, + ), + css('li').styles(margin: Margin.only(bottom: 2.px)), + ]), + ]), + + // ── Sidebar link (Docusaurus menu__link) ───────────────────────────────── + css('.sidebar-link').styles( + display: Display.flex, + padding: Padding.symmetric(horizontal: 0.75.rem, vertical: 0.375.rem), + radius: BorderRadius.circular(0.25.rem), + alignItems: AlignItems.center, + color: Color.inherit, + fontSize: 1.rem, + fontWeight: FontWeight.w500, + textDecoration: TextDecoration.none, + raw: { + 'line-height': '1.25', + 'transition': 'background 0.15s ease-in-out', + }, + ), + css('.sidebar-link:hover').styles( + backgroundColor: Color('rgba(0, 0, 0, 0.05)'), + ), + css('.sidebar-link.active').styles( + color: ContentColors.primary, + backgroundColor: Color('rgba(0, 0, 0, 0.05)'), + ), + // Dark mode active background: same tint as hover. + css('[data-theme="dark"] .sidebar-link.active').styles( + backgroundColor: Color('rgba(255, 255, 255, 0.05)'), + ), + // Parent category: color only, no background tint. + css('.sidebar-link.parent-active').styles( + color: ContentColors.primary, + ), + + // ── Category header: link + caret side by side ─────────────────────────── + // Mirrors Docusaurus's div.menu__list-item-collapsible. + css('.sidebar-category-header').styles( + display: Display.flex, + alignItems: AlignItems.stretch, + ), + // Category link takes remaining width; always semibold (Docusaurus default + // for menu__link--sublist regardless of expanded/active state). + css('.sidebar-category-link').styles( + flex: Flex(grow: 1), + fontWeight: FontWeight.w600, + ), + + // ── Caret toggle button (Docusaurus menu__caret) ───────────────────────── + css('.sidebar-caret').styles( + display: Display.flex, + padding: Padding.symmetric(horizontal: 0.25.rem), + radius: BorderRadius.circular(0.25.rem), + alignItems: AlignItems.center, + color: Color.inherit, + raw: { + 'opacity': '0.4', + 'border': 'none', + 'background': 'none', + 'cursor': 'pointer', + 'transition': 'transform 0.2s ease, opacity 0.15s ease', + 'flex-shrink': '0', + }, + ), + css('.sidebar-caret:hover').styles( + opacity: 0.8, + backgroundColor: Color('rgba(0, 0, 0, 0.05)'), + ), + // Caret stays visible (opacity 0.8) and rotates 90Β° when category is expanded. + css('.sidebar-collapsible.expanded .sidebar-caret').styles( + opacity: 0.8, + raw: {'transform': 'rotate(90deg)'}, + ), + + // ── Children collapse/expand animation (CSS grid trick) ───────────────── + // grid-template-rows: 0fr β†’ 1fr transitions on the real content height, + // giving symmetric smooth expand AND collapse animations. + // The direct child (ul) must have overflow:hidden to clip at 0fr. + css('.sidebar-children').styles( + raw: { + 'display': 'grid', + 'grid-template-rows': '0fr', + 'transition': 'grid-template-rows 0.3s ease', + // Indentation applied here (not on the inner ul) to avoid being + // overridden by the higher-specificity .sidebar .sidebar-group ul rule. + 'padding-left': '1rem', + }, + ), + css('.sidebar-children > ul').styles( + overflow: Overflow.hidden, + ), + css('.sidebar-collapsible.expanded .sidebar-children').styles( + raw: {'grid-template-rows': '1fr'}, + ), + + // ── Primary panel: mobile two-panel toggle ─────────────────────────────── + // When .show-primary is on the sidebar: show primary panel, hide back + // button and doc-links group. On desktop these rules have no effect since + // all mobile-only elements are already display:none at β‰₯ 1024 px. + css.media(MediaQuery.all(maxWidth: 1023.px), [ + css('.sidebar.show-primary .sidebar-primary').styles(display: Display.block), + css('.sidebar.show-primary .sidebar-back').styles( + raw: {'display': 'none !important'}, + ), + css('.sidebar.show-primary .sidebar-group').styles(display: Display.none), + ]), + + // ── Dark mode overrides ────────────────────────────────────────────────── + // Active uses var(--primary) which auto-switches via ContentTheme; only + // hover backgrounds and borders need explicit dark overrides. + css('[data-theme="dark"] .sidebar-link:hover').styles( + backgroundColor: Color('rgba(255, 255, 255, 0.05)'), + ), + css('[data-theme="dark"] .sidebar-caret:hover').styles( + backgroundColor: Color('rgba(255, 255, 255, 0.05)'), + ), + css('[data-theme="dark"] .sidebar-close:hover').styles( + backgroundColor: Color('rgba(255, 255, 255, 0.05)'), + ), + css('[data-theme="dark"] .sidebar-back:hover').styles( + backgroundColor: Color('rgba(255, 255, 255, 0.05)'), + ), + ]; +} diff --git a/site_jaspr/lib/components/doc_callout.dart b/site_jaspr/lib/components/doc_callout.dart new file mode 100644 index 00000000..141b6cf7 --- /dev/null +++ b/site_jaspr/lib/components/doc_callout.dart @@ -0,0 +1,192 @@ +import 'package:jaspr/dom.dart'; +import 'package:jaspr/jaspr.dart'; +import 'package:jaspr_content/jaspr_content.dart'; + +/// Docusaurus-compatible callout/admonition component. +/// +/// Replaces jaspr_content's [Callout] to match Docusaurus admonition layout: +/// - Left-border only (5 px), 4 px border-radius +/// - Heading row: filled SVG icon + uppercase type label +/// - Content body below +/// +/// SVG icon paths are the exact Octicon paths used by Docusaurus +/// (packages/docusaurus-theme-classic/src/theme/Admonition/Icon/). +/// +/// Color values match Infima's admonition tokens from +/// @infima/infima/dist/css/default/default.css. +class DocCallout extends CustomComponentBase { + DocCallout(); + + @override + final Pattern pattern = RegExp(r'Info|Warning|Error|Success'); + + @override + Component apply(String name, Map attributes, Component? child) { + return _DocCallout(type: name.toLowerCase(), child: child!); + } + + @css + static List get styles => [ + // Base layout: left-border only, Docusaurus sizing + subtle shadow. + css('.doc-callout').styles( + padding: Padding.symmetric(vertical: 12.px, horizontal: 16.px), + margin: Margin.only(bottom: 1.25.rem), + radius: BorderRadius.circular(4.px), + raw: { + 'border': '0 solid', + 'border-left-width': '5px', + 'box-shadow': '0 1px 2px 0 rgba(0, 0, 0, 0.1)', + }, + ), + + // Heading row: icon + uppercase type label. + // No explicit font-weight: Docusaurus's VGV site only loads Poppins 400, + // so the browser synthesizes a light faux-bold. Inheriting 400 (normal) + // matches that visual output exactly. + css('.doc-callout-heading').styles( + display: Display.flex, + margin: Margin.only(bottom: 0.3.rem), + alignItems: AlignItems.center, + fontSize: 0.875.rem, + fontWeight: FontWeight.w600, + raw: { + 'gap': '0.3em', + 'line-height': '1.25', + 'text-transform': 'uppercase', + }, + ), + + // Icon: 1.6em relative to heading font, fills current color. + css('.doc-callout-icon svg').styles( + raw: { + 'height': '1.6em', + 'width': '1.6em', + 'fill': 'currentColor', + 'flex-shrink': '0', + }, + ), + + // Remove default paragraph margin inside body. + css('.doc-callout-body > p').styles( + margin: Margin.only(bottom: Unit.zero), + ), + + // --- Light mode per-type colors (Infima tokens) --- + css('.doc-callout-info').styles( + color: Color('#193c47'), + backgroundColor: Color('#eef9fd'), + raw: {'border-color': '#54c7ec'}, + ), + css('.doc-callout-warning').styles( + color: Color('#4d3800'), + backgroundColor: Color('#fff8e6'), + raw: {'border-color': '#e6a700'}, + ), + css('.doc-callout-error').styles( + color: Color('#4b1113'), + backgroundColor: Color('#ffebec'), + raw: {'border-color': '#fa5252'}, + ), + css('.doc-callout-success').styles( + color: Color('#003100'), + backgroundColor: Color('#e6fce6'), + raw: {'border-color': '#00a400'}, + ), + + // --- Dark mode: semi-transparent bg, same border colors, inherit text --- + css('[data-theme="dark"] .doc-callout-info').styles( + color: Color.inherit, + backgroundColor: Color('rgba(84, 199, 236, 0.15)'), + raw: {'border-color': '#54c7ec'}, + ), + css('[data-theme="dark"] .doc-callout-warning').styles( + color: Color.inherit, + backgroundColor: Color('rgba(230, 167, 0, 0.15)'), + raw: {'border-color': '#e6a700'}, + ), + css('[data-theme="dark"] .doc-callout-error').styles( + color: Color.inherit, + backgroundColor: Color('rgba(250, 82, 82, 0.15)'), + raw: {'border-color': '#fa5252'}, + ), + css('[data-theme="dark"] .doc-callout-success').styles( + color: Color.inherit, + backgroundColor: Color('rgba(0, 164, 0, 0.15)'), + raw: {'border-color': '#00a400'}, + ), + ]; +} + +class _DocCallout extends StatelessComponent { + const _DocCallout({required this.type, required this.child}); + + final String type; + final Component child; + + @override + Component build(BuildContext context) { + final label = switch (type) { + 'info' => 'info', + 'warning' => 'warning', + 'error' => 'danger', + 'success' => 'tip', + _ => type, + }; + + return div(classes: 'doc-callout doc-callout-$type', [ + div(classes: 'doc-callout-heading', [ + span(classes: 'doc-callout-icon', [_buildIcon(type)]), + Component.text(label), + ]), + div(classes: 'doc-callout-body', [child]), + ]); + } + + static Component _buildIcon(String type) => switch (type) { + 'info' => svg( + viewBox: '0 0 14 16', + attributes: {'fill': 'currentColor'}, + [ + path( + d: 'M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z', + attributes: {'fill-rule': 'evenodd'}, + [], + ), + ], + ), + 'warning' => svg( + viewBox: '0 0 16 16', + attributes: {'fill': 'currentColor'}, + [ + path( + d: 'M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z', + attributes: {'fill-rule': 'evenodd'}, + [], + ), + ], + ), + 'error' => svg( + viewBox: '0 0 12 16', + attributes: {'fill': 'currentColor'}, + [ + path( + d: 'M5.05.31c.81 2.17.41 3.38-.52 4.31C3.55 5.67 1.98 6.45.9 7.98c-1.45 2.05-1.7 6.53 3.53 7.7-2.2-1.16-2.67-4.52-.3-6.61-.61 2.03.53 3.33 1.94 2.86 1.39-.47 2.3.53 2.27 1.67-.02.78-.31 1.44-1.13 1.81 3.42-.59 4.78-3.42 4.78-5.56 0-2.84-2.53-3.22-1.25-5.61-1.52.13-2.03 1.13-1.89 2.75.09 1.08-1.02 1.8-1.86 1.33-.67-.41-.66-1.19-.06-1.78C8.18 5.31 8.68 2.45 5.05.32L5.03.3l.02.01z', + attributes: {'fill-rule': 'evenodd'}, + [], + ), + ], + ), + _ => svg( + // success β†’ tip (lightbulb) + viewBox: '0 0 12 16', + attributes: {'fill': 'currentColor'}, + [ + path( + d: 'M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z', + attributes: {'fill-rule': 'evenodd'}, + [], + ), + ], + ), + }; +} diff --git a/site_jaspr/lib/components/edit_page_link.dart b/site_jaspr/lib/components/edit_page_link.dart new file mode 100644 index 00000000..6b5cea10 --- /dev/null +++ b/site_jaspr/lib/components/edit_page_link.dart @@ -0,0 +1,91 @@ +import 'package:jaspr/dom.dart'; +import 'package:jaspr/jaspr.dart'; +import 'package:jaspr_content/jaspr_content.dart'; + +/// "Edit this page" link shown below doc content, matching Docusaurus exactly. +/// +/// Replicates the behavior of Docusaurus's [EditThisPage] component + +/// [EditMetaRow] wrapper, using the same editUrl base from docusaurus.config.js: +/// https://github.com/VeryGoodOpenSource/very_good_workflows/tree/main/site +/// +/// The SVG pencil icon is the exact path from Docusaurus's Icon/Edit/index.tsx +/// (40Γ—40 viewBox, fill="currentColor"). +class EditPageLink extends StatelessComponent { + const EditPageLink({super.key}); + + static const _editUrlBase = 'https://github.com/VeryGoodOpenSource/very_good_workflows/tree/main/site'; + + @override + Component build(BuildContext context) { + // This component is server-rendered only; context.page is not available + // on the client. + if (kIsWeb) return Component.fragment([]); + + final url = context.page.url; + // Only render on /docs/* pages, but not the workflows category index. + if (!url.startsWith('/docs/') || url == '/docs/workflows') return Component.fragment([]); + + // /docs/workflows/license_check β†’ docs/workflows/license_check.md + final filePath = '${url.replaceFirst('/', '')}.md'; + final editUrl = '$_editUrlBase/$filePath'; + + return div(classes: 'edit-page-row', [ + a( + href: editUrl, + classes: 'edit-page-link theme-edit-this-page', + attributes: { + 'target': '_blank', + 'rel': 'noopener noreferrer', + }, + [ + // Exact SVG from Docusaurus packages/docusaurus-theme-classic/ + // src/theme/Icon/Edit/index.tsx + // viewBox="0 0 40 40", fill="currentColor" (filled pencil) + svg( + width: 20.px, + height: 20.px, + viewBox: '0 0 40 40', + attributes: { + 'fill': 'currentColor', + 'aria-hidden': 'true', + }, + [ + path( + d: 'm34.5 11.7l-3 3.1-6.3-6.3 3.1-3q0.5-0.5 1.2-0.5t1.1 0.5l3.9 3.9q0.5 0.4 0.5 1.1t-0.5 1.2z m-29.5 17.1l18.4-18.5 6.3 6.3-18.4 18.4h-6.3v-6.2z', + [], + ), + ], + ), + Component.text('Edit this page'), + ], + ), + ]); + } + + @css + static List get styles => [ + // Block container β€” provides the 3rem top margin gap from content, + // matching Docusaurus's `docusaurus-mt-lg` class on DocItemFooter. + css('.edit-page-row').styles( + margin: Margin.only(top: 1.75.rem), + ), + + // The link itself: inline-flex so icon and text sit side by side. + // color matches Infima's --ifm-link-color = var(--ifm-color-primary). + css('.edit-page-link').styles( + display: Display.inlineFlex, + alignItems: AlignItems.center, + color: Color('var(--primary)'), + textDecoration: TextDecoration.none, + raw: {'gap': '0.3em'}, + ), + css('.edit-page-link:hover').styles( + textDecoration: TextDecoration(line: TextDecorationLine.underline), + ), + + // Prevent the SVG from growing/shrinking inside the flex row. + css('.edit-page-link svg').styles( + flex: Flex(shrink: 0), + ), + ]; +} diff --git a/site_jaspr/lib/components/homepage_layout.dart b/site_jaspr/lib/components/homepage_layout.dart new file mode 100644 index 00000000..9fa17b69 --- /dev/null +++ b/site_jaspr/lib/components/homepage_layout.dart @@ -0,0 +1,503 @@ +import 'package:jaspr/dom.dart'; +import 'package:jaspr/server.dart'; +import 'package:jaspr_content/components/header.dart'; +import 'package:jaspr_content/components/theme_toggle.dart'; +import 'package:jaspr_content/jaspr_content.dart'; + +import 'icon_link.dart'; +import 'nav_link.dart'; +import 'site_footer.dart'; + +/// A custom page layout for the homepage. +class HomepageLayout extends PageLayoutBase { + const HomepageLayout(); + + @override + String get name => 'homepage'; + + @override + Iterable buildHead(Page page) sync* { + yield* super.buildHead(page); + yield Style(styles: _styles); + } + + @override + Component buildBody(Page page, Component child) { + return div(classes: 'homepage', [ + // data-has-sidebar makes the SidebarToggleButton (hamburger) visible + // at narrow viewports β€” it's part of the Header's leading slot. + div( + classes: 'header-container', + attributes: {'data-has-sidebar': ''}, + [ + Header( + title: 'Very Good Workflows', + logo: '/images/workflows_nav_icon.svg', + items: [ + NavLink(text: 'Get Started', href: '/docs/overview', isButton: true), + NavLink(text: 'VGV Dev Tools', href: 'https://verygood.ventures/dev'), + IconLink( + href: 'https://verygood.ventures', + iconSrc: '/images/vgv_logo_black.svg', + darkIconSrc: '/images/vgv_logo_fill.svg', + alt: 'Very Good Ventures', + ), + IconLink( + href: 'https://github.com/VeryGoodOpenSource/very_good_workflows', + iconSrc: '/images/github.svg', + darkIconSrc: '/images/github_white.svg', + alt: 'GitHub', + ), + ThemeToggle(), + ], + ), + ], + ), + // Mobile sidebar backdrop β€” must immediately precede .sidebar-container + // for the :has(+ .sidebar-container.open) CSS selector to work. + div(classes: 'sidebar-barrier', attributes: {'role': 'button'}, []), + // Mobile sidebar panel β€” same primary nav as the docs sidebar's primary + // panel (Get Started, VGV Dev Tools, VGV icon, GitHub icon). + // SidebarToggleButton (in the Header) toggles .open on this container. + div(classes: 'sidebar-container', [ + nav(classes: 'sidebar', [ + // Mobile header: logo pill + ThemeToggle + close Γ— + div(classes: 'sidebar-mobile-header', [ + div(classes: 'sidebar-mobile-nav', [ + a( + href: '/', + classes: 'mobile-workflows-btn', + [ + img( + src: '/images/workflows_nav_icon.svg', + alt: 'Workflows', + attributes: {'height': '32', 'width': '105'}, + ), + ], + ), + ThemeToggle(), + ]), + button( + classes: 'sidebar-close', + attributes: {'type': 'button', 'aria-label': 'Close menu'}, + [ + svg( + viewBox: '0 0 24 24', + attributes: { + 'width': '20', + 'height': '20', + 'fill': 'none', + 'stroke': 'currentColor', + 'stroke-width': '2', + }, + [ + line( + attributes: {'x1': '18', 'y1': '6', 'x2': '6', 'y2': '18'}, + [], + ), + line( + attributes: {'x1': '6', 'y1': '6', 'x2': '18', 'y2': '18'}, + [], + ), + ], + ), + ], + ), + ]), + // Primary nav items as sidebar-style links + div(classes: 'sidebar-home-nav', [ + ul([ + li([ + a( + href: '/docs/overview', + classes: 'sidebar-primary-link', + [Component.text('Get Started')], + ), + ]), + li([ + a( + href: 'https://verygood.ventures/dev', + target: Target.blank, + classes: 'sidebar-primary-link', + [Component.text('VGV Dev Tools')], + ), + ]), + li([ + a( + href: 'https://verygood.ventures', + target: Target.blank, + classes: 'sidebar-primary-link', + [ + img( + classes: 'sidebar-icon-light', + src: '/images/vgv_logo_black.svg', + alt: 'Very Good Ventures', + attributes: {'width': '24', 'height': '24'}, + ), + img( + classes: 'sidebar-icon-dark', + src: '/images/vgv_logo_fill.svg', + alt: 'Very Good Ventures', + attributes: {'width': '24', 'height': '24'}, + ), + ], + ), + ]), + li([ + a( + href: 'https://github.com/VeryGoodOpenSource/very_good_workflows', + target: Target.blank, + classes: 'sidebar-primary-link', + [ + img( + classes: 'sidebar-icon-light', + src: '/images/github.svg', + alt: 'GitHub', + attributes: {'width': '24', 'height': '24'}, + ), + img( + classes: 'sidebar-icon-dark', + src: '/images/github_white.svg', + alt: 'GitHub', + attributes: {'width': '24', 'height': '24'}, + ), + ], + ), + ]), + ]), + ]), + ]), + ]), + header(classes: 'hero-banner', [ + div(classes: 'hero-content', [ + img( + classes: 'hero-logo hero-logo-light', + src: '/images/workflows_logo.svg', + alt: 'Very Good Workflows Logo', + ), + img( + classes: 'hero-logo hero-logo-dark', + src: '/images/workflows_logo_dark.svg', + alt: 'Very Good Workflows Logo', + ), + p( + classes: 'hero-subtitle', + [ + Component.text( + 'A collection of helpful, reusable GitHub workflows used by VGV.', + ), + ], + ), + img( + classes: 'hero-image', + src: '/images/workflows_hero.png', + alt: 'Very Good Workflows Hero', + attributes: {'width': '720'}, + ), + div(classes: 'cta', [ + a( + classes: 'cta-button', + href: '/docs/overview', + [Component.text('Get Started >')], + ), + ]), + ]), + ]), + child, + main_([ + div(classes: 'blog-section', [ + div(classes: 'blog-row', [ + div(classes: 'blog-column', [ + img( + src: + 'https://uploads-ssl.webflow.com/5ee12d8e99cde2e20255c16c/61ef1d505cfdeb570f714a7f_Very%20good%20workflows.jpg', + alt: 'Configuring workflows for your Flutter projects', + attributes: {'width': '452', 'height': '254'}, + ), + ]), + div(classes: 'blog-column', [ + div(classes: 'blog-content', [ + h2([ + Component.text( + 'Configuring workflows for your Flutter projects', + ), + ]), + p([ + Component.text( + 'A guide for using Very Good Workflows in your projects.', + ), + ]), + a( + classes: 'blog-link', + href: 'https://verygood.ventures/blog/configuring-workflows-for-your-flutter-projects', + [Component.text('Read the Blog >')], + ), + ]), + ]), + ]), + ]), + ]), + const SiteFooter(), + ]); + } + + // Field order from Styles: display, position, zIndex, width, height, + // minWidth, minHeight, maxWidth, maxHeight, padding, margin, border, radius, + // opacity, ..., flexDirection, justifyContent, alignItems, gap, flex, color, + // textAlign, fontSize, fontWeight, textDecoration, lineHeight, backgroundColor, raw + static List get _styles => [ + css('.homepage', [ + css('&').styles(minHeight: 100.vh), + css('.header-container', [ + css('&').styles( + position: Position.fixed(top: Unit.zero, left: Unit.zero, right: Unit.zero), + zIndex: ZIndex(10), + backgroundColor: Color('var(--navbar-bg)'), + ), + ]), + + // Hero banner + css('.hero-banner', [ + css('&').styles( + padding: Padding.only(top: 8.rem, bottom: 4.rem, left: 2.rem, right: 2.rem), + textAlign: TextAlign.center, + backgroundColor: Color('var(--background)'), + ), + css.media(MediaQuery.all(maxWidth: 996.px), [ + css('&').styles( + padding: Padding.only(top: 6.rem, right: 2.rem, bottom: 2.rem, left: 2.rem), + ), + ]), + ]), + css('.hero-content', [ + css('&').styles( + display: Display.flex, + flexDirection: FlexDirection.column, + alignItems: AlignItems.center, + ), + ]), + css('.hero-logo', [ + css('&').styles( + width: 400.px, + margin: Margin.only(bottom: 1.rem), + ), + ]), + // Theme-aware logo switching + css('.hero-logo-dark').styles(display: Display.none), + css('.hero-subtitle', [ + css('&').styles( + margin: Margin.only(bottom: 1.25.rem), + color: Color('#444950'), + fontSize: 1.5.rem, + ), + ]), + css('.hero-image', [ + css('&').styles( + height: Unit.auto, + maxWidth: 100.percent, + margin: Margin.symmetric(vertical: 1.rem), + ), + ]), + + // CTA button + css('.cta', [ + css('&').styles( + display: Display.flex, + justifyContent: JustifyContent.center, + alignItems: AlignItems.center, + gap: Gap(column: 2.rem), + ), + ]), + css('.cta-button', [ + css('&').styles( + display: Display.inlineBlock, + padding: Padding.symmetric(horizontal: 2.rem, vertical: 0.5.rem), + radius: BorderRadius.circular(0.4.rem), + color: Colors.white, + fontSize: 1.2.rem, + fontWeight: FontWeight.w700, + textDecoration: TextDecoration.none, + backgroundColor: Color('var(--primary)'), + ), + css('&:hover').styles(backgroundColor: Color('var(--primary-hover)')), + ]), + // Blog section + css('.blog-section', [ + css('&').styles( + display: Display.flex, + padding: Padding.only(top: 4.rem, bottom: 6.5.rem, left: 3.rem, right: 3.rem), + flexDirection: FlexDirection.column, + alignItems: AlignItems.center, + gap: Gap(row: 0.5.rem), + ), + css.media(MediaQuery.all(maxWidth: 996.px), [ + css('&').styles( + padding: Padding.symmetric(horizontal: 1.rem, vertical: 2.rem), + ), + ]), + ]), + css('.blog-row', [ + css('&').styles( + display: Display.flex, + flexDirection: FlexDirection.row, + justifyContent: JustifyContent.center, + alignItems: AlignItems.center, + gap: Gap(column: 2.rem, row: 2.rem), + ), + css.media(MediaQuery.all(maxWidth: 996.px), [ + css('&').styles( + flexDirection: FlexDirection.column, + gap: Gap(column: 1.rem, row: 1.rem), + ), + ]), + ]), + css('.blog-column', [ + css('&').styles( + display: Display.flex, + maxWidth: 452.px, + flexDirection: FlexDirection.column, + alignItems: AlignItems.center, + gap: Gap(row: 1.rem), + ), + css('img').styles(height: Unit.auto, maxWidth: 100.percent, radius: BorderRadius.circular(8.px)), + ]), + css('.blog-content', [ + css('&').styles( + display: Display.flex, + flexDirection: FlexDirection.column, + alignItems: AlignItems.start, + gap: Gap(row: 1.rem), + lineHeight: 1.5.rem, + ), + css('h2').styles( + color: Color('var(--text)'), + fontSize: 1.5.rem, + fontWeight: FontWeight.w600, + lineHeight: 2.rem, + ), + css('p').styles(color: Color('var(--text)')), + ]), + css('.blog-link', [ + css('&').styles( + margin: Margin.only(top: 0.3.rem), + color: Color('var(--primary)'), + fontWeight: FontWeight.bold, + textDecoration: TextDecoration.none, + ), + css('&:hover').styles(textDecoration: TextDecoration(line: TextDecorationLine.underline)), + ]), + ]), + // Hide the empty content section rendered by the Content component + css('.homepage .content').styles(display: Display.none), + // Dark mode: swap hero logos + css('[data-theme="dark"] .hero-logo-light').styles(display: Display.none), + css('[data-theme="dark"] .hero-logo-dark').styles( + raw: {'display': 'block !important'}, + ), + // Dark mode: CTA button text (bg handled by --primary token) + css('[data-theme="dark"] .cta-button').styles( + color: Color('var(--background)'), + ), + // Dark mode: hero subtitle (unique color, not in tokens) + css('[data-theme="dark"] .hero-subtitle').styles(color: Color('#ebedf0')), + + // ── Home page mobile sidebar ───────────────────────────────────────────── + // The SidebarToggleButton in the Header toggles .open on .sidebar-container. + // The barrier uses :has(+ .sidebar-container.open) to show the scrim. + css('.homepage .sidebar-container').styles( + position: Position.fixed(top: Unit.zero, bottom: Unit.zero), + zIndex: ZIndex(200), + overflow: Overflow.only(y: Overflow.auto), + raw: { + 'width': '83vw', + 'transform': 'translateX(-100%)', + 'transition': 'transform 150ms ease-in-out', + 'background-color': 'var(--navbar-bg)', + }, + ), + css('.homepage .sidebar-container.open').styles( + raw: {'transform': 'translateX(0)'}, + ), + css('.homepage .sidebar-barrier').styles( + position: Position.fixed(), + zIndex: ZIndex(199), + opacity: 0, + pointerEvents: PointerEvents.none, + raw: {'inset': '0', 'background': '#000'}, + ), + css('.homepage .sidebar-barrier:has(+ .sidebar-container.open)').styles( + opacity: 0.5, + pointerEvents: PointerEvents.auto, + ), + // Home sidebar nav: reuse CollapsibleSidebar's sidebar-mobile-header layout. + // The .sidebar-mobile-header, .sidebar-mobile-nav, .sidebar-close styles + // are injected by CollapsibleSidebar on docs pages, so duplicate them here. + css('.homepage .sidebar .sidebar-mobile-header').styles( + display: Display.flex, + padding: Padding.symmetric(horizontal: 0.5.rem, vertical: 0.5.rem), + alignItems: AlignItems.center, + raw: {'flex-shrink': '0'}, + ), + css('.homepage .sidebar .sidebar-mobile-nav').styles( + display: Display.flex, + alignItems: AlignItems.center, + gap: Gap.column(0.25.rem), + flex: Flex(grow: 1), + raw: {'flex-wrap': 'wrap'}, + ), + css('.homepage .sidebar .sidebar-close').styles( + display: Display.flex, + width: 2.rem, + height: 2.rem, + radius: BorderRadius.circular(0.25.rem), + justifyContent: JustifyContent.center, + alignItems: AlignItems.center, + color: Color.inherit, + raw: { + 'flex-shrink': '0', + 'border': 'none', + 'background': 'none', + 'cursor': 'pointer', + 'opacity': '0.6', + 'transition': 'opacity 0.15s ease, background 0.15s ease', + }, + ), + css('.homepage .sidebar .sidebar-close:hover').styles( + opacity: 0.9, + backgroundColor: Color('rgba(0, 0, 0, 0.05)'), + ), + // Home sidebar nav items: .sidebar-home-nav ul/li + .sidebar-primary-link + css('.homepage .sidebar-home-nav').styles( + padding: Padding.only(top: 1.5.rem, right: 0.75.rem), + ), + css('.homepage .sidebar-home-nav ul').styles( + padding: Padding.zero, + margin: Margin.zero, + listStyle: ListStyle.none, + ), + css('.homepage .sidebar-home-nav li').styles(margin: Margin.only(bottom: 2.px)), + // Sidebar-style link for home primary panel items + css('.sidebar-primary-link').styles( + display: Display.flex, + padding: Padding.symmetric(horizontal: 0.75.rem, vertical: 0.375.rem), + radius: BorderRadius.circular(0.25.rem), + alignItems: AlignItems.center, + color: Color.inherit, + fontWeight: FontWeight.w400, + textDecoration: TextDecoration.none, + raw: { + 'line-height': '1.25', + 'transition': 'background 0.15s ease-in-out', + }, + ), + css('.sidebar-primary-link:hover').styles( + backgroundColor: Color('rgba(0, 0, 0, 0.05)'), + ), + css('[data-theme="dark"] .sidebar-primary-link:hover').styles( + backgroundColor: Color('rgba(255, 255, 255, 0.05)'), + ), + css('[data-theme="dark"] .homepage .sidebar .sidebar-close:hover').styles( + backgroundColor: Color('rgba(255, 255, 255, 0.05)'), + ), + ]; +} diff --git a/site_jaspr/lib/components/icon_link.dart b/site_jaspr/lib/components/icon_link.dart new file mode 100644 index 00000000..db8a99f0 --- /dev/null +++ b/site_jaspr/lib/components/icon_link.dart @@ -0,0 +1,61 @@ +import 'package:jaspr/dom.dart'; +import 'package:jaspr/jaspr.dart'; + +/// An icon-only navigation link for the header. +class IconLink extends StatelessComponent { + const IconLink({ + required this.href, + required this.iconSrc, + required this.alt, + this.darkIconSrc, + this.size = 24, + super.key, + }); + + final String href; + final String iconSrc; + final String alt; + final String? darkIconSrc; + final int size; + + @override + Component build(BuildContext context) { + final attrs = {'width': '$size', 'height': '$size'}; + final children = [ + img( + classes: darkIconSrc != null ? 'icon-light' : null, + src: iconSrc, + alt: alt, + attributes: attrs, + ), + if (darkIconSrc != null) + img( + classes: 'icon-dark', + src: darkIconSrc!, + alt: alt, + attributes: attrs, + ), + ]; + return a( + classes: 'icon-link', + href: href, + target: Target.blank, + children, + ); + } + + // Item 7: opacity 1 default, 0.6 on hover (matching original) + @css + static List get styles => [ + css('.icon-link', [ + css('&').styles( + display: Display.flex, + padding: Padding.symmetric(horizontal: 0.5.rem), + alignItems: AlignItems.center, + textDecoration: TextDecoration.none, + ), + css('&:hover').styles(opacity: 0.6), + css('.icon-dark').styles(display: Display.none), + ]), + ]; +} diff --git a/site_jaspr/lib/components/nav_link.dart b/site_jaspr/lib/components/nav_link.dart new file mode 100644 index 00000000..f4c3368d --- /dev/null +++ b/site_jaspr/lib/components/nav_link.dart @@ -0,0 +1,58 @@ +import 'package:jaspr/dom.dart'; +import 'package:jaspr/jaspr.dart'; + +/// A simple navigation link for the header. +/// +/// Set [isButton] to `true` to render as a styled button (e.g. "Get Started"). +class NavLink extends StatelessComponent { + const NavLink({ + required this.text, + required this.href, + this.isButton = false, + super.key, + }); + + final String text; + final String href; + final bool isButton; + + @override + Component build(BuildContext context) { + return a( + classes: isButton ? 'nav-link nav-button' : 'nav-link', + href: href, + target: href.startsWith('http') ? Target.blank : null, + [Component.text(text)], + ); + } + + @css + static List get styles => [ + css('.nav-link').styles( + padding: Padding.symmetric(horizontal: 0.75.rem, vertical: 0.25.rem), + color: Color('var(--text)'), + fontSize: 0.875.rem, + fontWeight: FontWeight.w500, + textDecoration: TextDecoration.none, + ), + css('.nav-button', [ + css('&').styles( + padding: Padding.symmetric(horizontal: 1.rem, vertical: 0.5.rem), + radius: BorderRadius.circular(0.4.rem), + color: Colors.white, + fontWeight: FontWeight.w600, + backgroundColor: Color('var(--primary)'), + raw: {'line-height': '1.25'}, + ), + css('&:hover').styles(backgroundColor: Color('var(--primary-hover)')), + css.media(MediaQuery.all(maxWidth: 996.px), [ + css('&').styles( + padding: Padding.symmetric(horizontal: 1.rem, vertical: 0.5.rem), + color: Color('var(--text)'), + fontWeight: FontWeight.w300, + backgroundColor: Colors.white, + ), + ]), + ]), + ]; +} diff --git a/site_jaspr/lib/components/page_navigation.dart b/site_jaspr/lib/components/page_navigation.dart new file mode 100644 index 00000000..b9117912 --- /dev/null +++ b/site_jaspr/lib/components/page_navigation.dart @@ -0,0 +1,88 @@ +import 'package:jaspr/dom.dart'; +import 'package:jaspr/jaspr.dart'; +import 'package:jaspr_content/jaspr_content.dart'; + +/// Previous/next navigation buttons for docs pages. +/// +/// Page order matches the sidebar configuration. +class PageNavigation extends StatelessComponent { + const PageNavigation({super.key}); + + static const _pages = [ + (title: 'Overview', href: '/docs/overview'), + (title: 'Workflows', href: '/docs/workflows'), + (title: 'Dart Package', href: '/docs/workflows/dart_package'), + (title: 'Dart Pub Publish', href: '/docs/workflows/dart_pub_publish'), + (title: 'Flutter Package', href: '/docs/workflows/flutter_package'), + (title: 'Flutter Pub Publish', href: '/docs/workflows/flutter_pub_publish'), + (title: 'License Check', href: '/docs/workflows/license_check'), + (title: 'Mason Publish', href: '/docs/workflows/mason_publish'), + (title: 'Pana', href: '/docs/workflows/pana'), + (title: 'Semantic Pull Request', href: '/docs/workflows/semantic_pull_request'), + (title: 'Spell Check', href: '/docs/workflows/spell_check'), + ]; + + @override + Component build(BuildContext context) { + if (kIsWeb) return Component.fragment([]); + + final url = context.page.url; + final index = _pages.indexWhere((pg) => pg.href == url); + if (index == -1) return Component.fragment([]); + + final prev = index > 0 ? _pages[index - 1] : null; + final next = index < _pages.length - 1 ? _pages[index + 1] : null; + + return nav(classes: 'page-nav', [ + if (prev != null) + a(classes: 'page-nav-prev', href: prev.href, [ + span(classes: 'page-nav-label', [Component.text('Previous')]), + span(classes: 'page-nav-title', [Component.text('\u00AB ${prev.title}')]), + ]), + if (next != null) + a(classes: 'page-nav-next', href: next.href, [ + span(classes: 'page-nav-label', [Component.text('Next')]), + span(classes: 'page-nav-title', [Component.text('${next.title} \u00BB')]), + ]), + ]); + } + + @css + static List get styles => [ + css('.page-nav', [ + css('&').styles( + display: Display.flex, + padding: Padding.zero, + margin: Margin.only(top: 48.px), + border: Border.only(top: BorderSide(color: Color('#0000000d'), width: 1.px)), + gap: Gap(column: 0.75.rem), + ), + ]), + css('.page-nav-prev, .page-nav-next').styles( + display: Display.flex, + padding: Padding.all(1.rem), + border: Border.all(color: Color('#0000001f'), width: 1.px), + radius: BorderRadius.circular(0.4.rem), + flexDirection: FlexDirection.column, + gap: Gap(row: 0.25.rem), + textDecoration: TextDecoration.none, + raw: {'flex': '0 1 calc(50% - 0.375rem)', 'line-height': '1.25'}, + ), + css('.page-nav-prev:hover, .page-nav-next:hover').styles( + border: Border.all(color: Color('var(--primary)'), width: 1.px), + ), + css('.page-nav-next').styles( + margin: Margin.only(left: Unit.auto), + alignItems: AlignItems.end, + ), + css('.page-nav-label').styles( + color: Color('var(--secondary-text)'), + fontSize: 0.875.rem, + fontWeight: FontWeight.w500, + ), + css('.page-nav-title').styles( + color: Color('var(--primary)'), + fontWeight: FontWeight.w700, + ), + ]; +} diff --git a/site_jaspr/lib/components/safe_code_block.dart b/site_jaspr/lib/components/safe_code_block.dart new file mode 100644 index 00000000..ea06b32e --- /dev/null +++ b/site_jaspr/lib/components/safe_code_block.dart @@ -0,0 +1,139 @@ +import 'package:jaspr/dom.dart'; +import 'package:jaspr/server.dart'; +import 'package:jaspr_content/components/_internal/code_block_copy_button.dart'; +import 'package:jaspr_content/components/code_block.dart'; +import 'package:jaspr_content/jaspr_content.dart'; +import 'package:syntax_highlight_lite/syntax_highlight_lite.dart' hide Color; + +/// A code block that renders syntax-highlighted code in **both** light and +/// dark themes at server-render time, toggling visibility via CSS. +/// +/// Docusaurus uses `prism-react-renderer` with: +/// - light: vsLight β†’ [HighlighterTheme.loadLightTheme()] (Light VS + Light+) +/// - dark: vsDark β†’ [HighlighterTheme.loadDarkTheme()] (Dark VS + Dark+) +/// +/// Both [pre] elements are emitted into the HTML; CSS hides the inactive one: +/// `.syntax-dark { display: none }` +/// `[data-theme="dark"] .syntax-light { display: none }` +/// `[data-theme="dark"] .syntax-dark { display: block }` +/// +/// Falls back to [CodeBlock.from] (plain, no highlighting) for any language +/// not in [grammars]. +class SafeCodeBlock extends CustomComponent { + SafeCodeBlock({this.grammars = const {}}) : super.base(); + + final Map grammars; + + bool _initialized = false; + HighlighterTheme? _lightTheme; + HighlighterTheme? _darkTheme; + + @override + Component? create(Node node, NodesBuilder builder) { + if (node + case ElementNode(tag: 'Code' || 'CodeBlock', :final children, :final attributes) || + ElementNode( + tag: 'pre', + children: [ElementNode(tag: 'code', :final children, :final attributes)], + )) { + var language = attributes['language']; + if (language == null && (attributes['class']?.startsWith('language-') ?? false)) { + language = attributes['class']!.substring('language-'.length); + } + + if (!_initialized) { + Highlighter.initialize(['dart']); + for (final entry in grammars.entries) { + Highlighter.addLanguage(entry.key, entry.value); + } + _initialized = true; + } + + final source = children?.map((c) => c.innerText).join(' ') ?? ''; + + // Fall back to plain (unhighlighted) rendering for unsupported languages. + if (language != null && !_supportedLanguages.contains(language)) { + return CodeBlock.from(source: source); + } + + return AsyncBuilder( + builder: (context) async { + _lightTheme ??= await HighlighterTheme.loadLightTheme(); + _darkTheme ??= await HighlighterTheme.loadDarkTheme(); + + final lang = language ?? 'dart'; + return _DualCodeBlock( + source: source, + lightHighlighter: Highlighter(language: lang, theme: _lightTheme!), + darkHighlighter: Highlighter(language: lang, theme: _darkTheme!), + ); + }, + ); + } + return null; + } + + Set get _supportedLanguages => {'dart', ...grammars.keys}; + + @css + static List get styles => [ + // Light theme visible by default; dark theme hidden. + css('.syntax-dark').styles(display: Display.none), + // Swap in dark mode. + css('[data-theme="dark"] .syntax-light').styles(display: Display.none), + css('[data-theme="dark"] .syntax-dark').styles(display: Display.block), + ]; +} + +/// Emits two [pre] blocks (one per theme) inside a `.code-block` wrapper. +/// CSS decides which is shown based on `[data-theme]`. +class _DualCodeBlock extends StatelessComponent { + const _DualCodeBlock({ + required this.source, + required this.lightHighlighter, + required this.darkHighlighter, + }); + + final String source; + final Highlighter lightHighlighter; + final Highlighter darkHighlighter; + + @override + Component build(BuildContext context) { + return div(classes: 'code-block', [ + CodeBlockCopyButton(), + pre(classes: 'syntax-light', [ + code([_buildSpan(lightHighlighter.highlight(source))]), + ]), + pre(classes: 'syntax-dark', [ + code([_buildSpan(darkHighlighter.highlight(source))]), + ]), + ]); + } + + /// Converts a [TextSpan] tree from the highlighter into jaspr [Component]s + /// with inline styles β€” mirrors `_CodeBlock.buildSpan` from jaspr_content. + static Component _buildSpan(TextSpan textSpan) { + Styles? styles; + + if (textSpan.style case final style?) { + styles = Styles( + color: Color.value(style.foreground.argb & 0x00FFFFFF), + fontWeight: style.bold ? FontWeight.bold : null, + fontStyle: style.italic ? FontStyle.italic : null, + textDecoration: style.underline + ? TextDecoration(line: TextDecorationLine.underline) + : null, + ); + } + + if (styles == null && textSpan.children.isEmpty) { + return Component.text(textSpan.text ?? ''); + } + + return span(styles: styles, [ + if (textSpan.text != null) Component.text(textSpan.text!), + for (final child in textSpan.children) _buildSpan(child), + ]); + } +} diff --git a/site_jaspr/lib/components/site_footer.dart b/site_jaspr/lib/components/site_footer.dart new file mode 100644 index 00000000..b24cfa1c --- /dev/null +++ b/site_jaspr/lib/components/site_footer.dart @@ -0,0 +1,148 @@ +import 'package:jaspr/dom.dart'; +import 'package:jaspr/jaspr.dart'; + +import '../styles/site_styles.dart'; + +/// The site footer with copyright notice. +/// +/// Also injects the Poppins font and comprehensive site styles into the +/// document head. +class SiteFooter extends StatelessComponent { + const SiteFooter({super.key}); + + @override + Component build(BuildContext context) { + return Component.fragment([ + Document.head(children: [ + link( + rel: 'preconnect', + href: 'https://fonts.googleapis.com', + ), + link( + rel: 'stylesheet', + href: 'https://fonts.googleapis.com/css2?family=Poppins:wght@400&display=swap', + ), + Style(styles: siteStyles), + script(defer: true, content: _tocScrollspy), + script(defer: true, content: _mobileToc), + script(defer: true, content: _relocateFooter), + ]), + footer(classes: 'site-footer', [ + p([ + Component.text('Built with \u{1F499} by '), + a(href: 'https://verygood.ventures', [b([Component.text('Very Good Ventures')])]), + ]), + p([ + Component.text('Copyright \u00A9 ${DateTime.now().year} Very Good Ventures.'), + ]), + ]), + ]); + } + + /// Inline script that highlights the active TOC link based on scroll position. + static const _tocScrollspy = ''' +(function(){ + function update() { + var links = document.querySelectorAll('.toc a'); + if (!links.length) return; + var ids = []; + links.forEach(function(a) { + var h = a.getAttribute('href'); + if (h) { var id = h.split('#')[1]; if (id) ids.push({id:id, el:a}); } + }); + if (!ids.length) return; + var active = null; + for (var i = 0; i < ids.length; i++) { + var t = document.getElementById(ids[i].id); + if (t && t.getBoundingClientRect().top <= 100) active = i; + } + links.forEach(function(a) { a.classList.remove('toc-active'); }); + if (active === null) active = 0; + ids[active].el.classList.add('toc-active'); + } + window.addEventListener('scroll', update, {passive:true}); + update(); +})(); +'''; + + /// Inline script that creates a collapsible mobile TOC above the content + /// when the right sidebar TOC is hidden (viewport < 1000px). + /// + /// Uses requestAnimationFrame to defer until after Jaspr's client-side + /// hydration completes (hydration replaces DOM, removing elements added + /// synchronously during defer script execution). + static const _mobileToc = ''' +(function(){ + function init(){ + if (document.querySelector('.mobile-toc')) return; + var toc = document.querySelector('aside.toc'); + if (!toc) return; + var ul = toc.querySelector('ul'); + if (!ul || !ul.children.length) return; + + var mobile = document.createElement('div'); + mobile.className = 'mobile-toc'; + + var toggle = document.createElement('button'); + toggle.className = 'mobile-toc-toggle'; + toggle.innerHTML = 'On this page'; + + var content = document.createElement('div'); + content.className = 'mobile-toc-content'; + content.appendChild(ul.cloneNode(true)); + + mobile.appendChild(toggle); + mobile.appendChild(content); + + var cc = document.querySelector('.content-container'); + var section = cc && cc.querySelector('section.content'); + if (cc && section) cc.insertBefore(mobile, section); + } + // Defer to next frame so Jaspr hydration completes first. + requestAnimationFrame(function(){ requestAnimationFrame(init); }); + // Toggle via event delegation (survives any future DOM changes). + document.addEventListener('click', function(e){ + var btn = e.target.closest('.mobile-toc-toggle'); + if (!btn) return; + btn.closest('.mobile-toc').classList.toggle('expanded'); + }); +})(); +'''; + + /// Moves `.site-footer` from inside `.content-container` to be a direct + /// child of `.main-container`, so its background spans the full viewport + /// width and paints above the sidebar border. + static const _relocateFooter = ''' +(function(){ + function move(){ + var footer = document.querySelector('.site-footer'); + var mc = document.querySelector('.main-container'); + if (footer && mc && footer.parentElement !== mc) { + mc.appendChild(footer); + } + } + requestAnimationFrame(function(){ requestAnimationFrame(move); }); +})(); +'''; + + @css + static List get styles => [ + css('.site-footer', [ + css('&').styles( + padding: Padding.all(2.rem), + color: Color('var(--text)'), + textAlign: TextAlign.center, + fontSize: 1.rem, + backgroundColor: Color('var(--background)'), + raw: { + 'position': 'relative', + 'z-index': '11', + }, + ), + css('a').styles( + color: Color('var(--primary)'), + textDecoration: TextDecoration.none, + ), + ]), + ]; +} diff --git a/site_jaspr/lib/extensions/code_aware_toc.dart b/site_jaspr/lib/extensions/code_aware_toc.dart new file mode 100644 index 00000000..f0f470f4 --- /dev/null +++ b/site_jaspr/lib/extensions/code_aware_toc.dart @@ -0,0 +1,97 @@ +import 'package:jaspr/dom.dart'; +import 'package:jaspr/jaspr.dart'; +import 'package:jaspr_content/jaspr_content.dart'; +import 'package:jaspr_router/jaspr_router.dart'; + +/// A post-processing extension that runs after [TableOfContentsExtension] +/// to preserve `` formatting in TOC entries. +/// +/// Headings written with backticks in markdown (e.g. `` ### `types` ``) will +/// render their TOC link text inside a `` element, matching Docusaurus +/// behavior where code-styled headings look visually distinct from regular +/// headings in the table of contents. +class CodeAwareTocPostProcessor implements PageExtension { + static final _headerRegex = RegExp(r'^h(\d)$', caseSensitive: false); + + @override + Future> apply(Page page, List nodes) async { + final codeHeadingIds = {}; + _findCodeHeadings(nodes, codeHeadingIds); + + if (codeHeadingIds.isNotEmpty) { + final toc = page.data['toc']; + if (toc is TableOfContents) { + page.apply(data: { + 'toc': _CodeAwareTableOfContents(toc.entries, codeHeadingIds), + }); + } + } + + return nodes; + } + + void _findCodeHeadings(List nodes, Set codeIds) { + for (final node in nodes) { + if (node is ElementNode) { + if (_headerRegex.hasMatch(node.tag)) { + final id = node.attributes['id']; + if (id != null && _hasCodeChild(node)) { + codeIds.add(id); + } + } + if (node.children != null) { + _findCodeHeadings(node.children!, codeIds); + } + } + } + } + + bool _hasCodeChild(ElementNode node) { + for (final child in node.children ?? const []) { + if (child is ElementNode) { + if (child.tag == 'code') return true; + if (_hasCodeChild(child)) return true; + } + } + return false; + } +} + +class _CodeAwareTableOfContents extends TableOfContents { + _CodeAwareTableOfContents(super.entries, this._codeEntryIds); + + final Set _codeEntryIds; + + @override + Component build() { + return ul([..._buildToc(entries)]); + } + + Iterable _buildToc(List toc, [int indent = 0]) sync* { + for (final entry in toc) { + final isCode = _codeEntryIds.contains(entry.id); + yield li( + styles: Styles(padding: Padding.only(left: (0.75 * indent).em)), + [ + Builder( + builder: (context) { + final route = RouteState.of(context); + return a( + href: '${route.path}#${entry.id}', + [ + if (isCode) + code([Component.text(entry.text)]) + else + Component.text(entry.text), + ], + ); + }, + ), + ], + ); + if (entry.children.isNotEmpty) { + yield* _buildToc(entry.children, indent + 1); + } + } + } +} diff --git a/site_jaspr/lib/main.client.dart b/site_jaspr/lib/main.client.dart new file mode 100644 index 00000000..224a0fe3 --- /dev/null +++ b/site_jaspr/lib/main.client.dart @@ -0,0 +1,28 @@ +/// The entrypoint for the **client** environment. +/// +/// The [main] method will only be executed on the client when loading the page. +/// To run code on the server during pre-rendering, check the `main.server.dart` file. +library; + +// Client-specific Jaspr import. +import 'package:jaspr/client.dart'; + +// This file is generated automatically by Jaspr, do not remove or edit. +import 'main.client.options.dart'; + +void main() { + // Initializes the client environment with the generated default options. + Jaspr.initializeApp( + options: defaultClientOptions, + ); + + // Starts the app. + // + // [ClientApp] automatically loads and renders all components annotated with @client. + // + // You can wrap this with additional [InheritedComponent]s to share state across multiple + // @client components if needed. + runApp( + const ClientApp(), + ); +} diff --git a/site_jaspr/lib/main.client.options.dart b/site_jaspr/lib/main.client.options.dart new file mode 100644 index 00000000..99f59f61 --- /dev/null +++ b/site_jaspr/lib/main.client.options.dart @@ -0,0 +1,57 @@ +// dart format off +// ignore_for_file: type=lint + +// GENERATED FILE, DO NOT MODIFY +// Generated with jaspr_builder + +import 'package:jaspr/client.dart'; + +import 'package:jaspr_content/components/_internal/code_block_copy_button.dart' + deferred as _code_block_copy_button; +import 'package:jaspr_content/components/_internal/zoomable_image.dart' + deferred as _zoomable_image; +import 'package:jaspr_content/components/sidebar_toggle_button.dart' + deferred as _sidebar_toggle_button; +import 'package:jaspr_content/components/theme_toggle.dart' + deferred as _theme_toggle; + +/// Default [ClientOptions] for use with your Jaspr project. +/// +/// Use this to initialize Jaspr **before** calling [runApp]. +/// +/// Example: +/// ```dart +/// import 'main.client.options.dart'; +/// +/// void main() { +/// Jaspr.initializeApp( +/// options: defaultClientOptions, +/// ); +/// +/// runApp(...); +/// } +/// ``` +ClientOptions get defaultClientOptions => ClientOptions( + clients: { + 'jaspr_content:code_block_copy_button': ClientLoader( + (p) => _code_block_copy_button.CodeBlockCopyButton(), + loader: _code_block_copy_button.loadLibrary, + ), + 'jaspr_content:zoomable_image': ClientLoader( + (p) => _zoomable_image.ZoomableImage( + src: p['src'] as String, + alt: p['alt'] as String?, + caption: p['caption'] as String?, + ), + loader: _zoomable_image.loadLibrary, + ), + 'jaspr_content:sidebar_toggle_button': ClientLoader( + (p) => _sidebar_toggle_button.SidebarToggleButton(), + loader: _sidebar_toggle_button.loadLibrary, + ), + 'jaspr_content:theme_toggle': ClientLoader( + (p) => _theme_toggle.ThemeToggle(), + loader: _theme_toggle.loadLibrary, + ), + }, +); diff --git a/site_jaspr/lib/main.server.dart b/site_jaspr/lib/main.server.dart new file mode 100644 index 00000000..29854754 --- /dev/null +++ b/site_jaspr/lib/main.server.dart @@ -0,0 +1,243 @@ +/// The entrypoint for the **server** environment. +/// +/// The [main] method will only be executed on the server during pre-rendering. +/// To run code on the client, check the `main.client.dart` file. +library; + +import 'dart:io'; + +// Server-specific Jaspr import. +import 'package:jaspr/dom.dart'; +import 'package:jaspr/server.dart'; + +import 'package:jaspr_content/components/header.dart'; +import 'package:jaspr_content/components/image.dart'; +import 'package:jaspr_content/components/sidebar.dart'; + +import 'components/collapsible_sidebar.dart'; +import 'package:jaspr_content/components/theme_toggle.dart'; +import 'package:jaspr_content/jaspr_content.dart'; +import 'package:jaspr_content/theme.dart'; + +import 'components/breadcrumb.dart'; +import 'components/doc_callout.dart'; +import 'extensions/code_aware_toc.dart'; +import 'components/edit_page_link.dart'; +import 'components/homepage_layout.dart'; +import 'components/icon_link.dart'; +import 'components/nav_link.dart'; +import 'components/page_navigation.dart'; +import 'components/safe_code_block.dart'; +import 'components/site_footer.dart'; + +// This file is generated automatically by Jaspr, do not remove or edit. +import 'main.server.options.dart'; + +void main() { + // Initializes the server environment with the generated default options. + Jaspr.initializeApp( + options: defaultServerOptions, + ); + + // Starts the app. + runApp( + ContentApp( + templateEngine: MustacheTemplateEngine(), + parsers: [ + MarkdownParser(), + ], + extensions: [ + HeadingAnchorsExtension(), + TableOfContentsExtension(), + CodeAwareTocPostProcessor(), + ], + components: [ + DocCallout(), + SafeCodeBlock( + grammars: { + 'yaml': File('grammars/yaml.tmLanguage.json').readAsStringSync(), + 'bash': File('grammars/bash.tmLanguage.json').readAsStringSync(), + 'json': File('grammars/json.tmLanguage.json').readAsStringSync(), + }, + ), + Image(zoom: true), + ], + layouts: [ + DocsLayout( + header: Header( + title: 'Very Good Workflows', + logo: '/images/workflows_nav_icon.svg', + items: [ + NavLink(text: 'Get Started', href: '/docs/overview', isButton: true), + NavLink(text: 'VGV Dev Tools', href: 'https://verygood.ventures/dev'), + IconLink( + href: 'https://verygood.ventures', + iconSrc: '/images/vgv_logo_black.svg', + darkIconSrc: '/images/vgv_logo_fill.svg', + alt: 'Very Good Ventures', + ), + IconLink( + href: 'https://github.com/VeryGoodOpenSource/very_good_workflows', + iconSrc: '/images/github.svg', + darkIconSrc: '/images/github_white.svg', + alt: 'GitHub', + ), + ThemeToggle(), + ], + ), + sidebar: CollapsibleSidebar( + // Mobile sidebar header: CTA button + ThemeToggle, matching + // Docusaurus's navbar-sidebar primary row (CTA Β· toggle Β· Γ—). + // Icon links and secondary text links are intentionally omitted + // to keep the header to a single row. + mobileNavItems: [ + a( + href: '/docs/workflows', + classes: 'mobile-workflows-btn', + [ + img( + src: '/images/workflows_nav_icon.svg', + alt: 'Workflows', + attributes: {'height': '32', 'width': '105'}, + ), + ], + ), + ThemeToggle(), + ], + // Primary panel: global nav items shown when "← Back to main menu" + // is tapped β€” mirrors Docusaurus's primary sidebar panel. + primaryNavItems: [ + a( + href: '/docs/overview', + classes: 'sidebar-link', + [Component.text('Get Started')], + ), + a( + href: 'https://verygood.ventures/dev', + target: Target.blank, + classes: 'sidebar-link', + [Component.text('VGV Dev Tools')], + ), + a( + href: 'https://verygood.ventures', + target: Target.blank, + classes: 'sidebar-link', + [ + img( + classes: 'sidebar-icon-light', + src: '/images/vgv_logo_black.svg', + alt: 'Very Good Ventures', + attributes: {'width': '24', 'height': '24'}, + ), + img( + classes: 'sidebar-icon-dark', + src: '/images/vgv_logo_fill.svg', + alt: 'Very Good Ventures', + attributes: {'width': '24', 'height': '24'}, + ), + ], + ), + a( + href: 'https://github.com/VeryGoodOpenSource/very_good_workflows', + target: Target.blank, + classes: 'sidebar-link', + [ + img( + classes: 'sidebar-icon-light', + src: '/images/github.svg', + alt: 'GitHub', + attributes: {'width': '24', 'height': '24'}, + ), + img( + classes: 'sidebar-icon-dark', + src: '/images/github_white.svg', + alt: 'GitHub', + attributes: {'width': '24', 'height': '24'}, + ), + ], + ), + ], + items: [ + SidebarEntry(text: 'Overview', href: '/docs/overview'), + SidebarEntry( + text: 'Workflows', + href: '/docs/workflows', + children: [ + SidebarLink(text: 'Dart Package', href: '/docs/workflows/dart_package'), + SidebarLink(text: 'Dart Pub Publish', href: '/docs/workflows/dart_pub_publish'), + SidebarLink(text: 'Flutter Package', href: '/docs/workflows/flutter_package'), + SidebarLink(text: 'Flutter Pub Publish', href: '/docs/workflows/flutter_pub_publish'), + SidebarLink(text: 'License Check', href: '/docs/workflows/license_check'), + SidebarLink(text: 'Mason Publish', href: '/docs/workflows/mason_publish'), + SidebarLink(text: 'Pana', href: '/docs/workflows/pana'), + SidebarLink(text: 'Semantic Pull Request', href: '/docs/workflows/semantic_pull_request'), + SidebarLink(text: 'Spell Check', href: '/docs/workflows/spell_check'), + ], + ), + ], + ), + footer: Component.fragment([ + const Breadcrumb(), + const EditPageLink(), + const PageNavigation(), + const SiteFooter(), + ]), + ), + const HomepageLayout(), + ], + theme: ContentTheme.raw( + colors: [ + // Exact Docusaurus/Infima color values from site/src/css/custom.css. + ColorToken('primary', Color('#2a48df'), dark: Color('#66fbd1')), + ColorToken('background', Color('#fbfcff'), dark: Color('#020f30')), + ColorToken('text', Color('#1c1e21'), dark: Color('#e3e3e3')), + ColorToken('content-headings', Color('#1c1e21'), dark: Color('#ffffff')), + ColorToken('content-lead', Color('#606770'), dark: Color('#a0a0a0')), + ColorToken('content-links', Color('#2a48df'), dark: Color('#66fbd1')), + ColorToken('content-bold', Color('#1c1e21'), dark: Color('#ffffff')), + ColorToken('content-counters', Color('#606770'), dark: Color('#a0a0a0')), + ColorToken('content-bullets', Color('#dadde1'), dark: Color('#444950')), + ColorToken('content-hr', Color('#dadde1'), dark: Color('#444950')), + ColorToken('content-quotes', Color('#606770'), dark: Color('#a0a0a0')), + ColorToken( + 'content-quote-borders', + Color('#2a48df'), + dark: Color('#66fbd1'), + ), + ColorToken('content-captions', Color('#606770'), dark: Color('#a0a0a0')), + ColorToken('content-kbd', Color('#1c1e21'), dark: Color('#ffffff')), + ColorToken('content-kbd-shadows', Color('#1c1e21'), dark: Color('#ffffff')), + ColorToken('content-code', Color('#1c1e21'), dark: Color('#ffffff')), + ColorToken( + 'content-pre-code', + Color('#393a34'), + dark: Color('#e3e3e3'), + ), + ColorToken( + 'content-pre-bg', + Color('#ffffff'), + dark: Color('rgb(0 0 0 / 50%)'), + ), + ColorToken('content-th-borders', Color('#dadde1'), dark: Color('#444950')), + ColorToken('content-td-borders', Color('#dadde1'), dark: Color('#444950')), + // UI component tokens (non-content). + ColorToken('navbar-bg', Color('#fbfcff'), dark: Color('#081842')), + ColorToken( + 'primary-hover', + Color('#1e38b0'), + dark: Color('#44fac7'), + ), + ColorToken('border', Color('#dadde1'), dark: Color('#444950')), + ColorToken( + 'secondary-text', + Color('#606770'), + dark: Color('#a0a0a0'), + ), + ColorToken('surface', Color('#ffffff'), dark: Color('#1e1e1e')), + ], + typography: ContentTypography.base, + reset: true, + ), + ), + ); +} diff --git a/site_jaspr/lib/main.server.options.dart b/site_jaspr/lib/main.server.options.dart new file mode 100644 index 00000000..dc47b17f --- /dev/null +++ b/site_jaspr/lib/main.server.options.dart @@ -0,0 +1,82 @@ +// dart format off +// ignore_for_file: type=lint + +// GENERATED FILE, DO NOT MODIFY +// Generated with jaspr_builder + +import 'package:jaspr/server.dart'; +import 'package:jaspr_content/components/_internal/code_block_copy_button.dart' + as _code_block_copy_button; +import 'package:jaspr_content/components/_internal/zoomable_image.dart' + as _zoomable_image; +import 'package:jaspr_content/components/code_block.dart' as _code_block; +import 'package:jaspr_content/components/image.dart' as _image; +import 'package:jaspr_content/components/sidebar_toggle_button.dart' + as _sidebar_toggle_button; +import 'package:jaspr_content/components/theme_toggle.dart' as _theme_toggle; +import 'package:site_jaspr/components/breadcrumb.dart' as _breadcrumb; +import 'package:site_jaspr/components/collapsible_sidebar.dart' + as _collapsible_sidebar; +import 'package:site_jaspr/components/doc_callout.dart' as _doc_callout; +import 'package:site_jaspr/components/edit_page_link.dart' as _edit_page_link; +import 'package:site_jaspr/components/icon_link.dart' as _icon_link; +import 'package:site_jaspr/components/nav_link.dart' as _nav_link; +import 'package:site_jaspr/components/page_navigation.dart' as _page_navigation; +import 'package:site_jaspr/components/safe_code_block.dart' as _safe_code_block; +import 'package:site_jaspr/components/site_footer.dart' as _site_footer; + +/// Default [ServerOptions] for use with your Jaspr project. +/// +/// Use this to initialize Jaspr **before** calling [runApp]. +/// +/// Example: +/// ```dart +/// import 'main.server.options.dart'; +/// +/// void main() { +/// Jaspr.initializeApp( +/// options: defaultServerOptions, +/// ); +/// +/// runApp(...); +/// } +/// ``` +ServerOptions get defaultServerOptions => ServerOptions( + clientId: 'main.client.dart.js', + clients: { + _code_block_copy_button.CodeBlockCopyButton: + ClientTarget<_code_block_copy_button.CodeBlockCopyButton>( + 'jaspr_content:code_block_copy_button', + ), + _zoomable_image.ZoomableImage: ClientTarget<_zoomable_image.ZoomableImage>( + 'jaspr_content:zoomable_image', + params: __zoomable_imageZoomableImage, + ), + _sidebar_toggle_button.SidebarToggleButton: + ClientTarget<_sidebar_toggle_button.SidebarToggleButton>( + 'jaspr_content:sidebar_toggle_button', + ), + _theme_toggle.ThemeToggle: ClientTarget<_theme_toggle.ThemeToggle>( + 'jaspr_content:theme_toggle', + ), + }, + styles: () => [ + ..._zoomable_image.ZoomableImage.styles, + ..._code_block.CodeBlock.styles, + ..._image.Image.styles, + ..._theme_toggle.ThemeToggleState.styles, + ..._breadcrumb.Breadcrumb.styles, + ..._collapsible_sidebar.CollapsibleSidebar.styles, + ..._doc_callout.DocCallout.styles, + ..._edit_page_link.EditPageLink.styles, + ..._icon_link.IconLink.styles, + ..._nav_link.NavLink.styles, + ..._page_navigation.PageNavigation.styles, + ..._safe_code_block.SafeCodeBlock.styles, + ..._site_footer.SiteFooter.styles, + ], +); + +Map __zoomable_imageZoomableImage( + _zoomable_image.ZoomableImage c, +) => {'src': c.src, 'alt': c.alt, 'caption': c.caption}; diff --git a/site_jaspr/lib/styles/site_styles.dart b/site_jaspr/lib/styles/site_styles.dart new file mode 100644 index 00000000..f223013b --- /dev/null +++ b/site_jaspr/lib/styles/site_styles.dart @@ -0,0 +1,759 @@ +import 'package:jaspr/dom.dart'; + +/// Comprehensive site styles matching the original Docusaurus/Infima design. +/// +/// This file provides ALL visual overrides in one place, replacing the scattered +/// CSS rules that were previously in `_fontStyles` in `site_footer.dart`. +/// +/// Color reference (from `site/src/css/custom.css`): +/// +/// | Variable | Light | Dark | +/// |-----------------------|-----------|-----------| +/// | primary | #2a48df | #66fbd1 | +/// | primary-dark (hover) | #1f3ccf | #44fac7 | +/// | background | #fbfcff | #020f30 | +/// | navbar-bg | #fbfcff | #081842 | +/// | code-bg (inline dark) | β€” | #081842 | + +/// Global site styles injected into `` via [Document.head]. +List get siteStyles => [ + // ─────────────────────────────────────────────────────────────────────── + // 1. FONT & BASE + // ─────────────────────────────────────────────────────────────────────── + css(':root').styles( + raw: { + '--content-font': + "'Poppins', ui-sans-serif, system-ui, sans-serif, " + "'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', " + "'Noto Color Emoji'", + // Infima monospace font stack + '--ifm-font-family-monospace': + "SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', " + "'Courier New', monospace", + }, + ), + css('html, body').styles( + raw: { + 'font-family': 'var(--content-font)', + 'line-height': '1.65', + }, + ), + + // ─────────────────────────────────────────────────────────────────────── + // 2. TYPOGRAPHY OVERRIDES + // Match Docusaurus/Infima heading sizes and spacing exactly. + // jaspr_content's ContentTypography uses smaller headings (h1=2.25em, + // h2=1.5em) and different line-height (1.75). Override to match Infima. + // ─────────────────────────────────────────────────────────────────────── + // Heading sizes: Infima uses h1=3rem, h2=2rem, h3=1.5rem, h4=1.25rem + // Heading anchors: framework uses display:flex on headings which makes + // the # anchor a block flex-child on a new line. Force inline so it + // doesn't add extra height. + css('.content :is(h1, h2, h3, h4, h5, h6)[anchor] > a').styles( + raw: {'position': 'absolute'}, + ), + css('.content h1').styles( + fontSize: 3.rem, + fontWeight: FontWeight.w700, + lineHeight: 1.25.em, + raw: {'margin': '0 0 1.5625rem'}, + ), + css('.content h2').styles( + fontSize: 2.rem, + fontWeight: FontWeight.w700, + raw: {'margin': '2.5rem 0 1.25rem', 'line-height': '1.25'}, + ), + css('.content h3').styles( + fontSize: 1.5.rem, + fontWeight: FontWeight.w700, + raw: {'margin': '1.875rem 0 1.25rem', 'line-height': '1.25'}, + ), + css('.content h4').styles( + fontSize: 1.25.rem, + lineHeight: 1.5.em, + raw: {'margin': '1.5rem 0 0.5rem'}, + ), + // Content line-height: Infima uses 1.65 (not jaspr_content's 1.75) + css('.content').styles(raw: {'line-height': '1.65'}), + // Paragraphs: Docusaurus uses bottom-only margin (1.25rem); Jaspr default is top+bottom + css('.content p').styles(raw: {'margin': '0 0 1.25rem'}), + // Lists: match Docusaurus/Infima spacing (bottom-only margin, left padding) + css('.content ul, .content ol').styles( + margin: Margin.only(top: Unit.zero, bottom: 1.25.rem), + raw: {'padding-left': '2rem'}, + ), + css('.content li').styles( + padding: Padding.zero, + margin: Margin.zero, + ), + // Remove backtick pseudo-elements and quote marks (Docusaurus doesn't have these) + css('.content code::before').styles(raw: {'content': 'none'}), + css('.content code::after').styles(raw: {'content': 'none'}), + css('.content blockquote p:first-of-type::before').styles( + raw: {'content': 'none'}, + ), + css('.content blockquote p:last-of-type::after').styles( + raw: {'content': 'none'}, + ), + // Inline code: light gray background, smaller font (matching Infima) + // Use :not(pre) > code to avoid styling code inside pre blocks + css('.content :not(pre) > code').styles( + padding: Padding.all(0.1.rem), + border: Border.all(color: Color('rgba(0, 0, 0, 0.1)'), width: 1.px), + radius: BorderRadius.circular(0.4.rem), + fontSize: Unit.percent(95), + raw: { + 'background-color': '#f6f7f8', + 'font-family': 'var(--ifm-font-family-monospace)', + 'font-weight': 'inherit', + }, + ), + // Pre/code blocks: match Docusaurus sizing (15.2px font, 22px line-height) + // Docusaurus: pre has no padding/margin, code has 16px padding. + // Jaspr base: pre has padding+margin from ContentTypography. Override to match. + css('.content pre').styles( + padding: Padding.zero, + fontSize: 0.95.rem, + raw: { + 'font-family': 'var(--ifm-font-family-monospace)', + 'line-height': '1.45', + 'margin': '0 0 1.25rem', + }, + ), + css('.content pre code').styles( + padding: Padding.all(1.rem), + fontSize: Unit.percent(100), + raw: { + 'font-family': 'var(--ifm-font-family-monospace)', + 'background': 'transparent', + 'border-radius': '0', + 'display': 'block', + 'line-height': 'inherit', + }, + ), + // Code block copy button: match Docusaurus style. + // Framework sets opacity:0 and position:absolute AFTER our styles, + // so we use !important to override. + css('.code-block button').styles( + display: Display.flex, + padding: Padding.all(0.25.rem), + border: Border.all(color: Color('var(--border)'), width: 1.px), + radius: BorderRadius.circular(0.4.rem), + justifyContent: JustifyContent.center, + alignItems: AlignItems.center, + backgroundColor: Color('var(--surface)'), + raw: { + 'position': 'absolute !important', + 'top': '0.5rem', + 'right': '0.5rem', + 'width': 'auto !important', + 'height': 'auto !important', + 'opacity': '0 !important', + 'color': 'var(--secondary-text) !important', + 'cursor': 'pointer', + 'transition': 'opacity 0.2s, color 0.2s', + }, + ), + css('.code-block button svg').styles( + raw: {'width': '18px !important', 'height': '18px !important'}, + ), + css('.code-block:hover button').styles( + raw: {'opacity': '0.5 !important'}, + ), + css('.code-block button:hover').styles( + raw: {'opacity': '1 !important', 'color': 'var(--text) !important'}, + ), + // Green check icon after successful copy (CheckIcon has no , CopyIcon does). + // Needs both light and dark mode rules to beat specificity of dark hover rule. + css('.code-block button:not(:has(svg rect))').styles( + raw: {'color': '#00a86b !important', 'opacity': '1 !important'}, + ), + css('.code-block button:not(:has(svg rect)):hover').styles( + raw: {'color': '#00a86b !important'}, + ), + css('[data-theme="dark"] .code-block button:not(:has(svg rect))').styles( + raw: {'color': '#00a86b !important', 'opacity': '1 !important'}, + ), + css('[data-theme="dark"] .code-block button:not(:has(svg rect)):hover').styles( + raw: {'color': '#00a86b !important'}, + ), + // Content links: primary color, no underline by default (matching original) + css('.content a:not(.workflow-card):not(.page-nav-prev):not(.page-nav-next)').styles( + textDecoration: TextDecoration.none, + ), + css('.content a:not(.workflow-card):not(.page-nav-prev):not(.page-nav-next):hover').styles( + textDecoration: TextDecoration(line: TextDecorationLine.underline), + ), + // Blockquote: no italic, normal weight (matching Infima defaults) + css('.content blockquote').styles( + fontWeight: FontWeight.normal, + fontStyle: FontStyle.normal, + ), + + // ─────────────────────────────────────────────────────────────────────── + // 3. HEADER / NAVBAR + // Docusaurus navbar: 60px height (3.75rem), padding 8px 16px + // ─────────────────────────────────────────────────────────────────────── + // Opaque header background for both modes + css('.docs .header-container').styles( + backgroundColor: Color('var(--navbar-bg)'), + raw: {'z-index': '20'}, + ), + // Match Docusaurus navbar: 60px height, box-shadow (not border-bottom) + css('.header').styles( + height: 3.75.rem, + padding: Padding.symmetric(horizontal: 1.rem, vertical: 0.5.rem), + border: Border.none, + raw: {'box-shadow': 'rgba(0, 0, 0, 0.1) 0px 1px 2px 0px'}, + ), + // Hide the title text in the header (original only shows the logo badge) + css('.header-title > span').styles(display: Display.none), + // Hide the duplicate title, description, and image rendered from frontmatter + css('.content-header').styles(display: Display.none), + // Larger logo badge in the header (matching Docusaurus 32px) + css('.header .header-title img').styles(height: 2.rem), + // Vertically center nav links with icons in the header + css('.header .header-items').styles(alignItems: AlignItems.center), + // Hamburger menu button: match Docusaurus 30Γ—30 SVG (jaspr_content default is 20Γ—20) + css('.sidebar-toggle-button svg').styles( + width: 30.px, + height: 30.px, + ), + // Hide header nav items on narrow viewports + css.media(MediaQuery.all(maxWidth: 1000.px), [ + css('.header .header-items').styles(display: Display.none), + ]), + + // ─────────────────────────────────────────────────────────────────────── + // 4. DARK MODE ICON SWITCHING + // Show .icon-light in light mode, .icon-dark in dark mode. + // .sidebar-icon-* are used in sidebar primary-panel icon links. + // ─────────────────────────────────────────────────────────────────────── + css('[data-theme="dark"] .icon-link .icon-light').styles( + display: Display.none, + ), + css('[data-theme="dark"] .icon-link .icon-dark').styles( + raw: {'display': 'flex !important'}, + ), + // Sidebar primary-panel icon switching (sidebar-icon-light / sidebar-icon-dark) + css('.sidebar-icon-dark').styles(display: Display.none), + css('[data-theme="dark"] .sidebar-icon-light').styles(display: Display.none), + css('[data-theme="dark"] .sidebar-icon-dark').styles( + raw: {'display': 'inline !important'}, + ), + + // ─────────────────────────────────────────────────────────────────────── + // 5. DARK MODE "GET STARTED" BUTTON + // ─────────────────────────────────────────────────────────────────────── + css('[data-theme="dark"] .nav-button').styles( + color: Color('var(--background)'), + ), + // Nav links (e.g. "VGV Dev Tools"): lighter weight, slightly smaller + css('.nav-link:not(.nav-button)').styles( + fontSize: 0.975.rem, + fontWeight: FontWeight.w400, + ), + // Dark mode: nav links should be white + css('[data-theme="dark"] .nav-link:not(.nav-button)').styles(color: Colors.white), + // Dark mode mobile: "Get Started" becomes a plain transparent link, matching + // Docusaurus custom.css html[data-theme='dark'] @media (max-width:996px) rule. + css.media(MediaQuery.all(maxWidth: 996.px), [ + css('[data-theme="dark"] .nav-button').styles( + raw: {'background-color': 'transparent', 'color': '#ffffff'}, + ), + ]), + + // ─────────────────────────────────────────────────────────────────────── + // 5b. BREADCRUMB COLORS + // ─────────────────────────────────────────────────────────────────────── + // Light mode: breadcrumb links match Docusaurus body text (#1c1e21) + css('.breadcrumb-link').styles(color: Color('var(--text)')), + // Dark mode: separator needs explicit override (--secondary-text dark β‰  #e3e3e3) + css('[data-theme="dark"] .breadcrumb-sep').styles( + color: Color('var(--text)'), + ), + + // ─────────────────────────────────────────────────────────────────────── + // 6. THEME TOGGLE + // Reverse the icons: show current-mode icon instead of target-mode. + // Light mode: hide moon (first span), show sun (last span). + // Dark mode: show moon (first span), hide sun (last span). + // ─────────────────────────────────────────────────────────────────────── + css('.theme-toggle').styles( + padding: Padding.zero, + justifyContent: JustifyContent.center, + alignItems: AlignItems.center, + raw: { + 'width': '32px', + 'height': '32px', + 'border-radius': '50% !important', + 'box-sizing': 'border-box', + 'cursor': 'pointer', + }, + ), + css('.theme-toggle:hover').styles( + backgroundColor: Color('rgba(0, 0, 0, 0.1)'), + ), + css('[data-theme="dark"] .theme-toggle:hover').styles( + backgroundColor: Color('rgba(255, 255, 255, 0.1)'), + ), + css('.theme-toggle svg').styles( + raw: {'width': '24px', 'height': '24px'}, + ), + // Reverse icons: show current state (sun in light, moon in dark). + // Framework default: moon in light, sun in dark. + css('[data-theme="light"] .theme-toggle > span:first-child').styles( + raw: {'display': 'none !important'}, + ), + css('[data-theme="light"] .theme-toggle > span:last-child').styles( + raw: {'display': 'inline !important'}, + ), + css('[data-theme="dark"] .theme-toggle > span:first-child').styles( + raw: {'display': 'inline !important'}, + ), + css('[data-theme="dark"] .theme-toggle > span:last-child').styles( + raw: {'display': 'none !important'}, + ), + + // ─────────────────────────────────────────────────────────────────────── + // 7. DOCS LAYOUT SPACING & BREADCRUMB + // Docusaurus: sticky navbar 60px + 16px gap = content at 76px. + // Jaspr default: fixed header 60px + main padding-top 4rem + div + // padding-top 2rem = 96px. Reduce to match Docusaurus. + // ─────────────────────────────────────────────────────────────────────── + // Reduce main padding-top to exactly header height + css('.docs .main-container main').styles( + padding: Padding.only(top: 3.75.rem), + ), + // Reduce inner div padding to match Docusaurus 16px gap + // max-width 1320px matches Docusaurus .container constraint + css('.docs .main-container main > div').styles( + padding: Padding.only(top: 1.rem, left: 1.rem, right: 1.rem), + raw: {'max-width': '1320px', 'margin': '0 auto'}, + ), + // Breadcrumb: position above content using flexbox order + css('.content-container').styles( + display: Display.flex, + flexDirection: FlexDirection.column, + ), + css('.content-footer').styles(raw: {'display': 'contents'}), + css('.breadcrumb').styles(raw: {'order': '-1'}), + // Breadcrumb: match Docusaurus sizing (12.8px font, link padding, bottom margin) + css('.docs .breadcrumb').styles( + padding: Padding.zero, + margin: Margin.only(bottom: 12.8.px), + fontSize: 12.8.px, + ), + css('.docs .breadcrumb-link, .docs .breadcrumb-current').styles( + padding: Padding.symmetric(vertical: 5.12.px, horizontal: 10.24.px), + ), + + // ─────────────────────────────────────────────────────────────────────── + // 8. SIDEBAR β€” layout/sizing only. + // Link/hover/active styles live in CollapsibleSidebar.styles using + // var(--primary) (theme-aware). Width matches Docusaurus 300px. + // ─────────────────────────────────────────────────────────────────────── + // Sidebar width: match Docusaurus 300px (--doc-sidebar-width) + // Use high specificity to override DocsLayout's .docs .main-container .sidebar-container + css('.docs .main-container .sidebar-container').styles( + width: 300.px, + padding: Padding.only(bottom: 8.rem), + raw: { + 'overflow-y': 'auto', + 'overflow-x': 'hidden', + 'scrollbar-width': 'none', + '-ms-overflow-style': 'none', + }, + ), + css('.docs .main-container .sidebar-container::-webkit-scrollbar').styles( + display: Display.none, + ), + css('.docs .sidebar').styles(width: 300.px, raw: {'overflow-x': 'hidden'}), + // Sidebar border-right: match Docusaurus 1px solid border + css('.docs .sidebar-container').styles( + border: Border.only( + right: BorderSide(width: 1.px, color: Color('var(--border)')), + ), + ), + // Dark mode sidebar links: Docusaurus uses #dadde1 + css('[data-theme="dark"] .docs .sidebar a').styles( + color: Color('#dadde1'), + ), + // Adjust main padding-left to match wider sidebar (desktop only) + css.media(MediaQuery.all(minWidth: 1024.px), [ + css('.docs .main-container main').styles( + padding: Padding.only(left: 300.px), + ), + ]), + // Desktop: align sidebar top with bottom of 3.75rem header. + // DocsLayout defaults to top: 4rem β€” override to close the 4px gap. + css.media(MediaQuery.all(minWidth: 1024.px), [ + css('.docs .main-container .sidebar-container').styles( + raw: {'top': '3.75rem'}, + ), + ]), + // Mobile: 83 vw width (matches Docusaurus --ifm-navbar-sidebar-width) + z-index + // above the fixed header (z-index 10) so the panel slides over the navbar. + // .sidebar must also be 100% so nav items fill the panel (not the 300px + // desktop value which would leave dead space on wider phones). + css.media(MediaQuery.all(maxWidth: 1023.px), [ + css('.docs .main-container .sidebar-container').styles( + zIndex: ZIndex(200), + raw: {'width': '83vw'}, + ), + css('.docs .sidebar').styles(width: Unit.percent(100)), + ]), + // Mobile sidebar: Workflows image-button (the SVG pill used in the header). + css('.mobile-workflows-btn').styles( + display: Display.flex, + alignItems: AlignItems.center, + textDecoration: TextDecoration.none, + ), + css('.mobile-workflows-btn:hover').styles(opacity: 0.8), + + // Dark mode mobile: sidebar panel uses the navbar background (#081842), + // matching Docusaurus where --ifm-navbar-background-color is applied to + // the mobile overlay instead of the page background (#020f30). + css.media(MediaQuery.all(maxWidth: 1023.px), [ + css('[data-theme="dark"] .docs .main-container .sidebar-container').styles( + backgroundColor: Color('var(--navbar-bg)'), + ), + ]), + // Fix DocsLayout's sidebar-barrier: + // β€’ position:fixed so it covers the full viewport (including the header). + // β€’ background:#000 so that at DocsLayout's opacity:0.5 it reads as + // rgba(0,0,0,0.5) β€” the standard Docusaurus dark scrim. + css('.docs .main-container .sidebar-barrier').styles( + raw: {'position': 'fixed', 'inset': '0', 'background': '#000'}, + ), + + // ─────────────────────────────────────────────────────────────────────── + // 9. CONTENT AREA LINKS + // Primary color, no underline; underline on hover (matching original). + // ─────────────────────────────────────────────────────────────────────── + css('.content-container a:not(.breadcrumb-link):not(.workflow-card):not(.page-nav-prev):not(.page-nav-next)').styles( + color: Color('var(--content-links)'), + textDecoration: TextDecoration.none, + ), + css( + '.content-container a:not(.breadcrumb-link):not(.workflow-card):not(.page-nav-prev):not(.page-nav-next):hover', + ).styles( + textDecoration: TextDecoration(line: TextDecorationLine.underline), + ), + // Callout links: inherit foreground color + always underlined (Infima alert behavior). + // Overrides the blue `.content-container a` rule above via cascade order. + css('.content-container .doc-callout a').styles( + color: Color.inherit, + fontWeight: FontWeight.w400, + textDecoration: TextDecoration(line: TextDecorationLine.underline), + ), + css('.content-container .doc-callout a:hover').styles( + raw: {'text-decoration-thickness': '2px'}, + ), + // Underline color matches the left border color per type. + css('.content-container .doc-callout-info a').styles( + raw: {'text-decoration-color': '#54c7ec'}, + ), + css('.content-container .doc-callout-warning a').styles( + raw: {'text-decoration-color': '#e6a700'}, + ), + css('.content-container .doc-callout-error a').styles( + raw: {'text-decoration-color': '#fa5252'}, + ), + css('.content-container .doc-callout-success a').styles( + raw: {'text-decoration-color': '#00a400'}, + ), + + // ─────────────────────────────────────────────────────────────────────── + // 10. DARK MODE: CODE BLOCKS & SCROLLBARS + // ─────────────────────────────────────────────────────────────────────── + // Dark mode: all code (inline + blocks) uses navy background + // Matches Docusaurus custom.css: html[data-theme='dark'] code { background: #081842 } + css('[data-theme="dark"] code').styles( + raw: {'background': '#081842 !important'}, + ), + // Dark mode inline code: match Docusaurus border and text color + css('[data-theme="dark"] .content :not(pre) > code').styles( + border: Border.all(color: Color('rgba(0, 0, 0, 0.1)'), width: 1.px), + color: Color('#e3e3e3'), + ), + css('[data-theme="dark"]').styles( + raw: {'scrollbar-color': '#ffffff30 transparent'}, + ), + // Dark mode headings: Docusaurus uses #e3e3e3, not pure white + css('[data-theme="dark"] .content h1, [data-theme="dark"] .content h2, [data-theme="dark"] .content h3').styles( + color: Color('#e3e3e3'), + ), + + // ─────────────────────────────────────────────────────────────────────── + // 11. FOOTER & PAGE NAVIGATION SPACING + // Docusaurus: footer margin-top 64px, pagination margin-top 48px. + // ─────────────────────────────────────────────────────────────────────── + css('.site-footer').styles(margin: Margin.only(top: 4.rem)), + // page-nav spacing is set in PageNavigation.styles (48px margin-top, zero padding) + // Dark mode: page nav prev/next blocks + css('[data-theme="dark"] .page-nav-prev, [data-theme="dark"] .page-nav-next').styles( + border: Border.all(color: Color('#606770'), width: 1.px), + ), + css('[data-theme="dark"] .page-nav-label').styles( + color: Colors.white, + ), + + // ─────────────────────────────────────────────────────────────────────── + // 12. TABLE OF CONTENTS & CONTENT WIDTH + // Docusaurus: 213px TOC, 12.8px font, 703px content width. + // Jaspr default: 272px TOC, extra padding β†’ only 588px content. + // Fix TOC width and reduce padding to match Docusaurus layout. + // ─────────────────────────────────────────────────────────────────────── + // TOC: hide "On this page" heading (Docusaurus doesn't show it) + css('.docs .main-container main > div aside.toc h3').styles( + display: Display.none, + ), + // Show TOC at >=1000px (DocsLayout default is 1280px) + css.media(MediaQuery.all(minWidth: 1000.px), [ + css('.docs .main-container main > div aside.toc').styles( + display: Display.block, + ), + ]), + // TOC: match Docusaurus styling (left border, link colors, sub-item pills) + css('.docs .main-container main > div aside.toc').styles( + raw: {'min-width': '140px', 'flex': '0 1 25%'}, + ), + css('.docs .main-container main > div aside.toc > div').styles( + padding: Padding.only(left: 0.5.rem), + border: Border.only( + left: BorderSide(width: 1.px, color: Color('var(--border)')), + ), + ), + css('.docs .main-container main > div aside.toc li').styles( + fontSize: 12.8.px, + ), + // TOC links: gray by default, blue when active (matching Docusaurus) + css('.toc a').styles( + color: Color('var(--secondary-text)'), + textDecoration: TextDecoration.none, + ), + css('.toc a:hover').styles( + color: Color('var(--primary)'), + ), + css('.toc a.toc-active').styles( + color: Color('var(--primary)'), + fontWeight: FontWeight.w500, + ), + // TOC code sub-items (backtick headings): rounded pill border + css('.toc a code').styles( + padding: Padding.symmetric(horizontal: 0.5.rem, vertical: 0.125.rem), + border: Border.all(color: Color('var(--border)'), width: 1.px), + radius: BorderRadius.circular(12.px), + fontSize: 11.px, + raw: {'font-family': 'inherit'}, + ), + // Dark mode TOC: override link color (--secondary-text dark is #a0a0a0, need white) + css('[data-theme="dark"] .toc a').styles( + color: Colors.white, + ), + // ─────────────────────────────────────────────────────────────────────── + // 12b. MOBILE TOC (collapsible "On this page" above content < 1000px) + // ─────────────────────────────────────────────────────────────────────── + css('.mobile-toc', [ + css('&').styles( + margin: Margin.only(bottom: 1.rem), + radius: BorderRadius.circular(0.4.rem), + raw: { + 'background': 'rgba(0, 0, 0, 0.05)', + }, + ), + ]), + // Hide mobile TOC at wide viewports where sidebar TOC is visible + css.media(MediaQuery.all(minWidth: 1000.px), [ + css('.mobile-toc').styles(display: Display.none), + ]), + css('.mobile-toc-toggle', [ + css('&').styles( + display: Display.flex, + width: Unit.percent(100), + padding: Padding.symmetric(horizontal: 1.rem, vertical: 0.5.rem), + cursor: Cursor.pointer, + fontSize: 0.875.rem, + fontWeight: FontWeight.w700, + raw: { + 'align-items': 'center', + 'justify-content': 'space-between', + 'background': 'none', + 'border': 'none', + 'color': 'inherit', + 'font-family': 'inherit', + }, + ), + css('svg').styles( + opacity: 0.5, + raw: { + 'transition': 'transform 0.2s ease', + }, + ), + ]), + css('.mobile-toc.expanded .mobile-toc-toggle svg').styles( + raw: {'transform': 'rotate(180deg)'}, + ), + css('.mobile-toc-content', [ + css('&').styles( + overflow: Overflow.hidden, + raw: { + 'max-height': '0', + 'transition': 'max-height 0.3s ease', + }, + ), + css('ul').styles( + padding: Padding.only( + left: 1.rem, + right: 1.rem, + bottom: 0.75.rem, + ), + margin: Margin.zero, + listStyle: ListStyle.none, + ), + css('li').styles( + padding: Padding.symmetric(vertical: 0.25.rem), + ), + ]), + // Mobile TOC code sub-items: pill border (same as sidebar TOC) + css('.mobile-toc .mobile-toc-content a code').styles( + padding: Padding.symmetric(horizontal: 0.5.rem, vertical: 0.125.rem), + border: Border.all(color: Color('var(--border)'), width: 1.px), + radius: BorderRadius.circular(12.px), + fontSize: 11.px, + raw: {'font-family': 'inherit'}, + ), + // Mobile TOC links: use higher specificity to override .content-container a + css('.mobile-toc .mobile-toc-content a').styles( + color: Color('var(--text)'), + fontSize: 0.8125.rem, + textDecoration: TextDecoration.none, + ), + css('.mobile-toc .mobile-toc-content a:hover').styles( + color: Color('var(--primary)'), + ), + css('.mobile-toc.expanded .mobile-toc-content').styles( + raw: {'max-height': '500px'}, + ), + // Dark mode mobile TOC + css('[data-theme="dark"] .mobile-toc').styles( + raw: { + 'background': 'rgba(255, 255, 255, 0.05)', + }, + ), + + // ─────────────────────────────────────────────────────────────────────── + // 12c. TABLES (matching Docusaurus/Infima table styling) + // ─────────────────────────────────────────────────────────────────────── + css('.content-container table').styles( + width: Unit.percent(100), + raw: {'display': 'table', 'border-collapse': 'collapse', 'margin-bottom': '1.25rem'}, + ), + css('.content-container th, .content-container td').styles( + padding: Padding.all(0.75.rem), + border: Border.all(color: Color('var(--border)'), width: 1.px), + ), + css('.content-container thead tr').styles( + raw: {'background': '#f6f7f8'}, + ), + css('.content-container th').styles( + fontWeight: FontWeight.w700, + ), + css('.content-container tr:nth-child(2n)').styles( + raw: {'background': '#fbfcfd'}, + ), + // Dark mode tables + css('[data-theme="dark"] .content-container thead tr').styles( + raw: {'background': 'rgba(255, 255, 255, 0.05)'}, + ), + css('[data-theme="dark"] .content-container tr:nth-child(2n)').styles( + raw: {'background': 'rgba(255, 255, 255, 0.02)'}, + ), + + // ─────────────────────────────────────────────────────────────────────── + // 13. WORKFLOW CARDS + // Docusaurus category page: 2-column card grid with icon, title, + // and truncated description. Matches /docs/category/workflows. + // ─────────────────────────────────────────────────────────────────────── + css('.workflow-cards', [ + css('&').styles( + display: Display.grid, + gap: Gap(column: 2.rem, row: 2.rem), + raw: {'grid-template-columns': '1fr', 'margin': '1.5rem 0'}, + ), + ]), + css.media(MediaQuery.all(minWidth: 1024.px), [ + css('.workflow-cards').styles( + raw: {'grid-template-columns': 'repeat(2, 1fr)'}, + ), + ]), + css('.workflow-card', [ + css('&').styles( + display: Display.block, + padding: Padding.all(2.rem), + radius: BorderRadius.circular(0.8.rem), + textDecoration: TextDecoration.none, + raw: { + 'border': '1px solid #ebedf0', + 'background': '#fbfcff', + 'color': 'inherit', + 'transition': 'border-color 0.2s ease', + 'box-shadow': 'rgba(0, 0, 0, 0.15) 0px 1.5px 3px 0px', + }, + ), + css('&:hover').styles( + textDecoration: TextDecoration.none, + raw: {'border-color': 'var(--primary)'}, + ), + css('h3').styles( + margin: Margin.only(bottom: 1.rem), + fontSize: 1.2.rem, + fontWeight: FontWeight.w700, + textDecoration: TextDecoration.none, + raw: {'margin-top': '0'}, + ), + css('p').styles( + margin: Margin.zero, + overflow: Overflow.hidden, + color: Color('#444950'), + fontSize: 0.8.rem, + textDecoration: TextDecoration.none, + raw: { + 'display': '-webkit-box', + '-webkit-line-clamp': '1', + '-webkit-box-orient': 'vertical', + }, + ), + ]), + // Dark mode workflow cards + css('[data-theme="dark"] .workflow-card').styles( + raw: { + 'background': '#314155', + 'border-color': '#444950', + }, + ), + css('[data-theme="dark"] .workflow-card p').styles( + color: Color('#ebedf0'), + ), + + // Remove main-container side padding (jaspr_content adds 1.25rem at 768px+) + // Docusaurus has no container-level padding β€” padding lives inside content column + css.media(MediaQuery.all(minWidth: 768.px), [ + css('.docs .main-container').styles( + padding: Padding.symmetric(horizontal: Unit.zero), + ), + ]), + // Desktop content layout: remove inner div horizontal padding and move it + // to content-container, so 75% is calculated on the full available space + // (matching Docusaurus where contentCol = 75% of docMainContainer width) + css.media(MediaQuery.all(minWidth: 1000.px), [ + css('.docs .main-container main > div').styles( + padding: Padding.only(top: 1.rem, left: Unit.zero, right: Unit.zero), + ), + css('.docs .main-container main > div .content-container').styles( + padding: Padding.symmetric(horizontal: 1.rem), + raw: {'max-width': '75%'}, + ), + ]), +]; diff --git a/site_jaspr/pubspec.lock b/site_jaspr/pubspec.lock new file mode 100644 index 00000000..cdc0d15b --- /dev/null +++ b/site_jaspr/pubspec.lock @@ -0,0 +1,693 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: "5b7468c326d2f8a4f630056404ca0d291ade42918f4a3c6233618e724f39da8e" + url: "https://pub.dev" + source: hosted + version: "92.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: "70e4b1ef8003c64793a9e268a551a82869a8a96f39deb73dea28084b0e8bf75e" + url: "https://pub.dev" + source: hosted + version: "9.0.0" + archive: + dependency: transitive + description: + name: archive + sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff + url: "https://pub.dev" + source: hosted + version: "4.0.9" + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" + async: + dependency: transitive + description: + name: async + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + url: "https://pub.dev" + source: hosted + version: "2.13.0" + bazel_worker: + dependency: transitive + description: + name: bazel_worker + sha256: "87cae9150fcf9942b8057e5f51c4848a3efde1289e97411e1c8f01e350120999" + url: "https://pub.dev" + source: hosted + version: "1.1.5" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + build: + dependency: transitive + description: + name: build + sha256: "275bf6bb2a00a9852c28d4e0b410da1d833a734d57d39d44f94bfc895a484ec3" + url: "https://pub.dev" + source: hosted + version: "4.0.4" + build_config: + dependency: transitive + description: + name: build_config + sha256: "4f64382b97504dc2fcdf487d5aae33418e08b4703fc21249e4db6d804a4d0187" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + build_daemon: + dependency: transitive + description: + name: build_daemon + sha256: bf05f6e12cfea92d3c09308d7bcdab1906cd8a179b023269eed00c071004b957 + url: "https://pub.dev" + source: hosted + version: "4.1.1" + build_modules: + dependency: transitive + description: + name: build_modules + sha256: "2e30f22597d45fd49058bb29f47e947d3a045db2a72d3fec27a064cb2164337e" + url: "https://pub.dev" + source: hosted + version: "5.1.7" + build_runner: + dependency: "direct dev" + description: + name: build_runner + sha256: "39ad4ca8a2876779737c60e4228b4bcd35d4352ef7e14e47514093edc012c734" + url: "https://pub.dev" + source: hosted + version: "2.11.1" + build_web_compilers: + dependency: "direct dev" + description: + name: build_web_compilers + sha256: "7975a0304abc80dfaaf9897047ae85d78753ec6b8b97ad113d428464dd17145e" + url: "https://pub.dev" + source: hosted + version: "4.4.13" + built_collection: + dependency: transitive + description: + name: built_collection + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.dev" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + sha256: "6ae8a6435a8c6520c7077b107e77f1fb4ba7009633259a4d49a8afd8e7efc5e9" + url: "https://pub.dev" + source: hosted + version: "8.12.4" + characters: + dependency: transitive + description: + name: characters + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b + url: "https://pub.dev" + source: hosted + version: "1.4.1" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f" + url: "https://pub.dev" + source: hosted + version: "2.0.4" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + code_builder: + dependency: transitive + description: + name: code_builder + sha256: "6a6cab2ba4680d6423f34a9b972a4c9a94ebe1b62ecec4e1a1f2cba91fd1319d" + url: "https://pub.dev" + source: hosted + version: "4.11.1" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + crypto: + dependency: transitive + description: + name: crypto + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf + url: "https://pub.dev" + source: hosted + version: "3.0.7" + csslib: + dependency: transitive + description: + name: csslib + sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e" + url: "https://pub.dev" + source: hosted + version: "1.0.2" + dart_style: + dependency: transitive + description: + name: dart_style + sha256: a9c30492da18ff84efe2422ba2d319a89942d93e58eb0b73d32abe822ef54b7b + url: "https://pub.dev" + source: hosted + version: "3.1.3" + fbh_front_matter: + dependency: transitive + description: + name: fbh_front_matter + sha256: "18b2f355326ff2b7ebd64eb1d969c091895ffa755ab55ede5de68f59cf115024" + url: "https://pub.dev" + source: hosted + version: "0.0.1" + ffi: + dependency: transitive + description: + name: ffi + sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" + gato: + dependency: transitive + description: + name: gato + sha256: "03fa3a33d50f71a912e30827ddf47c89e06e9acfd2b6b178aff0e1aa2f1e8b90" + url: "https://pub.dev" + source: hosted + version: "0.0.5+1" + glob: + dependency: transitive + description: + name: glob + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + url: "https://pub.dev" + source: hosted + version: "2.1.3" + graphs: + dependency: transitive + description: + name: graphs + sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + hotreloader: + dependency: transitive + description: + name: hotreloader + sha256: bc167a1163807b03bada490bfe2df25b0d744df359227880220a5cbd04e5734b + url: "https://pub.dev" + source: hosted + version: "4.3.0" + html: + dependency: transitive + description: + name: html + sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602" + url: "https://pub.dev" + source: hosted + version: "0.15.6" + html_unescape: + dependency: transitive + description: + name: html_unescape + sha256: "15362d7a18f19d7b742ef8dcb811f5fd2a2df98db9f80ea393c075189e0b61e3" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + http: + dependency: transitive + description: + name: http + sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" + url: "https://pub.dev" + source: hosted + version: "1.6.0" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 + url: "https://pub.dev" + source: hosted + version: "3.2.2" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + intl: + dependency: transitive + description: + name: intl + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" + url: "https://pub.dev" + source: hosted + version: "0.20.2" + io: + dependency: transitive + description: + name: io + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b + url: "https://pub.dev" + source: hosted + version: "1.0.5" + jaspr: + dependency: "direct main" + description: + name: jaspr + sha256: "1ef6f6bf5996209a8e2e650159c3554c07ad77613f5db2174d4f47b6204b6a19" + url: "https://pub.dev" + source: hosted + version: "0.22.3" + jaspr_builder: + dependency: "direct dev" + description: + name: jaspr_builder + sha256: "77d777d7bbd0ec795dee9d16a68c38c659d7b3b9a00992b857c2061954ae8a79" + url: "https://pub.dev" + source: hosted + version: "0.22.3" + jaspr_content: + dependency: "direct main" + description: + name: jaspr_content + sha256: e2fa5e7252f67894fc8ff08791f35780b91654f9f24f79a2d47a8b1fa71411ca + url: "https://pub.dev" + source: hosted + version: "0.5.0" + jaspr_router: + dependency: "direct main" + description: + name: jaspr_router + sha256: a82899af7e1717ef1643889a00a9f1a738983b58a7db13f1512a0066056d6a1e + url: "https://pub.dev" + source: hosted + version: "0.8.1" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: cb09e7dac6210041fad964ed7fbee004f14258b4eca4040f72d1234062ace4c8 + url: "https://pub.dev" + source: hosted + version: "4.11.0" + lints: + dependency: "direct dev" + description: + name: lints + sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 + url: "https://pub.dev" + source: hosted + version: "5.1.1" + liquify: + dependency: transitive + description: + name: liquify + sha256: "23aaa728dd2adc15dcc863772dec64425e8bc35bfa3f81323e0726fb83e1e28f" + url: "https://pub.dev" + source: hosted + version: "1.5.1" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + markdown: + dependency: transitive + description: + name: markdown + sha256: "935e23e1ff3bc02d390bad4d4be001208ee92cc217cb5b5a6c19bc14aaa318c1" + url: "https://pub.dev" + source: hosted + version: "7.3.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 + url: "https://pub.dev" + source: hosted + version: "0.12.19" + meta: + dependency: transitive + description: + name: meta + sha256: "9f29b9bcc8ee287b1a31e0d01be0eae99a930dbffdaecf04b3f3d82a969f296f" + url: "https://pub.dev" + source: hosted + version: "1.18.1" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + mustache_template: + dependency: transitive + description: + name: mustache_template + sha256: "4326d0002ff58c74b9486990ccbdab08157fca3c996fe9e197aff9d61badf307" + url: "https://pub.dev" + source: hosted + version: "2.0.3" + package_config: + dependency: transitive + description: + name: package_config + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc + url: "https://pub.dev" + source: hosted + version: "2.2.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" + url: "https://pub.dev" + source: hosted + version: "7.0.2" + pool: + dependency: transitive + description: + name: pool + sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d" + url: "https://pub.dev" + source: hosted + version: "1.5.2" + posix: + dependency: transitive + description: + name: posix + sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07" + url: "https://pub.dev" + source: hosted + version: "6.5.0" + protobuf: + dependency: transitive + description: + name: protobuf + sha256: "75ec242d22e950bdcc79ee38dd520ce4ee0bc491d7fadc4ea47694604d22bf06" + url: "https://pub.dev" + source: hosted + version: "6.0.0" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" + url: "https://pub.dev" + source: hosted + version: "1.5.0" + scratch_space: + dependency: transitive + description: + name: scratch_space + sha256: "3417e014d20b12cebc5bfb1c0b1f63806054177158596cc31cc4d9aaca767a60" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + shelf: + dependency: transitive + description: + name: shelf + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 + url: "https://pub.dev" + source: hosted + version: "1.4.2" + shelf_gzip: + dependency: transitive + description: + name: shelf_gzip + sha256: "4f4b793c0f969f348aece1ab4cc05fceba9fea431c1ce76b1bc0fa369cecfc15" + url: "https://pub.dev" + source: hosted + version: "4.1.0" + shelf_proxy: + dependency: transitive + description: + name: shelf_proxy + sha256: a71d2307f4393211930c590c3d2c00630f6c5a7a77edc1ef6436dfd85a6a7ee3 + url: "https://pub.dev" + source: hosted + version: "1.0.4" + shelf_static: + dependency: transitive + description: + name: shelf_static + sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3 + url: "https://pub.dev" + source: hosted + version: "1.1.3" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + source_gen: + dependency: transitive + description: + name: source_gen + sha256: "1d562a3c1f713904ebbed50d2760217fd8a51ca170ac4b05b0db490699dbac17" + url: "https://pub.dev" + source: hosted + version: "4.2.0" + source_maps: + dependency: transitive + description: + name: source_maps + sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812" + url: "https://pub.dev" + source: hosted + version: "0.10.13" + source_span: + dependency: transitive + description: + name: source_span + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" + url: "https://pub.dev" + source: hosted + version: "1.10.2" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 + url: "https://pub.dev" + source: hosted + version: "2.1.1" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + syntax_highlight_lite: + dependency: "direct main" + description: + name: syntax_highlight_lite + sha256: ff54f69159263e26b843c64a2dc556ac7bbbc66690eb6d500aefb341cf4957a0 + url: "https://pub.dev" + source: hosted + version: "0.0.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" + url: "https://pub.dev" + source: hosted + version: "0.7.10" + timezone: + dependency: transitive + description: + name: timezone + sha256: "784a5e34d2eb62e1326f24d6f600aaaee452eb8ca8ef2f384a59244e292d158b" + url: "https://pub.dev" + source: hosted + version: "0.11.0" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + universal_web: + dependency: transitive + description: + name: universal_web + sha256: "618a9d8659e9429875f69d4da0fb9c0c143d3d4cdab28b4545502088aa50ed62" + url: "https://pub.dev" + source: hosted + version: "1.1.1+1" + uuid: + dependency: transitive + description: + name: uuid + sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489" + url: "https://pub.dev" + source: hosted + version: "4.5.3" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" + url: "https://pub.dev" + source: hosted + version: "15.0.2" + watcher: + dependency: transitive + description: + name: watcher + sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 + url: "https://pub.dev" + source: hosted + version: "3.0.3" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" +sdks: + dart: ">=3.10.0 <3.12.0-z" diff --git a/site_jaspr/pubspec.yaml b/site_jaspr/pubspec.yaml new file mode 100644 index 00000000..8c198bcb --- /dev/null +++ b/site_jaspr/pubspec.yaml @@ -0,0 +1,22 @@ +name: site_jaspr +description: Very Good Workflows documentation site built with Jaspr +version: 0.0.1 +publish_to: none + +environment: + sdk: ^3.10.0 + +dependencies: + jaspr: ^0.22.3 + jaspr_content: ^0.5.0 + jaspr_router: ^0.8.1 + syntax_highlight_lite: ^0.0.1 + +dev_dependencies: + build_runner: ^2.10.0 + build_web_compilers: ^4.4.13 + jaspr_builder: ^0.22.3 + lints: ^5.0.0 + +jaspr: + mode: static diff --git a/site_jaspr/web/.nojekyll b/site_jaspr/web/.nojekyll new file mode 100644 index 00000000..e69de29b diff --git a/site_jaspr/web/404.html b/site_jaspr/web/404.html new file mode 100644 index 00000000..e8475c69 --- /dev/null +++ b/site_jaspr/web/404.html @@ -0,0 +1,54 @@ + + + + + + Page Not Found | Very Good Workflows + + + + + + +
+

404

+

The page you're looking for doesn't exist.

+ Go to Documentation +
+ + diff --git a/site_jaspr/web/CNAME b/site_jaspr/web/CNAME new file mode 100644 index 00000000..596be01b --- /dev/null +++ b/site_jaspr/web/CNAME @@ -0,0 +1 @@ +workflows.vgv.dev \ No newline at end of file diff --git a/site_jaspr/web/favicon.ico b/site_jaspr/web/favicon.ico new file mode 100644 index 00000000..cccc7fcb Binary files /dev/null and b/site_jaspr/web/favicon.ico differ diff --git a/site_jaspr/web/images/github.svg b/site_jaspr/web/images/github.svg new file mode 100644 index 00000000..5044b4d9 --- /dev/null +++ b/site_jaspr/web/images/github.svg @@ -0,0 +1 @@ + diff --git a/site_jaspr/web/images/github_white.svg b/site_jaspr/web/images/github_white.svg new file mode 100644 index 00000000..0ba5b3c5 --- /dev/null +++ b/site_jaspr/web/images/github_white.svg @@ -0,0 +1 @@ + diff --git a/site_jaspr/web/images/logo.svg b/site_jaspr/web/images/logo.svg new file mode 100644 index 00000000..f7d29f9e --- /dev/null +++ b/site_jaspr/web/images/logo.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/site_jaspr/web/images/meta/open-graph.png b/site_jaspr/web/images/meta/open-graph.png new file mode 100644 index 00000000..78dcc9db Binary files /dev/null and b/site_jaspr/web/images/meta/open-graph.png differ diff --git a/site_jaspr/web/images/vgv_logo_black.svg b/site_jaspr/web/images/vgv_logo_black.svg new file mode 100644 index 00000000..c1bf62ce --- /dev/null +++ b/site_jaspr/web/images/vgv_logo_black.svg @@ -0,0 +1,4 @@ + + + + diff --git a/site_jaspr/web/images/vgv_logo_fill.svg b/site_jaspr/web/images/vgv_logo_fill.svg new file mode 100644 index 00000000..446c7d7b --- /dev/null +++ b/site_jaspr/web/images/vgv_logo_fill.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/site_jaspr/web/images/workflows_hero.jpeg b/site_jaspr/web/images/workflows_hero.jpeg new file mode 100644 index 00000000..e7f5d22e Binary files /dev/null and b/site_jaspr/web/images/workflows_hero.jpeg differ diff --git a/site_jaspr/web/images/workflows_hero.png b/site_jaspr/web/images/workflows_hero.png new file mode 100644 index 00000000..a8a089f4 Binary files /dev/null and b/site_jaspr/web/images/workflows_hero.png differ diff --git a/site_jaspr/web/images/workflows_hero.svg b/site_jaspr/web/images/workflows_hero.svg new file mode 100644 index 00000000..1cbf4659 --- /dev/null +++ b/site_jaspr/web/images/workflows_hero.svg @@ -0,0 +1,179 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/site_jaspr/web/images/workflows_icon.svg b/site_jaspr/web/images/workflows_icon.svg new file mode 100644 index 00000000..c4358caf --- /dev/null +++ b/site_jaspr/web/images/workflows_icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/site_jaspr/web/images/workflows_logo.svg b/site_jaspr/web/images/workflows_logo.svg new file mode 100644 index 00000000..6bcef2db --- /dev/null +++ b/site_jaspr/web/images/workflows_logo.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/site_jaspr/web/images/workflows_logo_dark.svg b/site_jaspr/web/images/workflows_logo_dark.svg new file mode 100644 index 00000000..c63e6f6b --- /dev/null +++ b/site_jaspr/web/images/workflows_logo_dark.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/site_jaspr/web/images/workflows_nav_icon.svg b/site_jaspr/web/images/workflows_nav_icon.svg new file mode 100644 index 00000000..a6b161d7 --- /dev/null +++ b/site_jaspr/web/images/workflows_nav_icon.svg @@ -0,0 +1,4 @@ + + + +