From 6bf061ec756fbbb149e90df69cb12fc7f52cc50b Mon Sep 17 00:00:00 2001 From: aobityutskiy Date: Wed, 5 Nov 2025 19:56:58 +0300 Subject: [PATCH 1/3] feat: add advance settings for colors in themer page --- package-lock.json | 8 +- package.json | 2 +- public/locales/en/themes.json | 171 +++++++++- public/locales/ru/themes.json | 171 +++++++++- .../hooks/useThemeSemanticColorOption.tsx | 168 ++++++++++ .../Themes/hooks/useThemeUtilityColor.ts | 3 +- src/components/Themes/lib/constants.ts | 303 +++++++++++++----- .../Themes/lib/themeCreatorContext.ts | 4 +- .../Themes/lib/themeCreatorUtils.ts | 15 +- src/components/Themes/lib/types.ts | 47 ++- src/components/Themes/lib/utils.ts | 32 ++ .../AddExtraColor/AddExtraColor.scss | 13 + .../AddExtraColor/AddExtraColor.tsx | 35 ++ .../AdvancedSettingsTable.scss | 38 +++ .../AdvancedSettingsTable.tsx | 104 ++++++ .../ExtraColorName/ExtraColorName.scss | 9 + .../ExtraColorName/ExtraColorName.tsx | 75 +++++ .../ui/AdvancedSettingsTable/columns.tsx | 171 ++++++++++ .../Themes/ui/AdvancedSettingsTable/hooks.tsx | 106 ++++++ .../Themes/ui/BrandColors/BrandColors.scss | 5 +- .../Themes/ui/BrandColors/BrandColors.tsx | 88 ++--- .../ui/ColorPickerInput/ColorPickerInput.scss | 4 - .../ui/ColorPickerInput/ColorPickerInput.tsx | 7 +- .../Themes/ui/ColorPreview/ColorPreview.scss | 4 + .../Themes/ui/ColorPreview/ColorPreview.tsx | 10 +- .../AdvancedSettings/AdvancedSettings.scss | 9 + .../AdvancedSettings/AdvancedSettings.tsx | 74 +++++ .../BasicSettings/BasicSettings.scss | 19 ++ .../ColorsTab/BasicSettings/BasicSettings.tsx | 124 +++++++ .../Themes/ui/ColorsTab/ColorsTab.tsx | 102 +----- .../ColorSelectPopupContent.scss} | 15 +- .../ColorSelectPopupContent.tsx | 137 ++++++++ .../ColorSelectPopupContentItems.tsx | 248 ++++++++++++++ .../GravityColorSelect.scss} | 0 .../GravityColorSelect.tsx} | 110 +++++-- .../Themes/ui/GravityColorSelect/index.ts | 1 + .../types.ts | 0 .../Themes/ui/GravityColorSelect/utils.ts | 38 +++ .../Themes/ui/MainSettings/MainSettings.tsx | 30 +- .../PrivateColorSelectPopupContent.tsx | 161 ---------- .../Themes/ui/PrivateColorSelect/index.ts | 1 - .../PrivateColorsSettings.tsx | 6 +- .../Themes/ui/ThemeCreatorContextProvider.tsx | 22 +- src/components/Themes/ui/ThemeSection.tsx | 11 +- 44 files changed, 2254 insertions(+), 447 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/AdvancedSettingsTable/columns.tsx create mode 100644 src/components/Themes/ui/AdvancedSettingsTable/hooks.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 rename src/components/Themes/ui/{PrivateColorSelect/PrivateColorSelectPopupContent.scss => GravityColorSelect/ColorSelectPopupContent.scss} (83%) 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} (56%) 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 e210f5a46bb9..d0d42d70d993 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,7 @@ "@gravity-ui/navigation": "^3.11.0", "@gravity-ui/page-constructor": "^6.0.0-beta.6", "@gravity-ui/uikit": "^7.28.0", - "@gravity-ui/uikit-themer": "^1.4.1", + "@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", @@ -3631,9 +3631,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.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 5a683d17a83e..33e5fc256902 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "@gravity-ui/navigation": "^3.11.0", "@gravity-ui/page-constructor": "^6.0.0-beta.6", "@gravity-ui/uikit": "^7.28.0", - "@gravity-ui/uikit-themer": "^1.4.1", + "@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/public/locales/en/themes.json b/public/locales/en/themes.json index 3475cf0a1217..48d73791d851 100644 --- a/public/locales/en/themes.json +++ b/public/locales/en/themes.json @@ -19,6 +19,33 @@ "palette_colors_description": "Support Colors for various cases and
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-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", + "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 +68,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 +109,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..1b91944807bc 100644 --- a/public/locales/ru/themes.json +++ b/public/locales/ru/themes.json @@ -19,6 +19,33 @@ "palette_colors_description": "Оттенки для различных сценариев и состояний", "dark_theme": "Тёмная тема", "light_theme": "Светлая тема", + "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": "Базовый", + "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 +68,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 +109,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 new file mode 100644 index 000000000000..0a6342efee74 --- /dev/null +++ b/src/components/Themes/hooks/useThemeSemanticColorOption.tsx @@ -0,0 +1,168 @@ +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, + createInternalUtilityColorReference, + createUtilityColorCssVariable, + isInternalUtilityColorReference, + isUtilityColorToken, + parseInternalUtilityColorReference, +} from '@gravity-ui/uikit-themer'; +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/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: SemanticColorGroupItem[]; + }[]; +}; + +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 ; + default: + return ; + } +}; + +const resolveUtilityColor = ( + state: GravityTheme['utilityColors'], + themeVariant: Theme, + updatedColorToken: string, +) => { + let disabled = false; + + 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 {color: colorObject.value, disabled}; + }; + + return traverse; +}; + +export const useThemeSemanticColorOption = ( + themeVariant: Theme, + updatedColorToken: string, +): SemanticColorGroup[] => { + const {t} = useTranslation('themes'); + 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: t(`title_advance-color-settings-${advanceColorGroupName}`), + groups: Object.entries(advanceColorSubGroups).map( + ([advanceColorSubGroupName, advanceColorSubGroupItems]) => { + return { + title: t( + `title_advance-color-settings-group-${advanceColorSubGroupName}`, + ), + items: advanceColorSubGroupItems.map(({colorName}) => { + const isUtilityColor = isUtilityColorToken(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, + }; + } + }), + }; + }, + ), + }; + }); + }, [gravityTheme, themeVariant, updatedColorToken, t]); +}; 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 1c49ae968646..f8760c059e38 100644 --- a/src/components/Themes/lib/constants.ts +++ b/src/components/Themes/lib/constants.ts @@ -1,13 +1,15 @@ -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, } 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 UTILITY_COLOR_PREFIX = '--g-color-'; export const DEFAULT_NEW_COLOR_TITLE = 'New color'; @@ -20,17 +22,6 @@ export const DEFAULT_BRAND_COLORS = [ 'rgb(255 92 92)', ] as const; -export const TEXT_CONTRAST_COLORS: Record = { - 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: { @@ -72,6 +63,212 @@ 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'), + getDefaultAdvancedColorValue('text-brand-contrast'), + { + 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'), + ], + }, +}; + // 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 +283,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': { @@ -140,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'), @@ -203,10 +392,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': { @@ -265,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'), @@ -312,10 +493,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': { @@ -374,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'), @@ -421,10 +594,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': { @@ -483,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'), @@ -592,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'), @@ -701,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'), @@ -810,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/lib/themeCreatorContext.ts b/src/components/Themes/lib/themeCreatorContext.ts index 9732fe33872e..8d40fd25fe7f 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 {ColorsSettingsType, ThemeCreatorState} from './types'; export const ThemeCreatorContext = createContext( initThemeCreator(DEFAULT_THEME), @@ -40,6 +40,7 @@ export interface ThemeCreatorMethodsContextType { openMainSettings: () => void; setAdvancedMode: (enabled: boolean) => void; importTheme: (theme: GravityTheme) => void; + setColorsSettingsType: (type: ColorsSettingsType) => void; } export const ThemeCreatorMethodsContext = createContext({ @@ -60,4 +61,5 @@ 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..e9ebfbd923aa --- /dev/null +++ b/src/components/Themes/lib/utils.ts @@ -0,0 +1,32 @@ +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'; + +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 ''; +}; + +export const getColorName = (colorToken: string) => { + if (DEFAULT_GRAVITY_THEME['baseColors'][colorToken]) { + return capitalize(colorToken); + } + + if (colorToken === 'brand') { + return 'Brand Color'; + } + + return colorToken; +}; 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..381b916a6311 --- /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..178d4c8005ac --- /dev/null +++ b/src/components/Themes/ui/AdvancedSettingsTable/AdvancedSettingsTable.scss @@ -0,0 +1,38 @@ +@use '../../../../variables.scss'; +@use '~@gravity-ui/page-constructor/styles/variables.scss' as pcVariables; + +$block: '.#{variables.$ns}advanced-color-settings-table'; + +#{$block} { + 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-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); + } + + &_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; + } + + &__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 new file mode 100644 index 000000000000..29bdd3679be1 --- /dev/null +++ b/src/components/Themes/ui/AdvancedSettingsTable/AdvancedSettingsTable.tsx @@ -0,0 +1,104 @@ +import {Text} from '@gravity-ui/uikit'; +import {useTranslation} from 'next-i18next'; +import {Fragment} from 'react'; + +import {block} from '../../../../utils'; +import {DEFAULT_ADVANCED_COLORS} from '../../lib/constants'; +import type {AdvancedColorType} from '../../lib/types'; + +import {AddExtraColor} from './AddExtraColor/AddExtraColor'; +import './AdvancedSettingsTable.scss'; +import {useColumns, useExtraColors} from './hooks'; + +const b = block('advanced-color-settings-table'); + +export interface AdvancedSettingsTableProps { + colorType: AdvancedColorType; +} + +export const AdvancedSettingsTable = ({colorType}: AdvancedSettingsTableProps) => { + const {t} = useTranslation('themes'); + const extraColors = useExtraColors(); + const columns = useColumns({colorType}); + + return ( + + + + {columns.map(({title: Title}, index) => ( +
+ + </th> + ))} + </tr> + </thead> + <tbody className={b('body')}> + {Object.entries(DEFAULT_ADVANCED_COLORS[colorType]).map(([group, variables]) => { + return ( + <Fragment key={group}> + <tr className={b('row')}> + {columns.map(({key}) => ( + <td + className={b('cell', {group: true})} + key={`${group}-${key}`} + > + {key === 'variable' ? ( + <Text variant="subheader-1"> + {t(`title_advance-color-settings-group-${group}`)} + </Text> + ) : ( + '' + )} + </td> + ))} + </tr> + + {colorType === 'basic-palette' && group === 'extra-color' && ( + <Fragment> + {Object.entries(extraColors).map(([colorName, value]) => ( + <tr className={b('row')} key={colorName}> + {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')} key={colorName}> + {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..5636fe0e5302 --- /dev/null +++ b/src/components/Themes/ui/AdvancedSettingsTable/ExtraColorName/ExtraColorName.tsx @@ -0,0 +1,75 @@ +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 {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="primary"> + {token} + </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/AdvancedSettingsTable/columns.tsx b/src/components/Themes/ui/AdvancedSettingsTable/columns.tsx new file mode 100644 index 000000000000..1f5d248effc6 --- /dev/null +++ b/src/components/Themes/ui/AdvancedSettingsTable/columns.tsx @@ -0,0 +1,171 @@ +import {Moon, Sun} from '@gravity-ui/icons'; +import {Flex, HelpMark, Icon, SegmentedRadioGroup, Text} from '@gravity-ui/uikit'; +import { + type Theme, + type UtilityColor, + createInternalUtilityColorReference, + createUtilityColorCssVariable, + isUtilityColorToken, +} 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'; +import {ColorPickerInput} from '../ColorPickerInput/ColorPickerInput'; +import {GravityColorSelect} from '../GravityColorSelect'; + +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, + theme, + }); + + return ( + <ColorPickerInput + value={color.value} + defaultValue={value ?? '#000000'} + onChange={setColor} + withBorderInPreview + size="s" + view="clear" + /> + ); +}; + +const utilityColorsForDefaultColorPicker: UtilityColor[] = ['base-background']; + +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 (utilityColorsForDefaultColorPicker.includes(colorName)) { + return ( + <ColorPickerInput + value={color} + defaultValue={value ?? '#000000'} + onChange={setColor} + withBorderInPreview + size="s" + view="clear" + /> + ); + } + + return ( + <GravityColorSelect + privateGroups={themePrivateColorOptions} + semanticGroups={themeSemanticColorOptions} + defaultValue={value ?? '#000000'} + value={color} + onChange={setColor} + inputProps={{view: 'clear', size: 's'}} + buttonProps={{size: 's'}} + /> + ); +}; + +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} />; +}; + +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`); + + 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> + ); +}; + +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> + ); +}; + +export {ThemeValueColumn, TitleColumn, VariableColumn, ThemeToggleTitleColumn}; diff --git a/src/components/Themes/ui/AdvancedSettingsTable/hooks.tsx b/src/components/Themes/ui/AdvancedSettingsTable/hooks.tsx new file mode 100644 index 000000000000..45e2aea0d5a5 --- /dev/null +++ b/src/components/Themes/ui/AdvancedSettingsTable/hooks.tsx @@ -0,0 +1,106 @@ +import {BREAKPOINTS, useWindowBreakpoint} from '@gravity-ui/page-constructor'; +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 {ThemeToggleTitleColumn, 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 = ({colorType}: {colorType: AdvancedColorType}): 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={ + 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} /> + ), + }), + [t, colorType], + ); + + 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, toggleTheme, theme], + ); + + const desktopColumns: Column[] = 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} /> + ), + }, + ], + [variableColumn, t], + ); + + if (isTablet) { + return tabletColumns; + } + + return desktopColumns; +}; 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.scss b/src/components/Themes/ui/ColorPickerInput/ColorPickerInput.scss index bce7eb6580e7..a03198444ec8 100644 --- a/src/components/Themes/ui/ColorPickerInput/ColorPickerInput.scss +++ b/src/components/Themes/ui/ColorPickerInput/ColorPickerInput.scss @@ -14,10 +14,6 @@ $block: '.#{variables.$ns}color-picker'; &__preview { margin-inline-start: var(--g-spacing-2); margin-inline-end: var(--g-spacing-1); - - &_with-border { - border: 1px solid var(--g-color-line-generic); - } } &__input { diff --git a/src/components/Themes/ui/ColorPickerInput/ColorPickerInput.tsx b/src/components/Themes/ui/ColorPickerInput/ColorPickerInput.tsx index 67f732953b41..278e479d5deb 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,12 +127,13 @@ export const ColorPickerInput = ({ errorPlacement="inside" errorMessage={errorMessage || t('color-input_validation-format-error')} validationState={validationError} - view="normal" + view={view} size={size} onChange={onChange} startContent={ <ColorPreview - className={b('preview', {'with-border': withBorderInPreview})} + className={b('preview')} + withBorders={withBorderInPreview} color={color} /> } 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/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..63c8a5f45c82 --- /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: t('title_advance-color-settings-basic-palette'), + }, + { + value: 'brand-summary', + title: t('title_advance-color-settings-brand-summary'), + }, + { + value: 'texts', + title: t('title_advance-color-settings-texts'), + }, + { + value: 'backgrounds', + title: t('title_advance-color-settings-backgrounds'), + }, + { + value: 'lines', + title: t('title_advance-color-settings-lines'), + }, + { + value: 'effects', + title: t('title_advance-color-settings-effects'), + }, + { + value: 'misc', + title: t('title_advance-color-settings-misc'), + }, + ], + [t], + ); + + return ( + <ThemeSection + title="Colors setup" + titleActions={ + <Button size="xl" className={b('learn-more-button')} disabled> + <Icon data={CircleQuestion} /> + {t('action_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/PrivateColorSelectPopupContent.scss b/src/components/Themes/ui/GravityColorSelect/ColorSelectPopupContent.scss similarity index 83% rename from src/components/Themes/ui/PrivateColorSelect/PrivateColorSelectPopupContent.scss rename to src/components/Themes/ui/GravityColorSelect/ColorSelectPopupContent.scss index 105592cb9b5a..bb098ded9744 100644 --- a/src/components/Themes/ui/PrivateColorSelect/PrivateColorSelectPopupContent.scss +++ b/src/components/Themes/ui/GravityColorSelect/ColorSelectPopupContent.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,12 @@ $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); + + &_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..d16a3363fd6c --- /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, + parseInternalUtilityColorReference, +} from '@gravity-ui/uikit-themer'; +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={(item) => { + setCurrentGroupToken(item.token); + 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={(item) => onChange(item.color, item.token)} + /> + )} + </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..bab0fba4a389 --- /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, + parseInternalUtilityColorReference, +} from '@gravity-ui/uikit-themer'; +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: (item: BaseColor) => 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); + }, + [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="body-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 56% rename from src/components/Themes/ui/PrivateColorSelect/PrivateColorSelect.tsx rename to src/components/Themes/ui/GravityColorSelect/GravityColorSelect.tsx index dcd8091c5448..bdff259299aa 100644 --- a/src/components/Themes/ui/PrivateColorSelect/PrivateColorSelect.tsx +++ b/src/components/Themes/ui/GravityColorSelect/GravityColorSelect.tsx @@ -1,41 +1,63 @@ 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, + type ButtonProps, + Flex, + Icon, + Popup, + Sheet, + TextInput, + type TextInputProps, + ThemeProvider, +} from '@gravity-ui/uikit'; +import { + isInternalPrivateColorReference, + isInternalUtilityColorReference, + parseInternalUtilityColorReference, +} from '@gravity-ui/uikit-themer'; 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 {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; - groups: ColorGroup[]; + onChange: (color: string, ref?: string) => void; + privateGroups: ColorGroup[]; + semanticGroups?: SemanticColorGroup[]; + inputProps?: Pick<TextInputProps, 'size' | 'view'>; + buttonProps?: Pick<ButtonProps, 'size'>; } -export const PrivateColorSelect: React.FC<PrivateColorSelectProps> = ({ - groups, +export const GravityColorSelect = ({ + privateGroups, + semanticGroups, value, defaultValue, onChange, -}) => { + inputProps, + buttonProps, +}: GravityColorSelectProps) => { 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) => { - onChange(newVal); + (newVal: string, newRef?: string) => { + onChange(newVal, newRef); setShowPopup(false); }, [onChange], @@ -50,15 +72,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 +118,26 @@ 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} + withBorderInPreview + size={inputProps?.size} + defaultValue={value || ''} + onChange={onChange} + view={inputProps?.view} + /> ) : ( <TextInput className={b('input')} - value={privateColor?.title || ''} - view="normal" - size="l" + value={selectedColor?.title || ''} + view={inputProps?.view} + size={inputProps?.size ?? 'l'} startContent={ - <ColorPreview className={b('preview')} color={privateColor?.color} /> + <ColorPreview + className={b('preview')} + color={selectedColor?.color} + withBorders + /> } endContent={ <Flex gap={1}> @@ -103,7 +158,7 @@ export const PrivateColorSelect: React.FC<PrivateColorSelectProps> = ({ )} <Button className={b('customize-button')} - size="l" + size={buttonProps?.size ?? 'l'} view="flat" onClick={switchMode} selected={isCustomValue} @@ -118,8 +173,8 @@ export const PrivateColorSelect: React.FC<PrivateColorSelectProps> = ({ visible={showPopup} onClose={closePopup} > - <PrivateColorSelectPopupContent - groups={groups} + <ColorSelectPopupContent + privateGroups={privateGroups} value={value} onChange={handleChange} version="mobile" @@ -136,8 +191,9 @@ export const PrivateColorSelect: React.FC<PrivateColorSelectProps> = ({ } }} > - <PrivateColorSelectPopupContent - groups={groups} + <ColorSelectPopupContent + privateGroups={privateGroups} + semanticGroups={semanticGroups} value={value} onChange={handleChange} /> 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..f543e5a7e2f7 --- /dev/null +++ b/src/components/Themes/ui/GravityColorSelect/utils.ts @@ -0,0 +1,38 @@ +import { + parseInternalPrivateColorReference, + parseInternalUtilityColorReference, +} from '@gravity-ui/uikit-themer'; + +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/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, }, }} diff --git a/src/components/Themes/ui/PrivateColorSelect/PrivateColorSelectPopupContent.tsx b/src/components/Themes/ui/PrivateColorSelect/PrivateColorSelectPopupContent.tsx deleted file mode 100644 index 23ed018f3054..000000000000 --- a/src/components/Themes/ui/PrivateColorSelect/PrivateColorSelectPopupContent.tsx +++ /dev/null @@ -1,161 +0,0 @@ -import {List, Select, SelectOption, Text} from '@gravity-ui/uikit'; -import {parseInternalPrivateColorReference} from '@gravity-ui/uikit-themer'; -import React from 'react'; - -import {block} from '../../../../utils'; -import {ColorPreview} from '../ColorPreview/ColorPreview'; - -import './PrivateColorSelectPopupContent.scss'; -import type {BaseColor, ColorGroup} from './types'; - -const b = block('private-colors-select-popup'); - -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 PrivateColorSelectPopupContentProps { - groups: ColorGroup[]; - value?: string; - onChange: (token: string) => void; - version?: 'mobile' | 'desktop'; -} - -export const PrivateColorSelectPopupContent: React.FC<PrivateColorSelectPopupContentProps> = ({ - value, - groups, - onChange, - version = 'desktop', -}) => { - const colorsRef = React.useRef<HTMLDivElement>(null); - - const [currentGroupToken, setCurrentGroupToken] = React.useState<string | undefined>(() => - value ? parseInternalPrivateColorReference(value)?.mainColorToken : undefined, - ); - - React.useEffect(() => { - const mainColorToken = value - ? parseInternalPrivateColorReference(value)?.mainColorToken - : undefined; - - if (mainColorToken) { - setCurrentGroupToken(mainColorToken); - } - }, [value]); - - const groupToken = currentGroupToken || groups[0].token; - - React.useEffect(() => { - colorsRef.current?.scrollTo({ - top: 0, - behavior: 'smooth', - }); - }, [groupToken]); - - const groupPrivateColors = React.useMemo( - () => groups.find(({token}) => token === groupToken)?.privateColors || [], - [groups, groupToken], - ); - - return ( - <div className={b({version})}> - <div className={b('left')}> - <ColorsList - colors={groups} - value={groupToken} - onSelect={setCurrentGroupToken} - view={version === 'mobile' ? 'select' : 'list'} - /> - </div> - <div className={b('right')} ref={colorsRef}> - <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 d596cc54b99a..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,8 +46,8 @@ const PrivateColorEditor: React.FC<PrivateColorEditorProps> = ({name, theme, col }, [name, theme]); return ( - <PrivateColorSelect - groups={colorGroups} + <GravityColorSelect + 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..e9b16ac4974d 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 {ColorsSettingsType, ThemeCreatorState} from '../lib/types'; type ThemeCreatorAction = | { @@ -103,6 +103,10 @@ type ThemeCreatorAction = | { type: 'setAdvancedMode'; payload: boolean; + } + | { + type: 'setColorsSettingsType'; + payload: ColorsSettingsType; }; const themeCreatorReducer = ( @@ -155,6 +159,11 @@ const themeCreatorReducer = ( }; case 'reinitialize': return initThemeCreator(action.payload); + case 'setColorsSettingsType': + return { + ...newState, + colorsSettingsType: action.payload, + }; default: return prevState; } @@ -334,6 +343,15 @@ export const ThemeCreatorContextProvider: React.FC<ThemeCreatorProps> = ({ }); }, []); + const setColorsSettingsType = React.useCallback< + ThemeCreatorMethodsContextType['setColorsSettingsType'] + >((payload) => { + dispatchThemeCreator({ + type: 'setColorsSettingsType', + payload, + }); + }, []); + const methods = React.useMemo( () => ({ addColor, @@ -353,6 +371,7 @@ export const ThemeCreatorContextProvider: React.FC<ThemeCreatorProps> = ({ openMainSettings, setAdvancedMode, importTheme, + setColorsSettingsType, }), [ addColor, @@ -372,6 +391,7 @@ export const ThemeCreatorContextProvider: React.FC<ThemeCreatorProps> = ({ openMainSettings, setAdvancedMode, importTheme, + setColorsSettingsType, ], ); 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 7cb2e1e32a3fc0a5f743b5c2514c5b785da5b969 Mon Sep 17 00:00:00 2001 From: aobityutskiy <aobityutskiy@yandex-team.ru> Date: Tue, 9 Dec 2025 19:03:49 +0300 Subject: [PATCH 2/3] fix: remove old state --- src/components/Themes/lib/themeCreatorUtils.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/components/Themes/lib/themeCreatorUtils.ts b/src/components/Themes/lib/themeCreatorUtils.ts index af8625f3a54c..a50516cb6311 100644 --- a/src/components/Themes/lib/themeCreatorUtils.ts +++ b/src/components/Themes/lib/themeCreatorUtils.ts @@ -26,7 +26,6 @@ import lowerCase from 'lodash/lowerCase'; import { BrandPreset, - DEFAULT_ADVANCED_COLORS, DEFAULT_NEW_COLOR_TITLE, DEFAULT_PALETTE_TOKENS, RADIUS_PRESETS, @@ -362,7 +361,6 @@ export function initThemeCreator(inputTheme: GravityTheme): ThemeCreatorState { }, typography: defaultTypographyPreset, colorsSettingsType: 'basic', - advancedColors: DEFAULT_ADVANCED_COLORS, }; } From 2af6e5fa984c80b58cbf90373cf5fc8e90840446 Mon Sep 17 00:00:00 2001 From: aobityutskiy <aobityutskiy@yandex-team.ru> Date: Tue, 9 Dec 2025 19:09:25 +0300 Subject: [PATCH 3/3] fix: circular deps --- src/components/Themes/lib/constants.ts | 1 - src/components/Themes/lib/utils.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/Themes/lib/constants.ts b/src/components/Themes/lib/constants.ts index f8760c059e38..6e76f783c2bf 100644 --- a/src/components/Themes/lib/constants.ts +++ b/src/components/Themes/lib/constants.ts @@ -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 UTILITY_COLOR_PREFIX = '--g-color-'; export const DEFAULT_NEW_COLOR_TITLE = 'New color'; diff --git a/src/components/Themes/lib/utils.ts b/src/components/Themes/lib/utils.ts index e9ebfbd923aa..5b968a9ac606 100644 --- a/src/components/Themes/lib/utils.ts +++ b/src/components/Themes/lib/utils.ts @@ -5,7 +5,7 @@ import { } from '@gravity-ui/uikit-themer'; import capitalize from 'lodash/capitalize'; -import {UTILITY_COLOR_PREFIX} from './constants'; +export const UTILITY_COLOR_PREFIX = '--g-color-'; export const getDefaultAdvancedColorValue = (colorName: UtilityColor) => { return {colorName: colorName, ...DEFAULT_GRAVITY_THEME['utilityColors'][colorName]};