Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion .bun-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.3.14
1.4.3
5 changes: 3 additions & 2 deletions .github/actions/setup/action.yml
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
name: Setup
description: Install bun and the project dependencies (frozen lockfile).

# Mirrors the toolchain pinned in refresh-registry.yml so every workflow installs identically.
# Bun's version comes from .bun-version, as in refresh-registry.yml, so every workflow and every dev
# machine runs the same one. It must be at least 1.4.3: `bun run typecheck` is `bun check`, new in 1.4.3.
# Node is still installed because the theme checks run under `node --experimental-strip-types`,
# which is what type-strips the .ts files they import.
runs:
using: composite
steps:
- uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.14
bun-version-file: .bun-version

- uses: actions/setup-node@v4
with:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/refresh-registry.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ jobs:

- uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.14
bun-version-file: .bun-version

- uses: actions/setup-node@v4
with:
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
/.next/
/out/

# scripts/export-backgrounds.mjs output
/exports/

# production
/build

Expand Down
2 changes: 1 addition & 1 deletion .storybook/preview.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Preview } from "@storybook/react";
import "../app/globals.css";
import "../app/site.css";

const preview: Preview = {
parameters: {
Expand Down
40 changes: 40 additions & 0 deletions CHANGELOG.md

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ Visit `http://localhost:6006` after starting Storybook.
Every component lives in a single tree (`components/ui/[name].tsx`, published under `registry/ui/`) and draws its surface from the **material system** in `lib/material.ts`:

- **Five materials** (`glass`, `frosted`, `crystal`, `chakra`, `opaque`) via the `material` prop. Leave it off and the surface is **adaptive**: it follows the page's `data-glass` style; an explicit material pins the element under any page style.
- **Orthogonal axes** (`border`, `veil`, `diffuse`, `gradient`, `glow`, `sheen`) layered on top of any material. Booleans with typed refinements: `border="rim" | "frame"` (2px / 4px), `diffuse="stained"` (dyed glass), `glow="lg"`.
- **Orthogonal axes** (`border`, `veil`, `diffuse`, `gradient`, `glow`, `sheen`) layered on top of any material. Booleans with typed refinements: `border="rim" | "frame"` (2px / 4px), `borderColor="ink"` (the border in the text colour rather than the material's near-white edge, which reads as nothing on light themes), `diffuse="stained"` (dyed glass), `glow="lg"`.
- **Hover effects**: `effect="glow | shimmer | ripple | lift | scale"` from `lib/hover-effects.ts`.
- Accessibility from Radix UI primitives, fully typed, and themed through CSS tokens.

Expand All @@ -141,6 +141,8 @@ This floor is derived from what the theme actually uses, not chosen. The binding

Tailwind v4's own baseline (Safari 16.4 / Chrome 111 / Firefox 128) sits just under this, so the floor is set by Sistine's CSS rather than by Tailwind.

That `browserslist` belongs to this repository: it is what Sistine's own build targets and what the floor above was measured against. It does not travel with the registry items, so a consuming app is not covered by it. Declare your own `browserslist` at or above this floor; without one, your CSS toolchain falls back to its defaults, which may target engines the theme does not render in.

Below the floor there is **one safety net, not a polyfill**: `app/theme/tokens.css` ends with an `@supports not (color: oklch(…))` block that restores a legible background, foreground and panel fill. Because every colour is an oklch value passed through a custom property, an engine without oklch resolves those properties to invalid at substitution time and computes them to `unset`, so surfaces go transparent rather than falling back. The block prevents that; it does not reproduce the palette.

## 🎨 Customization
Expand Down
20 changes: 20 additions & 0 deletions app/backdrop/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Suspense } from "react";
import { BackdropScene } from "@/components/backdrop-scene";

export const metadata = {
title: "Backdrop · Sistine",
description: "A single Sistine pattern scene, full-screen: for wallpapers and for scripts/export-backgrounds.mjs.",
robots: {
index: false,
},
};

/* The scene reads the query, which a statically exported page only knows in the browser, so it renders
client-side under this boundary (the build fails on useSearchParams without one). */
export default function BackdropPage() {
return (
<Suspense>
<BackdropScene />
</Suspense>
);
}
7 changes: 7 additions & 0 deletions app/components/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ const newComponents = new Set([
"date-picker-input",
"context-menu",
"carousel",
"kbd",
"empty",
"progress",
"item",
"field",
"menubar",
"combobox",
]);

// Semantic Button variants (behavior styles), shown side-by-side.
Expand Down
4 changes: 2 additions & 2 deletions app/docs/getting-started/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -225,13 +225,13 @@ export function Example() {
:root {
--glass-sheet-a: 0.11; /* glass sheet alpha (how much body) */
--glass-border-a: 0.16; /* edge alpha */
--glass-opaque-l: 88; /* opaque floor lightness */
--glass-opaque-l: 93; /* opaque card floor lightness (under the tint wash) */
}

.dark {
--glass-sheet-a: 0.05;
--glass-border-a: 0.15;
--glass-opaque-l: 36.4;
--glass-opaque-l: 33.4;
}`}
/>
</div>
Expand Down
47 changes: 35 additions & 12 deletions app/docs/theming/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,9 @@ export default function ThemingPage() {
below).
</li>
<li>
<strong>Axes</strong> (orthogonal, so they compose freely on any material): <IC>border</IC>, <IC>veil</IC> (a legible floor for menus
/ overlays), <IC>diffuse</IC> (a readability blur floor), <IC>gradient</IC> (brand wash), <IC>glow</IC>, <IC>sheen</IC> (hover
shimmer). Each is a prop on a component, or a <IC>glass-*</IC> class on a bare element.
<strong>Axes</strong> (orthogonal, so they compose freely on any material): <IC>border</IC> (and its colour, <IC>borderColor</IC>),{" "}
<IC>veil</IC> (a legible floor for menus / overlays), <IC>diffuse</IC> (a readability blur floor), <IC>gradient</IC> (brand wash),{" "}
<IC>glow</IC>, <IC>sheen</IC> (hover shimmer). Each is a prop on a component, or a <IC>glass-*</IC> class on a bare element.
</li>
</ul>
<p className="text-muted-foreground">The five materials. Pin one with the prop, or on a raw element with the attribute:</p>
Expand Down Expand Up @@ -99,7 +99,7 @@ export default function ThemingPage() {
"opaque",
'material="opaque"',
'data-material="opaque"',
"fully solid, no see-through",
"fully solid, no see-through: the same tint (and a fresco's sweep) as the glass beside it, at full opacity",
],
].map(([m, prop, attr, look]) => (
<tr key={m}>
Expand Down Expand Up @@ -141,6 +141,12 @@ export default function ThemingPage() {
"glass-border / -rim / -frame",
"material edge: hairline 1px (0.5px under frosted) · rim 2px · frame 4px",
],
[
"border colour",
'borderColor="ink"',
"glass-border-ink",
"draw the border in the text colour (--foreground at --glass-border-ink-a, 0.5) instead of the material's near-white edge, which disappears on light themes; pairs with border",
],
[
"veil",
"veil",
Expand Down Expand Up @@ -279,8 +285,10 @@ export default function ThemingPage() {
</CardHeader>
<CardContent className="space-y-4">
<p className="text-muted-foreground">
Crystal&apos;s shine has three flavors, set with <IC>data-gloss</IC> on any ancestor (default / unset = <IC>tonal</IC>). It composes
with <IC>material=&quot;crystal&quot;</IC> and the <IC>data-glass=&quot;crystal&quot;</IC> page style:
Crystal&apos;s shine has three flavors, set with <IC>data-gloss</IC> on <IC>&lt;html&gt;</IC> or on any element below it (default /
unset = <IC>tonal</IC>). A scoped flavor recomposes the gloss for its own subtree, in the page&apos;s tint, and a nested{" "}
<IC>data-glass-tint</IC> keeps the flavor it inherits. It composes with <IC>material=&quot;crystal&quot;</IC>,{" "}
<IC>material=&quot;chakra&quot;</IC> and the <IC>data-glass=&quot;crystal&quot;</IC> page style:
</p>
<div className="overflow-x-auto">
<table className="w-full border-collapse text-sm">
Expand Down Expand Up @@ -416,6 +424,17 @@ export default function ThemingPage() {
<IC>data-material</IC> attribute directly:
</p>
<Code>{`<div className="glass glass-border glass-veil rounded-xl p-4">Legible overlay</div>`}</Code>
<p className="text-muted-foreground">
Wearing the material through <IC>@apply</IC> instead of the class? The page styles find surfaces by the <IC>glass</IC> class for the
parts they apply per element (the crystal and chakra text tiers, the opaque page&apos;s edge and text tier), and an <IC>@apply</IC>{" "}
leaves no class behind. Mark the element with <IC>data-surface</IC>, or <IC>data-surface=&quot;veil&quot;</IC> when it also applies{" "}
<IC>glass-veil</IC>. Everything else (the stacks, the blur, the solid floor the opaque page swaps in) is inherited and needs no marker.
</p>
<Code>{`.selection-pill { @apply glass glass-border; }
.menu-panel { @apply glass glass-border glass-veil; }

<button className="selection-pill" data-surface>…</button>
<div className="menu-panel" data-surface="veil">…</div>`}</Code>
</CardContent>
</Card>

Expand Down Expand Up @@ -733,8 +752,9 @@ const onAccent = useReadableForeground(accent, "large");
harmonic accents together. <IC>0</IC> = neutral.
</li>
<li>
<IC>--glass-opaque-l</IC>: tint <strong>body lightness</strong> (OKLCH L); lower = a deeper tint. Mode-aware default (88 light / 36.4
dark), and the floor AutoForeground bands opaque-card text against.
<IC>--glass-opaque-l</IC>: the opaque card's <strong>floor lightness</strong> (OKLCH L); lower = a deeper tint. The card is this floor
under the same tint wash every material carries, so it reads as the glass stack at full opacity. Mode-aware default (93 light / 33.4
dark), and the surface AutoForeground bands opaque-card text against.
</li>
<li>
<IC>--glass-opacity</IC>: <strong>solidity floor</strong> for any glass element: <IC>0</IC> = the material&apos;s natural sheer glass,{" "}
Expand All @@ -750,10 +770,13 @@ const onAccent = useReadableForeground(accent, "large");
<IC>--glass-stain-amount</IC>: how completely <IC>stained</IC> strips the backdrop&apos;s color (0–1; default <IC>1</IC>).
</li>
<li>
<IC>--glass-opaque-outline</IC>: optional <strong>accent outline</strong> for opaque surfaces. Unset → the plain glass border; set it
(e.g. <IC>var(--glass-accent)</IC>) on any ancestor to give every opaque component a colored edge. <IC>--glass-opaque-outline-w</IC>{" "}
sets its weight (the switcher&apos;s hairline / rim / frame = 1 / 2 / 4px); an element-level <IC>border=&quot;rim&quot;</IC> /{" "}
<IC>&quot;frame&quot;</IC> still wins.
<IC>data-glass-outline</IC> + <IC>--glass-outline</IC>: an optional <strong>outline</strong> on every glass surface, whatever its
material. Put <IC>data-glass-outline</IC> on <IC>&lt;html&gt;</IC> (or any ancestor) and every surface below it, borderless ones
included, gets an edge in the theme accent; <IC>--glass-outline</IC> recolours it (on its own, without the attribute, it recolours
only the edges surfaces already draw). <IC>--glass-outline-w</IC> sets the weight of every glass edge (the switcher&apos;s hairline /
rim / frame = each material&apos;s own hairline / 2 / 4px). An element-level <IC>border=&quot;rim&quot;</IC> /{" "}
<IC>&quot;frame&quot;</IC> and <IC>borderColor=&quot;ink&quot;</IC> still win. The old opaque-only names,{" "}
<IC>--glass-opaque-outline</IC> / <IC>-w</IC>, are still read.
</li>
<li>
<IC>--glass-tint-a</IC>: the tint <strong>film</strong> alpha (per preset).
Expand Down
6 changes: 3 additions & 3 deletions app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@
engine default by SOURCE ORDER at equal specificity),
- inside tokens.css, .dark comes after :root (same-element ties resolve by source order).
Consumers install a FLATTENED single-file build of this graph (registry/theme/globals.css, generated
by scripts/build-theme.mjs) — run `pnpm registry:check` after editing any partial and commit
registry/theme + public/r.
by scripts/build-theme.mjs) — run `bun run registry:check` after editing any partial and commit
registry/theme + public/r. Site-only CSS (the Geist font mapping) lives in app/site.css, which imports
this file, so nothing the documentation site defines leaks into the shipped theme.
*/
@import "tailwindcss";
@import "tw-animate-css";
Expand All @@ -20,4 +21,3 @@
@import "./theme/styles.css";
@import "./theme/presets.css";
@import "./theme/frescoes.css";
@import "./theme/sonner.css";
15 changes: 12 additions & 3 deletions app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { BackgroundProvider } from "@/components/background-provider";
import { Footer } from "@/components/footer";
import { Header } from "@/components/header";
import { Toaster } from "@/components/ui/sonner";
import "./globals.css";
import "./site.css";

const geistSans = Geist({
variable: "--font-geist-sans",
Expand Down Expand Up @@ -34,8 +34,17 @@ export default function RootLayout({
children: React.ReactNode;
}>) {
return (
<html lang="en" suppressHydrationWarning data-glass-tint="amethyst" data-glass="crystal">
<body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
/* The font variables go on <html>, not <body>: site.css maps --font-sans to --font-geist-sans in the
theme layer, which resolves on :root, so a variable defined lower down leaves it unresolved and
the page on the UA's system stack. */
<html
lang="en"
suppressHydrationWarning
data-glass-tint="amethyst"
data-glass="crystal"
className={`${geistSans.variable} ${geistMono.variable}`}
>
<body className="antialiased">
<ThemeProvider attribute="class" defaultTheme="dark" disableTransitionOnChange>
<BackgroundProvider>
<AutoForeground />
Expand Down
14 changes: 14 additions & 0 deletions app/site.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/*
The documentation site's own stylesheet: the Sistine theme plus what only this site defines.
app/globals.css is the theme a consumer installs (flattened into registry/theme/globals.css), so it
must not reference anything the consumer's app may not have. Geist is loaded by app/layout.tsx via
next/font, which defines --font-geist-sans / --font-geist-mono on <body>; mapping Tailwind's font
tokens to it belongs here, beside that loader. A consumer without these lines keeps Tailwind's
default stacks, or sets --font-sans / --font-mono in their own @theme.
*/
@import "./globals.css";

@theme inline {
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
}
Loading
Loading