@visual-z/xeno 0.2.1 → 0.3.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 (258) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +106 -26
  3. package/docs/install.md +51 -13
  4. package/docs/providers.md +11 -0
  5. package/package.json +5 -4
  6. package/src/apps/approve.ts +3 -3
  7. package/src/apps/commands.ts +38 -5
  8. package/src/apps/completions.ts +73 -19
  9. package/src/apps/image-draft.ts +46 -0
  10. package/src/apps/project-trust.ts +2 -42
  11. package/src/apps/provider-connect.ts +346 -0
  12. package/src/apps/render.ts +22 -47
  13. package/src/apps/resources.ts +5 -2
  14. package/src/apps/shell.ts +298 -1526
  15. package/src/apps/startup-input.ts +45 -9
  16. package/src/apps/startup.ts +128 -86
  17. package/src/apps/ui/app.ts +1383 -0
  18. package/src/apps/ui/extension-host.ts +194 -0
  19. package/src/apps/ui/image-cache.ts +78 -0
  20. package/src/apps/ui/palette.ts +88 -0
  21. package/src/apps/ui/project.ts +127 -0
  22. package/src/apps/ui/standalone.ts +146 -0
  23. package/src/apps/ui/status-model.ts +68 -0
  24. package/src/apps/ui/surface.ts +84 -0
  25. package/src/apps/ui/theme.ts +86 -0
  26. package/src/core/acceptance.ts +107 -0
  27. package/src/core/action-defaults.ts +47 -295
  28. package/src/core/benchmark.ts +2 -2
  29. package/src/core/branch-receipt.ts +24 -0
  30. package/src/core/checkpoints.ts +64 -0
  31. package/src/core/churn.ts +2 -0
  32. package/src/core/command-artifacts.ts +94 -0
  33. package/src/core/command-contract.ts +35 -0
  34. package/src/core/completion-review.ts +21 -5
  35. package/src/core/context-detail.ts +37 -0
  36. package/src/core/context-policy.ts +74 -1
  37. package/src/core/continuation.ts +17 -2
  38. package/src/core/derive.ts +29 -3
  39. package/src/core/draft-images.ts +56 -0
  40. package/src/core/environment.ts +145 -0
  41. package/src/core/evaluation-coverage.ts +74 -0
  42. package/src/core/event-validation.ts +80 -5
  43. package/src/core/events.ts +97 -3
  44. package/src/core/evidence-links.ts +7 -0
  45. package/src/core/evidence.ts +13 -3
  46. package/src/core/execution.ts +8 -2
  47. package/src/core/experiment.ts +133 -0
  48. package/src/core/export.ts +9 -0
  49. package/src/core/failure-recovery.ts +173 -0
  50. package/src/core/file-precondition.ts +72 -0
  51. package/src/core/handoff.ts +65 -4
  52. package/src/core/history-query.ts +38 -2
  53. package/src/core/history.ts +38 -1
  54. package/src/core/integration-health.ts +34 -0
  55. package/src/core/keymap.ts +8 -20
  56. package/src/core/model-picker.ts +64 -0
  57. package/src/core/native-eval.ts +25 -0
  58. package/src/core/note-guidance.ts +34 -0
  59. package/src/core/observation-view.ts +49 -8
  60. package/src/core/permissions.ts +24 -2
  61. package/src/core/process-receipt.ts +105 -0
  62. package/src/core/process-state.ts +37 -0
  63. package/src/core/project.ts +45 -18
  64. package/src/core/prompt-policy.ts +37 -0
  65. package/src/core/query-eval-contract.ts +14 -0
  66. package/src/core/read-observation.ts +82 -0
  67. package/src/core/recovery-corpus.ts +17 -0
  68. package/src/core/recovery-evaluation.ts +60 -0
  69. package/src/core/recovery-sources.ts +74 -0
  70. package/src/core/runtime-target.ts +94 -0
  71. package/src/core/search-observation.ts +97 -0
  72. package/src/core/session-tree.ts +147 -0
  73. package/src/core/settings.ts +26 -10
  74. package/src/core/skills.ts +44 -0
  75. package/src/core/sql-query.ts +211 -0
  76. package/src/core/theme.ts +36 -58
  77. package/src/core/tool-contract.ts +51 -2
  78. package/src/core/tool-discovery.ts +87 -0
  79. package/src/core/tool-failure.ts +60 -0
  80. package/src/core/tool-mode-protocol.ts +25 -0
  81. package/src/core/transcript.ts +19 -4
  82. package/src/core/trial-lifecycle.ts +14 -0
  83. package/src/core/tui-mode.ts +29 -0
  84. package/src/core/update-check.ts +97 -0
  85. package/src/core/version.ts +55 -0
  86. package/src/core/work-state.ts +49 -3
  87. package/src/core/working-set.ts +91 -0
  88. package/src/runtime/auth.ts +2 -2
  89. package/src/runtime/branch-jobs.ts +36 -0
  90. package/src/runtime/clipboard.ts +15 -4
  91. package/src/runtime/command-artifacts.ts +64 -0
  92. package/src/runtime/environment-tools.ts +60 -0
  93. package/src/runtime/evidence-notes.ts +4 -2
  94. package/src/runtime/exec-sessions.ts +206 -61
  95. package/src/runtime/execution-control.ts +1 -0
  96. package/src/runtime/extension-ui.ts +22 -30
  97. package/src/runtime/extensions.ts +95 -16
  98. package/src/runtime/harness-comparison.ts +27 -7
  99. package/src/runtime/harness-eval.ts +460 -0
  100. package/src/runtime/history-cache.ts +57 -0
  101. package/src/runtime/history-tools.ts +62 -12
  102. package/src/runtime/integrations.ts +210 -0
  103. package/src/runtime/loop.ts +1021 -268
  104. package/src/runtime/model-discovery.ts +208 -0
  105. package/src/runtime/native-eval-fixtures.ts +33 -0
  106. package/src/runtime/native-eval.ts +143 -0
  107. package/src/runtime/oauth.ts +1 -1
  108. package/src/runtime/provider-connect.ts +109 -0
  109. package/src/runtime/provider-presets.ts +87 -0
  110. package/src/runtime/query-eval-verifier.ts +28 -0
  111. package/src/runtime/recovery-coverage.ts +71 -0
  112. package/src/runtime/recovery-evidence.ts +76 -0
  113. package/src/runtime/recovery-fixtures.ts +116 -0
  114. package/src/runtime/runtime-target.ts +149 -0
  115. package/src/runtime/tool-broker.ts +137 -0
  116. package/src/runtime/tool-discovery.ts +57 -0
  117. package/src/runtime/tool-mode-eval.ts +146 -0
  118. package/src/runtime/tool-queue.ts +77 -0
  119. package/src/runtime/tools.ts +628 -183
  120. package/src/runtime/trial-control.ts +45 -0
  121. package/src/runtime/update-check.ts +47 -0
  122. package/src/runtime/window-trial.ts +2 -1
  123. package/src/runtime/working-notes.ts +33 -3
  124. package/src/runtime/workspace-lifecycle.ts +97 -0
  125. package/src/store/acceptance.ts +15 -0
  126. package/src/store/checkpoint-history.ts +43 -0
  127. package/src/store/command-artifacts.ts +96 -0
  128. package/src/store/command.ts +65 -37
  129. package/src/store/file-checkpoints.ts +104 -0
  130. package/src/store/file-index.ts +13 -1
  131. package/src/store/git-checkpoints.ts +67 -0
  132. package/src/store/observations.ts +61 -20
  133. package/src/store/process-receipts.ts +34 -0
  134. package/src/store/query-source.ts +13 -8
  135. package/src/store/recovery.ts +2 -3
  136. package/src/store/replay.ts +16 -4
  137. package/src/store/runtime-target.ts +64 -0
  138. package/src/store/search.ts +171 -132
  139. package/src/store/session-archive.ts +4 -0
  140. package/src/store/source-cancellation.ts +6 -0
  141. package/src/store/update-cache.ts +36 -0
  142. package/src/store/workspace-observation.ts +113 -0
  143. package/src/tui/ansi-spans.ts +126 -0
  144. package/src/tui/style.ts +19 -16
  145. package/src/tui/ui/atomic-draft.ts +121 -0
  146. package/src/tui/ui/canvas.ts +96 -0
  147. package/src/tui/ui/colors.ts +70 -0
  148. package/src/tui/ui/composer.ts +199 -0
  149. package/src/tui/ui/dialog.ts +379 -0
  150. package/src/tui/ui/draft.ts +23 -0
  151. package/src/tui/ui/edge.ts +186 -0
  152. package/src/tui/ui/image-size.ts +40 -0
  153. package/src/tui/ui/ink.ts +210 -0
  154. package/src/tui/ui/items.ts +40 -0
  155. package/src/tui/ui/keys.ts +40 -0
  156. package/src/tui/ui/layout.ts +74 -0
  157. package/src/tui/ui/list-model.ts +86 -0
  158. package/src/tui/ui/list-view.ts +172 -0
  159. package/src/tui/ui/mascot.ts +67 -0
  160. package/src/tui/ui/messages.ts +518 -0
  161. package/src/tui/ui/motion.ts +28 -0
  162. package/src/tui/ui/paths.ts +10 -0
  163. package/src/tui/ui/pictures.ts +78 -0
  164. package/src/tui/ui/screen.ts +421 -0
  165. package/src/tui/ui/status.ts +271 -0
  166. package/src/tui/ui/styled.ts +85 -0
  167. package/src/tui/ui/testing.ts +5 -0
  168. package/src/tui/ui/tool-card.ts +150 -0
  169. package/src/apps/extension-ui.ts +0 -330
  170. package/src/apps/resource-browser.ts +0 -75
  171. package/src/apps/session-browser.ts +0 -113
  172. package/src/apps/session-picker.ts +0 -55
  173. package/src/tui/ansi.ts +0 -67
  174. package/src/tui/autocomplete.ts +0 -29
  175. package/src/tui/chat-viewport.ts +0 -47
  176. package/src/tui/complete.ts +0 -87
  177. package/src/tui/editor.ts +0 -940
  178. package/src/tui/host.ts +0 -264
  179. package/src/tui/input.ts +0 -75
  180. package/src/tui/keys.ts +0 -378
  181. package/src/tui/markdown.ts +0 -17
  182. package/src/tui/render.ts +0 -153
  183. package/src/tui/select.ts +0 -258
  184. package/src/tui/settings.ts +0 -44
  185. package/src/tui/statusline.ts +0 -162
  186. package/src/tui/system-theme.ts +0 -700
  187. package/src/tui/terminal.ts +0 -30
  188. package/src/tui/text.ts +0 -52
  189. package/src/tui/toolview.ts +0 -386
  190. package/src/tui/transcript.ts +0 -123
  191. package/src/tui/tree.ts +0 -471
  192. package/src/vendor/pi-tui/LICENSE.upstream +0 -21
  193. package/src/vendor/pi-tui/UPSTREAM.json +0 -50
  194. package/src/vendor/pi-tui/VENDOR.md +0 -25
  195. package/src/vendor/pi-tui/alt-screen-search.ts +0 -327
  196. package/src/vendor/pi-tui/autocomplete.ts +0 -860
  197. package/src/vendor/pi-tui/colors.ts +0 -367
  198. package/src/vendor/pi-tui/components/alt-screen-flash.ts +0 -51
  199. package/src/vendor/pi-tui/components/box.ts +0 -166
  200. package/src/vendor/pi-tui/components/cancellable-loader.ts +0 -40
  201. package/src/vendor/pi-tui/components/editor.ts +0 -2472
  202. package/src/vendor/pi-tui/components/h-stack.ts +0 -44
  203. package/src/vendor/pi-tui/components/image.ts +0 -127
  204. package/src/vendor/pi-tui/components/input.ts +0 -494
  205. package/src/vendor/pi-tui/components/loader.ts +0 -101
  206. package/src/vendor/pi-tui/components/markdown.ts +0 -1021
  207. package/src/vendor/pi-tui/components/mouse-region.ts +0 -33
  208. package/src/vendor/pi-tui/components/scroll-view.ts +0 -224
  209. package/src/vendor/pi-tui/components/select-list.ts +0 -273
  210. package/src/vendor/pi-tui/components/settings-list.ts +0 -328
  211. package/src/vendor/pi-tui/components/spacer.ts +0 -28
  212. package/src/vendor/pi-tui/components/stack.ts +0 -154
  213. package/src/vendor/pi-tui/components/text.ts +0 -107
  214. package/src/vendor/pi-tui/components/truncated-text.ts +0 -65
  215. package/src/vendor/pi-tui/components/v-stack.ts +0 -33
  216. package/src/vendor/pi-tui/editor-component.ts +0 -74
  217. package/src/vendor/pi-tui/fuzzy.ts +0 -138
  218. package/src/vendor/pi-tui/index.ts +0 -186
  219. package/src/vendor/pi-tui/keybindings.ts +0 -320
  220. package/src/vendor/pi-tui/keys.ts +0 -1401
  221. package/src/vendor/pi-tui/kill-ring.ts +0 -46
  222. package/src/vendor/pi-tui/latex.ts +0 -1506
  223. package/src/vendor/pi-tui/layout-node.ts +0 -51
  224. package/src/vendor/pi-tui/layout.ts +0 -449
  225. package/src/vendor/pi-tui/native/UPSTREAM.json +0 -23
  226. package/src/vendor/pi-tui/native/clipboard.h +0 -208
  227. package/src/vendor/pi-tui/native/darwin/README.md +0 -22
  228. package/src/vendor/pi-tui/native/darwin/build.sh +0 -69
  229. package/src/vendor/pi-tui/native/darwin/prebuilds/darwin-arm64/darwin-platform.node +0 -0
  230. package/src/vendor/pi-tui/native/darwin/prebuilds/darwin-x64/darwin-platform.node +0 -0
  231. package/src/vendor/pi-tui/native/darwin/src/darwin-platform.m +0 -132
  232. package/src/vendor/pi-tui/native/linux/README.md +0 -23
  233. package/src/vendor/pi-tui/native/linux/build.sh +0 -65
  234. package/src/vendor/pi-tui/native/linux/prebuilds/linux-arm64/linux-platform-x11.node +0 -0
  235. package/src/vendor/pi-tui/native/linux/prebuilds/linux-x64/linux-platform-x11.node +0 -0
  236. package/src/vendor/pi-tui/native/linux/src/clipboard-worker.h +0 -70
  237. package/src/vendor/pi-tui/native/linux/src/linux-platform-x11.c +0 -274
  238. package/src/vendor/pi-tui/native/napi.h +0 -91
  239. package/src/vendor/pi-tui/native/win32/README.md +0 -31
  240. package/src/vendor/pi-tui/native/win32/build.mjs +0 -232
  241. package/src/vendor/pi-tui/native/win32/prebuilds/win32-arm64/win32-platform.node +0 -0
  242. package/src/vendor/pi-tui/native/win32/prebuilds/win32-x64/win32-platform.node +0 -0
  243. package/src/vendor/pi-tui/native/win32/src/win32-platform.c +0 -241
  244. package/src/vendor/pi-tui/native-modifiers.ts +0 -13
  245. package/src/vendor/pi-tui/native-module-path.ts +0 -31
  246. package/src/vendor/pi-tui/native-platform.ts +0 -65
  247. package/src/vendor/pi-tui/oklab.ts +0 -233
  248. package/src/vendor/pi-tui/stdin-buffer.ts +0 -444
  249. package/src/vendor/pi-tui/terminal-colors.ts +0 -91
  250. package/src/vendor/pi-tui/terminal-image.ts +0 -731
  251. package/src/vendor/pi-tui/terminal.ts +0 -554
  252. package/src/vendor/pi-tui/tui-alt-screen.ts +0 -1750
  253. package/src/vendor/pi-tui/tui-main-screen.ts +0 -655
  254. package/src/vendor/pi-tui/tui.ts +0 -1493
  255. package/src/vendor/pi-tui/undo-stack.ts +0 -28
  256. package/src/vendor/pi-tui/utils.ts +0 -1385
  257. package/src/vendor/pi-tui/wheel-scroll.ts +0 -82
  258. package/src/vendor/pi-tui/word-navigation.ts +0 -117
@@ -1,700 +0,0 @@
1
- /** Ported from Pi (MIT), coding-agent/theme/system-theme.ts at 02eed88. Only imports/types adapted. */
2
- /**
3
- * The `system` theme: pi's colors derived from the terminal's own theme.
4
- *
5
- * Every token belongs to a color family (its hue) and has contrast rules: it must reach a contrast level
6
- * on the background and on the panels it is drawn on. Hue and saturation come from the terminal's palette
7
- * color for the family's ANSI slot, or from the family's own hue when the terminal reports no palette.
8
- * Lightness comes from the rules alone. Colors are built in OKHSL, whose saturation is relative to the
9
- * sRGB gamut, and fade toward gray near black and white.
10
- *
11
- * A contrast level is a target-lightness curve: the OKLab lightness a token needs, given the lightness of
12
- * the surface below it. The curves were fitted to the reference theme design from the "Pi themes: system
13
- * and light/dark" review. On dark backgrounds they aim for nearly fixed lightness; on light backgrounds the
14
- * required difference grows as the background darkens.
15
- *
16
- * Depending on what the terminal reports, the theme is generated in one of three tiers:
17
- * - background and palette: hues from the palette, lightness from the background;
18
- * - background only: the families' own hues, lightness from the background;
19
- * - nothing: ANSI palette indices and the default colors, which the terminal renders itself.
20
- */
21
-
22
- import {
23
- colorToOkhsl,
24
- colorToOklch,
25
- type OkhslChannels,
26
- okhslColor,
27
- oklabToOkhslLightness,
28
- type RgbColor,
29
- rgbColor,
30
- } from "../vendor/pi-tui/index.ts";
31
- export type ThemeColor =
32
- | "accent"
33
- | "border"
34
- | "borderAccent"
35
- | "borderMuted"
36
- | "success"
37
- | "error"
38
- | "warning"
39
- | "muted"
40
- | "dim"
41
- | "text"
42
- | "thinkingText"
43
- | "scrollbarTrack"
44
- | "scrollbarThumb"
45
- | "searchMatchText"
46
- | "userMessageText"
47
- | "customMessageText"
48
- | "customMessageLabel"
49
- | "toolTitle"
50
- | "toolOutput"
51
- | "mdHeading"
52
- | "mdLink"
53
- | "mdLinkUrl"
54
- | "mdCode"
55
- | "mdCodeBlock"
56
- | "mdCodeBlockBorder"
57
- | "mdQuote"
58
- | "mdQuoteBorder"
59
- | "mdHr"
60
- | "mdListBullet"
61
- | "toolDiffAdded"
62
- | "toolDiffRemoved"
63
- | "toolDiffContext"
64
- | "syntaxComment"
65
- | "syntaxKeyword"
66
- | "syntaxFunction"
67
- | "syntaxVariable"
68
- | "syntaxString"
69
- | "syntaxNumber"
70
- | "syntaxType"
71
- | "syntaxOperator"
72
- | "syntaxPunctuation"
73
- | "thinkingOff"
74
- | "thinkingMinimal"
75
- | "thinkingLow"
76
- | "thinkingMedium"
77
- | "thinkingHigh"
78
- | "thinkingXhigh"
79
- | "thinkingMax"
80
- | "bashMode";
81
-
82
- export type ThemeBg =
83
- | "selectedBg"
84
- | "searchMatchBg"
85
- | "userMessageBg"
86
- | "customMessageBg"
87
- | "toolPendingBg"
88
- | "toolSuccessBg"
89
- | "toolErrorBg";
90
-
91
- export type ThemeToken = ThemeColor | ThemeBg;
92
-
93
-
94
- type ThemeAppearance = "dark" | "light";
95
-
96
- export const SYSTEM_THEME_NAME = "system";
97
-
98
- // ============================================================================
99
- // Recipe: color families and their tokens
100
- // ============================================================================
101
-
102
- /** A family's OKHSL hue and saturation: `max` at mid lightness, falling toward `min` at black and white. */
103
- interface Family {
104
- hue: number;
105
- saturation: { min: number; max: number };
106
- /** ANSI palette slot the family takes its hue and saturation from. */
107
- slot: number;
108
- }
109
-
110
- const FAMILIES = {
111
- neutral: { hue: 231.49, saturation: { min: 0.02, max: 0.08 }, slot: 8 },
112
- blue: { hue: 231.49, saturation: { min: 0.1, max: 0.68 }, slot: 4 },
113
- green: { hue: 158.68, saturation: { min: 0.1, max: 0.76 }, slot: 2 },
114
- red: { hue: 20, saturation: { min: 0.1, max: 0.92 }, slot: 1 },
115
- yellow: { hue: 82.36, saturation: { min: 0.5, max: 1 }, slot: 3 },
116
- orange: { hue: 52, saturation: { min: 0.12, max: 0.85 }, slot: 3 },
117
- violet: { hue: 295, saturation: { min: 0.2, max: 0.6 }, slot: 5 },
118
- calamine: { hue: 202.43, saturation: { min: 0.1, max: 0.74 }, slot: 6 },
119
- thinkingSlate: { hue: 231.49, saturation: { min: 0.08, max: 0.2 }, slot: 4 },
120
- thinkingBlue: { hue: 231.49, saturation: { min: 0.2, max: 0.45 }, slot: 4 },
121
- thinkingPeriwinkle: { hue: 263.25, saturation: { min: 0.3, max: 0.6 }, slot: 6 },
122
- thinkingViolet: { hue: 295, saturation: { min: 0.4, max: 0.75 }, slot: 5 },
123
- thinkingMagenta: { hue: 337.5, saturation: { min: 0.5, max: 0.85 }, slot: 13 },
124
- thinkingRed: { hue: 20, saturation: { min: 0.95, max: 1 }, slot: 1 },
125
- } satisfies Record<string, Family>;
126
-
127
- type FamilyName = keyof typeof FAMILIES;
128
-
129
- const TOKEN_FAMILIES: Record<ThemeToken, FamilyName> = {
130
- selectedBg: "blue",
131
- searchMatchBg: "orange",
132
- userMessageBg: "blue",
133
- customMessageBg: "violet",
134
- toolPendingBg: "neutral",
135
- toolSuccessBg: "green",
136
- toolErrorBg: "red",
137
-
138
- text: "neutral",
139
- userMessageText: "neutral",
140
- customMessageText: "neutral",
141
- toolTitle: "neutral",
142
- syntaxOperator: "neutral",
143
- syntaxPunctuation: "neutral",
144
- muted: "neutral",
145
- dim: "neutral",
146
- thinkingText: "neutral",
147
- toolOutput: "neutral",
148
- mdLinkUrl: "neutral",
149
- mdQuote: "neutral",
150
- mdQuoteBorder: "neutral",
151
- mdHr: "neutral",
152
- mdCodeBlockBorder: "neutral",
153
- toolDiffContext: "neutral",
154
- syntaxComment: "neutral",
155
- scrollbarTrack: "neutral",
156
- scrollbarThumb: "neutral",
157
- searchMatchText: "neutral",
158
- borderMuted: "neutral",
159
-
160
- accent: "violet",
161
- borderAccent: "violet",
162
- customMessageLabel: "violet",
163
- mdCode: "violet",
164
- mdListBullet: "violet",
165
- syntaxType: "violet",
166
- border: "blue",
167
- mdLink: "blue",
168
- syntaxKeyword: "blue",
169
- syntaxVariable: "calamine",
170
- success: "green",
171
- mdCodeBlock: "green",
172
- toolDiffAdded: "green",
173
- bashMode: "green",
174
- syntaxNumber: "green",
175
- error: "red",
176
- toolDiffRemoved: "red",
177
- warning: "yellow",
178
- mdHeading: "yellow",
179
- syntaxFunction: "yellow",
180
- syntaxString: "orange",
181
-
182
- thinkingOff: "neutral",
183
- thinkingMinimal: "thinkingSlate",
184
- thinkingLow: "thinkingBlue",
185
- thinkingMedium: "thinkingPeriwinkle",
186
- thinkingHigh: "thinkingViolet",
187
- thinkingXhigh: "thinkingMagenta",
188
- thinkingMax: "thinkingRed",
189
- };
190
-
191
- /** Palette slots for tokens that would otherwise share a hue with a similar token. */
192
- const TOKEN_SLOTS: Partial<Record<ThemeToken, number>> = { syntaxString: 2, syntaxNumber: 5, searchMatchBg: 3 };
193
-
194
- // ============================================================================
195
- // Contrast levels and rules
196
- // ============================================================================
197
-
198
- /**
199
- * Target-lightness curves: a polynomial in the surface's OKLab lightness giving the OKLab lightness a token
200
- * needs on it. `reachable` is the range of surface lightness where the level can be reached; beyond it the
201
- * level is relaxed.
202
- */
203
- interface Curve {
204
- coefficients: number[];
205
- reachable: [number, number];
206
- }
207
-
208
- const LEVELS = {
209
- panel: {
210
- dark: { coefficients: [0.29131, -0.39746, 2.33185, -0.85524, -1.2076, 0.86276], reachable: [0, 0.979] },
211
- light: { coefficients: [-3.74073, 27.94549, -78.44258, 112.6798, -79.60015, 22.11277], reachable: [0.348, 1] },
212
- },
213
- track: {
214
- dark: { coefficients: [0.39028, -0.23015, 0.83573, 2.43829, -4.38292, 2.01582], reachable: [0, 0.946] },
215
- light: { coefficients: [-5.24921, 38.37322, -107.28833, 152.10005, -106.17127, 29.18061], reachable: [0.368, 1] },
216
- },
217
- thinking0: {
218
- dark: { coefficients: [0.52988, -0.05809, -0.30924, 4.63567, -6.52933, 2.89108], reachable: [0, 0.873] },
219
- light: {
220
- coefficients: [-28.27749, 182.85284, -469.62416, 603.15916, -384.59976, 97.35147],
221
- reachable: [0.51, 1],
222
- },
223
- },
224
- thinking1: {
225
- dark: { coefficients: [0.55278, -0.03667, -0.45659, 4.95347, -6.90265, 3.0706], reachable: [0, 0.858] },
226
- light: {
227
- coefficients: [-37.10484, 235.86282, -596.62344, 754.3633, -474.00763, 118.3551],
228
- reachable: [0.535, 1],
229
- },
230
- },
231
- thinking2: {
232
- dark: { coefficients: [0.57486, -0.01765, -0.58987, 5.25227, -7.27175, 3.25532], reachable: [0, 0.842] },
233
- light: {
234
- coefficients: [-59.89653, 377.05024, -945.07843, 1182.03145, -734.96375, 181.68658],
235
- reachable: [0.556, 1],
236
- },
237
- },
238
- thinking3: {
239
- dark: { coefficients: [0.59621, -0.00062, -0.71148, 5.53588, -7.6392, 3.44606], reachable: [0, 0.827] },
240
- light: {
241
- coefficients: [-72.07122, 445.84082, -1099.57352, 1353.88793, -829.53392, 202.26164],
242
- reachable: [0.58, 1],
243
- },
244
- },
245
- thinking4: {
246
- dark: { coefficients: [0.61691, 0.01462, -0.82288, 5.80651, -8.00641, 3.64333], reachable: [0, 0.811] },
247
- light: {
248
- coefficients: [-110.14338, 674.21488, -1645.75941, 2004.32367, -1215.15899, 293.3183],
249
- reachable: [0.6, 1],
250
- },
251
- },
252
- thinking5: {
253
- dark: { coefficients: [0.63702, 0.02826, -0.92498, 6.06465, -8.37246, 3.84651], reachable: [0, 0.795] },
254
- light: {
255
- coefficients: [-175.47701, 1063.54495, -2570.70594, 3098.80776, -1860.15527, 444.76392],
256
- reachable: [0.62, 1],
257
- },
258
- },
259
- thinking6: {
260
- dark: { coefficients: [0.65658, 0.04044, -1.01835, 6.30989, -8.73529, 4.05439], reachable: [0, 0.779] },
261
- light: {
262
- coefficients: [-183.81712, 1094.70055, -2602.68539, 3088.71276, -1826.91131, 430.75931],
263
- reachable: [0.643, 1],
264
- },
265
- },
266
- subtle: {
267
- dark: { coefficients: [0.56762, -0.02475, -0.5383, 5.12628, -7.10931, 3.17324], reachable: [0, 0.848] },
268
- light: {
269
- coefficients: [-232.85459, 1376.54473, -3249.11801, 3827.91186, -2248.29472, 526.55751],
270
- reachable: [0.657, 1],
271
- },
272
- },
273
- thumb: {
274
- dark: { coefficients: [0.60323, 0.00278, -0.73328, 5.57157, -7.68067, 3.46933], reachable: [0, 0.823] },
275
- light: {
276
- coefficients: [-82.89897, 511.01355, -1255.98095, 1540.76821, -940.68087, 228.58523],
277
- reachable: [0.586, 1],
278
- },
279
- },
280
- readable: {
281
- dark: { coefficients: [0.66937, 0.04704, -1.06871, 6.43941, -8.9332, 4.17229], reachable: [0, 0.77] },
282
- light: {
283
- coefficients: [-1554.52576, 8733.56817, -19604.93507, 21977.72696, -12300.99599, 2749.81288],
284
- reachable: [0.751, 1],
285
- },
286
- },
287
- emphasis: {
288
- dark: { coefficients: [0.7303, 0.07695, -1.31626, 7.1681, -10.14436, 4.92846], reachable: [0, 0.712] },
289
- light: {
290
- coefficients: [-4948.31942, 26870.91986, -58334.48399, 63280.17197, -34298.01053, 7430.30146],
291
- reachable: [0.811, 1],
292
- },
293
- },
294
- textOnPanel: {
295
- dark: { coefficients: [0.86713, 0.05232, -0.89428, 4.79014, -5.5432, 1.75023], reachable: [0, 0.542] },
296
- light: {
297
- coefficients: [-8570.89457, 43954.60805, -90084.00702, 92220.6791, -47152.15802, 9632.27113],
298
- reachable: [0.867, 1],
299
- },
300
- },
301
- text: {
302
- dark: { coefficients: [0.89242, 0.02311, -0.44862, 2.34417, -0.06084, -2.63844], reachable: [0, 0.5] },
303
- light: {
304
- coefficients: [-2004.67048, 6664.47299, -6060.70202, -1792.61209, 5133.82359, -1939.85583],
305
- reachable: [0.894, 1],
306
- },
307
- },
308
- } satisfies Record<string, Record<ThemeAppearance, Curve>>;
309
-
310
- type Level = keyof typeof LEVELS;
311
-
312
- type Surface = ThemeBg | "background" | "scrollbarTrack";
313
-
314
- interface Rule {
315
- token: ThemeToken;
316
- on: Surface[];
317
- level: Level;
318
- }
319
-
320
- const TOOL_PANELS: Surface[] = ["toolPendingBg", "toolSuccessBg", "toolErrorBg"];
321
- const MESSAGE_PANELS: Surface[] = ["userMessageBg", "customMessageBg"];
322
- const PANELS: ThemeBg[] = [
323
- "userMessageBg",
324
- "toolPendingBg",
325
- "toolSuccessBg",
326
- "toolErrorBg",
327
- "selectedBg",
328
- "searchMatchBg",
329
- "customMessageBg",
330
- ];
331
- const THINKING: ThemeColor[] = [
332
- "thinkingOff",
333
- "thinkingMinimal",
334
- "thinkingLow",
335
- "thinkingMedium",
336
- "thinkingHigh",
337
- "thinkingXhigh",
338
- "thinkingMax",
339
- ];
340
- const THINKING_LEVELS: Level[] = [
341
- "thinking0",
342
- "thinking1",
343
- "thinking2",
344
- "thinking3",
345
- "thinking4",
346
- "thinking5",
347
- "thinking6",
348
- ];
349
-
350
- const each = (tokens: ThemeToken[], on: Surface[], level: Level): Rule[] =>
351
- tokens.map((token) => ({ token, on, level }));
352
-
353
- const RULES: Rule[] = [
354
- ...each(PANELS, ["background"], "panel"),
355
- { token: "text", on: ["background"], level: "text" },
356
- { token: "text", on: ["selectedBg"], level: "textOnPanel" },
357
- { token: "userMessageText", on: ["userMessageBg"], level: "textOnPanel" },
358
- { token: "toolTitle", on: TOOL_PANELS, level: "textOnPanel" },
359
- ...each(["accent", "success", "error", "warning"], ["background", "selectedBg", ...TOOL_PANELS], "readable"),
360
- { token: "muted", on: ["background", "selectedBg", "customMessageBg", ...TOOL_PANELS], level: "readable" },
361
- { token: "dim", on: ["background", "selectedBg", "customMessageBg", ...TOOL_PANELS], level: "subtle" },
362
- { token: "thinkingText", on: ["background"], level: "readable" },
363
- { token: "customMessageText", on: ["customMessageBg", ...TOOL_PANELS], level: "readable" },
364
- {
365
- token: "customMessageLabel",
366
- on: ["background", "customMessageBg", "selectedBg", ...TOOL_PANELS],
367
- level: "readable",
368
- },
369
- { token: "toolOutput", on: ["background", ...TOOL_PANELS], level: "readable" },
370
- ...each(
371
- ["mdHeading", "mdLink", "mdLinkUrl", "mdCode", "mdQuote", "mdCodeBlockBorder", "mdListBullet"],
372
- ["background", ...MESSAGE_PANELS],
373
- "readable",
374
- ),
375
- { token: "mdCodeBlock", on: ["background", ...MESSAGE_PANELS, ...TOOL_PANELS], level: "readable" },
376
- ...each(["toolDiffAdded", "toolDiffRemoved", "toolDiffContext"], ["background", ...TOOL_PANELS], "readable"),
377
- ...each(
378
- [
379
- "syntaxComment",
380
- "syntaxKeyword",
381
- "syntaxFunction",
382
- "syntaxVariable",
383
- "syntaxString",
384
- "syntaxNumber",
385
- "syntaxType",
386
- "syntaxOperator",
387
- "syntaxPunctuation",
388
- ],
389
- ["background", ...MESSAGE_PANELS, ...TOOL_PANELS],
390
- "readable",
391
- ),
392
- { token: "searchMatchText", on: ["searchMatchBg"], level: "readable" },
393
- ...each(["bashMode", "border", "borderAccent"], ["background"], "readable"),
394
- { token: "borderMuted", on: ["background"], level: "subtle" },
395
- ...each(["mdQuoteBorder", "mdHr"], ["background", ...MESSAGE_PANELS, ...TOOL_PANELS], "readable"),
396
- { token: "scrollbarTrack", on: ["background"], level: "track" },
397
- { token: "scrollbarThumb", on: ["scrollbarTrack"], level: "thumb" },
398
- ...THINKING.map((token, index): Rule => ({ token, on: ["background"], level: THINKING_LEVELS[index]! })),
399
- ];
400
-
401
- /** Relaxation compresses levels stronger than this one toward it before weakening all levels. */
402
- const READABLE_FLOOR: Record<ThemeAppearance, Level> = { dark: "readable", light: "subtle" };
403
-
404
- /** Body text uses the terminal's foreground when it reaches this level, which is clearly stronger than muted. */
405
- const FOREGROUND_LEVEL: Level = "emphasis";
406
-
407
- /** Text-level tokens that take the terminal's foreground. */
408
- const FOREGROUND_TOKENS: ThemeColor[] = ["text", "userMessageText", "toolTitle"];
409
-
410
- /** WCAG 2 contrast ratio that body text must reach on the surfaces it is drawn on. */
411
- const TEXT_MINIMUM_WCAG_CONTRAST = 4.5;
412
-
413
- /** Tokens in dependency order: every surface before the tokens drawn on it. */
414
- const SOLVE_ORDER: ThemeToken[] = (() => {
415
- const order: ThemeToken[] = [];
416
- const visit = (token: ThemeToken): void => {
417
- if (order.includes(token)) return;
418
- for (const rule of RULES) {
419
- if (rule.token !== token) continue;
420
- for (const surface of rule.on) if (surface !== "background") visit(surface);
421
- }
422
- order.push(token);
423
- };
424
- for (const rule of RULES) visit(rule.token);
425
- return order;
426
- })();
427
-
428
- // ============================================================================
429
- // Public API
430
- // ============================================================================
431
-
432
- export interface SystemThemeInput {
433
- foreground?: RgbColor;
434
- background?: RgbColor;
435
- /** ANSI colors 0-15. */
436
- palette?: RgbColor[];
437
- /** Saturation multiplier from 0 (grayscale) to 1. The first frame renders in grayscale until colors arrive. */
438
- saturation?: number;
439
- /** Appearance when the terminal did not report its background, e.g. from its light/dark report or COLORFGBG. */
440
- appearanceHint?: ThemeAppearance;
441
- }
442
-
443
- export interface SystemThemeColors {
444
- /** Hex colors, ANSI palette indices, or "" for the terminal default. */
445
- colors: Record<ThemeToken, string | number>;
446
- /** Foreground tokens rendered faint (SGR 2), for terminals that did not report colors. */
447
- dim: ThemeColor[];
448
- appearance: ThemeAppearance | undefined;
449
- }
450
-
451
- /** OKLab lightness of an sRGB color, 0-1. */
452
- function oklabLightness(color: RgbColor): number {
453
- return colorToOklch(rgbColor(color.r, color.g, color.b)).l;
454
- }
455
-
456
- /** WCAG 2 relative luminance. */
457
- export function relativeLuminance({ r, g, b }: RgbColor): number {
458
- const linear = (channel: number) => {
459
- const value = channel / 255;
460
- return value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;
461
- };
462
- return 0.2126 * linear(r) + 0.7152 * linear(g) + 0.0722 * linear(b);
463
- }
464
-
465
- /** WCAG 2 contrast ratio, 1-21. */
466
- export function wcagContrast(first: RgbColor, second: RgbColor): number {
467
- const a = relativeLuminance(first);
468
- const b = relativeLuminance(second);
469
- return (Math.max(a, b) + 0.05) / (Math.min(a, b) + 0.05);
470
- }
471
-
472
- /**
473
- * Whether a terminal is dark or light, from its reported colors: the direction of its own foreground
474
- * when text can be readable that way, otherwise dark when white text has more contrast on the
475
- * background than black text.
476
- */
477
- export function terminalAppearance(background: RgbColor, foreground?: RgbColor): ThemeAppearance {
478
- const white = { r: 255, g: 255, b: 255 };
479
- const black = { r: 0, g: 0, b: 0 };
480
- const whiteContrast = wcagContrast(white, background);
481
- const blackContrast = wcagContrast(black, background);
482
- if (foreground) {
483
- const foregroundL = oklabLightness(foreground);
484
- const backgroundL = oklabLightness(background);
485
- if (Math.abs(foregroundL - backgroundL) > 0.05) {
486
- const appearance = foregroundL > backgroundL ? "dark" : "light";
487
- const best = appearance === "dark" ? whiteContrast : blackContrast;
488
- if (best >= TEXT_MINIMUM_WCAG_CONTRAST) return appearance;
489
- }
490
- }
491
- return whiteContrast >= blackContrast ? "dark" : "light";
492
- }
493
-
494
- // ============================================================================
495
- // Generation
496
- // ============================================================================
497
-
498
- const clamp = (value: number, min: number, max: number) => Math.min(max, Math.max(min, value));
499
-
500
- function hexOf({ r, g, b }: RgbColor): string {
501
- return `#${[r, g, b].map((channel) => Math.round(channel).toString(16).padStart(2, "0")).join("")}`;
502
- }
503
-
504
- /** Saturation weight at a lightness: a Gaussian (center 0.5, sigma 0.25), 0 at black and white, 1 in the middle. */
505
- function bellWeight(lightness: number): number {
506
- const gaussian = (x: number): number => Math.exp(-((x - 0.5) ** 2) / (2 * 0.25 ** 2));
507
- return (gaussian(lightness) - gaussian(0)) / (1 - gaussian(0));
508
- }
509
-
510
- /** A family's saturation curve relative to its maximum: 1 at mid lightness, `min / max` at black and white. */
511
- function saturationCurve({ saturation: { min, max } }: Family, lightness: number): number {
512
- const floor = max > 0 ? min / max : 1;
513
- return floor + (1 - floor) * bellWeight(lightness);
514
- }
515
-
516
- /** The target lightness for a level on a surface, or undefined where the level cannot be reached. */
517
- function levelTarget(level: Level, appearance: ThemeAppearance, surfaceL: number): number | undefined {
518
- const curve: Curve = LEVELS[level][appearance];
519
- if (surfaceL < curve.reachable[0] || surfaceL > curve.reachable[1]) return undefined;
520
- return curve.coefficients.reduce((sum, coefficient, power) => sum + coefficient * surfaceL ** power, 0);
521
- }
522
-
523
- /**
524
- * Generate the system theme's colors from the terminal's reported colors.
525
- */
526
- export function generateSystemThemeColors(input: SystemThemeInput): SystemThemeColors {
527
- const saturation = clamp(input.saturation ?? 1, 0, 1);
528
- const { background, foreground } = input;
529
- if (!background) return indexedColors(saturation, input.appearanceHint);
530
- const palette = input.palette?.length === 16 ? input.palette.map(okhslOf) : undefined;
531
-
532
- const appearance = terminalAppearance(background, foreground);
533
- const lighter = appearance === "dark";
534
- const extreme = lighter ? 1 : 0;
535
- const backgroundL = oklabLightness(background);
536
-
537
- /**
538
- * A token's color at an OKLab lightness. With a palette, the palette color's saturation applies at its
539
- * own lightness and falls off toward black and white along the family's curve, never rising above it.
540
- */
541
- const paint = (token: ThemeToken, oklabL: number): RgbColor => {
542
- const lightness = oklabToOkhslLightness(oklabL);
543
- const family: Family = FAMILIES[TOKEN_FAMILIES[token]];
544
- if (!palette) {
545
- const { min, max } = family.saturation;
546
- return okhslColor(family.hue, (min + (max - min) * bellWeight(lightness)) * saturation, lightness);
547
- }
548
- return anchored(palette[TOKEN_SLOTS[token] ?? family.slot]!, family, lightness, saturation);
549
- };
550
-
551
- /**
552
- * The lightness a rule needs on a surface, relaxed by `t`: from 0 to 1, levels stronger than the readable
553
- * floor move toward it; from 1 to 2, all levels move toward the surface itself.
554
- */
555
- const target = (level: Level, surfaceL: number, t: number): number | undefined => {
556
- const reached = levelTarget(level, appearance, surfaceL);
557
- if (reached === undefined && t === 0) return undefined;
558
- const distance = (reached ?? extreme) - surfaceL;
559
- const floor = (levelTarget(READABLE_FLOOR[appearance], appearance, surfaceL) ?? extreme) - surfaceL;
560
- const compressed =
561
- Math.abs(distance) > Math.abs(floor) ? distance - (distance - floor) * Math.min(t, 1) : distance;
562
- return surfaceL + compressed * (1 - Math.max(0, t - 1));
563
- };
564
-
565
- /**
566
- * Keep a panel light enough (or dark enough) that white (or black) text still reaches the body text
567
- * minimum on it. This only matters for backgrounds near mid-gray, where it barely does on the background.
568
- */
569
- const extremeText = lighter ? { r: 255, g: 255, b: 255 } : { r: 0, g: 0, b: 0 };
570
- const readable = (color: RgbColor) => wcagContrast(extremeText, color) >= TEXT_MINIMUM_WCAG_CONTRAST;
571
- const limitPanel = (token: ThemeToken, l: number): RgbColor => {
572
- const color = paint(token, l);
573
- if (readable(color)) return color;
574
- let [low, high] = [backgroundL, l];
575
- for (let index = 0; index < 20; index++) {
576
- const middle = (low + high) / 2;
577
- if (readable(paint(token, middle))) low = middle;
578
- else high = middle;
579
- }
580
- return paint(token, low);
581
- };
582
-
583
- const solve = (t: number): Map<Surface | ThemeToken, RgbColor> | undefined => {
584
- const colors = new Map<Surface | ThemeToken, RgbColor>([["background", background]]);
585
- for (const token of SOLVE_ORDER) {
586
- const targets: number[] = [];
587
- for (const rule of RULES) {
588
- if (rule.token !== token) continue;
589
- for (const surface of rule.on) {
590
- const value = target(rule.level, oklabLightness(colors.get(surface) ?? background), t);
591
- if (value === undefined || value < 0 || value > 1) return undefined;
592
- targets.push(value);
593
- }
594
- }
595
- const l = lighter ? Math.max(...targets) : Math.min(...targets);
596
- colors.set(token, PANELS.includes(token as ThemeBg) ? limitPanel(token, l) : paint(token, l));
597
- }
598
- return colors;
599
- };
600
-
601
- let relaxation = 0;
602
- let colors = solve(0);
603
- if (!colors) {
604
- // Mid-gray backgrounds cannot fit every level: relax as little as possible. Full relaxation always fits.
605
- let [low, high] = [0, 2];
606
- colors = solve(high);
607
- for (let index = 0; index < 20; index++) {
608
- const middle = (low + high) / 2;
609
- const attempt = solve(middle);
610
- if (attempt) [high, colors] = [middle, attempt];
611
- else low = middle;
612
- }
613
- relaxation = high;
614
- }
615
- const solved = colors ?? new Map<Surface | ThemeToken, RgbColor>();
616
- const surfacesOf = (token: ThemeToken) =>
617
- RULES.filter((rule) => rule.token === token).flatMap((rule) =>
618
- rule.on.map((surface) => solved.get(surface) ?? background),
619
- );
620
-
621
- const result = {} as Record<ThemeToken, string | number>;
622
- for (const token of Object.keys(TOKEN_FAMILIES) as ThemeToken[]) {
623
- const color = solved.get(token);
624
- result[token] = color ? hexOf(color) : "";
625
- }
626
-
627
- for (const token of FOREGROUND_TOKENS) {
628
- const surfaces = surfacesOf(token);
629
- // Body text uses the terminal's own foreground where it is clearly stronger than muted text; otherwise
630
- // the foreground's hue at just enough lightness.
631
- let text = solved.get(token);
632
- if (foreground) {
633
- const targets = surfaces.map((surface) => target(FOREGROUND_LEVEL, oklabLightness(surface), relaxation));
634
- if (targets.every((value) => value !== undefined && value >= 0 && value <= 1)) {
635
- const needed = lighter ? Math.max(...(targets as number[])) : Math.min(...(targets as number[]));
636
- const foregroundL = oklabLightness(foreground);
637
- if (lighter ? foregroundL >= needed : foregroundL <= needed) {
638
- result[token] = "";
639
- continue;
640
- }
641
- text = anchored(okhslOf(foreground), FAMILIES.neutral, oklabToOkhslLightness(needed), saturation);
642
- }
643
- }
644
- // Body text keeps at least 4.5:1 on the surfaces it is drawn on, even on relaxed mid-gray backgrounds.
645
- if (text) result[token] = hexOf(withTextContrast(text, surfaces, lighter));
646
- }
647
- return { colors: result, dim: [], appearance };
648
- }
649
-
650
- function okhslOf({ r, g, b }: RgbColor): OkhslChannels {
651
- return colorToOkhsl(rgbColor(r, g, b));
652
- }
653
-
654
- /**
655
- * A source color's hue at another OKHSL lightness. Its saturation applies at its own lightness and falls off
656
- * toward black and white along the family's saturation curve, never rising above it.
657
- */
658
- function anchored(source: OkhslChannels, family: Family, lightness: number, saturation: number): RgbColor {
659
- const anchor = saturationCurve(family, source.l);
660
- const falloff = anchor > 0 ? Math.min(1, saturationCurve(family, lightness) / anchor) : 1;
661
- return okhslColor(source.h, source.s * falloff * saturation, lightness);
662
- }
663
-
664
- /** Move a text color toward white or black until it reaches the WCAG minimum on every surface. */
665
- function withTextContrast(color: RgbColor, surfaces: RgbColor[], lighter: boolean): RgbColor {
666
- const meets = (candidate: RgbColor) =>
667
- surfaces.every((surface) => wcagContrast(candidate, surface) >= TEXT_MINIMUM_WCAG_CONTRAST);
668
- if (meets(color)) return color;
669
- const { h, s, l } = okhslOf(color);
670
- const at = (lightness: number) => okhslColor(h, s, lightness);
671
- const extreme = lighter ? 1 : 0;
672
- if (!meets(at(extreme))) return at(extreme);
673
- let [low, high] = [l, extreme];
674
- for (let index = 0; index < 20; index++) {
675
- const middle = (low + high) / 2;
676
- if (meets(at(middle))) high = middle;
677
- else low = middle;
678
- }
679
- return at(high);
680
- }
681
-
682
- /**
683
- * Colors for terminals that reported nothing: the terminal renders ANSI indices 0-15 and the default
684
- * colors with its own theme, so they fit any background. Neutral tokens below body text are faint (SGR 2)
685
- * instead of bright black, which some themes make nearly invisible. Panels have no background.
686
- */
687
- function indexedColors(saturation: number, appearance: ThemeAppearance | undefined): SystemThemeColors {
688
- const colors = {} as Record<ThemeToken, string | number>;
689
- const dim: ThemeColor[] = [];
690
- for (const [token, familyName] of Object.entries(TOKEN_FAMILIES) as [ThemeToken, FamilyName][]) {
691
- if (PANELS.includes(token as ThemeBg)) {
692
- colors[token] = "";
693
- continue;
694
- }
695
- const neutral = familyName === "neutral";
696
- colors[token] = !neutral && saturation > 0 ? (TOKEN_SLOTS[token] ?? FAMILIES[familyName].slot) : "";
697
- if (neutral && !FOREGROUND_TOKENS.includes(token as ThemeColor)) dim.push(token as ThemeColor);
698
- }
699
- return { colors, dim, appearance };
700
- }