@flyingrobots/bijou-tui 5.0.0 → 7.1.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.
Files changed (125) hide show
  1. package/README.md +152 -1
  2. package/dist/animate.d.ts +4 -0
  3. package/dist/animate.d.ts.map +1 -1
  4. package/dist/animate.js +47 -20
  5. package/dist/animate.js.map +1 -1
  6. package/dist/app-frame-actions.d.ts.map +1 -1
  7. package/dist/app-frame-actions.js +56 -1
  8. package/dist/app-frame-actions.js.map +1 -1
  9. package/dist/app-frame-i18n.d.ts.map +1 -1
  10. package/dist/app-frame-i18n.js +3 -1
  11. package/dist/app-frame-i18n.js.map +1 -1
  12. package/dist/app-frame-overlays.d.ts +16 -1
  13. package/dist/app-frame-overlays.d.ts.map +1 -1
  14. package/dist/app-frame-overlays.js +63 -1
  15. package/dist/app-frame-overlays.js.map +1 -1
  16. package/dist/app-frame-palette.d.ts.map +1 -1
  17. package/dist/app-frame-palette.js +36 -16
  18. package/dist/app-frame-palette.js.map +1 -1
  19. package/dist/app-frame-render.d.ts.map +1 -1
  20. package/dist/app-frame-render.js +14 -5
  21. package/dist/app-frame-render.js.map +1 -1
  22. package/dist/app-frame-types.d.ts +22 -0
  23. package/dist/app-frame-types.d.ts.map +1 -1
  24. package/dist/app-frame-types.js.map +1 -1
  25. package/dist/app-frame-utils.d.ts +3 -1
  26. package/dist/app-frame-utils.d.ts.map +1 -1
  27. package/dist/app-frame-utils.js +5 -0
  28. package/dist/app-frame-utils.js.map +1 -1
  29. package/dist/app-frame.d.ts +68 -6
  30. package/dist/app-frame.d.ts.map +1 -1
  31. package/dist/app-frame.js +121 -17
  32. package/dist/app-frame.js.map +1 -1
  33. package/dist/canvas.d.ts +11 -3
  34. package/dist/canvas.d.ts.map +1 -1
  35. package/dist/canvas.js +122 -9
  36. package/dist/canvas.js.map +1 -1
  37. package/dist/cell-glyph-fit.d.ts +20 -0
  38. package/dist/cell-glyph-fit.d.ts.map +1 -0
  39. package/dist/cell-glyph-fit.js +82 -0
  40. package/dist/cell-glyph-fit.js.map +1 -0
  41. package/dist/command-palette.d.ts +2 -0
  42. package/dist/command-palette.d.ts.map +1 -1
  43. package/dist/command-palette.js +36 -19
  44. package/dist/command-palette.js.map +1 -1
  45. package/dist/eventbus.d.ts +20 -0
  46. package/dist/eventbus.d.ts.map +1 -1
  47. package/dist/eventbus.js +47 -0
  48. package/dist/eventbus.js.map +1 -1
  49. package/dist/file-picker.d.ts +4 -0
  50. package/dist/file-picker.d.ts.map +1 -1
  51. package/dist/file-picker.js +9 -1
  52. package/dist/file-picker.js.map +1 -1
  53. package/dist/focus-area.d.ts +6 -5
  54. package/dist/focus-area.d.ts.map +1 -1
  55. package/dist/focus-area.js +6 -5
  56. package/dist/focus-area.js.map +1 -1
  57. package/dist/focus-map.d.ts +40 -0
  58. package/dist/focus-map.d.ts.map +1 -0
  59. package/dist/focus-map.js +133 -0
  60. package/dist/focus-map.js.map +1 -0
  61. package/dist/index.d.ts +22 -4
  62. package/dist/index.d.ts.map +1 -1
  63. package/dist/index.js +14 -2
  64. package/dist/index.js.map +1 -1
  65. package/dist/input-map.d.ts +68 -0
  66. package/dist/input-map.d.ts.map +1 -0
  67. package/dist/input-map.js +162 -0
  68. package/dist/input-map.js.map +1 -0
  69. package/dist/input-routing-inspector.d.ts +24 -0
  70. package/dist/input-routing-inspector.d.ts.map +1 -0
  71. package/dist/input-routing-inspector.js +76 -0
  72. package/dist/input-routing-inspector.js.map +1 -0
  73. package/dist/keybindings.d.ts.map +1 -1
  74. package/dist/keybindings.js +3 -0
  75. package/dist/keybindings.js.map +1 -1
  76. package/dist/layout-inspector.d.ts +31 -0
  77. package/dist/layout-inspector.d.ts.map +1 -0
  78. package/dist/layout-inspector.js +133 -0
  79. package/dist/layout-inspector.js.map +1 -0
  80. package/dist/notification.d.ts.map +1 -1
  81. package/dist/notification.js +38 -5
  82. package/dist/notification.js.map +1 -1
  83. package/dist/pipeline/middleware/motion.d.ts.map +1 -1
  84. package/dist/pipeline/middleware/motion.js +2 -1
  85. package/dist/pipeline/middleware/motion.js.map +1 -1
  86. package/dist/pipeline/middleware/paint.d.ts.map +1 -1
  87. package/dist/pipeline/middleware/paint.js +2 -1
  88. package/dist/pipeline/middleware/paint.js.map +1 -1
  89. package/dist/pipeline/pipeline.d.ts +8 -1
  90. package/dist/pipeline/pipeline.d.ts.map +1 -1
  91. package/dist/pipeline/pipeline.js +43 -7
  92. package/dist/pipeline/pipeline.js.map +1 -1
  93. package/dist/raster-glyph.d.ts +45 -0
  94. package/dist/raster-glyph.d.ts.map +1 -0
  95. package/dist/raster-glyph.js +405 -0
  96. package/dist/raster-glyph.js.map +1 -0
  97. package/dist/raytrace.d.ts +59 -0
  98. package/dist/raytrace.d.ts.map +1 -0
  99. package/dist/raytrace.js +121 -0
  100. package/dist/raytrace.js.map +1 -0
  101. package/dist/runtime-binding.d.ts +59 -0
  102. package/dist/runtime-binding.d.ts.map +1 -0
  103. package/dist/runtime-binding.js +271 -0
  104. package/dist/runtime-binding.js.map +1 -0
  105. package/dist/runtime.d.ts.map +1 -1
  106. package/dist/runtime.js +51 -6
  107. package/dist/runtime.js.map +1 -1
  108. package/dist/shell-quit.js +1 -1
  109. package/dist/shell-quit.js.map +1 -1
  110. package/dist/surface-budget.d.ts +27 -0
  111. package/dist/surface-budget.d.ts.map +1 -0
  112. package/dist/surface-budget.js +61 -0
  113. package/dist/surface-budget.js.map +1 -0
  114. package/dist/surface-diff.d.ts +36 -0
  115. package/dist/surface-diff.d.ts.map +1 -0
  116. package/dist/surface-diff.js +202 -0
  117. package/dist/surface-diff.js.map +1 -0
  118. package/dist/surface-layout.d.ts +11 -0
  119. package/dist/surface-layout.d.ts.map +1 -1
  120. package/dist/surface-layout.js +16 -1
  121. package/dist/surface-layout.js.map +1 -1
  122. package/dist/types.d.ts +8 -0
  123. package/dist/types.d.ts.map +1 -1
  124. package/dist/types.js.map +1 -1
  125. package/package.json +4 -4
package/dist/app-frame.js CHANGED
@@ -4,7 +4,7 @@
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 { cloneContextWithResolvedTheme, createResolved, createSurface, setDefaultContext, resolveClock, resolveSafeCtx, } from '@flyingrobots/bijou';
7
+ import { cloneContextWithResolvedTheme, createSurface, perfOverlaySurface, setDefaultContext, resolveClock, resolveSafeCtx, } from '@flyingrobots/bijou';
8
8
  import { createKeyMap, formatKeyCombo } from './keybindings.js';
9
9
  import { isKeyMsg, isMouseMsg, isResizeMsg } from './types.js';
10
10
  import { quit } from './commands.js';
@@ -15,14 +15,15 @@ import { commandPaletteSurface, commandPaletteKeyMap, } from './command-palette.
15
15
  import { restoreLayoutState } from './layout-preset.js';
16
16
  import { createNotificationState, dismissNotification, hitTestNotificationStack, notificationsNeedTick, pushNotification, renderNotificationStack, tickNotifications, trimNotificationsToViewport, } from './notification.js';
17
17
  import { isFrameScopedMsg, isPageScopedMsg, wrapCmdForPage, emitMsg, emitMsgForPage, wrapFrameMsg, } from './app-frame-types.js';
18
- import { createFrameKeyMap, frameBodyRect, mergeBindingSources, } from './app-frame-utils.js';
19
- import { renderHelpOverlay, isHelpScrollAction, resolveCurrentShellTheme, resolveNextShellTheme, resolveShellThemeForContext, resolveFrameSettings, resolveFrameNotificationCenter, resolveSettingsLayout, resolveNotificationCenterLayout, resolveInputAreas, findInputAreaByPaneId, moveSettingsFocus, scrollSettingsBy, clampSettingsScroll, scrollNotificationCenterBy, cycleNotificationCenterFilter, clampSettingsFocus, renderSettingsDrawer, renderNotificationCenterDrawer, resolveNotificationFooterCue, } from './app-frame-overlays.js';
18
+ import { createFrameKeyMap, frameBodyRect, mergeBindingSources, resolveFramePageText, } from './app-frame-utils.js';
19
+ import { renderHelpOverlay, isHelpScrollAction, resolveFrameShellThemeChoices, resolveCurrentShellTheme, resolveNextShellTheme, resolveShellThemeForContext, resolveFrameSettings, resolveFrameNotificationCenter, resolveSettingsLayout, resolveNotificationCenterLayout, resolveInputAreas, findInputAreaByPaneId, moveSettingsFocus, scrollSettingsBy, clampSettingsScroll, scrollNotificationCenterBy, cycleNotificationCenterFilter, clampSettingsFocus, renderSettingsDrawer, renderNotificationCenterDrawer, resolveNotificationFooterCue, } from './app-frame-overlays.js';
20
20
  import { activeFrameLayer, describeFrameRuntimeViewStack, projectFrameControls, } from './app-frame-layers.js';
21
21
  import { applyRuntimeCommandBuffer, bufferRuntimeRouteResult, createRuntimeBuffers, createRuntimeRetainedLayouts, retainRuntimeLayout, routeRuntimeInput, } from './runtime-engine.js';
22
22
  import { frameMessage, frameNotificationFilterLabel, } from './app-frame-i18n.js';
23
23
  import { resolveHeaderLine, renderHelpLine, renderPageContent, renderPageContentInto, renderMaximizedPane, renderMaximizedPaneInto, renderTransition, createFramePaneScratchPool, } from './app-frame-render.js';
24
24
  import { applyFrameAction, scrollFocusedPane, switchTab, syncPageFrameState, } from './app-frame-actions.js';
25
25
  import { handlePaletteKey, openCommandPalette, openSearchPalette, } from './app-frame-palette.js';
26
+ import { createInputActionMap, createInputGestureRecognizer, } from './input-map.js';
26
27
  export { emitFrameAction, notify, } from './app-frame-types.js';
27
28
  export { activeFrameLayer, describeFrameLayerStack, describeFrameRuntimeViewStack, projectFrameControls, underlyingFrameLayer, } from './app-frame-layers.js';
28
29
  // ---------------------------------------------------------------------------
@@ -31,6 +32,7 @@ export { activeFrameLayer, describeFrameLayerStack, describeFrameRuntimeViewStac
31
32
  const FRAME_NOTIFICATION_TICK_MS = 40;
32
33
  const DEFAULT_FRAME_NOTIFICATION_DURATION_MS = 6_000;
33
34
  const SETTINGS_FEEDBACK_TOAST_WIDTH = 40;
35
+ const FRAME_FOOTER_DOUBLE_TAB_MS = 300;
34
36
  const EMPTY_RUNTIME_LAYOUTS = createRuntimeRetainedLayouts();
35
37
  function createQuitHelpKeys(i18n) {
36
38
  const t = (id, fallback) => frameMessage(i18n, id, fallback);
@@ -146,7 +148,7 @@ function cloneShellThemeContext(ctx, resolvedTheme) {
146
148
  function readStageDuration(timings, stage) {
147
149
  return timings.find((timing) => timing.stage === stage)?.durationMs ?? 0;
148
150
  }
149
- function summarizeFrameTimings(timings, frameBudgetMs) {
151
+ export function summarizeFrameTimings(timings, frameBudgetMs) {
150
152
  const frameTimeMs = timings.reduce((total, timing) => total + timing.durationMs, 0);
151
153
  return {
152
154
  frameTimeMs,
@@ -156,6 +158,36 @@ function summarizeFrameTimings(timings, frameBudgetMs) {
156
158
  frameOverBudget: frameBudgetMs != null && frameTimeMs > frameBudgetMs,
157
159
  };
158
160
  }
161
+ export function renderFramePerfHudOverlay(telemetry, options = {}) {
162
+ const refreshRate = telemetry.refreshRate ?? options.ctx?.runtime.refreshRate ?? 60;
163
+ const fps = telemetry.frameTimeMs > 0
164
+ ? Math.min(refreshRate, 1000 / telemetry.frameTimeMs)
165
+ : refreshRate;
166
+ const width = Math.max(12, Math.min(40, telemetry.columns - 2));
167
+ const surface = perfOverlaySurface({
168
+ fps,
169
+ frameTimeMs: telemetry.frameTimeMs,
170
+ width: telemetry.columns,
171
+ height: telemetry.rows,
172
+ extras: [
173
+ { label: 'view', value: `${telemetry.viewTimeMs.toFixed(2)} ms` },
174
+ { label: 'diff', value: `${telemetry.diffTimeMs.toFixed(2)} ms` },
175
+ ],
176
+ }, {
177
+ title: frameMessage(options.i18n, 'perfHud.title', 'Perf HUD'),
178
+ width,
179
+ showChart: false,
180
+ borderToken: options.ctx?.border('primary'),
181
+ bgToken: options.ctx?.surface('elevated'),
182
+ ctx: options.ctx,
183
+ });
184
+ return {
185
+ content: '',
186
+ surface,
187
+ row: 1,
188
+ col: Math.max(0, telemetry.columns - surface.width - 1),
189
+ };
190
+ }
159
191
  function applyFrameTimingSnapshot(model, snapshot) {
160
192
  if (model.frameTimeMs === snapshot.frameTimeMs
161
193
  && model.viewTimeMs === snapshot.viewTimeMs
@@ -206,7 +238,8 @@ export function createFramedApp(options) {
206
238
  const shellThemeSpecs = options.shellThemes ?? [];
207
239
  let defaultFrameCtx = options.ctx ?? resolveSafeCtx();
208
240
  let resolvedShellThemes = [];
209
- const enableShellThemeSettings = shellThemeSpecs.length > 1;
241
+ const enableShellThemeSettings = shellThemeSpecs.length > 1
242
+ || shellThemeSpecs.some((theme) => (theme.modes?.length ?? 0) > 1);
210
243
  const usesAmbientDefaultContext = options.ctx == null && defaultFrameCtx != null;
211
244
  let frameCtx = options.ctx;
212
245
  let frameCtxShellThemeId;
@@ -219,13 +252,7 @@ export function createFramedApp(options) {
219
252
  throw new Error('createFramedApp: shellThemes requires options.ctx, app.run({ ctx }), or a default Bijou context');
220
253
  }
221
254
  defaultFrameCtx ??= baseCtx;
222
- resolvedShellThemes = shellThemeSpecs.map((theme) => ({
223
- id: theme.id,
224
- label: theme.label,
225
- description: theme.description,
226
- shellTheme: theme,
227
- resolvedTheme: createResolved(theme.theme, defaultFrameCtx.theme.noColor, defaultFrameCtx.theme.colorScheme),
228
- }));
255
+ resolvedShellThemes = resolveFrameShellThemeChoices(shellThemeSpecs, defaultFrameCtx);
229
256
  }
230
257
  if (frameCtx != null) {
231
258
  ensureResolvedShellThemes(frameCtx);
@@ -261,6 +288,11 @@ export function createFramedApp(options) {
261
288
  }
262
289
  options.onShellThemeChange?.({
263
290
  shellTheme: nextTheme.shellTheme,
291
+ shellThemeSpec: nextTheme.shellThemeSpec,
292
+ shellThemeId: nextTheme.shellThemeId,
293
+ shellThemeLabel: nextTheme.shellThemeLabel,
294
+ modeId: nextTheme.modeId,
295
+ modeLabel: nextTheme.modeLabel,
264
296
  ctx: frameCtx,
265
297
  });
266
298
  return frameCtx;
@@ -270,6 +302,13 @@ export function createFramedApp(options) {
270
302
  enableNotifications: options.notificationCenter != null || options.runtimeNotifications !== false,
271
303
  i18n: options.i18n,
272
304
  });
305
+ const frameInputRecognizer = createInputGestureRecognizer({
306
+ doubleTapMs: FRAME_FOOTER_DOUBLE_TAB_MS,
307
+ });
308
+ const frameInputActions = createInputActionMap()
309
+ .bind('frame.footer.doubleTab', 'Toggle footer', [
310
+ { deviceId: 'keyboard', featureId: 'key.tab', type: 'double-tap' },
311
+ ], { type: 'toggle-footer' });
273
312
  const quitHelpKeys = createQuitHelpKeys(options.i18n);
274
313
  const helpLayerHelpKeys = createHelpLayerHelpKeys(options.i18n);
275
314
  const settingsHelpKeys = createSettingsHelpKeys(options.i18n);
@@ -553,6 +592,23 @@ export function createFramedApp(options) {
553
592
  }
554
593
  return [{ type: 'observed-key', msg, route }, { type: 'open-quit-confirm' }];
555
594
  }
595
+ function resolveFrameInputActionCommands(msg, route) {
596
+ const inputEvent = frameInputRecognizer.observeKey(msg, resolveClock(resolveFrameCtx()).now());
597
+ const inputAction = frameInputActions.handle(inputEvent);
598
+ if (inputAction == null || msg.ctrl || msg.alt || msg.shift) {
599
+ return undefined;
600
+ }
601
+ return [{ type: 'observed-key', msg, route }, { type: 'apply-frame-action', action: inputAction }];
602
+ }
603
+ function routeForFrameInputAction(layerKind) {
604
+ if (layerKind === 'search' || layerKind === 'command-palette')
605
+ return 'palette';
606
+ if (layerKind === 'help')
607
+ return 'help';
608
+ if (layerKind === 'page-modal')
609
+ return 'page';
610
+ return 'frame';
611
+ }
556
612
  function resolveFrameActionCommands(msg, action, route) {
557
613
  if (action.type === 'open-search' && options.enableCommandPalette) {
558
614
  return [{ type: 'observed-key', msg, route }, { type: 'open-search-palette' }];
@@ -584,6 +640,9 @@ export function createFramedApp(options) {
584
640
  if (frameAction?.type === 'toggle-notifications') {
585
641
  return [obs, { type: 'close-palette' }, { type: 'apply-frame-action', action: frameAction }];
586
642
  }
643
+ if (frameAction?.type === 'toggle-perf-hud') {
644
+ return [obs, { type: 'apply-frame-action', action: frameAction }];
645
+ }
587
646
  return [obs, { type: 'palette-key', msg }];
588
647
  }
589
648
  function handleHelpLayerKeyCommands(msg) {
@@ -595,6 +654,9 @@ export function createFramedApp(options) {
595
654
  return quitRequestCommands(msg, 'help');
596
655
  }
597
656
  const helpAction = frameKeys.handle(msg);
657
+ if (helpAction?.type === 'toggle-perf-hud') {
658
+ return [obs, { type: 'apply-frame-action', action: helpAction }];
659
+ }
598
660
  if (helpAction && isHelpScrollAction(helpAction)) {
599
661
  const action = helpAction.type === 'scroll-down' ? 'down'
600
662
  : helpAction.type === 'scroll-up' ? 'up'
@@ -633,6 +695,9 @@ export function createFramedApp(options) {
633
695
  if (settingsFrameAction?.type === 'toggle-notifications') {
634
696
  return [obs, { type: 'apply-frame-action', action: settingsFrameAction }];
635
697
  }
698
+ if (settingsFrameAction?.type === 'toggle-perf-hud') {
699
+ return [obs, { type: 'apply-frame-action', action: settingsFrameAction }];
700
+ }
636
701
  if (!msg.ctrl && !msg.alt && msg.key === 'up') {
637
702
  return [obs, { type: 'settings-focus-move', delta: -1 }];
638
703
  }
@@ -685,6 +750,9 @@ export function createFramedApp(options) {
685
750
  if (centerFrameAction?.type === 'toggle-notifications') {
686
751
  return [obs, { type: 'apply-frame-action', action: centerFrameAction }];
687
752
  }
753
+ if (centerFrameAction?.type === 'toggle-perf-hud') {
754
+ return [obs, { type: 'apply-frame-action', action: centerFrameAction }];
755
+ }
688
756
  if (!msg.ctrl && !msg.alt && msg.key === 'f2') {
689
757
  return [obs, { type: 'close-notification-center' }, { type: 'apply-frame-action', action: { type: 'toggle-settings' } }];
690
758
  }
@@ -772,6 +840,10 @@ export function createFramedApp(options) {
772
840
  const frameLayer = layer.model;
773
841
  if (frameLayer == null)
774
842
  return undefined;
843
+ const inputActionCommands = resolveFrameInputActionCommands(msg, routeForFrameInputAction(frameLayer.kind));
844
+ if (inputActionCommands != null) {
845
+ return { handled: true, commands: inputActionCommands };
846
+ }
775
847
  if (frameLayer.kind === 'search' || frameLayer.kind === 'command-palette') {
776
848
  return { handled: true, commands: handlePaletteLayerKeyCommands(msg, frameLayer.kind) };
777
849
  }
@@ -788,6 +860,10 @@ export function createFramedApp(options) {
788
860
  }
789
861
  if (frameLayer.kind === 'quit-confirm') {
790
862
  const obs = { type: 'observed-key', msg, route: 'frame' };
863
+ const frameAction = frameKeys.handle(msg);
864
+ if (frameAction?.type === 'toggle-perf-hud') {
865
+ return { handled: true, commands: [obs, { type: 'apply-frame-action', action: frameAction }] };
866
+ }
791
867
  if (isShellQuitConfirmAccept(msg)) {
792
868
  return { handled: true, commands: [obs, { type: 'close-quit-confirm' }, { type: 'quit' }] };
793
869
  }
@@ -799,6 +875,10 @@ export function createFramedApp(options) {
799
875
  if (frameLayer.kind === 'page-modal') {
800
876
  const { modalKeyMap } = context;
801
877
  const obs = { type: 'observed-key', msg, route: 'page' };
878
+ const frameAction = frameKeys.handle(msg);
879
+ if (frameAction?.type === 'toggle-perf-hud') {
880
+ return { handled: true, commands: [obs, { type: 'apply-frame-action', action: frameAction }] };
881
+ }
802
882
  if (modalKeyMap != null) {
803
883
  const modalAction = modalKeyMap.handle(msg);
804
884
  if (modalAction !== undefined) {
@@ -1101,21 +1181,22 @@ export function createFramedApp(options) {
1101
1181
  const quitHint = frameMessage(options.i18n, 'quit.footer', 'Y quit • N stay');
1102
1182
  const paletteTitle = model.commandPaletteTitle
1103
1183
  ?? frameMessage(options.i18n, 'palette.title', 'Command Palette');
1184
+ const activePageTitle = resolveFramePageText(activePage.title, activePageModel) ?? '';
1104
1185
  const searchTitle = model.commandPaletteTitle
1105
- ?? activePage.searchTitle
1186
+ ?? resolveFramePageText(activePage.searchTitle, activePageModel)
1106
1187
  ?? frameMessage(options.i18n, 'search.title', 'Search');
1107
1188
  const notificationsTitle = notificationCenter == null
1108
1189
  ? frameMessage(options.i18n, 'notifications.title', 'Notifications')
1109
1190
  : `${notificationCenter.title} • ${frameNotificationFilterLabel(options.i18n, notificationCenter.activeFilter)}`;
1110
1191
  const pageLayers = activePage.layers?.(activePageModel);
1111
1192
  const workspaceLayer = {
1112
- title: activePage.title,
1193
+ title: activePageTitle,
1113
1194
  hintSource: workspaceHintSource,
1114
1195
  helpSource: workspaceHelpSource,
1115
1196
  ...pageLayers?.workspace,
1116
1197
  };
1117
1198
  const pageModalLayer = {
1118
- title: activePage.title,
1199
+ title: activePageTitle,
1119
1200
  hintSource: modalKeyMap ?? activePage.helpSource ?? activePage.keyMap,
1120
1201
  helpSource: mergeBindingSources(quitHelpKeys, modalKeyMap, activePage.helpSource ?? activePage.keyMap),
1121
1202
  ...pageLayers?.['page-modal'],
@@ -1270,7 +1351,11 @@ export function createFramedApp(options) {
1270
1351
  diffTimeMs: 0,
1271
1352
  frameBudgetMs: undefined,
1272
1353
  frameOverBudget: false,
1354
+ perfHudOpen: false,
1273
1355
  helpOpen: false,
1356
+ footerVisible: true,
1357
+ footerTranslateY: 0,
1358
+ footerAnimationGeneration: 0,
1274
1359
  helpScrollY: 0,
1275
1360
  commandPaletteKind: undefined,
1276
1361
  settingsOpen: false,
@@ -1446,7 +1531,12 @@ export function createFramedApp(options) {
1446
1531
  frameSurface.clear();
1447
1532
  frameSurface.blit(header, 0, 0);
1448
1533
  if (model.rows > 1) {
1449
- frameSurface.blit(helpLine, 0, model.rows - 1);
1534
+ const footerVisible = model.footerVisible ?? true;
1535
+ const footerTranslateY = Math.max(0, Math.min(1, model.footerTranslateY ?? (footerVisible ? 0 : 1)));
1536
+ const footerRow = model.rows - helpLine.height + Math.floor(footerTranslateY * helpLine.height);
1537
+ if (footerRow < model.rows) {
1538
+ frameSurface.blit(helpLine, 0, footerRow);
1539
+ }
1450
1540
  }
1451
1541
  let activeResult;
1452
1542
  let bodySurface;
@@ -1551,10 +1641,24 @@ export function createFramedApp(options) {
1551
1641
  if (model.quitConfirmOpen) {
1552
1642
  overlays.push(renderShellQuitOverlay(model.columns, model.rows, options.i18n, themedFrameCtx));
1553
1643
  }
1644
+ const dimBackground = overlays.length > 0;
1645
+ if (model.perfHudOpen) {
1646
+ overlays.push(renderFramePerfHudOverlay({
1647
+ columns: model.columns,
1648
+ rows: model.rows,
1649
+ frameTimeMs: model.frameTimeMs,
1650
+ viewTimeMs: model.viewTimeMs,
1651
+ diffTimeMs: model.diffTimeMs,
1652
+ refreshRate: themedFrameCtx?.runtime.refreshRate,
1653
+ }, {
1654
+ i18n: options.i18n,
1655
+ ctx: themedFrameCtx,
1656
+ }));
1657
+ }
1554
1658
  if (bodySurface != null && bodyRect.width > 0 && bodyRect.height > 0) {
1555
1659
  frameSurface.blit(bodySurface, bodyRect.col, bodyRect.row);
1556
1660
  }
1557
- return compositeSurfaceInto(frameSurface, frameSurface, overlays, { dim: overlays.length > 0 });
1661
+ return compositeSurfaceInto(frameSurface, frameSurface, overlays, { dim: dimBackground });
1558
1662
  },
1559
1663
  routeRuntimeIssue(issue) {
1560
1664
  if (!frameNotificationOptions.enabled)