Skip to content

add two v3 invoices pages and a shared theme system - #191

Draft
tenub wants to merge 5 commits into
mainfrom
v3-invoices-demo
Draft

add two v3 invoices pages and a shared theme system#191
tenub wants to merge 5 commits into
mainfrom
v3-invoices-demo

Conversation

@tenub

@tenub tenub commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Demo pages for the invoices-only v3 slice (schematic-js#1688, backed by schematic-api#7529), plus the theming and shell work the pages needed to look like anything.

Two invoices pages

/invoices builds a billing-history card straight on the v3 data seam: useInvoices({ includePending }) for the page of invoices, deriveInvoiceList(page, { locale }) for the display rows, useResolvedLocale() for date and currency formatting, and useTranslator() / plural() for copy. The markup and Tailwind classes are the app's own, so nothing is injected into the document's styles.

/invoices-element renders the same data through the packaged <Invoices> element and styles it entirely from invoices.css, targeting the class names the element emits. Side by side, the two pages cover both ends of the v3 story: bring your own UI, or take the element and skin it.

Each row links to the hosted invoice where one exists, renders credits parenthesised and muted, and carries a status pill. Amounts are tabular-nums so the column aligns. Rows past the first 10 collapse behind a show-all toggle, and loadMore fetches the next page once the list is fully expanded.

Three states are handled explicitly:

  • Loading — a skeleton card marked aria-busy, matching the row layout so the card doesn't resize on arrival.
  • Failed with nothing to show — an error card with a retry button.
  • Failed with rows already on screen — the rows stay, and the message appears beneath them as a role="alert". A refetch that fails never blanks data the customer was already reading.

One palette, two consumers

src/styles/palette.ts holds the colours and fonts; everything else reads from it. globals.css maps them onto CSS custom properties and Tailwind theme tokens for the app's own markup, and useEmbedSettings() maps the same values onto EmbedSettings for the embedded components. /pricing, /usage and /custom-checkout now follow the site instead of the hardcoded dark embedSettings object, which is deleted.

ThemeProvider reads the preference with useSyncExternalStore over localStorage and prefers-color-scheme: an explicit choice wins, the system preference leads until someone makes one, and a storage listener keeps other tabs in step. An inline script in <head> applies the resolved class before first paint, so a dark reload never flashes light; when storage throws — private browsing, blocked site data — the choice falls back to memory and still works for the session. ThemeToggle sits in the navbar.

Type is set at the document level: every heading level pairs a font size with a line height, holding the same scale the embed's typography uses.

Token plumbing

SchematicProvider now receives a fetchAccessToken function, which it calls lazily and re-calls after a 401, with sessionKey set to the company. useAccessToken wraps the same /api/accessToken route for the components that take a token as a prop (/usage, custom checkout), cancelling in-flight requests so a slow first response can't land after a newer one. /usage drops its hand-rolled fetch effect for it, and neither invoices page needs token plumbing or a component ID of its own.

Shell and housekeeping

  • ui/{Card,Button,Badge} primitives over a cn helper, shared by the invoices page and the error and skeleton states.
  • Navbar restyled and made sticky, with links to both invoices pages.
  • README rewritten around what each route demonstrates; .env.example documents every variable, and demo mode is opt-in rather than on by default.
  • simple-import-sort replaces eslint-plugin-import's ordering, and no-restricted-imports bans parent-relative paths in favour of the @/ alias — imports updated throughout.

Trying it

Run in demo mode against a local API with the company-context-api flag enabled and invoices seeded for demo-co.

Not mergeable as-is

The ./v3 entry point only exists in @schematichq/schematic-components 3.0.0, which isn't published. pnpm-workspace.yaml overrides schematic-components, schematic-react and schematic-js to link:../schematic-js/*, and the lockfile records those links — so this installs only alongside a sibling schematic-js checkout. Both need to come back out, repinned to the published 3.0.0, before this can go green and merge.

@vercel

vercel Bot commented Aug 24, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
schematic-next-example Error Error Sep 3, 2026 2:38am UTC

Request Review

@tenub
tenub force-pushed the v3-invoices-demo branch from de8ac0d to 6698779 Compare August 24, 2026 18:27
@tenub tenub changed the title add an /invoices page on the v3 Invoices element add an /invoices demo on the v3 invoices hooks, plus a theme system Aug 28, 2026
@tenub
tenub force-pushed the v3-invoices-demo branch from d70d69c to 28ca1cb Compare August 28, 2026 18:22
@tenub tenub changed the title add an /invoices demo on the v3 invoices hooks, plus a theme system add an /invoices page on the v3 invoices hooks Aug 28, 2026
/invoices builds a billing-history card on the v3 data seam — useInvoices,
deriveInvoiceList, useResolvedLocale, useTranslator — with the app's own
markup and Tailwind classes. /invoices-element renders the same data through
the packaged <Invoices> element, styled entirely from invoices.css against
the class names the element emits.

Both pages needed somewhere to live, so the app shell comes with them. One
palette in src/styles/palette.ts feeds both globals.css, for the app's own
markup, and useEmbedSettings, for the embedded components — replacing the
hardcoded dark embedSettings object, so /pricing, /usage and /custom-checkout
follow the site. ThemeProvider resolves light/dark before first paint and a
navbar toggle switches it.

SchematicProvider now takes a fetchAccessToken function so it can re-issue a
token after a 401, and useAccessToken wraps the same route for components
that take a token as a prop.

Also: ui/{Card,Button,Badge} primitives, a rewritten README and .env.example,
simple-import-sort in place of eslint-plugin-import's ordering, and a ban on
parent-relative imports in favour of the @/ alias.

Not mergeable as-is: pnpm-workspace.yaml links schematic-components,
schematic-react and schematic-js to a sibling schematic-js checkout, since
the v3 entry point is unpublished.
@tenub tenub changed the title add an /invoices page on the v3 invoices hooks add two v3 invoices pages and a shared theme system Sep 1, 2026
The Schematic packages are linked to a sibling checkout while the v3 entry
point is unpublished. Turbopack traces only what sits under its inferred
root — this directory — so it cannot follow those symlinks and the v3 import
fails to resolve. Pointing the root one level up brings the sibling checkout
in scope. Comes out once the packages come from npm.
`/invoices` and `/invoices-element` named the data rather than the thing a
user came for, and neither had a home in the app's own navigation. They
become `/billing` — this app's markup on the v3 hooks — and
`/account/billing`, the packaged element over the same data.

The hand-built card moves out of the page it was declared in, to
`src/components/billing/InvoiceHistory.tsx`, so the page is a page. Both
routes now take their query, row limit, and copy from `src/utils/billing.ts`:
the pair only demonstrates anything if the two agree, and agreeing by
convention is agreeing by accident.
/account/billing is account settings, not a fifth destination in the primary
nav — it belongs where a user already looks for their own things. Clerk's
UserButton takes menu items, so it goes in there.

Demo mode has no ClerkProvider and so no user menu to hang it on; the link
stays in the header there, which is the only place left that keeps the route
reachable.
<SchematicStyles /> mounts on the provider in ClientWrapper, so
/account/billing now renders through the sheet the package ships rather than
through this app's own copy of it. That is the configuration worth demoing:
the packaged element as a host gets it out of the box, tracking the app's
theme toggle through `color-scheme` with nothing to wire up.

invoices.css stays on disk, unimported. Every selector in it moved onto the
classes the element now emits — __column/__cell rather than th/td, __link
rather than __date a, __chip rather than a descendant of .schematic-invoices
— so it remains a working example of restyling the element from scratch.
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.

1 participant