Files
supabase/packages/common/hooks/useThemeSandbox.tsx
Ali Waseem 1c2d28d5b3 chore: wrap local storage into helper methods that are safer (#46628)
## I have read the
[CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md)
file.

YES

## What kind of change does this PR introduce?

- Noticing our code we have many patterns of calling localstorage and
handling those errors
- We should add those in a single well tested file
- Handle those errors in the singleton which makes it easier for us to
debug customer issues. Logger is outputing local storage warnings for
feature we expose
- Side effect of this is random crashes on studio when local storage
isn't available or handled correctly

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

* **Refactor**
* Improved browser storage handling across the app for more reliable
persistence and graceful behavior in restricted or non-browser
environments (settings, previews, charts, tabs, sign-in/session flows,
integrations, and UI state).

* **New Features**
* Introduced a safe storage layer to standardize and harden
local/session persistence.

* **Tests**
  * Added comprehensive tests covering the new safe storage behavior.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-04 07:41:28 -06:00

119 lines
3.8 KiB
TypeScript

'use client'
import { useEffect, useState } from 'react'
import { IS_PROD } from '../constants'
import { safeLocalStorage } from '../safe-storage'
const defaultDark: { [name: string]: string } = {
'--brand-accent': '160deg 100% 50%',
'--brand-default': '159.9deg 100% 38.6%',
'--brand-600': '136deg 59.5% 70%',
'--brand-500': '160.4deg 100% 19.2%',
'--brand-400': '160.4deg 100% 9.6%',
'--brand-300': '158.4deg 100% 4.9%',
'--brand-200': '162deg 100% 2%',
'--border-stronger': '200deg 16.1% 22%',
'--border-strong': '200deg 16.5% 17.8%',
'--border-alternative': '200deg 16.4% 13.1%',
'--border-control': '200deg 10% 14%',
'--border-overlay': '200deg 20% 9%',
'--border-secondary': '200deg 21.6% 10%',
'--border-muted': '200deg 14.9% 9.2%',
'--border-default': '200deg 10% 11%',
'--background-muted': '200deg 9.1% 10.8%',
'--background-overlay-hover': '200deg 20% 11%',
'--background-overlay-default': '200deg 14.3% 6.9%',
'--background-surface-300': '200deg 20% 10%',
'--background-surface-200': '200deg 20% 7%',
'--background-surface-100': '200deg 20% 6%',
'--background-control': '200deg 9% 11%',
'--background-selection': '200deg 9.8% 10%',
'--background-alternative': '200deg 20% 2.9%',
'--background-default': '200deg 20% 4%',
'--foreground-muted': '200deg 10% 35%',
'--foreground-lighter': '200deg 8% 55%',
'--foreground-light': '200deg 5% 69%',
'--foreground-default': '200deg 0% 93%',
}
/**
* Shows a GUI to test color themes in dev and preview env.
*
* To access sandbox mode:
* - switch theme to dark mode
* - append "#theme-sandbox" to the url
* - select "Apply Theme" to apply preset (localStorage will keep track of changes so you don't lose new values)
* - select "Reset localStorage" and refresh page to restart
*/
export const useThemeSandbox = (): any => {
const isWindowUndefined = typeof window === 'undefined'
if (isWindowUndefined || IS_PROD) return null
const hash = window.location.hash
const defaultConfig = defaultDark // use dark default tokens
// const defaultConfig = defaultLight // use light default tokens
const localPreset = safeLocalStorage.getItem('theme-sandbox')
const isSandbox = hash.includes('#theme-sandbox') || localPreset !== null
const [themeConfig, setThemeConfig] = useState(
localPreset ? JSON.parse(localPreset) : defaultConfig
)
const styles = document.querySelector(':root') as any
const handleSetThemeConfig = (name: string, value: any) => {
updateCSSVariables()
setThemeConfig((prevConfig: any) => ({ ...prevConfig, [name]: value }))
}
const updateCSSVariables = () => {
Object.entries(themeConfig).map(([key, value]) => styles.style.setProperty(key, value))
safeLocalStorage.setItem('theme-sandbox', JSON.stringify(themeConfig))
}
const init = async () => {
if (!isSandbox) return
const dat = await import('dat.gui')
const gui = new dat.GUI()
gui.width = 500
Object.entries(defaultConfig).map(([key, _value]) => {
if (!themeConfig[key]) return safeLocalStorage.removeItem('theme-sandbox')
const folderName = key.split('-')[2]
const folder = gui.__folders[folderName] ?? gui.addFolder(folderName)
return folder
.add(themeConfig, key)
.name(key)
.onChange((newValue) => {
handleSetThemeConfig(key, newValue)
})
})
var obj = {
'Apply Theme': function () {
updateCSSVariables()
},
'Exit Sandbox': function () {
gui.destroy()
},
'Reset localStorage': function () {
safeLocalStorage.removeItem('theme-sandbox')
setThemeConfig(defaultConfig)
},
}
gui.add(obj, 'Apply Theme')
gui.add(obj, 'Reset localStorage')
gui.add(obj, 'Exit Sandbox')
gui.load
}
useEffect(() => {
init()
}, [])
return { themeConfig, handleSetThemeConfig, isSandbox }
}
export default useThemeSandbox