Files
supabase/apps/studio/components/interfaces/Account/Preferences/ThemeSettings.tsx
T
Danny White 512201dcd0 chore(ui): remove the Classic Dark theme (#50387)
## What kind of change does this PR introduce?

Chore.

## What is the current behaviour?

Classic Dark remains available across the shared theme library and
several apps. Studio now supports System, Dark, and Light as its theme
modes, but still carries compatibility paths for Classic Dark.

## What is the new behaviour?

- Removes Classic Dark from shared theme options, application commands,
stylesheets, previews, examples, and replay handling.
- Deletes the Classic Dark and faux Classic Dark stylesheets.
- Removes the now-unused Classic Dark branches from Studio theme colour
controls.
- Migrates `classic-dark` to `dark` so first rendered frame renders Dark
(not Light)

| After |
| --- |
| <img width="1458" height="1778" alt="CleanShot 2026-09-18 at 11 07
40@2x"
src="https://github.com/user-attachments/assets/679bf87f-a3c1-4599-ad2f-292d98d0b856"
/> |

## To test

1. In Studio, open Account Preferences → Appearance. Confirm the
available themes are System, Dark, and Light, and that theme colour
controls still work in each resolved mode.
2. Set the `theme` local storage value to `classic-dark`, then reload
Studio. Confirm it renders as Dark immediately and the stored value
becomes `dark`.
3. Open the theme switcher in Design System, Learn, and UI Library.
Confirm Classic Dark is no longer available and Light, Dark, and System
still apply correctly.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Changes**
* Removed the Classic Dark theme option from theme menus and settings
across the application.
* Classic Dark selections are automatically migrated to the standard
Dark theme.
* Updated theme documentation and demonstrations to list only System,
Light, and Dark.
* Removed Classic Dark styling and preview support; existing Dark,
Light, and System themes remain available.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-21 10:53:14 +10:00

144 lines
4.6 KiB
TypeScript

import { LOCAL_STORAGE_KEYS } from 'common'
import { useTheme } from 'next-themes'
import { memo, useEffect, useState } from 'react'
import SVG from 'react-inlinesvg'
import {
Card,
CardContent,
Label,
RadioGroup,
RadioGroupLargeItem,
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
singleThemes,
} from 'ui'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import {
PageSection,
PageSectionContent,
PageSectionDescription,
PageSectionMeta,
PageSectionSummary,
PageSectionTitle,
} from 'ui-patterns/PageSection'
import { ThemeColorSettings } from './ThemeColorSettings'
import { DEFAULT_SIDEBAR_BEHAVIOR } from '@/components/interfaces/Sidebar'
import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage'
import { BASE_PATH } from '@/lib/constants'
/**
* Declared at module scope and memoized deliberately. While this lived inside
* `ThemeSettings` React saw a brand new component type on every parent render
* and remounted the whole radio group, so the four `react-inlinesvg` previews
* restarted their fetch and rendered nothing until it resolved — collapsing
* the cards for a frame. That was invisible while the parent only re-rendered
* on a theme change, but it became a continuous flicker once dragging a
* customize-theme slider started re-rendering the parent every frame.
*/
const SingleThemeSelection = memo(function SingleThemeSelection({
theme,
setTheme,
}: {
theme: string | undefined
setTheme: (theme: string) => void
}) {
return (
<RadioGroup
name="theme"
onValueChange={setTheme}
aria-label="Choose a theme"
defaultValue={theme}
value={theme}
className="grid grid-cols-2 gap-4"
>
{singleThemes.map((themeMode) => (
<RadioGroupLargeItem
className="p-3 w-full"
key={themeMode.value}
value={themeMode.value}
label={themeMode.name}
>
<SVG src={`${BASE_PATH}/img/themes/${themeMode.value}.svg?v=2`} />
</RadioGroupLargeItem>
))}
</RadioGroup>
)
})
export const ThemeSettings = () => {
const [mounted, setMounted] = useState(false)
const { theme, setTheme } = useTheme()
const [sidebarBehaviour, setSidebarBehaviour] = useLocalStorageQuery(
LOCAL_STORAGE_KEYS.SIDEBAR_BEHAVIOR,
DEFAULT_SIDEBAR_BEHAVIOR
)
/**
* Avoid Hydration Mismatch
* https://github.com/pacocoursey/next-themes?tab=readme-ov-file#avoid-hydration-mismatch
*/
// useEffect only runs on the client, so now we can safely show the UI
useEffect(() => setMounted(true), [])
if (!mounted) return null
return (
<PageSection>
<PageSectionMeta>
<PageSectionSummary>
<PageSectionTitle>Appearance</PageSectionTitle>
<PageSectionDescription>
Choose how Supabase looks and behaves in the dashboard.
</PageSectionDescription>
</PageSectionSummary>
</PageSectionMeta>
<PageSectionContent>
<Card>
<CardContent className="grid grid-cols-12 gap-6">
<div className="col-span-full md:col-span-4 flex flex-col gap-2">
<Label htmlFor="theme" className="text-foreground">
Theme mode
</Label>
<p className="text-sm text-foreground-lighter">
Choose how Supabase looks to you. Select a single theme, or sync with your system.
</p>
</div>
<div className="col-span-full md:col-span-8 flex flex-col gap-4">
<SingleThemeSelection theme={theme} setTheme={setTheme} />
</div>
</CardContent>
<ThemeColorSettings />
<CardContent>
<FormItemLayout
isReactForm={false}
label="Sidebar behavior"
layout="flex-row-reverse"
description="Choose your preferred sidebar behavior: open, closed, or expand on hover."
>
<Select
value={sidebarBehaviour}
onValueChange={setSidebarBehaviour}
aria-label="Select an option"
>
<SelectTrigger>
<SelectValue placeholder="Choose an option" />
</SelectTrigger>
<SelectContent>
<SelectItem value="open">Expanded</SelectItem>
<SelectItem value="closed">Collapsed</SelectItem>
<SelectItem value="expandable">Expand on hover</SelectItem>
</SelectContent>
</Select>
</FormItemLayout>
</CardContent>
</Card>
</PageSectionContent>
</PageSection>
)
}