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.
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 +26 -0
  5. package/dist/create.js +90 -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/.env.example +2 -0
  10. package/template/CLAUDE.md +34 -0
  11. package/template/README.md +19 -0
  12. package/template/_gitignore +3 -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/design/flows.json +121 -0
  25. package/template/index.html +12 -0
  26. package/template/netlify.toml +28 -0
  27. package/template/package.json +30 -0
  28. package/template/pnpm-workspace.yaml +2 -0
  29. package/template/src/components/Page.module.css +5 -0
  30. package/template/src/components/Page.tsx +30 -0
  31. package/template/src/flows.ts +18 -0
  32. package/template/src/kit/Banner.module.css +48 -0
  33. package/template/src/kit/Banner.tsx +40 -0
  34. package/template/src/kit/Button.module.css +86 -0
  35. package/template/src/kit/Button.tsx +43 -0
  36. package/template/src/kit/Card.module.css +22 -0
  37. package/template/src/kit/Card.tsx +18 -0
  38. package/template/src/kit/Input.module.css +43 -0
  39. package/template/src/kit/Input.tsx +52 -0
  40. package/template/src/kit/Stack.tsx +30 -0
  41. package/template/src/kit/Text.module.css +32 -0
  42. package/template/src/kit/Text.tsx +19 -0
  43. package/template/src/main.tsx +11 -0
  44. package/template/src/mocks/cart.ts +43 -0
  45. package/template/src/screens/cart/screen.tsx +67 -0
  46. package/template/src/screens/checkout/confirmation/screen.tsx +34 -0
  47. package/template/src/screens/checkout/payment/screen.tsx +77 -0
  48. package/template/src/screens/checkout/shipping/screen.tsx +60 -0
  49. package/template/src/styles/global.css +12 -0
  50. package/template/tsconfig.json +17 -0
  51. package/template/vite.config.ts +7 -0
@@ -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;
@@ -0,0 +1,11 @@
1
+ import "virtual:kit/tokens.css";
2
+ import "./styles/global.css";
3
+ import kit from "virtual:kit/kit";
4
+ import { mountPrototype, type ScreenDefinition } from "@cogitato/runtime";
5
+
6
+ // Every src/screens/**/screen.tsx registers one screen with defineScreen.
7
+ const screens = Object.values(
8
+ import.meta.glob<ScreenDefinition>("./screens/**/screen.tsx", { eager: true, import: "default" }),
9
+ );
10
+
11
+ mountPrototype(document.getElementById("root"), { screens, kit });
@@ -0,0 +1,43 @@
1
+ // Mock data for the checkout flow. Prototypes have no backend; fixtures pick from these.
2
+
3
+ export interface CartItem {
4
+ id: string;
5
+ name: string;
6
+ variant: string;
7
+ quantity: number;
8
+ price: number;
9
+ }
10
+
11
+ export interface Cart {
12
+ items: CartItem[];
13
+ shipping: number;
14
+ }
15
+
16
+ export const sampleCart: Cart = {
17
+ items: [
18
+ { id: "tote", name: "Canvas tote", variant: "Natural", quantity: 1, price: 24 },
19
+ { id: "mug", name: "Ceramic mug", variant: "Speckled white", quantity: 2, price: 18 },
20
+ ],
21
+ shipping: 5,
22
+ };
23
+
24
+ export const emptyCart: Cart = { items: [], shipping: 0 };
25
+
26
+ export const subtotal = (cart: Cart) => cart.items.reduce((sum, i) => sum + i.price * i.quantity, 0);
27
+
28
+ export const money = (amount: number) =>
29
+ new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" }).format(amount);
30
+
31
+ export interface Address {
32
+ name: string;
33
+ street: string;
34
+ city: string;
35
+ postcode: string;
36
+ }
37
+
38
+ export const sampleAddress: Address = {
39
+ name: "Sam Rivera",
40
+ street: "12 Harbour Lane",
41
+ city: "Portland",
42
+ postcode: "97201",
43
+ };
@@ -0,0 +1,67 @@
1
+ import { defineScreen, useFixture, useGoToState } from "@cogitato/runtime";
2
+ import { Page } from "@/components/Page";
3
+ import { Button } from "@/kit/Button";
4
+ import { Card } from "@/kit/Card";
5
+ import { Stack } from "@/kit/Stack";
6
+ import { Text } from "@/kit/Text";
7
+ import { emptyCart, money, sampleCart, subtotal, type Cart } from "@/mocks/cart";
8
+
9
+ function CartScreen() {
10
+ const { cart } = useFixture<{ cart: Cart }>();
11
+ const goTo = useGoToState();
12
+
13
+ if (cart.items.length === 0) {
14
+ return (
15
+ <Page title="Your cart" step="Cart">
16
+ <Card padding="lg">
17
+ <Stack gap="3" align="start">
18
+ <Text>Your cart is empty.</Text>
19
+ <Button variant="secondary" data-kit-id="cart.keep-shopping">
20
+ Keep shopping
21
+ </Button>
22
+ </Stack>
23
+ </Card>
24
+ </Page>
25
+ );
26
+ }
27
+
28
+ return (
29
+ <Page title="Your cart" step="Cart">
30
+ <Card>
31
+ <Stack gap="4">
32
+ {cart.items.map((item) => (
33
+ <Stack key={item.id} direction="row" gap="4">
34
+ <Stack gap="1" style={{ flex: 1 }}>
35
+ <Text variant="label">{item.name}</Text>
36
+ <Text tone="muted">
37
+ {item.variant} · Qty {item.quantity}
38
+ </Text>
39
+ </Stack>
40
+ <Text>{money(item.price * item.quantity)}</Text>
41
+ </Stack>
42
+ ))}
43
+ </Stack>
44
+ </Card>
45
+ <Stack direction="row" gap="4">
46
+ <Text variant="label" style={{ flex: 1 }}>
47
+ Subtotal
48
+ </Text>
49
+ <Text variant="label">{money(subtotal(cart))}</Text>
50
+ </Stack>
51
+ <Button fullWidth size="lg" data-kit-id="cart.checkout" onPress={() => goTo("checkout.shipping.default")}>
52
+ Check out
53
+ </Button>
54
+ </Page>
55
+ );
56
+ }
57
+
58
+ export default defineScreen({
59
+ id: "cart",
60
+ route: "/cart",
61
+ title: "Cart",
62
+ fixtures: {
63
+ default: { title: "Two items", data: { cart: sampleCart } },
64
+ empty: { title: "Empty cart", data: { cart: emptyCart } },
65
+ },
66
+ component: CartScreen,
67
+ });
@@ -0,0 +1,34 @@
1
+ import { defineScreen, useFixture, useGoToState } from "@cogitato/runtime";
2
+ import { Page } from "@/components/Page";
3
+ import { Button } from "@/kit/Button";
4
+ import { Card } from "@/kit/Card";
5
+ import { Stack } from "@/kit/Stack";
6
+ import { Text } from "@/kit/Text";
7
+
8
+ function ConfirmationScreen() {
9
+ const { orderNumber, email } = useFixture<{ orderNumber: string; email: string }>();
10
+ const goTo = useGoToState();
11
+ return (
12
+ <Page title="Order confirmed">
13
+ <Card padding="lg" elevation="raised">
14
+ <Stack gap="2">
15
+ <Text variant="label">Order {orderNumber}</Text>
16
+ <Text tone="muted">We've sent a receipt to {email}. Your order ships in 1–2 business days.</Text>
17
+ </Stack>
18
+ </Card>
19
+ <Button variant="secondary" data-kit-id="checkout.confirmation.done" onPress={() => goTo("cart.empty")}>
20
+ Back to the shop
21
+ </Button>
22
+ </Page>
23
+ );
24
+ }
25
+
26
+ export default defineScreen({
27
+ id: "checkout.confirmation",
28
+ route: "/checkout/confirmation",
29
+ title: "Order confirmed",
30
+ fixtures: {
31
+ default: { title: "Confirmed", data: { orderNumber: "#10482", email: "sam@example.com" } },
32
+ },
33
+ component: ConfirmationScreen,
34
+ });
@@ -0,0 +1,77 @@
1
+ import { useEffect } from "react";
2
+ import { defineScreen, useFixture, useFrameMode, useGoToState } from "@cogitato/runtime";
3
+ import { Page } from "@/components/Page";
4
+ import { Banner } from "@/kit/Banner";
5
+ import { Button } from "@/kit/Button";
6
+ import { Card } from "@/kit/Card";
7
+ import { Input } from "@/kit/Input";
8
+ import { Stack } from "@/kit/Stack";
9
+ import { Text } from "@/kit/Text";
10
+ import { money, sampleCart, subtotal } from "@/mocks/cart";
11
+
12
+ interface PaymentData {
13
+ processing?: boolean;
14
+ declined?: boolean;
15
+ }
16
+
17
+ const total = subtotal(sampleCart) + sampleCart.shipping;
18
+
19
+ function PaymentScreen() {
20
+ const { processing = false, declined = false } = useFixture<PaymentData>();
21
+ const goTo = useGoToState();
22
+ const frame = useFrameMode();
23
+
24
+ // The mock payment always succeeds after a moment. Canvas frames hold still.
25
+ useEffect(() => {
26
+ if (!processing || frame) return;
27
+ const timer = setTimeout(() => goTo("checkout.confirmation.default"), 1500);
28
+ return () => clearTimeout(timer);
29
+ }, [processing, frame, goTo]);
30
+
31
+ return (
32
+ <Page title="Payment" step="Payment">
33
+ {declined && (
34
+ <Banner tone="danger" title="Your card was declined" data-kit-id="checkout.payment.error-banner">
35
+ Try another card, or check the details with your bank.
36
+ </Banner>
37
+ )}
38
+ <Card>
39
+ <Stack gap="4">
40
+ <Input label="Card number" placeholder="1234 5678 9012 3456" disabled={processing} data-kit-id="checkout.payment.card-number" />
41
+ <Stack direction="row" gap="3">
42
+ <Input label="Expiry" placeholder="MM / YY" disabled={processing} data-kit-id="checkout.payment.expiry" />
43
+ <Input label="Security code" placeholder="123" disabled={processing} data-kit-id="checkout.payment.cvc" />
44
+ </Stack>
45
+ <Input label="Name on card" disabled={processing} data-kit-id="checkout.payment.cardholder" />
46
+ </Stack>
47
+ </Card>
48
+ <Stack direction="row" gap="4">
49
+ <Text variant="label" style={{ flex: 1 }}>
50
+ Total
51
+ </Text>
52
+ <Text variant="label">{money(total)}</Text>
53
+ </Stack>
54
+ <Button
55
+ fullWidth
56
+ size="lg"
57
+ loading={processing}
58
+ data-kit-id="checkout.payment.submit"
59
+ onPress={() => goTo("checkout.payment.processing")}
60
+ >
61
+ {processing ? "Processing" : `Pay ${money(total)}`}
62
+ </Button>
63
+ </Page>
64
+ );
65
+ }
66
+
67
+ export default defineScreen<PaymentData>({
68
+ id: "checkout.payment",
69
+ route: "/checkout/payment",
70
+ title: "Payment",
71
+ fixtures: {
72
+ default: { title: "Empty form", data: {} },
73
+ processing: { title: "Processing", data: { processing: true } },
74
+ "card-declined": { title: "Card declined", data: { declined: true } },
75
+ },
76
+ component: PaymentScreen,
77
+ });
@@ -0,0 +1,60 @@
1
+ import { defineScreen, useFixture, useGoToState } from "@cogitato/runtime";
2
+ import { Page } from "@/components/Page";
3
+ import { Banner } from "@/kit/Banner";
4
+ import { Button } from "@/kit/Button";
5
+ import { Input } from "@/kit/Input";
6
+ import { Stack } from "@/kit/Stack";
7
+ import { sampleAddress, type Address } from "@/mocks/cart";
8
+
9
+ interface ShippingData {
10
+ address: Partial<Address>;
11
+ addressError?: string;
12
+ }
13
+
14
+ function ShippingScreen() {
15
+ const { address, addressError } = useFixture<ShippingData>();
16
+ const goTo = useGoToState();
17
+
18
+ return (
19
+ <Page title="Where should we send it?" step="Shipping">
20
+ {addressError && (
21
+ <Banner tone="danger" title="We couldn't find that address" data-kit-id="checkout.shipping.address-banner">
22
+ Check the street and postcode, then try again.
23
+ </Banner>
24
+ )}
25
+ <Stack gap="4">
26
+ <Input label="Full name" value={address.name} data-kit-id="checkout.shipping.name" />
27
+ <Input label="Street address" value={address.street} error={addressError} data-kit-id="checkout.shipping.street" />
28
+ <Stack direction="row" gap="3">
29
+ <Input label="City" value={address.city} data-kit-id="checkout.shipping.city" style={{ flex: 1 }} />
30
+ <Input label="Postcode" value={address.postcode} data-kit-id="checkout.shipping.postcode" />
31
+ </Stack>
32
+ </Stack>
33
+ <Stack gap="2">
34
+ <Button fullWidth size="lg" data-kit-id="checkout.shipping.continue" onPress={() => goTo("checkout.payment.default")}>
35
+ Continue to payment
36
+ </Button>
37
+ <Button variant="ghost" data-kit-id="checkout.shipping.back" onPress={() => goTo("cart.default")}>
38
+ Back to cart
39
+ </Button>
40
+ </Stack>
41
+ </Page>
42
+ );
43
+ }
44
+
45
+ export default defineScreen<ShippingData>({
46
+ id: "checkout.shipping",
47
+ route: "/checkout/shipping",
48
+ title: "Shipping",
49
+ fixtures: {
50
+ default: { title: "Empty form", data: { address: {} } },
51
+ "invalid-address": {
52
+ title: "Address not found",
53
+ data: {
54
+ address: { ...sampleAddress, street: "12 Harbor Ln" },
55
+ addressError: "We couldn't match this street to the postcode.",
56
+ },
57
+ },
58
+ },
59
+ component: ShippingScreen,
60
+ });
@@ -0,0 +1,12 @@
1
+ *,
2
+ *::before,
3
+ *::after {
4
+ box-sizing: border-box;
5
+ }
6
+
7
+ body {
8
+ margin: 0;
9
+ font: var(--kit-typography-body);
10
+ color: var(--kit-color-text-default);
11
+ background: var(--kit-color-surface-subtle);
12
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
5
+ "module": "ESNext",
6
+ "moduleResolution": "Bundler",
7
+ "jsx": "react-jsx",
8
+ "strict": true,
9
+ "noEmit": true,
10
+ "isolatedModules": true,
11
+ "skipLibCheck": true,
12
+ "resolveJsonModule": true,
13
+ "types": ["vite/client", "@cogitato/runtime/client"],
14
+ "paths": { "@/*": ["./src/*"] }
15
+ },
16
+ "include": ["src", "vite.config.ts"]
17
+ }
@@ -0,0 +1,7 @@
1
+ import react from "@vitejs/plugin-react";
2
+ import { defineConfig } from "vite";
3
+ import { kit } from "@cogitato/runtime/vite";
4
+
5
+ export default defineConfig({
6
+ plugins: [react(), kit()],
7
+ });