diff --git a/apps/docs/.storybook/preview.ts b/apps/docs/.storybook/preview.ts index 6948ffd..5a78e0c 100644 --- a/apps/docs/.storybook/preview.ts +++ b/apps/docs/.storybook/preview.ts @@ -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(); diff --git a/apps/docs/src/preview.css b/apps/docs/src/preview.css index bdbd419..de6f96a 100644 --- a/apps/docs/src/preview.css +++ b/apps/docs/src/preview.css @@ -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); } diff --git a/apps/docs/src/style-contract.stories.tsx b/apps/docs/src/style-contract.stories.tsx new file mode 100644 index 0000000..befb3b8 --- /dev/null +++ b/apps/docs/src/style-contract.stories.tsx @@ -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 ( +
+ {label} + {value} +
+ ); +} + +function SkinPanel({ + description, + name +}: { + description: string; + name: SkinName; +}) { + const [enabled, setEnabled] = useState(name !== "minimal"); + + return ( +
+
+
+

+ data-skin="{name}" +

+
+

+ {skinDetails[name].label} +

+

+ {description} +

+
+
+ + phase 1 + +
+ +
+
+ + +
+
+ + Preview controls + + +
+ + + +
+
+
+ ); +} + +function StyleContractShowcase({ + motion, + skin, + theme +}: StyleContractShowcaseProps) { + return ( +
+
+
+

+ AI UI / Phase 1 +

+

+ Runtime skin switching is now a first-class docs contract, even before + component recipes are extracted. +

+

+ 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. +

+
+ +
+ + + +
+ +
+
+

What Phase 1 includes

+
+ {[ + "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) => ( +
+

{item}

+
+ ))} +
+
+ +
+

What still waits for Phase 2

+
+ {[ + "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) => ( +
+

{item}

+
+ ))} +
+
+
+ +
+ {skinNames.map((name) => ( + + ))} +
+
+
+ ); +} + +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) => ( + + ) +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Overview: Story = {}; diff --git a/docs/rfcs/multi-style-architecture.md b/docs/rfcs/multi-style-architecture.md new file mode 100644 index 0000000..abdc062 --- /dev/null +++ b/docs/rfcs/multi-style-architecture.md @@ -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 + +``` + +`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 + + + +``` + +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`. diff --git a/packages/ui/package.json b/packages/ui/package.json index f7861a5..871c308 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -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", diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index 6db3d8b..f2d4395 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -401,3 +401,10 @@ export { motionScales, type MotionRecipeName } from "./lib/motion"; +export { + defaultSkin, + setSkin, + skinDetails, + skinNames, + type SkinName +} from "./lib/skin"; diff --git a/packages/ui/src/lib/skin.test.ts b/packages/ui/src/lib/skin.test.ts new file mode 100644 index 0000000..6dd1d5c --- /dev/null +++ b/packages/ui/src/lib/skin.test.ts @@ -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"); + }); +}); diff --git a/packages/ui/src/lib/skin.ts b/packages/ui/src/lib/skin.ts new file mode 100644 index 0000000..6b462bd --- /dev/null +++ b/packages/ui/src/lib/skin.ts @@ -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; + +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; +} diff --git a/packages/ui/src/skins.css b/packages/ui/src/skins.css new file mode 100644 index 0000000..4cf982d --- /dev/null +++ b/packages/ui/src/skins.css @@ -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; +}