mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-30 11:43:45 +00:00
feat(theme): extract TUI theme package (#39378)
This commit is contained in:
parent
27e7b0558a
commit
c445d98188
36 changed files with 419 additions and 235 deletions
18
bun.lock
18
bun.lock
|
|
@ -859,6 +859,20 @@
|
|||
"vite": "catalog:",
|
||||
},
|
||||
},
|
||||
"packages/theme": {
|
||||
"name": "@opencode-ai/theme",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@opentui/core": "catalog:",
|
||||
"effect": "catalog:",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/bun": "catalog:",
|
||||
"@types/bun": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
},
|
||||
},
|
||||
"packages/tui": {
|
||||
"name": "@opencode-ai/tui",
|
||||
"version": "1.18.4",
|
||||
|
|
@ -868,6 +882,7 @@
|
|||
"@opencode-ai/plugin": "workspace:*",
|
||||
"@opencode-ai/schema": "workspace:*",
|
||||
"@opencode-ai/simulation": "workspace:*",
|
||||
"@opencode-ai/theme": "workspace:*",
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
"@opencode-ai/util": "workspace:*",
|
||||
"@opentui/core": "catalog:",
|
||||
|
|
@ -1030,6 +1045,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@astrojs/cloudflare": "14.1.4",
|
||||
"@opencode-ai/theme": "workspace:*",
|
||||
"@types/bun": "catalog:",
|
||||
"astro": "7.1.3",
|
||||
"effect": "catalog:",
|
||||
|
|
@ -2075,6 +2091,8 @@
|
|||
|
||||
"@opencode-ai/storybook": ["@opencode-ai/storybook@workspace:packages/storybook"],
|
||||
|
||||
"@opencode-ai/theme": ["@opencode-ai/theme@workspace:packages/theme"],
|
||||
|
||||
"@opencode-ai/tui": ["@opencode-ai/tui@workspace:packages/tui"],
|
||||
|
||||
"@opencode-ai/ui": ["@opencode-ai/ui@workspace:packages/ui"],
|
||||
|
|
|
|||
36
packages/theme/package.json
Normal file
36
packages/theme/package.json
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/theme",
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/anomalyco/opencode.git",
|
||||
"directory": "packages/theme"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"exports": {
|
||||
"./tui": "./src/tui/index.ts",
|
||||
"./tui/v1": "./src/tui/v1.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "bun run script/build.ts",
|
||||
"typecheck": "tsgo --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@opentui/core": "catalog:",
|
||||
"effect": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/bun": "catalog:",
|
||||
"@types/bun": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"typescript": "catalog:"
|
||||
}
|
||||
}
|
||||
9
packages/theme/script/build.ts
Normal file
9
packages/theme/script/build.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
#!/usr/bin/env bun
|
||||
|
||||
import { $ } from "bun"
|
||||
import { fileURLToPath } from "node:url"
|
||||
|
||||
process.chdir(fileURLToPath(new URL("..", import.meta.url)))
|
||||
|
||||
await $`rm -rf dist`
|
||||
await $`bun tsc -p tsconfig.build.json`
|
||||
45
packages/theme/script/publish.ts
Normal file
45
packages/theme/script/publish.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
#!/usr/bin/env bun
|
||||
|
||||
import { Script } from "@opencode-ai/script"
|
||||
import { $ } from "bun"
|
||||
import { rm } from "node:fs/promises"
|
||||
import { fileURLToPath } from "node:url"
|
||||
|
||||
process.chdir(fileURLToPath(new URL("..", import.meta.url)))
|
||||
|
||||
const originalText = await Bun.file("package.json").text()
|
||||
const pkg = JSON.parse(originalText) as {
|
||||
name: string
|
||||
version: string
|
||||
exports: Record<string, string | { import: string; types: string }>
|
||||
}
|
||||
const tarball = `${pkg.name.replace("@", "").replace("/", "-")}-${pkg.version}.tgz`
|
||||
|
||||
if ((await $`npm view ${pkg.name}@${pkg.version} version`.nothrow()).exitCode === 0) {
|
||||
console.log(`already published ${pkg.name}@${pkg.version}`)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
try {
|
||||
await $`bun run typecheck`
|
||||
await $`bun run build`
|
||||
pkg.exports = Object.fromEntries(
|
||||
Object.entries(pkg.exports).map(([key, value]) => {
|
||||
if (typeof value !== "string") return [key, value]
|
||||
return [
|
||||
key,
|
||||
{
|
||||
import: value.replace("./src/", "./dist/").replace(/\.ts$/, ".js"),
|
||||
types: value.replace("./src/", "./dist/").replace(/\.ts$/, ".d.ts"),
|
||||
},
|
||||
]
|
||||
}),
|
||||
)
|
||||
await Bun.write("package.json", JSON.stringify(pkg, null, 2) + "\n")
|
||||
await rm(tarball, { force: true })
|
||||
await $`bun pm pack`
|
||||
await $`npm publish ${tarball} --tag ${Script.channel} --access public`
|
||||
} finally {
|
||||
await Bun.write("package.json", originalText)
|
||||
await rm(tarball, { force: true })
|
||||
}
|
||||
77
packages/theme/src/tui/color.ts
Normal file
77
packages/theme/src/tui/color.ts
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
type OklchColor = {
|
||||
l: number
|
||||
c: number
|
||||
h: number
|
||||
}
|
||||
|
||||
function clamp(value: number, min: number, max: number) {
|
||||
return Math.max(min, Math.min(max, value))
|
||||
}
|
||||
|
||||
function hue(value: number) {
|
||||
return ((value % 360) + 360) % 360
|
||||
}
|
||||
|
||||
function linearToSrgb(value: number) {
|
||||
if (value <= 0.0031308) return value * 12.92
|
||||
return 1.055 * Math.pow(value, 1 / 2.4) - 0.055
|
||||
}
|
||||
|
||||
function srgbToLinear(value: number) {
|
||||
if (value <= 0.04045) return value / 12.92
|
||||
return Math.pow((value + 0.055) / 1.055, 2.4)
|
||||
}
|
||||
|
||||
export function rgbToOklch(red: number, green: number, blue: number): OklchColor {
|
||||
const linearRed = srgbToLinear(red)
|
||||
const linearGreen = srgbToLinear(green)
|
||||
const linearBlue = srgbToLinear(blue)
|
||||
const lRoot = Math.cbrt(0.4122214708 * linearRed + 0.5363325363 * linearGreen + 0.0514459929 * linearBlue)
|
||||
const mRoot = Math.cbrt(0.2119034982 * linearRed + 0.6806995451 * linearGreen + 0.1073969566 * linearBlue)
|
||||
const sRoot = Math.cbrt(0.0883024619 * linearRed + 0.2817188376 * linearGreen + 0.6299787005 * linearBlue)
|
||||
const lightness = 0.2104542553 * lRoot + 0.793617785 * mRoot - 0.0040720468 * sRoot
|
||||
const a = 1.9779984951 * lRoot - 2.428592205 * mRoot + 0.4505937099 * sRoot
|
||||
const b = 0.0259040371 * lRoot + 0.7827717662 * mRoot - 0.808675766 * sRoot
|
||||
const chroma = Math.sqrt(a * a + b * b)
|
||||
const angle = Math.atan2(b, a) * (180 / Math.PI)
|
||||
return { l: lightness, c: chroma, h: angle < 0 ? angle + 360 : angle }
|
||||
}
|
||||
|
||||
function oklchToRgb(color: OklchColor) {
|
||||
const a = color.c * Math.cos((color.h * Math.PI) / 180)
|
||||
const b = color.c * Math.sin((color.h * Math.PI) / 180)
|
||||
const lRoot = color.l + 0.3963377774 * a + 0.2158037573 * b
|
||||
const mRoot = color.l - 0.1055613458 * a - 0.0638541728 * b
|
||||
const sRoot = color.l - 0.0894841775 * a - 1.291485548 * b
|
||||
const l = lRoot * lRoot * lRoot
|
||||
const m = mRoot * mRoot * mRoot
|
||||
const s = sRoot * sRoot * sRoot
|
||||
return {
|
||||
r: linearToSrgb(4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s),
|
||||
g: linearToSrgb(-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s),
|
||||
b: linearToSrgb(-0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s),
|
||||
}
|
||||
}
|
||||
|
||||
function fitOklch(color: OklchColor): OklchColor {
|
||||
const base = { l: clamp(color.l, 0, 1), c: Math.max(0, color.c), h: hue(color.h) }
|
||||
const rgb = oklchToRgb(base)
|
||||
if (rgb.r >= 0 && rgb.r <= 1 && rgb.g >= 0 && rgb.g <= 1 && rgb.b >= 0 && rgb.b <= 1) return base
|
||||
|
||||
const fitted = Array.from({ length: 24 }).reduce<OklchColor | undefined>((result, _, index) => {
|
||||
if (result) return result
|
||||
const next = { ...base, c: base.c * Math.pow(0.9, index + 1) }
|
||||
const output = oklchToRgb(next)
|
||||
if (output.r >= 0 && output.r <= 1 && output.g >= 0 && output.g <= 1 && output.b >= 0 && output.b <= 1) return next
|
||||
}, undefined)
|
||||
return fitted ?? { ...base, c: 0 }
|
||||
}
|
||||
|
||||
export function oklchToHex(color: OklchColor) {
|
||||
const rgb = oklchToRgb(fitOklch(color))
|
||||
const toHex = (value: number) =>
|
||||
Math.round(clamp(value, 0, 1) * 255)
|
||||
.toString(16)
|
||||
.padStart(2, "0")
|
||||
return `#${toHex(rgb.r)}${toHex(rgb.g)}${toHex(rgb.b)}`
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import type { HueName, ThemeDocument } from "./schema"
|
||||
import type { HueName, ThemeDocument } from "./schema.js"
|
||||
|
||||
export const DEFAULT_CATEGORICAL = [
|
||||
"blue",
|
||||
|
|
@ -4,8 +4,8 @@ import type {
|
|||
StatefulColorDefinition,
|
||||
TextDefinition,
|
||||
ThemeTokensDefinition,
|
||||
} from "./index"
|
||||
import { ActionState } from "./schema"
|
||||
} from "./index.js"
|
||||
import { ActionState } from "./schema.js"
|
||||
|
||||
export function expandTheme<Definition extends ModeDefinition>(definition: Definition): Definition {
|
||||
return {
|
||||
57
packages/theme/src/tui/fallback.ts
Normal file
57
packages/theme/src/tui/fallback.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import type { ThemeTokensDefinition } from "./index.js"
|
||||
import { ActionVariant, FeedbackKind } from "./schema.js"
|
||||
|
||||
export function fallback(): ThemeTokensDefinition {
|
||||
const red = "#ff0000"
|
||||
|
||||
return {
|
||||
text: {
|
||||
default: red,
|
||||
action: Object.fromEntries(ActionVariant.literals.map((variant) => [variant, { default: red }])),
|
||||
formfield: { default: red },
|
||||
feedback: Object.fromEntries(FeedbackKind.literals.map((kind) => [kind, { default: red }])),
|
||||
},
|
||||
background: {
|
||||
default: red,
|
||||
surface: { offset: red, overlay: red },
|
||||
action: Object.fromEntries(ActionVariant.literals.map((variant) => [variant, { default: red }])),
|
||||
formfield: { default: red },
|
||||
feedback: Object.fromEntries(FeedbackKind.literals.map((kind) => [kind, { default: red }])),
|
||||
},
|
||||
border: { default: red },
|
||||
scrollbar: { default: red },
|
||||
diff: {
|
||||
text: { added: red, removed: red, context: red, hunkHeader: red },
|
||||
background: { added: red, removed: red, context: red },
|
||||
highlight: { added: red, removed: red },
|
||||
lineNumber: { text: red, background: { added: red, removed: red } },
|
||||
},
|
||||
syntax: {
|
||||
comment: red,
|
||||
keyword: red,
|
||||
function: red,
|
||||
variable: red,
|
||||
string: red,
|
||||
number: red,
|
||||
type: red,
|
||||
operator: red,
|
||||
punctuation: red,
|
||||
},
|
||||
markdown: {
|
||||
text: red,
|
||||
heading: red,
|
||||
link: red,
|
||||
linkText: red,
|
||||
code: red,
|
||||
blockQuote: red,
|
||||
emphasis: red,
|
||||
strong: red,
|
||||
horizontalRule: red,
|
||||
listItem: red,
|
||||
listEnumeration: red,
|
||||
image: red,
|
||||
imageText: red,
|
||||
codeBlock: red,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -29,7 +29,7 @@ export {
|
|||
type ContextKey,
|
||||
type TextDefinition,
|
||||
type ThemeTokensDefinition,
|
||||
} from "./schema"
|
||||
} from "./schema.js"
|
||||
|
||||
export type {
|
||||
Categorical,
|
||||
|
|
@ -42,7 +42,9 @@ export type {
|
|||
ResolvedTheme,
|
||||
ResolvedThemeView,
|
||||
StatefulColor,
|
||||
} from "./types"
|
||||
export { DEFAULT_CATEGORICAL } from "./defaults"
|
||||
export { migrateV1 } from "./v1-migrate"
|
||||
export { selectTheme, selectThemeMode, supportsThemeMode, themeModes } from "./select"
|
||||
} from "./types.js"
|
||||
export { DEFAULT_CATEGORICAL, DEFAULT_THEME } from "./defaults.js"
|
||||
export { migrateV1 } from "./v1-migrate.js"
|
||||
export { resolveTheme, resolveThemeDocument, themeDecodeError } from "./resolve.js"
|
||||
export { selectTheme, selectThemeMode, supportsThemeMode, themeModes } from "./select.js"
|
||||
export { generateSyntax } from "./syntax.js"
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
import { RGBA } from "@opentui/core"
|
||||
import { Schema } from "effect"
|
||||
import { DEFAULT_CATEGORICAL, DEFAULT_THEME } from "./defaults"
|
||||
import { expandTheme, expandTokens, mergeTheme } from "./expand"
|
||||
import { fallback } from "./fallback"
|
||||
import { DEFAULT_CATEGORICAL, DEFAULT_THEME } from "./defaults.js"
|
||||
import { expandTheme, expandTokens, mergeTheme } from "./expand.js"
|
||||
import { fallback } from "./fallback.js"
|
||||
import {
|
||||
ActionState,
|
||||
ActionVariant,
|
||||
|
|
@ -12,7 +12,7 @@ import {
|
|||
HueStep,
|
||||
ThemeDefinition,
|
||||
ThemeDocument,
|
||||
} from "./schema"
|
||||
} from "./schema.js"
|
||||
import type {
|
||||
ActionStateKey,
|
||||
HueDefinition,
|
||||
|
|
@ -22,8 +22,8 @@ import type {
|
|||
ResolvedThemeView,
|
||||
StatefulColorDefinition,
|
||||
ThemeTokensDefinition,
|
||||
} from "./index"
|
||||
import { selectTheme, selectThemeMode } from "./select"
|
||||
} from "./index.js"
|
||||
import { selectTheme, selectThemeMode } from "./select.js"
|
||||
|
||||
const decodeThemeDefinitionSchema = Schema.decodeUnknownSync(ThemeDefinition)
|
||||
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { expandTheme, mergeTheme } from "./expand"
|
||||
import { expandTheme, mergeTheme } from "./expand.js"
|
||||
import type {
|
||||
FileThemeDefinition,
|
||||
MergeModeDefinition,
|
||||
|
|
@ -6,7 +6,7 @@ import type {
|
|||
ModeDefinition,
|
||||
ThemeDefinition,
|
||||
ThemeDocument,
|
||||
} from "./index"
|
||||
} from "./index.js"
|
||||
|
||||
export function selectTheme(
|
||||
document: ThemeDocument & { light: ThemeDefinition; dark: ThemeDefinition },
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { SyntaxStyle, type RGBA, type ThemeTokenStyle } from "@opentui/core"
|
||||
import type { Mode, ResolvedThemeView } from "./index"
|
||||
import type { Mode, ResolvedThemeView } from "./index.js"
|
||||
|
||||
export function generateSyntax(theme: ResolvedThemeView, mode: Mode) {
|
||||
const step = mode === "light" ? 800 : 200
|
||||
|
|
@ -9,7 +9,7 @@ import type {
|
|||
MarkdownToken,
|
||||
ContextKey,
|
||||
SyntaxToken,
|
||||
} from "./schema"
|
||||
} from "./schema.js"
|
||||
|
||||
export type ResolvedActionState = "default" | ActionState
|
||||
export type ResolvedFormfieldState = ResolvedActionState
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
import { RGBA } from "@opentui/core"
|
||||
import { oklchToHex, rgbToOklch } from "@opencode-ai/ui/theme/color"
|
||||
import type { Theme, ThemeV1Json } from "../v1"
|
||||
import { DEFAULT_CATEGORICAL, DEFAULT_THEME } from "./defaults"
|
||||
import type { FileThemeDefinition, Mode, ThemeDocument } from "./index"
|
||||
import { HueStep } from "./schema"
|
||||
import { oklchToHex, rgbToOklch } from "./color.js"
|
||||
import { DEFAULT_CATEGORICAL, DEFAULT_THEME } from "./defaults.js"
|
||||
import type { FileThemeDefinition, Mode, ThemeDocument } from "./index.js"
|
||||
import { HueStep } from "./schema.js"
|
||||
import type { Theme, ThemeV1Json } from "./v1.js"
|
||||
|
||||
type ThemeColor = Exclude<keyof Theme, "thinkingOpacity" | "_hasSelectedListItemText">
|
||||
type ChromaticHue = "red" | "orange" | "yellow" | "green" | "cyan" | "blue" | "purple"
|
||||
76
packages/theme/src/tui/v1.ts
Normal file
76
packages/theme/src/tui/v1.ts
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
import type { RGBA } from "@opentui/core"
|
||||
|
||||
export type Theme = {
|
||||
readonly primary: RGBA
|
||||
readonly secondary: RGBA
|
||||
readonly accent: RGBA
|
||||
readonly error: RGBA
|
||||
readonly warning: RGBA
|
||||
readonly success: RGBA
|
||||
readonly info: RGBA
|
||||
readonly text: RGBA
|
||||
readonly textMuted: RGBA
|
||||
readonly selectedListItemText: RGBA
|
||||
readonly background: RGBA
|
||||
readonly backgroundPanel: RGBA
|
||||
readonly backgroundElement: RGBA
|
||||
readonly backgroundMenu: RGBA
|
||||
readonly border: RGBA
|
||||
readonly borderActive: RGBA
|
||||
readonly borderSubtle: RGBA
|
||||
readonly diffAdded: RGBA
|
||||
readonly diffRemoved: RGBA
|
||||
readonly diffContext: RGBA
|
||||
readonly diffHunkHeader: RGBA
|
||||
readonly diffHighlightAdded: RGBA
|
||||
readonly diffHighlightRemoved: RGBA
|
||||
readonly diffAddedBg: RGBA
|
||||
readonly diffRemovedBg: RGBA
|
||||
readonly diffContextBg: RGBA
|
||||
readonly diffLineNumber: RGBA
|
||||
readonly diffAddedLineNumberBg: RGBA
|
||||
readonly diffRemovedLineNumberBg: RGBA
|
||||
readonly markdownText: RGBA
|
||||
readonly markdownHeading: RGBA
|
||||
readonly markdownLink: RGBA
|
||||
readonly markdownLinkText: RGBA
|
||||
readonly markdownCode: RGBA
|
||||
readonly markdownBlockQuote: RGBA
|
||||
readonly markdownEmph: RGBA
|
||||
readonly markdownStrong: RGBA
|
||||
readonly markdownHorizontalRule: RGBA
|
||||
readonly markdownListItem: RGBA
|
||||
readonly markdownListEnumeration: RGBA
|
||||
readonly markdownImage: RGBA
|
||||
readonly markdownImageText: RGBA
|
||||
readonly markdownCodeBlock: RGBA
|
||||
readonly syntaxComment: RGBA
|
||||
readonly syntaxKeyword: RGBA
|
||||
readonly syntaxFunction: RGBA
|
||||
readonly syntaxVariable: RGBA
|
||||
readonly syntaxString: RGBA
|
||||
readonly syntaxNumber: RGBA
|
||||
readonly syntaxType: RGBA
|
||||
readonly syntaxOperator: RGBA
|
||||
readonly syntaxPunctuation: RGBA
|
||||
readonly thinkingOpacity: number
|
||||
_hasSelectedListItemText: boolean
|
||||
}
|
||||
|
||||
export type ThemeColor = Exclude<keyof Theme, "thinkingOpacity" | "_hasSelectedListItemText">
|
||||
export type HexColor = `#${string}`
|
||||
export type RefName = string
|
||||
export type Variant = {
|
||||
dark: HexColor | RefName
|
||||
light: HexColor | RefName
|
||||
}
|
||||
export type ColorValue = HexColor | RefName | Variant | RGBA | number
|
||||
export type ThemeV1Json = {
|
||||
$schema?: string
|
||||
defs?: Record<string, HexColor | RefName>
|
||||
theme: Omit<Record<ThemeColor, ColorValue>, "selectedListItemText" | "backgroundMenu"> & {
|
||||
selectedListItemText?: ColorValue
|
||||
backgroundMenu?: ColorValue
|
||||
thinkingOpacity?: number
|
||||
}
|
||||
}
|
||||
10
packages/theme/sst-env.d.ts
vendored
Normal file
10
packages/theme/sst-env.d.ts
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
/* This file is auto-generated by SST. Do not edit. */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/* deno-fmt-ignore-file */
|
||||
/* biome-ignore-all lint: auto-generated */
|
||||
|
||||
/// <reference path="../../sst-env.d.ts" />
|
||||
|
||||
import "sst"
|
||||
export {}
|
||||
11
packages/theme/tsconfig.build.json
Normal file
11
packages/theme/tsconfig.build.json
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "dist",
|
||||
"noEmit": false,
|
||||
"declaration": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
12
packages/theme/tsconfig.json
Normal file
12
packages/theme/tsconfig.json
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "@tsconfig/bun/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"allowImportingTsExtensions": false,
|
||||
"allowJs": false,
|
||||
"noUncheckedIndexedAccess": false
|
||||
}
|
||||
}
|
||||
|
|
@ -83,6 +83,7 @@
|
|||
"@opencode-ai/plugin": "workspace:*",
|
||||
"@opencode-ai/schema": "workspace:*",
|
||||
"@opencode-ai/simulation": "workspace:*",
|
||||
"@opencode-ai/theme": "workspace:*",
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
"@opencode-ai/util": "workspace:*",
|
||||
"@opentui/core": "catalog:",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { CliRenderEvents, SyntaxStyle, type TerminalColors } from "@opentui/core"
|
||||
import { useRenderer } from "@opentui/solid"
|
||||
import { generateSyntax, resolveThemeDocument, themeModes } from "@opencode-ai/theme/tui"
|
||||
import {
|
||||
DEFAULT_THEMES,
|
||||
addTheme,
|
||||
|
|
@ -14,12 +15,9 @@ import {
|
|||
type Theme,
|
||||
type ThemeDocumentSource,
|
||||
} from "../theme"
|
||||
import { generateSyntax } from "../theme/v2/syntax"
|
||||
import { generateSystem, terminalMode } from "../theme/system"
|
||||
import { discoverThemes, themeDirectories } from "../theme/discovery"
|
||||
import { createComponentTheme, type ComponentTheme } from "../theme/v2/component"
|
||||
import { resolveThemeDocument } from "../theme/v2/resolve"
|
||||
import { themeModes } from "../theme/v2/select"
|
||||
import { createComponentTheme, type ComponentTheme } from "../theme/component"
|
||||
import { createEffect, createMemo, onCleanup, onMount, type Accessor, type ParentProps } from "solid-js"
|
||||
import { createStore, produce } from "solid-js/store"
|
||||
import { createSimpleContext } from "./helper"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { RGBA } from "@opentui/core"
|
||||
import type { Accessor } from "solid-js"
|
||||
import type { Mode, ResolvedThemeView } from "./index"
|
||||
import type { Mode, ResolvedThemeView } from "@opencode-ai/theme/tui"
|
||||
|
||||
export function createComponentTheme(current: Accessor<ResolvedThemeView>, mode: Accessor<Mode>) {
|
||||
return {
|
||||
|
|
@ -1,9 +1,7 @@
|
|||
import { Schema } from "effect"
|
||||
import { migrateV1, resolveThemeDocument, ThemeDocument, themeDecodeError } from "@opencode-ai/theme/tui"
|
||||
import { resolveThemeColors } from "./resolve"
|
||||
import { DEFAULT_THEMES, type Theme, type ThemeV1Json } from "./v1"
|
||||
import { resolveThemeDocument, themeDecodeError } from "./v2/resolve"
|
||||
import { ThemeDocument } from "./v2/schema"
|
||||
import { migrateV1 } from "./v2/v1-migrate"
|
||||
|
||||
export { DEFAULT_THEMES, generateSyntax, selectedForeground, type Theme, type ThemeV1Json } from "./v1"
|
||||
export { resolveThemeDocument, type ThemeDocument }
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { RGBA, SyntaxStyle } from "@opentui/core"
|
||||
import type { Theme, ThemeV1Json } from "@opencode-ai/theme/tui/v1"
|
||||
import aura from "./assets/aura.json" with { type: "json" }
|
||||
import ayu from "./assets/ayu.json" with { type: "json" }
|
||||
import carbonfox from "./assets/carbonfox.json" with { type: "json" }
|
||||
|
|
@ -33,80 +34,7 @@ import vercel from "./assets/vercel.json" with { type: "json" }
|
|||
import vesper from "./assets/vesper.json" with { type: "json" }
|
||||
import zenburn from "./assets/zenburn.json" with { type: "json" }
|
||||
|
||||
export type Theme = {
|
||||
readonly primary: RGBA
|
||||
readonly secondary: RGBA
|
||||
readonly accent: RGBA
|
||||
readonly error: RGBA
|
||||
readonly warning: RGBA
|
||||
readonly success: RGBA
|
||||
readonly info: RGBA
|
||||
readonly text: RGBA
|
||||
readonly textMuted: RGBA
|
||||
readonly selectedListItemText: RGBA
|
||||
readonly background: RGBA
|
||||
readonly backgroundPanel: RGBA
|
||||
readonly backgroundElement: RGBA
|
||||
readonly backgroundMenu: RGBA
|
||||
readonly border: RGBA
|
||||
readonly borderActive: RGBA
|
||||
readonly borderSubtle: RGBA
|
||||
readonly diffAdded: RGBA
|
||||
readonly diffRemoved: RGBA
|
||||
readonly diffContext: RGBA
|
||||
readonly diffHunkHeader: RGBA
|
||||
readonly diffHighlightAdded: RGBA
|
||||
readonly diffHighlightRemoved: RGBA
|
||||
readonly diffAddedBg: RGBA
|
||||
readonly diffRemovedBg: RGBA
|
||||
readonly diffContextBg: RGBA
|
||||
readonly diffLineNumber: RGBA
|
||||
readonly diffAddedLineNumberBg: RGBA
|
||||
readonly diffRemovedLineNumberBg: RGBA
|
||||
readonly markdownText: RGBA
|
||||
readonly markdownHeading: RGBA
|
||||
readonly markdownLink: RGBA
|
||||
readonly markdownLinkText: RGBA
|
||||
readonly markdownCode: RGBA
|
||||
readonly markdownBlockQuote: RGBA
|
||||
readonly markdownEmph: RGBA
|
||||
readonly markdownStrong: RGBA
|
||||
readonly markdownHorizontalRule: RGBA
|
||||
readonly markdownListItem: RGBA
|
||||
readonly markdownListEnumeration: RGBA
|
||||
readonly markdownImage: RGBA
|
||||
readonly markdownImageText: RGBA
|
||||
readonly markdownCodeBlock: RGBA
|
||||
readonly syntaxComment: RGBA
|
||||
readonly syntaxKeyword: RGBA
|
||||
readonly syntaxFunction: RGBA
|
||||
readonly syntaxVariable: RGBA
|
||||
readonly syntaxString: RGBA
|
||||
readonly syntaxNumber: RGBA
|
||||
readonly syntaxType: RGBA
|
||||
readonly syntaxOperator: RGBA
|
||||
readonly syntaxPunctuation: RGBA
|
||||
readonly thinkingOpacity: number
|
||||
_hasSelectedListItemText: boolean
|
||||
}
|
||||
|
||||
export type ThemeColor = Exclude<keyof Theme, "thinkingOpacity" | "_hasSelectedListItemText">
|
||||
export type HexColor = `#${string}`
|
||||
export type RefName = string
|
||||
export type Variant = {
|
||||
dark: HexColor | RefName
|
||||
light: HexColor | RefName
|
||||
}
|
||||
export type ColorValue = HexColor | RefName | Variant | RGBA | number
|
||||
export type ThemeV1Json = {
|
||||
$schema?: string
|
||||
defs?: Record<string, HexColor | RefName>
|
||||
theme: Omit<Record<ThemeColor, ColorValue>, "selectedListItemText" | "backgroundMenu"> & {
|
||||
selectedListItemText?: ColorValue
|
||||
backgroundMenu?: ColorValue
|
||||
thinkingOpacity?: number
|
||||
}
|
||||
}
|
||||
export type { ColorValue, HexColor, RefName, Theme, ThemeColor, ThemeV1Json, Variant } from "@opencode-ai/theme/tui/v1"
|
||||
|
||||
export const DEFAULT_THEMES: Record<string, ThemeV1Json> = {
|
||||
aura,
|
||||
|
|
|
|||
|
|
@ -1,57 +0,0 @@
|
|||
import type { ThemeTokensDefinition } from "./index"
|
||||
import { ActionVariant, FeedbackKind } from "./schema"
|
||||
|
||||
export function fallback(): ThemeTokensDefinition {
|
||||
const red = "#ff0000"
|
||||
|
||||
return {
|
||||
text: {
|
||||
default: red,
|
||||
action: Object.fromEntries(ActionVariant.literals.map((variant) => [variant, { default: red }])),
|
||||
formfield: { default: red },
|
||||
feedback: Object.fromEntries(FeedbackKind.literals.map((kind) => [kind, { default: red }])),
|
||||
},
|
||||
background: {
|
||||
default: red,
|
||||
surface: { offset: red, overlay: red },
|
||||
action: Object.fromEntries(ActionVariant.literals.map((variant) => [variant, { default: red }])),
|
||||
formfield: { default: red },
|
||||
feedback: Object.fromEntries(FeedbackKind.literals.map((kind) => [kind, { default: red }])),
|
||||
},
|
||||
border: { default: red },
|
||||
scrollbar: { default: red },
|
||||
diff: {
|
||||
text: { added: red, removed: red, context: red, hunkHeader: red },
|
||||
background: { added: red, removed: red, context: red },
|
||||
highlight: { added: red, removed: red },
|
||||
lineNumber: { text: red, background: { added: red, removed: red } },
|
||||
},
|
||||
syntax: {
|
||||
comment: red,
|
||||
keyword: red,
|
||||
function: red,
|
||||
variable: red,
|
||||
string: red,
|
||||
number: red,
|
||||
type: red,
|
||||
operator: red,
|
||||
punctuation: red,
|
||||
},
|
||||
markdown: {
|
||||
text: red,
|
||||
heading: red,
|
||||
link: red,
|
||||
linkText: red,
|
||||
code: red,
|
||||
blockQuote: red,
|
||||
emphasis: red,
|
||||
strong: red,
|
||||
horizontalRule: red,
|
||||
listItem: red,
|
||||
listEnumeration: red,
|
||||
image: red,
|
||||
imageText: red,
|
||||
codeBlock: red,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
import { createComponent, createContext, useContext, type Accessor, type ParentProps } from "solid-js"
|
||||
import { createComponentTheme, type ComponentTheme } from "./component"
|
||||
import type { ContextKey, Mode, ResolvedTheme } from "./index"
|
||||
|
||||
type ThemeRuntime = {
|
||||
readonly resolved: Accessor<ResolvedTheme>
|
||||
readonly mode: Accessor<Mode>
|
||||
readonly component: ComponentTheme
|
||||
}
|
||||
|
||||
const ThemeContext = createContext<ThemeRuntime>()
|
||||
|
||||
export function ThemeProvider(props: ParentProps<{ theme: ResolvedTheme; mode?: Mode }>) {
|
||||
const resolved = () => props.theme
|
||||
const mode = () => props.mode ?? "light"
|
||||
return createComponent(ThemeContext.Provider, {
|
||||
value: { resolved, mode, component: createComponentTheme(resolved, mode) },
|
||||
get children() {
|
||||
return props.children
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function ContextProvider(props: ParentProps<{ context: ContextKey }>) {
|
||||
const parent = runtime()
|
||||
const context = () => {
|
||||
const value = parent.resolved().contexts[props.context]
|
||||
if (!value) throw new Error(`Theme context is not defined: ${props.context}`)
|
||||
return value
|
||||
}
|
||||
context()
|
||||
return createComponent(ThemeContext.Provider, {
|
||||
value: { resolved: parent.resolved, mode: parent.mode, component: createComponentTheme(context, parent.mode) },
|
||||
get children() {
|
||||
return props.children
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useTheme() {
|
||||
return runtime().component
|
||||
}
|
||||
|
||||
export function useResolvedTheme() {
|
||||
return runtime().resolved
|
||||
}
|
||||
|
||||
function runtime() {
|
||||
const context = useContext(ThemeContext)
|
||||
if (!context) throw new Error("Theme context must be used within a ThemeProvider")
|
||||
return context
|
||||
}
|
||||
|
|
@ -2,10 +2,9 @@
|
|||
import { testRender } from "@opentui/solid"
|
||||
import { expect, test } from "bun:test"
|
||||
import { RGBA } from "@opentui/core"
|
||||
import { DEFAULT_THEME, selectTheme } from "@opencode-ai/theme/tui"
|
||||
import { createTuiResolvedConfig } from "../../fixture/tui-runtime"
|
||||
import { DEFAULT_THEMES } from "../../../src/theme"
|
||||
import { DEFAULT_THEME } from "../../../src/theme/v2/defaults"
|
||||
import { selectTheme } from "../../../src/theme/v2/select"
|
||||
import { ConfigProvider } from "../../../src/config"
|
||||
import { ThemeProvider, useTheme, type ThemeError } from "../../../src/context/theme"
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,8 @@
|
|||
import { expect, test } from "bun:test"
|
||||
import { createSignal } from "solid-js"
|
||||
import { RGBA } from "@opentui/core"
|
||||
import { createComponentTheme } from "../../../src/theme/v2/component"
|
||||
import { DEFAULT_THEME } from "../../../src/theme/v2/defaults"
|
||||
import { resolveTheme } from "../../../src/theme/v2/resolve"
|
||||
import { selectTheme } from "../../../src/theme/v2/select"
|
||||
import type { ContextKey } from "../../../src/theme/v2"
|
||||
import { DEFAULT_THEME, resolveTheme, selectTheme, type ContextKey } from "@opencode-ai/theme/tui"
|
||||
import { createComponentTheme } from "../../../src/theme/component"
|
||||
|
||||
test("provides reactive properties, states, contexts, and color operations", () => {
|
||||
const [resolved, setResolved] = createSignal(resolveTheme(selectTheme(DEFAULT_THEME, "light")))
|
||||
|
|
|
|||
|
|
@ -1,10 +1,14 @@
|
|||
import { expect, test } from "bun:test"
|
||||
import { RGBA } from "@opentui/core"
|
||||
import {
|
||||
DEFAULT_THEME,
|
||||
resolveTheme,
|
||||
resolveThemeDocument,
|
||||
selectTheme,
|
||||
type Mode,
|
||||
type ThemeDefinition,
|
||||
} from "@opencode-ai/theme/tui"
|
||||
import { parseTheme, type ThemeDocumentSource } from "../../../src/theme"
|
||||
import { DEFAULT_THEME } from "../../../src/theme/v2/defaults"
|
||||
import type { Mode, ThemeDefinition } from "../../../src/theme/v2"
|
||||
import { resolveTheme, resolveThemeDocument } from "../../../src/theme/v2/resolve"
|
||||
import { selectTheme } from "../../../src/theme/v2/select"
|
||||
|
||||
const light = selectTheme(DEFAULT_THEME, "light")
|
||||
const dark = selectTheme(DEFAULT_THEME, "dark")
|
||||
|
|
|
|||
|
|
@ -1,6 +1,13 @@
|
|||
import { expect, test } from "bun:test"
|
||||
import type { HueDefinition, ThemeDefinition, ThemeDocument } from "../../../src/theme/v2"
|
||||
import { selectTheme, selectThemeMode, supportsThemeMode, themeModes } from "../../../src/theme/v2/select"
|
||||
import {
|
||||
selectTheme,
|
||||
selectThemeMode,
|
||||
supportsThemeMode,
|
||||
themeModes,
|
||||
type HueDefinition,
|
||||
type ThemeDefinition,
|
||||
type ThemeDocument,
|
||||
} from "@opencode-ai/theme/tui"
|
||||
|
||||
const hue = {} as HueDefinition
|
||||
const light = { hue, categorical: ["blue"], text: { default: "#111111", subdued: "#222222" } } satisfies ThemeDefinition
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { expect, test } from "bun:test"
|
||||
import type { BackgroundDefinition, TextDefinition, ThemeDefinition, ThemeDocument } from "../../../src/theme/v2"
|
||||
import type { BackgroundDefinition, TextDefinition, ThemeDefinition, ThemeDocument } from "@opencode-ai/theme/tui"
|
||||
|
||||
const text = {
|
||||
default: "$hue.neutral.900",
|
||||
|
|
|
|||
|
|
@ -1,9 +1,13 @@
|
|||
import { expect, test } from "bun:test"
|
||||
import {
|
||||
DEFAULT_CATEGORICAL,
|
||||
DEFAULT_THEME,
|
||||
migrateV1,
|
||||
resolveThemeDocument,
|
||||
selectThemeMode,
|
||||
themeModes,
|
||||
} from "@opencode-ai/theme/tui"
|
||||
import { DEFAULT_THEMES, resolveTheme as resolveV1 } from "../../../src/theme"
|
||||
import { resolveThemeDocument } from "../../../src/theme/v2/resolve"
|
||||
import { selectThemeMode, themeModes } from "../../../src/theme/v2/select"
|
||||
import { migrateV1 } from "../../../src/theme/v2/v1-migrate"
|
||||
import { DEFAULT_CATEGORICAL, DEFAULT_THEME } from "../../../src/theme/v2/defaults"
|
||||
|
||||
test("migrates resolved V1 modes into V2 tokens", () => {
|
||||
const migrated = migrateV1(DEFAULT_THEMES.opencode)
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@astrojs/cloudflare": "14.1.4",
|
||||
"@opencode-ai/theme": "workspace:*",
|
||||
"@types/bun": "catalog:",
|
||||
"astro": "7.1.3",
|
||||
"effect": "catalog:",
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import { Schema, SchemaAST } from "effect"
|
||||
import { format } from "prettier"
|
||||
import { ThemeDefinition, ThemeDocument } from "../../tui/src/theme/v2/schema"
|
||||
import { ThemeDefinition, ThemeDocument } from "@opencode-ai/theme/tui"
|
||||
|
||||
const target = import.meta.dir + "/../snippets/generated/theme-tokens.mdx"
|
||||
const root = requireObject(ThemeDefinition.ast)
|
||||
|
|
@ -64,7 +64,7 @@ ${JSON.stringify(example, null, 2)}
|
|||
## Token reference
|
||||
|
||||
This reference is generated from the Effect schema in
|
||||
\`packages/tui/src/theme/v2/schema.ts\`. Changes to the runtime schema update
|
||||
\`@opencode-ai/theme/tui\`. Changes to the runtime schema update
|
||||
this section through \`bun run generate\`.
|
||||
|
||||
### Hue tokens
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@
|
|||
## Token reference
|
||||
|
||||
This reference is generated from the Effect schema in
|
||||
`packages/tui/src/theme/v2/schema.ts`. Changes to the runtime schema update
|
||||
`@opencode-ai/theme/tui`. Changes to the runtime schema update
|
||||
this section through `bun run generate`.
|
||||
|
||||
### Hue tokens
|
||||
|
|
|
|||
|
|
@ -38,6 +38,9 @@ await prepareReleaseFiles()
|
|||
console.log("\n=== schema ===\n")
|
||||
await $`bun ./packages/schema/script/publish.ts`
|
||||
|
||||
console.log("\n=== theme ===\n")
|
||||
await $`bun ./packages/theme/script/publish.ts`
|
||||
|
||||
console.log("\n=== ai ===\n")
|
||||
await $`bun ./packages/ai/script/publish.ts`
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue