mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 01:15:03 +03:00
feat(studio): support themed pipeline destination logos (#51073)
## Problem Some destination marks need different assets to maintain contrast across light and dark surfaces. Their paths are also duplicated between Pipelines and Wrappers. ## Solution Add a shared brand-icon registry that supports either one asset or light and dark variants. Pipeline destination logos resolve against the active theme, while Wrappers continue using the light variant for their white logo tiles. This keeps single-asset destinations unchanged and preserves the existing monogram treatment where applicable. | Light | Dark | | --- | --- | | <img width="926" height="742" alt="CleanShot 2026-09-30 at 15 03 31@2x" src="https://github.com/user-attachments/assets/d76e5995-1bb8-4cb7-b991-c5dc1a5d8cb0" /> | <img width="926" height="732" alt="CleanShot 2026-09-30 at 15 03 03@2x" src="https://github.com/user-attachments/assets/83d45f30-3bf6-4ddc-8607-e27628cb4966" /> | ## Review instructions 1. Open `/project/<ref>/database/replication` with pipelines using the themed destination marks. 2. Switch between light and dark themes from the account menu. 3. Confirm each themed mark swaps assets and remains legible in the pipeline list, diagram, and child-route header. 4. Open Database Integrations and confirm the corresponding Wrapper tiles continue using their light-surface assets. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Style** * Updated the BigQuery, ClickHouse, DuckLake, and Snowflake icons in integration and replication views to use branded marks. Icons now use theme-appropriate variants where available, helping them display consistently across light and dark themes. ClickHouse’s previous “CH” monogram is replaced with its branded mark. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
1 parent
cf063c4ae8
commit
5fbf1ac4ff
8 files changed
+93
-9
No files matched your search
@@ -1,15 +1,17 @@
|
||||
import { useTheme } from 'next-themes'
|
||||
import { cn, StatusIcon } from 'ui'
|
||||
|
||||
import { DestinationIcon } from './DestinationIcon'
|
||||
import type { DestinationType } from './DestinationPanel/DestinationPanel.types'
|
||||
import { BASE_PATH } from '@/lib/constants'
|
||||
import { BRAND_ICONS, resolveThemedIconSrc, type ThemedIconSrc } from '@/lib/brand-icons'
|
||||
import { resolveThemeOverrideMode } from '@/lib/theme-overrides'
|
||||
|
||||
type DestinationBrand = { type: 'mark'; src: string } | { type: 'monogram'; label: string }
|
||||
type DestinationBrand = { type: 'mark'; src: ThemedIconSrc } | { type: 'monogram'; label: string }
|
||||
|
||||
const BRAND_BY_TYPE: Partial<Record<DestinationType, DestinationBrand>> = {
|
||||
BigQuery: { type: 'mark', src: `${BASE_PATH}/img/icons/bigquery-icon.svg` },
|
||||
ClickHouse: { type: 'monogram', label: 'CH' },
|
||||
DuckLake: { type: 'mark', src: `${BASE_PATH}/img/icons/ducklake-icon.svg` },
|
||||
BigQuery: { type: 'mark', src: BRAND_ICONS.bigquery },
|
||||
ClickHouse: { type: 'mark', src: BRAND_ICONS.clickhouse },
|
||||
DuckLake: { type: 'mark', src: BRAND_ICONS.ducklake },
|
||||
Snowflake: { type: 'monogram', label: 'SF' },
|
||||
}
|
||||
|
||||
@@ -36,8 +38,11 @@ export const DestinationLogo = ({
|
||||
className,
|
||||
hasErrors = false,
|
||||
}: DestinationLogoProps) => {
|
||||
const { resolvedTheme } = useTheme()
|
||||
const isDark = resolveThemeOverrideMode(resolvedTheme) === 'dark'
|
||||
const sizing = SIZE_CLASS_NAME[size]
|
||||
const brand = BRAND_BY_TYPE[type]
|
||||
const brandMarkSrc = brand?.type === 'mark' ? resolveThemedIconSrc(brand.src, isDark) : undefined
|
||||
|
||||
return (
|
||||
<span className={cn('relative inline-flex shrink-0', className)}>
|
||||
@@ -46,7 +51,7 @@ export const DestinationLogo = ({
|
||||
<DestinationIcon type={type} size={sizing.icon} className="text-foreground-light" />
|
||||
)}
|
||||
{brand?.type === 'mark' && (
|
||||
<img src={brand.src} alt="" aria-hidden className={sizing.mark} />
|
||||
<img src={brandMarkSrc} alt="" aria-hidden className={sizing.mark} />
|
||||
)}
|
||||
{brand?.type === 'monogram' && (
|
||||
<span
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { ServerOption, WrapperMeta } from './Wrappers.types'
|
||||
import { BRAND_ICONS, resolveLightIconSrc } from '@/lib/brand-icons'
|
||||
import { BASE_PATH, DOCS_URL } from '@/lib/constants'
|
||||
|
||||
export const WRAPPER_HANDLERS = {
|
||||
@@ -1472,7 +1473,8 @@ export const WRAPPERS: WrapperMeta[] = [
|
||||
name: 'clickhouse_wrapper',
|
||||
handlerName: WRAPPER_HANDLERS.CLICK_HOUSE,
|
||||
validatorName: 'click_house_fdw_validator',
|
||||
icon: `${BASE_PATH}/img/icons/clickhouse-icon.svg`,
|
||||
// Light variant: IntegrationLogo / wrapper cards force a white tile.
|
||||
icon: resolveLightIconSrc(BRAND_ICONS.clickhouse),
|
||||
description: 'Column-oriented analytics database',
|
||||
extensionName: 'ClickHouseFdw',
|
||||
label: 'ClickHouse',
|
||||
@@ -1518,7 +1520,7 @@ export const WRAPPERS: WrapperMeta[] = [
|
||||
name: 'bigquery_wrapper',
|
||||
handlerName: WRAPPER_HANDLERS.BIG_QUERY,
|
||||
validatorName: 'big_query_fdw_validator',
|
||||
icon: `${BASE_PATH}/img/icons/bigquery-icon.svg`,
|
||||
icon: resolveLightIconSrc(BRAND_ICONS.bigquery),
|
||||
description: 'Serverless data warehouse and analytics',
|
||||
extensionName: 'BigQueryFdw',
|
||||
label: 'BigQuery',
|
||||
@@ -2298,7 +2300,7 @@ export const WRAPPERS: WrapperMeta[] = [
|
||||
name: 'snowflake_wrapper',
|
||||
handlerName: WRAPPER_HANDLERS.SNOWFLAKE,
|
||||
validatorName: 'wasm_fdw_validator',
|
||||
icon: `${BASE_PATH}/img/icons/snowflake-icon.svg`,
|
||||
icon: resolveLightIconSrc(BRAND_ICONS.snowflake),
|
||||
description: 'Cloud data warehouse platform',
|
||||
extensionName: 'snowflakeFdw',
|
||||
label: 'Snowflake',
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { resolveLightIconSrc, resolveThemedIconSrc } from './brand-icons'
|
||||
|
||||
describe('resolveThemedIconSrc', () => {
|
||||
it('returns a single-asset path for either theme', () => {
|
||||
expect(resolveThemedIconSrc('/img/icons/bigquery-icon.svg', false)).toBe(
|
||||
'/img/icons/bigquery-icon.svg'
|
||||
)
|
||||
expect(resolveThemedIconSrc('/img/icons/bigquery-icon.svg', true)).toBe(
|
||||
'/img/icons/bigquery-icon.svg'
|
||||
)
|
||||
})
|
||||
|
||||
it('picks light or dark when both are provided', () => {
|
||||
const src = { light: '/light.svg', dark: '/dark.svg' }
|
||||
expect(resolveThemedIconSrc(src, false)).toBe('/light.svg')
|
||||
expect(resolveThemedIconSrc(src, true)).toBe('/dark.svg')
|
||||
})
|
||||
})
|
||||
|
||||
describe('resolveLightIconSrc', () => {
|
||||
it('returns the only asset or the light variant', () => {
|
||||
expect(resolveLightIconSrc('/img/icons/bigquery-icon.svg')).toBe('/img/icons/bigquery-icon.svg')
|
||||
expect(resolveLightIconSrc({ light: '/light.svg', dark: '/dark.svg' })).toBe('/light.svg')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,35 @@
|
||||
import { BASE_PATH } from '@/lib/constants'
|
||||
|
||||
/**
|
||||
* Brand mark paths shared by Pipelines (`DestinationLogo`) and Integrations
|
||||
* Wrappers. A single string is theme-agnostic; `{ light, dark }` opts into
|
||||
* per-theme assets.
|
||||
*
|
||||
* Wrappers / `IntegrationLogo` force a white tile, so they should always use
|
||||
* {@link resolveLightIconSrc}. DestinationLogo sits on `bg-surface-100` and
|
||||
* should resolve against the active theme.
|
||||
*/
|
||||
export type ThemedIconSrc = string | { light: string; dark: string }
|
||||
|
||||
export const BRAND_ICONS = {
|
||||
bigquery: `${BASE_PATH}/img/icons/bigquery-icon.svg`,
|
||||
clickhouse: {
|
||||
light: `${BASE_PATH}/img/icons/clickhouse-icon-on-light.svg`,
|
||||
dark: `${BASE_PATH}/img/icons/clickhouse-icon-on-dark.svg`,
|
||||
},
|
||||
ducklake: {
|
||||
light: `${BASE_PATH}/img/icons/ducklake-icon-on-light.svg`,
|
||||
dark: `${BASE_PATH}/img/icons/ducklake-icon-on-dark.svg`,
|
||||
},
|
||||
snowflake: `${BASE_PATH}/img/icons/snowflake-icon.svg`,
|
||||
} as const satisfies Record<string, ThemedIconSrc>
|
||||
|
||||
export function resolveThemedIconSrc(src: ThemedIconSrc, isDark: boolean): string {
|
||||
if (typeof src === 'string') return src
|
||||
return isDark ? src.dark : src.light
|
||||
}
|
||||
|
||||
/** Light (or only) variant — for surfaces that force a white logo tile. */
|
||||
export function resolveLightIconSrc(src: ThemedIconSrc): string {
|
||||
return typeof src === 'string' ? src : src.light
|
||||
}
|
||||
File renamed without changes.
@@ -0,0 +1,4 @@
|
||||
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="512" height="512" rx="36" fill="#141600"/>
|
||||
<path d="M90 89.9612C90 87.6505 91.6505 86 93.9612 86H123.67C125.981 86 127.631 87.6505 127.631 89.9612V422.369C127.631 424.68 125.981 426.33 123.67 426.33H93.9612C91.6505 426.33 90 424.68 90 422.369V89.9612ZM165.592 89.9612C165.592 87.6505 167.243 86 169.553 86H199.262C201.573 86 203.223 87.6505 203.223 89.9612V422.369C203.223 424.68 201.573 426.33 199.262 426.33H169.553C167.243 426.33 165.592 424.68 165.592 422.369V89.9612ZM241.184 89.9612C241.184 87.6505 242.835 86 245.146 86H274.854C277.165 86 278.816 87.6505 278.816 89.9612V422.369C278.816 424.68 277.165 426.33 274.854 426.33H245.146C242.835 426.33 241.184 424.68 241.184 422.369V89.9612ZM316.777 89.9612C316.777 87.6505 318.427 86 320.738 86H350.447C352.757 86 354.408 87.6505 354.408 89.9612V422.369C354.408 424.68 352.757 426.33 350.447 426.33H320.738C318.427 426.33 316.777 424.68 316.777 422.369V89.9612ZM392.369 222.33C392.369 220.019 394.019 218.369 396.33 218.369H426.039C428.35 218.369 430 220.019 430 222.33V290C430 292.311 428.35 293.961 426.039 293.961H396.33C394.019 293.961 392.369 292.311 392.369 290V222.33Z" fill="#FCFF74"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
File renamed without changes.
@@ -0,0 +1,11 @@
|
||||
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_ducklake_light)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M256 0C114.618 0 0 114.618 0 256C0 397.381 114.618 512 256 512C397.382 512 512 397.381 512 256C512 114.618 397.382 0 256 0Z" fill="#1A1A1A"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M223.708 129.455C270.255 129.455 310.982 154.472 332.508 191.71H355.782C387.782 191.71 414.546 217.31 414.546 249.89C414.546 281.89 388.364 308.654 355.782 308.654H338.908C319.708 351.127 277.817 380.799 228.946 382.545H217.309C150.4 379.054 97.4543 323.781 97.4543 256.29C97.4543 186.472 153.891 129.455 223.708 129.455ZM223.708 167.855C175.417 167.855 135.855 207.419 135.855 255.71C135.855 304.581 175.417 343.563 223.708 343.563C262.691 343.563 295.855 317.963 307.491 283.054L311.564 269.672H355.782C366.255 269.672 375.564 260.363 375.564 249.89C375.564 239.419 366.835 230.11 355.782 230.11H308.655L303.417 219.054C289.455 188.799 259.2 167.855 223.708 167.855Z" fill="#2EAFFF"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_ducklake_light">
|
||||
<rect width="512" height="512" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
Reference in new issue
Block a user