diff --git a/.cursor/hooks/state/continual-learning-index.json b/.cursor/hooks/state/continual-learning-index.json index 34b2117..2571d5e 100644 --- a/.cursor/hooks/state/continual-learning-index.json +++ b/.cursor/hooks/state/continual-learning-index.json @@ -1,93 +1,157 @@ { "version": 1, "transcripts": { - "/Users/erikassiscardoso/.cursor/projects/Users-erikassiscardoso-Local-Sites-velora/agent-transcripts/18618295-bdf2-46e6-b561-c3a2775071d4/18618295-bdf2-46e6-b561-c3a2775071d4.jsonl": { - "mtimeMs": 1778184725699, - "mtimeSec": 1778184725 + "/Users/erikassiscardoso/.cursor/projects/Users-erikassiscardoso-Local-Sites-velora/agent-transcripts/ed1cd72f-1fd4-49a1-92ef-18325437cf3e/ed1cd72f-1fd4-49a1-92ef-18325437cf3e.jsonl": { + "mtimeMs": 1777753610368.671, + "mtimeSec": 1777753610 }, - "/Users/erikassiscardoso/.cursor/projects/Users-erikassiscardoso-Local-Sites-velora/agent-transcripts/18618295-bdf2-46e6-b561-c3a2775071d4/subagents/0f44cf87-7461-4845-9f03-338f5dc902fa.jsonl": { - "mtimeMs": 1778184539161, - "mtimeSec": 1778184539 + "/Users/erikassiscardoso/.cursor/projects/Users-erikassiscardoso-Local-Sites-velora/agent-transcripts/6607f714-8ca0-4f9c-8438-37e472087f00/subagents/5257044c-1ee3-4a8a-8bcc-e832c1098b75.jsonl": { + "mtimeMs": 1778186072210.1665, + "mtimeSec": 1778186072 }, - "/Users/erikassiscardoso/.cursor/projects/Users-erikassiscardoso-Local-Sites-velora/agent-transcripts/18618295-bdf2-46e6-b561-c3a2775071d4/subagents/3a538d77-2c55-4b14-9d9e-7cc1bc4a3e78.jsonl": { - "mtimeMs": 1778184550203, - "mtimeSec": 1778184550 + "/Users/erikassiscardoso/.cursor/projects/Users-erikassiscardoso-Local-Sites-velora/agent-transcripts/6607f714-8ca0-4f9c-8438-37e472087f00/subagents/71601be8-1824-4ff6-bff6-10979b3b9d1c.jsonl": { + "mtimeMs": 1778185707953.2952, + "mtimeSec": 1778185707 }, - "/Users/erikassiscardoso/.cursor/projects/Users-erikassiscardoso-Local-Sites-velora/agent-transcripts/18618295-bdf2-46e6-b561-c3a2775071d4/subagents/e7c111ca-606d-4a7c-b6ed-6f15fd594ad7.jsonl": { - "mtimeMs": 1778184530873, - "mtimeSec": 1778184530 + "/Users/erikassiscardoso/.cursor/projects/Users-erikassiscardoso-Local-Sites-velora/agent-transcripts/6607f714-8ca0-4f9c-8438-37e472087f00/subagents/5f1fbb68-c2a6-4653-bc18-39fd65714456.jsonl": { + "mtimeMs": 1778185807372.7222, + "mtimeSec": 1778185807 }, - "/Users/erikassiscardoso/.cursor/projects/Users-erikassiscardoso-Local-Sites-velora/agent-transcripts/1a0acb75-2678-41d9-a434-61d34aa5db6e/1a0acb75-2678-41d9-a434-61d34aa5db6e.jsonl": { - "mtimeMs": 1777455389418, - "mtimeSec": 1777455389 + "/Users/erikassiscardoso/.cursor/projects/Users-erikassiscardoso-Local-Sites-velora/agent-transcripts/6607f714-8ca0-4f9c-8438-37e472087f00/subagents/3421c42d-30dc-4c0a-8862-3e7bef09293c.jsonl": { + "mtimeMs": 1778185139365.1401, + "mtimeSec": 1778185139 }, - "/Users/erikassiscardoso/.cursor/projects/Users-erikassiscardoso-Local-Sites-velora/agent-transcripts/4784ec19-7f3e-4ce7-84bb-485c708340f8/4784ec19-7f3e-4ce7-84bb-485c708340f8.jsonl": { - "mtimeMs": 1778189613797, - "mtimeSec": 1778189613 + "/Users/erikassiscardoso/.cursor/projects/Users-erikassiscardoso-Local-Sites-velora/agent-transcripts/6607f714-8ca0-4f9c-8438-37e472087f00/subagents/8cdb1bbe-17af-4e99-99d4-753cd306a954.jsonl": { + "mtimeMs": 1778185806433.3477, + "mtimeSec": 1778185806 + }, + "/Users/erikassiscardoso/.cursor/projects/Users-erikassiscardoso-Local-Sites-velora/agent-transcripts/6607f714-8ca0-4f9c-8438-37e472087f00/6607f714-8ca0-4f9c-8438-37e472087f00.jsonl": { + "mtimeMs": 1778186899932.858, + "mtimeSec": 1778186899 }, "/Users/erikassiscardoso/.cursor/projects/Users-erikassiscardoso-Local-Sites-velora/agent-transcripts/558abbbe-b072-48ec-82e2-2b80941fbe18/558abbbe-b072-48ec-82e2-2b80941fbe18.jsonl": { - "mtimeMs": 1777456066747, + "mtimeMs": 1777456066747.9126, "mtimeSec": 1777456066 }, - "/Users/erikassiscardoso/.cursor/projects/Users-erikassiscardoso-Local-Sites-velora/agent-transcripts/6607f714-8ca0-4f9c-8438-37e472087f00/6607f714-8ca0-4f9c-8438-37e472087f00.jsonl": { - "mtimeMs": 1778186899932, - "mtimeSec": 1778186899 + "/Users/erikassiscardoso/.cursor/projects/Users-erikassiscardoso-Local-Sites-velora/agent-transcripts/9dbf47df-d1ff-4804-94ec-bcd6cc7a408f/9dbf47df-d1ff-4804-94ec-bcd6cc7a408f.jsonl": { + "mtimeMs": 1778167194799.754, + "mtimeSec": 1778167194 }, - "/Users/erikassiscardoso/.cursor/projects/Users-erikassiscardoso-Local-Sites-velora/agent-transcripts/6607f714-8ca0-4f9c-8438-37e472087f00/subagents/3421c42d-30dc-4c0a-8862-3e7bef09293c.jsonl": { - "mtimeMs": 1778185139365, - "mtimeSec": 1778185139 + "/Users/erikassiscardoso/.cursor/projects/Users-erikassiscardoso-Local-Sites-velora/agent-transcripts/e4897ba2-a6c9-4962-a6eb-182c160d18da/e4897ba2-a6c9-4962-a6eb-182c160d18da.jsonl": { + "mtimeMs": 1787316784737.6025, + "mtimeSec": 1787316784 }, - "/Users/erikassiscardoso/.cursor/projects/Users-erikassiscardoso-Local-Sites-velora/agent-transcripts/6607f714-8ca0-4f9c-8438-37e472087f00/subagents/5257044c-1ee3-4a8a-8bcc-e832c1098b75.jsonl": { - "mtimeMs": 1778186072210, - "mtimeSec": 1778186072 + "/Users/erikassiscardoso/.cursor/projects/Users-erikassiscardoso-Local-Sites-velora/agent-transcripts/1a0acb75-2678-41d9-a434-61d34aa5db6e/1a0acb75-2678-41d9-a434-61d34aa5db6e.jsonl": { + "mtimeMs": 1777455389418.547, + "mtimeSec": 1777455389 }, - "/Users/erikassiscardoso/.cursor/projects/Users-erikassiscardoso-Local-Sites-velora/agent-transcripts/6607f714-8ca0-4f9c-8438-37e472087f00/subagents/5f1fbb68-c2a6-4653-bc18-39fd65714456.jsonl": { - "mtimeMs": 1778185807372, - "mtimeSec": 1778185807 + "/Users/erikassiscardoso/.cursor/projects/Users-erikassiscardoso-Local-Sites-velora/agent-transcripts/f189cdc9-ce87-420b-a111-cc8b68b661d2/subagents/bb3ca315-3e7b-4179-bc25-84887e4252d7.jsonl": { + "mtimeMs": 1787858041666.5142, + "mtimeSec": 1787858041 }, - "/Users/erikassiscardoso/.cursor/projects/Users-erikassiscardoso-Local-Sites-velora/agent-transcripts/6607f714-8ca0-4f9c-8438-37e472087f00/subagents/71601be8-1824-4ff6-bff6-10979b3b9d1c.jsonl": { - "mtimeMs": 1778185707953, - "mtimeSec": 1778185707 + "/Users/erikassiscardoso/.cursor/projects/Users-erikassiscardoso-Local-Sites-velora/agent-transcripts/f189cdc9-ce87-420b-a111-cc8b68b661d2/subagents/23444bac-61e9-417f-a159-5b17c6f4bfb1.jsonl": { + "mtimeMs": 1787840620627.6536, + "mtimeSec": 1787840620 }, - "/Users/erikassiscardoso/.cursor/projects/Users-erikassiscardoso-Local-Sites-velora/agent-transcripts/6607f714-8ca0-4f9c-8438-37e472087f00/subagents/8cdb1bbe-17af-4e99-99d4-753cd306a954.jsonl": { - "mtimeMs": 1778185806433, - "mtimeSec": 1778185806 + "/Users/erikassiscardoso/.cursor/projects/Users-erikassiscardoso-Local-Sites-velora/agent-transcripts/f189cdc9-ce87-420b-a111-cc8b68b661d2/f189cdc9-ce87-420b-a111-cc8b68b661d2.jsonl": { + "mtimeMs": 1787860483908.09, + "mtimeSec": 1787860483 }, - "/Users/erikassiscardoso/.cursor/projects/Users-erikassiscardoso-Local-Sites-velora/agent-transcripts/6a780cd3-579a-44fb-8723-104e3be1c936/6a780cd3-579a-44fb-8723-104e3be1c936.jsonl": { - "mtimeMs": 1777826388114, - "mtimeSec": 1777826388 + "/Users/erikassiscardoso/.cursor/projects/Users-erikassiscardoso-Local-Sites-velora/agent-transcripts/4bd9dae4-d6bf-49bf-8e43-76c77fc4b56a/4bd9dae4-d6bf-49bf-8e43-76c77fc4b56a.jsonl": { + "mtimeMs": 1788108888571.2524, + "mtimeSec": 1788108888 }, - "/Users/erikassiscardoso/.cursor/projects/Users-erikassiscardoso-Local-Sites-velora/agent-transcripts/70831a21-356c-4321-aab2-c64a327f61b0/70831a21-356c-4321-aab2-c64a327f61b0.jsonl": { - "mtimeMs": 1787600611440, - "mtimeSec": 1787600611 + "/Users/erikassiscardoso/.cursor/projects/Users-erikassiscardoso-Local-Sites-velora/agent-transcripts/4bd9dae4-d6bf-49bf-8e43-76c77fc4b56a/subagents/5acf18e4-aebb-48d8-9119-ba8b99d96ff4.jsonl": { + "mtimeMs": 1788088034726.202, + "mtimeSec": 1788088034 + }, + "/Users/erikassiscardoso/.cursor/projects/Users-erikassiscardoso-Local-Sites-velora/agent-transcripts/4bd9dae4-d6bf-49bf-8e43-76c77fc4b56a/subagents/40f6206c-6a36-4701-b11a-98ac1302a332.jsonl": { + "mtimeMs": 1788087714970.336, + "mtimeSec": 1788087714 + }, + "/Users/erikassiscardoso/.cursor/projects/Users-erikassiscardoso-Local-Sites-velora/agent-transcripts/4bd9dae4-d6bf-49bf-8e43-76c77fc4b56a/subagents/1e9cc255-7eab-448a-a2c8-b973310eb608.jsonl": { + "mtimeMs": 1788107655785.1824, + "mtimeSec": 1788107655 + }, + "/Users/erikassiscardoso/.cursor/projects/Users-erikassiscardoso-Local-Sites-velora/agent-transcripts/4bd9dae4-d6bf-49bf-8e43-76c77fc4b56a/subagents/ef1b1794-65fe-4378-8364-20bcf45d5713.jsonl": { + "mtimeMs": 1788087712932.7095, + "mtimeSec": 1788087712 }, "/Users/erikassiscardoso/.cursor/projects/Users-erikassiscardoso-Local-Sites-velora/agent-transcripts/70831a21-356c-4321-aab2-c64a327f61b0/subagents/4392098b-6f8c-46d5-8890-ba2c6014af90.jsonl": { - "mtimeMs": 1787600620087, - "mtimeSec": 1787600620 + "mtimeMs": 1787600653398.6538, + "mtimeSec": 1787600653 }, "/Users/erikassiscardoso/.cursor/projects/Users-erikassiscardoso-Local-Sites-velora/agent-transcripts/70831a21-356c-4321-aab2-c64a327f61b0/subagents/43e4de2c-1f21-4226-92bf-d7a8a53125ed.jsonl": { - "mtimeMs": 1787582184721, + "mtimeMs": 1787582184721.024, "mtimeSec": 1787582184 }, "/Users/erikassiscardoso/.cursor/projects/Users-erikassiscardoso-Local-Sites-velora/agent-transcripts/70831a21-356c-4321-aab2-c64a327f61b0/subagents/cd35309d-843b-4b29-b69d-cd2ac29eee1e.jsonl": { - "mtimeMs": 1787572666994, + "mtimeMs": 1787572666994.6475, "mtimeSec": 1787572666 }, - "/Users/erikassiscardoso/.cursor/projects/Users-erikassiscardoso-Local-Sites-velora/agent-transcripts/9dbf47df-d1ff-4804-94ec-bcd6cc7a408f/9dbf47df-d1ff-4804-94ec-bcd6cc7a408f.jsonl": { - "mtimeMs": 1778167194799, - "mtimeSec": 1778167194 - }, - "/Users/erikassiscardoso/.cursor/projects/Users-erikassiscardoso-Local-Sites-velora/agent-transcripts/e4897ba2-a6c9-4962-a6eb-182c160d18da/e4897ba2-a6c9-4962-a6eb-182c160d18da.jsonl": { - "mtimeMs": 1787316784737, - "mtimeSec": 1787316784 + "/Users/erikassiscardoso/.cursor/projects/Users-erikassiscardoso-Local-Sites-velora/agent-transcripts/70831a21-356c-4321-aab2-c64a327f61b0/70831a21-356c-4321-aab2-c64a327f61b0.jsonl": { + "mtimeMs": 1787601340071.9563, + "mtimeSec": 1787601340 }, - "/Users/erikassiscardoso/.cursor/projects/Users-erikassiscardoso-Local-Sites-velora/agent-transcripts/ed1cd72f-1fd4-49a1-92ef-18325437cf3e/ed1cd72f-1fd4-49a1-92ef-18325437cf3e.jsonl": { - "mtimeMs": 1777753610368, - "mtimeSec": 1777753610 + "/Users/erikassiscardoso/.cursor/projects/Users-erikassiscardoso-Local-Sites-velora/agent-transcripts/6a780cd3-579a-44fb-8723-104e3be1c936/6a780cd3-579a-44fb-8723-104e3be1c936.jsonl": { + "mtimeMs": 1777826388114.6313, + "mtimeSec": 1777826388 }, "/Users/erikassiscardoso/.cursor/projects/Users-erikassiscardoso-Local-Sites-velora/agent-transcripts/f903c4dc-dba1-4bb0-bbc0-2916acc9cfb3/f903c4dc-dba1-4bb0-bbc0-2916acc9cfb3.jsonl": { - "mtimeMs": 1777633041954, + "mtimeMs": 1777633041954.3208, "mtimeSec": 1777633041 + }, + "/Users/erikassiscardoso/.cursor/projects/Users-erikassiscardoso-Local-Sites-velora/agent-transcripts/18618295-bdf2-46e6-b561-c3a2775071d4/subagents/e7c111ca-606d-4a7c-b6ed-6f15fd594ad7.jsonl": { + "mtimeMs": 1778184530873.8816, + "mtimeSec": 1778184530 + }, + "/Users/erikassiscardoso/.cursor/projects/Users-erikassiscardoso-Local-Sites-velora/agent-transcripts/18618295-bdf2-46e6-b561-c3a2775071d4/subagents/3a538d77-2c55-4b14-9d9e-7cc1bc4a3e78.jsonl": { + "mtimeMs": 1778184550203.0469, + "mtimeSec": 1778184550 + }, + "/Users/erikassiscardoso/.cursor/projects/Users-erikassiscardoso-Local-Sites-velora/agent-transcripts/18618295-bdf2-46e6-b561-c3a2775071d4/subagents/0f44cf87-7461-4845-9f03-338f5dc902fa.jsonl": { + "mtimeMs": 1778184539161.252, + "mtimeSec": 1778184539 + }, + "/Users/erikassiscardoso/.cursor/projects/Users-erikassiscardoso-Local-Sites-velora/agent-transcripts/18618295-bdf2-46e6-b561-c3a2775071d4/18618295-bdf2-46e6-b561-c3a2775071d4.jsonl": { + "mtimeMs": 1778184725699.5852, + "mtimeSec": 1778184725 + }, + "/Users/erikassiscardoso/.cursor/projects/Users-erikassiscardoso-Local-Sites-velora/agent-transcripts/68448bf6-9ece-4087-a506-8f4c4a1a73a0/subagents/6e97d7a8-752f-4d93-9a3d-93f27764207f.jsonl": { + "mtimeMs": 1787838000625.8323, + "mtimeSec": 1787838000 + }, + "/Users/erikassiscardoso/.cursor/projects/Users-erikassiscardoso-Local-Sites-velora/agent-transcripts/68448bf6-9ece-4087-a506-8f4c4a1a73a0/subagents/74ee04ac-21a7-42ab-9ab4-87df0e8b0dc5.jsonl": { + "mtimeMs": 1787837937201.3765, + "mtimeSec": 1787837937 + }, + "/Users/erikassiscardoso/.cursor/projects/Users-erikassiscardoso-Local-Sites-velora/agent-transcripts/68448bf6-9ece-4087-a506-8f4c4a1a73a0/68448bf6-9ece-4087-a506-8f4c4a1a73a0.jsonl": { + "mtimeMs": 1787839699778.3687, + "mtimeSec": 1787839699 + }, + "/Users/erikassiscardoso/.cursor/projects/Users-erikassiscardoso-Local-Sites-velora/agent-transcripts/67747426-9b14-4a8f-b653-9fefecfaac9e/67747426-9b14-4a8f-b653-9fefecfaac9e.jsonl": { + "mtimeMs": 1787623893941.035, + "mtimeSec": 1787623893 + }, + "/Users/erikassiscardoso/.cursor/projects/Users-erikassiscardoso-Local-Sites-velora/agent-transcripts/4784ec19-7f3e-4ce7-84bb-485c708340f8/4784ec19-7f3e-4ce7-84bb-485c708340f8.jsonl": { + "mtimeMs": 1778189613797.1755, + "mtimeSec": 1778189613 + }, + "/Users/erikassiscardoso/.cursor/projects/Users-erikassiscardoso-Local-Sites-velora/agent-transcripts/a4b519cd-43c8-42b2-97d8-a59ab53ce6a5/subagents/1ee0945b-ef60-46f8-9f1c-ee37a300bc3d.jsonl": { + "mtimeMs": 1788109413532.8337, + "mtimeSec": 1788109413 + }, + "/Users/erikassiscardoso/.cursor/projects/Users-erikassiscardoso-Local-Sites-velora/agent-transcripts/a4b519cd-43c8-42b2-97d8-a59ab53ce6a5/subagents/0c31c8df-636a-4d1c-b7ff-61303a8f8ce2.jsonl": { + "mtimeMs": 1788178851242.3418, + "mtimeSec": 1788178851 + }, + "/Users/erikassiscardoso/.cursor/projects/Users-erikassiscardoso-Local-Sites-velora/agent-transcripts/a4b519cd-43c8-42b2-97d8-a59ab53ce6a5/subagents/2ca383e1-b55b-4dd0-9f75-2d24a92a0c3c.jsonl": { + "mtimeMs": 1788109429360.8635, + "mtimeSec": 1788109429 + }, + "/Users/erikassiscardoso/.cursor/projects/Users-erikassiscardoso-Local-Sites-velora/agent-transcripts/a4b519cd-43c8-42b2-97d8-a59ab53ce6a5/a4b519cd-43c8-42b2-97d8-a59ab53ce6a5.jsonl": { + "mtimeMs": 1788178836494.3508, + "mtimeSec": 1788178836 } } } diff --git a/.cursor/hooks/state/continual-learning.json b/.cursor/hooks/state/continual-learning.json index b53db30..f2b030c 100644 --- a/.cursor/hooks/state/continual-learning.json +++ b/.cursor/hooks/state/continual-learning.json @@ -1,8 +1,8 @@ { "version": 1, - "lastRunAtMs": 1787600605625, - "turnsSinceLastRun": 1, - "lastTranscriptMtimeMs": 1787600605478.6738, - "lastProcessedGenerationId": "0851e63a-0490-413f-8bf7-d36a295dfdfc", + "lastRunAtMs": 1788178825203, + "turnsSinceLastRun": 6, + "lastTranscriptMtimeMs": 1788178825079.6353, + "lastProcessedGenerationId": "1d881672-78d7-49df-a1c5-450aec8c7c26", "trialStartedAtMs": null } diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a02b258..348d07b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,3 +37,29 @@ jobs: - name: Build run: pnpm build + + e2e: + runs-on: ubuntu-latest + needs: build + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22.13 + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Install Playwright browsers + run: pnpm exec playwright install --with-deps chromium firefox webkit + + - name: E2E tests + run: pnpm test:e2e diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 1db47d4..61ca100 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -1,5 +1,8 @@ name: Deploy Showcase to GitHub Pages +# Fallback deploy for apps/showcase — primary production URL is https://veloracss.io (Vercel). +# See docs/project/DEPLOY.md for DNS (GoDaddy) and docs.veloracss.io on a second Vercel project. + on: push: branches: diff --git a/.github/workflows/publish-npm.yml b/.github/workflows/publish-npm.yml new file mode 100644 index 0000000..7a2e385 --- /dev/null +++ b/.github/workflows/publish-npm.yml @@ -0,0 +1,45 @@ +name: Publish @velora/css + +on: + workflow_dispatch: + push: + tags: + - "css-v*" + +permissions: + contents: read + id-token: write + +jobs: + publish: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22.13 + registry-url: https://registry.npmjs.org + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Verify contract + run: pnpm verify:contract + + - name: Build CSS dist + run: pnpm --filter @velora/css build + + - name: Check dist + run: pnpm --filter @velora/css check:dist + + - name: Publish to npm + working-directory: packages/css + run: npm publish --provenance --access public diff --git a/.gitignore b/.gitignore index 915e73b..3e21577 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,8 @@ packages/css/dist/ **/.turbo/ **/.playwright-cli/ **/output/playwright/ +test-results/ +playwright-report/ pnpm-debug.log* # Astro diff --git a/AGENTS.md b/AGENTS.md index 20adbb5..16479c6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,25 +1,29 @@ ## Learned User Preferences - Prefer communication in Portuguese. -- Zero-JS motion is non-negotiable: never introduce JS animation runtimes; keep the CSS-only / HTML+CSS path. +- Zero animation runtime JavaScript is non-negotiable (not absolute “Zero JavaScript”); never introduce JS animation runtimes; keep the CSS-only / HTML+CSS path. - Treat Velora as a declarative CSS motion runtime and modern reference—not a utility framework; prioritize positioning, consistency, technical demos, and perceived value over feature volume. -- Prefer evaluate-then-plan against the execution roadmap before large changes; often wants page-by-page reformulation using Velora CSS and the Showcase template. -- Showcase pages should feel cinematic and exercise the design system plus motion catalog resources, not generic or simplistic layouts. -- Prefer complex, coordinated scenes (3D stages, large type, enter/exit choreography, horizontal/stacked sections) over sparse section layouts. -- Keep showcase UI patterns consistent across pages; avoid exaggerated glow and one-off chrome that breaks the shared look. +- Prefer evaluate-then-plan against `docs/AGENTS-VELORA.md` (P0/P1 public-readiness roadmap) and the showcase-readiness audit before large feature work or public/npm release; often proceed autonomously through roadmap items without pausing for approval; often wants page-by-page reformulation using Velora CSS and the Showcase template. +- Showcase pages should feel cinematic and exercise the design system plus motion catalog resources; copy must be practical show-don't-tell (state scroll/hover/tab actions, copyable snippets beside demos, metrics only from generated catalog)—not generic layouts or abstract marketing copy. +- Prefer complex, coordinated scenes (3D stages, large type, enter/exit choreography, continuous pin+scrub opera tracks, horizontal/stacked sections) over sparse section layouts or product-form “release desk” teaching UIs. +- Keep showcase UI patterns consistent across pages; avoid exaggerated glow and one-off chrome that breaks the shared look; motion catalog should use compact DX controls covering the full contract and varied demo visuals (not repetitive chrome); avoid overused narrative hooks (e.g. "Release 07 / 72%") outside Home idx-clock and Skins—follow `docs/project/SHOWCASE_COPY_GUIDE.md`. - Stage 3D should be a reusable CSS-only stage contract (perspective/preserve-3d on the container; positional transforms via CSS vars on items; motion on inner content), not cube-triad-specific demos; preserve text-ring-orbit and circle-text-scroll. -- Aim for GSAP-like scene authorship in HTML/CSS (shared scene clock, relative timing, pin+scrub)—preferred product direction for the scene engine. -- Core motion must stay host-agnostic and work with any UI (Tailwind and others); **Skins** is the product name for the Velora design-system layer (named themes via `data-editorial-theme`); Showcase is the cinematic reference UI, not the motion contract. +- Aim for GSAP-like scene authorship in HTML/CSS (shared scene clock, relative timing, pin+scrub); use `vl-in-view` for temporal viewport-gated enters and `vl-timeline="view"` (+ `vl-range`) for scrub—often hybrid on teaching pages; inside pinned scenes use chapter gates on the scene clock (`vl-act`/`vl-span`), not nested `vl-in-view` on `[vl-stage]`. +- Core motion must stay host-agnostic and work with any UI (Tailwind and others); **Skins** is the product name for the Velora design-system layer (named themes via `data-editorial-theme`); Showcase proves interactively (cinematic demos); Docs teach narratively and consume `@velora/catalog` at build—don't duplicate manual preset tables. - Prefer modern CSS capabilities (e.g. `if()`, typed `attr()`, `sibling-index()`) as progressive enhancement for scene choreography. -- Prefer fixing contract/consistency before expanding demos; prefer a lean public Showcase that archives surplus pages in-repo rather than deleting Skins/DS work; reuse existing motion examples rather than inventing parallel systems. +- Prefer hardening Scene Engine + `vl-in-view` contract/consistency before expanding demos; expand motion inventory before freezing core/catalog; prefer a lean public Showcase that archives surplus pages in-repo rather than deleting Skins/DS work; reuse existing motion examples rather than inventing parallel systems. ## Learned Workspace Facts - Monorepo layout: canonical CSS in `packages/css/src/`; Vite playground in `apps/showcase/`; Astro docs app in `apps/docs/`; markdown source of truth in repo-root `docs/`; plus `design-system/`, `examples/`, `experiments/`, and `starters/html-css-minimal/`. - Always edit CSS in `packages/css/src/` and sync to showcase (`pnpm sync:showcase-css`); `apps/showcase/public/css/` is derived and must not be edited as source. -- Motion is attribute-driven (`vl-effect`, `vl-timeline`, `vl-range`, `vl-scene`, related `vl-*`); rules live in ordered `@layer velora.*` (reset → tokens → layout → motion → components → transitions → utilities → overrides). -- Product surfaces split: Velora core = scene/motion engine; Showcase = cinematic reference UI/DS; docs site documents the framework once the API is stable. -- `@velora/css` ships separable entrypoints: `motion-core` (host-agnostic engine) vs `theme` / full bundle (visual Skins); editorial skins use `html[data-editorial-theme]` (e.g. noir, earth, aethel, meridian). -- Lean Showcase restarts should snapshot retired pages under a dated in-repo `archive/` (outside live Vite registry and contract checks), not delete recoverable DS/skin work. -- Workspace tooling is pnpm + Turborepo; common checks include `pnpm verify:contract` and showcase CSS drift checks. -- Root `AGENTS.md` holds learned memory only; agent operating instructions live under `docs/agents/AGENTS.md` and must not be mixed into the learned-memory file. +- Motion is attribute-driven (`vl-effect`, `vl-timeline`, `vl-range`, `vl-scene`, `vl-in-view`, related `vl-*`); `vl-in-view` is a temporal viewport gate (not scrub) that replays descendant motion on re-entry and must not pause scroll-driven timelines; do not nest `vl-in-view` on `[vl-scene][vl-timeline="view"] [vl-stage]`—use chapter gates on the scene clock for in-view-like enter/replay/exit inside pin; view-clock stages compose via named `--vl-scene` / `--vl-scroll-timeline`; rules live in ordered `@layer velora.*` (reset → tokens → layout → motion → components → transitions → utilities → overrides). +- Public deploy layout: Showcase `veloracss.io` + Docs `docs.veloracss.io` on Vercel; Hostinger VPS `examples.veloracss.io` for WordPress/host demos; GitHub Pages as showcase fallback; GoDaddy DNS only—`docs/project/DEPLOY.md`; canonical URLs in `apps/docs/src/lib/site.ts`. +- `@velora/css` ships separable entrypoints: `motion-core` (host-agnostic engine) vs `theme` / full bundle (visual Skins); editorial skins use `html[data-editorial-theme]` (e.g. noir, earth, aethel, meridian); cube-triad demos live in `scene-recipes.css`, not the core 3D stage contract; package is monorepo-local—not yet published to npm; defer publish until showcase and docs are ready (document honestly until P1.4). +- Showcase contract registry: 12 live pages (primary + reference); lean restarts snapshot retired pages under a dated in-repo `archive/` (outside live Vite registry and contract checks), not delete recoverable DS/skin work. +- Workspace tooling is pnpm + Turborepo; local Showcase via `pnpm dev` (or `pnpm --filter showcase dev`); docs site via `pnpm dev:docs` (Astro, http://localhost:4321); common checks include `pnpm verify:contract`, showcase CSS drift checks, and `pnpm generate:catalog` / `pnpm check:catalog`. +- Canonical GitHub remote: `https://github.com/erikconnect/Velora` (not legacy `velora-css/velora` URLs). +- P0 observability infrastructure (showcase audit, attribute grammar, generated catalog, compatibility matrix, Playwright E2E) is largely complete; npm publish remains deferred. +- Root `AGENTS.md` holds learned memory only; agent operating instructions live under `docs/agents/AGENTS.md`; `docs/AGENTS-VELORA.md` is the product/public-readiness roadmap (P0/P1)—keep all three distinct. +- Canonical public spec chain: `docs/spec/attribute-grammar.md` + generated `packages/catalog/` (`pnpm generate:catalog`) + operational matrix in `docs/project/CONTRACT.md`. +- Showcase motion authorship (gate vs scrub vs scene, anti-patterns) lives in `docs/project/SHOWCASE_PAGE_PLAYBOOK.md` §4; copy tone and page jobs in `docs/project/SHOWCASE_COPY_GUIDE.md`; bussola section IDs must compose `view-timeline-name` with `--vl-scene` (e.g. `--vl-scene, --tl-N`), and unlayered `.scene-tl-stage { position: relative }` breaks sticky pin on `[vl-stage]`. diff --git a/README.md b/README.md index f00fd59..cc01ae5 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ # Velora -**Motion-native CSS. Zero JavaScript. Cinematic interfaces.** +**Motion-native CSS. Zero animation runtime JavaScript. Cinematic interfaces.** ![License: ISC](https://img.shields.io/badge/license-ISC-blue.svg) Velora is a **declarative CSS motion engine**. HTML attributes describe intent; the browser executes it. No JavaScript animation runtime. Works with Tailwind, Relume, or any UI. -The optional **Skins** layer provides complete design-system models. The cinematic **Showcase** proves how Core behavior and Skin DNA combine; neither is required to turn motion on. +The optional **Skins** layer provides complete design-system models. The cinematic **Showcase** proves how Core behavior and Skin DNA combine; neither is required to turn motion on. ## Features @@ -16,9 +16,9 @@ The optional **Skins** layer provides complete design-system models. The cinemat 📐 **8-Layer Cascade Architecture** — Structured specificity via CSS `@layer` (reset → tokens → layout → motion → components → transitions → utilities → overrides) -🔄 **Element + Scroll Motion** — Native entry/exit, intrinsic-size, top-layer, path and scroll-driven motion +🔄 **Element + Scroll Motion** — Native entry/exit, intrinsic-size, top-layer, path and scroll-driven motion -📦 **Declarative Attribute Grammar** — Motion via HTML attributes (`vl-effect`, `vl-timeline`, `vl-children`, `vl-scene`) instead of CSS classes +📦 **Declarative Attribute Grammar** — Motion via HTML attributes (`vl-enter`, `vl-scroll`, `vl-scene`, `vl-timeline`, …) instead of CSS classes ⚡ **Container Queries & Density Modes** — Responsive components with default, compact, and presentation layout modes @@ -30,6 +30,14 @@ The optional **Skins** layer provides complete design-system models. The cinemat pnpm add @velora/css ``` +> **Note:** `@velora/css` is **publish-ready** (`dist/` + manifest) but **not yet on npm**. Use workspace, `pnpm pack:css`, or CDN URLs from [PUBLISH.md](docs/project/PUBLISH.md) after release. + +### CDN (after publish) + +```html + +``` + ### 2. Import Host-agnostic (any UI — Tailwind, etc.): @@ -50,7 +58,7 @@ Full Velora look (theme + components + motion): ```html
-

Zero-JS motion

+

Declarative motion

Same attributes on Tailwind or Velora UI.

@@ -83,9 +91,9 @@ It includes: - `@velora/css/premium` — premium components - `@velora/css/overrides` — last-mile override layer -Proofs: `examples/tailwind-host/` (motion-core + Tailwind) · `apps/showcase` (six primary areas: Home, Core, Elements, Scenes, Skins, Catalog). Hosts and Archive remain secondary references. Older Showcase pages: `archive/showcase-2026-08/`. - -**Skins** are named design-system models (`html[data-editorial-theme]`) covering typography, grid, density, geometry, materials, media and motion voice—not only color. They are optional; motion works without them. +Proofs: `examples/tailwind-host/` (motion-core + Tailwind) · `apps/showcase` (six primary areas: Home, Core, Elements, Scenes, Skins, Catalog). Hosts and Archive remain secondary references. Older Showcase pages: `archive/showcase-2026-08/`. + +**Skins** are named design-system models (`html[data-editorial-theme]`) covering typography, grid, density, geometry, materials, media and motion voice—not only color. They are optional; motion works without them. ## Motion Channels (v2) @@ -147,7 +155,9 @@ Apply a preset with a single attribute: ``` -No JavaScript required. +No animation runtime JavaScript required for cross-document MPA transitions. + +**Spec:** [`docs/spec/attribute-grammar.md`](docs/spec/attribute-grammar.md) · **Preset registry:** `packages/catalog/` (136 presets, `pnpm generate:catalog`) ## Workspace Layout @@ -158,8 +168,8 @@ velora/ ├── docs/ # Markdown source of truth (contract, workspace, agents) ├── packages/ │ ├── css/ # @velora/css — canonical framework source -│ ├── pages/ # legacy/reference HTML pages used by showcase tooling -│ └── velora-components/ # component HTML catalog and source material +│ ├── catalog/ # Generated preset registry (pnpm generate:catalog) +│ └── compiler/ # Grammar validator and motion scanner ├── apps/ │ ├── showcase/ # Vite showcase (primary interactive app + API catalogs) │ └── docs/ # Astro documentation site (published guides) diff --git a/apps/docs/astro.config.mjs b/apps/docs/astro.config.mjs index 9e21187..6fec4f8 100644 --- a/apps/docs/astro.config.mjs +++ b/apps/docs/astro.config.mjs @@ -5,7 +5,7 @@ import path from "node:path"; const root = path.dirname(fileURLToPath(import.meta.url)); export default defineConfig({ - site: "https://www.veloracss.io", + site: "https://docs.veloracss.io", vite: { resolve: { alias: { diff --git a/apps/docs/package.json b/apps/docs/package.json index 77d47ab..1d1729b 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -4,6 +4,7 @@ "version": "0.0.1", "private": true, "dependencies": { + "@velora/catalog": "workspace:*", "@velora/css": "workspace:*", "astro": "^7.2.6" }, diff --git a/apps/docs/public/js/docs-live.js b/apps/docs/public/js/docs-live.js new file mode 100644 index 0000000..6c1e331 --- /dev/null +++ b/apps/docs/public/js/docs-live.js @@ -0,0 +1,16 @@ +/** + * docs-live.js — replay scroll for in-page Velora demos (docs tooling only). + */ +(function () { + document.querySelectorAll("[data-docs-replay]").forEach((btn) => { + btn.addEventListener("click", () => { + const live = btn.closest(".docs-live"); + const scroller = live?.querySelector("[data-docs-scroll]"); + if (!scroller) return; + scroller.scrollTop = 0; + window.requestAnimationFrame(() => { + scroller.scrollTo({ top: scroller.scrollHeight, behavior: "smooth" }); + }); + }); + }); +})(); diff --git a/apps/docs/public/js/docs-state.js b/apps/docs/public/js/docs-state.js new file mode 100644 index 0000000..2b2c081 --- /dev/null +++ b/apps/docs/public/js/docs-state.js @@ -0,0 +1,53 @@ +/** + * docs-state.js — Velora Docs (Astro) + * Applies stored editorial/theme/transition before first paint (FOUC guard). + * Shares localStorage keys with Showcase for consistent Skins across sites. + * NOT part of @velora/css — docs tooling only. + */ +(function () { + const root = document.documentElement; + const ls = localStorage; + + const ALLOWED_THEMES = ["light", "dark", "auto"]; + const ALLOWED_EDITORIAL = ["noir", "earth", "aethel", "meridian"]; + const ALLOWED_TRANSITIONS = ["cinema", "wipe", "glide", "iris", "snap"]; + + const EDITORIAL_META = { + noir: { pack: "velora-noir", theme: "dark" }, + earth: { pack: "velora-earth", theme: "light" }, + aethel: { pack: "velora-aethel", theme: "dark" }, + meridian: { pack: "velora-meridian", theme: "dark" }, + }; + + function sanitize(value, allowed, fallback) { + return allowed.includes(value) ? value : fallback; + } + + const savedEditorial = ls.getItem("vl-editorial-theme"); + if (savedEditorial && ALLOWED_EDITORIAL.includes(savedEditorial)) { + const meta = EDITORIAL_META[savedEditorial]; + root.setAttribute("data-editorial-theme", savedEditorial); + root.setAttribute("data-theme-pack", meta.pack); + root.setAttribute("data-theme", meta.theme); + } else { + const savedTheme = ls.getItem("vl-theme"); + const theme = sanitize(savedTheme || root.getAttribute("data-theme"), ALLOWED_THEMES, "dark"); + if (theme === "auto") { + root.removeAttribute("data-theme"); + } else { + root.setAttribute("data-theme", theme); + } + if (!root.getAttribute("data-editorial-theme")) { + root.setAttribute("data-editorial-theme", theme === "light" ? "earth" : "noir"); + root.setAttribute("data-theme-pack", theme === "light" ? "velora-earth" : "velora-noir"); + } + } + + const savedTransition = ls.getItem("vl-page-transition"); + const transition = sanitize( + savedTransition || root.getAttribute("vl-page-transition"), + ALLOWED_TRANSITIONS, + "cinema", + ); + root.setAttribute("vl-page-transition", transition); +})(); diff --git a/apps/docs/public/js/docs-ui.js b/apps/docs/public/js/docs-ui.js new file mode 100644 index 0000000..d2ca8a1 --- /dev/null +++ b/apps/docs/public/js/docs-ui.js @@ -0,0 +1,136 @@ +/** + * docs-ui.js — Velora Docs skin controls (editorial + theme + transitions). + * NOT part of @velora/css — docs tooling only. + */ +(function () { + const root = document.documentElement; + const ls = localStorage; + + const THEME_KEY = "vl-theme"; + const TRANSITION_KEY = "vl-page-transition"; + const EDITORIAL_KEY = "vl-editorial-theme"; + + const ALLOWED_EDITORIAL = ["noir", "earth", "aethel", "meridian"]; + const ALLOWED_TRANSITIONS = ["cinema", "wipe", "glide", "iris", "snap"]; + + const EDITORIAL_META = { + noir: { pack: "velora-noir", theme: "dark" }, + earth: { pack: "velora-earth", theme: "light" }, + aethel: { pack: "velora-aethel", theme: "dark" }, + meridian: { pack: "velora-meridian", theme: "dark" }, + }; + + function syncThemeButtons(theme) { + document.querySelectorAll(".ds-theme-btn").forEach((btn) => { + const active = btn.dataset.dsTheme === theme; + btn.classList.toggle("is-active", active); + btn.setAttribute("aria-pressed", active ? "true" : "false"); + }); + } + + function applyTheme(theme) { + if (theme === "auto") { + root.removeAttribute("data-theme"); + } else { + root.setAttribute("data-theme", theme); + } + ls.setItem(THEME_KEY, theme); + syncThemeButtons(theme); + } + + function applyEditorial(value, { persist = true, syncThemeToggle = true } = {}) { + const v = ALLOWED_EDITORIAL.includes(value) ? value : "noir"; + const meta = EDITORIAL_META[v]; + + root.setAttribute("data-editorial-theme", v); + root.setAttribute("data-theme-pack", meta.pack); + root.setAttribute("data-theme", meta.theme); + + if (persist) { + ls.setItem(EDITORIAL_KEY, v); + ls.setItem(THEME_KEY, meta.theme); + } + if (syncThemeToggle) syncThemeButtons(meta.theme); + + document.querySelectorAll(".ds-editorial-select").forEach((sel) => { + sel.value = v; + }); + } + + function applyTransition(value) { + const v = ALLOWED_TRANSITIONS.includes(value) ? value : "cinema"; + root.setAttribute("vl-page-transition", v); + ls.setItem(TRANSITION_KEY, v); + document.querySelectorAll(".ds-vt-select").forEach((sel) => { + sel.value = v; + }); + } + + const currentTheme = root.getAttribute("data-theme") || "auto"; + const currentEditorial = root.getAttribute("data-editorial-theme") || "noir"; + const currentTransition = root.getAttribute("vl-page-transition") || "cinema"; + + syncThemeButtons(currentTheme); + document.querySelectorAll(".ds-vt-select").forEach((sel) => { + sel.value = currentTransition; + }); + document.querySelectorAll(".ds-editorial-select").forEach((sel) => { + sel.value = currentEditorial; + }); + + document.querySelectorAll(".ds-theme-btn").forEach((btn) => { + btn.addEventListener("click", () => { + const theme = btn.dataset.dsTheme || "auto"; + applyTheme(theme); + if (theme === "light") { + applyEditorial("earth", { syncThemeToggle: false }); + } else if (theme === "dark") { + applyEditorial("noir", { syncThemeToggle: false }); + } else if (theme === "auto") { + const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches; + applyEditorial(prefersDark ? "noir" : "earth", { syncThemeToggle: false }); + } + }); + }); + + document.querySelectorAll(".ds-editorial-select").forEach((sel) => { + sel.addEventListener("change", (e) => { + applyEditorial(e.target.value, { syncThemeToggle: true }); + }); + }); + + document.querySelectorAll(".ds-vt-select").forEach((sel) => { + sel.addEventListener("change", (e) => { + applyTransition(e.target.value); + }); + }); + + const header = document.querySelector(".vl-header"); + const menuBtn = document.querySelector(".vl-header__menu-btn"); + const drawer = document.querySelector(".vl-header__drawer"); + + if (header && menuBtn && drawer) { + function closeDrawer() { + header.removeAttribute("data-menu-open"); + menuBtn.setAttribute("aria-expanded", "false"); + drawer.setAttribute("aria-hidden", "true"); + } + + menuBtn.addEventListener("click", () => { + if (header.hasAttribute("data-menu-open")) { + closeDrawer(); + } else { + header.setAttribute("data-menu-open", ""); + menuBtn.setAttribute("aria-expanded", "true"); + drawer.setAttribute("aria-hidden", "false"); + } + }); + + document.addEventListener("keydown", (e) => { + if (e.key === "Escape" && header.hasAttribute("data-menu-open")) { + closeDrawer(); + menuBtn.focus(); + } + }); + } +})(); diff --git a/apps/docs/src/components/DocsLive.astro b/apps/docs/src/components/DocsLive.astro new file mode 100644 index 0000000..abcafb7 --- /dev/null +++ b/apps/docs/src/components/DocsLive.astro @@ -0,0 +1,46 @@ +--- +interface Props { + label?: string; + hint?: string; + /** none = static (hover/auto); short | tall | scene = internal scroll to drive view timelines */ + scroll?: "none" | "short" | "tall" | "scene"; + code?: string; +} + +const { + label = "Test here", + hint = "Scroll inside the frame to trigger view-linked motion. Use Replay to run again.", + scroll = "short", + code, +} = Astro.props; +--- + +
+
+ {label} + {scroll !== "none" && ( + + )} +
+ {scroll !== "none" && hint &&

{hint}

} + { + scroll === "none" ? ( +
+ +
+ ) : ( +
+ +
+ +
+ +
+ ) + } + {code && ( +
{code}
+ )} +
diff --git a/apps/docs/src/layouts/Base.astro b/apps/docs/src/layouts/Base.astro index 47f17f5..0ac18e3 100644 --- a/apps/docs/src/layouts/Base.astro +++ b/apps/docs/src/layouts/Base.astro @@ -1,12 +1,14 @@ --- import "@velora/css"; +import "../styles/docs-shell.css"; +import { SITE, showcaseUrl } from "../lib/site"; interface Props { title: string; description?: string; } -const { title, description = "Velora CSS — motion-native design system" } = Astro.props; +const { title, description = "Velora CSS — declarative motion for modern HTML and CSS" } = Astro.props; const pathname = Astro.url.pathname; function isActive(href: string): boolean { @@ -16,404 +18,121 @@ function isActive(href: string): boolean { --- - + - {title} — Velora + {title} — Velora Docs + - - -
-
+ + + diff --git a/apps/docs/src/lib/catalog.ts b/apps/docs/src/lib/catalog.ts new file mode 100644 index 0000000..5b86353 --- /dev/null +++ b/apps/docs/src/lib/catalog.ts @@ -0,0 +1,34 @@ +import catalogSummary from "@velora/catalog/summary"; +import presetsBundle from "@velora/catalog/presets"; +import compatMatrix from "@velora/catalog/compatibility-matrix"; + +export type CatalogSummary = typeof catalogSummary; +export type Preset = (typeof presetsBundle.presets)[number]; +export type CompatibilityMatrix = typeof compatMatrix; + +export function getCatalogSummary(): CatalogSummary { + return catalogSummary; +} + +export function getPresetsByAttribute(attribute: string): Preset[] { + return presetsBundle.presets + .filter((preset) => preset.attribute === attribute) + .sort((a, b) => a.name.localeCompare(b.name)); +} + +export function getCompatibilityMatrix(): CompatibilityMatrix { + return compatMatrix; +} + +export function formatChannelLabel(channel: string): string { + if (channel === "pageTransition") return "page transition"; + return channel; +} + +export function formatGeneratedAt(iso: string): string { + return new Date(iso).toLocaleDateString("en-US", { + year: "numeric", + month: "short", + day: "numeric", + }); +} diff --git a/apps/docs/src/lib/site.ts b/apps/docs/src/lib/site.ts new file mode 100644 index 0000000..9b20b21 --- /dev/null +++ b/apps/docs/src/lib/site.ts @@ -0,0 +1,13 @@ +/** Public surface URLs — keep in sync with docs/project/DEPLOY.md */ +export const SITE = { + showcase: "https://veloracss.io", + docs: "https://docs.veloracss.io", + github: "https://github.com/erikconnect/Velora", + /** Hostinger VPS — WordPress + static host examples (configure when live) */ + examples: "https://examples.veloracss.io", +} as const; + +export function showcaseUrl(path = ""): string { + if (!path) return SITE.showcase; + return `${SITE.showcase}${path.startsWith("/") ? path : `/${path}`}`; +} diff --git a/apps/docs/src/pages/agents.astro b/apps/docs/src/pages/agents.astro index 2c41b5f..2e474d2 100644 --- a/apps/docs/src/pages/agents.astro +++ b/apps/docs/src/pages/agents.astro @@ -1,5 +1,6 @@ --- import Base from "../layouts/Base.astro"; +import DocsLive from "../components/DocsLive.astro"; --- @@ -7,11 +8,16 @@ import Base from "../layouts/Base.astro";

Agent Handbook

- This is the operational guide for AI coding agents working on Velora — including GitHub Copilot, - Cursor, and similar tools. It defines the product thesis, non-negotiable rules, attribute system - contract, and safe prompting patterns that all agents must follow. + Operational guide for AI agents on Velora. Demos on other pages use the same public + vl-* API — test there, do not invent showcase-only motion.

+ +

+ If the docs frame can replay it, it is a public Velora feature. +

+
+

Product Thesis

@@ -28,11 +34,10 @@ import Base from "../layouts/Base.astro";
<section
   vl-scene="hero"
   vl-timeline="view"
-  vl-effect="scene-hero-reveal"
   vl-range="entry 0% cover 70%"
   vl-pin>
-  <h1>Build motion-rich interfaces with HTML and CSS</h1>
-  <p>No animation libraries. No framework runtime.</p>
+  <h1 vl-enter="fade-up">Build motion-rich interfaces with HTML and CSS</h1>
+  <p vl-enter="fade-up" vl-delay="120ms">No animation libraries. No animation runtime JS.</p>
 </section>

@@ -116,33 +121,40 @@ import Base from "../layouts/Base.astro"; AttributePurposeExample values - vl-sceneMarks a motion-aware scene container"hero", "story", "features" - vl-effectDeclares the named effect preset"fade-up", "flow-in", "scene-hero-reveal" - vl-timelineDeclares the progress model"view", "scroll", "auto", "hover" + vl-enterEntrance preset (preferred channel)"fade-up", "clip-rise", "blur-in" + vl-scrollScroll-linked preset"media-zoom", "parallax" + vl-hoverHover / focus interaction preset"hover-lift", "underline-expand" + vl-effectLegacy / composite preset escape hatch"fade-up", "flow-in" + vl-sceneScene track — shared clock, optional pin heightpresence or named recipe + vl-stageSticky viewport stage inside a scenepresence only + vl-act / vl-spanBeat index and duration on scene clockvl-act="2", vl-span="3" + vl-in-viewTemporal viewport gate (replay on re-entry)presence only + vl-timelineProgress model"view", "scroll", "auto", "hover" vl-rangeActive progress range for timeline motion"entry 0% cover 40%" - vl-pinBoolean — pinned/sticky scene behaviorpresence only - vl-scrubBoolean — scroll-scrubbed animationpresence only - vl-onceBoolean — animate once onlypresence only + vl-pinPinned scene track height (viewport multiples)vl-pin="3" + vl-scrubScroll-scrubbed linear fillpresence only + vl-onceAnimate once onlypresence only vl-childrenChild choreography mode"stagger", "cascade" vl-staggerStagger delay override"fast", "slow", numeric - vl-depthDepth/parallax intensity modifier"shallow", "deep" - vl-speedDuration/speed modifier"slow", "fast" - vl-targetsSelects specific descendant targetsCSS selector string - vl-page-transitionPage transition preset"fade", "slide", "morph" - vl-transitionShared element or local transition"cover" + vl-motionSubtree motion mode (engine token scale)"standard", "cinematic", "still" + vl-page-transitionMPA page transition preset on <html>"cinema", "wipe", "glide"

Primitive vs Scene Presets

This distinction is mandatory.

- A primitive effect affects the element itself: + A channel preset affects the element itself:

-
<div vl-effect="fade-up"></div>
+
<div vl-enter="fade-up" vl-timeline="view"></div>

- A scene preset coordinates the element and its descendants: + A scene coordinates children on a shared clock:

-
<section vl-scene="hero" vl-effect="scene-hero-reveal"></section>
+
<section vl-scene vl-timeline="view" vl-pin="3" vl-scrub>
+  <div vl-stage>
+    <h1 vl-enter="clip-rise" vl-act="1">Title</h1>
+  </div>
+</section>

Agents must not blur these concepts.


diff --git a/apps/docs/src/pages/attributes.astro b/apps/docs/src/pages/attributes.astro index 5d4a779..775ff27 100644 --- a/apps/docs/src/pages/attributes.astro +++ b/apps/docs/src/pages/attributes.astro @@ -1,344 +1,85 @@ --- import Base from "../layouts/Base.astro"; +import DocsLive from "../components/DocsLive.astro"; --- - + Reference

Attribute Reference

- Complete reference for every vl-* HTML attribute. Attributes are Velora's - declarative grammar — they express intent, not implementation. + Velora motion is declared with vl-* HTML attributes. Prefer channels + for new work. Each section includes a live frame — Test here.

-

Quick Reference

- - - - - - - - - - - - - - - - - - - - -
AttributeTypeBrief
vl-effectenumNamed motion preset
vl-timelineenumAnimation progress model
vl-rangeenumScroll-driven animation range
vl-scenestringScene container marker
vl-childrenenumChild choreography mode
vl-staggerdurationStagger interval override
vl-depthnumberMotion distance multiplier
vl-speedenumDuration category override
vl-pinbooleanSticky positioning
vl-scrubbooleanLinear scroll-linked timing
vl-oncebooleanOne-shot animation
vl-targetsselectorDescendant target hint
vl-page-transitionenumCross-document page transition preset
vl-transitionstringShared element / local transition
- -
- -

vl-effect

-

- Declares the named motion preset to apply to this element. See the - Effects reference for full - descriptions of each value. -

- - - - - - - - - - - - - -
CategoryValues
Entrancefade-in fade-up fade-down slide-left slide-right scale-in blur-in reveal-3d flow-in clip-rise tilt-in
Interactionhover-lift hover-glow underline-expand icon-shift
Stateaccordion drawer panel-swap
Continuousshimmer float glow-breathe wobble orbit rock morph elastic-in
Texttext-reveal text-reveal-up text-reveal-down
Cinemacinema-zoom parallax typewriter
Scene presetsscene-hero-reveal scene-feature-flow scene-story-pin scene-layer-stack
-
<div vl-effect="fade-up">...</div>
-<a href="/page" vl-effect="underline-expand">Link</a>
-<section vl-scene="hero" vl-effect="scene-hero-reveal">...</section>
- -
- -

vl-timeline

-

- Controls when the animation defined by vl-effect plays. Without a timeline, - effects play immediately on page load. -

- - - - - - - - - - - - - - - - - - - - - - - - - - -
ValueMechanismUse case
viewanimation-timeline: view(block)Reveal as element scrolls into viewport. Most common scroll-driven pattern.
scrollanimation-timeline: scroll(root)Progress tied to root scroll position (0% = top, 100% = bottom).
autoanimation-timeline: autoBrowser default (time-based). Lets you use a custom timeline via CSS.
hoverPauses animation, runs on :hoverPlay animation on hover — good for preview effects.
-
<!-- Scroll-driven reveal -->
-<div vl-effect="blur-in" vl-timeline="view">...</div>
-
-<!-- Plays on hover -->
-<div vl-effect="scale-in" vl-timeline="hover">...</div>
- -
- -

vl-range

-

- Sets the animation-range for scroll-driven effects. Only effective when used with - vl-timeline="view" or vl-timeline="scroll". -

- - - - - - - - - - - - -
Valueanimation-rangeFeel
entryentry 0% entry 100%Plays only while entering the viewport
entry-shortentry 15% cover 35%Quick reveal in the upper viewport zone
entry-longentry 0% cover 70%Extended reveal range — default for scenes
covercover 0% cover 100%Plays the full time element covers the viewport
containcontain 0% contain 100%Plays while element is fully contained in viewport
customvar(--vl-range)Use --vl-range inline for full control
-
<!-- Slow reveal across the full scroll zone -->
-<div vl-effect="flow-in" vl-timeline="view" vl-range="entry-long">...</div>
-
-<!-- Fully custom range -->
-<div
-  vl-effect="fade-up"
-  vl-timeline="view"
-  vl-range="custom"
-  style="--vl-range: entry 10% cover 60%;">
-  Custom range
-</div>
- -
- -

vl-scene

-

- Marks a container as a scene host. Applies CSS containment and enables scene-specific - child choreography. Value is a semantic label. -

- - - - - - - - - - -
ValueIntent
heroFull-width hero section with primary CTA
storyNarrative / storytelling pin section
featuresFeature grid or card collection
any stringCustom scene label for your own patterns
-
<section vl-scene="hero" vl-effect="scene-hero-reveal">...</section>
-<section vl-scene="features" vl-effect="scene-feature-flow" vl-timeline="view">...</section>
- -
- -

vl-children

-

- Applies staggered animations to all direct children. Do not combine with a scene preset — use one - or the other. -

- - - - - - - - - -
ValueEffectMax children
staggerfade-up with incremental delays12
cascadeflow-in with incremental delays8
sequencefade-up one-after-another (full duration gaps)6
-
<ul vl-children="stagger" vl-stagger="80ms" vl-timeline="view">
-  <li>Item 1</li>
-  <li>Item 2</li>
-  <li>Item 3</li>
-</ul>
- -
- -

vl-stagger

-

- Overrides the default stagger step (--vl-stagger-step: 45ms) for vl-children. -

- - - - - - - - - - - - -
ValueDelay per child
60ms60ms
80ms80ms
100ms100ms
120ms120ms
150ms150ms
200ms200ms
- -
- -

vl-depth

+

Channels (preferred)

+ +
+
+

vl-enter

+

fade-up on view

+
+
+

vl-hover

+

Hover this card

+
+
+
+ +

vl-in-view gate

- Multiplies the translation distance (--vl-motion-distance) for entrance effects. - Affects fade-up, fade-down, slide-left, slide-right, - flow-in, and other translate-based effects. + Temporal gate — descendant motion replays when the gate re-enters the scrollport. + Requires container-type: style (Chrome 128+, Safari 18+); otherwise + vl-in-view is inert and children animate on their own timelines.

- - - - - - - - - - -
Value--vl-motion-distance
10.75rem — subtle
21.5rem — moderate
32.5rem — pronounced
44rem — dramatic
-
<div vl-effect="fade-up" vl-depth="4">Deep dramatic reveal</div>
- -
- -

vl-speed

-

Overrides the animation duration category:

- - - - - - - - - -
ValueDuration
fast150ms
normal300ms
slow500ms
-
<div vl-effect="scale-in" vl-speed="fast">Quick snap in</div>
+

'}> +
+

Replay on re-entry

+

+ Scroll away, scroll back, or press Replay +

+
+
-
- -

vl-pin

-

- Boolean attribute. Makes the element sticky (position: sticky; top: 0) at - z-index: 200. Use on scene sections that should remain fixed during scroll. -

-
<section vl-scene="hero" vl-pin>
-  <!-- Stays at top of viewport while parent scrolls -->
-</section>
- -
- -

vl-scrub

-

- Boolean attribute. Switches timing to linear (animation-timing-function: linear) and - sets animation-fill-mode: both. Use with scroll-driven effects where you want - direct scroll-to-animation scrubbing without easing curves. -

-
<div vl-effect="parallax" vl-scrub>Directly scrubbed parallax</div>
- -
- -

vl-once

-

- Boolean attribute. Sets animation-iteration-count: 1 and - animation-fill-mode: forwards. The element animates once and stays in its final state. -

-
<div vl-effect="clip-rise" vl-timeline="view" vl-once>
-  Reveals once, stays visible
-</div>
- -
- -

vl-targets

-

- A CSS selector string that hints which descendants should be targeted by the scene or children - system. CSS-only support is limited — primary use is for documentation and future runtime hooks. -

-
<section vl-scene="features" vl-targets=".vl-card">
-  <div>Non-card child (not targeted)</div>
-  <div class="vl-card">Card (targeted)</div>
-  <div class="vl-card">Card (targeted)</div>
-</section>
- -
- -

vl-page-transition

-

- Set on <html> to select the cross-document page transition preset. - See Page Transitions for full details. -

+

Quick Reference

- + - - - - - - + + + + + + + + + + + +
ValueEffect
AttributeTypeBrief
(omitted)Default Velora signature: blur + scale
wipeClip-path curtain wipe
glideHorizontal slide
irisCircular iris open/close
cinemaDramatic blur + brightness + clip
snapQuick spring-based snap
vl-entertokensEntrance preset (preferred)
vl-exittokensExit preset
vl-scrolltokensScroll-linked motion
vl-hovertokensHover / focus interaction
vl-effecttokensLegacy / composite escape hatch
vl-timelineenumview, scroll, auto, hover, state
vl-in-viewbooleanViewport gate for descendant motion
vl-scene / vl-stagetrack + stageScene engine
vl-act / vl-spannumberScene beat index and span
vl-pin / vl-scrubnumber / booleanPin+scrub track
vl-motionenumSubtree motion mode
vl-page-transitionenumMPA transition on <html>
-
<html lang="en" vl-page-transition="cinema">
- -
- -

vl-transition

-

- Reserved for shared element and local element transition hints. Intended for future - Velora runtime integration and CSS-level patterns. -

-
<!-- Future: cover transition pattern -->
-<div vl-transition="cover">...</div>
- -
- -

Attribute Composition

-

Attributes compose cleanly — combine them to express complex motion intent:

-
<!-- Cinematic feature section -->
-<section
-  vl-scene="features"
-  vl-effect="scene-feature-flow"
-  vl-timeline="view"
-  vl-range="entry-long">
-  ...
-</section>
-
-<!-- Staggered grid with custom pacing -->
-<div
-  vl-children="stagger"
-  vl-stagger="120ms"
-  vl-timeline="view"
-  vl-once>
-  ...
-</div>
 
-<!-- Deep, slow hero heading -->
-<h1
-  vl-effect="clip-rise"
-  vl-timeline="view"
-  vl-depth="3"
-  vl-speed="slow"
-  vl-once>
-  Premium motion starts here
-</h1>
+

Scene engine

+ +
+
+

Title act

+

Scroll-linked child

+
+
+
+ +

Further reading

+ diff --git a/apps/docs/src/pages/compatibility.astro b/apps/docs/src/pages/compatibility.astro new file mode 100644 index 0000000..fc0425c --- /dev/null +++ b/apps/docs/src/pages/compatibility.astro @@ -0,0 +1,81 @@ +--- +import Base from "../layouts/Base.astro"; +import DocsLive from "../components/DocsLive.astro"; +import { + formatGeneratedAt, + getCatalogSummary, + getCompatibilityMatrix, +} from "../lib/catalog"; +import { showcaseUrl } from "../lib/site"; + +const matrix = getCompatibilityMatrix(); +const summary = getCatalogSummary(); +const generatedLabel = formatGeneratedAt(matrix.generatedAt); +--- + + + Truth layer + +

Compatibility

+

+ Support levels come from packages/catalog/compatibility-matrix.json — generated with the preset registry ({generatedLabel}), not marketing copy. +

+ +
+

Registry · {summary.presetCount} presets

+

+ Baseline targets: Chrome {matrix.baseline.chrome}, Safari {matrix.baseline.safari}, Firefox {matrix.baseline.firefox}. + {matrix.baseline.note} +

+
+ + +

Reduced motion safe

+

+ Enable Reduce motion in OS settings — travel collapses, loops stop +

+
+ +

Support levels

+ + + + + + {Object.entries(matrix.levels).map(([level, meaning]) => ( + + + + + ))} + +
LevelMeaning
{level}{meaning}
+ +

CSS primitives matrix

+

Primitive × browser status from the generated matrix. Interactive version lives on the Showcase.

+ + + + + + {matrix.primitives.map((row) => ( + + + + + + + + + ))} + +
PrimitiveStatusChromeSafariFirefoxFallback
{row.name}{row.status}{row.chrome}{row.safari}{row.firefox}{row.fallback}
+ +

Where to verify

+ + diff --git a/apps/docs/src/pages/effects.astro b/apps/docs/src/pages/effects.astro index 6c51924..5c89369 100644 --- a/apps/docs/src/pages/effects.astro +++ b/apps/docs/src/pages/effects.astro @@ -1,102 +1,95 @@ --- import Base from "../layouts/Base.astro"; +import DocsLive from "../components/DocsLive.astro"; +import { + formatChannelLabel, + formatGeneratedAt, + getCatalogSummary, + getPresetsByAttribute, +} from "../lib/catalog"; +import { showcaseUrl } from "../lib/site"; + +const summary = getCatalogSummary(); +const enterPresets = getPresetsByAttribute("vl-enter"); +const hoverPresets = getPresetsByAttribute("vl-hover"); +const scrollPresets = getPresetsByAttribute("vl-scroll"); +const loopPresets = getPresetsByAttribute("vl-loop"); +const generatedLabel = formatGeneratedAt(summary.generatedAt); --- - + Motion

Motion Effects

- Named motion presets applied via the vl-effect attribute. All effects are pure CSS — - no JavaScript involved. + Prefer channels v2: vl-enter, vl-scroll, vl-hover, etc. + vl-effect remains a legacy/composite escape hatch. All motion is CSS-only — zero animation runtime JavaScript. + See Attribute Reference.

-

How Effects Work

+
+

Generated registry · {generatedLabel}

+
+
{summary.presetCount}Presets
+
{summary.byStatus.stable ?? 0}Stable
+
{summary.byStatus.experimental ?? 0}Experimental
+
+

+ {Object.entries(summary.byChannel) + .sort(([a], [b]) => a.localeCompare(b)) + .map(([channel, count]) => `${formatChannelLabel(channel)} (${count})`) + .join(" · ")} +

+

+ Full searchable registry: + Showcase catalog + · source packages/catalog/ via pnpm generate:catalog +

+
+ +

How channels work

+ +

Channel-first

+

+ Prefer vl-enter, vl-scroll, vl-hover — not legacy vl-effect +

+

- Add vl-effect to any element to apply a preset. Combine with - vl-timeline to control when the animation plays: + Pick the channel that owns the behavior, then add vl-timeline when you need a progress model:

-
<!-- Plays immediately on page load -->
-<h1 vl-effect="fade-up">Heading</h1>
+  
<!-- Entrance (preferred) -->
+<h1 vl-enter="fade-up">Heading</h1>
 
-<!-- Plays when element enters the viewport -->
-<div vl-effect="blur-in" vl-timeline="view">Content</div>
+<!-- Viewport-gated enter -->
+<div vl-in-view>
+  <p vl-enter="fade-up" vl-timeline="view">Content</p>
+</div>
 
-<!-- Plays once, stays visible after -->
-<div vl-effect="clip-rise" vl-timeline="view" vl-once>One-shot reveal</div>
+<!-- Scroll-linked -->
+<img vl-scroll="media-zoom" vl-timeline="view" alt="" />
 
-<!-- Custom depth and speed -->
-<div
-  vl-effect="fade-up"
-  vl-timeline="view"
-  style="--vl-motion-distance: 2.5rem; --vl-motion-duration: 700ms;">
-  Deep, slow reveal
-</div>
+<!-- Legacy composite (avoid for new work) --> +<div vl-effect="blur-in" vl-timeline="view">…</div>
-

Entrance Primitives

+

Entrance presets (vl-enter)

+

+ {enterPresets.length} presets from the generated catalog ({generatedLabel}). Prefer channels for new markup — not legacy vl-effect. +

Affect the element itself. Best combined with vl-timeline="view" for scroll-driven reveals.

- + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + {enterPresets.map((preset) => ( + + + + + + ))}
ValueDescriptionDefault duration
ValueStatusReduced motion
fade-inSimple opacity 0 → 1 entrance--vl-duration-slow (500ms)
fade-upOpacity + translateY upward. Workhorse reveal effect.500ms
fade-downOpacity + translateY downward. Good for dropdowns and menus.500ms
slide-leftOpacity + slide in from the right. Uses --vl-ease-out-soft.500ms
slide-rightOpacity + slide in from the left. Uses --vl-ease-out-soft.500ms
scale-inScales from 0.94 with a subtle upward nudge. Uses spring easing.500ms
blur-inCinematic blur (14px) dissolve entrance. Dramatic and modern.--vl-duration-slower (800ms)
reveal-3dPerspective rotateX entry — page flip feel.500ms
flow-inBlur + translate + scale — cinematic combination. Premium look.800ms
clip-riseClip-path reveal from bottom. Text feels like it emerges from the page.800ms
tilt-inPerspective rotateY entry. Great for cards arriving from the side.800ms
{preset.name}{preset.status}{preset.reducedMotion ?? "static"}
@@ -111,51 +104,56 @@ import Base from "../layouts/Base.astro"; Custom entrance </div> -

Live Demo

-
-
-
fade-up
-

Opacity + translate Y.

-
-
-
blur-in
-

Cinematic blur dissolve.

+

Live Demo — entrance

+ +
+
+
fade-up
+

Opacity + translate Y.

+
+
+
blur-in
+

Cinematic blur dissolve.

+
+
+
reveal-3d
+

Perspective rotateX entry.

+
-
-
reveal-3d
-

Perspective rotateX entry.

-
-
+ -

Interaction Effects

+

Interaction presets (vl-hover)

- Applied to interactive elements. These respond to user hover — no timeline needed. + Applied via vl-hover on interactive elements — no timeline needed. + {hoverPresets.length} presets in the generated catalog.

+ +
+
+
hover-lift
+

Lift + shadow

+
+
+
hover-glow
+

Primary glow ring

+
+ + underline-expand + +
+
- + - - - - - - - - - - - - - - - - - - - - + {hoverPresets.map((preset) => ( + + + + + + ))}
ValueTriggerDescription
ValueStatusReduced motion
hover-lift:hoverTranslates up 4px + floating shadow. Classic card hover.
hover-glow:hoverAdds a primary-colored glow shadow ring. Great for media embeds.
underline-expand:hoverExpanding underline grows from center. Elegant nav link effect.
icon-shift:hoverLast child (icon/arrow) shifts 4px on hover. Use on inline elements with an icon sibling.
{preset.name}{preset.status}{preset.reducedMotion ?? "static"}
@@ -177,6 +175,44 @@ import Base from "../layouts/Base.astro"; Touch devices: Hover effects automatically disable via @media (hover: none).

+

Scroll presets (vl-scroll)

+

+ Scroll-linked transforms. {scrollPresets.length} presets — usually paired with vl-timeline="view" unless the preset is inherently scroll-driven. +

+ + + + + + {scrollPresets.map((preset) => ( + + + + + + ))} + +
ValueStatusReduced motion
{preset.name}{preset.status}{preset.reducedMotion ?? "static"}
+ +

Loop presets (vl-loop / vl-loop-effect)

+

+ Continuous ambient motion. {loopPresets.length} presets — loops stop under prefers-reduced-motion. +

+ + + + + + {loopPresets.map((preset) => ( + + + + + + ))} + +
ValueStatusReduced motion
{preset.name}{preset.status}{preset.reducedMotion ?? "static"}
+

State Effects

Used with toggled elements like accordions, drawers, and panels. Typically paired with @@ -209,26 +245,8 @@ import Base from "../layouts/Base.astro";

Continuous / Ambient Effects

- Loop indefinitely. Use to add life to decorative elements, loading states, or ambient backgrounds. - Always respect prefers-reduced-motion — wrap with the media query if adding ambient - effects to content-critical elements. + See the generated loop preset table above. Examples below use legacy vl-effect — prefer vl-loop / vl-loop-effect for new markup.

- - - - - - - - - - - - - - -
ValueLoop durationDescription
shimmer2.5sBackground shimmer sweep. Use on skeleton loaders or accent lines. Requires background-size: 200% auto and a gradient background.
float5.5sGentle vertical bob. Great for hero illustrations.
glow-breathe3.5sPulsing box-shadow. Use on live indicators or accent dots.
wobble2.8sTilt oscillation (±3deg). Attention-grabbing but subtle.
orbit14sCircular orbit — full 360deg spin. For orbital icons or decorative rings.
rock4sWider tilt oscillation (±6deg). More playful than wobble.
morph8sOrganic border-radius morphing. Use on blobs and abstract shapes.
elastic-in0.7sSpringy scale-in entrance. One-shot spring pop, not a loop.
-
<!-- Floating hero illustration -->
 <img src="hero-visual.png" alt="" vl-effect="float" />
 
diff --git a/apps/docs/src/pages/hosts.astro b/apps/docs/src/pages/hosts.astro
index 20d0125..2c80aa6 100644
--- a/apps/docs/src/pages/hosts.astro
+++ b/apps/docs/src/pages/hosts.astro
@@ -1,43 +1,40 @@
 ---
 import Base from "../layouts/Base.astro";
+import DocsLive from "../components/DocsLive.astro";
 ---
 
-
+
   Interoperability
 
-  

Hosts (Tailwind & others)

+

Hosts (Tailwind, WordPress & others)

- Velora is a motion engine. Your host owns layout and visual styling. + Velora is a motion engine. Your host owns layout and visual styling — motion attributes still work.

+ +
+

+ Host utility classes +

+

No .vl-card required

+

+ Only vl-* attributes drive motion +

+
+
+

Minimal import

@import "@velora/css/motion-core";
-/* optional page transitions */
-@import "@velora/css/transitions";
- -

Do not require @velora/css/theme or component classes for scenes to work.

- -

Tailwind example

-
<section vl-scene vl-timeline="view" vl-pin="3" vl-scrub>
-  <div vl-stage class="min-h-svh grid place-items-center gap-6 px-6">
-    <p class="text-sm uppercase tracking-widest" vl-enter="fade-up" vl-act="1">Kicker</p>
-    <h1 class="text-5xl font-semibold" vl-enter="clip-rise" vl-act="1">Title</h1>
-    <img class="w-full max-w-3xl rounded-2xl" vl-scroll="media-zoom" vl-act="2" vl-span="2" alt="" />
-  </div>
-</section>
- -

- Repo proof: examples/tailwind-host/. Showcase proves the Velora cinematic UI; - examples prove the host-agnostic path. -

+@import "@velora/css/transitions"; /* optional */
-

Layers

+

WordPress

- Velora uses @layer velora.*. Keep host utilities outside or after those layers so - Tailwind wins on appearance while vl-* attributes still drive animation. + Same attributes in PHP templates, Gutenberg, or Elementor — + examples/wordpress/.

- -

Optional skin

-
@import "@velora/css/motion-core";
-@import "@velora/css/theme"; /* brand tokens + scene recipes */
diff --git a/apps/docs/src/pages/index.astro b/apps/docs/src/pages/index.astro index 661a47a..8b2cb6c 100644 --- a/apps/docs/src/pages/index.astro +++ b/apps/docs/src/pages/index.astro @@ -1,55 +1,87 @@ --- import Base from "../layouts/Base.astro"; +import DocsLive from "../components/DocsLive.astro"; +import { SITE, showcaseUrl } from "../lib/site"; --- - + Documentation

Getting Started

A declarative motion language for HTML. Attributes describe intent; CSS executes it. - Works with Tailwind, Relume, or any UI. Showcase is the optional Velora look - (Skins — named models via data-editorial-theme). - The public Showcase is six pages; the previous demos live in - archive/showcase-2026-08/. + Every section below includes a Test here frame — scroll or interact on the page.

-

Installation

-

Install the package from the npm registry:

-
pnpm add @velora/css
-# or
-npm install @velora/css
+ Declarative motion

'} + > +

Declarative motion

+

+ Same public API as Showcase — test in the frame, copy into your project. +

+
-

Choose an import

- - - - - - - - - - - - - - - - - - -
ImportUse when
@velora/css/motion-coreHost UI already provides look (Tailwind, custom DS). Motion + scene engine only.
@velora/css/themeOptional Velora visual tokens + named scene recipes.
@velora/css / fullConvenience: theme + components + motion (Showcase / docs style).
+

Explore

+
+ +
+

Attributes

+

Channels v2 grammar and scene engine controls.

+
+
+ +
+

Scenes

+

Track, stage, acts — pin and scrub without JS.

+
+
+ +
+

Compatibility

+

Stable vs experimental — observable truth layer.

+
+
+
-
/* Host-agnostic (recommended for Tailwind projects) */
-@import "@velora/css/motion-core";
-@import "@velora/css/transitions"; /* optional */
+  

Installation

+

+ @velora/css is developed in this monorepo. It is not published to npm yet — + we are finishing Showcase and documentation before release. +

+
# After npm publish (coming soon)
+pnpm add @velora/css
 
-/* Or full Velora skin */
-@import "@velora/css";
+# Today: monorepo workspace or local pack +pnpm --filter @velora/css build +pnpm pack:css
+

+ Canonical spec: + Attribute grammar + · Live catalog: + Motion catalog + · Playground: + Motion Playground +

Quick Start — scene engine

-

Track + stage + acts: one shared clock, pin, scrub — no JavaScript.

+

Track + stage + acts: one shared clock, pin, scrub — no animation runtime JavaScript.

+ + …'} + > +
+
+

Act 1

+

Scene clock

+

Scrub progress in the frame

+
+
+
+
<section vl-scene vl-timeline="view" vl-pin="3" vl-scrub>
   <div vl-stage>
     <p vl-enter="fade-up" vl-act="1">Kicker</p>
@@ -58,15 +90,11 @@ npm install @velora/css
</div> </section> -

- Style with your own classes. Motion does not require .vl-card or brand tokens. - See Hosts (Tailwind) and Scenes. -

-

What Velora is

diff --git a/apps/docs/src/pages/motion-conditions.astro b/apps/docs/src/pages/motion-conditions.astro index 2173c80..f3c76eb 100644 --- a/apps/docs/src/pages/motion-conditions.astro +++ b/apps/docs/src/pages/motion-conditions.astro @@ -1,5 +1,7 @@ --- import Base from "../layouts/Base.astro"; +import DocsLive from "../components/DocsLive.astro"; +import { SITE } from "../lib/site"; --- vl-motion — selects a motion mode for an element and its subtree. It does not name a preset; instead it re-scales the shared engine tokens that every preset already reads, so one attribute adapts timing, travel, depth, blur and easing - across the whole subtree. Zero JavaScript. + across the whole subtree. Zero animation runtime JavaScript.

The Modes

- Add vl-motion to any element. Descendant vl-effect / vl-enter + Add vl-motion to any element. Descendant vl-enter / vl-scroll reveals inherit the mode automatically.

@@ -49,10 +51,24 @@ import Base from "../layouts/Base.astro";
+

Test here — compare modes

+ …'}> +
+
+

cinematic

+

Grand & deep

+
+
+

subtle

+

Quiet & fast

+
+
+
+
<!-- One attribute governs the whole subtree -->
 <section vl-motion="cinematic">
   <h2 vl-enter="reveal-cinematic" vl-timeline="view">Grand & deep</h2>
-  <p vl-effect="fade-up" vl-timeline="view">Slower, deeper, blurred entrance.</p>
+  <p vl-enter="fade-up" vl-timeline="view">Slower, deeper, blurred entrance.</p>
 </section>
 
 <section vl-motion="subtle">...quieter, faster motion...</section>
@@ -144,6 +160,6 @@ import Base from "../layouts/Base.astro";
     The vl-motion modes themselves work in all modern browsers. The if()-based
     refinement requires a browser with CSS if() support; everywhere else the baseline modes
     are used. See the live comparison on the
-    Zero-JS Motion showcase page.
+    Zero-JS Motion showcase page.
   

diff --git a/apps/docs/src/pages/scenes.astro b/apps/docs/src/pages/scenes.astro index 7f1bfca..b4c91f0 100644 --- a/apps/docs/src/pages/scenes.astro +++ b/apps/docs/src/pages/scenes.astro @@ -1,5 +1,6 @@ --- import Base from "../layouts/Base.astro"; +import DocsLive from "../components/DocsLive.astro"; --- @@ -7,52 +8,60 @@ import Base from "../layouts/Base.astro";

Scenes

- A scene is a shared clock for children — not a look. Layout and chrome come from the host UI - (or optional Velora theme recipes). + A scene is a shared clock for children — not a look. Scroll the frame to scrub acts on the track.

-

Track + stage + acts

-
<section vl-scene vl-timeline="view" vl-pin="3" vl-scrub>
-  <div vl-stage>
-    <p vl-enter="fade-up" vl-act="1">Kicker</p>
-    <h1 vl-enter="clip-rise" vl-act="1">Title</h1>
-    <img vl-scroll="media-zoom" vl-act="2" vl-span="2" alt="" />
-    <a vl-enter="fade-up" vl-act="5">CTA</a>
-  </div>
-</section>
+ +
+

Kicker

+

Title

+
+`} + > +
+
+

Kicker · act 1

+

Title · act 1

+

CTA · act 3–4

+ Finale · act 5 +
+
+
+

Track + stage + acts

- + - - + - - - +
AttributeRole
vl-sceneTrack — owns --vl-scene view-timeline and optional pin height
vl-sceneTrack — owns --vl-scene and pin height
vl-stageSticky viewport stage (required for pin+scrub)
vl-timelineview (scroll film) or auto (time film)
vl-pinTrack height in viewport multiples (e.g. 3)
vl-pinTrack height in viewport multiples
vl-scrubLinear fill — progress glued to scroll
vl-actBeat index (same act = overlap / GSAP <)
vl-spanHow many beats the tween spans
vl-rangeEscape hatch — overrides act-derived ranges
vl-act / vl-spanBeat index and duration on the clock

Why track ≠ stage

- A sticky element freezes its own anonymous view() progress. Children bind to the - named track timeline (--vl-scene) while the stage stays pinned. -

- -

Named presets (skin recipes)

-

- Values like vl-scene="cinematic-hero" remain for compatibility. They ship in - scene-recipes.css via @velora/css/theme / full — they mix look + motion. - Prefer track/stage/acts for portable hosts. + Children bind to the named track timeline (--vl-scene) while + [vl-stage] stays pinned in the scrollport.

Channels on children

-

- Use existing channels on stage children: vl-enter, vl-scroll, - vl-exit, etc. The scene clock drives when they play. -

+ +
+
+

vl-enter

+

On stage child

+
+
+

vl-scroll

+

Scroll channel

+
+
+
diff --git a/apps/docs/src/pages/skills.astro b/apps/docs/src/pages/skills.astro index 66ad0ae..1f49fd9 100644 --- a/apps/docs/src/pages/skills.astro +++ b/apps/docs/src/pages/skills.astro @@ -1,5 +1,6 @@ --- import Base from "../layouts/Base.astro"; +import DocsLive from "../components/DocsLive.astro"; --- @@ -7,18 +8,24 @@ import Base from "../layouts/Base.astro";

Agent Skill Sets

- These are project-specific operating profiles for AI coding agents — Cursor, GitHub Copilot, and - similar tools. Each skill set defines a mission, scope, allowed and forbidden actions, and ideal - prompts. Use them individually or in sequence. + Operating profiles for AI agents. Validate motion against live Test here frames + on Attributes, Effects, and Scenes.

+ +
+

No animation runtime JS

+

CSS-only motion in every public demo

+
+
+

Core Rule Shared by All Skill Sets

  • Semantic HTML first
  • Modern CSS as the primary rendering engine
  • -
  • Zero JavaScript for animation logic
  • +
  • Zero animation runtime JavaScript for motion logic
  • Minimal JavaScript only for progressive enhancement or accessibility where strictly needed
  • No React, Vue, GSAP, Framer Motion, Locomotive Scroll, Swup, or equivalent runtime in core
  • Strict namespace: .vl-*, --vl-*, vl-*
  • @@ -63,11 +70,11 @@ import Base from "../layouts/Base.astro";

    Motion Systems Engineer

    Builds and refines Velora's motion primitives and choreography system.

    -

    Scope: vl-effect, vl-timeline, vl-range, vl-scrub, motion tokens, reveal systems, hover systems, stagger systems.

    +

    Scope: vl-enter, vl-scroll, vl-hover, legacy vl-effect, vl-timeline, vl-range, vl-scrub, motion tokens, reveal systems, hover systems, stagger systems.

    Allowed: add new motion presets, refactor motion declarations into variables/tokens, create scroll-linked CSS patterns, define primitive vs scene motion distinctions.

    Forbidden: using JS to drive animations that modern CSS can handle, mixing scene orchestration into primitive effect definitions without need, introducing unregistered effect names casually.

    Ideal prompt

    -
    Implement vl-effect="flow-in" and vl-timeline="view" using layered CSS only. No JS. Use tokenized values and reduced-motion support.
    +
    Implement vl-enter="flow-in" and vl-timeline="view" using layered CSS only. No animation runtime JS. Use tokenized values and reduced-motion support.

    diff --git a/apps/docs/src/pages/timelines.astro b/apps/docs/src/pages/timelines.astro index c789373..252f246 100644 --- a/apps/docs/src/pages/timelines.astro +++ b/apps/docs/src/pages/timelines.astro @@ -1,5 +1,6 @@ --- import Base from "../layouts/Base.astro"; +import DocsLive from "../components/DocsLive.astro"; --- @@ -7,44 +8,52 @@ import Base from "../layouts/Base.astro";

    Timelines

    - Timelines decide what drives progress. Effects decide what moves. - The scene engine shares one clock across stage children. + Timelines decide what drives progress. Channels decide what moves. + Test each model in the frames below.

    -

    vl-timeline values

    +

    vl-timeline="view"

    + +

    View progress

    +

    + Linked to visibility in the scrollport +

    +
    + +

    vl-timeline="auto"

    + +

    Auto timeline

    +

    Time-based, no scroll needed

    +
    + +

    vl-timeline="hover"

    + +
    +

    Hover to play

    +

    Progress gated by intent

    +
    +
    + +

    Scene clock + scrub

    + +
    +
    +

    Shared clock

    +

    Scrub acts

    +
    +
    +
    + +

    Reference

    - - + + - +
    ValueBehavior
    viewProgress from element / scene visibility (view() or named --vl-scene)
    scrollProgress from root scroll
    viewElement / scene visibility (view() or --vl-scene)
    scrollRoot scroll progress
    autoTime-based CSS animation
    hoverPaused until hover
    hoverPaused until hover / focus
    - -

    Scene clock

    -

    - On [vl-scene][vl-timeline="view"], stage children use - animation-timeline: --vl-scene and ranges derived from - vl-act / vl-span / --vl-beats (default 8). -

    -

    - With vl-timeline="auto", the same acts map to - animation-delay: (act - 1) * --vl-beat. Pin/scrub are no-ops. -

    - -

    Ranges

    -

    - Presets via vl-range (entry, entry-short, cover, …) - still apply to standalone elements. Inside a pinned scene, prefer acts; use - vl-range only as an escape hatch. -

    - -

    Scrub

    -
    <section vl-scene vl-timeline="view" vl-pin="3" vl-scrub>
    -  …
    -</section>
    -

    vl-scrub forces linear timing and fill both on scene-driven children.

    diff --git a/apps/docs/src/pages/tokens.astro b/apps/docs/src/pages/tokens.astro index 4193a24..ff4aa3b 100644 --- a/apps/docs/src/pages/tokens.astro +++ b/apps/docs/src/pages/tokens.astro @@ -1,5 +1,6 @@ --- import Base from "../layouts/Base.astro"; +import DocsLive from "../components/DocsLive.astro"; --- @@ -13,6 +14,18 @@ import Base from "../layouts/Base.astro";

    Color System

    + +
    + --vl-color-primary + --vl-bg-surface + --vl-text-muted +
    +
    +

    Skin-driven

    +

    Noir · Earth · Aethel · Meridian

    +
    +
    +

    Brand Palette

    Velora's brand uses a natural, earthy palette built on hex values for precision:

    @@ -279,7 +292,7 @@ import Base from "../layouts/Base.astro";
    <!-- Override per element -->
     <div
    -  vl-effect="fade-up"
    +  vl-enter="fade-up"
       vl-timeline="view"
       style="--vl-motion-distance: 2rem; --vl-motion-duration: 600ms;">
       Custom depth and speed
    diff --git a/apps/docs/src/pages/transitions.astro b/apps/docs/src/pages/transitions.astro
    index e56bad7..41533fd 100644
    --- a/apps/docs/src/pages/transitions.astro
    +++ b/apps/docs/src/pages/transitions.astro
    @@ -1,5 +1,6 @@
     ---
     import Base from "../layouts/Base.astro";
    +import DocsLive from "../components/DocsLive.astro";
     ---
     
     
    @@ -7,10 +8,27 @@ import Base from "../layouts/Base.astro";
     
       

    Page Transitions

    - Native cross-document View Transitions — no router, no JavaScript. Just CSS and the - vl-page-transition attribute. + Native cross-document View Transitions — no animation runtime JavaScript. CSS and the + vl-page-transition attribute only.

    +

    Test here — page transitions

    + +

    Cross-document demo

    +

    + This site runs vl-page-transition on <html>. + Jump to Attributes or Effects to see the preset. +

    + +
    +

    How to Enable

    View Transitions require the browser's navigation opt-in. Velora ships this in its main CSS entry: diff --git a/apps/docs/src/styles/docs-shell.css b/apps/docs/src/styles/docs-shell.css new file mode 100644 index 0000000..a9a7fc7 --- /dev/null +++ b/apps/docs/src/styles/docs-shell.css @@ -0,0 +1,663 @@ +/** + * Velora Docs shell — Skins-aware layout on top of @velora/css full bundle. + * Uses editorial tokens (data-editorial-theme) + shared DS chrome patterns. + */ + +.docs-page-shell { + --docs-gutter: clamp(1rem, 3vw, 2rem); + --docs-sidebar-width: 16.5rem; + --docs-content-max: min(48rem, 100%); + background: var(--vl-bg-main); + color: var(--vl-text-primary); + scroll-padding-top: 5rem; +} + +/* Header skin controls (aligned with Showcase) */ +.ds-vt-select, +.ds-editorial-select { + appearance: none; + background: color-mix(in oklch, var(--vl-bg-surface) 60%, transparent); + border: 1px solid color-mix(in oklch, var(--vl-border-subtle) 70%, transparent); + border-radius: var(--vl-radius-sm); + color: var(--vl-text-secondary); + font-family: var(--vl-font-family-mono, monospace); + font-size: 0.7rem; + letter-spacing: 0.04em; + padding: 0.3rem 1.6rem 0.3rem 0.6rem; + cursor: pointer; + transition: + border-color var(--vl-transition-fast), + color var(--vl-transition-fast); + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%237f8668' stroke-width='1.5' fill='none' stroke-linecap='round'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 0.5rem center; +} + +.ds-editorial-select { + max-width: 7.5rem; +} + +.ds-vt-select:hover, +.ds-vt-select:focus-visible, +.ds-editorial-select:hover, +.ds-editorial-select:focus-visible { + border-color: var(--vl-color-primary); + color: var(--vl-text-primary); + outline: none; +} + +.ds-theme-toggle { + display: flex; + align-items: center; + gap: 1px; + background: color-mix(in oklch, var(--vl-bg-surface) 55%, transparent); + border: 1px solid color-mix(in oklch, var(--vl-border-subtle) 60%, transparent); + border-radius: var(--vl-radius-sm); + padding: 2px; +} + +.ds-theme-btn { + background: transparent; + border: none; + border-radius: calc(var(--vl-radius-sm) - 2px); + color: var(--vl-text-muted); + cursor: pointer; + font-size: 0.75rem; + line-height: 1; + padding: 0.25rem 0.4rem; + transition: + background var(--vl-transition-fast), + color var(--vl-transition-fast); +} + +.ds-theme-btn:hover { + background: color-mix(in oklch, var(--vl-color-primary) 15%, transparent); + color: var(--vl-text-primary); +} + +.ds-theme-btn.is-active { + background: var(--vl-color-primary); + color: var(--vl-bg-main); +} + +.ds-theme-btn:focus-visible { + outline: 2px solid var(--vl-focus-ring-color); + outline-offset: 1px; +} + +.docs-page-shell .vl-header__brand img { + block-size: 1.35rem; + inline-size: auto; +} + +html:not([data-theme="light"]) .docs-page-shell .vl-header__brand img { + filter: brightness(0) invert(1); + opacity: 0.92; +} + +html[data-editorial-theme="earth"] .docs-page-shell .vl-header__brand img { + filter: none; + opacity: 1; +} + +.docs-header-link { + display: inline-flex; + align-items: center; + min-height: 2rem; + padding: 0.35rem 0.75rem; + border: 1px solid color-mix(in oklch, var(--vl-border-subtle) 70%, transparent); + border-radius: var(--vl-radius-sm); + color: var(--vl-text-secondary); + font-size: var(--vl-font-size-xs); + font-weight: var(--vl-font-weight-semibold); + letter-spacing: 0.06em; + text-decoration: none; + text-transform: uppercase; + transition: + border-color var(--vl-transition-fast), + color var(--vl-transition-fast), + background var(--vl-transition-fast); +} + +.docs-header-link:hover { + border-color: color-mix(in oklch, var(--vl-color-primary) 44%, var(--vl-border-subtle)); + color: var(--vl-text-primary); + background: color-mix(in oklch, var(--vl-color-primary) 8%, transparent); +} + +/* Layout */ +.docs-layout { + display: grid; + grid-template-columns: var(--docs-sidebar-width) minmax(0, 1fr); + gap: clamp(1rem, 2.5vw, 2rem); + max-width: min(112rem, calc(100vw - (var(--docs-gutter) * 2))); + margin-inline: auto; + padding: var(--vl-space-lg) var(--docs-gutter) var(--vl-space-3xl); +} + +.docs-sidebar { + position: sticky; + top: 5.5rem; + align-self: start; + max-height: calc(100dvh - 6.5rem); + overflow: auto; + padding: var(--vl-space-md); + border-radius: var(--vl-radius-lg); +} + +.docs-sidebar nav { + display: flex; + flex-direction: column; + gap: 2px; +} + +.docs-nav-section { + font-size: var(--vl-font-size-xs); + font-weight: var(--vl-font-weight-semibold); + text-transform: uppercase; + letter-spacing: var(--vl-tracking-display); + color: var(--vl-text-muted); + padding-inline: var(--vl-space-sm); + margin-block: var(--vl-space-md) var(--vl-space-2xs); +} + +.docs-nav-section:first-of-type { + margin-top: 0; +} + +.docs-sidebar a { + display: block; + padding: var(--vl-space-xs) var(--vl-space-sm); + border-radius: var(--vl-radius-md); + color: var(--vl-text-muted); + text-decoration: none; + font-size: var(--vl-font-size-sm); + font-weight: var(--vl-font-weight-medium); + transition: + background var(--vl-transition-fast), + color var(--vl-transition-fast); +} + +.docs-sidebar a:hover { + background: color-mix(in oklch, var(--vl-color-primary) 10%, transparent); + color: var(--vl-text-secondary); +} + +.docs-sidebar a[aria-current="page"] { + background: color-mix(in oklch, var(--vl-color-primary) 14%, transparent); + color: var(--vl-color-primary); + font-weight: var(--vl-font-weight-semibold); +} + +.docs-main { + min-width: 0; + max-width: var(--docs-content-max); +} + +.docs-main > :first-child { + margin-top: 0; +} + +.docs-main > :where(p, ul, ol, blockquote) { + max-width: var(--vl-measure-prose, 65ch); +} + +/* Typography — showcase-ref rhythm, docs scale */ +.docs-page-title { + margin-top: var(--vl-space-sm); + margin-bottom: var(--vl-space-md); + font-family: var(--vl-font-family-display); + font-size: clamp(2rem, 4.5vw, var(--vl-font-size-4xl)); + font-weight: var(--vl-font-weight-bold); + letter-spacing: var(--vl-tracking-tight); + line-height: var(--vl-leading-tight); + color: var(--vl-text-primary); + text-wrap: balance; +} + +.docs-page-lead { + font-size: var(--vl-font-size-lg); + color: var(--vl-text-secondary); + line-height: var(--vl-leading-relaxed); + margin-bottom: var(--vl-space-xl); +} + +.docs-page-lead--compact { + font-size: var(--vl-font-size-md); +} + +.docs-page-lead--narrow { + max-width: 44ch; +} + +.docs-link-primary { + color: var(--vl-color-primary); + text-underline-offset: 0.2em; +} + +.docs-main h2 { + margin-block: var(--vl-space-3xl) var(--vl-space-md); + padding: var(--vl-space-sm) var(--vl-space-md); + border: 1px solid color-mix(in oklch, var(--vl-border-subtle) 72%, transparent); + border-radius: var(--vl-radius-lg); + background: + linear-gradient( + 180deg, + color-mix(in oklch, var(--vl-bg-surface-elevated) 74%, transparent), + color-mix(in oklch, var(--vl-bg-surface) 94%, transparent) + ); + box-shadow: var(--vl-shadow-sm); + font-family: var(--vl-font-family-display); + font-size: var(--vl-font-size-2xl); + font-weight: var(--vl-font-weight-semibold); + letter-spacing: var(--vl-tracking-tight); + color: var(--vl-text-primary); + scroll-margin-top: 6rem; +} + +.docs-main h3 { + margin-block: var(--vl-space-lg) var(--vl-space-sm); + font-family: var(--vl-font-family-display); + font-size: var(--vl-font-size-lg); + font-weight: var(--vl-font-weight-semibold); + color: var(--vl-text-primary); +} + +.docs-main h4 { + margin-block: var(--vl-space-md) var(--vl-space-xs); + font-size: var(--vl-font-size-sm); + font-weight: var(--vl-font-weight-semibold); + letter-spacing: var(--vl-tracking-wide); + text-transform: uppercase; + color: var(--vl-text-muted); +} + +.docs-main p { + color: var(--vl-text-secondary); + line-height: var(--vl-leading-relaxed); + margin-bottom: var(--vl-space-md); + text-wrap: pretty; +} + +.docs-main a { + color: color-mix(in oklch, var(--vl-color-primary) 86%, white 8%); + text-underline-offset: 0.2em; +} + +.docs-main a:hover { + color: var(--vl-color-primary); +} + +.docs-main ul, +.docs-main ol { + color: var(--vl-text-secondary); + line-height: var(--vl-leading-relaxed); + padding-left: var(--vl-space-lg); + margin-bottom: var(--vl-space-md); +} + +.docs-main li { + margin-bottom: var(--vl-space-xs); +} + +.docs-main code { + font-family: var(--vl-font-family-mono); + font-size: 0.85em; + background: color-mix(in oklch, var(--vl-color-primary) 12%, var(--vl-bg-surface)); + color: var(--vl-color-primary); + padding: 0.1em 0.4em; + border-radius: var(--vl-radius-sm); + border: 1px solid color-mix(in oklch, var(--vl-color-primary) 20%, transparent); +} + +.docs-main pre { + background: var(--vl-bg-inset); + border: 1px solid color-mix(in oklch, var(--vl-border-subtle) 72%, transparent); + border-radius: var(--vl-radius-lg); + padding: var(--vl-space-lg); + overflow-x: auto; + margin-block: var(--vl-space-lg); + box-shadow: var(--vl-shadow-sm); +} + +.docs-main pre code { + background: none; + border: none; + color: var(--vl-text-secondary); + padding: 0; + font-size: var(--vl-font-size-sm); + line-height: 1.8; +} + +.docs-main td code { + font-size: 0.85em; +} + +.docs-catalog-panel { + margin-block: var(--vl-space-xl); + padding: var(--vl-space-lg); + border: 1px solid color-mix(in oklch, var(--vl-border-subtle) 72%, transparent); + border-radius: var(--vl-radius-xl); + background: color-mix(in oklch, var(--vl-bg-surface) 92%, transparent); + box-shadow: var(--vl-shadow-sm); +} + +.docs-catalog-panel__kicker { + margin: 0 0 var(--vl-space-sm); + font-family: var(--vl-font-family-mono, monospace); + font-size: var(--vl-font-size-xs); + letter-spacing: var(--vl-tracking-wide); + text-transform: uppercase; + color: var(--vl-text-muted); +} + +.docs-catalog-panel__channels { + margin: var(--vl-space-sm) 0 0; + font-size: var(--vl-font-size-sm); + color: var(--vl-text-secondary); + line-height: 1.6; +} + +.docs-catalog-stats { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: var(--vl-space-sm); +} + +.docs-catalog-stat { + display: grid; + gap: 0.15rem; + padding: var(--vl-space-sm) var(--vl-space-md); + border-radius: var(--vl-radius-lg); + border: 1px solid color-mix(in oklch, var(--vl-border-subtle) 65%, transparent); + background: var(--vl-bg-inset); +} + +.docs-catalog-stat strong { + font-family: var(--vl-font-family-display); + font-size: clamp(1.35rem, 3vw, 1.75rem); + line-height: 1; + color: var(--vl-text-primary); +} + +.docs-catalog-stat span { + font-size: var(--vl-font-size-xs); + color: var(--vl-text-muted); + text-transform: uppercase; + letter-spacing: var(--vl-tracking-wide); +} + +.docs-main table { + width: 100%; + border-collapse: collapse; + margin-block: var(--vl-space-lg); + font-size: var(--vl-font-size-sm); + border: 1px solid color-mix(in oklch, var(--vl-border-subtle) 72%, transparent); + border-radius: var(--vl-radius-lg); + overflow: hidden; + background: color-mix(in oklch, var(--vl-bg-surface) 90%, transparent); + box-shadow: var(--vl-shadow-sm); +} + +.docs-main th { + text-align: left; + padding: var(--vl-space-sm) var(--vl-space-md); + font-weight: var(--vl-font-weight-semibold); + font-size: var(--vl-font-size-xs); + text-transform: uppercase; + letter-spacing: var(--vl-tracking-wide); + color: var(--vl-text-muted); + background: var(--vl-bg-inset); + border-bottom: 1px solid var(--vl-border-subtle); +} + +.docs-main td { + padding: var(--vl-space-sm) var(--vl-space-md); + border-bottom: 1px solid color-mix(in oklch, var(--vl-border-subtle) 50%, transparent); + color: var(--vl-text-secondary); + vertical-align: top; +} + +.docs-main tbody tr:nth-child(even) { + background: color-mix(in oklch, var(--vl-bg-inset) 28%, transparent); +} + +.docs-main tr:last-child td { + border-bottom: none; +} + +.docs-main blockquote { + border-left: 3px solid var(--vl-color-primary); + padding: var(--vl-space-md) var(--vl-space-lg); + background: color-mix(in oklch, var(--vl-color-primary) 6%, transparent); + border-radius: 0 var(--vl-radius-md) var(--vl-radius-md) 0; + color: var(--vl-text-secondary); + margin-block: var(--vl-space-lg); + font-style: italic; +} + +.docs-main hr { + border: none; + border-top: 1px solid var(--vl-border-subtle); + margin-block: var(--vl-space-2xl); +} + +.docs-grid-cards { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(14rem, 1fr)); + gap: var(--vl-space-md); + margin-top: var(--vl-space-lg); +} + +.docs-grid-3 { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: var(--vl-space-md); +} + +.docs-section-block { + margin-block: var(--vl-space-lg); +} + +.docs-card-link { + text-decoration: none; + display: block; + color: inherit; +} + +.docs-swatch { + display: inline-block; + width: 1rem; + height: 1rem; + border-radius: var(--vl-radius-sm); + vertical-align: middle; +} + +@media (max-width: 48rem) { + .docs-layout { + grid-template-columns: 1fr; + } + + .docs-grid-3 { + grid-template-columns: 1fr; + } + + .docs-sidebar { + position: static; + max-height: none; + } + + .docs-page-shell .vl-header__actions .ds-vt-select, + .docs-page-shell .vl-header__actions .ds-editorial-select, + .docs-page-shell .vl-header__actions .ds-theme-toggle { + display: none; + } +} + +/* ----------------------------------------------------------------------- + Live demos — scrollport drives view timelines inside the docs page + ----------------------------------------------------------------------- */ +.docs-live { + margin-block: var(--vl-space-lg); + border: 1px solid color-mix(in oklch, var(--vl-border-subtle) 72%, transparent); + border-radius: var(--vl-radius-lg); + background: + linear-gradient( + 180deg, + color-mix(in oklch, var(--vl-bg-surface-elevated) 70%, transparent), + color-mix(in oklch, var(--vl-bg-surface) 94%, transparent) + ); + box-shadow: var(--vl-shadow-sm); + overflow: hidden; +} + +.docs-live__bar { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--vl-space-sm); + padding: var(--vl-space-sm) var(--vl-space-md); + border-bottom: 1px solid color-mix(in oklch, var(--vl-border-subtle) 55%, transparent); + background: color-mix(in oklch, var(--vl-bg-inset) 40%, transparent); +} + +.docs-live__kicker { + margin: 0; +} + +.docs-live__replay { + cursor: pointer; + border: 1px solid color-mix(in oklch, var(--vl-color-primary) 35%, transparent); + background: color-mix(in oklch, var(--vl-color-primary) 10%, transparent); + color: var(--vl-color-primary); + font-family: var(--vl-font-family-mono); + font-size: var(--vl-font-size-xs); + letter-spacing: 0.06em; + text-transform: uppercase; +} + +.docs-live__replay:hover { + background: color-mix(in oklch, var(--vl-color-primary) 18%, transparent); +} + +.docs-live__hint { + margin: 0; + padding: var(--vl-space-xs) var(--vl-space-md); + font-size: var(--vl-font-size-xs); + color: var(--vl-text-muted); + border-bottom: 1px solid color-mix(in oklch, var(--vl-border-subtle) 40%, transparent); +} + +.docs-live__scroll { + max-height: 16rem; + overflow: auto; + overscroll-behavior: contain; + scroll-behavior: smooth; + background: var(--vl-bg-inset); +} + +.docs-live--tall .docs-live__scroll { + max-height: 22rem; +} + +.docs-live--scene .docs-live__scroll { + max-height: 26rem; +} + +.docs-live__pad { + block-size: 7rem; + pointer-events: none; +} + +.docs-live--tall .docs-live__pad { + block-size: 9rem; +} + +.docs-live--scene .docs-live__pad--top { + block-size: 3rem; +} + +.docs-live--scene .docs-live__pad--bottom { + block-size: 14rem; +} + +.docs-live__content { + padding: var(--vl-space-md); + display: grid; + gap: var(--vl-space-md); + place-items: center; + min-height: 8rem; +} + +.docs-live__stage { + padding: var(--vl-space-lg); + display: grid; + gap: var(--vl-space-md); + place-items: center; + min-height: 10rem; + background: var(--vl-bg-inset); +} + +.docs-live__code { + margin: 0; + border-radius: 0; + border: none; + border-top: 1px solid color-mix(in oklch, var(--vl-border-subtle) 55%, transparent); + font-size: var(--vl-font-size-xs); +} + +.docs-live .vl-card { + inline-size: min(100%, 14rem); +} + +.docs-live__scene-track { + inline-size: 100%; + min-block-size: 140%; +} + +.docs-live__scene-track [vl-stage] { + min-block-size: 12rem; + display: grid; + place-items: center; + gap: var(--vl-space-sm); + padding: var(--vl-space-md); + text-align: center; +} + +.docs-live__demo-title { + margin: 0; + font-family: var(--vl-font-family-display); + font-size: var(--vl-font-size-xl); + font-weight: var(--vl-font-weight-bold); +} + +.docs-live__demo-copy { + margin: 0; + color: var(--vl-text-secondary); + font-size: var(--vl-font-size-sm); +} + +.docs-live__demo-chip { + display: inline-flex; + padding: 0.35rem 0.75rem; + border-radius: var(--vl-radius-full); + border: 1px solid color-mix(in oklch, var(--vl-color-primary) 30%, transparent); + font-size: var(--vl-font-size-xs); + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--vl-text-muted); +} + +.docs-live__row { + display: flex; + flex-wrap: wrap; + gap: var(--vl-space-md); + justify-content: center; + inline-size: 100%; +} + +@media (max-width: 48rem) { + .docs-live__scroll { + max-height: 14rem; + } +} diff --git a/apps/docs/vercel.json b/apps/docs/vercel.json new file mode 100644 index 0000000..bb67885 --- /dev/null +++ b/apps/docs/vercel.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://openapi.vercel.sh/vercel.json", + "installCommand": "pnpm install", + "buildCommand": "pnpm generate:catalog && pnpm --filter docs build", + "outputDirectory": "dist", + "framework": "astro" +} diff --git a/apps/showcase/config/showcase-coverage.mjs b/apps/showcase/config/showcase-coverage.mjs index 4d29ff2..b37d568 100644 --- a/apps/showcase/config/showcase-coverage.mjs +++ b/apps/showcase/config/showcase-coverage.mjs @@ -28,7 +28,22 @@ export const SHOWCASE_COVERAGE = { "pages/motion/api-motion-catalog.html": { name: "Catalog", role: "Provide a concise public decision guide for channels, scenes, timelines, support and fallbacks.", - signature: "Intent-first reference with canonical recipes and a separate exhaustive Motion Lab", + signature: "Intent-first reference with canonical recipes, generated preset registry and a separate exhaustive Motion Lab", + }, + "pages/core/compatibility.html": { + name: "Compatibility", + role: "Document primitive support tiers, browser matrix, fallbacks and reduced-motion policy.", + signature: "Generated compatibility matrix linked to packages/catalog", + }, + "pages/core/benchmarks.html": { + name: "Benchmarks", + role: "Publish reproducible integration evidence — not unverified FPS marketing.", + signature: "Objective metrics table, methodology draft and automated bench roadmap", + }, + "pages/motion/playground.html": { + name: "Playground", + role: "Compose public vl-* attributes from the registry, preview CSS-only motion, copy markup.", + signature: "Playground UI uses JavaScript; preview motion uses Velora/CSS", }, }, componentGroups: { diff --git a/apps/showcase/config/template-registry.mjs b/apps/showcase/config/template-registry.mjs index 0d1e6e6..a74476d 100644 --- a/apps/showcase/config/template-registry.mjs +++ b/apps/showcase/config/template-registry.mjs @@ -22,8 +22,11 @@ export const TEMPLATE_REGISTRY = { ], secondary: [ "pages/core/hosts.html", + "pages/core/compatibility.html", + "pages/core/benchmarks.html", "pages/core/archive.html", "pages/motion/motion-lab.html", + "pages/motion/playground.html", ], }, }, diff --git a/apps/showcase/index.html b/apps/showcase/index.html index 68fda6a..245ac40 100644 --- a/apps/showcase/index.html +++ b/apps/showcase/index.html @@ -21,7 +21,7 @@ - + Skip to content

- + @@ -159,18 +159,19 @@
@@ -425,16 +426,16 @@

Frame. Cue. Cut.

aria-label="Shared scene clock" >
-

Act 02 · one shared clock

-

Five beats, one release decision. The Core controls time; the Skin controls atmosphere.

+

Act 02 · pin + scrub teaching clock

+

Scroll slowly through this section. The stage pins; each card is a beat on the shared CSS clock (vl-scene + vl-act).

-

Context · Release 07

-

A signal enters the room.

-

Evidence arrives second: 72% team alignment.

-

The evidence holds while the stage stays pinned and the story advances.

-

The decision lands last: ship the cinematic voice.

+

Beat 01 · frame

+

Pin the stage. Keep context.

+

vl-pin="2" holds the viewport while scroll advances --vl-scene.

+

Beat 03 · cue — descendants use vl-act / vl-enter, not nested view gates on the stage.

+

Beat 05 · cut — open the full scene timeline with five acts.

@@ -453,8 +454,8 @@

- Channel: enter. Type scale and reveal are engine; the font stack is a Skin. Copy from Catalog. - See catalog. + Scroll this section — vl-enter="clip-rise" + vl-range tune the view window; vl-effect="gradient-text-color" on the hero word. + Catalog → enter.

@@ -691,8 +692,9 @@

One motion grammar. Four moments users can feel.

- The attributes below are not decorative samples. Together they stage discovery, evidence, interaction and atmosphere — directly in semantic HTML. + Four live demos below — scroll into each, hover the fan, watch the grid wave. Every attribute is copyable from the Catalog.

+

Interact first: path follows scroll · fan responds to hover · grid-wave orchestrates children · loops respect reduced motion.

@@ -809,7 +811,7 @@

Timeline · Skins.

-

+

Hover each card — preview uses the attribute on the chip below.

@@ -840,10 +842,10 @@

One scene language across every page.

-

Scene 01

+

Scene 01 · pin+scrub

Hero Reveal

-

Launch-value surfaces with staged copy, brand media, CTAs, and immediate narrative focus.

- vl-scene="cinematic-hero" +

Pin the stage, scrub acts — vl-act choreographs title, evidence and handoff on one view clock.

+ vl-scene + vl-pin + vl-act
@@ -232,6 +232,30 @@

Test every preset. Prote

+
+
+

Registry inspector

+

Every public preset — metadata from packages/catalog/

+

Channel, status, HTML usage, primitives, fallback, reduced motion and source. Click a demo card or browse the list.

+
+
+ +
+
+
+

Preview uses the public Velora API only.

+
+
+
+
+
+
-
123known presets
123demonstrated here
0untracked gaps

Every card should state intent, channel, support level, reduced-motion behavior and where the recipe is proven in the Showcase.

+
registry presets
demonstrated on page
not yet demo'd

Counts are generated from presets-full.json — never hardcoded. Compatibility matrix · Public registry

@@ -992,7 +1016,18 @@

Ranges, Speed and Runtime Params

- + +

+
* + * + * Channels must be direct children of [vl-stage]. vl-range overrides + * act-derived animation-range only — the named --vl-scene clock stays. */ @layer velora.motion { /* --- Track: named view timeline + optional pin height ----------------- */ @@ -18,7 +21,8 @@ isolation: isolate; container-type: inline-size; container-name: vl-scene; - contain: layout style; + /* style-only: layout containment can break sticky [vl-stage] */ + contain: style; view-timeline-name: --vl-scene; view-timeline-axis: block; timeline-scope: --vl-scene; @@ -32,21 +36,28 @@ --vl-clock: auto; } - /* Baseline pin height (boolean or missing typed attr) */ - [vl-scene][vl-pin] { + /* + * Pin height only on view-clock tracks. On auto clock, vl-pin is a no-op + * (spec: pin/scrub ignored for time film). + */ + [vl-scene][vl-timeline="view"][vl-pin] { --vl-pin-factor: 1; min-block-size: calc(var(--vl-pin-factor) * 100svh); } - [vl-scene][vl-pin="1"] { --vl-pin-factor: 1; } - [vl-scene][vl-pin="2"] { --vl-pin-factor: 2; } - [vl-scene][vl-pin="3"] { --vl-pin-factor: 3; } - [vl-scene][vl-pin="4"] { --vl-pin-factor: 4; } - [vl-scene][vl-pin="5"] { --vl-pin-factor: 5; } - [vl-scene][vl-pin="6"] { --vl-pin-factor: 6; } + [vl-scene][vl-timeline="view"][vl-pin="1"] { --vl-pin-factor: 1; } + [vl-scene][vl-timeline="view"][vl-pin="2"] { --vl-pin-factor: 2; } + [vl-scene][vl-timeline="view"][vl-pin="3"] { --vl-pin-factor: 3; } + [vl-scene][vl-timeline="view"][vl-pin="4"] { --vl-pin-factor: 4; } + [vl-scene][vl-timeline="view"][vl-pin="5"] { --vl-pin-factor: 5; } + [vl-scene][vl-timeline="view"][vl-pin="6"] { --vl-pin-factor: 6; } + [vl-scene][vl-timeline="view"][vl-pin="7"] { --vl-pin-factor: 7; } + [vl-scene][vl-timeline="view"][vl-pin="8"] { --vl-pin-factor: 8; } + [vl-scene][vl-timeline="view"][vl-pin="9"] { --vl-pin-factor: 9; } + [vl-scene][vl-timeline="view"][vl-pin="10"] { --vl-pin-factor: 10; } @supports (min-block-size: attr(vl-pin type())) { - [vl-scene][vl-pin] { + [vl-scene][vl-timeline="view"][vl-pin] { --vl-pin-factor: attr(vl-pin type(), 1); min-block-size: calc(var(--vl-pin-factor) * 100svh); } @@ -78,6 +89,10 @@ [vl-stage] > *:nth-child(6) { --vl-act: 6; } [vl-stage] > *:nth-child(7) { --vl-act: 7; } [vl-stage] > *:nth-child(8) { --vl-act: 8; } + [vl-stage] > *:nth-child(9) { --vl-act: 9; } + [vl-stage] > *:nth-child(10) { --vl-act: 10; } + [vl-stage] > *:nth-child(11) { --vl-act: 11; } + [vl-stage] > *:nth-child(12) { --vl-act: 12; } [vl-stage] > *[vl-act="1"] { --vl-act: 1; } [vl-stage] > *[vl-act="2"] { --vl-act: 2; } @@ -87,6 +102,10 @@ [vl-stage] > *[vl-act="6"] { --vl-act: 6; } [vl-stage] > *[vl-act="7"] { --vl-act: 7; } [vl-stage] > *[vl-act="8"] { --vl-act: 8; } + [vl-stage] > *[vl-act="9"] { --vl-act: 9; } + [vl-stage] > *[vl-act="10"] { --vl-act: 10; } + [vl-stage] > *[vl-act="11"] { --vl-act: 11; } + [vl-stage] > *[vl-act="12"] { --vl-act: 12; } [vl-stage] > *[vl-span="1"] { --vl-span: 1; } [vl-stage] > *[vl-span="2"] { --vl-span: 2; } @@ -109,41 +128,56 @@ } } - /* View clock: bind stage children to the track timeline */ - [vl-scene][vl-timeline="view"] [vl-stage] > :is([vl-enter], [vl-scroll], [vl-effect], [vl-exit]):not([vl-range]) { + /* + * View clock: always bind to the named track timeline (including vl-range). + * Act-derived range applies only when vl-range is absent (escape hatch). + */ + [vl-scene][vl-timeline="view"] [vl-stage] > :is([vl-enter], [vl-scroll], [vl-effect], [vl-exit]) { + --vl-scroll-timeline: --vl-scene; animation-timeline: --vl-scene; - animation-range: var(--vl-act-start) var(--vl-act-end); + animation-duration: 1ms; animation-timing-function: linear; animation-fill-mode: both; } - /* Auto clock: act → delay */ + [vl-scene][vl-timeline="view"] [vl-stage] > :is([vl-enter], [vl-scroll], [vl-effect], [vl-exit]):not([vl-range]) { + animation-range: var(--vl-act-start) var(--vl-act-end); + } + + /* Auto clock: act → delay (pin height / scrub are no-ops) */ [vl-scene][vl-timeline="auto"] [vl-stage] > :is([vl-enter], [vl-effect], [vl-exit]), [vl-scene]:not([vl-timeline]) [vl-stage] > :is([vl-enter], [vl-effect], [vl-exit]) { animation-delay: calc((var(--vl-act) - 1) * var(--vl-beat)); } - /* Scrub: force linear both on scene-driven children */ - [vl-scene][vl-scrub] [vl-stage] > :is([vl-enter], [vl-scroll], [vl-effect], [vl-exit]) { + /* Scrub: only meaningful on view-clock tracks */ + [vl-scene][vl-timeline="view"][vl-scrub] [vl-stage] > :is([vl-enter], [vl-scroll], [vl-effect], [vl-exit]) { animation-timing-function: linear; animation-fill-mode: both; + animation-duration: 1ms; } - /* Progressive enhancement: single-rule clock switch via if() */ + /* + * Progressive enhancement: if() clock switch with the same specificity + * as the baseline view/auto selectors so the enhancement can win. + */ @supports (width: if(style(--vl-probe: 1): 1px; else: 2px)) { - [vl-scene] [vl-stage] > :is([vl-enter], [vl-scroll], [vl-effect], [vl-exit]):not([vl-range]) { - animation-timeline: if( - style(--vl-clock: view): --vl-scene; - else: auto - ); - animation-range: if( - style(--vl-clock: view): var(--vl-act-start) var(--vl-act-end); - else: normal - ); - animation-delay: if( - style(--vl-clock: auto): calc((var(--vl-act) - 1) * var(--vl-beat)); - else: 0s - ); + [vl-scene][vl-timeline="view"] [vl-stage] > :is([vl-enter], [vl-scroll], [vl-effect], [vl-exit]) { + --vl-scroll-timeline: --vl-scene; + animation-timeline: --vl-scene; + animation-duration: 1ms; + animation-delay: 0s; + } + + [vl-scene][vl-timeline="view"] [vl-stage] > :is([vl-enter], [vl-scroll], [vl-effect], [vl-exit]):not([vl-range]) { + animation-range: var(--vl-act-start) var(--vl-act-end); + } + + [vl-scene][vl-timeline="auto"] [vl-stage] > :is([vl-enter], [vl-effect], [vl-exit]), + [vl-scene]:not([vl-timeline]) [vl-stage] > :is([vl-enter], [vl-effect], [vl-exit]) { + animation-timeline: auto; + animation-range: normal; + animation-delay: calc((var(--vl-act) - 1) * var(--vl-beat)); } } diff --git a/packages/css/src/scene-recipes.css b/packages/css/src/scene-recipes.css index bb58a9b..ac488d1 100644 --- a/packages/css/src/scene-recipes.css +++ b/packages/css/src/scene-recipes.css @@ -1,197 +1,386 @@ -/** - * Velora scene recipes — look + layout for named vl-scene presets. - * Part of the Velora skin (theme), not required for host-agnostic motion. - * Choreography (animation names / timelines) stays compatible via these selectors. - */ -@layer velora.layout { - /* Scene look helpers (moved from core layout) */ - [vl-scene] { - --vl-scene-gap: clamp(var(--vl-space-md, 1rem), 2.4vw, var(--vl-space-xl, 4rem)); - --vl-scene-pad-inline: clamp(var(--vl-space-md, 1rem), 4vw, var(--vl-space-2xl, 6rem)); - --vl-scene-pad-block: clamp(var(--vl-space-xl, 4rem), 9vw, var(--vl-space-3xl, 8rem)); - --vl-scene-max: 100%; - --vl-scene-pane-a: 1fr; - --vl-scene-pane-b: 1fr; - --vl-scene-cluster-min: 16rem; - } - - [vl-scene][data-vl-scene-density="compact"] { - --vl-scene-gap: clamp(var(--vl-space-sm, 0.5rem), 1.8vw, var(--vl-space-lg, 2rem)); - --vl-scene-pad-inline: clamp(var(--vl-space-sm, 0.5rem), 2.5vw, var(--vl-space-lg, 2rem)); - --vl-scene-pad-block: clamp(var(--vl-space-lg, 2rem), 5vw, var(--vl-space-xl, 4rem)); - --vl-scene-cluster-min: 12rem; - } - - [vl-scene][data-vl-scene-density="editorial"] { - --vl-scene-gap: clamp(var(--vl-space-lg, 2rem), 3vw, var(--vl-space-2xl, 6rem)); - --vl-scene-pad-inline: clamp(var(--vl-space-md, 1rem), 4.5vw, var(--vl-space-2xl, 6rem)); - } - - [vl-scene][data-vl-scene-density="immersive"] { - --vl-scene-gap: clamp(var(--vl-space-lg, 2rem), 3.2vw, var(--vl-space-2xl, 6rem)); - --vl-scene-pad-inline: clamp(var(--vl-space-lg, 2rem), 5vw, var(--vl-space-3xl, 8rem)); - --vl-scene-pad-block: clamp(var(--vl-space-2xl, 6rem), 10vw, calc(var(--vl-space-3xl, 8rem) * 1.2)); - } - - [vl-scene][data-vl-scene-mode="full-bleed"] { - min-height: 100dvh; - align-content: center; - } - - [vl-scene] > :where(.vl-scene__inner, [data-vl-scene-inner]) { - display: grid; - gap: var(--vl-scene-gap); - width: min(100%, var(--vl-scene-max)); - margin-inline: auto; - padding-inline: var(--vl-scene-pad-inline); - min-width: 0; - } - - [vl-scene] :where(.vl-scene__layout--split, [data-vl-scene-layout~="split"]) { - display: grid; - grid-template-columns: minmax(0, 1fr); - gap: var(--vl-scene-gap); - align-items: start; - } - - [vl-scene] :where(.vl-scene__layout--cluster, [data-vl-scene-layout~="cluster"]) { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(min(100%, var(--vl-scene-cluster-min)), 1fr)); - gap: var(--vl-scene-gap); - } - - [vl-scene] :where(.vl-scene__rail, [data-vl-scene-role="rail"]) { - display: grid; - gap: var(--vl-space-sm, 0.5rem); - align-content: start; - } - - [vl-scene] :where(.vl-scene__stack, [data-vl-scene-role="stack"]) { - display: grid; - gap: var(--vl-scene-gap); - min-width: 0; - } - - @container vl-scene (min-width: 48rem) { - [vl-scene] :where(.vl-scene__layout--split, [data-vl-scene-layout~="split"]) { - grid-template-columns: - minmax(0, var(--vl-scene-pane-a)) - minmax(0, var(--vl-scene-pane-b)); - align-items: center; - } - } - - @container vl-scene (max-width: 36rem) { - [vl-scene][data-vl-scene-density="immersive"] { - --vl-scene-pad-inline: clamp(var(--vl-space-md, 1rem), 4vw, var(--vl-space-lg, 2rem)); - --vl-scene-pad-block: clamp(var(--vl-space-xl, 4rem), 6vw, var(--vl-space-2xl, 6rem)); - } - } -} - -@layer velora.motion { - /* - * Named scene presets — Velora-look recipes (compat). - * Host-agnostic authorship prefers vl-stage + vl-act (see 03c-scene-engine.css). - */ - - [vl-scene="cinematic-hero"] { - display: grid; - place-items: center; - min-height: 100svh; - overflow: clip; - isolation: isolate; - } - - [vl-scene="cinematic-hero"] > * { - animation: vl-reveal-cinematic var(--vl-duration-slower) var(--vl-ease-cinematic) both; - } - - [vl-scene="cinematic-hero"] > *:nth-child(2) { animation-delay: 160ms; } - [vl-scene="cinematic-hero"] > *:nth-child(3) { animation-delay: 300ms; } - [vl-scene="cinematic-hero"] > *:nth-child(4) { animation-delay: 440ms; } - [vl-scene="cinematic-hero"] > *:nth-child(n+5) { animation-delay: 560ms; } - - [vl-scene="sticky-story"] { - position: sticky; - top: 0; - min-height: 100svh; - overflow: clip; - display: grid; - align-content: start; - } - - [vl-scene="sticky-story"] > * { - animation: vl-fade-up both linear; - animation-timeline: view(block); - animation-range: entry 0% cover 55%; - } - - [vl-scene="glass-bento"] { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(min(100%, 18rem), 1fr)); - gap: var(--vl-space-md, 1rem); - } - - [vl-scene="glass-bento"] > * { - background: var(--vl-glass-bg, oklch(100% 0 0 / 0.07)); - backdrop-filter: blur(12px) saturate(1.4); - -webkit-backdrop-filter: blur(12px) saturate(1.4); - border: 1px solid color-mix(in oklch, var(--vl-border-subtle, oklch(60% 0 0)) 38%, transparent); - border-radius: var(--vl-radius-lg, 1rem); - animation: vl-scale-in both linear; - animation-timeline: view(block); - animation-range: entry 5% cover 45%; - } - - [vl-scene="product-reveal"] { - display: grid; - grid-template-columns: 1fr 1fr; - gap: var(--vl-space-xl, 3rem); - align-items: center; - container-type: inline-size; - } - - @container (max-width: 44rem) { - [vl-scene="product-reveal"] { - grid-template-columns: 1fr; - } - } - - [vl-scene="product-reveal"] > *:first-child { - animation: vl-tilt-in both linear; - animation-timeline: view(block); - animation-range: entry 5% cover 48%; - } - - [vl-scene="product-reveal"] > *:last-child { - animation: vl-fade-up both linear; - animation-timeline: view(block); - animation-range: entry 10% cover 50%; - } - - [vl-scene="editorial-cinema"] { - display: grid; - gap: var(--vl-space-xl, 3rem); - container-type: inline-size; - } - - [vl-scene="editorial-cinema"] > * { - animation: vl-clip-rise both linear; - animation-timeline: view(block); - animation-range: entry 5% cover 42%; - } - - @media (prefers-reduced-motion: reduce) { - [vl-scene="cinematic-hero"] > *, - [vl-scene="sticky-story"] > *, - [vl-scene="glass-bento"] > *, - [vl-scene="product-reveal"] > *, - [vl-scene="editorial-cinema"] > * { - animation: none !important; - opacity: 1; - transform: none; - filter: none; - } - } -} +/** + * Velora scene recipes — look + layout for named vl-scene presets. + * Part of the Velora skin (theme), not required for host-agnostic motion. + * Choreography (animation names / timelines) stays compatible via these selectors. + */ +@layer velora.layout { + /* Scene look helpers (moved from core layout) */ + [vl-scene] { + --vl-scene-gap: clamp(var(--vl-space-md, 1rem), 2.4vw, var(--vl-space-xl, 4rem)); + --vl-scene-pad-inline: clamp(var(--vl-space-md, 1rem), 4vw, var(--vl-space-2xl, 6rem)); + --vl-scene-pad-block: clamp(var(--vl-space-xl, 4rem), 9vw, var(--vl-space-3xl, 8rem)); + --vl-scene-max: 100%; + --vl-scene-pane-a: 1fr; + --vl-scene-pane-b: 1fr; + --vl-scene-cluster-min: 16rem; + } + + [vl-scene][data-vl-scene-density="compact"] { + --vl-scene-gap: clamp(var(--vl-space-sm, 0.5rem), 1.8vw, var(--vl-space-lg, 2rem)); + --vl-scene-pad-inline: clamp(var(--vl-space-sm, 0.5rem), 2.5vw, var(--vl-space-lg, 2rem)); + --vl-scene-pad-block: clamp(var(--vl-space-lg, 2rem), 5vw, var(--vl-space-xl, 4rem)); + --vl-scene-cluster-min: 12rem; + } + + [vl-scene][data-vl-scene-density="editorial"] { + --vl-scene-gap: clamp(var(--vl-space-lg, 2rem), 3vw, var(--vl-space-2xl, 6rem)); + --vl-scene-pad-inline: clamp(var(--vl-space-md, 1rem), 4.5vw, var(--vl-space-2xl, 6rem)); + } + + [vl-scene][data-vl-scene-density="immersive"] { + --vl-scene-gap: clamp(var(--vl-space-lg, 2rem), 3.2vw, var(--vl-space-2xl, 6rem)); + --vl-scene-pad-inline: clamp(var(--vl-space-lg, 2rem), 5vw, var(--vl-space-3xl, 8rem)); + --vl-scene-pad-block: clamp(var(--vl-space-2xl, 6rem), 10vw, calc(var(--vl-space-3xl, 8rem) * 1.2)); + } + + [vl-scene][data-vl-scene-mode="full-bleed"] { + min-height: 100dvh; + align-content: center; + } + + [vl-scene] > :where(.vl-scene__inner, [data-vl-scene-inner]) { + display: grid; + gap: var(--vl-scene-gap); + width: min(100%, var(--vl-scene-max)); + margin-inline: auto; + padding-inline: var(--vl-scene-pad-inline); + min-width: 0; + } + + [vl-scene] :where(.vl-scene__layout--split, [data-vl-scene-layout~="split"]) { + display: grid; + grid-template-columns: minmax(0, 1fr); + gap: var(--vl-scene-gap); + align-items: start; + } + + [vl-scene] :where(.vl-scene__layout--cluster, [data-vl-scene-layout~="cluster"]) { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(min(100%, var(--vl-scene-cluster-min)), 1fr)); + gap: var(--vl-scene-gap); + } + + [vl-scene] :where(.vl-scene__rail, [data-vl-scene-role="rail"]) { + display: grid; + gap: var(--vl-space-sm, 0.5rem); + align-content: start; + } + + [vl-scene] :where(.vl-scene__stack, [data-vl-scene-role="stack"]) { + display: grid; + gap: var(--vl-scene-gap); + min-width: 0; + } + + @container vl-scene (min-width: 48rem) { + [vl-scene] :where(.vl-scene__layout--split, [data-vl-scene-layout~="split"]) { + grid-template-columns: + minmax(0, var(--vl-scene-pane-a)) + minmax(0, var(--vl-scene-pane-b)); + align-items: center; + } + } + + @container vl-scene (max-width: 36rem) { + [vl-scene][data-vl-scene-density="immersive"] { + --vl-scene-pad-inline: clamp(var(--vl-space-md, 1rem), 4vw, var(--vl-space-lg, 2rem)); + --vl-scene-pad-block: clamp(var(--vl-space-xl, 4rem), 6vw, var(--vl-space-2xl, 6rem)); + } + } +} + +@layer velora.motion { + /* + * Named scene presets — Velora-look recipes (compat). + * Host-agnostic authorship prefers vl-stage + vl-act (see 03c-scene-engine.css). + */ + + [vl-scene="cinematic-hero"] { + display: grid; + place-items: center; + min-height: 100svh; + overflow: clip; + isolation: isolate; + } + + [vl-scene="cinematic-hero"] > * { + animation: vl-reveal-cinematic var(--vl-duration-slower) var(--vl-ease-cinematic) both; + } + + [vl-scene="cinematic-hero"] > *:nth-child(2) { animation-delay: 160ms; } + [vl-scene="cinematic-hero"] > *:nth-child(3) { animation-delay: 300ms; } + [vl-scene="cinematic-hero"] > *:nth-child(4) { animation-delay: 440ms; } + [vl-scene="cinematic-hero"] > *:nth-child(n+5) { animation-delay: 560ms; } + + [vl-scene="sticky-story"] { + position: sticky; + top: 0; + min-height: 100svh; + overflow: clip; + display: grid; + align-content: start; + } + + [vl-scene="sticky-story"] > * { + animation: vl-fade-up both linear; + animation-timeline: view(block); + animation-range: entry 0% cover 55%; + } + + [vl-scene="glass-bento"] { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(min(100%, 18rem), 1fr)); + gap: var(--vl-space-md, 1rem); + } + + [vl-scene="glass-bento"] > * { + background: var(--vl-glass-bg, oklch(100% 0 0 / 0.07)); + backdrop-filter: blur(12px) saturate(1.4); + -webkit-backdrop-filter: blur(12px) saturate(1.4); + border: 1px solid color-mix(in oklch, var(--vl-border-subtle, oklch(60% 0 0)) 38%, transparent); + border-radius: var(--vl-radius-lg, 1rem); + animation: vl-scale-in both linear; + animation-timeline: view(block); + animation-range: entry 5% cover 45%; + } + + [vl-scene="product-reveal"] { + display: grid; + grid-template-columns: 1fr 1fr; + gap: var(--vl-space-xl, 3rem); + align-items: center; + container-type: inline-size; + } + + @container (max-width: 44rem) { + [vl-scene="product-reveal"] { + grid-template-columns: 1fr; + } + } + + [vl-scene="product-reveal"] > *:first-child { + animation: vl-tilt-in both linear; + animation-timeline: view(block); + animation-range: entry 5% cover 48%; + } + + [vl-scene="product-reveal"] > *:last-child { + animation: vl-fade-up both linear; + animation-timeline: view(block); + animation-range: entry 10% cover 50%; + } + + [vl-scene="editorial-cinema"] { + display: grid; + gap: var(--vl-space-xl, 3rem); + container-type: inline-size; + } + + [vl-scene="editorial-cinema"] > * { + animation: vl-clip-rise both linear; + animation-timeline: view(block); + animation-range: entry 5% cover 42%; + } + + @media (prefers-reduced-motion: reduce) { + [vl-scene="cinematic-hero"] > *, + [vl-scene="sticky-story"] > *, + [vl-scene="glass-bento"] > *, + [vl-scene="product-reveal"] > *, + [vl-scene="editorial-cinema"] > * { + animation: none !important; + opacity: 1; + transform: none; + filter: none; + } + } +} + +@layer velora.motion { + + /* + * cube-triad-stage — Showcase/Skin demo recipe (not host-agnostic stage contract). + * Prefer .vl-3d-stage + CSS vars for reusable 3D stages. + */ + @keyframes vl-cube-scene-rotate { + from { transform: rotateX(-15deg) rotateY(0deg); } + to { transform: rotateX(-15deg) rotateY(360deg); } + } + + @keyframes vl-cube-stage-shadow { + 0%, 95%, 100% { transform: translate(-0.5em, -0.5em); } + 20%, 25% { transform: translate(-5.5em, -0.5em); } + 45%, 50% { transform: translate(-5.5em, -5.5em); } + 70%, 75% { transform: translate(-0.5em, -5.5em); } + } + + @keyframes vl-cube-bottom-cascade { + 0% { transform: none; } + 20%, 25% { transform: rotateZ(-90deg); } + 45%, 50% { transform: rotateZ(-90deg) rotateY(90deg); } + 70%, 75% { transform: rotateZ(-90deg) rotateY(90deg) rotateX(-90deg); } + 95%, 100% { transform: rotateZ(-90deg) rotateY(90deg) rotateX(-90deg) rotateZ(-90deg); } + } + + @keyframes vl-cube-mid-cascade { + 0%, 2% { transform: none; } + 23%, 27% { transform: rotateY(180deg); } + 48%, 52% { transform: rotateY(180deg) rotateZ(-180deg); } + 73%, 77% { transform: rotateY(180deg) rotateZ(-180deg) rotateX(180deg); } + 98%, 100% { transform: rotateY(180deg) rotateZ(-180deg) rotateX(180deg) rotateY(180deg); } + } + + @keyframes vl-cube-top-cascade { + 0%, 5% { transform: none; } + 25%, 30% { transform: rotateY(-180deg); } + 50%, 55% { transform: rotateY(-180deg) rotateZ(-180deg); } + 75%, 80% { transform: rotateY(-180deg) rotateZ(-180deg) rotateX(-180deg); } + 100% { transform: rotateY(-180deg) rotateZ(-180deg) rotateX(-180deg) rotateY(-180deg); } + } + + [vl-effect="cube-triad-stage"] { + --vl-cube-duration: 6s; + --vl-cube-unit: clamp(0.65rem, 1.15vmin, 1rem); + position: relative; + display: grid; + place-items: center; + min-height: clamp(17rem, 34vw, 22rem); + perspective: 75em; + overflow: hidden; + isolation: isolate; + } + + [vl-effect="cube-triad-stage"] *:not(:empty) { + transform-style: preserve-3d; + } + + [vl-effect="cube-triad-stage"] .scene { + position: relative; + width: 1px; + height: 1px; + animation: vl-cube-scene-rotate calc(var(--vl-cube-duration) * 13) linear infinite; + } + + [vl-effect="cube-triad-stage"] .floor { + position: absolute; + inset: -50em; + background-image: + radial-gradient(circle at center, transparent 0%, color-mix(in oklch, black 92%, var(--vl-bg-main)) 52%), + repeating-linear-gradient(45deg, color-mix(in oklch, white 12%, transparent), color-mix(in oklch, black 16%, transparent), color-mix(in oklch, white 12%, transparent) 2em), + repeating-linear-gradient(-45deg, color-mix(in oklch, white 10%, transparent), color-mix(in oklch, black 14%, transparent), color-mix(in oklch, white 10%, transparent) 2em); + background-color: color-mix(in oklch, var(--vl-bg-main) 58%, var(--vl-bg-surface-elevated)); + transform: rotateX(90deg) translateZ(calc((var(--vl-cube-unit) * -6.4) - 1px)); + } + + [vl-effect="cube-triad-stage"] .floor::before { + content: ""; + position: absolute; + left: 50%; + top: 50%; + width: calc(var(--vl-cube-unit) * 14.8); + height: calc(var(--vl-cube-unit) * 14.8); + box-shadow: 0 0 2em color-mix(in oklch, black 45%, transparent); + transform: translate(-50%, -50%); + } + + [vl-effect="cube-triad-stage"] .stage { + position: absolute; + left: 50%; + top: 50%; + width: calc(var(--vl-cube-unit) * 12); + height: calc(var(--vl-cube-unit) * 12); + transform: translate3d(-50%, -50%, calc(var(--vl-cube-unit) * 1.4)); + background: inherit; + box-shadow: 0 0 1em color-mix(in oklch, black 44%, transparent) inset; + } + + [vl-effect="cube-triad-stage"] .stage > div { + position: absolute; + left: calc(var(--vl-cube-unit) * -1.5); + top: 50%; + width: calc(var(--vl-cube-unit) * 15); + height: calc(var(--vl-cube-unit) * 2.1); + background: inherit; + box-shadow: inherit; + clip-path: polygon(1.5em 0, 13.5em 0, 100% 100%, 0 100%); + transform-origin: top; + transform: rotateZ(var(--vl-stage-rz, 0deg)) translateY(calc(var(--vl-cube-unit) * 6)) rotateX(-45deg); + } + + [vl-effect="cube-triad-stage"] .stage > div:nth-child(1) { --vl-stage-rz: 0deg; } + [vl-effect="cube-triad-stage"] .stage > div:nth-child(2) { --vl-stage-rz: 90deg; } + [vl-effect="cube-triad-stage"] .stage > div:nth-child(3) { --vl-stage-rz: 180deg; } + [vl-effect="cube-triad-stage"] .stage > div:nth-child(4) { --vl-stage-rz: 270deg; } + + [vl-effect="cube-triad-stage"] .stage::after { + content: ""; + position: absolute; + left: 50%; + top: 50%; + width: calc(var(--vl-cube-unit) * 6); + height: calc(var(--vl-cube-unit) * 6); + background: color-mix(in oklch, black 24%, transparent); + filter: blur(0.5em); + animation: vl-cube-stage-shadow var(--vl-cube-duration) ease-in-out infinite; + } + + [vl-effect="cube-triad-stage"] .bottomCube, + [vl-effect="cube-triad-stage"] .midCube, + [vl-effect="cube-triad-stage"] .topCube { + position: absolute; + width: calc(var(--vl-cube-unit) * 5); + height: calc(var(--vl-cube-unit) * 5); + background-color: color-mix(in oklch, var(--vl-cube-tone, var(--vl-color-primary)) 68%, transparent); + background-image: + repeating-linear-gradient(45deg, color-mix(in oklch, white 14%, transparent), color-mix(in oklch, black 14%, transparent), color-mix(in oklch, white 14%, transparent) 1em), + repeating-linear-gradient(-45deg, color-mix(in oklch, white 14%, transparent), color-mix(in oklch, black 14%, transparent), color-mix(in oklch, white 14%, transparent) 1em); + box-shadow: 0 0 1em color-mix(in oklch, black 55%, var(--vl-cube-tone, var(--vl-color-primary))) inset; + } + + [vl-effect="cube-triad-stage"] .bottomCube { + --vl-cube-tone: var(--vl-color-primary); + left: 0; + top: 0; + transform-origin: bottom left; + animation: vl-cube-bottom-cascade var(--vl-cube-duration) ease-in-out infinite; + } + + [vl-effect="cube-triad-stage"] .midCube { + --vl-cube-tone: var(--vl-color-secondary); + inset: 0; + transform-origin: bottom right; + animation: vl-cube-mid-cascade var(--vl-cube-duration) ease-in-out infinite; + } + + [vl-effect="cube-triad-stage"] .topCube { + --vl-cube-tone: var(--vl-color-accent, var(--vl-color-secondary)); + inset: 0; + transform-origin: top left; + animation: vl-cube-top-cascade var(--vl-cube-duration) ease-in-out infinite; + } + + [vl-effect="cube-triad-stage"] i { + position: absolute; + inset: 0; + margin: 0; + background: inherit; + box-shadow: inherit; + } + + [vl-effect="cube-triad-stage"] i:nth-child(1) { transform: rotateX(90deg); transform-origin: top; } + [vl-effect="cube-triad-stage"] i:nth-child(2) { transform: rotateX(-90deg); transform-origin: bottom; } + [vl-effect="cube-triad-stage"] i:nth-child(3) { transform: rotateY(-90deg); transform-origin: left; } + [vl-effect="cube-triad-stage"] i:nth-child(4) { transform: rotateY(90deg); transform-origin: right; } + [vl-effect="cube-triad-stage"] i:nth-child(5) { transform: translateZ(calc(var(--vl-cube-unit) * 5)); } + + @media (prefers-reduced-motion: reduce) { + [vl-effect="cube-triad-stage"] .scene, + [vl-effect="cube-triad-stage"] .stage::after, + [vl-effect="cube-triad-stage"] .bottomCube, + [vl-effect="cube-triad-stage"] .midCube, + [vl-effect="cube-triad-stage"] .topCube { + animation: none; + } + } + +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e0e6124..4964dc0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,12 +14,18 @@ importers: .: devDependencies: + '@playwright/test': + specifier: ^1.55.0 + version: 1.62.1 turbo: specifier: ^2.10.11 version: 2.10.11 apps/docs: dependencies: + '@velora/catalog': + specifier: workspace:* + version: link:../../packages/catalog '@velora/css': specifier: workspace:* version: link:../../packages/css @@ -36,6 +42,8 @@ importers: specifier: ^8.2.2 version: 8.2.2(esbuild@0.28.2)(jiti@1.21.7)(yaml@2.8.3) + packages/catalog: {} + packages/compiler: {} packages/css: {} @@ -568,6 +576,11 @@ packages: '@oxc-project/types@0.146.0': resolution: {integrity: sha512-XC0QsnnhVe7sLIWmYmdPw7x5P0h4W8vUU3Nv1ySgWXtvCz8NizoAEpGXA0sOYoJQV2Rl13LgURAHQ5cI5ILCSA==} + '@playwright/test@1.62.1': + resolution: {integrity: sha512-DTcUc8qii+cpHvtOwggMtBRMjKZHXYWdw8syRYu2vtzuq4Wxphqq4NfCs5Zt44L6mA8rfDfj+PHnxFc/FeK6mQ==} + engines: {node: '>=20'} + hasBin: true + '@rolldown/binding-android-arm-eabi@1.2.5': resolution: {integrity: sha512-DLe/i+l8ynIBY7XEQ191TeZvCoowIGa18R+dIV30GW7DiOtp74i/xX8hs8GUjW5ARV7VZuie3d6AumSmCwbeRA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1015,6 +1028,11 @@ packages: resolution: {integrity: sha512-Wp1zXWPVUPBmfoa3Cqc9ctaKuzKAV6uLstRqlR56kSjplf5uAce+qeyYym7F+PHbGTk+tCEdkCW6RD7DX/gBZw==} engines: {node: '>=20'} + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -1334,6 +1352,16 @@ packages: resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} engines: {node: '>= 6'} + playwright-core@1.62.1: + resolution: {integrity: sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==} + engines: {node: '>=20'} + hasBin: true + + playwright@1.62.1: + resolution: {integrity: sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==} + engines: {node: '>=20'} + hasBin: true + postcss-import@15.1.0: resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} engines: {node: '>=14.0.0'} @@ -2109,6 +2137,10 @@ snapshots: '@oxc-project/types@0.146.0': {} + '@playwright/test@1.62.1': + dependencies: + playwright: 1.62.1 + '@rolldown/binding-android-arm-eabi@1.2.5': optional: true @@ -2581,6 +2613,9 @@ snapshots: dependencies: tiny-inflate: 1.0.3 + fsevents@2.3.2: + optional: true + fsevents@2.3.3: optional: true @@ -2856,6 +2891,14 @@ snapshots: pirates@4.0.7: {} + playwright-core@1.62.1: {} + + playwright@1.62.1: + dependencies: + playwright-core: 1.62.1 + optionalDependencies: + fsevents: 2.3.2 + postcss-import@15.1.0(postcss@8.5.23): dependencies: postcss: 8.5.23 diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..178aa1c --- /dev/null +++ b/tests/README.md @@ -0,0 +1,41 @@ +# Velora tests + +## Unit / contract (fast) + +```bash +pnpm test # compiler + showcase library + audits +pnpm verify:contract # CSS sync + catalog + showcase build + motion validate +``` + +## E2E — Playwright (P0.8) + +Smoke coverage across Chromium, Firefox and WebKit: + +- Showcase live + secondary pages load +- Registry-driven catalog + compatibility matrix populate +- Motion Lab inspector +- `prefers-reduced-motion: reduce` content visibility +- Generated `/data/*.json` integrity + +### Setup (first time) + +```bash +pnpm install +pnpm exec playwright install chromium firefox webkit +``` + +### Run + +```bash +pnpm test:e2e +``` + +Against an existing preview server: + +```bash +pnpm --filter showcase build +pnpm --filter showcase exec vite preview --host 127.0.0.1 --port 4187 +PLAYWRIGHT_BASE_URL=http://127.0.0.1:4187 pnpm test:e2e +``` + +Manual reduced-motion checklist: [`docs/validation/reduced-motion-checklist.md`](../docs/validation/reduced-motion-checklist.md) diff --git a/tests/e2e/catalog-data.spec.mjs b/tests/e2e/catalog-data.spec.mjs new file mode 100644 index 0000000..291efb5 --- /dev/null +++ b/tests/e2e/catalog-data.spec.mjs @@ -0,0 +1,35 @@ +import { test, expect } from "@playwright/test"; + +test("catalog-summary.json is valid", async ({ request }) => { + const res = await request.get("/data/catalog-summary.json"); + expect(res.ok()).toBeTruthy(); + const data = await res.json(); + expect(data.presetCount).toBeGreaterThan(100); + expect(data.byChannel.enter).toBeGreaterThan(0); +}); + +test("presets-full.json matches summary count", async ({ request }) => { + const [summaryRes, fullRes] = await Promise.all([ + request.get("/data/catalog-summary.json"), + request.get("/data/presets-full.json"), + ]); + const summary = await summaryRes.json(); + const full = await fullRes.json(); + expect(full.presets.length).toBe(summary.presetCount); +}); + +test("compatibility-matrix.json has primitives", async ({ request }) => { + const res = await request.get("/data/compatibility-matrix.json"); + const data = await res.json(); + expect(data.primitives.length).toBeGreaterThan(10); + expect(data.baseline.chrome).toBeTruthy(); +}); + +test("benchmarks.json has generated claims", async ({ request }) => { + const res = await request.get("/data/benchmarks.json"); + expect(res.ok()).toBeTruthy(); + const data = await res.json(); + expect(data.claims.length).toBeGreaterThan(3); + expect(data.cdn.unpkg.motionCore).toContain("unpkg.com"); + expect(data.methodology.fpsClaims).toBe("not-published"); +}); diff --git a/tests/e2e/playwright.config.mjs b/tests/e2e/playwright.config.mjs new file mode 100644 index 0000000..a595a5e --- /dev/null +++ b/tests/e2e/playwright.config.mjs @@ -0,0 +1,36 @@ +import { defineConfig, devices } from "@playwright/test"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const repoRoot = path.resolve(__dirname, "../.."); +const baseURL = process.env.PLAYWRIGHT_BASE_URL ?? "http://127.0.0.1:4187"; + +export default defineConfig({ + testDir: __dirname, + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 2 : undefined, + reporter: process.env.CI ? "github" : "list", + timeout: 45_000, + use: { + baseURL, + trace: "on-first-retry", + }, + projects: [ + { name: "chromium", use: { ...devices["Desktop Chrome"] } }, + { name: "firefox", use: { ...devices["Desktop Firefox"] } }, + { name: "webkit", use: { ...devices["Desktop Safari"] } }, + ], + webServer: process.env.PLAYWRIGHT_BASE_URL + ? undefined + : { + command: + "pnpm generate:catalog && pnpm --filter @velora/css build && pnpm --filter showcase build && pnpm --filter showcase exec vite preview --host 127.0.0.1 --port 4187", + url: `${baseURL}/data/catalog-summary.json`, + cwd: repoRoot, + reuseExistingServer: false, + timeout: 180_000, + }, +}); diff --git a/tests/e2e/showcase.reduced-motion.spec.mjs b/tests/e2e/showcase.reduced-motion.spec.mjs new file mode 100644 index 0000000..a144e96 --- /dev/null +++ b/tests/e2e/showcase.reduced-motion.spec.mjs @@ -0,0 +1,38 @@ +import { test, expect } from "@playwright/test"; + +test.describe("prefers-reduced-motion: reduce", () => { + test.use({ + colorScheme: "dark", + }); + + test.beforeEach(async ({ page }) => { + await page.emulateMedia({ reducedMotion: "reduce" }); + }); + + test("core page content stays visible", async ({ page }) => { + await page.goto("/pages/core/core.html"); + const intro = page.locator("#core-intro, [id^='core-']").first(); + await expect(intro).toBeVisible(); + await expect(page.locator("#content")).toBeVisible(); + }); + + test("catalog targets remain visible", async ({ page }) => { + await page.goto("/pages/motion/api-motion-catalog.html#channels"); + const targets = page.locator(".api-target").first(); + await expect(targets).toBeVisible(); + const opacity = await targets.evaluate((el) => + Number.parseFloat(getComputedStyle(el).opacity), + ); + expect(opacity).toBeGreaterThan(0); + }); + + test("compatibility reduce section readable", async ({ page }) => { + await page.goto("/pages/core/compatibility.html#compat-reduced"); + await expect(page.getByRole("heading", { name: /Readable composed state/i })).toBeVisible(); + }); + + test("vl-motion=still card visible on compatibility page", async ({ page }) => { + await page.goto("/pages/core/compatibility.html#compat-reduced"); + await expect(page.locator('[vl-motion="still"]')).toBeVisible(); + }); +}); diff --git a/tests/e2e/showcase.smoke.spec.mjs b/tests/e2e/showcase.smoke.spec.mjs new file mode 100644 index 0000000..18df1a2 --- /dev/null +++ b/tests/e2e/showcase.smoke.spec.mjs @@ -0,0 +1,67 @@ +import { test, expect } from "@playwright/test"; + +const LIVE_PAGES = [ + "/index.html", + "/pages/core/core.html", + "/pages/core/elements.html", + "/pages/scenes/scene-timeline.html", + "/pages/core/skins.html", + "/pages/motion/api-motion-catalog.html", +]; + +const SECONDARY_PAGES = [ + "/pages/core/compatibility.html", + "/pages/core/benchmarks.html", + "/pages/core/hosts.html", + "/pages/motion/motion-lab.html", + "/pages/motion/playground.html", +]; + +for (const path of [...LIVE_PAGES, ...SECONDARY_PAGES]) { + test(`loads ${path}`, async ({ page }) => { + await page.goto(path, { waitUntil: "domcontentloaded" }); + await expect(page.locator("#content")).toBeVisible(); + await expect(page.locator("main")).toBeVisible(); + }); +} + +test("catalog registry section renders stats", async ({ page }) => { + await page.goto("/pages/motion/api-motion-catalog.html#preset-registry"); + await expect(page.locator("#catalog-registry-stats")).toBeVisible(); + await expect(page.locator("#catalog-registry-table tbody tr")).not.toHaveCount(0, { + timeout: 15_000, + }); +}); + +test("compatibility matrix table populates", async ({ page }) => { + await page.goto("/pages/core/compatibility.html#compat-matrix"); + await expect(page.locator("#compat-matrix-table tbody tr")).not.toHaveCount(0, { + timeout: 15_000, + }); +}); + +test("motion lab inspector loads", async ({ page }) => { + await page.goto("/pages/motion/motion-lab.html#preset-inspector"); + const items = page.locator("#lab-preset-list li"); + await expect(items).not.toHaveCount(0, { timeout: 15_000 }); + await items.first().click(); + await expect(page.locator("#lab-preset-detail")).toContainText(/Channel|preset/i); +}); + +test("playground composes markup from registry", async ({ page }) => { + await page.goto("/pages/motion/playground.html#play-compose"); + await expect(page.locator("#playground-markup")).not.toHaveText("", { + timeout: 15_000, + }); + await expect(page.locator("#playground-preview-target")).toBeVisible(); + await expect(page.locator("#playground-markup")).toContainText(/vl-[a-z-]+="/); +}); + +test("benchmarks page loads generated metrics", async ({ page }) => { + await page.goto("/pages/core/benchmarks.html#bench-metrics"); + await expect(page.locator("#bench-metrics-body tr")).not.toHaveCount(0, { + timeout: 15_000, + }); + await expect(page.locator("#bench-metrics-body")).toContainText(/Animation runtime JavaScript/); + await expect(page.locator("#bench-cdn-snippet")).toContainText("unpkg.com"); +});