You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

useStyle.tsx 7.8KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
  1. import * as React from 'react'
  2. import type { TLTheme } from '+types'
  3. const styles = new Map<string, HTMLStyleElement>()
  4. type AnyTheme = Record<string, string>
  5. function makeCssTheme<T = AnyTheme>(prefix: string, theme: T) {
  6. return Object.keys(theme).reduce((acc, key) => {
  7. const value = theme[key as keyof T]
  8. if (value) {
  9. return acc + `${`--${prefix}-${key}`}: ${value};\n`
  10. }
  11. return acc
  12. }, '')
  13. }
  14. function useTheme<T = AnyTheme>(prefix: string, theme: T, selector = ':root') {
  15. React.useLayoutEffect(() => {
  16. const style = document.createElement('style')
  17. const cssTheme = makeCssTheme(prefix, theme)
  18. style.setAttribute('id', `${prefix}-theme`)
  19. style.setAttribute('data-selector', selector)
  20. style.innerHTML = `
  21. ${selector} {
  22. ${cssTheme}
  23. }
  24. `
  25. document.head.appendChild(style)
  26. return () => {
  27. if (style && document.head.contains(style)) {
  28. document.head.removeChild(style)
  29. }
  30. }
  31. }, [prefix, theme, selector])
  32. }
  33. function useStyle(uid: string, rules: string) {
  34. React.useLayoutEffect(() => {
  35. if (styles.get(uid)) {
  36. return () => void null
  37. }
  38. const style = document.createElement('style')
  39. style.innerHTML = rules
  40. style.setAttribute('id', uid)
  41. document.head.appendChild(style)
  42. styles.set(uid, style)
  43. return () => {
  44. if (style && document.head.contains(style)) {
  45. document.head.removeChild(style)
  46. styles.delete(uid)
  47. }
  48. }
  49. }, [uid, rules])
  50. }
  51. const css = (strings: TemplateStringsArray, ...args: unknown[]) =>
  52. strings.reduce(
  53. (acc, string, index) => acc + string + (index < args.length ? args[index] : ''),
  54. ''
  55. )
  56. const defaultTheme: TLTheme = {
  57. brushFill: 'rgba(0,0,0,.05)',
  58. brushStroke: 'rgba(0,0,0,.25)',
  59. selectStroke: 'rgb(66, 133, 244)',
  60. selectFill: 'rgba(65, 132, 244, 0.05)',
  61. background: 'rgb(248, 249, 250)',
  62. foreground: 'rgb(51, 51, 51)',
  63. }
  64. const tlcss = css`
  65. @font-face {
  66. font-family: 'Recursive';
  67. font-style: normal;
  68. font-weight: 500;
  69. font-display: swap;
  70. src: url(https://fonts.gstatic.com/s/recursive/v23/8vI-7wMr0mhh-RQChyHEH06TlXhq_gukbYrFMk1QuAIcyEwG_X-dpEfaE5YaERmK-CImKsvxvU-MXGX2fSqasNfUlTGZnI14ZeY.woff2)
  71. format('woff2');
  72. unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
  73. U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
  74. }
  75. @font-face {
  76. font-family: 'Recursive';
  77. font-style: normal;
  78. font-weight: 700;
  79. font-display: swap;
  80. src: url(https://fonts.gstatic.com/s/recursive/v23/8vI-7wMr0mhh-RQChyHEH06TlXhq_gukbYrFMk1QuAIcyEwG_X-dpEfaE5YaERmK-CImKsvxvU-MXGX2fSqasNfUlTGZnI14ZeY.woff2)
  81. format('woff2');
  82. unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
  83. U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
  84. }
  85. @font-face {
  86. font-family: 'Recursive Mono';
  87. font-style: normal;
  88. font-weight: 420;
  89. font-display: swap;
  90. src: url(https://fonts.gstatic.com/s/recursive/v23/8vI-7wMr0mhh-RQChyHEH06TlXhq_gukbYrFMk1QuAIcyEwG_X-dpEfaE5YaERmK-CImqvTxvU-MXGX2fSqasNfUlTGZnI14ZeY.woff2)
  91. format('woff2');
  92. unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
  93. U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
  94. }
  95. .tl-container {
  96. --tl-zoom: 1;
  97. --tl-scale: calc(1 / var(--tl-zoom));
  98. --tl-camera-x: 0px;
  99. --tl-camera-y: 0px;
  100. --tl-padding: calc(64px * max(1, var(--tl-scale)));
  101. position: relative;
  102. top: 0px;
  103. left: 0px;
  104. width: 100%;
  105. height: 100%;
  106. max-width: 100%;
  107. max-height: 100%;
  108. box-sizing: border-box;
  109. padding: 0px;
  110. margin: 0px;
  111. z-index: 100;
  112. touch-action: none;
  113. overscroll-behavior: none;
  114. background-color: var(--tl-background);
  115. }
  116. .tl-container * {
  117. user-select: none;
  118. box-sizing: border-box;
  119. }
  120. .tl-canvas {
  121. position: absolute;
  122. overflow: hidden;
  123. width: 100%;
  124. height: 100%;
  125. touch-action: none;
  126. pointer-events: all;
  127. }
  128. .tl-layer {
  129. position: absolute;
  130. top: 0;
  131. left: 0;
  132. height: 0;
  133. width: 0;
  134. contain: layout size;
  135. transform: scale(var(--tl-zoom)) translate3d(var(--tl-camera-x), var(--tl-camera-y), 0px);
  136. }
  137. .tl-absolute {
  138. position: absolute;
  139. top: 0px;
  140. left: 0px;
  141. transform-origin: center center;
  142. }
  143. .tl-positioned {
  144. position: absolute;
  145. top: 0px;
  146. left: 0px;
  147. transform-origin: center center;
  148. pointer-events: none;
  149. display: flex;
  150. align-items: center;
  151. justify-content: center;
  152. overflow: clip;
  153. contain: layout size paint;
  154. }
  155. .tl-positioned-svg {
  156. width: 100%;
  157. height: 100%;
  158. overflow: clip;
  159. }
  160. .tl-positioned-div {
  161. position: relative;
  162. width: 100%;
  163. height: 100%;
  164. overflow: hidden;
  165. padding: var(--tl-padding);
  166. overflow: clip;
  167. }
  168. .tl-counter-scaled {
  169. transform: scale(var(--tl-scale));
  170. }
  171. .tl-dashed {
  172. stroke-dasharray: calc(2px * var(--tl-scale)), calc(2px * var(--tl-scale));
  173. }
  174. .tl-transparent {
  175. fill: transparent;
  176. stroke: transparent;
  177. }
  178. .tl-cursor-ns {
  179. cursor: ns-resize;
  180. }
  181. .tl-cursor-ew {
  182. cursor: ew-resize;
  183. }
  184. .tl-cursor-nesw {
  185. cursor: nesw-resize;
  186. }
  187. .tl-cursor-nwse {
  188. cursor: nwse-resize;
  189. }
  190. .tl-corner-handle {
  191. stroke: var(--tl-selectStroke);
  192. fill: var(--tl-background);
  193. stroke-width: calc(1.5px * var(--tl-scale));
  194. }
  195. .tl-rotate-handle {
  196. stroke: var(--tl-selectStroke);
  197. fill: var(--tl-background);
  198. stroke-width: calc(1.5px * var(--tl-scale));
  199. cursor: grab;
  200. }
  201. .tl-binding {
  202. fill: var(--tl-selectFill);
  203. stroke: var(--tl-selectStroke);
  204. stroke-width: calc(1px * var(--tl-scale));
  205. pointer-events: none;
  206. }
  207. .tl-selected {
  208. fill: transparent;
  209. stroke: var(--tl-selectStroke);
  210. stroke-width: calc(1.5px * var(--tl-scale));
  211. pointer-events: none;
  212. }
  213. .tl-hovered {
  214. fill: transparent;
  215. stroke: var(--tl-selectStroke);
  216. stroke-width: calc(1.5px * var(--tl-scale));
  217. pointer-events: none;
  218. }
  219. .tl-bounds {
  220. pointer-events: none;
  221. }
  222. .tl-bounds-center {
  223. fill: transparent;
  224. stroke: var(--tl-selectStroke);
  225. stroke-width: calc(1.5px * var(--tl-scale));
  226. }
  227. .tl-bounds-bg {
  228. stroke: none;
  229. fill: var(--tl-selectFill);
  230. pointer-events: all;
  231. }
  232. .tl-brush {
  233. fill: var(--tl-brushFill);
  234. stroke: var(--tl-brushStroke);
  235. stroke-width: calc(1px * var(--tl-scale));
  236. pointer-events: none;
  237. }
  238. .tl-dot {
  239. fill: var(--tl-background);
  240. stroke: var(--tl-foreground);
  241. stroke-width: 2px;
  242. }
  243. .tl-handle {
  244. pointer-events: all;
  245. }
  246. .tl-handle:hover .tl-handle-bg {
  247. fill: var(--tl-selectFill);
  248. }
  249. .tl-handle:hover .tl-handle-bg > * {
  250. stroke: var(--tl-selectFill);
  251. }
  252. .tl-handle:active .tl-handle-bg {
  253. fill: var(--tl-selectFill);
  254. }
  255. .tl-handle:active .tl-handle-bg > * {
  256. stroke: var(--tl-selectFill);
  257. }
  258. .tl-handle {
  259. fill: var(--tl-background);
  260. stroke: var(--tl-selectStroke);
  261. stroke-width: 1.5px;
  262. }
  263. .tl-handle-bg {
  264. fill: transparent;
  265. stroke: none;
  266. pointer-events: all;
  267. r: calc(20px / max(1, var(--tl-zoom)));
  268. }
  269. .tl-binding-indicator {
  270. stroke-width: calc(3px * var(--tl-scale));
  271. fill: var(--tl-selectFill);
  272. stroke: var(--tl-selected);
  273. }
  274. .tl-centered-g {
  275. transform: translate(var(--tl-padding), var(--tl-padding));
  276. }
  277. .tl-current-parent > *[data-shy='true'] {
  278. opacity: 1;
  279. }
  280. .tl-binding {
  281. fill: none;
  282. stroke: var(--tl-selectStroke);
  283. stroke-width: calc(2px * var(--tl-scale));
  284. }
  285. `
  286. export function useTLTheme(theme?: Partial<TLTheme>) {
  287. const tltheme = React.useMemo<TLTheme>(
  288. () => ({
  289. ...defaultTheme,
  290. ...theme,
  291. }),
  292. [theme]
  293. )
  294. useTheme('tl', tltheme)
  295. useStyle('tl-canvas', tlcss)
  296. }