codex-terminal-themes 1.0.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 (229) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/CONTRIBUTING.md +23 -0
  3. package/LICENSE +21 -0
  4. package/LICENSE.txt +21 -0
  5. package/README.md +169 -0
  6. package/SECURITY.md +5 -0
  7. package/SUPPORT.md +5 -0
  8. package/bin/codex-terminal-themes.mjs +12 -0
  9. package/docs/app.js +1393 -0
  10. package/docs/assets/theme-gallery-header.png +0 -0
  11. package/docs/favicon.svg +10 -0
  12. package/docs/index.html +240 -0
  13. package/docs/preview.svg +24 -0
  14. package/docs/site-data.json +51078 -0
  15. package/docs/styles.css +780 -0
  16. package/metadata/README.md +71 -0
  17. package/metadata/themes.json +16185 -0
  18. package/metadata/themes.schema.json +218 -0
  19. package/package.json +109 -0
  20. package/src/cli.mjs +1763 -0
  21. package/stylelint.config.mjs +12 -0
  22. package/themes/80s.tmTheme +229 -0
  23. package/themes/Active4D.tmTheme +407 -0
  24. package/themes/All Hallow's Eve Custom.tmTheme +275 -0
  25. package/themes/All Hallow's Eve.tmTheme +277 -0
  26. package/themes/All Hallows Eve.tmTheme +277 -0
  27. package/themes/Amy.tmTheme +559 -0
  28. package/themes/Artic Fall.tmTheme +275 -0
  29. package/themes/BBEdit.tmTheme +439 -0
  30. package/themes/Bespin.tmTheme +516 -0
  31. package/themes/Black Pearl II.tmTheme +496 -0
  32. package/themes/Black Pearl.tmTheme +400 -0
  33. package/themes/Blackboard 2.tmTheme +348 -0
  34. package/themes/Blackboard Black.tmTheme +350 -0
  35. package/themes/Blackboard.tmTheme +348 -0
  36. package/themes/Blueberry Jelly.tmTheme +242 -0
  37. package/themes/Bluesy.tmTheme +242 -0
  38. package/themes/Blurb 2.tmTheme +229 -0
  39. package/themes/Blurb.tmTheme +229 -0
  40. package/themes/Bongzilla 2.tmTheme +223 -0
  41. package/themes/Bongzilla.tmTheme +223 -0
  42. package/themes/Brightly Dark.tmTheme +242 -0
  43. package/themes/Brilliance Black.tmTheme +2619 -0
  44. package/themes/Brilliance Dull.tmTheme +2243 -0
  45. package/themes/Bromozoid.tmTheme +227 -0
  46. package/themes/CSSEdit.tmTheme +203 -0
  47. package/themes/Classic Modified.tmTheme +469 -0
  48. package/themes/Clouds Midnight.tmTheme +361 -0
  49. package/themes/Clouds of Ruby.tmTheme +649 -0
  50. package/themes/Clouds.tmTheme +348 -0
  51. package/themes/Cloudy Fields.tmTheme +240 -0
  52. package/themes/Coal Graal.tmTheme +282 -0
  53. package/themes/Cobalt.tmTheme +561 -0
  54. package/themes/Coda.tmTheme +317 -0
  55. package/themes/Colorful.tmTheme +307 -0
  56. package/themes/Cool Glow.tmTheme +234 -0
  57. package/themes/Corona.tmTheme +290 -0
  58. package/themes/Cowabunga.tmTheme +242 -0
  59. package/themes/DanBurst.tmTheme +665 -0
  60. package/themes/Daniel Fischer.tmTheme +627 -0
  61. package/themes/Dark Ocean.tmTheme +242 -0
  62. package/themes/DarkNeon.tmTheme +818 -0
  63. package/themes/Dawn.tmTheme +437 -0
  64. package/themes/DawnCustom.tmTheme +443 -0
  65. package/themes/Django (Smoothy).tmTheme +453 -0
  66. package/themes/Django Blues.tmTheme +182 -0
  67. package/themes/Django Extended.tmTheme +495 -0
  68. package/themes/Django.tmTheme +436 -0
  69. package/themes/Dobdark.tmTheme +615 -0
  70. package/themes/Dominion Day.tmTheme +562 -0
  71. package/themes/Doo-Daa.tmTheme +242 -0
  72. package/themes/Drankin Purp.tmTheme +227 -0
  73. package/themes/Dreamweaver_Blackbam_Aptana303.tmTheme +980 -0
  74. package/themes/Easy on my Eyes There Buddy.tmTheme +227 -0
  75. package/themes/Eiffel.tmTheme +435 -0
  76. package/themes/Emacs Strict.tmTheme +241 -0
  77. package/themes/Emacs.tmTheme +241 -0
  78. package/themes/Epic Blue.tmTheme +320 -0
  79. package/themes/Erebus.tmTheme +467 -0
  80. package/themes/Espresso Libre.tmTheme +402 -0
  81. package/themes/Espresso Tutti.tmTheme +392 -0
  82. package/themes/Espresso.tmTheme +329 -0
  83. package/themes/Fade to Grey.tmTheme +308 -0
  84. package/themes/Fluidvision.tmTheme +443 -0
  85. package/themes/ForLaTeX.tmTheme +214 -0
  86. package/themes/Freckle.tmTheme +279 -0
  87. package/themes/Friendship Bracelet.tmTheme +303 -0
  88. package/themes/GaGaGaGroovy.tmTheme +227 -0
  89. package/themes/Gangrene.tmTheme +242 -0
  90. package/themes/GitHub.tmTheme +653 -0
  91. package/themes/GlitterBomb.tmTheme +387 -0
  92. package/themes/Halloween Night.tmTheme +303 -0
  93. package/themes/Happy happy joy joy.tmTheme +841 -0
  94. package/themes/Happydeluxe.tmTheme +184 -0
  95. package/themes/HelvectorLight.tmTheme +557 -0
  96. package/themes/Humane.tmTheme +220 -0
  97. package/themes/IDLE.tmTheme +235 -0
  98. package/themes/IR_Black.tmTheme +810 -0
  99. package/themes/IR_White.tmTheme +792 -0
  100. package/themes/Jane Fonda, Baby Redux.tmTheme +264 -0
  101. package/themes/Johnny.tmTheme +798 -0
  102. package/themes/Juicy.tmTheme +250 -0
  103. package/themes/Kuroir Theme.tmTheme +707 -0
  104. package/themes/LAZY.tmTheme +291 -0
  105. package/themes/Lowlight.tmTheme +605 -0
  106. package/themes/Mac Classic.tmTheme +476 -0
  107. package/themes/Made of Code.tmTheme +695 -0
  108. package/themes/MagicWB (Amiga).tmTheme +376 -0
  109. package/themes/Malibu Nights.tmTheme +257 -0
  110. package/themes/Menage A Trois.tmTheme +881 -0
  111. package/themes/Merbivore Soft.tmTheme +285 -0
  112. package/themes/Merbivore.tmTheme +285 -0
  113. package/themes/Midnight.tmTheme +321 -0
  114. package/themes/Mmm Sandy.tmTheme +227 -0
  115. package/themes/Monokai.tmTheme +289 -0
  116. package/themes/MultiMarkdown.tmTheme +183 -0
  117. package/themes/Mustang.tmTheme +339 -0
  118. package/themes/Neopro Inverted.tmTheme +328 -0
  119. package/themes/Neopro.tmTheme +330 -0
  120. package/themes/Nice One.tmTheme +222 -0
  121. package/themes/No Way.tmTheme +255 -0
  122. package/themes/Overcast.tmTheme +659 -0
  123. package/themes/Pastels on Dark.tmTheme +703 -0
  124. package/themes/Pastie.tmTheme +321 -0
  125. package/themes/Peridinkle.tmTheme +240 -0
  126. package/themes/Play!.tmTheme +736 -0
  127. package/themes/Puss.tmTheme +227 -0
  128. package/themes/Putty.tmTheme +275 -0
  129. package/themes/Quail.tmTheme +257 -0
  130. package/themes/RDark.tmTheme +235 -0
  131. package/themes/Rails Envy.tmTheme +299 -0
  132. package/themes/Railscasts 2.tmTheme +368 -0
  133. package/themes/Railscasts.tmTheme +278 -0
  134. package/themes/Resesif.tmTheme +298 -0
  135. package/themes/Ringo.tmTheme +240 -0
  136. package/themes/Ruby Blue.tmTheme +366 -0
  137. package/themes/RubyRobot.tmTheme +250 -0
  138. package/themes/Ryan Light.tmTheme +232 -0
  139. package/themes/Seafoam.tmTheme +242 -0
  140. package/themes/Sidewalk Chalk.tmTheme +276 -0
  141. package/themes/Sin City (that yellow bastard).tmTheme +585 -0
  142. package/themes/Slate.tmTheme +436 -0
  143. package/themes/Slush & Poppies.tmTheme +336 -0
  144. package/themes/Slush and Poppies.tmTheme +336 -0
  145. package/themes/Smokey Morning.tmTheme +229 -0
  146. package/themes/Smoothy original.tmTheme +623 -0
  147. package/themes/Smoothy.tmTheme +623 -0
  148. package/themes/Solarized (dark).tmTheme +2051 -0
  149. package/themes/Solarized-dark.tmTheme +312 -0
  150. package/themes/Solarized-light.tmTheme +305 -0
  151. package/themes/Sometheme.tmTheme +240 -0
  152. package/themes/SoylentTheme.tmTheme +353 -0
  153. package/themes/SpaceCadet.tmTheme +212 -0
  154. package/themes/Spectacular.tmTheme +436 -0
  155. package/themes/Starlight.tmTheme +857 -0
  156. package/themes/Stoneship Bright.tmTheme +348 -0
  157. package/themes/Stoneship.tmTheme +361 -0
  158. package/themes/Summer Camp Mod.tmTheme +229 -0
  159. package/themes/Summer Camp.tmTheme +229 -0
  160. package/themes/Summery Drink.tmTheme +242 -0
  161. package/themes/Sunburst.tmTheme +665 -0
  162. package/themes/Swyphs II.tmTheme +306 -0
  163. package/themes/Tango Bright.tmTheme +1 -0
  164. package/themes/Tango.tmTheme +450 -0
  165. package/themes/Teenage Dream.tmTheme +242 -0
  166. package/themes/Texari.tmTheme +727 -0
  167. package/themes/Text Ex Machina.tmTheme +295 -0
  168. package/themes/Tomorrow-Night-Blue.tmTheme +175 -0
  169. package/themes/Tomorrow-Night-Eighties.tmTheme +175 -0
  170. package/themes/Tomorrow-Night.tmTheme +175 -0
  171. package/themes/Tomorrow.tmTheme +349 -0
  172. package/themes/ToyChest.tmTheme +503 -0
  173. package/themes/TravisJeffery.tmTheme +1261 -0
  174. package/themes/Tubster.tmTheme +280 -0
  175. package/themes/Twilight BG FG.tmTheme +1000 -0
  176. package/themes/Twilight.tmTheme +516 -0
  177. package/themes/TwilightMod.tmTheme +529 -0
  178. package/themes/Two Days Ago.tmTheme +242 -0
  179. package/themes/Vibrant Fin.tmTheme +447 -0
  180. package/themes/Vibrant Ink.tmTheme +447 -0
  181. package/themes/Vibrant Scala.tmTheme +292 -0
  182. package/themes/Vibrant Tango.tmTheme +438 -0
  183. package/themes/Wandering.tmTheme +681 -0
  184. package/themes/Whimsy in Blue.tmTheme +240 -0
  185. package/themes/Why/342/200/231s Poignant.tmTheme" +191 -0
  186. package/themes/Windows XP.tmTheme +350 -0
  187. package/themes/Witch.tmTheme +282 -0
  188. package/themes/Yurple.tmTheme +227 -0
  189. package/themes/ZZZ.tmTheme +131 -0
  190. package/themes/Zachstronaut.tmTheme +381 -0
  191. package/themes/Zenburnesque.tmTheme +343 -0
  192. package/themes/[ Argonaut ].tmTheme +387 -0
  193. package/themes/barf.tmTheme +254 -0
  194. package/themes/converted-vscode-AmoledShinyBlack.tmTheme +528 -0
  195. package/themes/converted-vscode-AmoledShinyBlack2.tmTheme +609 -0
  196. package/themes/converted-vscode-AmoledShinyBlack3.tmTheme +683 -0
  197. package/themes/converted-vscode-AmoledShinyBlack4.tmTheme +896 -0
  198. package/themes/converted-vscode-AmoledShinyBlack5.tmTheme +1023 -0
  199. package/themes/converted-vscode-AmoledShinyBlack6.tmTheme +2092 -0
  200. package/themes/eclips3.media (ECLM).tmTheme +294 -0
  201. package/themes/evin.tmTheme +253 -0
  202. package/themes/fake.tmTheme +669 -0
  203. package/themes/fapfap.tmTheme +508 -0
  204. package/themes/helloKitty.tmTheme +293 -0
  205. package/themes/iLife 05.tmTheme +619 -0
  206. package/themes/iPlastic.tmTheme +397 -0
  207. package/themes/idleFingers.tmTheme +380 -0
  208. package/themes/krTheme.tmTheme +551 -0
  209. package/themes/mark.james.name.tmTheme +1117 -0
  210. package/themes/minimal Theme.tmTheme +551 -0
  211. package/themes/mint.tmTheme +617 -0
  212. package/themes/mintBlue Dark.tmTheme +653 -0
  213. package/themes/mintBlue.tmTheme +655 -0
  214. package/themes/modifiedPastels.tmTheme +745 -0
  215. package/themes/monoindustrial.tmTheme +451 -0
  216. package/themes/my-theme-blackboard.tmTheme +363 -0
  217. package/themes/my-theme-classic.tmTheme +465 -0
  218. package/themes/nppGLua.tmTheme +293 -0
  219. package/themes/reST testing theme.tmTheme +554 -0
  220. package/themes/rose-pine-dawn.tmTheme +329 -0
  221. package/themes/rose-pine-moon.tmTheme +329 -0
  222. package/themes/rose-pine.tmTheme +329 -0
  223. package/themes/ryan-light.tmTheme +232 -0
  224. package/themes/wut.tmTheme +255 -0
  225. package/tools/build-pages-site.mjs +465 -0
  226. package/tools/generate-theme-metadata.mjs +680 -0
  227. package/tools/serve-pages-site.mjs +196 -0
  228. package/tools/validate-themes.mjs +272 -0
  229. package/types/index.d.ts +49 -0
package/docs/app.js ADDED
@@ -0,0 +1,1393 @@
1
+ /**
2
+ * @typedef {{
3
+ * readonly colorReferences?: number;
4
+ * readonly settings?: number;
5
+ * readonly uniqueScopes?: number;
6
+ * }} ThemeStatistics
7
+ *
8
+ * @typedef {{
9
+ * readonly background?: string;
10
+ * readonly fontStyle?: string;
11
+ * readonly foreground?: string;
12
+ * readonly scope: string;
13
+ * }} ThemeRule
14
+ *
15
+ * @typedef {{
16
+ * readonly appearance: string;
17
+ * readonly author: null | string;
18
+ * readonly colors: Record<string, null | string>;
19
+ * readonly fileName: string;
20
+ * readonly id: string;
21
+ * readonly name: string;
22
+ * readonly path: string;
23
+ * readonly rules: readonly ThemeRule[];
24
+ * readonly statistics: ThemeStatistics;
25
+ * readonly uuid: string;
26
+ * }} Theme
27
+ *
28
+ * @typedef {{
29
+ * readonly b: number;
30
+ * readonly g: number;
31
+ * readonly r: number;
32
+ * }} ColorRgb
33
+ *
34
+ * @typedef {{
35
+ * readonly hue: number;
36
+ * readonly lightness: number;
37
+ * readonly saturation: number;
38
+ * }} ColorHsl
39
+ *
40
+ * @typedef {Partial<
41
+ * Record<"background" | "fontStyle" | "foreground", string>
42
+ * >} MatchedStyle
43
+ *
44
+ * @typedef {{
45
+ * readonly extension: string;
46
+ * readonly name: string;
47
+ * readonly tokens: readonly (readonly [string, string])[];
48
+ * }} Sample
49
+ */
50
+
51
+ /** @type {readonly Sample[]} */
52
+ const samples = [
53
+ {
54
+ extension: "ts",
55
+ name: "TypeScript",
56
+ tokens: [
57
+ ["import", "keyword.control.import.ts"],
58
+ [" { ", "source.ts"],
59
+ ["readFile", "entity.name.function.ts"],
60
+ [" } ", "source.ts"],
61
+ ["from", "keyword.control.import.ts"],
62
+ [' "node:fs/promises"', "string.quoted.double.ts"],
63
+ [";\n\n", "punctuation.terminator.statement.ts"],
64
+ ["type", "storage.type.ts"],
65
+ [" ThemePreview", "entity.name.type.alias.ts"],
66
+ [" = {\n", "keyword.operator.assignment.ts"],
67
+ [" readonly", "storage.modifier.ts"],
68
+ [" name", "variable.other.property.ts"],
69
+ [": ", "punctuation.separator.key-value.ts"],
70
+ ["string", "support.type.primitive.ts"],
71
+ [";\n", "punctuation.terminator.statement.ts"],
72
+ [" readonly", "storage.modifier.ts"],
73
+ [" scopes", "variable.other.property.ts"],
74
+ [": ", "punctuation.separator.key-value.ts"],
75
+ ["readonly", "storage.modifier.ts"],
76
+ [" string", "support.type.primitive.ts"],
77
+ ["[];\n};\n\n", "source.ts"],
78
+ ["export", "storage.modifier.ts"],
79
+ [" async", "storage.modifier.async.ts"],
80
+ [" function", "storage.type.function.ts"],
81
+ [" loadTheme", "entity.name.function.ts"],
82
+ ["(", "punctuation.section.parameters.begin.ts"],
83
+ ["id", "variable.parameter.ts"],
84
+ [": ", "punctuation.separator.key-value.ts"],
85
+ ["string", "support.type.primitive.ts"],
86
+ [") {\n", "punctuation.section.parameters.end.ts"],
87
+ [" const", "storage.type.ts"],
88
+ [" file", "variable.other.readwrite.ts"],
89
+ [" = ", "keyword.operator.assignment.ts"],
90
+ ["await", "keyword.control.flow.ts"],
91
+ [" readFile", "entity.name.function.ts"],
92
+ ["(", "punctuation.section.arguments.begin.ts"],
93
+ ["id", "variable.other.readwrite.ts"],
94
+ [");\n", "punctuation.terminator.statement.ts"],
95
+ [" return", "keyword.control.flow.ts"],
96
+ [" JSON", "support.class.builtin.ts"],
97
+ [".", "punctuation.accessor.ts"],
98
+ ["parse", "support.function.builtin.ts"],
99
+ ["(", "punctuation.section.arguments.begin.ts"],
100
+ ["file", "variable.other.readwrite.ts"],
101
+ [");\n}", "punctuation.section.block.end.ts"],
102
+ ],
103
+ },
104
+ {
105
+ extension: "ps1",
106
+ name: "PowerShell",
107
+ tokens: [
108
+ ["param", "keyword.control.powershell"],
109
+ ["(\n", "punctuation.section.group.begin.powershell"],
110
+ [" [", "punctuation.definition.attribute.begin.powershell"],
111
+ ["string", "support.type.powershell"],
112
+ ["]", "punctuation.definition.attribute.end.powershell"],
113
+ [" $Theme", "variable.other.readwrite.powershell"],
114
+ [" = ", "keyword.operator.assignment.powershell"],
115
+ ['"AmoledShinyBlack6"', "string.quoted.double.powershell"],
116
+ ["\n)\n\n", "punctuation.section.group.end.powershell"],
117
+ ["$metadata", "variable.other.readwrite.powershell"],
118
+ [" = ", "keyword.operator.assignment.powershell"],
119
+ ["Get-Content", "support.function.powershell"],
120
+ [" ", "source.powershell"],
121
+ ["metadata/themes.json", "string.unquoted.powershell"],
122
+ [" | ", "keyword.operator.pipe.powershell"],
123
+ ["ConvertFrom-JSON", "support.function.powershell"],
124
+ ["\n\n", "source.powershell"],
125
+ ["foreach", "keyword.control.loop.powershell"],
126
+ [" (", "punctuation.section.group.begin.powershell"],
127
+ ["$item", "variable.other.readwrite.powershell"],
128
+ [" in ", "keyword.operator.word.powershell"],
129
+ ["$metadata", "variable.other.readwrite.powershell"],
130
+ [".themes", "variable.other.member.powershell"],
131
+ [") {\n", "punctuation.section.group.end.powershell"],
132
+ [" if", "keyword.control.conditional.powershell"],
133
+ [" (", "punctuation.section.group.begin.powershell"],
134
+ ["$item", "variable.other.readwrite.powershell"],
135
+ [".appearance", "variable.other.member.powershell"],
136
+ [" -eq ", "keyword.operator.comparison.powershell"],
137
+ ['"dark"', "string.quoted.double.powershell"],
138
+ [") {\n", "punctuation.section.group.end.powershell"],
139
+ [" Write-Host", "support.function.powershell"],
140
+ [" ", "source.powershell"],
141
+ ["$item", "variable.other.readwrite.powershell"],
142
+ [".name\n", "variable.other.member.powershell"],
143
+ [" }\n}", "punctuation.section.block.end.powershell"],
144
+ ],
145
+ },
146
+ {
147
+ extension: "py",
148
+ name: "Python",
149
+ tokens: [
150
+ ["from", "keyword.control.import.python"],
151
+ [" pathlib ", "source.python"],
152
+ ["import", "keyword.control.import.python"],
153
+ [" Path\n\n", "support.type.python"],
154
+ ["class", "storage.type.class.python"],
155
+ [" ThemeIndex", "entity.name.type.class.python"],
156
+ [":\n", "punctuation.separator.colon.python"],
157
+ [" def", "storage.type.function.python"],
158
+ [" __init__", "entity.name.function.python"],
159
+ ["(", "punctuation.section.parameters.begin.python"],
160
+ [
161
+ "self",
162
+ "variable.parameter.function.language.special.self.python",
163
+ ],
164
+ [", ", "punctuation.separator.parameters.python"],
165
+ ["root", "variable.parameter.function.python"],
166
+ [": ", "punctuation.separator.annotation.python"],
167
+ ["Path", "support.type.python"],
168
+ ["):\n", "punctuation.section.parameters.end.python"],
169
+ [" self", "variable.language.special.self.python"],
170
+ [".root", "variable.other.property.python"],
171
+ [" = ", "keyword.operator.assignment.python"],
172
+ ["root\n\n", "variable.other.readwrite.python"],
173
+ [" def", "storage.type.function.python"],
174
+ [" find", "entity.name.function.python"],
175
+ ["(", "punctuation.section.parameters.begin.python"],
176
+ [
177
+ "self",
178
+ "variable.parameter.function.language.special.self.python",
179
+ ],
180
+ [", ", "punctuation.separator.parameters.python"],
181
+ ["query", "variable.parameter.function.python"],
182
+ [": ", "punctuation.separator.annotation.python"],
183
+ ["str", "support.type.python"],
184
+ ["):\n", "punctuation.section.parameters.end.python"],
185
+ [" return", "keyword.control.flow.python"],
186
+ [" [", "punctuation.definition.list.begin.python"],
187
+ ["theme", "variable.other.readwrite.python"],
188
+ [" for ", "keyword.control.loop.python"],
189
+ ["theme", "variable.other.readwrite.python"],
190
+ [" in ", "keyword.control.loop.python"],
191
+ ["self", "variable.language.special.self.python"],
192
+ [".themes", "variable.other.property.python"],
193
+ [" if ", "keyword.control.conditional.python"],
194
+ ["query", "variable.other.readwrite.python"],
195
+ [".lower", "support.function.builtin.python"],
196
+ ["()", "punctuation.section.arguments.python"],
197
+ [" in ", "keyword.operator.word.python"],
198
+ ["theme", "variable.other.readwrite.python"],
199
+ [".name", "variable.other.property.python"],
200
+ [".lower", "support.function.builtin.python"],
201
+ ["()", "punctuation.section.arguments.python"],
202
+ ["]", "punctuation.definition.list.end.python"],
203
+ ],
204
+ },
205
+ {
206
+ extension: "html",
207
+ name: "HTML CSS",
208
+ tokens: [
209
+ ["<", "punctuation.definition.tag.begin.html"],
210
+ ["section", "entity.name.tag.html"],
211
+ [" class", "entity.other.attribute-name.html"],
212
+ ["=", "punctuation.separator.key-value.html"],
213
+ ['"preview"', "string.quoted.double.html"],
214
+ [">\n", "punctuation.definition.tag.end.html"],
215
+ [" <", "punctuation.definition.tag.begin.html"],
216
+ ["h2", "entity.name.tag.html"],
217
+ [">", "punctuation.definition.tag.end.html"],
218
+ ["Amoled preview", "text.html.basic"],
219
+ ["</", "punctuation.definition.tag.begin.html"],
220
+ ["h2", "entity.name.tag.html"],
221
+ [">\n", "punctuation.definition.tag.end.html"],
222
+ [" <", "punctuation.definition.tag.begin.html"],
223
+ ["style", "entity.name.tag.html"],
224
+ [">\n", "punctuation.definition.tag.end.html"],
225
+ [" .preview", "entity.other.attribute-name.class.css"],
226
+ [" {\n", "punctuation.section.property-list.begin.css"],
227
+ [" color", "support.type.property-name.css"],
228
+ [": ", "punctuation.separator.key-value.css"],
229
+ ["#8fd3c7", "constant.other.color.rgb-value.css"],
230
+ [";\n", "punctuation.terminator.rule.css"],
231
+ [" background", "support.type.property-name.css"],
232
+ [": ", "punctuation.separator.key-value.css"],
233
+ ["black", "support.constant.color.w3c-standard-color-name.css"],
234
+ [";\n }\n", "punctuation.terminator.rule.css"],
235
+ [" </", "punctuation.definition.tag.begin.html"],
236
+ ["style", "entity.name.tag.html"],
237
+ [">\n", "punctuation.definition.tag.end.html"],
238
+ ["</", "punctuation.definition.tag.begin.html"],
239
+ ["section", "entity.name.tag.html"],
240
+ [">", "punctuation.definition.tag.end.html"],
241
+ ],
242
+ },
243
+ ];
244
+
245
+ /**
246
+ * @type {{
247
+ * appearance: string;
248
+ * colorEnabled: boolean;
249
+ * colorHex: string;
250
+ * hue: string;
251
+ * query: string;
252
+ * selectedId: string;
253
+ * themes: Theme[];
254
+ * }}
255
+ */
256
+ const state = {
257
+ appearance: "all",
258
+ colorEnabled: false,
259
+ colorHex: "#69d6c6",
260
+ hue: "all",
261
+ query: "",
262
+ selectedId: "",
263
+ themes: [],
264
+ };
265
+
266
+ const colorRenderDelayMs = 140;
267
+
268
+ /** @type {readonly ("background" | "fontStyle" | "foreground")[]} */
269
+ const styleKeys = [
270
+ "background",
271
+ "fontStyle",
272
+ "foreground",
273
+ ];
274
+
275
+ /** @type {ReturnType<typeof globalThis.setTimeout> | 0} */
276
+ let colorRenderTimer = 0;
277
+
278
+ const elements = {
279
+ appearanceFilter: queryElement("#appearance_filter", HTMLSelectElement),
280
+ codeGrid: queryElement("#code_grid", HTMLElement),
281
+ colorEnabled: queryElement("#color_enabled", HTMLInputElement),
282
+ colorLightness: queryElement("#color_lightness", HTMLInputElement),
283
+ colorPicker: queryElement("#color_picker", HTMLInputElement),
284
+ colorPreview: queryElement("#color_preview", HTMLElement),
285
+ colorRgb: queryElement("#color_rgb", HTMLOutputElement),
286
+ colorWheel: queryElement("#color_wheel", HTMLElement),
287
+ colorWheelButton: queryElement("#color_wheel_button", HTMLButtonElement),
288
+ colorWheelMarker: queryElement("#color_wheel_marker", HTMLElement),
289
+ colorWheelPopover: queryElement("#color_wheel_popover", HTMLElement),
290
+ hueFilter: queryElement("#hue_filter", HTMLSelectElement),
291
+ metadataStrip: queryElement("#metadata_strip", HTMLElement),
292
+ scopeCount: queryElement("#scope_count", HTMLElement),
293
+ search: queryElement("#theme_search", HTMLInputElement),
294
+ selectedAppearance: queryElement("#selected_appearance", HTMLElement),
295
+ selectedName: queryElement("#selected_name", HTMLElement),
296
+ terminalFrame: queryElement("#terminal_frame", HTMLElement),
297
+ themeCount: queryElement("#theme_count", HTMLElement),
298
+ themeDownload: queryElement("#theme_download", HTMLAnchorElement),
299
+ themeList: queryElement("#theme_list", HTMLElement),
300
+ };
301
+
302
+ /**
303
+ * @param {HTMLElement} parent
304
+ * @param {string} className
305
+ * @param {string} text
306
+ *
307
+ * @returns {HTMLElement}
308
+ */
309
+ function appendElement(parent, className, text) {
310
+ const element = document.createElement("span");
311
+ element.className = className;
312
+ element.textContent = text;
313
+ parent.append(element);
314
+ return element;
315
+ }
316
+
317
+ /**
318
+ * @param {unknown} color
319
+ * @param {string} fallback
320
+ *
321
+ * @returns {string}
322
+ */
323
+ function colorOrFallback(color, fallback) {
324
+ return typeof color === "string" && color.length > 0 ? color : fallback;
325
+ }
326
+
327
+ function closeColorWheel() {
328
+ elements.colorWheelPopover.hidden = true;
329
+ elements.colorWheelButton.setAttribute("aria-expanded", "false");
330
+ }
331
+
332
+ function commitColorRender() {
333
+ if (colorRenderTimer !== 0) {
334
+ globalThis.clearTimeout(colorRenderTimer);
335
+ colorRenderTimer = 0;
336
+ }
337
+
338
+ render();
339
+ }
340
+
341
+ /**
342
+ * @param {string} fontStyle
343
+ *
344
+ * @returns {string}
345
+ */
346
+ function fontStyleClass(fontStyle) {
347
+ return fontStyle
348
+ .split(" ")
349
+ .filter((part) =>
350
+ [
351
+ "bold",
352
+ "italic",
353
+ "underline",
354
+ ].includes(part)
355
+ )
356
+ .join(" ");
357
+ }
358
+
359
+ /**
360
+ * @param {ColorRgb} color
361
+ * @param {ColorRgb} target
362
+ *
363
+ * @returns {number}
364
+ */
365
+ function getColorDistance(color, target) {
366
+ const redDelta = color.r - target.r;
367
+ const greenDelta = color.g - target.g;
368
+ const blueDelta = color.b - target.b;
369
+
370
+ return Math.hypot(redDelta, greenDelta, blueDelta);
371
+ }
372
+
373
+ /**
374
+ * @param {PointerEvent} event
375
+ *
376
+ * @returns {string}
377
+ */
378
+ function getColorFromWheelPointer(event) {
379
+ const bounds = elements.colorWheel.getBoundingClientRect();
380
+ const centerX = bounds.left + bounds.width / 2;
381
+ const centerY = bounds.top + bounds.height / 2;
382
+ const radius = bounds.width / 2;
383
+ const x = event.clientX - centerX;
384
+ const y = event.clientY - centerY;
385
+ const hue = (Math.atan2(y, x) * 180) / Math.PI;
386
+ const normalizedHue = (hue + 360) % 360;
387
+ const saturation = Math.min(Math.hypot(x, y) / radius, 1);
388
+ const lightness = Number.parseInt(elements.colorLightness.value, 10) / 100;
389
+
390
+ return rgbToHex(
391
+ hslToRgb({
392
+ hue: normalizedHue,
393
+ lightness,
394
+ saturation,
395
+ })
396
+ );
397
+ }
398
+
399
+ /**
400
+ * @returns {readonly Theme[]}
401
+ */
402
+ function getFilteredThemes() {
403
+ const queryParts = state.query
404
+ .toLowerCase()
405
+ .split(/\s+/v)
406
+ .filter((part) => part.length > 0);
407
+
408
+ return state.themes.filter((theme) => {
409
+ const matchesAppearance =
410
+ state.appearance === "all" || theme.appearance === state.appearance;
411
+ const searchText = [
412
+ theme.name,
413
+ theme.fileName,
414
+ theme.author,
415
+ theme.appearance,
416
+ theme.path,
417
+ theme.uuid,
418
+ ]
419
+ .join(" ")
420
+ .toLowerCase();
421
+ const matchesQuery = queryParts.every((part) =>
422
+ searchText.includes(part)
423
+ );
424
+ const matchesHue =
425
+ state.hue === "all" || themeMatchesHue(theme, state.hue);
426
+ const matchesColor = themeMatchesPickedColor(theme);
427
+
428
+ return matchesAppearance && matchesQuery && matchesHue && matchesColor;
429
+ });
430
+ }
431
+
432
+ /**
433
+ * @param {ColorRgb} color
434
+ *
435
+ * @returns {string}
436
+ */
437
+ function getHueCategory(color) {
438
+ const { hue, lightness, saturation } = rgbToHsl(color);
439
+
440
+ if (saturation < 0.16 || lightness < 0.08 || lightness > 0.94) {
441
+ return "neutral";
442
+ }
443
+
444
+ if (hue < 16 || hue >= 345) {
445
+ return "red";
446
+ }
447
+
448
+ if (hue < 45) {
449
+ return "orange";
450
+ }
451
+
452
+ if (hue < 71) {
453
+ return "yellow";
454
+ }
455
+
456
+ if (hue < 156) {
457
+ return "green";
458
+ }
459
+
460
+ if (hue < 196) {
461
+ return "cyan";
462
+ }
463
+
464
+ if (hue < 251) {
465
+ return "blue";
466
+ }
467
+
468
+ if (hue < 291) {
469
+ return "purple";
470
+ }
471
+
472
+ return "pink";
473
+ }
474
+
475
+ /**
476
+ * @param {ColorHsl} color
477
+ *
478
+ * @returns {ColorRgb}
479
+ */
480
+ function hslToRgb(color) {
481
+ const chroma = (1 - Math.abs(2 * color.lightness - 1)) * color.saturation;
482
+ const huePrime = color.hue / 60;
483
+ const secondary = chroma * (1 - Math.abs((huePrime % 2) - 1));
484
+ const match = color.lightness - chroma / 2;
485
+
486
+ /** @type {readonly [number, number, number]} */
487
+ let channels = [
488
+ chroma,
489
+ 0,
490
+ secondary,
491
+ ];
492
+
493
+ if (huePrime < 1) {
494
+ channels = [
495
+ chroma,
496
+ secondary,
497
+ 0,
498
+ ];
499
+ } else if (huePrime < 2) {
500
+ channels = [
501
+ secondary,
502
+ chroma,
503
+ 0,
504
+ ];
505
+ } else if (huePrime < 3) {
506
+ channels = [
507
+ 0,
508
+ chroma,
509
+ secondary,
510
+ ];
511
+ } else if (huePrime < 4) {
512
+ channels = [
513
+ 0,
514
+ secondary,
515
+ chroma,
516
+ ];
517
+ } else if (huePrime < 5) {
518
+ channels = [
519
+ secondary,
520
+ 0,
521
+ chroma,
522
+ ];
523
+ }
524
+
525
+ return {
526
+ b: Math.round((channels[2] + match) * 255),
527
+ g: Math.round((channels[1] + match) * 255),
528
+ r: Math.round((channels[0] + match) * 255),
529
+ };
530
+ }
531
+
532
+ /**
533
+ * @param {Theme} theme
534
+ *
535
+ * @returns {Record<string, string>}
536
+ */
537
+ function getThemeColors(theme) {
538
+ return {
539
+ background: colorOrFallback(theme.colors.background, "#101418"),
540
+ foreground: colorOrFallback(theme.colors.foreground, "#e8edf3"),
541
+ lineHighlight: colorOrFallback(theme.colors.lineHighlight, "#ffffff10"),
542
+ selection: colorOrFallback(theme.colors.selection, "#8fd3c766"),
543
+ };
544
+ }
545
+
546
+ /**
547
+ * @param {Theme} theme
548
+ *
549
+ * @returns {readonly ColorRgb[]}
550
+ */
551
+ function getThemeRgbColors(theme) {
552
+ return getThemeColorValues(theme)
553
+ .map((color) => parseHexColor(color))
554
+ .filter((color) => isColorRgb(color));
555
+ }
556
+
557
+ /**
558
+ * @param {Theme} theme
559
+ *
560
+ * @returns {readonly string[]}
561
+ */
562
+ function getThemeColorValues(theme) {
563
+ return [
564
+ ...Object.values(theme.colors),
565
+ ...theme.rules.flatMap((rule) => [rule.background, rule.foreground]),
566
+ ].filter((color) => isNonEmptyString(color));
567
+ }
568
+
569
+ /**
570
+ * @param {Theme} theme
571
+ *
572
+ * @returns {readonly ThemeRule[]}
573
+ */
574
+ function getThemeRules(theme) {
575
+ return theme.rules;
576
+ }
577
+
578
+ /**
579
+ * @param {unknown} value
580
+ *
581
+ * @returns {value is Record<string, unknown>}
582
+ */
583
+ function isRecord(value) {
584
+ return typeof value === "object" && value !== null && !Array.isArray(value);
585
+ }
586
+
587
+ /**
588
+ * @param {null | ColorRgb} value
589
+ *
590
+ * @returns {value is ColorRgb}
591
+ */
592
+ function isColorRgb(value) {
593
+ return value !== null;
594
+ }
595
+
596
+ /**
597
+ * @param {unknown} value
598
+ *
599
+ * @returns {value is string}
600
+ */
601
+ function isNonEmptyString(value) {
602
+ return typeof value === "string" && value.length > 0;
603
+ }
604
+
605
+ /**
606
+ * @param {string} value
607
+ *
608
+ * @returns {null | ColorRgb}
609
+ */
610
+ function parseHexColor(value) {
611
+ const normalized = value.trim().replace(/^#/v, "").toLowerCase();
612
+
613
+ if (
614
+ ![
615
+ 3,
616
+ 6,
617
+ 8,
618
+ ].includes(normalized.length) ||
619
+ !/^[\da-f]+$/v.test(normalized)
620
+ ) {
621
+ return null;
622
+ }
623
+
624
+ const rgb =
625
+ normalized.length === 3
626
+ ? `${normalized.charAt(0).repeat(2)}${normalized
627
+ .charAt(1)
628
+ .repeat(2)}${normalized.charAt(2).repeat(2)}`
629
+ : normalized.slice(0, 6);
630
+
631
+ return {
632
+ b: Number.parseInt(rgb.slice(4, 6), 16),
633
+ g: Number.parseInt(rgb.slice(2, 4), 16),
634
+ r: Number.parseInt(rgb.slice(0, 2), 16),
635
+ };
636
+ }
637
+
638
+ function openColorWheel() {
639
+ syncColorControlUi();
640
+ elements.colorWheelPopover.hidden = false;
641
+ elements.colorWheelButton.setAttribute("aria-expanded", "true");
642
+ elements.colorWheel.focus();
643
+ }
644
+
645
+ /**
646
+ * @template {Element} T
647
+ *
648
+ * @param {string} selector
649
+ * @param {new (...arguments_: never[]) => T} constructor
650
+ *
651
+ * @returns {T}
652
+ */
653
+ function queryElement(selector, constructor) {
654
+ const element = document.querySelector(selector);
655
+
656
+ if (element instanceof constructor) {
657
+ return element;
658
+ }
659
+
660
+ throw new TypeError(`Missing required element: ${selector}`);
661
+ }
662
+
663
+ /**
664
+ * @param {unknown} value
665
+ *
666
+ * @returns {Record<string, null | string>}
667
+ */
668
+ function readColors(value) {
669
+ if (!isRecord(value)) {
670
+ return {};
671
+ }
672
+
673
+ return Object.fromEntries(
674
+ Object.entries(value).map(([key, entryValue]) => [
675
+ key,
676
+ typeof entryValue === "string" ? entryValue : null,
677
+ ])
678
+ );
679
+ }
680
+
681
+ /**
682
+ * @param {Record<string, unknown>} record
683
+ * @param {string} key
684
+ *
685
+ * @returns {null | string}
686
+ */
687
+ function readNullableString(record, key) {
688
+ const value = record[key];
689
+ return typeof value === "string" ? value : null;
690
+ }
691
+
692
+ /**
693
+ * @param {unknown} value
694
+ *
695
+ * @returns {readonly ThemeRule[]}
696
+ */
697
+ function readRules(value) {
698
+ if (!Array.isArray(value)) {
699
+ return [];
700
+ }
701
+
702
+ return value
703
+ .filter((entry) => isRecord(entry))
704
+ .flatMap((entry) => {
705
+ const scope = readString(entry, "scope");
706
+
707
+ if (scope.length === 0) {
708
+ return [];
709
+ }
710
+
711
+ return [
712
+ {
713
+ ...(readString(entry, "background").length === 0
714
+ ? {}
715
+ : { background: readString(entry, "background") }),
716
+ ...(readString(entry, "fontStyle").length === 0
717
+ ? {}
718
+ : { fontStyle: readString(entry, "fontStyle") }),
719
+ ...(readString(entry, "foreground").length === 0
720
+ ? {}
721
+ : { foreground: readString(entry, "foreground") }),
722
+ scope,
723
+ },
724
+ ];
725
+ });
726
+ }
727
+
728
+ /**
729
+ * @param {Record<string, unknown>} record
730
+ * @param {string} key
731
+ *
732
+ * @returns {string}
733
+ */
734
+ function readString(record, key) {
735
+ const value = record[key];
736
+ return typeof value === "string" ? value : "";
737
+ }
738
+
739
+ /**
740
+ * @param {unknown} value
741
+ *
742
+ * @returns {ThemeStatistics}
743
+ */
744
+ function readStatistics(value) {
745
+ if (!isRecord(value)) {
746
+ return {};
747
+ }
748
+
749
+ return {
750
+ colorReferences:
751
+ typeof value.colorReferences === "number"
752
+ ? value.colorReferences
753
+ : 0,
754
+ settings: typeof value.settings === "number" ? value.settings : 0,
755
+ uniqueScopes:
756
+ typeof value.uniqueScopes === "number" ? value.uniqueScopes : 0,
757
+ };
758
+ }
759
+
760
+ /**
761
+ * @param {unknown} data
762
+ *
763
+ * @returns {Theme[]}
764
+ */
765
+ function readThemes(data) {
766
+ if (!isRecord(data) || !Array.isArray(data.themes)) {
767
+ return [];
768
+ }
769
+
770
+ return data.themes
771
+ .filter((entry) => isRecord(entry))
772
+ .map((entry) => ({
773
+ appearance: readString(entry, "appearance"),
774
+ author: readNullableString(entry, "author"),
775
+ colors: readColors(entry.colors),
776
+ fileName: readString(entry, "fileName"),
777
+ id: readString(entry, "id"),
778
+ name: readString(entry, "name"),
779
+ path: readString(entry, "path"),
780
+ rules: readRules(entry.rules),
781
+ statistics: readStatistics(entry.statistics),
782
+ uuid: readString(entry, "uuid"),
783
+ }));
784
+ }
785
+
786
+ /**
787
+ * @param {ThemeRule} rule
788
+ * @param {"background" | "fontStyle" | "foreground"} key
789
+ *
790
+ * @returns {string | undefined}
791
+ */
792
+ function readRuleStyleValue(rule, key) {
793
+ if (key === "background") {
794
+ return rule.background;
795
+ }
796
+
797
+ if (key === "fontStyle") {
798
+ return rule.fontStyle;
799
+ }
800
+
801
+ return rule.foreground;
802
+ }
803
+
804
+ /**
805
+ * @param {MatchedStyle} style
806
+ * @param {"background" | "fontStyle" | "foreground"} key
807
+ * @param {string} value
808
+ */
809
+ function writeMatchedStyle(style, key, value) {
810
+ if (key === "background") {
811
+ style.background = value;
812
+ } else if (key === "fontStyle") {
813
+ style.fontStyle = value;
814
+ } else {
815
+ style.foreground = value;
816
+ }
817
+ }
818
+
819
+ /**
820
+ * @param {readonly ThemeRule[]} rules
821
+ * @param {string} tokenScope
822
+ *
823
+ * @returns {MatchedStyle}
824
+ */
825
+ function matchStyle(rules, tokenScope) {
826
+ /** @type {MatchedStyle} */
827
+ const style = {};
828
+ /** @type {Record<string, number>} */
829
+ const scores = {};
830
+
831
+ for (const rule of rules) {
832
+ const selector = rule.scope;
833
+
834
+ if (typeof selector !== "string") {
835
+ continue;
836
+ }
837
+
838
+ const score = selectorScore(selector, tokenScope);
839
+
840
+ if (score <= 0) {
841
+ continue;
842
+ }
843
+
844
+ for (const key of styleKeys) {
845
+ const value = readRuleStyleValue(rule, key);
846
+
847
+ if (
848
+ typeof value === "string" &&
849
+ value.length > 0 &&
850
+ score >= (scores[key] ?? 0)
851
+ ) {
852
+ writeMatchedStyle(style, key, value);
853
+ scores[key] = score;
854
+ }
855
+ }
856
+ }
857
+
858
+ return style;
859
+ }
860
+
861
+ /**
862
+ * @param {Theme} theme
863
+ */
864
+ function renderMetadata(theme) {
865
+ const { statistics } = theme;
866
+ const metadata = [
867
+ theme.appearance,
868
+ `${statistics.settings ?? 0} settings`,
869
+ `${statistics.uniqueScopes ?? 0} scopes`,
870
+ `${statistics.colorReferences ?? 0} colors`,
871
+ theme.author ?? "unknown author",
872
+ ];
873
+
874
+ elements.metadataStrip.replaceChildren();
875
+
876
+ for (const item of metadata) {
877
+ appendElement(elements.metadataStrip, "metadata-pill", item);
878
+ }
879
+ }
880
+
881
+ function renderThemeList() {
882
+ const filteredThemes = getFilteredThemes();
883
+ elements.themeList.replaceChildren();
884
+
885
+ for (const theme of filteredThemes) {
886
+ const colors = getThemeColors(theme);
887
+ const button = document.createElement("button");
888
+ button.className = "theme-option";
889
+ button.type = "button";
890
+ button.setAttribute(
891
+ "aria-pressed",
892
+ String(theme.id === state.selectedId)
893
+ );
894
+ button.addEventListener("click", () => {
895
+ state.selectedId = theme.id;
896
+ render();
897
+ });
898
+
899
+ const swatch = document.createElement("span");
900
+ swatch.className = "swatch";
901
+ swatch.style.background = `linear-gradient(135deg, ${colors.background}, ${colors.selection})`;
902
+
903
+ const text = document.createElement("span");
904
+ const name = document.createElement("strong");
905
+ name.textContent = theme.name;
906
+ const meta = document.createElement("span");
907
+ meta.textContent = `${theme.appearance} / ${theme.fileName}`;
908
+
909
+ text.append(name, meta);
910
+ button.append(swatch, text);
911
+ elements.themeList.append(button);
912
+ }
913
+
914
+ if (filteredThemes.length === 0) {
915
+ appendElement(elements.themeList, "metadata-pill", "No themes match");
916
+ }
917
+ }
918
+
919
+ /**
920
+ * @param {Theme} theme
921
+ */
922
+ function renderThemePreview(theme) {
923
+ const colors = getThemeColors(theme);
924
+ const rules = getThemeRules(theme);
925
+
926
+ elements.selectedName.textContent = theme.name;
927
+ elements.selectedAppearance.textContent = `${theme.appearance} theme`;
928
+ elements.themeDownload.href = `../${theme.path}`;
929
+ elements.terminalFrame.style.backgroundColor = colors.background;
930
+ renderMetadata(theme);
931
+ elements.codeGrid.replaceChildren();
932
+
933
+ for (const sample of samples) {
934
+ const sampleElement = document.createElement("section");
935
+ sampleElement.className = "code-sample";
936
+ sampleElement.style.backgroundColor = colors.background;
937
+ sampleElement.style.color = colors.foreground;
938
+
939
+ const title = document.createElement("div");
940
+ title.className = "sample-title";
941
+ const sampleName = document.createElement("span");
942
+ sampleName.textContent = sample.name;
943
+ const sampleExtension = document.createElement("span");
944
+ sampleExtension.textContent = sample.extension;
945
+ title.append(sampleName, sampleExtension);
946
+
947
+ const pre = document.createElement("pre");
948
+ pre.style.color = colors.foreground;
949
+
950
+ for (const [text, scope] of sample.tokens) {
951
+ const token = document.createElement("span");
952
+ const style = matchStyle(rules, scope);
953
+ token.className = `token ${fontStyleClass(style.fontStyle ?? "")}`;
954
+ token.textContent = text;
955
+
956
+ if (style.foreground !== undefined) {
957
+ token.style.color = style.foreground;
958
+ }
959
+
960
+ if (style.background !== undefined) {
961
+ token.style.backgroundColor = style.background;
962
+ }
963
+
964
+ pre.append(token);
965
+ }
966
+
967
+ sampleElement.append(title, pre);
968
+ elements.codeGrid.append(sampleElement);
969
+ }
970
+ }
971
+
972
+ function render() {
973
+ const filteredThemes = getFilteredThemes();
974
+ const selectedThemeVisible = filteredThemes.some(
975
+ (theme) => theme.id === state.selectedId
976
+ );
977
+
978
+ elements.themeCount.textContent =
979
+ filteredThemes.length === state.themes.length
980
+ ? `${state.themes.length} themes`
981
+ : `${filteredThemes.length}/${state.themes.length} themes`;
982
+
983
+ if (!selectedThemeVisible) {
984
+ state.selectedId = "";
985
+
986
+ if (filteredThemes.length > 0) {
987
+ state.selectedId = filteredThemes[0].id;
988
+ }
989
+ }
990
+
991
+ renderThemeList();
992
+
993
+ const selectedTheme = state.themes.find(
994
+ (theme) => theme.id === state.selectedId
995
+ );
996
+
997
+ if (selectedTheme === undefined) {
998
+ renderEmptyPreview();
999
+ return;
1000
+ }
1001
+
1002
+ renderThemePreview(selectedTheme);
1003
+ }
1004
+
1005
+ function renderEmptyPreview() {
1006
+ elements.selectedName.textContent = "No matching themes";
1007
+ elements.selectedAppearance.textContent = "Theme";
1008
+ elements.themeDownload.href = "../themes/";
1009
+ elements.metadataStrip.replaceChildren();
1010
+ appendElement(elements.metadataStrip, "metadata-pill", "No color match");
1011
+ elements.codeGrid.replaceChildren();
1012
+ }
1013
+
1014
+ function scheduleColorRender() {
1015
+ if (colorRenderTimer !== 0) {
1016
+ globalThis.clearTimeout(colorRenderTimer);
1017
+ }
1018
+
1019
+ colorRenderTimer = globalThis.setTimeout(() => {
1020
+ colorRenderTimer = 0;
1021
+ render();
1022
+ }, colorRenderDelayMs);
1023
+ }
1024
+
1025
+ /**
1026
+ * @param {ColorRgb} color
1027
+ *
1028
+ * @returns {string}
1029
+ */
1030
+ function rgbToHex(color) {
1031
+ return `#${toHexChannel(color.r)}${toHexChannel(color.g)}${toHexChannel(
1032
+ color.b
1033
+ )}`;
1034
+ }
1035
+
1036
+ /**
1037
+ * @param {string} selector
1038
+ * @param {string} tokenScope
1039
+ *
1040
+ * @returns {number}
1041
+ */
1042
+ function selectorScore(selector, tokenScope) {
1043
+ const selectorParts = selector.split(/\s+/v);
1044
+ const selectorTarget = selectorParts.at(-1) ?? selector;
1045
+
1046
+ if (tokenScope === selectorTarget) {
1047
+ return 1000 + selectorTarget.length;
1048
+ }
1049
+
1050
+ if (tokenScope.startsWith(`${selectorTarget}.`)) {
1051
+ return 500 + selectorTarget.length;
1052
+ }
1053
+
1054
+ const scopeParts = tokenScope.split(".");
1055
+ const selectorSegments = selectorTarget.split(".");
1056
+ const matchesPrefix = selectorSegments.every(
1057
+ (segment, index) => scopeParts[index] === segment
1058
+ );
1059
+
1060
+ return matchesPrefix ? 100 + selectorTarget.length : 0;
1061
+ }
1062
+
1063
+ /**
1064
+ * @param {ColorRgb} color
1065
+ *
1066
+ * @returns {ColorHsl}
1067
+ */
1068
+ function rgbToHsl(color) {
1069
+ const red = color.r / 255;
1070
+ const green = color.g / 255;
1071
+ const blue = color.b / 255;
1072
+ const maximum = Math.max(red, green, blue);
1073
+ const minimum = Math.min(red, green, blue);
1074
+ const lightness = (maximum + minimum) / 2;
1075
+ const delta = maximum - minimum;
1076
+
1077
+ if (delta === 0) {
1078
+ return {
1079
+ hue: 0,
1080
+ lightness,
1081
+ saturation: 0,
1082
+ };
1083
+ }
1084
+
1085
+ let saturation = delta / (maximum + minimum);
1086
+
1087
+ if (lightness > 0.5) {
1088
+ saturation = delta / (2 - maximum - minimum);
1089
+ }
1090
+
1091
+ let hue = ((red - green) / delta + 4) * 60;
1092
+
1093
+ if (maximum === red) {
1094
+ hue = ((green - blue) / delta + (green < blue ? 6 : 0)) * 60;
1095
+ } else if (maximum === green) {
1096
+ hue = ((blue - red) / delta + 2) * 60;
1097
+ }
1098
+
1099
+ return {
1100
+ hue,
1101
+ lightness,
1102
+ saturation,
1103
+ };
1104
+ }
1105
+
1106
+ /**
1107
+ * @param {string} hex
1108
+ * @param {{ readonly renderMode?: "defer" | "none" | "now" }} [options]
1109
+ */
1110
+ function setSelectedColor(hex, options = {}) {
1111
+ const color = parseHexColor(hex);
1112
+
1113
+ if (color === null) {
1114
+ return;
1115
+ }
1116
+
1117
+ const normalizedHex = rgbToHex(color);
1118
+ state.colorHex = normalizedHex;
1119
+ elements.colorPicker.value = normalizedHex;
1120
+ syncColorControlUi();
1121
+
1122
+ const renderMode = options.renderMode ?? "now";
1123
+
1124
+ if (renderMode === "defer") {
1125
+ scheduleColorRender();
1126
+ } else if (renderMode === "now") {
1127
+ commitColorRender();
1128
+ }
1129
+ }
1130
+
1131
+ function syncColorControlUi() {
1132
+ const fallbackColor = /** @type {ColorRgb} */ ({
1133
+ b: 198,
1134
+ g: 214,
1135
+ r: 105,
1136
+ });
1137
+ let color = parseHexColor(state.colorHex);
1138
+ color ??= fallbackColor;
1139
+ const hsl = rgbToHsl(color);
1140
+ const hex = rgbToHex(color);
1141
+ const markerRadius = hsl.saturation * 46;
1142
+ const markerLeft = 50 + Math.cos((hsl.hue * Math.PI) / 180) * markerRadius;
1143
+ const markerTop = 50 + Math.sin((hsl.hue * Math.PI) / 180) * markerRadius;
1144
+ const shade = Math.max(0, (0.5 - hsl.lightness) * 1.7);
1145
+
1146
+ elements.colorPreview.style.backgroundColor = hex;
1147
+ elements.colorRgb.textContent = `rgb(${color.r}, ${color.g}, ${color.b})`;
1148
+ elements.colorWheel.style.setProperty("--selected-color", hex);
1149
+ elements.colorWheel.style.setProperty("--marker-left", `${markerLeft}%`);
1150
+ elements.colorWheel.style.setProperty("--marker-top", `${markerTop}%`);
1151
+ elements.colorWheel.style.setProperty("--wheel-shade", String(shade));
1152
+ elements.colorWheelMarker.style.backgroundColor = hex;
1153
+ elements.colorLightness.value = String(Math.round(hsl.lightness * 100));
1154
+ }
1155
+
1156
+ /**
1157
+ * @param {Theme} theme
1158
+ *
1159
+ * @returns {boolean}
1160
+ */
1161
+ function themeMatchesPickedColor(theme) {
1162
+ if (state.colorEnabled) {
1163
+ const target = parseHexColor(state.colorHex);
1164
+
1165
+ if (target === null) {
1166
+ return true;
1167
+ }
1168
+
1169
+ return getThemeRgbColors(theme).some(
1170
+ (color) => getColorDistance(color, target) <= 84
1171
+ );
1172
+ }
1173
+
1174
+ return true;
1175
+ }
1176
+
1177
+ /**
1178
+ * @param {Theme} theme
1179
+ * @param {string} hue
1180
+ *
1181
+ * @returns {boolean}
1182
+ */
1183
+ function themeMatchesHue(theme, hue) {
1184
+ return getThemeRgbColors(theme).some(
1185
+ (color) => getHueCategory(color) === hue
1186
+ );
1187
+ }
1188
+
1189
+ /**
1190
+ * @param {number} value
1191
+ *
1192
+ * @returns {string}
1193
+ */
1194
+ function toHexChannel(value) {
1195
+ return Math.min(255, Math.max(0, value)).toString(16).padStart(2, "0");
1196
+ }
1197
+
1198
+ function toggleColorWheel() {
1199
+ if (elements.colorWheelPopover.hidden === true) {
1200
+ openColorWheel();
1201
+ return;
1202
+ }
1203
+
1204
+ closeColorWheel();
1205
+ }
1206
+
1207
+ elements.search.addEventListener("input", (event) => {
1208
+ const target = event.target;
1209
+ state.query = target instanceof HTMLInputElement ? target.value : "";
1210
+ render();
1211
+ });
1212
+
1213
+ elements.appearanceFilter.addEventListener("change", (event) => {
1214
+ const target = event.target;
1215
+ state.appearance =
1216
+ target instanceof HTMLSelectElement ? target.value : "all";
1217
+ render();
1218
+ });
1219
+
1220
+ elements.hueFilter.addEventListener("change", (event) => {
1221
+ const target = event.target;
1222
+ state.hue = target instanceof HTMLSelectElement ? target.value : "all";
1223
+ render();
1224
+ });
1225
+
1226
+ elements.colorPicker.addEventListener("input", (event) => {
1227
+ const target = event.target;
1228
+ if (target instanceof HTMLInputElement) {
1229
+ state.colorHex = target.value;
1230
+
1231
+ if (parseHexColor(target.value) !== null) {
1232
+ syncColorControlUi();
1233
+ scheduleColorRender();
1234
+ }
1235
+ }
1236
+ });
1237
+
1238
+ elements.colorEnabled.addEventListener("change", (event) => {
1239
+ const target = event.target;
1240
+ state.colorEnabled =
1241
+ target instanceof HTMLInputElement ? target.checked : false;
1242
+ render();
1243
+ });
1244
+
1245
+ elements.colorLightness.addEventListener("input", () => {
1246
+ const color = parseHexColor(state.colorHex);
1247
+
1248
+ if (color === null) {
1249
+ return;
1250
+ }
1251
+
1252
+ const hsl = rgbToHsl(color);
1253
+ const lightness = Number.parseInt(elements.colorLightness.value, 10) / 100;
1254
+ setSelectedColor(
1255
+ rgbToHex(
1256
+ hslToRgb({
1257
+ ...hsl,
1258
+ lightness,
1259
+ })
1260
+ ),
1261
+ { renderMode: "defer" }
1262
+ );
1263
+ });
1264
+
1265
+ elements.colorWheel.addEventListener("keydown", (event) => {
1266
+ const color = parseHexColor(state.colorHex);
1267
+
1268
+ if (color === null) {
1269
+ return;
1270
+ }
1271
+
1272
+ const hsl = rgbToHsl(color);
1273
+
1274
+ switch (event.key) {
1275
+ case "ArrowDown": {
1276
+ event.preventDefault();
1277
+ setSelectedColor(
1278
+ rgbToHex(
1279
+ hslToRgb({
1280
+ ...hsl,
1281
+ saturation: Math.max(0, hsl.saturation - 0.05),
1282
+ })
1283
+ )
1284
+ );
1285
+ break;
1286
+ }
1287
+
1288
+ case "ArrowLeft": {
1289
+ event.preventDefault();
1290
+ setSelectedColor(
1291
+ rgbToHex(
1292
+ hslToRgb({
1293
+ ...hsl,
1294
+ hue: (hsl.hue + 354) % 360,
1295
+ })
1296
+ )
1297
+ );
1298
+ break;
1299
+ }
1300
+
1301
+ case "ArrowRight": {
1302
+ event.preventDefault();
1303
+ setSelectedColor(
1304
+ rgbToHex(
1305
+ hslToRgb({
1306
+ ...hsl,
1307
+ hue: (hsl.hue + 6) % 360,
1308
+ })
1309
+ )
1310
+ );
1311
+ break;
1312
+ }
1313
+
1314
+ case "ArrowUp": {
1315
+ event.preventDefault();
1316
+ setSelectedColor(
1317
+ rgbToHex(
1318
+ hslToRgb({
1319
+ ...hsl,
1320
+ saturation: Math.min(1, hsl.saturation + 0.05),
1321
+ })
1322
+ )
1323
+ );
1324
+ break;
1325
+ }
1326
+
1327
+ default: {
1328
+ break;
1329
+ }
1330
+ }
1331
+ });
1332
+
1333
+ elements.colorWheel.addEventListener("pointerdown", (event) => {
1334
+ elements.colorWheel.setPointerCapture(event.pointerId);
1335
+ setSelectedColor(getColorFromWheelPointer(event), { renderMode: "none" });
1336
+ });
1337
+
1338
+ elements.colorWheel.addEventListener("pointermove", (event) => {
1339
+ if (event.buttons > 0) {
1340
+ setSelectedColor(getColorFromWheelPointer(event), {
1341
+ renderMode: "none",
1342
+ });
1343
+ }
1344
+ });
1345
+
1346
+ elements.colorWheel.addEventListener("pointerup", () => {
1347
+ commitColorRender();
1348
+ });
1349
+
1350
+ elements.colorWheel.addEventListener("pointercancel", () => {
1351
+ commitColorRender();
1352
+ });
1353
+
1354
+ elements.colorWheelButton.addEventListener("click", (event) => {
1355
+ event.stopPropagation();
1356
+ toggleColorWheel();
1357
+ });
1358
+
1359
+ document.addEventListener("click", (event) => {
1360
+ const target = event.target;
1361
+
1362
+ if (
1363
+ target instanceof Node &&
1364
+ (elements.colorWheelPopover.contains(target) ||
1365
+ elements.colorWheelButton.contains(target))
1366
+ ) {
1367
+ return;
1368
+ }
1369
+
1370
+ closeColorWheel();
1371
+ });
1372
+
1373
+ document.addEventListener("keydown", (event) => {
1374
+ if (event.key === "Escape") {
1375
+ closeColorWheel();
1376
+ }
1377
+ });
1378
+
1379
+ syncColorControlUi();
1380
+
1381
+ // eslint-disable-next-line n/no-top-level-await -- This file runs as a browser module, not a published Node entry point.
1382
+ const response = await fetch("site-data.json");
1383
+ // eslint-disable-next-line n/no-top-level-await -- This file runs as a browser module, not a published Node entry point.
1384
+ const data = /** @type {unknown} */ (await response.json());
1385
+ state.themes = readThemes(data);
1386
+
1387
+ const allScopes = new Set(
1388
+ state.themes.flatMap((theme) => theme.rules.map((rule) => rule.scope))
1389
+ );
1390
+
1391
+ elements.themeCount.textContent = `${state.themes.length} themes`;
1392
+ elements.scopeCount.textContent = `${allScopes.size} styled scopes`;
1393
+ render();