Add runtime skin contract and docs
This commit is contained in:
@@ -1,6 +1,12 @@
|
|||||||
import "../src/preview.css";
|
import "../src/preview.css";
|
||||||
|
|
||||||
import type { Preview } from "@storybook/react";
|
import type { Preview } from "@storybook/react";
|
||||||
|
import {
|
||||||
|
defaultSkin,
|
||||||
|
setSkin,
|
||||||
|
skinDetails,
|
||||||
|
skinNames
|
||||||
|
} from "@ai-ui/ui";
|
||||||
import {
|
import {
|
||||||
defaultMotionMode,
|
defaultMotionMode,
|
||||||
defaultTheme,
|
defaultTheme,
|
||||||
@@ -34,10 +40,22 @@ const preview: Preview = {
|
|||||||
title: modeName === "system" ? "Motion / System" : "Motion / Reduced"
|
title: modeName === "system" ? "Motion / System" : "Motion / Reduced"
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
skin: {
|
||||||
|
description: "Preview component skin",
|
||||||
|
toolbar: {
|
||||||
|
icon: "mirror",
|
||||||
|
dynamicTitle: true,
|
||||||
|
items: skinNames.map((skinName) => ({
|
||||||
|
value: skinName,
|
||||||
|
title: skinDetails[skinName].label
|
||||||
|
}))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
initialGlobals: {
|
initialGlobals: {
|
||||||
motion: defaultMotionMode,
|
motion: defaultMotionMode,
|
||||||
|
skin: defaultSkin,
|
||||||
theme: defaultTheme
|
theme: defaultTheme
|
||||||
},
|
},
|
||||||
parameters: {
|
parameters: {
|
||||||
@@ -63,8 +81,10 @@ const preview: Preview = {
|
|||||||
if (typeof document !== "undefined") {
|
if (typeof document !== "undefined") {
|
||||||
setTheme(context.globals.theme ?? defaultTheme);
|
setTheme(context.globals.theme ?? defaultTheme);
|
||||||
setMotionMode(context.globals.motion ?? defaultMotionMode);
|
setMotionMode(context.globals.motion ?? defaultMotionMode);
|
||||||
|
setSkin(context.globals.skin ?? defaultSkin);
|
||||||
|
|
||||||
document.body.dataset.theme = context.globals.theme ?? defaultTheme;
|
document.body.dataset.theme = context.globals.theme ?? defaultTheme;
|
||||||
|
document.body.dataset.skin = context.globals.skin ?? defaultSkin;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Story();
|
return Story();
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
@import "@ai-ui/tokens/styles.css";
|
@import "@ai-ui/tokens/styles.css";
|
||||||
|
@import "@ai-ui/ui/skins.css";
|
||||||
@source "../../../packages/ui/src";
|
@source "../../../packages/ui/src";
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
@@ -14,5 +15,8 @@ body,
|
|||||||
|
|
||||||
body {
|
body {
|
||||||
background: var(--color-background);
|
background: var(--color-background);
|
||||||
|
background-image: var(--ui-canvas-image);
|
||||||
|
background-size: var(--ui-canvas-size);
|
||||||
|
background-attachment: fixed;
|
||||||
color: var(--color-foreground);
|
color: var(--color-foreground);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,260 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Input,
|
||||||
|
Skeleton,
|
||||||
|
Switch,
|
||||||
|
defaultSkin,
|
||||||
|
skinDetails,
|
||||||
|
skinNames,
|
||||||
|
type SkinName
|
||||||
|
} from "@ai-ui/ui";
|
||||||
|
import {
|
||||||
|
defaultMotionMode,
|
||||||
|
defaultTheme,
|
||||||
|
type MotionModeName,
|
||||||
|
type ThemeName
|
||||||
|
} from "@ai-ui/tokens";
|
||||||
|
import type { Meta, StoryObj } from "@storybook/react";
|
||||||
|
|
||||||
|
type StyleContractShowcaseProps = {
|
||||||
|
motion: MotionModeName;
|
||||||
|
skin: SkinName;
|
||||||
|
theme: ThemeName;
|
||||||
|
};
|
||||||
|
|
||||||
|
function RuntimeBadge({ label, value }: { label: string; value: string }) {
|
||||||
|
return (
|
||||||
|
<div className="rounded-[var(--radius-full)] border border-[var(--color-border)] bg-[var(--color-surface)] px-3 py-2 text-xs uppercase tracking-[var(--tracking-caps)] text-[var(--color-muted-foreground)]">
|
||||||
|
<span className="mr-2 text-[var(--color-foreground)]">{label}</span>
|
||||||
|
{value}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SkinPanel({
|
||||||
|
description,
|
||||||
|
name
|
||||||
|
}: {
|
||||||
|
description: string;
|
||||||
|
name: SkinName;
|
||||||
|
}) {
|
||||||
|
const [enabled, setEnabled] = useState(name !== "minimal");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<article
|
||||||
|
data-skin={name}
|
||||||
|
className="grid gap-4 border p-5"
|
||||||
|
style={{
|
||||||
|
background: "var(--ui-surface-bg)",
|
||||||
|
borderColor: "var(--ui-surface-border)",
|
||||||
|
borderRadius: "var(--ui-surface-radius)",
|
||||||
|
boxShadow: "var(--ui-surface-shadow)",
|
||||||
|
backdropFilter: "blur(var(--ui-surface-backdrop-blur))"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p className="text-xs uppercase tracking-[var(--tracking-caps)] text-[var(--color-muted-foreground)]">
|
||||||
|
data-skin="{name}"
|
||||||
|
</p>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-xl font-semibold text-[var(--color-foreground)]">
|
||||||
|
{skinDetails[name].label}
|
||||||
|
</h3>
|
||||||
|
<p className="mt-1 text-sm leading-6 text-[var(--color-muted-foreground)]">
|
||||||
|
{description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
className="rounded-full border px-3 py-1 text-xs font-medium text-[var(--color-foreground)]"
|
||||||
|
style={{
|
||||||
|
background: "var(--ui-control-bg)",
|
||||||
|
borderColor: "var(--ui-control-border)",
|
||||||
|
borderRadius: "var(--ui-control-radius)",
|
||||||
|
boxShadow: "var(--ui-control-shadow)"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
phase 1
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-3 sm:grid-cols-2">
|
||||||
|
<div
|
||||||
|
className="relative overflow-hidden border p-4"
|
||||||
|
style={{
|
||||||
|
background: "var(--ui-control-bg)",
|
||||||
|
borderColor: "var(--ui-control-border)",
|
||||||
|
borderRadius: "var(--ui-control-radius)",
|
||||||
|
boxShadow: "var(--ui-control-shadow)"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
className="pointer-events-none absolute inset-x-4 top-0 h-12"
|
||||||
|
style={{
|
||||||
|
background:
|
||||||
|
"linear-gradient(135deg, color-mix(in oklch, var(--color-primary) 24%, transparent), transparent)",
|
||||||
|
mixBlendMode:
|
||||||
|
name === "glass" ? "screen" : name === "pixel" ? "multiply" : "normal",
|
||||||
|
opacity: "var(--ui-ornament-opacity)"
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<p className="text-sm font-medium text-[var(--color-foreground)]">Surface hooks</p>
|
||||||
|
<p className="mt-2 text-sm leading-6 text-[var(--color-muted-foreground)]">
|
||||||
|
This panel reads the new Phase 1 skin variables directly. It is the proof that
|
||||||
|
root or nested `data-skin` scopes are now a stable runtime contract.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="grid gap-3 border p-4"
|
||||||
|
style={{
|
||||||
|
background: "var(--ui-control-bg)",
|
||||||
|
borderColor: "var(--ui-control-border)",
|
||||||
|
borderRadius: "var(--ui-control-radius)",
|
||||||
|
boxShadow: "var(--ui-control-shadow)"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between gap-3">
|
||||||
|
<span className="text-sm font-medium text-[var(--color-foreground)]">
|
||||||
|
Preview controls
|
||||||
|
</span>
|
||||||
|
<Switch checked={enabled} onCheckedChange={setEnabled} />
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
aria-label={`${name} skin search`}
|
||||||
|
defaultValue={skinDetails[name].note}
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
|
<Button variant={name === "glass" ? "secondary" : "primary"}>
|
||||||
|
Same API, future skin target
|
||||||
|
</Button>
|
||||||
|
<Skeleton shape="line" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function StyleContractShowcase({
|
||||||
|
motion,
|
||||||
|
skin,
|
||||||
|
theme
|
||||||
|
}: StyleContractShowcaseProps) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-[var(--color-background)] px-6 py-10 text-[var(--color-foreground)] sm:px-10">
|
||||||
|
<div className="mx-auto flex w-full max-w-6xl flex-col gap-8">
|
||||||
|
<header className="max-w-4xl space-y-4">
|
||||||
|
<p className="text-sm uppercase tracking-[var(--tracking-caps)] text-[var(--color-muted-foreground)]">
|
||||||
|
AI UI / Phase 1
|
||||||
|
</p>
|
||||||
|
<h1
|
||||||
|
className="font-semibold tracking-[var(--tracking-tight)]"
|
||||||
|
style={{
|
||||||
|
fontFamily: "var(--font-display)",
|
||||||
|
fontSize: "var(--text-4xl)",
|
||||||
|
lineHeight: "var(--leading-tight)"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Runtime skin switching is now a first-class docs contract, even before
|
||||||
|
component recipes are extracted.
|
||||||
|
</h1>
|
||||||
|
<p className="max-w-3xl text-[var(--text-lg)] leading-[var(--leading-loose)] text-[var(--color-muted-foreground)]">
|
||||||
|
Phase 1 introduces `data-skin`, root helpers, Storybook toolbar wiring, and a
|
||||||
|
dedicated skin CSS entrypoint. Phase 2 will move component recipes onto this
|
||||||
|
contract.
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section className="flex flex-wrap gap-3">
|
||||||
|
<RuntimeBadge label="theme" value={theme} />
|
||||||
|
<RuntimeBadge label="skin" value={skin} />
|
||||||
|
<RuntimeBadge label="motion" value={motion} />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="grid gap-4 lg:grid-cols-[minmax(0,1.2fr)_minmax(0,0.8fr)]">
|
||||||
|
<article className="rounded-[var(--radius-lg)] border border-[var(--color-border)] bg-[var(--color-card)] p-6 shadow-[var(--shadow-sm)]">
|
||||||
|
<h2 className="text-2xl font-semibold">What Phase 1 includes</h2>
|
||||||
|
<div className="mt-5 grid gap-3">
|
||||||
|
{[
|
||||||
|
"A new runtime attribute: `data-skin`",
|
||||||
|
"Public helpers from `@ai-ui/ui` for skin names, defaults, and root updates",
|
||||||
|
"A dedicated `@ai-ui/ui/skins.css` entrypoint imported by the docs app",
|
||||||
|
"Storybook globals that apply theme, skin, and motion together"
|
||||||
|
].map((item) => (
|
||||||
|
<div
|
||||||
|
key={item}
|
||||||
|
className="rounded-[var(--radius-sm)] border border-[var(--color-border)] bg-[var(--color-background)] px-4 py-3"
|
||||||
|
>
|
||||||
|
<p className="text-sm leading-6 text-[var(--color-foreground)]">{item}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article className="rounded-[var(--radius-lg)] border border-[var(--color-border)] bg-[var(--color-card)] p-6 shadow-[var(--shadow-sm)]">
|
||||||
|
<h2 className="text-2xl font-semibold">What still waits for Phase 2</h2>
|
||||||
|
<div className="mt-5 grid gap-3">
|
||||||
|
{[
|
||||||
|
"Button, card, input, dialog, switch, and skeleton recipe extraction",
|
||||||
|
"Skin-specific component semantic variables such as `--button-*` and `--panel-*`",
|
||||||
|
"A docs comparison page where existing components fully restyle under each skin"
|
||||||
|
].map((item) => (
|
||||||
|
<div
|
||||||
|
key={item}
|
||||||
|
className="rounded-[var(--radius-sm)] border border-[var(--color-border)] bg-[var(--color-background)] px-4 py-3"
|
||||||
|
>
|
||||||
|
<p className="text-sm leading-6 text-[var(--color-foreground)]">{item}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="grid gap-4">
|
||||||
|
{skinNames.map((name) => (
|
||||||
|
<SkinPanel
|
||||||
|
key={name}
|
||||||
|
description={skinDetails[name].note}
|
||||||
|
name={name}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: "Foundation/Style Contract",
|
||||||
|
component: StyleContractShowcase,
|
||||||
|
args: {
|
||||||
|
motion: defaultMotionMode,
|
||||||
|
skin: defaultSkin,
|
||||||
|
theme: defaultTheme
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
component:
|
||||||
|
"Phase 1 adds the runtime style contract. Use the Storybook toolbar to switch the active `theme`, `skin`, and `motion` values globally, or inspect the side-by-side nested `data-skin` panels below."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
render: (_args, context) => (
|
||||||
|
<StyleContractShowcase
|
||||||
|
motion={(context.globals.motion as MotionModeName | undefined) ?? defaultMotionMode}
|
||||||
|
skin={(context.globals.skin as SkinName | undefined) ?? defaultSkin}
|
||||||
|
theme={(context.globals.theme as ThemeName | undefined) ?? defaultTheme}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
} satisfies Meta<typeof StyleContractShowcase>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
export const Overview: Story = {};
|
||||||
@@ -0,0 +1,610 @@
|
|||||||
|
# RFC: Multi-Style Architecture
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Proposed
|
||||||
|
|
||||||
|
## Last Updated
|
||||||
|
|
||||||
|
2026-03-20
|
||||||
|
|
||||||
|
## Why this document exists
|
||||||
|
|
||||||
|
This document records the current plan for making Cadence UI support multiple visual
|
||||||
|
styles without forking component behavior or losing the existing source-owned model.
|
||||||
|
|
||||||
|
It is written as a handoff document. A different agent should be able to read this
|
||||||
|
file, inspect the listed repo files, and continue the work without reconstructing the
|
||||||
|
intent from chat history.
|
||||||
|
|
||||||
|
## Objective
|
||||||
|
|
||||||
|
Support runtime switching across multiple visual styles such as:
|
||||||
|
|
||||||
|
- `minimal`
|
||||||
|
- `glass`
|
||||||
|
- `pixel`
|
||||||
|
|
||||||
|
while preserving:
|
||||||
|
|
||||||
|
- the current public React component APIs
|
||||||
|
- accessibility behavior
|
||||||
|
- reduced-motion support
|
||||||
|
- source ownership inside this repository
|
||||||
|
|
||||||
|
## Summary Decision
|
||||||
|
|
||||||
|
Cadence UI should not treat "style" as a single token pack.
|
||||||
|
|
||||||
|
The working model should be:
|
||||||
|
|
||||||
|
1. `theme`: color, typography, radius, shadow baseline
|
||||||
|
2. `skin`: component appearance recipe
|
||||||
|
3. `motion`: interaction timing and effect vocabulary
|
||||||
|
4. `layout`: page composition patterns, handled outside the base component package
|
||||||
|
|
||||||
|
For the first milestone, this repo should implement `theme + skin + motion`. Layout
|
||||||
|
patterns such as `bento`, `sidebar`, or `magazine` should remain a docs or blocks
|
||||||
|
concern, not a base `@ai-ui/ui` concern.
|
||||||
|
|
||||||
|
## External Reference Synthesis
|
||||||
|
|
||||||
|
The site `https://ui-gallery.codebanana.app/` is useful because it separates style into
|
||||||
|
multiple dimensions:
|
||||||
|
|
||||||
|
- `UI 风格`
|
||||||
|
- `色调`
|
||||||
|
- `字体`
|
||||||
|
- `布局`
|
||||||
|
- `动效`
|
||||||
|
|
||||||
|
That separation matches the conclusion above: styles like `glassmorphism` or
|
||||||
|
`pixel art` are not just color swaps. They change component surfaces, borders,
|
||||||
|
decoration, motion, and sometimes page composition.
|
||||||
|
|
||||||
|
## Current Repo State
|
||||||
|
|
||||||
|
### What already exists
|
||||||
|
|
||||||
|
- `packages/tokens` already defines semantic CSS variables for color, typography,
|
||||||
|
radius, shadow, and motion.
|
||||||
|
- `packages/tokens` already supports multiple themes through `data-theme`.
|
||||||
|
- `packages/tokens` already supports reduced-motion behavior through `data-motion`
|
||||||
|
and `prefers-reduced-motion`.
|
||||||
|
- `packages/ui` already consumes semantic token variables in many component recipes.
|
||||||
|
- `packages/ui` now exposes a public skin contract through `skinNames`, `defaultSkin`,
|
||||||
|
`skinDetails`, and `setSkin`.
|
||||||
|
- `packages/ui` now exports a dedicated `@ai-ui/ui/skins.css` entrypoint.
|
||||||
|
- `apps/docs` already acts as the review surface and imports token CSS globally.
|
||||||
|
- `apps/docs` Storybook globals now apply `theme`, `skin`, and `motion` together.
|
||||||
|
- `apps/docs` now includes a `Foundation/Style Contract` page that documents the Phase 1
|
||||||
|
runtime contract.
|
||||||
|
|
||||||
|
### What does not exist yet
|
||||||
|
|
||||||
|
- No component skin layer exists.
|
||||||
|
- No style provider exists that treats `theme`, `skin`, and `motion` as separate axes.
|
||||||
|
- No existing component family fully restyles across multiple skins yet.
|
||||||
|
|
||||||
|
### Why the repo is not "multi-style" yet
|
||||||
|
|
||||||
|
The current system is multi-theme, not multi-style.
|
||||||
|
|
||||||
|
Many visual decisions are still written directly inside `packages/ui`, for example:
|
||||||
|
|
||||||
|
- button sheen gradient and animation
|
||||||
|
- skeleton shimmer gradient
|
||||||
|
- white switch thumb
|
||||||
|
- fixed rounded forms like `rounded-full`
|
||||||
|
- fixed spacing and size choices that encode a particular visual language
|
||||||
|
|
||||||
|
That means changing `tokens` alone can shift tone, but it cannot reliably produce
|
||||||
|
strongly different styles like `glass` or `pixel`.
|
||||||
|
|
||||||
|
## Read These Files First
|
||||||
|
|
||||||
|
Another agent resuming this work should inspect these files first:
|
||||||
|
|
||||||
|
- `roadmap.md`
|
||||||
|
- `CONTRIBUTING.md`
|
||||||
|
- `packages/tokens/src/index.ts`
|
||||||
|
- `packages/tokens/src/tokens.css`
|
||||||
|
- `packages/tokens/src/motion.css`
|
||||||
|
- `packages/ui/src/lib/skin.ts`
|
||||||
|
- `packages/ui/src/skins.css`
|
||||||
|
- `apps/docs/src/preview.css`
|
||||||
|
- `apps/docs/.storybook/preview.ts`
|
||||||
|
- `apps/docs/src/style-contract.stories.tsx`
|
||||||
|
- `packages/ui/src/lib/motion.ts`
|
||||||
|
- `packages/ui/src/components/button.tsx`
|
||||||
|
- `packages/ui/src/components/button.variants.ts`
|
||||||
|
- `packages/ui/src/components/skeleton.tsx`
|
||||||
|
- `packages/ui/src/components/switch.variants.ts`
|
||||||
|
|
||||||
|
These files capture the current token contract, motion contract, docs wiring, and the
|
||||||
|
best concrete examples of hardcoded visual decisions that block style switching.
|
||||||
|
|
||||||
|
## Working Definitions
|
||||||
|
|
||||||
|
### Theme
|
||||||
|
|
||||||
|
Theme sets the foundational visual baseline:
|
||||||
|
|
||||||
|
- color roles
|
||||||
|
- font families
|
||||||
|
- font scales
|
||||||
|
- radius scale
|
||||||
|
- shadow scale
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
- `editorial`
|
||||||
|
- `minimal`
|
||||||
|
- `dark-luxury`
|
||||||
|
|
||||||
|
### Skin
|
||||||
|
|
||||||
|
Skin defines how components look while keeping structure and behavior stable.
|
||||||
|
|
||||||
|
Skin can change:
|
||||||
|
|
||||||
|
- surface treatment
|
||||||
|
- border style
|
||||||
|
- blur and transparency
|
||||||
|
- decoration layers
|
||||||
|
- contrast level
|
||||||
|
- component-specific gradients
|
||||||
|
- special effects such as sheen or pixelated shadows
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
- `minimal`
|
||||||
|
- `glass`
|
||||||
|
- `pixel`
|
||||||
|
|
||||||
|
### Motion Pack
|
||||||
|
|
||||||
|
Motion pack defines interaction feel:
|
||||||
|
|
||||||
|
- durations
|
||||||
|
- easing
|
||||||
|
- hover feedback
|
||||||
|
- entrance and exit behavior
|
||||||
|
- loading behavior
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
- `calm`
|
||||||
|
- `micro`
|
||||||
|
- `spring`
|
||||||
|
|
||||||
|
### Layout Pattern
|
||||||
|
|
||||||
|
Layout pattern is page composition, not component skin.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
- `bento-grid`
|
||||||
|
- `sidebar-app`
|
||||||
|
- `magazine`
|
||||||
|
- `single-column-longform`
|
||||||
|
|
||||||
|
These should live in docs, blocks, or app-level patterns. They should not be mixed into
|
||||||
|
the core style-switching milestone.
|
||||||
|
|
||||||
|
## Proposed Architecture
|
||||||
|
|
||||||
|
### Layer 1: Semantic Foundation Tokens
|
||||||
|
|
||||||
|
Keep `packages/tokens` as the owner of global semantic variables.
|
||||||
|
|
||||||
|
Do not split multiple token packages yet. Keep a single package with multiple themes
|
||||||
|
until the contract is stable.
|
||||||
|
|
||||||
|
The existing token groups remain the baseline:
|
||||||
|
|
||||||
|
- `--color-*`
|
||||||
|
- `--font-*`
|
||||||
|
- `--text-*`
|
||||||
|
- `--radius-*`
|
||||||
|
- `--shadow-*`
|
||||||
|
- `--dur-*`
|
||||||
|
- `--ease-*`
|
||||||
|
- `--distance-*`
|
||||||
|
- `--scale-*`
|
||||||
|
|
||||||
|
### Layer 2: Skin Contract
|
||||||
|
|
||||||
|
Add a new root attribute:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<html data-theme="minimal" data-skin="glass" data-motion="micro">
|
||||||
|
```
|
||||||
|
|
||||||
|
`data-skin` should be the runtime contract for component appearance.
|
||||||
|
|
||||||
|
Recommended initial values:
|
||||||
|
|
||||||
|
- `minimal`
|
||||||
|
- `glass`
|
||||||
|
- `pixel`
|
||||||
|
|
||||||
|
### Layer 3: Component Semantic Style Variables
|
||||||
|
|
||||||
|
The current repo uses global semantic tokens directly in many components. That is good
|
||||||
|
for theme switching, but it is not enough for skin switching.
|
||||||
|
|
||||||
|
The next step should be introducing component-semantic variables or skin hooks for the
|
||||||
|
highest-value components first.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
- button:
|
||||||
|
- `--button-bg`
|
||||||
|
- `--button-border`
|
||||||
|
- `--button-fg`
|
||||||
|
- `--button-shadow`
|
||||||
|
- `--button-sheen-opacity`
|
||||||
|
- card:
|
||||||
|
- `--card-bg`
|
||||||
|
- `--card-border`
|
||||||
|
- `--card-shadow`
|
||||||
|
- input:
|
||||||
|
- `--input-bg`
|
||||||
|
- `--input-border`
|
||||||
|
- `--input-shadow`
|
||||||
|
- dialog or popover:
|
||||||
|
- `--panel-bg`
|
||||||
|
- `--panel-border`
|
||||||
|
- `--panel-shadow`
|
||||||
|
- `--panel-backdrop-blur`
|
||||||
|
- switch:
|
||||||
|
- `--switch-track-bg`
|
||||||
|
- `--switch-thumb-bg`
|
||||||
|
|
||||||
|
The rule is:
|
||||||
|
|
||||||
|
- global semantic tokens define product-wide roles
|
||||||
|
- component semantic variables define component appearance
|
||||||
|
- skins map global tokens to component variables
|
||||||
|
|
||||||
|
### Layer 4: Skin CSS Ownership
|
||||||
|
|
||||||
|
The recommended ownership split is:
|
||||||
|
|
||||||
|
- `packages/tokens` owns global theme and motion variables
|
||||||
|
- `packages/ui` owns component skin CSS
|
||||||
|
|
||||||
|
Reason:
|
||||||
|
|
||||||
|
- `theme` is a design-system baseline concern
|
||||||
|
- `skin` is a component recipe concern
|
||||||
|
|
||||||
|
That means the likely future CSS imports are:
|
||||||
|
|
||||||
|
```css
|
||||||
|
@import "@ai-ui/tokens/styles.css";
|
||||||
|
@import "@ai-ui/ui/skins.css";
|
||||||
|
```
|
||||||
|
|
||||||
|
Consumer ergonomics can later be improved by providing a convenience entry such as:
|
||||||
|
|
||||||
|
```css
|
||||||
|
@import "@ai-ui/ui/styles.css";
|
||||||
|
```
|
||||||
|
|
||||||
|
where `@ai-ui/ui/styles.css` re-exports both token and skin styles.
|
||||||
|
|
||||||
|
## Implementation Rules
|
||||||
|
|
||||||
|
### Rule 1: Preserve behavior and API
|
||||||
|
|
||||||
|
Do not fork component logic per skin.
|
||||||
|
|
||||||
|
Skins should change appearance, not:
|
||||||
|
|
||||||
|
- props
|
||||||
|
- ARIA behavior
|
||||||
|
- controlled or uncontrolled patterns
|
||||||
|
- slot naming
|
||||||
|
- `data-*` state contract
|
||||||
|
|
||||||
|
### Rule 2: Separate structure from appearance
|
||||||
|
|
||||||
|
Leave these in component code:
|
||||||
|
|
||||||
|
- layout structure
|
||||||
|
- sizing variants
|
||||||
|
- slot and state attributes
|
||||||
|
- accessibility and interaction logic
|
||||||
|
|
||||||
|
Move these toward skin hooks:
|
||||||
|
|
||||||
|
- surface fill
|
||||||
|
- border treatment
|
||||||
|
- shadow treatment
|
||||||
|
- blur
|
||||||
|
- decoration layers
|
||||||
|
- visual effect toggles
|
||||||
|
|
||||||
|
### Rule 3: Do not put layout patterns into the skin milestone
|
||||||
|
|
||||||
|
Page layouts are a separate concern and should not block the component skin work.
|
||||||
|
|
||||||
|
### Rule 4: Default gracefully
|
||||||
|
|
||||||
|
If a consumer does not specify `data-skin`, the system should default to the current
|
||||||
|
closest appearance, likely `minimal`.
|
||||||
|
|
||||||
|
### Rule 5: Reduced motion remains authoritative
|
||||||
|
|
||||||
|
No skin may bypass reduced-motion expectations. `data-motion` and
|
||||||
|
`prefers-reduced-motion` still govern the final motion behavior.
|
||||||
|
|
||||||
|
## Recommended First Slice
|
||||||
|
|
||||||
|
Do not start by updating every component.
|
||||||
|
|
||||||
|
Start with a focused pilot set that proves the architecture:
|
||||||
|
|
||||||
|
- `Button`
|
||||||
|
- `Card`
|
||||||
|
- `Input`
|
||||||
|
- `Dialog`
|
||||||
|
- `Switch`
|
||||||
|
- `Skeleton`
|
||||||
|
|
||||||
|
These are enough to validate:
|
||||||
|
|
||||||
|
- basic surfaces
|
||||||
|
- overlays
|
||||||
|
- focus treatments
|
||||||
|
- loading affordances
|
||||||
|
- special effects
|
||||||
|
|
||||||
|
## Suggested Skin Definitions
|
||||||
|
|
||||||
|
### Minimal
|
||||||
|
|
||||||
|
Intent:
|
||||||
|
|
||||||
|
- remove ornamental effects
|
||||||
|
- keep surfaces solid
|
||||||
|
- keep shadows subtle
|
||||||
|
- prefer clarity over decoration
|
||||||
|
|
||||||
|
Expected traits:
|
||||||
|
|
||||||
|
- no sheen
|
||||||
|
- no blur
|
||||||
|
- low-contrast borders
|
||||||
|
- restrained shadows
|
||||||
|
|
||||||
|
### Glass
|
||||||
|
|
||||||
|
Intent:
|
||||||
|
|
||||||
|
- translucent layers
|
||||||
|
- blurred panels
|
||||||
|
- floating surfaces
|
||||||
|
- higher use of overlays and edge highlights
|
||||||
|
|
||||||
|
Expected traits:
|
||||||
|
|
||||||
|
- semi-transparent surfaces
|
||||||
|
- stronger border highlights
|
||||||
|
- backdrop blur on floating panels
|
||||||
|
- controlled glow or sheen on selected components
|
||||||
|
|
||||||
|
### Pixel
|
||||||
|
|
||||||
|
Intent:
|
||||||
|
|
||||||
|
- square geometry
|
||||||
|
- hard edges
|
||||||
|
- crisp borders
|
||||||
|
- step-like shadow language
|
||||||
|
|
||||||
|
Expected traits:
|
||||||
|
|
||||||
|
- zero or near-zero radius
|
||||||
|
- no blur
|
||||||
|
- hard shadows
|
||||||
|
- no soft sheen
|
||||||
|
- sharper, less fluid motion
|
||||||
|
|
||||||
|
## Suggested Runtime API
|
||||||
|
|
||||||
|
The end state should support runtime switching without remounting components.
|
||||||
|
|
||||||
|
Minimum contract:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
type ThemeName = "light" | "dark" | "brand" | "minimal";
|
||||||
|
type SkinName = "minimal" | "glass" | "pixel";
|
||||||
|
type MotionName = "system" | "reduced" | "micro" | "spring";
|
||||||
|
```
|
||||||
|
|
||||||
|
Likely helpers:
|
||||||
|
|
||||||
|
- `setTheme(theme, root?)`
|
||||||
|
- `setSkin(skin, root?)`
|
||||||
|
- `setMotionMode(mode, root?)`
|
||||||
|
|
||||||
|
Provider shape if needed:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<StyleProvider theme="minimal" skin="glass" motion="micro">
|
||||||
|
<App />
|
||||||
|
</StyleProvider>
|
||||||
|
```
|
||||||
|
|
||||||
|
This provider is optional. Direct root attributes are acceptable if they keep the system
|
||||||
|
simple.
|
||||||
|
|
||||||
|
## Delivery Plan
|
||||||
|
|
||||||
|
### Phase 0: Analysis and Contract Draft
|
||||||
|
|
||||||
|
Goal:
|
||||||
|
|
||||||
|
- document the problem clearly
|
||||||
|
- separate `theme`, `skin`, `motion`, and `layout`
|
||||||
|
- identify which components prove the architecture
|
||||||
|
|
||||||
|
Deliverables:
|
||||||
|
|
||||||
|
- this RFC
|
||||||
|
- explicit initial skin names
|
||||||
|
- initial pilot component list
|
||||||
|
|
||||||
|
Status:
|
||||||
|
|
||||||
|
- completed
|
||||||
|
|
||||||
|
### Phase 1: Runtime Skin Contract
|
||||||
|
|
||||||
|
Goal:
|
||||||
|
|
||||||
|
- introduce `data-skin`
|
||||||
|
- define `skinNames` and related helpers
|
||||||
|
- decide final CSS file ownership and export path
|
||||||
|
|
||||||
|
Deliverables:
|
||||||
|
|
||||||
|
- root skin attribute contract
|
||||||
|
- helper API
|
||||||
|
- docs wiring for skin switching
|
||||||
|
|
||||||
|
Status:
|
||||||
|
|
||||||
|
- completed
|
||||||
|
|
||||||
|
### Phase 2: Pilot Skin Extraction
|
||||||
|
|
||||||
|
Goal:
|
||||||
|
|
||||||
|
- extract hardcoded visual decisions from the pilot components into skin-aware hooks
|
||||||
|
|
||||||
|
Deliverables:
|
||||||
|
|
||||||
|
- `Button`, `Card`, `Input`, `Dialog`, `Switch`, and `Skeleton` render distinctly under
|
||||||
|
`minimal`, `glass`, and `pixel`
|
||||||
|
- no public component API breakage
|
||||||
|
|
||||||
|
Status:
|
||||||
|
|
||||||
|
- not started
|
||||||
|
|
||||||
|
### Phase 3: Docs Validation Surface
|
||||||
|
|
||||||
|
Goal:
|
||||||
|
|
||||||
|
- create a docs page or stories that compare the same components across all supported
|
||||||
|
skins and motion modes
|
||||||
|
|
||||||
|
Deliverables:
|
||||||
|
|
||||||
|
- a stable review surface in `apps/docs`
|
||||||
|
- screenshot-friendly comparisons
|
||||||
|
- clear regression target for future work
|
||||||
|
|
||||||
|
Status:
|
||||||
|
|
||||||
|
- not started
|
||||||
|
|
||||||
|
### Phase 4: Expand Coverage
|
||||||
|
|
||||||
|
Goal:
|
||||||
|
|
||||||
|
- apply the same pattern to the rest of the component library
|
||||||
|
|
||||||
|
Priority order:
|
||||||
|
|
||||||
|
- form controls
|
||||||
|
- overlays
|
||||||
|
- navigation and menus
|
||||||
|
- feedback components
|
||||||
|
- advanced patterns such as `DataTable`
|
||||||
|
|
||||||
|
Status:
|
||||||
|
|
||||||
|
- not started
|
||||||
|
|
||||||
|
### Phase 5: Packaging and Consumer Ergonomics
|
||||||
|
|
||||||
|
Goal:
|
||||||
|
|
||||||
|
- make skin CSS consumable outside this repo without forcing consumers to understand the
|
||||||
|
internal file graph
|
||||||
|
|
||||||
|
Deliverables:
|
||||||
|
|
||||||
|
- final CSS import path
|
||||||
|
- final package exports
|
||||||
|
- install guidance in release and registry docs
|
||||||
|
|
||||||
|
Status:
|
||||||
|
|
||||||
|
- not started
|
||||||
|
|
||||||
|
## Acceptance Criteria For The First Real Milestone
|
||||||
|
|
||||||
|
The first milestone should be considered complete only when all of these are true:
|
||||||
|
|
||||||
|
- the same `Button`, `Card`, `Input`, `Dialog`, `Switch`, and `Skeleton` instances can
|
||||||
|
switch across `minimal`, `glass`, and `pixel`
|
||||||
|
- switching uses root attributes, not per-component prop forks
|
||||||
|
- reduced motion still works correctly
|
||||||
|
- Storybook or docs demonstrate the comparison clearly
|
||||||
|
- no public component APIs were broken to achieve the visual switching
|
||||||
|
|
||||||
|
## Risks
|
||||||
|
|
||||||
|
### Risk 1: Tailwind classes currently mix structure and appearance
|
||||||
|
|
||||||
|
This will make extraction repetitive. The work must stay disciplined and not turn into
|
||||||
|
ad hoc component rewrites.
|
||||||
|
|
||||||
|
### Risk 2: Some skins may require more than variables
|
||||||
|
|
||||||
|
`glass` and `pixel` may need extra selectors, pseudo-elements, or special wrapper
|
||||||
|
classes. That is acceptable, but those decisions must remain in the skin layer.
|
||||||
|
|
||||||
|
### Risk 3: Layout can derail the effort
|
||||||
|
|
||||||
|
The external reference site includes layout categories. If layout gets pulled into the
|
||||||
|
first milestone, the implementation scope will expand too quickly.
|
||||||
|
|
||||||
|
### Risk 4: Packaging decisions can arrive too early
|
||||||
|
|
||||||
|
Do not split multiple npm packages for separate skins until the internal contract is
|
||||||
|
proven inside this repo.
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
|
||||||
|
- Should `skin` helpers live in `@ai-ui/tokens` or `@ai-ui/ui`? The current
|
||||||
|
recommendation is `@ai-ui/ui`.
|
||||||
|
- Should the runtime API expose a `StyleProvider`, or should root attributes remain the
|
||||||
|
only public contract at first?
|
||||||
|
- Should motion packs beyond `system` and `reduced` ship in the first skin milestone, or
|
||||||
|
should additional motion packs wait until after the pilot components land?
|
||||||
|
- Should `minimal` become the new default appearance, or should the current warm
|
||||||
|
editorial look remain the default and be renamed explicitly?
|
||||||
|
|
||||||
|
## Current Completion Snapshot
|
||||||
|
|
||||||
|
As of 2026-03-20, the project is at this point:
|
||||||
|
|
||||||
|
- problem analysis completed
|
||||||
|
- architecture direction chosen
|
||||||
|
- scope boundaries chosen
|
||||||
|
- phase 1 runtime skin contract completed
|
||||||
|
- `@ai-ui/ui/skins.css` export added
|
||||||
|
- Storybook globals now switch `skin`
|
||||||
|
- docs switching surface added in `Foundation/Style Contract`
|
||||||
|
- no component extraction started
|
||||||
|
- no broad skin-aware component recipes implemented yet
|
||||||
|
|
||||||
|
The next implementation task should be Phase 2: pilot skin extraction for
|
||||||
|
`Button`, `Card`, `Input`, `Dialog`, `Switch`, and `Skeleton`.
|
||||||
@@ -3,8 +3,12 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
"sideEffects": [
|
||||||
|
"**/*.css"
|
||||||
|
],
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./src/index.ts"
|
".": "./src/index.ts",
|
||||||
|
"./skins.css": "./src/skins.css"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist",
|
"dist",
|
||||||
|
|||||||
@@ -401,3 +401,10 @@ export {
|
|||||||
motionScales,
|
motionScales,
|
||||||
type MotionRecipeName
|
type MotionRecipeName
|
||||||
} from "./lib/motion";
|
} from "./lib/motion";
|
||||||
|
export {
|
||||||
|
defaultSkin,
|
||||||
|
setSkin,
|
||||||
|
skinDetails,
|
||||||
|
skinNames,
|
||||||
|
type SkinName
|
||||||
|
} from "./lib/skin";
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import { defaultSkin, setSkin, skinDetails, skinNames } from "./skin";
|
||||||
|
|
||||||
|
describe("skin contract", () => {
|
||||||
|
it("exposes a default skin that exists in the public name set", () => {
|
||||||
|
expect(skinNames).toContain(defaultSkin);
|
||||||
|
expect(skinDetails[defaultSkin].label).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sets the document root skin when no target element is provided", () => {
|
||||||
|
setSkin("glass");
|
||||||
|
|
||||||
|
expect(document.documentElement.dataset.skin).toBe("glass");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sets the provided target element instead of the document root", () => {
|
||||||
|
const target = document.createElement("div");
|
||||||
|
|
||||||
|
setSkin("pixel", target);
|
||||||
|
|
||||||
|
expect(target.dataset.skin).toBe("pixel");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
export const skinNames = ["minimal", "glass", "pixel"] as const;
|
||||||
|
export type SkinName = (typeof skinNames)[number];
|
||||||
|
|
||||||
|
export const defaultSkin: SkinName = "minimal";
|
||||||
|
|
||||||
|
export const skinDetails = {
|
||||||
|
minimal: {
|
||||||
|
label: "Minimal",
|
||||||
|
note: "Restrained surfaces and low-ornament defaults"
|
||||||
|
},
|
||||||
|
glass: {
|
||||||
|
label: "Glass",
|
||||||
|
note: "Translucent layers, brighter edges, and blurred panels"
|
||||||
|
},
|
||||||
|
pixel: {
|
||||||
|
label: "Pixel",
|
||||||
|
note: "Hard edges, crisp borders, and stepped shadows"
|
||||||
|
}
|
||||||
|
} as const satisfies Record<SkinName, { label: string; note: string }>;
|
||||||
|
|
||||||
|
function getTargetElement(root?: HTMLElement) {
|
||||||
|
if (root) {
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof document === "undefined") {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return document.documentElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setSkin(skin: SkinName, root?: HTMLElement) {
|
||||||
|
const target = getTargetElement(root);
|
||||||
|
|
||||||
|
if (!target) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
target.dataset.skin = skin;
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
:root,
|
||||||
|
[data-skin="minimal"] {
|
||||||
|
--ui-canvas-image: radial-gradient(
|
||||||
|
circle at top,
|
||||||
|
color-mix(in oklch, var(--color-primary) 8%, transparent),
|
||||||
|
transparent 58%
|
||||||
|
);
|
||||||
|
--ui-canvas-size: auto;
|
||||||
|
--ui-surface-bg: color-mix(in oklch, var(--color-card) 88%, white 12%);
|
||||||
|
--ui-surface-border: color-mix(in oklch, var(--color-border) 92%, white 8%);
|
||||||
|
--ui-surface-shadow: var(--shadow-sm);
|
||||||
|
--ui-surface-radius: var(--radius-lg);
|
||||||
|
--ui-surface-backdrop-blur: 0px;
|
||||||
|
--ui-control-bg: color-mix(in oklch, var(--color-background) 92%, white 8%);
|
||||||
|
--ui-control-border: var(--color-border);
|
||||||
|
--ui-control-shadow: var(--shadow-xs);
|
||||||
|
--ui-control-radius: var(--radius-md);
|
||||||
|
--ui-ornament-opacity: 0.1;
|
||||||
|
--ui-ornament-mix: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-skin="glass"] {
|
||||||
|
--ui-canvas-image:
|
||||||
|
radial-gradient(
|
||||||
|
circle at top left,
|
||||||
|
color-mix(in oklch, var(--color-primary) 22%, transparent),
|
||||||
|
transparent 42%
|
||||||
|
),
|
||||||
|
radial-gradient(
|
||||||
|
circle at top right,
|
||||||
|
color-mix(in oklch, var(--color-accent) 18%, transparent),
|
||||||
|
transparent 48%
|
||||||
|
),
|
||||||
|
linear-gradient(
|
||||||
|
180deg,
|
||||||
|
color-mix(in oklch, var(--color-background) 64%, white 36%),
|
||||||
|
var(--color-background)
|
||||||
|
);
|
||||||
|
--ui-canvas-size: auto;
|
||||||
|
--ui-surface-bg: color-mix(in oklch, var(--color-card) 58%, transparent);
|
||||||
|
--ui-surface-border: color-mix(in oklch, white 46%, var(--color-border));
|
||||||
|
--ui-surface-shadow: 0 24px 64px oklch(0.18 0.03 255 / 0.18);
|
||||||
|
--ui-surface-radius: var(--radius-xl);
|
||||||
|
--ui-surface-backdrop-blur: 20px;
|
||||||
|
--ui-control-bg: color-mix(in oklch, var(--color-card) 52%, transparent);
|
||||||
|
--ui-control-border: color-mix(in oklch, white 36%, var(--color-border-strong));
|
||||||
|
--ui-control-shadow: 0 14px 38px oklch(0.2 0.03 255 / 0.14);
|
||||||
|
--ui-control-radius: var(--radius-lg);
|
||||||
|
--ui-ornament-opacity: 0.36;
|
||||||
|
--ui-ornament-mix: screen;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-skin="pixel"] {
|
||||||
|
--ui-canvas-image:
|
||||||
|
linear-gradient(
|
||||||
|
90deg,
|
||||||
|
color-mix(in oklch, var(--color-foreground) 7%, transparent) 1px,
|
||||||
|
transparent 1px
|
||||||
|
),
|
||||||
|
linear-gradient(
|
||||||
|
180deg,
|
||||||
|
color-mix(in oklch, var(--color-foreground) 7%, transparent) 1px,
|
||||||
|
transparent 1px
|
||||||
|
),
|
||||||
|
linear-gradient(
|
||||||
|
180deg,
|
||||||
|
color-mix(in oklch, var(--color-background) 92%, black 8%),
|
||||||
|
var(--color-background)
|
||||||
|
);
|
||||||
|
--ui-canvas-size: 12px 12px, 12px 12px, auto;
|
||||||
|
--ui-surface-bg: color-mix(in oklch, var(--color-card) 96%, white 4%);
|
||||||
|
--ui-surface-border: var(--color-foreground);
|
||||||
|
--ui-surface-shadow: 6px 6px 0 color-mix(in oklch, var(--color-foreground) 38%, transparent);
|
||||||
|
--ui-surface-radius: 0px;
|
||||||
|
--ui-surface-backdrop-blur: 0px;
|
||||||
|
--ui-control-bg: var(--color-background);
|
||||||
|
--ui-control-border: var(--color-foreground);
|
||||||
|
--ui-control-shadow: 3px 3px 0 color-mix(in oklch, var(--color-foreground) 30%, transparent);
|
||||||
|
--ui-control-radius: 0px;
|
||||||
|
--ui-ornament-opacity: 0.2;
|
||||||
|
--ui-ornament-mix: multiply;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user