From f4e3c3b0ca4dd22c0c20756dcb641263cc166133 Mon Sep 17 00:00:00 2001 From: Stan Lo Date: Tue, 1 Sep 2026 11:33:55 +0100 Subject: [PATCH 1/3] Build PR previews without maintainer approval (#1806) Fork previews currently pause each fork update in `fork-preview-protection` until a maintainer approves the environment deployment. This creates repeated approval requests, delays contributor feedback, and fills the Actions UI with waiting jobs. **This change removes the preview-specific maintainer approval step.** Every PR builds RDoc in a read-only workflow that has no secrets. A trusted `workflow_run` matches the `_site` artifact to the current PR head and rejects non-static Pages controls. ```mermaid sequenceDiagram actor Author as PR author participant PR as Pull request participant Build as Build PR Preview
Untrusted, no secrets participant Artifact as GitHub artifact store participant Deploy as Deploy PR Preview
Trusted participant Cloudflare as Cloudflare Pages Author->>PR: Open, update, or reopen the PR PR->>Build: Start pull_request workflow Build->>Build: Checkout PR code at the exact head SHA Build->>Build: Setup Ruby Build->>Build: Build site into _site alt The build produces an artifact Build->>Artifact: Upload preview site for one day else The build fails before upload Note over Build,Artifact: No preview artifact is available end Build-->>Deploy: workflow_run completed, success or failure Deploy->>Deploy: Resolve current pull request and artifact Deploy->>PR: Match one open PR to the exact head SHA Deploy->>Artifact: Match one artifact from this exact run alt The PR, SHA, or artifact is not current and valid Deploy--xDeploy: Stop before Cloudflare secrets else The candidate is current Artifact-->>Deploy: Download preview site Deploy->>Deploy: Validate preview site as safe static files Deploy->>PR: Confirm pull request head again alt The site is invalid or the PR head changed Deploy--xDeploy: Stop before deployment else The site is safe and the PR is current Deploy->>Deploy: Prepare trusted Wrangler directory Note over Deploy,Cloudflare: Cloudflare secrets are supplied only for this call Deploy->>Cloudflare: Deploy to the PR-number-preview branch Cloudflare-->>Deploy: Return an HTTPS pages.dev URL Deploy->>PR: Update preview comment after one final head and URL check Note over PR: Reuse one marked comment with the preview URL and commit SHA end end ``` Contributors now receive an updated preview after each commit that produces a preview artifact. Per-PR concurrency cancels older work, and one bot comment points to the latest successful preview. Maintainers no longer open individual environment deployments or receive repeated preview approval requests. The trusted workflow exposes the Cloudflare token only to the deployment step. It does not download the PR repository or run PR code. GitHub's separate public-fork policy can still hold workflows from contributors who are new to GitHub. Existing PRs can retain the old MATZBOT comment once. Later deployments update only the new GitHub Actions comment. --- .github/workflows/cloudflare-preview.yml | 401 +++++++++++++++++++--- .github/workflows/fork-preview-deploy.yml | 66 ---- .github/workflows/pr-preview-check.yml | 76 ++-- 3 files changed, 395 insertions(+), 148 deletions(-) delete mode 100644 .github/workflows/fork-preview-deploy.yml diff --git a/.github/workflows/cloudflare-preview.yml b/.github/workflows/cloudflare-preview.yml index cbe0695065..d9eae21eae 100644 --- a/.github/workflows/cloudflare-preview.yml +++ b/.github/workflows/cloudflare-preview.yml @@ -1,77 +1,398 @@ -name: Build and Deploy Cloudflare Preview +name: Deploy PR Preview on: - repository_dispatch: - types: [pr-preview-deploy] + workflow_run: + workflows: ["Build PR Preview"] + types: [completed] -permissions: - pull-requests: write # To allow commenting on the PR +permissions: {} jobs: - build-deploy-and-comment: - name: Build, Deploy, and Comment + resolve: + name: Resolve Preview + if: >- + github.repository == 'ruby/rdoc' && + github.event.workflow_run.event == 'pull_request' runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + actions: read + pull-requests: read + outputs: + current: ${{ steps.resolve.outputs.current }} + number: ${{ steps.resolve.outputs.number }} + head_sha: ${{ steps.resolve.outputs.head_sha }} steps: - - name: Checkout PR Code - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + # A forked workflow_run can omit pull request details, so the artifact + # must match the exact open pull request and commit. + - name: Resolve current pull request and artifact + id: resolve + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: - repository: ${{ github.event.client_payload.pr_checkout_repository }} - ref: ${{ github.event.client_payload.pr_head_sha }} + script: | + const run = context.payload.workflow_run; + const expectedRepository = 'ruby/rdoc'; + const expectedBase = 'master'; + const artifactName = 'pr-preview-site'; + const maximumArchiveBytes = 500 * 1024 * 1024; + + if (!['success', 'failure'].includes(run.conclusion)) { + core.setOutput('current', 'false'); + core.notice(`Skipped a preview build with conclusion: ${run.conclusion}.`); + return; + } + + if (!/^[0-9a-f]{40}$/.test(run.head_sha)) { + core.setFailed('The preview build supplied an invalid head SHA.'); + return; + } + + const headRepository = run.head_repository?.full_name; + const headOwner = run.head_repository?.owner?.login; + const headBranch = run.head_branch; + if (!headRepository || !headOwner || !headBranch) { + core.setFailed('The preview build did not identify its head repository and branch.'); + return; + } + + let candidateNumbers = new Set( + (run.pull_requests || []).map(pull => pull.number) + ); + + // workflow_run.pull_requests can be empty for fork pull requests. + if (candidateNumbers.size === 0) { + const branchPulls = await github.paginate(github.rest.pulls.list, { + owner: context.repo.owner, + repo: context.repo.repo, + state: 'open', + head: `${headOwner}:${headBranch}`, + base: expectedBase, + per_page: 100, + }); + candidateNumbers = new Set( + branchPulls + .filter(pull => pull.head.sha === run.head_sha) + .map(pull => pull.number) + ); + } + + if (candidateNumbers.size === 0) { + core.setOutput('current', 'false'); + core.notice('No open pull request uses this preview build.'); + return; + } + + const candidates = []; + for (const number of candidateNumbers) { + const { data: pull } = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: number, + }); + candidates.push(pull); + } + + const matches = candidates.filter(pull => + pull.base.repo.full_name === expectedRepository && + pull.base.ref === expectedBase && + pull.head.repo?.full_name === headRepository && + pull.head.ref === headBranch + ); + + if (matches.length !== 1) { + core.setFailed(`Expected one pull request for this preview build, found ${matches.length}.`); + return; + } + + const pull = matches[0]; + const current = pull.state === 'open' && pull.head.sha === run.head_sha; + core.setOutput('current', current.toString()); + core.setOutput('number', pull.number.toString()); + core.setOutput('head_sha', run.head_sha); + + if (!current) { + core.notice('The pull request changed or closed after this preview build.'); + return; + } + + const artifacts = await github.paginate( + github.rest.actions.listWorkflowRunArtifacts, + { + owner: context.repo.owner, + repo: context.repo.repo, + run_id: run.id, + name: artifactName, + per_page: 100, + } + ); + const matchingArtifacts = artifacts.filter(artifact => + artifact.name === artifactName && !artifact.expired + ); + + if (matchingArtifacts.length === 0 && run.conclusion === 'failure') { + core.setOutput('current', 'false'); + core.notice('The failed preview build did not publish an artifact.'); + return; + } - - name: Setup Ruby - uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # v1.321.0 + if (matchingArtifacts.length !== 1) { + core.setFailed(`Expected one ${artifactName} artifact, found ${matchingArtifacts.length}.`); + return; + } + + const artifact = matchingArtifacts[0]; + if (artifact.size_in_bytes <= 0 || artifact.size_in_bytes > maximumArchiveBytes) { + core.setFailed(`The preview artifact archive has an invalid size: ${artifact.size_in_bytes} bytes.`); + } + + deploy: + name: Deploy Preview + needs: resolve + if: needs.resolve.outputs.current == 'true' + runs-on: ubuntu-latest + timeout-minutes: 15 + concurrency: + group: pr-preview-deploy-${{ needs.resolve.outputs.number }} + cancel-in-progress: true + permissions: + actions: read + pull-requests: write + steps: + # The workflow treats the artifact as untrusted and uses no source + # checkout, so pull request code cannot run on the trusted runner. + - name: Download preview site + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: pr-preview-site + path: ${{ runner.temp }}/pr-preview-site + github-token: ${{ secrets.GITHUB_TOKEN }} + run-id: ${{ github.event.workflow_run.id }} + + # Pages control files can change deployment behavior. This gate blocks + # them and oversized artifacts before the deployment step receives + # Cloudflare secrets. + - name: Validate preview site + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + SITE_ROOT: ${{ runner.temp }}/pr-preview-site with: - ruby-version: '3.4' - bundler-cache: true + script: | + const fs = require('node:fs/promises'); + const path = require('node:path'); + + const root = path.resolve(process.env.SITE_ROOT); + const maximumFiles = 20_000; + const maximumFileBytes = 25 * 1024 * 1024; + const maximumTotalBytes = 500 * 1024 * 1024; + const blockedRootEntries = new Set([ + '.assetsignore', + '_headers', + '_redirects', + '_routes.json', + '_worker.bundle', + '_worker.js', + 'functions', + 'wrangler.json', + 'wrangler.jsonc', + 'wrangler.toml', + ]); + + const rootMetadata = await fs.lstat(root); + if (!rootMetadata.isDirectory() || rootMetadata.isSymbolicLink()) { + throw new Error('The preview artifact root is not a regular directory.'); + } + + let fileCount = 0; + let totalBytes = 0; + + async function inspect(directory, relativeDirectory = '') { + const entries = await fs.readdir(directory, { withFileTypes: true }); + for (const entry of entries) { + const fullPath = path.join(directory, entry.name); + const relativePath = path.posix.join(relativeDirectory, entry.name); + const metadata = await fs.lstat(fullPath); + + if (metadata.isSymbolicLink()) { + throw new Error(`The preview artifact contains a symbolic link: ${relativePath}`); + } - - name: Build site - run: bundle exec rake rdoc + if (!relativeDirectory && blockedRootEntries.has(entry.name)) { + throw new Error(`The preview artifact contains a blocked Pages entry: ${relativePath}`); + } + if (metadata.isDirectory()) { + await inspect(fullPath, relativePath); + continue; + } + + if (!metadata.isFile()) { + throw new Error(`The preview artifact contains a special file: ${relativePath}`); + } + + fileCount += 1; + totalBytes += metadata.size; + if (fileCount > maximumFiles) { + throw new Error(`The preview artifact exceeds ${maximumFiles} files.`); + } + if (metadata.size > maximumFileBytes) { + throw new Error(`The preview artifact contains a file larger than ${maximumFileBytes} bytes: ${relativePath}`); + } + if (totalBytes > maximumTotalBytes) { + throw new Error(`The preview artifact exceeds ${maximumTotalBytes} bytes.`); + } + } + } + + await inspect(root); + if (fileCount === 0) throw new Error('The preview artifact is empty.'); + + const indexMetadata = await fs.lstat(path.join(root, 'index.html')); + if (!indexMetadata.isFile() || indexMetadata.isSymbolicLink()) { + throw new Error('The preview artifact does not contain a regular index.html file.'); + } + + core.info(`Accepted ${fileCount} static files (${totalBytes} bytes).`); + + # The pull request can change after artifact selection. A second head + # comparison blocks deployment of a stale commit. + - name: Confirm pull request head + id: current + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + PR_NUMBER: ${{ needs.resolve.outputs.number }} + EXPECTED_SHA: ${{ needs.resolve.outputs.head_sha }} + with: + script: | + const number = process.env.PR_NUMBER; + const expectedSha = process.env.EXPECTED_SHA; + if (!/^[1-9][0-9]*$/.test(number) || !/^[0-9a-f]{40}$/.test(expectedSha)) { + core.setFailed('The resolved pull request metadata is invalid.'); + return; + } + + const { data: pull } = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: Number(number), + }); + const current = + pull.state === 'open' && + pull.base.repo.full_name === 'ruby/rdoc' && + pull.base.ref === 'master' && + pull.head.sha === expectedSha; + + core.setOutput('current', current.toString()); + if (!current) core.notice('Skipped a stale preview deployment.'); + + # Pull request packages and configuration files cannot affect Wrangler + # because it runs from a separate trusted directory. + - name: Prepare trusted Wrangler directory + if: steps.current.outputs.current == 'true' + run: mkdir -p "$RUNNER_TEMP/trusted-preview-deploy" + + # Only this step receives Cloudflare secrets, after all safety checks. + # Each pull request uses its own preview branch. - name: Deploy to Cloudflare Pages + if: steps.current.outputs.current == 'true' id: deploy - uses: cloudflare/wrangler-action@v4 + uses: cloudflare/wrangler-action@ebbaa1584979971c8614a24965b4405ff95890e0 # v4.0.0 with: apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - command: pages deploy ./_site --project-name=rdoc --branch="${{ github.event.client_payload.pr_number }}-preview" + packageManager: npm + wranglerVersion: '4.81.0' + workingDirectory: ${{ runner.temp }}/trusted-preview-deploy + command: >- + pages deploy "${{ runner.temp }}/pr-preview-site" + --project-name=rdoc + --branch="${{ needs.resolve.outputs.number }}-preview" + --commit-hash="${{ needs.resolve.outputs.head_sha }}" - - name: Comment on PR with preview URL - uses: actions/github-script@v9 + # The workflow reuses one marked comment to avoid notification spam and + # show the exact commit for the preview. + - name: Update preview comment + if: steps.current.outputs.current == 'true' + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + PR_NUMBER: ${{ needs.resolve.outputs.number }} + EXPECTED_SHA: ${{ needs.resolve.outputs.head_sha }} + PREVIEW_ALIAS_URL: ${{ steps.deploy.outputs.pages-deployment-alias-url }} + PREVIEW_DEPLOYMENT_URL: ${{ steps.deploy.outputs.deployment-url }} with: - github-token: ${{ secrets.MATZBOT_GITHUB_TOKEN }} script: | - const prNumber = ${{ github.event.client_payload.pr_number }}; - const url = "${{ steps.deploy.outputs.deployment-url }}"; - const commentMarker = "๐Ÿš€ Preview deployment available at:"; - const commitSha = '${{ github.event.client_payload.pr_head_sha }}'; + const marker = ''; + const number = process.env.PR_NUMBER; + const expectedSha = process.env.EXPECTED_SHA; + const previewUrl = ( + process.env.PREVIEW_ALIAS_URL || process.env.PREVIEW_DEPLOYMENT_URL || '' + ).trim(); + + if (!/^[1-9][0-9]*$/.test(number) || !/^[0-9a-f]{40}$/.test(expectedSha)) { + core.setFailed('The preview comment metadata is invalid.'); + return; + } - const comments = await github.rest.issues.listComments({ - issue_number: prNumber, + let parsedUrl; + try { + parsedUrl = new URL(previewUrl); + } catch { + core.setFailed('Cloudflare did not return a valid preview URL.'); + return; + } + if (parsedUrl.protocol !== 'https:' || !parsedUrl.hostname.endsWith('.pages.dev')) { + core.setFailed('Cloudflare returned an unexpected preview URL.'); + return; + } + + const pullNumber = Number(number); + const { data: pull } = await github.rest.pulls.get({ owner: context.repo.owner, repo: context.repo.repo, - per_page: 100 + pull_number: pullNumber, }); + const current = + pull.state === 'open' && + pull.base.repo.full_name === 'ruby/rdoc' && + pull.base.ref === 'master' && + pull.head.sha === expectedSha; + if (!current) { + core.notice('Skipped the preview comment because the pull request changed.'); + return; + } - const existingComment = comments.data.find(comment => - comment.body.includes(commentMarker) - ); + const shortSha = expectedSha.slice(0, 7); + const body = [ + marker, + '### Documentation preview', + '', + `[View the preview](${parsedUrl.href})`, + '', + `Commit: \`${shortSha}\``, + ].join('\n'); - const commentBody = `${commentMarker} [${url}](${url}) (commit: ${commitSha})`; + const comments = await github.paginate(github.rest.issues.listComments, { + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pullNumber, + per_page: 100, + }); + const existing = comments.find(comment => + comment.user?.login === 'github-actions[bot]' && + comment.body?.startsWith(marker) + ); - if (existingComment) { + if (existing) { await github.rest.issues.updateComment({ - comment_id: existingComment.id, owner: context.repo.owner, repo: context.repo.repo, - body: commentBody + comment_id: existing.id, + body, }); - console.log("Updated existing preview comment"); } else { await github.rest.issues.createComment({ - issue_number: prNumber, owner: context.repo.owner, repo: context.repo.repo, - body: commentBody + issue_number: pullNumber, + body, }); - console.log("Created new preview comment"); } diff --git a/.github/workflows/fork-preview-deploy.yml b/.github/workflows/fork-preview-deploy.yml deleted file mode 100644 index c69804e2e0..0000000000 --- a/.github/workflows/fork-preview-deploy.yml +++ /dev/null @@ -1,66 +0,0 @@ -name: Dispatch Fork PR Preview Deployment - -on: - workflow_run: - workflows: ["PR Preview Check"] - types: [completed] - -jobs: - deploy-fork: - name: Trigger Preview Build and Deploy (Fork) - runs-on: ubuntu-latest - if: | - github.event.workflow_run.conclusion == 'success' && - github.event.workflow_run.event == 'pull_request' - steps: - - name: Download PR information - uses: actions/download-artifact@v8 - with: - name: pr - github-token: ${{ secrets.GITHUB_TOKEN }} - run-id: ${{ github.event.workflow_run.id }} - - - name: Read PR information and trigger deployment - uses: actions/github-script@v9 - with: - script: | - const fs = require('fs'); - - // Check if this was a fork PR by checking if approve-fork job ran - const jobs = await github.rest.actions.listJobsForWorkflowRun({ - owner: context.repo.owner, - repo: context.repo.repo, - run_id: context.payload.workflow_run.id, - }); - - const approveJob = jobs.data.jobs.find(job => job.name === 'Approve Fork PR'); - if (!approveJob || approveJob.conclusion !== 'success') { - core.setFailed('Not a fork PR approval workflow run'); - return; - } - - // Read PR information from artifacts - let prNumber, prHeadSha, prCheckoutRepo; - try { - prNumber = fs.readFileSync('./pr_number', 'utf8').trim(); - prHeadSha = fs.readFileSync('./pr_head_sha', 'utf8').trim(); - prCheckoutRepo = fs.readFileSync('./pr_checkout_repository', 'utf8').trim(); - } catch (error) { - core.setFailed(`Failed to read PR information: ${error.message}`); - return; - } - - console.log(`Deploying approved fork PR #${prNumber}`); - - // Trigger deployment via repository dispatch - await github.rest.repos.createDispatchEvent({ - owner: context.repo.owner, - repo: context.repo.repo, - event_type: 'pr-preview-deploy', - client_payload: { - pr_number: prNumber, - pr_head_sha: prHeadSha, - pr_checkout_repository: prCheckoutRepo, - is_fork: 'true' - } - }); diff --git a/.github/workflows/pr-preview-check.yml b/.github/workflows/pr-preview-check.yml index 74310d88b8..8ce96d0887 100644 --- a/.github/workflows/pr-preview-check.yml +++ b/.github/workflows/pr-preview-check.yml @@ -1,53 +1,45 @@ -name: PR Preview Check +name: Build PR Preview on: pull_request: + types: [opened, synchronize, reopened] + +permissions: + contents: read + +concurrency: + group: pr-preview-build-${{ github.event.pull_request.number }} + cancel-in-progress: true jobs: - # Deploy main repo PRs directly - deploy-for-main: - name: Trigger Preview Build and Deploy (Main Repo) + build: + name: Build Preview + if: github.repository == 'ruby/rdoc' runs-on: ubuntu-latest - if: github.event.pull_request.head.repo.fork == false - permissions: - contents: write + timeout-minutes: 20 steps: - - name: Trigger preview deployment - uses: actions/github-script@v9 + # This job executes untrusted pull request code. It must not receive secrets + # or a token with write access. + - name: Checkout PR code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - await github.rest.repos.createDispatchEvent({ - owner: context.repo.owner, - repo: context.repo.repo, - event_type: 'pr-preview-deploy', - client_payload: { - pr_number: '${{ github.event.pull_request.number }}', - pr_head_sha: '${{ github.event.pull_request.head.sha }}', - pr_checkout_repository: '${{ github.repository }}', - is_fork: 'false' - } - }); - console.log('Triggered main repo preview deployment'); + repository: ${{ github.event.pull_request.head.repo.full_name }} + ref: ${{ github.event.pull_request.head.sha }} + persist-credentials: false - # Approval gate for fork PRs - approve-for-fork: - name: Approve Fork PR - runs-on: ubuntu-latest - if: github.event.pull_request.head.repo.fork == true - environment: fork-preview-protection - steps: - - name: Save PR information - run: | - echo "Fork PR #${{ github.event.pull_request.number }} approved for preview deployment" - mkdir -p ./pr - echo "${{ github.event.pull_request.number }}" > ./pr/pr_number - echo "${{ github.event.pull_request.head.sha }}" > ./pr/pr_head_sha - echo "${{ github.event.pull_request.head.repo.full_name }}" > ./pr/pr_checkout_repository - - - name: Upload PR information - uses: actions/upload-artifact@v7 + - name: Setup Ruby + uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # v1.321.0 + with: + ruby-version: '3.4' + bundler-cache: true + + - name: Build site + run: bundle exec rake rdoc + + - name: Upload preview site + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: pr - path: pr/ + name: pr-preview-site + path: _site/ + if-no-files-found: error retention-days: 1 From dcf037978811d3593d1bfe2cb87a65a6785f6f29 Mon Sep 17 00:00:00 2001 From: st0012 Date: Mon, 31 Aug 2026 13:25:42 +0100 Subject: [PATCH 2/3] Use one responsive search surface in Aliki --- lib/rdoc/generator/template/aliki/DESIGN.md | 10 +- .../generator/template/aliki/_header.rhtml | 73 ++-- .../template/aliki/_sidebar_search.rhtml | 8 +- .../generator/template/aliki/css/rdoc.css | 376 +++++++++--------- lib/rdoc/generator/template/aliki/js/aliki.js | 144 +++---- 5 files changed, 297 insertions(+), 314 deletions(-) diff --git a/lib/rdoc/generator/template/aliki/DESIGN.md b/lib/rdoc/generator/template/aliki/DESIGN.md index b4ca6b945b..3703055f61 100644 --- a/lib/rdoc/generator/template/aliki/DESIGN.md +++ b/lib/rdoc/generator/template/aliki/DESIGN.md @@ -289,12 +289,14 @@ horizontally. ### Search +One search form and one result list serve both header layouts. CSS changes the presentation at `1023px`. + | Element | Value | |----------------------------------|-------------------------------------------------------------| -| Desktop field (`#search-field`) | full-width border-box, `space-2 space-4` pad, 1px border, `radius-md`, `base`; focus โ†’ accent border + `0 0 0 3px accent-subtle` | -| Desktop dropdown (`#search-results-desktop`) | absolute inside the search form, full-width border-box to match the field, `max-height: 60vh`, `radius-lg`, `shadow-lg`, `z-popover` (500) | -| Mobile modal (`.search-modal-content`) | centered card, `max-width: 600px`, `max-height: 80vh`, `radius-lg`, `shadow-xl` | -| Modal result item | `space-3 space-4` pad, `radius-md`, hover `background-secondary` | +| Shared field (`#search-field`) | full-width border-box, `space-2 space-4` pad, 1px border, `radius-md`, `base`. Focus uses an accent border + `0 0 0 3px accent-subtle` | +| Desktop results (`#search-results`) | absolute inside the search surface, full-width border-box, `max-height: 60vh`, `radius-lg`, `shadow-lg`, `z-popover` (500) | +| Compact surface (`.navbar-search.is-open .search-surface`) | centered overlay, `max-width: 600px`, `max-height: 80vh`, `radius-lg`, `shadow-xl` | +| Compact result item | `space-3 space-4` pad, `radius-md`, hover `background-secondary` | | Servlet field | pill `border-radius: 1.25rem`, leading ๐Ÿ” (`\1F50D`) glyph | | Result lines | `.search-match` `base` ยท `.search-namespace` `sm` secondary ยท `.search-snippet` `sm` tertiary | | Type badge (`.search-type-*`) | inline-block, `space-0 space-2` pad, `xs`, weight 500, `radius-sm`, colors per ยง2 | diff --git a/lib/rdoc/generator/template/aliki/_header.rhtml b/lib/rdoc/generator/template/aliki/_header.rhtml index 39dde3f06c..b7bcce114f 100644 --- a/lib/rdoc/generator/template/aliki/_header.rhtml +++ b/lib/rdoc/generator/template/aliki/_header.rhtml @@ -3,54 +3,39 @@ <%= h @options.title %> - - - - - - - - - +