@flyingrobots/bijou-tui 4.4.0 → 4.4.1

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/dist/app-frame.js CHANGED
@@ -4,14 +4,14 @@
4
4
  * Provides tabs, pane focus/scroll isolation, shell key handling, help,
5
5
  * panel-scoped overlay context, and optional frame-level command palette.
6
6
  */
7
- import { createSurface, preparePreferenceSections, preferenceListSurface, resolvePreferenceRowLayout, resolveClock, resolveSafeCtx, } from '@flyingrobots/bijou';
7
+ import { cloneContextWithResolvedTheme, createResolved, createSurface, setDefaultContext, preparePreferenceSections, preferenceListSurface, resolvePreferenceRowLayout, resolveClock, resolveSafeCtx, } from '@flyingrobots/bijou';
8
8
  import { helpViewSurface } from './help.js';
9
9
  import { createKeyMap } from './keybindings.js';
10
10
  import { isKeyMsg, isMouseMsg, isResizeMsg } from './types.js';
11
11
  import { quit } from './commands.js';
12
12
  import { compositeSurfaceInto, drawer, modal } from './overlay.js';
13
13
  import { isShellQuitConfirmAccept, isShellQuitConfirmDismiss, isShellQuitRequest, renderShellQuitOverlay, shouldUseShellQuitConfirm, } from './shell-quit.js';
14
- import { commandPalette, commandPaletteKeyMap, } from './command-palette.js';
14
+ import { commandPaletteSurface, commandPaletteKeyMap, } from './command-palette.js';
15
15
  import { createPagerStateForSurface, pagerSurface, } from './pager.js';
16
16
  import { restoreLayoutState } from './layout-preset.js';
17
17
  import { countNotificationHistory, createNotificationState, dismissNotification, hitTestNotificationStack, notificationsNeedTick, pushNotification, renderNotificationHistorySurface, renderNotificationReviewEntrySurface, renderNotificationStack, tickNotifications, trimNotificationsToViewport, } from './notification.js';
@@ -134,6 +134,81 @@ function createFrameNotificationTickCmd() {
134
134
  });
135
135
  };
136
136
  }
137
+ function cloneShellThemeContext(ctx, resolvedTheme) {
138
+ return cloneContextWithResolvedTheme(ctx, resolvedTheme);
139
+ }
140
+ function resolveShellThemeOptionsText(shellThemes, i18n) {
141
+ const labels = shellThemes.map((theme) => theme.label);
142
+ if (labels.length === 0)
143
+ return '';
144
+ if (i18n == null)
145
+ return labels.join(', ');
146
+ return i18n.formatList(labels, i18n.locale);
147
+ }
148
+ function resolveCurrentShellTheme(shellThemes, activeShellThemeId) {
149
+ return shellThemes.find((theme) => theme.id === activeShellThemeId) ?? shellThemes[0];
150
+ }
151
+ function resolveNextShellTheme(shellThemes, activeShellThemeId) {
152
+ if (shellThemes.length === 0)
153
+ return undefined;
154
+ const currentIndex = Math.max(0, shellThemes.findIndex((theme) => theme.id === activeShellThemeId));
155
+ return shellThemes[(currentIndex + 1) % shellThemes.length];
156
+ }
157
+ function resolveShellThemeForContext(shellThemes, ctx) {
158
+ if (ctx == null)
159
+ return undefined;
160
+ return shellThemes.find((theme) => theme.resolvedTheme.theme === ctx.theme.theme);
161
+ }
162
+ function mergeShellThemeSettings(settings, shellThemes, activeShellThemeId, i18n) {
163
+ if (shellThemes.length < 2)
164
+ return settings;
165
+ const currentTheme = resolveCurrentShellTheme(shellThemes, activeShellThemeId);
166
+ const nextTheme = resolveNextShellTheme(shellThemes, activeShellThemeId);
167
+ if (currentTheme == null || nextTheme == null)
168
+ return settings;
169
+ const row = {
170
+ id: FRAME_SHELL_THEME_ROW_ID,
171
+ label: frameMessage(i18n, 'settings.shellTheme.label', 'Shell theme'),
172
+ description: currentTheme.description ?? frameMessage(i18n, 'settings.shellTheme.description', 'Current theme: {theme}. Options: {options}.', {
173
+ theme: currentTheme.label,
174
+ options: resolveShellThemeOptionsText(shellThemes, i18n),
175
+ }),
176
+ valueLabel: currentTheme.label,
177
+ kind: 'choice',
178
+ feedback: {
179
+ title: frameMessage(i18n, 'settings.title', 'Settings'),
180
+ message: frameMessage(i18n, 'settings.shellTheme.feedback', 'Shell theme set to {theme}.', { theme: nextTheme.label }),
181
+ },
182
+ };
183
+ const shellSectionTitle = frameMessage(i18n, 'settings.section.shell', 'Shell');
184
+ if (settings == null) {
185
+ return {
186
+ title: frameMessage(i18n, 'settings.title', 'Settings'),
187
+ sections: [{ id: 'shell', title: shellSectionTitle, rows: [row] }],
188
+ };
189
+ }
190
+ const shellSectionIndex = settings.sections.findIndex((section) => section.id === 'shell');
191
+ if (shellSectionIndex >= 0) {
192
+ const shellSection = settings.sections[shellSectionIndex];
193
+ const existingRowIndex = shellSection.rows.findIndex((existingRow) => existingRow.id === FRAME_SHELL_THEME_ROW_ID);
194
+ const nextRows = existingRowIndex >= 0
195
+ ? shellSection.rows.map((existingRow, index) => (index === existingRowIndex ? row : existingRow))
196
+ : [...shellSection.rows, row];
197
+ return {
198
+ ...settings,
199
+ sections: settings.sections.map((section, index) => (index === shellSectionIndex
200
+ ? { ...shellSection, rows: nextRows }
201
+ : section)),
202
+ };
203
+ }
204
+ return {
205
+ ...settings,
206
+ sections: [
207
+ { id: 'shell', title: shellSectionTitle, rows: [row] },
208
+ ...settings.sections,
209
+ ],
210
+ };
211
+ }
137
212
  // Factory
138
213
  // ---------------------------------------------------------------------------
139
214
  /**
@@ -155,8 +230,57 @@ export function createFramedApp(options) {
155
230
  if (!pagesById.has(defaultPageId)) {
156
231
  throw new Error(`createFramedApp: defaultPageId "${defaultPageId}" not found in pages`);
157
232
  }
233
+ const defaultFrameCtx = options.ctx ?? resolveSafeCtx();
234
+ if (options.shellThemes != null && options.shellThemes.length > 0 && defaultFrameCtx == null) {
235
+ throw new Error('createFramedApp: shellThemes requires options.ctx or a default Bijou context');
236
+ }
237
+ const resolvedShellThemes = options.shellThemes?.map((theme) => ({
238
+ id: theme.id,
239
+ label: theme.label,
240
+ description: theme.description,
241
+ shellTheme: theme,
242
+ resolvedTheme: createResolved(theme.theme, defaultFrameCtx.theme.noColor, defaultFrameCtx.theme.colorScheme),
243
+ })) ?? [];
244
+ const enableShellThemeSettings = resolvedShellThemes.length > 1;
245
+ const initialShellTheme = resolveShellThemeForContext(resolvedShellThemes, defaultFrameCtx)
246
+ ?? resolvedShellThemes[0];
247
+ const usesAmbientDefaultContext = options.ctx == null && defaultFrameCtx != null;
248
+ let frameCtx = options.ctx;
249
+ let frameCtxShellThemeId = resolveShellThemeForContext(resolvedShellThemes, frameCtx)?.id;
250
+ function resolveFrameCtx() {
251
+ return frameCtx ?? options.ctx ?? resolveSafeCtx();
252
+ }
253
+ function resolveFrameThemeCtx(activeShellThemeId) {
254
+ const baseCtx = resolveFrameCtx();
255
+ if (defaultFrameCtx == null)
256
+ return baseCtx;
257
+ const activeTheme = resolveCurrentShellTheme(resolvedShellThemes, activeShellThemeId);
258
+ if (activeTheme == null)
259
+ return baseCtx;
260
+ if (frameCtx != null && frameCtxShellThemeId === activeTheme.id) {
261
+ return frameCtx;
262
+ }
263
+ if (resolveShellThemeForContext(resolvedShellThemes, baseCtx)?.id === activeTheme.id) {
264
+ return baseCtx;
265
+ }
266
+ return cloneShellThemeContext(defaultFrameCtx, activeTheme.resolvedTheme);
267
+ }
268
+ function publishShellThemeContext(nextTheme) {
269
+ if (defaultFrameCtx == null)
270
+ return resolveFrameCtx();
271
+ frameCtx = cloneShellThemeContext(defaultFrameCtx, nextTheme.resolvedTheme);
272
+ frameCtxShellThemeId = nextTheme.id;
273
+ if (usesAmbientDefaultContext) {
274
+ setDefaultContext(frameCtx);
275
+ }
276
+ options.onShellThemeChange?.({
277
+ shellTheme: nextTheme.shellTheme,
278
+ ctx: frameCtx,
279
+ });
280
+ return frameCtx;
281
+ }
158
282
  const frameKeys = createFrameKeyMap({
159
- enableSettings: options.settings != null,
283
+ enableSettings: options.settings != null || enableShellThemeSettings,
160
284
  enableNotifications: options.notificationCenter != null || options.runtimeNotifications !== false,
161
285
  i18n: options.i18n,
162
286
  });
@@ -221,30 +345,35 @@ export function createFramedApp(options) {
221
345
  // --- settings ---
222
346
  'settings-focus-move': (model, cmd) => {
223
347
  const c = cmd;
224
- const layout = resolveSettingsLayout(model, options, pagesById);
348
+ const layout = resolveSettingsLayout(model, options, pagesById, resolvedShellThemes);
225
349
  return layout != null ? moveSettingsFocus(model, layout, c.delta) : model;
226
350
  },
227
351
  'settings-scroll': (model, cmd) => {
228
352
  const c = cmd;
229
- const layout = resolveSettingsLayout(model, options, pagesById);
353
+ const layout = resolveSettingsLayout(model, options, pagesById, resolvedShellThemes);
230
354
  return layout != null ? scrollSettingsBy(model, layout, c.delta) : model;
231
355
  },
232
356
  'settings-scroll-to': (model, cmd) => {
233
357
  const c = cmd;
234
- const layout = resolveSettingsLayout(model, options, pagesById);
358
+ const layout = resolveSettingsLayout(model, options, pagesById, resolvedShellThemes);
235
359
  if (layout == null)
236
360
  return model;
237
361
  return { ...model, settingsScrollY: c.position === 'top' ? 0 : layout.maxScrollY };
238
362
  },
239
363
  'activate-settings-row': (model, cmd, teaCmds) => {
240
364
  const c = cmd;
241
- const layout = resolveSettingsLayout(model, options, pagesById);
365
+ const layout = resolveSettingsLayout(model, options, pagesById, resolvedShellThemes);
242
366
  if (layout == null)
243
367
  return model;
244
368
  const hitRow = layout.rows.find((r) => r.index === c.rowIndex);
245
369
  if (hitRow == null)
246
370
  return model;
247
371
  const focusedModel = { ...model, settingsFocusIndex: hitRow.index };
372
+ if (hitRow.behavior === 'cycle-shell-theme') {
373
+ const [nextModel, cmds] = cycleShellThemeSetting(focusedModel, hitRow.row);
374
+ teaCmds.push(...cmds);
375
+ return nextModel;
376
+ }
248
377
  if (hitRow.row.action === undefined || hitRow.row.enabled === false || hitRow.row.kind === 'info') {
249
378
  return focusedModel;
250
379
  }
@@ -255,18 +384,18 @@ export function createFramedApp(options) {
255
384
  // --- notification center ---
256
385
  'notification-center-scroll': (model, cmd) => {
257
386
  const c = cmd;
258
- const layout = resolveNotificationCenterLayout(model, options, pagesById);
387
+ const layout = resolveNotificationCenterLayout(model, options, pagesById, resolveFrameThemeCtx(model.activeShellThemeId));
259
388
  return layout != null ? scrollNotificationCenterBy(model, layout, c.delta) : model;
260
389
  },
261
390
  'notification-center-scroll-to': (model, cmd) => {
262
391
  const c = cmd;
263
- const layout = resolveNotificationCenterLayout(model, options, pagesById);
392
+ const layout = resolveNotificationCenterLayout(model, options, pagesById, resolveFrameThemeCtx(model.activeShellThemeId));
264
393
  if (layout == null)
265
394
  return model;
266
395
  return { ...model, notificationCenterScrollY: c.position === 'top' ? 0 : layout.maxScrollY };
267
396
  },
268
397
  'cycle-notification-filter': (model, _cmd, teaCmds) => {
269
- const layout = resolveNotificationCenterLayout(model, options, pagesById);
398
+ const layout = resolveNotificationCenterLayout(model, options, pagesById, resolveFrameThemeCtx(model.activeShellThemeId));
270
399
  if (layout == null)
271
400
  return model;
272
401
  const [nextModel, cmds] = cycleNotificationCenterFilter(model, layout);
@@ -277,7 +406,7 @@ export function createFramedApp(options) {
277
406
  'help-scroll': (model, cmd) => {
278
407
  const c = cmd;
279
408
  const activePage = pagesById.get(model.activePageId);
280
- const overlay = renderHelpOverlay(model, activePage, frameKeys, paletteKeys, options, pagesById);
409
+ const overlay = renderHelpOverlay(model, activePage, frameKeys, paletteKeys, options, pagesById, resolvedShellThemes);
281
410
  const viewportHeight = Math.max(1, overlay.body.height - 1);
282
411
  const delta = c.action === 'down' ? 3
283
412
  : c.action === 'up' ? -3
@@ -337,7 +466,7 @@ export function createFramedApp(options) {
337
466
  const c = cmd;
338
467
  if (!frameNotificationOptions.enabled)
339
468
  return model;
340
- const nowMs = resolveClock(resolveSafeCtx()).now();
469
+ const nowMs = resolveClock(resolveFrameCtx()).now();
341
470
  const [nextModel, cmds] = applyFrameNotificationState(model, dismissNotification(model.runtimeNotifications, c.notificationId, nowMs), nowMs);
342
471
  teaCmds.push(...cmds);
343
472
  return nextModel;
@@ -436,7 +565,7 @@ export function createFramedApp(options) {
436
565
  return [obs];
437
566
  }
438
567
  function handleSettingsLayerKeyCommands(msg, model) {
439
- const layout = resolveSettingsLayout(model, options, pagesById);
568
+ const layout = resolveSettingsLayout(model, options, pagesById, resolvedShellThemes);
440
569
  if (layout == null)
441
570
  return undefined;
442
571
  const obs = { type: 'observed-key', msg, route: 'frame' };
@@ -489,7 +618,10 @@ export function createFramedApp(options) {
489
618
  if (!msg.ctrl && !msg.alt && (msg.key === 'enter' || msg.key === 'space')) {
490
619
  const rowIndex = clampSettingsFocus(model, layout);
491
620
  const row = layout.rows[rowIndex];
492
- if (row?.row.action !== undefined && row.row.enabled !== false && row.row.kind !== 'info') {
621
+ if (row != null
622
+ && row.row.enabled !== false
623
+ && row.row.kind !== 'info'
624
+ && (row.behavior === 'cycle-shell-theme' || row.row.action !== undefined)) {
493
625
  return [obs, { type: 'activate-settings-row', rowIndex: row.index }];
494
626
  }
495
627
  return [obs];
@@ -497,7 +629,7 @@ export function createFramedApp(options) {
497
629
  return [obs];
498
630
  }
499
631
  function handleNotificationCenterLayerKeyCommands(msg, model) {
500
- const layout = resolveNotificationCenterLayout(model, options, pagesById);
632
+ const layout = resolveNotificationCenterLayout(model, options, pagesById, resolveFrameThemeCtx(model.activeShellThemeId));
501
633
  if (layout == null)
502
634
  return undefined;
503
635
  const obs = { type: 'observed-key', msg, route: 'frame' };
@@ -651,13 +783,14 @@ export function createFramedApp(options) {
651
783
  const bodyRect = resolveBodyRect(model, options);
652
784
  const maxState = model.maximizedPaneByPage[model.activePageId];
653
785
  const maximizedPaneId = maxState?.maximizedPaneId;
786
+ const themedFrameCtx = resolveFrameThemeCtx(model.activeShellThemeId);
654
787
  const renderResult = maximizedPaneId
655
- ? renderMaximizedPane(model.activePageId, model, bodyRect, pagesById, maximizedPaneId, paneScratchPool)
656
- : renderPageContent(model.activePageId, model, bodyRect, pagesById);
788
+ ? renderMaximizedPane(model.activePageId, model, bodyRect, pagesById, maximizedPaneId, paneScratchPool, themedFrameCtx)
789
+ : renderPageContent(model.activePageId, model, bodyRect, pagesById, themedFrameCtx);
657
790
  return renderResult.paneRects;
658
791
  }
659
792
  function buildWorkspaceLayoutTree(model) {
660
- const header = resolveHeaderLine(model, options, pagesById, headerScratch);
793
+ const header = resolveHeaderLine(model, options, pagesById, headerScratch, resolveFrameThemeCtx(model.activeShellThemeId));
661
794
  headerScratch = header.surface;
662
795
  const tabChildren = header.tabTargets.map((target) => createShellRetainedLayoutNode(`tab:${target.pageId}`, {
663
796
  row: 0,
@@ -698,7 +831,9 @@ export function createFramedApp(options) {
698
831
  }
699
832
  function resolveFrameMouseRuntimeLayouts(model) {
700
833
  let layouts = EMPTY_RUNTIME_LAYOUTS;
701
- const settingsLayout = model.settingsOpen ? resolveSettingsLayout(model, options, pagesById) : undefined;
834
+ const settingsLayout = model.settingsOpen
835
+ ? resolveSettingsLayout(model, options, pagesById, resolvedShellThemes)
836
+ : undefined;
702
837
  if (settingsLayout != null) {
703
838
  layouts = retainRuntimeLayout(layouts, {
704
839
  viewId: 'settings',
@@ -710,7 +845,9 @@ export function createFramedApp(options) {
710
845
  }, buildSettingsRowChildren(model, settingsLayout)),
711
846
  });
712
847
  }
713
- const notificationCenterLayout = model.notificationCenterOpen ? resolveNotificationCenterLayout(model, options, pagesById) : undefined;
848
+ const notificationCenterLayout = model.notificationCenterOpen
849
+ ? resolveNotificationCenterLayout(model, options, pagesById, resolveFrameThemeCtx(model.activeShellThemeId))
850
+ : undefined;
714
851
  if (notificationCenterLayout != null) {
715
852
  layouts = retainRuntimeLayout(layouts, {
716
853
  viewId: 'notification-center',
@@ -790,7 +927,7 @@ export function createFramedApp(options) {
790
927
  screenHeight: model.rows,
791
928
  margin: frameNotificationOptions.margin,
792
929
  gap: frameNotificationOptions.gap,
793
- ctx: resolveSafeCtx() ?? undefined,
930
+ ctx: resolveFrameThemeCtx(model.activeShellThemeId) ?? undefined,
794
931
  }, msg.col, msg.row);
795
932
  if (notificationTarget?.kind === 'dismiss') {
796
933
  cmds.push({ type: 'dismiss-notification', notificationId: notificationTarget.item.id });
@@ -872,7 +1009,7 @@ export function createFramedApp(options) {
872
1009
  return helpLineOverride ?? mergeBindingSources(frameKeys, options.globalKeys, activeInputArea?.helpSource ?? activeInputArea?.keyMap, activePage.helpSource ?? activePage.keyMap);
873
1010
  }
874
1011
  function resolveLayerMetadata(model, activePage, activeInputArea, modalKeyMap) {
875
- const settings = resolveFrameSettings(model, options, pagesById);
1012
+ const settings = resolveFrameSettings(model, options, pagesById, resolvedShellThemes);
876
1013
  const notificationCenter = resolveFrameNotificationCenter(model, options, pagesById);
877
1014
  const workspaceHintSource = resolveWorkspaceHintSource(model, activePage, activeInputArea);
878
1015
  const workspaceHelpSource = resolveWorkspaceHelpSource(activePage, activeInputArea);
@@ -982,19 +1119,15 @@ export function createFramedApp(options) {
982
1119
  }
983
1120
  return [nextModel, []];
984
1121
  }
985
- function activateSettingsRow(model, row) {
986
- if (row.action === undefined || row.enabled === false || row.kind === 'info') {
987
- return [model, []];
988
- }
989
- const cmds = [emitMsgForPage(model.activePageId, row.action)];
1122
+ function pushSettingsFeedback(model, row) {
990
1123
  if (!frameNotificationOptions.enabled) {
991
- return [model, cmds];
1124
+ return [model, []];
992
1125
  }
993
1126
  const feedback = row.feedback ?? {
994
1127
  title: 'Setting updated',
995
1128
  message: `${row.label} updated.`,
996
1129
  };
997
- const nowMs = resolveClock(resolveSafeCtx()).now();
1130
+ const nowMs = resolveClock(resolveFrameCtx()).now();
998
1131
  const notifications = pushNotification(model.runtimeNotifications, {
999
1132
  title: feedback.title ?? 'Setting updated',
1000
1133
  message: feedback.message,
@@ -1005,9 +1138,28 @@ export function createFramedApp(options) {
1005
1138
  durationMs: feedback.durationMs ?? 2_500,
1006
1139
  overflow: frameNotificationOptions.overflow,
1007
1140
  }, nowMs);
1008
- const [nextModel, notificationCmds] = applyFrameNotificationState(model, notifications, nowMs);
1141
+ return applyFrameNotificationState(model, notifications, nowMs);
1142
+ }
1143
+ function activateSettingsRow(model, row) {
1144
+ if (row.action === undefined || row.enabled === false || row.kind === 'info') {
1145
+ return [model, []];
1146
+ }
1147
+ const cmds = [emitMsgForPage(model.activePageId, row.action)];
1148
+ const [nextModel, notificationCmds] = pushSettingsFeedback(model, row);
1009
1149
  return [nextModel, [...cmds, ...notificationCmds]];
1010
1150
  }
1151
+ function cycleShellThemeSetting(model, row) {
1152
+ const nextTheme = resolveNextShellTheme(resolvedShellThemes, model.activeShellThemeId);
1153
+ if (nextTheme == null) {
1154
+ return [model, []];
1155
+ }
1156
+ publishShellThemeContext(nextTheme);
1157
+ const [nextModel, notificationCmds] = pushSettingsFeedback({
1158
+ ...model,
1159
+ activeShellThemeId: nextTheme.id,
1160
+ }, row);
1161
+ return [nextModel, notificationCmds];
1162
+ }
1011
1163
  const app = {
1012
1164
  init() {
1013
1165
  const pageModels = {};
@@ -1044,7 +1196,11 @@ export function createFramedApp(options) {
1044
1196
  runtimeNotifications: createNotificationState(),
1045
1197
  runtimeNotificationHistoryFilter: 'ALL',
1046
1198
  runtimeNotificationLoopActive: false,
1199
+ activeShellThemeId: initialShellTheme?.id,
1047
1200
  };
1201
+ if (initialShellTheme != null) {
1202
+ publishShellThemeContext(initialShellTheme);
1203
+ }
1048
1204
  for (const pageId of pageOrder) {
1049
1205
  model = syncPageFrameState(model, pageId, pagesById);
1050
1206
  }
@@ -1159,11 +1315,12 @@ export function createFramedApp(options) {
1159
1315
  return updateTargetPage(model, model.activePageId, msg);
1160
1316
  },
1161
1317
  view(model) {
1318
+ const themedFrameCtx = resolveFrameThemeCtx(model.activeShellThemeId);
1162
1319
  const { activePage, layerStack, activeLayer, } = resolvePresentedLayerContext(model);
1163
- const headerResult = resolveHeaderLine(model, options, pagesById, headerScratch);
1320
+ const headerResult = resolveHeaderLine(model, options, pagesById, headerScratch, themedFrameCtx);
1164
1321
  headerScratch = headerResult.surface;
1165
1322
  const header = headerResult.surface;
1166
- helpLineScratch = renderHelpLine(model, activeLayer, options.i18n, resolveNotificationFooterCue(model, options, pagesById), helpLineScratch);
1323
+ helpLineScratch = renderHelpLine(model, activeLayer, options.i18n, resolveNotificationFooterCue(model, options, pagesById), helpLineScratch, themedFrameCtx);
1167
1324
  const helpLine = helpLineScratch;
1168
1325
  const bodyRect = resolveBodyRect(model, options);
1169
1326
  // Check for maximized pane — if set, render only that pane at full body rect
@@ -1182,20 +1339,20 @@ export function createFramedApp(options) {
1182
1339
  const activeTransition = model.activeTransition ?? options.transition;
1183
1340
  if (model.previousPageId != null && model.transitionProgress < 1 && activeTransition && activeTransition !== 'none') {
1184
1341
  const activeBodyResult = maximizedPaneId
1185
- ? renderMaximizedPane(model.activePageId, model, bodyRect, pagesById, maximizedPaneId, paneScratchPool)
1186
- : renderPageContent(model.activePageId, model, bodyRect, pagesById);
1342
+ ? renderMaximizedPane(model.activePageId, model, bodyRect, pagesById, maximizedPaneId, paneScratchPool, themedFrameCtx)
1343
+ : renderPageContent(model.activePageId, model, bodyRect, pagesById, themedFrameCtx);
1187
1344
  activeResult = activeBodyResult;
1188
1345
  bodySurface = activeBodyResult.surface;
1189
- const ctx = resolveSafeCtx();
1346
+ const ctx = themedFrameCtx;
1190
1347
  if (ctx) {
1191
- const prevResult = renderPageContent(model.previousPageId, model, bodyRect, pagesById);
1348
+ const prevResult = renderPageContent(model.previousPageId, model, bodyRect, pagesById, themedFrameCtx);
1192
1349
  bodySurface = renderTransition(prevResult.surface, activeBodyResult.surface, activeTransition, model.transitionProgress, bodyRect.width, bodyRect.height, ctx, model.transitionFrame);
1193
1350
  }
1194
1351
  }
1195
1352
  else {
1196
1353
  activeResult = maximizedPaneId
1197
- ? renderMaximizedPaneInto(model.activePageId, model, bodyRect, pagesById, maximizedPaneId, frameSurface, bodyRect.row, bodyRect.col, paneScratchPool)
1198
- : renderPageContentInto(model.activePageId, model, bodyRect, pagesById, frameSurface, bodyRect.row, bodyRect.col, paneScratchPool);
1354
+ ? renderMaximizedPaneInto(model.activePageId, model, bodyRect, pagesById, maximizedPaneId, frameSurface, bodyRect.row, bodyRect.col, paneScratchPool, themedFrameCtx)
1355
+ : renderPageContentInto(model.activePageId, model, bodyRect, pagesById, frameSurface, bodyRect.row, bodyRect.col, paneScratchPool, themedFrameCtx);
1199
1356
  }
1200
1357
  const overlays = [];
1201
1358
  if (options.overlayFactory != null) {
@@ -1207,7 +1364,7 @@ export function createFramedApp(options) {
1207
1364
  }));
1208
1365
  }
1209
1366
  if (frameNotificationOptions.enabled) {
1210
- const ctx = resolveSafeCtx();
1367
+ const ctx = themedFrameCtx;
1211
1368
  overlays.push(...renderNotificationStack(model.runtimeNotifications, {
1212
1369
  screenWidth: model.columns,
1213
1370
  screenHeight: model.rows,
@@ -1218,20 +1375,20 @@ export function createFramedApp(options) {
1218
1375
  }
1219
1376
  if (model.settingsOpen) {
1220
1377
  const settingsLayer = layerStack.find((layer) => layer.kind === 'settings');
1221
- const settingsOverlay = renderSettingsDrawer(model, options, pagesById, settingsLayer?.title);
1378
+ const settingsOverlay = renderSettingsDrawer(model, options, pagesById, resolvedShellThemes, settingsLayer?.title, themedFrameCtx);
1222
1379
  if (settingsOverlay != null) {
1223
1380
  overlays.push(settingsOverlay);
1224
1381
  }
1225
1382
  }
1226
1383
  if (model.notificationCenterOpen) {
1227
1384
  const notificationLayer = layerStack.find((layer) => layer.kind === 'notification-center');
1228
- const notificationCenterOverlay = renderNotificationCenterDrawer(model, options, pagesById, notificationLayer?.title);
1385
+ const notificationCenterOverlay = renderNotificationCenterDrawer(model, options, pagesById, notificationLayer?.title, themedFrameCtx);
1229
1386
  if (notificationCenterOverlay != null) {
1230
1387
  overlays.push(notificationCenterOverlay);
1231
1388
  }
1232
1389
  }
1233
1390
  if (model.helpOpen) {
1234
- const helpOverlay = renderHelpOverlay(model, activePage, frameKeys, paletteKeys, options, pagesById);
1391
+ const helpOverlay = renderHelpOverlay(model, activePage, frameKeys, paletteKeys, options, pagesById, resolvedShellThemes);
1235
1392
  overlays.push(modal({
1236
1393
  title: activeLayer.kind === 'help'
1237
1394
  ? (activeLayer.title ?? frameMessage(options.i18n, 'help.title', 'Keyboard Help'))
@@ -1240,6 +1397,9 @@ export function createFramedApp(options) {
1240
1397
  hint: typeof activeLayer.hintSource === 'string'
1241
1398
  ? activeLayer.hintSource
1242
1399
  : frameMessage(options.i18n, 'help.hint', 'j/k scroll • d/u page • g/G top/bottom • mouse wheel • ?/Esc close'),
1400
+ borderToken: themedFrameCtx?.border('primary'),
1401
+ bgToken: themedFrameCtx?.surface('elevated'),
1402
+ ctx: themedFrameCtx,
1243
1403
  width: helpOverlay.body.width + 4,
1244
1404
  screenWidth: model.columns,
1245
1405
  screenHeight: model.rows,
@@ -1247,7 +1407,11 @@ export function createFramedApp(options) {
1247
1407
  }
1248
1408
  if (model.commandPalette != null) {
1249
1409
  const paletteWidth = Math.max(20, Math.min(80, model.columns - 4));
1250
- const paletteBody = commandPalette(model.commandPalette, { width: Math.max(16, paletteWidth - 4) });
1410
+ const paletteBody = commandPaletteSurface(model.commandPalette, {
1411
+ width: Math.max(16, paletteWidth - 4),
1412
+ ctx: themedFrameCtx,
1413
+ showScrollbar: false,
1414
+ });
1251
1415
  const paletteLayer = activeLayer.kind === 'search' || activeLayer.kind === 'command-palette'
1252
1416
  ? activeLayer
1253
1417
  : undefined;
@@ -1257,13 +1421,16 @@ export function createFramedApp(options) {
1257
1421
  hint: typeof paletteLayer?.hintSource === 'string'
1258
1422
  ? paletteLayer.hintSource
1259
1423
  : frameMessage(options.i18n, 'palette.hint', 'Enter select • Esc close'),
1424
+ borderToken: themedFrameCtx?.border('primary'),
1425
+ bgToken: themedFrameCtx?.surface('elevated'),
1426
+ ctx: themedFrameCtx,
1260
1427
  width: paletteWidth,
1261
1428
  screenWidth: model.columns,
1262
1429
  screenHeight: model.rows,
1263
1430
  }));
1264
1431
  }
1265
1432
  if (model.quitConfirmOpen) {
1266
- overlays.push(renderShellQuitOverlay(model.columns, model.rows, options.i18n));
1433
+ overlays.push(renderShellQuitOverlay(model.columns, model.rows, options.i18n, themedFrameCtx));
1267
1434
  }
1268
1435
  if (bodySurface != null && bodyRect.width > 0 && bodyRect.height > 0) {
1269
1436
  frameSurface.blit(bodySurface, bodyRect.col, bodyRect.row);
@@ -1292,11 +1459,11 @@ function focusPane(model, paneId) {
1292
1459
  function resolveBodyRect(model, options) {
1293
1460
  return frameBodyRect(model.columns, model.rows, options.bodyTopRows ?? 1, options.bodyBottomRows ?? 1);
1294
1461
  }
1295
- function renderHelpOverlay(model, activePage, frameKeys, paletteKeys, options, pagesById) {
1462
+ function renderHelpOverlay(model, activePage, frameKeys, paletteKeys, options, pagesById, shellThemes) {
1296
1463
  const activePageModel = model.pageModels[model.activePageId];
1297
1464
  const activeInputArea = findInputAreaByPaneId(resolveInputAreas(activePage, activePageModel), model.focusedPaneByPage[model.activePageId]);
1298
1465
  const modalKeyMap = activePage.modalKeyMap?.(activePageModel);
1299
- const settings = resolveFrameSettings(model, options, pagesById);
1466
+ const settings = resolveFrameSettings(model, options, pagesById, shellThemes);
1300
1467
  const notificationCenter = resolveFrameNotificationCenter(model, options, pagesById);
1301
1468
  const workspaceHintSource = options.helpLineSource?.({
1302
1469
  model,
@@ -1389,13 +1556,15 @@ function isHelpScrollAction(action) {
1389
1556
  || action.type === 'top'
1390
1557
  || action.type === 'bottom';
1391
1558
  }
1392
- function resolveFrameSettings(model, options, pagesById) {
1559
+ const FRAME_SHELL_THEME_ROW_ID = '__frame-shell-theme__';
1560
+ function resolveFrameSettings(model, options, pagesById, shellThemes) {
1393
1561
  const activePage = pagesById.get(model.activePageId);
1394
- return options.settings?.({
1562
+ const provided = options.settings?.({
1395
1563
  model,
1396
1564
  activePage,
1397
1565
  pageModel: model.pageModels[model.activePageId],
1398
1566
  });
1567
+ return mergeShellThemeSettings(provided, shellThemes, model.activeShellThemeId, options.i18n);
1399
1568
  }
1400
1569
  function resolveFrameNotificationCenter(model, options, pagesById) {
1401
1570
  const activePage = pagesById.get(model.activePageId);
@@ -1430,8 +1599,8 @@ function resolveFrameNotificationCenter(model, options, pagesById) {
1430
1599
  activeFilter: model.runtimeNotificationHistoryFilter,
1431
1600
  };
1432
1601
  }
1433
- function resolveSettingsLayout(model, options, pagesById) {
1434
- const settings = resolveFrameSettings(model, options, pagesById);
1602
+ function resolveSettingsLayout(model, options, pagesById, shellThemes) {
1603
+ const settings = resolveFrameSettings(model, options, pagesById, shellThemes);
1435
1604
  if (settings == null)
1436
1605
  return undefined;
1437
1606
  const sections = settings.sections.filter((section) => section.rows.length > 0);
@@ -1460,6 +1629,7 @@ function resolveSettingsLayout(model, options, pagesById) {
1460
1629
  line,
1461
1630
  height: rowLayout.height,
1462
1631
  row,
1632
+ behavior: row.id === FRAME_SHELL_THEME_ROW_ID ? 'cycle-shell-theme' : undefined,
1463
1633
  });
1464
1634
  line += rowLayout.height;
1465
1635
  if (rowIndex < section.rows.length - 1) {
@@ -1490,7 +1660,7 @@ function resolveNotificationCenterDrawerWidth(columns) {
1490
1660
  const boundedColumns = Math.max(28, columns);
1491
1661
  return Math.min(Math.max(32, Math.floor(boundedColumns * 0.34)), Math.max(32, boundedColumns - 4), 52);
1492
1662
  }
1493
- function resolveNotificationCenterLayout(model, options, pagesById) {
1663
+ function resolveNotificationCenterLayout(model, options, pagesById, ctx) {
1494
1664
  const center = resolveFrameNotificationCenter(model, options, pagesById);
1495
1665
  if (center == null)
1496
1666
  return undefined;
@@ -1498,7 +1668,7 @@ function resolveNotificationCenterLayout(model, options, pagesById) {
1498
1668
  const anchor = frameEndAnchor(options.i18n);
1499
1669
  const startCol = anchor === 'left' ? 0 : Math.max(0, model.columns - drawerWidth);
1500
1670
  const contentWidth = Math.max(18, drawerWidth - 4);
1501
- const content = renderNotificationCenterSurface(center, contentWidth, options.i18n);
1671
+ const content = renderNotificationCenterSurface(center, contentWidth, options.i18n, ctx);
1502
1672
  const contentHeight = Math.max(1, model.rows - 2);
1503
1673
  const pagerState = createPagerStateForSurface(content, {
1504
1674
  width: contentWidth,
@@ -1588,12 +1758,12 @@ function cycleNotificationCenterFilter(model, layout) {
1588
1758
  notificationCenterScrollY: 0,
1589
1759
  }, []];
1590
1760
  }
1591
- function renderSettingsDrawer(model, options, pagesById, titleOverride) {
1592
- const layout = resolveSettingsLayout(model, options, pagesById);
1761
+ function renderSettingsDrawer(model, options, pagesById, shellThemes, titleOverride, ctx) {
1762
+ const layout = resolveSettingsLayout(model, options, pagesById, shellThemes);
1593
1763
  if (layout == null)
1594
1764
  return undefined;
1595
1765
  const scrollY = clampSettingsScroll(model, layout);
1596
- const content = renderSettingsSurface(layout, model);
1766
+ const content = renderSettingsSurface(layout, model, ctx);
1597
1767
  const pagerState = createPagerStateForSurface(content, {
1598
1768
  width: layout.contentWidth,
1599
1769
  height: layout.contentHeight,
@@ -1615,14 +1785,14 @@ function renderSettingsDrawer(model, options, pagesById, titleOverride) {
1615
1785
  content: body,
1616
1786
  borderToken: layout.settings.borderToken,
1617
1787
  bgToken: layout.settings.bgToken,
1618
- ctx: resolveSafeCtx() ?? undefined,
1788
+ ctx,
1619
1789
  width: layout.drawerWidth,
1620
1790
  screenWidth: model.columns,
1621
1791
  screenHeight: model.rows,
1622
1792
  });
1623
1793
  }
1624
- function renderNotificationCenterDrawer(model, options, pagesById, titleOverride) {
1625
- const layout = resolveNotificationCenterLayout(model, options, pagesById);
1794
+ function renderNotificationCenterDrawer(model, options, pagesById, titleOverride, ctx) {
1795
+ const layout = resolveNotificationCenterLayout(model, options, pagesById, ctx);
1626
1796
  if (layout == null)
1627
1797
  return undefined;
1628
1798
  const pagerState = createPagerStateForSurface(layout.content, {
@@ -1644,17 +1814,20 @@ function renderNotificationCenterDrawer(model, options, pagesById, titleOverride
1644
1814
  anchor: layout.anchor,
1645
1815
  title: titleOverride ?? `${layout.center.title} • ${frameNotificationFilterLabel(options.i18n, layout.center.activeFilter)}`,
1646
1816
  content: body,
1817
+ borderToken: ctx?.border('primary'),
1818
+ bgToken: ctx?.surface('elevated'),
1819
+ ctx,
1647
1820
  width: layout.drawerWidth,
1648
1821
  screenWidth: model.columns,
1649
1822
  screenHeight: model.rows,
1650
1823
  });
1651
1824
  }
1652
- function renderSettingsSurface(layout, model) {
1825
+ function renderSettingsSurface(layout, model, ctx) {
1653
1826
  const focusedIndex = clampSettingsFocus(model, layout);
1654
1827
  return preferenceListSurface(layout.preferenceSections, {
1655
1828
  width: layout.contentWidth,
1656
1829
  selectedRowId: layout.rows[focusedIndex]?.row.id,
1657
- ctx: resolveSafeCtx() ?? undefined,
1830
+ ctx,
1658
1831
  theme: layout.settings.listTheme,
1659
1832
  });
1660
1833
  }
@@ -1684,8 +1857,7 @@ function resolveNotificationFooterCue(model, options, pagesById) {
1684
1857
  const archivedCount = countNotificationHistory(center.state, center.activeFilter);
1685
1858
  return frameNotificationCue(options.i18n, liveCount, archivedCount);
1686
1859
  }
1687
- function renderNotificationCenterSurface(center, width, i18n) {
1688
- const ctx = resolveSafeCtx() ?? undefined;
1860
+ function renderNotificationCenterSurface(center, width, i18n, ctx) {
1689
1861
  const rows = [
1690
1862
  insetLineSurface(`Live: ${center.state.items.length} • Archived: ${center.state.history.length}`, width),
1691
1863
  insetLineSurface(`Filter: ${frameNotificationFilterLabel(i18n, center.activeFilter)}`, width),