@brimveyn/aimux-config 0.10.9 → 0.11.0
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.
- package/package.json +1 -1
- package/src/actions.ts +1 -1
- package/src/app-types.ts +1467 -0
- package/src/index.ts +24 -13
- package/src/keymap-builder.ts +6 -0
- package/src/plugin-actions-runtime.ts +50 -0
- package/src/resolver.ts +22 -0
- package/src/settings-types.ts +160 -0
- package/src/tui/index.ts +11 -1
- package/src/tui/registry.ts +46 -3
- package/src/types.ts +95 -994
package/src/index.ts
CHANGED
|
@@ -5,15 +5,19 @@ export * as actions from './actions'
|
|
|
5
5
|
|
|
6
6
|
// TUI theme system (1:1 port of opencode TUI).
|
|
7
7
|
export {
|
|
8
|
+
BUILTIN_THEME_IDS,
|
|
8
9
|
type ClaudeThemeFile,
|
|
10
|
+
clearRuntimeThemes,
|
|
11
|
+
getTuiTheme,
|
|
9
12
|
isKnownThemeId,
|
|
10
13
|
migrateThemeId,
|
|
14
|
+
registerTuiTheme,
|
|
11
15
|
resolveClaudeTheme,
|
|
12
16
|
type ResolvedTuiTheme,
|
|
13
17
|
resolveTuiTheme,
|
|
14
18
|
type RGBA,
|
|
15
|
-
THEME_IDS,
|
|
16
19
|
type ThemeId,
|
|
20
|
+
themeIds,
|
|
17
21
|
type ThemeMode,
|
|
18
22
|
TUI_COLOR_TOKENS,
|
|
19
23
|
TUI_THEMES,
|
|
@@ -28,6 +32,13 @@ export { GroupBuilder, KeymapBuilder, ModeBindingBuilder } from './keymap-builde
|
|
|
28
32
|
export { getDefaultKeymapConfig } from './defaults'
|
|
29
33
|
export { resolveConfig } from './resolver'
|
|
30
34
|
export { isAutoCommitEnabled, setAutoCommitEnabled } from './auto-commit-runtime'
|
|
35
|
+
export {
|
|
36
|
+
clearPluginActions,
|
|
37
|
+
hasPluginAction,
|
|
38
|
+
pluginAction,
|
|
39
|
+
pluginActionNames,
|
|
40
|
+
registerPluginAction,
|
|
41
|
+
} from './plugin-actions-runtime'
|
|
31
42
|
export { getMultiRepoConfig, setMultiRepoConfig } from './multi-repo-runtime'
|
|
32
43
|
export { getStatusBarSeparator, setStatusBarSeparator } from './status-bar-runtime'
|
|
33
44
|
export {
|
|
@@ -38,6 +49,13 @@ export {
|
|
|
38
49
|
} from './external-editor-runtime'
|
|
39
50
|
export { DEFAULT_MULTI_REPO_CONFIG } from './defaults'
|
|
40
51
|
|
|
52
|
+
// The settings screen's row and section shapes; a plugin registers one.
|
|
53
|
+
export type * from './settings-types'
|
|
54
|
+
|
|
55
|
+
// The application state, mode and layout shapes. Defined in this package —
|
|
56
|
+
// `src/` re-exports them rather than declaring its own copies.
|
|
57
|
+
export type * from './app-types'
|
|
58
|
+
|
|
41
59
|
// User-facing config types (keymap/backends/projects/etc.).
|
|
42
60
|
export type {
|
|
43
61
|
Action,
|
|
@@ -47,45 +65,38 @@ export type {
|
|
|
47
65
|
AIUsageTool,
|
|
48
66
|
AIUsageToolConfig,
|
|
49
67
|
AppAction,
|
|
50
|
-
AppState,
|
|
51
68
|
AutoCommitConfig,
|
|
52
69
|
AutoRenameConfig,
|
|
53
70
|
BackendConfig,
|
|
54
71
|
BindingDef,
|
|
55
|
-
DiscoveredRepo,
|
|
56
72
|
ExternalEditorConfig,
|
|
57
|
-
FocusMode,
|
|
58
|
-
GitFileListMode,
|
|
59
73
|
GitPaneConfig,
|
|
60
|
-
GitPaneDiffCountConfig,
|
|
61
74
|
GitPaneEmbeddedConfig,
|
|
62
75
|
GitPanePaneConfig,
|
|
63
|
-
GitPanePathConfig,
|
|
64
|
-
GitPaneState,
|
|
65
76
|
GroupBuilderApi,
|
|
66
77
|
HooksConfig,
|
|
67
78
|
KeyInput,
|
|
68
79
|
KeymapBuilderApi,
|
|
69
80
|
KeyResult,
|
|
81
|
+
LayoutLeaf,
|
|
82
|
+
LayoutLeafKind,
|
|
70
83
|
LayoutNode,
|
|
71
|
-
|
|
84
|
+
LayoutSplit,
|
|
72
85
|
ModeBindingBuilderApi,
|
|
73
86
|
ModeContext,
|
|
74
87
|
ModeId,
|
|
75
88
|
ModeKeymapDef,
|
|
76
89
|
MultiRepoConfig,
|
|
77
|
-
|
|
78
|
-
|
|
90
|
+
PluginConfigDecl,
|
|
91
|
+
PluginConfigEntry,
|
|
79
92
|
ResolvedConfig,
|
|
80
93
|
ResolvedKeymapConfig,
|
|
81
94
|
SidebarConfig,
|
|
82
95
|
SideEffect,
|
|
83
96
|
SnippetDef,
|
|
84
|
-
SnippetRecord,
|
|
85
97
|
SnippetShellVar,
|
|
86
98
|
SnippetVar,
|
|
87
99
|
SplitDirection,
|
|
88
100
|
StatusBarConfig,
|
|
89
101
|
StatusBarSeparator,
|
|
90
|
-
TabSession,
|
|
91
102
|
} from './types'
|
package/src/keymap-builder.ts
CHANGED
|
@@ -10,6 +10,8 @@ import type {
|
|
|
10
10
|
ResolvedKeymapConfig,
|
|
11
11
|
} from './types'
|
|
12
12
|
|
|
13
|
+
import { pluginAction } from './plugin-actions-runtime'
|
|
14
|
+
|
|
13
15
|
// ---------------------------------------------------------------------------
|
|
14
16
|
// GroupBuilder — sugar for <leader>-prefixed sub-trees
|
|
15
17
|
// ---------------------------------------------------------------------------
|
|
@@ -101,6 +103,10 @@ export class KeymapBuilder implements KeymapBuilderApi {
|
|
|
101
103
|
return this
|
|
102
104
|
}
|
|
103
105
|
|
|
106
|
+
plugin(name: string): Action {
|
|
107
|
+
return pluginAction(name)
|
|
108
|
+
}
|
|
109
|
+
|
|
104
110
|
mode(
|
|
105
111
|
id: ModeId | readonly ModeId[],
|
|
106
112
|
configure: (m: ModeBindingBuilderApi) => ModeBindingBuilderApi
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { ActionFn, KeyResult, ModeContext } from './types'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Named actions a plugin contributes, so a user's keymap can bind one by name
|
|
5
|
+
* before the plugin that answers it has loaded — or at all.
|
|
6
|
+
*
|
|
7
|
+
* A keymap is resolved at startup from `aimux.config.ts`, and plugins load
|
|
8
|
+
* after that. Binding a function directly would mean the config file could
|
|
9
|
+
* only reference plugins it imported, which is exactly the coupling a plugin
|
|
10
|
+
* system is supposed to remove. Binding a *name* defers the lookup to the
|
|
11
|
+
* moment the key is pressed.
|
|
12
|
+
*
|
|
13
|
+
* Lives in this package, alongside the other runtime singletons
|
|
14
|
+
* (`auto-commit-runtime`, `multi-repo-runtime`), because that is where the
|
|
15
|
+
* action factories a config file calls already are.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** Qualified `<pluginId>.<verb>` — the id a keymap writes. */
|
|
19
|
+
const handlers = new Map<string, ActionFn>()
|
|
20
|
+
|
|
21
|
+
export function registerPluginAction(name: string, handler: ActionFn): () => void {
|
|
22
|
+
handlers.set(name, handler)
|
|
23
|
+
return () => {
|
|
24
|
+
if (handlers.get(name) === handler) handlers.delete(name)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Test seam. Never called by the app. */
|
|
29
|
+
export function clearPluginActions(): void {
|
|
30
|
+
handlers.clear()
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function pluginActionNames(): string[] {
|
|
34
|
+
return [...handlers.keys()]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function hasPluginAction(name: string): boolean {
|
|
38
|
+
return handlers.has(name)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The `Action` a keymap binds. Resolved on every keypress rather than at
|
|
43
|
+
* registration: a plugin that is not loaded yet, is disabled, or failed simply
|
|
44
|
+
* yields `null`, which is the same "this key does nothing here" a mode with no
|
|
45
|
+
* binding produces. Anything louder would turn one broken plugin into a broken
|
|
46
|
+
* keyboard.
|
|
47
|
+
*/
|
|
48
|
+
export function pluginAction(name: string): ActionFn {
|
|
49
|
+
return (ctx: ModeContext): KeyResult | null => handlers.get(name)?.(ctx) ?? null
|
|
50
|
+
}
|
package/src/resolver.ts
CHANGED
|
@@ -5,6 +5,8 @@ import type {
|
|
|
5
5
|
ModeId,
|
|
6
6
|
ModeKeymapDef,
|
|
7
7
|
MultiRepoConfig,
|
|
8
|
+
PluginConfigDecl,
|
|
9
|
+
PluginConfigEntry,
|
|
8
10
|
ResolvedConfig,
|
|
9
11
|
ResolvedKeymapConfig,
|
|
10
12
|
} from './types'
|
|
@@ -60,6 +62,7 @@ export function resolveConfig(userConfig: AimuxUserConfig): ResolvedConfig {
|
|
|
60
62
|
integrations: resolveIntegrations(userConfig.integrations),
|
|
61
63
|
keymaps,
|
|
62
64
|
multiRepo,
|
|
65
|
+
plugins: resolvePlugins(userConfig.plugins),
|
|
63
66
|
// ponytail: `sessionBar` is the pre-rename key. Unknown keys parse
|
|
64
67
|
// silently, so without the fallback the setting just vanishes.
|
|
65
68
|
projectBar: resolveProjectBar(userConfig.projectBar ?? userConfig.sessionBar),
|
|
@@ -71,6 +74,25 @@ export function resolveConfig(userConfig: AimuxUserConfig): ResolvedConfig {
|
|
|
71
74
|
}
|
|
72
75
|
}
|
|
73
76
|
|
|
77
|
+
/**
|
|
78
|
+
* Normalises the shorthand. A declaration with neither `path` nor `id` names
|
|
79
|
+
* nothing loadable, so it is dropped here rather than becoming a mystery
|
|
80
|
+
* "plugin not found" at load time.
|
|
81
|
+
*/
|
|
82
|
+
function resolvePlugins(declarations: PluginConfigDecl[] | undefined): PluginConfigEntry[] {
|
|
83
|
+
if (!declarations) return []
|
|
84
|
+
const entries: PluginConfigEntry[] = []
|
|
85
|
+
for (const declaration of declarations) {
|
|
86
|
+
if (typeof declaration === 'string') {
|
|
87
|
+
if (declaration.trim() !== '') entries.push({ path: declaration })
|
|
88
|
+
continue
|
|
89
|
+
}
|
|
90
|
+
if (declaration.path === undefined && declaration.id === undefined) continue
|
|
91
|
+
entries.push(declaration)
|
|
92
|
+
}
|
|
93
|
+
return entries
|
|
94
|
+
}
|
|
95
|
+
|
|
74
96
|
function resolveIntegrations(
|
|
75
97
|
userConfig: AimuxUserConfig['integrations']
|
|
76
98
|
): ResolvedConfig['integrations'] {
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// -----------------------------------------------------------------------------
|
|
2
|
+
// The settings screen's row and section shapes.
|
|
3
|
+
//
|
|
4
|
+
// In this package rather than in `src/` because a plugin registers a settings
|
|
5
|
+
// section, and a plugin has to be able to type one without depending on the
|
|
6
|
+
// aimux binary. `src/settings/types.ts` re-exports from here.
|
|
7
|
+
// -----------------------------------------------------------------------------
|
|
8
|
+
|
|
9
|
+
import type { AppState, ProjectRecord } from './app-types'
|
|
10
|
+
import type { AimuxUserConfig } from './types'
|
|
11
|
+
|
|
12
|
+
export type SettingValue = boolean | number | string
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The values the settings screen owns, keyed by row id. Sparse on purpose: a
|
|
16
|
+
* missing key means "never touched", which is not the same as `false`.
|
|
17
|
+
*/
|
|
18
|
+
export type StoredSettings = Record<string, SettingValue>
|
|
19
|
+
|
|
20
|
+
export interface SettingCtx {
|
|
21
|
+
state: AppState
|
|
22
|
+
values: StoredSettings
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface SettingOption {
|
|
26
|
+
value: SettingValue
|
|
27
|
+
label: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** How a row draws itself, and what activating it does. */
|
|
31
|
+
type SettingKind =
|
|
32
|
+
| { kind: 'toggle' }
|
|
33
|
+
| { kind: 'select'; options: readonly SettingOption[] }
|
|
34
|
+
| { kind: 'number'; min: number; max: number; step: number }
|
|
35
|
+
/** Activating it opens a one-field modal. Empty means "unset", not "empty string". */
|
|
36
|
+
| { kind: 'text'; placeholder?: string }
|
|
37
|
+
|
|
38
|
+
interface SettingRowBase {
|
|
39
|
+
id: string
|
|
40
|
+
label: string
|
|
41
|
+
description?: string
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* A row with no other home for its value: it lives in the `settings` block of
|
|
46
|
+
* `aimux.json`, and `readRow`/`writeRow` reach it by id. Nothing here repeats
|
|
47
|
+
* the id, so a row can't read one key and write another.
|
|
48
|
+
*/
|
|
49
|
+
interface StoredRow {
|
|
50
|
+
storage: 'settings'
|
|
51
|
+
/** Used when neither the config file nor the settings screen has a value. */
|
|
52
|
+
fallback: SettingValue
|
|
53
|
+
/**
|
|
54
|
+
* The value the user's `aimux.config.ts` declares for this row, or undefined
|
|
55
|
+
* when it doesn't declare it. Declared means it wins at every startup — a UI
|
|
56
|
+
* edit then lasts until the next launch, and the row says so.
|
|
57
|
+
*/
|
|
58
|
+
fromConfig?: (config: AimuxUserConfig) => SettingValue | undefined
|
|
59
|
+
/**
|
|
60
|
+
* Hands a new value to whatever owns it in the running app. Not every live row
|
|
61
|
+
* needs one — a value whose only reader subscribes to this store is live on its
|
|
62
|
+
* own, which is why "needs a restart" is declared rather than inferred.
|
|
63
|
+
*/
|
|
64
|
+
apply?: (value: SettingValue) => void
|
|
65
|
+
/** The running app won't see the new value; the row says so. */
|
|
66
|
+
restart?: true
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* A view over a value `AppState` already owns (git pane preferences, bar
|
|
71
|
+
* visibility, …). It delegates to that value's existing action or side effect
|
|
72
|
+
* rather than keeping a second copy.
|
|
73
|
+
*/
|
|
74
|
+
interface DerivedRow {
|
|
75
|
+
storage: 'app'
|
|
76
|
+
read: (ctx: SettingCtx) => SettingValue
|
|
77
|
+
/** Same context `read` gets, so a write that has to merge into a record can. */
|
|
78
|
+
write: (value: SettingValue, ctx: SettingCtx) => void
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* A row over one key of a plugin's configuration.
|
|
83
|
+
*
|
|
84
|
+
* Not a `settings` row: that block of `aimux.json` is never read by plugin
|
|
85
|
+
* discovery, so a value written there would silently reach no plugin. Not an
|
|
86
|
+
* `app` row either — its value has neither a home in `AppState` nor a place in
|
|
87
|
+
* the settings screen's hydration, and the two marks a plugin row most needs
|
|
88
|
+
* (`~` the registry has an override, `*` `aimux.config.ts` declares it and
|
|
89
|
+
* keeps winning) come from the plugin's own layers rather than from this
|
|
90
|
+
* screen's bookkeeping.
|
|
91
|
+
*
|
|
92
|
+
* The three functions are closures, like `DerivedRow`'s: the settings store
|
|
93
|
+
* must not learn how to reach a plugin registry.
|
|
94
|
+
*/
|
|
95
|
+
interface PluginConfigRow {
|
|
96
|
+
storage: 'plugin'
|
|
97
|
+
/** For the detail view, and for a test to name the row it means. */
|
|
98
|
+
pluginId: string
|
|
99
|
+
field: string
|
|
100
|
+
/** Never rendered and edited from empty. */
|
|
101
|
+
secret?: boolean
|
|
102
|
+
/** `aimux.config.ts` declares this key and keeps winning. Drives `*`. */
|
|
103
|
+
fromConfigFile: boolean
|
|
104
|
+
/** A layer above the manifest default set it. Drives `~`. */
|
|
105
|
+
isSet: boolean
|
|
106
|
+
read: () => SettingValue
|
|
107
|
+
write: (value: SettingValue) => void
|
|
108
|
+
/** Drop the override and fall back through the layers underneath. */
|
|
109
|
+
reset: () => void
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** A row whose value lives in the `settings` block of `aimux.json`. */
|
|
113
|
+
export type StoredSettingRow = SettingRowBase & SettingKind & StoredRow
|
|
114
|
+
|
|
115
|
+
/** A row over one key of a plugin's config. Narrowed by `storage === 'plugin'`. */
|
|
116
|
+
export type PluginSettingRow = SettingRowBase & SettingKind & PluginConfigRow
|
|
117
|
+
|
|
118
|
+
export type SettingRow =
|
|
119
|
+
| StoredSettingRow
|
|
120
|
+
| PluginSettingRow
|
|
121
|
+
| (SettingRowBase & SettingKind & DerivedRow)
|
|
122
|
+
| (SettingRowBase & { kind: 'info'; value: (ctx: SettingCtx) => string })
|
|
123
|
+
/**
|
|
124
|
+
* Not a setting: a button. For the things a row cannot hold — a multi-line
|
|
125
|
+
* script, a list — where the honest move is to hand over to whatever can.
|
|
126
|
+
*/
|
|
127
|
+
| (SettingRowBase & { kind: 'action'; value: (ctx: SettingCtx) => string; run: () => void })
|
|
128
|
+
|
|
129
|
+
export interface SettingSection {
|
|
130
|
+
id: string
|
|
131
|
+
/**
|
|
132
|
+
* One cell, text presentation, present in the base fonts — the same rule the
|
|
133
|
+
* stats screen's section glyphs follow, so the eye finds a section by shape
|
|
134
|
+
* before it reads the label.
|
|
135
|
+
*
|
|
136
|
+
* Required, and on the section rather than in a lookup keyed by id: a map
|
|
137
|
+
* would need a fallback, and a fallback turns a renamed or added section into
|
|
138
|
+
* a silent placeholder instead of a compile error.
|
|
139
|
+
*/
|
|
140
|
+
glyph: string
|
|
141
|
+
label: string
|
|
142
|
+
/** Shown once under the section's title, for a caveat that covers every row. */
|
|
143
|
+
description?: string
|
|
144
|
+
/**
|
|
145
|
+
* A function when the rows depend on the projects — one row per project, say.
|
|
146
|
+
* The projects and nothing else: a builder handed the whole state would be free
|
|
147
|
+
* to depend on anything in it, and every caller would have to have all of it.
|
|
148
|
+
*
|
|
149
|
+
* Building a row may cost something (Setup reads a script off disk), so anything
|
|
150
|
+
* that only needs to know *how many* rows there are asks `rowCount` instead.
|
|
151
|
+
*/
|
|
152
|
+
rows: SettingRow[] | ((projects: readonly ProjectRecord[]) => SettingRow[])
|
|
153
|
+
/**
|
|
154
|
+
* How many rows there will be, without building them. Required alongside a
|
|
155
|
+
* dynamic `rows`, and kept honest by `settings-schema.test.ts`: the reducer
|
|
156
|
+
* clamps the cursor with this, and a count that disagrees with the list is a
|
|
157
|
+
* cursor that stops one row short of the end, or one past it.
|
|
158
|
+
*/
|
|
159
|
+
rowCount?: (projects: readonly ProjectRecord[]) => number
|
|
160
|
+
}
|
package/src/tui/index.ts
CHANGED
|
@@ -1,4 +1,14 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export {
|
|
2
|
+
BUILTIN_THEME_IDS,
|
|
3
|
+
clearRuntimeThemes,
|
|
4
|
+
getTuiTheme,
|
|
5
|
+
isKnownThemeId,
|
|
6
|
+
migrateThemeId,
|
|
7
|
+
registerTuiTheme,
|
|
8
|
+
type ThemeId,
|
|
9
|
+
themeIds,
|
|
10
|
+
TUI_THEMES,
|
|
11
|
+
} from './registry'
|
|
2
12
|
export { type ClaudeThemeFile, resolveClaudeTheme, resolveTuiTheme } from './resolve'
|
|
3
13
|
export { type TuiShikiOptions, tuiThemeToShiki } from './shiki'
|
|
4
14
|
export { TUI_COLOR_TOKENS, type TuiColorToken } from './tokens'
|
package/src/tui/registry.ts
CHANGED
|
@@ -76,12 +76,55 @@ export const TUI_THEMES: Record<string, TuiThemeJson> = {
|
|
|
76
76
|
'zenburn': zenburn as TuiThemeJson,
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
|
|
79
|
+
/**
|
|
80
|
+
* A shipped theme id. Widened with `(string & {})` because a theme can also be
|
|
81
|
+
* registered at runtime — from `<profile>/themes/` or by a plugin — and those
|
|
82
|
+
* ids are not knowable here. The literal half survives, so autocomplete still
|
|
83
|
+
* lists the shipped ones.
|
|
84
|
+
*/
|
|
85
|
+
export type ThemeId = keyof typeof TUI_THEMES | (string & {})
|
|
80
86
|
|
|
81
|
-
|
|
87
|
+
/** Ids aimux ships, sorted. Runtime-registered ones are not in here. */
|
|
88
|
+
export const BUILTIN_THEME_IDS: string[] = Object.keys(TUI_THEMES).sort()
|
|
89
|
+
|
|
90
|
+
const runtimeThemes = new Map<string, TuiThemeJson>()
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Registers a theme loaded at runtime — a JSON file in `<profile>/themes/`, or
|
|
94
|
+
* one a plugin ships. Returns the disposer; unregistering a theme that is
|
|
95
|
+
* currently applied leaves the applied colours alone, since they were already
|
|
96
|
+
* resolved into the theme store.
|
|
97
|
+
*
|
|
98
|
+
* A runtime id may not shadow a shipped one: silently replacing `dracula`
|
|
99
|
+
* would make "which dracula?" depend on load order.
|
|
100
|
+
*/
|
|
101
|
+
export function registerTuiTheme(id: string, theme: TuiThemeJson): () => void {
|
|
102
|
+
if (id in TUI_THEMES) {
|
|
103
|
+
throw new Error(`theme "${id}" is shipped with aimux and cannot be replaced`)
|
|
104
|
+
}
|
|
105
|
+
runtimeThemes.set(id, theme)
|
|
106
|
+
return () => {
|
|
107
|
+
runtimeThemes.delete(id)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Test seam. Never called by the app. */
|
|
112
|
+
export function clearRuntimeThemes(): void {
|
|
113
|
+
runtimeThemes.clear()
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Every id the picker offers: shipped plus runtime-registered, sorted. */
|
|
117
|
+
export function themeIds(): string[] {
|
|
118
|
+
if (runtimeThemes.size === 0) return BUILTIN_THEME_IDS
|
|
119
|
+
return [...BUILTIN_THEME_IDS, ...runtimeThemes.keys()].sort()
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function getTuiTheme(id: string): TuiThemeJson | undefined {
|
|
123
|
+
return TUI_THEMES[id] ?? runtimeThemes.get(id)
|
|
124
|
+
}
|
|
82
125
|
|
|
83
126
|
export function isKnownThemeId(id: string): id is ThemeId {
|
|
84
|
-
return id in TUI_THEMES
|
|
127
|
+
return id in TUI_THEMES || runtimeThemes.has(id)
|
|
85
128
|
}
|
|
86
129
|
|
|
87
130
|
const LEGACY_ID_MAP: Record<string, ThemeId> = {
|