From d33780e83a0f0a51386c8963f62987d16ec49f5a Mon Sep 17 00:00:00 2001 From: wangdicoder Date: Thu, 16 Apr 2026 21:14:20 +1000 Subject: [PATCH 1/7] fix: design token --- .../containers/theme-studio/color-utils.ts | 199 ++++++++++-- .../src/containers/theme-studio/defaults.ts | 1 + .../containers/theme-studio/editor-draft.ts | 1 + .../containers/theme-studio/editor-fields.tsx | 144 +++++++++ .../theme-studio/runtime-presets.ts | 51 ++-- .../theme-studio/sidebar-content.tsx | 288 ++++++++++++++---- .../theme-studio/theme-document-adapter.ts | 225 ++++++++++---- .../containers/theme-studio/theme-studio.scss | 58 +++- .../docs/src/containers/theme-studio/types.ts | 1 + packages/react/SCSS_AUTHORING.md | 15 +- packages/react/src/button/style/_mixin.scss | 36 ++- packages/react/src/button/style/index.scss | 65 +++- packages/react/src/checkbox/style/index.scss | 1 + packages/react/src/input/style/_mixin.scss | 1 + packages/react/src/input/style/index.scss | 1 + packages/react/src/radio/style/index.scss | 1 + packages/react/src/select/style/index.scss | 1 + packages/tokens/REGISTRY_SPEC.md | 18 +- packages/tokens/source/components/button.json | 36 +++ .../tokens/source/components/checkbox.json | 6 + packages/tokens/source/components/input.json | 6 + packages/tokens/source/components/radio.json | 6 + packages/tokens/source/components/select.json | 6 + .../tokens/source/schema/theme.schema.json | 4 +- packages/tokens/source/semantic/effects.json | 5 + 25 files changed, 987 insertions(+), 189 deletions(-) diff --git a/apps/docs/src/containers/theme-studio/color-utils.ts b/apps/docs/src/containers/theme-studio/color-utils.ts index f1c62588..1137cb87 100644 --- a/apps/docs/src/containers/theme-studio/color-utils.ts +++ b/apps/docs/src/containers/theme-studio/color-utils.ts @@ -7,6 +7,14 @@ type OklchColor = { c: number; h: number; }; +export type ShadowValue = { + color: string; + opacity: number; + blur: number; + spread: number; + offsetX: number; + offsetY: number; +}; function normalizeAlpha(value: number): number { if (!Number.isFinite(value)) return 1; @@ -72,42 +80,66 @@ export function formatOklchColor(color: OklchColor): string { return `oklch(${color.l.toFixed(3)} ${color.c.toFixed(3)} ${color.h.toFixed(3)})`; } -export function deriveStatusPalette(styles: RuntimeStyles): Pick< +export function deriveStatusPalette( + styles: RuntimeStyles +): Pick< ThemeEditorFields, - 'success' | 'successForeground' | 'info' | 'infoForeground' | 'warning' | 'warningForeground' | 'danger' | 'dangerForeground' + | 'success' + | 'successForeground' + | 'info' + | 'infoForeground' + | 'warning' + | 'warningForeground' + | 'danger' + | 'dangerForeground' > { - const mode: ThemeMode = parseCssScalar(styles.background ?? '') != null - ? ((parseOklchColor(styles.background)?.l ?? 1) < 0.45 ? 'dark' : 'light') - : 'light'; - const seed = parseOklchColor(styles.primary) ?? parseOklchColor(styles.accent) ?? parseOklchColor(styles.ring); + const mode: ThemeMode = + parseCssScalar(styles.background ?? '') != null + ? (parseOklchColor(styles.background)?.l ?? 1) < 0.45 + ? 'dark' + : 'light' + : 'light'; + const seed = + parseOklchColor(styles.primary) ?? + parseOklchColor(styles.accent) ?? + parseOklchColor(styles.ring); const chromaBase = clamp(seed?.c ?? (mode === 'dark' ? 0.17 : 0.19), 0.1, 0.24); const lightnessShift = seed ? (seed.l - (mode === 'dark' ? 0.78 : 0.58)) * 0.08 : 0; const hueShift = seed ? ((seed.h - 220) / 220) * 6 : 0; const statusForeground = mode === 'dark' ? 'oklch(0.145 0 0)' : 'oklch(0.985 0 0)'; - const createStatus = (hue: number, lightness: number, chromaScale: number) => formatOklchColor({ - l: clamp(lightness + lightnessShift, mode === 'dark' ? 0.68 : 0.54, mode === 'dark' ? 0.84 : 0.74), - c: clamp(chromaBase * chromaScale, 0.12, 0.26), - h: normalizeHue(hue + hueShift), - }); + const createStatus = (hue: number, lightness: number, chromaScale: number) => + formatOklchColor({ + l: clamp( + lightness + lightnessShift, + mode === 'dark' ? 0.68 : 0.54, + mode === 'dark' ? 0.84 : 0.74 + ), + c: clamp(chromaBase * chromaScale, 0.12, 0.26), + h: normalizeHue(hue + hueShift), + }); return { success: createStatus(148, mode === 'dark' ? 0.76 : 0.62, 0.92), successForeground: statusForeground, - info: createStatus(238, mode === 'dark' ? 0.74 : 0.60, 0.96), + info: createStatus(238, mode === 'dark' ? 0.74 : 0.6, 0.96), infoForeground: statusForeground, - warning: createStatus(72, mode === 'dark' ? 0.80 : 0.69, 0.94), + warning: createStatus(72, mode === 'dark' ? 0.8 : 0.69, 0.94), warningForeground: mode === 'dark' ? 'oklch(0.145 0 0)' : 'oklch(0.205 0 0)', - danger: createStatus(28, mode === 'dark' ? 0.72 : 0.60, 1), + danger: createStatus(28, mode === 'dark' ? 0.72 : 0.6, 1), dangerForeground: statusForeground, }; } function parseHexColor(color: string): [number, number, number] | null { const normalized = color.trim().replace('#', ''); - const value = normalized.length === 3 - ? normalized.split('').map((char) => `${char}${char}`).join('') - : normalized; + const value = + normalized.length === 3 + ? normalized + .split('') + .map((char) => `${char}${char}`) + .join('') + : normalized; if (!/^[0-9a-f]{6}$/i.test(value)) return null; @@ -155,9 +187,138 @@ export function softenSurface(color: string, mode: ThemeMode, amount: number): s return mode === 'dark' ? tintColor(color, amount) : shadeColor(color, amount); } -export function buildShadow(styles: RuntimeStyles): string { +function splitTopLevelTokens(value: string): string[] { + const tokens: string[] = []; + let current = ''; + let depth = 0; + + for (const char of value) { + if (char === '(') depth += 1; + if (char === ')') depth = Math.max(0, depth - 1); + + if (/\s/.test(char) && depth === 0) { + if (current) { + tokens.push(current); + current = ''; + } + continue; + } + + current += char; + } + + if (current) tokens.push(current); + return tokens; +} + +function parsePxValue(token: string): number | null { + const trimmed = token.trim(); + if (/^-?0(?:\.0+)?(?:px)?$/i.test(trimmed)) return 0; + + const match = /^(-?\d+(?:\.\d+)?)px$/i.exec(trimmed); + if (!match) return null; + + const parsed = Number.parseFloat(match[1]); + return Number.isFinite(parsed) ? parsed : null; +} + +function rgbChannelToHex(value: string): string | null { + const parsed = Number.parseInt(value.trim(), 10); + if (!Number.isFinite(parsed) || parsed < 0 || parsed > 255) return null; + return parsed.toString(16).padStart(2, '0'); +} + +function parseShadowColor(value: string): Pick | null { + const colorMixMatch = + /^color-mix\(in srgb,\s*(.+?)\s+(-?\d+(?:\.\d+)?)%,\s*transparent\s*\)$/i.exec(value.trim()); + if (colorMixMatch) { + const percentage = Number.parseFloat(colorMixMatch[2]); + if (Number.isFinite(percentage)) { + return { + color: colorMixMatch[1].trim(), + opacity: clamp(percentage / 100, 0, 1), + }; + } + } + + const rgbaMatch = /^rgba\(\s*([^,]+)\s*,\s*([^,]+)\s*,\s*([^,]+)\s*,\s*([^)]+)\)$/i.exec( + value.trim() + ); + if (rgbaMatch) { + const red = rgbChannelToHex(rgbaMatch[1]); + const green = rgbChannelToHex(rgbaMatch[2]); + const blue = rgbChannelToHex(rgbaMatch[3]); + const alpha = Number.parseFloat(rgbaMatch[4]); + + if (red && green && blue && Number.isFinite(alpha)) { + return { + color: `#${red}${green}${blue}`, + opacity: clamp(alpha, 0, 1), + }; + } + } + + const rgbMatch = /^rgb\(\s*([^,]+)\s*,\s*([^,]+)\s*,\s*([^,]+)\s*\)$/i.exec(value.trim()); + if (rgbMatch) { + const red = rgbChannelToHex(rgbMatch[1]); + const green = rgbChannelToHex(rgbMatch[2]); + const blue = rgbChannelToHex(rgbMatch[3]); + + if (red && green && blue) { + return { + color: `#${red}${green}${blue}`, + opacity: 1, + }; + } + } + + return value.trim() + ? { + color: value.trim(), + opacity: 1, + } + : null; +} + +export function formatShadowValue(shadow: ShadowValue): string { + const offsetX = `${shadow.offsetX}px`; + const offsetY = `${shadow.offsetY}px`; + const blur = `${shadow.blur}px`; + const spread = `${shadow.spread}px`; + + return `${offsetX} ${offsetY} ${blur} ${spread} ${toRgba(shadow.color, shadow.opacity)}`; +} + +export function parseShadowValue(value: string, fallback: ShadowValue): ShadowValue { + const trimmed = value.trim(); + if (!trimmed || trimmed.toLowerCase() === 'none') return fallback; + + const tokens = splitTopLevelTokens(trimmed); + if (tokens.length < 5) return fallback; + + const offsetX = parsePxValue(tokens[0]); + const offsetY = parsePxValue(tokens[1]); + const blur = parsePxValue(tokens[2]); + const spread = parsePxValue(tokens[3]); + const color = parseShadowColor(tokens.slice(4).join(' ')); + + if (offsetX == null || offsetY == null || blur == null || spread == null || !color) { + return fallback; + } + + return { + color: color.color, + opacity: color.opacity, + blur, + spread, + offsetX, + offsetY, + }; +} + +export function buildShadow(styles: RuntimeStyles, fallback = DEFAULT_FIELDS.shadowCard): string { const color = styles['shadow-color']; - if (!color) return DEFAULT_FIELDS.shadowCard; + if (!color) return fallback; const opacity = Number.parseFloat(styles['shadow-opacity'] ?? '0.1'); const blur = styles['shadow-blur'] ?? '0px'; diff --git a/apps/docs/src/containers/theme-studio/defaults.ts b/apps/docs/src/containers/theme-studio/defaults.ts index d8ecb58d..3d24bb0f 100644 --- a/apps/docs/src/containers/theme-studio/defaults.ts +++ b/apps/docs/src/containers/theme-studio/defaults.ts @@ -50,6 +50,7 @@ export const DEFAULT_FIELDS: ThemeEditorFields = { h2Size: '32px', letterSpacing: '-0.02em', radius: '0.3rem', + shadowControl: 'none', shadowCard: '0 20px 55px rgba(17, 24, 39, 0.08)', shadowFocus: '0 0 0 3px rgba(110, 65, 191, 0.22)', buttonRadius: '0.3rem', diff --git a/apps/docs/src/containers/theme-studio/editor-draft.ts b/apps/docs/src/containers/theme-studio/editor-draft.ts index ab512113..ad2adb04 100644 --- a/apps/docs/src/containers/theme-studio/editor-draft.ts +++ b/apps/docs/src/containers/theme-studio/editor-draft.ts @@ -128,6 +128,7 @@ export function buildPreviewVars(fields: ThemeEditorFields): React.CSSProperties '--editor-h2-size': fields.h2Size, '--editor-letter-spacing': fields.letterSpacing, '--editor-radius': fields.radius, + '--editor-shadow-control': fields.shadowControl, '--editor-shadow-card': fields.shadowCard, '--editor-shadow-focus': fields.shadowFocus, '--editor-button-radius': fields.buttonRadius, diff --git a/apps/docs/src/containers/theme-studio/editor-fields.tsx b/apps/docs/src/containers/theme-studio/editor-fields.tsx index b8c06040..698ccb15 100644 --- a/apps/docs/src/containers/theme-studio/editor-fields.tsx +++ b/apps/docs/src/containers/theme-studio/editor-fields.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { ColorPicker, Input, InputNumber, Slider, Textarea } from '@tiny-design/react'; +import { formatShadowValue, parseShadowValue, type ShadowValue } from './color-utils'; import type { SliderFieldConfig } from './types'; export function swatchTextStyle(background: string, foreground: string): React.CSSProperties { @@ -140,3 +141,146 @@ export function SliderField({ ); } + +function ShadowMetricField({ + label, + value, + min, + max, + step, + unit, + onChange, +}: { + label: string; + value: number; + min: number; + max: number; + step: number; + unit?: string; + onChange: (next: number) => void; +}): React.ReactElement { + return ( + + ); +} + +export function ShadowField({ + label, + value, + onChange, + fallback, +}: { + label: string; + value: string; + onChange: (next: string) => void; + fallback: ShadowValue; +}): React.ReactElement { + const parsed = React.useMemo(() => parseShadowValue(value, fallback), [fallback, value]); + + const updateShadow = (patch: Partial) => { + onChange( + formatShadowValue({ + ...parsed, + ...patch, + }) + ); + }; + + return ( +
+ {label} +
+
+ Color +
+ updateShadow({ color: next })} + presets={[ + '#000000', + '#ffffff', + '#1e9df1', + '#0f1419', + '#e11d48', + '#22c55e', + '#f59e0b', + '#8b5cf6', + ]} + /> + updateShadow({ color: event.target.value })} + /> +
+
+ updateShadow({ opacity: next })} + /> + updateShadow({ blur: next })} + /> + updateShadow({ spread: next })} + /> + updateShadow({ offsetX: next })} + /> + updateShadow({ offsetY: next })} + /> +
+
+ ); +} diff --git a/apps/docs/src/containers/theme-studio/runtime-presets.ts b/apps/docs/src/containers/theme-studio/runtime-presets.ts index 346d2f90..371b3a16 100644 --- a/apps/docs/src/containers/theme-studio/runtime-presets.ts +++ b/apps/docs/src/containers/theme-studio/runtime-presets.ts @@ -18,15 +18,20 @@ function buildPresetDescription(source: TweakcnRuntimePresetSource): string { : 'Imported from tweakcn runtime preset.'; } -function readRuntimeStyle(styles: RuntimeStyles, key: string, fallbackStyles?: RuntimeStyles): string | undefined { +function readRuntimeStyle( + styles: RuntimeStyles, + key: string, + fallbackStyles?: RuntimeStyles +): string | undefined { return styles[key] ?? fallbackStyles?.[key]; } function mapRuntimeStylesToFields( styles: RuntimeStyles, - typographyFallbackStyles?: RuntimeStyles, + typographyFallbackStyles?: RuntimeStyles ): Partial { - const radius = styles.radius ?? DEFAULT_FIELDS.radius; + const radius = + readRuntimeStyle(styles, 'radius', typographyFallbackStyles) ?? DEFAULT_FIELDS.radius; const ring = styles.ring ?? styles.primary ?? DEFAULT_FIELDS.ring; const statusPalette = deriveStatusPalette(styles); @@ -64,16 +69,23 @@ function mapRuntimeStylesToFields( sidebar: styles.sidebar ?? DEFAULT_FIELDS.sidebar, sidebarForeground: styles['sidebar-foreground'] ?? DEFAULT_FIELDS.sidebarForeground, sidebarPrimary: styles['sidebar-primary'] ?? DEFAULT_FIELDS.sidebarPrimary, - sidebarPrimaryForeground: styles['sidebar-primary-foreground'] ?? DEFAULT_FIELDS.sidebarPrimaryForeground, + sidebarPrimaryForeground: + styles['sidebar-primary-foreground'] ?? DEFAULT_FIELDS.sidebarPrimaryForeground, sidebarAccent: styles['sidebar-accent'] ?? DEFAULT_FIELDS.sidebarAccent, - sidebarAccentForeground: styles['sidebar-accent-foreground'] ?? DEFAULT_FIELDS.sidebarAccentForeground, + sidebarAccentForeground: + styles['sidebar-accent-foreground'] ?? DEFAULT_FIELDS.sidebarAccentForeground, sidebarBorder: styles['sidebar-border'] ?? DEFAULT_FIELDS.sidebarBorder, sidebarRing: styles['sidebar-ring'] ?? DEFAULT_FIELDS.sidebarRing, - fontSans: readRuntimeStyle(styles, 'font-sans', typographyFallbackStyles) ?? DEFAULT_FIELDS.fontSans, - fontMono: readRuntimeStyle(styles, 'font-mono', typographyFallbackStyles) ?? DEFAULT_FIELDS.fontMono, - letterSpacing: readRuntimeStyle(styles, 'letter-spacing', typographyFallbackStyles) ?? DEFAULT_FIELDS.letterSpacing, + fontSans: + readRuntimeStyle(styles, 'font-sans', typographyFallbackStyles) ?? DEFAULT_FIELDS.fontSans, + fontMono: + readRuntimeStyle(styles, 'font-mono', typographyFallbackStyles) ?? DEFAULT_FIELDS.fontMono, + letterSpacing: + readRuntimeStyle(styles, 'letter-spacing', typographyFallbackStyles) ?? + DEFAULT_FIELDS.letterSpacing, radius, - shadowCard: buildShadow(styles), + shadowControl: buildShadow(styles, DEFAULT_FIELDS.shadowControl), + shadowCard: buildShadow(styles, DEFAULT_FIELDS.shadowCard), shadowFocus: `0 0 0 3px ${toRgba(ring, 0.24)}`, buttonRadius: radius, inputRadius: radius, @@ -98,26 +110,15 @@ function buildPresetFromRuntimeSource(source: TweakcnRuntimePresetSource): Theme lightStyles.sidebar ?? DEFAULT_FIELDS.sidebar, ], drafts: { - light: createDraft( - source.id, - source.label, - 'tweakcn', - 'light', - lightFields, - ), - dark: createDraft( - source.id, - source.label, - 'tweakcn', - 'dark', - darkFields, - ), + light: createDraft(source.id, source.label, 'tweakcn', 'light', lightFields), + dark: createDraft(source.id, source.label, 'tweakcn', 'dark', darkFields), }, }; } -export const THEME_EDITOR_PRESETS: ThemeEditorPreset[] = - TWEAKCN_RUNTIME_PRESET_SOURCES.map(buildPresetFromRuntimeSource); +export const THEME_EDITOR_PRESETS: ThemeEditorPreset[] = TWEAKCN_RUNTIME_PRESET_SOURCES.map( + buildPresetFromRuntimeSource +); export function getPresetById(presetId: string): ThemeEditorPreset { return THEME_EDITOR_PRESETS.find((preset) => preset.id === presetId) ?? THEME_EDITOR_PRESETS[0]; diff --git a/apps/docs/src/containers/theme-studio/sidebar-content.tsx b/apps/docs/src/containers/theme-studio/sidebar-content.tsx index ba106fa1..7637789f 100644 --- a/apps/docs/src/containers/theme-studio/sidebar-content.tsx +++ b/apps/docs/src/containers/theme-studio/sidebar-content.tsx @@ -1,13 +1,45 @@ import React from 'react'; import { Collapse, Select, Text } from '@tiny-design/react'; import { COLOR_GROUPS, CORE_COLOR_GROUP_TITLES, FONT_OPTIONS, MONO_OPTIONS } from './editor-config'; -import { ColorField, SliderField, TextField } from './editor-fields'; -import type { FieldKey, ThemeEditorDraft, ThemeEditorSection, ThemeEditorColorGroup } from './types'; +import { ColorField, ShadowField, SliderField, TextField } from './editor-fields'; +import type { + FieldKey, + ThemeEditorDraft, + ThemeEditorSection, + ThemeEditorColorGroup, +} from './types'; + +const CONTROL_SHADOW_FALLBACK = { + color: 'oklch(0 0 0)', + opacity: 0.1, + blur: 3, + spread: 0, + offsetX: 0, + offsetY: 1, +}; + +const CARD_SHADOW_FALLBACK = { + color: 'oklch(0 0 0)', + opacity: 0.1, + blur: 3, + spread: 0, + offsetX: 0, + offsetY: 1, +}; + +const FOCUS_SHADOW_FALLBACK = { + color: '#6e41bf', + opacity: 0.24, + blur: 0, + spread: 3, + offsetX: 0, + offsetY: 0, +}; function renderColorGroups( groups: ThemeEditorColorGroup[], draft: ThemeEditorDraft, - updateField: (key: FieldKey, value: string) => void, + updateField: (key: FieldKey, value: string) => void ): React.ReactElement[] { return groups.map((group) => (
@@ -38,7 +70,9 @@ export function ThemeStudioSidebarContent({ updateField: (key: FieldKey, value: string) => void; }): React.ReactElement | null { const coreColorGroups = COLOR_GROUPS.filter((group) => CORE_COLOR_GROUP_TITLES.has(group.title)); - const advancedColorGroups = COLOR_GROUPS.filter((group) => !CORE_COLOR_GROUP_TITLES.has(group.title)); + const advancedColorGroups = COLOR_GROUPS.filter( + (group) => !CORE_COLOR_GROUP_TITLES.has(group.title) + ); if (section === 'colors') { return ( @@ -46,9 +80,7 @@ export function ThemeStudioSidebarContent({
Core Colors - - Start with brand, surfaces, feedback, and focus. - + Start with brand, surfaces, feedback, and focus.
{renderColorGroups(coreColorGroups, draft, updateField)}
@@ -62,12 +94,10 @@ export function ThemeStudioSidebarContent({ { key: 'advanced-colors', label: ( -
- Advanced Tokens - - Card, popover, sidebar, chart. - -
+
+ Advanced Tokens + Card, popover, sidebar, chart. +
), children: renderColorGroups(advancedColorGroups, draft, updateField), }, @@ -108,12 +138,18 @@ export function ThemeStudioSidebarContent({
Font Family
- The quick brown fox jumps over the lazy dog. - const theme = { mode: "{draft.mode}" } + + The quick brown fox jumps over the lazy dog. + + + const theme = { mode: "{draft.mode}" } +
- updateField('fontSans', next)} /> + updateField('fontSans', next)} + /> - updateField('fontMono', next)} /> + updateField('fontMono', next)} + />
Type Scale - updateField('fontSizeBase', next)} config={{ min: 12, max: 20, step: 1, unit: 'px' }} /> - updateField('lineHeightBase', next)} config={{ min: 1.1, max: 2, step: 0.05 }} /> - updateField('h1Size', next)} config={{ min: 28, max: 64, step: 1, unit: 'px' }} /> - updateField('h2Size', next)} config={{ min: 22, max: 48, step: 1, unit: 'px' }} /> + updateField('fontSizeBase', next)} + config={{ min: 12, max: 20, step: 1, unit: 'px' }} + /> + updateField('lineHeightBase', next)} + config={{ min: 1.1, max: 2, step: 0.05 }} + /> + updateField('h1Size', next)} + config={{ min: 28, max: 64, step: 1, unit: 'px' }} + /> + updateField('h2Size', next)} + config={{ min: 22, max: 48, step: 1, unit: 'px' }} + />
Fine Tuning - updateField('letterSpacing', next)} config={{ min: -0.08, max: 0.08, step: 0.01, unit: 'em' }} /> + updateField('letterSpacing', next)} + config={{ min: -0.08, max: 0.08, step: 0.01, unit: 'em' }} + />
- Heading Preview - Body copy reflects base scale and rhythm. + + Heading Preview + + + Body copy reflects base scale and rhythm. +
@@ -187,10 +267,30 @@ export function ThemeStudioSidebarContent({
Shape - updateField('radius', next)} config={{ min: 0, max: 2, step: 0.0625, unit: 'rem' }} /> - updateField('buttonRadius', next)} config={{ min: 0, max: 4, step: 0.0625, unit: 'rem' }} /> - updateField('inputRadius', next)} config={{ min: 0, max: 2, step: 0.0625, unit: 'rem' }} /> - updateField('cardRadius', next)} config={{ min: 0, max: 2.5, step: 0.0625, unit: 'rem' }} /> + updateField('radius', next)} + config={{ min: 0, max: 2, step: 0.0625, unit: 'rem' }} + /> + updateField('buttonRadius', next)} + config={{ min: 0, max: 4, step: 0.0625, unit: 'rem' }} + /> + updateField('inputRadius', next)} + config={{ min: 0, max: 2, step: 0.0625, unit: 'rem' }} + /> + updateField('cardRadius', next)} + config={{ min: 0, max: 2.5, step: 0.0625, unit: 'rem' }} + />
@@ -200,8 +300,18 @@ export function ThemeStudioSidebarContent({
Fields - updateField('fieldHeightMd', next)} config={{ min: 20, max: 56, step: 1, unit: 'px' }} /> - updateField('fieldPaddingMd', next)} config={{ min: 0, max: 32, step: 1, unit: 'px' }} /> + updateField('fieldHeightMd', next)} + config={{ min: 20, max: 56, step: 1, unit: 'px' }} + /> + updateField('fieldPaddingMd', next)} + config={{ min: 0, max: 32, step: 1, unit: 'px' }} + /> - Advanced Sizes - Small and large field density. -
+
+ Advanced Sizes + Small and large field density. +
), children: ( <> - updateField('fieldHeightSm', next)} config={{ min: 20, max: 56, step: 1, unit: 'px' }} /> - updateField('fieldHeightLg', next)} config={{ min: 20, max: 56, step: 1, unit: 'px' }} /> - updateField('fieldPaddingSm', next)} config={{ min: 0, max: 32, step: 1, unit: 'px' }} /> - updateField('fieldPaddingLg', next)} config={{ min: 0, max: 32, step: 1, unit: 'px' }} /> + updateField('fieldHeightSm', next)} + config={{ min: 20, max: 56, step: 1, unit: 'px' }} + /> + updateField('fieldHeightLg', next)} + config={{ min: 20, max: 56, step: 1, unit: 'px' }} + /> + updateField('fieldPaddingSm', next)} + config={{ min: 0, max: 32, step: 1, unit: 'px' }} + /> + updateField('fieldPaddingLg', next)} + config={{ min: 0, max: 32, step: 1, unit: 'px' }} + /> ), }, @@ -230,8 +360,18 @@ export function ThemeStudioSidebarContent({
Buttons - updateField('buttonHeightMd', next)} config={{ min: 20, max: 56, step: 1, unit: 'px' }} /> - updateField('buttonPaddingMd', next)} config={{ min: 0, max: 32, step: 1, unit: 'px' }} /> + updateField('buttonHeightMd', next)} + config={{ min: 20, max: 56, step: 1, unit: 'px' }} + /> + updateField('buttonPaddingMd', next)} + config={{ min: 0, max: 32, step: 1, unit: 'px' }} + /> - Advanced Sizes - Small and large button density. -
+
+ Advanced Sizes + Small and large button density. +
), children: ( <> - updateField('buttonHeightSm', next)} config={{ min: 20, max: 56, step: 1, unit: 'px' }} /> - updateField('buttonHeightLg', next)} config={{ min: 20, max: 56, step: 1, unit: 'px' }} /> - updateField('buttonPaddingSm', next)} config={{ min: 0, max: 32, step: 1, unit: 'px' }} /> - updateField('buttonPaddingLg', next)} config={{ min: 0, max: 32, step: 1, unit: 'px' }} /> + updateField('buttonHeightSm', next)} + config={{ min: 20, max: 56, step: 1, unit: 'px' }} + /> + updateField('buttonHeightLg', next)} + config={{ min: 20, max: 56, step: 1, unit: 'px' }} + /> + updateField('buttonPaddingSm', next)} + config={{ min: 0, max: 32, step: 1, unit: 'px' }} + /> + updateField('buttonPaddingLg', next)} + config={{ min: 0, max: 32, step: 1, unit: 'px' }} + /> ), }, @@ -260,18 +420,40 @@ export function ThemeStudioSidebarContent({
Cards - updateField('cardPadding', next)} config={{ min: 12, max: 40, step: 1, unit: 'px' }} /> + updateField('cardPadding', next)} + config={{ min: 12, max: 40, step: 1, unit: 'px' }} + />
Elevation & Focus
+
Control Surface
Card Surface
Focus Ring
- updateField('shadowCard', next)} /> - updateField('shadowFocus', next)} /> + updateField('shadowControl', next)} + /> + updateField('shadowCard', next)} + /> + updateField('shadowFocus', next)} + />
); diff --git a/apps/docs/src/containers/theme-studio/theme-document-adapter.ts b/apps/docs/src/containers/theme-studio/theme-document-adapter.ts index 76534249..d7a92cf5 100644 --- a/apps/docs/src/containers/theme-studio/theme-document-adapter.ts +++ b/apps/docs/src/containers/theme-studio/theme-document-adapter.ts @@ -30,6 +30,15 @@ function normalizeImportedThemeDocument(theme: ThemeDocument): ThemeDocument { return validation.normalizedDocument as ThemeDocument; } +function combineShadowValues(...values: Array): string { + return ( + values + .map((value) => value?.trim()) + .filter((value): value is string => Boolean(value) && value !== 'none') + .join(', ') || 'none' + ); +} + export function buildThemeDocumentFromDraft(draft: ThemeEditorDraft): ThemeDocument { const { fields } = draft; const primaryHover = tintColor(fields.primary, draft.mode === 'dark' ? 0.12 : 0.08); @@ -37,9 +46,21 @@ export function buildThemeDocumentFromDraft(draft: ThemeEditorDraft): ThemeDocum const primarySurface = toRgba(fields.primary, draft.mode === 'dark' ? 0.16 : 0.08); const primarySurfaceActive = toRgba(fields.primary, draft.mode === 'dark' ? 0.28 : 0.18); const defaultHover = softenSurface(fields.muted, draft.mode, draft.mode === 'dark' ? 0.1 : 0.06); - const defaultActive = softenSurface(fields.muted, draft.mode, draft.mode === 'dark' ? 0.18 : 0.12); - const defaultBorderHover = softenSurface(fields.border, draft.mode, draft.mode === 'dark' ? 0.16 : 0.12); - const defaultBorderActive = softenSurface(fields.border, draft.mode, draft.mode === 'dark' ? 0.24 : 0.18); + const defaultActive = softenSurface( + fields.muted, + draft.mode, + draft.mode === 'dark' ? 0.18 : 0.12 + ); + const defaultBorderHover = softenSurface( + fields.border, + draft.mode, + draft.mode === 'dark' ? 0.16 : 0.12 + ); + const defaultBorderActive = softenSurface( + fields.border, + draft.mode, + draft.mode === 'dark' ? 0.24 : 0.18 + ); const infoHover = tintColor(fields.info, draft.mode === 'dark' ? 0.12 : 0.08); const infoActive = tintColor(fields.info, draft.mode === 'dark' ? 0.2 : 0.16); const successHover = tintColor(fields.success, draft.mode === 'dark' ? 0.12 : 0.08); @@ -48,6 +69,7 @@ export function buildThemeDocumentFromDraft(draft: ThemeEditorDraft): ThemeDocum const warningActive = tintColor(fields.warning, draft.mode === 'dark' ? 0.2 : 0.16); const dangerHover = tintColor(fields.danger, draft.mode === 'dark' ? 0.12 : 0.08); const dangerActive = tintColor(fields.danger, draft.mode === 'dark' ? 0.2 : 0.16); + const controlFocusShadow = combineShadowValues(fields.shadowControl, fields.shadowFocus); return { meta: { @@ -117,6 +139,7 @@ export function buildThemeDocumentFromDraft(draft: ThemeEditorDraft): ThemeDocum 'h1-font-size': fields.h1Size, 'h2-font-size': fields.h2Size, 'border-radius': fields.radius, + 'shadow-control': fields.shadowControl, 'shadow-card': fields.shadowCard, 'shadow-focus': fields.shadowFocus, 'control.height.sm': fields.fieldHeightSm, @@ -127,19 +150,39 @@ export function buildThemeDocumentFromDraft(draft: ThemeEditorDraft): ThemeDocum 'control.padding-inline.lg': fields.fieldPaddingLg, }, components: { - 'button.bg.primary': fields.primary, - 'button.bg.primary-hover': primaryHover, - 'button.bg.primary-active': primaryActive, - 'button.text.primary': fields.primaryForeground, - 'button.bg.default': fields.muted, - 'button.bg.default-hover': defaultHover, - 'button.bg.default-active': defaultActive, - 'button.border.default': fields.border, - 'button.border.default-hover': defaultBorderHover, - 'button.border.default-active': defaultBorderActive, - 'button.text.default': fields.baseForeground, - 'button.text.default-hover': fields.baseForeground, - 'button.text.default-active': fields.baseForeground, + 'button.solid.primary.bg': fields.primary, + 'button.solid.primary.bg-hover': primaryHover, + 'button.solid.primary.bg-active': primaryActive, + 'button.solid.primary.border': fields.primary, + 'button.solid.primary.border-hover': primaryHover, + 'button.solid.primary.border-active': primaryActive, + 'button.solid.primary.text': fields.primaryForeground, + 'button.solid.primary.text-hover': fields.primaryForeground, + 'button.solid.primary.text-active': fields.primaryForeground, + 'button.solid.default.bg': fields.muted, + 'button.solid.default.bg-hover': defaultHover, + 'button.solid.default.bg-active': defaultActive, + 'button.solid.default.border': fields.border, + 'button.solid.default.border-hover': defaultBorderHover, + 'button.solid.default.border-active': defaultBorderActive, + 'button.solid.default.text': fields.baseForeground, + 'button.solid.default.text-hover': fields.baseForeground, + 'button.solid.default.text-active': fields.baseForeground, + 'button.outline.default.bg': fields.base, + 'button.outline.default.bg-hover': defaultHover, + 'button.outline.default.bg-active': defaultActive, + 'button.outline.default.border': fields.border, + 'button.outline.default.border-hover': defaultBorderHover, + 'button.outline.default.border-active': defaultBorderActive, + 'button.outline.default.text': fields.baseForeground, + 'button.outline.default.text-hover': fields.baseForeground, + 'button.outline.default.text-active': fields.baseForeground, + 'button.solid.default.shadow': fields.shadowControl, + 'button.solid.default.shadow-hover': fields.shadowControl, + 'button.solid.default.shadow-active': fields.shadowControl, + 'button.outline.default.shadow': fields.shadowControl, + 'button.outline.default.shadow-hover': fields.shadowControl, + 'button.outline.default.shadow-active': fields.shadowControl, 'button.radius': fields.buttonRadius, 'button.height.sm': fields.buttonHeightSm, 'button.height.md': fields.buttonHeightMd, @@ -159,9 +202,10 @@ export function buildThemeDocumentFromDraft(draft: ThemeEditorDraft): ThemeDocum 'input.bg': fields.base, 'input.color': fields.baseForeground, 'input.border': fields.input, + 'input.shadow': fields.shadowControl, 'input.border.hover': fields.ring, 'input.border.focus': fields.ring, - 'input.shadow.focus': fields.shadowFocus, + 'input.shadow.focus': controlFocusShadow, 'input.radius': fields.inputRadius, 'input.height.sm': fields.fieldHeightSm, 'input.height.md': fields.fieldHeightMd, @@ -172,9 +216,10 @@ export function buildThemeDocumentFromDraft(draft: ThemeEditorDraft): ThemeDocum 'select.bg': fields.base, 'select.color': fields.baseForeground, 'select.border': fields.input, + 'select.shadow': fields.shadowControl, 'select.border.hover': fields.ring, 'select.border.focus': fields.ring, - 'select.shadow.focus': fields.shadowFocus, + 'select.shadow.focus': controlFocusShadow, 'select.radius': fields.inputRadius, 'select.height.sm': fields.fieldHeightSm, 'select.height.md': fields.fieldHeightMd, @@ -190,7 +235,7 @@ export function buildThemeDocumentFromDraft(draft: ThemeEditorDraft): ThemeDocum 'picker.input-border': fields.input, 'picker.input-border-hover': fields.ring, 'picker.input-border-focus': fields.ring, - 'picker.input-shadow-focus': fields.shadowFocus, + 'picker.input-shadow-focus': controlFocusShadow, 'picker.input-color': fields.baseForeground, 'picker.input-color-placeholder': fields.mutedForeground, 'picker.input-color-muted': fields.mutedForeground, @@ -270,7 +315,7 @@ export function buildThemeDocumentFromDraft(draft: ThemeEditorDraft): ThemeDocum 'cascader.border': fields.input, 'cascader.border-hover': fields.ring, 'cascader.border-focus': fields.ring, - 'cascader.shadow-focus': fields.shadowFocus, + 'cascader.shadow-focus': controlFocusShadow, 'cascader.radius': fields.inputRadius, 'cascader.padding.sm': `0 calc(${fields.fieldPaddingSm} + 20px) 0 ${fields.fieldPaddingSm}`, 'cascader.padding.md': `0 calc(${fields.fieldPaddingMd} + 20px) 0 ${fields.fieldPaddingMd}`, @@ -279,10 +324,11 @@ export function buildThemeDocumentFromDraft(draft: ThemeEditorDraft): ThemeDocum 'native-select.border': fields.input, 'native-select.border-hover': fields.ring, 'native-select.border-focus': fields.ring, - 'native-select.shadow-focus': fields.shadowFocus, + 'native-select.shadow-focus': controlFocusShadow, 'native-select.radius': fields.inputRadius, 'checkbox.bg': fields.base, 'checkbox.border': fields.input, + 'checkbox.shadow': fields.shadowControl, 'checkbox.border.hover': fields.ring, 'checkbox.radius': fields.radius, 'checkbox.bg.checked': fields.primary, @@ -290,6 +336,7 @@ export function buildThemeDocumentFromDraft(draft: ThemeEditorDraft): ThemeDocum 'checkbox.indicator-color': fields.primaryForeground, 'radio.bg': fields.base, 'radio.border': fields.input, + 'radio.shadow': fields.shadowControl, 'radio.border.checked': fields.primary, 'radio.dot-bg': fields.primary, 'switch.bg': fields.mutedForeground, @@ -342,7 +389,11 @@ export function buildThemeDocumentFromDraft(draft: ThemeEditorDraft): ThemeDocum }; } -function readToken(theme: ThemeDocument, semanticKey: string, componentKey?: string): string | undefined { +function readToken( + theme: ThemeDocument, + semanticKey: string, + componentKey?: string +): string | undefined { const semantic = theme.tokens?.semantic?.[semanticKey]; if (semantic != null) return String(semantic); @@ -354,7 +405,11 @@ function readToken(theme: ThemeDocument, semanticKey: string, componentKey?: str return undefined; } -function readComponentFirst(theme: ThemeDocument, componentKey: string, semanticKey?: string): string | undefined { +function readComponentFirst( + theme: ThemeDocument, + componentKey: string, + semanticKey?: string +): string | undefined { const component = theme.tokens?.components?.[componentKey]; if (component != null) return String(component); @@ -384,9 +439,22 @@ export function buildDraftFromThemeDocument(theme: ThemeDocument): ThemeEditorDr fields: { ...baseFields, primary: readToken(normalizedTheme, 'color-primary') ?? baseFields.primary, - primaryForeground: readComponentFirst(normalizedTheme, 'button.text.primary') ?? baseFields.primaryForeground, - secondary: readToken(normalizedTheme, 'color-fill', 'button.bg.default') ?? baseFields.secondary, - secondaryForeground: readToken(normalizedTheme, 'color-text', 'button.text.default') ?? baseFields.secondaryForeground, + primaryForeground: + readComponentFirst(normalizedTheme, 'button.solid.primary.text') ?? + readComponentFirst(normalizedTheme, 'button.text.primary') ?? + baseFields.primaryForeground, + secondary: + readComponentFirst(normalizedTheme, 'card.bg.filled') ?? + readComponentFirst(normalizedTheme, 'table.header-bg') ?? + readComponentFirst(normalizedTheme, 'tag.bg') ?? + readToken(normalizedTheme, 'color-fill', 'button.solid.default.bg') ?? + readToken(normalizedTheme, 'color-fill', 'button.bg.default') ?? + baseFields.secondary, + secondaryForeground: + readComponentFirst(normalizedTheme, 'tag.color') ?? + readToken(normalizedTheme, 'color-text', 'button.solid.default.text') ?? + readToken(normalizedTheme, 'color-text', 'button.text.default') ?? + baseFields.secondaryForeground, accent: readToken(normalizedTheme, 'color-primary-bg') ?? baseFields.accent, success: readToken(normalizedTheme, 'color-success') ?? baseFields.success, info: readToken(normalizedTheme, 'color-info') ?? baseFields.info, @@ -395,10 +463,13 @@ export function buildDraftFromThemeDocument(theme: ThemeDocument): ThemeEditorDr base: readToken(normalizedTheme, 'color-bg') ?? baseFields.base, baseForeground: readToken(normalizedTheme, 'color-text') ?? baseFields.baseForeground, card: readToken(normalizedTheme, 'color-bg-container', 'card.bg') ?? baseFields.card, - cardForeground: readToken(normalizedTheme, 'color-text-heading', 'card.header-color') ?? baseFields.cardForeground, + cardForeground: + readToken(normalizedTheme, 'color-text-heading', 'card.header-color') ?? + baseFields.cardForeground, popover: readToken(normalizedTheme, 'color-bg-elevated') ?? baseFields.popover, muted: readToken(normalizedTheme, 'color-bg-spotlight') ?? baseFields.muted, - mutedForeground: readToken(normalizedTheme, 'color-text-secondary') ?? baseFields.mutedForeground, + mutedForeground: + readToken(normalizedTheme, 'color-text-secondary') ?? baseFields.mutedForeground, border: readToken(normalizedTheme, 'color-border') ?? baseFields.border, input: readToken(normalizedTheme, 'color-border', 'input.border') ?? baseFields.input, ring: readToken(normalizedTheme, 'color-primary', 'input.border.focus') ?? baseFields.ring, @@ -407,8 +478,11 @@ export function buildDraftFromThemeDocument(theme: ThemeDocument): ThemeEditorDr chart3: readToken(normalizedTheme, 'chart-3') ?? baseFields.chart3, chart4: readToken(normalizedTheme, 'chart-4') ?? baseFields.chart4, chart5: readToken(normalizedTheme, 'chart-5') ?? baseFields.chart5, - sidebar: readToken(normalizedTheme, 'color-bg-layout', 'layout.sidebar-bg') ?? baseFields.sidebar, - sidebarForeground: readToken(normalizedTheme, 'color-text', 'layout.sidebar-color') ?? baseFields.sidebarForeground, + sidebar: + readToken(normalizedTheme, 'color-bg-layout', 'layout.sidebar-bg') ?? baseFields.sidebar, + sidebarForeground: + readToken(normalizedTheme, 'color-text', 'layout.sidebar-color') ?? + baseFields.sidebarForeground, fontSans: readToken(normalizedTheme, 'font-family') ?? baseFields.fontSans, fontMono: readToken(normalizedTheme, 'font-family-monospace') ?? baseFields.fontMono, fontSizeBase: readToken(normalizedTheme, 'font-size-base') ?? baseFields.fontSizeBase, @@ -416,51 +490,76 @@ export function buildDraftFromThemeDocument(theme: ThemeDocument): ThemeEditorDr h1Size: readToken(normalizedTheme, 'h1-font-size') ?? baseFields.h1Size, h2Size: readToken(normalizedTheme, 'h2-font-size') ?? baseFields.h2Size, radius: readToken(normalizedTheme, 'border-radius') ?? baseFields.radius, + shadowControl: + readToken(normalizedTheme, 'shadow-control', 'input.shadow') ?? baseFields.shadowControl, shadowCard: readToken(normalizedTheme, 'shadow-card', 'card.shadow') ?? baseFields.shadowCard, - shadowFocus: readToken(normalizedTheme, 'shadow-focus', 'input.shadow.focus') ?? toRgba(baseFields.primary, 0.22), - buttonRadius: readToken(normalizedTheme, 'border-radius', 'button.radius') ?? baseFields.buttonRadius, - inputRadius: readToken(normalizedTheme, 'border-radius', 'input.radius') ?? baseFields.inputRadius, - cardRadius: readToken(normalizedTheme, 'border-radius', 'card.radius') ?? baseFields.cardRadius, + shadowFocus: readToken(normalizedTheme, 'shadow-focus') ?? toRgba(baseFields.primary, 0.22), + buttonRadius: + readToken(normalizedTheme, 'border-radius', 'button.radius') ?? baseFields.buttonRadius, + inputRadius: + readToken(normalizedTheme, 'border-radius', 'input.radius') ?? baseFields.inputRadius, + cardRadius: + readToken(normalizedTheme, 'border-radius', 'card.radius') ?? baseFields.cardRadius, fieldPaddingSm: - readComponentFirst(normalizedTheme, 'input.padding-inline-sm', 'control.padding-inline.sm') - ?? baseFields.fieldPaddingSm, + readComponentFirst( + normalizedTheme, + 'input.padding-inline-sm', + 'control.padding-inline.sm' + ) ?? baseFields.fieldPaddingSm, fieldPaddingMd: - readComponentFirst(normalizedTheme, 'input.padding-inline-md', 'control.padding-inline.md') - ?? readToken(normalizedTheme, 'spacing-4') - ?? baseFields.fieldPaddingMd, + readComponentFirst( + normalizedTheme, + 'input.padding-inline-md', + 'control.padding-inline.md' + ) ?? + readToken(normalizedTheme, 'spacing-4') ?? + baseFields.fieldPaddingMd, fieldPaddingLg: - readComponentFirst(normalizedTheme, 'input.padding-inline-lg', 'control.padding-inline.lg') - ?? baseFields.fieldPaddingLg, + readComponentFirst( + normalizedTheme, + 'input.padding-inline-lg', + 'control.padding-inline.lg' + ) ?? baseFields.fieldPaddingLg, buttonPaddingSm: - readComponentFirst(normalizedTheme, 'button.padding-inline-sm', 'control.padding-inline.sm') - ?? baseFields.buttonPaddingSm, + readComponentFirst( + normalizedTheme, + 'button.padding-inline-sm', + 'control.padding-inline.sm' + ) ?? baseFields.buttonPaddingSm, buttonPaddingMd: - readComponentFirst(normalizedTheme, 'button.padding-inline-md', 'control.padding-inline.md') - ?? baseFields.buttonPaddingMd, + readComponentFirst( + normalizedTheme, + 'button.padding-inline-md', + 'control.padding-inline.md' + ) ?? baseFields.buttonPaddingMd, buttonPaddingLg: - readComponentFirst(normalizedTheme, 'button.padding-inline-lg', 'control.padding-inline.lg') - ?? baseFields.buttonPaddingLg, + readComponentFirst( + normalizedTheme, + 'button.padding-inline-lg', + 'control.padding-inline.lg' + ) ?? baseFields.buttonPaddingLg, fieldHeightSm: - readComponentFirst(normalizedTheme, 'input.height.sm', 'control.height.sm') - ?? baseFields.fieldHeightSm, + readComponentFirst(normalizedTheme, 'input.height.sm', 'control.height.sm') ?? + baseFields.fieldHeightSm, fieldHeightMd: - readComponentFirst(normalizedTheme, 'input.height.md', 'control.height.md') - ?? readToken(normalizedTheme, 'height-md') - ?? baseFields.fieldHeightMd, + readComponentFirst(normalizedTheme, 'input.height.md', 'control.height.md') ?? + readToken(normalizedTheme, 'height-md') ?? + baseFields.fieldHeightMd, fieldHeightLg: - readComponentFirst(normalizedTheme, 'input.height.lg', 'control.height.lg') - ?? baseFields.fieldHeightLg, + readComponentFirst(normalizedTheme, 'input.height.lg', 'control.height.lg') ?? + baseFields.fieldHeightLg, buttonHeightSm: - readComponentFirst(normalizedTheme, 'button.height.sm', 'control.height.sm') - ?? baseFields.buttonHeightSm, + readComponentFirst(normalizedTheme, 'button.height.sm', 'control.height.sm') ?? + baseFields.buttonHeightSm, buttonHeightMd: - readComponentFirst(normalizedTheme, 'button.height.md', 'control.height.md') - ?? readToken(normalizedTheme, 'height-md') - ?? baseFields.buttonHeightMd, + readComponentFirst(normalizedTheme, 'button.height.md', 'control.height.md') ?? + readToken(normalizedTheme, 'height-md') ?? + baseFields.buttonHeightMd, buttonHeightLg: - readComponentFirst(normalizedTheme, 'button.height.lg', 'control.height.lg') - ?? baseFields.buttonHeightLg, - cardPadding: readToken(normalizedTheme, 'spacing-5', 'card.body-padding') ?? baseFields.cardPadding, + readComponentFirst(normalizedTheme, 'button.height.lg', 'control.height.lg') ?? + baseFields.buttonHeightLg, + cardPadding: + readToken(normalizedTheme, 'spacing-5', 'card.body-padding') ?? baseFields.cardPadding, }, }; } @@ -472,7 +571,7 @@ export function createDefaultDraft(): ThemeEditorDraft { export function applyPresetToDraft( presetId: string, current?: ThemeEditorDraft, - modeOverride?: ThemeMode, + modeOverride?: ThemeMode ): ThemeEditorDraft { const draft = getPresetDraft(presetId, modeOverride ?? current?.mode ?? 'light'); diff --git a/apps/docs/src/containers/theme-studio/theme-studio.scss b/apps/docs/src/containers/theme-studio/theme-studio.scss index ff9c824e..62a048c4 100644 --- a/apps/docs/src/containers/theme-studio/theme-studio.scss +++ b/apps/docs/src/containers/theme-studio/theme-studio.scss @@ -16,8 +16,16 @@ border: 1px solid var(--ty-color-border-secondary, var(--ty-color-border)); border-radius: 12px; background: - radial-gradient(circle at top right, color-mix(in srgb, var(--editor-primary), transparent 95%), transparent 22%), - linear-gradient(180deg, var(--ty-color-bg-container) 0%, color-mix(in srgb, var(--ty-color-fill), transparent 40%) 100%); + radial-gradient( + circle at top right, + color-mix(in srgb, var(--editor-primary), transparent 95%), + transparent 22% + ), + linear-gradient( + 180deg, + var(--ty-color-bg-container) 0%, + color-mix(in srgb, var(--ty-color-fill), transparent 40%) 100% + ); box-shadow: 0 6px 16px rgba(17, 24, 39, 0.03); } @@ -288,6 +296,32 @@ align-items: center; } +.theme-studio__shadow-field { + margin-top: 10px; +} + +.theme-studio__shadow-editor { + display: flex; + flex-direction: column; + gap: 10px; +} + +.theme-studio__shadow-color-row { + display: flex; + flex-direction: column; + gap: 6px; +} + +.theme-studio__shadow-color-label, +.theme-studio__shadow-metric-label { + font-size: 12px; + color: var(--ty-color-text-secondary, var(--ty-color-text)); +} + +.theme-studio__shadow-metric .theme-studio__field-row { + margin-bottom: 4px; +} + .theme-studio__pill { padding: 8px 12px; border-radius: 999px; @@ -338,7 +372,11 @@ padding: 9px; background: radial-gradient(circle at top, rgba(255, 255, 255, 0.6), transparent 30%), - linear-gradient(180deg, var(--editor-base) 0%, color-mix(in srgb, var(--editor-base), black 4%) 100%); + linear-gradient( + 180deg, + var(--editor-base) 0%, + color-mix(in srgb, var(--editor-base), black 4%) 100% + ); color: var(--editor-base-foreground); box-shadow: 0 30px 80px rgba(17, 24, 39, 0.08); font-family: var(--editor-font-sans); @@ -350,7 +388,11 @@ .theme-studio__preview_dark { background: radial-gradient(circle at top, rgba(255, 255, 255, 0.06), transparent 24%), - linear-gradient(180deg, var(--editor-base) 0%, color-mix(in srgb, var(--editor-base), black 10%) 100%); + linear-gradient( + 180deg, + var(--editor-base) 0%, + color-mix(in srgb, var(--editor-base), black 10%) 100% + ); } .theme-studio__metrics { @@ -524,12 +566,20 @@ gap: 8px; } +.theme-studio__control-proxy, .theme-studio__focus-proxy, .theme-studio__surface-proxy { border: 1px solid var(--editor-border); font-size: 11px; } +.theme-studio__control-proxy { + padding: 8px 10px; + border-radius: var(--editor-input-radius); + background: var(--editor-base); + box-shadow: var(--editor-shadow-control); +} + .theme-studio__focus-proxy { padding: 8px 10px; border-radius: var(--editor-input-radius); diff --git a/apps/docs/src/containers/theme-studio/types.ts b/apps/docs/src/containers/theme-studio/types.ts index 173db777..0c8f3ada 100644 --- a/apps/docs/src/containers/theme-studio/types.ts +++ b/apps/docs/src/containers/theme-studio/types.ts @@ -52,6 +52,7 @@ export interface ThemeEditorFields { h2Size: string; letterSpacing: string; radius: string; + shadowControl: string; shadowCard: string; shadowFocus: string; buttonRadius: string; diff --git a/packages/react/SCSS_AUTHORING.md b/packages/react/SCSS_AUTHORING.md index f8be00c4..827753e8 100644 --- a/packages/react/SCSS_AUTHORING.md +++ b/packages/react/SCSS_AUTHORING.md @@ -1,16 +1,19 @@ # SCSS Authoring Spec ## Purpose + This spec defines how component styles consume runtime theme tokens in `@tiny-design/react`. The goal is to keep existing class names and SCSS ergonomics while making every visual decision themeable through CSS custom properties. ## Rules + 1. SCSS defines structure, state, and selector relationships only. 2. Visual values must come from `var(--ty-*)` tokens. 3. New hard-coded colors, radii, shadows, font sizes, spacing, and motion values are not allowed. -4. Component tokens use dot notation with kebab-case segments in source registries, for example `button.bg.primary-hover`. +4. Component tokens use dot notation with kebab-case segments in source registries, for example `button.solid.primary.bg-hover`. 5. CSS variables stay on the existing `--ty-*` prefix for v2 compatibility. ## Token Priority + Use this fallback chain consistently: 1. Component token @@ -36,11 +39,13 @@ Direct semantic usage is only correct when the property should never diverge by ``` ## Naming Conventions + - Semantic CSS vars: `--ty-color-primary`, `--ty-border-radius`, `--ty-font-size-base` -- Component CSS vars: `--ty-button-bg-primary`, `--ty-button-bg-primary-hover`, `--ty-card-header-padding` +- Component CSS vars: `--ty-button-solid-primary-bg`, `--ty-button-solid-primary-bg-hover`, `--ty-card-header-padding` - Avoid aliases like `btn` or `picker` for new token names. Use full component names. ## Allowed Hard-coded Values + Only structural values may be hard-coded when tokenizing them would not improve theming: - `display`, `position`, `flex`, `overflow`, `white-space`, `pointer-events` @@ -51,6 +56,7 @@ Only structural values may be hard-coded when tokenizing them would not improve If a value affects brand, density, readability, affordance, or perceived motion, it must be tokenized. ## Examples + Preferred: ```scss @@ -58,8 +64,8 @@ Preferred: height: var(--ty-button-height-md, var(--ty-height-md)); padding-inline: var(--ty-button-padding-inline-md, var(--ty-spacing-4)); border-radius: var(--ty-button-radius, var(--ty-border-radius)); - background: var(--ty-button-bg-primary, var(--ty-color-primary)); - box-shadow: var(--ty-button-shadow-focus, var(--ty-shadow-focus)); + background: var(--ty-button-solid-primary-bg, var(--ty-color-primary)); + box-shadow: var(--ty-shadow-focus); } ``` @@ -75,6 +81,7 @@ Avoid: ``` ## Migration Checklist + When editing an existing component style file: 1. Replace visual literals with `var(--ty-...)`. diff --git a/packages/react/src/button/style/_mixin.scss b/packages/react/src/button/style/_mixin.scss index 8fd2b6a4..39af557a 100755 --- a/packages/react/src/button/style/_mixin.scss +++ b/packages/react/src/button/style/_mixin.scss @@ -2,17 +2,24 @@ $color, $background, $border, + $shadow: null, $hover-background: null, $hover-border: null, $hover-color: $color, + $hover-shadow: $shadow, $active-background: null, $active-border: null, - $active-color: $color + $active-color: $color, + $active-shadow: $hover-shadow ) { color: $color; background: $background; border-color: $border; + @if $shadow { + box-shadow: $shadow; + } + &:hover { color: $hover-color; @@ -23,6 +30,10 @@ @if $hover-border { border-color: $hover-border; } + + @if $hover-shadow { + box-shadow: $hover-shadow; + } } &:focus { @@ -36,6 +47,10 @@ border-color: $hover-border; } + @if $hover-shadow { + box-shadow: $hover-shadow; + } + z-index: 1; } @@ -49,12 +64,17 @@ @if $active-border { border-color: $active-border; } + + @if $active-shadow { + box-shadow: $active-shadow; + } } &:disabled { color: var(--ty-button-disabled-text, var(--ty-color-text-quaternary)); background-color: var(--ty-button-disabled-bg, var(--ty-color-bg-disabled)); border-color: var(--ty-button-disabled-border, var(--ty-color-border)); + box-shadow: none; } } @@ -80,11 +100,15 @@ $hover-text-fallback: $text-fallback, $active-bg-fallback: $hover-bg-fallback, $active-border-fallback: $hover-border-fallback, - $active-text-fallback: $hover-text-fallback + $active-text-fallback: $hover-text-fallback, + $shadow-fallback: none, + $hover-shadow-fallback: $shadow-fallback, + $active-shadow-fallback: $hover-shadow-fallback ) { --ty-button-current-text: var(--ty-button-#{$variant}-#{$color}-text, #{$text-fallback}); --ty-button-current-bg: var(--ty-button-#{$variant}-#{$color}-bg, #{$bg-fallback}); --ty-button-current-border: var(--ty-button-#{$variant}-#{$color}-border, #{$border-fallback}); + --ty-button-current-shadow: var(--ty-button-#{$variant}-#{$color}-shadow, #{$shadow-fallback}); --ty-button-current-text-hover: var( --ty-button-#{$variant}-#{$color}-text-hover, #{$hover-text-fallback} @@ -97,6 +121,10 @@ --ty-button-#{$variant}-#{$color}-border-hover, #{$hover-border-fallback} ); + --ty-button-current-shadow-hover: var( + --ty-button-#{$variant}-#{$color}-shadow-hover, + #{$hover-shadow-fallback} + ); --ty-button-current-text-active: var( --ty-button-#{$variant}-#{$color}-text-active, #{$active-text-fallback} @@ -109,4 +137,8 @@ --ty-button-#{$variant}-#{$color}-border-active, #{$active-border-fallback} ); + --ty-button-current-shadow-active: var( + --ty-button-#{$variant}-#{$color}-shadow-active, + #{$active-shadow-fallback} + ); } diff --git a/packages/react/src/button/style/index.scss b/packages/react/src/button/style/index.scss index 7b33127c..1cc18816 100755 --- a/packages/react/src/button/style/index.scss +++ b/packages/react/src/button/style/index.scss @@ -41,12 +41,15 @@ $btn-prefix: #{$prefix}-btn; var(--ty-button-current-text), var(--ty-button-current-bg), var(--ty-button-current-border), + var(--ty-button-current-shadow, none), var(--ty-button-current-bg-hover), var(--ty-button-current-border-hover), var(--ty-button-current-text-hover), + var(--ty-button-current-shadow-hover, var(--ty-button-current-shadow, none)), var(--ty-button-current-bg-active), var(--ty-button-current-border-active), - var(--ty-button-current-text-active) + var(--ty-button-current-text-active), + var(--ty-button-current-shadow-active, var(--ty-button-current-shadow-hover, var(--ty-button-current-shadow, none))) ); &__loader { @@ -88,7 +91,10 @@ $btn-prefix: #{$prefix}-btn; var(--ty-color-primary), var(--ty-color-fill), var(--ty-color-primary), - var(--ty-color-primary) + var(--ty-color-primary), + var(--ty-button-solid-default-shadow, var(--ty-shadow-control, none)), + var(--ty-button-solid-default-shadow-hover, var(--ty-button-solid-default-shadow, var(--ty-shadow-control, none))), + var(--ty-button-solid-default-shadow-active, var(--ty-button-solid-default-shadow-hover, var(--ty-button-solid-default-shadow, var(--ty-shadow-control, none)))) ); } @@ -104,7 +110,10 @@ $btn-prefix: #{$prefix}-btn; #fff, var(--ty-color-primary-active), var(--ty-color-primary-active), - #fff + #fff, + none, + none, + none ); } @@ -120,7 +129,10 @@ $btn-prefix: #{$prefix}-btn; #fff, var(--ty-color-info-active), var(--ty-color-info-active), - #fff + #fff, + none, + none, + none ); } @@ -136,7 +148,10 @@ $btn-prefix: #{$prefix}-btn; #fff, var(--ty-color-success-active), var(--ty-color-success-active), - #fff + #fff, + none, + none, + none ); } @@ -152,7 +167,10 @@ $btn-prefix: #{$prefix}-btn; #fff, var(--ty-color-warning-active), var(--ty-color-warning-active), - #fff + #fff, + none, + none, + none ); } @@ -168,7 +186,10 @@ $btn-prefix: #{$prefix}-btn; #fff, var(--ty-color-danger-active), var(--ty-color-danger-active), - #fff + #fff, + none, + none, + none ); } } @@ -186,7 +207,10 @@ $btn-prefix: #{$prefix}-btn; var(--ty-color-text), var(--ty-color-fill-secondary), var(--ty-color-border-secondary), - var(--ty-color-text) + var(--ty-color-text), + var(--ty-button-outline-default-shadow, var(--ty-shadow-control, none)), + var(--ty-button-outline-default-shadow-hover, var(--ty-button-outline-default-shadow, var(--ty-shadow-control, none))), + var(--ty-button-outline-default-shadow-active, var(--ty-button-outline-default-shadow-hover, var(--ty-button-outline-default-shadow, var(--ty-shadow-control, none)))) ); } @@ -202,7 +226,10 @@ $btn-prefix: #{$prefix}-btn; var(--ty-color-primary), var(--ty-color-primary-bg-hover), var(--ty-color-primary-active), - var(--ty-color-primary) + var(--ty-color-primary), + none, + none, + none ); } @@ -218,7 +245,10 @@ $btn-prefix: #{$prefix}-btn; var(--ty-color-info), rgba(64, 169, 255, 0.18), var(--ty-color-info-active), - var(--ty-color-info) + var(--ty-color-info), + none, + none, + none ); } @@ -234,7 +264,10 @@ $btn-prefix: #{$prefix}-btn; var(--ty-color-success), rgba(82, 196, 26, 0.18), var(--ty-color-success-active), - var(--ty-color-success) + var(--ty-color-success), + none, + none, + none ); } @@ -250,7 +283,10 @@ $btn-prefix: #{$prefix}-btn; var(--ty-color-warning), rgba(250, 173, 20, 0.18), var(--ty-color-warning-active), - var(--ty-color-warning) + var(--ty-color-warning), + none, + none, + none ); } @@ -266,7 +302,10 @@ $btn-prefix: #{$prefix}-btn; var(--ty-color-danger), rgba(255, 77, 79, 0.18), var(--ty-color-danger-active), - var(--ty-color-danger) + var(--ty-color-danger), + none, + none, + none ); } } diff --git a/packages/react/src/checkbox/style/index.scss b/packages/react/src/checkbox/style/index.scss index 605cb96c..36887138 100644 --- a/packages/react/src/checkbox/style/index.scss +++ b/packages/react/src/checkbox/style/index.scss @@ -36,6 +36,7 @@ border-radius: var(--ty-checkbox-radius, 2px); border: 1px solid var(--ty-checkbox-border, var(--ty-color-border)); background-color: var(--ty-checkbox-bg, var(--ty-color-bg-container)); + box-shadow: var(--ty-checkbox-shadow, none); transition: 200ms; line-height: 1; vertical-align: middle; diff --git a/packages/react/src/input/style/_mixin.scss b/packages/react/src/input/style/_mixin.scss index c4295d4e..effb2ce8 100755 --- a/packages/react/src/input/style/_mixin.scss +++ b/packages/react/src/input/style/_mixin.scss @@ -12,6 +12,7 @@ border-radius: var(--ty-input-radius, var(--ty-border-radius)); font-size: var(--ty-input-font-size-md, var(--ty-font-size-base)); background-color: var(--ty-input-bg, var(--ty-color-bg-container)); + box-shadow: var(--ty-input-shadow, none); &:hover { border-color: var(--ty-input-border-hover, var(--ty-color-primary)); diff --git a/packages/react/src/input/style/index.scss b/packages/react/src/input/style/index.scss index 98f1a947..476838b4 100755 --- a/packages/react/src/input/style/index.scss +++ b/packages/react/src/input/style/index.scss @@ -12,6 +12,7 @@ border: 1px solid var(--ty-input-border); border-radius: var(--ty-input-radius, var(--ty-border-radius)); background-color: var(--ty-input-bg, var(--ty-color-bg-container)); + box-shadow: var(--ty-input-shadow, none); transition: border-color 0.3s, box-shadow 0.3s, background-color 0.3s; &:hover { diff --git a/packages/react/src/radio/style/index.scss b/packages/react/src/radio/style/index.scss index 939a2d58..8ee66ad8 100644 --- a/packages/react/src/radio/style/index.scss +++ b/packages/react/src/radio/style/index.scss @@ -29,6 +29,7 @@ border-radius: 100%; background-color: var(--ty-radio-bg, var(--ty-color-bg-container)); border: 1px solid var(--ty-radio-border, var(--ty-color-primary)); + box-shadow: var(--ty-radio-shadow, none); align-items: center; justify-content: center; diff --git a/packages/react/src/select/style/index.scss b/packages/react/src/select/style/index.scss index 4bd491a0..79755624 100644 --- a/packages/react/src/select/style/index.scss +++ b/packages/react/src/select/style/index.scss @@ -64,6 +64,7 @@ border: 1px solid var(--ty-select-border, var(--ty-input-border, var(--ty-color-border))); border-radius: var(--ty-select-radius, var(--ty-border-radius)); background-color: var(--ty-select-bg, var(--ty-input-bg, var(--ty-color-bg-container))); + box-shadow: var(--ty-select-shadow, var(--ty-input-shadow, none)); box-sizing: border-box; transition: all 0.3s; diff --git a/packages/tokens/REGISTRY_SPEC.md b/packages/tokens/REGISTRY_SPEC.md index 8fae22c4..ddce407f 100644 --- a/packages/tokens/REGISTRY_SPEC.md +++ b/packages/tokens/REGISTRY_SPEC.md @@ -1,6 +1,7 @@ # Token Registry Spec ## Purpose + The token registry is the canonical machine-readable index of all supported v2 tokens. It is generated from JSON token sources and consumed by: - Theme Studio @@ -11,6 +12,7 @@ The token registry is the canonical machine-readable index of all supported v2 t The registry is metadata, not a theme document. It describes what tokens exist, how they map to CSS variables, and what fallback behavior they expect. ## Output Location + The build step should generate: - `packages/tokens/dist/registry.json` @@ -30,8 +32,8 @@ The build step should generate: ```json { - "key": "button.bg.primary", - "cssVar": "--ty-button-bg-primary", + "key": "button.solid.primary.bg", + "cssVar": "--ty-button-solid-primary-bg", "category": "component", "component": "button", "type": "color", @@ -45,6 +47,7 @@ The build step should generate: ``` ## Required Fields + - `key` - `cssVar` - `category` @@ -53,9 +56,10 @@ The build step should generate: - `status` ## Field Definitions + - `key` Stable token id in dot notation with kebab-case segments. - Examples: `color-primary`, `button.bg.primary`, `card.header-padding` + Examples: `color-primary`, `button.solid.primary.bg`, `card.header-padding` - `cssVar` Public runtime CSS variable name. - `category` @@ -76,23 +80,28 @@ The build step should generate: Recommended component style fallback target. This field is guidance metadata for component authors and docs tooling; it does not mean the build step will emit an automatic fallback chain in generated CSS. - `status` One of: `active`, `deprecated`, `internal` + ## Naming Rules + - `key` must match the theme schema token key pattern. - `cssVar` must always use kebab-case. - `component` names must use full nouns such as `button`, `input`, `card`. - New entries must use the primary v2 names directly. Short prefixes like `btn`, `picker`, or `kbd` are not allowed. ## Fallback Rules + - Primitive tokens should not appear in authored component source styles. - Semantic tokens usually have no registry fallback. - Component tokens should include the semantic fallback they are expected to use in authored component source styles. Examples: -- `button.bg.primary` -> fallback `--ty-color-primary` + +- `button.solid.primary.bg` -> fallback `--ty-color-primary` - `button.radius` -> fallback `--ty-border-radius` - `card.bg` -> fallback `--ty-color-bg-container` ## Status Rules + - `active` Visible in Theme Studio and allowed in theme documents. - `deprecated` @@ -132,6 +141,7 @@ Examples: ``` ## Validation Rules + Build should fail when: 1. Two entries share the same `key` diff --git a/packages/tokens/source/components/button.json b/packages/tokens/source/components/button.json index 0dc4fc66..58412ab2 100644 --- a/packages/tokens/source/components/button.json +++ b/packages/tokens/source/components/button.json @@ -44,6 +44,42 @@ "$type": "number", "description": "Overlay opacity during button loading state." }, + "button.solid.default.shadow": { + "$value": "{shadow-control}", + "$type": "shadow", + "description": "solid default button shadow.", + "fallback": "--ty-shadow-control" + }, + "button.solid.default.shadow-hover": { + "$value": "{shadow-control}", + "$type": "shadow", + "description": "solid default button hover shadow.", + "fallback": "--ty-shadow-control" + }, + "button.solid.default.shadow-active": { + "$value": "{shadow-control}", + "$type": "shadow", + "description": "solid default button active shadow.", + "fallback": "--ty-shadow-control" + }, + "button.outline.default.shadow": { + "$value": "{shadow-control}", + "$type": "shadow", + "description": "outline default button shadow.", + "fallback": "--ty-shadow-control" + }, + "button.outline.default.shadow-hover": { + "$value": "{shadow-control}", + "$type": "shadow", + "description": "outline default button hover shadow.", + "fallback": "--ty-shadow-control" + }, + "button.outline.default.shadow-active": { + "$value": "{shadow-control}", + "$type": "shadow", + "description": "outline default button active shadow.", + "fallback": "--ty-shadow-control" + }, "button.font-size-sm": { "$value": "{control.font-size.sm}", "$type": "dimension", diff --git a/packages/tokens/source/components/checkbox.json b/packages/tokens/source/components/checkbox.json index bcb07dfc..3e016ae9 100644 --- a/packages/tokens/source/components/checkbox.json +++ b/packages/tokens/source/components/checkbox.json @@ -37,6 +37,12 @@ "description": "Checkbox border color.", "fallback": "--ty-color-border" }, + "checkbox.shadow": { + "$value": "{shadow-control}", + "$type": "shadow", + "description": "Checkbox control shadow.", + "fallback": "--ty-shadow-control" + }, "checkbox.border.hover": { "$value": "{color-primary}", "$type": "color", diff --git a/packages/tokens/source/components/input.json b/packages/tokens/source/components/input.json index 580fff68..946394b4 100644 --- a/packages/tokens/source/components/input.json +++ b/packages/tokens/source/components/input.json @@ -29,6 +29,12 @@ "description": "Input border color.", "fallback": "--ty-color-border" }, + "input.shadow": { + "$value": "{shadow-control}", + "$type": "shadow", + "description": "Input surface shadow.", + "fallback": "--ty-shadow-control" + }, "input.border.hover": { "$value": "{color-primary}", "$type": "color", diff --git a/packages/tokens/source/components/radio.json b/packages/tokens/source/components/radio.json index 8be06562..28a30981 100644 --- a/packages/tokens/source/components/radio.json +++ b/packages/tokens/source/components/radio.json @@ -32,6 +32,12 @@ "description": "Radio border color.", "fallback": "--ty-color-primary" }, + "radio.shadow": { + "$value": "{shadow-control}", + "$type": "shadow", + "description": "Radio control shadow.", + "fallback": "--ty-shadow-control" + }, "radio.border.checked": { "$value": "{color-primary}", "$type": "color", diff --git a/packages/tokens/source/components/select.json b/packages/tokens/source/components/select.json index e044d6e5..e675bc9c 100644 --- a/packages/tokens/source/components/select.json +++ b/packages/tokens/source/components/select.json @@ -34,6 +34,12 @@ "description": "Select trigger border color.", "fallback": "--ty-color-border" }, + "select.shadow": { + "$value": "{shadow-control}", + "$type": "shadow", + "description": "Select trigger surface shadow.", + "fallback": "--ty-shadow-control" + }, "select.border.hover": { "$value": "{color-primary}", "$type": "color", diff --git a/packages/tokens/source/schema/theme.schema.json b/packages/tokens/source/schema/theme.schema.json index b9b9a9be..8c87b4ee 100644 --- a/packages/tokens/source/schema/theme.schema.json +++ b/packages/tokens/source/schema/theme.schema.json @@ -125,8 +125,8 @@ "border-radius": "12px" }, "components": { - "button.bg.primary": "#3b82f6", - "button.bg.primary-hover": "#2563eb", + "button.solid.primary.bg": "#3b82f6", + "button.solid.primary.bg-hover": "#2563eb", "button.radius": "999px", "card.header-padding": "16px 20px" } diff --git a/packages/tokens/source/semantic/effects.json b/packages/tokens/source/semantic/effects.json index 38c96676..76ff5b97 100644 --- a/packages/tokens/source/semantic/effects.json +++ b/packages/tokens/source/semantic/effects.json @@ -1,4 +1,9 @@ { + "shadow-control": { + "$value": "none", + "$type": "shadow", + "description": "Control surface shadow used by input-like components and neutral buttons." + }, "shadow-btn": { "$value": "inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 1px rgba(0, 0, 0, 0.075)", "$type": "shadow", From 976167e0f63df1af07e4d3fbd35374839b526425 Mon Sep 17 00:00:00 2001 From: wangdicoder Date: Fri, 17 Apr 2026 10:22:33 +1000 Subject: [PATCH 2/7] feat: update card tab --- .../theme-studio/preview-components.tsx | 1016 -------------- .../preview-components/cards-preview.tsx | 633 +++++++++ .../preview-components/dashboard-preview.tsx | 922 +++++++++++++ .../theme-studio/preview-components/index.tsx | 30 + .../preview-components/mail-preview.tsx | 154 +++ .../preview-components/pricing-preview.tsx | 110 ++ .../containers/theme-studio/theme-studio.scss | 1191 +++++++++++++---- apps/docs/src/index.scss | 4 +- packages/icons/src/icon-more.tsx | 26 + packages/icons/src/index.ts | 1 + 10 files changed, 2783 insertions(+), 1304 deletions(-) delete mode 100644 apps/docs/src/containers/theme-studio/preview-components.tsx create mode 100644 apps/docs/src/containers/theme-studio/preview-components/cards-preview.tsx create mode 100644 apps/docs/src/containers/theme-studio/preview-components/dashboard-preview.tsx create mode 100644 apps/docs/src/containers/theme-studio/preview-components/index.tsx create mode 100644 apps/docs/src/containers/theme-studio/preview-components/mail-preview.tsx create mode 100644 apps/docs/src/containers/theme-studio/preview-components/pricing-preview.tsx create mode 100644 packages/icons/src/icon-more.tsx diff --git a/apps/docs/src/containers/theme-studio/preview-components.tsx b/apps/docs/src/containers/theme-studio/preview-components.tsx deleted file mode 100644 index cd61ea42..00000000 --- a/apps/docs/src/containers/theme-studio/preview-components.tsx +++ /dev/null @@ -1,1016 +0,0 @@ -import React from 'react'; -import { - Alert, - Avatar, - Button, - Card, - Checkbox, - DatePicker, - Divider, - Flex, - Grid, - Input, - Progress, - Radio, - Select, - Segmented, - Switch, - Table, - Tag, - Textarea, - Calendar, - Heading, - Paragraph, - Text, -} from '@tiny-design/react'; -import { - ChartContainer, - ChartTooltip, - ChartTooltipContent, - type ChartConfig, -} from '@tiny-design/charts'; -import { - Area, - AreaChart, - Bar, - BarChart, - CartesianGrid, - Line, - LineChart, - XAxis, - YAxis, -} from 'recharts'; -import { swatchTextStyle } from './editor-fields'; -import type { ThemeEditorFields, ThemeEditorSection, ThemePreviewTemplate } from './types'; -import { IconGithub, IconGoogle } from '@tiny-design/icons'; - -const revenueData = [ - { month: 'Jan', revenue: 4200, target: 3800 }, - { month: 'Feb', revenue: 5100, target: 4300 }, - { month: 'Mar', revenue: 4800, target: 4500 }, - { month: 'Apr', revenue: 6200, target: 5200 }, - { month: 'May', revenue: 6900, target: 6100 }, - { month: 'Jun', revenue: 7600, target: 6600 }, -]; - -const revenueChartConfig: ChartConfig = { - revenue: { - label: 'Revenue', - color: 'var(--ty-chart-1)', - }, - target: { - label: 'Target', - color: 'var(--ty-chart-2)', - }, -}; - -const cardsRevenueData = [ - { month: 'Jan', value: 8.4 }, - { month: 'Feb', value: 7.2 }, - { month: 'Mar', value: 7.8 }, - { month: 'Apr', value: 6.9 }, - { month: 'May', value: 7.4 }, - { month: 'Jun', value: 8.1 }, - { month: 'Jul', value: 12.4 }, -]; - -const cardsRevenueChartConfig: ChartConfig = { - value: { - label: 'Revenue', - color: 'var(--ty-chart-4)', - }, -}; - -const moveGoalData = [ - { key: 'm1', dayLabel: 'M', value: 280 }, - { key: 't1', dayLabel: 'T', value: 340 }, - { key: 'w1', dayLabel: 'W', value: 220 }, - { key: 'th1', dayLabel: 'T', value: 310 }, - { key: 'f1', dayLabel: 'F', value: 360 }, - { key: 's1', dayLabel: 'S', value: 240 }, - { key: 'su1', dayLabel: 'S', value: 300 }, - { key: 'm2', dayLabel: 'M', value: 330 }, - { key: 't2', dayLabel: 'T', value: 260 }, - { key: 'w2', dayLabel: 'W', value: 320 }, - { key: 'th2', dayLabel: 'T', value: 350 }, - { key: 'f2', dayLabel: 'F', value: 290 }, -]; - -const moveGoalChartConfig: ChartConfig = { - value: { - label: 'Calories', - color: 'var(--ty-chart-1)', - }, -}; - -const cardsExerciseData = [ - { month: 'Mar', personal: 18, average: 22 }, - { month: 'Apr', personal: 14, average: 18 }, - { month: 'May', personal: 58, average: 26 }, - { month: 'Jun', personal: 28, average: 24 }, - { month: 'Jul', personal: 22, average: 20 }, - { month: 'Aug', personal: 26, average: 23 }, -]; - -const cardsExerciseChartConfig: ChartConfig = { - personal: { - label: 'You', - color: 'var(--ty-chart-2)', - }, - average: { - label: 'Average', - color: 'var(--ty-chart-5)', - }, -}; - -const subscriptionsData = [ - { month: 'Jan', value: 400 }, - { month: 'Feb', value: 620 }, - { month: 'Mar', value: 980 }, - { month: 'Apr', value: 1480 }, - { month: 'May', value: 1910 }, - { month: 'Jun', value: 880 }, - { month: 'Jul', value: 2350 }, -]; - -const subscriptionsChartConfig: ChartConfig = { - value: { - label: 'Subscriptions', - color: 'var(--editor-card-foreground)', - }, -}; - -const currYear = new Date().getFullYear(); -const currMonth = new Date().getMonth(); - -function LiveResponsePanel({ fields }: { fields: ThemeEditorFields }): React.ReactElement { - const colorPairs = [ - ['Primary', fields.primary, fields.primaryForeground], - ['Accent', fields.accent, fields.accentForeground], - ['Success', fields.success, fields.successForeground], - ['Info', fields.info, fields.infoForeground], - ['Warning', fields.warning, fields.warningForeground], - ['Danger', fields.danger, fields.dangerForeground], - ['Card', fields.card, fields.cardForeground], - ]; - - return ( - - - Live Colors - - {colorPairs.map(([label, background, foreground]) => ( -
- {label} -
- ))} -
-
- - - Charts - - {[fields.chart1, fields.chart2, fields.chart3, fields.chart4, fields.chart5].map( - (color, index) => ( - - ) - )} - - - - - Typography - - Ag - {fields.fontSans.split(',')[0].replaceAll('"', '')} - - {fields.fontSizeBase} / {fields.lineHeightBase} - - - - - - Surface - -
Card
-
Focus
-
-
- - - Sidebar - - -
- Primary -
-
- Accent -
-
-
-
-
- ); -} - -function CardsPreview(): React.ReactElement { - return ( - - - - - - - - Total Revenue - +20.1% from last month - - - $15,231.89 - - - - - } /> - - - - - - - - - - - Subscriptions - +180.1% from last month - - - +2,350 - - - - - - - - - - - } /> - - - - - - - - - - - Upgrade your subscription - - You are currently on the free plan. Upgrade to the pro plan to get access to all - features. - - -
- - - - - - - - - - - -
- -
- Starter Plan - Perfect for small businesses. -
-
-
-
- -
- Pro Plan - More features and storage. -
-
-
-
-
-