From e5a71844ed47e0c19f68d295edab1a97565ac7b2 Mon Sep 17 00:00:00 2001 From: Simone Leonelli Date: Sat, 31 Oct 2026 17:57:31 +1100 Subject: [PATCH 1/2] docs: prepare registry developer trial and live verification Co-Authored-By: Codex --- docs/registry/README.md | 23 ++++++-- docs/registry/demo.tsx | 78 +++++++++++++++++++++++++++ docs/registry/trial.md | 82 +++++++++++++++++++++++++++++ tools/registry/consumers.mjs | 19 +++++-- tools/registry/verify-consumers.mjs | 7 ++- 5 files changed, 201 insertions(+), 8 deletions(-) create mode 100644 docs/registry/demo.tsx create mode 100644 docs/registry/trial.md diff --git a/docs/registry/README.md b/docs/registry/README.md index 93984bd..fe1440e 100644 --- a/docs/registry/README.md +++ b/docs/registry/README.md @@ -19,11 +19,17 @@ npx shadcn@4.23.0 add https://mustbesimo.github.io/web-design-studio/r/scrub-vid npx shadcn@4.23.0 add https://mustbesimo.github.io/web-design-studio/r/tilt-card.json ``` -The URLs become available when the PR is merged and the existing Pages workflow -deploys successfully. Preview locally with `npm run registry:build`; the consumer +The public URLs are served by the existing Pages workflow. Preview locally with `npm run registry:build`; the consumer harness serves the generated JSON and exercises real URL installs before release. No directory listing or registered `@namespace` is implied. +To check the deployed payloads against your checkout and run fresh consumer installs +through the public URLs: + +```sh +npm run test:registry:consumers -- --registry-base https://mustbesimo.github.io/web-design-studio +``` + Components install under your configured component alias. Tilt card also installs the shared cinematic runtime under your lib alias, including TypeScript declarations. Entrance reveal adds `gsap` and `@gsap/react`; tilt adds `gsap`; scrub video uses @@ -47,12 +53,23 @@ No component contacts Web Design Studio or sends analytics. Only your media URLs are fetched at runtime. External media you supply is allowed. Keep copy and any CTA in semantic markup; footage must not be the sole carrier of information. +## Developer trial + +Use the [trial instructions and feedback form](trial.md) with the +[copyable Next demo](demo.tsx). The demo uses the three installed components and +existing KERN sample media. + ## Verification and limits `npm run test:registry` checks deterministic payloads and dependency closure. `npm run test:registry:consumers` creates fresh Next and Vite projects, runs the pinned shadcn CLI against local URLs, typechecks, builds and exercises the browser -matrix. It requires Chrome and ffmpeg; neither is silently skipped. Generated +matrix. It requires Chrome and ffmpeg; neither is silently skipped. For an additional +Playwright WebKit run, install the matching browser with +`node node_modules/playwright-core/cli.js install webkit`, then use +`REGISTRY_BROWSER=webkit npm run test:registry:consumers -- --browser-only` after +the fresh consumer run. This tests the bundled WebKit engine, not an iPhone or +the installed Safari app. Generated projects, media, screenshots and timing reports stay in `.verify/registry/`. Next verifies readable server markup with JavaScript actually disabled. Vite is diff --git a/docs/registry/demo.tsx b/docs/registry/demo.tsx new file mode 100644 index 0000000..69c91f9 --- /dev/null +++ b/docs/registry/demo.tsx @@ -0,0 +1,78 @@ +'use client'; + +import EntranceReveal from '@/components/entrance-reveal'; +import ScrubVideo from '@/components/scrub-video'; +import { TiltCard } from '@/components/tilt-card'; + +const site = 'https://mustbesimo.github.io/web-design-studio'; + +/** Next App Router demo. Install the three items first; place this file at app/pilot/page.tsx. */ +export default function RegistryPilotDemo() { + return
+
Web Design StudioReact component pilot
+
+

Three small mechanisms. Your visual identity.

+

Motion, in your own hands.

+

Entrance reveal, tilt card and scroll-controlled video, installed as source into your project.

+ Try the components ↗ +
+
+

01 / Entrance reveal

+ +
+
+

02 / Tilt card

+
+ +

Move across this card. The inner surface responds while its outer layout stays put.

+ Install tilt card +
+ +

These cards inherit this page’s colors and type. Keyboard links stay native.

+ Read the component guide +
+
+
+
+

03 / Scrub video

+

Scroll forward, then back. The KERN sample clip follows your position.

+ +

On narrow screens, coarse pointers or reduced motion, the same section uses its poster in natural flow. This is sample artwork from the existing KERN example.

+
+
+

Try it / Source-owned components

+

Bring one into your project.

+

Use a React + TypeScript project already configured for shadcn. Pick one component to start.

+
{`npx shadcn@4.23.0 add ${site}/r/entrance-reveal.json\nnpx shadcn@4.23.0 add ${site}/r/tilt-card.json\nnpx shadcn@4.23.0 add ${site}/r/scrub-video.json`}
+ Read the install guide ↗ +
+
Web Design Studio · Registry pilot · No account or API key required
+ +
; +} diff --git a/docs/registry/trial.md b/docs/registry/trial.md new file mode 100644 index 0000000..047fb0b --- /dev/null +++ b/docs/registry/trial.md @@ -0,0 +1,82 @@ +# Web Design Studio: component pilot + +This is a small usability trial for 3–5 developers. The question is whether the +three components install cleanly and fit an existing React project. Participation +is optional; no analytics, account, API key or telemetry is involved. + +## Try one component + +Allow about 15 minutes. Use a disposable React + TypeScript project already +configured for shadcn, or a branch of a project you can safely modify. + +1. Pick **entrance reveal**, **tilt card** or **scrub video**. +2. Install it with the matching command below. +3. Add it to a page, replace the example text with your own and adapt one host + color or spacing value. +4. Try keyboard navigation, a narrow window and reduced motion. For video, supply + a poster and a silent clip; the permanent poster must remain useful if it fails. +5. Send the short feedback form below, including unsuccessful attempts. + +```sh +npx shadcn@4.23.0 add https://mustbesimo.github.io/web-design-studio/r/entrance-reveal.json +npx shadcn@4.23.0 add https://mustbesimo.github.io/web-design-studio/r/tilt-card.json +npx shadcn@4.23.0 add https://mustbesimo.github.io/web-design-studio/r/scrub-video.json +``` + +Install only the component you chose. For the full demonstration, install all +three and copy [demo.tsx](demo.tsx) to `app/pilot/page.tsx` in a Next App Router +project. Open `/pilot`. Its sample footage and poster come from the existing KERN +example on the Web Design Studio site; replace those URLs with your own assets +when integrating it. Vite projects can use the same component body, adjusting +imports to match `components.json` and mounting it in their normal entry point. + +- [Install guide and limitations](README.md) +- [Entrance reveal props](entrance-reveal.md) +- [Tilt card props and runtime](tilt-card.md) +- [Scrub video props and media requirements](scrub-video.md) + +The entrance reveal does not pin. Components inherit host semantic CSS tokens; +they do not install a global theme. Client-only Vite needs host SSR/prerendering +for meaningful content with JavaScript disabled. + +## Send back + +Copy this and fill in whatever you observed. Do not include private source, +credentials, customer information or internal URLs in a public reply. + +```text +Project/framework and browser/device: +Component chosen: +Did the install command succeed? If not, paste the non-sensitive error: +Time from starting the install to a visible working component: +Time to a result you would keep: +What did you have to change, look up or guess? +Did keyboard, narrow layout and reduced motion behave as expected? +What made you stop, if you stopped? +One improvement that would help most: +May Simone quote your feedback? Yes / No (default: No) +``` + +These are individual usability observations, not adoption or conversion figures. +Keep an empty result empty; record a failure as a failure. Ask before quoting or +publishing a participant's name or feedback. + +## Safari and iPhone check + +These are manual device checks until a result is recorded. A WebKit automation +result is not a physical iPhone result. + +- On desktop Safari, scroll the sample clip forward and backward, resize narrow + then wide, and confirm the poster remains if the video is blocked or missing. +- On iPhone Safari, confirm a readable poster and caption in natural page flow, + with no long pinned track or pointer tilt. Try portrait and landscape. +- Enable the device's Reduce Motion setting and reload. Confirm all text and + links remain usable. Note device model, OS/browser version and the actual media. + +## Invitation text + +> I’m testing three small React motion components for Web Design Studio. Would you +> try one in a disposable project for about 15 minutes and tell me where the +> install or customization gets confusing? Failed attempts are useful feedback. +> There’s no account, API key or tracking. Here are the demo source, install +> instructions and a short feedback form. I’ll ask before quoting anything. diff --git a/tools/registry/consumers.mjs b/tools/registry/consumers.mjs index b4542fe..8425ea2 100644 --- a/tools/registry/consumers.mjs +++ b/tools/registry/consumers.mjs @@ -8,7 +8,10 @@ import { writeConsumer } from './fixture.mjs'; import { verifyConsumers } from './verify-consumers.mjs'; const output = join(root, '.verify/registry'); -const report = { mode: process.argv.includes('--browser-only') ? 'browser-only' : 'fresh', versions: { shadcn: '4.23.0', next: '16.4.3', vite: '7.3.7', react: '19.3.1', node: process.version }, timings: [], assertions: [] }; +const remoteIndex = process.argv.indexOf('--registry-base'); +const remoteBase = remoteIndex < 0 ? null : process.argv[remoteIndex + 1]?.replace(/\/$/, ''); +if (remoteIndex >= 0 && (!remoteBase || !/^https?:\/\//.test(remoteBase))) throw new Error('--registry-base requires an HTTP(S) base URL'); +const report = { registryBase: remoteBase || 'local', mode: process.argv.includes('--browser-only') ? 'browser-only' : 'fresh', versions: { shadcn: '4.23.0', next: '16.4.3', vite: '7.3.7', react: '19.3.1', node: process.version }, timings: [], assertions: [] }; mkdirSync(output, { recursive: true }); const services = []; const env = { ...process.env, NEXT_TELEMETRY_DISABLED: '1', SHADCN_TELEMETRY_DISABLED: '1' }; @@ -43,9 +46,19 @@ const registry = createServer((req, res) => { catch { res.writeHead(404).end(); } }); await new Promise(r => registry.listen(0, '127.0.0.1', r)); -const base = `http://127.0.0.1:${registry.address().port}/web-design-studio`; +const base = remoteBase || `http://127.0.0.1:${registry.address().port}/web-design-studio`; try { - buildRegistry({ base }); + const expected = buildRegistry({ base }); + if (remoteBase) { + for (const item of expected) { + const response = await fetch(`${base}/r/${item.name}.json`); + if (!response.ok) throw new Error(`Live registry ${item.name}: HTTP ${response.status}`); + const published = await response.json(); + if (JSON.stringify(published) !== JSON.stringify(item)) throw new Error(`Live registry differs from this checkout: ${item.name}`); + } + const index = await fetch(`${base}/llms.txt`); + if (!index.ok || await index.text() !== readFileSync(join(root, 'llms.txt'), 'utf8')) throw new Error('Live llms.txt is missing or stale'); + } if (!process.argv.includes('--browser-only')) { const cli = join(output, 'cli'); mkdirSync(cli, { recursive: true }); await run('npm', ['install', '--prefix', cli, '--no-audit', '--no-fund', 'shadcn@4.23.0'], root, 'install-cli'); diff --git a/tools/registry/verify-consumers.mjs b/tools/registry/verify-consumers.mjs index f713cd6..7a9640b 100644 --- a/tools/registry/verify-consumers.mjs +++ b/tools/registry/verify-consumers.mjs @@ -1,6 +1,6 @@ import assert from 'node:assert/strict'; import { join } from 'node:path'; -import { chromium } from 'playwright-core'; +import { chromium, webkit } from 'playwright-core'; async function eventually(check, label, timeout = 10000) { const deadline = Date.now() + timeout; @@ -45,7 +45,10 @@ function instrument() { export async function verifyConsumers({ output, report, endpoints }) { const executablePath = process.env.CHROME_PATH || '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'; - const browser = await chromium.launch({ executablePath, headless: true }); + const engine = process.env.REGISTRY_BROWSER || 'chromium'; + if (!['chromium', 'webkit'].includes(engine)) throw new Error('REGISTRY_BROWSER must be chromium or webkit'); + const browser = await (engine === 'webkit' ? webkit.launch({headless:true}) : chromium.launch({ executablePath, headless: true })); + report.browser = engine; try { for (const [kind, origin] of endpoints) { console.log('Browser matrix: ' + kind); From a65fb6676ef2e1520c450b898d0322b06b6e05c2 Mon Sep 17 00:00:00 2001 From: Simone Leonelli Date: Sat, 31 Oct 2026 18:04:09 +1100 Subject: [PATCH 2/2] test: cover WebKit keyboard and restored-scroll behavior Co-Authored-By: Codex --- docs/registry/README.md | 6 +++++- docs/registry/trial.md | 4 +++- tools/registry/verify-consumers.mjs | 17 +++++++++++++---- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/docs/registry/README.md b/docs/registry/README.md index fe1440e..5b9d5d5 100644 --- a/docs/registry/README.md +++ b/docs/registry/README.md @@ -61,6 +61,8 @@ existing KERN sample media. ## Verification and limits +Run these maintainer checks from a full repository checkout. + `npm run test:registry` checks deterministic payloads and dependency closure. `npm run test:registry:consumers` creates fresh Next and Vite projects, runs the pinned shadcn CLI against local URLs, typechecks, builds and exercises the browser @@ -69,7 +71,9 @@ Playwright WebKit run, install the matching browser with `node node_modules/playwright-core/cli.js install webkit`, then use `REGISTRY_BROWSER=webkit npm run test:registry:consumers -- --browser-only` after the fresh consumer run. This tests the bundled WebKit engine, not an iPhone or -the installed Safari app. Generated +the installed Safari app. The WebKit runner resets scroll on reload, including on +a static control page, so that run tests an explicit host-restored position; +Chrome separately proves native reload restoration. Generated projects, media, screenshots and timing reports stay in `.verify/registry/`. Next verifies readable server markup with JavaScript actually disabled. Vite is diff --git a/docs/registry/trial.md b/docs/registry/trial.md index 047fb0b..4cd957d 100644 --- a/docs/registry/trial.md +++ b/docs/registry/trial.md @@ -66,8 +66,10 @@ publishing a participant's name or feedback. These are manual device checks until a result is recorded. A WebKit automation result is not a physical iPhone result. +- For keyboard navigation on macOS Safari, use Option-Tab for links if plain Tab + skips them ([Apple keyboard guide](https://support.apple.com/guide/safari/cpsh003/mac)). - On desktop Safari, scroll the sample clip forward and backward, resize narrow - then wide, and confirm the poster remains if the video is blocked or missing. + then wide, reload mid-page and note the restored position, and confirm the poster remains if the video is blocked or missing. - On iPhone Safari, confirm a readable poster and caption in natural page flow, with no long pinned track or pointer tilt. Try portrait and landscape. - Enable the device's Reduce Motion setting and reload. Confirm all text and diff --git a/tools/registry/verify-consumers.mjs b/tools/registry/verify-consumers.mjs index 7a9640b..8d1621e 100644 --- a/tools/registry/verify-consumers.mjs +++ b/tools/registry/verify-consumers.mjs @@ -101,9 +101,11 @@ export async function verifyConsumers({ output, report, endpoints }) { await page.mouse.move(box.x + box.width - 4, box.y + box.height / 2); await eventually(async () => assert.notEqual(await visual.evaluate(el => getComputedStyle(el).transform), neutral), 'pointer tilt response'); await page.getByRole('link', {name:'Read item 1'}).focus(); - await page.keyboard.press('Tab'); - await page.keyboard.press('Shift+Tab'); - assert.equal(await page.evaluate(() => document.activeElement.textContent), 'Read item 1'); + // macOS WebKit uses Option-Tab to include links unless full keyboard access is enabled. + const linkModifier = engine === 'webkit' && process.platform === 'darwin' ? 'Alt+' : ''; + await page.keyboard.press(linkModifier + 'Tab'); + await page.keyboard.press(linkModifier + 'Shift+Tab'); + assert.equal(await page.getByRole('link', {name:'Read item 1'}).evaluate(el => document.activeElement === el), true, 'keyboard focus reaches the native link'); assert.notEqual(await page.getByRole('link', {name:'Read item 1'}).evaluate(el => getComputedStyle(el).outlineStyle), 'none'); // Seek forward, backward, then resize at a nonzero scroll position. const video = page.locator('video').first(); @@ -117,11 +119,18 @@ export async function verifyConsumers({ output, report, endpoints }) { assert.ok(Math.abs(values[0]-values[1])<.06); }, 'resize updates mapping'); if (kind === 'next') { + await page.evaluate(() => document.activeElement instanceof HTMLElement && document.activeElement.blur()); const savedScroll = await page.evaluate(() => scrollY); await page.reload(); await page.waitForFunction(() => window.fixtureReady); await eventually(async () => assert.equal(await page.locator('[data-wds-ready]').count(), 2), 'restored media ready'); - await eventually(async () => assert.ok(Math.abs(await page.evaluate(() => scrollY) - savedScroll) < 2), 'browser restores scroll on reload'); + if (engine === 'webkit') { + // This WebKit runner resets reload scroll even on a static HTTP control page. + // Exercise host restoration explicitly; do not claim native Safari restoration. + await page.evaluate(y => scrollTo(0, y), savedScroll); + report.assertions.push('WebKit: host-restored scroll mapping; native Safari reload restoration remains manual'); + } + await eventually(async () => { const actual = await page.evaluate(() => scrollY); assert.ok(Math.abs(actual - savedScroll) < 2, `saved=${savedScroll}, restored=${actual}`); }, 'browser restores scroll on reload'); await eventually(async () => { const values = await video.evaluate(el => {const r=el.closest('section').getBoundingClientRect();return [el.currentTime,Math.max(0,Math.min(1,-r.top/(r.height-innerHeight)))*(el.duration-.001)];}); assert.ok(Math.abs(values[0]-values[1])<.06);