diff --git a/airflow-core/docs/howto/customize-ui.rst b/airflow-core/docs/howto/customize-ui.rst index 3681554f8aee8..557a5ae67d98d 100644 --- a/airflow-core/docs/howto/customize-ui.rst +++ b/airflow-core/docs/howto/customize-ui.rst @@ -61,6 +61,54 @@ After .. image:: ../img/change-site-title/example_instance_name_configuration.png +Customizing side/navbar color +----------------------------- + +We can provide a color to generate and apply a custom color palette for the UI's side/navbar. + +The color is used as the base for generating a cohesive theme that adapts text, backgrounds, and accent colors automatically. + +.. important:: + + - The color must be provided as a six-digit hexadecimal value in the form ``#RRGGBB`` + - Invalid or missing values the theme to the built-in default color scheme. + +.. note:: + + Both pure white ``#ffffff`` and pure black ``#000000`` will generate the same color palette (grayscale). + They have no hue or color saturation. Their only difference is brightness, not color tone. + +To make this change, simply: + +1. Add the configuration option of ``theme`` under the ``[api]`` section inside ``airflow.cfg``: + +.. code-block:: + + [api] + + theme = "#ff0000" + + +2. Alternatively, you can set a custom title using the environment variable: + +.. code-block:: + + AIRFLOW__API__THEME = "#ff0000" + + +Screenshots +^^^^^^^^^^^ + +Light Mode +"""""""""" + +.. image:: ../img/change-theme/exmaple_theme_configuration_light_mode.png + +Dark Mode +""""""""" + +.. image:: ../img/change-theme/exmaple_theme_configuration_dark_mode.png + | Adding Dashboard Alert Messages diff --git a/airflow-core/docs/img/change-theme/exmaple_theme_configuration_dark_mode.png b/airflow-core/docs/img/change-theme/exmaple_theme_configuration_dark_mode.png new file mode 100644 index 0000000000000..52db2fce0a4e3 Binary files /dev/null and b/airflow-core/docs/img/change-theme/exmaple_theme_configuration_dark_mode.png differ diff --git a/airflow-core/docs/img/change-theme/exmaple_theme_configuration_light_mode.png b/airflow-core/docs/img/change-theme/exmaple_theme_configuration_light_mode.png new file mode 100644 index 0000000000000..b31ad03571f8b Binary files /dev/null and b/airflow-core/docs/img/change-theme/exmaple_theme_configuration_light_mode.png differ diff --git a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/ui/config.py b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/ui/config.py index 5644f49e0cfa2..3e8a9cb734325 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/ui/config.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/ui/config.py @@ -35,3 +35,4 @@ class ConfigResponse(BaseModel): dashboard_alert: list[UIAlert] show_external_log_redirect: bool external_log_name: str | None = None + theme: str diff --git a/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml b/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml index bc916225b7f49..81f4a305a88c2 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml +++ b/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml @@ -1245,6 +1245,9 @@ components: - type: string - type: 'null' title: External Log Name + theme: + type: string + title: Theme type: object required: - page_size @@ -1257,6 +1260,7 @@ components: - test_connection - dashboard_alert - show_external_log_redirect + - theme title: ConfigResponse description: configuration serializer. ConnectionHookFieldBehavior: diff --git a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/config.py b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/config.py index c0b7dd2e3b638..01a8118b015e6 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/config.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/config.py @@ -59,6 +59,7 @@ def get_configs() -> ConfigResponse: "dashboard_alert": [alert for alert in DASHBOARD_UIALERTS if isinstance(alert, UIAlert)], "show_external_log_redirect": task_log_reader.supports_external_link, "external_log_name": getattr(task_log_reader.log_handler, "log_name", None), + "theme": conf.get("api", "theme", fallback=""), } config.update({key: value for key, value in additional_config.items()}) diff --git a/airflow-core/src/airflow/config_templates/config.yml b/airflow-core/src/airflow/config_templates/config.yml index 131d71c5b57a3..452e6115403bd 100644 --- a/airflow-core/src/airflow/config_templates/config.yml +++ b/airflow-core/src/airflow/config_templates/config.yml @@ -1360,6 +1360,14 @@ api: type: string example: ~ default: + theme: + description: | + Creates and sets a custom color palette for the side/navbar based on a color. + It expects a hex six-digit form. If the value is not provided UI will use default theme. + version_added: ~ + type: string + example: "#dce7f5" + default: enable_swagger_ui: description: | Boolean for running SwaggerUI in the webserver. diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts b/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts index 5e826f98f4b13..a1cd1f7c98b0a 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts @@ -6983,10 +6983,14 @@ export const $ConfigResponse = { } ], title: 'External Log Name' + }, + theme: { + type: 'string', + title: 'Theme' } }, type: 'object', - required: ['page_size', 'auto_refresh_interval', 'hide_paused_dags_by_default', 'instance_name', 'enable_swagger_ui', 'require_confirmation_dag_change', 'default_wrap', 'test_connection', 'dashboard_alert', 'show_external_log_redirect'], + required: ['page_size', 'auto_refresh_interval', 'hide_paused_dags_by_default', 'instance_name', 'enable_swagger_ui', 'require_confirmation_dag_change', 'default_wrap', 'test_connection', 'dashboard_alert', 'show_external_log_redirect', 'theme'], title: 'ConfigResponse', description: 'configuration serializer.' } as const; diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts b/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts index 6db170921b020..16e9e0ea2f094 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts @@ -1749,6 +1749,7 @@ export type ConfigResponse = { dashboard_alert: Array; show_external_log_redirect: boolean; external_log_name?: string | null; + theme: string; }; /** diff --git a/airflow-core/src/airflow/ui/package.json b/airflow-core/src/airflow/ui/package.json index 49b0bdc1faa9a..ccb062854d4af 100644 --- a/airflow-core/src/airflow/ui/package.json +++ b/airflow-core/src/airflow/ui/package.json @@ -66,7 +66,9 @@ "use-debounce": "^10.0.4", "usehooks-ts": "^3.1.1", "yaml": "^2.6.1", - "zustand": "^5.0.4" + "zustand": "^5.0.4", + "culori": "^4.0.2", + "@types/culori": "^4.0.1" }, "devDependencies": { "@7nohe/openapi-react-query-codegen": "^1.6.2", diff --git a/airflow-core/src/airflow/ui/pnpm-lock.yaml b/airflow-core/src/airflow/ui/pnpm-lock.yaml index 36dca4e6b6b97..442a26cbbabad 100644 --- a/airflow-core/src/airflow/ui/pnpm-lock.yaml +++ b/airflow-core/src/airflow/ui/pnpm-lock.yaml @@ -29,6 +29,9 @@ importers: '@tanstack/react-virtual': specifier: ^3.13.8 version: 3.13.8(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@types/culori': + specifier: ^4.0.1 + version: 4.0.1 '@types/debounce-promise': specifier: ^3.1.9 version: 3.1.9 @@ -68,6 +71,9 @@ importers: chartjs-plugin-annotation: specifier: ^3.1.0 version: 3.1.0(chart.js@4.4.9) + culori: + specifier: ^4.0.2 + version: 4.0.2 dayjs: specifier: ^1.11.13 version: 1.11.13 @@ -1150,6 +1156,9 @@ packages: '@types/cookie@0.6.0': resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} + '@types/culori@4.0.1': + resolution: {integrity: sha512-43M51r/22CjhbOXyGT361GZ9vncSVQ39u62x5eJdBQFviI8zWp2X5jzqg7k4M6PVgDQAClpy2bUe2dtwEgEDVQ==} + '@types/d3-array@3.0.3': resolution: {integrity: sha512-Reoy+pKnvsksN0lQUlcH6dOGjRZ/3WRwXR//m+/8lt1BXeI4xyaUZoqULNjyXXRuh0Mj4LNpkCvhUpQlY3X5xQ==} @@ -2220,6 +2229,10 @@ packages: csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + culori@4.0.2: + resolution: {integrity: sha512-1+BhOB8ahCn4O0cep0Sh2l9KCOfOdY+BXJnKMHFFzDEouSr/el18QwXEMRlOj9UY5nCeA8UN3a/82rUWRBeyBw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + d3-array@3.2.1: resolution: {integrity: sha512-gUY/qeHq/yNqqoCKNq4vtpFLdoCdvyNpWoC/KNjhGbhDuQpAM9sIQQKkXSNpXa9h5KySs/gzm7R88WkUutgwWQ==} engines: {node: '>=12'} @@ -5679,6 +5692,8 @@ snapshots: '@types/cookie@0.6.0': {} + '@types/culori@4.0.1': {} + '@types/d3-array@3.0.3': {} '@types/d3-color@3.1.0': {} @@ -7400,6 +7415,8 @@ snapshots: csstype@3.1.3: {} + culori@4.0.2: {} + d3-array@3.2.1: dependencies: internmap: 2.0.3 diff --git a/airflow-core/src/airflow/ui/src/context/chakraCustom/ChakraCustomProvider.tsx b/airflow-core/src/airflow/ui/src/context/chakraCustom/ChakraCustomProvider.tsx new file mode 100644 index 0000000000000..f351460c761e8 --- /dev/null +++ b/airflow-core/src/airflow/ui/src/context/chakraCustom/ChakraCustomProvider.tsx @@ -0,0 +1,40 @@ +/*! + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { ChakraProvider } from "@chakra-ui/react"; +import { useMemo, type PropsWithChildren } from "react"; + +import { useConfig } from "src/queries/useConfig"; +import { createTheme } from "src/theme"; +import { generatePalette } from "src/utils/generatePalette"; + +export const ChakraCustomProvider = ({ children }: PropsWithChildren) => { + const getTheme = useConfig("theme"); + + const system = useMemo(() => { + if (typeof getTheme === "undefined") { + return undefined; + } + + const theme = typeof getTheme === "string" ? getTheme : ""; + + return createTheme(generatePalette(theme)); + }, [getTheme]); + + return system && {children}; +}; diff --git a/airflow-core/src/airflow/ui/src/context/chakraCustom/index.ts b/airflow-core/src/airflow/ui/src/context/chakraCustom/index.ts new file mode 100644 index 0000000000000..0ad3c5ef2293b --- /dev/null +++ b/airflow-core/src/airflow/ui/src/context/chakraCustom/index.ts @@ -0,0 +1,20 @@ +/*! + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from "./ChakraCustomProvider"; diff --git a/airflow-core/src/airflow/ui/src/main.tsx b/airflow-core/src/airflow/ui/src/main.tsx index a98de89b4868e..6ed6f3dba7613 100644 --- a/airflow-core/src/airflow/ui/src/main.tsx +++ b/airflow-core/src/airflow/ui/src/main.tsx @@ -16,7 +16,6 @@ * specific language governing permissions and limitations * under the License. */ -import { ChakraProvider } from "@chakra-ui/react"; import { QueryClientProvider } from "@tanstack/react-query"; import axios, { type AxiosError } from "axios"; import { StrictMode } from "react"; @@ -29,6 +28,7 @@ import * as ReactRouterDOM from "react-router-dom"; import * as ReactJSXRuntime from "react/jsx-runtime"; import type { HTTPExceptionResponse } from "openapi/requests/types.gen"; +import { ChakraCustomProvider } from "src/context/chakraCustom"; import { ColorModeProvider } from "src/context/colorMode"; import { TimezoneProvider } from "src/context/timezone"; import { router } from "src/router"; @@ -36,7 +36,6 @@ import { getRedirectPath } from "src/utils/links.ts"; import i18n from "./i18n/config"; import { client } from "./queryClient"; -import { system } from "./theme"; // Set React, ReactDOM, and ReactJSXRuntime on globalThis to share them with the dynamically imported React plugins. // Only one instance of React should be used. @@ -69,15 +68,15 @@ axios.interceptors.response.use( createRoot(document.querySelector("#root") as HTMLDivElement).render( - - - + + + - - - + + + , ); diff --git a/airflow-core/src/airflow/ui/src/mocks/handlers/config.ts b/airflow-core/src/airflow/ui/src/mocks/handlers/config.ts index 3caa610c125b5..bc5f2a035966b 100644 --- a/airflow-core/src/airflow/ui/src/mocks/handlers/config.ts +++ b/airflow-core/src/airflow/ui/src/mocks/handlers/config.ts @@ -29,6 +29,7 @@ export const handlers: Array = [ page_size: 15, require_confirmation_dag_change: false, test_connection: "Disabled", + theme: "", }), ), ]; diff --git a/airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/TaskLogHeader.tsx b/airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/TaskLogHeader.tsx index 0d8d6c88e7722..bf925e7fcdee6 100644 --- a/airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/TaskLogHeader.tsx +++ b/airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/TaskLogHeader.tsx @@ -42,7 +42,7 @@ import type { TaskInstanceResponse } from "openapi/requests/types.gen"; import { TaskTrySelect } from "src/components/TaskTrySelect"; import { Button, Menu, Select, Tooltip } from "src/components/ui"; import { SearchParamsKeys } from "src/constants/searchParams"; -import { system } from "src/theme"; +import { createTheme } from "src/theme"; import { type LogLevel, logLevelColorMapping, logLevelOptions } from "src/utils/logs"; type Props = { @@ -85,6 +85,7 @@ export const TaskLogHeader = ({ const sources = searchParams.getAll(SearchParamsKeys.SOURCE); const logLevels = searchParams.getAll(SearchParamsKeys.LOG_LEVEL); const hasLogLevels = logLevels.length > 0; + const system = createTheme(); // Have select zIndex greater than modal zIndex in fullscreen so that // select options are displayed. diff --git a/airflow-core/src/airflow/ui/src/theme.ts b/airflow-core/src/airflow/ui/src/theme.ts index bd8206c11d338..f60a23ef187ed 100644 --- a/airflow-core/src/airflow/ui/src/theme.ts +++ b/airflow-core/src/airflow/ui/src/theme.ts @@ -23,6 +23,8 @@ import { createSystem, defaultConfig, defineConfig } from "@chakra-ui/react"; import type { CSSProperties } from "react"; +import type { Palette } from "src/utils/generatePalette"; + const generateSemanticTokens = (color: string, darkContrast: string = "white") => ({ solid: { value: `{colors.${color}.600}` }, contrast: { value: { _light: "white", _dark: darkContrast } }, @@ -33,371 +35,375 @@ const generateSemanticTokens = (color: string, darkContrast: string = "white") = focusRing: { value: { _light: `{colors.${color}.800}`, _dark: `{colors.${color}.200}` } }, }); -export const customConfig = defineConfig({ - // See https://chakra-ui.com/docs/theming/colors for more information on the colors used here. - theme: { - tokens: { - colors: { - black: { value: "oklch(0.23185 0.0323 266.44)" }, // Custom value for dark mode - brand: { - "50": { value: "oklch(0.98 0.006 248.717)" }, - "100": { value: "oklch(0.962 0.012 249.460)" }, - "200": { value: "oklch(0.923 0.023 255.082)" }, - "300": { value: "oklch(0.865 0.039 252.420)" }, - "400": { value: "oklch(0.705 0.066 256.378)" }, - "500": { value: "oklch(0.575 0.08 257.759)" }, - "600": { value: "oklch(0.469 0.084 257.657)" }, - "700": { value: "oklch(0.399 0.084 257.850)" }, - "800": { value: "oklch(0.324 0.072 260.329)" }, - "900": { value: "oklch(0.259 0.062 265.566)" }, - "950": { value: "oklch(0.179 0.05 265.487)" }, - }, - gray: { - // Values modified from original Tailwind to improve contrast in Chakra UI - "50": { value: "oklch(0.985 0.004 253)" }, // Original: oklch(0.985 0.002 247.839) - "100": { value: "oklch(0.955 0.006 253)" }, // Original: oklch(0.967 0.003 264.542) - "200": { value: "oklch(0.915 0.01 253)" }, // Original: oklch(0.928 0.006 264.531) - "300": { value: "oklch(0.85 0.016 253)" }, // Original: oklch(0.872 0.01 258.338) - "400": { value: "oklch(0.75 0.025 252)" }, // Original: oklch(0.707 0.022 261.325) - "500": { value: "oklch(0.63 0.042 252)" }, // Original: oklch(0.551 0.027 264.364) - "600": { value: "oklch(0.45 0.055 251)" }, // Original: oklch(0.446 0.03 256.802) - "700": { value: "oklch(0.35 0.045 251)" }, // Original: oklch(0.373 0.034 259.733) - "800": { value: "oklch(0.28 0.035 251)" }, // Original: oklch(0.278 0.033 256.848) - "900": { value: "oklch(0.18 0.03 251)" }, // Original: oklch(0.21 0.034 264.665) - "950": { value: "oklch(0.11 0.025 251)" }, // Original: oklch(0.13 0.028 261.692) - }, - // TAILWIND 4.0 COLORS - // See https://tailwindcss.com/docs/colors for more information on the colors used here. - red: { - "50": { value: "oklch(0.971 0.013 17.38)" }, - "100": { value: "oklch(0.936 0.032 17.717)" }, - "200": { value: "oklch(0.885 0.062 18.334)" }, - "300": { value: "oklch(0.808 0.114 19.571)" }, - "400": { value: "oklch(0.704 0.191 22.216)" }, - "500": { value: "oklch(0.637 0.237 25.331)" }, - "600": { value: "oklch(0.577 0.245 27.325)" }, - "700": { value: "oklch(0.505 0.213 27.518)" }, - "800": { value: "oklch(0.444 0.177 26.899)" }, - "900": { value: "oklch(0.396 0.141 25.723)" }, - "950": { value: "oklch(0.258 0.092 26.042)" }, - }, - // Values modified from original Tailwind to improve contrast in Chakra UI - orange: { - "50": { value: "oklch(0.982 0.013 83.915)" }, - "100": { value: "oklch(0.961 0.033 82.320)" }, - "200": { value: "oklch(0.918 0.065 79.975)" }, - "300": { value: "oklch(0.857 0.118 76.815)" }, - "400": { value: "oklch(0.7492 0.1439 62.081)" }, // Original: oklch(0.774 0.186 71.555) - "500": { value: "oklch(0.6462 0.1979 43.792)" }, // Original: oklch(0.705 0.213 47.604) - "600": { value: "oklch(0.5902 0.198 35.93)" }, // Original: oklch(0.632 0.214 41.185) - "700": { value: "oklch(0.553 0.184 41.777)" }, - "800": { value: "oklch(0.469 0.144 45.164)" }, - "900": { value: "oklch(0.414 0.110 48.717)" }, - "950": { value: "oklch(0.271 0.069 52.345)" }, - }, - amber: { - "50": { value: "oklch(0.987 0.022 95.277)" }, - "100": { value: "oklch(0.962 0.059 95.617)" }, - "200": { value: "oklch(0.924 0.12 95.746)" }, - "300": { value: "oklch(0.879 0.169 91.605)" }, - "400": { value: "oklch(0.828 0.189 84.429)" }, - "500": { value: "oklch(0.769 0.188 70.08)" }, - "600": { value: "oklch(0.666 0.179 58.318)" }, - "700": { value: "oklch(0.555 0.163 48.998)" }, - "800": { value: "oklch(0.473 0.137 46.201)" }, - "900": { value: "oklch(0.414 0.112 45.904)" }, - "950": { value: "oklch(0.279 0.077 45.635)" }, - }, - yellow: { - "50": { value: "oklch(0.987 0.026 102.212)" }, - "100": { value: "oklch(0.973 0.071 103.193)" }, - "200": { value: "oklch(0.945 0.129 101.54)" }, - "300": { value: "oklch(0.905 0.182 98.111)" }, - "400": { value: "oklch(0.852 0.199 91.936)" }, - "500": { value: "oklch(0.795 0.184 86.047)" }, - "600": { value: "oklch(0.681 0.162 75.834)" }, - "700": { value: "oklch(0.554 0.135 66.442)" }, - "800": { value: "oklch(0.476 0.114 61.907)" }, - "900": { value: "oklch(0.421 0.095 57.708)" }, - "950": { value: "oklch(0.286 0.066 53.813)" }, - }, - lime: { - "50": { value: "oklch(0.986 0.031 120.757)" }, - "100": { value: "oklch(0.967 0.067 122.328)" }, - "200": { value: "oklch(0.938 0.127 124.321)" }, - "300": { value: "oklch(0.897 0.196 126.665)" }, - "400": { value: "oklch(0.841 0.238 128.85)" }, - "500": { value: "oklch(0.768 0.233 130.85)" }, - "600": { value: "oklch(0.648 0.2 131.684)" }, - "700": { value: "oklch(0.532 0.157 131.589)" }, - "800": { value: "oklch(0.453 0.124 130.933)" }, - "900": { value: "oklch(0.405 0.101 131.063)" }, - "950": { value: "oklch(0.274 0.072 132.109)" }, - }, - green: { +const defaultBrandPalette: Palette = { + "50": { value: "oklch(0.98 0.006 248.717)" }, + "100": { value: "oklch(0.962 0.012 249.460)" }, + "200": { value: "oklch(0.923 0.023 255.082)" }, + "300": { value: "oklch(0.865 0.039 252.420)" }, + "400": { value: "oklch(0.705 0.066 256.378)" }, + "500": { value: "oklch(0.575 0.08 257.759)" }, + "600": { value: "oklch(0.469 0.084 257.657)" }, + "700": { value: "oklch(0.399 0.084 257.850)" }, + "800": { value: "oklch(0.324 0.072 260.329)" }, + "900": { value: "oklch(0.259 0.062 265.566)" }, + "950": { value: "oklch(0.179 0.05 265.487)" }, +}; + +export const createTheme = (palette: Palette | undefined = defaultBrandPalette) => { + const customConfig = defineConfig({ + // See https://chakra-ui.com/docs/theming/colors for more information on the colors used here. + theme: { + tokens: { + colors: { + black: { value: "oklch(0.23185 0.0323 266.44)" }, // Custom value for dark mode + brand: palette, + gray: { + // Values modified from original Tailwind to improve contrast in Chakra UI + "50": { value: "oklch(0.985 0.004 253)" }, // Original: oklch(0.985 0.002 247.839) + "100": { value: "oklch(0.955 0.006 253)" }, // Original: oklch(0.967 0.003 264.542) + "200": { value: "oklch(0.915 0.01 253)" }, // Original: oklch(0.928 0.006 264.531) + "300": { value: "oklch(0.85 0.016 253)" }, // Original: oklch(0.872 0.01 258.338) + "400": { value: "oklch(0.75 0.025 252)" }, // Original: oklch(0.707 0.022 261.325) + "500": { value: "oklch(0.63 0.042 252)" }, // Original: oklch(0.551 0.027 264.364) + "600": { value: "oklch(0.45 0.055 251)" }, // Original: oklch(0.446 0.03 256.802) + "700": { value: "oklch(0.35 0.045 251)" }, // Original: oklch(0.373 0.034 259.733) + "800": { value: "oklch(0.28 0.035 251)" }, // Original: oklch(0.278 0.033 256.848) + "900": { value: "oklch(0.18 0.03 251)" }, // Original: oklch(0.21 0.034 264.665) + "950": { value: "oklch(0.11 0.025 251)" }, // Original: oklch(0.13 0.028 261.692) + }, + // TAILWIND 4.0 COLORS + // See https://tailwindcss.com/docs/colors for more information on the colors used here. + red: { + "50": { value: "oklch(0.971 0.013 17.38)" }, + "100": { value: "oklch(0.936 0.032 17.717)" }, + "200": { value: "oklch(0.885 0.062 18.334)" }, + "300": { value: "oklch(0.808 0.114 19.571)" }, + "400": { value: "oklch(0.704 0.191 22.216)" }, + "500": { value: "oklch(0.637 0.237 25.331)" }, + "600": { value: "oklch(0.577 0.245 27.325)" }, + "700": { value: "oklch(0.505 0.213 27.518)" }, + "800": { value: "oklch(0.444 0.177 26.899)" }, + "900": { value: "oklch(0.396 0.141 25.723)" }, + "950": { value: "oklch(0.258 0.092 26.042)" }, + }, // Values modified from original Tailwind to improve contrast in Chakra UI - "50": { value: "oklch(0.982 0.018 155.826)" }, - "100": { value: "oklch(0.962 0.044 156.743)" }, - "200": { value: "oklch(0.925 0.084 155.995)" }, - "300": { value: "oklch(0.75 0.18 153.0)" }, // Original: oklch(0.871 0.15 154.449) - "400": { value: "oklch(0.625 0.209 150.0)" }, // Original: oklch(0.792 0.209 151.711) - "500": { value: "oklch(0.528 0.219 149.579)" }, // Original: oklch(0.723 0.219 149.579) - "600": { value: "oklch(0.47 0.20 149.0)" }, // Original: oklch(0.627 0.194 149.214) - "700": { value: "oklch(0.40 0.16 149.5)" }, // Original: oklch(0.527 0.154 150.069) - "800": { value: "oklch(0.448 0.119 151.328)" }, - "900": { value: "oklch(0.393 0.095 152.535)" }, - "950": { value: "oklch(0.266 0.065 152.934)" }, - }, - emerald: { - "50": { value: "oklch(0.979 0.021 166.113)" }, - "100": { value: "oklch(0.95 0.052 163.051)" }, - "200": { value: "oklch(0.905 0.093 164.15)" }, - "300": { value: "oklch(0.845 0.143 164.978)" }, - "400": { value: "oklch(0.765 0.177 163.223)" }, - "500": { value: "oklch(0.696 0.17 162.48)" }, - "600": { value: "oklch(0.596 0.145 163.225)" }, - "700": { value: "oklch(0.508 0.118 165.612)" }, - "800": { value: "oklch(0.432 0.095 166.913)" }, - "900": { value: "oklch(0.378 0.077 168.94)" }, - "950": { value: "oklch(0.262 0.051 172.552)" }, - }, - teal: { - "50": { value: "oklch(0.984 0.014 180.72)" }, - "100": { value: "oklch(0.953 0.051 180.801)" }, - "200": { value: "oklch(0.91 0.096 180.426)" }, - "300": { value: "oklch(0.855 0.138 181.071)" }, - "400": { value: "oklch(0.777 0.152 181.912)" }, - "500": { value: "oklch(0.704 0.14 182.503)" }, - "600": { value: "oklch(0.6 0.118 184.704)" }, - "700": { value: "oklch(0.511 0.096 186.391)" }, - "800": { value: "oklch(0.437 0.078 188.216)" }, - "900": { value: "oklch(0.386 0.063 188.416)" }, - "950": { value: "oklch(0.277 0.046 192.524)" }, - }, - cyan: { - "50": { value: "oklch(0.984 0.019 200.873)" }, - "100": { value: "oklch(0.956 0.045 203.388)" }, - "200": { value: "oklch(0.917 0.08 205.041)" }, - "300": { value: "oklch(0.865 0.127 207.078)" }, - "400": { value: "oklch(0.789 0.154 211.53)" }, - "500": { value: "oklch(0.715 0.143 215.221)" }, - "600": { value: "oklch(0.609 0.126 221.723)" }, - "700": { value: "oklch(0.52 0.105 223.128)" }, - "800": { value: "oklch(0.45 0.085 224.283)" }, - "900": { value: "oklch(0.398 0.07 227.392)" }, - "950": { value: "oklch(0.302 0.056 229.695)" }, - }, - sky: { - "50": { value: "oklch(0.977 0.013 236.62)" }, - "100": { value: "oklch(0.951 0.026 236.824)" }, - "200": { value: "oklch(0.901 0.058 230.902)" }, - "300": { value: "oklch(0.828 0.111 230.318)" }, - "400": { value: "oklch(0.746 0.16 232.661)" }, - "500": { value: "oklch(0.685 0.169 237.323)" }, - "600": { value: "oklch(0.588 0.158 241.966)" }, - "700": { value: "oklch(0.5 0.134 242.749)" }, - "800": { value: "oklch(0.443 0.11 240.79)" }, - "900": { value: "oklch(0.391 0.09 240.876)" }, - "950": { value: "oklch(0.293 0.066 243.157)" }, - }, - blue: { - "50": { value: "oklch(0.97 0.014 254.604)" }, - "100": { value: "oklch(0.932 0.032 255.585)" }, - "200": { value: "oklch(0.882 0.059 254.128)" }, - "300": { value: "oklch(0.809 0.105 251.813)" }, - "400": { value: "oklch(0.707 0.165 254.624)" }, - "500": { value: "oklch(0.623 0.214 259.815)" }, - "600": { value: "oklch(0.546 0.245 262.881)" }, - "700": { value: "oklch(0.488 0.243 264.376)" }, - "800": { value: "oklch(0.424 0.199 265.638)" }, - "900": { value: "oklch(0.379 0.146 265.522)" }, - "950": { value: "oklch(0.282 0.091 267.935)" }, - }, - indigo: { - "50": { value: "oklch(0.962 0.018 272.314)" }, - "100": { value: "oklch(0.93 0.034 272.788)" }, - "200": { value: "oklch(0.87 0.065 274.039)" }, - "300": { value: "oklch(0.785 0.115 274.713)" }, - "400": { value: "oklch(0.673 0.182 276.935)" }, - "500": { value: "oklch(0.585 0.233 277.117)" }, - "600": { value: "oklch(0.511 0.262 276.966)" }, - "700": { value: "oklch(0.457 0.24 277.023)" }, - "800": { value: "oklch(0.398 0.195 277.366)" }, - "900": { value: "oklch(0.359 0.144 278.697)" }, - "950": { value: "oklch(0.257 0.09 281.288)" }, - }, - violet: { - "50": { value: "oklch(0.969 0.016 293.756)" }, - "100": { value: "oklch(0.943 0.029 294.588)" }, - "200": { value: "oklch(0.894 0.057 293.283)" }, - "300": { value: "oklch(0.811 0.111 293.571)" }, - "400": { value: "oklch(0.702 0.183 293.541)" }, - "500": { value: "oklch(0.606 0.25 292.717)" }, - "600": { value: "oklch(0.541 0.281 293.009)" }, - "700": { value: "oklch(0.491 0.27 292.581)" }, - "800": { value: "oklch(0.432 0.232 292.759)" }, - "900": { value: "oklch(0.38 0.189 293.745)" }, - "950": { value: "oklch(0.283 0.141 291.089)" }, - }, - purple: { - "50": { value: "oklch(0.977 0.014 308.299)" }, - "100": { value: "oklch(0.946 0.033 307.174)" }, - "200": { value: "oklch(0.902 0.063 306.703)" }, - "300": { value: "oklch(0.827 0.119 306.383)" }, - "400": { value: "oklch(0.714 0.203 305.504)" }, - "500": { value: "oklch(0.627 0.265 303.9)" }, - "600": { value: "oklch(0.558 0.288 302.321)" }, - "700": { value: "oklch(0.496 0.265 301.924)" }, - "800": { value: "oklch(0.438 0.218 303.724)" }, - "900": { value: "oklch(0.381 0.176 304.987)" }, - "950": { value: "oklch(0.291 0.149 302.717)" }, - }, - fuchsia: { - "50": { value: "oklch(0.977 0.017 320.058)" }, - "100": { value: "oklch(0.952 0.037 318.852)" }, - "200": { value: "oklch(0.903 0.076 319.62)" }, - "300": { value: "oklch(0.833 0.145 321.434)" }, - "400": { value: "oklch(0.74 0.238 322.16)" }, - "500": { value: "oklch(0.667 0.295 322.15)" }, - "600": { value: "oklch(0.591 0.293 322.896)" }, - "700": { value: "oklch(0.518 0.253 323.949)" }, - "800": { value: "oklch(0.452 0.211 324.591)" }, - "900": { value: "oklch(0.401 0.17 325.612)" }, - "950": { value: "oklch(0.293 0.136 325.661)" }, - }, - pink: { - "50": { value: "oklch(0.971 0.014 343.198)" }, - "100": { value: "oklch(0.948 0.028 342.258)" }, - "200": { value: "oklch(0.899 0.061 343.231)" }, - "300": { value: "oklch(0.823 0.12 346.018)" }, - "400": { value: "oklch(0.718 0.202 349.761)" }, - "500": { value: "oklch(0.656 0.241 354.308)" }, - "600": { value: "oklch(0.592 0.249 0.584)" }, - "700": { value: "oklch(0.525 0.223 3.958)" }, - "800": { value: "oklch(0.459 0.187 3.815)" }, - "900": { value: "oklch(0.408 0.153 2.432)" }, - "950": { value: "oklch(0.284 0.109 3.907)" }, - }, - rose: { - "50": { value: "oklch(0.969 0.015 12.422)" }, - "100": { value: "oklch(0.941 0.03 12.58)" }, - "200": { value: "oklch(0.892 0.058 10.001)" }, - "300": { value: "oklch(0.81 0.117 11.638)" }, - "400": { value: "oklch(0.712 0.194 13.428)" }, - "500": { value: "oklch(0.645 0.246 16.439)" }, - "600": { value: "oklch(0.586 0.253 17.585)" }, - "700": { value: "oklch(0.514 0.222 16.935)" }, - "800": { value: "oklch(0.455 0.188 13.697)" }, - "900": { value: "oklch(0.41 0.159 10.272)" }, - "950": { value: "oklch(0.271 0.105 12.094)" }, - }, - slate: { - "50": { value: "oklch(0.984 0.003 247.858)" }, - "100": { value: "oklch(0.968 0.007 247.896)" }, - "200": { value: "oklch(0.929 0.013 255.508)" }, - "300": { value: "oklch(0.869 0.022 252.894)" }, - "400": { value: "oklch(0.704 0.04 256.788)" }, - "500": { value: "oklch(0.554 0.046 257.417)" }, - "600": { value: "oklch(0.446 0.043 257.281)" }, - "700": { value: "oklch(0.372 0.044 257.287)" }, - "800": { value: "oklch(0.279 0.041 260.031)" }, - "900": { value: "oklch(0.208 0.042 265.755)" }, - "950": { value: "oklch(0.129 0.042 264.695)" }, - }, - zinc: { - "50": { value: "oklch(0.985 0 0)" }, - "100": { value: "oklch(0.967 0.001 286.375)" }, - "200": { value: "oklch(0.92 0.004 286.32)" }, - "300": { value: "oklch(0.871 0.006 286.286)" }, - "400": { value: "oklch(0.705 0.015 286.067)" }, - "500": { value: "oklch(0.552 0.016 285.938)" }, - "600": { value: "oklch(0.442 0.017 285.786)" }, - "700": { value: "oklch(0.37 0.013 285.805)" }, - "800": { value: "oklch(0.274 0.006 286.033)" }, - "900": { value: "oklch(0.21 0.006 285.885)" }, - "950": { value: "oklch(0.141 0.005 285.823)" }, - }, - neutral: { - "50": { value: "oklch(0.985 0 0)" }, - "100": { value: "oklch(0.97 0 0)" }, - "200": { value: "oklch(0.922 0 0)" }, - "300": { value: "oklch(0.87 0 0)" }, - "400": { value: "oklch(0.708 0 0)" }, - "500": { value: "oklch(0.556 0 0)" }, - "600": { value: "oklch(0.439 0 0)" }, - "700": { value: "oklch(0.371 0 0)" }, - "800": { value: "oklch(0.269 0 0)" }, - "900": { value: "oklch(0.205 0 0)" }, - "950": { value: "oklch(0.145 0 0)" }, - }, - stone: { - "50": { value: "oklch(0.985 0.001 106.423)" }, - "100": { value: "oklch(0.97 0.001 106.424)" }, - "200": { value: "oklch(0.923 0.003 48.717)" }, - "300": { value: "oklch(0.869 0.005 56.366)" }, - "400": { value: "oklch(0.709 0.01 56.259)" }, - "500": { value: "oklch(0.553 0.013 58.071)" }, - "600": { value: "oklch(0.444 0.011 73.639)" }, - "700": { value: "oklch(0.374 0.01 67.558)" }, - "800": { value: "oklch(0.268 0.007 34.298)" }, - "900": { value: "oklch(0.216 0.006 56.043)" }, - "950": { value: "oklch(0.147 0.004 49.25)" }, + orange: { + "50": { value: "oklch(0.982 0.013 83.915)" }, + "100": { value: "oklch(0.961 0.033 82.320)" }, + "200": { value: "oklch(0.918 0.065 79.975)" }, + "300": { value: "oklch(0.857 0.118 76.815)" }, + "400": { value: "oklch(0.7492 0.1439 62.081)" }, // Original: oklch(0.774 0.186 71.555) + "500": { value: "oklch(0.6462 0.1979 43.792)" }, // Original: oklch(0.705 0.213 47.604) + "600": { value: "oklch(0.5902 0.198 35.93)" }, // Original: oklch(0.632 0.214 41.185) + "700": { value: "oklch(0.553 0.184 41.777)" }, + "800": { value: "oklch(0.469 0.144 45.164)" }, + "900": { value: "oklch(0.414 0.110 48.717)" }, + "950": { value: "oklch(0.271 0.069 52.345)" }, + }, + amber: { + "50": { value: "oklch(0.987 0.022 95.277)" }, + "100": { value: "oklch(0.962 0.059 95.617)" }, + "200": { value: "oklch(0.924 0.12 95.746)" }, + "300": { value: "oklch(0.879 0.169 91.605)" }, + "400": { value: "oklch(0.828 0.189 84.429)" }, + "500": { value: "oklch(0.769 0.188 70.08)" }, + "600": { value: "oklch(0.666 0.179 58.318)" }, + "700": { value: "oklch(0.555 0.163 48.998)" }, + "800": { value: "oklch(0.473 0.137 46.201)" }, + "900": { value: "oklch(0.414 0.112 45.904)" }, + "950": { value: "oklch(0.279 0.077 45.635)" }, + }, + yellow: { + "50": { value: "oklch(0.987 0.026 102.212)" }, + "100": { value: "oklch(0.973 0.071 103.193)" }, + "200": { value: "oklch(0.945 0.129 101.54)" }, + "300": { value: "oklch(0.905 0.182 98.111)" }, + "400": { value: "oklch(0.852 0.199 91.936)" }, + "500": { value: "oklch(0.795 0.184 86.047)" }, + "600": { value: "oklch(0.681 0.162 75.834)" }, + "700": { value: "oklch(0.554 0.135 66.442)" }, + "800": { value: "oklch(0.476 0.114 61.907)" }, + "900": { value: "oklch(0.421 0.095 57.708)" }, + "950": { value: "oklch(0.286 0.066 53.813)" }, + }, + lime: { + "50": { value: "oklch(0.986 0.031 120.757)" }, + "100": { value: "oklch(0.967 0.067 122.328)" }, + "200": { value: "oklch(0.938 0.127 124.321)" }, + "300": { value: "oklch(0.897 0.196 126.665)" }, + "400": { value: "oklch(0.841 0.238 128.85)" }, + "500": { value: "oklch(0.768 0.233 130.85)" }, + "600": { value: "oklch(0.648 0.2 131.684)" }, + "700": { value: "oklch(0.532 0.157 131.589)" }, + "800": { value: "oklch(0.453 0.124 130.933)" }, + "900": { value: "oklch(0.405 0.101 131.063)" }, + "950": { value: "oklch(0.274 0.072 132.109)" }, + }, + green: { + // Values modified from original Tailwind to improve contrast in Chakra UI + "50": { value: "oklch(0.982 0.018 155.826)" }, + "100": { value: "oklch(0.962 0.044 156.743)" }, + "200": { value: "oklch(0.925 0.084 155.995)" }, + "300": { value: "oklch(0.75 0.18 153.0)" }, // Original: oklch(0.871 0.15 154.449) + "400": { value: "oklch(0.625 0.209 150.0)" }, // Original: oklch(0.792 0.209 151.711) + "500": { value: "oklch(0.528 0.219 149.579)" }, // Original: oklch(0.723 0.219 149.579) + "600": { value: "oklch(0.47 0.20 149.0)" }, // Original: oklch(0.627 0.194 149.214) + "700": { value: "oklch(0.40 0.16 149.5)" }, // Original: oklch(0.527 0.154 150.069) + "800": { value: "oklch(0.448 0.119 151.328)" }, + "900": { value: "oklch(0.393 0.095 152.535)" }, + "950": { value: "oklch(0.266 0.065 152.934)" }, + }, + emerald: { + "50": { value: "oklch(0.979 0.021 166.113)" }, + "100": { value: "oklch(0.95 0.052 163.051)" }, + "200": { value: "oklch(0.905 0.093 164.15)" }, + "300": { value: "oklch(0.845 0.143 164.978)" }, + "400": { value: "oklch(0.765 0.177 163.223)" }, + "500": { value: "oklch(0.696 0.17 162.48)" }, + "600": { value: "oklch(0.596 0.145 163.225)" }, + "700": { value: "oklch(0.508 0.118 165.612)" }, + "800": { value: "oklch(0.432 0.095 166.913)" }, + "900": { value: "oklch(0.378 0.077 168.94)" }, + "950": { value: "oklch(0.262 0.051 172.552)" }, + }, + teal: { + "50": { value: "oklch(0.984 0.014 180.72)" }, + "100": { value: "oklch(0.953 0.051 180.801)" }, + "200": { value: "oklch(0.91 0.096 180.426)" }, + "300": { value: "oklch(0.855 0.138 181.071)" }, + "400": { value: "oklch(0.777 0.152 181.912)" }, + "500": { value: "oklch(0.704 0.14 182.503)" }, + "600": { value: "oklch(0.6 0.118 184.704)" }, + "700": { value: "oklch(0.511 0.096 186.391)" }, + "800": { value: "oklch(0.437 0.078 188.216)" }, + "900": { value: "oklch(0.386 0.063 188.416)" }, + "950": { value: "oklch(0.277 0.046 192.524)" }, + }, + cyan: { + "50": { value: "oklch(0.984 0.019 200.873)" }, + "100": { value: "oklch(0.956 0.045 203.388)" }, + "200": { value: "oklch(0.917 0.08 205.041)" }, + "300": { value: "oklch(0.865 0.127 207.078)" }, + "400": { value: "oklch(0.789 0.154 211.53)" }, + "500": { value: "oklch(0.715 0.143 215.221)" }, + "600": { value: "oklch(0.609 0.126 221.723)" }, + "700": { value: "oklch(0.52 0.105 223.128)" }, + "800": { value: "oklch(0.45 0.085 224.283)" }, + "900": { value: "oklch(0.398 0.07 227.392)" }, + "950": { value: "oklch(0.302 0.056 229.695)" }, + }, + sky: { + "50": { value: "oklch(0.977 0.013 236.62)" }, + "100": { value: "oklch(0.951 0.026 236.824)" }, + "200": { value: "oklch(0.901 0.058 230.902)" }, + "300": { value: "oklch(0.828 0.111 230.318)" }, + "400": { value: "oklch(0.746 0.16 232.661)" }, + "500": { value: "oklch(0.685 0.169 237.323)" }, + "600": { value: "oklch(0.588 0.158 241.966)" }, + "700": { value: "oklch(0.5 0.134 242.749)" }, + "800": { value: "oklch(0.443 0.11 240.79)" }, + "900": { value: "oklch(0.391 0.09 240.876)" }, + "950": { value: "oklch(0.293 0.066 243.157)" }, + }, + blue: { + "50": { value: "oklch(0.97 0.014 254.604)" }, + "100": { value: "oklch(0.932 0.032 255.585)" }, + "200": { value: "oklch(0.882 0.059 254.128)" }, + "300": { value: "oklch(0.809 0.105 251.813)" }, + "400": { value: "oklch(0.707 0.165 254.624)" }, + "500": { value: "oklch(0.623 0.214 259.815)" }, + "600": { value: "oklch(0.546 0.245 262.881)" }, + "700": { value: "oklch(0.488 0.243 264.376)" }, + "800": { value: "oklch(0.424 0.199 265.638)" }, + "900": { value: "oklch(0.379 0.146 265.522)" }, + "950": { value: "oklch(0.282 0.091 267.935)" }, + }, + indigo: { + "50": { value: "oklch(0.962 0.018 272.314)" }, + "100": { value: "oklch(0.93 0.034 272.788)" }, + "200": { value: "oklch(0.87 0.065 274.039)" }, + "300": { value: "oklch(0.785 0.115 274.713)" }, + "400": { value: "oklch(0.673 0.182 276.935)" }, + "500": { value: "oklch(0.585 0.233 277.117)" }, + "600": { value: "oklch(0.511 0.262 276.966)" }, + "700": { value: "oklch(0.457 0.24 277.023)" }, + "800": { value: "oklch(0.398 0.195 277.366)" }, + "900": { value: "oklch(0.359 0.144 278.697)" }, + "950": { value: "oklch(0.257 0.09 281.288)" }, + }, + violet: { + "50": { value: "oklch(0.969 0.016 293.756)" }, + "100": { value: "oklch(0.943 0.029 294.588)" }, + "200": { value: "oklch(0.894 0.057 293.283)" }, + "300": { value: "oklch(0.811 0.111 293.571)" }, + "400": { value: "oklch(0.702 0.183 293.541)" }, + "500": { value: "oklch(0.606 0.25 292.717)" }, + "600": { value: "oklch(0.541 0.281 293.009)" }, + "700": { value: "oklch(0.491 0.27 292.581)" }, + "800": { value: "oklch(0.432 0.232 292.759)" }, + "900": { value: "oklch(0.38 0.189 293.745)" }, + "950": { value: "oklch(0.283 0.141 291.089)" }, + }, + purple: { + "50": { value: "oklch(0.977 0.014 308.299)" }, + "100": { value: "oklch(0.946 0.033 307.174)" }, + "200": { value: "oklch(0.902 0.063 306.703)" }, + "300": { value: "oklch(0.827 0.119 306.383)" }, + "400": { value: "oklch(0.714 0.203 305.504)" }, + "500": { value: "oklch(0.627 0.265 303.9)" }, + "600": { value: "oklch(0.558 0.288 302.321)" }, + "700": { value: "oklch(0.496 0.265 301.924)" }, + "800": { value: "oklch(0.438 0.218 303.724)" }, + "900": { value: "oklch(0.381 0.176 304.987)" }, + "950": { value: "oklch(0.291 0.149 302.717)" }, + }, + fuchsia: { + "50": { value: "oklch(0.977 0.017 320.058)" }, + "100": { value: "oklch(0.952 0.037 318.852)" }, + "200": { value: "oklch(0.903 0.076 319.62)" }, + "300": { value: "oklch(0.833 0.145 321.434)" }, + "400": { value: "oklch(0.74 0.238 322.16)" }, + "500": { value: "oklch(0.667 0.295 322.15)" }, + "600": { value: "oklch(0.591 0.293 322.896)" }, + "700": { value: "oklch(0.518 0.253 323.949)" }, + "800": { value: "oklch(0.452 0.211 324.591)" }, + "900": { value: "oklch(0.401 0.17 325.612)" }, + "950": { value: "oklch(0.293 0.136 325.661)" }, + }, + pink: { + "50": { value: "oklch(0.971 0.014 343.198)" }, + "100": { value: "oklch(0.948 0.028 342.258)" }, + "200": { value: "oklch(0.899 0.061 343.231)" }, + "300": { value: "oklch(0.823 0.12 346.018)" }, + "400": { value: "oklch(0.718 0.202 349.761)" }, + "500": { value: "oklch(0.656 0.241 354.308)" }, + "600": { value: "oklch(0.592 0.249 0.584)" }, + "700": { value: "oklch(0.525 0.223 3.958)" }, + "800": { value: "oklch(0.459 0.187 3.815)" }, + "900": { value: "oklch(0.408 0.153 2.432)" }, + "950": { value: "oklch(0.284 0.109 3.907)" }, + }, + rose: { + "50": { value: "oklch(0.969 0.015 12.422)" }, + "100": { value: "oklch(0.941 0.03 12.58)" }, + "200": { value: "oklch(0.892 0.058 10.001)" }, + "300": { value: "oklch(0.81 0.117 11.638)" }, + "400": { value: "oklch(0.712 0.194 13.428)" }, + "500": { value: "oklch(0.645 0.246 16.439)" }, + "600": { value: "oklch(0.586 0.253 17.585)" }, + "700": { value: "oklch(0.514 0.222 16.935)" }, + "800": { value: "oklch(0.455 0.188 13.697)" }, + "900": { value: "oklch(0.41 0.159 10.272)" }, + "950": { value: "oklch(0.271 0.105 12.094)" }, + }, + slate: { + "50": { value: "oklch(0.984 0.003 247.858)" }, + "100": { value: "oklch(0.968 0.007 247.896)" }, + "200": { value: "oklch(0.929 0.013 255.508)" }, + "300": { value: "oklch(0.869 0.022 252.894)" }, + "400": { value: "oklch(0.704 0.04 256.788)" }, + "500": { value: "oklch(0.554 0.046 257.417)" }, + "600": { value: "oklch(0.446 0.043 257.281)" }, + "700": { value: "oklch(0.372 0.044 257.287)" }, + "800": { value: "oklch(0.279 0.041 260.031)" }, + "900": { value: "oklch(0.208 0.042 265.755)" }, + "950": { value: "oklch(0.129 0.042 264.695)" }, + }, + zinc: { + "50": { value: "oklch(0.985 0 0)" }, + "100": { value: "oklch(0.967 0.001 286.375)" }, + "200": { value: "oklch(0.92 0.004 286.32)" }, + "300": { value: "oklch(0.871 0.006 286.286)" }, + "400": { value: "oklch(0.705 0.015 286.067)" }, + "500": { value: "oklch(0.552 0.016 285.938)" }, + "600": { value: "oklch(0.442 0.017 285.786)" }, + "700": { value: "oklch(0.37 0.013 285.805)" }, + "800": { value: "oklch(0.274 0.006 286.033)" }, + "900": { value: "oklch(0.21 0.006 285.885)" }, + "950": { value: "oklch(0.141 0.005 285.823)" }, + }, + neutral: { + "50": { value: "oklch(0.985 0 0)" }, + "100": { value: "oklch(0.97 0 0)" }, + "200": { value: "oklch(0.922 0 0)" }, + "300": { value: "oklch(0.87 0 0)" }, + "400": { value: "oklch(0.708 0 0)" }, + "500": { value: "oklch(0.556 0 0)" }, + "600": { value: "oklch(0.439 0 0)" }, + "700": { value: "oklch(0.371 0 0)" }, + "800": { value: "oklch(0.269 0 0)" }, + "900": { value: "oklch(0.205 0 0)" }, + "950": { value: "oklch(0.145 0 0)" }, + }, + stone: { + "50": { value: "oklch(0.985 0.001 106.423)" }, + "100": { value: "oklch(0.97 0.001 106.424)" }, + "200": { value: "oklch(0.923 0.003 48.717)" }, + "300": { value: "oklch(0.869 0.005 56.366)" }, + "400": { value: "oklch(0.709 0.01 56.259)" }, + "500": { value: "oklch(0.553 0.013 58.071)" }, + "600": { value: "oklch(0.444 0.011 73.639)" }, + "700": { value: "oklch(0.374 0.01 67.558)" }, + "800": { value: "oklch(0.268 0.007 34.298)" }, + "900": { value: "oklch(0.216 0.006 56.043)" }, + "950": { value: "oklch(0.147 0.004 49.25)" }, + }, }, }, - }, - semanticTokens: { - colors: { - // Brand colors for consistent theming - brand: generateSemanticTokens("brand"), - // GENERIC STATE - danger: generateSemanticTokens("red"), - info: generateSemanticTokens("blue"), - warning: generateSemanticTokens("amber"), - error: generateSemanticTokens("red"), - // AIRFLOW TASK STATE - active: generateSemanticTokens("blue"), - success: generateSemanticTokens("green"), - failed: generateSemanticTokens("red"), - queued: generateSemanticTokens("stone"), - skipped: generateSemanticTokens("pink"), - up_for_reschedule: generateSemanticTokens("sky"), - up_for_retry: generateSemanticTokens("yellow"), - upstream_failed: generateSemanticTokens("orange"), - running: generateSemanticTokens("cyan"), - restarting: generateSemanticTokens("violet"), - deferred: generateSemanticTokens("purple"), - scheduled: generateSemanticTokens("zinc"), - none: generateSemanticTokens("gray"), - removed: generateSemanticTokens("slate"), - // TAILWIND 4.0 COLORS - red: generateSemanticTokens("red"), - orange: generateSemanticTokens("orange"), - amber: generateSemanticTokens("amber"), - yellow: generateSemanticTokens("yellow"), - lime: generateSemanticTokens("lime"), - green: generateSemanticTokens("green"), - emerald: generateSemanticTokens("emerald"), - teal: generateSemanticTokens("teal"), - cyan: generateSemanticTokens("cyan"), - sky: generateSemanticTokens("sky"), - blue: generateSemanticTokens("blue"), - indigo: generateSemanticTokens("indigo"), - violet: generateSemanticTokens("violet"), - purple: generateSemanticTokens("purple"), - fuchsia: generateSemanticTokens("fuchsia"), - pink: generateSemanticTokens("pink"), - rose: generateSemanticTokens("rose"), - slate: generateSemanticTokens("slate"), - gray: generateSemanticTokens("gray"), - zinc: generateSemanticTokens("zinc"), - neutral: generateSemanticTokens("neutral"), - stone: generateSemanticTokens("stone"), + semanticTokens: { + colors: { + // Brand colors for consistent theming + brand: generateSemanticTokens("brand"), + // GENERIC STATE + danger: generateSemanticTokens("red"), + info: generateSemanticTokens("blue"), + warning: generateSemanticTokens("amber"), + error: generateSemanticTokens("red"), + // AIRFLOW TASK STATE + active: generateSemanticTokens("blue"), + success: generateSemanticTokens("green"), + failed: generateSemanticTokens("red"), + queued: generateSemanticTokens("stone"), + skipped: generateSemanticTokens("pink"), + up_for_reschedule: generateSemanticTokens("sky"), + up_for_retry: generateSemanticTokens("yellow"), + upstream_failed: generateSemanticTokens("orange"), + running: generateSemanticTokens("cyan"), + restarting: generateSemanticTokens("violet"), + deferred: generateSemanticTokens("purple"), + scheduled: generateSemanticTokens("zinc"), + none: generateSemanticTokens("gray"), + removed: generateSemanticTokens("slate"), + // TAILWIND 4.0 COLORS + red: generateSemanticTokens("red"), + orange: generateSemanticTokens("orange"), + amber: generateSemanticTokens("amber"), + yellow: generateSemanticTokens("yellow"), + lime: generateSemanticTokens("lime"), + green: generateSemanticTokens("green"), + emerald: generateSemanticTokens("emerald"), + teal: generateSemanticTokens("teal"), + cyan: generateSemanticTokens("cyan"), + sky: generateSemanticTokens("sky"), + blue: generateSemanticTokens("blue"), + indigo: generateSemanticTokens("indigo"), + violet: generateSemanticTokens("violet"), + purple: generateSemanticTokens("purple"), + fuchsia: generateSemanticTokens("fuchsia"), + pink: generateSemanticTokens("pink"), + rose: generateSemanticTokens("rose"), + slate: generateSemanticTokens("slate"), + gray: generateSemanticTokens("gray"), + zinc: generateSemanticTokens("zinc"), + neutral: generateSemanticTokens("neutral"), + stone: generateSemanticTokens("stone"), + }, }, }, - }, -}); + }); -export const system = createSystem(defaultConfig, customConfig); + return createSystem(defaultConfig, customConfig); +}; // Utility function to resolve CSS variables to their computed values // See: https://github.com/chakra-ui/panda/discussions/2200 diff --git a/airflow-core/src/airflow/ui/src/utils/generatePalette.test.ts b/airflow-core/src/airflow/ui/src/utils/generatePalette.test.ts new file mode 100644 index 0000000000000..8c7b5120ff35a --- /dev/null +++ b/airflow-core/src/airflow/ui/src/utils/generatePalette.test.ts @@ -0,0 +1,117 @@ +/*! + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { describe, it, expect } from "vitest"; + +import { + roundTo3rdDecimal, + quadraticLagrangeInterpolatingPolynomial, + generatePalette, +} from "./generatePalette"; + +describe("roundTo3rdDecimal", () => { + const roundTestCases = [ + [0, 0], + [1.5, 1.5], + [9.8456, 9.846], + [215_415.098_704_36, 215_415.099], + [-15.4564, -15.456], + [-8.9265, -8.926], + ]; + + it.each(roundTestCases)("round to 3rd decimal %d", (number, expected) => { + expect(roundTo3rdDecimal(number)).toBe(expected); + }); +}); + +describe("quadraticLagrangeInterpolatingPolynomial", () => { + it("interpolate three points", () => { + // f(x) = 2x^2 - 5x + 1 + const point0: [number, number] = [0, 1]; + const point1: [number, number] = [3, 4]; + const point2: [number, number] = [5, 26]; + + // f(2) + expect(quadraticLagrangeInterpolatingPolynomial(point0, point1, point2)(2)).toBe(-1); + }); + + it("order of points does not matter", () => { + // f(x) = x^2 + const point0: [number, number] = [-2, 4]; + const point1: [number, number] = [1, 1]; + const point2: [number, number] = [3, 9]; + + // f(2) + expect(quadraticLagrangeInterpolatingPolynomial(point1, point0, point2)(2)).toBe(4); + }); +}); + +describe("generatePalette", () => { + const whiteBlackPalette = { + "50": { value: "oklch(0.97 0 0)" }, + "100": { value: "oklch(0.932 0 0)" }, + "200": { value: "oklch(0.857 0 0)" }, + "300": { value: "oklch(0.781 0 0)" }, + "400": { value: "oklch(0.706 0 0)" }, + "500": { value: "oklch(0.63 0 0)" }, + "600": { value: "oklch(0.554 0 0)" }, + "700": { value: "oklch(0.479 0 0)" }, + "800": { value: "oklch(0.403 0 0)" }, + "900": { value: "oklch(0.328 0 0)" }, + "950": { value: "oklch(0.29 0 0)" }, + }; + + const paletteTestCase = [ + { + color: "#ffffff", + palette: whiteBlackPalette, + }, + { + color: "#000000", + palette: whiteBlackPalette, + }, + { + color: "#cdd5d2", + palette: { + "50": { value: "oklch(0.97 0.005 171.759)" }, + "100": { value: "oklch(0.932 0.006 171.759)" }, + "200": { value: "oklch(0.857 0.008 171.759)" }, + "300": { value: "oklch(0.781 0.009 171.759)" }, + "400": { value: "oklch(0.706 0.01 171.759)" }, + "500": { value: "oklch(0.63 0.01 171.759)" }, + "600": { value: "oklch(0.554 0.01 171.759)" }, + "700": { value: "oklch(0.479 0.009 171.759)" }, + "800": { value: "oklch(0.403 0.008 171.759)" }, + "900": { value: "oklch(0.328 0.006 171.759)" }, + "950": { value: "oklch(0.29 0.005 171.759)" }, + }, + }, + ]; + + it("wrong hex", () => { + expect(generatePalette("#ttgh12")).toBe(undefined); + }); + + it("no six-digit form hex ", () => { + expect(generatePalette("#f12")).toBe(undefined); + }); + + it.each(paletteTestCase)("generate palette for $color", ({ color, palette }) => { + expect(generatePalette(color)).toStrictEqual(palette); + }); +}); diff --git a/airflow-core/src/airflow/ui/src/utils/generatePalette.ts b/airflow-core/src/airflow/ui/src/utils/generatePalette.ts new file mode 100644 index 0000000000000..673137d5792d8 --- /dev/null +++ b/airflow-core/src/airflow/ui/src/utils/generatePalette.ts @@ -0,0 +1,86 @@ +/*! + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { oklch } from "culori"; + +export type Palette = Record; + +export const roundTo3rdDecimal = (number: number) => { + const factor = 10 ** 3; + + return Math.round((number + Number.EPSILON) * factor) / factor; +}; + +export const quadraticLagrangeInterpolatingPolynomial = ( + point0: [number, number], + point1: [number, number], + point2: [number, number], +) => { + // For mathematical background see https://en.wikipedia.org/wiki/Lagrange_polynomial + const base0 = (value: number) => + ((value - point1[0]) / (point0[0] - point1[0])) * ((value - point2[0]) / (point0[0] - point2[0])); + const base1 = (value: number) => + ((value - point0[0]) / (point1[0] - point0[0])) * ((value - point2[0]) / (point1[0] - point2[0])); + const base2 = (value: number) => + ((value - point1[0]) / (point2[0] - point1[0])) * ((value - point0[0]) / (point2[0] - point0[0])); + + const linearCombinationBasis = (value: number) => + base0(value) * point0[1] + base1(value) * point1[1] + base2(value) * point2[1]; + + return linearCombinationBasis; +}; + +export const generatePalette = (hex: string): Palette | undefined => { + if (!/^#[\da-f]{6}$/iu.test(hex)) { + return undefined; + } + + const base = oklch(hex); + + if (!base) { + return undefined; + } + + let { c: chroma = 0, h: hue = 0 } = base; + + chroma = roundTo3rdDecimal(chroma); + hue = roundTo3rdDecimal(hue); + + const keys = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950]; + + const lightnessSteps = [0.97, 0.932, 0.857, 0.781, 0.706, 0.63, 0.554, 0.479, 0.403, 0.328, 0.29]; + + const chroma50 = chroma < 0.013 ? roundTo3rdDecimal(chroma / 2) : 0.013; + const chroma950 = chroma < 0.048 ? roundTo3rdDecimal(chroma / 2) : 0.048; + const chromaSteps = + chroma === 0 + ? () => chroma + : quadraticLagrangeInterpolatingPolynomial([50, chroma50], [500, chroma], [950, chroma950]); + + const palette: Palette = {}; + + keys.forEach((key, position) => { + const lightness = lightnessSteps[position]; + const newChroma = roundTo3rdDecimal(chromaSteps(key)); + const color = { value: `oklch(${lightness} ${newChroma} ${hue})` }; + + palette[key.toString()] = color; + }); + + return palette; +}; diff --git a/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_config.py b/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_config.py index e98ad44bb72ad..9682a423e2f5f 100644 --- a/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_config.py +++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_config.py @@ -34,6 +34,7 @@ "dashboard_alert": [], "show_external_log_redirect": False, "external_log_name": None, + "theme": "", } @@ -51,6 +52,7 @@ def mock_config_data(): ("api", "default_wrap"): "false", ("api", "auto_refresh_interval"): "3", ("api", "require_confirmation_dag_change"): "false", + ("api", "theme"): "", } ): yield diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt index ec25fe54498b6..77895acb45d9a 100644 --- a/docs/spelling_wordlist.txt +++ b/docs/spelling_wordlist.txt @@ -787,6 +787,7 @@ gpu gpus Grafana graphviz +grayscale greenlet Groupalia groupId