useStylesheet.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534
  1. import * as React from 'react'
  2. import { BINDING_DISTANCE, TLTheme } from '@tldraw/core'
  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 = '.logseq-tldraw') {
  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. accent: 'rgb(255, 0, 0)',
  58. brushFill: 'rgba(0,0,0,.05)',
  59. brushStroke: 'rgba(0,0,0,.25)',
  60. selectStroke: 'rgb(66, 133, 244)',
  61. selectFill: 'rgba(65, 132, 244, 0.05)',
  62. binding: 'rgba(65, 132, 244, 0.5)',
  63. background: 'var(--ls-primary-background-color)',
  64. foreground: 'var(--ls-secondary-text-color)',
  65. grid: 'var(--ls-quaternary-background-color)',
  66. }
  67. const tlcss = css`
  68. @font-face {
  69. font-family: 'Recursive';
  70. font-style: normal;
  71. font-weight: 500;
  72. font-display: swap;
  73. src: url(https://fonts.gstatic.com/s/recursive/v23/8vI-7wMr0mhh-RQChyHEH06TlXhq_gukbYrFMk1QuAIcyEwG_X-dpEfaE5YaERmK-CImKsvxvU-MXGX2fSqasNfUlTGZnI14ZeY.woff2)
  74. format('woff2');
  75. unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
  76. U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
  77. }
  78. @font-face {
  79. font-family: 'Recursive';
  80. font-style: normal;
  81. font-weight: 700;
  82. font-display: swap;
  83. src: url(https://fonts.gstatic.com/s/recursive/v23/8vI-7wMr0mhh-RQChyHEH06TlXhq_gukbYrFMk1QuAIcyEwG_X-dpEfaE5YaERmK-CImKsvxvU-MXGX2fSqasNfUlTGZnI14ZeY.woff2)
  84. format('woff2');
  85. unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
  86. U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
  87. }
  88. @font-face {
  89. font-family: 'Recursive Mono';
  90. font-style: normal;
  91. font-weight: 420;
  92. font-display: swap;
  93. src: url(https://fonts.gstatic.com/s/recursive/v23/8vI-7wMr0mhh-RQChyHEH06TlXhq_gukbYrFMk1QuAIcyEwG_X-dpEfaE5YaERmK-CImqvTxvU-MXGX2fSqasNfUlTGZnI14ZeY.woff2)
  94. format('woff2');
  95. unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
  96. U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
  97. }
  98. .tl-container {
  99. --tl-cursor: inherit;
  100. --tl-zoom: 1;
  101. --tl-scale: calc(1 / var(--tl-zoom));
  102. --tl-padding: 64px;
  103. --tl-shadow-color: 0deg 0% 0%;
  104. --tl-binding-distance: ${BINDING_DISTANCE}px;
  105. --tl-shadow-elevation-low: 0px 0.4px 0.5px hsl(var(--tl-shadow-color) / 0.04),
  106. 0px 0.6px 0.8px -0.7px hsl(var(--tl-shadow-color) / 0.06),
  107. 0.1px 1.2px 1.5px -1.4px hsl(var(--tl-shadow-color) / 0.08);
  108. --tl-shadow-elevation-medium: 0px 0.4px 0.5px hsl(var(--tl-shadow-color) / 0.04),
  109. 0.1px 1.3px 1.7px -0.5px hsl(var(--tl-shadow-color) / 0.06),
  110. 0.1px 2.8px 3.6px -1px hsl(var(--tl-shadow-color) / 0.07),
  111. 0.3px 6.1px 7.8px -1.4px hsl(var(--tl-shadow-color) / 0.09);
  112. --tl-shadow-elevation-high: 0px 0.4px 0.5px hsl(var(--tl-shadow-color) / 0.04),
  113. 0.1px 2.3px 3px -0.2px hsl(var(--tl-shadow-color) / 0.05),
  114. 0.2px 4.1px 5.3px -0.5px hsl(var(--tl-shadow-color) / 0.06),
  115. 0.4px 6.6px 8.5px -0.7px hsl(var(--tl-shadow-color) / 0.07),
  116. 0.6px 10.3px 13.2px -1px hsl(var(--tl-shadow-color) / 0.08),
  117. 0.9px 16px 20.6px -1.2px hsl(var(--tl-shadow-color) / 0.09),
  118. 1.3px 24.3px 31.2px -1.4px hsl(var(--tl-shadow-color) / 0.1);
  119. box-sizing: border-box;
  120. position: relative;
  121. top: 0px;
  122. left: 0px;
  123. width: 100%;
  124. height: 100%;
  125. max-width: 100%;
  126. max-height: 100%;
  127. box-sizing: border-box;
  128. padding: 0px;
  129. margin: 0px;
  130. outline: none;
  131. z-index: 100;
  132. user-select: none;
  133. touch-action: none;
  134. overscroll-behavior: none;
  135. background-color: var(--tl-background);
  136. cursor: var(--tl-cursor) !important;
  137. box-sizing: border-box;
  138. color: var(--tl-foreground);
  139. willChange: transform;
  140. }
  141. .tl-overlay {
  142. background: none;
  143. fill: transparent;
  144. position: absolute;
  145. width: 100%;
  146. height: 100%;
  147. touch-action: none;
  148. pointer-events: none;
  149. }
  150. .tl-grid {
  151. position: absolute;
  152. width: 100%;
  153. height: 100%;
  154. touch-action: none;
  155. pointer-events: none;
  156. user-select: none;
  157. }
  158. .tl-snap-line {
  159. stroke: var(--tl-accent);
  160. stroke-width: calc(1px * var(--tl-scale));
  161. }
  162. .tl-snap-point {
  163. stroke: var(--tl-accent);
  164. stroke-width: calc(1px * var(--tl-scale));
  165. }
  166. .tl-canvas {
  167. position: absolute;
  168. width: 100%;
  169. height: 100%;
  170. touch-action: none;
  171. pointer-events: all;
  172. overflow: clip;
  173. outline: none;
  174. }
  175. .tl-layer {
  176. position: absolute;
  177. top: 0px;
  178. left: 0px;
  179. height: 0px;
  180. width: 0px;
  181. contain: layout style size;
  182. }
  183. .tl-absolute {
  184. position: absolute;
  185. top: 0px;
  186. left: 0px;
  187. transform-origin: center center;
  188. contain: layout style size;
  189. willChange: transform;
  190. }
  191. .tl-positioned {
  192. position: absolute;
  193. transform-origin: center center;
  194. pointer-events: none;
  195. display: flex;
  196. align-items: center;
  197. justify-content: center;
  198. contain: layout style size;
  199. }
  200. .tl-positioned-svg {
  201. width: 100%;
  202. height: 100%;
  203. overflow: hidden;
  204. contain: layout style size;
  205. pointer-events: none;
  206. }
  207. .tl-positioned-div {
  208. position: relative;
  209. width: 100%;
  210. height: 100%;
  211. padding: var(--tl-padding);
  212. contain: layout style size;
  213. }
  214. .tl-positioned-inner {
  215. position: relative;
  216. width: 100%;
  217. height: 100%;
  218. }
  219. .tl-counter-scaled {
  220. transform: scale(var(--tl-scale));
  221. }
  222. .tl-dashed {
  223. stroke-dasharray: calc(2px * var(--tl-scale)), calc(2px * var(--tl-scale));
  224. }
  225. .tl-transparent {
  226. fill: transparent;
  227. stroke: transparent;
  228. }
  229. .tl-corner-handle {
  230. stroke: var(--tl-selectStroke);
  231. fill: var(--tl-background);
  232. stroke-width: calc(1.5px * var(--tl-scale));
  233. }
  234. .tl-rotate-handle {
  235. stroke: var(--tl-selectStroke);
  236. fill: var(--tl-background);
  237. stroke-width: calc(1.5px * var(--tl-scale));
  238. }
  239. .tl-binding {
  240. fill: var(--tl-selectFill);
  241. stroke: var(--tl-selectStroke);
  242. stroke-width: calc(1px * var(--tl-scale));
  243. pointer-events: none;
  244. }
  245. .tl-user {
  246. left: -4px;
  247. top: -4px;
  248. height: 8px;
  249. width: 8px;
  250. border-radius: 100%;
  251. pointer-events: none;
  252. }
  253. .tl-indicator {
  254. fill: transparent;
  255. stroke-width: calc(1.5px * var(--tl-scale));
  256. pointer-events: none;
  257. }
  258. .tl-indicator-container {
  259. transform-origin: 0 0;
  260. fill: transparent;
  261. stroke-width: calc(1.5px * var(--tl-scale));
  262. pointer-events: none;
  263. }
  264. .tl-user-indicator-bounds {
  265. border-style: solid;
  266. border-width: calc(1px * var(--tl-scale));
  267. }
  268. .tl-selected {
  269. stroke: var(--tl-selectStroke);
  270. }
  271. .tl-hovered {
  272. stroke: var(--tl-selectStroke);
  273. }
  274. .tl-clone-target {
  275. pointer-events: all;
  276. }
  277. .tl-clone-target:hover .tl-clone-button {
  278. opacity: 1;
  279. }
  280. .tl-clone-button-target {
  281. cursor: pointer;
  282. pointer-events: all;
  283. }
  284. .tl-clone-button-target:hover .tl-clone-button {
  285. fill: var(--tl-selectStroke);
  286. }
  287. .tl-clone-button {
  288. opacity: 0;
  289. r: calc(8px * var(--tl-scale));
  290. stroke-width: calc(1.5px * var(--tl-scale));
  291. stroke: var(--tl-selectStroke);
  292. fill: var(--tl-background);
  293. }
  294. .tl-bounds {
  295. pointer-events: none;
  296. contain: layout style size;
  297. }
  298. .tl-bounds-bg {
  299. stroke: none;
  300. fill: var(--tl-selectFill);
  301. pointer-events: all;
  302. contain: layout style size;
  303. }
  304. .tl-bounds-fg {
  305. fill: transparent;
  306. stroke: var(--tl-selectStroke);
  307. stroke-width: calc(1.5px * var(--tl-scale));
  308. }
  309. .tl-brush {
  310. fill: var(--tl-brushFill);
  311. stroke: var(--tl-brushStroke);
  312. stroke-width: calc(1px * var(--tl-scale));
  313. pointer-events: none;
  314. }
  315. .tl-dot {
  316. fill: var(--tl-background);
  317. stroke: var(--tl-foreground);
  318. stroke-width: 2px;
  319. }
  320. .tl-handle {
  321. fill: var(--tl-background);
  322. stroke: var(--tl-selectStroke);
  323. stroke-width: 1.5px;
  324. pointer-events: none;
  325. }
  326. .tl-handle-bg {
  327. fill: transparent;
  328. stroke: none;
  329. r: calc(16px / max(1, var(--tl-zoom)));
  330. pointer-events: all;
  331. cursor: grab;
  332. }
  333. .tl-handle-bg:active {
  334. pointer-events: all;
  335. fill: none;
  336. }
  337. .tl-handle-bg:hover {
  338. cursor: grab;
  339. fill: var(--tl-selectFill);
  340. }
  341. .tl-binding-indicator {
  342. fill: transparent;
  343. stroke: var(--tl-binding);
  344. }
  345. .tl-centered {
  346. display: grid;
  347. place-content: center;
  348. place-items: center;
  349. }
  350. .tl-centered > * {
  351. grid-column: 1;
  352. grid-row: 1;
  353. }
  354. .tl-centered-g {
  355. transform: translate(var(--tl-padding), var(--tl-padding));
  356. }
  357. .tl-current-parent > *[data-shy='true'] {
  358. opacity: 1;
  359. }
  360. .tl-binding {
  361. fill: none;
  362. stroke: var(--tl-selectStroke);
  363. stroke-width: calc(2px * var(--tl-scale));
  364. }
  365. .tl-grid-dot {
  366. fill: var(--tl-grid);
  367. }
  368. .tl-counter-scaled-positioned {
  369. position: absolute;
  370. top: 0;
  371. left: 0;
  372. pointer-events: none;
  373. padding: 0;
  374. contain: layout style size;
  375. }
  376. .tl-fade-in {
  377. opacity: 1;
  378. transition-timing-function: ease-in-out;
  379. transition-property: opacity;
  380. transition-duration: 0.12s;
  381. transition-delay: 0;
  382. }
  383. .tl-fade-out {
  384. opacity: 0;
  385. transition-timing-function: ease-out;
  386. transition-property: opacity;
  387. transition-duration: 0.12s;
  388. transition-delay: 0;
  389. }
  390. .tl-counter-scaled-positioned > .tl-positioned-div {
  391. user-select: none;
  392. padding: 64px;
  393. }
  394. .tl-context-bar > * {
  395. grid-column: 1;
  396. grid-row: 1;
  397. }
  398. .tl-bounds-detail {
  399. padding: 2px 3px;
  400. border-radius: 1px;
  401. white-space: nowrap;
  402. width: fit-content;
  403. text-align: center;
  404. font-size: 12px;
  405. font-weight: 500;
  406. background-color: var(--tl-selectStroke);
  407. color: var(--tl-background);
  408. }
  409. .tl-hitarea-stroke {
  410. fill: none;
  411. stroke: transparent;
  412. pointer-events: stroke;
  413. stroke-width: min(100px, calc(24px * var(--tl-scale)));
  414. }
  415. .tl-hitarea-fill {
  416. fill: transparent;
  417. stroke: transparent;
  418. pointer-events: all;
  419. stroke-width: min(100px, calc(24px * var(--tl-scale)));
  420. }
  421. .tl-grid {
  422. position: absolute;
  423. width: 100%;
  424. height: 100%;
  425. touch-action: none;
  426. pointer-events: none;
  427. user-select: none;
  428. }
  429. .tl-grid-dot {
  430. fill: var(--tl-grid);
  431. }
  432. .tl-html-canvas {
  433. position: absolute;
  434. top: 0px;
  435. left: 0px;
  436. width: 100%;
  437. height: 100%;
  438. zindex: 20000;
  439. pointer-events: none;
  440. border: 2px solid red;
  441. }
  442. .tl-direction-indicator {
  443. z-index: 100000;
  444. position: absolute;
  445. top: 0px;
  446. left: 0px;
  447. fill: var(--tl-selectStroke);
  448. }
  449. `
  450. export function useStylesheet(theme?: Partial<TLTheme>, selector?: string) {
  451. const tltheme = React.useMemo<TLTheme>(
  452. () => ({
  453. ...defaultTheme,
  454. ...theme,
  455. }),
  456. [theme]
  457. )
  458. useTheme('tl', tltheme, selector)
  459. useStyle('tl-canvas', tlcss)
  460. }