svelte-tweakpane-ui
Version:
A Svelte component library wrapping UI elements from Tweakpane, plus some additional functionality for convenience and flexibility.
321 lines (320 loc) • 14.4 kB
JavaScript
import { getWindowDocument, isRgbaColorObject, isRgbColorObject } from '@tweakpane/core'
// Standard Tweakpane themes from https://tweakpane.github.io/docs/theming/#builder Must be kept in
// sync with TP...
const standard = {
baseBackgroundColor: 'hsl(230, 7%, 17%)',
baseBorderRadius: '6px',
baseFontFamily: 'Roboto Mono, Source Code Pro, Menlo, Courier, monospace',
baseShadowColor: 'rgba(0, 0, 0, 0.2)',
bladeBorderRadius: '2px',
bladeHorizontalPadding: '4px',
bladeValueWidth: '160px',
buttonBackgroundColor: 'hsl(230, 7%, 70%)',
buttonBackgroundColorActive: '#d6d7db',
buttonBackgroundColorFocus: '#c8cad0',
buttonBackgroundColorHover: '#bbbcc4',
buttonForegroundColor: 'hsl(230, 7%, 17%)',
containerBackgroundColor: 'rgba(187, 188, 196, 0.1)',
containerBackgroundColorActive: 'rgba(187, 188, 196, 0.25)',
containerBackgroundColorFocus: 'rgba(187, 188, 196, 0.2)',
containerBackgroundColorHover: 'rgba(187, 188, 196, 0.15)',
containerForegroundColor: 'hsl(230, 7%, 75%)',
containerHorizontalPadding: '4px',
containerUnitSize: '20px',
containerUnitSpacing: '4px',
containerVerticalPadding: '4px',
grooveForegroundColor: 'rgba(187, 188, 196, 0.1)',
inputBackgroundColor: 'rgba(187, 188, 196, 0.1)',
inputBackgroundColorActive: 'rgba(187, 188, 196, 0.25)',
inputBackgroundColorFocus: 'rgba(187, 188, 196, 0.2)',
inputBackgroundColorHover: 'rgba(187, 188, 196, 0.15)',
inputForegroundColor: 'hsl(230, 7%, 75%)',
labelForegroundColor: 'rgba(187, 188, 196, 0.7)',
monitorBackgroundColor: 'rgba(0, 0, 0, 0.2)',
monitorForegroundColor: 'rgba(187, 188, 196, 0.7)',
pluginImageDraggingColor: 'hsla(230, 100%, 66%, 1)',
// PluginThumbnailListHeight: '400px', pluginThumbnailListThumbSize: '20px',
// pluginThumbnailListWidth: '200px'
}
// eslint-disable-next-line unicorn/no-array-reduce
export const keys = Object.keys(standard).reduce((acc, key) => {
acc[key] = key
return acc
}, {})
const light = {
baseBackgroundColor: 'hsla(230, 5%, 90%, 1.00)',
baseShadowColor: 'hsla(0, 0%, 0%, 0.10)',
buttonBackgroundColor: 'hsla(230, 7%, 75%, 1.00)',
buttonBackgroundColorActive: 'hsla(230, 7%, 60%, 1.00)',
buttonBackgroundColorFocus: 'hsla(230, 7%, 65%, 1.00)',
buttonBackgroundColorHover: 'hsla(230, 7%, 70%, 1.00)',
buttonForegroundColor: 'hsla(230, 10%, 30%, 1.00)',
containerBackgroundColor: 'hsla(230, 15%, 30%, 0.20)',
containerBackgroundColorActive: 'hsla(230, 15%, 30%, 0.32)',
containerBackgroundColorFocus: 'hsla(230, 15%, 30%, 0.28)',
containerBackgroundColorHover: 'hsla(230, 15%, 30%, 0.24)',
containerForegroundColor: 'hsla(230, 10%, 30%, 1.00)',
grooveForegroundColor: 'hsla(230, 15%, 30%, 0.10)',
inputBackgroundColor: 'hsla(230, 15%, 30%, 0.10)',
inputBackgroundColorActive: 'hsla(230, 15%, 30%, 0.22)',
inputBackgroundColorFocus: 'hsla(230, 15%, 30%, 0.18)',
inputBackgroundColorHover: 'hsla(230, 15%, 30%, 0.14)',
inputForegroundColor: 'hsla(230, 10%, 30%, 1.00)',
labelForegroundColor: 'hsla(230, 10%, 30%, 0.70)',
monitorBackgroundColor: 'hsla(230, 15%, 30%, 0.10)',
monitorForegroundColor: 'hsla(230, 10%, 30%, 0.50)',
}
const iceberg = {
baseBackgroundColor: 'hsla(230, 20%, 11%, 1.00)',
baseShadowColor: 'hsla(0, 0%, 0%, 0.20)',
buttonBackgroundColor: 'hsla(230, 10%, 80%, 1.00)',
buttonBackgroundColorActive: 'hsla(230, 10%, 95%, 1.00)',
buttonBackgroundColorFocus: 'hsla(230, 10%, 90%, 1.00)',
buttonBackgroundColorHover: 'hsla(230, 10%, 85%, 1.00)',
buttonForegroundColor: 'hsla(230, 20%, 11%, 1.00)',
containerBackgroundColor: 'hsla(230, 25%, 16%, 1.00)',
containerBackgroundColorActive: 'hsla(230, 25%, 31%, 1.00)',
containerBackgroundColorFocus: 'hsla(230, 25%, 26%, 1.00)',
containerBackgroundColorHover: 'hsla(230, 25%, 21%, 1.00)',
containerForegroundColor: 'hsla(230, 10%, 80%, 1.00)',
grooveForegroundColor: 'hsla(230, 20%, 8%, 1.00)',
inputBackgroundColor: 'hsla(230, 20%, 8%, 1.00)',
inputBackgroundColorActive: 'hsla(230, 28%, 23%, 1.00)',
inputBackgroundColorFocus: 'hsla(230, 28%, 18%, 1.00)',
inputBackgroundColorHover: 'hsla(230, 20%, 13%, 1.00)',
inputForegroundColor: 'hsla(230, 10%, 80%, 1.00)',
labelForegroundColor: 'hsla(230, 12%, 48%, 1.00)',
monitorBackgroundColor: 'hsla(230, 20%, 8%, 1.00)',
monitorForegroundColor: 'hsla(230, 12%, 48%, 1.00)',
}
const jetblack = {
baseBackgroundColor: 'hsla(0, 0%, 0%, 1.00)',
baseShadowColor: 'hsla(0, 0%, 0%, 0.2)',
buttonBackgroundColor: 'hsla(0, 0%, 70%, 1.00)',
buttonBackgroundColorActive: 'hsla(0, 0%, 85%, 1.00)',
buttonBackgroundColorFocus: 'hsla(0, 0%, 80%, 1.00)',
buttonBackgroundColorHover: 'hsla(0, 0%, 75%, 1.00)',
buttonForegroundColor: 'hsla(0, 0%, 0%, 1.00)',
containerBackgroundColor: 'hsla(0, 0%, 10%, 1.00)',
containerBackgroundColorActive: 'hsla(0, 0%, 25%, 1.00)',
containerBackgroundColorFocus: 'hsla(0, 0%, 20%, 1.00)',
containerBackgroundColorHover: 'hsla(0, 0%, 15%, 1.00)',
containerForegroundColor: 'hsla(0, 0%, 50%, 1.00)',
grooveForegroundColor: 'hsla(0, 0%, 10%, 1.00)',
inputBackgroundColor: 'hsla(0, 0%, 10%, 1.00)',
inputBackgroundColorActive: 'hsla(0, 0%, 25%, 1.00)',
inputBackgroundColorFocus: 'hsla(0, 0%, 20%, 1.00)',
inputBackgroundColorHover: 'hsla(0, 0%, 15%, 1.00)',
inputForegroundColor: 'hsla(0, 0%, 70%, 1.00)',
labelForegroundColor: 'hsla(0, 0%, 50%, 1.00)',
monitorBackgroundColor: 'hsla(0, 0%, 8%, 1.00)',
monitorForegroundColor: 'hsla(0, 0%, 48%, 1.00)',
}
const retro = {
baseBackgroundColor: 'hsla(40, 3%, 90%, 1.00)',
baseShadowColor: 'hsla(0, 0%, 0%, 0.30)',
buttonBackgroundColor: 'hsla(40, 3%, 70%, 1.00)',
buttonBackgroundColorActive: 'hsla(40, 3%, 55%, 1.00)',
buttonBackgroundColorFocus: 'hsla(40, 3%, 60%, 1.00)',
buttonBackgroundColorHover: 'hsla(40, 3%, 65%, 1.00)',
buttonForegroundColor: 'hsla(40, 3%, 20%, 1.00)',
containerBackgroundColor: 'hsla(40, 3%, 70%, 1.00)',
containerBackgroundColorActive: 'hsla(40, 3%, 55%, 1.00)',
containerBackgroundColorFocus: 'hsla(40, 3%, 60%, 1.00)',
containerBackgroundColorHover: 'hsla(40, 3%, 65%, 1.00)',
containerForegroundColor: 'hsla(40, 3%, 20%, 1.00)',
grooveForegroundColor: 'hsla(40, 3%, 40%, 1.00)',
inputBackgroundColor: 'hsla(120, 3%, 20%, 1.00)',
inputBackgroundColorActive: 'hsla(120, 3%, 35%, 1.00)',
inputBackgroundColorFocus: 'hsla(120, 3%, 30%, 1.00)',
inputBackgroundColorHover: 'hsla(120, 3%, 25%, 1.00)',
inputForegroundColor: 'hsla(120, 40%, 60%, 1.00)',
labelForegroundColor: 'hsla(40, 3%, 50%, 1.00)',
monitorBackgroundColor: 'hsla(120, 3%, 20%, 1.00)',
monitorForegroundColor: 'hsla(120, 40%, 60%, 0.80)',
}
const translucent = {
baseBackgroundColor: 'hsla(0, 0%, 10%, 0.80)',
baseShadowColor: 'hsla(0, 0%, 0%, 0.20)',
buttonBackgroundColor: 'hsla(0, 0%, 80%, 1.00)',
buttonBackgroundColorActive: 'hsla(0, 0%, 100%, 1.00)',
buttonBackgroundColorFocus: 'hsla(0, 0%, 95%, 1.00)',
buttonBackgroundColorHover: 'hsla(0, 0%, 85%, 1.00)',
buttonForegroundColor: 'hsla(0, 0%, 0%, 0.80)',
containerBackgroundColor: 'hsla(0, 0%, 0%, 0.30)',
containerBackgroundColorActive: 'hsla(0, 0%, 0%, 0.60)',
containerBackgroundColorFocus: 'hsla(0, 0%, 0%, 0.50)',
containerBackgroundColorHover: 'hsla(0, 0%, 0%, 0.40)',
containerForegroundColor: 'hsla(0, 0%, 100%, 0.50)',
grooveForegroundColor: 'hsla(0, 0%, 0%, 0.20)',
inputBackgroundColor: 'hsla(0, 0%, 0%, 0.30)',
inputBackgroundColorActive: 'hsla(0, 0%, 0%, 0.60)',
inputBackgroundColorFocus: 'hsla(0, 0%, 0%, 0.50)',
inputBackgroundColorHover: 'hsla(0, 0%, 0%, 0.40)',
inputForegroundColor: 'hsla(0, 0%, 100%, 0.50)',
labelForegroundColor: 'hsla(0, 0%, 100%, 0.50)',
monitorBackgroundColor: 'hsla(0, 0%, 0%, 0.30)',
monitorForegroundColor: 'hsla(0, 0%, 100%, 0.30)',
}
const vivid = {
baseBackgroundColor: 'hsla(0, 80%, 40%, 1)',
baseShadowColor: 'hsla(0, 0%, 0%, 0.2)',
buttonBackgroundColor: 'hsla(0, 0%, 100%, 1)',
buttonBackgroundColorActive: 'hsla(0, 0%, 85%, 1)',
buttonBackgroundColorFocus: 'hsla(0, 0%, 90%, 1)',
buttonBackgroundColorHover: 'hsla(0, 0%, 95%, 1)',
buttonForegroundColor: 'hsla(230, 20%, 11%, 1)',
containerBackgroundColor: 'hsla(0, 0%, 0%, 0.2)',
containerBackgroundColorActive: 'hsla(0, 0%, 0%, 0.35)',
containerBackgroundColorFocus: 'hsla(0, 0%, 0%, 0.3)',
containerBackgroundColorHover: 'hsla(0, 0%, 0%, 0.25)',
containerForegroundColor: 'hsla(0, 0%, 100%, 0.9)',
grooveForegroundColor: 'hsla(0, 0%, 0%, 0.5)',
inputBackgroundColor: 'hsla(0, 0%, 0%, 0.5)',
inputBackgroundColorActive: 'hsla(0, 0%, 0%, 0.65)',
inputBackgroundColorFocus: 'hsla(0, 0%, 0%, 0.60)',
inputBackgroundColorHover: 'hsla(0, 0%, 0%, 0.55)',
inputForegroundColor: 'hsla(0, 0%, 100%, 0.9)',
labelForegroundColor: 'hsla(0, 0%, 100%, 0.9)',
monitorBackgroundColor: 'hsla(0, 0%, 0%, 0.5)',
monitorForegroundColor: 'hsla(0, 0%, 100%, 0.5)',
}
export const presets = {
/** Dark blue theme. */
iceberg,
/** Extra dark theme. */
jetblack,
/** Standard Tweakpane light theme. */
light,
/** Light theme with CRT vibes. */
retro,
/** Standard Tweakpane dark theme. This is the default theme. */
standard,
/** Dark translucent theme. */
translucent,
/** Red theme. */
vivid,
}
// More humane theme names... Note that the shorthand variables don't work
const keyToCssVariableMap = new Map([
// Tweakpane
['baseBackgroundColor', '--tp-base-background-color'],
['baseBorderRadius', '--tp-base-border-radius'],
['baseFontFamily', '--tp-base-font-family'],
['baseShadowColor', '--tp-base-shadow-color'],
['bladeBorderRadius', '--tp-blade-border-radius'],
['bladeHorizontalPadding', '--tp-blade-horizontal-padding'],
['bladeValueWidth', '--tp-blade-value-width'],
['buttonBackgroundColor', '--tp-button-background-color'],
['buttonBackgroundColorActive', '--tp-button-background-color-active'],
['buttonBackgroundColorFocus', '--tp-button-background-color-focus'],
['buttonBackgroundColorHover', '--tp-button-background-color-hover'],
['buttonForegroundColor', '--tp-button-foreground-color'],
['containerBackgroundColor', '--tp-container-background-color'],
['containerBackgroundColorActive', '--tp-container-background-color-active'],
['containerBackgroundColorFocus', '--tp-container-background-color-focus'],
['containerBackgroundColorHover', '--tp-container-background-color-hover'],
['containerForegroundColor', '--tp-container-foreground-color'],
['containerHorizontalPadding', '--tp-container-horizontal-padding'],
['containerUnitSize', '--tp-container-unit-size'],
['containerUnitSpacing', '--tp-container-unit-spacing'],
['containerVerticalPadding', '--tp-container-vertical-padding'],
['grooveForegroundColor', '--tp-groove-foreground-color'],
['inputBackgroundColor', '--tp-input-background-color'],
['inputBackgroundColorActive', '--tp-input-background-color-active'],
['inputBackgroundColorFocus', '--tp-input-background-color-focus'],
['inputBackgroundColorHover', '--tp-input-background-color-hover'],
['inputForegroundColor', '--tp-input-foreground-color'],
['labelForegroundColor', '--tp-label-foreground-color'],
['monitorBackgroundColor', '--tp-monitor-background-color'],
['monitorForegroundColor', '--tp-monitor-foreground-color'],
// Plugins
['pluginImageDraggingColor', '--tp-plugin-image-dragging-color'],
// ['pluginThumbnailListHeight', '--tp-plugin-thumbnail-list-height'],
// ['pluginThumbnailListThumbSize', '--tp-plugin-thumbnail-list-thumb-size'],
// ['pluginThumbnailListWidth', '--tp-plugin-thumbnail-list-width']
])
// Just do it dynamically instead of the map? function transformToCustomProperty(str: string):
// string { return '--tp-' + str.replace(/([a-zA-Z])(?=[A-Z])/g, '$1-').toLowerCase();
// }
// eslint-disable-next-line ts/no-redundant-type-constituents
function stringToCssValue(v) {
if (v === undefined) {
return undefined
}
if (typeof v === 'string') {
return v
}
if (isRgbaColorObject(v)) {
return `rgba(${v.r}, ${v.g}, ${v.b}, ${v.a})`
}
if (isRgbColorObject(v)) {
return `rgb(${v.r}, ${v.g}, ${v.b})`
}
}
function expandVariableKey(name) {
// Pass explicit variables through
if (name.startsWith('--tp-')) {
return name
}
const variableName = keyToCssVariableMap.get(name)
if (variableName) {
return variableName
}
throw new Error(`Unknown Tweakpane CSS theme map variable key: "${name}"`)
}
/**
* Used during SSR to calculate metrics Returns CSS string.
*/
export function getValueOrFallback(theme, key) {
return theme?.[key] === undefined ? stringToCssValue(standard[key]) : stringToCssValue(theme[key])
}
export function applyTheme(element, theme) {
const rootDocument = getWindowDocument().documentElement
if (theme === undefined) {
for (const k of Object.keys(standard)) {
const key = expandVariableKey(k)
if (element.style.getPropertyValue(key).length > 0) {
element.style.removeProperty(key)
}
}
} else {
for (const [k, v] of Object.entries(theme)) {
const key = expandVariableKey(k)
const value = stringToCssValue(v)
// Only set the variable if it deviates from the standard theme or the root theme (set
// by setGlobalDefaultTheme).... but if theme is explicitly standard and not undefined,
// then apply it anyway so that any global theme is overridden TODO normalize color
// representation for comparison? TODO tests for this logic
const standardValue = standard[k] || undefined
const rootValue = rootDocument.style.getPropertyValue(key) || undefined
const isDeviationFromRoot = (rootValue && value !== rootValue) ?? false
const isDeviationFromStandard = (standardValue && value !== standardValue) ?? false
if (value !== undefined && (isDeviationFromRoot || (!rootValue && isDeviationFromStandard))) {
element.style.setProperty(key, value)
} else if (element.style.getPropertyValue(key).length > 0) {
element.style.removeProperty(key)
}
}
}
}
/**
* Sets a default theme for all Tweakpane components on the page. Useful if you have a light / dark
* mode toggle.
*/
export function setGlobalDefaultTheme(theme) {
// Wait for dom ready... better outside?
// eslint-disable-next-line unicorn/prefer-global-this, ts/no-unnecessary-condition
if (window?.document) {
applyTheme(getWindowDocument().documentElement, theme)
}
}
// Library exports
export default {
/**
* A collection of default theme color schemes, matching those provided in the Tweakpane
* [Panebuilder presets](https://tweakpane.github.io/docs/theming/#builder).
*/
presets,
setGlobalDefaultTheme,
}