From 6fc34df1a6b45bbe8a15c70b754888ad613bcae2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=99=BD=E7=86=B1?= Date: Mon, 23 Mar 2026 12:47:08 +0800 Subject: [PATCH] feat: add theme builder example (#6721) --- .../components/sidebar-header.tsx | 102 +++++ .../components/theme-sections.tsx | 401 ++++++++++++++++++ .../components/token-editor-panel.tsx | 51 +++ .../components/toolbar-controls.tsx | 80 ++++ .../components/univer-preview.tsx | 130 ++++++ examples/src/theme-customizer/constants.ts | 42 ++ .../hooks/use-theme-customizer-state.ts | 128 ++++++ examples/src/theme-customizer/main.tsx | 28 ++ .../theme-customizer/theme-customizer-app.tsx | 142 +++++++ examples/src/theme-customizer/theme-utils.ts | 128 ++++++ examples/src/theme-customizer/types.ts | 29 ++ 11 files changed, 1261 insertions(+) create mode 100644 examples/src/theme-customizer/components/sidebar-header.tsx create mode 100644 examples/src/theme-customizer/components/theme-sections.tsx create mode 100644 examples/src/theme-customizer/components/token-editor-panel.tsx create mode 100644 examples/src/theme-customizer/components/toolbar-controls.tsx create mode 100644 examples/src/theme-customizer/components/univer-preview.tsx create mode 100644 examples/src/theme-customizer/constants.ts create mode 100644 examples/src/theme-customizer/hooks/use-theme-customizer-state.ts create mode 100644 examples/src/theme-customizer/main.tsx create mode 100644 examples/src/theme-customizer/theme-customizer-app.tsx create mode 100644 examples/src/theme-customizer/theme-utils.ts create mode 100644 examples/src/theme-customizer/types.ts diff --git a/examples/src/theme-customizer/components/sidebar-header.tsx b/examples/src/theme-customizer/components/sidebar-header.tsx new file mode 100644 index 0000000000..ad5e2b1c9e --- /dev/null +++ b/examples/src/theme-customizer/components/sidebar-header.tsx @@ -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 ( +
+
+

+ Theme Customizer +

+ +
+ + +
+
+ +
+
+ + onDarkModeChange(value === 'dark')} + /> + + + + onEditorModeChange(value as EditorMode)} + /> + + + {editorMode === 'tokens' && ( + + onTokenDensityChange(value as TokenDensity)} + /> + + )} +
+
+
+ ); +} diff --git a/examples/src/theme-customizer/components/theme-sections.tsx b/examples/src/theme-customizer/components/theme-sections.tsx new file mode 100644 index 0000000000..cc897beec0 --- /dev/null +++ b/examples/src/theme-customizer/components/theme-sections.tsx @@ -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 ( +
+            {children}
+        
+ ); +} + +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 ( + + + { + const normalizedValue = normalizeHexColor(nextValue); + + if (normalizedValue) { + setDraftValue(normalizedValue); + onChange(normalizedValue); + } + }} + /> + + )} + > + + + {expanded && ( +
+ {COLOR_SHADE_KEYS.map((shade) => ( + onChange(scale, shade, value)} + /> + ))} +
+ )} + + ); +} + +export function ThemeRootColorsSection(props: { + theme: Theme; + onChange: (key: 'white' | 'black', value: string) => void; +}) { + const { theme, onChange } = props; + + return ( +
+
+ Base Tokens +
+
+ onChange('white', value)} /> + onChange('black', value)} /> +
+
+ ); +} + +export function ThemeLoopColorSection(props: { + theme: Theme; + onChange: (key: LoopColorKey, value: string) => void; +}) { + const { theme, onChange } = props; + + return ( +
+
+ loop-color +
+
+ {LOOP_COLOR_KEYS.map((key) => ( + +