@flyingrobots/bijou-tui 4.0.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 (95) hide show
  1. package/README.md +24 -7
  2. package/dist/app-frame-actions.d.ts +5 -5
  3. package/dist/app-frame-actions.d.ts.map +1 -1
  4. package/dist/app-frame-actions.js +71 -5
  5. package/dist/app-frame-actions.js.map +1 -1
  6. package/dist/app-frame-i18n.d.ts +12 -0
  7. package/dist/app-frame-i18n.d.ts.map +1 -0
  8. package/dist/app-frame-i18n.js +92 -0
  9. package/dist/app-frame-i18n.js.map +1 -0
  10. package/dist/app-frame-layers.d.ts +29 -0
  11. package/dist/app-frame-layers.d.ts.map +1 -0
  12. package/dist/app-frame-layers.js +104 -0
  13. package/dist/app-frame-layers.js.map +1 -0
  14. package/dist/app-frame-palette.d.ts +6 -2
  15. package/dist/app-frame-palette.d.ts.map +1 -1
  16. package/dist/app-frame-palette.js +42 -10
  17. package/dist/app-frame-palette.js.map +1 -1
  18. package/dist/app-frame-render.d.ts +13 -5
  19. package/dist/app-frame-render.d.ts.map +1 -1
  20. package/dist/app-frame-render.js +216 -70
  21. package/dist/app-frame-render.js.map +1 -1
  22. package/dist/app-frame-types.d.ts +40 -20
  23. package/dist/app-frame-types.d.ts.map +1 -1
  24. package/dist/app-frame-types.js +8 -6
  25. package/dist/app-frame-types.js.map +1 -1
  26. package/dist/app-frame-utils.d.ts +8 -3
  27. package/dist/app-frame-utils.d.ts.map +1 -1
  28. package/dist/app-frame-utils.js +42 -27
  29. package/dist/app-frame-utils.js.map +1 -1
  30. package/dist/app-frame.d.ts +125 -11
  31. package/dist/app-frame.d.ts.map +1 -1
  32. package/dist/app-frame.js +1184 -91
  33. package/dist/app-frame.js.map +1 -1
  34. package/dist/browsable-list.d.ts.map +1 -1
  35. package/dist/browsable-list.js +15 -5
  36. package/dist/browsable-list.js.map +1 -1
  37. package/dist/collection-surface.d.ts +8 -0
  38. package/dist/collection-surface.d.ts.map +1 -0
  39. package/dist/collection-surface.js +41 -0
  40. package/dist/collection-surface.js.map +1 -0
  41. package/dist/command-palette.d.ts.map +1 -1
  42. package/dist/command-palette.js +8 -3
  43. package/dist/command-palette.js.map +1 -1
  44. package/dist/commands.js +1 -1
  45. package/dist/commands.js.map +1 -1
  46. package/dist/eventbus.d.ts.map +1 -1
  47. package/dist/eventbus.js +21 -1
  48. package/dist/eventbus.js.map +1 -1
  49. package/dist/focus-area.d.ts +7 -0
  50. package/dist/focus-area.d.ts.map +1 -1
  51. package/dist/focus-area.js +33 -15
  52. package/dist/focus-area.js.map +1 -1
  53. package/dist/index.d.ts +7 -2
  54. package/dist/index.d.ts.map +1 -1
  55. package/dist/index.js +6 -2
  56. package/dist/index.js.map +1 -1
  57. package/dist/inspector-drawer.d.ts +36 -0
  58. package/dist/inspector-drawer.d.ts.map +1 -0
  59. package/dist/inspector-drawer.js +68 -0
  60. package/dist/inspector-drawer.js.map +1 -0
  61. package/dist/layout-node-surface.d.ts +8 -0
  62. package/dist/layout-node-surface.d.ts.map +1 -1
  63. package/dist/layout-node-surface.js +26 -7
  64. package/dist/layout-node-surface.js.map +1 -1
  65. package/dist/notification.d.ts +10 -1
  66. package/dist/notification.d.ts.map +1 -1
  67. package/dist/notification.js +142 -17
  68. package/dist/notification.js.map +1 -1
  69. package/dist/overlay.d.ts +8 -0
  70. package/dist/overlay.d.ts.map +1 -1
  71. package/dist/overlay.js +23 -10
  72. package/dist/overlay.js.map +1 -1
  73. package/dist/runtime-engine.d.ts +223 -0
  74. package/dist/runtime-engine.d.ts.map +1 -0
  75. package/dist/runtime-engine.js +457 -0
  76. package/dist/runtime-engine.js.map +1 -0
  77. package/dist/runtime.d.ts.map +1 -1
  78. package/dist/runtime.js +27 -7
  79. package/dist/runtime.js.map +1 -1
  80. package/dist/shell-quit.d.ts +12 -0
  81. package/dist/shell-quit.d.ts.map +1 -0
  82. package/dist/shell-quit.js +36 -0
  83. package/dist/shell-quit.js.map +1 -0
  84. package/dist/subapp/mount.d.ts.map +1 -1
  85. package/dist/subapp/mount.js +3 -0
  86. package/dist/subapp/mount.js.map +1 -1
  87. package/dist/types.d.ts +19 -5
  88. package/dist/types.d.ts.map +1 -1
  89. package/dist/types.js +11 -0
  90. package/dist/types.js.map +1 -1
  91. package/dist/view-output.d.ts +1 -0
  92. package/dist/view-output.d.ts.map +1 -1
  93. package/dist/view-output.js +25 -15
  94. package/dist/view-output.js.map +1 -1
  95. package/package.json +5 -2
package/dist/app-frame.js CHANGED
@@ -4,23 +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, resolveClock, 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 { compositeSurface, modal } from './overlay.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';
11
14
  import { commandPalette, commandPaletteKeyMap, } from './command-palette.js';
15
+ import { createPagerStateForSurface, pagerPageDown, pagerPageUp, pagerScrollBy, pagerScrollToBottom, pagerScrollToTop, pagerSurface, } from './pager.js';
12
16
  import { restoreLayoutState } from './layout-preset.js';
13
- import { createNotificationState, dismissNotification, hitTestNotificationStack, 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';
14
20
  import { isFrameScopedMsg, isPageScopedMsg, wrapCmdForPage, emitMsg, emitMsgForPage, wrapFrameMsg, } from './app-frame-types.js';
15
21
  import { createFrameKeyMap, frameBodyRect, mergeBindingSources, } from './app-frame-utils.js';
16
- import { resolveHeaderLine, renderHelpLine, renderPageContent, renderMaximizedPane, renderTransition, } from './app-frame-render.js';
17
- import { applyFrameAction, switchTab, syncPageFrameState, } from './app-frame-actions.js';
18
- import { handlePaletteKey, openCommandPalette, } from './app-frame-palette.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';
19
28
  // ---------------------------------------------------------------------------
20
29
  // Frame Notification Helpers
21
30
  // ---------------------------------------------------------------------------
22
31
  const FRAME_NOTIFICATION_TICK_MS = 40;
23
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' }));
24
100
  function resolveFrameNotificationOptions(options) {
25
101
  if (options.runtimeNotifications === false) {
26
102
  return {
@@ -77,8 +153,13 @@ export function createFramedApp(options) {
77
153
  if (!pagesById.has(defaultPageId)) {
78
154
  throw new Error(`createFramedApp: defaultPageId "${defaultPageId}" not found in pages`);
79
155
  }
80
- 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
+ });
81
161
  const frameNotificationOptions = resolveFrameNotificationOptions(options);
162
+ let composedFrameScratch = null;
82
163
  const paletteKeys = commandPaletteKeyMap({
83
164
  focusNext: { type: 'cp-next' },
84
165
  focusPrev: { type: 'cp-prev' },
@@ -87,69 +168,298 @@ export function createFramedApp(options) {
87
168
  select: { type: 'cp-select' },
88
169
  close: { type: 'cp-close' },
89
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
+ }
90
179
  function withObservedKey(model, cmds, msg, route) {
91
180
  const observed = options.observeKey?.(msg, route);
92
181
  if (observed === undefined)
93
182
  return [...cmds];
94
183
  return [emitMsgForPage(model.activePageId, observed), ...cmds];
95
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
+ }
96
328
  function updateTargetPage(model, targetPageId, targetMsg) {
97
329
  const targetPage = pagesById.get(targetPageId);
98
330
  if (targetPage == null)
99
331
  return [model, []];
100
332
  const pageModel = model.pageModels[targetPageId];
101
333
  const updateResult = targetPage.update(targetMsg, pageModel);
102
- let nextPageModel = pageModel;
103
- let cmds = [];
104
- if (updateResult !== undefined && updateResult !== null) {
105
- if (Array.isArray(updateResult)) {
106
- nextPageModel = (updateResult[0] ?? pageModel);
107
- cmds = (updateResult[1] ?? []);
108
- }
109
- else {
110
- nextPageModel = updateResult;
111
- }
112
- }
334
+ const [nextPageModel, cmds = []] = updateResult;
113
335
  const nextModels = { ...model.pageModels, [targetPageId]: nextPageModel };
114
336
  const synced = syncPageFrameState({ ...model, pageModels: nextModels }, targetPageId, pagesById);
115
- const wrappedCmds = Array.isArray(cmds)
116
- ? cmds.map((cmd) => wrapCmdForPage(targetPageId, cmd))
117
- : [];
337
+ const wrappedCmds = cmds.map((cmd) => wrapCmdForPage(targetPageId, cmd));
118
338
  return [synced, wrappedCmds];
119
339
  }
120
340
  function handleFrameMouse(msg, model) {
121
- if (model.helpOpen || model.commandPalette != null)
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') {
122
349
  return [model, []];
123
- if (msg.action !== 'press' || msg.button !== 'left')
124
- return undefined;
125
- if (frameNotificationOptions.enabled) {
126
- const nowMs = resolveClock(resolveSafeCtx()).now();
127
- const notificationTarget = hitTestNotificationStack(model.runtimeNotifications, {
128
- screenWidth: model.columns,
129
- screenHeight: model.rows,
130
- margin: frameNotificationOptions.margin,
131
- gap: frameNotificationOptions.gap,
132
- ctx: resolveSafeCtx() ?? undefined,
133
- }, msg.col, msg.row);
134
- if (notificationTarget?.kind === 'dismiss') {
135
- return applyFrameNotificationState(model, dismissNotification(model.runtimeNotifications, notificationTarget.item.id, nowMs), nowMs);
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, []];
136
380
  }
137
- if (notificationTarget != null) {
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
+ }
138
397
  return [model, []];
139
398
  }
140
399
  }
141
- if (msg.row === 0) {
142
- const header = resolveHeaderLine(model, options, pagesById);
143
- const tab = header.tabTargets.find((target) => msg.col >= target.startCol && msg.col <= target.endCol);
144
- if (tab != null) {
145
- const currentIndex = model.pageOrder.indexOf(model.activePageId);
146
- const nextIndex = model.pageOrder.indexOf(tab.pageId);
147
- if (currentIndex >= 0 && nextIndex >= 0 && nextIndex !== currentIndex) {
148
- return switchTab(model, nextIndex - currentIndex, pagesById, options);
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, []];
149
427
  }
150
428
  return [model, []];
151
429
  }
152
- return [model, []];
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
+ }
153
463
  }
154
464
  return undefined;
155
465
  }
@@ -171,6 +481,32 @@ export function createFramedApp(options) {
171
481
  }
172
482
  return [nextModel, []];
173
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
+ }
174
510
  const app = {
175
511
  init() {
176
512
  const pageModels = {};
@@ -189,6 +525,14 @@ export function createFramedApp(options) {
189
525
  columns: Math.max(1, options.initialColumns ?? 80),
190
526
  rows: Math.max(1, options.initialRows ?? 24),
191
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,
192
536
  transitionProgress: 1,
193
537
  transitionGeneration: 0,
194
538
  transitionFrame: 0,
@@ -197,6 +541,7 @@ export function createFramedApp(options) {
197
541
  dockStateByPage: {},
198
542
  splitRatioOverrides: {},
199
543
  runtimeNotifications: createNotificationState(),
544
+ runtimeNotificationHistoryFilter: 'ALL',
200
545
  runtimeNotificationLoopActive: false,
201
546
  };
202
547
  for (const pageId of pageOrder) {
@@ -297,32 +642,226 @@ export function createFramedApp(options) {
297
642
  }, []];
298
643
  }
299
644
  if (isKeyMsg(msg)) {
300
- 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
+ }
301
670
  const [nextModel, cmds] = handlePaletteKey(msg, model, paletteKeys, options, pagesById);
302
671
  return [nextModel, withObservedKey(model, cmds, msg, 'palette')];
303
672
  }
304
- // Help acts as a modal layer when open: only close keys are handled.
305
- if (model.helpOpen) {
306
- if (!msg.ctrl && !msg.alt && (msg.key === 'escape' || msg.key === '?')) {
307
- 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
+ ];
308
686
  }
309
687
  return [model, withObservedKey(model, [], msg, 'help')];
310
688
  }
311
- const activePage = pagesById.get(model.activePageId);
312
- const activePageModel = model.pageModels[model.activePageId];
313
- const modalKeyMap = activePage.modalKeyMap?.(activePageModel);
314
- if (modalKeyMap != null) {
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) {
315
847
  const modalAction = modalKeyMap.handle(msg);
316
848
  if (modalAction !== undefined) {
317
849
  return [model, withObservedKey(model, [emitMsgForPage(model.activePageId, modalAction)], msg, 'page')];
318
850
  }
319
851
  return [model, withObservedKey(model, [], msg, 'page')];
320
852
  }
853
+ if (isShellQuitRequest(msg)) {
854
+ return applyQuitRequest(model, msg);
855
+ }
856
+ const paneAction = activeInputArea?.keyMap?.handle(msg);
321
857
  const pageAction = activePage.keyMap?.handle(msg);
322
858
  const globalAction = options.globalKeys?.handle(msg);
323
859
  const frameAction = frameKeys.handle(msg);
324
860
  const keyPriority = options.keyPriority ?? 'frame-first';
325
861
  if (keyPriority === 'page-first') {
862
+ if (paneAction !== undefined) {
863
+ return [model, withObservedKey(model, [emitMsgForPage(model.activePageId, paneAction)], msg, 'page')];
864
+ }
326
865
  if (pageAction !== undefined) {
327
866
  return [model, withObservedKey(model, [emitMsgForPage(model.activePageId, pageAction)], msg, 'page')];
328
867
  }
@@ -330,6 +869,9 @@ export function createFramedApp(options) {
330
869
  return [model, withObservedKey(model, [emitMsg(globalAction)], msg, 'global')];
331
870
  }
332
871
  if (frameAction !== undefined) {
872
+ if (frameAction.type === 'open-search' && options.enableCommandPalette) {
873
+ return [openSearchPalette(model, frameKeys, options, pagesById), withObservedKey(model, [], msg, 'frame')];
874
+ }
333
875
  if (frameAction.type === 'open-palette' && options.enableCommandPalette) {
334
876
  return [openCommandPalette(model, frameKeys, options, pagesById), withObservedKey(model, [], msg, 'frame')];
335
877
  }
@@ -340,12 +882,18 @@ export function createFramedApp(options) {
340
882
  }
341
883
  if (frameAction !== undefined) {
342
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
+ }
343
888
  if (frameAction.type === 'open-palette' && options.enableCommandPalette) {
344
889
  return [openCommandPalette(model, frameKeys, options, pagesById), withObservedKey(model, [], msg, 'frame')];
345
890
  }
346
891
  const [nextModel, cmds] = applyFrameAction(frameAction, model, options, pagesById);
347
892
  return [nextModel, withObservedKey(model, cmds, msg, 'frame')];
348
893
  }
894
+ if (paneAction !== undefined) {
895
+ return [model, withObservedKey(model, [emitMsgForPage(model.activePageId, paneAction)], msg, 'page')];
896
+ }
349
897
  if (globalAction !== undefined) {
350
898
  return [model, withObservedKey(model, [emitMsg(globalAction)], msg, 'global')];
351
899
  }
@@ -361,31 +909,45 @@ export function createFramedApp(options) {
361
909
  return updateTargetPage(model, model.activePageId, msg);
362
910
  }
363
911
  // Custom message path: route to originating page when command messages are scoped.
364
- const scoped = isPageScopedMsg(msg) ? msg : undefined;
365
- const targetPageId = scoped?.pageId ?? model.activePageId;
366
- const targetMsg = scoped?.msg ?? msg;
367
- return updateTargetPage(model, targetPageId, targetMsg);
912
+ if (isPageScopedMsg(msg)) {
913
+ return updateTargetPage(model, msg.pageId, msg.msg);
914
+ }
915
+ return updateTargetPage(model, model.activePageId, msg);
368
916
  },
369
917
  view(model) {
370
- const activePage = pagesById.get(model.activePageId);
918
+ const { activePage, layerStack, activeLayer, } = resolvePresentedLayerContext(model);
371
919
  const header = resolveHeaderLine(model, options, pagesById).surface;
372
- const helpLine = renderHelpLine(model, frameKeys, options, activePage);
373
- const bodyRect = frameBodyRect(model.columns, model.rows);
920
+ const helpLine = renderHelpLine(model, activeLayer, options.i18n, resolveNotificationFooterCue(model, options, pagesById));
921
+ const bodyRect = resolveBodyRect(model, options);
374
922
  // Check for maximized pane — if set, render only that pane at full body rect
375
923
  const maxState = model.maximizedPaneByPage[model.activePageId];
376
924
  const maximizedPaneId = maxState?.maximizedPaneId;
377
- const activeResult = maximizedPaneId
378
- ? renderMaximizedPane(model.activePageId, model, bodyRect, pagesById, maximizedPaneId)
379
- : renderPageContent(model.activePageId, model, bodyRect, pagesById);
380
- let bodySurface = activeResult.surface;
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;
381
933
  const activeTransition = model.activeTransition ?? options.transition;
382
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;
383
940
  const ctx = resolveSafeCtx();
384
941
  if (ctx) {
385
942
  const prevResult = renderPageContent(model.previousPageId, model, bodyRect, pagesById);
386
- bodySurface = renderTransition(prevResult.surface, activeResult.surface, 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);
387
944
  }
388
945
  }
946
+ else {
947
+ activeResult = maximizedPaneId
948
+ ? renderMaximizedPaneInto(model.activePageId, model, bodyRect, pagesById, maximizedPaneId, frameSurface)
949
+ : renderPageContentInto(model.activePageId, model, bodyRect, pagesById, frameSurface);
950
+ }
389
951
  const overlays = [];
390
952
  if (options.overlayFactory != null) {
391
953
  overlays.push(...options.overlayFactory({
@@ -405,12 +967,31 @@ export function createFramedApp(options) {
405
967
  ctx: ctx ?? undefined,
406
968
  }));
407
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
+ }
408
984
  if (model.helpOpen) {
409
- const full = helpView(mergeBindingSources(frameKeys, options.globalKeys, activePage.helpSource ?? activePage.keyMap));
985
+ const helpOverlay = renderHelpOverlay(model, activePage, frameKeys, paletteKeys, options, pagesById);
410
986
  overlays.push(modal({
411
- title: 'Keyboard Help',
412
- body: full.length > 0 ? full : 'No bindings',
413
- 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,
414
995
  screenWidth: model.columns,
415
996
  screenHeight: model.rows,
416
997
  }));
@@ -418,25 +999,27 @@ export function createFramedApp(options) {
418
999
  if (model.commandPalette != null) {
419
1000
  const paletteWidth = Math.max(20, Math.min(80, model.columns - 4));
420
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;
421
1005
  overlays.push(modal({
422
- title: 'Command Palette',
1006
+ title: paletteLayer?.title ?? model.commandPaletteTitle ?? frameMessage(options.i18n, 'palette.title', 'Command Palette'),
423
1007
  body: paletteBody,
424
- hint: 'Enter select Esc close',
1008
+ hint: typeof paletteLayer?.hintSource === 'string'
1009
+ ? paletteLayer.hintSource
1010
+ : frameMessage(options.i18n, 'palette.hint', 'Enter select • Esc close'),
425
1011
  width: paletteWidth,
426
1012
  screenWidth: model.columns,
427
1013
  screenHeight: model.rows,
428
1014
  }));
429
1015
  }
430
- return composeFrameSurface({
431
- width: model.columns,
432
- height: model.rows,
433
- headerSurface: header,
434
- helpLineSurface: helpLine,
435
- bodySurface,
436
- bodyRect,
437
- overlays,
438
- dimBackground: overlays.length > 0,
439
- });
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 });
440
1023
  },
441
1024
  routeRuntimeIssue(issue) {
442
1025
  if (!frameNotificationOptions.enabled)
@@ -446,15 +1029,525 @@ export function createFramedApp(options) {
446
1029
  };
447
1030
  return app;
448
1031
  }
449
- function composeFrameSurface(options) {
450
- const frame = createSurface(options.width, options.height);
451
- frame.blit(options.headerSurface, 0, 0);
452
- if (options.height > 1) {
453
- frame.blit(options.helpLineSurface, 0, 1);
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
+ }
454
1057
  }
455
- if (options.bodyRect.width > 0 && options.bodyRect.height > 0) {
456
- frame.blit(options.bodySurface, options.bodyRect.col, options.bodyRect.row);
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;
1195
+ }
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
+ };
1239
+ }
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
+ }
1284
+ }
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
+ };
1381
+ }
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
+ }
457
1543
  }
458
- return compositeSurface(frame, options.overlays, { dim: options.dimBackground });
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);
459
1552
  }
460
1553
  //# sourceMappingURL=app-frame.js.map