@flyingrobots/bijou-tui 4.4.0 → 5.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 (149) hide show
  1. package/README.md +102 -600
  2. package/dist/app-frame-actions.d.ts.map +1 -1
  3. package/dist/app-frame-actions.js +6 -1
  4. package/dist/app-frame-actions.js.map +1 -1
  5. package/dist/app-frame-i18n.d.ts.map +1 -1
  6. package/dist/app-frame-i18n.js +53 -0
  7. package/dist/app-frame-i18n.js.map +1 -1
  8. package/dist/app-frame-layers.d.ts +11 -1
  9. package/dist/app-frame-layers.d.ts.map +1 -1
  10. package/dist/app-frame-layers.js +19 -0
  11. package/dist/app-frame-layers.js.map +1 -1
  12. package/dist/app-frame-overlays.d.ts +82 -0
  13. package/dist/app-frame-overlays.d.ts.map +1 -0
  14. package/dist/app-frame-overlays.js +480 -0
  15. package/dist/app-frame-overlays.js.map +1 -0
  16. package/dist/app-frame-render.d.ts +8 -8
  17. package/dist/app-frame-render.d.ts.map +1 -1
  18. package/dist/app-frame-render.js +84 -25
  19. package/dist/app-frame-render.js.map +1 -1
  20. package/dist/app-frame-types.d.ts +122 -5
  21. package/dist/app-frame-types.d.ts.map +1 -1
  22. package/dist/app-frame-types.js +17 -1
  23. package/dist/app-frame-types.js.map +1 -1
  24. package/dist/app-frame-utils.d.ts.map +1 -1
  25. package/dist/app-frame-utils.js +6 -5
  26. package/dist/app-frame-utils.js.map +1 -1
  27. package/dist/app-frame.d.ts +43 -83
  28. package/dist/app-frame.d.ts.map +1 -1
  29. package/dist/app-frame.js +499 -575
  30. package/dist/app-frame.js.map +1 -1
  31. package/dist/browsable-list.d.ts +12 -0
  32. package/dist/browsable-list.d.ts.map +1 -1
  33. package/dist/browsable-list.js +17 -6
  34. package/dist/browsable-list.js.map +1 -1
  35. package/dist/canvas.d.ts.map +1 -1
  36. package/dist/canvas.js +27 -7
  37. package/dist/canvas.js.map +1 -1
  38. package/dist/collection-surface.d.ts +8 -0
  39. package/dist/collection-surface.d.ts.map +1 -1
  40. package/dist/collection-surface.js +72 -8
  41. package/dist/collection-surface.js.map +1 -1
  42. package/dist/command-palette.d.ts +5 -3
  43. package/dist/command-palette.d.ts.map +1 -1
  44. package/dist/command-palette.js +5 -3
  45. package/dist/command-palette.js.map +1 -1
  46. package/dist/css/text-style.d.ts +2 -0
  47. package/dist/css/text-style.d.ts.map +1 -1
  48. package/dist/css/text-style.js +18 -8
  49. package/dist/css/text-style.js.map +1 -1
  50. package/dist/debug-overlay.d.ts +19 -0
  51. package/dist/debug-overlay.d.ts.map +1 -0
  52. package/dist/debug-overlay.js +25 -0
  53. package/dist/debug-overlay.js.map +1 -0
  54. package/dist/design-language.d.ts.map +1 -1
  55. package/dist/design-language.js +4 -5
  56. package/dist/design-language.js.map +1 -1
  57. package/dist/driver.d.ts +102 -0
  58. package/dist/driver.d.ts.map +1 -1
  59. package/dist/driver.js +259 -19
  60. package/dist/driver.js.map +1 -1
  61. package/dist/file-picker.d.ts.map +1 -1
  62. package/dist/file-picker.js +2 -2
  63. package/dist/file-picker.js.map +1 -1
  64. package/dist/flex.d.ts.map +1 -1
  65. package/dist/flex.js +6 -6
  66. package/dist/flex.js.map +1 -1
  67. package/dist/focus-area.d.ts +9 -1
  68. package/dist/focus-area.d.ts.map +1 -1
  69. package/dist/focus-area.js +69 -40
  70. package/dist/focus-area.js.map +1 -1
  71. package/dist/grid.d.ts +2 -2
  72. package/dist/grid.d.ts.map +1 -1
  73. package/dist/grid.js +11 -148
  74. package/dist/grid.js.map +1 -1
  75. package/dist/help.d.ts +2 -0
  76. package/dist/help.d.ts.map +1 -1
  77. package/dist/help.js +6 -4
  78. package/dist/help.js.map +1 -1
  79. package/dist/icon-presentation.d.ts +10 -0
  80. package/dist/icon-presentation.d.ts.map +1 -0
  81. package/dist/icon-presentation.js +12 -0
  82. package/dist/icon-presentation.js.map +1 -0
  83. package/dist/index.d.ts +14 -8
  84. package/dist/index.d.ts.map +1 -1
  85. package/dist/index.js +7 -5
  86. package/dist/index.js.map +1 -1
  87. package/dist/layout-preset.d.ts +1 -1
  88. package/dist/layout-preset.d.ts.map +1 -1
  89. package/dist/motion/reconciler.d.ts +6 -2
  90. package/dist/motion/reconciler.d.ts.map +1 -1
  91. package/dist/motion/reconciler.js +40 -10
  92. package/dist/motion/reconciler.js.map +1 -1
  93. package/dist/motion/types.d.ts +3 -1
  94. package/dist/motion/types.d.ts.map +1 -1
  95. package/dist/navigable-table.d.ts +9 -10
  96. package/dist/navigable-table.d.ts.map +1 -1
  97. package/dist/navigable-table.js +33 -14
  98. package/dist/navigable-table.js.map +1 -1
  99. package/dist/notification.d.ts +15 -0
  100. package/dist/notification.d.ts.map +1 -1
  101. package/dist/notification.js +98 -29
  102. package/dist/notification.js.map +1 -1
  103. package/dist/overlay.d.ts.map +1 -1
  104. package/dist/overlay.js +50 -19
  105. package/dist/overlay.js.map +1 -1
  106. package/dist/pager.d.ts +3 -1
  107. package/dist/pager.d.ts.map +1 -1
  108. package/dist/pager.js +4 -0
  109. package/dist/pager.js.map +1 -1
  110. package/dist/pipeline/middleware/grayscale.d.ts.map +1 -1
  111. package/dist/pipeline/middleware/grayscale.js +5 -5
  112. package/dist/pipeline/middleware/grayscale.js.map +1 -1
  113. package/dist/pipeline/middleware/motion.js +2 -2
  114. package/dist/pipeline/middleware/motion.js.map +1 -1
  115. package/dist/pipeline/middleware/surface-shaders.d.ts +37 -0
  116. package/dist/pipeline/middleware/surface-shaders.d.ts.map +1 -0
  117. package/dist/pipeline/middleware/surface-shaders.js +164 -0
  118. package/dist/pipeline/middleware/surface-shaders.js.map +1 -0
  119. package/dist/pipeline/pipeline.d.ts +29 -1
  120. package/dist/pipeline/pipeline.d.ts.map +1 -1
  121. package/dist/pipeline/pipeline.js +86 -23
  122. package/dist/pipeline/pipeline.js.map +1 -1
  123. package/dist/runtime.d.ts +19 -0
  124. package/dist/runtime.d.ts.map +1 -1
  125. package/dist/runtime.js +183 -30
  126. package/dist/runtime.js.map +1 -1
  127. package/dist/shell-quit.d.ts +1 -1
  128. package/dist/shell-quit.d.ts.map +1 -1
  129. package/dist/shell-quit.js +14 -3
  130. package/dist/shell-quit.js.map +1 -1
  131. package/dist/split-pane.d.ts +2 -2
  132. package/dist/split-pane.d.ts.map +1 -1
  133. package/dist/split-pane.js +28 -49
  134. package/dist/split-pane.js.map +1 -1
  135. package/dist/subapp/mount.d.ts +17 -0
  136. package/dist/subapp/mount.d.ts.map +1 -1
  137. package/dist/subapp/mount.js +13 -0
  138. package/dist/subapp/mount.js.map +1 -1
  139. package/dist/surface-layout.d.ts +8 -3
  140. package/dist/surface-layout.d.ts.map +1 -1
  141. package/dist/surface-layout.js +27 -12
  142. package/dist/surface-layout.js.map +1 -1
  143. package/dist/view-output.js +2 -2
  144. package/dist/view-output.js.map +1 -1
  145. package/dist/viewport.d.ts +13 -1
  146. package/dist/viewport.d.ts.map +1 -1
  147. package/dist/viewport.js +33 -11
  148. package/dist/viewport.js.map +1 -1
  149. package/package.json +3 -3
package/dist/app-frame.js CHANGED
@@ -4,28 +4,27 @@
4
4
  * Provides tabs, pane focus/scroll isolation, shell key handling, help,
5
5
  * panel-scoped overlay context, and optional frame-level command palette.
6
6
  */
7
- import { createSurface, preparePreferenceSections, preferenceListSurface, resolvePreferenceRowLayout, resolveClock, resolveSafeCtx, } from '@flyingrobots/bijou';
8
- import { helpViewSurface } from './help.js';
9
- import { createKeyMap } from './keybindings.js';
7
+ import { cloneContextWithResolvedTheme, createResolved, createSurface, 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
- import { commandPalette, commandPaletteKeyMap, } from './command-palette.js';
15
- import { createPagerStateForSurface, pagerSurface, } from './pager.js';
14
+ import { commandPaletteSurface, commandPaletteKeyMap, } from './command-palette.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
18
  import { createFrameKeyMap, frameBodyRect, mergeBindingSources, } from './app-frame-utils.js';
22
- import { activeFrameLayer, describeFrameLayerStack, describeFrameRuntimeViewStack, } from './app-frame-layers.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
+ export { emitFrameAction, notify, } from './app-frame-types.js';
27
+ export { activeFrameLayer, describeFrameLayerStack, describeFrameRuntimeViewStack, projectFrameControls, underlyingFrameLayer, } from './app-frame-layers.js';
29
28
  // ---------------------------------------------------------------------------
30
29
  // Frame Notification Helpers
31
30
  // ---------------------------------------------------------------------------
@@ -33,72 +32,79 @@ const FRAME_NOTIFICATION_TICK_MS = 40;
33
32
  const DEFAULT_FRAME_NOTIFICATION_DURATION_MS = 6_000;
34
33
  const SETTINGS_FEEDBACK_TOAST_WIDTH = 40;
35
34
  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' }));
35
+ function createQuitHelpKeys(i18n) {
36
+ const t = (id, fallback) => frameMessage(i18n, id, fallback);
37
+ return createKeyMap()
38
+ .group(t('help.group.quit', 'Quit'), (g) => g
39
+ .bind('q', t('help.key.quit', 'Quit'), { type: 'toggle-help' })
40
+ .bind('escape', t('help.key.quit', 'Quit'), { type: 'toggle-help' })
41
+ .bind('ctrl+c', t('help.key.quit', 'Quit'), { type: 'toggle-help' }));
42
+ }
43
+ function createHelpLayerHelpKeys(i18n) {
44
+ const t = (id, fallback) => frameMessage(i18n, id, fallback);
45
+ return createKeyMap()
46
+ .group(t('help.group.help', 'Help'), (g) => g
47
+ .bind('escape', t('help.key.closeHelp', 'Close help'), { type: 'noop' })
48
+ .bind('?', t('help.key.closeHelp', 'Close help'), { type: 'noop' })
49
+ .bind('up', t('key.scrollUp', 'Scroll up'), { type: 'noop' })
50
+ .bind('down', t('key.scrollDown', 'Scroll down'), { type: 'noop' })
51
+ .bind('j', t('key.scrollDown', 'Scroll down'), { type: 'noop' })
52
+ .bind('k', t('key.scrollUp', 'Scroll up'), { type: 'noop' })
53
+ .bind('d', t('key.pageDown', 'Page down'), { type: 'noop' })
54
+ .bind('u', t('key.pageUp', 'Page up'), { type: 'noop' })
55
+ .bind('g', t('key.top', 'Top'), { type: 'noop' })
56
+ .bind('shift+g', t('key.bottom', 'Bottom'), { type: 'noop' }));
57
+ }
58
+ function createSettingsHelpKeys(i18n) {
59
+ const t = (id, fallback) => frameMessage(i18n, id, fallback);
60
+ return createKeyMap()
61
+ .group(t('help.group.settings', 'Settings'), (g) => g
62
+ .bind('escape', t('help.key.closeSettings', 'Close settings'), { type: 'toggle-settings' })
63
+ .bind('f2', t('help.key.closeSettings', 'Close settings'), { type: 'toggle-settings' })
64
+ .bind('up', t('help.key.previousRow', 'Previous row'), { type: 'scroll-up' })
65
+ .bind('down', t('help.key.nextRow', 'Next row'), { type: 'scroll-down' })
66
+ .bind('enter', t('help.key.activateSetting', 'Activate setting'), { type: 'toggle-settings' })
67
+ .bind('space', t('help.key.activateSetting', 'Activate setting'), { type: 'toggle-settings' })
68
+ .bind('j', t('key.scrollDown', 'Scroll down'), { type: 'scroll-down' })
69
+ .bind('k', t('key.scrollUp', 'Scroll up'), { type: 'scroll-up' })
70
+ .bind('d', t('key.pageDown', 'Page down'), { type: 'page-down' })
71
+ .bind('u', t('key.pageUp', 'Page up'), { type: 'page-up' })
72
+ .bind('g', t('key.top', 'Top'), { type: 'top' })
73
+ .bind('shift+g', t('key.bottom', 'Bottom'), { type: 'bottom' })
74
+ .bind('/', t('key.search', 'Search'), { type: 'open-search' })
75
+ .bind('ctrl+p', t('key.openPalette', 'Open command palette'), { type: 'open-palette' })
76
+ .bind(':', t('key.openPalette', 'Open command palette'), { type: 'open-palette' })
77
+ .bind('?', t('key.toggleHelp', 'Toggle help'), { type: 'toggle-help' }));
78
+ }
79
+ function createNotificationCenterHelpKeys(i18n) {
80
+ const t = (id, fallback) => frameMessage(i18n, id, fallback);
81
+ return createKeyMap()
82
+ .group(t('help.group.notifications', 'Notifications'), (g) => g
83
+ .bind('shift+n', t('help.key.closeNotifications', 'Close notification center'), { type: 'noop' })
84
+ .bind('up', t('key.scrollUp', 'Scroll up'), { type: 'noop' })
85
+ .bind('down', t('key.scrollDown', 'Scroll down'), { type: 'noop' })
86
+ .bind('j', t('key.scrollDown', 'Scroll down'), { type: 'noop' })
87
+ .bind('k', t('key.scrollUp', 'Scroll up'), { type: 'noop' })
88
+ .bind('d', t('key.pageDown', 'Page down'), { type: 'noop' })
89
+ .bind('u', t('key.pageUp', 'Page up'), { type: 'noop' })
90
+ .bind('g', t('key.top', 'Top'), { type: 'noop' })
91
+ .bind('shift+g', t('key.bottom', 'Bottom'), { type: 'noop' })
92
+ .bind('f', t('help.key.cycleFilter', 'Cycle filter'), { type: 'noop' })
93
+ .bind('/', t('key.search', 'Search'), { type: 'noop' })
94
+ .bind('ctrl+p', t('key.openPalette', 'Open command palette'), { type: 'noop' })
95
+ .bind(':', t('key.openPalette', 'Open command palette'), { type: 'noop' })
96
+ .bind('?', t('key.toggleHelp', 'Toggle help'), { type: 'noop' }));
97
+ }
98
+ function createQuitConfirmHelpKeys(i18n) {
99
+ const t = (id, fallback) => frameMessage(i18n, id, fallback);
100
+ return createKeyMap()
101
+ .group(t('help.group.quit', 'Quit'), (g) => g
102
+ .bind('y', t('help.key.quit', 'Quit'), { type: 'noop' })
103
+ .bind('enter', t('help.key.quit', 'Quit'), { type: 'noop' })
104
+ .bind('n', t('help.key.stay', 'Stay'), { type: 'noop' })
105
+ .bind('escape', t('help.key.stay', 'Stay'), { type: 'noop' })
106
+ .bind('q', t('help.key.stay', 'Stay'), { type: 'noop' }));
107
+ }
102
108
  function resolveFrameNotificationOptions(options) {
103
109
  if (options.runtimeNotifications === false) {
104
110
  return {
@@ -134,6 +140,48 @@ function createFrameNotificationTickCmd() {
134
140
  });
135
141
  };
136
142
  }
143
+ function cloneShellThemeContext(ctx, resolvedTheme) {
144
+ return cloneContextWithResolvedTheme(ctx, resolvedTheme);
145
+ }
146
+ function readStageDuration(timings, stage) {
147
+ return timings.find((timing) => timing.stage === stage)?.durationMs ?? 0;
148
+ }
149
+ function summarizeFrameTimings(timings, frameBudgetMs) {
150
+ const frameTimeMs = timings.reduce((total, timing) => total + timing.durationMs, 0);
151
+ return {
152
+ frameTimeMs,
153
+ viewTimeMs: readStageDuration(timings, 'Layout'),
154
+ diffTimeMs: readStageDuration(timings, 'Diff'),
155
+ frameBudgetMs,
156
+ frameOverBudget: frameBudgetMs != null && frameTimeMs > frameBudgetMs,
157
+ };
158
+ }
159
+ function applyFrameTimingSnapshot(model, snapshot) {
160
+ if (model.frameTimeMs === snapshot.frameTimeMs
161
+ && model.viewTimeMs === snapshot.viewTimeMs
162
+ && model.diffTimeMs === snapshot.diffTimeMs
163
+ && model.frameBudgetMs === snapshot.frameBudgetMs
164
+ && model.frameOverBudget === snapshot.frameOverBudget) {
165
+ return model;
166
+ }
167
+ return {
168
+ ...model,
169
+ frameTimeMs: snapshot.frameTimeMs,
170
+ viewTimeMs: snapshot.viewTimeMs,
171
+ diffTimeMs: snapshot.diffTimeMs,
172
+ frameBudgetMs: snapshot.frameBudgetMs,
173
+ frameOverBudget: snapshot.frameOverBudget,
174
+ };
175
+ }
176
+ function resolveFrameBudgetMs(runOptions, fallbackCtx) {
177
+ if (runOptions?.frameBudgetMs != null)
178
+ return runOptions.frameBudgetMs;
179
+ const refreshRate = runOptions?.ctx?.runtime.refreshRate ?? fallbackCtx?.runtime.refreshRate;
180
+ if (refreshRate == null || !Number.isFinite(refreshRate) || refreshRate <= 0) {
181
+ return undefined;
182
+ }
183
+ return 1_000 / refreshRate;
184
+ }
137
185
  // Factory
138
186
  // ---------------------------------------------------------------------------
139
187
  /**
@@ -155,16 +203,84 @@ export function createFramedApp(options) {
155
203
  if (!pagesById.has(defaultPageId)) {
156
204
  throw new Error(`createFramedApp: defaultPageId "${defaultPageId}" not found in pages`);
157
205
  }
206
+ const shellThemeSpecs = options.shellThemes ?? [];
207
+ let defaultFrameCtx = options.ctx ?? resolveSafeCtx();
208
+ let resolvedShellThemes = [];
209
+ const enableShellThemeSettings = shellThemeSpecs.length > 1;
210
+ const usesAmbientDefaultContext = options.ctx == null && defaultFrameCtx != null;
211
+ let frameCtx = options.ctx;
212
+ let frameCtxShellThemeId;
213
+ let useRunScopedFrameCtx = false;
214
+ function ensureResolvedShellThemes(explicitCtx) {
215
+ if (shellThemeSpecs.length === 0 || resolvedShellThemes.length > 0)
216
+ return;
217
+ const baseCtx = explicitCtx ?? frameCtx ?? options.ctx ?? resolveSafeCtx();
218
+ if (baseCtx == null) {
219
+ throw new Error('createFramedApp: shellThemes requires options.ctx, app.run({ ctx }), or a default Bijou context');
220
+ }
221
+ 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
+ }));
229
+ }
230
+ if (frameCtx != null) {
231
+ ensureResolvedShellThemes(frameCtx);
232
+ frameCtxShellThemeId = resolveShellThemeForContext(resolvedShellThemes, frameCtx)?.id;
233
+ }
234
+ function resolveFrameCtx() {
235
+ return frameCtx ?? options.ctx ?? resolveSafeCtx();
236
+ }
237
+ function resolveFrameThemeCtx(activeShellThemeId) {
238
+ const baseCtx = resolveFrameCtx();
239
+ ensureResolvedShellThemes(baseCtx);
240
+ if (defaultFrameCtx == null)
241
+ return baseCtx;
242
+ const activeTheme = resolveCurrentShellTheme(resolvedShellThemes, activeShellThemeId);
243
+ if (activeTheme == null)
244
+ return baseCtx;
245
+ if (frameCtx != null && frameCtxShellThemeId === activeTheme.id) {
246
+ return frameCtx;
247
+ }
248
+ if (resolveShellThemeForContext(resolvedShellThemes, baseCtx)?.id === activeTheme.id) {
249
+ return baseCtx;
250
+ }
251
+ return cloneShellThemeContext(defaultFrameCtx, activeTheme.resolvedTheme);
252
+ }
253
+ function publishShellThemeContext(nextTheme) {
254
+ ensureResolvedShellThemes(resolveFrameCtx());
255
+ if (defaultFrameCtx == null)
256
+ return resolveFrameCtx();
257
+ frameCtx = cloneShellThemeContext(defaultFrameCtx, nextTheme.resolvedTheme);
258
+ frameCtxShellThemeId = nextTheme.id;
259
+ if (usesAmbientDefaultContext && !useRunScopedFrameCtx) {
260
+ setDefaultContext(frameCtx);
261
+ }
262
+ options.onShellThemeChange?.({
263
+ shellTheme: nextTheme.shellTheme,
264
+ ctx: frameCtx,
265
+ });
266
+ return frameCtx;
267
+ }
158
268
  const frameKeys = createFrameKeyMap({
159
- enableSettings: options.settings != null,
269
+ enableSettings: options.settings != null || enableShellThemeSettings,
160
270
  enableNotifications: options.notificationCenter != null || options.runtimeNotifications !== false,
161
271
  i18n: options.i18n,
162
272
  });
273
+ const quitHelpKeys = createQuitHelpKeys(options.i18n);
274
+ const helpLayerHelpKeys = createHelpLayerHelpKeys(options.i18n);
275
+ const settingsHelpKeys = createSettingsHelpKeys(options.i18n);
276
+ const notificationCenterHelpKeys = createNotificationCenterHelpKeys(options.i18n);
277
+ const quitConfirmHelpKeys = createQuitConfirmHelpKeys(options.i18n);
163
278
  const frameNotificationOptions = resolveFrameNotificationOptions(options);
164
279
  let composedFrameScratch = null;
165
280
  let headerScratch;
166
281
  let helpLineScratch;
167
282
  const paneScratchPool = createFramePaneScratchPool();
283
+ let workspaceLayoutCache;
168
284
  const paletteKeys = commandPaletteKeyMap({
169
285
  focusNext: { type: 'cp-next' },
170
286
  focusPrev: { type: 'cp-prev' },
@@ -173,6 +289,49 @@ export function createFramedApp(options) {
173
289
  select: { type: 'cp-select' },
174
290
  close: { type: 'cp-close' },
175
291
  });
292
+ function bindingComboKey(binding) {
293
+ const combo = binding.combo;
294
+ return `${combo.key}|${combo.ctrl ? 1 : 0}|${combo.alt ? 1 : 0}|${combo.shift ? 1 : 0}`;
295
+ }
296
+ function findBindingForMessage(bindings, msg) {
297
+ const comboKey = `${msg.key}|${msg.ctrl ? 1 : 0}|${msg.alt ? 1 : 0}|${msg.shift ? 1 : 0}`;
298
+ return bindings.find((binding) => binding.enabled && bindingComboKey(binding) === comboKey);
299
+ }
300
+ function queueFrameKeyCollisionWarning(model, msg, teaCmds) {
301
+ if (!frameNotificationOptions.enabled)
302
+ return model;
303
+ if ((options.keyPriority ?? 'frame-first') !== 'frame-first')
304
+ return model;
305
+ if (model.warnedFrameKeyCollisionPages[model.activePageId])
306
+ return model;
307
+ const activePage = pagesById.get(model.activePageId);
308
+ if (activePage?.keyMap == null)
309
+ return model;
310
+ const pageBinding = findBindingForMessage(activePage.keyMap.bindings(), msg);
311
+ const frameBinding = findBindingForMessage(frameKeys.bindings(), msg);
312
+ if (pageBinding == null || frameBinding == null)
313
+ return model;
314
+ const warningCmd = async () => wrapFrameMsg({
315
+ type: 'runtime-issue',
316
+ issue: {
317
+ level: 'warning',
318
+ source: 'runtime',
319
+ message: `Page "${model.activePageId}" key binding ${formatKeyCombo(pageBinding.combo)} `
320
+ + `("${pageBinding.description}") is shadowed by the frame binding `
321
+ + `"${frameBinding.description}" under keyPriority="frame-first". `
322
+ + `Use keyPriority: 'page-first' or choose a different page binding.`,
323
+ atMs: resolveClock(resolveFrameCtx()).now(),
324
+ },
325
+ });
326
+ teaCmds.push(warningCmd);
327
+ return {
328
+ ...model,
329
+ warnedFrameKeyCollisionPages: {
330
+ ...model.warnedFrameKeyCollisionPages,
331
+ [model.activePageId]: true,
332
+ },
333
+ };
334
+ }
176
335
  function getComposedFrameScratch(width, height) {
177
336
  if (composedFrameScratch == null
178
337
  || composedFrameScratch.width !== width
@@ -221,30 +380,35 @@ export function createFramedApp(options) {
221
380
  // --- settings ---
222
381
  'settings-focus-move': (model, cmd) => {
223
382
  const c = cmd;
224
- const layout = resolveSettingsLayout(model, options, pagesById);
383
+ const layout = resolveSettingsLayout(model, options, pagesById, resolvedShellThemes);
225
384
  return layout != null ? moveSettingsFocus(model, layout, c.delta) : model;
226
385
  },
227
386
  'settings-scroll': (model, cmd) => {
228
387
  const c = cmd;
229
- const layout = resolveSettingsLayout(model, options, pagesById);
388
+ const layout = resolveSettingsLayout(model, options, pagesById, resolvedShellThemes);
230
389
  return layout != null ? scrollSettingsBy(model, layout, c.delta) : model;
231
390
  },
232
391
  'settings-scroll-to': (model, cmd) => {
233
392
  const c = cmd;
234
- const layout = resolveSettingsLayout(model, options, pagesById);
393
+ const layout = resolveSettingsLayout(model, options, pagesById, resolvedShellThemes);
235
394
  if (layout == null)
236
395
  return model;
237
396
  return { ...model, settingsScrollY: c.position === 'top' ? 0 : layout.maxScrollY };
238
397
  },
239
398
  'activate-settings-row': (model, cmd, teaCmds) => {
240
399
  const c = cmd;
241
- const layout = resolveSettingsLayout(model, options, pagesById);
400
+ const layout = resolveSettingsLayout(model, options, pagesById, resolvedShellThemes);
242
401
  if (layout == null)
243
402
  return model;
244
403
  const hitRow = layout.rows.find((r) => r.index === c.rowIndex);
245
404
  if (hitRow == null)
246
405
  return model;
247
406
  const focusedModel = { ...model, settingsFocusIndex: hitRow.index };
407
+ if (hitRow.behavior === 'cycle-shell-theme') {
408
+ const [nextModel, cmds] = cycleShellThemeSetting(focusedModel, hitRow.row);
409
+ teaCmds.push(...cmds);
410
+ return nextModel;
411
+ }
248
412
  if (hitRow.row.action === undefined || hitRow.row.enabled === false || hitRow.row.kind === 'info') {
249
413
  return focusedModel;
250
414
  }
@@ -255,29 +419,36 @@ export function createFramedApp(options) {
255
419
  // --- notification center ---
256
420
  'notification-center-scroll': (model, cmd) => {
257
421
  const c = cmd;
258
- const layout = resolveNotificationCenterLayout(model, options, pagesById);
422
+ const layout = resolveNotificationCenterLayout(model, options, pagesById, resolveFrameThemeCtx(model.activeShellThemeId));
259
423
  return layout != null ? scrollNotificationCenterBy(model, layout, c.delta) : model;
260
424
  },
261
425
  'notification-center-scroll-to': (model, cmd) => {
262
426
  const c = cmd;
263
- const layout = resolveNotificationCenterLayout(model, options, pagesById);
427
+ const layout = resolveNotificationCenterLayout(model, options, pagesById, resolveFrameThemeCtx(model.activeShellThemeId));
264
428
  if (layout == null)
265
429
  return model;
266
430
  return { ...model, notificationCenterScrollY: c.position === 'top' ? 0 : layout.maxScrollY };
267
431
  },
268
432
  'cycle-notification-filter': (model, _cmd, teaCmds) => {
269
- const layout = resolveNotificationCenterLayout(model, options, pagesById);
433
+ const layout = resolveNotificationCenterLayout(model, options, pagesById, resolveFrameThemeCtx(model.activeShellThemeId));
270
434
  if (layout == null)
271
435
  return model;
272
436
  const [nextModel, cmds] = cycleNotificationCenterFilter(model, layout);
273
437
  teaCmds.push(...cmds);
274
438
  return nextModel;
275
439
  },
440
+ 'warn-frame-key-collision': (model, cmd, teaCmds) => {
441
+ const c = cmd;
442
+ return queueFrameKeyCollisionWarning(model, c.msg, teaCmds);
443
+ },
276
444
  // --- help ---
277
445
  'help-scroll': (model, cmd) => {
278
446
  const c = cmd;
279
- const activePage = pagesById.get(model.activePageId);
280
- const overlay = renderHelpOverlay(model, activePage, frameKeys, paletteKeys, options, pagesById);
447
+ const helpSource = resolvePresentedLayerContext(model).controlProjection.helpSource;
448
+ if (helpSource == null) {
449
+ return model;
450
+ }
451
+ const overlay = renderHelpOverlay(model, helpSource, options.i18n);
281
452
  const viewportHeight = Math.max(1, overlay.body.height - 1);
282
453
  const delta = c.action === 'down' ? 3
283
454
  : c.action === 'up' ? -3
@@ -337,7 +508,7 @@ export function createFramedApp(options) {
337
508
  const c = cmd;
338
509
  if (!frameNotificationOptions.enabled)
339
510
  return model;
340
- const nowMs = resolveClock(resolveSafeCtx()).now();
511
+ const nowMs = resolveClock(resolveFrameCtx()).now();
341
512
  const [nextModel, cmds] = applyFrameNotificationState(model, dismissNotification(model.runtimeNotifications, c.notificationId, nowMs), nowMs);
342
513
  teaCmds.push(...cmds);
343
514
  return nextModel;
@@ -436,7 +607,7 @@ export function createFramedApp(options) {
436
607
  return [obs];
437
608
  }
438
609
  function handleSettingsLayerKeyCommands(msg, model) {
439
- const layout = resolveSettingsLayout(model, options, pagesById);
610
+ const layout = resolveSettingsLayout(model, options, pagesById, resolvedShellThemes);
440
611
  if (layout == null)
441
612
  return undefined;
442
613
  const obs = { type: 'observed-key', msg, route: 'frame' };
@@ -489,7 +660,10 @@ export function createFramedApp(options) {
489
660
  if (!msg.ctrl && !msg.alt && (msg.key === 'enter' || msg.key === 'space')) {
490
661
  const rowIndex = clampSettingsFocus(model, layout);
491
662
  const row = layout.rows[rowIndex];
492
- if (row?.row.action !== undefined && row.row.enabled !== false && row.row.kind !== 'info') {
663
+ if (row != null
664
+ && row.row.enabled !== false
665
+ && row.row.kind !== 'info'
666
+ && (row.behavior === 'cycle-shell-theme' || row.row.action !== undefined)) {
493
667
  return [obs, { type: 'activate-settings-row', rowIndex: row.index }];
494
668
  }
495
669
  return [obs];
@@ -497,7 +671,7 @@ export function createFramedApp(options) {
497
671
  return [obs];
498
672
  }
499
673
  function handleNotificationCenterLayerKeyCommands(msg, model) {
500
- const layout = resolveNotificationCenterLayout(model, options, pagesById);
674
+ const layout = resolveNotificationCenterLayout(model, options, pagesById, resolveFrameThemeCtx(model.activeShellThemeId));
501
675
  if (layout == null)
502
676
  return undefined;
503
677
  const obs = { type: 'observed-key', msg, route: 'frame' };
@@ -574,7 +748,9 @@ export function createFramedApp(options) {
574
748
  }
575
749
  // frame-first (default)
576
750
  if (frameAction !== undefined) {
577
- return resolveFrameActionCommands(msg, frameAction, 'frame');
751
+ return pageAction !== undefined
752
+ ? [...resolveFrameActionCommands(msg, frameAction, 'frame'), { type: 'warn-frame-key-collision', msg }]
753
+ : resolveFrameActionCommands(msg, frameAction, 'frame');
578
754
  }
579
755
  if (paneAction !== undefined) {
580
756
  return [{ type: 'observed-key', msg, route: 'page' }, { type: 'emit-page-msg', pageId: model.activePageId, msg: paneAction }];
@@ -647,26 +823,15 @@ export function createFramedApp(options) {
647
823
  children: children ?? [],
648
824
  };
649
825
  }
650
- function resolveWorkspacePaneRects(model) {
651
- const bodyRect = resolveBodyRect(model, options);
652
- const maxState = model.maximizedPaneByPage[model.activePageId];
653
- const maximizedPaneId = maxState?.maximizedPaneId;
654
- const renderResult = maximizedPaneId
655
- ? renderMaximizedPane(model.activePageId, model, bodyRect, pagesById, maximizedPaneId, paneScratchPool)
656
- : renderPageContent(model.activePageId, model, bodyRect, pagesById);
657
- return renderResult.paneRects;
658
- }
659
- function buildWorkspaceLayoutTree(model) {
660
- const header = resolveHeaderLine(model, options, pagesById, headerScratch);
661
- headerScratch = header.surface;
662
- const tabChildren = header.tabTargets.map((target) => createShellRetainedLayoutNode(`tab:${target.pageId}`, {
826
+ function buildWorkspaceLayoutTreeFromPaneRects(model, paneRects, tabTargets) {
827
+ const resolvedTabTargets = tabTargets ?? resolveHeaderLine(model, options, pagesById, headerScratch, resolveFrameThemeCtx(model.activeShellThemeId)).tabTargets;
828
+ const tabChildren = resolvedTabTargets.map((target) => createShellRetainedLayoutNode(`tab:${target.pageId}`, {
663
829
  row: 0,
664
830
  col: target.startCol,
665
831
  width: target.endCol - target.startCol + 1,
666
832
  height: 1,
667
833
  }));
668
834
  const bodyRect = resolveBodyRect(model, options);
669
- const paneRects = resolveWorkspacePaneRects(model);
670
835
  const paneChildren = [];
671
836
  for (const [paneId, rect] of paneRects.entries()) {
672
837
  paneChildren.push(createShellRetainedLayoutNode(`pane:${paneId}`, rect));
@@ -676,6 +841,55 @@ export function createFramedApp(options) {
676
841
  createShellRetainedLayoutNode('workspace-body', bodyRect, paneChildren),
677
842
  ]);
678
843
  }
844
+ function rememberWorkspaceLayout(model, paneRects, tabTargets) {
845
+ const activePageId = model.activePageId;
846
+ const next = {
847
+ activePageId,
848
+ activePageModel: model.pageModels[activePageId],
849
+ columns: model.columns,
850
+ rows: model.rows,
851
+ visibilityState: model.minimizedByPage[activePageId],
852
+ dockState: model.dockStateByPage[activePageId],
853
+ splitRatioOverrides: model.splitRatioOverrides,
854
+ maximizedPaneId: model.maximizedPaneByPage[activePageId]?.maximizedPaneId,
855
+ paneRects,
856
+ tree: buildWorkspaceLayoutTreeFromPaneRects(model, paneRects, tabTargets),
857
+ };
858
+ workspaceLayoutCache = next;
859
+ return next;
860
+ }
861
+ function matchesWorkspaceLayoutCache(model) {
862
+ if (workspaceLayoutCache == null)
863
+ return false;
864
+ const activePageId = model.activePageId;
865
+ return workspaceLayoutCache.activePageId === activePageId
866
+ && workspaceLayoutCache.activePageModel === model.pageModels[activePageId]
867
+ && workspaceLayoutCache.columns === model.columns
868
+ && workspaceLayoutCache.rows === model.rows
869
+ && workspaceLayoutCache.visibilityState === model.minimizedByPage[activePageId]
870
+ && workspaceLayoutCache.dockState === model.dockStateByPage[activePageId]
871
+ && workspaceLayoutCache.splitRatioOverrides === model.splitRatioOverrides
872
+ && workspaceLayoutCache.maximizedPaneId === model.maximizedPaneByPage[activePageId]?.maximizedPaneId;
873
+ }
874
+ function resolveWorkspaceLayout(model) {
875
+ if (matchesWorkspaceLayoutCache(model)) {
876
+ return workspaceLayoutCache;
877
+ }
878
+ const bodyRect = resolveBodyRect(model, options);
879
+ const maxState = model.maximizedPaneByPage[model.activePageId];
880
+ const maximizedPaneId = maxState?.maximizedPaneId;
881
+ const themedFrameCtx = resolveFrameThemeCtx(model.activeShellThemeId);
882
+ const renderResult = maximizedPaneId
883
+ ? renderMaximizedPane(model.activePageId, model, bodyRect, pagesById, maximizedPaneId, paneScratchPool, themedFrameCtx)
884
+ : renderPageContent(model.activePageId, model, bodyRect, pagesById, themedFrameCtx);
885
+ return rememberWorkspaceLayout(model, renderResult.paneRects);
886
+ }
887
+ function resolveWorkspacePaneRects(model) {
888
+ return resolveWorkspaceLayout(model).paneRects;
889
+ }
890
+ function buildWorkspaceLayoutTree(model) {
891
+ return resolveWorkspaceLayout(model).tree;
892
+ }
679
893
  function buildSettingsRowChildren(model, layout) {
680
894
  const scrollY = clampSettingsScroll(model, layout);
681
895
  const viewportTop = 1;
@@ -698,7 +912,9 @@ export function createFramedApp(options) {
698
912
  }
699
913
  function resolveFrameMouseRuntimeLayouts(model) {
700
914
  let layouts = EMPTY_RUNTIME_LAYOUTS;
701
- const settingsLayout = model.settingsOpen ? resolveSettingsLayout(model, options, pagesById) : undefined;
915
+ const settingsLayout = model.settingsOpen
916
+ ? resolveSettingsLayout(model, options, pagesById, resolvedShellThemes)
917
+ : undefined;
702
918
  if (settingsLayout != null) {
703
919
  layouts = retainRuntimeLayout(layouts, {
704
920
  viewId: 'settings',
@@ -710,7 +926,9 @@ export function createFramedApp(options) {
710
926
  }, buildSettingsRowChildren(model, settingsLayout)),
711
927
  });
712
928
  }
713
- const notificationCenterLayout = model.notificationCenterOpen ? resolveNotificationCenterLayout(model, options, pagesById) : undefined;
929
+ const notificationCenterLayout = model.notificationCenterOpen
930
+ ? resolveNotificationCenterLayout(model, options, pagesById, resolveFrameThemeCtx(model.activeShellThemeId))
931
+ : undefined;
714
932
  if (notificationCenterLayout != null) {
715
933
  layouts = retainRuntimeLayout(layouts, {
716
934
  viewId: 'notification-center',
@@ -790,7 +1008,7 @@ export function createFramedApp(options) {
790
1008
  screenHeight: model.rows,
791
1009
  margin: frameNotificationOptions.margin,
792
1010
  gap: frameNotificationOptions.gap,
793
- ctx: resolveSafeCtx() ?? undefined,
1011
+ ctx: resolveFrameThemeCtx(model.activeShellThemeId) ?? undefined,
794
1012
  }, msg.col, msg.row);
795
1013
  if (notificationTarget?.kind === 'dismiss') {
796
1014
  cmds.push({ type: 'dismiss-notification', notificationId: notificationTarget.item.id });
@@ -871,8 +1089,8 @@ export function createFramedApp(options) {
871
1089
  }
872
1090
  return helpLineOverride ?? mergeBindingSources(frameKeys, options.globalKeys, activeInputArea?.helpSource ?? activeInputArea?.keyMap, activePage.helpSource ?? activePage.keyMap);
873
1091
  }
874
- function resolveLayerMetadata(model, activePage, activeInputArea, modalKeyMap) {
875
- const settings = resolveFrameSettings(model, options, pagesById);
1092
+ function resolveLayerMetadata(model, activePage, activePageModel, activeInputArea, modalKeyMap) {
1093
+ const settings = resolveFrameSettings(model, options, pagesById, resolvedShellThemes);
876
1094
  const notificationCenter = resolveFrameNotificationCenter(model, options, pagesById);
877
1095
  const workspaceHintSource = resolveWorkspaceHintSource(model, activePage, activeInputArea);
878
1096
  const workspaceHelpSource = resolveWorkspaceHelpSource(activePage, activeInputArea);
@@ -889,17 +1107,22 @@ export function createFramedApp(options) {
889
1107
  const notificationsTitle = notificationCenter == null
890
1108
  ? frameMessage(options.i18n, 'notifications.title', 'Notifications')
891
1109
  : `${notificationCenter.title} • ${frameNotificationFilterLabel(options.i18n, notificationCenter.activeFilter)}`;
1110
+ const pageLayers = activePage.layers?.(activePageModel);
1111
+ const workspaceLayer = {
1112
+ title: activePage.title,
1113
+ hintSource: workspaceHintSource,
1114
+ helpSource: workspaceHelpSource,
1115
+ ...pageLayers?.workspace,
1116
+ };
1117
+ const pageModalLayer = {
1118
+ title: activePage.title,
1119
+ hintSource: modalKeyMap ?? activePage.helpSource ?? activePage.keyMap,
1120
+ helpSource: mergeBindingSources(quitHelpKeys, modalKeyMap, activePage.helpSource ?? activePage.keyMap),
1121
+ ...pageLayers?.['page-modal'],
1122
+ };
892
1123
  return {
893
- workspace: {
894
- title: activePage.title,
895
- hintSource: workspaceHintSource,
896
- helpSource: workspaceHelpSource,
897
- },
898
- 'page-modal': {
899
- title: activePage.title,
900
- hintSource: modalKeyMap ?? activePage.helpSource ?? activePage.keyMap,
901
- helpSource: mergeBindingSources(quitHelpKeys, modalKeyMap, activePage.helpSource ?? activePage.keyMap),
902
- },
1124
+ workspace: workspaceLayer,
1125
+ 'page-modal': pageModalLayer,
903
1126
  settings: {
904
1127
  title: settings?.title ?? frameMessage(options.i18n, 'settings.title', 'Settings'),
905
1128
  hintSource: settingsHint,
@@ -934,12 +1157,11 @@ export function createFramedApp(options) {
934
1157
  }
935
1158
  function resolvePresentedLayerContext(model) {
936
1159
  const { activePage, activePageModel, inputAreas, activeInputArea, modalKeyMap, pageModalOpen, } = resolveLayerContext(model);
937
- const layerStack = describeFrameLayerStack(model, {
1160
+ const layerMetadata = resolveLayerMetadata(model, activePage, activePageModel, activeInputArea, modalKeyMap);
1161
+ const controlProjection = projectFrameControls(model, {
938
1162
  pageModalOpen,
939
- layers: resolveLayerMetadata(model, activePage, activeInputArea, modalKeyMap),
1163
+ layers: layerMetadata,
940
1164
  });
941
- const activeLayer = layerStack[layerStack.length - 1];
942
- const underlyingLayer = layerStack.length > 1 ? layerStack[layerStack.length - 2] : undefined;
943
1165
  return {
944
1166
  activePage,
945
1167
  activePageModel,
@@ -947,9 +1169,11 @@ export function createFramedApp(options) {
947
1169
  activeInputArea,
948
1170
  modalKeyMap,
949
1171
  pageModalOpen,
950
- layerStack,
951
- activeLayer,
952
- underlyingLayer,
1172
+ layerMetadata,
1173
+ controlProjection,
1174
+ layerStack: controlProjection.layerStack,
1175
+ activeLayer: controlProjection.activeLayer,
1176
+ underlyingLayer: controlProjection.underlyingLayer,
953
1177
  };
954
1178
  }
955
1179
  function updateTargetPage(model, targetPageId, targetMsg) {
@@ -982,19 +1206,15 @@ export function createFramedApp(options) {
982
1206
  }
983
1207
  return [nextModel, []];
984
1208
  }
985
- function activateSettingsRow(model, row) {
986
- if (row.action === undefined || row.enabled === false || row.kind === 'info') {
987
- return [model, []];
988
- }
989
- const cmds = [emitMsgForPage(model.activePageId, row.action)];
1209
+ function pushSettingsFeedback(model, row) {
990
1210
  if (!frameNotificationOptions.enabled) {
991
- return [model, cmds];
1211
+ return [model, []];
992
1212
  }
993
1213
  const feedback = row.feedback ?? {
994
1214
  title: 'Setting updated',
995
1215
  message: `${row.label} updated.`,
996
1216
  };
997
- const nowMs = resolveClock(resolveSafeCtx()).now();
1217
+ const nowMs = resolveClock(resolveFrameCtx()).now();
998
1218
  const notifications = pushNotification(model.runtimeNotifications, {
999
1219
  title: feedback.title ?? 'Setting updated',
1000
1220
  message: feedback.message,
@@ -1005,9 +1225,28 @@ export function createFramedApp(options) {
1005
1225
  durationMs: feedback.durationMs ?? 2_500,
1006
1226
  overflow: frameNotificationOptions.overflow,
1007
1227
  }, nowMs);
1008
- const [nextModel, notificationCmds] = applyFrameNotificationState(model, notifications, nowMs);
1228
+ return applyFrameNotificationState(model, notifications, nowMs);
1229
+ }
1230
+ function activateSettingsRow(model, row) {
1231
+ if (row.action === undefined || row.enabled === false || row.kind === 'info') {
1232
+ return [model, []];
1233
+ }
1234
+ const cmds = [emitMsgForPage(model.activePageId, row.action)];
1235
+ const [nextModel, notificationCmds] = pushSettingsFeedback(model, row);
1009
1236
  return [nextModel, [...cmds, ...notificationCmds]];
1010
1237
  }
1238
+ function cycleShellThemeSetting(model, row) {
1239
+ const nextTheme = resolveNextShellTheme(resolvedShellThemes, model.activeShellThemeId);
1240
+ if (nextTheme == null) {
1241
+ return [model, []];
1242
+ }
1243
+ publishShellThemeContext(nextTheme);
1244
+ const [nextModel, notificationCmds] = pushSettingsFeedback({
1245
+ ...model,
1246
+ activeShellThemeId: nextTheme.id,
1247
+ }, row);
1248
+ return [nextModel, notificationCmds];
1249
+ }
1011
1250
  const app = {
1012
1251
  init() {
1013
1252
  const pageModels = {};
@@ -1021,10 +1260,16 @@ export function createFramedApp(options) {
1021
1260
  activePageId: defaultPageId,
1022
1261
  pageOrder,
1023
1262
  pageModels,
1263
+ warnedFrameKeyCollisionPages: {},
1024
1264
  focusedPaneByPage: {},
1025
1265
  scrollByPage: {},
1026
1266
  columns: Math.max(1, options.initialColumns ?? 80),
1027
1267
  rows: Math.max(1, options.initialRows ?? 24),
1268
+ frameTimeMs: 0,
1269
+ viewTimeMs: 0,
1270
+ diffTimeMs: 0,
1271
+ frameBudgetMs: undefined,
1272
+ frameOverBudget: false,
1028
1273
  helpOpen: false,
1029
1274
  helpScrollY: 0,
1030
1275
  commandPaletteKind: undefined,
@@ -1044,7 +1289,18 @@ export function createFramedApp(options) {
1044
1289
  runtimeNotifications: createNotificationState(),
1045
1290
  runtimeNotificationHistoryFilter: 'ALL',
1046
1291
  runtimeNotificationLoopActive: false,
1292
+ activeShellThemeId: undefined,
1047
1293
  };
1294
+ ensureResolvedShellThemes(resolveFrameCtx());
1295
+ const initialShellTheme = resolveShellThemeForContext(resolvedShellThemes, resolveFrameCtx())
1296
+ ?? resolvedShellThemes[0];
1297
+ model = {
1298
+ ...model,
1299
+ activeShellThemeId: initialShellTheme?.id,
1300
+ };
1301
+ if (initialShellTheme != null) {
1302
+ publishShellThemeContext(initialShellTheme);
1303
+ }
1048
1304
  for (const pageId of pageOrder) {
1049
1305
  model = syncPageFrameState(model, pageId, pagesById);
1050
1306
  }
@@ -1080,6 +1336,20 @@ export function createFramedApp(options) {
1080
1336
  }, action.issue.atMs);
1081
1337
  return applyFrameNotificationState(model, notifications, action.issue.atMs);
1082
1338
  }
1339
+ if (action.type === 'push-notification') {
1340
+ if (!frameNotificationOptions.enabled)
1341
+ return [model, []];
1342
+ const nowMs = resolveClock(resolveFrameCtx()).now();
1343
+ const notifications = pushNotification(model.runtimeNotifications, {
1344
+ ...action.notification,
1345
+ placement: action.notification.placement ?? frameNotificationOptions.placement,
1346
+ durationMs: action.notification.durationMs === undefined
1347
+ ? frameNotificationOptions.durationMs
1348
+ : action.notification.durationMs,
1349
+ overflow: action.notification.overflow ?? frameNotificationOptions.overflow,
1350
+ }, nowMs);
1351
+ return applyFrameNotificationState(model, notifications, nowMs);
1352
+ }
1083
1353
  if (action.type === 'notification-tick') {
1084
1354
  const notifications = tickNotifications(model.runtimeNotifications, action.atMs);
1085
1355
  return applyFrameNotificationState(model, notifications, action.atMs, true);
@@ -1159,11 +1429,12 @@ export function createFramedApp(options) {
1159
1429
  return updateTargetPage(model, model.activePageId, msg);
1160
1430
  },
1161
1431
  view(model) {
1162
- const { activePage, layerStack, activeLayer, } = resolvePresentedLayerContext(model);
1163
- const headerResult = resolveHeaderLine(model, options, pagesById, headerScratch);
1432
+ const themedFrameCtx = resolveFrameThemeCtx(model.activeShellThemeId);
1433
+ const { controlProjection, layerStack, activeLayer, } = resolvePresentedLayerContext(model);
1434
+ const headerResult = resolveHeaderLine(model, options, pagesById, headerScratch, themedFrameCtx);
1164
1435
  headerScratch = headerResult.surface;
1165
1436
  const header = headerResult.surface;
1166
- helpLineScratch = renderHelpLine(model, activeLayer, options.i18n, resolveNotificationFooterCue(model, options, pagesById), helpLineScratch);
1437
+ helpLineScratch = renderHelpLine(model, activeLayer, options.i18n, resolveNotificationFooterCue(model, options, pagesById), helpLineScratch, themedFrameCtx);
1167
1438
  const helpLine = helpLineScratch;
1168
1439
  const bodyRect = resolveBodyRect(model, options);
1169
1440
  // Check for maximized pane — if set, render only that pane at full body rect
@@ -1182,21 +1453,22 @@ export function createFramedApp(options) {
1182
1453
  const activeTransition = model.activeTransition ?? options.transition;
1183
1454
  if (model.previousPageId != null && model.transitionProgress < 1 && activeTransition && activeTransition !== 'none') {
1184
1455
  const activeBodyResult = maximizedPaneId
1185
- ? renderMaximizedPane(model.activePageId, model, bodyRect, pagesById, maximizedPaneId, paneScratchPool)
1186
- : renderPageContent(model.activePageId, model, bodyRect, pagesById);
1456
+ ? renderMaximizedPane(model.activePageId, model, bodyRect, pagesById, maximizedPaneId, paneScratchPool, themedFrameCtx)
1457
+ : renderPageContent(model.activePageId, model, bodyRect, pagesById, themedFrameCtx);
1187
1458
  activeResult = activeBodyResult;
1188
1459
  bodySurface = activeBodyResult.surface;
1189
- const ctx = resolveSafeCtx();
1460
+ const ctx = themedFrameCtx;
1190
1461
  if (ctx) {
1191
- const prevResult = renderPageContent(model.previousPageId, model, bodyRect, pagesById);
1462
+ const prevResult = renderPageContent(model.previousPageId, model, bodyRect, pagesById, themedFrameCtx);
1192
1463
  bodySurface = renderTransition(prevResult.surface, activeBodyResult.surface, activeTransition, model.transitionProgress, bodyRect.width, bodyRect.height, ctx, model.transitionFrame);
1193
1464
  }
1194
1465
  }
1195
1466
  else {
1196
1467
  activeResult = maximizedPaneId
1197
- ? renderMaximizedPaneInto(model.activePageId, model, bodyRect, pagesById, maximizedPaneId, frameSurface, bodyRect.row, bodyRect.col, paneScratchPool)
1198
- : renderPageContentInto(model.activePageId, model, bodyRect, pagesById, frameSurface, bodyRect.row, bodyRect.col, paneScratchPool);
1468
+ ? renderMaximizedPaneInto(model.activePageId, model, bodyRect, pagesById, maximizedPaneId, frameSurface, bodyRect.row, bodyRect.col, paneScratchPool, themedFrameCtx)
1469
+ : renderPageContentInto(model.activePageId, model, bodyRect, pagesById, frameSurface, bodyRect.row, bodyRect.col, paneScratchPool, themedFrameCtx);
1199
1470
  }
1471
+ rememberWorkspaceLayout(model, activeResult.paneRects, headerResult.tabTargets);
1200
1472
  const overlays = [];
1201
1473
  if (options.overlayFactory != null) {
1202
1474
  overlays.push(...options.overlayFactory({
@@ -1207,7 +1479,7 @@ export function createFramedApp(options) {
1207
1479
  }));
1208
1480
  }
1209
1481
  if (frameNotificationOptions.enabled) {
1210
- const ctx = resolveSafeCtx();
1482
+ const ctx = themedFrameCtx;
1211
1483
  overlays.push(...renderNotificationStack(model.runtimeNotifications, {
1212
1484
  screenWidth: model.columns,
1213
1485
  screenHeight: model.rows,
@@ -1218,20 +1490,24 @@ export function createFramedApp(options) {
1218
1490
  }
1219
1491
  if (model.settingsOpen) {
1220
1492
  const settingsLayer = layerStack.find((layer) => layer.kind === 'settings');
1221
- const settingsOverlay = renderSettingsDrawer(model, options, pagesById, settingsLayer?.title);
1493
+ const settingsOverlay = renderSettingsDrawer(model, options, pagesById, resolvedShellThemes, settingsLayer?.title, themedFrameCtx);
1222
1494
  if (settingsOverlay != null) {
1223
1495
  overlays.push(settingsOverlay);
1224
1496
  }
1225
1497
  }
1226
1498
  if (model.notificationCenterOpen) {
1227
1499
  const notificationLayer = layerStack.find((layer) => layer.kind === 'notification-center');
1228
- const notificationCenterOverlay = renderNotificationCenterDrawer(model, options, pagesById, notificationLayer?.title);
1500
+ const notificationCenterOverlay = renderNotificationCenterDrawer(model, options, pagesById, notificationLayer?.title, themedFrameCtx);
1229
1501
  if (notificationCenterOverlay != null) {
1230
1502
  overlays.push(notificationCenterOverlay);
1231
1503
  }
1232
1504
  }
1233
1505
  if (model.helpOpen) {
1234
- const helpOverlay = renderHelpOverlay(model, activePage, frameKeys, paletteKeys, options, pagesById);
1506
+ const helpSource = controlProjection.helpSource;
1507
+ if (helpSource == null) {
1508
+ throw new Error('createFramedApp: help layer projection is missing a help source');
1509
+ }
1510
+ const helpOverlay = renderHelpOverlay(model, helpSource, options.i18n);
1235
1511
  overlays.push(modal({
1236
1512
  title: activeLayer.kind === 'help'
1237
1513
  ? (activeLayer.title ?? frameMessage(options.i18n, 'help.title', 'Keyboard Help'))
@@ -1240,6 +1516,9 @@ export function createFramedApp(options) {
1240
1516
  hint: typeof activeLayer.hintSource === 'string'
1241
1517
  ? activeLayer.hintSource
1242
1518
  : frameMessage(options.i18n, 'help.hint', 'j/k scroll • d/u page • g/G top/bottom • mouse wheel • ?/Esc close'),
1519
+ borderToken: themedFrameCtx?.border('primary'),
1520
+ bgToken: themedFrameCtx?.surface('elevated'),
1521
+ ctx: themedFrameCtx,
1243
1522
  width: helpOverlay.body.width + 4,
1244
1523
  screenWidth: model.columns,
1245
1524
  screenHeight: model.rows,
@@ -1247,7 +1526,11 @@ export function createFramedApp(options) {
1247
1526
  }
1248
1527
  if (model.commandPalette != null) {
1249
1528
  const paletteWidth = Math.max(20, Math.min(80, model.columns - 4));
1250
- const paletteBody = commandPalette(model.commandPalette, { width: Math.max(16, paletteWidth - 4) });
1529
+ const paletteBody = commandPaletteSurface(model.commandPalette, {
1530
+ width: Math.max(16, paletteWidth - 4),
1531
+ ctx: themedFrameCtx,
1532
+ showScrollbar: false,
1533
+ });
1251
1534
  const paletteLayer = activeLayer.kind === 'search' || activeLayer.kind === 'command-palette'
1252
1535
  ? activeLayer
1253
1536
  : undefined;
@@ -1257,13 +1540,16 @@ export function createFramedApp(options) {
1257
1540
  hint: typeof paletteLayer?.hintSource === 'string'
1258
1541
  ? paletteLayer.hintSource
1259
1542
  : frameMessage(options.i18n, 'palette.hint', 'Enter select • Esc close'),
1543
+ borderToken: themedFrameCtx?.border('primary'),
1544
+ bgToken: themedFrameCtx?.surface('elevated'),
1545
+ ctx: themedFrameCtx,
1260
1546
  width: paletteWidth,
1261
1547
  screenWidth: model.columns,
1262
1548
  screenHeight: model.rows,
1263
1549
  }));
1264
1550
  }
1265
1551
  if (model.quitConfirmOpen) {
1266
- overlays.push(renderShellQuitOverlay(model.columns, model.rows, options.i18n));
1552
+ overlays.push(renderShellQuitOverlay(model.columns, model.rows, options.i18n, themedFrameCtx));
1267
1553
  }
1268
1554
  if (bodySurface != null && bodyRect.width > 0 && bodyRect.height > 0) {
1269
1555
  frameSurface.blit(bodySurface, bodyRect.col, bodyRect.row);
@@ -1276,7 +1562,67 @@ export function createFramedApp(options) {
1276
1562
  return wrapFrameMsg({ type: 'runtime-issue', issue });
1277
1563
  },
1278
1564
  };
1279
- return app;
1565
+ const framedApp = app;
1566
+ let hostedRunActive = false;
1567
+ framedApp.run = async (runOptions) => {
1568
+ if (hostedRunActive) {
1569
+ throw new Error('createFramedApp: concurrent app.run() calls on the same framed app are not supported');
1570
+ }
1571
+ hostedRunActive = true;
1572
+ const previousFrameCtx = frameCtx;
1573
+ const previousFrameCtxShellThemeId = frameCtxShellThemeId;
1574
+ const previousDefaultFrameCtx = defaultFrameCtx;
1575
+ const previousResolvedShellThemes = resolvedShellThemes;
1576
+ const runtimeCtx = runOptions?.ctx;
1577
+ if (runtimeCtx != null && options.ctx == null) {
1578
+ useRunScopedFrameCtx = true;
1579
+ frameCtx = runtimeCtx;
1580
+ defaultFrameCtx = runtimeCtx;
1581
+ resolvedShellThemes = [];
1582
+ ensureResolvedShellThemes(runtimeCtx);
1583
+ frameCtxShellThemeId = resolveShellThemeForContext(resolvedShellThemes, runtimeCtx)?.id;
1584
+ }
1585
+ const frameBudgetMs = resolveFrameBudgetMs(runOptions, resolveFrameCtx());
1586
+ let pendingTimingSnapshot;
1587
+ let needsTimingHydrationRender = true;
1588
+ try {
1589
+ await runWithLifecycleHooks(framedApp, {
1590
+ ...runOptions,
1591
+ mouse: runOptions?.mouse ?? true,
1592
+ }, {
1593
+ beforeRender(model) {
1594
+ if (pendingTimingSnapshot == null)
1595
+ return model;
1596
+ return applyFrameTimingSnapshot(model, pendingTimingSnapshot);
1597
+ },
1598
+ afterRender({ timings }) {
1599
+ pendingTimingSnapshot = summarizeFrameTimings(timings, frameBudgetMs);
1600
+ if (!needsTimingHydrationRender)
1601
+ return;
1602
+ needsTimingHydrationRender = false;
1603
+ return { requestRender: true };
1604
+ },
1605
+ });
1606
+ }
1607
+ finally {
1608
+ hostedRunActive = false;
1609
+ useRunScopedFrameCtx = false;
1610
+ frameCtx = previousFrameCtx;
1611
+ frameCtxShellThemeId = previousFrameCtxShellThemeId;
1612
+ defaultFrameCtx = previousDefaultFrameCtx;
1613
+ resolvedShellThemes = previousResolvedShellThemes;
1614
+ }
1615
+ };
1616
+ return framedApp;
1617
+ }
1618
+ /**
1619
+ * Create and immediately run a batteries-included framed shell.
1620
+ *
1621
+ * This is the one-call hosted path for users who want the frame to own the
1622
+ * runtime pump while `run(app)` remains the low-level TEA contract.
1623
+ */
1624
+ export async function runFramedApp(options, runOptions) {
1625
+ await createFramedApp(options).run(runOptions);
1280
1626
  }
1281
1627
  function focusPane(model, paneId) {
1282
1628
  if (model.focusedPaneByPage[model.activePageId] === paneId)
@@ -1292,426 +1638,4 @@ function focusPane(model, paneId) {
1292
1638
  function resolveBodyRect(model, options) {
1293
1639
  return frameBodyRect(model.columns, model.rows, options.bodyTopRows ?? 1, options.bodyBottomRows ?? 1);
1294
1640
  }
1295
- function renderHelpOverlay(model, activePage, frameKeys, paletteKeys, options, pagesById) {
1296
- const activePageModel = model.pageModels[model.activePageId];
1297
- const activeInputArea = findInputAreaByPaneId(resolveInputAreas(activePage, activePageModel), model.focusedPaneByPage[model.activePageId]);
1298
- const modalKeyMap = activePage.modalKeyMap?.(activePageModel);
1299
- const settings = resolveFrameSettings(model, options, pagesById);
1300
- const notificationCenter = resolveFrameNotificationCenter(model, options, pagesById);
1301
- const workspaceHintSource = options.helpLineSource?.({
1302
- model,
1303
- activePage,
1304
- frameKeys,
1305
- globalKeys: options.globalKeys,
1306
- });
1307
- const workspaceHelpSource = mergeBindingSources(frameKeys, quitHelpKeys, options.globalKeys, activeInputArea?.helpSource ?? activeInputArea?.keyMap, activePage.helpSource ?? activePage.keyMap);
1308
- const layerStack = describeFrameLayerStack(model, {
1309
- pageModalOpen: modalKeyMap != null,
1310
- layers: {
1311
- workspace: {
1312
- title: activePage.title,
1313
- hintSource: typeof workspaceHintSource === 'string'
1314
- ? workspaceHintSource
1315
- : workspaceHintSource ?? mergeBindingSources(frameKeys, options.globalKeys, activeInputArea?.helpSource ?? activeInputArea?.keyMap, activePage.helpSource ?? activePage.keyMap),
1316
- helpSource: workspaceHelpSource,
1317
- },
1318
- 'page-modal': {
1319
- title: activePage.title,
1320
- hintSource: modalKeyMap ?? activePage.helpSource ?? activePage.keyMap,
1321
- helpSource: mergeBindingSources(quitHelpKeys, modalKeyMap, activePage.helpSource ?? activePage.keyMap),
1322
- },
1323
- settings: {
1324
- title: settings?.title ?? frameMessage(options.i18n, 'settings.title', 'Settings'),
1325
- hintSource: frameMessage(options.i18n, 'settings.footer', 'F2/Esc close • ↑/↓ rows • Enter toggle • / search • q quit'),
1326
- helpSource: mergeBindingSources(settingsHelpKeys, quitHelpKeys),
1327
- },
1328
- help: {
1329
- title: frameMessage(options.i18n, 'help.title', 'Keyboard Help'),
1330
- hintSource: frameMessage(options.i18n, 'help.hint', 'j/k scroll • d/u page • g/G top/bottom • mouse wheel • ?/Esc close'),
1331
- helpSource: helpLayerHelpKeys,
1332
- },
1333
- 'notification-center': {
1334
- title: notificationCenter == null
1335
- ? frameMessage(options.i18n, 'notifications.title', 'Notifications')
1336
- : `${notificationCenter.title} • ${frameNotificationFilterLabel(options.i18n, notificationCenter.activeFilter)}`,
1337
- hintSource: frameMessage(options.i18n, 'notifications.footer', 'Shift+N close • f filter • j/k scroll • q quit'),
1338
- helpSource: mergeBindingSources(notificationCenterHelpKeys, quitHelpKeys),
1339
- },
1340
- search: {
1341
- title: model.commandPaletteTitle ?? activePage.searchTitle ?? frameMessage(options.i18n, 'search.title', 'Search'),
1342
- hintSource: frameMessage(options.i18n, 'palette.hint', 'Enter select • Esc close'),
1343
- helpSource: mergeBindingSources(paletteKeys, quitHelpKeys),
1344
- },
1345
- 'command-palette': {
1346
- title: model.commandPaletteTitle ?? frameMessage(options.i18n, 'palette.title', 'Command Palette'),
1347
- hintSource: frameMessage(options.i18n, 'palette.hint', 'Enter select • Esc close'),
1348
- helpSource: mergeBindingSources(paletteKeys, quitHelpKeys),
1349
- },
1350
- 'quit-confirm': {
1351
- title: frameMessage(options.i18n, 'quit.title', 'Quit?'),
1352
- hintSource: frameMessage(options.i18n, 'quit.footer', 'Y quit • N stay'),
1353
- helpSource: quitConfirmHelpKeys,
1354
- },
1355
- },
1356
- });
1357
- const beneathHelpLayer = layerStack.length > 1 ? layerStack[layerStack.length - 2] : undefined;
1358
- const source = beneathHelpLayer?.helpSource ?? workspaceHelpSource;
1359
- const maxDialogWidth = Math.max(28, Math.min(model.columns - 4, 88));
1360
- const bodyWidth = Math.max(20, maxDialogWidth - 4);
1361
- const helpSurface = helpViewSurface(source, {
1362
- title: undefined,
1363
- width: bodyWidth,
1364
- });
1365
- const pagerHeight = Math.max(4, Math.min(helpSurface.height + 1, Math.max(4, model.rows - 8)));
1366
- const pagerState = createPagerStateForSurface(helpSurface, {
1367
- width: bodyWidth,
1368
- height: pagerHeight,
1369
- });
1370
- const scrollY = Math.max(0, Math.min(model.helpScrollY, pagerState.scroll.maxY));
1371
- const scrolledState = {
1372
- ...pagerState,
1373
- scroll: {
1374
- ...pagerState.scroll,
1375
- y: scrollY,
1376
- },
1377
- };
1378
- return {
1379
- body: pagerSurface(helpSurface, scrolledState, { showScrollbar: true, showStatus: true }),
1380
- maxScrollY: pagerState.scroll.maxY,
1381
- scrollY,
1382
- };
1383
- }
1384
- function isHelpScrollAction(action) {
1385
- return action.type === 'scroll-up'
1386
- || action.type === 'scroll-down'
1387
- || action.type === 'page-up'
1388
- || action.type === 'page-down'
1389
- || action.type === 'top'
1390
- || action.type === 'bottom';
1391
- }
1392
- function resolveFrameSettings(model, options, pagesById) {
1393
- const activePage = pagesById.get(model.activePageId);
1394
- return options.settings?.({
1395
- model,
1396
- activePage,
1397
- pageModel: model.pageModels[model.activePageId],
1398
- });
1399
- }
1400
- function resolveFrameNotificationCenter(model, options, pagesById) {
1401
- const activePage = pagesById.get(model.activePageId);
1402
- const pageModel = model.pageModels[model.activePageId];
1403
- const provided = options.notificationCenter?.({
1404
- model,
1405
- activePage,
1406
- pageModel,
1407
- runtimeNotifications: model.runtimeNotifications,
1408
- });
1409
- if (provided != null) {
1410
- const filters = provided.filters != null && provided.filters.length > 0
1411
- ? provided.filters
1412
- : DEFAULT_NOTIFICATION_CENTER_FILTERS;
1413
- const activeFilter = filters.includes(provided.activeFilter ?? 'ALL')
1414
- ? (provided.activeFilter ?? 'ALL')
1415
- : filters[0];
1416
- return {
1417
- title: provided.title ?? frameMessage(options.i18n, 'notifications.title', 'Notifications'),
1418
- state: provided.state,
1419
- filters,
1420
- activeFilter,
1421
- onFilterChange: provided.onFilterChange,
1422
- };
1423
- }
1424
- if (options.runtimeNotifications === false)
1425
- return undefined;
1426
- return {
1427
- title: frameMessage(options.i18n, 'notifications.title', 'Notifications'),
1428
- state: model.runtimeNotifications,
1429
- filters: DEFAULT_NOTIFICATION_CENTER_FILTERS,
1430
- activeFilter: model.runtimeNotificationHistoryFilter,
1431
- };
1432
- }
1433
- function resolveSettingsLayout(model, options, pagesById) {
1434
- const settings = resolveFrameSettings(model, options, pagesById);
1435
- if (settings == null)
1436
- return undefined;
1437
- const sections = settings.sections.filter((section) => section.rows.length > 0);
1438
- if (sections.length === 0)
1439
- return undefined;
1440
- const drawerWidth = resolveSettingsDrawerWidth(model.columns);
1441
- const anchor = frameStartAnchor(options.i18n);
1442
- const startCol = anchor === 'left' ? 0 : Math.max(0, model.columns - drawerWidth);
1443
- const contentWidth = Math.max(16, drawerWidth - 4);
1444
- const preferenceSections = preparePreferenceSections(toPreferenceSections(sections));
1445
- const rows = [];
1446
- let line = 0;
1447
- for (let sectionIndex = 0; sectionIndex < preferenceSections.length; sectionIndex++) {
1448
- const section = preferenceSections[sectionIndex];
1449
- if (sectionIndex > 0) {
1450
- line += 1;
1451
- }
1452
- line += 1;
1453
- line += 1;
1454
- for (let rowIndex = 0; rowIndex < section.rows.length; rowIndex++) {
1455
- const preparedRow = section.rows[rowIndex];
1456
- const row = sections[sectionIndex].rows[rowIndex];
1457
- const rowLayout = resolvePreferenceRowLayout(preparedRow, contentWidth);
1458
- rows.push({
1459
- index: rows.length,
1460
- line,
1461
- height: rowLayout.height,
1462
- row,
1463
- });
1464
- line += rowLayout.height;
1465
- if (rowIndex < section.rows.length - 1) {
1466
- line += 1;
1467
- }
1468
- }
1469
- }
1470
- const contentHeight = Math.max(1, model.rows - 2);
1471
- const totalLines = Math.max(1, line);
1472
- const maxScrollY = Math.max(0, totalLines - contentHeight);
1473
- return {
1474
- settings: {
1475
- ...settings,
1476
- sections,
1477
- },
1478
- preferenceSections,
1479
- rows,
1480
- anchor,
1481
- startCol,
1482
- drawerWidth,
1483
- contentWidth,
1484
- contentHeight,
1485
- totalLines,
1486
- maxScrollY,
1487
- };
1488
- }
1489
- function resolveNotificationCenterDrawerWidth(columns) {
1490
- const boundedColumns = Math.max(28, columns);
1491
- return Math.min(Math.max(32, Math.floor(boundedColumns * 0.34)), Math.max(32, boundedColumns - 4), 52);
1492
- }
1493
- function resolveNotificationCenterLayout(model, options, pagesById) {
1494
- const center = resolveFrameNotificationCenter(model, options, pagesById);
1495
- if (center == null)
1496
- return undefined;
1497
- const drawerWidth = resolveNotificationCenterDrawerWidth(model.columns);
1498
- const anchor = frameEndAnchor(options.i18n);
1499
- const startCol = anchor === 'left' ? 0 : Math.max(0, model.columns - drawerWidth);
1500
- const contentWidth = Math.max(18, drawerWidth - 4);
1501
- const content = renderNotificationCenterSurface(center, contentWidth, options.i18n);
1502
- const contentHeight = Math.max(1, model.rows - 2);
1503
- const pagerState = createPagerStateForSurface(content, {
1504
- width: contentWidth,
1505
- height: contentHeight,
1506
- });
1507
- return {
1508
- center,
1509
- anchor,
1510
- startCol,
1511
- drawerWidth,
1512
- contentWidth,
1513
- contentHeight,
1514
- content,
1515
- maxScrollY: pagerState.scroll.maxY,
1516
- };
1517
- }
1518
- function resolveSettingsDrawerWidth(columns) {
1519
- const boundedColumns = Math.max(24, columns);
1520
- return Math.min(Math.max(28, Math.floor(boundedColumns * 0.3)), Math.max(28, boundedColumns - 4), 42);
1521
- }
1522
- function clampSettingsFocus(model, layout) {
1523
- if (layout.rows.length === 0)
1524
- return 0;
1525
- return Math.max(0, Math.min(model.settingsFocusIndex, layout.rows.length - 1));
1526
- }
1527
- function clampSettingsScroll(model, layout) {
1528
- return Math.max(0, Math.min(model.settingsScrollY, layout.maxScrollY));
1529
- }
1530
- function resolveInputAreas(page, pageModel) {
1531
- return page.inputAreas?.(pageModel) ?? [];
1532
- }
1533
- function findInputAreaByPaneId(inputAreas, paneId) {
1534
- if (paneId == null)
1535
- return undefined;
1536
- return inputAreas.find((area) => area.paneId === paneId);
1537
- }
1538
- function ensureSettingsRangeVisible(startLine, height, scrollY, visibleLines, maxScrollY) {
1539
- let next = scrollY;
1540
- const endLine = startLine + Math.max(1, height) - 1;
1541
- if (startLine < next) {
1542
- next = startLine;
1543
- }
1544
- else if (endLine >= next + visibleLines) {
1545
- next = endLine - visibleLines + 1;
1546
- }
1547
- return Math.max(0, Math.min(next, maxScrollY));
1548
- }
1549
- function moveSettingsFocus(model, layout, delta) {
1550
- if (layout.rows.length === 0)
1551
- return model;
1552
- const nextFocus = Math.max(0, Math.min(clampSettingsFocus(model, layout) + delta, layout.rows.length - 1));
1553
- const focusedRow = layout.rows[nextFocus];
1554
- return {
1555
- ...model,
1556
- settingsFocusIndex: nextFocus,
1557
- settingsScrollY: ensureSettingsRangeVisible(focusedRow.line, focusedRow.height, clampSettingsScroll(model, layout), layout.contentHeight, layout.maxScrollY),
1558
- };
1559
- }
1560
- function scrollSettingsBy(model, layout, delta) {
1561
- return {
1562
- ...model,
1563
- settingsScrollY: Math.max(0, Math.min(clampSettingsScroll(model, layout) + delta, layout.maxScrollY)),
1564
- };
1565
- }
1566
- function scrollNotificationCenterBy(model, layout, delta) {
1567
- return {
1568
- ...model,
1569
- notificationCenterScrollY: Math.max(0, Math.min(model.notificationCenterScrollY + delta, layout.maxScrollY)),
1570
- };
1571
- }
1572
- function cycleNotificationCenterFilter(model, layout) {
1573
- const filters = layout.center.filters;
1574
- if (filters.length < 2)
1575
- return [model, []];
1576
- const currentIndex = Math.max(0, filters.indexOf(layout.center.activeFilter));
1577
- const nextFilter = filters[(currentIndex + 1) % filters.length];
1578
- if (layout.center.onFilterChange != null) {
1579
- const action = layout.center.onFilterChange(nextFilter);
1580
- return [{
1581
- ...model,
1582
- notificationCenterScrollY: 0,
1583
- }, action === undefined ? [] : [emitMsgForPage(model.activePageId, action)]];
1584
- }
1585
- return [{
1586
- ...model,
1587
- runtimeNotificationHistoryFilter: nextFilter,
1588
- notificationCenterScrollY: 0,
1589
- }, []];
1590
- }
1591
- function renderSettingsDrawer(model, options, pagesById, titleOverride) {
1592
- const layout = resolveSettingsLayout(model, options, pagesById);
1593
- if (layout == null)
1594
- return undefined;
1595
- const scrollY = clampSettingsScroll(model, layout);
1596
- const content = renderSettingsSurface(layout, model);
1597
- const pagerState = createPagerStateForSurface(content, {
1598
- width: layout.contentWidth,
1599
- height: layout.contentHeight,
1600
- });
1601
- const scrolledState = {
1602
- ...pagerState,
1603
- scroll: {
1604
- ...pagerState.scroll,
1605
- y: scrollY,
1606
- },
1607
- };
1608
- const body = pagerSurface(content, scrolledState, {
1609
- showScrollbar: layout.maxScrollY > 0,
1610
- showStatus: false,
1611
- });
1612
- return drawer({
1613
- anchor: layout.anchor,
1614
- title: titleOverride ?? layout.settings.title ?? frameMessage(options.i18n, 'settings.title', 'Settings'),
1615
- content: body,
1616
- borderToken: layout.settings.borderToken,
1617
- bgToken: layout.settings.bgToken,
1618
- ctx: resolveSafeCtx() ?? undefined,
1619
- width: layout.drawerWidth,
1620
- screenWidth: model.columns,
1621
- screenHeight: model.rows,
1622
- });
1623
- }
1624
- function renderNotificationCenterDrawer(model, options, pagesById, titleOverride) {
1625
- const layout = resolveNotificationCenterLayout(model, options, pagesById);
1626
- if (layout == null)
1627
- return undefined;
1628
- const pagerState = createPagerStateForSurface(layout.content, {
1629
- width: layout.contentWidth,
1630
- height: layout.contentHeight,
1631
- });
1632
- const scrolledState = {
1633
- ...pagerState,
1634
- scroll: {
1635
- ...pagerState.scroll,
1636
- y: Math.max(0, Math.min(model.notificationCenterScrollY, layout.maxScrollY)),
1637
- },
1638
- };
1639
- const body = pagerSurface(layout.content, scrolledState, {
1640
- showScrollbar: layout.maxScrollY > 0,
1641
- showStatus: false,
1642
- });
1643
- return drawer({
1644
- anchor: layout.anchor,
1645
- title: titleOverride ?? `${layout.center.title} • ${frameNotificationFilterLabel(options.i18n, layout.center.activeFilter)}`,
1646
- content: body,
1647
- width: layout.drawerWidth,
1648
- screenWidth: model.columns,
1649
- screenHeight: model.rows,
1650
- });
1651
- }
1652
- function renderSettingsSurface(layout, model) {
1653
- const focusedIndex = clampSettingsFocus(model, layout);
1654
- return preferenceListSurface(layout.preferenceSections, {
1655
- width: layout.contentWidth,
1656
- selectedRowId: layout.rows[focusedIndex]?.row.id,
1657
- ctx: resolveSafeCtx() ?? undefined,
1658
- theme: layout.settings.listTheme,
1659
- });
1660
- }
1661
- function toPreferenceSections(sections) {
1662
- return sections.map((section) => ({
1663
- id: section.id,
1664
- title: section.title,
1665
- rows: section.rows.map((row) => toPreferenceRow(row)),
1666
- }));
1667
- }
1668
- function toPreferenceRow(row) {
1669
- return {
1670
- id: row.id,
1671
- label: row.label,
1672
- description: row.description,
1673
- valueLabel: row.valueLabel,
1674
- kind: row.kind,
1675
- checked: row.checked,
1676
- enabled: row.enabled,
1677
- };
1678
- }
1679
- function resolveNotificationFooterCue(model, options, pagesById) {
1680
- const center = resolveFrameNotificationCenter(model, options, pagesById);
1681
- if (center == null)
1682
- return undefined;
1683
- const liveCount = center.state.items.length;
1684
- const archivedCount = countNotificationHistory(center.state, center.activeFilter);
1685
- return frameNotificationCue(options.i18n, liveCount, archivedCount);
1686
- }
1687
- function renderNotificationCenterSurface(center, width, i18n) {
1688
- const ctx = resolveSafeCtx() ?? undefined;
1689
- const rows = [
1690
- insetLineSurface(`Live: ${center.state.items.length} • Archived: ${center.state.history.length}`, width),
1691
- insetLineSurface(`Filter: ${frameNotificationFilterLabel(i18n, center.activeFilter)}`, width),
1692
- ];
1693
- const liveItems = [...center.state.items].sort((left, right) => right.updatedAtMs - left.updatedAtMs || right.id - left.id);
1694
- if (liveItems.length > 0) {
1695
- rows.push(createSurface(width, 1));
1696
- rows.push(insetLineSurface(ctx == null ? 'Current stack' : ctx.style.bold('Current stack'), width));
1697
- rows.push(createSurface(width, 1));
1698
- for (let index = 0; index < liveItems.length; index++) {
1699
- rows.push(renderNotificationReviewEntrySurface(liveItems[index], {
1700
- width,
1701
- ctx,
1702
- metaLabel: `${liveItems[index].variant} • live`,
1703
- }));
1704
- if (index < liveItems.length - 1)
1705
- rows.push(createSurface(width, 1));
1706
- }
1707
- }
1708
- rows.push(createSurface(width, 1));
1709
- rows.push(renderNotificationHistorySurface(center.state, {
1710
- width,
1711
- height: Number.MAX_SAFE_INTEGER,
1712
- filter: center.activeFilter,
1713
- ctx,
1714
- }));
1715
- return vstackSurface(...rows);
1716
- }
1717
1641
  //# sourceMappingURL=app-frame.js.map