diff --git a/.github/workflows/compile-typescript.yml b/.github/workflows/compile-typescript.yml new file mode 100644 index 00000000000..101ee37b4a2 --- /dev/null +++ b/.github/workflows/compile-typescript.yml @@ -0,0 +1,48 @@ +name: Compile TypeScript + +on: + push: + paths: + - 'packages/**/src/**/*.ts' + +jobs: + compile: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Needed for the git commit step + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Install dependencies + run: | + npm ci + cd packages/vite-plugin && npm ci + cd ../.. + + - name: Compile TypeScript + run: | + cd packages/vite-plugin + npm run build + cd ../.. + + - name: Check for changes + id: check_changes + run: | + git diff --exit-code --quiet packages/vite-plugin/dist || echo "changes=true" >> $GITHUB_OUTPUT + + - name: Commit changes + if: steps.check_changes.outputs.changes == 'true' + run: | + git config --local user.email "github-actions[bot]@users.noreply.github.com" + git config --local user.name "github-actions[bot]" + git add packages/vite-plugin/dist + git commit -m "Compile TypeScript" + git push diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 040288caea7..52be9e7164f 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -12,7 +12,7 @@ jobs: steps: - uses: shivammathur/setup-php@v2 with: - php-version: "8.1" + php-version: "8.2" extensions: fileinfo - uses: actions/checkout@v4 @@ -74,7 +74,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, macos-latest, windows-latest] - php: [8.1, 8.2, 8.3] + php: [8.2, 8.3, 8.4] runs-on: ${{ matrix.os }} steps: @@ -109,41 +109,6 @@ jobs: run: php monorepo/scripts/tests/${{ matrix.script }}.php - build-hydefront-assets: - - runs-on: ubuntu-latest - needs: run-smoke-tests - - steps: - - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - cache: 'npm' - - - name: Install Node.js dependencies - working-directory: 'packages/hydefront' - run: npm ci - - - name: Build assets for production - working-directory: 'packages/hydefront' - run: npm run prod - - - name: Upload artifacts - uses: actions/upload-artifact@v4 - with: - name: 'hydefront' - path: 'packages/hydefront/dist' - - - name: Commit changes - uses: EndBug/add-and-commit@v9 - with: - add: 'packages/hydefront/dist' - message: 'Compile HydeFront assets for production' - new_branch: compile-hydefront - - build-tailwindcss: runs-on: ubuntu-latest @@ -164,7 +129,7 @@ jobs: run: sed -i 's/\.\/vendor\/hyde\/framework\/resources\/views\/\*\*\/\*\.blade\.php/\.\/packages\/framework\/resources\/views\/\*\*\/\*\.blade\.php/' tailwind.config.js - name: Build assets for production - run: npm run prod + run: npm run build - name: Copy compiled app.css file to HydeFront dist folder run: cp _media/app.css packages/hydefront/dist/app.css @@ -196,7 +161,7 @@ jobs: steps: - uses: shivammathur/setup-php@v2 with: - php-version: "8.1" + php-version: "8.2" coverage: xdebug extensions: fileinfo - uses: actions/checkout@v4 @@ -290,7 +255,7 @@ jobs: - uses: actions/checkout@v4 - uses: shivammathur/setup-php@v2 with: - php-version: "8.1" + php-version: "8.2" - name: Cache Composer packages id: composer-cache @@ -332,7 +297,7 @@ jobs: - uses: actions/checkout@v4 - uses: shivammathur/setup-php@v2 with: - php-version: "8.1" + php-version: "8.2" - name: Cache Composer packages id: composer-cache @@ -377,7 +342,7 @@ jobs: - uses: actions/checkout@v4 - uses: shivammathur/setup-php@v2 with: - php-version: "8.1" + php-version: "8.2" - name: Cache Composer packages id: composer-cache @@ -544,7 +509,7 @@ jobs: steps: - uses: shivammathur/setup-php@v2 with: - php-version: "8.1" + php-version: "8.2" - uses: actions/checkout@v4 - name: Download static analysis tools @@ -564,7 +529,7 @@ jobs: steps: - uses: shivammathur/setup-php@v2 with: - php-version: "8.1" + php-version: "8.2" - uses: actions/checkout@v4 - name: Install Composer Dependencies @@ -582,7 +547,7 @@ jobs: steps: - uses: shivammathur/setup-php@v2 with: - php-version: "8.1" + php-version: "8.2" - uses: actions/checkout@v4 - name: Install Composer Dependencies diff --git a/.github/workflows/coverage-tests.yml b/.github/workflows/coverage-tests.yml index 170eba8b329..cc5e5e80564 100644 --- a/.github/workflows/coverage-tests.yml +++ b/.github/workflows/coverage-tests.yml @@ -9,7 +9,7 @@ jobs: steps: - uses: shivammathur/setup-php@v2 with: - php-version: "8.1" + php-version: "8.2" coverage: xdebug extensions: fileinfo - uses: actions/checkout@v4 diff --git a/.github/workflows/end-to-end-tests.yml b/.github/workflows/end-to-end-tests.yml index bbaa4612fea..365fc37d009 100644 --- a/.github/workflows/end-to-end-tests.yml +++ b/.github/workflows/end-to-end-tests.yml @@ -33,7 +33,7 @@ jobs: run: vendor/bin/pest --stop-on-failure - name: Prepare the Environment - run: echo -e "APP_URL=http://localhost:8080 \nDUSK_ENABLED=true\nSERVER_DASHBOARD=false" > .env + run: echo -e "APP_URL=http://localhost:8080 \nDUSK_ENABLED=true\nSERVER_DASHBOARD=false\nSERVER_SAVE_PREVIEW=true" > .env - name: Upgrade Chrome Driver run: php hyde dusk:chrome-driver `/opt/google/chrome/chrome --version | cut -d " " -f3 | cut -d "." -f1` diff --git a/.github/workflows/matrix-tests.yml b/.github/workflows/matrix-tests.yml index ff1f7cdaf14..4cb88606756 100644 --- a/.github/workflows/matrix-tests.yml +++ b/.github/workflows/matrix-tests.yml @@ -2,7 +2,7 @@ name: Matrix Tests on: pull_request: - branches: [ "master" ] + branches: [ "master", "2.x-dev" ] paths: - 'app/**' - 'packages/**' @@ -19,7 +19,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, windows-latest, macos-latest] - php: [8.1, 8.2, 8.3] + php: [8.2, 8.3, 8.4] runs-on: ${{ matrix.os }} steps: diff --git a/.github/workflows/split-monorepo.yml b/.github/workflows/split-monorepo.yml index bf45381112b..cc605dc92de 100644 --- a/.github/workflows/split-monorepo.yml +++ b/.github/workflows/split-monorepo.yml @@ -390,3 +390,53 @@ jobs: git commit --author="$COMMIT_AUTHOR_NAME <$COMMIT_AUTHOR_EMAIL>" -m "$COMMIT_MESSAGE https://github.com/hydephp/develop/commit/${{ github.sha }}" git push upstream master + + vite-plugin: + runs-on: ubuntu-latest + continue-on-error: true + environment: + name: hydephp/vite-plugin + url: https://github.com/hydephp/vite-plugin/tree/master + + steps: + - name: Checkout hydephp/develop + uses: actions/checkout@v4 + with: + path: develop + persist-credentials: false + + - name: Checkout hydephp/vite-plugin + uses: actions/checkout@v4 + with: + repository: hydephp/vite-plugin + path: vite-plugin + ref: master + fetch-depth: 0 + persist-credentials: false + + - name: Empty the destination directory + run: rm -rf vite-plugin/* + + - name: Copy over files + run: cp -rf develop/packages/vite-plugin/. vite-plugin -v + + - name: Commit and push changes + env: + COMMIT_MESSAGE: ${{ github.event.head_commit.message }} + COMMIT_AUTHOR_NAME: ${{ github.event.head_commit.author.name }} + COMMIT_AUTHOR_EMAIL: ${{ github.event.head_commit.author.email }} + run: | + cd vite-plugin + if ! [[ `git status --porcelain` ]]; then + echo "No changes to this package. Exiting gracefully."; + exit 0; + fi + + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git remote add upstream https://oauth2:${{ secrets.SPLIT_MONOREPO_TOKEN }}@github.com/hydephp/vite-plugin.git + + git add . + git commit --author="$COMMIT_AUTHOR_NAME <$COMMIT_AUTHOR_EMAIL>" -m "$COMMIT_MESSAGE https://github.com/hydephp/develop/commit/${{ github.sha }}" + + git push upstream master diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index 91eaf200989..096406ad18d 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -2,7 +2,7 @@ name: 🔎 Static Analysis on: pull_request: - branches: [ "master" ] + branches: [ "master", "2.x-dev" ] jobs: diff --git a/.idea/develop.iml b/.idea/develop.iml index e5c500c8de7..d55c9677f2f 100644 --- a/.idea/develop.iml +++ b/.idea/develop.iml @@ -13,6 +13,10 @@ + + + + @@ -63,6 +67,25 @@ + + + + + + + + + + + + + + + + + + + diff --git a/.idea/php.xml b/.idea/php.xml index 59d9085e017..b8fd8b2d22b 100644 --- a/.idea/php.xml +++ b/.idea/php.xml @@ -150,7 +150,6 @@ - @@ -199,9 +198,28 @@ + + + + + + + + + + + + + + + + + + + - + diff --git a/.idea/phpunit.xml b/.idea/phpunit.xml index d0e69a37e22..8a06c9eef22 100644 --- a/.idea/phpunit.xml +++ b/.idea/phpunit.xml @@ -8,7 +8,6 @@ diff --git a/.idea/vcs.xml b/.idea/vcs.xml index f5c7bf895ad..dea01c435ab 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -5,5 +5,6 @@ + \ No newline at end of file diff --git a/.phpstorm.meta.php b/.phpstorm.meta.php index e5e3acca117..db6c871a9b6 100644 --- a/.phpstorm.meta.php +++ b/.phpstorm.meta.php @@ -3,5 +3,7 @@ namespace PHPSTORM_META { override(\app(0), map([ 'hyde' => \Hyde\Foundation\HydeKernel::class, + 'navigation.main' => \Hyde\Framework\Features\Navigation\MainNavigationMenu::class, + 'navigation.sidebar' => \Hyde\Framework\Features\Navigation\DocumentationSidebar::class, ])); } diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index e3d1b60364c..5e60ccdfe1c 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,4 +1,4 @@ -## [Unreleased] - YYYY-MM-DD +## [v2-dev] - YYYY-MM-DD ### About @@ -10,19 +10,680 @@ This serves two purposes: 2. At release time, you can move the Unreleased section changes into a new release version section. ### Added -- for new features. + +- **Added a new consolidated Asset API to better handle media files.** in [#2006](https://github.com/hydephp/develop/pull/2006) + - Added several new fluent methods to the `MediaFile` class, like `getLink()`, `getLength()`, `getMimeType()`, etc + - Added new `HydeFront` facade to handle CDN links and Tailwind config injection + - Added method `Asset::exists()` to check if a media file exists + - Added a `Hyde::assets()` method to get all media file instances in the site +- **Improved Routes facade API with more intuitive method names** in [#2179](https://github.com/hydephp/develop/pull/2179) + - **Breaking:** Renamed `Routes::get()` to `Routes::find()` to better indicate it may return null + - **Breaking:** Renamed `Routes::getOrFail()` to `Routes::get()` to make the exception-throwing behavior the default and match Laravel conventions + - This change requires code updates if you were using these methods - see upgrade guide below +- **Many MediaFile related helpers have been changed or completely rewritten** to provide a simplified API for interacting with media files + - **Note:** For most end users, the changes will have minimal direct impact, but if you have custom code that interacts with media files, you may need to update it + - The `Asset` facade has been restructured to be more scoped and easier to use, splitting out a separate `HydeFront` facade and inlining the `AssetService` class + - All asset retrieval methods now return a `MediaFile` instance, which can be fluently interacted with, or cast to a string to get the link (which was the previous behavior) + - The `Hyde::asset()` method and `asset()` function now return `MediaFile` instances instead of strings, and will throw an exception if the asset does not exist + - Renamed method `Asset::hasMediaFile` to `Asset::exists` in [#1957](https://github.com/hydephp/develop/pull/1957) + - Renamed method `MediaFile::getContentLength` to `MediaFile::getLength` in [#1904](https://github.com/hydephp/develop/pull/1904) + - Replaced method `Hyde::mediaPath` with `MediaFile::sourcePath` in [#1911](https://github.com/hydephp/develop/pull/1911) + - Replaced method `Hyde::siteMediaPath` with `MediaFile::outputPath` in [#1911](https://github.com/hydephp/develop/pull/1911) + - An exception will now be thrown if you try to get a media file that does not exist in order to prevent missing assets from going unnoticed in [#1932](https://github.com/hydephp/develop/pull/1932) +- **MediaFile performance improvements:** + - Media assets are now cached in the HydeKernel, giving a massive performance boost and making it easier to access the instances in [#1917](https://github.com/hydephp/develop/pull/1917) + - Media file metadata is now lazy loaded and then cached in memory, providing performance improvements for files that may not be used in a build in [#1933](https://github.com/hydephp/develop/pull/1933) + - We now use the much faster `CRC32` hashing algorithm instead of `MD5` for cache busting keys in [#1918](https://github.com/hydephp/develop/pull/1918) +- **Ported the HydeSearch plugin used for the documentation search to be an Alpine.js implementation** in [#2029](https://github.com/hydephp/develop/pull/2029) + - Renamed Blade component `hyde::components.docs.search-widget` to `hyde::components.docs.search-modal` in [#2029](https://github.com/hydephp/develop/pull/2029) + - Added support for customizing the search implementation by creating a `resources/js/HydeSearch.js` file in [#2031](https://github.com/hydephp/develop/pull/2031) +- **Replaced Laravel Mix with Vite for frontend asset compilation** in [#2010](https://github.com/hydephp/develop/pull/2010) + - **Breaking:** You must now use `npm run build` to compile your assets, instead of `npm run prod` + - Bundled assets are now compiled directly into the `_media` folder, and will not be copied to the `_site/media` folder by the NPM command in [#2011](https://github.com/hydephp/develop/pull/2011) +- Added Vite as a build tool in [#2010](https://github.com/hydephp/develop/pull/2010) +- Added Vite facade in [#2016](https://github.com/hydephp/develop/pull/2016) +- Added a Vite HMR support for the realtime compiler in [#2016](https://github.com/hydephp/develop/pull/2016) +- Added a `Feature::fromName()` enum helper in [#1895](https://github.com/hydephp/develop/pull/1895) +- Added a custom Blade-based heading renderer for Markdown conversions in [#2047](https://github.com/hydephp/develop/pull/2047) +- Added a new Hyde Vite plugin in [#2160](https://github.com/hydephp/develop/pull/2160) +- Added a new `\Hyde\Framework\Actions\PreBuildTasks\TransferMediaAssets` build task to handle media assets transfers for site builds in [#1536](https://github.com/hydephp/develop/pull/1536) +- Added a new `\Hyde\Framework\Exceptions\InvalidConfigurationException` exception class to handle invalid configuration exceptions in [#1799](https://github.com/hydephp/develop/pull/1799) +- Added a new `\Hyde\Framework\Exceptions\ParseException` exception class to handle parsing exceptions in data collection files in [#1732](https://github.com/hydephp/develop/pull/1732) +- Added a new simplified blog post image front matter schema using a new "caption" field in [#2175](https://github.com/hydephp/develop/pull/2175) +- Added environment variable support for saving previews in [#1996](https://github.com/hydephp/develop/pull/1996) +- Added new `npm run build` command for compiling frontend assets with Vite in [#2010](https://github.com/hydephp/develop/pull/2010) +- Added support for PHP 8.4 in [#2141](https://github.com/hydephp/develop/pull/2141) +- Added support for resolving dynamic links to source files in Markdown documents in [#1590](https://github.com/hydephp/develop/pull/1590) +- Added support for setting `booting()` and `booted()` callbacks in `HydeExtension` classes, allowing extension developers to hook into the kernel boot process more easily in [#1847](https://github.com/hydephp/develop/pull/1847) +- Added support for setting custom navigation items in the YAML configuration in [#1818](https://github.com/hydephp/develop/pull/1818) +- Added support for setting extra attributes for navigation items in [#1824](https://github.com/hydephp/develop/pull/1824) +- Added support for setting the blog post publishing date as a prefix in the source file name in [#2000](https://github.com/hydephp/develop/pull/2000) +- Added support for specifying features in the YAML configuration in [#1896](https://github.com/hydephp/develop/pull/1896) +- Introduced a new navigation config builder class to simplify navigation configuration in [#1827](https://github.com/hydephp/develop/pull/1827) +- Markdown headings are now compiled using our custom Blade-based heading renderer in [#2047](https://github.com/hydephp/develop/pull/2047) - The `id` attributes for heading permalinks have been moved from the anchor to the heading element in [#2052](https://github.com/hydephp/develop/pull/2052) +- The `\Hyde\Facades\Features` class is no longer marked as internal, and is now thus part of the public API in [#1647](https://github.com/hydephp/develop/pull/1647) +- The `publish:views` command is now interactive on Unix-like systems in [#2062](https://github.com/hydephp/develop/pull/2062) +- You can now add custom posts to the blog post feed component when including it directly in [#1893](https://github.com/hydephp/develop/pull/1893) +- You can now specify sidebar item priorities by adding a numeric prefix to documentation page source file names in [#1709](https://github.com/hydephp/develop/pull/1709) ### Changed -- for changes in existing functionality. -### Deprecated -- for soon-to-be removed features. +- **Breaking:** Renamed class `DataCollections` to `DataCollection` in [#1732](https://github.com/hydephp/develop/pull/1732) For more information, see below. +- **Breaking:** Renamed the `hyde.enable_cache_busting` configuration option to `hyde.cache_busting` in [#1980](https://github.com/hydephp/develop/pull/1980) +- **Breaking:** Renamed the `hyde.navigation.subdirectories` configuration option to `hyde.navigation.subdirectory_display` in [#1818](https://github.com/hydephp/develop/pull/1818) +- **Breaking:** Replaced `--run-dev` and `--run-prod` build command flags with a single `--run-vite` flag that uses Vite to build assets in [#2013](https://github.com/hydephp/develop/pull/2013) +- **Breaking:** The `Author::create()` method now returns an array instead of a `PostAuthor` instance in [#1798](https://github.com/hydephp/develop/pull/1798) For more information, see below. +- **Breaking:** The `Author::get()` method now returns `null` if an author is not found, rather than creating a new instance in [#1798](https://github.com/hydephp/develop/pull/1798) For more information, see below. +- **Breaking:** The `hyde.authors` config setting should now be keyed by the usernames in [#1782](https://github.com/hydephp/develop/pull/1782) For more information, see below. +- **Breaking:** The `hyde.features` configuration format has changed to use Enums instead of static method calls in [#1649](https://github.com/hydephp/develop/pull/1649) For more information, see below. +- **Breaking:** The custom navigation item configuration now uses array inputs instead of the previous format in [#1818](https://github.com/hydephp/develop/pull/1818) For more information, see the upgrade guide below. +- **Breaking:** The navigation system internals have been rewritten into a new Navigation API in [#1568](https://github.com/hydephp/develop/pull/1568) This change is breaking for custom navigation implementations. For more information, see below. +- **Breaking:** We now support PHP 8.2–8.4 instead of 8.1–8.3 in [#2141](https://github.com/hydephp/develop/pull/2141) +- **Breaking:** We switched from using CJS to ESM in the frontend tool scaffolding. If you have custom script includes you need to migrate them. See below and the pull request for details in [#2159](https://github.com/hydephp/develop/pull/2159) +- **Breaking:** We upgraded from the TailwindCSS version from v3 to v4 in [#2146](https://github.com/hydephp/develop/pull/2146) You may want to run `npx @tailwindcss/upgrade` in your project if you have custom Tailwind classes in your project. See the information below for details. +- **Dependency:** Switched to forked version of the Torchlight client in [#2141](https://github.com/hydephp/develop/pull/2141) +- **Dependency:** Updated Symfony/yaml to ^7.0 in [#2141](https://github.com/hydephp/develop/pull/2141) +- **Dependency:** Updated illuminate/support and illuminate/view to ^11.0 in [#2141](https://github.com/hydephp/develop/pull/2141) +- **Dependency:** Updated minimum PHP requirement to 8.2 in [#2141](https://github.com/hydephp/develop/pull/2141) +- **Dependency:** Upgraded from Laravel 10 to Laravel 11 in [#2141](https://github.com/hydephp/develop/pull/2141) +- **Medium:** The `route` function will now throw a `RouteNotFoundException` if the route does not exist in [#1741](https://github.com/hydephp/develop/pull/1741) +- **Minor:** Changed the default build task message to make it more concise in [#1659](https://github.com/hydephp/develop/pull/1659) +- **Minor:** Data collection files are now validated for syntax errors during discovery in [#1732](https://github.com/hydephp/develop/pull/1732) +- **Minor:** Due to changes in the navigation system, it is possible that existing configuration files will need to be adjusted in order for menus to look the same (in terms of ordering etc.) +- **Minor:** Methods in the `Includes` facade now return `HtmlString` objects instead of `string` in [#1738](https://github.com/hydephp/develop/pull/1738) For more information, see below. +- **Minor:** Navigation menu items are now no longer filtered by duplicates (meaning two items with the same label can now exist in the same menu) in [#1573](https://github.com/hydephp/develop/pull/1573) +- **Minor:** The `processing_time_ms` attribute in the `sitemap.xml` file has now been removed in [#1744](https://github.com/hydephp/develop/pull/1744) +- **Minor:** The documentation article component now supports disabling the semantic rendering using a falsy value in [#1566](https://github.com/hydephp/develop/pull/1566) +- **Minor:** Updated the `Hyde::url()` helper throw a `BadMethodCallException` instead `BaseUrlNotSetException` when no site URL is set and no path was provided to the method in [#1890](https://github.com/hydephp/develop/pull/1890) +- **Minor:** Updated the blog post layout and post feed component to use the `BlogPosting` Schema.org type instead of `Article` in [#1887](https://github.com/hydephp/develop/pull/1887) +- **Minor:** `Includes::path()` and `Includes::get()` methods now normalize paths to be basenames to match the behavior of the other include methods in [#1738](https://github.com/hydephp/develop/pull/1738) This means that nested directories are no longer supported, as you should use a data collection for these +- Added more rich markup data to blog post components in [#1888](https://github.com/hydephp/develop/pull/1888) (Note that this inevitably changes the HTML output of the blog post components, and that any customized templates will need to be republished to reflect these changes) +- Calling the `DataCollection` methods will no longer create the data collections directory in [#1732](https://github.com/hydephp/develop/pull/1732) +- Calling the `Include::path()` method will no longer create the includes directory in [#1707](https://github.com/hydephp/develop/pull/1707) +- Changed how the documentation search is generated, to be an `InMemoryPage` instead of a post-build task in [#1498](https://github.com/hydephp/develop/pull/1498) +- Colored Markdown blockquotes are now rendered using Blade and TailwindCSS in [#2056](https://github.com/hydephp/develop/pull/2056) This change is not visible in the rendered result, but the HTML output has changed +- Extracted CSS component partials in HydeFront in [#2038](https://github.com/hydephp/develop/pull/2038) +- Improved how the `MarkdownService` class is accessed by binding it into the service container in [#1922](https://github.com/hydephp/develop/pull/1922) +- Improved the media asset transfer build task to have better output in [#1904](https://github.com/hydephp/develop/pull/1904) +- Improved the sitemap data generation to be smarter and more dynamic in [#1744](https://github.com/hydephp/develop/pull/1744) +- Markdown includes are now converted to HTML using the custom HydePHP Markdown service, meaning they now support full GFM spec and custom Hyde features like colored blockquotes and code block filepath labels in [#1738](https://github.com/hydephp/develop/pull/1738) +- Markdown returned from includes are now trimmed of trailing whitespace and newlines in [#1738](https://github.com/hydephp/develop/pull/1738) +- Media asset files are now copied using the new build task instead of the deprecated `BuildService::transferMediaAssets()` method in [#2024](https://github.com/hydephp/develop/pull/2024) +- Moved Blade view `hyde::pages.documentation-search` to `hyde::pages.docs.search` in [#2033](https://github.com/hydephp/develop/pull/2033) +- Moved the Vite build step to run before the site build to prevent duplicate media asset transfers in [#2013](https://github.com/hydephp/develop/pull/2013) +- Moved the sidebar documentation to the documentation pages section for better organization +- Normalized default Tailwind Typography Prose code block styles to match Torchlight's theme, ensuring consistent styling across Markdown and Torchlight code blocks in [#2036](https://github.com/hydephp/develop/pull/2036) +- Overhauled the blog post author feature in [#1782](https://github.com/hydephp/develop/pull/1782) +- Renamed the parameter `category` to `group` in the `publish:views` command in [#2166](https://github.com/hydephp/develop/pull/2166) +- Reorganized and cleaned up the navigation and sidebar documentation for improved clarity +- Replaced HydeFront styles with Tailwind in [#2024](https://github.com/hydephp/develop/pull/2024) +- Skipped build tasks will now exit with an exit code of 3 instead of 0 in [#1749](https://github.com/hydephp/develop/pull/1749) +- The `Markdown::render()` method will now always render Markdown using the custom HydePHP Markdown service (thus getting smart features like our Markdown processors) in [#1900](https://github.com/hydephp/develop/pull/1900) +- The `hasFeature` method on the Hyde facade and HydeKernel now only accepts a Feature enum value instead of a string for its parameter in [#1650](https://github.com/hydephp/develop/pull/1650) +- The build command now groups together all `InMemoryPage` instances under one progress bar group in [#1897](https://github.com/hydephp/develop/pull/1897) +- The full page documentation search now generates its heading using smarter natural language processing based on the configured sidebar header in [#2032](https://github.com/hydephp/develop/pull/2032) +- The realtime compiler now only serves assets from the media source directory (`_media`), and no longer checks the site output directory (`_site/media`) in [#2012](https://github.com/hydephp/develop/pull/2012) +- Updated default configuration to no longer save previewed pages in [#1995](https://github.com/hydephp/develop/pull/1995) + +### Fixed + +- Added missing collection key types in Hyde facade method annotations in [#1784](https://github.com/hydephp/develop/pull/1784) +- The `app.css` file will no longer be copied to the media output directory when app styles are configured to be loaded from a CDN in [#2180](https://github.com/hydephp/develop/pull/2180) +- The `app.js` file will now only be compiled if it has scripts in [#2028](https://github.com/hydephp/develop/pull/2028) ### Removed -- for now removed features. -### Fixed -- for any bug fixes. +- Added missing collection key types in Hyde facade method annotations in [#1784](https://github.com/hydephp/develop/pull/1784) +- The `app.css` file will no longer be copied to the media output directory when app styles are configured to be loaded from a CDN in [#2180](https://github.com/hydephp/develop/pull/2180) +- The `app.js` file will now only be compiled if it has scripts in [#2028](https://github.com/hydephp/develop/pull/2028) + +### Package updates + +#### Realtime Compiler + +- Simplified the asset file locator to only serve files from the media source directory in https://github.com/hydephp/develop/pull/2012 +- Added Vite HMR support in https://github.com/hydephp/develop/pull/2016 + +#### HydeFront + +- Removed all Sass styles after porting everything to Tailwind in https://github.com/hydephp/develop/pull/2024 +- Removed the `hyde.css` file in https://github.com/hydephp/develop/pull/2037 as all its styles were refactored to Tailwind in https://github.com/hydephp/develop/pull/2024 +- Extracted CSS component partials in https://github.com/hydephp/develop/pull/2038 + +### Upgrade Guide + +Please see the "Breaking changes & upgrade guide" section below for more information. + +## Breaking changes & upgrade guide + + + +Please read through this section to ensure your site upgrades smoothly. + +## Before you start + +Before you start, please upgrade your application to at least HydePHP v1.6 as that version contains helpers to make the upgrade process easier. + +## TailwindCSS v4 + +We upgraded the TailwindCSS version from v3 to v4. If you have written custom markup with Tailwind classes you should read the Tailwind v4 [Upgrade Guide](https://tailwindcss.com/docs/upgrade-guide#changes-from-v3) so you know which breaking changes were introduced. Thankfully, the team behind Tailwind provide an automated [Upgrade Tool](https://tailwindcss.com/docs/upgrade-guide#using-the-upgrade-tool). Run it with the following command: + +```bash +$ npx @tailwindcss/upgrade +``` + +## High impact + +### Switch frontend tooling to full ESM support + +The frontend tooling has been switched from CommonJS to ESM. This means that all JavaScript files are now ESM modules, and you will need to update your custom scripts to use ESM syntax. + +If you only used the default HydePHP frontend (without custom JavaScript), no action is needed. Otherwise, please read the upgrade guide here: https://github.com/hydephp/develop/pull/2159 + +### Navigation system rewrite + +The navigation system has been rewritten into a new Navigation API. This change is breaking for custom navigation implementations, see more down below. + +For most users, the only impact will be that configuration files need to be updated to use the new configuration format. Due to the internal changes, +it's also possible that menu items will be in a slightly different order than before, depending on your setup. Please verify that your site's menus +look as expected after upgrading, and adjust the configuration files if necessary, before deploying to production. + +### Navigation and sidebar configuration changes + +The navigation and sidebar configuration files have been updated to use the new Navigation API. +This means that you will need to update your configuration files to use the new format. + +The easiest way to upgrade is to publish updated configuration files (`hyde.php` and `docs.php`) and copy over your customizations. + +The following configuration entries have been updated: + +- Changed configuration option `docs.sidebar_order` to `docs.sidebar.order` in https://github.com/hydephp/develop/pull/1583 +- Upgrade path: Move the `sidebar_order` option's array in the `config/docs.php` file into the `sidebar` array in the same file. + +- Changed configuration option `docs.table_of_contents` to `docs.sidebar.table_of_contents` in https://github.com/hydephp/develop/pull/1584 +- Upgrade path: Move the `table_of_contents` option's array in the `config/docs.php` file into the `sidebar` array in the same file. + +### Features configuration changes + +The `hyde.features` configuration format has changed to use Enums instead of static method calls. This change is breaking as it will require you to update your `config/hyde.php` file. + +#### Instead of + +```php +// filepath: config/hyde.php + +'features' => [ + // Page Modules + Features::htmlPages(), + Features::markdownPosts(), + Features::bladePages(), + Features::markdownPages(), + Features::documentationPages(), + + // Frontend Features + Features::darkmode(), + Features::documentationSearch(), + + // Integrations + Features::torchlight(), +], +``` + +#### Use instead + +```php +// filepath: config/hyde.php + +'features' => [ + // Page Modules + Feature::HtmlPages, + Feature::MarkdownPosts, + Feature::BladePages, + Feature::MarkdownPages, + Feature::DocumentationPages, + + // Frontend Features + Feature::Darkmode, + Feature::DocumentationSearch, + + // Integrations + Feature::Torchlight, +], +``` + +Of course, if you have disabled any of the features, do not include them in the new array. + +## General impact + +### Post Author changes + +This release makes major improvements to the usability and design of the blog post author feature. + +Here is the full list of changes: + +- Breaking: The `hyde.authors` config setting must now be keyed by the usernames, instead of providing the username in the author facade constructor. +- Breaking: The `Author::create()` method now returns an array instead of a `PostAuthor` instance. This only affects custom code that uses the `Author` facade. +- Breaking: The `Author::get()` method now returns `null` if an author is not found, rather than creating a new instance. This only affects custom code that uses the `Author` facade. +- Removed: The deprecated `PostAuthor::getName()` method has been removed (use `$author->name` instead). +- Changed: Author usernames are now automatically normalized (converted to lowercase and spaces replaced with underscores in order to ensure URL routability). +- Changed: If an author display name is not provided, it is now intelligently generated from the username. +- Feature: Authors can now be set in the YAML configuration. +- Feature: Added a `$author->getPosts()` method to get all of an author's posts. +- Feature: Authors now support custom biographies, avatars, and social media links. Note that these are not currently used in any of the default templates, but you can use them in your custom views. +- The collection of site authors is now stored in the HydeKernel, meaning authors can be accessed through `Hyde::authors()`. +- The `PostAuthor` class is now Arrayable and JsonSerializable. + +#### Upgrade guide: + +1. Update your `config/hyde.php` file to use the new author configuration format: + + ```php + 'authors' => [ + 'username' => Author::create( + name: 'Display Name', + website: 'https://example.com', + bio: 'Author bio', + avatar: 'avatar.png', + socials: ['twitter' => '@username'] + ), + ], + ``` + +2. Review and update any code that uses the `Author` facade: + +- The `create()` method now returns an array instead of a `PostAuthor` instance. +- The `get()` method may return `null`, so handle this case in your code. + +3. Check your blog post front matter and ensure that `author` fields match the new username keys in your configuration. + +4. If you have custom templates that use author data, update them to: + +- Optional: Feel free to use the new available fields: `bio`, `avatar`, and `socials`. +- Account for usernames now being lowercase with underscores which may lead to changed HTML or URL paths. + +5. If you were relying on `Author::get()` to create new authors on the fly, update your code to handle `null` returns or create authors explicitly. + +For more information, see https://github.com/hydephp/develop/pull/1782 and https://github.com/hydephp/develop/pull/1798 + +### Documentation search page changes + +The documentation search page and search index have been changed to be generated as `InMemoryPages` instead of a post-build task. + +The main impact noticeable to most users by this is the implicit changes, like the pages showing up in the dashboard and route list command. + +In case you have customized the `GenerateSearch` post-build task you may, depending on what you were trying to do, +want to adapt your code to interact with the new `InMemoryPage`, which is generated in the `HydeCoreExtension` class. + +For more information, see https://github.com/hydephp/develop/pull/1498. + +## Medium impact + +### Features class method renames + +The following methods in the `Features` class have been renamed to follow a more consistent naming convention: + +- `Features::enabled()` has been renamed to `Features::has()` +- `Features::sitemap()` has been renamed to `Features::hasSitemap()` +- `Features::rss()` has been renamed to `Features::hasRss()` + +Note that this class was previously marked as internal in v1, but the change is logged here in case it was used in configuration files or custom code. + +### Asset API Changes + +#### Overview + +For most end users, the changes to the Asset API in HydePHP 2.x will have minimal direct impact. However, if you have custom code that interacts with media files, you may need to update it. + +The most important thing to note is that all asset retrieval methods now return a `MediaFile` instance, which can be fluently interacted with, or cast to a string to get the link (which was the previous behavior). + +#### Side effects to consider + +Regardless of if you need to make changes to your code, there are a few side effects to consider: + +- All cache busting keys will have changed since we changed the hashing algorithm from `MD5` to `CRC32`. +- Media file getters now return MediaFile instances instead of strings. But these can still be used the same way in Blade `{{ }}` tags, as they can be cast to strings. +- Due to the internal normalizations, we will consistently use cache busting keys and use qualified paths when site URLs are set. +- An exception will be thrown if you try to get a media file that does not exist in order to prevent missing assets from going unnoticed. + +These side effects should not have any negative impact on your site, but may cause the generated HTML to look slightly different. + +#### Impact on Your Code + +If you are using strict type declarations, you may need to update your code to expect a `MediaFile` instance instead of a string path; or you should cast the `MediaFile` instance to a string when needed. + +Most changes were made in https://github.com/hydephp/develop/pull/1904 which contains extra information and the reasoning behind the changes. + +#### Updating Your Code + +Once you have determined that you need to update your code, here are the steps you should take: + +1. Update calls to renamed methods: + + ```php + // Replace this: With this: + Hyde::mediaLink('image.png') => Hyde::asset('image.png'); + Asset::mediaLink('image.png') => Asset::get('image.png'); + Asset::hasMediaFile('image.png') => Asset::exists('image.png'); + Asset::cdnLink('app.css') => HydeFront::cdnLink('app.css'); + Asset::injectTailwindConfig() => HydeFront::injectTailwindConfig(); + FeaturedImage::isRemote($source) => Hyperlinks::isRemote($source); + ``` + +2. Rename the option `hyde.enable_cache_busting` to `hyde.cache_busting` in your configuration file. + +3. Remove any references to `hyde.hydefront_version` and `hyde.hydefront_cdn_url` in your config files as these options have been removed. + +4. If you were using `AssetService` directly, refactor your code to use the new `Asset` facade, `MediaFile` class, or `HydeFront` facade as appropriate. + +These changes simplify the Asset API and provide more robust handling of media files. The new `MediaFile` class offers additional functionality for working with assets. + +## Low impact + +### Navigation internal changes + +The navigation system has been rewritten into a new Navigation API. This change is breaking for custom navigation implementations. + +If you have previously in your custom code done any of the following, or similar, you will need to adapt your code to use the new Navigation API: + +- Created custom navigation menus or Blade components +- Extended or called the navigation related classes directly +- Customized the navigation system in any way beyond the standard configuration + +#### Upgrade guide + +Due to the scope of the rewrite, the easiest and fastest way to upgrade your code is to recreate it using the new Navigation API. + +- For a full comparison of the changes, you may see the PR that introduced the new API: https://github.com/hydephp/develop/pull/1568/files +- For information on how to use the new Navigation API, see the documentation: https://hydephp.com/docs/2.x/navigation-api +- If you use DataCollections, you should read the upgrade path below as there are breaking changes to the DataCollection API. + +### HTML ID changes + +Some HTML IDs have been renamed to follow a more consistent naming convention. + +If you have used any of the following selectors in custom code you wrote yourself, you will need to update to use the new changed IDs. + +#### https://github.com/hydephp/develop/pull/1622 + +- Rename HTML ID `#searchMenu` to `#search-menu` +- Rename HTML ID `#searchMenuButton` to `#search-menu-button` +- Rename HTML ID `#searchMenuButtonMobile` to `#search-menu-button-mobile` + +### New documentation search implementation + +As the new documentation search implementation brings changes to their code API you may need to adapt your code +according to the information below in case you wrote custom code that interacted with these parts of the codebase. + +- The `GenerateSearch` post-build task has been removed. If you have previously extended or customized this class, + you will need to adapt your code, as the search index files are now handled implicitly during the standard build process, + as the search pages are now added to the kernel page and route collection. (https://github.com/hydephp/develop/pull/1498) + +- If your site has a custom documentation search page, for example `_docs/search.md` or `_pages/docs/search.blade.php`, + that page will no longer be built when using the specific `build:search` command. It will, of course, + be built using the standard `build` command. https://github.com/hydephp/develop/commit/82dc71f4a0e7b6be7a9f8d822fbebe39d2289ced + +- In the highly unlikely event your site customizes any of the search pages by replacing them in the kernel route collection, + you would now need to do that in the kernel page collection due to the search pages being generated earlier in the lifecycle. + https://github.com/hydephp/develop/commit/82dc71f4a0e7b6be7a9f8d822fbebe39d2289ced + +### Media asset transfer implementation changes + +The internals of how media asset files are copied during the build process have been changed. For most users, this change +has no impact. However, if you have previously extended this method, or called it directly from your custom code, +you will need to adapt your code to use the new `TransferMediaAssets` build task. + +For example, if you triggered the media transfer with a build service method call, use the new build task instead: + +```php +(new BuildService)->transferMediaAssets(); + +(new TransferMediaAssets())->run(); +``` + +### Includes facade changes + +The following methods in the `Includes` facade now return `HtmlString` objects instead of `string`: + +- `Includes::html()` +- `Includes::blade()` +- `Includes::markdown()` + +- This means that you no longer need to use `{!! !!}` to render the output of these methods in Blade templates, instead just use `{{ }}`. +- If you have used the return value of these methods in custom code, you may need to adjust your code to work with the new return type. + +For more information, see the RFC that proposed this change: https://github.com/hydephp/develop/issues/1734 +The RFC was implemented in https://github.com/hydephp/develop/pull/1738 + +#### Remember to escape output if necessary + +**Note:** Remember that this means that includes are **no longer escaped** by default, so make sure to escape the output if necessary, for example if the content is user-generated. + +- (Use `{{ e(Includes::html('foo')) }}` instead of `{{ Includes::html('foo') }}` to escape the output, matching the previous behavior.) + +### DataCollection API changes + +The DataCollection feature has been reworked to improve the developer experience and make it more consistent with the rest of the API. + +Unfortunately, this means that existing setups may need to be adjusted to work with the new API. + +#### Upgrade guide + +- The `DataCollections` class has been renamed to `DataCollection`. If you have used the `DataCollections` class in your code, you will need to update your code to use the new class name. + +#### Changes + +- Calling the `DataCollection` methods will no longer create the data collections directory automatically. +- The `DataCollection` class now validates the syntax of all data collection files during discovery, and throws a `ParseException` if the syntax is invalid. + +#### Issues that may arise + +If you start getting a `ParseException` when using the `DataCollection` class, it may be due to malformed data collection files. +Starting from this version, we validate the syntax of JSON and YAML in data files during discovery, including any front matter in Markdown data files. +We do this to help you catch errors early. See https://github.com/hydephp/develop/issues/1736 for more information. + +For example, an empty or malformed JSON file will now throw an exception like this: + +```php +\Hyde\Framework\Exceptions\ParseException: Invalid JSON in file: 'foo/baz.json' (Syntax error) +``` + +In order to normalize the thrown exceptions, we now rethrow the `ParseException` from `Symfony/Yaml` as our custom `ParseException` to match the JSON and Markdown validation. +Additionally, an exception will be thrown if a data file is empty, as this is unlikely to be intentional. Markdown files can have an empty body if front matter is present. + +### Removal of `FeaturedImage::isRemote()` method + +The `FeaturedImage::isRemote()` method has been removed in v2.0. This method was deprecated in v1.8.0 and has now been completely removed. + +#### Upgrade guide + +If you were using `FeaturedImage::isRemote()` in your code, you should replace it with `Hyperlinks::isRemote()`. Here's how to update your code: + +```php +// Old code +FeaturedImage::isRemote($source); + +// New code +use Hyde\Foundation\Kernel\Hyperlinks; + +Hyperlinks::isRemote($source); +``` + +This change was implemented in https://github.com/hydephp/develop/pull/1883. Make sure to update any instances of `FeaturedImage::isRemote()` in your codebase to ensure compatibility with HydePHP v2.0. + +### Routes facade API changes + +The Routes facade API has been improved to better follow Laravel naming conventions and make the API more intuitive. This change affects code that directly uses the Routes facade methods. + +#### Changes + +- The `Routes::get()` method has been renamed to `Routes::find()` to better indicate that it may return null if a route is not found +- The `Routes::getOrFail()` method has been renamed to `Routes::get()` to make the exception-throwing behavior the default, matching Laravel conventions + +#### Upgrade guide + +If you have used the Routes facade in your custom code, update it as follows: + +```php +// Old code: +$route = Routes::get('some-route'); // Returns null if not found +$route = Routes::getOrFail('some-route'); // Throws exception if not found + +// New code: +$route = Routes::find('some-route'); // Returns null if not found +$route = Routes::get('some-route'); // Throws exception if not found +``` + +This change provides more intuitive method names and better type safety, with `find()` returning `?Route` and `get()` returning `Route`. + +This change was implemented in https://github.com/hydephp/develop/pull/2179. + +## New Asset System + +### Abstract + +The new asset system is a complete rewrite of the HydeFront asset handling system, replacing Laravel Mix with Vite, and favouring Blade-based components with Tailwind classes over CSS partials and custom stylesheets. + +### Enhancements + +- **Replaced Laravel Mix with Vite for frontend asset compilation.** ([#2010], [#2011], [#2012], [#2013], [#2016], [#2021]) + - Bundled assets are now compiled directly into the `_media` folder. + - The realtime compiler now only serves assets from the media source directory (`_media`). + - Added a new `npm run build` command for compiling frontend assets with Vite. + - Added Vite facade for Blade templates. + - Added Vite Hot Module Replacement (HMR) support to the realtime compiler. + - Build command now uses Vite to compile assets when the `--run-vite` flag is passed. + +- **Improved HydeFront integration.** ([#2024], [#2029], [#2031], [#2036], [#2037], [#2038], [#2039]) + - HydeFront styles are now refactored into Tailwind. + - HydeFront now acts as a component library with granular Tailwind styles in `app.css`. + - HydeSearch plugin ported to Alpine.js, improving performance and customizability. + - Normalized Tailwind Typography Prose code block styles to match Torchlight. + - Extracted CSS component partials in HydeFront. + - Removed `hyde.css` from HydeFront, as all styles are now included in `app.css`. + +- **Implemented a custom Blade-based heading renderer for Markdown.** ([#2047], [#2052]) + - Improves permalink handling and customization options. + - `id` attributes for heading permalinks have been moved from the anchor to the heading element. +- **Colored Markdown blockquotes are now rendered using Blade and Tailwind CSS.** ([#2056]) +- The `app.js` file will now only be compiled if it has scripts. ([#2028]) + + +### Breaking Changes + +- Replaced Laravel Mix with Vite. ([#2010]) + - You must now use `npm run build` to compile your assets, instead of `npm run prod`. +- Removed `--run-dev` and `--run-prod` build command flags, replaced by `--run-vite`. ([#2013]) +- Removed `DocumentationPage::getTableOfContents()` method. Table of contents are now generated using a Blade component. ([#2045]) +- Removed `hyde.css` from HydeFront, requiring recompilation of assets if you were extending it. ([#2037]) +- Changed how HydeFront is included in projects. Instead of separate `hyde.css` and `app.css`, all styles are now in `app.css`. ([#2024]) + + +### Removals + +- Removed Laravel Mix as a dependency. ([#2010]) +- Removed `npm run prod` command. ([#2010]) +- Removed CDN include for HydeSearch plugin. ([#2029]) +- Removed the `` and `` Blade components, replaced by ``. ([#2029]) +- Removed the `.torchlight-enabled` CSS class. ([#2036]) +- Removed the `MarkdownService::withPermalinks` and `MarkdownService::canEnablePermalinks` methods. ([#2047]) + + +### Blade-based table of contents generator + +The way we generate table of contents for documentation pages have been changed from a helper method to a Blade component. + +This new system is much easier to customize and style, and is up to 40 times faster than the old system. + +See https://github.com/hydephp/develop/pull/2045 for more information. + +#### Scope + +The likelihood of impact is low, but if any of the following are true, you may need to update your code: + +- If you have used the `Hyde\Framework\Actions\GeneratesTableOfContents` class in custom code, you will likely need to update that code for the rewritten class. +- If you have published the `resources/views/components/docs/sidebar-item.blade.php` component, you will need to update it to call the new component instead of the old generator rendering. +- If you have called the now removed `getTableOfContents` method of the `DocumentationPage` class in custom code, you will need to update that usage as to possibly call the new Blade component directly, depending on your use case. +- If you have called the now removed `hasTableOfContents` method of the `DocumentationPage` class in custom code you will need to replace the method call with `Config::getBool('docs.sidebar.table_of_contents.enabled', true)` + +#### Changes +- Adds a new `resources/views/components/docs/table-of-contents.blade.php` component containing the structure and styles for the table of contents +- Rewrites the `GeneratesTableOfContents` class to use a custom implementation instead of using CommonMark +- The `execute` method of the `GeneratesTableOfContents` class now returns an array of data, instead of a string of HTML. This data should be fed into the new component +- Removed the `table-of-contents.css` file as styles are now made using Tailwind +- Removed the `heading-permalinks.css` file as styles are now made using Tailwind +- Removed the `blockquotes.css` file as styles are now made using Tailwind + +## New features + + + +### Navigation configuration changes + +The custom navigation item configuration format has been updated to use array inputs. This change allows for more flexibility and consistency in defining navigation items. + +#### Old format: + +```php +'navigation' => [ + 'custom_items' => [ + 'Custom Item' => '/custom-page', + ], +], +``` + +#### New format: + +```php +'navigation' => [ + 'custom_items' => [ + ['label' => 'Custom Item', 'destination' => '/custom-page'], + ], +], +``` + +Additionally, the `hyde.navigation.subdirectories` configuration option has been renamed to `hyde.navigation.subdirectory_display`. Update your configuration files accordingly. + +### YAML configuration for navigation items + +You can now set custom navigation items directly in your YAML configuration files. This provides an alternative to defining them in the PHP configuration files. + +Example: + +```yaml +navigation: + custom_items: + - label: Custom Item + destination: /custom-page +``` + +### Extra attributes for navigation items + +Navigation items now support extra attributes, allowing you to add custom data or styling to your navigation elements. You can set these attributes in both PHP and YAML configurations. + +Example in PHP: + +```php +'navigation' => [ + 'custom_items' => [ + [ + 'label' => 'Custom Item', + 'destination' => '/custom-page', + 'attributes' => ['class' => 'special-link', 'target' => '_blank'], + ], + ], +], +``` + +Example in YAML: + +```yaml +navigation: + custom_items: + - label: Custom Item + destination: /custom-page + attributes: + class: special-link + target: _blank +``` -### Security -- in case of vulnerabilities. +These changes provide more flexibility and control over your site's navigation structure. Make sure to update your configuration files and any custom code that interacts with navigation items to align with these new formats and features. diff --git a/SECURITY.md b/SECURITY.md index a155f70d1d5..5c325d806cd 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -6,7 +6,8 @@ These are the version ranges of HydePHP, and their support status. We follow [Se | Version | Supported | Classification | |---------|--------------------|----------------------| -| 1.x LTS | :white_check_mark: | General Availability | +| 2.x | :white_check_mark: | General Availability | +| 1.x LTS | :white_check_mark: | Long Term Support | | < 0.64 | :x: | Beta | | < 0.8 | :x: | Alpha | diff --git a/_ide_helper.php b/_ide_helper.php index 7d868d7bbb5..88203364b33 100644 --- a/_ide_helper.php +++ b/_ide_helper.php @@ -22,20 +22,25 @@ /** @var string $routeKey The route key for the page being compiled/previewed */ $routeKey = \Hyde\Support\Facades\Render::getRouteKey(); +// Variables available only to some page types + +/** @var \Hyde\Framework\Features\Navigation\DocumentationSidebar $sidebar */ +$sidebar = app('navigation.sidebar'); + // Facades (aliased in app/config.php) /** @mixin \Hyde\Foundation\HydeKernel */ class Hyde extends \Hyde\Hyde {} class Site extends \Hyde\Facades\Site {} class Meta extends \Hyde\Facades\Meta {} -/** @mixin \Hyde\Framework\Services\AssetService */ class Asset extends \Hyde\Facades\Asset {} class Author extends \Hyde\Facades\Author {} class Features extends \Hyde\Facades\Features {} class Config extends \Hyde\Facades\Config {} +class Vite extends \Hyde\Facades\Vite {} /** @mixin \Illuminate\Filesystem\Filesystem */ class Filesystem extends \Hyde\Facades\Filesystem {} -class DataCollections extends \Hyde\Support\DataCollections {} +class DataCollection extends \Hyde\Support\DataCollection {} class Includes extends \Hyde\Support\Includes {} /** @mixin \Hyde\Foundation\Kernel\RouteCollection */ class Routes extends \Hyde\Foundation\Facades\Routes {} diff --git a/_media/app.css b/_media/app.css index 157650ea21f..f4bbbd9907e 100644 --- a/_media/app.css +++ b/_media/app.css @@ -1,4 +1 @@ -/*! HydeFront v3.4.1 | MIT License | https://hydephp.com*/.hyde-search-context{margin-bottom:10px}.hyde-search-results{margin-top:1.25em;max-height:60vh;overflow-y:auto}#search-status{margin-top:0}#sidebar-toggle{display:inline-block;height:2rem;position:relative;width:2rem}#sidebar-toggle span.icon-bar{background-color:#000;display:block;height:2.375px;left:5.5px;position:absolute;transition:all .3s ease-out;width:20px}#sidebar-toggle span.icon-bar:first-child{top:9px}#sidebar-toggle span.icon-bar:nth-child(2),#sidebar-toggle span.icon-bar:nth-child(3){top:15px;transform-origin:center}#sidebar-toggle span.icon-bar:last-child{top:21px}#sidebar-toggle.active span.icon-bar:first-child{opacity:0}#sidebar-toggle.active span.icon-bar:nth-child(2){transform:rotate(45deg)}#sidebar-toggle.active span.icon-bar:nth-child(3){transform:rotate(-45deg)}#sidebar-toggle.active span.icon-bar:last-child{opacity:0}.dark #sidebar-toggle span.icon-bar{background-color:#fff;height:2px}.table-of-contents{padding-bottom:.75rem}.table-of-contents>li{margin-bottom:.35rem;margin-top:.15rem}.table-of-contents ul{padding-left:.5rem}.table-of-contents a{display:block;margin-left:-2rem;opacity:.825;padding-left:2rem}.table-of-contents a:before{content:"#";font-size:75%;margin-right:4px;opacity:.5;transition:opacity .3s ease-in-out}.table-of-contents a:hover{background-color:hsla(0,0%,50%,.2);opacity:1;transition:opacity,background .3s ease-in-out}.table-of-contents a:hover:before{opacity:1}#hyde-docs .prose h1,#hyde-docs .prose h2,#hyde-docs .prose h3,#hyde-docs .prose h4,#hyde-docs .prose h5,#hyde-docs .prose h6{width:-moz-fit-content;width:fit-content}#hyde-docs .prose h1:focus .heading-permalink,#hyde-docs .prose h1:hover .heading-permalink,#hyde-docs .prose h2:focus .heading-permalink,#hyde-docs .prose h2:hover .heading-permalink,#hyde-docs .prose h3:focus .heading-permalink,#hyde-docs .prose h3:hover .heading-permalink,#hyde-docs .prose h4:focus .heading-permalink,#hyde-docs .prose h4:hover .heading-permalink,#hyde-docs .prose h5:focus .heading-permalink,#hyde-docs .prose h5:hover .heading-permalink,#hyde-docs .prose h6:focus .heading-permalink,#hyde-docs .prose h6:hover .heading-permalink{filter:grayscale(100%);opacity:.75;transition:opacity .1s ease-out}#hyde-docs .prose h1 .heading-permalink,#hyde-docs .prose h2 .heading-permalink,#hyde-docs .prose h3 .heading-permalink,#hyde-docs .prose h4 .heading-permalink,#hyde-docs .prose h5 .heading-permalink,#hyde-docs .prose h6 .heading-permalink{margin-left:.25rem;opacity:0;padding:0 .25rem;scroll-margin:1rem;transition:opacity .3s ease}#hyde-docs .prose h1 .heading-permalink:before,#hyde-docs .prose h2 .heading-permalink:before,#hyde-docs .prose h3 .heading-permalink:before,#hyde-docs .prose h4 .heading-permalink:before,#hyde-docs .prose h5 .heading-permalink:before,#hyde-docs .prose h6 .heading-permalink:before{content:"#"}#hyde-docs .prose h1 .heading-permalink:focus,#hyde-docs .prose h1 .heading-permalink:hover,#hyde-docs .prose h2 .heading-permalink:focus,#hyde-docs .prose h2 .heading-permalink:hover,#hyde-docs .prose h3 .heading-permalink:focus,#hyde-docs .prose h3 .heading-permalink:hover,#hyde-docs .prose h4 .heading-permalink:focus,#hyde-docs .prose h4 .heading-permalink:hover,#hyde-docs .prose h5 .heading-permalink:focus,#hyde-docs .prose h5 .heading-permalink:hover,#hyde-docs .prose h6 .heading-permalink:focus,#hyde-docs .prose h6 .heading-permalink:hover{filter:unset;opacity:1}html{scroll-behavior:smooth}.torchlight-enabled pre{border-radius:.25rem;margin-bottom:1rem;margin-top:1rem;overflow-x:auto;padding:0}.torchlight-enabled pre code.torchlight{display:block;min-width:-moz-max-content;min-width:max-content;padding-bottom:1rem;padding-top:1rem}.torchlight-enabled pre code.torchlight .line{padding-left:1rem;padding-right:1rem}.torchlight-enabled pre code.torchlight .line-number,.torchlight-enabled pre code.torchlight .summary-caret{margin-right:1rem}.prose blockquote.info{--tw-border-opacity:1;border-color:rgb(59 130 246/var(--tw-border-opacity,1))}.prose blockquote.success{--tw-border-opacity:1;border-color:rgb(34 197 94/var(--tw-border-opacity,1))}.prose blockquote.warning{--tw-border-opacity:1;border-color:rgb(245 158 11/var(--tw-border-opacity,1))}.prose blockquote.danger{--tw-border-opacity:1;border-color:rgb(220 38 38/var(--tw-border-opacity,1))}code{max-width:80vw;overflow-x:auto;vertical-align:top;word-break:break-all}pre code{display:block;max-width:unset}pre>code>.filepath{float:right;opacity:.5;position:relative;right:.25rem;top:-.25rem;transition:opacity .25s}pre>code>.filepath:hover{opacity:1}.torchlight-enabled pre>code>.filepath{right:1rem}@media screen and (max-width:767px){pre>code>.filepath{display:none}}@media screen and (min-width:768px){[x-cloak].x-uncloak-md{display:revert!important}}.prose :where(th,td):not(:where([class~=not-prose],[class~=not-prose] *)){text-align:start} -*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: } - -/*! tailwindcss v3.4.17 | MIT License | https://tailwindcss.com*/*,:after,:before{border:0 solid #e5e7eb;box-sizing:border-box}:after,:before{--tw-content:""}:host,html{-webkit-text-size-adjust:100%;font-feature-settings:normal;-webkit-tap-highlight-color:transparent;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-variation-settings:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4}body{line-height:inherit;margin:0}hr{border-top-width:1px;color:inherit;height:0}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-feature-settings:normal;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em;font-variation-settings:normal}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{border-collapse:collapse;border-color:inherit;text-indent:0}button,input,optgroup,select,textarea{font-feature-settings:inherit;color:inherit;font-family:inherit;font-size:100%;font-variation-settings:inherit;font-weight:inherit;letter-spacing:inherit;line-height:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{color:#9ca3af;opacity:1}input::placeholder,textarea::placeholder{color:#9ca3af;opacity:1}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{height:auto;max-width:100%}[hidden]:where(:not([hidden=until-found])){display:none}.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}.prose{color:var(--tw-prose-body);max-width:96ch}.prose :where(p):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:1.25em;margin-top:1.25em}.prose :where([class~=lead]):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-lead);font-size:1.25em;line-height:1.6;margin-bottom:1.2em;margin-top:1.2em}.prose :where(a):not(:where([class~=not-prose],[class~=not-prose] *)){color:#5956eb;font-weight:500;text-decoration:none}.prose :where(a):not(:where([class~=not-prose],[class~=not-prose] *)):hover{color:#4f46e5}.prose :where(strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-bold);font-weight:600}.prose :where(a strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(blockquote strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(thead th strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(ol):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:decimal;margin-bottom:1.25em;margin-top:1.25em;padding-inline-start:1.625em}.prose :where(ol[type=A]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-alpha}.prose :where(ol[type=a]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-alpha}.prose :where(ol[type=A s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-alpha}.prose :where(ol[type=a s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-alpha}.prose :where(ol[type=I]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-roman}.prose :where(ol[type=i]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-roman}.prose :where(ol[type=I s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-roman}.prose :where(ol[type=i s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-roman}.prose :where(ol[type="1"]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:decimal}.prose :where(ul):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:disc;margin-bottom:1.25em;margin-top:1.25em;padding-inline-start:1.625em}.prose :where(ol>li):not(:where([class~=not-prose],[class~=not-prose] *))::marker{color:var(--tw-prose-counters);font-weight:400}.prose :where(ul>li):not(:where([class~=not-prose],[class~=not-prose] *))::marker{color:var(--tw-prose-bullets)}.prose :where(dt):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:600;margin-top:1.25em}.prose :where(hr):not(:where([class~=not-prose],[class~=not-prose] *)){border-color:var(--tw-prose-hr);border-top-width:1px;margin-bottom:3em;margin-top:3em}.prose :where(blockquote):not(:where([class~=not-prose],[class~=not-prose] *)){background-color:#80808020;border-inline-start-color:var(--tw-prose-quote-borders);border-inline-start-width:.25rem;border-left-color:#d1d5db;color:unset;font-style:unset;font-weight:500;line-height:1.25em;margin-bottom:1em;margin-top:1em;padding-bottom:.25em;padding-inline-start:1em;padding-left:.75em;padding-top:.25em;quotes:"\201C""\201D""\2018""\2019"}.prose :where(blockquote):not(:where([class~=not-prose],[class~=not-prose] *)) p{margin-bottom:.25em;margin-top:.25em;padding-right:.25em}.prose :where(blockquote):not(:where([class~=not-prose],[class~=not-prose] *)) p:before{content:unset}.prose :where(blockquote):not(:where([class~=not-prose],[class~=not-prose] *)) p:after{content:unset}.prose :where(blockquote p:first-of-type):not(:where([class~=not-prose],[class~=not-prose] *)):before{content:open-quote}.prose :where(blockquote p:last-of-type):not(:where([class~=not-prose],[class~=not-prose] *)):after{content:close-quote}.prose :where(h1):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);font-size:2.25em;font-weight:800;line-height:1.1111111;margin-bottom:.8888889em;margin-top:0}.prose :where(h1 strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-weight:900}.prose :where(h2):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);font-size:1.5em;font-weight:700;line-height:1.3333333;margin-bottom:.75em;margin-top:1.5em}.prose :where(h2 strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-weight:800}.prose :where(h3):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);font-size:1.25em;font-weight:600;line-height:1.6;margin-bottom:.6em;margin-top:1.6em}.prose :where(h3 strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-weight:700}.prose :where(h4):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:600;line-height:1.5;margin-bottom:.5em;margin-top:1.5em}.prose :where(h4 strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-weight:700}.prose :where(img):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:2em;margin-top:2em}.prose :where(picture):not(:where([class~=not-prose],[class~=not-prose] *)){display:block;margin-bottom:2em;margin-top:2em}.prose :where(video):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:2em;margin-top:2em}.prose :where(kbd):not(:where([class~=not-prose],[class~=not-prose] *)){border-radius:.3125rem;box-shadow:0 0 0 1px rgb(var(--tw-prose-kbd-shadows)/10%),0 3px 0 rgb(var(--tw-prose-kbd-shadows)/10%);color:var(--tw-prose-kbd);font-family:inherit;font-size:.875em;font-weight:500;padding-inline-end:.375em;padding-bottom:.1875em;padding-top:.1875em;padding-inline-start:.375em}.prose :where(code):not(:where([class~=not-prose],[class~=not-prose] *)){background-color:#80808033;border-radius:4px;color:var(--tw-prose-code);font-size:.875em;font-weight:600;font:unset;margin-left:-2px;margin-right:1px;padding-left:4px;padding-right:4px}.prose :where(code):not(:where([class~=not-prose],[class~=not-prose] *)):before{content:unset}.prose :where(code):not(:where([class~=not-prose],[class~=not-prose] *)):after{content:unset}.prose :where(a code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(h1 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(h2 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-size:.875em}.prose :where(h3 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-size:.9em}.prose :where(h4 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(blockquote code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(thead th code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(pre):not(:where([class~=not-prose],[class~=not-prose] *)){background-color:var(--tw-prose-pre-bg);border-radius:.375rem;color:var(--tw-prose-pre-code);font-size:.875em;font-weight:400;line-height:1.7142857;margin-bottom:1.7142857em;margin-top:1.7142857em;overflow-x:auto;padding-inline-end:1.1428571em;padding-bottom:.8571429em;padding-top:.8571429em;padding-inline-start:1.1428571em}.prose :where(pre):not(:where([class~=not-prose],[class~=not-prose] *)) code{font-family:Fira Code Regular,Consolas,Monospace,Courier New}.prose :where(pre code):not(:where([class~=not-prose],[class~=not-prose] *)){background-color:transparent;border-radius:0;border-width:0;color:inherit;font-family:inherit;font-size:inherit;font-weight:inherit;line-height:inherit;padding:0}.prose :where(pre code):not(:where([class~=not-prose],[class~=not-prose] *)):before{content:none}.prose :where(pre code):not(:where([class~=not-prose],[class~=not-prose] *)):after{content:none}.prose :where(table):not(:where([class~=not-prose],[class~=not-prose] *)){font-size:.875em;line-height:1.7142857;margin-bottom:2em;margin-top:2em;table-layout:auto;width:100%}.prose :where(thead):not(:where([class~=not-prose],[class~=not-prose] *)){border-bottom-color:var(--tw-prose-th-borders);border-bottom-width:1px}.prose :where(thead th):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:600;padding-inline-end:.5714286em;padding-bottom:.5714286em;padding-inline-start:.5714286em;vertical-align:bottom}.prose :where(tbody tr):not(:where([class~=not-prose],[class~=not-prose] *)){border-bottom-color:var(--tw-prose-td-borders);border-bottom-width:1px}.prose :where(tbody tr:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){border-bottom-width:0}.prose :where(tbody td):not(:where([class~=not-prose],[class~=not-prose] *)){vertical-align:baseline}.prose :where(tfoot):not(:where([class~=not-prose],[class~=not-prose] *)){border-top-color:var(--tw-prose-th-borders);border-top-width:1px}.prose :where(tfoot td):not(:where([class~=not-prose],[class~=not-prose] *)){vertical-align:top}.prose :where(th,td):not(:where([class~=not-prose],[class~=not-prose] *)){text-align:start}.prose :where(figure>*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:0;margin-top:0}.prose :where(figcaption):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-captions);font-size:.875em;line-height:1.4285714;margin-top:.8571429em}.prose{--tw-prose-body:#374151;--tw-prose-headings:#111827;--tw-prose-lead:#4b5563;--tw-prose-links:#111827;--tw-prose-bold:#111827;--tw-prose-counters:#6b7280;--tw-prose-bullets:#d1d5db;--tw-prose-hr:#e5e7eb;--tw-prose-quotes:#111827;--tw-prose-quote-borders:#e5e7eb;--tw-prose-captions:#6b7280;--tw-prose-kbd:#111827;--tw-prose-kbd-shadows:17 24 39;--tw-prose-code:#111827;--tw-prose-pre-code:#e5e7eb;--tw-prose-pre-bg:#1f2937;--tw-prose-th-borders:#d1d5db;--tw-prose-td-borders:#e5e7eb;--tw-prose-invert-body:#d1d5db;--tw-prose-invert-headings:#fff;--tw-prose-invert-lead:#9ca3af;--tw-prose-invert-links:#fff;--tw-prose-invert-bold:#fff;--tw-prose-invert-counters:#9ca3af;--tw-prose-invert-bullets:#4b5563;--tw-prose-invert-hr:#374151;--tw-prose-invert-quotes:#f3f4f6;--tw-prose-invert-quote-borders:#374151;--tw-prose-invert-captions:#9ca3af;--tw-prose-invert-kbd:#fff;--tw-prose-invert-kbd-shadows:255 255 255;--tw-prose-invert-code:#fff;--tw-prose-invert-pre-code:#d1d5db;--tw-prose-invert-pre-bg:rgba(0,0,0,.5);--tw-prose-invert-th-borders:#4b5563;--tw-prose-invert-td-borders:#374151;font-size:1rem;line-height:1.5em}.prose :where(picture>img):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:0;margin-top:0}.prose :where(li):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:.5em;margin-top:.5em}.prose :where(ol>li):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:.375em}.prose :where(ul>li):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:.375em}.prose :where(.prose>ul>li p):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:.75em;margin-top:.75em}.prose :where(.prose>ul>li>p:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em}.prose :where(.prose>ul>li>p:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:1.25em}.prose :where(.prose>ol>li>p:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em}.prose :where(.prose>ol>li>p:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:1.25em}.prose :where(ul ul,ul ol,ol ul,ol ol):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:.75em;margin-top:.75em}.prose :where(dl):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:1.25em;margin-top:1.25em}.prose :where(dd):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.5em;padding-inline-start:1.625em}.prose :where(hr+*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose :where(h2+*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose :where(h3+*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose :where(h4+*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose :where(thead th:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:0}.prose :where(thead th:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-end:0}.prose :where(tbody td,tfoot td):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-end:.5714286em;padding-bottom:.5714286em;padding-top:.5714286em;padding-inline-start:.5714286em}.prose :where(tbody td:first-child,tfoot td:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:0}.prose :where(tbody td:last-child,tfoot td:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-end:0}.prose :where(figure):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:2em;margin-top:2em}.prose :where(.prose>:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose :where(.prose>:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:0}.sr-only{clip:rect(0,0,0,0);border-width:0;height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;white-space:nowrap;width:1px}.visible{visibility:visible}.invisible{visibility:hidden}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.-left-64{left:-16rem}.bottom-0{bottom:0}.bottom-4{bottom:1rem}.left-0{left:0}.left-80{left:20rem}.right-0{right:0}.right-4{right:1rem}.top-0{top:0}.top-16{top:4rem}.top-4{top:1rem}.top-auto{top:auto}.z-10{z-index:10}.z-30{z-index:30}.z-40{z-index:40}.z-50{z-index:50}.float-right{float:right}.float-left{float:left}.m-2{margin:.5rem}.m-8{margin:2rem}.-mx-4{margin-left:-1rem;margin-right:-1rem}.mx-0{margin-left:0;margin-right:0}.mx-3{margin-left:.75rem;margin-right:.75rem}.mx-4{margin-left:1rem;margin-right:1rem}.mx-8{margin-left:2rem;margin-right:2rem}.mx-auto{margin-left:auto;margin-right:auto}.my-0{margin-bottom:0;margin-top:0}.my-1{margin-bottom:.25rem;margin-top:.25rem}.my-2{margin-bottom:.5rem;margin-top:.5rem}.my-3{margin-bottom:.75rem;margin-top:.75rem}.my-4{margin-bottom:1rem;margin-top:1rem}.my-8{margin-bottom:2rem;margin-top:2rem}.my-auto{margin-bottom:auto;margin-top:auto}.-mb-2{margin-bottom:-.5rem}.-ml-2{margin-left:-.5rem}.-ml-4{margin-left:-1rem}.-ml-6{margin-left:-1.5rem}.-ml-8{margin-left:-2rem}.mb-0{margin-bottom:0}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-8{margin-bottom:2rem}.ml-4{margin-left:1rem}.ml-auto{margin-left:auto}.mr-1{margin-right:.25rem}.mr-4{margin-right:1rem}.mr-auto{margin-right:auto}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-8{margin-top:2rem}.mt-auto{margin-top:auto}.block{display:block}.inline{display:inline}.flex{display:flex}.contents{display:contents}.hidden{display:none}.h-0{height:0}.h-1{height:.25rem}.h-16{height:4rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-8{height:2rem}.h-auto{height:auto}.h-full{height:100%}.h-screen{height:100vh}.max-h-\[75vh\]{max-height:75vh}.min-h-\[300px\]{min-height:300px}.min-h-\[calc\(100vh_-_4rem\)\]{min-height:calc(100vh - 4rem)}.min-h-screen{min-height:100vh}.w-1{width:.25rem}.w-16{width:4rem}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-64{width:16rem}.w-\[70ch\]{width:70ch}.w-fit{width:-moz-fit-content;width:fit-content}.w-full{width:100%}.w-screen{width:100vw}.max-w-3xl{max-width:48rem}.max-w-7xl{max-width:80rem}.max-w-\[1000px\]{max-width:1000px}.max-w-full{max-width:100%}.max-w-lg{max-width:32rem}.max-w-sm{max-width:24rem}.max-w-xs{max-width:20rem}.flex-shrink-0{flex-shrink:0}.flex-grow{flex-grow:1}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.cursor-auto{cursor:auto}.cursor-pointer{cursor:pointer}.list-none{list-style-type:none}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.overflow-x-hidden{overflow-x:hidden}.overflow-y-hidden{overflow-y:hidden}.whitespace-nowrap{white-space:nowrap}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.border-2{border-width:2px}.border-4{border-width:4px}.border-y{border-top-width:1px}.border-b,.border-y{border-bottom-width:1px}.border-b-4{border-bottom-width:4px}.border-l-4{border-left-width:4px}.border-l-\[0\.325rem\]{border-left-width:.325rem}.border-t{border-top-width:1px}.border-gray-200{--tw-border-opacity:1;border-color:rgb(229 231 235/var(--tw-border-opacity,1))}.border-gray-300{--tw-border-opacity:1;border-color:rgb(209 213 219/var(--tw-border-opacity,1))}.border-indigo-400{--tw-border-opacity:1;border-color:rgb(129 140 248/var(--tw-border-opacity,1))}.border-indigo-500{--tw-border-opacity:1;border-color:rgb(89 86 235/var(--tw-border-opacity,1))}.border-transparent{border-color:transparent}.border-yellow-400{--tw-border-opacity:1;border-color:rgb(250 204 21/var(--tw-border-opacity,1))}.bg-black{--tw-bg-opacity:1;background-color:rgb(0 0 0/var(--tw-bg-opacity,1))}.bg-black\/5{background-color:rgba(0,0,0,.05)}.bg-black\/50{background-color:rgba(0,0,0,.5)}.bg-gray-100{--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity,1))}.bg-gray-200{--tw-bg-opacity:1;background-color:rgb(229 231 235/var(--tw-bg-opacity,1))}.bg-gray-50{--tw-bg-opacity:1;background-color:rgb(249 250 251/var(--tw-bg-opacity,1))}.bg-slate-100{--tw-bg-opacity:1;background-color:rgb(241 245 249/var(--tw-bg-opacity,1))}.bg-transparent{background-color:transparent}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity,1))}.bg-gradient-to-br{background-image:linear-gradient(to bottom right,var(--tw-gradient-stops))}.bg-cover{background-size:cover}.bg-clip-text{-webkit-background-clip:text;background-clip:text}.bg-no-repeat{background-repeat:no-repeat}.fill-black{fill:#000}.fill-current{fill:currentColor}.p-12{padding:3rem}.p-2{padding:.5rem}.p-4{padding:1rem}.px-0{padding-left:0;padding-right:0}.px-1{padding-left:.25rem;padding-right:.25rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.px-8{padding-left:2rem;padding-right:2rem}.py-0{padding-bottom:0;padding-top:0}.py-1{padding-bottom:.25rem;padding-top:.25rem}.py-12{padding-bottom:3rem;padding-top:3rem}.py-16{padding-bottom:4rem;padding-top:4rem}.py-2{padding-bottom:.5rem;padding-top:.5rem}.py-24{padding-bottom:6rem;padding-top:6rem}.py-3{padding-bottom:.75rem;padding-top:.75rem}.py-32{padding-bottom:8rem;padding-top:8rem}.py-4{padding-bottom:1rem;padding-top:1rem}.py-8{padding-bottom:2rem;padding-top:2rem}.pb-12{padding-bottom:3rem}.pb-3{padding-bottom:.75rem}.pl-2{padding-left:.5rem}.pl-4{padding-left:1rem}.pl-5{padding-left:1.25rem}.pl-8{padding-left:2rem}.pt-3{padding-top:.75rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.font-sans{font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-5xl{font-size:3rem;line-height:1}.text-\[90\%\]{font-size:90%}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.font-black{font-weight:900}.font-bold{font-weight:700}.font-extrabold{font-weight:800}.font-light{font-weight:300}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.leading-10{line-height:2.5rem}.leading-4{line-height:1rem}.leading-8{line-height:2rem}.leading-normal{line-height:1.5}.leading-relaxed{line-height:1.625}.tracking-normal{letter-spacing:0}.tracking-tight{letter-spacing:-.025em}.tracking-wide{letter-spacing:.025em}.text-black{--tw-text-opacity:1;color:rgb(0 0 0/var(--tw-text-opacity,1))}.text-gray-100{--tw-text-opacity:1;color:rgb(243 244 246/var(--tw-text-opacity,1))}.text-gray-200{--tw-text-opacity:1;color:rgb(229 231 235/var(--tw-text-opacity,1))}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity,1))}.text-gray-700{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity,1))}.text-indigo-500{--tw-text-opacity:1;color:rgb(89 86 235/var(--tw-text-opacity,1))}.text-indigo-600{--tw-text-opacity:1;color:rgb(79 70 229/var(--tw-text-opacity,1))}.text-transparent{color:transparent}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.opacity-50{opacity:.5}.opacity-75{opacity:.75}.shadow-lg{--tw-shadow:0 10px 15px -3px rgba(0,0,0,.1),0 4px 6px -4px rgba(0,0,0,.1);--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color)}.shadow-lg,.shadow-md{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-md{--tw-shadow:0 4px 6px -1px rgba(0,0,0,.1),0 2px 4px -2px rgba(0,0,0,.1);--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color),0 2px 4px -2px var(--tw-shadow-color)}.drop-shadow-2xl{--tw-drop-shadow:drop-shadow(0 25px 25px rgba(0,0,0,.15));filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition-all{transition-duration:.15s;transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-colors{transition-duration:.15s;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1)}.duration-300{transition-duration:.3s}.duration-75{transition-duration:75ms}.ease-in-out{transition-timing-function:cubic-bezier(.4,0,.2,1)}[x-cloak]{display:none!important}.dark\:prose-invert:is(.dark *){--tw-prose-body:var(--tw-prose-invert-body);--tw-prose-headings:var(--tw-prose-invert-headings);--tw-prose-lead:var(--tw-prose-invert-lead);--tw-prose-links:var(--tw-prose-invert-links);--tw-prose-bold:var(--tw-prose-invert-bold);--tw-prose-counters:var(--tw-prose-invert-counters);--tw-prose-bullets:var(--tw-prose-invert-bullets);--tw-prose-hr:var(--tw-prose-invert-hr);--tw-prose-quotes:var(--tw-prose-invert-quotes);--tw-prose-quote-borders:var(--tw-prose-invert-quote-borders);--tw-prose-captions:var(--tw-prose-invert-captions);--tw-prose-kbd:var(--tw-prose-invert-kbd);--tw-prose-kbd-shadows:var(--tw-prose-invert-kbd-shadows);--tw-prose-code:var(--tw-prose-invert-code);--tw-prose-pre-code:var(--tw-prose-invert-pre-code);--tw-prose-pre-bg:var(--tw-prose-invert-pre-bg);--tw-prose-th-borders:var(--tw-prose-invert-th-borders);--tw-prose-td-borders:var(--tw-prose-invert-td-borders)}.dark\:prose-invert:is(.dark *) :where(a):not(:where([class~=not-prose],[class~=not-prose] *)){color:#818cf8}.dark\:prose-invert:is(.dark *) :where(a):not(:where([class~=not-prose],[class~=not-prose] *)):hover{color:#6366f1}.hover\:bg-black\/10:hover{background-color:rgba(0,0,0,.1)}.hover\:bg-black\/5:hover{background-color:rgba(0,0,0,.05)}.hover\:text-gray-700:hover{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity,1))}.hover\:text-gray-900:hover{--tw-text-opacity:1;color:rgb(17 24 39/var(--tw-text-opacity,1))}.hover\:underline:hover{text-decoration-line:underline}.hover\:opacity-100:hover{opacity:1}.focus\:not-sr-only:focus{clip:auto;height:auto;margin:0;overflow:visible;padding:0;position:static;white-space:normal;width:auto}.focus\:absolute:focus{position:absolute}.focus\:mx-auto:focus{margin-left:auto;margin-right:auto}.focus\:mt-2:focus{margin-top:.5rem}.focus\:w-64:focus{width:16rem}.focus\:p-2:focus{padding:.5rem}.group:hover .group-hover\:opacity-100{opacity:1}.prose-h1\:mb-3 :is(:where(h1):not(:where([class~=not-prose],[class~=not-prose] *))){margin-bottom:.75rem}.prose-p\:my-3 :is(:where(p):not(:where([class~=not-prose],[class~=not-prose] *))){margin-bottom:.75rem;margin-top:.75rem}.prose-img\:inline :is(:where(img):not(:where([class~=not-prose],[class~=not-prose] *))){display:inline}.dark\:block:is(.dark *){display:block}.dark\:hidden:is(.dark *){display:none}.dark\:border-\[\#1b2533\]:is(.dark *){--tw-border-opacity:1;border-color:rgb(27 37 51/var(--tw-border-opacity,1))}.dark\:border-gray-700:is(.dark *){--tw-border-opacity:1;border-color:rgb(55 65 81/var(--tw-border-opacity,1))}.dark\:bg-black\/10:is(.dark *){background-color:rgba(0,0,0,.1)}.dark\:bg-gray-700:is(.dark *){--tw-bg-opacity:1;background-color:rgb(55 65 81/var(--tw-bg-opacity,1))}.dark\:bg-gray-800:is(.dark *){--tw-bg-opacity:1;background-color:rgb(31 41 55/var(--tw-bg-opacity,1))}.dark\:bg-gray-900:is(.dark *){--tw-bg-opacity:1;background-color:rgb(17 24 39/var(--tw-bg-opacity,1))}.dark\:bg-white:is(.dark *){--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity,1))}.dark\:fill-gray-200:is(.dark *){fill:#e5e7eb}.dark\:fill-white:is(.dark *){fill:#fff}.dark\:font-medium:is(.dark *){font-weight:500}.dark\:text-gray-100:is(.dark *){--tw-text-opacity:1;color:rgb(243 244 246/var(--tw-text-opacity,1))}.dark\:text-gray-200:is(.dark *){--tw-text-opacity:1;color:rgb(229 231 235/var(--tw-text-opacity,1))}.dark\:text-indigo-400:is(.dark *){--tw-text-opacity:1;color:rgb(129 140 248/var(--tw-text-opacity,1))}.dark\:text-white:is(.dark *){--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.dark\:hover\:bg-black\/10:hover:is(.dark *){background-color:rgba(0,0,0,.1)}.dark\:hover\:text-white:hover:is(.dark *){--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.group:hover .dark\:group-hover\:text-white:is(.dark *){--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}@media (min-width:640px){.sm\:mb-0{margin-bottom:0}.sm\:mt-4{margin-top:1rem}.sm\:flex{display:flex}.sm\:leading-none{line-height:1}.sm\:shadow-xl{--tw-shadow:0 20px 25px -5px rgba(0,0,0,.1),0 8px 10px -6px rgba(0,0,0,.1);--tw-shadow-colored:0 20px 25px -5px var(--tw-shadow-color),0 8px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}}@media (min-width:768px){.md\:visible{visibility:visible}.md\:left-0{left:0}.md\:left-64{left:16rem}.md\:top-0{top:0}.md\:mx-2{margin-left:.5rem;margin-right:.5rem}.md\:my-0{margin-bottom:0;margin-top:0}.md\:my-6{margin-bottom:1.5rem;margin-top:1.5rem}.md\:mb-12{margin-bottom:3rem}.md\:ml-0{margin-left:0}.md\:mt-0{margin-top:0}.md\:mt-8{margin-top:2rem}.md\:block{display:block}.md\:inline-block{display:inline-block}.md\:flex{display:flex}.md\:hidden{display:none}.md\:min-h-screen{min-height:100vh}.md\:w-1\/2{width:50%}.md\:w-\[calc\(100vw_-_16rem\)\]{width:calc(100vw - 16rem)}.md\:w-auto{width:auto}.md\:max-w-2xl{max-width:42rem}.md\:max-w-none{max-width:none}.md\:flex-grow{flex-grow:1}.md\:flex-grow-0{flex-grow:0}.md\:items-center{align-items:center}.md\:border-none{border-style:none}.md\:bg-transparent{background-color:transparent}.md\:bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity,1))}.md\:bg-left{background-position:0}.md\:px-16{padding-left:4rem;padding-right:4rem}.md\:py-0{padding-bottom:0;padding-top:0}.md\:py-16{padding-bottom:4rem;padding-top:4rem}.md\:pb-0{padding-bottom:0}.md\:pl-0{padding-left:0}.md\:text-center{text-align:center}.md\:text-3xl{font-size:1.875rem;line-height:2.25rem}.md\:text-4xl{font-size:2.25rem;line-height:2.5rem}.md\:text-5xl{font-size:3rem;line-height:1}.md\:text-6xl{font-size:3.75rem;line-height:1}.md\:shadow-none{--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.dark\:md\:bg-transparent:is(.dark *){background-color:transparent}}@media (min-width:1024px){.lg\:mb-12{margin-bottom:3rem}.lg\:ml-8{margin-left:2rem}.lg\:bg-center{background-position:50%}.lg\:text-5xl{font-size:3rem;line-height:1}.lg\:text-7xl{font-size:4.5rem;line-height:1}.lg\:text-lg{font-size:1.125rem;line-height:1.75rem}}@media (min-width:1280px){.xl\:mb-16{margin-bottom:4rem}}@media print{.print\:top-0{top:0}.print\:hidden{display:none}} +/*! tailwindcss v4.1.4 | MIT License | https://tailwindcss.com */@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-outline-style:solid;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-duration:initial;--tw-ease:initial}}}@layer base{pre code.torchlight .line-number,pre code.torchlight .summary-caret{margin-right:calc(var(--spacing)*4)}.prose .torchlight-link,.torchlight-link{text-decoration-line:underline}.torchlight.has-focus-lines .line:not(.line-focus){filter:blur(.095rem);opacity:.65;transition:filter .35s,opacity .35s}.torchlight.has-focus-lines:hover .line:not(.line-focus){filter:blur();opacity:1}.torchlight summary:focus{--tw-outline-style:none;outline-style:none}.torchlight details>summary::marker{display:none}.torchlight details>summary::-webkit-details-marker{display:none}.torchlight details .summary-caret:after{pointer-events:none}.torchlight .summary-caret-empty:after,.torchlight details .summary-caret-middle:after,.torchlight details .summary-caret-end:after{content:" "}.torchlight details[open] .summary-caret-start:after{content:"-"}.torchlight details:not([open]) .summary-caret-start:after{content:"+"}.torchlight details[open] .summary-hide-when-open{display:none}.torchlight details:not([open]) .summary-hide-when-open{display:block}*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;-moz-tab-size:4;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab,red,red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){-webkit-appearance:button;-moz-appearance:button;appearance:button}::file-selector-button{-webkit-appearance:button;-moz-appearance:button;appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer theme{:root,:host{--font-sans:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--color-red-500:oklch(63.7% .237 25.331);--color-red-600:oklch(57.7% .245 27.325);--color-amber-500:oklch(76.9% .188 70.08);--color-yellow-300:oklch(90.5% .182 98.111);--color-yellow-400:oklch(85.2% .199 91.936);--color-yellow-500:oklch(79.5% .184 86.047);--color-green-500:oklch(72.3% .219 149.579);--color-blue-500:oklch(62.3% .214 259.815);--color-indigo-400:oklch(67.3% .182 276.935);--color-indigo-600:oklch(51.1% .262 276.966);--color-indigo-700:oklch(45.7% .24 277.023);--color-slate-100:oklch(96.8% .007 247.896);--color-slate-800:oklch(27.9% .041 260.031);--color-gray-50:oklch(98.5% .002 247.839);--color-gray-100:oklch(96.7% .003 264.542);--color-gray-200:oklch(92.8% .006 264.531);--color-gray-300:oklch(87.2% .01 258.338);--color-gray-400:oklch(70.7% .022 261.325);--color-gray-500:oklch(55.1% .027 264.364);--color-gray-600:oklch(44.6% .03 256.802);--color-gray-700:oklch(37.3% .034 259.733);--color-gray-800:oklch(27.8% .033 256.848);--color-gray-900:oklch(21% .034 264.665);--color-black:#000;--color-white:#fff;--spacing:.25rem;--container-sm:24rem;--container-lg:32rem;--container-xl:36rem;--container-2xl:42rem;--container-3xl:48rem;--container-7xl:80rem;--text-sm:.875rem;--text-sm--line-height:calc(1.25/.875);--text-base:1rem;--text-base--line-height: 1.5 ;--text-lg:1.125rem;--text-lg--line-height:calc(1.75/1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75/1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2/1.5);--text-3xl:1.875rem;--text-3xl--line-height: 1.2 ;--text-4xl:2.25rem;--text-4xl--line-height:calc(2.5/2.25);--text-5xl:3rem;--text-5xl--line-height:1;--text-6xl:3.75rem;--text-6xl--line-height:1;--text-7xl:4.5rem;--text-7xl--line-height:1;--font-weight-light:300;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--font-weight-extrabold:800;--font-weight-black:900;--tracking-tight:-.025em;--tracking-normal:0em;--tracking-wide:.025em;--leading-normal:1.5;--leading-relaxed:1.625;--radius-sm:.25rem;--radius-md:.375rem;--radius-lg:.5rem;--drop-shadow-2xl:0 25px 25px #00000026;--ease-out:cubic-bezier(0,0,.2,1);--ease-in-out:cubic-bezier(.4,0,.2,1);--animate-spin:spin 1s linear infinite;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4,0,.2,1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer components;@layer utilities{.invisible{visibility:hidden}.visible{visibility:visible}.sr-only{clip:rect(0,0,0,0);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.sticky{position:sticky}.inset-0{inset:calc(var(--spacing)*0)}.end-0{inset-inline-end:calc(var(--spacing)*0)}.-top-1{top:calc(var(--spacing)*-1)}.top-0{top:calc(var(--spacing)*0)}.top-2\.5{top:calc(var(--spacing)*2.5)}.top-4{top:calc(var(--spacing)*4)}.top-16{top:calc(var(--spacing)*16)}.top-auto{top:auto}.right-0{right:calc(var(--spacing)*0)}.right-1{right:calc(var(--spacing)*1)}.right-3{right:calc(var(--spacing)*3)}.right-4{right:calc(var(--spacing)*4)}.bottom-0{bottom:calc(var(--spacing)*0)}.bottom-4{bottom:calc(var(--spacing)*4)}.-left-64{left:calc(var(--spacing)*-64)}.left-0{left:calc(var(--spacing)*0)}.z-10{z-index:10}.z-30{z-index:30}.z-40{z-index:40}.z-50{z-index:50}.col-12{grid-column:12}.col-auto{grid-column:auto}.float-end{float:inline-end}.float-left{float:left}.float-right{float:right}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.m-1{margin:calc(var(--spacing)*1)}.m-2{margin:calc(var(--spacing)*2)}.m-8{margin:calc(var(--spacing)*8)}.-mx-4{margin-inline:calc(var(--spacing)*-4)}.mx-0{margin-inline:calc(var(--spacing)*0)}.mx-0\.5{margin-inline:calc(var(--spacing)*.5)}.mx-3{margin-inline:calc(var(--spacing)*3)}.mx-4{margin-inline:calc(var(--spacing)*4)}.mx-8{margin-inline:calc(var(--spacing)*8)}.mx-auto{margin-inline:auto}.my-0{margin-block:calc(var(--spacing)*0)}.my-0\.5{margin-block:calc(var(--spacing)*.5)}.my-1{margin-block:calc(var(--spacing)*1)}.my-2{margin-block:calc(var(--spacing)*2)}.my-3{margin-block:calc(var(--spacing)*3)}.my-4{margin-block:calc(var(--spacing)*4)}.my-8{margin-block:calc(var(--spacing)*8)}.my-auto{margin-block:auto}.me-0{margin-inline-end:calc(var(--spacing)*0)}.me-2{margin-inline-end:calc(var(--spacing)*2)}.me-auto{margin-inline-end:auto}.prose{color:var(--tw-prose-body);--tw-prose-body:oklch(37.3% .034 259.733);--tw-prose-headings:oklch(21% .034 264.665);--tw-prose-lead:oklch(44.6% .03 256.802);--tw-prose-links:oklch(21% .034 264.665);--tw-prose-bold:oklch(21% .034 264.665);--tw-prose-counters:oklch(55.1% .027 264.364);--tw-prose-bullets:oklch(87.2% .01 258.338);--tw-prose-hr:oklch(92.8% .006 264.531);--tw-prose-quotes:oklch(21% .034 264.665);--tw-prose-quote-borders:oklch(92.8% .006 264.531);--tw-prose-captions:oklch(55.1% .027 264.364);--tw-prose-kbd:oklch(21% .034 264.665);--tw-prose-kbd-shadows:NaN NaN NaN;--tw-prose-code:oklch(21% .034 264.665);--tw-prose-pre-code:oklch(92.8% .006 264.531);--tw-prose-pre-bg:oklch(27.8% .033 256.848);--tw-prose-th-borders:oklch(87.2% .01 258.338);--tw-prose-td-borders:oklch(92.8% .006 264.531);--tw-prose-invert-body:oklch(87.2% .01 258.338);--tw-prose-invert-headings:#fff;--tw-prose-invert-lead:oklch(70.7% .022 261.325);--tw-prose-invert-links:#fff;--tw-prose-invert-bold:#fff;--tw-prose-invert-counters:oklch(70.7% .022 261.325);--tw-prose-invert-bullets:oklch(44.6% .03 256.802);--tw-prose-invert-hr:oklch(37.3% .034 259.733);--tw-prose-invert-quotes:oklch(96.7% .003 264.542);--tw-prose-invert-quote-borders:oklch(37.3% .034 259.733);--tw-prose-invert-captions:oklch(70.7% .022 261.325);--tw-prose-invert-kbd:#fff;--tw-prose-invert-kbd-shadows:255 255 255;--tw-prose-invert-code:#fff;--tw-prose-invert-pre-code:oklch(87.2% .01 258.338);--tw-prose-invert-pre-bg:#00000080;--tw-prose-invert-th-borders:oklch(44.6% .03 256.802);--tw-prose-invert-td-borders:oklch(37.3% .034 259.733);max-width:96ch;font-size:1rem;line-height:1.5em}.prose :where(p):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em;margin-bottom:1.25em}.prose :where([class~=lead]):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-lead);margin-top:1.2em;margin-bottom:1.2em;font-size:1.25em;line-height:1.6}.prose :where(a):not(:where([class~=not-prose],[class~=not-prose] *)){color:#5956eb;font-weight:500;text-decoration:none}.prose :where(a):not(:where([class~=not-prose],[class~=not-prose] *)):hover{color:#4f46e5}.prose :where(strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-bold);font-weight:600}.prose :where(a strong):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(blockquote strong):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(thead th strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(ol):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em;margin-bottom:1.25em;padding-inline-start:1.625em;list-style-type:decimal}.prose :where(ol[type=A]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-alpha}.prose :where(ol[type=a]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-alpha}.prose :where(ol[type=A s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-alpha}.prose :where(ol[type=a s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-alpha}.prose :where(ol[type=I]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-roman}.prose :where(ol[type=i]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-roman}.prose :where(ol[type=I s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-roman}.prose :where(ol[type=i s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-roman}.prose :where(ol[type="1"]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:decimal}.prose :where(ul):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em;margin-bottom:1.25em;padding-inline-start:1.625em;list-style-type:disc}.prose :where(ol>li):not(:where([class~=not-prose],[class~=not-prose] *))::marker{color:var(--tw-prose-counters);font-weight:400}.prose :where(ul>li):not(:where([class~=not-prose],[class~=not-prose] *))::marker{color:var(--tw-prose-bullets)}.prose :where(dt):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);margin-top:1.25em;font-weight:600}.prose :where(hr):not(:where([class~=not-prose],[class~=not-prose] *)){border-color:var(--tw-prose-hr);border-top-width:1px;margin-top:3em;margin-bottom:3em}.prose :where(blockquote):not(:where([class~=not-prose],[class~=not-prose] *)){font-weight:500;font-style:unset;color:unset;border-inline-start-width:.25rem;border-inline-start-color:var(--tw-prose-quote-borders);quotes:"“""”""‘""’";padding-inline-start:1em;background-color:#80808020;border-left-color:#d1d5db;margin-top:1em;margin-bottom:1em;padding-top:.25em;padding-bottom:.25em;padding-left:.75em;line-height:1.25em}.prose :where(blockquote):not(:where([class~=not-prose],[class~=not-prose] *)) p{margin-top:.25em;margin-bottom:.25em;padding-right:.25em}.prose :where(blockquote):not(:where([class~=not-prose],[class~=not-prose] *)) p:before,.prose :where(blockquote):not(:where([class~=not-prose],[class~=not-prose] *)) p:after{content:unset}.prose :where(blockquote p:first-of-type):not(:where([class~=not-prose],[class~=not-prose] *)):before{content:open-quote}.prose :where(blockquote p:last-of-type):not(:where([class~=not-prose],[class~=not-prose] *)):after{content:close-quote}.prose :where(h1):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);margin-top:0;margin-bottom:.888889em;font-size:2.25em;font-weight:800;line-height:1.11111}.prose :where(h1 strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-weight:900}.prose :where(h2):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);margin-top:1.5em;margin-bottom:.75em;font-size:1.5em;font-weight:700;line-height:1.33333}.prose :where(h2 strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-weight:800}.prose :where(h3):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);margin-top:1.6em;margin-bottom:.6em;font-size:1.25em;font-weight:600;line-height:1.6}.prose :where(h3 strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-weight:700}.prose :where(h4):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);margin-top:1.5em;margin-bottom:.5em;font-weight:600;line-height:1.5}.prose :where(h4 strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-weight:700}.prose :where(img):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:2em;margin-bottom:2em}.prose :where(picture):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:2em;margin-bottom:2em;display:block}.prose :where(video):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:2em;margin-bottom:2em}.prose :where(kbd):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-kbd);box-shadow:0 0 0 1px rgb(var(--tw-prose-kbd-shadows)/10%),0 3px rgb(var(--tw-prose-kbd-shadows)/10%);padding-top:.1875em;padding-inline-end:.375em;padding-bottom:.1875em;border-radius:.3125rem;padding-inline-start:.375em;font-family:inherit;font-size:.875em;font-weight:500}.prose :where(code):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-code);font-size:.875em;font-weight:600}.prose :where(code):not(:where([class~=not-prose],[class~=not-prose] *)):before,.prose :where(code):not(:where([class~=not-prose],[class~=not-prose] *)):after{content:unset}.prose :where(a code):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(h1 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(h2 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-size:.875em}.prose :where(h3 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-size:.9em}.prose :where(h4 code):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(blockquote code):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(thead th code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(pre):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-pre-code);padding-top:.857143em;padding-inline-end:1.14286em;padding-bottom:.857143em;background-color:#292d3e;border-radius:.25rem;margin-top:1rem;margin-bottom:1rem;padding-inline-start:1.14286em;font-size:.875em;font-weight:400;line-height:1.71429;overflow-x:auto}.prose :where(pre):not(:where([class~=not-prose],[class~=not-prose] *)) code{font-family:Fira Code Regular,Consolas,monospace,Courier New}.prose :where(pre code):not(:where([class~=not-prose],[class~=not-prose] *)){font-weight:inherit;color:inherit;font-size:inherit;font-family:inherit;line-height:inherit;background-color:#0000;border-width:0;border-radius:0;padding:0}.prose :where(pre code):not(:where([class~=not-prose],[class~=not-prose] *)):before,.prose :where(pre code):not(:where([class~=not-prose],[class~=not-prose] *)):after{content:none}.prose :where(table):not(:where([class~=not-prose],[class~=not-prose] *)){table-layout:auto;width:100%;margin-top:2em;margin-bottom:2em;font-size:.875em;line-height:1.71429}.prose :where(thead):not(:where([class~=not-prose],[class~=not-prose] *)){border-bottom-width:1px;border-bottom-color:var(--tw-prose-th-borders)}.prose :where(thead th):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);vertical-align:bottom;padding-inline-end:.571429em;padding-bottom:.571429em;padding-inline-start:.571429em;font-weight:600}.prose :where(tbody tr):not(:where([class~=not-prose],[class~=not-prose] *)){border-bottom-width:1px;border-bottom-color:var(--tw-prose-td-borders)}.prose :where(tbody tr:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){border-bottom-width:0}.prose :where(tbody td):not(:where([class~=not-prose],[class~=not-prose] *)){vertical-align:baseline}.prose :where(tfoot):not(:where([class~=not-prose],[class~=not-prose] *)){border-top-width:1px;border-top-color:var(--tw-prose-th-borders)}.prose :where(tfoot td):not(:where([class~=not-prose],[class~=not-prose] *)){vertical-align:top}.prose :where(th,td):not(:where([class~=not-prose],[class~=not-prose] *)){text-align:start}.prose :where(figure>*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0;margin-bottom:0}.prose :where(figcaption):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-captions);margin-top:.857143em;font-size:.875em;line-height:1.42857}.prose :where(picture>img):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0;margin-bottom:0}.prose :where(li):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.5em;margin-bottom:.5em}.prose :where(ol>li):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(ul>li):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:.375em}.prose :where(.prose>ul>li p):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.75em;margin-bottom:.75em}.prose :where(.prose>ul>li>p:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em}.prose :where(.prose>ul>li>p:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:1.25em}.prose :where(.prose>ol>li>p:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em}.prose :where(.prose>ol>li>p:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:1.25em}.prose :where(ul ul,ul ol,ol ul,ol ol):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.75em;margin-bottom:.75em}.prose :where(dl):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em;margin-bottom:1.25em}.prose :where(dd):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.5em;padding-inline-start:1.625em}.prose :where(hr+*):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(h2+*):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(h3+*):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(h4+*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose :where(thead th:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:0}.prose :where(thead th:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-end:0}.prose :where(tbody td,tfoot td):not(:where([class~=not-prose],[class~=not-prose] *)){padding-top:.571429em;padding-inline-end:.571429em;padding-bottom:.571429em;padding-inline-start:.571429em}.prose :where(tbody td:first-child,tfoot td:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:0}.prose :where(tbody td:last-child,tfoot td:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-end:0}.prose :where(figure):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:2em;margin-bottom:2em}.prose :where(.prose>:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose :where(.prose>:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:0}.prose :where(code:not(pre code)):not(:where([class~=not-prose],[class~=not-prose] *)){font:unset;vertical-align:top;word-break:break-all;background-color:#80808033;border-radius:4px;max-width:80vw;margin-left:-2px;margin-right:1px;padding-left:4px;padding-right:4px;overflow-x:auto}.-mt-4{margin-top:calc(var(--spacing)*-4)}.mt-1{margin-top:calc(var(--spacing)*1)}.mt-2{margin-top:calc(var(--spacing)*2)}.mt-3{margin-top:calc(var(--spacing)*3)}.mt-4{margin-top:calc(var(--spacing)*4)}.mt-6{margin-top:calc(var(--spacing)*6)}.mt-8{margin-top:calc(var(--spacing)*8)}.mt-12{margin-top:calc(var(--spacing)*12)}.mt-auto{margin-top:auto}.mr-1{margin-right:calc(var(--spacing)*1)}.mr-3{margin-right:calc(var(--spacing)*3)}.mr-4{margin-right:calc(var(--spacing)*4)}.mr-auto{margin-right:auto}.mb-0{margin-bottom:calc(var(--spacing)*0)}.mb-1{margin-bottom:calc(var(--spacing)*1)}.mb-2{margin-bottom:calc(var(--spacing)*2)}.mb-3{margin-bottom:calc(var(--spacing)*3)}.mb-4{margin-bottom:calc(var(--spacing)*4)}.mb-8{margin-bottom:calc(var(--spacing)*8)}.mb-12{margin-bottom:calc(var(--spacing)*12)}.mb-auto{margin-bottom:auto}.-ml-2{margin-left:calc(var(--spacing)*-2)}.-ml-4{margin-left:calc(var(--spacing)*-4)}.-ml-6{margin-left:calc(var(--spacing)*-6)}.-ml-8{margin-left:calc(var(--spacing)*-8)}.ml-1{margin-left:calc(var(--spacing)*1)}.ml-3{margin-left:calc(var(--spacing)*3)}.ml-4{margin-left:calc(var(--spacing)*4)}.ml-auto{margin-left:auto}.block{display:block}.block\!{display:block!important}.contents{display:contents}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.table{display:table}.h-0\.5{height:calc(var(--spacing)*.5)}.h-1{height:calc(var(--spacing)*1)}.h-4{height:calc(var(--spacing)*4)}.h-5{height:calc(var(--spacing)*5)}.h-6{height:calc(var(--spacing)*6)}.h-8{height:calc(var(--spacing)*8)}.h-16{height:calc(var(--spacing)*16)}.h-24{height:calc(var(--spacing)*24)}.h-auto{height:auto}.h-full{height:100%}.h-screen{height:100vh}.max-h-\[60vh\]{max-height:60vh}.max-h-\[75vh\]{max-height:75vh}.min-h-\[300px\]{min-height:300px}.min-h-\[calc\(100vh_-_4rem\)\]{min-height:calc(100vh - 4rem)}.min-h-screen{min-height:100vh}.w-5{width:calc(var(--spacing)*5)}.w-6{width:calc(var(--spacing)*6)}.w-8{width:calc(var(--spacing)*8)}.w-16{width:calc(var(--spacing)*16)}.w-64{width:calc(var(--spacing)*64)}.w-80{width:calc(var(--spacing)*80)}.w-96{width:calc(var(--spacing)*96)}.w-100{width:calc(var(--spacing)*100)}.w-\[70ch\]{width:70ch}.w-auto{width:auto}.w-fit{width:fit-content}.w-full{width:100%}.w-screen{width:100vw}.max-w-3xl{max-width:var(--container-3xl)}.max-w-7xl{max-width:var(--container-7xl)}.max-w-\[1000px\]{max-width:1000px}.max-w-full{max-width:100%}.max-w-lg{max-width:var(--container-lg)}.max-w-sm{max-width:var(--container-sm)}.min-w-screen{min-width:100vw}.flex-shrink{flex-shrink:1}.shrink-0{flex-shrink:0}.flex-grow,.flex-grow-1,.grow{flex-grow:1}.table-auto{table-layout:auto}.table-fixed{table-layout:fixed}.border-collapse{border-collapse:collapse}.border-separate{border-collapse:separate}.origin-center{transform-origin:50%}.-rotate-45{rotate:-45deg}.rotate-45{rotate:45deg}.transform{transform:var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,)}.animate-spin{animation:var(--animate-spin)}.cursor-auto{cursor:auto}.cursor-pointer{cursor:pointer}.scroll-mt-2{scroll-margin-top:calc(var(--spacing)*2)}.list-none{list-style-type:none}.appearance-none{-webkit-appearance:none;-moz-appearance:none;appearance:none}.flex-col{flex-direction:column}.flex-row{flex-direction:row}.flex-nowrap{flex-wrap:nowrap}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.gap-4{gap:calc(var(--spacing)*4)}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*4)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*4)*calc(1 - var(--tw-space-y-reverse)))}:where(.divide-y>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px*var(--tw-divide-y-reverse));border-bottom-width:calc(1px*calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-gray-300>:not(:last-child)){border-color:var(--color-gray-300)}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-hidden{overflow-x:hidden}.overflow-y-auto{overflow-y:auto}.overflow-y-hidden{overflow-y:hidden}.scroll-smooth{scroll-behavior:smooth}.rounded{border-radius:.25rem}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-sm{border-radius:var(--radius-sm)}.rounded-t{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.rounded-l{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.rounded-tl{border-top-left-radius:.25rem}.rounded-r{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.rounded-tr{border-top-right-radius:.25rem}.rounded-b{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.rounded-br{border-bottom-right-radius:.25rem}.rounded-bl{border-bottom-left-radius:.25rem}.border{border-style:var(--tw-border-style);border-width:1px}.border-2{border-style:var(--tw-border-style);border-width:2px}.border-4{border-style:var(--tw-border-style);border-width:4px}.border-x{border-inline-style:var(--tw-border-style);border-inline-width:1px}.border-y{border-block-style:var(--tw-border-style);border-block-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-l-4{border-left-style:var(--tw-border-style);border-left-width:4px}.border-l-\[0\.325rem\]{border-left-style:var(--tw-border-style);border-left-width:.325rem}.border-dashed{--tw-border-style:dashed;border-style:dashed}.border-dotted{--tw-border-style:dotted;border-style:dotted}.border-double{--tw-border-style:double;border-style:double}.border-none{--tw-border-style:none;border-style:none}.border-solid{--tw-border-style:solid;border-style:solid}.border-amber-500{border-color:var(--color-amber-500)}.border-blue-500{border-color:var(--color-blue-500)}.border-gray-200{border-color:var(--color-gray-200)}.border-gray-300{border-color:var(--color-gray-300)}.border-gray-500{border-color:var(--color-gray-500)}.border-gray-900{border-color:var(--color-gray-900)}.border-green-500{border-color:var(--color-green-500)}.border-indigo-500{border-color:#5956eb}.border-red-600{border-color:var(--color-red-600)}.border-transparent{border-color:#0000}.border-yellow-400{border-color:var(--color-yellow-400)}.border-t-transparent{border-top-color:#0000}.border-r-gray-900{border-right-color:var(--color-gray-900)}.bg-black{background-color:var(--color-black)}.bg-black\/5{background-color:#0000000d}@supports (color:color-mix(in lab,red,red)){.bg-black\/5{background-color:color-mix(in oklab,var(--color-black)5%,transparent)}}.bg-black\/50{background-color:#00000080}@supports (color:color-mix(in lab,red,red)){.bg-black\/50{background-color:color-mix(in oklab,var(--color-black)50%,transparent)}}.bg-current{background-color:currentColor}.bg-gray-50{background-color:var(--color-gray-50)}.bg-gray-100{background-color:var(--color-gray-100)}.bg-gray-200{background-color:var(--color-gray-200)}.bg-gray-500{background-color:var(--color-gray-500)}.bg-indigo-600{background-color:var(--color-indigo-600)}.bg-slate-100{background-color:var(--color-slate-100)}.bg-transparent{background-color:#0000}.bg-white{background-color:var(--color-white)}.bg-yellow-400{background-color:var(--color-yellow-400)}.bg-linear-to-br{--tw-gradient-position:to bottom right;background-image:linear-gradient(var(--tw-gradient-stops))}@supports (background-image:linear-gradient(in lab,red,red)){.bg-linear-to-br{--tw-gradient-position:to bottom right in oklab}}.bg-cover{background-size:cover}.bg-clip-text{-webkit-background-clip:text;background-clip:text}.bg-no-repeat{background-repeat:no-repeat}.fill-black{fill:var(--color-black)}.fill-current{fill:currentColor}.p-0{padding:calc(var(--spacing)*0)}.p-2{padding:calc(var(--spacing)*2)}.p-3{padding:calc(var(--spacing)*3)}.p-4{padding:calc(var(--spacing)*4)}.p-8{padding:calc(var(--spacing)*8)}.p-12{padding:calc(var(--spacing)*12)}.px-0{padding-inline:calc(var(--spacing)*0)}.px-1{padding-inline:calc(var(--spacing)*1)}.px-2{padding-inline:calc(var(--spacing)*2)}.px-3{padding-inline:calc(var(--spacing)*3)}.px-4{padding-inline:calc(var(--spacing)*4)}.px-5{padding-inline:calc(var(--spacing)*5)}.px-6{padding-inline:calc(var(--spacing)*6)}.px-8{padding-inline:calc(var(--spacing)*8)}.py-0{padding-block:calc(var(--spacing)*0)}.py-1{padding-block:calc(var(--spacing)*1)}.py-2{padding-block:calc(var(--spacing)*2)}.py-3{padding-block:calc(var(--spacing)*3)}.py-4{padding-block:calc(var(--spacing)*4)}.py-5{padding-block:calc(var(--spacing)*5)}.py-6{padding-block:calc(var(--spacing)*6)}.py-8{padding-block:calc(var(--spacing)*8)}.py-12{padding-block:calc(var(--spacing)*12)}.py-16{padding-block:calc(var(--spacing)*16)}.py-24{padding-block:calc(var(--spacing)*24)}.py-32{padding-block:calc(var(--spacing)*32)}.ps-0{padding-inline-start:calc(var(--spacing)*0)}.ps-2{padding-inline-start:calc(var(--spacing)*2)}.ps-3{padding-inline-start:calc(var(--spacing)*3)}.pe-0{padding-inline-end:calc(var(--spacing)*0)}.pe-2{padding-inline-end:calc(var(--spacing)*2)}.pe-4{padding-inline-end:calc(var(--spacing)*4)}.pt-3{padding-top:calc(var(--spacing)*3)}.pt-4{padding-top:calc(var(--spacing)*4)}.pt-8{padding-top:calc(var(--spacing)*8)}.pt-10{padding-top:calc(var(--spacing)*10)}.pb-2{padding-bottom:calc(var(--spacing)*2)}.pb-3{padding-bottom:calc(var(--spacing)*3)}.pb-4{padding-bottom:calc(var(--spacing)*4)}.pb-8{padding-bottom:calc(var(--spacing)*8)}.pb-12{padding-bottom:calc(var(--spacing)*12)}.pl-2{padding-left:calc(var(--spacing)*2)}.pl-4{padding-left:calc(var(--spacing)*4)}.pl-5{padding-left:calc(var(--spacing)*5)}.pl-8{padding-left:calc(var(--spacing)*8)}.text-center{text-align:center}.text-end{text-align:end}.text-left{text-align:left}.text-right{text-align:right}.font-mono{font-family:var(--font-mono)}.font-sans{font-family:var(--font-sans)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.text-4xl{font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height))}.text-5xl{font-size:var(--text-5xl);line-height:var(--tw-leading,var(--text-5xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-\[75\%\]{font-size:75%}.text-\[90\%\]{font-size:90%}.leading-7{--tw-leading:calc(var(--spacing)*7);line-height:calc(var(--spacing)*7)}.leading-8{--tw-leading:calc(var(--spacing)*8);line-height:calc(var(--spacing)*8)}.leading-10{--tw-leading:calc(var(--spacing)*10);line-height:calc(var(--spacing)*10)}.leading-normal{--tw-leading:var(--leading-normal);line-height:var(--leading-normal)}.leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.font-black{--tw-font-weight:var(--font-weight-black);font-weight:var(--font-weight-black)}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-extrabold{--tw-font-weight:var(--font-weight-extrabold);font-weight:var(--font-weight-extrabold)}.font-light{--tw-font-weight:var(--font-weight-light);font-weight:var(--font-weight-light)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-normal{--tw-tracking:var(--tracking-normal);letter-spacing:var(--tracking-normal)}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.text-nowrap{text-wrap:nowrap}.break-all{word-break:break-all}.whitespace-nowrap{white-space:nowrap}.text-black{color:var(--color-black)}.text-blue-500{color:var(--color-blue-500)}.text-gray-100{color:var(--color-gray-100)}.text-gray-200{color:var(--color-gray-200)}.text-gray-400{color:var(--color-gray-400)}.text-gray-500{color:var(--color-gray-500)}.text-gray-600{color:var(--color-gray-600)}.text-gray-700{color:var(--color-gray-700)}.text-green-500{color:var(--color-green-500)}.text-indigo-500{color:#5956eb}.text-indigo-600{color:var(--color-indigo-600)}.text-red-500{color:var(--color-red-500)}.text-transparent{color:#0000}.text-white{color:var(--color-white)}.text-yellow-500{color:var(--color-yellow-500)}.capitalize{text-transform:capitalize}.lowercase{text-transform:lowercase}.uppercase{text-transform:uppercase}.italic{font-style:italic}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.opacity-0{opacity:0}.opacity-50{opacity:.5}.opacity-75{opacity:.75}.opacity-80{opacity:.8}.shadow{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a),0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-md{--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a),0 2px 4px -2px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-none{--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-xl{--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a),0 8px 10px -6px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring-1{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(1px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring-gray-900\/5{--tw-ring-color:#1018280d}@supports (color:color-mix(in lab,red,red)){.ring-gray-900\/5{--tw-ring-color:color-mix(in oklab,var(--color-gray-900)5%,transparent)}}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.drop-shadow-2xl{--tw-drop-shadow-size:drop-shadow(0 25px 25px var(--tw-drop-shadow-color,#00000026));--tw-drop-shadow:drop-shadow(var(--drop-shadow-2xl));filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.invert{--tw-invert:invert(100%);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-opacity{transition-property:opacity;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-shadow{transition-property:box-shadow;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-75{--tw-duration:75ms;transition-duration:75ms}.duration-250{--tw-duration:.25s;transition-duration:.25s}.duration-300{--tw-duration:.3s;transition-duration:.3s}.ease-in-out{--tw-ease:var(--ease-in-out);transition-timing-function:var(--ease-in-out)}.ease-linear{--tw-ease:linear;transition-timing-function:linear}.ease-out{--tw-ease:var(--ease-out);transition-timing-function:var(--ease-out)}@media (hover:hover){.group-hover\:opacity-100:is(:where(.group):hover *){opacity:1}.group-hover\:grayscale-0:is(:where(.group):hover *){--tw-grayscale:grayscale(0%);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.hover\:bg-black\/5:hover{background-color:#0000000d}@supports (color:color-mix(in lab,red,red)){.hover\:bg-black\/5:hover{background-color:color-mix(in oklab,var(--color-black)5%,transparent)}}.hover\:bg-black\/10:hover{background-color:#0000001a}@supports (color:color-mix(in lab,red,red)){.hover\:bg-black\/10:hover{background-color:color-mix(in oklab,var(--color-black)10%,transparent)}}.hover\:bg-gray-200\/20:hover{background-color:#e5e7eb33}@supports (color:color-mix(in lab,red,red)){.hover\:bg-gray-200\/20:hover{background-color:color-mix(in oklab,var(--color-gray-200)20%,transparent)}}.hover\:bg-gray-600:hover{background-color:var(--color-gray-600)}.hover\:bg-indigo-500:hover{background-color:#5956eb}.hover\:text-gray-700:hover{color:var(--color-gray-700)}.hover\:text-gray-900:hover{color:var(--color-gray-900)}.hover\:text-indigo-600:hover{color:var(--color-indigo-600)}.hover\:underline:hover{text-decoration-line:underline}.hover\:opacity-100:hover{opacity:1}.hover\:shadow-xl:hover{--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a),0 8px 10px -6px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}}.focus\:not-sr-only:focus{clip:auto;white-space:normal;width:auto;height:auto;margin:0;padding:0;position:static;overflow:visible}.focus\:absolute:focus{position:absolute}.focus\:mx-auto:focus{margin-inline:auto}.focus\:mt-2:focus{margin-top:calc(var(--spacing)*2)}.focus\:w-64:focus{width:calc(var(--spacing)*64)}.focus\:bg-gray-700:focus{background-color:var(--color-gray-700)}.focus\:bg-indigo-700:focus{background-color:var(--color-indigo-700)}.focus\:p-2:focus{padding:calc(var(--spacing)*2)}.focus\:opacity-100:focus{opacity:1}.focus\:ring-2:focus{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(2px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:ring-gray-700:focus{--tw-ring-color:var(--color-gray-700)}.focus\:ring-indigo-600:focus{--tw-ring-color:var(--color-indigo-600)}.focus\:grayscale-0:focus{--tw-grayscale:grayscale(0%);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}@media (min-width:40rem){.sm\:-mx-6{margin-inline:calc(var(--spacing)*-6)}.sm\:mx-auto{margin-inline:auto}.sm\:mt-4{margin-top:calc(var(--spacing)*4)}.sm\:mb-0{margin-bottom:calc(var(--spacing)*0)}.sm\:block{display:block}.sm\:max-w-xl{max-width:var(--container-xl)}.sm\:rounded-lg{border-radius:var(--radius-lg)}.sm\:px-6{padding-inline:calc(var(--spacing)*6)}.sm\:px-10{padding-inline:calc(var(--spacing)*10)}.sm\:py-12{padding-block:calc(var(--spacing)*12)}.sm\:leading-none{--tw-leading:1;line-height:1}.sm\:shadow-xl{--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a),0 8px 10px -6px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}}@media (min-width:48rem){.md\:visible{visibility:visible}.md\:top-0{top:calc(var(--spacing)*0)}.md\:left-0{left:calc(var(--spacing)*0)}.md\:left-64{left:calc(var(--spacing)*64)}.md\:mx-2{margin-inline:calc(var(--spacing)*2)}.md\:my-0{margin-block:calc(var(--spacing)*0)}.md\:my-6{margin-block:calc(var(--spacing)*6)}.md\:mt-0{margin-top:calc(var(--spacing)*0)}.md\:mt-8{margin-top:calc(var(--spacing)*8)}.md\:mb-12{margin-bottom:calc(var(--spacing)*12)}.md\:ml-0{margin-left:calc(var(--spacing)*0)}.md\:block{display:block}.md\:flex{display:flex}.md\:hidden{display:none}.md\:inline-block{display:inline-block}.md\:min-h-screen{min-height:100vh}.md\:w-1\/2{width:50%}.md\:w-\[calc\(100vw_-_16rem\)\]{width:calc(100vw - 16rem)}.md\:w-auto{width:auto}.md\:max-w-2xl{max-width:var(--container-2xl)}.md\:max-w-none{max-width:none}.md\:grow{flex-grow:1}.md\:grow-0{flex-grow:0}.md\:items-center{align-items:center}.md\:border-none{--tw-border-style:none;border-style:none}.md\:bg-transparent{background-color:#0000}.md\:bg-white{background-color:var(--color-white)}.md\:bg-left{background-position:0}.md\:px-16{padding-inline:calc(var(--spacing)*16)}.md\:py-0{padding-block:calc(var(--spacing)*0)}.md\:py-16{padding-block:calc(var(--spacing)*16)}.md\:pb-0{padding-bottom:calc(var(--spacing)*0)}.md\:pl-0{padding-left:calc(var(--spacing)*0)}.md\:text-center{text-align:center}.md\:text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.md\:text-4xl{font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height))}.md\:text-5xl{font-size:var(--text-5xl);line-height:var(--tw-leading,var(--text-5xl--line-height))}.md\:text-6xl{font-size:var(--text-6xl);line-height:var(--tw-leading,var(--text-6xl--line-height))}.md\:shadow-none{--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}}@media (min-width:64rem){.lg\:mb-12{margin-bottom:calc(var(--spacing)*12)}.lg\:ml-8{margin-left:calc(var(--spacing)*8)}.lg\:flex{display:flex}.lg\:bg-center{background-position:50%}.lg\:text-5xl{font-size:var(--text-5xl);line-height:var(--tw-leading,var(--text-5xl--line-height))}.lg\:text-7xl{font-size:var(--text-7xl);line-height:var(--tw-leading,var(--text-7xl--line-height))}.lg\:text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}}@media (min-width:80rem){.xl\:mb-16{margin-bottom:calc(var(--spacing)*16)}}.dark\:block:is(.dark *){display:block}.dark\:hidden:is(.dark *){display:none}:where(.dark\:divide-gray-700:is(.dark *)>:not(:last-child)){border-color:var(--color-gray-700)}.dark\:border-\[\#1b2533\]:is(.dark *){border-color:#1b2533}.dark\:border-gray-700:is(.dark *){border-color:var(--color-gray-700)}.dark\:bg-black\/10:is(.dark *){background-color:#0000001a}@supports (color:color-mix(in lab,red,red)){.dark\:bg-black\/10:is(.dark *){background-color:color-mix(in oklab,var(--color-black)10%,transparent)}}.dark\:bg-gray-600:is(.dark *){background-color:var(--color-gray-600)}.dark\:bg-gray-700:is(.dark *){background-color:var(--color-gray-700)}.dark\:bg-gray-800:is(.dark *){background-color:var(--color-gray-800)}.dark\:bg-gray-900:is(.dark *){background-color:var(--color-gray-900)}.dark\:bg-slate-800:is(.dark *){background-color:var(--color-slate-800)}.dark\:bg-yellow-300:is(.dark *){background-color:var(--color-yellow-300)}.dark\:fill-gray-200:is(.dark *){fill:var(--color-gray-200)}.dark\:fill-white:is(.dark *){fill:var(--color-white)}.dark\:font-medium:is(.dark *){--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.dark\:prose-invert:is(.dark *){--tw-prose-body:var(--tw-prose-invert-body);--tw-prose-headings:var(--tw-prose-invert-headings);--tw-prose-lead:var(--tw-prose-invert-lead);--tw-prose-links:var(--tw-prose-invert-links);--tw-prose-bold:var(--tw-prose-invert-bold);--tw-prose-counters:var(--tw-prose-invert-counters);--tw-prose-bullets:var(--tw-prose-invert-bullets);--tw-prose-hr:var(--tw-prose-invert-hr);--tw-prose-quotes:var(--tw-prose-invert-quotes);--tw-prose-quote-borders:var(--tw-prose-invert-quote-borders);--tw-prose-captions:var(--tw-prose-invert-captions);--tw-prose-kbd:var(--tw-prose-invert-kbd);--tw-prose-kbd-shadows:var(--tw-prose-invert-kbd-shadows);--tw-prose-code:var(--tw-prose-invert-code);--tw-prose-pre-code:var(--tw-prose-invert-pre-code);--tw-prose-pre-bg:var(--tw-prose-invert-pre-bg);--tw-prose-th-borders:var(--tw-prose-invert-th-borders);--tw-prose-td-borders:var(--tw-prose-invert-td-borders)}.dark\:prose-invert:is(.dark *) :where(a):not(:where([class~=not-prose],[class~=not-prose] *)){color:#818cf8}.dark\:prose-invert:is(.dark *) :where(a):not(:where([class~=not-prose],[class~=not-prose] *)):hover{color:#6366f1}.dark\:text-gray-100:is(.dark *){color:var(--color-gray-100)}.dark\:text-gray-200:is(.dark *){color:var(--color-gray-200)}.dark\:text-gray-300:is(.dark *){color:var(--color-gray-300)}.dark\:text-gray-400:is(.dark *){color:var(--color-gray-400)}.dark\:text-indigo-400:is(.dark *){color:var(--color-indigo-400)}.dark\:text-white:is(.dark *){color:var(--color-white)}@media (hover:hover){.dark\:group-hover\:text-white:is(.dark *):is(:where(.group):hover *){color:var(--color-white)}.dark\:hover\:bg-black\/10:is(.dark *):hover{background-color:#0000001a}@supports (color:color-mix(in lab,red,red)){.dark\:hover\:bg-black\/10:is(.dark *):hover{background-color:color-mix(in oklab,var(--color-black)10%,transparent)}}.dark\:hover\:bg-gray-500:is(.dark *):hover{background-color:var(--color-gray-500)}.dark\:hover\:text-white:is(.dark *):hover{color:var(--color-white)}}.dark\:focus\:text-white:is(.dark *):focus{color:var(--color-white)}.dark\:focus\:ring-indigo-500:is(.dark *):focus{--tw-ring-color:#5956eb}@media (min-width:48rem){.dark\:md\:bg-transparent:is(.dark *){background-color:#0000}}@media print{.print\:top-0{top:calc(var(--spacing)*0)}.print\:hidden{display:none}}.prose-h1\:mb-3 :where(h1):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:calc(var(--spacing)*3)}.prose-p\:my-3 :where(p):not(:where([class~=not-prose],[class~=not-prose] *)){margin-block:calc(var(--spacing)*3)}.prose-img\:inline :where(img):not(:where([class~=not-prose],[class~=not-prose] *)){display:inline}}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}@keyframes spin{to{transform:rotate(360deg)}} diff --git a/_pages/404.blade.php b/_pages/404.blade.php index 7a4328b1fde..0360a58111d 100644 --- a/_pages/404.blade.php +++ b/_pages/404.blade.php @@ -31,7 +31,7 @@ Sorry, the page you are looking for could not be found.

- + diff --git a/_pages/index.blade.php b/_pages/index.blade.php index c8989f5ba9e..ed3a8c6175a 100644 --- a/_pages/index.blade.php +++ b/_pages/index.blade.php @@ -9,7 +9,7 @@ {{-- The compiled Tailwind/App styles --}} -@if(config('hyde.load_app_styles_from_cdn', false)) - -@elseif(Asset::hasMediaFile('app.css')) - -@endif +@if(Vite::running()) + {{ Vite::assets(['resources/assets/app.css']) }} +@else + @if(config('hyde.load_app_styles_from_cdn', false)) + + @elseif(Asset::exists('app.css')) + + @endif + -{{-- Dynamic TailwindCSS Play CDN --}} -@if(config('hyde.use_play_cdn', false)) - - - + {{-- Dynamic TailwindCSS Play CDN --}} + @if(config('hyde.use_play_cdn', false)) + + + + @endif @endif {{-- Add any extra styles to include after the others --}} diff --git a/packages/framework/resources/views/pages/404.blade.php b/packages/framework/resources/views/pages/404.blade.php index 7a4328b1fde..0360a58111d 100644 --- a/packages/framework/resources/views/pages/404.blade.php +++ b/packages/framework/resources/views/pages/404.blade.php @@ -31,7 +31,7 @@ Sorry, the page you are looking for could not be found.

-
+ diff --git a/packages/framework/resources/views/pages/docs/search.blade.php b/packages/framework/resources/views/pages/docs/search.blade.php new file mode 100644 index 00000000000..58f0c1a98d0 --- /dev/null +++ b/packages/framework/resources/views/pages/docs/search.blade.php @@ -0,0 +1,15 @@ +@extends('hyde::layouts.docs') +@section('content') + @php + $title = Config::getString('docs.sidebar.header', 'Documentation'); + + $searchTitle = str_ends_with(strtolower($title), ' docs') + ? 'Search the ' . substr($title, 0, -5) . ' Documentation' + : 'Search ' . $title; + @endphp +

{{ $searchTitle }}

+ +
+ +
+@endsection diff --git a/packages/framework/resources/views/pages/documentation-search.blade.php b/packages/framework/resources/views/pages/documentation-search.blade.php deleted file mode 100644 index 8eed2b28948..00000000000 --- a/packages/framework/resources/views/pages/documentation-search.blade.php +++ /dev/null @@ -1,14 +0,0 @@ -@extends('hyde::layouts.docs') -@section('content') -

Search the documentation site

- - -@endsection \ No newline at end of file diff --git a/packages/framework/src/Console/Commands/BuildSearchCommand.php b/packages/framework/src/Console/Commands/BuildSearchCommand.php index 03cf8f95934..4388c64cb68 100644 --- a/packages/framework/src/Console/Commands/BuildSearchCommand.php +++ b/packages/framework/src/Console/Commands/BuildSearchCommand.php @@ -4,8 +4,11 @@ namespace Hyde\Console\Commands; -use Hyde\Framework\Actions\PostBuildTasks\GenerateSearch; +use Hyde\Foundation\Facades\Pages; +use Hyde\Framework\Actions\StaticPageBuilder; use LaravelZero\Framework\Commands\Command; +use Hyde\Framework\Features\Documentation\DocumentationSearchPage; +use Hyde\Framework\Features\Documentation\DocumentationSearchIndex; /** * Run the build process for the documentation search index. @@ -20,6 +23,12 @@ class BuildSearchCommand extends Command public function handle(): int { - return (new GenerateSearch())->run($this->output); + StaticPageBuilder::handle(Pages::get('docs/search.json') ?? new DocumentationSearchIndex()); + + if (DocumentationSearchPage::enabled()) { + StaticPageBuilder::handle(Pages::get('docs/search') ?? new DocumentationSearchPage()); + } + + return Command::SUCCESS; } } diff --git a/packages/framework/src/Console/Commands/BuildSiteCommand.php b/packages/framework/src/Console/Commands/BuildSiteCommand.php index a51a9c149ad..db199299638 100644 --- a/packages/framework/src/Console/Commands/BuildSiteCommand.php +++ b/packages/framework/src/Console/Commands/BuildSiteCommand.php @@ -26,11 +26,12 @@ class BuildSiteCommand extends Command { /** @var string */ protected $signature = 'build - {--run-dev : Run the NPM dev script after build} - {--run-prod : Run the NPM prod script after build} + {--run-vite : Build frontend assets using Vite} {--run-prettier : Format the output using NPM Prettier} {--pretty-urls : Should links in output use pretty URLs?} - {--no-api : Disable API calls, for example, Torchlight}'; + {--no-api : Disable API calls, for example, Torchlight} + {--run-dev : [Removed] Use --run-vite instead} + {--run-prod : [Removed] Use --run-vite instead}'; /** @var string */ protected $description = 'Build the static site'; @@ -40,6 +41,8 @@ class BuildSiteCommand extends Command public function handle(): int { + $this->checkForDeprecatedRunMixCommandUsage(); + $timeStart = microtime(true); $this->title('Building your static site!'); @@ -50,8 +53,6 @@ public function handle(): int $this->runPreBuildActions(); - $this->service->transferMediaAssets(); - $this->service->compileStaticPages(); $this->runPostBuildActions(); @@ -87,6 +88,10 @@ protected function runPreBuildActions(): void Config::set(['hyde.pretty_urls' => true]); } + if ($this->option('run-vite')) { + $this->runNodeCommand('npm run build', 'Building frontend assets for production!'); + } + $this->taskService->runPreBuildTasks(); } @@ -101,14 +106,6 @@ public function runPostBuildActions(): void 'prettify code' ); } - - if ($this->option('run-dev')) { - $this->runNodeCommand('npm run dev', 'Building frontend assets for development!'); - } - - if ($this->option('run-prod')) { - $this->runNodeCommand('npm run prod', 'Building frontend assets for production!'); - } } protected function printFinishMessage(float $timeStart): void @@ -160,4 +157,23 @@ protected function getExitCode(): int return Command::SUCCESS; } + + /** + * This method is called when the removed --run-dev or --run-prod options are used. + * + * @deprecated Use --run-vite instead + * @since v2.0 - This will be removed after 2-3 minor releases depending on the timeframe between them. (~v2.3) + * + * @codeCoverageIgnore + */ + protected function checkForDeprecatedRunMixCommandUsage(): void + { + if ($this->option('run-dev') || $this->option('run-prod')) { + $this->error('The --run-dev and --run-prod options have been removed in HydePHP v2.0.'); + $this->info('Please use --run-vite instead to build assets for production with Vite.'); + $this->line('See https://github.com/hydephp/develop/pull/2013 for more information.'); + + exit(Command::INVALID); + } + } } diff --git a/packages/framework/src/Console/Commands/PublishViewsCommand.php b/packages/framework/src/Console/Commands/PublishViewsCommand.php index 46ef8d6c1be..96e47972f80 100644 --- a/packages/framework/src/Console/Commands/PublishViewsCommand.php +++ b/packages/framework/src/Console/Commands/PublishViewsCommand.php @@ -4,9 +4,17 @@ namespace Hyde\Console\Commands; +use Closure; use Hyde\Console\Concerns\Command; -use Illuminate\Support\Facades\Artisan; - +use Hyde\Console\Helpers\ConsoleHelper; +use Hyde\Console\Helpers\InteractivePublishCommandHelper; +use Hyde\Console\Helpers\ViewPublishGroup; +use Illuminate\Support\Str; +use Laravel\Prompts\Key; +use Laravel\Prompts\MultiSelectPrompt; +use Laravel\Prompts\SelectPrompt; + +use function Laravel\Prompts\select; use function str_replace; use function sprintf; use function strstr; @@ -17,81 +25,134 @@ class PublishViewsCommand extends Command { /** @var string */ - protected $signature = 'publish:views {category? : The category to publish}'; + protected $signature = 'publish:views {group? : The group to publish}'; /** @var string */ - protected $description = 'Publish the hyde components for customization. Note that existing files will be overwritten'; - - /** @var array> */ - protected array $options = [ - 'layouts' => [ - 'name' => 'Blade Layouts', - 'description' => 'Shared layout views, such as the app layout, navigation menu, and Markdown page templates', - 'group' => 'hyde-layouts', - ], - 'components' => [ - 'name' => 'Blade Components', - 'description' => 'More or less self contained components, extracted for customizability and DRY code', - 'group' => 'hyde-components', - ], - 'page-404' => [ - 'name' => '404 Page', - 'description' => 'A beautiful 404 error page by the Laravel Collective', - 'group' => 'hyde-page-404', - ], - ]; + protected $description = 'Publish the Hyde components for customization. Note that existing files will be overwritten'; + + /** @var array */ + protected array $options; public function handle(): int { - $selected = (string) ($this->argument('category') ?? $this->promptForCategory()); + $this->options = static::mapToKeys([ + ViewPublishGroup::fromGroup('hyde-layouts', 'Blade Layouts', 'Shared layout views, such as the app layout, navigation menu, and Markdown page templates'), + ViewPublishGroup::fromGroup('hyde-components', 'Blade Components', 'More or less self contained components, extracted for customizability and DRY code'), + ]); - if ($selected === 'all' || $selected === '') { - foreach ($this->options as $key => $_ignored) { - $this->publishOption($key); - } - } else { - $this->publishOption($selected); + $selected = ($this->argument('group') ?? $this->promptForGroup()) ?: 'all'; + + if ($selected !== 'all' && (bool) $this->argument('group') === false && ConsoleHelper::canUseLaravelPrompts($this->input)) { + $this->infoComment(sprintf('Selected group [%s]', $selected)); } + if (! in_array($selected, $allowed = array_merge(['all'], array_keys($this->options)), true)) { + $this->error("Invalid selection: '$selected'"); + $this->infoComment('Allowed values are: ['.implode(', ', $allowed).']'); + + return Command::FAILURE; + } + + $files = $selected === 'all' + ? collect($this->options)->flatMap(fn (ViewPublishGroup $option): array => $option->publishableFilesMap())->all() + : $this->options[$selected]->publishableFilesMap(); + + $publisher = $this->publishSelectedFiles($files, $selected === 'all'); + + $this->infoComment($publisher->formatOutput($selected)); + return Command::SUCCESS; } - protected function publishOption(string $selected): void + protected function promptForGroup(): string { - Artisan::call('vendor:publish', [ - '--tag' => $this->options[$selected]['group'] ?? $selected, - '--force' => true, - ], $this->output); - } + SelectPrompt::fallbackUsing(function (SelectPrompt $prompt): string { + return $this->choice($prompt->label, $prompt->options, $prompt->default); + }); - protected function promptForCategory(): string - { - /** @var string $choice */ - $choice = $this->choice( - 'Which category do you want to publish?', - $this->formatPublishableChoices(), - 0 + return $this->parseChoiceIntoKey( + select('Which group do you want to publish?', $this->formatPublishableChoices(), 0) ?: 'all' ); + } - $selection = $this->parseChoiceIntoKey($choice); + protected function formatPublishableChoices(): array + { + return collect($this->options) + ->map(fn (ViewPublishGroup $option, string $key): string => sprintf('%s: %s', $key, $option->description)) + ->prepend('Publish all groups listed below') + ->values() + ->all(); + } - $this->infoComment(sprintf("Selected category [%s]\n", $selection ?: 'all')); + protected function parseChoiceIntoKey(string $choice): string + { + return strstr(str_replace(['', ''], '', $choice), ':', true) ?: ''; + } - return $selection; + /** + * @param array $groups + * @return array + */ + protected static function mapToKeys(array $groups): array + { + return collect($groups)->mapWithKeys(function (ViewPublishGroup $group): array { + return [Str::after($group->group, 'hyde-') => $group]; + })->all(); } - protected function formatPublishableChoices(): array + /** @param array $files */ + protected function publishSelectedFiles(array $files, bool $isPublishingAll): InteractivePublishCommandHelper { - $keys = ['Publish all categories listed below']; - foreach ($this->options as $key => $option) { - $keys[] = "$key: {$option['description']}"; + $publisher = new InteractivePublishCommandHelper($files); + + if (! $isPublishingAll && ConsoleHelper::canUseLaravelPrompts($this->input)) { + $publisher->only($this->promptUserForWhichFilesToPublish($publisher->getFileChoices())); } - return $keys; + $publisher->publishFiles(); + + return $publisher; } - protected function parseChoiceIntoKey(string $choice): string + /** + * @param array $files + * @return array + */ + protected function promptUserForWhichFilesToPublish(array $files): array { - return strstr(str_replace(['', ''], '', $choice), ':', true) ?: ''; + $choices = array_merge(['all' => 'All files'], $files); + + $prompt = new MultiSelectPrompt('Select the files you want to publish', $choices, [], 10, 'required', hint: 'Navigate with arrow keys, space to select, enter to confirm.'); + + $prompt->on('key', static::supportTogglingAll($prompt)); + + return (array) $prompt->prompt(); + } + + protected static function supportTogglingAll(MultiSelectPrompt $prompt): Closure + { + return function (string $key) use ($prompt): void { + static $isToggled = false; + + if ($prompt->isHighlighted('all')) { + if ($key === Key::SPACE) { + $prompt->emit('key', Key::CTRL_A); + + if ($isToggled) { + // We need to emit CTRL+A twice to deselect all for some reason + $prompt->emit('key', Key::CTRL_A); + $isToggled = false; + } else { + $isToggled = true; + } + } elseif ($key === Key::ENTER) { + if (! $isToggled) { + $prompt->emit('key', Key::CTRL_A); + } + + $prompt->state = 'submit'; + } + } + }; } } diff --git a/packages/framework/src/Console/Commands/RebuildPageCommand.php b/packages/framework/src/Console/Commands/RebuildPageCommand.php index a6ddf26cc0b..0611c314b11 100644 --- a/packages/framework/src/Console/Commands/RebuildPageCommand.php +++ b/packages/framework/src/Console/Commands/RebuildPageCommand.php @@ -9,7 +9,7 @@ use Hyde\Foundation\Facades\Pages; use Hyde\Framework\Actions\StaticPageBuilder; use Hyde\Framework\Features\BuildTasks\BuildTask; -use Hyde\Framework\Services\BuildService; +use Hyde\Framework\Actions\PreBuildTasks\TransferMediaAssets; use Hyde\Hyde; use Hyde\Pages\BladePage; use Hyde\Pages\DocumentationPage; @@ -37,7 +37,7 @@ class RebuildPageCommand extends Command public function handle(): int { if ($this->argument('path') === Hyde::getMediaDirectory()) { - (new BuildService($this->getOutput()))->transferMediaAssets(); + return (new TransferMediaAssets())->run($this->output); $this->info('All done!'); diff --git a/packages/framework/src/Console/Commands/ServeCommand.php b/packages/framework/src/Console/Commands/ServeCommand.php index 0480b0b4293..c7132e4fc0a 100644 --- a/packages/framework/src/Console/Commands/ServeCommand.php +++ b/packages/framework/src/Console/Commands/ServeCommand.php @@ -5,9 +5,12 @@ namespace Hyde\Console\Commands; use Closure; +use Hyde\Facades\Filesystem; use Hyde\Hyde; use Hyde\Facades\Config; +use Illuminate\Contracts\Process\InvokedProcess; use Illuminate\Support\Arr; +use Illuminate\Support\Sleep; use InvalidArgumentException; use Hyde\Console\Concerns\Command; use Hyde\RealtimeCompiler\ConsoleOutput; @@ -35,6 +38,7 @@ class ServeCommand extends Command {--pretty-urls= : Enable pretty URLs. (Overrides config setting)} {--play-cdn= : Enable the Tailwind Play CDN. (Overrides config setting)} {--open=false : Open the site preview in the browser.} + {--vite : Enable Vite for Hot Module Replacement (HMR)} '; /** @var string */ @@ -42,6 +46,9 @@ class ServeCommand extends Command protected ConsoleOutput $console; + protected InvokedProcess $server; + protected ?InvokedProcess $vite = null; + public function safeHandle(): int { $this->configureOutput(); @@ -51,12 +58,18 @@ public function safeHandle(): int $this->openInBrowser((string) $this->option('open')); } + if ($this->option('vite')) { + $this->runViteProcess(); + } + $this->runServerProcess(sprintf('php -S %s:%d %s', $this->getHostSelection(), $this->getPortSelection(), escapeshellarg($this->getExecutablePath()), )); + $this->handleRunningProcesses(); + return Command::SUCCESS; } @@ -77,7 +90,7 @@ protected function getExecutablePath(): string protected function runServerProcess(string $command): void { - Process::forever()->env($this->getEnvironmentVariables())->run($command, $this->getOutputHandler()); + $this->server = Process::forever()->env($this->getEnvironmentVariables())->start($command, $this->getOutputHandler()); } protected function getEnvironmentVariables(): array @@ -102,7 +115,7 @@ protected function printStartMessage(): void { $this->useBasicOutput() ? $this->output->writeln('Starting the HydeRC server... Use Ctrl+C to stop') - : $this->console->printStartMessage($this->getHostSelection(), $this->getPortSelection(), $this->getEnvironmentVariables()); + : $this->console->printStartMessage($this->getHostSelection(), $this->getPortSelection(), $this->getEnvironmentVariables(), $this->option('vite')); } protected function getOutputHandler(): Closure @@ -169,4 +182,51 @@ protected function getOpenCommand(string $osFamily): ?string default => null }; } + + protected function runViteProcess(): void + { + if (! $this->isPortAvailable(5173)) { + throw new InvalidArgumentException( + 'Unable to start Vite server: Port 5173 is already in use. '. + 'Please stop any other Vite processes and try again.' + ); + } + + Filesystem::touch('app/storage/framework/runtime/vite.hot'); + + $this->vite = Process::forever()->start('npm run dev'); + } + + protected function handleRunningProcesses(): void + { + while ($this->server->running()) { + $this->handleViteOutput(); + + Sleep::for(100)->milliseconds(); + } + } + + protected function handleViteOutput(): void + { + if ($this->vite?->running()) { + $output = $this->vite->latestOutput(); + + if ($output) { + $this->output->write($output); + } + } + } + + /** @experimental This feature may be removed before the final release. */ + protected function isPortAvailable(int $port): bool + { + $socket = @fsockopen('localhost', $port, $errno, $errstr, 1); + if ($socket !== false) { + fclose($socket); + + return false; + } + + return true; + } } diff --git a/packages/framework/src/Console/Helpers/ConsoleHelper.php b/packages/framework/src/Console/Helpers/ConsoleHelper.php new file mode 100644 index 00000000000..f59a8a09ab3 --- /dev/null +++ b/packages/framework/src/Console/Helpers/ConsoleHelper.php @@ -0,0 +1,43 @@ +isInteractive() && windows_os() === false && Prompt::shouldFallback() === false; + } +} diff --git a/packages/framework/src/Console/Helpers/InteractivePublishCommandHelper.php b/packages/framework/src/Console/Helpers/InteractivePublishCommandHelper.php new file mode 100644 index 00000000000..faf33bb20a7 --- /dev/null +++ b/packages/framework/src/Console/Helpers/InteractivePublishCommandHelper.php @@ -0,0 +1,87 @@ + Map of source files to target files */ + protected array $publishableFilesMap; + + protected readonly int $originalFileCount; + + /** @param array $publishableFilesMap */ + public function __construct(array $publishableFilesMap) + { + $this->publishableFilesMap = $publishableFilesMap; + $this->originalFileCount = count($publishableFilesMap); + } + + /** @return array */ + public function getFileChoices(): array + { + return Arr::mapWithKeys($this->publishableFilesMap, /** @return array */ function (string $target, string $source): array { + return [$source => $this->pathRelativeToDirectory($source, $this->getBaseDirectory())]; + }); + } + + /** + * Only publish the selected files. + * + * @param array $selectedFiles Array of selected file paths, matching the keys of the publishableFilesMap. + */ + public function only(array $selectedFiles): void + { + $this->publishableFilesMap = Arr::only($this->publishableFilesMap, $selectedFiles); + } + + /** Find the most specific common parent directory path for the files, trimming as much as possible whilst keeping specificity and uniqueness. */ + public function getBaseDirectory(): string + { + $partsMap = collect($this->publishableFilesMap)->map(function (string $file): array { + return explode('/', $file); + }); + + $commonParts = $partsMap->reduce(function (array $carry, array $parts): array { + return array_intersect($carry, $parts); + }, $partsMap->first()); + + return implode('/', $commonParts); + } + + public function publishFiles(): void + { + foreach ($this->publishableFilesMap as $source => $target) { + Filesystem::ensureDirectoryExists(dirname($target)); + Filesystem::copy($source, $target); + } + } + + public function formatOutput(string $group): string + { + $fileCount = count($this->publishableFilesMap); + $publishedOneFile = $fileCount === 1; + $publishedAllGroups = $group === 'all'; + $publishedAllFiles = $fileCount === $this->originalFileCount; + $selectedFilesModifier = $publishedAllFiles ? 'all' : 'selected'; + + return match (true) { + $publishedAllGroups => sprintf('Published all %d files to [%s]', $fileCount, $this->getBaseDirectory()), + $publishedOneFile => sprintf('Published selected file to [%s]', reset($this->publishableFilesMap)), + default => sprintf('Published %s [%s] files to [%s]', $selectedFilesModifier, Str::singular($group), $this->getBaseDirectory()) + }; + } + + protected function pathRelativeToDirectory(string $source, string $directory): string + { + return Str::after($source, basename($directory).'/'); + } +} diff --git a/packages/framework/src/Console/Helpers/ViewPublishGroup.php b/packages/framework/src/Console/Helpers/ViewPublishGroup.php new file mode 100644 index 00000000000..43d381d7916 --- /dev/null +++ b/packages/framework/src/Console/Helpers/ViewPublishGroup.php @@ -0,0 +1,88 @@ + The filenames relative to the source directory. */ + public readonly array $files; + + /** @var class-string<\Hyde\Foundation\Providers\ViewServiceProvider> */ + protected static string $provider = ViewServiceProvider::class; + + protected function __construct(string $group, string $source, string $target, array $files, ?string $name = null, ?string $description = null) + { + $this->group = $group; + $this->source = $source; + $this->target = $target; + $this->files = $files; + + $this->name = $name ?? Hyde::makeTitle($group); + $this->description = $description ?? "Publish the '$group' files for customization."; + } + + public static function fromGroup(string $group, ?string $name = null, ?string $description = null): static + { + [$source, $target] = static::keyedArrayToTuple(static::$provider::pathsToPublish(static::$provider, $group)); + [$source, $target] = [static::normalizePath($source), static::normalizePath($target)]; + + $files = static::findFiles($source); + + return new static($group, $source, $target, $files, $name, $description); + } + + /** @return array The source file paths mapped to their target file paths. */ + public function publishableFilesMap(): array + { + return collect($this->files)->mapWithKeys(fn (string $file): array => [ + path_join($this->source, $file) => path_join($this->target, $file), + ])->all(); + } + + /** + * @param array $array + * @return list + */ + protected static function keyedArrayToTuple(array $array): array + { + return [key($array), current($array)]; + } + + /** @return array */ + protected static function findFiles(string $source): array + { + return Filesystem::findFiles($source, recursive: true) + ->map(fn (string $file) => static::normalizePath($file)) + ->map(fn (string $file) => unslash(Str::after($file, $source))) + ->sort(fn (string $a, string $b): int => substr_count($a, '/') <=> substr_count($b, '/') ?: strcmp($a, $b)) + ->all(); + } + + protected static function normalizePath(string $path): string + { + return Hyde::pathToRelative( + Filesystem::exists($path) ? realpath($path) : $path + ); + } +} diff --git a/packages/framework/src/Enums/Feature.php b/packages/framework/src/Enums/Feature.php index 85694270e0c..2c854356686 100644 --- a/packages/framework/src/Enums/Feature.php +++ b/packages/framework/src/Enums/Feature.php @@ -4,6 +4,9 @@ namespace Hyde\Enums; +use function defined; +use function constant; + /** * A configurable feature that belongs to the Features class. * @@ -24,4 +27,14 @@ enum Feature // Integrations case Torchlight; + + /** Translates a case name into the corresponding Enum case, if any. If there is no matching case defined, it will return null. */ + public static function fromName(string $name): ?self + { + if (! defined("self::$name")) { + return null; + } + + return constant("self::$name"); + } } diff --git a/packages/framework/src/Facades/Asset.php b/packages/framework/src/Facades/Asset.php index f0bd3b6c651..3bc01acfa1c 100644 --- a/packages/framework/src/Facades/Asset.php +++ b/packages/framework/src/Facades/Asset.php @@ -4,25 +4,30 @@ namespace Hyde\Facades; -use Hyde\Framework\Services\AssetService; -use Illuminate\Support\Facades\Facade; +use Hyde\Support\Filesystem\MediaFile; /** - * Handles the retrieval of core asset files, either from the HydeFront CDN or from the local media folder. + * Simplified facade to interact with media files. * - * @see \Hyde\Framework\Services\AssetService - * - * @method static string version() - * @method static string cdnLink(string $file) - * @method static string mediaLink(string $file) - * @method static bool hasMediaFile(string $file) - * @method static string injectTailwindConfig() + * @see \Hyde\Support\Filesystem\MediaFile */ -class Asset extends Facade +class Asset { - /** @psalm-return AssetService::class */ - protected static function getFacadeAccessor(): string + /** + * Get a MediaFile instance for the given filename in the media source directory. + * + * @throws \Hyde\Framework\Exceptions\FileNotFoundException If the file does not exist in the `_media` source directory. + */ + public static function get(string $file): MediaFile + { + return MediaFile::get($file); + } + + /** + * Check if a media file exists in the source directory. + */ + public static function exists(string $file): bool { - return AssetService::class; + return Filesystem::exists(MediaFile::sourcePath($file)); } } diff --git a/packages/framework/src/Facades/Author.php b/packages/framework/src/Facades/Author.php index d9bbbb89084..09d1bad4a5f 100644 --- a/packages/framework/src/Facades/Author.php +++ b/packages/framework/src/Facades/Author.php @@ -7,6 +7,8 @@ use Hyde\Framework\Features\Blogging\Models\PostAuthor; use Illuminate\Support\Collection; +use function compact; + /** * Allows you to easily add pre-defined authors for your blog posts. * @@ -15,25 +17,27 @@ class Author { /** - * Construct a new Post Author. For Hyde to discover this author, - * you must call this method from your hyde.php config file. + * Configuration helper method to define a new blog post author, with better IDE support. + * + * The returned array will then be used by the framework to create a new PostAuthor instance. * * @see https://hydephp.com/docs/1.x/customization#authors * - * @param string $username The username of the author. This is the key used to find authors in the config. * @param string|null $name The optional display name of the author, leave blank to use the username. * @param string|null $website The author's optional website URL. Website, Twitter, etc. + * @param string|null $bio The author's optional biography text. Markdown supported. + * @param string|null $avatar The author's optional avatar image. Supports both image names and full URIs. + * @param array|null $socials The author's optional social media links/handles. */ - public static function create(string $username, ?string $name = null, ?string $website = null): PostAuthor + public static function create(?string $name = null, ?string $website = null, ?string $bio = null, ?string $avatar = null, ?array $socials = null): array { - return new PostAuthor($username, $name, $website); + return compact('name', 'website', 'bio', 'avatar', 'socials'); } /** - * Get a Post Author instance from the config. If no author matching the username is found, - * a new Post Author instance will be created with just username supplied to the method. + * Get a Post Author instance by username, or null if not found. */ - public static function get(string $username): PostAuthor + public static function get(string $username): ?PostAuthor { return PostAuthor::get($username); } diff --git a/packages/framework/src/Facades/Features.php b/packages/framework/src/Facades/Features.php index c1928d535ea..4a9a6383eee 100644 --- a/packages/framework/src/Facades/Features.php +++ b/packages/framework/src/Facades/Features.php @@ -5,88 +5,86 @@ namespace Hyde\Facades; use Hyde\Hyde; -use Hyde\Enums\Feature; +use Illuminate\Support\Str; use Hyde\Pages\MarkdownPost; use Hyde\Pages\DocumentationPage; -use JetBrains\PhpStorm\Deprecated; +use Hyde\Enums\Feature; use Hyde\Support\Concerns\Serializable; use Hyde\Support\Contracts\SerializableContract; -use Hyde\Framework\Concerns\Internal\MockableFeatures; -use Illuminate\Support\Str; +use Illuminate\Support\Arr; -use function get_class_methods; +use function collect; +use function array_filter; use function extension_loaded; -use function str_starts_with; use function in_array; -use function collect; -use function substr; use function count; use function app; /** * Allows features to be enabled and disabled in a simple object-oriented manner. - * - * @internal Until this class is split into a service/manager class, it should not be used outside of Hyde as the API is subject to change. - * - * @todo Split facade logic to service/manager class. (Initial and mock data could be set with boot/set methods) - * Based entirely on Laravel Jetstream (License MIT) - * - * @see https://jetstream.laravel.com/ */ class Features implements SerializableContract { use Serializable; - use MockableFeatures; + + /** + * The features that are enabled. + * + * @var array<\Hyde\Enums\Feature> + */ + protected array $features = []; + + public function __construct() + { + $this->features = Config::getArray('hyde.features', Feature::cases()); + } /** * Determine if the given specified is enabled. */ - public static function enabled(Feature $feature): bool + public static function has(Feature $feature): bool { - return static::resolveMockedInstance($feature->name) ?? in_array( - $feature, Config::getArray('hyde.features', static::getDefaultOptions()) - ); + return in_array($feature, Hyde::features()->features); } - // ================================================ - // Determine if a given feature is enabled. - // ================================================ + /** + * Get all enabled features. + * + * @return array + */ + public static function enabled(): array + { + return Arr::map(Hyde::features()->features, fn (Feature $feature): string => Str::kebab($feature->name)); + } public static function hasHtmlPages(): bool { - return static::enabled(Feature::HtmlPages); + return static::has(Feature::HtmlPages); } public static function hasBladePages(): bool { - return static::enabled(Feature::BladePages); + return static::has(Feature::BladePages); } public static function hasMarkdownPages(): bool { - return static::enabled(Feature::MarkdownPages); + return static::has(Feature::MarkdownPages); } public static function hasMarkdownPosts(): bool { - return static::enabled(Feature::MarkdownPosts); + return static::has(Feature::MarkdownPosts); } public static function hasDocumentationPages(): bool { - return static::enabled(Feature::DocumentationPages); - } - - public static function hasDocumentationSearch(): bool - { - return static::enabled(Feature::DocumentationSearch) - && static::hasDocumentationPages() - && count(DocumentationPage::files()) > 0; + return static::has(Feature::DocumentationPages); } public static function hasDarkmode(): bool { - return static::enabled(Feature::Darkmode); + return static::has(Feature::Darkmode); } public static function hasThemeToggleButtons(): bool @@ -95,129 +93,46 @@ public static function hasThemeToggleButtons(): bool } /** - * Torchlight is by default enabled automatically when an API token - * is set in the .env file but is disabled when running tests. - */ - public static function hasTorchlight(): bool - { - return static::enabled(Feature::Torchlight) - && (Config::getNullableString('torchlight.token') !== null) - && (app('env') !== 'testing'); - } - - // ================================================= - // Configure features to be used in the config file. - // ================================================= - - /** - * @codeCoverageIgnore Deprecated method. - * - * @deprecated This method will be removed in v2.0. Please use `Feature::HtmlPages` instead. - */ - #[Deprecated(reason: 'Replaced by the \Hyde\Enums\Feature::HtmlPages Enum case', replacement: 'Feature::HtmlPages', since: '1.6.0')] - public static function htmlPages(): Feature - { - return Feature::HtmlPages; - } - - /** - * @codeCoverageIgnore Deprecated method. - * - * @deprecated This method will be removed in v2.0. Please use `Feature::BladePages` instead. - */ - #[Deprecated(reason: 'Replaced by the \Hyde\Enums\Feature::BladePages Enum case', replacement: 'Feature::BladePages', since: '1.6.0')] - public static function bladePages(): Feature - { - return Feature::BladePages; - } - - /** - * @codeCoverageIgnore Deprecated method. - * - * @deprecated This method will be removed in v2.0. Please use `Feature::MarkdownPages` instead. - */ - #[Deprecated(reason: 'Replaced by the \Hyde\Enums\Feature::MarkdownPages Enum case', replacement: 'Feature::MarkdownPages', since: '1.6.0')] - public static function markdownPages(): Feature - { - return Feature::MarkdownPages; - } - - /** - * @codeCoverageIgnore Deprecated method. - * - * @deprecated This method will be removed in v2.0. Please use `Feature::MarkdownPosts` instead. + * Can a sitemap be generated? */ - #[Deprecated(reason: 'Replaced by the \Hyde\Enums\Feature::MarkdownPosts Enum case', replacement: 'Feature::MarkdownPosts', since: '1.6.0')] - public static function markdownPosts(): Feature + public static function hasSitemap(): bool { - return Feature::MarkdownPosts; - } - - /** - * @codeCoverageIgnore Deprecated method. - * - * @deprecated This method will be removed in v2.0. Please use `Feature::DocumentationPages` instead. - */ - #[Deprecated(reason: 'Replaced by the \Hyde\Enums\Feature::DocumentationPages Enum case', replacement: 'Feature::DocumentationPages', since: '1.6.0')] - public static function documentationPages(): Feature - { - return Feature::DocumentationPages; + return Hyde::hasSiteUrl() + && Config::getBool('hyde.generate_sitemap', true) + && extension_loaded('simplexml'); } /** - * @codeCoverageIgnore Deprecated method. - * - * @deprecated This method will be removed in v2.0. Please use `Feature::DocumentationSearch` instead. + * Can an RSS feed be generated? */ - #[Deprecated(reason: 'Replaced by the \Hyde\Enums\Feature::DocumentationSearch Enum case', replacement: 'Feature::DocumentationSearch', since: '1.6.0')] - public static function documentationSearch(): Feature + public static function hasRss(): bool { - return Feature::DocumentationSearch; + return Hyde::hasSiteUrl() + && static::hasMarkdownPosts() + && Config::getBool('hyde.rss.enabled', true) + && extension_loaded('simplexml') + && count(MarkdownPost::files()) > 0; } /** - * @codeCoverageIgnore Deprecated method. - * - * @deprecated This method will be removed in v2.0. Please use `Feature::Darkmode` instead. + * Should documentation search be enabled? */ - #[Deprecated(reason: 'Replaced by the \Hyde\Enums\Feature::Darkmode Enum case', replacement: 'Feature::Darkmode', since: '1.6.0')] - public static function darkmode(): Feature + public static function hasDocumentationSearch(): bool { - return Feature::Darkmode; + return static::has(Feature::DocumentationSearch) + && static::hasDocumentationPages() + && count(DocumentationPage::files()) > 0; } /** - * @codeCoverageIgnore Deprecated method. - * - * @deprecated This method will be removed in v2.0. Please use `Feature::Torchlight` instead. + * Torchlight is by default enabled automatically when an API token + * is set in the `.env` file but is disabled when running tests. */ - #[Deprecated(reason: 'Replaced by the \Hyde\Enums\Feature::Torchlight Enum case', replacement: 'Feature::Torchlight', since: '1.6.0')] - public static function torchlight(): Feature - { - return Feature::Torchlight; - } - - // ==================================================== - // Dynamic features that in addition to being enabled - // in the config file, require preconditions to be met. - // ==================================================== - - /** Can a sitemap be generated? */ - public static function sitemap(): bool - { - return static::resolveMockedInstance('sitemap') ?? Hyde::hasSiteUrl() - && Config::getBool('hyde.generate_sitemap', true) - && extension_loaded('simplexml'); - } - - /** Can an RSS feed be generated? */ - public static function rss(): bool + public static function hasTorchlight(): bool { - return static::resolveMockedInstance('rss') ?? Hyde::hasSiteUrl() - && static::hasMarkdownPosts() - && Config::getBool('hyde.rss.enabled', true) - && extension_loaded('simplexml') - && count(MarkdownPost::files()) > 0; + return static::has(Feature::Torchlight) + && (Config::getNullableString('torchlight.token') !== null) + && (app('env') !== 'testing'); } /** @@ -229,29 +144,20 @@ public static function rss(): bool */ public function toArray(): array { - return collect(get_class_methods(static::class)) - ->filter(fn (string $method): bool => str_starts_with($method, 'has')) - ->mapWithKeys(fn (string $method): array => [ - Str::kebab(substr($method, 3)) => static::{$method}(), - ])->toArray(); + return Arr::mapWithKeys(Feature::cases(), fn (Feature $feature): array => [ + Str::kebab($feature->name) => static::has($feature), + ]); } - protected static function getDefaultOptions(): array + /** @internal This method is not covered by the backward compatibility promise. */ + public static function mock(string $feature, ?bool $enabled = null): void { - return [ - // Page Modules - Feature::HtmlPages, - Feature::MarkdownPosts, - Feature::BladePages, - Feature::MarkdownPages, - Feature::DocumentationPages, - - // Frontend Features - Feature::Darkmode, - Feature::DocumentationSearch, - - // Integrations - Feature::Torchlight, - ]; + if ($enabled === true) { + // Add the feature if it doesn't already exist. + Hyde::features()->features[] = collect(Feature::cases())->firstOrFail(fn (Feature $search): bool => Str::kebab($search->name) === $feature); + } else { + // Remove the feature if it exists. + Hyde::features()->features = array_filter(Hyde::features()->features, fn (Feature $search): bool => Str::kebab($search->name) !== $feature); + } } } diff --git a/packages/framework/src/Facades/Filesystem.php b/packages/framework/src/Facades/Filesystem.php index 2cbcf58bd59..5523f658c5f 100644 --- a/packages/framework/src/Facades/Filesystem.php +++ b/packages/framework/src/Facades/Filesystem.php @@ -144,6 +144,42 @@ public static function putContents(string $path, string $contents, bool $lock = return self::put(...func_get_args()); } + /** + * Improved mime type detection for the given path that can be relative to the project root, + * or a remote URL where this will try to guess the mime type based on the file extension. + * + * @param string $path The path to the file, relative to the project root or a remote URL. + */ + public static function findMimeType(string $path): string + { + $extension = self::extension($path); + + $lookup = [ + 'txt' => 'text/plain', + 'md' => 'text/markdown', + 'html' => 'text/html', + 'css' => 'text/css', + 'svg' => 'image/svg+xml', + 'png' => 'image/png', + 'jpg' => 'image/jpeg', + 'jpeg' => 'image/jpeg', + 'gif' => 'image/gif', + 'json' => 'application/json', + 'js' => 'application/javascript', + 'xml' => 'application/xml', + ]; + + if (isset($lookup[$extension])) { + return $lookup[$extension]; + } + + if (extension_loaded('fileinfo') && self::exists($path)) { + return self::mimeType($path); + } + + return 'text/plain'; + } + protected static function filesystem(): \Illuminate\Filesystem\Filesystem { return app(\Illuminate\Filesystem\Filesystem::class); diff --git a/packages/framework/src/Facades/HydeFront.php b/packages/framework/src/Facades/HydeFront.php new file mode 100644 index 00000000000..ed1dd511888 --- /dev/null +++ b/packages/framework/src/Facades/HydeFront.php @@ -0,0 +1,59 @@ +|string $destination Route key, or an external URI. + * @param string|null $label If not provided, Hyde will try to get it from the route's connected page, or from the URL. + * @param int|null $priority If not provided, Hyde will try to get it from the route or the default priority of 500. + * @param array $attributes Additional attributes for the navigation item. + * @return array{destination: string, label: ?string, priority: ?int, attributes: array} + */ + public static function item(string $destination, ?string $label = null, ?int $priority = null, array $attributes = []): array + { + return compact('destination', 'label', 'priority', 'attributes'); + } +} diff --git a/packages/framework/src/Facades/Vite.php b/packages/framework/src/Facades/Vite.php new file mode 100644 index 00000000000..1a03d4dc8c8 --- /dev/null +++ b/packages/framework/src/Facades/Vite.php @@ -0,0 +1,79 @@ + $paths */ + public static function assets(array $paths): HtmlString + { + $html = ''; + + foreach ($paths as $path) { + $html .= static::formatAssetPath($path); + } + + return new HtmlString($html); + } + + /** @throws InvalidArgumentException If the asset type is not supported. */ + protected static function formatAssetPath(string $path): string + { + if (static::isCssPath($path)) { + return static::formatStylesheetLink($path); + } + + if (static::isJsPath($path)) { + return static::formatScriptInclude($path); + } + + // We don't know how to handle other asset types, so we throw an exception to let the user know. + throw new InvalidArgumentException("Unsupported asset type for path: '$path'"); + } + + protected static function isCssPath(string $path): bool + { + return static::checkFileExtensionForPath($path, static::CSS_EXTENSIONS); + } + + protected static function isJsPath(string $path): bool + { + return static::checkFileExtensionForPath($path, static::JS_EXTENSIONS); + } + + protected static function checkFileExtensionForPath(string $path, array $extensions): bool + { + return preg_match('/\.('.implode('|', $extensions).')$/', $path) === 1; + } + + protected static function formatStylesheetLink(string $path): string + { + return sprintf('', $path); + } + + protected static function formatScriptInclude(string $path): string + { + return sprintf('', $path); + } +} diff --git a/packages/framework/src/Foundation/Concerns/ForwardsFilesystem.php b/packages/framework/src/Foundation/Concerns/ForwardsFilesystem.php index 88e8aba452b..b294170474f 100644 --- a/packages/framework/src/Foundation/Concerns/ForwardsFilesystem.php +++ b/packages/framework/src/Foundation/Concerns/ForwardsFilesystem.php @@ -5,6 +5,7 @@ namespace Hyde\Foundation\Concerns; use Hyde\Foundation\Kernel\Filesystem; +use Illuminate\Support\Collection; /** * @internal Single-use trait for the HydeKernel class. @@ -28,21 +29,11 @@ public function vendorPath(string $path = '', string $package = 'framework'): st return $this->filesystem->vendorPath($path, $package); } - public function mediaPath(string $path = ''): string - { - return $this->filesystem->mediaPath($path); - } - public function sitePath(string $path = ''): string { return $this->filesystem->sitePath($path); } - public function siteMediaPath(string $path = ''): string - { - return $this->filesystem->siteMediaPath($path); - } - public function pathToAbsolute(string|array $path): string|array { return $this->filesystem->pathToAbsolute($path); @@ -52,4 +43,10 @@ public function pathToRelative(string $path): string { return $this->filesystem->pathToRelative($path); } + + /** @return \Illuminate\Support\Collection */ + public function assets(): Collection + { + return $this->filesystem->assets(); + } } diff --git a/packages/framework/src/Foundation/Concerns/ForwardsHyperlinks.php b/packages/framework/src/Foundation/Concerns/ForwardsHyperlinks.php index 7f5bef1d14e..9c7cd2aded7 100644 --- a/packages/framework/src/Foundation/Concerns/ForwardsHyperlinks.php +++ b/packages/framework/src/Foundation/Concerns/ForwardsHyperlinks.php @@ -5,7 +5,7 @@ namespace Hyde\Foundation\Concerns; use Hyde\Support\Models\Route; -use JetBrains\PhpStorm\Deprecated; +use Hyde\Support\Filesystem\MediaFile; /** * @internal Single-use trait for the HydeKernel class. @@ -24,20 +24,10 @@ public function relativeLink(string $destination): string return $this->hyperlinks->relativeLink($destination); } - /** - * @deprecated This method will be removed in v2.0. Please use `asset()` instead. - */ - #[Deprecated(reason: 'Use `asset` method instead.', replacement: '%class%::asset(%parameter0%)')] - public function mediaLink(string $destination, bool $validate = false): string + /** @throws \Hyde\Framework\Exceptions\FileNotFoundException If the file does not exist in the `_media` source directory. */ + public function asset(string $name): MediaFile { - trigger_deprecation('hyde/framework', '1.8.0', 'The %s() method is deprecated, use %s() instead.', __METHOD__, 'asset'); - - return $this->hyperlinks->mediaLink($destination, $validate); - } - - public function asset(string $name, bool $preferQualifiedUrl = false): string - { - return $this->hyperlinks->asset($name, $preferQualifiedUrl); + return $this->hyperlinks->asset($name); } public function url(string $path = ''): string diff --git a/packages/framework/src/Foundation/Concerns/HasKernelData.php b/packages/framework/src/Foundation/Concerns/HasKernelData.php new file mode 100644 index 00000000000..071d8d8caa6 --- /dev/null +++ b/packages/framework/src/Foundation/Concerns/HasKernelData.php @@ -0,0 +1,65 @@ + + */ + protected Collection $authors; + + /** + * Get the collection of authors defined in the config. + * + * @return \Illuminate\Support\Collection + */ + public function authors(): Collection + { + if (isset($this->authors)) { + return $this->authors; + } + + $config = collect(Config::getArray('hyde.authors', [])); + + if ($config->isEmpty()) { + // Defer setting the authors property until the next try. + return $config; + } + + return $this->authors = $this->parseConfigurationAuthors($config); + } + + protected function parseConfigurationAuthors(Collection $authors): Collection + { + return $authors->mapWithKeys(function (array $author, string $username): array { + if (! $username) { + throw new InvalidConfigurationException('Author username cannot be empty. Did you forget to set the author\'s array key?', 'hyde', 'authors'); + } + + $username = PostAuthor::normalizeUsername($username); + + $author['username'] = $username; + + return [$username => PostAuthor::create($author)]; + }); + } +} diff --git a/packages/framework/src/Foundation/Concerns/HasMediaFiles.php b/packages/framework/src/Foundation/Concerns/HasMediaFiles.php new file mode 100644 index 00000000000..ce1d7a1ab31 --- /dev/null +++ b/packages/framework/src/Foundation/Concerns/HasMediaFiles.php @@ -0,0 +1,50 @@ + The Collection keys are the filenames relative to the _media/ directory */ + protected Collection $assets; + + /** + * Get all media files in the project. + * + * @return Collection + */ + public function assets(): Collection + { + return $this->assets ??= static::discoverMediaFiles(); + } + + protected static function discoverMediaFiles(): Collection + { + return collect(static::getMediaFiles())->mapWithKeys(function (string $path): array { + $file = MediaFile::make($path); + + return [$file->getIdentifier() => $file]; + }); + } + + protected static function getMediaFiles(): array + { + return Filesystem::findFiles(Hyde::getMediaDirectory(), + Config::getArray('hyde.media_extensions', MediaFile::EXTENSIONS), recursive: true + )->all(); + } +} diff --git a/packages/framework/src/Foundation/Concerns/HydeExtension.php b/packages/framework/src/Foundation/Concerns/HydeExtension.php index 0eb9f6ccf0c..b83a83780bc 100644 --- a/packages/framework/src/Foundation/Concerns/HydeExtension.php +++ b/packages/framework/src/Foundation/Concerns/HydeExtension.php @@ -4,6 +4,7 @@ namespace Hyde\Foundation\Concerns; +use Hyde\Foundation\HydeKernel; use Hyde\Foundation\Kernel\FileCollection; use Hyde\Foundation\Kernel\PageCollection; use Hyde\Foundation\Kernel\RouteCollection; @@ -77,4 +78,22 @@ public function discoverRoutes(RouteCollection $collection): void { // } + + /** + * Register a callback to be run before the kernel is booted, + * which is before any file/page/route discovery has begun. + */ + public function booting(HydeKernel $kernel): void + { + // + } + + /** + * Register a callback to be run after the kernel is booted, + * which is after file/page/route discovery has completed. + */ + public function booted(HydeKernel $kernel): void + { + // + } } diff --git a/packages/framework/src/Foundation/Concerns/ManagesExtensions.php b/packages/framework/src/Foundation/Concerns/ManagesExtensions.php index 080c74e5344..52de766e3cf 100644 --- a/packages/framework/src/Foundation/Concerns/ManagesExtensions.php +++ b/packages/framework/src/Foundation/Concerns/ManagesExtensions.php @@ -54,7 +54,11 @@ public function registerExtension(string $extension): void throw new InvalidArgumentException("Extension [$extension] is already registered."); } - $this->extensions[$extension] = new $extension(); + $instance = new $extension(); + $this->extensions[$extension] = $instance; + + $this->booting([$instance, 'booting']); + $this->booted([$instance, 'booted']); } /** diff --git a/packages/framework/src/Foundation/Facades/Routes.php b/packages/framework/src/Foundation/Facades/Routes.php index f82acc2695e..7378f375b62 100644 --- a/packages/framework/src/Foundation/Facades/Routes.php +++ b/packages/framework/src/Foundation/Facades/Routes.php @@ -24,28 +24,46 @@ public static function getFacadeRoot(): RouteCollection return HydeKernel::getInstance()->routes(); } + /** + * Check if a route exists by its route key. + */ public static function exists(string $routeKey): bool { return static::getFacadeRoot()->has($routeKey); } - public static function get(string $routeKey): ?Route + /** + * Try to get a route by its route key. If it doesn't exist, null is returned. + */ + public static function find(string $routeKey): ?Route { return static::getFacadeRoot()->get($routeKey); } - /** @throws \Hyde\Framework\Exceptions\RouteNotFoundException */ - public static function getOrFail(string $routeKey): Route + /** + * Get a route by its route key. If it doesn't exist, an exception is thrown. + * + * @throws \Hyde\Framework\Exceptions\RouteNotFoundException + */ + public static function get(string $routeKey): Route { return static::getFacadeRoot()->getRoute($routeKey); } + /** + * Get all the routes for the site as a collection of route instances, keyed by route key. + * + * @return \Hyde\Foundation\Kernel\RouteCollection + */ public static function all(): RouteCollection { return static::getFacadeRoot()->getRoutes(); } - /** Get the current route for the page being rendered. */ + /** + * Get the route instance for the page currently being rendered. + * If a render is not in progress, this will return null. + */ public static function current(): ?Route { return Hyde::currentRoute(); diff --git a/packages/framework/src/Foundation/HydeCoreExtension.php b/packages/framework/src/Foundation/HydeCoreExtension.php index 6c48936a370..38772c55871 100644 --- a/packages/framework/src/Foundation/HydeCoreExtension.php +++ b/packages/framework/src/Foundation/HydeCoreExtension.php @@ -9,8 +9,11 @@ use Hyde\Pages\MarkdownPage; use Hyde\Pages\MarkdownPost; use Hyde\Pages\DocumentationPage; +use Hyde\Foundation\Kernel\PageCollection; use Hyde\Foundation\Concerns\HydeExtension; use Hyde\Facades\Features; +use Hyde\Framework\Features\Documentation\DocumentationSearchPage; +use Hyde\Framework\Features\Documentation\DocumentationSearchIndex; use function array_filter; use function array_keys; @@ -28,4 +31,15 @@ public static function getPageClasses(): array DocumentationPage::class => Features::hasDocumentationPages(), ], fn (bool $value): bool => $value)); } + + public function discoverPages(PageCollection $collection): void + { + if (Features::hasDocumentationSearch()) { + $collection->addPage(new DocumentationSearchIndex()); + + if (DocumentationSearchPage::enabled()) { + $collection->addPage(new DocumentationSearchPage()); + } + } + } } diff --git a/packages/framework/src/Foundation/HydeKernel.php b/packages/framework/src/Foundation/HydeKernel.php index f6ce8ea4eab..8dfc3fc8abe 100644 --- a/packages/framework/src/Foundation/HydeKernel.php +++ b/packages/framework/src/Foundation/HydeKernel.php @@ -4,9 +4,8 @@ namespace Hyde\Foundation; -use Hyde\Enums\Feature; use Hyde\Facades\Features; -use Hyde\Support\BuildWarnings; +use Hyde\Enums\Feature; use Hyde\Foundation\Kernel\Filesystem; use Hyde\Foundation\Kernel\Hyperlinks; use Hyde\Foundation\Kernel\FileCollection; @@ -15,14 +14,6 @@ use Hyde\Support\Contracts\SerializableContract; use Hyde\Support\Concerns\Serializable; use Illuminate\Support\Traits\Macroable; -use Illuminate\Support\Str; - -use function getcwd; -use function sprintf; -use function is_string; -use function var_export; -use function debug_backtrace; -use function trigger_deprecation; /** * Encapsulates a HydePHP project, providing helpful methods for interacting with it. @@ -54,6 +45,7 @@ class HydeKernel implements SerializableContract use Concerns\ManagesExtensions; use Concerns\ManagesViewData; use Concerns\BootsHydeKernel; + use Concerns\HasKernelData; use Serializable; use Macroable; @@ -67,6 +59,7 @@ class HydeKernel implements SerializableContract protected string $outputDirectory = '_site'; protected string $mediaDirectory = '_media'; + protected Features $features; protected Filesystem $filesystem; protected Hyperlinks $hyperlinks; @@ -82,6 +75,7 @@ class HydeKernel implements SerializableContract public function __construct(?string $basePath = null) { $this->setBasePath($basePath ?? getcwd()); + $this->filesystem = new Filesystem($this); $this->hyperlinks = new Hyperlinks($this); @@ -95,41 +89,12 @@ public static function version(): string public function features(): Features { - return new Features; + return $this->features ??= new Features(); } - public function hasFeature(Feature|string $feature): bool + public function hasFeature(Feature $feature): bool { - if (is_string($feature)) { - /** @see https://github.com/hydephp/develop/pull/1650 */ - - // @codeCoverageIgnoreStart - - $message = 'Passing a string to HydeKernel::hasFeature() is deprecated. Use a Feature enum case instead.'; - trigger_deprecation('hydephp/hyde', '1.5.0', $message); - - BuildWarnings::report(sprintf("$message\n Replace `%s` with `%s` \n in file %s:%s", - sprintf('HydeKernel::hasFeature(%s)', var_export($feature, true)), - sprintf('HydeKernel::hasFeature(Feature::%s)', Str::studly($feature)), - debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]['file'], - debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]['line'] - )); - - $feature = match ($feature) { - 'html-pages' => Feature::HtmlPages, - 'markdown-posts' => Feature::MarkdownPosts, - 'blade-pages' => Feature::BladePages, - 'markdown-pages' => Feature::MarkdownPages, - 'documentation-pages' => Feature::DocumentationPages, - 'darkmode' => Feature::Darkmode, - 'documentation-search' => Feature::DocumentationSearch, - 'torchlight' => Feature::Torchlight, - }; - - // @codeCoverageIgnoreEnd - } - - return Features::enabled($feature); + return Features::has($feature); } /** @inheritDoc */ @@ -146,6 +111,7 @@ public function toArray(): array 'files' => $this->files(), 'pages' => $this->pages(), 'routes' => $this->routes(), + 'authors' => $this->authors(), ]; } } diff --git a/packages/framework/src/Foundation/Internal/LoadYamlConfiguration.php b/packages/framework/src/Foundation/Internal/LoadYamlConfiguration.php index aae50195185..7a0e07e9d03 100644 --- a/packages/framework/src/Foundation/Internal/LoadYamlConfiguration.php +++ b/packages/framework/src/Foundation/Internal/LoadYamlConfiguration.php @@ -4,10 +4,15 @@ namespace Hyde\Foundation\Internal; +use Hyde\Enums\Feature; use Illuminate\Support\Arr; +use Illuminate\Support\Str; use Hyde\Foundation\Application; use Illuminate\Config\Repository; +use Hyde\Framework\Features\Blogging\Models\PostAuthor; +use Hyde\Framework\Exceptions\InvalidConfigurationException; +use function tap; use function array_merge; /** @@ -48,6 +53,15 @@ public function bootstrap(Application $app): void protected function mergeParsedConfiguration(): void { foreach ($this->yaml->getData() as $namespace => $data) { + if ($namespace === 'hyde' && isset($data['authors'])) { + // Todo: We may not actually need this, since the parser in the kernel can handle this. See https://github.com/hydephp/develop/pull/1824/commits/6a076b831b3cf07341605c314f0c29bfa4c0a8da + $data['authors'] = $this->parseAuthors($data['authors']); + } + + if ($namespace === 'hyde' && isset($data['features'])) { + $data['features'] = $this->parseFeatures($data['features']); + } + $this->mergeConfiguration($namespace, Arr::undot($data ?: [])); } } @@ -56,4 +70,35 @@ protected function mergeConfiguration(string $namespace, array $yaml): void { $this->config[$namespace] = array_merge($this->config[$namespace] ?? [], $yaml); } + + /** + * @param array}> $authors + * @return array + */ + protected function parseAuthors(array $authors): array + { + return Arr::mapWithKeys($authors, function (array $author, string $username): array { + $message = 'Invalid author configuration detected in the YAML config file. Please double check the syntax.'; + + return InvalidConfigurationException::try(fn () => [$username => PostAuthor::create($author)], $message); + }); + } + + /** + * @param array $features + * @return array<\Hyde\Enums\Feature> + */ + protected function parseFeatures(array $features): array + { + return array_map(function (string $feature): Feature { + $name = Str::studly($feature); + $case = Feature::fromName($name); + + if (! $case) { + throw new InvalidConfigurationException("Invalid feature '$feature' specified in the YAML config file. (Feature::$name does not exist)"); + } + + return $case; + }, $features); + } } diff --git a/packages/framework/src/Foundation/Kernel/Filesystem.php b/packages/framework/src/Foundation/Kernel/Filesystem.php index c4532c40a22..1c5c6730776 100644 --- a/packages/framework/src/Foundation/Kernel/Filesystem.php +++ b/packages/framework/src/Foundation/Kernel/Filesystem.php @@ -7,6 +7,7 @@ use Hyde\Hyde; use Hyde\Foundation\HydeKernel; use Hyde\Foundation\PharSupport; +use Hyde\Foundation\Concerns\HasMediaFiles; use Illuminate\Support\Collection; use Hyde\Framework\Actions\Internal\FileFinder; @@ -30,6 +31,8 @@ */ class Filesystem { + use HasMediaFiles; + protected HydeKernel $kernel; public function __construct(HydeKernel $kernel) @@ -92,18 +95,6 @@ public function pathToRelative(string $path): string : $path); } - /** - * Get the absolute path to the media source directory, or a file within it. - */ - public function mediaPath(string $path = ''): string - { - if (empty($path)) { - return $this->path(Hyde::getMediaDirectory()); - } - - return $this->path(path_join(Hyde::getMediaDirectory(), unslash($path))); - } - /** * Get the absolute path to the compiled site directory, or a file within it. */ @@ -116,20 +107,6 @@ public function sitePath(string $path = ''): string return $this->path(path_join(Hyde::getOutputDirectory(), unslash($path))); } - /** - * Get the absolute path to the compiled site's media directory, or a file within it. - */ - public function siteMediaPath(string $path = ''): string - { - if (empty($path)) { - return $this->sitePath(Hyde::getMediaOutputDirectory()); - } - - $path = unslash($path); - - return $this->sitePath(Hyde::getMediaOutputDirectory()."/$path"); - } - /** * Works similarly to the path() function, but returns a file in the Framework package. * diff --git a/packages/framework/src/Foundation/Kernel/Hyperlinks.php b/packages/framework/src/Foundation/Kernel/Hyperlinks.php index 2a94165d2c2..c23498ebae3 100644 --- a/packages/framework/src/Foundation/Kernel/Hyperlinks.php +++ b/packages/framework/src/Foundation/Kernel/Hyperlinks.php @@ -5,17 +5,14 @@ namespace Hyde\Foundation\Kernel; use Hyde\Facades\Config; +use BadMethodCallException; use Hyde\Support\Models\Route; use Hyde\Foundation\HydeKernel; -use JetBrains\PhpStorm\Deprecated; -use Hyde\Framework\Exceptions\BaseUrlNotSetException; -use Hyde\Framework\Exceptions\FileNotFoundException; -use Illuminate\Support\Str; +use Hyde\Support\Filesystem\MediaFile; use function str_ends_with; use function str_starts_with; use function substr_count; -use function file_exists; use function str_replace; use function str_repeat; use function substr; @@ -89,43 +86,17 @@ public function relativeLink(string $destination): string } /** - * Gets a relative web link to the given file stored in the _site/media folder. + * Gets a MediaAsset instance for the given file stored in the `_site/media` folder. + * The returned value can be cast into a string in Blade views to resole the URL. * - * An exception will be thrown if the file does not exist in the _media directory, - * and the second argument is set to true. + * If a base URL is configured, the image will be returned with a qualified absolute URL. + * Otherwise, a relative path will be returned based on the rendered page's location. * - * @deprecated This method will be removed in v2.0. Please use `asset()` instead. + * @throws \Hyde\Framework\Exceptions\FileNotFoundException If the file does not exist in the `_media` source directory. */ - #[Deprecated(reason: 'Use `asset` method instead.', replacement: '%class%->asset(%parameter0%)')] - public function mediaLink(string $destination, bool $validate = false): string + public function asset(string $name): MediaFile { - if ($validate && ! file_exists($sourcePath = "{$this->kernel->getMediaDirectory()}/$destination")) { - throw new FileNotFoundException($sourcePath); - } - - return $this->relativeLink("{$this->kernel->getMediaOutputDirectory()}/$destination"); - } - - /** - * Gets a relative web link to the given image stored in the _site/media folder. - * If the image is remote (starts with http) it will be returned as is. - * - * If true is passed as the second argument, and a base URL is set, - * the image will be returned with a qualified absolute URL. - */ - public function asset(string $name, bool $preferQualifiedUrl = false): string - { - if (static::isRemote($name)) { - return $name; - } - - $name = Str::start($name, "{$this->kernel->getMediaOutputDirectory()}/"); - - if ($preferQualifiedUrl && $this->hasSiteUrl()) { - return $this->url($name); - } - - return $this->relativeLink($name); + return MediaFile::get($name); } /** @@ -144,8 +115,9 @@ public function hasSiteUrl(): bool * Return a qualified URL to the supplied path if a base URL is set. * * @param string $path An optional relative path suffix. Omit to return the base URL. + * @return string The qualified URL, or the base URL if no path is supplied. * - * @throws BaseUrlNotSetException If no site URL is set and no path is provided. + * @throws BadMethodCallException If the site URL is not set in the configuration and no path is supplied. */ public function url(string $path = ''): string { @@ -165,9 +137,8 @@ public function url(string $path = ''): string return $path; } - // User is trying to get the base URL, but it's not set - // This exception is deprecated and will be removed in v2.0.0, and we will throw a BadMethodCallException instead. - throw new BaseUrlNotSetException(); + // User is trying to get the base URL, but it's not set, so we throw an exception. + throw new BadMethodCallException('The site URL is not set in the configuration.'); } /** diff --git a/packages/framework/src/Foundation/Providers/NavigationServiceProvider.php b/packages/framework/src/Foundation/Providers/NavigationServiceProvider.php new file mode 100644 index 00000000000..02af9f13ec1 --- /dev/null +++ b/packages/framework/src/Foundation/Providers/NavigationServiceProvider.php @@ -0,0 +1,27 @@ +app->make(HydeKernel::class)->booted(function (): void { + $this->app->singleton('navigation.main', function (): MainNavigationMenu { + return NavigationMenuGenerator::handle(MainNavigationMenu::class); + }); + + $this->app->singleton('navigation.sidebar', function (): DocumentationSidebar { + return NavigationMenuGenerator::handle(DocumentationSidebar::class); + }); + }); + } +} diff --git a/packages/framework/src/Framework/Actions/GeneratesDocumentationSearchIndex.php b/packages/framework/src/Framework/Actions/GeneratesDocumentationSearchIndex.php index 1e0948e47cf..7cd75691d9d 100644 --- a/packages/framework/src/Framework/Actions/GeneratesDocumentationSearchIndex.php +++ b/packages/framework/src/Framework/Actions/GeneratesDocumentationSearchIndex.php @@ -4,9 +4,7 @@ namespace Hyde\Framework\Actions; -use Hyde\Hyde; use Hyde\Facades\Config; -use Hyde\Facades\Filesystem; use Hyde\Framework\Concerns\InteractsWithDirectories; use Hyde\Pages\DocumentationPage; use Illuminate\Support\Collection; @@ -16,17 +14,16 @@ use function trim; /** - * @internal Generate a JSON file that can be used as a search index for documentation pages. + * @internal Generate a JSON string that can be used as a search index for documentation pages. */ class GeneratesDocumentationSearchIndex { use InteractsWithDirectories; protected Collection $index; - protected string $path; /** - * Generate the search index and save it to disk. + * @since v2.x This method returns the JSON string instead of saving it to disk and returning the path. * * @return string The path to the generated file. */ @@ -34,21 +31,19 @@ public static function handle(): string { $service = new static(); $service->run(); - $service->save(); - return $service->path; + return $service->index->toJson(); } protected function __construct() { $this->index = new Collection(); - $this->path = $this->getPath(); } protected function run(): void { DocumentationPage::all()->each(function (DocumentationPage $page): void { - if (! in_array($page->identifier, Config::getArray('docs.exclude_from_search', []))) { + if (! in_array($page->identifier, $this->getPagesToExcludeFromSearch())) { $this->index->push($this->generatePageEntry($page)); } }); @@ -67,13 +62,6 @@ protected function generatePageEntry(DocumentationPage $page): array ]; } - protected function save(): void - { - $this->needsParentDirectory($this->path); - - Filesystem::putContents($this->path, $this->index->toJson()); - } - protected function getSearchContentForDocument(DocumentationPage $page): string { return (new ConvertsMarkdownToPlainText($page->markdown->body()))->execute(); @@ -88,8 +76,10 @@ protected function formatDestination(string $slug): string return "$slug.html"; } - protected function getPath(): string + protected function getPagesToExcludeFromSearch(): array { - return Hyde::sitePath(DocumentationPage::outputDirectory().'/search.json'); + return array_merge(Config::getArray('docs.exclude_from_search', []), + Config::getBool('docs.create_search_page', true) ? ['search'] : [] + ); } } diff --git a/packages/framework/src/Framework/Actions/GeneratesTableOfContents.php b/packages/framework/src/Framework/Actions/GeneratesTableOfContents.php index 0a68d1b032b..45ade6461f1 100644 --- a/packages/framework/src/Framework/Actions/GeneratesTableOfContents.php +++ b/packages/framework/src/Framework/Actions/GeneratesTableOfContents.php @@ -6,64 +6,177 @@ use Hyde\Facades\Config; use Hyde\Markdown\Models\Markdown; -use League\CommonMark\Environment\Environment; -use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension; -use League\CommonMark\Extension\HeadingPermalink\HeadingPermalinkExtension; -use League\CommonMark\Extension\TableOfContents\TableOfContentsExtension; -use League\CommonMark\MarkdownConverter; - -use function strpos; -use function substr; +use Hyde\Markdown\Processing\HeadingRenderer; /** - * Generates a table of contents for the Markdown document, most commonly used for the sidebar. + * Generates a nested table of contents from Markdown headings. */ class GeneratesTableOfContents { protected string $markdown; + protected int $minHeadingLevel = 2; + protected int $maxHeadingLevel = 4; + public function __construct(Markdown|string $markdown) { $this->markdown = (string) $markdown; + + $this->minHeadingLevel = Config::getInt('docs.sidebar.table_of_contents.min_heading_level', 2); + $this->maxHeadingLevel = Config::getInt('docs.sidebar.table_of_contents.max_heading_level', 4); + } + + /** @return array */ + public function execute(): array + { + return $this->buildTableOfContents($this->parseHeadings()); + } + + /** @return array */ + protected function parseHeadings(): array + { + $matches = $this->matchHeadingPatterns(); + $headings = []; + + foreach ($matches[0] as $index => $heading) { + $headingData = $this->parseHeadingData($heading, $matches, $index); + + if ($headingData !== null) { + $headings[] = $this->createHeadingEntry($headingData); + } + } + + return $headings; } - public function execute(): string + /** @return array{0: array, 1: array, 2: array, 3: array} */ + protected function matchHeadingPatterns(): array { - $config = [ - 'table_of_contents' => [ - 'html_class' => 'table-of-contents', - 'position' => 'placeholder', - 'placeholder' => '[[START_TOC]]', - 'style' => 'bullet', - 'min_heading_level' => Config::getInt('docs.table_of_contents.min_heading_level', 2), - 'max_heading_level' => Config::getInt('docs.table_of_contents.max_heading_level', 4), - 'normalize' => 'relative', - ], - 'heading_permalink' => [ - 'fragment_prefix' => '', - ], + // Match both ATX-style (###) and Setext-style (===, ---) headers + $pattern = '/^(?:#{1,6}\s+(.+)|(.+)\n([=\-])\3+)$/m'; + + preg_match_all($pattern, $this->markdown, $matches); + + return $matches; + } + + /** + * @param array{0: array, 1: array, 2: array, 3: array} $matches + * @return array{level: int, title: string}|null + */ + protected function parseHeadingData(string $heading, array $matches, int $index): ?array + { + if (str_starts_with($heading, '#')) { + return $this->parseAtxHeading($heading, $matches[1][$index]); + } + + return $this->parseSetextHeading($matches[2][$index], $matches[3][$index]); + } + + /** @return array{level: int, title: string} */ + protected function parseAtxHeading(string $heading, string $title): array + { + return [ + 'level' => substr_count($heading, '#'), + 'title' => $title, ]; + } - $environment = new Environment($config); - $environment->addExtension(new CommonMarkCoreExtension()); - $environment->addExtension(new HeadingPermalinkExtension()); - $environment->addExtension(new TableOfContentsExtension()); + /** @return array{level: int, title: string}|null */ + protected function parseSetextHeading(string $title, string $marker): ?array + { + $level = $marker === '=' ? 1 : 2; - $converter = new MarkdownConverter($environment); - $html = $converter->convert($this->markdown."\n[[START_TOC]]")->getContent(); + if ($level < $this->minHeadingLevel) { + return null; + } - return $this->extractTableOfContents($html); + return [ + 'level' => $level, + 'title' => trim($title), + ]; } - protected function extractTableOfContents(string $html): string + /** + * @param array{level: int, title: string} $headingData + * @return array{level: int, title: string, identifier: string} + */ + protected function createHeadingEntry(array $headingData): array { - // The table of contents is always at the end of the document, so we can just strip everything before it. - $position = strpos($html, '