@milesroxas/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.
- package/LICENSE +21 -0
- package/README.md +80 -2
- package/client.d.ts +6 -0
- package/configs/biome/biome.json +64 -0
- package/configs/tsconfig.project.json +17 -0
- package/core/bytes.test.ts +14 -0
- package/core/bytes.ts +14 -0
- package/core/catalog.test.ts +44 -0
- package/core/catalog.ts +43 -0
- package/core/checks.test.ts +23 -0
- package/core/checks.ts +72 -0
- package/core/contacts.test.ts +31 -0
- package/core/contacts.ts +38 -0
- package/core/doc.ts +4 -0
- package/core/feature.ts +6 -0
- package/core/fixtures.test.ts +33 -0
- package/core/fixtures.ts +69 -0
- package/core/frontmatter.ts +15 -0
- package/core/html.ts +3 -0
- package/core/image.test.ts +22 -0
- package/core/image.ts +37 -0
- package/core/jev.test.ts +66 -0
- package/core/jev.ts +77 -0
- package/core/review/model.ts +189 -0
- package/core/review/policy.ts +26 -0
- package/core/review/service.test.ts +163 -0
- package/core/review/service.ts +309 -0
- package/core/review/store.ts +51 -0
- package/core/review/triage.test.ts +41 -0
- package/core/review/triage.ts +58 -0
- package/core/states.test.ts +36 -0
- package/core/states.ts +34 -0
- package/dist/core/bytes.d.ts +5 -0
- package/dist/core/bytes.js +9 -0
- package/dist/core/catalog.d.ts +17 -0
- package/dist/core/catalog.js +24 -0
- package/dist/core/checks.d.ts +41 -0
- package/dist/core/checks.js +53 -0
- package/dist/core/contacts.d.ts +9 -0
- package/dist/core/contacts.js +34 -0
- package/dist/core/doc.d.ts +11 -0
- package/dist/core/doc.js +1 -0
- package/dist/core/feature.d.ts +3 -0
- package/dist/core/feature.js +4 -0
- package/dist/core/fixtures.d.ts +19 -0
- package/dist/core/fixtures.js +52 -0
- package/dist/core/frontmatter.d.ts +4 -0
- package/dist/core/frontmatter.js +15 -0
- package/dist/core/html.d.ts +2 -0
- package/dist/core/html.js +2 -0
- package/dist/core/image.d.ts +6 -0
- package/dist/core/image.js +34 -0
- package/dist/core/jev.d.ts +54 -0
- package/dist/core/jev.js +52 -0
- package/dist/core/review/model.d.ts +85 -0
- package/dist/core/review/model.js +112 -0
- package/dist/core/review/policy.d.ts +3 -0
- package/dist/core/review/policy.js +19 -0
- package/dist/core/review/service.d.ts +79 -0
- package/dist/core/review/service.js +245 -0
- package/dist/core/review/store.d.ts +20 -0
- package/dist/core/review/store.js +37 -0
- package/dist/core/review/triage.d.ts +6 -0
- package/dist/core/review/triage.js +45 -0
- package/dist/core/states.d.ts +6 -0
- package/dist/core/states.js +29 -0
- package/dist/scripts/bootstrap.d.ts +51 -0
- package/dist/scripts/bootstrap.js +278 -0
- package/dist/scripts/calibrate.d.ts +127 -0
- package/dist/scripts/calibrate.js +164 -0
- package/dist/scripts/check.d.ts +25 -0
- package/dist/scripts/check.js +30 -0
- package/dist/scripts/cli.d.ts +2 -0
- package/dist/scripts/cli.js +73 -0
- package/dist/scripts/commands.d.ts +25 -0
- package/dist/scripts/commands.js +49 -0
- package/dist/scripts/comments.d.ts +63 -0
- package/dist/scripts/comments.js +152 -0
- package/dist/scripts/dev.d.ts +46 -0
- package/dist/scripts/dev.js +84 -0
- package/dist/scripts/elyx.d.ts +61 -0
- package/dist/scripts/elyx.js +195 -0
- package/dist/scripts/fixtures-pull.d.ts +60 -0
- package/dist/scripts/fixtures-pull.js +121 -0
- package/dist/scripts/guard.d.ts +58 -0
- package/dist/scripts/guard.js +201 -0
- package/dist/scripts/hook.d.ts +18 -0
- package/dist/scripts/hook.js +46 -0
- package/dist/scripts/lib/bin.d.ts +4 -0
- package/dist/scripts/lib/bin.js +20 -0
- package/dist/scripts/lib/catalog.d.ts +7 -0
- package/dist/scripts/lib/catalog.js +18 -0
- package/dist/scripts/lib/cli.d.ts +75 -0
- package/dist/scripts/lib/cli.js +88 -0
- package/dist/scripts/lib/css-tokens.d.ts +13 -0
- package/dist/scripts/lib/css-tokens.js +67 -0
- package/dist/scripts/lib/judge.d.ts +24 -0
- package/dist/scripts/lib/judge.js +60 -0
- package/dist/scripts/lib/ports.d.ts +35 -0
- package/dist/scripts/lib/ports.js +113 -0
- package/dist/scripts/lib/review.d.ts +7 -0
- package/dist/scripts/lib/review.js +22 -0
- package/dist/scripts/lib/rules.d.ts +12 -0
- package/dist/scripts/lib/rules.js +20 -0
- package/dist/scripts/lib/vercel.d.ts +17 -0
- package/dist/scripts/lib/vercel.js +53 -0
- package/dist/scripts/new.d.ts +47 -0
- package/dist/scripts/new.js +113 -0
- package/dist/scripts/people.d.ts +59 -0
- package/dist/scripts/people.js +73 -0
- package/dist/scripts/preflight.d.ts +35 -0
- package/dist/scripts/preflight.js +251 -0
- package/dist/scripts/release-check.d.ts +19 -0
- package/dist/scripts/release-check.js +39 -0
- package/dist/scripts/skill-hint.d.ts +32 -0
- package/dist/scripts/skill-hint.js +103 -0
- package/dist/scripts/skills.d.ts +13 -0
- package/dist/scripts/skills.js +34 -0
- package/dist/scripts/visual-match.d.ts +46 -0
- package/dist/scripts/visual-match.js +93 -0
- package/dist/server/api.d.ts +22 -0
- package/dist/server/api.js +143 -0
- package/dist/server/checks.d.ts +4 -0
- package/dist/server/checks.js +22 -0
- package/dist/server/dev.d.ts +2 -0
- package/dist/server/dev.js +54 -0
- package/dist/server/env.d.ts +8 -0
- package/dist/server/env.js +57 -0
- package/dist/server/fake-upstash.d.ts +1 -0
- package/dist/server/fake-upstash.js +37 -0
- package/dist/server/file-kv.d.ts +2 -0
- package/dist/server/file-kv.js +24 -0
- package/dist/server/local.d.ts +4 -0
- package/dist/server/local.js +25 -0
- package/dist/server/paths.d.ts +22 -0
- package/dist/server/paths.js +81 -0
- package/dist/server/session.d.ts +22 -0
- package/dist/server/session.js +110 -0
- package/dist/server/upstash.d.ts +7 -0
- package/dist/server/upstash.js +41 -0
- package/dist/vercel/index.d.ts +5 -0
- package/dist/vercel/index.js +12 -0
- package/dist/vercel/proxy.d.ts +11 -0
- package/dist/vercel/proxy.js +238 -0
- package/dist/vite/kit.d.ts +4 -0
- package/dist/vite/kit.js +35 -0
- package/dist/vite/markdown.d.ts +2 -0
- package/dist/vite/markdown.js +212 -0
- package/dist/vite/plugin.d.ts +15 -0
- package/dist/vite/plugin.js +84 -0
- package/dist/vite/registry.d.ts +7 -0
- package/dist/vite/registry.js +70 -0
- package/kit/Icon.tsx +39 -0
- package/kit/Space.tsx +25 -0
- package/kit/Variants.tsx +134 -0
- package/kit/ViewTools.tsx +34 -0
- package/kit/base.css +27 -0
- package/kit/dials.css +98 -0
- package/kit/dials.tsx +322 -0
- package/kit/fixtures.ts +3 -0
- package/kit/format.ts +13 -0
- package/kit/harness.css +99 -0
- package/kit/hooks.ts +35 -0
- package/kit/icons.ts +329 -0
- package/kit/motion.ts +18 -0
- package/kit/prefs.ts +111 -0
- package/kit/space.css +12 -0
- package/kit/ui/Button.tsx +135 -0
- package/kit/ui/Menu.tsx +386 -0
- package/kit/ui/Overlay.tsx +300 -0
- package/kit/ui/Toast.tsx +187 -0
- package/kit/ui/index.ts +13 -0
- package/kit/ui/layer.ts +51 -0
- package/kit/ui/ui.css +238 -0
- package/package.json +87 -3
- package/shell/Guide.tsx +160 -0
- package/shell/Index.tsx +301 -0
- package/shell/Notice.tsx +27 -0
- package/shell/Prototype.tsx +31 -0
- package/shell/a11y.ts +50 -0
- package/shell/doc.d.ts +7 -0
- package/shell/doc.ts +3 -0
- package/shell/guide.css +133 -0
- package/shell/index.html +17 -0
- package/shell/main.tsx +53 -0
- package/shell/public/favicon.svg +1 -0
- package/shell/registry.ts +21 -0
- package/shell/registry.virtual.d.ts +10 -0
- package/shell/review/Review.tsx +989 -0
- package/shell/review/anchor.ts +132 -0
- package/shell/review/client.ts +165 -0
- package/shell/review/mount.tsx +12 -0
- package/shell/review/review.css +296 -0
- package/shell/shell.css +160 -0
- package/shell/system.css +238 -0
- package/skills/miles-conductor/SKILL.md +37 -0
- package/skills/miles-deploy/SKILL.md +67 -0
- package/skills/miles-design-hub/SKILL.md +62 -0
- package/skills/miles-get-hostile/SKILL.md +61 -0
- package/skills/miles-new-feature/SKILL.md +40 -0
- package/skills/miles-paper-to-prototype/SKILL.md +46 -0
- package/skills/miles-port/SKILL.md +59 -0
- package/skills/miles-port/references/astro.md +35 -0
- package/skills/miles-port/references/next.md +36 -0
- package/skills/miles-prototype/SKILL.md +83 -0
- package/skills/miles-prototype/references/accessibility.md +48 -0
- package/skills/miles-prototype/references/motion.md +59 -0
- package/skills/miles-prototype/references/performance.md +45 -0
- package/skills/miles-review/SKILL.md +39 -0
- package/templates/app/README.md +41 -0
- package/templates/app/index.tsx +32 -0
- package/templates/app/styles.css +1 -0
- package/templates/conductor/settings.toml +29 -0
- package/templates/guide.md +38 -0
- package/templates/space/README.md +37 -0
- package/templates/space/index.tsx +43 -0
- package/templates/space/styles.css +13 -0
package/shell/shell.css
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/* The index: one group per feature (its primary prototype, its alternates on
|
|
2
|
+
a rail, its internal explorations folded into cards), then prototypes with
|
|
3
|
+
no feature, then the archive. Rows draw no dividers; their hover surface
|
|
4
|
+
bleeds 12px into the gutter. The list ends in a foot (the Ledger Foot Rule).
|
|
5
|
+
The rules are in README.md, The index. */
|
|
6
|
+
|
|
7
|
+
/* One grid for the whole page, handed down by subgrid, so the context,
|
|
8
|
+
status and date columns line up across every group without fixed widths.
|
|
9
|
+
The page is 720px of text plus the 12px bleed each side; everything that
|
|
10
|
+
is not a row carries that 12px itself. */
|
|
11
|
+
.index { width: min(744px, 100% - 8px); margin: 0 auto; padding: 96px 0 128px; display: grid; grid-template-columns: minmax(0, 1fr) auto auto auto; column-gap: 20px; align-content: start; }
|
|
12
|
+
.index > * { grid-column: 1 / -1; }
|
|
13
|
+
.index-head, .index-filter, .index-section, .index-subsection, .index-empty, .index-none, .index-foot { padding-inline: 12px; }
|
|
14
|
+
|
|
15
|
+
.index-head { display: flex; flex-direction: column; gap: 8px; margin-bottom: 40px; }
|
|
16
|
+
.index-head h1 { font-size: 18px; line-height: 28px; font-weight: 600; text-wrap: balance; }
|
|
17
|
+
.index-voice { font: 400 18px/28px var(--font-voice); letter-spacing: -0.01em; color: var(--text-secondary); max-width: 44ch; text-wrap: pretty; }
|
|
18
|
+
.index code { font: 13px/20px var(--sys-font-mono); color: var(--text-secondary); }
|
|
19
|
+
|
|
20
|
+
.index-filter { margin-bottom: 24px; }
|
|
21
|
+
.index-filter input { width: 100%; height: 40px; padding: 0 12px; border: 1px solid var(--border-primary); border-radius: var(--radius-md); background: var(--bg-primary); box-shadow: var(--shadow-xs); font-size: 16px; transition: border-color var(--dur-hover) var(--ease-hover); }
|
|
22
|
+
.index-filter input::placeholder { color: var(--text-placeholder); }
|
|
23
|
+
.index-filter input:focus-visible { border-color: var(--border-brand); }
|
|
24
|
+
|
|
25
|
+
.index-group { display: grid; grid-template-columns: subgrid; row-gap: 4px; }
|
|
26
|
+
.index-group + .index-group { margin-top: 40px; }
|
|
27
|
+
.index-section { grid-column: 1 / -1; display: flex; align-items: baseline; justify-content: space-between; gap: 16px; font-size: 14px; line-height: 20px; font-weight: 600; color: var(--text-tertiary); }
|
|
28
|
+
/* A feature's walkthrough sits at the end of its heading line, in the same tier. */
|
|
29
|
+
.index-guide { display: inline-flex; align-items: center; gap: 2px; color: var(--text-secondary); text-decoration: none; border-radius: var(--radius-xs); transition: color var(--dur-hover) var(--ease-hover); }
|
|
30
|
+
.index-guide:hover { color: var(--text-primary); }
|
|
31
|
+
/* Alternates are other takes on the primary: indented 20px under it and
|
|
32
|
+
joined by a 1px rail on the primary's text edge, so they read as children
|
|
33
|
+
of that one row. Subgrid carries the padding to the first column only, so
|
|
34
|
+
the meta columns still line up with every other row. */
|
|
35
|
+
.index-alternates { position: relative; grid-column: 1 / -1; display: grid; grid-template-columns: subgrid; padding-left: 20px; }
|
|
36
|
+
.index-alternates::before { content: ""; position: absolute; left: 12px; top: 2px; bottom: 12px; width: 1px; background: var(--border-secondary); }
|
|
37
|
+
.index-alternates > .index-list { row-gap: 0; }
|
|
38
|
+
.index-alternates-label { grid-column: 1 / -1; padding: 4px 12px 0; font-size: 12px; line-height: 18px; font-weight: 500; color: var(--text-quaternary); }
|
|
39
|
+
|
|
40
|
+
/* A feature's explorations sit under the sprint, folded away by default
|
|
41
|
+
behind a caption one tier below the heading; the page remembers the fold. */
|
|
42
|
+
/* Rhythm inside a group: each block's text sits 20px under the text above it
|
|
43
|
+
(the primary under the heading, each caption under the last row's summary),
|
|
44
|
+
and a caption sits 8px over what it labels, so it belongs to what follows.
|
|
45
|
+
Groups stay 40px apart. */
|
|
46
|
+
.index-subsection { grid-column: 1 / -1; margin-top: 4px; margin-bottom: 4px; font-size: 12px; line-height: 18px; font-weight: 500; }
|
|
47
|
+
.index-fold-toggle { display: inline-flex; align-items: center; gap: 4px; min-height: 24px; padding: 0; border: 0; background: none; font: inherit; color: var(--text-quaternary); cursor: pointer; border-radius: var(--radius-xs); transition: color var(--dur-hover) var(--ease-hover); }
|
|
48
|
+
.index-fold-count { color: var(--text-quaternary); font-weight: 400; }
|
|
49
|
+
.index-fold-toggle svg { transition: transform var(--dur-dialog) var(--ease-out); }
|
|
50
|
+
.index-fold-toggle[aria-expanded="true"] svg { transform: rotate(180deg); }
|
|
51
|
+
|
|
52
|
+
/* The fold opens by growing its one grid row from 0fr to 1fr while the cards
|
|
53
|
+
fade in, and closes faster than it opens. The cards clip only a few pixels
|
|
54
|
+
past their edge, so the hover shadow survives and a closed fold shows nothing. */
|
|
55
|
+
.index-fold { grid-column: 1 / -1; display: grid; grid-template-rows: 0fr; opacity: 0; transition: grid-template-rows var(--dur-dialog-out) var(--ease-out), opacity var(--dur-dialog-out) var(--ease-out); }
|
|
56
|
+
.index-fold[data-open] { grid-template-rows: 1fr; opacity: 1; transition-duration: var(--dur-dialog); }
|
|
57
|
+
.index-fold[data-instant] { transition: none; }
|
|
58
|
+
|
|
59
|
+
/* Cards, not rows: the explorations are side questions, met as a set. The
|
|
60
|
+
grid's outer edges sit on the rows' hover bleed, so card text keeps the
|
|
61
|
+
page's text edge. The edge is an inset ring (a border would sit pasted on
|
|
62
|
+
the page, an outer ring would be clipped by the fold), one shadow layer for
|
|
63
|
+
lift in light; dark has no shadows, so the ring carries it alone. */
|
|
64
|
+
/* Light rests on a ring and two soft layers and lifts on hover by deepening
|
|
65
|
+
them, the surface staying white so the card rises rather than sinks. Dark
|
|
66
|
+
has no shadows, so the ring brightens and the surface steps up instead. */
|
|
67
|
+
.index {
|
|
68
|
+
--index-card-edge: inset 0 0 0 1px oklch(0 0 0 / 0.08), 0 1px 2px oklch(0 0 0 / 0.04), 0 2px 4px -2px oklch(0 0 0 / 0.04);
|
|
69
|
+
--index-card-edge-hover: inset 0 0 0 1px oklch(0 0 0 / 0.12), 0 1px 3px oklch(0 0 0 / 0.06), 0 6px 12px -6px oklch(0 0 0 / 0.12);
|
|
70
|
+
--index-card-bg-hover: var(--bg-primary);
|
|
71
|
+
}
|
|
72
|
+
.dark-mode .index { --index-card-edge: inset 0 0 0 1px oklch(1 0 0 / 0.09); --index-card-edge-hover: inset 0 0 0 1px oklch(1 0 0 / 0.16); --index-card-bg-hover: var(--bg-primary_hover); }
|
|
73
|
+
.index-cards { min-height: 0; overflow: clip; overflow-clip-margin: 12px; list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(auto-fill, minmax(208px, 1fr)); gap: 8px; }
|
|
74
|
+
.index-cards > li { display: flex; }
|
|
75
|
+
.index-card { flex: 1; display: flex; flex-direction: column; gap: 4px; padding: 12px; border-radius: var(--radius-lg); background: var(--bg-primary); box-shadow: var(--index-card-edge); text-decoration: none; transition: background-color var(--dur-hover) var(--ease-hover), box-shadow var(--dur-hover) var(--ease-hover), transform var(--dur-press-out) var(--ease-press), opacity var(--dur-dialog-out) var(--ease-in); }
|
|
76
|
+
/* The base focus ring is a box-shadow, so the card's edge would erase it; stack both. */
|
|
77
|
+
.index-card:focus-visible { box-shadow: var(--focus-ring), var(--index-card-edge); }
|
|
78
|
+
.index-card-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 8px; }
|
|
79
|
+
.index-card-head svg { flex-shrink: 0; margin-top: 2px; color: var(--fg-quaternary); transition: color var(--dur-hover) var(--ease-hover), transform var(--dur-hover) var(--ease-out); }
|
|
80
|
+
/* The summary steps down to caption size so the title leads and three cards
|
|
81
|
+
scan as three titles; Ink Tertiary keeps it a rung above the meta. */
|
|
82
|
+
.index-card .index-summary { font-size: 12px; line-height: 18px; }
|
|
83
|
+
.index-card-meta { margin-top: auto; padding-top: 12px; display: flex; align-items: center; gap: 12px; color: var(--text-quaternary); font-size: 12px; line-height: 18px; white-space: nowrap; }
|
|
84
|
+
.index-card-meta > :last-child { margin-left: auto; }
|
|
85
|
+
|
|
86
|
+
.index-list { list-style: none; padding: 0; margin: 0; display: grid; grid-template-columns: subgrid; grid-column: 1 / -1; }
|
|
87
|
+
.index-list > li { display: grid; grid-template-columns: subgrid; grid-column: 1 / -1; }
|
|
88
|
+
.index-row { display: grid; grid-template-columns: subgrid; grid-column: 1 / -1; row-gap: 2px; padding: 12px; min-height: 56px; border-radius: var(--radius-md); text-decoration: none; transition: background-color var(--dur-hover) var(--ease-hover), transform var(--dur-press-out) var(--ease-press), opacity var(--dur-dialog-out) var(--ease-in); }
|
|
89
|
+
.index-title { grid-column: 1; font-weight: 600; color: var(--text-primary); }
|
|
90
|
+
.index-summary { grid-column: 1; color: var(--text-tertiary); text-wrap: pretty; transition: color var(--dur-hover) var(--ease-hover); }
|
|
91
|
+
.index-meta { display: grid; grid-template-columns: subgrid; grid-column: 2 / 5; grid-row: 1 / span 2; align-self: center; color: var(--text-quaternary); font-size: 12px; line-height: 18px; white-space: nowrap; }
|
|
92
|
+
.index-meta > :last-child { justify-self: end; }
|
|
93
|
+
|
|
94
|
+
/* The sprint is the one prototype opened every day: its title steps up one
|
|
95
|
+
size, nothing else changes, so the row is heavier without a new shape. */
|
|
96
|
+
.index-row[data-kind="sprint"] .index-title { font-size: 16px; line-height: 24px; }
|
|
97
|
+
|
|
98
|
+
/* Status wears a dot so the column scans by colour before it is read:
|
|
99
|
+
Ink for a question still open, Success for one answered, quiet for the
|
|
100
|
+
archive. The word stays, so colour is never the only signal. */
|
|
101
|
+
.index-status { display: inline-flex; align-items: center; gap: 6px; text-transform: capitalize; }
|
|
102
|
+
.index-status::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: var(--fg-quinary); }
|
|
103
|
+
.index-status[data-status="exploring"]::before { background: var(--fg-brand-primary); }
|
|
104
|
+
.index-status[data-status="decided"]::before { background: var(--fg-success-primary); }
|
|
105
|
+
|
|
106
|
+
.index-none { min-height: 56px; display: flex; align-items: center; color: var(--text-tertiary); }
|
|
107
|
+
.index-empty { display: flex; flex-direction: column; gap: 4px; min-height: 56px; }
|
|
108
|
+
.index-empty h2 { font-size: 14px; font-weight: 600; }
|
|
109
|
+
.index-empty p { color: var(--text-tertiary); max-width: 52ch; text-wrap: pretty; }
|
|
110
|
+
|
|
111
|
+
.index-foot { margin-top: 32px; padding-top: 16px; border-top: 1px solid var(--border-secondary); color: var(--text-primary); }
|
|
112
|
+
|
|
113
|
+
@media (hover: hover) and (pointer: fine) {
|
|
114
|
+
.index-row:hover { background: var(--bg-primary_hover); }
|
|
115
|
+
.index-row:hover .index-summary { color: var(--text-secondary); }
|
|
116
|
+
.index-card:hover { background: var(--index-card-bg-hover); box-shadow: var(--index-card-edge-hover); }
|
|
117
|
+
.index-card:hover:focus-visible { box-shadow: var(--focus-ring), var(--index-card-edge-hover); }
|
|
118
|
+
.index-card:hover .index-summary { color: var(--text-secondary); }
|
|
119
|
+
.index-card:hover .index-card-head svg { color: var(--fg-secondary); transform: translateX(2px); }
|
|
120
|
+
.index-fold-toggle:hover { color: var(--text-secondary); }
|
|
121
|
+
}
|
|
122
|
+
.index-row:active { transform: scale(0.97); transition-duration: var(--dur-hover), var(--dur-press-in); }
|
|
123
|
+
.index-card:active { transform: scale(0.97); transition-duration: var(--dur-hover), var(--dur-hover), var(--dur-press-in); }
|
|
124
|
+
|
|
125
|
+
/* Arrival, one shot: the head fades, then each heading and row rises 4px
|
|
126
|
+
in order, 30ms apart, then the foot. Seen many times a day, so it stays
|
|
127
|
+
under 300ms per element and ends inside half a second. Keyframes fill
|
|
128
|
+
backwards only, so once a row has arrived its opacity is free to transition. */
|
|
129
|
+
.index-head { animation: index-fade 200ms var(--ease-out) backwards; }
|
|
130
|
+
.index-rise { animation: index-rise 220ms var(--ease-out) calc(40ms + var(--i, 0) * 30ms) backwards; }
|
|
131
|
+
@keyframes index-fade { from { opacity: 0; } }
|
|
132
|
+
@keyframes index-rise { from { opacity: 0; translate: 0 4px; } }
|
|
133
|
+
|
|
134
|
+
/* Departure, pointer only: the moment a row is clicked the rest of the page
|
|
135
|
+
accelerates away (the Layer Exit curve) while the browser fetches the
|
|
136
|
+
prototype, and the chosen row holds, so the eye keeps what it picked.
|
|
137
|
+
The browser paints this page until the next one is ready, so the fade
|
|
138
|
+
fills a wait that was blank. */
|
|
139
|
+
.index-head, .index-filter, .index-section, .index-subsection, .index-alternates-label, .index-foot { transition: opacity var(--dur-dialog-out) var(--ease-in); }
|
|
140
|
+
.index.is-leaving .index-alternates::before { opacity: 0; transition: opacity var(--dur-dialog-out) var(--ease-in); }
|
|
141
|
+
.index.is-leaving :is(.index-head, .index-filter, .index-section, .index-subsection, .index-alternates-label, .index-foot, .index-row:not([data-chosen]), .index-card:not([data-chosen])) { opacity: 0; }
|
|
142
|
+
.index.is-leaving :is(.index-row, .index-card) { pointer-events: none; }
|
|
143
|
+
|
|
144
|
+
:root[data-motion="reduce"] .index-rise { animation-name: index-fade; animation-duration: 120ms; animation-timing-function: linear; }
|
|
145
|
+
:root[data-motion="reduce"] :is(.index-row, .index-card):active { transform: none; }
|
|
146
|
+
:root[data-motion="reduce"] .index-fold { transition-property: opacity; }
|
|
147
|
+
:root[data-motion="reduce"] .index-fold-toggle svg { transition: none; }
|
|
148
|
+
:root[data-motion="reduce"] .index-card:hover .index-card-head svg { transform: none; }
|
|
149
|
+
|
|
150
|
+
@media (max-width: 560px) {
|
|
151
|
+
.index, .index-group, .index-alternates, .index-list, .index-list > li, .index-row { display: flex; flex-direction: column; }
|
|
152
|
+
.index-row { gap: 2px; }
|
|
153
|
+
.index-meta { display: flex; gap: 12px; margin-top: 4px; }
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.shell-notice { width: min(720px, 100% - 32px); margin: 0 auto; padding: 96px 0; display: flex; flex-direction: column; gap: 12px; align-items: flex-start; }
|
|
157
|
+
.shell-notice h1 { font-size: 18px; line-height: 28px; font-weight: 600; }
|
|
158
|
+
.shell-notice p { color: var(--text-tertiary); }
|
|
159
|
+
.shell-notice pre { max-width: 100%; overflow: auto; padding: 12px; border-radius: var(--radius-md); background: var(--bg-secondary); font: 12px/18px var(--sys-font-mono); color: var(--text-secondary); }
|
|
160
|
+
.shell-notice a { color: var(--text-brand-secondary); font-weight: 600; }
|
package/shell/system.css
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/* The hub's own look: the index, the walkthrough, notices, and every harness
|
|
2
|
+
layer drawn over a prototype (review, picker, dials). Unbranded on purpose:
|
|
3
|
+
Geist and pure neutrals (chroma 0), with hue kept only for status. It never
|
|
4
|
+
reads as the design under review, and a project's tokens.css never reaches it.
|
|
5
|
+
|
|
6
|
+
Two parts. The --sys-font* and --harness-* values sit on :root, because
|
|
7
|
+
harness layers float over prototypes. The `.sys` scope remaps the semantic
|
|
8
|
+
token names the hub stylesheets (and @kit/ui) read, so the index and guide
|
|
9
|
+
stay neutral whatever tokens.css holds. main.tsx puts `.sys` on <html> for
|
|
10
|
+
hub routes; a notice carries it itself. Every name in the light block is
|
|
11
|
+
restated in the dark block, so no light value leaks into dark. */
|
|
12
|
+
|
|
13
|
+
@import '@fontsource-variable/geist';
|
|
14
|
+
@import '@fontsource-variable/geist-mono';
|
|
15
|
+
|
|
16
|
+
:root {
|
|
17
|
+
--sys-font: 'Geist Variable', 'Geist', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif;
|
|
18
|
+
--sys-font-mono: 'Geist Mono Variable', 'Geist Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
19
|
+
|
|
20
|
+
/* Harness material: one dark neutral surface in both themes, for anything
|
|
21
|
+
the prototype says in its own voice (scope toasts, dial hints, review). */
|
|
22
|
+
--harness-bg: oklch(0.277 0 0);
|
|
23
|
+
--harness-fg: oklch(1 0 0 / 0.85);
|
|
24
|
+
--harness-fg-muted: oklch(1 0 0 / 0.55);
|
|
25
|
+
--harness-border: oklch(1 0 0 / 0.1);
|
|
26
|
+
--harness-shadow: 0 8px 24px oklch(0 0 0 / 0.4);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.sys {
|
|
30
|
+
--font: var(--sys-font);
|
|
31
|
+
--font-voice: var(--sys-font);
|
|
32
|
+
font-family: var(--sys-font);
|
|
33
|
+
|
|
34
|
+
/* Neutral ramp, one hue (none). Status keeps its hue so a dot still
|
|
35
|
+
reads as success, warning or error. */
|
|
36
|
+
--brand-400: oklch(0.87 0 0);
|
|
37
|
+
|
|
38
|
+
--text-primary: oklch(0.205 0 0);
|
|
39
|
+
--text-secondary: oklch(0.371 0 0);
|
|
40
|
+
--text-secondary_hover: oklch(0.269 0 0);
|
|
41
|
+
--text-tertiary: oklch(0.46 0 0);
|
|
42
|
+
--text-quaternary: oklch(0.54 0 0);
|
|
43
|
+
--text-disabled: oklch(0.62 0 0);
|
|
44
|
+
--text-placeholder: oklch(0.6 0 0);
|
|
45
|
+
--text-white: oklch(1 0 0);
|
|
46
|
+
--text-brand-primary: oklch(0.205 0 0);
|
|
47
|
+
--text-brand-secondary: oklch(0.205 0 0);
|
|
48
|
+
--text-brand-tertiary: oklch(0.371 0 0);
|
|
49
|
+
--text-error-primary: oklch(0.505 0.213 27.518);
|
|
50
|
+
--text-error-primary_hover: oklch(0.444 0.177 26.899);
|
|
51
|
+
--text-warning-primary: oklch(0.555 0.163 48.998);
|
|
52
|
+
--text-success-primary: oklch(0.527 0.154 150.069);
|
|
53
|
+
|
|
54
|
+
--border-primary: oklch(0.87 0 0);
|
|
55
|
+
--border-secondary: oklch(0.922 0 0);
|
|
56
|
+
--border-tertiary: oklch(0.97 0 0);
|
|
57
|
+
--border-disabled: oklch(0.87 0 0);
|
|
58
|
+
--border-brand: oklch(0.556 0 0);
|
|
59
|
+
--border-brand_alt: oklch(0.205 0 0);
|
|
60
|
+
--border-error: oklch(0.637 0.237 25.331);
|
|
61
|
+
|
|
62
|
+
--fg-primary: oklch(0.205 0 0);
|
|
63
|
+
--fg-secondary: oklch(0.371 0 0);
|
|
64
|
+
--fg-tertiary: oklch(0.46 0 0);
|
|
65
|
+
--fg-quaternary: oklch(0.54 0 0);
|
|
66
|
+
--fg-quinary: oklch(0.708 0 0);
|
|
67
|
+
--fg-white: oklch(1 0 0);
|
|
68
|
+
--fg-disabled: oklch(0.708 0 0);
|
|
69
|
+
--fg-brand-primary: oklch(0.205 0 0);
|
|
70
|
+
--fg-brand-secondary: oklch(0.371 0 0);
|
|
71
|
+
--fg-error-primary: oklch(0.577 0.245 27.325);
|
|
72
|
+
--fg-warning-primary: oklch(0.666 0.179 58.318);
|
|
73
|
+
--fg-success-primary: oklch(0.627 0.194 149.214);
|
|
74
|
+
|
|
75
|
+
--bg-primary: oklch(1 0 0);
|
|
76
|
+
--bg-primary_alt: oklch(1 0 0);
|
|
77
|
+
--bg-primary_hover: oklch(0.97 0 0);
|
|
78
|
+
--bg-secondary: oklch(0.985 0 0);
|
|
79
|
+
--bg-secondary_alt: oklch(0.985 0 0);
|
|
80
|
+
--bg-secondary_hover: oklch(0.97 0 0);
|
|
81
|
+
--bg-secondary_hover_deep: oklch(0.94 0 0);
|
|
82
|
+
--bg-secondary_subtle: oklch(0.99 0 0);
|
|
83
|
+
--bg-tertiary: oklch(0.97 0 0);
|
|
84
|
+
--bg-active: oklch(0.97 0 0);
|
|
85
|
+
--bg-disabled: oklch(0.97 0 0);
|
|
86
|
+
--bg-overlay: oklch(0.145 0 0);
|
|
87
|
+
--bg-brand-primary: oklch(0.97 0 0);
|
|
88
|
+
--bg-brand-secondary: oklch(0.94 0 0);
|
|
89
|
+
--bg-brand-solid: oklch(0.205 0 0);
|
|
90
|
+
--bg-brand-solid_hover: oklch(0.3 0 0);
|
|
91
|
+
--bg-error-primary: oklch(0.971 0.013 17.38);
|
|
92
|
+
--bg-error-secondary: oklch(0.936 0.032 17.717);
|
|
93
|
+
--bg-error-solid: oklch(0.577 0.245 27.325);
|
|
94
|
+
--bg-warning-primary: oklch(0.987 0.022 95.277);
|
|
95
|
+
--bg-success-primary: oklch(0.982 0.018 155.826);
|
|
96
|
+
|
|
97
|
+
--button-primary-bg: oklch(0.205 0 0);
|
|
98
|
+
--button-primary-bg_hover: oklch(0.3 0 0);
|
|
99
|
+
--button-primary-fg: oklch(0.985 0 0);
|
|
100
|
+
--button-secondary-bg: oklch(1 0 0);
|
|
101
|
+
--button-secondary-bg_hover: oklch(0.97 0 0);
|
|
102
|
+
--button-secondary-bg_open: oklch(0.94 0 0);
|
|
103
|
+
--button-secondary-bg_deep: oklch(0.922 0 0);
|
|
104
|
+
--button-secondary-fg: oklch(0.269 0 0);
|
|
105
|
+
--button-secondary-fg_hover: oklch(0.145 0 0);
|
|
106
|
+
--button-secondary-border: oklch(0.87 0 0);
|
|
107
|
+
--button-tertiary-fg: oklch(0.46 0 0);
|
|
108
|
+
--button-tertiary-fg_hover: oklch(0.269 0 0);
|
|
109
|
+
--button-tertiary-bg_hover: oklch(0.97 0 0);
|
|
110
|
+
--button-primary-error-bg: oklch(0.577 0.245 27.325);
|
|
111
|
+
--button-primary-error-bg_hover: oklch(0.505 0.213 27.518);
|
|
112
|
+
--button-secondary-error-fg: oklch(0.505 0.213 27.518);
|
|
113
|
+
--button-secondary-error-border: oklch(0.808 0.114 19.571);
|
|
114
|
+
--button-secondary-error-bg_hover: oklch(0.971 0.013 17.38);
|
|
115
|
+
--nav-item-icon-fg: oklch(0.54 0 0);
|
|
116
|
+
--nav-item-icon-fg_active: oklch(0.269 0 0);
|
|
117
|
+
--avatar-bg: oklch(0.97 0 0);
|
|
118
|
+
--avatar-contrast-border: oklch(0 0 0 / 0.08);
|
|
119
|
+
|
|
120
|
+
--utility-gray-50: oklch(0.985 0 0); --utility-gray-200: oklch(0.922 0 0); --utility-gray-700: oklch(0.371 0 0);
|
|
121
|
+
--utility-brand-50: oklch(0.985 0 0); --utility-brand-200: oklch(0.922 0 0); --utility-brand-700: oklch(0.371 0 0);
|
|
122
|
+
--utility-error-50: oklch(0.971 0.013 17.38); --utility-error-200: oklch(0.885 0.062 18.334); --utility-error-600: oklch(0.577 0.245 27.325); --utility-error-700: oklch(0.505 0.213 27.518);
|
|
123
|
+
--utility-warning-50: oklch(0.987 0.022 95.277); --utility-warning-200: oklch(0.924 0.12 95.746); --utility-warning-600: oklch(0.666 0.179 58.318); --utility-warning-700: oklch(0.555 0.163 48.998);
|
|
124
|
+
--utility-success-50: oklch(0.982 0.018 155.826); --utility-success-200: oklch(0.925 0.084 155.995); --utility-success-600: oklch(0.627 0.194 149.214); --utility-success-700: oklch(0.527 0.154 150.069);
|
|
125
|
+
|
|
126
|
+
--shadow-xs: 0 1px 2px 0 oklch(0 0 0 / 0.05);
|
|
127
|
+
--shadow-sm: 0 1px 3px 0 oklch(0 0 0 / 0.1), 0 1px 2px 0 oklch(0 0 0 / 0.06);
|
|
128
|
+
--shadow-md: 0 4px 8px -2px oklch(0 0 0 / 0.1), 0 2px 4px -2px oklch(0 0 0 / 0.06);
|
|
129
|
+
--shadow-lg: 0 12px 16px -4px oklch(0 0 0 / 0.08), 0 4px 6px -2px oklch(0 0 0 / 0.03);
|
|
130
|
+
--shadow-xl: 0 20px 24px -4px oklch(0 0 0 / 0.08), 0 8px 8px -4px oklch(0 0 0 / 0.03);
|
|
131
|
+
--focus-ring: 0 0 0 2px oklch(1 0 0), 0 0 0 4px oklch(0.556 0 0);
|
|
132
|
+
--focus-ring-error: 0 0 0 2px oklch(1 0 0), 0 0 0 4px oklch(0.637 0.237 25.331);
|
|
133
|
+
|
|
134
|
+
--radius-xxs: 2px; --radius-xs: 4px; --radius-sm: 6px; --radius-md: 8px;
|
|
135
|
+
--radius-lg: 10px; --radius-xl: 12px; --radius-2xl: 16px; --radius-full: 9999px;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/* Dark is the light ramp turned over: each role takes the step from the
|
|
139
|
+
other end, so the gaps between roles hold. */
|
|
140
|
+
.sys.dark-mode,
|
|
141
|
+
.dark-mode .sys {
|
|
142
|
+
--brand-400: oklch(0.87 0 0);
|
|
143
|
+
|
|
144
|
+
--text-primary: oklch(0.985 0 0);
|
|
145
|
+
--text-secondary: oklch(0.87 0 0);
|
|
146
|
+
--text-secondary_hover: oklch(0.922 0 0);
|
|
147
|
+
--text-tertiary: oklch(0.72 0 0);
|
|
148
|
+
--text-quaternary: oklch(0.66 0 0);
|
|
149
|
+
--text-disabled: oklch(0.5 0 0);
|
|
150
|
+
--text-placeholder: oklch(0.55 0 0);
|
|
151
|
+
--text-white: oklch(1 0 0);
|
|
152
|
+
--text-brand-primary: oklch(0.985 0 0);
|
|
153
|
+
--text-brand-secondary: oklch(0.985 0 0);
|
|
154
|
+
--text-brand-tertiary: oklch(0.87 0 0);
|
|
155
|
+
--text-error-primary: oklch(0.704 0.191 22.216);
|
|
156
|
+
--text-error-primary_hover: oklch(0.808 0.114 19.571);
|
|
157
|
+
--text-warning-primary: oklch(0.828 0.189 84.429);
|
|
158
|
+
--text-success-primary: oklch(0.792 0.209 151.711);
|
|
159
|
+
|
|
160
|
+
--border-primary: oklch(0.32 0 0);
|
|
161
|
+
--border-secondary: oklch(0.26 0 0);
|
|
162
|
+
--border-tertiary: oklch(0.215 0 0);
|
|
163
|
+
--border-disabled: oklch(0.32 0 0);
|
|
164
|
+
--border-brand: oklch(0.708 0 0);
|
|
165
|
+
--border-brand_alt: oklch(0.32 0 0);
|
|
166
|
+
--border-error: oklch(0.704 0.191 22.216);
|
|
167
|
+
|
|
168
|
+
--fg-primary: oklch(0.985 0 0);
|
|
169
|
+
--fg-secondary: oklch(0.87 0 0);
|
|
170
|
+
--fg-tertiary: oklch(0.72 0 0);
|
|
171
|
+
--fg-quaternary: oklch(0.66 0 0);
|
|
172
|
+
--fg-quinary: oklch(0.5 0 0);
|
|
173
|
+
--fg-white: oklch(1 0 0);
|
|
174
|
+
--fg-disabled: oklch(0.5 0 0);
|
|
175
|
+
--fg-brand-primary: oklch(0.985 0 0);
|
|
176
|
+
--fg-brand-secondary: oklch(0.87 0 0);
|
|
177
|
+
--fg-error-primary: oklch(0.637 0.237 25.331);
|
|
178
|
+
--fg-warning-primary: oklch(0.769 0.188 70.08);
|
|
179
|
+
--fg-success-primary: oklch(0.723 0.219 149.579);
|
|
180
|
+
|
|
181
|
+
--bg-primary: oklch(0.145 0 0);
|
|
182
|
+
--bg-primary_alt: oklch(0.175 0 0);
|
|
183
|
+
--bg-primary_hover: oklch(0.215 0 0);
|
|
184
|
+
--bg-secondary: oklch(0.175 0 0);
|
|
185
|
+
--bg-secondary_alt: oklch(0.145 0 0);
|
|
186
|
+
--bg-secondary_hover: oklch(0.215 0 0);
|
|
187
|
+
--bg-secondary_hover_deep: oklch(0.26 0 0);
|
|
188
|
+
--bg-secondary_subtle: oklch(0.175 0 0);
|
|
189
|
+
--bg-tertiary: oklch(0.215 0 0);
|
|
190
|
+
--bg-active: oklch(0.215 0 0);
|
|
191
|
+
--bg-disabled: oklch(0.215 0 0);
|
|
192
|
+
--bg-overlay: oklch(0.215 0 0);
|
|
193
|
+
--bg-brand-primary: oklch(0.26 0 0);
|
|
194
|
+
--bg-brand-secondary: oklch(0.32 0 0);
|
|
195
|
+
--bg-brand-solid: oklch(0.985 0 0);
|
|
196
|
+
--bg-brand-solid_hover: oklch(0.87 0 0);
|
|
197
|
+
--bg-error-primary: oklch(0.258 0.092 26.042);
|
|
198
|
+
--bg-error-secondary: oklch(0.396 0.141 25.723);
|
|
199
|
+
--bg-error-solid: oklch(0.577 0.245 27.325);
|
|
200
|
+
--bg-warning-primary: oklch(0.279 0.077 45.635);
|
|
201
|
+
--bg-success-primary: oklch(0.266 0.065 152.934);
|
|
202
|
+
|
|
203
|
+
--button-primary-bg: oklch(0.985 0 0);
|
|
204
|
+
--button-primary-bg_hover: oklch(0.87 0 0);
|
|
205
|
+
--button-primary-fg: oklch(0.205 0 0);
|
|
206
|
+
--button-secondary-bg: oklch(0.175 0 0);
|
|
207
|
+
--button-secondary-bg_hover: oklch(0.215 0 0);
|
|
208
|
+
--button-secondary-bg_open: oklch(0.26 0 0);
|
|
209
|
+
--button-secondary-bg_deep: oklch(0.32 0 0);
|
|
210
|
+
--button-secondary-fg: oklch(0.87 0 0);
|
|
211
|
+
--button-secondary-fg_hover: oklch(0.97 0 0);
|
|
212
|
+
--button-secondary-border: oklch(0.32 0 0);
|
|
213
|
+
--button-tertiary-fg: oklch(0.72 0 0);
|
|
214
|
+
--button-tertiary-fg_hover: oklch(0.922 0 0);
|
|
215
|
+
--button-tertiary-bg_hover: oklch(0.215 0 0);
|
|
216
|
+
--button-primary-error-bg: oklch(0.577 0.245 27.325);
|
|
217
|
+
--button-primary-error-bg_hover: oklch(0.505 0.213 27.518);
|
|
218
|
+
--button-secondary-error-fg: oklch(0.808 0.114 19.571);
|
|
219
|
+
--button-secondary-error-border: oklch(0.396 0.141 25.723);
|
|
220
|
+
--button-secondary-error-bg_hover: oklch(0.258 0.092 26.042);
|
|
221
|
+
--nav-item-icon-fg: oklch(0.66 0 0);
|
|
222
|
+
--nav-item-icon-fg_active: oklch(0.922 0 0);
|
|
223
|
+
--avatar-bg: oklch(0.215 0 0);
|
|
224
|
+
--avatar-contrast-border: oklch(1 0 0 / 0.12);
|
|
225
|
+
|
|
226
|
+
--utility-gray-50: oklch(0.175 0 0); --utility-gray-200: oklch(0.32 0 0); --utility-gray-700: oklch(0.87 0 0);
|
|
227
|
+
--utility-brand-50: oklch(0.175 0 0); --utility-brand-200: oklch(0.32 0 0); --utility-brand-700: oklch(0.87 0 0);
|
|
228
|
+
--utility-error-50: oklch(0.258 0.092 26.042); --utility-error-200: oklch(0.396 0.141 25.723); --utility-error-600: oklch(0.704 0.191 22.216); --utility-error-700: oklch(0.808 0.114 19.571);
|
|
229
|
+
--utility-warning-50: oklch(0.279 0.077 45.635); --utility-warning-200: oklch(0.414 0.112 45.904); --utility-warning-600: oklch(0.828 0.189 84.429); --utility-warning-700: oklch(0.879 0.169 91.605);
|
|
230
|
+
--utility-success-50: oklch(0.266 0.065 152.934); --utility-success-200: oklch(0.393 0.095 152.535); --utility-success-600: oklch(0.792 0.209 151.711); --utility-success-700: oklch(0.871 0.15 154.449);
|
|
231
|
+
|
|
232
|
+
--shadow-xs: none; --shadow-sm: none; --shadow-md: none; --shadow-lg: none; --shadow-xl: none;
|
|
233
|
+
--focus-ring: 0 0 0 2px oklch(0.145 0 0), 0 0 0 4px oklch(0.708 0 0);
|
|
234
|
+
--focus-ring-error: 0 0 0 2px oklch(0.145 0 0), 0 0 0 4px oklch(0.704 0.191 22.216);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/* Code in hub pages takes the mono face. */
|
|
238
|
+
.sys :is(code, kbd, pre, samp) { font-family: var(--sys-font-mono); }
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: miles-conductor
|
|
3
|
+
description: "Run this project in Conductor (conductor.build) workspaces: the settings file, a port per workspace, and the machine port registry that keeps projects off each other's ports. Load when the user mentions Conductor, workspaces, parallel agents, or a dev server that answers with the wrong project. Triggers on: Conductor, workspace, workspaces, CONDUCTOR_PORT, parallel agents, port taken, port collision, wrong site, ports.json, port registry."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Conductor
|
|
7
|
+
|
|
8
|
+
Conductor gives each workspace its own `$CONDUCTOR_PORT`, but never checks that the port is free. A dev server orphaned in another project can still hold it, and the workspace's Open button then shows the other project with no error. Everything here exists to make that failure loud.
|
|
9
|
+
|
|
10
|
+
## Set up
|
|
11
|
+
|
|
12
|
+
1. From the project root: `pnpm bootstrap --conductor --branch <integration branch>`. It writes `.conductor/settings.toml`: setup runs `pnpm bootstrap` in each new workspace, run starts `pnpm dev`, and the preview URL is the workspace's port. A file the user has edited is kept, and the JSON warns that it differs.
|
|
13
|
+
2. Commit `.conductor/settings.toml`. `.conductor/settings.local.toml` is gitignored, for one person's overrides.
|
|
14
|
+
3. Tell the user to add the repository in Conductor and set its base branch to the same branch. Conductor keeps that setting in its app only, so nothing in the repo can set or check it.
|
|
15
|
+
|
|
16
|
+
## Ports
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
Where is the dev server running?
|
|
20
|
+
├── A Conductor workspace → $CONDUCTOR_PORT, always. The dev server reads it before .env.
|
|
21
|
+
│ It never enters the machine registry: Conductor owns that range.
|
|
22
|
+
└── A main checkout → the port pnpm bootstrap picked, written to .env and claimed in
|
|
23
|
+
~/.config/design-hub/ports.json ($DESIGN_HUB_PORTS overrides the path).
|
|
24
|
+
Bootstrap skips every port another project claims, running or not.
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
On a taken port, `pnpm dev` refuses to start (`strictPort`) and `pnpm bootstrap` names the process and its folder. Then:
|
|
28
|
+
|
|
29
|
+
1. Never pass another port. Why: the Open button and `.claude/launch.json` keep pointing at the configured one, so a second port is a server nobody opens.
|
|
30
|
+
2. Find the holder: `lsof -nP -iTCP:<port> -sTCP:LISTEN`, then `lsof -a -p <pid> -d cwd` for its folder.
|
|
31
|
+
3. An orphaned server of this project: stop it. A server of another project: ask the user before stopping it.
|
|
32
|
+
|
|
33
|
+
When the user keeps their own record of which project owns which port, add this project's port to it too.
|
|
34
|
+
|
|
35
|
+
## Nothing shared
|
|
36
|
+
|
|
37
|
+
Workspaces share no database and no container. The review store's local copy is a file in each checkout's `.design-hub/`, and the deployed store is remote, so two workspaces never see each other's local comments.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: miles-deploy
|
|
3
|
+
description: "Deploy the prototypes to Vercel behind the password gate and hand out the share link: the pre-flight (pnpm preflight), the GATE_* env, the review store for comments, the handover message. Load when the user asks to deploy, share, send a link, set or rotate the password, turn on comments, or check the work before any of those. Triggers on: deploy, share, share link, send to the client, Vercel, password, gate, GATE_PASSWORD, production, preview URL, pre-flight, preflight, ready to send, turn on comments, review store."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Deploy
|
|
7
|
+
|
|
8
|
+
Pushing `main` deploys the project to the linked Vercel project. Every path sits behind the gate in `proxy.ts`: the shared password opens it to view, and an invite link opens it to one named person who can comment. The mechanics are scripts; this skill decides when they run and what the user is told.
|
|
9
|
+
|
|
10
|
+
## Before every link goes out
|
|
11
|
+
|
|
12
|
+
Run from the project root:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pnpm preflight
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
It runs every check in one pass and prints JSON with one entry per step. Read it, then walk this for each step with `ok: false`:
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
step failed?
|
|
22
|
+
├── contacts → show the user each value with its file and path. Replace it in fixtures.json,
|
|
23
|
+
│ or, only after the user says it is a business contact:
|
|
24
|
+
│ pnpm preflight --allow-contact <value>
|
|
25
|
+
├── states → add the missing rows to that README's States table (miles-prototype), then rerun
|
|
26
|
+
├── gate → pnpm bootstrap --vercel <project> sets GATE_PASSWORD and GATE_SECRET
|
|
27
|
+
└── any other → fix what `detail` names; `fix` says how
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Never send the link with a failed step, and never skip one with `--skip` without the user's word. Why: the link is what the client signs off on, and every step guards something they would see (a private phone number, a state nobody listed, a page that will not load).
|
|
31
|
+
|
|
32
|
+
## First deploy
|
|
33
|
+
|
|
34
|
+
1. The user signs in once, in their own terminal: `vercel login`. It is interactive; never run it for them.
|
|
35
|
+
2. From the project root:
|
|
36
|
+
```bash
|
|
37
|
+
pnpm bootstrap --vercel <project-name>
|
|
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.
|
|
40
|
+
3. `pnpm preflight`, then push `main` (or `vercel --prod` from the project root).
|
|
41
|
+
4. 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
|
+
## Comments
|
|
44
|
+
|
|
45
|
+
Ask the user once whether reviewers should comment on the link. If yes:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pnpm bootstrap --vercel <project> --review [--typesafe-key <key>]
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
It adds Upstash Redis to the project through the Vercel Marketplace and copies the store's credentials into `.env`. If it stops on the Marketplace terms, the user runs `vercel integration add upstash/upstash-kv` once in their terminal, then you rerun. With TypeSafe on, comments are sorted by kind and state as they arrive. Redeploy so the env reaches the site, then invite the first editor:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pnpm people invite --name "<user>" --team "<studio>" --role editor --site https://<deployment>
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Send them the link; from then on, editors invite reviewers from the People tab. `miles-review` owns working from the comments.
|
|
58
|
+
|
|
59
|
+
## Rotating access
|
|
60
|
+
|
|
61
|
+
- **A reviewer leaves:** revoke them in the People tab, or `pnpm people revoke <id>`. Their sessions end on their next page load.
|
|
62
|
+
- **The shared password leaked:** `pnpm bootstrap --vercel <project> --gate-password <new>`, then redeploy. Every password visitor signs in again; invited people are not affected.
|
|
63
|
+
|
|
64
|
+
## Security, never weakened for a prototype
|
|
65
|
+
|
|
66
|
+
- `vercel.json` sets `noindex`, `X-Frame-Options: DENY`, `nosniff`, HSTS and a same-origin referrer on every path. The gate and invite pages add a hash CSP and send no referrer.
|
|
67
|
+
- A wrong password waits 800ms. For a link going to a large client list, add a Vercel Firewall rate-limit rule on `POST` before it goes out.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: miles-design-hub
|
|
3
|
+
description: "Start a new Design Hub project: ask every question up front, create it with `npm create @milesroxas/design-hub`, run the setup, hand off to the Paper skill. Global install only; never load inside a project. Triggers on: design hub, new design project, start a project, new prototype repo, set up prototypes, from the template."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Design Hub
|
|
7
|
+
|
|
8
|
+
The entry point. The user never starts in a terminal: they ask for a new project, this skill asks every question before it touches disk, then runs two commands and hands off. A question asked up front is one Claude never guesses later.
|
|
9
|
+
|
|
10
|
+
Someone who does not use Claude Code gets the same result from the setup app: `Design Hub.command` in the Design Hub repo opens a page that asks the name, the place and GitHub, then runs the same two commands. Point a non-technical user there instead of this skill when they ask how to start without Claude.
|
|
11
|
+
|
|
12
|
+
## 1. Ask, all at once, before anything runs
|
|
13
|
+
|
|
14
|
+
Ask these in one message and wait for every answer. Defaults in brackets.
|
|
15
|
+
|
|
16
|
+
1. **Where should the project live?** An absolute path; its folder name becomes the repo name. [`~/SITES/<name>`]
|
|
17
|
+
2. **Which GitHub account or org owns the repo, private or public, or no GitHub repo?** [the account `gh` is signed in as, private]
|
|
18
|
+
3. **Start from a Paper comp or from the sample prototype?** [sample]
|
|
19
|
+
4. **If Paper: the link to the Paper frame or page.**
|
|
20
|
+
5. **Does the design include a shell? Which frames hold it?** A product shell is sidebar, top bar and content; a website shell is header and footer, plus a pre-footer when the design treats it as a global. [no shell: prototypes stand in the Space]
|
|
21
|
+
6. **Install the `miles-*` skills globally or for this project only?** [project only; the clone already has them]
|
|
22
|
+
7. **Turn on TypeSafe checks? If yes, the TypeSafe API key.** [off] Why opt-in: a template anyone can use cannot require a paid account.
|
|
23
|
+
8. **Will the project run in Conductor workspaces? If yes, the branch they land on.** [no]
|
|
24
|
+
|
|
25
|
+
Comments and the share link come later, with `miles-deploy`, because they need a Vercel project.
|
|
26
|
+
|
|
27
|
+
## 2. Check the machine
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
node --version # 22.18 or newer; else install the current LTS
|
|
31
|
+
pnpm --version # else: corepack enable
|
|
32
|
+
gh auth status # only with a GitHub repo; else ask the user to run gh auth login
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The skills these lean on, installed globally: `emil-prototype`, `emil-writing-skills`, `impeccable`. Check `~/.claude/skills/` and install any that is missing with `npx skills add <source> -g -y`; say which were added.
|
|
36
|
+
|
|
37
|
+
## 3. Create the project
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npm create @milesroxas/design-hub@latest -- --path <path> --visibility private|public|none [--owner <owner>]
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
It copies the template, pins the `@milesroxas/design-hub` package, installs, puts the `miles-*` skills in `.claude/skills/`, makes the first commit, and with `private` or `public` creates the GitHub repo and pushes. It prints one JSON object; `ok: false` names the fix. A second run finds the project and changes nothing, so rerun it after a fix instead of finishing by hand.
|
|
44
|
+
|
|
45
|
+
## 4. Run the setup
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pnpm --dir <path> bootstrap [--typesafe-key <key>] [--conductor --branch <branch>]
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
It picks a port nothing holds and no other project claims, writes it to `.env` and `.claude/launch.json`, and applies the flags. Read the JSON's `warnings`. With question 6 answered globally: `npx skills add milesroxas/design-hub -g -y`.
|
|
52
|
+
|
|
53
|
+
## 5. Record what was answered
|
|
54
|
+
|
|
55
|
+
Write the shell answer under `DESIGN.md`, Shell (kind, the Paper frames, or "none"), so it is built once and every later session reads it instead of asking. Fill `PRODUCT.md` from what the user said about the product; leave a section empty only when they said nothing about it, and say so.
|
|
56
|
+
|
|
57
|
+
## 6. Hand off
|
|
58
|
+
|
|
59
|
+
- **Paper start:** open the Paper link and load `miles-paper-to-prototype`. It reads the fonts and tokens first, then the shell frames, then the feature frame, and leaves `sample-list/` in place as the reference until the first feature exists.
|
|
60
|
+
- **Sample start:** `pnpm dev` and open `http://localhost:<port>/sample-list/`; point the user at `/guide/sample/` for the walkthrough. The next step is `miles-new-feature`.
|
|
61
|
+
|
|
62
|
+
Close with the path, the port, the repo URL when there is one, whether TypeSafe is on, and the one command that starts the server. Nothing is committed unless the user asks.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: miles-get-hostile
|
|
3
|
+
description: "Hostile fixtures for a prototype: fixtures.json with real data from the CMS or the comp, the missing set by hand, and the strategy and design answers for the edge cases (longest, empty, maximum count, missing media). Load when writing or changing fixtures.json, when a reviewer asks what happens with no items or a long name, or when a state needs data to reach it. Triggers on: fixtures, fixture set, hostile, edge case, worst content, longest, empty state, max count, missing image, real data, CMS pull, collection items, fixtures-pull, Webflow token."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Get hostile
|
|
7
|
+
|
|
8
|
+
Pretty placeholder data hides the decision. Every prototype carries five sets, switched by a dial, and a reviewer signs off on all five or none. The cost per feature is one pull and a few hand-written entries.
|
|
9
|
+
|
|
10
|
+
## The sets
|
|
11
|
+
|
|
12
|
+
| Set | Holds | Who writes it |
|
|
13
|
+
| --- | --- | --- |
|
|
14
|
+
| `real` | 8 to 20 real items with only the fields the component renders | the CMS, or the comp |
|
|
15
|
+
| `longest` | the first item with each free-text field named in `text` at the longest real value, doubled | `@kit/fixtures` |
|
|
16
|
+
| `empty` | nothing | `@kit/fixtures` |
|
|
17
|
+
| `max` | the real items repeated to the design's maximum count | `@kit/fixtures` |
|
|
18
|
+
| `missing` | items with no image, no link, no owner, no name | you, by hand |
|
|
19
|
+
|
|
20
|
+
`fixtures.json` holds `real` and `missing`. `fixtureSets(data, { max, text })` fills the rest at runtime; a key written in the JSON by hand wins over the derived one, for the component whose edge no rule can know.
|
|
21
|
+
|
|
22
|
+
`text` lists the fields a reviewer reads as prose: a name, a title, a description. Never a date, a status, an id, a URL or a colour. Why: doubling those does not make a longer value, it makes a broken one (`Invalid Date`, a status the code does not know), and the review then argues about a bug the CMS can never produce.
|
|
23
|
+
|
|
24
|
+
## Where `real` comes from, cheapest first
|
|
25
|
+
|
|
26
|
+
1. **A Webflow project with `WEBFLOW_TOKEN` in `.env`.** Run `pnpm fixtures-pull <slug> --collection <id>` once with no `--map`: it lists the collection's fields. Then pull with one `--map key=field.path` per field the component renders (`--map cover=thumbnail.url`). It pages through any `--limit` and keeps every other key in `fixtures.json`. Why a script: it costs no tokens per item, so a pull of 200 items costs the same as 20.
|
|
27
|
+
2. **A Webflow project without a token.** One call to the Webflow MCP's CMS tool for the collection behind the design, `limit` 20 or fewer. Keep only the fields the component renders. Never a full-collection pull: every field of every item spends tokens on nothing the component shows.
|
|
28
|
+
3. **No CMS.** Write `real` from the content in the Paper comp, word for word. Eight items minimum, because a list of three never shows its rhythm.
|
|
29
|
+
4. **A different CMS.** One read of up to 20 items with the same field cut. Say which tool was used in the README's Fixtures section.
|
|
30
|
+
|
|
31
|
+
Strip anything that identifies a private person before writing it down: a phone number, a personal email, a home address. `pnpm preflight` stops on every email and phone number until each is replaced or confirmed as a business contact.
|
|
32
|
+
|
|
33
|
+
## The shape
|
|
34
|
+
|
|
35
|
+
There is no shared schema. The component's props type is the schema, and TypeScript checks the JSON against it:
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
const sets = fixtureSets(data satisfies { real: Item[]; missing: Item[] }, { max: 40, text: ['name', 'owner'] });
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Keep `id` on every item: `max` suffixes it to keep keys unique.
|
|
42
|
+
|
|
43
|
+
## What each edge case means for the design
|
|
44
|
+
|
|
45
|
+
Decide these before the user sees the set, and write the answer in the README:
|
|
46
|
+
|
|
47
|
+
- **Longest.** The text wraps or truncates, and the choice is written down. A column that holds the longest value is the column's width; nothing else on the row moves. Why: the CMS will hold a longer title than anything in it today, and a layout that moves under it was never tested.
|
|
48
|
+
- **Empty.** One sentence that says the state and the next step, and the one action that starts it. Never a blank card, never a disabled button beside an empty table. Why: the first visit is the page's teaching moment.
|
|
49
|
+
- **Max.** The page at the design's maximum count, scrolled. Entrance stagger capped at eight so the fortieth row does not wait a second; over about 200 rows, `content-visibility: auto` or a window. Why: the maximum is where performance gets judged.
|
|
50
|
+
- **Missing.** Each absent field says what is absent where the value would be (`Untitled`, `No owner`, a dashed tile for a cover). Never a broken image, never a blank cell. Why: a blank reads as a bug, a word reads as a state.
|
|
51
|
+
|
|
52
|
+
## On the dial
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
const review = useDials('Review', {
|
|
56
|
+
fixtures: { type: 'select', options: [...SET_NAMES], default: 'real' },
|
|
57
|
+
}, { hints: { fixtures: 'Which fixture set the list shows.' } });
|
|
58
|
+
const items = sets[review.fixtures as SetName];
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Each set is a row in the README's States table (`Controls → Review → Fixtures → **longest**`), so the walkthrough and the port report can name it.
|