diff --git a/.dockerignore b/.dockerignore index 74c010b43..521c55109 100644 --- a/.dockerignore +++ b/.dockerignore @@ -6,7 +6,14 @@ npm-debug.log README.md .next .turbo +.pnpm-store +**/.pnpm-store +coverage +**/coverage +**/.docusaurus +**/build .git +.github dev .build e2e diff --git a/.env.example b/.env.example index a6ca0698d..22dca3adb 100644 --- a/.env.example +++ b/.env.example @@ -66,11 +66,4 @@ UNSAFE_ENABLE_MOCK_INTEGRATION=true # Whether demo mode is read-only. Defaults to true (the public demo blocks all mutations). # Set to false for writable preview deployments so the demo user can modify the dashboard. -# DEMO_READ_ONLY=false - -# Community workshop (PocketBase) base URL used by the in-app "Install from Workshop" buttons -# Workshop API override for local Homarr development: -# NEXT_PUBLIC_WORKSHOP_API_URL=http://localhost:8090 -# Docs site workshop URL used for "Open Workshop" links -# In production this defaults to https://homarr.dev/workshop -# NEXT_PUBLIC_WORKSHOP_URL=http://localhost:3003/workshop +# DEMO_READ_ONLY=false \ No newline at end of file diff --git a/.github/act/.gitignore b/.github/act/.gitignore new file mode 100644 index 000000000..bf697bacf --- /dev/null +++ b/.github/act/.gitignore @@ -0,0 +1,2 @@ +.secrets +!.secrets.example diff --git a/.github/act/.secrets.example b/.github/act/.secrets.example new file mode 100644 index 000000000..377f2b6cf --- /dev/null +++ b/.github/act/.secrets.example @@ -0,0 +1,4 @@ +TURBO_API= +TURBO_TEAM= +TURBO_TOKEN= +TURBO_REMOTE_CACHE_SIGNATURE_KEY= diff --git a/.github/act/README.md b/.github/act/README.md new file mode 100644 index 000000000..8bac83dfa --- /dev/null +++ b/.github/act/README.md @@ -0,0 +1,36 @@ +# Running CI locally + +Install [Act](https://nektosact.com/installation/index.html) and ensure Docker is running, then use: + +```bash +pnpm ci:act:fast +pnpm ci:act:docker +pnpm ci:act:all +pnpm ci:act:cleanup +``` + +`fast` runs the quality gate. `docker` and `all` run the gate followed by the single container build, E2E suite, and memory regression test. `cleanup` validates the manual cleanup path without calling GitHub. The wrapper supports regular clones and Git worktrees, including non-default Docker contexts. Linux dependencies are kept in Docker volumes instead of overwriting host `node_modules`. The first run downloads the Act runner and Playwright browser dependencies. + +Remote caching is optional locally. Copy `.github/act/.secrets.example` to `.github/act/.secrets`, populate it with rotated credentials, and run with: + +```bash +ACT_SECRET_FILE=.github/act/.secrets pnpm ci:act:all +``` + +Act never publishes or deletes GHCR images. The secret file is ignored by Git. + +## PR preview images + +Successful non-draft pull requests from this repository publish an amd64 image: + +```bash +docker pull ghcr.io/homarr-labs/homarr-test:pr- +``` + +The tag is replaced when the pull request is updated and deleted when it closes. Production Homarr images remain the supported multi-architecture releases. The `homarr-test` package must be configured as public in GHCR for unauthenticated testers. + +## Dokploy preview cache + +Keep `needs-demo` as the Dokploy preview label. Under the application's **Preview Build-time Secrets**, configure `TURBO_API`, `TURBO_TEAM`, `TURBO_TOKEN`, and `TURBO_REMOTE_CACHE_SIGNATURE_KEY`. Dokploy passes these to the Dockerfile as BuildKit secrets. Do not configure the token or signature key as Docker build arguments. + +Rotate any credentials that have appeared in chat, logs, or committed configuration before enabling the cache. diff --git a/.github/act/workshop.md b/.github/act/workshop.md deleted file mode 100644 index a0746d4aa..000000000 --- a/.github/act/workshop.md +++ /dev/null @@ -1,19 +0,0 @@ -# Workshop deployment with ACT - -The Workshop workflow builds the same image used on the VPS and smoke-tests both Docusaurus and PocketBase. Registry login and pushes are skipped under ACT. - -From the repository root: - -```sh -pnpm ci:act:workshop -``` - -The wrapper selects `.github/workflows/deployment-workshop.yml`, derives the active Docker socket (including Docker Desktop and OrbStack contexts), and runs only the `deploy` job using a `workflow_dispatch` event. - -To inspect the workflow without running it: - -```sh -act workflow_dispatch --list --workflows .github/workflows/deployment-workshop.yml -``` - -No GitHub or registry secrets are required for the local run. diff --git a/.github/workflows/autofix.yml b/.github/workflows/autofix.yml new file mode 100644 index 000000000..9d54d9669 --- /dev/null +++ b/.github/workflows/autofix.yml @@ -0,0 +1,47 @@ +name: Auto-fix lint and format + +on: + pull_request: + types: [opened, synchronize, reopened] + branches: ["*"] + +permissions: + contents: write + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + autofix: + name: Auto-fix + if: github.event.pull_request.head.repo.id == github.repository_id + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@v7 + with: + fetch-depth: 0 + ref: ${{ github.head_ref }} + + - name: Setup + uses: ./tooling/github/setup + + - name: Get changed files + id: changed + env: + BASE_REF: ${{ github.base_ref }} + run: | + git diff --name-only --diff-filter=ACMR "origin/${BASE_REF}...HEAD" -- '*.ts' '*.tsx' '*.js' '*.jsx' > /tmp/changed-files + echo "count=$(wc -l < /tmp/changed-files | tr -d '[:space:]')" >> $GITHUB_OUTPUT + + - name: Auto-fix lint + format + if: steps.changed.outputs.count != '0' + run: | + xargs -r -a /tmp/changed-files pnpm exec oxlint --fix || true + xargs -r -a /tmp/changed-files pnpm exec oxfmt --write + + - name: Commit fixes + uses: stefanzweifel/git-auto-commit-action@v7 + with: + commit_message: "style: auto-fix lint and format" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..f62afeb28 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,347 @@ +name: CI + +on: + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + branches: ["*"] + pull_request_target: + types: [closed] + branches: ["*"] + push: + branches: [main, dev, beta, next] + merge_group: + workflow_dispatch: + inputs: + operation: + description: Operation to run + required: true + default: validate + type: choice + options: + - validate + - publish + - cleanup + pr_number: + description: PR number for publish or cleanup + required: false + type: number + ref: + description: Git ref to validate or publish + required: false + type: string + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number || inputs.pr_number || github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + +env: + FORCE_COLOR: 3 + SKIP_ENV_VALIDATION: true + TURBO_TELEMETRY_DISABLED: 1 + REGISTRY: ghcr.io + PREVIEW_IMAGE: ghcr.io/homarr-labs/homarr-test + +jobs: + fast-gate: + name: Fast gate + if: github.event_name != 'pull_request_target' && (github.event_name != 'workflow_dispatch' || inputs.operation != 'cleanup') + runs-on: ubuntu-latest + timeout-minutes: 15 + env: + TURBO_API: ${{ secrets.TURBO_API }} + TURBO_TEAM: ${{ secrets.TURBO_TEAM }} + TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} + TURBO_REMOTE_CACHE_SIGNATURE_KEY: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }} + TURBO_SCM_BASE: ${{ github.event.pull_request.base.sha || github.event.merge_group.base_sha || github.event.before || 'HEAD~1' }} + TURBO_SCM_HEAD: ${{ github.sha }} + TURBO_PLATFORM: linux/amd64 + steps: + - uses: actions/checkout@v7 + if: '!env.ACT' + with: + fetch-depth: 0 + persist-credentials: false + ref: ${{ github.event_name == 'workflow_dispatch' && inputs.ref || '' }} + + - name: Validate manual inputs + if: github.event_name == 'workflow_dispatch' && inputs.operation == 'publish' + env: + PR_NUMBER: ${{ inputs.pr_number }} + BUILD_REF: ${{ inputs.ref }} + run: | + test -n "$PR_NUMBER" || { echo "pr_number is required for publish"; exit 1; } + test -n "$BUILD_REF" || { echo "ref is required for publish"; exit 1; } + + - name: Setup + uses: ./tooling/github/setup + + - name: Copy environment defaults + run: cp .env.example .env + + - name: Check formatting in affected workspaces + run: pnpm turbo run format --affected --continue -- --check + + - name: Lint, typecheck, and build affected workspaces + run: pnpm turbo run lint typecheck build --affected --continue=dependencies-successful + + - name: Validate workspace configuration + run: pnpm lint:ws + + - name: Run unit tests + run: pnpm test + + - name: Report coverage + if: always() && !env.ACT + uses: davelosert/vitest-coverage-report-action@v2 + + container: + name: Container, E2E, and memory + needs: fast-gate + if: | + github.event_name == 'merge_group' || + (github.event_name == 'pull_request' && github.event.pull_request.draft == false) || + (github.event_name == 'workflow_dispatch' && (inputs.operation == 'validate' || inputs.operation == 'publish')) + runs-on: ubuntu-latest + timeout-minutes: 30 + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v7 + if: '!env.ACT' + with: + persist-credentials: false + ref: ${{ github.event_name == 'workflow_dispatch' && inputs.ref || '' }} + + - name: Setup + uses: ./tooling/github/setup + + - name: Set up Docker Buildx + if: '!env.ACT' + uses: docker/setup-buildx-action@v4 + + - name: Build container once + if: '!env.ACT' + uses: docker/build-push-action@v7 + with: + context: . + network: host + platforms: linux/amd64 + load: true + push: false + tags: | + homarr-ci + homarr-e2e + labels: | + org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }} + org.opencontainers.image.revision=${{ github.sha }} + org.opencontainers.image.ref.name=pr-${{ github.event.pull_request.number || inputs.pr_number || 'none' }} + cache-from: type=gha,scope=homarr-linux-amd64 + cache-to: type=gha,scope=homarr-linux-amd64,mode=max + secrets: | + TURBO_API=${{ secrets.TURBO_API }} + TURBO_TEAM=${{ secrets.TURBO_TEAM }} + TURBO_TOKEN=${{ secrets.TURBO_TOKEN }} + TURBO_REMOTE_CACHE_SIGNATURE_KEY=${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }} + + - name: Build container once with Act + if: env.ACT + env: + TURBO_API: ${{ secrets.TURBO_API }} + TURBO_TEAM: ${{ secrets.TURBO_TEAM }} + TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} + TURBO_REMOTE_CACHE_SIGNATURE_KEY: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }} + run: | + secret_args=() + for secret in TURBO_API TURBO_TEAM TURBO_TOKEN TURBO_REMOTE_CACHE_SIGNATURE_KEY; do + if [ -n "${!secret:-}" ]; then + secret_args+=(--secret "id=${secret},env=${secret}") + fi + done + DOCKER_BUILDKIT=1 docker build --platform linux/amd64 \ + --tag homarr-ci --tag homarr-e2e "${secret_args[@]}" . + + - name: Cache Playwright browsers + if: '!env.ACT' + id: playwright-cache + uses: actions/cache@v6 + with: + path: ~/.cache/ms-playwright + key: playwright-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }} + + - name: Install Playwright Chromium + if: steps.playwright-cache.outputs.cache-hit != 'true' || env.ACT + run: pnpm exec playwright install --with-deps chromium + + - name: Install Playwright system deps + if: steps.playwright-cache.outputs.cache-hit == 'true' + run: pnpm exec playwright install-deps chromium + + - name: Run E2E tests + run: pnpm test:e2e + + - name: Upload E2E artifacts + if: failure() && !env.ACT + uses: actions/upload-artifact@v7 + with: + name: playwright-report + path: e2e/test-results/ + retention-days: 7 + + - name: Run memory regression test + env: + CONTAINER_NAME: homarr-mem-${{ github.run_id }}-${{ github.run_attempt }} + SECRET_ENCRYPTION_KEY: 0000000000000000000000000000000000000000000000000000000000000000 + run: | + docker run -d --name "$CONTAINER_NAME" -p 127.0.0.1::7575 \ + -e NO_EXTERNAL_CONNECTION=true \ + -e SECRET_ENCRYPTION_KEY="$SECRET_ENCRYPTION_KEY" \ + homarr-ci + PORT=$(docker port "$CONTAINER_NAME" 7575/tcp | sed 's/.*://') + [[ "$PORT" =~ ^[0-9]+$ ]] || { echo "ERROR: invalid port mapping: '$PORT'"; exit 1; } + URL="http://127.0.0.1:${PORT}" + + healthy=false + for i in $(seq 1 30); do + if curl -sf "$URL/api/health/ready" >/dev/null 2>&1; then + echo "Container healthy after $((i * 2))s" + healthy=true + break + fi + sleep 2 + done + + if [ "$healthy" != "true" ]; then + echo "ERROR: Container never became healthy" + docker logs "$CONTAINER_NAME" --tail 100 + exit 1 + fi + + warmup_ok=0 + for _ in $(seq 1 5); do + curl -sf "$URL/" >/dev/null 2>&1 && warmup_ok=$((warmup_ok + 1)) + sleep 2 + done + if [ "$warmup_ok" -eq 0 ]; then + echo "ERROR: All warmup requests failed" + docker logs "$CONTAINER_NAME" --tail 50 + exit 1 + fi + + docker inspect -f '{{.State.Running}}' "$CONTAINER_NAME" | grep -qx true || { + echo "ERROR: Container died before memory measurement" + docker logs "$CONTAINER_NAME" --tail 100 + exit 1 + } + + RAW_MEMORY=$(docker stats --no-stream --format '{{.MemUsage}}' "$CONTAINER_NAME" | sed 's| /.*||') + MEMORY_MB=$(echo "$RAW_MEMORY" | awk ' + /GiB/ { gsub(/GiB/, ""); print $1 * 1024; next } + /MiB/ { gsub(/MiB/, ""); print $1; next } + /KiB/ { gsub(/KiB/, ""); print $1 / 1024; next } + { print $1 / 1048576 } + ') + + echo "Memory usage: ${MEMORY_MB} MB (${RAW_MEMORY})" + [[ "$MEMORY_MB" =~ ^[0-9]+(\.[0-9]+)?$ ]] || { + echo "ERROR: non-numeric memory value: '$MEMORY_MB' (raw: '$RAW_MEMORY')" + docker logs "$CONTAINER_NAME" --tail 100 + exit 1 + } + if awk "BEGIN { exit !($MEMORY_MB < 50) }"; then + echo "ERROR: Memory suspiciously low (${MEMORY_MB} MB) — container may have crashed" + docker logs "$CONTAINER_NAME" --tail 100 + exit 1 + elif awk "BEGIN { exit !($MEMORY_MB > 700) }"; then + echo "ERROR: Memory usage ${MEMORY_MB} MB exceeds 700 MB" + docker logs "$CONTAINER_NAME" --tail 100 + exit 1 + fi + + - name: Clean up memory test container + if: always() + env: + CONTAINER_NAME: homarr-mem-${{ github.run_id }}-${{ github.run_attempt }} + run: docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true + + - name: Determine preview tag + id: preview + if: | + !env.ACT && + ( + (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) || + (github.event_name == 'workflow_dispatch' && inputs.operation == 'publish') + ) + env: + PR_NUMBER: ${{ github.event.pull_request.number || inputs.pr_number }} + run: echo "tag=pr-${PR_NUMBER}" >> "$GITHUB_OUTPUT" + + - name: Log in to GHCR + if: steps.preview.outputs.tag != '' + uses: docker/login-action@v4 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Push preview image + if: steps.preview.outputs.tag != '' + env: + PREVIEW_TAG: ${{ steps.preview.outputs.tag }} + run: | + docker tag homarr-ci "${PREVIEW_IMAGE}:${PREVIEW_TAG}" + docker push "${PREVIEW_IMAGE}:${PREVIEW_TAG}" + + - name: Show Act publication result + if: env.ACT + run: echo "Act validated the publication path; external GHCR writes are disabled locally." + + cleanup: + name: Clean up preview image + if: github.event_name == 'pull_request_target' || (github.event_name == 'workflow_dispatch' && inputs.operation == 'cleanup') + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + packages: write + env: + IMAGE_TAG: pr-${{ github.event.pull_request.number || inputs.pr_number }} + steps: + - name: Validate cleanup input + if: github.event_name == 'workflow_dispatch' + env: + PR_NUMBER: ${{ inputs.pr_number }} + run: test -n "$PR_NUMBER" || { echo "pr_number is required for cleanup"; exit 1; } + + - name: Delete GHCR package version + if: '!env.ACT' + uses: actions/github-script@v8 + with: + script: | + const packageName = "homarr-test"; + const org = context.repo.owner; + const tag = process.env.IMAGE_TAG; + const versions = await github.paginate( + github.rest.packages.getAllPackageVersionsForPackageOwnedByOrg, + { package_type: "container", package_name: packageName, org, per_page: 100 }, + ); + const target = versions.find((version) => + version.metadata?.container?.tags?.includes(tag), + ); + if (!target) { + core.info(`No package version found with tag "${tag}"`); + return; + } + await github.rest.packages.deletePackageVersionForOrg({ + package_type: "container", + package_name: packageName, + org, + package_version_id: target.id, + }); + core.info(`Deleted package version ${target.id} (${tag})`); + + - name: Show Act cleanup result + if: env.ACT + run: echo "Act validated cleanup for ${IMAGE_TAG}; external GHCR writes are disabled locally." diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml deleted file mode 100644 index 48d017cde..000000000 --- a/.github/workflows/code-quality.yml +++ /dev/null @@ -1,165 +0,0 @@ -name: "[Quality] Code Analysis" -permissions: - contents: read - -on: - pull_request: - branches: ["*"] - push: - branches: ["main"] - merge_group: - -concurrency: - group: "${{ github.workflow }}-${{ github.ref }}" - cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} - -env: - FORCE_COLOR: 3 - SKIP_ENV_VALIDATION: true - TURBO_API: ${{ secrets.TURBO_API }} - TURBO_TEAM: ${{ secrets.TURBO_TEAM }} - TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} - TURBO_REMOTE_CACHE_SIGNATURE_KEY: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }} - TURBO_SCM_BASE: ${{ github.event.pull_request.base.sha || github.event.before || 'HEAD~1' }} - TURBO_SCM_HEAD: ${{ github.sha }} - -jobs: - lint-format-autofix: - if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.id == github.repository_id - runs-on: ubuntu-latest - timeout-minutes: 5 - permissions: - contents: write - steps: - - uses: actions/checkout@v7 - with: - fetch-depth: 0 - ref: ${{ github.head_ref }} - - - name: Setup - uses: ./tooling/github/setup - - - name: Get changed files - id: changed - run: | - git diff --name-only --diff-filter=ACMR origin/${{ github.base_ref }}...HEAD -- '*.ts' '*.tsx' '*.js' '*.jsx' > /tmp/changed-files - echo "count=$(wc -l < /tmp/changed-files | tr -d '[:space:]')" >> $GITHUB_OUTPUT - - - name: Auto-fix lint + format - if: steps.changed.outputs.count != '0' - run: | - xargs -r -a /tmp/changed-files pnpm exec oxlint --fix || true - xargs -r -a /tmp/changed-files pnpm exec oxfmt --write - - - name: Commit fixes - uses: stefanzweifel/git-auto-commit-action@v7 - with: - commit_message: "style: auto-fix lint and format" - - lint: - runs-on: ubuntu-latest - timeout-minutes: 10 - steps: - - uses: actions/checkout@v7 - with: - fetch-depth: 0 - - - name: Setup - uses: ./tooling/github/setup - - - name: Copy env - shell: bash - run: cp .env.example .env - - - name: Lint - run: pnpm turbo lint --affected && pnpm lint:ws - - format: - runs-on: ubuntu-latest - timeout-minutes: 10 - steps: - - uses: actions/checkout@v7 - with: - fetch-depth: 0 - - - name: Setup - uses: ./tooling/github/setup - - - name: Format - run: pnpm turbo format --affected --continue - - typecheck: - runs-on: ubuntu-latest - timeout-minutes: 10 - steps: - - uses: actions/checkout@v7 - with: - fetch-depth: 0 - - - name: Setup - uses: ./tooling/github/setup - - - name: Typecheck - run: pnpm turbo typecheck --affected - - test: - runs-on: ubuntu-latest - timeout-minutes: 10 - steps: - - uses: actions/checkout@v7 - - - name: Setup - uses: ./tooling/github/setup - - - name: Test - run: pnpm test - - - name: "Report Coverage" - # Set if: always() to also generate the report if tests are failing - # Only works if you set `reportOnFailure: true` in your vite config as specified above - if: always() - uses: davelosert/vitest-coverage-report-action@v2 - - e2e: - runs-on: ubuntu-latest - timeout-minutes: 15 - steps: - - uses: actions/checkout@v7 - - name: Setup - uses: ./tooling/github/setup - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v4 - - name: Build docker image - id: build-docker-image - uses: docker/build-push-action@v7 - with: - platforms: linux/amd64 - context: . - push: false - load: true - tags: homarr-e2e - network: host - cache-from: type=gha - cache-to: type=gha,mode=max - env: - SKIP_ENV_VALIDATION: true - - name: Install playwright browsers - run: pnpm exec playwright install chromium - - name: Run E2E Tests - shell: bash - run: pnpm test:e2e - - build: - runs-on: ubuntu-latest - timeout-minutes: 10 - steps: - - uses: actions/checkout@v7 - with: - fetch-depth: 0 - - name: Setup - uses: ./tooling/github/setup - - name: Copy env - shell: bash - run: cp .env.example .env - - name: Build - run: pnpm turbo build --affected diff --git a/.github/workflows/deployment-docker-image.yml b/.github/workflows/deployment-docker-image.yml index 3ab3a758e..9ea0c2a09 100644 --- a/.github/workflows/deployment-docker-image.yml +++ b/.github/workflows/deployment-docker-image.yml @@ -148,6 +148,11 @@ jobs: platforms: linux/amd64 labels: ${{ steps.meta.outputs.labels }} outputs: type=image,"name=${{ env.GHCR_REPO }}",push-by-digest=true,name-canonical=true,push=true + secrets: | + TURBO_API=${{ secrets.TURBO_API }} + TURBO_TEAM=${{ secrets.TURBO_TEAM }} + TURBO_TOKEN=${{ secrets.TURBO_TOKEN }} + TURBO_REMOTE_CACHE_SIGNATURE_KEY=${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }} env: SKIP_ENV_VALIDATION: true build-arm64: @@ -187,6 +192,11 @@ jobs: platforms: linux/arm64 labels: ${{ steps.meta.outputs.labels }} outputs: type=image,"name=${{ env.GHCR_REPO }}",push-by-digest=true,name-canonical=true,push=true + secrets: | + TURBO_API=${{ secrets.TURBO_API }} + TURBO_TEAM=${{ secrets.TURBO_TEAM }} + TURBO_TOKEN=${{ secrets.TURBO_TOKEN }} + TURBO_REMOTE_CACHE_SIGNATURE_KEY=${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }} env: SKIP_ENV_VALIDATION: true extract-asset-amd64: diff --git a/.github/workflows/deployment-docs.yml b/.github/workflows/deployment-docs.yml new file mode 100644 index 000000000..eeb53d101 --- /dev/null +++ b/.github/workflows/deployment-docs.yml @@ -0,0 +1,56 @@ +name: "[Deployment] Documentation" + +on: + push: + branches: ["dev"] + paths: + - "apps/docs/**" + - "packages/definitions/src/docs/**" + - ".github/workflows/deployment-docs.yml" + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: "pages" + cancel-in-progress: false + +env: + SKIP_ENV_VALIDATION: true + TURBO_API: ${{ secrets.TURBO_API }} + TURBO_TEAM: ${{ secrets.TURBO_TEAM }} + TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} + TURBO_REMOTE_CACHE_SIGNATURE_KEY: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }} + +jobs: + build: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v7 + - name: Setup + uses: ./tooling/github/setup + - name: Build docs + run: pnpm turbo build --filter=@homarr/docs + env: + CI: "true" + - name: Setup Pages + uses: actions/configure-pages@v6 + - name: Upload pages artifact + uses: actions/upload-pages-artifact@v5 + with: + path: "apps/docs/build" + + deploy: + runs-on: ubuntu-latest + needs: build + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v5 diff --git a/.github/workflows/deployment-workshop.yml b/.github/workflows/deployment-workshop.yml deleted file mode 100644 index 621c4609c..000000000 --- a/.github/workflows/deployment-workshop.yml +++ /dev/null @@ -1,47 +0,0 @@ -name: "[Deployment] Workshop" - -on: - push: - branches: ["dev"] - paths: - - "apps/docs/**" - - "apps/workshop/**" - workflow_dispatch: - -permissions: - contents: read - packages: write - -concurrency: - group: workshop-deployment - cancel-in-progress: true - -jobs: - deploy: - name: Build, test, and publish - runs-on: ubuntu-latest - timeout-minutes: 25 - env: - LOCAL_IMAGE: homarr/workshop:ci - REGISTRY_IMAGE: ghcr.io/homarr-labs/workshop - steps: - - name: Check out repository - uses: actions/checkout@v7 - - - name: Build combined Workshop image - run: docker build --file apps/workshop/Dockerfile --tag "$LOCAL_IMAGE" . - - - name: Smoke-test static docs and PocketBase - run: bash apps/workshop/scripts/smoke-test.sh "$LOCAL_IMAGE" - - - name: Log in to GitHub Container Registry - if: ${{ env.ACT != 'true' }} - run: echo "${{ github.token }}" | docker login ghcr.io --username "${{ github.actor }}" --password-stdin - - - name: Publish immutable and latest tags - if: ${{ env.ACT != 'true' }} - run: | - docker tag "$LOCAL_IMAGE" "$REGISTRY_IMAGE:sha-${GITHUB_SHA}" - docker tag "$LOCAL_IMAGE" "$REGISTRY_IMAGE:latest" - docker push "$REGISTRY_IMAGE:sha-${GITHUB_SHA}" - docker push "$REGISTRY_IMAGE:latest" diff --git a/AGENTS.md b/AGENTS.md index 265f7fe07..be1afe9fb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -81,5 +81,3 @@ When modifying user-facing code, you MUST also update the corresponding document - UI: Mantine (not Tailwind) — Tailwind is only used in docs app - Icons: @tabler/icons-react - Docs app can import from `@homarr/definitions` for shared types -- Use `dayjs` for date/time formatting or comparisons; do not hand-roll relative time or date math. -- Extract shared helpers/components when the same logic appears in multiple workshop/store files. diff --git a/Dockerfile b/Dockerfile index e7b5b679c..8f4c5e49a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,24 +1,43 @@ +# syntax=docker/dockerfile:1.25 + FROM node:24.18.0-alpine AS base FROM base AS builder -RUN apk add --no-cache libc6-compat -RUN apk update - -# Set working directory WORKDIR /app -RUN apk add --no-cache libc6-compat curl bash -RUN apk update +RUN apk add --no-cache libc6-compat curl bash && apk update + +RUN corepack enable pnpm +COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./ +COPY patches ./patches +# Workaround for pnpm/pnpm#5268: pnpm fetch crashes when patchedDependencies +# are configured with nodeLinker: hoisted. The applyPatchToDir function tries +# to chdir into node_modules/ which doesn't exist during fetch (only the +# content-addressable store is populated). By temporarily switching to the +# isolated linker, patches apply inside the virtual store (node_modules/.pnpm/...) +# which IS created by pnpm fetch. The original hoisted linker is restored +# before the install step so the final node_modules layout stays flat. +RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store \ + pnpm config set store-dir /pnpm/store && \ + sed -i 's/nodeLinker: hoisted/nodeLinker: isolated/' pnpm-workspace.yaml && \ + pnpm fetch && \ + sed -i 's/nodeLinker: isolated/nodeLinker: hoisted/' pnpm-workspace.yaml + COPY . . +# Follow the pnpm fetch pattern from https://pnpm.io/cli/fetch +# --frozen-lockfile is omitted as recommended by the pnpm fetch docs +RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store \ + pnpm install --recursive -RUN corepack enable pnpm && pnpm install --recursive --frozen-lockfile - -# Copy static data as it is not part of the build -COPY static-data ./static-data ARG SKIP_ENV_VALIDATION='true' ARG CI='true' ARG DISABLE_REDIS_LOGS='true' +ARG TARGETPLATFORM -RUN corepack enable pnpm && pnpm build +RUN --mount=type=secret,id=TURBO_API,env=TURBO_API \ + --mount=type=secret,id=TURBO_TEAM,env=TURBO_TEAM \ + --mount=type=secret,id=TURBO_TOKEN,env=TURBO_TOKEN \ + --mount=type=secret,id=TURBO_REMOTE_CACHE_SIGNATURE_KEY,env=TURBO_REMOTE_CACHE_SIGNATURE_KEY \ + TURBO_PLATFORM="${TARGETPLATFORM:-linux/amd64}" pnpm build FROM base AS runner WORKDIR /app diff --git a/PRODUCT.md b/PRODUCT.md deleted file mode 100644 index 629b0abb5..000000000 --- a/PRODUCT.md +++ /dev/null @@ -1,35 +0,0 @@ -# Product - -## Register - -GitHub OAuth is used for workshop accounts so community members can submit, vote on, comment on, and report shared widgets or CSS themes without creating a separate Homarr-specific identity. - -## Users - -Self-hosting enthusiasts managing homelab services. Technical users comfortable with Docker, APIs, and configuration files. They use Homarr as a personal dashboard to monitor and control 50+ services (media servers, DNS, NAS, download clients). Context: desktop browser, often on a local network, task-focused. - -## Product Purpose - -Homarr is a self-hosted dashboard for homelab management. It aggregates service status, media libraries, system stats, and administrative controls into a single drag-and-drop interface with 39+ widgets. The marketplace extension lets users share and install custom widgets and CSS themes created by the community. - -## Brand Personality - -Practical, capable, community-driven. - -## Anti-references - -- Generic SaaS landing pages with gradient hero sections and metric grids -- Overly corporate enterprise dashboards (ServiceNow, Salesforce) -- Cluttered "hacker" UIs with no visual hierarchy - -## Design Principles - -1. **Tool disappears into the task.** The interface should feel invisible; users came to manage their homelab, not admire the dashboard. -2. **Density when it earns it.** Show useful information compactly; don't spread thin content across oversized cards. -3. **Community first.** Sharing, voting, and installing should feel effortless and trustworthy. -4. **Consistency across surfaces.** The marketplace in the docs app should feel like it belongs to the same product as the main Next.js app. -5. **Respect the user's time.** Fast loads, instant feedback, no choreographed animations. - -## Accessibility & Inclusion - -WCAG 2.1 AA. Support for reduced motion. Sufficient contrast ratios (4.5:1 body text, 3:1 large text). Keyboard navigable. diff --git a/apps/docs/components.json b/apps/docs/components.json deleted file mode 100644 index 15e244db4..000000000 --- a/apps/docs/components.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "$schema": "https://ui.shadcn.com/schema.json", - "style": "base-nova", - "rsc": false, - "tsx": true, - "tailwind": { - "config": "", - "css": "src/css/custom.css", - "baseColor": "mist", - "cssVariables": true, - "prefix": "" - }, - "aliases": { - "components": "@/components", - "utils": "@/lib/utils", - "ui": "@/components/ui", - "lib": "@/lib", - "hooks": "@/hooks" - }, - "iconLibrary": "tabler", - "rtl": false, - "menuColor": "default", - "menuAccent": "subtle" -} diff --git a/apps/docs/docs/management/workshop/index.mdx b/apps/docs/docs/management/workshop/index.mdx deleted file mode 100644 index c187e2f99..000000000 --- a/apps/docs/docs/management/workshop/index.mdx +++ /dev/null @@ -1,59 +0,0 @@ ---- -tags: - - Workshop - - Custom Widgets - - Custom CSS ---- - -# Community Workshop - -The [Community Workshop](pathname:///workshop) is where Homarr users share and discover -**custom CSS themes** and **custom widgets**. You can browse submissions, upvote the ones -you like, report abuse, and install entries directly into your own Homarr instance. - -:::warning - -The workshop contains community-made content. Widgets may contain unwanted or malicious content and it is your duty to check & verify safety of the content. While we try to remove malicious content as best as we can, we take no responsibility. Always review the widget JSON and all CSS before installing it on your instance and take frequent backups as prevention measures. - -::: - -## Browsing and voting - -- Filter by **Widgets** or **CSS**, and sort by **Top rated** or **Newest**. -- Sign in with **GitHub** to upvote/downvote, submit your own creations, or report a submission. -- Each submission shows its title, description, author, version, an optional screenshot, and the full content. - -## Submitting - -1. Sign in with GitHub. -2. Click **Submit** and choose the type: - - **Custom widget** — paste the JSON exported from **Manage → Custom Widgets → Export**. - It must match the `homarr-custom-widget-v2` schema (validated before submission). - - **Custom CSS** — paste your stylesheet (max 16384 characters). -3. Add a title, description, optional screenshot, and changelog, then submit. - -Submissions are published immediately. Reported entries are reviewed and may be removed. - -## Submission guidelines - -Submissions must be useful to Homarr users and safe to install. Do not submit content that is -malicious, deceptive, illegal, hateful, sexually explicit, gore, gambling-focused, spam, or content -that primarily promotes unrelated products or services. - -## Installing into Homarr - -You can install workshop entries without leaving Homarr: - -- **Custom widgets** — go to **Manage → Custom Widgets**, click **Install from Workshop**, pick an - entry, and it is imported (secrets such as API keys must be re-entered afterwards). -- **Custom CSS** — open a board's **Settings → Custom CSS**, click **Install from Workshop**, and - pick a theme to load it into the editor before saving. - -Alternatively, use **Copy** (or **Download** for widgets) on any submission and import it -manually. - -## Workshop deployment - -Workshop is powered by [PocketBase](https://pocketbase.io). The production image compiles the current Docusaurus site and serves it together with the API from one origin. The deployment lives in `apps/workshop/` with a Dockerfile, Compose configuration, version-controlled migrations, hooks, a smoke test, and ACT-local workflow instructions. - -Homarr instances use `NEXT_PUBLIC_WORKSHOP_API_URL` to reach the central API and `NEXT_PUBLIC_WORKSHOP_URL` for links to the public page. Docusaurus uses `WORKSHOP_URL` only when its development server runs separately; the combined production image always uses its own origin. diff --git a/apps/docs/docusaurus.config.ts b/apps/docs/docusaurus.config.ts index 62384c741..0edf8a8f3 100644 --- a/apps/docs/docusaurus.config.ts +++ b/apps/docs/docusaurus.config.ts @@ -20,11 +20,6 @@ const config: Config = { onBrokenAnchors: "throw", onDuplicateRoutes: "throw", - customFields: { - // Empty means same-origin, which is the production Workshop image shape. - workshopUrl: process.env.WORKSHOP_URL ?? "", - }, - future: { v4: { removeLegacyPostBuildHeadAttribute: true, @@ -129,11 +124,6 @@ const config: Config = { position: "left", to: "/blog", }, - { - label: "Workshop", - position: "left", - to: "/workshop", - }, { label: "About us", position: "left", @@ -288,12 +278,7 @@ const config: Config = { return { name: "resolve-homarr-packages", configureWebpack() { - return { - resolve: { - symlinks: false, - alias: { "@": require("path").resolve(__dirname, "src") }, - }, - }; + return { resolve: { symlinks: false } }; }, }; }, @@ -324,18 +309,6 @@ const config: Config = { }, }; }, - function workshopDetailRoutePlugin() { - return { - name: "workshop-detail-route", - async contentLoaded({ actions }) { - actions.addRoute({ - path: "/workshop/:id", - component: "@site/src/components/workshop/DetailPage", - exact: true, - }); - }, - }; - }, "@signalwire/docusaurus-plugin-llms-txt", async function tailwindCssPlugin() { return { diff --git a/apps/docs/package.json b/apps/docs/package.json index b5b359b80..a627a4d37 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -21,7 +21,6 @@ "verify:search": "node ./scripts/verify-algolia-search.mjs" }, "dependencies": { - "@base-ui/react": "catalog:", "@docusaurus/core": "^3.10.1", "@docusaurus/faster": "^3.10.1", "@docusaurus/plugin-content-docs": "^3.10.1", @@ -34,22 +33,18 @@ "@signalwire/docusaurus-plugin-llms-txt": "^1.2.2", "@tabler/icons-react": "catalog:", "@tailwindcss/postcss": "^4.3.2", - "class-variance-authority": "catalog:", "clsx": "catalog:", "dayjs": "catalog:", "docusaurus-plugin-image-zoom": "^3.0.1", "leader-line-new": "^1.1.9", - "pocketbase": "catalog:", "postcss": "^8.5.16", "posthog-docusaurus": "^2.0.5", "prism-react-renderer": "^2.4.1", "random-word-slugs": "^0.1.7", "react": "catalog:", "react-dom": "catalog:", - "swiper": "^14.0.2", - "tailwind-merge": "catalog:", - "tailwindcss": "^4.3.2", - "zod": "catalog:" + "swiper": "^14.0.5", + "tailwindcss": "^4.3.2" }, "devDependencies": { "@docusaurus/module-type-aliases": "^3.10.1", @@ -60,7 +55,8 @@ "@types/react": "catalog:", "@types/react-dom": "catalog:", "cheerio": "^1.2.0", - "typescript": "catalog:" + "typescript": "catalog:", + "zod": "catalog:" }, "peerDependencies": { "@docusaurus/theme-common": "^3.10.1", diff --git a/apps/docs/src/components/ui/badge.tsx b/apps/docs/src/components/ui/badge.tsx index e1997af40..5776a59c3 100644 --- a/apps/docs/src/components/ui/badge.tsx +++ b/apps/docs/src/components/ui/badge.tsx @@ -1,49 +1,13 @@ -import { mergeProps } from "@base-ui/react/merge-props"; -import { useRender } from "@base-ui/react/use-render"; -import { cva, type VariantProps } from "class-variance-authority"; +import { ReactNode } from "react"; -import { cn } from "@/lib/utils"; - -const badgeVariants = cva( - "group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!", - { - variants: { - variant: { - default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80", - secondary: "bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80", - destructive: - "bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20", - outline: "border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground", - ghost: "hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50", - link: "text-primary underline-offset-4 hover:underline", - }, - }, - defaultVariants: { - variant: "default", - }, - }, -); - -function Badge({ - className, - variant = "default", - render, - ...props -}: useRender.ComponentProps<"span"> & VariantProps) { - return useRender({ - defaultTagName: "span", - props: mergeProps<"span">( - { - className: cn(badgeVariants({ variant }), className), - }, - props, - ), - render, - state: { - slot: "badge", - variant, - }, - }); +interface BadgeProps { + children: ReactNode; } -export { Badge, badgeVariants }; +export const Badge = ({ children }: BadgeProps) => { + return ( + + {children} + + ); +}; diff --git a/apps/docs/src/components/ui/button.tsx b/apps/docs/src/components/ui/button.tsx deleted file mode 100644 index 914b2251f..000000000 --- a/apps/docs/src/components/ui/button.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import { Button as ButtonPrimitive } from "@base-ui/react/button"; -import { cva, type VariantProps } from "class-variance-authority"; - -import { cn } from "@/lib/utils"; - -const buttonVariants = cva( - "group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", - { - variants: { - variant: { - default: "bg-primary text-primary-foreground hover:bg-primary/80", - outline: - "border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50", - secondary: - "bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground", - ghost: - "hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50", - destructive: - "bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40", - link: "text-primary underline-offset-4 hover:underline", - }, - size: { - default: "h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2", - xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3", - sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5", - lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2", - icon: "size-8", - "icon-xs": - "size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3", - "icon-sm": "size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg", - "icon-lg": "size-9", - }, - }, - defaultVariants: { - variant: "default", - size: "default", - }, - }, -); - -function Button({ - className, - variant = "default", - size = "default", - ...props -}: ButtonPrimitive.Props & VariantProps) { - return ; -} - -export { Button, buttonVariants }; diff --git a/apps/docs/src/components/ui/card.tsx b/apps/docs/src/components/ui/card.tsx deleted file mode 100644 index 9ac0597c8..000000000 --- a/apps/docs/src/components/ui/card.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import * as React from "react"; - -import { cn } from "@/lib/utils"; - -function Card({ className, size = "default", ...props }: React.ComponentProps<"div"> & { size?: "default" | "sm" }) { - return ( -
img:first-child]:pt-0 data-[size=sm]:[--card-spacing:--spacing(3)] data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl", - className, - )} - {...props} - /> - ); -} - -function CardHeader({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ); -} - -function CardTitle({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ); -} - -function CardDescription({ className, ...props }: React.ComponentProps<"div">) { - return
; -} - -function CardAction({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ); -} - -function CardContent({ className, ...props }: React.ComponentProps<"div">) { - return
; -} - -function CardFooter({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ); -} - -export { Card, CardHeader, CardFooter, CardTitle, CardAction, CardDescription, CardContent }; diff --git a/apps/docs/src/components/ui/dialog.tsx b/apps/docs/src/components/ui/dialog.tsx deleted file mode 100644 index 1794a2773..000000000 --- a/apps/docs/src/components/ui/dialog.tsx +++ /dev/null @@ -1,134 +0,0 @@ -"use client"; - -import * as React from "react"; -import { Dialog as DialogPrimitive } from "@base-ui/react/dialog"; - -import { cn } from "@/lib/utils"; -import { Button } from "@/components/ui/button"; -import { IconX } from "@tabler/icons-react"; - -function Dialog({ ...props }: DialogPrimitive.Root.Props) { - return ; -} - -function DialogTrigger({ ...props }: DialogPrimitive.Trigger.Props) { - return ; -} - -function DialogPortal({ ...props }: DialogPrimitive.Portal.Props) { - return ; -} - -function DialogClose({ ...props }: DialogPrimitive.Close.Props) { - return ; -} - -function DialogOverlay({ className, ...props }: DialogPrimitive.Backdrop.Props) { - return ( - - ); -} - -function DialogContent({ - className, - children, - showCloseButton = true, - ...props -}: DialogPrimitive.Popup.Props & { - showCloseButton?: boolean; -}) { - return ( - - - - {children} - {showCloseButton && ( - } - > - - Close - - )} - - - ); -} - -function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { - return
; -} - -function DialogFooter({ - className, - showCloseButton = false, - children, - ...props -}: React.ComponentProps<"div"> & { - showCloseButton?: boolean; -}) { - return ( -
- {children} - {showCloseButton && }>Close} -
- ); -} - -function DialogTitle({ className, ...props }: DialogPrimitive.Title.Props) { - return ( - - ); -} - -function DialogDescription({ className, ...props }: DialogPrimitive.Description.Props) { - return ( - - ); -} - -export { - Dialog, - DialogClose, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogOverlay, - DialogPortal, - DialogTitle, - DialogTrigger, -}; diff --git a/apps/docs/src/components/ui/header.tsx b/apps/docs/src/components/ui/header.tsx index ef5907cbb..5e99e0cff 100644 --- a/apps/docs/src/components/ui/header.tsx +++ b/apps/docs/src/components/ui/header.tsx @@ -1,4 +1,4 @@ -import type { ReactNode } from "react"; +import { ReactNode } from "react"; import { Badge } from "../ui/badge"; interface DocsHeaderProps { @@ -18,7 +18,7 @@ export const DocsHeader = (props: DocsHeaderProps) => {

{props.title}

{props.categories.map((category) => ( - {category} + {category} ))}
diff --git a/apps/docs/src/components/ui/input.tsx b/apps/docs/src/components/ui/input.tsx deleted file mode 100644 index abb87eb98..000000000 --- a/apps/docs/src/components/ui/input.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import * as React from "react"; -import { Input as InputPrimitive } from "@base-ui/react/input"; - -import { cn } from "@/lib/utils"; - -function Input({ className, type, ...props }: React.ComponentProps<"input">) { - return ( - - ); -} - -export { Input }; diff --git a/apps/docs/src/components/ui/select.tsx b/apps/docs/src/components/ui/select.tsx deleted file mode 100644 index fbd671c7e..000000000 --- a/apps/docs/src/components/ui/select.tsx +++ /dev/null @@ -1,167 +0,0 @@ -"use client"; - -import * as React from "react"; -import { Select as SelectPrimitive } from "@base-ui/react/select"; - -import { cn } from "@/lib/utils"; -import { IconSelector, IconCheck, IconChevronUp, IconChevronDown } from "@tabler/icons-react"; - -const Select = SelectPrimitive.Root; - -function SelectGroup({ className, ...props }: SelectPrimitive.Group.Props) { - return ; -} - -function SelectValue({ className, ...props }: SelectPrimitive.Value.Props) { - return ( - - ); -} - -function SelectTrigger({ - className, - size = "default", - children, - ...props -}: SelectPrimitive.Trigger.Props & { - size?: "sm" | "default"; -}) { - return ( - - {children} - } /> - - ); -} - -function SelectContent({ - className, - children, - side = "bottom", - sideOffset = 4, - align = "center", - alignOffset = 0, - alignItemWithTrigger = true, - ...props -}: SelectPrimitive.Popup.Props & - Pick) { - return ( - - - - - {children} - - - - - ); -} - -function SelectLabel({ className, ...props }: SelectPrimitive.GroupLabel.Props) { - return ( - - ); -} - -function SelectItem({ className, children, ...props }: SelectPrimitive.Item.Props) { - return ( - - - {children} - - } - > - - - - ); -} - -function SelectSeparator({ className, ...props }: SelectPrimitive.Separator.Props) { - return ( - - ); -} - -function SelectScrollUpButton({ className, ...props }: React.ComponentProps) { - return ( - - - - ); -} - -function SelectScrollDownButton({ className, ...props }: React.ComponentProps) { - return ( - - - - ); -} - -export { - Select, - SelectContent, - SelectGroup, - SelectItem, - SelectLabel, - SelectScrollDownButton, - SelectScrollUpButton, - SelectSeparator, - SelectTrigger, - SelectValue, -}; diff --git a/apps/docs/src/components/ui/separator.tsx b/apps/docs/src/components/ui/separator.tsx deleted file mode 100644 index e99046a0d..000000000 --- a/apps/docs/src/components/ui/separator.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { Separator as SeparatorPrimitive } from "@base-ui/react/separator"; - -import { cn } from "@/lib/utils"; - -function Separator({ className, orientation = "horizontal", ...props }: SeparatorPrimitive.Props) { - return ( - - ); -} - -export { Separator }; diff --git a/apps/docs/src/components/ui/textarea.tsx b/apps/docs/src/components/ui/textarea.tsx deleted file mode 100644 index d102c701b..000000000 --- a/apps/docs/src/components/ui/textarea.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import * as React from "react"; - -import { cn } from "@/lib/utils"; - -function Textarea({ className, ...props }: React.ComponentProps<"textarea">) { - return ( -