| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136 |
- #!/usr/bin/env bun
- import { Schema, SchemaAST } from "effect"
- import { format } from "prettier"
- import { ThemeDefinition, ThemeDocument } from "@opencode-ai/theme/tui"
- const target = import.meta.dir + "/../snippets/generated/theme-tokens.mdx"
- const root = requireObject(ThemeDefinition.ast)
- const hue = requireObject(requireField(root, "hue").type)
- const hueNames = hue.propertySignatures.map((field) => String(field.name))
- const hueSteps = requireObject(requireField(hue, hueNames[0]).type).propertySignatures.map((field) =>
- String(field.name),
- )
- const contexts = root.propertySignatures
- .map((field) => String(field.name))
- .filter((name) => name.startsWith("@context:"))
- const tokens = root.propertySignatures
- .filter((field) => {
- const name = String(field.name)
- return name !== "hue" && name !== "categorical" && !name.startsWith("@context:")
- })
- .flatMap((field) => tokenPaths(field.type, String(field.name)))
- const groups = Map.groupBy(tokens, (token) =>
- token
- .split(".")
- .slice(0, token.split(".").length > 2 ? 2 : 1)
- .join("."),
- )
- const table = [...groups]
- .map(([group, values]) => `| \`${group}\` | ${values.map((value) => `\`${value}\``).join("<br />")} |`)
- .join("\n")
- const example = {
- version: 2,
- light: {
- hue: {
- accent: "$hue.purple",
- interactive: "$hue.purple",
- },
- text: {
- default: "$hue.neutral.900",
- },
- background: {
- default: "#fafafa",
- },
- },
- dark: {
- mergeMode: true,
- text: {
- default: "$hue.neutral.100",
- },
- background: {
- default: "#101014",
- },
- },
- } satisfies ThemeDocument
- Schema.decodeUnknownSync(ThemeDocument)(example)
- const output = await format(
- `{/* Generated by packages/www/script/generate-theme-tokens.ts. Do not edit. */}
- \`\`\`json title="my-theme.json"
- ${JSON.stringify(example, null, 2)}
- \`\`\`
- ## Token reference
- This reference is generated from the Effect schema in
- \`@opencode-ai/theme/tui\`. Changes to the runtime schema update
- this section through \`bun run generate\`.
- ### Hue tokens
- Every hue is a ${hueSteps.length}-step scale. Define a scale with all of these
- steps, or alias it to another hue with a value such as \`$hue.blue\`.
- | | Values |
- | --- | --- |
- | Hues | ${hueNames.map((name) => `\`${name}\``).join(", ")} |
- | Steps | ${hueSteps.map((step) => `\`${step}\``).join(", ")} |
- Reference a hue color as \`$hue.<name>.<step>\`, for example
- \`$hue.interactive.500\`.
- ### Semantic tokens
- Semantic values can reference another token by prefixing its path with \`$\`,
- for example \`$text.default\`. Stateful tokens inherit their \`default\`
- value when a state is omitted.
- | Group | Tokens |
- | --- | --- |
- ${table}
- ### Contexts
- ${contexts.map((context) => `\`${context}\``).join(" and ")} accept partial
- overrides of the semantic tokens above. Components apply these contexts to
- surfaces that need different contrast without changing the base theme.
- `,
- { parser: "mdx", printWidth: 120, semi: false },
- )
- if (process.argv.includes("--check")) {
- const current = await Bun.file(target).text()
- if (current === output) process.exit(0)
- console.error("Generated theme token documentation is stale. Run `bun run generate` from packages/www.")
- process.exit(1)
- }
- await Bun.write(target, output)
- function requireObject(ast: SchemaAST.AST): SchemaAST.Objects {
- if (SchemaAST.isObjects(ast)) return ast
- if (SchemaAST.isUnion(ast)) {
- const object = ast.types.map(findObject).find((value) => value !== undefined)
- if (object) return object
- }
- throw new Error(`Expected an object schema, received ${ast._tag}`)
- }
- function findObject(ast: SchemaAST.AST): SchemaAST.Objects | undefined {
- if (SchemaAST.isObjects(ast)) return ast
- if (SchemaAST.isUnion(ast)) return ast.types.map(findObject).find((value) => value !== undefined)
- if (SchemaAST.isSuspend(ast)) return findObject(ast.thunk())
- }
- function requireField(ast: SchemaAST.Objects, name: string) {
- const field = ast.propertySignatures.find((field) => String(field.name) === name)
- if (field) return field
- throw new Error(`Theme schema field not found: ${name}`)
- }
- function tokenPaths(ast: SchemaAST.AST, prefix: string): string[] {
- const object = findObject(ast)
- if (!object || object.propertySignatures.length === 0) return [prefix]
- return object.propertySignatures.flatMap((field) => tokenPaths(field.type, `${prefix}.${String(field.name)}`))
- }
|