Skip to content

Add the design system, and put it online behind accounts - #21

Open
ada33-arch wants to merge 18 commits into
mainfrom
claude/design-template-system-coding-kmfrib
Open

Add the design system, and put it online behind accounts#21
ada33-arch wants to merge 18 commits into
mainfrom
claude/design-template-system-coding-kmfrib

Conversation

@ada33-arch

@ada33-arch ada33-arch commented Aug 3, 2026

Copy link
Copy Markdown
Owner

Codifies the house style from two reference sets, adds an intake that scopes each job before anything is designed, and then puts the whole thing online: registration, saved projects, and downloads the server builds and controls.

Charging is deliberately not built. Downloads are free. Every one is counted, because a usage count is the one thing that cannot be added retrospectively — switch charging on in six months and you still need to know what each person already took.


Part one — the design system

Two reference sets, two layers

Set 1 — 32 corporate deck templates. Flat, printed, photography-led.
Set 2 — 12 web landing pages ("favourites"). Screen-native: gradients, glow, product UI as the hero visual.

Folding the second into the first would have blurred both, so the web patterns live in their own layer over shared tokens.

Deliverable Import Default accent
Presentation, PPT template, report + css/slides.css amber
Landing page, SaaS site, portfolio + css/web.css violet

The shared rules

  • One saturated accent hue per document, never two, carrying every point of emphasis
  • A near-black / deep-navy anchor, with white or off-white dominating 55–70%
  • Heavy geometric headline type, tightly tracked, over roomy untracked body
  • Uppercase wide-tracked eyebrow labels above nearly every headline — the most recognisable tic
  • Two-tone headlines where one word flips to the accent, or gets an accent underline
  • Generous rounded corners, oversized section numerals, masked imagery
  • Charts where series 1 is the accent and everything after recedes to grey

Nothing is designed before the intake runs

INTAKE.md + examples/picker.html — six questions, each mapping to a fixed setting, so the same answers always produce the same starting point regardless of who designs it.

The output is a scope, not a restatement. It names the exact files to import, the exact classes to compose from, and the one doc section to read — then says explicitly "do not re-read, regenerate or restate the tokens, the components, or the rest of the system." One page costs one file, not a pass over the whole system, and it stops parallel CSS drifting away from the house style.

Colour module

Any hex generates the full 50→700 ramp — lightness anchored to fixed targets rather than offset from the input, so a very dark or very light brand colour still yields a usable 50 and 700 — derives the gradient, applies live, and outputs a :root block for the project's stylesheet. tokens.css is never edited for one project.

Imagery, logo, copy

Uploads render instantly in all four house treatments and are read with FileReader — they never leave the browser. Sourcing is constrained to free-licence libraries; PHOTOS.md rules out search-results pages, competitor sites and watermark-removed previews. Logo, copy and reference files get separate slots because the rules differ by kind.

Organisation

examples/ holds reference demos and doesn't change. Everything commissioned lives in projects/NNN-short-name/ with a required SPEC.md and an assets/ tree split by kind. projects/001-portfolio-marin/ is a real intake run kept as a worked example — it shows the process catching a conflict between the portfolio archetype's portrait hero and an illustration-only imagery answer, which would otherwise have surfaced as a revision round.


Part two — accounts, and a download the server controls

The change everything else rests on

Generation moved out of the page and into design-system/lib/package.js, imported unchanged by both the browser (instant preview) and the Worker (the copy actually delivered).

This was not tidying. While the browser assembled the package, no gate could ever hold — the browser already had everything it needed, so any check was a suggestion. Server-side generation is what makes the free allowance, and later the payment, mean anything.

Two copies of the generator would drift, and the copy that drifted would be the one customers received.

Escaping came with it

Interpolating customer text raw was harmless while the only person you could attack was yourself. As a multi-user service a brief is stored and later rendered where someone else is signed in, so every value reaching markup is escaped and a logo src must be a data:image URL. Verified by rendering a hostile brief in a browser: no script elements, no alerts, payload inert.

Three ways in, one account

Google, Apple and an emailed link all resolve through an identities table to a single user row. Signing in with Google today and by link next month is one person with one allowance, not two.

Email addresses are only matched when the provider actually verified them — otherwise asserting someone's address at a lax provider would claim their account.

  • Sessions and sign-in tokens are stored as SHA-256 hashes, so a leaked backup yields nothing presentable.
  • One-shot tokens are read and burned in a single statement; a second use of the same link is refused, 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.
  • A provider with no credentials does not appear on the sign-in page. A button that leads to a configuration error is worse than no button.

The download gate

POST /api/briefs/:id/download is the only place a package exists. Each of these was tested against a running Worker rather than argued for:

Attempt Result
No cookie 401
Forged cookie 401
GET instead of POST 404
Cross-origin POST 403
Another account's project 404 — not 403, so no hint the id exists
Paywall on, allowance spent 402
Paywall on, subscription granted 200, recorded as included
Re-downloading one already taken 200, never charged twice

Files are rebuilt from stored answers rather than kept, so a fix to the generator improves every past project — the contrast corrections below apply retroactively to work delivered before them.

Contrast defects found by testing the real UI

Two classes of failure, both invisible to code review:

  • Green and coral put white on --accent-500 — 3.12:1 and 3.47:1 against a 4.5:1 minimum. Every "Your button" in those palette cards was hard to read, on the screen where customers pick their look. The ramps keep their colour; the label flips to ink.
  • Amber, teal and lime had an --accent-600 failing against white — 2.39:1, 3.49:1, 2.11:1. That token is what every link, eyebrow and accent phrase is drawn in. Darkened in-hue until each clears, with --accent-700 pulled down to stay the darker end.

Darkening 600 broke the primary button's hover, which reused --accent-on against the new fill. Split out --accent-on-600: because every 600 now clears 4.5:1 on white by construction, white is the one value that works on it in all nine themes.

tools/check-contrast.js now checks all three pairings from the token file alone and runs on every pull request, not only at deploy. Verified to catch the regression by reintroducing the old green.

Other defects found by rendering, not reading

  • Slide spacing restated in container query units — rem tokens are right for a scrolling document, wrong on a fixed canvas, where a 24px gap ate a tenth of the slide.
  • Ornaments moved to a negative z-index layer inside an isolated stacking context; the earlier approach clobbered position: absolute on slide furniture.
  • Every child of a flipped step-rail row needs an explicit order — the numeral kept its default 0 and squashed the visual.
  • .deck--wide overflowed a 390px viewport; both deck minimums capped with min(px, 100%).
  • A sticky spec panel overlaid the last question and swallowed its clicks — found by a Playwright click timing out, which a person would have hit too.
  • The brief list returned raw column names while every other brief response returned mapped ones, so every project read "started Invalid Date" — caught by looking at the rendered page, not by the tests.

Verification

141 assertions, all driving a real browser against a really running server.

Suite Assertions Against
tests/online.js 32 a running Worker + D1
tests/journey.js 41 the offline path
intake suite 58 five pages × nine themes, three viewports
package suite 10 the standalone build

Downloads are verified by unzipping with real unzip -t, and the delivered page is re-rendered with the network fully blocked to prove it is self-contained.

What is deliberately not built

Named plainly, because the gap between this and a finished product should be a short list rather than a vague one:

  • Taking payment. The gate, the counting and the entitlements table are in place and tested; the provider integration is not.
  • An admin screen for releasing files, needed before REVIEW_BEFORE_RELEASE is any use.
  • Account deletion. People can register, so they must be able to leave and take their data. The schema cascades; the route does not exist.
  • A privacy policy and terms, before real customers — personal data is held the moment someone registers.

Setup, costs and the switch-on path are documented in app/README.md — including that Apple sign-in needs a paid developer account, and that without an email provider nobody can sign in at all.

claude added 2 commits August 3, 2026 13:53
Distils a 32-template reference set of corporate presentation and web
designs into a reusable system, so websites, PowerPoint templates and
commissioned work all come back in the same format.

Contents:
- DESIGN-BRIEF.md, the spec to hand a designer or an AI tool: colour and
  type rules, shape language, imagery and chart treatment, the standard
  14-step deck sequence, an acceptance checklist, and an explicit list of
  what the style is not.
- tokens/ as JSON and CSS custom properties. Six accent themes (amber,
  electric, navy, teal, green, coral) swap from a single class; each
  carries an --accent-on value that clears 4.5:1 on its accent.
- css/ split into base (reset and type primitives), components (cards,
  stats, steps, timelines, SWOT, buttons, media treatments, ornament) and
  slides (16:9 surfaces and slide archetypes).
- powerpoint/SPEC.md with the theme colour slot mapping, the type scale in
  points with pre-computed tracking, corner radii as PowerPoint adj values,
  and the 14 slide-master layouts.
- examples/ with a web landing page and a 14-slide deck, both composed
  from system classes only. No build step, no dependencies, no network
  requests; open either file in a browser.

Slide internals restate their spacing in container query units rather than
the rem-based space tokens, so a slide composes identically as a 460px
thumbnail and as a full-screen projection. Ornaments sit on a negative
z-index layer inside an isolated stacking context, which keeps them behind
content without forcing a position on the slide furniture. The offset
accent slab is held inside its own box so it survives clipping ancestors.

Verified by rendering both examples in Chromium: no page errors, no
console errors, no content overflowing any of the 14 slides, and all six
themes applying cleanly.
Folds a second reference set of 12 web landing pages into the system as
its own layer rather than into the deck rules. Where the deck set is
flat, printed and photography-led, the favourites set is screen-native:
gradients, glow, and product UI as the hero visual.

New accents: violet (signature of the set, six of twelve references, now
the default for web work), indigo, and lime — the last pairing with a
cream ground rather than white. Nine themes total, all with complete
ramps and an --accent-on that clears 4.5:1.

New css/web.css covers what the deck layer does not: gradient heroes with
glow orbs and wave dividers, phone and browser mockup frames with floating
stat cards, portrait label pins, trust bars, tinted feature cards,
oversized ghost numerals on a dotted step rail, pricing with a featured
tier, FAQ accordion, marquee strip, CTA bands and newsletter fields, plus
the overrides that carry all of it onto a dark page.

FAVORITES.md indexes the twelve references and states the three rules
that differ from the core brief: gradients are allowed in three places
and must stay within one accent ramp, product UI replaces stock
photography in the hero, and dark pages are permitted here only. The
brief and README now route deliverables to the right layer — decks to
slides.css and amber, web to web.css and violet.

Kept deliberately lean: the web layer is one file of new patterns with no
duplication of the deck layer, and the tokens gained three ramps and four
gradient values rather than a parallel set.

Verified in Chromium across all three example pages by all nine themes:
no page or console errors, no horizontal overflow, every accent ramp
resolving. Fixed a flip-order bug found in rendering where the step rail
left the numeral at its default order, pushing the visual into the narrow
centre column.
@ada33-arch ada33-arch changed the title Add design system codifying the house style Add design system codifying the house style, across decks and web Aug 3, 2026
claude added 4 commits August 3, 2026 14:09
Nothing gets designed until six questions are answered, and each answer
maps to a fixed setting — so the same answers always produce the same
starting point, whoever is designing.

The questions decide layer and section set, accent and ground, whether
brand colours override the recommendation, the CTA treatment, the media
treatment, and light/dark plus output medium. Three deck-only follow-ups
cover length, presenter and whether anyone edits it afterwards.

The output is deliberately a scope rather than a restatement of the
system. It names the exact files to import, the exact classes to compose
from, and the single doc section to read, and says explicitly not to
re-read or regenerate the tokens and components. Producing one page
should not cost a pass over the whole system every time, and parallel CSS
written from scratch is how a house style drifts.

examples/picker.html runs the questions interactively, re-themes live as
the accent answer changes, and emits the scope block ready to paste. It
guards the one contradictory combination: choosing a dark ground for a
deck self-corrects to light with a note, since dark is web-only here.

Verified in Chromium: all example pages across all nine themes with no
page or console errors and no horizontal overflow; the picker driven end
to end through all six answers produces the expected scope, and the
deck+dark guard fires. Fixed a usability bug the test caught, where the
sticky spec panel overlaid the last question and swallowed its clicks.
Runs the intake end to end and keeps the result as a worked example:
portfolio site, trusted/established, buy or sign up, illustration.

Those answers resolve to the web layer on indigo, an illustration-led
hero, work grid, services, process rail, three fixed-price packages with
a featured tier, testimonial, FAQ and a gradient CTA band.

The run also surfaced the collision the intake exists to catch. The
portfolio archetype normally opens on a portrait hero with label pins,
but the imagery answer was illustration with no photography. The imagery
answer wins, so the portrait and every photo mask are dropped and inline
SVG on tinted grounds takes their place. Left to a build, that would have
come back as a revision round.

Written as one file, as the scope block instructs: imports the system,
composes from existing classes, and adds five lines of scoped CSS for a
hero-specific grid fix. Nothing under css/ or tokens/ was touched — the
hero column is narrower than .stat-row's auto-fit minimum, so the third
figure is pinned locally rather than loosening the component for every
other page that uses it.

Verified in Chromium: no page or console errors, no horizontal overflow
at 1440px or 390px, and the other example pages still clean across all
nine themes.
Question 5 gains two answers. Both end in the same place — masked to a
house treatment, never dropped in raw.

Upload my own: drop a file into the picker and it renders straight away
in all four treatments (notched, circle, accent wash, dark scrim with type
over it) in the current accent theme. Read with FileReader, so the file
stays in the browser and is never uploaded. Once the dimensions are known
the picker warns if the resolution is under the floor for the intended
use, or if the crop is wrong for a hero.

Find one for me: the picker builds a search brief from the other answers
— subject from the deliverable, mood from what the audience should feel,
plus the composition constraints the system actually needs, which are the
part people forget. Headlines sit over imagery here, so the brief asks for
16:9, negative space on the side the layout needs, and no baked-in text or
heavy filter. Links go to free-licence libraries with the lead phrase
only, since the full comma-separated brief returns poor results.

Sourcing is deliberately constrained to free-licence libraries or bought
stock, and the spec grows a sourcing line either way: rights confirmation
and model release for a client photo, licence and attribution for stock.
PHOTOS.md carries the reasoning, the editorial-only trap, the attribution
line to keep, resolution floors per use, and the treatment rules. Pulling
images from a search results page or a competitor's site puts a takedown
notice on the client, so it is ruled out explicitly rather than left to
judgement.

Verified in Chromium: both routes driven end to end. Upload produces a
local data URL, renders in all four masks and emits the right warnings for
a 941x1672 portrait; search generates the expected brief and three working
library URLs. Example pages still clean across all nine themes.
Three changes, all from the same idea: the intake should collect what a
project actually needs, and each project should live in its own place.

Colours. Answering that you have brand colours opens a colour module.
Type any hex and it generates the full 50-700 ramp by anchoring lightness
to fixed targets rather than offsetting from the input, so a very dark or
very light brand colour still yields a usable 50 and 700. It derives the
gradient, applies everything to the page live, and outputs a :root block
for the project stylesheet — tokens.css is never edited for one project.

The contrast check is the reason the module exists. A custom accent is the
most common way this system gets broken: a mid-tone brand colour often
clears 4.5:1 against neither white nor near-black, so no text can sit on
it at all. The picker computes both ratios, picks the better --accent-on,
and says plainly when neither passes.

Assets. Three separate slots — logo, copy, existing material — because the
rules differ by kind and one combined upload box loses that. The logo
previews on white, ink and accent at once, since the usual failure is a
logo that looks right in the navbar and disappears in the dark footer; it
also flags JPG (no transparency), aspect ratio against the slot it will
sit in, and raster under 400px. Copy is checked against the 62-character
body measure and the ~45-character display limit. Reference files are
listed by filename only and never read.

Organisation. examples/ now holds only reference demos of the system;
everything commissioned lives in projects/NNN-short-name/ with a required
SPEC.md recording the answers and the scope they produced, and an assets/
tree split by kind. The portfolio moved there as 001 with its spec written
up, including the intake conflict it resolved.

Verified in Chromium: all five pages across all nine themes with no page
or console errors, no horizontal overflow, and stylesheets resolving after
the move. Colour, logo, copy and asset-tree modules driven end to end.
Fixed an over-escaped regex that reported every copy file as "1 words".
@ada33-arch ada33-arch changed the title Add design system codifying the house style, across decks and web Add design system: house style codified, with intake, colour picker and per-project organisation Aug 3, 2026
claude added 6 commits August 3, 2026 20:31
Testing the system as a shareable page surfaced three real defects.

Ground was applied to :root. The colour module's ground control set
--surface-page on the document, which overrode the page's own light/dark
theme and left light text on white cards — unreadable the moment anyone
picked a colour in dark mode. Ground is a setting for the page being
designed, not for the tool, so it now previews scoped to the sample block.

The spec ignored the colour module. Dialling in a custom hex left the spec
still reporting the accent family the feeling question suggested, and the
ground from that answer rather than the one actually chosen. The colour
handlers called renderColour() directly, which never re-rendered the spec;
they now go through render(). A custom hex supersedes the suggested family
and reports which text colour clears on it.

The deck page overflowed on mobile. .deck--wide asked for a 460px track
minimum, which forces the grid wider than a 390px viewport instead of
collapsing. Both deck track minimums are now capped with min(px, 100%).

Adds examples/intake-standalone.html, a single self-contained build of the
picker with every stylesheet inlined and a token-level dark layer, so the
intake can be opened or shared without the repo. The layer redefines
surfaces, foregrounds and the ink steps used as borders; because the system
is entirely token-driven no component needed a dark variant. Source stays
light-only, matching the light-ground rule for decks — the dark layer is
deliberately build-only.

Verified by a 58-assertion Chromium suite covering all six deliverables
resolving coherent scopes, the deck+dark guard self-correcting, ramp
generation and WCAG contrast across dark, light and failing mid-tone
accents, local-only photo upload with resolution and crop warnings, logo
previewed on three grounds, copy measured against the 62-character rule,
the asset tree, the scope wording, five pages across nine themes plus
mobile, and slide overflow at three viewports. Plus a dedicated regression
for the ground collision across both files, both schemes and every ground.
The scope told someone to import tokens/tokens.css, which is useless if
they never cloned anything. Three routes now close that gap, differing
only in what the other person has to install.

A link. design-system/index.html is a front door that routes the three
audiences — commissioning, designing, using AI — to the right entry point,
and a Pages workflow publishes the folder as a static site on every push
to main. The workflow gates the build on the property that makes the site
worth having: it fails if any HTML picks up an external src or href, since
the whole point is that this runs offline and under a strict CSP. Enabling
Pages needs one switch in repo settings, documented rather than assumed.

A file. tools/build-standalone.js inlines every stylesheet and adds the
token-level dark layer, producing examples/intake-standalone.html — the
whole intake in one file that can be emailed or opened from a USB stick.
It refuses to write a build containing external references. The dark layer
lives in the build rather than tokens.css because the system is
deliberately light-first for decks.

A starter file. The intake now hands over a working page instead of
instructions: a self-contained HTML document with the CSS inlined, the
chosen accent baked in as a :root override, a scaffold of the sections the
answers selected, and a header comment recording the spec. Deck answers
get slide archetypes, web answers get a landing page, and pricing appears
only when the action answer asks for it.

Two defects fixed on the way. The starter template emitted literal
</style>, </body> and </html> tags, which is fragile in any consumer that
parses by splitting — the closing tags are now escaped so the source never
contains them. The bundler split on <body>, which broke once a script
legitimately contained that string; it uses first-index and last-index
instead.

SHARING.md documents all three routes and what to send whom.

Verified: 58-assertion suite still green; starter files generated for both
layers download, are self-contained, carry the custom accent through to
render, and show no overflow; the standalone build and its dark layer pass
their regression; the home page renders at 1440 and 390 with all twelve
links resolving; the Pages external-asset gate passes against the tree.
The intake was written for designers and AI tools — layers, accent hex,
class names, a scope block. Send that to a client and you get silence.
brief.html asks the same job in four plain questions with no file names,
class names or CSS anywhere on the page.

Colour is chosen by looking, not by typing a hex. Nine looks are shown as
cards that render actual sample type, a button and the palette, named for
how they feel rather than what they are — "warm and confident", "calm and
trusted", "dark and premium". Two are dark. Every one inherits the
contrast guarantee already in the tokens, so no choice can produce
unreadable text.

The two things that usually stall a project both have a way through.
No logo: describe it instead — name, rough style, a sentence on the feel.
Nothing written: answer six short questions about the business and the
page is drafted from those words, labelled a first draft everywhere it
appears because that is what it is. A supplied logo still previews on
light, dark and the chosen colour at once, since the usual failure is one
that looks right in the navbar and vanishes in the footer.

Word documents are read in the browser. A .docx is a zip, so the central
directory is walked, word/document.xml located and inflated with the
platform's own DecompressionStream — no library and nothing uploaded.
Uncompressed entries and corrupt files are both handled, the latter with
a message naming the likely cause rather than a stack trace.

The output is a finished page carrying the customer's own words, with the
brief embedded as a comment for whoever builds from it.

Two defects fixed. The CSS collector matched a var(--accent-500) usage
rather than the token definition, so it silently inlined this page's own
40 lines instead of the system and produced an unstyled 9KB file; it now
matches the definition, and the same latent bug is fixed in the picker.
This page's .step class collided with the deck component of the same name
and inherited its injected "01" counter above every question — renamed.

Verified by a 31-assertion suite: no code strings anywhere in the visible
page, nine looks including dark, both logo routes, .docx in compressed and
uncompressed form plus graceful failure on a corrupt file, plain text, the
drafting path, and a saved page that renders with the chosen colour and
the customer's own headline at 1440 and 390. Existing 58-assertion suite,
starter files, dark regression and link check all still pass.
… plan

The brief now ends where it should: the client looks at their page,
approves it, and gets everything as one download.

The package is a .zip written by hand in the browser — CRC-32, per-file
headers, central directory — so the page keeps its no-dependency promise.
Entries deflate through the platform's CompressionStream and fall back to
stored when that doesn't help. It carries the finished page, their words
as a separate document they can edit without touching markup, the brief
they approved, and a plain-English guide to changing text and putting it
online. Verified against unzip -t and Python's zipfile rather than trusted.

There is deliberately no card form. A static page has no server, so it can
neither verify a charge nor hold card details safely, and a form here would
be theatre at best and PCI exposure at worst. Checkout hands off to a
payment link instead — Stripe, Gumroad, Lemon Squeezy or Paddle all work
from a static page. Leave the link unset and nothing pretends: the button
emails the brief and says plainly that payment isn't set up yet.

SELLING.md states the limitation the flow cannot engineer around: a
download in a static page cannot be gated behind a payment, because anyone
who can open the page can reach the file. The approval checkbox is a
confirmation, not a lock. Three honest ways to handle that are documented,
recommending the provider deliver the file. It also flags what to settle
before charging — revision policy, that the copy is a draft, who draws a
logo that was only described, and the imagery licensing that becomes your
problem once you sell the page.

ROADMAP.md records the marketplace plan against a trigger of ten or more
finished systems, currently one. It argues for building the catalogue
before the storefront, keeps a table to count against, and notes that ten
genuinely different systems means differing in type personality, structure
and job — not ten recolours, which would be one system with themes.

Verified by a new 10-assertion suite covering the approval gate, order
summary, the unlock, the download, the absence of any card field, and that
an unconfigured checkout refuses to pretend. Existing 31, 58, starter,
dark and link suites all still pass.
The summary panel was sticky, so "Your brief / See my page / Save my
brief / Start again" floated over the questions from the moment the page
loaded and stayed there all the way down. It is a recap to read once the
questions are answered, not a heads-up display, so it now sits in the
flow: questions, then the brief, then approve and download.

That also removes the reason the sticky panel existed to work around —
it previously overlaid the last question and swallowed its clicks, which
an earlier commit patched with a large bottom padding. Nothing needs to
dodge it now.

Two layout defects the screenshot showed once the panel stopped moving.
The "what you get" list put each <strong> and the text following it into
separate flex items, so every line wrapped in two places and read as a
column of fragments; the copy is now one item. The approval checkbox used
the .bar utility, which wraps, so the box detached from its wording — it
has its own non-wrapping row now, with only the wording wrapping.

Verified by a new 8-assertion suite: the summary is static, starts below
the fold, overlaps no question, and appears in the order questions →
brief → approve, with every control still sized and the download still
working. Existing 10, 31 and 58 assertion suites and the link check pass.
Routing the primary button to the client brief left the hero and the meta
description behind: both still promised six questions and a "scope", which
is the designer intake's shape and its vocabulary. The route card below
already said four. Someone landing on the page was told one thing by the
headline and taken somewhere else by the button.

The hero now describes what the button actually does — a few plain
questions, a page you can look at, and a brief precise enough to hand on —
without borrowing the technical word for it.

@ada33-arch ada33-arch left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

@ada33-arch
ada33-arch marked this pull request as ready for review August 7, 2026 22:07
claude added 4 commits August 8, 2026 14:41
Testing the client journey end to end surfaced two readable-text defects
that code review had missed:

- Green and coral put white on --accent-500 (3.12:1 and 3.47:1). Both
  clear 4.5:1 with dark ink, so the ramp keeps its colour and the label
  flips instead.
- Amber, teal and lime had an --accent-600 that failed against white
  (2.39:1, 3.49:1, 2.11:1) — and that token is what links, eyebrows and
  .text-accent are drawn in. Darkened in-hue until each clears 4.5:1,
  with --accent-700 pulled down to stay the darker end of the ramp.

Darkening 600 broke the primary button's hover, which reused --accent-on
against the new fill. Split out --accent-on-600: because every 600 now
clears 4.5:1 on white by construction, white is the one value that works
on it in all nine themes.

Also record the client's name in their brief — the download was already
named after them, but the brief itself never said whose job it was.

tools/check-contrast.js checks all three pairings from the token file
alone, and runs on pull requests as well as at deploy.
Going online means the download has to be built by the server: a package
the browser assembles can never be gated behind a sign-in or a payment,
because the browser already holds everything it needs.

design-system/lib/package.js is now the single source of truth for what a
customer receives, and runs unchanged in the browser (instant preview) and
in a Worker (the copy actually delivered). Keeping two copies would drift,
and the one that drifted would be the one customers got.

Escaping came with it. Interpolating customer text raw was harmless while
the only person you could attack was yourself; as a multi-user service the
brief is stored and later rendered where someone else is signed in, so
every value reaching markup is escaped and a logo src must be a data:image
URL. Verified by rendering a hostile brief: no script elements, no alerts,
payload inert. The brief block sits in an HTML comment, so runs of hyphens
are collapsed rather than allowed to close it early.

The bundler now inlines the module for the offline build, honouring the
import alias — without that the inlined buildPage collided with the page's
own wrapper and the script failed to parse. It fails the build if module
syntax survives.

41-assertion journey, 58-assertion suite and the 10-assertion package test
all pass, served and offline.
Registration is live and downloads are free, as asked. Payment is not
built — but the tables and the check that payment needs are, because a
usage count cannot be added retrospectively: you can never answer "how
many did this person already take?" for anyone who signed up before you
started counting.

Cloudflare Workers + D1. The Worker owns /auth/* and /api/*; the design
system's pages are served as static assets alongside.

Three ways in, one account. Google, Apple and an emailed link all resolve
through identities to a single user row, so signing in with Google and
later by link is one person with one allowance rather than two. Matching
on email only happens when the provider actually verified it — otherwise
asserting an address at a lax provider would claim someone's account.

Sign-in tokens and session cookies are stored as SHA-256 hashes, so a
leaked backup yields nothing presentable. One-shot tokens are read and
burned in a single statement; a second use of the same link is refused.
ID tokens are verified properly — signature against JWKS, issuer,
audience, expiry and nonce — since a merely decoded JWT proves nothing.

The download is the point of the whole change. It is a POST, it assembles
the package server-side from the stored answers, and it is the only place
a package exists. Verified against a running Worker: no cookie 401,
forged cookie 401, GET 404, cross-origin POST 403, another user's project
404. With the paywall flag on, the third download is refused 402 while a
granted subscription entitlement lets it through as "included", and
re-downloading something already taken never charges twice.

Files are rebuilt from answers rather than stored, so fixing the
generator improves every past project, not only new ones.
The brief page now has two lives and asks the server which one it is in
rather than guessing from the URL. Served by the Worker it belongs to a
signed-in person: the project is saved to their account and the files come
from the API. Opened as the standalone file there is no server, and it
builds everything locally exactly as before.

Added the projects page the account strip links to — without it that link
was a 404 I had shipped. Looking at it rendered caught a second defect the
tests had missed: the list endpoint returned raw column names while every
other brief response returned mapped ones, so every project read "started
Invalid Date". Both routes go through publicBrief now, and the suite
asserts on the date.

Routing simplified. The assets layer already strips .html, so /brief and
/signin resolve on their own; /start is a redirect to the one canonical
URL rather than a rewrite that left the address bar disagreeing.

tests/ carries both suites, since a test that only exists in scratch is a
test nobody will run. 32 assertions against a running Worker, 41 against a
static server, plus the 58- and 10-assertion suites — all passing.

app/README.md documents what is live, what going live costs (Apple needs a
paid developer account; without an email provider nobody can sign in), how
to switch charging on, and what is still missing — payment, an admin
release screen, account deletion, and a privacy policy before real
customers.
@ada33-arch ada33-arch changed the title Add design system: house style codified, with intake, colour picker and per-project organisation Add the design system, and put it online behind accounts Aug 8, 2026
claude added 2 commits August 8, 2026 20:12
Generating a real sample and looking at it showed two faults in the words
every delivered page carries — neither visible in code, both visible
immediately on screen.

People type answers, not sentences. "Quarterly planning that survives
contact with reality" has no full stop, so joining it to the next clause
produced one run-on line: "...with reality Made for operations leads at
growing firms." Each clause is closed before the next begins.

Worse, an answer could appear twice on one page. The hero headline takes
the "why us" answer, and the Why-us section below took the same string —
so the page said the identical sentence twice in two type sizes. Same for
"what you do" landing in both the hero subline and the section title under
it. Each answer now earns exactly one slot, and whichever slot claims it
releases the others; a section with nothing left to say is dropped rather
than filled with a repeat.

Checked across nine combinations of answered and blank fields, including
none at all and copy taken from a document: no slot repeats in any of
them. 141 assertions still pass.
…back

Asked for a link people could try, which means the page has to survive being
embedded — and two things in it did not.

The preview opened a new tab via window.open. A pop-up blocker refuses that,
and a sandboxed frame refuses it always, so the central moment of the whole
product could simply not happen for someone following a shared link. It now
renders inline in an iframe on the page, with the new tab demoted to a
secondary button that says so when it is blocked. Better anyway: you can see
an answer change the page without leaving it.

tools/build-shareable.js produces the version that goes out as a link — the
standalone file plus a banner saying what it is and who it's for, and a
feedback panel. There is no server to receive an opinion, so the panel
assembles what they chose and what they wrote into one block of text and puts
it on the clipboard. The Clipboard API is itself blocked by permissions policy
in an embedded frame, so the text appears on screen and selected either way,
with a message saying which happened.

tests/sandbox.js runs the whole thing inside sandbox="allow-scripts
allow-forms" — no pop-ups, no clipboard, no same-origin. 15 assertions, and
the rendering confirmed by screenshot, because a nested opaque-origin frame
cannot be read by the driver.

The fragment guard needed fixing too: it searched the whole file for </body>,
which the page generator legitimately emits as text, so it failed a build that
was correct. It checks the ends now.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants