@milesroxas/design-hub 0.1.0 → 0.1.2

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.
@@ -11,7 +11,8 @@
11
11
  "!**/.impeccable",
12
12
  "!**/.claude",
13
13
  "!**/*.md",
14
- "!**/pnpm-lock.yaml"
14
+ "!**/pnpm-lock.yaml",
15
+ "!**/pnpm-workspace.yaml"
15
16
  ]
16
17
  },
17
18
  "formatter": {
@@ -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
+ }
@@ -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
- type LapseHandle = { engine: { setSpeed(x: number): void; getSpeed(): number } };
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.0",
3
+ "version": "0.1.2",
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
- // Opaque specifier: the package is optional and absent on Vercel, so Rollup must not resolve it.
18
- const panel = '@aiforui/lapse/panel';
19
- const { mountLapse } = await import(/* @vite-ignore */ panel);
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
+ }
@@ -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: the Space speed dial, or `?lapse` for frame-by-frame takes, scrubbing and a jank report.
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`, record a take of the interaction: its report lists dropped frames, layout shifts and which properties animated on layout or paint.
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.