diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 23624e32d..960c8e366 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1157,6 +1157,9 @@ importers: '@mdx-js/react': specifier: ^3.0.0 version: 3.1.0(@types/react@18.0.15)(react@18.2.0) + '@monaco-editor/react': + specifier: ^4.6.0 + version: 4.6.0(monaco-editor@0.52.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) axios: specifier: ^1.7.7 version: 1.7.7 @@ -3492,6 +3495,18 @@ packages: '@types/react': '>=16' react: '>=16' + '@monaco-editor/loader@1.4.0': + resolution: {integrity: sha512-00ioBig0x642hytVspPl7DbQyaSWRaolYie/UFNjoTdvoKPzo6xrXLhTk9ixgIKcLH5b5vDOjVNiGyY+uDCUlg==} + peerDependencies: + monaco-editor: '>= 0.21.0 < 1' + + '@monaco-editor/react@4.6.0': + resolution: {integrity: sha512-RFkU9/i7cN2bsq/iTkurMWOEErmYcY6JiQI3Jn+WeR/FGISH8JbHERjpS9oRuSOPvDMJI0Z8nJeKkbOs9sBYQw==} + peerDependencies: + monaco-editor: '>= 0.25.0 < 1' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + '@napi-rs/wasm-runtime@0.2.5': resolution: {integrity: sha512-kwUxR7J9WLutBbulqg1dfOrMTwhMdXLdcGUhcbCcGwnPLt3gz19uHVdwH1syKVDbE022ZS2vZxOWflFLS0YTjw==} @@ -9081,6 +9096,9 @@ packages: engines: {node: '>=10'} hasBin: true + monaco-editor@0.52.2: + resolution: {integrity: sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==} + mrmime@2.0.0: resolution: {integrity: sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==} engines: {node: '>=10'} @@ -10902,6 +10920,9 @@ packages: starts-with@1.0.2: resolution: {integrity: sha512-QUw5X+IMTGDm1nrdowEdDaA0MNiUmRlQFwpTTXmhuPKQc+7b0h8fOHtlt1zZqcEK5x1Fsitrobo7KEusc+d1rg==} + state-local@1.0.7: + resolution: {integrity: sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==} + state-toggle@1.0.3: resolution: {integrity: sha512-d/5Z4/2iiCnHw6Xzghyhb+GcmF89bxwgXG60wjIiZaxnymbyOmI8Hk4VqHXiVVp6u2ysaskFfXg3ekCj4WNftQ==} @@ -16526,6 +16547,18 @@ snapshots: '@types/react': 18.0.15 react: 18.2.0 + '@monaco-editor/loader@1.4.0(monaco-editor@0.52.2)': + dependencies: + monaco-editor: 0.52.2 + state-local: 1.0.7 + + '@monaco-editor/react@4.6.0(monaco-editor@0.52.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@monaco-editor/loader': 1.4.0(monaco-editor@0.52.2) + monaco-editor: 0.52.2 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + '@napi-rs/wasm-runtime@0.2.5': dependencies: '@emnapi/core': 1.3.1 @@ -24060,6 +24093,8 @@ snapshots: mkdirp@1.0.4: {} + monaco-editor@0.52.2: {} + mrmime@2.0.0: {} ms@2.0.0: {} @@ -26185,6 +26220,8 @@ snapshots: starts-with@1.0.2: {} + state-local@1.0.7: {} + state-toggle@1.0.3: {} statuses@1.5.0: {} diff --git a/website/package.json b/website/package.json index 2717a857f..7d79437ff 100644 --- a/website/package.json +++ b/website/package.json @@ -25,6 +25,7 @@ "@docusaurus/theme-live-codeblock": "^3.5.2", "@easyops-cn/docusaurus-search-local": "^0.44.5", "@mdx-js/react": "^3.0.0", + "@monaco-editor/react": "^4.6.0", "axios": "^1.7.7", "clsx": "^2.0.0", "date-fns": "^3.6.0", @@ -41,13 +42,13 @@ "@docusaurus/module-type-aliases": "^3.5.2", "@docusaurus/tsconfig": "^3.5.2", "@docusaurus/types": "^3.5.2", + "@heroicons/react": "^2.2.0", "@types/react": "^18.0.0", "autoprefixer": "^10.4.20", "d3-array": "^2.4.0", "d3-scale": "^3.2.1", "d3-time": "^1.1.0", "find-cache-dir": "5.0.0", - "@heroicons/react": "^2.2.0", "mdast-util-from-markdown": "^2.0.1", "postcss": "^8.4.38", "prismjs": "^1.29.0", diff --git a/website/src/pages/themes/_components/base-theme-panel.tsx b/website/src/pages/themes/_components/base-theme-panel.tsx index bdbff9dda..470e4ae8c 100644 --- a/website/src/pages/themes/_components/base-theme-panel.tsx +++ b/website/src/pages/themes/_components/base-theme-panel.tsx @@ -1,25 +1,79 @@ -import React from "react"; +import React, { useEffect, useState } from "react"; import Select from "./select"; -import { themes, useTheme } from "../_providers/themeProvider"; +import { CUSTOM_THEME, themes, useTheme } from "../_providers/themeProvider"; import { usePreviewOptions } from "../_providers/previewOptionsProvider"; import PanelHeader from "./panel-header"; +import Editor from "@monaco-editor/react"; +import { Button } from "@site/src/components/button"; +import { stringifyWithoutQuotes } from "../_utils"; -const themeOptions = themes.map((theme) => ({ - label: theme.name, - value: theme.name, -})); +const EDITOR_OPTIONS = { + minimap: { enabled: false }, + fontSize: 12, +}; + +const themeOptions = [ + ...themes.map((theme) => ({ + label: theme.name, + value: theme.name, + })), +]; const BaseThemePanel = () => { - const { baseTheme, onBaseThemeSelect } = useTheme(); + const { baseTheme, onBaseThemeSelect, customThemeConfig } = useTheme(); const { resetPreviewOptions } = usePreviewOptions(); + const [customTheme, setCustomTheme] = useState(() => + stringifyWithoutQuotes(customThemeConfig), + ); + const [error, setError] = useState(null); + + useEffect(() => { + setCustomTheme(stringifyWithoutQuotes(customThemeConfig)); + }, [customThemeConfig]); const handleThemeSelect = (themeName?: string) => { - onBaseThemeSelect(themeName); + const theme = themes.find((t) => t.name === themeName); + if (!theme) return; + onBaseThemeSelect(theme); resetPreviewOptions(); }; + const handleCustomThemeChange = (value: string | undefined) => { + setCustomTheme(value || ""); + setError(null); + }; + + const applyCustomTheme = () => { + try { + const parsedTheme = new Function(`return (${customTheme.trim()});`)(); + if (typeof parsedTheme !== "object" || Array.isArray(parsedTheme)) { + throw new Error("Invalid theme structure. Must be an object."); + } + + onBaseThemeSelect({ + ...CUSTOM_THEME, + config: parsedTheme, + }); + setError(null); + } catch { + setError( + "Invalid JavaScript object. Please check your theme configuration.", + ); + } + }; + + const handleEditorMount = (_, monaco) => { + monaco.languages.json.jsonDefaults.setDiagnosticsOptions({ + validate: false, + enableSchemaRequest: false, + schemas: [], + }); + }; + + const isCustomTheme = baseTheme?.name === CUSTOM_THEME.name; + return ( - <> +
{ options={themeOptions} label="Theme" /> - + {isCustomTheme && ( +
+

+ Extend the theme object below to apply your own theme configuration. +

+
+ +
+ {error &&

{error}

} + +
+ )} +
); }; + export default BaseThemePanel; diff --git a/website/src/pages/themes/_components/color-palette-selector.tsx b/website/src/pages/themes/_components/color-palette-selector.tsx index ddef79a5b..745f85e2d 100644 --- a/website/src/pages/themes/_components/color-palette-selector.tsx +++ b/website/src/pages/themes/_components/color-palette-selector.tsx @@ -4,6 +4,8 @@ import { ColorScalePropType, VictoryThemeDefinition } from "victory"; import { ColorChangeArgs } from "./control"; import clsx from "clsx"; import { usePreviewOptions } from "../_providers/previewOptionsProvider"; +import { useTheme } from "../_providers/themeProvider"; +import { TiPlus } from "react-icons/ti"; type ColorPaletteSelectorProps = { label: string; @@ -23,22 +25,39 @@ const ColorPaletteSelector = ({ className, }: ColorPaletteSelectorProps) => { const { colorScale, updateColorScale } = usePreviewOptions(); + const { updateCustomThemeConfig } = useTheme(); const handleRadioChange = () => { updateColorScale(value); }; const handleColorChange = (newColor, i, cScale) => { - onColorChange({ - newColor, - index: i, - colorScale: cScale, - }); + if (newColor === undefined) { + // Remove color if undefined + const updatedColors = palette?.[cScale]?.filter( + (_, index) => index !== i, + ); + updateCustomThemeConfig(`palette.${cScale}`, updatedColors); + } else { + onColorChange({ + newColor, + index: i, + colorScale: cScale, + }); + } if (colorScale !== cScale) { updateColorScale(cScale); } }; + const handleAddColor = () => { + const updatedColors = [ + ...(palette?.[colorScaleType as string] || []), + "#000000", + ]; + updateCustomThemeConfig(`palette.${colorScaleType}`, updatedColors); + }; + const isSelected = colorScale === value; return ( @@ -69,6 +88,12 @@ const ColorPaletteSelector = ({ } /> ))} + )} diff --git a/website/src/pages/themes/_components/color-picker.tsx b/website/src/pages/themes/_components/color-picker.tsx index f0c4477d0..e0b2a4947 100644 --- a/website/src/pages/themes/_components/color-picker.tsx +++ b/website/src/pages/themes/_components/color-picker.tsx @@ -1,7 +1,8 @@ import React, { useId } from "react"; -import { TiPencil } from "react-icons/ti"; +import { TiMinus, TiPencil } from "react-icons/ti"; import clsx from "clsx"; import Select from "./select"; +import { useTheme } from "../_providers/themeProvider"; type ColorPickerProps = { label?: string; @@ -25,6 +26,7 @@ const ColorPicker = ({ showColorName = false, className, }: ColorPickerProps) => { + const { updateCustomThemeConfig } = useTheme(); const [isPickerOpen, setIsPickerOpen] = React.useState(false); const [colorOption, setColorOption] = React.useState( () => { @@ -50,9 +52,11 @@ const ColorPicker = ({ }; const handleChange = (event: React.ChangeEvent) => { - if (onColorChange) { - onColorChange(event.target.value); - } + onColorChange(event.target.value); + }; + + const handleRemoveColor = () => { + onColorChange(undefined); }; const id = useId(); @@ -106,13 +110,21 @@ const ColorPicker = ({ }} /> {!showColorName && ( -
- -
+ <> +
+ +
+ + )} {showColorName && ( diff --git a/website/src/pages/themes/_components/color-scale-override-selector.tsx b/website/src/pages/themes/_components/color-scale-override-selector.tsx index a3f42eeb2..36456002b 100644 --- a/website/src/pages/themes/_components/color-scale-override-selector.tsx +++ b/website/src/pages/themes/_components/color-scale-override-selector.tsx @@ -6,6 +6,7 @@ import { defaultColorScale, usePreviewOptions, } from "../_providers/previewOptionsProvider"; +import { TiPlus } from "react-icons/ti"; type ColorScaleOverrideSelectorProps = { id: string; @@ -25,11 +26,7 @@ const ColorScaleOverrideSelector = ({ className, }: ColorScaleOverrideSelectorProps) => { const { colorScale, updateColorScale } = usePreviewOptions(); - const hasCustomValue = Array.isArray(value); - const [initialCustomValue] = React.useState( - hasCustomValue ? value : undefined, - ); - + const [showCustomColors, setShowCustomColors] = React.useState(false); const setColorScaleToDefault = useCallback(() => { if (colorScale !== defaultColorScale) { updateColorScale(defaultColorScale); @@ -37,46 +34,66 @@ const ColorScaleOverrideSelector = ({ }, [colorScale, updateColorScale]); const onCheckboxChange = (isChecked) => { - if (isChecked) { - onChange(initialCustomValue); - } else { + setShowCustomColors(isChecked); + if (!isChecked) { onChange(undefined); } setColorScaleToDefault(); }; const handleColorChange = (newColor, index) => { - const newValue = [...(value as string[])]; - newValue[index] = newColor; + if (newColor === undefined) { + // Remove color if undefined + const newValue = [...(value as string[])].filter((_, i) => i !== index); + onChange(newValue); + } else { + const newValue = [...(value as string[])]; + newValue[index] = newColor; + onChange(newValue); + } + setColorScaleToDefault(); + }; + + const handleAddColor = () => { + const newValue = Array.isArray(value) + ? [...(value as string[]), "#000000"] + : ["#000000"]; onChange(newValue); setColorScaleToDefault(); }; return ( -