create-cogitato 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.
- package/LICENSE +202 -0
- package/README.md +12 -2
- package/bin/create-cogitato.js +4 -0
- package/dist/create.d.ts +26 -0
- package/dist/create.js +90 -0
- package/dist/create.js.map +1 -0
- package/package.json +38 -3
- package/template/.claude/settings.json +16 -0
- package/template/.env.example +2 -0
- package/template/CLAUDE.md +34 -0
- package/template/README.md +19 -0
- package/template/_gitignore +3 -0
- package/template/design/contract/components/banner.json +22 -0
- package/template/design/contract/components/button.json +27 -0
- package/template/design/contract/components/card.json +18 -0
- package/template/design/contract/components/input.json +27 -0
- package/template/design/contract/components/stack.json +16 -0
- package/template/design/contract/components/text.json +19 -0
- package/template/design/contract/contract.json +12 -0
- package/template/design/contract/decisions.json +41 -0
- package/template/design/contract/gaps.json +16 -0
- package/template/design/contract/rules.md +19 -0
- package/template/design/contract/tokens.json +126 -0
- package/template/design/flows.json +121 -0
- package/template/index.html +12 -0
- package/template/netlify.toml +28 -0
- package/template/package.json +30 -0
- package/template/pnpm-workspace.yaml +2 -0
- package/template/src/components/Page.module.css +5 -0
- package/template/src/components/Page.tsx +30 -0
- package/template/src/flows.ts +18 -0
- package/template/src/kit/Banner.module.css +48 -0
- package/template/src/kit/Banner.tsx +40 -0
- package/template/src/kit/Button.module.css +86 -0
- package/template/src/kit/Button.tsx +43 -0
- package/template/src/kit/Card.module.css +22 -0
- package/template/src/kit/Card.tsx +18 -0
- package/template/src/kit/Input.module.css +43 -0
- package/template/src/kit/Input.tsx +52 -0
- package/template/src/kit/Stack.tsx +30 -0
- package/template/src/kit/Text.module.css +32 -0
- package/template/src/kit/Text.tsx +19 -0
- package/template/src/main.tsx +11 -0
- package/template/src/mocks/cart.ts +43 -0
- package/template/src/screens/cart/screen.tsx +67 -0
- package/template/src/screens/checkout/confirmation/screen.tsx +34 -0
- package/template/src/screens/checkout/payment/screen.tsx +77 -0
- package/template/src/screens/checkout/shipping/screen.tsx +60 -0
- package/template/src/styles/global.css +12 -0
- package/template/tsconfig.json +17 -0
- package/template/vite.config.ts +7 -0
|
@@ -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,121 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../node_modules/@cogitato/contract/schema/flows.schema.json",
|
|
3
|
+
"schemaVersion": 1,
|
|
4
|
+
"screens": [
|
|
5
|
+
{
|
|
6
|
+
"id": "cart",
|
|
7
|
+
"route": "/cart",
|
|
8
|
+
"title": "Cart",
|
|
9
|
+
"states": [
|
|
10
|
+
{
|
|
11
|
+
"id": "default",
|
|
12
|
+
"fixture": "default",
|
|
13
|
+
"title": "Two items"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"id": "empty",
|
|
17
|
+
"fixture": "empty",
|
|
18
|
+
"title": "Empty cart"
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"id": "checkout.shipping",
|
|
24
|
+
"route": "/checkout/shipping",
|
|
25
|
+
"title": "Shipping",
|
|
26
|
+
"states": [
|
|
27
|
+
{
|
|
28
|
+
"id": "default",
|
|
29
|
+
"fixture": "default",
|
|
30
|
+
"title": "Empty form"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"id": "invalid-address",
|
|
34
|
+
"fixture": "invalid-address",
|
|
35
|
+
"title": "Address not found"
|
|
36
|
+
}
|
|
37
|
+
]
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"id": "checkout.payment",
|
|
41
|
+
"route": "/checkout/payment",
|
|
42
|
+
"title": "Payment",
|
|
43
|
+
"states": [
|
|
44
|
+
{
|
|
45
|
+
"id": "default",
|
|
46
|
+
"fixture": "default",
|
|
47
|
+
"title": "Empty form"
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"id": "processing",
|
|
51
|
+
"fixture": "processing",
|
|
52
|
+
"title": "Processing"
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"id": "card-declined",
|
|
56
|
+
"fixture": "card-declined",
|
|
57
|
+
"title": "Card declined"
|
|
58
|
+
}
|
|
59
|
+
]
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"id": "checkout.confirmation",
|
|
63
|
+
"route": "/checkout/confirmation",
|
|
64
|
+
"title": "Order confirmed",
|
|
65
|
+
"states": [
|
|
66
|
+
{
|
|
67
|
+
"id": "default",
|
|
68
|
+
"fixture": "default",
|
|
69
|
+
"title": "Confirmed"
|
|
70
|
+
}
|
|
71
|
+
]
|
|
72
|
+
}
|
|
73
|
+
],
|
|
74
|
+
"flows": [
|
|
75
|
+
{
|
|
76
|
+
"id": "checkout",
|
|
77
|
+
"title": "Checkout",
|
|
78
|
+
"nodes": [
|
|
79
|
+
"cart.default",
|
|
80
|
+
"checkout.shipping.default",
|
|
81
|
+
"checkout.shipping.invalid-address",
|
|
82
|
+
"checkout.payment.default",
|
|
83
|
+
"checkout.payment.processing",
|
|
84
|
+
"checkout.payment.card-declined",
|
|
85
|
+
"checkout.confirmation.default"
|
|
86
|
+
],
|
|
87
|
+
"edges": [
|
|
88
|
+
{
|
|
89
|
+
"from": "cart.default",
|
|
90
|
+
"to": "checkout.shipping.default",
|
|
91
|
+
"label": "Check out"
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"from": "checkout.shipping.default",
|
|
95
|
+
"to": "checkout.shipping.invalid-address",
|
|
96
|
+
"label": "Continue, address not found"
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"from": "checkout.shipping.default",
|
|
100
|
+
"to": "checkout.payment.default",
|
|
101
|
+
"label": "Continue"
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
"from": "checkout.payment.default",
|
|
105
|
+
"to": "checkout.payment.processing",
|
|
106
|
+
"label": "Pay now"
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
"from": "checkout.payment.processing",
|
|
110
|
+
"to": "checkout.payment.card-declined",
|
|
111
|
+
"label": "Declined"
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
"from": "checkout.payment.processing",
|
|
115
|
+
"to": "checkout.confirmation.default",
|
|
116
|
+
"label": "Approved"
|
|
117
|
+
}
|
|
118
|
+
]
|
|
119
|
+
}
|
|
120
|
+
]
|
|
121
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Acme Shop prototype</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div id="root"></div>
|
|
10
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Netlify config for a Cogitato prototype repo (ADR 013). Every branch push gets a preview whose
|
|
2
|
+
# canvas is at /__canvas. Snapshots need Chromium; if it can't run, the build still succeeds and
|
|
3
|
+
# the canvas shows placeholders.
|
|
4
|
+
|
|
5
|
+
[build]
|
|
6
|
+
command = "npx playwright install chromium && pnpm build"
|
|
7
|
+
publish = "dist"
|
|
8
|
+
|
|
9
|
+
[build.environment]
|
|
10
|
+
NODE_VERSION = "22"
|
|
11
|
+
|
|
12
|
+
# Screens are client-side routes: every path serves the app.
|
|
13
|
+
[[redirects]]
|
|
14
|
+
from = "/*"
|
|
15
|
+
to = "/index.html"
|
|
16
|
+
status = 200
|
|
17
|
+
|
|
18
|
+
# Previews may be framed only by their own canvas. Widen this when a team canvas on another
|
|
19
|
+
# site needs to show them (Phase 4).
|
|
20
|
+
[[headers]]
|
|
21
|
+
for = "/*"
|
|
22
|
+
[headers.values]
|
|
23
|
+
Content-Security-Policy = "frame-ancestors 'self'"
|
|
24
|
+
|
|
25
|
+
[[headers]]
|
|
26
|
+
for = "/__kit-data/*"
|
|
27
|
+
[headers.values]
|
|
28
|
+
Cache-Control = "public, max-age=0, must-revalidate"
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "my-prototype",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "kit export-flows && vite",
|
|
8
|
+
"build": "kit validate && kit export-flows && tsc --noEmit && vite build && kit snapshots",
|
|
9
|
+
"typecheck": "tsc --noEmit",
|
|
10
|
+
"validate": "kit validate",
|
|
11
|
+
"kit": "kit"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@cogitato/runtime": "^0.1.0",
|
|
15
|
+
"react": "^18.3.1",
|
|
16
|
+
"react-dom": "^18.3.1",
|
|
17
|
+
"react-router-dom": "^6.28.0"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@cogitato/contract": "^0.1.0",
|
|
21
|
+
"@cogitato/kit-cli": "^0.1.0",
|
|
22
|
+
"@cogitato/kit-mcp": "^0.1.0",
|
|
23
|
+
"@types/react": "^18.3.12",
|
|
24
|
+
"@types/react-dom": "^18.3.1",
|
|
25
|
+
"@vitejs/plugin-react": "^4.3.4",
|
|
26
|
+
"playwright": "^1.64.1",
|
|
27
|
+
"typescript": "^5.7.2",
|
|
28
|
+
"vite": "^6.0.7"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { Stack } from "@/kit/Stack";
|
|
3
|
+
import { Text } from "@/kit/Text";
|
|
4
|
+
import styles from "./Page.module.css";
|
|
5
|
+
|
|
6
|
+
const STEPS = ["Cart", "Shipping", "Payment"] as const;
|
|
7
|
+
|
|
8
|
+
/** Page frame shared by the checkout screens. `step` highlights the progress row. */
|
|
9
|
+
export function Page({ title, step, children }: { title: string; step?: (typeof STEPS)[number]; children: ReactNode }) {
|
|
10
|
+
return (
|
|
11
|
+
<main className={styles.page}>
|
|
12
|
+
<Stack gap="6">
|
|
13
|
+
{step && (
|
|
14
|
+
// Stand-in for the Stepper component until it exists (gap_stepper).
|
|
15
|
+
<Stack direction="row" gap="3" aria-label="Checkout progress">
|
|
16
|
+
{STEPS.map((s) => (
|
|
17
|
+
<Text key={s} as="span" variant="label" tone={s === step ? "default" : "muted"}>
|
|
18
|
+
{s}
|
|
19
|
+
</Text>
|
|
20
|
+
))}
|
|
21
|
+
</Stack>
|
|
22
|
+
)}
|
|
23
|
+
<Text as="h1" variant="heading">
|
|
24
|
+
{title}
|
|
25
|
+
</Text>
|
|
26
|
+
{children}
|
|
27
|
+
</Stack>
|
|
28
|
+
</main>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { defineFlows } from "@cogitato/runtime";
|
|
2
|
+
|
|
3
|
+
// How screens connect. Each step is "{screen id}.{state}". `kit export-flows` turns this into
|
|
4
|
+
// design/flows.json for the canvas; steps appear in the order they're first mentioned.
|
|
5
|
+
export default defineFlows([
|
|
6
|
+
{
|
|
7
|
+
id: "checkout",
|
|
8
|
+
title: "Checkout",
|
|
9
|
+
edges: [
|
|
10
|
+
{ from: "cart.default", to: "checkout.shipping.default", label: "Check out" },
|
|
11
|
+
{ from: "checkout.shipping.default", to: "checkout.shipping.invalid-address", label: "Continue, address not found" },
|
|
12
|
+
{ from: "checkout.shipping.default", to: "checkout.payment.default", label: "Continue" },
|
|
13
|
+
{ from: "checkout.payment.default", to: "checkout.payment.processing", label: "Pay now" },
|
|
14
|
+
{ from: "checkout.payment.processing", to: "checkout.payment.card-declined", label: "Declined" },
|
|
15
|
+
{ from: "checkout.payment.processing", to: "checkout.confirmation.default", label: "Approved" },
|
|
16
|
+
],
|
|
17
|
+
},
|
|
18
|
+
]);
|
|
@@ -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;
|