@mohou/host 1.0.20

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 (108) hide show
  1. package/README.md +10 -0
  2. package/package.json +65 -0
  3. package/src/apps/monogram.ts +15 -0
  4. package/src/apps/registry.ts +238 -0
  5. package/src/apps/startup-seed.ts +95 -0
  6. package/src/bash/client.ts +21 -0
  7. package/src/bash/codes.ts +21 -0
  8. package/src/compile/allowlist.ts +89 -0
  9. package/src/compile/asset-path.ts +48 -0
  10. package/src/compile/autogen.ts +71 -0
  11. package/src/compile/build-vendor-cli.ts +8 -0
  12. package/src/compile/build-vendor.ts +160 -0
  13. package/src/compile/bundle-ui.ts +137 -0
  14. package/src/compile/codes.ts +22 -0
  15. package/src/compile/failure.ts +38 -0
  16. package/src/compile/imports.ts +43 -0
  17. package/src/compile/leave.ts +47 -0
  18. package/src/compile/load-backend.ts +95 -0
  19. package/src/compile/review.ts +342 -0
  20. package/src/compile/runner.ts +205 -0
  21. package/src/compile/sheet.ts +174 -0
  22. package/src/compile/wrapper.ts +297 -0
  23. package/src/credentials/codes.ts +26 -0
  24. package/src/credentials/create.ts +69 -0
  25. package/src/credentials/file.ts +113 -0
  26. package/src/credentials/provider.ts +66 -0
  27. package/src/events/app-events.ts +214 -0
  28. package/src/events/error-ring.ts +109 -0
  29. package/src/events/host-events.ts +39 -0
  30. package/src/events/view-bridge.ts +402 -0
  31. package/src/events/view-eval.ts +213 -0
  32. package/src/files/codes.ts +28 -0
  33. package/src/files/skip.ts +23 -0
  34. package/src/files/tools.ts +215 -0
  35. package/src/history/codes.ts +22 -0
  36. package/src/history/store.ts +287 -0
  37. package/src/host/activity.ts +110 -0
  38. package/src/host/author-mcp.ts +248 -0
  39. package/src/host/author-skill.ts +196 -0
  40. package/src/host/codes.ts +21 -0
  41. package/src/host/config.ts +239 -0
  42. package/src/host/heat.ts +35 -0
  43. package/src/host/layout.ts +81 -0
  44. package/src/host/log.ts +142 -0
  45. package/src/host/mcp-editor.ts +205 -0
  46. package/src/host/mcp-import.ts +141 -0
  47. package/src/host/mcp.ts +109 -0
  48. package/src/host/port.ts +83 -0
  49. package/src/host/session.ts +358 -0
  50. package/src/host/workbench.ts +38 -0
  51. package/src/http/app.ts +46 -0
  52. package/src/http/asset.ts +49 -0
  53. package/src/http/author.ts +157 -0
  54. package/src/http/bind.ts +217 -0
  55. package/src/http/client.ts +175 -0
  56. package/src/http/codes.ts +29 -0
  57. package/src/http/env.ts +3 -0
  58. package/src/http/guard.ts +20 -0
  59. package/src/http/iframe.ts +212 -0
  60. package/src/http/layout.ts +63 -0
  61. package/src/http/owner.ts +380 -0
  62. package/src/http/ports.ts +114 -0
  63. package/src/http/reply.ts +53 -0
  64. package/src/http/route-codes.ts +19 -0
  65. package/src/http/updates.ts +258 -0
  66. package/src/index.ts +201 -0
  67. package/src/install/codes.ts +21 -0
  68. package/src/install/install.ts +230 -0
  69. package/src/install/layout.ts +16 -0
  70. package/src/kernel/bind-brain.ts +117 -0
  71. package/src/kernel/call.ts +134 -0
  72. package/src/kernel/codes.ts +21 -0
  73. package/src/kernel/index.ts +13 -0
  74. package/src/kernel/model-policy.ts +83 -0
  75. package/src/kernel/model-stream.ts +86 -0
  76. package/src/kernel/present.ts +4 -0
  77. package/src/kernel/working-directory.ts +35 -0
  78. package/src/metrics/codes.ts +22 -0
  79. package/src/metrics/read.ts +48 -0
  80. package/src/owner/read.ts +85 -0
  81. package/src/pwsh/client.ts +47 -0
  82. package/src/pwsh/codes.ts +21 -0
  83. package/src/shell/command.ts +172 -0
  84. package/src/storage/codes.ts +33 -0
  85. package/src/storage/layout.ts +44 -0
  86. package/src/storage/open.ts +461 -0
  87. package/src/storage/schema.ts +176 -0
  88. package/src/storage/sql.ts +40 -0
  89. package/src/theme/codes.ts +20 -0
  90. package/src/theme/layout.ts +42 -0
  91. package/src/theme/paint.ts +108 -0
  92. package/src/theme/parse.ts +58 -0
  93. package/src/theme/pin.ts +197 -0
  94. package/src/theme/tokens.ts +30 -0
  95. package/src/tools/author.ts +970 -0
  96. package/src/tools/codes.ts +21 -0
  97. package/src/tools/diagnostics.ts +53 -0
  98. package/src/tools/http.ts +48 -0
  99. package/src/tools/mcp-list.ts +90 -0
  100. package/src/tools/schemas.ts +185 -0
  101. package/themes/theme-default.css +4 -0
  102. package/themes/theme-forest.css +4 -0
  103. package/themes/theme-hokage.css +4 -0
  104. package/themes/theme-matcha.css +4 -0
  105. package/themes/theme-slate.css +4 -0
  106. package/themes/theme-tokyo.css +4 -0
  107. package/themes/theme-yellow.css +4 -0
  108. package/themes/theme-zoro.css +4 -0
@@ -0,0 +1,42 @@
1
+ import path from 'node:path'
2
+ import { fileURLToPath } from 'node:url'
3
+
4
+ /** Theme file names and pin sentinels. Spell them here, not at each call site. */
5
+ export const themeLayout = {
6
+ appCss: 'theme.css',
7
+ appPin: 'theme.json',
8
+ followHost: '__global__',
9
+ appFile: '__local__',
10
+ customPrefix: 'theme-',
11
+ customSuffix: '.css',
12
+ } as const
13
+
14
+ const CUSTOM_ID = /^[a-z0-9-]+$/
15
+
16
+ export function appThemeCss(appDir: string): string {
17
+ return path.join(appDir, themeLayout.appCss)
18
+ }
19
+
20
+ export function appThemePin(appDir: string): string {
21
+ return path.join(appDir, themeLayout.appPin)
22
+ }
23
+
24
+ export function customThemePath(themesDir: string, id: string): string {
25
+ return path.join(themesDir, `${themeLayout.customPrefix}${id}${themeLayout.customSuffix}`)
26
+ }
27
+
28
+ /** Shipped theme files. Same contract as a custom file. Ids come from the filenames. */
29
+ export function builtinThemesDir(): string {
30
+ return fileURLToPath(new URL('../../themes', import.meta.url))
31
+ }
32
+
33
+ export function builtinThemePath(id: string): string {
34
+ return customThemePath(builtinThemesDir(), id)
35
+ }
36
+
37
+ /** Parse `theme-<id>.css`. A name that does not match is not a palette id. */
38
+ export function customThemeId(filename: string): string | undefined {
39
+ if (!filename.startsWith(themeLayout.customPrefix) || !filename.endsWith(themeLayout.customSuffix)) return undefined
40
+ const id = filename.slice(themeLayout.customPrefix.length, -themeLayout.customSuffix.length)
41
+ return CUSTOM_ID.test(id) ? id : undefined
42
+ }
@@ -0,0 +1,108 @@
1
+ import { readFile } from 'node:fs/promises'
2
+
3
+ import type { AppPin } from './pin.ts'
4
+ import { themeTokens } from './tokens.ts'
5
+ import { parseThemeCss } from './parse.ts'
6
+ import { appThemeCss, builtinThemePath, customThemePath } from './layout.ts'
7
+
8
+ export interface FirstPaint {
9
+ source: 'app-file' | 'custom' | 'builtin' | 'host'
10
+ paletteId: string | null
11
+ ignored?: string
12
+ light: Record<string, string>
13
+ dark: Record<string, string>
14
+ style: string
15
+ }
16
+
17
+ /**
18
+ * Choose the palette that paints the first frame. Token names are copied, not renamed.
19
+ * A file that fails to parse is ignored. The next source in order is used.
20
+ * @param input - pin, host palette id, and the directories that hold theme files
21
+ */
22
+ export async function resolveFirstPaint(input: {
23
+ appDir: string
24
+ pin: AppPin
25
+ hostPalette: string
26
+ themesDir: string
27
+ }): Promise<FirstPaint> {
28
+ if (input.pin.kind === 'follow-host') return await hostPaint(input.hostPalette, input.themesDir)
29
+ if (input.pin.kind === 'palette') {
30
+ const chosen = await readPalette(input.themesDir, input.pin.id)
31
+ if (chosen !== undefined) return chosen
32
+ }
33
+ const local = await readPaint(appThemeCss(input.appDir), 'app', 'app-file')
34
+ const ignored = input.pin.kind === 'palette'
35
+ ? `custom palette ${input.pin.id} was ignored`
36
+ : input.pin.kind === 'app-file' ? 'app theme.css was ignored' : undefined
37
+ if (local !== undefined) return { ...local, ...ignored === undefined ? {} : { ignored } }
38
+ const host = await hostPaint(input.hostPalette, input.themesDir)
39
+ return { ...host, ...ignored === undefined ? {} : { ignored } }
40
+ }
41
+
42
+ /** Both modes from one theme file. The picker applies this without reloading a document. */
43
+ export function styleFromThemeCss(css: string): string {
44
+ return firstPaintStyle(varsFor(css, 'light'), varsFor(css, 'dark'))
45
+ }
46
+
47
+ /** CSS to bake into the first HTML response. Empty when the source has no file. */
48
+ export function firstPaintStyle(light: Record<string, string>, dark: Record<string, string>): string {
49
+ return `${block('light', light)}${block('dark', dark)}`
50
+ }
51
+
52
+ async function hostPaint(paletteId: string, themesDir: string): Promise<FirstPaint> {
53
+ return await readPalette(themesDir, paletteId) ?? { source: 'host', paletteId, light: {}, dark: {}, style: '' }
54
+ }
55
+
56
+ async function readPalette(themesDir: string, id: string): Promise<FirstPaint | undefined> {
57
+ const custom = await readPaint(customThemePath(themesDir, id), id, 'custom')
58
+ if (custom !== undefined) return { ...custom, paletteId: id }
59
+ const builtin = await readPaint(builtinThemePath(id), id, 'builtin')
60
+ return builtin === undefined ? undefined : { ...builtin, paletteId: id }
61
+ }
62
+
63
+ async function readPaint(
64
+ file: string,
65
+ fallbackName: string,
66
+ source: 'app-file' | 'custom' | 'builtin',
67
+ ): Promise<FirstPaint | undefined> {
68
+ const css = await readFile(file, 'utf8').catch(() => undefined)
69
+ if (css === undefined) return undefined
70
+ const parsed = parseThemeCss(css, fallbackName)
71
+ if (!parsed.ok) return undefined
72
+ const light = varsFor(css, 'light')
73
+ const dark = varsFor(css, 'dark')
74
+ return { source, paletteId: null, light, dark, style: firstPaintStyle(light, dark) }
75
+ }
76
+
77
+ function varsFor(css: string, mode: 'light' | 'dark'): Record<string, string> {
78
+ const body = modeBlock(css, mode)
79
+ if (body === undefined) return {}
80
+ const vars: Record<string, string> = {}
81
+ for (const token of themeTokens) {
82
+ const value = tokenValue(body, token)
83
+ if (value === undefined) continue
84
+ vars[`--${token}`] = value
85
+ }
86
+ return vars
87
+ }
88
+
89
+ function modeBlock(css: string, mode: 'light' | 'dark'): string | undefined {
90
+ const at = css.indexOf(`data-mode="${mode}"`)
91
+ if (at < 0) return undefined
92
+ const open = css.indexOf('{', at)
93
+ const close = open < 0 ? -1 : css.indexOf('}', open)
94
+ if (open < 0 || close < 0) return undefined
95
+ return css.slice(open + 1, close)
96
+ }
97
+
98
+ function tokenValue(block: string, token: string): string | undefined {
99
+ const match = new RegExp(`(?:^|[\\s;{])--${token}\\s*:\\s*([^;\\n]+)`).exec(block)
100
+ const value = match?.[1]?.trim()
101
+ return value === undefined || value.length === 0 ? undefined : value
102
+ }
103
+
104
+ function block(mode: 'light' | 'dark', vars: Record<string, string>): string {
105
+ const body = Object.entries(vars).map(([name, value]) => `${name}:${value}`).join(';')
106
+ if (body.length === 0) return ''
107
+ return `:root[data-mode="${mode}"]{${body}}`
108
+ }
@@ -0,0 +1,58 @@
1
+ import { requiredThemeTokens } from './tokens.ts'
2
+
3
+ /** Token names required in both modes. A short name such as `--bg` does not count. */
4
+
5
+ export interface ParsedTheme {
6
+ /** Display name for English (and fallback). */
7
+ name: string
8
+ /** Optional Chinese display name. */
9
+ nameZh?: string
10
+ }
11
+
12
+ /**
13
+ * Admit a theme file. A missing required key ignores the file. It is not partially applied.
14
+ * @param css - file text
15
+ * @param fallbackName - id used when the header has no name
16
+ */
17
+ export function parseThemeCss(css: string, fallbackName: string): { ok: true; theme: ParsedTheme } | { ok: false; reason: string } {
18
+ for (const mode of ['light', 'dark'] as const) {
19
+ const block = modeBlock(css, mode)
20
+ if (block === undefined) return { ok: false, reason: `missing ${mode} mode` }
21
+ for (const key of requiredThemeTokens) {
22
+ if (!hasShort(block, key)) return { ok: false, reason: `missing --${key} in ${mode}` }
23
+ }
24
+ }
25
+ return { ok: true, theme: headerNames(css, fallbackName) }
26
+ }
27
+
28
+ function modeBlock(css: string, mode: 'light' | 'dark'): string | undefined {
29
+ const at = css.indexOf(`data-mode="${mode}"`)
30
+ if (at < 0) return undefined
31
+ const open = css.indexOf('{', at)
32
+ const close = open < 0 ? -1 : css.indexOf('}', open)
33
+ if (open < 0 || close < 0) return undefined
34
+ return css.slice(open + 1, close)
35
+ }
36
+
37
+ function hasShort(block: string, key: string): boolean {
38
+ return new RegExp(`(?:^|[\\s;{])--${key}\\s*:`).test(block)
39
+ }
40
+
41
+ /**
42
+ * `/* name: English *\/` and optional `/* name-zh-CN: 中文 *\/`.
43
+ * A single `name` that is only Chinese still works as the sole label.
44
+ */
45
+ function headerNames(css: string, fallbackName: string): ParsedTheme {
46
+ const name = headerField(css, 'name')
47
+ const nameZh = headerField(css, 'name-zh-CN')
48
+ if (name !== undefined && nameZh !== undefined) return { name, nameZh }
49
+ if (name !== undefined) return { name }
50
+ if (nameZh !== undefined) return { name: nameZh, nameZh }
51
+ return { name: fallbackName }
52
+ }
53
+
54
+ function headerField(css: string, field: string): string | undefined {
55
+ const match = new RegExp(`/\\*\\s*${field}:\\s*([^*]+?)\\s*\\*/`).exec(css)
56
+ const value = match?.[1]?.trim()
57
+ return value === undefined || value.length === 0 ? undefined : value
58
+ }
@@ -0,0 +1,197 @@
1
+ import { readdir, readFile, rm, writeFile } from 'node:fs/promises'
2
+
3
+ import type { AppSummary } from '../apps/registry.ts'
4
+ import { builtinThemesDir, customThemeId, customThemePath, themeLayout, appThemeCss, appThemePin } from './layout.ts'
5
+ import { ThemeError } from './codes.ts'
6
+ import { styleFromThemeCss } from './paint.ts'
7
+ import { parseThemeCss } from './parse.ts'
8
+
9
+ export type AppPin =
10
+ | { kind: 'default' }
11
+ | { kind: 'follow-host' }
12
+ | { kind: 'app-file' }
13
+ | { kind: 'palette'; id: string }
14
+
15
+ export interface PaletteList {
16
+ palettes: Array<{ id: string; name: string; nameZh?: string; swatch: string; style: string; origin: 'builtin' | 'custom' }>
17
+ ignored: Array<{ file: string; reason: string }>
18
+ }
19
+
20
+ interface Registry {
21
+ get: (appId: string) => Promise<AppSummary>
22
+ }
23
+
24
+ /**
25
+ * Owner pin and palette list. Panel HTTP calls these through the loopback routes.
26
+ * @param registry - registered apps
27
+ * @param themesDir - host-global custom theme directory; creating it is allowed, listing does not require it
28
+ */
29
+ export function createThemePins(registry: Registry, themesDir: string) {
30
+ return {
31
+ listPalettes: () => listPalettes(themesDir),
32
+ readPin: (appId: string) => readPin(registry, appId),
33
+ appFile: (appId: string) => appFileParsed(registry, appId),
34
+ readAppTheme: (appId: string) => readAppTheme(registry, appId),
35
+ setPin: (appId: string, pin: AppPin) => setPin(registry, themesDir, appId, pin),
36
+ }
37
+ }
38
+
39
+ async function listPalettes(themesDir: string): Promise<PaletteList> {
40
+ const palettes = await readThemeDir(builtinThemesDir(), [])
41
+ const ignored: PaletteList['ignored'] = []
42
+ const names = await readdir(themesDir).catch(() => [])
43
+ for (const file of names) {
44
+ const id = customThemeId(file)
45
+ if (id === undefined) {
46
+ ignored.push({ file, reason: 'file name is not theme-<id>.css' })
47
+ continue
48
+ }
49
+ const text = await readFile(customThemePath(themesDir, id), 'utf8').catch(() => undefined)
50
+ if (text === undefined) {
51
+ ignored.push({ file, reason: 'theme file is unreadable' })
52
+ continue
53
+ }
54
+ const parsed = parseThemeCss(text, id)
55
+ if (!parsed.ok) {
56
+ ignored.push({ file, reason: parsed.reason })
57
+ continue
58
+ }
59
+ const at = palettes.findIndex(item => item.id === id)
60
+ if (at >= 0) palettes.splice(at, 1)
61
+ palettes.push(paletteRow(id, parsed.theme, text, 'custom'))
62
+ }
63
+ return { palettes, ignored }
64
+ }
65
+
66
+ async function readThemeDir(dir: string, ignored: PaletteList['ignored']): Promise<PaletteList['palettes']> {
67
+ const palettes: PaletteList['palettes'] = []
68
+ const names = await readdir(dir).catch(() => [])
69
+ for (const file of names) {
70
+ const id = customThemeId(file)
71
+ if (id === undefined) continue
72
+ const text = await readFile(customThemePath(dir, id), 'utf8').catch(() => undefined)
73
+ if (text === undefined) continue
74
+ const parsed = parseThemeCss(text, id)
75
+ if (!parsed.ok) {
76
+ ignored.push({ file, reason: parsed.reason })
77
+ continue
78
+ }
79
+ palettes.push(paletteRow(id, parsed.theme, text, 'builtin'))
80
+ }
81
+ return palettes
82
+ }
83
+
84
+ function paletteRow(
85
+ id: string,
86
+ theme: { name: string; nameZh?: string },
87
+ text: string,
88
+ origin: 'builtin' | 'custom',
89
+ ): PaletteList['palettes'][number] {
90
+ return {
91
+ id,
92
+ name: theme.name,
93
+ ...theme.nameZh === undefined ? {} : { nameZh: theme.nameZh },
94
+ swatch: swatchOf(text),
95
+ style: styleFromThemeCss(text),
96
+ origin,
97
+ }
98
+ }
99
+
100
+ function swatchOf(css: string): string {
101
+ const light = css.split('[data-mode="dark"]')[0] ?? css
102
+ const match = /--primary:\s*([^;]+)/.exec(light)
103
+ const value = match?.[1]?.trim()
104
+ return value !== undefined && value.length > 0 ? value : '#888'
105
+ }
106
+
107
+ async function readPin(registry: Registry, appId: string): Promise<AppPin> {
108
+ const app = await registry.get(appId)
109
+ const text = await readFile(appThemePin(app.directory), 'utf8').catch(() => undefined)
110
+ if (text === undefined) return { kind: 'default' }
111
+ return admitPin(text)
112
+ }
113
+
114
+ async function setPin(registry: Registry, themesDir: string, appId: string, pin: AppPin): Promise<AppPin> {
115
+ const app = await registry.get(appId)
116
+ const file = appThemePin(app.directory)
117
+ if (pin.kind === 'default') {
118
+ await rm(file, { force: true })
119
+ return { kind: 'default' }
120
+ }
121
+ if (pin.kind === 'palette') await assertPalette(themesDir, pin.id)
122
+ if (pin.kind === 'app-file') await assertAppFile(app.directory)
123
+ const previous = await readFile(file, 'utf8').catch(() => undefined)
124
+ try {
125
+ await writeFile(file, `${JSON.stringify({ palette: stored(pin) })}\n`)
126
+ return pin
127
+ } catch (error) {
128
+ if (previous === undefined) await rm(file, { force: true })
129
+ else await writeFile(file, previous)
130
+ throw error
131
+ }
132
+ }
133
+
134
+ async function assertPalette(themesDir: string, id: string): Promise<void> {
135
+ const listed = await listPalettes(themesDir)
136
+ if (listed.palettes.some(item => item.id === id)) return
137
+ throw new ThemeError('theme-invalid', `unknown palette: ${id}`)
138
+ }
139
+
140
+ async function assertAppFile(appDir: string): Promise<void> {
141
+ const parsed = await readAppFile(appDir)
142
+ if (parsed === undefined) throw new ThemeError('theme-invalid', 'app theme.css is missing')
143
+ if (!parsed.ok) throw new ThemeError('theme-invalid', parsed.reason)
144
+ }
145
+
146
+ async function appFileParsed(registry: Registry, appId: string): Promise<boolean> {
147
+ const app = await registry.get(appId)
148
+ const parsed = await readAppFile(app.directory)
149
+ return parsed?.ok === true
150
+ }
151
+
152
+ async function readAppTheme(registry: Registry, appId: string): Promise<{
153
+ name?: string
154
+ nameZh?: string
155
+ swatch: string
156
+ style: string
157
+ } | null> {
158
+ const app = await registry.get(appId)
159
+ const text = await readFile(appThemeCss(app.directory), 'utf8').catch(() => undefined)
160
+ if (text === undefined) return null
161
+ const parsed = parseThemeCss(text, 'app')
162
+ if (!parsed.ok) return null
163
+ const named = parsed.theme.name !== 'app'
164
+ return {
165
+ ...named ? { name: parsed.theme.name } : {},
166
+ ...parsed.theme.nameZh === undefined ? {} : { nameZh: parsed.theme.nameZh },
167
+ swatch: swatchOf(text),
168
+ style: styleFromThemeCss(text),
169
+ }
170
+ }
171
+
172
+ async function readAppFile(appDir: string) {
173
+ const text = await readFile(appThemeCss(appDir), 'utf8').catch(() => undefined)
174
+ if (text === undefined) return undefined
175
+ return parseThemeCss(text, themeLayout.appFile)
176
+ }
177
+
178
+ function stored(pin: Exclude<AppPin, { kind: 'default' }>): string {
179
+ if (pin.kind === 'follow-host') return themeLayout.followHost
180
+ if (pin.kind === 'app-file') return themeLayout.appFile
181
+ return pin.id
182
+ }
183
+
184
+ function admitPin(text: string): AppPin {
185
+ let parsed: unknown
186
+ try {
187
+ parsed = JSON.parse(text) as unknown
188
+ } catch {
189
+ return { kind: 'default' }
190
+ }
191
+ if (typeof parsed !== 'object' || parsed === null || !('palette' in parsed) || typeof parsed.palette !== 'string') {
192
+ return { kind: 'default' }
193
+ }
194
+ if (parsed.palette === themeLayout.followHost) return { kind: 'follow-host' }
195
+ if (parsed.palette === themeLayout.appFile) return { kind: 'app-file' }
196
+ return { kind: 'palette', id: parsed.palette }
197
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Theme tokens. The theme file declares these names. There is no second spelling.
3
+ * A missing optional token is omitted. No invented fallback colour.
4
+ */
5
+ export const themeTokens = [
6
+ 'background',
7
+ 'foreground',
8
+ 'card',
9
+ 'card-foreground',
10
+ 'border',
11
+ 'muted',
12
+ 'muted-foreground',
13
+ 'primary',
14
+ 'primary-foreground',
15
+ 'secondary',
16
+ 'secondary-foreground',
17
+ 'accent',
18
+ 'accent-foreground',
19
+ 'destructive',
20
+ 'destructive-foreground',
21
+ 'ring',
22
+ 'input',
23
+ 'radius',
24
+ 'shadow',
25
+ ] as const
26
+
27
+ export type ThemeToken = (typeof themeTokens)[number]
28
+
29
+ /** Required in both modes. A file missing one is ignored, not partially applied. */
30
+ export const requiredThemeTokens = ['background', 'foreground', 'primary'] as const