diff --git a/bun.lock b/bun.lock index 5ddeac231fe..e9c7917910f 100644 --- a/bun.lock +++ b/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"], diff --git a/packages/theme/package.json b/packages/theme/package.json new file mode 100644 index 00000000000..aa54b64232c --- /dev/null +++ b/packages/theme/package.json @@ -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:" + } +} diff --git a/packages/theme/script/build.ts b/packages/theme/script/build.ts new file mode 100644 index 00000000000..323a63ddf96 --- /dev/null +++ b/packages/theme/script/build.ts @@ -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` diff --git a/packages/theme/script/publish.ts b/packages/theme/script/publish.ts new file mode 100644 index 00000000000..8e37674b644 --- /dev/null +++ b/packages/theme/script/publish.ts @@ -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 +} +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 }) +} diff --git a/packages/theme/src/tui/color.ts b/packages/theme/src/tui/color.ts new file mode 100644 index 00000000000..7f7387db0e1 --- /dev/null +++ b/packages/theme/src/tui/color.ts @@ -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((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)}` +} diff --git a/packages/tui/src/theme/v2/defaults.ts b/packages/theme/src/tui/defaults.ts similarity index 99% rename from packages/tui/src/theme/v2/defaults.ts rename to packages/theme/src/tui/defaults.ts index e9eb352a07f..847eb9cb695 100644 --- a/packages/tui/src/theme/v2/defaults.ts +++ b/packages/theme/src/tui/defaults.ts @@ -1,4 +1,4 @@ -import type { HueName, ThemeDocument } from "./schema" +import type { HueName, ThemeDocument } from "./schema.js" export const DEFAULT_CATEGORICAL = [ "blue", diff --git a/packages/tui/src/theme/v2/expand.ts b/packages/theme/src/tui/expand.ts similarity index 98% rename from packages/tui/src/theme/v2/expand.ts rename to packages/theme/src/tui/expand.ts index 6601c476a39..fb39a8d7e28 100644 --- a/packages/tui/src/theme/v2/expand.ts +++ b/packages/theme/src/tui/expand.ts @@ -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: Definition): Definition { return { diff --git a/packages/theme/src/tui/fallback.ts b/packages/theme/src/tui/fallback.ts new file mode 100644 index 00000000000..20325a72e40 --- /dev/null +++ b/packages/theme/src/tui/fallback.ts @@ -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, + }, + } +} diff --git a/packages/tui/src/theme/v2/index.ts b/packages/theme/src/tui/index.ts similarity index 74% rename from packages/tui/src/theme/v2/index.ts rename to packages/theme/src/tui/index.ts index b6cfbb98f52..0ad7eab5ceb 100644 --- a/packages/tui/src/theme/v2/index.ts +++ b/packages/theme/src/tui/index.ts @@ -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" diff --git a/packages/tui/src/theme/v2/resolve.ts b/packages/theme/src/tui/resolve.ts similarity index 97% rename from packages/tui/src/theme/v2/resolve.ts rename to packages/theme/src/tui/resolve.ts index 500f853dbd8..4443e88e5de 100644 --- a/packages/tui/src/theme/v2/resolve.ts +++ b/packages/theme/src/tui/resolve.ts @@ -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) diff --git a/packages/tui/src/theme/v2/schema.ts b/packages/theme/src/tui/schema.ts similarity index 100% rename from packages/tui/src/theme/v2/schema.ts rename to packages/theme/src/tui/schema.ts diff --git a/packages/tui/src/theme/v2/select.ts b/packages/theme/src/tui/select.ts similarity index 96% rename from packages/tui/src/theme/v2/select.ts rename to packages/theme/src/tui/select.ts index 17d47488fa1..261e6c3a6db 100644 --- a/packages/tui/src/theme/v2/select.ts +++ b/packages/theme/src/tui/select.ts @@ -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 }, diff --git a/packages/tui/src/theme/v2/syntax.ts b/packages/theme/src/tui/syntax.ts similarity index 98% rename from packages/tui/src/theme/v2/syntax.ts rename to packages/theme/src/tui/syntax.ts index 4a8917f089d..6330c9913a2 100644 --- a/packages/tui/src/theme/v2/syntax.ts +++ b/packages/theme/src/tui/syntax.ts @@ -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 diff --git a/packages/tui/src/theme/v2/types.ts b/packages/theme/src/tui/types.ts similarity index 99% rename from packages/tui/src/theme/v2/types.ts rename to packages/theme/src/tui/types.ts index ff21ccfa604..66e9333a9a0 100644 --- a/packages/tui/src/theme/v2/types.ts +++ b/packages/theme/src/tui/types.ts @@ -9,7 +9,7 @@ import type { MarkdownToken, ContextKey, SyntaxToken, -} from "./schema" +} from "./schema.js" export type ResolvedActionState = "default" | ActionState export type ResolvedFormfieldState = ResolvedActionState diff --git a/packages/tui/src/theme/v2/v1-migrate.ts b/packages/theme/src/tui/v1-migrate.ts similarity index 98% rename from packages/tui/src/theme/v2/v1-migrate.ts rename to packages/theme/src/tui/v1-migrate.ts index ac81ee4f5c5..1269f631451 100644 --- a/packages/tui/src/theme/v2/v1-migrate.ts +++ b/packages/theme/src/tui/v1-migrate.ts @@ -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 type ChromaticHue = "red" | "orange" | "yellow" | "green" | "cyan" | "blue" | "purple" diff --git a/packages/theme/src/tui/v1.ts b/packages/theme/src/tui/v1.ts new file mode 100644 index 00000000000..4765630edf9 --- /dev/null +++ b/packages/theme/src/tui/v1.ts @@ -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 +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 + theme: Omit, "selectedListItemText" | "backgroundMenu"> & { + selectedListItemText?: ColorValue + backgroundMenu?: ColorValue + thinkingOpacity?: number + } +} diff --git a/packages/theme/sst-env.d.ts b/packages/theme/sst-env.d.ts new file mode 100644 index 00000000000..f25b9714550 --- /dev/null +++ b/packages/theme/sst-env.d.ts @@ -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 */ + +/// + +import "sst" +export {} diff --git a/packages/theme/tsconfig.build.json b/packages/theme/tsconfig.build.json new file mode 100644 index 00000000000..e235ae78cf6 --- /dev/null +++ b/packages/theme/tsconfig.build.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "./tsconfig.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "dist", + "noEmit": false, + "declaration": true + }, + "include": ["src"] +} diff --git a/packages/theme/tsconfig.json b/packages/theme/tsconfig.json new file mode 100644 index 00000000000..189db6f3e96 --- /dev/null +++ b/packages/theme/tsconfig.json @@ -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 + } +} diff --git a/packages/tui/package.json b/packages/tui/package.json index 0fb182f8d35..95945eb5daf 100644 --- a/packages/tui/package.json +++ b/packages/tui/package.json @@ -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:", diff --git a/packages/tui/src/context/theme.tsx b/packages/tui/src/context/theme.tsx index 9b53e8d07a2..699653a51a9 100644 --- a/packages/tui/src/context/theme.tsx +++ b/packages/tui/src/context/theme.tsx @@ -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" diff --git a/packages/tui/src/theme/v2/component.ts b/packages/tui/src/theme/component.ts similarity index 94% rename from packages/tui/src/theme/v2/component.ts rename to packages/tui/src/theme/component.ts index 797d4b8b734..28e52658d70 100644 --- a/packages/tui/src/theme/v2/component.ts +++ b/packages/tui/src/theme/component.ts @@ -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, mode: Accessor) { return { diff --git a/packages/tui/src/theme/index.ts b/packages/tui/src/theme/index.ts index d128a75539f..320b894137a 100644 --- a/packages/tui/src/theme/index.ts +++ b/packages/tui/src/theme/index.ts @@ -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 } diff --git a/packages/tui/src/theme/v1.ts b/packages/tui/src/theme/v1.ts index 5cf1df1b9f2..6946f9cf728 100644 --- a/packages/tui/src/theme/v1.ts +++ b/packages/tui/src/theme/v1.ts @@ -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 -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 - theme: Omit, "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 = { aura, diff --git a/packages/tui/src/theme/v2/fallback.ts b/packages/tui/src/theme/v2/fallback.ts deleted file mode 100644 index ed4137bc0f2..00000000000 --- a/packages/tui/src/theme/v2/fallback.ts +++ /dev/null @@ -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, - }, - } -} diff --git a/packages/tui/src/theme/v2/solid.ts b/packages/tui/src/theme/v2/solid.ts deleted file mode 100644 index d7088d208bd..00000000000 --- a/packages/tui/src/theme/v2/solid.ts +++ /dev/null @@ -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 - readonly mode: Accessor - readonly component: ComponentTheme -} - -const ThemeContext = createContext() - -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 -} diff --git a/packages/tui/test/cli/tui/theme-mode.test.tsx b/packages/tui/test/cli/tui/theme-mode.test.tsx index 0b8123f05c7..34aa59ffd73 100644 --- a/packages/tui/test/cli/tui/theme-mode.test.tsx +++ b/packages/tui/test/cli/tui/theme-mode.test.tsx @@ -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" diff --git a/packages/tui/test/theme/v2/component.test.ts b/packages/tui/test/theme/v2/component.test.ts index 469624f8ba7..87c3265777f 100644 --- a/packages/tui/test/theme/v2/component.test.ts +++ b/packages/tui/test/theme/v2/component.test.ts @@ -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"))) diff --git a/packages/tui/test/theme/v2/resolve.test.ts b/packages/tui/test/theme/v2/resolve.test.ts index 9d42b403413..f3e3837de6f 100644 --- a/packages/tui/test/theme/v2/resolve.test.ts +++ b/packages/tui/test/theme/v2/resolve.test.ts @@ -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") diff --git a/packages/tui/test/theme/v2/select.test.ts b/packages/tui/test/theme/v2/select.test.ts index 00f0bd437de..9da37b2ac3c 100644 --- a/packages/tui/test/theme/v2/select.test.ts +++ b/packages/tui/test/theme/v2/select.test.ts @@ -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 diff --git a/packages/tui/test/theme/v2/types.test.ts b/packages/tui/test/theme/v2/types.test.ts index f98a013f5ef..09f6dce6ba9 100644 --- a/packages/tui/test/theme/v2/types.test.ts +++ b/packages/tui/test/theme/v2/types.test.ts @@ -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", diff --git a/packages/tui/test/theme/v2/v1-migrate.test.ts b/packages/tui/test/theme/v2/v1-migrate.test.ts index fed2c9ba79b..31b5b25e5df 100644 --- a/packages/tui/test/theme/v2/v1-migrate.test.ts +++ b/packages/tui/test/theme/v2/v1-migrate.test.ts @@ -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) diff --git a/packages/www/package.json b/packages/www/package.json index 1774df10353..d687bd8ea0e 100644 --- a/packages/www/package.json +++ b/packages/www/package.json @@ -18,6 +18,7 @@ }, "devDependencies": { "@astrojs/cloudflare": "14.1.4", + "@opencode-ai/theme": "workspace:*", "@types/bun": "catalog:", "astro": "7.1.3", "effect": "catalog:", diff --git a/packages/www/script/generate-theme-tokens.ts b/packages/www/script/generate-theme-tokens.ts index 0c849377142..fc76c57d8c2 100644 --- a/packages/www/script/generate-theme-tokens.ts +++ b/packages/www/script/generate-theme-tokens.ts @@ -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 diff --git a/packages/www/snippets/generated/theme-tokens.mdx b/packages/www/snippets/generated/theme-tokens.mdx index 39873c35714..03ca51c7a85 100644 --- a/packages/www/snippets/generated/theme-tokens.mdx +++ b/packages/www/snippets/generated/theme-tokens.mdx @@ -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 diff --git a/script/publish.ts b/script/publish.ts index 3623eeb7776..4baf6b03b77 100755 --- a/script/publish.ts +++ b/script/publish.ts @@ -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`