@opencode/theme 2.0.7 → 2.0.9

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.
@@ -1,11 +1,36 @@
1
1
  import { RGBA } from "@opentui/core";
2
2
  import { oklchToHex, rgbToOklch } from "./color.js";
3
- import { DEFAULT_CATEGORICAL, DEFAULT_THEME } from "./defaults.js";
3
+ import { DEFAULT_CATEGORICAL } from "./categorical.js";
4
4
  import { HueStep } from "./schema.js";
5
5
  const chromaticHues = ["red", "orange", "yellow", "green", "cyan", "blue", "purple"];
6
6
  const categoricalTokens = ["secondary", "accent", "success", "warning", "primary", "error"];
7
7
  const minimumChroma = 0.03;
8
8
  const lightThreshold = 0.6;
9
+ // Canonical swatches copied from the original default-theme classifier keep V1 migration self-contained.
10
+ const hueReferences = {
11
+ light: {
12
+ red: "#fca5a5",
13
+ orange: "#fdba74",
14
+ yellow: "#fde047",
15
+ green: "#86efac",
16
+ cyan: "#67e8f9",
17
+ blue: "#93c5fd",
18
+ purple: "#d8b4fe",
19
+ },
20
+ dark: {
21
+ red: "#b91c1c",
22
+ orange: "#c2410c",
23
+ yellow: "#a16207",
24
+ green: "#15803d",
25
+ cyan: "#0e7490",
26
+ blue: "#1d4ed8",
27
+ purple: "#7e22ce",
28
+ },
29
+ };
30
+ const hueAngles = Object.fromEntries(Object.entries(hueReferences).map(([level, colors]) => [
31
+ level,
32
+ Object.fromEntries(Object.entries(colors).map(([name, color]) => [name, toOklch(RGBA.fromHex(color)).h])),
33
+ ]));
9
34
  export function migrateV1(theme) {
10
35
  const light = resolveV1(theme, "light");
11
36
  const dark = resolveV1(theme, "dark");
@@ -13,18 +38,24 @@ export function migrateV1(theme) {
13
38
  const lightMode = detectMode(light);
14
39
  const darkMode = detectMode(dark);
15
40
  if (lightMode === darkMode) {
41
+ const definition = migrateMode(lightMode === "light" ? light : dark, lightMode);
16
42
  if (lightMode === "light")
17
- return { version: 2, standalone: true, light: migrateMode(light, "light") };
18
- return { version: 2, standalone: true, dark: migrateMode(dark, "dark") };
43
+ return { base: base(definition), light: { hue: definition.hue } };
44
+ return { base: base(definition), dark: { hue: definition.hue } };
19
45
  }
20
46
  }
47
+ const lightDefinition = migrateMode(light, "light");
48
+ const darkDefinition = migrateMode(dark, "dark");
21
49
  return {
22
- version: 2,
23
- standalone: true,
24
- light: migrateMode(light, "light"),
25
- dark: migrateMode(dark, "dark"),
50
+ base: base(lightDefinition),
51
+ light: { hue: lightDefinition.hue },
52
+ dark: darkDefinition,
26
53
  };
27
54
  }
55
+ function base(definition) {
56
+ const { hue: _, ...base } = definition;
57
+ return base;
58
+ }
28
59
  function detectMode(theme) {
29
60
  return luminance(theme.text) > luminance(theme.background) ? "dark" : "light";
30
61
  }
@@ -35,21 +66,22 @@ function migrateMode(theme, mode) {
35
66
  const color = (key) => hex(theme[key]);
36
67
  const selected = hex(selectedForeground(theme, theme.primary));
37
68
  const destructive = hex(selectedForeground(theme, theme.error));
38
- const hues = inferHues(theme, mode);
69
+ const hues = inferHues(theme);
39
70
  const categorical = categoricalTokens.flatMap((token) => {
40
71
  const hue = hues.byToken[token];
41
72
  return hue ? [hue] : [];
42
73
  });
43
74
  const uniqueCategorical = categorical.filter((hue, index) => categorical.indexOf(hue) === index);
44
- const text = mode === "light" ? "$hue.neutral.800" : "$hue.neutral.200";
45
- const textMuted = mode === "light" ? "$hue.neutral.600" : "$hue.neutral.400";
46
- const primary = mode === "light" ? "$hue.interactive.800" : "$hue.interactive.200";
47
- const background = mode === "light" ? "$hue.neutral.200" : "$hue.neutral.800";
48
- const backgroundPanel = mode === "light" ? "$hue.neutral.300" : "$hue.neutral.700";
49
- const backgroundMenu = mode === "light" ? "$hue.neutral.400" : "$hue.neutral.600";
75
+ const text = "$hue.neutral.200";
76
+ const textMuted = "$hue.neutral.400";
77
+ const primary = "$hue.interactive.200";
78
+ const background = "$hue.neutral.800";
79
+ const backgroundPanel = "$hue.neutral.700";
80
+ const backgroundMenu = "$hue.neutral.600";
81
+ const backgroundRaisedMax = "$hue.neutral.500";
50
82
  return referenceHues({
51
83
  hue: {
52
- gray: neutralScale(theme, mode),
84
+ gray: neutralScale(theme),
53
85
  ...Object.fromEntries(chromaticHues.map((name) => {
54
86
  const match = hues.byHue[name];
55
87
  return [name, match ? hueScale(match.color, mode) : "$hue.gray"];
@@ -60,20 +92,20 @@ function migrateMode(theme, mode) {
60
92
  },
61
93
  categorical: uniqueCategorical.length ? uniqueCategorical : DEFAULT_CATEGORICAL,
62
94
  text: {
63
- default: text,
64
- subdued: textMuted,
95
+ base: text,
96
+ muted: textMuted,
65
97
  action: {
66
98
  primary: {
67
- default: "$text.default",
99
+ base: "$text.base",
68
100
  $disabled: textMuted,
69
101
  $focused: selected,
70
102
  $selected: primary,
71
103
  },
72
- secondary: { default: "$text.subdued", $hovered: "$text.default" },
73
- destructive: { default: destructive, $disabled: textMuted },
104
+ secondary: { base: "$text.muted", $hovered: "$text.base" },
105
+ destructive: { base: destructive, $disabled: textMuted },
74
106
  },
75
107
  formfield: {
76
- default: text,
108
+ base: text,
77
109
  $hovered: primary,
78
110
  $focused: primary,
79
111
  $pressed: primary,
@@ -81,35 +113,36 @@ function migrateMode(theme, mode) {
81
113
  $selected: primary,
82
114
  },
83
115
  feedback: {
84
- error: { default: color("error") },
85
- warning: { default: color("warning") },
86
- success: { default: color("success") },
87
- info: { default: color("info") },
116
+ error: { base: color("error") },
117
+ warning: { base: color("warning") },
118
+ success: { base: color("success") },
119
+ info: { base: color("info") },
88
120
  },
89
121
  },
90
122
  background: {
91
- default: background,
92
- surface: {
93
- offset: backgroundPanel,
94
- overlay: backgroundMenu,
123
+ base: background,
124
+ raised: {
125
+ base: backgroundPanel,
126
+ high: backgroundMenu,
127
+ max: backgroundRaisedMax,
95
128
  },
96
129
  action: {
97
- primary: { default: "transparent", $hovered: backgroundPanel, $focused: primary, $selected: "transparent" },
98
- secondary: { default: "transparent" },
99
- destructive: { default: color("error") },
130
+ primary: { base: "transparent", $hovered: backgroundPanel, $focused: primary, $selected: "transparent" },
131
+ secondary: { base: "transparent" },
132
+ destructive: { base: color("error") },
100
133
  },
101
134
  formfield: {
102
- default: "$background.default",
135
+ base: "$background.base",
103
136
  },
104
137
  feedback: {
105
- error: { default: "$background.default" },
106
- warning: { default: "$background.default" },
107
- success: { default: "$background.default" },
108
- info: { default: "$background.default" },
138
+ error: { base: "$background.base" },
139
+ warning: { base: "$background.base" },
140
+ success: { base: "$background.base" },
141
+ info: { base: "$background.base" },
109
142
  },
110
143
  },
111
- border: { default: color("border") },
112
- scrollbar: { default: color("borderActive") },
144
+ border: { base: color("border") },
145
+ scrollbar: { base: color("borderActive") },
113
146
  diff: {
114
147
  text: {
115
148
  added: color("diffAdded"),
@@ -158,13 +191,12 @@ function migrateMode(theme, mode) {
158
191
  imageText: color("markdownImageText"),
159
192
  codeBlock: color("markdownCodeBlock"),
160
193
  },
161
- "@context:elevated": {
194
+ "@dialog": {
162
195
  background: {
163
- default: "$background.surface.offset",
164
- action: { primary: { $hovered: "$background.surface.overlay" } },
196
+ base: "$background.raised.base",
197
+ action: { primary: { $hovered: "$background.raised.high" } },
165
198
  },
166
199
  },
167
- "@context:overlay": { background: { default: "$background.surface.overlay" } },
168
200
  });
169
201
  }
170
202
  function referenceHues(theme) {
@@ -219,7 +251,7 @@ function referenceHues(theme) {
219
251
  }
220
252
  return Object.fromEntries(Object.entries(theme).map(([key, value]) => [key, key === "hue" || key === "categorical" ? value : replace(value)]));
221
253
  }
222
- function inferHues(theme, mode) {
254
+ function inferHues(theme) {
223
255
  const colors = [
224
256
  ["accent", theme.accent],
225
257
  ["success", theme.success],
@@ -230,7 +262,7 @@ function inferHues(theme, mode) {
230
262
  ["secondary", theme.secondary],
231
263
  ];
232
264
  const inferred = colors.reduce((result, [token, color]) => {
233
- const nearest = inferHue(color, mode);
265
+ const nearest = inferHue(color);
234
266
  if (!nearest)
235
267
  return result;
236
268
  const current = result.byHue[nearest.name];
@@ -245,7 +277,7 @@ function inferHues(theme, mode) {
245
277
  ["accent", theme.accent],
246
278
  ["primary", theme.primary],
247
279
  ].reduce((result, [token, color]) => {
248
- const nearest = inferHue(color, mode);
280
+ const nearest = inferHue(color);
249
281
  if (!nearest)
250
282
  return result;
251
283
  return {
@@ -254,21 +286,18 @@ function inferHues(theme, mode) {
254
286
  };
255
287
  }, inferred);
256
288
  }
257
- function inferHue(color, mode) {
289
+ function inferHue(color) {
258
290
  const value = toOklch(color);
259
291
  if (ambiguous(color, value.c))
260
292
  return;
261
- const anchor = inferenceAnchor(value.l);
293
+ const reference = value.l >= lightThreshold ? hueAngles.light : hueAngles.dark;
262
294
  return chromaticHues
263
295
  .map((name) => ({
264
296
  name,
265
- distance: hueDistance(value.h, toOklch(RGBA.fromHex(DEFAULT_THEME[mode].hue[name][anchor])).h),
297
+ distance: hueDistance(value.h, reference[name]),
266
298
  }))
267
299
  .sort((first, second) => first.distance - second.distance)[0];
268
300
  }
269
- function inferenceAnchor(lightness) {
270
- return lightness >= lightThreshold ? 300 : 700;
271
- }
272
301
  function hueDistance(first, second) {
273
302
  const difference = Math.abs(first - second);
274
303
  return Math.min(difference, 360 - difference);
@@ -326,13 +355,13 @@ function selectedForeground(theme, background) {
326
355
  }
327
356
  function hueScale(color, mode) {
328
357
  const value = toOklch(color);
329
- const anchor = mode === "light" ? 800 : 200;
358
+ const anchor = 200;
330
359
  const endpoint = mode === "light" ? Math.max(0.97, value.l) : Math.min(0.18, value.l);
331
360
  const alpha = color.toInts()[3];
332
361
  return Object.fromEntries(HueStep.literals.map((step) => {
333
362
  if (step === anchor)
334
363
  return [step, hex(color)];
335
- const progress = mode === "light" ? (anchor - step) / (anchor - 100) : (step - anchor) / (900 - anchor);
364
+ const progress = (step - anchor) / (900 - anchor);
336
365
  const generated = oklchToHex({
337
366
  l: value.l + (endpoint - value.l) * progress,
338
367
  c: value.c * (1 - progress * 0.5),
@@ -341,8 +370,8 @@ function hueScale(color, mode) {
341
370
  return [step, alpha === 255 ? generated : `${generated}${byte(alpha)}`];
342
371
  }));
343
372
  }
344
- function neutralScale(theme, mode) {
345
- const anchors = neutralAnchors(theme, mode);
373
+ function neutralScale(theme) {
374
+ const anchors = neutralAnchors(theme);
346
375
  return Object.fromEntries(HueStep.literals.map((step) => {
347
376
  const exact = anchors.find((anchor) => anchor.step === step);
348
377
  if (exact)
@@ -357,7 +386,7 @@ function neutralScale(theme, mode) {
357
386
  return [step, interpolate(lower.color, upper.color, (step - lower.step) / (upper.step - lower.step))];
358
387
  }));
359
388
  }
360
- function neutralAnchors(theme, mode) {
389
+ function neutralAnchors(theme) {
361
390
  const light = [
362
391
  { step: 200, color: theme.background },
363
392
  { step: 300, color: theme.backgroundPanel },
@@ -365,8 +394,6 @@ function neutralAnchors(theme, mode) {
365
394
  { step: 600, color: theme.textMuted },
366
395
  { step: 800, color: theme.text },
367
396
  ];
368
- if (mode === "light")
369
- return light;
370
397
  return light.toReversed().map((source) => ({ ...source, step: (1000 - source.step) }));
371
398
  }
372
399
  function interpolate(first, second, amount) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "@opencode/theme",
4
- "version": "2.0.7",
4
+ "version": "2.0.9",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "repository": {