136 lines
4.3 KiB
TypeScript
136 lines
4.3 KiB
TypeScript
#!/usr/bin/env bun
|
|
|
|
import { Schema, SchemaAST } from "effect"
|
|
import { format } from "prettier"
|
|
import { ThemeDefinition, ThemeFile } from "../../tui/src/theme/v2/schema"
|
|
|
|
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 ThemeFile
|
|
Schema.decodeUnknownSync(ThemeFile)(example)
|
|
const output = await format(
|
|
`{/* Generated by packages/docs/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
|
|
\`packages/tui/src/theme/v2/schema.ts\`. 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/docs.")
|
|
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)}`))
|
|
}
|