Feat(Tools): Support theme

This commit is contained in:
2024-11-03 11:59:20 +08:00
parent b3e1b67515
commit e2fe240877
11 changed files with 345 additions and 12 deletions

View File

@@ -24,6 +24,7 @@ interface EditorProps {
options?: IEditorOptions
onJumpFile?: (fileName: string) => void
extraLibs?: ExtraLib[]
onEditorDidMount?: (editor: editor.IStandaloneCodeEditor, monaco: Monaco) => void
}
const Editor = ({
@@ -35,7 +36,8 @@ const Editor = ({
onChange,
options,
onJumpFile,
extraLibs = []
extraLibs = [],
onEditorDidMount
}: EditorProps) => {
const { styles } = useStyles()
const editorRef = useRef<editor.IStandaloneCodeEditor>()
@@ -96,6 +98,8 @@ const Editor = ({
monaco.languages.typescript.typescriptDefaults.addExtraLib(item.content, item.path)
)
onEditorDidMount?.(editor, monaco)
void autoLoadExtraLib(editor, monaco, file.value, onWatch)
}

View File

@@ -1,3 +1,5 @@
import { Monaco } from '@monaco-editor/react'
import { editor } from 'monaco-editor'
import _ from 'lodash'
import useStyles from '@/components/Playground/CodeEditor/index.style'
import FlexBox from '@/components/common/FlexBox'
@@ -22,6 +24,7 @@ interface CodeEditorProps {
selectedFileName?: string
options?: IEditorOptions
extraLibs?: ExtraLib[]
onEditorDidMount?: (editor: editor.IStandaloneCodeEditor, monaco: Monaco) => void
onSelectedFileChange?: (fileName: string) => void
onAddFile?: (fileName: string, files: IFiles) => void
onRemoveFile?: (fileName: string, files: IFiles) => void
@@ -46,6 +49,7 @@ const CodeEditor = ({
onChangeFileContent,
onError,
extraLibs,
onEditorDidMount,
...props
}: CodeEditorProps) => {
const { styles } = useStyles()
@@ -157,6 +161,7 @@ const CodeEditor = ({
onChange={handleOnChangeFileContent}
onJumpFile={handleOnChangeSelectedFile}
extraLibs={extraLibs}
onEditorDidMount={onEditorDidMount}
/>
{errorMsg && <div className={styles.errorMessage}>{errorMsg}</div>}
</FlexBox>

View File

@@ -2,6 +2,7 @@ import Icon from '@ant-design/icons'
import { Background, Controls, MiniMap, Node, Panel, ReactFlow } from '@xyflow/react'
import '@xyflow/react/dist/style.css'
import { AppContext } from '@/App'
import { generateThemeCssVariable } from '@/util/common'
import useStyles from '@/components/Playground/Output/Preview/render.style'
import iframeRaw from '@/components/Playground/Output/Preview/iframe.html?raw'
import devices, { DeviceName } from '@/components/Playground/Output/Preview/devices'
@@ -14,11 +15,12 @@ interface RenderProps {
}
interface IMessage {
type: 'LOADED' | 'ERROR' | 'UPDATE' | 'DONE'
type: 'LOADED' | 'ERROR' | 'UPDATE' | 'DONE' | 'THEME'
msg: string
data: {
compiledCode?: string
zoom?: number
themeSrc?: string
}
}
@@ -57,6 +59,16 @@ const Render = ({ iframeKey, compiledCode, mobileMode = false }: RenderProps) =>
setIsRotate(!isRotate)
}
const loadTheme = () => {
iframeRef.current?.contentWindow?.postMessage(
{
type: 'THEME',
data: { themeSrc: generateThemeCssVariable(theme).styles }
} as IMessage,
'*'
)
}
useEffect(() => {
if (!isLoaded) {
return
@@ -68,8 +80,16 @@ const Render = ({ iframeKey, compiledCode, mobileMode = false }: RenderProps) =>
} as IMessage,
'*'
)
loadTheme()
}, [isLoaded, compiledCode])
useEffect(() => {
if (!isLoaded) {
return
}
loadTheme()
}, [isLoaded, isDarkMode])
return mobileMode ? (
<>
<ReactFlow

View File

@@ -4,13 +4,14 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Preview</title>
<style id="theme-variable"></style>
</head>
<body>
<script>
window.addEventListener("message", ({ data }) => {
if (data?.type === "UPDATE") {
// Record old styles that need to be removed
const appStyleElement = document.querySelectorAll("style:not(style[id$=\"_oxygen_base_style.css\"])") || [];
const appStyleElement = document.querySelectorAll("style:not(:is(style[id$=\"_oxygen_base_style.css\"], style[id=\"theme-variable\"]))") || [];
// Remove old app
const appSrcElement = document.querySelector("#appSrc");
@@ -35,6 +36,10 @@
document.body.appendChild(script);
URL.revokeObjectURL(oldSrc);
}
if (data?.type === "THEME") {
document.querySelector("#theme-variable").textContent = data.data.themeSrc;
}
});
</script>
<script type="module" id="appSrc"></script>

View File

@@ -2,7 +2,7 @@ import { createStyles } from 'antd-style'
export default createStyles(({ token }) => ({
renderRoot: {
border: 'none',
border: `1px solid ${token.colorBorder}`,
height: '100%',
width: '100%',
flex: 1

View File

@@ -7,7 +7,7 @@ import {
DATABASE_SELECT_SUCCESS,
DATABASE_UPDATE_SUCCESS
} from '@/constants/common.constants'
import { message, modal } from '@/util/common'
import { addExtraCssVariable, message, modal } from '@/util/common'
import { utcToLocalTime } from '@/util/datetime'
import { hasPermission } from '@/util/auth'
import editorExtraLibs from '@/util/editorExtraLibs'
@@ -1113,6 +1113,7 @@ const Base = () => {
!hasPermission('system:tool:modify:base')
}
extraLibs={editorExtraLibs}
onEditorDidMount={(_, monaco) => addExtraCssVariable(monaco)}
/>
<div
className={styles.closeEditorBtn}

View File

@@ -2,7 +2,7 @@ import Draggable from 'react-draggable'
import Icon from '@ant-design/icons'
import useStyles from '@/assets/css/pages/system/tools/code.style'
import { DATABASE_NO_RECORD_FOUND, DATABASE_SELECT_SUCCESS } from '@/constants/common.constants'
import { message, modal, checkDesktop } from '@/util/common'
import { message, modal, checkDesktop, addExtraCssVariable } from '@/util/common'
import { navigateToExecute, navigateToRepository } from '@/util/navigation'
import editorExtraLibs from '@/util/editorExtraLibs'
import { r_sys_tool_get_one } from '@/services/system'
@@ -93,6 +93,7 @@ const Code = () => {
selectedFileName={selectedFileName}
onSelectedFileChange={setSelectedFileName}
extraLibs={editorExtraLibs}
onEditorDidMount={(_, monaco) => addExtraCssVariable(monaco)}
/>
</Card>

View File

@@ -7,7 +7,7 @@ import {
DATABASE_SELECT_SUCCESS,
DATABASE_UPDATE_SUCCESS
} from '@/constants/common.constants'
import { message, modal } from '@/util/common'
import { addExtraCssVariable, message, modal } from '@/util/common'
import { utcToLocalTime } from '@/util/datetime'
import { hasPermission } from '@/util/auth'
import editorExtraLibs from '@/util/editorExtraLibs'
@@ -1056,6 +1056,7 @@ const Template = () => {
!hasPermission('system:tool:modify:template')
}
extraLibs={editorExtraLibs}
onEditorDidMount={(_, monaco) => addExtraCssVariable(monaco)}
/>
<div
className={styles.closeEditorBtn}

View File

@@ -8,7 +8,7 @@ import {
TOOL_HAS_BEEN_PUBLISHED,
TOOL_UNDER_REVIEW
} from '@/constants/common.constants'
import { message } from '@/util/common'
import { addExtraCssVariable, message } from '@/util/common'
import { navigateToRepository } from '@/util/navigation'
import editorExtraLibs from '@/util/editorExtraLibs'
import { r_tool_category_get, r_tool_detail, r_tool_update } from '@/services/tool'
@@ -457,6 +457,7 @@ const Edit = () => {
onChangeFileContent={handleOnChangeFileContent}
onSelectedFileChange={setSelectedFileName}
extraLibs={editorExtraLibs}
onEditorDidMount={(_, monaco) => addExtraCssVariable(monaco)}
/>
<Playground.Output
isDarkMode={isDarkMode}

View File

@@ -1,6 +1,6 @@
import useStyles from '@/assets/css/pages/tools/source.style'
import { DATABASE_NO_RECORD_FOUND, DATABASE_SELECT_SUCCESS } from '@/constants/common.constants'
import { message } from '@/util/common'
import { addExtraCssVariable, message } from '@/util/common'
import { getLoginStatus } from '@/util/auth'
import { navigateToRepository, navigateToSource } from '@/util/navigation'
import editorExtraLibs from '@/util/editorExtraLibs'
@@ -98,6 +98,7 @@ const Source = () => {
selectedFileName={selectedFileName}
onSelectedFileChange={setSelectedFileName}
extraLibs={editorExtraLibs}
onEditorDidMount={(_, monaco) => addExtraCssVariable(monaco)}
/>
</Card>
</FitFullscreen>

View File

@@ -1,4 +1,10 @@
import { createRoot } from 'react-dom/client'
import { editor, languages, Position } from 'monaco-editor'
import { Monaco } from '@monaco-editor/react'
import { MessageInstance } from 'antd/es/message/interface'
import { HookAPI } from 'antd/es/modal/useModal'
import { NotificationInstance } from 'antd/es/notification/interface'
import { Theme, css } from 'antd-style'
import { floor } from 'lodash'
import {
STORAGE_COLLAPSE_SIDEBAR_KEY,
@@ -10,9 +16,6 @@ import {
} from '@/constants/common.constants'
import { getLocalStorage, setLocalStorage } from '@/util/browser'
import FullscreenLoadingMask from '@/components/common/FullscreenLoadingMask'
import { MessageInstance } from 'antd/es/message/interface'
import { NotificationInstance } from 'antd/es/notification/interface'
import { HookAPI } from 'antd/es/modal/useModal'
export type ThemeMode = typeof THEME_FOLLOW_SYSTEM | typeof THEME_LIGHT | typeof THEME_DARK
@@ -241,3 +244,294 @@ export const getThemeMode = (): ThemeMode => {
export const setThemeMode = (themeMode: ThemeMode) => {
setLocalStorage(STORAGE_THEME_MODE_KEY, themeMode)
}
const cssColors = [
'blue',
'purple',
'cyan',
'green',
'magenta',
'pink',
'red',
'orange',
'yellow',
'volcano',
'geekblue',
'gold',
'lime'
].reduce((prev: string[], current) => {
let temp: string[] = []
for (let i = 1; i <= 10; i++) {
temp = [...temp, `${current}${i}`]
}
return [...prev, current, ...temp]
}, [])
const cssVariables: string[] = [
...cssColors,
'colorPrimary',
'colorSuccess',
'colorWarning',
'colorError',
'colorInfo',
'colorLink',
'colorTextBase',
'colorBgBase',
'fontFamily',
'fontFamilyCode',
'fontSize',
'lineWidth',
'lineType',
'motionUnit',
'motionBase',
'motionEaseOutCirc',
'motionEaseInOutCirc',
'motionEaseOut',
'motionEaseInOut',
'motionEaseOutBack',
'motionEaseInBack',
'motionEaseInQuint',
'motionEaseOutQuint',
'borderRadius',
'sizeUnit',
'sizeStep',
'sizePopupArrow',
'controlHeight',
'zIndexBase',
'zIndexPopupBase',
'opacityImage',
'wireframe',
'motion',
'colorLinkHover',
'colorText',
'colorTextSecondary',
'colorTextTertiary',
'colorTextQuaternary',
'colorFill',
'colorFillSecondary',
'colorFillTertiary',
'colorFillQuaternary',
'colorBgSolid',
'colorBgSolidHover',
'colorBgSolidActive',
'colorBgLayout',
'colorBgContainer',
'colorBgElevated',
'colorBgSpotlight',
'colorBgBlur',
'colorBorder',
'colorBorderSecondary',
'colorPrimaryBg',
'colorPrimaryBgHover',
'colorPrimaryBorder',
'colorPrimaryBorderHover',
'colorPrimaryHover',
'colorPrimaryActive',
'colorPrimaryTextHover',
'colorPrimaryText',
'colorPrimaryTextActive',
'colorSuccessBg',
'colorSuccessBgHover',
'colorSuccessBorder',
'colorSuccessBorderHover',
'colorSuccessHover',
'colorSuccessActive',
'colorSuccessTextHover',
'colorSuccessText',
'colorSuccessTextActive',
'colorErrorBg',
'colorErrorBgHover',
'colorErrorBgFilledHover',
'colorErrorBgActive',
'colorErrorBorder',
'colorErrorBorderHover',
'colorErrorHover',
'colorErrorActive',
'colorErrorTextHover',
'colorErrorText',
'colorErrorTextActive',
'colorWarningBg',
'colorWarningBgHover',
'colorWarningBorder',
'colorWarningBorderHover',
'colorWarningHover',
'colorWarningActive',
'colorWarningTextHover',
'colorWarningText',
'colorWarningTextActive',
'colorInfoBg',
'colorInfoBgHover',
'colorInfoBorder',
'colorInfoBorderHover',
'colorInfoHover',
'colorInfoActive',
'colorInfoTextHover',
'colorInfoText',
'colorInfoTextActive',
'colorLinkActive',
'colorBgMask',
'colorWhite',
'fontSizeSM',
'fontSizeLG',
'fontSizeXL',
'fontSizeHeading1',
'fontSizeHeading2',
'fontSizeHeading3',
'fontSizeHeading4',
'fontSizeHeading5',
'lineHeight',
'lineHeightLG',
'lineHeightSM',
'lineHeightHeading1',
'lineHeightHeading2',
'lineHeightHeading3',
'lineHeightHeading4',
'lineHeightHeading5',
'sizeXXL',
'sizeXL',
'sizeLG',
'sizeMD',
'sizeMS',
'size',
'sizeSM',
'sizeXS',
'sizeXXS',
'controlHeightSM',
'controlHeightXS',
'controlHeightLG',
'motionDurationFast',
'motionDurationMid',
'motionDurationSlow',
'lineWidthBold',
'borderRadiusXS',
'borderRadiusSM',
'borderRadiusLG',
'borderRadiusOuter',
'colorFillContent',
'colorFillContentHover',
'colorFillAlter',
'colorBgContainerDisabled',
'colorBorderBg',
'colorSplit',
'colorTextPlaceholder',
'colorTextDisabled',
'colorTextHeading',
'colorTextLabel',
'colorTextDescription',
'colorTextLightSolid',
'colorHighlight',
'colorBgTextHover',
'colorBgTextActive',
'colorIcon',
'colorIconHover',
'colorErrorOutline',
'colorWarningOutline',
'fontSizeIcon',
'lineWidthFocus',
'controlOutlineWidth',
'controlInteractiveSize',
'controlItemBgHover',
'controlItemBgActive',
'controlItemBgActiveHover',
'controlItemBgActiveDisabled',
'controlOutline',
'fontWeightStrong',
'opacityLoading',
'linkDecoration',
'linkHoverDecoration',
'linkFocusDecoration',
'controlPaddingHorizontal',
'controlPaddingHorizontalSM',
'paddingXXS',
'paddingXS',
'paddingSM',
'padding',
'paddingMD',
'paddingLG',
'paddingXL',
'paddingContentHorizontalLG',
'paddingContentVerticalLG',
'paddingContentHorizontal',
'paddingContentVertical',
'paddingContentHorizontalSM',
'paddingContentVerticalSM',
'marginXXS',
'marginXS',
'marginSM',
'margin',
'marginMD',
'marginLG',
'marginXL',
'marginXXL',
'boxShadow',
'boxShadowSecondary',
'boxShadowTertiary',
'screenXS',
'screenXSMin',
'screenXSMax',
'screenSM',
'screenSMMin',
'screenSMMax',
'screenMD',
'screenMDMin',
'screenMDMax',
'screenLG',
'screenLGMin',
'screenLGMax',
'screenXL',
'screenXLMin',
'screenXLMax',
'screenXXL',
'screenXXLMin'
]
export const generateThemeCssVariable = (theme: Omit<Theme, 'prefixCls'>) => {
const cssContent = cssVariables
.map((variable) => `--${variable}: ${theme[variable]};`)
.join('\n')
return css`
:root {
${cssContent}
}
`
}
export const addExtraCssVariable = (monaco: Monaco) => {
monaco.languages.registerCompletionItemProvider('css', {
provideCompletionItems: (
model: editor.ITextModel,
position: Position
): languages.ProviderResult<languages.CompletionList> => {
const textUntilPosition = model.getValueInRange({
startLineNumber: 1,
startColumn: 1,
endLineNumber: position.lineNumber,
endColumn: position.column
})
if (!textUntilPosition.match(/var\(([^)]*)$/)) {
return { suggestions: [] }
}
const word = model.getWordUntilPosition(position)
const range = new monaco.Range(
position.lineNumber,
word.startColumn,
position.lineNumber,
word.endColumn
)
return {
suggestions: cssVariables.map(
(variable): languages.CompletionItem => ({
label: `--${variable}`,
kind: monaco.languages.CompletionItemKind.Variable,
insertText: `--${variable}`,
range,
detail: 'Oxygen Theme Variable'
})
)
}
}
})
}