import * as React from 'react' import type { TLTheme } from '~types' const styles = new Map() type AnyTheme = Record function makeCssTheme(prefix: string, theme: T) { return Object.keys(theme).reduce((acc, key) => { const value = theme[key as keyof T] if (value) { return acc + `${`--${prefix}-${key}`}: ${value};\n` } return acc }, '') } function useTheme(prefix: string, theme: T, selector = ':root') { React.useLayoutEffect(() => { const style = document.createElement('style') const cssTheme = makeCssTheme(prefix, theme) style.setAttribute('id', `${prefix}-theme`) style.setAttribute('data-selector', selector) style.innerHTML = ` ${selector} { ${cssTheme} } ` document.head.appendChild(style) return () => { if (style && document.head.contains(style)) { document.head.removeChild(style) } } }, [prefix, theme, selector]) } function useStyle(uid: string, rules: string) { React.useLayoutEffect(() => { if (styles.get(uid)) { return () => void null } const style = document.createElement('style') style.innerHTML = rules style.setAttribute('id', uid) document.head.appendChild(style) styles.set(uid, style) return () => { if (style && document.head.contains(style)) { document.head.removeChild(style) styles.delete(uid) } } }, [uid, rules]) } const css = (strings: TemplateStringsArray, ...args: unknown[]) => strings.reduce( (acc, string, index) => acc + string + (index < args.length ? args[index] : ''), '' ) const defaultTheme: TLTheme = { accent: 'rgb(255, 0, 0)', brushFill: 'rgba(0,0,0,.05)', brushStroke: 'rgba(0,0,0,.25)', selectStroke: 'rgb(66, 133, 244)', selectFill: 'rgba(65, 132, 244, 0.05)', background: 'rgb(248, 249, 250)', foreground: 'rgb(51, 51, 51)', grid: 'rgba(144, 144, 144, 1)', } const tlcss = css` @font-face { font-family: 'Recursive'; font-style: normal; font-weight: 500; font-display: swap; src: url(https://fonts.gstatic.com/s/recursive/v23/8vI-7wMr0mhh-RQChyHEH06TlXhq_gukbYrFMk1QuAIcyEwG_X-dpEfaE5YaERmK-CImKsvxvU-MXGX2fSqasNfUlTGZnI14ZeY.woff2) format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } @font-face { font-family: 'Recursive'; font-style: normal; font-weight: 700; font-display: swap; src: url(https://fonts.gstatic.com/s/recursive/v23/8vI-7wMr0mhh-RQChyHEH06TlXhq_gukbYrFMk1QuAIcyEwG_X-dpEfaE5YaERmK-CImKsvxvU-MXGX2fSqasNfUlTGZnI14ZeY.woff2) format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } @font-face { font-family: 'Recursive Mono'; font-style: normal; font-weight: 420; font-display: swap; src: url(https://fonts.gstatic.com/s/recursive/v23/8vI-7wMr0mhh-RQChyHEH06TlXhq_gukbYrFMk1QuAIcyEwG_X-dpEfaE5YaERmK-CImqvTxvU-MXGX2fSqasNfUlTGZnI14ZeY.woff2) format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } .tl-container { --tl-zoom: 1; --tl-scale: calc(1 / var(--tl-zoom)); --tl-padding: calc(64px * max(1, var(--tl-scale))); position: relative; top: 0px; left: 0px; width: 100%; height: 100%; max-width: 100%; max-height: 100%; box-sizing: border-box; padding: 0px; margin: 0px; z-index: 100; overflow: hidden; touch-action: none; overscroll-behavior: none; background-color: var(--tl-background); } .tl-container * { box-sizing: border-box; } .tl-overlay { position: absolute; width: 100%; height: 100%; touch-action: none; pointer-events: none; } .tl-grid { position: absolute; width: 100%; height: 100%; touch-action: none; pointer-events: none; user-select: none; } .tl-snap-line { stroke: var(--tl-accent); stroke-width: calc(1px * var(--tl-scale)); } .tl-snap-point { stroke: var(--tl-accent); stroke-width: calc(1px * var(--tl-scale)); } .tl-canvas { position: absolute; width: 100%; height: 100%; touch-action: none; pointer-events: all; overflow: clip; } .tl-layer { position: absolute; top: 0px; left: 0px; height: 0px; width: 0px; contain: layout style size; } .tl-absolute { position: absolute; top: 0px; left: 0px; transform-origin: center center; contain: layout style size; } .tl-positioned { position: absolute; top: 0px; left: 0px; transform-origin: center center; pointer-events: none; display: flex; align-items: center; justify-content: center; overflow: hidden; contain: layout style size; } .tl-positioned-svg { width: 100%; height: 100%; overflow: hidden; contain: layout style size; } .tl-positioned-div { position: relative; width: 100%; height: 100%; overflow: hidden; padding: var(--tl-padding); overflow: hidden; contain: layout style size; } .tl-counter-scaled { transform: scale(var(--tl-scale)); } .tl-dashed { stroke-dasharray: calc(2px * var(--tl-scale)), calc(2px * var(--tl-scale)); } .tl-transparent { fill: transparent; stroke: transparent; } .tl-cursor-ns { cursor: ns-resize; } .tl-cursor-ew { cursor: ew-resize; } .tl-cursor-nesw { cursor: nesw-resize; } .tl-cursor-nwse { cursor: nwse-resize; } .tl-corner-handle { stroke: var(--tl-selectStroke); fill: var(--tl-background); stroke-width: calc(1.5px * var(--tl-scale)); } .tl-rotate-handle { stroke: var(--tl-selectStroke); fill: var(--tl-background); stroke-width: calc(1.5px * var(--tl-scale)); cursor: grab; } .tl-binding { fill: var(--tl-selectFill); stroke: var(--tl-selectStroke); stroke-width: calc(1px * var(--tl-scale)); pointer-events: none; } .tl-user { left: -4px; top: -4px; height: 8px; width: 8px; border-radius: 100%; pointer-events: none; } .tl-indicator { fill: transparent; stroke-width: calc(1.5px * var(--tl-scale)); pointer-events: none; } .tl-user-indicator-bounds { border-style: solid; border-width: calc(1px * var(--tl-scale)); } .tl-selected { stroke: var(--tl-selectStroke); } .tl-hovered { stroke: var(--tl-selectStroke); } .tl-clone-target { pointer-events: all; } .tl-clone-target:hover .tl-clone-button { opacity: 1; } .tl-clone-button-target { cursor: pointer; pointer-events: all; } .tl-clone-button-target:hover .tl-clone-button { fill: var(--tl-selectStroke); } .tl-clone-button { opacity: 0; r: calc(8px * var(--tl-scale)); stroke-width: calc(1.5px * var(--tl-scale)); stroke: var(--tl-selectStroke); fill: var(--tl-background); } .tl-bounds { pointer-events: none; contain: layout style size; } .tl-bounds-bg { stroke: none; fill: var(--tl-selectFill); pointer-events: all; contain: layout style size; } .tl-bounds-center { fill: transparent; stroke: var(--tl-selectStroke); stroke-width: calc(1.5px * var(--tl-scale)); } .tl-brush { fill: var(--tl-brushFill); stroke: var(--tl-brushStroke); stroke-width: calc(1px * var(--tl-scale)); pointer-events: none; contain: layout style size; } .tl-dot { fill: var(--tl-background); stroke: var(--tl-foreground); stroke-width: 2px; } .tl-handle { pointer-events: all; } .tl-handle:hover .tl-handle-bg { fill: var(--tl-selectFill); } .tl-handle:hover .tl-handle-bg > * { stroke: var(--tl-selectFill); } .tl-handle:active .tl-handle-bg { fill: var(--tl-selectFill); } .tl-handle:active .tl-handle-bg > * { stroke: var(--tl-selectFill); } .tl-handle { fill: var(--tl-background); stroke: var(--tl-selectStroke); stroke-width: 1.5px; } .tl-handle-bg { fill: transparent; stroke: none; pointer-events: all; r: calc(16px / max(1, var(--tl-zoom))); } .tl-binding-indicator { stroke-width: calc(3px * var(--tl-scale)); fill: var(--tl-selectFill); stroke: var(--tl-selected); } .tl-centered-g { transform: translate(var(--tl-padding), var(--tl-padding)); } .tl-current-parent > *[data-shy='true'] { opacity: 1; } .tl-binding { fill: none; stroke: var(--tl-selectStroke); stroke-width: calc(2px * var(--tl-scale)); } .tl-grid-dot { fill: var(--tl-grid); } ` export function useTLTheme(theme?: Partial, selector?: string) { const tltheme = React.useMemo( () => ({ ...defaultTheme, ...theme, }), [theme] ) useTheme('tl', tltheme, selector) useStyle('tl-canvas', tlcss) }