diff --git a/.env.example b/.env.example index 8ffcb39..3557091 100644 --- a/.env.example +++ b/.env.example @@ -9,3 +9,18 @@ HOSTNAME= CLOUDFLARE_API_TOKEN=<***> # Your Cloudflare email CLOUDFLARE_EMAIL=<***> + +# Observability - Posthog +# See https://app.posthog.com/project/settings +VITE_PUBLIC_POSTHOG_HOST= +VITE_PUBLIC_POSTHOG_KEY=<***> +VITE_PUBLIC_POSTHOG_DEBUG= +VITE_PUBLIC_POSTHOG_ENABLED= + +# Observability - Posthog sourcemap upload (CI only) +# See https://app.posthog.com/settings/project#variables +POSTHOG_CLI_HOST= +POSTHOG_CLI_ENV_ID=<***> +# Personal API key with error tracking write and organization read scopes +# See https://app.posthog.com/settings/user-api-keys#variables +POSTHOG_CLI_TOKEN=<***> diff --git a/.github/workflows/cleanup-preview.yaml b/.github/workflows/cleanup-preview.yaml new file mode 100644 index 0000000..e42d219 --- /dev/null +++ b/.github/workflows/cleanup-preview.yaml @@ -0,0 +1,48 @@ +# This workflow destroys the preview environment/deployment when a PR is closed. +# It prevents preview resources from leaking/stale after close. + +name: Cleanup Preview +on: + pull_request: + branches: + - main + types: + - closed +concurrency: + group: cleanup-preview-${{ github.ref }} + cancel-in-progress: false +env: + NODE_VERSION: '24.11.1' + PNPM_VERSION: '10.23.0' + STAGE: ${{ format('preview-{0}', github.event.number) }} +jobs: + cleanup: + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + pull-requests: write + steps: + - uses: actions/checkout@v4 + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: ${{ env.PNPM_VERSION }} + run_install: false + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: pnpm + - name: Install dependencies + run: pnpm install + - name: Destroy Preview Environment + run: pnpm alchemy destroy --stage ${{ env.STAGE }} + env: + # Alchemy + HOSTNAME: ${{ vars.HOSTNAME }} + ALCHEMY_SECRET: ${{ secrets.ALCHEMY_SECRET }} + ALCHEMY_STATE_TOKEN: ${{ secrets.ALCHEMY_STATE_TOKEN }} + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_EMAIL: ${{ secrets.CLOUDFLARE_EMAIL }} + PULL_REQUEST: ${{ github.event.number }} diff --git a/.github/workflows/deployment-preview.yaml b/.github/workflows/deployment-preview.yaml new file mode 100644 index 0000000..05be620 --- /dev/null +++ b/.github/workflows/deployment-preview.yaml @@ -0,0 +1,63 @@ +# Deploys a per-PR preview environment for this repository. +# Triggered when a PR targeting main (commonly from release/* or hotfix/*) is opened, reopened, or updated (synchronize). +# Deploys to the "preview" environment with stage name "preview-" . + +name: Deployment (Preview) +on: + pull_request: + branches: + - main + types: + - opened + - reopened + - synchronize +concurrency: + group: deployment-preview-${{ github.ref }} + cancel-in-progress: false +env: + NODE_VERSION: '24.11.1' + PNPM_VERSION: '10.23.0' + STAGE: ${{ format('preview-{0}', github.event.number) }} +jobs: + deploy: + runs-on: ubuntu-latest + environment: + name: preview + permissions: + contents: read + pull-requests: write + steps: + - uses: actions/checkout@v4 + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: ${{ env.PNPM_VERSION }} + run_install: false + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: pnpm + - name: Install dependencies + run: pnpm install + - name: Build and deploy + run: pnpm alchemy deploy --stage ${{ env.STAGE }} + env: + # Alchemy + HOSTNAME: ${{ vars.HOSTNAME }} + ALCHEMY_SECRET: ${{ secrets.ALCHEMY_SECRET }} + ALCHEMY_STATE_TOKEN: ${{ secrets.ALCHEMY_STATE_TOKEN }} + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_EMAIL: ${{ secrets.CLOUDFLARE_EMAIL }} + PULL_REQUEST: ${{ github.event.number }} + GITHUB_SHA: ${{ github.sha }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # Build + VITE_PUBLIC_POSTHOG_HOST: ${{ vars.VITE_PUBLIC_POSTHOG_HOST }} + VITE_PUBLIC_POSTHOG_KEY: ${{ secrets.VITE_PUBLIC_POSTHOG_KEY }} + VITE_PUBLIC_POSTHOG_DEBUG: ${{ vars.VITE_PUBLIC_POSTHOG_DEBUG }} + VITE_PUBLIC_POSTHOG_ENABLED: ${{ vars.VITE_PUBLIC_POSTHOG_ENABLED }} + # Sourcemap inject and upload + POSTHOG_CLI_HOST: ${{ vars.POSTHOG_CLI_HOST }} + POSTHOG_CLI_ENV_ID: ${{ secrets.POSTHOG_CLI_ENV_ID }} + POSTHOG_CLI_TOKEN: ${{ secrets.POSTHOG_CLI_TOKEN }} diff --git a/.github/workflows/deployment-production.yaml b/.github/workflows/deployment-production.yaml new file mode 100644 index 0000000..d0492ed --- /dev/null +++ b/.github/workflows/deployment-production.yaml @@ -0,0 +1,54 @@ +# Deploys the main branch to the production environment on every push to main. +# Deploys to the GitHub environment "production" with stage name "production". + +name: Deployment (Production) +on: + push: + branches: + - main +concurrency: + group: deployment-production-${{ github.ref }} + cancel-in-progress: false +env: + NODE_VERSION: '24.11.1' + PNPM_VERSION: '10.23.0' + STAGE: 'production' +jobs: + deploy: + runs-on: ubuntu-latest + environment: + name: production + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: ${{ env.PNPM_VERSION }} + run_install: false + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: pnpm + - name: Install dependencies + run: pnpm install + - name: Build and deploy + run: pnpm alchemy deploy --stage ${{ env.STAGE }} + env: + # Alchemy + HOSTNAME: ${{ vars.HOSTNAME }} + ALCHEMY_SECRET: ${{ secrets.ALCHEMY_SECRET }} + ALCHEMY_STATE_TOKEN: ${{ secrets.ALCHEMY_STATE_TOKEN }} + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_EMAIL: ${{ secrets.CLOUDFLARE_EMAIL }} + # Build + VITE_PUBLIC_POSTHOG_HOST: ${{ vars.VITE_PUBLIC_POSTHOG_HOST }} + VITE_PUBLIC_POSTHOG_KEY: ${{ secrets.VITE_PUBLIC_POSTHOG_KEY }} + VITE_PUBLIC_POSTHOG_DEBUG: ${{ vars.VITE_PUBLIC_POSTHOG_DEBUG }} + VITE_PUBLIC_POSTHOG_ENABLED: ${{ vars.VITE_PUBLIC_POSTHOG_ENABLED }} + # Sourcemap inject and upload + POSTHOG_CLI_HOST: ${{ vars.POSTHOG_CLI_HOST }} + POSTHOG_CLI_ENV_ID: ${{ secrets.POSTHOG_CLI_ENV_ID }} + POSTHOG_CLI_TOKEN: ${{ secrets.POSTHOG_CLI_TOKEN }} diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml deleted file mode 100644 index 8202d71..0000000 --- a/.github/workflows/deployment.yml +++ /dev/null @@ -1,88 +0,0 @@ -name: Deployment -on: - push: - branches: - - main - pull_request: - branches: - - main - types: - - opened - - reopened - - synchronize - - closed -concurrency: - group: deployment-${{ github.ref }} - cancel-in-progress: false -env: - NODE_VERSION: '24.11.1' - PNPM_VERSION: '10.23.0' - STAGE: ${{ github.event_name == 'pull_request' && format('preview-{0}', github.event.number) || (github.ref == 'refs/heads/main' && 'production' || github.ref_name) }} -jobs: - deploy: - if: ${{ github.event.action != 'closed' }} - runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: write - steps: - - uses: actions/checkout@v4 - - name: Setup pnpm - uses: pnpm/action-setup@v4 - with: - version: ${{ env.PNPM_VERSION }} - run_install: false - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: ${{ env.NODE_VERSION }} - cache: pnpm - - name: Install dependencies - run: pnpm install - - name: Deploy - run: pnpm alchemy deploy --stage ${{ env.STAGE }} - env: - HOSTNAME: ${{ vars.HOSTNAME }} - ALCHEMY_SECRET: ${{ secrets.ALCHEMY_SECRET }} - ALCHEMY_STATE_TOKEN: ${{ secrets.ALCHEMY_STATE_TOKEN }} - CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} - CLOUDFLARE_EMAIL: ${{ secrets.CLOUDFLARE_EMAIL }} - PULL_REQUEST: ${{ github.event.number }} - GITHUB_SHA: ${{ github.sha }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - cleanup: - runs-on: ubuntu-latest - if: ${{ github.event_name == 'pull_request' && github.event.action == 'closed' }} - permissions: - id-token: write - contents: read - pull-requests: write - steps: - - uses: actions/checkout@v4 - - name: Setup pnpm - uses: pnpm/action-setup@v4 - with: - version: ${{ env.PNPM_VERSION }} - run_install: false - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: ${{ env.NODE_VERSION }} - cache: pnpm - - name: Install dependencies - run: pnpm install - - name: Safety Check - run: |- - if [ "${{ env.STAGE }}" = "production" ]; then - echo "ERROR: Cannot destroy production environment in cleanup job" - exit 1 - fi - - name: Destroy Preview Environment - run: pnpm alchemy destroy --stage ${{ env.STAGE }} - env: - HOSTNAME: ${{ vars.HOSTNAME }} - ALCHEMY_SECRET: ${{ secrets.ALCHEMY_SECRET }} - ALCHEMY_STATE_TOKEN: ${{ secrets.ALCHEMY_STATE_TOKEN }} - CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} - CLOUDFLARE_EMAIL: ${{ secrets.CLOUDFLARE_EMAIL }} - PULL_REQUEST: ${{ github.event.number }} diff --git a/.oxfmtrc.json b/.oxfmtrc.json index 2b1769a..334cd76 100644 --- a/.oxfmtrc.json +++ b/.oxfmtrc.json @@ -13,7 +13,32 @@ "bracketSameLine": false, "singleAttributePerLine": false, "arrowParens": "always", - "ignorePatterns": ["pnpm-lock.yaml", "routeTree.gen.ts", "public/"], + "ignorePatterns": [ + "pnpm-lock.yaml", + "routeTree.gen.ts", + "public/", + "src/lib/i18n/" + ], "experimentalSortPackageJson": true, - "experimentalSortImports": { "order": "asc" } + "experimentalSortImports": { + "order": "asc", + "internalPattern": ["~/"], + "groups": [ + ["side-effect"], + ["side-effect-style"], + ["builtin"], + ["external", "external-type"], + ["internal", "internal-type"], + ["parent", "parent-type"], + ["sibling", "sibling-type"], + ["index", "index-type"] + ] + }, + "experimentalTailwindcss": { + "stylesheet": "./src/ui/styles/app.css", + "attributes": ["class", "className", "classNames"], + "functions": ["clsx", "twMerge", "cn", "cva"], + "preserveDuplicates": false, + "preserveWhitespace": false + } } diff --git a/.oxlintrc.json b/.oxlintrc.json index f32c073..715d339 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -1,6 +1,11 @@ { "$schema": "./node_modules/oxlint/configuration_schema.json", - "ignorePatterns": ["pnpm-lock.yaml", "routeTree.gen.ts", "public/"], + "ignorePatterns": [ + "pnpm-lock.yaml", + "routeTree.gen.ts", + "public/", + "src/lib/i18n/" + ], "env": { "browser": true, "node": true @@ -63,7 +68,7 @@ "react/rules-of-hooks": "error", // Style - "capitalized-comments": "warn", + "capitalized-comments": "off", "default-case-last": "error", "default-param-last": "error", "func-names": ["error", "as-needed", { "generators": "as-needed" }], diff --git a/.storybook/main.ts b/.storybook/main.ts index 1ae8f29..e05f4d4 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -3,6 +3,13 @@ import { defineMain } from '@storybook/react-vite/node'; export default defineMain({ stories: ['../src/**/*.stories.@(js|jsx|mjs|ts|tsx|mdx)'], staticDirs: ['../public'], - framework: '@storybook/react-vite', addons: ['@storybook/addon-docs'], + framework: { + name: '@storybook/react-vite', + options: { + builder: { + viteConfigPath: './vite.storybook.ts', + }, + }, + }, }); diff --git a/.storybook/preview.ts b/.storybook/preview.ts index d679584..d6dc4a6 100644 --- a/.storybook/preview.ts +++ b/.storybook/preview.ts @@ -1,8 +1,9 @@ +import '~/ui/styles/app.css'; +import '~/ui/styles/fonts.css'; + import addonDocs from '@storybook/addon-docs'; import { definePreview } from '@storybook/react-vite'; -import '../src/ui/styles/app.css'; - export default definePreview({ addons: [addonDocs()], tags: ['autodocs'], diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 4ea727f..fc5a256 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,3 +1,7 @@ { - "recommendations": ["oxc.oxc-vscode", "bradlc.vscode-tailwindcss"] + "recommendations": [ + "oxc.oxc-vscode", + "bradlc.vscode-tailwindcss", + "inlang.vs-code-extension" + ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index 09ecbb7..d948946 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -28,6 +28,7 @@ // Extension - Oxc "oxc.enable": true, + "oxc.fmt.configPath": ".oxfmtrc.json", // Extension - Tailwind CSS "tailwindCSS.classAttributes": ["class", "className", "classNames"], diff --git a/CHANGELOG.md b/CHANGELOG.md index 78a3b18..1c67540 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,31 @@ # Changelog +## [0.3.0](https://github.com/devsantara/kit/compare/0.2.0...0.3.0) (2026-01-16) + +### Features + +- **i18n:** setup paraglide ([3435a64](https://github.com/devsantara/kit/commit/3435a647c1600d1264ca4195a9571d69952427e6)) +- **observability:** setup posthog ([7f4c399](https://github.com/devsantara/kit/commit/7f4c3991d99acb437e80fa0b35d6c404623f89b9)) +- **seo:** add metadata, opengraph, web manifest and icons ([5c0e864](https://github.com/devsantara/kit/commit/5c0e864792972ba09239d8408365a48d7bf74f6d)) +- **seo:** create head metadata builder ([b3c41ec](https://github.com/devsantara/kit/commit/b3c41ec2378fb83a4d491a548c304f76d73973c5)) + +### Bug Fixes + +- **storybook:** missing iframe.html on production build ([13ff3ab](https://github.com/devsantara/kit/commit/13ff3ab1d6debda0a39ceeb23125d4038f676487)) + +### Build System + +- manually split posthog-js and @posthog/react from main bundle ([a0ecfb0](https://github.com/devsantara/kit/commit/a0ecfb040041cadfa1d519f86ad828855d6740c0)) + +### Continuous Integration + +- **workflow:** split deployment and cleanup workflows ([1e2753a](https://github.com/devsantara/kit/commit/1e2753a437378dec73416946e2e23d8fc3b6467a)) + +### Chores + +- **linter:** turn off capitalized comments rule ([ea65319](https://github.com/devsantara/kit/commit/ea6531995169a001a4b60d62b3c0ace0cea91788)) +- **oxfmt:** update and add import and tailwindcss options ([c56f260](https://github.com/devsantara/kit/commit/c56f2608bb0cf6a0246900fc5126eef9da8ffff9)) + ## [0.2.0](https://github.com/devsantara/kit/compare/0.1.0...0.2.0) (2026-01-06) ### Features diff --git a/messages/en.json b/messages/en.json new file mode 100644 index 0000000..4e2a872 --- /dev/null +++ b/messages/en.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://inlang.com/schema/inlang-message-format", + "app_name": "Devsantara Kit", + "app_description": "The blueprint for your next big idea" +} diff --git a/messages/id.json b/messages/id.json new file mode 100644 index 0000000..3f79448 --- /dev/null +++ b/messages/id.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://inlang.com/schema/inlang-message-format", + "app_name": "Devsantara Kit", + "app_description": "Rancangan dasar untuk ide besar kamu berikutnya" +} diff --git a/messages/zh-CN.json b/messages/zh-CN.json new file mode 100644 index 0000000..095667c --- /dev/null +++ b/messages/zh-CN.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://inlang.com/schema/inlang-message-format", + "app_name": "Devsantara 工具包", + "app_description": "您下一个伟大构想的基石计划" +} diff --git a/package.json b/package.json index 2ff3e0d..cbea9de 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "kit", - "version": "0.2.0", + "version": "0.3.0", "private": true, "description": "The blueprint for your next big idea", "keywords": [], @@ -25,6 +25,8 @@ "alchemy:dev": "alchemy dev --env-file ./.env", "alchemy:deploy": "alchemy deploy --env-file ./.env", "alchemy:destroy": "alchemy destroy --env-file ./.env", + "sourcemap:inject": "posthog-cli sourcemap inject --directory ./dist", + "sourcemap:upload": "posthog-cli sourcemap upload --directory ./dist --delete-after", "commitlint": "commitlint --edit", "format": "oxfmt", "format:check": "oxfmt --check", @@ -40,6 +42,7 @@ "dependencies": { "@fontsource-variable/jetbrains-mono": "^5.2.8", "@fontsource-variable/plus-jakarta-sans": "^5.2.8", + "@posthog/react": "^1.5.2", "@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-aspect-ratio": "^1.1.8", @@ -78,6 +81,8 @@ "input-otp": "^1.4.2", "lucide-react": "^0.562.0", "next-themes": "^0.4.6", + "posthog-js": "^1.314.0", + "posthog-node": "^5.19.0", "react": "^19.2.3", "react-day-picker": "^9.13.0", "react-dom": "^19.2.3", @@ -95,6 +100,10 @@ "@commitlint/cli": "^20.3.0", "@commitlint/config-conventional": "^20.3.0", "@commitlint/types": "^20.2.0", + "@inlang/cli": "^3.0.0", + "@inlang/paraglide-js": "^2.8.0", + "@posthog/cli": "^0.5.22", + "@posthog/rollup-plugin": "^1.1.7", "@release-it/conventional-changelog": "10.0.4", "@storybook/addon-docs": "^10.1.11", "@storybook/react-vite": "^10.1.11", @@ -110,7 +119,7 @@ "conventional-changelog-conventionalcommits": "9.1.0", "husky": "^9.1.7", "lint-staged": "^16.2.7", - "oxfmt": "^0.22.0", + "oxfmt": "^0.24.0", "oxlint": "^1.37.0", "release-it": "19.2.2", "storybook": "^10.1.11", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e773d65..5b1c80e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,6 +18,9 @@ importers: '@fontsource-variable/plus-jakarta-sans': specifier: ^5.2.8 version: 5.2.8 + '@posthog/react': + specifier: ^1.5.2 + version: 1.5.2(@types/react@19.2.7)(posthog-js@1.314.0)(react@19.2.3) '@radix-ui/react-accordion': specifier: ^1.2.12 version: 1.2.12(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -132,6 +135,12 @@ importers: next-themes: specifier: ^0.4.6 version: 0.4.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + posthog-js: + specifier: ^1.314.0 + version: 1.314.0 + posthog-node: + specifier: ^5.19.0 + version: 5.19.0 react: specifier: ^19.2.3 version: 19.2.3 @@ -178,6 +187,18 @@ importers: '@commitlint/types': specifier: ^20.2.0 version: 20.2.0 + '@inlang/cli': + specifier: ^3.0.0 + version: 3.1.2 + '@inlang/paraglide-js': + specifier: ^2.8.0 + version: 2.8.0 + '@posthog/cli': + specifier: ^0.5.22 + version: 0.5.22 + '@posthog/rollup-plugin': + specifier: ^1.1.7 + version: 1.1.7(rollup@4.55.1) '@release-it/conventional-changelog': specifier: 10.0.4 version: 10.0.4(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.2.1)(release-it@19.2.2(@types/node@25.0.3)) @@ -210,7 +231,7 @@ importers: version: 5.1.2(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2)) alchemy: specifier: ^0.83.0 - version: 0.83.0(@cloudflare/vite-plugin@1.19.0(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2))(workerd@1.20251217.0)(wrangler@4.56.0(@cloudflare/workers-types@4.20260103.0)))(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2))(workerd@1.20251217.0)(wrangler@4.56.0(@cloudflare/workers-types@4.20260103.0)) + version: 0.83.0(@cloudflare/vite-plugin@1.19.0(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2))(workerd@1.20251217.0)(wrangler@4.56.0(@cloudflare/workers-types@4.20260103.0)))(kysely@0.27.6)(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2))(workerd@1.20251217.0)(wrangler@4.56.0(@cloudflare/workers-types@4.20260103.0)) babel-plugin-react-compiler: specifier: ^1.0.0 version: 1.0.0 @@ -224,8 +245,8 @@ importers: specifier: ^16.2.7 version: 16.2.7 oxfmt: - specifier: ^0.22.0 - version: 0.22.0 + specifier: ^0.24.0 + version: 0.24.0 oxlint: specifier: ^1.37.0 version: 1.37.0(oxlint-tsgolint@0.10.0) @@ -1226,6 +1247,22 @@ packages: cpu: [x64] os: [win32] + '@inlang/cli@3.1.2': + resolution: {integrity: sha512-Dwt9b21N0qVZv//x5DVhNTHWE4rl7XFY/Jvx6dir3/EphyKVxBOHKq0RtYEB48BrE/OHIru6tmNJgmuAPsVETA==} + engines: {node: '>=18.0.0'} + hasBin: true + + '@inlang/paraglide-js@2.8.0': + resolution: {integrity: sha512-ataaSmV53zz+tIr+KJLdC3tTB1uikS79hvtLlZk2ikbGRB/kcyQeg+lsqzjsXCAvy0/O28ucCRjxbHsTzOVQVg==} + hasBin: true + + '@inlang/recommend-sherlock@0.2.1': + resolution: {integrity: sha512-ckv8HvHy/iTqaVAEKrr+gnl+p3XFNwe5D2+6w6wJk2ORV2XkcRkKOJ/XsTUJbPSiyi4PI+p+T3bqbmNx/rDUlg==} + + '@inlang/sdk@2.6.0': + resolution: {integrity: sha512-f4iVHVXyzOi0CXlXSAT7XPrReLBaVXy/po/qrOPf2OHh+hUwyD1bDx2EYC5KgrZ16z3ylWfqWVuc7o4l7/tuUQ==} + engines: {node: '>=18.0.0'} + '@inquirer/ansi@1.0.2': resolution: {integrity: sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==} engines: {node: '>=18'} @@ -1400,6 +1437,13 @@ packages: '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + '@lix-js/sdk@0.4.7': + resolution: {integrity: sha512-pRbW+joG12L0ULfMiWYosIW0plmW4AsUdiPCp+Z8rAsElJ+wJ6in58zhD3UwUcd4BNcpldEGjg6PdA7e0RgsDQ==} + engines: {node: '>=18'} + + '@lix-js/server-protocol-schema@0.1.1': + resolution: {integrity: sha512-jBeALB6prAbtr5q4vTuxnRZZv1M2rKe8iNqRQhFJ4Tv7150unEa0vKyz0hs8Gl3fUGsWaNJBh3J8++fpbrpRBQ==} + '@mdx-js/react@3.1.1': resolution: {integrity: sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw==} peerDependencies: @@ -1535,43 +1579,43 @@ packages: resolution: {integrity: sha512-hAX0pT/73190NLqBPPWSdBVGtbY6VOhWYK3qqHqtXQ1gK7kS2yz4+ivsN07hpJ6I3aeMtKP6J6npsEKOAzuTLA==} engines: {node: '>=20.0'} - '@oxfmt/darwin-arm64@0.22.0': - resolution: {integrity: sha512-dhz2m2uLrHT3MwM+LAdvr97EojJZTwaZ6BuMTRftJzqa9dHYDG/MtSBuDD2DpGpZ0SM2iVwni2wCzCYGKTojbA==} + '@oxfmt/darwin-arm64@0.24.0': + resolution: {integrity: sha512-aYXuGf/yq8nsyEcHindGhiz9I+GEqLkVq8CfPbd+6VE259CpPEH+CaGHEO1j6vIOmNr8KHRq+IAjeRO2uJpb8A==} cpu: [arm64] os: [darwin] - '@oxfmt/darwin-x64@0.22.0': - resolution: {integrity: sha512-VykUbibvqSOG5YIFUMpHtZVrY1YKDl9Il2SvFemUfR5Ac1t1BFZOnazYe98jtZGFY4sEdEORs0ImBARnyMX/hw==} + '@oxfmt/darwin-x64@0.24.0': + resolution: {integrity: sha512-vs3b8Bs53hbiNvcNeBilzE/+IhDTWKjSBB3v/ztr664nZk65j0xr+5IHMBNz3CFppmX7o/aBta2PxY+t+4KoPg==} cpu: [x64] os: [darwin] - '@oxfmt/linux-arm64-gnu@0.22.0': - resolution: {integrity: sha512-y0MBha/K34TztYAZUn6KQE9xLPLNHqRpOdzRp96fhkbrQTeEXo+jF+8+aV8VnqjG0y7p+IQN4ATxNSstSPO9sA==} + '@oxfmt/linux-arm64-gnu@0.24.0': + resolution: {integrity: sha512-ItPDOPoQ0wLj/s8osc5ch57uUcA1Wk8r0YdO8vLRpXA3UNg7KPOm1vdbkIZRRiSUphZcuX5ioOEetEK8H7RlTw==} cpu: [arm64] os: [linux] - '@oxfmt/linux-arm64-musl@0.22.0': - resolution: {integrity: sha512-8a0p2UEmavB+moQ7ID17i+dE7N2xng6lPU8vrrNnnwKde0YpGHdW6hmuH4mS+rrltvs0fjyGRSvCnD2Qm9IAcA==} + '@oxfmt/linux-arm64-musl@0.24.0': + resolution: {integrity: sha512-JkQO3WnQjQTJONx8nxdgVBfl6BBFfpp9bKhChYhWeakwJdr7QPOAWJ/v3FGZfr0TbqINwnNR74aVZayDDRyXEA==} cpu: [arm64] os: [linux] - '@oxfmt/linux-x64-gnu@0.22.0': - resolution: {integrity: sha512-ZA1lS6MLvtGfD9AaDylCSTTiOWVQs1eIl9uqsGYs+Zr8p0mI7QRIRA6juWk9FXn1hHfmYBdBgWu2GdIW0YFCFA==} + '@oxfmt/linux-x64-gnu@0.24.0': + resolution: {integrity: sha512-N/SXlFO+2kak5gMt0oxApi0WXQDhwA0PShR0UbkY0PwtHjfSiDqJSOumyNqgQVoroKr1GNnoRmUqjZIz6DKIcw==} cpu: [x64] os: [linux] - '@oxfmt/linux-x64-musl@0.22.0': - resolution: {integrity: sha512-J5zFB8T5yk6Jx63rdKuXfcPqR1cAp12nO5/NJfGITH00AML2Yj9JM4dRnmssJomYHKa8dNSr40l6OdxRZN88CQ==} + '@oxfmt/linux-x64-musl@0.24.0': + resolution: {integrity: sha512-WM0pek5YDCQf50XQ7GLCE9sMBCMPW/NPAEPH/Hx6Qyir37lEsP4rUmSECo/QFNTU6KBc9NnsviAyJruWPpCMXw==} cpu: [x64] os: [linux] - '@oxfmt/win32-arm64@0.22.0': - resolution: {integrity: sha512-XYxyIiOf3HqlfETLFKqCHYL88mhw+Ka25vDVgmlcghbJv9BPoVzquZW7P4i0T3D5GWp4LHhZHmMo8BuK8PP5BA==} + '@oxfmt/win32-arm64@0.24.0': + resolution: {integrity: sha512-vFCseli1KWtwdHrVlT/jWfZ8jP8oYpnPPEjI23mPLW8K/6GEJmmvy0PZP5NpWUFNTzX0lqie58XnrATJYAe9Xw==} cpu: [arm64] os: [win32] - '@oxfmt/win32-x64@0.22.0': - resolution: {integrity: sha512-/shfU+wwlXcKP2NkZt+kYCSVom2EEu8MwbENlYCak6LtPPrN5xAQhHuOSFByjDzTBApdQugch0j0ZB/4Wyaljg==} + '@oxfmt/win32-x64@0.24.0': + resolution: {integrity: sha512-0tmlNzcyewAnauNeBCq0xmAkmiKzl+H09p0IdHy+QKrTQdtixtf+AOjDAADbRfihkS+heF15Pjc4IyJMdAAJjw==} cpu: [x64] os: [win32] @@ -1662,6 +1706,32 @@ packages: '@poppinss/exception@1.2.3': resolution: {integrity: sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==} + '@posthog/cli@0.5.22': + resolution: {integrity: sha512-Hgt/s4rjvO7Rvdzja4ccFu60NdQzZROCFiLYtTBqabG+ovLGZpf3JL0MgsIJP3GCZdwoDDaWDUHkhOXmAFTtcA==} + engines: {node: '>=14', npm: '>=6'} + hasBin: true + + '@posthog/core@1.9.0': + resolution: {integrity: sha512-j7KSWxJTUtNyKynLt/p0hfip/3I46dWU2dk+pt7dKRoz2l5CYueHuHK4EO7Wlgno5yo1HO4sc4s30MXMTICHJw==} + + '@posthog/core@1.9.1': + resolution: {integrity: sha512-kRb1ch2dhQjsAapZmu6V66551IF2LnCbc1rnrQqnR7ArooVyJN9KOPXre16AJ3ObJz2eTfuP7x25BMyS2Y5Exw==} + + '@posthog/react@1.5.2': + resolution: {integrity: sha512-KHdXbV1yba7Y2l8BVmwXlySWxqKVLNQ5ZiVvWOf7r3Eo7GIFxCM4CaNK/z83kKWn8KTskmKy7AGF6Hl6INWK3g==} + peerDependencies: + '@types/react': '>=16.8.0' + posthog-js: '>=1.257.2' + react: '>=16.8.0' + peerDependenciesMeta: + '@types/react': + optional: true + + '@posthog/rollup-plugin@1.1.7': + resolution: {integrity: sha512-jpi6x4bueP78Psuf+Xx8fn/jglpHf0wUfs+iiCzyUgo+JghFUNoLtRMcu8vfeuo8i9RPibl0BJNlafupQsXkzQ==} + peerDependencies: + rollup: '>= 4.0.0' + '@radix-ui/number@1.1.1': resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==} @@ -2476,6 +2546,9 @@ packages: resolution: {integrity: sha512-6rsHTjodIn/t90lv5snQjRPVtOosM7Vp0AKdrObymq45ojlgVwnpAqdc+0OBBrpEiy31zZ6/TKeIVqV1HwvnuQ==} engines: {node: '>=18'} + '@sinclair/typebox@0.31.28': + resolution: {integrity: sha512-/s55Jujywdw/Jpan+vsy6JZs1z2ZTGxTmbZTPiuSL2wz9mfzA2gN1zzaqmvfi4pq+uOt7Du85fkiwv5ymW84aQ==} + '@sindresorhus/is@7.2.0': resolution: {integrity: sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==} engines: {node: '>=18'} @@ -2689,6 +2762,10 @@ packages: '@speed-highlight/core@1.2.14': resolution: {integrity: sha512-G4ewlBNhUtlLvrJTb88d2mdy2KRijzs4UhnlrOSRT4bmjh/IqNElZa3zkrZ+TC47TwtlDWzVLFADljF1Ijp5hA==} + '@sqlite.org/sqlite-wasm@3.48.0-build4': + resolution: {integrity: sha512-hI6twvUkzOmyGZhQMza1gpfqErZxXRw6JEsiVjUbo7tFanVD+8Oil0Ih3l2nGzHdxPI41zFmfUQG7GHqhciKZQ==} + hasBin: true + '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} @@ -3294,6 +3371,9 @@ packages: array-ify@1.0.0: resolution: {integrity: sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==} + array-timsort@1.0.3: + resolution: {integrity: sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==} + assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} @@ -3309,9 +3389,18 @@ packages: async-retry@1.3.3: resolution: {integrity: sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==} + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + aws4fetch@1.0.20: resolution: {integrity: sha512-/djoAN709iY65ETD6LKCtyyEI04XIBP5xVvfmNxsEP0uJB5tyaGBztSryRr4HqMStr9R06PisQE7m9zDTXKu6g==} + axios-proxy-builder@0.1.2: + resolution: {integrity: sha512-6uBVsBZzkB3tCC8iyx59mCjQckhB8+GQrI9Cop8eC7ybIsvs/KtnNgEBfRMSEa7GqK2VBGUzgjNYMdPIfotyPA==} + + axios@1.13.2: + resolution: {integrity: sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==} + babel-dead-code-elimination@1.0.11: resolution: {integrity: sha512-mwq3W3e/pKSI6TG8lXMiDWvEi1VXYlSBlJlB3l+I0bAb5u1RNUl88udos85eOPNK3m5EXK9uO7d2g08pesTySQ==} @@ -3375,6 +3464,10 @@ packages: magicast: optional: true + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} @@ -3446,6 +3539,10 @@ packages: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} + clone@1.0.4: + resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} + engines: {node: '>=0.8'} + clsx@2.1.1: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} @@ -3473,6 +3570,14 @@ packages: colorette@2.0.20: resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + commander@11.1.0: + resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} + engines: {node: '>=16'} + commander@12.1.0: resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} engines: {node: '>=18'} @@ -3481,6 +3586,10 @@ packages: resolution: {integrity: sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==} engines: {node: '>=20'} + comment-json@4.5.1: + resolution: {integrity: sha512-taEtr3ozUmOB7it68Jll7s0Pwm+aoiHyXKrEC8SEodL4rNpdfDLqa7PfBlrgFoCNNdR8ImL+muti5IGvktJAAg==} + engines: {node: '>= 6'} + compare-func@2.0.0: resolution: {integrity: sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==} @@ -3491,10 +3600,18 @@ packages: confbox@0.2.2: resolution: {integrity: sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==} + consola@3.4.0: + resolution: {integrity: sha512-EiPU8G6dQG0GFHNR8ljnZFki/8a+cQwEQ+7wpxdChl02Q8HXlwEZWD5lqAF8vC2sEC3Tehr8hy7vErz88LHyUA==} + engines: {node: ^14.18.0 || >=16.10.0} + consola@3.4.2: resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} engines: {node: ^14.18.0 || >=16.10.0} + console.table@0.10.0: + resolution: {integrity: sha512-dPyZofqggxuvSf7WXvNjuRfnsOk1YazkVP8FdxH4tcH2c37wc79/Yl6Bhr7Lsu00KMgy2ql/qCMuNu8xctZM8g==} + engines: {node: '> 0.10'} + conventional-changelog-angular@7.0.0: resolution: {integrity: sha512-ROjNchA9LgfNMTTFSIWPzebCwOGFdgkEq45EnvvrmSLvCtAw0HSmrCs7/ty+wAeYUZyNay0YMUNYFTRL72PkBQ==} engines: {node: '>=16'} @@ -3550,6 +3667,9 @@ packages: resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} engines: {node: '>=18'} + core-js@3.47.0: + resolution: {integrity: sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg==} + core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} @@ -3665,6 +3785,14 @@ packages: decimal.js-light@2.5.1: resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} + dedent@1.5.1: + resolution: {integrity: sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg==} + peerDependencies: + babel-plugin-macros: ^3.1.0 + peerDependenciesMeta: + babel-plugin-macros: + optional: true + deep-eql@5.0.2: resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} engines: {node: '>=6'} @@ -3677,6 +3805,9 @@ packages: resolution: {integrity: sha512-XDuvSq38Hr1MdN47EDvYtx3U0MTqpCEn+F6ft8z2vYDzMrvQhVp0ui9oQdqW3MvK3vqUETglt1tVGgjLuJ5izg==} engines: {node: '>=18'} + defaults@1.0.4: + resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} + define-lazy-prop@3.0.0: resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} engines: {node: '>=12'} @@ -3688,6 +3819,10 @@ packages: resolution: {integrity: sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==} engines: {node: '>= 14'} + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} @@ -3829,9 +3964,16 @@ packages: sqlite3: optional: true + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + easy-table@1.1.0: + resolution: {integrity: sha512-oq33hWOSSnl2Hoh00tZWaIPi1ievrD9aFG82/IgjlycAnW9hHx5PkJiXpxPsgEE+H7BsbVQXFVFST8TEXS6/pA==} + electron-to-chromium@1.5.267: resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==} @@ -3894,9 +4036,30 @@ packages: error-stack-parser-es@1.0.5: resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==} + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + es-toolkit@1.43.0: resolution: {integrity: sha512-SKCT8AsWvYzBBuUqMk4NPwFlSdqLpJwmy6AP322ERn8W2YLIB6JBXnwMI2Qsh2gfphT3q7EKAxKb23cvFHFwKA==} + esbuild-wasm@0.19.12: + resolution: {integrity: sha512-Zmc4hk6FibJZBcTx5/8K/4jT3/oG1vkGTEeKJUQFCUQKimD6Q7+adp/bdVQyYJFolMKaXkQnVZdV4O5ZaTYmyQ==} + engines: {node: '>=12'} + hasBin: true + esbuild@0.25.12: resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} engines: {node: '>=18'} @@ -3997,6 +4160,9 @@ packages: picomatch: optional: true + fflate@0.4.8: + resolution: {integrity: sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==} + figures@6.1.0: resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} engines: {node: '>=18'} @@ -4013,10 +4179,23 @@ packages: resolution: {integrity: sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g==} engines: {node: '>=18'} + follow-redirects@1.15.11: + resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + foreground-child@3.3.1: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + engines: {node: '>= 6'} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -4037,6 +4216,10 @@ packages: resolution: {integrity: sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==} engines: {node: '>=18'} + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + get-nonce@1.0.1: resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} engines: {node: '>=6'} @@ -4045,6 +4228,10 @@ packages: resolution: {integrity: sha512-QB9NKEeDg3xxVwCCwJQ9+xycaz6pBB6iQ76wiWMl1927n0Kir6alPiP+yuiICLLU4jpMe08dXfpebuQppFA2zw==} engines: {node: '>=16'} + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + get-stream@8.0.1: resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} engines: {node: '>=16'} @@ -4091,6 +4278,10 @@ packages: engines: {node: 20 || >=22} hasBin: true + glob@13.0.0: + resolution: {integrity: sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==} + engines: {node: 20 || >=22} + global-directory@4.0.1: resolution: {integrity: sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==} engines: {node: '>=18'} @@ -4103,6 +4294,10 @@ packages: peerDependencies: csstype: ^3.0.10 + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} @@ -4124,6 +4319,14 @@ packages: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + hasown@2.0.2: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} @@ -4143,6 +4346,10 @@ packages: resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} engines: {node: '>= 14'} + human-id@4.1.3: + resolution: {integrity: sha512-tsYlhAYpjCKa//8rXZ9DqKEawhPoSytweBC2eNvcaDK+57RZLHGqNs3PZTQO6yekLFSuvA6AlnAfrw1uBvtb+Q==} + hasBin: true + human-signals@5.0.0: resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} engines: {node: '>=16.17.0'} @@ -4318,6 +4525,9 @@ packages: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true + js-sha256@0.11.1: + resolution: {integrity: sha512-o6WSo/LUvY2uC4j7mO50a2ms7E/EAdbP0swigLV+nzHKTTaYnaLIWJ02VdXrsJX0vGedDESQnLsOekr94ryfjg==} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -4352,6 +4562,10 @@ packages: resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} engines: {node: '>=6'} + kysely@0.27.6: + resolution: {integrity: sha512-FIyV/64EkKhJmjgC0g2hygpBv5RNWVPyNCqSAD7eTCv6eFWNIi4PN1UvdSJGicN/o35bnevgis4Y0UDC0qi8jQ==} + engines: {node: '>=14.0.0'} + launch-editor@2.12.0: resolution: {integrity: sha512-giOHXoOtifjdHqUamwKq6c49GzBdLjvxrd2D+Q4V6uOHopJv7p9VJxikDsQ/CBXZbEITgUqSVHXLTG3VhPP1Dg==} @@ -4537,6 +4751,10 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + meow@12.1.1: resolution: {integrity: sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==} engines: {node: '>=16.10'} @@ -4552,10 +4770,18 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + mime-db@1.54.0: resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} engines: {node: '>= 0.6'} + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + mime-types@3.0.2: resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} engines: {node: '>=18'} @@ -4690,8 +4916,8 @@ packages: resolution: {integrity: sha512-zBd1G8HkewNd2A8oQ8c6BN/f/c9EId7rSUueOLGu28govmUctXmM+3765GwsByv9nYUdrLqHphXlYIc86saYsg==} engines: {node: '>=18'} - oxfmt@0.22.0: - resolution: {integrity: sha512-Z7JM5yv4KaDz5kT21MxRAvtYo5Eu9Ti/XY1JYShSOlaH859XSn4UaS3wlZKyz4Mpbo8ISkxhU75UY5yp+OQUyA==} + oxfmt@0.24.0: + resolution: {integrity: sha512-UjeM3Peez8Tl7IJ9s5UwAoZSiDRMww7BEc21gDYxLq3S3/KqJnM3mjNxsoSHgmBvSeX6RBhoVc2MfC/+96RdSw==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true @@ -4818,6 +5044,16 @@ packages: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} + posthog-js@1.314.0: + resolution: {integrity: sha512-qW1T73UAFpA0g2Ln0blsOUJxRhv0Tn4DrPdhGyTpw+MupW+qvVjzEg/i9jWQ4Al+8AkrNcmZFafJcSWXxWsWqg==} + + posthog-node@5.19.0: + resolution: {integrity: sha512-5Nx+/b1JbAy6TaCENuO+mLFfZIXyYavae+D6FABd52gzkGryRkRTsKi+WLlILI5shSCAMWs5gxzGAZ3nOC0gFA==} + engines: {node: '>=20'} + + preact@10.28.1: + resolution: {integrity: sha512-u1/ixq/lVQI0CakKNvLDEcW5zfCjUQfZdK9qqWuIJtsezuyG6pk9TWj75GMuI/EzRSZB/VAE43sNWWZfiy8psw==} + prettier@3.7.4: resolution: {integrity: sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==} engines: {node: '>=14'} @@ -5018,6 +5254,11 @@ packages: rfdc@1.4.1: resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + rimraf@6.1.2: + resolution: {integrity: sha512-cFCkPslJv7BAXJsYlK1dZsbP8/ZNLkCAQ0bi1hf5EKX2QHegmDFEFA6QhuYJlk7UDdc+02JjO80YSOrWPpw06g==} + engines: {node: 20 || >=22} + hasBin: true + rollup@4.55.1: resolution: {integrity: sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -5160,6 +5401,11 @@ packages: resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} engines: {node: '>= 10.x'} + sqlite-wasm-kysely@0.3.0: + resolution: {integrity: sha512-TzjBNv7KwRw6E3pdKdlRyZiTmUIE0UttT/Sl56MVwVARl/u5gp978KepazCJZewFUnlWHz9i3NQd4kOtP/Afdg==} + peerDependencies: + kysely: '*' + srvx@0.10.0: resolution: {integrity: sha512-NqIsR+wQCfkvvwczBh8J8uM4wTZx41K2lLSEp/3oMp917ODVVMtW5Me4epCmQ3gH8D+0b+/t4xxkUKutyhimTA==} engines: {node: '>=20.16.0'} @@ -5324,6 +5570,10 @@ packages: engines: {node: '>=18.0.0'} hasBin: true + tunnel@0.0.6: + resolution: {integrity: sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==} + engines: {node: '>=0.6.11 <=0.7.0 || >=0.7.3'} + tw-animate-css@1.4.0: resolution: {integrity: sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==} @@ -5396,6 +5646,9 @@ packages: resolution: {integrity: sha512-n2huDr9h9yzd6exQVnH/jU5mr+Pfx08LRXXZhkLLetAMESRj+anQsTAh940iMrIetKAmry9coFuZQ2jY8/p3WA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + urlpattern-polyfill@10.1.0: + resolution: {integrity: sha512-IGjKp/o0NL3Bso1PymYURCJxMPNAf/ILOpendP9f5B6e1rTJgdgiOvgfoT8VxCAdY+Wisb9uhGaJJf3yZ2V9nw==} + use-callback-ref@1.3.3: resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} engines: {node: '>=10'} @@ -5424,6 +5677,14 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + uuid@10.0.0: + resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==} + hasBin: true + + uuid@13.0.0: + resolution: {integrity: sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==} + hasBin: true + validate-npm-package-license@3.0.4: resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} @@ -5496,6 +5757,12 @@ packages: resolution: {integrity: sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==} engines: {node: 20 || >=22} + wcwidth@1.0.1: + resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} + + web-vitals@4.2.4: + resolution: {integrity: sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==} + webpack-virtual-modules@0.6.2: resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} @@ -5531,6 +5798,7 @@ packages: wrangler@4.56.0: resolution: {integrity: sha512-Nqi8duQeRbA+31QrD6QlWHW3IZVnuuRxMy7DEg46deUzywivmaRV/euBN5KKXDPtA24VyhYsK7I0tkb7P5DM2w==} engines: {node: '>=20.0.0'} + deprecated: Version 4.55.0 and 4.56.0 can incorrectly automatically delegate 'wrangler deploy' to 'opennextjs-cloudflare'. Use an older or newer version. hasBin: true peerDependencies: '@cloudflare/workers-types': ^4.20251217.0 @@ -6676,6 +6944,39 @@ snapshots: '@img/sharp-win32-x64@0.33.5': optional: true + '@inlang/cli@3.1.2': + dependencies: + '@inlang/sdk': 2.6.0 + esbuild-wasm: 0.19.12 + transitivePeerDependencies: + - babel-plugin-macros + + '@inlang/paraglide-js@2.8.0': + dependencies: + '@inlang/recommend-sherlock': 0.2.1 + '@inlang/sdk': 2.6.0 + commander: 11.1.0 + consola: 3.4.0 + json5: 2.2.3 + unplugin: 2.3.11 + urlpattern-polyfill: 10.1.0 + transitivePeerDependencies: + - babel-plugin-macros + + '@inlang/recommend-sherlock@0.2.1': + dependencies: + comment-json: 4.5.1 + + '@inlang/sdk@2.6.0': + dependencies: + '@lix-js/sdk': 0.4.7 + '@sinclair/typebox': 0.31.28 + kysely: 0.27.6 + sqlite-wasm-kysely: 0.3.0(kysely@0.27.6) + uuid: 13.0.0 + transitivePeerDependencies: + - babel-plugin-macros + '@inquirer/ansi@1.0.2': {} '@inquirer/checkbox@4.3.2(@types/node@25.0.3)': @@ -6848,6 +7149,20 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@lix-js/sdk@0.4.7': + dependencies: + '@lix-js/server-protocol-schema': 0.1.1 + dedent: 1.5.1 + human-id: 4.1.3 + js-sha256: 0.11.1 + kysely: 0.27.6 + sqlite-wasm-kysely: 0.3.0(kysely@0.27.6) + uuid: 10.0.0 + transitivePeerDependencies: + - babel-plugin-macros + + '@lix-js/server-protocol-schema@0.1.1': {} + '@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3)': dependencies: '@types/mdx': 2.0.13 @@ -7005,28 +7320,28 @@ snapshots: '@oozcitak/util@10.0.0': {} - '@oxfmt/darwin-arm64@0.22.0': + '@oxfmt/darwin-arm64@0.24.0': optional: true - '@oxfmt/darwin-x64@0.22.0': + '@oxfmt/darwin-x64@0.24.0': optional: true - '@oxfmt/linux-arm64-gnu@0.22.0': + '@oxfmt/linux-arm64-gnu@0.24.0': optional: true - '@oxfmt/linux-arm64-musl@0.22.0': + '@oxfmt/linux-arm64-musl@0.24.0': optional: true - '@oxfmt/linux-x64-gnu@0.22.0': + '@oxfmt/linux-x64-gnu@0.24.0': optional: true - '@oxfmt/linux-x64-musl@0.22.0': + '@oxfmt/linux-x64-musl@0.24.0': optional: true - '@oxfmt/win32-arm64@0.22.0': + '@oxfmt/win32-arm64@0.24.0': optional: true - '@oxfmt/win32-x64@0.22.0': + '@oxfmt/win32-x64@0.24.0': optional: true '@oxlint-tsgolint/darwin-arm64@0.10.0': @@ -7088,6 +7403,39 @@ snapshots: '@poppinss/exception@1.2.3': {} + '@posthog/cli@0.5.22': + dependencies: + axios: 1.13.2 + axios-proxy-builder: 0.1.2 + console.table: 0.10.0 + detect-libc: 2.1.2 + rimraf: 6.1.2 + transitivePeerDependencies: + - debug + + '@posthog/core@1.9.0': + dependencies: + cross-spawn: 7.0.6 + + '@posthog/core@1.9.1': + dependencies: + cross-spawn: 7.0.6 + + '@posthog/react@1.5.2(@types/react@19.2.7)(posthog-js@1.314.0)(react@19.2.3)': + dependencies: + posthog-js: 1.314.0 + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.7 + + '@posthog/rollup-plugin@1.1.7(rollup@4.55.1)': + dependencies: + '@posthog/cli': 0.5.22 + '@posthog/core': 1.9.1 + rollup: 4.55.1 + transitivePeerDependencies: + - debug + '@radix-ui/number@1.1.1': {} '@radix-ui/primitive@1.1.3': {} @@ -7889,6 +8237,8 @@ snapshots: dependencies: '@types/node': 22.19.3 + '@sinclair/typebox@0.31.28': {} + '@sindresorhus/is@7.2.0': {} '@sindresorhus/merge-streams@4.0.0': {} @@ -8203,6 +8553,8 @@ snapshots: '@speed-highlight/core@1.2.14': {} + '@sqlite.org/sqlite-wasm@3.48.0-build4': {} + '@standard-schema/spec@1.1.0': {} '@standard-schema/utils@0.3.0': {} @@ -8817,7 +9169,7 @@ snapshots: json-schema-traverse: 1.0.0 require-from-string: 2.0.2 - alchemy@0.83.0(@cloudflare/vite-plugin@1.19.0(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2))(workerd@1.20251217.0)(wrangler@4.56.0(@cloudflare/workers-types@4.20260103.0)))(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2))(workerd@1.20251217.0)(wrangler@4.56.0(@cloudflare/workers-types@4.20260103.0)): + alchemy@0.83.0(@cloudflare/vite-plugin@1.19.0(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2))(workerd@1.20251217.0)(wrangler@4.56.0(@cloudflare/workers-types@4.20260103.0)))(kysely@0.27.6)(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2))(workerd@1.20251217.0)(wrangler@4.56.0(@cloudflare/workers-types@4.20260103.0)): dependencies: '@aws-sdk/credential-providers': 3.962.0 '@cloudflare/unenv-preset': 2.7.7(unenv@2.0.0-rc.21)(workerd@1.20251217.0) @@ -8827,7 +9179,7 @@ snapshots: '@smithy/node-config-provider': 4.3.7 '@smithy/types': 4.11.0 aws4fetch: 1.0.20 - drizzle-orm: 0.45.1(@cloudflare/workers-types@4.20260103.0) + drizzle-orm: 0.45.1(@cloudflare/workers-types@4.20260103.0)(kysely@0.27.6) env-paths: 3.0.0 esbuild: 0.25.12 execa: 9.6.1 @@ -8922,6 +9274,8 @@ snapshots: array-ify@1.0.0: {} + array-timsort@1.0.3: {} + assertion-error@2.0.1: {} ast-types@0.13.4: @@ -8936,8 +9290,22 @@ snapshots: dependencies: retry: 0.13.1 + asynckit@0.4.0: {} + aws4fetch@1.0.20: {} + axios-proxy-builder@0.1.2: + dependencies: + tunnel: 0.0.6 + + axios@1.13.2: + dependencies: + follow-redirects: 1.15.11 + form-data: 4.0.5 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + babel-dead-code-elimination@1.0.11: dependencies: '@babel/core': 7.28.5 @@ -9006,6 +9374,11 @@ snapshots: pkg-types: 2.3.0 rc9: 2.1.2 + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + callsites@3.1.0: {} caniuse-lite@1.0.30001762: {} @@ -9097,6 +9470,9 @@ snapshots: strip-ansi: 6.0.1 wrap-ansi: 7.0.0 + clone@1.0.4: + optional: true + clsx@2.1.1: {} cmdk@1.1.1(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3): @@ -9129,10 +9505,22 @@ snapshots: colorette@2.0.20: {} + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + commander@11.1.0: {} + commander@12.1.0: {} commander@14.0.2: {} + comment-json@4.5.1: + dependencies: + array-timsort: 1.0.3 + core-util-is: 1.0.3 + esprima: 4.0.1 + compare-func@2.0.0: dependencies: array-ify: 1.0.0 @@ -9147,8 +9535,14 @@ snapshots: confbox@0.2.2: {} + consola@3.4.0: {} + consola@3.4.2: {} + console.table@0.10.0: + dependencies: + easy-table: 1.1.0 + conventional-changelog-angular@7.0.0: dependencies: compare-func: 2.0.0 @@ -9210,6 +9604,8 @@ snapshots: cookie@1.1.1: {} + core-js@3.47.0: {} + core-util-is@1.0.3: {} cosmiconfig-typescript-loader@6.2.0(@types/node@25.0.3)(cosmiconfig@9.0.0(typescript@5.9.3))(typescript@5.9.3): @@ -9305,6 +9701,8 @@ snapshots: decimal.js-light@2.5.1: {} + dedent@1.5.1: {} + deep-eql@5.0.2: {} default-browser-id@5.0.1: {} @@ -9314,6 +9712,11 @@ snapshots: bundle-name: 4.1.0 default-browser-id: 5.0.1 + defaults@1.0.4: + dependencies: + clone: 1.0.4 + optional: true + define-lazy-prop@3.0.0: {} defu@6.1.4: {} @@ -9324,6 +9727,8 @@ snapshots: escodegen: 2.1.0 esprima: 4.0.1 + delayed-stream@1.0.0: {} + dequal@2.0.3: {} destr@2.0.5: {} @@ -9366,12 +9771,23 @@ snapshots: dotenv@17.2.3: {} - drizzle-orm@0.45.1(@cloudflare/workers-types@4.20260103.0): + drizzle-orm@0.45.1(@cloudflare/workers-types@4.20260103.0)(kysely@0.27.6): optionalDependencies: '@cloudflare/workers-types': 4.20260103.0 + kysely: 0.27.6 + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 eastasianwidth@0.2.0: {} + easy-table@1.1.0: + optionalDependencies: + wcwidth: 1.0.1 + electron-to-chromium@1.5.267: {} embla-carousel-react@8.6.0(react@19.2.3): @@ -9420,8 +9836,25 @@ snapshots: error-stack-parser-es@1.0.5: {} + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + es-toolkit@1.43.0: {} + esbuild-wasm@0.19.12: {} + esbuild@0.25.12: optionalDependencies: '@esbuild/aix-ppc64': 0.25.12 @@ -9592,6 +10025,8 @@ snapshots: optionalDependencies: picomatch: 4.0.3 + fflate@0.4.8: {} + figures@6.1.0: dependencies: is-unicode-supported: 2.1.0 @@ -9612,11 +10047,21 @@ snapshots: path-exists: 5.0.0 unicorn-magic: 0.1.0 + follow-redirects@1.15.11: {} + foreground-child@3.3.1: dependencies: cross-spawn: 7.0.6 signal-exit: 4.1.0 + form-data@4.0.5: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + fsevents@2.3.3: optional: true @@ -9628,10 +10073,28 @@ snapshots: get-east-asian-width@1.4.0: {} + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + get-nonce@1.0.1: {} get-port@7.1.0: {} + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + get-stream@8.0.1: {} get-stream@9.0.1: @@ -9699,6 +10162,12 @@ snapshots: package-json-from-dist: 1.0.1 path-scurry: 2.0.1 + glob@13.0.0: + dependencies: + minimatch: 10.1.1 + minipass: 7.1.2 + path-scurry: 2.0.1 + global-directory@4.0.1: dependencies: ini: 4.1.1 @@ -9709,6 +10178,8 @@ snapshots: dependencies: csstype: 3.2.3 + gopd@1.2.0: {} + graceful-fs@4.2.11: {} h3@2.0.1-rc.7(crossws@0.4.1(srvx@0.10.0)): @@ -9729,6 +10200,12 @@ snapshots: has-flag@4.0.0: {} + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + hasown@2.0.2: dependencies: function-bind: 1.1.2 @@ -9758,6 +10235,8 @@ snapshots: transitivePeerDependencies: - supports-color + human-id@4.1.3: {} + human-signals@5.0.0: {} human-signals@8.0.1: {} @@ -9894,6 +10373,8 @@ snapshots: jiti@2.6.1: {} + js-sha256@0.11.1: {} + js-tokens@4.0.0: {} js-yaml@4.1.1: @@ -9919,6 +10400,8 @@ snapshots: kleur@4.1.5: {} + kysely@0.27.6: {} + launch-editor@2.12.0: dependencies: picocolors: 1.1.1 @@ -10075,6 +10558,8 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + math-intrinsics@1.1.0: {} + meow@12.1.1: {} meow@13.2.0: {} @@ -10086,8 +10571,14 @@ snapshots: braces: 3.0.3 picomatch: 2.3.1 + mime-db@1.52.0: {} + mime-db@1.54.0: {} + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + mime-types@3.0.2: dependencies: mime-db: 1.54.0 @@ -10224,18 +10715,18 @@ snapshots: macos-release: 3.4.0 windows-release: 6.1.0 - oxfmt@0.22.0: + oxfmt@0.24.0: dependencies: tinypool: 2.0.0 optionalDependencies: - '@oxfmt/darwin-arm64': 0.22.0 - '@oxfmt/darwin-x64': 0.22.0 - '@oxfmt/linux-arm64-gnu': 0.22.0 - '@oxfmt/linux-arm64-musl': 0.22.0 - '@oxfmt/linux-x64-gnu': 0.22.0 - '@oxfmt/linux-x64-musl': 0.22.0 - '@oxfmt/win32-arm64': 0.22.0 - '@oxfmt/win32-x64': 0.22.0 + '@oxfmt/darwin-arm64': 0.24.0 + '@oxfmt/darwin-x64': 0.24.0 + '@oxfmt/linux-arm64-gnu': 0.24.0 + '@oxfmt/linux-arm64-musl': 0.24.0 + '@oxfmt/linux-x64-gnu': 0.24.0 + '@oxfmt/linux-x64-musl': 0.24.0 + '@oxfmt/win32-arm64': 0.24.0 + '@oxfmt/win32-x64': 0.24.0 oxlint-tsgolint@0.10.0: optionalDependencies: @@ -10370,6 +10861,20 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + posthog-js@1.314.0: + dependencies: + '@posthog/core': 1.9.0 + core-js: 3.47.0 + fflate: 0.4.8 + preact: 10.28.1 + web-vitals: 4.2.4 + + posthog-node@5.19.0: + dependencies: + '@posthog/core': 1.9.0 + + preact@10.28.1: {} + prettier@3.7.4: {} pretty-format@27.5.1: @@ -10612,6 +11117,11 @@ snapshots: rfdc@1.4.1: {} + rimraf@6.1.2: + dependencies: + glob: 13.0.0 + package-json-from-dist: 1.0.1 + rollup@4.55.1: dependencies: '@types/estree': 1.0.8 @@ -10774,6 +11284,11 @@ snapshots: split2@4.2.0: {} + sqlite-wasm-kysely@0.3.0(kysely@0.27.6): + dependencies: + '@sqlite.org/sqlite-wasm': 3.48.0-build4 + kysely: 0.27.6 + srvx@0.10.0: {} stdin-discarder@0.2.2: {} @@ -10918,6 +11433,8 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + tunnel@0.0.6: {} + tw-animate-css@1.4.0: {} type-fest@2.19.0: {} @@ -10974,6 +11491,8 @@ snapshots: url-join@5.0.0: {} + urlpattern-polyfill@10.1.0: {} + use-callback-ref@1.3.3(@types/react@19.2.7)(react@19.2.3): dependencies: react: 19.2.3 @@ -10995,6 +11514,10 @@ snapshots: util-deprecate@1.0.2: {} + uuid@10.0.0: {} + + uuid@13.0.0: {} + validate-npm-package-license@3.0.4: dependencies: spdx-correct: 3.2.0 @@ -11059,6 +11582,13 @@ snapshots: walk-up-path@4.0.0: {} + wcwidth@1.0.1: + dependencies: + defaults: 1.0.4 + optional: true + + web-vitals@4.2.4: {} + webpack-virtual-modules@0.6.2: {} whatwg-encoding@3.1.1: diff --git a/project.inlang/settings.json b/project.inlang/settings.json new file mode 100644 index 0000000..5d336d0 --- /dev/null +++ b/project.inlang/settings.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://inlang.com/schema/project-settings", + "baseLocale": "en", + "locales": ["en", "id", "zh-CN"], + "modules": [ + "https://cdn.jsdelivr.net/npm/@inlang/plugin-message-format@4/dist/index.js", + "https://cdn.jsdelivr.net/npm/@inlang/plugin-m-function-matcher@2/dist/index.js" + ], + "plugin.inlang.messageFormat": { + "pathPattern": "./messages/{locale}.json" + } +} diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png new file mode 100644 index 0000000..82f3428 Binary files /dev/null and b/public/apple-touch-icon.png differ diff --git a/public/favicon-96x96.png b/public/favicon-96x96.png new file mode 100644 index 0000000..e70ee86 Binary files /dev/null and b/public/favicon-96x96.png differ diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..551c1a7 Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/favicon.svg b/public/favicon.svg new file mode 100644 index 0000000..03dc6a2 --- /dev/null +++ b/public/favicon.svg @@ -0,0 +1,17 @@ + \ No newline at end of file diff --git a/public/site.webmanifest b/public/site.webmanifest new file mode 100644 index 0000000..59beee9 --- /dev/null +++ b/public/site.webmanifest @@ -0,0 +1,23 @@ +{ + "name": "Devsantara Kit", + "short_name": "Kit", + "description": "The blueprint for your next big idea", + "start_url": "/", + "theme_color": "#000000", + "background_color": "#ffffff", + "display": "standalone", + "icons": [ + { + "src": "/web-app-manifest-192x192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "/web-app-manifest-512x512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/public/web-app-manifest-192x192.png b/public/web-app-manifest-192x192.png new file mode 100644 index 0000000..b5e66c6 Binary files /dev/null and b/public/web-app-manifest-192x192.png differ diff --git a/public/web-app-manifest-512x512.png b/public/web-app-manifest-512x512.png new file mode 100644 index 0000000..7f8e48f Binary files /dev/null and b/public/web-app-manifest-512x512.png differ diff --git a/src/client.tsx b/src/client.tsx index 1cc5fb0..c766152 100644 --- a/src/client.tsx +++ b/src/client.tsx @@ -2,6 +2,10 @@ import { StartClient } from '@tanstack/react-start/client'; import * as React from 'react'; import { hydrateRoot } from 'react-dom/client'; +import { initializePosthogClient } from '~/lib/posthog/client'; + +initializePosthogClient(); + hydrateRoot( document, diff --git a/src/devtools/router-devtools.tsx b/src/devtools/router-devtools.tsx index 2a5ae71..36092a8 100644 --- a/src/devtools/router-devtools.tsx +++ b/src/devtools/router-devtools.tsx @@ -1,5 +1,4 @@ import type { TanStackDevtoolsReactPlugin } from '@tanstack/react-devtools'; - import { TanStackRouterDevtoolsPanel } from '@tanstack/react-router-devtools'; export const tanstackRouterDevtools: TanStackDevtoolsReactPlugin = { diff --git a/src/lib/env/client.ts b/src/lib/env/client.ts index f9a76b5..f25f127 100644 --- a/src/lib/env/client.ts +++ b/src/lib/env/client.ts @@ -1,10 +1,17 @@ import { createEnv } from '@t3-oss/env-core'; -import * as _ from 'zod/v4'; +import * as z from 'zod/v4'; + +import { coerceBoolean } from './utils'; /** Env schema for client bundle */ export const clientEnv = createEnv({ clientPrefix: 'VITE_', - client: {}, + client: { + VITE_PUBLIC_POSTHOG_KEY: z.string().nonempty(), + VITE_PUBLIC_POSTHOG_HOST: z.url(), + VITE_PUBLIC_POSTHOG_DEBUG: coerceBoolean().default(false), + VITE_PUBLIC_POSTHOG_ENABLED: coerceBoolean().default(false), + }, runtimeEnv: import.meta.env, emptyStringAsUndefined: true, }); diff --git a/src/lib/env/utils.ts b/src/lib/env/utils.ts new file mode 100644 index 0000000..e186a60 --- /dev/null +++ b/src/lib/env/utils.ts @@ -0,0 +1,6 @@ +import * as z from 'zod/v4'; + +/** Coerce string to boolean. */ +export function coerceBoolean() { + return z.enum(['true', 'false']).transform((val) => val === 'true'); +} diff --git a/src/lib/posthog/client.ts b/src/lib/posthog/client.ts new file mode 100644 index 0000000..5b05a73 --- /dev/null +++ b/src/lib/posthog/client.ts @@ -0,0 +1,14 @@ +import { posthog } from 'posthog-js'; + +import { clientEnv } from '~/lib/env/client'; + +/** Initialize browser Posthog client */ +export function initializePosthogClient() { + if (!clientEnv.VITE_PUBLIC_POSTHOG_ENABLED) return; + + posthog.init(clientEnv.VITE_PUBLIC_POSTHOG_KEY, { + defaults: '2025-11-30', + api_host: clientEnv.VITE_PUBLIC_POSTHOG_HOST, + debug: clientEnv.VITE_PUBLIC_POSTHOG_DEBUG, + }); +} diff --git a/src/lib/posthog/plugin.ts b/src/lib/posthog/plugin.ts new file mode 100644 index 0000000..5b54f29 --- /dev/null +++ b/src/lib/posthog/plugin.ts @@ -0,0 +1,38 @@ +import posthogVitePlugin, { + type PostHogRollupPluginOptions, +} from '@posthog/rollup-plugin'; + +/** + * Create a PostHog Vite/Rollup plugin instance only when the required + * PostHog configuration is present. + * + * This wrapper is useful when you want sourcemap upload + injection in some + * environments (e.g. production CI) but want to avoid running the plugin in + * others (e.g. local dev, preview builds). + * + * If any required option is missing, this function returns `undefined` so it + * can be conditionally included in your Vite/Rollup plugins array without + * additional branching. + * + * When enabled, the underlying PostHog plugin will: + * - inject sourcemap references + * - upload sourcemaps + * - delete sourcemaps after upload (as configured) + */ +export function posthog(options: Partial) { + if (!options.personalApiKey || !options.envId || !options.host) { + return undefined; + } + + return posthogVitePlugin({ + host: options.host, + envId: options.envId, + personalApiKey: options.personalApiKey, + ...options, + sourcemaps: { + enabled: true, + deleteAfterUpload: true, + ...options.sourcemaps, + }, + }); +} diff --git a/src/lib/posthog/provider.tsx b/src/lib/posthog/provider.tsx new file mode 100644 index 0000000..32c9807 --- /dev/null +++ b/src/lib/posthog/provider.tsx @@ -0,0 +1,7 @@ +import { PostHogProvider as BasePosthogProvider } from '@posthog/react'; +import { posthog } from 'posthog-js'; +import * as React from 'react'; + +export function PostHogProvider({ children }: { children: React.ReactNode }) { + return {children}; +} diff --git a/src/lib/posthog/server.ts b/src/lib/posthog/server.ts new file mode 100644 index 0000000..11b3670 --- /dev/null +++ b/src/lib/posthog/server.ts @@ -0,0 +1,13 @@ +import { PostHog } from 'posthog-node'; + +import { clientEnv } from '~/lib/env/client'; + +/** Create server Posthog client instance */ +export function createPosthogClient() { + return new PostHog(clientEnv.VITE_PUBLIC_POSTHOG_KEY, { + host: clientEnv.VITE_PUBLIC_POSTHOG_HOST, + flushAt: 1, // Flush after every event + flushInterval: 0, // No batching delays + disabled: !clientEnv.VITE_PUBLIC_POSTHOG_ENABLED, + }); +} diff --git a/src/lib/seo/head.ts b/src/lib/seo/head.ts new file mode 100644 index 0000000..4902982 --- /dev/null +++ b/src/lib/seo/head.ts @@ -0,0 +1,814 @@ +import * as React from 'react'; + +import type { AlternateLocaleOptions } from '~/lib/seo/types/alternate'; +import type { + ColorSchemeOptions, + IconOptions, + RobotOptions, + ViewportOptions, +} from '~/lib/seo/types/metadata'; +import type { OpenGraphOptions } from '~/lib/seo/types/opengraph'; +import type { TwitterOptions } from '~/lib/seo/types/twitter'; + +type Awaitable = T | Promise; +type Meta = React.DetailedHTMLProps< + React.MetaHTMLAttributes, + HTMLMetaElement +>; +type Link = React.DetailedHTMLProps< + React.LinkHTMLAttributes, + HTMLLinkElement +>; +type Script = React.DetailedHTMLProps< + React.ScriptHTMLAttributes, + HTMLScriptElement +>; +type Style = React.DetailedHTMLProps< + React.StyleHTMLAttributes, + HTMLStyleElement +>; + +interface HeadResult { + meta?: Meta[]; + links?: Link[]; + scripts?: Script[]; + styles?: Style[]; +} + +/** + * HeadBuilder - A fluent builder class for constructing SEO-optimized HTML head metadata + * + * This class provides a comprehensive, chainable API for building HTML head elements including + * meta tags, link elements, scripts, and styles. It supports common SEO practices such as + * Open Graph, Twitter Cards, canonical URLs, and robot directives. + * + * The builder follows the Builder design pattern, allowing you to chain multiple method calls + * to construct a complete head configuration before calling `build()` to generate the result. + * + * @example + * const head = new HeadBuilder('https://devsantara.com') + * .addCharSet('utf-8') + * .addTitle('My Awesome Website') + * .addDescription('A comprehensive guide to building great websites') + * .addViewport({ + * width: 'device-width', + * initialScale: 1, + * }) + * .build(); + * + * // Result: + * { + * meta: [ + * { charSet: 'utf-8' }, + * { title: 'My Awesome Website' }, + * { name: 'description', content: 'A comprehensive guide to building great websites' }, + * { name: 'viewport', content: 'width=device-width, initial-scale=1' } + * ], + * links: [], + * scripts: [], + * styles: [] + * } + */ +export class HeadBuilder { + private metadataBase?: URL; + private meta: Meta[] = []; + private links: Link[] = []; + private scripts: Script[] = []; + private styles: Style[] = []; + + /** + * Creates a new HeadBuilder instance with optional metadataBase configuration + * + * The metadataBase serves as the base path and origin for absolute URLs in various + * metadata fields. When relative URLs (for Open Graph images, alternates, etc.) are used, + * they are composed with this base. If not provided, relative URLs will be used as-is. + * + * @param metadataBase - The base URL to use for resolving relative URLs in metadata + * + * @example + * const head = new HeadBuilder('https://devsantara.com') + * .addOpenGraph({ + * title: 'My Article', + * image: { url: '/og-image.jpg' } // Will resolve to https://devsantara.com/og-image.jpg + * }) + * .build(); + */ + constructor(metadataBase?: URL) { + this.metadataBase = metadataBase; + } + + /** + * Resolves a URL by composing relative URLs with the metadataBase + * + * If the URL is already absolute (contains a protocol), it's returned unchanged. + * If the URL is relative and metadataBase is provided, they're composed together. + * If the URL is relative and metadataBase is not provided, the URL is returned as-is. + */ + private resolveUrl(url: string | URL) { + if (url instanceof URL) return url.toString(); + if (!this.metadataBase) return url; + + try { + // Try to parse as absolute URL + // oxlint-disable-next-line no-new + new URL(url); + return url; + } catch { + try { + // It's relative, compose with base + return new URL(url, this.metadataBase).toString(); + } catch { + // If base is invalid, return original URL + return url; + } + } + } + + /** + * Adds a charset meta tag to specify the character encoding of the document + * + * The charset meta tag declares the character encoding used by the document, typically UTF-8. + * This should be one of the first meta tags in the head section to ensure proper character + * rendering across all browsers and devices. + * + * @example + * const head = new HeadBuilder() + * .addCharSet('utf-8') + * .build(); + * + * // Result: + * + */ + addCharSet(charSet: string) { + this.meta.push({ charSet }); + return this; + } + + /** + * Adds a viewport meta tag to control layout and scaling on mobile devices + * + * The viewport meta tag is crucial for responsive web design. It instructs the browser + * how to scale and render the page, particularly on mobile devices. Without this tag, + * mobile browsers will attempt to scale the full desktop page to fit the screen, often + * resulting in zoomed-out, unreadable content. + * + * @example + * const head = new HeadBuilder() + * .addViewport({ + * width: 'device-width', + * initialScale: 1, + * }) + * .build(); + * + * // Result: + * + */ + addViewport(options: ViewportOptions) { + const contentParts: string[] = []; + if (options.width) { + contentParts.push(`width=${options.width}`); + } + if (options.height) { + contentParts.push(`height=${options.height}`); + } + if (options.initialScale) { + contentParts.push(`initial-scale=${options.initialScale}`); + } + if (options.minimumScale) { + contentParts.push(`minimum-scale=${options.minimumScale}`); + } + if (options.maximumScale) { + contentParts.push(`maximum-scale=${options.maximumScale}`); + } + if (options.userScalable !== undefined) { + contentParts.push(`user-scalable=${options.userScalable ? 'yes' : 'no'}`); + } + if (options.viewportFit) { + contentParts.push(`viewport-fit=${options.viewportFit}`); + } + if (options.interactiveWidget) { + contentParts.push(`interactive-widget=${options.interactiveWidget}`); + } + + const content = contentParts.join(', '); + this.meta.push({ name: 'viewport', content }); + return this; + } + + /** + * Adds a canonical link tag to specify the preferred version of a web page + * + * The canonical URL tells search engines which version of a page to prioritize when + * multiple URLs have similar or identical content. This is essential for: + * - Preventing duplicate content issues with search engines + * - Consolidating ranking signals to the preferred URL + * - Managing URL variants (HTTP vs HTTPS, www vs non-www, trailing slashes) + * - Handling pagination and session parameters + * + * Relative URLs are resolved using the metadataBase if provided. + * + * @example + * const head = new HeadBuilder('https://devsantara.com') + * .addCanonical('/blogs/nusantara') + * .build(); + * + * // Result: + * + */ + addCanonical(url: string | URL) { + this.links.push({ rel: 'canonical', href: this.resolveUrl(url) }); + return this; + } + + /** + * Adds alternate locale links to specify the page's translations or variants + * + * Alternate locale links help search engines understand which pages are translations + * or variants of the same content in different languages or for different regions. + * This is crucial for international SEO and helps users find content in their locale. + * + * Relative URLs are resolved using the metadataBase if provided. + * + * @example + * const head = new HeadBuilder() + * .addAlternateLocales({ + * 'x-default': 'https://devsantara.com/en', + * 'en': 'https://devsantara.com/en', + * 'fr': 'https://devsantara.com/fr' + * }) + * .build(); + * + * // Result: + * + * + * + */ + addAlternateLocales( + alternates: AlternateLocaleOptions, + ) { + Object.entries(alternates).map(([locale, url]) => { + this.links.push({ + rel: 'alternate', + hrefLang: locale, + href: this.resolveUrl(url), + }); + }); + return this; + } + + /** + * Adds a manifest link for progressive web app (PWA) configuration + * + * The manifest link points to a JSON file that contains metadata about your web application, + * including the app name, icons, theme colors, and display preferences. This is essential + * for progressive web apps that can be installed on mobile devices and desktops. + * + * @example + * const head = new HeadBuilder() + * .addManifest('/manifest.json') + * .build(); + * + * // Result: + * + */ + addManifest(url: string | URL) { + this.links.push({ rel: 'manifest', href: String(url) }); + return this; + } + + /** + * Adds keywords meta tag for search engine optimization + * + * The keywords meta tag provides a list of keywords relevant to the page content. + * While modern search engines like Google place less emphasis on keywords, they can still + * be useful for other search engines and content discovery. Keep keywords relevant and + * avoid keyword stuffing. + * + * @example + * const head = new HeadBuilder() + * .addKeywords(['web development', 'SEO', 'best practices']) + * .build(); + * + * // Result: + * + */ + addKeywords(keywords: string[]) { + this.meta.push({ name: 'keywords', content: keywords.join(', ') }); + return this; + } + + /** + * Adds a color-scheme meta tag to indicate the color theme preferences + * + * The color-scheme meta tag allows web developers to declare which color schemes + * their page supports (light mode, dark mode, or both). This helps browsers and + * operating systems render the page appropriately and improves user experience + * by respecting user's system preferences. + * + * @example + * const head = new HeadBuilder() + * .addColorScheme('light dark') + * .build(); + * + * // Result: + * + */ + addColorScheme(scheme: ColorSchemeOptions) { + this.meta.push({ name: 'color-scheme', content: scheme }); + return this; + } + + /** + * Adds a title meta tag specifying the page title + * + * The title tag is critical for SEO and user experience. It appears in search engine + * results, browser tabs, and browser history. Keep titles concise, + * descriptive, and unique across your site. Include primary keywords naturally without + * keyword stuffing. + * + * @example + * const head = new HeadBuilder() + * .addTitle('Devsantara') + * .build(); + * + * // Result: + * Devsantara + */ + addTitle(title: string) { + this.meta.push({ title }); + return this; + } + + /** + * Adds a meta description tag summarizing the page content + * + * The meta description provides a concise summary of the page content + * that appears in search engine results. + * + * @example + * // Basic page description + * const head = new HeadBuilder() + * .addDescription('The blueprint for your next big idea') + * .build(); + * + * // HTML Result: + * + */ + addDescription(description: string) { + this.meta.push({ name: 'description', content: description }); + return this; + } + + /** + * Adds Open Graph (OG) meta tags for rich preview on social media + * + * Open Graph meta tags control how your content is previewed when shared on Facebook, + * LinkedIn, Twitter (X), and other social platforms. Proper OG tags increase social + * engagement by providing attractive, accurate previews with titles, descriptions, and images. + * + * Open Graph includes standard properties for all content types plus type-specific properties + * for articles, videos, music, and more. + * + * Relative URLs are resolved using the metadataBase if provided. + * + * @example + * const head = new HeadBuilder('https://devsantara.com') + * .addOpenGraph({ + * title: 'Devsantara', + * description: 'The blueprint for your next big idea', + * url: '/', + * image: { + * url: '/assets/og.jpg', + * } + * }) + * .build(); + * + * // Result: + * + * + * + * + */ + addOpenGraph(options: OpenGraphOptions) { + if (options.title) { + this.meta.push({ property: 'og:title', content: options.title }); + } + if (options.description) { + this.meta.push({ + property: 'og:description', + content: options.description, + }); + } + if (options.url) { + this.meta.push({ + property: 'og:url', + content: this.resolveUrl(options.url), + }); + } + if (options.locale) { + this.meta.push({ property: 'og:locale', content: options.locale }); + } + if (options.image) { + this.meta.push({ + property: 'og:image', + content: this.resolveUrl(options.image.url), + }); + if (options.image.alt) { + this.meta.push({ + property: 'og:image:alt', + content: options.image.alt, + }); + } + if (options.image.type) { + this.meta.push({ + property: 'og:image:type', + content: options.image.type, + }); + } + if (options.image.width) { + this.meta.push({ + property: 'og:image:width', + content: String(options.image.width), + }); + } + if (options.image.height) { + this.meta.push({ + property: 'og:image:height', + content: String(options.image.height), + }); + } + } + if (options.type) { + this.meta.push({ property: 'og:type', content: options.type.name }); + if ('properties' in options.type) { + for (const property of options.type.properties) { + this.meta.push({ + property: property.name, + content: property.content, + }); + } + } + } + return this; + } + + /** + * Adds Twitter Card meta tags for enhanced sharing on Twitter/X + * + * Twitter Cards enable rich media presentations of content when shared on Twitter. + * They control how your content appears with summaries, images, and interactive elements. + * Different card types optimize content differently (summary, summary_large_image, player, etc.). + * + * @example + * const head = new HeadBuilder() + * .addTwitter({ + * card: { name: 'summary' }, + * title: 'Devsantara', + * description: 'The blueprint for your next big idea', + * site: '@devsantara', + * creator: '@edwintantawi' + * }) + * .build(); + * + * // Result: + * + * + * + * + * + */ + addTwitter(options: TwitterOptions) { + if (options.title) { + this.meta.push({ name: 'twitter:title', content: options.title }); + } + if (options.description) { + this.meta.push({ + name: 'twitter:description', + content: options.description, + }); + } + if (options.site) { + this.meta.push({ name: 'twitter:site', content: options.site }); + } + if (options.siteId) { + this.meta.push({ name: 'twitter:site:id', content: options.siteId }); + } + if (options.creator) { + this.meta.push({ name: 'twitter:creator', content: options.creator }); + } + if (options.creatorId) { + this.meta.push({ + name: 'twitter:creator:id', + content: options.creatorId, + }); + } + if (options.image) { + this.meta.push({ + name: 'twitter:image', + content: String(options.image.url), + }); + if (options.image.alt) { + this.meta.push({ + name: 'twitter:image:alt', + content: options.image.alt, + }); + } + } + if (options.card) { + this.meta.push({ name: 'twitter:card', content: options.card.name }); + if ('properties' in options.card) { + for (const property of options.card.properties) { + this.meta.push({ + name: property.name, + content: String(property.content), + }); + } + } + } + return this; + } + + /** + * Adds favicon and other icon link elements + * + * Favicon and icon link elements help users identify your site visually in browser tabs, + * bookmarks, history, and on mobile home screens. Support multiple icon formats and types + * for optimal display across different devices and platforms (desktop, iOS, Android, etc.). + * + * Relative URLs are resolved using the metadataBase if provided. + * + * @example + * const head = new HeadBuilder('https://devsantara.com') + * .addIcons({ + * shortcut: [ + * { url: '/favicon.ico' } + * ] + * }) + * .build(); + * + * // Result: + * + */ + addIcons(options: IconOptions) { + if (options.icon) { + for (const { url, ...iconOptions } of options.icon) { + this.links.push({ + rel: 'icon', + href: this.resolveUrl(url), + ...iconOptions, + }); + } + } + if (options.shortcut) { + for (const { url, ...shortcutOptions } of options.shortcut) { + this.links.push({ + rel: 'shortcut icon', + href: this.resolveUrl(url), + ...shortcutOptions, + }); + } + } + if (options.apple) { + for (const { url, ...appleOptions } of options.apple) { + this.links.push({ + rel: 'apple-touch-icon', + href: this.resolveUrl(url), + ...appleOptions, + }); + } + } + if (options.other) { + for (const { rel, url, ...otherOptions } of options.other) { + this.links.push({ + rel: rel || 'icon', + href: this.resolveUrl(url), + ...otherOptions, + }); + } + } + return this; + } + + /** + * Adds robot directives meta tag controlling search engine crawler behavior + * + * The robots meta tag provides directives to search engine crawlers and other automated + * bots about how they should treat and index your page. It controls indexing, following + * links, snippet generation, image indexing, and other crawler behaviors. + * + * @example + * const head = new HeadBuilder() + * .addRobot({ + * index: true, + * follow: true + * }) + * .build(); + * + * // Result: + * + */ + addRobot(options: RobotOptions) { + const contentParts: string[] = []; + + if (options.index) { + contentParts.push(options.index ? 'index' : 'noindex'); + } + if (options.follow) { + contentParts.push(options.follow ? 'follow' : 'nofollow'); + } + if (options.noarchive) { + contentParts.push('noarchive'); + } + if (options.nosnippet) { + contentParts.push('nosnippet'); + } + if (options.noimageindex) { + contentParts.push('noimageindex'); + } + if (options.nocache) { + contentParts.push('nocache'); + } + if (options.notranslate) { + contentParts.push('notranslate'); + } + if (options.indexifembedded) { + contentParts.push('indexifembedded'); + } + if (options.nositelinkssearchbox) { + contentParts.push('nositelinkssearchbox'); + } + if (options.unavailable_after) { + contentParts.push(`unavailable_after:${options.unavailable_after}`); + } + if (options['max-video-preview']) { + contentParts.push(`max-video-preview:${options['max-video-preview']}`); + } + if (options['max-image-preview']) { + contentParts.push(`max-image-preview:${options['max-image-preview']}`); + } + if (options['max-snippet']) { + contentParts.push(`max-snippet:${options['max-snippet']}`); + } + + if (contentParts.length > 0) { + const content = contentParts.join(', '); + this.meta.push({ name: 'robots', content }); + } + + return this; + } + + /** + * Adds external stylesheet link elements + * + * External stylesheets define the visual styling for your page. This method adds link elements + * with rel="stylesheet" pointing to CSS files. You can specify multiple stylesheets and include + * additional attributes like media queries, integrity hashes for security, and preload hints. + * + * + * @example + * const head = new HeadBuilder() + * .addStylesheets([ + * { href: '/styles.css' } + * ]) + * .build(); + * + * // Result: + * + */ + addStylesheets(stylesheets: Omit[]) { + for (const { href, ...stylesheet } of stylesheets) { + this.links.push({ rel: 'stylesheet', href, ...stylesheet }); + } + return this; + } + + /** + * Adds custom link elements + * + * This method allows adding any link element with custom rel values and attributes. + * Use this for adding links like preconnect, prefetch, dns-prefetch, preload, or custom + * relationships that aren't covered by other specific methods. + * + * @example + * const HEAD = new HeadBuilder() + * .addOtherLinks([ + * { rel: 'preconnect', href: 'https://fonts.googleapis.com' }, + * { rel: 'preconnect', href: 'https://cdn.devsantara.com' } + * ]) + * .build(); + * + * // Result: + * + * + */ + addLinks(links: Link[]) { + for (const link of links) { + this.links.push(link); + } + return this; + } + + /** + * Adds custom meta elements + * + * This method allows adding any meta element that isn't covered by the specific + * helper methods. Use this for custom properties, tracking codes, application-specific + * meta tags, or platform-specific directives. + * + * @example + * const head = new HeadBuilder() + * .addMeta([ + * { name: 'apple-mobile-web-app-capable', content: 'yes' }, + * { name: 'apple-mobile-web-app-status-bar-style', content: 'black-translucent' } + * ]) + * .build(); + * + * // Result: + * + * + */ + addMeta(meta: Meta[]) { + for (const m of meta) { + this.meta.push(m); + } + return this; + } + + /** + * Adds custom script elements + * + * This method allows adding script elements to the head section, such as analytics, + * tracking scripts, third-party integrations, or utility scripts that need to execute + * before page content loads. You can specify attributes like async, defer, type, and more. + * + * @example + * // Analytics and tracking scripts + * const head = new HeadBuilder() + * .addScripts([ + * { src: 'https://www.googletagmanager.com/gtag/js?id=GA-123456', async: true }, + * { src: 'https://cdn.devsantara.com/analytics.js', defer: true } + * ]) + * .build(); + * + * // Result: + * + * + */ + addScripts(scripts: Script[]) { + for (const script of scripts) { + this.scripts.push(script); + } + return this; + } + + /** + * Adds custom style elements + * + * This method allows adding inline style elements directly to the head section. + * Use this for critical CSS, utility styles, or dynamic styles that need to be + * applied before stylesheets load. Inline styles have higher priority than external + * stylesheets for loading performance. + * + * @example + * const head = new HeadBuilder() + * .addStyles([ + * { + * children: ` + * .header { background: #333; color: white; padding: 20px; } + * .hero { min-height: 100vh; display: flex; align-items: center; } + * ` + * } + * ]) + * .build(); + * + * // HTML Result: + * + */ + addStyles(styles: Style[]) { + for (const style of styles) { + this.styles.push(style); + } + return this; + } + + /** + * Builds and returns the complete head result object + * + * This method finalizes the construction of all head elements collected through + * the various add* methods and returns them as a structured HeadResult object. + * The result contains separate arrays for meta tags, link elements, scripts, and styles, + * ready to be rendered in the document head. + */ + build(): Awaitable { + return { + links: this.links, + scripts: this.scripts, + meta: this.meta, + styles: this.styles, + }; + } +} diff --git a/src/lib/seo/types/alternate.ts b/src/lib/seo/types/alternate.ts new file mode 100644 index 0000000..889e234 --- /dev/null +++ b/src/lib/seo/types/alternate.ts @@ -0,0 +1,8 @@ +export type AlternateLocaleKey = + | ('x-default' | TLocale) + | (string & {}); + +export type AlternateLocaleOptions = Record< + AlternateLocaleKey, + string | URL +>; diff --git a/src/lib/seo/types/metadata.ts b/src/lib/seo/types/metadata.ts new file mode 100644 index 0000000..7cb2015 --- /dev/null +++ b/src/lib/seo/types/metadata.ts @@ -0,0 +1,51 @@ +export interface ViewportOptions { + width?: string | number; + height?: string | number; + initialScale?: number; + minimumScale?: number; + maximumScale?: number; + userScalable?: boolean; + viewportFit?: 'auto' | 'cover' | 'contain'; + interactiveWidget?: 'resizes-visual' | 'resizes-content' | 'overlays-content'; +} + +export type ColorSchemeOptions = + | 'normal' + | 'light' + | 'dark' + | 'light dark' + | 'dark light' + | 'only light'; + +export interface RobotOptions { + index?: boolean; + follow?: boolean; + noarchive?: boolean; + nosnippet?: boolean; + noimageindex?: boolean; + nocache?: boolean; + notranslate?: boolean; + indexifembedded?: boolean; + nositelinkssearchbox?: boolean; + unavailable_after?: string; + 'max-video-preview'?: number | string; + 'max-image-preview'?: 'none' | 'standard' | 'large'; + 'max-snippet'?: number; +} + +export interface IconOptions { + icon?: Omit[]; + shortcut?: Omit[]; + apple?: Omit[]; + other?: Icon[]; +} + +interface Icon { + url: string | URL; + type?: string; + sizes?: string; + color?: string; + rel?: string; + media?: string; + fetchPriority?: 'high' | 'low' | 'auto'; +} diff --git a/src/lib/seo/types/opengraph.ts b/src/lib/seo/types/opengraph.ts new file mode 100644 index 0000000..1548b74 --- /dev/null +++ b/src/lib/seo/types/opengraph.ts @@ -0,0 +1,113 @@ +export interface OpenGraphOptions { + title?: string; + description?: string; + url?: string; + locale?: string; + image?: { + url: string | URL; + alt?: string; + type?: string; + width?: number; + height?: number; + }; + type?: OpenGraphTypeProperty; +} + +type OpenGraphTypeProperty = + | { name: 'article'; properties: ArticleMetadataProperty[] } + | { name: 'book'; properties: BookMetadataProperty[] } + | { name: 'music.song'; properties: MusicSongMetadataProperty[] } + | { name: 'music.album'; properties: MusicAlbumMetadataProperty[] } + | { name: 'music.playlist'; properties: MusicPlaylistMetadataProperty[] } + | { + name: 'music.radio_station'; + properties: MusicRadioStationMetadataProperty[]; + } + | { name: 'profile'; properties: ProfileMetadataProperty[] } + | { name: 'video.tv_show'; properties: VideoTvShowMetadataProperty[] } + | { name: 'video.other'; properties: VideoOtherMetadataProperty[] } + | { name: 'video.movie'; properties: VideoMovieProperty[] } + | { name: 'video.episode'; properties: VideoEpisodeMetadataProperty[] } + | { name: 'website' }; + +type ArticleMetadataProperty = + | { name: 'article:published_time'; content: string } + | { name: 'article:modified_time'; content: string } + | { name: 'article:expiration_time'; content: string } + | { name: 'article:author'; content: string } + | { name: 'article:section'; content: string } + | { name: 'article:tag'; content: string }; + +type BookMetadataProperty = + | { name: 'book:isbn'; content: string } + | { name: 'book:release_date'; content: string } + | { name: 'book:author'; content: string } + | { name: 'book:tag'; content: string }; + +type ProfileMetadataProperty = + | { name: 'profile:first_name'; content: string } + | { name: 'profile:last_name'; content: string } + | { name: 'profile:username'; content: string } + | { name: 'profile:gender'; content: string }; + +type MusicSongMetadataProperty = + | { name: 'music:duration'; content: string } + | { name: 'music:album'; content: string } + | { name: 'music:album:disc'; content: string } + | { name: 'music:album:track'; content: string } + | { name: 'music:musician'; content: string }; + +type MusicAlbumMetadataProperty = + | { name: 'music:song'; content: string } + | { name: 'music:song:disc'; content: string } + | { name: 'music:song:track'; content: string } + | { name: 'music:musician'; content: string } + | { name: 'music:release_date'; content: string }; + +type MusicPlaylistMetadataProperty = + | { name: 'music:song'; content: string } + | { name: 'music:song:disc'; content: string } + | { name: 'music:song:track'; content: string } + | { name: 'music:creator'; content: string }; + +interface MusicRadioStationMetadataProperty { + name: 'music:creator'; + content: string; +} + +type VideoMovieProperty = + | { name: 'video:actor'; content: string } + | { name: 'video:actor:role'; content: string } + | { name: 'video:director'; content: string } + | { name: 'video:writer'; content: string } + | { name: 'video:duration'; content: string } + | { name: 'video:release_date'; content: string } + | { name: 'video:tag'; content: string }; + +type VideoEpisodeMetadataProperty = + | { name: 'video:actor'; content: string } + | { name: 'video:actor:role'; content: string } + | { name: 'video:director'; content: string } + | { name: 'video:writer'; content: string } + | { name: 'video:duration'; content: string } + | { name: 'video:release_date'; content: string } + | { name: 'video:tag'; content: string } + | { name: 'video:series'; content: string }; + +type VideoTvShowMetadataProperty = + | { name: 'video:actor'; content: string } + | { name: 'video:actor:role'; content: string } + | { name: 'video:director'; content: string } + | { name: 'video:writer'; content: string } + | { name: 'video:duration'; content: string } + | { name: 'video:release_date'; content: string } + | { name: 'video:tag'; content: string }; + +type VideoOtherMetadataProperty = + | { name: 'video:actor'; content: string } + | { name: 'video:actor:role'; content: string } + | { name: 'video:director'; content: string } + | { name: 'video:writer'; content: string } + | { name: 'video:duration'; content: string } + | { name: 'video:release_date'; content: string } + | { name: 'video:tag'; content: string }; diff --git a/src/lib/seo/types/twitter.ts b/src/lib/seo/types/twitter.ts new file mode 100644 index 0000000..fff565b --- /dev/null +++ b/src/lib/seo/types/twitter.ts @@ -0,0 +1,42 @@ +export interface TwitterOptions { + title?: string; + description?: string; + site?: string; + siteId?: string; + creator?: string; + creatorId?: string; + image?: { + url: string | URL; + alt?: string; + }; + card?: TwitterCardProperty; +} + +type TwitterCardProperty = + | { name: 'summary' } + | { name: 'summary_large_image' } + | { + name: 'player'; + properties: TwitterPlayerProperty[]; + } + | { + name: 'app'; + properties: TwitterAppProperty[]; + }; + +type TwitterPlayerProperty = + | { name: 'twitter:player'; content: string | URL } + | { name: 'twitter:player:width'; content: number } + | { name: 'twitter:player:height'; content: number } + | { name: 'twitter:player:stream'; content: string | URL }; + +type TwitterAppProperty = + | { name: 'twitter:app:name:iphone'; content: string } + | { name: 'twitter:app:id:iphone'; content: string | number } + | { name: 'twitter:app:url:iphone'; content: string | URL } + | { name: 'twitter:app:name:ipad'; content: string } + | { name: 'twitter:app:id:ipad'; content: string | number } + | { name: 'twitter:app:url:ipad'; content: string | URL } + | { name: 'twitter:app:name:googleplay'; content: string } + | { name: 'twitter:app:id:googleplay'; content: string } + | { name: 'twitter:app:url:googleplay'; content: string | URL }; diff --git a/src/router.ts b/src/router.ts index 8151b77..d17e02b 100644 --- a/src/router.ts +++ b/src/router.ts @@ -1,11 +1,27 @@ -import { createRouter as createTanstackRouter } from '@tanstack/react-router'; +import { + createRouter as createTanstackRouter, + ErrorComponent, +} from '@tanstack/react-router'; +import { posthog } from 'posthog-js'; +import { deLocalizeUrl, localizeUrl } from '~/lib/i18n/runtime'; import { routeTree } from '~/routeTree.gen'; export function getRouter() { - return createTanstackRouter({ + const router = createTanstackRouter({ routeTree, defaultPreload: 'intent', scrollRestoration: true, + trailingSlash: 'never', + defaultErrorComponent: ErrorComponent, + defaultOnCatch(error) { + posthog.captureException(error); + }, + rewrite: { + input: ({ url }) => deLocalizeUrl(url), + output: ({ url }) => localizeUrl(url), + }, }); + + return router; } diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx index 4c391ba..13a15df 100644 --- a/src/routes/__root.tsx +++ b/src/routes/__root.tsx @@ -8,23 +8,74 @@ import * as React from 'react'; import { preload } from 'react-dom'; import { tanstackRouterDevtools } from '~/devtools/router-devtools'; +import { m } from '~/lib/i18n/messages'; +import { + baseLocale, + getLocale, + localizeHref, + type Locale, +} from '~/lib/i18n/runtime'; +import { PostHogProvider } from '~/lib/posthog/provider'; +import { HeadBuilder } from '~/lib/seo/head'; import { Toaster } from '~/ui/components/core/sonner'; import appStylesheet from '~/ui/styles/app.css?url'; import fontStylesheet from '~/ui/styles/fonts.css?url'; import { ThemeProvider } from '~/ui/styles/theme'; export const Route = createRootRoute({ - head: () => ({ - meta: [ - { charSet: 'utf-8' }, - { name: 'viewport', content: 'width=device-width, initial-scale=1' }, - { title: 'Devsantara Kit' }, - ], - links: [ - { rel: 'stylesheet', href: fontStylesheet }, - { rel: 'stylesheet', href: appStylesheet }, - ], - }), + loader: ({ location }) => { + return { currentHref: location.url.origin + location.url.pathname }; + }, + head: ({ loaderData }) => { + /** @example http://localhost:3000/path/without/locale */ + const currentHref = loaderData?.currentHref ?? ''; + + return new HeadBuilder(new URL(currentHref)) + .addCharSet('utf-8') + .addViewport({ width: 'device-width', initialScale: 1 }) + .addStylesheets([{ href: fontStylesheet }, { href: appStylesheet }]) + .addTitle(m.app_name()) + .addDescription(m.app_description()) + .addColorScheme('light dark') + .addManifest('/site.webmanifest') + .addCanonical(localizeHref(currentHref)) + .addAlternateLocales({ + 'x-default': localizeHref(currentHref, { locale: baseLocale }), + en: localizeHref(currentHref, { locale: 'en' }), + id: localizeHref(currentHref, { locale: 'id' }), + 'zh-CN': localizeHref(currentHref, { locale: 'zh-CN' }), + }) + .addIcons({ + shortcut: [{ url: '/favicon.ico', sizes: '48x48' }], + icon: [ + { type: 'image/svg+xml', url: '/favicon.svg', sizes: 'any' }, + { type: 'image/png', url: '/favicon-96x96.png', sizes: '96x96' }, + ], + apple: [{ url: '/apple-touch-icon.png', sizes: '180x180' }], + }) + .addOpenGraph({ + title: m.app_name(), + description: m.app_description(), + locale: getLocale(), + type: { name: 'website' }, + url: localizeHref(currentHref), + image: { + url: 'https://assets.devsantara.com/kit/thumbnail.jpg', + type: 'image/jpeg', + width: 1280, + height: 640, + }, + }) + .addTwitter({ + card: { name: 'summary' }, + title: m.app_name(), + description: m.app_description(), + site: '@devsantara_hq', + image: { url: 'https://assets.devsantara.com/kit/thumbnail.jpg' }, + }) + .addMeta([{ name: 'apple-mobile-web-app-title', content: m.app_name() }]) + .build(); + }, shellComponent: RootDocument, }); @@ -33,14 +84,14 @@ function RootDocument({ children }: { children: React.ReactNode }) { preload(jetBrainsMonoFont, { as: 'font', type: 'font/woff2' }); return ( - + - {children} + {children}

- Devsantara Kit + {m.app_name()}

- The blueprint for your next big idea + {m.app_description()}

git@github.com:devsantara/kit.git diff --git a/src/server.ts b/src/server.ts index 827b26f..2194408 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,32 +1,9 @@ -import handler, { createServerEntry } from '@tanstack/react-start/server-entry'; -import { waitUntil as workerWaitUntil } from 'cloudflare:workers'; +import handler from '@tanstack/react-start/server-entry'; -/** - * Schedules a promise to continue running after the current request has returned. - * - * On Cloudflare Workers, this delegates to the platform `waitUntil()` to extend - * the lifetime of the invocation for background work (e.g. flushing logs, analytics, graceful shutdown tasks). - * - * In non-Workers runtimes (or when the Workers API isn't available), it falls - * back to returning the promise without scheduling—callers may still `await` it, - * but it won't be kept alive beyond the request lifecycle by the platform. - * - * @param promise - Work to run in the background / after the response is sent. - */ -function waitUntil(promise: Promise) { - if (workerWaitUntil) { - return workerWaitUntil(promise); - } - return promise; -} +import { paraglideMiddleware } from '~/lib/i18n/server'; -/** Perform tasks before server shutdown */ -async function shutdown() {} - -export default createServerEntry({ - async fetch(request) { - const response = handler.fetch(request); - await waitUntil(shutdown()); - return response; +export default { + fetch(req: Request): Promise { + return paraglideMiddleware(req, () => handler.fetch(req)); }, -}); +}; diff --git a/src/ui/components/core/alert.tsx b/src/ui/components/core/alert.tsx index 0f9439e..d933a42 100644 --- a/src/ui/components/core/alert.tsx +++ b/src/ui/components/core/alert.tsx @@ -10,7 +10,7 @@ const alertVariants = cva( variant: { default: 'bg-card text-card-foreground', destructive: - 'text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90', + 'bg-card text-destructive *:data-[slot=alert-description]:text-destructive/90 [&>svg]:text-current', }, }, defaultVariants: { diff --git a/src/ui/components/core/badge.tsx b/src/ui/components/core/badge.tsx index 8d5bb1f..645c982 100644 --- a/src/ui/components/core/badge.tsx +++ b/src/ui/components/core/badge.tsx @@ -14,7 +14,7 @@ const badgeVariants = cva( secondary: 'border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90', destructive: - 'border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60', + 'border-transparent bg-destructive text-white focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40 [a&]:hover:bg-destructive/90', outline: 'text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground', }, diff --git a/src/ui/components/core/button.tsx b/src/ui/components/core/button.tsx index e6e9f36..0cbadc8 100644 --- a/src/ui/components/core/button.tsx +++ b/src/ui/components/core/button.tsx @@ -11,9 +11,9 @@ const buttonVariants = cva( variant: { default: 'bg-primary text-primary-foreground hover:bg-primary/90', destructive: - 'bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60', + 'bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40', outline: - 'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50', + 'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50', secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80', ghost: @@ -22,7 +22,7 @@ const buttonVariants = cva( }, size: { default: 'h-9 px-4 py-2 has-[>svg]:px-3', - sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5', + sm: 'h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5', lg: 'h-10 rounded-md px-6 has-[>svg]:px-4', icon: 'size-9', 'icon-sm': 'size-8', diff --git a/src/ui/components/core/calendar.tsx b/src/ui/components/core/calendar.tsx index dcae41f..81302cf 100644 --- a/src/ui/components/core/calendar.tsx +++ b/src/ui/components/core/calendar.tsx @@ -83,7 +83,7 @@ function Calendar({ 'font-medium select-none', captionLayout === 'label' ? 'text-sm' - : 'rounded-md pl-2 pr-1 flex items-center gap-1 text-sm h-8 [&>svg]:text-muted-foreground [&>svg]:size-3.5', + : 'flex h-8 items-center gap-1 rounded-md pr-1 pl-2 text-sm [&>svg]:size-3.5 [&>svg]:text-muted-foreground', defaultClassNames.caption_label, ), table: 'w-full border-collapse', diff --git a/src/ui/components/core/chart.tsx b/src/ui/components/core/chart.tsx index cd7cf2f..58bfd81 100644 --- a/src/ui/components/core/chart.tsx +++ b/src/ui/components/core/chart.tsx @@ -1,13 +1,12 @@ -import type { LegendPayload } from 'recharts/types/component/DefaultLegendContent'; -import type { Props as LegendProps } from 'recharts/types/component/Legend'; - import * as React from 'react'; import * as RechartsPrimitive from 'recharts'; +import type { LegendPayload } from 'recharts/types/component/DefaultLegendContent'; import { NameType, Payload, ValueType, } from 'recharts/types/component/DefaultTooltipContent'; +import type { Props as LegendProps } from 'recharts/types/component/Legend'; import { TooltipContentProps } from 'recharts/types/component/Tooltip'; import { cn } from '~/ui/utils'; @@ -94,7 +93,7 @@ function ChartContainer({ data-slot="chart" data-chart={chartId} className={cn( - "[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border flex aspect-video justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden", + "flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden", className, )} {...props} @@ -209,7 +208,7 @@ function ChartTooltipContent({ return (
@@ -224,7 +223,7 @@ function ChartTooltipContent({
svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5', + 'flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground', indicator === 'dot' && 'items-center', )} > @@ -269,7 +268,7 @@ function ChartTooltipContent({
{item.value && ( - + {item.value.toLocaleString()} )} @@ -315,7 +314,7 @@ function ChartLegendContent({
svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3', + 'flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground', )} > {itemConfig?.icon && !hideIcon ? ( diff --git a/src/ui/components/core/empty.tsx b/src/ui/components/core/empty.tsx index c4a0d89..e687401 100644 --- a/src/ui/components/core/empty.tsx +++ b/src/ui/components/core/empty.tsx @@ -34,7 +34,7 @@ const emptyMediaVariants = cva( variants: { variant: { default: 'bg-transparent', - icon: "bg-muted text-foreground flex size-10 shrink-0 items-center justify-center rounded-lg [&_svg:not([class*='size-'])]:size-6", + icon: "flex size-10 shrink-0 items-center justify-center rounded-lg bg-muted text-foreground [&_svg:not([class*='size-'])]:size-6", }, }, defaultVariants: { diff --git a/src/ui/components/core/field.tsx b/src/ui/components/core/field.tsx index 3d46872..490d418 100644 --- a/src/ui/components/core/field.tsx +++ b/src/ui/components/core/field.tsx @@ -64,7 +64,7 @@ const fieldVariants = cva( 'has-[>[data-slot=field-content]]:items-start has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px', ], responsive: [ - 'flex-col *:w-full [&>.sr-only]:w-auto @md/field-group:flex-row @md/field-group:items-center @md/field-group:*:w-auto', + 'flex-col *:w-full @md/field-group:flex-row @md/field-group:items-center @md/field-group:*:w-auto [&>.sr-only]:w-auto', '@md/field-group:*:data-[slot=field-label]:flex-auto', '@md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px', ], diff --git a/src/ui/components/core/input-group.tsx b/src/ui/components/core/input-group.tsx index 8c6d51f..2c38611 100644 --- a/src/ui/components/core/input-group.tsx +++ b/src/ui/components/core/input-group.tsx @@ -44,9 +44,9 @@ const inputGroupAddonVariants = cva( 'inline-end': 'order-last pr-3 has-[>button]:mr-[-0.45rem] has-[>kbd]:mr-[-0.35rem]', 'block-start': - 'order-first w-full justify-start px-3 pt-3 [.border-b]:pb-3 group-has-[>input]/input-group:pt-2.5', + 'order-first w-full justify-start px-3 pt-3 group-has-[>input]/input-group:pt-2.5 [.border-b]:pb-3', 'block-end': - 'order-last w-full justify-start px-3 pb-3 [.border-t]:pt-3 group-has-[>input]/input-group:pb-2.5', + 'order-last w-full justify-start px-3 pb-3 group-has-[>input]/input-group:pb-2.5 [.border-t]:pt-3', }, }, defaultVariants: { @@ -83,8 +83,8 @@ const inputGroupButtonVariants = cva( { variants: { size: { - xs: "h-6 gap-1 px-2 rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-3.5 has-[>svg]:px-2", - sm: 'h-8 px-2.5 gap-1.5 rounded-md has-[>svg]:px-2.5', + xs: "h-6 gap-1 rounded-[calc(var(--radius)-5px)] px-2 has-[>svg]:px-2 [&>svg:not([class*='size-'])]:size-3.5", + sm: 'h-8 gap-1.5 rounded-md px-2.5 has-[>svg]:px-2.5', 'icon-xs': 'size-6 rounded-[calc(var(--radius)-5px)] p-0 has-[>svg]:p-0', 'icon-sm': 'size-8 p-0 has-[>svg]:p-0', diff --git a/src/ui/components/core/item.tsx b/src/ui/components/core/item.tsx index b5b900c..7dc1db6 100644 --- a/src/ui/components/core/item.tsx +++ b/src/ui/components/core/item.tsx @@ -40,8 +40,8 @@ const itemVariants = cva( muted: 'bg-muted/50', }, size: { - default: 'p-4 gap-4 ', - sm: 'py-3 px-4 gap-2.5', + default: 'gap-4 p-4', + sm: 'gap-2.5 px-4 py-3', }, }, defaultVariants: { @@ -77,9 +77,9 @@ const itemMediaVariants = cva( variants: { variant: { default: 'bg-transparent', - icon: "size-8 border rounded-sm bg-muted [&_svg:not([class*='size-'])]:size-4", + icon: "size-8 rounded-sm border bg-muted [&_svg:not([class*='size-'])]:size-4", image: - 'size-10 rounded-sm overflow-hidden [&_img]:size-full [&_img]:object-cover', + 'size-10 overflow-hidden rounded-sm [&_img]:size-full [&_img]:object-cover', }, }, defaultVariants: { diff --git a/src/ui/components/core/resizable.tsx b/src/ui/components/core/resizable.tsx index 038241a..2238dab 100644 --- a/src/ui/components/core/resizable.tsx +++ b/src/ui/components/core/resizable.tsx @@ -39,7 +39,7 @@ function ResizableSeparator({ className={cn( 'relative flex items-center justify-center bg-border', // When in focus - 'focus-visible:ring-ring focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:outline-hidden', + 'focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:outline-hidden', // Dom pseudo element :after 'after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2', // When the orientation changes @@ -54,7 +54,7 @@ function ResizableSeparator({ {...props} > {withHandle && ( -
+
)} diff --git a/src/ui/components/core/sheet.tsx b/src/ui/components/core/sheet.tsx index 20c2342..467c916 100644 --- a/src/ui/components/core/sheet.tsx +++ b/src/ui/components/core/sheet.tsx @@ -58,13 +58,13 @@ function SheetContent({ className={cn( 'fixed z-50 flex flex-col gap-4 bg-background shadow-lg transition ease-in-out data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:animate-in data-[state=open]:duration-500', side === 'right' && - 'data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm', + 'inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm', side === 'left' && - 'data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm', + 'inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm', side === 'top' && - 'data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b', + 'inset-x-0 top-0 h-auto border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top', side === 'bottom' && - 'data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t', + 'inset-x-0 bottom-0 h-auto border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom', className, )} {...props} diff --git a/src/ui/components/core/sidebar.tsx b/src/ui/components/core/sidebar.tsx index 8f51276..4114e5a 100644 --- a/src/ui/components/core/sidebar.tsx +++ b/src/ui/components/core/sidebar.tsx @@ -565,7 +565,7 @@ function SidebarMenuAction({ 'peer-data-[size=lg]/menu-button:top-2.5', 'group-data-[collapsible=icon]:hidden', showOnHover && - 'peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0', + 'group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground data-[state=open]:opacity-100 md:opacity-0', className, )} {...props} diff --git a/src/ui/components/core/toggle-group.tsx b/src/ui/components/core/toggle-group.tsx index d35d7aa..bb5e732 100644 --- a/src/ui/components/core/toggle-group.tsx +++ b/src/ui/components/core/toggle-group.tsx @@ -1,6 +1,5 @@ -import type { VariantProps } from 'class-variance-authority'; - import * as ToggleGroupPrimitive from '@radix-ui/react-toggle-group'; +import type { VariantProps } from 'class-variance-authority'; import * as React from 'react'; import { toggleVariants } from '~/ui/components/core/toggle'; diff --git a/src/ui/components/core/toggle.tsx b/src/ui/components/core/toggle.tsx index 46f81a7..cbc3f7f 100644 --- a/src/ui/components/core/toggle.tsx +++ b/src/ui/components/core/toggle.tsx @@ -14,9 +14,9 @@ const toggleVariants = cva( 'border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground', }, size: { - default: 'h-9 px-2 min-w-9', - sm: 'h-8 px-1.5 min-w-8', - lg: 'h-10 px-2.5 min-w-10', + default: 'h-9 min-w-9 px-2', + sm: 'h-8 min-w-8 px-1.5', + lg: 'h-10 min-w-10 px-2.5', }, }, defaultVariants: { diff --git a/tsconfig.json b/tsconfig.json index acd4bac..3bcc0e1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,7 @@ { "include": [ "@types/**/*.ts", + "src/**/*.js", "src/**/*.ts", "src/**/*.tsx", ".storybook/**/*.ts", @@ -17,6 +18,7 @@ "target": "ES2022", "jsx": "react-jsx", "module": "ESNext", + "allowJs": true, "lib": ["ES2022", "DOM", "DOM.Iterable"], "types": ["vite/client", "@cloudflare/workers-types"], diff --git a/vite.config.ts b/vite.config.ts index ec05082..8d6776e 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,3 +1,4 @@ +import { paraglideVitePlugin as paraglide } from '@inlang/paraglide-js'; import tailwindcss from '@tailwindcss/vite'; import { devtools } from '@tanstack/devtools-vite'; import { tanstackStart } from '@tanstack/react-start/plugin/vite'; @@ -6,6 +7,8 @@ import alchemy from 'alchemy/cloudflare/tanstack-start'; import { defineConfig, loadEnv, type ConfigEnv } from 'vite'; import tsConfigPaths from 'vite-tsconfig-paths'; +import { posthog } from './src/lib/posthog/plugin'; + export default async function viteConfig({ mode }: ConfigEnv) { /** * Environment Variables aren't loaded automatically @@ -23,18 +26,58 @@ export default async function viteConfig({ mode }: ConfigEnv) { target: 'esnext', minify: true, cssMinify: true, + sourcemap: true, rollupOptions: { + output: { + manualChunks: (id) => { + if (id.includes('posthog-js') || id.includes('@posthog/react')) { + return 'posthog'; + } + }, + }, external: ['node:async_hooks', 'cloudflare:workers'], }, }, plugins: [ - alchemy(), + alchemy({ viteEnvironment: { name: 'ssr' } }), devtools(), tailwindcss(), tsConfigPaths({ projects: ['./tsconfig.json'] }), tanstackStart({ srcDirectory: 'src', router: { routeToken: 'layout' } }), // React's vite plugin must come after start's vite plugin viteReact({ babel: { plugins: ['babel-plugin-react-compiler'] } }), + paraglide({ + project: './project.inlang', + outdir: './src/lib/i18n', + cookieName: 'LOCALE', + outputStructure: 'message-modules', + strategy: ['url', 'cookie', 'preferredLanguage', 'baseLocale'], + // DisableAsyncLocalStorage should ONLY be used in serverless environments like Cloudflare Workers. + disableAsyncLocalStorage: true, + urlPatterns: [ + { + pattern: '/', + localized: [ + ['en', '/en'], + ['id', '/id'], + ['zh-CN', '/zh-CN'], + ], + }, + { + pattern: '/:path(.*)?', + localized: [ + ['en', '/en/:path(.*)?'], + ['id', '/id/:path(.*)?'], + ['zh-CN', '/zh-CN/:path(.*)?'], + ], + }, + ], + }), + posthog({ + host: process.env.POSTHOG_CLI_HOST, + envId: process.env.POSTHOG_CLI_ENV_ID, + personalApiKey: process.env.POSTHOG_CLI_TOKEN, + }), ], }); } diff --git a/vite.storybook.ts b/vite.storybook.ts new file mode 100644 index 0000000..42f4e65 --- /dev/null +++ b/vite.storybook.ts @@ -0,0 +1,7 @@ +import tailwindcss from '@tailwindcss/vite'; +import { defineConfig } from 'vite'; +import tsConfigPaths from 'vite-tsconfig-paths'; + +export default defineConfig({ + plugins: [tsConfigPaths({ projects: ['./tsconfig.json'] }), tailwindcss()], +});