@flyingrobots/bijou-tui 3.0.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/LICENSE +159 -21
  2. package/README.md +181 -32
  3. package/dist/app-frame-actions.d.ts +1 -5
  4. package/dist/app-frame-actions.d.ts.map +1 -1
  5. package/dist/app-frame-actions.js +23 -20
  6. package/dist/app-frame-actions.js.map +1 -1
  7. package/dist/app-frame-render.d.ts +17 -11
  8. package/dist/app-frame-render.d.ts.map +1 -1
  9. package/dist/app-frame-render.js +106 -100
  10. package/dist/app-frame-render.js.map +1 -1
  11. package/dist/app-frame-types.d.ts +10 -1
  12. package/dist/app-frame-types.d.ts.map +1 -1
  13. package/dist/app-frame-types.js.map +1 -1
  14. package/dist/app-frame.d.ts +39 -2
  15. package/dist/app-frame.d.ts.map +1 -1
  16. package/dist/app-frame.js +222 -78
  17. package/dist/app-frame.js.map +1 -1
  18. package/dist/browsable-list.d.ts +20 -1
  19. package/dist/browsable-list.d.ts.map +1 -1
  20. package/dist/browsable-list.js +38 -10
  21. package/dist/browsable-list.js.map +1 -1
  22. package/dist/command-palette.d.ts +17 -1
  23. package/dist/command-palette.d.ts.map +1 -1
  24. package/dist/command-palette.js +45 -20
  25. package/dist/command-palette.js.map +1 -1
  26. package/dist/commands.d.ts.map +1 -1
  27. package/dist/commands.js +11 -3
  28. package/dist/commands.js.map +1 -1
  29. package/dist/css/text-style.d.ts +2 -1
  30. package/dist/css/text-style.d.ts.map +1 -1
  31. package/dist/css/text-style.js +33 -0
  32. package/dist/css/text-style.js.map +1 -1
  33. package/dist/design-language.d.ts +49 -0
  34. package/dist/design-language.d.ts.map +1 -0
  35. package/dist/design-language.js +70 -0
  36. package/dist/design-language.js.map +1 -0
  37. package/dist/driver.d.ts +10 -6
  38. package/dist/driver.d.ts.map +1 -1
  39. package/dist/driver.js +18 -17
  40. package/dist/driver.js.map +1 -1
  41. package/dist/eventbus.d.ts +5 -0
  42. package/dist/eventbus.d.ts.map +1 -1
  43. package/dist/eventbus.js +44 -7
  44. package/dist/eventbus.js.map +1 -1
  45. package/dist/file-picker.d.ts +19 -1
  46. package/dist/file-picker.d.ts.map +1 -1
  47. package/dist/file-picker.js +47 -20
  48. package/dist/file-picker.js.map +1 -1
  49. package/dist/flex.d.ts +35 -1
  50. package/dist/flex.d.ts.map +1 -1
  51. package/dist/flex.js +127 -1
  52. package/dist/flex.js.map +1 -1
  53. package/dist/focus-area.d.ts +13 -1
  54. package/dist/focus-area.d.ts.map +1 -1
  55. package/dist/focus-area.js +89 -12
  56. package/dist/focus-area.js.map +1 -1
  57. package/dist/grid.d.ts +10 -0
  58. package/dist/grid.d.ts.map +1 -1
  59. package/dist/grid.js +25 -0
  60. package/dist/grid.js.map +1 -1
  61. package/dist/help.d.ts +41 -0
  62. package/dist/help.d.ts.map +1 -1
  63. package/dist/help.js +50 -0
  64. package/dist/help.js.map +1 -1
  65. package/dist/index.d.ts +19 -17
  66. package/dist/index.d.ts.map +1 -1
  67. package/dist/index.js +18 -14
  68. package/dist/index.js.map +1 -1
  69. package/dist/layout-node-surface.d.ts +9 -0
  70. package/dist/layout-node-surface.d.ts.map +1 -0
  71. package/dist/layout-node-surface.js +42 -0
  72. package/dist/layout-node-surface.js.map +1 -0
  73. package/dist/navigable-table.d.ts +16 -1
  74. package/dist/navigable-table.d.ts.map +1 -1
  75. package/dist/navigable-table.js +32 -1
  76. package/dist/navigable-table.js.map +1 -1
  77. package/dist/notification.d.ts +89 -0
  78. package/dist/notification.d.ts.map +1 -0
  79. package/dist/notification.js +821 -0
  80. package/dist/notification.js.map +1 -0
  81. package/dist/overlay.d.ts +24 -9
  82. package/dist/overlay.d.ts.map +1 -1
  83. package/dist/overlay.js +236 -138
  84. package/dist/overlay.js.map +1 -1
  85. package/dist/pager.d.ts +16 -0
  86. package/dist/pager.d.ts.map +1 -1
  87. package/dist/pager.js +61 -1
  88. package/dist/pager.js.map +1 -1
  89. package/dist/runtime.d.ts.map +1 -1
  90. package/dist/runtime.js +79 -68
  91. package/dist/runtime.js.map +1 -1
  92. package/dist/split-pane.d.ts +12 -1
  93. package/dist/split-pane.d.ts.map +1 -1
  94. package/dist/split-pane.js +31 -1
  95. package/dist/split-pane.js.map +1 -1
  96. package/dist/status-bar.d.ts +12 -0
  97. package/dist/status-bar.d.ts.map +1 -1
  98. package/dist/status-bar.js +45 -16
  99. package/dist/status-bar.js.map +1 -1
  100. package/dist/surface-layout.d.ts +19 -0
  101. package/dist/surface-layout.d.ts.map +1 -0
  102. package/dist/surface-layout.js +87 -0
  103. package/dist/surface-layout.js.map +1 -0
  104. package/dist/transition-shaders.d.ts +10 -8
  105. package/dist/transition-shaders.d.ts.map +1 -1
  106. package/dist/transition-shaders.js +65 -19
  107. package/dist/transition-shaders.js.map +1 -1
  108. package/dist/types.d.ts +32 -2
  109. package/dist/types.d.ts.map +1 -1
  110. package/dist/view-output.d.ts +4 -4
  111. package/dist/view-output.d.ts.map +1 -1
  112. package/dist/view-output.js +24 -26
  113. package/dist/view-output.js.map +1 -1
  114. package/dist/viewport.d.ts +30 -1
  115. package/dist/viewport.d.ts.map +1 -1
  116. package/dist/viewport.js +77 -1
  117. package/dist/viewport.js.map +1 -1
  118. package/package.json +4 -4
  119. package/dist/layout-v3.d.ts +0 -10
  120. package/dist/layout-v3.d.ts.map +0 -1
  121. package/dist/layout-v3.js +0 -35
  122. package/dist/layout-v3.js.map +0 -1
@@ -0,0 +1,821 @@
1
+ import { createSurface, segmentGraphemes, surfaceToString, wrapToWidth, } from '@flyingrobots/bijou';
2
+ import { visibleLength } from './viewport.js';
3
+ import { resolveNotificationGap, resolveOverlayMargin } from './design-language.js';
4
+ const ENTER_DURATION_MS = 180;
5
+ const EXIT_DURATION_MS = 320;
6
+ const HISTORY_LIMIT = 250;
7
+ const TONE_ICONS = {
8
+ INFO: '\u2139',
9
+ SUCCESS: '\u2714',
10
+ WARNING: '\u26a0',
11
+ ERROR: '\u2718',
12
+ };
13
+ const TONE_BORDER_KEYS = {
14
+ INFO: 'primary',
15
+ SUCCESS: 'success',
16
+ WARNING: 'warning',
17
+ ERROR: 'error',
18
+ };
19
+ export function createNotificationState() {
20
+ return {
21
+ items: [],
22
+ overflowExits: [],
23
+ history: [],
24
+ nextId: 1,
25
+ };
26
+ }
27
+ function matchesHistoryFilter(item, filter) {
28
+ if (filter === 'ALL')
29
+ return true;
30
+ if (filter === 'ACTIONABLE')
31
+ return item.variant === 'ACTIONABLE';
32
+ return item.tone === filter;
33
+ }
34
+ export function countNotificationHistory(state, filter = 'ALL') {
35
+ let count = 0;
36
+ for (const item of state.history) {
37
+ if (matchesHistoryFilter(item, filter))
38
+ count++;
39
+ }
40
+ return count;
41
+ }
42
+ function filterLabel(filter) {
43
+ return filter === 'ALL' ? 'All' : filter === 'ACTIONABLE' ? 'Actionable' : filter;
44
+ }
45
+ function renderHistoryEntry(item, width, ctx) {
46
+ const safeWidth = Math.max(1, width);
47
+ const toneLabel = `[${item.tone}]`;
48
+ const title = ctx == null
49
+ ? `${toneLabel} ${item.title}`
50
+ : `${ctx.style.styled(ctx.semantic(toneSemanticKey(item.tone)), toneLabel)} ${ctx.style.bold(item.title)}`;
51
+ const meta = `${formatTimeLabel(item.createdAtMs)} • ${item.variant} • ${item.placement}`;
52
+ const metaLine = ctx == null ? meta : ctx.style.styled(ctx.semantic('muted'), meta);
53
+ const actionLine = item.action == null
54
+ ? undefined
55
+ : (ctx == null
56
+ ? `Action: ${item.action.label}`
57
+ : ctx.style.styled(ctx.semantic('muted'), `Action: ${item.action.label}`));
58
+ const messageLine = item.message.length === 0
59
+ ? undefined
60
+ : (ctx == null ? item.message : ctx.style.styled(ctx.semantic('muted'), item.message));
61
+ const lines = [
62
+ ...wrapToWidth(title, safeWidth),
63
+ ...wrapToWidth(metaLine, safeWidth),
64
+ ...(messageLine == null ? [] : wrapToWidth(messageLine, safeWidth)),
65
+ ...(actionLine == null ? [] : wrapToWidth(actionLine, safeWidth)),
66
+ ];
67
+ return lines.length === 0 ? [''] : lines;
68
+ }
69
+ export function renderNotificationHistory(state, options) {
70
+ const safeWidth = Math.max(1, options.width);
71
+ const safeHeight = Math.max(3, options.height);
72
+ const filter = options.filter ?? 'ALL';
73
+ const filtered = state.history.filter((item) => matchesHistoryFilter(item, filter));
74
+ const maxBodyLines = Math.max(1, safeHeight - 2);
75
+ const start = Math.max(0, Math.min(options.scroll ?? 0, Math.max(0, filtered.length - 1)));
76
+ if (filtered.length === 0) {
77
+ const empty = wrapToWidth(options.ctx == null
78
+ ? `No archived notifications for ${filterLabel(filter)} yet.`
79
+ : options.ctx.style.styled(options.ctx.semantic('muted'), `No archived notifications for ${filterLabel(filter)} yet.`), safeWidth).slice(0, maxBodyLines);
80
+ return [
81
+ `History • ${filterLabel(filter)} • 0 items`,
82
+ '',
83
+ ...empty,
84
+ ].join('\n');
85
+ }
86
+ const bodyLines = [];
87
+ let renderedCount = 0;
88
+ for (const item of filtered.slice(start)) {
89
+ const entryLines = renderHistoryEntry(item, safeWidth, options.ctx);
90
+ const remaining = maxBodyLines - bodyLines.length;
91
+ if (remaining <= 0)
92
+ break;
93
+ if (bodyLines.length > 0) {
94
+ if (remaining <= 1)
95
+ break;
96
+ bodyLines.push('');
97
+ }
98
+ bodyLines.push(...entryLines.slice(0, maxBodyLines - bodyLines.length));
99
+ renderedCount++;
100
+ if (bodyLines.length >= maxBodyLines)
101
+ break;
102
+ }
103
+ const end = Math.min(filtered.length, start + Math.max(1, renderedCount));
104
+ return [
105
+ `History • ${filterLabel(filter)} • ${start + 1}-${end} of ${filtered.length}`,
106
+ '',
107
+ ...bodyLines,
108
+ ].join('\n');
109
+ }
110
+ function defaultDurationMs(variant) {
111
+ switch (variant) {
112
+ case 'ACTIONABLE':
113
+ return null;
114
+ case 'INLINE':
115
+ return 5_000;
116
+ case 'TOAST':
117
+ return 4_000;
118
+ }
119
+ }
120
+ function focusableIds(items) {
121
+ return items
122
+ .filter((item) => item.action != null)
123
+ .map((item) => item.id);
124
+ }
125
+ function normalizeFocusedId(items, focusedId) {
126
+ const focusable = focusableIds(items);
127
+ if (focusable.length === 0)
128
+ return undefined;
129
+ if (focusedId != null && focusable.includes(focusedId))
130
+ return focusedId;
131
+ return focusable[focusable.length - 1];
132
+ }
133
+ function archiveNotifications(history, items) {
134
+ if (items.length === 0)
135
+ return history;
136
+ const archived = [...items].sort((left, right) => right.updatedAtMs - left.updatedAtMs || right.id - left.id);
137
+ return [...archived, ...history].slice(0, HISTORY_LIMIT);
138
+ }
139
+ function advanceExitRecord(item, nowMs) {
140
+ const deltaMs = Math.max(0, nowMs - item.updatedAtMs);
141
+ const progress = Math.max(0, item.progress - (deltaMs / EXIT_DURATION_MS));
142
+ if (progress > 0) {
143
+ return {
144
+ active: {
145
+ ...item,
146
+ progress,
147
+ updatedAtMs: nowMs,
148
+ },
149
+ };
150
+ }
151
+ return {
152
+ archived: {
153
+ ...item,
154
+ progress: 0,
155
+ updatedAtMs: nowMs,
156
+ },
157
+ };
158
+ }
159
+ export function pushNotification(state, spec, nowMs) {
160
+ const variant = spec.variant ?? 'TOAST';
161
+ const next = {
162
+ id: state.nextId,
163
+ title: spec.title,
164
+ message: spec.message ?? '',
165
+ variant,
166
+ tone: spec.tone ?? 'INFO',
167
+ durationMs: spec.durationMs === undefined ? defaultDurationMs(variant) : spec.durationMs,
168
+ placement: spec.placement ?? 'LOWER_RIGHT',
169
+ action: spec.action,
170
+ bgToken: spec.bgToken,
171
+ accentToken: spec.accentToken,
172
+ overflow: spec.overflow ?? 'wrap',
173
+ createdAtMs: nowMs,
174
+ updatedAtMs: nowMs,
175
+ phase: 'entering',
176
+ progress: 0,
177
+ };
178
+ const items = [...state.items, next];
179
+ return {
180
+ ...state,
181
+ items,
182
+ nextId: state.nextId + 1,
183
+ focusedId: normalizeFocusedId(items, next.action != null ? next.id : state.focusedId),
184
+ };
185
+ }
186
+ export function dismissNotification(state, id, nowMs) {
187
+ const items = state.items.map((item) => {
188
+ if (item.id !== id || item.phase === 'exiting')
189
+ return item;
190
+ return {
191
+ ...item,
192
+ phase: 'exiting',
193
+ updatedAtMs: nowMs,
194
+ exitStartedAtMs: nowMs,
195
+ };
196
+ });
197
+ return {
198
+ ...state,
199
+ items,
200
+ focusedId: normalizeFocusedId(items, state.focusedId),
201
+ };
202
+ }
203
+ export function dismissFocusedNotification(state, nowMs) {
204
+ if (state.focusedId == null)
205
+ return state;
206
+ return dismissNotification(state, state.focusedId, nowMs);
207
+ }
208
+ export function relocateNotifications(state, placement, nowMs) {
209
+ if (state.items.every((item) => item.placement === placement))
210
+ return state;
211
+ return {
212
+ ...state,
213
+ items: state.items.map((item) => {
214
+ if (nowMs == null || item.phase === 'exiting') {
215
+ return { ...item, placement };
216
+ }
217
+ return {
218
+ ...item,
219
+ placement,
220
+ phase: 'entering',
221
+ progress: 0,
222
+ updatedAtMs: nowMs,
223
+ enteredAtMs: undefined,
224
+ exitStartedAtMs: undefined,
225
+ };
226
+ }),
227
+ };
228
+ }
229
+ export function cycleNotificationFocus(state, delta) {
230
+ const focusable = focusableIds(state.items);
231
+ if (focusable.length === 0)
232
+ return state;
233
+ const index = state.focusedId == null ? -1 : focusable.indexOf(state.focusedId);
234
+ const nextIndex = index < 0
235
+ ? (delta >= 0 ? 0 : focusable.length - 1)
236
+ : (index + delta + focusable.length) % focusable.length;
237
+ return {
238
+ ...state,
239
+ focusedId: focusable[nextIndex],
240
+ };
241
+ }
242
+ export function activateFocusedNotification(state, nowMs) {
243
+ if (state.focusedId == null)
244
+ return { state };
245
+ const target = state.items.find((item) => item.id === state.focusedId);
246
+ if (target?.action == null)
247
+ return { state };
248
+ return {
249
+ state: dismissNotification(state, target.id, nowMs),
250
+ payload: target.action.payload,
251
+ };
252
+ }
253
+ export function tickNotifications(state, nowMs) {
254
+ const nextItems = [];
255
+ const archived = [];
256
+ const nextOverflowExits = [];
257
+ const archivedOverflowExits = [];
258
+ for (const item of state.items) {
259
+ const deltaMs = Math.max(0, nowMs - item.updatedAtMs);
260
+ if (item.phase === 'entering') {
261
+ const progress = Math.min(1, item.progress + (deltaMs / ENTER_DURATION_MS));
262
+ nextItems.push(progress >= 1
263
+ ? {
264
+ ...item,
265
+ phase: 'visible',
266
+ progress: 1,
267
+ enteredAtMs: nowMs,
268
+ updatedAtMs: nowMs,
269
+ }
270
+ : {
271
+ ...item,
272
+ progress,
273
+ updatedAtMs: nowMs,
274
+ });
275
+ continue;
276
+ }
277
+ if (item.phase === 'visible') {
278
+ const visibleSince = item.enteredAtMs ?? item.createdAtMs;
279
+ if (item.durationMs != null && nowMs - visibleSince >= item.durationMs) {
280
+ nextItems.push({
281
+ ...item,
282
+ phase: 'exiting',
283
+ exitStartedAtMs: nowMs,
284
+ updatedAtMs: nowMs,
285
+ });
286
+ }
287
+ else {
288
+ nextItems.push({
289
+ ...item,
290
+ updatedAtMs: nowMs,
291
+ });
292
+ }
293
+ continue;
294
+ }
295
+ const result = advanceExitRecord(item, nowMs);
296
+ if (result.active != null) {
297
+ nextItems.push(result.active);
298
+ continue;
299
+ }
300
+ if (result.archived != null) {
301
+ archived.push(result.archived);
302
+ }
303
+ }
304
+ for (const item of state.overflowExits) {
305
+ const result = advanceExitRecord(item, nowMs);
306
+ if (result.active != null) {
307
+ nextOverflowExits.push(result.active);
308
+ continue;
309
+ }
310
+ if (result.archived != null) {
311
+ archivedOverflowExits.push(result.archived);
312
+ }
313
+ }
314
+ return {
315
+ ...state,
316
+ items: nextItems,
317
+ overflowExits: nextOverflowExits,
318
+ history: archiveNotifications(state.history, [...archived, ...archivedOverflowExits]),
319
+ focusedId: normalizeFocusedId(nextItems, state.focusedId),
320
+ };
321
+ }
322
+ export function hasNotifications(state) {
323
+ return state.items.length > 0;
324
+ }
325
+ export function notificationsNeedTick(state) {
326
+ return state.overflowExits.length > 0
327
+ || state.items.some((item) => item.phase !== 'visible' || item.durationMs != null);
328
+ }
329
+ function toneSemanticKey(tone) {
330
+ switch (tone) {
331
+ case 'INFO':
332
+ return 'info';
333
+ case 'SUCCESS':
334
+ return 'success';
335
+ case 'WARNING':
336
+ return 'warning';
337
+ case 'ERROR':
338
+ return 'error';
339
+ }
340
+ }
341
+ function defaultBgToken(ctx) {
342
+ return ctx?.theme.theme.surface.overlay;
343
+ }
344
+ function formatTimeLabel(ms) {
345
+ return new Date(ms).toLocaleTimeString('en-US', {
346
+ hour: '2-digit',
347
+ minute: '2-digit',
348
+ second: '2-digit',
349
+ });
350
+ }
351
+ function tokenToCellStyle(token) {
352
+ if (token == null)
353
+ return {};
354
+ return {
355
+ fg: token.hex,
356
+ bg: token.bg,
357
+ modifiers: token.modifiers,
358
+ };
359
+ }
360
+ function withModifiers(style, modifiers) {
361
+ const next = new Set(style.modifiers ?? []);
362
+ for (const modifier of modifiers) {
363
+ next.add(modifier);
364
+ }
365
+ return {
366
+ ...style,
367
+ modifiers: next.size === 0 ? undefined : Array.from(next),
368
+ };
369
+ }
370
+ function createSegmentSurface(segments) {
371
+ const graphemeSegments = segments.map((segment) => ({
372
+ graphemes: segmentGraphemes(segment.text ?? ''),
373
+ style: segment.style,
374
+ }));
375
+ const width = graphemeSegments.reduce((sum, segment) => sum + segment.graphemes.length, 0);
376
+ const surface = createSurface(width, 1);
377
+ let x = 0;
378
+ for (const segment of graphemeSegments) {
379
+ for (const char of segment.graphemes) {
380
+ surface.set(x, 0, {
381
+ char,
382
+ fg: segment.style?.fg,
383
+ bg: segment.style?.bg,
384
+ modifiers: segment.style?.modifiers ? [...segment.style.modifiers] : undefined,
385
+ empty: false,
386
+ });
387
+ x++;
388
+ }
389
+ }
390
+ return surface;
391
+ }
392
+ function createBlankLineSurface(width) {
393
+ return createSurface(Math.max(0, width), 1);
394
+ }
395
+ function fitLineSurface(surface, width) {
396
+ const safeWidth = Math.max(0, width);
397
+ const line = createSurface(safeWidth, 1);
398
+ if (safeWidth > 0) {
399
+ line.blit(surface, 0, 0, 0, 0, safeWidth, 1);
400
+ }
401
+ return line;
402
+ }
403
+ function wrapLineSurface(surface, width) {
404
+ const safeWidth = Math.max(1, width);
405
+ if (surface.width === 0)
406
+ return [createBlankLineSurface(safeWidth)];
407
+ const rows = [];
408
+ for (let offset = 0; offset < surface.width; offset += safeWidth) {
409
+ const row = createSurface(safeWidth, 1);
410
+ row.blit(surface, 0, 0, offset, 0, safeWidth, 1);
411
+ rows.push(row);
412
+ }
413
+ return rows;
414
+ }
415
+ function renderPlainSurface(surface) {
416
+ const lines = [];
417
+ for (let y = 0; y < surface.height; y++) {
418
+ let line = '';
419
+ for (let x = 0; x < surface.width; x++) {
420
+ line += surface.get(x, y).char;
421
+ }
422
+ lines.push(line);
423
+ }
424
+ return lines.join('\n');
425
+ }
426
+ function standaloneRows(lineSurface, width, overflow) {
427
+ if (overflow === 'truncate')
428
+ return [fitLineSurface(lineSurface, width)];
429
+ return wrapLineSurface(lineSurface, width);
430
+ }
431
+ function composeColumnRows(left, right, width, overflow) {
432
+ const safeWidth = Math.max(1, width);
433
+ const rightWidth = Math.min(right.width, safeWidth);
434
+ if (overflow === 'truncate') {
435
+ const row = createSurface(safeWidth, 1);
436
+ const leftWidth = Math.max(0, safeWidth - rightWidth);
437
+ if (leftWidth > 0) {
438
+ row.blit(left, 0, 0, 0, 0, leftWidth, 1);
439
+ }
440
+ if (rightWidth > 0) {
441
+ row.blit(right, safeWidth - rightWidth, 0, Math.max(0, right.width - rightWidth), 0, rightWidth, 1);
442
+ }
443
+ return [row];
444
+ }
445
+ const gap = rightWidth > 0 ? 1 : 0;
446
+ const leftWidth = Math.max(1, safeWidth - rightWidth - gap);
447
+ const wrappedLeft = wrapLineSurface(left, leftWidth);
448
+ return wrappedLeft.map((rowSurface, index) => {
449
+ const row = createSurface(safeWidth, 1);
450
+ row.blit(rowSurface, 0, 0);
451
+ if (index === 0 && rightWidth > 0) {
452
+ row.blit(right, safeWidth - rightWidth, 0, Math.max(0, right.width - rightWidth), 0, rightWidth, 1);
453
+ }
454
+ return row;
455
+ });
456
+ }
457
+ function resolveRegion(options) {
458
+ const screenWidth = Math.max(0, options.screenWidth);
459
+ const screenHeight = Math.max(0, options.screenHeight);
460
+ if (options.region == null) {
461
+ return { row: 0, col: 0, width: screenWidth, height: screenHeight };
462
+ }
463
+ return {
464
+ row: Math.max(0, options.region.row),
465
+ col: Math.max(0, options.region.col),
466
+ width: Math.max(0, options.region.width),
467
+ height: Math.max(0, options.region.height),
468
+ };
469
+ }
470
+ function measureTextWidth(item, screenWidth) {
471
+ const available = Math.max(18, screenWidth - 7);
472
+ const titleWidth = visibleLength(item.title);
473
+ const messageWidth = visibleLength(item.message);
474
+ const buttonWidth = item.action == null ? 0 : visibleLength(item.action.label) + 6;
475
+ const base = Math.max(titleWidth + 8, messageWidth + 2, buttonWidth + 2);
476
+ if (item.variant === 'INLINE') {
477
+ const target = Math.max(base + 8, Math.floor(screenWidth * 0.66));
478
+ return Math.min(available, Math.max(28, target));
479
+ }
480
+ return Math.min(available, Math.max(26, Math.min(52, base + 6)));
481
+ }
482
+ function renderNotificationSurface(item, options, focused) {
483
+ const ctx = options.ctx;
484
+ const textWidth = measureTextWidth(item, resolveRegion(options).width);
485
+ const mutedStyle = tokenToCellStyle(ctx?.semantic('muted'));
486
+ const titleStyle = withModifiers({}, ['bold']);
487
+ const iconStyle = tokenToCellStyle(ctx?.semantic(toneSemanticKey(item.tone)));
488
+ const accentStyle = tokenToCellStyle(item.accentToken ?? ctx?.border(TONE_BORDER_KEYS[item.tone]));
489
+ const backgroundStyle = tokenToCellStyle(item.bgToken ?? defaultBgToken(ctx));
490
+ const closeSurface = createSegmentSurface([{ text: '\u2715', style: mutedStyle }]);
491
+ const icon = TONE_ICONS[item.tone];
492
+ const overflow = item.overflow;
493
+ const rows = [];
494
+ let actionRect;
495
+ if (item.variant === 'INLINE') {
496
+ const left = createSegmentSurface([
497
+ { text: icon, style: iconStyle },
498
+ { text: ' ' },
499
+ { text: item.title, style: titleStyle },
500
+ ...(item.message.length > 0
501
+ ? [
502
+ { text: ' ' },
503
+ { text: item.message, style: mutedStyle },
504
+ ]
505
+ : []),
506
+ ]);
507
+ rows.push(...composeColumnRows(left, closeSurface, textWidth, overflow));
508
+ }
509
+ else {
510
+ const titleLeft = createSegmentSurface([
511
+ { text: icon, style: iconStyle },
512
+ { text: ' ' },
513
+ { text: item.title, style: titleStyle },
514
+ ]);
515
+ rows.push(...composeColumnRows(titleLeft, closeSurface, textWidth, overflow));
516
+ if (item.message.length > 0) {
517
+ const messageSurface = createSegmentSurface([{ text: item.message, style: mutedStyle }]);
518
+ rows.push(...standaloneRows(messageSurface, textWidth, overflow));
519
+ }
520
+ if (item.variant === 'ACTIONABLE') {
521
+ rows.push(createBlankLineSurface(textWidth));
522
+ const actionLabel = item.action == null
523
+ ? 'Dismiss'
524
+ : (focused ? `[ ${item.action.label} ]` : ` ${item.action.label} `);
525
+ const actionStyle = focused ? withModifiers({}, ['bold']) : {};
526
+ const actionRows = standaloneRows(createSegmentSurface([{ text: actionLabel, style: actionStyle }]), textWidth, overflow);
527
+ actionRect = {
528
+ row: rows.length,
529
+ col: 2,
530
+ width: textWidth,
531
+ height: actionRows.length,
532
+ };
533
+ rows.push(...actionRows);
534
+ }
535
+ if (item.variant === 'TOAST') {
536
+ rows.push(createBlankLineSurface(textWidth));
537
+ const timestampSurface = createSegmentSurface([{ text: formatTimeLabel(item.createdAtMs), style: mutedStyle }]);
538
+ rows.push(...standaloneRows(timestampSurface, textWidth, overflow));
539
+ }
540
+ }
541
+ const contentRows = rows.length === 0 ? [createBlankLineSurface(textWidth)] : rows;
542
+ const cardWidth = textWidth + 3;
543
+ const cardHeight = contentRows.length;
544
+ const card = createSurface(cardWidth, cardHeight, {
545
+ char: ' ',
546
+ fg: backgroundStyle.fg,
547
+ bg: backgroundStyle.bg,
548
+ modifiers: backgroundStyle.modifiers ? [...backgroundStyle.modifiers] : undefined,
549
+ empty: false,
550
+ });
551
+ for (let y = 0; y < contentRows.length; y++) {
552
+ card.set(0, y, {
553
+ char: '\u258e',
554
+ fg: accentStyle.fg,
555
+ bg: backgroundStyle.bg,
556
+ modifiers: accentStyle.modifiers ? [...accentStyle.modifiers] : undefined,
557
+ empty: false,
558
+ });
559
+ card.blit(contentRows[y], 2, y, 0, 0, contentRows[y].width, 1, {
560
+ char: true,
561
+ fg: true,
562
+ bg: false,
563
+ modifiers: true,
564
+ alpha: true,
565
+ });
566
+ }
567
+ return {
568
+ item,
569
+ surface: card,
570
+ dismissRect: {
571
+ row: 0,
572
+ col: Math.max(0, card.width - 2),
573
+ width: 1,
574
+ height: 1,
575
+ },
576
+ actionRect,
577
+ };
578
+ }
579
+ function sortForPlacement(items, placement) {
580
+ const ordered = [...items].sort((left, right) => right.createdAtMs - left.createdAtMs || right.id - left.id);
581
+ return placementSortSign(placement) === 'bottom' ? ordered.reverse() : ordered;
582
+ }
583
+ function placementSortSign(placement) {
584
+ switch (placement) {
585
+ case 'UPPER_LEFT':
586
+ case 'UPPER_RIGHT':
587
+ case 'TOP_CENTER':
588
+ return 'top';
589
+ case 'LOWER_LEFT':
590
+ case 'LOWER_RIGHT':
591
+ case 'BOTTOM_CENTER':
592
+ return 'bottom';
593
+ case 'CENTER':
594
+ return 'center';
595
+ }
596
+ }
597
+ function anchoredCol(placement, width, screenWidth, margin) {
598
+ switch (placement) {
599
+ case 'UPPER_LEFT':
600
+ case 'LOWER_LEFT':
601
+ return margin;
602
+ case 'UPPER_RIGHT':
603
+ case 'LOWER_RIGHT':
604
+ return Math.max(margin, screenWidth - width - margin);
605
+ case 'TOP_CENTER':
606
+ case 'BOTTOM_CENTER':
607
+ case 'CENTER':
608
+ return Math.max(0, Math.floor((screenWidth - width) / 2));
609
+ }
610
+ }
611
+ function applyAnimationOffset(placement, width, height, margin, progress) {
612
+ const remaining = 1 - progress;
613
+ const slideX = Math.round(remaining * (width + margin));
614
+ const slideY = Math.round(remaining * (height + margin));
615
+ switch (placement) {
616
+ case 'UPPER_LEFT':
617
+ case 'LOWER_LEFT':
618
+ return { rowDelta: 0, colDelta: -slideX };
619
+ case 'UPPER_RIGHT':
620
+ case 'LOWER_RIGHT':
621
+ return { rowDelta: 0, colDelta: slideX };
622
+ case 'TOP_CENTER':
623
+ return { rowDelta: -slideY, colDelta: 0 };
624
+ case 'BOTTOM_CENTER':
625
+ return { rowDelta: slideY, colDelta: 0 };
626
+ case 'CENTER':
627
+ return { rowDelta: -slideY, colDelta: 0 };
628
+ }
629
+ }
630
+ function createRenderEntry(item, options, focusedId) {
631
+ return renderNotificationSurface(item, options, focusedId === item.id);
632
+ }
633
+ function selectVisibleNotificationIds(state, options) {
634
+ const region = resolveRegion(options);
635
+ const margin = resolveOverlayMargin(region.width, region.height, options.margin);
636
+ const gap = resolveNotificationGap(options.gap);
637
+ const availableHeight = Math.max(1, region.height - (margin * 2));
638
+ const grouped = new Map();
639
+ for (const item of state.items) {
640
+ const placementItems = grouped.get(item.placement) ?? [];
641
+ placementItems.push(item);
642
+ grouped.set(item.placement, placementItems);
643
+ }
644
+ const visibleIds = new Set();
645
+ for (const items of grouped.values()) {
646
+ const newestFirst = [...items].sort((left, right) => right.createdAtMs - left.createdAtMs || right.id - left.id);
647
+ let usedHeight = 0;
648
+ let keptCount = 0;
649
+ for (const item of newestFirst) {
650
+ const entry = createRenderEntry(item, options, state.focusedId);
651
+ const required = entry.surface.height + (keptCount > 0 ? gap : 0);
652
+ if (keptCount === 0 || usedHeight + required <= availableHeight) {
653
+ visibleIds.add(item.id);
654
+ usedHeight += required;
655
+ keptCount++;
656
+ }
657
+ }
658
+ }
659
+ return visibleIds;
660
+ }
661
+ export function trimNotificationsToViewport(state, options, nowMs) {
662
+ const visibleIds = selectVisibleNotificationIds(state, options);
663
+ const keptItems = state.items.filter((item) => visibleIds.has(item.id));
664
+ if (keptItems.length === state.items.length) {
665
+ const focusedId = normalizeFocusedId(keptItems, state.focusedId);
666
+ return focusedId === state.focusedId ? state : { ...state, focusedId };
667
+ }
668
+ const evictedItems = state.items.filter((item) => !visibleIds.has(item.id));
669
+ const exitStartedAtMs = nowMs ?? evictedItems.reduce((max, item) => Math.max(max, item.updatedAtMs, item.createdAtMs), 0);
670
+ const overflowExits = [
671
+ ...state.overflowExits,
672
+ ...evictedItems.map((item) => ({
673
+ ...item,
674
+ phase: 'exiting',
675
+ progress: 1,
676
+ updatedAtMs: exitStartedAtMs,
677
+ exitStartedAtMs,
678
+ })),
679
+ ].sort((left, right) => right.updatedAtMs - left.updatedAtMs || right.id - left.id);
680
+ return {
681
+ ...state,
682
+ items: keptItems,
683
+ overflowExits,
684
+ focusedId: normalizeFocusedId(keptItems, state.focusedId),
685
+ };
686
+ }
687
+ function renderOverflowExits(exits, placement, activeTotalHeight, region, margin, gap, options, focusedId) {
688
+ if (exits.length === 0)
689
+ return [];
690
+ const rendered = [...exits]
691
+ .sort((left, right) => right.updatedAtMs - left.updatedAtMs || right.id - left.id)
692
+ .map((item) => createRenderEntry(item, options, focusedId));
693
+ const entries = [];
694
+ const mode = placementSortSign(placement);
695
+ if (mode === 'bottom') {
696
+ let cursor = Math.max(margin, region.height - activeTotalHeight - margin) - gap;
697
+ for (const entry of rendered) {
698
+ cursor -= entry.surface.height;
699
+ const baseCol = anchoredCol(placement, entry.surface.width, region.width, margin);
700
+ const offset = applyAnimationOffset(placement, entry.surface.width, entry.surface.height, margin, entry.item.progress);
701
+ entries.push({
702
+ ...entry,
703
+ row: region.row + cursor + offset.rowDelta,
704
+ col: region.col + baseCol + offset.colDelta,
705
+ });
706
+ cursor -= gap;
707
+ }
708
+ return entries;
709
+ }
710
+ let cursor = mode === 'top'
711
+ ? margin + activeTotalHeight + (activeTotalHeight > 0 ? gap : 0)
712
+ : Math.max(0, Math.floor((region.height + activeTotalHeight) / 2) + gap);
713
+ for (const entry of rendered) {
714
+ const baseCol = anchoredCol(placement, entry.surface.width, region.width, margin);
715
+ const offset = applyAnimationOffset(placement, entry.surface.width, entry.surface.height, margin, entry.item.progress);
716
+ entries.push({
717
+ ...entry,
718
+ row: region.row + cursor + offset.rowDelta,
719
+ col: region.col + baseCol + offset.colDelta,
720
+ });
721
+ cursor += entry.surface.height + gap;
722
+ }
723
+ return entries;
724
+ }
725
+ function resolveNotificationOverlayEntries(state, options) {
726
+ const screenWidth = Math.max(0, options.screenWidth);
727
+ const screenHeight = Math.max(0, options.screenHeight);
728
+ if (screenWidth <= 0 || screenHeight <= 0)
729
+ return [];
730
+ const region = resolveRegion(options);
731
+ if (region.width <= 0 || region.height <= 0)
732
+ return [];
733
+ const margin = resolveOverlayMargin(region.width, region.height, options.margin);
734
+ const gap = resolveNotificationGap(options.gap);
735
+ const visibleIds = selectVisibleNotificationIds(state, options);
736
+ const grouped = new Map();
737
+ const overflowGrouped = new Map();
738
+ for (const item of state.items) {
739
+ if (!visibleIds.has(item.id))
740
+ continue;
741
+ const placementItems = grouped.get(item.placement) ?? [];
742
+ placementItems.push(item);
743
+ grouped.set(item.placement, placementItems);
744
+ }
745
+ for (const item of state.overflowExits) {
746
+ const placementItems = overflowGrouped.get(item.placement) ?? [];
747
+ placementItems.push(item);
748
+ overflowGrouped.set(item.placement, placementItems);
749
+ }
750
+ const entries = [];
751
+ const placements = new Set([
752
+ ...grouped.keys(),
753
+ ...overflowGrouped.keys(),
754
+ ]);
755
+ for (const placement of placements) {
756
+ const items = grouped.get(placement) ?? [];
757
+ const rendered = sortForPlacement(items, placement).map((item) => createRenderEntry(item, options, state.focusedId));
758
+ const totalHeight = rendered.reduce((sum, entry) => sum + entry.surface.height, 0)
759
+ + Math.max(0, rendered.length - 1) * gap;
760
+ const mode = placementSortSign(placement);
761
+ let cursor = mode === 'top'
762
+ ? margin
763
+ : (mode === 'bottom'
764
+ ? Math.max(margin, region.height - totalHeight - margin)
765
+ : Math.max(0, Math.floor((region.height - totalHeight) / 2)));
766
+ for (const entry of rendered) {
767
+ const baseRow = cursor;
768
+ const baseCol = anchoredCol(placement, entry.surface.width, region.width, margin);
769
+ const offset = applyAnimationOffset(placement, entry.surface.width, entry.surface.height, margin, entry.item.progress);
770
+ entries.push({
771
+ ...entry,
772
+ row: region.row + baseRow + offset.rowDelta,
773
+ col: region.col + baseCol + offset.colDelta,
774
+ });
775
+ cursor += entry.surface.height + gap;
776
+ }
777
+ entries.push(...renderOverflowExits(overflowGrouped.get(placement) ?? [], placement, totalHeight, region, margin, gap, options, state.focusedId));
778
+ }
779
+ return entries;
780
+ }
781
+ function containsRect(rect, col, row) {
782
+ if (rect == null)
783
+ return false;
784
+ return col >= rect.col
785
+ && col < rect.col + rect.width
786
+ && row >= rect.row
787
+ && row < rect.row + rect.height;
788
+ }
789
+ export function hitTestNotificationStack(state, options, col, row) {
790
+ const entries = resolveNotificationOverlayEntries(state, options);
791
+ for (let index = entries.length - 1; index >= 0; index--) {
792
+ const entry = entries[index];
793
+ if (col < entry.col
794
+ || col >= entry.col + entry.surface.width
795
+ || row < entry.row
796
+ || row >= entry.row + entry.surface.height) {
797
+ continue;
798
+ }
799
+ const localCol = col - entry.col;
800
+ const localRow = row - entry.row;
801
+ if (containsRect(entry.dismissRect, localCol, localRow)) {
802
+ return { item: entry.item, kind: 'dismiss' };
803
+ }
804
+ if (containsRect(entry.actionRect, localCol, localRow)) {
805
+ return { item: entry.item, kind: 'action' };
806
+ }
807
+ return { item: entry.item, kind: 'body' };
808
+ }
809
+ return undefined;
810
+ }
811
+ export function renderNotificationStack(state, options) {
812
+ return resolveNotificationOverlayEntries(state, options).map((entry) => ({
813
+ row: entry.row,
814
+ col: entry.col,
815
+ surface: entry.surface,
816
+ content: options.ctx != null
817
+ ? surfaceToString(entry.surface, options.ctx.style)
818
+ : renderPlainSurface(entry.surface),
819
+ }));
820
+ }
821
+ //# sourceMappingURL=notification.js.map