@milesroxas/design-hub 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/scripts/bootstrap.d.ts +4 -0
- package/dist/scripts/bootstrap.js +34 -7
- package/dist/scripts/lib/vercel.d.ts +3 -1
- package/dist/scripts/lib/vercel.js +5 -2
- package/dist/vite/lapse.d.ts +6 -0
- package/dist/vite/lapse.js +42 -0
- package/dist/vite/plugin.js +2 -0
- package/kit/prefs.ts +3 -1
- package/package.json +1 -4
- package/shell/main.tsx +4 -4
- package/shell/registry.virtual.d.ts +9 -0
- package/skills/miles-deploy/SKILL.md +4 -3
- package/skills/miles-prototype/references/motion.md +1 -1
- package/skills/miles-prototype/references/performance.md +2 -2
|
@@ -29,6 +29,10 @@ export declare const spec: {
|
|
|
29
29
|
readonly type: 'string';
|
|
30
30
|
readonly description: 'Link this Vercel project and set the password gate env on it.';
|
|
31
31
|
};
|
|
32
|
+
readonly team: {
|
|
33
|
+
readonly type: 'string';
|
|
34
|
+
readonly description: 'The Vercel team (its slug) the project lives in or is created in. Absent, the CLI’s current team.';
|
|
35
|
+
};
|
|
32
36
|
readonly 'gate-password': {
|
|
33
37
|
readonly type: 'string';
|
|
34
38
|
readonly description: 'The share password (8+ characters). Given again, it rotates.';
|
|
@@ -12,7 +12,7 @@ import { readEnv, writeEnv } from '../server/env.js';
|
|
|
12
12
|
import { ROOT, TEMPLATES } from '../server/paths.js';
|
|
13
13
|
import { CliError } from './lib/cli.js';
|
|
14
14
|
import { holderOf, pickPort, readClaims, writeClaim } from './lib/ports.js';
|
|
15
|
-
import { ENVIRONMENTS, envNames, linkedProject, STORE_ENV, setEnv, vercel, } from './lib/vercel.js';
|
|
15
|
+
import { ENVIRONMENTS, envNames, linkedOrg, linkedProject, STORE_ENV, setEnv, vercel, } from './lib/vercel.js';
|
|
16
16
|
export const spec = {
|
|
17
17
|
name: 'bootstrap',
|
|
18
18
|
summary: 'Set the project up on this machine, and optionally on Vercel. Safe to run again.',
|
|
@@ -26,6 +26,10 @@ export const spec = {
|
|
|
26
26
|
conductor: { type: 'boolean', description: 'Write .conductor/settings.toml for Conductor workspaces.' },
|
|
27
27
|
branch: { type: 'string', default: 'main', description: 'The branch Conductor workspaces land on.' },
|
|
28
28
|
vercel: { type: 'string', description: 'Link this Vercel project and set the password gate env on it.' },
|
|
29
|
+
team: {
|
|
30
|
+
type: 'string',
|
|
31
|
+
description: 'The Vercel team (its slug) the project lives in or is created in. Absent, the CLI’s current team.',
|
|
32
|
+
},
|
|
29
33
|
'gate-password': {
|
|
30
34
|
type: 'string',
|
|
31
35
|
description: 'The share password (8+ characters). Given again, it rotates.',
|
|
@@ -120,13 +124,15 @@ function cli(args, input) {
|
|
|
120
124
|
});
|
|
121
125
|
return out.stdout;
|
|
122
126
|
}
|
|
123
|
-
function link(project, warnings) {
|
|
127
|
+
function link(project, team, warnings) {
|
|
124
128
|
const current = linkedProject(ROOT);
|
|
125
129
|
if (current === project)
|
|
126
130
|
return 'kept';
|
|
127
131
|
if (current)
|
|
128
132
|
warnings.push(`.vercel/project.json pointed at ${current}; relinked to ${project}.`);
|
|
129
|
-
|
|
133
|
+
// A new project is created in the scope given; without one the CLI's current team wins, which
|
|
134
|
+
// is whichever team the user last switched to, not necessarily where this project belongs.
|
|
135
|
+
cli(['link', '--yes', '--project', project, ...(team ? ['--scope', team] : [])]);
|
|
130
136
|
return 'ran';
|
|
131
137
|
}
|
|
132
138
|
/** Sets each value where it is missing; `rotate` names the ones given again, which replace what is there. */
|
|
@@ -169,7 +175,21 @@ function pullValues(target, names) {
|
|
|
169
175
|
rmSync(dir, { recursive: true, force: true });
|
|
170
176
|
}
|
|
171
177
|
}
|
|
172
|
-
|
|
178
|
+
/** `--scope` for the commands that name the project instead of reading the link. */
|
|
179
|
+
const scopeArgs = (team) => {
|
|
180
|
+
const scope = team ?? linkedOrg(ROOT);
|
|
181
|
+
return scope ? ['--scope', scope] : [];
|
|
182
|
+
};
|
|
183
|
+
/**
|
|
184
|
+
* Vercel Authentication is on for every new project on a team, and it sends each
|
|
185
|
+
* visitor to a Vercel login before the gate. The gate is the protection, so it goes
|
|
186
|
+
* off. Idempotent: disabling twice is fine.
|
|
187
|
+
*/
|
|
188
|
+
function protection(project, team) {
|
|
189
|
+
cli(['project', 'protection', 'disable', project, '--sso', ...scopeArgs(team)]);
|
|
190
|
+
return 'off';
|
|
191
|
+
}
|
|
192
|
+
function provisionReview(project, team, warnings) {
|
|
173
193
|
const names = envNames(ROOT, 'production');
|
|
174
194
|
let store = 'kept';
|
|
175
195
|
if (!names || !STORE_ENV.every((n) => names.has(n))) {
|
|
@@ -186,6 +206,8 @@ function provisionReview(project, warnings) {
|
|
|
186
206
|
'-e',
|
|
187
207
|
'development',
|
|
188
208
|
'--no-env-pull',
|
|
209
|
+
// Unlike env and deploy, this command ignores the project link and takes the CLI's current team.
|
|
210
|
+
...scopeArgs(team),
|
|
189
211
|
]);
|
|
190
212
|
if (out?.status !== 0)
|
|
191
213
|
throw new CliError('Could not add the Upstash for Redis store.', {
|
|
@@ -226,7 +248,7 @@ export function gateValues(given, env) {
|
|
|
226
248
|
return { password, secret, rotated: given !== undefined && given !== env.GATE_PASSWORD };
|
|
227
249
|
}
|
|
228
250
|
function setUpVercel(project, flags, env, typesafeKey, warnings) {
|
|
229
|
-
const linked = link(project, warnings);
|
|
251
|
+
const linked = link(project, flags.team, warnings);
|
|
230
252
|
const { password, secret, rotated } = gateValues(flags['gate-password'], env);
|
|
231
253
|
writeEnv(ROOT, { GATE_PASSWORD: password, GATE_SECRET: secret });
|
|
232
254
|
const values = { GATE_PASSWORD: password, GATE_SECRET: secret };
|
|
@@ -236,13 +258,18 @@ function setUpVercel(project, flags, env, typesafeKey, warnings) {
|
|
|
236
258
|
if (rotated)
|
|
237
259
|
warnings.push('The password changed: every visitor signs in again.');
|
|
238
260
|
const result = {
|
|
239
|
-
vercel: {
|
|
261
|
+
vercel: {
|
|
262
|
+
project,
|
|
263
|
+
linked,
|
|
264
|
+
protection: protection(project, flags.team),
|
|
265
|
+
env: ensureEnv(values, new Set(rotated ? ['GATE_PASSWORD'] : [])),
|
|
266
|
+
},
|
|
240
267
|
share: { password },
|
|
241
268
|
};
|
|
242
269
|
if (!flags.review)
|
|
243
270
|
return result;
|
|
244
271
|
warnings.push('Redeploy so the new env reaches the site; then design-hub people invite --name "<you>" --role editor.');
|
|
245
|
-
return { ...result, review: provisionReview(project, warnings) };
|
|
272
|
+
return { ...result, review: provisionReview(project, flags.team, warnings) };
|
|
246
273
|
}
|
|
247
274
|
function envIgnored(warnings) {
|
|
248
275
|
try {
|
|
@@ -8,7 +8,9 @@ export type Run = {
|
|
|
8
8
|
stderr: string;
|
|
9
9
|
};
|
|
10
10
|
export declare function vercel(cwd: string, args: string[], input?: string): Run | null;
|
|
11
|
-
export declare
|
|
11
|
+
export declare const linkedProject: (cwd: string) => string | null;
|
|
12
|
+
/** The team the linked project belongs to. The commands that take a project name instead of the link need it as `--scope`. */
|
|
13
|
+
export declare const linkedOrg: (cwd: string) => string | null;
|
|
12
14
|
/** The variable names set for a target, read from `vercel env ls`'s table. */
|
|
13
15
|
export declare function envNames(cwd: string, target: Target): Set<string> | null;
|
|
14
16
|
/** Sets one variable for one target; `force` replaces a value that is already there. */
|
|
@@ -15,17 +15,20 @@ export function vercel(cwd, args, input) {
|
|
|
15
15
|
return null;
|
|
16
16
|
return { status: out.status ?? 1, stdout: out.stdout ?? '', stderr: out.stderr ?? '' };
|
|
17
17
|
}
|
|
18
|
-
|
|
18
|
+
function readLink(cwd) {
|
|
19
19
|
const file = join(cwd, '.vercel', 'project.json');
|
|
20
20
|
if (!existsSync(file))
|
|
21
21
|
return null;
|
|
22
22
|
try {
|
|
23
|
-
return JSON.parse(readFileSync(file, 'utf8'))
|
|
23
|
+
return JSON.parse(readFileSync(file, 'utf8'));
|
|
24
24
|
}
|
|
25
25
|
catch {
|
|
26
26
|
return null;
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
|
+
export const linkedProject = (cwd) => readLink(cwd)?.projectName ?? null;
|
|
30
|
+
/** The team the linked project belongs to. The commands that take a project name instead of the link need it as `--scope`. */
|
|
31
|
+
export const linkedOrg = (cwd) => readLink(cwd)?.orgId ?? null;
|
|
29
32
|
/** The variable names set for a target, read from `vercel env ls`'s table. */
|
|
30
33
|
export function envNames(cwd, target) {
|
|
31
34
|
const out = vercel(cwd, ['env', 'ls', target]);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Plugin } from 'vite';
|
|
2
|
+
/** Whether the project (never the hub) has Lapse installed. */
|
|
3
|
+
export declare const hasLapse: (root: string) => boolean;
|
|
4
|
+
/** The module's source: a re-export of the resolved panel, or a null mount. */
|
|
5
|
+
export declare function lapseSource(panel: string | null): string;
|
|
6
|
+
export declare function lapse(root: string): Plugin;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
// `virtual:design-hub/lapse`: the Lapse panel when the project has installed
|
|
3
|
+
// @aiforui/lapse, else nothing. Lapse lives on a private registry, so the hub
|
|
4
|
+
// never depends on it: a project opts in with `pnpm add -D @aiforui/lapse`
|
|
5
|
+
// and ?lapse loads it from the project's node_modules. The build always gets
|
|
6
|
+
// the empty module, since Lapse is a dev tool and Rollup must not resolve it
|
|
7
|
+
// on Vercel.
|
|
8
|
+
import { existsSync } from 'node:fs';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
const LAPSE_ID = 'virtual:design-hub/lapse';
|
|
11
|
+
const RESOLVED = `\0${LAPSE_ID}`;
|
|
12
|
+
const LAPSE_PACKAGE = '@aiforui/lapse';
|
|
13
|
+
const LAPSE_PANEL = `${LAPSE_PACKAGE}/panel`;
|
|
14
|
+
/** Whether the project (never the hub) has Lapse installed. */
|
|
15
|
+
export const hasLapse = (root) => existsSync(join(root, 'node_modules', ...LAPSE_PACKAGE.split('/'), 'package.json'));
|
|
16
|
+
/** The module's source: a re-export of the resolved panel, or a null mount. */
|
|
17
|
+
export function lapseSource(panel) {
|
|
18
|
+
return panel
|
|
19
|
+
? `export { mountLapse } from ${JSON.stringify(panel)};\n`
|
|
20
|
+
: 'export const mountLapse = null;\n';
|
|
21
|
+
}
|
|
22
|
+
export function lapse(root) {
|
|
23
|
+
let serve = false;
|
|
24
|
+
return {
|
|
25
|
+
name: 'design-hub-lapse',
|
|
26
|
+
configResolved(config) {
|
|
27
|
+
serve = config.command === 'serve';
|
|
28
|
+
},
|
|
29
|
+
resolveId(id) {
|
|
30
|
+
return id === LAPSE_ID ? RESOLVED : null;
|
|
31
|
+
},
|
|
32
|
+
async load(id) {
|
|
33
|
+
if (id !== RESOLVED)
|
|
34
|
+
return null;
|
|
35
|
+
if (!serve || !hasLapse(root))
|
|
36
|
+
return lapseSource(null);
|
|
37
|
+
// Resolved from the project, so the package's exports map and pnpm's layout both apply.
|
|
38
|
+
const panel = await this.resolve(LAPSE_PANEL, join(root, 'index.html'));
|
|
39
|
+
return lapseSource(panel?.id ?? null);
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
package/dist/vite/plugin.js
CHANGED
|
@@ -14,6 +14,7 @@ import { devPort, readEnv } from '../server/env.js';
|
|
|
14
14
|
import { PACKAGE, PACKAGE_KIT, SHELL } from '../server/paths.js';
|
|
15
15
|
import { GATE_FONT, gatePreview } from '../vercel/proxy.js';
|
|
16
16
|
import { kitResolver } from './kit.js';
|
|
17
|
+
import { lapse } from './lapse.js';
|
|
17
18
|
import { markdownDocs } from './markdown.js';
|
|
18
19
|
import { registry } from './registry.js';
|
|
19
20
|
const require = createRequire(import.meta.url);
|
|
@@ -52,6 +53,7 @@ export function designHub({ root }) {
|
|
|
52
53
|
react(),
|
|
53
54
|
kitResolver(join(root, 'kit'), PACKAGE_KIT),
|
|
54
55
|
registry(root),
|
|
56
|
+
lapse(root),
|
|
55
57
|
markdownDocs(),
|
|
56
58
|
devReview(),
|
|
57
59
|
gateFont(),
|
package/kit/prefs.ts
CHANGED
|
@@ -5,7 +5,8 @@ import { useSyncExternalStore } from 'react';
|
|
|
5
5
|
|
|
6
6
|
export type Appearance = 'system' | 'light' | 'dark';
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
/** What the Lapse panel hands back when a project has it installed: its clock engine. */
|
|
9
|
+
export type LapseHandle = { engine: { setSpeed(x: number): void; getSpeed(): number } };
|
|
9
10
|
|
|
10
11
|
const root = document.documentElement;
|
|
11
12
|
const darkQuery = matchMedia('(prefers-color-scheme: dark)');
|
|
@@ -77,6 +78,7 @@ export const useAppearance = () => useSyncExternalStore(subscribe, () => appeara
|
|
|
77
78
|
export const useDark = () => useSyncExternalStore(subscribe, isDark);
|
|
78
79
|
export const useReducedMotion = () => useSyncExternalStore(subscribe, prefersReducedMotion);
|
|
79
80
|
|
|
81
|
+
/** Lapse owns the clocks once attached; the speed dial drives it instead of retiming animations itself. */
|
|
80
82
|
export function attachLapse(handle: LapseHandle) {
|
|
81
83
|
lapse = handle;
|
|
82
84
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@milesroxas/design-hub",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "The Design Hub product: the hub that lists and serves prototypes, the review panel and its API, the password gate, the kit, and the design-hub CLI. A project installs it and keeps only its prototypes, kit materials and docs.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -77,9 +77,6 @@
|
|
|
77
77
|
"react": "^19.3.1",
|
|
78
78
|
"react-dom": "^19.3.1"
|
|
79
79
|
},
|
|
80
|
-
"optionalDependencies": {
|
|
81
|
-
"@aiforui/lapse": "^0.19.0"
|
|
82
|
-
},
|
|
83
80
|
"scripts": {
|
|
84
81
|
"build": "rm -rf dist && tsc -p tsconfig.build.json",
|
|
85
82
|
"check": "tsc --noEmit && biome check .",
|
package/shell/main.tsx
CHANGED
|
@@ -13,11 +13,11 @@ import { findEntry, guides } from './registry';
|
|
|
13
13
|
const params = new URLSearchParams(location.search);
|
|
14
14
|
|
|
15
15
|
// Lapse patches the clocks, so it lands before any prototype (and Motion) loads.
|
|
16
|
+
// The module is empty unless the project installed @aiforui/lapse (vite/lapse.ts).
|
|
16
17
|
if (import.meta.env.DEV && params.has('lapse')) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
attachLapse(mountLapse({ position: 'bottom-left' }));
|
|
18
|
+
const { mountLapse } = await import('virtual:design-hub/lapse');
|
|
19
|
+
if (mountLapse) attachLapse(mountLapse({ position: 'bottom-left' }));
|
|
20
|
+
else console.warn('[design-hub] ?lapse needs Lapse in the project: pnpm add -D @aiforui/lapse');
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
if (import.meta.env.DEV && params.get('a11y') !== '0') {
|
|
@@ -8,3 +8,12 @@ declare module 'virtual:design-hub/registry' {
|
|
|
8
8
|
/** Each feature's compiled walkthrough loader, by feature. */
|
|
9
9
|
export const guideDocs: Record<string, () => Promise<Doc>>;
|
|
10
10
|
}
|
|
11
|
+
|
|
12
|
+
declare module 'virtual:design-hub/lapse' {
|
|
13
|
+
/** The Lapse panel's mount, or null when the project has not installed @aiforui/lapse (vite/lapse.ts). */
|
|
14
|
+
export const mountLapse:
|
|
15
|
+
| ((options: { position: 'bottom-left' | 'bottom-right' | 'top-left' | 'top-right' }) => {
|
|
16
|
+
engine: { setSpeed(x: number): void; getSpeed(): number };
|
|
17
|
+
})
|
|
18
|
+
| null;
|
|
19
|
+
}
|
|
@@ -34,11 +34,12 @@ Never send the link with a failed step, and never skip one with `--skip` without
|
|
|
34
34
|
1. The user signs in once, in their own terminal: `vercel login`. It is interactive; never run it for them.
|
|
35
35
|
2. From the project root:
|
|
36
36
|
```bash
|
|
37
|
-
pnpm bootstrap --vercel <project-name>
|
|
37
|
+
pnpm bootstrap --vercel <project-name> --team <team-slug>
|
|
38
38
|
```
|
|
39
|
-
It links the project, writes `GATE_PASSWORD` (12 readable characters, or `--gate-password <word>`) and a fresh `GATE_SECRET` to `.env` and to Production and Preview, and prints the password in `share.password`. A second run keeps what is set.
|
|
39
|
+
`--team` is the Vercel team the project is created in (`vercel teams ls` lists them); without it the CLI's current team wins, which is often not the one the user means. It links the project, writes `GATE_PASSWORD` (12 readable characters, or `--gate-password <word>`) and a fresh `GATE_SECRET` to `.env` and to Production and Preview, and prints the password in `share.password`. A second run keeps what is set.
|
|
40
40
|
3. `pnpm preflight`, then push `main` (or `vercel --prod` from the project root).
|
|
41
|
-
4.
|
|
41
|
+
4. On a Pro team Vercel turns Vercel Authentication on for every new project, which sends each visitor to a Vercel login before the gate. Turn it off for Production once: Project Settings, Deployment Protection, Vercel Authentication, set to Only Preview Deployments or Disabled. The hub's gate is the protection. Test by opening the production URL in a private window: the password page must appear, not vercel.com.
|
|
42
|
+
5. The handover message, in one message: the URL, the password, which prototype to open (the sprint's slug), the walkthrough at `/guide/<feature>/`, and that Controls is the round button at the top of the page. A reviewer who is not told about Controls reviews one state. Never send `GATE_SECRET`.
|
|
42
43
|
|
|
43
44
|
## Comments
|
|
44
45
|
|
|
@@ -55,5 +55,5 @@ In JS, `useReducedMotion()` from `@kit/prefs`. Keep fades and color changes, dro
|
|
|
55
55
|
## Tuning and review
|
|
56
56
|
|
|
57
57
|
- Every number you are unsure of goes on a dial (`DialVars` for CSS, `useDials` for JS). The decision is the number the user settles on; copy it from DialKit into the README.
|
|
58
|
-
- Watch at 0.25x
|
|
58
|
+
- Watch at 0.25x with the Space speed dial, or `?lapse` for frame-by-frame takes, scrubbing and a jank report when the project has `@aiforui/lapse` installed.
|
|
59
59
|
- Test the interruption: open, then close mid-open. It should reverse from where it is.
|
|
@@ -5,7 +5,7 @@ A prototype that stutters gets judged on the stutter. The floor below keeps the
|
|
|
5
5
|
## Already handled by the shell
|
|
6
6
|
|
|
7
7
|
- Each prototype is its own lazy chunk; the index loads none of them.
|
|
8
|
-
- Dev-only tools (axe, Lapse) never ship in a build and load only when used.
|
|
8
|
+
- Dev-only tools (axe, and Lapse when a project installs it) never ship in a build and load only when used.
|
|
9
9
|
- `motion` and DialKit load only in prototypes that import them.
|
|
10
10
|
|
|
11
11
|
## Rendering
|
|
@@ -41,5 +41,5 @@ A prototype that stutters gets judged on the stutter. The floor below keeps the
|
|
|
41
41
|
## How to check
|
|
42
42
|
|
|
43
43
|
1. Chrome Performance panel with 4x CPU throttling on the main interaction: no long tasks over 50ms during an animation.
|
|
44
|
-
2. `?lapse
|
|
44
|
+
2. With `@aiforui/lapse` installed, `?lapse` and record a take of the interaction: its report lists dropped frames, layout shifts and which properties animated on layout or paint.
|
|
45
45
|
3. React DevTools Profiler, "Highlight updates": a slider or a tick should light up only what it changes.
|