Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: custom themes #26056

Open
wants to merge 28 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
3d12162
feat: custom themes
daibhin Nov 7, 2024
856ecfc
Update UI snapshots for `chromium` (1)
github-actions[bot] Nov 7, 2024
3ed4733
add custom styles to body style attr
ghoti143 Nov 7, 2024
3bcadcb
code editor
ghoti143 Nov 7, 2024
243aba5
Update UI snapshots for `chromium` (1)
github-actions[bot] Nov 7, 2024
78eb43b
put custom styles in head
ghoti143 Nov 7, 2024
3acce15
Merge branch 'dn-feat/custom-themes' of https://github.com/PostHog/po…
ghoti143 Nov 7, 2024
4df9200
Update UI snapshots for `chromium` (1)
github-actions[bot] Nov 7, 2024
5a21569
theme library
daibhin Nov 7, 2024
df09b1e
Merge branch 'dn-feat/custom-themes' of github.com:PostHog/posthog in…
daibhin Nov 7, 2024
6d657c6
remove v0
ghoti143 Nov 7, 2024
95ef27b
Update UI snapshots for `chromium` (1)
github-actions[bot] Nov 7, 2024
6644185
light / dark theme options
daibhin Nov 7, 2024
308b2de
Merge branch 'dn-feat/custom-themes' of github.com:PostHog/posthog in…
daibhin Nov 7, 2024
ca518fd
custom theme saving
daibhin Nov 7, 2024
74b0948
demo
daibhin Nov 7, 2024
0655c49
theme picker
daibhin Nov 11, 2024
14288dd
Merge branch 'master' into dn-feat/custom-themes
daibhin Nov 11, 2024
a739b51
Merge branch 'master' into dn-feat/custom-themes
daibhin Nov 19, 2024
8e995b6
custom themes minus previewing
daibhin Nov 19, 2024
425c2ed
Merge branch 'master' into dn-feat/custom-themes
daibhin Nov 21, 2024
1ac5df7
editing dialog
daibhin Nov 21, 2024
8ea5dd7
barbie v1
daibhin Nov 21, 2024
176aef3
Merge branch 'master' into dn-feat/custom-themes
daibhin Nov 22, 2024
0b8bd27
reset snapshots
daibhin Nov 22, 2024
5bc85c4
barbie theme
daibhin Nov 22, 2024
43fc3af
fix typing
daibhin Nov 22, 2024
e61d718
tweak theme
daibhin Nov 22, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions frontend/src/layout/GlobalModals.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { CreateProjectModal } from 'scenes/project/CreateProjectModal'
import { SessionPlayerModal } from 'scenes/session-recordings/player/modal/SessionPlayerModal'
import { inviteLogic } from 'scenes/settings/organization/inviteLogic'
import { InviteModal } from 'scenes/settings/organization/InviteModal'
import { PreviewingCustomCssModal } from 'scenes/themes/PreviewingCustomCssModal'
import { userLogic } from 'scenes/userLogic'

import type { globalModalsLogicType } from './GlobalModalsType'
Expand Down Expand Up @@ -70,6 +71,7 @@ export function GlobalModals(): JSX.Element {
<ConfirmUpgradeModal />
<TimeSensitiveAuthenticationModal />
<SessionPlayerModal />
<PreviewingCustomCssModal />
{user && user.organization?.enforce_2fa && !user.is_2fa_enabled && (
<LemonModal title="Set up 2FA" closable={false}>
<p>
Expand Down
33 changes: 32 additions & 1 deletion frontend/src/layout/navigation-3000/themeLogic.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { actions, connect, events, kea, path, reducers, selectors } from 'kea'
import { actions, connect, events, kea, listeners, path, reducers, selectors } from 'kea'
import { FEATURE_FLAGS } from 'lib/constants'
import { featureFlagLogic } from 'lib/logic/featureFlagLogic'
import { sceneLogic } from 'scenes/sceneLogic'
Expand All @@ -15,6 +15,9 @@ export const themeLogic = kea<themeLogicType>([
actions({
syncDarkModePreference: (darkModePreference: boolean) => ({ darkModePreference }),
setTheme: (theme: string | null) => ({ theme }),
saveCustomCss: true,
setPersistedCustomCss: (css: string | null) => ({ css }),
setPreviewingCustomCss: (css: string | null) => ({ css }),
}),
reducers({
darkModeSystemPreference: [
Expand All @@ -30,6 +33,20 @@ export const themeLogic = kea<themeLogicType>([
setTheme: (_, { theme }) => theme,
},
],
persistedCustomCss: [
null as string | null,
{ persist: true },
{
setPersistedCustomCss: (_, { css }) => css,
},
],
previewingCustomCss: [
null as string | null,
{ persist: true },
{
setPreviewingCustomCss: (_, { css }) => css,
},
],
}),
selectors({
theme: [
Expand All @@ -43,6 +60,14 @@ export const themeLogic = kea<themeLogicType>([
)
},
],
customCssEnabled: [
(s) => [s.featureFlags],
(featureFlags): boolean => !!featureFlags[FEATURE_FLAGS.CUSTOM_CSS_THEMES],
],
customCss: [
(s) => [s.persistedCustomCss, s.previewingCustomCss],
(persistedCustomCss, previewingCustomCss): string | null => previewingCustomCss || persistedCustomCss,
],
isDarkModeOn: [
(s) => [s.themeMode, s.darkModeSystemPreference, sceneLogic.selectors.sceneConfig, s.theme],
(themeMode, darkModeSystemPreference, sceneConfig, theme) => {
Expand Down Expand Up @@ -70,6 +95,12 @@ export const themeLogic = kea<themeLogicType>([
},
],
}),
listeners(({ values, actions }) => ({
saveCustomCss() {
actions.setPersistedCustomCss(values.previewingCustomCss)
actions.setPreviewingCustomCss(null)
},
})),
events(({ cache, actions }) => ({
afterMount() {
cache.prefersColorSchemeMedia = window.matchMedia('(prefers-color-scheme: dark)')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
IconLive,
IconNight,
IconNotebook,
IconPalette,
IconPeople,
IconPeopleFilled,
IconPieChart,
Expand Down Expand Up @@ -858,6 +859,11 @@ export const commandPaletteLogic = kea<commandPaletteLogicType>([
actions.updateUser({ theme_mode: 'system' })
},
},
{
icon: IconPalette,
display: 'Add custom CSS',
executor: () => push(urls.customCss()),
},
],
}),
},
Expand Down
1 change: 1 addition & 0 deletions frontend/src/lib/constants.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,7 @@ export const FEATURE_FLAGS = {
CUSTOM_CHANNEL_TYPE_RULES: 'custom-channel-type-rules', // owner: @robbie-c #team-web-analytics
SELF_SERVE_CREDIT_OVERRIDE: 'self-serve-credit-override', // owner: @zach
EXPERIMENTS_MIGRATION_DISABLE_UI: 'experiments-migration-disable-ui', // owner: @jurajmajerik #team-experiments
CUSTOM_CSS_THEMES: 'custom-css-themes', // owner: @daibhin
} as const
export type FeatureFlagKey = (typeof FEATURE_FLAGS)[keyof typeof FEATURE_FLAGS]

Expand Down
21 changes: 19 additions & 2 deletions frontend/src/lib/hooks/useThemedHtml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,33 @@ import { sceneLogic } from 'scenes/sceneLogic'
import { themeLogic } from '~/layout/navigation-3000/themeLogic'

export function useThemedHtml(overflowHidden = true): void {
const { isDarkModeOn } = useValues(themeLogic)
const { isDarkModeOn, customCss } = useValues(themeLogic)
const { sceneConfig } = useValues(sceneLogic)

const CUSTOM_THEME_STYLES_ID = 'ph-custom-theme-styles'

useEffect(() => {
const oldStyle = document.getElementById(CUSTOM_THEME_STYLES_ID)
if (oldStyle) {
document.head.removeChild(oldStyle)
}

document.body.setAttribute('theme', isDarkModeOn ? 'dark' : 'light')

if (customCss) {
const newStyle = document.createElement('style')
newStyle.id = CUSTOM_THEME_STYLES_ID
newStyle.appendChild(document.createTextNode(customCss))
document.head.appendChild(newStyle)
}
}, [isDarkModeOn, customCss])

useEffect(() => {
// overflow-hidden since each area handles scrolling individually (e.g. navbar, scene, side panel)
if (overflowHidden) {
document.body.classList.add('overflow-hidden')
}
}, [isDarkModeOn])
}, [overflowHidden])

useEffect(() => {
// Add a theme-color meta tag to the head to change the address bar color on browsers that support it
Expand Down
1 change: 1 addition & 0 deletions frontend/src/scenes/appScenes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export const appScenes: Record<Scene, () => any> = {
[Scene.ErrorTrackingGroup]: () => import('./error-tracking/ErrorTrackingGroupScene'),
[Scene.Surveys]: () => import('./surveys/Surveys'),
[Scene.Survey]: () => import('./surveys/Survey'),
[Scene.CustomCss]: () => import('./themes/CustomCssScene'),
[Scene.SurveyTemplates]: () => import('./surveys/SurveyTemplates'),
[Scene.DataModel]: () => import('./data-model/DataModelScene'),
[Scene.DataWarehouse]: () => import('./data-warehouse/external/DataWarehouseExternalScene'),
Expand Down
1 change: 1 addition & 0 deletions frontend/src/scenes/sceneTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export enum Scene {
ReplaySingle = 'ReplaySingle',
ReplayPlaylist = 'ReplayPlaylist',
ReplayFilePlayback = 'ReplayFilePlayback',
CustomCss = 'CustomCss',
PersonsManagement = 'PersonsManagement',
Person = 'Person',
PipelineNodeNew = 'PipelineNodeNew',
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/scenes/scenes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,10 @@ export const sceneConfigurations: Record<Scene, SceneConfig> = {
activityScope: ActivityScope.REPLAY,
defaultDocsPath: '/docs/session-replay',
},
[Scene.CustomCss]: {
projectBased: true,
name: 'Custom CSS',
},
[Scene.ReplayPlaylist]: {
projectBased: true,
name: 'Replay playlist',
Expand Down Expand Up @@ -539,6 +543,7 @@ export const routes: Record<string, Scene> = {
[urls.pipeline(':tab')]: Scene.Pipeline,
[urls.pipelineNode(':stage', ':id', ':nodeTab')]: Scene.PipelineNode,
[urls.pipelineNode(':stage', ':id')]: Scene.PipelineNode,
[urls.customCss()]: Scene.CustomCss,
[urls.groups(':groupTypeIndex')]: Scene.PersonsManagement,
[urls.group(':groupTypeIndex', ':groupKey', false)]: Scene.Group,
[urls.group(':groupTypeIndex', ':groupKey', false, ':groupTab')]: Scene.Group,
Expand Down
37 changes: 30 additions & 7 deletions frontend/src/scenes/settings/user/ThemeSwitcher.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,39 @@
import { IconDay, IconLaptop, IconNight } from '@posthog/icons'
import { LemonSelect, LemonSelectProps } from '@posthog/lemon-ui'
import { IconDay, IconLaptop, IconNight, IconPalette } from '@posthog/icons'
import { LemonSelect, LemonSelectOptions, LemonSelectProps } from '@posthog/lemon-ui'
import { useActions, useValues } from 'kea'
import { router } from 'kea-router'
import { urls } from 'scenes/urls'
import { userLogic } from 'scenes/userLogic'

import { themeLogic } from '~/layout/navigation-3000/themeLogic'

export function ThemeSwitcher({
onlyLabel,
...props
}: Partial<LemonSelectProps<any>> & { onlyLabel?: boolean }): JSX.Element {
const { themeMode } = useValues(userLogic)
const { updateUser } = useActions(userLogic)
const { customCssEnabled } = useValues(themeLogic)

return (
<LemonSelect
options={[
const themeOptions: LemonSelectOptions<string> = [
{
options: [
{ icon: <IconDay />, value: 'light', label: 'Light mode' },
{ icon: <IconNight />, value: 'dark', label: 'Dark mode' },
{ icon: <IconLaptop />, value: 'system', label: `Sync with system` },
]}
],
},
]

if (customCssEnabled) {
themeOptions.push({
options: [{ icon: <IconPalette />, value: 'custom', label: 'Edit custom CSS' }],
})
}

return (
<LemonSelect
options={themeOptions}
value={themeMode}
renderButtonContent={(leaf) => {
const labelText = leaf ? leaf.label : 'Sync with system'
Expand All @@ -31,7 +48,13 @@ export function ThemeSwitcher({
</>
)
}}
onChange={(value) => updateUser({ theme_mode: value })}
onChange={(value) => {
if (value === 'custom') {
router.actions.push(urls.customCss())
} else {
updateUser({ theme_mode: value })
}
}}
dropdownPlacement="right-start"
dropdownMatchSelectWidth={false}
{...props}
Expand Down
132 changes: 132 additions & 0 deletions frontend/src/scenes/themes/CustomCssScene.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { LemonButton, Link } from '@posthog/lemon-ui'
import { useActions, useValues } from 'kea'
import { router } from 'kea-router'
import { PageHeader } from 'lib/components/PageHeader'
import { CodeEditor } from 'lib/monaco/CodeEditor'
import { useEffect } from 'react'
import { SceneExport } from 'scenes/sceneTypes'

import { themeLogic } from '~/layout/navigation-3000/themeLogic'

import { urls } from '../urls'

export const scene: SceneExport = {
component: CustomCssScene,
}

const TRON_THEME = `:root {
--radius: 0px;
}

body[theme=dark] {
--border: rgba(0, 255, 1, 0.5);
--link: #00FF01;
--border-bold: #00FF01;
--bg-3000: #111;
--glass-bg-3000: #111;
--bg-light: #222;
--bg-table: #222;
--muted-3000: #0EA70E;
--primary-3000: #00FF01;
--primary-3000-hover: #00FF01;
--primary-alt-highlight: rgba(0, 255, 1, 0.1);
--text-3000: #00FF01;
--accent-3000: #222;
--glass-border-3000: rgba(0,0,0,.3);
--font-title: ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, 'DejaVu Sans Mono', monospace;

--primary-3000-frame-bg-light: #00FF01;
--primary-3000-button-bg: #00FF01;
--primary-3000-button-border: #00FF01;
--text-secondary-3000: #00FF01;
}

.TopBar3000__content {
border-bottom: solid 1px #00FF01;
}`

const BARBIE_THEME = `:root {
--radius: 16px;
}

body[theme=light] {
--border: rgba(255, 105, 180, 0.5);
--border-3000: #ff409f;
--link: #E306AD;
--border-bold: rgba(255, 105, 180, 0.8);
--bg-3000: #FED9E9;
--glass-bg-3000: rgba(255, 192, 203, 0.8);
--bg-light: #FFF0F5;
--bg-table: #F8BBD0;
--muted-3000: #E306AD;
--primary-3000: #FF69B4;
--primary-3000-hover: #FF1493;
--primary-alt-highlight: rgba(255, 105, 180, 0.1);
--text-3000: #ed3993;
--text-3000-light: #58003f;
--accent-3000: #FEBDE2;
--glass-border-3000: rgba(245, 145, 199, 0.3);

--primary-3000-frame-bg-light: #F18DBC;
--primary-3000-button-bg: #FF69B4;
--primary-3000-button-border: #FF1493;
--primary-3000-button-border-hover: #db097b;
--text-secondary-3000: #FFB6C1;

--secondary-3000-button-border: #FF1493;
--secondary-3000-frame-bg-light: #F7B9D7;
--secondary-3000-button-border-hover: #d40b76;
}`

export function CustomCssScene(): JSX.Element {
const { persistedCustomCss, previewingCustomCss } = useValues(themeLogic)
const { saveCustomCss, setPreviewingCustomCss } = useActions(themeLogic)

useEffect(() => {
setPreviewingCustomCss(previewingCustomCss || persistedCustomCss || '')
}, [])

const onPreview = (): void => {
router.actions.push(urls.projectHomepage())
}

return (
<div className="flex flex-col space-y-2">
<PageHeader
buttons={
<>
<LemonButton type="secondary" onClick={onPreview}>
Preview
</LemonButton>
<LemonButton
type="primary"
onClick={() => {
saveCustomCss()
router.actions.push(urls.projectHomepage())
}}
>
Save and set
</LemonButton>
</>
}
/>
<p>
You can add custom CSS to change the style of your PostHog instance. If you need some inspiration try
our templates: <Link onClick={() => setPreviewingCustomCss(TRON_THEME)}>Tron</Link>,{' '}
<Link onClick={() => setPreviewingCustomCss(BARBIE_THEME)}>Barbie</Link>
</p>
<CodeEditor
className="border"
language="css"
value={previewingCustomCss || ''}
onChange={(v) => setPreviewingCustomCss(v ?? null)}
height={600}
options={{
minimap: {
enabled: false,
},
}}
/>
</div>
)
}
Loading
Loading