Every token, primitive and section this theme ships
Sections
23
Primitives
27
Pages
13
Tokens
84
× 2 modes × 3 presets
The tokens
The sheet is painted from the live custom properties, so it cannot go stale: change theme.css and it changes with it.
token-sheet
src/components/token-sheet.tsx
The tokens
84 tokens, two modes, three presets
Surfaces
Page--background
Card stock--card
Tab at rest--muted
Secondary--secondary
Hairline--border
Ink and brand
Ink--foreground
Quiet ink--muted-foreground
Stamp red--primary
Served-now chip--accent
Link--link
Focus--ring
The slip's own material
These five do not invert.
Slip--slip
Slip ink--slip-ink
Stamp--slip-stamp
Slip edge--slip-edge
Clip--clip
Status
Positive--positive-fill
Warning--warning-fill
Critical--critical-fill
The type ladder
Ratio 1.250, anchored at 1.5rem. Brygada 1918 sets the card and the headings, Radio Canada the chrome, Sometype Mono the slip.
Lunch, until half past twod4 · 2.93rem, fluid
Lunch, until half past twod3 · 2.344rem, fluid
Lunch, until half past twod2 · 1.875rem
Lunch, until half past twod1 · 1.5rem
Lunch, until half past twolead · 1.125rem
Lunch, until half past twobody · 1rem
Lunch, until half past twosmall · 0.875rem
Lunch, until half past twocaption · 0.75rem
Geometry and stroke
Paper has square corners. The card, the slip and every photograph are radius zero; controls take a quarter-rem; the served-now chip is the one full-radius object in the theme.
Card and slip--radius-card · 0
Controls--radius · 0.25rem
Served-now chip--radius-chip · full
Icons--icon-stroke 1.5 · size-4 / 5 / 6
The presets, three by two
Zinc, Cresson and Faïence are a token swap on one attribute — set data-preset on the root element. A preset repaints the room and the ink; it never repaints the paper, and it never moves anything.
Zinclight
Le midi
11:30 – 14:30 · Paris-style bistro
16 SEPSoupe
Get directionsServed now
Zincdark
Le midi
11:30 – 14:30 · Paris-style bistro
16 SEPSoupe
Get directionsServed now
Cressonlight
Le midi
11:30 – 14:30 · Morning and brunch
16 SEPSoupe
Get directionsServed now
Cressondark
Le midi
11:30 – 14:30 · Morning and brunch
16 SEPSoupe
Get directionsServed now
Faïencelight
Le midi
11:30 – 14:30 · Tiled bakery-café
16 SEPSoupe
Get directionsServed now
Faïencedark
Le midi
11:30 – 14:30 · Tiled bakery-café
16 SEPSoupe
Get directionsServed now
The shell
Two sections every route carries. The header derives whether the café is open from the hours record and the clock; nobody writes that sentence.
Tell us about an allergy before you order and we will tell you what is in the dish. One kitchen, shared pans, no separate fryer.
Prices
Taxes (TPS/TVQ) added at the till
menu-dayparts
src/components/menu-dayparts.tsx
Every service down the page, each opening on its counter band. Forty of the fifty-two items have no photograph and render the type-only row — never an empty square.
01
Le petit-déjeuner
Breakfast · Tuesday to Friday · 07:30 – 11:00
Marks: V Vegetarian · VG Vegan · N Contains nuts
Viennoiseries
Croissant au beurreVLaminated overnight, baked at seven$4.25
Pain au chocolatVTwo batons of Valrhona, same dough as the croissant$4.75
Chausson aux pommesVCortland apples from Rougemont, barely sweetened$4.50
Kouign-amannVButter, sugar, and forty minutes of patience. Two batches a day$5.50
Thirty-four seats and two rushes a day. Between 11:30 and 14:30, and again from 17:30, a table with a laptop on it is a table nobody can eat at. Outside those hours the wifi is free, the plugs are along the banquette, and nobody will hurry you.
find-us
src/components/find-us.tsx
Getting in
6340 rue Beaubien Est
Montréal, Québec H1M 1A9
The corner door on Beaubien, level with the pavement, no bell.
Bus
The 18 Beaubien stops at 6e Avenue, thirty metres from the door
Metro
Beaubien, orange line, is a twelve-minute walk or two stops west
Parking
Free on 6e Avenue; the Beaubien side is metered until 21:00
Bikes
Two racks under the awning and a Bixi stand at 8e Avenue
Door
One door on Beaubien, 86cm wide, level with the pavement
Washroom
Ground floor, 80cm door, grab rail, no step from the room
Room
Tables at 74cm with room to take a chair away at two of them
Mireille6340 rue Beaubien EstMontréal, Québec H1M 1A9
The coordinates, for a map app that wants numbers
45.5761, -73.5556
notices
src/components/notices.tsx
The one route a buyer edits rather than deletes. The six fields /book collects, what each is for and how long it is kept, then five closed native disclosures. Every answer is written against THIS form and THIS build: change a field, add a third-party script, or turn the booking widget on, and the page is wrong until site.privacy and this section are edited.
Notices
Personal information, and what we do with it
What the enquiry form collects, and who reads it.
No third-party script runs here, so there is nothing to consent to.
Updated 2026-09-16
The enquiry form
6 fields · kept for 12 months
What is this aboutWhich of the two enquiries this is, so the right person reads it
Your nameSo the reply is addressed to somebody
EmailThe only way we answer. Nothing else is ever sent to it
How many peopleWhether the room can take the party, before we say yes
Preferred dateOptional. It tells us which service you mean
Anything we should knowWhatever you choose to tell us. Please leave out health details
Who to ask
Responsible
Élise Tremblay. Owner-chef, and the person responsible for personal information.
Élise reads the enquiry and answers by email, usually the same day. It is used for that and for nothing else: no newsletter, no list, no advertising. Nothing is kept at the counter except the booking itself, written in the book, without your address.
Seeing it, correcting it, or having it deleted
You may ask to see what we hold, have it corrected, have it deleted, and withdraw your consent at any time. One email does all four: vieprivee@mireille.example.
Who else sees it
Nobody. The enquiry reaches the café's own mailbox and is not passed on, sold, or used for marketing. The site runs no analytics, no advertising and no third-party script.
Cookies, and why there is no banner
The site keeps one thing in your browser: your choice of the light or the dark mode. It stays on your device, we never read it, and it is shared with nobody. There is nothing else to consent to.
Allergies, and what we can promise
Tell us about an allergy before you order and we will tell you what is in the dish. One kitchen, shared pans, no separate fryer.
This page ships as a working shape, filled in for a fictional café. Replace every name, address and answer with your client's own, and have somebody qualified read it before launch. It is not legal advice.
Book
Two services take a table and the rest of the day is walk-in. The provider seam ships in all three of its modes; the demo runs on link.
booking-policy
src/components/booking-policy.tsx
Booking
What takes a booking, and what does not
Two services take a table; the rest of the day is walk-in.
Walk in for everything else, every day we are open.
site.booking.mode picks the branch: provider embeds the partner, link sends the reader out, none says walk-in only. The link renders under the container in every case.
Booking a table
Our reservation partner takes the table; we keep the record.
The theme's only form. Empty submit gives a role=alert summary that takes focus and links to each control; one bad field gives no summary and focus to the control itself. Values survive a rejected submit.
Handover
The editing guide, which is deleted before launch. It maps every region of the site to the field behind it and to the WordPress screen it becomes.
handover-map
src/components/handover-map.tsx
Framed with bands={false}: on its own route each key is printed over the live band it names, and those six sections are already framed above.
The home page
Where each region comes from
Every band is stamped, then rendered underneath its own stamp.
Every band below is the section itself, rendered at the instant the card was.
1
The header bar
The wordmark, the three routes, and whether the café is open right now.
In this build
src/fixtures/mireille.ts
site.name · site.nav[].label, .href
src/components/site-header.tsx
the bar's structure and the quiet action
src/fixtures/derive.ts
openStatus(), derived from the dayparts and never typed
In WordPress
parts/header.html
Site Editor › Patterns › Template parts › Header
Settings › General
Site title, and the Site Logo block
Site Editor › Navigation
the three routes
carafe/open-status
a dynamic block; change the hours, not the sentence
2
The photograph and its words
The counter at whatever hour it is, with the daypart's own clause over it.
In this build
mireille.ts › dayparts[i].image
key and alt: the plate for this service
mireille.ts › site.hero
the fallback plate, used when nothing is being served
src/assets/index.ts
focal: the measured crop, and the one the hero uses
not authored
the heading is built from dayparts[i].label, .start and .end
In WordPress
carafe/hero-served-now
core/cover with its native focal point
Settings › Café › Dayparts
Photograph and focal, per service. This is where the hero changes
Site Editor › Templates › Front page
the Cover block, for the fallback plate
carafe/served-heading
the derived H1; it has no text to lose on import
ManualThe overlay's contrast is measured against the crop, not guessed. Replace a counter plate with a lighter one and the white type over it stops passing AA; WordPress has no equivalent of src/assets/measure-overlay.ts, so the check stays a person's job at the port.
3
The card, and today's slips
The service being served, its dishes, and the dated paper clipped over it.
Soupe du jour ships with no description; the day's special supplies it.
In this build
menuItems[i].description
null. A printed card cannot know today's soup
slips[] › kind: special
lines[].text, lines[].priceCents
In WordPress
Menu items › the dish
leave Excerpt empty
Day slips › Add new
Kind: Today. Target: the daypart
11
A struck row
A replaces slip strikes the row, stamps its number, and hides its plate.
In this build
slips[] › kind: replaces
replaces: the menuItems id whose row it crosses out
nothing on the item changes
delete the slip or let it expire and the row is back
In WordPress
Day slips › Add new
Kind: Instead. Target: the daypart. Replaces: the dish
Day slips › the slip
Trash it, or move Valid to into the past
12
The card foot
Every mark used on this card, expanded once, and where tax is added.
In this build
derived from the rows
the key lists only the marks this card prints
strings.ts › dietary
V, VG, GF, DF, N. The future carafe text domain
mireille.ts › site.taxNote
the tax line
In WordPress
carafe/dietary-key
built from the rows the card printed
Settings › Café
Tax note
handover-tasks
src/components/handover-tasks.tsx
What a client does
The four editing tasks
The four the buyer research puts in front of an operator.
Left is this repository. Right is the block theme and its companion plugin.
Task 1 of 4
Update a dish
Its name, the line under it, its diet marks and which services it is on.
In this build
1Find the item by its id.src/fixtures/mireille.ts › menuItems[]
2Edit the French name and the English line under it.name · description
3Set the marks and the division it prints under.dietary[] · section
4List every service it is served at.dayparts[]
5Move the date, so the footer's freshness stamp moves with it.modified
6Run the fixture check; it refuses a section or daypart that does not exist.bun themes/carafe/src/fixtures/check.ts
In WordPress
1Open the dish.Menu items › the dish
2The title is the French name; the excerpt is the line under it.Title · Excerpt
3Set the marks and the services.Dish details › Dietary, Dayparts
4Pick one term.Menu sections
5Update. The stamp reads post_modified, so it moves on its own.Update
Task 2 of 4
Change a price
For good, or for one service only, without touching the printed card.
In this build
1Edit the price in cents; 1700 prints as $17 and 425 as $4.25.menuItems[i].priceCents
2Add-ons carry their own.menuItems[i].addOns[j].priceCents
3For today only, add a slip instead and leave the item alone.slips[] › kind: "replaces"
4Put the new price on the slip's line and close the window.lines[].priceCents · validFrom · validTo
In WordPress
1Open the dish and set the price.Menu items › the dish › Dish details › Price
2Add-ons are rows in the same panel.Dish details › Add-ons
3For today only, post a slip instead.Day slips › Add new › Kind: Instead
4Give the line its own price and its valid window.Lines · Valid from · Valid to
Task 3 of 4
Replace an image
A new plate, and the point in it the crop has to keep.
In this build
1Put the file beside the others.src/assets/dishes/
2Add its static import and its row to the register.src/assets/index.ts › PLATES
3Record the point the crop must hold; it is measured, not guessed.PLATES[key].focal
4Point the item at the key, with no file extension on it.menuItems[i].image.key
5For a counter plate, re-measure the overlay before it carries type.bun themes/carafe/src/assets/measure-overlay.ts
In WordPress
1Set the dish's photograph.Menu items › the dish › Featured image
2Drag the crop point onto the food.Dish details › Focal point
3A service's counter plate is set with the service.Settings › Café › Dayparts › Photograph
4The hero fallback is the Cover block's own image and focal point.Site Editor › Templates › Front page
ManualThe hero has no scrim: its white type is legible because every counter plate was measured dark where the words land. Core has no equivalent check, so a replacement counter plate is somebody's eye at the port. A dish thumbnail is unaffected; it never carries type.
Task 4 of 4
Point a visitor at a reservation
Which services take a booking, and where the button sends them.
In this build
1Set the mode; the demo ships "link".mireille.ts › site.booking.mode
2Give it the café's own booking page.site.booking.url
3Mark which services take a booking at all.dayparts[i].bookable
4Set where one table ends and a group enquiry begins.site.booking.partyMax · groupMin · groupEmail
5A live policy slip removes a service from the booking line without editing any of this.slips[] › kind: "policy"
In WordPress
1Set the mode, the provider and the URL.Settings › Café › Booking
2Set the party limits and the group address.Booking › Party max, Group min, Group email
3Mark the bookable services.Settings › Café › Dayparts › Bookable
4The header's action is a button in the header part.Site Editor › Patterns › Template parts › Header
UnverifiedOpenTable is the one documented adapter and its current embed requirements have not been confirmed. Until they are, the demo ships the link mode, the provider path is a documented seam, and no listing copy promises compatibility.
handover-slips
src/components/handover-slips.tsx
Every morning
Posting today’s change
Five kinds of slip, and what each one does to the card.
Every slip carries a window. Outside it the card underneath is the truth again.
Today
Wed 16 Sep
Soupe: courge rôtie et pomme, graines grillées · VG$9
Posted 07:52 by Élise
special
Post today's soup
Adds a line to the card for its window, and fills the row whose description was left empty.
In this build
kind: "special"
target: { type: "daypart", id }, the card it clips to
lines[]
text, and priceCents when the line carries a price
In WordPress
Day slips › Add new
Kind: Today
Target
Daypart, then the service
Instead
Wed 16 Sep
Tomates du marché Jean-Talon instead, last of the season$15
Posted 07:52 by Élise
replaces
Strike a dish, or re-price it for one day
Crosses the row out, stamps its number in the price column, and hides that dish's photograph.
In this build
kind: "replaces"
replaces: the menuItems id whose row it strikes
lines[].priceCents
a price here is today's price; the item's own does not move
In WordPress
Day slips › Add new
Kind: Instead
Target · Replaces
the service, then the dish. The list is filtered to it
Hours
Mon 14 Sep
Terrace closes for the season after Sunday 27 September
Posted 08:12 by Élise
hours
Close the terrace for the season
Strikes one row of the hours record, on the home page and on the hours card.
In this build
kind: "hours"
target: { type: "hours", id } and rowKey
rowKey
mon to sun, or terrace. A key the hours record holds
In WordPress
Day slips › Add new
Kind: Hours
Target
Hours, then the row
Booking
Mon 14 Sep
Weekend brunch is walk-in only until 27 September; terrace tables go first
Posted 08:20 by Élise
policy
Make a service walk-in only
Strikes its booking row, and the home page's booking line stops naming it.
In this build
kind: "policy"
target: { type: "booking", id } and rowKey
rowKey
a bookable daypart; the build refuses one that takes no bookings
In WordPress
Day slips › Add new
Kind: Booking
Target
Booking, then the service
Note
Wed 16 Sep
Kouign-amann: second batch out at 14:30
Posted 07:05 by Élise
notice
Say something that changes no row
A line on the card, and nothing struck. The second batch, the last of the season.
In this build
kind: "notice"
target: the card it belongs on
lines[]
text; no price, no replaces, no rowKey
In WordPress
Day slips › Add new
Kind: Note
Target
Daypart, then the service
What the build refuses to draw
5 checks
Every slip in the fixture is validated once, when the module is imported, so a slip that points at nothing cannot reach a page: the page never renders. The port runs the same checks in a rest_pre_insert_day_slip filter, so the editor refuses the row instead.
1A target that does not exist. A slip renders only on its target's card, so an orphan is a change nobody can read.
2A replaces slip whose dish is not served in the target service. The row it crosses out is not on that card.
3An hours slip naming a row the hours record does not have.
4A policy slip naming a service the café takes no bookings for.
5A window that ends before it starts, which is a change nobody made.
handover-limits
src/components/handover-limits.tsx
Before you promise it
What the port does not do
8 boundaries, stated before a client is told otherwise.
Without the companion plugin, there is no card
Plugin
The dishes, the sections and the slips are custom post types, a taxonomy and their meta, and those are plugin territory rather than theme territory. The block theme ships the templates, the parts, the patterns, theme.json and the style variations; carafe-menu ships the content model, the options screen, the dynamic blocks and the slip validation. Deactivate the plugin and the theme degrades to core-block patterns, a Group with a paragraph and a core Table for the card, so a client who stops paying still has a site. They do not still have slips.
Dark mode is not a core feature
Static CSS
Core has no dark mode, so the port carries one static stylesheet enqueued front end and editor, which remaps every --wp--preset--color--* to its dark value under prefers-color-scheme and again under [data-theme="dark"], and a toggle that sets the attribute. It is not editable in Styles and it is not a setting a client can break. A new colour added in Styles will not have a dark value until somebody adds one to that file.
Both languages ship; the document head is still yours
en-CA · fr-CA
Every string a visitor meets is authored in both languages — the chrome in src/fixtures/strings.ts, which becomes the carafe text domain with a .pot, and the content as { fr, en } pairs in src/fixtures/mireille.ts. site.locale picks the half a build prints, every route’s root element carries it as lang, and French runs inside an English page are marked so a screen reader does not read “Œufs” with English phonemes. English is the default and French is prefixed, and the switch across is in the header on every page. All five public routes are built in both languages and every internal link resolves into the one being read, so a French reader never lands on the English card. What is not here is the <link rel="alternate"> pair: in React the document head belongs to whoever mounts the theme, so the hreflang lines go in your own metadata export and README § Two languages has them verbatim. Name only a page that exists — the French entry’s routes list is what the switch and the alternates both read, and it is the field to edit when a route joins or leaves one language. The HTML edition is single-locale English by the same ruling. At the WordPress port the pairs become one multilingual plugin’s translated records, chosen and tested there.
The booking provider is a seam, not an integration
Unverified
One adapter is documented, OpenTable, as an embed container rendered only when the mode is provider. Its current embed requirements have not been confirmed, so the demo ships mode: "link" and sends a visitor to the café’s existing booking page. The theme never shows live availability and never confirms a table: a website that says a table is held, when nothing held it, is the one failure a restaurant cannot absorb.
Flipping that field changes what the site is. The provider branch mounts a third-party loader, which is a script, which sets cookies. Today this build runs no third-party script at all, which is why /notices can say there is nothing to consent to and why there is no cookie banner. Turn the widget on and that sentence stops being true: under Law 25 the consent obligations attach, and the notice has to be rewritten before the site goes live, not after.
The order link is a seam, and there is still no cart
site.order
site.order.mode has the same three branches as the booking seam: direct for the café’s own ordering page, partner for a named marketplace, and none, which draws nothing. That is the whole of it. There is no basket, no quantity on a dish row, no checkout, no payment and no delivery, and the theme claims no preparation time and no queue because it cannot know either. If a client wants ordering rather than a link to ordering, that is a commerce plugin and a different quote.
/notices is edited before launch, never deleted
Law 25
It is the one route here that is not a demo. Québec’s Law 25 has required a published, plain-language privacy notice and a named, reachable person responsible of any business collecting personal information through a website since 22 September 2023, with no size floor, and /book collects six fields. The page is written against this form and this build: change the fields, add an analytics script, or turn the booking widget on, and the notice is wrong until somebody edits site.privacy and the answers on src/components/notices.tsx. It is not legal advice and it is not a substitute for a client’s own; it is the shape, filled in.
This page and /components do not become templates
Deleted
Both are demo routes. Delete src/app/handover.tsx, src/app/components.tsx and the two site.demoNav rows before a launch, and nothing links to either. Their content does not vanish at the port: this map becomes the plugin’s Appearance › Café guide screen, and every pattern region carries a metadata.name matching the label used here, so the Site Editor’s List View names regions exactly as this page does.
A cached page cannot freeze what is being served
Interactivity API
The card prints the day’s schedule and every slip’s window into data-wp-context, and a view store re-derives the served tab, the chip and slip visibility on load. So a page held in a cache for six hours still shows the right service. With JavaScript off the answer is the full-day card, which is also what this build renders when no clock is passed and what the not-found page shows.
handover-annotation
src/components/handover-annotation.tsx
The four parts that route is drawn with. The stamp is the slip's own stamp: one marking language on the site, not two.
3
Today’s card
The service being served, with the day’s slips clipped on.
In this build
src/fixtures/mireille.ts
MENU_ITEMS[].name, .priceCents
In WordPress
Menu items → Edit
Title, Price
PortThe card’s order is the service’s order, which WordPress keeps as menu order rather than as a date.
Not found
The error page keeps its job: the closed counter, every service and its hours, and the way back to the card.
not-found
src/components/not-found.tsx
Rendered clock-free (now = null): a page built once and cached cannot claim an hour, so it prints the whole day instead of one service.
A dated sheet clipped over a card, and the row underneath it that the sheet supersedes. Five kinds, one of which strikes a row out.
ui/slip
src/components/ui/slip.tsx
All five kinds, at the fixture instant. An expired slip and a future one are in the record and neither renders — s-06 lapsed on 15 September, s-07 starts on 12 October.
special
Today
Wed 16 Sep
Soupe: courge rôtie et pomme, graines grillées · VG$9
Posted 07:52 by Élise
replaces
Instead
Wed 16 Sep
Tomates du marché Jean-Talon instead, last of the season$15
Posted 07:52 by Élise
notice
Note
Wed 16 Sep
Kouign-amann: second batch out at 14:30
Posted 07:05 by Élise
hours
Hours
Mon 14 Sep
Terrace closes for the season after Sunday 27 September
Posted 08:12 by Élise
policy
Booking
Mon 14 Sep
Weekend brunch is walk-in only until 27 September; terrace tables go first
Posted 08:20 by Élise
ui/clipped-card
src/components/ui/clipped-card.tsx
The whole device: a card, its slip lane, and a row struck out by one of the sheets in that lane. Three sheets is the density the lane was measured at.
Le midi
Wednesday 16 September · 11:30 – 14:30
Marks: V Vegetarian · VG Vegan · GF No gluten
Instead
Wed 16 Sep
Tomates du marché Jean-Talon instead, last of the season$15
Posted 07:52 by Élise
Soupe du jourCourge rôtie et pomme, graines grillées$9
Croque MireilleHam, Comté, béchamel, house sourdough$17
Tartine aux champignonsVOyster mushrooms, crème fraîche, chives, ryeslip · 2$15
Le midi
Wednesday 16 September · 11:30 – 14:30
Today
Wed 16 Sep
Soupe: courge rôtie et pomme, graines grillées · VG$9
Posted 07:52 by Élise
Instead
Wed 16 Sep
Tomates du marché Jean-Talon instead, last of the season$15
Posted 07:52 by Élise
Note
Wed 16 Sep
Kouign-amann: second batch out at 14:30
Posted 07:05 by Élise
Croque MireilleHam, Comté, béchamel, house sourdough$17
Salade de lentillesVGFNPuy lentils, roast beets, chèvre, walnuts$16
The variant axes
Two axes, both states of each. The fiction picks one per route; the buyer is buying both, so both are shown and both are captured.
ui/item-row
src/components/ui/item-row.tsx
The item-rows axis. Type-only is the default on /, thumbnails on /menu. An item with no photograph renders the type-only row on either, and a superseded row hides its plate.
type-only · default on /
Croque MireilleHam, Comté, béchamel, house sourdough$17
Salade de lentillesVGFNPuy lentils, roast beets, chèvre, walnuts$16
Magret de canard, chou rouge braisé et poires pochées au cidreGFDFDuck breast rested pink, red cabbage, cider-poached pear$34
with thumbnail · default on /menu
Croque MireilleHam, Comté, béchamel, house sourdough$17
Salade de lentillesVGFNPuy lentils, roast beets, chèvre, walnuts$16
Magret de canard, chou rouge braisé et poires pochées au cidreGFDFDuck breast rested pink, red cabbage, cider-poached pear$34
superseded · struck, stamped, and the plate hidden
Tartine aux champignonsVOyster mushrooms, crème fraîche, chives, ryeAdd: Egg $2.50$15
Tartine aux champignonsVOyster mushrooms, crème fraîche, chives, ryeslip · 2$15
menu layout
The second axis is composition, so both of its states are sections: switcher is served-card, one card with folder tabs opening on the service being served, and stacked is menu-dayparts, every service down the page on its own counter band. Both are framed above.
Primitives
27 parts, each installable on its own. Seven are tt-ui 1.5.1 vendored into the theme and restyled; the rest are the theme's own.
ui
src/components/ui/
The chrome, the actions, the card's marks and the controls, at the sizes they ship at.
the card's marks · dietary letters, the key, the slip stamp
VGF
Marks: V Vegetarian · VG Vegan · GF No gluten
slip · 2the mark a superseded row carries
controls · text, number, select, multi-line
Up to six at one table
ui/copy-line · press to copy, select-and-say-so when the clipboard is refused
6340 rue Beaubien Est Montréal
ui/card-panel
src/components/ui/card-panel.tsx
Le midi
Tuesday to Friday · 11:30 – 14:30
Tartine aux champignonsVOyster mushrooms, crème fraîche, chives, ryeAdd: Egg $2.50$15
Soupe du jour$9
Salade de lentillesVGFNPuy lentils, roast beets, chèvre, walnuts$16
ui/hours-row
src/components/ui/hours-row.tsx
Today is marked, and a row an hours slip supersedes is struck with the sheet's own number in the value column.
MondayClosed
Tuesday07:30 – 17:00
Wednesday07:30 – 17:00
Thursday07:30 – 22:00
Terraceslip · 11 May to 15 October, weather allowing
ui/tabs
src/components/ui/tabs.tsx
Arrow keys move the selection and the panel follows. Souper runs Thursday to Saturday, so on the fixture's Wednesday its tab is present and not selectable — a state of this switcher, not a specimen.
Le midi, Tuesday to Friday, 11:30 to 14:30.
Le souper is not on tonight.
ui/accordion-disclosure
src/components/ui/accordion-disclosure.tsx
Enter or Space opens the panel the summary names; the heading stays a heading, so a screen reader's heading list is still the page's outline.
Allergens and substitutions
5 marks
Every dish carries its marks on the card. Tell whoever takes your order and the kitchen will say what it can change; the fryer is shared, so nothing is guaranteed free of gluten.
Tables of seven or more
By email
Write to the café with the date, the time and how many of you there are. Groups eat from a shorter card, which is sent back with the confirmation.
ui/field
src/components/ui/field.tsx
Every state the control ships in. The error is bound to the input, so it is announced rather than merely printed near it.
at rest
Up to six at one table
invalid · the message is bound to the control
Tables here seat six; write to us for seven or more
valid
We will write back the same day
read-only · focusable, copyable, not editable
Souper is Thursday to Saturday
disabled · out of the tab order
Closed for the season after 27 September
ui/form-error-summary
src/components/ui/form-error-summary.tsx
role=alert, focused after a rejected submit, one link per failing control. It appears only when more than one field failed; a single failure sends focus straight to that control.
The theme's only modal layer, and the narrow-width navigation built on it. Enter opens it, focus lands inside and is trapped, Escape closes it and returns focus to the trigger; the page behind it does not scroll.
The two ground drawings, on the page they are drawn for. Each is a grayscale luminance mask over a --texture-ink layer, so the file carries no colour at all and one asset serves both modes and all three presets. Switch the mode above and the ink inverts with the token; the drawing does not change.
counter-rings · behind the served card on /
maple-drift · behind the visit strip on / and the house rules on /visit
the arithmetic
The alpha is a token, per mode, and it is measured rather than chosen: 6.5% on the light page for 3.4 to 4.5 L* of relief, 12.8% on the dark page for 8.3 to 10.4, because a near-black ground is compressive and cannot be darkened. Four other surfaces were argued to zero — every band that carries a photograph, the whole of /menu, the booking form, and the footer, which appears on all seven routes and would have made this wallpaper.
The theme's only menu, and the one thing it opens: the language switch in the header. Arrows move between the rows, Home and End jump to the ends, a letter jumps to the row starting with it, Enter chooses, Tab and Escape close and put focus back on the trigger. Each row is a real link to the same page in the other language, so it works with JavaScript off.