Feat/tailor themes#222
Conversation
commit: |
…buttons Made-with: Cursor
…, border 0 on press Made-with: Cursor
Rename theme API and html[data-theme] from tailor-* to short names. LocalStorage values tailor-light, tailor-bloom, tailor-dark map to cream, bloom, deep-dark on read. deep-dark keeps Tailwind dark class. Includes palette CSS, shell gradients, tactile globals, docs, and changesets. Checkpoint: cream + bloom + deep-dark brand themes alongside default light/dark. Made-with: Cursor
- Add ThemeSwitcher (grid) and Tailor palettes (cream, bloom, deep-dark); wire sidebar slot; export THEMES/options. - Theme context: bloom label, resolver updates; muted token comments aligned. - Globals: tactile Cream/Bloom/deep-dark rules for composed controls (dialog-close/trigger slots); table layout fix. - Dialog: card surface for content; footer/close compose with branded buttons. - Vite example: CSS load order note (Tailwind before app-shell styles). - Docs: use-theme and sidebar-layout; styling-theming palettes table. Made-with: Cursor
After rebase onto latest main, restore dev/docs and nextjs-app local config to match origin/main so theme work stays separate from dev ergonomics (turbopack root, extra scripts). Made-with: Cursor
Align snapshot output with upstream DataTable/menu DOM after merging main. Made-with: Cursor
…sidebar chrome - Remove deep-dark palette + types + switcher entry + globals selectors; dark covers the same need. - Default theme now bloom (existing localStorage choices preserved); legacy tailor-dark id remaps to dark. - Strip tactile button overrides; cream/bloom/dark buttons inherit default shadcn Button styling. - Drop sidebar right-edge divider (border-r/border-l and inset-variant border-x). - Dark --sidebar matches --background; sidebar blends with app surface. - Unify --destructive at #dc2626 across light/cream/bloom for brand-consistent red. - Neutral badge uses Tailwind neutral palette so lavender --secondary doesn't bleed in. - Outline button transparent on cream/bloom only (rule sits in @layer utilities to beat Tailwind's astw:bg-background which also lives in utilities). - Secondary button hover now brightness-based (visible regardless of token value). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…om shell - Font axis (Geist default, Inter): `useFont` hook + `data-font` attr, added to ThemeSwitcher as a second axis alongside the color palette. - Cream theme drops the dark-green text-on-violet pairing; secondary / accent / sidebar-accent foregrounds now use the same near-black as bloom. - Cream/Bloom shell gradient: multi-stop fade with pure white covering the bottom 30% for a softer, more blended feel. - Sidebar active item: hairline outline (`var(--border)`) + `--semantic-shadow-xs` drop so the selected row reads as elevated. Rule lives in `@layer utilities` so it wins against Tailwind's `outline-hidden` utility. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ba6c830 to
5defc35
Compare
- Wrap `value` in `useMemo`; wrap `setTheme`/`setFont` in `useCallback` so consumers don't re-render when ThemeProvider re-renders for an unrelated reason and identity-comparison code (effect deps, memo selectors) stays stable across renders. - `useTheme` / `useFont` return a memoized projection of the context so the hook return identity matches the underlying state slice. - Change `ThemeProviderContext` default to `undefined` so the `useTheme must be used within a ThemeProvider` runtime guard actually fires instead of silently returning no-op setters from a fallback `initialState`. - Drop redundant `defaultTheme ?? "bloom"` / `defaultFont ?? "geist"` in `AppShell` — `ThemeProvider` already has the same defaults; single source. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Move the cream/bloom `body` and sidebar-chrome transparency rules from `@layer base` to `@layer utilities` so they win the cascade against Tailwind utilities (`bg-sidebar*` on sidebar slots) on layer order, and drop the `!important` declarations — selector specificity now suffices. `!important` is retained only on the WebKit/Firefox autofill rule, where it's the documented way to defeat browser default styling. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… fallbacks
- Extract `radioItemClasses(active)` shared by the color and font grids;
the two grids were carrying byte-identical 7-line class strings.
- Remove the inline `style={{ display: "grid", gridTemplateColumns: ..., gap: ... }}`
fallbacks on `RadioGroup` and the matching `display: flex` block on
`ThemePreviewSwatches`. The Tailwind `astw:grid astw:grid-cols-N astw:gap-2`
utilities cover the same layout and are now the single source of truth, in
line with the project rule that components must be self-contained / portable
(`.agents/skills/add-component/SKILL.md`).
- Drop the now-obsolete "inline fallback" note from the vite example's
`index.css`; the load-order rationale remains.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…/fonts subpath
Remove the eight runtime `@import url("https://cdn.jsdelivr.net/...")` lines
from `globals.css`. The default `@import "@tailor-platform/app-shell/styles"`
is now font-free, eliminating the third-party CDN dependency (supply-chain
risk, request privacy, render-blocking imports, no offline support, not
lockfile-pinned).
In its place, AppShell ships a separate, opt-in stylesheet at
`@tailor-platform/app-shell/fonts` which pulls in `@fontsource-variable/geist`
and `@fontsource-variable/inter` — self-hosted, lockfile-pinned, single
variable font per family (~30KB each vs. 4× static weights).
Consumers pick a loading strategy:
1. `@import "@tailor-platform/app-shell/fonts"` — zero-config.
2. `next/font/google` with the conventional family name (`Geist`,
`Inter`) — the new `font-family` fallback chain
`"Geist Variable", "Geist Sans", …` catches it.
3. Bring their own font; AppShell's `data-font` attribute still drives
which axis is active.
Wiring:
- New `packages/core/src/assets/fonts.css` (copied to `dist/fonts.css` by
`publicDir` during build).
- New `"./fonts": "./dist/fonts.css"` entry in `package.json` exports.
- `@fontsource-variable/{geist,inter}` added to core deps.
- Example apps updated to import the subpath so demos still render Geist.
- `docs/concepts/styling-theming.md` documents the three loading patterns.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tion Add a public helper that returns the source of a tiny IIFE consumers inline in `<head>` so the stored theme + font are applied **before** React mounts. Closes the FOUC / hydration-warning gap left by `ThemeProvider`'s post-mount effect. The returned script reads `localStorage`, runs the same legacy-id migration as `parseStoredTheme`, resolves `system` via `matchMedia`, and writes `data-theme`, `data-font`, and `class="light"|"dark"` on `<html>`. Wired into the Next.js example via a `<script dangerouslySetInnerHTML>` in `app/layout.tsx`. Documented in `docs/api/use-theme.md` with usage for Next.js App Router and Vite/static HTML. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… script
- `theme-context.test.tsx`: legacy `tailor-{light,bloom,dark}` ids map to
`{cream,bloom,dark}` on first render; unrecognized theme / font fall back
to `defaultTheme` / `defaultFont`; `system` resolves to `dark` / `light`
via a mocked `matchMedia`; `useTheme` / `useFont` throw outside a
`ThemeProvider` (now that the context default is `undefined`).
- `initial-appearance.test.ts`: the script returned by
`getInitialAppearanceScript()` writes `data-theme` / `data-font` /
`class="light"|"dark"` correctly, applies legacy migration, resolves
`system`, honors custom storage keys, and silently swallows storage
errors so it never blocks paint.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Remove the stale `bloom-tailor-theme.md` (Bloom is now the default, not additive; covered in the consolidated entry). - Rewrite `tailor-theme-palettes.md` to reflect what actually ships: cream/bloom palettes, independent font axis, ThemeSwitcher slot, the new `getInitialAppearanceScript()` helper, the cross-theme refactors (Badge neutral, Table.Row hover, Dialog.Close button wrap, transparent inputs, outline button on cream/bloom), and the full set of new public exports. Also pick up oxfmt's trailing reformatting of the new theme-context / initial-appearance tests and theme-switcher's `radioItemClasses` call. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…earanceScript` The Next.js example's `app/layout.tsx` is a Server Component. Importing `getInitialAppearanceScript` from the package root pulled in the full client barrel, transitively `react-router`, which exports `useRouteError` that doesn't exist in the React Server Components build — Turbopack RSC bailed with "Export useRouteError doesn't exist in target module". Fix: ship `getInitialAppearanceScript` as a leaf subpath `@tailor-platform/app-shell/initial-appearance`. The source already has no React / react-router imports (only `type` imports from `theme-context` that are erased at compile time), so it's a ~1KB zero-runtime-dep entry — safe to call from any RSC layout. Added to `vite.config.ts` build entries and `package.json` exports. The top-level export remains for non-RSC consumers; docs and the Next.js example use the leaf subpath. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
762041e to
99c2541
Compare
|
@copilot fix CI failure |
Agent-Logs-Url: https://github.com/tailor-platform/app-shell/sessions/4872c979-d346-46d8-8d37-c2ecba3c7bcf Co-authored-by: IzumiSy <982850+IzumiSy@users.noreply.github.com>
Addressed in commit |
|
@copilot what breaking changes does this PR have? |
Main breakpoints to watch:
No new UI changes were made in this follow-up comment response, so there is no additional screenshot to attach. |
| "./initial-appearance": { | ||
| "types": "./dist/initial-appearance.d.ts", | ||
| "default": "./dist/initial-appearance.js" | ||
| }, |
There was a problem hiding this comment.
That entry is needed to expose @tailor-platform/app-shell/initial-appearance as a separate leaf export. It lets consumers (especially Next.js app/layout.tsx / RSC) import getInitialAppearanceScript() without pulling the main React bundle entry. It’s used in the Next.js example and docs, and enables pre-paint theme/font application to avoid FOUC/hydration mismatch.
No UI change in this follow-up, so there’s no screenshot to attach.
| <head> | ||
| {/* Apply stored appearance before paint to avoid FOUC + hydration warnings. */} | ||
| <script dangerouslySetInnerHTML={{ __html: getInitialAppearanceScript() }} /> | ||
| </head> |
There was a problem hiding this comment.
@copilot I don't want to use dangerouslySetInnerHTML. Do we have better alternative?
There was a problem hiding this comment.
Yes — updated in e0bd797 to avoid direct dangerouslySetInnerHTML in the Next.js example/docs. It now uses next/script with strategy="beforeInteractive":
<Script id="app-shell-initial-appearance" strategy="beforeInteractive">{getInitialAppearanceScript()}</Script>
This keeps pre-paint behavior (no FOUC/hydration mismatch) without using dangerouslySetInnerHTML in app code. No visual/UI change, so no screenshot.
Agent-Logs-Url: https://github.com/tailor-platform/app-shell/sessions/08329a5f-d86d-4778-8393-a5757806ebae Co-authored-by: IzumiSy <982850+IzumiSy@users.noreply.github.com>
I checked the latest branch runs and CI is currently green for
Breaking changes to watch:
|
Summary
Introduces the Tailor brand appearance for AppShell — two new color palettes (
cream,bloom), an independent font axis (geist/inter), a ready-madeThemeSwitcher, a pre-paint helper that prevents FOUC + hydration warnings, and a handful of cross-theme chrome refactors.bloomis the newdefaultTheme. Storedtailor-light/tailor-bloom/tailor-darkids are migrated tocream/bloom/darkon first read.See
.changeset/tailor-theme-palettes.mdfor the consolidated changeset.What's new
Color palettes
cream(off-white shell, light-violet accents) andbloom(lavender shell, white accent surfaces) on top oflight,dark,system.<html>(light tint at top → white in the bottom ~30%) and use squircle corners where the browser supportscorner-shape.theme.css:--shell-gradient-{start,end},--status-{default,neutral,completed,attention,danger},--semantic-shadow-{xs,sm,md,lg}mapped through@theme.var(--border)) +--semantic-shadow-xsto read as elevated.Font axis
setFont("inter")via the newuseFonthook, or<AppShell defaultFont="inter">.cdn.jsdelivr.net@imports inglobals.cssare gone. Consumers pick a loading strategy:@import "@tailor-platform/app-shell/fonts"ships self-hosted variable Geist + Inter (one variable font per family, ~30KB each, lockfile-pinned via@fontsource-variable/*).next/font/googlewith the conventional family name — the family chain"Geist Variable", "Geist Sans", …catches it automatically.font-familyonbodyin your own CSS; AppShell'sdata-fontattribute still drives the axis.ThemeSwitcher+SidebarLayout.themeSwitcherThemeSwitchercomponent (exported from@tailor-platform/app-shelland@tailor-platform/app-shell/sidebar) renders a two-axis appearance menu.SidebarLayoutnow mounts it in the header by default, replacing the oldSunIconlight/dark toggle.themeSwitcher={<MyComponent />}or hide withthemeSwitcher={null}.getInitialAppearanceScript()<head>. ReadslocalStorage, runs legacy-id migration, resolvessystemviamatchMedia, setsdata-theme/data-font/class="light"|"dark"— all before first paint. Closes the FOUC + hydration-warning gap thatThemeProvider's post-mount effect leaves on SSR'd apps.app/layout.tsx. Documented indocs/api/use-theme.mdfor Next.js + Vite.Cross-theme refactors (visible on all palettes, not just cream/bloom)
These are intentional but worth calling out for downstream apps:
Badge.neutraluses literalbg-neutral-200/dark:bg-neutral-800(wasbg-secondary, which would render light-violet on cream/bloom).Table.Rowhover is nowbg-muted(wasbg-muted/50— twice as opaque on every DataTable).Dialog.Closeis wrapped with<Button variant="ghost" size="icon">— inherits standard button accessibility / keyboard handling.Input,Select.Trigger,Combobox.Input/Chips,Autocomplete.Input,Field.Control) flip tobg-transparent+dark:bg-input/30so they pick up the surface behind them.Buttonon cream/bloom is transparent so the shell gradient shows through; hover restores the accent fill.API additions
useFont·THEME_OPTIONS·FONT_OPTIONS·ThemeSwitcher·getInitialAppearanceScript· typesTheme,ResolvedTheme,ThemeOption,Font,FontOption.AppShellprops:defaultTheme,defaultFont.SidebarLayoutprop:themeSwitcher.Implementation quality
ThemeProvidervalue isuseMemo'd;setTheme/setFontareuseCallback'd. Hooks return memoized projections so consumer identity-equality stays stable across unrelated re-renders.ThemeProviderContextdefaults toundefined;useTheme/useFontthrow outside a provider rather than silently returning no-op setters.@layer utilities, no!importantneeded (only the WebKit/Firefox autofill rule retains it — documented browser-override territory).ThemeSwitcher: sharedradioItemClasses(active)helper for the color and font grids; no inlinestyle={{…}}duplicates of Tailwind utilities.Tests
ThemeSwitcherUI tests (menu list, system trigger title, theme apply, font apply).ThemeProviderunit tests: legacytailor-{light,bloom,dark}migration; unrecognized theme/font fallback;system→dark/lightvia mockedmatchMedia;useTheme/useFontguard throws outside a provider.getInitialAppearanceScripttests covering the pre-paint script's full behavior: theme + font apply, legacy migration, system resolution, custom storage keys, default fallbacks, and silent error handling.The pre-existing snapshot failures in
menu/select/combobox/autocompleteare inherited frommain(base-ui inline-style ordering); not introduced or worsened by this PR.Test plan
light→dark→cream→bloomwith both fonts.tailor-lightmigrates tocream(DevTools → Application → Local Storage).@import "@tailor-platform/app-shell/fonts"loads Geist Variable / Inter Variable.pnpm type-check,pnpm lint,pnpm fmt: clean.🤖 Generated with Claude Code