Add runtime skin contract and docs

This commit is contained in:
2026-03-20 11:22:03 +08:00
parent 5c9eb84c63
commit 246851a68e
9 changed files with 1053 additions and 1 deletions
+20
View File
@@ -1,6 +1,12 @@
import "../src/preview.css";
import type { Preview } from "@storybook/react";
import {
defaultSkin,
setSkin,
skinDetails,
skinNames
} from "@ai-ui/ui";
import {
defaultMotionMode,
defaultTheme,
@@ -34,10 +40,22 @@ const preview: Preview = {
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: {
motion: defaultMotionMode,
skin: defaultSkin,
theme: defaultTheme
},
parameters: {
@@ -63,8 +81,10 @@ const preview: Preview = {
if (typeof document !== "undefined") {
setTheme(context.globals.theme ?? defaultTheme);
setMotionMode(context.globals.motion ?? defaultMotionMode);
setSkin(context.globals.skin ?? defaultSkin);
document.body.dataset.theme = context.globals.theme ?? defaultTheme;
document.body.dataset.skin = context.globals.skin ?? defaultSkin;
}
return Story();
+4
View File
@@ -1,5 +1,6 @@
@import "tailwindcss";
@import "@ai-ui/tokens/styles.css";
@import "@ai-ui/ui/skins.css";
@source "../../../packages/ui/src";
:root {
@@ -14,5 +15,8 @@ body,
body {
background: var(--color-background);
background-image: var(--ui-canvas-image);
background-size: var(--ui-canvas-size);
background-attachment: fixed;
color: var(--color-foreground);
}
+260
View File
@@ -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=&quot;{name}&quot;
</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 = {};
+610
View File
@@ -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`.
+5 -1
View File
@@ -3,8 +3,12 @@
"version": "0.0.0",
"private": true,
"type": "module",
"sideEffects": [
"**/*.css"
],
"exports": {
".": "./src/index.ts"
".": "./src/index.ts",
"./skins.css": "./src/skins.css"
},
"files": [
"dist",
+7
View File
@@ -401,3 +401,10 @@ export {
motionScales,
type MotionRecipeName
} from "./lib/motion";
export {
defaultSkin,
setSkin,
skinDetails,
skinNames,
type SkinName
} from "./lib/skin";
+24
View File
@@ -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");
});
});
+41
View File
@@ -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;
}
+82
View File
@@ -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;
}