@opencode/ui 2.0.23 → 2.0.25

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 (82) hide show
  1. package/dist/context/marked-parser.d.ts +2 -0
  2. package/dist/i18n/en.d.ts +1 -0
  3. package/dist/icons/catalog.d.ts +36 -0
  4. package/dist/navigation/segmented-control/segmented-control.d.ts +21 -2
  5. package/package.json +2 -1
  6. package/src/actions/button/button.css +11 -9
  7. package/src/actions/button/button.tsx +1 -0
  8. package/src/actions/icon-button/icon-button.css +1 -5
  9. package/src/actions/icon-button/icon-button.tsx +1 -0
  10. package/src/actions/split-button/split-button.tsx +3 -0
  11. package/src/actions/submit.css +14 -12
  12. package/src/components/animated-number.tsx +13 -0
  13. package/src/components/app-icon.tsx +2 -0
  14. package/src/components/card.tsx +21 -0
  15. package/src/components/collapsible.tsx +1 -0
  16. package/src/components/context-menu.tsx +34 -0
  17. package/src/components/dock-surface.tsx +3 -0
  18. package/src/components/file-icon.tsx +12 -0
  19. package/src/components/image-preview.tsx +1 -0
  20. package/src/components/list.tsx +32 -0
  21. package/src/components/motion-spring.tsx +5 -0
  22. package/src/components/popover.tsx +12 -0
  23. package/src/components/provider-icon.tsx +1 -0
  24. package/src/components/resize-handle.tsx +8 -0
  25. package/src/components/scroll-view.tsx +39 -0
  26. package/src/components/spinner.tsx +2 -0
  27. package/src/components/text-field.tsx +6 -0
  28. package/src/components/text-reveal.tsx +19 -0
  29. package/src/components/text-strikethrough.tsx +8 -0
  30. package/src/components/typewriter.tsx +4 -0
  31. package/src/context/dialog.tsx +12 -0
  32. package/src/context/file.tsx +1 -0
  33. package/src/context/helper.tsx +4 -0
  34. package/src/context/i18n.tsx +20 -0
  35. package/src/context/index.ts +3 -0
  36. package/src/context/marked-base.tsx +9 -1
  37. package/src/context/marked-parser.tsx +57 -18
  38. package/src/context/marked.tsx +3 -0
  39. package/src/context/worker-pool.tsx +2 -0
  40. package/src/data-display/avatar/avatar.tsx +4 -0
  41. package/src/data-display/badge/badge.tsx +1 -0
  42. package/src/data-display/diff-changes/diff-changes.tsx +2 -0
  43. package/src/data-display/keybind/keybind.tsx +1 -0
  44. package/src/data-display/line-comment/line-comment.tsx +22 -0
  45. package/src/data-display/project-avatar/project-avatar.tsx +10 -0
  46. package/src/feedback/loader/loader.tsx +1 -0
  47. package/src/feedback/toast/description.ts +1 -0
  48. package/src/forms/checkbox/checkbox.tsx +1 -0
  49. package/src/forms/field/field.tsx +8 -0
  50. package/src/forms/inline-input/inline-input.tsx +4 -0
  51. package/src/forms/radio/radio.tsx +2 -0
  52. package/src/forms/select/select.tsx +11 -0
  53. package/src/forms/switch/switch.tsx +1 -0
  54. package/src/forms/text-input/text-input.tsx +3 -0
  55. package/src/hooks/create-auto-scroll.tsx +34 -0
  56. package/src/hooks/index.ts +1 -0
  57. package/src/hooks/use-filtered-list.tsx +14 -0
  58. package/src/i18n/en.ts +1 -0
  59. package/src/i18n/no.ts +1 -0
  60. package/src/icons/catalog.ts +36 -0
  61. package/src/icons/icon/icon.tsx +2 -0
  62. package/src/layout/divider/divider.tsx +1 -0
  63. package/src/navigation/menu/menu.tsx +13 -0
  64. package/src/navigation/segmented-control/segmented-control.css +37 -4
  65. package/src/navigation/segmented-control/segmented-control.tsx +210 -14
  66. package/src/navigation/tab-state-indicator.tsx +1 -0
  67. package/src/navigation/tabs/tabs-current.css +1 -5
  68. package/src/navigation/tabs/tabs.css +5 -22
  69. package/src/navigation/tabs/tabs.tsx +18 -0
  70. package/src/overlays/dialog/dialog.tsx +3 -0
  71. package/src/overlays/tooltip/tooltip.tsx +15 -0
  72. package/src/storybook/scaffold.tsx +3 -0
  73. package/src/styles/theme.css +1 -1
  74. package/src/theme/color.ts +27 -0
  75. package/src/theme/context.tsx +58 -0
  76. package/src/theme/default-themes.ts +35 -0
  77. package/src/theme/index.ts +3 -0
  78. package/src/theme/loader.ts +11 -0
  79. package/src/theme/resolve.ts +28 -0
  80. package/src/theme/v2/foreground.ts +8 -0
  81. package/src/theme/v2/resolve.ts +10 -0
  82. package/src/typography/text-shimmer/text-shimmer.tsx +1 -0
@@ -37,40 +37,75 @@ import vesperThemeJson from "./themes/vesper.json"
37
37
  import zenburnThemeJson from "./themes/zenburn.json"
38
38
 
39
39
  export const oc2Theme = oc2ThemeJson as DesktopTheme
40
+
40
41
  export const amoledTheme = amoledThemeJson as DesktopTheme
42
+
41
43
  export const auraTheme = auraThemeJson as DesktopTheme
44
+
42
45
  export const ayuTheme = ayuThemeJson as DesktopTheme
46
+
43
47
  export const carbonfoxTheme = carbonfoxThemeJson as DesktopTheme
48
+
44
49
  export const catppuccinTheme = catppuccinThemeJson as DesktopTheme
50
+
45
51
  export const catppuccinFrappeTheme = catppuccinFrappeThemeJson as DesktopTheme
52
+
46
53
  export const catppuccinMacchiatoTheme = catppuccinMacchiatoThemeJson as DesktopTheme
54
+
47
55
  export const cobalt2Theme = cobalt2ThemeJson as DesktopTheme
56
+
48
57
  export const cursorTheme = cursorThemeJson as DesktopTheme
58
+
49
59
  export const draculaTheme = draculaThemeJson as DesktopTheme
60
+
50
61
  export const everforestTheme = everforestThemeJson as DesktopTheme
62
+
51
63
  export const flexokiTheme = flexokiThemeJson as DesktopTheme
64
+
52
65
  export const githubTheme = githubThemeJson as DesktopTheme
66
+
53
67
  export const gruvboxTheme = gruvboxThemeJson as DesktopTheme
68
+
54
69
  export const kanagawaTheme = kanagawaThemeJson as DesktopTheme
70
+
55
71
  export const lucentOrngTheme = lucentOrngThemeJson as DesktopTheme
72
+
56
73
  export const materialTheme = materialThemeJson as DesktopTheme
74
+
57
75
  export const matrixTheme = matrixThemeJson as DesktopTheme
76
+
58
77
  export const mercuryTheme = mercuryThemeJson as DesktopTheme
78
+
59
79
  export const monokaiTheme = monokaiThemeJson as DesktopTheme
80
+
60
81
  export const nightowlTheme = nightowlThemeJson as DesktopTheme
82
+
61
83
  export const nordTheme = nordThemeJson as DesktopTheme
84
+
62
85
  export const oneDarkTheme = oneDarkThemeJson as DesktopTheme
86
+
63
87
  export const oneDarkProTheme = oneDarkProThemeJson as DesktopTheme
88
+
64
89
  export const orngTheme = orngThemeJson as DesktopTheme
90
+
65
91
  export const osakaJadeTheme = osakaJadeThemeJson as DesktopTheme
92
+
66
93
  export const palenightTheme = palenightThemeJson as DesktopTheme
94
+
67
95
  export const rosepineTheme = rosepineThemeJson as DesktopTheme
96
+
68
97
  export const shadesOfPurpleTheme = shadesOfPurpleThemeJson as DesktopTheme
98
+
69
99
  export const solarizedTheme = solarizedThemeJson as DesktopTheme
100
+
70
101
  export const synthwave84Theme = synthwave84ThemeJson as DesktopTheme
102
+
71
103
  export const tokyonightTheme = tokyonightThemeJson as DesktopTheme
104
+
72
105
  export const vercelTheme = vercelThemeJson as DesktopTheme
106
+
73
107
  export const vesperTheme = vesperThemeJson as DesktopTheme
108
+
74
109
  export const zenburnTheme = zenburnThemeJson as DesktopTheme
75
110
 
76
111
  export const DEFAULT_THEMES: Record<string, DesktopTheme> = {
@@ -32,8 +32,11 @@ export {
32
32
  } from "./color"
33
33
 
34
34
  export { resolveThemeVariant, resolveTheme, themeToCss } from "./resolve"
35
+
35
36
  export { resolveThemeVariantV2, resolveThemeV2, themeV2ToCss, generateV2Primitives } from "./v2/resolve"
37
+
36
38
  export { applyTheme, loadThemeFromUrl, getActiveTheme, removeTheme, setColorScheme } from "./loader"
39
+
37
40
  export { ThemeProvider, useTheme, type ColorScheme } from "./context"
38
41
 
39
42
  export {
@@ -3,16 +3,20 @@ import { resolveThemeVariant, themeToCss } from "./resolve"
3
3
  import { resolveThemeVariantV2, themeV2ToCss } from "./v2/resolve"
4
4
 
5
5
  let activeTheme: DesktopTheme | null = null
6
+
6
7
  const THEME_STYLE_ID = "opencode-theme"
7
8
 
8
9
  function ensureLoaderStyleElement(): HTMLStyleElement {
9
10
  const existing = document.getElementById(THEME_STYLE_ID) as HTMLStyleElement | null
11
+
10
12
  if (existing) {
11
13
  return existing
12
14
  }
15
+
13
16
  const element = document.createElement("style")
14
17
  element.id = THEME_STYLE_ID
15
18
  document.head.appendChild(element)
19
+
16
20
  return element
17
21
  }
18
22
 
@@ -77,29 +81,36 @@ html[data-theme="${themeId}"] {
77
81
 
78
82
  export async function loadThemeFromUrl(url: string): Promise<DesktopTheme> {
79
83
  const response = await fetch(url)
84
+
80
85
  if (!response.ok) {
81
86
  throw new Error(`Failed to load theme from ${url}: ${response.statusText}`)
82
87
  }
88
+
83
89
  return response.json()
84
90
  }
85
91
 
86
92
  export function getActiveTheme(): DesktopTheme | null {
87
93
  const activeId = document.documentElement.getAttribute("data-theme")
94
+
88
95
  if (!activeId) {
89
96
  return null
90
97
  }
98
+
91
99
  if (activeTheme?.id === activeId) {
92
100
  return activeTheme
93
101
  }
102
+
94
103
  return null
95
104
  }
96
105
 
97
106
  export function removeTheme(): void {
98
107
  activeTheme = null
99
108
  const existingElement = document.getElementById(THEME_STYLE_ID)
109
+
100
110
  if (existingElement) {
101
111
  existingElement.remove()
102
112
  }
113
+
103
114
  document.documentElement.removeAttribute("data-theme")
104
115
  }
105
116
 
@@ -13,48 +13,62 @@ export function resolveThemeVariant(variant: ThemeVariant, isDark: boolean): Res
13
13
  const error = generateScale(colors.error, isDark)
14
14
  const info = generateScale(colors.info, isDark)
15
15
  const interactive = generateScale(colors.interactive, isDark)
16
+
16
17
  const amber = generateScale(
17
18
  shift(colors.warning, isDark ? { h: -16, l: -0.058, c: 1.14 } : { h: -22, l: -0.082, c: 0.94 }),
18
19
  isDark,
19
20
  )
21
+
20
22
  const blue = generateScale(shift(colors.interactive, { h: -12, l: 0.128, c: 1.12 }), isDark)
23
+
21
24
  const diffAdd = generateScale(
22
25
  colors.diffAdd ?? shift(colors.success, { c: isDark ? 0.7 : 0.55, l: isDark ? -0.18 : 0.14 }),
23
26
  isDark,
24
27
  )
28
+
25
29
  const diffDelete = generateScale(
26
30
  colors.diffDelete ?? shift(colors.error, { c: isDark ? 0.82 : 0.7, l: isDark ? -0.08 : 0.08 }),
27
31
  isDark,
28
32
  )
33
+
29
34
  const ink = colors.ink ?? colors.neutral
30
35
  const tint = colors.compact ? hexToOklch(ink) : undefined
36
+
31
37
  const body = tint
32
38
  ? shift(ink, {
33
39
  l: isDark ? Math.max(0, 0.88 - tint.l) * 0.4 : -Math.max(0, tint.l - 0.18) * 0.24,
34
40
  c: isDark ? 1.04 : 1.02,
35
41
  })
36
42
  : undefined
43
+
37
44
  const backgroundOverride = overrides["background-base"]
38
45
  const backgroundHex = getHex(backgroundOverride)
39
46
  const overlay = Boolean(backgroundOverride) && !backgroundHex
47
+
40
48
  const content = (seed: HexColor, scale: HexColor[]) => {
41
49
  const base = hexToOklch(seed)
42
50
  const value = isDark ? (base.l > 0.84 ? shift(seed, { c: 1.18 }) : scale[10]) : scale[10]
51
+
43
52
  return shift(value, { l: isDark ? 0.034 : -0.024, c: isDark ? 1.3 : 1.18 })
44
53
  }
54
+
45
55
  const modified = () => {
46
56
  if (!colors.compact) return isDark ? "#ffba92" : "#FF8C00"
47
57
  const warningHue = hexToOklch(colors.warning).h
48
58
  const deleteHue = hexToOklch(colors.diffDelete ?? colors.error).h
49
59
  const delta = Math.abs(((((deleteHue - warningHue) % 360) + 540) % 360) - 180)
60
+
50
61
  if (delta < 48) return isDark ? "#ffba92" : "#FF8C00"
62
+
51
63
  return content(colors.warning, warning)
52
64
  }
65
+
53
66
  const surface = (
54
67
  seed: HexColor,
55
68
  alpha: { base: number; weak: number; weaker: number; strong: number; stronger: number },
56
69
  ) => {
57
70
  const base = alphaTone(seed, alpha.base)
71
+
58
72
  return {
59
73
  base,
60
74
  weak: alphaTone(seed, alpha.weak),
@@ -63,11 +77,15 @@ export function resolveThemeVariant(variant: ThemeVariant, isDark: boolean): Res
63
77
  stronger: alphaTone(seed, alpha.stronger),
64
78
  }
65
79
  }
80
+
66
81
  const background = backgroundHex ?? neutral[0]
82
+
67
83
  const alphaTone = (color: HexColor, alpha: number) =>
68
84
  overlay ? (withAlpha(color, alpha) as ColorValue) : blend(color, background, alpha)
85
+
69
86
  const borderTone = (light: number, dark: number) =>
70
87
  alphaTone(ink, isDark ? Math.min(1, dark + 0.024 + (colors.compact ? 0.08 : 0)) : Math.min(1, light + 0.024))
88
+
71
89
  const diffHiddenSurface = surface(
72
90
  isDark ? shift(colors.interactive, { c: 0.55, l: 0 }) : shift(colors.interactive, { c: 0.45, l: 0.08 }),
73
91
  isDark
@@ -93,21 +111,27 @@ export function resolveThemeVariant(variant: ThemeVariant, isDark: boolean): Res
93
111
  const infob = info[isDark ? 6 : 4]
94
112
  const infow = info[isDark ? 5 : 3]
95
113
  const infos = info[10]
114
+
96
115
  const lum = (hex: HexColor) => {
97
116
  const rgb = hexToRgb(hex)
98
117
  const lift = (v: number) => (v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4))
118
+
99
119
  return 0.2126 * lift(rgb.r) + 0.7152 * lift(rgb.g) + 0.0722 * lift(rgb.b)
100
120
  }
121
+
101
122
  const hit = (a: HexColor, b: HexColor) => {
102
123
  const x = lum(a)
103
124
  const y = lum(b)
104
125
  const light = Math.max(x, y)
105
126
  const dark = Math.min(x, y)
127
+
106
128
  return (light + 0.05) / (dark + 0.05)
107
129
  }
130
+
108
131
  const on = (fill: HexColor) => {
109
132
  const light = "#ffffff" as HexColor
110
133
  const dark = "#000000" as HexColor
134
+
111
135
  return hit(light, fill) > hit(dark, fill) ? light : dark
112
136
  }
113
137
 
@@ -432,6 +456,7 @@ export function resolveThemeVariant(variant: ThemeVariant, isDark: boolean): Res
432
456
 
433
457
  if (colors.compact && "text-weak" in overrides && !("text-weaker" in overrides)) {
434
458
  const weak = tokens["text-weak"]
459
+
435
460
  if (weak.startsWith("#")) {
436
461
  tokens["text-weaker"] = shift(weak as HexColor, { l: isDark ? -0.12 : 0.12, c: 0.75 })
437
462
  } else {
@@ -443,6 +468,7 @@ export function resolveThemeVariant(variant: ThemeVariant, isDark: boolean): Res
443
468
  if (!("markdown-text" in overrides)) {
444
469
  tokens["markdown-text"] = tokens["text-base"]
445
470
  }
471
+
446
472
  if (!("markdown-code-block" in overrides)) {
447
473
  tokens["markdown-code-block"] = tokens["text-base"]
448
474
  }
@@ -472,6 +498,7 @@ interface ThemeColors {
472
498
 
473
499
  function getColors(variant: ThemeVariant): ThemeColors {
474
500
  const input = variant as { palette?: unknown; seeds?: unknown }
501
+
475
502
  if (input.palette && input.seeds) {
476
503
  throw new Error("Theme variant cannot define both `palette` and `seeds`")
477
504
  }
@@ -523,6 +550,7 @@ function generateNeutralAlphaScale(neutralScale: HexColor[], isDark: boolean): H
523
550
 
524
551
  function getHex(value: ColorValue | undefined): HexColor | undefined {
525
552
  if (!value?.startsWith("#")) return
553
+
526
554
  return value as HexColor
527
555
  }
528
556
 
@@ -8,23 +8,30 @@ const greyRef = (step: number): V2ColorValue => `var(--v2-grey-${step})`
8
8
 
9
9
  function greyHex(primitives: Record<string, V2ColorValue>, step: number) {
10
10
  const hex = primitives[`v2-grey-${step}`]
11
+
11
12
  if (typeof hex === "string" && hex.startsWith("#")) return hex as HexColor
12
13
  }
13
14
 
14
15
  function resolveGreyRef(value: V2ColorValue, primitives: Record<string, V2ColorValue>) {
15
16
  const step = value.match(/^var\(--v2-grey-(\d+)\)$/)?.[1]
17
+
16
18
  if (!step) throw new Error(`Expected grey primitive ref, got ${value}`)
17
19
  const hex = greyHex(primitives, Number(step))
20
+
18
21
  if (!hex) throw new Error(`Missing grey primitive v2-grey-${step}`)
22
+
19
23
  return hex
20
24
  }
21
25
 
22
26
  function pickGrey(primitives: Record<string, V2ColorValue>, background: HexColor, minContrast: number, target: number) {
23
27
  const matches = GREY_STEPS.filter((step) => {
24
28
  const hex = greyHex(primitives, step)
29
+
25
30
  return hex && contrastRatio(hex, background) >= minContrast
26
31
  })
32
+
27
33
  if (matches.length === 0) return target
34
+
28
35
  return matches.reduce((best, step) => (Math.abs(step - target) < Math.abs(best - target) ? step : best))
29
36
  }
30
37
 
@@ -35,6 +42,7 @@ export function mapV2Foreground(
35
42
  overrides: Record<string, ColorValue> = {},
36
43
  ): Record<string, V2ColorValue> {
37
44
  const tint = hexToOklch(ink)
45
+
38
46
  const body = shift(ink, {
39
47
  l: isDark ? Math.max(0, 0.88 - tint.l) * 0.4 : -Math.max(0, tint.l - 0.18) * 0.24,
40
48
  c: isDark ? 1.04 : 1.02,
@@ -30,6 +30,7 @@ function clamp(v: number, min: number, max: number) {
30
30
  function generateV2HueScale(seed: HexColor, isDark: boolean): HexColor[] {
31
31
  const base = hexToOklch(seed)
32
32
  const chromaBoost = isDark ? 1 : 1.05
33
+
33
34
  const lightSteps = [
34
35
  0.99,
35
36
  0.965,
@@ -44,6 +45,7 @@ function generateV2HueScale(seed: HexColor, isDark: boolean): HexColor[] {
44
45
  clamp(base.l - 0.34, 0.12, 0.3),
45
46
  clamp(base.l - 0.4, 0.08, 0.22),
46
47
  ]
48
+
47
49
  const chromaMultipliers = [0.28, 0.48, 0.68, 0.86, 1.02, 1.28, 1.34, 1.28, 1.18, 1.08, 0.98, 0.88]
48
50
 
49
51
  return lightSteps.map((l, i) =>
@@ -58,20 +60,24 @@ function generateV2HueScale(seed: HexColor, isDark: boolean): HexColor[] {
58
60
  /** Grey ramp: 100 = lightest, 1200 = darkest. Derived from palette neutral → ink like v1. */
59
61
  function generateV2NeutralScale(neutral: HexColor, ink: HexColor, isDark: boolean): HexColor[] {
60
62
  const scale = generateNeutralScale(neutral, isDark, ink)
63
+
61
64
  return isDark ? scale.toReversed() : scale
62
65
  }
63
66
 
64
67
  function assignHueRamp(prefix: string, scale: HexColor[]): Record<string, V2ColorValue> {
65
68
  const tokens: Record<string, V2ColorValue> = {}
69
+
66
70
  for (let i = 0; i < V2_STEPS.length; i++) {
67
71
  tokens[`v2-${prefix}-${V2_STEPS[i]}`] = scale[i]!
68
72
  }
73
+
69
74
  return tokens
70
75
  }
71
76
 
72
77
  function readPalette(variant: ThemeVariant): PaletteInput {
73
78
  if ("palette" in variant && variant.palette) {
74
79
  const palette = variant.palette
80
+
75
81
  return {
76
82
  neutral: palette.neutral,
77
83
  ink: palette.ink,
@@ -86,8 +92,10 @@ function readPalette(variant: ThemeVariant): PaletteInput {
86
92
  diffDelete: palette.diffDelete ?? palette.error,
87
93
  }
88
94
  }
95
+
89
96
  if ("seeds" in variant && variant.seeds) {
90
97
  const seeds = variant.seeds
98
+
91
99
  return {
92
100
  neutral: seeds.neutral,
93
101
  ink: seeds.neutral,
@@ -102,6 +110,7 @@ function readPalette(variant: ThemeVariant): PaletteInput {
102
110
  diffDelete: seeds.diffDelete,
103
111
  }
104
112
  }
113
+
105
114
  throw new Error("Theme variant requires `palette` or `seeds`")
106
115
  }
107
116
 
@@ -136,6 +145,7 @@ export function resolveThemeVariantV2(variant: ThemeVariant, isDark: boolean): R
136
145
  const primitives = generateV2Primitives(variant, isDark)
137
146
  const semantics = mapV2Semantics(isDark)
138
147
  const foreground = mapV2Foreground(readPalette(variant).ink, isDark, primitives, variant.overrides)
148
+
139
149
  return mergeV2Tokens(primitives, semantics, foreground, variant.v2Overrides ?? {})
140
150
  }
141
151
 
@@ -24,6 +24,7 @@ export const TextShimmer = <T extends ValidComponent = "span">(props: {
24
24
 
25
25
  if (active()) {
26
26
  setRun(true)
27
+
27
28
  return
28
29
  }
29
30