From 2af6623295c50060840c070f55a7f0103bf4bf53 Mon Sep 17 00:00:00 2001 From: aobityutskiy Date: Wed, 5 Nov 2025 19:56:58 +0300 Subject: [PATCH 1/5] feat: add advance settings for colors in themer page --- package-lock.json | 8 +- package.json | 2 +- .../hooks/useThemeSemanticColorOption.tsx | 134 +++++++ src/components/Themes/lib/constants.ts | 373 +++++++++++++++++- .../Themes/lib/themeCreatorContext.ts | 6 +- .../Themes/lib/themeCreatorUtils.ts | 11 +- src/components/Themes/lib/types.ts | 48 ++- src/components/Themes/lib/utils.ts | 16 + .../AddExtraColor/AddExtraColor.scss | 13 + .../AddExtraColor/AddExtraColor.tsx | 35 ++ .../AdvancedSettingsTable.scss | 27 ++ .../AdvancedSettingsTable.tsx | 315 +++++++++++++++ .../ExtraColorName/ExtraColorName.scss | 9 + .../ExtraColorName/ExtraColorName.tsx | 79 ++++ .../Themes/ui/BrandColors/BrandColors.scss | 5 +- .../Themes/ui/BrandColors/BrandColors.tsx | 88 +++-- .../ui/ColorPickerInput/ColorPickerInput.tsx | 4 +- .../AdvancedSettings/AdvancedSettings.scss | 9 + .../AdvancedSettings/AdvancedSettings.tsx | 74 ++++ .../BasicSettings/BasicSettings.scss | 19 + .../ColorsTab/BasicSettings/BasicSettings.tsx | 124 ++++++ .../Themes/ui/ColorsTab/ColorsTab.tsx | 102 +---- .../PrivateColorSelect/PrivateColorSelect.tsx | 77 +++- .../PrivateColorSelectPopupContent.scss | 6 +- .../PrivateColorSelectPopupContent.tsx | 204 +++++++++- .../PrivateColorsSettings.tsx | 2 +- .../Themes/ui/ThemeCreatorContextProvider.tsx | 42 +- src/components/Themes/ui/ThemeSection.tsx | 11 +- 28 files changed, 1646 insertions(+), 197 deletions(-) create mode 100644 src/components/Themes/hooks/useThemeSemanticColorOption.tsx create mode 100644 src/components/Themes/lib/utils.ts create mode 100644 src/components/Themes/ui/AdvancedSettingsTable/AddExtraColor/AddExtraColor.scss create mode 100644 src/components/Themes/ui/AdvancedSettingsTable/AddExtraColor/AddExtraColor.tsx create mode 100644 src/components/Themes/ui/AdvancedSettingsTable/AdvancedSettingsTable.scss create mode 100644 src/components/Themes/ui/AdvancedSettingsTable/AdvancedSettingsTable.tsx create mode 100644 src/components/Themes/ui/AdvancedSettingsTable/ExtraColorName/ExtraColorName.scss create mode 100644 src/components/Themes/ui/AdvancedSettingsTable/ExtraColorName/ExtraColorName.tsx create mode 100644 src/components/Themes/ui/ColorsTab/AdvancedSettings/AdvancedSettings.scss create mode 100644 src/components/Themes/ui/ColorsTab/AdvancedSettings/AdvancedSettings.tsx create mode 100644 src/components/Themes/ui/ColorsTab/BasicSettings/BasicSettings.scss create mode 100644 src/components/Themes/ui/ColorsTab/BasicSettings/BasicSettings.tsx diff --git a/package-lock.json b/package-lock.json index 8c43668cfb67..756f7090a2e1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,7 @@ "@gravity-ui/navigation": "^3.10.1", "@gravity-ui/page-constructor": "^6.0.0-beta.6", "@gravity-ui/uikit": "^7.26.2", - "@gravity-ui/uikit-themer": "^1.4.1", + "@gravity-ui/uikit-themer": "^1.4.2", "@mdx-js/mdx": "^2.3.0", "@mdx-js/react": "^2.3.0", "@monaco-editor/react": "^4.6.0", @@ -3630,9 +3630,9 @@ } }, "node_modules/@gravity-ui/uikit-themer": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@gravity-ui/uikit-themer/-/uikit-themer-1.4.1.tgz", - "integrity": "sha512-avhPqqRRPE3ORR/hHe4D7vwnX3ypEkevEmvusKH5DKrZbGc5JQD96hCqOqoDoWRvXZ87dMfm8DZyBJpJMf9MxA==", + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@gravity-ui/uikit-themer/-/uikit-themer-1.4.2.tgz", + "integrity": "sha512-L4M7nSrUk9aGpo69+GIMUtuG0i0BakMXmEV99PIGi4Fn7XfORemb6paluTdfgs38Ukt4xQhkFe0m3SiXRMQSfA==", "dependencies": { "chroma-js": "^3.1.2", "lodash-es": "^4.17.21" diff --git a/package.json b/package.json index 2684694ab470..62bac4db035a 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "@gravity-ui/navigation": "^3.10.1", "@gravity-ui/page-constructor": "^6.0.0-beta.6", "@gravity-ui/uikit": "^7.26.2", - "@gravity-ui/uikit-themer": "^1.4.1", + "@gravity-ui/uikit-themer": "^1.4.2", "@mdx-js/mdx": "^2.3.0", "@mdx-js/react": "^2.3.0", "@monaco-editor/react": "^4.6.0", diff --git a/src/components/Themes/hooks/useThemeSemanticColorOption.tsx b/src/components/Themes/hooks/useThemeSemanticColorOption.tsx new file mode 100644 index 000000000000..163c6ca4df78 --- /dev/null +++ b/src/components/Themes/hooks/useThemeSemanticColorOption.tsx @@ -0,0 +1,134 @@ +import { + Cube, + Layers, + MagicWand, + PencilToLine, + TargetDart, + Text as TextIcon, +} from '@gravity-ui/icons'; +import {Icon} from '@gravity-ui/uikit'; +import { + type ColorOptions, + type GravityTheme, + type Theme, + type UtilityColor, + createUtilityColorCssVariable, + isInternalUtilityColorReference, +} from '@gravity-ui/uikit-themer'; +import { + createInternalUtilityColorReference, + isUtilityColorToken, + parseInternalUtilityColorReference, +} from '@gravity-ui/uikit-themer/dist/utils'; +import {useMemo} from 'react'; + +import {DEFAULT_ADVANCED_COLORS} from '../lib/constants'; +import type {AdvancedColorType} from '../lib/types'; +import type {BaseColor} from '../ui/PrivateColorSelect/types'; + +import {useThemeCreator} from './useThemeCreator'; + +export type SemanticColorGroup = { + icon: React.ReactNode; + key: string; + title: string; + groups: { + title: string; + items: (BaseColor & {name?: string; ref?: string})[]; + }[]; +}; + +const getIconByGroup = (group: Exclude) => { + switch (group) { + case 'brand-summary': + return ; + case 'texts': + return ; + case 'backgrounds': + return ; + case 'lines': + return ; + case 'effects': + return ; + case 'misc': + return ; + } +}; + +const resolveUtilityColor = (state: GravityTheme['utilityColors'], themeVariant: Theme) => { + const traverse = (colorObject: ColorOptions): string => { + if (colorObject.ref && isInternalUtilityColorReference(colorObject.ref)) { + const nextUtilityColorToken = parseInternalUtilityColorReference(colorObject.ref); + + if (nextUtilityColorToken) { + const nextUtilityColor = state[nextUtilityColorToken]; + + return traverse(nextUtilityColor[themeVariant]); + } + + return colorObject.value; + } + + return colorObject.value; + }; + + return traverse; +}; + +export const useThemeSemanticColorOption = (themeVariant: Theme): SemanticColorGroup[] => { + const themeState = useThemeCreator(); + const {gravityTheme} = themeState; + + return useMemo(() => { + return Object.entries(DEFAULT_ADVANCED_COLORS) + .filter(([advanceColorGroupName]) => advanceColorGroupName !== 'basic-palette') + .map(([advanceColorGroupName, advanceColorSubGroups]) => { + return { + key: advanceColorGroupName, + icon: getIconByGroup( + advanceColorGroupName as Exclude, + ), + title: advanceColorGroupName, + groups: Object.entries(advanceColorSubGroups).map( + ([advanceColorSubGroupName, advanceColorSubGroupItems]) => { + return { + title: advanceColorSubGroupName, + items: advanceColorSubGroupItems.map(({colorName}) => { + const isUtilityColor = isUtilityColorToken(colorName); + + const colorObject = isUtilityColor + ? gravityTheme.utilityColors[colorName as UtilityColor][ + themeVariant + ] + : gravityTheme.baseColors[colorName]?.[themeVariant]; + + const resolvedValue = isUtilityColor + ? resolveUtilityColor( + gravityTheme.utilityColors, + themeVariant, + )( + gravityTheme.utilityColors[colorName as UtilityColor][ + themeVariant + ], + ) + : colorObject.value; + + return { + name: colorName, + title: isUtilityColor + ? createUtilityColorCssVariable(colorName) + : colorName, + color: resolvedValue, + ref: colorObject.ref, + token: isUtilityColor + ? createInternalUtilityColorReference(colorName) + : colorName, + }; + }), + }; + }, + ), + }; + }); + }, [themeState, themeVariant]); +}; diff --git a/src/components/Themes/lib/constants.ts b/src/components/Themes/lib/constants.ts index 1c49ae968646..721c6bff7ba2 100644 --- a/src/components/Themes/lib/constants.ts +++ b/src/components/Themes/lib/constants.ts @@ -1,13 +1,16 @@ -import type {BordersOptions, GravityTheme, Theme} from '@gravity-ui/uikit-themer'; +import type {BordersOptions, GravityTheme, Theme, UtilityColor} from '@gravity-ui/uikit-themer'; import { DEFAULT_THEME as DEFAULT_GRAVITY_THEME, createInternalPrivateColorReference, } from '@gravity-ui/uikit-themer'; -import {RadiusPresetName} from './types'; +import {type AdvanceColors, RadiusPresetName} from './types'; import {DEFAULT_FONT_FAMILY_SETTINGS} from './typography/constants'; +import {getDefaultAdvancedColorValue} from './utils'; export const THEME_BORDER_RADIUS_VARIABLE_PREFIX = '--g-border-radius'; +export const PRIVATE_COLOR_PREFIX = '--g-color-private-'; +export const UTILITY_COLOR_PREFIX = '--g-color-'; export const DEFAULT_NEW_COLOR_TITLE = 'New color'; @@ -72,6 +75,356 @@ export const RADIUS_PRESETS: Record = { [RadiusPresetName.Custom]: DEFAULT_RADIUS, }; +export const DEFAULT_ADVANCED_COLORS: AdvanceColors = { + texts: { + base: [ + getDefaultAdvancedColorValue('text-primary'), + getDefaultAdvancedColorValue('text-complementary'), + getDefaultAdvancedColorValue('text-secondary'), + getDefaultAdvancedColorValue('text-hint'), + ], + semantic: [ + getDefaultAdvancedColorValue('text-info'), + getDefaultAdvancedColorValue('text-info-heavy'), + getDefaultAdvancedColorValue('text-positive'), + getDefaultAdvancedColorValue('text-positive-heavy'), + getDefaultAdvancedColorValue('text-warning'), + getDefaultAdvancedColorValue('text-warning-heavy'), + getDefaultAdvancedColorValue('text-danger'), + getDefaultAdvancedColorValue('text-danger-heavy'), + getDefaultAdvancedColorValue('text-utility'), + getDefaultAdvancedColorValue('text-utility-heavy'), + getDefaultAdvancedColorValue('text-misc'), + getDefaultAdvancedColorValue('text-misc-heavy'), + ], + brand: [ + getDefaultAdvancedColorValue('text-brand'), + getDefaultAdvancedColorValue('text-link'), + getDefaultAdvancedColorValue('text-link-hover'), + getDefaultAdvancedColorValue('text-link-visited'), + getDefaultAdvancedColorValue('text-link-visited-hover'), + ], + + 'always-dark': [ + getDefaultAdvancedColorValue('text-dark-primary'), + getDefaultAdvancedColorValue('text-dark-complementary'), + getDefaultAdvancedColorValue('text-dark-secondary'), + getDefaultAdvancedColorValue('text-dark-hint'), + ], + 'always-light': [ + getDefaultAdvancedColorValue('text-light-primary'), + getDefaultAdvancedColorValue('text-light-complementary'), + getDefaultAdvancedColorValue('text-light-secondary'), + getDefaultAdvancedColorValue('text-light-hint'), + ], + 'main-inversion': [ + getDefaultAdvancedColorValue('text-inverted-primary'), + getDefaultAdvancedColorValue('text-inverted-complementary'), + getDefaultAdvancedColorValue('text-inverted-secondary'), + getDefaultAdvancedColorValue('text-inverted-hint'), + ], + }, + backgrounds: { + basic: [ + getDefaultAdvancedColorValue('base-background'), + getDefaultAdvancedColorValue('base-generic'), + getDefaultAdvancedColorValue('base-generic-hover'), + getDefaultAdvancedColorValue('base-generic-medium'), + getDefaultAdvancedColorValue('base-generic-medium-hover'), + getDefaultAdvancedColorValue('base-generic-accent'), + getDefaultAdvancedColorValue('base-generic-accent-disabled'), + getDefaultAdvancedColorValue('base-generic-ultralight'), + getDefaultAdvancedColorValue('base-simple-hover'), + getDefaultAdvancedColorValue('base-simple-hover-solid'), + ], + brand: [ + getDefaultAdvancedColorValue('base-brand'), + getDefaultAdvancedColorValue('base-brand-hover'), + getDefaultAdvancedColorValue('base-selection'), + getDefaultAdvancedColorValue('base-selection-hover'), + ], + 'light-semantic': [ + getDefaultAdvancedColorValue('base-info-light'), + getDefaultAdvancedColorValue('base-info-light-hover'), + getDefaultAdvancedColorValue('base-positive-light'), + getDefaultAdvancedColorValue('base-positive-light-hover'), + getDefaultAdvancedColorValue('base-warning-light'), + getDefaultAdvancedColorValue('base-warning-light-hover'), + getDefaultAdvancedColorValue('base-danger-light'), + getDefaultAdvancedColorValue('base-danger-light-hover'), + getDefaultAdvancedColorValue('base-utility-light'), + getDefaultAdvancedColorValue('base-utility-light-hover'), + getDefaultAdvancedColorValue('base-neutral-light'), + getDefaultAdvancedColorValue('base-neutral-light-hover'), + getDefaultAdvancedColorValue('base-misc-light'), + getDefaultAdvancedColorValue('base-misc-light-hover'), + ], + 'medium-semantic': [ + getDefaultAdvancedColorValue('base-info-medium'), + getDefaultAdvancedColorValue('base-info-medium-hover'), + getDefaultAdvancedColorValue('base-positive-medium'), + getDefaultAdvancedColorValue('base-positive-medium-hover'), + getDefaultAdvancedColorValue('base-warning-medium'), + getDefaultAdvancedColorValue('base-warning-medium-hover'), + getDefaultAdvancedColorValue('base-danger-medium'), + getDefaultAdvancedColorValue('base-danger-medium-hover'), + getDefaultAdvancedColorValue('base-utility-medium'), + getDefaultAdvancedColorValue('base-utility-medium-hover'), + getDefaultAdvancedColorValue('base-neutral-medium'), + getDefaultAdvancedColorValue('base-neutral-medium-hover'), + getDefaultAdvancedColorValue('base-misc-medium'), + getDefaultAdvancedColorValue('base-misc-medium-hover'), + ], + 'heavy-semantic': [ + getDefaultAdvancedColorValue('base-info-heavy'), + getDefaultAdvancedColorValue('base-info-heavy-hover'), + getDefaultAdvancedColorValue('base-positive-heavy'), + getDefaultAdvancedColorValue('base-positive-heavy-hover'), + getDefaultAdvancedColorValue('base-warning-heavy'), + getDefaultAdvancedColorValue('base-warning-heavy-hover'), + getDefaultAdvancedColorValue('base-danger-heavy'), + getDefaultAdvancedColorValue('base-danger-heavy-hover'), + getDefaultAdvancedColorValue('base-utility-heavy'), + getDefaultAdvancedColorValue('base-utility-heavy-hover'), + getDefaultAdvancedColorValue('base-neutral-heavy'), + getDefaultAdvancedColorValue('base-neutral-heavy-hover'), + getDefaultAdvancedColorValue('base-misc-heavy'), + getDefaultAdvancedColorValue('base-misc-heavy-hover'), + ], + 'always-light': [ + getDefaultAdvancedColorValue('base-light'), + getDefaultAdvancedColorValue('base-light-hover'), + getDefaultAdvancedColorValue('base-light-simple-hover'), + getDefaultAdvancedColorValue('base-light-disabled'), + getDefaultAdvancedColorValue('base-light-accent-disabled'), + ], + floats: [ + getDefaultAdvancedColorValue('base-float'), + getDefaultAdvancedColorValue('base-float-hover'), + getDefaultAdvancedColorValue('base-float-medium'), + getDefaultAdvancedColorValue('base-float-heavy'), + getDefaultAdvancedColorValue('base-float-accent'), + getDefaultAdvancedColorValue('base-float-accent-hover'), + getDefaultAdvancedColorValue('base-modal'), + ], + }, + lines: { + general: [ + getDefaultAdvancedColorValue('line-generic'), + getDefaultAdvancedColorValue('line-generic-hover'), + getDefaultAdvancedColorValue('line-generic-active'), + getDefaultAdvancedColorValue('line-generic-accent'), + getDefaultAdvancedColorValue('line-generic-accent-hover'), + getDefaultAdvancedColorValue('line-generic-solid'), + ], + semantic: [ + getDefaultAdvancedColorValue('line-info'), + getDefaultAdvancedColorValue('line-positive'), + getDefaultAdvancedColorValue('line-warning'), + getDefaultAdvancedColorValue('line-danger'), + getDefaultAdvancedColorValue('line-utility'), + getDefaultAdvancedColorValue('line-misc'), + ], + 'always-light': [getDefaultAdvancedColorValue('line-light')], + }, + effects: { + other: [ + getDefaultAdvancedColorValue('sfx-veil'), + getDefaultAdvancedColorValue('sfx-shadow'), + getDefaultAdvancedColorValue('sfx-shadow-heavy'), + getDefaultAdvancedColorValue('sfx-shadow-light'), + getDefaultAdvancedColorValue('sfx-fade'), + ], + }, + misc: { + scroll: [ + getDefaultAdvancedColorValue('scroll-track'), + getDefaultAdvancedColorValue('scroll-handle'), + getDefaultAdvancedColorValue('scroll-handle-hover'), + getDefaultAdvancedColorValue('scroll-corner'), + ], + axes: [getDefaultAdvancedColorValue('infographics-axis')], + tooltips: [getDefaultAdvancedColorValue('infographics-tooltip-bg')], + }, + 'basic-palette': { + 'base-color': [ + ...Object.entries(DEFAULT_GRAVITY_THEME.baseColors).map(([colorName, colorValue]) => ({ + colorName, + ...colorValue, + })), + ], + 'extra-color': [], + }, + 'brand-summary': { + 'brand-palette': [ + getDefaultAdvancedColorValue('base-background'), + { + colorName: 'brand', + ...DEFAULT_GRAVITY_THEME.baseColors.brand, + }, + ], + 'advanced-brand-palette': [ + getDefaultAdvancedColorValue('base-brand-hover'), + getDefaultAdvancedColorValue('text-brand'), + getDefaultAdvancedColorValue('text-brand-heavy'), + getDefaultAdvancedColorValue('line-brand'), + getDefaultAdvancedColorValue('base-selection'), + getDefaultAdvancedColorValue('base-selection-hover'), + ], + 'additional-colors': [ + getDefaultAdvancedColorValue('text-link'), + getDefaultAdvancedColorValue('text-link-hover'), + getDefaultAdvancedColorValue('text-link-visited'), + getDefaultAdvancedColorValue('text-link-visited-hover'), + ], + }, +}; + +//TODO: add translates + +export const UTILITY_COLOR_HELP_CONTENT: Record = { + // text + 'text-primary': 'Primary text on the page. It is default for headers, paragraphs, buttons.', + 'text-complementary': 'Complementary text on the page. Controls, notes, etc.', + 'text-secondary': + 'Secondary text on the page. Captions, definitions, nonessential information.', + 'text-hint': 'Control hint.', + 'text-info': 'Info text.', + 'text-info-heavy': 'Info text with underlay.', + 'text-positive': 'Positive text.', + 'text-positive-heavy': 'Positive text with underlay.', + 'text-warning': 'Warning text.', + 'text-warning-heavy': 'Warning text with underlay.', + 'text-danger': 'Danger text.', + 'text-danger-heavy': 'Danger text with underlay.', + 'text-utility': 'For emphasizing, without semantic.', + 'text-utility-heavy': 'Utility text with underlay.', + 'text-misc': 'For emphasizing, without semantic.', + 'text-misc-heavy': 'Misc text with underlay.', + 'text-brand': 'Brand text.', + 'text-brand-heavy': 'Brand text with underlay.', + 'text-brand-contrast': 'Brand text with high contrast.', + 'text-link': 'Links.', + 'text-link-hover': 'Hover for Link.', + 'text-link-visited': 'Visited Link.', + 'text-link-visited-hover': 'Hover for Visited Link.', + 'text-dark-primary': 'Primary text over light background.', + 'text-dark-complementary': 'Complementary text over light background.', + 'text-dark-secondary': 'Secondary text over light background.', + 'text-dark-hint': 'Minimal contrast.', + 'text-light-primary': 'Primary text over dark background.', + 'text-light-complementary': 'Complementary text over dark background.', + 'text-light-secondary': 'Secondary text over dark background.', + 'text-light-hint': 'Minimal contrast.', + 'text-inverted-primary': 'Primary text.', + 'text-inverted-complementary': 'Complementary text.', + 'text-inverted-secondary': 'Secondary text.', + 'text-inverted-hint': 'Minimal contrast.', + + // backgrounds + 'base-background': "Page's background.", + 'base-generic': 'Generic gray base, buttons and other objects.', + 'base-generic-hover': 'Hover for Generic.', + 'base-generic-medium': 'Neutral blocks with medium contrast.', + 'base-generic-medium-hover': 'Hover for Generic Medium.', + 'base-generic-accent': 'Background for controls (checkbox, radio, etc.).', + 'base-generic-accent-disabled': 'Disabled background for controls.', + 'base-generic-ultralight': 'Background with minimal contrast. Not recommended to use.', + 'base-simple-hover': 'Hover for transparent objects (works over light backgrounds).', + 'base-simple-hover-solid': 'Hover for transparent objects (works over light backgrounds).', + 'base-brand': 'Background for accented object.', + 'base-brand-hover': 'Hover for Brand.', + 'base-selection': 'Highlight selected objects in menus, calendars, etc.', + 'base-selection-hover': 'Hover for Selection.', + 'base-info-light': 'Info semantic background.', + 'base-info-light-hover': 'Hover for Info.', + 'base-positive-light': 'Positive semantic background.', + 'base-positive-light-hover': 'Hover for Positive.', + 'base-warning-light': 'Warning semantic background.', + 'base-warning-light-hover': 'Hover for Warning.', + 'base-danger-light': 'Negative semantic background.', + 'base-danger-light-hover': 'Hover for Danger.', + 'base-utility-light': 'Utility semantic background.', + 'base-utility-light-hover': 'Hover for Utility.', + 'base-misc-light': 'Uncategorized semantic background.', + 'base-misc-light-hover': 'Hover for Misc.', + 'base-neutral-light': 'Neutral semantic background.', + 'base-neutral-light-hover': 'Hover for Neutral.', + 'base-info-medium': 'Info semantic background, medium accent.', + 'base-info-medium-hover': 'Hover for Info Medium.', + 'base-positive-medium': 'Positive semantic background, medium accent.', + 'base-positive-medium-hover': 'Hover for Positive Medium.', + 'base-warning-medium': 'Warning semantic background, medium accent.', + 'base-warning-medium-hover': 'Hover for Warning Medium.', + 'base-danger-medium': 'Danger semantic background, medium accent.', + 'base-danger-medium-hover': 'Hover for Danger Medium.', + 'base-utility-medium': 'Utility semantic background, medium accent.', + 'base-utility-medium-hover': 'Hover for Utility Medium.', + 'base-misc-medium': 'Uncategorized semantic background, medium accent.', + 'base-misc-medium-hover': 'Hover for Misc Medium.', + 'base-neutral-medium': 'Neutral semantic background, medium accent.', + 'base-neutral-medium-hover': 'Hover for Neutral Medium.', + 'base-info-heavy': 'Info semantic background, strong accent.', + 'base-info-heavy-hover': 'Hover for Info Heavy.', + 'base-positive-heavy': 'Positive semantic background, strong accent.', + 'base-positive-heavy-hover': 'Hover for Positive Heavy.', + 'base-warning-heavy': 'Warning semantic background, strong accent.', + 'base-warning-heavy-hover': 'Hover for Warning Heavy.', + 'base-danger-heavy': 'Negative semantic background, strong accent.', + 'base-danger-heavy-hover': 'Hover for Danger Heavy', + 'base-utility-heavy': 'Utility semantic background, strong accent.', + 'base-utility-heavy-hover': 'Hover for Utility Heavy.', + 'base-misc-heavy': 'Uncategorized semantic background, strong accent.', + 'base-misc-heavy-hover': 'Hover for Misc Heavy.', + 'base-neutral-heavy': 'Neutral semantic background, strong accent.', + 'base-neutral-heavy-hover': 'Hover for Neutral Heavy.', + 'base-light': 'Background on top of another darker background.', + 'base-light-hover': 'Hover for Light.', + 'base-light-simple-hover': 'Hover for transparent objects (works over dark backgrounds).', + 'base-light-disabled': 'Disabled controls.', + 'base-light-accent-disabled': 'Disabled active controls.', + 'base-float': 'Raised layers background.', + 'base-float-hover': 'Hover for Float.', + 'base-float-medium': 'Float for medium contrast.', + 'base-float-heavy': 'Float for strong contrast.', + 'base-float-accent': 'Raised controls.', + 'base-float-accent-hover': 'Hover for Float Accent.', + 'base-float-announcement': 'Float background for announcements.', + 'base-modal': 'Floating components with a veil.', + + // lines + 'line-generic': 'Button borders, dividers, basic block borders. Almost all lines.', + 'line-generic-hover': 'Hover for Generic.', + 'line-generic-active': 'Active state for Generic.', + 'line-generic-accent': 'Control borders.', + 'line-generic-accent-hover': 'Hover for Generic Accent.', + 'line-generic-solid': 'Generic without transparency (to avoid collision artefacts).', + 'line-brand': 'Brand blocks', + 'line-focus': 'Focused blocks', + 'line-info': 'Info blocks.', + 'line-positive': 'Positive blocks.', + 'line-warning': 'Warning blocks.', + 'line-danger': 'Danger blocks. Blocks with negative context.', + 'line-utility': 'Utility blocks.', + 'line-misc': 'Uncategorized blocks.', + 'line-light': 'Dividers and borders over dark background.', + + // effects + 'sfx-veil': 'Popup backdrop.', + 'sfx-shadow': 'Shadow for everything that might have it.', + 'sfx-shadow-light': 'Lighter version of shadow.', + 'sfx-shadow-heavy': 'Heavy shadows. DEPRECATED.', + 'sfx-fade': 'Enlighten while loading.', + + // misc + 'scroll-track': 'Scroll background.', + 'scroll-handle': 'The handle to move the scroll.', + 'scroll-handle-hover': 'Hover state for scroll handle.', + 'scroll-corner': 'A corner where horizontal and vertical scrolls meet.', + 'infographics-axis': 'Graph axis', + 'infographics-tooltip-bg': 'Main background for tooltips.', +}; + // Default colors mappings (values from gravity-ui styles) // https://github.com/gravity-ui/uikit/tree/main/styles/themes export const DEFAULT_COLORS: GravityTheme['utilityColors'] = { @@ -86,10 +439,10 @@ export const DEFAULT_COLORS: GravityTheme['utilityColors'] = { }, 'base-background': { light: { - value: 'rgb(255 255 255)', + value: 'rgb(255, 255, 255)', }, dark: { - value: 'rgb(34 29 34)', + value: 'rgb(34, 29, 34)', }, }, 'base-brand-hover': { @@ -203,10 +556,10 @@ export const BRAND_COLORS_PRESETS: BrandPreset[] = [ utilityColors: { 'base-background': { light: { - value: 'rgb(255 255 255)', + value: 'rgb(255, 255, 255)', }, dark: { - value: 'rgb(34 29 34)', + value: 'rgb(34, 29, 34)', }, }, 'base-brand': { @@ -312,10 +665,10 @@ export const BRAND_COLORS_PRESETS: BrandPreset[] = [ utilityColors: { 'base-background': { light: { - value: 'rgb(255 255 255)', + value: 'rgb(255, 255, 255)', }, dark: { - value: 'rgb(34 29 34)', + value: 'rgb(34, 29, 34)', }, }, 'base-brand': { @@ -421,10 +774,10 @@ export const BRAND_COLORS_PRESETS: BrandPreset[] = [ utilityColors: { 'base-background': { light: { - value: 'rgb(255 255 255)', + value: 'rgb(255, 255, 255)', }, dark: { - value: 'rgb(34 29 34)', + value: 'rgb(34, 29, 34)', }, }, 'base-brand': { diff --git a/src/components/Themes/lib/themeCreatorContext.ts b/src/components/Themes/lib/themeCreatorContext.ts index 9732fe33872e..7dfe7f31c582 100644 --- a/src/components/Themes/lib/themeCreatorContext.ts +++ b/src/components/Themes/lib/themeCreatorContext.ts @@ -16,7 +16,7 @@ import type { UpdateFontFamilyParams, UpdateFontFamilyTypeTitleParams, } from './themeCreatorUtils'; -import type {ThemeCreatorState} from './types'; +import type {AdvanceColors, ColorsSettingsType, ThemeCreatorState} from './types'; export const ThemeCreatorContext = createContext( initThemeCreator(DEFAULT_THEME), @@ -40,6 +40,8 @@ export interface ThemeCreatorMethodsContextType { openMainSettings: () => void; setAdvancedMode: (enabled: boolean) => void; importTheme: (theme: GravityTheme) => void; + setColorsSettingsType: (type: ColorsSettingsType) => void; + updateAdvancedColors: (colors: AdvanceColors) => void; } export const ThemeCreatorMethodsContext = createContext({ @@ -60,4 +62,6 @@ export const ThemeCreatorMethodsContext = createContext; }[]; + +export type AdvanceColors = { + [K in AdvancedColorType]: Record< + AdvancedColorTypeGroup[K], + (({colorName: UtilityColor} | {colorName: string}) & { + light?: { + value: string; + ref?: string; + }; + dark?: { + value: string; + ref?: string; + }; + })[] + >; +}; diff --git a/src/components/Themes/lib/utils.ts b/src/components/Themes/lib/utils.ts new file mode 100644 index 000000000000..91aa1d49cc13 --- /dev/null +++ b/src/components/Themes/lib/utils.ts @@ -0,0 +1,16 @@ +import {DEFAULT_THEME as DEFAULT_GRAVITY_THEME, type UtilityColor} from '@gravity-ui/uikit-themer'; +import {isUtilityColorToken} from '@gravity-ui/uikit-themer/dist/utils'; + +import {PRIVATE_COLOR_PREFIX, UTILITY_COLOR_PREFIX} from './constants'; + +export const getDefaultAdvancedColorValue = (colorName: UtilityColor) => { + return {colorName: colorName, ...DEFAULT_GRAVITY_THEME['utilityColors'][colorName]}; +}; + +export const getColorPrefix = (colorToken: string) => { + if (isUtilityColorToken(colorToken)) { + return UTILITY_COLOR_PREFIX; + } + + return PRIVATE_COLOR_PREFIX; +}; diff --git a/src/components/Themes/ui/AdvancedSettingsTable/AddExtraColor/AddExtraColor.scss b/src/components/Themes/ui/AdvancedSettingsTable/AddExtraColor/AddExtraColor.scss new file mode 100644 index 000000000000..219d129d55f3 --- /dev/null +++ b/src/components/Themes/ui/AdvancedSettingsTable/AddExtraColor/AddExtraColor.scss @@ -0,0 +1,13 @@ +@use '../../../../../variables.scss'; + +$block: '.#{variables.$ns}add-extra-color'; + +#{$block} { + width: 100%; + border-block-end: 1px solid var(--g-color-line-generic); + + &__button { + color: var(--g-color-text-secondary); + --g-button-border-radius: 8px; + } +} diff --git a/src/components/Themes/ui/AdvancedSettingsTable/AddExtraColor/AddExtraColor.tsx b/src/components/Themes/ui/AdvancedSettingsTable/AddExtraColor/AddExtraColor.tsx new file mode 100644 index 000000000000..da2d522bbe8f --- /dev/null +++ b/src/components/Themes/ui/AdvancedSettingsTable/AddExtraColor/AddExtraColor.tsx @@ -0,0 +1,35 @@ +import {Plus} from '@gravity-ui/icons'; +import {Button, Icon} from '@gravity-ui/uikit'; +// import {useTranslation} from 'next-i18next'; +import {useCallback} from 'react'; + +import {block} from '../../../../../utils'; +import {useThemeCreatorMethods} from '../../../hooks/useThemeCreator'; + +import './AddExtraColor.scss'; + +const b = block('add-extra-color'); + +export const AddExtraColor = () => { + // const {t} = useTranslation('themes'); + + const {addColor} = useThemeCreatorMethods(); + + const handleAddColor = useCallback(() => { + addColor({ + colors: { + light: '#000000', + dark: '#000000', + }, + }); + }, [addColor]); + + return ( +
+ +
+ ); +}; diff --git a/src/components/Themes/ui/AdvancedSettingsTable/AdvancedSettingsTable.scss b/src/components/Themes/ui/AdvancedSettingsTable/AdvancedSettingsTable.scss new file mode 100644 index 000000000000..a366d19b2ba2 --- /dev/null +++ b/src/components/Themes/ui/AdvancedSettingsTable/AdvancedSettingsTable.scss @@ -0,0 +1,27 @@ +@use '../../../../variables.scss'; + +$block: '.#{variables.$ns}advanced-color-settings-table'; + +#{$block} { + border-collapse: collapse; + + &__cell { + padding-inline: var(--g-spacing-5); + text-align: left; + border-block-end: 1px solid var(--g-color-line-generic); + border-inline-end: 1px solid var(--g-color-line-generic); + + &_group { + padding-block: var(--g-spacing-4) var(--g-spacing-1); + } + + &_header { + border-block-end: 5px solid var(--g-color-border-secondary); + padding-block-end: var(--g-spacing-6); + } + } + + &__row > &__cell:first-child { + padding-inline-start: 0; + } +} diff --git a/src/components/Themes/ui/AdvancedSettingsTable/AdvancedSettingsTable.tsx b/src/components/Themes/ui/AdvancedSettingsTable/AdvancedSettingsTable.tsx new file mode 100644 index 000000000000..ede23a3b4e6e --- /dev/null +++ b/src/components/Themes/ui/AdvancedSettingsTable/AdvancedSettingsTable.tsx @@ -0,0 +1,315 @@ +import {Moon, Sun} from '@gravity-ui/icons'; +import {BREAKPOINTS, useWindowBreakpoint} from '@gravity-ui/page-constructor'; +import {Flex, HelpMark, Icon, SegmentedRadioGroup, Text} from '@gravity-ui/uikit'; +import type {BaseColors, Theme, UtilityColor} from '@gravity-ui/uikit-themer'; +import { + createUtilityColorCssVariable, + isUtilityColorToken, +} from '@gravity-ui/uikit-themer/dist/utils'; +import {Fragment, type ReactElement, useMemo, useState} from 'react'; + +import {block} from '../../../../utils'; +import { + useThemeCreator, + useThemePaletteColor, + useThemePrivateColorOptions, + useThemeUtilityColor, +} from '../../hooks'; +import {useThemeSemanticColorOption} from '../../hooks/useThemeSemanticColorOption'; +import {DEFAULT_ADVANCED_COLORS, UTILITY_COLOR_HELP_CONTENT} from '../../lib/constants'; +import {isManuallyCreatedPaletteToken} from '../../lib/themeCreatorUtils'; +import type {AdvancedColorType} from '../../lib/types'; +import {getColorPrefix} from '../../lib/utils'; +import {ColorPickerInput} from '../ColorPickerInput/ColorPickerInput'; +import {PrivateColorSelect} from '../PrivateColorSelect'; + +import {AddExtraColor} from './AddExtraColor/AddExtraColor'; +import './AdvancedSettingsTable.scss'; +import {ExtraColorName} from './ExtraColorName/ExtraColorName'; + +const b = block('advanced-color-settings-table'); + +export interface AdvancedSettingsTableProps { + colorType: AdvancedColorType; +} + +type ValueCellProps = {colorName: string; theme: Theme; value?: string}; + +type Column = { + title: () => ReactElement; + key: string; + render: (props: { + colorName: string; + light?: string; + dark?: string; + extraVariable?: boolean; + }) => ReactElement; +}; + +const getContentForUtilityVariable = (value: UtilityColor) => { + const content = UTILITY_COLOR_HELP_CONTENT[value]; + + return ( + + + {createUtilityColorCssVariable(value)} + + + {content} + + + ); +}; + +const VariableCell = ({name, extraVariable}: {name: string; extraVariable?: boolean}) => { + if (extraVariable) { + return ; + } + + const isUtilityColor = isUtilityColorToken(name as UtilityColor); + + return ( + + + {getColorPrefix(name)} + + {name} + + + {isUtilityColor && ( + + {getContentForUtilityVariable(name as UtilityColor)} + + )} + + ); +}; + +const UtilityThemeValueCell = ({ + colorName, + theme, + value, +}: Omit & {colorName: UtilityColor}) => { + const [color, setColor] = useThemeUtilityColor({ + name: colorName, + theme, + }); + + const themePrivateColorOptions = useThemePrivateColorOptions(theme); + const themeSemanticColorOptions = useThemeSemanticColorOption(theme); + + console.log('themeSemanticColorOptions', themeSemanticColorOptions); + console.log('themePrivateColorOptions', themePrivateColorOptions); + + if (colorName === 'base-background') { + return ( + + ); + } + + return ( + + ); +}; + +const PaletteThemeValueCell = ({colorName, theme, value}: ValueCellProps) => { + const [color, setColor] = useThemePaletteColor({ + token: colorName, + theme, + }); + + return ( + + ); +}; + +const ThemeValueCell = ({colorName, theme, value}: ValueCellProps) => { + const isUtilityColor = isUtilityColorToken(colorName); + + if (isUtilityColor) { + return ; + } + + return ; +}; + +const TitleCell = ({value}: {value: string}) => { + return {value}; +}; + +export const AdvancedSettingsTable = ({colorType}: AdvancedSettingsTableProps) => { + const state = useThemeCreator(); + const {gravityTheme} = state; + + console.log('gravityTheme', gravityTheme); + + //todo: move to hook + const extraColors = useMemo(() => { + return Object.entries(state.gravityTheme.baseColors) + .filter(([key]) => isManuallyCreatedPaletteToken(key)) + .reduce((acc, [key, value]) => { + acc[key] = value; + return acc; + }, {}); + }, [state]); + + const breakpoint = useWindowBreakpoint(); + const [theme, toggleTheme] = useState('light'); + + //TODO: move to hook + const columns = useMemo((): Column[] => { + const isTablet = breakpoint < BREAKPOINTS.lg; + + const variableColumn: Column = { + title: () => , + key: 'variable', + render: ({colorName, extraVariable = false}) => ( + + ), + }; + + if (isTablet) { + return [ + variableColumn, + { + title: () => ( + { + toggleTheme(e.target.value as Theme); + }} + > + + + Light + + + + Dark + + + ), + key: 'themeToggle', + render: ({colorName, light, dark}) => ( + + ), + }, + ]; + } + + return [ + variableColumn, + { + title: () => , + key: 'light', + render: ({colorName, light}) => ( + + ), + }, + { + title: () => , + key: 'dark', + render: ({colorName, dark}) => ( + + ), + }, + ]; + }, [breakpoint, theme]); + + return ( + + + + {columns.map(({title: Title}, index) => ( +
+ + </th> + ))} + </tr> + </thead> + <tbody className={b('body')}> + {Object.entries(DEFAULT_ADVANCED_COLORS[colorType]).map(([group, variables]) => { + return ( + <Fragment> + <tr className={b('row')}> + {columns.map(({key}) => ( + <td + className={b('cell', {group: true})} + key={`${group}-${key}`} + > + {key === 'variable' ? group : ''} + </td> + ))} + </tr> + + {colorType === 'basic-palette' && group === 'extra-color' && ( + <Fragment> + {Object.entries(extraColors).map(([colorName, value]) => ( + <tr className={b('row')}> + {columns.map(({render: Render, key}) => ( + <td + className={b('cell')} + key={`${colorName}-${key}`} + > + <Render + colorName={colorName} + light={ + value.light?.ref ?? value.light?.value + } + dark={value.dark?.ref ?? value.dark?.value} + extraVariable + /> + </td> + ))} + </tr> + ))} + <tr> + <td colSpan={columns.length}> + <AddExtraColor /> + </td> + </tr> + </Fragment> + )} + + {variables.map(({colorName, light, dark}) => ( + <tr className={b('row')}> + {columns.map(({render: Render, key}) => ( + <td className={b('cell')} key={`${colorName}-${key}`}> + <Render + colorName={colorName} + light={light?.ref ?? light?.value} + dark={dark?.ref ?? dark?.value} + /> + </td> + ))} + </tr> + ))} + </Fragment> + ); + })} + </tbody> + </table> + ); +}; diff --git a/src/components/Themes/ui/AdvancedSettingsTable/ExtraColorName/ExtraColorName.scss b/src/components/Themes/ui/AdvancedSettingsTable/ExtraColorName/ExtraColorName.scss new file mode 100644 index 000000000000..2e9bdee852a6 --- /dev/null +++ b/src/components/Themes/ui/AdvancedSettingsTable/ExtraColorName/ExtraColorName.scss @@ -0,0 +1,9 @@ +@use '../../../../../variables.scss'; + +$block: '.#{variables.$ns}extra-color-name'; + +#{$block} { + &__delete-icon { + color: var(--g-color-text-danger); + } +} diff --git a/src/components/Themes/ui/AdvancedSettingsTable/ExtraColorName/ExtraColorName.tsx b/src/components/Themes/ui/AdvancedSettingsTable/ExtraColorName/ExtraColorName.tsx new file mode 100644 index 000000000000..0e95b550a9bc --- /dev/null +++ b/src/components/Themes/ui/AdvancedSettingsTable/ExtraColorName/ExtraColorName.tsx @@ -0,0 +1,79 @@ +import {PencilToLine, TrashBin} from '@gravity-ui/icons'; +import {Button, Flex, Icon, Text, TextInput} from '@gravity-ui/uikit'; +import {useCallback, useRef, useState} from 'react'; + +import {block} from '../../../../../utils'; +import {useThemeCreatorMethods} from '../../../hooks'; +import {PRIVATE_COLOR_PREFIX} from '../../../lib/constants'; +import {createColorToken, createTitleFromToken} from '../../../lib/themeCreatorUtils'; + +import './ExtraColorName.scss'; + +export interface ExtraColorNameProps { + token: string; +} + +const b = block('extra-color-name'); + +export const ExtraColorName = ({token}: ExtraColorNameProps) => { + const [mode, setMode] = useState<'edit' | 'view'>('view'); + + const {removeColor, renameColor} = useThemeCreatorMethods(); + const nameInputRef = useRef<HTMLInputElement | null>(null); + + const handleChangeMode = useCallback(() => { + if (mode === 'view') { + setMode('edit'); + return; + } else if (nameInputRef.current && nameInputRef.current.value) { + const newToken = createColorToken(nameInputRef.current?.value); + + if (newToken !== token) { + renameColor({oldTitle: token, newTitle: newToken}); + } + } + + setMode('view'); + }, [mode]); + + const handleDelete = useCallback(() => { + removeColor(token); + }, [removeColor, name]); + + return ( + <Flex gap={2} justifyContent="space-between" alignItems="center"> + {mode === 'view' ? ( + <Text variant="body-1" color="secondary"> + {PRIVATE_COLOR_PREFIX} + <Text variant="body-1" color="primary"> + {token} + </Text> + </Text> + ) : ( + <TextInput + defaultValue={token} + controlRef={(node) => { + if (node) { + nameInputRef.current = node; + nameInputRef.current.value = createTitleFromToken(token); + } + }} + onBlur={handleChangeMode} + /> + )} + <Flex gap={2}> + <Button + view={mode === 'view' ? 'flat' : 'action'} + size="s" + onClick={handleChangeMode} + > + <Icon data={PencilToLine} /> + </Button> + + <Button view="flat" size="s" onClick={handleDelete}> + <Icon data={TrashBin} className={b('delete-icon')} /> + </Button> + </Flex> + </Flex> + ); +}; diff --git a/src/components/Themes/ui/BrandColors/BrandColors.scss b/src/components/Themes/ui/BrandColors/BrandColors.scss index e3b7f14e2139..c2419a69be95 100644 --- a/src/components/Themes/ui/BrandColors/BrandColors.scss +++ b/src/components/Themes/ui/BrandColors/BrandColors.scss @@ -58,8 +58,7 @@ $block: '.#{variables.$ns}brand-colors'; } } - &__switch-button { - --g-button-border-radius: 8px; - width: min-content; + &__colors-settings-type-radio-group { + --g-border-radius-xl: 8px; } } diff --git a/src/components/Themes/ui/BrandColors/BrandColors.tsx b/src/components/Themes/ui/BrandColors/BrandColors.tsx index 8da7f3dd8dbc..faf6eabe6f46 100644 --- a/src/components/Themes/ui/BrandColors/BrandColors.tsx +++ b/src/components/Themes/ui/BrandColors/BrandColors.tsx @@ -1,11 +1,13 @@ -import {Sliders} from '@gravity-ui/icons'; -import {Button, Flex, Icon, Text} from '@gravity-ui/uikit'; +import {Flask, HandOk} from '@gravity-ui/icons'; +import {Flex, Icon, SegmentedRadioGroup, Text} from '@gravity-ui/uikit'; import {useTranslation} from 'next-i18next'; import React from 'react'; +import {useIsMobile} from '../../../../hooks/useIsMobile'; import {block} from '../../../../utils'; -import {useThemeCreatorMethods, useThemePaletteColor} from '../../hooks'; +import {useThemeCreator, useThemeCreatorMethods, useThemePaletteColor} from '../../hooks'; import {BRAND_COLORS_PRESETS} from '../../lib/constants'; +import type {ColorsSettingsType} from '../../lib/types'; import {ThemeSection} from '../ThemeSection'; import './BrandColors.scss'; @@ -13,19 +15,14 @@ import './BrandColors.scss'; const b = block('brand-colors'); interface BrandColorsProps { - showThemeEditButton?: boolean; - onEditThemeClick: () => void; onSelectCustomColor: () => void; } -export const BrandColors: React.FC<BrandColorsProps> = ({ - showThemeEditButton, - onEditThemeClick, - onSelectCustomColor, -}) => { +export const BrandColors: React.FC<BrandColorsProps> = ({onSelectCustomColor}) => { const {t} = useTranslation('themes'); const [customModeEnabled, setCustomMode] = React.useState(false); + const isMobile = useIsMobile(); const [lightBrandColor] = useThemePaletteColor({ token: 'brand', @@ -36,7 +33,8 @@ export const BrandColors: React.FC<BrandColorsProps> = ({ theme: 'dark', }); - const {applyBrandPreset} = useThemeCreatorMethods(); + const {applyBrandPreset, setColorsSettingsType} = useThemeCreatorMethods(); + const {colorsSettingsType} = useThemeCreator(); const activeColorIndex = React.useMemo(() => { return BRAND_COLORS_PRESETS.findIndex( @@ -67,43 +65,53 @@ export const BrandColors: React.FC<BrandColorsProps> = ({ return ( <ThemeSection className={b()} title={t('title_brand-colors')}> <Flex direction="column"> - <div className={b('brand-color-picker')}> - {BRAND_COLORS_PRESETS.map((value, index) => ( + <Flex gap={2} justifyContent="space-between"> + <div className={b('brand-color-picker')}> + {BRAND_COLORS_PRESETS.map((value, index) => ( + <div + key={index} + className={b('color', { + selected: !customModeEnabled && index === activeColorIndex, + })} + // @ts-ignore + style={{'--color-value': value.brandColor}} + onClick={() => setBrandPreset(index)} + > + <div className={b('color-inner')} /> + </div> + ))} <div - key={index} className={b('color', { - selected: !customModeEnabled && index === activeColorIndex, + selected: customModeEnabled || activeColorIndex === -1, + custom: true, })} - // @ts-ignore - style={{'--color-value': value.brandColor}} - onClick={() => setBrandPreset(index)} + onClick={handleSelectCustomColor} > <div className={b('color-inner')} /> + <Text variant="body-2">{t('label_custom-color')}</Text> </div> - ))} - <div - className={b('color', { - selected: customModeEnabled || activeColorIndex === -1, - custom: true, - })} - onClick={handleSelectCustomColor} - > - <div className={b('color-inner')} /> - <Text variant="body-2">{t('label_custom-color')}</Text> </div> - </div> + {!isMobile && ( + <SegmentedRadioGroup + size="xl" + className={b('colors-settings-type-radio-group')} + defaultValue={colorsSettingsType} + onChange={(e) => { + setColorsSettingsType(e.target.value as ColorsSettingsType); + }} + > + <SegmentedRadioGroup.Option value="basic"> + <Icon data={HandOk} /> + Basic + </SegmentedRadioGroup.Option> + <SegmentedRadioGroup.Option value="advanced"> + <Icon data={Flask} /> + Advanced + </SegmentedRadioGroup.Option> + </SegmentedRadioGroup> + )} + </Flex> </Flex> - {showThemeEditButton && ( - <Button - className={b('switch-button')} - onClick={onEditThemeClick} - view="normal" - size="xl" - > - <Icon data={Sliders} size={20} /> - {t('action_edit-theme')} - </Button> - )} </ThemeSection> ); }; diff --git a/src/components/Themes/ui/ColorPickerInput/ColorPickerInput.tsx b/src/components/Themes/ui/ColorPickerInput/ColorPickerInput.tsx index 67f732953b41..c1dc6513fb04 100644 --- a/src/components/Themes/ui/ColorPickerInput/ColorPickerInput.tsx +++ b/src/components/Themes/ui/ColorPickerInput/ColorPickerInput.tsx @@ -21,6 +21,7 @@ export interface ColorPickerInputProps { errorMessage?: string; size?: TextInputProps['size']; withBorderInPreview?: boolean; + view?: TextInputProps['view']; } export const ColorPickerInput = ({ @@ -31,6 +32,7 @@ export const ColorPickerInput = ({ errorMessage, size = 'l', withBorderInPreview, + view = 'normal', }: ColorPickerInputProps) => { const {t} = useTranslation('themes'); @@ -125,7 +127,7 @@ export const ColorPickerInput = ({ errorPlacement="inside" errorMessage={errorMessage || t('color-input_validation-format-error')} validationState={validationError} - view="normal" + view={view} size={size} onChange={onChange} startContent={ diff --git a/src/components/Themes/ui/ColorsTab/AdvancedSettings/AdvancedSettings.scss b/src/components/Themes/ui/ColorsTab/AdvancedSettings/AdvancedSettings.scss new file mode 100644 index 000000000000..0d2b9808e9bd --- /dev/null +++ b/src/components/Themes/ui/ColorsTab/AdvancedSettings/AdvancedSettings.scss @@ -0,0 +1,9 @@ +@use '../../../../../variables.scss'; + +$block: '.#{variables.$ns}advanced-color-settings'; + +#{$block} { + &__learn-more-button { + --g-button-border-radius: 8px; + } +} diff --git a/src/components/Themes/ui/ColorsTab/AdvancedSettings/AdvancedSettings.tsx b/src/components/Themes/ui/ColorsTab/AdvancedSettings/AdvancedSettings.tsx new file mode 100644 index 000000000000..361f2d35ae60 --- /dev/null +++ b/src/components/Themes/ui/ColorsTab/AdvancedSettings/AdvancedSettings.tsx @@ -0,0 +1,74 @@ +import {CircleQuestion} from '@gravity-ui/icons'; +import {Button, Flex, Icon} from '@gravity-ui/uikit'; +import {useTranslation} from 'next-i18next'; +import {useMemo, useState} from 'react'; + +import {block} from '../../../../../utils'; +import {type TagItem, Tags} from '../../../../Tags/Tags'; +import type {AdvancedColorType} from '../../../lib/types'; +import {AdvancedSettingsTable} from '../../AdvancedSettingsTable/AdvancedSettingsTable'; +import {ThemeSection} from '../../ThemeSection'; + +const b = block('advanced-color-settings'); + +export const AdvancedSettings = () => { + const {t} = useTranslation('themes'); + const [activeTab, setActiveTab] = useState<AdvancedColorType>('basic-palette'); + + const tags: TagItem<AdvancedColorType>[] = useMemo( + () => [ + { + value: 'basic-palette', + title: 'Basic palette', + }, + { + value: 'brand-summary', + title: 'Brand summary', + }, + { + value: 'texts', + title: 'Texts', + }, + { + value: 'backgrounds', + title: 'Backgrounds', + }, + { + value: 'lines', + title: 'Lines', + }, + { + value: 'effects', + title: 'Effects', + }, + { + value: 'misc', + title: 'Misc', + }, + ], + [t], + ); + + return ( + <ThemeSection + title="Colors setup" + titleActions={ + <Button size="xl" className={b('learn-more-button')} disabled> + <Icon data={CircleQuestion} /> + Learn More + </Button> + } + > + <Flex direction="column"> + <Tags + className={b('tags')} + items={tags} + value={activeTab} + onChange={setActiveTab} + /> + </Flex> + + <AdvancedSettingsTable colorType={activeTab} /> + </ThemeSection> + ); +}; diff --git a/src/components/Themes/ui/ColorsTab/BasicSettings/BasicSettings.scss b/src/components/Themes/ui/ColorsTab/BasicSettings/BasicSettings.scss new file mode 100644 index 000000000000..1ee3d3b18b1c --- /dev/null +++ b/src/components/Themes/ui/ColorsTab/BasicSettings/BasicSettings.scss @@ -0,0 +1,19 @@ +@use '../../../../../variables.scss'; +@use '~@gravity-ui/page-constructor/styles/variables.scss' as pcVariables; + +$block: '.#{variables.$ns}basic-color-settings'; + +#{$block} { + &__wrapper { + gap: calc(var(--g-spacing-base) * 24); + + @media (max-width: map-get(pcVariables.$gridBreakpoints, 'sm')) { + gap: calc(var(--g-spacing-base) * 12); + } + } + + &__switch-button { + --g-button-border-radius: 8px; + width: fit-content; + } +} diff --git a/src/components/Themes/ui/ColorsTab/BasicSettings/BasicSettings.tsx b/src/components/Themes/ui/ColorsTab/BasicSettings/BasicSettings.tsx new file mode 100644 index 000000000000..b961427963dc --- /dev/null +++ b/src/components/Themes/ui/ColorsTab/BasicSettings/BasicSettings.tsx @@ -0,0 +1,124 @@ +import {Sliders} from '@gravity-ui/icons'; +import {Button, Flex, Icon} from '@gravity-ui/uikit'; +import {Trans, useTranslation} from 'next-i18next'; +import {Fragment, useCallback, useMemo} from 'react'; + +import {useLocale} from '../../../../../hooks/useLocale'; +import {block} from '../../../../../utils'; +import {useThemeCreator, useThemeCreatorMethods} from '../../../hooks/useThemeCreator'; +import {BasicPalette} from '../../BasicPalette/BasicPalette'; +import {MainSettings} from '../../MainSettings/MainSettings'; +import { + type EditableColorOption, + PrivateColorsSettings, +} from '../../PrivateColorsSettings/PrivateColorsSettings'; + +import './BasicSettings.scss'; + +const b = block('basic-color-settings'); + +export const BasicSettings = () => { + const {t} = useTranslation('themes'); + const locale = useLocale(); + + const advancedColorsOptions = useMemo<EditableColorOption[]>( + () => [ + { + title: t('label_advanced-colors_base-brand-hover'), + name: 'base-brand-hover', + }, + { + title: t('label_advanced-colors_text-brand'), + name: 'text-brand', + }, + { + title: t('label_advanced-colors_text-brand-heavy'), + name: 'text-brand-heavy', + }, + { + title: t('label_advanced-colors_line-brand'), + name: 'line-brand', + }, + { + title: t('label_advanced-colors_base-selection'), + name: 'base-selection', + }, + { + title: t('label_advanced-colors_base-selection-hover'), + name: 'base-selection-hover', + }, + ], + [locale], + ); + + const additionalColorsOptions = useMemo<EditableColorOption[]>( + () => [ + { + title: t('label_additional-colors_text-link'), + name: 'text-link', + }, + { + title: t('label_additional-colors_text-link-hover'), + name: 'text-link-hover', + }, + { + title: t('label_additional-colors_text-link-visited'), + name: 'text-link-visited', + }, + { + title: t('label_additional-colors_text-link-visited-hover'), + name: 'text-link-visited-hover', + }, + ], + [locale], + ); + + const {advancedModeEnabled, showMainSettings} = useThemeCreator(); + const {setAdvancedMode, openMainSettings} = useThemeCreatorMethods(); + + const toggleAdvancedMode = useCallback( + () => setAdvancedMode(!advancedModeEnabled), + [setAdvancedMode, advancedModeEnabled], + ); + + return ( + <Flex direction="column" className={b('wrapper')}> + {showMainSettings ? ( + <MainSettings + advancedModeEnabled={advancedModeEnabled} + toggleAdvancedMode={toggleAdvancedMode} + /> + ) : ( + <Button + className={b('switch-button')} + onClick={openMainSettings} + view="normal" + size="xl" + > + <Icon data={Sliders} size={20} /> + {t('action_edit-theme')} + </Button> + )} + + {advancedModeEnabled && ( + <Fragment> + <BasicPalette /> + <PrivateColorsSettings + title={t('advanced_brand_palette')} + cardsTitle={ + <Trans i18nKey="palette_colors_description" t={t}> + <br /> + </Trans> + } + options={advancedColorsOptions} + /> + <PrivateColorsSettings + title={t('additional_colors')} + cardsTitle={t('label_links-color')} + options={additionalColorsOptions} + /> + </Fragment> + )} + </Flex> + ); +}; diff --git a/src/components/Themes/ui/ColorsTab/ColorsTab.tsx b/src/components/Themes/ui/ColorsTab/ColorsTab.tsx index 0fa49e8b5e32..2db4259c94a7 100644 --- a/src/components/Themes/ui/ColorsTab/ColorsTab.tsx +++ b/src/components/Themes/ui/ColorsTab/ColorsTab.tsx @@ -1,85 +1,22 @@ import {Flex} from '@gravity-ui/uikit'; -import {Trans, useTranslation} from 'next-i18next'; import React from 'react'; -import {useLocale} from '../../../../hooks/useLocale'; import {block} from '../../../../utils'; import {useThemeCreator, useThemeCreatorMethods} from '../../hooks'; -import {BasicPalette} from '../BasicPalette/BasicPalette'; import {BrandColors} from '../BrandColors/BrandColors'; import {ComponentPreview} from '../ComponentPreview/ComponentPreview'; import {ExportThemeSection} from '../ExportThemeSection/ExportThemeSection'; -import {MainSettings} from '../MainSettings/MainSettings'; -import {EditableColorOption, PrivateColorsSettings} from '../PrivateColorsSettings'; +import {AdvancedSettings} from './AdvancedSettings/AdvancedSettings'; +import {BasicSettings} from './BasicSettings/BasicSettings'; import './ColorsTab.scss'; const b = block('colors-tab'); export const ColorsTab = () => { - const {t} = useTranslation('themes'); - const locale = useLocale(); - - const advancedColorsOptions = React.useMemo<EditableColorOption[]>( - () => [ - { - title: t('label_advanced-colors_base-brand-hover'), - name: 'base-brand-hover', - }, - { - title: t('label_advanced-colors_text-brand'), - name: 'text-brand', - }, - { - title: t('label_advanced-colors_text-brand-heavy'), - name: 'text-brand-heavy', - }, - { - title: t('label_advanced-colors_line-brand'), - name: 'line-brand', - }, - { - title: t('label_advanced-colors_base-selection'), - name: 'base-selection', - }, - { - title: t('label_advanced-colors_base-selection-hover'), - name: 'base-selection-hover', - }, - ], - [locale], - ); - - const additionalColorsOptions = React.useMemo<EditableColorOption[]>( - () => [ - { - title: t('label_additional-colors_text-link'), - name: 'text-link', - }, - { - title: t('label_additional-colors_text-link-hover'), - name: 'text-link-hover', - }, - { - title: t('label_additional-colors_text-link-visited'), - name: 'text-link-visited', - }, - { - title: t('label_additional-colors_text-link-visited-hover'), - name: 'text-link-visited-hover', - }, - ], - [locale], - ); - - const {advancedModeEnabled, showMainSettings} = useThemeCreator(); + const {colorsSettingsType} = useThemeCreator(); const {setAdvancedMode, openMainSettings} = useThemeCreatorMethods(); - const toggleAdvancedMode = React.useCallback( - () => setAdvancedMode(!advancedModeEnabled), - [setAdvancedMode, advancedModeEnabled], - ); - const handleSelectCustomColor = React.useCallback(() => { openMainSettings(); setAdvancedMode(true); @@ -87,36 +24,9 @@ export const ColorsTab = () => { return ( <Flex direction="column" className={b()}> - <BrandColors - showThemeEditButton={!showMainSettings} - onEditThemeClick={openMainSettings} - onSelectCustomColor={handleSelectCustomColor} - /> - {showMainSettings && ( - <MainSettings - advancedModeEnabled={advancedModeEnabled} - toggleAdvancedMode={toggleAdvancedMode} - /> - )} - {advancedModeEnabled && ( - <React.Fragment> - <BasicPalette /> - <PrivateColorsSettings - title={t('advanced_brand_palette')} - cardsTitle={ - <Trans i18nKey="palette_colors_description" t={t}> - <br /> - </Trans> - } - options={advancedColorsOptions} - /> - <PrivateColorsSettings - title={t('additional_colors')} - cardsTitle={t('label_links-color')} - options={additionalColorsOptions} - /> - </React.Fragment> - )} + <BrandColors onSelectCustomColor={handleSelectCustomColor} /> + {colorsSettingsType === 'basic' && <BasicSettings />} + {colorsSettingsType === 'advanced' && <AdvancedSettings />} <ComponentPreview /> <ExportThemeSection /> </Flex> diff --git a/src/components/Themes/ui/PrivateColorSelect/PrivateColorSelect.tsx b/src/components/Themes/ui/PrivateColorSelect/PrivateColorSelect.tsx index dcd8091c5448..b9e912b2d8db 100644 --- a/src/components/Themes/ui/PrivateColorSelect/PrivateColorSelect.tsx +++ b/src/components/Themes/ui/PrivateColorSelect/PrivateColorSelect.tsx @@ -1,16 +1,30 @@ import {ChevronDown, PencilToLine} from '@gravity-ui/icons'; -import {Button, Flex, Icon, Popup, Sheet, TextInput, ThemeProvider} from '@gravity-ui/uikit'; -import {isInternalPrivateColorReference} from '@gravity-ui/uikit-themer'; +import { + Button, + Flex, + Icon, + Popup, + Sheet, + TextInput, + type TextInputProps, + ThemeProvider, +} from '@gravity-ui/uikit'; +import { + isInternalPrivateColorReference, + isInternalUtilityColorReference, +} from '@gravity-ui/uikit-themer'; +import {parseInternalUtilityColorReference} from '@gravity-ui/uikit-themer/dist/utils'; import React from 'react'; import {useIsMobile} from '../../../../hooks/useIsMobile'; import {block} from '../../../../utils'; +import type {SemanticColorGroup} from '../../hooks/useThemeSemanticColorOption'; import {ColorPickerInput} from '../ColorPickerInput/ColorPickerInput'; import {ColorPreview} from '../ColorPreview/ColorPreview'; import './PrivateColorSelect.scss'; import {PrivateColorSelectPopupContent} from './PrivateColorSelectPopupContent'; -import type {ColorGroup} from './types'; +import type {BaseColor, ColorGroup} from './types'; const b = block('private-colors-select'); @@ -18,20 +32,25 @@ interface PrivateColorSelectProps { value?: string; defaultValue: string; onChange: (color: string) => void; - groups: ColorGroup[]; + privateGroups: ColorGroup[]; + semanticGroups?: SemanticColorGroup[]; + inputView?: TextInputProps['view']; } export const PrivateColorSelect: React.FC<PrivateColorSelectProps> = ({ - groups, + privateGroups, + semanticGroups, value, defaultValue, onChange, + inputView = 'normal', }) => { const isMobile = useIsMobile(); const [containerElement, setContainerElement] = React.useState<HTMLDivElement | null>(null); const [showPopup, setShowPopup] = React.useState(false); - const isCustomValue = !isInternalPrivateColorReference(value); + const isCustomValue = + !isInternalPrivateColorReference(value) && !isInternalUtilityColorReference(value); const handleChange = React.useCallback( (newVal: string) => { @@ -50,15 +69,37 @@ export const PrivateColorSelect: React.FC<PrivateColorSelectProps> = ({ } }, [isCustomValue, onChange, defaultValue, showPopup]); - const privateColor = React.useMemo(() => { + const selectedColor = React.useMemo(() => { + const isUtilityColor = isInternalUtilityColorReference(value); + + if (isUtilityColor && value) { + const tokenName = parseInternalUtilityColorReference(value); + let semanticItem: BaseColor | undefined; + + semanticGroups?.forEach((group) => + group.groups.forEach((nestedGroup) => + nestedGroup.items.forEach((item) => { + if (item.name === tokenName) { + semanticItem = item; + return; + } + }), + ), + ); + + return semanticItem; + } + const colorGroup = value - ? groups.find((group) => group.privateColors.some((color) => color.token === value)) + ? privateGroups.find((group) => + group.privateColors.some((color) => color.token === value), + ) : undefined; return value ? colorGroup?.privateColors?.find((color) => color.token === value) : undefined; - }, [groups, value]); + }, [privateGroups, value]); const toggleShowPopup = React.useCallback(() => setShowPopup((prev) => !prev), []); const closePopup = React.useCallback(() => setShowPopup(false), []); @@ -74,15 +115,20 @@ export const PrivateColorSelect: React.FC<PrivateColorSelectProps> = ({ return ( <Flex className={b()} ref={setContainerElement} gap={1}> {isCustomValue ? ( - <ColorPickerInput value={value} defaultValue={value || ''} onChange={onChange} /> + <ColorPickerInput + value={value} + defaultValue={value || ''} + onChange={onChange} + view={inputView} + /> ) : ( <TextInput className={b('input')} - value={privateColor?.title || ''} - view="normal" + value={selectedColor?.title || ''} + view={inputView} size="l" startContent={ - <ColorPreview className={b('preview')} color={privateColor?.color} /> + <ColorPreview className={b('preview')} color={selectedColor?.color} /> } endContent={ <Flex gap={1}> @@ -119,7 +165,7 @@ export const PrivateColorSelect: React.FC<PrivateColorSelectProps> = ({ onClose={closePopup} > <PrivateColorSelectPopupContent - groups={groups} + privateGroups={privateGroups} value={value} onChange={handleChange} version="mobile" @@ -137,7 +183,8 @@ export const PrivateColorSelect: React.FC<PrivateColorSelectProps> = ({ }} > <PrivateColorSelectPopupContent - groups={groups} + privateGroups={privateGroups} + semanticGroups={semanticGroups} value={value} onChange={handleChange} /> diff --git a/src/components/Themes/ui/PrivateColorSelect/PrivateColorSelectPopupContent.scss b/src/components/Themes/ui/PrivateColorSelect/PrivateColorSelectPopupContent.scss index 105592cb9b5a..ad192fbcbd16 100644 --- a/src/components/Themes/ui/PrivateColorSelect/PrivateColorSelectPopupContent.scss +++ b/src/components/Themes/ui/PrivateColorSelect/PrivateColorSelectPopupContent.scss @@ -10,7 +10,7 @@ $block: '.#{variables.$ns}private-colors-select-popup'; &_version { &_desktop { width: 465px; - height: 472px; + height: 496px; border-radius: 8px; @media (max-width: map-get(pcVariables.$gridBreakpoints, 'lg')) { @@ -33,7 +33,7 @@ $block: '.#{variables.$ns}private-colors-select-popup'; &__left, &__right { - padding: var(--g-spacing-2) var(--g-spacing-2) 0; + padding: var(--g-spacing-2) var(--g-spacing-2); height: 100%; overflow: auto; } @@ -60,7 +60,7 @@ $block: '.#{variables.$ns}private-colors-select-popup'; display: flex; align-items: center; gap: var(--g-spacing-1); - padding: 5px var(--g-spacing-2); + padding: 3px var(--g-spacing-2); } &__colors-select { diff --git a/src/components/Themes/ui/PrivateColorSelect/PrivateColorSelectPopupContent.tsx b/src/components/Themes/ui/PrivateColorSelect/PrivateColorSelectPopupContent.tsx index 23ed018f3054..db05cf3c3e25 100644 --- a/src/components/Themes/ui/PrivateColorSelect/PrivateColorSelectPopupContent.tsx +++ b/src/components/Themes/ui/PrivateColorSelect/PrivateColorSelectPopupContent.tsx @@ -1,8 +1,15 @@ -import {List, Select, SelectOption, Text} from '@gravity-ui/uikit'; -import {parseInternalPrivateColorReference} from '@gravity-ui/uikit-themer'; -import React from 'react'; +import {Divider, Flex, List, Select, SelectOption, Text} from '@gravity-ui/uikit'; +import { + type UtilityColor, + isInternalPrivateColorReference, + isInternalUtilityColorReference, + parseInternalPrivateColorReference, +} from '@gravity-ui/uikit-themer'; +import {parseInternalUtilityColorReference} from '@gravity-ui/uikit-themer/dist/utils'; +import React, {Fragment} from 'react'; import {block} from '../../../../utils'; +import type {SemanticColorGroup} from '../../hooks/useThemeSemanticColorOption'; import {ColorPreview} from '../ColorPreview/ColorPreview'; import './PrivateColorSelectPopupContent.scss'; @@ -10,6 +17,7 @@ import type {BaseColor, ColorGroup} from './types'; const b = block('private-colors-select-popup'); +// TODO: split components for folders type ColorItemProps = { color: string; title: string; @@ -100,8 +108,125 @@ const ColorsList: React.FC<ColorsListProps> = ({colors, value, onSelect, view = ); }; +interface SemanticGroupListProps { + groups: SemanticColorGroup[]; + value?: string; + onSelect: (value: string) => void; +} + +interface SemanticGroupColorsListProps { + groups: SemanticColorGroup['groups']; + value?: string; + privateGroups: ColorGroup[]; + onSelect: (value: string) => void; +} + +const SemanticGroupColorsList = ({ + groups, + value, + onSelect, + privateGroups, +}: SemanticGroupColorsListProps) => { + const selectedColorTokenName = parseInternalUtilityColorReference(value as UtilityColor); + + const handleSelect = React.useCallback( + (item: BaseColor) => { + onSelect(item.token); + }, + [onSelect], + ); + + const renderItem = React.useCallback((item: BaseColor) => { + if (!item.color) { + return null; + } + + let color: string | undefined = item.color; + + if (isInternalPrivateColorReference(item.color)) { + const parsedPrivateColorToken = parseInternalPrivateColorReference(item.color); + + if (parsedPrivateColorToken) { + const {mainColorToken, privateColorCode} = parsedPrivateColorToken; + + color = privateGroups + .find((group) => group.token === mainColorToken) + ?.privateColors.find( + (privateColor) => privateColor.token === privateColorCode, + )?.color; + } + } + + return <ColorItem key={item.token} color={color ?? ''} title={item.title} />; + }, []); + + return ( + <Flex direction="column" gap={3}> + {groups.map((group) => { + const selectedIndex = group.items.findIndex( + (item) => item.name === selectedColorTokenName, + ); + + return ( + <Flex key={group.title} direction="column" gap={1}> + <Text>{group.title}</Text> + + <List<BaseColor> + items={group.items} + filterable={false} + virtualized={false} + selectedItemIndex={selectedIndex === -1 ? undefined : selectedIndex} + onItemClick={handleSelect} + renderItem={renderItem} + className={b('colors-list')} + itemClassName={b('colors-list-item')} + /> + </Flex> + ); + })} + </Flex> + ); +}; + +const SemanticGroupList = ({groups, value, onSelect}: SemanticGroupListProps) => { + const selectedIndex = React.useMemo( + () => groups.findIndex((item) => item.key === value), + [groups, value], + ); + + const handleSelect = React.useCallback( + (item: SemanticColorGroup) => { + onSelect(item.key); + }, + [onSelect], + ); + + const renderItem = React.useCallback((item: SemanticColorGroup) => { + return ( + <Flex key={item.key} gap={1} alignItems="center" className={b('color-item')}> + {item.icon} + <Text>{item.title}</Text> + </Flex> + ); + }, []); + + return ( + <List<SemanticColorGroup> + items={groups} + filterable={false} + virtualized={false} + selectedItemIndex={selectedIndex} + onItemClick={handleSelect} + renderItem={renderItem} + className={b('colors-list')} + itemClassName={b('colors-list-item')} + /> + ); +}; + interface PrivateColorSelectPopupContentProps { - groups: ColorGroup[]; + privateGroups: ColorGroup[]; + semanticGroups?: SemanticColorGroup[]; value?: string; onChange: (token: string) => void; version?: 'mobile' | 'desktop'; @@ -109,15 +234,32 @@ interface PrivateColorSelectPopupContentProps { export const PrivateColorSelectPopupContent: React.FC<PrivateColorSelectPopupContentProps> = ({ value, - groups, + privateGroups, + semanticGroups, onChange, version = 'desktop', }) => { const colorsRef = React.useRef<HTMLDivElement>(null); + const isUtilityColor = isInternalUtilityColorReference(value); - const [currentGroupToken, setCurrentGroupToken] = React.useState<string | undefined>(() => - value ? parseInternalPrivateColorReference(value)?.mainColorToken : undefined, - ); + const [currentGroupToken, setCurrentGroupToken] = React.useState<string | undefined>(() => { + if (isUtilityColor) { + const utilityTokenName = parseInternalUtilityColorReference(value as UtilityColor); + const groupName = semanticGroups?.find((item) => + item.groups.some((group) => + group.items.some((item) => item.name === utilityTokenName), + ), + ); + + return groupName?.key; + } + + return value ? parseInternalPrivateColorReference(value)?.mainColorToken : undefined; + }); + + const [selectedGroupType, setSelectedGroupType] = React.useState< + 'private' | 'semantic' | undefined + >(isUtilityColor ? 'semantic' : 'private'); React.useEffect(() => { const mainColorToken = value @@ -129,7 +271,7 @@ export const PrivateColorSelectPopupContent: React.FC<PrivateColorSelectPopupCon } }, [value]); - const groupToken = currentGroupToken || groups[0].token; + const groupToken = currentGroupToken || privateGroups[0].token; React.useEffect(() => { colorsRef.current?.scrollTo({ @@ -139,22 +281,56 @@ export const PrivateColorSelectPopupContent: React.FC<PrivateColorSelectPopupCon }, [groupToken]); const groupPrivateColors = React.useMemo( - () => groups.find(({token}) => token === groupToken)?.privateColors || [], - [groups, groupToken], + () => privateGroups.find(({token}) => token === groupToken)?.privateColors || [], + [privateGroups, groupToken], ); return ( <div className={b({version})}> <div className={b('left')}> + <Text variant="caption-2" color="secondary"> + PRIVATE COLORS + </Text> <ColorsList - colors={groups} + colors={privateGroups} value={groupToken} - onSelect={setCurrentGroupToken} + onSelect={(val) => { + setCurrentGroupToken(val); + setSelectedGroupType('private'); + }} view={version === 'mobile' ? 'select' : 'list'} /> + + {semanticGroups && Boolean(semanticGroups?.length) && ( + <Fragment> + <Divider /> + <Text variant="caption-2" color="secondary"> + SEMANTIC COLORS + </Text> + <SemanticGroupList + groups={semanticGroups} + value={groupToken} + onSelect={(val) => { + setCurrentGroupToken(val); + setSelectedGroupType('semantic'); + }} + /> + </Fragment> + )} </div> <div className={b('right')} ref={colorsRef}> - <ColorsList colors={groupPrivateColors} value={value} onSelect={onChange} /> + {selectedGroupType === 'semantic' ? ( + <SemanticGroupColorsList + groups={ + semanticGroups?.find((item) => item.key === groupToken)?.groups || [] + } + privateGroups={privateGroups} + value={value} + onSelect={onChange} + /> + ) : ( + <ColorsList colors={groupPrivateColors} value={value} onSelect={onChange} /> + )} </div> </div> ); diff --git a/src/components/Themes/ui/PrivateColorsSettings/PrivateColorsSettings.tsx b/src/components/Themes/ui/PrivateColorsSettings/PrivateColorsSettings.tsx index d596cc54b99a..abd120f52c09 100644 --- a/src/components/Themes/ui/PrivateColorsSettings/PrivateColorsSettings.tsx +++ b/src/components/Themes/ui/PrivateColorsSettings/PrivateColorsSettings.tsx @@ -47,7 +47,7 @@ const PrivateColorEditor: React.FC<PrivateColorEditorProps> = ({name, theme, col return ( <PrivateColorSelect - groups={colorGroups} + privateGroups={colorGroups} defaultValue={defaultValue} value={color} onChange={setColor} diff --git a/src/components/Themes/ui/ThemeCreatorContextProvider.tsx b/src/components/Themes/ui/ThemeCreatorContextProvider.tsx index 83ef2d3f50a5..392020c4c909 100644 --- a/src/components/Themes/ui/ThemeCreatorContextProvider.tsx +++ b/src/components/Themes/ui/ThemeCreatorContextProvider.tsx @@ -35,7 +35,7 @@ import { updateFontFamilyInTheme, updateFontFamilyTypeTitleInTheme, } from '../lib/themeCreatorUtils'; -import type {ThemeCreatorState} from '../lib/types'; +import type {AdvanceColors, ColorsSettingsType, ThemeCreatorState} from '../lib/types'; type ThemeCreatorAction = | { @@ -103,6 +103,14 @@ type ThemeCreatorAction = | { type: 'setAdvancedMode'; payload: boolean; + } + | { + type: 'setColorsSettingsType'; + payload: ColorsSettingsType; + } + | { + type: 'updateAdvancedColors'; + payload: AdvanceColors; }; const themeCreatorReducer = ( @@ -155,6 +163,16 @@ const themeCreatorReducer = ( }; case 'reinitialize': return initThemeCreator(action.payload); + case 'setColorsSettingsType': + return { + ...newState, + colorsSettingsType: action.payload, + }; + case 'updateAdvancedColors': + return { + ...newState, + advancedColors: action.payload, + }; default: return prevState; } @@ -334,6 +352,24 @@ export const ThemeCreatorContextProvider: React.FC<ThemeCreatorProps> = ({ }); }, []); + const setColorsSettingsType = React.useCallback< + ThemeCreatorMethodsContextType['setColorsSettingsType'] + >((payload) => { + dispatchThemeCreator({ + type: 'setColorsSettingsType', + payload, + }); + }, []); + + const updateAdvancedColors = React.useCallback< + ThemeCreatorMethodsContextType['updateAdvancedColors'] + >((payload) => { + dispatchThemeCreator({ + type: 'updateAdvancedColors', + payload, + }); + }, []); + const methods = React.useMemo( () => ({ addColor, @@ -353,6 +389,8 @@ export const ThemeCreatorContextProvider: React.FC<ThemeCreatorProps> = ({ openMainSettings, setAdvancedMode, importTheme, + setColorsSettingsType, + updateAdvancedColors, }), [ addColor, @@ -372,6 +410,8 @@ export const ThemeCreatorContextProvider: React.FC<ThemeCreatorProps> = ({ openMainSettings, setAdvancedMode, importTheme, + setColorsSettingsType, + updateAdvancedColors, ], ); diff --git a/src/components/Themes/ui/ThemeSection.tsx b/src/components/Themes/ui/ThemeSection.tsx index 168140657849..af3454b162b0 100644 --- a/src/components/Themes/ui/ThemeSection.tsx +++ b/src/components/Themes/ui/ThemeSection.tsx @@ -11,13 +11,20 @@ interface ThemeSectionProps { title: string; children?: React.ReactNode; className?: string; + titleActions?: React.ReactNode; } -export const ThemeSection: React.FC<ThemeSectionProps> = ({title, className, children}) => { +export const ThemeSection: React.FC<ThemeSectionProps> = ({ + title, + className, + children, + titleActions, +}) => { return ( <div className={b(null, className)}> - <Flex> + <Flex justifyContent="space-between"> <Text className={b('title')}>{title}</Text> + {titleActions} </Flex> {children} </div> From 47de41fb40a03fe577ddd5e22a32cfcf4011a17e Mon Sep 17 00:00:00 2001 From: aobityutskiy <aobityutskiy@yandex-team.ru> Date: Wed, 26 Nov 2025 22:27:04 +0300 Subject: [PATCH 2/5] feat: add logic for disable recursive color selecting + refactoring --- package-lock.json | 6 +- public/locales/en/themes.json | 170 ++++++++- public/locales/ru/themes.json | 170 ++++++++- .../hooks/useThemeSemanticColorOption.tsx | 111 ++++-- .../Themes/hooks/useThemeUtilityColor.ts | 3 +- src/components/Themes/lib/constants.ts | 148 +------- .../Themes/lib/themeCreatorUtils.ts | 4 +- src/components/Themes/lib/utils.ts | 13 +- .../AddExtraColor/AddExtraColor.tsx | 6 +- .../AdvancedSettingsTable.tsx | 242 +------------ .../ExtraColorName/ExtraColorName.tsx | 8 +- .../ui/AdvancedSettingsTable/columns.tsx | 130 +++++++ .../Themes/ui/AdvancedSettingsTable/hooks.tsx | 112 ++++++ .../AdvancedSettings/AdvancedSettings.tsx | 16 +- .../ColorSelectPopupContent.scss} | 9 + .../ColorSelectPopupContent.tsx | 137 +++++++ .../ColorSelectPopupContentItems.tsx | 248 +++++++++++++ .../GravityColorSelect.scss} | 0 .../GravityColorSelect.tsx} | 20 +- .../Themes/ui/GravityColorSelect/index.ts | 1 + .../types.ts | 0 .../Themes/ui/GravityColorSelect/utils.ts | 36 ++ .../PrivateColorSelectPopupContent.tsx | 337 ------------------ .../Themes/ui/PrivateColorSelect/index.ts | 1 - .../PrivateColorsSettings.tsx | 4 +- 25 files changed, 1140 insertions(+), 792 deletions(-) create mode 100644 src/components/Themes/ui/AdvancedSettingsTable/columns.tsx create mode 100644 src/components/Themes/ui/AdvancedSettingsTable/hooks.tsx rename src/components/Themes/ui/{PrivateColorSelect/PrivateColorSelectPopupContent.scss => GravityColorSelect/ColorSelectPopupContent.scss} (91%) create mode 100644 src/components/Themes/ui/GravityColorSelect/ColorSelectPopupContent.tsx create mode 100644 src/components/Themes/ui/GravityColorSelect/ColorSelectPopupContentItems.tsx rename src/components/Themes/ui/{PrivateColorSelect/PrivateColorSelect.scss => GravityColorSelect/GravityColorSelect.scss} (100%) rename src/components/Themes/ui/{PrivateColorSelect/PrivateColorSelect.tsx => GravityColorSelect/GravityColorSelect.tsx} (93%) create mode 100644 src/components/Themes/ui/GravityColorSelect/index.ts rename src/components/Themes/ui/{PrivateColorSelect => GravityColorSelect}/types.ts (100%) create mode 100644 src/components/Themes/ui/GravityColorSelect/utils.ts delete mode 100644 src/components/Themes/ui/PrivateColorSelect/PrivateColorSelectPopupContent.tsx delete mode 100644 src/components/Themes/ui/PrivateColorSelect/index.ts diff --git a/package-lock.json b/package-lock.json index 756f7090a2e1..02bcfd8917bb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3630,9 +3630,9 @@ } }, "node_modules/@gravity-ui/uikit-themer": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/@gravity-ui/uikit-themer/-/uikit-themer-1.4.2.tgz", - "integrity": "sha512-L4M7nSrUk9aGpo69+GIMUtuG0i0BakMXmEV99PIGi4Fn7XfORemb6paluTdfgs38Ukt4xQhkFe0m3SiXRMQSfA==", + "version": "1.4.1", + "resolved": "file:../uikit-themer/gravity-ui-uikit-themer-1.4.1.tgz", + "integrity": "sha512-yRTHOT3smgz+Px5VUievZVD4U4t62bkxJPXdwxSif2NOk0Ee5FFufAXWaR38rwvgB2B9adTMb5qgHTNViZqnMg==", "dependencies": { "chroma-js": "^3.1.2", "lodash-es": "^4.17.21" diff --git a/public/locales/en/themes.json b/public/locales/en/themes.json index 3475cf0a1217..647990892752 100644 --- a/public/locales/en/themes.json +++ b/public/locales/en/themes.json @@ -19,6 +19,32 @@ "palette_colors_description": "Support Colors for various cases and <br /> states", "dark_theme": "Dark theme", "light_theme": "Light theme", + "theme_name_light": "Light", + "theme_name_dark": "Dark", + "title_advance-settings-table_title-variable": "Variable", + "title_advance-settings-table_title-light": "Light theme value", + "title_advance-settings-table_title-dark": "Dark theme value", + "title_advance-color-settings-group-base": "Base", + "title_advance-color-settings-group-basic": "Base", + "title_advance-color-settings-group-semantic": "Semantic", + "title_advance-color-settings-group-brand": "Brand", + "title_advance-color-settings-group-extra-color": "Extra color", + "title_advance-color-settings-group-always-dark": "Always Dark", + "title_advance-color-settings-group-always-light": "Always Light", + "title_advance-color-settings-group-main-inversion": "Main Inversion", + "title_advance-color-settings-group-light-semantic": "Light Semantic", + "title_advance-color-settings-group-medium-semantic": "Medium Semantic", + "title_advance-color-settings-group-heavy-semantic": "Heavy Semantic", + "title_advance-color-settings-group-floats": "Floats", + "title_advance-color-settings-group-general": "General", + "title_advance-color-settings-group-other": "Other", + "title_advance-color-settings-group-scroll": "Scroll", + "title_advance-color-settings-group-axes": "Axes", + "title_advance-color-settings-group-tooltips": "Tooltips", + "title_advance-color-settings-group-base-color": "Base Color", + "title_advance-color-settings-group-brand-palette": "Brand Palette", + "title_advance-color-settings-group-advanced-brand-palette": "Advanced Brand Palette", + "title_advance-color-settings-group-additional-colors": "Additional Colors", "advanced_brand_palette": "Advanced Brand Palette", "additional_colors": "Additional Colors", "component_preview": "Component preview", @@ -41,6 +67,8 @@ "button": "Button", "title_brand-colors": "Brand colors", "action_edit-theme": "Edit theme", + "action_learn-more": "Learn More", + "action_add-extra-color": "Add Extra Color", "title_brand-palette-foundations": "Brand Palette Foundations", "label_text-on-brand": "Text on Brand", "label_colors-to-generate-palette": "The colors to generate the palette", @@ -80,5 +108,143 @@ "action_hide-advanced-settings": "Hide Advanced Settings", "action_open-advanced-settings": "Advanced Settings", "label_link-to-font": "Link to font", - "title_typography-styles": "Typography Styles" -} \ No newline at end of file + "title_typography-styles": "Typography Styles", + "title_advance-color-settings-basic-palette": "Basic Palette", + "title_advance-color-settings-brand-summary": "Brand summary", + "title_advance-color-settings-texts": "Texts", + "title_advance-color-settings-backgrounds": "Backgrounds", + "title_advance-color-settings-lines": "Lines", + "title_advance-color-settings-effects": "Effects", + "title_advance-color-settings-misc": "Misc", + "text_utility-color_text-primary_description": "Primary text on the page. It is default for headers, paragraphs, buttons.", + "text_utility-color_text-complementary_description": "Complementary text on the page. Controls, notes, etc.", + "text_utility-color_text-secondary_description": "Secondary text on the page. Captions, definitions, nonessential information.", + "text_utility-color_text-hint_description": "Control hint.", + "text_utility-color_text-info_description": "Info text.", + "text_utility-color_text-info-heavy_description": "Info text with underlay.", + "text_utility-color_text-positive_description": "Positive text.", + "text_utility-color_text-positive-heavy_description": "Positive text with underlay.", + "text_utility-color_text-warning_description": "Warning text.", + "text_utility-color_text-warning-heavy_description": "Warning text with underlay.", + "text_utility-color_text-danger_description": "Danger text.", + "text_utility-color_text-danger-heavy_description": "Danger text with underlay.", + "text_utility-color_text-utility_description": "For emphasizing, without semantic.", + "text_utility-color_text-utility-heavy_description": "Utility text with underlay.", + "text_utility-color_text-misc_description": "For emphasizing, without semantic.", + "text_utility-color_text-misc-heavy_description": "Misc text with underlay.", + "text_utility-color_text-brand_description": "Brand text.", + "text_utility-color_text-brand-heavy_description": "Brand text with underlay.", + "text_utility-color_text-brand-contrast_description": "Brand text with high contrast.", + "text_utility-color_text-link_description": "Links.", + "text_utility-color_text-link-hover_description": "Hover for Link.", + "text_utility-color_text-link-visited_description": "Visited Link.", + "text_utility-color_text-link-visited-hover_description": "Hover for Visited Link.", + "text_utility-color_text-dark-primary_description": "Primary text over light background.", + "text_utility-color_text-dark-complementary_description": "Complementary text over light background.", + "text_utility-color_text-dark-secondary_description": "Secondary text over light background.", + "text_utility-color_text-dark-hint_description": "Minimal contrast.", + "text_utility-color_text-light-primary_description": "Primary text over dark background.", + "text_utility-color_text-light-complementary_description": "Complementary text over dark background.", + "text_utility-color_text-light-secondary_description": "Secondary text over dark background.", + "text_utility-color_text-light-hint_description": "Minimal contrast.", + "text_utility-color_text-inverted-primary_description": "Primary text.", + "text_utility-color_text-inverted-complementary_description": "Complementary text.", + "text_utility-color_text-inverted-secondary_description": "Secondary text.", + "text_utility-color_text-inverted-hint_description": "Minimal contrast.", + "text_utility-color_base-background_description": "Page's background.", + "text_utility-color_base-generic_description": "Generic gray base, buttons and other objects.", + "text_utility-color_base-generic-hover_description": "Hover for Generic.", + "text_utility-color_base-generic-medium_description": "Neutral blocks with medium contrast.", + "text_utility-color_base-generic-medium-hover_description": "Hover for Generic Medium.", + "text_utility-color_base-generic-accent_description": "Background for controls (checkbox, radio, etc.).", + "text_utility-color_base-generic-accent-disabled_description": "Disabled background for controls.", + "text_utility-color_base-generic-ultralight_description": "Background with minimal contrast. Not recommended to use.", + "text_utility-color_base-simple-hover_description": "Hover for transparent objects (works over light backgrounds).", + "text_utility-color_base-simple-hover-solid_description": "Hover for transparent objects (works over light backgrounds).", + "text_utility-color_base-brand_description": "Background for accented object.", + "text_utility-color_base-brand-hover_description": "Hover for Brand.", + "text_utility-color_base-selection_description": "Highlight selected objects in menus, calendars, etc.", + "text_utility-color_base-selection-hover_description": "Hover for Selection.", + "text_utility-color_base-info-light_description": "Info semantic background.", + "text_utility-color_base-info-light-hover_description": "Hover for Info.", + "text_utility-color_base-positive-light_description": "Positive semantic background.", + "text_utility-color_base-positive-light-hover_description": "Hover for Positive.", + "text_utility-color_base-warning-light_description": "Warning semantic background.", + "text_utility-color_base-warning-light-hover_description": "Hover for Warning.", + "text_utility-color_base-danger-light_description": "Negative semantic background.", + "text_utility-color_base-danger-light-hover_description": "Hover for Danger.", + "text_utility-color_base-utility-light_description": "Utility semantic background.", + "text_utility-color_base-utility-light-hover_description": "Hover for Utility.", + "text_utility-color_base-misc-light_description": "Uncategorized semantic background.", + "text_utility-color_base-misc-light-hover_description": "Hover for Misc.", + "text_utility-color_base-neutral-light_description": "Neutral semantic background.", + "text_utility-color_base-neutral-light-hover_description": "Hover for Neutral.", + "text_utility-color_base-info-medium_description": "Info semantic background, medium accent.", + "text_utility-color_base-info-medium-hover_description": "Hover for Info Medium.", + "text_utility-color_base-positive-medium_description": "Positive semantic background, medium accent.", + "text_utility-color_base-positive-medium-hover_description": "Hover for Positive Medium.", + "text_utility-color_base-warning-medium_description": "Warning semantic background, medium accent.", + "text_utility-color_base-warning-medium-hover_description": "Hover for Warning Medium.", + "text_utility-color_base-danger-medium_description": "Danger semantic background, medium accent.", + "text_utility-color_base-danger-medium-hover_description": "Hover for Danger Medium.", + "text_utility-color_base-utility-medium_description": "Utility semantic background, medium accent.", + "text_utility-color_base-utility-medium-hover_description": "Hover for Utility Medium.", + "text_utility-color_base-misc-medium_description": "Uncategorized semantic background, medium accent.", + "text_utility-color_base-misc-medium-hover_description": "Hover for Misc Medium.", + "text_utility-color_base-neutral-medium_description": "Neutral semantic background, medium accent.", + "text_utility-color_base-neutral-medium-hover_description": "Hover for Neutral Medium.", + "text_utility-color_base-info-heavy_description": "Info semantic background, strong accent.", + "text_utility-color_base-info-heavy-hover_description": "Hover for Info Heavy.", + "text_utility-color_base-positive-heavy_description": "Positive semantic background, strong accent.", + "text_utility-color_base-positive-heavy-hover_description": "Hover for Positive Heavy.", + "text_utility-color_base-warning-heavy_description": "Warning semantic background, strong accent.", + "text_utility-color_base-warning-heavy-hover_description": "Hover for Warning Heavy.", + "text_utility-color_base-danger-heavy_description": "Negative semantic background, strong accent.", + "text_utility-color_base-danger-heavy-hover_description": "Hover for Danger Heavy", + "text_utility-color_base-utility-heavy_description": "Utility semantic background, strong accent.", + "text_utility-color_base-utility-heavy-hover_description": "Hover for Utility Heavy.", + "text_utility-color_base-misc-heavy_description": "Uncategorized semantic background, strong accent.", + "text_utility-color_base-misc-heavy-hover_description": "Hover for Misc Heavy.", + "text_utility-color_base-neutral-heavy_description": "Neutral semantic background, strong accent.", + "text_utility-color_base-neutral-heavy-hover_description": "Hover for Neutral Heavy.", + "text_utility-color_base-light_description": "Background on top of another darker background.", + "text_utility-color_base-light-hover_description": "Hover for Light.", + "text_utility-color_base-light-simple-hover_description": "Hover for transparent objects (works over dark backgrounds).", + "text_utility-color_base-light-disabled_description": "Disabled controls.", + "text_utility-color_base-light-accent-disabled_description": "Disabled active controls.", + "text_utility-color_base-float_description": "Raised layers background.", + "text_utility-color_base-float-hover_description": "Hover for Float.", + "text_utility-color_base-float-medium_description": "Float for medium contrast.", + "text_utility-color_base-float-heavy_description": "Float for strong contrast.", + "text_utility-color_base-float-accent_description": "Raised controls.", + "text_utility-color_base-float-accent-hover_description": "Hover for Float Accent.", + "text_utility-color_base-float-announcement_description": "Float background for announcements.", + "text_utility-color_base-modal_description": "Floating components with a veil.", + "text_utility-color_line-generic_description": "Button borders, dividers, basic block borders. Almost all lines.", + "text_utility-color_line-generic-hover_description": "Hover for Generic.", + "text_utility-color_line-generic-active_description": "Active state for Generic.", + "text_utility-color_line-generic-accent_description": "Control borders.", + "text_utility-color_line-generic-accent-hover_description": "Hover for Generic Accent.", + "text_utility-color_line-generic-solid_description": "Generic without transparency (to avoid collision artefacts).", + "text_utility-color_line-brand_description": "Brand blocks", + "text_utility-color_line-focus_description": "Focused blocks", + "text_utility-color_line-info_description": "Info blocks.", + "text_utility-color_line-positive_description": "Positive blocks.", + "text_utility-color_line-warning_description": "Warning blocks.", + "text_utility-color_line-danger_description": "Danger blocks. Blocks with negative context.", + "text_utility-color_line-utility_description": "Utility blocks.", + "text_utility-color_line-misc_description": "Uncategorized blocks.", + "text_utility-color_line-light_description": "Dividers and borders over dark background.", + "text_utility-color_sfx-veil_description": "Popup backdrop.", + "text_utility-color_sfx-shadow_description": "Shadow for everything that might have it.", + "text_utility-color_sfx-shadow-light_description": "Lighter version of shadow.", + "text_utility-color_sfx-shadow-heavy_description": "Heavy shadows. DEPRECATED.", + "text_utility-color_sfx-fade_description": "Enlighten while loading.", + "text_utility-color_scroll-track_description": "Scroll background.", + "text_utility-color_scroll-handle_description": "The handle to move the scroll.", + "text_utility-color_scroll-handle-hover_description": "Hover state for scroll handle.", + "text_utility-color_scroll-corner_description": "A corner where horizontal and vertical scrolls meet.", + "text_utility-color_infographics-axis_description": "Graph axis", + "text_utility-color_infographics-tooltip-bg_description": "Main background for tooltips.", + "text_utility-color_disabled_description": "Disabled: recursive color reference detected" +} diff --git a/public/locales/ru/themes.json b/public/locales/ru/themes.json index bc74483b0f5e..a36ebfee29e9 100644 --- a/public/locales/ru/themes.json +++ b/public/locales/ru/themes.json @@ -19,6 +19,32 @@ "palette_colors_description": "Оттенки для различных сценариев и состояний", "dark_theme": "Тёмная тема", "light_theme": "Светлая тема", + "theme_name_light": "Светлая", + "theme_name_dark": "Тёмная", + "title_advance-settings-table_title-variable": "Переменная", + "title_advance-settings-table_title-light": "Значение светлой темы", + "title_advance-settings-table_title-dark": "Значение тёмной темы", + "title_advance-color-settings-base": "Базовый", + "title_advance-color-settings-group-basic": "Базовый", + "title_advance-color-settings-semantic": "Семантический", + "title_advance-color-settings-group-brand": "Бренд", + "title_advance-color-settings-group-extra-color": "Дополнительный цвет", + "title_advance-color-settings-group-always-dark": "Всегда тёмный", + "title_advance-color-settings-group-always-light": "Всегда светлый", + "title_advance-color-settings-group-main-inversion": "Основной инвертированный", + "title_advance-color-settings-group-light-semantic": "Светлый семантический", + "title_advance-color-settings-group-medium-semantic": "Среднеконтрастный семантический", + "title_advance-color-settings-group-heavy-semantic": "Высококонтрастный семантический", + "title_advance-color-settings-group-floats": "Плавающие", + "title_advance-color-settings-group-general": "Общий", + "title_advance-color-settings-group-other": "Другой", + "title_advance-color-settings-group-scroll": "Скролл", + "title_advance-color-settings-group-axes": "Оси", + "title_advance-color-settings-group-tooltips": "Подсказки", + "title_advance-color-settings-group-base-color": "Базовый цвет", + "title_advance-color-settings-group-brand-palette": "Брендовая палитра", + "title_advance-color-settings-group-advanced-brand-palette": "Расширенная брендовая палитра", + "title_advance-color-settings-group-additional-colors": "Дополнительные цвета", "advanced_brand_palette": "Расширенная брендовая палитра", "additional_colors": "Дополнительные цвета", "component_preview": "Предпросмотр компонентов", @@ -41,6 +67,8 @@ "button": "Кнопка", "title_brand-colors": "Брендовый цвет", "action_edit-theme": "Редактировать тему", + "action_learn-more": "Узнать больше", + "action_add-extra-color": "Добавить дополнительный цвет", "title_brand-palette-foundations": "Палитра брендирования", "label_text-on-brand": "Текст на брендовом цвете", "label_colors-to-generate-palette": "Цвета для генерации палитры", @@ -80,5 +108,143 @@ "action_hide-advanced-settings": "Скрыть расширенные настройки", "action_open-advanced-settings": "Расширенные настройки", "label_link-to-font": "Ссылка на шрифт", - "title_typography-styles": "Типографика" -} \ No newline at end of file + "title_typography-styles": "Типографика", + "title_advance-color-settings-basic-palette": "Базовая палитра", + "title_advance-color-settings-brand-summary": "Брендовая палитра", + "title_advance-color-settings-texts": "Текст", + "title_advance-color-settings-backgrounds": "Фон", + "title_advance-color-settings-lines": "Линия", + "title_advance-color-settings-effects": "Эффект", + "title_advance-color-settings-misc": "Разное", + "text_utility-color_text-primary_description": "Primary text on the page. It is default for headers, paragraphs, buttons.", + "text_utility-color_text-complementary_description": "Complementary text on the page. Controls, notes, etc.", + "text_utility-color_text-secondary_description": "Secondary text on the page. Captions, definitions, nonessential information.", + "text_utility-color_text-hint_description": "Control hint.", + "text_utility-color_text-info_description": "Info text.", + "text_utility-color_text-info-heavy_description": "Info text with underlay.", + "text_utility-color_text-positive_description": "Positive text.", + "text_utility-color_text-positive-heavy_description": "Positive text with underlay.", + "text_utility-color_text-warning_description": "Warning text.", + "text_utility-color_text-warning-heavy_description": "Warning text with underlay.", + "text_utility-color_text-danger_description": "Danger text.", + "text_utility-color_text-danger-heavy_description": "Danger text with underlay.", + "text_utility-color_text-utility_description": "For emphasizing, without semantic.", + "text_utility-color_text-utility-heavy_description": "Utility text with underlay.", + "text_utility-color_text-misc_description": "For emphasizing, without semantic.", + "text_utility-color_text-misc-heavy_description": "Misc text with underlay.", + "text_utility-color_text-brand_description": "Brand text.", + "text_utility-color_text-brand-heavy_description": "Brand text with underlay.", + "text_utility-color_text-brand-contrast_description": "Brand text with high contrast.", + "text_utility-color_text-link_description": "Links.", + "text_utility-color_text-link-hover_description": "Hover for Link.", + "text_utility-color_text-link-visited_description": "Visited Link.", + "text_utility-color_text-link-visited-hover_description": "Hover for Visited Link.", + "text_utility-color_text-dark-primary_description": "Primary text over light background.", + "text_utility-color_text-dark-complementary_description": "Complementary text over light background.", + "text_utility-color_text-dark-secondary_description": "Secondary text over light background.", + "text_utility-color_text-dark-hint_description": "Minimal contrast.", + "text_utility-color_text-light-primary_description": "Primary text over dark background.", + "text_utility-color_text-light-complementary_description": "Complementary text over dark background.", + "text_utility-color_text-light-secondary_description": "Secondary text over dark background.", + "text_utility-color_text-light-hint_description": "Minimal contrast.", + "text_utility-color_text-inverted-primary_description": "Primary text.", + "text_utility-color_text-inverted-complementary_description": "Complementary text.", + "text_utility-color_text-inverted-secondary_description": "Secondary text.", + "text_utility-color_text-inverted-hint_description": "Minimal contrast.", + "text_utility-color_base-background_description": "Page's background.", + "text_utility-color_base-generic_description": "Generic gray base, buttons and other objects.", + "text_utility-color_base-generic-hover_description": "Hover for Generic.", + "text_utility-color_base-generic-medium_description": "Neutral blocks with medium contrast.", + "text_utility-color_base-generic-medium-hover_description": "Hover for Generic Medium.", + "text_utility-color_base-generic-accent_description": "Background for controls (checkbox, radio, etc.).", + "text_utility-color_base-generic-accent-disabled_description": "Disabled background for controls.", + "text_utility-color_base-generic-ultralight_description": "Background with minimal contrast. Not recommended to use.", + "text_utility-color_base-simple-hover_description": "Hover for transparent objects (works over light backgrounds).", + "text_utility-color_base-simple-hover-solid_description": "Hover for transparent objects (works over light backgrounds).", + "text_utility-color_base-brand_description": "Background for accented object.", + "text_utility-color_base-brand-hover_description": "Hover for Brand.", + "text_utility-color_base-selection_description": "Highlight selected objects in menus, calendars, etc.", + "text_utility-color_base-selection-hover_description": "Hover for Selection.", + "text_utility-color_base-info-light_description": "Info semantic background.", + "text_utility-color_base-info-light-hover_description": "Hover for Info.", + "text_utility-color_base-positive-light_description": "Positive semantic background.", + "text_utility-color_base-positive-light-hover_description": "Hover for Positive.", + "text_utility-color_base-warning-light_description": "Warning semantic background.", + "text_utility-color_base-warning-light-hover_description": "Hover for Warning.", + "text_utility-color_base-danger-light_description": "Negative semantic background.", + "text_utility-color_base-danger-light-hover_description": "Hover for Danger.", + "text_utility-color_base-utility-light_description": "Utility semantic background.", + "text_utility-color_base-utility-light-hover_description": "Hover for Utility.", + "text_utility-color_base-misc-light_description": "Uncategorized semantic background.", + "text_utility-color_base-misc-light-hover_description": "Hover for Misc.", + "text_utility-color_base-neutral-light_description": "Neutral semantic background.", + "text_utility-color_base-neutral-light-hover_description": "Hover for Neutral.", + "text_utility-color_base-info-medium_description": "Info semantic background, medium accent.", + "text_utility-color_base-info-medium-hover_description": "Hover for Info Medium.", + "text_utility-color_base-positive-medium_description": "Positive semantic background, medium accent.", + "text_utility-color_base-positive-medium-hover_description": "Hover for Positive Medium.", + "text_utility-color_base-warning-medium_description": "Warning semantic background, medium accent.", + "text_utility-color_base-warning-medium-hover_description": "Hover for Warning Medium.", + "text_utility-color_base-danger-medium_description": "Danger semantic background, medium accent.", + "text_utility-color_base-danger-medium-hover_description": "Hover for Danger Medium.", + "text_utility-color_base-utility-medium_description": "Utility semantic background, medium accent.", + "text_utility-color_base-utility-medium-hover_description": "Hover for Utility Medium.", + "text_utility-color_base-misc-medium_description": "Uncategorized semantic background, medium accent.", + "text_utility-color_base-misc-medium-hover_description": "Hover for Misc Medium.", + "text_utility-color_base-neutral-medium_description": "Neutral semantic background, medium accent.", + "text_utility-color_base-neutral-medium-hover_description": "Hover for Neutral Medium.", + "text_utility-color_base-info-heavy_description": "Info semantic background, strong accent.", + "text_utility-color_base-info-heavy-hover_description": "Hover for Info Heavy.", + "text_utility-color_base-positive-heavy_description": "Positive semantic background, strong accent.", + "text_utility-color_base-positive-heavy-hover_description": "Hover for Positive Heavy.", + "text_utility-color_base-warning-heavy_description": "Warning semantic background, strong accent.", + "text_utility-color_base-warning-heavy-hover_description": "Hover for Warning Heavy.", + "text_utility-color_base-danger-heavy_description": "Negative semantic background, strong accent.", + "text_utility-color_base-danger-heavy-hover_description": "Hover for Danger Heavy", + "text_utility-color_base-utility-heavy_description": "Utility semantic background, strong accent.", + "text_utility-color_base-utility-heavy-hover_description": "Hover for Utility Heavy.", + "text_utility-color_base-misc-heavy_description": "Uncategorized semantic background, strong accent.", + "text_utility-color_base-misc-heavy-hover_description": "Hover for Misc Heavy.", + "text_utility-color_base-neutral-heavy_description": "Neutral semantic background, strong accent.", + "text_utility-color_base-neutral-heavy-hover_description": "Hover for Neutral Heavy.", + "text_utility-color_base-light_description": "Background on top of another darker background.", + "text_utility-color_base-light-hover_description": "Hover for Light.", + "text_utility-color_base-light-simple-hover_description": "Hover for transparent objects (works over dark backgrounds).", + "text_utility-color_base-light-disabled_description": "Disabled controls.", + "text_utility-color_base-light-accent-disabled_description": "Disabled active controls.", + "text_utility-color_base-float_description": "Raised layers background.", + "text_utility-color_base-float-hover_description": "Hover for Float.", + "text_utility-color_base-float-medium_description": "Float for medium contrast.", + "text_utility-color_base-float-heavy_description": "Float for strong contrast.", + "text_utility-color_base-float-accent_description": "Raised controls.", + "text_utility-color_base-float-accent-hover_description": "Hover for Float Accent.", + "text_utility-color_base-float-announcement_description": "Float background for announcements.", + "text_utility-color_base-modal_description": "Floating components with a veil.", + "text_utility-color_line-generic_description": "Button borders, dividers, basic block borders. Almost all lines.", + "text_utility-color_line-generic-hover_description": "Hover for Generic.", + "text_utility-color_line-generic-active_description": "Active state for Generic.", + "text_utility-color_line-generic-accent_description": "Control borders.", + "text_utility-color_line-generic-accent-hover_description": "Hover for Generic Accent.", + "text_utility-color_line-generic-solid_description": "Generic without transparency (to avoid collision artefacts).", + "text_utility-color_line-brand_description": "Brand blocks", + "text_utility-color_line-focus_description": "Focused blocks", + "text_utility-color_line-info_description": "Info blocks.", + "text_utility-color_line-positive_description": "Positive blocks.", + "text_utility-color_line-warning_description": "Warning blocks.", + "text_utility-color_line-danger_description": "Danger blocks. Blocks with negative context.", + "text_utility-color_line-utility_description": "Utility blocks.", + "text_utility-color_line-misc_description": "Uncategorized blocks.", + "text_utility-color_line-light_description": "Dividers and borders over dark background.", + "text_utility-color_sfx-veil_description": "Popup backdrop.", + "text_utility-color_sfx-shadow_description": "Shadow for everything that might have it.", + "text_utility-color_sfx-shadow-light_description": "Lighter version of shadow.", + "text_utility-color_sfx-shadow-heavy_description": "Heavy shadows. DEPRECATED.", + "text_utility-color_sfx-fade_description": "Enlighten while loading.", + "text_utility-color_scroll-track_description": "Scroll background.", + "text_utility-color_scroll-handle_description": "The handle to move the scroll.", + "text_utility-color_scroll-handle-hover_description": "Hover state for scroll handle.", + "text_utility-color_scroll-corner_description": "A corner where horizontal and vertical scrolls meet.", + "text_utility-color_infographics-axis_description": "Graph axis", + "text_utility-color_infographics-tooltip-bg_description": "Main background for tooltips.", + "text_utility-color_disabled_description": "Заблокировано: обнаружено рекурсивное значение" +} diff --git a/src/components/Themes/hooks/useThemeSemanticColorOption.tsx b/src/components/Themes/hooks/useThemeSemanticColorOption.tsx index 163c6ca4df78..00e31282e00a 100644 --- a/src/components/Themes/hooks/useThemeSemanticColorOption.tsx +++ b/src/components/Themes/hooks/useThemeSemanticColorOption.tsx @@ -20,21 +20,28 @@ import { isUtilityColorToken, parseInternalUtilityColorReference, } from '@gravity-ui/uikit-themer/dist/utils'; +import {useTranslation} from 'next-i18next'; import {useMemo} from 'react'; import {DEFAULT_ADVANCED_COLORS} from '../lib/constants'; import type {AdvancedColorType} from '../lib/types'; -import type {BaseColor} from '../ui/PrivateColorSelect/types'; +import type {BaseColor} from '../ui/GravityColorSelect/types'; import {useThemeCreator} from './useThemeCreator'; +export type SemanticColorGroupItem = BaseColor & { + name?: string; + ref?: string; + disabled?: boolean; +}; + export type SemanticColorGroup = { icon: React.ReactNode; key: string; title: string; groups: { title: string; - items: (BaseColor & {name?: string; ref?: string})[]; + items: SemanticColorGroupItem[]; }[]; }; @@ -55,27 +62,46 @@ const getIconByGroup = (group: Exclude<AdvancedColorType, 'basic-palette'>) => { } }; -const resolveUtilityColor = (state: GravityTheme['utilityColors'], themeVariant: Theme) => { - const traverse = (colorObject: ColorOptions): string => { +const resolveUtilityColor = ( + state: GravityTheme['utilityColors'], + themeVariant: Theme, + updatedColorToken: string, +) => { + let disabled = false; + console.log('updatedColorToken', updatedColorToken); + + const traverse = ( + colorObject: ColorOptions & {token?: string}, + ): {color: string; disabled: boolean} => { + if (colorObject.token === updatedColorToken) { + disabled = true; + } + if (colorObject.ref && isInternalUtilityColorReference(colorObject.ref)) { const nextUtilityColorToken = parseInternalUtilityColorReference(colorObject.ref); + if (colorObject.ref === updatedColorToken) { + disabled = true; + } + if (nextUtilityColorToken) { const nextUtilityColor = state[nextUtilityColorToken]; return traverse(nextUtilityColor[themeVariant]); } - - return colorObject.value; } - return colorObject.value; + return {color: colorObject.value, disabled}; }; return traverse; }; -export const useThemeSemanticColorOption = (themeVariant: Theme): SemanticColorGroup[] => { +export const useThemeSemanticColorOption = ( + themeVariant: Theme, + updatedColorToken: string, +): SemanticColorGroup[] => { + const {t} = useTranslation('themes'); const themeState = useThemeCreator(); const {gravityTheme} = themeState; @@ -88,42 +114,51 @@ export const useThemeSemanticColorOption = (themeVariant: Theme): SemanticColorG icon: getIconByGroup( advanceColorGroupName as Exclude<AdvancedColorType, 'basic-palette'>, ), - title: advanceColorGroupName, + title: t(`title_advance-color-settings-${advanceColorGroupName}`), groups: Object.entries(advanceColorSubGroups).map( ([advanceColorSubGroupName, advanceColorSubGroupItems]) => { return { - title: advanceColorSubGroupName, + title: t( + `title_advance-color-settings-group-${advanceColorSubGroupName}`, + ), items: advanceColorSubGroupItems.map(({colorName}) => { const isUtilityColor = isUtilityColorToken(colorName); - const colorObject = isUtilityColor - ? gravityTheme.utilityColors[colorName as UtilityColor][ - themeVariant - ] - : gravityTheme.baseColors[colorName]?.[themeVariant]; - - const resolvedValue = isUtilityColor - ? resolveUtilityColor( - gravityTheme.utilityColors, - themeVariant, - )( - gravityTheme.utilityColors[colorName as UtilityColor][ - themeVariant - ], - ) - : colorObject.value; - - return { - name: colorName, - title: isUtilityColor - ? createUtilityColorCssVariable(colorName) - : colorName, - color: resolvedValue, - ref: colorObject.ref, - token: isUtilityColor - ? createInternalUtilityColorReference(colorName) - : colorName, - }; + if (isUtilityColor) { + const colorObject = + gravityTheme.utilityColors[colorName as UtilityColor][ + themeVariant + ]; + const token = + createInternalUtilityColorReference(colorName); + + const {color, disabled} = resolveUtilityColor( + gravityTheme.utilityColors, + themeVariant, + updatedColorToken, + )({...colorObject, token}); + + return { + color, + disabled, + token, + name: colorName, + title: createUtilityColorCssVariable(colorName), + ref: colorObject.ref, + }; + } else { + const colorObject = + gravityTheme.baseColors[colorName]?.[themeVariant]; + + return { + name: colorName, + title: colorName, + color: colorObject.value, + ref: colorObject.ref, + disabled: false, + token: colorName, + }; + } }), }; }, diff --git a/src/components/Themes/hooks/useThemeUtilityColor.ts b/src/components/Themes/hooks/useThemeUtilityColor.ts index 49dcce08faca..90a6879ec6ca 100644 --- a/src/components/Themes/hooks/useThemeUtilityColor.ts +++ b/src/components/Themes/hooks/useThemeUtilityColor.ts @@ -25,11 +25,12 @@ export const useThemeUtilityColor = ({name, theme, withoutRef}: UseThemeColorPar }, [themeState, name, theme]); const updateValue = React.useCallback( - (newValue: string) => { + (newValue: string, newRef?: string) => { changeUtilityColor({ themeVariant: theme, name, value: newValue, + ref: newRef, }); }, [name, theme, changeUtilityColor], diff --git a/src/components/Themes/lib/constants.ts b/src/components/Themes/lib/constants.ts index 721c6bff7ba2..145a6bb96036 100644 --- a/src/components/Themes/lib/constants.ts +++ b/src/components/Themes/lib/constants.ts @@ -1,4 +1,4 @@ -import type {BordersOptions, GravityTheme, Theme, UtilityColor} from '@gravity-ui/uikit-themer'; +import type {BordersOptions, GravityTheme, Theme} from '@gravity-ui/uikit-themer'; import { DEFAULT_THEME as DEFAULT_GRAVITY_THEME, createInternalPrivateColorReference, @@ -9,7 +9,6 @@ import {DEFAULT_FONT_FAMILY_SETTINGS} from './typography/constants'; import {getDefaultAdvancedColorValue} from './utils'; export const THEME_BORDER_RADIUS_VARIABLE_PREFIX = '--g-border-radius'; -export const PRIVATE_COLOR_PREFIX = '--g-color-private-'; export const UTILITY_COLOR_PREFIX = '--g-color-'; export const DEFAULT_NEW_COLOR_TITLE = 'New color'; @@ -280,151 +279,6 @@ export const DEFAULT_ADVANCED_COLORS: AdvanceColors = { }, }; -//TODO: add translates - -export const UTILITY_COLOR_HELP_CONTENT: Record<UtilityColor, string> = { - // text - 'text-primary': 'Primary text on the page. It is default for headers, paragraphs, buttons.', - 'text-complementary': 'Complementary text on the page. Controls, notes, etc.', - 'text-secondary': - 'Secondary text on the page. Captions, definitions, nonessential information.', - 'text-hint': 'Control hint.', - 'text-info': 'Info text.', - 'text-info-heavy': 'Info text with underlay.', - 'text-positive': 'Positive text.', - 'text-positive-heavy': 'Positive text with underlay.', - 'text-warning': 'Warning text.', - 'text-warning-heavy': 'Warning text with underlay.', - 'text-danger': 'Danger text.', - 'text-danger-heavy': 'Danger text with underlay.', - 'text-utility': 'For emphasizing, without semantic.', - 'text-utility-heavy': 'Utility text with underlay.', - 'text-misc': 'For emphasizing, without semantic.', - 'text-misc-heavy': 'Misc text with underlay.', - 'text-brand': 'Brand text.', - 'text-brand-heavy': 'Brand text with underlay.', - 'text-brand-contrast': 'Brand text with high contrast.', - 'text-link': 'Links.', - 'text-link-hover': 'Hover for Link.', - 'text-link-visited': 'Visited Link.', - 'text-link-visited-hover': 'Hover for Visited Link.', - 'text-dark-primary': 'Primary text over light background.', - 'text-dark-complementary': 'Complementary text over light background.', - 'text-dark-secondary': 'Secondary text over light background.', - 'text-dark-hint': 'Minimal contrast.', - 'text-light-primary': 'Primary text over dark background.', - 'text-light-complementary': 'Complementary text over dark background.', - 'text-light-secondary': 'Secondary text over dark background.', - 'text-light-hint': 'Minimal contrast.', - 'text-inverted-primary': 'Primary text.', - 'text-inverted-complementary': 'Complementary text.', - 'text-inverted-secondary': 'Secondary text.', - 'text-inverted-hint': 'Minimal contrast.', - - // backgrounds - 'base-background': "Page's background.", - 'base-generic': 'Generic gray base, buttons and other objects.', - 'base-generic-hover': 'Hover for Generic.', - 'base-generic-medium': 'Neutral blocks with medium contrast.', - 'base-generic-medium-hover': 'Hover for Generic Medium.', - 'base-generic-accent': 'Background for controls (checkbox, radio, etc.).', - 'base-generic-accent-disabled': 'Disabled background for controls.', - 'base-generic-ultralight': 'Background with minimal contrast. Not recommended to use.', - 'base-simple-hover': 'Hover for transparent objects (works over light backgrounds).', - 'base-simple-hover-solid': 'Hover for transparent objects (works over light backgrounds).', - 'base-brand': 'Background for accented object.', - 'base-brand-hover': 'Hover for Brand.', - 'base-selection': 'Highlight selected objects in menus, calendars, etc.', - 'base-selection-hover': 'Hover for Selection.', - 'base-info-light': 'Info semantic background.', - 'base-info-light-hover': 'Hover for Info.', - 'base-positive-light': 'Positive semantic background.', - 'base-positive-light-hover': 'Hover for Positive.', - 'base-warning-light': 'Warning semantic background.', - 'base-warning-light-hover': 'Hover for Warning.', - 'base-danger-light': 'Negative semantic background.', - 'base-danger-light-hover': 'Hover for Danger.', - 'base-utility-light': 'Utility semantic background.', - 'base-utility-light-hover': 'Hover for Utility.', - 'base-misc-light': 'Uncategorized semantic background.', - 'base-misc-light-hover': 'Hover for Misc.', - 'base-neutral-light': 'Neutral semantic background.', - 'base-neutral-light-hover': 'Hover for Neutral.', - 'base-info-medium': 'Info semantic background, medium accent.', - 'base-info-medium-hover': 'Hover for Info Medium.', - 'base-positive-medium': 'Positive semantic background, medium accent.', - 'base-positive-medium-hover': 'Hover for Positive Medium.', - 'base-warning-medium': 'Warning semantic background, medium accent.', - 'base-warning-medium-hover': 'Hover for Warning Medium.', - 'base-danger-medium': 'Danger semantic background, medium accent.', - 'base-danger-medium-hover': 'Hover for Danger Medium.', - 'base-utility-medium': 'Utility semantic background, medium accent.', - 'base-utility-medium-hover': 'Hover for Utility Medium.', - 'base-misc-medium': 'Uncategorized semantic background, medium accent.', - 'base-misc-medium-hover': 'Hover for Misc Medium.', - 'base-neutral-medium': 'Neutral semantic background, medium accent.', - 'base-neutral-medium-hover': 'Hover for Neutral Medium.', - 'base-info-heavy': 'Info semantic background, strong accent.', - 'base-info-heavy-hover': 'Hover for Info Heavy.', - 'base-positive-heavy': 'Positive semantic background, strong accent.', - 'base-positive-heavy-hover': 'Hover for Positive Heavy.', - 'base-warning-heavy': 'Warning semantic background, strong accent.', - 'base-warning-heavy-hover': 'Hover for Warning Heavy.', - 'base-danger-heavy': 'Negative semantic background, strong accent.', - 'base-danger-heavy-hover': 'Hover for Danger Heavy', - 'base-utility-heavy': 'Utility semantic background, strong accent.', - 'base-utility-heavy-hover': 'Hover for Utility Heavy.', - 'base-misc-heavy': 'Uncategorized semantic background, strong accent.', - 'base-misc-heavy-hover': 'Hover for Misc Heavy.', - 'base-neutral-heavy': 'Neutral semantic background, strong accent.', - 'base-neutral-heavy-hover': 'Hover for Neutral Heavy.', - 'base-light': 'Background on top of another darker background.', - 'base-light-hover': 'Hover for Light.', - 'base-light-simple-hover': 'Hover for transparent objects (works over dark backgrounds).', - 'base-light-disabled': 'Disabled controls.', - 'base-light-accent-disabled': 'Disabled active controls.', - 'base-float': 'Raised layers background.', - 'base-float-hover': 'Hover for Float.', - 'base-float-medium': 'Float for medium contrast.', - 'base-float-heavy': 'Float for strong contrast.', - 'base-float-accent': 'Raised controls.', - 'base-float-accent-hover': 'Hover for Float Accent.', - 'base-float-announcement': 'Float background for announcements.', - 'base-modal': 'Floating components with a veil.', - - // lines - 'line-generic': 'Button borders, dividers, basic block borders. Almost all lines.', - 'line-generic-hover': 'Hover for Generic.', - 'line-generic-active': 'Active state for Generic.', - 'line-generic-accent': 'Control borders.', - 'line-generic-accent-hover': 'Hover for Generic Accent.', - 'line-generic-solid': 'Generic without transparency (to avoid collision artefacts).', - 'line-brand': 'Brand blocks', - 'line-focus': 'Focused blocks', - 'line-info': 'Info blocks.', - 'line-positive': 'Positive blocks.', - 'line-warning': 'Warning blocks.', - 'line-danger': 'Danger blocks. Blocks with negative context.', - 'line-utility': 'Utility blocks.', - 'line-misc': 'Uncategorized blocks.', - 'line-light': 'Dividers and borders over dark background.', - - // effects - 'sfx-veil': 'Popup backdrop.', - 'sfx-shadow': 'Shadow for everything that might have it.', - 'sfx-shadow-light': 'Lighter version of shadow.', - 'sfx-shadow-heavy': 'Heavy shadows. DEPRECATED.', - 'sfx-fade': 'Enlighten while loading.', - - // misc - 'scroll-track': 'Scroll background.', - 'scroll-handle': 'The handle to move the scroll.', - 'scroll-handle-hover': 'Hover state for scroll handle.', - 'scroll-corner': 'A corner where horizontal and vertical scrolls meet.', - 'infographics-axis': 'Graph axis', - 'infographics-tooltip-bg': 'Main background for tooltips.', -}; - // Default colors mappings (values from gravity-ui styles) // https://github.com/gravity-ui/uikit/tree/main/styles/themes export const DEFAULT_COLORS: GravityTheme['utilityColors'] = { diff --git a/src/components/Themes/lib/themeCreatorUtils.ts b/src/components/Themes/lib/themeCreatorUtils.ts index 08f8ea336204..af8625f3a54c 100644 --- a/src/components/Themes/lib/themeCreatorUtils.ts +++ b/src/components/Themes/lib/themeCreatorUtils.ts @@ -274,17 +274,19 @@ export type ChangeUtilityColorInThemeParams = { themeVariant: Theme; name: UtilityColor; value: string; + ref?: string; }; export function changeUtilityColorInTheme( themeState: ThemeCreatorState, - {themeVariant, name, value}: ChangeUtilityColorInThemeParams, + {themeVariant, name, value, ref}: ChangeUtilityColorInThemeParams, ): ThemeCreatorState { const updatedGravityTheme = updateUtilityColor({ theme: themeState.gravityTheme, themeVariant, colorToken: name, value, + ref, }); return {...themeState, gravityTheme: updatedGravityTheme}; diff --git a/src/components/Themes/lib/utils.ts b/src/components/Themes/lib/utils.ts index 91aa1d49cc13..a98d22b812ac 100644 --- a/src/components/Themes/lib/utils.ts +++ b/src/components/Themes/lib/utils.ts @@ -1,7 +1,8 @@ import {DEFAULT_THEME as DEFAULT_GRAVITY_THEME, type UtilityColor} from '@gravity-ui/uikit-themer'; import {isUtilityColorToken} from '@gravity-ui/uikit-themer/dist/utils'; +import capitalize from 'lodash/capitalize'; -import {PRIVATE_COLOR_PREFIX, UTILITY_COLOR_PREFIX} from './constants'; +import {UTILITY_COLOR_PREFIX} from './constants'; export const getDefaultAdvancedColorValue = (colorName: UtilityColor) => { return {colorName: colorName, ...DEFAULT_GRAVITY_THEME['utilityColors'][colorName]}; @@ -12,5 +13,13 @@ export const getColorPrefix = (colorToken: string) => { return UTILITY_COLOR_PREFIX; } - return PRIVATE_COLOR_PREFIX; + return ''; +}; + +export const getColorName = (colorToken: string) => { + if (DEFAULT_GRAVITY_THEME['baseColors'][colorToken]) { + return capitalize(colorToken); + } + + return colorToken; }; diff --git a/src/components/Themes/ui/AdvancedSettingsTable/AddExtraColor/AddExtraColor.tsx b/src/components/Themes/ui/AdvancedSettingsTable/AddExtraColor/AddExtraColor.tsx index da2d522bbe8f..381b916a6311 100644 --- a/src/components/Themes/ui/AdvancedSettingsTable/AddExtraColor/AddExtraColor.tsx +++ b/src/components/Themes/ui/AdvancedSettingsTable/AddExtraColor/AddExtraColor.tsx @@ -1,6 +1,6 @@ import {Plus} from '@gravity-ui/icons'; import {Button, Icon} from '@gravity-ui/uikit'; -// import {useTranslation} from 'next-i18next'; +import {useTranslation} from 'next-i18next'; import {useCallback} from 'react'; import {block} from '../../../../../utils'; @@ -11,7 +11,7 @@ import './AddExtraColor.scss'; const b = block('add-extra-color'); export const AddExtraColor = () => { - // const {t} = useTranslation('themes'); + const {t} = useTranslation('themes'); const {addColor} = useThemeCreatorMethods(); @@ -28,7 +28,7 @@ export const AddExtraColor = () => { <div className={b()}> <Button view="flat" size="l" className={b('button')} onClick={handleAddColor}> <Icon data={Plus} size={12} /> - Add Extra Color + {t('action_add-extra-color')} </Button> </div> ); diff --git a/src/components/Themes/ui/AdvancedSettingsTable/AdvancedSettingsTable.tsx b/src/components/Themes/ui/AdvancedSettingsTable/AdvancedSettingsTable.tsx index ede23a3b4e6e..dfc3c8b7df34 100644 --- a/src/components/Themes/ui/AdvancedSettingsTable/AdvancedSettingsTable.tsx +++ b/src/components/Themes/ui/AdvancedSettingsTable/AdvancedSettingsTable.tsx @@ -1,31 +1,13 @@ -import {Moon, Sun} from '@gravity-ui/icons'; -import {BREAKPOINTS, useWindowBreakpoint} from '@gravity-ui/page-constructor'; -import {Flex, HelpMark, Icon, SegmentedRadioGroup, Text} from '@gravity-ui/uikit'; -import type {BaseColors, Theme, UtilityColor} from '@gravity-ui/uikit-themer'; -import { - createUtilityColorCssVariable, - isUtilityColorToken, -} from '@gravity-ui/uikit-themer/dist/utils'; -import {Fragment, type ReactElement, useMemo, useState} from 'react'; +import {useTranslation} from 'next-i18next'; +import {Fragment} from 'react'; import {block} from '../../../../utils'; -import { - useThemeCreator, - useThemePaletteColor, - useThemePrivateColorOptions, - useThemeUtilityColor, -} from '../../hooks'; -import {useThemeSemanticColorOption} from '../../hooks/useThemeSemanticColorOption'; -import {DEFAULT_ADVANCED_COLORS, UTILITY_COLOR_HELP_CONTENT} from '../../lib/constants'; -import {isManuallyCreatedPaletteToken} from '../../lib/themeCreatorUtils'; +import {DEFAULT_ADVANCED_COLORS} from '../../lib/constants'; import type {AdvancedColorType} from '../../lib/types'; -import {getColorPrefix} from '../../lib/utils'; -import {ColorPickerInput} from '../ColorPickerInput/ColorPickerInput'; -import {PrivateColorSelect} from '../PrivateColorSelect'; import {AddExtraColor} from './AddExtraColor/AddExtraColor'; import './AdvancedSettingsTable.scss'; -import {ExtraColorName} from './ExtraColorName/ExtraColorName'; +import {useColumns, useExtraColors} from './hooks'; const b = block('advanced-color-settings-table'); @@ -33,210 +15,10 @@ export interface AdvancedSettingsTableProps { colorType: AdvancedColorType; } -type ValueCellProps = {colorName: string; theme: Theme; value?: string}; - -type Column = { - title: () => ReactElement; - key: string; - render: (props: { - colorName: string; - light?: string; - dark?: string; - extraVariable?: boolean; - }) => ReactElement; -}; - -const getContentForUtilityVariable = (value: UtilityColor) => { - const content = UTILITY_COLOR_HELP_CONTENT[value]; - - return ( - <Flex direction="column" gap={1} justifyContent="center"> - <Text variant="body-1" color="secondary"> - {createUtilityColorCssVariable(value)} - </Text> - <Text variant="body-1" color="primary"> - {content} - </Text> - </Flex> - ); -}; - -const VariableCell = ({name, extraVariable}: {name: string; extraVariable?: boolean}) => { - if (extraVariable) { - return <ExtraColorName token={name} />; - } - - const isUtilityColor = isUtilityColorToken(name as UtilityColor); - - return ( - <Flex justifyContent="space-between" gap={2}> - <Text variant="body-1" color="secondary"> - {getColorPrefix(name)} - <Text variant="body-1" color="primary"> - {name} - </Text> - </Text> - {isUtilityColor && ( - <HelpMark iconSize="m" popoverProps={{placement: 'top-start'}}> - {getContentForUtilityVariable(name as UtilityColor)} - </HelpMark> - )} - </Flex> - ); -}; - -const UtilityThemeValueCell = ({ - colorName, - theme, - value, -}: Omit<ValueCellProps, 'colorName'> & {colorName: UtilityColor}) => { - const [color, setColor] = useThemeUtilityColor({ - name: colorName, - theme, - }); - - const themePrivateColorOptions = useThemePrivateColorOptions(theme); - const themeSemanticColorOptions = useThemeSemanticColorOption(theme); - - console.log('themeSemanticColorOptions', themeSemanticColorOptions); - console.log('themePrivateColorOptions', themePrivateColorOptions); - - if (colorName === 'base-background') { - return ( - <ColorPickerInput - value={color} - defaultValue={value ?? '#000000'} - onChange={setColor} - view="clear" - /> - ); - } - - return ( - <PrivateColorSelect - privateGroups={themePrivateColorOptions} - semanticGroups={themeSemanticColorOptions} - defaultValue={value ?? '#000000'} - value={color} - onChange={setColor} - inputView="clear" - /> - ); -}; - -const PaletteThemeValueCell = ({colorName, theme, value}: ValueCellProps) => { - const [color, setColor] = useThemePaletteColor({ - token: colorName, - theme, - }); - - return ( - <ColorPickerInput - value={color.value} - defaultValue={value ?? '#000000'} - onChange={setColor} - view="clear" - /> - ); -}; - -const ThemeValueCell = ({colorName, theme, value}: ValueCellProps) => { - const isUtilityColor = isUtilityColorToken(colorName); - - if (isUtilityColor) { - return <UtilityThemeValueCell colorName={colorName} theme={theme} value={value} />; - } - - return <PaletteThemeValueCell colorName={colorName} theme={theme} value={value} />; -}; - -const TitleCell = ({value}: {value: string}) => { - return <Text variant="header-1">{value}</Text>; -}; - export const AdvancedSettingsTable = ({colorType}: AdvancedSettingsTableProps) => { - const state = useThemeCreator(); - const {gravityTheme} = state; - - console.log('gravityTheme', gravityTheme); - - //todo: move to hook - const extraColors = useMemo(() => { - return Object.entries(state.gravityTheme.baseColors) - .filter(([key]) => isManuallyCreatedPaletteToken(key)) - .reduce<BaseColors>((acc, [key, value]) => { - acc[key] = value; - return acc; - }, {}); - }, [state]); - - const breakpoint = useWindowBreakpoint(); - const [theme, toggleTheme] = useState<Theme>('light'); - - //TODO: move to hook - const columns = useMemo((): Column[] => { - const isTablet = breakpoint < BREAKPOINTS.lg; - - const variableColumn: Column = { - title: () => <TitleCell value="Variable" />, - key: 'variable', - render: ({colorName, extraVariable = false}) => ( - <VariableCell name={colorName} extraVariable={extraVariable} /> - ), - }; - - if (isTablet) { - return [ - variableColumn, - { - title: () => ( - <SegmentedRadioGroup - size="xl" - defaultValue={theme} - onChange={(e) => { - toggleTheme(e.target.value as Theme); - }} - > - <SegmentedRadioGroup.Option value="light"> - <Icon data={Sun} /> - Light - </SegmentedRadioGroup.Option> - <SegmentedRadioGroup.Option value="dark"> - <Icon data={Moon} /> - Dark - </SegmentedRadioGroup.Option> - </SegmentedRadioGroup> - ), - key: 'themeToggle', - render: ({colorName, light, dark}) => ( - <ThemeValueCell - theme={theme} - colorName={colorName} - value={theme === 'light' ? light : dark} - /> - ), - }, - ]; - } - - return [ - variableColumn, - { - title: () => <TitleCell value="Light theme value" />, - key: 'light', - render: ({colorName, light}) => ( - <ThemeValueCell theme="light" colorName={colorName} value={light} /> - ), - }, - { - title: () => <TitleCell value="Dark theme value" />, - key: 'dark', - render: ({colorName, dark}) => ( - <ThemeValueCell theme="dark" colorName={colorName} value={dark} /> - ), - }, - ]; - }, [breakpoint, theme]); + const {t} = useTranslation('themes'); + const extraColors = useExtraColors(); + const columns = useColumns(); return ( <table className={b()}> @@ -252,14 +34,16 @@ export const AdvancedSettingsTable = ({colorType}: AdvancedSettingsTableProps) = <tbody className={b('body')}> {Object.entries(DEFAULT_ADVANCED_COLORS[colorType]).map(([group, variables]) => { return ( - <Fragment> + <Fragment key={group}> <tr className={b('row')}> {columns.map(({key}) => ( <td className={b('cell', {group: true})} key={`${group}-${key}`} > - {key === 'variable' ? group : ''} + {key === 'variable' + ? t(`title_advance-color-settings-group-${group}`) + : ''} </td> ))} </tr> @@ -267,7 +51,7 @@ export const AdvancedSettingsTable = ({colorType}: AdvancedSettingsTableProps) = {colorType === 'basic-palette' && group === 'extra-color' && ( <Fragment> {Object.entries(extraColors).map(([colorName, value]) => ( - <tr className={b('row')}> + <tr className={b('row')} key={colorName}> {columns.map(({render: Render, key}) => ( <td className={b('cell')} @@ -294,7 +78,7 @@ export const AdvancedSettingsTable = ({colorType}: AdvancedSettingsTableProps) = )} {variables.map(({colorName, light, dark}) => ( - <tr className={b('row')}> + <tr className={b('row')} key={colorName}> {columns.map(({render: Render, key}) => ( <td className={b('cell')} key={`${colorName}-${key}`}> <Render diff --git a/src/components/Themes/ui/AdvancedSettingsTable/ExtraColorName/ExtraColorName.tsx b/src/components/Themes/ui/AdvancedSettingsTable/ExtraColorName/ExtraColorName.tsx index 0e95b550a9bc..5636fe0e5302 100644 --- a/src/components/Themes/ui/AdvancedSettingsTable/ExtraColorName/ExtraColorName.tsx +++ b/src/components/Themes/ui/AdvancedSettingsTable/ExtraColorName/ExtraColorName.tsx @@ -4,7 +4,6 @@ import {useCallback, useRef, useState} from 'react'; import {block} from '../../../../../utils'; import {useThemeCreatorMethods} from '../../../hooks'; -import {PRIVATE_COLOR_PREFIX} from '../../../lib/constants'; import {createColorToken, createTitleFromToken} from '../../../lib/themeCreatorUtils'; import './ExtraColorName.scss'; @@ -43,11 +42,8 @@ export const ExtraColorName = ({token}: ExtraColorNameProps) => { return ( <Flex gap={2} justifyContent="space-between" alignItems="center"> {mode === 'view' ? ( - <Text variant="body-1" color="secondary"> - {PRIVATE_COLOR_PREFIX} - <Text variant="body-1" color="primary"> - {token} - </Text> + <Text variant="body-1" color="primary"> + {token} </Text> ) : ( <TextInput diff --git a/src/components/Themes/ui/AdvancedSettingsTable/columns.tsx b/src/components/Themes/ui/AdvancedSettingsTable/columns.tsx new file mode 100644 index 000000000000..d93a1c1becb0 --- /dev/null +++ b/src/components/Themes/ui/AdvancedSettingsTable/columns.tsx @@ -0,0 +1,130 @@ +import {Flex, HelpMark, Text} from '@gravity-ui/uikit'; +import { + type Theme, + type UtilityColor, + createUtilityColorCssVariable, +} from '@gravity-ui/uikit-themer'; +import { + createInternalUtilityColorReference, + isUtilityColorToken, +} from '@gravity-ui/uikit-themer/dist/utils'; +import {useTranslation} from 'next-i18next'; + +import {useThemePaletteColor, useThemePrivateColorOptions, useThemeUtilityColor} from '../../hooks'; +import {useThemeSemanticColorOption} from '../../hooks/useThemeSemanticColorOption'; +import {getColorName, getColorPrefix} from '../../lib/utils'; +import {ColorPickerInput} from '../ColorPickerInput/ColorPickerInput'; +import {GravityColorSelect} from '../GravityColorSelect'; + +import {ExtraColorName} from './ExtraColorName/ExtraColorName'; + +type ColumnProps = {colorName: string; theme: Theme; value?: string}; + +const PaletteThemeValueColumn = ({colorName, theme, value}: ColumnProps) => { + const [color, setColor] = useThemePaletteColor({ + token: colorName, + theme, + }); + + return ( + <ColorPickerInput + value={color.value} + defaultValue={value ?? '#000000'} + onChange={setColor} + view="clear" + /> + ); +}; + +const UtilityThemeValueColumn = ({ + colorName, + theme, + value, +}: Omit<ColumnProps, 'colorName'> & {colorName: UtilityColor}) => { + const [color, setColor] = useThemeUtilityColor({ + name: colorName, + theme, + }); + + const themePrivateColorOptions = useThemePrivateColorOptions(theme); + const themeSemanticColorOptions = useThemeSemanticColorOption( + theme, + createInternalUtilityColorReference(colorName), + ); + + if (colorName === 'base-background') { + return ( + <ColorPickerInput + value={color} + defaultValue={value ?? '#000000'} + onChange={setColor} + view="clear" + /> + ); + } + + return ( + <GravityColorSelect + privateGroups={themePrivateColorOptions} + semanticGroups={themeSemanticColorOptions} + defaultValue={value ?? '#000000'} + value={color} + onChange={setColor} + inputView="clear" + /> + ); +}; + +export const ThemeValueColumn = ({colorName, theme, value}: ColumnProps) => { + const isUtilityColor = isUtilityColorToken(colorName); + + if (isUtilityColor) { + return <UtilityThemeValueColumn colorName={colorName} theme={theme} value={value} />; + } + + return <PaletteThemeValueColumn colorName={colorName} theme={theme} value={value} />; +}; + +export const TitleColumn = ({value}: {value: string}) => { + return <Text variant="header-1">{value}</Text>; +}; + +const UtilityVariableDescription = ({name}: {name: UtilityColor}) => { + const {t} = useTranslation('themes'); + const content = t(`text_utility-color_${name}_description`); + + return ( + <Flex direction="column" gap={1} justifyContent="center"> + <Text variant="body-1" color="secondary"> + {createUtilityColorCssVariable(name)} + </Text> + <Text variant="body-1" color="primary"> + {content} + </Text> + </Flex> + ); +}; + +export const VariableColumn = ({name, extraVariable}: {name: string; extraVariable?: boolean}) => { + if (extraVariable) { + return <ExtraColorName token={name} />; + } + + const isUtilityColor = isUtilityColorToken(name as UtilityColor); + + return ( + <Flex justifyContent="space-between" gap={2}> + <Text variant="body-1" color="secondary"> + {getColorPrefix(name)} + <Text variant="body-1" color="primary"> + {getColorName(name)} + </Text> + </Text> + {isUtilityColor && ( + <HelpMark iconSize="m" popoverProps={{placement: 'top-start'}}> + <UtilityVariableDescription name={name as UtilityColor} /> + </HelpMark> + )} + </Flex> + ); +}; diff --git a/src/components/Themes/ui/AdvancedSettingsTable/hooks.tsx b/src/components/Themes/ui/AdvancedSettingsTable/hooks.tsx new file mode 100644 index 000000000000..d0ef476f4bc3 --- /dev/null +++ b/src/components/Themes/ui/AdvancedSettingsTable/hooks.tsx @@ -0,0 +1,112 @@ +import {Moon, Sun} from '@gravity-ui/icons'; +import {BREAKPOINTS, useWindowBreakpoint} from '@gravity-ui/page-constructor'; +import {Icon, SegmentedRadioGroup} from '@gravity-ui/uikit'; +import type {BaseColors, Theme} from '@gravity-ui/uikit-themer'; +import {useTranslation} from 'next-i18next'; +import {type ReactElement, useMemo, useState} from 'react'; + +import {useThemeCreator} from '../../hooks'; +import {isManuallyCreatedPaletteToken} from '../../lib/themeCreatorUtils'; + +import {ThemeValueColumn, TitleColumn, VariableColumn} from './columns'; + +export const useExtraColors = () => { + const {gravityTheme} = useThemeCreator(); + + return useMemo(() => { + return Object.entries(gravityTheme.baseColors) + .filter(([key]) => isManuallyCreatedPaletteToken(key)) + .reduce<BaseColors>((acc, [key, value]) => { + acc[key] = value; + return acc; + }, {}); + }, [gravityTheme]); +}; + +type Column = { + title: () => ReactElement; + key: string; + render: (props: { + colorName: string; + light?: string; + dark?: string; + extraVariable?: boolean; + }) => ReactElement; +}; + +export const useColumns = (): Column[] => { + const [theme, toggleTheme] = useState<Theme>('light'); + const {t} = useTranslation('themes'); + + const breakpoint = useWindowBreakpoint(); + const isTablet = breakpoint < BREAKPOINTS.lg; + + const variableColumn: Column = useMemo( + () => ({ + title: () => <TitleColumn value={t('title_advance-settings-table_title-variable')} />, + key: 'variable', + render: ({colorName, extraVariable = false}) => ( + <VariableColumn name={colorName} extraVariable={extraVariable} /> + ), + }), + [t], + ); + + if (isTablet) { + return useMemo( + () => [ + variableColumn, + { + title: () => ( + <SegmentedRadioGroup + size="xl" + defaultValue={theme} + onChange={(e) => { + toggleTheme(e.target.value as Theme); + }} + > + <SegmentedRadioGroup.Option value="light"> + <Icon data={Sun} /> + {t('theme_name_light')} + </SegmentedRadioGroup.Option> + <SegmentedRadioGroup.Option value="dark"> + <Icon data={Moon} /> + {t('theme_name_dark')} + </SegmentedRadioGroup.Option> + </SegmentedRadioGroup> + ), + key: 'themeToggle', + render: ({colorName, light, dark}) => ( + <ThemeValueColumn + theme={theme} + colorName={colorName} + value={theme === 'light' ? light : dark} + /> + ), + }, + ], + [theme, t], + ); + } + + return useMemo( + () => [ + variableColumn, + { + title: () => <TitleColumn value={t('title_advance-settings-table_title-light')} />, + key: 'light', + render: ({colorName, light}) => ( + <ThemeValueColumn theme="light" colorName={colorName} value={light} /> + ), + }, + { + title: () => <TitleColumn value={t('title_advance-settings-table_title-dark')} />, + key: 'dark', + render: ({colorName, dark}) => ( + <ThemeValueColumn theme="dark" colorName={colorName} value={dark} /> + ), + }, + ], + [t], + ); +}; diff --git a/src/components/Themes/ui/ColorsTab/AdvancedSettings/AdvancedSettings.tsx b/src/components/Themes/ui/ColorsTab/AdvancedSettings/AdvancedSettings.tsx index 361f2d35ae60..63c8a5f45c82 100644 --- a/src/components/Themes/ui/ColorsTab/AdvancedSettings/AdvancedSettings.tsx +++ b/src/components/Themes/ui/ColorsTab/AdvancedSettings/AdvancedSettings.tsx @@ -19,31 +19,31 @@ export const AdvancedSettings = () => { () => [ { value: 'basic-palette', - title: 'Basic palette', + title: t('title_advance-color-settings-basic-palette'), }, { value: 'brand-summary', - title: 'Brand summary', + title: t('title_advance-color-settings-brand-summary'), }, { value: 'texts', - title: 'Texts', + title: t('title_advance-color-settings-texts'), }, { value: 'backgrounds', - title: 'Backgrounds', + title: t('title_advance-color-settings-backgrounds'), }, { value: 'lines', - title: 'Lines', + title: t('title_advance-color-settings-lines'), }, { value: 'effects', - title: 'Effects', + title: t('title_advance-color-settings-effects'), }, { value: 'misc', - title: 'Misc', + title: t('title_advance-color-settings-misc'), }, ], [t], @@ -55,7 +55,7 @@ export const AdvancedSettings = () => { titleActions={ <Button size="xl" className={b('learn-more-button')} disabled> <Icon data={CircleQuestion} /> - Learn More + {t('action_learn-more')} </Button> } > diff --git a/src/components/Themes/ui/PrivateColorSelect/PrivateColorSelectPopupContent.scss b/src/components/Themes/ui/GravityColorSelect/ColorSelectPopupContent.scss similarity index 91% rename from src/components/Themes/ui/PrivateColorSelect/PrivateColorSelectPopupContent.scss rename to src/components/Themes/ui/GravityColorSelect/ColorSelectPopupContent.scss index ad192fbcbd16..bb098ded9744 100644 --- a/src/components/Themes/ui/PrivateColorSelect/PrivateColorSelectPopupContent.scss +++ b/src/components/Themes/ui/GravityColorSelect/ColorSelectPopupContent.scss @@ -61,6 +61,11 @@ $block: '.#{variables.$ns}private-colors-select-popup'; align-items: center; gap: var(--g-spacing-1); padding: 3px var(--g-spacing-2); + + &_disabled { + opacity: 0.5; + cursor: not-allowed; + } } &__colors-select { @@ -68,4 +73,8 @@ $block: '.#{variables.$ns}private-colors-select-popup'; --g-border-radius-xl: 8px; padding-bottom: var(--g-spacing-3); } + + &__divider { + margin: var(--g-spacing-2) 0; + } } diff --git a/src/components/Themes/ui/GravityColorSelect/ColorSelectPopupContent.tsx b/src/components/Themes/ui/GravityColorSelect/ColorSelectPopupContent.tsx new file mode 100644 index 000000000000..187e29558652 --- /dev/null +++ b/src/components/Themes/ui/GravityColorSelect/ColorSelectPopupContent.tsx @@ -0,0 +1,137 @@ +import {Divider, Text} from '@gravity-ui/uikit'; +import { + type UtilityColor, + isInternalUtilityColorReference, + parseInternalPrivateColorReference, +} from '@gravity-ui/uikit-themer'; +import {parseInternalUtilityColorReference} from '@gravity-ui/uikit-themer/dist/utils'; +import React, {Fragment} from 'react'; + +import {block} from '../../../../utils'; +import type {SemanticColorGroup} from '../../hooks/useThemeSemanticColorOption'; + +import './ColorSelectPopupContent.scss'; +import { + PrivateColorsList, + SemanticGroupColorsList, + SemanticGroupList, +} from './ColorSelectPopupContentItems'; +import type {ColorGroup} from './types'; + +const b = block('private-colors-select-popup'); + +interface ColorSelectPopupContentProps { + privateGroups: ColorGroup[]; + semanticGroups?: SemanticColorGroup[]; + value?: string; + onChange: (token: string, ref?: string) => void; + version?: 'mobile' | 'desktop'; +} + +export const ColorSelectPopupContent = ({ + value, + privateGroups, + semanticGroups, + onChange, + version = 'desktop', +}: ColorSelectPopupContentProps) => { + const colorsRef = React.useRef<HTMLDivElement>(null); + const isUtilityColor = isInternalUtilityColorReference(value); + + const [currentGroupToken, setCurrentGroupToken] = React.useState<string | undefined>(() => { + if (isUtilityColor) { + const utilityTokenName = parseInternalUtilityColorReference(value as UtilityColor); + const groupName = semanticGroups?.find((item) => + item.groups.some((group) => + group.items.some((item) => item.name === utilityTokenName), + ), + ); + + return groupName?.key; + } + + return value ? parseInternalPrivateColorReference(value)?.mainColorToken : undefined; + }); + + const [selectedGroupType, setSelectedGroupType] = React.useState< + 'private' | 'semantic' | undefined + >(isUtilityColor ? 'semantic' : 'private'); + + React.useEffect(() => { + const mainColorToken = value + ? parseInternalPrivateColorReference(value)?.mainColorToken + : undefined; + + if (mainColorToken) { + setCurrentGroupToken(mainColorToken); + } + }, [value]); + + const groupToken = currentGroupToken || privateGroups[0].token; + + React.useEffect(() => { + colorsRef.current?.scrollTo({ + top: 0, + behavior: 'smooth', + }); + }, [groupToken]); + + const groupPrivateColors = React.useMemo( + () => privateGroups.find(({token}) => token === groupToken)?.privateColors || [], + [privateGroups, groupToken], + ); + + return ( + <div className={b({version})}> + <div className={b('left')}> + <Text variant="caption-2" color="secondary"> + PRIVATE COLORS + </Text> + <PrivateColorsList + colors={privateGroups} + value={groupToken} + onSelect={(val) => { + setCurrentGroupToken(val); + setSelectedGroupType('private'); + }} + view={version === 'mobile' ? 'select' : 'list'} + /> + + {semanticGroups && Boolean(semanticGroups?.length) && ( + <Fragment> + <Divider className={b('divider')} /> + <Text variant="caption-2" color="secondary"> + SEMANTIC COLORS + </Text> + <SemanticGroupList + groups={semanticGroups} + value={groupToken} + onSelect={(val) => { + setCurrentGroupToken(val); + setSelectedGroupType('semantic'); + }} + /> + </Fragment> + )} + </div> + <div className={b('right')} ref={colorsRef}> + {selectedGroupType === 'semantic' ? ( + <SemanticGroupColorsList + groups={ + semanticGroups?.find((item) => item.key === groupToken)?.groups || [] + } + privateGroups={privateGroups} + value={value} + onSelect={onChange} + /> + ) : ( + <PrivateColorsList + colors={groupPrivateColors} + value={value} + onSelect={onChange} + /> + )} + </div> + </div> + ); +}; diff --git a/src/components/Themes/ui/GravityColorSelect/ColorSelectPopupContentItems.tsx b/src/components/Themes/ui/GravityColorSelect/ColorSelectPopupContentItems.tsx new file mode 100644 index 000000000000..27e18cf601d8 --- /dev/null +++ b/src/components/Themes/ui/GravityColorSelect/ColorSelectPopupContentItems.tsx @@ -0,0 +1,248 @@ +import {Flex, HelpMark, List, Select, SelectOption, Text} from '@gravity-ui/uikit'; +import { + type UtilityColor, + isInternalPrivateColorReference, + isInternalUtilityColorReference, +} from '@gravity-ui/uikit-themer'; +import {parseInternalUtilityColorReference} from '@gravity-ui/uikit-themer/dist/utils'; +import {useTranslation} from 'next-i18next'; +import React from 'react'; + +import {block} from '../../../../utils'; +import type { + SemanticColorGroup, + SemanticColorGroupItem, +} from '../../hooks/useThemeSemanticColorOption'; +import {ColorPreview} from '../ColorPreview/ColorPreview'; + +import './ColorSelectPopupContent.scss'; +import type {BaseColor, ColorGroup} from './types'; +import {getColorFromPrivateColor, getColorFromUtilityColor} from './utils'; + +type ColorItemProps = { + color: string; + title: string; + disabled?: boolean; +}; + +const b = block('private-colors-select-popup'); + +const ColorItem: React.FC<ColorItemProps> = ({title, color, disabled}) => { + return ( + <div className={b('color-item', {disabled})}> + <ColorPreview color={color} /> + <Text>{title}</Text> + </div> + ); +}; + +interface PrivateColorsListProps { + colors: BaseColor[]; + value?: string; + onSelect: (value: string) => void; + view?: 'select' | 'list'; +} + +export const PrivateColorsList = ({ + colors, + value, + onSelect, + view = 'list', +}: PrivateColorsListProps) => { + const selectedIndex = React.useMemo( + () => colors.findIndex((item) => item.token === value), + [colors, value], + ); + + const handleSelect = React.useCallback( + (item: BaseColor) => { + onSelect(item.token); + }, + [onSelect], + ); + + const renderItem = React.useCallback( + (item: BaseColor) => <ColorItem color={item.color} title={item.title} />, + [], + ); + + const selectOptions = React.useMemo(() => { + return colors.map((color) => ({data: color, value: color.token})); + }, [colors]); + + const renderOption = React.useCallback( + (option: SelectOption<BaseColor>) => { + return ( + <div key={option.value} className={b('color-option')}> + {renderItem(option.data as BaseColor)} + </div> + ); + }, + [renderItem], + ); + + const handleSelectChange = React.useCallback( + (newToken: string[]) => { + const newColor = colors.find((item) => item.token === newToken[0]); + if (newColor) { + handleSelect(newColor); + } + }, + [colors, handleSelect], + ); + + return view === 'select' ? ( + <Select<BaseColor> + className={b('colors-select')} + size="xl" + options={selectOptions} + renderOption={renderOption} + renderSelectedOption={renderOption} + popupPlacement={'top-end'} + value={value ? [value] : undefined} + onUpdate={handleSelectChange} + disablePortal + /> + ) : ( + <List<BaseColor> + items={colors} + filterable={false} + virtualized={false} + selectedItemIndex={selectedIndex} + onItemClick={handleSelect} + renderItem={renderItem} + className={b('colors-list')} + itemClassName={b('colors-list-item')} + /> + ); +}; + +interface SemanticGroupListProps { + groups: SemanticColorGroup[]; + value?: string; + onSelect: (value: string) => void; +} + +interface SemanticGroupColorsListProps { + groups: SemanticColorGroup['groups']; + value?: string; + privateGroups: ColorGroup[]; + onSelect: (value: string, ref?: string) => void; +} + +export const SemanticGroupColorsList = ({ + groups, + value, + onSelect, + privateGroups, +}: SemanticGroupColorsListProps) => { + const {t} = useTranslation('themes'); + const selectedColorTokenName = parseInternalUtilityColorReference(value as UtilityColor); + + const handleSelect = React.useCallback( + (item: SemanticColorGroupItem) => { + onSelect(item.color, item.token); + }, + [onSelect], + ); + + const renderItem = React.useCallback( + (item: SemanticColorGroupItem) => { + if (!item.color) { + return null; + } + + let color: string | undefined = item.color; + + if (isInternalUtilityColorReference(item.color)) { + color = getColorFromUtilityColor(item.color, groups)?.color; + } + + if (isInternalPrivateColorReference(item.color)) { + color = getColorFromPrivateColor(item.color, privateGroups)?.color; + } + + return ( + <Flex gap={1} alignItems="center"> + <ColorItem + key={item.token} + color={color ?? ''} + title={item.title} + disabled={item.disabled} + /> + {item.disabled && ( + <HelpMark iconSize="s"> + <Text variant="caption-1" color="secondary"> + {t('text_utility-color_disabled_description')} + </Text> + </HelpMark> + )} + </Flex> + ); + }, + [t], + ); + + return ( + <Flex direction="column" gap={3}> + {groups.map((group) => { + const selectedIndex = group.items.findIndex( + (item) => item.name === selectedColorTokenName, + ); + + return ( + <Flex key={group.title} direction="column" gap={1}> + <Text>{group.title}</Text> + + <List<SemanticColorGroupItem> + items={group.items} + filterable={false} + virtualized={false} + selectedItemIndex={selectedIndex === -1 ? undefined : selectedIndex} + onItemClick={handleSelect} + renderItem={renderItem} + className={b('colors-list')} + itemClassName={b('colors-list-item')} + /> + </Flex> + ); + })} + </Flex> + ); +}; + +export const SemanticGroupList = ({groups, value, onSelect}: SemanticGroupListProps) => { + const selectedIndex = React.useMemo( + () => groups.findIndex((item) => item.key === value), + [groups, value], + ); + + const handleSelect = React.useCallback( + (item: SemanticColorGroup) => { + onSelect(item.key); + }, + [onSelect], + ); + + const renderItem = React.useCallback((item: SemanticColorGroup) => { + return ( + <Flex key={item.key} gap={1} alignItems="center" className={b('color-item')}> + {item.icon} + <Text>{item.title}</Text> + </Flex> + ); + }, []); + + return ( + <List<SemanticColorGroup> + items={groups} + filterable={false} + virtualized={false} + selectedItemIndex={selectedIndex} + onItemClick={handleSelect} + renderItem={renderItem} + className={b('colors-list')} + itemClassName={b('colors-list-item')} + /> + ); +}; diff --git a/src/components/Themes/ui/PrivateColorSelect/PrivateColorSelect.scss b/src/components/Themes/ui/GravityColorSelect/GravityColorSelect.scss similarity index 100% rename from src/components/Themes/ui/PrivateColorSelect/PrivateColorSelect.scss rename to src/components/Themes/ui/GravityColorSelect/GravityColorSelect.scss diff --git a/src/components/Themes/ui/PrivateColorSelect/PrivateColorSelect.tsx b/src/components/Themes/ui/GravityColorSelect/GravityColorSelect.tsx similarity index 93% rename from src/components/Themes/ui/PrivateColorSelect/PrivateColorSelect.tsx rename to src/components/Themes/ui/GravityColorSelect/GravityColorSelect.tsx index b9e912b2d8db..d74aa611c89d 100644 --- a/src/components/Themes/ui/PrivateColorSelect/PrivateColorSelect.tsx +++ b/src/components/Themes/ui/GravityColorSelect/GravityColorSelect.tsx @@ -22,29 +22,29 @@ import type {SemanticColorGroup} from '../../hooks/useThemeSemanticColorOption'; import {ColorPickerInput} from '../ColorPickerInput/ColorPickerInput'; import {ColorPreview} from '../ColorPreview/ColorPreview'; -import './PrivateColorSelect.scss'; -import {PrivateColorSelectPopupContent} from './PrivateColorSelectPopupContent'; +import {ColorSelectPopupContent} from './ColorSelectPopupContent'; +import './GravityColorSelect.scss'; import type {BaseColor, ColorGroup} from './types'; const b = block('private-colors-select'); -interface PrivateColorSelectProps { +interface GravityColorSelectProps { value?: string; defaultValue: string; - onChange: (color: string) => void; + onChange: (color: string, ref?: string) => void; privateGroups: ColorGroup[]; semanticGroups?: SemanticColorGroup[]; inputView?: TextInputProps['view']; } -export const PrivateColorSelect: React.FC<PrivateColorSelectProps> = ({ +export const GravityColorSelect = ({ privateGroups, semanticGroups, value, defaultValue, onChange, inputView = 'normal', -}) => { +}: GravityColorSelectProps) => { const isMobile = useIsMobile(); const [containerElement, setContainerElement] = React.useState<HTMLDivElement | null>(null); @@ -53,8 +53,8 @@ export const PrivateColorSelect: React.FC<PrivateColorSelectProps> = ({ !isInternalPrivateColorReference(value) && !isInternalUtilityColorReference(value); const handleChange = React.useCallback( - (newVal: string) => { - onChange(newVal); + (newVal: string, newRef?: string) => { + onChange(newVal, newRef); setShowPopup(false); }, [onChange], @@ -164,7 +164,7 @@ export const PrivateColorSelect: React.FC<PrivateColorSelectProps> = ({ visible={showPopup} onClose={closePopup} > - <PrivateColorSelectPopupContent + <ColorSelectPopupContent privateGroups={privateGroups} value={value} onChange={handleChange} @@ -182,7 +182,7 @@ export const PrivateColorSelect: React.FC<PrivateColorSelectProps> = ({ } }} > - <PrivateColorSelectPopupContent + <ColorSelectPopupContent privateGroups={privateGroups} semanticGroups={semanticGroups} value={value} diff --git a/src/components/Themes/ui/GravityColorSelect/index.ts b/src/components/Themes/ui/GravityColorSelect/index.ts new file mode 100644 index 000000000000..95c2e3630321 --- /dev/null +++ b/src/components/Themes/ui/GravityColorSelect/index.ts @@ -0,0 +1 @@ +export {GravityColorSelect} from './GravityColorSelect'; diff --git a/src/components/Themes/ui/PrivateColorSelect/types.ts b/src/components/Themes/ui/GravityColorSelect/types.ts similarity index 100% rename from src/components/Themes/ui/PrivateColorSelect/types.ts rename to src/components/Themes/ui/GravityColorSelect/types.ts diff --git a/src/components/Themes/ui/GravityColorSelect/utils.ts b/src/components/Themes/ui/GravityColorSelect/utils.ts new file mode 100644 index 000000000000..f409b10780c8 --- /dev/null +++ b/src/components/Themes/ui/GravityColorSelect/utils.ts @@ -0,0 +1,36 @@ +import {parseInternalPrivateColorReference} from '@gravity-ui/uikit-themer'; +import {parseInternalUtilityColorReference} from '@gravity-ui/uikit-themer/dist/utils'; + +import type {SemanticColorGroup} from '../../hooks/useThemeSemanticColorOption'; + +import type {BaseColor, ColorGroup} from './types'; + +export const getColorFromUtilityColor = (color: string, groups: SemanticColorGroup['groups']) => { + const tokenName = parseInternalUtilityColorReference(color); + let semanticItem: BaseColor | undefined; + + groups.forEach((nestedGroup) => + nestedGroup.items.forEach((item) => { + if (item.name === tokenName) { + semanticItem = item; + return; + } + }), + ); + + return semanticItem; +}; + +export const getColorFromPrivateColor = (color: string, privateGroups: ColorGroup[]) => { + const parsedPrivateColorToken = parseInternalPrivateColorReference(color); + + if (parsedPrivateColorToken) { + const {mainColorToken} = parsedPrivateColorToken; + + return privateGroups + .find((group) => group.token === mainColorToken) + ?.privateColors.find((privateColor) => privateColor.token === color); + } + + return undefined; +}; diff --git a/src/components/Themes/ui/PrivateColorSelect/PrivateColorSelectPopupContent.tsx b/src/components/Themes/ui/PrivateColorSelect/PrivateColorSelectPopupContent.tsx deleted file mode 100644 index db05cf3c3e25..000000000000 --- a/src/components/Themes/ui/PrivateColorSelect/PrivateColorSelectPopupContent.tsx +++ /dev/null @@ -1,337 +0,0 @@ -import {Divider, Flex, List, Select, SelectOption, Text} from '@gravity-ui/uikit'; -import { - type UtilityColor, - isInternalPrivateColorReference, - isInternalUtilityColorReference, - parseInternalPrivateColorReference, -} from '@gravity-ui/uikit-themer'; -import {parseInternalUtilityColorReference} from '@gravity-ui/uikit-themer/dist/utils'; -import React, {Fragment} from 'react'; - -import {block} from '../../../../utils'; -import type {SemanticColorGroup} from '../../hooks/useThemeSemanticColorOption'; -import {ColorPreview} from '../ColorPreview/ColorPreview'; - -import './PrivateColorSelectPopupContent.scss'; -import type {BaseColor, ColorGroup} from './types'; - -const b = block('private-colors-select-popup'); - -// TODO: split components for folders -type ColorItemProps = { - color: string; - title: string; -}; - -const ColorItem: React.FC<ColorItemProps> = ({title, color}) => { - return ( - <div className={b('color-item')}> - <ColorPreview color={color} /> - <Text>{title}</Text> - </div> - ); -}; - -interface ColorsListProps { - colors: BaseColor[]; - value?: string; - onSelect: (value: string) => void; - view?: 'select' | 'list'; -} - -const ColorsList: React.FC<ColorsListProps> = ({colors, value, onSelect, view = 'list'}) => { - const selectedIndex = React.useMemo( - () => colors.findIndex((item) => item.token === value), - [colors, value], - ); - - const handleSelect = React.useCallback( - (item: BaseColor) => { - onSelect(item.token); - }, - [onSelect], - ); - - const renderItem = React.useCallback( - (item: BaseColor) => <ColorItem color={item.color} title={item.title} />, - [], - ); - - const selectOptions = React.useMemo(() => { - return colors.map((color) => ({data: color, value: color.token})); - }, [colors]); - - const renderOption = React.useCallback( - (option: SelectOption<BaseColor>) => { - return ( - <div key={option.value} className={b('color-option')}> - {renderItem(option.data as BaseColor)} - </div> - ); - }, - [renderItem], - ); - - const handleSelectChange = React.useCallback( - (newToken: string[]) => { - const newColor = colors.find((item) => item.token === newToken[0]); - if (newColor) { - handleSelect(newColor); - } - }, - [colors, handleSelect], - ); - - return view === 'select' ? ( - <Select<BaseColor> - className={b('colors-select')} - size="xl" - options={selectOptions} - renderOption={renderOption} - renderSelectedOption={renderOption} - popupPlacement={'top-end'} - value={value ? [value] : undefined} - onUpdate={handleSelectChange} - disablePortal - /> - ) : ( - <List<BaseColor> - items={colors} - filterable={false} - virtualized={false} - selectedItemIndex={selectedIndex} - onItemClick={handleSelect} - renderItem={renderItem} - className={b('colors-list')} - itemClassName={b('colors-list-item')} - /> - ); -}; - -interface SemanticGroupListProps { - groups: SemanticColorGroup[]; - value?: string; - onSelect: (value: string) => void; -} - -interface SemanticGroupColorsListProps { - groups: SemanticColorGroup['groups']; - value?: string; - privateGroups: ColorGroup[]; - onSelect: (value: string) => void; -} - -const SemanticGroupColorsList = ({ - groups, - value, - onSelect, - privateGroups, -}: SemanticGroupColorsListProps) => { - const selectedColorTokenName = parseInternalUtilityColorReference(value as UtilityColor); - - const handleSelect = React.useCallback( - (item: BaseColor) => { - onSelect(item.token); - }, - [onSelect], - ); - - const renderItem = React.useCallback((item: BaseColor) => { - if (!item.color) { - return null; - } - - let color: string | undefined = item.color; - - if (isInternalPrivateColorReference(item.color)) { - const parsedPrivateColorToken = parseInternalPrivateColorReference(item.color); - - if (parsedPrivateColorToken) { - const {mainColorToken, privateColorCode} = parsedPrivateColorToken; - - color = privateGroups - .find((group) => group.token === mainColorToken) - ?.privateColors.find( - (privateColor) => privateColor.token === privateColorCode, - )?.color; - } - } - - return <ColorItem key={item.token} color={color ?? ''} title={item.title} />; - }, []); - - return ( - <Flex direction="column" gap={3}> - {groups.map((group) => { - const selectedIndex = group.items.findIndex( - (item) => item.name === selectedColorTokenName, - ); - - return ( - <Flex key={group.title} direction="column" gap={1}> - <Text>{group.title}</Text> - - <List<BaseColor> - items={group.items} - filterable={false} - virtualized={false} - selectedItemIndex={selectedIndex === -1 ? undefined : selectedIndex} - onItemClick={handleSelect} - renderItem={renderItem} - className={b('colors-list')} - itemClassName={b('colors-list-item')} - /> - </Flex> - ); - })} - </Flex> - ); -}; - -const SemanticGroupList = ({groups, value, onSelect}: SemanticGroupListProps) => { - const selectedIndex = React.useMemo( - () => groups.findIndex((item) => item.key === value), - [groups, value], - ); - - const handleSelect = React.useCallback( - (item: SemanticColorGroup) => { - onSelect(item.key); - }, - [onSelect], - ); - - const renderItem = React.useCallback((item: SemanticColorGroup) => { - return ( - <Flex key={item.key} gap={1} alignItems="center" className={b('color-item')}> - {item.icon} - <Text>{item.title}</Text> - </Flex> - ); - }, []); - - return ( - <List<SemanticColorGroup> - items={groups} - filterable={false} - virtualized={false} - selectedItemIndex={selectedIndex} - onItemClick={handleSelect} - renderItem={renderItem} - className={b('colors-list')} - itemClassName={b('colors-list-item')} - /> - ); -}; - -interface PrivateColorSelectPopupContentProps { - privateGroups: ColorGroup[]; - semanticGroups?: SemanticColorGroup[]; - value?: string; - onChange: (token: string) => void; - version?: 'mobile' | 'desktop'; -} - -export const PrivateColorSelectPopupContent: React.FC<PrivateColorSelectPopupContentProps> = ({ - value, - privateGroups, - semanticGroups, - onChange, - version = 'desktop', -}) => { - const colorsRef = React.useRef<HTMLDivElement>(null); - const isUtilityColor = isInternalUtilityColorReference(value); - - const [currentGroupToken, setCurrentGroupToken] = React.useState<string | undefined>(() => { - if (isUtilityColor) { - const utilityTokenName = parseInternalUtilityColorReference(value as UtilityColor); - const groupName = semanticGroups?.find((item) => - item.groups.some((group) => - group.items.some((item) => item.name === utilityTokenName), - ), - ); - - return groupName?.key; - } - - return value ? parseInternalPrivateColorReference(value)?.mainColorToken : undefined; - }); - - const [selectedGroupType, setSelectedGroupType] = React.useState< - 'private' | 'semantic' | undefined - >(isUtilityColor ? 'semantic' : 'private'); - - React.useEffect(() => { - const mainColorToken = value - ? parseInternalPrivateColorReference(value)?.mainColorToken - : undefined; - - if (mainColorToken) { - setCurrentGroupToken(mainColorToken); - } - }, [value]); - - const groupToken = currentGroupToken || privateGroups[0].token; - - React.useEffect(() => { - colorsRef.current?.scrollTo({ - top: 0, - behavior: 'smooth', - }); - }, [groupToken]); - - const groupPrivateColors = React.useMemo( - () => privateGroups.find(({token}) => token === groupToken)?.privateColors || [], - [privateGroups, groupToken], - ); - - return ( - <div className={b({version})}> - <div className={b('left')}> - <Text variant="caption-2" color="secondary"> - PRIVATE COLORS - </Text> - <ColorsList - colors={privateGroups} - value={groupToken} - onSelect={(val) => { - setCurrentGroupToken(val); - setSelectedGroupType('private'); - }} - view={version === 'mobile' ? 'select' : 'list'} - /> - - {semanticGroups && Boolean(semanticGroups?.length) && ( - <Fragment> - <Divider /> - <Text variant="caption-2" color="secondary"> - SEMANTIC COLORS - </Text> - <SemanticGroupList - groups={semanticGroups} - value={groupToken} - onSelect={(val) => { - setCurrentGroupToken(val); - setSelectedGroupType('semantic'); - }} - /> - </Fragment> - )} - </div> - <div className={b('right')} ref={colorsRef}> - {selectedGroupType === 'semantic' ? ( - <SemanticGroupColorsList - groups={ - semanticGroups?.find((item) => item.key === groupToken)?.groups || [] - } - privateGroups={privateGroups} - value={value} - onSelect={onChange} - /> - ) : ( - <ColorsList colors={groupPrivateColors} value={value} onSelect={onChange} /> - )} - </div> - </div> - ); -}; diff --git a/src/components/Themes/ui/PrivateColorSelect/index.ts b/src/components/Themes/ui/PrivateColorSelect/index.ts deleted file mode 100644 index 80cd714d3529..000000000000 --- a/src/components/Themes/ui/PrivateColorSelect/index.ts +++ /dev/null @@ -1 +0,0 @@ -export {PrivateColorSelect} from './PrivateColorSelect'; diff --git a/src/components/Themes/ui/PrivateColorsSettings/PrivateColorsSettings.tsx b/src/components/Themes/ui/PrivateColorsSettings/PrivateColorsSettings.tsx index abd120f52c09..ce063f91a6b3 100644 --- a/src/components/Themes/ui/PrivateColorsSettings/PrivateColorsSettings.tsx +++ b/src/components/Themes/ui/PrivateColorsSettings/PrivateColorsSettings.tsx @@ -10,7 +10,7 @@ import React from 'react'; import {block} from '../../../../utils'; import {useThemePrivateColorOptions, useThemeUtilityColor} from '../../hooks'; import {ThemeColorOption} from '../../lib/themeCreatorUtils'; -import {PrivateColorSelect} from '../PrivateColorSelect'; +import {GravityColorSelect} from '../GravityColorSelect'; import {ThemableSettings} from '../ThemableSettings/ThemableSettings'; import {ThemableRow} from '../ThemableSettings/types'; import {ThemeSection} from '../ThemeSection'; @@ -46,7 +46,7 @@ const PrivateColorEditor: React.FC<PrivateColorEditorProps> = ({name, theme, col }, [name, theme]); return ( - <PrivateColorSelect + <GravityColorSelect privateGroups={colorGroups} defaultValue={defaultValue} value={color} From 7cba2d5a7b924a957b6e7a51e4dc4101473835aa Mon Sep 17 00:00:00 2001 From: aobityutskiy <aobityutskiy@yandex-team.ru> Date: Thu, 27 Nov 2025 16:24:39 +0300 Subject: [PATCH 3/5] feat: update themer package, add recursive color select logic --- package-lock.json | 8 ++++---- package.json | 2 +- .../Themes/hooks/useThemeSemanticColorOption.tsx | 7 ++----- src/components/Themes/lib/utils.ts | 7 +++++-- .../Themes/ui/AdvancedSettingsTable/columns.tsx | 6 ++---- .../ui/GravityColorSelect/ColorSelectPopupContent.tsx | 2 +- .../GravityColorSelect/ColorSelectPopupContentItems.tsx | 6 +++--- .../Themes/ui/GravityColorSelect/GravityColorSelect.tsx | 2 +- src/components/Themes/ui/GravityColorSelect/utils.ts | 6 ++++-- 9 files changed, 23 insertions(+), 23 deletions(-) diff --git a/package-lock.json b/package-lock.json index 02bcfd8917bb..e94afdf2de2f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,7 @@ "@gravity-ui/navigation": "^3.10.1", "@gravity-ui/page-constructor": "^6.0.0-beta.6", "@gravity-ui/uikit": "^7.26.2", - "@gravity-ui/uikit-themer": "^1.4.2", + "@gravity-ui/uikit-themer": "^1.5.0", "@mdx-js/mdx": "^2.3.0", "@mdx-js/react": "^2.3.0", "@monaco-editor/react": "^4.6.0", @@ -3630,9 +3630,9 @@ } }, "node_modules/@gravity-ui/uikit-themer": { - "version": "1.4.1", - "resolved": "file:../uikit-themer/gravity-ui-uikit-themer-1.4.1.tgz", - "integrity": "sha512-yRTHOT3smgz+Px5VUievZVD4U4t62bkxJPXdwxSif2NOk0Ee5FFufAXWaR38rwvgB2B9adTMb5qgHTNViZqnMg==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@gravity-ui/uikit-themer/-/uikit-themer-1.5.0.tgz", + "integrity": "sha512-kdxJgmmtjb4oOdBPgt0Vges5Uh4eJ/YUd4RkHbTiXv09YxiNvOtNR9tdzVc/uPX52WL5BRyiXGDuexZyiaaKyg==", "dependencies": { "chroma-js": "^3.1.2", "lodash-es": "^4.17.21" diff --git a/package.json b/package.json index 62bac4db035a..a92e496b3dc4 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "@gravity-ui/navigation": "^3.10.1", "@gravity-ui/page-constructor": "^6.0.0-beta.6", "@gravity-ui/uikit": "^7.26.2", - "@gravity-ui/uikit-themer": "^1.4.2", + "@gravity-ui/uikit-themer": "^1.5.0", "@mdx-js/mdx": "^2.3.0", "@mdx-js/react": "^2.3.0", "@monaco-editor/react": "^4.6.0", diff --git a/src/components/Themes/hooks/useThemeSemanticColorOption.tsx b/src/components/Themes/hooks/useThemeSemanticColorOption.tsx index 00e31282e00a..21aa2059836a 100644 --- a/src/components/Themes/hooks/useThemeSemanticColorOption.tsx +++ b/src/components/Themes/hooks/useThemeSemanticColorOption.tsx @@ -12,14 +12,12 @@ import { type GravityTheme, type Theme, type UtilityColor, + createInternalUtilityColorReference, createUtilityColorCssVariable, isInternalUtilityColorReference, -} from '@gravity-ui/uikit-themer'; -import { - createInternalUtilityColorReference, isUtilityColorToken, parseInternalUtilityColorReference, -} from '@gravity-ui/uikit-themer/dist/utils'; +} from '@gravity-ui/uikit-themer'; import {useTranslation} from 'next-i18next'; import {useMemo} from 'react'; @@ -68,7 +66,6 @@ const resolveUtilityColor = ( updatedColorToken: string, ) => { let disabled = false; - console.log('updatedColorToken', updatedColorToken); const traverse = ( colorObject: ColorOptions & {token?: string}, diff --git a/src/components/Themes/lib/utils.ts b/src/components/Themes/lib/utils.ts index a98d22b812ac..e5b462b48e12 100644 --- a/src/components/Themes/lib/utils.ts +++ b/src/components/Themes/lib/utils.ts @@ -1,5 +1,8 @@ -import {DEFAULT_THEME as DEFAULT_GRAVITY_THEME, type UtilityColor} from '@gravity-ui/uikit-themer'; -import {isUtilityColorToken} from '@gravity-ui/uikit-themer/dist/utils'; +import { + DEFAULT_THEME as DEFAULT_GRAVITY_THEME, + type UtilityColor, + isUtilityColorToken, +} from '@gravity-ui/uikit-themer'; import capitalize from 'lodash/capitalize'; import {UTILITY_COLOR_PREFIX} from './constants'; diff --git a/src/components/Themes/ui/AdvancedSettingsTable/columns.tsx b/src/components/Themes/ui/AdvancedSettingsTable/columns.tsx index d93a1c1becb0..a30953d4380d 100644 --- a/src/components/Themes/ui/AdvancedSettingsTable/columns.tsx +++ b/src/components/Themes/ui/AdvancedSettingsTable/columns.tsx @@ -2,12 +2,10 @@ import {Flex, HelpMark, Text} from '@gravity-ui/uikit'; import { type Theme, type UtilityColor, - createUtilityColorCssVariable, -} from '@gravity-ui/uikit-themer'; -import { createInternalUtilityColorReference, + createUtilityColorCssVariable, isUtilityColorToken, -} from '@gravity-ui/uikit-themer/dist/utils'; +} from '@gravity-ui/uikit-themer'; import {useTranslation} from 'next-i18next'; import {useThemePaletteColor, useThemePrivateColorOptions, useThemeUtilityColor} from '../../hooks'; diff --git a/src/components/Themes/ui/GravityColorSelect/ColorSelectPopupContent.tsx b/src/components/Themes/ui/GravityColorSelect/ColorSelectPopupContent.tsx index 187e29558652..092b505f6ee1 100644 --- a/src/components/Themes/ui/GravityColorSelect/ColorSelectPopupContent.tsx +++ b/src/components/Themes/ui/GravityColorSelect/ColorSelectPopupContent.tsx @@ -3,8 +3,8 @@ import { type UtilityColor, isInternalUtilityColorReference, parseInternalPrivateColorReference, + parseInternalUtilityColorReference, } from '@gravity-ui/uikit-themer'; -import {parseInternalUtilityColorReference} from '@gravity-ui/uikit-themer/dist/utils'; import React, {Fragment} from 'react'; import {block} from '../../../../utils'; diff --git a/src/components/Themes/ui/GravityColorSelect/ColorSelectPopupContentItems.tsx b/src/components/Themes/ui/GravityColorSelect/ColorSelectPopupContentItems.tsx index 27e18cf601d8..2dc0f75ba5e4 100644 --- a/src/components/Themes/ui/GravityColorSelect/ColorSelectPopupContentItems.tsx +++ b/src/components/Themes/ui/GravityColorSelect/ColorSelectPopupContentItems.tsx @@ -3,8 +3,8 @@ import { type UtilityColor, isInternalPrivateColorReference, isInternalUtilityColorReference, + parseInternalUtilityColorReference, } from '@gravity-ui/uikit-themer'; -import {parseInternalUtilityColorReference} from '@gravity-ui/uikit-themer/dist/utils'; import {useTranslation} from 'next-i18next'; import React from 'react'; @@ -39,7 +39,7 @@ const ColorItem: React.FC<ColorItemProps> = ({title, color, disabled}) => { interface PrivateColorsListProps { colors: BaseColor[]; value?: string; - onSelect: (value: string) => void; + onSelect: (value: string, ref?: string) => void; view?: 'select' | 'list'; } @@ -56,7 +56,7 @@ export const PrivateColorsList = ({ const handleSelect = React.useCallback( (item: BaseColor) => { - onSelect(item.token); + onSelect(item.color, item.token); }, [onSelect], ); diff --git a/src/components/Themes/ui/GravityColorSelect/GravityColorSelect.tsx b/src/components/Themes/ui/GravityColorSelect/GravityColorSelect.tsx index d74aa611c89d..93ac16b8b3a2 100644 --- a/src/components/Themes/ui/GravityColorSelect/GravityColorSelect.tsx +++ b/src/components/Themes/ui/GravityColorSelect/GravityColorSelect.tsx @@ -12,8 +12,8 @@ import { import { isInternalPrivateColorReference, isInternalUtilityColorReference, + parseInternalUtilityColorReference, } from '@gravity-ui/uikit-themer'; -import {parseInternalUtilityColorReference} from '@gravity-ui/uikit-themer/dist/utils'; import React from 'react'; import {useIsMobile} from '../../../../hooks/useIsMobile'; diff --git a/src/components/Themes/ui/GravityColorSelect/utils.ts b/src/components/Themes/ui/GravityColorSelect/utils.ts index f409b10780c8..f543e5a7e2f7 100644 --- a/src/components/Themes/ui/GravityColorSelect/utils.ts +++ b/src/components/Themes/ui/GravityColorSelect/utils.ts @@ -1,5 +1,7 @@ -import {parseInternalPrivateColorReference} from '@gravity-ui/uikit-themer'; -import {parseInternalUtilityColorReference} from '@gravity-ui/uikit-themer/dist/utils'; +import { + parseInternalPrivateColorReference, + parseInternalUtilityColorReference, +} from '@gravity-ui/uikit-themer'; import type {SemanticColorGroup} from '../../hooks/useThemeSemanticColorOption'; From ac11479a70dbff35cf80979f52540024eefa51d7 Mon Sep 17 00:00:00 2001 From: aobityutskiy <aobityutskiy@yandex-team.ru> Date: Fri, 28 Nov 2025 17:08:26 +0300 Subject: [PATCH 4/5] fix design review issues and fix color selector --- public/locales/en/themes.json | 1 + public/locales/ru/themes.json | 1 + .../hooks/useThemeSemanticColorOption.tsx | 4 +- src/components/Themes/lib/constants.ts | 1 + src/components/Themes/lib/utils.ts | 4 + .../AdvancedSettingsTable.scss | 15 +++- .../AdvancedSettingsTable.tsx | 13 ++- .../ui/AdvancedSettingsTable/columns.tsx | 51 ++++++++++-- .../Themes/ui/AdvancedSettingsTable/hooks.tsx | 80 +++++++++---------- .../ColorSelectPopupContent.tsx | 6 +- .../ColorSelectPopupContentItems.tsx | 6 +- 11 files changed, 121 insertions(+), 61 deletions(-) diff --git a/public/locales/en/themes.json b/public/locales/en/themes.json index 647990892752..48d73791d851 100644 --- a/public/locales/en/themes.json +++ b/public/locales/en/themes.json @@ -22,6 +22,7 @@ "theme_name_light": "Light", "theme_name_dark": "Dark", "title_advance-settings-table_title-variable": "Variable", + "title_advance-settings-table_title-color": "Color", "title_advance-settings-table_title-light": "Light theme value", "title_advance-settings-table_title-dark": "Dark theme value", "title_advance-color-settings-group-base": "Base", diff --git a/public/locales/ru/themes.json b/public/locales/ru/themes.json index a36ebfee29e9..1b91944807bc 100644 --- a/public/locales/ru/themes.json +++ b/public/locales/ru/themes.json @@ -22,6 +22,7 @@ "theme_name_light": "Светлая", "theme_name_dark": "Тёмная", "title_advance-settings-table_title-variable": "Переменная", + "title_advance-settings-table_title-color": "Цвет", "title_advance-settings-table_title-light": "Значение светлой темы", "title_advance-settings-table_title-dark": "Значение тёмной темы", "title_advance-color-settings-base": "Базовый", diff --git a/src/components/Themes/hooks/useThemeSemanticColorOption.tsx b/src/components/Themes/hooks/useThemeSemanticColorOption.tsx index 21aa2059836a..0a6342efee74 100644 --- a/src/components/Themes/hooks/useThemeSemanticColorOption.tsx +++ b/src/components/Themes/hooks/useThemeSemanticColorOption.tsx @@ -57,6 +57,8 @@ const getIconByGroup = (group: Exclude<AdvancedColorType, 'basic-palette'>) => { return <Icon data={MagicWand} />; case 'misc': return <Icon data={Cube} />; + default: + return <Icon data={Cube} />; } }; @@ -162,5 +164,5 @@ export const useThemeSemanticColorOption = ( ), }; }); - }, [themeState, themeVariant]); + }, [gravityTheme, themeVariant, updatedColorToken, t]); }; diff --git a/src/components/Themes/lib/constants.ts b/src/components/Themes/lib/constants.ts index 145a6bb96036..b97127bfe55d 100644 --- a/src/components/Themes/lib/constants.ts +++ b/src/components/Themes/lib/constants.ts @@ -257,6 +257,7 @@ export const DEFAULT_ADVANCED_COLORS: AdvanceColors = { 'brand-summary': { 'brand-palette': [ getDefaultAdvancedColorValue('base-background'), + getDefaultAdvancedColorValue('text-brand-contrast'), { colorName: 'brand', ...DEFAULT_GRAVITY_THEME.baseColors.brand, diff --git a/src/components/Themes/lib/utils.ts b/src/components/Themes/lib/utils.ts index e5b462b48e12..e9ebfbd923aa 100644 --- a/src/components/Themes/lib/utils.ts +++ b/src/components/Themes/lib/utils.ts @@ -24,5 +24,9 @@ export const getColorName = (colorToken: string) => { return capitalize(colorToken); } + if (colorToken === 'brand') { + return 'Brand Color'; + } + return colorToken; }; diff --git a/src/components/Themes/ui/AdvancedSettingsTable/AdvancedSettingsTable.scss b/src/components/Themes/ui/AdvancedSettingsTable/AdvancedSettingsTable.scss index a366d19b2ba2..178d4c8005ac 100644 --- a/src/components/Themes/ui/AdvancedSettingsTable/AdvancedSettingsTable.scss +++ b/src/components/Themes/ui/AdvancedSettingsTable/AdvancedSettingsTable.scss @@ -1,4 +1,5 @@ @use '../../../../variables.scss'; +@use '~@gravity-ui/page-constructor/styles/variables.scss' as pcVariables; $block: '.#{variables.$ns}advanced-color-settings-table'; @@ -6,10 +7,15 @@ $block: '.#{variables.$ns}advanced-color-settings-table'; border-collapse: collapse; &__cell { + width: 410px; padding-inline: var(--g-spacing-5); text-align: left; - border-block-end: 1px solid var(--g-color-line-generic); - border-inline-end: 1px solid var(--g-color-line-generic); + border-block-end: 1px solid var(--g-color-line-generic-solid); + border-inline-end: 1px solid var(--g-color-line-generic-solid); + + @media (max-width: map-get(pcVariables.$gridBreakpoints, 'lg')) { + width: 335px; + } &_group { padding-block: var(--g-spacing-4) var(--g-spacing-1); @@ -24,4 +30,9 @@ $block: '.#{variables.$ns}advanced-color-settings-table'; &__row > &__cell:first-child { padding-inline-start: 0; } + + &__theme-toggle { + width: 100%; + --g-border-radius-xl: 8px; + } } diff --git a/src/components/Themes/ui/AdvancedSettingsTable/AdvancedSettingsTable.tsx b/src/components/Themes/ui/AdvancedSettingsTable/AdvancedSettingsTable.tsx index dfc3c8b7df34..29bdd3679be1 100644 --- a/src/components/Themes/ui/AdvancedSettingsTable/AdvancedSettingsTable.tsx +++ b/src/components/Themes/ui/AdvancedSettingsTable/AdvancedSettingsTable.tsx @@ -1,3 +1,4 @@ +import {Text} from '@gravity-ui/uikit'; import {useTranslation} from 'next-i18next'; import {Fragment} from 'react'; @@ -18,7 +19,7 @@ export interface AdvancedSettingsTableProps { export const AdvancedSettingsTable = ({colorType}: AdvancedSettingsTableProps) => { const {t} = useTranslation('themes'); const extraColors = useExtraColors(); - const columns = useColumns(); + const columns = useColumns({colorType}); return ( <table className={b()}> @@ -41,9 +42,13 @@ export const AdvancedSettingsTable = ({colorType}: AdvancedSettingsTableProps) = className={b('cell', {group: true})} key={`${group}-${key}`} > - {key === 'variable' - ? t(`title_advance-color-settings-group-${group}`) - : ''} + {key === 'variable' ? ( + <Text variant="subheader-1"> + {t(`title_advance-color-settings-group-${group}`)} + </Text> + ) : ( + '' + )} </td> ))} </tr> diff --git a/src/components/Themes/ui/AdvancedSettingsTable/columns.tsx b/src/components/Themes/ui/AdvancedSettingsTable/columns.tsx index a30953d4380d..930b85686517 100644 --- a/src/components/Themes/ui/AdvancedSettingsTable/columns.tsx +++ b/src/components/Themes/ui/AdvancedSettingsTable/columns.tsx @@ -1,4 +1,5 @@ -import {Flex, HelpMark, Text} from '@gravity-ui/uikit'; +import {Moon, Sun} from '@gravity-ui/icons'; +import {Flex, HelpMark, Icon, SegmentedRadioGroup, Text} from '@gravity-ui/uikit'; import { type Theme, type UtilityColor, @@ -8,6 +9,7 @@ import { } from '@gravity-ui/uikit-themer'; import {useTranslation} from 'next-i18next'; +import {block} from '../../../../utils'; import {useThemePaletteColor, useThemePrivateColorOptions, useThemeUtilityColor} from '../../hooks'; import {useThemeSemanticColorOption} from '../../hooks/useThemeSemanticColorOption'; import {getColorName, getColorPrefix} from '../../lib/utils'; @@ -18,6 +20,8 @@ import {ExtraColorName} from './ExtraColorName/ExtraColorName'; type ColumnProps = {colorName: string; theme: Theme; value?: string}; +const b = block('advanced-color-settings-table'); + const PaletteThemeValueColumn = ({colorName, theme, value}: ColumnProps) => { const [color, setColor] = useThemePaletteColor({ token: colorName, @@ -34,6 +38,11 @@ const PaletteThemeValueColumn = ({colorName, theme, value}: ColumnProps) => { ); }; +const utilityColorsForDefaultColorPicker: UtilityColor[] = [ + 'base-background', + 'text-brand-contrast', +]; + const UtilityThemeValueColumn = ({ colorName, theme, @@ -50,7 +59,7 @@ const UtilityThemeValueColumn = ({ createInternalUtilityColorReference(colorName), ); - if (colorName === 'base-background') { + if (utilityColorsForDefaultColorPicker.includes(colorName)) { return ( <ColorPickerInput value={color} @@ -73,7 +82,7 @@ const UtilityThemeValueColumn = ({ ); }; -export const ThemeValueColumn = ({colorName, theme, value}: ColumnProps) => { +const ThemeValueColumn = ({colorName, theme, value}: ColumnProps) => { const isUtilityColor = isUtilityColorToken(colorName); if (isUtilityColor) { @@ -83,10 +92,40 @@ export const ThemeValueColumn = ({colorName, theme, value}: ColumnProps) => { return <PaletteThemeValueColumn colorName={colorName} theme={theme} value={value} />; }; -export const TitleColumn = ({value}: {value: string}) => { +const TitleColumn = ({value}: {value: string}) => { return <Text variant="header-1">{value}</Text>; }; +const ThemeToggleTitleColumn = ({ + theme, + toggleTheme, +}: { + theme: Theme; + toggleTheme: (theme: Theme) => void; +}) => { + const {t} = useTranslation('themes'); + + return ( + <SegmentedRadioGroup + size="xl" + defaultValue={theme} + className={b('theme-toggle')} + onChange={(e) => { + toggleTheme(e.target.value as Theme); + }} + > + <SegmentedRadioGroup.Option value="light"> + <Icon data={Sun} /> + {t('theme_name_light')} + </SegmentedRadioGroup.Option> + <SegmentedRadioGroup.Option value="dark"> + <Icon data={Moon} /> + {t('theme_name_dark')} + </SegmentedRadioGroup.Option> + </SegmentedRadioGroup> + ); +}; + const UtilityVariableDescription = ({name}: {name: UtilityColor}) => { const {t} = useTranslation('themes'); const content = t(`text_utility-color_${name}_description`); @@ -103,7 +142,7 @@ const UtilityVariableDescription = ({name}: {name: UtilityColor}) => { ); }; -export const VariableColumn = ({name, extraVariable}: {name: string; extraVariable?: boolean}) => { +const VariableColumn = ({name, extraVariable}: {name: string; extraVariable?: boolean}) => { if (extraVariable) { return <ExtraColorName token={name} />; } @@ -126,3 +165,5 @@ export const VariableColumn = ({name, extraVariable}: {name: string; extraVariab </Flex> ); }; + +export {ThemeValueColumn, TitleColumn, VariableColumn, ThemeToggleTitleColumn}; diff --git a/src/components/Themes/ui/AdvancedSettingsTable/hooks.tsx b/src/components/Themes/ui/AdvancedSettingsTable/hooks.tsx index d0ef476f4bc3..5bc87f37bf22 100644 --- a/src/components/Themes/ui/AdvancedSettingsTable/hooks.tsx +++ b/src/components/Themes/ui/AdvancedSettingsTable/hooks.tsx @@ -1,14 +1,13 @@ -import {Moon, Sun} from '@gravity-ui/icons'; import {BREAKPOINTS, useWindowBreakpoint} from '@gravity-ui/page-constructor'; -import {Icon, SegmentedRadioGroup} from '@gravity-ui/uikit'; import type {BaseColors, Theme} from '@gravity-ui/uikit-themer'; import {useTranslation} from 'next-i18next'; import {type ReactElement, useMemo, useState} from 'react'; import {useThemeCreator} from '../../hooks'; import {isManuallyCreatedPaletteToken} from '../../lib/themeCreatorUtils'; +import type {AdvancedColorType} from '../../lib/types'; -import {ThemeValueColumn, TitleColumn, VariableColumn} from './columns'; +import {ThemeToggleTitleColumn, ThemeValueColumn, TitleColumn, VariableColumn} from './columns'; export const useExtraColors = () => { const {gravityTheme} = useThemeCreator(); @@ -34,7 +33,7 @@ type Column = { }) => ReactElement; }; -export const useColumns = (): Column[] => { +export const useColumns = ({colorType}: {colorType: AdvancedColorType}): Column[] => { const [theme, toggleTheme] = useState<Theme>('light'); const {t} = useTranslation('themes'); @@ -43,7 +42,15 @@ export const useColumns = (): Column[] => { const variableColumn: Column = useMemo( () => ({ - title: () => <TitleColumn value={t('title_advance-settings-table_title-variable')} />, + title: () => ( + <TitleColumn + value={ + colorType === 'basic-palette' + ? t('title_advance-settings-table_title-color') + : t('title_advance-settings-table_title-variable') + } + /> + ), key: 'variable', render: ({colorName, extraVariable = false}) => ( <VariableColumn name={colorName} extraVariable={extraVariable} /> @@ -52,44 +59,25 @@ export const useColumns = (): Column[] => { [t], ); - if (isTablet) { - return useMemo( - () => [ - variableColumn, - { - title: () => ( - <SegmentedRadioGroup - size="xl" - defaultValue={theme} - onChange={(e) => { - toggleTheme(e.target.value as Theme); - }} - > - <SegmentedRadioGroup.Option value="light"> - <Icon data={Sun} /> - {t('theme_name_light')} - </SegmentedRadioGroup.Option> - <SegmentedRadioGroup.Option value="dark"> - <Icon data={Moon} /> - {t('theme_name_dark')} - </SegmentedRadioGroup.Option> - </SegmentedRadioGroup> - ), - key: 'themeToggle', - render: ({colorName, light, dark}) => ( - <ThemeValueColumn - theme={theme} - colorName={colorName} - value={theme === 'light' ? light : dark} - /> - ), - }, - ], - [theme, t], - ); - } + const tabletColumns: Column[] = useMemo( + () => [ + variableColumn, + { + title: () => <ThemeToggleTitleColumn theme={theme} toggleTheme={toggleTheme} />, + key: 'themeToggle', + render: ({colorName, light, dark}) => ( + <ThemeValueColumn + theme={theme} + colorName={colorName} + value={theme === 'light' ? light : dark} + /> + ), + }, + ], + [variableColumn, theme], + ); - return useMemo( + const desktopColumns: Column[] = useMemo( () => [ variableColumn, { @@ -107,6 +95,12 @@ export const useColumns = (): Column[] => { ), }, ], - [t], + [variableColumn, t], ); + + if (isTablet) { + return tabletColumns; + } + + return desktopColumns; }; diff --git a/src/components/Themes/ui/GravityColorSelect/ColorSelectPopupContent.tsx b/src/components/Themes/ui/GravityColorSelect/ColorSelectPopupContent.tsx index 092b505f6ee1..d16a3363fd6c 100644 --- a/src/components/Themes/ui/GravityColorSelect/ColorSelectPopupContent.tsx +++ b/src/components/Themes/ui/GravityColorSelect/ColorSelectPopupContent.tsx @@ -90,8 +90,8 @@ export const ColorSelectPopupContent = ({ <PrivateColorsList colors={privateGroups} value={groupToken} - onSelect={(val) => { - setCurrentGroupToken(val); + onSelect={(item) => { + setCurrentGroupToken(item.token); setSelectedGroupType('private'); }} view={version === 'mobile' ? 'select' : 'list'} @@ -128,7 +128,7 @@ export const ColorSelectPopupContent = ({ <PrivateColorsList colors={groupPrivateColors} value={value} - onSelect={onChange} + onSelect={(item) => onChange(item.color, item.token)} /> )} </div> diff --git a/src/components/Themes/ui/GravityColorSelect/ColorSelectPopupContentItems.tsx b/src/components/Themes/ui/GravityColorSelect/ColorSelectPopupContentItems.tsx index 2dc0f75ba5e4..bab0fba4a389 100644 --- a/src/components/Themes/ui/GravityColorSelect/ColorSelectPopupContentItems.tsx +++ b/src/components/Themes/ui/GravityColorSelect/ColorSelectPopupContentItems.tsx @@ -39,7 +39,7 @@ const ColorItem: React.FC<ColorItemProps> = ({title, color, disabled}) => { interface PrivateColorsListProps { colors: BaseColor[]; value?: string; - onSelect: (value: string, ref?: string) => void; + onSelect: (item: BaseColor) => void; view?: 'select' | 'list'; } @@ -56,7 +56,7 @@ export const PrivateColorsList = ({ const handleSelect = React.useCallback( (item: BaseColor) => { - onSelect(item.color, item.token); + onSelect(item); }, [onSelect], ); @@ -172,7 +172,7 @@ export const SemanticGroupColorsList = ({ /> {item.disabled && ( <HelpMark iconSize="s"> - <Text variant="caption-1" color="secondary"> + <Text variant="body-1" color="secondary"> {t('text_utility-color_disabled_description')} </Text> </HelpMark> From 5397e9f2475b3770ee3c7dbc6718385ed1ff1b2c Mon Sep 17 00:00:00 2001 From: aobityutskiy <aobityutskiy@yandex-team.ru> Date: Mon, 8 Dec 2025 14:55:05 +0300 Subject: [PATCH 5/5] feat: update themer lib; add new private colors; fix styles --- package-lock.json | 8 +- package.json | 2 +- src/components/Themes/lib/constants.ts | 77 +++---------------- .../ui/AdvancedSettingsTable/columns.tsx | 10 +-- .../Themes/ui/AdvancedSettingsTable/hooks.tsx | 4 +- .../Themes/ui/ColorPreview/ColorPreview.scss | 4 + .../Themes/ui/ColorPreview/ColorPreview.tsx | 10 ++- .../GravityColorSelect/GravityColorSelect.tsx | 22 ++++-- .../Themes/ui/MainSettings/MainSettings.tsx | 30 ++++++-- 9 files changed, 70 insertions(+), 97 deletions(-) diff --git a/package-lock.json b/package-lock.json index e94afdf2de2f..df7e5bd8b308 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,7 @@ "@gravity-ui/navigation": "^3.10.1", "@gravity-ui/page-constructor": "^6.0.0-beta.6", "@gravity-ui/uikit": "^7.26.2", - "@gravity-ui/uikit-themer": "^1.5.0", + "@gravity-ui/uikit-themer": "^1.6.1", "@mdx-js/mdx": "^2.3.0", "@mdx-js/react": "^2.3.0", "@monaco-editor/react": "^4.6.0", @@ -3630,9 +3630,9 @@ } }, "node_modules/@gravity-ui/uikit-themer": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@gravity-ui/uikit-themer/-/uikit-themer-1.5.0.tgz", - "integrity": "sha512-kdxJgmmtjb4oOdBPgt0Vges5Uh4eJ/YUd4RkHbTiXv09YxiNvOtNR9tdzVc/uPX52WL5BRyiXGDuexZyiaaKyg==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@gravity-ui/uikit-themer/-/uikit-themer-1.6.1.tgz", + "integrity": "sha512-YPQjAU4FzWSAvaTlcXvi8YAieuWwf0JvrhM548Uwq63b+WMqZJMRcJyXwxObl8Gy+AVYPT2/yzAzZuWyq2lhMw==", "dependencies": { "chroma-js": "^3.1.2", "lodash-es": "^4.17.21" diff --git a/package.json b/package.json index a92e496b3dc4..0d29b006eea1 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "@gravity-ui/navigation": "^3.10.1", "@gravity-ui/page-constructor": "^6.0.0-beta.6", "@gravity-ui/uikit": "^7.26.2", - "@gravity-ui/uikit-themer": "^1.5.0", + "@gravity-ui/uikit-themer": "^1.6.1", "@mdx-js/mdx": "^2.3.0", "@mdx-js/react": "^2.3.0", "@monaco-editor/react": "^4.6.0", diff --git a/src/components/Themes/lib/constants.ts b/src/components/Themes/lib/constants.ts index b97127bfe55d..f8760c059e38 100644 --- a/src/components/Themes/lib/constants.ts +++ b/src/components/Themes/lib/constants.ts @@ -1,4 +1,4 @@ -import type {BordersOptions, GravityTheme, Theme} from '@gravity-ui/uikit-themer'; +import type {BordersOptions, GravityTheme} from '@gravity-ui/uikit-themer'; import { DEFAULT_THEME as DEFAULT_GRAVITY_THEME, createInternalPrivateColorReference, @@ -22,17 +22,6 @@ export const DEFAULT_BRAND_COLORS = [ 'rgb(255 92 92)', ] as const; -export const TEXT_CONTRAST_COLORS: Record<Theme, {white: string; black: string}> = { - dark: { - white: 'rgb(255 255 255)', - black: 'rgba(0 0 0 / 0.9)', // --g-color-private-black-900 - }, - light: { - white: 'rgb(255 255 255)', - black: 'rgba(0 0 0 / 0.85)', // --g-color-private-black-850 - }, -}; - export const DEFAULT_PALETTE: GravityTheme['baseColors'] = { ...DEFAULT_GRAVITY_THEME.baseColors, brand: { @@ -348,14 +337,6 @@ export const DEFAULT_COLORS: GravityTheme['utilityColors'] = { value: createInternalPrivateColorReference('brand', '700-solid'), }, }, - 'text-brand-contrast': { - light: { - value: TEXT_CONTRAST_COLORS.light.black, - }, - dark: { - value: TEXT_CONTRAST_COLORS.dark.black, - }, - }, 'text-link': { light: { value: createInternalPrivateColorReference('brand', '600-solid'), @@ -473,14 +454,6 @@ export const BRAND_COLORS_PRESETS: BrandPreset[] = [ value: createInternalPrivateColorReference('brand', '700-solid'), }, }, - 'text-brand-contrast': { - light: { - value: TEXT_CONTRAST_COLORS.light.black, - }, - dark: { - value: TEXT_CONTRAST_COLORS.dark.black, - }, - }, 'text-link': { light: { value: createInternalPrivateColorReference('brand', '600-solid'), @@ -582,14 +555,6 @@ export const BRAND_COLORS_PRESETS: BrandPreset[] = [ value: createInternalPrivateColorReference('brand', '700-solid'), }, }, - 'text-brand-contrast': { - light: { - value: TEXT_CONTRAST_COLORS.light.white, - }, - dark: { - value: TEXT_CONTRAST_COLORS.dark.white, - }, - }, 'text-link': { light: { value: createInternalPrivateColorReference('brand', '600-solid'), @@ -691,14 +656,6 @@ export const BRAND_COLORS_PRESETS: BrandPreset[] = [ value: createInternalPrivateColorReference('brand', '700-solid'), }, }, - 'text-brand-contrast': { - light: { - value: TEXT_CONTRAST_COLORS.light.white, - }, - dark: { - value: TEXT_CONTRAST_COLORS.dark.white, - }, - }, 'text-link': { light: { value: createInternalPrivateColorReference('brand', '600-solid'), @@ -800,14 +757,14 @@ export const BRAND_COLORS_PRESETS: BrandPreset[] = [ value: createInternalPrivateColorReference('brand', '700-solid'), }, }, - 'text-brand-contrast': { - light: { - value: TEXT_CONTRAST_COLORS.light.white, - }, - dark: { - value: TEXT_CONTRAST_COLORS.dark.white, - }, - }, + // 'text-brand-contrast': { + // light: { + // value: TEXT_CONTRAST_COLORS.light.white, + // }, + // dark: { + // value: TEXT_CONTRAST_COLORS.dark.white, + // }, + // }, 'text-link': { light: { value: createInternalPrivateColorReference('brand', '600-solid'), @@ -909,14 +866,6 @@ export const BRAND_COLORS_PRESETS: BrandPreset[] = [ value: createInternalPrivateColorReference('brand', '700-solid'), }, }, - 'text-brand-contrast': { - light: { - value: TEXT_CONTRAST_COLORS.light.black, - }, - dark: { - value: TEXT_CONTRAST_COLORS.dark.black, - }, - }, 'text-link': { light: { value: createInternalPrivateColorReference('brand', '600-solid'), @@ -1018,14 +967,6 @@ export const BRAND_COLORS_PRESETS: BrandPreset[] = [ value: createInternalPrivateColorReference('brand', '700-solid'), }, }, - 'text-brand-contrast': { - light: { - value: TEXT_CONTRAST_COLORS.light.white, - }, - dark: { - value: TEXT_CONTRAST_COLORS.dark.white, - }, - }, 'text-link': { light: { value: createInternalPrivateColorReference('brand', '600-solid'), diff --git a/src/components/Themes/ui/AdvancedSettingsTable/columns.tsx b/src/components/Themes/ui/AdvancedSettingsTable/columns.tsx index 930b85686517..de0a2d8dbd5c 100644 --- a/src/components/Themes/ui/AdvancedSettingsTable/columns.tsx +++ b/src/components/Themes/ui/AdvancedSettingsTable/columns.tsx @@ -33,15 +33,13 @@ const PaletteThemeValueColumn = ({colorName, theme, value}: ColumnProps) => { value={color.value} defaultValue={value ?? '#000000'} onChange={setColor} + size="s" view="clear" /> ); }; -const utilityColorsForDefaultColorPicker: UtilityColor[] = [ - 'base-background', - 'text-brand-contrast', -]; +const utilityColorsForDefaultColorPicker: UtilityColor[] = ['base-background']; const UtilityThemeValueColumn = ({ colorName, @@ -65,6 +63,7 @@ const UtilityThemeValueColumn = ({ value={color} defaultValue={value ?? '#000000'} onChange={setColor} + size="s" view="clear" /> ); @@ -77,7 +76,8 @@ const UtilityThemeValueColumn = ({ defaultValue={value ?? '#000000'} value={color} onChange={setColor} - inputView="clear" + inputProps={{view: 'clear', size: 's'}} + buttonProps={{size: 's'}} /> ); }; diff --git a/src/components/Themes/ui/AdvancedSettingsTable/hooks.tsx b/src/components/Themes/ui/AdvancedSettingsTable/hooks.tsx index 5bc87f37bf22..45e2aea0d5a5 100644 --- a/src/components/Themes/ui/AdvancedSettingsTable/hooks.tsx +++ b/src/components/Themes/ui/AdvancedSettingsTable/hooks.tsx @@ -56,7 +56,7 @@ export const useColumns = ({colorType}: {colorType: AdvancedColorType}): Column[ <VariableColumn name={colorName} extraVariable={extraVariable} /> ), }), - [t], + [t, colorType], ); const tabletColumns: Column[] = useMemo( @@ -74,7 +74,7 @@ export const useColumns = ({colorType}: {colorType: AdvancedColorType}): Column[ ), }, ], - [variableColumn, theme], + [variableColumn, toggleTheme, theme], ); const desktopColumns: Column[] = useMemo( diff --git a/src/components/Themes/ui/ColorPreview/ColorPreview.scss b/src/components/Themes/ui/ColorPreview/ColorPreview.scss index 3ef56f7f9baf..693c58eccff7 100644 --- a/src/components/Themes/ui/ColorPreview/ColorPreview.scss +++ b/src/components/Themes/ui/ColorPreview/ColorPreview.scss @@ -32,4 +32,8 @@ $block: '.#{variables.$ns}color-preview'; background: var(--opacity-pattern); } } + + &_with-borders { + border: 1px solid var(--g-color-line-generic); + } } diff --git a/src/components/Themes/ui/ColorPreview/ColorPreview.tsx b/src/components/Themes/ui/ColorPreview/ColorPreview.tsx index 235b8c69100e..af270be9531f 100644 --- a/src/components/Themes/ui/ColorPreview/ColorPreview.tsx +++ b/src/components/Themes/ui/ColorPreview/ColorPreview.tsx @@ -7,15 +7,21 @@ import './ColorPreview.scss'; export interface ColorPreviewProps { color?: string; className?: string; + withBorders?: boolean; } const b = block('color-preview'); const isColorWithOpacity = (color?: string) => !color || color?.startsWith('rgba'); -export const ColorPreview = ({color, className}: ColorPreviewProps) => { +export const ColorPreview = ({color, className, withBorders}: ColorPreviewProps) => { return ( - <div className={b({'with-opacity': isColorWithOpacity(color)}, className)}> + <div + className={b( + {'with-opacity': isColorWithOpacity(color), 'with-borders': withBorders}, + className, + )} + > <div className={b('color')} style={{backgroundColor: color}} /> </div> ); diff --git a/src/components/Themes/ui/GravityColorSelect/GravityColorSelect.tsx b/src/components/Themes/ui/GravityColorSelect/GravityColorSelect.tsx index 93ac16b8b3a2..582f0147b89d 100644 --- a/src/components/Themes/ui/GravityColorSelect/GravityColorSelect.tsx +++ b/src/components/Themes/ui/GravityColorSelect/GravityColorSelect.tsx @@ -1,6 +1,7 @@ import {ChevronDown, PencilToLine} from '@gravity-ui/icons'; import { Button, + type ButtonProps, Flex, Icon, Popup, @@ -34,7 +35,8 @@ interface GravityColorSelectProps { onChange: (color: string, ref?: string) => void; privateGroups: ColorGroup[]; semanticGroups?: SemanticColorGroup[]; - inputView?: TextInputProps['view']; + inputProps?: Pick<TextInputProps, 'size' | 'view'>; + buttonProps?: Pick<ButtonProps, 'size'>; } export const GravityColorSelect = ({ @@ -43,7 +45,8 @@ export const GravityColorSelect = ({ value, defaultValue, onChange, - inputView = 'normal', + inputProps, + buttonProps, }: GravityColorSelectProps) => { const isMobile = useIsMobile(); @@ -117,18 +120,23 @@ export const GravityColorSelect = ({ {isCustomValue ? ( <ColorPickerInput value={value} + size={inputProps?.size} defaultValue={value || ''} onChange={onChange} - view={inputView} + view={inputProps?.view} /> ) : ( <TextInput className={b('input')} value={selectedColor?.title || ''} - view={inputView} - size="l" + view={inputProps?.view} + size={inputProps?.size ?? 'l'} startContent={ - <ColorPreview className={b('preview')} color={selectedColor?.color} /> + <ColorPreview + className={b('preview')} + color={selectedColor?.color} + withBorders + /> } endContent={ <Flex gap={1}> @@ -149,7 +157,7 @@ export const GravityColorSelect = ({ )} <Button className={b('customize-button')} - size="l" + size={buttonProps?.size ?? 'l'} view="flat" onClick={switchMode} selected={isCustomValue} diff --git a/src/components/Themes/ui/MainSettings/MainSettings.tsx b/src/components/Themes/ui/MainSettings/MainSettings.tsx index c5df14d4faa8..9486ece5a189 100644 --- a/src/components/Themes/ui/MainSettings/MainSettings.tsx +++ b/src/components/Themes/ui/MainSettings/MainSettings.tsx @@ -1,13 +1,12 @@ import {Sliders} from '@gravity-ui/icons'; import {Button, Flex, Icon, Text} from '@gravity-ui/uikit'; -import type {Theme, UtilityColor} from '@gravity-ui/uikit-themer'; +import {DEFAULT_THEME, type Theme, type UtilityColor} from '@gravity-ui/uikit-themer'; import {useTranslation} from 'next-i18next'; import React from 'react'; import {block} from '../../../../utils'; import {SelectableCard} from '../../../SelectableCard/SelectableCard'; import {useThemePaletteColor, useThemeUtilityColor} from '../../hooks'; -import {TEXT_CONTRAST_COLORS} from '../../lib/constants'; import {ColorPickerInput} from '../ColorPickerInput/ColorPickerInput'; import {ThemableSettings} from '../ThemableSettings/ThemableSettings'; import {ThemableRow} from '../ThemableSettings/types'; @@ -62,6 +61,11 @@ const BrandColorEditor: React.FC<{theme: Theme}> = ({theme}) => { ); }; +const textBrandContrastDefaults = { + light: DEFAULT_THEME.utilityColors['text-brand-contrast'].light, + dark: DEFAULT_THEME.utilityColors['text-brand-contrast'].dark, +}; + const TextContrastColorEditor: React.FC<{theme: Theme}> = ({theme}) => { const [brandTextColor, setBrandTextColor] = useThemeUtilityColor({ name: 'text-brand-contrast', @@ -75,12 +79,17 @@ const TextContrastColorEditor: React.FC<{theme: Theme}> = ({theme}) => { <SelectableCard className={b('text-card')} text="Black text" - selected={brandTextColor === TEXT_CONTRAST_COLORS[theme].black} - onClick={() => setBrandTextColor(TEXT_CONTRAST_COLORS[theme].black)} + selected={brandTextColor === textBrandContrastDefaults.light.ref} + onClick={() => + setBrandTextColor( + textBrandContrastDefaults.light.value, + textBrandContrastDefaults.light.ref, + ) + } textProps={{ style: { ...BASE_CARD_BUTTON_STYLES, - color: TEXT_CONTRAST_COLORS[theme].black, + color: textBrandContrastDefaults.light.value, backgroundColor: brandColor.value, }, }} @@ -88,12 +97,17 @@ const TextContrastColorEditor: React.FC<{theme: Theme}> = ({theme}) => { <SelectableCard className={b('text-card')} text="White text" - selected={brandTextColor === TEXT_CONTRAST_COLORS[theme].white} - onClick={() => setBrandTextColor(TEXT_CONTRAST_COLORS[theme].white)} + selected={brandTextColor === textBrandContrastDefaults.dark.ref} + onClick={() => + setBrandTextColor( + textBrandContrastDefaults.dark.value, + textBrandContrastDefaults.dark.ref, + ) + } textProps={{ style: { ...BASE_CARD_BUTTON_STYLES, - color: TEXT_CONTRAST_COLORS[theme].white, + color: textBrandContrastDefaults.dark.value, backgroundColor: brandColor.value, }, }}