@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,32 @@
1
+ {
2
+ "file-explorer": true,
3
+ "global-search": true,
4
+ "switcher": true,
5
+ "graph": true,
6
+ "backlink": true,
7
+ "canvas": true,
8
+ "outgoing-link": true,
9
+ "tag-pane": true,
10
+ "footnotes": false,
11
+ "properties": true,
12
+ "page-preview": true,
13
+ "daily-notes": true,
14
+ "templates": true,
15
+ "note-composer": true,
16
+ "command-palette": true,
17
+ "slash-command": false,
18
+ "editor-status": true,
19
+ "bookmarks": true,
20
+ "zk-prefixer": false,
21
+ "random-note": false,
22
+ "outline": true,
23
+ "word-count": true,
24
+ "slides": false,
25
+ "audio-recorder": false,
26
+ "workspaces": false,
27
+ "file-recovery": true,
28
+ "publish": false,
29
+ "sync": true,
30
+ "bases": true,
31
+ "webviewer": false
32
+ }
@@ -0,0 +1,19 @@
1
+ # Docs
2
+
3
+ Written documentation for the project's design work. This folder is also an Obsidian vault: open `docs/` in Obsidian to edit here, or edit the Markdown in any editor. Git is the sync.
4
+
5
+ ## Layout
6
+
7
+ | Folder | What goes there |
8
+ | --- | --- |
9
+ | `sprints/<feature>/` | One feature's working documents: `design-doc.md`, `guide.md` (the walkthrough reviewers follow), `port.md` (the port report `miles-port` writes), the exploration it set aside under `archive/`, and `assets/` for the figures only these documents embed. The folder name is the feature's slug, the same one its prototypes use. |
10
+ | `research/` | Product walkthroughs, meeting notes, findings. |
11
+ | `assets/` | Product screenshots and other figures more than one document embeds. |
12
+
13
+ ## Conventions
14
+
15
+ - Standard Markdown links and images (`[text](path.md)`, `![alt](assets/x.jpg)`), not wikilinks, so GitHub and other editors render them. The vault settings in `.obsidian/app.json` enforce this for new links.
16
+ - Callouts use the GitHub alert syntax: `> [!NOTE]`, `> [!TIP]`, `> [!IMPORTANT]`, `> [!WARNING]`, `> [!CAUTION]`. Obsidian renders the same markers.
17
+ - Diagrams go in fenced `mermaid` blocks. Charts go in `assets/` as SVG and are embedded as images.
18
+ - `.obsidian/workspace*.json` is ignored; the rest of `.obsidian/` is committed.
19
+ - Every `sprints/<feature>/guide.md` is also a page on the prototypes site, at `/guide/<feature>/`: the build compiles the Markdown and its images, so an edit here is the edit there, and a new guide needs no wiring `pnpm guide <feature>` starts one.
@@ -0,0 +1,69 @@
1
+ ---
2
+ type: guide
3
+ feature: sample
4
+ date: 2026-10-30
5
+ audience: Anyone learning the template
6
+ ---
7
+
8
+ # Sample list: a walkthrough
9
+
10
+ The one prototype that ships with the template: a list page inside the product shell, with every fixture set on a dial. Nothing here is a real product. It exists so a reviewer can see what a prototype from this hub looks like, and so Claude has a working example of the conventions to read.
11
+
12
+ ## Before you start
13
+
14
+ 1. **Open the prototype** at `/sample-list/`.
15
+ 2. **Find Controls.** The dark round button at the top of the page opens a panel that belongs to the prototype, not the product. Review holds the fixture set and the row density; Motion holds the two numbers the entrance uses; View holds theme, reduced motion and speed. The panel's last row, **Open the walkthrough**, brings this page back in a new tab.
16
+ 3. **Nothing persists.** Reload and the list is back on the real set.
17
+
18
+ ## 1. Read the list
19
+
20
+ **Do:** open the prototype. Eight items arrive one after another.
21
+
22
+ - The row is the link. Its name and owner sit beside a cover; status and date are pinned right so they line up down the list.
23
+ - The foot counts the items (the Ledger Foot Rule).
24
+
25
+ **Try:** click a row. A dark toast says the item would open here. That is the prototype speaking, not the product: anything a control would do that the prototype does not build says so in that voice (the Scope Toast Rule).
26
+
27
+ ## 2. Break it
28
+
29
+ Controls → Review → Fixtures → **longest**
30
+
31
+ The first item carries, in every text field, the longest real value doubled. The name wraps; the status and the date stay where they were.
32
+
33
+ Controls → Review → Fixtures → **empty**
34
+
35
+ The empty state replaces the list. The one action stays in the page head.
36
+
37
+ Controls → Review → Fixtures → **max**
38
+
39
+ Forty rows, the design's maximum. Scroll: the entrance stagger is capped at the eighth row, so the fortieth does not wait a second to appear.
40
+
41
+ Controls → Review → Fixtures → **missing**
42
+
43
+ An item with no cover, one with no owner, one with no name. Each says what is missing where the value would be.
44
+
45
+ ## 3. Tune the entrance
46
+
47
+ Controls → Motion → **Rise** and **Stagger**
48
+
49
+ The numbers the entrance uses, live. Watch at 0.25× (Controls → View → Speed) to see the curve. When a pair feels right, copy it into the README's Decision.
50
+
51
+ ## Every state, and how to reach it
52
+
53
+ | State | How to reach it |
54
+ | --- | --- |
55
+ | Real list | Controls → Review → Fixtures → **real** |
56
+ | Longest names | Controls → Review → Fixtures → **longest** |
57
+ | Empty | Controls → Review → Fixtures → **empty** |
58
+ | Maximum count | Controls → Review → Fixtures → **max** |
59
+ | Missing media | Controls → Review → Fixtures → **missing** |
60
+ | Compact rows | Controls → Review → Density → **compact** |
61
+ | Dark | Controls → View → Theme → **dark** |
62
+
63
+ ## What the prototype does not do
64
+
65
+ Opening an item and adding one are scope toasts. The sidebar's other pages are the shell's stubs.
66
+
67
+ ## What we need from you
68
+
69
+ Nothing. Delete `prototypes/sample-list/` and `docs/sprints/sample/` once the project has a feature of its own.
@@ -0,0 +1,149 @@
1
+ import { type ReactNode, useEffect } from 'react';
2
+ import './app-shell.css';
3
+ import { parkDials } from '@kit/dials';
4
+ import { useHashRoute } from '@kit/hooks';
5
+ import { Icon } from '@kit/Icon';
6
+ import { BRAND, type NavItem, PRODUCT, type User, type Workspace } from '@kit/shell';
7
+ import { Button } from '@kit/ui/Button';
8
+ import { installPressFeedback } from '@kit/ui/layer';
9
+ import { useMenu } from '@kit/ui/Menu';
10
+ import { OverlayHost } from '@kit/ui/Overlay';
11
+ import { Toaster, toast } from '@kit/ui/Toast';
12
+ import { ViewTools } from '@kit/ViewTools';
13
+
14
+ parkDials();
15
+
16
+ type Props = {
17
+ active?: string;
18
+ role?: string;
19
+ nav?: NavItem[];
20
+ navFoot?: NavItem[];
21
+ workspace?: Workspace;
22
+ user?: User;
23
+ children: ReactNode;
24
+ };
25
+
26
+ // The disposable product shell: a sidebar around a page, plus the dialog,
27
+ // sheet and toast hosts and the View dials. The brand, the navigation and
28
+ // the people come from kit/shell.ts; a prototype overrides them by prop
29
+ // only when its question is about them. Nav items are hash routes.
30
+ // Product chrome only: prototype controls belong in DialKit, never in the sidebar.
31
+ export function AppShell({
32
+ active,
33
+ role = 'Owner',
34
+ nav = PRODUCT.nav,
35
+ navFoot = PRODUCT.navFoot,
36
+ workspace = PRODUCT.workspace,
37
+ user = PRODUCT.user,
38
+ children,
39
+ }: Props) {
40
+ const route = useHashRoute();
41
+ const path = active ?? route.path;
42
+
43
+ const workspaceMenu = useMenu({
44
+ items: [
45
+ { label: workspace.name, desc: role, value: 'current' },
46
+ { label: 'Another workspace', desc: 'Viewer', value: 'other' },
47
+ ],
48
+ align: 'start',
49
+ width: 288,
50
+ checkable: true,
51
+ selected: 'current',
52
+ onSelect: (v) => {
53
+ if (v === 'other') toast(`Only ${workspace.name} is in this prototype.`, { kind: 'scope' });
54
+ },
55
+ });
56
+
57
+ useEffect(installPressFeedback, []);
58
+
59
+ const link = (n: NavItem) => {
60
+ const on = path.startsWith(n.href);
61
+ return (
62
+ <a
63
+ key={n.href}
64
+ className={on ? 'nav-item is-active' : 'nav-item'}
65
+ href={n.href}
66
+ aria-current={on ? 'page' : undefined}
67
+ >
68
+ <Icon name={n.glyph} size={20} />
69
+ <span className="nav-label">{n.label}</span>
70
+ </a>
71
+ );
72
+ };
73
+
74
+ return (
75
+ <>
76
+ <div className="shell">
77
+ <aside className="sidebar" aria-label="Sidebar">
78
+ <a className="sidebar-logo" href={nav[0]?.href ?? '#/'} aria-label={BRAND.name}>
79
+ <Icon name={BRAND.glyph} size={24} />
80
+ <span className="sidebar-brand">{BRAND.name}</span>
81
+ </a>
82
+ <button type="button" className="team" {...workspaceMenu.triggerProps}>
83
+ <span className="team-avatar" style={{ background: workspace.color }} aria-hidden="true">
84
+ {workspace.initials}
85
+ </span>
86
+ <span className="team-text">
87
+ <span className="team-name">{workspace.name}</span>
88
+ <span className="team-role">{role}</span>
89
+ </span>
90
+ <Icon name="chevron-selector-vertical" size={20} />
91
+ </button>
92
+ {workspaceMenu.menu}
93
+ <nav className="nav" aria-label="Primary">
94
+ {nav.map(link)}
95
+ </nav>
96
+ <div className="sidebar-foot">
97
+ <nav className="nav" aria-label="Account">
98
+ {navFoot.map(link)}
99
+ </nav>
100
+ <div className="user">
101
+ <span className="user-avatar">{user.initials}</span>
102
+ <span className="user-text">
103
+ <span className="user-name">{user.name}</span>
104
+ <span className="user-email">{user.email}</span>
105
+ </span>
106
+ <Button kind="tertiary" size={null} icon="log-out-01" aria-label="Log out" />
107
+ </div>
108
+ </div>
109
+ </aside>
110
+ <main className="content" id="main">
111
+ {children}
112
+ </main>
113
+ </div>
114
+ <OverlayHost />
115
+ <Toaster />
116
+ <ViewTools />
117
+ </>
118
+ );
119
+ }
120
+
121
+ export function PageHead({
122
+ title,
123
+ desc,
124
+ back,
125
+ actions,
126
+ }: {
127
+ title: string;
128
+ desc?: string;
129
+ back?: { href: string; label: string };
130
+ actions?: ReactNode;
131
+ }) {
132
+ return (
133
+ <>
134
+ {back ? (
135
+ <a className="back-link" href={back.href}>
136
+ <Icon name="arrow-left" size={16} />
137
+ {back.label}
138
+ </a>
139
+ ) : null}
140
+ <div className="page-head">
141
+ <div>
142
+ <h1 className="page-title">{title}</h1>
143
+ {desc ? <p className="page-desc">{desc}</p> : null}
144
+ </div>
145
+ <div className="page-actions">{actions}</div>
146
+ </div>
147
+ </>
148
+ );
149
+ }
@@ -0,0 +1,75 @@
1
+ import { type ReactNode, useEffect } from 'react';
2
+ import './site-shell.css';
3
+ import { useHashRoute } from '@kit/hooks';
4
+ import { Icon } from '@kit/Icon';
5
+ import { BRAND, SITE, type SiteLink } from '@kit/shell';
6
+ import { installPressFeedback } from '@kit/ui/layer';
7
+ import { OverlayHost } from '@kit/ui/Overlay';
8
+ import { Toaster } from '@kit/ui/Toast';
9
+ import { ViewTools } from '@kit/ViewTools';
10
+
11
+ type Props = {
12
+ active?: string;
13
+ nav?: SiteLink[];
14
+ preFooter?: ReactNode;
15
+ children: ReactNode;
16
+ };
17
+
18
+ // The disposable website shell: a site header and footer around a page, with
19
+ // a pre-footer slot for anything the design treats as a global (a newsletter
20
+ // band, a call to action). Reads kit/shell.ts like AppShell does.
21
+ export function SiteShell({ active, nav = SITE.nav, preFooter, children }: Props) {
22
+ const route = useHashRoute();
23
+ const path = active ?? route.path;
24
+
25
+ useEffect(installPressFeedback, []);
26
+
27
+ return (
28
+ <>
29
+ <div className="site">
30
+ <header className="site-head">
31
+ <a className="site-brand" href="#/" aria-label={BRAND.name}>
32
+ <Icon name={BRAND.glyph} size={24} />
33
+ <span>{BRAND.name}</span>
34
+ </a>
35
+ <nav className="site-nav" aria-label="Primary">
36
+ {nav.map((n) => {
37
+ const on = n.href === '#/' ? path === '#/' : path.startsWith(n.href);
38
+ return (
39
+ <a key={n.href} href={n.href} aria-current={on ? 'page' : undefined}>
40
+ {n.label}
41
+ </a>
42
+ );
43
+ })}
44
+ </nav>
45
+ </header>
46
+ <main className="site-main" id="main">
47
+ {children}
48
+ </main>
49
+ {preFooter ? <section className="site-prefoot">{preFooter}</section> : null}
50
+ <footer className="site-foot">
51
+ <div className="site-foot-grid">
52
+ <div className="site-foot-brand">
53
+ <Icon name={BRAND.glyph} size={20} />
54
+ <span>{BRAND.name}</span>
55
+ </div>
56
+ {SITE.footer.columns.map((c) => (
57
+ <nav key={c.title} className="site-foot-col" aria-label={c.title}>
58
+ <p className="site-foot-title">{c.title}</p>
59
+ {c.links.map((l) => (
60
+ <a key={l.href} href={l.href}>
61
+ {l.label}
62
+ </a>
63
+ ))}
64
+ </nav>
65
+ ))}
66
+ </div>
67
+ <p className="site-foot-note">{SITE.footer.note}</p>
68
+ </footer>
69
+ </div>
70
+ <OverlayHost />
71
+ <Toaster />
72
+ <ViewTools />
73
+ </>
74
+ );
75
+ }
@@ -0,0 +1,70 @@
1
+ /* The product shell: sidebar, content column, page head. Brand and navigation come from kit/shell.ts. */
2
+
3
+ /* dvh, not vh: the phone's URL bar is part of 100vh, so a 100vh sidebar
4
+ overflows by its height until the bar collapses. */
5
+ .shell { display: flex; min-height: 100dvh; }
6
+ .sidebar { width: 320px; flex: none; background: var(--bg-secondary_subtle); border-right: 1px solid var(--border-secondary); display: flex; flex-direction: column; padding: calc(24px + env(safe-area-inset-top, 0px)) 16px calc(32px + env(safe-area-inset-bottom, 0px)) calc(16px + env(safe-area-inset-left, 0px)); position: sticky; top: 0; height: 100dvh; }
7
+ .sidebar-logo { display: flex; align-items: center; gap: 8px; padding: 0 8px; color: var(--fg-primary); height: 28px; margin-bottom: 24px; text-decoration: none; border-radius: var(--radius-xs); }
8
+ .sidebar-brand { font-weight: 600; font-size: 16px; line-height: 24px; color: var(--text-primary); }
9
+ .team { display: flex; align-items: center; gap: 12px; padding: 8px; border-radius: var(--radius-sm); width: 100%; border: 0; background: transparent; text-align: left; cursor: pointer; margin-bottom: 24px; }
10
+ .team-avatar { width: 40px; height: 40px; border-radius: var(--radius-sm); display: grid; place-items: center; font-weight: 600; font-size: 16px; color: var(--gray-900); flex: none; }
11
+ .team-text { flex: 1; min-width: 0; }
12
+ .team-name { font-weight: 600; display: block; }
13
+ .team-role { color: var(--text-tertiary); display: block; }
14
+ .team .icon { color: var(--fg-quaternary); }
15
+ .team { transition: background-color var(--dur-hover) var(--ease-hover); }
16
+ @media (hover: hover) and (pointer: fine) { .team:hover:not([aria-expanded="true"]) { background: var(--bg-primary_hover); } }
17
+ .team[aria-expanded="true"] { background: var(--button-secondary-bg_open); }
18
+ .nav { display: flex; flex-direction: column; gap: 4px; }
19
+ .nav-item { display: flex; align-items: center; gap: 10px; height: 36px; padding: 8px; border-radius: var(--radius-sm); color: var(--text-secondary); font-weight: 600; text-decoration: none; border: 0; background: transparent; width: 100%; cursor: pointer; }
20
+ .nav-item .icon { color: var(--nav-item-icon-fg); }
21
+ .nav-item.is-active { background: var(--bg-active); color: var(--text-primary); }
22
+ .nav-item.is-active .icon { color: var(--nav-item-icon-fg_active); }
23
+ @media (hover: hover) and (pointer: fine) { .nav-item:hover { background: var(--bg-primary_hover); } }
24
+ .nav-item:active { background: var(--bg-active); }
25
+ .sidebar-foot { margin-top: auto; display: flex; flex-direction: column; gap: 4px; }
26
+ .user { display: flex; align-items: center; gap: 12px; padding: 8px; margin-top: 24px; }
27
+ .user-avatar { width: 40px; height: 40px; border-radius: 50%; display: grid; place-items: center; background: var(--avatar-bg); border: 1px solid var(--avatar-contrast-border); font-weight: 600; color: var(--text-secondary); flex: none; }
28
+ .user-text { flex: 1; min-width: 0; }
29
+ .user-name { font-weight: 600; display: block; }
30
+ .user-email { color: var(--text-tertiary); display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
31
+ .user .btn-icon { color: var(--fg-quaternary); }
32
+ .content { flex: 1; min-width: 0; }
33
+
34
+ .page { padding: 32px 24px 96px; max-width: 1532px; margin: 0 auto; }
35
+ .page-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; margin-bottom: 24px; }
36
+ /* The title row is 42px, a header button's height, title centred in it. */
37
+ .page-title { font-size: 24px; line-height: 32px; font-weight: 600; padding: 5px 0; }
38
+ .page-desc { color: var(--text-tertiary); font-size: 16px; line-height: 24px; margin-top: 4px; }
39
+ .page-actions { display: flex; gap: 12px; align-items: center; flex: none; }
40
+ /* A 20px line with a 44px reach: the pseudo-element carries the finger's share without moving the title. */
41
+ .back-link { position: relative; display: inline-flex; align-self: flex-start; align-items: center; gap: 6px; color: var(--text-tertiary); text-decoration: none; font-size: 14px; line-height: 20px; margin-bottom: 6px; border-radius: var(--radius-xs); }
42
+ .back-link::before { content: ''; position: absolute; inset: -12px -8px; }
43
+ @media (hover: hover) and (pointer: fine) { .back-link:hover { color: var(--text-secondary); } }
44
+ .back-link:active { color: var(--text-secondary); }
45
+
46
+ /* Harness: parkDials() puts the DialKit trigger beside the wordmark, where the sidebar is empty. */
47
+ @media (min-width: 1024px) { .dialkit-root .dialkit-panel[data-position="top-left"] { left: 262px; } }
48
+
49
+ @media (max-width: 1023px) {
50
+ .sidebar { width: calc(68px + env(safe-area-inset-left, 0px)); padding: calc(24px + env(safe-area-inset-top, 0px)) 12px calc(32px + env(safe-area-inset-bottom, 0px)) calc(12px + env(safe-area-inset-left, 0px)); }
51
+ .sidebar-brand { display: none; } .sidebar-logo { justify-content: center; padding: 0; }
52
+ .team .icon, .user-text, .user .btn-icon { display: none; }
53
+ .team-text, .nav-item .nav-label { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
54
+ .team { justify-content: center; padding: 0; } .team-avatar { width: 36px; height: 36px; font-size: 14px; }
55
+ .nav-item { justify-content: center; padding: 0; width: 44px; margin: 0 auto; }
56
+ .user { justify-content: center; padding: 0; }
57
+ /* The head's actions wrap under the title; an unshrinkable row would push the page wider than the phone. */
58
+ .page-actions { flex: 0 1 auto; min-width: 0; max-width: 100%; flex-wrap: wrap; }
59
+ }
60
+
61
+ /* A finger needs 44px: the rail's icon targets grow to it, keeping their rhythm (28 + 24 = 44 + 8). */
62
+ @media (max-width: 1023px) and (pointer: coarse) {
63
+ .sidebar-logo { height: 44px; margin-bottom: 8px; }
64
+ .team { min-height: 44px; margin-bottom: 16px; }
65
+ .nav-item { height: 44px; }
66
+ }
67
+
68
+ @media (max-width: 767px) {
69
+ .page { padding: 24px 16px 96px; }
70
+ }
@@ -0,0 +1,11 @@
1
+ // The project's icons: the package's Untitled UI line set, plus the project's
2
+ // own glyphs. Add one by pasting its inner SVG here under a name; `@kit/Icon`
3
+ // draws from this file, so a glyph added here is on every prototype at once.
4
+ import { icons as base } from '@milesroxas/design-hub/kit/icons';
5
+
6
+ export const icons = {
7
+ ...base,
8
+ // logo: { vb: '0 0 24 24', inner: '<path d="…" fill="currentColor"/>' },
9
+ } as const;
10
+
11
+ export type IconName = keyof typeof icons;
@@ -0,0 +1,59 @@
1
+ import type { IconName } from '@kit/icons';
2
+
3
+ // The one place a project names its shell. DESIGN.md, Shell, says which Paper
4
+ // frames hold it; the values below are what the kit draws from that answer.
5
+ // AppShell (product: sidebar, content) and SiteShell (website: header,
6
+ // footer) both read this file, so the brand and the navigation are written
7
+ // once and never again inside a prototype.
8
+
9
+ export type NavItem = { href: string; label: string; glyph: IconName };
10
+ export type SiteLink = { href: string; label: string };
11
+ export type User = { name: string; email: string; initials: string };
12
+ export type Workspace = { name: string; initials: string; color: string };
13
+
14
+ export const BRAND = {
15
+ name: 'Design Hub',
16
+ // The product shell draws this glyph beside the name; a project swaps it
17
+ // for its own mark in kit/icons.ts and names it here.
18
+ glyph: 'logo' as IconName,
19
+ };
20
+
21
+ export const PRODUCT = {
22
+ nav: [
23
+ { href: '#/overview', label: 'Overview', glyph: 'grid-01' },
24
+ { href: '#/items', label: 'Items', glyph: 'file-06' },
25
+ { href: '#/people', label: 'People', glyph: 'users-01' },
26
+ { href: '#/reports', label: 'Reports', glyph: 'bar-chart-01' },
27
+ ] satisfies NavItem[],
28
+ navFoot: [{ href: '#/settings', label: 'Settings', glyph: 'settings-02' }] satisfies NavItem[],
29
+ workspace: { name: 'Sample workspace', initials: 'SW', color: '#dbe6fe' } satisfies Workspace,
30
+ user: { name: 'Sam Reviewer', email: 'sam@example.com', initials: 'SR' } satisfies User,
31
+ };
32
+
33
+ export const SITE = {
34
+ nav: [
35
+ { href: '#/', label: 'Home' },
36
+ { href: '#/work', label: 'Work' },
37
+ { href: '#/about', label: 'About' },
38
+ { href: '#/contact', label: 'Contact' },
39
+ ] satisfies SiteLink[],
40
+ footer: {
41
+ columns: [
42
+ {
43
+ title: 'Company',
44
+ links: [
45
+ { href: '#/about', label: 'About' },
46
+ { href: '#/careers', label: 'Careers' },
47
+ ],
48
+ },
49
+ {
50
+ title: 'Legal',
51
+ links: [
52
+ { href: '#/privacy', label: 'Privacy' },
53
+ { href: '#/terms', label: 'Terms' },
54
+ ],
55
+ },
56
+ ] satisfies { title: string; links: SiteLink[] }[],
57
+ note: `© ${new Date().getFullYear()} ${BRAND.name}`,
58
+ },
59
+ };
@@ -0,0 +1,27 @@
1
+ /* The website shell: header, content, optional pre-footer, footer. One
2
+ measure (1120px) for everything, 24px gutters, 16px on a phone. */
3
+ .site { min-height: 100dvh; display: flex; flex-direction: column; }
4
+ .site-head { position: sticky; top: 0; z-index: 10; display: flex; align-items: center; justify-content: space-between; gap: 24px; height: 64px; padding: 0 max(24px, calc((100% - 1120px) / 2)); background: var(--bg-primary); border-bottom: 1px solid var(--border-secondary); }
5
+ .site-brand { display: inline-flex; align-items: center; gap: 8px; font-weight: 600; color: var(--text-primary); text-decoration: none; border-radius: var(--radius-xs); }
6
+ .site-nav { display: flex; gap: 4px; }
7
+ .site-nav a { display: inline-flex; align-items: center; height: 40px; padding: 0 12px; border-radius: var(--radius-sm); color: var(--text-secondary); font-weight: 500; text-decoration: none; transition: background-color var(--dur-hover) var(--ease-hover), color var(--dur-hover) var(--ease-hover); }
8
+ .site-nav a[aria-current="page"] { color: var(--text-primary); background: var(--bg-active); }
9
+ @media (hover: hover) and (pointer: fine) { .site-nav a:hover { background: var(--bg-primary_hover); color: var(--text-primary); } }
10
+ .site-nav a:active { transform: scale(0.97); }
11
+ .site-main { flex: 1; width: min(1120px, 100% - 48px); margin: 0 auto; padding: 48px 0 96px; }
12
+ .site-prefoot { padding: 48px max(24px, calc((100% - 1120px) / 2)); background: var(--bg-secondary); border-top: 1px solid var(--border-secondary); }
13
+ .site-foot { padding: 48px max(24px, calc((100% - 1120px) / 2)) 32px; border-top: 1px solid var(--border-secondary); color: var(--text-tertiary); }
14
+ .site-foot-grid { display: grid; grid-template-columns: 2fr repeat(auto-fit, minmax(140px, 1fr)); gap: 32px; }
15
+ .site-foot-brand { display: inline-flex; align-items: center; gap: 8px; align-self: start; font-weight: 600; color: var(--text-primary); }
16
+ .site-foot-col { display: flex; flex-direction: column; gap: 8px; }
17
+ .site-foot-title { font-weight: 600; color: var(--text-primary); margin-bottom: 4px; }
18
+ .site-foot-col a { color: var(--text-tertiary); text-decoration: none; transition: color var(--dur-hover) var(--ease-hover); }
19
+ @media (hover: hover) and (pointer: fine) { .site-foot-col a:hover { color: var(--text-primary); } }
20
+ .site-foot-note { margin-top: 32px; padding-top: 16px; border-top: 1px solid var(--border-secondary); font-size: 12px; line-height: 18px; }
21
+ :root[data-motion="reduce"] .site-nav a:active { transform: none; }
22
+ @media (max-width: 767px) {
23
+ .site-head { height: 56px; }
24
+ .site-nav a { padding: 0 8px; }
25
+ .site-main { width: min(1120px, 100% - 32px); padding: 32px 0 64px; }
26
+ .site-foot-grid { grid-template-columns: 1fr 1fr; }
27
+ }