feat: add token system and ui contracts
This commit is contained in:
@@ -6,6 +6,7 @@
|
||||
|
||||
html {
|
||||
color-scheme: light;
|
||||
background: var(--color-background);
|
||||
}
|
||||
|
||||
body {
|
||||
@@ -14,6 +15,8 @@ body {
|
||||
background: var(--color-background);
|
||||
color: var(--color-foreground);
|
||||
font-family: var(--font-sans);
|
||||
font-size: var(--text-base);
|
||||
line-height: var(--leading-normal);
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
@@ -24,3 +27,10 @@ select {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
::selection {
|
||||
background: color-mix(in oklch, var(--color-primary) 24%, transparent);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,176 @@
|
||||
export const themeNames = ["light", "dark"] as const;
|
||||
export const themeNames = ["light", "dark", "brand"] as const;
|
||||
export type ThemeName = (typeof themeNames)[number];
|
||||
|
||||
export const defaultTheme: ThemeName = "light";
|
||||
|
||||
export const themeDetails = {
|
||||
light: {
|
||||
label: "Light",
|
||||
note: "Warm editorial default"
|
||||
},
|
||||
dark: {
|
||||
label: "Dark",
|
||||
note: "Warm charcoal default"
|
||||
},
|
||||
brand: {
|
||||
label: "Brand",
|
||||
note: "Verdant accent scaffold"
|
||||
}
|
||||
} as const satisfies Record<ThemeName, { label: string; note: string }>;
|
||||
|
||||
export const motionModeNames = ["system", "reduced"] as const;
|
||||
export type MotionModeName = (typeof motionModeNames)[number];
|
||||
|
||||
export const defaultMotionMode: MotionModeName = "system";
|
||||
|
||||
export const motionScale = {
|
||||
instant: "var(--dur-instant)",
|
||||
fast: "var(--dur-fast)",
|
||||
base: "var(--dur-base)",
|
||||
slow: "var(--dur-slow)"
|
||||
slow: "var(--dur-slow)",
|
||||
deliberate: "var(--dur-deliberate)"
|
||||
} as const;
|
||||
|
||||
export const colorTokens = [
|
||||
{ name: "background", cssVar: "--color-background", role: "Application canvas" },
|
||||
{ name: "foreground", cssVar: "--color-foreground", role: "Primary text and icons" },
|
||||
{ name: "surface", cssVar: "--color-surface", role: "Secondary surface backgrounds" },
|
||||
{
|
||||
name: "surface-strong",
|
||||
cssVar: "--color-surface-strong",
|
||||
role: "Elevated surface emphasis"
|
||||
},
|
||||
{ name: "card", cssVar: "--color-card", role: "Cards and floating panels" },
|
||||
{ name: "border", cssVar: "--color-border", role: "Default dividers and input borders" },
|
||||
{
|
||||
name: "border-strong",
|
||||
cssVar: "--color-border-strong",
|
||||
role: "Higher emphasis dividers"
|
||||
},
|
||||
{ name: "primary", cssVar: "--color-primary", role: "Primary actions and highlights" },
|
||||
{
|
||||
name: "secondary",
|
||||
cssVar: "--color-secondary",
|
||||
role: "Secondary fills and supporting actions"
|
||||
},
|
||||
{ name: "muted", cssVar: "--color-muted", role: "Subtle supporting surfaces" },
|
||||
{
|
||||
name: "muted-foreground",
|
||||
cssVar: "--color-muted-foreground",
|
||||
role: "Secondary text and captions"
|
||||
},
|
||||
{ name: "accent", cssVar: "--color-accent", role: "Moments of emphasis or delight" },
|
||||
{ name: "success", cssVar: "--color-success", role: "Success feedback" },
|
||||
{ name: "warning", cssVar: "--color-warning", role: "Warning feedback" },
|
||||
{ name: "destructive", cssVar: "--color-destructive", role: "Destructive actions" }
|
||||
] as const;
|
||||
|
||||
export const typographyTokens = [
|
||||
{
|
||||
name: "caption",
|
||||
fontVar: "--text-xs",
|
||||
lineHeightVar: "--leading-normal",
|
||||
familyVar: "--font-sans",
|
||||
sample: "Small labels, metadata, and supporting notes."
|
||||
},
|
||||
{
|
||||
name: "body",
|
||||
fontVar: "--text-base",
|
||||
lineHeightVar: "--leading-normal",
|
||||
familyVar: "--font-sans",
|
||||
sample: "Body copy stays warm, readable, and stable across themes."
|
||||
},
|
||||
{
|
||||
name: "lead",
|
||||
fontVar: "--text-xl",
|
||||
lineHeightVar: "--leading-loose",
|
||||
familyVar: "--font-sans",
|
||||
sample: "Lead text introduces a surface without becoming display copy."
|
||||
},
|
||||
{
|
||||
name: "display",
|
||||
fontVar: "--text-4xl",
|
||||
lineHeightVar: "--leading-tight",
|
||||
familyVar: "--font-display",
|
||||
sample: "Display text carries the editorial voice of the system."
|
||||
}
|
||||
] as const;
|
||||
|
||||
export const radiusTokens = [
|
||||
{ name: "xs", cssVar: "--radius-xs" },
|
||||
{ name: "sm", cssVar: "--radius-sm" },
|
||||
{ name: "md", cssVar: "--radius-md" },
|
||||
{ name: "lg", cssVar: "--radius-lg" },
|
||||
{ name: "xl", cssVar: "--radius-xl" },
|
||||
{ name: "full", cssVar: "--radius-full" }
|
||||
] as const;
|
||||
|
||||
export const shadowTokens = [
|
||||
{ name: "xs", cssVar: "--shadow-xs" },
|
||||
{ name: "sm", cssVar: "--shadow-sm" },
|
||||
{ name: "md", cssVar: "--shadow-md" },
|
||||
{ name: "lg", cssVar: "--shadow-lg" }
|
||||
] as const;
|
||||
|
||||
export const motionTokens = {
|
||||
durations: [
|
||||
{ name: "instant", cssVar: "--dur-instant" },
|
||||
{ name: "fast", cssVar: "--dur-fast" },
|
||||
{ name: "base", cssVar: "--dur-base" },
|
||||
{ name: "slow", cssVar: "--dur-slow" },
|
||||
{ name: "deliberate", cssVar: "--dur-deliberate" }
|
||||
],
|
||||
easings: [
|
||||
{ name: "standard", cssVar: "--ease-standard" },
|
||||
{ name: "emphasized", cssVar: "--ease-emphasized" },
|
||||
{ name: "exit", cssVar: "--ease-exit" }
|
||||
],
|
||||
distances: [
|
||||
{ name: "xs", cssVar: "--distance-xs" },
|
||||
{ name: "sm", cssVar: "--distance-sm" },
|
||||
{ name: "md", cssVar: "--distance-md" },
|
||||
{ name: "lg", cssVar: "--distance-lg" }
|
||||
],
|
||||
scales: [
|
||||
{ name: "press", cssVar: "--scale-press" },
|
||||
{ name: "hover", cssVar: "--scale-hover" },
|
||||
{ name: "pop", cssVar: "--scale-pop" }
|
||||
]
|
||||
} as const;
|
||||
|
||||
function getTargetElement(root?: HTMLElement) {
|
||||
if (root) {
|
||||
return root;
|
||||
}
|
||||
|
||||
if (typeof document === "undefined") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return document.documentElement;
|
||||
}
|
||||
|
||||
export function setTheme(theme: ThemeName, root?: HTMLElement) {
|
||||
const target = getTargetElement(root);
|
||||
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
|
||||
target.dataset.theme = theme;
|
||||
}
|
||||
|
||||
export function setMotionMode(mode: MotionModeName, root?: HTMLElement) {
|
||||
const target = getTargetElement(root);
|
||||
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mode === "system") {
|
||||
delete target.dataset.motion;
|
||||
return;
|
||||
}
|
||||
|
||||
target.dataset.motion = mode;
|
||||
}
|
||||
|
||||
@@ -1,28 +1,62 @@
|
||||
:root {
|
||||
--dur-instant: 1ms;
|
||||
--dur-fast: 120ms;
|
||||
--dur-base: 200ms;
|
||||
--dur-slow: 320ms;
|
||||
--dur-deliberate: 460ms;
|
||||
|
||||
--ease-standard: cubic-bezier(0.22, 1, 0.36, 1);
|
||||
--ease-emphasized: cubic-bezier(0.16, 1, 0.3, 1);
|
||||
--ease-exit: cubic-bezier(0.4, 0, 1, 1);
|
||||
|
||||
--distance-xs: 4px;
|
||||
--distance-sm: 8px;
|
||||
--distance-md: 16px;
|
||||
--distance-lg: 24px;
|
||||
|
||||
--scale-press: 0.98;
|
||||
--scale-hover: 1.01;
|
||||
--scale-pop: 1.02;
|
||||
}
|
||||
|
||||
:root[data-motion="reduced"] {
|
||||
--dur-instant: 1ms;
|
||||
--dur-fast: 1ms;
|
||||
--dur-base: 1ms;
|
||||
--dur-slow: 1ms;
|
||||
--dur-deliberate: 1ms;
|
||||
--distance-xs: 0px;
|
||||
--distance-sm: 0px;
|
||||
--distance-md: 0px;
|
||||
--distance-lg: 0px;
|
||||
--scale-press: 1;
|
||||
--scale-hover: 1;
|
||||
--scale-pop: 1;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
:root {
|
||||
:root:not([data-motion="full"]) {
|
||||
--dur-instant: 1ms;
|
||||
--dur-fast: 1ms;
|
||||
--dur-base: 1ms;
|
||||
--dur-slow: 1ms;
|
||||
--dur-deliberate: 1ms;
|
||||
--distance-xs: 0px;
|
||||
--distance-sm: 0px;
|
||||
--distance-md: 0px;
|
||||
--distance-lg: 0px;
|
||||
--scale-press: 1;
|
||||
--scale-hover: 1;
|
||||
--scale-pop: 1;
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
:root {
|
||||
scroll-behavior: auto;
|
||||
}
|
||||
|
||||
:root:not([data-motion="full"]) *,
|
||||
:root:not([data-motion="full"]) *::before,
|
||||
:root:not([data-motion="full"]) *::after {
|
||||
animation-duration: 1ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
scroll-behavior: auto !important;
|
||||
@@ -30,3 +64,98 @@
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes aiui-fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes aiui-fade-out {
|
||||
from {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes aiui-slide-up-sm {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(var(--distance-sm));
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes aiui-slide-down-sm {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(calc(var(--distance-sm) * -1));
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.motion-transition {
|
||||
transition-duration: var(--dur-base);
|
||||
transition-property: color, background-color, border-color, box-shadow, opacity,
|
||||
transform;
|
||||
transition-timing-function: var(--ease-standard);
|
||||
}
|
||||
|
||||
.motion-pressable {
|
||||
transition-duration: var(--dur-fast);
|
||||
transition-property: color, background-color, border-color, box-shadow, transform;
|
||||
transition-timing-function: var(--ease-standard);
|
||||
}
|
||||
|
||||
.motion-pressable:hover {
|
||||
transform: translateY(calc(var(--distance-xs) * -0.25)) scale(var(--scale-hover));
|
||||
}
|
||||
|
||||
.motion-pressable:active {
|
||||
transform: scale(var(--scale-press));
|
||||
}
|
||||
|
||||
.motion-enter-fade {
|
||||
animation: aiui-fade-in var(--dur-base) var(--ease-standard) both;
|
||||
}
|
||||
|
||||
.motion-enter-rise {
|
||||
animation: aiui-slide-up-sm var(--dur-slow) var(--ease-emphasized) both;
|
||||
}
|
||||
|
||||
.motion-overlay-enter {
|
||||
animation: aiui-fade-in var(--dur-fast) var(--ease-standard) both;
|
||||
}
|
||||
|
||||
.motion-overlay-exit {
|
||||
animation: aiui-fade-out var(--dur-fast) var(--ease-exit) both;
|
||||
}
|
||||
|
||||
.motion-exit-fade {
|
||||
animation: aiui-fade-out var(--dur-fast) var(--ease-exit) both;
|
||||
}
|
||||
|
||||
.motion-exit-drop {
|
||||
animation: aiui-slide-down-sm calc(var(--dur-fast) * 0.9) var(--ease-exit) reverse
|
||||
both;
|
||||
}
|
||||
|
||||
.motion-ring {
|
||||
transition-duration: var(--dur-fast);
|
||||
transition-property: box-shadow, outline-color, border-color;
|
||||
transition-timing-function: var(--ease-standard);
|
||||
}
|
||||
|
||||
+104
-19
@@ -1,30 +1,72 @@
|
||||
:root,
|
||||
[data-theme="light"] {
|
||||
color-scheme: light;
|
||||
:root {
|
||||
--font-sans: "Avenir Next", "Segoe UI", sans-serif;
|
||||
--font-display: "Iowan Old Style", "Palatino Linotype", "Book Antiqua", Georgia,
|
||||
serif;
|
||||
--font-mono: "SF Mono", "SFMono-Regular", "Consolas", monospace;
|
||||
|
||||
--color-background: oklch(0.985 0.004 85);
|
||||
--color-foreground: oklch(0.24 0.03 60);
|
||||
--color-surface: oklch(0.965 0.008 80);
|
||||
--color-surface-strong: oklch(0.93 0.012 78);
|
||||
--color-border: oklch(0.87 0.01 75);
|
||||
--color-ring: oklch(0.56 0.12 32);
|
||||
--color-primary: oklch(0.53 0.15 30);
|
||||
--color-primary-foreground: oklch(0.98 0.01 80);
|
||||
--color-muted: oklch(0.94 0.008 78);
|
||||
--color-muted-foreground: oklch(0.42 0.028 60);
|
||||
--color-accent: oklch(0.76 0.1 82);
|
||||
--color-card: color-mix(in oklch, var(--color-surface) 86%, white 14%);
|
||||
--color-card-foreground: var(--color-foreground);
|
||||
--text-xs: 0.75rem;
|
||||
--text-sm: 0.875rem;
|
||||
--text-base: 1rem;
|
||||
--text-lg: 1.125rem;
|
||||
--text-xl: 1.25rem;
|
||||
--text-2xl: 1.5rem;
|
||||
--text-3xl: 2rem;
|
||||
--text-4xl: clamp(2.5rem, 4vw, 4rem);
|
||||
|
||||
--radius-sm: 10px;
|
||||
--radius-md: 16px;
|
||||
--radius-lg: 24px;
|
||||
--leading-tight: 1.1;
|
||||
--leading-snug: 1.25;
|
||||
--leading-normal: 1.5;
|
||||
--leading-loose: 1.7;
|
||||
|
||||
--tracking-tight: -0.03em;
|
||||
--tracking-normal: 0;
|
||||
--tracking-caps: 0.18em;
|
||||
|
||||
--border-width-thin: 1px;
|
||||
--border-width-strong: 1.5px;
|
||||
|
||||
--radius-xs: 8px;
|
||||
--radius-sm: 12px;
|
||||
--radius-md: 18px;
|
||||
--radius-lg: 28px;
|
||||
--radius-xl: 40px;
|
||||
--radius-full: 999px;
|
||||
|
||||
--shadow-xs: 0 1px 2px oklch(0.28 0.02 55 / 0.06);
|
||||
--shadow-sm: 0 8px 24px oklch(0.28 0.02 55 / 0.08);
|
||||
--shadow-md: 0 18px 48px oklch(0.28 0.03 55 / 0.12);
|
||||
--shadow-lg: 0 32px 72px oklch(0.2 0.02 55 / 0.16);
|
||||
}
|
||||
|
||||
:root,
|
||||
[data-theme="light"] {
|
||||
color-scheme: light;
|
||||
--color-background: oklch(0.985 0.004 85);
|
||||
--color-foreground: oklch(0.24 0.03 60);
|
||||
--color-surface: oklch(0.965 0.008 80);
|
||||
--color-surface-strong: oklch(0.93 0.012 78);
|
||||
--color-surface-contrast: oklch(0.28 0.028 58);
|
||||
--color-border: oklch(0.87 0.01 75);
|
||||
--color-border-strong: oklch(0.72 0.018 68);
|
||||
--color-input: var(--color-border);
|
||||
--color-ring: oklch(0.56 0.12 32);
|
||||
--color-primary: oklch(0.53 0.15 30);
|
||||
--color-primary-foreground: oklch(0.98 0.01 80);
|
||||
--color-secondary: oklch(0.9 0.02 74);
|
||||
--color-secondary-foreground: oklch(0.26 0.024 60);
|
||||
--color-muted: oklch(0.94 0.008 78);
|
||||
--color-muted-foreground: oklch(0.42 0.028 60);
|
||||
--color-accent: oklch(0.76 0.1 82);
|
||||
--color-accent-foreground: oklch(0.24 0.03 60);
|
||||
--color-success: oklch(0.58 0.12 152);
|
||||
--color-success-foreground: oklch(0.97 0.01 155);
|
||||
--color-warning: oklch(0.74 0.12 80);
|
||||
--color-warning-foreground: oklch(0.22 0.02 64);
|
||||
--color-destructive: oklch(0.51 0.18 28);
|
||||
--color-destructive-foreground: oklch(0.98 0.01 80);
|
||||
--color-card: color-mix(in oklch, var(--color-surface) 86%, white 14%);
|
||||
--color-card-foreground: var(--color-foreground);
|
||||
--color-overlay: oklch(0.12 0.01 40 / 0.48);
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
@@ -33,13 +75,56 @@
|
||||
--color-foreground: oklch(0.94 0.01 80);
|
||||
--color-surface: oklch(0.26 0.018 60);
|
||||
--color-surface-strong: oklch(0.31 0.018 60);
|
||||
--color-surface-contrast: oklch(0.93 0.012 78);
|
||||
--color-border: oklch(0.4 0.015 60);
|
||||
--color-border-strong: oklch(0.56 0.028 64);
|
||||
--color-input: var(--color-border);
|
||||
--color-ring: oklch(0.7 0.12 35);
|
||||
--color-primary: oklch(0.72 0.13 40);
|
||||
--color-primary-foreground: oklch(0.22 0.014 60);
|
||||
--color-secondary: oklch(0.39 0.035 66);
|
||||
--color-secondary-foreground: oklch(0.95 0.01 80);
|
||||
--color-muted: oklch(0.28 0.015 60);
|
||||
--color-muted-foreground: oklch(0.8 0.02 72);
|
||||
--color-accent: oklch(0.68 0.09 82);
|
||||
--color-accent-foreground: oklch(0.17 0.012 60);
|
||||
--color-success: oklch(0.7 0.12 152);
|
||||
--color-success-foreground: oklch(0.17 0.012 152);
|
||||
--color-warning: oklch(0.78 0.12 82);
|
||||
--color-warning-foreground: oklch(0.16 0.012 64);
|
||||
--color-destructive: oklch(0.68 0.16 28);
|
||||
--color-destructive-foreground: oklch(0.16 0.012 60);
|
||||
--color-card: color-mix(in oklch, var(--color-surface) 90%, black 10%);
|
||||
--color-card-foreground: var(--color-foreground);
|
||||
--color-overlay: oklch(0.05 0.01 50 / 0.72);
|
||||
}
|
||||
|
||||
[data-theme="brand"] {
|
||||
color-scheme: light;
|
||||
--color-background: oklch(0.972 0.016 172);
|
||||
--color-foreground: oklch(0.24 0.03 182);
|
||||
--color-surface: oklch(0.946 0.018 172);
|
||||
--color-surface-strong: oklch(0.91 0.024 172);
|
||||
--color-surface-contrast: oklch(0.29 0.034 182);
|
||||
--color-border: oklch(0.83 0.026 172);
|
||||
--color-border-strong: oklch(0.67 0.045 176);
|
||||
--color-input: var(--color-border);
|
||||
--color-ring: oklch(0.53 0.12 190);
|
||||
--color-primary: oklch(0.48 0.12 188);
|
||||
--color-primary-foreground: oklch(0.97 0.008 172);
|
||||
--color-secondary: oklch(0.82 0.066 156);
|
||||
--color-secondary-foreground: oklch(0.2 0.02 178);
|
||||
--color-muted: oklch(0.91 0.018 172);
|
||||
--color-muted-foreground: oklch(0.42 0.03 180);
|
||||
--color-accent: oklch(0.75 0.105 130);
|
||||
--color-accent-foreground: oklch(0.2 0.02 160);
|
||||
--color-success: oklch(0.6 0.12 155);
|
||||
--color-success-foreground: oklch(0.98 0.006 170);
|
||||
--color-warning: oklch(0.76 0.13 86);
|
||||
--color-warning-foreground: oklch(0.22 0.02 74);
|
||||
--color-destructive: oklch(0.53 0.16 30);
|
||||
--color-destructive-foreground: oklch(0.98 0.01 80);
|
||||
--color-card: color-mix(in oklch, var(--color-surface) 88%, white 12%);
|
||||
--color-card-foreground: var(--color-foreground);
|
||||
--color-overlay: oklch(0.13 0.015 185 / 0.5);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user