@flyingrobots/bijou-tui 4.4.1 → 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 (139) hide show
  1. package/README.md +92 -20
  2. package/dist/app-frame-actions.d.ts.map +1 -1
  3. package/dist/app-frame-actions.js +2 -0
  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 +49 -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.js +2 -2
  17. package/dist/app-frame-render.js.map +1 -1
  18. package/dist/app-frame-types.d.ts +119 -5
  19. package/dist/app-frame-types.d.ts.map +1 -1
  20. package/dist/app-frame-types.js +17 -1
  21. package/dist/app-frame-types.js.map +1 -1
  22. package/dist/app-frame-utils.d.ts.map +1 -1
  23. package/dist/app-frame-utils.js +6 -5
  24. package/dist/app-frame-utils.js.map +1 -1
  25. package/dist/app-frame.d.ts +18 -84
  26. package/dist/app-frame.d.ts.map +1 -1
  27. package/dist/app-frame.js +377 -625
  28. package/dist/app-frame.js.map +1 -1
  29. package/dist/browsable-list.d.ts +12 -0
  30. package/dist/browsable-list.d.ts.map +1 -1
  31. package/dist/browsable-list.js +17 -6
  32. package/dist/browsable-list.js.map +1 -1
  33. package/dist/canvas.d.ts.map +1 -1
  34. package/dist/canvas.js +27 -7
  35. package/dist/canvas.js.map +1 -1
  36. package/dist/collection-surface.d.ts +8 -0
  37. package/dist/collection-surface.d.ts.map +1 -1
  38. package/dist/collection-surface.js +72 -8
  39. package/dist/collection-surface.js.map +1 -1
  40. package/dist/css/text-style.d.ts +2 -0
  41. package/dist/css/text-style.d.ts.map +1 -1
  42. package/dist/css/text-style.js +18 -8
  43. package/dist/css/text-style.js.map +1 -1
  44. package/dist/debug-overlay.d.ts +19 -0
  45. package/dist/debug-overlay.d.ts.map +1 -0
  46. package/dist/debug-overlay.js +25 -0
  47. package/dist/debug-overlay.js.map +1 -0
  48. package/dist/design-language.d.ts.map +1 -1
  49. package/dist/design-language.js +4 -5
  50. package/dist/design-language.js.map +1 -1
  51. package/dist/driver.d.ts +102 -0
  52. package/dist/driver.d.ts.map +1 -1
  53. package/dist/driver.js +259 -19
  54. package/dist/driver.js.map +1 -1
  55. package/dist/file-picker.d.ts.map +1 -1
  56. package/dist/file-picker.js +2 -2
  57. package/dist/file-picker.js.map +1 -1
  58. package/dist/flex.d.ts.map +1 -1
  59. package/dist/flex.js +6 -6
  60. package/dist/flex.js.map +1 -1
  61. package/dist/focus-area.d.ts +9 -1
  62. package/dist/focus-area.d.ts.map +1 -1
  63. package/dist/focus-area.js +52 -11
  64. package/dist/focus-area.js.map +1 -1
  65. package/dist/grid.d.ts +2 -2
  66. package/dist/grid.d.ts.map +1 -1
  67. package/dist/grid.js +11 -148
  68. package/dist/grid.js.map +1 -1
  69. package/dist/help.d.ts +2 -0
  70. package/dist/help.d.ts.map +1 -1
  71. package/dist/help.js +6 -4
  72. package/dist/help.js.map +1 -1
  73. package/dist/icon-presentation.d.ts +10 -0
  74. package/dist/icon-presentation.d.ts.map +1 -0
  75. package/dist/icon-presentation.js +12 -0
  76. package/dist/icon-presentation.js.map +1 -0
  77. package/dist/index.d.ts +14 -8
  78. package/dist/index.d.ts.map +1 -1
  79. package/dist/index.js +7 -5
  80. package/dist/index.js.map +1 -1
  81. package/dist/layout-preset.d.ts +1 -1
  82. package/dist/layout-preset.d.ts.map +1 -1
  83. package/dist/motion/reconciler.d.ts +6 -2
  84. package/dist/motion/reconciler.d.ts.map +1 -1
  85. package/dist/motion/reconciler.js +40 -10
  86. package/dist/motion/reconciler.js.map +1 -1
  87. package/dist/motion/types.d.ts +3 -1
  88. package/dist/motion/types.d.ts.map +1 -1
  89. package/dist/navigable-table.d.ts +9 -10
  90. package/dist/navigable-table.d.ts.map +1 -1
  91. package/dist/navigable-table.js +33 -14
  92. package/dist/navigable-table.js.map +1 -1
  93. package/dist/notification.d.ts +15 -0
  94. package/dist/notification.d.ts.map +1 -1
  95. package/dist/notification.js +98 -29
  96. package/dist/notification.js.map +1 -1
  97. package/dist/overlay.d.ts.map +1 -1
  98. package/dist/overlay.js +40 -14
  99. package/dist/overlay.js.map +1 -1
  100. package/dist/pager.d.ts +3 -1
  101. package/dist/pager.d.ts.map +1 -1
  102. package/dist/pager.js +4 -0
  103. package/dist/pager.js.map +1 -1
  104. package/dist/pipeline/middleware/grayscale.d.ts.map +1 -1
  105. package/dist/pipeline/middleware/grayscale.js +5 -5
  106. package/dist/pipeline/middleware/grayscale.js.map +1 -1
  107. package/dist/pipeline/middleware/motion.js +2 -2
  108. package/dist/pipeline/middleware/motion.js.map +1 -1
  109. package/dist/pipeline/middleware/surface-shaders.d.ts +37 -0
  110. package/dist/pipeline/middleware/surface-shaders.d.ts.map +1 -0
  111. package/dist/pipeline/middleware/surface-shaders.js +164 -0
  112. package/dist/pipeline/middleware/surface-shaders.js.map +1 -0
  113. package/dist/pipeline/pipeline.d.ts +29 -1
  114. package/dist/pipeline/pipeline.d.ts.map +1 -1
  115. package/dist/pipeline/pipeline.js +86 -23
  116. package/dist/pipeline/pipeline.js.map +1 -1
  117. package/dist/runtime.d.ts +19 -0
  118. package/dist/runtime.d.ts.map +1 -1
  119. package/dist/runtime.js +183 -30
  120. package/dist/runtime.js.map +1 -1
  121. package/dist/split-pane.d.ts +2 -2
  122. package/dist/split-pane.d.ts.map +1 -1
  123. package/dist/split-pane.js +28 -49
  124. package/dist/split-pane.js.map +1 -1
  125. package/dist/subapp/mount.d.ts +17 -0
  126. package/dist/subapp/mount.d.ts.map +1 -1
  127. package/dist/subapp/mount.js +13 -0
  128. package/dist/subapp/mount.js.map +1 -1
  129. package/dist/surface-layout.d.ts +8 -3
  130. package/dist/surface-layout.d.ts.map +1 -1
  131. package/dist/surface-layout.js +27 -12
  132. package/dist/surface-layout.js.map +1 -1
  133. package/dist/view-output.js +2 -2
  134. package/dist/view-output.js.map +1 -1
  135. package/dist/viewport.d.ts +13 -1
  136. package/dist/viewport.d.ts.map +1 -1
  137. package/dist/viewport.js +33 -11
  138. package/dist/viewport.js.map +1 -1
  139. 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 { 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, 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
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 {
@@ -137,78 +143,45 @@ function createFrameNotificationTickCmd() {
137
143
  function cloneShellThemeContext(ctx, resolvedTheme) {
138
144
  return cloneContextWithResolvedTheme(ctx, resolvedTheme);
139
145
  }
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];
146
+ function readStageDuration(timings, stage) {
147
+ return timings.find((timing) => timing.stage === stage)?.durationMs ?? 0;
150
148
  }
151
- function resolveNextShellTheme(shellThemes, activeShellThemeId) {
152
- if (shellThemes.length === 0)
153
- return undefined;
154
- const currentIndex = Math.max(0, shellThemes.findIndex((theme) => theme.id === activeShellThemeId));
155
- return shellThemes[(currentIndex + 1) % shellThemes.length];
156
- }
157
- function resolveShellThemeForContext(shellThemes, ctx) {
158
- if (ctx == null)
159
- return undefined;
160
- return shellThemes.find((theme) => theme.resolvedTheme.theme === ctx.theme.theme);
161
- }
162
- function mergeShellThemeSettings(settings, shellThemes, activeShellThemeId, i18n) {
163
- if (shellThemes.length < 2)
164
- return settings;
165
- const currentTheme = resolveCurrentShellTheme(shellThemes, activeShellThemeId);
166
- const nextTheme = resolveNextShellTheme(shellThemes, activeShellThemeId);
167
- if (currentTheme == null || nextTheme == null)
168
- return settings;
169
- const row = {
170
- id: FRAME_SHELL_THEME_ROW_ID,
171
- label: frameMessage(i18n, 'settings.shellTheme.label', 'Shell theme'),
172
- description: currentTheme.description ?? frameMessage(i18n, 'settings.shellTheme.description', 'Current theme: {theme}. Options: {options}.', {
173
- theme: currentTheme.label,
174
- options: resolveShellThemeOptionsText(shellThemes, i18n),
175
- }),
176
- valueLabel: currentTheme.label,
177
- kind: 'choice',
178
- feedback: {
179
- title: frameMessage(i18n, 'settings.title', 'Settings'),
180
- message: frameMessage(i18n, 'settings.shellTheme.feedback', 'Shell theme set to {theme}.', { theme: nextTheme.label }),
181
- },
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,
182
157
  };
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
- };
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;
203
166
  }
204
167
  return {
205
- ...settings,
206
- sections: [
207
- { id: 'shell', title: shellSectionTitle, rows: [row] },
208
- ...settings.sections,
209
- ],
168
+ ...model,
169
+ frameTimeMs: snapshot.frameTimeMs,
170
+ viewTimeMs: snapshot.viewTimeMs,
171
+ diffTimeMs: snapshot.diffTimeMs,
172
+ frameBudgetMs: snapshot.frameBudgetMs,
173
+ frameOverBudget: snapshot.frameOverBudget,
210
174
  };
211
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
+ }
212
185
  // Factory
213
186
  // ---------------------------------------------------------------------------
214
187
  /**
@@ -230,28 +203,40 @@ export function createFramedApp(options) {
230
203
  if (!pagesById.has(defaultPageId)) {
231
204
  throw new Error(`createFramedApp: defaultPageId "${defaultPageId}" not found in pages`);
232
205
  }
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];
206
+ const shellThemeSpecs = options.shellThemes ?? [];
207
+ let defaultFrameCtx = options.ctx ?? resolveSafeCtx();
208
+ let resolvedShellThemes = [];
209
+ const enableShellThemeSettings = shellThemeSpecs.length > 1;
247
210
  const usesAmbientDefaultContext = options.ctx == null && defaultFrameCtx != null;
248
211
  let frameCtx = options.ctx;
249
- let frameCtxShellThemeId = resolveShellThemeForContext(resolvedShellThemes, frameCtx)?.id;
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
+ }
250
234
  function resolveFrameCtx() {
251
235
  return frameCtx ?? options.ctx ?? resolveSafeCtx();
252
236
  }
253
237
  function resolveFrameThemeCtx(activeShellThemeId) {
254
238
  const baseCtx = resolveFrameCtx();
239
+ ensureResolvedShellThemes(baseCtx);
255
240
  if (defaultFrameCtx == null)
256
241
  return baseCtx;
257
242
  const activeTheme = resolveCurrentShellTheme(resolvedShellThemes, activeShellThemeId);
@@ -266,11 +251,12 @@ export function createFramedApp(options) {
266
251
  return cloneShellThemeContext(defaultFrameCtx, activeTheme.resolvedTheme);
267
252
  }
268
253
  function publishShellThemeContext(nextTheme) {
254
+ ensureResolvedShellThemes(resolveFrameCtx());
269
255
  if (defaultFrameCtx == null)
270
256
  return resolveFrameCtx();
271
257
  frameCtx = cloneShellThemeContext(defaultFrameCtx, nextTheme.resolvedTheme);
272
258
  frameCtxShellThemeId = nextTheme.id;
273
- if (usesAmbientDefaultContext) {
259
+ if (usesAmbientDefaultContext && !useRunScopedFrameCtx) {
274
260
  setDefaultContext(frameCtx);
275
261
  }
276
262
  options.onShellThemeChange?.({
@@ -284,11 +270,17 @@ export function createFramedApp(options) {
284
270
  enableNotifications: options.notificationCenter != null || options.runtimeNotifications !== false,
285
271
  i18n: options.i18n,
286
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);
287
278
  const frameNotificationOptions = resolveFrameNotificationOptions(options);
288
279
  let composedFrameScratch = null;
289
280
  let headerScratch;
290
281
  let helpLineScratch;
291
282
  const paneScratchPool = createFramePaneScratchPool();
283
+ let workspaceLayoutCache;
292
284
  const paletteKeys = commandPaletteKeyMap({
293
285
  focusNext: { type: 'cp-next' },
294
286
  focusPrev: { type: 'cp-prev' },
@@ -297,6 +289,49 @@ export function createFramedApp(options) {
297
289
  select: { type: 'cp-select' },
298
290
  close: { type: 'cp-close' },
299
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
+ }
300
335
  function getComposedFrameScratch(width, height) {
301
336
  if (composedFrameScratch == null
302
337
  || composedFrameScratch.width !== width
@@ -402,11 +437,18 @@ export function createFramedApp(options) {
402
437
  teaCmds.push(...cmds);
403
438
  return nextModel;
404
439
  },
440
+ 'warn-frame-key-collision': (model, cmd, teaCmds) => {
441
+ const c = cmd;
442
+ return queueFrameKeyCollisionWarning(model, c.msg, teaCmds);
443
+ },
405
444
  // --- help ---
406
445
  'help-scroll': (model, cmd) => {
407
446
  const c = cmd;
408
- const activePage = pagesById.get(model.activePageId);
409
- const overlay = renderHelpOverlay(model, activePage, frameKeys, paletteKeys, options, pagesById, resolvedShellThemes);
447
+ const helpSource = resolvePresentedLayerContext(model).controlProjection.helpSource;
448
+ if (helpSource == null) {
449
+ return model;
450
+ }
451
+ const overlay = renderHelpOverlay(model, helpSource, options.i18n);
410
452
  const viewportHeight = Math.max(1, overlay.body.height - 1);
411
453
  const delta = c.action === 'down' ? 3
412
454
  : c.action === 'up' ? -3
@@ -706,7 +748,9 @@ export function createFramedApp(options) {
706
748
  }
707
749
  // frame-first (default)
708
750
  if (frameAction !== undefined) {
709
- 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');
710
754
  }
711
755
  if (paneAction !== undefined) {
712
756
  return [{ type: 'observed-key', msg, route: 'page' }, { type: 'emit-page-msg', pageId: model.activePageId, msg: paneAction }];
@@ -779,27 +823,15 @@ export function createFramedApp(options) {
779
823
  children: children ?? [],
780
824
  };
781
825
  }
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}`, {
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}`, {
796
829
  row: 0,
797
830
  col: target.startCol,
798
831
  width: target.endCol - target.startCol + 1,
799
832
  height: 1,
800
833
  }));
801
834
  const bodyRect = resolveBodyRect(model, options);
802
- const paneRects = resolveWorkspacePaneRects(model);
803
835
  const paneChildren = [];
804
836
  for (const [paneId, rect] of paneRects.entries()) {
805
837
  paneChildren.push(createShellRetainedLayoutNode(`pane:${paneId}`, rect));
@@ -809,6 +841,55 @@ export function createFramedApp(options) {
809
841
  createShellRetainedLayoutNode('workspace-body', bodyRect, paneChildren),
810
842
  ]);
811
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
+ }
812
893
  function buildSettingsRowChildren(model, layout) {
813
894
  const scrollY = clampSettingsScroll(model, layout);
814
895
  const viewportTop = 1;
@@ -1008,7 +1089,7 @@ export function createFramedApp(options) {
1008
1089
  }
1009
1090
  return helpLineOverride ?? mergeBindingSources(frameKeys, options.globalKeys, activeInputArea?.helpSource ?? activeInputArea?.keyMap, activePage.helpSource ?? activePage.keyMap);
1010
1091
  }
1011
- function resolveLayerMetadata(model, activePage, activeInputArea, modalKeyMap) {
1092
+ function resolveLayerMetadata(model, activePage, activePageModel, activeInputArea, modalKeyMap) {
1012
1093
  const settings = resolveFrameSettings(model, options, pagesById, resolvedShellThemes);
1013
1094
  const notificationCenter = resolveFrameNotificationCenter(model, options, pagesById);
1014
1095
  const workspaceHintSource = resolveWorkspaceHintSource(model, activePage, activeInputArea);
@@ -1026,17 +1107,22 @@ export function createFramedApp(options) {
1026
1107
  const notificationsTitle = notificationCenter == null
1027
1108
  ? frameMessage(options.i18n, 'notifications.title', 'Notifications')
1028
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
+ };
1029
1123
  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
- },
1124
+ workspace: workspaceLayer,
1125
+ 'page-modal': pageModalLayer,
1040
1126
  settings: {
1041
1127
  title: settings?.title ?? frameMessage(options.i18n, 'settings.title', 'Settings'),
1042
1128
  hintSource: settingsHint,
@@ -1071,12 +1157,11 @@ export function createFramedApp(options) {
1071
1157
  }
1072
1158
  function resolvePresentedLayerContext(model) {
1073
1159
  const { activePage, activePageModel, inputAreas, activeInputArea, modalKeyMap, pageModalOpen, } = resolveLayerContext(model);
1074
- const layerStack = describeFrameLayerStack(model, {
1160
+ const layerMetadata = resolveLayerMetadata(model, activePage, activePageModel, activeInputArea, modalKeyMap);
1161
+ const controlProjection = projectFrameControls(model, {
1075
1162
  pageModalOpen,
1076
- layers: resolveLayerMetadata(model, activePage, activeInputArea, modalKeyMap),
1163
+ layers: layerMetadata,
1077
1164
  });
1078
- const activeLayer = layerStack[layerStack.length - 1];
1079
- const underlyingLayer = layerStack.length > 1 ? layerStack[layerStack.length - 2] : undefined;
1080
1165
  return {
1081
1166
  activePage,
1082
1167
  activePageModel,
@@ -1084,9 +1169,11 @@ export function createFramedApp(options) {
1084
1169
  activeInputArea,
1085
1170
  modalKeyMap,
1086
1171
  pageModalOpen,
1087
- layerStack,
1088
- activeLayer,
1089
- underlyingLayer,
1172
+ layerMetadata,
1173
+ controlProjection,
1174
+ layerStack: controlProjection.layerStack,
1175
+ activeLayer: controlProjection.activeLayer,
1176
+ underlyingLayer: controlProjection.underlyingLayer,
1090
1177
  };
1091
1178
  }
1092
1179
  function updateTargetPage(model, targetPageId, targetMsg) {
@@ -1173,10 +1260,16 @@ export function createFramedApp(options) {
1173
1260
  activePageId: defaultPageId,
1174
1261
  pageOrder,
1175
1262
  pageModels,
1263
+ warnedFrameKeyCollisionPages: {},
1176
1264
  focusedPaneByPage: {},
1177
1265
  scrollByPage: {},
1178
1266
  columns: Math.max(1, options.initialColumns ?? 80),
1179
1267
  rows: Math.max(1, options.initialRows ?? 24),
1268
+ frameTimeMs: 0,
1269
+ viewTimeMs: 0,
1270
+ diffTimeMs: 0,
1271
+ frameBudgetMs: undefined,
1272
+ frameOverBudget: false,
1180
1273
  helpOpen: false,
1181
1274
  helpScrollY: 0,
1182
1275
  commandPaletteKind: undefined,
@@ -1196,6 +1289,13 @@ export function createFramedApp(options) {
1196
1289
  runtimeNotifications: createNotificationState(),
1197
1290
  runtimeNotificationHistoryFilter: 'ALL',
1198
1291
  runtimeNotificationLoopActive: false,
1292
+ activeShellThemeId: undefined,
1293
+ };
1294
+ ensureResolvedShellThemes(resolveFrameCtx());
1295
+ const initialShellTheme = resolveShellThemeForContext(resolvedShellThemes, resolveFrameCtx())
1296
+ ?? resolvedShellThemes[0];
1297
+ model = {
1298
+ ...model,
1199
1299
  activeShellThemeId: initialShellTheme?.id,
1200
1300
  };
1201
1301
  if (initialShellTheme != null) {
@@ -1236,6 +1336,20 @@ export function createFramedApp(options) {
1236
1336
  }, action.issue.atMs);
1237
1337
  return applyFrameNotificationState(model, notifications, action.issue.atMs);
1238
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
+ }
1239
1353
  if (action.type === 'notification-tick') {
1240
1354
  const notifications = tickNotifications(model.runtimeNotifications, action.atMs);
1241
1355
  return applyFrameNotificationState(model, notifications, action.atMs, true);
@@ -1316,7 +1430,7 @@ export function createFramedApp(options) {
1316
1430
  },
1317
1431
  view(model) {
1318
1432
  const themedFrameCtx = resolveFrameThemeCtx(model.activeShellThemeId);
1319
- const { activePage, layerStack, activeLayer, } = resolvePresentedLayerContext(model);
1433
+ const { controlProjection, layerStack, activeLayer, } = resolvePresentedLayerContext(model);
1320
1434
  const headerResult = resolveHeaderLine(model, options, pagesById, headerScratch, themedFrameCtx);
1321
1435
  headerScratch = headerResult.surface;
1322
1436
  const header = headerResult.surface;
@@ -1354,6 +1468,7 @@ export function createFramedApp(options) {
1354
1468
  ? renderMaximizedPaneInto(model.activePageId, model, bodyRect, pagesById, maximizedPaneId, frameSurface, bodyRect.row, bodyRect.col, paneScratchPool, themedFrameCtx)
1355
1469
  : renderPageContentInto(model.activePageId, model, bodyRect, pagesById, frameSurface, bodyRect.row, bodyRect.col, paneScratchPool, themedFrameCtx);
1356
1470
  }
1471
+ rememberWorkspaceLayout(model, activeResult.paneRects, headerResult.tabTargets);
1357
1472
  const overlays = [];
1358
1473
  if (options.overlayFactory != null) {
1359
1474
  overlays.push(...options.overlayFactory({
@@ -1388,7 +1503,11 @@ export function createFramedApp(options) {
1388
1503
  }
1389
1504
  }
1390
1505
  if (model.helpOpen) {
1391
- const helpOverlay = renderHelpOverlay(model, activePage, frameKeys, paletteKeys, options, pagesById, resolvedShellThemes);
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);
1392
1511
  overlays.push(modal({
1393
1512
  title: activeLayer.kind === 'help'
1394
1513
  ? (activeLayer.title ?? frameMessage(options.i18n, 'help.title', 'Keyboard Help'))
@@ -1443,7 +1562,67 @@ export function createFramedApp(options) {
1443
1562
  return wrapFrameMsg({ type: 'runtime-issue', issue });
1444
1563
  },
1445
1564
  };
1446
- 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);
1447
1626
  }
1448
1627
  function focusPane(model, paneId) {
1449
1628
  if (model.focusedPaneByPage[model.activePageId] === paneId)
@@ -1459,431 +1638,4 @@ function focusPane(model, paneId) {
1459
1638
  function resolveBodyRect(model, options) {
1460
1639
  return frameBodyRect(model.columns, model.rows, options.bodyTopRows ?? 1, options.bodyBottomRows ?? 1);
1461
1640
  }
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
1641
  //# sourceMappingURL=app-frame.js.map