Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
b3a6843
Add design system codifying the house style
claude Aug 3, 2026
bc5ac1e
Add favourites set: web-landing layer, three accents, step rail
claude Aug 3, 2026
07aec0c
Add intake: six questions that scope the work before designing
claude Aug 3, 2026
14bcfbe
Add portfolio page built from a real intake run
claude Aug 3, 2026
f0bf4ad
Add photo routes to intake: upload your own, or source to brief
claude Aug 3, 2026
46226ad
Add colour picker and asset slots; separate projects from examples
claude Aug 3, 2026
36bd29d
Fix dark-mode ground collision; add standalone build and test suite
claude Aug 3, 2026
8b29f82
Make the system usable by people who don't have the repo
claude Aug 3, 2026
fd72dd7
Add a client-facing brief: pick a look, no code, no blockers
claude Aug 4, 2026
67681cd
Add approval and delivery to the client brief; record the marketplace…
claude Aug 4, 2026
6256b82
Move the brief summary to the end instead of following the page down
claude Aug 4, 2026
9264f28
Fix front-door copy that promised the wrong number of questions
claude Aug 4, 2026
2344157
Fix accent colours that failed contrast, and gate it in CI
claude Aug 8, 2026
5c7329d
Extract the package generator into a module both sides can run
claude Aug 8, 2026
9fcd43d
Put the system online: accounts, saved projects, server-built downloads
claude Aug 8, 2026
8222068
Wire the pages to accounts, add a projects list, document going live
claude Aug 8, 2026
37839be
Stop the drafted copy repeating itself
claude Aug 8, 2026
3d072d8
Make the brief shareable: inline preview, and a way to send feedback …
claude Aug 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,15 @@ jobs:
uses: ./.github/actions/docker-demo
with:
greeting: 'hello'

# Catches an unreadable colour before it reaches main, rather than at deploy
# time — the Pages workflow runs the same check, but only after merge.
design-system:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- name: Check every accent theme stays readable
run: node design-system/tools/check-contrast.js
67 changes: 67 additions & 0 deletions .github/workflows/pages.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
name: Deploy design system to Pages

# Publishes design-system/ as a public website, so anyone can use the intake
# and browse the examples from a URL without cloning anything.
#
# One-time setup: repo Settings → Pages → Source: "GitHub Actions".
# Until that is switched on this workflow builds but cannot deploy.

on:
push:
branches: [main]
paths:
- 'design-system/**'
- '.github/workflows/pages.yml'
workflow_dispatch:

permissions:
contents: read
pages: write
id-token: write

# Let a running deploy finish rather than cancelling it mid-publish.
concurrency:
group: pages
cancel-in-progress: false

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Check the site has no external dependencies
run: |
# The system is meant to be self-contained. A stray CDN link would
# break it offline and behind strict CSP, so fail the build instead.
if grep -rIn --include='*.html' -E '(src|href)="https?://' design-system/ ; then
echo "::error::Found an external asset reference in design-system/"
exit 1
fi
echo "No external asset references."

- name: Check every accent theme stays readable
run: node design-system/tools/check-contrast.js

- name: Verify entry points exist
run: |
for f in index.html examples/picker.html examples/deck.html \
examples/landing.html examples/intake-standalone.html; do
test -f "design-system/$f" || { echo "::error::missing design-system/$f"; exit 1; }
done
echo "All entry points present."

- uses: actions/configure-pages@v5
- uses: actions/upload-pages-artifact@v3
with:
path: design-system

deploy:
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- id: deployment
uses: actions/deploy-pages@v4
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,24 @@ committing, and opening a pull request.
This change was made from a remote Claude Code session to confirm that
kicking off work from outside the terminal (web/mobile) works end to end.

## Design system

`design-system/` holds my house style, codified so a website, a PowerPoint
template, or anything I commission comes back in the same format.

- [`design-system/DESIGN-BRIEF.md`](design-system/DESIGN-BRIEF.md) — the spec to
hand a designer or an AI tool
- [`design-system/tokens/`](design-system/tokens) — colour, type, space and
radius tokens as JSON and CSS custom properties
- [`design-system/css/`](design-system/css) — the component and slide layer
- [`design-system/powerpoint/SPEC.md`](design-system/powerpoint/SPEC.md) — point
sizes and inch positions for building a `.potx`
- [`design-system/examples/`](design-system/examples) — a web landing page and a
14-slide deck, both built from the system; open either in a browser

Six accent themes ship with it (amber, electric, navy, teal, green, coral) and
swap with a single class on `<html>`.

## Agent

A minimal Claude Agent SDK (TypeScript) starter lives in `agent.ts`.
Expand Down
3 changes: 3 additions & 0 deletions app/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules/
.wrangler/
.dev.vars
197 changes: 197 additions & 0 deletions app/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
# The online service

Accounts, saved projects and server-built downloads for the design system.

Registration is live. **Downloads are free** — payment is deliberately not
built yet. Every download is counted from day one, because a usage count is the
one thing that cannot be added retrospectively: turn charging on in six months
and you still need to know what everyone already took.

---

## What runs where

| | |
|---|---|
| **Pages** | `design-system/` served as static assets |
| **API** | A Cloudflare Worker owning `/auth/*` and `/api/*` |
| **Data** | D1 (SQLite) — users, projects, downloads |
| **Files** | Built on demand from the stored answers; nothing is kept |

Files are rebuilt rather than stored, so improving the generator improves every
past project, not only new ones.

---

## Running it locally

```bash
cd app
npm install
npm run db:local # create the tables
npm run dev # http://localhost:8787
```

With no mail provider configured, the sign-in route returns the link in its own
response and the page shows it. That happens **only** when `ENVIRONMENT` is
`development` — never in production.

---

## Going live

### 1. A Cloudflare account

```bash
npx wrangler login
npx wrangler d1 create design-system
```

Put the printed `database_id` into `wrangler.toml`, then create the tables:

```bash
npm run db:remote
npm run deploy
```

That is already a working site: people can register by email, build a project
and download it.

### 2. Email, so sign-in links arrive

Without this, nobody can sign in — the link is generated but never sent.

1. Create a [Resend](https://resend.com) account (free tier is ample) and verify
your sending domain.
2. `npx wrangler secret put RESEND_API_KEY`
3. Set `MAIL_FROM` in `wrangler.toml` to an address on that domain.

### 3. Google sign-in *(optional)*

1. Google Cloud console → APIs & Services → Credentials → **OAuth client ID**,
type *Web application*.
2. Authorised redirect URI: `https://yourdomain.com/auth/google/callback`
3. `npx wrangler secret put GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET`

### 4. Apple sign-in *(optional, and the fiddly one)*

Apple needs a **paid Apple Developer account (~$99/year)**. Worth knowing before
you start, because nothing here works without it.

1. Register an **App ID**, then a **Services ID** (this is your `APPLE_CLIENT_ID`,
e.g. `com.yourcompany.designsystem.web`).
2. Configure the Services ID: domain `yourdomain.com`, return URL
`https://yourdomain.com/auth/apple/callback`.
3. Create a **Sign in with Apple key** and download the `.p8`. **You can only
download it once.**
4. Set four secrets:
```bash
npx wrangler secret put APPLE_CLIENT_ID # the Services ID
npx wrangler secret put APPLE_TEAM_ID # Membership page
npx wrangler secret put APPLE_KEY_ID # the key's ID
npx wrangler secret put APPLE_PRIVATE_KEY # the whole .p8 contents
```

Apple only ever sends someone's **name on their first authorisation** — miss it
and it is gone permanently. That is handled, but it is why an Apple user may
have no name where a Google user does.

**A provider with no credentials simply does not appear on the sign-in page.**
A button that leads to a configuration error is worse than no button.

---

## Turning charging on later

The check is already written. Switching it on is configuration, not a rewrite.

```toml
PAYWALL = "on" # in wrangler.toml
```

That alone enforces the free allowance: `FREE_DOWNLOADS` in `src/index.js` (2),
then `402 payment_required`. What is still missing is the part that takes money:

1. **Pick a provider.** For selling internationally, a merchant of record
(Lemon Squeezy, Paddle) is the seller of record and handles VAT in every
country. Stripe is cheaper but leaves tax registration and filing to you.
2. **Add a checkout route** that sends people to the provider's hosted page.
Do not build a card form — see [`SELLING.md`](../design-system/SELLING.md).
3. **Add a webhook** that verifies the provider's signature and writes one row
into `entitlements`:
- a subscription → `kind='subscription'` with `ends_at`
- a single template → `kind='purchase'` with the `brief_id`

The download check already reads that table first and falls back to the free
allowance, so nothing else changes. This has been tested: with the paywall on
and the allowance spent, inserting a subscription row lets the download through
and records it as `included`.

### Holding files until you have checked them

You mentioned confirming a job needs no further work before the files go out.
That is built and off by default:

```toml
REVIEW_BEFORE_RELEASE = "on"
```

Downloads then return `409` with "we're checking your files over" until a brief
reaches `status = 'released'`. **The admin screen to do the releasing is not
built yet** — today you would set it in SQL. Leave this `off` until it is.

---

## Configuration

| Setting | Where | Default | What it does |
|---|---|---|---|
| `PAYWALL` | `wrangler.toml` | `off` | Enforce the free-download allowance |
| `REVIEW_BEFORE_RELEASE` | `wrangler.toml` | `off` | Hold files until released |
| `SUPPORT_EMAIL` | `wrangler.toml` | — | Printed in the delivered README |
| `RESEND_API_KEY` | secret | — | Sends the sign-in email |
| `GOOGLE_CLIENT_ID` / `_SECRET` | secret | — | Enables Google |
| `APPLE_CLIENT_ID` / `_TEAM_ID` / `_KEY_ID` / `_PRIVATE_KEY` | secret | — | Enables Apple |

Secrets never go in `wrangler.toml` — it is committed.

---

## How the security holds up

Each of these is verified by a test against a running Worker, not by argument:

- **The download is server-only.** It is a `POST`, it assembles the package from
stored answers, and it is the only place a package exists. A page that builds
its own zip can never be gated, which is why generation moved server-side.
- **Sessions and sign-in tokens are stored as SHA-256 hashes.** A leaked backup
yields nothing presentable.
- **One-shot tokens are read and burned in one statement**, so the same link
cannot be used twice — including by two requests racing.
- **ID tokens are fully verified**: signature against the provider's JWKS,
issuer, audience, expiry and nonce. A decoded JWT proves nothing on its own.
- **Email matching requires the provider to have verified it**, so asserting
someone's address at a lax provider cannot claim their account.
- **Customer text is escaped** wherever it reaches markup, and a logo `src` must
be a `data:image` URL. Briefs are stored and later rendered where someone else
is signed in, so this is a real boundary rather than self-inflicted.
- **Cross-origin state changes are rejected** on `Origin`, backing up
`SameSite=Lax`.
- **Another account's project returns 404**, not 403 — no confirmation that an
id exists.

---

## Still to build

Named honestly, because they are the difference between this and a finished
product:

- **Taking money.** Everything around it is in place; the provider is not.
- **An admin screen** for releasing files, needed before
`REVIEW_BEFORE_RELEASE` is any use.
- **Account deletion.** People can sign up, so they must be able to leave and
take their data with them. `ON DELETE CASCADE` is in the schema; the route is
not.
- **A privacy policy and terms**, before real customers — you are storing
personal data the moment someone registers.
Loading
Loading