Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
e27c06c
feat(themes): tailor-light/dark palettes, ThemeProvider API, tactile …
itsprade Apr 28, 2026
cb7c346
style(tailor-light): tactile buttons with solid fills, 2px bottom lip…
itsprade Apr 28, 2026
1821c31
feat(themes): preset ids light, dark, deep-dark, cream, bloom
itsprade Apr 28, 2026
d79378f
feat(themes): theme switcher, tactile slots, Bloom/Cream refinements
itsprade Apr 29, 2026
64855c3
chore: align README, CLAUDE, Next example configs with main
itsprade Apr 29, 2026
8372c7b
test: refresh menu and table snapshots after rebase onto main
itsprade Apr 29, 2026
8efe8d3
refactor(themes): drop deep-dark, default to bloom, flatten button + …
itsprade May 13, 2026
5defc35
feat(themes): font axis, neutral cream foregrounds, refined cream/blo…
itsprade May 15, 2026
62252fb
refactor(themes): memoize ThemeProvider value + stable hook returns
itsprade May 15, 2026
15b5c80
refactor(themes): drop !important on cream/bloom transparent overrides
itsprade May 15, 2026
017c3da
refactor(theme-switcher): dedup radio-item classes, drop inline-style…
itsprade May 15, 2026
1e37dc7
feat(themes): self-host fonts via optional @tailor-platform/app-shell…
itsprade May 15, 2026
cd1e567
feat(themes): getInitialAppearanceScript() helper for pre-paint hydra…
itsprade May 15, 2026
b6f77de
test(themes): cover legacy id migration, system resolution, pre-paint…
itsprade May 15, 2026
4d4e84d
docs(changeset): consolidate themes changeset; rewrite for current state
itsprade May 15, 2026
99c2541
fix(themes): RSC-safe `initial-appearance` subpath for `getInitialApp…
itsprade May 15, 2026
8e189e4
test: refresh failing UI snapshots for CI stability
Copilot May 18, 2026
e0bd797
refactor(nextjs-example): use next/script for initial appearance preload
Copilot May 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions .changeset/tailor-theme-palettes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
---
"@tailor-platform/app-shell": minor
---

Introduce the **Tailor** brand appearance: two new color palettes, an independent font axis, a ready-made `ThemeSwitcher`, and a pre-paint helper to avoid FOUC.

### Color palettes — `cream`, `bloom`

`useTheme` / `ThemeProvider` now accept `light`, `dark`, `cream`, `bloom`, and `system`. The document root sets `data-theme` to the resolved palette and keeps `class="light"` or `class="dark"` for Tailwind's `dark:` variant. `cream` and `bloom` paint a fixed vertical shell gradient on `<html>` (light tint at top → white at bottom) and use squircle corners where the browser supports `corner-shape`.

`bloom` is the new default when no `defaultTheme` is provided. Existing apps with `tailor-light` / `tailor-bloom` / `tailor-dark` stored in `localStorage` are migrated on read to `cream` / `bloom` / `dark`.

### Independent font axis — `geist`, `inter`

A second appearance axis, separate from the color theme. Applied to `<html>` as `data-font`, persisted to `localStorage` under `appshell-ui-font`. Set via `useFont` (`setFont("inter")`) or `AppShell`'s `defaultFont` prop (default `"geist"`).

AppShell no longer fetches fonts at runtime. Pick a loading strategy:

```css
/* (a) zero-config: bundled Geist + Inter variable fonts */
@import "@tailor-platform/app-shell/styles";
@import "@tailor-platform/app-shell/fonts";
```

```tsx
// (b) next/font/google — the new family fallback chain ("Geist Variable", "Geist Sans", …)
// catches Next's registered family automatically
import { Geist, Inter } from "next/font/google";
```

### `ThemeSwitcher` + `SidebarLayout.themeSwitcher`

New `ThemeSwitcher` (exported from `@tailor-platform/app-shell` and `@tailor-platform/app-shell/sidebar`) provides a two-axis appearance menu (color grid + font grid). `SidebarLayout` now mounts it in the header by default, replacing the old `SunIcon` light/dark toggle. Override or hide via the new `themeSwitcher` prop:

```tsx
<SidebarLayout themeSwitcher={null}> {/* hide */}
<SidebarLayout themeSwitcher={<MySwitcher />}> {/* replace */}
```

### Pre-paint script — `getInitialAppearanceScript()`

`ThemeProvider` writes `data-theme` / `data-font` from a post-mount effect. To avoid FOUC and React hydration warnings on SSR'd apps, inline the new helper in `<head>`:

```tsx
// app/layout.tsx
import { getInitialAppearanceScript } from "@tailor-platform/app-shell";

<html suppressHydrationWarning>
<head>
<script dangerouslySetInnerHTML={{ __html: getInitialAppearanceScript() }} />
</head>
</html>;
```

### New tokens in `theme.css`

- `--shell-gradient-start` / `--shell-gradient-end` (cream/bloom).
- `--status-{default,neutral,completed,attention,danger}`.
- `--semantic-shadow-{xs,sm,md,lg}`, mapped through `@theme` so each palette can override elevation.

### Cross-theme refactors bundled here

These are intentional and visible on **all** palettes, not just cream/bloom — call them out when communicating the upgrade:

- **`Badge` `neutral` variant** is no longer themed. It uses literal `bg-neutral-200` / `dark:bg-neutral-800` so a neutral badge stays neutral on light, dark, cream, and bloom (previously `bg-secondary`, which is light violet on cream/bloom).
- **`Table.Row` hover** is now `bg-muted` (was `bg-muted/50`) — twice as opaque on every DataTable.
- **`Dialog.Close`** is now wrapped with `<Button variant="ghost" size="icon">` so the close button inherits standard button accessibility and key handling.
- **Inputs** (`Input`, `Select.Trigger`, `Combobox.Input` / `Chips`, `Autocomplete.Input`, `Field.Control`) use `bg-transparent` with a `dark:bg-input/30` wash so they pick up the surface behind them. Inside non-card containers, the page background shows through on light/dark too.
- **Outline `Button` on cream/bloom** is transparent so the shell gradient shows through; hover restores the accent fill.

### Public API additions

`useFont`, `THEME_OPTIONS`, `FONT_OPTIONS`, `ThemeSwitcher`, `getInitialAppearanceScript`, plus types `Theme`, `ResolvedTheme`, `ThemeOption`, `Font`, `FontOption`.
201 changes: 159 additions & 42 deletions docs/api/use-theme.md
Original file line number Diff line number Diff line change
@@ -1,103 +1,220 @@
---
title: useTheme
description: Hook for accessing and controlling theme (light/dark mode)
description: Hook for accessing and controlling theme appearance (named palettes including Cream and Bloom)
---

# useTheme

React hook to access and control the current theme (light/dark mode).
React hook to access and control the current appearance theme.

## Signature

Exported types:

```typescript
export type Theme = "light" | "dark" | "cream" | "bloom" | "system";

export type ResolvedTheme = "light" | "dark" | "cream" | "bloom";

export type ThemeOption = { readonly value: Theme; readonly label: string };

/** Ordered labels for UI (e.g. theme menus); includes System last. */
export const THEME_OPTIONS: readonly ThemeOption[];
```

Hook return value:

```typescript
const useTheme: () => {
theme: "light" | "dark" | "system";
setTheme: (theme: "light" | "dark" | "system") => void;
systemTheme: "light" | "dark";
theme: Theme;
resolvedTheme: ResolvedTheme;
setTheme: (theme: Theme) => void;
};
```

## Return Value
## Return value

### `theme`

- **Type:** `"light" | "dark" | "system"`
- **Description:** Current theme setting
- **Type:** `Theme`
- **Description:** Stored theme preference. **`system`** means “follow OS light/dark” for the **default** light/dark palettes only (not cream or bloom).

### `setTheme()`
### `resolvedTheme`

- **Type:** `(theme: "light" | "dark" | "system") => void`
- **Description:** Set the theme. Persisted to localStorage.
- **Type:** `ResolvedTheme`
- **Description:** Concrete palette after resolving **`system`** to **`light`** or **`dark`**. **`cream`** and **`bloom`** are never produced by **`system`**; pick them explicitly with **`setTheme`**.

### `systemTheme`
When **`resolvedTheme`** changes, **`document.documentElement`** gets **`data-theme`** set to this value and a **`light`** / **`dark`** class for Tailwind **`dark`** variant compatibility.

- **Type:** `"light" | "dark"`
- **Description:** System preference (from OS settings)
### `setTheme(theme)`

- **Type:** `(theme: Theme) => void`
- **Description:** Set the theme. Persisted to **`localStorage`** (key **`appshell-ui-theme`** when using AppShell’s built-in provider).

Previously saved ids **`tailor-light`**, **`tailor-bloom`**, **`tailor-dark`** are mapped on read to **`cream`**, **`bloom`**, **`dark`** (see **`LEGACY_THEME_IDS`** in **`theme-context.tsx`**).

## Usage

### Display Current Theme
### Display current themes

```typescript
import { useTheme } from "@tailor-platform/app-shell";

function ThemeDisplay() {
const { theme } = useTheme();
const { theme, resolvedTheme } = useTheme();

return <div>Current theme: {theme}</div>;
return (
<div>
Preference: {theme} · Effective: {resolvedTheme}
</div>
);
}
```

### Theme Toggle
### Set a named palette

```typescript
function ThemeToggle() {
const { theme, setTheme } = useTheme();
function UseCream() {
const { setTheme } = useTheme();
return <button onClick={() => setTheme("cream")}>Cream</button>;
}
```

const toggleTheme = () => {
setTheme(theme === "dark" ? "light" : "dark");
};
### Theme menu (**`ThemeSwitcher`**)

return <button onClick={toggleTheme}>Toggle Theme</button>;
`SidebarLayout` includes a header **`ThemeSwitcher`** by default. You can reuse it elsewhere or build a custom control from **`THEME_OPTIONS`**:

```tsx
import { ThemeSwitcher } from "@tailor-platform/app-shell";

function Toolbar() {
return <ThemeSwitcher />;
}
```

### Theme Selector

```typescript
function ThemeSelector() {
import { THEME_OPTIONS, useTheme, type Theme } from "@tailor-platform/app-shell";

function CustomThemeList() {
const { theme, setTheme } = useTheme();

return (
<select value={theme} onChange={(e) => setTheme(e.target.value)}>
<option value="light">Light</option>
<option value="dark">Dark</option>
<option value="system">System</option>
</select>
<ul>
{THEME_OPTIONS.map(({ value, label }) => (
<li key={value}>
<button type="button" aria-pressed={theme === value} onClick={() => setTheme(value as Theme)}>
{label}
</button>
</li>
))}
</ul>
);
}
```

### Conditional Rendering
### Logo or assets by lightness

Use **`resolvedTheme`** and treat **`light`**, **`cream`**, and **`bloom`** like “light” palettes and **`dark`** like “dark” for monochrome assets:

```typescript
function Logo() {
const { theme, systemTheme } = useTheme();
const effectiveTheme = theme === "system" ? systemTheme : theme;
const { resolvedTheme } = useTheme();
const darkish = resolvedTheme === "dark";

return <img src={darkish ? "/logo-dark.svg" : "/logo-light.svg"} alt="Logo" />;
}
```

## Theme persistence

The built-in **`ThemeProvider`** (used inside **`AppShell`**) persists the **`theme`** value to **`localStorage`** and restores it on reload.

Use **`AppShell`**’s **`defaultTheme`** prop for the initial value when nothing is stored.

# useFont

Sibling hook to **`useTheme`** that controls the **font axis**, independent of color theme. Any color theme works with either font; users pick them separately in **`ThemeSwitcher`**.

## Signature

```typescript
export type Font = "inter" | "geist";

export type FontOption = { readonly value: Font; readonly label: string };

export const FONT_OPTIONS: readonly FontOption[];
```

```typescript
const useFont: () => {
font: Font;
setFont: (font: Font) => void;
};
```

## Return value

### `font`

- **Type:** `Font`
- **Description:** Stored font preference. Applied to **`<html>`** as **`data-font`**, which the package CSS uses to set **`font-family`** on **`body`** and headings.

### `setFont(font)`

- **Type:** `(font: Font) => void`
- **Description:** Set the font. Persisted to **`localStorage`** (key **`appshell-ui-font`** when using AppShell’s built-in provider).

## Usage

```typescript
import { useFont } from "@tailor-platform/app-shell";

function UseInter() {
const { setFont } = useFont();
return <button onClick={() => setFont("inter")}>Inter</button>;
}
```

Use **`AppShell`**’s **`defaultFont`** prop for the initial value when nothing is stored. Default is **`"geist"`**.

## Avoiding FOUC: `getInitialAppearanceScript()`

`ThemeProvider` writes **`data-theme`** / **`data-font`** to **`<html>`** from a **post-mount effect**. On SSR'd apps that creates a flash of the default palette before the stored preference is applied, plus a React hydration warning.

The package exports **`getInitialAppearanceScript()`** which returns a tiny script string consumers inline in **`<head>`** so the stored preference is applied **before first paint**. Import from the leaf subpath **`@tailor-platform/app-shell/initial-appearance`** — it's a zero-React-dep entry, safe to call from Next.js Server Components / RSC.

```tsx
// app/layout.tsx (Next.js App Router)
import Script from "next/script";
import { getInitialAppearanceScript } from "@tailor-platform/app-shell/initial-appearance";

export default function RootLayout({ children }) {
return (
<img
src={effectiveTheme === "dark" ? "/logo-dark.png" : "/logo-light.png"}
alt="Logo"
/>
<html lang="en" suppressHydrationWarning>
<head>
<Script id="app-shell-initial-appearance" strategy="beforeInteractive">
{getInitialAppearanceScript()}
</Script>
</head>
<body>{children}</body>
</html>
);
}
```

## Theme Persistence
The script reads **`localStorage`**, runs the same legacy-id migration as the provider, resolves **`system`** via **`matchMedia`**, and sets **`data-theme`**, **`data-font`**, and **`class="light"|"dark"`** on **`<html>`**. Safe to call without arguments; pass overrides for non-default storage keys / defaults.

```ts
getInitialAppearanceScript({
storageKey: "my-app-theme",
fontStorageKey: "my-app-font",
defaultTheme: "cream",
defaultFont: "inter",
});
```

Theme preference is automatically saved to localStorage and restored on page load.
For Vite / static HTML apps, paste the returned string into a **`<script>`** tag in **`index.html`**.

## Related

- [Styling & Theming](../concepts/styling-theming.md) - Theme customization
- [Styling & Theming](../concepts/styling-theming.md)
16 changes: 11 additions & 5 deletions docs/components/sidebar-layout.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
---
title: SidebarLayout
description: The default layout component with sidebar navigation, breadcrumbs, and theme toggle
description: The default layout component with sidebar navigation, breadcrumbs, and theme menu
---

# SidebarLayout

`SidebarLayout` is the default layout component that provides a responsive sidebar navigation, breadcrumb trail, and theme toggle. It's designed to work seamlessly with AppShell's module system.
`SidebarLayout` is the default layout component that provides a responsive sidebar navigation, breadcrumb trail, and theme menu (named palettes plus **System**). It's designed to work seamlessly with AppShell's module system.

## Import

Expand All @@ -31,7 +31,7 @@ This gives you:

- ✅ Responsive sidebar with auto-generated navigation from modules
- ✅ Breadcrumb navigation
- ✅ Theme toggle (light/dark mode)
- ✅ Theme menu (all palettes + **System**)
- ✅ Mobile-friendly collapsible sidebar

## Props
Expand All @@ -55,6 +55,12 @@ This gives you:

The `Outlet` component renders your current route's component.

### themeSwitcher

- **Type:** `React.ReactNode` (optional)
- **Default:** `<ThemeSwitcher />` — dropdown listing every [`Theme`](../api/use-theme.md) plus **System**
- **Description:** Pass **`null`** to hide the header theme control, or pass a custom node to replace it.

### sidebar

- **Type:** `React.ReactNode` (optional)
Expand Down Expand Up @@ -119,9 +125,9 @@ Dashboard > Products > Product Details

Breadcrumbs update automatically as users navigate through your application.

### Theme Toggle
### Theme menu

A sun/moon icon button in the header allows users to switch between light and dark themes. The theme preference is persisted to localStorage.
A palette icon button in the header opens a grid of every palette (each with a two-color preview) plus **System**. When **System** is selected, the button’s **tooltip** (`title`) summarizes the effective palette. The choice is persisted to `localStorage`. Override or hide via the **`themeSwitcher`** prop.

## Customization Examples

Expand Down
Loading