From 288d7adcb2b0002e76b447eb25420703543fcde9 Mon Sep 17 00:00:00 2001 From: Danny Roosevelt Date: Thu, 4 Dec 2025 20:23:13 +1100 Subject: [PATCH 1/7] Dark mode polish --- packages/connect-react/package.json | 1 + .../src/components/ControlApp.tsx | 44 ++++++++++ .../src/components/SelectApp.tsx | 83 ++++++++++++++++++- .../src/components/SelectComponent.tsx | 76 ++++++++++++++++- .../src/hooks/customization-context.tsx | 2 + 5 files changed, 204 insertions(+), 2 deletions(-) diff --git a/packages/connect-react/package.json b/packages/connect-react/package.json index bcd1a8a0d89f4..d7039ff9eeaaa 100644 --- a/packages/connect-react/package.json +++ b/packages/connect-react/package.json @@ -6,6 +6,7 @@ "dist" ], "type": "module", + "main": "./dist/connect-react.umd.js", "browser": "./dist/connect-react.umd.js", "module": "./dist/connect-react.es.js", "types": "./dist/connect-react.umd.d.ts", diff --git a/packages/connect-react/src/components/ControlApp.tsx b/packages/connect-react/src/components/ControlApp.tsx index 2c97211dfd320..4e72cb86f16ac 100644 --- a/packages/connect-react/src/components/ControlApp.tsx +++ b/packages/connect-react/src/components/ControlApp.tsx @@ -5,6 +5,7 @@ import { useFormFieldContext } from "../hooks/form-field-context"; import { useFormContext } from "../hooks/form-context"; import { useCustomize } from "../hooks/customization-context"; import type { BaseReactSelectProps } from "../hooks/customization-context"; +import { defaultTheme } from "../theme"; import { useMemo } from "react"; import type { CSSProperties } from "react"; import type { OptionProps } from "react-select"; @@ -48,6 +49,23 @@ export function ControlApp({ app }: ControlAppProps) { const { getProps, select, theme, } = useCustomize(); + const resolveColor = ( + key: keyof typeof defaultTheme.colors, + fallback: string, + ) => { + const current = theme.colors[key]; + const baseline = defaultTheme.colors[key]; + return current && current !== baseline + ? current + : fallback; + }; + + const surface = resolveColor("neutral0", "var(--pd-surface, #0f1011)"); + const border = resolveColor("neutral20", "var(--pd-border, rgba(255,255,255,0.16))"); + const text = resolveColor("neutral80", "var(--pd-text, #e5e7eb)"); + const textStrong = resolveColor("neutral90", "var(--pd-text-strong, #f9fafb)"); + const primary25 = resolveColor("primary25", "var(--pd-primary-25, rgba(59,130,246,0.18))"); + const surfaceStrong = resolveColor("neutral10", "var(--pd-surface-strong, rgba(255,255,255,0.06))"); const baseStyles: CSSProperties = { color: theme.colors.neutral60, @@ -71,8 +89,34 @@ export function ControlApp({ app }: ControlAppProps) { control: (base) => ({ ...base, gridArea: "control", + backgroundColor: surface, + borderColor: border, + color: text, boxShadow: theme.boxShadow.input, }), + menu: (base) => ({ + ...base, + backgroundColor: surface, + boxShadow: theme.boxShadow.dropdown, + }), + singleValue: (base) => ({ + ...base, + color: text, + }), + input: (base) => ({ + ...base, + color: text, + }), + option: (base, state) => ({ + ...base, + backgroundColor: state.isSelected + ? primary25 + : state.isFocused + ? surfaceStrong + : base.backgroundColor + ?? surface, + color: textStrong, + }), }, }; const selectProps = select.getProps("controlAppSelect", baseSelectProps); diff --git a/packages/connect-react/src/components/SelectApp.tsx b/packages/connect-react/src/components/SelectApp.tsx index d44989e8d2852..bee97edac8314 100644 --- a/packages/connect-react/src/components/SelectApp.tsx +++ b/packages/connect-react/src/components/SelectApp.tsx @@ -6,6 +6,11 @@ import type { MenuListProps, OptionProps, SingleValueProps, } from "react-select"; import { useApps } from "../hooks/use-apps"; +import { defaultTheme } from "../theme"; +import { + useCustomize, + type BaseReactSelectProps, +} from "../hooks/customization-context"; import type { App, AppsListRequest, @@ -62,6 +67,7 @@ export function SelectApp({ SingleValue, MenuList, } = components; + const { select, theme } = useCustomize(); const isLoadingMoreRef = useRef(isLoadingMore); isLoadingMoreRef.current = isLoadingMore; @@ -87,6 +93,70 @@ export function SelectApp({ loadMore, ]); + const resolveColor = ( + key: keyof typeof defaultTheme.colors, + fallback: string, + ) => { + const current = theme.colors[key]; + const baseline = defaultTheme.colors[key]; + return current && current !== baseline + ? current + : fallback; + }; + + const surface = resolveColor("neutral0", "#18181b"); + const border = resolveColor("neutral20", "rgba(255,255,255,0.16)"); + const text = resolveColor("neutral80", "#a1a1aa"); + const textStrong = resolveColor("neutral90", "#e4e4e7"); + // Hover state - visible gray + const hoverBg = "#27272a"; + // Selected state - subtle blue + const selectedBg = "rgba(59,130,246,0.2)"; + // Selected + hover - brighter blue + const selectedHoverBg = "rgba(59,130,246,0.35)"; + + const baseSelectProps: BaseReactSelectProps = { + styles: { + control: (base) => ({ + ...base, + backgroundColor: surface, + borderColor: border, + color: text, + boxShadow: theme.boxShadow.input, + }), + menu: (base) => ({ + ...base, + backgroundColor: surface, + boxShadow: theme.boxShadow.dropdown, + }), + singleValue: (base) => ({ + ...base, + color: text, + }), + input: (base) => ({ + ...base, + color: text, + }), + option: (base, state) => { + let bg = surface; + if (state.isSelected && state.isFocused) { + bg = selectedHoverBg; + } else if (state.isSelected) { + bg = selectedBg; + } else if (state.isFocused) { + bg = hoverBg; + } + return { + ...base, + backgroundColor: bg, + color: textStrong, + }; + }, + }, + }; + + const selectProps = select.getProps("selectApp", baseSelectProps); + // Memoize custom components to prevent remounting const customComponents = useMemo(() => ({ Option: (optionProps: OptionProps) => ( @@ -100,6 +170,9 @@ export function SelectApp({ style={{ height: 24, width: 24, + backgroundColor: "#fff", + borderRadius: 6, + padding: 2, }} alt={optionProps.data.name} /> @@ -121,6 +194,9 @@ export function SelectApp({ style={{ height: 24, width: 24, + backgroundColor: "#fff", + borderRadius: 6, + padding: 2, }} alt={singleValueProps.data.name} /> @@ -157,8 +233,12 @@ export function SelectApp({ o.name || o.key} @@ -97,7 +167,10 @@ export function SelectComponent({ onChange={(o) => onChange?.((o as Component) || undefined)} onMenuScrollToBottom={handleMenuScrollToBottom} isLoading={isLoading} - components={customComponents} + components={{ + ...selectProps.components, + ...customComponents, + }} menuPortalTarget={ typeof document !== "undefined" ? document.body @@ -105,6 +178,7 @@ export function SelectComponent({ } menuPosition="fixed" styles={{ + ...(selectProps.styles ?? {}), menuPortal: (base) => ({ ...base, zIndex: 99999, diff --git a/packages/connect-react/src/hooks/customization-context.tsx b/packages/connect-react/src/hooks/customization-context.tsx index bd9b377ffa87c..f9422dc789148 100644 --- a/packages/connect-react/src/hooks/customization-context.tsx +++ b/packages/connect-react/src/hooks/customization-context.tsx @@ -54,6 +54,8 @@ export const defaultComponents = { export type ReactSelectComponents = { controlAppSelect: typeof ControlApp; controlSelect: typeof ControlSelect; + selectApp: never; + selectComponent: never; }; export type CustomComponents> = { From 67fd47d367b248823d1ab99d9128289bd9248236 Mon Sep 17 00:00:00 2001 From: Danny Roosevelt Date: Thu, 4 Dec 2025 20:27:54 +1100 Subject: [PATCH 2/7] Linting --- packages/connect-react/src/components/SelectApp.tsx | 4 +++- packages/connect-react/src/components/SelectComponent.tsx | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/connect-react/src/components/SelectApp.tsx b/packages/connect-react/src/components/SelectApp.tsx index bee97edac8314..4750e98000cb1 100644 --- a/packages/connect-react/src/components/SelectApp.tsx +++ b/packages/connect-react/src/components/SelectApp.tsx @@ -67,7 +67,9 @@ export function SelectApp({ SingleValue, MenuList, } = components; - const { select, theme } = useCustomize(); + const { + select, theme, + } = useCustomize(); const isLoadingMoreRef = useRef(isLoadingMore); isLoadingMoreRef.current = isLoadingMore; diff --git a/packages/connect-react/src/components/SelectComponent.tsx b/packages/connect-react/src/components/SelectComponent.tsx index d8e2b375e3938..578000d0893e7 100644 --- a/packages/connect-react/src/components/SelectComponent.tsx +++ b/packages/connect-react/src/components/SelectComponent.tsx @@ -43,7 +43,9 @@ export function SelectComponent({ }); const { MenuList } = components; - const { select, theme } = useCustomize(); + const { + select, theme, + } = useCustomize(); const resolveColor = ( key: keyof typeof defaultTheme.colors, fallback: string, From d389f09223cb03904ee423b0655d883e286f4eff Mon Sep 17 00:00:00 2001 From: Danny Roosevelt Date: Thu, 4 Dec 2025 20:34:12 +1100 Subject: [PATCH 3/7] pr feedback --- .../src/components/ControlApp.tsx | 39 +++++++++++-------- .../src/components/SelectApp.tsx | 10 ++--- .../src/components/SelectComponent.tsx | 10 ++--- packages/connect-react/src/theme.ts | 8 ++++ 4 files changed, 39 insertions(+), 28 deletions(-) diff --git a/packages/connect-react/src/components/ControlApp.tsx b/packages/connect-react/src/components/ControlApp.tsx index 4e72cb86f16ac..f59c0dc0bb976 100644 --- a/packages/connect-react/src/components/ControlApp.tsx +++ b/packages/connect-react/src/components/ControlApp.tsx @@ -60,12 +60,14 @@ export function ControlApp({ app }: ControlAppProps) { : fallback; }; - const surface = resolveColor("neutral0", "var(--pd-surface, #0f1011)"); - const border = resolveColor("neutral20", "var(--pd-border, rgba(255,255,255,0.16))"); - const text = resolveColor("neutral80", "var(--pd-text, #e5e7eb)"); - const textStrong = resolveColor("neutral90", "var(--pd-text-strong, #f9fafb)"); - const primary25 = resolveColor("primary25", "var(--pd-primary-25, rgba(59,130,246,0.18))"); - const surfaceStrong = resolveColor("neutral10", "var(--pd-surface-strong, rgba(255,255,255,0.06))"); + const surface = resolveColor("neutral0", "#18181b"); + const border = resolveColor("neutral20", "rgba(255,255,255,0.16)"); + const text = resolveColor("neutral80", "#a1a1aa"); + const textStrong = resolveColor("neutral90", "#e4e4e7"); + // Option state backgrounds - theme-aware with dark mode fallbacks + const hoverBg = theme.colors.optionHover ?? "#27272a"; + const selectedBg = theme.colors.optionSelected ?? "rgba(59,130,246,0.2)"; + const selectedHoverBg = theme.colors.optionSelectedHover ?? "rgba(59,130,246,0.35)"; const baseStyles: CSSProperties = { color: theme.colors.neutral60, @@ -107,16 +109,21 @@ export function ControlApp({ app }: ControlAppProps) { ...base, color: text, }), - option: (base, state) => ({ - ...base, - backgroundColor: state.isSelected - ? primary25 - : state.isFocused - ? surfaceStrong - : base.backgroundColor - ?? surface, - color: textStrong, - }), + option: (base, state) => { + let bg = surface; + if (state.isSelected && state.isFocused) { + bg = selectedHoverBg; + } else if (state.isSelected) { + bg = selectedBg; + } else if (state.isFocused) { + bg = hoverBg; + } + return { + ...base, + backgroundColor: bg, + color: textStrong, + }; + }, }, }; const selectProps = select.getProps("controlAppSelect", baseSelectProps); diff --git a/packages/connect-react/src/components/SelectApp.tsx b/packages/connect-react/src/components/SelectApp.tsx index 4750e98000cb1..36d02fa21bebd 100644 --- a/packages/connect-react/src/components/SelectApp.tsx +++ b/packages/connect-react/src/components/SelectApp.tsx @@ -110,12 +110,10 @@ export function SelectApp({ const border = resolveColor("neutral20", "rgba(255,255,255,0.16)"); const text = resolveColor("neutral80", "#a1a1aa"); const textStrong = resolveColor("neutral90", "#e4e4e7"); - // Hover state - visible gray - const hoverBg = "#27272a"; - // Selected state - subtle blue - const selectedBg = "rgba(59,130,246,0.2)"; - // Selected + hover - brighter blue - const selectedHoverBg = "rgba(59,130,246,0.35)"; + // Option state backgrounds - theme-aware with dark mode fallbacks + const hoverBg = theme.colors.optionHover ?? "#27272a"; + const selectedBg = theme.colors.optionSelected ?? "rgba(59,130,246,0.2)"; + const selectedHoverBg = theme.colors.optionSelectedHover ?? "rgba(59,130,246,0.35)"; const baseSelectProps: BaseReactSelectProps = { styles: { diff --git a/packages/connect-react/src/components/SelectComponent.tsx b/packages/connect-react/src/components/SelectComponent.tsx index 578000d0893e7..56203b1828e11 100644 --- a/packages/connect-react/src/components/SelectComponent.tsx +++ b/packages/connect-react/src/components/SelectComponent.tsx @@ -61,12 +61,10 @@ export function SelectComponent({ const border = resolveColor("neutral20", "rgba(255,255,255,0.16)"); const text = resolveColor("neutral80", "#a1a1aa"); const textStrong = resolveColor("neutral90", "#e4e4e7"); - // Hover state - visible gray - const hoverBg = "#27272a"; - // Selected state - subtle blue - const selectedBg = "rgba(59,130,246,0.2)"; - // Selected + hover - brighter blue - const selectedHoverBg = "rgba(59,130,246,0.35)"; + // Option state backgrounds - theme-aware with dark mode fallbacks + const hoverBg = theme.colors.optionHover ?? "#27272a"; + const selectedBg = theme.colors.optionSelected ?? "rgba(59,130,246,0.2)"; + const selectedHoverBg = theme.colors.optionSelectedHover ?? "rgba(59,130,246,0.35)"; const isLoadingMoreRef = useRef(isLoadingMore); isLoadingMoreRef.current = isLoadingMore; diff --git a/packages/connect-react/src/theme.ts b/packages/connect-react/src/theme.ts index 0c670d8d5516f..ce08bf3f45b63 100644 --- a/packages/connect-react/src/theme.ts +++ b/packages/connect-react/src/theme.ts @@ -59,6 +59,14 @@ export type Colors = { // select.singleValue:color neutral80: string; neutral90: string; + + // Option state backgrounds (dark mode friendly) + // select.option:hover:backgroundColor + optionHover?: string; + // select.option:selected:backgroundColor + optionSelected?: string; + // select.option:selected:hover:backgroundColor + optionSelectedHover?: string; }; export type Shadows = { From 73177bb4594f728d0d6c132df4e755f5dfdec61b Mon Sep 17 00:00:00 2001 From: Danny Roosevelt Date: Thu, 4 Dec 2025 20:59:51 +1100 Subject: [PATCH 4/7] pr feedback --- .../src/components/SelectApp.tsx | 66 +++++---------- .../src/components/SelectComponent.tsx | 68 +++++----------- .../connect-react/src/utils/select-styles.ts | 81 +++++++++++++++++++ 3 files changed, 121 insertions(+), 94 deletions(-) create mode 100644 packages/connect-react/src/utils/select-styles.ts diff --git a/packages/connect-react/src/components/SelectApp.tsx b/packages/connect-react/src/components/SelectApp.tsx index 36d02fa21bebd..a03f7d49a5e70 100644 --- a/packages/connect-react/src/components/SelectApp.tsx +++ b/packages/connect-react/src/components/SelectApp.tsx @@ -6,11 +6,11 @@ import type { MenuListProps, OptionProps, SingleValueProps, } from "react-select"; import { useApps } from "../hooks/use-apps"; -import { defaultTheme } from "../theme"; import { useCustomize, type BaseReactSelectProps, } from "../hooks/customization-context"; +import { createBaseSelectStyles } from "../utils/select-styles"; import type { App, AppsListRequest, @@ -95,64 +95,36 @@ export function SelectApp({ loadMore, ]); + // Resolve theme color with fallback - uses theme value if defined, otherwise fallback const resolveColor = ( - key: keyof typeof defaultTheme.colors, + key: keyof typeof theme.colors, fallback: string, - ) => { + ): string => { const current = theme.colors[key]; - const baseline = defaultTheme.colors[key]; - return current && current !== baseline - ? current - : fallback; + return current !== undefined ? current : fallback; }; const surface = resolveColor("neutral0", "#18181b"); const border = resolveColor("neutral20", "rgba(255,255,255,0.16)"); const text = resolveColor("neutral80", "#a1a1aa"); const textStrong = resolveColor("neutral90", "#e4e4e7"); - // Option state backgrounds - theme-aware with dark mode fallbacks - const hoverBg = theme.colors.optionHover ?? "#27272a"; - const selectedBg = theme.colors.optionSelected ?? "rgba(59,130,246,0.2)"; - const selectedHoverBg = theme.colors.optionSelectedHover ?? "rgba(59,130,246,0.35)"; + const hoverBg = resolveColor("optionHover", "#27272a"); + const selectedBg = resolveColor("optionSelected", "rgba(59,130,246,0.2)"); + const selectedHoverBg = resolveColor("optionSelectedHover", "rgba(59,130,246,0.35)"); const baseSelectProps: BaseReactSelectProps = { - styles: { - control: (base) => ({ - ...base, - backgroundColor: surface, - borderColor: border, - color: text, - boxShadow: theme.boxShadow.input, - }), - menu: (base) => ({ - ...base, - backgroundColor: surface, - boxShadow: theme.boxShadow.dropdown, - }), - singleValue: (base) => ({ - ...base, - color: text, - }), - input: (base) => ({ - ...base, - color: text, - }), - option: (base, state) => { - let bg = surface; - if (state.isSelected && state.isFocused) { - bg = selectedHoverBg; - } else if (state.isSelected) { - bg = selectedBg; - } else if (state.isFocused) { - bg = hoverBg; - } - return { - ...base, - backgroundColor: bg, - color: textStrong, - }; + styles: createBaseSelectStyles({ + colors: { + surface, + border, + text, + textStrong, + hoverBg, + selectedBg, + selectedHoverBg, }, - }, + boxShadow: theme.boxShadow, + }), }; const selectProps = select.getProps("selectApp", baseSelectProps); diff --git a/packages/connect-react/src/components/SelectComponent.tsx b/packages/connect-react/src/components/SelectComponent.tsx index 56203b1828e11..9e122d5c79341 100644 --- a/packages/connect-react/src/components/SelectComponent.tsx +++ b/packages/connect-react/src/components/SelectComponent.tsx @@ -11,11 +11,11 @@ import { import Select, { components } from "react-select"; import type { MenuListProps } from "react-select"; import { useComponents } from "../hooks/use-components"; -import { defaultTheme } from "../theme"; import { useCustomize, type BaseReactSelectProps, } from "../hooks/customization-context"; +import { createBaseSelectStyles } from "../utils/select-styles"; type SelectComponentProps = { app?: Partial & { nameSlug: string; }; @@ -46,25 +46,24 @@ export function SelectComponent({ const { select, theme, } = useCustomize(); + + // Resolve theme color with fallback - uses theme value if defined, otherwise fallback const resolveColor = ( - key: keyof typeof defaultTheme.colors, + key: keyof typeof theme.colors, fallback: string, - ) => { + ): string => { const current = theme.colors[key]; - const baseline = defaultTheme.colors[key]; - return current && current !== baseline - ? current - : fallback; + return current !== undefined ? current : fallback; }; const surface = resolveColor("neutral0", "#18181b"); const border = resolveColor("neutral20", "rgba(255,255,255,0.16)"); const text = resolveColor("neutral80", "#a1a1aa"); const textStrong = resolveColor("neutral90", "#e4e4e7"); - // Option state backgrounds - theme-aware with dark mode fallbacks - const hoverBg = theme.colors.optionHover ?? "#27272a"; - const selectedBg = theme.colors.optionSelected ?? "rgba(59,130,246,0.2)"; - const selectedHoverBg = theme.colors.optionSelectedHover ?? "rgba(59,130,246,0.35)"; + const hoverBg = resolveColor("optionHover", "#27272a"); + const selectedBg = resolveColor("optionSelected", "rgba(59,130,246,0.2)"); + const selectedHoverBg = resolveColor("optionSelectedHover", "rgba(59,130,246,0.35)"); + const isLoadingMoreRef = useRef(isLoadingMore); isLoadingMoreRef.current = isLoadingMore; @@ -88,43 +87,18 @@ export function SelectComponent({ ]); const baseSelectProps: BaseReactSelectProps = { - styles: { - control: (base) => ({ - ...base, - backgroundColor: surface, - borderColor: border, - color: text, - boxShadow: theme.boxShadow.input, - }), - menu: (base) => ({ - ...base, - backgroundColor: surface, - boxShadow: theme.boxShadow.dropdown, - }), - singleValue: (base) => ({ - ...base, - color: text, - }), - input: (base) => ({ - ...base, - color: text, - }), - option: (base, state) => { - let bg = surface; - if (state.isSelected && state.isFocused) { - bg = selectedHoverBg; - } else if (state.isSelected) { - bg = selectedBg; - } else if (state.isFocused) { - bg = hoverBg; - } - return { - ...base, - backgroundColor: bg, - color: textStrong, - }; + styles: createBaseSelectStyles({ + colors: { + surface, + border, + text, + textStrong, + hoverBg, + selectedBg, + selectedHoverBg, }, - }, + boxShadow: theme.boxShadow, + }), }; const selectProps = select.getProps("selectComponent", baseSelectProps); diff --git a/packages/connect-react/src/utils/select-styles.ts b/packages/connect-react/src/utils/select-styles.ts new file mode 100644 index 0000000000000..0e81c5a63c3e5 --- /dev/null +++ b/packages/connect-react/src/utils/select-styles.ts @@ -0,0 +1,81 @@ +import type { CSSObjectWithLabel, GroupBase, StylesConfig } from "react-select"; +import type { Shadows } from "../theme"; + +export type SelectColorConfig = { + surface: string; + border: string; + text: string; + textStrong: string; + hoverBg: string; + selectedBg: string; + selectedHoverBg: string; +}; + +export type SelectStyleConfig = { + colors: SelectColorConfig; + boxShadow: Shadows; +}; + +/** + * Creates base styles for react-select components with dark mode support. + * Shared across SelectApp, SelectComponent, and ControlApp. + */ +export function createBaseSelectStyles< + Option, + IsMulti extends boolean = false, + Group extends GroupBase