mirror of
https://github.com/dream-num/univer.git
synced 2026-09-24 15:32:18 +08:00
feat: add theme builder example (#6721)
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { EditorMode, TokenDensity } from '../types';
|
||||
import { Button } from '@univerjs/design';
|
||||
import { ToolbarField, ToolbarToggleGroup } from './toolbar-controls';
|
||||
|
||||
export function SidebarHeader(props: {
|
||||
darkMode: boolean;
|
||||
editorMode: EditorMode;
|
||||
tokenDensity: TokenDensity;
|
||||
onDarkModeChange: (darkMode: boolean) => void;
|
||||
onEditorModeChange: (mode: EditorMode) => void;
|
||||
onPresetApply: (presetKey: 'default' | 'green') => void;
|
||||
onTokenDensityChange: (density: TokenDensity) => void;
|
||||
}) {
|
||||
const {
|
||||
darkMode,
|
||||
editorMode,
|
||||
tokenDensity,
|
||||
onDarkModeChange,
|
||||
onEditorModeChange,
|
||||
onPresetApply,
|
||||
onTokenDensityChange,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div className="univer-p-3">
|
||||
<div className="univer-flex univer-flex-wrap univer-items-center univer-justify-between univer-gap-2.5">
|
||||
<h2
|
||||
className="
|
||||
univer-m-0 univer-text-lg univer-font-semibold univer-text-slate-950
|
||||
dark:!univer-text-white
|
||||
"
|
||||
>
|
||||
Theme Customizer
|
||||
</h2>
|
||||
|
||||
<div className="univer-flex univer-flex-wrap univer-gap-2">
|
||||
<Button size="small" onClick={() => onPresetApply('default')}>
|
||||
Reset Default
|
||||
</Button>
|
||||
<Button size="small" onClick={() => onPresetApply('green')}>
|
||||
Apply Green
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="univer-mt-2.5">
|
||||
<div className="univer-flex univer-flex-wrap univer-items-center univer-gap-x-4 univer-gap-y-2">
|
||||
<ToolbarField label="Appearance">
|
||||
<ToolbarToggleGroup
|
||||
items={[
|
||||
{ label: 'Light', value: 'light' },
|
||||
{ label: 'Dark', value: 'dark' },
|
||||
]}
|
||||
value={darkMode ? 'dark' : 'light'}
|
||||
onChange={(value) => onDarkModeChange(value === 'dark')}
|
||||
/>
|
||||
</ToolbarField>
|
||||
|
||||
<ToolbarField label="Mode">
|
||||
<ToolbarToggleGroup
|
||||
items={[
|
||||
{ label: 'Token', value: 'tokens' },
|
||||
{ label: 'JSON', value: 'json' },
|
||||
]}
|
||||
value={editorMode}
|
||||
onChange={(value) => onEditorModeChange(value as EditorMode)}
|
||||
/>
|
||||
</ToolbarField>
|
||||
|
||||
{editorMode === 'tokens' && (
|
||||
<ToolbarField label="Scope">
|
||||
<ToolbarToggleGroup
|
||||
items={[
|
||||
{ label: 'Core Palette', value: 'core' },
|
||||
{ label: 'Full Schema', value: 'full' },
|
||||
]}
|
||||
value={tokenDensity}
|
||||
onChange={(value) => onTokenDensityChange(value as TokenDensity)}
|
||||
/>
|
||||
</ToolbarField>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,401 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { Theme } from '@univerjs/themes';
|
||||
import type { ReactNode } from 'react';
|
||||
import type { LoopColorKey, ThemeScaleKey, ThemeShadeKey } from '../types';
|
||||
import { Button, clsx, ColorPicker, Dropdown, FormLayout, Input, Select, Textarea } from '@univerjs/design';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { COLOR_SHADE_KEYS, LOOP_COLOR_KEYS, LOOP_COLOR_OPTIONS } from '../constants';
|
||||
import { normalizeHexColor } from '../theme-utils';
|
||||
|
||||
export function ThemeCodeBlock({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<pre
|
||||
className={clsx(`
|
||||
univer-m-0 univer-overflow-x-auto univer-rounded-xl univer-bg-slate-950 univer-p-4 univer-font-mono
|
||||
univer-text-xs univer-leading-6 univer-text-slate-100
|
||||
`)}
|
||||
>
|
||||
{children}
|
||||
</pre>
|
||||
);
|
||||
}
|
||||
|
||||
export function ThemeColorField(props: {
|
||||
label: string;
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
}) {
|
||||
const { label, value, onChange } = props;
|
||||
const [draftValue, setDraftValue] = useState(value);
|
||||
|
||||
useEffect(() => {
|
||||
setDraftValue(value);
|
||||
}, [value]);
|
||||
|
||||
const error = draftValue.trim().length > 0 && !normalizeHexColor(draftValue) ? 'Enter a valid HEX value, for example #466AF7.' : undefined;
|
||||
|
||||
function handleInputChange(nextValue: string) {
|
||||
setDraftValue(nextValue);
|
||||
|
||||
const normalizedValue = normalizeHexColor(nextValue);
|
||||
|
||||
if (normalizedValue) {
|
||||
onChange(normalizedValue);
|
||||
}
|
||||
}
|
||||
|
||||
function handleBlur() {
|
||||
const normalizedValue = normalizeHexColor(draftValue);
|
||||
setDraftValue(normalizedValue ?? value);
|
||||
}
|
||||
|
||||
return (
|
||||
<FormLayout label={label} error={error} className="univer-mb-0">
|
||||
<Input
|
||||
value={draftValue}
|
||||
onBlur={handleBlur}
|
||||
onChange={handleInputChange}
|
||||
placeholder="#000000"
|
||||
slot={(
|
||||
<Dropdown
|
||||
align="end"
|
||||
side="bottom"
|
||||
overlay={(
|
||||
<div className="univer-p-2">
|
||||
<ColorPicker
|
||||
value={value}
|
||||
onChange={(nextValue) => {
|
||||
const normalizedValue = normalizeHexColor(nextValue);
|
||||
|
||||
if (normalizedValue) {
|
||||
setDraftValue(normalizedValue);
|
||||
onChange(normalizedValue);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={`Choose ${label} color`}
|
||||
className={clsx(`
|
||||
univer-size-5 univer-cursor-pointer univer-rounded-full univer-border univer-border-solid
|
||||
univer-border-slate-300 univer-bg-transparent univer-p-0
|
||||
focus:univer-outline-none focus:univer-ring-2 focus:univer-ring-primary-50
|
||||
`)}
|
||||
style={{ backgroundColor: value }}
|
||||
/>
|
||||
</Dropdown>
|
||||
)}
|
||||
/>
|
||||
</FormLayout>
|
||||
);
|
||||
}
|
||||
|
||||
export function ThemeScaleSection(props: {
|
||||
title: string;
|
||||
scale: ThemeScaleKey;
|
||||
theme: Theme;
|
||||
defaultExpanded?: boolean;
|
||||
onChange: (scale: ThemeScaleKey, shade: ThemeShadeKey, value: string) => void;
|
||||
}) {
|
||||
const { title, scale, theme, defaultExpanded = false, onChange } = props;
|
||||
const [expanded, setExpanded] = useState(defaultExpanded);
|
||||
|
||||
return (
|
||||
<section
|
||||
className={clsx(`
|
||||
univer-rounded-2xl univer-bg-white
|
||||
dark:!univer-bg-gray-800
|
||||
`)}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className={clsx(`
|
||||
univer-flex univer-w-full univer-items-center univer-justify-between univer-border-none
|
||||
univer-bg-transparent univer-px-4 univer-py-3 univer-text-left
|
||||
`)}
|
||||
onClick={() => setExpanded((value) => !value)}
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
className="
|
||||
univer-text-sm univer-font-semibold univer-text-slate-900
|
||||
dark:!univer-text-white
|
||||
"
|
||||
>
|
||||
{title}
|
||||
</div>
|
||||
<div
|
||||
className="
|
||||
univer-mt-1 univer-text-xs univer-text-slate-500
|
||||
dark:!univer-text-gray-300
|
||||
"
|
||||
>
|
||||
{scale}
|
||||
.500
|
||||
{' = '}
|
||||
{theme[scale][500]}
|
||||
</div>
|
||||
</div>
|
||||
<div className="univer-flex univer-items-center univer-gap-2">
|
||||
<span
|
||||
className="
|
||||
univer-inline-flex univer-size-6 univer-rounded-full univer-border univer-border-solid
|
||||
univer-border-slate-200
|
||||
"
|
||||
style={{ backgroundColor: theme[scale][500] }}
|
||||
/>
|
||||
<span
|
||||
className="
|
||||
univer-text-xs univer-text-slate-500
|
||||
dark:!univer-text-gray-300
|
||||
"
|
||||
>
|
||||
{expanded ? 'Collapse' : 'Expand'}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{expanded && (
|
||||
<div
|
||||
className="
|
||||
univer-grid univer-gap-3 univer-p-4
|
||||
sm:univer-grid-cols-2
|
||||
"
|
||||
>
|
||||
{COLOR_SHADE_KEYS.map((shade) => (
|
||||
<ThemeColorField
|
||||
key={shade}
|
||||
label={shade}
|
||||
value={theme[scale][shade]}
|
||||
onChange={(value) => onChange(scale, shade, value)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export function ThemeRootColorsSection(props: {
|
||||
theme: Theme;
|
||||
onChange: (key: 'white' | 'black', value: string) => void;
|
||||
}) {
|
||||
const { theme, onChange } = props;
|
||||
|
||||
return (
|
||||
<section
|
||||
className={clsx(`
|
||||
univer-rounded-2xl univer-bg-white univer-p-4
|
||||
dark:!univer-bg-gray-800
|
||||
`)}
|
||||
>
|
||||
<div
|
||||
className="
|
||||
univer-mb-3 univer-text-sm univer-font-semibold univer-text-slate-900
|
||||
dark:!univer-text-white
|
||||
"
|
||||
>
|
||||
Base Tokens
|
||||
</div>
|
||||
<div
|
||||
className="
|
||||
univer-grid univer-gap-3
|
||||
sm:univer-grid-cols-2
|
||||
"
|
||||
>
|
||||
<ThemeColorField label="white" value={theme.white} onChange={(value) => onChange('white', value)} />
|
||||
<ThemeColorField label="black" value={theme.black} onChange={(value) => onChange('black', value)} />
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export function ThemeLoopColorSection(props: {
|
||||
theme: Theme;
|
||||
onChange: (key: LoopColorKey, value: string) => void;
|
||||
}) {
|
||||
const { theme, onChange } = props;
|
||||
|
||||
return (
|
||||
<section
|
||||
className={clsx(`
|
||||
univer-rounded-2xl univer-bg-white univer-p-4
|
||||
dark:!univer-bg-gray-800
|
||||
`)}
|
||||
>
|
||||
<div
|
||||
className="
|
||||
univer-mb-3 univer-text-sm univer-font-semibold univer-text-slate-900
|
||||
dark:!univer-text-white
|
||||
"
|
||||
>
|
||||
loop-color
|
||||
</div>
|
||||
<div
|
||||
className="
|
||||
univer-grid univer-gap-3
|
||||
sm:univer-grid-cols-2
|
||||
"
|
||||
>
|
||||
{LOOP_COLOR_KEYS.map((key) => (
|
||||
<FormLayout key={key} label={key} className="univer-mb-0">
|
||||
<Select
|
||||
value={(theme['loop-color'] as Record<string, string>)[key]}
|
||||
options={LOOP_COLOR_OPTIONS}
|
||||
onChange={(value) => onChange(key, value)}
|
||||
/>
|
||||
</FormLayout>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export function IntegrationExampleSection(props: {
|
||||
copyLabel: string;
|
||||
onCopy: () => void;
|
||||
}) {
|
||||
const { copyLabel, onCopy } = props;
|
||||
|
||||
return (
|
||||
<section
|
||||
className={clsx(`
|
||||
univer-rounded-2xl univer-bg-slate-50 univer-p-4
|
||||
dark:!univer-bg-gray-800
|
||||
`)}
|
||||
>
|
||||
<div className="univer-mb-3 univer-flex univer-items-center univer-justify-between">
|
||||
<div
|
||||
className="
|
||||
univer-text-sm univer-font-semibold univer-text-slate-900
|
||||
dark:!univer-text-white
|
||||
"
|
||||
>
|
||||
Integration Example
|
||||
</div>
|
||||
<Button size="middle" onClick={onCopy}>
|
||||
{copyLabel}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<ThemeCodeBlock>
|
||||
{`import { Univer } from '@univerjs/core';
|
||||
import { customTheme } from './custom-theme';
|
||||
|
||||
const univer = new Univer({
|
||||
theme: customTheme,
|
||||
locale: LocaleType.EN_US,
|
||||
});`}
|
||||
</ThemeCodeBlock>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export function JsonEditorPanel(props: {
|
||||
copyLabel: string;
|
||||
jsonDraft: string;
|
||||
jsonError: string | null;
|
||||
onCopy: () => void;
|
||||
onFormatCurrent: () => void;
|
||||
onJsonChange: (value: string) => void;
|
||||
onSyncCurrent: () => void;
|
||||
onViewDefault: () => void;
|
||||
}) {
|
||||
const { copyLabel, jsonDraft, jsonError, onCopy, onFormatCurrent, onJsonChange, onSyncCurrent, onViewDefault } = props;
|
||||
|
||||
return (
|
||||
<section
|
||||
className={clsx(`
|
||||
univer-rounded-2xl univer-bg-slate-50 univer-p-4
|
||||
dark:!univer-bg-gray-800
|
||||
`)}
|
||||
>
|
||||
<div className="univer-flex univer-flex-wrap univer-items-center univer-justify-between univer-gap-3">
|
||||
<div>
|
||||
<div
|
||||
className="
|
||||
univer-text-sm univer-font-semibold univer-text-slate-900
|
||||
dark:!univer-text-white
|
||||
"
|
||||
>
|
||||
Theme JSON
|
||||
</div>
|
||||
<p
|
||||
className="
|
||||
univer-m-0 univer-mt-1 univer-text-xs univer-leading-5 univer-text-slate-500
|
||||
dark:!univer-text-gray-300
|
||||
"
|
||||
>
|
||||
A valid JSON patch is merged and applied to the running `ThemeService` immediately.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="univer-flex univer-flex-wrap univer-gap-2">
|
||||
<Button size="middle" onClick={onSyncCurrent}>
|
||||
Sync Current Theme
|
||||
</Button>
|
||||
<Button size="middle" onClick={onViewDefault}>
|
||||
View Default
|
||||
</Button>
|
||||
<Button size="middle" onClick={onCopy}>
|
||||
{copyLabel}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="univer-mt-4">
|
||||
<Textarea
|
||||
value={jsonDraft}
|
||||
spellCheck={false}
|
||||
className={clsx(`
|
||||
univer-h-[640px] univer-rounded-2xl univer-bg-slate-950 univer-p-4 univer-font-mono univer-text-sm
|
||||
univer-leading-6 univer-text-slate-100
|
||||
`, jsonError ? 'univer-border-red-500' : 'univer-border-slate-700')}
|
||||
style={{
|
||||
color: '#E2E8F0',
|
||||
backgroundColor: '#020617',
|
||||
caretColor: '#F8FAFC',
|
||||
}}
|
||||
onValueChange={onJsonChange}
|
||||
/>
|
||||
<div
|
||||
className="
|
||||
univer-mt-3 univer-flex univer-flex-wrap univer-items-center univer-justify-between univer-gap-3
|
||||
"
|
||||
>
|
||||
<div
|
||||
className={clsx('univer-text-xs univer-leading-5', jsonError
|
||||
? 'univer-text-red-500'
|
||||
: `
|
||||
univer-text-slate-500
|
||||
dark:!univer-text-gray-300
|
||||
`)}
|
||||
>
|
||||
{jsonError}
|
||||
</div>
|
||||
<Button size="middle" variant="primary" onClick={onFormatCurrent}>
|
||||
Format Current Result
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { Theme } from '@univerjs/themes';
|
||||
import type { LoopColorKey, ThemeScaleKey, ThemeShadeKey } from '../types';
|
||||
import { IntegrationExampleSection, ThemeLoopColorSection, ThemeRootColorsSection, ThemeScaleSection } from './theme-sections';
|
||||
|
||||
export function TokenEditorPanel(props: {
|
||||
copyLabel: string;
|
||||
theme: Theme;
|
||||
visibleScaleKeys: ThemeScaleKey[];
|
||||
onCopy: () => void;
|
||||
onLoopColorChange: (key: LoopColorKey, value: string) => void;
|
||||
onRootColorChange: (key: 'white' | 'black', value: string) => void;
|
||||
onScaleColorChange: (scale: ThemeScaleKey, shade: ThemeShadeKey, value: string) => void;
|
||||
}) {
|
||||
const { copyLabel, theme, visibleScaleKeys, onCopy, onLoopColorChange, onRootColorChange, onScaleColorChange } = props;
|
||||
|
||||
return (
|
||||
<div className="univer-flex univer-flex-col univer-gap-4">
|
||||
<ThemeRootColorsSection theme={theme} onChange={onRootColorChange} />
|
||||
|
||||
{visibleScaleKeys.map((scale, index) => (
|
||||
<ThemeScaleSection
|
||||
key={scale}
|
||||
title={scale}
|
||||
scale={scale}
|
||||
theme={theme}
|
||||
defaultExpanded={index < 2}
|
||||
onChange={onScaleColorChange}
|
||||
/>
|
||||
))}
|
||||
|
||||
<ThemeLoopColorSection theme={theme} onChange={onLoopColorChange} />
|
||||
<IntegrationExampleSection copyLabel={copyLabel} onCopy={onCopy} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { ReactNode } from 'react';
|
||||
import { clsx } from '@univerjs/design';
|
||||
|
||||
export function ToolbarField(props: { label: string; children: ReactNode }) {
|
||||
const { label, children } = props;
|
||||
|
||||
return (
|
||||
<div className="univer-inline-flex univer-items-center univer-gap-2">
|
||||
<div
|
||||
className="
|
||||
univer-shrink-0 univer-text-[11px] univer-font-medium univer-uppercase univer-tracking-[0.06em]
|
||||
univer-text-slate-500
|
||||
dark:!univer-text-gray-400
|
||||
"
|
||||
>
|
||||
{label}
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ToolbarToggleGroup(props: {
|
||||
items: Array<{ label: string; value: string }>;
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
}) {
|
||||
const { items, value, onChange } = props;
|
||||
|
||||
return (
|
||||
<div className="univer-inline-flex univer-items-center univer-gap-1">
|
||||
{items.map((item) => {
|
||||
const active = item.value === value;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={item.value}
|
||||
type="button"
|
||||
className={clsx(`
|
||||
univer-cursor-pointer univer-rounded-md univer-border-none univer-bg-transparent univer-px-2.5
|
||||
univer-py-1 univer-text-sm univer-font-medium univer-transition-colors
|
||||
`, active
|
||||
? ''
|
||||
: `
|
||||
univer-text-slate-600
|
||||
hover:univer-bg-slate-100 hover:univer-text-slate-900
|
||||
dark:!univer-text-gray-300
|
||||
dark:hover:!univer-bg-gray-800 dark:hover:!univer-text-white
|
||||
`)}
|
||||
style={active
|
||||
? {
|
||||
backgroundColor: 'var(--univer-primary-600)',
|
||||
color: '#FFFFFF',
|
||||
}
|
||||
: undefined}
|
||||
onClick={() => onChange(item.value)}
|
||||
>
|
||||
{item.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { Theme } from '@univerjs/themes';
|
||||
import { LocaleType, LogLevel, ThemeService, Univer, UniverInstanceType } from '@univerjs/core';
|
||||
import { clsx } from '@univerjs/design';
|
||||
import { UniverDocsPlugin } from '@univerjs/docs';
|
||||
import { UniverDocsUIPlugin } from '@univerjs/docs-ui';
|
||||
import { UniverFormulaEnginePlugin } from '@univerjs/engine-formula';
|
||||
import { UniverRenderEnginePlugin } from '@univerjs/engine-render';
|
||||
import { DEFAULT_WORKBOOK_DATA_DEMO } from '@univerjs/mockdata';
|
||||
import enUS from '@univerjs/mockdata/locales/en-US';
|
||||
import { UniverSheetsPlugin } from '@univerjs/sheets';
|
||||
import { UniverSheetsConditionalFormattingPlugin } from '@univerjs/sheets-conditional-formatting';
|
||||
import { UniverSheetsConditionalFormattingUIPlugin } from '@univerjs/sheets-conditional-formatting-ui';
|
||||
import { UniverSheetsDataValidationPlugin } from '@univerjs/sheets-data-validation';
|
||||
import { UniverSheetsDataValidationUIPlugin } from '@univerjs/sheets-data-validation-ui';
|
||||
import { UniverSheetsFilterPlugin } from '@univerjs/sheets-filter';
|
||||
import { UniverSheetsFilterUIPlugin } from '@univerjs/sheets-filter-ui';
|
||||
import { UniverSheetsFormulaPlugin } from '@univerjs/sheets-formula';
|
||||
import { UniverSheetsFormulaUIPlugin } from '@univerjs/sheets-formula-ui';
|
||||
import { UniverSheetsHyperLinkPlugin } from '@univerjs/sheets-hyper-link';
|
||||
import { UniverSheetsHyperLinkUIPlugin } from '@univerjs/sheets-hyper-link-ui';
|
||||
import { UniverSheetsNotePlugin } from '@univerjs/sheets-note';
|
||||
import { UniverSheetsNoteUIPlugin } from '@univerjs/sheets-note-ui';
|
||||
import { UniverSheetsNumfmtPlugin } from '@univerjs/sheets-numfmt';
|
||||
import { UniverSheetsNumfmtUIPlugin } from '@univerjs/sheets-numfmt-ui';
|
||||
import { UniverSheetsSortPlugin } from '@univerjs/sheets-sort';
|
||||
import { UniverSheetsSortUIPlugin } from '@univerjs/sheets-sort-ui';
|
||||
import { UniverSheetsTablePlugin } from '@univerjs/sheets-table';
|
||||
import { UniverSheetsTableUIPlugin } from '@univerjs/sheets-table-ui';
|
||||
import { UniverSheetsUIPlugin } from '@univerjs/sheets-ui';
|
||||
import { UniverSheetsZenEditorPlugin } from '@univerjs/sheets-zen-editor';
|
||||
import { UniverUIPlugin } from '@univerjs/ui';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { PREVIEW_CONTAINER_ID } from '../constants';
|
||||
|
||||
export function UniverPreview(props: { theme: Theme; darkMode: boolean }) {
|
||||
const { theme, darkMode } = props;
|
||||
const univerRef = useRef<Univer | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const univer = new Univer({
|
||||
theme,
|
||||
darkMode,
|
||||
locale: LocaleType.EN_US,
|
||||
locales: {
|
||||
[LocaleType.EN_US]: enUS,
|
||||
},
|
||||
logLevel: LogLevel.ERROR,
|
||||
});
|
||||
|
||||
univer.registerPlugins([
|
||||
[UniverDocsPlugin],
|
||||
[UniverRenderEnginePlugin],
|
||||
[UniverUIPlugin, {
|
||||
container: PREVIEW_CONTAINER_ID,
|
||||
ribbonType: 'classic',
|
||||
}],
|
||||
[UniverDocsUIPlugin],
|
||||
[UniverSheetsPlugin, {
|
||||
autoHeightForMergedCells: true,
|
||||
}],
|
||||
[UniverSheetsUIPlugin],
|
||||
[UniverSheetsNumfmtPlugin],
|
||||
[UniverSheetsZenEditorPlugin],
|
||||
[UniverFormulaEnginePlugin],
|
||||
[UniverSheetsFormulaPlugin],
|
||||
[UniverSheetsFormulaUIPlugin],
|
||||
[UniverSheetsDataValidationPlugin],
|
||||
[UniverSheetsDataValidationUIPlugin],
|
||||
[UniverSheetsConditionalFormattingPlugin],
|
||||
[UniverSheetsConditionalFormattingUIPlugin],
|
||||
[UniverSheetsFilterPlugin],
|
||||
[UniverSheetsFilterUIPlugin, { useRemoteFilterValuesGenerator: false }],
|
||||
[UniverSheetsSortPlugin],
|
||||
[UniverSheetsSortUIPlugin],
|
||||
[UniverSheetsHyperLinkPlugin],
|
||||
[UniverSheetsHyperLinkUIPlugin],
|
||||
[UniverSheetsTablePlugin],
|
||||
[UniverSheetsTableUIPlugin],
|
||||
[UniverSheetsNotePlugin],
|
||||
[UniverSheetsNoteUIPlugin],
|
||||
[UniverSheetsNumfmtUIPlugin],
|
||||
]);
|
||||
|
||||
univer.createUnit(UniverInstanceType.UNIVER_SHEET, DEFAULT_WORKBOOK_DATA_DEMO);
|
||||
univerRef.current = univer;
|
||||
|
||||
return () => {
|
||||
univer.dispose();
|
||||
univerRef.current = null;
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (univerRef.current) {
|
||||
univerRef.current.__getInjector().get(ThemeService).setTheme(theme);
|
||||
}
|
||||
}, [theme]);
|
||||
|
||||
useEffect(() => {
|
||||
if (univerRef.current) {
|
||||
univerRef.current.__getInjector().get(ThemeService).setDarkMode(darkMode);
|
||||
}
|
||||
}, [darkMode]);
|
||||
|
||||
return (
|
||||
<div
|
||||
id={PREVIEW_CONTAINER_ID}
|
||||
className={clsx(`
|
||||
univer-h-full univer-min-h-[520px] univer-w-full univer-overflow-hidden univer-rounded-[20px]
|
||||
univer-bg-white
|
||||
`)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { IThemePreset, ThemeScaleKey, ThemeShadeKey, TokenDensity } from './types';
|
||||
import { defaultTheme, greenTheme } from '@univerjs/themes';
|
||||
|
||||
export const PREVIEW_CONTAINER_ID = 'theme-customizer-preview';
|
||||
|
||||
export const COLOR_SHADE_KEYS: ThemeShadeKey[] = ['50', '100', '200', '300', '400', '500', '600', '700', '800', '900'];
|
||||
export const COLOR_SCALE_KEYS: ThemeScaleKey[] = ['primary', 'gray', 'blue', 'red', 'orange', 'yellow', 'green', 'jiqing', 'indigo', 'purple', 'pink'];
|
||||
export const CORE_SCALE_KEYS: ThemeScaleKey[] = ['primary', 'gray', 'blue', 'green', 'red'];
|
||||
export const LOOP_COLOR_KEYS = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'] as const;
|
||||
|
||||
export const LOOP_COLOR_OPTIONS = COLOR_SCALE_KEYS.flatMap((scale) =>
|
||||
COLOR_SHADE_KEYS.map((shade) => ({
|
||||
label: `${scale}.${shade}`,
|
||||
value: `${scale}.${shade}`,
|
||||
}))
|
||||
);
|
||||
|
||||
export const THEME_PRESETS: IThemePreset[] = [
|
||||
{ key: 'default', label: 'defaultTheme', theme: defaultTheme },
|
||||
{ key: 'green', label: 'greenTheme', theme: greenTheme },
|
||||
];
|
||||
|
||||
export const TOKEN_DENSITY_LABELS: Record<TokenDensity, string> = {
|
||||
core: 'Core Palette',
|
||||
full: 'Full Schema',
|
||||
};
|
||||
@@ -0,0 +1,128 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { Theme } from '@univerjs/themes';
|
||||
import type { EditorMode, LoopColorKey, ThemeScaleKey, ThemeShadeKey, TokenDensity } from '../types';
|
||||
import { defaultTheme } from '@univerjs/themes';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { COLOR_SCALE_KEYS, CORE_SCALE_KEYS, THEME_PRESETS } from '../constants';
|
||||
import { cloneTheme, formatTheme, mergeThemePatch, updateLoopColor, updateScaleColor, updateThemeRootColor } from '../theme-utils';
|
||||
|
||||
// eslint-disable-next-line max-lines-per-function
|
||||
export function useThemeCustomizerState() {
|
||||
const [theme, setTheme] = useState<Theme>(() => cloneTheme(defaultTheme));
|
||||
const [editorMode, setEditorMode] = useState<EditorMode>('tokens');
|
||||
const [tokenDensity, setTokenDensity] = useState<TokenDensity>('core');
|
||||
const [darkMode, setDarkMode] = useState(false);
|
||||
const [jsonDraft, setJsonDraft] = useState(() => formatTheme(defaultTheme));
|
||||
const [jsonError, setJsonError] = useState<string | null>(null);
|
||||
const [copyLabel, setCopyLabel] = useState('Copy JSON');
|
||||
|
||||
useEffect(() => {
|
||||
document.documentElement.classList.toggle('univer-dark', darkMode);
|
||||
document.body.classList.toggle('univer-dark', darkMode);
|
||||
|
||||
return () => {
|
||||
document.documentElement.classList.remove('univer-dark');
|
||||
document.body.classList.remove('univer-dark');
|
||||
};
|
||||
}, [darkMode]);
|
||||
|
||||
const visibleScaleKeys = useMemo(() => (
|
||||
tokenDensity === 'core' ? CORE_SCALE_KEYS : COLOR_SCALE_KEYS
|
||||
), [tokenDensity]);
|
||||
|
||||
function applyTheme(nextTheme: Theme) {
|
||||
setTheme(nextTheme);
|
||||
setJsonDraft(formatTheme(nextTheme));
|
||||
setJsonError(null);
|
||||
}
|
||||
|
||||
function handleScaleColorChange(scale: ThemeScaleKey, shade: ThemeShadeKey, value: string) {
|
||||
applyTheme(updateScaleColor(theme, scale, shade, value));
|
||||
}
|
||||
|
||||
function handleRootColorChange(key: 'white' | 'black', value: string) {
|
||||
applyTheme(updateThemeRootColor(theme, key, value));
|
||||
}
|
||||
|
||||
function handleLoopColorChange(key: LoopColorKey, value: string) {
|
||||
applyTheme(updateLoopColor(theme, key, value));
|
||||
}
|
||||
|
||||
function handlePresetApply(presetKey: 'default' | 'green') {
|
||||
const preset = THEME_PRESETS.find((item) => item.key === presetKey);
|
||||
|
||||
if (!preset) {
|
||||
return;
|
||||
}
|
||||
|
||||
applyTheme(cloneTheme(preset.theme));
|
||||
}
|
||||
|
||||
function handleJsonChange(value: string) {
|
||||
setJsonDraft(value);
|
||||
|
||||
try {
|
||||
const parsedValue = JSON.parse(value) as unknown;
|
||||
const mergedTheme = mergeThemePatch(theme, parsedValue);
|
||||
|
||||
if (!mergedTheme) {
|
||||
setJsonError('JSON must be an object.');
|
||||
return;
|
||||
}
|
||||
|
||||
setTheme(mergedTheme);
|
||||
setJsonError(null);
|
||||
} catch (error) {
|
||||
setJsonError(error instanceof Error ? error.message : 'Failed to parse JSON.');
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCopyTheme() {
|
||||
if (!navigator.clipboard) {
|
||||
return;
|
||||
}
|
||||
|
||||
await navigator.clipboard.writeText(formatTheme(theme));
|
||||
setCopyLabel('Copied');
|
||||
|
||||
window.setTimeout(() => {
|
||||
setCopyLabel('Copy JSON');
|
||||
}, 1600);
|
||||
}
|
||||
|
||||
return {
|
||||
copyLabel,
|
||||
darkMode,
|
||||
editorMode,
|
||||
jsonDraft,
|
||||
jsonError,
|
||||
theme,
|
||||
tokenDensity,
|
||||
visibleScaleKeys,
|
||||
setDarkMode,
|
||||
setEditorMode,
|
||||
setJsonDraft,
|
||||
setTokenDensity,
|
||||
handleCopyTheme,
|
||||
handleJsonChange,
|
||||
handleLoopColorChange,
|
||||
handlePresetApply,
|
||||
handleRootColorChange,
|
||||
handleScaleColorChange,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ConfigProvider, render } from '@univerjs/design';
|
||||
import enUS from '@univerjs/mockdata/locales/en-US';
|
||||
import { ThemeCustomizerApp } from './theme-customizer-app';
|
||||
|
||||
import '../global.css';
|
||||
|
||||
render(
|
||||
<ConfigProvider locale={enUS.design} mountContainer={document.body}>
|
||||
<ThemeCustomizerApp />
|
||||
</ConfigProvider>,
|
||||
document.getElementById('app')!
|
||||
);
|
||||
@@ -0,0 +1,142 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { clsx } from '@univerjs/design';
|
||||
import { defaultTheme } from '@univerjs/themes';
|
||||
import { SidebarHeader } from './components/sidebar-header';
|
||||
import { JsonEditorPanel } from './components/theme-sections';
|
||||
import { TokenEditorPanel } from './components/token-editor-panel';
|
||||
import { UniverPreview } from './components/univer-preview';
|
||||
import { useThemeCustomizerState } from './hooks/use-theme-customizer-state';
|
||||
import { cloneTheme, formatTheme } from './theme-utils';
|
||||
|
||||
export function ThemeCustomizerApp() {
|
||||
const {
|
||||
copyLabel,
|
||||
darkMode,
|
||||
editorMode,
|
||||
jsonDraft,
|
||||
jsonError,
|
||||
theme,
|
||||
tokenDensity,
|
||||
visibleScaleKeys,
|
||||
setDarkMode,
|
||||
setEditorMode,
|
||||
setJsonDraft,
|
||||
setTokenDensity,
|
||||
handleCopyTheme,
|
||||
handleJsonChange,
|
||||
handleLoopColorChange,
|
||||
handlePresetApply,
|
||||
handleRootColorChange,
|
||||
handleScaleColorChange,
|
||||
} = useThemeCustomizerState();
|
||||
|
||||
return (
|
||||
<main
|
||||
className={clsx(`
|
||||
univer-box-border univer-h-screen univer-overflow-hidden univer-p-2.5 univer-text-slate-900
|
||||
lg:univer-p-3
|
||||
`, darkMode
|
||||
? 'univer-bg-[linear-gradient(180deg,#0f172a_0%,#111827_100%)] univer-text-slate-100'
|
||||
: 'univer-bg-[linear-gradient(180deg,#f8fafc_0%,#eef2ff_100%)]')}
|
||||
>
|
||||
<div className="univer-mx-auto univer-h-full univer-max-w-[1680px]">
|
||||
<section
|
||||
className="
|
||||
univer-grid univer-h-full univer-gap-3
|
||||
xl:univer-grid-cols-[400px_minmax(0,1fr)]
|
||||
"
|
||||
>
|
||||
<aside
|
||||
className={clsx(`
|
||||
univer-flex univer-h-full univer-min-h-0 univer-flex-col univer-overflow-hidden
|
||||
univer-rounded-[28px] univer-bg-white univer-shadow-[0_16px_48px_rgba(15,23,42,0.08)]
|
||||
dark:!univer-bg-gray-900
|
||||
`)}
|
||||
>
|
||||
<SidebarHeader
|
||||
darkMode={darkMode}
|
||||
editorMode={editorMode}
|
||||
tokenDensity={tokenDensity}
|
||||
onDarkModeChange={setDarkMode}
|
||||
onEditorModeChange={setEditorMode}
|
||||
onPresetApply={handlePresetApply}
|
||||
onTokenDensityChange={setTokenDensity}
|
||||
/>
|
||||
|
||||
<div className="univer-flex-1 univer-overflow-y-auto univer-p-4">
|
||||
{editorMode === 'tokens'
|
||||
? (
|
||||
<TokenEditorPanel
|
||||
copyLabel={copyLabel}
|
||||
theme={theme}
|
||||
visibleScaleKeys={visibleScaleKeys}
|
||||
onCopy={handleCopyTheme}
|
||||
onLoopColorChange={handleLoopColorChange}
|
||||
onRootColorChange={handleRootColorChange}
|
||||
onScaleColorChange={handleScaleColorChange}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<div className="univer-flex univer-h-full univer-flex-col univer-gap-4">
|
||||
<JsonEditorPanel
|
||||
copyLabel={copyLabel}
|
||||
jsonDraft={jsonDraft}
|
||||
jsonError={jsonError}
|
||||
onCopy={handleCopyTheme}
|
||||
onFormatCurrent={() => setJsonDraft(formatTheme(theme))}
|
||||
onJsonChange={handleJsonChange}
|
||||
onSyncCurrent={() => setJsonDraft(formatTheme(theme))}
|
||||
onViewDefault={() => setJsonDraft(formatTheme(cloneTheme(defaultTheme)))}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<section
|
||||
className={clsx(`
|
||||
univer-flex univer-h-full univer-min-h-0 univer-flex-col univer-overflow-hidden
|
||||
univer-rounded-[28px] univer-bg-white univer-shadow-[0_18px_56px_rgba(15,23,42,0.16)]
|
||||
dark:!univer-bg-gray-900
|
||||
`)}
|
||||
>
|
||||
<div
|
||||
className="
|
||||
univer-bg-white univer-p-4
|
||||
dark:!univer-bg-gray-900
|
||||
"
|
||||
>
|
||||
<h2
|
||||
className="
|
||||
univer-m-0 univer-text-base univer-font-semibold univer-text-slate-950
|
||||
dark:!univer-text-white
|
||||
"
|
||||
>
|
||||
Live Preview
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="univer-min-h-[640px] univer-flex-1 univer-overflow-hidden">
|
||||
<UniverPreview theme={theme} darkMode={darkMode} />
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { Theme } from '@univerjs/themes';
|
||||
import type { LoopColorKey, ThemeScaleKey, ThemeShadeKey } from './types';
|
||||
import { COLOR_SCALE_KEYS, COLOR_SHADE_KEYS, LOOP_COLOR_KEYS } from './constants';
|
||||
|
||||
export function cloneTheme(theme: Theme): Theme {
|
||||
return JSON.parse(JSON.stringify(theme)) as Theme;
|
||||
}
|
||||
|
||||
export function normalizeHexColor(value: string): string | null {
|
||||
const trimmedValue = value.trim();
|
||||
if (!trimmedValue) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const prefixedValue = trimmedValue.startsWith('#') ? trimmedValue : `#${trimmedValue}`;
|
||||
const shortHexMatch = prefixedValue.match(/^#([0-9a-fA-F]{3})$/);
|
||||
|
||||
if (shortHexMatch) {
|
||||
const [r, g, b] = shortHexMatch[1].split('');
|
||||
return `#${r}${r}${g}${g}${b}${b}`.toUpperCase();
|
||||
}
|
||||
|
||||
if (/^#([0-9a-fA-F]{6})$/.test(prefixedValue)) {
|
||||
return prefixedValue.toUpperCase();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function formatTheme(theme: Theme): string {
|
||||
return JSON.stringify(theme, null, 4);
|
||||
}
|
||||
|
||||
export function mergeThemePatch(baseTheme: Theme, patch: unknown): Theme | null {
|
||||
if (!patch || typeof patch !== 'object' || Array.isArray(patch)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const nextTheme = cloneTheme(baseTheme);
|
||||
const record = patch as Record<string, unknown>;
|
||||
|
||||
if (typeof record.white === 'string') {
|
||||
nextTheme.white = record.white;
|
||||
}
|
||||
|
||||
if (typeof record.black === 'string') {
|
||||
nextTheme.black = record.black;
|
||||
}
|
||||
|
||||
for (const scale of COLOR_SCALE_KEYS) {
|
||||
const scalePatch = record[scale];
|
||||
|
||||
if (!scalePatch || typeof scalePatch !== 'object' || Array.isArray(scalePatch)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const scaleRecord = scalePatch as Record<string, unknown>;
|
||||
const mergedScale = { ...nextTheme[scale] } as Record<string, string>;
|
||||
|
||||
for (const shade of COLOR_SHADE_KEYS) {
|
||||
if (typeof scaleRecord[shade] === 'string') {
|
||||
mergedScale[shade] = scaleRecord[shade] as string;
|
||||
}
|
||||
}
|
||||
|
||||
nextTheme[scale] = mergedScale as Theme[ThemeScaleKey];
|
||||
}
|
||||
|
||||
const loopColorPatch = record['loop-color'];
|
||||
|
||||
if (loopColorPatch && typeof loopColorPatch === 'object' && !Array.isArray(loopColorPatch)) {
|
||||
const loopRecord = loopColorPatch as Record<string, unknown>;
|
||||
const mergedLoopColor = { ...(nextTheme['loop-color'] as Record<string, string>) };
|
||||
|
||||
for (const key of LOOP_COLOR_KEYS) {
|
||||
if (typeof loopRecord[key] === 'string') {
|
||||
mergedLoopColor[key] = loopRecord[key] as string;
|
||||
}
|
||||
}
|
||||
|
||||
nextTheme['loop-color'] = mergedLoopColor as Theme['loop-color'];
|
||||
}
|
||||
|
||||
return nextTheme;
|
||||
}
|
||||
|
||||
export function updateScaleColor(theme: Theme, scale: ThemeScaleKey, shade: ThemeShadeKey, value: string): Theme {
|
||||
return {
|
||||
...theme,
|
||||
[scale]: {
|
||||
...theme[scale],
|
||||
[shade]: value,
|
||||
},
|
||||
} as Theme;
|
||||
}
|
||||
|
||||
export function updateThemeRootColor(theme: Theme, key: 'white' | 'black', value: string): Theme {
|
||||
return {
|
||||
...theme,
|
||||
[key]: value,
|
||||
};
|
||||
}
|
||||
|
||||
export function updateLoopColor(theme: Theme, key: LoopColorKey, value: string): Theme {
|
||||
return {
|
||||
...theme,
|
||||
'loop-color': {
|
||||
...theme['loop-color'],
|
||||
[key]: value,
|
||||
},
|
||||
} as Theme;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { Theme } from '@univerjs/themes';
|
||||
|
||||
export type ThemeShadeKey = '50' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900';
|
||||
export type ThemeScaleKey = 'primary' | 'gray' | 'blue' | 'red' | 'orange' | 'yellow' | 'green' | 'jiqing' | 'indigo' | 'purple' | 'pink';
|
||||
export type LoopColorKey = '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | '10' | '11' | '12';
|
||||
export type EditorMode = 'tokens' | 'json';
|
||||
export type TokenDensity = 'core' | 'full';
|
||||
|
||||
export interface IThemePreset {
|
||||
key: 'default' | 'green';
|
||||
label: string;
|
||||
theme: Theme;
|
||||
}
|
||||
Reference in New Issue
Block a user