diff --git a/apps/mobile/src/features/tasks/components/PrStatusBadge.test.tsx b/apps/mobile/src/features/tasks/components/PrStatusBadge.test.tsx index a330030fde..357bb1f5d9 100644 --- a/apps/mobile/src/features/tasks/components/PrStatusBadge.test.tsx +++ b/apps/mobile/src/features/tasks/components/PrStatusBadge.test.tsx @@ -14,7 +14,7 @@ vi.mock("phosphor-react-native", () => ({ vi.mock("@/lib/theme", () => ({ useThemeColors: () => ({ gray: { 11: "#444444" }, - status: { success: "#00aa00", error: "#cc0000" }, + status: { success: "#00aa00", error: "#cc0000", merged: "#8800aa" }, }), toRgba: (hex: string, alpha: number) => `${hex}/${alpha}`, })); diff --git a/apps/mobile/src/features/tasks/components/PrStatusBadge.tsx b/apps/mobile/src/features/tasks/components/PrStatusBadge.tsx index 646b491866..e901cebaf5 100644 --- a/apps/mobile/src/features/tasks/components/PrStatusBadge.tsx +++ b/apps/mobile/src/features/tasks/components/PrStatusBadge.tsx @@ -1,3 +1,4 @@ +import { getPrVisualConfig } from "@posthog/core/git-interaction/prStatus"; import { GitMerge, GitPullRequest } from "phosphor-react-native"; import { Pressable } from "react-native"; import { openExternalUrl } from "@/lib/openExternalUrl"; @@ -13,11 +14,6 @@ interface PrStatusBadgeProps { size?: "sm" | "md"; } -// Mirrors the desktop "merged" PR color (Radix purple-9 family). Theme tokens -// don't include a purple, and merged-PR purple is recognisable enough that a -// fixed value works in both light and dark. -const MERGED_COLOR = "#8e4ec6"; - export function PrStatusBadge({ prUrl, hideWhenUnresolved = false, @@ -32,24 +28,23 @@ export function PrStatusBadge({ openExternalUrl(prUrl); }; - let color: string = themeColors.gray[11]; - let Icon: typeof GitPullRequest = GitPullRequest; - let label = "Open PR"; - - if (status?.merged) { - color = MERGED_COLOR; - Icon = GitMerge; - label = "Open merged PR"; - } else if (status?.state === "closed") { - color = themeColors.status.error; - label = "Open closed PR"; - } else if (status?.draft) { - color = themeColors.gray[11]; - label = "Open draft PR"; - } else if (status?.state === "open") { - color = themeColors.status.success; - label = "Open PR"; - } + const visual = getPrVisualConfig( + status?.state ?? "open", + status?.merged ?? false, + status?.draft ?? false, + ); + const colorByTone = { + gray: themeColors.gray[11], + green: themeColors.status.success, + red: themeColors.status.error, + purple: themeColors.status.merged, + } satisfies Record; + const color = colorByTone[visual.color]; + const Icon = visual.icon === "merged" ? GitMerge : GitPullRequest; + const label = + visual.label === "Open" + ? "Open PR" + : `Open ${visual.label.toLowerCase()} PR`; const box = size === "sm" ? "h-7 w-7" : "h-9 w-9"; const iconSize = size === "sm" ? 16 : 20; diff --git a/apps/mobile/src/lib/theme.ts b/apps/mobile/src/lib/theme.ts index 0fa4d0e819..813b82c3fe 100644 --- a/apps/mobile/src/lib/theme.ts +++ b/apps/mobile/src/lib/theme.ts @@ -1,95 +1,6 @@ +import { APP_COLOR_PALETTE, type AppThemeColors } from "@posthog/shared/theme"; import { useColorScheme, vars } from "nativewind"; -/** - * Single source of truth for all theme colors. - * Defined as hex for readability, converted to RGB for NativeWind vars(). - */ -// Color palette mirrored from the desktop app (apps/code globals.css). -// Light: slate gray + orange accent. Dark: slate gray + yellow accent. -const colors = { - light: { - gray: { - 1: "#f2f3ee", - 2: "#eceee8", - 3: "#e4e5de", - 4: "#d8dbd1", - 5: "#cbd0c3", - 6: "#bcc1b4", - 7: "#a9af9f", - 8: "#93998a", - 9: "#6b7165", - 10: "#5a6054", - 11: "#3a4036", - 12: "#0d0d0d", - }, - accent: { - 1: "#fff5f0", - 2: "#ffe8dc", - 3: "#ffd0b8", - 4: "#ffb38a", - 5: "#ff8f56", - 6: "#f57030", - 7: "#e05a18", - 8: "#c94800", - 9: "#f54d00", - 10: "#e64600", - 11: "#a33300", - 12: "#4d1800", - contrast: "#ffffff", - }, - status: { - success: "#16a34a", - error: "#dc2626", - warning: "#d97706", - info: "#2563eb", - }, - background: "#f2f3ee", - // "Card" surface — used for raised UI like buttons, composer card, pills. - // Pure white in light mode for max contrast against the cream background; - // gray-3 in dark mode so cards lift slightly off the bg. - card: "#ffffff", - }, - dark: { - gray: { - 1: "#131316", - 2: "#18181f", - 3: "#1e1e28", - 4: "#24243e", - 5: "#2a2a37", - 6: "#2e2e3d", - 7: "#40405a", - 8: "#616180", - 9: "#7c7c9e", - 10: "#8d8daa", - 11: "#9898b6", - 12: "#e6e6e6", - }, - accent: { - 1: "#14120a", - 2: "#1a1608", - 3: "#261e07", - 4: "#362900", - 5: "#443300", - 6: "#524007", - 7: "#6b561a", - 8: "#8c7230", - 9: "#f8be2a", - 10: "#ebb520", - 11: "#fcc84e", - 12: "#fde8b8", - contrast: "#1a1200", - }, - status: { - success: "#4ade80", - error: "#f87171", - warning: "#fbbf24", - info: "#60a5fa", - }, - background: "#131316", - card: "#1e1e28", - }, -} as const; - // Convert hex to RGB space-separated format for NativeWind vars() function hexToRgb(hex: string): string { const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); @@ -108,7 +19,7 @@ function hexToRgba(hex: string, alpha: number): string { } // Generate NativeWind vars() from color definitions -function createThemeVars(theme: (typeof colors)["light" | "dark"]) { +function createThemeVars(theme: AppThemeColors) { return vars({ "--gray-1": hexToRgb(theme.gray[1]), "--gray-2": hexToRgb(theme.gray[2]), @@ -145,11 +56,11 @@ function createThemeVars(theme: (typeof colors)["light" | "dark"]) { } // NativeWind vars() for runtime theming (used in root View style) -export const lightTheme = createThemeVars(colors.light); -export const darkTheme = createThemeVars(colors.dark); +export const lightTheme = createThemeVars(APP_COLOR_PALETTE.light); +export const darkTheme = createThemeVars(APP_COLOR_PALETTE.dark); // Types -export type ThemeColors = (typeof colors)["light" | "dark"]; +export type ThemeColors = AppThemeColors; /** * Hook to get raw hex color values for native components. @@ -162,7 +73,9 @@ export type ThemeColors = (typeof colors)["light" | "dark"]; */ export function useThemeColors(): ThemeColors { const { colorScheme } = useColorScheme(); - return colorScheme === "dark" ? colors.dark : colors.light; + return colorScheme === "dark" + ? APP_COLOR_PALETTE.dark + : APP_COLOR_PALETTE.light; } /** diff --git a/packages/shared/package.json b/packages/shared/package.json index 49925dfa55..7eaacedf4e 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -43,6 +43,10 @@ "./posthog-property-headers": { "types": "./dist/posthog-property-headers.d.ts", "import": "./dist/posthog-property-headers.js" + }, + "./theme": { + "types": "./dist/theme.d.ts", + "import": "./dist/theme.js" } }, "scripts": { diff --git a/packages/shared/src/theme.ts b/packages/shared/src/theme.ts new file mode 100644 index 0000000000..605c770ec4 --- /dev/null +++ b/packages/shared/src/theme.ts @@ -0,0 +1,93 @@ +export const APP_COLOR_PALETTE = { + light: { + gray: { + 1: "#f2f3ee", + 2: "#eceee8", + 3: "#e4e5de", + 4: "#d8dbd1", + 5: "#cbd0c3", + 6: "#bcc1b4", + 7: "#a9af9f", + 8: "#93998a", + 9: "#6b7165", + 10: "#5a6054", + 11: "#3a4036", + 12: "#0d0d0d", + }, + accent: { + 1: "#fff5f0", + 2: "#ffe8dc", + 3: "#ffd0b8", + 4: "#ffb38a", + 5: "#ff8f56", + 6: "#f57030", + 7: "#e05a18", + 8: "#c94800", + 9: "#f54d00", + 10: "#e64600", + 11: "#a33300", + 12: "#4d1800", + contrast: "#ffffff", + }, + status: { + success: "#16a34a", + error: "#dc2626", + warning: "#d97706", + info: "#2563eb", + merged: "#8e4ec6", + }, + background: "#f2f3ee", + card: "#ffffff", + }, + dark: { + gray: { + 1: "#131316", + 2: "#18181f", + 3: "#1e1e28", + 4: "#24243e", + 5: "#2a2a37", + 6: "#2e2e3d", + 7: "#40405a", + 8: "#616180", + 9: "#7c7c9e", + 10: "#8d8daa", + 11: "#9898b6", + 12: "#e6e6e6", + }, + accent: { + 1: "#14120a", + 2: "#1a1608", + 3: "#261e07", + 4: "#362900", + 5: "#443300", + 6: "#524007", + 7: "#6b561a", + 8: "#8c7230", + 9: "#f8be2a", + 10: "#ebb520", + 11: "#fcc84e", + 12: "#fde8b8", + contrast: "#1a1200", + }, + status: { + success: "#4ade80", + error: "#f87171", + warning: "#fbbf24", + info: "#60a5fa", + merged: "#8e4ec6", + }, + background: "#131316", + card: "#1e1e28", + }, +} as const; + +export type AppThemeColors = + (typeof APP_COLOR_PALETTE)[keyof typeof APP_COLOR_PALETTE]; + +export const POSTHOG_BRAND_COLORS = [ + APP_COLOR_PALETTE.light.accent[9], + APP_COLOR_PALETTE.dark.accent[9], + "#1d4aff", + "#000000", + "#ffffff", +] as const; diff --git a/packages/shared/tsup.config.ts b/packages/shared/tsup.config.ts index 4d3231a785..6636f10e6d 100644 --- a/packages/shared/tsup.config.ts +++ b/packages/shared/tsup.config.ts @@ -11,6 +11,7 @@ export default defineConfig({ "src/domain-types.ts", "src/mcp-sandbox-proxy.ts", "src/posthog-property-headers.ts", + "src/theme.ts", "src/types.ts", ], format: ["esm"], diff --git a/packages/ui/src/features/code-editor/theme/editorTheme.ts b/packages/ui/src/features/code-editor/theme/editorTheme.ts index 99dfab5388..a222b9cfaa 100644 --- a/packages/ui/src/features/code-editor/theme/editorTheme.ts +++ b/packages/ui/src/features/code-editor/theme/editorTheme.ts @@ -2,6 +2,7 @@ import { HighlightStyle, syntaxHighlighting } from "@codemirror/language"; import type { Extension } from "@codemirror/state"; import { EditorView } from "@codemirror/view"; import { tags as t } from "@lezer/highlight"; +import { APP_COLOR_PALETTE } from "@posthog/shared/theme"; function withAlpha(hex: string, alpha: number): string { const a = Math.round(alpha * 255) @@ -15,40 +16,42 @@ const dark = { chalky: "#e5c07b", coral: "#e06c75", cyan: "#56b6c2", - invalid: "#e6e6e6", - ivory: "#e6e6e6", - stone: "#9898b6", + invalid: APP_COLOR_PALETTE.dark.gray[12], + ivory: APP_COLOR_PALETTE.dark.gray[12], + stone: APP_COLOR_PALETTE.dark.gray[11], malibu: "#61afef", sage: "#98c379", whiskey: "#d19a66", violet: "#c678dd", - background: "#131316", - highlightBackground: "#1e1e28", - tooltipBackground: "#24243e", - selection: "#2e2e3d", - cursor: "#f8be2a", + background: APP_COLOR_PALETTE.dark.background, + highlightBackground: APP_COLOR_PALETTE.dark.gray[3], + tooltipBackground: APP_COLOR_PALETTE.dark.gray[4], + selection: APP_COLOR_PALETTE.dark.gray[6], + cursor: APP_COLOR_PALETTE.dark.accent[9], }; +type EditorColors = { [K in keyof typeof dark]: string }; + // PostHog-inspired light theme colors const light = { chalky: "#c18401", coral: "#c45649", cyan: "#0184bc", - invalid: "#0d0d0d", + invalid: APP_COLOR_PALETTE.light.gray[12], ivory: "#1a1d17", - stone: "#6b7165", + stone: APP_COLOR_PALETTE.light.gray[9], malibu: "#4078f2", sage: "#50a14f", whiskey: "#986801", violet: "#a626a4", - background: "#f2f3ee", - highlightBackground: "#e4e5de", - tooltipBackground: "#eceee8", - selection: "#d8dbd1", - cursor: "#f54d00", + background: APP_COLOR_PALETTE.light.background, + highlightBackground: APP_COLOR_PALETTE.light.gray[3], + tooltipBackground: APP_COLOR_PALETTE.light.gray[2], + selection: APP_COLOR_PALETTE.light.gray[4], + cursor: APP_COLOR_PALETTE.light.accent[9], }; -function createEditorTheme(colors: typeof dark, isDark: boolean) { +function createEditorTheme(colors: EditorColors, isDark: boolean) { return EditorView.theme( { "&": { @@ -184,7 +187,7 @@ function createEditorTheme(colors: typeof dark, isDark: boolean) { ); } -function createHighlightStyle(colors: typeof dark) { +function createHighlightStyle(colors: EditorColors) { return HighlightStyle.define([ { tag: t.keyword, color: colors.violet }, { diff --git a/packages/ui/src/features/terminal/TerminalManager.ts b/packages/ui/src/features/terminal/TerminalManager.ts index e8ce975bb6..e12b1a59d5 100644 --- a/packages/ui/src/features/terminal/TerminalManager.ts +++ b/packages/ui/src/features/terminal/TerminalManager.ts @@ -1,6 +1,7 @@ import { DEFAULT_TERMINAL_FONT_FAMILY } from "@posthog/core/terminal/resolveTerminalFontFamily"; import { resolveService } from "@posthog/di/container"; import { getErrorMessage } from "@posthog/shared"; +import { APP_COLOR_PALETTE } from "@posthog/shared/theme"; import { SHELL_CLIENT, type ShellClient, @@ -89,22 +90,22 @@ type Listener = (payload: EventPayloadMap[T]) => void; function getTerminalTheme(isDarkMode: boolean) { return isDarkMode ? { - background: "#131316", - foreground: "#e6e6e6", - cursor: "#f8be2a", - cursorAccent: "#131316", + background: APP_COLOR_PALETTE.dark.background, + foreground: APP_COLOR_PALETTE.dark.gray[12], + cursor: APP_COLOR_PALETTE.dark.accent[9], + cursorAccent: APP_COLOR_PALETTE.dark.background, selectionBackground: "rgba(248, 190, 42, 0.25)", selectionInactiveBackground: "rgba(248, 190, 42, 0.12)", - selectionForeground: "#e6e6e6", + selectionForeground: APP_COLOR_PALETTE.dark.gray[12], } : { - background: "#f2f3ee", - foreground: "#3a4036", - cursor: "#f54d00", - cursorAccent: "#f2f3ee", + background: APP_COLOR_PALETTE.light.background, + foreground: APP_COLOR_PALETTE.light.gray[11], + cursor: APP_COLOR_PALETTE.light.accent[9], + cursorAccent: APP_COLOR_PALETTE.light.background, selectionBackground: "#fbd0b8", selectionInactiveBackground: "#f3e2d6", - selectionForeground: "#3a4036", + selectionForeground: APP_COLOR_PALETTE.light.gray[11], }; } diff --git a/packages/ui/src/primitives/confetti.ts b/packages/ui/src/primitives/confetti.ts index 46c03bfab9..eacfb3563b 100644 --- a/packages/ui/src/primitives/confetti.ts +++ b/packages/ui/src/primitives/confetti.ts @@ -1,7 +1,6 @@ +import { POSTHOG_BRAND_COLORS } from "@posthog/shared/theme"; import confetti from "canvas-confetti"; -const POSTHOG_COLORS = ["#f54d00", "#f8be2a", "#1d4aff", "#000000", "#ffffff"]; - function reducedMotion(): boolean { return ( window.matchMedia?.("(prefers-reduced-motion: reduce)").matches ?? false @@ -14,7 +13,7 @@ export function celebrate(options?: confetti.Options): void { particleCount: 80, spread: 70, origin: { y: 0.7 }, - colors: POSTHOG_COLORS, + colors: [...POSTHOG_BRAND_COLORS], ...options, }); } @@ -28,14 +27,14 @@ export function shipIt(): void { angle: 60, spread: 55, origin: { x: 0, y: 0.8 }, - colors: POSTHOG_COLORS, + colors: [...POSTHOG_BRAND_COLORS], }); confetti({ particleCount: 6, angle: 120, spread: 55, origin: { x: 1, y: 0.8 }, - colors: POSTHOG_COLORS, + colors: [...POSTHOG_BRAND_COLORS], }); if (Date.now() < end) requestAnimationFrame(fire); }; @@ -55,7 +54,7 @@ export function fireFrom( spread: 60, startVelocity: 35, origin: { x, y }, - colors: POSTHOG_COLORS, + colors: [...POSTHOG_BRAND_COLORS], ...options, }); }