@milesroxas/create-design-hub 0.0.0-stage → 0.1.1

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.
Files changed (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +14 -2
  3. package/index.mjs +154 -0
  4. package/lib.mjs +65 -0
  5. package/package.json +37 -3
  6. package/setup/.fallowrc.jsonc +6 -0
  7. package/setup/app.html +861 -0
  8. package/setup/lib.mjs +147 -0
  9. package/setup/lib.test.mjs +96 -0
  10. package/setup/server.mjs +444 -0
  11. package/setup/server.test.mjs +99 -0
  12. package/setup/slug.mjs +15 -0
  13. package/template/.claude/launch.json +18 -0
  14. package/template/.claude/settings.json +35 -0
  15. package/template/.design-hub/checks.json +9 -0
  16. package/template/.env.example +28 -0
  17. package/template/.impeccable/config.json +16 -0
  18. package/template/AGENTS.md +133 -0
  19. package/template/CLAUDE.md +1 -0
  20. package/template/DESIGN.md +126 -0
  21. package/template/Design Hub.command +11 -0
  22. package/template/PRODUCT.md +65 -0
  23. package/template/README.md +20 -0
  24. package/template/_gitattributes +2 -0
  25. package/template/_gitignore +41 -0
  26. package/template/api/review.ts +5 -0
  27. package/template/docs/.obsidian/app.json +6 -0
  28. package/template/docs/.obsidian/appearance.json +1 -0
  29. package/template/docs/.obsidian/core-plugins.json +32 -0
  30. package/template/docs/README.md +19 -0
  31. package/template/docs/sprints/sample/guide.md +69 -0
  32. package/template/kit/AppShell.tsx +149 -0
  33. package/template/kit/SiteShell.tsx +75 -0
  34. package/template/kit/app-shell.css +70 -0
  35. package/template/kit/icons.ts +11 -0
  36. package/template/kit/shell.ts +59 -0
  37. package/template/kit/site-shell.css +27 -0
  38. package/template/kit/tokens.css +249 -0
  39. package/template/package.json +38 -0
  40. package/template/plans/README.md +11 -0
  41. package/template/prototypes/sample-list/README.md +51 -0
  42. package/template/prototypes/sample-list/fixtures.json +87 -0
  43. package/template/prototypes/sample-list/index.tsx +122 -0
  44. package/template/prototypes/sample-list/styles.css +25 -0
  45. package/template/proxy.ts +4 -0
  46. package/template/sketches/paper/README.md +3 -0
  47. package/template/tsconfig.json +10 -0
  48. package/template/vercel.json +49 -0
@@ -0,0 +1,249 @@
1
+ /* The neutral default scale. A project replaces these values with its own
2
+ design's tokens (miles-paper-to-prototype reads them from Paper) and keeps
3
+ the names, so every kit stylesheet and every prototype keeps working.
4
+ Light on :root, dark on .dark-mode (prefs.ts mirrors the theme there). */
5
+
6
+ :root {
7
+ color-scheme: light;
8
+
9
+ /* Primitives: the only place a hex is allowed outside this file is a
10
+ README that names the decision. */
11
+ --brand-50: #eff4ff; --brand-100: #dbe6fe; --brand-200: #b9cdfc; --brand-300: #8fb0f9;
12
+ --brand-400: #5f8bf3; --brand-500: #3d6ee8; --brand-600: #2b57cc; --brand-700: #2246a6;
13
+ --gray-25: #fcfcfd; --gray-50: #f9fafb; --gray-100: #f2f4f7; --gray-200: #eaecf0;
14
+ --gray-300: #d0d5dd; --gray-400: #98a2b3; --gray-500: #667085; --gray-600: #475467;
15
+ --gray-700: #344054; --gray-800: #182230; --gray-900: #101828; --gray-950: #0c111d;
16
+ --error-500: #f04438;
17
+
18
+ /* Semantic: text */
19
+ --text-primary: #101828;
20
+ --text-secondary: #344054;
21
+ --text-secondary_hover: #182230;
22
+ --text-tertiary: #475467;
23
+ --text-quaternary: #667085;
24
+ --text-disabled: #667085;
25
+ --text-placeholder: #667085;
26
+ --text-white: #ffffff;
27
+ --text-brand-primary: #2246a6;
28
+ --text-brand-secondary: #2b57cc;
29
+ --text-brand-tertiary: #3d6ee8;
30
+ --text-error-primary: #d92d20;
31
+ --text-error-primary_hover: #b32318; /* a destructive word under the pointer on a hover fill (the Hover Ink Rule) */
32
+ --text-warning-primary: #dc6803;
33
+ --text-success-primary: #079455;
34
+
35
+ /* Semantic: borders */
36
+ --border-primary: #d0d5dd;
37
+ --border-secondary: #eaecf0;
38
+ --border-tertiary: #f2f4f7;
39
+ --border-disabled: #d0d5dd;
40
+ --border-brand: #3d6ee8;
41
+ --border-brand_alt: #2b57cc;
42
+ --border-error: #f04438;
43
+
44
+ /* Semantic: foreground (icons) */
45
+ --fg-primary: #101828;
46
+ --fg-secondary: #344054;
47
+ --fg-tertiary: #475467;
48
+ --fg-quaternary: #667085;
49
+ --fg-quinary: #98a2b3;
50
+ --fg-white: #ffffff;
51
+ --fg-disabled: #98a2b3;
52
+ --fg-brand-primary: #2b57cc;
53
+ --fg-brand-secondary: #3d6ee8;
54
+ --fg-error-primary: #d92d20;
55
+ --fg-warning-primary: #dc6803;
56
+ --fg-success-primary: #079455;
57
+
58
+ /* Semantic: backgrounds */
59
+ --bg-primary: #ffffff;
60
+ --bg-primary_alt: #ffffff;
61
+ --bg-primary_hover: #f9fafb;
62
+ --bg-secondary: #f9fafb;
63
+ --bg-secondary_alt: #f9fafb;
64
+ --bg-secondary_hover: #f2f4f7;
65
+ --bg-secondary_hover_deep: #eaecf0; /* hover on a row that already rests on bg-secondary */
66
+ --bg-secondary_subtle: #fcfcfd;
67
+ --bg-tertiary: #f2f4f7;
68
+ --bg-active: #f2f4f7;
69
+ --bg-disabled: #f2f4f7;
70
+ --bg-overlay: #0c111d;
71
+ --bg-brand-primary: #eff4ff;
72
+ --bg-brand-secondary: #dbe6fe;
73
+ --bg-brand-solid: #2b57cc;
74
+ --bg-brand-solid_hover: #2246a6;
75
+ --bg-error-primary: #fef3f2;
76
+ --bg-error-secondary: #fee4e2;
77
+ --bg-error-solid: #d92d20;
78
+ --bg-warning-primary: #fffaeb;
79
+ --bg-success-primary: #ecfdf3;
80
+
81
+ /* Component */
82
+ --button-primary-bg: #2b57cc;
83
+ --button-primary-bg_hover: #2246a6;
84
+ --button-primary-fg: #ffffff;
85
+ --button-secondary-bg: #ffffff;
86
+ --button-secondary-bg_hover: #f9fafb;
87
+ --button-secondary-bg_open: #f2f4f7; /* menu open or pressed: one step past hover (the Engaged State Rule) */
88
+ --button-secondary-bg_deep: #eaecf0; /* open or pressed on a surface that already wears the hover fill */
89
+ --button-secondary-fg: #344054;
90
+ --button-secondary-fg_hover: #182230;
91
+ --button-secondary-border: #d0d5dd;
92
+ --button-tertiary-fg: #475467;
93
+ --button-tertiary-fg_hover: #344054;
94
+ --button-tertiary-bg_hover: #f9fafb;
95
+ --button-primary-error-bg: #d92d20;
96
+ --button-primary-error-bg_hover: #b32318;
97
+ --button-secondary-error-fg: #b32318;
98
+ --button-secondary-error-border: #fda19b;
99
+ --button-secondary-error-bg_hover: #fef3f2;
100
+ --nav-item-icon-fg: #667085;
101
+ --nav-item-icon-fg_active: #344054;
102
+ --avatar-bg: #f2f4f7;
103
+ --avatar-contrast-border: rgb(0 0 0 / 0.08);
104
+
105
+ /* Utility: badges */
106
+ --utility-gray-50: #f9fafb; --utility-gray-200: #eaecf0; --utility-gray-700: #344054;
107
+ --utility-brand-50: #eff4ff; --utility-brand-200: #b9cdfc; --utility-brand-700: #2246a6;
108
+ --utility-error-50: #fef3f2; --utility-error-200: #fecdc9; --utility-error-600: #d92d20; --utility-error-700: #b32318;
109
+ --utility-warning-50: #fffaeb; --utility-warning-200: #fedf89; --utility-warning-600: #dc6803; --utility-warning-700: #b54708;
110
+ --utility-success-50: #ecfdf3; --utility-success-200: #a9efc5; --utility-success-600: #079455; --utility-success-700: #067647;
111
+
112
+ /* Shadows, tinted gray-900 in light mode; dark draws none (the Hairline First Rule) */
113
+ --shadow-xs: 0 1px 2px 0 rgb(16 24 40 / 0.05);
114
+ --shadow-sm: 0 1px 3px 0 rgb(16 24 40 / 0.10), 0 1px 2px 0 rgb(16 24 40 / 0.06);
115
+ --shadow-md: 0 4px 8px -2px rgb(16 24 40 / 0.10), 0 2px 4px -2px rgb(16 24 40 / 0.06);
116
+ --shadow-lg: 0 12px 16px -4px rgb(16 24 40 / 0.08), 0 4px 6px -2px rgb(16 24 40 / 0.03);
117
+ --shadow-xl: 0 20px 24px -4px rgb(16 24 40 / 0.08), 0 8px 8px -4px rgb(16 24 40 / 0.03);
118
+ --focus-ring: 0 0 0 2px #ffffff, 0 0 0 4px #3d6ee8;
119
+ --focus-ring-error: 0 0 0 2px #ffffff, 0 0 0 4px #f04438;
120
+
121
+ /* Radius */
122
+ --radius-xxs: 2px; --radius-xs: 4px; --radius-sm: 6px; --radius-md: 8px;
123
+ --radius-lg: 10px; --radius-xl: 12px; --radius-2xl: 16px; --radius-full: 9999px;
124
+
125
+ /* Type. No font files ship with the template: a project adds its own
126
+ @font-face here and names it first in --font. */
127
+ --font: "Inter", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
128
+ --font-voice: Georgia, "Times New Roman", serif;
129
+
130
+ /* Motion, one set so it can be reviewed as one. ease-out enters, ease-in
131
+ leaves, ease-hover for fills and colours, ease-press for a press. */
132
+ --ease-out: cubic-bezier(0.23, 1, 0.32, 1);
133
+ --ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);
134
+ --ease-drawer: cubic-bezier(0.32, 0.72, 0, 1);
135
+ --ease-press: cubic-bezier(0.33, 1, 0.68, 1); /* easeOutCubic: an even settle, no snap */
136
+ --ease-morph: cubic-bezier(0.45, 0, 0.55, 1); /* easeInOutQuad: a glyph changing shape in place */
137
+ --ease-hover: cubic-bezier(0.25, 0.46, 0.45, 0.94); /* easeOutQuad: gentler than `ease` for fills */
138
+ --ease-in: cubic-bezier(0.4, 0, 1, 1); /* easeInQuad: a layer leaving accelerates away */
139
+ --dur-press: 160ms;
140
+ --dur-press-in: 90ms;
141
+ --dur-press-out: 160ms;
142
+ --dur-hover: 120ms;
143
+ --dur-menu: 180ms;
144
+ --dur-menu-out: 120ms;
145
+ --dur-dialog: 220ms;
146
+ --dur-dialog-out: 150ms;
147
+ --dur-sheet: 320ms;
148
+ --dur-toast: 260ms;
149
+ --dur-toast-out: 180ms;
150
+ --dur-swap-out: 240ms;
151
+ --dur-swap-in: 380ms;
152
+
153
+ /* The harness material (--harness-*) and the hub's fonts live in
154
+ the hub's own system.css: they belong to the hub, not the design. */
155
+ }
156
+
157
+ .dark-mode {
158
+ color-scheme: dark;
159
+
160
+ --gray-25: #fafafa; --gray-50: #f7f7f7; --gray-100: #f0f1f1; --gray-200: #ececed;
161
+ --gray-300: #cecfd2; --gray-400: #94969c; --gray-500: #85888e; --gray-600: #61656c;
162
+ --gray-700: #373a41; --gray-800: #22262f; --gray-900: #13161b; --gray-950: #0c0e12;
163
+
164
+ --text-primary: #f7f7f7;
165
+ --text-secondary: #cecfd2;
166
+ --text-secondary_hover: #ececed;
167
+ --text-tertiary: #94969c;
168
+ --text-quaternary: #94969c;
169
+ --text-disabled: #85888e;
170
+ --text-placeholder: #85888e;
171
+ --text-brand-primary: #f7f7f7;
172
+ --text-brand-secondary: #cecfd2;
173
+ --text-brand-tertiary: #94969c;
174
+ --text-error-primary: #f97066;
175
+ --text-error-primary_hover: #fda19b;
176
+ --text-warning-primary: #fdb022;
177
+ --text-success-primary: #47cd89;
178
+
179
+ --border-primary: #373a41;
180
+ --border-secondary: #22262f;
181
+ --border-tertiary: #22262f;
182
+ --border-disabled: #373a41;
183
+ --border-brand: #5f8bf3;
184
+ --border-brand_alt: #373a41;
185
+ --border-error: #f97066;
186
+
187
+ --fg-primary: #ffffff;
188
+ --fg-secondary: #cecfd2;
189
+ --fg-tertiary: #94969c;
190
+ --fg-quaternary: #94969c;
191
+ --fg-quinary: #85888e;
192
+ --fg-disabled: #85888e;
193
+ --fg-brand-primary: #3d6ee8;
194
+ --fg-brand-secondary: #2b57cc;
195
+ --fg-error-primary: #f04438;
196
+ --fg-warning-primary: #f79009;
197
+ --fg-success-primary: #17b26a;
198
+
199
+ --bg-primary: #0c0e12;
200
+ --bg-primary_alt: #13161b;
201
+ --bg-primary_hover: #22262f;
202
+ --bg-secondary: #13161b;
203
+ --bg-secondary_alt: #0c0e12;
204
+ --bg-secondary_hover: #22262f;
205
+ --bg-secondary_hover_deep: #22262f;
206
+ --bg-secondary_subtle: #13161b;
207
+ --bg-tertiary: #22262f;
208
+ --bg-active: #22262f;
209
+ --bg-disabled: #22262f;
210
+ --bg-overlay: #22262f;
211
+ --bg-brand-primary: #3d6ee8;
212
+ --bg-brand-secondary: #2b57cc;
213
+ --bg-brand-solid: #3d6ee8;
214
+ --bg-brand-solid_hover: #5f8bf3;
215
+ --bg-error-primary: #f04438;
216
+ --bg-error-secondary: #d92d20;
217
+ --bg-warning-primary: #f79009;
218
+ --bg-success-primary: #17b26a;
219
+
220
+ --button-primary-bg: #3d6ee8;
221
+ --button-primary-bg_hover: #2b57cc;
222
+ --button-secondary-bg: #13161b;
223
+ --button-secondary-bg_hover: #22262f;
224
+ --button-secondary-bg_open: #373a41;
225
+ --button-secondary-bg_deep: #42464d; /* one even step past bg_open (oklch L 0.385), so Error Ink still passes on it */
226
+ --button-secondary-fg: #cecfd2;
227
+ --button-secondary-fg_hover: #f0f1f1;
228
+ --button-secondary-border: #373a41;
229
+ --button-tertiary-fg: #94969c;
230
+ --button-tertiary-fg_hover: #ececed;
231
+ --button-tertiary-bg_hover: #22262f;
232
+ --button-secondary-error-fg: #fecdc9;
233
+ --button-secondary-error-border: #912018;
234
+ --button-secondary-error-bg_hover: #7a271a;
235
+ --nav-item-icon-fg: #94969c;
236
+ --nav-item-icon-fg_active: #ececed;
237
+ --avatar-bg: #22262f;
238
+ --avatar-contrast-border: rgb(255 255 255 / 0.12);
239
+
240
+ --utility-gray-50: #13161b; --utility-gray-200: #373a41; --utility-gray-700: #cecfd2;
241
+ --utility-brand-50: #0f1f4d; --utility-brand-200: #1c3a8a; --utility-brand-700: #8fb0f9;
242
+ --utility-error-50: #55160c; --utility-error-200: #912018; --utility-error-600: #f97066; --utility-error-700: #fda19b;
243
+ --utility-warning-50: #4e1d09; --utility-warning-200: #93370d; --utility-warning-600: #fdb022; --utility-warning-700: #fec84b;
244
+ --utility-success-50: #053321; --utility-success-200: #085d3a; --utility-success-600: #47cd89; --utility-success-700: #75e0a7;
245
+
246
+ --shadow-xs: none; --shadow-sm: none; --shadow-md: none; --shadow-lg: none; --shadow-xl: none;
247
+ --focus-ring: 0 0 0 2px #0c0e12, 0 0 0 4px #5f8bf3;
248
+ --focus-ring-error: 0 0 0 2px #0c0e12, 0 0 0 4px #f97066;
249
+ }
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "design-hub-project",
3
+ "private": true,
4
+ "type": "module",
5
+ "engines": {
6
+ "node": ">=22.18"
7
+ },
8
+ "scripts": {
9
+ "dev": "design-hub dev",
10
+ "build": "design-hub build",
11
+ "preview": "design-hub preview",
12
+ "bootstrap": "design-hub bootstrap",
13
+ "new": "design-hub new",
14
+ "guide": "design-hub guide",
15
+ "check": "design-hub check",
16
+ "fix": "design-hub check --fix",
17
+ "guard": "design-hub guard",
18
+ "preflight": "design-hub preflight",
19
+ "comments": "design-hub comments",
20
+ "people": "design-hub people",
21
+ "calibrate": "design-hub calibrate",
22
+ "fixtures-pull": "design-hub fixtures-pull",
23
+ "visual-match": "design-hub visual-match",
24
+ "elyx": "design-hub elyx",
25
+ "skills": "design-hub skills",
26
+ "commands": "design-hub commands"
27
+ },
28
+ "dependencies": {
29
+ "@milesroxas/design-hub": "workspace:*",
30
+ "motion": "^14.6.0",
31
+ "react": "^19.3.1",
32
+ "react-dom": "^19.3.1"
33
+ },
34
+ "devDependencies": {
35
+ "@types/react": "^19.3.0",
36
+ "@types/react-dom": "^19.3.0"
37
+ }
38
+ }
@@ -0,0 +1,11 @@
1
+ # Animation plans
2
+
3
+ Plans written by the `improve-animations` skill. Each one is self-contained: an executor with no context from the session that wrote it can carry it out from the file alone. Run one with `improve-animations execute plans/NNN-slug.md`, or hand it to any agent.
4
+
5
+ | # | Plan | Severity | Status |
6
+ | --- | --- | --- | --- |
7
+ | | | | |
8
+
9
+ ## Order and dependencies
10
+
11
+ None yet. When a plan depends on another, say so here, and record where the result was written (the prototype's README and, for a rule, `DESIGN.md`).
@@ -0,0 +1,51 @@
1
+ ---
2
+ title: Sample list
3
+ summary: A list page with every fixture set on a dial, as the reference for the conventions.
4
+ context: app
5
+ status: exploring
6
+ kind: sprint
7
+ feature: sample
8
+ created: 2026-10-30
9
+ ---
10
+
11
+ # Sample list
12
+
13
+ The one prototype that ships with the template. It is the reference Claude reads for the conventions, because a working example teaches them more reliably than prose: a feature folder, the README frontmatter the index reads, hostile fixtures on a dial, motion on `DialVars`, scope toasts, and a walkthrough at `/guide/sample/`. Delete it when the project has a feature of its own.
14
+
15
+ ## Problem
16
+
17
+ A list is where edge cases hide: the name nobody expected, the empty team, the fortieth row, the item with no image. Reviewers see one pretty state and sign off on it.
18
+
19
+ ## Question
20
+
21
+ Does the row hold up under every fixture set without the layout moving?
22
+
23
+ ## Flows
24
+
25
+ | Flow | Starts from | Edge cases |
26
+ | --- | --- | --- |
27
+ | Read the list | `#/items` | longest name, no items, 40 items, no cover |
28
+ | Open an item | a row | the row is the link; Open is a scope toast |
29
+ | Add an item | New item | a scope toast; adding is not this prototype's question |
30
+
31
+ ## States
32
+
33
+ Every state the prototype holds, one row each. The fidelity guard and `miles-port` read this table.
34
+
35
+ | State | How to reach it |
36
+ | --- | --- |
37
+ | Real list | Controls → Review → Fixtures → **real** |
38
+ | Longest names | Controls → Review → Fixtures → **longest** |
39
+ | Empty | Controls → Review → Fixtures → **empty** |
40
+ | Maximum count | Controls → Review → Fixtures → **max** |
41
+ | Missing media | Controls → Review → Fixtures → **missing** |
42
+ | Compact rows | Controls → Review → Density → **compact** |
43
+ | Dark | Controls → View → Theme → **dark** |
44
+
45
+ ## Fixtures
46
+
47
+ `fixtures.json` holds `real` (eight items written from the comp; a Webflow project would pull them with one `collections_items_list_items` call) and `missing` (hand-written: no cover, no owner, no name). `@kit/fixtures` derives `longest`, `empty` and `max`; the design's maximum is 40 rows.
48
+
49
+ ## Decision
50
+
51
+ Empty until picked. What was chosen, its values, and what was rejected and why. Then delete the folder.
@@ -0,0 +1,87 @@
1
+ {
2
+ "real": [
3
+ {
4
+ "id": "1",
5
+ "name": "Spring launch page",
6
+ "owner": "Ada Lovelace",
7
+ "status": "active",
8
+ "updated": "2026-10-28",
9
+ "cover": "#dbe6fe"
10
+ },
11
+ {
12
+ "id": "2",
13
+ "name": "Pricing table refresh",
14
+ "owner": "Grace Hopper",
15
+ "status": "active",
16
+ "updated": "2026-10-27",
17
+ "cover": "#fedf89"
18
+ },
19
+ {
20
+ "id": "3",
21
+ "name": "Onboarding checklist",
22
+ "owner": "Katherine Johnson",
23
+ "status": "paused",
24
+ "updated": "2026-10-21",
25
+ "cover": "#a9efc5"
26
+ },
27
+ {
28
+ "id": "4",
29
+ "name": "Help centre search",
30
+ "owner": "Mary Jackson",
31
+ "status": "done",
32
+ "updated": "2026-10-14",
33
+ "cover": "#fecdc9"
34
+ },
35
+ {
36
+ "id": "5",
37
+ "name": "Team settings",
38
+ "owner": "Dorothy Vaughan",
39
+ "status": "active",
40
+ "updated": "2026-10-12",
41
+ "cover": "#eaecf0"
42
+ },
43
+ {
44
+ "id": "6",
45
+ "name": "Invoice email",
46
+ "owner": "Radia Perlman",
47
+ "status": "done",
48
+ "updated": "2026-10-03",
49
+ "cover": "#dbe6fe"
50
+ },
51
+ {
52
+ "id": "7",
53
+ "name": "Mobile navigation",
54
+ "owner": "Hedy Lamarr",
55
+ "status": "paused",
56
+ "updated": "2026-09-30",
57
+ "cover": "#fedf89"
58
+ },
59
+ {
60
+ "id": "8",
61
+ "name": "Changelog",
62
+ "owner": "Annie Easley",
63
+ "status": "active",
64
+ "updated": "2026-09-22",
65
+ "cover": "#a9efc5"
66
+ }
67
+ ],
68
+ "missing": [
69
+ {
70
+ "id": "m1",
71
+ "name": "Item with no cover",
72
+ "owner": "Ada Lovelace",
73
+ "status": "active",
74
+ "updated": "2026-10-28",
75
+ "cover": null
76
+ },
77
+ {
78
+ "id": "m2",
79
+ "name": "Item with no owner",
80
+ "owner": "",
81
+ "status": "paused",
82
+ "updated": "2026-10-20",
83
+ "cover": "#dbe6fe"
84
+ },
85
+ { "id": "m3", "name": "", "owner": "Grace Hopper", "status": "done", "updated": "", "cover": null }
86
+ ]
87
+ }
@@ -0,0 +1,122 @@
1
+ import { AppShell, PageHead } from '@kit/AppShell';
2
+ import { DialVars, useDials } from '@kit/dials';
3
+ import { type FixtureSets, fixtureSets, SET_NAMES, type SetName } from '@kit/fixtures';
4
+ import { fmtDate, plural } from '@kit/format';
5
+ import { useHashRoute } from '@kit/hooks';
6
+ import { Badge, Button, notInPrototype, toast } from '@kit/ui';
7
+ import type { CSSProperties } from 'react';
8
+ import data from './fixtures.json';
9
+ import './styles.css';
10
+
11
+ type Item = {
12
+ id: string;
13
+ name: string;
14
+ owner: string;
15
+ status: string;
16
+ updated: string;
17
+ cover: string | null;
18
+ };
19
+
20
+ const MAX_ROWS = 40;
21
+ const sets: FixtureSets<Item> = fixtureSets(data satisfies { real: Item[]; missing: Item[] }, {
22
+ max: MAX_ROWS,
23
+ text: ['name', 'owner'],
24
+ });
25
+
26
+ const TONE: Record<string, 'success' | 'warning' | 'brand'> = {
27
+ active: 'brand',
28
+ paused: 'warning',
29
+ done: 'success',
30
+ };
31
+
32
+ // The list is its own component so its Review panel registers before the
33
+ // Motion dials and View: a child registers before its parent, and siblings in order.
34
+ function ItemsPage() {
35
+ const review = useDials(
36
+ 'Review',
37
+ {
38
+ fixtures: { type: 'select', options: [...SET_NAMES], default: 'real' },
39
+ density: { type: 'select', options: ['comfortable', 'compact'], default: 'comfortable' },
40
+ },
41
+ {
42
+ hints: {
43
+ fixtures:
44
+ 'Which fixture set the list shows. longest, empty and max are derived from real by @kit/fixtures.',
45
+ density: 'Row height: 56px comfortable, 44px compact.',
46
+ },
47
+ },
48
+ );
49
+ const items = sets[review.fixtures as SetName];
50
+
51
+ return (
52
+ <div className="page" data-density={review.density}>
53
+ <PageHead
54
+ title="Items"
55
+ actions={<Button kind="primary" label="New item" onClick={() => notInPrototype('Adding an item')} />}
56
+ />
57
+ {items.length === 0 ? (
58
+ <div className="card empty">
59
+ <h3>No items yet</h3>
60
+ <p>New item starts the first one.</p>
61
+ </div>
62
+ ) : (
63
+ <div className="card table-card">
64
+ <ul className="items">
65
+ {items.map((item, i) => (
66
+ <li key={item.id} className="item-row" style={{ '--i': Math.min(i, 8) } as CSSProperties}>
67
+ <a
68
+ className="item-link"
69
+ href={`#/items/${item.id}`}
70
+ onClick={(e) => {
71
+ e.preventDefault();
72
+ toast(`${item.name || 'The item'} would open here.`, { kind: 'scope' });
73
+ }}
74
+ >
75
+ {item.cover ? (
76
+ <span className="item-cover" style={{ background: item.cover }} aria-hidden="true" />
77
+ ) : (
78
+ <span className="item-cover is-missing" aria-hidden="true" />
79
+ )}
80
+ <span className="item-text">
81
+ <span className="item-name">
82
+ {item.name || <span className="item-unnamed">Untitled</span>}
83
+ </span>
84
+ <span className="item-owner">{item.owner || 'No owner'}</span>
85
+ </span>
86
+ </a>
87
+ <Badge tone={TONE[item.status]} dot>
88
+ {item.status}
89
+ </Badge>
90
+ <time className="item-date tabular" dateTime={item.updated}>
91
+ {fmtDate(item.updated) || 'Never'}
92
+ </time>
93
+ </li>
94
+ ))}
95
+ </ul>
96
+ <div className="table-foot">
97
+ <span className="foot-sum">{plural(items.length, 'item')}</span>
98
+ </div>
99
+ </div>
100
+ )}
101
+ </div>
102
+ );
103
+ }
104
+
105
+ export default function Prototype() {
106
+ const { path } = useHashRoute('#/items');
107
+ return (
108
+ <AppShell active={path}>
109
+ <ItemsPage />
110
+ <DialVars
111
+ name="Motion"
112
+ config={{ rise: [220, 0, 600, 10], stagger: [30, 0, 120, 5] }}
113
+ options={{
114
+ hints: {
115
+ rise: 'How long each row takes to arrive, in ms.',
116
+ stagger: 'The gap between one row and the next, in ms.',
117
+ },
118
+ }}
119
+ />
120
+ </AppShell>
121
+ );
122
+ }
@@ -0,0 +1,25 @@
1
+ /* The list: one grid per row, the cover and the text in the first column,
2
+ status and date pinned right. Rows arrive 4px up, one after another, on
3
+ the dials in index.tsx (--rise, --stagger, unitless ms). */
4
+ .items { list-style: none; margin: 0; padding: 0; }
5
+ .item-row { display: grid; grid-template-columns: minmax(0, 1fr) auto 112px; align-items: center; gap: 16px; min-height: 56px; padding: 8px 16px 8px 8px; border-bottom: 1px solid var(--border-secondary); animation: item-rise calc(var(--rise, 220) * 1ms) var(--ease-out) calc(var(--i, 0) * var(--stagger, 30) * 1ms) backwards; }
6
+ .item-row:last-child { border-bottom: 0; }
7
+ .page[data-density="compact"] .item-row { min-height: 44px; padding-block: 4px; }
8
+ .item-link { display: flex; align-items: center; gap: 12px; min-width: 0; padding: 4px 8px; margin: -4px -8px; border-radius: var(--radius-sm); color: inherit; text-decoration: none; transition: background-color var(--dur-hover) var(--ease-hover); }
9
+ @media (hover: hover) and (pointer: fine) { .item-link:hover { background: var(--bg-primary_hover); } }
10
+ .item-link:active { background: var(--bg-active); }
11
+ .item-cover { width: 40px; height: 40px; flex: none; border-radius: var(--radius-sm); box-shadow: inset 0 0 0 1px rgb(16 24 40 / 0.08); }
12
+ .page[data-density="compact"] .item-cover { width: 32px; height: 32px; }
13
+ .item-cover.is-missing { background: var(--bg-secondary); box-shadow: none; outline: 1px dashed var(--border-primary); outline-offset: -1px; }
14
+ .item-text { display: flex; flex-direction: column; min-width: 0; }
15
+ .item-name { font-weight: 500; color: var(--text-primary); overflow-wrap: anywhere; }
16
+ .item-unnamed { color: var(--text-quaternary); font-style: italic; }
17
+ .item-owner { color: var(--text-tertiary); font-size: 12px; line-height: 18px; }
18
+ .item-date { color: var(--text-tertiary); text-align: right; }
19
+ @keyframes item-rise { from { opacity: 0; translate: 0 4px; } }
20
+ :root[data-motion="reduce"] .item-row { animation-name: item-fade; }
21
+ @keyframes item-fade { from { opacity: 0; } }
22
+ @media (max-width: 640px) {
23
+ .item-row { grid-template-columns: minmax(0, 1fr) auto; }
24
+ .item-date { display: none; }
25
+ }
@@ -0,0 +1,4 @@
1
+ // The password gate in front of every deployed path (vercel.json, proxy.entrypoint).
2
+ import { createProxy } from '@milesroxas/design-hub/vercel';
3
+
4
+ export default createProxy({ siteName: 'Prototypes' });
@@ -0,0 +1,3 @@
1
+ # Paper sketches
2
+
3
+ Exports from Paper Desktop that a prototype or a design doc cites: PNG, PDF, HTML or JSX. Name a file by the frame it came from. The live comp stays in Paper; `miles-paper-to-prototype` reads it over MCP, never from here, because an export drifts from the frame the moment the frame changes.
@@ -0,0 +1,10 @@
1
+ {
2
+ "extends": "@milesroxas/design-hub/tsconfig.project.json",
3
+ "compilerOptions": {
4
+ "paths": {
5
+ "@kit/*": ["./kit/*", "./node_modules/@milesroxas/design-hub/kit/*"],
6
+ "@milesroxas/design-hub/kit/*": ["./node_modules/@milesroxas/design-hub/kit/*"]
7
+ }
8
+ },
9
+ "include": ["prototypes", "kit", "api", "proxy.ts"]
10
+ }
@@ -0,0 +1,49 @@
1
+ {
2
+ "$schema": "https://openapi.vercel.sh/vercel.json",
3
+ "framework": "vite",
4
+ "installCommand": "pnpm install --frozen-lockfile",
5
+ "buildCommand": "pnpm build",
6
+ "outputDirectory": "dist",
7
+ "proxy": {
8
+ "entrypoint": "proxy.ts",
9
+ "matcher": ["/(.*)"]
10
+ },
11
+ "rewrites": [
12
+ {
13
+ "source": "/((?!api/).*)",
14
+ "destination": "/index.html"
15
+ }
16
+ ],
17
+ "headers": [
18
+ {
19
+ "source": "/(.*)",
20
+ "headers": [
21
+ {
22
+ "key": "X-Robots-Tag",
23
+ "value": "noindex, nofollow, noarchive"
24
+ },
25
+ {
26
+ "key": "X-Content-Type-Options",
27
+ "value": "nosniff"
28
+ },
29
+ {
30
+ "key": "X-Frame-Options",
31
+ "value": "DENY"
32
+ },
33
+ {
34
+ "key": "Referrer-Policy",
35
+ "value": "same-origin"
36
+ },
37
+ {
38
+ "key": "Strict-Transport-Security",
39
+ "value": "max-age=31536000; includeSubDomains"
40
+ }
41
+ ]
42
+ }
43
+ ],
44
+ "functions": {
45
+ "api/review.ts": {
46
+ "maxDuration": 10
47
+ }
48
+ }
49
+ }