Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 24 additions & 3 deletions docs/registry/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -47,12 +53,27 @@ 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

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
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. 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
Expand Down
78 changes: 78 additions & 0 deletions docs/registry/demo.tsx
Original file line number Diff line number Diff line change
@@ -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 <main data-registry-demo>
<header><a href={site}>Web Design Studio</a><span>React component pilot</span></header>
<div className="pilot-intro">
<p className="pilot-kicker">Three small mechanisms. Your visual identity.</p>
<h1>Motion, in your own hands.</h1>
<p>Entrance reveal, tilt card and scroll-controlled video, installed as source into your project.</p>
<a className="pilot-action" href="#pilot-install">Try the components <span aria-hidden>↗</span></a>
</div>
<section className="pilot-chapter" aria-label="Entrance reveal demonstration">
<p className="pilot-index">01 / Entrance reveal</p>
<EntranceReveal eyebrow="Normal document flow" title="A quiet arrival."
summary="The eyebrow, heading and summary arrive together as the section enters view. Reduced motion shows the complete section." />
</section>
<section className="pilot-chapter" aria-label="Tilt card demonstration">
<p className="pilot-index">02 / Tilt card</p>
<div className="pilot-cards">
<TiltCard title="Follow the pointer" eyebrow="Fine-hover devices" maxDeg={7}>
<p>Move across this card. The inner surface responds while its outer layout stays put.</p>
<a href="#pilot-install">Install tilt card</a>
</TiltCard>
<TiltCard title="Keep your own theme" eyebrow="Host semantic tokens" maxDeg={7}>
<p>These cards inherit this page’s colors and type. Keyboard links stay native.</p>
<a href={`${site}/docs/registry/tilt-card.md`}>Read the component guide</a>
</TiltCard>
</div>
</section>
<section className="pilot-chapter" aria-label="Scrub video demonstration">
<p className="pilot-index">03 / Scrub video</p>
<p>Scroll forward, then back. The KERN sample clip follows your position.</p>
<ScrubVideo src={`${site}/examples/kern-calibration/assets/kern-color.mp4`}
poster={`${site}/examples/kern-calibration/assets/kern-poster.jpg`}
label="Rendered optical mechanism from the KERN example" title="A frame for every position." trackVh={2.4} />
<p className="pilot-note">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.</p>
</section>
<section className="pilot-chapter" id="pilot-install" aria-labelledby="pilot-install-title">
<p className="pilot-index">Try it / Source-owned components</p>
<h2 id="pilot-install-title">Bring one into your project.</h2>
<p>Use a React + TypeScript project already configured for shadcn. Pick one component to start.</p>
<pre><code>{`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`}</code></pre>
<a className="pilot-action" href={`${site}/docs/registry/README.md`}>Read the install guide <span aria-hidden>↗</span></a>
</section>
<footer>Web Design Studio · Registry pilot · No account or API key required</footer>
<style>{`
[data-registry-demo]{--background:#171914;--foreground:#f3f0e6;--card:#22261e;--card-foreground:#f3f0e6;--muted-foreground:#c0c6b5;--border:#4c5740;--ring:#d5ff73;--radius:.35rem;
max-width:none;margin:0;padding:0;background:var(--background);color:var(--foreground);font:16px/1.6 system-ui,sans-serif}
[data-registry-demo] *{box-sizing:border-box}
[data-registry-demo] header,[data-registry-demo] footer{display:flex;justify-content:space-between;gap:1rem;flex-wrap:wrap;padding:1.3rem clamp(1.25rem,6vw,6rem);border-bottom:1px solid var(--border);font-size:.85rem}
[data-registry-demo] footer{border-top:1px solid var(--border);border-bottom:0;color:var(--muted-foreground)}
[data-registry-demo] a{color:inherit;text-underline-offset:.25em}
[data-registry-demo] :focus-visible{outline:2px solid var(--ring);outline-offset:5px}
[data-registry-demo] h1,[data-registry-demo] h2{font-family:Georgia,serif;font-weight:400;line-height:1.1;text-wrap:balance}
[data-registry-demo] h1{font-size:clamp(3.5rem,8vw,7rem);max-width:11ch;margin:1.5rem 0}
[data-registry-demo] h2{font-size:clamp(2rem,4vw,3.8rem);margin:1rem 0}
[data-registry-demo] p{max-width:62ch}
[data-registry-demo] .pilot-intro,[data-registry-demo] .pilot-chapter{padding:clamp(3.5rem,8vw,7rem) clamp(1.25rem,6vw,6rem)}
[data-registry-demo] .pilot-chapter{border-top:1px solid var(--border)}
[data-registry-demo] .pilot-index,[data-registry-demo] .pilot-kicker{color:#d5ff73;font-size:.85rem;letter-spacing:.04em}
[data-registry-demo] .pilot-index{margin:0 0 3rem}
[data-registry-demo] .pilot-action{display:inline-flex;gap:2rem;align-items:center;margin-top:1.5rem;padding:.75rem 1rem;background:#d5ff73;color:#171914;text-decoration:none}
[data-registry-demo] .pilot-cards{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:2rem}
[data-registry-demo] [data-wds-tilt]{max-width:30rem !important}
[data-registry-demo] [data-wds-tilt] h2{font-size:1.8rem !important}
[data-registry-demo] .pilot-note{font-size:.9rem;color:var(--muted-foreground)}
[data-registry-demo] pre{white-space:pre-wrap;overflow-wrap:anywhere;background:var(--card);border:1px solid var(--border);padding:1.2rem;font-size:.85rem}
@media(max-width:680px){[data-registry-demo] .pilot-cards{grid-template-columns:1fr}}
`}</style>
</main>;
}
84 changes: 84 additions & 0 deletions docs/registry/trial.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# 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.

- 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, 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
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.
19 changes: 16 additions & 3 deletions tools/registry/consumers.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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' };
Expand Down Expand Up @@ -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');
Expand Down
24 changes: 18 additions & 6 deletions tools/registry/verify-consumers.mjs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -98,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();
Expand All @@ -114,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);
Expand Down
Loading