@flyingrobots/bijou-tui 4.4.1 → 7.0.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 (203) hide show
  1. package/README.md +244 -21
  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 +55 -0
  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 +51 -0
  11. package/dist/app-frame-i18n.js.map +1 -1
  12. package/dist/app-frame-layers.d.ts +11 -1
  13. package/dist/app-frame-layers.d.ts.map +1 -1
  14. package/dist/app-frame-layers.js +19 -0
  15. package/dist/app-frame-layers.js.map +1 -1
  16. package/dist/app-frame-overlays.d.ts +82 -0
  17. package/dist/app-frame-overlays.d.ts.map +1 -0
  18. package/dist/app-frame-overlays.js +480 -0
  19. package/dist/app-frame-overlays.js.map +1 -0
  20. package/dist/app-frame-palette.d.ts.map +1 -1
  21. package/dist/app-frame-palette.js +36 -16
  22. package/dist/app-frame-palette.js.map +1 -1
  23. package/dist/app-frame-render.d.ts.map +1 -1
  24. package/dist/app-frame-render.js +16 -7
  25. package/dist/app-frame-render.js.map +1 -1
  26. package/dist/app-frame-types.d.ts +141 -5
  27. package/dist/app-frame-types.d.ts.map +1 -1
  28. package/dist/app-frame-types.js +17 -1
  29. package/dist/app-frame-types.js.map +1 -1
  30. package/dist/app-frame-utils.d.ts +3 -1
  31. package/dist/app-frame-utils.d.ts.map +1 -1
  32. package/dist/app-frame-utils.js +11 -5
  33. package/dist/app-frame-utils.js.map +1 -1
  34. package/dist/app-frame.d.ts +22 -86
  35. package/dist/app-frame.d.ts.map +1 -1
  36. package/dist/app-frame.js +475 -629
  37. package/dist/app-frame.js.map +1 -1
  38. package/dist/browsable-list.d.ts +12 -0
  39. package/dist/browsable-list.d.ts.map +1 -1
  40. package/dist/browsable-list.js +17 -6
  41. package/dist/browsable-list.js.map +1 -1
  42. package/dist/canvas.d.ts +11 -3
  43. package/dist/canvas.d.ts.map +1 -1
  44. package/dist/canvas.js +146 -13
  45. package/dist/canvas.js.map +1 -1
  46. package/dist/cell-glyph-fit.d.ts +20 -0
  47. package/dist/cell-glyph-fit.d.ts.map +1 -0
  48. package/dist/cell-glyph-fit.js +82 -0
  49. package/dist/cell-glyph-fit.js.map +1 -0
  50. package/dist/collection-surface.d.ts +8 -0
  51. package/dist/collection-surface.d.ts.map +1 -1
  52. package/dist/collection-surface.js +72 -8
  53. package/dist/collection-surface.js.map +1 -1
  54. package/dist/command-palette.d.ts +2 -0
  55. package/dist/command-palette.d.ts.map +1 -1
  56. package/dist/command-palette.js +36 -19
  57. package/dist/command-palette.js.map +1 -1
  58. package/dist/css/text-style.d.ts +2 -0
  59. package/dist/css/text-style.d.ts.map +1 -1
  60. package/dist/css/text-style.js +18 -8
  61. package/dist/css/text-style.js.map +1 -1
  62. package/dist/debug-overlay.d.ts +19 -0
  63. package/dist/debug-overlay.d.ts.map +1 -0
  64. package/dist/debug-overlay.js +25 -0
  65. package/dist/debug-overlay.js.map +1 -0
  66. package/dist/design-language.d.ts.map +1 -1
  67. package/dist/design-language.js +4 -5
  68. package/dist/design-language.js.map +1 -1
  69. package/dist/driver.d.ts +102 -0
  70. package/dist/driver.d.ts.map +1 -1
  71. package/dist/driver.js +259 -19
  72. package/dist/driver.js.map +1 -1
  73. package/dist/eventbus.d.ts +20 -0
  74. package/dist/eventbus.d.ts.map +1 -1
  75. package/dist/eventbus.js +47 -0
  76. package/dist/eventbus.js.map +1 -1
  77. package/dist/file-picker.d.ts.map +1 -1
  78. package/dist/file-picker.js +2 -2
  79. package/dist/file-picker.js.map +1 -1
  80. package/dist/flex.d.ts.map +1 -1
  81. package/dist/flex.js +6 -6
  82. package/dist/flex.js.map +1 -1
  83. package/dist/focus-area.d.ts +13 -4
  84. package/dist/focus-area.d.ts.map +1 -1
  85. package/dist/focus-area.js +56 -14
  86. package/dist/focus-area.js.map +1 -1
  87. package/dist/focus-map.d.ts +40 -0
  88. package/dist/focus-map.d.ts.map +1 -0
  89. package/dist/focus-map.js +133 -0
  90. package/dist/focus-map.js.map +1 -0
  91. package/dist/grid.d.ts +2 -2
  92. package/dist/grid.d.ts.map +1 -1
  93. package/dist/grid.js +11 -148
  94. package/dist/grid.js.map +1 -1
  95. package/dist/help.d.ts +2 -0
  96. package/dist/help.d.ts.map +1 -1
  97. package/dist/help.js +6 -4
  98. package/dist/help.js.map +1 -1
  99. package/dist/icon-presentation.d.ts +10 -0
  100. package/dist/icon-presentation.d.ts.map +1 -0
  101. package/dist/icon-presentation.js +12 -0
  102. package/dist/icon-presentation.js.map +1 -0
  103. package/dist/index.d.ts +32 -9
  104. package/dist/index.d.ts.map +1 -1
  105. package/dist/index.js +17 -5
  106. package/dist/index.js.map +1 -1
  107. package/dist/input-map.d.ts +68 -0
  108. package/dist/input-map.d.ts.map +1 -0
  109. package/dist/input-map.js +162 -0
  110. package/dist/input-map.js.map +1 -0
  111. package/dist/input-routing-inspector.d.ts +24 -0
  112. package/dist/input-routing-inspector.d.ts.map +1 -0
  113. package/dist/input-routing-inspector.js +76 -0
  114. package/dist/input-routing-inspector.js.map +1 -0
  115. package/dist/keybindings.d.ts.map +1 -1
  116. package/dist/keybindings.js +3 -0
  117. package/dist/keybindings.js.map +1 -1
  118. package/dist/layout-inspector.d.ts +31 -0
  119. package/dist/layout-inspector.d.ts.map +1 -0
  120. package/dist/layout-inspector.js +133 -0
  121. package/dist/layout-inspector.js.map +1 -0
  122. package/dist/layout-preset.d.ts +1 -1
  123. package/dist/layout-preset.d.ts.map +1 -1
  124. package/dist/motion/reconciler.d.ts +6 -2
  125. package/dist/motion/reconciler.d.ts.map +1 -1
  126. package/dist/motion/reconciler.js +40 -10
  127. package/dist/motion/reconciler.js.map +1 -1
  128. package/dist/motion/types.d.ts +3 -1
  129. package/dist/motion/types.d.ts.map +1 -1
  130. package/dist/navigable-table.d.ts +9 -10
  131. package/dist/navigable-table.d.ts.map +1 -1
  132. package/dist/navigable-table.js +33 -14
  133. package/dist/navigable-table.js.map +1 -1
  134. package/dist/notification.d.ts +15 -0
  135. package/dist/notification.d.ts.map +1 -1
  136. package/dist/notification.js +136 -34
  137. package/dist/notification.js.map +1 -1
  138. package/dist/overlay.d.ts.map +1 -1
  139. package/dist/overlay.js +40 -14
  140. package/dist/overlay.js.map +1 -1
  141. package/dist/pager.d.ts +3 -1
  142. package/dist/pager.d.ts.map +1 -1
  143. package/dist/pager.js +4 -0
  144. package/dist/pager.js.map +1 -1
  145. package/dist/pipeline/middleware/grayscale.d.ts.map +1 -1
  146. package/dist/pipeline/middleware/grayscale.js +5 -5
  147. package/dist/pipeline/middleware/grayscale.js.map +1 -1
  148. package/dist/pipeline/middleware/motion.d.ts.map +1 -1
  149. package/dist/pipeline/middleware/motion.js +4 -3
  150. package/dist/pipeline/middleware/motion.js.map +1 -1
  151. package/dist/pipeline/middleware/paint.d.ts.map +1 -1
  152. package/dist/pipeline/middleware/paint.js +2 -1
  153. package/dist/pipeline/middleware/paint.js.map +1 -1
  154. package/dist/pipeline/middleware/surface-shaders.d.ts +37 -0
  155. package/dist/pipeline/middleware/surface-shaders.d.ts.map +1 -0
  156. package/dist/pipeline/middleware/surface-shaders.js +164 -0
  157. package/dist/pipeline/middleware/surface-shaders.js.map +1 -0
  158. package/dist/pipeline/pipeline.d.ts +37 -2
  159. package/dist/pipeline/pipeline.d.ts.map +1 -1
  160. package/dist/pipeline/pipeline.js +121 -22
  161. package/dist/pipeline/pipeline.js.map +1 -1
  162. package/dist/raytrace.d.ts +59 -0
  163. package/dist/raytrace.d.ts.map +1 -0
  164. package/dist/raytrace.js +121 -0
  165. package/dist/raytrace.js.map +1 -0
  166. package/dist/runtime-binding.d.ts +59 -0
  167. package/dist/runtime-binding.d.ts.map +1 -0
  168. package/dist/runtime-binding.js +271 -0
  169. package/dist/runtime-binding.js.map +1 -0
  170. package/dist/runtime.d.ts +19 -0
  171. package/dist/runtime.d.ts.map +1 -1
  172. package/dist/runtime.js +222 -31
  173. package/dist/runtime.js.map +1 -1
  174. package/dist/split-pane.d.ts +2 -2
  175. package/dist/split-pane.d.ts.map +1 -1
  176. package/dist/split-pane.js +28 -49
  177. package/dist/split-pane.js.map +1 -1
  178. package/dist/subapp/mount.d.ts +17 -0
  179. package/dist/subapp/mount.d.ts.map +1 -1
  180. package/dist/subapp/mount.js +13 -0
  181. package/dist/subapp/mount.js.map +1 -1
  182. package/dist/surface-budget.d.ts +27 -0
  183. package/dist/surface-budget.d.ts.map +1 -0
  184. package/dist/surface-budget.js +61 -0
  185. package/dist/surface-budget.js.map +1 -0
  186. package/dist/surface-diff.d.ts +36 -0
  187. package/dist/surface-diff.d.ts.map +1 -0
  188. package/dist/surface-diff.js +202 -0
  189. package/dist/surface-diff.js.map +1 -0
  190. package/dist/surface-layout.d.ts +19 -3
  191. package/dist/surface-layout.d.ts.map +1 -1
  192. package/dist/surface-layout.js +42 -12
  193. package/dist/surface-layout.js.map +1 -1
  194. package/dist/types.d.ts +8 -0
  195. package/dist/types.d.ts.map +1 -1
  196. package/dist/types.js.map +1 -1
  197. package/dist/view-output.js +2 -2
  198. package/dist/view-output.js.map +1 -1
  199. package/dist/viewport.d.ts +13 -1
  200. package/dist/viewport.d.ts.map +1 -1
  201. package/dist/viewport.js +33 -11
  202. package/dist/viewport.js.map +1 -1
  203. package/package.json +4 -4
package/dist/app-frame.js CHANGED
@@ -4,101 +4,109 @@
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, preparePreferenceSections, preferenceListSurface, resolvePreferenceRowLayout, resolveClock, resolveSafeCtx, } from '@flyingrobots/bijou';
8
- import { helpViewSurface } from './help.js';
9
- import { createKeyMap } from './keybindings.js';
7
+ import { cloneContextWithResolvedTheme, createResolved, createSurface, perfOverlaySurface, setDefaultContext, resolveClock, resolveSafeCtx, } from '@flyingrobots/bijou';
8
+ import { createKeyMap, formatKeyCombo } from './keybindings.js';
10
9
  import { isKeyMsg, isMouseMsg, isResizeMsg } from './types.js';
11
10
  import { quit } from './commands.js';
12
- import { compositeSurfaceInto, drawer, modal } from './overlay.js';
11
+ import { runWithLifecycleHooks } from './runtime.js';
12
+ import { compositeSurfaceInto, modal } from './overlay.js';
13
13
  import { isShellQuitConfirmAccept, isShellQuitConfirmDismiss, isShellQuitRequest, renderShellQuitOverlay, shouldUseShellQuitConfirm, } from './shell-quit.js';
14
14
  import { commandPaletteSurface, commandPaletteKeyMap, } from './command-palette.js';
15
- import { createPagerStateForSurface, pagerSurface, } from './pager.js';
16
15
  import { restoreLayoutState } from './layout-preset.js';
17
- import { countNotificationHistory, createNotificationState, dismissNotification, hitTestNotificationStack, notificationsNeedTick, pushNotification, renderNotificationHistorySurface, renderNotificationReviewEntrySurface, renderNotificationStack, tickNotifications, trimNotificationsToViewport, } from './notification.js';
18
- import { insetLineSurface } from './collection-surface.js';
19
- import { vstackSurface } from './surface-layout.js';
16
+ import { createNotificationState, dismissNotification, hitTestNotificationStack, notificationsNeedTick, pushNotification, renderNotificationStack, tickNotifications, trimNotificationsToViewport, } from './notification.js';
20
17
  import { isFrameScopedMsg, isPageScopedMsg, wrapCmdForPage, emitMsg, emitMsgForPage, wrapFrameMsg, } from './app-frame-types.js';
21
- import { createFrameKeyMap, frameBodyRect, mergeBindingSources, } from './app-frame-utils.js';
22
- import { activeFrameLayer, describeFrameLayerStack, describeFrameRuntimeViewStack, } from './app-frame-layers.js';
18
+ import { createFrameKeyMap, frameBodyRect, mergeBindingSources, resolveFramePageText, } 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';
20
+ import { activeFrameLayer, describeFrameRuntimeViewStack, projectFrameControls, } from './app-frame-layers.js';
23
21
  import { applyRuntimeCommandBuffer, bufferRuntimeRouteResult, createRuntimeBuffers, createRuntimeRetainedLayouts, retainRuntimeLayout, routeRuntimeInput, } from './runtime-engine.js';
24
- import { frameEndAnchor, frameMessage, frameNotificationCue, frameNotificationFilterLabel, frameStartAnchor, } from './app-frame-i18n.js';
22
+ import { frameMessage, frameNotificationFilterLabel, } from './app-frame-i18n.js';
25
23
  import { resolveHeaderLine, renderHelpLine, renderPageContent, renderPageContentInto, renderMaximizedPane, renderMaximizedPaneInto, renderTransition, createFramePaneScratchPool, } from './app-frame-render.js';
26
24
  import { applyFrameAction, scrollFocusedPane, switchTab, syncPageFrameState, } from './app-frame-actions.js';
27
25
  import { handlePaletteKey, openCommandPalette, openSearchPalette, } from './app-frame-palette.js';
28
- export { activeFrameLayer, describeFrameLayerStack, describeFrameRuntimeViewStack, underlyingFrameLayer, } from './app-frame-layers.js';
26
+ import { createInputActionMap, createInputGestureRecognizer, } from './input-map.js';
27
+ export { emitFrameAction, notify, } from './app-frame-types.js';
28
+ export { activeFrameLayer, describeFrameLayerStack, describeFrameRuntimeViewStack, projectFrameControls, underlyingFrameLayer, } from './app-frame-layers.js';
29
29
  // ---------------------------------------------------------------------------
30
30
  // Frame Notification Helpers
31
31
  // ---------------------------------------------------------------------------
32
32
  const FRAME_NOTIFICATION_TICK_MS = 40;
33
33
  const DEFAULT_FRAME_NOTIFICATION_DURATION_MS = 6_000;
34
34
  const SETTINGS_FEEDBACK_TOAST_WIDTH = 40;
35
+ const FRAME_FOOTER_DOUBLE_TAB_MS = 300;
35
36
  const EMPTY_RUNTIME_LAYOUTS = createRuntimeRetainedLayouts();
36
- const DEFAULT_NOTIFICATION_CENTER_FILTERS = [
37
- 'ALL',
38
- 'ACTIONABLE',
39
- 'ERROR',
40
- 'WARNING',
41
- 'SUCCESS',
42
- 'INFO',
43
- ];
44
- const quitHelpKeys = createKeyMap()
45
- .group('Exit', (g) => g
46
- .bind('q', 'Quit', { type: 'toggle-help' })
47
- .bind('escape', 'Quit', { type: 'toggle-help' })
48
- .bind('ctrl+c', 'Quit', { type: 'toggle-help' }));
49
- const helpLayerHelpKeys = createKeyMap()
50
- .group('Help', (g) => g
51
- .bind('escape', 'Close help', { type: 'noop' })
52
- .bind('?', 'Close help', { type: 'noop' })
53
- .bind('up', 'Scroll up', { type: 'noop' })
54
- .bind('down', 'Scroll down', { type: 'noop' })
55
- .bind('j', 'Scroll down', { type: 'noop' })
56
- .bind('k', 'Scroll up', { type: 'noop' })
57
- .bind('d', 'Page down', { type: 'noop' })
58
- .bind('u', 'Page up', { type: 'noop' })
59
- .bind('g', 'Top', { type: 'noop' })
60
- .bind('shift+g', 'Bottom', { type: 'noop' }));
61
- const settingsHelpKeys = createKeyMap()
62
- .group('Settings', (g) => g
63
- .bind('escape', 'Close settings', { type: 'toggle-settings' })
64
- .bind('f2', 'Close settings', { type: 'toggle-settings' })
65
- .bind('up', 'Previous row', { type: 'scroll-up' })
66
- .bind('down', 'Next row', { type: 'scroll-down' })
67
- .bind('enter', 'Activate setting', { type: 'toggle-settings' })
68
- .bind('space', 'Activate setting', { type: 'toggle-settings' })
69
- .bind('j', 'Scroll down', { type: 'scroll-down' })
70
- .bind('k', 'Scroll up', { type: 'scroll-up' })
71
- .bind('d', 'Page down', { type: 'page-down' })
72
- .bind('u', 'Page up', { type: 'page-up' })
73
- .bind('g', 'Top', { type: 'top' })
74
- .bind('shift+g', 'Bottom', { type: 'bottom' })
75
- .bind('/', 'Search', { type: 'open-search' })
76
- .bind('ctrl+p', 'Open command palette', { type: 'open-palette' })
77
- .bind(':', 'Open command palette', { type: 'open-palette' })
78
- .bind('?', 'Toggle help', { type: 'toggle-help' }));
79
- const notificationCenterHelpKeys = createKeyMap()
80
- .group('Notifications', (g) => g
81
- .bind('shift+n', 'Close notification center', { type: 'noop' })
82
- .bind('up', 'Scroll up', { type: 'noop' })
83
- .bind('down', 'Scroll down', { type: 'noop' })
84
- .bind('j', 'Scroll down', { type: 'noop' })
85
- .bind('k', 'Scroll up', { type: 'noop' })
86
- .bind('d', 'Page down', { type: 'noop' })
87
- .bind('u', 'Page up', { type: 'noop' })
88
- .bind('g', 'Top', { type: 'noop' })
89
- .bind('shift+g', 'Bottom', { type: 'noop' })
90
- .bind('f', 'Cycle filter', { type: 'noop' })
91
- .bind('/', 'Search', { type: 'noop' })
92
- .bind('ctrl+p', 'Open command palette', { type: 'noop' })
93
- .bind(':', 'Open command palette', { type: 'noop' })
94
- .bind('?', 'Toggle help', { type: 'noop' }));
95
- const quitConfirmHelpKeys = createKeyMap()
96
- .group('Quit', (g) => g
97
- .bind('y', 'Quit', { type: 'noop' })
98
- .bind('enter', 'Quit', { type: 'noop' })
99
- .bind('n', 'Stay', { type: 'noop' })
100
- .bind('escape', 'Stay', { type: 'noop' })
101
- .bind('q', 'Stay', { type: 'noop' }));
37
+ function createQuitHelpKeys(i18n) {
38
+ const t = (id, fallback) => frameMessage(i18n, id, fallback);
39
+ return createKeyMap()
40
+ .group(t('help.group.quit', 'Quit'), (g) => g
41
+ .bind('q', t('help.key.quit', 'Quit'), { type: 'toggle-help' })
42
+ .bind('escape', t('help.key.quit', 'Quit'), { type: 'toggle-help' })
43
+ .bind('ctrl+c', t('help.key.quit', 'Quit'), { type: 'toggle-help' }));
44
+ }
45
+ function createHelpLayerHelpKeys(i18n) {
46
+ const t = (id, fallback) => frameMessage(i18n, id, fallback);
47
+ return createKeyMap()
48
+ .group(t('help.group.help', 'Help'), (g) => g
49
+ .bind('escape', t('help.key.closeHelp', 'Close help'), { type: 'noop' })
50
+ .bind('?', t('help.key.closeHelp', 'Close help'), { type: 'noop' })
51
+ .bind('up', t('key.scrollUp', 'Scroll up'), { type: 'noop' })
52
+ .bind('down', t('key.scrollDown', 'Scroll down'), { type: 'noop' })
53
+ .bind('j', t('key.scrollDown', 'Scroll down'), { type: 'noop' })
54
+ .bind('k', t('key.scrollUp', 'Scroll up'), { type: 'noop' })
55
+ .bind('d', t('key.pageDown', 'Page down'), { type: 'noop' })
56
+ .bind('u', t('key.pageUp', 'Page up'), { type: 'noop' })
57
+ .bind('g', t('key.top', 'Top'), { type: 'noop' })
58
+ .bind('shift+g', t('key.bottom', 'Bottom'), { type: 'noop' }));
59
+ }
60
+ function createSettingsHelpKeys(i18n) {
61
+ const t = (id, fallback) => frameMessage(i18n, id, fallback);
62
+ return createKeyMap()
63
+ .group(t('help.group.settings', 'Settings'), (g) => g
64
+ .bind('escape', t('help.key.closeSettings', 'Close settings'), { type: 'toggle-settings' })
65
+ .bind('f2', t('help.key.closeSettings', 'Close settings'), { type: 'toggle-settings' })
66
+ .bind('up', t('help.key.previousRow', 'Previous row'), { type: 'scroll-up' })
67
+ .bind('down', t('help.key.nextRow', 'Next row'), { type: 'scroll-down' })
68
+ .bind('enter', t('help.key.activateSetting', 'Activate setting'), { type: 'toggle-settings' })
69
+ .bind('space', t('help.key.activateSetting', 'Activate setting'), { type: 'toggle-settings' })
70
+ .bind('j', t('key.scrollDown', 'Scroll down'), { type: 'scroll-down' })
71
+ .bind('k', t('key.scrollUp', 'Scroll up'), { type: 'scroll-up' })
72
+ .bind('d', t('key.pageDown', 'Page down'), { type: 'page-down' })
73
+ .bind('u', t('key.pageUp', 'Page up'), { type: 'page-up' })
74
+ .bind('g', t('key.top', 'Top'), { type: 'top' })
75
+ .bind('shift+g', t('key.bottom', 'Bottom'), { type: 'bottom' })
76
+ .bind('/', t('key.search', 'Search'), { type: 'open-search' })
77
+ .bind('ctrl+p', t('key.openPalette', 'Open command palette'), { type: 'open-palette' })
78
+ .bind(':', t('key.openPalette', 'Open command palette'), { type: 'open-palette' })
79
+ .bind('?', t('key.toggleHelp', 'Toggle help'), { type: 'toggle-help' }));
80
+ }
81
+ function createNotificationCenterHelpKeys(i18n) {
82
+ const t = (id, fallback) => frameMessage(i18n, id, fallback);
83
+ return createKeyMap()
84
+ .group(t('help.group.notifications', 'Notifications'), (g) => g
85
+ .bind('shift+n', t('help.key.closeNotifications', 'Close notification center'), { type: 'noop' })
86
+ .bind('up', t('key.scrollUp', 'Scroll up'), { type: 'noop' })
87
+ .bind('down', t('key.scrollDown', 'Scroll down'), { type: 'noop' })
88
+ .bind('j', t('key.scrollDown', 'Scroll down'), { type: 'noop' })
89
+ .bind('k', t('key.scrollUp', 'Scroll up'), { type: 'noop' })
90
+ .bind('d', t('key.pageDown', 'Page down'), { type: 'noop' })
91
+ .bind('u', t('key.pageUp', 'Page up'), { type: 'noop' })
92
+ .bind('g', t('key.top', 'Top'), { type: 'noop' })
93
+ .bind('shift+g', t('key.bottom', 'Bottom'), { type: 'noop' })
94
+ .bind('f', t('help.key.cycleFilter', 'Cycle filter'), { type: 'noop' })
95
+ .bind('/', t('key.search', 'Search'), { type: 'noop' })
96
+ .bind('ctrl+p', t('key.openPalette', 'Open command palette'), { type: 'noop' })
97
+ .bind(':', t('key.openPalette', 'Open command palette'), { type: 'noop' })
98
+ .bind('?', t('key.toggleHelp', 'Toggle help'), { type: 'noop' }));
99
+ }
100
+ function createQuitConfirmHelpKeys(i18n) {
101
+ const t = (id, fallback) => frameMessage(i18n, id, fallback);
102
+ return createKeyMap()
103
+ .group(t('help.group.quit', 'Quit'), (g) => g
104
+ .bind('y', t('help.key.quit', 'Quit'), { type: 'noop' })
105
+ .bind('enter', t('help.key.quit', 'Quit'), { type: 'noop' })
106
+ .bind('n', t('help.key.stay', 'Stay'), { type: 'noop' })
107
+ .bind('escape', t('help.key.stay', 'Stay'), { type: 'noop' })
108
+ .bind('q', t('help.key.stay', 'Stay'), { type: 'noop' }));
109
+ }
102
110
  function resolveFrameNotificationOptions(options) {
103
111
  if (options.runtimeNotifications === false) {
104
112
  return {
@@ -137,78 +145,45 @@ function createFrameNotificationTickCmd() {
137
145
  function cloneShellThemeContext(ctx, resolvedTheme) {
138
146
  return cloneContextWithResolvedTheme(ctx, resolvedTheme);
139
147
  }
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];
148
+ function readStageDuration(timings, stage) {
149
+ return timings.find((timing) => timing.stage === stage)?.durationMs ?? 0;
156
150
  }
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
- },
151
+ function summarizeFrameTimings(timings, frameBudgetMs) {
152
+ const frameTimeMs = timings.reduce((total, timing) => total + timing.durationMs, 0);
153
+ return {
154
+ frameTimeMs,
155
+ viewTimeMs: readStageDuration(timings, 'Layout'),
156
+ diffTimeMs: readStageDuration(timings, 'Diff'),
157
+ frameBudgetMs,
158
+ frameOverBudget: frameBudgetMs != null && frameTimeMs > frameBudgetMs,
182
159
  };
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
- };
160
+ }
161
+ function applyFrameTimingSnapshot(model, snapshot) {
162
+ if (model.frameTimeMs === snapshot.frameTimeMs
163
+ && model.viewTimeMs === snapshot.viewTimeMs
164
+ && model.diffTimeMs === snapshot.diffTimeMs
165
+ && model.frameBudgetMs === snapshot.frameBudgetMs
166
+ && model.frameOverBudget === snapshot.frameOverBudget) {
167
+ return model;
203
168
  }
204
169
  return {
205
- ...settings,
206
- sections: [
207
- { id: 'shell', title: shellSectionTitle, rows: [row] },
208
- ...settings.sections,
209
- ],
170
+ ...model,
171
+ frameTimeMs: snapshot.frameTimeMs,
172
+ viewTimeMs: snapshot.viewTimeMs,
173
+ diffTimeMs: snapshot.diffTimeMs,
174
+ frameBudgetMs: snapshot.frameBudgetMs,
175
+ frameOverBudget: snapshot.frameOverBudget,
210
176
  };
211
177
  }
178
+ function resolveFrameBudgetMs(runOptions, fallbackCtx) {
179
+ if (runOptions?.frameBudgetMs != null)
180
+ return runOptions.frameBudgetMs;
181
+ const refreshRate = runOptions?.ctx?.runtime.refreshRate ?? fallbackCtx?.runtime.refreshRate;
182
+ if (refreshRate == null || !Number.isFinite(refreshRate) || refreshRate <= 0) {
183
+ return undefined;
184
+ }
185
+ return 1_000 / refreshRate;
186
+ }
212
187
  // Factory
213
188
  // ---------------------------------------------------------------------------
214
189
  /**
@@ -230,28 +205,40 @@ export function createFramedApp(options) {
230
205
  if (!pagesById.has(defaultPageId)) {
231
206
  throw new Error(`createFramedApp: defaultPageId "${defaultPageId}" not found in pages`);
232
207
  }
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];
208
+ const shellThemeSpecs = options.shellThemes ?? [];
209
+ let defaultFrameCtx = options.ctx ?? resolveSafeCtx();
210
+ let resolvedShellThemes = [];
211
+ const enableShellThemeSettings = shellThemeSpecs.length > 1;
247
212
  const usesAmbientDefaultContext = options.ctx == null && defaultFrameCtx != null;
248
213
  let frameCtx = options.ctx;
249
- let frameCtxShellThemeId = resolveShellThemeForContext(resolvedShellThemes, frameCtx)?.id;
214
+ let frameCtxShellThemeId;
215
+ let useRunScopedFrameCtx = false;
216
+ function ensureResolvedShellThemes(explicitCtx) {
217
+ if (shellThemeSpecs.length === 0 || resolvedShellThemes.length > 0)
218
+ return;
219
+ const baseCtx = explicitCtx ?? frameCtx ?? options.ctx ?? resolveSafeCtx();
220
+ if (baseCtx == null) {
221
+ throw new Error('createFramedApp: shellThemes requires options.ctx, app.run({ ctx }), or a default Bijou context');
222
+ }
223
+ defaultFrameCtx ??= baseCtx;
224
+ resolvedShellThemes = shellThemeSpecs.map((theme) => ({
225
+ id: theme.id,
226
+ label: theme.label,
227
+ description: theme.description,
228
+ shellTheme: theme,
229
+ resolvedTheme: createResolved(theme.theme, defaultFrameCtx.theme.noColor, defaultFrameCtx.theme.colorScheme),
230
+ }));
231
+ }
232
+ if (frameCtx != null) {
233
+ ensureResolvedShellThemes(frameCtx);
234
+ frameCtxShellThemeId = resolveShellThemeForContext(resolvedShellThemes, frameCtx)?.id;
235
+ }
250
236
  function resolveFrameCtx() {
251
237
  return frameCtx ?? options.ctx ?? resolveSafeCtx();
252
238
  }
253
239
  function resolveFrameThemeCtx(activeShellThemeId) {
254
240
  const baseCtx = resolveFrameCtx();
241
+ ensureResolvedShellThemes(baseCtx);
255
242
  if (defaultFrameCtx == null)
256
243
  return baseCtx;
257
244
  const activeTheme = resolveCurrentShellTheme(resolvedShellThemes, activeShellThemeId);
@@ -266,11 +253,12 @@ export function createFramedApp(options) {
266
253
  return cloneShellThemeContext(defaultFrameCtx, activeTheme.resolvedTheme);
267
254
  }
268
255
  function publishShellThemeContext(nextTheme) {
256
+ ensureResolvedShellThemes(resolveFrameCtx());
269
257
  if (defaultFrameCtx == null)
270
258
  return resolveFrameCtx();
271
259
  frameCtx = cloneShellThemeContext(defaultFrameCtx, nextTheme.resolvedTheme);
272
260
  frameCtxShellThemeId = nextTheme.id;
273
- if (usesAmbientDefaultContext) {
261
+ if (usesAmbientDefaultContext && !useRunScopedFrameCtx) {
274
262
  setDefaultContext(frameCtx);
275
263
  }
276
264
  options.onShellThemeChange?.({
@@ -284,11 +272,24 @@ export function createFramedApp(options) {
284
272
  enableNotifications: options.notificationCenter != null || options.runtimeNotifications !== false,
285
273
  i18n: options.i18n,
286
274
  });
275
+ const frameInputRecognizer = createInputGestureRecognizer({
276
+ doubleTapMs: FRAME_FOOTER_DOUBLE_TAB_MS,
277
+ });
278
+ const frameInputActions = createInputActionMap()
279
+ .bind('frame.footer.doubleTab', 'Toggle footer', [
280
+ { deviceId: 'keyboard', featureId: 'key.tab', type: 'double-tap' },
281
+ ], { type: 'toggle-footer' });
282
+ const quitHelpKeys = createQuitHelpKeys(options.i18n);
283
+ const helpLayerHelpKeys = createHelpLayerHelpKeys(options.i18n);
284
+ const settingsHelpKeys = createSettingsHelpKeys(options.i18n);
285
+ const notificationCenterHelpKeys = createNotificationCenterHelpKeys(options.i18n);
286
+ const quitConfirmHelpKeys = createQuitConfirmHelpKeys(options.i18n);
287
287
  const frameNotificationOptions = resolveFrameNotificationOptions(options);
288
288
  let composedFrameScratch = null;
289
289
  let headerScratch;
290
290
  let helpLineScratch;
291
291
  const paneScratchPool = createFramePaneScratchPool();
292
+ let workspaceLayoutCache;
292
293
  const paletteKeys = commandPaletteKeyMap({
293
294
  focusNext: { type: 'cp-next' },
294
295
  focusPrev: { type: 'cp-prev' },
@@ -297,6 +298,49 @@ export function createFramedApp(options) {
297
298
  select: { type: 'cp-select' },
298
299
  close: { type: 'cp-close' },
299
300
  });
301
+ function bindingComboKey(binding) {
302
+ const combo = binding.combo;
303
+ return `${combo.key}|${combo.ctrl ? 1 : 0}|${combo.alt ? 1 : 0}|${combo.shift ? 1 : 0}`;
304
+ }
305
+ function findBindingForMessage(bindings, msg) {
306
+ const comboKey = `${msg.key}|${msg.ctrl ? 1 : 0}|${msg.alt ? 1 : 0}|${msg.shift ? 1 : 0}`;
307
+ return bindings.find((binding) => binding.enabled && bindingComboKey(binding) === comboKey);
308
+ }
309
+ function queueFrameKeyCollisionWarning(model, msg, teaCmds) {
310
+ if (!frameNotificationOptions.enabled)
311
+ return model;
312
+ if ((options.keyPriority ?? 'frame-first') !== 'frame-first')
313
+ return model;
314
+ if (model.warnedFrameKeyCollisionPages[model.activePageId])
315
+ return model;
316
+ const activePage = pagesById.get(model.activePageId);
317
+ if (activePage?.keyMap == null)
318
+ return model;
319
+ const pageBinding = findBindingForMessage(activePage.keyMap.bindings(), msg);
320
+ const frameBinding = findBindingForMessage(frameKeys.bindings(), msg);
321
+ if (pageBinding == null || frameBinding == null)
322
+ return model;
323
+ const warningCmd = async () => wrapFrameMsg({
324
+ type: 'runtime-issue',
325
+ issue: {
326
+ level: 'warning',
327
+ source: 'runtime',
328
+ message: `Page "${model.activePageId}" key binding ${formatKeyCombo(pageBinding.combo)} `
329
+ + `("${pageBinding.description}") is shadowed by the frame binding `
330
+ + `"${frameBinding.description}" under keyPriority="frame-first". `
331
+ + `Use keyPriority: 'page-first' or choose a different page binding.`,
332
+ atMs: resolveClock(resolveFrameCtx()).now(),
333
+ },
334
+ });
335
+ teaCmds.push(warningCmd);
336
+ return {
337
+ ...model,
338
+ warnedFrameKeyCollisionPages: {
339
+ ...model.warnedFrameKeyCollisionPages,
340
+ [model.activePageId]: true,
341
+ },
342
+ };
343
+ }
300
344
  function getComposedFrameScratch(width, height) {
301
345
  if (composedFrameScratch == null
302
346
  || composedFrameScratch.width !== width
@@ -305,6 +349,36 @@ export function createFramedApp(options) {
305
349
  }
306
350
  return composedFrameScratch;
307
351
  }
352
+ function renderFramePerfHudOverlay(model, ctx) {
353
+ const refreshRate = ctx?.runtime.refreshRate ?? 60;
354
+ const fps = model.frameTimeMs > 0
355
+ ? Math.min(refreshRate, 1000 / model.frameTimeMs)
356
+ : refreshRate;
357
+ const width = Math.max(12, Math.min(40, model.columns - 2));
358
+ const surface = perfOverlaySurface({
359
+ fps,
360
+ frameTimeMs: model.frameTimeMs,
361
+ width: model.columns,
362
+ height: model.rows,
363
+ extras: [
364
+ { label: 'view', value: `${model.viewTimeMs.toFixed(2)} ms` },
365
+ { label: 'diff', value: `${model.diffTimeMs.toFixed(2)} ms` },
366
+ ],
367
+ }, {
368
+ title: frameMessage(options.i18n, 'perfHud.title', 'Perf HUD'),
369
+ width,
370
+ showChart: false,
371
+ borderToken: ctx?.border('primary'),
372
+ bgToken: ctx?.surface('elevated'),
373
+ ctx,
374
+ });
375
+ return {
376
+ content: '',
377
+ surface,
378
+ row: 1,
379
+ col: Math.max(0, model.columns - surface.width - 1),
380
+ };
381
+ }
308
382
  function closeCommandPalette(model) {
309
383
  return {
310
384
  ...model,
@@ -402,11 +476,18 @@ export function createFramedApp(options) {
402
476
  teaCmds.push(...cmds);
403
477
  return nextModel;
404
478
  },
479
+ 'warn-frame-key-collision': (model, cmd, teaCmds) => {
480
+ const c = cmd;
481
+ return queueFrameKeyCollisionWarning(model, c.msg, teaCmds);
482
+ },
405
483
  // --- help ---
406
484
  'help-scroll': (model, cmd) => {
407
485
  const c = cmd;
408
- const activePage = pagesById.get(model.activePageId);
409
- const overlay = renderHelpOverlay(model, activePage, frameKeys, paletteKeys, options, pagesById, resolvedShellThemes);
486
+ const helpSource = resolvePresentedLayerContext(model).controlProjection.helpSource;
487
+ if (helpSource == null) {
488
+ return model;
489
+ }
490
+ const overlay = renderHelpOverlay(model, helpSource, options.i18n);
410
491
  const viewportHeight = Math.max(1, overlay.body.height - 1);
411
492
  const delta = c.action === 'down' ? 3
412
493
  : c.action === 'up' ? -3
@@ -511,6 +592,23 @@ export function createFramedApp(options) {
511
592
  }
512
593
  return [{ type: 'observed-key', msg, route }, { type: 'open-quit-confirm' }];
513
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
+ }
514
612
  function resolveFrameActionCommands(msg, action, route) {
515
613
  if (action.type === 'open-search' && options.enableCommandPalette) {
516
614
  return [{ type: 'observed-key', msg, route }, { type: 'open-search-palette' }];
@@ -542,6 +640,9 @@ export function createFramedApp(options) {
542
640
  if (frameAction?.type === 'toggle-notifications') {
543
641
  return [obs, { type: 'close-palette' }, { type: 'apply-frame-action', action: frameAction }];
544
642
  }
643
+ if (frameAction?.type === 'toggle-perf-hud') {
644
+ return [obs, { type: 'apply-frame-action', action: frameAction }];
645
+ }
545
646
  return [obs, { type: 'palette-key', msg }];
546
647
  }
547
648
  function handleHelpLayerKeyCommands(msg) {
@@ -553,6 +654,9 @@ export function createFramedApp(options) {
553
654
  return quitRequestCommands(msg, 'help');
554
655
  }
555
656
  const helpAction = frameKeys.handle(msg);
657
+ if (helpAction?.type === 'toggle-perf-hud') {
658
+ return [obs, { type: 'apply-frame-action', action: helpAction }];
659
+ }
556
660
  if (helpAction && isHelpScrollAction(helpAction)) {
557
661
  const action = helpAction.type === 'scroll-down' ? 'down'
558
662
  : helpAction.type === 'scroll-up' ? 'up'
@@ -591,6 +695,9 @@ export function createFramedApp(options) {
591
695
  if (settingsFrameAction?.type === 'toggle-notifications') {
592
696
  return [obs, { type: 'apply-frame-action', action: settingsFrameAction }];
593
697
  }
698
+ if (settingsFrameAction?.type === 'toggle-perf-hud') {
699
+ return [obs, { type: 'apply-frame-action', action: settingsFrameAction }];
700
+ }
594
701
  if (!msg.ctrl && !msg.alt && msg.key === 'up') {
595
702
  return [obs, { type: 'settings-focus-move', delta: -1 }];
596
703
  }
@@ -643,6 +750,9 @@ export function createFramedApp(options) {
643
750
  if (centerFrameAction?.type === 'toggle-notifications') {
644
751
  return [obs, { type: 'apply-frame-action', action: centerFrameAction }];
645
752
  }
753
+ if (centerFrameAction?.type === 'toggle-perf-hud') {
754
+ return [obs, { type: 'apply-frame-action', action: centerFrameAction }];
755
+ }
646
756
  if (!msg.ctrl && !msg.alt && msg.key === 'f2') {
647
757
  return [obs, { type: 'close-notification-center' }, { type: 'apply-frame-action', action: { type: 'toggle-settings' } }];
648
758
  }
@@ -706,7 +816,9 @@ export function createFramedApp(options) {
706
816
  }
707
817
  // frame-first (default)
708
818
  if (frameAction !== undefined) {
709
- return resolveFrameActionCommands(msg, frameAction, 'frame');
819
+ return pageAction !== undefined
820
+ ? [...resolveFrameActionCommands(msg, frameAction, 'frame'), { type: 'warn-frame-key-collision', msg }]
821
+ : resolveFrameActionCommands(msg, frameAction, 'frame');
710
822
  }
711
823
  if (paneAction !== undefined) {
712
824
  return [{ type: 'observed-key', msg, route: 'page' }, { type: 'emit-page-msg', pageId: model.activePageId, msg: paneAction }];
@@ -728,6 +840,10 @@ export function createFramedApp(options) {
728
840
  const frameLayer = layer.model;
729
841
  if (frameLayer == null)
730
842
  return undefined;
843
+ const inputActionCommands = resolveFrameInputActionCommands(msg, routeForFrameInputAction(frameLayer.kind));
844
+ if (inputActionCommands != null) {
845
+ return { handled: true, commands: inputActionCommands };
846
+ }
731
847
  if (frameLayer.kind === 'search' || frameLayer.kind === 'command-palette') {
732
848
  return { handled: true, commands: handlePaletteLayerKeyCommands(msg, frameLayer.kind) };
733
849
  }
@@ -744,6 +860,10 @@ export function createFramedApp(options) {
744
860
  }
745
861
  if (frameLayer.kind === 'quit-confirm') {
746
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
+ }
747
867
  if (isShellQuitConfirmAccept(msg)) {
748
868
  return { handled: true, commands: [obs, { type: 'close-quit-confirm' }, { type: 'quit' }] };
749
869
  }
@@ -755,6 +875,10 @@ export function createFramedApp(options) {
755
875
  if (frameLayer.kind === 'page-modal') {
756
876
  const { modalKeyMap } = context;
757
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
+ }
758
882
  if (modalKeyMap != null) {
759
883
  const modalAction = modalKeyMap.handle(msg);
760
884
  if (modalAction !== undefined) {
@@ -779,27 +903,15 @@ export function createFramedApp(options) {
779
903
  children: children ?? [],
780
904
  };
781
905
  }
782
- function resolveWorkspacePaneRects(model) {
783
- const bodyRect = resolveBodyRect(model, options);
784
- const maxState = model.maximizedPaneByPage[model.activePageId];
785
- const maximizedPaneId = maxState?.maximizedPaneId;
786
- const themedFrameCtx = resolveFrameThemeCtx(model.activeShellThemeId);
787
- const renderResult = maximizedPaneId
788
- ? renderMaximizedPane(model.activePageId, model, bodyRect, pagesById, maximizedPaneId, paneScratchPool, themedFrameCtx)
789
- : renderPageContent(model.activePageId, model, bodyRect, pagesById, themedFrameCtx);
790
- return renderResult.paneRects;
791
- }
792
- function buildWorkspaceLayoutTree(model) {
793
- const header = resolveHeaderLine(model, options, pagesById, headerScratch, resolveFrameThemeCtx(model.activeShellThemeId));
794
- headerScratch = header.surface;
795
- const tabChildren = header.tabTargets.map((target) => createShellRetainedLayoutNode(`tab:${target.pageId}`, {
906
+ function buildWorkspaceLayoutTreeFromPaneRects(model, paneRects, tabTargets) {
907
+ const resolvedTabTargets = tabTargets ?? resolveHeaderLine(model, options, pagesById, headerScratch, resolveFrameThemeCtx(model.activeShellThemeId)).tabTargets;
908
+ const tabChildren = resolvedTabTargets.map((target) => createShellRetainedLayoutNode(`tab:${target.pageId}`, {
796
909
  row: 0,
797
910
  col: target.startCol,
798
911
  width: target.endCol - target.startCol + 1,
799
912
  height: 1,
800
913
  }));
801
914
  const bodyRect = resolveBodyRect(model, options);
802
- const paneRects = resolveWorkspacePaneRects(model);
803
915
  const paneChildren = [];
804
916
  for (const [paneId, rect] of paneRects.entries()) {
805
917
  paneChildren.push(createShellRetainedLayoutNode(`pane:${paneId}`, rect));
@@ -809,6 +921,55 @@ export function createFramedApp(options) {
809
921
  createShellRetainedLayoutNode('workspace-body', bodyRect, paneChildren),
810
922
  ]);
811
923
  }
924
+ function rememberWorkspaceLayout(model, paneRects, tabTargets) {
925
+ const activePageId = model.activePageId;
926
+ const next = {
927
+ activePageId,
928
+ activePageModel: model.pageModels[activePageId],
929
+ columns: model.columns,
930
+ rows: model.rows,
931
+ visibilityState: model.minimizedByPage[activePageId],
932
+ dockState: model.dockStateByPage[activePageId],
933
+ splitRatioOverrides: model.splitRatioOverrides,
934
+ maximizedPaneId: model.maximizedPaneByPage[activePageId]?.maximizedPaneId,
935
+ paneRects,
936
+ tree: buildWorkspaceLayoutTreeFromPaneRects(model, paneRects, tabTargets),
937
+ };
938
+ workspaceLayoutCache = next;
939
+ return next;
940
+ }
941
+ function matchesWorkspaceLayoutCache(model) {
942
+ if (workspaceLayoutCache == null)
943
+ return false;
944
+ const activePageId = model.activePageId;
945
+ return workspaceLayoutCache.activePageId === activePageId
946
+ && workspaceLayoutCache.activePageModel === model.pageModels[activePageId]
947
+ && workspaceLayoutCache.columns === model.columns
948
+ && workspaceLayoutCache.rows === model.rows
949
+ && workspaceLayoutCache.visibilityState === model.minimizedByPage[activePageId]
950
+ && workspaceLayoutCache.dockState === model.dockStateByPage[activePageId]
951
+ && workspaceLayoutCache.splitRatioOverrides === model.splitRatioOverrides
952
+ && workspaceLayoutCache.maximizedPaneId === model.maximizedPaneByPage[activePageId]?.maximizedPaneId;
953
+ }
954
+ function resolveWorkspaceLayout(model) {
955
+ if (matchesWorkspaceLayoutCache(model)) {
956
+ return workspaceLayoutCache;
957
+ }
958
+ const bodyRect = resolveBodyRect(model, options);
959
+ const maxState = model.maximizedPaneByPage[model.activePageId];
960
+ const maximizedPaneId = maxState?.maximizedPaneId;
961
+ const themedFrameCtx = resolveFrameThemeCtx(model.activeShellThemeId);
962
+ const renderResult = maximizedPaneId
963
+ ? renderMaximizedPane(model.activePageId, model, bodyRect, pagesById, maximizedPaneId, paneScratchPool, themedFrameCtx)
964
+ : renderPageContent(model.activePageId, model, bodyRect, pagesById, themedFrameCtx);
965
+ return rememberWorkspaceLayout(model, renderResult.paneRects);
966
+ }
967
+ function resolveWorkspacePaneRects(model) {
968
+ return resolveWorkspaceLayout(model).paneRects;
969
+ }
970
+ function buildWorkspaceLayoutTree(model) {
971
+ return resolveWorkspaceLayout(model).tree;
972
+ }
812
973
  function buildSettingsRowChildren(model, layout) {
813
974
  const scrollY = clampSettingsScroll(model, layout);
814
975
  const viewportTop = 1;
@@ -1008,7 +1169,7 @@ export function createFramedApp(options) {
1008
1169
  }
1009
1170
  return helpLineOverride ?? mergeBindingSources(frameKeys, options.globalKeys, activeInputArea?.helpSource ?? activeInputArea?.keyMap, activePage.helpSource ?? activePage.keyMap);
1010
1171
  }
1011
- function resolveLayerMetadata(model, activePage, activeInputArea, modalKeyMap) {
1172
+ function resolveLayerMetadata(model, activePage, activePageModel, activeInputArea, modalKeyMap) {
1012
1173
  const settings = resolveFrameSettings(model, options, pagesById, resolvedShellThemes);
1013
1174
  const notificationCenter = resolveFrameNotificationCenter(model, options, pagesById);
1014
1175
  const workspaceHintSource = resolveWorkspaceHintSource(model, activePage, activeInputArea);
@@ -1020,23 +1181,29 @@ export function createFramedApp(options) {
1020
1181
  const quitHint = frameMessage(options.i18n, 'quit.footer', 'Y quit • N stay');
1021
1182
  const paletteTitle = model.commandPaletteTitle
1022
1183
  ?? frameMessage(options.i18n, 'palette.title', 'Command Palette');
1184
+ const activePageTitle = resolveFramePageText(activePage.title, activePageModel) ?? '';
1023
1185
  const searchTitle = model.commandPaletteTitle
1024
- ?? activePage.searchTitle
1186
+ ?? resolveFramePageText(activePage.searchTitle, activePageModel)
1025
1187
  ?? frameMessage(options.i18n, 'search.title', 'Search');
1026
1188
  const notificationsTitle = notificationCenter == null
1027
1189
  ? frameMessage(options.i18n, 'notifications.title', 'Notifications')
1028
1190
  : `${notificationCenter.title} • ${frameNotificationFilterLabel(options.i18n, notificationCenter.activeFilter)}`;
1191
+ const pageLayers = activePage.layers?.(activePageModel);
1192
+ const workspaceLayer = {
1193
+ title: activePageTitle,
1194
+ hintSource: workspaceHintSource,
1195
+ helpSource: workspaceHelpSource,
1196
+ ...pageLayers?.workspace,
1197
+ };
1198
+ const pageModalLayer = {
1199
+ title: activePageTitle,
1200
+ hintSource: modalKeyMap ?? activePage.helpSource ?? activePage.keyMap,
1201
+ helpSource: mergeBindingSources(quitHelpKeys, modalKeyMap, activePage.helpSource ?? activePage.keyMap),
1202
+ ...pageLayers?.['page-modal'],
1203
+ };
1029
1204
  return {
1030
- workspace: {
1031
- title: activePage.title,
1032
- hintSource: workspaceHintSource,
1033
- helpSource: workspaceHelpSource,
1034
- },
1035
- 'page-modal': {
1036
- title: activePage.title,
1037
- hintSource: modalKeyMap ?? activePage.helpSource ?? activePage.keyMap,
1038
- helpSource: mergeBindingSources(quitHelpKeys, modalKeyMap, activePage.helpSource ?? activePage.keyMap),
1039
- },
1205
+ workspace: workspaceLayer,
1206
+ 'page-modal': pageModalLayer,
1040
1207
  settings: {
1041
1208
  title: settings?.title ?? frameMessage(options.i18n, 'settings.title', 'Settings'),
1042
1209
  hintSource: settingsHint,
@@ -1071,12 +1238,11 @@ export function createFramedApp(options) {
1071
1238
  }
1072
1239
  function resolvePresentedLayerContext(model) {
1073
1240
  const { activePage, activePageModel, inputAreas, activeInputArea, modalKeyMap, pageModalOpen, } = resolveLayerContext(model);
1074
- const layerStack = describeFrameLayerStack(model, {
1241
+ const layerMetadata = resolveLayerMetadata(model, activePage, activePageModel, activeInputArea, modalKeyMap);
1242
+ const controlProjection = projectFrameControls(model, {
1075
1243
  pageModalOpen,
1076
- layers: resolveLayerMetadata(model, activePage, activeInputArea, modalKeyMap),
1244
+ layers: layerMetadata,
1077
1245
  });
1078
- const activeLayer = layerStack[layerStack.length - 1];
1079
- const underlyingLayer = layerStack.length > 1 ? layerStack[layerStack.length - 2] : undefined;
1080
1246
  return {
1081
1247
  activePage,
1082
1248
  activePageModel,
@@ -1084,9 +1250,11 @@ export function createFramedApp(options) {
1084
1250
  activeInputArea,
1085
1251
  modalKeyMap,
1086
1252
  pageModalOpen,
1087
- layerStack,
1088
- activeLayer,
1089
- underlyingLayer,
1253
+ layerMetadata,
1254
+ controlProjection,
1255
+ layerStack: controlProjection.layerStack,
1256
+ activeLayer: controlProjection.activeLayer,
1257
+ underlyingLayer: controlProjection.underlyingLayer,
1090
1258
  };
1091
1259
  }
1092
1260
  function updateTargetPage(model, targetPageId, targetMsg) {
@@ -1173,11 +1341,21 @@ export function createFramedApp(options) {
1173
1341
  activePageId: defaultPageId,
1174
1342
  pageOrder,
1175
1343
  pageModels,
1344
+ warnedFrameKeyCollisionPages: {},
1176
1345
  focusedPaneByPage: {},
1177
1346
  scrollByPage: {},
1178
1347
  columns: Math.max(1, options.initialColumns ?? 80),
1179
1348
  rows: Math.max(1, options.initialRows ?? 24),
1349
+ frameTimeMs: 0,
1350
+ viewTimeMs: 0,
1351
+ diffTimeMs: 0,
1352
+ frameBudgetMs: undefined,
1353
+ frameOverBudget: false,
1354
+ perfHudOpen: false,
1180
1355
  helpOpen: false,
1356
+ footerVisible: true,
1357
+ footerTranslateY: 0,
1358
+ footerAnimationGeneration: 0,
1181
1359
  helpScrollY: 0,
1182
1360
  commandPaletteKind: undefined,
1183
1361
  settingsOpen: false,
@@ -1196,6 +1374,13 @@ export function createFramedApp(options) {
1196
1374
  runtimeNotifications: createNotificationState(),
1197
1375
  runtimeNotificationHistoryFilter: 'ALL',
1198
1376
  runtimeNotificationLoopActive: false,
1377
+ activeShellThemeId: undefined,
1378
+ };
1379
+ ensureResolvedShellThemes(resolveFrameCtx());
1380
+ const initialShellTheme = resolveShellThemeForContext(resolvedShellThemes, resolveFrameCtx())
1381
+ ?? resolvedShellThemes[0];
1382
+ model = {
1383
+ ...model,
1199
1384
  activeShellThemeId: initialShellTheme?.id,
1200
1385
  };
1201
1386
  if (initialShellTheme != null) {
@@ -1236,6 +1421,20 @@ export function createFramedApp(options) {
1236
1421
  }, action.issue.atMs);
1237
1422
  return applyFrameNotificationState(model, notifications, action.issue.atMs);
1238
1423
  }
1424
+ if (action.type === 'push-notification') {
1425
+ if (!frameNotificationOptions.enabled)
1426
+ return [model, []];
1427
+ const nowMs = resolveClock(resolveFrameCtx()).now();
1428
+ const notifications = pushNotification(model.runtimeNotifications, {
1429
+ ...action.notification,
1430
+ placement: action.notification.placement ?? frameNotificationOptions.placement,
1431
+ durationMs: action.notification.durationMs === undefined
1432
+ ? frameNotificationOptions.durationMs
1433
+ : action.notification.durationMs,
1434
+ overflow: action.notification.overflow ?? frameNotificationOptions.overflow,
1435
+ }, nowMs);
1436
+ return applyFrameNotificationState(model, notifications, nowMs);
1437
+ }
1239
1438
  if (action.type === 'notification-tick') {
1240
1439
  const notifications = tickNotifications(model.runtimeNotifications, action.atMs);
1241
1440
  return applyFrameNotificationState(model, notifications, action.atMs, true);
@@ -1316,7 +1515,7 @@ export function createFramedApp(options) {
1316
1515
  },
1317
1516
  view(model) {
1318
1517
  const themedFrameCtx = resolveFrameThemeCtx(model.activeShellThemeId);
1319
- const { activePage, layerStack, activeLayer, } = resolvePresentedLayerContext(model);
1518
+ const { controlProjection, layerStack, activeLayer, } = resolvePresentedLayerContext(model);
1320
1519
  const headerResult = resolveHeaderLine(model, options, pagesById, headerScratch, themedFrameCtx);
1321
1520
  headerScratch = headerResult.surface;
1322
1521
  const header = headerResult.surface;
@@ -1332,7 +1531,12 @@ export function createFramedApp(options) {
1332
1531
  frameSurface.clear();
1333
1532
  frameSurface.blit(header, 0, 0);
1334
1533
  if (model.rows > 1) {
1335
- 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
+ }
1336
1540
  }
1337
1541
  let activeResult;
1338
1542
  let bodySurface;
@@ -1354,6 +1558,7 @@ export function createFramedApp(options) {
1354
1558
  ? renderMaximizedPaneInto(model.activePageId, model, bodyRect, pagesById, maximizedPaneId, frameSurface, bodyRect.row, bodyRect.col, paneScratchPool, themedFrameCtx)
1355
1559
  : renderPageContentInto(model.activePageId, model, bodyRect, pagesById, frameSurface, bodyRect.row, bodyRect.col, paneScratchPool, themedFrameCtx);
1356
1560
  }
1561
+ rememberWorkspaceLayout(model, activeResult.paneRects, headerResult.tabTargets);
1357
1562
  const overlays = [];
1358
1563
  if (options.overlayFactory != null) {
1359
1564
  overlays.push(...options.overlayFactory({
@@ -1388,7 +1593,11 @@ export function createFramedApp(options) {
1388
1593
  }
1389
1594
  }
1390
1595
  if (model.helpOpen) {
1391
- const helpOverlay = renderHelpOverlay(model, activePage, frameKeys, paletteKeys, options, pagesById, resolvedShellThemes);
1596
+ const helpSource = controlProjection.helpSource;
1597
+ if (helpSource == null) {
1598
+ throw new Error('createFramedApp: help layer projection is missing a help source');
1599
+ }
1600
+ const helpOverlay = renderHelpOverlay(model, helpSource, options.i18n);
1392
1601
  overlays.push(modal({
1393
1602
  title: activeLayer.kind === 'help'
1394
1603
  ? (activeLayer.title ?? frameMessage(options.i18n, 'help.title', 'Keyboard Help'))
@@ -1432,10 +1641,14 @@ export function createFramedApp(options) {
1432
1641
  if (model.quitConfirmOpen) {
1433
1642
  overlays.push(renderShellQuitOverlay(model.columns, model.rows, options.i18n, themedFrameCtx));
1434
1643
  }
1644
+ const dimBackground = overlays.length > 0;
1645
+ if (model.perfHudOpen) {
1646
+ overlays.push(renderFramePerfHudOverlay(model, themedFrameCtx));
1647
+ }
1435
1648
  if (bodySurface != null && bodyRect.width > 0 && bodyRect.height > 0) {
1436
1649
  frameSurface.blit(bodySurface, bodyRect.col, bodyRect.row);
1437
1650
  }
1438
- return compositeSurfaceInto(frameSurface, frameSurface, overlays, { dim: overlays.length > 0 });
1651
+ return compositeSurfaceInto(frameSurface, frameSurface, overlays, { dim: dimBackground });
1439
1652
  },
1440
1653
  routeRuntimeIssue(issue) {
1441
1654
  if (!frameNotificationOptions.enabled)
@@ -1443,7 +1656,67 @@ export function createFramedApp(options) {
1443
1656
  return wrapFrameMsg({ type: 'runtime-issue', issue });
1444
1657
  },
1445
1658
  };
1446
- return app;
1659
+ const framedApp = app;
1660
+ let hostedRunActive = false;
1661
+ framedApp.run = async (runOptions) => {
1662
+ if (hostedRunActive) {
1663
+ throw new Error('createFramedApp: concurrent app.run() calls on the same framed app are not supported');
1664
+ }
1665
+ hostedRunActive = true;
1666
+ const previousFrameCtx = frameCtx;
1667
+ const previousFrameCtxShellThemeId = frameCtxShellThemeId;
1668
+ const previousDefaultFrameCtx = defaultFrameCtx;
1669
+ const previousResolvedShellThemes = resolvedShellThemes;
1670
+ const runtimeCtx = runOptions?.ctx;
1671
+ if (runtimeCtx != null && options.ctx == null) {
1672
+ useRunScopedFrameCtx = true;
1673
+ frameCtx = runtimeCtx;
1674
+ defaultFrameCtx = runtimeCtx;
1675
+ resolvedShellThemes = [];
1676
+ ensureResolvedShellThemes(runtimeCtx);
1677
+ frameCtxShellThemeId = resolveShellThemeForContext(resolvedShellThemes, runtimeCtx)?.id;
1678
+ }
1679
+ const frameBudgetMs = resolveFrameBudgetMs(runOptions, resolveFrameCtx());
1680
+ let pendingTimingSnapshot;
1681
+ let needsTimingHydrationRender = true;
1682
+ try {
1683
+ await runWithLifecycleHooks(framedApp, {
1684
+ ...runOptions,
1685
+ mouse: runOptions?.mouse ?? true,
1686
+ }, {
1687
+ beforeRender(model) {
1688
+ if (pendingTimingSnapshot == null)
1689
+ return model;
1690
+ return applyFrameTimingSnapshot(model, pendingTimingSnapshot);
1691
+ },
1692
+ afterRender({ timings }) {
1693
+ pendingTimingSnapshot = summarizeFrameTimings(timings, frameBudgetMs);
1694
+ if (!needsTimingHydrationRender)
1695
+ return;
1696
+ needsTimingHydrationRender = false;
1697
+ return { requestRender: true };
1698
+ },
1699
+ });
1700
+ }
1701
+ finally {
1702
+ hostedRunActive = false;
1703
+ useRunScopedFrameCtx = false;
1704
+ frameCtx = previousFrameCtx;
1705
+ frameCtxShellThemeId = previousFrameCtxShellThemeId;
1706
+ defaultFrameCtx = previousDefaultFrameCtx;
1707
+ resolvedShellThemes = previousResolvedShellThemes;
1708
+ }
1709
+ };
1710
+ return framedApp;
1711
+ }
1712
+ /**
1713
+ * Create and immediately run a batteries-included framed shell.
1714
+ *
1715
+ * This is the one-call hosted path for users who want the frame to own the
1716
+ * runtime pump while `run(app)` remains the low-level TEA contract.
1717
+ */
1718
+ export async function runFramedApp(options, runOptions) {
1719
+ await createFramedApp(options).run(runOptions);
1447
1720
  }
1448
1721
  function focusPane(model, paneId) {
1449
1722
  if (model.focusedPaneByPage[model.activePageId] === paneId)
@@ -1459,431 +1732,4 @@ function focusPane(model, paneId) {
1459
1732
  function resolveBodyRect(model, options) {
1460
1733
  return frameBodyRect(model.columns, model.rows, options.bodyTopRows ?? 1, options.bodyBottomRows ?? 1);
1461
1734
  }
1462
- function renderHelpOverlay(model, activePage, frameKeys, paletteKeys, options, pagesById, shellThemes) {
1463
- const activePageModel = model.pageModels[model.activePageId];
1464
- const activeInputArea = findInputAreaByPaneId(resolveInputAreas(activePage, activePageModel), model.focusedPaneByPage[model.activePageId]);
1465
- const modalKeyMap = activePage.modalKeyMap?.(activePageModel);
1466
- const settings = resolveFrameSettings(model, options, pagesById, shellThemes);
1467
- const notificationCenter = resolveFrameNotificationCenter(model, options, pagesById);
1468
- const workspaceHintSource = options.helpLineSource?.({
1469
- model,
1470
- activePage,
1471
- frameKeys,
1472
- globalKeys: options.globalKeys,
1473
- });
1474
- const workspaceHelpSource = mergeBindingSources(frameKeys, quitHelpKeys, options.globalKeys, activeInputArea?.helpSource ?? activeInputArea?.keyMap, activePage.helpSource ?? activePage.keyMap);
1475
- const layerStack = describeFrameLayerStack(model, {
1476
- pageModalOpen: modalKeyMap != null,
1477
- layers: {
1478
- workspace: {
1479
- title: activePage.title,
1480
- hintSource: typeof workspaceHintSource === 'string'
1481
- ? workspaceHintSource
1482
- : workspaceHintSource ?? mergeBindingSources(frameKeys, options.globalKeys, activeInputArea?.helpSource ?? activeInputArea?.keyMap, activePage.helpSource ?? activePage.keyMap),
1483
- helpSource: workspaceHelpSource,
1484
- },
1485
- 'page-modal': {
1486
- title: activePage.title,
1487
- hintSource: modalKeyMap ?? activePage.helpSource ?? activePage.keyMap,
1488
- helpSource: mergeBindingSources(quitHelpKeys, modalKeyMap, activePage.helpSource ?? activePage.keyMap),
1489
- },
1490
- settings: {
1491
- title: settings?.title ?? frameMessage(options.i18n, 'settings.title', 'Settings'),
1492
- hintSource: frameMessage(options.i18n, 'settings.footer', 'F2/Esc close • ↑/↓ rows • Enter toggle • / search • q quit'),
1493
- helpSource: mergeBindingSources(settingsHelpKeys, quitHelpKeys),
1494
- },
1495
- help: {
1496
- title: frameMessage(options.i18n, 'help.title', 'Keyboard Help'),
1497
- hintSource: frameMessage(options.i18n, 'help.hint', 'j/k scroll • d/u page • g/G top/bottom • mouse wheel • ?/Esc close'),
1498
- helpSource: helpLayerHelpKeys,
1499
- },
1500
- 'notification-center': {
1501
- title: notificationCenter == null
1502
- ? frameMessage(options.i18n, 'notifications.title', 'Notifications')
1503
- : `${notificationCenter.title} • ${frameNotificationFilterLabel(options.i18n, notificationCenter.activeFilter)}`,
1504
- hintSource: frameMessage(options.i18n, 'notifications.footer', 'Shift+N close • f filter • j/k scroll • q quit'),
1505
- helpSource: mergeBindingSources(notificationCenterHelpKeys, quitHelpKeys),
1506
- },
1507
- search: {
1508
- title: model.commandPaletteTitle ?? activePage.searchTitle ?? frameMessage(options.i18n, 'search.title', 'Search'),
1509
- hintSource: frameMessage(options.i18n, 'palette.hint', 'Enter select • Esc close'),
1510
- helpSource: mergeBindingSources(paletteKeys, quitHelpKeys),
1511
- },
1512
- 'command-palette': {
1513
- title: model.commandPaletteTitle ?? frameMessage(options.i18n, 'palette.title', 'Command Palette'),
1514
- hintSource: frameMessage(options.i18n, 'palette.hint', 'Enter select • Esc close'),
1515
- helpSource: mergeBindingSources(paletteKeys, quitHelpKeys),
1516
- },
1517
- 'quit-confirm': {
1518
- title: frameMessage(options.i18n, 'quit.title', 'Quit?'),
1519
- hintSource: frameMessage(options.i18n, 'quit.footer', 'Y quit • N stay'),
1520
- helpSource: quitConfirmHelpKeys,
1521
- },
1522
- },
1523
- });
1524
- const beneathHelpLayer = layerStack.length > 1 ? layerStack[layerStack.length - 2] : undefined;
1525
- const source = beneathHelpLayer?.helpSource ?? workspaceHelpSource;
1526
- const maxDialogWidth = Math.max(28, Math.min(model.columns - 4, 88));
1527
- const bodyWidth = Math.max(20, maxDialogWidth - 4);
1528
- const helpSurface = helpViewSurface(source, {
1529
- title: undefined,
1530
- width: bodyWidth,
1531
- });
1532
- const pagerHeight = Math.max(4, Math.min(helpSurface.height + 1, Math.max(4, model.rows - 8)));
1533
- const pagerState = createPagerStateForSurface(helpSurface, {
1534
- width: bodyWidth,
1535
- height: pagerHeight,
1536
- });
1537
- const scrollY = Math.max(0, Math.min(model.helpScrollY, pagerState.scroll.maxY));
1538
- const scrolledState = {
1539
- ...pagerState,
1540
- scroll: {
1541
- ...pagerState.scroll,
1542
- y: scrollY,
1543
- },
1544
- };
1545
- return {
1546
- body: pagerSurface(helpSurface, scrolledState, { showScrollbar: true, showStatus: true }),
1547
- maxScrollY: pagerState.scroll.maxY,
1548
- scrollY,
1549
- };
1550
- }
1551
- function isHelpScrollAction(action) {
1552
- return action.type === 'scroll-up'
1553
- || action.type === 'scroll-down'
1554
- || action.type === 'page-up'
1555
- || action.type === 'page-down'
1556
- || action.type === 'top'
1557
- || action.type === 'bottom';
1558
- }
1559
- const FRAME_SHELL_THEME_ROW_ID = '__frame-shell-theme__';
1560
- function resolveFrameSettings(model, options, pagesById, shellThemes) {
1561
- const activePage = pagesById.get(model.activePageId);
1562
- const provided = options.settings?.({
1563
- model,
1564
- activePage,
1565
- pageModel: model.pageModels[model.activePageId],
1566
- });
1567
- return mergeShellThemeSettings(provided, shellThemes, model.activeShellThemeId, options.i18n);
1568
- }
1569
- function resolveFrameNotificationCenter(model, options, pagesById) {
1570
- const activePage = pagesById.get(model.activePageId);
1571
- const pageModel = model.pageModels[model.activePageId];
1572
- const provided = options.notificationCenter?.({
1573
- model,
1574
- activePage,
1575
- pageModel,
1576
- runtimeNotifications: model.runtimeNotifications,
1577
- });
1578
- if (provided != null) {
1579
- const filters = provided.filters != null && provided.filters.length > 0
1580
- ? provided.filters
1581
- : DEFAULT_NOTIFICATION_CENTER_FILTERS;
1582
- const activeFilter = filters.includes(provided.activeFilter ?? 'ALL')
1583
- ? (provided.activeFilter ?? 'ALL')
1584
- : filters[0];
1585
- return {
1586
- title: provided.title ?? frameMessage(options.i18n, 'notifications.title', 'Notifications'),
1587
- state: provided.state,
1588
- filters,
1589
- activeFilter,
1590
- onFilterChange: provided.onFilterChange,
1591
- };
1592
- }
1593
- if (options.runtimeNotifications === false)
1594
- return undefined;
1595
- return {
1596
- title: frameMessage(options.i18n, 'notifications.title', 'Notifications'),
1597
- state: model.runtimeNotifications,
1598
- filters: DEFAULT_NOTIFICATION_CENTER_FILTERS,
1599
- activeFilter: model.runtimeNotificationHistoryFilter,
1600
- };
1601
- }
1602
- function resolveSettingsLayout(model, options, pagesById, shellThemes) {
1603
- const settings = resolveFrameSettings(model, options, pagesById, shellThemes);
1604
- if (settings == null)
1605
- return undefined;
1606
- const sections = settings.sections.filter((section) => section.rows.length > 0);
1607
- if (sections.length === 0)
1608
- return undefined;
1609
- const drawerWidth = resolveSettingsDrawerWidth(model.columns);
1610
- const anchor = frameStartAnchor(options.i18n);
1611
- const startCol = anchor === 'left' ? 0 : Math.max(0, model.columns - drawerWidth);
1612
- const contentWidth = Math.max(16, drawerWidth - 4);
1613
- const preferenceSections = preparePreferenceSections(toPreferenceSections(sections));
1614
- const rows = [];
1615
- let line = 0;
1616
- for (let sectionIndex = 0; sectionIndex < preferenceSections.length; sectionIndex++) {
1617
- const section = preferenceSections[sectionIndex];
1618
- if (sectionIndex > 0) {
1619
- line += 1;
1620
- }
1621
- line += 1;
1622
- line += 1;
1623
- for (let rowIndex = 0; rowIndex < section.rows.length; rowIndex++) {
1624
- const preparedRow = section.rows[rowIndex];
1625
- const row = sections[sectionIndex].rows[rowIndex];
1626
- const rowLayout = resolvePreferenceRowLayout(preparedRow, contentWidth);
1627
- rows.push({
1628
- index: rows.length,
1629
- line,
1630
- height: rowLayout.height,
1631
- row,
1632
- behavior: row.id === FRAME_SHELL_THEME_ROW_ID ? 'cycle-shell-theme' : undefined,
1633
- });
1634
- line += rowLayout.height;
1635
- if (rowIndex < section.rows.length - 1) {
1636
- line += 1;
1637
- }
1638
- }
1639
- }
1640
- const contentHeight = Math.max(1, model.rows - 2);
1641
- const totalLines = Math.max(1, line);
1642
- const maxScrollY = Math.max(0, totalLines - contentHeight);
1643
- return {
1644
- settings: {
1645
- ...settings,
1646
- sections,
1647
- },
1648
- preferenceSections,
1649
- rows,
1650
- anchor,
1651
- startCol,
1652
- drawerWidth,
1653
- contentWidth,
1654
- contentHeight,
1655
- totalLines,
1656
- maxScrollY,
1657
- };
1658
- }
1659
- function resolveNotificationCenterDrawerWidth(columns) {
1660
- const boundedColumns = Math.max(28, columns);
1661
- return Math.min(Math.max(32, Math.floor(boundedColumns * 0.34)), Math.max(32, boundedColumns - 4), 52);
1662
- }
1663
- function resolveNotificationCenterLayout(model, options, pagesById, ctx) {
1664
- const center = resolveFrameNotificationCenter(model, options, pagesById);
1665
- if (center == null)
1666
- return undefined;
1667
- const drawerWidth = resolveNotificationCenterDrawerWidth(model.columns);
1668
- const anchor = frameEndAnchor(options.i18n);
1669
- const startCol = anchor === 'left' ? 0 : Math.max(0, model.columns - drawerWidth);
1670
- const contentWidth = Math.max(18, drawerWidth - 4);
1671
- const content = renderNotificationCenterSurface(center, contentWidth, options.i18n, ctx);
1672
- const contentHeight = Math.max(1, model.rows - 2);
1673
- const pagerState = createPagerStateForSurface(content, {
1674
- width: contentWidth,
1675
- height: contentHeight,
1676
- });
1677
- return {
1678
- center,
1679
- anchor,
1680
- startCol,
1681
- drawerWidth,
1682
- contentWidth,
1683
- contentHeight,
1684
- content,
1685
- maxScrollY: pagerState.scroll.maxY,
1686
- };
1687
- }
1688
- function resolveSettingsDrawerWidth(columns) {
1689
- const boundedColumns = Math.max(24, columns);
1690
- return Math.min(Math.max(28, Math.floor(boundedColumns * 0.3)), Math.max(28, boundedColumns - 4), 42);
1691
- }
1692
- function clampSettingsFocus(model, layout) {
1693
- if (layout.rows.length === 0)
1694
- return 0;
1695
- return Math.max(0, Math.min(model.settingsFocusIndex, layout.rows.length - 1));
1696
- }
1697
- function clampSettingsScroll(model, layout) {
1698
- return Math.max(0, Math.min(model.settingsScrollY, layout.maxScrollY));
1699
- }
1700
- function resolveInputAreas(page, pageModel) {
1701
- return page.inputAreas?.(pageModel) ?? [];
1702
- }
1703
- function findInputAreaByPaneId(inputAreas, paneId) {
1704
- if (paneId == null)
1705
- return undefined;
1706
- return inputAreas.find((area) => area.paneId === paneId);
1707
- }
1708
- function ensureSettingsRangeVisible(startLine, height, scrollY, visibleLines, maxScrollY) {
1709
- let next = scrollY;
1710
- const endLine = startLine + Math.max(1, height) - 1;
1711
- if (startLine < next) {
1712
- next = startLine;
1713
- }
1714
- else if (endLine >= next + visibleLines) {
1715
- next = endLine - visibleLines + 1;
1716
- }
1717
- return Math.max(0, Math.min(next, maxScrollY));
1718
- }
1719
- function moveSettingsFocus(model, layout, delta) {
1720
- if (layout.rows.length === 0)
1721
- return model;
1722
- const nextFocus = Math.max(0, Math.min(clampSettingsFocus(model, layout) + delta, layout.rows.length - 1));
1723
- const focusedRow = layout.rows[nextFocus];
1724
- return {
1725
- ...model,
1726
- settingsFocusIndex: nextFocus,
1727
- settingsScrollY: ensureSettingsRangeVisible(focusedRow.line, focusedRow.height, clampSettingsScroll(model, layout), layout.contentHeight, layout.maxScrollY),
1728
- };
1729
- }
1730
- function scrollSettingsBy(model, layout, delta) {
1731
- return {
1732
- ...model,
1733
- settingsScrollY: Math.max(0, Math.min(clampSettingsScroll(model, layout) + delta, layout.maxScrollY)),
1734
- };
1735
- }
1736
- function scrollNotificationCenterBy(model, layout, delta) {
1737
- return {
1738
- ...model,
1739
- notificationCenterScrollY: Math.max(0, Math.min(model.notificationCenterScrollY + delta, layout.maxScrollY)),
1740
- };
1741
- }
1742
- function cycleNotificationCenterFilter(model, layout) {
1743
- const filters = layout.center.filters;
1744
- if (filters.length < 2)
1745
- return [model, []];
1746
- const currentIndex = Math.max(0, filters.indexOf(layout.center.activeFilter));
1747
- const nextFilter = filters[(currentIndex + 1) % filters.length];
1748
- if (layout.center.onFilterChange != null) {
1749
- const action = layout.center.onFilterChange(nextFilter);
1750
- return [{
1751
- ...model,
1752
- notificationCenterScrollY: 0,
1753
- }, action === undefined ? [] : [emitMsgForPage(model.activePageId, action)]];
1754
- }
1755
- return [{
1756
- ...model,
1757
- runtimeNotificationHistoryFilter: nextFilter,
1758
- notificationCenterScrollY: 0,
1759
- }, []];
1760
- }
1761
- function renderSettingsDrawer(model, options, pagesById, shellThemes, titleOverride, ctx) {
1762
- const layout = resolveSettingsLayout(model, options, pagesById, shellThemes);
1763
- if (layout == null)
1764
- return undefined;
1765
- const scrollY = clampSettingsScroll(model, layout);
1766
- const content = renderSettingsSurface(layout, model, ctx);
1767
- const pagerState = createPagerStateForSurface(content, {
1768
- width: layout.contentWidth,
1769
- height: layout.contentHeight,
1770
- });
1771
- const scrolledState = {
1772
- ...pagerState,
1773
- scroll: {
1774
- ...pagerState.scroll,
1775
- y: scrollY,
1776
- },
1777
- };
1778
- const body = pagerSurface(content, scrolledState, {
1779
- showScrollbar: layout.maxScrollY > 0,
1780
- showStatus: false,
1781
- });
1782
- return drawer({
1783
- anchor: layout.anchor,
1784
- title: titleOverride ?? layout.settings.title ?? frameMessage(options.i18n, 'settings.title', 'Settings'),
1785
- content: body,
1786
- borderToken: layout.settings.borderToken,
1787
- bgToken: layout.settings.bgToken,
1788
- ctx,
1789
- width: layout.drawerWidth,
1790
- screenWidth: model.columns,
1791
- screenHeight: model.rows,
1792
- });
1793
- }
1794
- function renderNotificationCenterDrawer(model, options, pagesById, titleOverride, ctx) {
1795
- const layout = resolveNotificationCenterLayout(model, options, pagesById, ctx);
1796
- if (layout == null)
1797
- return undefined;
1798
- const pagerState = createPagerStateForSurface(layout.content, {
1799
- width: layout.contentWidth,
1800
- height: layout.contentHeight,
1801
- });
1802
- const scrolledState = {
1803
- ...pagerState,
1804
- scroll: {
1805
- ...pagerState.scroll,
1806
- y: Math.max(0, Math.min(model.notificationCenterScrollY, layout.maxScrollY)),
1807
- },
1808
- };
1809
- const body = pagerSurface(layout.content, scrolledState, {
1810
- showScrollbar: layout.maxScrollY > 0,
1811
- showStatus: false,
1812
- });
1813
- return drawer({
1814
- anchor: layout.anchor,
1815
- title: titleOverride ?? `${layout.center.title} • ${frameNotificationFilterLabel(options.i18n, layout.center.activeFilter)}`,
1816
- content: body,
1817
- borderToken: ctx?.border('primary'),
1818
- bgToken: ctx?.surface('elevated'),
1819
- ctx,
1820
- width: layout.drawerWidth,
1821
- screenWidth: model.columns,
1822
- screenHeight: model.rows,
1823
- });
1824
- }
1825
- function renderSettingsSurface(layout, model, ctx) {
1826
- const focusedIndex = clampSettingsFocus(model, layout);
1827
- return preferenceListSurface(layout.preferenceSections, {
1828
- width: layout.contentWidth,
1829
- selectedRowId: layout.rows[focusedIndex]?.row.id,
1830
- ctx,
1831
- theme: layout.settings.listTheme,
1832
- });
1833
- }
1834
- function toPreferenceSections(sections) {
1835
- return sections.map((section) => ({
1836
- id: section.id,
1837
- title: section.title,
1838
- rows: section.rows.map((row) => toPreferenceRow(row)),
1839
- }));
1840
- }
1841
- function toPreferenceRow(row) {
1842
- return {
1843
- id: row.id,
1844
- label: row.label,
1845
- description: row.description,
1846
- valueLabel: row.valueLabel,
1847
- kind: row.kind,
1848
- checked: row.checked,
1849
- enabled: row.enabled,
1850
- };
1851
- }
1852
- function resolveNotificationFooterCue(model, options, pagesById) {
1853
- const center = resolveFrameNotificationCenter(model, options, pagesById);
1854
- if (center == null)
1855
- return undefined;
1856
- const liveCount = center.state.items.length;
1857
- const archivedCount = countNotificationHistory(center.state, center.activeFilter);
1858
- return frameNotificationCue(options.i18n, liveCount, archivedCount);
1859
- }
1860
- function renderNotificationCenterSurface(center, width, i18n, ctx) {
1861
- const rows = [
1862
- insetLineSurface(`Live: ${center.state.items.length} • Archived: ${center.state.history.length}`, width),
1863
- insetLineSurface(`Filter: ${frameNotificationFilterLabel(i18n, center.activeFilter)}`, width),
1864
- ];
1865
- const liveItems = [...center.state.items].sort((left, right) => right.updatedAtMs - left.updatedAtMs || right.id - left.id);
1866
- if (liveItems.length > 0) {
1867
- rows.push(createSurface(width, 1));
1868
- rows.push(insetLineSurface(ctx == null ? 'Current stack' : ctx.style.bold('Current stack'), width));
1869
- rows.push(createSurface(width, 1));
1870
- for (let index = 0; index < liveItems.length; index++) {
1871
- rows.push(renderNotificationReviewEntrySurface(liveItems[index], {
1872
- width,
1873
- ctx,
1874
- metaLabel: `${liveItems[index].variant} • live`,
1875
- }));
1876
- if (index < liveItems.length - 1)
1877
- rows.push(createSurface(width, 1));
1878
- }
1879
- }
1880
- rows.push(createSurface(width, 1));
1881
- rows.push(renderNotificationHistorySurface(center.state, {
1882
- width,
1883
- height: Number.MAX_SAFE_INTEGER,
1884
- filter: center.activeFilter,
1885
- ctx,
1886
- }));
1887
- return vstackSurface(...rows);
1888
- }
1889
1735
  //# sourceMappingURL=app-frame.js.map