feat(q-2-scheme): 添加颜色方案预览组件并优化类型定义

新增 Q2SchemePreview 组件用于展示颜色方案的预览效果
将 Map 类型改为 Record 以简化数据结构
This commit is contained in:
徐涛 2025-07-18 15:45:20 +08:00
parent a7ef8eb576
commit a77fb3f18b
4 changed files with 231 additions and 2 deletions

View File

@ -6,6 +6,7 @@ import { Q2SchemeStorage } from '../../q-2-scheme';
import { isNilOrEmpty } from '../../utls';
import { SchemeExport } from './Export';
import { Q2SchemeBuilder } from './q-2-scheme/Builder';
import Q2SchemePreview from './q-2-scheme/Preview';
const tabOptions = [
{ title: 'Overview', id: 'overview' },
@ -33,6 +34,7 @@ export function Q2Scheme({ scheme }: Q2SchemeProps) {
export: isNilOrEmpty(scheme.schemeStorage?.cssVariables),
}}
/>
{isEqual(activeTab, 'overview') && <Q2SchemePreview scheme={scheme} />}
{isEqual(activeTab, 'builder') && (
<Q2SchemeBuilder scheme={scheme} onBuildCompleted={() => setActiveTab('overview')} />
)}

View File

@ -0,0 +1,58 @@
@layer pages {
.preview_layout {
padding: var(--spacing-s) var(--spacing-m);
width: 100%;
display: flex;
flex-direction: column;
align-items: stretch;
gap: var(--spacing-m);
}
.preview_block {
width: inherit;
padding: var(--spacing-xl) var(--spacing-m);
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
gap: var(--spacing-xs);
h2 {
font-size: var(--font-size-xl);
font-weight: bold;
line-height: 1.7em;
}
}
.preview_unit {
width: inherit;
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: var(--spacing-xs);
}
.preview_indi_block {
width: inherit;
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: var(--spacing-xs);
}
.preview_swatch {
width: inherit;
display: grid;
grid-template-columns: repeat(16, 1fr);
gap: var(--spacing-xs);
.preview_swatch_cell {
height: 1em;
}
}
.preview_cell {
padding: var(--spacing-xs) var(--spacing-s);
display: flex;
flex-direction: column;
align-items: flex-start;
gap: var(--spacing-xxs);
font-size: var(--font-size-s);
line-height: 1.5em;
.wacg {
font-size: var(--font-size-xxs);
line-height: 1em;
}
}
}

View File

@ -0,0 +1,169 @@
import { capitalize, keys } from 'lodash-es';
import { FC, ReactNode, useMemo } from 'react';
import { useColorFunction } from '../../../ColorFunctionContext';
import { ScrollArea } from '../../../components/ScrollArea';
import { SchemeContent } from '../../../models';
import { Q2Baseline, Q2ColorSet, Q2ColorUnit, Q2SchemeStorage } from '../../../q-2-scheme';
import styles from './Preview.module.css';
interface PreviewCellProps {
bg: string;
fg: string;
children: ReactNode;
}
const PreviewCell: FC<PreviewCellProps> = ({ bg, fg, children }) => {
const { colorFn } = useColorFunction();
const wacgRatio = useMemo(() => {
try {
if (!colorFn) return null;
return colorFn.wacg_relative_contrast(fg, bg);
} catch (e) {
console.error('[Error on calc WACG Ratio]', e);
}
return null;
}, [bg, fg]);
return (
<div className={styles.preview_cell} style={{ backgroundColor: `#${bg}`, color: `#${fg}` }}>
<span>{children}</span>
{wacgRatio && <span className={styles.wacg}>WACG {wacgRatio?.toFixed(2)}</span>}
</div>
);
};
interface PreviewLineProps {
name: string;
unit: Q2ColorSet;
}
const PreviewLine: FC<PreviewLineProps> = ({ name, unit }) => {
return (
<div className={styles.preview_unit}>
<PreviewCell bg={unit.root} fg={unit.onRoot}>
{name}
</PreviewCell>
<PreviewCell bg={unit.hover} fg={unit.onRoot}>
{name} Hover
</PreviewCell>
<PreviewCell bg={unit.active} fg={unit.onRoot}>
{name} Active
</PreviewCell>
<PreviewCell bg={unit.focus} fg={unit.onRoot}>
{name} Focus
</PreviewCell>
<PreviewCell bg={unit.disabled} fg={unit.onDisabled}>
{name} Disabled
</PreviewCell>
</div>
);
};
interface PreviewSwatchLineProps {
swatch: Record<string, string>;
}
const PreviewSwatchLine: FC<PreviewSwatchLineProps> = ({ swatch }) => {
const cells = useMemo(() => {
const collection: ReactNode[] = [];
for (const key of keys(swatch)) {
const color = swatch[key];
collection.push(
<div className={styles.preview_swatch_cell} style={{ backgroundColor: `#${color}` }} />,
);
}
return collection;
}, [swatch]);
return <div className={styles.preview_swatch}>{cells}</div>;
};
interface PreviewSetProps {
name: string;
colorUnit: Q2ColorUnit;
}
const PreviewSet: FC<PreviewSetProps> = ({ name, colorUnit }) => {
return (
<>
<PreviewLine name={name} unit={colorUnit.root} />
<PreviewLine name={`${name} Surface`} unit={colorUnit.surface} />
<PreviewSwatchLine name={name} swatch={colorUnit.swatch} />
</>
);
};
interface PreviewBlockProps {
baseline: Q2Baseline;
title: string;
}
const PreviewBlock: FC<PreviewBlockProps> = ({ baseline, title }) => {
const customSets = useMemo(() => {
const colors = keys(baseline.custom);
const elements: ReactNode[] = [];
for (const key of colors) {
const color = baseline.custom[key];
elements.push(<PreviewSet name={capitalize(key)} colorUnit={color} />);
}
return elements;
}, [baseline.custom]);
return (
<div className={styles.preview_block} style={{ backgroundColor: `#${baseline.surface.root}` }}>
<h2 style={{ color: `#${baseline.surface.onRoot}` }}>{title}</h2>
<PreviewSet name="Primary" colorUnit={baseline.primary} />
<PreviewSet name="Secondary" colorUnit={baseline.secondary} />
<PreviewSet name="Tertiary" colorUnit={baseline.tertiary} />
<PreviewSet name="Accent" colorUnit={baseline.accent} />
<PreviewSet name="Danger" colorUnit={baseline.danger} />
<PreviewSet name="Success" colorUnit={baseline.success} />
<PreviewSet name="Warn" colorUnit={baseline.warn} />
<PreviewSet name="Info" colorUnit={baseline.info} />
<PreviewLine name="Neutral" unit={baseline.neutral} />
<PreviewLine name="Neutral Variant" unit={baseline.neutralVariant} />
<PreviewLine name="Surface" unit={baseline.surface} />
<PreviewLine name="Surface Variant" unit={baseline.surfaceVariant} />
<div className={styles.preview_indi_block}>
<PreviewCell bg={baseline.shadow} fg={baseline.surface.root.onRoot}>
Shadow
</PreviewCell>
<PreviewCell bg={baseline.overlay} fg={baseline.surface.root.onRoot}>
Overlay
</PreviewCell>
<PreviewCell bg={baseline.outline} fg={baseline.surface.root.onRoot}>
Outline
</PreviewCell>
<PreviewCell bg={baseline.outlineVariant} fg={baseline.surface.root.onRoot}>
Outline Variant
</PreviewCell>
</div>
{customSets}
</div>
);
};
interface PreviewProps {
scheme: SchemeContent<Q2SchemeStorage>;
}
const Q2SchemePreview: FC<PreviewProps> = ({ scheme }) => {
return (
<ScrollArea enableY>
<div className={styles.preview_layout}>
<div className={styles.preview_layout}>
{scheme.schemeStorage.scheme?.light && (
<PreviewBlock baseline={scheme.schemeStorage.scheme.light} title="Light Scheme" />
)}
{scheme.schemeStorage.scheme?.dark && (
<PreviewBlock baseline={scheme.schemeStorage.scheme.dark} title="Dark Scheme" />
)}
</div>
</div>
</ScrollArea>
);
};
export default Q2SchemePreview;

View File

@ -13,7 +13,7 @@ export type Q2ColorSet = {
export type Q2ColorUnit = {
root: Q2ColorSet;
surface: Q2ColorSet;
swatch: Map<string, string>;
swatch: Record<string, string>;
};
export type Q2Baseline = {
@ -33,7 +33,7 @@ export type Q2Baseline = {
overlay: string;
outline: string;
outlineVariant: string;
custom: Map<string, Q2ColorUnit>;
custom: Record<string, Q2ColorUnit>;
};
export type Q2Scheme = {