Feat/tailor themes#222
Open
itsprade wants to merge 16 commits into
Open
Conversation
6 tasks
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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