diff --git a/package-lock.json b/package-lock.json index d5161063..433474f1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,9 @@ "@ant-design/icons": "^6.0.0", "@ant-design/v5-patch-for-react-19": "^1.0.3", "@blockly/theme-dark": "^8.0.1", + "@blockly/theme-deuteranopia": "7.0.1", + "@blockly/theme-highcontrast": "7.0.1", + "@blockly/theme-tritanopia": "7.0.1", "@tailwindcss/postcss": "^4.1.10", "@types/react": "^19.0.2", "@types/react-dom": "^19.0.2", @@ -510,6 +513,42 @@ "blockly": "^12.0.0" } }, + "node_modules/@blockly/theme-deuteranopia": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@blockly/theme-deuteranopia/-/theme-deuteranopia-7.0.1.tgz", + "integrity": "sha512-V05Hk2hzQZict47LfzDdSTP+J5HlYiF7de/8LR/bsRQB/ft7UUTraqDLIivYc9gL2alsVtKzq/yFs9wi7FMAqQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.17.0" + }, + "peerDependencies": { + "blockly": "^12.0.0" + } + }, + "node_modules/@blockly/theme-highcontrast": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@blockly/theme-highcontrast/-/theme-highcontrast-7.0.1.tgz", + "integrity": "sha512-dMhysbXf8QtHxuhI1EY5GdZErlfEhjpCogwfzglDKSu8MF2C+5qzOQBxKmqfnEYJl6G9B2HNGw+mEaUo8oel6Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.17.0" + }, + "peerDependencies": { + "blockly": "^12.0.0" + } + }, + "node_modules/@blockly/theme-tritanopia": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@blockly/theme-tritanopia/-/theme-tritanopia-7.0.1.tgz", + "integrity": "sha512-eLqPCmW6xvSYvyTFFE5uz0Bw806LxOmaQrCOzbUywkT41s2ITP06OP1BVQrHdkZSt5whipZYpB1RMGxYxS/Bpw==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.17.0" + }, + "peerDependencies": { + "blockly": "^12.0.0" + } + }, "node_modules/@csstools/color-helpers": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.0.2.tgz", diff --git a/package.json b/package.json index 64aeef22..db6f34d0 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,9 @@ "@ant-design/icons": "^6.0.0", "@ant-design/v5-patch-for-react-19": "^1.0.3", "@blockly/theme-dark": "^8.0.1", + "@blockly/theme-deuteranopia": "7.0.1", + "@blockly/theme-tritanopia": "7.0.1", + "@blockly/theme-highcontrast": "7.0.1", "@tailwindcss/postcss": "^4.1.10", "@types/react": "^19.0.2", "@types/react-dom": "^19.0.2", diff --git a/src/blocks/mrc_class_method_def.ts b/src/blocks/mrc_class_method_def.ts index ae45c326..a8a8cfd9 100644 --- a/src/blocks/mrc_class_method_def.ts +++ b/src/blocks/mrc_class_method_def.ts @@ -20,7 +20,7 @@ * @author alan@porpoiseful.com (Alan Smith) */ import * as Blockly from 'blockly'; -import { MRC_STYLE_CLASS_BLOCKS } from '../themes/styles'; +import { MRC_STYLE_FUNCTIONS } from '../themes/styles'; import { createFieldNonEditableText } from '../fields/FieldNonEditableText' import { createFieldFlydown } from '../fields/field_flydown'; import { Order } from 'blockly/python'; @@ -88,7 +88,7 @@ const CLASS_METHOD_DEF = { this.appendDummyInput("TITLE") .appendField('', 'NAME'); this.setOutput(false); - this.setStyle(MRC_STYLE_CLASS_BLOCKS); + this.setStyle(MRC_STYLE_FUNCTIONS); this.appendStatementInput('STACK').appendField(''); this.mrcParameters = []; this.setPreviousStatement(false); diff --git a/src/reactComponents/BlocklyComponent.tsx b/src/reactComponents/BlocklyComponent.tsx index 3ce48557..0edd3fbb 100644 --- a/src/reactComponents/BlocklyComponent.tsx +++ b/src/reactComponents/BlocklyComponent.tsx @@ -21,8 +21,7 @@ import * as React from 'react'; import * as Blockly from 'blockly/core'; import * as locale from 'blockly/msg/en'; -import * as MrcDarkTheme from '../themes/mrc_theme_dark'; -import * as MrcLightTheme from '../themes/mrc_theme_light'; +import { themes } from '../themes/mrc_themes'; import {pluginInfo as HardwareConnectionsPluginInfo} from '../blocks/utils/connection_checker'; import 'blockly/blocks'; // Includes standard blocks like controls_if, logic_compare, etc. @@ -80,14 +79,15 @@ const BlocklyComponent = React.forwardRef(null); const getBlocklyTheme = (): Blockly.Theme => { - if (theme === 'dark' || theme === 'compact-dark') { - return MrcDarkTheme.theme; + const blocklyTheme = 'mrc_theme_' + theme.replace(/-/g, '_'); + // Find the theme by key + const themeObj = themes.find(t => t.name === blocklyTheme); + if (!themeObj) { + throw new Error(`Theme not found: ${blocklyTheme}`); } - if (theme === 'light' || theme === 'compact') { - return MrcLightTheme.theme; - } - // Default to light theme if unknown - return MrcLightTheme.theme; + + // Return the corresponding Blockly theme + return themeObj; }; /** Creates the Blockly workspace configuration object. */ diff --git a/src/reactComponents/CodeDisplay.tsx b/src/reactComponents/CodeDisplay.tsx index 66a1ba25..4a647a89 100644 --- a/src/reactComponents/CodeDisplay.tsx +++ b/src/reactComponents/CodeDisplay.tsx @@ -55,16 +55,12 @@ const COPY_ERROR_MESSAGE_PREFIX = 'Could not copy code: '; */ export default function CodeDisplay(props: CodeDisplayProps): React.JSX.Element { const syntaxHighligherFromTheme = (theme: string) => { - switch (theme) { - case 'dark': - case 'compact-dark': - return dracula; - case 'light': - case 'compact': - return oneLight; - default: - return dracula; // Default to dracula if theme is unknown + const isDarkTheme = theme.endsWith('-dark') || theme === 'dark'; + + if (isDarkTheme){ + return dracula } + return oneLight } const { token } = Antd.theme.useToken(); @@ -122,4 +118,4 @@ export default function CodeDisplay(props: CodeDisplayProps): React.JSX.Element {renderCodeBlock()} ); -} \ No newline at end of file +} diff --git a/src/reactComponents/ThemeModal.tsx b/src/reactComponents/ThemeModal.tsx index cec3bf6b..bc20eb8e 100644 --- a/src/reactComponents/ThemeModal.tsx +++ b/src/reactComponents/ThemeModal.tsx @@ -9,7 +9,6 @@ import { CheckOutlined, MoonOutlined, SunOutlined, - DesktopOutlined, } from '@ant-design/icons'; export interface ThemeOption { @@ -40,16 +39,28 @@ const THEME_OPTIONS: ThemeOption[] = [ description: 'Easy on the eyes for low-light environments', }, { - key: 'compact', - name: 'Compact Theme', - icon: , - description: 'More content in less space', + key: 'tritanopia', + name: 'Tritanopia Theme', + icon: , + description: 'Designed for those with Tritanopia color blindness', + }, + { + key: 'tritanopia-dark', + name: 'Tritanopia Dark', + icon: , + description: 'Dark theme for those with Tritanopia color blindness', + }, + { + key: 'deuteranopia', + name: 'Deuteranopia Theme', + icon: , + description: 'Designed for those with Deuteranopia color blindness', }, { - key: 'compact-dark', - name: 'Compact Dark', - icon: , - description: 'Dark theme with compact layout', + key: 'deuteranopia-dark', + name: 'Deuteranopia Dark', + icon: , + description: 'Dark theme for those with Deuteranopia color blindness', }, ]; @@ -195,15 +206,16 @@ export default ThemeModal; export const antdThemeFromString = (theme: string): Antd.ThemeConfig => { let compact = false; + if (theme == 'compact-dark') { compact = true; - theme = 'dark'; } else if (theme == 'compact') { compact = true; - theme = 'light'; } - if (theme === 'dark') { + const isDarkTheme = theme.endsWith('-dark') || theme === 'dark'; + + if (isDarkTheme) { return { algorithm: compact ? [Antd.theme.darkAlgorithm, Antd.theme.compactAlgorithm] : Antd.theme.darkAlgorithm, components: { @@ -219,7 +231,7 @@ export const antdThemeFromString = (theme: string): Antd.ThemeConfig => { } } } - else if (theme === 'light') { + else { return { algorithm: compact ? [Antd.theme.defaultAlgorithm, Antd.theme.compactAlgorithm] : Antd.theme.defaultAlgorithm, components: { @@ -230,10 +242,12 @@ export const antdThemeFromString = (theme: string): Antd.ThemeConfig => { triggerColor: '#000000', }, Menu: { + darkItemDisabledColor: '#cccccc', darkItemBg: '#ffffff', darkSubMenuItemBg: '#ffffff', - darkItemColor: '#000000', + darkItemColor: '#666666', darkItemSelectedColor: '#000000', + darkItemHoverColor: '#000000', } } } diff --git a/src/themes/external_themes.d.ts b/src/themes/external_themes.d.ts deleted file mode 100644 index 4bc09431..00000000 --- a/src/themes/external_themes.d.ts +++ /dev/null @@ -1,22 +0,0 @@ -/** - * @license - * Copyright 2025 Porpoiseful LLC - * - * 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 - * - * https://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. - */ - -/** - * @fileoverview External themes (make typescript declarations) - * @author alan@porpoiseful.com (Alan Smith) - */ -declare module '@blockly/theme-dark'; \ No newline at end of file diff --git a/src/themes/mrc_theme_dark.ts b/src/themes/mrc_theme_dark.ts deleted file mode 100644 index e0779b7e..00000000 --- a/src/themes/mrc_theme_dark.ts +++ /dev/null @@ -1,36 +0,0 @@ -/** - * @license - * Copyright 2025 Porpoiseful LLC - * - * 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 - * - * https://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. - */ - -/** - * @fileoverview Dark theme - * @author alan@porpoiseful.com (Alan Smith) - */ -import * as Blockly from 'blockly/core' -import DarkTheme from '@blockly/theme-dark'; -import { add_mrc_styles } from './styles'; - -const THEME_NAME = 'mrc_theme_dark'; - -const create_theme = function() : Blockly.Theme{ - let newTheme = Blockly.Theme.defineTheme(THEME_NAME, { - name: THEME_NAME, - base: DarkTheme, - }); - return add_mrc_styles(newTheme); -} - -export const theme = create_theme(); \ No newline at end of file diff --git a/src/themes/mrc_theme_light.ts b/src/themes/mrc_theme_light.ts deleted file mode 100644 index 2a645e4c..00000000 --- a/src/themes/mrc_theme_light.ts +++ /dev/null @@ -1,35 +0,0 @@ -/** - * @license - * Copyright 2025 Porpoiseful LLC - * - * 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 - * - * https://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. - */ - -/** - * @fileoverview Light theme - * @author alan@porpoiseful.com (Alan Smith) - */ -import * as Blockly from 'blockly/core' -import { add_mrc_styles } from './styles'; - -const THEME_NAME = 'mrc_theme_light'; - -const create_theme = function() : Blockly.Theme{ - let newTheme = Blockly.Theme.defineTheme(THEME_NAME, { - name: THEME_NAME, - base: Blockly.Themes.Classic, - }); - return add_mrc_styles(newTheme); -} - -export const theme = create_theme(); \ No newline at end of file diff --git a/src/themes/mrc_themes.ts b/src/themes/mrc_themes.ts new file mode 100644 index 00000000..ea360e37 --- /dev/null +++ b/src/themes/mrc_themes.ts @@ -0,0 +1,54 @@ +import * as Blockly from 'blockly/core'; + +import DeuteranopiaTheme from '@blockly/theme-deuteranopia'; +import TritanopiaTheme from '@blockly/theme-tritanopia'; +import HighContrastTheme from '@blockly/theme-highcontrast'; + +import { add_mrc_styles } from './styles'; + +export const DARK_THEME_NAME = 'mrc_theme_dark'; +export const LIGHT_THEME_NAME = 'mrc_theme_light'; +export const DEUTERANOPIA_THEME_NAME = 'mrc_theme_deuteranopia'; +export const TRITANOPIA_THEME_NAME = 'mrc_theme_tritanopia'; +export const HIGHCONTRAST_THEME_NAME = 'mrc_theme_highcontrast'; +export const DEUTERANOPIA_DARK_THEME_NAME = 'mrc_theme_deuteranopia_dark'; +export const TRITANOPIA_DARK_THEME_NAME = 'mrc_theme_tritanopia_dark'; +export const HIGHCONTRAST_DARK_THEME_NAME = 'mrc_theme_highcontrast_dark'; + +const create_theme = function (name: string, base: Blockly.Theme, dark: boolean = false): Blockly.Theme { + let newTheme = Blockly.Theme.defineTheme(name, { + name: name, + base: base, + }); + if (dark) { + // These all come from the Blockly Dark theme plugin at + // https://github.com/google/blockly-samples/blob/master/plugins/theme-dark/src/index.ts + newTheme.setComponentStyle('workspaceBackgroundColour', '#1e1e1e'); + newTheme.setComponentStyle('toolboxBackgroundColour', '#333'); + newTheme.setComponentStyle('toolboxForegroundColour', '#fff'); + newTheme.setComponentStyle('flyoutBackgroundColour', '#252526'); + newTheme.setComponentStyle('flyoutForegroundColour', '#ccc'); + newTheme.setComponentStyle('flyoutOpacity', 1); + newTheme.setComponentStyle('scrollbarColour', '#797979'); + newTheme.setComponentStyle('insertionMarkerColour', '#fff'); + newTheme.setComponentStyle('insertionMarkerOpacity', 0.3); + newTheme.setComponentStyle('scrollbarOpacity', 0.4); + newTheme.setComponentStyle('cursorColour', '#d0d0d0'); + } + return add_mrc_styles(newTheme); +}; + +const create_themes = function (): Blockly.Theme[] { + return [ + create_theme(DARK_THEME_NAME, Blockly.Themes.Classic, true), + create_theme(LIGHT_THEME_NAME, Blockly.Themes.Classic), + create_theme(DEUTERANOPIA_THEME_NAME, DeuteranopiaTheme), + create_theme(TRITANOPIA_THEME_NAME, TritanopiaTheme), + create_theme(HIGHCONTRAST_THEME_NAME, HighContrastTheme), + create_theme(DEUTERANOPIA_DARK_THEME_NAME, DeuteranopiaTheme, true), + create_theme(TRITANOPIA_DARK_THEME_NAME, TritanopiaTheme, true), + create_theme(HIGHCONTRAST_DARK_THEME_NAME, HighContrastTheme, true), + ]; +}; + +export const themes = create_themes(); \ No newline at end of file diff --git a/src/themes/styles.ts b/src/themes/styles.ts index dff4a07c..55da8136 100644 --- a/src/themes/styles.ts +++ b/src/themes/styles.ts @@ -36,35 +36,24 @@ export const MRC_STYLE_EVENT_HANDLER = 'mrc_style_event_handler'; export const MRC_CATEGORY_STYLE_COMPONENTS = 'mrc_category_style_components'; export const add_mrc_styles = function (theme: Blockly.Theme): Blockly.Theme { + const procedureStyle = theme.blockStyles['procedure_blocks']; + const variableStyle = theme.blockStyles['variable_blocks']; + theme.setBlockStyle(MRC_STYLE_FUNCTIONS, { - colourPrimary: "#805ba5", - colourSecondary: "#e6deed", - colourTertiary: "#664984", - hat: "" + ...procedureStyle }); theme.setBlockStyle(MRC_STYLE_EVENT_HANDLER, { - colourPrimary: "#805ba5", - colourSecondary: "#e6deed", - colourTertiary: "#664984", + ...procedureStyle, hat: "cap" }); theme.setBlockStyle(MRC_STYLE_EVENTS, { - colourPrimary: "#805ba5", - colourSecondary: "#e6deed", - colourTertiary: "#664984", - hat: "" + ...procedureStyle }); theme.setBlockStyle(MRC_STYLE_ENUM, { - colourPrimary: "#5ba5a5", - colourSecondary: "#deeded", - colourTertiary: "#498484", - hat: "" + ...variableStyle }); theme.setBlockStyle(MRC_STYLE_VARIABLES, { - colourPrimary: "#5ba55b", - colourSecondary: "#deedde", - colourTertiary: "#498449", - hat: "" + ...variableStyle }); theme.setBlockStyle(MRC_STYLE_CLASS_BLOCKS, { colourPrimary: "#4a148c", @@ -72,12 +61,6 @@ export const add_mrc_styles = function (theme: Blockly.Theme): Blockly.Theme { colourTertiary: "#CDB6E9", hat: "" }); - theme.setCategoryStyle(MRC_CATEGORY_STYLE_METHODS, { - colour: '#4A148C', - }); - theme.setCategoryStyle(MRC_CATEGORY_STYLE_COMPONENTS, { - colour: '#4A148C', - }); theme.setBlockStyle(MRC_STYLE_COMMENTS, { colourPrimary: "#5b5ba5", @@ -92,16 +75,16 @@ export const add_mrc_styles = function (theme: Blockly.Theme): Blockly.Theme { hat: "" }); theme.setBlockStyle(MRC_STYLE_MECHANISMS, { - "colourPrimary": "#5b61a5", - "colourSecondary": "#dedfed", - "colourTertiary": "#494e84", + colourPrimary: "#5b61a5", + colourSecondary: "#dedfed", + colourTertiary: "#494e84", hat: "" }); theme.setBlockStyle(MRC_STYLE_COMPONENTS, { - "colourPrimary": "#5b80a5", - "colourSecondary": "#dee6ed", - "colourTertiary": "#496684", + colourPrimary: "#5b80a5", + colourSecondary: "#dee6ed", + colourTertiary: "#496684", hat: "" }); theme.setBlockStyle(MRC_STYLE_PORTS, { @@ -110,6 +93,14 @@ export const add_mrc_styles = function (theme: Blockly.Theme): Blockly.Theme { colourTertiary: "#498449", hat: "" }); + const functionCategoryStyle = theme.categoryStyles['procedure_category']; + + theme.setCategoryStyle(MRC_CATEGORY_STYLE_METHODS, { + ...functionCategoryStyle, + }); + theme.setCategoryStyle(MRC_CATEGORY_STYLE_COMPONENTS, { + colour: '#4A148C', + }); return theme; } \ No newline at end of file