@flyingrobots/bijou-tui 3.1.0 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/LICENSE +159 -21
  2. package/README.md +205 -39
  3. package/dist/app-frame-actions.d.ts +5 -5
  4. package/dist/app-frame-actions.d.ts.map +1 -1
  5. package/dist/app-frame-actions.js +74 -9
  6. package/dist/app-frame-actions.js.map +1 -1
  7. package/dist/app-frame-i18n.d.ts +12 -0
  8. package/dist/app-frame-i18n.d.ts.map +1 -0
  9. package/dist/app-frame-i18n.js +92 -0
  10. package/dist/app-frame-i18n.js.map +1 -0
  11. package/dist/app-frame-layers.d.ts +29 -0
  12. package/dist/app-frame-layers.d.ts.map +1 -0
  13. package/dist/app-frame-layers.js +104 -0
  14. package/dist/app-frame-layers.js.map +1 -0
  15. package/dist/app-frame-palette.d.ts +6 -2
  16. package/dist/app-frame-palette.d.ts.map +1 -1
  17. package/dist/app-frame-palette.js +42 -10
  18. package/dist/app-frame-palette.js.map +1 -1
  19. package/dist/app-frame-render.d.ts +28 -14
  20. package/dist/app-frame-render.d.ts.map +1 -1
  21. package/dist/app-frame-render.js +285 -138
  22. package/dist/app-frame-render.js.map +1 -1
  23. package/dist/app-frame-types.d.ts +41 -21
  24. package/dist/app-frame-types.d.ts.map +1 -1
  25. package/dist/app-frame-types.js +8 -6
  26. package/dist/app-frame-types.js.map +1 -1
  27. package/dist/app-frame-utils.d.ts +8 -3
  28. package/dist/app-frame-utils.d.ts.map +1 -1
  29. package/dist/app-frame-utils.js +42 -27
  30. package/dist/app-frame-utils.js.map +1 -1
  31. package/dist/app-frame.d.ts +128 -12
  32. package/dist/app-frame.d.ts.map +1 -1
  33. package/dist/app-frame.js +1212 -91
  34. package/dist/app-frame.js.map +1 -1
  35. package/dist/browsable-list.d.ts +20 -1
  36. package/dist/browsable-list.d.ts.map +1 -1
  37. package/dist/browsable-list.js +48 -10
  38. package/dist/browsable-list.js.map +1 -1
  39. package/dist/collection-surface.d.ts +8 -0
  40. package/dist/collection-surface.d.ts.map +1 -0
  41. package/dist/collection-surface.js +41 -0
  42. package/dist/collection-surface.js.map +1 -0
  43. package/dist/command-palette.d.ts +17 -1
  44. package/dist/command-palette.d.ts.map +1 -1
  45. package/dist/command-palette.js +50 -20
  46. package/dist/command-palette.js.map +1 -1
  47. package/dist/commands.js +1 -1
  48. package/dist/commands.js.map +1 -1
  49. package/dist/css/text-style.d.ts +2 -1
  50. package/dist/css/text-style.d.ts.map +1 -1
  51. package/dist/css/text-style.js +33 -0
  52. package/dist/css/text-style.js.map +1 -1
  53. package/dist/design-language.d.ts +49 -0
  54. package/dist/design-language.d.ts.map +1 -0
  55. package/dist/design-language.js +70 -0
  56. package/dist/design-language.js.map +1 -0
  57. package/dist/driver.d.ts +6 -1
  58. package/dist/driver.d.ts.map +1 -1
  59. package/dist/driver.js +3 -0
  60. package/dist/driver.js.map +1 -1
  61. package/dist/eventbus.d.ts.map +1 -1
  62. package/dist/eventbus.js +21 -1
  63. package/dist/eventbus.js.map +1 -1
  64. package/dist/file-picker.d.ts +19 -1
  65. package/dist/file-picker.d.ts.map +1 -1
  66. package/dist/file-picker.js +47 -20
  67. package/dist/file-picker.js.map +1 -1
  68. package/dist/flex.d.ts +35 -1
  69. package/dist/flex.d.ts.map +1 -1
  70. package/dist/flex.js +127 -1
  71. package/dist/flex.js.map +1 -1
  72. package/dist/focus-area.d.ts +20 -1
  73. package/dist/focus-area.d.ts.map +1 -1
  74. package/dist/focus-area.js +107 -12
  75. package/dist/focus-area.js.map +1 -1
  76. package/dist/grid.d.ts +10 -0
  77. package/dist/grid.d.ts.map +1 -1
  78. package/dist/grid.js +25 -0
  79. package/dist/grid.js.map +1 -1
  80. package/dist/help.d.ts +41 -0
  81. package/dist/help.d.ts.map +1 -1
  82. package/dist/help.js +50 -0
  83. package/dist/help.js.map +1 -1
  84. package/dist/index.d.ts +23 -17
  85. package/dist/index.d.ts.map +1 -1
  86. package/dist/index.js +22 -16
  87. package/dist/index.js.map +1 -1
  88. package/dist/inspector-drawer.d.ts +36 -0
  89. package/dist/inspector-drawer.d.ts.map +1 -0
  90. package/dist/inspector-drawer.js +68 -0
  91. package/dist/inspector-drawer.js.map +1 -0
  92. package/dist/layout-node-surface.d.ts +17 -0
  93. package/dist/layout-node-surface.d.ts.map +1 -0
  94. package/dist/layout-node-surface.js +61 -0
  95. package/dist/layout-node-surface.js.map +1 -0
  96. package/dist/navigable-table.d.ts +16 -1
  97. package/dist/navigable-table.d.ts.map +1 -1
  98. package/dist/navigable-table.js +32 -1
  99. package/dist/navigable-table.js.map +1 -1
  100. package/dist/notification.d.ts +26 -1
  101. package/dist/notification.d.ts.map +1 -1
  102. package/dist/notification.js +294 -41
  103. package/dist/notification.js.map +1 -1
  104. package/dist/overlay.d.ts +29 -8
  105. package/dist/overlay.d.ts.map +1 -1
  106. package/dist/overlay.js +248 -137
  107. package/dist/overlay.js.map +1 -1
  108. package/dist/pager.d.ts +16 -0
  109. package/dist/pager.d.ts.map +1 -1
  110. package/dist/pager.js +61 -1
  111. package/dist/pager.js.map +1 -1
  112. package/dist/runtime-engine.d.ts +223 -0
  113. package/dist/runtime-engine.d.ts.map +1 -0
  114. package/dist/runtime-engine.js +457 -0
  115. package/dist/runtime-engine.js.map +1 -0
  116. package/dist/runtime.d.ts.map +1 -1
  117. package/dist/runtime.js +33 -19
  118. package/dist/runtime.js.map +1 -1
  119. package/dist/shell-quit.d.ts +12 -0
  120. package/dist/shell-quit.d.ts.map +1 -0
  121. package/dist/shell-quit.js +36 -0
  122. package/dist/shell-quit.js.map +1 -0
  123. package/dist/split-pane.d.ts +12 -1
  124. package/dist/split-pane.d.ts.map +1 -1
  125. package/dist/split-pane.js +31 -1
  126. package/dist/split-pane.js.map +1 -1
  127. package/dist/status-bar.d.ts +12 -0
  128. package/dist/status-bar.d.ts.map +1 -1
  129. package/dist/status-bar.js +45 -16
  130. package/dist/status-bar.js.map +1 -1
  131. package/dist/subapp/mount.d.ts.map +1 -1
  132. package/dist/subapp/mount.js +3 -0
  133. package/dist/subapp/mount.js.map +1 -1
  134. package/dist/surface-layout.d.ts +19 -0
  135. package/dist/surface-layout.d.ts.map +1 -0
  136. package/dist/surface-layout.js +87 -0
  137. package/dist/surface-layout.js.map +1 -0
  138. package/dist/transition-shaders.d.ts +10 -8
  139. package/dist/transition-shaders.d.ts.map +1 -1
  140. package/dist/transition-shaders.js +65 -19
  141. package/dist/transition-shaders.js.map +1 -1
  142. package/dist/types.d.ts +21 -7
  143. package/dist/types.d.ts.map +1 -1
  144. package/dist/types.js +11 -0
  145. package/dist/types.js.map +1 -1
  146. package/dist/view-output.d.ts +5 -4
  147. package/dist/view-output.d.ts.map +1 -1
  148. package/dist/view-output.js +37 -29
  149. package/dist/view-output.js.map +1 -1
  150. package/dist/viewport.d.ts +30 -1
  151. package/dist/viewport.d.ts.map +1 -1
  152. package/dist/viewport.js +77 -1
  153. package/dist/viewport.js.map +1 -1
  154. package/package.json +6 -3
  155. package/dist/layout-v3.d.ts +0 -10
  156. package/dist/layout-v3.d.ts.map +0 -1
  157. package/dist/layout-v3.js +0 -35
  158. package/dist/layout-v3.js.map +0 -1
package/dist/app-frame.js CHANGED
@@ -4,25 +4,99 @@
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, parseAnsiToSurface, resolveSafeCtx, } from '@flyingrobots/bijou';
8
- import { helpView } from './help.js';
7
+ import { createSurface, preparePreferenceSections, preferenceListSurface, resolvePreferenceRowLayout, resolveClock, resolveSafeCtx, } from '@flyingrobots/bijou';
8
+ import { helpViewSurface } from './help.js';
9
+ import { createKeyMap } from './keybindings.js';
9
10
  import { isKeyMsg, isMouseMsg, isResizeMsg } from './types.js';
10
- import { modal } from './overlay.js';
11
- import { fitBlock } from './layout-utils.js';
11
+ import { quit } from './commands.js';
12
+ import { compositeSurfaceInto, drawer, modal } from './overlay.js';
13
+ import { isShellQuitConfirmAccept, isShellQuitConfirmDismiss, isShellQuitRequest, renderShellQuitOverlay, shouldUseShellQuitConfirm, } from './shell-quit.js';
12
14
  import { commandPalette, commandPaletteKeyMap, } from './command-palette.js';
15
+ import { createPagerStateForSurface, pagerPageDown, pagerPageUp, pagerScrollBy, pagerScrollToBottom, pagerScrollToTop, pagerSurface, } from './pager.js';
13
16
  import { restoreLayoutState } from './layout-preset.js';
14
- import { createNotificationState, notificationsNeedTick, pushNotification, renderNotificationStack, tickNotifications, trimNotificationsToViewport, } from './notification.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';
15
20
  import { isFrameScopedMsg, isPageScopedMsg, wrapCmdForPage, emitMsg, emitMsgForPage, wrapFrameMsg, } from './app-frame-types.js';
16
21
  import { createFrameKeyMap, frameBodyRect, mergeBindingSources, } from './app-frame-utils.js';
17
- import { renderHeaderLine, renderHelpLine, renderPageContent, renderMaximizedPane, renderTransition, } from './app-frame-render.js';
18
- import { applyFrameAction, syncPageFrameState, } from './app-frame-actions.js';
19
- import { handlePaletteKey, openCommandPalette, } from './app-frame-palette.js';
20
- import { visibleLength } from './viewport.js';
22
+ import { activeFrameLayer, describeFrameLayerStack, } from './app-frame-layers.js';
23
+ import { frameEndAnchor, frameMessage, frameNotificationCue, frameNotificationFilterLabel, frameStartAnchor, } from './app-frame-i18n.js';
24
+ import { resolveHeaderLine, renderHelpLine, renderPageContent, renderPageContentInto, renderMaximizedPane, renderMaximizedPaneInto, renderTransition, } from './app-frame-render.js';
25
+ import { applyFrameAction, scrollFocusedPane, switchTab, syncPageFrameState, } from './app-frame-actions.js';
26
+ import { handlePaletteKey, openCommandPalette, openSearchPalette, } from './app-frame-palette.js';
27
+ export { activeFrameLayer, describeFrameLayerStack, underlyingFrameLayer, } from './app-frame-layers.js';
21
28
  // ---------------------------------------------------------------------------
22
29
  // Frame Notification Helpers
23
30
  // ---------------------------------------------------------------------------
24
31
  const FRAME_NOTIFICATION_TICK_MS = 40;
25
32
  const DEFAULT_FRAME_NOTIFICATION_DURATION_MS = 6_000;
33
+ const SETTINGS_FEEDBACK_TOAST_WIDTH = 40;
34
+ const DEFAULT_NOTIFICATION_CENTER_FILTERS = [
35
+ 'ALL',
36
+ 'ACTIONABLE',
37
+ 'ERROR',
38
+ 'WARNING',
39
+ 'SUCCESS',
40
+ 'INFO',
41
+ ];
42
+ const quitHelpKeys = createKeyMap()
43
+ .group('Exit', (g) => g
44
+ .bind('q', 'Quit', { type: 'toggle-help' })
45
+ .bind('escape', 'Quit', { type: 'toggle-help' })
46
+ .bind('ctrl+c', 'Quit', { type: 'toggle-help' }));
47
+ const helpLayerHelpKeys = createKeyMap()
48
+ .group('Help', (g) => g
49
+ .bind('escape', 'Close help', { type: 'noop' })
50
+ .bind('?', 'Close help', { type: 'noop' })
51
+ .bind('up', 'Scroll up', { type: 'noop' })
52
+ .bind('down', 'Scroll down', { type: 'noop' })
53
+ .bind('j', 'Scroll down', { type: 'noop' })
54
+ .bind('k', 'Scroll up', { type: 'noop' })
55
+ .bind('d', 'Page down', { type: 'noop' })
56
+ .bind('u', 'Page up', { type: 'noop' })
57
+ .bind('g', 'Top', { type: 'noop' })
58
+ .bind('shift+g', 'Bottom', { type: 'noop' }));
59
+ const settingsHelpKeys = createKeyMap()
60
+ .group('Settings', (g) => g
61
+ .bind('escape', 'Close settings', { type: 'toggle-settings' })
62
+ .bind('f2', 'Close settings', { type: 'toggle-settings' })
63
+ .bind('up', 'Previous row', { type: 'scroll-up' })
64
+ .bind('down', 'Next row', { type: 'scroll-down' })
65
+ .bind('enter', 'Activate setting', { type: 'toggle-settings' })
66
+ .bind('space', 'Activate setting', { type: 'toggle-settings' })
67
+ .bind('j', 'Scroll down', { type: 'scroll-down' })
68
+ .bind('k', 'Scroll up', { type: 'scroll-up' })
69
+ .bind('d', 'Page down', { type: 'page-down' })
70
+ .bind('u', 'Page up', { type: 'page-up' })
71
+ .bind('g', 'Top', { type: 'top' })
72
+ .bind('shift+g', 'Bottom', { type: 'bottom' })
73
+ .bind('/', 'Search', { type: 'open-search' })
74
+ .bind('ctrl+p', 'Open command palette', { type: 'open-palette' })
75
+ .bind(':', 'Open command palette', { type: 'open-palette' })
76
+ .bind('?', 'Toggle help', { type: 'toggle-help' }));
77
+ const notificationCenterHelpKeys = createKeyMap()
78
+ .group('Notifications', (g) => g
79
+ .bind('shift+n', 'Close notification center', { type: 'noop' })
80
+ .bind('up', 'Scroll up', { type: 'noop' })
81
+ .bind('down', 'Scroll down', { type: 'noop' })
82
+ .bind('j', 'Scroll down', { type: 'noop' })
83
+ .bind('k', 'Scroll up', { type: 'noop' })
84
+ .bind('d', 'Page down', { type: 'noop' })
85
+ .bind('u', 'Page up', { type: 'noop' })
86
+ .bind('g', 'Top', { type: 'noop' })
87
+ .bind('shift+g', 'Bottom', { type: 'noop' })
88
+ .bind('f', 'Cycle filter', { type: 'noop' })
89
+ .bind('/', 'Search', { type: 'noop' })
90
+ .bind('ctrl+p', 'Open command palette', { type: 'noop' })
91
+ .bind(':', 'Open command palette', { type: 'noop' })
92
+ .bind('?', 'Toggle help', { type: 'noop' }));
93
+ const quitConfirmHelpKeys = createKeyMap()
94
+ .group('Quit', (g) => g
95
+ .bind('y', 'Quit', { type: 'noop' })
96
+ .bind('enter', 'Quit', { type: 'noop' })
97
+ .bind('n', 'Stay', { type: 'noop' })
98
+ .bind('escape', 'Stay', { type: 'noop' })
99
+ .bind('q', 'Stay', { type: 'noop' }));
26
100
  function resolveFrameNotificationOptions(options) {
27
101
  if (options.runtimeNotifications === false) {
28
102
  return {
@@ -79,8 +153,13 @@ export function createFramedApp(options) {
79
153
  if (!pagesById.has(defaultPageId)) {
80
154
  throw new Error(`createFramedApp: defaultPageId "${defaultPageId}" not found in pages`);
81
155
  }
82
- const frameKeys = createFrameKeyMap();
156
+ const frameKeys = createFrameKeyMap({
157
+ enableSettings: options.settings != null,
158
+ enableNotifications: options.notificationCenter != null || options.runtimeNotifications !== false,
159
+ i18n: options.i18n,
160
+ });
83
161
  const frameNotificationOptions = resolveFrameNotificationOptions(options);
162
+ let composedFrameScratch = null;
84
163
  const paletteKeys = commandPaletteKeyMap({
85
164
  focusNext: { type: 'cp-next' },
86
165
  focusPrev: { type: 'cp-prev' },
@@ -89,12 +168,301 @@ export function createFramedApp(options) {
89
168
  select: { type: 'cp-select' },
90
169
  close: { type: 'cp-close' },
91
170
  });
171
+ function getComposedFrameScratch(width, height) {
172
+ if (composedFrameScratch == null
173
+ || composedFrameScratch.width !== width
174
+ || composedFrameScratch.height !== height) {
175
+ composedFrameScratch = createSurface(width, height);
176
+ }
177
+ return composedFrameScratch;
178
+ }
92
179
  function withObservedKey(model, cmds, msg, route) {
93
180
  const observed = options.observeKey?.(msg, route);
94
181
  if (observed === undefined)
95
182
  return [...cmds];
96
183
  return [emitMsgForPage(model.activePageId, observed), ...cmds];
97
184
  }
185
+ function applyQuitRequest(model, msg) {
186
+ if (!shouldUseShellQuitConfirm()) {
187
+ return [model, withObservedKey(model, [quit()], msg, 'frame')];
188
+ }
189
+ if (model.quitConfirmOpen) {
190
+ return [model, withObservedKey(model, [], msg, 'frame')];
191
+ }
192
+ return [{
193
+ ...model,
194
+ quitConfirmOpen: true,
195
+ helpOpen: false,
196
+ helpScrollY: 0,
197
+ settingsOpen: false,
198
+ notificationCenterOpen: false,
199
+ commandPalette: undefined,
200
+ commandPaletteEntries: undefined,
201
+ commandPaletteTitle: undefined,
202
+ commandPaletteKind: undefined,
203
+ }, withObservedKey(model, [], msg, 'frame')];
204
+ }
205
+ function closeCommandPalette(model) {
206
+ return {
207
+ ...model,
208
+ commandPalette: undefined,
209
+ commandPaletteEntries: undefined,
210
+ commandPaletteTitle: undefined,
211
+ commandPaletteKind: undefined,
212
+ };
213
+ }
214
+ function resolveLayerContext(model) {
215
+ const activePage = pagesById.get(model.activePageId);
216
+ const activePageModel = model.pageModels[model.activePageId];
217
+ const inputAreas = resolveInputAreas(activePage, activePageModel);
218
+ const activeInputArea = findInputAreaByPaneId(inputAreas, model.focusedPaneByPage[model.activePageId]);
219
+ const modalKeyMap = activePage.modalKeyMap?.(activePageModel);
220
+ const pageModalOpen = modalKeyMap != null;
221
+ const activeLayer = activeFrameLayer(model, { pageModalOpen });
222
+ return {
223
+ activePage,
224
+ activePageModel,
225
+ inputAreas,
226
+ activeInputArea,
227
+ modalKeyMap,
228
+ pageModalOpen,
229
+ activeLayer,
230
+ };
231
+ }
232
+ function resolveWorkspaceHelpSource(activePage, activeInputArea) {
233
+ return mergeBindingSources(frameKeys, quitHelpKeys, options.globalKeys, activeInputArea?.helpSource ?? activeInputArea?.keyMap, activePage.helpSource ?? activePage.keyMap);
234
+ }
235
+ function resolveWorkspaceHintSource(model, activePage, activeInputArea) {
236
+ const helpLineOverride = options.helpLineSource?.({
237
+ model,
238
+ activePage,
239
+ frameKeys,
240
+ globalKeys: options.globalKeys,
241
+ });
242
+ if (typeof helpLineOverride === 'string') {
243
+ return helpLineOverride;
244
+ }
245
+ return helpLineOverride ?? mergeBindingSources(frameKeys, options.globalKeys, activeInputArea?.helpSource ?? activeInputArea?.keyMap, activePage.helpSource ?? activePage.keyMap);
246
+ }
247
+ function resolveLayerMetadata(model, activePage, activeInputArea, modalKeyMap) {
248
+ const settings = resolveFrameSettings(model, options, pagesById);
249
+ const notificationCenter = resolveFrameNotificationCenter(model, options, pagesById);
250
+ const workspaceHintSource = resolveWorkspaceHintSource(model, activePage, activeInputArea);
251
+ const workspaceHelpSource = resolveWorkspaceHelpSource(activePage, activeInputArea);
252
+ const paletteHint = frameMessage(options.i18n, 'palette.hint', 'Enter select • Esc close');
253
+ const helpHint = frameMessage(options.i18n, 'help.hint', 'j/k scroll • d/u page • g/G top/bottom • mouse wheel • ?/Esc close');
254
+ const settingsHint = frameMessage(options.i18n, 'settings.footer', 'F2/Esc close • ↑/↓ rows • Enter toggle • / search • q quit');
255
+ const notificationsHint = frameMessage(options.i18n, 'notifications.footer', 'Shift+N close • f filter • j/k scroll • q quit');
256
+ const quitHint = frameMessage(options.i18n, 'quit.footer', 'Y quit • N stay');
257
+ const paletteTitle = model.commandPaletteTitle
258
+ ?? frameMessage(options.i18n, 'palette.title', 'Command Palette');
259
+ const searchTitle = model.commandPaletteTitle
260
+ ?? activePage.searchTitle
261
+ ?? frameMessage(options.i18n, 'search.title', 'Search');
262
+ const notificationsTitle = notificationCenter == null
263
+ ? frameMessage(options.i18n, 'notifications.title', 'Notifications')
264
+ : `${notificationCenter.title} • ${frameNotificationFilterLabel(options.i18n, notificationCenter.activeFilter)}`;
265
+ return {
266
+ workspace: {
267
+ title: activePage.title,
268
+ hintSource: workspaceHintSource,
269
+ helpSource: workspaceHelpSource,
270
+ },
271
+ 'page-modal': {
272
+ title: activePage.title,
273
+ hintSource: modalKeyMap ?? activePage.helpSource ?? activePage.keyMap,
274
+ helpSource: mergeBindingSources(quitHelpKeys, modalKeyMap, activePage.helpSource ?? activePage.keyMap),
275
+ },
276
+ settings: {
277
+ title: settings?.title ?? frameMessage(options.i18n, 'settings.title', 'Settings'),
278
+ hintSource: settingsHint,
279
+ helpSource: mergeBindingSources(settingsHelpKeys, quitHelpKeys),
280
+ },
281
+ help: {
282
+ title: frameMessage(options.i18n, 'help.title', 'Keyboard Help'),
283
+ hintSource: helpHint,
284
+ helpSource: helpLayerHelpKeys,
285
+ },
286
+ 'notification-center': {
287
+ title: notificationsTitle,
288
+ hintSource: notificationsHint,
289
+ helpSource: mergeBindingSources(notificationCenterHelpKeys, quitHelpKeys),
290
+ },
291
+ search: {
292
+ title: searchTitle,
293
+ hintSource: paletteHint,
294
+ helpSource: mergeBindingSources(paletteKeys, quitHelpKeys),
295
+ },
296
+ 'command-palette': {
297
+ title: paletteTitle,
298
+ hintSource: paletteHint,
299
+ helpSource: mergeBindingSources(paletteKeys, quitHelpKeys),
300
+ },
301
+ 'quit-confirm': {
302
+ title: frameMessage(options.i18n, 'quit.title', 'Quit?'),
303
+ hintSource: quitHint,
304
+ helpSource: quitConfirmHelpKeys,
305
+ },
306
+ };
307
+ }
308
+ function resolvePresentedLayerContext(model) {
309
+ const { activePage, activePageModel, inputAreas, activeInputArea, modalKeyMap, pageModalOpen, } = resolveLayerContext(model);
310
+ const layerStack = describeFrameLayerStack(model, {
311
+ pageModalOpen,
312
+ layers: resolveLayerMetadata(model, activePage, activeInputArea, modalKeyMap),
313
+ });
314
+ const activeLayer = layerStack[layerStack.length - 1];
315
+ const underlyingLayer = layerStack.length > 1 ? layerStack[layerStack.length - 2] : undefined;
316
+ return {
317
+ activePage,
318
+ activePageModel,
319
+ inputAreas,
320
+ activeInputArea,
321
+ modalKeyMap,
322
+ pageModalOpen,
323
+ layerStack,
324
+ activeLayer,
325
+ underlyingLayer,
326
+ };
327
+ }
328
+ function updateTargetPage(model, targetPageId, targetMsg) {
329
+ const targetPage = pagesById.get(targetPageId);
330
+ if (targetPage == null)
331
+ return [model, []];
332
+ const pageModel = model.pageModels[targetPageId];
333
+ const updateResult = targetPage.update(targetMsg, pageModel);
334
+ const [nextPageModel, cmds = []] = updateResult;
335
+ const nextModels = { ...model.pageModels, [targetPageId]: nextPageModel };
336
+ const synced = syncPageFrameState({ ...model, pageModels: nextModels }, targetPageId, pagesById);
337
+ const wrappedCmds = cmds.map((cmd) => wrapCmdForPage(targetPageId, cmd));
338
+ return [synced, wrappedCmds];
339
+ }
340
+ function handleFrameMouse(msg, model) {
341
+ const { activePage, activePageModel, inputAreas, activeLayer, } = resolveLayerContext(model);
342
+ if (activeLayer.kind === 'help') {
343
+ if (msg.action === 'scroll-up' || msg.action === 'scroll-down') {
344
+ return [applyHelpScroll(model, activePage, msg.action === 'scroll-down' ? 3 : -3, frameKeys, paletteKeys, options, pagesById), []];
345
+ }
346
+ return [model, []];
347
+ }
348
+ if (activeLayer.kind === 'search' || activeLayer.kind === 'command-palette') {
349
+ return [model, []];
350
+ }
351
+ if (activeLayer.kind === 'quit-confirm' || activeLayer.kind === 'page-modal') {
352
+ return [model, []];
353
+ }
354
+ if (activeLayer.kind === 'settings') {
355
+ const layout = resolveSettingsLayout(model, options, pagesById);
356
+ if (layout != null) {
357
+ if (msg.action === 'scroll-up' || msg.action === 'scroll-down') {
358
+ if (isInsideSettingsDrawer(msg.col, msg.row, layout, model)) {
359
+ return [
360
+ scrollSettingsBy(model, layout, msg.action === 'scroll-down' ? 3 : -3),
361
+ [],
362
+ ];
363
+ }
364
+ return [model, []];
365
+ }
366
+ if (msg.action === 'press' && msg.button === 'left') {
367
+ if (!isInsideSettingsDrawer(msg.col, msg.row, layout, model)) {
368
+ return [model, []];
369
+ }
370
+ const hit = settingsRowAtPosition(msg.col, msg.row, model, layout);
371
+ if (hit == null)
372
+ return [model, []];
373
+ const focusedModel = { ...model, settingsFocusIndex: hit.index };
374
+ if (hit.row.action === undefined || hit.row.enabled === false || hit.row.kind === 'info') {
375
+ return [focusedModel, []];
376
+ }
377
+ return activateSettingsRow(focusedModel, hit.row);
378
+ }
379
+ return [model, []];
380
+ }
381
+ }
382
+ if (activeLayer.kind === 'notification-center') {
383
+ const layout = resolveNotificationCenterLayout(model, options, pagesById);
384
+ if (layout != null) {
385
+ if (msg.action === 'scroll-up' || msg.action === 'scroll-down') {
386
+ if (isInsideNotificationCenterDrawer(msg.col, msg.row, layout, model)) {
387
+ return [
388
+ scrollNotificationCenterBy(model, layout, msg.action === 'scroll-down' ? 3 : -3),
389
+ [],
390
+ ];
391
+ }
392
+ return [model, []];
393
+ }
394
+ if (msg.action === 'press' && msg.button === 'left') {
395
+ return [model, []];
396
+ }
397
+ return [model, []];
398
+ }
399
+ }
400
+ if (msg.action === 'press' && msg.button === 'left') {
401
+ if (frameNotificationOptions.enabled) {
402
+ const nowMs = resolveClock(resolveSafeCtx()).now();
403
+ const notificationTarget = hitTestNotificationStack(model.runtimeNotifications, {
404
+ screenWidth: model.columns,
405
+ screenHeight: model.rows,
406
+ margin: frameNotificationOptions.margin,
407
+ gap: frameNotificationOptions.gap,
408
+ ctx: resolveSafeCtx() ?? undefined,
409
+ }, msg.col, msg.row);
410
+ if (notificationTarget?.kind === 'dismiss') {
411
+ return applyFrameNotificationState(model, dismissNotification(model.runtimeNotifications, notificationTarget.item.id, nowMs), nowMs);
412
+ }
413
+ if (notificationTarget != null) {
414
+ return [model, []];
415
+ }
416
+ }
417
+ if (msg.row === 0) {
418
+ const header = resolveHeaderLine(model, options, pagesById);
419
+ const tab = header.tabTargets.find((target) => msg.col >= target.startCol && msg.col <= target.endCol);
420
+ if (tab != null) {
421
+ const currentIndex = model.pageOrder.indexOf(model.activePageId);
422
+ const nextIndex = model.pageOrder.indexOf(tab.pageId);
423
+ if (currentIndex >= 0 && nextIndex >= 0 && nextIndex !== currentIndex) {
424
+ return switchTab(model, nextIndex - currentIndex, pagesById, options);
425
+ }
426
+ return [model, []];
427
+ }
428
+ return [model, []];
429
+ }
430
+ const clickedPane = paneHitAtPosition(model, msg.col, msg.row, pagesById, options);
431
+ if (clickedPane != null) {
432
+ const focusedModel = focusPane(model, clickedPane.paneId);
433
+ const inputArea = findInputAreaByPaneId(inputAreas, clickedPane.paneId);
434
+ const areaMsg = inputArea?.mouse?.({
435
+ msg,
436
+ model: activePageModel,
437
+ rect: clickedPane.rect,
438
+ });
439
+ if (areaMsg !== undefined) {
440
+ return [focusedModel, [emitMsgForPage(model.activePageId, areaMsg)]];
441
+ }
442
+ return [focusedModel, [emitMsgForPage(model.activePageId, msg)]];
443
+ }
444
+ }
445
+ if (msg.action === 'scroll-up' || msg.action === 'scroll-down') {
446
+ const hoveredPane = paneHitAtPosition(model, msg.col, msg.row, pagesById, options);
447
+ if (hoveredPane != null) {
448
+ const focusedModel = focusPane(model, hoveredPane.paneId);
449
+ const inputArea = findInputAreaByPaneId(inputAreas, hoveredPane.paneId);
450
+ const areaMsg = inputArea?.mouse?.({
451
+ msg,
452
+ model: activePageModel,
453
+ rect: hoveredPane.rect,
454
+ });
455
+ if (areaMsg !== undefined) {
456
+ return [focusedModel, [emitMsgForPage(model.activePageId, areaMsg)]];
457
+ }
458
+ const action = msg.action === 'scroll-down'
459
+ ? { type: 'scroll-down' }
460
+ : { type: 'scroll-up' };
461
+ return [scrollFocusedPane(focusedModel, action, pagesById, options), []];
462
+ }
463
+ }
464
+ return undefined;
465
+ }
98
466
  function applyFrameNotificationState(model, notifications, nowMs, forceTick = false) {
99
467
  const trimmed = trimNotificationsToViewport(notifications, {
100
468
  screenWidth: model.columns,
@@ -113,6 +481,32 @@ export function createFramedApp(options) {
113
481
  }
114
482
  return [nextModel, []];
115
483
  }
484
+ function activateSettingsRow(model, row) {
485
+ if (row.action === undefined || row.enabled === false || row.kind === 'info') {
486
+ return [model, []];
487
+ }
488
+ const cmds = [emitMsgForPage(model.activePageId, row.action)];
489
+ if (!frameNotificationOptions.enabled) {
490
+ return [model, cmds];
491
+ }
492
+ const feedback = row.feedback ?? {
493
+ title: 'Setting updated',
494
+ message: `${row.label} updated.`,
495
+ };
496
+ const nowMs = resolveClock(resolveSafeCtx()).now();
497
+ const notifications = pushNotification(model.runtimeNotifications, {
498
+ title: feedback.title ?? 'Setting updated',
499
+ message: feedback.message,
500
+ variant: 'TOAST',
501
+ tone: feedback.tone ?? 'INFO',
502
+ width: SETTINGS_FEEDBACK_TOAST_WIDTH,
503
+ placement: frameNotificationOptions.placement,
504
+ durationMs: feedback.durationMs ?? 2_500,
505
+ overflow: frameNotificationOptions.overflow,
506
+ }, nowMs);
507
+ const [nextModel, notificationCmds] = applyFrameNotificationState(model, notifications, nowMs);
508
+ return [nextModel, [...cmds, ...notificationCmds]];
509
+ }
116
510
  const app = {
117
511
  init() {
118
512
  const pageModels = {};
@@ -131,6 +525,14 @@ export function createFramedApp(options) {
131
525
  columns: Math.max(1, options.initialColumns ?? 80),
132
526
  rows: Math.max(1, options.initialRows ?? 24),
133
527
  helpOpen: false,
528
+ helpScrollY: 0,
529
+ commandPaletteKind: undefined,
530
+ settingsOpen: false,
531
+ notificationCenterOpen: false,
532
+ quitConfirmOpen: false,
533
+ settingsFocusIndex: 0,
534
+ settingsScrollY: 0,
535
+ notificationCenterScrollY: 0,
134
536
  transitionProgress: 1,
135
537
  transitionGeneration: 0,
136
538
  transitionFrame: 0,
@@ -139,6 +541,7 @@ export function createFramedApp(options) {
139
541
  dockStateByPage: {},
140
542
  splitRatioOverrides: {},
141
543
  runtimeNotifications: createNotificationState(),
544
+ runtimeNotificationHistoryFilter: 'ALL',
142
545
  runtimeNotificationLoopActive: false,
143
546
  };
144
547
  for (const pageId of pageOrder) {
@@ -239,23 +642,226 @@ export function createFramedApp(options) {
239
642
  }, []];
240
643
  }
241
644
  if (isKeyMsg(msg)) {
242
- if (model.commandPalette != null) {
645
+ const { activePage, activeInputArea, modalKeyMap, activeLayer, } = resolveLayerContext(model);
646
+ if (activeLayer.kind === 'search' || activeLayer.kind === 'command-palette') {
647
+ if (msg.ctrl && !msg.alt && msg.key === 'c') {
648
+ return applyQuitRequest(model, msg);
649
+ }
650
+ if (!msg.ctrl && !msg.alt && !msg.shift && msg.key === 'escape') {
651
+ return [closeCommandPalette(model), withObservedKey(model, [], msg, 'palette')];
652
+ }
653
+ const frameAction = frameKeys.handle(msg);
654
+ if (frameAction?.type === 'open-search') {
655
+ if (activeLayer.kind === 'search') {
656
+ return [closeCommandPalette(model), withObservedKey(model, [], msg, 'palette')];
657
+ }
658
+ return [openSearchPalette(model, frameKeys, options, pagesById), withObservedKey(model, [], msg, 'palette')];
659
+ }
660
+ if (frameAction?.type === 'open-palette') {
661
+ if (activeLayer.kind === 'command-palette') {
662
+ return [closeCommandPalette(model), withObservedKey(model, [], msg, 'palette')];
663
+ }
664
+ return [openCommandPalette(model, frameKeys, options, pagesById), withObservedKey(model, [], msg, 'palette')];
665
+ }
666
+ if (frameAction?.type === 'toggle-notifications') {
667
+ const [nextModel, cmds] = applyFrameAction(frameAction, closeCommandPalette(model), options, pagesById);
668
+ return [nextModel, withObservedKey(model, cmds, msg, 'palette')];
669
+ }
243
670
  const [nextModel, cmds] = handlePaletteKey(msg, model, paletteKeys, options, pagesById);
244
671
  return [nextModel, withObservedKey(model, cmds, msg, 'palette')];
245
672
  }
246
- // Help acts as a modal layer when open: only close keys are handled.
247
- if (model.helpOpen) {
248
- if (!msg.ctrl && !msg.alt && (msg.key === 'escape' || msg.key === '?')) {
249
- return [{ ...model, helpOpen: false }, withObservedKey(model, [], msg, 'help')];
673
+ if (activeLayer.kind === 'help') {
674
+ if (!msg.ctrl && !msg.alt && (msg.key === '?' || msg.key === 'escape')) {
675
+ return [{ ...model, helpOpen: false, helpScrollY: 0 }, withObservedKey(model, [], msg, 'help')];
676
+ }
677
+ if (isShellQuitRequest(msg)) {
678
+ return applyQuitRequest(model, msg);
679
+ }
680
+ const helpAction = frameKeys.handle(msg);
681
+ if (helpAction && isHelpScrollAction(helpAction)) {
682
+ return [
683
+ applyHelpScrollAction(model, activePage, helpAction, frameKeys, paletteKeys, options, pagesById),
684
+ withObservedKey(model, [], msg, 'help'),
685
+ ];
250
686
  }
251
687
  return [model, withObservedKey(model, [], msg, 'help')];
252
688
  }
253
- const activePage = pagesById.get(model.activePageId);
689
+ if (activeLayer.kind === 'settings') {
690
+ const layout = resolveSettingsLayout(model, options, pagesById);
691
+ if (layout != null) {
692
+ const settingsFrameAction = frameKeys.handle(msg);
693
+ if (!msg.ctrl && !msg.alt && msg.key === 'escape') {
694
+ return [{
695
+ ...model,
696
+ settingsOpen: false,
697
+ }, withObservedKey(model, [], msg, 'frame')];
698
+ }
699
+ if (msg.ctrl && !msg.alt && msg.key === ',') {
700
+ return [{
701
+ ...model,
702
+ settingsOpen: false,
703
+ }, withObservedKey(model, [], msg, 'frame')];
704
+ }
705
+ if (!msg.ctrl && !msg.alt && msg.key === 'f2') {
706
+ return [{
707
+ ...model,
708
+ settingsOpen: false,
709
+ }, withObservedKey(model, [], msg, 'frame')];
710
+ }
711
+ if (!msg.ctrl && !msg.alt && msg.key === '?') {
712
+ return [{ ...model, helpOpen: true }, withObservedKey(model, [], msg, 'frame')];
713
+ }
714
+ if (isShellQuitRequest(msg)) {
715
+ return applyQuitRequest(model, msg);
716
+ }
717
+ if (options.enableCommandPalette && !msg.ctrl && !msg.alt && msg.key === '/') {
718
+ return [openSearchPalette(model, frameKeys, options, pagesById), withObservedKey(model, [], msg, 'frame')];
719
+ }
720
+ if (options.enableCommandPalette && ((msg.ctrl && !msg.alt && msg.key === 'p') || (!msg.ctrl && !msg.alt && msg.key === ':'))) {
721
+ return [openCommandPalette(model, frameKeys, options, pagesById), withObservedKey(model, [], msg, 'frame')];
722
+ }
723
+ if (settingsFrameAction?.type === 'toggle-notifications') {
724
+ const [nextModel, cmds] = applyFrameAction(settingsFrameAction, model, options, pagesById);
725
+ return [nextModel, withObservedKey(model, cmds, msg, 'frame')];
726
+ }
727
+ if (!msg.ctrl && !msg.alt && msg.key === 'up') {
728
+ return [moveSettingsFocus(model, layout, -1), withObservedKey(model, [], msg, 'frame')];
729
+ }
730
+ if (!msg.ctrl && !msg.alt && msg.key === 'down') {
731
+ return [moveSettingsFocus(model, layout, 1), withObservedKey(model, [], msg, 'frame')];
732
+ }
733
+ if (!msg.ctrl && !msg.alt && msg.key === 'j') {
734
+ return [scrollSettingsBy(model, layout, 1), withObservedKey(model, [], msg, 'frame')];
735
+ }
736
+ if (!msg.ctrl && !msg.alt && msg.key === 'k') {
737
+ return [scrollSettingsBy(model, layout, -1), withObservedKey(model, [], msg, 'frame')];
738
+ }
739
+ if (!msg.ctrl && !msg.alt && msg.key === 'd') {
740
+ return [scrollSettingsBy(model, layout, Math.max(1, layout.contentHeight - 1)), withObservedKey(model, [], msg, 'frame')];
741
+ }
742
+ if (!msg.ctrl && !msg.alt && msg.key === 'u') {
743
+ return [scrollSettingsBy(model, layout, -Math.max(1, layout.contentHeight - 1)), withObservedKey(model, [], msg, 'frame')];
744
+ }
745
+ if (!msg.ctrl && !msg.alt && msg.key === 'g') {
746
+ return [{ ...model, settingsScrollY: 0 }, withObservedKey(model, [], msg, 'frame')];
747
+ }
748
+ if (!msg.ctrl && !msg.alt && msg.key === 'G') {
749
+ return [{ ...model, settingsScrollY: layout.maxScrollY }, withObservedKey(model, [], msg, 'frame')];
750
+ }
751
+ if (!msg.ctrl && !msg.alt && (msg.key === 'enter' || msg.key === 'space')) {
752
+ const row = layout.rows[clampSettingsFocus(model, layout)]?.row;
753
+ if (row?.action !== undefined && row.enabled !== false && row.kind !== 'info') {
754
+ const [nextModel, cmds] = activateSettingsRow(model, row);
755
+ return [nextModel, withObservedKey(model, cmds, msg, 'frame')];
756
+ }
757
+ return [model, withObservedKey(model, [], msg, 'frame')];
758
+ }
759
+ return [model, withObservedKey(model, [], msg, 'frame')];
760
+ }
761
+ }
762
+ if (activeLayer.kind === 'notification-center') {
763
+ const layout = resolveNotificationCenterLayout(model, options, pagesById);
764
+ if (layout != null) {
765
+ const centerFrameAction = frameKeys.handle(msg);
766
+ if (!msg.ctrl && !msg.alt && msg.key === 'escape') {
767
+ return [{
768
+ ...model,
769
+ notificationCenterOpen: false,
770
+ notificationCenterScrollY: 0,
771
+ }, withObservedKey(model, [], msg, 'frame')];
772
+ }
773
+ if (isShellQuitRequest(msg)) {
774
+ return applyQuitRequest(model, msg);
775
+ }
776
+ if (centerFrameAction?.type === 'toggle-notifications') {
777
+ const [nextModel, cmds] = applyFrameAction(centerFrameAction, model, options, pagesById);
778
+ return [nextModel, withObservedKey(model, cmds, msg, 'frame')];
779
+ }
780
+ if (!msg.ctrl && !msg.alt && msg.key === 'f2') {
781
+ const [nextModel, cmds] = applyFrameAction({ type: 'toggle-settings' }, model, options, pagesById);
782
+ return [nextModel, withObservedKey(model, cmds, msg, 'frame')];
783
+ }
784
+ if (!msg.ctrl && !msg.alt && msg.key === '?') {
785
+ return [{
786
+ ...model,
787
+ helpOpen: true,
788
+ notificationCenterOpen: false,
789
+ notificationCenterScrollY: 0,
790
+ }, withObservedKey(model, [], msg, 'frame')];
791
+ }
792
+ if (options.enableCommandPalette && !msg.ctrl && !msg.alt && msg.key === '/') {
793
+ return [openSearchPalette({
794
+ ...model,
795
+ notificationCenterOpen: false,
796
+ notificationCenterScrollY: 0,
797
+ }, frameKeys, options, pagesById), withObservedKey(model, [], msg, 'frame')];
798
+ }
799
+ if (options.enableCommandPalette && ((msg.ctrl && !msg.alt && msg.key === 'p') || (!msg.ctrl && !msg.alt && msg.key === ':'))) {
800
+ return [openCommandPalette({
801
+ ...model,
802
+ notificationCenterOpen: false,
803
+ notificationCenterScrollY: 0,
804
+ }, frameKeys, options, pagesById), withObservedKey(model, [], msg, 'frame')];
805
+ }
806
+ if (!msg.ctrl && !msg.alt && (msg.key === 'up' || msg.key === 'k')) {
807
+ return [scrollNotificationCenterBy(model, layout, -1), withObservedKey(model, [], msg, 'frame')];
808
+ }
809
+ if (!msg.ctrl && !msg.alt && (msg.key === 'down' || msg.key === 'j')) {
810
+ return [scrollNotificationCenterBy(model, layout, 1), withObservedKey(model, [], msg, 'frame')];
811
+ }
812
+ if (!msg.ctrl && !msg.alt && msg.key === 'd') {
813
+ return [scrollNotificationCenterBy(model, layout, Math.max(1, layout.contentHeight - 2)), withObservedKey(model, [], msg, 'frame')];
814
+ }
815
+ if (!msg.ctrl && !msg.alt && msg.key === 'u') {
816
+ return [scrollNotificationCenterBy(model, layout, -Math.max(1, layout.contentHeight - 2)), withObservedKey(model, [], msg, 'frame')];
817
+ }
818
+ if (!msg.ctrl && !msg.alt && msg.key === 'g') {
819
+ return [{ ...model, notificationCenterScrollY: 0 }, withObservedKey(model, [], msg, 'frame')];
820
+ }
821
+ if (!msg.ctrl && !msg.alt && msg.key === 'G') {
822
+ return [{ ...model, notificationCenterScrollY: layout.maxScrollY }, withObservedKey(model, [], msg, 'frame')];
823
+ }
824
+ if (!msg.ctrl && !msg.alt && msg.key === 'f') {
825
+ const [nextModel, cmds] = cycleNotificationCenterFilter(model, layout);
826
+ return [nextModel, withObservedKey(model, cmds, msg, 'frame')];
827
+ }
828
+ return [model, withObservedKey(model, [], msg, 'frame')];
829
+ }
830
+ }
831
+ if (activeLayer.kind === 'quit-confirm') {
832
+ if (isShellQuitConfirmAccept(msg)) {
833
+ return [{
834
+ ...model,
835
+ quitConfirmOpen: false,
836
+ }, withObservedKey(model, [quit()], msg, 'frame')];
837
+ }
838
+ if (isShellQuitConfirmDismiss(msg)) {
839
+ return [{
840
+ ...model,
841
+ quitConfirmOpen: false,
842
+ }, withObservedKey(model, [], msg, 'frame')];
843
+ }
844
+ return [model, withObservedKey(model, [], msg, 'frame')];
845
+ }
846
+ if (activeLayer.kind === 'page-modal' && modalKeyMap != null) {
847
+ const modalAction = modalKeyMap.handle(msg);
848
+ if (modalAction !== undefined) {
849
+ return [model, withObservedKey(model, [emitMsgForPage(model.activePageId, modalAction)], msg, 'page')];
850
+ }
851
+ return [model, withObservedKey(model, [], msg, 'page')];
852
+ }
853
+ if (isShellQuitRequest(msg)) {
854
+ return applyQuitRequest(model, msg);
855
+ }
856
+ const paneAction = activeInputArea?.keyMap?.handle(msg);
254
857
  const pageAction = activePage.keyMap?.handle(msg);
255
858
  const globalAction = options.globalKeys?.handle(msg);
256
859
  const frameAction = frameKeys.handle(msg);
257
860
  const keyPriority = options.keyPriority ?? 'frame-first';
258
861
  if (keyPriority === 'page-first') {
862
+ if (paneAction !== undefined) {
863
+ return [model, withObservedKey(model, [emitMsgForPage(model.activePageId, paneAction)], msg, 'page')];
864
+ }
259
865
  if (pageAction !== undefined) {
260
866
  return [model, withObservedKey(model, [emitMsgForPage(model.activePageId, pageAction)], msg, 'page')];
261
867
  }
@@ -263,6 +869,9 @@ export function createFramedApp(options) {
263
869
  return [model, withObservedKey(model, [emitMsg(globalAction)], msg, 'global')];
264
870
  }
265
871
  if (frameAction !== undefined) {
872
+ if (frameAction.type === 'open-search' && options.enableCommandPalette) {
873
+ return [openSearchPalette(model, frameKeys, options, pagesById), withObservedKey(model, [], msg, 'frame')];
874
+ }
266
875
  if (frameAction.type === 'open-palette' && options.enableCommandPalette) {
267
876
  return [openCommandPalette(model, frameKeys, options, pagesById), withObservedKey(model, [], msg, 'frame')];
268
877
  }
@@ -273,12 +882,18 @@ export function createFramedApp(options) {
273
882
  }
274
883
  if (frameAction !== undefined) {
275
884
  // Handle palette opening here since applyFrameAction doesn't have access to palette deps
885
+ if (frameAction.type === 'open-search' && options.enableCommandPalette) {
886
+ return [openSearchPalette(model, frameKeys, options, pagesById), withObservedKey(model, [], msg, 'frame')];
887
+ }
276
888
  if (frameAction.type === 'open-palette' && options.enableCommandPalette) {
277
889
  return [openCommandPalette(model, frameKeys, options, pagesById), withObservedKey(model, [], msg, 'frame')];
278
890
  }
279
891
  const [nextModel, cmds] = applyFrameAction(frameAction, model, options, pagesById);
280
892
  return [nextModel, withObservedKey(model, cmds, msg, 'frame')];
281
893
  }
894
+ if (paneAction !== undefined) {
895
+ return [model, withObservedKey(model, [emitMsgForPage(model.activePageId, paneAction)], msg, 'page')];
896
+ }
282
897
  if (globalAction !== undefined) {
283
898
  return [model, withObservedKey(model, [emitMsg(globalAction)], msg, 'global')];
284
899
  }
@@ -288,54 +903,51 @@ export function createFramedApp(options) {
288
903
  return [model, withObservedKey(model, [], msg, 'unhandled')];
289
904
  }
290
905
  if (isMouseMsg(msg)) {
291
- return [model, []];
906
+ const frameResult = handleFrameMouse(msg, model);
907
+ if (frameResult != null)
908
+ return frameResult;
909
+ return updateTargetPage(model, model.activePageId, msg);
292
910
  }
293
911
  // Custom message path: route to originating page when command messages are scoped.
294
- const scoped = isPageScopedMsg(msg) ? msg : undefined;
295
- const targetPageId = scoped?.pageId ?? model.activePageId;
296
- const targetPage = pagesById.get(targetPageId);
297
- if (targetPage == null)
298
- return [model, []];
299
- const targetMsg = scoped?.msg ?? msg;
300
- const pageModel = model.pageModels[targetPageId];
301
- const updateResult = targetPage.update(targetMsg, pageModel);
302
- let nextPageModel = pageModel; // Default to current
303
- let cmds = [];
304
- if (updateResult !== undefined && updateResult !== null) {
305
- if (Array.isArray(updateResult)) {
306
- nextPageModel = (updateResult[0] ?? pageModel);
307
- cmds = (updateResult[1] ?? []);
308
- }
309
- else {
310
- nextPageModel = updateResult;
311
- }
912
+ if (isPageScopedMsg(msg)) {
913
+ return updateTargetPage(model, msg.pageId, msg.msg);
312
914
  }
313
- const nextModels = { ...model.pageModels, [targetPageId]: nextPageModel };
314
- const synced = syncPageFrameState({ ...model, pageModels: nextModels }, targetPageId, pagesById);
315
- const wrappedCmds = Array.isArray(cmds) ? cmds.map((cmd) => wrapCmdForPage(targetPageId, cmd)) : [];
316
- return [synced, wrappedCmds];
915
+ return updateTargetPage(model, model.activePageId, msg);
317
916
  },
318
917
  view(model) {
319
- const activePage = pagesById.get(model.activePageId);
320
- const header = renderHeaderLine(model, options, pagesById);
321
- const helpLine = renderHelpLine(model, frameKeys, options, activePage);
322
- const bodyRect = frameBodyRect(model.columns, model.rows);
918
+ const { activePage, layerStack, activeLayer, } = resolvePresentedLayerContext(model);
919
+ const header = resolveHeaderLine(model, options, pagesById).surface;
920
+ const helpLine = renderHelpLine(model, activeLayer, options.i18n, resolveNotificationFooterCue(model, options, pagesById));
921
+ const bodyRect = resolveBodyRect(model, options);
323
922
  // Check for maximized pane — if set, render only that pane at full body rect
324
923
  const maxState = model.maximizedPaneByPage[model.activePageId];
325
924
  const maximizedPaneId = maxState?.maximizedPaneId;
326
- const activeResult = maximizedPaneId
327
- ? renderMaximizedPane(model.activePageId, model, bodyRect, pagesById, maximizedPaneId)
328
- : renderPageContent(model.activePageId, model, bodyRect, pagesById);
329
- let bodyOutput = activeResult.output;
925
+ const frameSurface = getComposedFrameScratch(model.columns, model.rows);
926
+ frameSurface.clear();
927
+ frameSurface.blit(header, 0, 0);
928
+ if (model.rows > 1) {
929
+ frameSurface.blit(helpLine, 0, model.rows - 1);
930
+ }
931
+ let activeResult;
932
+ let bodySurface;
330
933
  const activeTransition = model.activeTransition ?? options.transition;
331
934
  if (model.previousPageId != null && model.transitionProgress < 1 && activeTransition && activeTransition !== 'none') {
935
+ const activeBodyResult = maximizedPaneId
936
+ ? renderMaximizedPane(model.activePageId, model, bodyRect, pagesById, maximizedPaneId)
937
+ : renderPageContent(model.activePageId, model, bodyRect, pagesById);
938
+ activeResult = activeBodyResult;
939
+ bodySurface = activeBodyResult.surface;
332
940
  const ctx = resolveSafeCtx();
333
941
  if (ctx) {
334
942
  const prevResult = renderPageContent(model.previousPageId, model, bodyRect, pagesById);
335
- bodyOutput = renderTransition(prevResult.output, activeResult.output, activeTransition, model.transitionProgress, bodyRect.width, bodyRect.height, ctx, model.transitionFrame);
943
+ bodySurface = renderTransition(prevResult.surface, activeBodyResult.surface, activeTransition, model.transitionProgress, bodyRect.width, bodyRect.height, ctx, model.transitionFrame);
336
944
  }
337
945
  }
338
- bodyOutput = fitBlock(bodyOutput, bodyRect.width, bodyRect.height).join('\n');
946
+ else {
947
+ activeResult = maximizedPaneId
948
+ ? renderMaximizedPaneInto(model.activePageId, model, bodyRect, pagesById, maximizedPaneId, frameSurface)
949
+ : renderPageContentInto(model.activePageId, model, bodyRect, pagesById, frameSurface);
950
+ }
339
951
  const overlays = [];
340
952
  if (options.overlayFactory != null) {
341
953
  overlays.push(...options.overlayFactory({
@@ -355,12 +967,31 @@ export function createFramedApp(options) {
355
967
  ctx: ctx ?? undefined,
356
968
  }));
357
969
  }
970
+ if (model.settingsOpen) {
971
+ const settingsLayer = layerStack.find((layer) => layer.kind === 'settings');
972
+ const settingsOverlay = renderSettingsDrawer(model, options, pagesById, settingsLayer?.title);
973
+ if (settingsOverlay != null) {
974
+ overlays.push(settingsOverlay);
975
+ }
976
+ }
977
+ if (model.notificationCenterOpen) {
978
+ const notificationLayer = layerStack.find((layer) => layer.kind === 'notification-center');
979
+ const notificationCenterOverlay = renderNotificationCenterDrawer(model, options, pagesById, notificationLayer?.title);
980
+ if (notificationCenterOverlay != null) {
981
+ overlays.push(notificationCenterOverlay);
982
+ }
983
+ }
358
984
  if (model.helpOpen) {
359
- const full = helpView(mergeBindingSources(frameKeys, options.globalKeys, activePage.helpSource ?? activePage.keyMap));
985
+ const helpOverlay = renderHelpOverlay(model, activePage, frameKeys, paletteKeys, options, pagesById);
360
986
  overlays.push(modal({
361
- title: 'Keyboard Help',
362
- body: full.length > 0 ? full : 'No bindings',
363
- hint: 'Press ? to close',
987
+ title: activeLayer.kind === 'help'
988
+ ? (activeLayer.title ?? frameMessage(options.i18n, 'help.title', 'Keyboard Help'))
989
+ : frameMessage(options.i18n, 'help.title', 'Keyboard Help'),
990
+ body: helpOverlay.body,
991
+ hint: typeof activeLayer.hintSource === 'string'
992
+ ? activeLayer.hintSource
993
+ : frameMessage(options.i18n, 'help.hint', 'j/k scroll • d/u page • g/G top/bottom • mouse wheel • ?/Esc close'),
994
+ width: helpOverlay.body.width + 4,
364
995
  screenWidth: model.columns,
365
996
  screenHeight: model.rows,
366
997
  }));
@@ -368,25 +999,27 @@ export function createFramedApp(options) {
368
999
  if (model.commandPalette != null) {
369
1000
  const paletteWidth = Math.max(20, Math.min(80, model.columns - 4));
370
1001
  const paletteBody = commandPalette(model.commandPalette, { width: Math.max(16, paletteWidth - 4) });
1002
+ const paletteLayer = activeLayer.kind === 'search' || activeLayer.kind === 'command-palette'
1003
+ ? activeLayer
1004
+ : undefined;
371
1005
  overlays.push(modal({
372
- title: 'Command Palette',
1006
+ title: paletteLayer?.title ?? model.commandPaletteTitle ?? frameMessage(options.i18n, 'palette.title', 'Command Palette'),
373
1007
  body: paletteBody,
374
- hint: 'Enter select Esc close',
1008
+ hint: typeof paletteLayer?.hintSource === 'string'
1009
+ ? paletteLayer.hintSource
1010
+ : frameMessage(options.i18n, 'palette.hint', 'Enter select • Esc close'),
375
1011
  width: paletteWidth,
376
1012
  screenWidth: model.columns,
377
1013
  screenHeight: model.rows,
378
1014
  }));
379
1015
  }
380
- return composeFrameSurface({
381
- width: model.columns,
382
- height: model.rows,
383
- header,
384
- helpLine,
385
- bodyOutput,
386
- bodyRect,
387
- overlays,
388
- dimBackground: overlays.length > 0,
389
- });
1016
+ if (model.quitConfirmOpen) {
1017
+ overlays.push(renderShellQuitOverlay(model.columns, model.rows, options.i18n));
1018
+ }
1019
+ if (bodySurface != null && bodyRect.width > 0 && bodyRect.height > 0) {
1020
+ frameSurface.blit(bodySurface, bodyRect.col, bodyRect.row);
1021
+ }
1022
+ return compositeSurfaceInto(frameSurface, frameSurface, overlays, { dim: overlays.length > 0 });
390
1023
  },
391
1024
  routeRuntimeIssue(issue) {
392
1025
  if (!frameNotificationOptions.enabled)
@@ -396,37 +1029,525 @@ export function createFramedApp(options) {
396
1029
  };
397
1030
  return app;
398
1031
  }
399
- function composeFrameSurface(options) {
400
- const frame = createSurface(options.width, options.height);
401
- frame.blit(parseAnsiToSurface(options.header, options.width, 1), 0, 0);
402
- if (options.height > 1) {
403
- frame.blit(parseAnsiToSurface(options.helpLine, options.width, 1), 0, 1);
404
- }
405
- if (options.bodyRect.width > 0 && options.bodyRect.height > 0) {
406
- frame.blit(parseAnsiToSurface(options.bodyOutput, options.bodyRect.width, options.bodyRect.height), options.bodyRect.col, options.bodyRect.row);
1032
+ function focusPane(model, paneId) {
1033
+ if (model.focusedPaneByPage[model.activePageId] === paneId)
1034
+ return model;
1035
+ return {
1036
+ ...model,
1037
+ focusedPaneByPage: {
1038
+ ...model.focusedPaneByPage,
1039
+ [model.activePageId]: paneId,
1040
+ },
1041
+ };
1042
+ }
1043
+ function paneHitAtPosition(model, col, row, pagesById, options) {
1044
+ const bodyRect = resolveBodyRect(model, options);
1045
+ const maxState = model.maximizedPaneByPage[model.activePageId];
1046
+ const maximizedPaneId = maxState?.maximizedPaneId;
1047
+ const renderResult = maximizedPaneId
1048
+ ? renderMaximizedPane(model.activePageId, model, bodyRect, pagesById, maximizedPaneId)
1049
+ : renderPageContent(model.activePageId, model, bodyRect, pagesById);
1050
+ for (const [paneId, rect] of renderResult.paneRects.entries()) {
1051
+ if (col >= rect.col
1052
+ && col < rect.col + rect.width
1053
+ && row >= rect.row
1054
+ && row < rect.row + rect.height) {
1055
+ return { paneId, rect };
1056
+ }
407
1057
  }
408
- if (options.dimBackground) {
409
- dimSurface(frame);
1058
+ return undefined;
1059
+ }
1060
+ function resolveBodyRect(model, options) {
1061
+ return frameBodyRect(model.columns, model.rows, options.bodyTopRows ?? 1, options.bodyBottomRows ?? 1);
1062
+ }
1063
+ function renderHelpOverlay(model, activePage, frameKeys, paletteKeys, options, pagesById) {
1064
+ const activePageModel = model.pageModels[model.activePageId];
1065
+ const activeInputArea = findInputAreaByPaneId(resolveInputAreas(activePage, activePageModel), model.focusedPaneByPage[model.activePageId]);
1066
+ const modalKeyMap = activePage.modalKeyMap?.(activePageModel);
1067
+ const settings = resolveFrameSettings(model, options, pagesById);
1068
+ const notificationCenter = resolveFrameNotificationCenter(model, options, pagesById);
1069
+ const workspaceHintSource = options.helpLineSource?.({
1070
+ model,
1071
+ activePage,
1072
+ frameKeys,
1073
+ globalKeys: options.globalKeys,
1074
+ });
1075
+ const workspaceHelpSource = mergeBindingSources(frameKeys, quitHelpKeys, options.globalKeys, activeInputArea?.helpSource ?? activeInputArea?.keyMap, activePage.helpSource ?? activePage.keyMap);
1076
+ const layerStack = describeFrameLayerStack(model, {
1077
+ pageModalOpen: modalKeyMap != null,
1078
+ layers: {
1079
+ workspace: {
1080
+ title: activePage.title,
1081
+ hintSource: typeof workspaceHintSource === 'string'
1082
+ ? workspaceHintSource
1083
+ : workspaceHintSource ?? mergeBindingSources(frameKeys, options.globalKeys, activeInputArea?.helpSource ?? activeInputArea?.keyMap, activePage.helpSource ?? activePage.keyMap),
1084
+ helpSource: workspaceHelpSource,
1085
+ },
1086
+ 'page-modal': {
1087
+ title: activePage.title,
1088
+ hintSource: modalKeyMap ?? activePage.helpSource ?? activePage.keyMap,
1089
+ helpSource: mergeBindingSources(quitHelpKeys, modalKeyMap, activePage.helpSource ?? activePage.keyMap),
1090
+ },
1091
+ settings: {
1092
+ title: settings?.title ?? frameMessage(options.i18n, 'settings.title', 'Settings'),
1093
+ hintSource: frameMessage(options.i18n, 'settings.footer', 'F2/Esc close • ↑/↓ rows • Enter toggle • / search • q quit'),
1094
+ helpSource: mergeBindingSources(settingsHelpKeys, quitHelpKeys),
1095
+ },
1096
+ help: {
1097
+ title: frameMessage(options.i18n, 'help.title', 'Keyboard Help'),
1098
+ hintSource: frameMessage(options.i18n, 'help.hint', 'j/k scroll • d/u page • g/G top/bottom • mouse wheel • ?/Esc close'),
1099
+ helpSource: helpLayerHelpKeys,
1100
+ },
1101
+ 'notification-center': {
1102
+ title: notificationCenter == null
1103
+ ? frameMessage(options.i18n, 'notifications.title', 'Notifications')
1104
+ : `${notificationCenter.title} • ${frameNotificationFilterLabel(options.i18n, notificationCenter.activeFilter)}`,
1105
+ hintSource: frameMessage(options.i18n, 'notifications.footer', 'Shift+N close • f filter • j/k scroll • q quit'),
1106
+ helpSource: mergeBindingSources(notificationCenterHelpKeys, quitHelpKeys),
1107
+ },
1108
+ search: {
1109
+ title: model.commandPaletteTitle ?? activePage.searchTitle ?? frameMessage(options.i18n, 'search.title', 'Search'),
1110
+ hintSource: frameMessage(options.i18n, 'palette.hint', 'Enter select • Esc close'),
1111
+ helpSource: mergeBindingSources(paletteKeys, quitHelpKeys),
1112
+ },
1113
+ 'command-palette': {
1114
+ title: model.commandPaletteTitle ?? frameMessage(options.i18n, 'palette.title', 'Command Palette'),
1115
+ hintSource: frameMessage(options.i18n, 'palette.hint', 'Enter select • Esc close'),
1116
+ helpSource: mergeBindingSources(paletteKeys, quitHelpKeys),
1117
+ },
1118
+ 'quit-confirm': {
1119
+ title: frameMessage(options.i18n, 'quit.title', 'Quit?'),
1120
+ hintSource: frameMessage(options.i18n, 'quit.footer', 'Y quit • N stay'),
1121
+ helpSource: quitConfirmHelpKeys,
1122
+ },
1123
+ },
1124
+ });
1125
+ const beneathHelpLayer = layerStack.length > 1 ? layerStack[layerStack.length - 2] : undefined;
1126
+ const source = beneathHelpLayer?.helpSource ?? workspaceHelpSource;
1127
+ const maxDialogWidth = Math.max(28, Math.min(model.columns - 4, 88));
1128
+ const bodyWidth = Math.max(20, maxDialogWidth - 4);
1129
+ const helpSurface = helpViewSurface(source, {
1130
+ title: undefined,
1131
+ width: bodyWidth,
1132
+ });
1133
+ const pagerHeight = Math.max(4, Math.min(helpSurface.height + 1, Math.max(4, model.rows - 8)));
1134
+ const pagerState = createPagerStateForSurface(helpSurface, {
1135
+ width: bodyWidth,
1136
+ height: pagerHeight,
1137
+ });
1138
+ const scrollY = Math.max(0, Math.min(model.helpScrollY, pagerState.scroll.maxY));
1139
+ const scrolledState = {
1140
+ ...pagerState,
1141
+ scroll: {
1142
+ ...pagerState.scroll,
1143
+ y: scrollY,
1144
+ },
1145
+ };
1146
+ return {
1147
+ body: pagerSurface(helpSurface, scrolledState, { showScrollbar: true, showStatus: true }),
1148
+ maxScrollY: pagerState.scroll.maxY,
1149
+ scrollY,
1150
+ };
1151
+ }
1152
+ function isHelpScrollAction(action) {
1153
+ return action.type === 'scroll-up'
1154
+ || action.type === 'scroll-down'
1155
+ || action.type === 'page-up'
1156
+ || action.type === 'page-down'
1157
+ || action.type === 'top'
1158
+ || action.type === 'bottom';
1159
+ }
1160
+ function applyHelpScrollAction(model, activePage, action, frameKeys, paletteKeys, options, pagesById) {
1161
+ const overlay = renderHelpOverlay(model, activePage, frameKeys, paletteKeys, options, pagesById);
1162
+ const pagerState = {
1163
+ scroll: {
1164
+ y: overlay.scrollY,
1165
+ maxY: overlay.maxScrollY,
1166
+ x: 0,
1167
+ maxX: 0,
1168
+ totalLines: overlay.maxScrollY + Math.max(1, overlay.body.height - 1),
1169
+ visibleLines: Math.max(1, overlay.body.height - 1),
1170
+ },
1171
+ content: '',
1172
+ width: overlay.body.width,
1173
+ height: overlay.body.height,
1174
+ };
1175
+ let next = pagerState;
1176
+ switch (action.type) {
1177
+ case 'scroll-up':
1178
+ next = pagerScrollBy(pagerState, -1);
1179
+ break;
1180
+ case 'scroll-down':
1181
+ next = pagerScrollBy(pagerState, 1);
1182
+ break;
1183
+ case 'page-up':
1184
+ next = pagerPageUp(pagerState);
1185
+ break;
1186
+ case 'page-down':
1187
+ next = pagerPageDown(pagerState);
1188
+ break;
1189
+ case 'top':
1190
+ next = pagerScrollToTop(pagerState);
1191
+ break;
1192
+ case 'bottom':
1193
+ next = pagerScrollToBottom(pagerState);
1194
+ break;
410
1195
  }
411
- for (const overlay of options.overlays) {
412
- const overlaySurface = overlay.surface ?? parseAnsiToSurface(overlay.content, maxVisibleWidth(overlay.content), overlay.content.split('\n').length);
413
- frame.blit(overlaySurface, overlay.col, overlay.row);
1196
+ return {
1197
+ ...model,
1198
+ helpScrollY: next.scroll.y,
1199
+ };
1200
+ }
1201
+ function applyHelpScroll(model, activePage, delta, frameKeys, paletteKeys, options, pagesById) {
1202
+ const overlay = renderHelpOverlay(model, activePage, frameKeys, paletteKeys, options, pagesById);
1203
+ return {
1204
+ ...model,
1205
+ helpScrollY: Math.max(0, Math.min(overlay.maxScrollY, overlay.scrollY + delta)),
1206
+ };
1207
+ }
1208
+ function resolveFrameSettings(model, options, pagesById) {
1209
+ const activePage = pagesById.get(model.activePageId);
1210
+ return options.settings?.({
1211
+ model,
1212
+ activePage,
1213
+ pageModel: model.pageModels[model.activePageId],
1214
+ });
1215
+ }
1216
+ function resolveFrameNotificationCenter(model, options, pagesById) {
1217
+ const activePage = pagesById.get(model.activePageId);
1218
+ const pageModel = model.pageModels[model.activePageId];
1219
+ const provided = options.notificationCenter?.({
1220
+ model,
1221
+ activePage,
1222
+ pageModel,
1223
+ runtimeNotifications: model.runtimeNotifications,
1224
+ });
1225
+ if (provided != null) {
1226
+ const filters = provided.filters != null && provided.filters.length > 0
1227
+ ? provided.filters
1228
+ : DEFAULT_NOTIFICATION_CENTER_FILTERS;
1229
+ const activeFilter = filters.includes(provided.activeFilter ?? 'ALL')
1230
+ ? (provided.activeFilter ?? 'ALL')
1231
+ : filters[0];
1232
+ return {
1233
+ title: provided.title ?? frameMessage(options.i18n, 'notifications.title', 'Notifications'),
1234
+ state: provided.state,
1235
+ filters,
1236
+ activeFilter,
1237
+ onFilterChange: provided.onFilterChange,
1238
+ };
414
1239
  }
415
- return frame;
416
- }
417
- function dimSurface(surface) {
418
- for (let y = 0; y < surface.height; y++) {
419
- for (let x = 0; x < surface.width; x++) {
420
- const cell = surface.get(x, y);
421
- if (cell.empty || cell.char === ' ')
422
- continue;
423
- const modifiers = new Set(cell.modifiers ?? []);
424
- modifiers.add('dim');
425
- surface.set(x, y, { ...cell, modifiers: Array.from(modifiers) });
1240
+ if (options.runtimeNotifications === false)
1241
+ return undefined;
1242
+ return {
1243
+ title: frameMessage(options.i18n, 'notifications.title', 'Notifications'),
1244
+ state: model.runtimeNotifications,
1245
+ filters: DEFAULT_NOTIFICATION_CENTER_FILTERS,
1246
+ activeFilter: model.runtimeNotificationHistoryFilter,
1247
+ };
1248
+ }
1249
+ function resolveSettingsLayout(model, options, pagesById) {
1250
+ const settings = resolveFrameSettings(model, options, pagesById);
1251
+ if (settings == null)
1252
+ return undefined;
1253
+ const sections = settings.sections.filter((section) => section.rows.length > 0);
1254
+ if (sections.length === 0)
1255
+ return undefined;
1256
+ const drawerWidth = resolveSettingsDrawerWidth(model.columns);
1257
+ const anchor = frameStartAnchor(options.i18n);
1258
+ const startCol = anchor === 'left' ? 0 : Math.max(0, model.columns - drawerWidth);
1259
+ const contentWidth = Math.max(16, drawerWidth - 4);
1260
+ const preferenceSections = preparePreferenceSections(toPreferenceSections(sections));
1261
+ const rows = [];
1262
+ let line = 0;
1263
+ for (let sectionIndex = 0; sectionIndex < preferenceSections.length; sectionIndex++) {
1264
+ const section = preferenceSections[sectionIndex];
1265
+ if (sectionIndex > 0) {
1266
+ line += 1;
1267
+ }
1268
+ line += 1;
1269
+ line += 1;
1270
+ for (let rowIndex = 0; rowIndex < section.rows.length; rowIndex++) {
1271
+ const preparedRow = section.rows[rowIndex];
1272
+ const row = sections[sectionIndex].rows[rowIndex];
1273
+ const rowLayout = resolvePreferenceRowLayout(preparedRow, contentWidth);
1274
+ rows.push({
1275
+ index: rows.length,
1276
+ line,
1277
+ height: rowLayout.height,
1278
+ row,
1279
+ });
1280
+ line += rowLayout.height;
1281
+ if (rowIndex < section.rows.length - 1) {
1282
+ line += 1;
1283
+ }
426
1284
  }
427
1285
  }
1286
+ const contentHeight = Math.max(1, model.rows - 2);
1287
+ const totalLines = Math.max(1, line);
1288
+ const maxScrollY = Math.max(0, totalLines - contentHeight);
1289
+ return {
1290
+ settings: {
1291
+ ...settings,
1292
+ sections,
1293
+ },
1294
+ preferenceSections,
1295
+ rows,
1296
+ anchor,
1297
+ startCol,
1298
+ drawerWidth,
1299
+ contentWidth,
1300
+ contentHeight,
1301
+ totalLines,
1302
+ maxScrollY,
1303
+ };
1304
+ }
1305
+ function resolveNotificationCenterDrawerWidth(columns) {
1306
+ const boundedColumns = Math.max(28, columns);
1307
+ return Math.min(Math.max(32, Math.floor(boundedColumns * 0.34)), Math.max(32, boundedColumns - 4), 52);
1308
+ }
1309
+ function resolveNotificationCenterLayout(model, options, pagesById) {
1310
+ const center = resolveFrameNotificationCenter(model, options, pagesById);
1311
+ if (center == null)
1312
+ return undefined;
1313
+ const drawerWidth = resolveNotificationCenterDrawerWidth(model.columns);
1314
+ const anchor = frameEndAnchor(options.i18n);
1315
+ const startCol = anchor === 'left' ? 0 : Math.max(0, model.columns - drawerWidth);
1316
+ const contentWidth = Math.max(18, drawerWidth - 4);
1317
+ const content = renderNotificationCenterSurface(center, contentWidth, options.i18n);
1318
+ const contentHeight = Math.max(1, model.rows - 2);
1319
+ const pagerState = createPagerStateForSurface(content, {
1320
+ width: contentWidth,
1321
+ height: contentHeight,
1322
+ });
1323
+ return {
1324
+ center,
1325
+ anchor,
1326
+ startCol,
1327
+ drawerWidth,
1328
+ contentWidth,
1329
+ contentHeight,
1330
+ content,
1331
+ maxScrollY: pagerState.scroll.maxY,
1332
+ };
1333
+ }
1334
+ function resolveSettingsDrawerWidth(columns) {
1335
+ const boundedColumns = Math.max(24, columns);
1336
+ return Math.min(Math.max(28, Math.floor(boundedColumns * 0.3)), Math.max(28, boundedColumns - 4), 42);
1337
+ }
1338
+ function clampSettingsFocus(model, layout) {
1339
+ if (layout.rows.length === 0)
1340
+ return 0;
1341
+ return Math.max(0, Math.min(model.settingsFocusIndex, layout.rows.length - 1));
1342
+ }
1343
+ function clampSettingsScroll(model, layout) {
1344
+ return Math.max(0, Math.min(model.settingsScrollY, layout.maxScrollY));
1345
+ }
1346
+ function resolveInputAreas(page, pageModel) {
1347
+ return page.inputAreas?.(pageModel) ?? [];
1348
+ }
1349
+ function findInputAreaByPaneId(inputAreas, paneId) {
1350
+ if (paneId == null)
1351
+ return undefined;
1352
+ return inputAreas.find((area) => area.paneId === paneId);
1353
+ }
1354
+ function ensureSettingsRangeVisible(startLine, height, scrollY, visibleLines, maxScrollY) {
1355
+ let next = scrollY;
1356
+ const endLine = startLine + Math.max(1, height) - 1;
1357
+ if (startLine < next) {
1358
+ next = startLine;
1359
+ }
1360
+ else if (endLine >= next + visibleLines) {
1361
+ next = endLine - visibleLines + 1;
1362
+ }
1363
+ return Math.max(0, Math.min(next, maxScrollY));
1364
+ }
1365
+ function moveSettingsFocus(model, layout, delta) {
1366
+ if (layout.rows.length === 0)
1367
+ return model;
1368
+ const nextFocus = Math.max(0, Math.min(clampSettingsFocus(model, layout) + delta, layout.rows.length - 1));
1369
+ const focusedRow = layout.rows[nextFocus];
1370
+ return {
1371
+ ...model,
1372
+ settingsFocusIndex: nextFocus,
1373
+ settingsScrollY: ensureSettingsRangeVisible(focusedRow.line, focusedRow.height, clampSettingsScroll(model, layout), layout.contentHeight, layout.maxScrollY),
1374
+ };
1375
+ }
1376
+ function scrollSettingsBy(model, layout, delta) {
1377
+ return {
1378
+ ...model,
1379
+ settingsScrollY: Math.max(0, Math.min(clampSettingsScroll(model, layout) + delta, layout.maxScrollY)),
1380
+ };
428
1381
  }
429
- function maxVisibleWidth(text) {
430
- return text.split('\n').reduce((max, line) => Math.max(max, visibleLength(line)), 0);
1382
+ function scrollNotificationCenterBy(model, layout, delta) {
1383
+ return {
1384
+ ...model,
1385
+ notificationCenterScrollY: Math.max(0, Math.min(model.notificationCenterScrollY + delta, layout.maxScrollY)),
1386
+ };
1387
+ }
1388
+ function cycleNotificationCenterFilter(model, layout) {
1389
+ const filters = layout.center.filters;
1390
+ if (filters.length < 2)
1391
+ return [model, []];
1392
+ const currentIndex = Math.max(0, filters.indexOf(layout.center.activeFilter));
1393
+ const nextFilter = filters[(currentIndex + 1) % filters.length];
1394
+ if (layout.center.onFilterChange != null) {
1395
+ const action = layout.center.onFilterChange(nextFilter);
1396
+ return [{
1397
+ ...model,
1398
+ notificationCenterScrollY: 0,
1399
+ }, action === undefined ? [] : [emitMsgForPage(model.activePageId, action)]];
1400
+ }
1401
+ return [{
1402
+ ...model,
1403
+ runtimeNotificationHistoryFilter: nextFilter,
1404
+ notificationCenterScrollY: 0,
1405
+ }, []];
1406
+ }
1407
+ function isInsideSettingsDrawer(col, row, layout, model) {
1408
+ return col >= layout.startCol
1409
+ && col < layout.startCol + layout.drawerWidth
1410
+ && row >= 0
1411
+ && row < model.rows;
1412
+ }
1413
+ function settingsRowAtPosition(col, row, model, layout) {
1414
+ if (!isInsideSettingsDrawer(col, row, layout, model))
1415
+ return undefined;
1416
+ if (row <= 0 || row >= model.rows - 1)
1417
+ return undefined;
1418
+ const contentLine = (row - 1) + clampSettingsScroll(model, layout);
1419
+ return layout.rows.find((candidate) => contentLine >= candidate.line && contentLine < candidate.line + candidate.height);
1420
+ }
1421
+ function isInsideNotificationCenterDrawer(col, row, layout, model) {
1422
+ return col >= layout.startCol
1423
+ && col < layout.startCol + layout.drawerWidth
1424
+ && row >= 0
1425
+ && row < model.rows;
1426
+ }
1427
+ function renderSettingsDrawer(model, options, pagesById, titleOverride) {
1428
+ const layout = resolveSettingsLayout(model, options, pagesById);
1429
+ if (layout == null)
1430
+ return undefined;
1431
+ const scrollY = clampSettingsScroll(model, layout);
1432
+ const content = renderSettingsSurface(layout, model);
1433
+ const pagerState = createPagerStateForSurface(content, {
1434
+ width: layout.contentWidth,
1435
+ height: layout.contentHeight,
1436
+ });
1437
+ const scrolledState = {
1438
+ ...pagerState,
1439
+ scroll: {
1440
+ ...pagerState.scroll,
1441
+ y: scrollY,
1442
+ },
1443
+ };
1444
+ const body = pagerSurface(content, scrolledState, {
1445
+ showScrollbar: layout.maxScrollY > 0,
1446
+ showStatus: false,
1447
+ });
1448
+ return drawer({
1449
+ anchor: layout.anchor,
1450
+ title: titleOverride ?? layout.settings.title ?? frameMessage(options.i18n, 'settings.title', 'Settings'),
1451
+ content: body,
1452
+ borderToken: layout.settings.borderToken,
1453
+ bgToken: layout.settings.bgToken,
1454
+ ctx: resolveSafeCtx() ?? undefined,
1455
+ width: layout.drawerWidth,
1456
+ screenWidth: model.columns,
1457
+ screenHeight: model.rows,
1458
+ });
1459
+ }
1460
+ function renderNotificationCenterDrawer(model, options, pagesById, titleOverride) {
1461
+ const layout = resolveNotificationCenterLayout(model, options, pagesById);
1462
+ if (layout == null)
1463
+ return undefined;
1464
+ const pagerState = createPagerStateForSurface(layout.content, {
1465
+ width: layout.contentWidth,
1466
+ height: layout.contentHeight,
1467
+ });
1468
+ const scrolledState = {
1469
+ ...pagerState,
1470
+ scroll: {
1471
+ ...pagerState.scroll,
1472
+ y: Math.max(0, Math.min(model.notificationCenterScrollY, layout.maxScrollY)),
1473
+ },
1474
+ };
1475
+ const body = pagerSurface(layout.content, scrolledState, {
1476
+ showScrollbar: layout.maxScrollY > 0,
1477
+ showStatus: false,
1478
+ });
1479
+ return drawer({
1480
+ anchor: layout.anchor,
1481
+ title: titleOverride ?? `${layout.center.title} • ${frameNotificationFilterLabel(options.i18n, layout.center.activeFilter)}`,
1482
+ content: body,
1483
+ width: layout.drawerWidth,
1484
+ screenWidth: model.columns,
1485
+ screenHeight: model.rows,
1486
+ });
1487
+ }
1488
+ function renderSettingsSurface(layout, model) {
1489
+ const focusedIndex = clampSettingsFocus(model, layout);
1490
+ return preferenceListSurface(layout.preferenceSections, {
1491
+ width: layout.contentWidth,
1492
+ selectedRowId: layout.rows[focusedIndex]?.row.id,
1493
+ ctx: resolveSafeCtx() ?? undefined,
1494
+ theme: layout.settings.listTheme,
1495
+ });
1496
+ }
1497
+ function toPreferenceSections(sections) {
1498
+ return sections.map((section) => ({
1499
+ id: section.id,
1500
+ title: section.title,
1501
+ rows: section.rows.map((row) => toPreferenceRow(row)),
1502
+ }));
1503
+ }
1504
+ function toPreferenceRow(row) {
1505
+ return {
1506
+ id: row.id,
1507
+ label: row.label,
1508
+ description: row.description,
1509
+ valueLabel: row.valueLabel,
1510
+ kind: row.kind,
1511
+ checked: row.checked,
1512
+ enabled: row.enabled,
1513
+ };
1514
+ }
1515
+ function resolveNotificationFooterCue(model, options, pagesById) {
1516
+ const center = resolveFrameNotificationCenter(model, options, pagesById);
1517
+ if (center == null)
1518
+ return undefined;
1519
+ const liveCount = center.state.items.length;
1520
+ const archivedCount = countNotificationHistory(center.state, center.activeFilter);
1521
+ return frameNotificationCue(options.i18n, liveCount, archivedCount);
1522
+ }
1523
+ function renderNotificationCenterSurface(center, width, i18n) {
1524
+ const ctx = resolveSafeCtx() ?? undefined;
1525
+ const rows = [
1526
+ insetLineSurface(`Live: ${center.state.items.length} • Archived: ${center.state.history.length}`, width),
1527
+ insetLineSurface(`Filter: ${frameNotificationFilterLabel(i18n, center.activeFilter)}`, width),
1528
+ ];
1529
+ const liveItems = [...center.state.items].sort((left, right) => right.updatedAtMs - left.updatedAtMs || right.id - left.id);
1530
+ if (liveItems.length > 0) {
1531
+ rows.push(createSurface(width, 1));
1532
+ rows.push(insetLineSurface(ctx == null ? 'Current stack' : ctx.style.bold('Current stack'), width));
1533
+ rows.push(createSurface(width, 1));
1534
+ for (let index = 0; index < liveItems.length; index++) {
1535
+ rows.push(renderNotificationReviewEntrySurface(liveItems[index], {
1536
+ width,
1537
+ ctx,
1538
+ metaLabel: `${liveItems[index].variant} • live`,
1539
+ }));
1540
+ if (index < liveItems.length - 1)
1541
+ rows.push(createSurface(width, 1));
1542
+ }
1543
+ }
1544
+ rows.push(createSurface(width, 1));
1545
+ rows.push(renderNotificationHistorySurface(center.state, {
1546
+ width,
1547
+ height: Number.MAX_SAFE_INTEGER,
1548
+ filter: center.activeFilter,
1549
+ ctx,
1550
+ }));
1551
+ return vstackSurface(...rows);
431
1552
  }
432
1553
  //# sourceMappingURL=app-frame.js.map