fulldev-ui 0.3.36 → 0.4.0-beta.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 (287) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.yml +57 -0
  2. package/.github/ISSUE_TEMPLATE/config.yml +5 -0
  3. package/.github/ISSUE_TEMPLATE/feature_request.yml +39 -0
  4. package/.github/pull_request_template.md +23 -0
  5. package/.github/workflows/github-releases-to-discord.yml +21 -0
  6. package/.prettierignore +15 -0
  7. package/.prettierrc.yaml +34 -0
  8. package/.vscode/extensions.json +4 -0
  9. package/.vscode/launch.json +11 -0
  10. package/.vscode/settings.json +23 -0
  11. package/CHANGELOG.md +100 -0
  12. package/CONTRIBUTING.md +102 -0
  13. package/LICENCE +21 -0
  14. package/README.md +9 -6
  15. package/astro.config.ts +44 -0
  16. package/package.json +41 -27
  17. package/pnpm-lock.yaml +6229 -0
  18. package/public/favicon/android-chrome-192x192.png +0 -0
  19. package/public/favicon/android-chrome-512x512.png +0 -0
  20. package/public/favicon/apple-touch-icon.png +0 -0
  21. package/public/favicon/browserconfig.xml +9 -0
  22. package/public/favicon/favicon-16x16.png +0 -0
  23. package/public/favicon/favicon-32x32.png +0 -0
  24. package/public/favicon/favicon.ico +0 -0
  25. package/public/favicon/mstile-150x150.png +0 -0
  26. package/public/favicon/safari-pinned-tab.svg +17 -0
  27. package/public/favicon/site.webmanifest +19 -0
  28. package/public/images/avatar-placeholder.jpg +0 -0
  29. package/public/images/avatars/1.jpeg +0 -0
  30. package/public/images/avatars/2.jpeg +0 -0
  31. package/public/images/avatars/3.jpeg +0 -0
  32. package/public/images/avatars/300/1.jpeg +0 -0
  33. package/public/images/avatars/300/2.jpeg +0 -0
  34. package/public/images/avatars/300/3.jpeg +0 -0
  35. package/public/images/avatars/300/4.jpeg +0 -0
  36. package/public/images/avatars/4.jpeg +0 -0
  37. package/public/images/avatars/5.jpeg +0 -0
  38. package/public/images/avatars/500/1.jpeg +0 -0
  39. package/public/images/avatars/500/2.jpeg +0 -0
  40. package/public/images/avatars/500/3.jpeg +0 -0
  41. package/public/images/avatars/500/4.jpeg +0 -0
  42. package/public/images/avatars/6.jpeg +0 -0
  43. package/public/images/avatars/7.jpeg +0 -0
  44. package/public/images/avatars/8.jpeg +0 -0
  45. package/public/images/block-code.png +0 -0
  46. package/public/images/blocks/colleagues.png +0 -0
  47. package/public/images/blocks/content.png +0 -0
  48. package/public/images/blocks/cta.png +0 -0
  49. package/public/images/blocks/features.png +0 -0
  50. package/public/images/blocks/footer.png +0 -0
  51. package/public/images/blocks/header.png +0 -0
  52. package/public/images/blocks/hero.png +0 -0
  53. package/public/images/blocks/intro.png +0 -0
  54. package/public/images/blocks/posts.png +0 -0
  55. package/public/images/blocks/prices.png +0 -0
  56. package/public/images/blocks/reviews.png +0 -0
  57. package/public/images/component-code.png +0 -0
  58. package/public/images/logo-placeholder.svg +1 -0
  59. package/public/images/placeholder-center.webp +0 -0
  60. package/public/images/placeholder-landscape-dark.png +0 -0
  61. package/public/images/placeholder-landscape.webp +0 -0
  62. package/public/images/placeholder-portrait.webp +0 -0
  63. package/public/images/placeholder-right.webp +0 -0
  64. package/public/images/placeholder.webp +0 -0
  65. package/siteicon.png +0 -0
  66. package/src/blocks/Categories.astro +12 -0
  67. package/src/blocks/Colleagues.astro +12 -0
  68. package/src/blocks/Contact.astro +152 -0
  69. package/src/blocks/Content.astro +12 -0
  70. package/src/blocks/Cta.astro +14 -0
  71. package/src/blocks/Features.astro +11 -0
  72. package/src/blocks/Footer.astro +7 -0
  73. package/src/blocks/Header.astro +7 -0
  74. package/src/blocks/Hero.astro +16 -0
  75. package/src/blocks/Intro.astro +19 -0
  76. package/src/blocks/Jobs.astro +12 -0
  77. package/src/blocks/Posts.astro +11 -0
  78. package/src/blocks/Prices.astro +22 -0
  79. package/src/blocks/Product.astro +64 -0
  80. package/src/blocks/Products.astro +11 -0
  81. package/src/blocks/Reviews.astro +13 -0
  82. package/src/components/{base/Avatar.astro → Avatar.astro} +7 -11
  83. package/src/components/{base/Badge.astro → Badge.astro} +36 -22
  84. package/src/components/{structure/Bento.astro → Bento.astro} +6 -9
  85. package/src/components/Block.astro +157 -0
  86. package/src/components/Blocks.astro +5 -0
  87. package/src/components/Box.astro +60 -0
  88. package/src/components/Button.astro +140 -0
  89. package/src/components/Card.astro +212 -0
  90. package/src/components/{structure/Carousel.astro → Carousel.astro} +13 -16
  91. package/src/components/Checkbox.astro +63 -0
  92. package/src/components/{structure/Column.astro → Column.astro} +14 -9
  93. package/src/components/Dialog.astro +65 -0
  94. package/src/components/Drawer.astro +99 -0
  95. package/src/components/Footer.astro +144 -0
  96. package/src/components/{structure/Grid.astro → Grid.astro} +7 -10
  97. package/src/components/Head.astro +85 -0
  98. package/src/components/Header.astro +204 -0
  99. package/src/components/Heading.astro +79 -0
  100. package/src/components/{base/Icon.astro → Icon.astro} +7 -9
  101. package/src/components/{base/Image.astro → Image.astro} +32 -30
  102. package/src/components/Input.astro +119 -0
  103. package/src/components/{typography/Label.astro → Label.astro} +12 -12
  104. package/src/components/{typography/Link.astro → Link.astro} +15 -13
  105. package/src/components/List.astro +41 -0
  106. package/src/components/{structure/Masonry.astro → Masonry.astro} +9 -11
  107. package/src/components/Rating.astro +82 -0
  108. package/src/components/{_Component.astro → Root.astro} +7 -11
  109. package/src/components/{structure/Row.astro → Row.astro} +6 -9
  110. package/src/components/Search.astro +62 -0
  111. package/src/components/Section.astro +274 -0
  112. package/src/components/Select.astro +144 -0
  113. package/src/components/Sidebar.astro +71 -0
  114. package/src/components/{structure/Split.astro → Split.astro} +6 -9
  115. package/src/components/{structure/Spread.astro → Spread.astro} +6 -9
  116. package/src/components/Switch.astro +114 -0
  117. package/src/components/Table.astro +121 -0
  118. package/src/components/Tagline.astro +35 -0
  119. package/src/components/Text.astro +43 -0
  120. package/src/components/Textarea.astro +105 -0
  121. package/src/components/Toc.astro +52 -0
  122. package/src/components/{structure/Wrap.astro → Wrap.astro} +5 -10
  123. package/src/components/docs/DecorativeBox.astro +15 -0
  124. package/src/components/docs/PropsTable.astro +95 -0
  125. package/src/components/experimental/Channel.astro +15 -10
  126. package/src/components/experimental/Form.astro +34 -0
  127. package/src/components/experimental/Gallery.astro +180 -0
  128. package/src/components/experimental/GalleryVertical.astro +161 -0
  129. package/src/components/{base → experimental}/Logo.astro +3 -5
  130. package/src/components/experimental/Price.astro +17 -17
  131. package/src/components/experimental/Prose.astro +154 -0
  132. package/src/components/{base → experimental}/Video.astro +1 -1
  133. package/src/components/experimental/Window.astro +173 -0
  134. package/src/content/config.ts +16 -13
  135. package/src/content/pages/_changelog.mdx +10 -0
  136. package/src/content/pages/_segment/footer.mdx +244 -0
  137. package/src/content/pages/_segment/header.mdx +210 -0
  138. package/src/content/pages/base/_dialog.mdx +17 -0
  139. package/src/content/pages/base/_drawer.mdx +17 -0
  140. package/src/content/pages/base/_image.mdx +14 -0
  141. package/src/content/pages/base/_switch.mdx +33 -0
  142. package/src/content/pages/base/badge.mdx +70 -0
  143. package/src/content/pages/base/button.mdx +71 -0
  144. package/src/content/pages/base/card.mdx +114 -0
  145. package/src/content/pages/base/checkbox.mdx +43 -0
  146. package/src/content/pages/base/icon.mdx +54 -0
  147. package/src/content/pages/base/input.mdx +67 -0
  148. package/src/content/pages/base/rating.mdx +66 -0
  149. package/src/content/pages/base/section.mdx +79 -0
  150. package/src/content/pages/base/select.mdx +77 -0
  151. package/src/content/pages/base/table.mdx +133 -0
  152. package/src/content/pages/base/textarea.mdx +73 -0
  153. package/src/content/pages/blocks/_banner.mdx +13 -0
  154. package/src/content/pages/blocks/_categories.mdx +149 -0
  155. package/src/content/pages/blocks/_jobs.mdx +37 -0
  156. package/src/content/pages/blocks/_product.mdx +29 -0
  157. package/src/content/pages/blocks/_products.mdx +55 -0
  158. package/src/content/pages/blocks/_stats.mdx +34 -0
  159. package/src/content/pages/blocks/colleagues.mdx +42 -0
  160. package/src/content/pages/blocks/content.mdx +101 -0
  161. package/src/content/pages/blocks/cta.mdx +192 -0
  162. package/src/content/pages/blocks/features.mdx +134 -0
  163. package/src/content/pages/blocks/footer.mdx +71 -0
  164. package/src/content/pages/blocks/header.mdx +192 -0
  165. package/src/content/pages/blocks/hero.mdx +97 -0
  166. package/src/content/pages/blocks/intro.mdx +132 -0
  167. package/src/content/pages/blocks/posts.mdx +40 -0
  168. package/src/content/pages/blocks/prices.mdx +126 -0
  169. package/src/content/pages/blocks/reviews.mdx +249 -0
  170. package/src/content/pages/blocks.mdx +10 -0
  171. package/src/content/pages/index.mdx +65 -0
  172. package/src/content/pages/overview/frameworks.mdx +180 -0
  173. package/src/content/pages/overview/installation.mdx +20 -0
  174. package/src/content/pages/overview/integration.mdx +37 -0
  175. package/src/content/pages/overview/introduction.mdx +20 -0
  176. package/src/content/pages/overview/theming.mdx +83 -0
  177. package/src/content/pages/structure/_row.mdx +23 -0
  178. package/src/content/pages/structure/_wrap.mdx +25 -0
  179. package/src/content/pages/structure/carousel.mdx +31 -0
  180. package/src/content/pages/structure/column.mdx +21 -0
  181. package/src/content/pages/structure/grid.mdx +26 -0
  182. package/src/content/pages/structure/masonry.mdx +32 -0
  183. package/src/content/pages/structure/split.mdx +21 -0
  184. package/src/content/pages/structure/spread.mdx +21 -0
  185. package/src/content/pages/typography/_tagline.mdx +36 -0
  186. package/src/content/pages/typography/heading.mdx +74 -0
  187. package/src/content/pages/typography/label.mdx +42 -0
  188. package/src/content/pages/typography/link.mdx +63 -0
  189. package/src/content/pages/typography/list.mdx +52 -0
  190. package/src/content/pages/typography/text.mdx +62 -0
  191. package/src/content/records/record.yml +1 -0
  192. package/src/content/settings/base.yml +48 -0
  193. package/src/content/settings/components.yml +27 -0
  194. package/src/css/color.css +6 -67
  195. package/src/css/custom.css +59 -0
  196. package/src/css/size.css +130 -0
  197. package/src/css/space.css +47 -26
  198. package/src/css/styles.css +4 -2
  199. package/src/env.d.ts +1 -1
  200. package/src/integration/generate-colors.ts +637 -0
  201. package/src/integration/index.ts +158 -0
  202. package/src/integration/tailwind-plugin.ts +80 -0
  203. package/src/integration/unocss-preset.ts +84 -0
  204. package/src/layouts/BlockLayout.astro +24 -0
  205. package/src/layouts/BlockOverviewLayout.astro +39 -0
  206. package/src/layouts/ComponentLayout.astro +23 -0
  207. package/src/layouts/JobsLayout.astro +40 -0
  208. package/src/layouts/Layout.astro +168 -0
  209. package/src/layouts/OverviewLayout.astro +45 -0
  210. package/src/layouts/PageLayout.astro +32 -0
  211. package/src/layouts/PostLayout.astro +32 -0
  212. package/src/layouts/ProductLayout.astro +31 -0
  213. package/src/pages/[...page].astro +41 -0
  214. package/src/schemas/base.ts +81 -0
  215. package/src/schemas/block.ts +15 -0
  216. package/src/schemas/card.ts +3 -0
  217. package/src/schemas/page.ts +43 -0
  218. package/src/schemas/utils.ts +22 -0
  219. package/src/utils/mergeEntries.ts +28 -0
  220. package/tsconfig.json +10 -1
  221. package/.astro/settings.json +0 -5
  222. package/.astro/types.d.ts +0 -147
  223. package/.env +0 -1
  224. package/astro.config.mjs +0 -6
  225. package/src/components/_Block.astro +0 -20
  226. package/src/components/_Blocks.astro +0 -11
  227. package/src/components/_Children.astro +0 -31
  228. package/src/components/_archive/Body.astro +0 -37
  229. package/src/components/_archive/Main.astro +0 -28
  230. package/src/components/_archive/Nav.astro +0 -50
  231. package/src/components/_todo/Form.astro +0 -36
  232. package/src/components/_todo/Prose.astro +0 -139
  233. package/src/components/_todo/Writeup.astro +0 -24
  234. package/src/components/base/Button.astro +0 -123
  235. package/src/components/base/Rating.astro +0 -47
  236. package/src/components/base/checkbox/Checkbox.astro +0 -52
  237. package/src/components/base/checkbox/CheckboxElement.astro +0 -31
  238. package/src/components/base/input/Input.astro +0 -79
  239. package/src/components/base/input/InputElement.astro +0 -69
  240. package/src/components/base/radio/Radio.astro +0 -66
  241. package/src/components/base/radio/RadioElement.astro +0 -34
  242. package/src/components/base/select/Select.astro +0 -90
  243. package/src/components/base/select/SelectElement.astro +0 -56
  244. package/src/components/base/select/SelectOption.astro +0 -32
  245. package/src/components/base/switch/Switch.astro +0 -55
  246. package/src/components/base/switch/SwitchElement.astro +0 -84
  247. package/src/components/base/table/Table.astro +0 -57
  248. package/src/components/base/table/TableBody.astro +0 -38
  249. package/src/components/base/table/TableCell.astro +0 -30
  250. package/src/components/base/table/TableFooter.astro +0 -48
  251. package/src/components/base/table/TableHead.astro +0 -31
  252. package/src/components/base/table/TableHeader.astro +0 -44
  253. package/src/components/base/table/TableRow.astro +0 -46
  254. package/src/components/base/textarea/Textarea.astro +0 -79
  255. package/src/components/base/textarea/TextareaElement.astro +0 -68
  256. package/src/components/segment/Box.astro +0 -46
  257. package/src/components/segment/Card.astro +0 -114
  258. package/src/components/segment/Footer.astro +0 -94
  259. package/src/components/segment/Header.astro +0 -100
  260. package/src/components/segment/Section.astro +0 -103
  261. package/src/components/segment/_regels.md +0 -32
  262. package/src/components/typography/Heading.astro +0 -66
  263. package/src/components/typography/List.astro +0 -47
  264. package/src/components/typography/ListItem.astro +0 -36
  265. package/src/components/typography/Paragraph.astro +0 -36
  266. package/src/components/typography/Tagline.astro +0 -36
  267. package/src/components/typography/Text.astro +0 -37
  268. package/src/components/utility/Head.astro +0 -83
  269. package/src/components/utility/Layout.astro +0 -46
  270. package/src/content/pages/index.md +0 -2
  271. package/src/css/color.scss +0 -289
  272. package/src/css/size.scss +0 -296
  273. package/src/css/utils.scss +0 -16
  274. package/src/index.ts +0 -71
  275. package/src/integration/integration.ts +0 -88
  276. package/src/integration/tailwind.ts +0 -34
  277. package/src/integration/unocss.ts +0 -38
  278. /package/src/{_archive → utils/_archive}/Childmorphic.ts +0 -0
  279. /package/src/{_archive → utils/_archive}/getAllCollections.ts +0 -0
  280. /package/src/{_archive → utils/_archive}/getCollectionCascade.ts +0 -0
  281. /package/src/{_archive → utils/_archive}/getPathname.ts +0 -0
  282. /package/src/{_archive → utils/_archive}/staticPageSchema.ts +0 -0
  283. /package/src/{_archive → utils/_archive}/transform/transform.ts +0 -0
  284. /package/src/{_archive → utils/_archive}/transform/transformCasing.ts +0 -0
  285. /package/src/{_archive → utils/_archive}/transform/transformReferences.ts +0 -0
  286. /package/src/{_archive → utils/_archive}/transform/transformSelfs.ts +0 -0
  287. /package/src/{_archive → utils/_archive}/transform/transformUnderscores.ts +0 -0
@@ -0,0 +1,637 @@
1
+ import * as RadixColors from '@radix-ui/colors'
2
+ import BezierEasing from 'bezier-easing'
3
+ import Color from 'colorjs.io'
4
+
5
+ type ArrayOf12<T> = [T, T, T, T, T, T, T, T, T, T, T, T]
6
+ const arrayOf12 = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] as const
7
+
8
+ // prettier-ignore
9
+ const grayScaleNames = ['gray', 'mauve', 'slate', 'sage', 'olive', 'sand'] as const;
10
+
11
+ // prettier-ignore
12
+ const scaleNames = [...grayScaleNames, 'tomato', 'red', 'ruby', 'crimson', 'pink',
13
+ 'plum', 'purple', 'violet', 'iris', 'indigo', 'blue', 'cyan', 'teal', 'jade', 'green',
14
+ 'grass', 'brown', 'orange', 'sky', 'mint', 'lime', 'yellow', 'amber'] as const;
15
+
16
+ const lightColors = Object.fromEntries(
17
+ scaleNames.map((scaleName) => [
18
+ scaleName,
19
+ Object.values(RadixColors[`${scaleName}P3`]).map((str) =>
20
+ new Color(str).to('oklch')
21
+ ),
22
+ ])
23
+ ) as Record<(typeof scaleNames)[number], ArrayOf12<Color>>
24
+
25
+ const darkColors = Object.fromEntries(
26
+ scaleNames.map((scaleName) => [
27
+ scaleName,
28
+ Object.values(RadixColors[`${scaleName}DarkP3`]).map((str) =>
29
+ new Color(str).to('oklch')
30
+ ),
31
+ ])
32
+ ) as Record<(typeof scaleNames)[number], ArrayOf12<Color>>
33
+
34
+ const lightGrayColors = Object.fromEntries(
35
+ grayScaleNames.map((scaleName) => [
36
+ scaleName,
37
+ Object.values(RadixColors[`${scaleName}P3`]).map((str) =>
38
+ new Color(str).to('oklch')
39
+ ),
40
+ ])
41
+ ) as Record<(typeof grayScaleNames)[number], ArrayOf12<Color>>
42
+
43
+ const darkGrayColors = Object.fromEntries(
44
+ grayScaleNames.map((scaleName) => [
45
+ scaleName,
46
+ Object.values(RadixColors[`${scaleName}DarkP3`]).map((str) =>
47
+ new Color(str).to('oklch')
48
+ ),
49
+ ])
50
+ ) as Record<(typeof grayScaleNames)[number], ArrayOf12<Color>>
51
+
52
+ export const generateRadixColors = ({
53
+ appearance,
54
+ ...args
55
+ }: {
56
+ appearance: 'light' | 'dark'
57
+ accent: string
58
+ gray: string
59
+ background: string
60
+ }) => {
61
+ const allScales = appearance === 'light' ? lightColors : darkColors
62
+ const grayScales = appearance === 'light' ? lightGrayColors : darkGrayColors
63
+ const backgroundColor = new Color(args.background).to('oklch')
64
+
65
+ const grayBaseColor = new Color(args.gray).to('oklch')
66
+ const grayScaleColors = getScaleFromColor(
67
+ grayBaseColor,
68
+ grayScales,
69
+ backgroundColor
70
+ )
71
+
72
+ const accentBaseColor = new Color(args.accent).to('oklch')
73
+
74
+ let accentScaleColors = getScaleFromColor(
75
+ accentBaseColor,
76
+ allScales,
77
+ backgroundColor
78
+ )
79
+
80
+ // Enforce srgb for the background color
81
+ const backgroundHex = backgroundColor.to('srgb').toString({ format: 'hex' })
82
+
83
+ // Make sure we use the tint from the gray scale for when base is pure white or black
84
+ const accentBaseHex = accentBaseColor.to('srgb').toString({ format: 'hex' })
85
+ if (accentBaseHex === '#000' || accentBaseHex === '#fff') {
86
+ accentScaleColors = grayScaleColors.map((color) =>
87
+ color.clone()
88
+ ) as ArrayOf12<Color>
89
+ }
90
+
91
+ const [accent9Color, accentContrastColor] = getStep9Colors(
92
+ accentScaleColors,
93
+ accentBaseColor
94
+ )
95
+
96
+ accentScaleColors[8] = accent9Color
97
+ accentScaleColors[9] = getButtonHoverColor(accent9Color, [accentScaleColors])
98
+
99
+ // Limit saturation of the text colors
100
+ accentScaleColors[10].coords[1] = Math.min(
101
+ Math.max(accentScaleColors[8].coords[1], accentScaleColors[7].coords[1]),
102
+ accentScaleColors[10].coords[1]
103
+ )
104
+ accentScaleColors[11].coords[1] = Math.min(
105
+ Math.max(accentScaleColors[8].coords[1], accentScaleColors[7].coords[1]),
106
+ accentScaleColors[11].coords[1]
107
+ )
108
+
109
+ const accentScaleHex = accentScaleColors.map((color) =>
110
+ color.to('srgb').toString({ format: 'hex' })
111
+ ) as ArrayOf12<string>
112
+
113
+ const accentScaleWideGamut = accentScaleColors.map(
114
+ toOklchString
115
+ ) as ArrayOf12<string>
116
+
117
+ const accentScaleAlphaHex = accentScaleHex.map((color) =>
118
+ getAlphaColorSrgb(color, backgroundHex)
119
+ ) as ArrayOf12<string>
120
+
121
+ const accentScaleAlphaWideGamutString = accentScaleHex.map((color) =>
122
+ getAlphaColorP3(color, backgroundHex)
123
+ ) as ArrayOf12<string>
124
+
125
+ const accentContrastColorHex = accentContrastColor
126
+ .to('srgb')
127
+ .toString({ format: 'hex' })
128
+
129
+ const grayScaleHex = grayScaleColors.map((color) =>
130
+ color.to('srgb').toString({ format: 'hex' })
131
+ ) as ArrayOf12<string>
132
+
133
+ const grayScaleWideGamut = grayScaleColors.map(
134
+ toOklchString
135
+ ) as ArrayOf12<string>
136
+
137
+ const grayScaleAlphaHex = grayScaleHex.map((color) =>
138
+ getAlphaColorSrgb(color, backgroundHex)
139
+ ) as ArrayOf12<string>
140
+
141
+ const grayScaleAlphaWideGamutString = grayScaleHex.map((color) =>
142
+ getAlphaColorP3(color, backgroundHex)
143
+ ) as ArrayOf12<string>
144
+
145
+ const accentSurfaceHex =
146
+ appearance === 'light'
147
+ ? getAlphaColorSrgb(accentScaleHex[1], backgroundHex, 0.8)
148
+ : getAlphaColorSrgb(accentScaleHex[1], backgroundHex, 0.5)
149
+
150
+ const accentSurfaceWideGamutString =
151
+ appearance === 'light'
152
+ ? getAlphaColorP3(accentScaleWideGamut[1], backgroundHex, 0.8)
153
+ : getAlphaColorP3(accentScaleWideGamut[1], backgroundHex, 0.5)
154
+
155
+ return {
156
+ accentScale: accentScaleHex,
157
+ accentScaleAlpha: accentScaleAlphaHex,
158
+ accentScaleWideGamut: accentScaleWideGamut,
159
+ accentScaleAlphaWideGamut: accentScaleAlphaWideGamutString,
160
+ accentContrast: accentContrastColorHex,
161
+
162
+ grayScale: grayScaleHex,
163
+ grayScaleAlpha: grayScaleAlphaHex,
164
+ grayScaleWideGamut: grayScaleWideGamut,
165
+ grayScaleAlphaWideGamut: grayScaleAlphaWideGamutString,
166
+
167
+ graySurface: appearance === 'light' ? '#ffffffcc' : 'rgba(0, 0, 0, 0.05)',
168
+ graySurfaceWideGamut:
169
+ appearance === 'light'
170
+ ? 'color(display-p3 1 1 1 / 80%)'
171
+ : 'color(display-p3 0 0 0 / 5%)',
172
+
173
+ accentSurface: accentSurfaceHex,
174
+ accentSurfaceWideGamut: accentSurfaceWideGamutString,
175
+
176
+ background: backgroundHex,
177
+ }
178
+ }
179
+
180
+ function getStep9Colors(
181
+ scale: ArrayOf12<Color>,
182
+ accentBaseColor: Color
183
+ ): [Color, Color] {
184
+ const referenceBackgroundColor = scale[0]
185
+ const distance = accentBaseColor.deltaEOK(referenceBackgroundColor) * 100
186
+
187
+ // If the accent base color is close to the page background color, it's likely
188
+ // white on white or black on black, so we want to return something that makes sense instead
189
+ if (distance < 25) {
190
+ return [scale[8], getTextColor(scale[8])]
191
+ }
192
+
193
+ return [accentBaseColor, getTextColor(accentBaseColor)]
194
+ }
195
+
196
+ function getButtonHoverColor(source: Color, scales: ArrayOf12<Color>[]) {
197
+ const [L, C, H] = source.coords
198
+ const newL = L > 0.4 ? L - 0.03 / (L + 0.1) : L + 0.03 / (L + 0.1)
199
+ const newC = L > 0.4 && !isNaN(H) ? C * 0.93 + 0 : C
200
+ const buttonHoverColor = new Color('oklch', [newL, newC, H])
201
+
202
+ // Find closest in-scale color to donate the chroma and hue.
203
+ // Especially useful when the source color is pure white or black,
204
+ // but the gray scale is tinted.
205
+ let closestColor = buttonHoverColor
206
+ let minDistance = Infinity
207
+
208
+ scales.forEach((scale) => {
209
+ for (const color of scale) {
210
+ const distance = buttonHoverColor.deltaEOK(color)
211
+ if (distance < minDistance) {
212
+ minDistance = distance
213
+ closestColor = color
214
+ }
215
+ }
216
+ })
217
+
218
+ buttonHoverColor.coords[1] = closestColor.coords[1]
219
+ buttonHoverColor.coords[2] = closestColor.coords[2]
220
+ return buttonHoverColor
221
+ }
222
+
223
+ function getScaleFromColor(
224
+ source: Color,
225
+ scales: Record<string, ArrayOf12<Color>>,
226
+ backgroundColor: Color
227
+ ) {
228
+ let allColors: { scale: string; color: Color; distance: number }[] = []
229
+
230
+ Object.entries(scales).forEach(([name, scale]) => {
231
+ for (const color of scale) {
232
+ const distance = source.deltaEOK(color)
233
+ allColors.push({ scale: name, distance, color })
234
+ }
235
+ })
236
+
237
+ allColors.sort((a, b) => a.distance - b.distance)
238
+
239
+ // Remove non-unique scales
240
+ let closestColors = allColors.filter(
241
+ (color, i, arr) =>
242
+ i === arr.findIndex((value) => value.scale === color.scale)
243
+ )
244
+
245
+ // If the next two closest colors are both grays, remove the second one until it’s not a gray anymore.
246
+ // This is because up next we will be comparing how close the two closest colors are to the source color,
247
+ // and since the grays are all extremely close to each other, we won’t get any useful data from the second
248
+ // closest color if it’s also a gray.
249
+ const grayScaleNamesStr = grayScaleNames as readonly string[]
250
+ const allAreGrays = closestColors.every((color) =>
251
+ grayScaleNamesStr.includes(color.scale)
252
+ )
253
+ if (!allAreGrays && grayScaleNamesStr.includes(closestColors[0].scale)) {
254
+ while (grayScaleNamesStr.includes(closestColors[1].scale)) {
255
+ closestColors.splice(1, 1)
256
+ }
257
+ }
258
+
259
+ let colorA = closestColors[0]
260
+ let colorB = closestColors[1]
261
+
262
+ // Light trigonometry ahead.
263
+ //
264
+ // We want to determine the color that is the closest to the source color. Sometimes it makes sense
265
+ // to proportionally mix the two closest colors together, but sometimes it is not useful at all.
266
+ // Color coords are spatial in 3D, however we can treat the data we have as a 2D projection that is good enough.
267
+ //
268
+ // Case 1:
269
+ // If the distances between the source color, the 1st closest color (A) and the 2nd closest color (B) form
270
+ // a triangle where NEITHER angle A nor B are larger than 90 degrees, then we want to mix the 1st and the 2nd
271
+ // closest colors in the same proportion as distances AD and BD are to each other. Mixing the two would result
272
+ // in a color that would be closer to the source color than either of the two original closest colors.
273
+ // Example: source color is a desaturated blue, which is between "indigo" and "slate" scales.
274
+ //
275
+ // C ← Source color
276
+ // /|⟍
277
+ // / | ⟍
278
+ // b / | ⟍ a
279
+ // / | ⟍
280
+ // / | ⟍
281
+ // A --- D -------- B
282
+ // ↑
283
+ // The color we want to use as the base, which is a mix of A and B.
284
+ //
285
+ // Case 2:
286
+ // If the distances between the source color, the 1st closest color (A) and the 2nd closest color (B) form
287
+ // a triangle where EITHER angle A or B are larger than 90 degrees, then we don’t care about point B because it’s
288
+ // directionally the same as A, as mixing A and B can’t provide us with a color that is any closer to the source.
289
+ // Example: source color is a saturated blue, with "blue" being the closest scale, and "indigo" just being further.
290
+ //
291
+ // C ← Source color
292
+ // \⟍
293
+ // \ ⟍
294
+ // \ ⟍ a
295
+ // b \ ⟍
296
+ // \ ⟍
297
+ // A ------- B
298
+ // ↑
299
+ // The color we want to use as the base, which is not influenced by B.
300
+
301
+ // We’ll need all the lengths of the triangle sides, named after the angles they look at:
302
+ const a = colorB.distance
303
+ const b = colorA.distance
304
+ const c = colorA.color.deltaEOK(colorB.color)
305
+
306
+ // We can get the ratios of AD to BD lengths with trigonometry using tangents,
307
+ // as the ratio of the tangents of the opposite angles will match.
308
+ const cosA = (b ** 2 + c ** 2 - a ** 2) / (2 * b * c)
309
+ const radA = Math.acos(cosA)
310
+ const sinA = Math.sin(radA)
311
+
312
+ const cosB = (a ** 2 + c ** 2 - b ** 2) / (2 * a * c)
313
+ const radB = Math.acos(cosB)
314
+ const sinB = Math.sin(radB)
315
+
316
+ // Tangent of angle C in the ACD triangle
317
+ const tanC1 = cosA / sinA
318
+
319
+ // Tangent of angle C in the BCD triangle
320
+ const tanC2 = cosB / sinB
321
+
322
+ // The ratio of the tangents corresponds to the ratio of the distances AD to BD
323
+ // In the end, it means how much of scale B we want to mix into scale A.
324
+ // If it’s "0" or less, this is an obtuse triangle from case 2, and we use just scale A.
325
+ const ratio = Math.max(0, tanC1 / tanC2) * 0.5
326
+
327
+ // The base scale is going to be a mix of the two closest scales, with the mix ratio we determined before
328
+ const scaleA = scales[colorA.scale]
329
+ const scaleB = scales[colorB.scale]
330
+ const scale = arrayOf12.map((i) =>
331
+ new Color(Color.mix(scaleA[i], scaleB[i], ratio)).to('oklch')
332
+ ) as ArrayOf12<Color>
333
+
334
+ // Get the closest color from the pre-mixed scale we created
335
+ const baseColor = scale
336
+ .slice()
337
+ .sort((a, b) => source.deltaEOK(a) - source.deltaEOK(b))[0]
338
+
339
+ // Note the chroma difference between the source color and the base color
340
+ const ratioC = source.coords[1] / baseColor.coords[1]
341
+
342
+ // Modify hue and chroma of the scale to match the source color
343
+ scale.forEach((color) => {
344
+ color.coords[1] = Math.min(source.coords[1] * 1.5, color.coords[1] * ratioC)
345
+ color.coords[2] = source.coords[2]
346
+ })
347
+
348
+ // Light mode
349
+ if (scale[0].coords[0] > 0.5) {
350
+ const lightnessScale = scale.map(({ coords }) => coords[0])
351
+ const backgroundL = Math.max(0, Math.min(1, backgroundColor.coords[0]))
352
+ const newLightnessScale = transposeProgressionStart(
353
+ backgroundL,
354
+ // Add white as the first "step" of the light scale
355
+ [1, ...lightnessScale],
356
+ lightModeEasing
357
+ )
358
+
359
+ // Remove the step we added
360
+ newLightnessScale.shift()
361
+
362
+ newLightnessScale.forEach((lightness, i) => {
363
+ scale[i].coords[0] = lightness
364
+ })
365
+
366
+ return scale
367
+ }
368
+
369
+ // Dark mode
370
+ let ease: typeof darkModeEasing = [...darkModeEasing]
371
+ const referenceBackgroundColorL = scale[0].coords[0]
372
+ const backgroundColorL = Math.max(0, Math.min(1, backgroundColor.coords[0]))
373
+
374
+ // If background is lighter than step 0, we want to gradually change the easing to linear
375
+ const ratioL = backgroundColorL / referenceBackgroundColorL
376
+
377
+ if (ratioL > 1) {
378
+ const maxRatio = 1.5
379
+
380
+ for (let i = 0; i < ease.length; i++) {
381
+ const metaRatio = (ratioL - 1) * (maxRatio / (maxRatio - 1))
382
+ ease[i] = ratioL > maxRatio ? 0 : Math.max(0, ease[i] * (1 - metaRatio))
383
+ }
384
+ }
385
+
386
+ const lightnessScale = scale.map(({ coords }) => coords[0])
387
+ const backgroundL = backgroundColor.coords[0]
388
+ const newLightnessScale = transposeProgressionStart(
389
+ backgroundL,
390
+ lightnessScale,
391
+ ease
392
+ )
393
+
394
+ newLightnessScale.forEach((lightness, i) => {
395
+ scale[i].coords[0] = lightness
396
+ })
397
+
398
+ return scale
399
+ }
400
+
401
+ function getTextColor(background: Color) {
402
+ const white = new Color('oklch', [1, 0, 0])
403
+
404
+ if (Math.abs(white.contrastAPCA(background)) < 40) {
405
+ const [C, H] = background.coords
406
+ return new Color('oklch', [0.25, Math.max(0.08 * C, 0.04), H])
407
+ }
408
+
409
+ return white
410
+ }
411
+
412
+ // target = background * (1 - alpha) + foreground * alpha
413
+ // alpha = (target - background) / (foreground - background)
414
+ // Expects 0-1 numbers for the RGB channels
415
+ function getAlphaColor(
416
+ targetRgb: number[],
417
+ backgroundRgb: number[],
418
+ rgbPrecision: number,
419
+ alphaPrecision: number,
420
+ targetAlpha?: number
421
+ ) {
422
+ const [tr, tg, tb] = targetRgb.map((c) => Math.round(c * rgbPrecision))
423
+ const [br, bg, bb] = backgroundRgb.map((c) => Math.round(c * rgbPrecision))
424
+
425
+ if (
426
+ tr === undefined ||
427
+ tg === undefined ||
428
+ tb === undefined ||
429
+ br === undefined ||
430
+ bg === undefined ||
431
+ bb === undefined
432
+ ) {
433
+ throw Error('Color is undefined')
434
+ }
435
+
436
+ // Is the background color lighter, RGB-wise, than target color?
437
+ // Decide whether we want to add as little color or as much color as possible,
438
+ // darkening or lightening the background respectively.
439
+ // If at least one of the bits of the target RGB value
440
+ // is lighter than the background, we want to lighten it.
441
+ let desiredRgb = 0
442
+ if (tr > br) {
443
+ desiredRgb = rgbPrecision
444
+ } else if (tg > bg) {
445
+ desiredRgb = rgbPrecision
446
+ } else if (tb > bb) {
447
+ desiredRgb = rgbPrecision
448
+ }
449
+
450
+ const alphaR = (tr - br) / (desiredRgb - br)
451
+ const alphaG = (tg - bg) / (desiredRgb - bg)
452
+ const alphaB = (tb - bb) / (desiredRgb - bb)
453
+
454
+ const isPureGray = [alphaR, alphaG, alphaB].every((alpha) => alpha === alphaR)
455
+
456
+ // No need for precision gymnastics with pure grays, and we can get cleaner output
457
+ if (!targetAlpha && isPureGray) {
458
+ // Convert back to 0-1 values
459
+ const V = desiredRgb / rgbPrecision
460
+ return [V, V, V, alphaR] as const
461
+ }
462
+
463
+ const clampRgb = (n: number) =>
464
+ isNaN(n) ? 0 : Math.min(rgbPrecision, Math.max(0, n))
465
+ const clampA = (n: number) =>
466
+ isNaN(n) ? 0 : Math.min(alphaPrecision, Math.max(0, n))
467
+ const maxAlpha = targetAlpha ?? Math.max(alphaR, alphaG, alphaB)
468
+
469
+ const A = clampA(Math.ceil(maxAlpha * alphaPrecision)) / alphaPrecision
470
+ let R = clampRgb(((br * (1 - A) - tr) / A) * -1)
471
+ let G = clampRgb(((bg * (1 - A) - tg) / A) * -1)
472
+ let B = clampRgb(((bb * (1 - A) - tb) / A) * -1)
473
+
474
+ R = Math.ceil(R)
475
+ G = Math.ceil(G)
476
+ B = Math.ceil(B)
477
+
478
+ const blendedR = blendAlpha(R, A, br)
479
+ const blendedG = blendAlpha(G, A, bg)
480
+ const blendedB = blendAlpha(B, A, bb)
481
+
482
+ // Correct for rounding errors in light mode
483
+ if (desiredRgb === 0) {
484
+ if (tr <= br && tr !== blendedR) {
485
+ R = tr > blendedR ? R + 1 : R - 1
486
+ }
487
+
488
+ if (tg <= bg && tg !== blendedG) {
489
+ G = tg > blendedG ? G + 1 : G - 1
490
+ }
491
+
492
+ if (tb <= bb && tb !== blendedB) {
493
+ B = tb > blendedB ? B + 1 : B - 1
494
+ }
495
+ }
496
+
497
+ // Correct for rounding errors in dark mode
498
+ if (desiredRgb === rgbPrecision) {
499
+ if (tr >= br && tr !== blendedR) {
500
+ R = tr > blendedR ? R + 1 : R - 1
501
+ }
502
+
503
+ if (tg >= bg && tg !== blendedG) {
504
+ G = tg > blendedG ? G + 1 : G - 1
505
+ }
506
+
507
+ if (tb >= bb && tb !== blendedB) {
508
+ B = tb > blendedB ? B + 1 : B - 1
509
+ }
510
+ }
511
+
512
+ // Convert back to 0-1 values
513
+ R = R / rgbPrecision
514
+ G = G / rgbPrecision
515
+ B = B / rgbPrecision
516
+
517
+ return [R, G, B, A] as const
518
+ }
519
+
520
+ // Important – I empirically discovered that this rounding is how the browser actually overlays
521
+ // transparent RGB bits over each other. It does NOT round the whole result altogether.
522
+ function blendAlpha(
523
+ foreground: number,
524
+ alpha: number,
525
+ background: number,
526
+ round = true
527
+ ) {
528
+ if (round) {
529
+ return Math.round(background * (1 - alpha)) + Math.round(foreground * alpha)
530
+ }
531
+
532
+ return background * (1 - alpha) + foreground * alpha
533
+ }
534
+
535
+ function getAlphaColorSrgb(
536
+ targetColor: string,
537
+ backgroundColor: string,
538
+ targetAlpha?: number
539
+ ) {
540
+ const [r, g, b, a] = getAlphaColor(
541
+ new Color(targetColor).to('srgb').coords,
542
+ new Color(backgroundColor).to('srgb').coords,
543
+ 255,
544
+ 255,
545
+ targetAlpha
546
+ )
547
+
548
+ return formatHex(new Color('srgb', [r, g, b], a).toString({ format: 'hex' }))
549
+ }
550
+
551
+ function getAlphaColorP3(
552
+ targetColor: string,
553
+ backgroundColor: string,
554
+ targetAlpha?: number
555
+ ) {
556
+ const [r, g, b, a] = getAlphaColor(
557
+ new Color(targetColor).to('p3').coords,
558
+ new Color(backgroundColor).to('p3').coords,
559
+ // Not sure why, but the resulting P3 alpha colors are blended in the browser most precisely when
560
+ // rounded to 255 integers too. Is the browser using 0-255 rather than 0-1 under the hood for P3 too?
561
+ 255,
562
+ 1000,
563
+ targetAlpha
564
+ )
565
+
566
+ return (
567
+ new Color('p3', [r, g, b], a)
568
+ .toString({ precision: 4 })
569
+ // Important: in non-browser environments colorjs.io outputs a different format for some reason
570
+ .replace('color(p3 ', 'color(display-p3 ')
571
+ )
572
+ }
573
+
574
+ // Format shortform hex to longform
575
+ function formatHex(str: string) {
576
+ if (!str.startsWith('#')) {
577
+ return str
578
+ }
579
+
580
+ if (str.length === 4) {
581
+ const hash = str.charAt(0)
582
+ const r = str.charAt(1)
583
+ const g = str.charAt(2)
584
+ const b = str.charAt(3)
585
+ return hash + r + r + g + g + b + b
586
+ }
587
+
588
+ if (str.length === 5) {
589
+ const hash = str.charAt(0)
590
+ const r = str.charAt(1)
591
+ const g = str.charAt(2)
592
+ const b = str.charAt(3)
593
+ const a = str.charAt(4)
594
+ return hash + r + r + g + g + b + b + a + a
595
+ }
596
+
597
+ return str
598
+ }
599
+
600
+ const darkModeEasing = [1, 0, 1, 0] as [number, number, number, number]
601
+ const lightModeEasing = [0, 2, 0, 2] as [number, number, number, number]
602
+
603
+ export function transposeProgressionStart(
604
+ to: number,
605
+ arr: number[],
606
+ curve: [number, number, number, number]
607
+ ) {
608
+ return arr.map((n, i, arr) => {
609
+ const lastIndex = arr.length - 1
610
+ const diff = arr[0] - to
611
+ const fn = BezierEasing(...curve)
612
+ return n - diff * fn(1 - i / lastIndex)
613
+ })
614
+ }
615
+
616
+ export function transposeProgressionEnd(
617
+ to: number,
618
+ arr: number[],
619
+ curve: [number, number, number, number]
620
+ ) {
621
+ return arr.map((n, i, arr) => {
622
+ const lastIndex = arr.length - 1
623
+ const diff = arr[lastIndex] - to
624
+ const fn = BezierEasing(...curve)
625
+ return n - diff * fn(i / lastIndex)
626
+ })
627
+ }
628
+
629
+ // Convert to OKLCH string with percentage for the lightness channel
630
+ // https://github.com/radix-ui/themes/issues/420
631
+ function toOklchString(color: Color) {
632
+ const L = +(color.coords[0] * 100).toFixed(1)
633
+ return color
634
+ .to('oklch')
635
+ .toString({ precision: 4 })
636
+ .replace(/(\S+)(.+)/, `oklch(${L}%$2`)
637
+ }