@milesroxas/design-hub 0.0.0-stage → 0.1.0

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 (217) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +80 -2
  3. package/client.d.ts +6 -0
  4. package/configs/biome/biome.json +63 -0
  5. package/configs/tsconfig.project.json +17 -0
  6. package/core/bytes.test.ts +14 -0
  7. package/core/bytes.ts +14 -0
  8. package/core/catalog.test.ts +44 -0
  9. package/core/catalog.ts +43 -0
  10. package/core/checks.test.ts +23 -0
  11. package/core/checks.ts +72 -0
  12. package/core/contacts.test.ts +31 -0
  13. package/core/contacts.ts +38 -0
  14. package/core/doc.ts +4 -0
  15. package/core/feature.ts +6 -0
  16. package/core/fixtures.test.ts +33 -0
  17. package/core/fixtures.ts +69 -0
  18. package/core/frontmatter.ts +15 -0
  19. package/core/html.ts +3 -0
  20. package/core/image.test.ts +22 -0
  21. package/core/image.ts +37 -0
  22. package/core/jev.test.ts +66 -0
  23. package/core/jev.ts +77 -0
  24. package/core/review/model.ts +189 -0
  25. package/core/review/policy.ts +26 -0
  26. package/core/review/service.test.ts +163 -0
  27. package/core/review/service.ts +309 -0
  28. package/core/review/store.ts +51 -0
  29. package/core/review/triage.test.ts +41 -0
  30. package/core/review/triage.ts +58 -0
  31. package/core/states.test.ts +36 -0
  32. package/core/states.ts +34 -0
  33. package/dist/core/bytes.d.ts +5 -0
  34. package/dist/core/bytes.js +9 -0
  35. package/dist/core/catalog.d.ts +17 -0
  36. package/dist/core/catalog.js +24 -0
  37. package/dist/core/checks.d.ts +41 -0
  38. package/dist/core/checks.js +53 -0
  39. package/dist/core/contacts.d.ts +9 -0
  40. package/dist/core/contacts.js +34 -0
  41. package/dist/core/doc.d.ts +11 -0
  42. package/dist/core/doc.js +1 -0
  43. package/dist/core/feature.d.ts +3 -0
  44. package/dist/core/feature.js +4 -0
  45. package/dist/core/fixtures.d.ts +19 -0
  46. package/dist/core/fixtures.js +52 -0
  47. package/dist/core/frontmatter.d.ts +4 -0
  48. package/dist/core/frontmatter.js +15 -0
  49. package/dist/core/html.d.ts +2 -0
  50. package/dist/core/html.js +2 -0
  51. package/dist/core/image.d.ts +6 -0
  52. package/dist/core/image.js +34 -0
  53. package/dist/core/jev.d.ts +54 -0
  54. package/dist/core/jev.js +52 -0
  55. package/dist/core/review/model.d.ts +85 -0
  56. package/dist/core/review/model.js +112 -0
  57. package/dist/core/review/policy.d.ts +3 -0
  58. package/dist/core/review/policy.js +19 -0
  59. package/dist/core/review/service.d.ts +79 -0
  60. package/dist/core/review/service.js +245 -0
  61. package/dist/core/review/store.d.ts +20 -0
  62. package/dist/core/review/store.js +37 -0
  63. package/dist/core/review/triage.d.ts +6 -0
  64. package/dist/core/review/triage.js +45 -0
  65. package/dist/core/states.d.ts +6 -0
  66. package/dist/core/states.js +29 -0
  67. package/dist/scripts/bootstrap.d.ts +51 -0
  68. package/dist/scripts/bootstrap.js +278 -0
  69. package/dist/scripts/calibrate.d.ts +127 -0
  70. package/dist/scripts/calibrate.js +164 -0
  71. package/dist/scripts/check.d.ts +25 -0
  72. package/dist/scripts/check.js +30 -0
  73. package/dist/scripts/cli.d.ts +2 -0
  74. package/dist/scripts/cli.js +73 -0
  75. package/dist/scripts/commands.d.ts +25 -0
  76. package/dist/scripts/commands.js +49 -0
  77. package/dist/scripts/comments.d.ts +63 -0
  78. package/dist/scripts/comments.js +152 -0
  79. package/dist/scripts/dev.d.ts +46 -0
  80. package/dist/scripts/dev.js +84 -0
  81. package/dist/scripts/elyx.d.ts +61 -0
  82. package/dist/scripts/elyx.js +195 -0
  83. package/dist/scripts/fixtures-pull.d.ts +60 -0
  84. package/dist/scripts/fixtures-pull.js +121 -0
  85. package/dist/scripts/guard.d.ts +58 -0
  86. package/dist/scripts/guard.js +201 -0
  87. package/dist/scripts/hook.d.ts +18 -0
  88. package/dist/scripts/hook.js +46 -0
  89. package/dist/scripts/lib/bin.d.ts +4 -0
  90. package/dist/scripts/lib/bin.js +20 -0
  91. package/dist/scripts/lib/catalog.d.ts +7 -0
  92. package/dist/scripts/lib/catalog.js +18 -0
  93. package/dist/scripts/lib/cli.d.ts +75 -0
  94. package/dist/scripts/lib/cli.js +88 -0
  95. package/dist/scripts/lib/css-tokens.d.ts +13 -0
  96. package/dist/scripts/lib/css-tokens.js +67 -0
  97. package/dist/scripts/lib/judge.d.ts +24 -0
  98. package/dist/scripts/lib/judge.js +60 -0
  99. package/dist/scripts/lib/ports.d.ts +35 -0
  100. package/dist/scripts/lib/ports.js +113 -0
  101. package/dist/scripts/lib/review.d.ts +7 -0
  102. package/dist/scripts/lib/review.js +22 -0
  103. package/dist/scripts/lib/rules.d.ts +12 -0
  104. package/dist/scripts/lib/rules.js +20 -0
  105. package/dist/scripts/lib/vercel.d.ts +17 -0
  106. package/dist/scripts/lib/vercel.js +53 -0
  107. package/dist/scripts/new.d.ts +47 -0
  108. package/dist/scripts/new.js +113 -0
  109. package/dist/scripts/people.d.ts +59 -0
  110. package/dist/scripts/people.js +73 -0
  111. package/dist/scripts/preflight.d.ts +35 -0
  112. package/dist/scripts/preflight.js +251 -0
  113. package/dist/scripts/release-check.d.ts +19 -0
  114. package/dist/scripts/release-check.js +39 -0
  115. package/dist/scripts/skill-hint.d.ts +32 -0
  116. package/dist/scripts/skill-hint.js +103 -0
  117. package/dist/scripts/skills.d.ts +13 -0
  118. package/dist/scripts/skills.js +34 -0
  119. package/dist/scripts/visual-match.d.ts +46 -0
  120. package/dist/scripts/visual-match.js +93 -0
  121. package/dist/server/api.d.ts +22 -0
  122. package/dist/server/api.js +143 -0
  123. package/dist/server/checks.d.ts +4 -0
  124. package/dist/server/checks.js +22 -0
  125. package/dist/server/dev.d.ts +2 -0
  126. package/dist/server/dev.js +54 -0
  127. package/dist/server/env.d.ts +8 -0
  128. package/dist/server/env.js +57 -0
  129. package/dist/server/fake-upstash.d.ts +1 -0
  130. package/dist/server/fake-upstash.js +37 -0
  131. package/dist/server/file-kv.d.ts +2 -0
  132. package/dist/server/file-kv.js +24 -0
  133. package/dist/server/local.d.ts +4 -0
  134. package/dist/server/local.js +25 -0
  135. package/dist/server/paths.d.ts +22 -0
  136. package/dist/server/paths.js +81 -0
  137. package/dist/server/session.d.ts +22 -0
  138. package/dist/server/session.js +110 -0
  139. package/dist/server/upstash.d.ts +7 -0
  140. package/dist/server/upstash.js +41 -0
  141. package/dist/vercel/index.d.ts +5 -0
  142. package/dist/vercel/index.js +12 -0
  143. package/dist/vercel/proxy.d.ts +11 -0
  144. package/dist/vercel/proxy.js +238 -0
  145. package/dist/vite/kit.d.ts +4 -0
  146. package/dist/vite/kit.js +35 -0
  147. package/dist/vite/markdown.d.ts +2 -0
  148. package/dist/vite/markdown.js +212 -0
  149. package/dist/vite/plugin.d.ts +15 -0
  150. package/dist/vite/plugin.js +84 -0
  151. package/dist/vite/registry.d.ts +7 -0
  152. package/dist/vite/registry.js +70 -0
  153. package/kit/Icon.tsx +39 -0
  154. package/kit/Space.tsx +25 -0
  155. package/kit/Variants.tsx +134 -0
  156. package/kit/ViewTools.tsx +34 -0
  157. package/kit/base.css +27 -0
  158. package/kit/dials.css +98 -0
  159. package/kit/dials.tsx +322 -0
  160. package/kit/fixtures.ts +3 -0
  161. package/kit/format.ts +13 -0
  162. package/kit/harness.css +99 -0
  163. package/kit/hooks.ts +35 -0
  164. package/kit/icons.ts +329 -0
  165. package/kit/motion.ts +18 -0
  166. package/kit/prefs.ts +111 -0
  167. package/kit/space.css +12 -0
  168. package/kit/ui/Button.tsx +135 -0
  169. package/kit/ui/Menu.tsx +386 -0
  170. package/kit/ui/Overlay.tsx +300 -0
  171. package/kit/ui/Toast.tsx +187 -0
  172. package/kit/ui/index.ts +13 -0
  173. package/kit/ui/layer.ts +51 -0
  174. package/kit/ui/ui.css +238 -0
  175. package/package.json +87 -3
  176. package/shell/Guide.tsx +160 -0
  177. package/shell/Index.tsx +301 -0
  178. package/shell/Notice.tsx +27 -0
  179. package/shell/Prototype.tsx +31 -0
  180. package/shell/a11y.ts +50 -0
  181. package/shell/doc.d.ts +7 -0
  182. package/shell/doc.ts +3 -0
  183. package/shell/guide.css +133 -0
  184. package/shell/index.html +17 -0
  185. package/shell/main.tsx +53 -0
  186. package/shell/public/favicon.svg +1 -0
  187. package/shell/registry.ts +21 -0
  188. package/shell/registry.virtual.d.ts +10 -0
  189. package/shell/review/Review.tsx +989 -0
  190. package/shell/review/anchor.ts +132 -0
  191. package/shell/review/client.ts +165 -0
  192. package/shell/review/mount.tsx +12 -0
  193. package/shell/review/review.css +296 -0
  194. package/shell/shell.css +160 -0
  195. package/shell/system.css +238 -0
  196. package/skills/miles-conductor/SKILL.md +37 -0
  197. package/skills/miles-deploy/SKILL.md +67 -0
  198. package/skills/miles-design-hub/SKILL.md +62 -0
  199. package/skills/miles-get-hostile/SKILL.md +61 -0
  200. package/skills/miles-new-feature/SKILL.md +40 -0
  201. package/skills/miles-paper-to-prototype/SKILL.md +46 -0
  202. package/skills/miles-port/SKILL.md +59 -0
  203. package/skills/miles-port/references/astro.md +35 -0
  204. package/skills/miles-port/references/next.md +36 -0
  205. package/skills/miles-prototype/SKILL.md +83 -0
  206. package/skills/miles-prototype/references/accessibility.md +48 -0
  207. package/skills/miles-prototype/references/motion.md +59 -0
  208. package/skills/miles-prototype/references/performance.md +45 -0
  209. package/skills/miles-review/SKILL.md +39 -0
  210. package/templates/app/README.md +41 -0
  211. package/templates/app/index.tsx +32 -0
  212. package/templates/app/styles.css +1 -0
  213. package/templates/conductor/settings.toml +29 -0
  214. package/templates/guide.md +38 -0
  215. package/templates/space/README.md +37 -0
  216. package/templates/space/index.tsx +43 -0
  217. package/templates/space/styles.css +13 -0
@@ -0,0 +1,187 @@
1
+ import { useCallback, useEffect, useLayoutEffect, useRef, useState, useSyncExternalStore } from 'react';
2
+ import { createPortal } from 'react-dom';
3
+ import { Icon, type IconName } from '../Icon';
4
+ import { prefersReducedMotion } from '../prefs';
5
+ import { afterTransition } from './layer';
6
+ import './ui.css';
7
+
8
+ type ToastAction = { label: string; onClick: () => void };
9
+ type ToastKind = 'default' | 'error' | 'scope';
10
+ type ToastEntry = {
11
+ id: number;
12
+ message: string;
13
+ action?: ToastAction;
14
+ kind: ToastKind;
15
+ duration: number;
16
+ };
17
+
18
+ // default and error are the product speaking; scope is the prototype itself,
19
+ // in the harness material, for anything the sketch does not build.
20
+ const GLYPH: Record<ToastKind, IconName> = {
21
+ default: 'check',
22
+ error: 'alert-triangle',
23
+ scope: 'circle-dashed',
24
+ };
25
+
26
+ let toasts: ToastEntry[] = [];
27
+ let seq = 0;
28
+ let beforeChange: (() => void) | null = null;
29
+ const listeners = new Set<() => void>();
30
+ const dismissers = new Map<number, () => void>();
31
+
32
+ function setToasts(next: ToastEntry[]) {
33
+ beforeChange?.();
34
+ toasts = next;
35
+ for (const fn of listeners) fn();
36
+ }
37
+
38
+ const subscribe = (fn: () => void) => {
39
+ listeners.add(fn);
40
+ return () => listeners.delete(fn);
41
+ };
42
+
43
+ export function toast(
44
+ message: string,
45
+ {
46
+ action,
47
+ duration = 5000,
48
+ kind = 'default',
49
+ }: { action?: ToastAction; duration?: number; kind?: ToastKind } = {},
50
+ ) {
51
+ const id = ++seq;
52
+ setToasts([...toasts, { id, message, action, kind, duration }]);
53
+ return { dismiss: () => dismissers.get(id)?.() };
54
+ }
55
+
56
+ export function notInPrototype(what: string) {
57
+ return toast(`${what} is not part of this prototype.`, { kind: 'scope' });
58
+ }
59
+
60
+ // Siblings slide to their new slot when a toast lands or leaves (FLIP, transform only).
61
+ export function Toaster() {
62
+ const list = useSyncExternalStore(subscribe, () => toasts);
63
+ const host = useRef<HTMLDivElement>(null);
64
+ const before = useRef(new Map<string, number>());
65
+
66
+ useLayoutEffect(() => {
67
+ beforeChange = () => {
68
+ before.current = new Map();
69
+ if (prefersReducedMotion()) return;
70
+ for (const n of host.current?.querySelectorAll<HTMLElement>('.toast.is-open') ?? []) {
71
+ before.current.set(n.dataset.id ?? '', n.getBoundingClientRect().top);
72
+ }
73
+ };
74
+ return () => {
75
+ beforeChange = null;
76
+ };
77
+ }, []);
78
+
79
+ useLayoutEffect(() => {
80
+ if (!list) return;
81
+ for (const [id, top] of before.current) {
82
+ const n = host.current?.querySelector<HTMLElement>(`[data-id="${id}"]`);
83
+ if (!n) continue;
84
+ const dy = top - n.getBoundingClientRect().top;
85
+ if (!dy) continue;
86
+ n.style.transition = 'none';
87
+ n.style.transform = `translateY(${dy}px)`;
88
+ void n.offsetWidth;
89
+ n.style.transition = '';
90
+ n.style.transform = '';
91
+ }
92
+ before.current.clear();
93
+ }, [list]);
94
+
95
+ return createPortal(
96
+ <div ref={host} id="toasts" className="toasts" aria-live="polite">
97
+ {list.map((t) => (
98
+ <ToastItem key={t.id} entry={t} />
99
+ ))}
100
+ </div>,
101
+ document.body,
102
+ );
103
+ }
104
+
105
+ const RESUME_MS = 2500;
106
+
107
+ function ToastItem({ entry }: { entry: ToastEntry }) {
108
+ const ref = useRef<HTMLDivElement>(null);
109
+ const timer = useRef(0);
110
+ const [phase, setPhase] = useState<'enter' | 'open' | 'leaving'>('enter');
111
+ const leaving = useRef(false);
112
+
113
+ const dismiss = useCallback(() => {
114
+ if (leaving.current) return;
115
+ leaving.current = true;
116
+ clearTimeout(timer.current);
117
+ setPhase('leaving');
118
+ afterTransition(ref.current, 200).then(() => setToasts(toasts.filter((t) => t.id !== entry.id)));
119
+ }, [entry.id]);
120
+
121
+ const hold = useCallback(() => clearTimeout(timer.current), []);
122
+ const resume = useCallback(() => {
123
+ if (leaving.current) return;
124
+ clearTimeout(timer.current);
125
+ timer.current = window.setTimeout(dismiss, RESUME_MS);
126
+ }, [dismiss]);
127
+
128
+ useLayoutEffect(() => {
129
+ void ref.current?.offsetWidth;
130
+ setPhase((p) => (p === 'enter' ? 'open' : p));
131
+ }, []);
132
+
133
+ useEffect(() => {
134
+ timer.current = window.setTimeout(dismiss, entry.duration);
135
+ dismissers.set(entry.id, dismiss);
136
+ // A toast the user cannot see should not run out while the tab is hidden.
137
+ const onVisibility = () => (document.hidden ? hold() : resume());
138
+ document.addEventListener('visibilitychange', onVisibility);
139
+ return () => {
140
+ clearTimeout(timer.current);
141
+ dismissers.delete(entry.id);
142
+ document.removeEventListener('visibilitychange', onVisibility);
143
+ };
144
+ }, [dismiss, hold, resume, entry]);
145
+
146
+ const cls = [
147
+ 'toast',
148
+ `toast-${entry.kind}`,
149
+ phase === 'open' ? 'is-open' : '',
150
+ phase === 'leaving' ? 'is-leaving' : '',
151
+ ]
152
+ .filter(Boolean)
153
+ .join(' ');
154
+
155
+ return (
156
+ <div
157
+ ref={ref}
158
+ data-id={entry.id}
159
+ className={cls}
160
+ role="status"
161
+ onPointerEnter={hold}
162
+ onPointerLeave={resume}
163
+ onFocus={hold}
164
+ onBlur={resume}
165
+ >
166
+ <span className="toast-glyph">
167
+ <Icon name={GLYPH[entry.kind]} size={16} />
168
+ </span>
169
+ <span className="toast-text">{entry.message}</span>
170
+ {entry.action ? (
171
+ <button
172
+ type="button"
173
+ className="toast-action"
174
+ onClick={() => {
175
+ entry.action?.onClick();
176
+ dismiss();
177
+ }}
178
+ >
179
+ {entry.action.label}
180
+ </button>
181
+ ) : null}
182
+ <button type="button" className="toast-close" aria-label="Dismiss" onClick={dismiss}>
183
+ <Icon name="x-close" size={16} />
184
+ </button>
185
+ </div>
186
+ );
187
+ }
@@ -0,0 +1,13 @@
1
+ export { Badge, Button, Checkbox, Chevron, Segmented } from './Button';
2
+ export { installPressFeedback } from './layer';
3
+ export { type MenuConfig, type MenuEntry, type MenuItem, Select, type SelectOption, useMenu } from './Menu';
4
+ export {
5
+ confirmDialog,
6
+ OverlayHost,
7
+ openDialog,
8
+ openSheet,
9
+ SheetBody,
10
+ SheetFoot,
11
+ type SheetHandle,
12
+ } from './Overlay';
13
+ export { notInPrototype, Toaster, toast } from './Toast';
@@ -0,0 +1,51 @@
1
+ // Resolves on the node's own transitionend (not a child's) or after ms, whichever comes first.
2
+ export function afterTransition(node: Element | null, ms: number) {
3
+ return new Promise<void>((resolve) => {
4
+ if (!node) return resolve();
5
+ let done = false;
6
+ const finish = (e?: Event) => {
7
+ if (e && e.target !== node) return;
8
+ if (done) return;
9
+ done = true;
10
+ node.removeEventListener('transitionend', finish);
11
+ resolve();
12
+ };
13
+ node.addEventListener('transitionend', finish);
14
+ setTimeout(finish, ms + 60);
15
+ });
16
+ }
17
+
18
+ export const FOCUSABLE =
19
+ 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
20
+
21
+ export function trapTab(e: KeyboardEvent, root: HTMLElement) {
22
+ if (e.key !== 'Tab') return;
23
+ const list = [...root.querySelectorAll<HTMLElement>(FOCUSABLE)].filter((n) => n.offsetParent !== null);
24
+ if (!list.length) return;
25
+ const i = list.indexOf(document.activeElement as HTMLElement);
26
+ if (e.shiftKey && i <= 0) {
27
+ e.preventDefault();
28
+ list.at(-1)?.focus();
29
+ } else if (!e.shiftKey && (i === list.length - 1 || i === -1)) {
30
+ e.preventDefault();
31
+ list[0].focus();
32
+ }
33
+ }
34
+
35
+ let pressInstalled = false;
36
+
37
+ // Menu items and rows press on pointerdown; buttons do it with :active alone.
38
+ export function installPressFeedback() {
39
+ if (pressInstalled) return;
40
+ pressInstalled = true;
41
+ const lift = () => {
42
+ for (const n of document.querySelectorAll('.is-pressed')) n.classList.remove('is-pressed');
43
+ };
44
+ document.addEventListener('pointerdown', (e) => {
45
+ const b = (e.target as Element).closest?.('.menu-item, .list-row') as HTMLButtonElement | null;
46
+ if (b && !b.disabled) b.classList.add('is-pressed');
47
+ });
48
+ document.addEventListener('pointerup', lift);
49
+ document.addEventListener('pointercancel', lift);
50
+ document.addEventListener('pointerleave', lift, true);
51
+ }
package/kit/ui/ui.css ADDED
@@ -0,0 +1,238 @@
1
+ /* Product materials: the kit's controls on the tokens in tokens.css, with
2
+ the motion layer from the same file, transform and opacity only.
3
+ Reduced motion keys off :root[data-motion="reduce"] (prefs.ts). */
4
+
5
+ body.has-overlay { overflow: hidden; }
6
+ /* iOS ignores overflow: hidden on body for touch scrolling: the overlay owns the
7
+ touch instead, and a sheet's body scrolls on its own without chaining to the page. */
8
+ .overlay { touch-action: none; overscroll-behavior: contain; }
9
+ .sheet-body { touch-action: pan-y; overscroll-behavior: contain; }
10
+
11
+ /* ---------- buttons ---------- */
12
+ /* One height per row: 42px (md) in heads and dialogs, 38px (sm) in tighter
13
+ rows. Fill steps rest, hover, then engaged while its menu is open or held. */
14
+ .btn { display: inline-flex; align-items: center; justify-content: center; gap: 4px; border-radius: var(--radius-sm); font-weight: 600; cursor: pointer; border: 1px solid transparent; white-space: nowrap; text-decoration: none; transition: transform var(--dur-press) var(--ease-press), background-color var(--dur-hover) var(--ease-hover), border-color var(--dur-hover) var(--ease-hover), color var(--dur-hover) var(--ease-hover); }
15
+ .btn .icon { transition: color var(--dur-hover) var(--ease-hover), transform var(--dur-menu) var(--ease-out); }
16
+ .btn-md { height: 42px; padding: 10px 14px; }
17
+ .btn-sm { height: 38px; padding: 8px 12px; }
18
+ .btn.btn-icon { padding: 8px; width: 38px; height: 38px; }
19
+ .btn.btn-icon.btn-md { width: 42px; height: 42px; padding: 10px; }
20
+ /* Toolbar rows are 40px, the height of inputs and selects. */
21
+ .toolbar .btn-md, .bulk-bar .btn-md { height: 40px; padding: 9px 14px; }
22
+ .toolbar .btn.btn-icon.btn-md, .bulk-bar .btn.btn-icon { width: 40px; height: 40px; padding: 9px; }
23
+ .btn-tertiary.btn-icon { width: 36px; height: 36px; padding: 10px; }
24
+ /* In over 90ms so the press lands under the finger; back over 160ms. A reversed CSS transition is shortened by how far it got, so a 90ms press-in keeps a quick tap's release near the full 160ms. */
25
+ .btn:active:not(:disabled) { transform: scale(0.97); transition-duration: var(--dur-press-in), var(--dur-hover), var(--dur-hover), var(--dur-hover); }
26
+ .btn:disabled { cursor: not-allowed; opacity: 0.55; }
27
+ .btn-primary { background: var(--button-primary-bg); border-color: var(--button-primary-bg); color: var(--button-primary-fg); box-shadow: var(--shadow-xs); }
28
+ .btn-secondary { background: var(--button-secondary-bg); border-color: var(--button-secondary-border); color: var(--button-secondary-fg); box-shadow: var(--shadow-xs); }
29
+ .btn-secondary[aria-expanded="true"], .btn-secondary:active:not(:disabled) { background: var(--button-secondary-bg_open); color: var(--button-secondary-fg_hover); }
30
+ .btn[aria-expanded="true"] .btn-chevron { transform: rotate(180deg); }
31
+ /* The chevron's 20px box has 5px of air past its ink; the padding gives it back. */
32
+ .btn:has(> .btn-chevron:last-child) { padding-right: 10px; }
33
+ /* A leading glyph stands 2px closer to its edge than the label does to its own (the Hit Area Rule). */
34
+ .btn-md:has(> .icon:first-child, > .btn-content > .icon:first-child):not(.btn-icon) { padding-left: 12px; }
35
+ .btn-sm:has(> .icon:first-child, > .btn-content > .icon:first-child):not(.btn-icon) { padding-left: 10px; }
36
+ .btn-tertiary { background: transparent; color: var(--button-tertiary-fg); }
37
+ .btn-tertiary[aria-expanded="true"], .btn-tertiary:active:not(:disabled) { background: var(--button-secondary-bg_open); color: var(--button-tertiary-fg_hover); }
38
+ .btn-primary-error { background: var(--button-primary-error-bg); border-color: var(--button-primary-error-bg); color: #fff; }
39
+ .btn-secondary-error { background: var(--button-secondary-bg); border-color: var(--button-secondary-error-border); color: var(--button-secondary-error-fg); }
40
+ .btn-label { line-height: 20px; }
41
+ /* Hover is a pointer state: a tap never leaves a button in it. */
42
+ @media (hover: hover) and (pointer: fine) {
43
+ .btn-primary:hover:not(:disabled) { background: var(--button-primary-bg_hover); border-color: var(--button-primary-bg_hover); }
44
+ .btn-secondary:hover:not(:disabled):not([aria-expanded="true"]) { background: var(--button-secondary-bg_hover); color: var(--button-secondary-fg_hover); }
45
+ .btn-tertiary:hover:not(:disabled):not([aria-expanded="true"]) { background: var(--button-tertiary-bg_hover); color: var(--button-tertiary-fg_hover); }
46
+ .btn-primary-error:hover:not(:disabled) { background: var(--button-primary-error-bg_hover); }
47
+ .btn-secondary-error:hover:not(:disabled) { background: var(--button-secondary-error-bg_hover); }
48
+ }
49
+ @keyframes spin { to { transform: rotate(360deg); } }
50
+ .btn-content { display: inline-flex; align-items: center; gap: 4px; transition: filter 200ms var(--ease-hover), opacity 200ms var(--ease-hover); }
51
+ .btn.is-busy .btn-content { filter: blur(2px); opacity: 0.6; }
52
+ .btn.is-busy { pointer-events: none; position: relative; }
53
+ .btn.is-busy::after { content: ''; position: absolute; left: 50%; top: 50%; width: 16px; height: 16px; margin: -8px 0 0 -8px; border: 2px solid currentColor; border-right-color: transparent; border-radius: 50%; animation: spin 0.6s linear infinite; }
54
+
55
+ /* ---------- inputs ---------- */
56
+ .input, .select, .textarea { width: 100%; height: 40px; padding: 0 12px; border: 1px solid var(--border-primary); border-radius: var(--radius-sm); background: var(--bg-primary); color: var(--text-primary); box-shadow: var(--shadow-xs); font: inherit; }
57
+ .input::placeholder { color: var(--text-placeholder); }
58
+ .input:focus-visible, .select:focus-visible, .textarea:focus-visible { box-shadow: var(--focus-ring), var(--shadow-xs); border-color: var(--border-primary); outline: 2px solid transparent; }
59
+ .textarea { height: auto; min-height: 120px; padding: 10px 12px; resize: vertical; line-height: 20px; }
60
+ .search { position: relative; }
61
+ .search .icon { position: absolute; left: 12px; top: 50%; transform: translateY(-50%); color: var(--fg-quaternary); }
62
+ .search .input { padding-left: 32px; }
63
+ .select { display: inline-flex; align-items: center; justify-content: space-between; gap: 8px; cursor: pointer; text-align: left; }
64
+ .select-value { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
65
+ .select-value.is-placeholder { color: var(--text-placeholder); }
66
+ .select { transition: border-color var(--dur-hover) var(--ease-hover), background-color var(--dur-hover) var(--ease-hover); }
67
+ .select-chevron { color: var(--fg-quaternary); transition: transform var(--dur-menu) var(--ease-out); }
68
+ @media (hover: hover) and (pointer: fine) { .select:hover:not([aria-expanded="true"]) { background: var(--button-secondary-bg_hover); } }
69
+ .select[aria-expanded="true"] { border-color: var(--border-primary); background: var(--button-secondary-bg_open); }
70
+ .select[aria-expanded="true"] .select-chevron { transform: rotate(180deg); color: var(--fg-secondary); }
71
+ .field { display: flex; flex-direction: column; gap: 6px; }
72
+ .field-label { font-weight: 500; color: var(--text-secondary); }
73
+ .field-hint { color: var(--text-tertiary); }
74
+ .field-hint.is-warn { color: var(--utility-warning-700); }
75
+ .checkbox { width: 16px; height: 16px; border-radius: var(--radius-xs); border: 1px solid var(--border-primary); background: var(--bg-primary); display: inline-grid; place-items: center; vertical-align: middle; cursor: pointer; flex: none; color: var(--fg-white); appearance: none; margin: 0; }
76
+ .checkbox:disabled { cursor: default; }
77
+ .checkbox:checked, .checkbox:indeterminate { background: var(--bg-brand-solid); border-color: var(--bg-brand-solid); }
78
+ /* The 12px check.svg glyph from the comp, and a dash drawn at the same stroke. */
79
+ .checkbox:checked::after, .checkbox:indeterminate::after { content: ''; width: 12px; height: 12px; background: currentColor; mask: var(--checkbox-mark) center / 12px no-repeat; }
80
+ .checkbox:checked::after { --checkbox-mark: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6L9 17L4 12'/%3E%3C/svg%3E"); }
81
+ .checkbox:indeterminate::after { --checkbox-mark: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round'%3E%3Cpath d='M5 12H19'/%3E%3C/svg%3E"); }
82
+ .segmented { display: inline-flex; border: 1px solid var(--border-primary); border-radius: var(--radius-sm); overflow: hidden; box-shadow: var(--shadow-xs); position: relative; background: var(--bg-primary); }
83
+ .segmented button { flex: 1; height: 38px; padding: 0 16px; white-space: nowrap; background: transparent; border: 0; font-weight: 600; color: var(--text-secondary); cursor: pointer; position: relative; z-index: 1; }
84
+ .segmented button + button { border-left: 1px solid var(--border-primary); }
85
+ .segmented button { transition: background-color var(--dur-hover) var(--ease-hover), color var(--dur-hover) var(--ease-hover); }
86
+ @media (hover: hover) and (pointer: fine) { .segmented button:hover:not([aria-pressed="true"]) { background: var(--button-secondary-bg_hover); color: var(--text-primary); } }
87
+ .segmented button:active:not([aria-pressed="true"]) { background: var(--button-secondary-bg_hover); color: var(--text-primary); }
88
+ .segmented button[aria-pressed="true"] { background: var(--bg-active); color: var(--text-primary); }
89
+ .segmented.w-full { width: 100%; }
90
+ .check-list { display: flex; flex-direction: column; gap: 4px; }
91
+ .check-row { display: flex; align-items: center; gap: 12px; padding: 10px 12px; border-radius: var(--radius-sm); cursor: pointer; }
92
+ @media (hover: hover) and (pointer: fine) { .check-row:hover { background: var(--bg-primary_hover); } }
93
+ .check-row:active { background: var(--bg-primary_hover); }
94
+
95
+ /* ---------- badges ---------- */
96
+ .badge { display: inline-flex; align-items: center; gap: 6px; height: 24px; padding: 2px 8px; border-radius: var(--radius-sm); border: 1px solid var(--border-primary); font-size: 12px; line-height: 18px; color: var(--utility-gray-700); background: var(--bg-primary); white-space: nowrap; }
97
+ .badge .dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
98
+ .badge-success { background: var(--utility-success-50); border-color: var(--utility-success-200); color: var(--utility-success-700); }
99
+ .badge-error { background: var(--utility-error-50); border-color: var(--utility-error-200); color: var(--utility-error-700); }
100
+ .badge-warning { background: var(--utility-warning-50); border-color: var(--utility-warning-200); color: var(--utility-warning-700); }
101
+ .badge-brand { background: var(--utility-brand-50); border-color: var(--utility-brand-200); color: var(--utility-brand-700); }
102
+
103
+ /* ---------- cards, tables, empty states ---------- */
104
+ .card { background: var(--bg-primary); border: 1px solid var(--border-secondary); border-radius: var(--radius-lg); box-shadow: var(--shadow-xs); }
105
+ .card-pad { padding: 24px; }
106
+ .card-title { font-size: 18px; line-height: 28px; font-weight: 600; }
107
+ .card-sub { color: var(--text-tertiary); margin-top: 2px; }
108
+ .card-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 16px; margin-bottom: 16px; }
109
+ .stack { display: flex; flex-direction: column; gap: 24px; }
110
+ .table-card { overflow: hidden; }
111
+ .table-scroll { overflow-x: auto; scrollbar-color: var(--utility-gray-200) var(--bg-primary); }
112
+ .table { width: 100%; border-collapse: separate; border-spacing: 0; }
113
+ .table th { height: 60px; padding: 20px 16px; text-align: left; font-weight: 600; color: var(--text-secondary); background: var(--bg-secondary); border-bottom: 1px solid var(--border-secondary); white-space: nowrap; position: relative; }
114
+ /* A column handle: 2px by 28px at the column's right edge. */
115
+ .table th:not(:last-child)::after { content: ''; position: absolute; right: 0; top: 16px; bottom: 16px; width: 2px; background: var(--border-secondary); }
116
+ .table th.num, .table td.num { text-align: right; }
117
+ .table td { height: 56px; padding: 16px; border-bottom: 1px solid var(--border-secondary); white-space: nowrap; vertical-align: middle; }
118
+ .table tbody tr:last-child td { border-bottom: 0; }
119
+ .table tbody tr.is-link { cursor: pointer; }
120
+ .table tbody tr.is-selected td { background: var(--bg-primary_hover); }
121
+ @media (hover: hover) and (pointer: fine) { .table tbody tr.is-link:hover td { background: var(--bg-primary_hover); } }
122
+ .table tbody tr.is-link:active td { background: var(--bg-primary_hover); }
123
+ .table td.actions { text-align: right; padding-top: 8px; padding-bottom: 8px; }
124
+ .table td.actions .row-actions { display: inline-flex; align-items: center; gap: 4px; }
125
+ /* Checkbox column per the comp: 16px inset, 18px to the name, no divider. The label fills the cell as the hit area.
126
+ The cell is the hit area's containing block wherever the label sits; without it the ::before covers the page. */
127
+ .table td.check, .table th.check { width: 50px; padding: 0; position: relative; }
128
+ .table td:has(> .check-hit) { position: relative; }
129
+ .table .check + th, .table .check + td { padding-left: 0; }
130
+ .table th.check::after { content: none; }
131
+ .table .check-hit { display: flex; align-items: center; width: 50px; padding-left: 16px; cursor: pointer; }
132
+ .table .check-hit::before { content: ''; position: absolute; inset: 0; }
133
+ .table td > .checkbox { display: grid; }
134
+ .table .cell-strong { font-weight: 500; }
135
+ .table .cell-muted { color: var(--text-tertiary); }
136
+ /* The Ledger Foot: a 56px row like the body's, text on the first column's edge, controls 40px (the Ledger Foot Rule). */
137
+ .table-foot { display: flex; align-items: center; justify-content: space-between; height: 56px; padding: 0 16px; border-top: 1px solid var(--border-secondary); color: var(--text-tertiary); font-variant-numeric: tabular-nums; }
138
+ .table-foot .foot-sum, .table-foot .pager { display: flex; align-items: center; gap: 4px; }
139
+ .table-foot .foot-sum { gap: 8px; }
140
+ .table-foot .btn-md { height: 40px; padding: 9px 14px; }
141
+ .table-foot .btn:has(> .btn-chevron:last-child) { padding-right: 10px; }
142
+ .empty { display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; padding: 96px 24px; gap: 8px; color: var(--text-tertiary); min-height: 320px; }
143
+ .empty h3 { color: var(--text-primary); font-size: 16px; line-height: 24px; font-weight: 600; }
144
+ .empty p { max-width: 560px; }
145
+ .empty .featured { width: 48px; height: 48px; border-radius: var(--radius-lg); border: 1px solid var(--border-secondary); display: grid; place-items: center; color: var(--fg-secondary); margin-bottom: 8px; box-shadow: var(--shadow-xs); }
146
+
147
+ /* ---------- menus ---------- */
148
+ /* Grows from its trigger: 4px toward it and 97% at rest, 180ms in and
149
+ 120ms out on the strong ease-out, on its own layer so text scales as one. */
150
+ .menu { position: absolute; z-index: 50; min-width: 200px; max-width: 420px; background: var(--bg-primary); border: 1px solid var(--border-secondary); border-radius: var(--radius-md); box-shadow: var(--shadow-md); padding: 4px; display: flex; flex-direction: column; gap: 0; opacity: 0; transform: translateY(-4px) scale(0.97); will-change: transform, opacity; transition: opacity var(--dur-menu-out) var(--ease-out), transform var(--dur-menu-out) var(--ease-out); }
151
+ .menu[data-side="above"] { transform: translateY(4px) scale(0.97); }
152
+ .menu[data-side="side"] { transform: translateX(-4px) scale(0.97); }
153
+ .menu.is-open { opacity: 1; transform: none; transition: opacity var(--dur-menu) var(--ease-out), transform var(--dur-menu) var(--ease-out); }
154
+ .menu:focus-visible { box-shadow: var(--shadow-md); }
155
+ .menu-item { display: flex; align-items: center; gap: 10px; padding: 8px 10px; border-radius: var(--radius-sm); border: 0; background: transparent; text-align: left; cursor: pointer; width: 100%; color: var(--text-primary); position: relative; --press-dur: var(--dur-press-out); transition: transform var(--press-dur) var(--ease-out), background-color var(--dur-hover) var(--ease-hover); }
156
+ .menu-item:focus-visible, .menu-item.has-open, .menu-item.is-pressed { background: var(--bg-primary_hover); box-shadow: none; }
157
+ @media (hover: hover) and (pointer: fine) { .menu-item:hover { background: var(--bg-primary_hover); box-shadow: none; } .menu-item:hover .menu-glyph { color: var(--fg-secondary); } }
158
+ .menu-item.is-pressed { --press-dur: var(--dur-press-in); transform: scale(0.995); }
159
+ .menu-item[disabled] { color: var(--text-disabled); cursor: not-allowed; }
160
+ /* A menu row is a finger's target where the pointer is one. */
161
+ @media (pointer: coarse) { .menu-item { min-height: 44px; } }
162
+ .menu-glyph { color: var(--fg-quaternary); display: grid; place-items: center; }
163
+ /* A danger item keeps its colour through hover and focus: the glyph never falls back to Ink. */
164
+ .menu-item.is-danger { color: var(--text-error-primary); }
165
+ .menu-item.is-danger .menu-glyph { color: var(--fg-error-primary); }
166
+ .menu-text { display: flex; flex-direction: column; flex: 1; min-width: 0; }
167
+ .menu-label { font-weight: 500; }
168
+ .menu-item.is-selected .menu-label { font-weight: 600; }
169
+ .menu-desc { color: var(--text-tertiary); font-size: 12px; line-height: 18px; }
170
+ .menu-keys { display: flex; gap: 4px; margin-left: auto; flex: none; }
171
+ .kbd { display: inline-grid; place-items: center; min-width: 20px; height: 20px; padding: 0 5px; border: 1px solid var(--border-secondary); border-radius: var(--radius-xs); font: 500 12px/18px var(--font); color: var(--text-tertiary); }
172
+ .kbd:has(.icon) { padding: 0; color: var(--fg-quaternary); }
173
+ .menu-chevron, .menu-check { color: var(--fg-quaternary); margin-left: auto; display: grid; place-items: center; }
174
+ .menu-check { color: var(--fg-brand-primary); }
175
+ .menu-sep { height: 1px; background: var(--border-secondary); margin: 4px 0; flex: none; }
176
+ .menu-header { padding: 8px 10px 4px; color: var(--text-tertiary); font-size: 12px; line-height: 18px; font-weight: 500; }
177
+
178
+ /* ---------- overlays ---------- */
179
+ /* Dialogs are centred, so they scale from their own centre. The exit is
180
+ shorter than the entrance and accelerates away (the Layer Exit Rule). */
181
+ .overlay { position: fixed; inset: 0; z-index: 60; background: rgb(12 17 29 / 0.5); display: grid; place-items: center; opacity: 0; transition: opacity var(--dur-dialog-out) var(--ease-in); }
182
+ .overlay.is-open { opacity: 1; transition: opacity var(--dur-dialog) var(--ease-hover); }
183
+ .dialog { background: var(--bg-primary); border-radius: var(--radius-xl); box-shadow: var(--shadow-xl); border: 1px solid var(--border-secondary); max-width: calc(100vw - 32px); overflow: hidden; transform: scale(0.96); opacity: 0; transition: transform var(--dur-dialog-out) var(--ease-in), opacity var(--dur-dialog-out) var(--ease-in); }
184
+ .overlay.is-open .dialog { transform: scale(1); opacity: 1; transition: transform var(--dur-dialog) var(--ease-out), opacity var(--dur-dialog) var(--ease-out); }
185
+ /* The message and the verbs are two bands set apart by one 24px gap and nothing drawn (the Dialog Foot Rule). */
186
+ .dialog-body { padding: 24px 24px 0; }
187
+ .dialog-title { font-size: 18px; line-height: 28px; font-weight: 600; margin-bottom: 12px; text-wrap: balance; }
188
+ .dialog-text { color: var(--text-tertiary); text-wrap: pretty; }
189
+ .dialog-text p { margin: 0; }
190
+ .dialog-text p + p { margin-top: 8px; }
191
+ .dialog-actions { display: flex; justify-content: flex-end; gap: 12px; padding: 24px; }
192
+ .overlay-sheet { place-items: stretch end; justify-items: end; }
193
+ .overlay-sheet, .overlay-sheet.is-open { transition: opacity var(--dur-dialog) var(--ease-hover); }
194
+ .sheet { background: var(--bg-primary); height: 100%; max-width: 100vw; display: flex; flex-direction: column; box-shadow: var(--shadow-xl); border-left: 1px solid var(--border-secondary); transform: translateX(100%); transition: transform 240ms var(--ease-drawer); }
195
+ .overlay.is-open .sheet { transform: translateX(0); transition: transform var(--dur-sheet) var(--ease-drawer); }
196
+ .sheet-head { display: flex; align-items: center; justify-content: space-between; padding: 24px 24px 16px; }
197
+ .sheet-title { font-size: 18px; line-height: 28px; font-weight: 600; }
198
+ .sheet-body { flex: 1; overflow: auto; padding: 0 24px 24px; display: flex; flex-direction: column; gap: 20px; }
199
+ .sheet-foot { display: flex; justify-content: flex-end; gap: 12px; padding: 16px 24px calc(24px + env(safe-area-inset-bottom, 0px)); border-top: 1px solid var(--border-secondary); }
200
+ /* A 36px glyph button at a sheet's or the rail's edge reaches 44px; nothing sits within 4px of it. */
201
+ .sheet-head .btn-icon, .user .btn-icon { position: relative; }
202
+ .sheet-head .btn-icon::before, .user .btn-icon::before { content: ''; position: absolute; inset: -4px; }
203
+
204
+ /* ---------- toasts ---------- */
205
+ /* In and out through the bottom on transitions, so a second toast retargets. --toast-lift stacks the host above a floating bar. */
206
+ .toasts { position: fixed; right: 24px; bottom: calc(24px + var(--toast-lift, 0px) + env(safe-area-inset-bottom, 0px)); transition: bottom 220ms var(--ease-out); display: flex; flex-direction: column; gap: 8px; z-index: 80; align-items: flex-end; }
207
+ @media (max-width: 767px) { .toasts { left: 16px; right: 16px; bottom: calc(16px + var(--toast-lift, 0px) + env(safe-area-inset-bottom, 0px)); align-items: stretch; } .toast { max-width: none; } }
208
+ .toast { display: flex; align-items: center; gap: 10px; background: var(--bg-primary); border: 1px solid var(--border-secondary); border-radius: var(--radius-lg); box-shadow: var(--shadow-lg); padding: 12px 12px 12px 16px; max-width: 480px; transform: translateY(calc(100% + 24px)); opacity: 0; transition: transform var(--dur-toast) var(--ease-out), opacity var(--dur-toast) var(--ease-out); }
209
+ .toast.is-open { transform: translateY(0); opacity: 1; }
210
+ .toast.is-leaving { transition-duration: var(--dur-toast-out); }
211
+ .toast-glyph { color: var(--fg-success-primary); display: grid; place-items: center; }
212
+ .toast-error .toast-glyph { color: var(--fg-error-primary); }
213
+ .toast-text { text-wrap: pretty; }
214
+ .toast-action { background: transparent; border: 0; font-weight: 600; color: var(--text-brand-secondary); cursor: pointer; padding: 4px 8px; border-radius: var(--radius-xs); white-space: nowrap; transition: transform var(--dur-press) var(--ease-press); }
215
+ .dark-mode .toast-action { color: var(--brand-400); }
216
+ /* A 16px glyph with a 40px target: the pseudo-element carries the extra reach. */
217
+ .toast-close { position: relative; background: transparent; border: 0; color: var(--fg-quaternary); cursor: pointer; padding: 4px; border-radius: var(--radius-xs); display: grid; place-items: center; transition: color var(--dur-hover) var(--ease-hover), transform var(--dur-press) var(--ease-press); }
218
+ .toast-close::before { content: ""; position: absolute; inset: -8px; }
219
+ .toast-action:active, .toast-close:active { transform: scale(0.96); }
220
+ @media (hover: hover) and (pointer: fine) { .toast-close:hover { color: var(--fg-primary); } }
221
+
222
+ /* The scope toast is the prototype speaking, not the product: the harness
223
+ material in both themes, a dashed circle for a slot the sketch leaves empty. */
224
+ .toast-scope { background: var(--harness-bg); border-color: var(--harness-border); box-shadow: var(--harness-shadow); color: var(--harness-fg); }
225
+ .toast-scope .toast-glyph, .toast-scope .toast-close { color: var(--harness-fg-muted); }
226
+ .toast-scope .toast-action { color: #fff; }
227
+ @media (hover: hover) and (pointer: fine) { .toast-scope .toast-close:hover { color: #fff; } }
228
+ .toast-scope :focus-visible { box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.4); }
229
+
230
+ /* ---------- motion gates ---------- */
231
+ :root[data-motion="reduce"] :is(.btn, .btn .icon, .select, .select-chevron, .menu-item) { transition-duration: 0ms; }
232
+ :root[data-motion="reduce"] .menu-item.is-pressed { transform: none; }
233
+ :root[data-motion="reduce"] :is(.menu, .menu[data-side], .menu.is-open, .dialog, .overlay.is-open .dialog) { transform: none; transition: opacity 160ms var(--ease-hover); }
234
+ :root[data-motion="reduce"] :is(.sheet, .overlay.is-open .sheet) { transform: none; transition: opacity 160ms var(--ease-hover); }
235
+ :root[data-motion="reduce"] .overlay-sheet:not(.is-open) .sheet { opacity: 0; }
236
+ :root[data-motion="reduce"] :is(.toast, .toast.is-open, .toast.is-leaving) { transform: none; transition: opacity 200ms var(--ease-hover); }
237
+ :root[data-motion="reduce"] .toasts { transition: none; }
238
+ :root[data-motion="reduce"] .btn.is-busy::after { animation-duration: 1.2s; }
package/package.json CHANGED
@@ -1,6 +1,90 @@
1
1
  {
2
2
  "name": "@milesroxas/design-hub",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0",
4
+ "description": "The Design Hub product: the hub that lists and serves prototypes, the review panel and its API, the password gate, the kit, and the design-hub CLI. A project installs it and keeps only its prototypes, kit materials and docs.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/milesroxas/design-hub.git",
9
+ "directory": "packages/design-hub"
10
+ },
11
+ "type": "module",
12
+ "engines": {
13
+ "node": ">=22.18"
14
+ },
15
+ "bin": {
16
+ "design-hub": "./dist/scripts/cli.js"
17
+ },
18
+ "exports": {
19
+ ".": {
20
+ "types": "./dist/vite/plugin.d.ts",
21
+ "default": "./dist/vite/plugin.js"
22
+ },
23
+ "./vite": {
24
+ "types": "./dist/vite/plugin.d.ts",
25
+ "default": "./dist/vite/plugin.js"
26
+ },
27
+ "./vercel": {
28
+ "types": "./dist/vercel/index.d.ts",
29
+ "default": "./dist/vercel/index.js"
30
+ },
31
+ "./core/*": {
32
+ "types": "./dist/core/*.d.ts",
33
+ "default": "./dist/core/*.js"
34
+ },
35
+ "./kit/*": "./kit/*",
36
+ "./shell/*": "./shell/*",
37
+ "./client": {
38
+ "types": "./client.d.ts"
39
+ },
40
+ "./tsconfig.project.json": "./configs/tsconfig.project.json",
41
+ "./package.json": "./package.json"
42
+ },
43
+ "files": [
44
+ "dist",
45
+ "core",
46
+ "kit",
47
+ "shell",
48
+ "templates",
49
+ "guides",
50
+ "skills",
51
+ "configs",
52
+ "client.d.ts",
53
+ "README.md",
54
+ "LICENSE"
55
+ ],
56
+ "peerDependencies": {
57
+ "react": "^19.0.0",
58
+ "react-dom": "^19.0.0"
59
+ },
60
+ "dependencies": {
61
+ "@biomejs/biome": "2.6.3",
62
+ "@fontsource-variable/geist": "^5.3.0",
63
+ "@fontsource-variable/geist-mono": "^5.3.0",
64
+ "@types/node": "^26.6.6",
65
+ "@types/react": "^19.3.0",
66
+ "@types/react-dom": "^19.3.0",
67
+ "@vercel/functions": "^3.10.4",
68
+ "@vitejs/plugin-react": "^6.1.2",
69
+ "axe-core": "^4.14.0",
70
+ "dialkit": "^2.1.0",
71
+ "marked": "^18.1.2",
72
+ "motion": "^14.6.0",
73
+ "typescript": "^7.0.2",
74
+ "vite": "^8.3.5"
75
+ },
76
+ "devDependencies": {
77
+ "react": "^19.3.1",
78
+ "react-dom": "^19.3.1"
79
+ },
80
+ "optionalDependencies": {
81
+ "@aiforui/lapse": "^0.19.0"
82
+ },
83
+ "scripts": {
84
+ "build": "rm -rf dist && tsc -p tsconfig.build.json",
85
+ "check": "tsc --noEmit && biome check .",
86
+ "fix": "biome check --write .",
87
+ "test": "node --test \"core/**/*.test.ts\" \"server/**/*.test.ts\" \"scripts/**/*.test.ts\" \"vite/**/*.test.ts\" \"vercel/**/*.test.ts\"",
88
+ "quality": "rm -rf .coverage && NODE_V8_COVERAGE=.coverage pnpm run --silent test > /dev/null && npx -y fallow@3.38.0 --coverage .coverage"
89
+ }
6
90
  }