create-cogitato 0.0.0-stage → 0.2.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 (51) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +12 -2
  3. package/bin/create-cogitato.js +4 -0
  4. package/dist/create.d.ts +28 -0
  5. package/dist/create.js +100 -0
  6. package/dist/create.js.map +1 -0
  7. package/package.json +38 -3
  8. package/template/.claude/settings.json +16 -0
  9. package/template/CLAUDE.md +45 -0
  10. package/template/README.md +32 -0
  11. package/template/_gitignore +3 -0
  12. package/template/_pnpm-workspace.yaml +4 -0
  13. package/template/design/contract/components/banner.json +22 -0
  14. package/template/design/contract/components/button.json +27 -0
  15. package/template/design/contract/components/card.json +18 -0
  16. package/template/design/contract/components/input.json +27 -0
  17. package/template/design/contract/components/stack.json +16 -0
  18. package/template/design/contract/components/text.json +19 -0
  19. package/template/design/contract/contract.json +12 -0
  20. package/template/design/contract/decisions.json +41 -0
  21. package/template/design/contract/gaps.json +16 -0
  22. package/template/design/contract/rules.md +19 -0
  23. package/template/design/contract/tokens.json +126 -0
  24. package/template/kit/Banner.module.css +48 -0
  25. package/template/kit/Banner.tsx +40 -0
  26. package/template/kit/Button.module.css +86 -0
  27. package/template/kit/Button.tsx +43 -0
  28. package/template/kit/Card.module.css +22 -0
  29. package/template/kit/Card.tsx +18 -0
  30. package/template/kit/Input.module.css +43 -0
  31. package/template/kit/Input.tsx +52 -0
  32. package/template/kit/Stack.tsx +30 -0
  33. package/template/kit/Text.module.css +32 -0
  34. package/template/kit/Text.tsx +19 -0
  35. package/template/package.json +25 -0
  36. package/template/prototypes/checkout/.env.example +2 -0
  37. package/template/prototypes/checkout/design/flows.json +121 -0
  38. package/template/prototypes/checkout/index.html +12 -0
  39. package/template/prototypes/checkout/package.json +28 -0
  40. package/template/prototypes/checkout/src/components/Page.module.css +5 -0
  41. package/template/prototypes/checkout/src/components/Page.tsx +30 -0
  42. package/template/prototypes/checkout/src/flows.ts +18 -0
  43. package/template/prototypes/checkout/src/main.tsx +11 -0
  44. package/template/prototypes/checkout/src/mocks/cart.ts +43 -0
  45. package/template/prototypes/checkout/src/screens/cart/screen.tsx +67 -0
  46. package/template/prototypes/checkout/src/screens/checkout/confirmation/screen.tsx +34 -0
  47. package/template/prototypes/checkout/src/screens/checkout/payment/screen.tsx +77 -0
  48. package/template/prototypes/checkout/src/screens/checkout/shipping/screen.tsx +60 -0
  49. package/template/prototypes/checkout/src/styles/global.css +12 -0
  50. package/template/prototypes/checkout/tsconfig.json +17 -0
  51. package/template/prototypes/checkout/vite.config.ts +7 -0
@@ -0,0 +1,19 @@
1
+ {
2
+ "$schema": "../../../node_modules/@cogitato/contract/schema/component.schema.json",
3
+ "name": "Text",
4
+ "description": "All text. Picks typography and color from tokens.",
5
+ "source": "production",
6
+ "confidence": "high",
7
+ "import": "@/kit/Text",
8
+ "props": {
9
+ "variant": { "type": "enum", "values": ["body", "label", "heading"], "default": "body" },
10
+ "tone": { "type": "enum", "values": ["default", "muted", "danger", "inverse"], "default": "default" },
11
+ "as": { "type": "enum", "values": ["p", "span", "h1", "h2", "h3", "label"], "default": "p" },
12
+ "children": { "type": "node", "required": true }
13
+ },
14
+ "states": ["default"],
15
+ "rules": ["One h1 per screen."],
16
+ "mapping": {
17
+ "production": { "path": "src/ui/Text.tsx", "export": "Text" }
18
+ }
19
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "$schema": "../../node_modules/@cogitato/contract/schema/contract.schema.json",
3
+ "schemaVersion": 1,
4
+ "name": "Acme Shop",
5
+ "version": "0.1.0",
6
+ "description": "Reference contract for an invented e-commerce product. Hand-written to exercise every Cogitato format before intake exists.",
7
+ "sources": [
8
+ { "kind": "manual", "ref": "Hand-written reference contract (ADR 003)" },
9
+ { "kind": "code", "ref": "acme/shop-web: src/ui", "importedAt": "2026-11-01T00:00:00Z" },
10
+ { "kind": "figma", "ref": "Acme Shop Library", "importedAt": "2026-11-01T00:00:00Z" }
11
+ ]
12
+ }
@@ -0,0 +1,41 @@
1
+ {
2
+ "$schema": "../../node_modules/@cogitato/contract/schema/decisions.schema.json",
3
+ "decisions": [
4
+ {
5
+ "id": "dec_brand-blue-600",
6
+ "topic": "tokens.color.brand.600",
7
+ "question": "Code and Figma use slightly different values for the main brand blue. Which is canonical?",
8
+ "options": [
9
+ {
10
+ "id": "code",
11
+ "label": "Keep the production value",
12
+ "value": "#2563eb",
13
+ "evidence": [{ "source": "code", "detail": "src/ui/theme.ts: brand600, used in 214 places" }]
14
+ },
15
+ {
16
+ "id": "figma",
17
+ "label": "Use the Figma value",
18
+ "value": "#2462e8",
19
+ "evidence": [{ "source": "figma", "detail": "Variable Brand/600 in Acme Shop Library" }]
20
+ }
21
+ ],
22
+ "recommendation": "code",
23
+ "status": "decided",
24
+ "choice": "code",
25
+ "decidedBy": "dev_alex",
26
+ "decidedAt": "2026-11-01T00:00:00Z",
27
+ "note": "Figma variable to be updated to match."
28
+ },
29
+ {
30
+ "id": "dec_input-name",
31
+ "topic": "components.Input",
32
+ "question": "Production calls the text field TextInput; Figma calls it Input. Which name do designers see?",
33
+ "options": [
34
+ { "id": "input", "label": "Input (Figma name)" },
35
+ { "id": "text-input", "label": "TextInput (production name)" }
36
+ ],
37
+ "recommendation": "input",
38
+ "status": "open"
39
+ }
40
+ ]
41
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "$schema": "../../node_modules/@cogitato/contract/schema/gaps.schema.json",
3
+ "gaps": [
4
+ {
5
+ "id": "gap_stepper",
6
+ "kind": "component",
7
+ "name": "Stepper",
8
+ "context": "Show checkout progress (Cart, Shipping, Payment) at the top of each checkout screen.",
9
+ "screen": "checkout.shipping",
10
+ "workaround": "A row of Text labels in a Stack.",
11
+ "requestedBy": "designer_sam",
12
+ "status": "open",
13
+ "createdAt": "2026-11-01T00:00:00Z"
14
+ }
15
+ ]
16
+ }
@@ -0,0 +1,19 @@
1
+ # Acme Shop design rules
2
+
3
+ These apply to every prototype screen. The validator enforces the ones marked **(checked)**.
4
+
5
+ ## Do
6
+ - Use tokens for every color, size, space, radius and shadow. **(checked)**
7
+ - Build with kit components from `@/kit/...`. **(checked)**
8
+ - Give every interactive element a `data-kit-id`. **(checked)**
9
+ - Lay out with `Stack` and its `gap`, not custom margins.
10
+ - Keep one primary `Button` per view.
11
+ - Write button labels as verbs ("Pay now").
12
+
13
+ ## Don't
14
+ - Hardcode values like `#2563eb` or `16px`. **(checked)**
15
+ - Nest a `Card` inside another `Card`.
16
+ - Use `danger` styles for anything that isn't destructive or an error.
17
+
18
+ ## Missing something?
19
+ If the kit doesn't have what you need, log a gap instead of improvising. Developers review gaps before the next contract update.
@@ -0,0 +1,126 @@
1
+ {
2
+ "$schema": "../../node_modules/@cogitato/contract/schema/tokens.schema.json",
3
+ "color": {
4
+ "$type": "color",
5
+ "brand": {
6
+ "50": { "$value": "#eff6ff", "$description": "Brand Blue 50" },
7
+ "100": { "$value": "#dbeafe", "$description": "Brand Blue 100" },
8
+ "500": { "$value": "#3b82f6", "$description": "Brand Blue 500" },
9
+ "600": { "$value": "#2563eb", "$description": "Brand Blue 600" },
10
+ "700": { "$value": "#1d4ed8", "$description": "Brand Blue 700" }
11
+ },
12
+ "neutral": {
13
+ "0": { "$value": "#ffffff", "$description": "White" },
14
+ "50": { "$value": "#f8fafc", "$description": "Neutral 50" },
15
+ "100": { "$value": "#f1f5f9", "$description": "Neutral 100" },
16
+ "200": { "$value": "#e2e8f0", "$description": "Neutral 200" },
17
+ "500": { "$value": "#64748b", "$description": "Neutral 500" },
18
+ "700": { "$value": "#334155", "$description": "Neutral 700" },
19
+ "900": { "$value": "#0f172a", "$description": "Neutral 900" }
20
+ },
21
+ "feedback": {
22
+ "success": { "$value": "#15803d", "$description": "Success green" },
23
+ "warning": { "$value": "#b45309", "$description": "Warning amber" },
24
+ "danger": { "$value": "#b91c1c", "$description": "Danger red" },
25
+ "danger-subtle": { "$value": "#fef2f2", "$description": "Danger background" },
26
+ "info": { "$value": "{color.brand.600}", "$description": "Info uses brand blue" }
27
+ },
28
+ "text": {
29
+ "default": { "$value": "{color.neutral.900}" },
30
+ "muted": { "$value": "{color.neutral.500}" },
31
+ "inverse": { "$value": "{color.neutral.0}" },
32
+ "danger": { "$value": "{color.feedback.danger}" }
33
+ },
34
+ "surface": {
35
+ "default": { "$value": "{color.neutral.0}" },
36
+ "subtle": { "$value": "{color.neutral.50}" }
37
+ },
38
+ "border": {
39
+ "default": { "$value": "{color.neutral.200}" },
40
+ "focus": { "$value": "{color.brand.500}" }
41
+ }
42
+ },
43
+ "space": {
44
+ "$type": "dimension",
45
+ "$description": "Spacing scale. Use for padding, margins and gaps.",
46
+ "0": { "$value": { "value": 0, "unit": "px" } },
47
+ "1": { "$value": { "value": 4, "unit": "px" } },
48
+ "2": { "$value": { "value": 8, "unit": "px" } },
49
+ "3": { "$value": { "value": 12, "unit": "px" } },
50
+ "4": { "$value": { "value": 16, "unit": "px" } },
51
+ "6": { "$value": { "value": 24, "unit": "px" } },
52
+ "8": { "$value": { "value": 32, "unit": "px" } }
53
+ },
54
+ "radius": {
55
+ "$type": "dimension",
56
+ "sm": { "$value": { "value": 4, "unit": "px" } },
57
+ "md": { "$value": { "value": 8, "unit": "px" } },
58
+ "lg": { "$value": { "value": 12, "unit": "px" } },
59
+ "full": { "$value": { "value": 9999, "unit": "px" } }
60
+ },
61
+ "font": {
62
+ "family": {
63
+ "$type": "fontFamily",
64
+ "sans": { "$value": ["Inter", "system-ui", "sans-serif"] }
65
+ },
66
+ "weight": {
67
+ "$type": "fontWeight",
68
+ "regular": { "$value": 400 },
69
+ "medium": { "$value": 500 },
70
+ "semibold": { "$value": 600 },
71
+ "bold": { "$value": 700 }
72
+ },
73
+ "size": {
74
+ "$type": "dimension",
75
+ "sm": { "$value": { "value": 14, "unit": "px" } },
76
+ "md": { "$value": { "value": 16, "unit": "px" } },
77
+ "lg": { "$value": { "value": 20, "unit": "px" } },
78
+ "xl": { "$value": { "value": 28, "unit": "px" } }
79
+ }
80
+ },
81
+ "typography": {
82
+ "$type": "typography",
83
+ "body": {
84
+ "$value": {
85
+ "fontFamily": "{font.family.sans}",
86
+ "fontSize": "{font.size.md}",
87
+ "fontWeight": "{font.weight.regular}",
88
+ "lineHeight": 1.5
89
+ }
90
+ },
91
+ "label": {
92
+ "$value": {
93
+ "fontFamily": "{font.family.sans}",
94
+ "fontSize": "{font.size.sm}",
95
+ "fontWeight": "{font.weight.medium}",
96
+ "lineHeight": 1.4
97
+ }
98
+ },
99
+ "heading": {
100
+ "$value": {
101
+ "fontFamily": "{font.family.sans}",
102
+ "fontSize": "{font.size.xl}",
103
+ "fontWeight": "{font.weight.bold}",
104
+ "lineHeight": 1.2,
105
+ "letterSpacing": { "value": -0.02, "unit": "rem" }
106
+ }
107
+ }
108
+ },
109
+ "shadow": {
110
+ "$type": "shadow",
111
+ "card": {
112
+ "$value": {
113
+ "color": "#0f172a1a",
114
+ "offsetX": { "value": 0, "unit": "px" },
115
+ "offsetY": { "value": 1, "unit": "px" },
116
+ "blur": { "value": 3, "unit": "px" },
117
+ "spread": { "value": 0, "unit": "px" }
118
+ }
119
+ }
120
+ },
121
+ "duration": {
122
+ "$type": "duration",
123
+ "fast": { "$value": { "value": 120, "unit": "ms" } },
124
+ "normal": { "$value": { "value": 200, "unit": "ms" } }
125
+ }
126
+ }
@@ -0,0 +1,48 @@
1
+ .banner {
2
+ display: flex;
3
+ align-items: flex-start;
4
+ gap: var(--kit-space-3);
5
+ padding: var(--kit-space-3) var(--kit-space-4);
6
+ border-radius: var(--kit-radius-md);
7
+ border-left: 4px solid currentColor;
8
+ }
9
+
10
+ .body {
11
+ flex: 1;
12
+ display: flex;
13
+ flex-direction: column;
14
+ gap: var(--kit-space-1);
15
+ color: var(--kit-color-text-default);
16
+ }
17
+
18
+ .title {
19
+ font: var(--kit-typography-label);
20
+ }
21
+
22
+ .info {
23
+ color: var(--kit-color-feedback-info);
24
+ background: var(--kit-color-brand-50);
25
+ }
26
+
27
+ .success {
28
+ color: var(--kit-color-feedback-success);
29
+ background: var(--kit-color-surface-subtle);
30
+ }
31
+
32
+ .warning {
33
+ color: var(--kit-color-feedback-warning);
34
+ background: var(--kit-color-surface-subtle);
35
+ }
36
+
37
+ .danger {
38
+ color: var(--kit-color-feedback-danger);
39
+ background: var(--kit-color-feedback-danger-subtle);
40
+ }
41
+
42
+ .dismiss {
43
+ border: 0;
44
+ background: transparent;
45
+ color: var(--kit-color-text-muted);
46
+ font-size: var(--kit-font-size-lg);
47
+ cursor: pointer;
48
+ }
@@ -0,0 +1,40 @@
1
+ import type { HTMLAttributes, ReactNode } from "react";
2
+ import styles from "./Banner.module.css";
3
+
4
+ export interface BannerProps extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
5
+ tone?: "info" | "success" | "warning" | "danger";
6
+ title?: string;
7
+ children: ReactNode;
8
+ /** Shows a close button when set. */
9
+ onDismiss?: () => void;
10
+ "data-kit-id"?: string;
11
+ }
12
+
13
+ export function Banner({ tone = "info", title, children, onDismiss, className, ...rest }: BannerProps) {
14
+ const kitId = rest["data-kit-id"];
15
+ return (
16
+ <div
17
+ {...rest}
18
+ role={tone === "danger" || tone === "warning" ? "alert" : "status"}
19
+ className={[styles.banner, styles[tone], className].filter(Boolean).join(" ")}
20
+ >
21
+ <div className={styles.body}>
22
+ {title && <strong className={styles.title}>{title}</strong>}
23
+ <div>{children}</div>
24
+ </div>
25
+ {onDismiss && (
26
+ <button
27
+ type="button"
28
+ className={styles.dismiss}
29
+ aria-label="Dismiss"
30
+ data-kit-id={kitId ? `${kitId}.dismiss` : undefined}
31
+ onClick={onDismiss}
32
+ >
33
+ ×
34
+ </button>
35
+ )}
36
+ </div>
37
+ );
38
+ }
39
+
40
+ export default Banner;
@@ -0,0 +1,86 @@
1
+ .button {
2
+ display: inline-flex;
3
+ align-items: center;
4
+ justify-content: center;
5
+ gap: var(--kit-space-2);
6
+ border: 1px solid transparent;
7
+ border-radius: var(--kit-radius-md);
8
+ font: var(--kit-typography-label);
9
+ cursor: pointer;
10
+ transition: background-color var(--kit-duration-fast), border-color var(--kit-duration-fast);
11
+ }
12
+
13
+ .button:focus-visible {
14
+ outline: 2px solid var(--kit-color-border-focus);
15
+ outline-offset: 2px;
16
+ }
17
+
18
+ .button:disabled {
19
+ cursor: not-allowed;
20
+ opacity: 0.55;
21
+ }
22
+
23
+ .sm {
24
+ padding: var(--kit-space-1) var(--kit-space-3);
25
+ }
26
+
27
+ .md {
28
+ padding: var(--kit-space-2) var(--kit-space-4);
29
+ }
30
+
31
+ .lg {
32
+ padding: var(--kit-space-3) var(--kit-space-6);
33
+ font-size: var(--kit-font-size-md);
34
+ }
35
+
36
+ .full {
37
+ width: 100%;
38
+ }
39
+
40
+ .primary {
41
+ background: var(--kit-color-brand-600);
42
+ color: var(--kit-color-text-inverse);
43
+ }
44
+
45
+ .primary:hover:not(:disabled) {
46
+ background: var(--kit-color-brand-700);
47
+ }
48
+
49
+ .secondary {
50
+ background: var(--kit-color-surface-default);
51
+ border-color: var(--kit-color-border-default);
52
+ color: var(--kit-color-text-default);
53
+ }
54
+
55
+ .secondary:hover:not(:disabled) {
56
+ background: var(--kit-color-neutral-100);
57
+ }
58
+
59
+ .ghost {
60
+ background: transparent;
61
+ color: var(--kit-color-brand-600);
62
+ }
63
+
64
+ .ghost:hover:not(:disabled) {
65
+ background: var(--kit-color-brand-50);
66
+ }
67
+
68
+ .danger {
69
+ background: var(--kit-color-feedback-danger);
70
+ color: var(--kit-color-text-inverse);
71
+ }
72
+
73
+ .spinner {
74
+ width: 1em;
75
+ height: 1em;
76
+ border: 2px solid currentColor;
77
+ border-right-color: transparent;
78
+ border-radius: var(--kit-radius-full);
79
+ animation: spin 0.8s linear infinite;
80
+ }
81
+
82
+ @keyframes spin {
83
+ to {
84
+ transform: rotate(360deg);
85
+ }
86
+ }
@@ -0,0 +1,43 @@
1
+ import type { ButtonHTMLAttributes, ReactNode } from "react";
2
+ import styles from "./Button.module.css";
3
+
4
+ export interface ButtonProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, "onClick" | "children"> {
5
+ variant?: "primary" | "secondary" | "ghost" | "danger";
6
+ size?: "sm" | "md" | "lg";
7
+ disabled?: boolean;
8
+ /** Shows a spinner and blocks presses. */
9
+ loading?: boolean;
10
+ fullWidth?: boolean;
11
+ /** Button label. */
12
+ children: ReactNode;
13
+ onPress?: () => void;
14
+ }
15
+
16
+ export function Button({
17
+ variant = "primary",
18
+ size = "md",
19
+ disabled = false,
20
+ loading = false,
21
+ fullWidth = false,
22
+ children,
23
+ onPress,
24
+ className,
25
+ ...rest
26
+ }: ButtonProps) {
27
+ const classes = [styles.button, styles[variant], styles[size], fullWidth && styles.full, className];
28
+ return (
29
+ <button
30
+ type="button"
31
+ {...rest}
32
+ className={classes.filter(Boolean).join(" ")}
33
+ disabled={disabled || loading}
34
+ aria-busy={loading || undefined}
35
+ onClick={onPress}
36
+ >
37
+ {loading && <span className={styles.spinner} aria-hidden="true" />}
38
+ {children}
39
+ </button>
40
+ );
41
+ }
42
+
43
+ export default Button;
@@ -0,0 +1,22 @@
1
+ .card {
2
+ background: var(--kit-color-surface-default);
3
+ border: 1px solid var(--kit-color-border-default);
4
+ border-radius: var(--kit-radius-lg);
5
+ }
6
+
7
+ .sm {
8
+ padding: var(--kit-space-3);
9
+ }
10
+
11
+ .md {
12
+ padding: var(--kit-space-4);
13
+ }
14
+
15
+ .lg {
16
+ padding: var(--kit-space-6);
17
+ }
18
+
19
+ .raised {
20
+ border-color: transparent;
21
+ box-shadow: var(--kit-shadow-card);
22
+ }
@@ -0,0 +1,18 @@
1
+ import type { HTMLAttributes, ReactNode } from "react";
2
+ import styles from "./Card.module.css";
3
+
4
+ export interface CardProps extends HTMLAttributes<HTMLDivElement> {
5
+ padding?: "sm" | "md" | "lg";
6
+ elevation?: "flat" | "raised";
7
+ children: ReactNode;
8
+ }
9
+
10
+ export function Card({ padding = "md", elevation = "flat", children, className, ...rest }: CardProps) {
11
+ return (
12
+ <div {...rest} className={[styles.card, styles[padding], styles[elevation], className].filter(Boolean).join(" ")}>
13
+ {children}
14
+ </div>
15
+ );
16
+ }
17
+
18
+ export default Card;
@@ -0,0 +1,43 @@
1
+ .field {
2
+ display: flex;
3
+ flex-direction: column;
4
+ gap: var(--kit-space-1);
5
+ }
6
+
7
+ .label {
8
+ font: var(--kit-typography-label);
9
+ color: var(--kit-color-text-default);
10
+ }
11
+
12
+ .input {
13
+ font: var(--kit-typography-body);
14
+ padding: var(--kit-space-2) var(--kit-space-3);
15
+ border: 1px solid var(--kit-color-border-default);
16
+ border-radius: var(--kit-radius-md);
17
+ background: var(--kit-color-surface-default);
18
+ color: var(--kit-color-text-default);
19
+ }
20
+
21
+ .input:focus {
22
+ outline: 2px solid var(--kit-color-border-focus);
23
+ outline-offset: -1px;
24
+ }
25
+
26
+ .input:disabled {
27
+ background: var(--kit-color-neutral-100);
28
+ color: var(--kit-color-text-muted);
29
+ }
30
+
31
+ .invalid {
32
+ border-color: var(--kit-color-feedback-danger);
33
+ }
34
+
35
+ .hint {
36
+ font-size: var(--kit-font-size-sm);
37
+ color: var(--kit-color-text-muted);
38
+ }
39
+
40
+ .error {
41
+ font-size: var(--kit-font-size-sm);
42
+ color: var(--kit-color-text-danger);
43
+ }
@@ -0,0 +1,52 @@
1
+ import { useId, type InputHTMLAttributes } from "react";
2
+ import styles from "./Input.module.css";
3
+
4
+ export interface InputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "onChange" | "value" | "type"> {
5
+ label: string;
6
+ type?: "text" | "email" | "password" | "number" | "tel";
7
+ /** Starting value. The field stays editable, so prototypes can be typed into. */
8
+ value?: string;
9
+ placeholder?: string;
10
+ /** Help text shown under the field. */
11
+ hint?: string;
12
+ /** Error text. Setting it shows the error state. */
13
+ error?: string;
14
+ disabled?: boolean;
15
+ onChange?: (value: string) => void;
16
+ }
17
+
18
+ export function Input({ label, type = "text", value, hint, error, disabled = false, onChange, className, ...rest }: InputProps) {
19
+ const id = useId();
20
+ const describedBy = error ? `${id}-error` : hint ? `${id}-hint` : undefined;
21
+ return (
22
+ <div className={[styles.field, className].filter(Boolean).join(" ")}>
23
+ <label htmlFor={id} className={styles.label}>
24
+ {label}
25
+ </label>
26
+ <input
27
+ {...rest}
28
+ id={id}
29
+ type={type}
30
+ defaultValue={value}
31
+ disabled={disabled}
32
+ aria-invalid={error ? true : undefined}
33
+ aria-describedby={describedBy}
34
+ className={[styles.input, error && styles.invalid].filter(Boolean).join(" ")}
35
+ onChange={(e) => onChange?.(e.target.value)}
36
+ />
37
+ {error ? (
38
+ <span id={`${id}-error`} className={styles.error}>
39
+ {error}
40
+ </span>
41
+ ) : (
42
+ hint && (
43
+ <span id={`${id}-hint`} className={styles.hint}>
44
+ {hint}
45
+ </span>
46
+ )
47
+ )}
48
+ </div>
49
+ );
50
+ }
51
+
52
+ export default Input;
@@ -0,0 +1,30 @@
1
+ import type { HTMLAttributes, ReactNode } from "react";
2
+
3
+ export interface StackProps extends HTMLAttributes<HTMLDivElement> {
4
+ direction?: "row" | "column";
5
+ /** Step on the space scale. */
6
+ gap?: "0" | "1" | "2" | "3" | "4" | "6" | "8";
7
+ align?: "start" | "center" | "end" | "stretch";
8
+ children: ReactNode;
9
+ }
10
+
11
+ const ALIGN = { start: "flex-start", center: "center", end: "flex-end", stretch: "stretch" } as const;
12
+
13
+ export function Stack({ direction = "column", gap = "4", align = "stretch", children, style, ...rest }: StackProps) {
14
+ return (
15
+ <div
16
+ {...rest}
17
+ style={{
18
+ display: "flex",
19
+ flexDirection: direction,
20
+ alignItems: ALIGN[align],
21
+ gap: `var(--kit-space-${gap})`,
22
+ ...style,
23
+ }}
24
+ >
25
+ {children}
26
+ </div>
27
+ );
28
+ }
29
+
30
+ export default Stack;
@@ -0,0 +1,32 @@
1
+ .text {
2
+ margin: 0;
3
+ }
4
+
5
+ .body {
6
+ font: var(--kit-typography-body);
7
+ }
8
+
9
+ .label {
10
+ font: var(--kit-typography-label);
11
+ }
12
+
13
+ .heading {
14
+ font: var(--kit-typography-heading);
15
+ letter-spacing: var(--kit-typography-heading-letter-spacing);
16
+ }
17
+
18
+ .default {
19
+ color: var(--kit-color-text-default);
20
+ }
21
+
22
+ .muted {
23
+ color: var(--kit-color-text-muted);
24
+ }
25
+
26
+ .danger {
27
+ color: var(--kit-color-text-danger);
28
+ }
29
+
30
+ .inverse {
31
+ color: var(--kit-color-text-inverse);
32
+ }
@@ -0,0 +1,19 @@
1
+ import type { HTMLAttributes, ReactNode } from "react";
2
+ import styles from "./Text.module.css";
3
+
4
+ export interface TextProps extends HTMLAttributes<HTMLElement> {
5
+ variant?: "body" | "label" | "heading";
6
+ tone?: "default" | "muted" | "danger" | "inverse";
7
+ as?: "p" | "span" | "h1" | "h2" | "h3" | "label";
8
+ children: ReactNode;
9
+ }
10
+
11
+ export function Text({ variant = "body", tone = "default", as: Tag = "p", children, className, ...rest }: TextProps) {
12
+ return (
13
+ <Tag {...rest} className={[styles.text, styles[variant], styles[tone], className].filter(Boolean).join(" ")}>
14
+ {children}
15
+ </Tag>
16
+ );
17
+ }
18
+
19
+ export default Text;