@starcier/create-builder 0.0.0-stage → 1.0.0-next.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 +62 -2
- package/dist/api.js +44 -0
- package/dist/args.js +79 -0
- package/dist/auth.js +68 -0
- package/dist/cli.js +23 -0
- package/dist/defaults.js +15 -0
- package/dist/errors.js +10 -0
- package/dist/main.js +162 -0
- package/dist/prompt.js +23 -0
- package/dist/slug.js +20 -0
- package/dist/template.js +61 -0
- package/package.json +34 -4
- package/template/.claude/skills/design-system/SKILL.md +177 -0
- package/template/.claude/skills/design-system-audit/SKILL.md +125 -0
- package/template/.claude/skills/design-work-review/SKILL.md +93 -0
- package/template/.claude/skills/process-interviewer/SKILL.md +124 -0
- package/template/.claude/skills/process-interviewer/references/plan-output-template.md +53 -0
- package/template/.claude/skills/process-interviewer/references/skill-output-template.md +72 -0
- package/template/.claude/skills/prototype-page-builder/SKILL.md +76 -0
- package/template/.cursor/rules/code-review.mdc +30 -0
- package/template/.cursor/rules/prototype-authoring.mdc +31 -0
- package/template/.cursor/rules/tokens.mdc +36 -0
- package/template/.cursor/rules/ui-system.mdc +39 -0
- package/template/.env.example +19 -0
- package/template/AGENTS.md +77 -0
- package/template/CLAUDE.md +19 -0
- package/template/README.md +65 -0
- package/template/_gitignore +15 -0
- package/template/docs/design-system-log.md +17 -0
- package/template/eslint.config.js +23 -0
- package/template/index.html +13 -0
- package/template/package.json +35 -0
- package/template/public/favicon.svg +1 -0
- package/template/src/App.tsx +65 -0
- package/template/src/app.css +5 -0
- package/template/src/brand.ts +2 -0
- package/template/src/config.ts +15 -0
- package/template/src/design-system/ThemeProvider.tsx +25 -0
- package/template/src/design-system/components/Button/Button.tsx +15 -0
- package/template/src/design-system/components/Button/button.css +62 -0
- package/template/src/design-system/components/Button/index.ts +1 -0
- package/template/src/design-system/components/Card/Card.tsx +22 -0
- package/template/src/design-system/components/Card/card.css +27 -0
- package/template/src/design-system/components/Card/index.ts +1 -0
- package/template/src/design-system/components/Field/Field.tsx +28 -0
- package/template/src/design-system/components/Field/field.css +37 -0
- package/template/src/design-system/components/Field/index.ts +1 -0
- package/template/src/design-system/components/Modal/Modal.tsx +55 -0
- package/template/src/design-system/components/Modal/index.ts +1 -0
- package/template/src/design-system/components/Modal/modal.css +39 -0
- package/template/src/design-system/components/ThemeToggle/ThemeToggle.tsx +13 -0
- package/template/src/design-system/components/ThemeToggle/index.ts +1 -0
- package/template/src/design-system/global.css +26 -0
- package/template/src/design-system/palette.ts +40 -0
- package/template/src/design-system/theme.ts +55 -0
- package/template/src/design-system/tokens.ts +126 -0
- package/template/src/main.tsx +13 -0
- package/template/src/prototypes/getting-started/welcome/README.md +6 -0
- package/template/src/prototypes/getting-started/welcome/Welcome.tsx +74 -0
- package/template/src/prototypes/getting-started/welcome/data.ts +14 -0
- package/template/src/prototypes/getting-started/welcome/index.ts +2 -0
- package/template/src/prototypes/getting-started/welcome/welcome.css +71 -0
- package/template/src/prototypes/getting-started/welcome/workflows.ts +51 -0
- package/template/src/prototypes/registry.ts +20 -0
- package/template/src/prototypes/workflows.ts +9 -0
- package/template/tsconfig.app.json +28 -0
- package/template/tsconfig.json +7 -0
- package/template/tsconfig.node.json +26 -0
- package/template/vercel.json +4 -0
- package/template/vite.config.ts +12 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
.ds-button {
|
|
2
|
+
display: inline-flex;
|
|
3
|
+
align-items: center;
|
|
4
|
+
justify-content: center;
|
|
5
|
+
gap: var(--space-s);
|
|
6
|
+
min-height: 24px;
|
|
7
|
+
border: 1px solid transparent;
|
|
8
|
+
border-radius: var(--radius-s);
|
|
9
|
+
font-family: var(--font-sans);
|
|
10
|
+
font-size: var(--text-label-size);
|
|
11
|
+
font-weight: var(--text-label-weight);
|
|
12
|
+
line-height: var(--text-label-line);
|
|
13
|
+
cursor: pointer;
|
|
14
|
+
transition: background-color 120ms ease, border-color 120ms ease;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.ds-button:focus-visible {
|
|
18
|
+
outline: 2px solid var(--color-border-focus);
|
|
19
|
+
outline-offset: 2px;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.ds-button:disabled {
|
|
23
|
+
opacity: 0.5;
|
|
24
|
+
cursor: not-allowed;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.ds-button--medium {
|
|
28
|
+
padding: var(--space-s) var(--space-l);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.ds-button--small {
|
|
32
|
+
padding: var(--space-2xs) var(--space-m);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.ds-button--primary {
|
|
36
|
+
background: var(--color-accent-default);
|
|
37
|
+
color: var(--color-text-inverse);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.ds-button--primary:hover:not(:disabled) {
|
|
41
|
+
background: var(--color-accent-hover);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.ds-button--secondary {
|
|
45
|
+
background: var(--color-bg-surface);
|
|
46
|
+
border-color: var(--color-border-strong);
|
|
47
|
+
color: var(--color-text-primary);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.ds-button--secondary:hover:not(:disabled) {
|
|
51
|
+
background: var(--color-bg-surface-muted);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.ds-button--ghost {
|
|
55
|
+
background: transparent;
|
|
56
|
+
color: var(--color-text-secondary);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.ds-button--ghost:hover:not(:disabled) {
|
|
60
|
+
background: var(--color-bg-surface-muted);
|
|
61
|
+
color: var(--color-text-primary);
|
|
62
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Button, type ButtonProps, type ButtonSize, type ButtonVariant } from './Button';
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { HTMLAttributes, ReactNode } from 'react';
|
|
2
|
+
import './card.css';
|
|
3
|
+
|
|
4
|
+
export interface CardProps extends Omit<HTMLAttributes<HTMLElement>, 'title'> {
|
|
5
|
+
title?: ReactNode;
|
|
6
|
+
/** Shown under the title, in secondary text. */
|
|
7
|
+
description?: ReactNode;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function Card({ title, description, className, children, ...rest }: CardProps) {
|
|
11
|
+
return (
|
|
12
|
+
<section className={['ds-card', className].filter(Boolean).join(' ')} {...rest}>
|
|
13
|
+
{(title || description) && (
|
|
14
|
+
<header className="ds-card__header">
|
|
15
|
+
{title && <h2 className="ds-card__title">{title}</h2>}
|
|
16
|
+
{description && <p className="ds-card__description">{description}</p>}
|
|
17
|
+
</header>
|
|
18
|
+
)}
|
|
19
|
+
{children}
|
|
20
|
+
</section>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
.ds-card {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-direction: column;
|
|
4
|
+
gap: var(--space-l);
|
|
5
|
+
padding: var(--space-2xl);
|
|
6
|
+
background: var(--color-bg-surface);
|
|
7
|
+
border: 1px solid var(--color-border-default);
|
|
8
|
+
border-radius: var(--radius-m);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.ds-card__header {
|
|
12
|
+
display: flex;
|
|
13
|
+
flex-direction: column;
|
|
14
|
+
gap: var(--space-2xs);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.ds-card__title {
|
|
18
|
+
margin: 0;
|
|
19
|
+
font-size: var(--text-title-size);
|
|
20
|
+
font-weight: var(--text-title-weight);
|
|
21
|
+
line-height: var(--text-title-line);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.ds-card__description {
|
|
25
|
+
margin: 0;
|
|
26
|
+
color: var(--color-text-secondary);
|
|
27
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Card, type CardProps } from './Card';
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { useId, type InputHTMLAttributes } from 'react';
|
|
2
|
+
import './field.css';
|
|
3
|
+
|
|
4
|
+
export interface FieldProps extends InputHTMLAttributes<HTMLInputElement> {
|
|
5
|
+
label: string;
|
|
6
|
+
/** Shown under the input, in secondary text. */
|
|
7
|
+
hint?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** A labelled text input. The `.ds-field` wrapper is what the review layer recognises. */
|
|
11
|
+
export function Field({ label, hint, id, className, ...rest }: FieldProps) {
|
|
12
|
+
const generated = useId();
|
|
13
|
+
const inputId = id ?? generated;
|
|
14
|
+
const hintId = hint ? `${inputId}-hint` : undefined;
|
|
15
|
+
return (
|
|
16
|
+
<div className={['ds-field', className].filter(Boolean).join(' ')}>
|
|
17
|
+
<label className="ds-field__label" htmlFor={inputId}>
|
|
18
|
+
{label}
|
|
19
|
+
</label>
|
|
20
|
+
<input id={inputId} className="ds-field__input" aria-describedby={hintId} {...rest} />
|
|
21
|
+
{hint && (
|
|
22
|
+
<span id={hintId} className="ds-field__hint">
|
|
23
|
+
{hint}
|
|
24
|
+
</span>
|
|
25
|
+
)}
|
|
26
|
+
</div>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
.ds-field {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-direction: column;
|
|
4
|
+
gap: var(--space-2xs);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.ds-field__label {
|
|
8
|
+
font-size: var(--text-label-size);
|
|
9
|
+
font-weight: var(--text-label-weight);
|
|
10
|
+
line-height: var(--text-label-line);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.ds-field__input {
|
|
14
|
+
padding: var(--space-s) var(--space-m);
|
|
15
|
+
background: var(--color-bg-surface);
|
|
16
|
+
border: 1px solid var(--color-border-strong);
|
|
17
|
+
border-radius: var(--radius-s);
|
|
18
|
+
color: var(--color-text-primary);
|
|
19
|
+
font-family: var(--font-sans);
|
|
20
|
+
font-size: var(--text-body-size);
|
|
21
|
+
line-height: var(--text-body-line);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.ds-field__input::placeholder {
|
|
25
|
+
color: var(--color-text-secondary);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.ds-field__input:focus-visible {
|
|
29
|
+
outline: 2px solid var(--color-border-focus);
|
|
30
|
+
outline-offset: 1px;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.ds-field__hint {
|
|
34
|
+
color: var(--color-text-secondary);
|
|
35
|
+
font-size: var(--text-caption-size);
|
|
36
|
+
line-height: var(--text-caption-line);
|
|
37
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Field, type FieldProps } from './Field';
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { useEffect, useId, useRef, type HTMLAttributes, type ReactNode } from 'react';
|
|
2
|
+
import { createPortal } from 'react-dom';
|
|
3
|
+
import './modal.css';
|
|
4
|
+
|
|
5
|
+
export interface ModalProps extends Omit<HTMLAttributes<HTMLDivElement>, 'title'> {
|
|
6
|
+
open: boolean;
|
|
7
|
+
onClose(): void;
|
|
8
|
+
title: ReactNode;
|
|
9
|
+
/** Buttons along the bottom, right-aligned. */
|
|
10
|
+
footer?: ReactNode;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A dialog over a scrim, rendered at the end of the document. Escape and a
|
|
15
|
+
* click on the scrim close it. Its root carries `.ds-modal`, which the builder
|
|
16
|
+
* passes to the review layer as an overlay (see `App.tsx`).
|
|
17
|
+
*/
|
|
18
|
+
export function Modal({ open, onClose, title, footer, children, className, ...rest }: ModalProps) {
|
|
19
|
+
const titleId = useId();
|
|
20
|
+
const dialogRef = useRef<HTMLDivElement>(null);
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (!open) return;
|
|
24
|
+
dialogRef.current?.focus();
|
|
25
|
+
const onKey = (event: KeyboardEvent) => {
|
|
26
|
+
if (event.key === 'Escape') onClose();
|
|
27
|
+
};
|
|
28
|
+
document.addEventListener('keydown', onKey);
|
|
29
|
+
return () => document.removeEventListener('keydown', onKey);
|
|
30
|
+
}, [open, onClose]);
|
|
31
|
+
|
|
32
|
+
if (!open) return null;
|
|
33
|
+
|
|
34
|
+
return createPortal(
|
|
35
|
+
<div className="ds-modal" onClick={onClose}>
|
|
36
|
+
<div
|
|
37
|
+
ref={dialogRef}
|
|
38
|
+
role="dialog"
|
|
39
|
+
aria-modal="true"
|
|
40
|
+
aria-labelledby={titleId}
|
|
41
|
+
tabIndex={-1}
|
|
42
|
+
className={['ds-modal__dialog', className].filter(Boolean).join(' ')}
|
|
43
|
+
onClick={(event) => event.stopPropagation()}
|
|
44
|
+
{...rest}
|
|
45
|
+
>
|
|
46
|
+
<h2 id={titleId} className="ds-modal__title">
|
|
47
|
+
{title}
|
|
48
|
+
</h2>
|
|
49
|
+
<div className="ds-modal__body">{children}</div>
|
|
50
|
+
{footer && <div className="ds-modal__footer">{footer}</div>}
|
|
51
|
+
</div>
|
|
52
|
+
</div>,
|
|
53
|
+
document.body,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Modal, type ModalProps } from './Modal';
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/* Above the builder's top bar (200), below its comments and tours (9140 up). */
|
|
2
|
+
.ds-modal {
|
|
3
|
+
position: fixed;
|
|
4
|
+
inset: 0;
|
|
5
|
+
z-index: var(--z-modal);
|
|
6
|
+
display: flex;
|
|
7
|
+
align-items: center;
|
|
8
|
+
justify-content: center;
|
|
9
|
+
padding: var(--space-2xl);
|
|
10
|
+
background: var(--color-scrim);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.ds-modal__dialog {
|
|
14
|
+
display: flex;
|
|
15
|
+
flex-direction: column;
|
|
16
|
+
gap: var(--space-l);
|
|
17
|
+
width: min(480px, 100%);
|
|
18
|
+
padding: var(--space-2xl);
|
|
19
|
+
background: var(--color-bg-surface);
|
|
20
|
+
border-radius: var(--radius-l);
|
|
21
|
+
box-shadow: var(--color-shadow-l);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.ds-modal__dialog:focus {
|
|
25
|
+
outline: none;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.ds-modal__title {
|
|
29
|
+
margin: 0;
|
|
30
|
+
font-size: var(--text-title-size);
|
|
31
|
+
font-weight: var(--text-title-weight);
|
|
32
|
+
line-height: var(--text-title-line);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.ds-modal__footer {
|
|
36
|
+
display: flex;
|
|
37
|
+
justify-content: flex-end;
|
|
38
|
+
gap: var(--space-s);
|
|
39
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { useTheme } from '../../theme';
|
|
2
|
+
import { Button } from '../Button';
|
|
3
|
+
|
|
4
|
+
/** Switches between the light and dark themes. Shown in the builder's top bar. */
|
|
5
|
+
export function ThemeToggle() {
|
|
6
|
+
const { theme, toggle } = useTheme();
|
|
7
|
+
const next = theme === 'light' ? 'dark' : 'light';
|
|
8
|
+
return (
|
|
9
|
+
<Button variant="ghost" size="small" onClick={toggle} aria-label={`Switch to ${next} theme`}>
|
|
10
|
+
{theme === 'light' ? 'Dark theme' : 'Light theme'}
|
|
11
|
+
</Button>
|
|
12
|
+
);
|
|
13
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { ThemeToggle } from './ThemeToggle';
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/* Document-wide defaults. The custom properties are set by ThemeProvider. */
|
|
2
|
+
|
|
3
|
+
*,
|
|
4
|
+
*::before,
|
|
5
|
+
*::after {
|
|
6
|
+
box-sizing: border-box;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
body {
|
|
10
|
+
margin: 0;
|
|
11
|
+
background: var(--color-bg-page);
|
|
12
|
+
color: var(--color-text-primary);
|
|
13
|
+
font-family: var(--font-sans);
|
|
14
|
+
font-size: var(--text-body-size);
|
|
15
|
+
line-height: var(--text-body-line);
|
|
16
|
+
-webkit-font-smoothing: antialiased;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
@media (prefers-reduced-motion: reduce) {
|
|
20
|
+
*,
|
|
21
|
+
*::before,
|
|
22
|
+
*::after {
|
|
23
|
+
transition-duration: 0s !important;
|
|
24
|
+
animation-duration: 0s !important;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { BuilderPalette } from '@starcier/collab-ui';
|
|
2
|
+
import { colors, radius, space, typography, type ThemeName } from './tokens';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Maps this design system's tokens onto `BuilderPalette`, the one visual
|
|
6
|
+
* contract between a builder and the Starcier packages. It colours the
|
|
7
|
+
* builder's chrome — the catalogue, the top bar, sign-in, the share and invite
|
|
8
|
+
* pages — so they look like this product.
|
|
9
|
+
*
|
|
10
|
+
* Comment pins and guided tours do not use it: they keep a fixed look of their
|
|
11
|
+
* own in every builder, on purpose.
|
|
12
|
+
*
|
|
13
|
+
* Change the right-hand sides when the tokens change; the left-hand slot names
|
|
14
|
+
* are the contract and stay as they are.
|
|
15
|
+
*/
|
|
16
|
+
export function resolveBuilderPalette(theme: ThemeName): BuilderPalette {
|
|
17
|
+
const c = colors[theme];
|
|
18
|
+
return {
|
|
19
|
+
pageBg: c.bg.page,
|
|
20
|
+
panel: c.bg.surface,
|
|
21
|
+
panelMuted: c.bg.surfaceMuted,
|
|
22
|
+
border: c.border.default,
|
|
23
|
+
borderAccent: c.border.focus,
|
|
24
|
+
textPrimary: c.text.primary,
|
|
25
|
+
textSecondary: c.text.secondary,
|
|
26
|
+
textAccent: c.text.accent,
|
|
27
|
+
textInverse: c.text.inverse,
|
|
28
|
+
accent: c.accent.default,
|
|
29
|
+
accentHover: c.accent.hover,
|
|
30
|
+
accentSubtle: c.accent.subtle,
|
|
31
|
+
scrim: c.scrim,
|
|
32
|
+
status: c.status,
|
|
33
|
+
shadow: c.shadow,
|
|
34
|
+
space,
|
|
35
|
+
// The contract has five steps below `full`; this scale has four, so the
|
|
36
|
+
// two smallest share a value.
|
|
37
|
+
radius: { '2xs': radius.xs, xs: radius.xs, s: radius.s, m: radius.m, l: radius.l, full: radius.full },
|
|
38
|
+
type: typography,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react';
|
|
2
|
+
import { colors, fontFamily, radius, space, typography, zIndex, type ThemeName } from './tokens';
|
|
3
|
+
|
|
4
|
+
export const THEME_STORAGE_KEY = 'builder-theme';
|
|
5
|
+
|
|
6
|
+
export interface ThemeValue {
|
|
7
|
+
theme: ThemeName;
|
|
8
|
+
toggle(): void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const ThemeContext = createContext<ThemeValue>({ theme: 'light', toggle: () => {} });
|
|
12
|
+
|
|
13
|
+
/** The active theme, and a way to switch it. */
|
|
14
|
+
export function useTheme(): ThemeValue {
|
|
15
|
+
return useContext(ThemeContext);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** The theme saved in this browser, or else the system's preference. */
|
|
19
|
+
export function initialTheme(): ThemeName {
|
|
20
|
+
try {
|
|
21
|
+
const saved = localStorage.getItem(THEME_STORAGE_KEY);
|
|
22
|
+
if (saved === 'light' || saved === 'dark') return saved;
|
|
23
|
+
} catch {
|
|
24
|
+
/* storage unavailable: fall through to the system setting */
|
|
25
|
+
}
|
|
26
|
+
return window.matchMedia?.('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Every token as a CSS custom property, so component stylesheets can use
|
|
31
|
+
* `var(--color-accent-default)` and switch with the theme. Names follow the
|
|
32
|
+
* token paths: `colors.bg.page` → `--color-bg-page`, `space.m` → `--space-m`.
|
|
33
|
+
*/
|
|
34
|
+
export function cssVariables(theme: ThemeName): Record<string, string> {
|
|
35
|
+
const vars: Record<string, string> = {};
|
|
36
|
+
const flatten = (prefix: string, value: unknown) => {
|
|
37
|
+
if (typeof value === 'string') vars[prefix] = value;
|
|
38
|
+
else if (value && typeof value === 'object') {
|
|
39
|
+
for (const [key, inner] of Object.entries(value)) flatten(`${prefix}-${key}`, inner);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
flatten('--color', colors[theme]);
|
|
43
|
+
flatten('--space', space);
|
|
44
|
+
flatten('--radius', radius);
|
|
45
|
+
flatten('--font', fontFamily);
|
|
46
|
+
for (const [role, style] of Object.entries(typography)) {
|
|
47
|
+
vars[`--text-${role}-size`] = style.fontSize;
|
|
48
|
+
vars[`--text-${role}-weight`] = String(style.fontWeight);
|
|
49
|
+
vars[`--text-${role}-line`] = style.lineHeight;
|
|
50
|
+
vars[`--text-${role}-tracking`] = style.letterSpacing;
|
|
51
|
+
vars[`--text-${role}-case`] = style.textTransform;
|
|
52
|
+
}
|
|
53
|
+
vars['--z-modal'] = String(zIndex.modal);
|
|
54
|
+
return vars;
|
|
55
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The design tokens: the single source of every colour, size and text style in
|
|
3
|
+
* this builder. A neutral starting point; replace the values with your own
|
|
4
|
+
* design system's, keep the names, and everything follows.
|
|
5
|
+
*
|
|
6
|
+
* Colours come in a light and a dark set with identical keys. The rest is
|
|
7
|
+
* shared by both themes.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export type ThemeName = 'light' | 'dark';
|
|
11
|
+
|
|
12
|
+
interface StatusColor {
|
|
13
|
+
/** Soft tinted surface for pills and banners. */
|
|
14
|
+
surface: string;
|
|
15
|
+
/** Solid colour for icons, text and fills. */
|
|
16
|
+
accent: string;
|
|
17
|
+
border: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ColorTokens {
|
|
21
|
+
bg: { page: string; surface: string; surfaceMuted: string };
|
|
22
|
+
border: { default: string; strong: string; focus: string };
|
|
23
|
+
text: { primary: string; secondary: string; accent: string; inverse: string };
|
|
24
|
+
accent: { default: string; hover: string; subtle: string };
|
|
25
|
+
status: { info: StatusColor; success: StatusColor; warning: StatusColor; danger: StatusColor };
|
|
26
|
+
scrim: string;
|
|
27
|
+
shadow: { s: string; l: string };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const colors: Record<ThemeName, ColorTokens> = {
|
|
31
|
+
light: {
|
|
32
|
+
bg: { page: '#F7F7F8', surface: '#FFFFFF', surfaceMuted: '#EFEFF2' },
|
|
33
|
+
border: { default: '#DCDCE2', strong: '#B6B6C0', focus: '#4F46E5' },
|
|
34
|
+
text: { primary: '#16161A', secondary: '#5B5B66', accent: '#4338CA', inverse: '#FFFFFF' },
|
|
35
|
+
accent: { default: '#4F46E5', hover: '#4338CA', subtle: '#EEEEFD' },
|
|
36
|
+
status: {
|
|
37
|
+
info: { surface: '#E8F1FD', accent: '#1F6FD1', border: '#1F6FD1' },
|
|
38
|
+
success: { surface: '#E5F5EA', accent: '#1C8A45', border: '#1C8A45' },
|
|
39
|
+
warning: { surface: '#FDF1DE', accent: '#A8620C', border: '#A8620C' },
|
|
40
|
+
danger: { surface: '#FCE7E7', accent: '#C23434', border: '#C23434' },
|
|
41
|
+
},
|
|
42
|
+
scrim: 'rgba(22, 22, 26, 0.5)',
|
|
43
|
+
shadow: { s: '0 2px 6px rgba(22, 22, 26, 0.16)', l: '0 12px 32px rgba(22, 22, 26, 0.2)' },
|
|
44
|
+
},
|
|
45
|
+
dark: {
|
|
46
|
+
bg: { page: '#111114', surface: '#1B1B20', surfaceMuted: '#26262D' },
|
|
47
|
+
border: { default: '#34343D', strong: '#4A4A55', focus: '#8B85FF' },
|
|
48
|
+
text: { primary: '#F2F2F5', secondary: '#A6A6B2', accent: '#A5A0FF', inverse: '#111114' },
|
|
49
|
+
accent: { default: '#8B85FF', hover: '#A5A0FF', subtle: '#25234A' },
|
|
50
|
+
status: {
|
|
51
|
+
info: { surface: '#16273D', accent: '#6CA8F0', border: '#6CA8F0' },
|
|
52
|
+
success: { surface: '#15301F', accent: '#5FCB86', border: '#5FCB86' },
|
|
53
|
+
warning: { surface: '#36280F', accent: '#E8A64A', border: '#E8A64A' },
|
|
54
|
+
danger: { surface: '#3A1919', accent: '#F07676', border: '#F07676' },
|
|
55
|
+
},
|
|
56
|
+
scrim: 'rgba(0, 0, 0, 0.6)',
|
|
57
|
+
shadow: { s: '0 2px 6px rgba(0, 0, 0, 0.4)', l: '0 12px 32px rgba(0, 0, 0, 0.5)' },
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/** Spacing scale, smallest to largest. */
|
|
62
|
+
export const space = {
|
|
63
|
+
'3xs': '2px',
|
|
64
|
+
'2xs': '4px',
|
|
65
|
+
xs: '6px',
|
|
66
|
+
s: '8px',
|
|
67
|
+
m: '12px',
|
|
68
|
+
l: '16px',
|
|
69
|
+
xl: '20px',
|
|
70
|
+
'2xl': '24px',
|
|
71
|
+
'3xl': '32px',
|
|
72
|
+
'4xl': '40px',
|
|
73
|
+
} as const;
|
|
74
|
+
|
|
75
|
+
/** Corner radii. */
|
|
76
|
+
export const radius = {
|
|
77
|
+
xs: '4px',
|
|
78
|
+
s: '6px',
|
|
79
|
+
m: '10px',
|
|
80
|
+
l: '16px',
|
|
81
|
+
full: '9999px',
|
|
82
|
+
} as const;
|
|
83
|
+
|
|
84
|
+
export const fontFamily = {
|
|
85
|
+
sans: 'system-ui, -apple-system, "Segoe UI", Roboto, sans-serif',
|
|
86
|
+
mono: 'ui-monospace, SFMono-Regular, Menlo, Consolas, monospace',
|
|
87
|
+
} as const;
|
|
88
|
+
|
|
89
|
+
export interface TextStyle {
|
|
90
|
+
fontFamily: string;
|
|
91
|
+
fontSize: string;
|
|
92
|
+
fontWeight: number;
|
|
93
|
+
lineHeight: string;
|
|
94
|
+
letterSpacing: string;
|
|
95
|
+
/** Kept to what every supported `@starcier/collab-ui` accepts in `BuilderTextStyle`. */
|
|
96
|
+
textTransform: 'none' | 'uppercase';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const text = (
|
|
100
|
+
family: string,
|
|
101
|
+
fontWeight: number,
|
|
102
|
+
fontSize: string,
|
|
103
|
+
lineHeight: string,
|
|
104
|
+
textTransform: TextStyle['textTransform'] = 'none',
|
|
105
|
+
letterSpacing = '0px',
|
|
106
|
+
): TextStyle => ({ fontFamily: family, fontWeight, fontSize, lineHeight, letterSpacing, textTransform });
|
|
107
|
+
|
|
108
|
+
/** Text styles by role. */
|
|
109
|
+
export const typography = {
|
|
110
|
+
heading: text(fontFamily.sans, 600, '28px', '36px'),
|
|
111
|
+
title: text(fontFamily.sans, 600, '18px', '24px'),
|
|
112
|
+
body: text(fontFamily.sans, 400, '14px', '20px'),
|
|
113
|
+
label: text(fontFamily.sans, 500, '14px', '20px'),
|
|
114
|
+
caption: text(fontFamily.sans, 400, '12px', '16px'),
|
|
115
|
+
overline: text(fontFamily.sans, 600, '11px', '16px', 'uppercase', '0.06em'),
|
|
116
|
+
mono: text(fontFamily.mono, 500, '13px', '20px'),
|
|
117
|
+
} as const;
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Stacking order. The prototype builder's top bar sits at 200 and its
|
|
121
|
+
* comments and tours from 9140 up, so overlays live between the two: above the
|
|
122
|
+
* bar, below the collaboration layer.
|
|
123
|
+
*/
|
|
124
|
+
export const zIndex = {
|
|
125
|
+
modal: 1000,
|
|
126
|
+
} as const;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { StrictMode } from 'react';
|
|
2
|
+
import { createRoot } from 'react-dom/client';
|
|
3
|
+
import { BrowserRouter } from 'react-router';
|
|
4
|
+
import { App } from './App';
|
|
5
|
+
import './design-system/global.css';
|
|
6
|
+
|
|
7
|
+
createRoot(document.getElementById('root')!).render(
|
|
8
|
+
<StrictMode>
|
|
9
|
+
<BrowserRouter>
|
|
10
|
+
<App />
|
|
11
|
+
</BrowserRouter>
|
|
12
|
+
</StrictMode>,
|
|
13
|
+
);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Welcome
|
|
2
|
+
|
|
3
|
+
The example prototype: a small dashboard and a form that opens a dialog. It exists to show the
|
|
4
|
+
shape of a prototype folder; copy it for new prototypes, then delete it.
|
|
5
|
+
|
|
6
|
+
Workflows: **Welcome walkthrough** — the summary, the form (filled in for you) and its dialog.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { Button } from '../../../design-system/components/Button';
|
|
3
|
+
import { Card } from '../../../design-system/components/Card';
|
|
4
|
+
import { Field } from '../../../design-system/components/Field';
|
|
5
|
+
import { Modal } from '../../../design-system/components/Modal';
|
|
6
|
+
import { stats } from './data';
|
|
7
|
+
import './welcome.css';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The example prototype: a small dashboard and a form that opens a dialog.
|
|
11
|
+
* Copy its shape for new prototypes, then delete it.
|
|
12
|
+
*/
|
|
13
|
+
export function Welcome() {
|
|
14
|
+
const [name, setName] = useState('');
|
|
15
|
+
const [email, setEmail] = useState('');
|
|
16
|
+
const [confirming, setConfirming] = useState(false);
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<div className="welcome">
|
|
20
|
+
<header className="welcome__header" data-tour="welcome-header">
|
|
21
|
+
<span className="welcome__overline">Getting started</span>
|
|
22
|
+
<h1 className="welcome__title">Welcome</h1>
|
|
23
|
+
<p className="welcome__lead">An example prototype. Replace it with your own screens.</p>
|
|
24
|
+
</header>
|
|
25
|
+
|
|
26
|
+
<div className="welcome__stats" data-tour="welcome-stats">
|
|
27
|
+
{stats.map((stat) => (
|
|
28
|
+
<Card key={stat.id}>
|
|
29
|
+
<span className="welcome__stat-label">{stat.label}</span>
|
|
30
|
+
<span className="welcome__stat-value">{stat.value}</span>
|
|
31
|
+
<span className="welcome__stat-note">{stat.note}</span>
|
|
32
|
+
</Card>
|
|
33
|
+
))}
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<Card title="Request access" description="Fill in the form and review your request." data-tour="welcome-form">
|
|
37
|
+
<div className="welcome__form">
|
|
38
|
+
<Field label="Name" placeholder="Ana Example" value={name} onChange={(e) => setName(e.target.value)} />
|
|
39
|
+
<Field
|
|
40
|
+
label="Work email"
|
|
41
|
+
type="email"
|
|
42
|
+
placeholder="ana@example.com"
|
|
43
|
+
value={email}
|
|
44
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
45
|
+
/>
|
|
46
|
+
<div>
|
|
47
|
+
<Button data-tour="welcome-review" disabled={!name || !email} onClick={() => setConfirming(true)}>
|
|
48
|
+
Review request
|
|
49
|
+
</Button>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
</Card>
|
|
53
|
+
|
|
54
|
+
<Modal
|
|
55
|
+
open={confirming}
|
|
56
|
+
onClose={() => setConfirming(false)}
|
|
57
|
+
title="Send this request?"
|
|
58
|
+
data-tour="welcome-modal"
|
|
59
|
+
footer={
|
|
60
|
+
<>
|
|
61
|
+
<Button variant="secondary" onClick={() => setConfirming(false)}>
|
|
62
|
+
Cancel
|
|
63
|
+
</Button>
|
|
64
|
+
<Button onClick={() => setConfirming(false)}>Send request</Button>
|
|
65
|
+
</>
|
|
66
|
+
}
|
|
67
|
+
>
|
|
68
|
+
<p className="welcome__summary">
|
|
69
|
+
{name || 'Your name'} · {email || 'your email'}
|
|
70
|
+
</p>
|
|
71
|
+
</Modal>
|
|
72
|
+
</div>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Mock data for the Welcome prototype. Static and local: prototypes never call a backend. */
|
|
2
|
+
|
|
3
|
+
export interface Stat {
|
|
4
|
+
id: string;
|
|
5
|
+
label: string;
|
|
6
|
+
value: string;
|
|
7
|
+
note: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const stats: readonly Stat[] = [
|
|
11
|
+
{ id: 'projects', label: 'Active projects', value: '12', note: '3 started this month' },
|
|
12
|
+
{ id: 'reviews', label: 'Open reviews', value: '5', note: '2 waiting on you' },
|
|
13
|
+
{ id: 'shipped', label: 'Shipped this quarter', value: '27', note: 'Up from 19 last quarter' },
|
|
14
|
+
];
|