@basementstudio/shader-lab 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (261) hide show
  1. package/.biome/plugins/README.md +21 -0
  2. package/.biome/plugins/no-anchor-element.grit +12 -0
  3. package/.biome/plugins/no-relative-parent-imports.grit +10 -0
  4. package/.biome/plugins/no-unnecessary-forwardref.grit +9 -0
  5. package/.changeset/README.md +17 -0
  6. package/.changeset/config.json +11 -0
  7. package/.editorconfig +40 -0
  8. package/.env.example +81 -0
  9. package/.gitattributes +19 -0
  10. package/.github/workflows/canary.yml +80 -0
  11. package/.github/workflows/ci.yml +37 -0
  12. package/.github/workflows/release.yml +56 -0
  13. package/.tldrignore +84 -0
  14. package/.vscode/extensions.json +20 -0
  15. package/.vscode/settings.json +105 -0
  16. package/README.md +119 -0
  17. package/biome.json +249 -0
  18. package/bun.lock +1224 -0
  19. package/next.config.ts +131 -0
  20. package/package.json +73 -0
  21. package/packages/shader-lab-react/CHANGELOG.md +9 -0
  22. package/packages/shader-lab-react/README.md +119 -0
  23. package/packages/shader-lab-react/assets/patterns/bars/1.svg +3 -0
  24. package/packages/shader-lab-react/assets/patterns/bars/2.svg +3 -0
  25. package/packages/shader-lab-react/assets/patterns/bars/3.svg +3 -0
  26. package/packages/shader-lab-react/assets/patterns/bars/4.svg +3 -0
  27. package/packages/shader-lab-react/assets/patterns/bars/5.svg +3 -0
  28. package/packages/shader-lab-react/assets/patterns/bars/6.svg +3 -0
  29. package/packages/shader-lab-react/assets/patterns/candles/1.svg +3 -0
  30. package/packages/shader-lab-react/assets/patterns/candles/2.svg +3 -0
  31. package/packages/shader-lab-react/assets/patterns/candles/3.svg +3 -0
  32. package/packages/shader-lab-react/assets/patterns/candles/4.svg +3 -0
  33. package/packages/shader-lab-react/assets/patterns/shapes/1.svg +3 -0
  34. package/packages/shader-lab-react/assets/patterns/shapes/2.svg +3 -0
  35. package/packages/shader-lab-react/assets/patterns/shapes/3.svg +3 -0
  36. package/packages/shader-lab-react/assets/patterns/shapes/4.svg +4 -0
  37. package/packages/shader-lab-react/assets/patterns/shapes/5.svg +3 -0
  38. package/packages/shader-lab-react/assets/patterns/shapes/6.svg +4 -0
  39. package/packages/shader-lab-react/assets/textures/blue-noise.png +0 -0
  40. package/packages/shader-lab-react/package.json +36 -0
  41. package/packages/shader-lab-react/scripts/fix-esm-specifiers.mjs +57 -0
  42. package/packages/shader-lab-react/scripts/prepare-dist.mjs +4 -0
  43. package/packages/shader-lab-react/src/ambient/three-tsl.d.ts +146 -0
  44. package/packages/shader-lab-react/src/ambient/three-webgpu.d.ts +51 -0
  45. package/packages/shader-lab-react/src/easings.ts +4 -0
  46. package/packages/shader-lab-react/src/index.ts +35 -0
  47. package/packages/shader-lab-react/src/lib/editor/custom-shader/shared.ts +2 -0
  48. package/packages/shader-lab-react/src/renderer/ascii-atlas.ts +83 -0
  49. package/packages/shader-lab-react/src/renderer/ascii-pass.ts +416 -0
  50. package/packages/shader-lab-react/src/renderer/asset-url.ts +3 -0
  51. package/packages/shader-lab-react/src/renderer/blend-modes.ts +229 -0
  52. package/packages/shader-lab-react/src/renderer/contracts.ts +54 -0
  53. package/packages/shader-lab-react/src/renderer/create-webgpu-renderer.ts +48 -0
  54. package/packages/shader-lab-react/src/renderer/crt-pass.ts +1040 -0
  55. package/packages/shader-lab-react/src/renderer/custom-shader-pass.ts +108 -0
  56. package/packages/shader-lab-react/src/renderer/custom-shader-runtime.ts +309 -0
  57. package/packages/shader-lab-react/src/renderer/dither-textures.ts +99 -0
  58. package/packages/shader-lab-react/src/renderer/dithering-pass.ts +322 -0
  59. package/packages/shader-lab-react/src/renderer/gradient-pass.ts +521 -0
  60. package/packages/shader-lab-react/src/renderer/halftone-pass.ts +932 -0
  61. package/packages/shader-lab-react/src/renderer/ink-pass.ts +802 -0
  62. package/packages/shader-lab-react/src/renderer/live-pass.ts +194 -0
  63. package/packages/shader-lab-react/src/renderer/media-pass.ts +187 -0
  64. package/packages/shader-lab-react/src/renderer/media-texture.ts +66 -0
  65. package/packages/shader-lab-react/src/renderer/particle-grid-pass.ts +389 -0
  66. package/packages/shader-lab-react/src/renderer/pass-node.ts +209 -0
  67. package/packages/shader-lab-react/src/renderer/pattern-atlas.ts +133 -0
  68. package/packages/shader-lab-react/src/renderer/pattern-pass.ts +552 -0
  69. package/packages/shader-lab-react/src/renderer/pipeline-manager.ts +369 -0
  70. package/packages/shader-lab-react/src/renderer/pixel-sorting-pass.ts +277 -0
  71. package/packages/shader-lab-react/src/renderer/shaders/tsl/color/tonemapping.ts +87 -0
  72. package/packages/shader-lab-react/src/renderer/shaders/tsl/cosine-palette.ts +9 -0
  73. package/packages/shader-lab-react/src/renderer/shaders/tsl/noise/common.ts +31 -0
  74. package/packages/shader-lab-react/src/renderer/shaders/tsl/noise/curl-noise-3d.ts +36 -0
  75. package/packages/shader-lab-react/src/renderer/shaders/tsl/noise/curl-noise-4d.ts +36 -0
  76. package/packages/shader-lab-react/src/renderer/shaders/tsl/noise/fbm.ts +13 -0
  77. package/packages/shader-lab-react/src/renderer/shaders/tsl/noise/perlin-noise-3d.ts +96 -0
  78. package/packages/shader-lab-react/src/renderer/shaders/tsl/noise/ridge-noise.ts +24 -0
  79. package/packages/shader-lab-react/src/renderer/shaders/tsl/noise/simplex-noise-3d.ts +79 -0
  80. package/packages/shader-lab-react/src/renderer/shaders/tsl/noise/simplex-noise-4d.ts +89 -0
  81. package/packages/shader-lab-react/src/renderer/shaders/tsl/noise/turbulence.ts +56 -0
  82. package/packages/shader-lab-react/src/renderer/shaders/tsl/noise/value-noise-3d.ts +32 -0
  83. package/packages/shader-lab-react/src/renderer/shaders/tsl/noise/voronoi-noise-3d.ts +60 -0
  84. package/packages/shader-lab-react/src/renderer/shaders/tsl/patterns/bloom-edge-pattern.ts +15 -0
  85. package/packages/shader-lab-react/src/renderer/shaders/tsl/patterns/bloom.ts +11 -0
  86. package/packages/shader-lab-react/src/renderer/shaders/tsl/patterns/canvas-weave-pattern.ts +24 -0
  87. package/packages/shader-lab-react/src/renderer/shaders/tsl/patterns/grain-texture-pattern.ts +9 -0
  88. package/packages/shader-lab-react/src/renderer/shaders/tsl/patterns/repeating-pattern.ts +11 -0
  89. package/packages/shader-lab-react/src/renderer/shaders/tsl/utils/atan2.ts +9 -0
  90. package/packages/shader-lab-react/src/renderer/shaders/tsl/utils/complex-conj.ts +9 -0
  91. package/packages/shader-lab-react/src/renderer/shaders/tsl/utils/complex-cos.ts +10 -0
  92. package/packages/shader-lab-react/src/renderer/shaders/tsl/utils/complex-div.ts +11 -0
  93. package/packages/shader-lab-react/src/renderer/shaders/tsl/utils/complex-log.ts +7 -0
  94. package/packages/shader-lab-react/src/renderer/shaders/tsl/utils/complex-mobius.ts +12 -0
  95. package/packages/shader-lab-react/src/renderer/shaders/tsl/utils/complex-mul.ts +9 -0
  96. package/packages/shader-lab-react/src/renderer/shaders/tsl/utils/complex-pow.ts +16 -0
  97. package/packages/shader-lab-react/src/renderer/shaders/tsl/utils/complex-sin.ts +10 -0
  98. package/packages/shader-lab-react/src/renderer/shaders/tsl/utils/complex-sqrt.ts +18 -0
  99. package/packages/shader-lab-react/src/renderer/shaders/tsl/utils/complex-tan.ts +12 -0
  100. package/packages/shader-lab-react/src/renderer/shaders/tsl/utils/complex-to-polar.ts +10 -0
  101. package/packages/shader-lab-react/src/renderer/shaders/tsl/utils/hyperbolic.ts +20 -0
  102. package/packages/shader-lab-react/src/renderer/shaders/tsl/utils/index.ts +48 -0
  103. package/packages/shader-lab-react/src/renderer/shaders/tsl/utils/rotate.ts +15 -0
  104. package/packages/shader-lab-react/src/renderer/shaders/tsl/utils/screen-aspect-uv.ts +15 -0
  105. package/packages/shader-lab-react/src/renderer/shaders/tsl/utils/sd-box-2d.ts +6 -0
  106. package/packages/shader-lab-react/src/renderer/shaders/tsl/utils/sd-diamond.ts +6 -0
  107. package/packages/shader-lab-react/src/renderer/shaders/tsl/utils/sd-rhombus.ts +27 -0
  108. package/packages/shader-lab-react/src/renderer/shaders/tsl/utils/sd-sphere.ts +6 -0
  109. package/packages/shader-lab-react/src/renderer/shaders/tsl/utils/smax.ts +7 -0
  110. package/packages/shader-lab-react/src/renderer/shaders/tsl/utils/smin.ts +7 -0
  111. package/packages/shader-lab-react/src/renderer/text-pass.ts +176 -0
  112. package/packages/shader-lab-react/src/runtime-clock.ts +42 -0
  113. package/packages/shader-lab-react/src/runtime-frame.ts +29 -0
  114. package/packages/shader-lab-react/src/shader-lab-composition.tsx +163 -0
  115. package/packages/shader-lab-react/src/timeline.ts +283 -0
  116. package/packages/shader-lab-react/src/types/editor.ts +5 -0
  117. package/packages/shader-lab-react/src/types.ts +141 -0
  118. package/packages/shader-lab-react/tsconfig.build.json +8 -0
  119. package/packages/shader-lab-react/tsconfig.json +21 -0
  120. package/postcss.config.mjs +5 -0
  121. package/public/assets/fonts/msdf/geist-mono/GeistMono-Regular-msdf-atlas.png +0 -0
  122. package/public/assets/fonts/msdf/geist-mono/GeistMono-Regular-msdf.json +1412 -0
  123. package/public/assets/patterns/bars/1.svg +3 -0
  124. package/public/assets/patterns/bars/2.svg +3 -0
  125. package/public/assets/patterns/bars/3.svg +3 -0
  126. package/public/assets/patterns/bars/4.svg +3 -0
  127. package/public/assets/patterns/bars/5.svg +3 -0
  128. package/public/assets/patterns/bars/6.svg +3 -0
  129. package/public/assets/patterns/candles/1.svg +3 -0
  130. package/public/assets/patterns/candles/2.svg +3 -0
  131. package/public/assets/patterns/candles/3.svg +3 -0
  132. package/public/assets/patterns/candles/4.svg +3 -0
  133. package/public/assets/patterns/shapes/1.svg +3 -0
  134. package/public/assets/patterns/shapes/2.svg +3 -0
  135. package/public/assets/patterns/shapes/3.svg +3 -0
  136. package/public/assets/patterns/shapes/4.svg +4 -0
  137. package/public/assets/patterns/shapes/5.svg +3 -0
  138. package/public/assets/patterns/shapes/6.svg +4 -0
  139. package/public/fonts/geist/Geist-Mono.woff2 +0 -0
  140. package/public/textures/blue-noise.png +0 -0
  141. package/public/textures/crt-mask.png +0 -0
  142. package/src/app/design/page.tsx +398 -0
  143. package/src/app/favicon.ico +0 -0
  144. package/src/app/globals.css +280 -0
  145. package/src/app/layout.tsx +89 -0
  146. package/src/app/page.tsx +20 -0
  147. package/src/app/robots.ts +13 -0
  148. package/src/app/sitemap.ts +13 -0
  149. package/src/components/editor/editor-canvas-viewport.tsx +116 -0
  150. package/src/components/editor/editor-export-dialog.tsx +1177 -0
  151. package/src/components/editor/editor-timeline-overlay.tsx +983 -0
  152. package/src/components/editor/editor-topbar.tsx +287 -0
  153. package/src/components/editor/layer-sidebar.tsx +738 -0
  154. package/src/components/editor/properties-sidebar-content.tsx +574 -0
  155. package/src/components/editor/properties-sidebar-fields.tsx +389 -0
  156. package/src/components/editor/properties-sidebar-utils.ts +178 -0
  157. package/src/components/editor/properties-sidebar.tsx +421 -0
  158. package/src/components/ui/button/index.tsx +57 -0
  159. package/src/components/ui/color-picker/index.tsx +358 -0
  160. package/src/components/ui/glass-panel/index.tsx +45 -0
  161. package/src/components/ui/icon-button/index.tsx +46 -0
  162. package/src/components/ui/select/index.tsx +136 -0
  163. package/src/components/ui/slider/index.tsx +192 -0
  164. package/src/components/ui/toggle/index.tsx +34 -0
  165. package/src/components/ui/typography/index.tsx +61 -0
  166. package/src/components/ui/xy-pad/index.tsx +160 -0
  167. package/src/features/editor/components/editor-export-dialog.module.css +271 -0
  168. package/src/hooks/use-editor-renderer.ts +182 -0
  169. package/src/lib/app.ts +6 -0
  170. package/src/lib/cn.ts +7 -0
  171. package/src/lib/easings.ts +240 -0
  172. package/src/lib/editor/config/layer-registry.ts +2434 -0
  173. package/src/lib/editor/custom-shader/shared.ts +28 -0
  174. package/src/lib/editor/export.ts +420 -0
  175. package/src/lib/editor/history.ts +71 -0
  176. package/src/lib/editor/layers.ts +76 -0
  177. package/src/lib/editor/parameter-schema.ts +75 -0
  178. package/src/lib/editor/project-file.ts +145 -0
  179. package/src/lib/editor/shader-export-snippet.ts +37 -0
  180. package/src/lib/editor/shader-export.ts +315 -0
  181. package/src/lib/editor/timeline/evaluate.ts +252 -0
  182. package/src/lib/editor/view-transform.ts +58 -0
  183. package/src/lib/fonts.ts +28 -0
  184. package/src/renderer/ascii-atlas.ts +83 -0
  185. package/src/renderer/ascii-pass.ts +416 -0
  186. package/src/renderer/blend-modes.ts +229 -0
  187. package/src/renderer/contracts.ts +161 -0
  188. package/src/renderer/create-webgpu-renderer.ts +48 -0
  189. package/src/renderer/crt-pass.ts +1040 -0
  190. package/src/renderer/custom-shader-pass.ts +117 -0
  191. package/src/renderer/custom-shader-runtime.ts +309 -0
  192. package/src/renderer/dither-textures.ts +99 -0
  193. package/src/renderer/dithering-pass.ts +322 -0
  194. package/src/renderer/gradient-pass.ts +520 -0
  195. package/src/renderer/halftone-pass.ts +932 -0
  196. package/src/renderer/ink-pass.ts +683 -0
  197. package/src/renderer/live-pass.ts +194 -0
  198. package/src/renderer/media-pass.ts +187 -0
  199. package/src/renderer/media-texture.ts +66 -0
  200. package/src/renderer/particle-grid-pass.ts +389 -0
  201. package/src/renderer/pass-node-factory.ts +33 -0
  202. package/src/renderer/pass-node.ts +209 -0
  203. package/src/renderer/pattern-atlas.ts +97 -0
  204. package/src/renderer/pattern-pass.ts +552 -0
  205. package/src/renderer/pipeline-manager.ts +343 -0
  206. package/src/renderer/pixel-sorting-pass.ts +277 -0
  207. package/src/renderer/project-clock.ts +57 -0
  208. package/src/renderer/shaders/tsl/color/tonemapping.ts +86 -0
  209. package/src/renderer/shaders/tsl/cosine-palette.ts +8 -0
  210. package/src/renderer/shaders/tsl/noise/common.ts +30 -0
  211. package/src/renderer/shaders/tsl/noise/curl-noise-3d.ts +35 -0
  212. package/src/renderer/shaders/tsl/noise/curl-noise-4d.ts +35 -0
  213. package/src/renderer/shaders/tsl/noise/fbm.ts +12 -0
  214. package/src/renderer/shaders/tsl/noise/perlin-noise-3d.ts +97 -0
  215. package/src/renderer/shaders/tsl/noise/ridge-noise.ts +23 -0
  216. package/src/renderer/shaders/tsl/noise/simplex-noise-3d.ts +78 -0
  217. package/src/renderer/shaders/tsl/noise/simplex-noise-4d.ts +88 -0
  218. package/src/renderer/shaders/tsl/noise/turbulence.ts +55 -0
  219. package/src/renderer/shaders/tsl/noise/value-noise-3d.ts +31 -0
  220. package/src/renderer/shaders/tsl/noise/voronoi-noise-3d.ts +59 -0
  221. package/src/renderer/shaders/tsl/patterns/bloom-edge-pattern.ts +14 -0
  222. package/src/renderer/shaders/tsl/patterns/bloom.ts +10 -0
  223. package/src/renderer/shaders/tsl/patterns/canvas-weave-pattern.ts +23 -0
  224. package/src/renderer/shaders/tsl/patterns/grain-texture-pattern.ts +8 -0
  225. package/src/renderer/shaders/tsl/patterns/repeating-pattern.ts +10 -0
  226. package/src/renderer/shaders/tsl/utils/atan2.ts +8 -0
  227. package/src/renderer/shaders/tsl/utils/complex-conj.ts +8 -0
  228. package/src/renderer/shaders/tsl/utils/complex-cos.ts +9 -0
  229. package/src/renderer/shaders/tsl/utils/complex-div.ts +10 -0
  230. package/src/renderer/shaders/tsl/utils/complex-log.ts +6 -0
  231. package/src/renderer/shaders/tsl/utils/complex-mobius.ts +11 -0
  232. package/src/renderer/shaders/tsl/utils/complex-mul.ts +8 -0
  233. package/src/renderer/shaders/tsl/utils/complex-pow.ts +15 -0
  234. package/src/renderer/shaders/tsl/utils/complex-sin.ts +9 -0
  235. package/src/renderer/shaders/tsl/utils/complex-sqrt.ts +17 -0
  236. package/src/renderer/shaders/tsl/utils/complex-tan.ts +11 -0
  237. package/src/renderer/shaders/tsl/utils/complex-to-polar.ts +9 -0
  238. package/src/renderer/shaders/tsl/utils/hyperbolic.ts +19 -0
  239. package/src/renderer/shaders/tsl/utils/index.ts +47 -0
  240. package/src/renderer/shaders/tsl/utils/rotate.ts +14 -0
  241. package/src/renderer/shaders/tsl/utils/screen-aspect-uv.ts +14 -0
  242. package/src/renderer/shaders/tsl/utils/sd-box-2d.ts +5 -0
  243. package/src/renderer/shaders/tsl/utils/sd-diamond.ts +5 -0
  244. package/src/renderer/shaders/tsl/utils/sd-rhombus.ts +26 -0
  245. package/src/renderer/shaders/tsl/utils/sd-sphere.ts +5 -0
  246. package/src/renderer/shaders/tsl/utils/smax.ts +7 -0
  247. package/src/renderer/shaders/tsl/utils/smin.ts +6 -0
  248. package/src/renderer/text-pass.ts +176 -0
  249. package/src/store/asset-store.ts +193 -0
  250. package/src/store/editor-store.ts +223 -0
  251. package/src/store/history-store.ts +172 -0
  252. package/src/store/index.ts +31 -0
  253. package/src/store/layer-store.ts +675 -0
  254. package/src/store/timeline-store.ts +572 -0
  255. package/src/types/assets.d.ts +6 -0
  256. package/src/types/css.d.ts +21 -0
  257. package/src/types/editor.ts +357 -0
  258. package/src/types/react.d.ts +15 -0
  259. package/src/types/three-tsl.d.ts +146 -0
  260. package/src/types/three-webgpu.d.ts +51 -0
  261. package/tsconfig.json +49 -0
@@ -0,0 +1,358 @@
1
+ "use client"
2
+
3
+ import { useEffect, useMemo, useRef, useState, type CSSProperties } from "react"
4
+ import { createPortal } from "react-dom"
5
+ import { cn } from "@/lib/cn"
6
+ import { GlassPanel } from "@/components/ui/glass-panel"
7
+
8
+ type HsvColor = {
9
+ h: number
10
+ s: number
11
+ v: number
12
+ }
13
+
14
+ type PopupPosition = {
15
+ left: number
16
+ top: number
17
+ }
18
+
19
+ type ColorPickerProps = {
20
+ className?: string
21
+ onValueChange: (value: string) => void
22
+ value: string
23
+ }
24
+
25
+ function clamp(value: number, min: number, max: number): number {
26
+ return Math.min(max, Math.max(min, value))
27
+ }
28
+
29
+ function normalizeHex(value: string): string | null {
30
+ const normalized = value.trim().replace(/^#/, "")
31
+
32
+ if (/^[\da-fA-F]{3}$/.test(normalized)) {
33
+ const expanded = normalized
34
+ .split("")
35
+ .map((entry) => `${entry}${entry}`)
36
+ .join("")
37
+
38
+ return `#${expanded.toUpperCase()}`
39
+ }
40
+
41
+ if (/^[\da-fA-F]{6}$/.test(normalized)) {
42
+ return `#${normalized.toUpperCase()}`
43
+ }
44
+
45
+ return null
46
+ }
47
+
48
+ function hexToRgb(value: string): { b: number; g: number; r: number } {
49
+ const hex = normalizeHex(value) ?? "#FFFFFF"
50
+ return {
51
+ b: Number.parseInt(hex.slice(5, 7), 16),
52
+ g: Number.parseInt(hex.slice(3, 5), 16),
53
+ r: Number.parseInt(hex.slice(1, 3), 16),
54
+ }
55
+ }
56
+
57
+ function rgbToHex(red: number, green: number, blue: number): string {
58
+ return `#${[red, green, blue]
59
+ .map((value) => clamp(Math.round(value), 0, 255).toString(16).padStart(2, "0"))
60
+ .join("")
61
+ .toUpperCase()}`
62
+ }
63
+
64
+ function rgbToHsv(red: number, green: number, blue: number): HsvColor {
65
+ const r = red / 255
66
+ const g = green / 255
67
+ const b = blue / 255
68
+ const max = Math.max(r, g, b)
69
+ const min = Math.min(r, g, b)
70
+ const delta = max - min
71
+ let h = 0
72
+
73
+ if (delta !== 0) {
74
+ if (max === r) {
75
+ h = ((g - b) / delta) % 6
76
+ } else if (max === g) {
77
+ h = (b - r) / delta + 2
78
+ } else {
79
+ h = (r - g) / delta + 4
80
+ }
81
+ }
82
+
83
+ return {
84
+ h: ((h * 60) + 360) % 360,
85
+ s: max === 0 ? 0 : delta / max,
86
+ v: max,
87
+ }
88
+ }
89
+
90
+ function hsvToRgb(hue: number, saturation: number, value: number) {
91
+ const chroma = value * saturation
92
+ const huePrime = (((hue % 360) + 360) % 360) / 60
93
+ const x = chroma * (1 - Math.abs((huePrime % 2) - 1))
94
+ let red = 0
95
+ let green = 0
96
+ let blue = 0
97
+
98
+ if (huePrime >= 0 && huePrime < 1) {
99
+ red = chroma
100
+ green = x
101
+ } else if (huePrime < 2) {
102
+ red = x
103
+ green = chroma
104
+ } else if (huePrime < 3) {
105
+ green = chroma
106
+ blue = x
107
+ } else if (huePrime < 4) {
108
+ green = x
109
+ blue = chroma
110
+ } else if (huePrime < 5) {
111
+ red = x
112
+ blue = chroma
113
+ } else {
114
+ red = chroma
115
+ blue = x
116
+ }
117
+
118
+ const match = value - chroma
119
+
120
+ return {
121
+ b: (blue + match) * 255,
122
+ g: (green + match) * 255,
123
+ r: (red + match) * 255,
124
+ }
125
+ }
126
+
127
+ function colorFromHsv(color: HsvColor): string {
128
+ const rgb = hsvToRgb(color.h, color.s, color.v)
129
+ return rgbToHex(rgb.r, rgb.g, rgb.b)
130
+ }
131
+
132
+ function hueToHex(hue: number): string {
133
+ const rgb = hsvToRgb(hue, 1, 1)
134
+ return rgbToHex(rgb.r, rgb.g, rgb.b)
135
+ }
136
+
137
+ export function ColorPicker({ className, onValueChange, value }: ColorPickerProps) {
138
+ const triggerRef = useRef<HTMLButtonElement | null>(null)
139
+ const surfaceRef = useRef<HTMLDivElement | null>(null)
140
+ const hueRef = useRef<HTMLDivElement | null>(null)
141
+ const [isOpen, setIsOpen] = useState(false)
142
+ const [inputValue, setInputValue] = useState(normalizeHex(value) ?? "#FFFFFF")
143
+ const [popupPosition, setPopupPosition] = useState<PopupPosition>({ left: 0, top: 0 })
144
+ const [color, setColor] = useState(() => {
145
+ const rgb = hexToRgb(value)
146
+ return rgbToHsv(rgb.r, rgb.g, rgb.b)
147
+ })
148
+
149
+ useEffect(() => {
150
+ const hex = normalizeHex(value) ?? "#FFFFFF"
151
+ const rgb = hexToRgb(hex)
152
+ setInputValue(hex)
153
+ setColor(rgbToHsv(rgb.r, rgb.g, rgb.b))
154
+ }, [value])
155
+
156
+ useEffect(() => {
157
+ if (!isOpen) {
158
+ return
159
+ }
160
+
161
+ const updatePosition = () => {
162
+ const trigger = triggerRef.current
163
+ if (!trigger) {
164
+ return
165
+ }
166
+
167
+ const rect = trigger.getBoundingClientRect()
168
+ setPopupPosition({
169
+ left: rect.right - 208,
170
+ top: rect.bottom + 8,
171
+ })
172
+ }
173
+
174
+ const handlePointerDown = (event: PointerEvent) => {
175
+ const trigger = triggerRef.current
176
+ const surface = surfaceRef.current
177
+ const hue = hueRef.current
178
+ const target = event.target as Node | null
179
+
180
+ if (
181
+ trigger?.contains(target) ||
182
+ surface?.contains(target) ||
183
+ hue?.contains(target) ||
184
+ (target instanceof HTMLElement && target.closest("[data-color-picker-popup]"))
185
+ ) {
186
+ return
187
+ }
188
+
189
+ setIsOpen(false)
190
+ }
191
+
192
+ const handleKeyDown = (event: KeyboardEvent) => {
193
+ if (event.key === "Escape") {
194
+ setIsOpen(false)
195
+ }
196
+ }
197
+
198
+ updatePosition()
199
+ window.addEventListener("resize", updatePosition)
200
+ window.addEventListener("scroll", updatePosition, true)
201
+ window.addEventListener("pointerdown", handlePointerDown)
202
+ window.addEventListener("keydown", handleKeyDown)
203
+
204
+ return () => {
205
+ window.removeEventListener("resize", updatePosition)
206
+ window.removeEventListener("scroll", updatePosition, true)
207
+ window.removeEventListener("pointerdown", handlePointerDown)
208
+ window.removeEventListener("keydown", handleKeyDown)
209
+ }
210
+ }, [isOpen])
211
+
212
+ const hueColor = useMemo(() => hueToHex(color.h), [color.h])
213
+
214
+ const commitColor = (nextColor: HsvColor) => {
215
+ const nextHex = colorFromHsv(nextColor)
216
+ setColor(nextColor)
217
+ setInputValue(nextHex)
218
+ onValueChange(nextHex)
219
+ }
220
+
221
+ const updateSurface = (clientX: number, clientY: number) => {
222
+ const surface = surfaceRef.current
223
+ if (!surface) {
224
+ return
225
+ }
226
+
227
+ const rect = surface.getBoundingClientRect()
228
+ const saturation = clamp((clientX - rect.left) / rect.width, 0, 1)
229
+ const valueLevel = 1 - clamp((clientY - rect.top) / rect.height, 0, 1)
230
+ commitColor({ h: color.h, s: saturation, v: valueLevel })
231
+ }
232
+
233
+ const updateHue = (clientX: number) => {
234
+ const hueElement = hueRef.current
235
+ if (!hueElement) {
236
+ return
237
+ }
238
+
239
+ const rect = hueElement.getBoundingClientRect()
240
+ const hue = clamp((clientX - rect.left) / rect.width, 0, 1) * 360
241
+ commitColor({ h: hue, s: color.s, v: color.v })
242
+ }
243
+
244
+ const handleSurfacePointerDown = (event: React.PointerEvent<HTMLDivElement>) => {
245
+ event.currentTarget.setPointerCapture(event.pointerId)
246
+ updateSurface(event.clientX, event.clientY)
247
+ }
248
+
249
+ const handleSurfacePointerMove = (event: React.PointerEvent<HTMLDivElement>) => {
250
+ if (event.buttons !== 1) {
251
+ return
252
+ }
253
+
254
+ updateSurface(event.clientX, event.clientY)
255
+ }
256
+
257
+ const handleHuePointerDown = (event: React.PointerEvent<HTMLDivElement>) => {
258
+ event.currentTarget.setPointerCapture(event.pointerId)
259
+ updateHue(event.clientX)
260
+ }
261
+
262
+ const handleHuePointerMove = (event: React.PointerEvent<HTMLDivElement>) => {
263
+ if (event.buttons !== 1) {
264
+ return
265
+ }
266
+
267
+ updateHue(event.clientX)
268
+ }
269
+
270
+ const popupStyle = {
271
+ left: `${popupPosition.left}px`,
272
+ position: "fixed",
273
+ top: `${popupPosition.top}px`,
274
+ zIndex: 80,
275
+ } as CSSProperties
276
+
277
+ return (
278
+ <div className={cn("w-[132px]", className)}>
279
+ <button
280
+ className="grid min-h-8 w-full grid-cols-[24px_minmax(0,1fr)] items-center gap-2 rounded-[var(--ds-radius-control)] border border-[var(--ds-border-divider)] bg-[var(--ds-color-surface-control)] px-2 pt-1 pr-2 pb-1 pl-1 text-[var(--ds-color-text-secondary)] transition-[background-color,border-color,transform] duration-160 ease-[var(--ease-out-cubic)] hover:bg-white/8 hover:border-[var(--ds-border-hover)] focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-2 focus-visible:outline-[var(--ds-border-active)] data-[open]:bg-white/8 data-[open]:border-[var(--ds-border-active)]"
281
+ data-open={isOpen ? "" : undefined}
282
+ onClick={() => setIsOpen((current) => !current)}
283
+ ref={triggerRef}
284
+ type="button"
285
+ >
286
+ <span
287
+ className="h-6 w-6 rounded-[var(--ds-radius-thumb)] border border-white/8 shadow-[inset_0_0_0_1px_rgb(0_0_0_/_0.12)]"
288
+ style={{ backgroundColor: inputValue }}
289
+ />
290
+ <span className="overflow-hidden text-ellipsis whitespace-nowrap text-left font-[var(--ds-font-mono)] text-[11px] leading-[14px] uppercase">
291
+ {inputValue}
292
+ </span>
293
+ </button>
294
+
295
+ {isOpen
296
+ ? createPortal(
297
+ <div data-color-picker-popup="" style={popupStyle}>
298
+ <GlassPanel className="flex w-[208px] flex-col gap-3 p-3" variant="panel">
299
+ <div
300
+ className="relative h-[132px] w-full cursor-crosshair overflow-hidden rounded-[10px] select-none"
301
+ onPointerDown={handleSurfacePointerDown}
302
+ onPointerMove={handleSurfacePointerMove}
303
+ ref={surfaceRef}
304
+ style={{ backgroundColor: hueColor }}
305
+ >
306
+ <div className="absolute inset-0 bg-[linear-gradient(90deg,#fff,rgb(255_255_255_/_0%))]" />
307
+ <div className="absolute inset-0 bg-[linear-gradient(0deg,#000,rgb(0_0_0_/_0%))]" />
308
+ <div
309
+ className="absolute h-3 w-3 -translate-x-1/2 -translate-y-1/2 rounded-full border-2 border-white/95 shadow-[0_0_0_1px_rgb(0_0_0_/_0.35),0_2px_6px_rgb(0_0_0_/_0.3)]"
310
+ style={{
311
+ left: `${color.s * 100}%`,
312
+ top: `${(1 - color.v) * 100}%`,
313
+ }}
314
+ />
315
+ </div>
316
+
317
+ <div
318
+ className="relative h-3 w-full cursor-ew-resize rounded-full select-none bg-[linear-gradient(90deg,#ff0000_0%,#ffff00_16.66%,#00ff00_33.33%,#00ffff_50%,#0000ff_66.66%,#ff00ff_83.33%,#ff0000_100%)]"
319
+ onPointerDown={handleHuePointerDown}
320
+ onPointerMove={handleHuePointerMove}
321
+ ref={hueRef}
322
+ >
323
+ <div
324
+ className="absolute top-1/2 h-4 w-[10px] -translate-x-1/2 -translate-y-1/2 rounded-full bg-white/96 shadow-[0_0_0_1px_rgb(0_0_0_/_0.28),0_1px_4px_rgb(0_0_0_/_0.3)]"
325
+ style={{ left: `${(color.h / 360) * 100}%` }}
326
+ />
327
+ </div>
328
+
329
+ <div className="grid grid-cols-[1fr_auto] items-center gap-2">
330
+ <input
331
+ className="min-h-[30px] w-full rounded-[var(--ds-radius-control)] border border-[var(--ds-border-divider)] bg-white/4 px-[10px] font-[var(--ds-font-mono)] text-[11px] leading-[14px] text-[var(--ds-color-text-secondary)] uppercase outline-none focus:border-[var(--ds-border-active)]"
332
+ onChange={(event) => {
333
+ const nextValue = event.target.value.toUpperCase()
334
+ setInputValue(nextValue)
335
+ const nextHex = normalizeHex(nextValue)
336
+ if (!nextHex) {
337
+ return
338
+ }
339
+ const rgb = hexToRgb(nextHex)
340
+ setColor(rgbToHsv(rgb.r, rgb.g, rgb.b))
341
+ onValueChange(nextHex)
342
+ }}
343
+ spellCheck={false}
344
+ type="text"
345
+ value={inputValue}
346
+ />
347
+ <span className="text-right font-[var(--ds-font-mono)] text-[10px] leading-3 text-[var(--ds-color-text-muted)] uppercase">
348
+ HEX
349
+ </span>
350
+ </div>
351
+ </GlassPanel>
352
+ </div>,
353
+ document.body,
354
+ )
355
+ : null}
356
+ </div>
357
+ )
358
+ }
@@ -0,0 +1,45 @@
1
+ import { cva, type VariantProps } from "class-variance-authority"
2
+ import type { HTMLAttributes, ReactNode } from "react"
3
+ import { cn } from "@/lib/cn"
4
+
5
+ const glassPanelVariants = cva(
6
+ "border border-[var(--ds-border-panel)] text-[var(--ds-color-text-primary)] backdrop-blur-[24px] transition-[border-color,background-color,box-shadow] duration-160 ease-[var(--ease-out-cubic)]",
7
+ {
8
+ variants: {
9
+ variant: {
10
+ panel:
11
+ "overflow-clip rounded-[var(--ds-radius-panel)] bg-[var(--ds-color-glass-panel)] shadow-[var(--ds-shadow-panel-dark)]",
12
+ pill:
13
+ "inline-flex min-h-9 items-center gap-[var(--ds-space-2)] rounded-[var(--ds-radius-bar)] bg-[var(--ds-color-glass-pill)] px-[var(--ds-space-3)] shadow-[var(--ds-shadow-pill-dark)]",
14
+ },
15
+ interactive: {
16
+ true: "hover:border-[var(--ds-border-hover)]",
17
+ },
18
+ },
19
+ defaultVariants: {
20
+ variant: "panel",
21
+ },
22
+ })
23
+
24
+ interface GlassPanelProps
25
+ extends HTMLAttributes<HTMLDivElement>,
26
+ VariantProps<typeof glassPanelVariants> {
27
+ children?: ReactNode
28
+ }
29
+
30
+ export function GlassPanel({
31
+ children,
32
+ className,
33
+ interactive,
34
+ variant,
35
+ ...props
36
+ }: GlassPanelProps) {
37
+ return (
38
+ <div
39
+ className={cn(glassPanelVariants({ variant, interactive }), className)}
40
+ {...props}
41
+ >
42
+ {children}
43
+ </div>
44
+ )
45
+ }
@@ -0,0 +1,46 @@
1
+ import { cva, type VariantProps } from "class-variance-authority"
2
+ import type { ButtonHTMLAttributes, ReactNode } from "react"
3
+ import { cn } from "@/lib/cn"
4
+
5
+ const iconButtonVariants = cva(
6
+ "inline-flex h-7 w-7 shrink-0 origin-center items-center justify-center rounded-[var(--ds-radius-icon)] border-0 bg-transparent text-[var(--ds-color-text-tertiary)] transition-[background-color,box-shadow,color,transform] duration-160 ease-[var(--ease-out-cubic)] will-change-transform [&_svg]:h-3.5 [&_svg]:w-3.5 hover:not-disabled:shadow-[inset_0_0_0_1px_rgb(255_255_255_/_0.04)] active:not-disabled:scale-[0.96]",
7
+ {
8
+ variants: {
9
+ variant: {
10
+ ghost:
11
+ "bg-transparent text-[var(--ds-color-text-tertiary)] hover:bg-transparent hover:text-[var(--ds-color-text-primary)] hover:shadow-none",
12
+ default:
13
+ "bg-[var(--ds-color-surface-subtle)] text-[var(--ds-color-text-tertiary)] hover:bg-white/8 hover:text-[var(--ds-color-text-secondary)]",
14
+ hover:
15
+ "bg-[var(--ds-color-surface-active)] text-[var(--ds-color-text-secondary)]",
16
+ active: "bg-white/12 text-white/70",
17
+ },
18
+ },
19
+ defaultVariants: {
20
+ variant: "default",
21
+ },
22
+ })
23
+
24
+ type CommonIconButtonProps = {
25
+ children?: ReactNode
26
+ } & VariantProps<typeof iconButtonVariants>
27
+
28
+ type IconButtonProps = CommonIconButtonProps &
29
+ Omit<ButtonHTMLAttributes<HTMLButtonElement>, "children">
30
+
31
+ export function IconButton({
32
+ children,
33
+ className,
34
+ variant,
35
+ ...props
36
+ }: IconButtonProps) {
37
+ return (
38
+ <button
39
+ className={cn(iconButtonVariants({ variant }), className)}
40
+ type="button"
41
+ {...props}
42
+ >
43
+ {children}
44
+ </button>
45
+ )
46
+ }
@@ -0,0 +1,136 @@
1
+ "use client"
2
+
3
+ import { Select as BaseSelect } from "@base-ui/react/select"
4
+ import type { ReactNode } from "react"
5
+ import { cn } from "@/lib/cn"
6
+
7
+ export interface SelectOption {
8
+ disabled?: boolean
9
+ label: ReactNode
10
+ value: string
11
+ }
12
+
13
+ type SelectProps = Omit<
14
+ BaseSelect.Root.Props<string>,
15
+ "children" | "className" | "items"
16
+ > & {
17
+ className?: string
18
+ iconClassName?: string
19
+ label?: ReactNode
20
+ options: readonly SelectOption[]
21
+ placeholder?: ReactNode
22
+ popupClassName?: string
23
+ triggerAriaLabel?: string
24
+ triggerClassName?: string
25
+ triggerVariant?: "default" | "icon"
26
+ valueClassName?: string
27
+ }
28
+
29
+ function ChevronIcon() {
30
+ return (
31
+ <svg aria-hidden="true" fill="none" viewBox="0 0 10 10">
32
+ <path
33
+ d="M3 4L5 6L7 4"
34
+ stroke="currentColor"
35
+ strokeLinecap="round"
36
+ strokeLinejoin="round"
37
+ strokeWidth="1.2"
38
+ />
39
+ </svg>
40
+ )
41
+ }
42
+
43
+ export function Select({
44
+ className,
45
+ iconClassName,
46
+ label,
47
+ options,
48
+ placeholder = "Select",
49
+ popupClassName,
50
+ triggerAriaLabel,
51
+ triggerClassName,
52
+ triggerVariant = "default",
53
+ valueClassName,
54
+ ...props
55
+ }: SelectProps) {
56
+ const isIconTrigger = triggerVariant === "icon"
57
+
58
+ return (
59
+ <BaseSelect.Root
60
+ items={options.map(({ label: itemLabel, value }) => ({
61
+ label: itemLabel,
62
+ value,
63
+ }))}
64
+ modal={false}
65
+ {...props}
66
+ >
67
+ <div className={cn("flex w-fit flex-col gap-1", className)}>
68
+ {label ? (
69
+ <BaseSelect.Label className="font-[var(--ds-font-mono)] text-[10px] leading-3 text-[var(--ds-color-text-muted)]">
70
+ {label}
71
+ </BaseSelect.Label>
72
+ ) : null}
73
+
74
+ <BaseSelect.Trigger
75
+ aria-label={triggerAriaLabel}
76
+ className={cn(
77
+ isIconTrigger
78
+ ? "group inline-flex h-7 w-7 min-w-0 shrink-0 items-center justify-center rounded-[var(--ds-radius-icon)] border-0 bg-transparent p-0 text-[var(--ds-color-text-tertiary)] transition-[background-color,box-shadow,color,transform] duration-160 ease-[var(--ease-out-cubic)] will-change-transform hover:not-data-disabled:shadow-[inset_0_0_0_1px_rgb(255_255_255_/_0.04)] active:not-data-disabled:scale-[0.96] data-[popup-open]:bg-white/12 data-[popup-open]:text-white/70 data-[disabled]:cursor-not-allowed data-[disabled]:opacity-45 focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-2 focus-visible:outline-[var(--ds-border-active)]"
79
+ : "group inline-flex min-h-8 w-fit min-w-0 items-center justify-between gap-[10px] rounded-[var(--ds-radius-icon)] border border-[var(--ds-border-divider)] bg-[var(--ds-color-surface-control)] px-[10px] py-[6px] text-[var(--ds-color-text-secondary)] transition-[background-color,border-color,transform] duration-160 ease-[var(--ease-out-cubic)] hover:not-data-disabled:bg-white/8 hover:not-data-disabled:border-[var(--ds-border-hover)] active:not-data-disabled:scale-[0.98] data-[popup-open]:bg-white/8 data-[popup-open]:border-[var(--ds-border-hover)] data-[disabled]:cursor-not-allowed data-[disabled]:opacity-45 focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-2 focus-visible:outline-[var(--ds-border-active)]",
80
+ triggerClassName
81
+ )}
82
+ >
83
+ <BaseSelect.Value
84
+ className={cn(
85
+ isIconTrigger
86
+ ? "inline-flex items-center justify-center leading-none text-inherit"
87
+ : "min-w-0 flex-1 font-[var(--ds-font-mono)] text-[11px] leading-[14px] text-inherit data-[placeholder]:text-[var(--ds-color-text-secondary)]",
88
+ valueClassName
89
+ )}
90
+ placeholder={placeholder}
91
+ />
92
+ <BaseSelect.Icon
93
+ className={cn(
94
+ isIconTrigger
95
+ ? "hidden"
96
+ : "inline-flex shrink-0 text-[var(--ds-color-text-tertiary)] transition-[color,transform] duration-160 ease-[var(--ease-out-cubic)] group-data-[popup-open]:rotate-180 group-data-[popup-open]:text-[var(--ds-color-text-secondary)] [&_svg]:h-[10px] [&_svg]:w-[10px]",
97
+ iconClassName
98
+ )}
99
+ >
100
+ <ChevronIcon />
101
+ </BaseSelect.Icon>
102
+ </BaseSelect.Trigger>
103
+ </div>
104
+
105
+ <BaseSelect.Portal>
106
+ <BaseSelect.Positioner
107
+ alignItemWithTrigger={false}
108
+ className="z-50 outline-none"
109
+ sideOffset={8}
110
+ >
111
+ <BaseSelect.Popup
112
+ className={cn(
113
+ "min-w-[var(--anchor-width)] overflow-hidden rounded-[var(--ds-radius-control)] border border-[var(--ds-border-panel)] bg-[rgb(18_18_22_/_0.72)] shadow-[var(--ds-shadow-panel-dark)] backdrop-blur-[24px]",
114
+ popupClassName
115
+ )}
116
+ >
117
+ <BaseSelect.List className="flex flex-col gap-0.5 p-1">
118
+ {options.map((option) => (
119
+ <BaseSelect.Item
120
+ className="cursor-pointer rounded-[var(--ds-radius-icon)] px-[10px] py-[6px] text-[var(--ds-color-text-secondary)] outline-none transition-[background-color,color] duration-140 ease-[var(--ease-out-cubic)] data-[highlighted]:bg-[var(--ds-color-surface-active)] data-[selected]:bg-[var(--ds-color-surface-active)] data-[highlighted]:text-[var(--ds-color-text-primary)] data-[selected]:text-[var(--ds-color-text-primary)] data-[disabled]:cursor-not-allowed data-[disabled]:text-[var(--ds-color-text-disabled)]"
121
+ disabled={option.disabled}
122
+ key={option.value}
123
+ value={option.value}
124
+ >
125
+ <BaseSelect.ItemText className="block font-[var(--ds-font-mono)] text-[11px] leading-[14px]">
126
+ {option.label}
127
+ </BaseSelect.ItemText>
128
+ </BaseSelect.Item>
129
+ ))}
130
+ </BaseSelect.List>
131
+ </BaseSelect.Popup>
132
+ </BaseSelect.Positioner>
133
+ </BaseSelect.Portal>
134
+ </BaseSelect.Root>
135
+ )
136
+ }