diff --git a/.github/workflows/deployment-storybook.yml b/.github/workflows/deployment-storybook.yml index c94a35109..9c5c2105f 100644 --- a/.github/workflows/deployment-storybook.yml +++ b/.github/workflows/deployment-storybook.yml @@ -24,6 +24,13 @@ jobs: - uses: actions/checkout@main with: fetch-depth: 0 + - name: Workflow git state + run: | + echo github.ref: ${{ github.ref }} + echo github.event_name: ${{ github.event_name }} + echo github.actor: ${{ github.actor }} + git status + git log --oneline -1 - uses: actions/setup-node@main with: node-version: "18" @@ -32,7 +39,7 @@ jobs: - name: Create jest results run: yarn test:generate-output - name: Publish to Chromatic - uses: chromaui/action@v11 + uses: chromaui/action@main with: token: ${{ secrets.GITHUB_TOKEN }} projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} diff --git a/.storybook/main.js b/.storybook/main.js index 48794d304..fb5986c91 100644 --- a/.storybook/main.js +++ b/.storybook/main.js @@ -64,7 +64,12 @@ module.exports = { }, webpackFinal: async (config, { configType }) => { // `configType` has a value of 'DEVELOPMENT' or 'PRODUCTION' - + if (configType === "PRODUCTION") { + // remove source maps from production storybook + // this may lead to errors when it is created via github workers + // reason is currently not known + config.devtool = false; + } config.module.rules = [ { test: /\.(png|jpg|gif|svg)(\\?.*)?$/, diff --git a/CHANGELOG.md b/CHANGELOG.md index 9892806ef..bcd756b17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,18 @@ This is a major release, and it might be not compatible with your current usage - component for React Flow v12, displaying new connection lines - `` - component to display a visual tour multi-step tour of the current view +- new color palette that includes 4 sections with 20+ color tints in 5 weights each + - indentity, semantic, layout, extra + - managed via CSS custom properties + - see `README.md` for inf about usage +- SCSS color functions + - `eccgui-color-var`: returns a var of a custom property used for palette color + - `eccgui-color-mix`: mix 2 colors in `srgb`, works with all types of color values and CSS custom properties + - `eccgui-color-rgba`: like `rgba()` but it works also for CSS custom properties +- `colorCalculateDistance()` + - function to calculate the difference between 2 colors using the simple CIE76 formula +- `textToColorHash()` + - function to calculate a color from a text string ### Removed @@ -37,12 +49,16 @@ This is a major release, and it might be not compatible with your current usage - support now v9 and v12 of react flow - `` - use `` by default for new connection lines, you can overwrite it by setting `connectionLineComponent` to `undefined` +- overrite the native SCSS `rgba()` function, so it now works for SCSS color values and CSS custom properties +- `getColorConfiguration()` works with CSS custom properties ### Deprecated - support for React Flow v9 will be removed in v26 - `` - use `` or build it on single `` +- property names returned by `getCOlorConfiguration` were changed to kebab case because they are originally defined via CSS custom properties + - e.g. `graphNode` is now `eccgui-graph-node` and `graphNodeBright` is `eccgui-graph-node-bright` ## [24.4.1] - 2025-08-25 diff --git a/README.md b/README.md index 7356b56b1..c7f87a6ba 100644 --- a/README.md +++ b/README.md @@ -28,22 +28,69 @@ yarn add --dev @types/carbon-components-react ### Configuration -All [configuration variables](https://github.com/eccenca/gui-elements/blob/develop/src/configuration/_variables.scss) can be set before importing the full library or the default configuration but for the main changes you should need to change only a few parameters: - -- Basic colors - - `$eccgui-color-primary`: color for very important buttons and switches - - `$eccgui-color-primary-contrast`: readable text color used on primary color areas - - `$eccgui-color-accent`: color for most conformation buttons, links, etc - - `$eccgui-color-accent-contrast`: readable text color used on accent color areas - - `$eccgui-color-applicationheader-text` - - `$eccgui-color-applicationheader-background` - - `$eccgui-color-workspace-text` - - `$eccgui-color-workspace-background` -- Basic sizes - - `$eccgui-size-typo-base`: size including absolute unit, currently only `px` is supported - - `$eccgui-size-typo-base-lineheight`: only ratio to font size, no unit! - - `$eccgui-size-type-levelratio`: ratio without unit! used to calculate different text sizes based on `$eccgui-size-typo-base` - - `$eccgui-size-block-whitespace`: white space between block level elements, currently only `px` is supported +All [configuration variables](https://github.com/eccenca/gui-elements/blob/develop/src/configuration/_variables.scss) can be set before importing the full library or the default configuration but for the main properties you should need to change only a few parameters + +#### Colors + +Since v25 we use a color palette as basic foundation for color configurations. The palette is defined in 4 sections containing various color tints, each tint includes 5 different weights from 100 (light color) to 900 (dark color). + +The default palette can be overwritten if it is defined before the configuration or full library is imported to your Sass styles. The palette need to be defined entirely, we currently don't support overwriting it partly. + +``` +$eccgui-color-palette-light: ( + "identity": ( + "brand": #fae1cc #f8cd99 #f6b966 #f4a533 #f29100, + "accent": #e5f4fb #aecfe3 #77abca #4186b2 #0a6199, + "text": #f8f8f8 #bcbcbc #818181 #434343 #090909, + "background": #fff #e8e8e8 #d6d6d6 #d4d4d4 #d3d3d3, + ), + "semantic": ( + "info": #e5f4fb #aecfe3 #77aaca #4086b2 #096199, + "success": #e8f5e9 #b2c6b4 #7c967e #466749 #103713, + "warning": #fff3e0 #fad2b3 #f5b287 #f0915a #eb702d, + "danger": #fff5f6 #edbfc0 #db8989 #c95253 #b71c1c, + ), + "layout": ( + "yellow": #fff6d5 #f1ecb5 #e3db79 #d4c93c #c1a500, + "purple": #f4ddf3 #c8a2d1 #9d6eb8 #71378f #480e75, + "magenta": #ffd8e8 #f5a6c3 #e276a4 #be4c80 #59122d, + "pink": #fde4f1 #e6b4ce #d08aae #bb5f8e #711c4d, + "violet": #f4e3f4 #d8b0d8 #b377b3 #904490 #570057, + "indigo": #efe4fb #b89ee0 #8f72c5 #6547aa #3b1e8f, + "cyan": #dff9fc #86d6e5 #5abfd4 #2da9c4 #0092b3, + "teal": #d4f2ec #a3ddd3 #6dc0b2 #479d8d #104c42, + "lime": #cde0d6 #bce3c2 #9dcd99 #7ba66c #87b347, + "amber": #ffe7b8 #ffe9c4 #f9cd8d #eeb757 #ef8f00, + "vermilion": #ffd8cc #e4c4ba #b27a6b #8c4b3a #651c09, + "grey": #f5f6f7 #b7b7b7 #808080 #484848 #1c2329, + ), + "extra": ( + "gold": #fff7d5 #ebd893 #dfc670 #d3b44e #c7a22b, + "silver": #f0f0f0 #dedede #ccc #bababa #a8a8a8, + "bronze": #fbe9db #f2d6bc #eac29d #e1af7e #d89b5f, + ), +); +``` + +All palette colors will be transformed into CSS custom properties automatically and can be referenced by name scheme `--eccgui-color-palette-{groupname}-{colortint}-{colorweight}`, e.g. `--eccgui-color-palette-identity-brand-100`. + +All other colors are based on the palette but it is still possible to set them before the default values are used by importing the configuration or the full library. + +- `$eccgui-color-primary`: color for very important buttons and switches +- `$eccgui-color-primary-contrast`: readable text color used on primary color areas +- `$eccgui-color-accent`: color for most conformation buttons, links, etc +- `$eccgui-color-accent-contrast`: readable text color used on accent color areas +- `$eccgui-color-applicationheader-text` +- `$eccgui-color-applicationheader-background` +- `$eccgui-color-workspace-text` +- `$eccgui-color-workspace-background` + +#### Sizes + +- `$eccgui-size-typo-base`: size including absolute unit, currently only `px` is supported +- `$eccgui-size-typo-base-lineheight`: only ratio to font size, no unit! +- `$eccgui-size-type-levelratio`: ratio without unit! used to calculate different text sizes based on `$eccgui-size-typo-base` +- `$eccgui-size-block-whitespace`: white space between block level elements, currently only `px` is supported ## Development @@ -125,7 +172,7 @@ After you tested the GUI elements package locally you can Clean up your applicat 3. Pull request from release branch into `main` need to be approved - then ["Publish: final release "](https://github.com/eccenca/gui-elements/actions/workflows/publish-final-release.yml) can be used on `main` (or `next` and `legacy`) to publish final release packages - another PR is automatically created for changes done during publishing process - + ## License Apache License, Version 2.0, January 2004 diff --git a/src/_shame.scss b/src/_shame.scss index e808a65f0..cc2509725 100644 --- a/src/_shame.scss +++ b/src/_shame.scss @@ -51,6 +51,6 @@ textarea:focus-visible { } :focus-visible { - outline: rgba($eccgui-color-accent, $eccgui-opacity-muted) solid 2px; + outline: eccgui-color-rgba($eccgui-color-accent, $eccgui-opacity-muted) solid 2px; outline-offset: 1px; } diff --git a/src/cmem/markdown/Markdown.stories.tsx b/src/cmem/markdown/Markdown.stories.tsx index d067e7912..2754e20cf 100644 --- a/src/cmem/markdown/Markdown.stories.tsx +++ b/src/cmem/markdown/Markdown.stories.tsx @@ -1,5 +1,4 @@ import React from "react"; -import { Blockquote } from "@blueprintjs/core"; import { Meta, StoryFn } from "@storybook/react"; import { Markdown } from "./../../../index"; diff --git a/src/cmem/react-flow/ReactFlow/ReactFlow.tsx b/src/cmem/react-flow/ReactFlow/ReactFlow.tsx index 0caa31bac..17d56e096 100644 --- a/src/cmem/react-flow/ReactFlow/ReactFlow.tsx +++ b/src/cmem/react-flow/ReactFlow/ReactFlow.tsx @@ -1,6 +1,6 @@ -import React, {ReactElement, Ref} from "react"; +import React, { ReactElement, Ref } from "react"; import { KeyCode as KeyCodeV9 } from "react-flow-renderer"; -import { KeyCode as KeyCodeV12} from "@xyflow/react"; +import { KeyCode as KeyCodeV12 } from "@xyflow/react"; import { CLASSPREFIX as eccgui } from "../../../configuration/constants"; import { ReactFlowMarkers } from "../../../extensions/react-flow/markers/ReactFlowMarkers"; @@ -63,10 +63,15 @@ interface ReactFlowExtendedVersion12SupportProps { scrollOnDrag?: never; } -export type ReactFlowExtendedPropsV9 = ReactFlowExtendedVersion9SupportProps & ReactFlowV9ContainerProps & ReactFlowExtendedExtraProps & ReactFlowExtendedScrollProps -export type ReactFlowExtendedPropsV12 = ReactFlowExtendedVersion12SupportProps & ReactFlowV12ContainerProps & ReactFlowExtendedExtraProps +export type ReactFlowExtendedPropsV9 = ReactFlowExtendedVersion9SupportProps & + ReactFlowV9ContainerProps & + ReactFlowExtendedExtraProps & + ReactFlowExtendedScrollProps; +export type ReactFlowExtendedPropsV12 = ReactFlowExtendedVersion12SupportProps & + ReactFlowV12ContainerProps & + ReactFlowExtendedExtraProps; -export type ReactFlowExtendedProps = ReactFlowExtendedPropsV9 | ReactFlowExtendedPropsV12 +export type ReactFlowExtendedProps = ReactFlowExtendedPropsV9 | ReactFlowExtendedPropsV12; /** * `ReactFlow` container extension that includes pre-configured nodes and edges for @@ -74,133 +79,139 @@ export type ReactFlowExtendedProps = ReactFlowExtendedPropsV9 | ReactFlowExtende * * @param T The concrete type of the corresponding version, i.e. either one of ReactFlowExtendedPropsV9 or ReactFlowExtendedPropsV12 */ -const ReactFlowExtendedPlain = ({ - configuration = "unspecified", - flowVersion = ReactFlowVersions.V9, - dropzoneFor, - scrollOnDrag, - children, - className, - selectionKeyCode, - multiSelectionKeyCode, - deleteKeyCode, - zoomActivationKeyCode, - ...originalProps - }: T, - outerRef: Ref - ) => { - const innerRef = React.useRef(null); - React.useImperativeHandle(outerRef, () => innerRef.current!, []); - - React.useEffect(() => { - const reactflowContainer = innerRef?.current; - - if (reactflowContainer && dropzoneFor) { - const addDragover = (event: DragEvent) => { - reactflowContainer.classList.add(`${eccgui}-graphviz__canvas--draghover`); - event.preventDefault(); - }; - - const removeDragover = (event: DragEvent) => { - if (reactflowContainer === event.target) { - reactflowContainer.classList.remove(`${eccgui}-graphviz__canvas--draghover`); - } - }; - - reactflowContainer.addEventListener("dragover", addDragover); - reactflowContainer.addEventListener("dragleave", removeDragover); - reactflowContainer.addEventListener("drop", removeDragover); - return () => { - reactflowContainer.removeEventListener("dragover", addDragover); - reactflowContainer.removeEventListener("dragleave", removeDragover); - reactflowContainer.removeEventListener("drop", removeDragover); - }; - } - return; - }, [innerRef, dropzoneFor]); - - /** If the hot keys should be disabled. By default, they are always disabled. */ - const { hotKeysDisabled } = React.useContext(ReactFlowHotkeyContext); - - const configReactFlow = { - unspecified: unspecifiedConfig, - graph: graphConfig, - workflow: workflowConfig, - linking: linkingConfig, - }; - - const sharedProperties = { - className: `${eccgui}-graphviz__canvas` + (className ? ` ${className}` : ""), - nodeTypes: configReactFlow[configuration].nodeTypes, - edgeTypes: configReactFlow[configuration].edgeTypes, - "data-dropzone-for": dropzoneFor ? dropzoneFor.join(" ") : undefined, - }; - - let keyCodeConfig = {}; - switch (flowVersion) { - case "v9": - keyCodeConfig = { - selectionKeyCode: hotKeysDisabled ? undefined : (selectionKeyCode as KeyCodeV9), - deleteKeyCode: hotKeysDisabled ? undefined : (deleteKeyCode as KeyCodeV9), - multiSelectionKeyCode: hotKeysDisabled ? undefined : (multiSelectionKeyCode as KeyCodeV9), - zoomActivationKeyCode: hotKeysDisabled ? undefined : (zoomActivationKeyCode as KeyCodeV9), - }; - break; - case "v12": - keyCodeConfig = { - selectionKeyCode: hotKeysDisabled ? null : (selectionKeyCode as KeyCodeV12), - deleteKeyCode: hotKeysDisabled ? null : (deleteKeyCode as KeyCodeV12), - multiSelectionKeyCode: hotKeysDisabled ? null : (multiSelectionKeyCode as KeyCodeV12), - zoomActivationKeyCode: hotKeysDisabled ? null : (zoomActivationKeyCode as KeyCodeV12), - }; - break; +const ReactFlowExtendedPlain = ( + { + configuration = "unspecified", + flowVersion = ReactFlowVersions.V9, + dropzoneFor, + scrollOnDrag, + children, + className, + selectionKeyCode, + multiSelectionKeyCode, + deleteKeyCode, + zoomActivationKeyCode, + ...originalProps + }: T, + outerRef: Ref +) => { + const innerRef = React.useRef(null); + React.useImperativeHandle(outerRef, () => innerRef.current!, []); + + React.useEffect(() => { + const reactflowContainer = innerRef?.current; + + if (reactflowContainer && dropzoneFor) { + const addDragover = (event: DragEvent) => { + reactflowContainer.classList.add(`${eccgui}-graphviz__canvas--draghover`); + event.preventDefault(); + }; + + const removeDragover = (event: DragEvent) => { + if (reactflowContainer === event.target) { + reactflowContainer.classList.remove(`${eccgui}-graphviz__canvas--draghover`); + } + }; + + reactflowContainer.addEventListener("dragover", addDragover); + reactflowContainer.addEventListener("dragleave", removeDragover); + reactflowContainer.addEventListener("drop", removeDragover); + return () => { + reactflowContainer.removeEventListener("dragover", addDragover); + reactflowContainer.removeEventListener("dragleave", removeDragover); + reactflowContainer.removeEventListener("drop", removeDragover); + }; } + return; + }, [innerRef, dropzoneFor]); - let scrollOnDragFunctions = {}; - switch (flowVersion) { - case "v9": - scrollOnDragFunctions = useReactFlowScrollOnDragV9({ - reactFlowProps: (originalProps as unknown) as ReactFlowV9ContainerProps, - scrollOnDrag, - }); - break; - // should not be necessary for v12 - } + /** If the hot keys should be disabled. By default, they are always disabled. */ + const { hotKeysDisabled } = React.useContext(ReactFlowHotkeyContext); - const containerConfig = { - ...sharedProperties, - ...keyCodeConfig, - ...originalProps, - ...scrollOnDragFunctions, - }; - - switch (flowVersion) { - case "v9": - return ( - - {children} - - - ); - case "v12": - return ( - - {children} - - - ); - default: - return
; - } + const configReactFlow = { + unspecified: unspecifiedConfig, + graph: graphConfig, + workflow: workflowConfig, + linking: linkingConfig, + }; + + const sharedProperties = { + className: + `${eccgui}-graphviz__canvas` + + ` ${eccgui}-configuration--colors__react-flow-${configuration}` + + (className ? ` ${className}` : ""), + nodeTypes: configReactFlow[configuration].nodeTypes, + edgeTypes: configReactFlow[configuration].edgeTypes, + "data-dropzone-for": dropzoneFor ? dropzoneFor.join(" ") : undefined, + }; + + let keyCodeConfig = {}; + switch (flowVersion) { + case "v9": + keyCodeConfig = { + selectionKeyCode: hotKeysDisabled ? undefined : (selectionKeyCode as KeyCodeV9), + deleteKeyCode: hotKeysDisabled ? undefined : (deleteKeyCode as KeyCodeV9), + multiSelectionKeyCode: hotKeysDisabled ? undefined : (multiSelectionKeyCode as KeyCodeV9), + zoomActivationKeyCode: hotKeysDisabled ? undefined : (zoomActivationKeyCode as KeyCodeV9), + }; + break; + case "v12": + keyCodeConfig = { + selectionKeyCode: hotKeysDisabled ? null : (selectionKeyCode as KeyCodeV12), + deleteKeyCode: hotKeysDisabled ? null : (deleteKeyCode as KeyCodeV12), + multiSelectionKeyCode: hotKeysDisabled ? null : (multiSelectionKeyCode as KeyCodeV12), + zoomActivationKeyCode: hotKeysDisabled ? null : (zoomActivationKeyCode as KeyCodeV12), + }; + break; + } + + let scrollOnDragFunctions = {}; + switch (flowVersion) { + case "v9": + scrollOnDragFunctions = useReactFlowScrollOnDragV9({ + reactFlowProps: originalProps as unknown as ReactFlowV9ContainerProps, + scrollOnDrag, + }); + break; + // should not be necessary for v12 } + const containerConfig = { + ...sharedProperties, + ...keyCodeConfig, + ...originalProps, + ...scrollOnDragFunctions, + }; + + switch (flowVersion) { + case "v9": + return ( + + {children} + + + ); + case "v12": + return ( + + {children} + + + ); + default: + return
; + } +}; + /** Hack to make the Type Parameter work with the forward ref. */ -export const ReactFlowExtended = React.forwardRef(ReactFlowExtendedPlain) as (p: T & { ref?: Ref }) => ReactElement +export const ReactFlowExtended = React.forwardRef(ReactFlowExtendedPlain) as ( + p: T & { ref?: Ref } +) => ReactElement; - /** +/** * @deprecated (v26) use `ReactFlowExtended` */ export const ReactFlow = ReactFlowExtended; /** Classes that when set for an element, prevent that they trigger react-flow dragging, wheel and panning actions. */ -export const preventReactFlowActionsClasses = "nodrag nopan nowheel" +export const preventReactFlowActionsClasses = "nodrag nopan nowheel"; diff --git a/src/cmem/react-flow/_canvas.scss b/src/cmem/react-flow/_canvas.scss index 78311180b..6a7fbd2ed 100644 --- a/src/cmem/react-flow/_canvas.scss +++ b/src/cmem/react-flow/_canvas.scss @@ -1,7 +1,7 @@ .eccgui-graphviz__canvas--draghover { body[data-monitor-dropzone~="application/reactflow"] &[data-dropzone-for~="application/reactflow"], body[data-monitor-dropzone~="Files"] &[data-dropzone-for~="Files"] { - background-color: rgba($eccgui-color-accent, $eccgui-opacity-ghostly); + background-color: eccgui-color-rgba($eccgui-color-accent, $eccgui-opacity-ghostly); box-shadow: 0 0 $eccgui-size-block-whitespace $eccgui-color-accent inset; & > * { diff --git a/src/cmem/react-flow/_edges.scss b/src/cmem/react-flow/_edges.scss index 3ba6db29e..731ca3939 100644 --- a/src/cmem/react-flow/_edges.scss +++ b/src/cmem/react-flow/_edges.scss @@ -1,24 +1,24 @@ -@mixin edgetypestyles($type, $color) { +@mixin edgetypestyles($type) { .react-flow__edge-#{$type} { - @include edgecoloring($color); + @include edgecoloring($type); } } -@mixin edgecoloring($color) { - --#{$eccgui}-reactflow-node-color-default: #{$color}; - --#{$eccgui}-reactflow-node-color-hover: #{$color}; - --#{$eccgui}-reactflow-node-color-selected: #{$color}; +@mixin edgecoloring($type) { + --#{$eccgui}-reactflow-node-color-default: var(--#{$eccgui}-#{$type}-edge); + --#{$eccgui}-reactflow-node-color-hover: var(--#{$eccgui}-#{$type}-edge); + --#{$eccgui}-reactflow-node-color-selected: var(--#{$eccgui}-#{$type}-edge); } // Graph edge types -@include edgetypestyles("implicit", $reactflow-color-implicit-edge); -@include edgetypestyles("import", $reactflow-color-import-edge); -@include edgetypestyles("subclass", $reactflow-color-subclass-edge); -@include edgetypestyles("subproperty", $reactflow-color-subproperty-edge); -@include edgetypestyles("rdftype", $reactflow-color-rdftype-edge); +@include edgetypestyles("implicit"); +@include edgetypestyles("import"); +@include edgetypestyles("subclass"); +@include edgetypestyles("subproperty"); +@include edgetypestyles("rdftype"); // Linking edge types -@include edgetypestyles("value", $reactflow-color-value-edge); -@include edgetypestyles("score", $reactflow-color-score-edge); +@include edgetypestyles("value"); +@include edgetypestyles("score"); diff --git a/src/cmem/react-flow/_handles.scss b/src/cmem/react-flow/_handles.scss index 5d1f07e7d..a753b7458 100644 --- a/src/cmem/react-flow/_handles.scss +++ b/src/cmem/react-flow/_handles.scss @@ -1,34 +1,34 @@ -@mixin handletypestyles($type, $color) { +@mixin handletypestyles($type) { .react-flow__node-#{$type} { - @include handlecoloring($color); + @include handlecoloring($type); } } -@mixin handlecoloring($color) { +@mixin handlecoloring($type) { .react-flow__handle { - color: $color; + color: var(--#{$eccgui}-#{$type}-node); } } // Graph handle types -@include handletypestyles("graph", $reactflow-color-graph-node); -@include handletypestyles("class", $reactflow-color-class-node); -@include handletypestyles("instance", $reactflow-color-instance-node); -@include handletypestyles("property", $reactflow-color-property-node); +@include handletypestyles("graph"); +@include handletypestyles("class"); +@include handletypestyles("instance"); +@include handletypestyles("property"); // Workflow handle types -@include handletypestyles("dataset", $reactflow-color-dataset-node); -@include handletypestyles("linking", $reactflow-color-linking-node); -@include handletypestyles("transform", $reactflow-color-transform-node); -@include handletypestyles("task", $reactflow-color-task-node); -@include handletypestyles("workflow", $reactflow-color-workflow-node); +@include handletypestyles("dataset"); +@include handletypestyles("linking"); +@include handletypestyles("transform"); +@include handletypestyles("task"); +@include handletypestyles("workflow"); // Linking handle types -@include handletypestyles("sourcepath", $reactflow-color-sourcepath-node); -@include handletypestyles("targetpath", $reactflow-color-targetpath-node); -@include handletypestyles("transformation", $reactflow-color-transformation-node); -@include handletypestyles("comparator", $reactflow-color-comparator-node); -@include handletypestyles("aggregator", $reactflow-color-aggregator-node); +@include handletypestyles("sourcepath"); +@include handletypestyles("targetpath"); +@include handletypestyles("transformation"); +@include handletypestyles("comparator"); +@include handletypestyles("aggregator"); diff --git a/src/cmem/react-flow/_minimap.scss b/src/cmem/react-flow/_minimap.scss index 4ba99c3ab..d40c08ec4 100644 --- a/src/cmem/react-flow/_minimap.scss +++ b/src/cmem/react-flow/_minimap.scss @@ -1,38 +1,38 @@ -@mixin mapnodestyles($type, $color) { +@mixin mapnodestyles($type) { .#{$eccgui}-graphviz__minimap__node--#{$type} { - @include mapcoloring($color); + @include mapcoloring($type); } } -@mixin mapcoloring($color) { +@mixin mapcoloring($type) { &:not([fill]) { - fill: $color; + fill: var(--#{$eccgui}-#{$type}-node); } &:not([stroke]) { - stroke: $color; + stroke: var(--#{$eccgui}-#{$type}-node); } } // Graph node type -@include mapnodestyles("graph", $reactflow-color-graph-node); -@include mapnodestyles("class", $reactflow-color-class-node); -@include mapnodestyles("instance", $reactflow-color-instance-node); -@include mapnodestyles("property", $reactflow-color-property-node); +@include mapnodestyles("graph"); +@include mapnodestyles("class"); +@include mapnodestyles("instance"); +@include mapnodestyles("property"); // Workflow node types -@include mapnodestyles("dataset", $reactflow-color-dataset-node); -@include mapnodestyles("linking", $reactflow-color-linking-node); -@include mapnodestyles("transform", $reactflow-color-transform-node); -@include mapnodestyles("task", $reactflow-color-task-node); -@include mapnodestyles("workflow", $reactflow-color-workflow-node); +@include mapnodestyles("dataset"); +@include mapnodestyles("linking"); +@include mapnodestyles("transform"); +@include mapnodestyles("task"); +@include mapnodestyles("workflow"); // Linking node types -@include mapnodestyles("sourcepath", $reactflow-color-sourcepath-node); -@include mapnodestyles("targetpath", $reactflow-color-targetpath-node); -@include mapnodestyles("transformation", $reactflow-color-transformation-node); -@include mapnodestyles("comparator", $reactflow-color-comparator-node); -@include mapnodestyles("aggregator", $reactflow-color-aggregator-node); +@include mapnodestyles("sourcepath"); +@include mapnodestyles("targetpath"); +@include mapnodestyles("transformation"); +@include mapnodestyles("comparator"); +@include mapnodestyles("aggregator"); diff --git a/src/cmem/react-flow/configuration/_colors-graph.scss b/src/cmem/react-flow/configuration/_colors-graph.scss index 96816cba5..e43165b3a 100644 --- a/src/cmem/react-flow/configuration/_colors-graph.scss +++ b/src/cmem/react-flow/configuration/_colors-graph.scss @@ -1,37 +1,20 @@ -$reactflow-color-graph-node: #745a85 !default; -$reactflow-color-class-node: #3a7896 !default; -$reactflow-color-instance-node: #0097a7 !default; -$reactflow-color-property-node: #40a691 !default; -$reactflow-color-implicit-edge: #ae3c74 !default; -$reactflow-color-import-edge: $reactflow-color-graph-node !default; -$reactflow-color-subclass-edge: $reactflow-color-class-node !default; -$reactflow-color-subproperty-edge: $reactflow-color-property-node !default; -$reactflow-color-rdftype-edge: $reactflow-color-instance-node !default; - -@function bright($color) { - @return mix($color, #fff, 24%); -} - -.#{eccgui}-configuration--colors__react-flow-graph { - /* stylelint-disable custom-property-pattern */ - // TODO: we should correct custom property names later but atm this would lead to broken consumer apps - --graphNode: #{$reactflow-color-graph-node}; - --classNode: #{$reactflow-color-class-node}; - --instanceNode: #{$reactflow-color-instance-node}; - --propertyNode: #{$reactflow-color-property-node}; - --implicitEdge: #{$reactflow-color-implicit-edge}; - --importEdge: #{$reactflow-color-import-edge}; - --subclassEdge: #{$reactflow-color-subclass-edge}; - --subpropertyEdge: #{$reactflow-color-subproperty-edge}; - --rdftypeEdge: #{$reactflow-color-rdftype-edge}; - --graphNodeBright: #{bright($reactflow-color-graph-node)}; - --classNodeBright: #{bright($reactflow-color-class-node)}; - --instanceNodeBright: #{bright($reactflow-color-instance-node)}; - --propertyNodeBright: #{bright($reactflow-color-property-node)}; - --implicitEdgeBright: #{bright($reactflow-color-implicit-edge)}; - --importEdgeBright: #{bright($reactflow-color-import-edge)}; - --subclassEdgeBright: #{bright($reactflow-color-subclass-edge)}; - --subpropertyEdgeBright: #{bright($reactflow-color-subproperty-edge)}; - --rdftypeEdgeBright: #{bright($reactflow-color-rdftype-edge)}; - /* stylelint-enable custom-property-pattern */ +.#{$eccgui}-configuration--colors__react-flow-graph { + --#{$eccgui}-graph-node: #{eccgui-color-var("layout", "purple", 700)}; + --#{$eccgui}-graph-node-bright: #{eccgui-color-var("layout", "purple", 100)}; + --#{$eccgui}-class-node: #{eccgui-color-var("layout", "magenta", 900)}; + --#{$eccgui}-class-node-bright: #{eccgui-color-var("layout", "magenta", 100)}; + --#{$eccgui}-instance-node: #{eccgui-color-var("layout", "magenta", 500)}; + --#{$eccgui}-instance-node-bright: #{eccgui-color-var("layout", "magenta", 100)}; + --#{$eccgui}-property-node: #{eccgui-color-var("layout", "teal", 700)}; + --#{$eccgui}-property-node-bright: #{eccgui-color-var("layout", "teal", 100)}; + --#{$eccgui}-implicit-edge: #{eccgui-color-var("identity", "text", 700)}; + --#{$eccgui}-implicit-edge-bright: #{eccgui-color-var("identity", "text", 100)}; + --#{$eccgui}-import-edge: #{eccgui-color-var("layout", "purple", 700)}; + --#{$eccgui}-import-edge-bright: #{eccgui-color-var("layout", "purple", 100)}; + --#{$eccgui}-subclass-edge: #{eccgui-color-var("layout", "magenta", 900)}; + --#{$eccgui}-subclass-edge-bright: #{eccgui-color-var("layout", "magenta", 100)}; + --#{$eccgui}-subproperty-edge: #{eccgui-color-var("layout", "teal", 700)}; + --#{$eccgui}-subproperty-edge-bright: #{eccgui-color-var("layout", "teal", 100)}; + --#{$eccgui}-rdftype-edge: #{eccgui-color-var("layout", "magenta", 500)}; + --#{$eccgui}-rdftype-edge-bright: #{eccgui-color-var("layout", "magenta", 100)}; } diff --git a/src/cmem/react-flow/configuration/_colors-linking.scss b/src/cmem/react-flow/configuration/_colors-linking.scss index bc2e119e6..5b5b06360 100644 --- a/src/cmem/react-flow/configuration/_colors-linking.scss +++ b/src/cmem/react-flow/configuration/_colors-linking.scss @@ -1,28 +1,16 @@ -$reactflow-color-sourcepath-node: #745a85 !default; -$reactflow-color-targetpath-node: #3a7896 !default; -$reactflow-color-transformation-node: #ae3c74 !default; -$reactflow-color-comparator-node: #40a691 !default; -$reactflow-color-aggregator-node: #0097a7 !default; -$reactflow-color-value-edge: #222 !default; -$reactflow-color-score-edge: $reactflow-color-aggregator-node !default; - -@function bright($color) { - @return mix($color, #fff, 24%); -} - .#{eccgui}-configuration--colors__react-flow-linking { - --sourcepathNode: #{$reactflow-color-sourcepath-node}; - --targetpathNode: #{$reactflow-color-targetpath-node}; - --transformationNode: #{$reactflow-color-transformation-node}; - --comparatorNode: #{$reactflow-color-comparator-node}; - --aggregatorNode: #{$reactflow-color-aggregator-node}; - --valueEdge: #{$reactflow-color-value-edge}; - --scoreEdge: #{$reactflow-color-score-edge}; - --sourcepathNodeBright: #{bright($reactflow-color-sourcepath-node)}; - --targetpathNodeBright: #{bright($reactflow-color-targetpath-node)}; - --transformationNodeBright: #{bright($reactflow-color-transformation-node)}; - --comparatorNodeBright: #{bright($reactflow-color-comparator-node)}; - --aggregatorNodeBright: #{bright($reactflow-color-aggregator-node)}; - --valueEdgeBright: #{bright($reactflow-color-value-edge)}; - --scoreEdgeBright: #{bright($reactflow-color-score-edge)}; + --#{$eccgui}-sourcepath-node: #{eccgui-color-var("layout", "violet", 700)}; + --#{$eccgui}-sourcepath-node-bright: #{eccgui-color-var("layout", "violet", 100)}; + --#{$eccgui}-targetpath-node: #{eccgui-color-var("layout", "cyan", 900)}; + --#{$eccgui}-targetpath-node-bright: #{eccgui-color-var("layout", "cyan", 100)}; + --#{$eccgui}-transformation-node: #{eccgui-color-var("layout", "pink", 700)}; + --#{$eccgui}-transformation-node-bright: #{eccgui-color-var("layout", "pink", 100)}; + --#{$eccgui}-comparator-node: #{eccgui-color-var("layout", "teal", 700)}; + --#{$eccgui}-comparator-node-bright: #{eccgui-color-var("layout", "teal", 100)}; + --#{$eccgui}-aggregator-node: #{eccgui-color-var("layout", "cyan", 700)}; + --#{$eccgui}-aggregator-node-bright: #{eccgui-color-var("layout", "cyan", 100)}; + --#{$eccgui}-value-edge: #{eccgui-color-var("layout", "grey", 700)}; + --#{$eccgui}-value-edge-bright: #{eccgui-color-var("layout", "grey", 100)}; + --#{$eccgui}-score-edge: #{eccgui-color-var("layout", "cyan", 900)}; + --#{$eccgui}-score-edge-bright: #{eccgui-color-var("layout", "cyan", 100)}; } diff --git a/src/cmem/react-flow/configuration/_colors-workflow.scss b/src/cmem/react-flow/configuration/_colors-workflow.scss index 7ef9ac1d9..5c310a009 100644 --- a/src/cmem/react-flow/configuration/_colors-workflow.scss +++ b/src/cmem/react-flow/configuration/_colors-workflow.scss @@ -1,28 +1,16 @@ -$reactflow-color-project-node: #A5356E !default; // Cannot be part of a workflow but we have no other place atm to configure it -$reactflow-color-dataset-node: #3a7896 !default; -$reactflow-color-linking-node: #0097a7 !default; -$reactflow-color-transform-node: #40a691 !default; -$reactflow-color-task-node: #80b67b !default; -$reactflow-color-workflow-node: #745a85 !default; -$reactflow-color-replaceable-input: #faa854 !default; - -@function bright($color) { - @return mix($color, #fff, 24%); -} - -.#{eccgui}-configuration--colors__react-flow-workflow { - --projectNode: #{$reactflow-color-project-node}; - --datasetNode: #{$reactflow-color-dataset-node}; - --linkingNode: #{$reactflow-color-linking-node}; - --transformNode: #{$reactflow-color-transform-node}; - --taskNode: #{$reactflow-color-task-node}; - --workflowNode: #{$reactflow-color-workflow-node}; - --replaceableInput: #{$reactflow-color-replaceable-input}; - --projectNodeBright: #{bright($reactflow-color-project-node)}; - --datasetNodeBright: #{bright($reactflow-color-dataset-node)}; - --linkingNodeBright: #{bright($reactflow-color-linking-node)}; - --transformNodeBright: #{bright($reactflow-color-transform-node)}; - --taskNodeBright: #{bright($reactflow-color-task-node)}; - --workflowNodeBright: #{bright($reactflow-color-workflow-node)}; - --replaceableInputBright: #{bright($reactflow-color-replaceable-input)}; +.#{$eccgui}-configuration--colors__react-flow-workflow { + --#{$eccgui}-project-node: #{eccgui-color-var("layout", "magenta", 700)}; + --#{$eccgui}-project-node-bright: #{eccgui-color-var("layout", "magenta", 100)}; + --#{$eccgui}-dataset-node: #{eccgui-color-var("layout", "cyan", 900)}; + --#{$eccgui}-dataset-node-bright: #{eccgui-color-var("layout", "cyan", 100)}; + --#{$eccgui}-linking-node: #{eccgui-color-var("layout", "teal", 900)}; + --#{$eccgui}-linking-node-bright: #{eccgui-color-var("layout", "teal", 100)}; + --#{$eccgui}-transform-node: #{eccgui-color-var("layout", "pink", 700)}; + --#{$eccgui}-transform-node-bright: #{eccgui-color-var("layout", "pink", 100)}; + --#{$eccgui}-task-node: #{eccgui-color-var("layout", "lime", 700)}; + --#{$eccgui}-task-node-bright: #{eccgui-color-var("layout", "lime", 100)}; + --#{$eccgui}-workflow-node: #{eccgui-color-var("layout", "purple", 700)}; + --#{$eccgui}-workflow-node-bright: #{eccgui-color-var("layout", "purple", 100)}; + --#{$eccgui}-replaceableInput: #{eccgui-color-var("layout", "amber", 700)}; + --#{$eccgui}-replaceableInput-bright: #{eccgui-color-var("layout", "amber", 100)}; } diff --git a/src/cmem/react-flow/nodes/_colors.scss b/src/cmem/react-flow/nodes/_colors.scss index 60a1f5c07..e5d1950f6 100644 --- a/src/cmem/react-flow/nodes/_colors.scss +++ b/src/cmem/react-flow/nodes/_colors.scss @@ -1,17 +1,17 @@ -@mixin nodetypestyles($type, $color) { +@mixin nodetypestyles($type) { .react-flow__node-#{$type} { - @include nodecoloring($color); + @include nodecoloring($type); } } -@mixin nodecoloring($color) { +@mixin nodecoloring($type) { .#{$eccgui}-graphviz__node { - background-color: mix($color, #fff, 24%); - border-color: $color; + background-color: var(--#{$eccgui}-#{$type}-node-bright); + border-color: var(--#{$eccgui}-#{$type}-node); } .#{$eccgui}-graphviz__node__extension--expanded { &.#{$eccgui}-graphviz__node__extension--slideout { - border-color: $color; + border-color: var(--#{$eccgui}-#{$type}-node); } } } @@ -19,7 +19,7 @@ // Graph node types .react-flow__node-graph { - @include nodecoloring($reactflow-color-graph-node); + @include nodecoloring("graph"); .#{$eccgui}-graphviz__node { border-style: double; border-width: 3 * $reactflow-node-border-width; @@ -34,11 +34,11 @@ } } -@include nodetypestyles("class", $reactflow-color-class-node); -@include nodetypestyles("instance", $reactflow-color-instance-node); +@include nodetypestyles("class"); +@include nodetypestyles("instance"); .react-flow__node-property { - @include nodecoloring($reactflow-color-property-node); + @include nodecoloring("property"); .#{$eccgui}-graphviz__node { border-style: dashed; } @@ -51,16 +51,16 @@ // Workflow node types -@include nodetypestyles("dataset", $reactflow-color-dataset-node); -@include nodetypestyles("linking", $reactflow-color-linking-node); -@include nodetypestyles("transform", $reactflow-color-transform-node); -@include nodetypestyles("task", $reactflow-color-task-node); -@include nodetypestyles("workflow", $reactflow-color-workflow-node); +@include nodetypestyles("dataset"); +@include nodetypestyles("linking"); +@include nodetypestyles("transform"); +@include nodetypestyles("task"); +@include nodetypestyles("workflow"); // Linking node types -@include nodetypestyles("sourcepath", $reactflow-color-sourcepath-node); -@include nodetypestyles("targetpath", $reactflow-color-targetpath-node); -@include nodetypestyles("transformation", $reactflow-color-transformation-node); -@include nodetypestyles("comparator", $reactflow-color-comparator-node); -@include nodetypestyles("aggregator", $reactflow-color-aggregator-node); +@include nodetypestyles("sourcepath"); +@include nodetypestyles("targetpath"); +@include nodetypestyles("transformation"); +@include nodetypestyles("comparator"); +@include nodetypestyles("aggregator"); diff --git a/src/common/index.ts b/src/common/index.ts index 39f8af464..aca74bd28 100644 --- a/src/common/index.ts +++ b/src/common/index.ts @@ -1,5 +1,7 @@ import { invisibleZeroWidthCharacters } from "./utils/characters"; +import { colorCalculateDistance } from "./utils/colorCalculateDistance"; import decideContrastColorValue from "./utils/colorDecideContrastvalue"; +import { getEnabledColorsFromPalette, textToColorHash } from "./utils/colorHash"; import getColorConfiguration from "./utils/getColorConfiguration"; import { getScrollParent } from "./utils/getScrollParent"; import { getGlobalVar, setGlobalVar } from "./utils/globalVars"; @@ -9,9 +11,12 @@ export type { IntentTypes as IntentBaseTypes } from "./Intent"; export const utils = { openInNewTab, decideContrastColorValue, + colorCalculateDistance, getColorConfiguration, invisibleZeroWidthCharacters, getGlobalVar, setGlobalVar, getScrollParent, + getEnabledColorsFromPalette, + textToColorHash, }; diff --git a/src/common/scss/_color-functions.scss b/src/common/scss/_color-functions.scss new file mode 100644 index 000000000..5a1a2685e --- /dev/null +++ b/src/common/scss/_color-functions.scss @@ -0,0 +1,111 @@ +@use "sass:list"; +@use "sass:color"; +@use "sass:math"; +@use "sass:meta"; + +/** + * Always provide a list of 5 color tints. + * If the list do not provide 5 colors then create them based on the first and last color in the list. + */ +@function eccgui-create-color-tints($colorset) { + $colorset-steps: 5; // number of tints + $count-colors: list.length($colorset); + + @if $count-colors == $colorset-steps { + @return $colorset; + } + + @if $count-colors < 1 { + @error "Need at least 1 color to create color tints."; + } + + // we asume that it correct to give only start and end of tint weights + // only echo debug message if we have a 1, 3 or 4 color values + @if $count-colors != 2 { + @debug "Got only #{$count-colors} tints: #{$colorset}"; + } + + $color-tint-start: rgb( + color.red(list.nth($colorset, 1)), + color.green(list.nth($colorset, 1)), + color.blue(list.nth($colorset, 1)) + ); + $color-tint-end: rgb( + color.red(list.nth($colorset, -1)), + color.green(list.nth($colorset, -1)), + color.blue(list.nth($colorset, -1)) + ); + $colorset-fallback: $color-tint-start; + + @for $i from 2 to $colorset-steps { + $tint-step: color.mix($color-tint-end, $color-tint-start, 100% * math.div($i - 1, $colorset-steps - 1)); + $colorset-fallback: list.append( + $colorset-fallback, + rgb(color.red($tint-step), color.green($tint-step), color.blue($tint-step)) + ); + + // @debug("mix #{$color-tint-start} with #{$color-tint-end} by #{math.div($i - 1, $colorset-steps - 1)} -> #{rgb(color.red($tint-step), color.green($tint-step), color.blue($tint-step))}") + } + + $colorset-fallback: list.append($colorset-fallback, $color-tint-end); + + @if $count-colors != 2 { + @debug "Create fallback with #{$colorset-steps} tints: #{$colorset-fallback}"; + } + + @return $colorset-fallback; +} + +/** + * Create name for custom property. + */ +@function eccgui-color-name($group_or_name, $tint: null, $weight: null) { + @if $group_or_name and $tint and $weight { + @return "--#{$eccgui}-color-palette-#{$group_or_name}-#{$tint}-#{$weight}"; + } @else { + @return "--#{$eccgui}-color-#{$group_or_name}"; + } +} + +/** + * Create custom property for palette color. + */ +@function eccgui-color-var($group_or_name, $tint: null, $weight: null) { + @return var(#{eccgui-color-name($group_or_name, $tint, $weight)}); +} + +/** + * Wraps the CSS color-mix function to control the color space on only one place. + */ +@function eccgui-color-mix($color1, $color2) { + @return color-mix(in srgb, $color1, $color2); +} + +/** + * Similar to SCSS function rgba($color, $alpha). + * Created to replace them easily for CSS custom properties. + */ +@function eccgui-color-rgba($color, $alpha) { + @return eccgui-color-mix($color 100% * $alpha, transparent); +} + +/** + * Overwrite SCSS built-in rgba function to support colors by custom properties and CSS color methods. + * TODO: we need to check if this is future proof, maybe this bahaviour is not planned by Dart Sass library. + */ +@function rgba($color, $alpha) { + @if meta.type-of($color) == "color" { + // value is SASS color value, we use SASS color functionality + $alphacolor: color.change($color, $alpha: $alpha); + + @if $color == transparent { + $alphacolor: transparent; + } + + @debug "Color value $color is not defined by CSS custom property: rgba(#{$color}, #{$alpha}) -> #{$alphacolor}"; + @return $alphacolor; + } @else { + // value is not a SASS color value, we use CSS color method + @return eccgui-color-rgba($color, $alpha); + } +} diff --git a/src/common/utils/CssCustomProperties.ts b/src/common/utils/CssCustomProperties.ts index ec9495c1d..598dda86b 100644 --- a/src/common/utils/CssCustomProperties.ts +++ b/src/common/utils/CssCustomProperties.ts @@ -6,13 +6,14 @@ type AllowedCSSRule = CSSStyleRule | CSSPageRule; // they have necessary `selectorText` and `style` properties interface getLocalCssStyleRulesProps { - cssRuleType?: "CSSStyleRule" | "CSSPageRule"; + cssRuleType?: "CSSStyleRule"; selectorText?: string; } interface getLocalCssStyleRulePropertiesProps extends getLocalCssStyleRulesProps { propertyType?: "all" | "normal" | "custom"; } interface getCustomPropertiesProps extends getLocalCssStyleRulesProps { + filterName?: (name: string) => boolean; removeDashPrefix?: boolean; returnObject?: boolean; } @@ -27,7 +28,7 @@ export default class CssCustomProperties { // Methods - customProperties = (props: getCustomPropertiesProps = {}) => { + customProperties = (props: getCustomPropertiesProps = {}): string[][] | Record => { // FIXME: // in case of performance issues results should get saved at least into intern variables // other cache strategies could be also tested @@ -64,7 +65,7 @@ export default class CssCustomProperties { .flat(); }; - static listLocalCssStyleRules = (filter: getLocalCssStyleRulesProps = {}): AllowedCSSRule[] => { + static listLocalCssStyleRules = (filter: getLocalCssStyleRulesProps = {}): CSSStyleRule[] => { const { cssRuleType = "CSSStyleRule", selectorText } = filter; const cssStyleRules = CssCustomProperties.listLocalCssRules().filter((rule) => { const cssrule = rule as AllowedCSSRule; @@ -80,14 +81,14 @@ export default class CssCustomProperties { return false; } }); - return cssStyleRules as AllowedCSSRule[]; + return cssStyleRules as CSSStyleRule[]; }; - static listLocalCssStyleRuleProperties = (filter: getLocalCssStyleRulePropertiesProps = {}) => { + static listLocalCssStyleRuleProperties = (filter: getLocalCssStyleRulePropertiesProps = {}): string[][] => { const { propertyType = "all", ...otherFilters } = filter; return CssCustomProperties.listLocalCssStyleRules(otherFilters) .map((cssrule) => { - return [...(cssrule as any).style].map((propertyname) => { + return [...(cssrule as CSSStyleRule).style].map((propertyname) => { return [propertyname.trim(), (cssrule as CSSStyleRule).style.getPropertyValue(propertyname).trim()]; }); }) @@ -103,19 +104,25 @@ export default class CssCustomProperties { }); }; - static listCustomProperties = (props: getCustomPropertiesProps = {}) => { - const { removeDashPrefix = true, returnObject = true, ...filterProps } = props; + static listCustomProperties = (props: getCustomPropertiesProps = {}): string[][] | Record => { + const { removeDashPrefix = true, returnObject = true, filterName = () => true, ...filterProps } = props; const customProperties = CssCustomProperties.listLocalCssStyleRuleProperties({ ...filterProps, propertyType: "custom", - }).map((declaration) => { - if (removeDashPrefix) { - return [declaration[0].substr(2), declaration[1]]; - } - return declaration; - }); + }) + .filter((declaration) => { + return filterName(declaration[0]); + }) + .map((declaration) => { + if (removeDashPrefix) { + return [declaration[0].substr(2), declaration[1]]; + } + return declaration; + }); - return returnObject ? Object.fromEntries(customProperties) : customProperties; + return returnObject + ? (Object.fromEntries(customProperties) as Record) + : (customProperties as string[][]); }; } diff --git a/src/common/utils/colorCalculateDistance.ts b/src/common/utils/colorCalculateDistance.ts new file mode 100644 index 000000000..2dd04df27 --- /dev/null +++ b/src/common/utils/colorCalculateDistance.ts @@ -0,0 +1,28 @@ +import Color from "color"; + +export type colorValue = Color | string; + +export interface colorCalculateDistanceProps { + // Color used to calculate distance to other color. + color1: colorValue; + // Other color used to calculate distance to color. + color2: colorValue; +} + +/** + * Calculates the distance between 2 colors. + * To keep it simple the CIE76 formula is used. + * @see https://en.wikipedia.org/wiki/Color_difference#CIE76 + */ +export const colorCalculateDistance = ({ color1, color2 }: colorCalculateDistanceProps): number | null => { + let colorDistance: number | null = null; + try { + const lab1 = Color(color1).lab(); + const lab2 = Color(color2).lab(); + colorDistance = ((lab1.l() - lab2.l()) ** 2 + (lab1.a() - lab2.a()) ** 2 + (lab1.b() - lab2.b()) ** 2) ** 0.5; + } catch (error) { + // eslint-disable-next-line no-console + console.warn("Received invalid colors", { color1, color2, error }); + } + return colorDistance; +}; diff --git a/src/common/utils/colorHash.ts b/src/common/utils/colorHash.ts new file mode 100644 index 000000000..45dab3daa --- /dev/null +++ b/src/common/utils/colorHash.ts @@ -0,0 +1,195 @@ +import Color from "color"; + +import { CLASSPREFIX as eccgui, COLORMINDISTANCE } from "../../configuration/constants"; + +import { colorCalculateDistance } from "./colorCalculateDistance"; +import CssCustomProperties from "./CssCustomProperties"; + +type ColorOrFalse = Color | false; +type ColorWeight = 100 | 300 | 500 | 700 | 900; +type PaletteGroup = "identity" | "semantic" | "layout" | "extra"; + +interface getEnabledColorsProps { + /** Specify the palette groups used to define the set of colors. */ + includePaletteGroup?: PaletteGroup[]; + /** Use only some weights of a color tint. */ + includeColorWeight?: ColorWeight[]; + /** Only keep colors in the stack with a minimal color distance to all other colors. */ + minimalColorDistance?: number; + /** Extend color stack by values generated by mixing tints of the same weight, e.g. `yellow100` with `purple100`. */ + // includeMixedColors?: boolean; +} + +const getEnabledColorsFromPaletteCache = new Map(); + +export function getEnabledColorsFromPalette({ + includePaletteGroup = ["layout"], + includeColorWeight = [100, 300, 500, 700, 900], + // TODO (planned for later): includeMixedColors = false, + minimalColorDistance = COLORMINDISTANCE, +}: getEnabledColorsProps): Color[] { + const configId = JSON.stringify({ + includePaletteGroup, + includeColorWeight, + }); + + if (getEnabledColorsFromPaletteCache.has(configId)) { + return getEnabledColorsFromPaletteCache.get(configId)!; + } + + const colorsFromPalette = new CssCustomProperties({ + selectorText: `:root`, + filterName: (name: string) => { + if (!name.includes(`--${eccgui}-color-palette-`)) { + // only allow custom properties created for the palette + return false; + } + // test for correct group and weight of the palette color + const tint = name.substring(`--${eccgui}-color-palette-`.length).split("-"); + const group = tint[0] as PaletteGroup; + const weight = parseInt(tint[2], 10) as ColorWeight; + return includePaletteGroup.includes(group) && includeColorWeight.includes(weight); + }, + removeDashPrefix: false, + returnObject: true, + }).customProperties(); + + const colorsFromPaletteValues = Object.values(colorsFromPalette) as string[]; + + const colorsFromPaletteWithEnoughDistance = + minimalColorDistance > 0 + ? colorsFromPaletteValues.reduce((enoughDistance: string[], color: string) => { + if (enoughDistance.includes(color)) { + return enoughDistance.filter((checkColor) => { + const distance = colorCalculateDistance({ color1: color, color2: checkColor }); + return checkColor === color || (distance && minimalColorDistance <= distance); + }); + } else { + return enoughDistance; + } + }, colorsFromPaletteValues) + : colorsFromPaletteValues; + + getEnabledColorsFromPaletteCache.set( + configId, + colorsFromPaletteWithEnoughDistance.map((color: string) => { + return Color(color); + }) + ); + + return getEnabledColorsFromPaletteCache.get(configId)!; +} + +function getColorcode(text: string): ColorOrFalse { + try { + return Color(text); + } catch { + return false; + } +} + +interface textToColorOptions { + /** Stack of colors that are allowed to be returned. */ + enabledColors: Color[] | "all" | getEnabledColorsProps; + /** Return input text if it represents a valid color string, e.g. `#000` or `black`. */ + returnValidColorsDirectly: boolean; +} + +interface textToColorProps { + text: string; + options?: textToColorOptions; +} + +/** + * Map a text string to a color. + * It always returns the same color for a text as long as the options stay the same. + * It returns `false` in case there are no colors defined to chose from. + */ +export function textToColorHash({ + text, + options = { + enabledColors: getEnabledColorsFromPalette({}), + returnValidColorsDirectly: false, + }, +}: textToColorProps): string | false { + let color = getColorcode(text); + + if (options.returnValidColorsDirectly && color) { + // return color code for text because it was a valid color string + return color.hex().toString(); + } + + if (!color) { + color = getColorcode(stringToHexColorHash(text)) as Color; + } + + if (options.enabledColors === "all" && color) { + // all colors are allowed as return value + return color.hex().toString(); + } + + let enabledColors = [] as Color[]; + + if (Array.isArray(options.enabledColors)) { + enabledColors = options.enabledColors; + } else { + enabledColors = getEnabledColorsFromPalette(options.enabledColors as getEnabledColorsProps); + } + + if (enabledColors.length === 0) { + // eslint-disable-next-line no-console + console.warn("textToColorHash functionaliy need enabledColors list with at least 1 color."); + return false; + } + + return nearestColorNeighbour(color, enabledColors as Color[]) + .hex() + .toString(); +} + +function stringToIntegerHash(inputString: string): number { + /* this function is idempotend, meaning it retrieves the same result for the same input + no matter how many times it's called */ + // Convert the string to a hash code + let hashCode = 0; + for (let i = 0; i < inputString.length; i++) { + hashCode = (hashCode << 5) - hashCode + inputString.charCodeAt(i); + hashCode &= hashCode; // Convert to 32bit integer + } + return hashCode; +} + +function integerToHexColor(number: number): string { + // Convert the hash code to a positive number (32unsigned) + const hash = Math.abs(number + Math.pow(31, 2)); + // Convert the number to a hex color (excluding white) + const hexColor = "#" + (hash % 0xffffff).toString(16).padStart(6, "0"); + return hexColor; +} + +function stringToHexColorHash(inputString: string): string { + const integerHash = stringToIntegerHash(inputString); + return integerToHexColor(integerHash); +} + +function nearestColorNeighbour(color: Color, enabledColors: Color[]): Color { + const nearestNeighbour = enabledColors.reduce( + (nearestColor, enabledColorsItem) => { + const distance = colorCalculateDistance({ + color1: color, + color2: enabledColorsItem, + }); + return distance && distance < nearestColor.distance + ? { + distance, + color: enabledColorsItem, + } + : nearestColor; + }, + { + distance: Number.POSITIVE_INFINITY, + color: enabledColors[0], + } + ); + return nearestNeighbour.color; +} diff --git a/src/common/utils/getColorConfiguration.ts b/src/common/utils/getColorConfiguration.ts index 0642d7d38..bb446d609 100644 --- a/src/common/utils/getColorConfiguration.ts +++ b/src/common/utils/getColorConfiguration.ts @@ -1,3 +1,5 @@ +import Color from "color"; + import { CLASSPREFIX as eccgui } from "../../configuration/constants"; import CssCustomProperties from "./CssCustomProperties"; @@ -5,22 +7,58 @@ import CssCustomProperties from "./CssCustomProperties"; // Configurations can be found in `src/cmem/react-flow/configuration/_colors-*.scss` type colorconfigs = "react-flow-graph" | "react-flow-linking" | "react-flow-workflow" | "stickynotes"; -const colorConfigurationMemo = new Map(); +const colorConfigurationMemo = new Map>(); /** * Read and returns color values provided by CSS custom properties. * They are defined for special CSS classes. * Currently color configurations for the react flow editors are supported. **/ -const getColorConfiguration = (configId: colorconfigs) => { +const getColorConfiguration = (configId: colorconfigs): Record => { if (!colorConfigurationMemo.has(configId)) { + const selectorClass = `${eccgui}-configuration--colors__${configId}`; colorConfigurationMemo.set( configId, - new CssCustomProperties({ - selectorText: `.${eccgui}-configuration--colors__${configId}`, - removeDashPrefix: true, - returnObject: true, - }).customProperties() + Object.fromEntries( + ( + new CssCustomProperties({ + selectorText: `.${selectorClass}`, + removeDashPrefix: true, + returnObject: false, + }).customProperties() as string[][] + ).map((setting) => { + // check if the value could be a color + + let testColorValue = setting[1]; + // check if value itself is a reference to another css custom property + if (testColorValue.slice(0, 3) === "var") { + // we currently only extract the first part and ignore any fallbacks + const customPropertyName = /var\(\s*(--[a-zA-Z0-9_-]+)/g.exec(testColorValue); + if (customPropertyName && customPropertyName[1]) { + let selectorElement = document.getElementsByClassName(selectorClass)[0]; + if (!selectorElement) { + // we need to add an empty element that the JS API can read the value of the custom prop + selectorElement = document.createElement("div"); + selectorElement.classList.add(selectorClass); + selectorElement.setAttribute("style", "display: none"); + document.body.appendChild(selectorElement); + } + // only check 1 time, not recursive + testColorValue = getComputedStyle(selectorElement).getPropertyValue(customPropertyName[1]); + } + } + + try { + if (Color(testColorValue)) { + return [setting[0], testColorValue]; + } else { + return [setting[0], undefined]; + } + } catch { + return [setting[0], undefined]; + } + }) + ) as Record ); } return colorConfigurationMemo.get(configId)!; diff --git a/src/components/Accordion/accordion.scss b/src/components/Accordion/accordion.scss index 7e399ffcf..493c35778 100644 --- a/src/components/Accordion/accordion.scss +++ b/src/components/Accordion/accordion.scss @@ -4,12 +4,11 @@ @include accordion.accordion; // own vars -$eccgui-color-accordion-background-elevated: rgba($eccgui-color-accent, 0.1) !default; +$eccgui-color-accordion-background-elevated: eccgui-color-var("identity", "accent", "100") !default; $eccgui-color-accordion-toggler-hover: $menu-item-color-hover !default; -$eccgui-color-accordion-toggler-elevated-hover: color.mix( - $eccgui-color-accordion-background-elevated, - $eccgui-color-accordion-toggler-hover, - 50% +$eccgui-color-accordion-toggler-elevated-hover: eccgui-color-mix( + #{$eccgui-color-accordion-background-elevated}, + #{$eccgui-color-accordion-toggler-hover} ) !default; $eccgui-size-accordion-header-baseheight: mini-units(5) !default; $eccgui-size-accordion-content-basespace: $eccgui-size-block-whitespace * 0.5 !default; @@ -178,6 +177,8 @@ $eccgui-size-accordion-separation: $eccgui-size-block-whitespace * 0.5 !default; background-color: $eccgui-color-accordion-background-elevated; .#{$prefix}--accordion__heading { + &:hover, + &:focus, &:hover::before, &:focus::before { background-color: $eccgui-color-accordion-toggler-elevated-hover; diff --git a/src/components/Application/_colors.scss b/src/components/Application/_colors.scss new file mode 100644 index 000000000..6cd103d11 --- /dev/null +++ b/src/components/Application/_colors.scss @@ -0,0 +1,15 @@ +@use "sass:map"; +@use "sass:list"; + +:root { + @each $palette-group-name, $palette-group-tints in $eccgui-color-palette-light { + @each $palette-tint-name, $palette-tint-colors in $palette-group-tints { + @for $i from 1 through list.length($palette-tint-colors) { + $css-property-name: #{eccgui-color-name($palette-group-name, $palette-tint-name, ($i * 2 - 1) * 100)}; + $css-property-value: #{list.nth($palette-tint-colors, $i)}; + + #{$css-property-name}: #{$css-property-value}; + } + } + } +} diff --git a/src/components/Application/_header.scss b/src/components/Application/_header.scss index e6775dbaa..59ec6b7fa 100644 --- a/src/components/Application/_header.scss +++ b/src/components/Application/_header.scss @@ -14,10 +14,10 @@ $shell-header-bg-01: var( ) !default; /// Header menu trigger hover, Header nav link hover -$shell-header-bg-02: color-mix(in srgb, $shell-header-bg-01, black 5%) !default; +$shell-header-bg-02: eccgui-color-mix($shell-header-bg-01, black 5%) !default; /// Header action active background -$shell-header-bg-03: color-mix(in srgb, $shell-header-bg-01, black 10%) !default; +$shell-header-bg-03: eccgui-color-mix($shell-header-bg-01, black 10%) !default; /// Header submenu link hover $shell-header-bg-04: $shell-header-bg-02 !default; @@ -32,10 +32,10 @@ $shell-header-bg-06: $shell-header-bg-03 !default; $shell-header-border-01: transparent !default; /// Header focus -$shell-header-focus: color.adjust($eccgui-color-applicationheader-text, $lightness: 39%) !default; +$shell-header-focus: eccgui-color-mix($eccgui-color-applicationheader-text, white 39%) !default; /// Primary text in header, Title text -$shell-header-text-01: color.adjust($eccgui-color-applicationheader-text, $lightness: -5%) !default; +$shell-header-text-01: eccgui-color-mix($eccgui-color-applicationheader-text, black 5%) !default; /// Secondary text in header, Menu item nav text $shell-header-text-02: $eccgui-color-applicationheader-text !default; @@ -196,12 +196,12 @@ a.#{$prefix}--header__menu-item:active { a.#{$prefix}--header__name:focus, a.#{$prefix}--header__menu-item:focus { border: none; - outline: 1px dotted color.adjust($eccgui-color-applicationheader-text, $lightness: 39%); + outline: 1px dotted $shell-header-focus; outline-offset: -1px; box-shadow: none; } .#{$prefix}--header__menu-title[aria-expanded="true"] { - color: color.adjust($eccgui-color-applicationheader-text, $lightness: 39%); + color: $shell-header-focus; } // $shell-header-text-01 @@ -214,7 +214,7 @@ a.#{$prefix}--header__menu-item:active, a.#{$prefix}--header__menu-item:focus, .#{$prefix}--header__menu .#{$prefix}--header__menu-item:hover, .#{$prefix}--skip-to-content:focus { - color: color.adjust($eccgui-color-applicationheader-text, $lightness: -5%); + color: $shell-header-text-01; } // $shell-header-text-02 diff --git a/src/components/Application/_toolbar.scss b/src/components/Application/_toolbar.scss index 932f45482..e3c01b50d 100644 --- a/src/components/Application/_toolbar.scss +++ b/src/components/Application/_toolbar.scss @@ -12,25 +12,25 @@ $shell-panel-bg-01: $shell-header-bg-01 !default; /// Panel item hover background -$shell-panel-bg-02: color-mix(in srgb, $shell-header-bg-01, black 10%) !default; +$shell-panel-bg-02: eccgui-color-mix($shell-header-bg-01, black 10%) !default; /// Panel item focus and active background -$shell-panel-bg-03: color-mix(in srgb, $shell-header-bg-01, black 5%) !default; +$shell-panel-bg-03: eccgui-color-mix($shell-header-bg-01, black 5%) !default; /// Panel item link selected background -$shell-panel-bg-04: color-mix(in srgb, $shell-header-bg-01, white 5%) !default; +$shell-panel-bg-04: eccgui-color-mix($shell-header-bg-01, white 5%) !default; /// Panel border $shell-panel-border: $shell-panel-bg-03 !default; /// Header panel text -$shell-panel-text-01: color.adjust($eccgui-color-applicationheader-text, $lightness: -5%) !default; +$shell-panel-text-01: $shell-header-text-01 !default; /// Header panel secondary text $shell-panel-text-02: $eccgui-color-applicationheader-text !default; /// Header panel focus border -$shell-panel-focus: color.adjust($eccgui-color-applicationheader-text, $lightness: 39%) !default; +$shell-panel-focus: $shell-header-focus !default; @import "~@carbon/react/scss/components/ui-shell/header-panel/index"; diff --git a/src/components/Application/application.scss b/src/components/Application/application.scss index 78d1b5d45..ea0de1677 100644 --- a/src/components/Application/application.scss +++ b/src/components/Application/application.scss @@ -1,4 +1,5 @@ // @import 'config'; +@import "colors"; @import "header"; @import "toolbar"; diff --git a/src/components/Application/stories/Application.stories.tsx b/src/components/Application/stories/Application.stories.tsx index e2feb5907..7e0aa6cf4 100644 --- a/src/components/Application/stories/Application.stories.tsx +++ b/src/components/Application/stories/Application.stories.tsx @@ -29,11 +29,11 @@ interface ApplicationBasicExampleProps { } function ApplicationBasicExample(args: ApplicationBasicExampleProps) { - return <>; + return args ? <> : <>; } export default { - title: "Components/Application", + title: "Components/Application/Elements", component: ApplicationBasicExample, subcomponents: { ApplicationContainer, diff --git a/src/components/Application/stories/ColorPalettes.stories.tsx b/src/components/Application/stories/ColorPalettes.stories.tsx new file mode 100644 index 000000000..3827f1104 --- /dev/null +++ b/src/components/Application/stories/ColorPalettes.stories.tsx @@ -0,0 +1,885 @@ +import React from "react"; +import { render } from "react-dom"; +import { loremIpsum } from "react-lorem-ipsum"; +import { Meta, StoryFn } from "@storybook/react"; +import Color from "color"; + +import CssCustomProperties from "./../../../common/utils/CssCustomProperties"; +import { + ApplicationContainer, + Badge, + Button, + Checkbox, + CLASSPREFIX as eccgui, + COLORMINCONTRAST, + COLORMINDISTANCE, + ContextMenu, + FieldItem, + FieldItemRow, + FieldSet, + FlexibleLayoutContainer, + FlexibleLayoutItem, + IconButton, + MenuItem, + Section, + SectionHeader, + Spacing, + Switch, + Tabs, + TabTitle, + Tag, + TagList, + TextField, + TitleSubsection, + utils, +} from "./../../../index"; + +interface ColorPaletteConfiguratorProps { + /** Color palette as custom CSS properties */ + customColorProperties?: string; + /** Default value for minimal color distance */ + distanceMin?: number; + /** Default value for minimal contrast */ + contrastMin?: number; + /** Enable color checks by default */ + enableCalculations?: boolean; +} + +const ColorPaletteConfigurator = ({ + customColorProperties, + distanceMin = COLORMINDISTANCE, // @see https://wisotop.de/farbabstand-farben-vergleichen.php + contrastMin = COLORMINCONTRAST, + enableCalculations = false, +}: ColorPaletteConfiguratorProps) => { + const palettePrefix = `--${eccgui}-color-palette-`; + const userInputDelayTime = 500; + const correctionStep = 0.01; + let userInputDelay; // timeout id + const refConfigurator = React.useRef(null); + const [calculateDistanceWarnings, setCalculateDistanceWarnings] = React.useState(enableCalculations); + const [calculateContrastWarnings, setCalculateContrastWarnings] = React.useState(enableCalculations); + const [minimalDistance, setMinimalDistance] = React.useState(distanceMin); + const [minimalContrast, setMinimalContrast] = React.useState(contrastMin); + const [paletteData, setPaletteData] = React.useState(undefined); + const [hashtestGroups, setHashtestGroups] = React.useState(["layout"]); + const [hashtestWeights, setHashtestWeights] = React.useState(["100", "300", "500", "700", "900"]); + const userPaletteRef = React.useRef(null); + + const createPaletteData = (csscustomprops: string | undefined) => { + const colors = ( + csscustomprops + ? csscustomprops.split(";").map((rule: string) => { + return rule.split(":").map((rulepart: string) => { + return rulepart.trim(); + }); + }) + : new CssCustomProperties({ + selectorText: `:root`, + filterName: (name: string) => { + return name.includes(palettePrefix); + }, + removeDashPrefix: false, + returnObject: false, + }).customProperties() + ) + .filter((colorconfig: object) => { + if (!Array.isArray(colorconfig)) { + return false; + } + if (colorconfig.length !== 2) { + return false; + } + return true; + }) + .map((colorconfig: object) => { + return [colorconfig[0].replace(palettePrefix, ""), Color(colorconfig[1]).rgb()]; + }); + + const data = new Object(); + + for (const [key, value] of colors) { + const hierarchy = key.split("-"); + if (!data[hierarchy[0]]) { + data[hierarchy[0]] = new Object(); + } + if (!data[hierarchy[0]][hierarchy[1]]) { + data[hierarchy[0]][hierarchy[1]] = new Object(); + } + if (!data[hierarchy[0]][hierarchy[1]][hierarchy[2]]) { + data[hierarchy[0]][hierarchy[1]][hierarchy[2]] = value; + } + } + + return data; + }; + + const createSimpleColorList = (data: object, checkColorDistance: boolean) => { + let colorlist = [] as Color[]; + for (const [group, tints] of Object.entries(data)) { + if (hashtestGroups.includes(group)) { + for (const [, weights] of Object.entries(tints as object)) { + for (const [weight, value] of Object.entries(weights)) { + if (hashtestWeights.includes(weight)) { + colorlist.push(value as Color); + } + } + } + } + } + + if (checkColorDistance) { + colorlist = colorlist.reduce((enoughDistance: Color[], color: Color) => { + if (enoughDistance.includes(color)) { + return enoughDistance.filter((checkColor) => { + const distance = utils.colorCalculateDistance({ color1: color, color2: checkColor }); + return checkColor === color || (distance && distanceMin <= distance); + }); + } else { + return enoughDistance; + } + }, colorlist); + } + + return colorlist; + }; + + const createCustomPropsSerialization = (data: object) => { + let serialization = ""; + for (const [group, tints] of Object.entries(data)) { + for (const [tint, weights] of Object.entries(tints as object)) { + for (const [weight, value] of Object.entries(weights)) { + serialization = + serialization + + `--${eccgui}-color-palette-${group}-${tint}-${weight}: ${(value as Color).hex()};\n`; + } + } + } + return serialization.trim(); + }; + + const createSassSerialization = (data: object) => { + const createTintData = (tint: string, weights: object) => { + return `\t\t"${tint}": eccgui-create-color-tints(${Object.values(weights) + .map((color) => color.hex().toLowerCase()) + .join(" ")}),\n`; + }; + + const createGroupData = (group: string, tints: object) => { + let groupData = `\t"${group}": (\n`; + for (const [tint, weights] of Object.entries(tints)) { + groupData = groupData + createTintData(tint, weights); + } + return (groupData + `\t),\n`).replaceAll("\t", " "); + }; + + let sassData = `$eccgui-color-palette-light: (\n`; + + for (const [group, tints] of Object.entries(data)) { + sassData = sassData + createGroupData(group, tints); + } + + return sassData + `) !default;`; + }; + + React.useEffect(() => { + if (refConfigurator.current) { + const panelConfig = document.getElementById("bp5-tab-panel_colorconfig_editor"); + if (panelConfig) { + const warnings = Array.from(panelConfig.getElementsByClassName("eccgui-badge")) + .map((warning: Element) => { + return (warning as HTMLElement).textContent; + }) + .reduce((partial, value) => { + return partial + parseInt(value ?? ""); + }, 0 as number); + const warningsTarget = document.getElementById("sumWarnings"); + if (warningsTarget) { + if (warnings > 0) { + render({warnings}, warningsTarget); + } else { + render(<>, warningsTarget); + } + } + } + } + }); + + React.useEffect(() => { + const paletteData = createPaletteData(customColorProperties); + setPaletteData(paletteData); + }, [customColorProperties]); + + React.useEffect(() => { + if (userPaletteRef && userPaletteRef.current) { + userPaletteRef.current.value = createCustomPropsSerialization(paletteData || {}); + } + }, [paletteData]); + + const updateHashtestGroups = (group: string, active: boolean) => { + let updatedGroups; + if (active) { + updatedGroups = [...hashtestGroups, group]; + } else { + updatedGroups = hashtestGroups.filter((value) => value !== group); + } + setHashtestGroups(updatedGroups); + }; + + const updateHashtestWeights = (weight: string, active: boolean) => { + let updatedWeights; + if (active) { + updatedWeights = [...hashtestWeights, weight]; + } else { + updatedWeights = hashtestWeights.filter((value) => value !== weight); + } + setHashtestWeights(updatedWeights); + }; + + const fixColorByLuminosity = ( + color: Color, + colorTest: Color, + testFn: (color1: Color, color2: Color) => boolean + ) => { + let fixedColor = color as Color; + let check = testFn(fixedColor, colorTest); + while (check === true && fixedColor.luminosity() > 0 && fixedColor.luminosity() < 1) { + if (fixedColor.luminosity() < (colorTest as Color).luminosity()) { + fixedColor = fixedColor.darken(correctionStep); + } else { + fixedColor = fixedColor.lighten(correctionStep); + } + check = testFn(fixedColor, colorTest); + } + + return fixedColor; + }; + + const createWarnings = (id: string[], colors: object) => { + if ( + (!calculateDistanceWarnings && !calculateContrastWarnings) || + !colors[id[0]] || + !colors[id[0]][id[1]] || + !colors[id[0]][id[1]][id[2]] + ) { + return undefined; + } + const color = colors[id[0]][id[1]][id[2]]; + const warningsDistance: React.ReactElement[] = []; + const warningsContrast: React.ReactElement[] = []; + for (const [group, tints] of Object.entries(colors)) { + for (const [tint, weights] of Object.entries(tints as object)) { + for (const [weight, value] of Object.entries(weights)) { + if (color.hex().toString() !== (value as Color).hex().toString()) { + if (calculateDistanceWarnings) { + // color distance + const distance = utils.colorCalculateDistance({ color1: color, color2: value as Color }); + if (distance && distance < minimalDistance) { + warningsDistance.push( + + Fix with{" "} + {tint + weight} ( + {distance.toPrecision(2)}) + + } + > + + Fix{" "} + + {`${id[1]}}${id[2]}}`} + + + } + onClick={() => { + colors[id[0]][id[1]][id[2]] = fixColorByLuminosity( + color, + value as Color, + (c1, c2) => { + const distance = + utils.colorCalculateDistance({ color1: c1, color2: c2 }) ?? + 0; + // eslint-disable-next-line no-console + console.log(`${c1.hex()} -> ${distance}`); + return distance < minimalDistance; + } + ); + setPaletteData({ ...colors }); + }} + /> + + Fix{" "} + + {`${tint}${weight}`} + + + } + onClick={() => { + colors[group][tint][weight] = fixColorByLuminosity( + value as Color, + color, + (c1, c2) => { + const distance = + utils.colorCalculateDistance({ color1: c1, color2: c2 }) ?? + 0; + // eslint-disable-next-line no-console + console.log(`${c1.hex()} -> ${distance}`); + return distance < minimalDistance; + } + ); + setPaletteData({ ...colors }); + }} + /> + + ); + } + } + if (calculateContrastWarnings) { + // color contrasts + if ( + // test to text/background colors in identity group + ((group === "identity" && (tint === "text" || tint === "background")) || + // test to same color tint + (group === id[0] && tint === id[1])) && + // test only for light and strong weights, let out 500 + // 500 is necessary to have a good gradient but they are never good to use as text color/bg + id[2] !== "500" && + weight !== "500" + ) { + if ( + // only calculate light versions to dark versions b/c other usage combination would not make sense at all + (color.isDark() && (value as Color).isLight()) || + (color.isLight() && (value as Color).isDark()) + ) { + if (color.contrast(value as Color) < minimalContrast) { + warningsContrast.push( + + Fix with{" "} + + {`${tint}${weight}`} ( + {color.contrast(value as Color).toPrecision(2)}) + + + } + > + + Fix{" "} + + {`${id[1]}}${id[2]}}`} + + + } + onClick={() => { + colors[id[0]][id[1]][id[2]] = fixColorByLuminosity( + color, + value as Color, + (c1, c2) => { + const contrast = c1.contrast(c2 as Color); + // eslint-disable-next-line no-console + console.log(`${c1.hex()} -> ${contrast}`); + return contrast < minimalContrast; + } + ); + setPaletteData({ ...colors }); + }} + /> + + Fix{" "} + + {`${tint}${weight}`} + + + } + onClick={() => { + colors[group][tint][weight] = fixColorByLuminosity( + value as Color, + color, + (c1, c2) => { + const contrast = c1.contrast(c2 as Color); + // eslint-disable-next-line no-console + console.log(`${c1.hex()} -> ${contrast}`); + return contrast < minimalContrast; + } + ); + setPaletteData({ ...colors }); + }} + /> + + ); + } + } + } + } + } + } + } + } + return warningsDistance.length + warningsContrast.length > 0 ? ( + + } + > + {warningsDistance.length > 0 ? Distances : <>} + <>{warningsDistance} + {warningsContrast.length > 0 ? Contrasts : <>} + <>{warningsContrast} + + ) : undefined; + }; + + const renderColorInput = ( + paletteData: object = {}, + label: string, + id: string[], + updateFn: (color: string) => void + ) => { + if (!paletteData[id[0]] || !paletteData[id[0]][id[1]] || !paletteData[id[0]][id[1]][id[2]]) { + return <>; + } + const color = paletteData[id[0]][id[1]][id[2]]; + const menuWarnings = createWarnings(id, paletteData); + return ( + + { + if (userInputDelay) { + clearTimeout(userInputDelay); + } + userInputDelay = setTimeout(() => { + updateFn(newcolor); + }, userInputDelayTime); + }} + intent={menuWarnings ? "warning" : undefined} + rightElement={menuWarnings} + /> + + ); + }; + + const editorPanel = ( +
+ + + { + if (userInputDelay) { + clearTimeout(userInputDelay); + } + userInputDelay = setTimeout(() => { + setMinimalDistance(parseInt(value, 10)); + }, userInputDelayTime); + }} + rightElement={ + setCalculateDistanceWarnings(!calculateDistanceWarnings)} + /> + } + /> + + + { + if (userInputDelay) { + clearTimeout(userInputDelay); + } + userInputDelay = setTimeout(() => { + setMinimalContrast(parseFloat(value)); + }, userInputDelayTime); + }} + rightElement={ + setCalculateContrastWarnings(!calculateContrastWarnings)} + /> + } + /> + + + {paletteData && + Object.keys(paletteData).map((group, id) => { + return ( +
+ + {group} + + + {Object.keys(paletteData[group]).map((tint, id) => { + return ( + + + + {Object.keys(paletteData[group][tint]).map((weight) => { + return renderColorInput( + paletteData, + `${tint}${weight}`, + [group, tint, weight], + (newcolor) => { + paletteData[group][tint][weight] = Color(newcolor).rgb(); + setPaletteData({ ...paletteData }); + } + ); + })} + + + + { + const tintValues = Object.values( + paletteData[group][tint] + ) as Color[]; + const tintKeys = Object.keys(paletteData[group][tint]); + if (tintValues.length > 0) { + const tint100 = tintValues[0]; + const tint900 = tintValues[tintValues.length - 1]; + tintKeys.forEach((weight, id) => { + paletteData[group][tint][weight] = Color(tint100).mix( + Color(tint900), + id / (tintValues.length - 1) + ); + // eslint-disable-next-line no-console + console.log( + `mix ${Color(tint100).hex()} with ${Color( + tint900 + ).hex()} by ${id / (tintValues.length - 1)} -> ${ + paletteData[group][tint][weight] + }` + ); + }); + } + setPaletteData({ ...paletteData }); + }} + /> + + + ); + })} + +
+ ); + })} +
+ ); + + const currentLayoutColorList = createSimpleColorList(paletteData ?? {}, calculateDistanceWarnings); + + return ( + +
+ {}} + tabs={[ + { + id: "editor", + panel: editorPanel, + title: } />, + }, + { + id: "css", + panel: ( +
+